标签:定义类 运行 系统 停止 strong 分享 语句 double html
全局作用域是最大的名字空间作用域,不同于用户自定义的名字空间作用域,全局作用域不需要显示地定义,它天然存在于C++程序中。全局作用域是一个最外层的容器,是所有作用域的父作用域。在全局作用域中,可以定义其他的名字空间,类型,函数,变量,模版等。
在全局作用域中定义的函数是全局函数,在全局作用域中定义的变量是全局对象。全局函数和全局对象在整个全局作用域及其子作用域中有效,它们的生命周期贯穿于整个程序的运行。从定义它们开始直到整个程序运行结束。
变量可以被声明多次,但只能被定义一次。声明和定义是两个不同的概念。
在变量定义的时候,除了向程序表明变量的类型和名称外,还要为变量分配存储空间以及进行初始化操作。在定义全局变量的时候,如果没有为变量赋初值,那么系统将执行默认的初始化工作,如:整型变量赋值0,字符串变量赋空值等。全局变量的值存储在程序的全局存储区,在全局存储区中还存储着常量的值,以及静态变量的值。
声明操作是定义操作的子集,在声明变量的时候,仅仅向程序表明变量的类型和名称,它不会为变量分配存储空间,因此也不会有初始化工作。
全局变量和全局函数的声明和定义的格式如下:
//声明 Void myFunction(int);//声明一个函数,指定该函数的名称,返回值,以及参数列表。 Extern void myFunction(int);//在声明函数的时候,extern关键字可选,加上该关键字表示显//示声明。 Extern in myVar; //声明一个变量。必须加关键字extern。表明此处仅仅是声明,而定义在其//他地方 Extern const char* const myConstVar;//声明一个指向常量的常量指针。 //定义 Void myFunction(int Para)//函数定义,包括函数的声明部分和函数体 { //函数的功能代码 } Int myVar = 10;//变量定义 const char* const myConstVar = new char[10];//常量定义。
Extern int myIntVar = 50;//变量的定义。 |
在声明全局变量的时候,必须加关键字extern,并且不能对变量进行初始化。如果对变量执行了初始化操作,即使在前面加了extern关键字,也会被认为是变量的定义,而不是变量的声明。
声明和定义的关系是:定义也是声明,定义包含了声明,但声明仅仅是定义的一个子步骤。有的时候,声明和定义可以合在一起进行,如局部变量的声明和定义,而在某种情况下,声明和定义是需要分开进行的。当一个变量需要在多个文件中被使用的时候,就需要将声明和定义分开(一般为全局变量);否则使用一个定义语句同时完成变量的声明和定义即可(一般为局部变量)。
一个变量可以被声明多次,但它只能被定义一次。在这里存在一个法则,一次定义法则:全局对象和全局函数或者只有一次定义,或者在一个程序中有多个完全相同的定义(内两函数和全局常量的情况)。
为了遵守一次定义法则,需要合理的文件组织形式。在一般情况下,全局作用域中定义的全局变量和全局函数都需要在多个文件中被使用。在这种情况下,最合理的文件组织形式就是:“头文件+cpp文件”的形式。具体的过程描述如下:
头文件是用来声明而不是定义函数和变量的,而源文件则是用来实现变量和函数定义的。头文件中一般包含类的定义,枚举的定义,符号常量的定义,内联函数的定义,extern变量的声明,函数的声明,typedef声明。如果将变量定义到头文件中,那么当其他文件引入该头文件的时候,就会认为该对头文件中的变量又执行了一次定义。根据一次定义法则,这是不允许的。但是也有三个例外,类,内联函数,在编译时值可以确定的符号常量是可以在头文件中定义的。
按照头文件的方式处理全局变量和全局函数的具体代码的格式如下:
//头文件中实现函数和变量的声明 --------------------A.h---------------------------------- Extern int myIntVar;//声明一个变量,必须以关键字extern开头。 Void myFunction(int);//声明一个函数,指定函数名称,返回值,参数列表。 Const double mydlVar = 3.14;//定义一个常量。该常量的值在编译时可知。 Inline double GetdlVar()//定义内联函数 { Return mydlVar; } Extern const char* const myConstVar;//声明全局常量,该常量的值在编译时不可知。
//源文件中实现变量和函数的定义。 ---------------------------A.cpp----------------------------------- #include “A.h” Int myIntVar = 100;//定义变量 Void myFunction(int Para) { //函数的实现代码 } const char* const myConstVar = new char[100];//常量的值在运行时才能确定,因此在这里实现它的定义,而在头文件中仅仅是声明。 //在其他文件中使用定义的全局变量和全局函数,首先引入该头文件。 ----------------------------other.cpp-------------------------------- #include “A.h” --以下是具体使用。 |
在C++中,编译一个程序可以划分为如下的阶段:
如果在一个源文件中使用了“#Include”命令,将一个头文件引入到该源文件中。那么在编译器执行编译之前,会首先执行该预处理命令。即:将头文件中所包含的代码全部合并的目标源文件中,合并后的文件将作为一个文件存在。也就是说,编译器在执行编译的时候,只会看到源文件,头文件在编译阶段是不可见的。
那么,假设有如下的实现方式:在头文件A.h中,实现了一个变量的定义(不是声明),如:“int a = 10;”。每当该头文件被其他源文件引用一次以后(#include “A.h”),那么编译器就会认为对该变量执行了一次定义。如果该头文件被引用两次以上,就会发生重定义错误。这是违反一次定义法则的。
基于以上原因,对于普通的全局变量,正确的使用方法是:首先在头文件中使用extern关键字实现该全局变量的声明;然后在源文件中引入该头文件,并且实现该全局变量的定义;最后,在使用该全局变量的源文件中引入声明了该全局变量的头文件,然后使用之。
对于普通的全局变量,只能使用此方法处理。
在全局作用域中定义的常量只具有文件作用域。也就是说,在全局作用域中定义的常量只在定义它的文件内有效,它不会影响到其他文件中定义的同名常量。举例如下:
--------------------------A.h---------------------------- Const int myIntVar = 100;//在头文件中定义了一个常量 ------------------------B.cpp---------------------------- #include “A.h” Int a = myIntVar; ------------------------C.cpp---------------------------- #include “A.h” Int b = myIntVar;
|
示例1
上面的代码能够正确运行。虽然“A.h”头文件被引用了多次,但是由于全局常量只具有文件作用域,所以在B.cpp文件中定义的全局常量“myIntVar”与在C.cpp文件中定义的全局常量“myIntVar”互不影响。
这是使用全局常量的一种方式,只要该全局常量的值在编译时刻是确定的。在上面的示例中,全局常量“myIntVar”不会被正真地存储到全局区。在编译阶段,编译器会用该全局常量的值去替换使用“myIntVar”名称的地方。因此,要求在使用全局常量的地方,该全局常量的定义是可见的。示例1中的方式能够满足这种要求。当对符号常量使用extern关键字,或者取符号常量的地址的时候,编译器会为符号常量分配存储空间,否则只是执行编译时刻的替换。
现在考虑如下问题,如果全局常量的值在编译时刻不确定呢?比如:使用new操作符定义一个全局常量,那么情况会如何?举例如下:
-------------------A.h--------------------- Char* const pChar = new char[100];//定义一个常量指针。注意与:const char* pChar = new char[100]的区别。 -------------------B.cpp------------------ #include “A.h”
------------------C.cpp-------------------- #include “A.h” |
示例2
每当在源文件中引用一次该头文件,那么就会执行一次内存分配。这显然与我们期望的结果不符。我们想要的是一个常量指针,该指针指向一个字符串。然后,我们可以在其他多个文件中使用该常量指针。
可以使用关键字extern打破const常量的文件作用域,使之具有全局作用域。具体的作法举例如下:
-------------------------A.h--------------------- Extern char* const pChar;//声明全局常量,该头文件可以被其他文件引用 ------------------------A.cpp-------------------- #include “A.h” Char* const pChar = new char[100];//定义常量,编译器分配存储空间 Memset(pChar,’\0’,100); -------------------------B.cpp------------------- #include “A.h”//引入头文件,可以在此使用声明的全局常量 …. |
示例3
该作法是:使用关键字extern在头文件中声明全局常量。使用extern关键字后,该全局常量具有全局作用域,而不在局限于文件作用域。然后在源文件中实现该全局常量的定义。最后,在使用该全局常量的源文件中引用声明了该全局常量的头文件即可。这是使用全局常量的第二种方式,当全局常量的值在编译时刻不确定的时候,将采用这种方式。
由此我们可以看出,全局常量有两种使用方式。当全局常量的值在编译阶段是可以确定的情况下,我们可以使用第一种方式,如:示例1;当全局常量的值在编译阶段是不确定的,但在运行阶段是可以确定的情况下,我们可以使用第二种方式,如:示例3。
在全局作用域中定义的静态变量也具有文件作用域。该静态变量只在定义它的文件中有效。在多个文件中定义的同名静态变量互不影响,不会出现重定义错误。并且,这些同名静态变量各自保持一份独立的数据拷贝,一个文件中的静态变量值发生变化的时候,不会影响到另外一个文件中的同名静态变量的值。
静态变量的文件作用域是不可打破的,不能使用关键字extern使在全局作用域中定义的静态变量具有全局作用域。
因此,全局静态变量只有一种使用方式,类似于使用全局常量的第一种方式。举例如下:
-----------------------A.h------------------------- Static in myIntVar = 100; ----------------------B.h------------------------- Int myFunction1(); --------------------C.h---------------------------- Int myFunction2(); ----------------------B.cpp------------------------- #include “A.h” //引用定义静态变量的头文件,相当于在该源文件中定义了一次静态变量 #cindlue “B.h” Void myFunction1() { Int a = myIntVar; myIntVar++; return a; } ----------------------------c.cpp--------------------------- #include “A.h”//引用定义静态变量的头文件,相当于在该源文件中定义了一次静态变量 #include “C.h” Int myFunction2() { Int b = myIntVar; myIntVar++; return b; } -----------------------------.main.cpp----------------------------- #include “B.h” #include “c.h” Void main() { Int a = myFunction1(); Int b = myFunction2(); //在执行完毕后,a,b的值均为100。说明在文件作用域中的静态变量各自保持一份独立的数据拷贝,互相不影响。 } |
基于以上原因,全局静态变量定义在源文件中即可,哪里需要,哪里定义。不需要事先定义到头文件中。
注意全局常量与全局静态变量的区别:默认情况下,全局常量和全局静态变量都具有文件作用域。但是,可以适应关键字extern,打破全局常量的文件作用域,使其在其他文件中也具有可访问性;不能使用关键字extern打破全局静态变量的文件作用域。
在编译阶段,编译器会将内联函数在调用点展开,将函数体中的代码合并到调用点。而不是在运行阶段执行压栈,出栈方式的函数调用。在编译器展开内联函数的时候,在当前文件中,内联函数的定义必须是可见的。因此,内联函数必须定义在头文件中,当其他的源文件引入了该头文件后,就相当于该内联函数的定义在该源文件中是可见的。根据一次定义法则,内联函数可以多次定义,只要保证多次定义的形式是相同的即可。
使用头文件和源文件结合的方式完成一个类的定义。类的定义,内联函数的定义,静态成员变量的声明实现在头文件中;类成员函数的定义,静态成员变量的定义实现在源文件中。在实现源文件的时候,必须引入定义该类的头文件。具体的示例代码如下:
---------------------------A.h------------------------------------ Class myClass { Typedef int sb4;//typedef定义,引入int类型的助记符。 Public: myClass(sb4 Para);//声明构造函数 //内联函数的声明在当前位置解析,内联函数的//定义在整个类域解析 inline void setValue(sb4 Para)//内联函数的声明部分。sb4可以直接被使用,因为在它之前已经声明了sb4。 { M_IVar = Para;//内联函数的定义部分。在整个类域解析,所以可以直接使用成员变量m_IVar。 } Static sb4 getValue();//声明静态成员函数 Private: Sb4 m_IVar;//定义成员变量 Static sb4 m_StaticVar;//声明静态变量 };//在整个花括号范围内,都属于类域 ----------------------------A.cpp--------------------------------- #include “A.h”//必须引入头文件 myClass::sb4 myClass::m_StaticVar = getValue();//静态成员变量的定义,并初始化。红色部分属于类域,成员函数getValue()可以被直接调用;绿色部分不属于类域,必须使用修饰限定名称引用。类型sb4必须使用修饰限定名称引用。 myClass::myClass(sb4 Para) { M_IVar = Para;//成员变量的初始化 }//红色部分属于类域,类型sb4可以直接使用,成员变量m_IVar可以直接使用。 myClass::sb4 myClass::getValue()//静态成员函数的定义。绿色部分不属于类域,必须使用修饰限定名称引用。类型sb4必须使用修饰限定名称引用。 { Return m_StaticVar; } |
在上面的代码中,将类定义分成了两大部分,分别在头文件和源文件中实现。头文件是对外的接口,源文件中封装具体实现。
每定义一个类就会引入一个类域,不同的类具有不同的类域。每一个类成员,包括成员变量和成员函数都属于该类域。在类域内部,可以直接使用成员名称访问类成员;在类域外部,必须通过成员访问操作符或域解析操作符访问类成员。类域由如下三部分组成:
在源文件中,成员函数修饰限定名称或者静态变量修饰限定名称之前的部分不属于类域。如果要在这部分引用类域中的名称,必须使用修饰限定名。见2.4.1节的绿色部分。
在源文件中,属于类域中的成员名称可以被直接使用,不需要修饰限定。见2.4.1节红色部分。
当需要在某个程序文本文件中使用某个类的时候,需要在该文件中引入实现该类定义的头文件。在使用类成员的时候,有两种形式,分别是:在类域外部使用类的某个成员和在类域内部使用类的某个成员。
情况1:在类域外部使用某个类的成员的时候,必须使用成员访问操作符或域解析操作符。具体代码如下:
//开始在类外部使用该类的成员 Void main() { myClass objClass;//定义类对象; myClass* pClass = new myClass;//定义类指针。 objClass. setValue (100);//在类域外部,使用类成员访问操作符-点号的形式访问类成员。 pClass-> setValue (100);//在类域外部,使用类成员访问操作符-箭头的形式访问类成员。 myClass:: getValue ();//在类域外部,使用域解析操作符的形式访问类的静态成员。 myClass::sb4 myVar = 100;//在类域外部,使用域解析操作符的形式使用类中定义的类型。 } |
在类域外部,对于类对象或指向类对象的指针,需要使用成员访问操作符访问类成员;对于类的静态成员或在类中定义的类型,需要使用域解析操作符进行访问。
情况2:在类域内部使用某个类的成员的时候,可以直接使用成员名称。但是,该成员名称在使用之间必须被声明,否则无法使用。具体代码如下:
----------------------A.h----------------------------- Class myClass { Void setValue(sb4 Para);//错误。不能在此位置使用类型sb4,因为在使用之前没有声明该类型。 Typedef int sb4; Void setValue(sb4 Para);//正确。可以在此位置使用类型sb4,因为在使用之间已经声明了该类型。 }; |
由此可以看出,类成员的声明顺序很重要,它会影响到在类域内部对类成员的使用。
特例:在这里存在一个特殊情况,当在内联函数中使用类成员的时候,内联函数的声明部分在当前位置解析,内联函数的定义部分,在整个类域中解析。因此,在内联函数的声明部分,只能使用在它之前所声明的名称,遵守情况2所描述的规则;在内联函数的定义部分,可以直接使用整个类域中声明的名称。具体代码见2.4.1部分关于内联函数的示例。
在局部作用域中定义的变量是局部变量,局部变量又可以进一步划分为:自动变量,寄存器变量,以及静态局部变量。在C++程序中,除了局部变量外,还存在着其他类型的变量,如:全局变量,常量,静态变量,以及使用new操作符定义的变量,这些变量在内存中的分布情况如下图所示:
当启动一个应用程序的时候,就会启动一个进程。在32的系统中,为该进程分配4G的内存空间。一个进程下面,又会根据需要启动若干个线程,其中一个线程是主线程。在这4G的内存空间中,包含如下类型的存储区域:
无论是变量,函数,枚举,指针,引用还是类型(包括内置类型或类类型),在使用之前,它们必须被声明或者定义。举例如下:
Int a = 10; a = a + 1; class myClass;//声明一个类
void myFunction(myClass& objClass)//定义一个函数。该函数的参数为myClass类型的引用。虽然在这里myClass类还没有被定义,但是这是允许的。 { } myClass * pClass = NULL;
class myClass { Public: Int m_IValue; } pClass = new myClass; myClass objClass; myFunction(objClass);//调用函数。 |
在语句“int a = 10;”中,如果要定义变量“a”,那么类型“int”必须首先被定义。因为类型int为内置类型,在使用该类型之前,它已经被定义了。所以在语句“int a = 10;”中,编译器知道为变量a分配四个字节的内存,并且在该内存位置存储数据10。如果在执行该语句之前,类型int是未知的,那么将会出现未定义错误。
在语句“a = a + 1;”中,如果要执行该语句,那么变量a必须在该语句之前被定义。在执行该语句的时候,从变量a所对应的内存中取出数据,加1后再存储到变量a所对应的内存中。如果在之前没有对变量a定义,也就是说没有为变量a分配内存,那么在执行该语句的时候,将会出现错误。
在语句“myClass * pClass = NULL;”中,该语句能够被正确执行,因为在该语句的前面,已经声明了myClass类。注意,这里仅仅是声明,类myClass还没有被定义。但这是允许的,因为对于任何类型的指针变量,它的大小都是固定的四个字节,在知道该类声明的情况下,就可以定义指针变量,并且为该指针变量分配四个字节的内存。在这时候,属于该指针的内存并没有存储myClass对象的地址,因为myClass类还没有被定义,编译器不知道为该对象分配多少内存,该指针被初始化为NULL。当完成myClass类的定义以后,在执行语句“pClass = new myClass;”的时候,编译器知道了需要为myClass类型对象分配多大的内存。所以,开始定义一个myClass类型的对象,并且将它的地址存储在指针pClass所属的内存中。
在定义类对象指针的时候,可以分两步进行:第一步:完成类的声明,在类声明之后可以定义该类的指针,该指针还不能被初始化为有意义的值,一般指向NULL。第二布:完成类的定义,在此之后,可以定义类的对象,并且将类对象的地址赋给指针。
根据上面的规则,在一个类定义的内部,可以定义该类自身类型的指针,但是不允许定义该类自身类型的对象。举例如下:
Class Node { Public: Node * pNext;//正确,这是允许的。编译器只分配四个字节的内存。 Node objNext;//错误。类定义尚未完成,编译器不知道分配多少内存。 };
Node objNode;//正确,类定义已经完成,编译器知道应该分配多少内存。 |
在定义某个类型的指针或引用的时候,该类型可以先声明,然后在后续部分实现定义;在使用变量,函数,枚举等对象的时候,这些对象必须被提前定义。
在C++中,如果要使用一个对象实体(如:内置类型,类类型,用户定义的变量,函数,指针等),那么该对象实体在被使用之前必须要被声明或定义。编译器在编译C++程序的时候,如果在程序代码中发现一个实体名称(如上面代码中的int,a,myClass等),那么编译器就会在C++的各种作用域中查找该名称的声明或定义(因为需要知道该对象实体内存的大小,内存地址,以及内存中的值)。我们将这一过程叫做名字解析。
这些能够被编译器查找的作用域包括:全局作用域,名字空间作用域,类域,局部作用域等。在该节主要讲述两个问题:
名字解析的过程和顺序如下图所示:
在名字解析的过程中,名字解析的顺序是:从名字的被使用位置开始,从小作用域到大作用域的顺序进行查找,当查找到该名字的声明或定义后,名字查找动作结束。
当在两个不同的作用域中定义相同名称的实体的时候,小作用域中定义的实体会隐藏大作用域中定义的实体。因为编译器从小作用域开始查找,找到第一个名称的定义或声明后,名字查找动作停止,所以小作用域中定义的名称会隐藏大作用域中定义的名称。
在上图中,线索1的查找顺序是:类域,名字空间作用域,全局作用域;线索2的查找顺序是:局部作用域,名字空间作用域,全局作用域;线索3的查找顺序是:局部作用域,类域,名字空间作用域,全局作用域;线索4的查找顺序是:名字空间作用域,全局作用域。
根据名字被使用的位置,可以将名字解析划分成如下的情况:
Int a = 10;//被隐藏 Namespace mySpace [ Int a = 100;//在此处找到a变量的定义,查找停止。如果这里不定义a变量,那么查找将会继续向前,直到在到全局作用域中找到a变量的定义。 a = a + 1;//在使用a的时候,开始从该位置向前查找a的定义。 a = ::a + 1//使用全局作用域中被隐藏的变量。 } |
Typedef short myData;//该类型定义被mySpace中的定义隐藏。 Namespace mySpace { Typedef int myData;//查找到此处,找到myData名称的定义,名字解析结束。 Class myClass { myClass(myData Para);//从此位置向前查找。Para的类型为int。
typedef double myData;//该位置不会被查找到。 } } |
-------------------------------------A.h-------------------------------------- Int myData = 10;//被隐藏 Namespace mySpace { Int myData = 100;//被隐藏 Class myClass { Public: Void myFunction(); int myData;//变量的名字解析在此处找到。 };
Void DealData()//函数的名字解析在此处找到 { } }
-------------------------------A.cpp-------------------------------- #include “A.h” Void myClass::myFunction() { DealData(myData);//在这里,需要进行两次名字解析。首先是函数DealData()的名字解析,因为在调用该函数的时候,在前面的全局作用域中已经完成了该函数的定义,因此名字解析成功。第二个名字解析的是变量myData,在整个类域中找到了该变量的定义。 } |
标签:定义类 运行 系统 停止 strong 分享 语句 double html
原文地址:http://www.cnblogs.com/feng9exe/p/6431554.html