1概述
一个C++程序就是一系列数据与操作的集合。当一个C++程序开始运行的时候,与该程序相关的数据就会被加载到内存中。当数据与内存发生关联的时候,这些数据就会具有如下的特性:
- 数据在内存中的地址。这个地址决定了数据在内存中的存储位置。在32位的系统中,每一个C++程序都具有4GB大小的内存地址空间,这个4GB大小的内存空间又被划分为若干个区域,如:栈区,堆区,全局(静态)区,文字常量区,以及程序代码区。不同内存地址的数据将会被存储在不同的内存区域中;
- 数据在内存中的值。如果该值可变,那么该数据就是变量;如果该值不可变,那么该数据就是常量;
- 数据的类型。数据的类型决定了数据占用内存的多少。如:Int型占4个字节,Short型占两个字节;
- 内存分配的时间点。有的数据在编译时由编译器分配内存,这些数据是:全局变量/常量,文字常量,静态变量。在编译阶段,编译器就为这些数据分配了内存,当程序运行的时候,这些数据就会被直接加载到已分配的内存位置上;而有的数据只能在运行时由系统分配内存,如:函数内部的局部变量,以及使用new操作符分配的变量。这些数据的内存地址在程序运行时刻才能确定。
在编码阶段,程序员可以定义各种常量和变量;在编译阶段,编译器会将程序员定义的变量和常量放到不同的数据段中;在程序运行阶段,这些变量和常量又会被加载到不同的内存区域。以下,将从编码阶段,编译阶段,程序运行阶段,三个阶段来说明变量/常量在内存,数据段中的分布情况。
在C++的编码阶段,程序员可以定义各种作用域或生命周期的变量或常量,如:全局变量/常量,静态变量/常量,局部变量/常量,以及使用new操作符分配的变量或常量。
在C++的编译阶段,编译器将在编译时刻能够确定内存地址的变量或常量放到不同的数据段中。在编译后的可执行文件中,一般会包含如下的数据段:
- .text段,该数据段存放程序代码;
- .bss段,该数据段存放未初始化的全局变量或静态变量;
- .data段,该数据段存放已初始化的全局变量或静态变量;
- .rdata段,该数据段存放文字常量,以及在全局作用域中定义的字符常量。
在C++程序的执行阶段,程序代码中定义的变量和常量会被加载到不同的内存区域。如:全局变量,静态变量加载到全局区,文字常量加载到文字常量区。
具体情况如下表所示:
创建时刻 |
程序代码 |
可执行文件 |
内存 |
备注 |
编译时刻 |
已初始化的全局变量,静态变量 |
.data段 |
全局区(初始化) |
可读,可写 |
未初始化的全局变量,静态变量 |
.bss段 |
全局区(未初始化) |
可读,可写 |
|
文字常量 |
.rdata段 |
文字常量区 |
只读 |
|
全局符号常量 |
.rdata段 |
文字常量区 |
只读 |
|
运行时刻 |
局部自动变量,符号常量 |
栈区 |
属于线程 |
|
New操作符分配的变量,常量 |
堆区 |
属于进程 |
2常量的分类
2.1文字常量
当一个数值,字符或者字符串,例如 1,’A’,”Test Const”等,出现在程序代码中的时候,它们被称为文字常量。称之为“文字”是因为我们只能以值的形式表述它们;称之为“常量”是因为它们的值不能被改变。文字常量出现在C++代码中,一般为其他变量赋初始值。具体的示例代码如下:
Int a = 10;//10为整型文字常量。在编译时,数值类型文字常量被当作立即数处理。从汇编指令中可以看到这个情况 int a = 10; 001F153D mov dword ptr [ebp-14h],A //10被当立即数处理。直接出现在指令中 Double pi = 3.1415;//3.1415为浮点型文字常量。 Char b = ‘a’;//a为字符型文字常量。在初始化的时候,从文字常量区将a拷贝到变量b所在的内存地址处。 Char* strName = “wolf”;//wolf为字符串型文字常量。指针strName指向文字常量区中字符串wolf的首地址。 Char[] arrName = “wolf”;//wolf为字符串型文字常量。数组初始化的时候,从文字常量区将wolf拷贝到数组所在的地址中。如果arrName在局部定义,那么就拷贝到栈中,如果在全局作用域定义,那么就拷贝到内存的全局区。 //wolf在文字常量区只有一份拷贝。 |
在C++代码中,直接以值的形式出现的各种类型的数据都属于文字常量。在编译阶段,编译器为字符型文字常量分配内存地址,将数值型文字常量处理成立即数;在编译后的可执行文件(PE文件)中,文字常量被放到.rdata数据段中;在程序运行阶段,文字常量被加载到内存的文字常量区。在具体使用的时候,将会从文字常量区将文字常量的值拷贝到使用文字常量的变量的内存地址处,如:Char[] arrName = “wolf”;或者变量的指针直接指向文字常量区中文字常量的位置,如:char * strName = “wolf”。如果在代码中存在多个相同的文字常量,那么在存储的时候,在文字常量区只保持该文字常量的一份拷贝,使用该文字常量的多个变量将从此文字常量处取值。
按照数据类型的不同,文字常量可以划分为数值常量,字符常量,以及字符串常量。其中,数值常量又包括整型常量和浮点型常量,字符常量又包括普通字符常量和转义字符常量。具体分类结构如下图所示:
字符常量用单引号表示,字符串常量用双引号表示。如:’a’表示字符常量,而”a”则表示字符串常量。
2.2符号常量
在C++代码中,常常用一个符号名称来代表一个常量,这个符号名称被称为符号常量。通常有两种方式定义符号常量,一种方式是使用关键字const,另外一种方式是使用宏定义。
使用关键字const定义符号常量的格式如下:
Const常量的数据类型 符号常量名 = 文字常量; 举例: Const double PI = 3.14;//定义double类型的符号常量。PI是符号常量,3.14是文字常量 Const char* strName = “wolf”; |
使用宏定义方式定义符号常量的格式如下:
#define 符号常量名文字常量。 #define PI 3.14 //定义符号常量PI,从这个语句中区分不出数据类型。 #define strName“wolf” |
在实际的编码过程中,建议尽可能多地使用关键字const定义符号常量,而不是使用宏定义的方式。
使用关键字const定义符号常量以后,必须要进行初始化工作,并且其值在随后的代码中不能被改变。C++有两种方式来保证符号常量的常量性。一种方法是使用内存区域的只读属性来保证,这种方法适用于全局常量,如果该符号常量的值发生变化,系统会报错;另外一种方法是在编译阶段,由编译器利用常量规则来保证,这种方法适用于局部常量。在编译器编译的时候,如果在代码中发现更改符号常量值的情况,编译器就会报错。但是当代码编译通过以后,在运行阶段,可以采用一些特殊的方法改变该符号常量的值,并且系统不会报错。
如果对符号常量执行取地址操作,或者对符号常量使用关键字extern,那么在编译的时候,编译器将为符号常量分配内存;否则,在编译的时候,符号常量的名称只被放到符号列表中,不执行内存分配,在使用符号常量的地方执行常量折叠。
可以在各种作用域中定义符号常量。
3符号常量与作用域的关系
3.1常量折叠
如果符号常量的定义在当前文件可见,那么在编译的时候,编译器将执行常量折叠。在使用符号常量的代码处,编译器会用符号常量的实际值去替换符号常量。如果符号常量的定义在当前文件不可见,将无法进行常量折叠。具体代码举例如下:
----------------------------------------------A.h------------------------------------------------------- Extern const int fa;//声明符号常量。该符号常量具有全局作用域 --------------------------------A.cpp--------------------------------------- #include"A.h" constint fa = 3;//定义符号常量。 --------------------------------------------main.cpp------------------------------------------------- #include<iostream> #include"A.h"//在main.cpp文件中,只知道符号常量fa的声明,不知道它的定义 usingnamespace std; int main() { constint a = 10;//定义局部常量。在栈中为该符号常量分配了内存。 int b = a + 2;//符号常量定义可见,在编译时刻执行了常量折叠。 int c = fa + 2;//符号常量不可见,没有执行常量折叠。 constint * pa = &a;//执行取常量地址操作。该符号常量的常量性由编译器在编译阶段保证。 *(const_cast<int*>(pa)) = 100;//取消指针的常量性,改变常量的值。可以这么干,完全没有问题 //也可以采用下面的方式更改常量的值。 //int* c = (int*)pa; //*c = 100; cout << *pa <<endl; cout << a <<endl;//符号常量定义可见,在编译时刻执行了常量折叠。 cout << b <<endl; int k; cin >> k; } ----------------------------------------汇编代码------------------------------------ const int a = 10; 002C13EE mov dword ptr [a],0Ah //在栈中为a分配了内存,并将10存储在该内存处 int b = a + 2;//符号常量定义可见,在编译时刻执行了常量折叠,已经计算出10+2的值。因此下面汇编代码中,只给出了立即数0Ch,也就是十进制的数值12。 002C13F5 mov dword ptr [b],0Ch int c = fa + 2;//没有进行常量折叠,只能在运行时计算结果。 002C13FC mov eax,dword ptr [a (2C5800h)] 002C1401 add eax,2 002C1404 mov dword ptr [c],eax //在现实中,一般很少出现定义了符号常量,然后又改变其值的蛋疼操作。这里只是说明问题而已。将常量值的改变与常量折叠进行对比,更能深入理解常量折叠这个操作。 |
上面的代码执行完毕以后,输出的结果是:100,10,12。在上面的代码中,*pa输出了100,那是因为在程序运行阶段,我们改变了符号常量的值;a输出10,是因为在编译阶段,编译器执行了常量折叠,用10替换了a;代码cout<< a <<endl; 变成了cout<< 10 <<endl;b输出12也是这个道理。
在局部作用域中定义的符号常量,在运行时刻可以改变它的值。因为它的内存被分配到栈上,它的常量性是在编译阶段由编译器通过规则保证的。在运行阶段,编译器就没有办法了。
对于在全局作用域中定义的符号常量,如上面代码中的fa,在运行时刻无法改变其值。因为在全局作用域中定义的符号常量的内存地址被分配到了文字常量区,该区具有只读属性。全局作用域中的符号常量的常量性是通过内存的只读属性来控制的。
3.2符号常量与文件作用域的关系
头文件是用来声明而不是定义函数和变量的,而源文件则是用来实现变量和函数定义的。头文件中一般包含类的定义,枚举的定义,符号常量的定义,内联函数的定义,extern变量的声明,函数的声明,typedef声明。如果将变量定义到头文件中,那么当其他文件引入该头文件的时候,就会认为该对头文件中的变量又执行了一次定义。根据一次定义法则,这是不允许的。但是也有三个例外,类,内联函数,在编译时值可以确定的符号常量是可以在头文件中定义的。
符号常量默认具有文件作用域,所以符号常量可以定义到头文件中,然后被其他源文件引用。在多个源文件中,可以存在同名的符号常量,它们是独立的个体,互不影响。具体代码举例如下:
------------------------------A.h------------------------------------ Const double PI = 3.14;//定义符号常量,虽然该符号常量被定义在全局作用域中,但它默认只具有文件作用域。在其他文件中是无法访问该符号常量的。 ----------------------------B.cpp------------------------------------ #include “A.h”//引入头文件,在编译阶段,头文件A.h和源文件B.cpp被合并为一个文件,在头文件A.h中定义的符号常量PI,相当于在新合并后的源文件中被定义。 Double GetMJ(double r) { Return r * r * PI;//使用常量,因为符号常量的定义在当前文件可见,所以编译器用3.14替换符号常量PI。具体的代码就变成了“return r*r*3.14;”。这个替换的过程叫常量折叠。 } ----------------------------C.cpp------------------------------------------ #include “A.h”//引入头文件,相当于在C.cpp源文件中又定义了一次符号常量PI。但是符号常量默认具有文件作用域,B.cpp源文件中的符号常量PI与C.cpp源文件中的PI互不影响。 Double GetZC(double r) { Return 2 * PI * r;//编译时执行常量折叠。 } |
在文件作用域中定义的符号常量,一般其值明确,所以编译器在编译阶段执行了常量折叠。并且,没有为该符号常量分配内存,只是将该符号常量的名称保存到了符号列表中而已。符号列表是编译技术中的一个概念。
3.3符号常量与全局作用域的关系
符号常量默认具有文件作用域。当在编译时刻符号常量的值已知的情况下,编译器只是将符号常量的名称放到符号列表中,并不对该符号常量分配内存。在使用该符号常量的代码处,编译器执行了常量折叠。
如果对符号常量使用了关键字extern,那么该符号常量就具备了全局作用域。编译器在编译阶段将为该符号常量分配内存。
如果符号常量的值在编译阶段未知,那么也需要对该符号常量使用关键字extern。在这两种情况下,编译器无法进行常量折叠。
具体代码举例如下:
-----------------------------------A.h---------------------------------- externconstint a;//在全局作用域中声明符号常量,使用了关键字extern,所以该符号常量具有全局作用域 -------------------------A.cpp------------------------ #include"A.h" constint a = 10;//在此处定义符号常量,并初始化其值为10.编译器为该符号常量分配内存。 ------------------------main.cpp------------------------ #include<iostream> usingnamespace std; #include"A.h"//引用了头文件,相当于声明了一次符号常量,符号常量的真正定义在A.cpp中。Main.cpp不知道该符号常量的定义。 int main() { int b = 5 * a;//这里没有进行常量折叠。在运行时,从符号常量a的内存地址处取得其值。具体情况见汇编代码。 Int c = 5*10; cout << a <<endl; cout << b <<endl; int k; cin >> k; } --------------------------汇编代码-------------------------- int b = 5 * a; 0104360E mov eax,dword ptr [a (1045800h)] //将a值从内存地址1045800h出取出,放到eax中。 01043613 imul eax,eax,5 //执行乘法操作。如果执行常量折叠的话,这里根本不会执行乘法操作。 01043616 mov dword ptr [b],eax //将乘法结果放到b中。 int c = 5*10; 00AF3619 mov dword ptr [c],32h // 编译阶段已经计算完毕,这里直接给出立即数 |
从上面的示例可以看出,关键字extern使符号常量具备了全局作用域,并且为该符号常量分配了内存地址,同时,在编译的时候,编译器不为该符号常量进行常量折叠。因为该符号常量的定义在当前文件不可见。
所以,在使用符号常量的时候,如果该符号常量的值在编译时刻已知,那么就将该符号常量处理成具有文件作用域即可。这样,在编译阶段,编译器能够为该符号常量进行了常量折叠,从而提高了执行阶段的效率。
3.4符号常量与类域的关系
可以在类中定义符号常量,该符号常量作为该类的数据成员出现。对于每个类对象来说,符号常量的值是固定的;但是,对于整个类的类型来说,符号常量的值是可变的。
在具体操作的时候,可以将符号常量定义在实现该类定义的头文件中,但是,符号常量的初始化操作必须在该类构造函数的初始化列表中实现。
具体代码如下:
---------------------------------------A.h--------------------------------------- #ifndef _MyClass_H #define _MyClass_H class MyClass { public: MyClass(int Para); private: constint m_conVal;//定义常量 }; #endif --------------------------------A.cpp------------------------------ #include"B.h" MyClass::MyClass(int Para):m_conVal(Para)//初始化常量 { } |
因为常量是在类的构造函数中被初始化,所以对于每一个类对象来说,常量的值是固定的;对于整个类的类型来说,在每次初始化类对象的时候,向构造函数中传递的参数可能不同,因此,常量的值也会不同。
如果想要建立在整个类中都恒定的常量,应该用类中的枚举常量来实现。具体示例代码如下:
Class myClass { Enum { size1 = 100,size2 = 200,size3 = 300}; Int arr1[size1]; Int arr2[size2]; }; |
枚举常量不会占用对象的存储空间,他们在编译时被全部求值。但是枚举常量的隐含数据类型是整数,其最大值有限,且不能表示浮点数。
3.5符号常量与局部作用域的关系
可以在局部作用域中定义符号常量,在运行时刻,该符号常量在栈中被分配内存。该符号常量的常量性由编译器在编译时刻保证,如果在代码中发现有改变该符号常量的值的语句,那么编译器就会报错。
如果符号常量的定义在当前文件是可见的,那么在编译阶段,编译器将会执行常量折叠;如果符号常量的定义在当前文件不可见,那么将会在运行时刻,从符号常量的存储位置取得该值。
程序员可以改变运行时刻局部符号常量的值。具体代码举例如下:
---------------------------------main.cpp------------------------------------------ #include<iostream> usingnamespace std; int main() { constint a = 10; int b = 10; int c = a * 5; int d = b * 5; int k; cin >> k; } -----------------------------------汇编代码------------------------------------- 00D713EC rep stos dword ptr es:[edi] const int a = 10; //为a分配了内存 00D713EE mov dword ptr [a],0Ah int b = 10; //为b分配了内存 00D713F5 mov dword ptr [b],0Ah int c = a * 5; //执行了常量折叠,汇编代码中只给出了立即数32h,也就是十进制的50 00D713FC mov dword ptr [c],32h int d = b * 5; //对于变量,执行了计算操作。 00D71403 mov eax,dword ptr [b] 00D71406 imul eax,eax,5 00D71409 mov dword ptr [d],eax |