0%

面试知识点详细解读之const关键字的实现

编译器是如何实现const关键字功能的

const用于声明变量

const定义的变量只有类型为整数或枚举,且以常量表达式初始化时,在其它地方使用该变量的地方才会被以常量替换。其他情况下它只是一个const限定的变量。但它们都分配了内存地址,把它们统称为常量。

  • 修饰全局变量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    struct 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分区(只读),程序默认不拥有写权限,故运行时不可更改

    接下来,控制变量pconst功能就交给操作系统中内存分页机制了。操作系统会将.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
    13
    struct 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
      2
      void func(const int val); // 传递过来的参数不可变
      void func(int *const p); // 指针本身不可变

      编译器的实现跟前面讨论的修饰函数内局部变量一样。

    • reference or pointer with const

      1
      2
      void strcpy(char *dst, const char *src); // 参数指针所指内容为常量不可变
      void func(const A &a) // 参数为引用, 为了增加效率同时防止修改
  • 修饰函数返回类型

    • value with const

      1
      const Point func1(); // 无意义, 因为参数返回本身就是赋值给其他的变量!
    • reference or pointer with const

      1
      2
      const Point* func2(); // 指针指向内容不可变
      const Point& func2(); // 引用的内容不可变
const用于声明函数
  • 修饰类的成员函数

    const修饰的类对象,只能访问类中const成员函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    struct 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* strchar* strchar str[]const char str[]的区别
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
/* 注意, 下面的讨论都是把此定义放在 全局 */

// 编译器都会将常量 "hello" 存放在只读数据段
// 而且这和前面是否使用 const 修饰没关系
// 程序持有的只是一个指针变量而已
// 所以, 通过指针修改这个串 "hello", 都会报错的
const char* str = "hello";
// 或
char* str = "hello";

// 但如果这样定义
// 此时定义的是数组变量, 有 const 的话会放在只读数据段
const char str[] = "hello";

// 没有 const 的话就会放在可写段
char str[] = "hello";

/////////////////////////////////////////
const char *s1 = "hello";
char *s2 = "world";
char s3[] = "dfasd";
const char s4[] = "asdfasdf";

void func()
{
*s1 = 'e'; // error
char *ps1 = (char *)s1;
*ps1 = 'd'; // error

*s2 = 'e'; // error
*s3 = 'e'; // correct

*s4 = 'r'; // error
char *ps4 = (char *)s4;
*ps4 = 'd'; // error
}

/* 注意, 下面的讨论都是把此定义放在 局部 */

// 和定义在全局相同
// 编译器都会将常量 "hello" 存放在只读数据段
// 而且这和前面是否使用 const 修饰没关系
// 程序持有的只是一个指针变量而已
// 所以, 通过指针修改这个串 "hello", 都会报错的
const char* str = "hello";
// 或
char* str = "hello";

// 但如果这样定义
// 此时定义的是数组变量, 有 const 的话 只会放在函数的栈帧上
// 编译器会负责检查, 不允许你显示修改 str 数组
// 可以通过前面介绍的 trick 骗过编译器来修改
const char str[] = "hello";

// 没有 const 的话, 放在函数的栈帧上, 访问就随意啦
char str[] = "hello";

/////////////////////////////////////////
void func()
{
const char *s1 = "hello";
char *s2 = "world";
char s3[] = "dfasd";
const char s4[] = "asdfasdf";

*s1 = 'e'; // error
char *ps1 = (char *)s1;
*ps1 = 'd'; // error

*s2 = 'e'; // error
*s3 = 'e'; // correct

*s4 = 'r'; // error
char *ps4 = (char *)s4;
*ps4 = 'd'; // correct
}

编译器是如何实现强制类型转换的

关于强制类型转换的分类和使用参考《Effective C++》条款27

进行强制类型转换后,内存空间里面原变量的内容是不会发生改变的,改变的是运算时产生的临时数据对象的类型,是你去读取这个内存空间时的解析方法。

从编译原理的角度去看,C++编译器会维护一份程序中所有变量的名称和其类型之间的一个映射表。通过变量名称去操作内存空间时,会查看这个映射表,获取变量所属的类型之后再决定操作的内存范围。当使用强制类型转换时,会首先改变这个临时数据对象的类型,再去操作内存。