指针与指针变量是C或C++的一把利刃,但也是一把双刃剑。指针变量是变量之间关系的一种数据表达,在实现函数的副作用(指针变量,包括函数指针做函数参数)、构造链式存储(数据元素的存储位置可以不连续,元素之间通过指针建立联系)、获取动态内存时,不可或缺。但指针变量与函数、数组等混杂在一起,有一定的复杂性。
话说第一台电子计算机Eniac创建于1945年。当时的程序并不存储于内存memory,也没有控制器。程序如何运行呢?其程序也不是现代意义上的程序,而是一份操作清单,提供一个如何连线和插接线路板的操作指南,来组合不同的硬件模块。这种方式可以称为硬连接或硬编程,自然,不够自动化。为此,堪称全才的美国人冯诺依曼提出了“存储程序控制”的概念,增加存储器和控制器,将数据和代码存储到存储器,由控制器依次(或按状态控制器跳转)从存储器中读取代码,翻译指令,产生控制信号来控制计算机的其它部件操作。
数据或代码是一块块地存在于内存中的,通常我们称其为一个段。而且代码和数据是分开存放的,即不储存于同于一个段中,而且各种数据也是分开存放在不同的段中的。部分数据(如全局数据)在程序加载时加载,程序从main函数开始执行,部分数据在需要时分配存储空间。
存储到内存中的数据或代码能够被随机访问(读或写),是因为内存是按字节编址的,内存就像一排长长的开关(有多长呢?如果是32位系统,则是2^32=0xFFFFFFFF),以8个为一组提供一个地址(值域就是0x00000000~0xFFFFFFFF)。代码和内存被编译成二进制码后,加载到内存时,如同需要噼噼啪啪地不同按开关。

程序和数据存储到内存后,其所在内存的位置(地址)由标识符,如变量、常量、函数名称来标识。由此,标识符有两重含义,地址和其本身的值,或称己址和己值。通常,显示使用其值,隐式使用其址。隐式使用某个地址的变量称为指针变量,指针变量存储一个指针(地址),这个指针可以是变量、常量或函数名称对应的地址(存储函数地址的变量称为函数指针或函数指针变量)。

指针的算术运算:

指针的移动:

STL的迭代器通常是容器的内部模板类,称为迭代器类,迭代器类内含一个指向容器元素的指针,重载上述的一些运算符,实现指针的移动,用于容器内元素的遍历。
指针变量可以用const修饰,可以是修饰自身或指针变量指向的类型。
2.1 const修饰指针变量自身
intn =0;intm =0;int*constp = p =// error,p是const,不能再更新
2.2 const修饰指针变量指向的对象
intn =0;constint* p =// int const* p = 是效果一致的写法n =5; *p =2;// error,p指向一个常量,不能用p去更新
规则:const写在指针类型符号*右边表示修饰其自身,写在左边,表示修饰其指向的类型。
int arr; // 标识符arr→往右看→是数组符号→arr是一个数组,有5个元素→往左看,数组元素的类型,类型是int。其类型可以理解为int,也就是挖掉标识符以后剩下的内容。
数组元素的类型当然可以是指针变量,如int *,则写成
int* arr; // 其类型可以理解为int*
数组的声明是分裂式的,右边部分是数组符号及元素数量,左边部分是元素的类型。核心是右边的[],所以先往右看。
指向数组指针的声明,将标识符用(*p)填充:
同样的int*(*p)[5]; // 也是按上述思路去理解,p是指针,指向有5个元素的数组,元素类型是int*。
英文的表达是先干先枝,好像更准确:
int vari[10]; | vari is array of int |
int vari[10][3]; | vari is array of array of int |
int *vari[10] | vari is array of pointer to int |
int(*vari)[3]; | vari is pointer to array of int |
int vari; vari往右看,是一个数组,有10个元素,元素的类型是int。
将上述数据的符号[]改成(),数组元素的个数改成参数类型,则成了函数的声明,其理解规则基本一致。
int arr; // 标识符arr→往右看→是函数符号→arr是一个函数,其参数类型是int→往左看,函数返回值的类型,类型是int。其类型可以理解为int,也就是挖掉标识符以后剩下的内容。
函数元素的类型当然可以是指针变量,如int *,则写成
int* arr; // 其类型可以理解为int*
函数的声明也是分裂式的,右边部分是函数符号及参数类型,左边部分是返回值类型。核心是右边的(),所以先往右看。
指向函数指针的声明,也是标识符用(*p)填充:
同样的int*(*p)(int); // 也是按上述思路去理解,p是指针,指向参数类型是int的函数,函数返回值类型是是int*。
英文的表达是先干先枝,好像更准确:
int func; | func is function returning int |
int ; | func is pointer to function returning int |
其类型和声明的写法是分裂式的。
对于数组,右边是数组符号、数组元素数量,数组元素的类型写在左边。
对于函数,右边是函数符号、函数参数类型,函数返回值类型写在左边。
核心是右边,以理解的规则是,从标识符开始,往右看,看是数组还是函数,及元素数量或函数参数类型,然后再往左看,对应元素类型或返回值类型。如果有括号,则先理解括号内的部分,规则也是先右后左(也可以理解为符号[]和()相对于*,有较高的优先级)。(在按上述规则理解时,如果先看到的是右括号,自然是优先级的括号,如果先看到的是左括号,则一般是函数标识符括号)。

二级指针和二维数组也可以应用于上述规则:
int n = 5;int *p = int**pp = // p先右后左,右边没有,左边首先是*,表示p是一个指针,指向的类型是int*int vari[10][3]; // vari往右看,是一个数组,有10个元素,元素的类型是int[10]。
数组名的实质是一个指针变量,指向一块内存的基地址,数组元素的分量以其地址为基准进行偏移,如:
intarr[5]; arr[3] =6; *(arr+3) =6;// []写法是指针写法的语法糖
intflag =999;intarr[5] = {1,2,3,4,5}; arr[3] =6; *(arr+3) =6;// []写法是指针写法的语法糖//int *p = // cannot convert from 'int ' to 'int *'int(*pp)[5] = pp++;// pp指向了整个数组的后一个元素flag(栈地址是递减分配的)inta = **pp;// a = 999int*p = arr; p++;//p指向了数组元素的下一个元素intb = *p;// b = 2printf("%d %d n",a,b);// 999 2
数组名的上下文中,只有三种情况表示数组本身,其它情形都蜕变为指向数组首元素的指针。
数据与指针的等价关系:

比较一下指针和数组:

主要用做函数参数。
7.1 数组指针用作函数参数
#includestdio.h#includestdlib.hvoidfunc(int(*p)[4]){}voidfunc2(int**pp){}intmain(){intarr[3][4] = {{1},{2},{3,10,11,12}};int(*p)[4] = arr; func; func;int**pp = (int**)malloc(sizeof(int*)*3); func2(pp);return0;}
一元数组和二元数组用做函数参数:
#includestdio.h#includeiostreamusingnamespacestd;voidfunc0(intarr[10]){}voidfunc2(intarr[]){}voidfunc3(int*arr){}voidfunc4(int*arr[20]){}voidfunc5(int**arr){}voidoneDimension(){intarr[10] = {0};int*arr2[20] = {0}; func0; func2; func3; func4; func5;}voidtest0(intarr[3][5]){}voidtest2(intarr[][5]){}voidtest3(int(*parr)[5]){}voidtwoDimension(){intarr[3][5]={0}; test0; test2; test3;}intmain(){ oneDimension; twoDimension;return0;}
7.2 函数指针用作函数参数
#includestdio.hboolcomp(inta,intb){returnfalse;}voidsort(int(*p)[4],bool(*pf)(int,int)){}intmain(){bool(*pf)(int,int) = comp;intarr[3][4] = {{1},{2},{3,10,11,12}};int(*p)[4] = arr; sort; sort;return0;}