最近看《C++编程思想》发现自己的基础确实不牢固,也想起了以前写代码时也因为const的事情浪费过时间,这里总结下几个要点。
首先说下内部链接和外部链接。
当一个cpp文件在编译时,预处理器首先递归包含头文件,形成一个含有所有必要信息的单个源文件,这个源文件就是一个编译单元。编译器对每个编译单元(.cpp文件)进行编译生成相应.obj文件。PS:.c文件对应.o文件
接下来关键一步:链接器将所有不相关的.obj文件进行链接,生成最终可执行文件(.exe文件)
// a.cpp: #include "stdafx.h" void show(); int main() { show(); return 0; }
// b.cpp #include "stdafx.h" #include <iostream> void show() { std::cout << "Hello World" << std::endl; }
看上述代码,函数的声明和定义可以在不同的cpp文件中,并且声明可以有很多个,定义只能有一个!
在某个编译单元中调用show函数,那么必须进行声明。来看下外部链接和内部链接的定义:
外部链接:一个名称对编译单元来说不是局部的,链接的时候其他编译单元可以访问它。
内部链接:一个名词对编译单元来说是局部的,链接的时候其他编译单元无法链接到它且不会与其他编译单元的同样称冲突。
可以看出,show函数是外部链接,a.cpp中的void show();等价于extern void show();
而static和全局的变量或函数都是外部链接,而由于函数的声明跟定义明显有区别(一个后接分号一个后接大括号),所以不用特地加extern区分。
回到主题,C++的const默认是内部链接的!也就是说const仅在const被定义过的文件里才是可见的。除非用extern进行声明,才会使用外部链接。
默认内部链接时,C++编译器不会给const创建存储空间,而是把它的定义放在符号表中。这称之为常量折叠,起到了跟#宏一样的效果。当然,在运行时还是会创建的,见下述例子。
#include <iostream> int main() { const int i = 10; int* p = const_cast<int*>(&i); *p = 8; std::cout << *p << " " << i << std::endl; return 0; }
结果是8 10
说明运行时可以取得const常量的地址,并且可以改变存放的值。但是对于const常量来说,实际只是从符号表中查找值。因此虽然内容发生了变化,但是i输出还是10。
再来看看const的常见用法(下面的int均可换成其他基本数据类型或类名)
1、和指针一起使用
const int*或int const *——不能通过指针改变int的值,但是指针指向的地址可以改变,一般使用前者
int* const——不能改变指针指向的地址,但是可以通过指针改变int的值
const int* const或int const* const——不能通过指针改变int的值,也不能改变指针指向的地址
2、一般要把const int*转换成int*需要强制转换,而字符数组是没有强调const特性的,见下面代码
#include <iostream> int main() { char* pc = "hello world!"; std::cout << pc << std::endl; // 可以输出 pc[2] = ‘s‘; // 运行时错误! return 0; }
编译通过,也能输出hello world!,但是运行时出错。这里"hello world!"被编译器当作常量字符数组建立的,所以得到的只是数组在内存里的首地址,对字符数组内任何字符进行修改会导致运行时错误。
要想修改字符串需要把它放在数组中,即把char* pc改成char pc[]
3、作为函数参数和返回值
作为参数:比如void f(const int i){}假如调用f(5);相当于在函数体中const int i = 5;然后再对i进行操作,所以不能修改i。
作为返回值:比如const int f(){ return 1; }假如调用int i = f();相当于const int tmp = f(); int i = tmp;所以没有任何意义,返回值进行值传递,还是能改变。
但是返回的是对象(假如是类A的对象)时,比如const A f(); 返回值不能作为左值!(左值右值以后再详述)
虽然A g();中g()的返回值可以作为左值,但是也只是临时对象,表达式被编译过后临时对象也会被清除