深入分析const关键字模型

来源:互联网 发布:软件著作权申请登记 编辑:程序博客网 时间:2024/06/15 21:33

    • 前言
    • const和define
      • const
        • const是编译期的行为
        • const声明占用内存
        • const是伪常量
        • const和复合类型
      • 顶层const和底层const
        • 概念
        • 拷贝操作
      • const和其他关键字
        • constexpr
        • auto

前言

最近在复习c++ primer,把以前没注意到的都深入研究了一下。
此篇博客的结论都建立于c++11或者c++14的新标准上,编译器为VS2015 community版本,G++可能会有较大出入(这点笔者已经在其他博客上验证)

const和#define

c语言中的宏机制被继承到了c++,宏是一种替换行为,而且是完全的字符替换,发生在预编译期,所以在这种机制下,编译期无法对宏进行任何的类型检查,我们来看汇编中,宏是如何实现的:

#define define_var 100    int temp_num = define_var;008C6478  mov         dword ptr [temp_num],64h

可以看到,宏本身是不分配任何内存的,而是在预编译时进行字符替换。

值得庆幸的是,现代的大多数IDE,都能在写代码阶段就判断程序员的大多数错误,当然也包括了宏内部的类型检查。

const

const被使用至今,已经从当初宏的替代品,发展成了一个很复杂的东西,特别是从c++11和14引入弱类型的特性(auto和delctype等关键字)后,const的使用稍不注意就会发生很多错误。

const是编译期的行为

const是一种编译期的行为,在编译期内,具有类型,会执行类型检测,所以他能由编译器指出程序在运行之前的错误,关于这点,我会在后面给出验证。

const声明占用内存

最初const和宏最大的区别可能就在于此,以前有种论调认为const声明的变量位于程序的符号表中,经笔者证明,并非是这样,或者说并非仅仅是这样。
至于const占用内存,我们可以从下面一段汇编中看出来:

    const int ci = 0;01081FA8  mov         dword ptr [ci],0      const int &i = 0;01081FC2  mov         dword ptr [ebp-48h],0  01081FC9  lea         eax,[ebp-48h]  01081FCC  mov         dword ptr [i],eax  

我们知道,直接用常量表达式初始化引用是不合法的,但是常量引用是可以是被任意表达式初始化的,我们可以看到程序提前声明了一块空间用于存放“0”,然后再将引用绑定到指定地址。

const是伪常量

const的设计非常奇怪,虽然在编译期进行严格的类型检查,但是却不在运行期给予任何保障,而且最重要的是,允许常指针类型转化为普通指针类型,引用类型与之相同,请看下面的语句:

const int cVar = -100;    const int* p = &cVar;    int* x = (int*)&cVar;    *x = 5;    cout << "cVar: " << cVar << endl;    cout << "p: " << *p << endl;    cout << "x: " << *x << endl;    cout << "origin rom: " << &cVar << endl;    cout << "p rom: " << p << endl;    cout << "x rom: " << x << endl;

输出结果是:

cVar: -100
p: 5
x: 5
origin rom: 00AFF988
p rom: 00AFF988
x rom: 00AFF988

笔者刚看到这一块的时候也觉得很奇怪,有两个奇怪的点:

1、虽然内存相同,但是输出的值却不同,分析汇编之后才得出结论,由于输入输出流的汇编有点麻烦,我们看这一句:

    int test = cVar;00912610  mov         dword ptr [test],0FFFFFF9Ch  

我们可以看到,虽然cVar在初始化的时候分配了内存,但是当编译器在遇到cVar这个字符串的时候,还是采取了和宏相同的方案——进行展开替换。

2、常量的值被指针修改了,其实这也理所应当,const的机制没有对内存有任何的操作,一旦进入运行期,存放const变量的内存却没有任何标记证明它是不可修改的,自然指针会把它当做普通内存来处理,这一点笔者不是很明白语言设计者的思路,为什么要允许这种强制转换?所以c++的灵活性也常常为人所诟病——太过灵活导致太容易出bug。

notice:有的内存块是只能写的(比如main函数之外的const申请的就是这样的内存),这时如果用指针修改常量会发生错误

const和复合类型

const和指针、引用笔者不想多说,因为这些全是一些很生硬的规则,如果读者还不清楚常量指针和 指向常量的指针等知识,可以去翻阅c++primer p54-p57

顶层const和底层const

概念

顶层const和底层const是为了方便常量的赋值,类型推断等操作而提出的概念。
一般来讲,所有非复合类型的常对象,是一个顶层const,表示该对象本身是一个常量。
而复合类型,如果绑定的对象是常量,我们称其为底层const,如果我们说这种绑定关系是恒定不变的,那么叫做顶层const。

const int *p 指向常量的指针,这是底层const
int *const p 常量指针,这是顶层const
const int *const p,第一个const为底层const,第二个是顶层const
由于引用本身就是固定的绑定关系,所以常引用都是底层const

拷贝操作

在拷贝(赋值)操作时,顶层const被忽略,但是拷入对象和拷出对象必须拥有相同的底层const资格,具体如下:

  • 普通指针不能直接绑定常对象。
    虽然常对象本身是顶层的const,但是关于指针的赋值操作,其实是先让一个寄存器绑定到对象,然后把寄存器的地址赋值给指针,而寄存器是一个底层的const,普通指针没有底层的const,汇编如下:
    int originVar = 0;0091249F  mov         dword ptr [originVar],0      int* pO0 = &originVar;009124A6  lea         eax,[originVar]  009124A9  mov         dword ptr [pO0],eax  
  • 普通的引用不能绑定到常对象上。
    这一点和指针理由相同。

  • 总结来说,一般情况,非常量可以转换为常量,反之不行。

const和其他关键字

const本身就已经有很多复杂的行为,当它和其他关键字混合使用时,将产生更多的误区。

constexpr

关于constexpr笔者使用得很少,这里提出一点:const int *p指向常量的指针,而constexpr int *p却是指向int的常量指针。

auto

c++11很重要的类型推断特性,当auto用于推断const对象时,会忽略顶层const,保留底层const,这一点和拷贝操作时一样,但是要注意指针和引用的赋值结构。

0 0
原创粉丝点击