1.进一步探头指向指针的指针
int i;
int *pi;
int **ppi;
若有如下打印:
1.printf("%d\n",ppi);
2.printf("%d\n",&ppi);
3.*ppi=5
1)若ppi是自动变量,它未被初始化,这条语句将打印一个随机值。若是个静态变量,这条语句将打印0;
2)这条语句将把存储ppi的地址作为十进制整数打印出来,这个值并不是很有用;
3)这条语句的结果是不可预测的,对ppi不应该执行间接访问的操作,因为它尚未被初始化。
ppi=π这条语句把ppi初始化为指向变量pi,以后我们就可以安全地对ppi执行间接访问操作了。
*ppi=&i;这条语句把pi(通过ppi间接访问)初始化为指向变量i,经过上述两条语句后,变为:
i(?)<---pi()<---ppi()
此时使用一下语句具有相同的效果:
i='a';
*pi='a';
**ppi='a';
在一条简单的对i赋值的语句就可以完成任务的情况下,为什么还要使用更为复杂的涉及间接访问的方法呢?这是因为简单赋值并不总是可行的,例如链表的插入。这那行函数中,无法使用简单赋值,因为变量名在函数的作用域内部是未知的。函数所拥有的只是一个指向需要修改的内存位置的指针,所以要对该指针进行间接访问操作以访问需要修改的变量。
提示:只有当确实需要时,才应该使用多层间接访问,不然的话,你的程序将会变得更庞大、更缓慢、更难于维护。
2.高级声明
int f;//一个整型变量
int *f;//一个指向整型的指针
int* f,g;//f是指向整型的指针,g是整型变量
int f();//声明f为一个函数,它的返回值是一个整数
int *f();//f是一个函数,他的返回值是一个指向整型的指针
int (*f)();//f成为一个函数指针,它所指向的函数返回一个整型值
int *(*f)();//f是一个函数指针,它所指向的函数返回值是一个整型指针,必须间接访问才能得到一个整型值
int f[];//f整型数组
int *f[];//f是一个数组,它的元素类型是指向整型的指针
int f()[];//f是一个函数,返回值是一个整型数组,是非法的,因函数只能返回标量值,不能返回数组
int f[]();//这个也是非法的,因数组元素必须具有相同的长度,但不同的函数显然可能具有不同的长度
int (*f[])();//f是一个元素为某种类型的指针的数组,数组元素的类型是函数指针,它所指向的函数返回值s是一个整型值
int *(*f[])();//创建了一个函数指针数组,指针所指向的类型是返回值为整型指针的函数
3.函数指针
函数指针最常见的两个用途是转换表和作为参数传递给另一个函数。和其它指针一样,对函数指针执行间接访问之前必须把它初始化为指向某个函数,例如:
int f(int);
int (*pf)(int)=&f;
在函数指针的初始化之前具有f的原型是很重要的,否则编译器就无法检查f的类型是否与pf所指向的类型一致。初始化表达式中的&操作符是可选的,因为函数名被使用时总是由编译器把它转换为函数指针,&操作符只是显示地说明编译器将隐式执行的任务。
回调函数:用户把函数指针作为参数传递给其它函数,后者将“回调”用户的函数,当不知道上下指针类型,解决这个难题大方法是把参数声明为void *,表示“一个指向未知类型的指针”。
转移表:
switch(oper){
case ADD:
result=add(op1,op2);
break;
case SUB:
result=sub(op1,op2);
break;
case MUL:
result=mul(op1,op2);
break;
case DIV:
result=div(op1,op2);
break;
......
上述为某袖珍式计算器,对于具有上百操作符的计算器,使用switch方法将会非常之长。可以使用转换表来实现相同的任务,转换表就是一个函数指针数组。
创建一个转换表需要两个步骤,首先,声明并初始化一个函数指针数组,唯一需要留心之处就是确保这些函数的原型出现在这个数组的声明之前。
double add(double,double);
double sub(double,double);
double mul(double,double);
double div(double.double);
...
double (*oper_func[])(double,double)={
add,sub,mul,div,...
}
初始化列表中各个函数名的正确顺序取决于程序中用于表示每个操作符的整个代码,则上述可以计算器可替换为:
result=oper_func[oper](op1,op2);
oper从数组中选择正确的函数指针,而函数调用操作符将执行这个函数。
注意:在转换表中越界下标引用就像在其它任何数组中一样是不合法的,但一旦出现这种情况,把它诊断出来就要困难得多。当出现这种错误发生时,程序有可能在三个地方终止:
1)访问数组越界,它所标识的位置可能在分配给该程序的内存之外,有的操作系统能检测到这个错误并终止错误;
2)访问到有效越界(不可预测的值代表程序中一个有效地址)地址,若不是有效地址程序会终止,否则问题的调试将极其困难;
3)若拿到有效越界地址,如这个随机地址位于一块存储数据内存中,程序通常会很快终止,这通常是由于非法指令或非法的操作数地址所致。要想知道机器为什么会到这个地方,唯一的线索是转移表调用函数时存储于堆栈中的返回地址,若任何随机指令在执行时修改了堆栈或堆栈指针,那么这个线索也消失了。更糟的是如果这个随机地址恰好位于一个函数内部,那么该函数就会继续执行,修改谁也不知道的数据直到程序结束。
为了防止越界访问的情况,可以在调用转移表前后加打印信息帮助定位,也可以在调用转移表之前做相关越界判断来防止非法访问。
4.命令行参数
处理命令行参数是指向指针的指针的另一个用武之地,int main(int argc,char **argv);
5.字符串常量
当一个字符串常量出现于表达式中时,它的值是一个指针常量,当数组名用于表达式中时,它们的值也是指针常量。
6.总结
一个指针变量可以指向另一个指针变量,和其他的指针变量一样,一个指向指针的指针它在使用之前必须进行初始化,为了取得目标对象,必须对指针的指针执行双重的间接访问操作。可以使用函数指针来实现回调函数,一个指向回调函数的指针作为参数传递给另一个函数后者使用这个指针调用回调函数。转移表也是函数指针,使用转移表必须始终保证下标值处于适当的范围内,因为在转移表中调试错误是非常困难的。若某个执行环境实现了命令行参数,这些参数是通过两个形参传递给mian函数的,程序可以通过对argv使用间接访问操作来访问命令行参数。出现在表达式中的字符串常量的值也是一个常量指针,它指向字符串的第一个字符,和数组名一样,既可以使用指针表达式也可以用下标来使用字符串常量。
7.警告的总结
1)对一个未初始化的指针执行间接访问操作;
2)在转移表中使用越界下标;
8.编程提示总结
1)如果并非必要,避免使用多重间接访问;
2)cdecl程序可以帮助分析复杂的声明;
3)把void *强制换行为其它类型的指针时必须格外小心;
4)使用转移表时,应始终验证下标的有效性;
5)破坏性的命令行参数处理方式将使用户以后无法再次进行处理;
6)不寻常的代码始终应该加上一条注释,描述它的目的和原理;