叠甲:仅为记录个人学习过程;内容很多是从别处看来的,在参考中列出;缺乏实践经验,有些地方纸上谈兵;难免有错误,敬请指出。
系列目录:【我看UE源码】(0)系列开篇 & 目录
Const的内存分配
(接上文)
对于上述通过指针、引用(也就是地址)来修改const变量的值,只对局部变量有效。
const int ga = 1;
int main()
{
int* pga = const_cast<int*>(&ga);
*pga = 10; // 运行时报错
std::cout << "a:" << const_cast<int*>(&a) << ' ' << a << std::endl;
int& rga = const_cast<int&>(ga);
rga = 20; // 运行时报错
std::cout << "a:" << const_cast<int*>(&a) << ' ' << a << std::endl;
}
如果进行上面代码中的操作,试图修改全局const变量的值,编译器不报错,但运行时在指针、引用赋值的地方报错:“引发了异常:写入访问权限冲突”。
这是由C++ const的内存分配方式导致的。
全局const变量的内存
const全局变量存放在.rodata段的Read Only Data也就是常量存储区(只读数据区),无法通过取地址方式修改,所以报错
关键字修饰变量的内存存储方式如下:
- static:
- static表示是静态变量,内存中只存在一份。无论是全局变量还是局部变量都存储在全局/静态区域,在编译期就为其分配内存,在程序结束时释放
- 类的static成员变量必须在类声明的外部进行初始化。初始化时不赋值就默认为0
- 全局数据区的变量都有默认初始值0,而动态数据区(堆区、栈区)变量的默认值不确定,一般是垃圾值
- static成员变量属于类而不属于对象,在全局区分配内存,不占用对象的内存
- static成员变量不随着对象创建而分配内存,不随着对象销毁而释放内存
- extern:在当前源文件声明全局变量,告知编译器去别的地方(其他文件,或本文件其他行)找定义
- const
- const全局变量存储在只读数据段,编译期最初将其保存在符号表中,第一次使用时为其分配内存,在程序结束时释放
- const局部变量存储在栈中,代码块结束时释放(见下一节)
- const修饰的全局变量默认是内部链接,即只在当前源文件有效,不能直接用于其他源文件。如果必须在其他源文件中使用只读的全局变量,需要加extern将变量转换成外部链接
局部const变量的内存
局部const变量的内存分配有一些争议,可以看看这篇文章。据我尝试,跟这篇文章一样,VS2019对于基础类型赋值的const变量,release模式下不分配内存,debug模式下分配内存。
一般来说,总结为:
- 用基础类型赋值的const变量,放入符号表中,不会分配内存
- 对const变量取地址的时候(&a),给这个变量开辟空间
- 当用变量给const初始化赋值时,直接给它开辟空间,不会放入符号表中
- 自定义数据类型(结构体、类)的const对象,const数组等会分配内存空间
举例:
#include<iostream>
struct Student
{
int age;
char name[10];
};
int main()
{
const int a = 10; // 用基础类型值初始化,a不分配内存,放到符号表里
int* pa = const_cast<int*>(&a); // 对a取地址,分配内存
*pa = 20;
std::cout << *pa << ' ' << a << std::endl; // 由于编译器优化(见上篇),*pa=20,a=10
int b = 30;
const int c = b; // 用变量初始化,直接给c分配内存空间,不放入符号表
int* pc = const_cast<int*>(&c);
*pc = 40;
std::cout << *pc << ' ' << c << std::endl; // 40 40
const Student stu = { 20, "tom" }; // 自定义类型的const对象,分配内存
Student* pstu = const_cast<Student*>(&stu);
pstu->age = 21;
std::cout << pstu->age << ' ' << stu.age << std::endl; // 21 21
return 0;
}
总结
const int a = 3; // 全局const变量,符号表,使用时分配在常量区(只读数据段)
int b = 2; // 全局变量,分配在静态/全局区
static int c = 1; // 静态全局变量,分配在静态/全局区
static int d; // 未初始化的静态全局变量,有初始值0
int e; // 未初始化的全局变量,有初始值0
int main(){
static int f = 4; // 初始化的静态局部变量,分配在静态/全局区
static int g; // 未初始化的静态局部变量,分配在静态/全局区,有初始值0
int h = 5; // 初始化的局部变量,分配在栈区
int i; // 未初始化局部变量,分配在栈区,初始值是无效值
const int j = 6; // 局部const变量,放入符号表,取地址时分配在栈区
const int k = h; // 局部const变量,直接分配在栈区
}
const和define
const和define都可以用来定义常量,它们的区别是:
- const常量是编译期的概念
- const常量有数据类型,宏没有。编译器可以对const进行类型安全检查
- define宏定义作用在预处理期
- 程序在预处理阶段,把define定义的内容进行字符串替换。需要注意运算符优先级之类的边缘效应问题
- 编译期不会进行数据类型检查
- 程序运行时,系统不为define定义的常量分配内存
在C++里,如果想定义一个常量,最好用const而不是define。关于尽量使用const,可以看《Effective C++》的条款三。
const,引用传参,常量引用
在前文说过,可以把const看作是一种注释,对于局部const变量,不一定分配了内存,即使分配了或许也不在常量区。它只是让编译器注意不要直接修改其值,或者让程序员不要写直接修改值的代码。
const还有一大作用,就是在函数传参时,传递常量的引用类型。这是C++机制上的一个问题,可以看看这个答案下的回答。
引用传参
在函数传参时,如果传值,会新建值的对象并且进行拷贝,在函数中对新对象进行操作,不会影响原来的对象;而引用相当于给一块内存起一个别名,如果传递引用,就可以直接对实参进行操作。
指针和引用基本上是一样的,区别在于:
- 指针会分配内存,用来存储指向的内存地址。可以找到自身开辟的内存空间,可以通过
*
运算符解引用找到指向的内存单元。 - 对于引用,虽然在C++底层处理时和指针处理方式相同,不过在用到引用变量的地方,会自动对其进行解引用,这一步骤系统默认进行,所以我们找不到引用自身开辟的内存单元,从这里看,引用好像没有开辟自身内存,只是给引用对象起了一个别名。
关于指针、引用的区别和用它们传参,可以看这篇文章。其中提到了使用引用的几个条件:
- 引用必须在声明时就进行初始化
- 引用初始化的变量一定要能取地址
- 引用关系是不可变的
以及引用和指针的区别,不是本篇重点,但八股会问。稍微记录一下
- 从内存分配上看,指针是对象实体,程序为其分配内存空间。而引用从表面上看,只是变量的别名,程序不需要为其分配空间(实际上引用跟指针是一样存的,但在使用时会自动解引用,用起来像别名)
- 引用使用时无需解引用,系统会为其自动进行解引用,而指针使用时需要手动解引用(
*
运算符) - 指向关系上,引用只能在定义时被初始化一次,之后则不可变。指针则可以改变
- 引用不能为空(必须初始化),指针可以为空。
- “sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身(所指向的变量或对象的地址)的大小(为了完整寻址,32位4字节,64位8字节)
- 指针和引用的自增(++)运算意义不一样。指针自增是让其指向下一段内存单元,而引用自增是使其对应的变量自增
常量引用
对于第二条,类似int& a = 10;
是不行的。也就是引用只能作用于一个变量,不能赋值到字面常量,比如1, 2, 3
等字面量,"Hello World"
等常量字符串。它们是不可以被修改的,不能使用非const类型的引用传递。
C++引入了常量引用的概念来处理这种情况。常量引用是“对const的引用”的简称,把它指向的对象看作是常量(不一定真是),不可以通过该引用修改它指向的对象的值。(严格来说不存在常量引用,因为引用不是一个对象,无法让引用本身恒定不变;由第三条,C++不允许随意改变引用绑定的对象,从这种意义上理解,所有引用本身都是常量。)
在使用上:
指向常量对象时,必须用常量引用
const int a = 10; const int& ra = a; // 正确,指向常量对象 ra = 20; // 错误 int& rb = a; // 错误
常量引用可以指向非常量对象,但不能修改它的值
int a = 10; int& ra = a; const int& rb = a; // 正确 ra = 20; // 正确 rb = 20; // 错误
常量引用可以指向字面值、常量字符串、一般表达式等非左值、不能赋值的对象
int i = 10; const int& ra = i; // 正确 const int& rb = 10; // 正确,普通引用不行 const int& rc = ra * 2; // 正确,普通引用不行
实际上,只要被引用的类型能够转换为常量引用的类型即可
double dval = 3.14; const int& pi = dval; // 可用
在上面一句引用语句中,编译器实际上相当于执行了以下两条语句:
const int temp = dval; // 生成一个临时整形常量
const int& pi = temp; // 给常量引用初始化
在这种情况下,常量引用实际上是绑定了一个临时量(temporary)对象。也就是说,允许常量引用指向一个临时量对象。
假设pi不是常量引用,就允许对它赋值,改变pi所引用的对象的值。但此时绑定的对象是一个临时量而非 dval,不能改变dval的值。在const int& pi = dval
中,我们既然想让pi引用dval,就肯定想通过pi改变 dval的值。如此看来,基本上在使用时不会想着把引用绑定到临时量上,C++ 语言也就把这种行为归为非法。
也就是说,不允许一个普通引用与字面值或者某个表达式的计算结果,或类型不匹配的对象绑定在一起,其实就是不允许一个普通引用指向一个临时变量,只允许将常量引用指向临时对象。
常量引用传参
在函数参数中,使用常量引用很重要。因为函数有可能接收临时对象,并且同时要禁止对引用的对象进行修改。
int test(){ return 1; }
void func1(int& x){ std::cout << x << std::endl; }
void func2(const int& x){ std::cout << x << std::endl; }
int main()
{
int m = 1;
func1(m); // 正确
func1(1); // 错误
func1(test()); // 错误
func2(m); // 正确
func2(1); // 正确
func2(test()); // 正确
}
日常使用是在写模板的时候,不知道会传入什么类型的模板参数,就必须要让模板适配所有的情况。一个 const T & 就是一种能够承载所有输入类型的办法。但一个 T& 就做不到接收字面量作为参数,字面量无法直接转化为非常量引用。当我们需要constT&或者T&作为返回值的时候,问题会更多。
本质上也就是const T& 能够承载右值(字面量),而单纯的T&只能承载一个变量的引用,不能存放一个字面量。接下来应该还有一篇:C++ 11的右值引用。