C++|怪异的const及其不可变性、可修改性、连接性

服务器

  const的最初动机是取代预处理器#define来进行值替代,可用于修饰指针、函数参数和返回值、类对象及成员等。 在C中,对于一些无法理解的重复出现的字面数值常量,使用#define是一个不错的选择,但因为其只是简单的值替代,没有类型检查是其被人诟病之所在。 C++之父Bjarne Stroustrup在《The design and evolution of C++》一书中详细阐述了const的由来。 “在操作系统中,常常能见到人们用两个二进制位直接或间接地对一块存储区进行访问控制,其中用一个位指明某个用户能否在这里写,另一个指明该用户能否从这里读。我觉得这种思想可以直接用到C++中,因此也考虑过允许把一个类型描述为readOnly或者writeOnly。” “直到现在,在C语言中还不能规定一个数据元素是只读的,也就是说,它的值必须保持不变。也没有任何办法去限制函数对传给它的参数的使用方式……readOnly运算符可用于防止对某些位置的更新。它说明,对于所有访问这个位置的合法手段而言,只有那些不改变存储在这里的值的手段才是真正合法的。” “我在离开这次会议时得到的是同意(通过投票)把readOnly引进C语言(而不是C with Classes 或者C++),但把它另外命名为了const。” “const来表示常数,可以成为宏的一种有用替代物,但要求全局const隐含地只在它所在的编译单元起作用。因为只有在这种情况下,编译器才能很容易推导出这些东西的值确实没有改变…把简单的const用到常量表达式中,并可以避免为这些常量分配空间。” 常量表达式中的值不必为其分配内存空间,与程序代码存储在一起。编译器通常不为普通const只读变量分配存储空间,而是将它们保存在符号表中,这使它成为一个编译期间的值,没有了存储与读内存的操作,使得它的效率也更高。 “因为C语言中的const不能用在常量表达式中,这就使const在C语言中远没有在C++中那样有用。” (在C中,const用来限定一个变量是只读的,即不可变的。默认具有外部链接,并分配内存空间。) 1 const外部变量的链接性 const修饰外部变量时,其作用域是整个文件,默认为内部连接(因为其不能完全避免不分配存储空间,否则,众多的const在cpp文件中分配内存,会导致连接错误)。 为了使const成为外部连接以便让另外一个文件可以对它引用,必须明确把它定义成extern。extern const强制进行了空间分配,extern意味着使用外部链接,因此必须分配存储空间(当然就要考虑重复定义的问题了)。 #define的值替换是放到头文件供其它cpp文件包含的,同样f ,const声明的常量也要能放到头文件中,头文件中只能是声明,是要避免内存分配的,而编译器也是这样做的,将它们保存在符号表中,但当涉及到与地址相关的操作时,不得已会分配内存,所以安全的做法就是内部链接性了。这就是其独特所在,在头文件中,他看起来好像是一个定义,其实只是一个符号常量,还具有内部链接性。 2 const局部变量的copy propagation及间接修改性 const局部变量,存储在栈区,不可直接修改,可通过指针间接修改: #include <iostream>using namespace std;const int gci = 55; // const全局变量,常量区,一旦初始化不可直接或间接修改int main(){ const int ci = 2*5;// const局部变量,栈区,可间接修改,不可直接修改 int *pi = (int*)&ci; *pi = 20; int *pgci = (int*)&gci; //*pgci = 66; // error cout << &gci << l; // 0046F01C cout << &ci << " " << pi <<l; // 0012FF44 0012FF44cout << ci << " " << *pi <<l; // 10 20 while(1); return 0;}/*0046F01C0012FF44 0012FF4410 20*/这时会发现&ci和&pi的值(地址)一样,而ci和*pi的值不一样,就是说在相同的地址下的内存空间有不同的值。 ?为什么呢? ? 因为在编译期间进行了优化,即常量传播(copy propagation),又称常量替换,程序在编译过程中经过两种过程: 一是const folding,上例中const int ci = 2*5; ?计算表达式的值时,编译器将2 * 5算成10,这个过程被称为const folding。 二是copy propagation,编译器在遇到ci这个标识符时,自动将ci替换成10,而不是去相应地址中取值,这个过程就是copy propagation。 上例就是这两个过程的结果,在定义了const int ci后,后面代码中遇到ci的地方,编译器就自动将ci替换成10。 常量折叠是在编译时间简单化常量表达的一个过程。简单来说就是将常量表达式计算求值,并用求得的值来替换表达式,放入常量表。 常量传播和常量折叠都是编译器的优化手段(目的是在不影响程序正常逻辑的前提下,尽可能生成立即数寻址的目标代码,减少内存的访问次数)。VS下开启O2优化后,会进行常量传播和折叠。当然,针对的是内建基本类型,而不是复合结构类型, 因为编译器不会将一个复合结构类型保存到符号表,所以会分配存储空间。 当使用volatile关键字修饰变量时可以禁止编译器这一优化,每次读都会经过寻址从原地址空间取得变量当时的真实值。(所以使用这个关键字时会对性能有一定影响。) #include <iostream>using namespace std;int main() { volatile const int a = 10; volatile int *p = (int *) &a; cout << *p << " " << a << l; cout << p << " " << &p << l; *p = 20; cout << *p << " " << a << l; cout << p << " " << &p << l; while(1); return 0;}/*output10 101 0x7fff4b0a9ac020 201 0x7fff4b0a9ac0修饰函数形参时,也可间接修改: const int showValue(const int & f) { //f = 1000; int *p = (int*)&f; *p = 100; cout << f << " " << &f<< l; // 100 0012FECC return f;}void test(){ int a = 10; showValue(a); cout << a <<" "<< &a << l; //100 0012FECC ,地址和上面相同}const int showValue2(const int *f) { // *f = 1000; // error int *p = const_cast<int *>(f); *p = 200; cout << *f <<" " << f<< l; // 200 0012FECC return *p;}void test2(){ int a = 20; showValue2(&a); cout << a <<" "<< &a << l; //100 0012FECC ,地址和上面相同}3 编译时常量与运行时常量 编译时常量在编译后变量值已经确定,程序运行时常量值保持不变。 而运行时常量在程序运行时每次值都不一样。 例如: #include <iostream>using namespace std;const int cmpl = 5+1; // 编译时常量,不涉及存储空间int read_at_runtime(){ int val; cin >> val; return val; }const int rt = read_at_runtime(); // 运行时常量,涉及到存储空间int main(){ cout<<rt<<" "<<cmpl<<l; int arr[cmpl]; //int ar2[rt]; // error while(1); return 0;}以下几种情况不能使用运行时常量: I 数组边界; II switch case表达式; III 位域长; IV 枚举初始化; V 模板的非类型参数赋值。 所以判断某个const常量是不是一个编译器常量的简单方法就是用其声明一个静态数组: const int size = 55;int arr[size];4 const常量分配内存空间的几种特殊情况 const将常量放到符号表,才是一个真正意义上的常量,特殊情况下,如果要分配存储空间,则是声明这块存储空间为只读,此时的const就等同于C编译器的做法。 在通常情况下编译器是不会为const对象分配内存,只有在几种情况下会分配地址: 4.1 extern和const同时使用,使得变量具有了外部链接属性。 4.2 涉及const对象地址相关操作时。此时会强制分配内存地址。 4.3 runtime的const,编译器是需要为它分配空间的,而且也不在符号表里面记录相关信息。 4.4 对于自定义数据类型 ,也会分配内存,可以用指针修改值 #include <iostream>using namespace std;struct Person{ char name[12];// 如果是string name;因其是聚合数据时不能直接初始化 int age;};int main(){ const Person p = {"wwu",18}; int *ptr = const_cast<int*>(&p.age); *ptr = 18; cout<<p.age<<l; // 28 while(1); return 0;}5 C中的const 在C中,const修饰的标识符有分配存储空间,但限定为只读,默认为外部链接。 而在C++中,const修饰的对象是否分配存储空间取决于对它如何使用。一般说来, 如果一个const仅仅用来把一个名字用一个值代替(如同使用#define一样), 那么该存储空间就不必创建。要是存储空间没有创建的话(这依赖于数据类型的复杂性以及编译器的性能),在进行完数据类型检查之后,为了代码更加有效, 值也许会折叠到代码中,这和以前使用#define不同。 不过, 如果取一个const的地址(甚至不知不觉地把它传递给一个带引用参数的函数)或者把它定义成extern, 则会为该const创建内存空间。 C编译器不能把const看成一个编译期间的常量,在C中,如果写: const int bufsize = 11;char buf[bufsize]; // error因为bufsize占用了某块内存,所以C编译器不知道它在编译时的值,而声明静态数组需要一个编译器的常量。 在C中,在函数外声明 const int bufsize; 是被允许的,因为在C中,const默认为外部链接,如果此标识符在另外另有定义,则此处是声明,否则就是定义,被初始化为0。 在C++则不被允许,声明和定义要严格区分,怎样区分?通过初始化: extern const int i; // 声明,表明i在另处有定义extern const int i = 55; // extern是外部连接声明,初始化表明此处是定义const int k = 66; // 初始化表明此处是定义,默认为内部链接所以在C++中,const修饰的标识符,要么是extern声明,要么有初始化。 当进行了extern const声明时, 编译器就不能够进行常量折叠了, 因为在分配了存储空间,在编译期就不知道具体的值了。 6 const修饰指针变量 指针相当于变量之间的纽带,涉及到两块内存,有他型、他址和己址、己值,所以const修饰指针变量时,既可以修饰己型,也可以修饰己值(指针变量名自身),以符号*区隔即可简单区分修饰的是哪一部分。 int x = 55; int y = 88; const int *px = &x; // const在符号*前面,修饰后一部分,即他型,指针指向的内存区域的类型 int const *py = &y; // 需要同时初始化 //*px = 66; // error //*py = 99; // error px = &y; py = &x; int * const cp = &x; // const在符号*后面,修饰后一部分,即己值,指针变量自身 //cp = &y; // error *cp =11; const int const *pp = &x; //*pp = 33; //error //pp = &6; //error int *ptr = &x; px = ptr; // ptr = px; // error, 一个const指针变量只能赋给一个另一个const指针变量; py =px;7 char*指向一个字符串字面量 const char* p = "hello";char* q = "hello";这两种写法表达的意思都是一个指向hello这个常量字符串的char指针。从C语言时代起后一种写法就是如此,而到了C++时代,为了兼容以前的程序所以做了同样的规定,但是const char*这种写法相对而言更规范。 既然是常量字符串,自然其内容不能被修改,不能有以下写法: p[2] = 'i';q[2] = 'j';如果想修改,定义一个字符数组好了: char str[] = "hello"; // str是容纳"hello"的起始地址,可以是栈区,也可以是全局区 8 const修饰函数参数和返回值 对于const修饰指针或引用参数,这很好理解,表明是传址,但不更新其值。返回指针或引用也是如此,只是如果是返回const指针或引用,在做右值时,其值也需具有const属性。 但对于修饰一个传值的参数,只是表明这个初值不做更新。 如果是按值返回,返回的是一个内建类型的值,则用const修饰没有什么意义。 如果按值返回的是一个自定义类型,则用const限定其只读属性。 需要注意的是,用用一个函数调用做实参时,会产生一个临时对象,这样的临时对象,如果是一个const修饰的引用形参是可以接受的,而const修饰的指针形参才不被接受,原因是指针需要一个确切的地址,这是临时对象所欠缺的。为什么需要引用形参用const修饰其引用类型呢?因为临时对象自动赋予了const属性,C++编译器在做类型检查时,如果右值具有const属性,则要求左值也是一个const: void f(int&) {}void g(const int&) {}class X {};char str[] = "hello"; // str是容纳"hello"的起始地址,可以是栈区,也可以是全局区X f() { return X(); } // Return by valuevoid g1(X&) {} // Pass by non-const referencevoid g2(const X&) {} // Pass by const referenceint main() { // Error: const temporary created by f()://! g1(f()); // OK: g2 takes a const reference: g2(f());//! f(1); // Error,temporaries automatically const g(1);} ///:~9 const与类 const可以修饰一个类对象,表明此类对象不能更新对象的状态,也不能调用成员函数去更新对象的状态,如何确保这一点呢?就是将成员函数声明为const,const放在成员函数标识符的后面(放到前面则是修饰函数返回类型了)来做修饰。 由此,const成员函数也不同更新对象的数据成员,也不能调用非const成员函数。对于const类对象,如果某一数据成员想让const成员函数更新,怎么办?用mutable去修饰这个数据成员。还有一种变通的方法是通过const指针去更改: static_cast<ClassName*>(this))->memVar++; const数据成员只是在实例化后的对象中具有 const属性,不同的对象可以是不同的值。const修饰的数据成员在初始化列表中初始化为不同的值,当然也只能在初始化列表中初始化。 编译期间类里的常量: class StringStack { //enum{size = 100}; static const int size = 100; const string* stack[size]; int index;public: StringStack(); void push(const string* s); const string* pop();}10 const VS enum和#define 在C++中,常量有3种表达方式:cosnt、enum和#define,这3种方式有所不同: #define在编译预处理时进行数据替换,没有存储空间; enum和const是类型安全的,而#define不是; 只有enum和#define可以被用于 switch。 小结一下: ref Bruce Eckel, Chuck Allison:C++编程思想(上卷) -End-

标签: 服务器