作为C++语言的亮点与精髓,指针一直是备受人们追捧和病诟的东西。不知道有多少精巧的代码是通过它实现的,也不知道有多少难缠的bug是由它引发的,这篇文章先对指针做一个全局的总结,从声明、赋值、调用和实现机制上对所有指针做一个说明,希望对大家有所帮助。在后续的文章中,我将给出那些使用指针的技巧。

       首先是一个列表,列出我们今天讨论的内容:

  1. 指向基本数据类型的指针
  2. 指向指针的指针
  3. 指向数组的指针
  4. 指向函数的指针
  5. 指向结构体和对象的指针
  6. 指向类成员变量的指针
  7. 指向类成员函数的指针
1. 指向基本数据类型的指针

 

定义

示例

声明

[数据类型] * [指针名];

int* pa;

赋值

[指针名] = &[变量];

int a;

pa = &a;

取值

*[指针名]

int b = *pa;

实现机制:

对于指向基本数据类型的指针,它包含两个信息:数据所在的地址、取数据时的长度(类型)。

其中地址信息存放在指针pa中,如果是一个32-bit的计算机,pa的长度应为4 bytes,即地址的长度是32位(unsigned int)。那么怎样获取一个变量的地址呢?

13: 	int* pa = &a;
00411A5A  lea         eax,[a] 
00411A5D  mov         dword ptr [pa],eax

可以使用取地址运算实现,即lea:Load Effective Address。

不过这不是唯一一种对基本数据类型指针的赋值方式,其他方式还有:

(1) 强制把其他类型的指针转换为该类型的指针:

double *px = &x;
	pa = (int *) px;
(2) 直接使用常数或者变量值对指针赋值:
pa = (int *) 0;
我就经常把指向对象的指针强制转换为unsigned char *以便研究对象内部结构,不过强制转换面临的最大问题是,如果指针指向了不可访问的内存空间,取值的时候就会引发异常,相信大家都见过这个:

它是怎么引发的呢?

我们先来看一下取值的原理:

14: 	int b = *pa;
00412250  mov         eax,dword ptr [pa] 
00412253  mov         ecx,dword ptr [eax] 
00412255  mov         dword ptr [b],ecx

 

现获取pa的值,放到eax中,然后再把eax当做地址,取出4个字节(int型)的值。“取出多少字节的数据”就是指针所表示的第二个信息(指针的类型),这个信息由编译器自动转换为目标代码(即dword ptr, word ptr等)。

(注:dword ptr [pa]表示从pa开始,取出4个byte的数据,组成一个32位的整数。在asm32中,访问修饰符byte表示1个字节,word:2个字节,dword:4个字节,qword:8个字节。而我们学过一个字(word)表示一台计算机一次处理的数据长度,按理说word应该是4个字节,这里却是两个,原因是微软为了兼容它16位程序代码而导致的,同样,在windows API中,我们看到的DWORD也被定义为16位,即typedef short DWORD)

图中的访问错误异常就产生在mov ecx, dword ptr[eax]这一句,如果eax里面放的地址,映射到不存在、不可读或者没有权限访问的页面,就会产生CPU内部错误,由于我们的程序没有捕获错误,所以把错误抛给了windows,windows就弹出了一个很难看的对话框,然后终止进程的执行。

这里还要说一下指针的运算,如果把指针加1,表示地址向后移动了指针类型那么长的字节数,也就是说,pa+1表示pa的地址加4个字节,可使用下面的伪代码表示:

pa + 1 == (int *)((char *)pa + sizeof(a))

指针不单可以做加减运算(不能做乘除),还可以做[]运算,pa[i]表示*(pa + i),但这并不能表示指针和数组是同一类型。

如果用指针做乘法,则会产生这样的编译错误:

error C2296: '*' : illegal, left operand has type 'int *'
2. 指向指针的指针

指向指针的指针,是存储指针地址的变量,而且每次都取出4个byte的数据(因为地址是统一的)。定义方式和原理与指向基本数据变量的指针一模一样,这里再写一下:

 

定义

示例

声明

[数据类型] *[*…] [指针名];

int** ppa;

赋值

[指针名] = &[指针];

ppa = &pa;

取值

*[*…][指针名]

int b = **pa;

指向指针的指针没有什么特别的地方,把指针看做基本数据类型来对待就行了。

[int*]        * ppa          =  &pa;

[数据类型]  * [指针名]    =  &[变量名];

3. 指向数组的指针

还记得前文说过数组和指针根本不同吗,要理解指向数组的指针,我们先来看什么是数组。

指针是一个存放地址的变量,而数组是一个地址。

或者说,数组是一个指针常量,数组只能指向确定的数据。数组声明时一定要赋值,这是为了它肯定能指向一个有用的地址空间(数组变量一旦确定了地址,就不能变了,因为编译器已经把它定死了(或者说把它替换为一个常量了))。

 

定义

示例

声明和赋值

[数据类型] [数组名][数组长度];


[数据类型] [数组名][];

int arr_a[10];


int arr_b[] = {1,2,3};

取值

[数组名][索引号]

int a = arr_a[1];

 

当使用int arr_a[10]声明数组时,会发生两种情况:如果arr_a在函数中申请,那么编译器就会自动计算这个函数所使用的栈空间,然后把arr_a安插到栈中,转换为ebp-常数;而如果arr_a在全局数据区申请(函数外),那么编译器就在未初始化数据段(.bss = Block Started by Symbol)中申请一块可以放10个int的符号,然后将所有使用arr_a的地方,都转换为那个地址。

局部数组:

24: 	int arr_c[3];
    26: 	int d = arr_c[1];
00412F47  mov         eax,dword ptr [ebp-2Ch] 
00412F4A  mov         dword ptr [ebp-38h],eax

全局数组:

9: int arr_d[4];
    27: 	int e = arr_d[1];
00412F4D  mov         eax,dword ptr [arr_d+4 (42E2ACh)] 
00412F52  mov         dword ptr [ebp-3Ch],eax

而使用int arr_b[] = {1,2,3}这种形式,则更为麻烦一点,因为除了给数组分配空间外,还要赋初值。如果声明在函数中,那么在声明这条语句的代码块里,编译器会写入赋值语句,来初始化这个数组;如果声明在函数外,编译器就会把它放入到数据段中(.data),在程序加载时,由loader赋值。

局部数组:

25: 	int arr_a[] = {1,2};
00412F36  mov         dword ptr [ebp-24h],1 
00412F3D  mov         dword ptr [ebp-20h],2


当然,如果使用static int arr_c[10];也可以将数组放入数据段中,并且还能将初值设为全0。

另外一种将数组元素设为全0的方法是int arr_a[10] = {0},不过这种形式只是int arr_a[10] = {0, 0, …, 0}的简化版罢了,如果在给数组赋初值时,大括号里面的元素不足数组的长度,就用0替代。即int arr_a[10] = {1, 2}会被转换为int arr_a[10] = {1, 2, 0, 0,…, 0}。

为什么初学者会认为数组就是指针呢,大概是数组类型变量可以直接赋值给指针类型变量吧,即:

int* pArr_a = arr_a;

 

但这只是一种赋值,就像int a = 1,反过来就不行了(1 = a),就连使用强制转换都不行。

// x:\工程\deepintocpp\objectmodel\04_data\pointer.cpp(35) : error C2440: 'initializing' : cannot convert from 'int *' to 'int []'
//         There are no conversions to array types, although there are conversions to references or pointers to arrays
/*35:*/	int arr_p[] = pArr_a;

这里还要说的是,数组也不能赋值给数组(就像不能写1=1一样,常量是不能赋值给常量的):

int arr_f[10];
// wrong:
// x:\工程\deepintocpp\objectmodel\04_data\pointer.cpp(39) : error C2440: 'initializing' : cannot convert from 'int [10]' to 'int []'
//         There is no context in which this conversion is possible
int arr_g[] = arr_f;

// wrong:
// x:\工程\deepintocpp\objectmodel\04_data\pointer.cpp(41) : error C2075: 'arr_i' : array initialization needs curly braces
int arr_i[10] = arr_f;

但是有一个例外:在传递函数参数时,可以使用int []表示一个数组。

void func(int arr[])
{
	return;
}

// correct.
	func(arr_a);
// also correct.
	func(pArr_a);

但是这种数组其实就是指针,只是给人看上去好看一点,对于编译器来说,它和指针是一样的:

// passing array.
    13: void func(int arr[])
    14: {
00412540  push        ebp  
00412541  mov         ebp,esp 
00412543  sub         esp,40h 
00412546  push        ebx  
00412547  push        esi  
00412548  push        edi  
    15: 	arr[0] = 1;
00412549  mov         eax,dword ptr [arr] 
0041254C  mov         dword ptr [eax],1 
    16: 	return;
    17: }

// passing pointer.
    19: void func_p(int* pArr)
    20: {
00412560  push        ebp  
00412561  mov         ebp,esp 
00412563  sub         esp,40h 
00412566  push        ebx  
00412567  push        esi  
00412568  push        edi  
    21: 	pArr[0] = 1;
00412569  mov         eax,dword ptr [pArr] 
0041256C  mov         dword ptr [eax],1 
    22: 	return;
    23: }

指针和数组的另一个相同之处是,指针+1表示指针所指向的地址+sizeof(指针的类型),而数组+1同样表示表示数组首地址+数组元素类型长度:

printf("arr_a size = %d, arr_a = 0x%08x, arr_a + 1 = 0x%08x\n", sizeof(arr_a), arr_a, arr_a + 1);
printf("pArr_a size = %d, pArr_a = 0x%08x, pArr_a + 1 = 0x%08x\n", sizeof(pArr_a), pArr_a, pArr_a + 1);

// output: 
// arr_a size = 40, arr_a = 0x0012ff34, arr_a + 1 = 0x0012ff38
// pArr_a size = 4, pArr_a = 0x0012ff34, pArr_a + 1 = 0x0012ff38

 

arr_a的长度是10个int。因此可以用sizeof(arr_a)/sizeof(arr_a[0])来计算一个数组的长度(当然这个长度本来就是编译时已知的)。

弄清了数组和指针的关系,我们来看什么是指向数组的指针:

内部存放数组首地址的变量。

对于定义来说,比较容易理解,可是形式就比较难记了:

 

定义

示例

声明

[数据类型] (* [指针名])[数组长度];

int (*pArr)[10];

赋值

[指针名] = &[数组名];

pArr = &arr;

取值

(*[指针名])[下标]

int b = (*pa)[1];

 

由于operator[]的优先级高于*,因此要在指针名和*之上加一个括号,表示声明的是指针。若不加,则表示的是存放指针的数组。

// pointer, point to an array which has 10 int elements.
int (*pArr)[10] = &arr_a;
// array, stores 10 pointers.
int *arrP[10];
arrP[0] = pa;

 

现在的问题是:pArr+1表示的是什么?

printf("sizeof(pArr) = %d, sizeof(*pArr) = %d, pArr = 0x%08x, pArr + 1 = 0x%08x\n", sizeof(pArr), sizeof(*pArr), pArr, pArr + 1);

// output:
// sizeof(pArr) = 4, sizeof(*pArr) = 40, pArr = 0x0012ff34, pArr + 1 = 0x0012ff5c

从代码中,可以看出:

首先pArr是一个指针,因此它的长度是4个字节,而*pArr是一个有10个int元素的数组, 因此它的长度是40。使用第一节的指针+1公式:

pArr + 1 == (int [10])((char *)pArr + sizeof(int [10])) == pArr的地址 + pArr指向的数组长度 == pArr指向的地址+40个字节

由此可知,pArr+1表示下一个数组。

由于*pArr取出的是数组,那么就要使用一个可以指向数组的变量来存放它的值了。int []表示常量,肯定不能作为左值,因此要想取出pArr指向的内容,只能使用指针或者operator[]了:

61: 	int (*pArr)[10] = &arr_a;
00413CE1  lea         eax,[ebp-44h] 
00413CE4  mov         dword ptr [ebp-6Ch],eax 

    65: 	b = (*pArr)[0];
00413CF0  mov         eax,dword ptr [ebp-6Ch] 
00413CF3  mov         ecx,dword ptr [eax] 
00413CF5  mov         dword ptr [ebp-10h],ecx

注意:上面代码中lea  eax,[ebp - 44h]表示“把ebp-44放入eax中”,这是一种聪明的地址运算的做法,否则就需要用两条语句来实现:

mov  eax, ebp

sub  eax, 44h

(lea表示取某个变量的地址,而[…]表示取某个地址指向变量的值,两者叠加就变为“取某个地址所指向变量的值的地址”,相互抵消,就变成了计算括号中的值,然后把它放入到前面那个寄存器中。)

ebp-44h就是数组名arr_a所转换为的地址常量。

4. 指向函数的指针