指针,引用和动态分配内存是 C/C++语言中最强大的特性,这些特性使得程序员能够直接操作计算机中非常珍贵的记忆体资源,进而对内存进行最大性能和高效的使用。 然而指针也是一把双刃剑,它也是 C/C++编程语言中最复杂和最难的特性。

指针之所以这么强大,是因为它允许你通过地址来访问和操作对应记忆体中存储的内容。但是指针也很难被驾驭,使用好的话确实能够大大提升性能,而用的 不好的话,也会导致很多问题,例如著名的内存泄漏和缓冲区溢出,这些 bug 会致使系统发生紊乱。一些新的编程语言(例如 Java 和 C#),将指针从它的语法 中去掉,通过提供自动内存管理的方式来避免使用指针导致的许多问题。

虽然你在编写 C/C++代码的时候可以不使用指针,但是在学习 C/C++的时候很难不提及指针。指针也许不是为新手和笨蛋而设计的。

1. 指针变量

计算机记忆体位置有一个地址,对应地址处保存数据。记忆体地址通常是一个数字(一般用十六进制表示),这个数字很难被程序员直接使用。通常一个地址位置 的容量是 8-bit(也就是 1-byte),里面可以存储整数,实数,字符或者字符串,这完全取决于程序员如何解析。

为了减轻程序员使用数字地址和解析数据的负担,早期的编程语言(例如 C 语言)中产生了一种新的变量——这种变量是一个被命名了的变量的位置,它可以存储一个特定类型的值。 取代数字地址的是用名字(或者标识符)直接关联到确定的地址上,而且变量类型(如int, double, char)与之关联,从而简化了对数据的解析。

每个内存地址占 8 位(也就是 1 个字节),一个 4 个字节的int值需要 4 个内存位置。一个 32 位的系统通常使用 32 位的地址。同样的,存储这个 32 位的地址也需要 4 个内存位置。

下面的插图形象的描绘了计算机内存地址,内存中数据,变量名,变量类型以及变量值之间的关系。

1.1 指针变量

指针变量(简称指针)基本上跟其他变量一样,都可以用来存放数据,但是跟普通变量不同的是,普通变量存储的是数值,而指针存放的是内存地址。

1.2 申明指针

指针在使用前必须先申明。申明指针的语法是在指针名前加上一个*符号。指针必须跟类型关联。

1type *ptr;
2// or
3type* ptr;
4// or
5type * ptr;

例如:

1int * iPtr;
2double *dPtr;

需要注意的是,*必须放在指针变量名前面,一个*只作用于跟在它后面的指针变量名。*在申明语句中不是一个操作符,仅仅表明跟在它后门的变量是一个指针变量。

例如:

1int *p1, *p2, i;
2int* p1, p2, i;
3int * p1, * p2, i;

指针变量名的命名规则:用"p"或者"ptr"作为前缀或后缀。

1.3 使用取址操作符(&)初始化指针

当你申明一个指针的时候,它并没有被初始化。也就是说,它指向一个不确定的非法地址,这是很危险的。你需要通过给它赋值为一个合法地址来对它进行初始化,而要完成这一 操作,需要使用取址操作符(&)。

取址操作符(&)作用于变量,返回该变量的地址。例如,如果number是一个int类型的变量,那么&number返回的就是这个变量的地址。

1int number = 80;
2int *pNumber;
3pNumber = &number;
4
5int *pAnother = &number;

如上图所示,int型变量number的起始地址是0x22ccec,其中存放了一个int值 88。表达式&number返回该变量的地址,也就是0x22ccec,然后赋值给了指针变量pNumber。 取址操作符只能用在 RHS(right hand side).

1.4 间接寻址或解引操作符(\*)

间接寻址操作符(或解引操作符)(*)作用于指针变量,返回存放在指针指向地址处的数据。例如,如果pNumber是一个int型指针,*pNumber返回指针pNumber指向的int数值。

1int number = 88;
2int *pNumber = &number;
3cout << pNumber << endl;
4cout << *pNumber << endl;
5*pNumber = 99;
6cout << *pNumber << endl;
7cout << number << endl;

需要注意的是pNumber存放的是内存地址,也就是说*pNumber是指指针存放的地址处存放的数值,或者也可以说是指针指向的数值。

正如前面插图描绘的,变量直接引用数值,而指针通过内存地址间接引用数值。而这种间接引用被称为间接寻址或解引。

解引操作符既能被用于 RHS(temp = *pNumber),也可以被用于 LHS(*pNumber = 99).

注意:*在申明语句中和表达式中有不同的含义,在申明语句中,它表示跟在它后门的变量是一个指针变量,而在表达式中,它指的是指针指向的数值。

1.5 指针也有类型

指针在申明的时候就要确定它所关联的类型。指针只能保存它所申明的类型的变量的地址。

 1int i = 88;
 2double d = 55.66;
 3int *iPtr = &i;
 4double *dPtr = &d;
 5
 6iPtr = &d; // ERROR, cannot hold address of different type
 7dPtr = &i; // ERROR
 8iPtr = i;  // ERROR, pointer holds address of an int , NOT int value
 9
10int j = 99;
11iPtr = &j; // You can change the address stored in a pointer.

示例

 1#include <iostream>
 2
 3using namespace std;
 4
 5int main(int argc, char *argv[])
 6{
 7	int number = 88;
 8	int *pNumber;
 9	pNumber = &number;
10
11	cout << pNumber << endl;
12	cout << &number << endl;
13	cout << *pNumber << endl;
14	cout << number << endl;
15
16	*pNumber = 99;
17	cout << pNumber << endl;
18	cout << &number << endl;
19	cout << *pNumber << endl;
20	cout << number << endl;
21
22	cout << &pNumber << endl;
23	return 0;
24}

运行结果为:

 1→ g++ test_pointer.cpp  && ./a.out
 20x7ffee123c04c
 30x7ffee123c04c
 488
 588
 60x7ffee123c04c
 70x7ffee123c04c
 899
 999
100x7ffee123c040

注意:你得到的地址可能跟我得到的不同,操作系统将程序加载到可用的空闲地址,而不是固定的地址。

1.7 空指针

你可以将指针初始化为0或者NULL,那么它将指向空,我们称之为空指针。解引空指针会引发STATUS_ACCESS_VIOLATION异常。

1int *iPtr = 0;
2cout << *iPtr << endl; // ERROR! STATUS_ACCESS_VIOLATION exception
3
4int *p = NULL;

2. 引用变量

C++增加了一种叫做引用变量(或者简称为引用)的特性。一个引用就是一个已有变量的别名。例如,假设给paul起了个引用(别名)peter,那么peterpaul都指的是同一个人。

引用的主要作用就是作为函数的形参以实现按引用传递(pass-by-reference)的特性。当一个引用变量传递给函数的时候,函数作用于该变量的原始值(而不是变量的拷贝)。对于该变量的操作 会同时反映到函数内和函数外部。

引用类似于指针,在一些情况下,引用可以取代指针,尤其是作为函数参数的时候。

2.1 引用(别名)(&)

回顾前面讲到的,C/C++使用&符号在表达式中作为取址操作符,而 C++赋予了它额外的含义,在 C++的申明语句中,可以用&申明一个引用变量。

&用于申明语句(包括函数形参)中的时候,它是类型标识符的一部分,用于申明一个引用变量。

1type &newName = existingName;
2// or
3type& newName = existingName;
4// or
5type & newName = existingName;

“newName"被称为"existingName"的引用或别名。此时,你可以通过"newName"或"existingName"访问变量。

 1#include <iostream>
 2
 3using namespace std;
 4
 5int main(int argc, char *argv[])
 6{
 7	int number = 88;
 8	int &refNumber = number;
 9
10	cout << number << endl;
11	cout << refNumber << endl;
12
13	refNumber = 99;
14	cout << number << endl;
15	cout << refNumber << endl;
16
17	number = 55;
18	cout << number << endl;
19	cout << refNumber << endl;
20
21	return 0;
22}

编译运行结果为:

1→ g++ test_reference.cpp && ./a.out
288
388
499
599
655
755

2.2 引用是如何工作的?

引用跟指针的工作原理一样,如图说是,引用保存的是变量的地址:

2.3 引用 vs. 指针

指针和引用是等价的,除了以下几种情况:

1 引用是一个地址的名字常量,声明的时候必须初始化

1int & iRef; // ERROR: 'iRef' declared as reference but not initialized

引用一旦建立,不能修改。

2 获取指针指向的数据,需要使用解引操作,而给指针赋值,需要用到取址操作符。而在引用中,引用和解引都是隐式的。

 1#include <iostream>
 2
 3using namespace std;
 4
 5int main(int argc, char *argv[])
 6{
 7	int number1 = 88, number2 = 22;
 8
 9	// Create a pointer pointing to number1;
10	int *pNumber1 = &number1;
11	*pNumber1 = 99;
12	cout << *pNumber1 << endl;
13	cout << &number1 << endl;
14	cout << pNumber1 << endl;
15	cout << &pNumber1 << endl;
16
17	pNumber1 = &number2;
18
19	// Create a reference (alias) to number1
20	int &refNumber1 = number1;
21	refNumber1 = 11;
22	cout << refNumber1 << endl;
23	cout << &number1 << endl;
24	cout << &refNumber1 << endl;
25
26	// refNumber1 = &number2; // Error! Reference cannot be re-assigned
27
28	refNumber1 = number2;
29	number2++;
30	cout << refNumber1 << endl;
31	cout << number1 << endl;
32	cout << number2 << endl;
33	return 0;
34}

编译运行结果:

 1→ g++ test_pointer_reference.cpp && ./a.out
 299
 30x7ffeeacc503c
 40x7ffeeacc503c
 50x7ffeeacc5030
 611
 70x7ffeeacc503c
 80x7ffeeacc503c
 922
1022
1123

引用为已有的变量提供了一个别名。引用包含了隐式的解引操作,所以不需要使用*操作符来显式解引。而指针变量存储的是地址,你可以修改它存储的地址,访问指针指向的数据需要显式解引操作。引用可以被视为常量指针,它在申明的时候必须初始化,而且它的值不能被修改。 引用很接近于指针,在一些情况下,引用可以代替指针。引用允许你通过指针管理对象而不需要使用指针的语法。

2.4 引用参数 vs. 指针参数,两种实现 Pass-By-Reference 的方式

Pass-by-Value

在 C/C++中,默认情况下,函数参数是按值传递给函数的。也就是说,函数中接收的是参数的一份拷贝,在函数体内对参数的操作不会影响到函数体外部,换句话说,被呼叫的函数没有访问呼叫者的变量:

 1#include <iostream>
 2
 3using namespace std;
 4
 5int square(int);
 6
 7int main(int argc, char *argv[])
 8{
 9	int number = 8;
10	cout << "In main():" << &number << endl;
11	cout << number << endl;
12	cout << square(number) << endl;
13	cout << number << endl;
14	return 0;
15}
16
17int square(int n) {
18	cout << "In square():" << &n << endl;
19	n *= n;
20	return n;
21}

编译运行:

1→ g++ call_by_value.cpp && ./a.out
2In main():0x7ffee38b909c
38
4In square():0x7ffee38b904c
564
68

通过指针参数实现 Pass-by-Reference

有时候,我们希望直接修改原始对象来避免没必要的拷贝,这样就需要使用到 pass-by-reference,下面是使用指针实现 pass-by-reference 的例子:

 1#include <iostream>
 2
 3using namespace std;
 4
 5void square(int *);
 6
 7int main(int argc, char *argv[])
 8{
 9	int number = 8;
10	cout << "In main():" << &number << endl;
11	cout << number << endl;
12	square(&number);
13	cout << number << endl;
14	return 0;
15}
16
17void square(int *pNumber) {
18	cout << "In square():" << pNumber << endl;
19	*pNumber *= *pNumber;
20}

编译运行:

1→ g++ call_by_ref_with_pointer.cpp && ./a.out
2In main():0x7ffee23e20ac
38
4In square():0x7ffee23e20ac
564

译者注:

在 C/C++中,使用指针参数并不能称作Pass-by-Reference,因为指针作为参数的实质还是call-by-value,因为指针存放的是地址, 所以指针的拷贝中存放的也是同样的地址,对指针指向的数据的操作会改变到函数外部的数值。验证函数是否为call-by-ref,实参的地址,内外是否相同:

 1#include <iostream>
 2
 3using namespace std;
 4
 5void foo(int *pNumber);
 6
 7int main(int argc, char *argv[])
 8{
 9	int a = 10;
10	int *pNumber = &a;
11	cout << &pNumber << endl;
12	foo(pNumber);
13	return 0;
14}
15
16void foo(int *pNumber) {
17	cout << &pNumber << endl;
18}

运行结果为:

1→ g++ test.cpp && ./a.out
20x7ffee59c80c0
30x7ffee59c8088

使用引用实现 Pass-by-reference

除了上面使用指针的方式实现 Pass-by-Reference,你还可以使用引用实现。

 1#include <iostream>
 2
 3using namespace std;
 4
 5void square(int &);
 6
 7int main() {
 8	int number = 8;
 9	cout << "In main():" << &number << endl;
10	cout << number << endl;
11	square(number);
12	cout << number << endl;
13}
14
15void square(int &rNumber) {
16	cout << "In square():" << &rNumber << endl;
17	rNumber *= rNumber;
18}

“const” 参数

函数的形式参数如果被"const"修饰的话,那么实参在函数体内部是不能被修改的。 被"const"修饰的形参既能接收"const"类型的实参也能接收非"const"类型的实参,而一个非"const"类型的引用/指针参数,只能接收 非"const"类型的实参:

 1#include <iostream>
 2
 3using namespace std;
 4
 5int squareConst(const int number) {
 6	// number *= number; // error: assignment of read-only parameter
 7	return number * number;
 8}
 9
10int squareNonConst(int number) {
11	number *= number;
12	return number;
13}
14
15int squareConstRef(const int & number) {
16	return number * number;
17}
18
19int squareNonConstRef(int * number) {
20	return number * number;
21}
22
23int main() {
24	int number = 8;
25	cout int constNumber = 9;
26	cout << squareConst(number) << endl;
27	cout << squareConst(constNumber) << endl;
28	cout << squareNonConst(number) << endl;
29	cout << squareNonConstRef(constNumber) << endl;
30	cout << squareConstRef(number) << endl;
31
32	cout << squareConstRef(constNumber) << endl;
33	cout << squareNonConstRef(number) << endl;
34	// cout << squareNonConstRef(constNumber) << endl;
35       // error: invalid initialization of reference of
36       //  type 'int&' from expression of type 'const int'
37
38	return 0;
39}

2.5 函数的返回值

函数返回引用

函数的返回值既可以是引用也可以是指针,例如:

 1/* Passing back return value using reference (TestPassByReferenceReturn.cpp) */
 2#include <iostream>
 3using namespace std;
 4
 5int & squareRef(int &);
 6int * squarePtr(int *);
 7
 8int main() {
 9   int number1 = 8;
10   cout <<  "In main() &number1: " << &number1 << endl;  // 0x22ff14
11   int & result = squareRef(number1);
12   cout <<  "In main() &result: " << &result << endl;  // 0x22ff14
13   cout << result << endl;   // 64
14   cout << number1 << endl;  // 64
15
16   int number2 = 9;
17   cout <<  "In main() &number2: " << &number2 << endl;  // 0x22ff10
18   int * pResult = squarePtr(&number2);
19   cout <<  "In main() pResult: " << pResult << endl;  // 0x22ff10
20   cout << *pResult << endl;   // 81
21   cout << number2 << endl;    // 81
22}
23
24int & squareRef(int & rNumber) {
25   cout <<  "In squareRef(): " << &rNumber << endl;  // 0x22ff14
26   rNumber *= rNumber;
27   return rNumber;
28}
29
30int * squarePtr(int * pNumber) {
31   cout <<  "In squarePtr(): " << pNumber << endl;  // 0x22ff10
32   *pNumber *= *pNumber;
33   return pNumber;
34}

不要返回函数体内本地变量的引用

 1/* Test passing the result (TestPassResultLocal.cpp) */
 2#include <iostream>
 3using namespace std;
 4
 5int * squarePtr(int);
 6int & squareRef(int);
 7
 8int main() {
 9   int number = 8;
10   cout << number << endl;  // 8
11   cout << *squarePtr(number) << endl;  // ??
12   cout << squareRef(number) << endl;   // ??
13}
14
15int * squarePtr(int number) {
16   int localResult = number * number;
17   return &localResult;
18      // warning: address of local variable 'localResult' returned
19}
20
21int & squareRef(int number) {
22   int localResult = number * number;
23   return localResult;
24      // warning: reference of local variable 'localResult' returned
25}

上面的程序有一个严重的逻辑错误,因为函数体内的本地变量的引用被函数返回。本地变量的作用于只在函数体内 ��������������� 放函数退出的时候,函数体内的本地变量就会被销毁。 GCC 编译器足够聪明,在编译上述代码的时候会给出警告(但不是错误)。

将动态申请的内存作为引用返回

 1/* Test passing the result (TestPassResultNew.cpp) */
 2#include <iostream>
 3using namespace std;
 4
 5int * squarePtr(int);
 6int & squareRef(int);
 7
 8int main() {
 9   int number = 8;
10   cout << number << endl;  // 8
11   cout << *squarePtr(number) << endl;  // 64
12   cout << squareRef(number) << endl;   // 64
13}
14
15int * squarePtr(int number) {
16   int * dynamicAllocatedResult = new int(number * number);
17   return dynamicAllocatedResult;
18}
19
20int & squareRef(int number) {
21   int * dynamicAllocatedResult = new int(number * number);
22   return *dynamicAllocatedResult;
23}

2.6 总结

指针和引用掌握起来都很复杂,但是它们能提高程序的效率。所以一定要深入理解,合理使用。

3. 动态内存分配

3.1 new 和 delete 操作符

除了定义一个int型变量,然后将其地址赋值给int指针,记忆体也可以在运行时通过new操作符动态分配。 在 C++里,但凡只用new动态分配的记忆体,都需要使用delete来释放,也就是将内存返还给堆。

new操作符返回指向分配的记忆体的指针,delete操作符需要一个指针作为唯一参数。

 1// Static allocation
 2int number = 88;
 3int * p1 = &number;  // Assign a "valid" address into pointer
 4
 5// Dynamic Allocation
 6int * p2;            // Not initialize, points to somewhere which is invalid
 7cout << p2 << endl; // Print address before allocation
 8p2 = new int;       // Dynamically allocate an int and assign its address to pointer
 9                    // The pointer gets a valid address with memory allocated
10*p2 = 99;
11cout << p2 << endl;  // Print address after allocation
12cout << *p2 << endl; // Print value point-to
13delete p2;           // Remove the dynamically allocated storage

注意到,newdelete都作用于指针。

初始化一个动态分配的记忆体,可以使用基本数据类型的初始化,也可以调用构造函数来初始化对象。

 1// use an initializer to initialize a fundamental type (such as int, double)
 2int * p1 = new int(88);
 3double * p2 = new double(1.23);
 4
 5// C++11 brace initialization syntax
 6int * p1 = new int {88};
 7double * p2 = new double {1.23};
 8
 9// invoke a constructor to initialize an object (such as Date, Time)
10Date * date1 = new Date(1999, 1, 1);
11Time * time1 = new Time(12, 34, 56);

你也可以在函数内动态分配一个全局指针变量。动态分配的记忆体在函数调用后一直被占用,直到被释放。

 1// Dynamically allocate global pointers (TestDynamicAllocation.cpp)
 2#include <iostream>
 3using namespace std;
 4
 5int * p1, * p2;  // Global int pointers
 6
 7// This function allocates storage for the int*
 8// which is available outside the function
 9void allocate() {
10   p1 = new int;     // Allocate memory, initial content unknown
11   *p1 = 88;         // Assign value into location pointed to by pointer
12   p2 = new int(99); // Allocate and initialize
13}
14
15int main() {
16   allocate();
17   cout << *p1 << endl;  // 88
18   cout << *p2 << endl;  // 99
19   delete p1;  // Deallocate
20   delete p2;
21   return 0;
22}

静态分配和动态分配的主要区别:

  1. 静态分配是编译器通过内存管理器自动完成的,而动态分配,是程序员自己操作的。对于动态分配的记忆体,程序员有对这段记忆体的全部控制权。
  2. 静态分配是通过变量名管理的,而动态分配是通过指针。

3.2 new[] 和 delete[] 操作符

动态数组是在运行时通过new[]操作符动态分配的。相应的通过delete[]操作符释放。

 1/* Test dynamic allocation of array  (TestDynamicArray.cpp) */
 2#include <iostream>
 3#include <cstdlib>
 4using namespace std;
 5
 6int main() {
 7   const int SIZE = 5;
 8   int * pArray;
 9
10   pArray = new int[SIZE];  // Allocate array via new[] operator
11
12   // Assign random numbers between 0 and 99
13   for (int i = 0; i < SIZE; ++i) {
14      *(pArray + i) = rand() % 100;
15   }
16   // Print array
17   for (int i = 0; i < SIZE; ++i) {
18      cout << *(pArray + i) << " ";
19   }
20   cout << endl;
21
22   delete[] pArray;  // Deallocate array via delete[] operator
23   return 0;
24}

C++03 不允许初始化动态分配的数组,C++11 中可以用下面的方式对动态数组初始化:

1int *p = new int[5] {1, 2, 3, 4, 5};

4 指针,数组和函数

4.1 数组被视为指针

在 C/C++中,数组的名字就是指向数组第一个元素的地址的指针。例如有一个int类型数组"numbers”,“numbers"就是&numbers[0],相应地,*numbersnumbers[0], *(numbers + i)numbers[i]

 1/* Pointer and Array (TestPointerArray.cpp) */
 2#include <iostream>
 3using namespace std;
 4
 5int main() {
 6   const int SIZE = 5;
 7   int numbers[SIZE] = {11, 22, 44, 21, 41};  // An int array
 8
 9   // The array name numbers is an int pointer, pointing at the
10   // first item of the array, i.e., numbers = &numbers[0]
11   cout << &numbers[0] << endl; // Print address of first element (0x22fef8)
12   cout << numbers << endl;     // Same as above (0x22fef8)
13   cout << *numbers << endl;         // Same as numbers[0] (11)
14   cout << *(numbers + 1) << endl;   // Same as numbers[1] (22)
15   cout << *(numbers + 4) << endl;   // Same as numbers[4] (41)
16}

4.2 指针运算

正如上一节中提到的,如果’numbers’是一个int型的数组,它可以被视为int型的指针,指向这个数组的第一个元素。(numbers + 1)指向下一个元素,需要注意的是, int通常占 4 字节,也就是说(numbers + 1)会增加 4 个地址位置,或者sizeof(int)个步长。

1int numbers[] = {11, 22, 33};
2int * iPtr = numbers;
3cout << iPtr << endl;        // 0x22cd30
4cout << iPtr + 1 << endl;    // 0x22cd34 (increase by 4 - sizeof int)
5cout << *iPtr << endl;       // 11
6cout << *(iPtr + 1) << endl; // 22
7cout << *iPtr + 1 << endl;   // 12

4.3 sizeof 数组

对数组执行sizeof操作会返回该数组所占用的字节数。你可以通过数组总字节数除以单个元素字节数来计算数组长度。

1int numbers[100];
2cout << sizeof(numbers) << endl;     // Size of entire array in bytes (400)
3cout << sizeof(numbers[0]) << endl;  // Size of first element of the array in bytes (4)
4cout << "Array size is " << sizeof(numbers) / sizeof(numbers[0]) << endl;  // (100)

4.4 数组作为函数参数或返回值

将数组传递给函数实际上传递的是数组第一个元素的指针。在申明函数的时候既可以使用数组形式,也可以使用指针形式,例如下面的申明是等效的:

1int max(int numbers[], int size);
2int max(int *numbers, int size);
3int max(int number[50], int size);

上面的申明中参数都会被编译器视为int *,给定的数组大小会被忽略。

数组的大小不是数组参数的一部分,所以需要传递另一个整数参数来指定数组长度。编译器不能根据数组指针推断数组长度,也不会检查数组边界。

下面是使用数组申明的例子:

 1/* Passing array in/out function (TestArrayPassing.cpp) */
 2#include <iostream>
 3using namespace std;
 4
 5// Function prototypes
 6int max(const int arr[], int size);
 7void replaceByMax(int arr[], int size);
 8void print(const int arr[], int size);
 9
10int main() {
11   const int SIZE = 4;
12   int numbers[SIZE] = {11, 22, 33, 22};
13   print(numbers, SIZE);
14   cout << max(numbers, SIZE) << endl;
15   replaceByMax(numbers, SIZE);
16   print(numbers, SIZE);
17}
18
19// Return the maximum value of the given array.
20// The array is declared const, and cannot be modified inside the function.
21int max(const int arr[], int size) {
22   int max = arr[0];
23   for (int i = 1; i < size; ++i) {
24      if (max < arr[i]) max = arr[i];
25   }
26   return max;
27}
28
29// Replace all elements of the given array by its maximum value
30// Array is passed by reference. Modify the caller's copy.
31void replaceByMax(int arr[], int size) {
32   int maxValue = max(arr, size);
33   for (int i = 0; i < size; ++i) {
34      arr[i] = maxValue;
35   }
36}
37
38// Print the array's content
39void print(const int arr[], int size) {
40   cout << "{";
41   for (int i = 0; i < size; ++i) {
42      cout << arr[i];
43      if (i < size - 1) cout << ",";
44   }
45   cout << "}" << endl;
46}

接着是使用指针申明的例子:

 1/* Passing array in/out function using pointer (TestArrayPassingPointer.cpp) */
 2#include <iostream>
 3using namespace std;
 4
 5// Function prototype
 6int max(const int *arr, int size);
 7
 8int main() {
 9   const int SIZE = 5;
10   int numbers[SIZE] = {10, 20, 90, 76, 22};
11   cout << max(numbers, SIZE) << endl;
12}
13
14// Return the maximum value of the given array
15int max(const int *arr, int size) {
16   int max = *arr;
17   for (int i = 1; i < size; ++i) {
18      if (max < *(arr+i)) max = *(arr+i);
19   }
20   return max;
21}

4.5 Pass-by-Reference 和 sizeof

 1/* Test sizeof array (TestSizeofArray.cpp) */
 2#include <iostream>
 3using namespace std;
 4
 5// Function prototypes
 6void fun(const int *arr, int size);
 7
 8// Test Driver
 9int main() {
10   const int SIZE = 5;
11   int a[SIZE] = {8, 4, 5, 3, 2};
12   cout << "sizeof in main() is " << sizeof(a) << endl;
13   cout << "address in main() is " << a << endl;
14   fun(a, SIZE);
15}
16
17// Function definitions
18void fun(const int *arr, int size) {
19   cout << "sizeof in function is " << sizeof(arr) << endl;
20   cout << "address in function is " << arr << endl;
21}

编译运行:

1→ g++ test_sizeof_array.cpp && ./a.out
2sizeof in main() is 20
3address in main() is 0x7ffeeab9d0d0
4sizeof in function is 8
5address in function is 0x7ffeeab9d0d0

看到这个结果是不是很不可思议,在main函数和fun中打印的地址相同,但是sizeof的结果却不一样,这是为什么呢? 因为在mainsizeof是对数组操作的,而在fun中,sizeof操作的是指针,指针变量占的字节数是 4。

4.6 数组的遍历操作

 1/* Function to compute the sum of a range of an array (SumArrayRange.cpp) */
 2#include <iostream>
 3using namespace std;
 4
 5// Function prototype
 6int sum(const int *begin, const int *end);
 7
 8// Test Driver
 9int main() {
10   int a[] = {8, 4, 5, 3, 2, 1, 4, 8};
11   cout << sum(a, a+8) << endl;        // a[0] to a[7]
12   cout << sum(a+2, a+5) << endl;      // a[2] to a[4]
13   cout << sum(&a[2], &a[5]) << endl;  // a[2] to a[4]
14}
15
16// Function definition
17// Return the sum of the given array of the range from
18// begin to end, exclude end.
19int sum(const int *begin, const int *end) {
20   int sum = 0;
21   for (const int *p = begin; p != end; ++p) {
22      sum += *p;
23   }
24   return sum;
25}

4.7 C-String 和 指针

C-String (在 C 语言中)是一个以字符'\0'结尾的字符数组:

 1/* Testing C-string (TestCString.cpp) */
 2#include <iostream>
 3#include <cstring>
 4using namespace std;
 5
 6int main() {
 7   char msg1[] = "Hello";
 8   char *msg2 = "Hello";
 9      // warning: deprecated conversion from string constant to 'char*'
10
11   cout << strlen(msg1) << endl;    // 5
12   cout << strlen(msg2) << endl;
13   cout << strlen("Hello") << endl;
14
15   int size = sizeof(msg1)/sizeof(char);
16   cout << size << endl;  // 6 - including the terminating '\0'
17   for (int i = 0; msg1[i] != '\0'; ++i) {
18      cout << msg1[i];
19   }
20   cout << endl;
21
22   for (char *p = msg1; *p != '\0'; ++p) {
23          // *p != '\0' is the same as *p != 0, is the same as *p
24      cout << *p;
25   }
26   cout << endl;
27}

需要注意的是,像strlen()这样的 C-String 函数,不需要传递数组长度。因为 C-String 以'\0'结尾,这些函数可以 遍历字符数组直到遇到字符'\0'.

 1/* Function to count the occurrence of a char in a string (CountChar.cpp) */
 2#include <iostream>
 3#include <cstring>
 4using namespace std;
 5
 6int count(const char *str, const char c);  // No need to pass the array size
 7
 8int main() {
 9   char msg1[] = "Hello, world";
10   char *msg2 = "Hello, world";
11
12   cout << count(msg1, 'l') << endl;
13   cout << count(msg2, 'l') << endl;
14   cout << count("Hello, world", 'l') << endl;
15}
16
17// Count the occurrence of c in str
18// No need to pass the size of char[] as C-string is terminated with '\0'
19int count(const char *str, const char c) {
20   int count = 0;
21   while (*str) {   // same as (*str != '\0')
22      if (*str == c) ++count;
23      ++str;
24   }
25   return count;
26}

5 指针的其他方面

5.1 函数指针

在 C/C++中,函数跟其他数据一样,都有自己的地址。函数名就是函数在记忆体中的起始地址,因此函数名可以被视为指针。 我们可以给函数传递一个函数指针,语法如下:

 1// Function-pointer declaration
 2return-type (* function-ptr-name) (parameter-list)
 3
 4// Example
 5double (*fp)(int, int)  // fp points to a function that takes two ints and returns a double (function-pointer)
 6double *dp;             // dp points to a double (double-pointer)
 7double *fun(int, int)   // fun is a function that takes two ints and returns a double-pointer
 8
 9double f(int, int);      // f is a function that takes two ints and returns a double
10fp = f;                 // Assign function f to fp function-pointer
 1/* Test Function Pointers (TestFunctionPointer.cpp) */
 2#include <iostream>
 3using namespace std;
 4
 5int arithmetic(int, int, int (*)(int, int));
 6    // Take 3 arguments, 2 int's and a function pointer
 7    //   int (*)(int, int), which takes two int's and return an int
 8int add(int, int);
 9int sub(int, int);
10
11int add(int n1, int n2) { return n1 + n2; }
12int sub(int n1, int n2) { return n1 - n2; }
13
14int arithmetic(int n1, int n2, int (*operation) (int, int)) {
15   return (*operation)(n1, n2);
16}
17
18int main() {
19   int number1 = 5, number2 = 6;
20
21   // add
22   cout << arithmetic(number1, number2, add) << endl;
23   // subtract
24   cout << arithmetic(number1, number2, sub) << endl;
25}

5.2 通用指针或者 void 指针(void \*)

void指针可以用来存放任何数据类型的地址(除了函数指针)。我们不能对void指针执行操作,因为它的类型是未知的。 但是我们可以使用void指针来跟其他地址做比较。

5.3 常数的指针 vs. 指针是常数

非常数指针指向常数:指针指向的数据不能修改,但是指针本身可以修改为指向其他数据,例如:

1int i1 = 8, i2 = 9;
2const int *iptr = &i1; // non-constant pointer pointing to constant data
3// *iptr = 9; // error: assignment of read-only location
4iptr = &i2; // ok

常数指针指向非常量数据:指向的数据可以被修改,但是指针本身不能修改:

1int i1 = 8, i2 = 9;
2int * const iptr = &i1;
3*iptr = 9; // ok
4// iptr = &i2; // error: assignment of read-only variable

常数指针指向常量:指向的数据和指针本身都不能修改:

1int i1 = 8, i2 = 9;
2const int * const iptr = &i1;  // constant pointer pointing to constant data
3// *iptr = 9;   // error: assignment of read-only variable
4// iptr = &i2;  // error: assignment of read-only variable

非常数指针指向非常数:指向的数据和指针都能被修改

1int i1 = 8, i2 = 9;
2int * iptr = &i1;  // non-constant pointer pointing to non-constant data
3*iptr = 9;   // okay
4iptr = &i2;  // okay