编译器是如何实现const
关键字功能的
const
用于声明变量
const
定义的变量只有类型为整数或枚举,且以常量表达式初始化时,在其它地方使用该变量的地方才会被以常量替换。其他情况下它只是一个const
限定的变量。但它们都分配了内存地址,把它们统称为常量。
修饰全局变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15struct Point
{
int x;
int y;
};
// 全局
const Point p{1, 2};
void func()
{
Point *pp = (Point *)&p;
pp->x = 3; // error!
// Exception has occurred. Segmentation fault
}未被
const
修饰的全局变量默认为extern
,不需要extern
显式声明即可以在其它文件中访问!而全局const
常量需要显式声明extern
,并且需要做初始化,才能在其它文件中访问!因为常量在定义后就不能被修改,所以定义时必须初始化。虽然可以编译通过(骗过了编译器)。但上述全局
const
修饰的变量p
会被编译器存放在ELF
文件的.rodata
分区(只读),程序默认不拥有写权限,故运行时不可更改。接下来,控制变量
p
的const
功能就交给操作系统中内存分页机制了。操作系统会将.rodata
所在的内存页的权限标记为只读。每当程序访问内存时,CPU
都会检查内存地址对应的权限。如果权限不符,那么CPU
就会产生中断并调用操作系统所设置的中断处理例程。在这个例子中,当CPU
发现程序想要写一块只读内存时,就会产生中断。而Linux
设置的默认动作是终止程序,并打印 “Segmentation fault (core dumped)
”。修饰局部变量
这时只是编译器负责检查你有没有显式的通过
p
来修改const
修饰的变量p
的值。但是你可以通过其它技巧骗过编译器来修改。比如:1
2
3
4
5
6
7
8// 局部
void func()
{
const Point p{1, 2};
p.x = 3; // error!
Point *pp = (Point *)&p;
pp->x = 3; // correct!
}编译器的实现为,函数内变量放在函数的栈帧里的,程序拥有对这个存储区自由读写的权限。
修饰类的成员变量
编译器的实现和前面说的修饰函数内局部变量一样。但类中的
const
成员变量必须通过初始化列表进行初始化。1
2
3
4
5
6
7
8
9
10
11
12
13struct Point
{
const int x;
int y;
Point(int x_, int y_) : x(x_), y(y_) {}
};
void func()
{
const Point p(1, 2); // p 对象中 x = 1, y = 2
int *pi = (int *)&p;
*pi = 3; // // p 对象中 x = 3, y = 2
}通过指针竟然可以修改类对象内声明为
const
的对象。细思极恐,,这其实就是指针的被广为诟病的地方了。修饰函数形参
value with const
1
2void func(const int val); // 传递过来的参数不可变
void func(int *const p); // 指针本身不可变编译器的实现跟前面讨论的修饰函数内局部变量一样。
reference or pointer with const
1
2void strcpy(char *dst, const char *src); // 参数指针所指内容为常量不可变
void func(const A &a) // 参数为引用, 为了增加效率同时防止修改
修饰函数返回类型
value with const
1
const Point func1(); // 无意义, 因为参数返回本身就是赋值给其他的变量!
reference or pointer with const
1
2const Point* func2(); // 指针指向内容不可变
const Point& func2(); // 引用的内容不可变
const
用于声明函数
修饰类的成员函数
被
const
修饰的类对象,只能访问类中const
成员函数。1
2
3
4
5
6
7
8
9
10struct Point
{
const int x;
int y;
Point(int x_, int y_) : x(x_), y(y_) {}
int getX() const
{
return x;
}
};编译器会将被
const
修饰的成员函数getX()
转化为:1
2
3
4
5
6
7
8
9
10
11
12
13// 转化为下面这样
// 前面的那个 const_ 才是 const 修饰起到的作用, 后面的本身就有
int getX(const_ Point* const this)
{
return this->x;
}
// 这样的调用
Point p(1, 2);
p.getX();
// 转化为下面这样
getX(&p);也就是说,
const
修饰类成员函数时,修饰的只是传进来的this
指针而已。
const char* str
、char* str
、char str[]
、const char str[]
的区别
1 | /* 注意, 下面的讨论都是把此定义放在 全局 */ |
编译器是如何实现强制类型转换的
关于强制类型转换的分类和使用参考《Effective C++
》条款27
。
进行强制类型转换后,内存空间里面原变量的内容是不会发生改变的,改变的是运算时产生的临时数据对象的类型,是你去读取这个内存空间时的解析方法。
从编译原理的角度去看,C++
编译器会维护一份程序中所有变量的名称和其类型之间的一个映射表。通过变量名称去操作内存空间时,会查看这个映射表,获取变量所属的类型之后再决定操作的内存范围。当使用强制类型转换时,会首先改变这个临时数据对象的类型,再去操作内存。