我们知道,指针就是数据或代码在内存中的地址,指针变量指向的就是内存中的数据或代码。这里有一个关键词需要强调,就是内存,指针只能指向内存,不能指向寄存器或者硬盘,因为寄存器和硬盘没法寻址。
其实 C++ 代码中的大部分内容都是放在内存中的,例如定义的变量、创建的对象、字符串常量、函数形参、函数体本身、new或malloc()分配的内存等,这些内容都可以用&来获取地址,进而用指针指向它们。除此之外,还有一些我们平时不太留意的临时数据,例如表达式的结果、函数的返回值等,它们可能会放在内存中,也可能会放在寄存器中。一旦它们被放到了寄存器中,就没法用&获取它们的地址了,也就没法用指针指向它们了。
下面的代码演示了表达式所产生的临时结果:
- int n = 100, m = 200;
- int *p1 = &(m + n); //m + n 的结果为 300
- int *p2 = &(n + 100); //n + 100 的结果为 200
- bool *p4 = &(m < n); //m < n 的结果为 false
这些表达式的结果都会被放到寄存器中,尝试用&获取它们的地址都是错误的。
下面的代码演示了函数返回值所产生的临时结果:
- int func(){
- int n = 100;
- return n;
- }
- int *p = &(func());
func() 的返回值 100 也会被放到寄存器中,也没法用&获取它的地址。
寄存器离 CPU 近,并且速度比内存快,将临时数据放到寄存器是为了加快程序运行。但是寄存器的数量是非常有限的,容纳不下较大的数据,所以只能将较小的临时数据放在寄存器中。int、double、bool、char 等基本类型的数据往往不超过 8 个字节,用一两个寄存器就能存储,所以这些类型的临时数据通常会放到寄存器中;而对象、结构体变量是自定义类型的数据,大小不可预测,所以这些类型的临时数据通常会放到内存中。
下面的代码是正确的,它证明了结构体类型的临时数据会被放到内存中:
- #include <iostream>
- using namespace std;
- typedef struct{
- int a;
- int b;
- } S;
- //这里用到了一点新知识,叫做运算符重载,我们会在《运算符重载》一章中详细讲解
- S operator+(const S &A, const S &B){
- S C;
- C.a = A.a + B.a;
- C.b = A.b + B.b;
- return C;
- }
- S func(){
- S a;
- a.a = 100;
- a.b = 200;
- return a;
- }
- int main(){
- S s1 = {23, 45};
- S s2 = {90, 75};
- S *p1 = &(s1 + s2);
- S *p2 = &(func());
- cout<<p1<<", "<<p2<<endl;
- return 0;
- }
运行结果:0x28ff28, 0x28ff18
第10行代码用到了运算符重载,我们将在《C++运算符重载》一章中详细讲解。
诸如 100、200+34、34.5*23、3+7/3 等不包含变量的表达式称为常量表达式(Constant expression)。
常量表达式由于不包含变量,没有不稳定因素,所以在编译阶段就能求值。编译器不会分配单独的内存来存储常量表达式的值,而是将常量表达式的值和代码合并到一起,放到虚拟地址空间中的代码区。从汇编的角度看,常量表达式的值就是一个立即数,会被“硬编码”到指令中,不能寻址。
关于虚拟地址空间的分区,我们已在《Linux下C语言程序的内存布局》一节中讲到。
总起来说,常量表达式的值虽然在内存中,但是没有办法寻址,所以也不能使用&来获取它的地址,更不能用指针指向它。下面的代码是错误的,它证明了不能用&来获取常量表达式的地址:
- int *p1 = &(100);
- int *p2 = &(23 + 45 * 2);
引用和指针在本质上是一样的,引用仅仅是对指针进行了简单的封装。引用和指针都不能绑定到无法寻址的临时数据,并且 C++ 对引用的要求更加严格,在某些编译器下甚至连放在内存中的临时数据都不能指代。
下面的代码中,我们将引用绑定到了临时数据:
- typedef struct{
- int a;
- int b;
- } S;
- int func_int(){
- int n = 100;
- return n;
- }
- S func_s(){
- S a;
- a.a = 100;
- a.b = 200;
- return a;
- }
- //这里用到了一点新知识,叫做运算符重载,我们会在《运算符重载》一章中详细讲解
- S operator+(const S &A, const S &B){
- S C;
- C.a = A.a + B.a;
- C.b = A.b + B.b;
- return C;
- }
- int main(){
- //下面的代码在GCC和Visual C++下都是错误的
- int m = 100, n = 36;
- int &r1 = m + n;
- int &r2 = m + 28;
- int &r3 = 12 * 3;
- int &r4 = 50;
- int &r5 = func_int();
-
- //下面的代码在GCC下是错误的,在Visual C++下是正确的
- S s1 = {23, 45};
- S s2 = {90, 75};
- S &r6 = func_s();
- S &r7 = s1 + s2;
- return 0;
- }
第 28~33 行代码在 GCC 和 Visual C++ 下都不能编译通过,第 38~39 行代码在 Visual C++ 下能够编译通过,但是在 GCC 下编译失败。这说明:
当引用作为函数参数时,有时候很容易给它传递临时数据。下面的 isOdd() 函数用来判断一个数是否是奇数:
- bool isOdd(int &n){
- if(n/2 == 0){
- return false;
- }else{
- return true;
- }
- }
- int main(){
- int a = 100;
- isOdd(a); //正确
- isOdd(a + 9); //错误
- isOdd(27); //错误
- isOdd(23 + 55); //错误
- return 0;
- }
isOdd() 函数用来判断一个数是否为奇数,它的参数是引用类型,只能传递变量,不能传递常量或者表达式。但用来判断奇数的函数不能接受一个数字又让人感觉很奇怪,所以类似这样的函数应该坚持使用值传递,而不是引用传递。
下面是更改后的代码:
- bool isOdd(int n){ //改为值传递
- if(n/2 == 0){
- return false;
- }else{
- return true;
- }
- }
- int main(){
- int a = 100;
- isOdd(a); //正确
- isOdd(a + 9); //正确
- isOdd(27); //正确
- isOdd(23 + 55); //正确
- return 0;
- }