游戏开发工具

我们知道,指针就是数据或代码在内存中的地址,指针变量指向的就是内存中的数据或代码。这里有一个关键词需要强调,就是内存,指针只能指向内存,不能指向寄存器或者硬盘,因为寄存器和硬盘没法寻址。

其实 C++ 代码中的大部分内容都是放在内存中的,例如定义的变量、创建的对象、字符串常量、函数形参、函数体本身、newmalloc()分配的内存等,这些内容都可以用&来获取地址,进而用指针指向它们。除此之外,还有一些我们平时不太留意的临时数据,例如表达式的结果、函数的返回值等,它们可能会放在内存中,也可能会放在寄存器中。一旦它们被放到了寄存器中,就没法用&获取它们的地址了,也就没法用指针指向它们了。

下面的代码演示了表达式所产生的临时结果:

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 下编译失败。这说明:

1、在 GCC 下,引用不能指代任何临时数据,不管它保存到哪里;

2、在 Visual C++ 下,引用只能指代位于内存中(非代码区)的临时数据,不能指代寄存器中的临时数据。

引用作为函数参数

当引用作为函数参数时,有时候很容易给它传递临时数据。下面的 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;
}