4常量的内存分配
4.1应用程序的内存结构
一个由C++编译的应用程序,占用的内存可以划分为如下几个部分:
- 栈(stack)。由编译器自动分配释放。存放函数参数和函数里的局部变量(又称自动变量)。其操作方式类似于数据结构中的栈。例如,声明在函数中一个局部变量int x; 系统自动在栈中为x分配一块空间,该空间存储x的值。
- 堆(heap)。用于动态内存空间分配。一般由程序员进行分配和释放,若程序员不释放,程序结束时可能由操作系统回收。注意它与数据结构中的堆是两回事,分配方式类似于链表。内存分配在C中使用malloc函数,在C++中用new操作符。以下为C语言小示例:p1 = (char *)malloc(10);注意p1本身是在栈中的,而malloc(10)的数据存放在堆中,p1只是指向了这些数据。
- 全局区(静态区)(static)。全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放。
- 文字常量区。常量字符串就是放在这里(全局区)。程序结束后由系统释放。
- 程序代码区。存放函数体的二进制代码。
具体代码举例如下:
//main.cpp int a = 0; // 全局初始化区 char *p1; // 全局未初始化区 main() { int b; // 栈 char s[] = "abc"; // 变量s在栈中,文字常量”abc”在文字常量区。将从文字常量区向栈中执行初始化操作。即:将”abc”从文字常量区拷贝到栈中为数组s分配的内存区域中。 char *p2; // 栈 char *p3 = "123456"; // 123456\0在文字常量区,p3在栈上。P3指向文字常量区的地址 static int c =0; // 全局(静态)初始化区 p1 = (char *)malloc(10); //分配的10字节的区域在堆区。 p2 = (char *)malloc(20); //分配的20字节的区域在堆区。 strcpy(p1, "123456"); // 123456\0放在文字常量区,p1指向的内容在堆中。将从文字常量区向堆中执行数据拷贝。 free(p1); free(p2); } |
编译生成的二进制代码中,各个变量的名称将转换成对应的地址。对于应用程序,其设计的变量通常转换为绝对内存地址,即程序运行时这些变量实际对应的地址。作为动态链接库等共享库的代码中的变量的地址通常为相对内存地址,当动态链接库被加载时,这些相对内存地址会进行重新定位,转换为对应的内存地址。
4.2文字常量的内存分配
在程序代码中出现的文字常量,在编译阶段,由编译器分配内存到文字常量区。文字常量不可寻址,不能对文字常量执行取地址操作。如果在程序代码中出现多个相同的文字常量,那么在文字常量区只保持该文字常量的一份拷贝,其他使用该文字常量的代码将从此拷贝处取值。具体代码举例如下:
Char* pStr = “lifeng”; Char arrStr[] = “lifeng”;//假设arrStr定义在局部作用域中 |
在文字常量区,只保持文字常量”lifeng”的一份拷贝。指针pStr指向文字常量区中文字常量”lifeng”的首地址;变量arrStr在栈上分配内存空间,在数组初始化的时候,从文字常量区将字符串”lifeng”拷贝到栈上为该数组分配的内存空间中。
文字常量区具有只读属性,任何对该区数据的修改操作都会引起错误。
4.3符号常量的内存分配
符号常量的内存分配情况如下表所示:
全局作用域 |
文件作用域 |
局部作用域 |
|
内存分配 |
在文字常量区分配内存,该区域内存具有只读属性。任何试图更改该区数据值的操作都会引起错误。 |
不分配内存,符号常量的名称只保存在名称列表中。如果在代码中执行取符号常量地址的操作,也会引起内存分配。 |
在栈中分配内存,由于栈不具有只读属性,因此,符号常量的只读性由编译器在编译阶段保证 |
常量折叠 |
不执行常量折叠 |
执行常量折叠 |
执行常量折叠 |
内存分配的时刻 |
编译时 |
运行时 |
符号常量默认具有文件作用域,在编译时刻,如果其值明确,编译器将执行常量折叠,并且不会为该符号常量分配内存。如果对符号常量执行了取地址操作,或者对符号常量使用了关键字extern,那么在编译阶段,编译器将为该符号常量分配内存。
如果想让符号常量具有全局作用域,或者符号常量的值在编译时刻不可知,这时候就需要对符号常量使用关键字extern。
关键字const只是告诉编译器由它修饰的数据其内容不能更改,这个规则是在编译阶段控制的。在编译的时候,如果编译器发现有更改常量数据值的代码,那么编译器就会报错。
5符号常量与指针的关系
符号常量与指针结合以后,将会涉及到三方面的内容,它们分别是:
l 指针常量,即指针本身不可变,定义之后必须初始化;
l 指向常量的指针,即指针所指向的数据值不可变,定义之后可以不初始化;
l 指向常量的指针常量,即指针本身和指针所指向的数据值均不可变,定义之后必须初始化。
具体的示例代码如下:
//指针常量,即指针所指向的地址不可变。 Char * const pStr = “lifeng”;//可以用文字常量初始化字符类型指针 Int a = 10; Int * const pInt = &a;//pInt不能再指向其他的地址,但可以通过pInt改变a的值 //指向常量的指针,即指针所指向的数据值不可变 Const char* pStr = “lifeng”;//定义并初始化 Const char* pChar;//定义之后可以不初始化 pChar = “lifeng”;//后续赋值 Int a = 10; Const int* pInt = &a;//指向常量的指针可以存储变量的地址。不能通过pInt改变a的值。 //指向常量的指针常量,即指针本身和指针所指向的数据值均不可变 Const char * const pStr = “lifeng”; Int a = 10; Const int * const pInt = &a; |
这里有一个规则,关键字const位于星号(*)的左边表示指针所指向的数据的值不可变;关键字const位于星号(*)的右边表示指针本身不可变。
指针与常量和变量之间的关系如下图所示:
指向常量的指针即可以保存符号常量的地址,也可以保存普通变量的地址。不能通过指向常量的指针去改变它所指向的常量或变量的数据值。指向变量的指针不能存储符号常量的地址,因为通过指向变量的指针可以改变它所指向的数据的值。如果指向变量的指针指向了符号常量,那么就可以改变该符号常量的数据值,这是不允许的。
关键字const所修饰的指针的常量性是由编译器在编译阶段保证的。
6符号常量与引用的关系
从定义上来说,引用是一个变量的别名,所有对该引用的操作都相当于对这个变量的操作。但从本质上来讲,引用就是指针。但是引用是被限制了的指针,它相当于指针常量。引用一旦指向一个变量以后,就不能再次指向其他的变量。
引用是C++语法范畴的概念,提出引用的概念是为了方便程序员编写程序。从汇编语言的角度来看,在处理方式上,引用和指针没有却别。也就是说,在汇编的层面上,不存在引用的概念。具体代码如下:
-------------------------------C++代码--------------------------------- double pi = 3.14; double& ypi = pi; double* ppi = π --------------------------------------------汇编代码---------------------------------------- double pi = 3.14;//为浮点数据分配内存,并初始化 00A4141E fld qword ptr [__real@40091eb851eb851f (0A45800h)] 00A41424 fstp qword ptr [pi] double& ypi = pi;//取出pi的地址,并赋给ypi 00A41427 lea eax,[pi] 00A4142A mov dword ptr [ypi],eax double* ppi = π//取出pi的地址,并赋给ppi。从这两处代码可以看出,对指针和引用的处理方式是一样的 00A4142D lea eax,[pi] 00A41430 mov dword ptr [ppi],eax |
如果关键字const不与引用结合,那么只能采取下面的方式定义和初始化引用:
//只能采用下面的方式定义,并初始化引用 Int a = 10;//定义变量a Int& b = a;//定义a的引用。 //下面的作法是错误的 Int& b = 10;//错误 Double pi = 3.14; Int& c = pi;//错误 |
当关键子const与引用结合以后,可以采用如下的方式定义并初始化引用:
Const Int& a = 10;//使用整型文字常量初始化整型引用 Const Int& b = 3.14;//使用浮点型文字常量初始化整型引用 Double pi = 3.14; Int& c = pi;//使用浮点型变量初始化整型引用 Double&ypi = pi; |
可以使用不同类型的对象初始化Const引用,只要能从一种类型转换成另外一种类型即可;也可以使用不可寻址的文字常量。同样的初始化方式对于非const引用是不合法的。
能够这样做的原因是:在编译阶段,编译器首先生成一个临时变量,然后将这些数据值,如上面代码中的10,3.14赋给这个临时变量,然后再将常量引用指向这个临时变量。当我们定义的引用是常量引用的时候,由于不能修改被引用的对象的值,临时变量的值和实际的数据值保持一致,不会有错误产生;当我们定义的引用是非常量引用的时候,如果也采用临时变量的方式处理,因为可以更改引用所指向的变量的值,但这时候更改的是临时变量的值,而实际的数据值没有变化。所以,非const引用不会采取临时变量的方式处理。
Const引用一般会被当作函数参数来使用,具体代码举例如下:
//使用const引用作为参数的函数 Void dealData(const int& Para); //可以有如下的调用方式 dealData(10); dealData(3.14); double pi = 3.14; dealData(pi); int a = 100; dealData(a); //使用非const引用作为函数的参数 Void dealData(int& Para); //可用的调用方式 Int a = 10; dealData(a); |
由上面的代码可以看到,使用const修饰了引用类型的参数以后,可以采用更灵活的方式来调用该函数。
7符号常量与函数的关系
函数的参数有三种形式,分别是:类型对象(内置类型或类类型),指针,引用;函数的返回值也有三种形式,分别是:类型对象(内置类型或类类型),指针,引用。
关键字const可以修饰函数的参数,函数的返回值。如果该函数是类的成员函数,那么关键字还可以修饰整个函数,表示该函数不会修改类的数据成员。
7.1符号常量修饰函数参数
符号常量与函数参数结合使用的时候,一般有两种情况:一种情况是修饰指针类型的函数参数,另外一种情况是修饰引用类型的参数。
默认情况下,在传递参数的时候,函数采用值传递的方式。即:将实参的值复制一份到临时变量中,然后将临时变量值传递到函数中(压栈)。在函数体中,当对函数参数操作的时候,如改变参数的值,实际上操作的是临时变量的数据值,函数的实参不受影响。通过这种方式,起到了对函数实参保护的作用,即:在函数体中不能随意更改函数的实参值。
这种值传递的方式,对于C++内部数据类型,如:整型,浮点型,字符型等,是没有问题的。因为内部数据类型所占用的内存较小,在复制的时候,不会引起大的性能问题。
当函数的参数是一个类的对象,并且在这个类类型中包含了大量的成员数据的时候,如果依旧采用值传递的方式处理,那么就会引起大的性能问题。在这种情况下,我们需要将传递的参数更改成指针或者引用。
如果函数的参数是指针,虽然依旧执行了值传递的规则(传递参数的时候,指针被复制了一份,但这两个指针都指向同一个对象),但是指针只占用4个字节,不会影响效率;如果函数的参数是引用,函数实参自身被传递到函数体中,在这个过程中,不需要数据复制。无论是采用指针的方式还是采用引用的方式,在函数体中都可以对指针指向的对象或引用所代表的对象的内容进行修改。
如果需要对函数的实参进行保护,即:在函数体中不能修改函数实参的内容,那么就需要在指针或引用前面使用关键字const。具体的代码举例如下:
//函数的声明 Class A;//声明类 Void dealData(const A* pA);//采用指针的方式传递参数 Void dealData(const A& objA);//采用引用的方式传递参数 //函数调用 A* pA = new A;//定义类A的指针 A objA;//定义类A的对象。 dealData(pA);//此处依旧执行了值传递,指针pA被复制一份传递到函数中。但指针所指向的内容不可修改。 dealData(objA);//用objA初始化引用参数。实参直接传递到函数中,由于使用关键字const修饰参数,引用所代表的实参不能被修改。 |
对于非内部数据类型的输入参数,为了考虑效率,一般采用指针或引用的方式传递参数值。如果在函数中不需要对实参的值进行更改,最好将关键字const与指针或引用参数结合使用。
对于内部数据类型的输入参数,采用值传递的方式处理即可。由于值传递规则的存在,关键字const与非指针或引用类型的参数结合起来是没有意义的。
另外,关键字const与引用参数结合使用以后,当在调用该函数的时候,就可以才用多种灵活的调用方式,如:直接传递文字常量,或其它的数据类型。具体情况见第六节的描述。
7.2符号常量修饰函数返回值
默认情况下,当从一个函数中返回一个数据值的时候,采用的是值传递的形式。即:在函数中生成一个临时变量,将要返回的数据值复制到该临时变量中,然后将该临时变量返回。当函数的返回值是引用类型的时候,在返回数据值的时候,该函数不产生临时变量,直接将要返回的数据值返回。
在对二目操作符重载,并产生新对象的情况下,一般用关键字const修饰返回值,并且该返回值为对象(非指针或引用)。具体代码举例如下:
Class myClass { Public: VoidmyClass(int Para1,Para2); Int m_Data1; Int m_Data2; }; ConstmyClass operator+ (constmyClass& Para1,const myClass& Para2) { Return myClass(Para1.m_Data1+Para2.m_Data1,Para1.m_Data2+Para2.m_Data2); } |
这样做的目的是为了防止如下情况的发生:
Class myClass; myClass A; myClass B; myClass C; (A*B) = C; |
除了这种情况外,一般很少用const修饰返回对象。因为一旦用const修饰了返回的对象,那么该对象就具有常量性,在该对象上只能调用常量函数。
8符号常量与类的关系
关键字const可以修饰类的成员函数,使之称为常量成员函数。常量成员函数不能修改该类型的数据成员。应该把所有不修改数据成员的函数定义为常量成员函数。如果在常量成员函数中修改了类的数据成员,那么在编译阶段,编译器将报错。
关键字const可以修饰类对象,类指针,类引用,使这些类对象,类指针,类引用具有常量性。不能修改具有常量性的类对象,类指针所指向的类对象,以及类引用所关联的类对象的数据成员,并且只能使用这些类对象,类指针,类引用调用该类型的常量成员函数,不能调用非常量成员函数。普通的类对象,类指针,类引用可以调用所有的该类型的成员函数,包括常量成员函数。它们的关系如下图所示:
由于不能通过具有常量性的类对象,类指针,类引用修改类的数据成员,所以它们只能调用不修改类数据成员的常量成员函数。而普通的类对象,指针,引用没有这个限制,所以它们可以调用所有的类成员函数。具体的代码举例如下:
Class myClass { Public: Void Func(int Para);//普通函数成员 Void Func(int Para) const;//重载Func,常量函数成员。关键字const可以实现函数重载 Int GetData() const;//常量函数成员.在函数后面加关键字const,表示该函数不修改类的数据成员 Void DealData(int Para);//普通函数成员 }; MyClass objClass;//定义类对象 Const myClass objConstClass;//定义类的常量对象 objClass.Func(100);//正确。普通对象调用普通的成员函数。 objClass.DealData(50);//正确 objClass.GetData();//正确,普通类对象可以调用常量函数 objConstClass.Func(200);//正确,常量对象调用常量成员函数 objConstClass.GetData();//正确。 objConstClass.DealData(50);//错误,常量对象不能调用非常量函数 |
关键字const可以实现函数的重载。在函数调用的时候,普通类对象调用普通的成员函数(Void Func(int Para);),常量对象调用常量成员函数(Void Func(int Para) const;)。