Intro

尽管引用、指针和数组看起来像是单独的概念,但它们是密切相关的,因此本文将把它们放到一起去讨论它们的相似与区别。

引用

引用是一个用来关联一个已经存在的变量的变量。我们通过在变量类型后面添加 & 来把一个变量表示为引用。

默认情况下,函数按值传递参数(按值传递),这意味着在代码中调用函数的时候,是把参数的值复制到新的变量当中。这导致在函数中对参数的修改不会体现在原参数中。

例如,下面的代码是一个交换两个整数的值的 Swap 函数的错误实现:

1
2
3
4
5
6
void Swap(int a, int b)
{
int temp = a;
a = b;
b = temp;
}

这个函数的问题是,变量 a 和变量 b 与原参数无关,它们是独立的新创建出来的变量,只是创建时复制了原参数的值,因此交换 ab 的值不会对原参数产生任何影响。要解决这个问题,就需要把函数的参数声明为引用,如下所示:

1
2
3
4
5
6
void Swap(int& a, int& b)
{
int temp = a;
a = b;
b = temp;
}

当通过引用传递参数(按引用传递)时,函数中对参数的修改将同时影响原参数。

需要注意的是,因为引用变量 a 和引用变量 b 现在是对整数的引用,所以他们必须引用已有的变量。在代码中不能在函数中传入临时值,例如 Swap(50, 100) 是无效的。

在 C++ 中,函数中的所有参数在默认情况下都是按值传递(PASS-BY-VALUE)的,包括对象参数。相比之下,Java 等语言默认是按引用传递。

指针

要理解指针,首先要记住计算机在内存中存储变量的方式。在程序执行期间,进入函数时会在被称为的内存片段中自动为局部变量分配内存空间。这意味着函数中所有局部变量都具有 C++ 程序可查询到的内存地址。

下表显示了一些变量可能在内存中的位置。每个变量都有一个对应的内存地址。一般情况下,内存地址用十六进制数显示。

代码 变量 内存地址
int x = 50; x 0xC230 50
int y = 100; y 0xC234 100
int z = 200; z 0xC238 200

我们可以通过地址运算符(&)来查询变量的地址。例如:

1
std::cout << &y;

上面的代码将输出值 0xC234

指针是记录变量的内存地址的变量,我们通过在类型后面添加 * 来把一个变量表示为指针。

下面的代码声明了指针 p,它存储了变量 y 的内存地址:

1
int* p = &y;

下表显示了指针 p 的情况。它与其他变量一样,同时具有值和内存地址。因为它是一个指针,所以他的值是变量 y 的内存地址。

代码 变量 内存地址
int x = 50; x 0xC230 50
int y = 100; y 0xC234 100
int z = 200; z 0xC238 200
int* p = &y; p 0xC23C 0xC234

* 运算符也可以用于间接引用指针。间接引用指针可以访问指针指向的内存。

下面的代码通过间接引用指针修改了变量 y 的值。

1
*p = 42

因为间接引用指针 p 会指向内存地址为 0xC234 的内容,它对英语内存中变量 y 的位置,因此上面的代码修改了变量 y 的值。

与引用不同,指针可以不指向任何变量。指向为空的指针是空指针,我们可以通过关键字 nullptr 来声明一个空指针:

1
char* ptr = nullptr;

间接引用空指针会导致程序崩溃。

C 语言不支持引用,因此在 C 语言中不存在通过引用传递参数的概念。在 C 语言中程序员只能使用指针。例如,在 C 语言中, Swap 函数将被编写为如下形式:

1
2
3
4
5
6
void Swap(int* a, int* b)
{
int temp = *a;
*a = *b;
*b = temp;
}

然后,调用该版本的 Swap 函数的时候需要使用地址运算符:

1
2
3
int x = 20;
int y = 30;
Swap(&x, &y);

在程序执行时,引用和指针的工作方式没有区别。但是,引用必须关联一个已经存在的变量,指针可以是空指针。

许多 C++ 程序员更喜欢通过引用传递而不是通过指针传递,因为通过指针传递意味着 nullptr 是一个有效的参数。

数组

数组是同一类型的多个元素的集合。下面的代码声明了一个名为 a 的具有 10 个整数的数组,然后代码将数组的第一个元素(索引为 0)的值设置为 50:

1
2
int a[10];
a[0] = 50;

默认情况下,数组中的元素不会被初始化。虽然可以手动初始化数组中的每个元素,但使用数组初始化语法或者通过循环来进行初始化会更方便。

数组初始化语法会使用大括号,如下所示:

1
int array[5] = {0, 1, 2, 3, 4};

当元素数量过多时,最好使用循环进行初始化。下面的代码将数组中的 50 个元素都初始化为 0:

1
2
3
4
5
int array[50];
for (int i = 0; i < 50; i++)
{
array[i] = 0;
}

数组不进行边界约束性检查,请求无效索引的数组内容可能会导致内存片段损坏和其他错误。

在 C++ 中,数组被连续地存储在内存中。这意味着索引为 0 的数据紧邻着索引为 1 的数据,索引为 1 的数据紧邻着索引为 2 的数据,以此类推。下表显示了内存中元素的存储情况。数组变量(没有下标)引用索引 0 的内存地址。因此可以通过指针将数组传递给函数。

变量 内存地址
array 0xC230 0xC230
array[0] 0xC230 0
array[1] 0xC234 1
array[2] 0xC238 2

在 C++ 中还可以声明多维数组。下面的代码创建了一个四行四列的浮点数构成的 2 维数组。

1
float matrix[4][4];

如果要把多维数组传递给函数,必须显式地指定数组的尺寸规模,如下所示:

1
2
3
4
void InvertMatrix(float m[4][4])
{
// Code
}

动态内存分配

在 C++ 中,局部变量的内存分配是自动进行的,这些变量最终会被存储于被称为的内存片段中。自动分配机制对于临时变量和函数参数非常有用。但因为下面这些原因,有些时候只使用局部变量是不够的:

  1. 栈的空间是有限的。例如,Microsoft Visual C++ 编译器的默认栈大小为 1 MB。
  2. 局部变量具有固定的生命周期,它们仅可以在它们的作用域内生存。它们的作用域通常被局限在函数内,因为全局变量的使用在文体(即代码风格)上是不受欢迎的。

动态内存分配中,程序员控制变量内存的分配和释放。动态分配的变量会被存储在中。与栈相比,堆的空间要大得多(可达数 G 字节),并且在堆中存储的数据会一直存在,直到程序员删除该数据或程序运行结束。

在 C++ 中, new 运算符和 delete 运算符分别用于在堆空间中分配和释放内存。new 运算符为请求的变量类型分配内存空间,对于类和结构,new 运算符会调用它们的构造函数。delete 运算符执行相反的操作,它会调用类和结构的析构函数,并释放它们占用的内存。

例如,下面的代码为单个 int 变量动态分配内存:

1
int* dynamicInt = new int;

如果需要释放动态分配的变量占用的内存,则可以使用 delete 运算符:

1
delete dynamicInt;

如果忘记删除动态分配的变量会导致内存泄漏,这意味着被泄漏的内存在程序的剩余生命周期内将无法被使用。对于需要长时间运行的程序来说,小的内存泄漏会随着时间的推移而累积,并最终会导致堆内存耗尽。如果堆内存不足,程序会很快崩溃。

动态分配数组

如果要动态分配数组,需要在类型后面加方括号,并在括号中指定数组的大小:

1
2
char* dynamicArray = new char[4*1024*1024];
dynamicArray[0] = 32; // Set the first element to 32

要删除动态分配的数组,必须使用 delete[] 运算符显式释放内存空间:

1
delete[] dynamicArray;

值得注意的是,除了因为存储方式导致的区别以外,动态分配数组与静态分配数组还有一个重要区别是创建方式的不同。静态分配数组是在编译时期确定数组大小,数组的大小在编译时必须是一个常量表达式。动态分配数组可以在程序运行时根据需要来动态创建数组,动态数组的大小可以是变量、可计算的公式或用户输入的值。