指针,引用和动态分配内存是 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 申明指针

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

type *ptr;
// or
type* ptr;
// or
type * ptr;

例如:

int * iPtr;
double *dPtr;

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

例如:

int *p1, *p2, i;
int* p1, p2, i;
int * p1, * p2, i;

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

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

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

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

int number = 80;
int *pNumber;
pNumber = &number;

int *pAnother = &number;

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

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

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

int number = 88;
int *pNumber = &number;
cout << pNumber << endl;
cout << *pNumber << endl;
*pNumber = 99;
cout << *pNumber << endl;
cout << number << endl;

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

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

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

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

1.5 指针也有类型

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

int i = 88;
double d = 55.66;
int *iPtr = &i;
double *dPtr = &d;

iPtr = &d; // ERROR, cannot hold address of different type
dPtr = &i; // ERROR
iPtr = i;  // ERROR, pointer holds address of an int , NOT int value

int j = 99;
iPtr = &j; // You can change the address stored in a pointer.

示例

#include <iostream>

using namespace std;

int main(int argc, char *argv[])
{
	int number = 88;
	int *pNumber;
	pNumber = &number;

	cout << pNumber << endl;
	cout << &number << endl;
	cout << *pNumber << endl;
	cout << number << endl;

	*pNumber = 99;
	cout << pNumber << endl;
	cout << &number << endl;
	cout << *pNumber << endl;
	cout << number << endl;

	cout << &pNumber << endl;
	return 0;
}

运行结果为:

→ g++ test_pointer.cpp  && ./a.out
0x7ffee123c04c
0x7ffee123c04c
88
88
0x7ffee123c04c
0x7ffee123c04c
99
99
0x7ffee123c040

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

1.7 空指针

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

int *iPtr = 0;
cout << *iPtr << endl; // ERROR! STATUS_ACCESS_VIOLATION exception

int *p = NULL;

2. 引用变量

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

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

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

2.1 引用(别名)(&)

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

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

type &newName = existingName;
// or
type& newName = existingName;
// or
type & newName = existingName;

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

#include <iostream>

using namespace std;

int main(int argc, char *argv[])
{
	int number = 88;
	int &refNumber = number;

	cout << number << endl;
	cout << refNumber << endl;

	refNumber = 99;
	cout << number << endl;
	cout << refNumber << endl;

	number = 55;
	cout << number << endl;
	cout << refNumber << endl;

	return 0;
}

编译运行结果为:

→ g++ test_reference.cpp && ./a.out
88
88
99
99
55
55

2.2 引用是如何工作的?

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

2.3 引用 vs. 指针

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

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

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

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

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

#include <iostream>

using namespace std;

int main(int argc, char *argv[])
{
	int number1 = 88, number2 = 22;

	// Create a pointer pointing to number1;
	int *pNumber1 = &number1;
	*pNumber1 = 99;
	cout << *pNumber1 << endl;
	cout << &number1 << endl;
	cout << pNumber1 << endl;
	cout << &pNumber1 << endl;

	pNumber1 = &number2;

	// Create a reference (alias) to number1
	int &refNumber1 = number1;
	refNumber1 = 11;
	cout << refNumber1 << endl;
	cout << &number1 << endl;
	cout << &refNumber1 << endl;

	// refNumber1 = &number2; // Error! Reference cannot be re-assigned

	refNumber1 = number2;
	number2++;
	cout << refNumber1 << endl;
	cout << number1 << endl;
	cout << number2 << endl;
	return 0;
}

编译运行结果:

→ g++ test_pointer_reference.cpp && ./a.out
99
0x7ffeeacc503c
0x7ffeeacc503c
0x7ffeeacc5030
11
0x7ffeeacc503c
0x7ffeeacc503c
22
22
23

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

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

Pass-by-Value

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

#include <iostream>

using namespace std;

int square(int);

int main(int argc, char *argv[])
{
	int number = 8;
	cout << "In main():" << &number << endl;
	cout << number << endl;
	cout << square(number) << endl;
	cout << number << endl;
	return 0;
}

int square(int n) {
	cout << "In square():" << &n << endl;
	n *= n;
	return n;
}

编译运行:

→ g++ call_by_value.cpp && ./a.out
In main():0x7ffee38b909c
8
In square():0x7ffee38b904c
64
8

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

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

#include <iostream>

using namespace std;

void square(int *);

int main(int argc, char *argv[])
{
	int number = 8;
	cout << "In main():" << &number << endl;
	cout << number << endl;
	square(&number);
	cout << number << endl;
	return 0;
}

void square(int *pNumber) {
	cout << "In square():" << pNumber << endl;
	*pNumber *= *pNumber;
}

编译运行:

→ g++ call_by_ref_with_pointer.cpp && ./a.out
In main():0x7ffee23e20ac
8
In square():0x7ffee23e20ac
64

译者注:

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

#include <iostream>

using namespace std;

void foo(int *pNumber);

int main(int argc, char *argv[])
{
	int a = 10;
	int *pNumber = &a;
	cout << &pNumber << endl;
	foo(pNumber);
	return 0;
}

void foo(int *pNumber) {
	cout << &pNumber << endl;
}

运行结果为:

→ g++ test.cpp && ./a.out
0x7ffee59c80c0
0x7ffee59c8088

使用引用实现 Pass-by-reference

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

#include <iostream>

using namespace std;

void square(int &);

int main() {
	int number = 8;
	cout << "In main():" << &number << endl;
	cout << number << endl;
	square(number);
	cout << number << endl;
}

void square(int &rNumber) {
	cout << "In square():" << &rNumber << endl;
	rNumber *= rNumber;
}

“const” 参数

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

#include <iostream>

using namespace std;

int squareConst(const int number) {
	// number *= number; // error: assignment of read-only parameter
	return number * number;
}

int squareNonConst(int number) {
	number *= number;
	return number;
}

int squareConstRef(const int & number) {
	return number * number;
}

int squareNonConstRef(int * number) {
	return number * number;
}

int main() {
	int number = 8;
	cout int constNumber = 9;
	cout << squareConst(number) << endl;
	cout << squareConst(constNumber) << endl;
	cout << squareNonConst(number) << endl;
	cout << squareNonConstRef(constNumber) << endl;
	cout << squareConstRef(number) << endl;

	cout << squareConstRef(constNumber) << endl;
	cout << squareNonConstRef(number) << endl;
	// cout << squareNonConstRef(constNumber) << endl;
       // error: invalid initialization of reference of
       //  type 'int&' from expression of type 'const int'

	return 0;
}

2.5 函数的返回值

函数返回引用

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

/* Passing back return value using reference (TestPassByReferenceReturn.cpp) */
#include <iostream>
using namespace std;

int & squareRef(int &);
int * squarePtr(int *);

int main() {
   int number1 = 8;
   cout <<  "In main() &number1: " << &number1 << endl;  // 0x22ff14
   int & result = squareRef(number1);
   cout <<  "In main() &result: " << &result << endl;  // 0x22ff14
   cout << result << endl;   // 64
   cout << number1 << endl;  // 64

   int number2 = 9;
   cout <<  "In main() &number2: " << &number2 << endl;  // 0x22ff10
   int * pResult = squarePtr(&number2);
   cout <<  "In main() pResult: " << pResult << endl;  // 0x22ff10
   cout << *pResult << endl;   // 81
   cout << number2 << endl;    // 81
}

int & squareRef(int & rNumber) {
   cout <<  "In squareRef(): " << &rNumber << endl;  // 0x22ff14
   rNumber *= rNumber;
   return rNumber;
}

int * squarePtr(int * pNumber) {
   cout <<  "In squarePtr(): " << pNumber << endl;  // 0x22ff10
   *pNumber *= *pNumber;
   return pNumber;
}

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

/* Test passing the result (TestPassResultLocal.cpp) */
#include <iostream>
using namespace std;

int * squarePtr(int);
int & squareRef(int);

int main() {
   int number = 8;
   cout << number << endl;  // 8
   cout << *squarePtr(number) << endl;  // ??
   cout << squareRef(number) << endl;   // ??
}

int * squarePtr(int number) {
   int localResult = number * number;
   return &localResult;
      // warning: address of local variable 'localResult' returned
}

int & squareRef(int number) {
   int localResult = number * number;
   return localResult;
      // warning: reference of local variable 'localResult' returned
}

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

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

/* Test passing the result (TestPassResultNew.cpp) */
#include <iostream>
using namespace std;

int * squarePtr(int);
int & squareRef(int);

int main() {
   int number = 8;
   cout << number << endl;  // 8
   cout << *squarePtr(number) << endl;  // 64
   cout << squareRef(number) << endl;   // 64
}

int * squarePtr(int number) {
   int * dynamicAllocatedResult = new int(number * number);
   return dynamicAllocatedResult;
}

int & squareRef(int number) {
   int * dynamicAllocatedResult = new int(number * number);
   return *dynamicAllocatedResult;
}

2.6 总结

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

3. 动态内存分配

3.1 new 和 delete 操作符

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

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

// Static allocation
int number = 88;
int * p1 = &number;  // Assign a "valid" address into pointer

// Dynamic Allocation
int * p2;            // Not initialize, points to somewhere which is invalid
cout << p2 << endl; // Print address before allocation
p2 = new int;       // Dynamically allocate an int and assign its address to pointer
                    // The pointer gets a valid address with memory allocated
*p2 = 99;
cout << p2 << endl;  // Print address after allocation
cout << *p2 << endl; // Print value point-to
delete p2;           // Remove the dynamically allocated storage

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

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

// use an initializer to initialize a fundamental type (such as int, double)
int * p1 = new int(88);
double * p2 = new double(1.23);

// C++11 brace initialization syntax
int * p1 = new int {88};
double * p2 = new double {1.23};

// invoke a constructor to initialize an object (such as Date, Time)
Date * date1 = new Date(1999, 1, 1);
Time * time1 = new Time(12, 34, 56);

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

// Dynamically allocate global pointers (TestDynamicAllocation.cpp)
#include <iostream>
using namespace std;

int * p1, * p2;  // Global int pointers

// This function allocates storage for the int*
// which is available outside the function
void allocate() {
   p1 = new int;     // Allocate memory, initial content unknown
   *p1 = 88;         // Assign value into location pointed to by pointer
   p2 = new int(99); // Allocate and initialize
}

int main() {
   allocate();
   cout << *p1 << endl;  // 88
   cout << *p2 << endl;  // 99
   delete p1;  // Deallocate
   delete p2;
   return 0;
}

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

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

3.2 new[] 和 delete[] 操作符

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

/* Test dynamic allocation of array  (TestDynamicArray.cpp) */
#include <iostream>
#include <cstdlib>
using namespace std;

int main() {
   const int SIZE = 5;
   int * pArray;

   pArray = new int[SIZE];  // Allocate array via new[] operator

   // Assign random numbers between 0 and 99
   for (int i = 0; i < SIZE; ++i) {
      *(pArray + i) = rand() % 100;
   }
   // Print array
   for (int i = 0; i < SIZE; ++i) {
      cout << *(pArray + i) << " ";
   }
   cout << endl;

   delete[] pArray;  // Deallocate array via delete[] operator
   return 0;
}

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

int *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]

/* Pointer and Array (TestPointerArray.cpp) */
#include <iostream>
using namespace std;

int main() {
   const int SIZE = 5;
   int numbers[SIZE] = {11, 22, 44, 21, 41};  // An int array

   // The array name numbers is an int pointer, pointing at the
   // first item of the array, i.e., numbers = &numbers[0]
   cout << &numbers[0] << endl; // Print address of first element (0x22fef8)
   cout << numbers << endl;     // Same as above (0x22fef8)
   cout << *numbers << endl;         // Same as numbers[0] (11)
   cout << *(numbers + 1) << endl;   // Same as numbers[1] (22)
   cout << *(numbers + 4) << endl;   // Same as numbers[4] (41)
}

4.2 指针运算

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

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

4.3 sizeof 数组

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

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

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

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

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

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

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

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

/* Passing array in/out function (TestArrayPassing.cpp) */
#include <iostream>
using namespace std;

// Function prototypes
int max(const int arr[], int size);
void replaceByMax(int arr[], int size);
void print(const int arr[], int size);

int main() {
   const int SIZE = 4;
   int numbers[SIZE] = {11, 22, 33, 22};
   print(numbers, SIZE);
   cout << max(numbers, SIZE) << endl;
   replaceByMax(numbers, SIZE);
   print(numbers, SIZE);
}

// Return the maximum value of the given array.
// The array is declared const, and cannot be modified inside the function.
int max(const int arr[], int size) {
   int max = arr[0];
   for (int i = 1; i < size; ++i) {
      if (max < arr[i]) max = arr[i];
   }
   return max;
}

// Replace all elements of the given array by its maximum value
// Array is passed by reference. Modify the caller's copy.
void replaceByMax(int arr[], int size) {
   int maxValue = max(arr, size);
   for (int i = 0; i < size; ++i) {
      arr[i] = maxValue;
   }
}

// Print the array's content
void print(const int arr[], int size) {
   cout << "{";
   for (int i = 0; i < size; ++i) {
      cout << arr[i];
      if (i < size - 1) cout << ",";
   }
   cout << "}" << endl;
}

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

/* Passing array in/out function using pointer (TestArrayPassingPointer.cpp) */
#include <iostream>
using namespace std;

// Function prototype
int max(const int *arr, int size);

int main() {
   const int SIZE = 5;
   int numbers[SIZE] = {10, 20, 90, 76, 22};
   cout << max(numbers, SIZE) << endl;
}

// Return the maximum value of the given array
int max(const int *arr, int size) {
   int max = *arr;
   for (int i = 1; i < size; ++i) {
      if (max < *(arr+i)) max = *(arr+i);
   }
   return max;
}

4.5 Pass-by-Reference 和 sizeof

/* Test sizeof array (TestSizeofArray.cpp) */
#include <iostream>
using namespace std;

// Function prototypes
void fun(const int *arr, int size);

// Test Driver
int main() {
   const int SIZE = 5;
   int a[SIZE] = {8, 4, 5, 3, 2};
   cout << "sizeof in main() is " << sizeof(a) << endl;
   cout << "address in main() is " << a << endl;
   fun(a, SIZE);
}

// Function definitions
void fun(const int *arr, int size) {
   cout << "sizeof in function is " << sizeof(arr) << endl;
   cout << "address in function is " << arr << endl;
}

编译运行:

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

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

4.6 数组的遍历操作

/* Function to compute the sum of a range of an array (SumArrayRange.cpp) */
#include <iostream>
using namespace std;

// Function prototype
int sum(const int *begin, const int *end);

// Test Driver
int main() {
   int a[] = {8, 4, 5, 3, 2, 1, 4, 8};
   cout << sum(a, a+8) << endl;        // a[0] to a[7]
   cout << sum(a+2, a+5) << endl;      // a[2] to a[4]
   cout << sum(&a[2], &a[5]) << endl;  // a[2] to a[4]
}

// Function definition
// Return the sum of the given array of the range from
// begin to end, exclude end.
int sum(const int *begin, const int *end) {
   int sum = 0;
   for (const int *p = begin; p != end; ++p) {
      sum += *p;
   }
   return sum;
}

4.7 C-String 和 指针

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

/* Testing C-string (TestCString.cpp) */
#include <iostream>
#include <cstring>
using namespace std;

int main() {
   char msg1[] = "Hello";
   char *msg2 = "Hello";
      // warning: deprecated conversion from string constant to 'char*'

   cout << strlen(msg1) << endl;    // 5
   cout << strlen(msg2) << endl;
   cout << strlen("Hello") << endl;

   int size = sizeof(msg1)/sizeof(char);
   cout << size << endl;  // 6 - including the terminating '\0'
   for (int i = 0; msg1[i] != '\0'; ++i) {
      cout << msg1[i];
   }
   cout << endl;

   for (char *p = msg1; *p != '\0'; ++p) {
          // *p != '\0' is the same as *p != 0, is the same as *p
      cout << *p;
   }
   cout << endl;
}

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

/* Function to count the occurrence of a char in a string (CountChar.cpp) */
#include <iostream>
#include <cstring>
using namespace std;

int count(const char *str, const char c);  // No need to pass the array size

int main() {
   char msg1[] = "Hello, world";
   char *msg2 = "Hello, world";

   cout << count(msg1, 'l') << endl;
   cout << count(msg2, 'l') << endl;
   cout << count("Hello, world", 'l') << endl;
}

// Count the occurrence of c in str
// No need to pass the size of char[] as C-string is terminated with '\0'
int count(const char *str, const char c) {
   int count = 0;
   while (*str) {   // same as (*str != '\0')
      if (*str == c) ++count;
      ++str;
   }
   return count;
}

5 指针的其他方面

5.1 函数指针

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

// Function-pointer declaration
return-type (* function-ptr-name) (parameter-list)

// Example
double (*fp)(int, int)  // fp points to a function that takes two ints and returns a double (function-pointer)
double *dp;             // dp points to a double (double-pointer)
double *fun(int, int)   // fun is a function that takes two ints and returns a double-pointer

double f(int, int);      // f is a function that takes two ints and returns a double
fp = f;                 // Assign function f to fp function-pointer
/* Test Function Pointers (TestFunctionPointer.cpp) */
#include <iostream>
using namespace std;

int arithmetic(int, int, int (*)(int, int));
    // Take 3 arguments, 2 int's and a function pointer
    //   int (*)(int, int), which takes two int's and return an int
int add(int, int);
int sub(int, int);

int add(int n1, int n2) { return n1 + n2; }
int sub(int n1, int n2) { return n1 - n2; }

int arithmetic(int n1, int n2, int (*operation) (int, int)) {
   return (*operation)(n1, n2);
}

int main() {
   int number1 = 5, number2 = 6;

   // add
   cout << arithmetic(number1, number2, add) << endl;
   // subtract
   cout << arithmetic(number1, number2, sub) << endl;
}

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

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

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

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

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

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

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

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

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

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

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