C语言中级指针


C语言中级培训C语言中级培训 八、再谈指针 先说地址的概念 内存地址:物理内存中存储单元的编号。 使用内存:在地址所标识的存储单元中存放数 据。据。 注意:内存单元的地址与内存单元中的数据是 两个完全不同的概念。两个完全不同的概念。 一个是address; dd另一个是value; 访问方式: address name value访问方式: (1)直接访问──使用变量名进行存取 2 (2)间接访问──通过该变量的地址来访问 指针的概念 指针是C语言提供给编程者符号化使用硬件地址 的一种方法,也是将常量地址变量化的方法。的一种方法,也是将常量地址变量化的方法。 是一种特殊形式的变量。是一种特殊形式的变量。 特在哪儿?特在哪儿? • 其value 是别人的地址、 address • 类型不用于指导开辟空间、 • 可以无类型、 address name value • 可以无类型、 • 间址访问才有意义、 3 • 行为受限。 指针和地址指针和地址 指针是一种容纳地址的变量,通常也叫指针变量,统称指指针是一种容纳地址的变量,通常也叫指针变量,统称指 针。而地址则是内存单元的编号,是值。指针绝对不等 同于地址,千万不要把二者意义混淆。 针。而地址则是内存单元的编号,是值。指针绝对不等 同于地址,千万不要把二者意义混淆。同于地址,千万不要把二者意义混淆。同于地址,千万不要把二者意义混淆。 实际上指针是数据类型的一种 它应该跟整型、字符■ 实际上指针是数据类型的一种,它应该跟整型、字符 型,浮点型是一样的,只不过是组合类型罢了 。 ■ 指针变量只是存储地址类型的变量,它不是数据类型。■ 指针变量只是存储地址类型的变量,它不是数据类型。 指针能够进行加减法,原因并不是因为它是指针。加减法指针能够进行加减法,原因并不是因为它是指针。加减法 不是指针变量的专利,而是地址这种数据类型的本能, 正是因为地址具有加减的能力,所以才使指针作为存放地 4 址的变量能够进行加减运算 。 指针的初始化 一般形式: [存储类型] 数据类型 *指针变量名=初始地址值;[存储类型] 数据类型 指针变量名=初始地址值; 例例 void main( ) { int i; static int *p=&i; // ( × ) .............. } 不能用auto变量的地址 去初始化static型指针 5 Why ? 指针的声明 b是指针吗? 1)char * a,b; 2)char *a b;2)char *a,b; 应该写成:应该写成: char *a,*b; 或干脆另写一句:char * b;或干脆另写一句:char b; 这会万无一失! 还可以: typedef char * pchar; 然后 pchar a b c; 6 然后 pchar a,b,c; 指针的类型 对于 int * p = &i ;语句的正确理解是:定义了一个 类型为 int * 的指针。 此时不要将 int 和 * 分开,因为 int *是个新类型,是 组合类型。许多人错误地认为这是定义了一个整型( int) 的指针 。这恰是将“定义了一个指针”,还是“对的指针(*p)。这恰是将“定义了一个指针”,还是“对p 作求值运算”混淆的根源。 df i * I Ptypedef int * IntPtr; IntPtr pa,pb,pc ; tdfit**ItPtPttypedef int ** IntPtrPtr; IntPtrPtr ppa,ppb,ppc ; tdfh*ChPttypedef char * CharPtr; CharPtr pCh1 , pCh2; tdfid*VidPt 7 typedef void * VoidPtr; VoidPtr pVoid; 指针的使用 可以有以下9种用法: • 赋值;• 赋值; • 求值(间接访问); • 取址; • 自增/自减;• 自增/自减; • 加/减一个整数; • 求差; • 比较;• 比较; • 作函数的参数或返回值; 8 • 调用所指的函数。 指针的赋值 仅有5种形式: p= &a; p = A;p ; p = q; p=f;p = f; p = NULL; 指针是最”手眼通天、神通广大的特权者” ,它 不受栈的约束,可以访问栈存储区、静态存储 区、堆存储区,甚至代码区。 9 区、堆存储区,甚至代码区。 最能体现C的灵活性,但也为C程序埋下了隐患。 指针的算术运算 可以进行的算术运算,只有以下几种: px±n, ++px/px++, --px /px--, px-py px±n:将指针从当前位置向前(+n)或回px±n:将指针从当前位置向前(+n)或回 退(-n)n个数据单位,而不是n个字节。显 然, 和 是 ± 的特例然,++px/ px++和--px/px-- 是px±n的特例 (n=1)。 px-py:两指针之间的数据个数,而不是指 针的地址之差。针的地址之差。 10 指出下面各题的错误,分析错误原因指出下面各题的错误,分析错误原因 1. int i,*p; p=i; 2 h ****t2. char *p,*q,*r,*t; r=(p+q)/2;r(pq)/2; t=p+(q-p)/2; 3. int *ipt; fl t *f tfloat *fpt; int(ipt-fpt)>0 11 (p p) fpt=ipt; 指针与变量的关系 指针变量和它所指向的变量之间的关系是用指针运算 符“ ”表示的。 求值,又叫“间接访问 、“间符“ * ”表示的。—— 求值,又叫“间接访问”、“间 址运算” 例如,指针变量num_pointer与它所指向的变量num 的关系,表示为:*num pointer,的关系,表示为: _p , 即*num_pointer等价于变量num。 因此,下面两个语句的作用相同:因此,下面两个语句的作用相同: num=3; /*将3直接赋给变量num*/ num_pointer=# //使num_pointer指向num *num pointer=3; //将3赋给指针所指向的变量 12 num_pointer=3; //将3赋给指针所指向的变量 指针变量与零值比较: 指针变量的零值是“空”(记为NULL)。尽管NULL 的值与0相同,但是两者意义不同。假设指针变量的的值与0相同,但是两者意义不同。假设指针变量的 名字为p,它与零值比较的标准if语句如下: if (p == NULL)// p与NULL比较,强调p是指针变量if (p == NULL)// p与NULL比较,强调p是指针变量 或 if (p != NULL) 不要写成 if (p == 0) // 容易让人误解p是整型变量if (p 0) // 容易让人误解p是整型变量 if (p != 0) 或:或: if (p) // 容易让人误解p是布尔变量 13 if (!p) 有时候我们可能会看到: if (NULL )if (NULL == p) 为何有意把p和NULL的位置颠倒?p 因为当把 if (p == NULL)不小心误写成 if (p = NULL)时,编译器会认为 if (p = NULL) 是合法的,它NULL)时,编译器会认为 if (p = NULL) 是合法的,它 将执行一条赋值语句,然后将表达式的值作为if的判断 依据。显然不是我们的本意。但若把依据。显然不是我们的本意。但若把 if ( NULL == p) 写成 if ( NULL = p),编译器会指出该语法错误,因为(p) NULL不能被赋值。这样可以避免不易察觉的错误的发 生,让编译器替我们排除更多的错误。 14 生,让编译器替我们排除更多的错误。 指针的漏洞 VC++6.0竟然允许这样使用指针: #i l d tdi h 思考会引起什#include void main() 思考会引起什 么严重后果? !{ int a = 1234; ! int a 1234; // 0x0012ff7c是&a的值。 int *p=((int*)0x0012ff7c );int p =( (int )0x0012ff7c ); printf("&a = %x\n",&a); i tf(" %d\ *%d\ "*)printf("a = %d\n*p = %d\n",a,*p); } 15 关于“野指针” “野指针”是未指向具体变量的指针 (又称 ”指针悬空”,此时它会乱指一气)。指针悬空 ,此时它会乱指一气)。 因为创建一个指针时,系统只是分配了指针 本身的空间,没有分配所指目标的空间。 “野指针”产生的3种原因:“野指针”产生的3种原因: - 指针变量没有被初始化。指针变量刚被创建时指针变量没有被初始化。指针变量刚被创建时 不会自动成为NULL指针,它的默认值是随机的; 指针被f 或dl 之后,没有置为NULL,- 指针被free或delete之后,没有置为NULL, 让人误以为它仍然是个合法的指针; 16 ; - 指针操作超越了变量的作用范围; 避免“野指针”的对策: 使用指针前一定要保证它指向了有效的内存空间( 或者申请,或者让指针指向一块合法的空间)或者申请,或者让指针指向一块合法的空间) 用malloc或new申请内存之后,应该立即检查指针 值是否为NULL。值是否为NULL。 不要忘记为数组和动态内存赋初值。防止将未被初 始化的内存作为右值使用。始化的内存作为右值使用。 避免数组或指针的下标越界,特别要当心发生“多 1”或者“少1”操作。 动态内存的申请与释放必须配对,防止内存泄漏。 动态内存的申请与释放必须配对,防止内存泄漏。 用free或delete释放了内存之后,立即将指针设置 为NULL,防止产生“野指针”。 17 为NULL,防止产生“野指针”。 下面程序会是何结果?——野指针的害处 void main() { char *input1, *input2; input1 = (char*)malloc(20);input1 = (char*)malloc(20); strcpy (input1, "this is string1"); printf("%s\n", input1); //printf( %s\n, input1); // free(input1); // 此时的指针是野指针 input2 = (char*)malloc(20); //input2得到的空间是 input1的 strcpy (input2, "this is string2"); printf("%s\n", input2); if( i t1 ! NULL ) // 此时it1指向的是it2的空间if( input1 != NULL ) // 此时input1指向的是input2的空间 { strcpy (input1 “hello world”); //此时更改的是input2strcpy (input1, hello world ); //此时更改的是input2 } printf(“%s\n”, input1); // 两个指针所指内容相同 18 printf("%s\n", input2); } 指针类型转换的风险 大存储类型的指针向小存储类型的指针转换,会带来错误风险 如 int a =0x12345678; int *pa = &a; char *pc = (char*)pa; (*pa)的数值为0x12345678; (*pc)的数值为0x12;( pa)的数值为0x12345678; ( pc)的数值为0x12; pa 0x12 0x34 0x56 0x78 a(4byte) cp 小存储类型的指针向大存储类型的指针转换,会带来极大的错 误风险 如 char b = 0x12; pb如 char b = 0x12; char *pb = &b; int *pd = (int*)pb; 0x12 未知 未知 未知 pd pb 19 int pd = (int )pb; *pb的数值为0x12,*pd 的数值未知;b(1byte) pd 什么是数组名 数组名是一个地址,而且还是一个不可修改的常地址。数组名是一个地址,而且还是一个不可修改的常地址。数组名是一个地址,而且还是一个不可修改的常地址。 完整的说,数组名就是一个地址常量。 数组名这个符号代表了数组内存的首地址。 数组名是一个地址,而且还是一个不可修改的常地址。 完整的说,数组名就是一个地址常量。 数组名这个符号代表了数组内存的首地址。数组名这个符号代表了数组内存的首地址。 注意:不是这个符号的值是首地址,是这个符号本身 数组名这个符号代表了数组内存的首地址。 注意:不是这个符号的值是首地址,是这个符号本身 就代表一个地址。就代表一个地址。 由于数组名是一个符号常量,因此它只能是个右值, 而指针作为变量是个左值, 数组名永远都不会是一个指针变量。 20 C语言中对于多维数组的实现C语言中对于多维数组的实现 C语言只能用数组的数组当作多维数组。C语言只能用数组的数组当作多维数组。 数组的数组与多维数组的主要区别,就在于数组的数组 各维之间的内在关系是一种鲜明的层级关系。各维之间的内在关系是一种鲜明的层级关系。 上一维把下一维看作下一级数组,也就是数组嵌套。 类型是层次间联系的纽带。类型是层次间联系的纽带。 数组引用时需要层层解析,直到最后一维。 21 数组引用时需要层层解析,直到最后一维。 实际存储的格式 A[4][5][3] 多维数组外观 [0][0] [0][1] [0][2] [0][0][0] [0][0][1] [0][0][2] [0][1][0] A002 A012 A022 A032 A042 A001 A011 A021 A031 A041 [0][3] [0][4] [1][0] [0][1][1] [0][1][2] [0][2][0] [0][2][1] A102 A112 A122 A132 A142 A101 A111 A121 A131 A141 A000 A010 A020 A030 A040 [0] A [1][1] [1][2] [1][3] [0][2][1] [0][2][2] [0][3][0] [0][3][1] [0][3][2] A202 A212 A222 A232 A242 A201 A211 A221 A231 A241 A100 A110 A120 A130 A140 [1] [2] [3] [1][4] [2][0] [2][1] [0][3][2] [0][4][0] [0][4][1] [0][4][2] A302 A312 A322 A332 A342 A301 A311 A321 A331 A341 A200 A210 A220 A230 A240 [2][2] [2][3] [2][4] [3][0] A300 A310 A320 A330 A340 [3][0] [3][1] [3][2] [3][3] 22 [3][3] [3][4] 指针与数组 永恒的公式:间址(首址 + 偏移量)永恒的公式:间址(首址 + 偏移量) “ 指针即含有一维数组!” 与一维数组的关系:指向一维数组的指针。与一维数组的关系:指向一维数组的指针。 与N维数组的关系:指向N-1维数组的指针。 23 访问数组元素 地址 元素 地址 元素 a[0] a[1] a +1 a[0] [1] a[0] a[1] p +1 *p *( +1) *a *(a+1) p[0] p[1]a[1] a[2] a[3] a+1 a+2 a[1] a[2] a[1] a[2] a[3] p+1 p+2 *(p+1) *(p+2) *(a+1) *(a+2) p[1] p[2] a[3] a[9] ... a+9 a[9] a[3] a[9] ... p+9 *(p+9)*(a+9) p[9][] p (p) 下标法 指针法计算法 指针下标法 24 a[i] ⇔ *(a+i) ⇔ *(p+i) ⇔ p[i] 三种方法的统一:三种方法的统一: 首址 偏移量 运算首址 偏移量 运算 下标法 aia[ i ]a[ i ]下标法 [][] 计算法 ai**(a+i)(a+i) 指针法 已用 移指针法 p 已用p++移 到当前位置 *p 三种方法的本质都是:间址(变址)间址(变址) 无论是下标法(a[a[ ii ]]))还是计算法(**(a+i)(a+i)))还是指针法无论是下标法(a[a[ ii ]]))还是计算法(**(a+i)(a+i)))还是指针法 (*((p +i)+i) ),尽管表现形式不同,可本质都是: 25 **((首址首址++ 偏移量偏移量)) 与与指针相关的指针相关的运算符运算符 名称 定义符 运算符名称 定义符 运算符 * 指针运算符 定义了一个指针 间址运算符间址运算符((求值求值)) int *p = & i; * p= 30;* p= 30; & 取址运算符 定义了一个引用 取址运算符取址运算符((求址求址))& 取址运算符 定义了一个引用 int &r = i; 取址运算符取址运算符((求址求址)) &i&i [] 下标运算符 定义了一个数组 下标运算符(求元素)[ ] 下标运算符 定义了一个数组 int a[10] ; 下标运算符(求元素) a[4] = -1; 下标运算符[ ] 则是 *(首址 + 偏移量 )。其中含 有多重运算:先求类型尺寸,再乘,再加,变址后, 26 再间址。括号提高了运算优先级。 对二维数组元素的访问:对二维数组元素的访问: 因因aa是二维数组名,代表二维数组的首地址,不能用是二维数组名,代表二维数组的首地址,不能用**aa 来获得来获得a[0][0]a[0][0]的值的值,*a,*a只相当于只相当于a[0];a[0];来获得来获得 [][][][]的值的值,, 只相当于只相当于 [];[]; 正确的访问形式是:正确的访问形式是: intint ii,,a[ 3 ] [ 4 ] = { { 1,2 },{ 3,4,5 },{ 6,7,8,9 } }a[ 3 ] [ 4 ] = { { 1,2 },{ 3,4,5 },{ 6,7,8,9 } };;,, [ ][ ] {{ , },{ , , },{ , , , }}[ ][ ] {{ , },{ , , },{ , , , }};; intint (*p)[4] = a;(*p)[4] = a; ii=a[0][1];=a[0][1]; 下标法下标法ii a[0][1]; a[0][1]; 下标法下标法 或或 ii=*(*(a+0)+1); =*(*(a+0)+1); 计算法计算法 或或 ii=*(*(p+0)+1);=*(*(p+0)+1); 指针法指针法或或 ii ((p0)1); ((p0)1); 指针法指针法 或或 ii=*(a[0]+1); =*(a[0]+1); 下标计算法下标计算法 或或 ii=*(a+0)[1];=*(a+0)[1]; 计算下标法计算下标法或或 ii (a+0)[1]; (a+0)[1]; 计算下标法计算下标法 或或 ii=*(p+0)[1]; =*(p+0)[1]; 指针计算下标法指针计算下标法 或或 ii=*(p[0]+1);=*(p[0]+1); 指针下标计算法指针下标计算法 27 或或 ii (p[0]+1); (p[0]+1); 指针下标计算法指针下标计算法 C语言规定:数组名代表了数组的地址,所以a[i] 是第i行那个一维数组的地址, 它指向该行的第0列元 素。 a[i]可视为“一个一维数组的列指针”。素。 a[i]可视为“一个一维数组的列指针”。 同理, a可视为“一个以一维数组为大元素的一维数组 的指针”。 以此作为递归单元,即可推衍出N维数组。以此作为递归单元,即可推衍出N维数组。 28 对于 :int A[4][5][3]; 定位一个具体的值 如 ,只要定位一个具体的值, 如 A[3][4][1] ,只要 *(*(*( A+3)+4)+1)((( ) ) ) C语言的数组实现并非真正的多维数组,而是 数组嵌套,访问某个元素的时候,需要逐层向下解析。数组嵌套,访问某个元素的时候,需要逐层向下解析。 第一维元素A[0] 是A[0]所代表的那个数组的首地址, 但是这个表达式在C 里面有特殊意义。 特殊之处在于 第一维元素A[0] 是A[0]所代表的那个数组的首地址, 但是这个表达式在C 里面有特殊意义。 特殊之处在于但是这个表达式在C 里面有特殊意义。 特殊之处在于 它所代表的东西与一般的地址不同。而且类型也不是 一般的地址类型,叫数组类型。 但是这个表达式在C 里面有特殊意义。 特殊之处在于 它所代表的东西与一般的地址不同。而且类型也不是 一般的地址类型,叫数组类型。 29 一般的地址类型,叫数组类型。一般的地址类型,叫数组类型。 若有 int i = 10; it* &iint * p = & i ; & * p 意味着什么? pp 意味着什么? * & i 又意味着什么? p i 当遇到[ ][ ]运算符的时候,编译器只是简单地把它转换 为类似*(*(A+i)+j)这样的等价表达式 。为类似*(*(A+i)+j)这样的等价表达式 。 例如 A[B] 等价于 *(A+B). 对于 例如 &A[B]的操作 ,编译器将直接将其优化成 A+B 30 A+B. 数组类型跟一般地址类型的区别数组类型跟一般地址类型的区别 ■ 最主要的区别就是长度不同 ■ 一般的地址长度为4 (不同的系统可能会不一样)■ 一般的地址长度为4 (不同的系统可能会不一样) ■ 数组类型的长度代表的数组的长度。 如在int A[5][3];中 sizeof( A[0] ) ==3*sizeof(int) 数组类型在数组的定义与引用中具有非常重要的作用, 它可以用来识别一个标识符或表达式是否真正的数组。 数组类型在数组的定义与引用中具有非常重要的作用, 它可以用来识别一个标识符或表达式是否真正的数组。它可以用来识别一个标识符或表达式是否真正的数组。 一个真正数组的数组名,是一个具有数组类型的地址常 量,它的长度,是整个数组的长度,并非一个一般地址 它可以用来识别一个标识符或表达式是否真正的数组。 一个真正数组的数组名,是一个具有数组类型的地址常 量,它的长度,是整个数组的长度,并非一个一般地址量,它的长度,是整个数组的长度,并非一个一般地址 的长度,如果一个标识符不具备数组类型,它就不是 量,它的长度,是整个数组的长度,并非一个一般地址 的长度,如果一个标识符不具备数组类型,它就不是 31 一个真正的数组。一个真正的数组。 &a[0][0][0]仅仅是一个地址,它的意义,仅仅表示元素&a[0][0][0]仅仅是一个地址,它的意义,仅仅表示元素 a[0][0][0]的地址。sizeof(&a[0][0][0])的结果只是4。 不少人把它说成是数组a的首地址,错误!这是对 a[0][0][0]的地址。sizeof(&a[0][0][0])的结果只是4。 不少人把它说成是数组a的首地址,错误!这是对不少人把它说成是数组a的首地址,错误!这是对 数组首地址概念的滥用。真正能代表数组a的数组首地 不少人把它说成是数组a的首地址,错误!这是对 数组首地址概念的滥用。真正能代表数组a的数组首地 址只有a本身,数组首地址是具有数组类型的地址, sizeof(a)结果是i*j*k*sizeof(int),而不是4。只不过由 址只有a本身,数组首地址是具有数组类型的地址, sizeof(a)结果是i*j*k*sizeof(int),而不是4。只不过由sizeof(a)结果是i j k sizeof(int),而不是4。只不过由 于a[0][0][0]位置特殊,是数组a的第一个元素,所以它 sizeof(a)结果是i j k sizeof(int),而不是4。只不过由 于a[0][0][0]位置特殊,是数组a的第一个元素,所以它 们的地址值才相同。们的地址值才相同。 32 虽然指针和数组都表示地址,有时可以混用、相互替换。 但它们是有区别的。但它们是有区别的。 指针指向的是内存块,可以改变;而数组名是某块固定 内存的地址,不能改变。内存的地址,不能改变。 如:char a[10], *p; p = a; //OK char a[10], b[10]; a = b; // error 通过数组名可以求得数据块的大小,通过指针则不能 int a[10]; int *p;p; p = a; printf( “%d”, sizeof( a )); /* output is 40 */ 33 p (% , ( )); p printf( “%d”, sizeof( p )); /* output is 4 */ 对于指针和数组的区别还表现在使用scanf和printf上: void main() { char name[14],*p=name; scanf(“%s”, &name); 对于数组名可以带&也可以不带scanf(“%s”, name); scanf(“%s”, p); 对于数组名可以带&也可以不带 (,p); scanf(“%s”, &p); printf("%s\n",&name); 但对于指针则一定不能带& printf( %s\n, &name); printf("%s\n", name); printf("%s\n",p); 对于数组名可以带&也可以不带 printf( %s\n, p); printf("%s\n", &p); } 但对于指针则一定不能带& 34 } 当数组作为函数的参数进行传递时,该数组自动蜕化当数组作为函数的参数进行传递时,该数组自动蜕化当数组作为函数的参数进行传递时,该数组自动蜕化 为同类型的指针。 当数组作为函数的参数进行传递时,该数组自动蜕化 为同类型的指针。 因为当初ANSI委员会制定标准时,从C程序的执行效率 出发,不主张参数传递时复制整个数组,而是传递数组 因为当初ANSI委员会制定标准时,从C程序的执行效率 出发,不主张参数传递时复制整个数组,而是传递数组出发,不主张参数传递时复制整个数组,而是传递数组 的首地址,由被调函数根据这个首址处理数组中的内容。 出发,不主张参数传递时复制整个数组,而是传递数组 的首地址,由被调函数根据这个首址处理数组中的内容。 那么谁能承担这种“转换”呢?主体必须具有地址数据 类型,同时应该是一个变量,满足这两个条件的,当然 那么谁能承担这种“转换”呢?主体必须具有地址数据 类型,同时应该是一个变量,满足这两个条件的,当然类型,同时应该是一个变量,满足这两个条件的,当然 非指针莫属 。 类型,同时应该是一个变量,满足这两个条件的,当然 非指针莫属 。 35 指向多维数组的指针指向多维数组的指针 通常,对于int a[8][9]这个二维数组,我们可以定义 一个指向它的指针: int (*p)[9]; 这叫“指向二维数组指针变量”。( p)[ ]; 这叫“指向二维数组指针变量”。 它的一般说明形式为: 类型说明符 (*指针变量名)[长度]类型说明符 (*指针变量名)[长度] 其中“类型说明符”为所指数组的数据类型。“*” 表表 示其后的变量是指针类型。“长度”表示二维数组分 解为多个一维数组时, 一维数组的长度,也就是二 36 解为多个一维数组时, 一维数组的长度,也就是二 维数组的列数。 思考: i t (* )[9] 和 it*[9]的区别?思考: int (*p)[9] 和 int *p[9] 的区别? void main() {{ static int a[3][4]={{0,1,2,3},{4,5,6,7},{8,9,10,11}}; int(*p)[4];int(*p)[4]; int i,j; p=a;p=a; for(i=0;i<3;i++) for(j=0;j<4;j++)for(j=0;j<4;j++) printf("%2d ",*(*(p+i)+j)); } 37 } 类似的 对于 一个指向 维数组的指针可以这样定义:类似的,对于 一个指向N维数组的指针可以这样定义: 类型说明符 指针变量名 长度 长度 长度类型说明符 (*指针变量名)[长度][长度]……[长度] int (*p)[x2][x3]......[xn]; 一个例子:int (* ptr)[3]; (p)[] ptr=new int[2][3]; int (* ptr2)[3][5]; ( )[][] ptr2=new int[4][3][5]; 38 再谈指针与数组 读这段代码,给出输出结果,并解释为什么? #i l d tdi h可见,“类型”#include void main() 请注意&a的含义! &a的类型是int(*) [5] 型 加1已经在界外, 可见,“类型” 起着关键作用!() { inta[5]={12345}; 型,加1已经在界外, 强转是恢复其普通地 址的身份。int a[5]={1,2,3,4,5}; int *ptr=(int *)(&a+1); //先取地址再加1 输出: 址的身份。 //printf(“%p,%p\n”,&a+1,ptr); // 将地址显示出来 printf("%d %d\n"*(a+1) *(ptr 1)); 输出:2,5 Why? printf( %d,%d\n , (a+1), (ptr-1)); } *(a+1)就是a[1],*(ptr 1)就是a[4] 执行结果是25。 39 *(a+1)就是a[1],*(ptr-1)就是a[4],执行结果是2,5 。 解释: 对于 系统不认为 是数组对于int *ptr=(int *)(&a+1); 系统不认为&a+1是数组 首地址+1。对数组名求址 (&a) ,会导致数组维数升级, 变成了数组指针,其类型为 。而这样的指针加变成了数组指针,其类型为 int (*)[5]。而这样的指针加 1,则要根据指针类型加上一个a数组的偏移,是偏移了 整个数组的大小(本例是 个 ),所以要加整个数组的大小(本例是5个int),所以要加 5*sizeof(int) 。于是ptr实际是&(a[5]),也就是a+5了, 实际指向 。ptr实际指向a[5]。 但,prt与(&a+1)类型是不相同的,prt-1只会减去一 个整型位置,即sizeof(int*)。 尽管a、&a的地址是同一个,但含意是不一样的。a 是数组首地址,也就是a[0]的地址,&a是对象(数组整 体)首地址。a+1是数组下一元素的地址,即a[1],而 40 [] &a+1是下一个数组的地址,即a[5]。 众所周知,C语言是没有字符串变量的,因而,C89 规定,字符串常量就是一个字符数组。因此,尽管字 符串常量的外部表现形式跟数组完全不同,但它的确符串常量的外部表现形式跟数组完全不同,但它的确 是一个真正的数组 . 字符串常量本身就是这个数组的首地址,并且具有数 组类型,对一个字符串常量进行sizeof运算,例如组类型,对一个字符串常量进行sizeof运算,例如 sizeof("abcdefghi"),结果是10,而不是4. 41 指针与数组的区别: char str[] = “hello”; char *p = str;char *p = str; int n = 10; 在32位计算机上,计算sizeof的值: sizeof (str) = ?sizeof (str) ? sizeof (p) = ? if()?sizeof (n) = ? void Fun(char str[100]) { sizeof (str) = ? 42 sizeof (str) ? } 字符串数组与一般数组的区别字符串数组与一般数组的区别字符串数组与一般数组的区别字符串数组与一般数组的区别 字符串常量存放在静态存储区,而一般数组(非static)字符串常量存放在静态存储区,而一般数组(非static) 则是在栈中静态分配的。 由于字符串常量是数组首地址,因此可以数组引用的 形式使用它,例如:形式使用它,例如: printf("%s", &"abcdefghi"[4]); 这将打印出字符串 。还可以这样:这将打印出字符串efghi。还可以这样: printf("%s", "abcdefghi"+4); 同样打印出字符串 。实际上,同样打印出字符串efghi。实际上,&“abcdefghi”[4] 等价于&*(“abcdefghi”+4),去掉&*后,就是 了 43 "abcdefghi"+4了 字符串的问题 【问题1】请问以下(1)和 (2)有区别吗? (1) h t [ ] “H ll W ld” 对。可以通过数组 名或指针去修改。(1) char str[ ] = “HelloWorld”; char *p = str; 名或指针去修改。 (2) char *p = “HelloWorld”; 【问题2】请问b和c的值各是多少? 错。因为指针指向 的是常量区,而p 却是个指向变量的【问题2】请问b和c的值各是多少? char a[ ] = “Hello”; 却是个指向变量的 指针,那意味着可 改所指的数据,故int b, c; b = sizeof(a); 改所指的数据,故 应视为错误:缺 const修饰。b sizeof(a); c = strlen(a); 修饰。 44 【问题3】回答以下问题: char str[]=“HelloWorld”;char str[ ] = “HelloWorld”; char *p = “HelloChina”; (1) 可以这样做吗? p = str ; (2) 这样做结果会怎样?(2) 这样做结果会怎样? (3) 串“HelloChina” 会消失吗? 这样可以吗?(4) 这样可以吗? str = p; (5) 可以这样做吗?为什么?() str[ 1 ] = ‘u’; p[1]=‘u’; 45 p [ 1 ] = ‘u’ ; [例] 有若干计算机图书,请按字母顺序,从小到大 输出书名。解题要求:使用排序函数完成排序,在主输出书名。解题要求:使用排序函数完成排序,在主 函数中进行输入输出。 /*案例代码文件名:AL9_12.C*/ /*程序功能:指针数组应用示例*/程序功能:指针数组应用示例 46 /* sort()函数:对字符指针数组进行排序 */ /*形参:name 字符指针数组,count 元素个数*//*形参:name——字符指针数组,count——元素个数*/ /*返回值:无 */ /***********************************************/ void sort(char *name[], int count) { char *temp p;char *temp_p; int i,j,min; /*使用选择法排序*/使用选择法排序 for(i=0; i0) /*若存在更小的串*/ min=j; /*则保存之*/min j; / 则保存之 / if(min!=i) /*存在更小的串,交换位置*/ temp_p=name[i],name[i]=name[min],name[min]=temp_p; } 47 } } main() /*主函数main()*/ {{ char *name[5]= {“BASIC”,”FORTRAN”,”PASCAL”,”C”,”FoxBASE”};{, , ,,}; int i=0; sort(name,5); /*使用数组名作实参,调用排序函数sort()*/ /*输出排序结果*/ for(; i<5; i++) i tf(“% \ ”[i])printf(“%s\n”,name[i]); } 程序运行结果:程序运行结果: BASIC C FORTRAN FoxBASE 48 PASCAL 程序说明: (1)实参对形参的值传递:(1)实参对形参的值传递: sort( name , 5 ); ↓ ↓↓ ↓ void sort(char *name[], int count); (2)字符串的比较只能使用strcmp()函数。 形参字符指针数组name的每个元素,都是一个指向字形参字符指针数组name的每个元素,都是一个指向字 符串的指针,所以有strcmp(name[min],name[j])。 49 【例】“锯齿数组”分配内存和释放内存的过程 样例。样例。 #include #include void main()void main() { int a1 = 5;int a1 = 5; int a2[ ] = {4,2,3,2,4}; itijint i,j; int * * p_array; p_array = (int**)calloc(a1,sizeof(int *)); for(i=0; i #include g void main() { int a[]={10 15 4 25 3 4};{ int a[]={10,15,4,25,3,-4}; int * p=&a[2]; printf("%d\n" , *(p+1)) ; printf("%d\n" p[-1]) ;printf( %d\n , p[-1]) ; printf("%d\n" , p-a) ; printf("%d\n" , a[*p++]) ; printf("%d\n" , *(a+a[2])) ; 52 p( , ([])) ; } 该程序显示什么? #i l d tdi h#include #include void main() {{ char s[]="computer", * p = s; printf("%s\n" , s) ; printf("\n%c\n"*p++) ;printf( \n%c\n , p++) ; printf("%c\n" , *(p++)) ; printf("%c\n" , (*p)++) ; printf("%s\n" s) ; 53 printf( %s\n , s) ; printf("\n%c\n" , *++p) ; printf("%c\n" , *(++p)) ; printf("%c\n" , ++*p) ; printf("%c\n" ++(*p)) ;printf("%c\n" , ++(*p)) ; printf("\n%s\n" , s) ;printf( \n%s\n , s) ; printf("\n%s\n" , p) ; } 又 strcmp( s ? ) ==0 54 又: strcmp( s , ? ) ==0 该程序显示什么? #include #include #include void main() { char * msg = "Hello world !";char msg = Hello world ! ; printf(" %s \n", msg ) ; strcpy(msg , "Hi !"); //试图修改常量区! printf(" %s \n" msg ) ;printf( %s \n , msg ) ; } 55 该程序显示什么? void main() { id * bf ll (0)void * buf =malloc(0); if ( buf == null )if ( buf == null ) printf(" is null \n" ) ; else printf(" is not null \n" ) ; } 56 } 指针与常( const ) 情况一:指针指向常量const int i const int i=40;const int i=40; int *pi=&i; //这样可以吗?不行,编译错。 但 只给出了警告。但VC++只给出了警告。 const int 类型的i的地址是不能赋给指向int 类型地址 的指针pi的。否则pi岂不是能修改i的值了吗! pi=(int* ) &i; //这样可以吗?强制类型转换可是C语() 言所支持的。 VC++下编译通过,但是仍不能通过*pi=80来修改i的下编译通过,但是仍不能通过 p 来修改 的 值。试试!看看具体的结果。 正确的写法是:const int * pi = &i ; 57 正确的写法是:const int pi = &i ; 情况二:常指针指向变量 int i=40;; int * const pi = &i; 此时指针是常量,它忠心耿耿的指着i,决不见异思此时指针是常量,它忠心耿耿的指着i,决不见异思 迁。但是通过pi却可以修改i的值,因为i是变量。 情况三:指向常量的常指针 ;const int i; const int * const pi=&i;p ; 你能想像pi能够作什么操作吗?pi值不能改,也不能 通过pi修改i的值。因为不管是*pi还是pi都是const的 58 通过pi修改i的值。因为不管是 pi还是pi都是const的. const与non-const的赋值问题 const char *p const char chconst char p const char ch char *p char ch 59 “ const * = non-const * ” const 指针可以得到变量的地址。 const char *p = NULL; char ch = ‘a’;char ch = a; p = &ch; //正确!p;正确! 非const指针不可以用const对象的地址赋值: char *p = NULL; const char ch = ‘a’; p = &ch; //错误! 60 p = &ch; //错误! 练习: void main() {{ char *p1 = NULL; char **p = &p1;char **p = &p1; const char **q = NULL; //以下语句都当作独立的语句处理。 q = p; //rightqp; //gt p = q; //error *p = *q //error*p = *q; //error *q = *p; //right 61 } 解释: q = p;正确! 是指向 类型的指针q是指向const char *类型的指针。 而p是指向char *类型的指针而p是指向char 类型的指针。 “const = non-const” p = q; 错误! 因为这样p经过两次间址就可以修改const对象的 值,编译器不允许这样。值,编译器不允许这样。 “non-const = const” 62 指针与函数 这叫返回类型 这叫参数类型指针出现在函数的三个地方: 这叫参数类型 返回类型 函数名( 形参表 ); ⑴ ⑵ ⑶⑴ ⑵ ⑶ 合起来叫函数类型 可以依此证实: typedef float (*pf)(int, double); 63 typedef float ( pf)(int, double); pf 是个类型名 指针与函数参数 函数的形参表中可以含有指针。有多种形式: 1. 指针作形参,如void f(int *p); 作用是对作 为变量的实参传址,如 f(& )为变量的实参传址,如 f ( &a ); 2 指向指针的指针作形参,如void f(int **pp);2. 指向指针的指针作形参,如void f(int pp); 作用是对作为指针的实参传址,如 f ( &p ); 3. 指针的引用作形参,如void f(int *&rp); 作 用是对作为指针的实参传址 如用是对作为指针的实参传址,如 f ( p ); 64 指针变量,既可以作为函数的形参,也可以作函数的实 参。 函数的形参即使写成数组形式,其实质也是指针: void fun( int * ptr)和void fun( int ptr[ ])void fun( int * ptr)和void fun( int ptr[ ]) 是等价的。甚至可以写成: 和void fun( int *)和void fun( int [ ]) 指针变量作实参时,与普通变量一样,也是“值传递”,指针变量作实参时,与普通变量一样,也是“值传递”, 即将指针变量的值(一个地址)传递给被调用函数 的形参(必须是一个指针变量)。的形参(必须是一个指针变量)。 请注意:被调用函数可以改变实参指针变量所指向的变 量的值。也可以改变形参的值,但对于实参指针变 65 量的值。也可以改变形参的值,但对于实参指针变 量没有影响——改了也白改。 void Allocate( char * & p , int size) { (h *) ll(i )p = (char*) malloc(size); } 此处改为char * p 行吗? void Test (void ) { 改为char ** p 呢? { char *str = NULL; Allocate ( str , 100 ) ; strcpy ( str , “Hello World!” ); printf ( str ) ; // 可以这样用 free ( str ); 66 } 用字符指针变量作形参: void copy_string(char *from,char *to) //指针作形参 { 此处的*to++=属”后++不否?应视为: *to = *from;for (;*from!=‘\0’;from++,to++) *to=*from; void copy_string(char *from,char *to) 此处的 属 后 不 能作左值”的情况吗?*to = *from; to++; from++;*to=‘\0’; } () { while ((*to=*from)!=‘\0’) from ; 作左值的不是后++, 而是指针自身,这 main() { h [] “I t h ” {from++;to++} }void copy_string(char *from,char *to) 完全不同于: i++ = j ++; char a[]=“I am a teacher.”; char b[]=“You are a student.”; i tf(“\ ti % \ ti b % \ ”b)void copy string(char *from char *to) { while (*to++=*from++); printf(“\nstring a=%s\nstring b=%s\n”,a,b); copy_string(a,b); printf(“\nstring a=%s\nstring b=%s\n” a b) void copy_string(char from,char to) { while ((*to++=*from++)!=‘\0’); } 注意: 和 可以换成 67 printf(“\nstring a=%s\nstring b=%s\n”,a,b) } while ((*to++=*from++)!=‘\0’); } 注意:a和b可以换成 指针吗?为什么? 主函数main()的形参 在以往的程序中,主函数main()都使用无参形式。实 际上,主函数 也是可以指定形参的。际上,主函数main()也是可以指定形参的。 主函数main()的有参形式: ,main(int argc, char *argv[]) {……} 实参的来源: 运行带形参的主函数,必须在操作系统状态下,输入主 函数所在的可执行文件名,以及所需的实参,然后回车 即可。即可。 命令行的一般格式为: 可执行文件名 实参[ 实参2 ] 68 可执行文件名 实参[ 实参2……] 本案例程序的用法:lock +|- <被处理的文件名>←┘ [案例9.13] 用同一程序实现文件的加密和解密。[ ] 约定:程序的可执行文件名为lock.exe; 其用法为:lock +|- <被处理的文件名>其用法为:lock | <被处理的文件名> 其中“+”为加密,“-”为解密。 /*案例代码文件名:AL9 13 C*// 案例代码文件名:AL9_13.C/ /*程序功能:带参主函数的应用示例*/ 69 main(int argc, char *argv[]) { char c;{ char c; if (argc != 3) printf("参数个数不对!\n"); else { c=*argv[1]; /*截取第二个实参字符串的第一个字符*/ switch(c) { case '+': /*执行加密*/{ case '+': /*执行加密*/ {/*加密程序段*/ printf("执行加密程序段。\n");p(执行加密程序段。 ); } break; case '-': /*执行解密*/ { /*解密程序段*/{ /*解密程序段*/ printf("执行解密程序段。\n"); } break;} break; default: printf("第二个参数错误!\n"); } } 70 } } 形参说明 (1)形参argc是命令行中参数的个数(可执行文件名本(1)形参argc是命令行中参数的个数(可执行文件名本 身也算一个)。 在本案例中,形参 的值为3 (l k、 | 、文件名)。在本案例中,形参argc的值为3 (lock、+|-、文件名)。 (2)形参argv是一个字符指针数组,即形参argv首先是 一个数组(元素个数为形参argc的值),其元素值都是 指向实参字符串的指针。指向实参字符串的指针。 在本例中,元素argv[0]指向第1个实参字符串“lock”, 元素argv[1] 指向第2个实参字符串“+|-”,元素argv[2]元素argv[1] 指向第2个实参字符串“+|- ,元素argv[2] 指向第3个实参字符串“被处理的文件名”。 71 函数的定义: 函数以及函数名 函数的定义: functiontype functionname( argument list) { …… function body …… } 函数名:函数名: 函数名是个常量(不能修改)。利用函数名可以访问 (调用)函数;(调用)函数; 那么我们可以定义函数指针,利用函数指针来完成对函 数调用。这个指针应该和所指的函数具有相同的类型。 72 数调用。这个指针应该和所指的函数具有相同的类型。 指向函数的指针 一个通常的函数调用的例子(没使用指针): void MyFun(int x); //此处的声明也可写成:y( ); 此处的声明也可写成: void MyFun( int ); void main(int argc, char* argv[])( g , g []) { MyFun(10); //这里是调用MyFun(10);函数y(); y(); } void MyFun(int x) //这里定义一个MyFun函数 { printf(“%d\n”,x); }} 从功能上或者说从数学意义上理解函数,MyFun函数名 代表的是一个功能(或是说一段代码)。 73 代表的是一个功能(或是说一段代码)。 函数指针变量的声明 如同某一数据变量的内存地址可以存储在相应的指 针变量中一样,函数的首地址也可以存储在某个指针针变量中一样,函数的首地址也可以存储在某个指针 变量里。这样,就可以通过这个指针变量来调用所指 向的函数了。向的函数了。 在C系列语言中,任何一个变量,总是要先声明,之 后才能使用的。那么,函数指针变量也应该先声明。后才能使用的。那么,函数指针变量也应该先声明。 以上面的例子为例,来声明一个可以指向MyFun函数 的函数指针变量FunP: void (*FunP)(int x) ; 也可写成void (*FunP)(int); 函数指针变量的声明格式,如同函数MyFun的声明一 样,只不过把MyFun改成(*FunP)而已。这样就有 了一个能指向MyFun函数的指针了。当然,这个指针 变量也可以指向其它所有具有相同函数格式的函数。 74 变量也可以指向其它所有具有相同函数格式的函数。 通过函数指针变量调用函数 有了FunP指针变量后,就可以对它赋值,指向 MyFun,然后通过FunP来调用MyFun函数了。MyFun,然后通过FunP来调用MyFun函数了。 void MyFun(int x); 也可声明成 ,void (*FunP)(int ); //也可声明成void(*FunP)(int x), 但习惯上一般不这样。 void main(int argc, char* argv[]) { MyFun(10); //这是直接调用MyFun函数 FunP=MyFun; //对函数指针赋值y; (*FunP)(20); //通过函数指针FunP来调用MyFun函数 FunP(20); //也可以写成这样 75 FunP(20); //也可以写成这样 } void MyFun (int x) //定义了MyFun函数 {{ printf(“%d\n”,x); }} MyFun与FunP的类型是相吻合的。函数MyFun好像 是一个 类型的数组名(常量),而 则是一个void (int )类型的数组名(常量),而*FunP则 是一个void (int )类型的指针变量。 就如同 : int A[10],*pi;int A[10], pi; pi = A; //与FunP=MyFun比较。 你的感觉呢? 76 你的感觉呢? 继续看以下几种情况:(这些可都是可以正确运行的代 码!)码!) void main(int argc, char* argv[]) {{ MyFun(10); //调用MyFun(10);函数 FunP=MyFun; //将函数地址赋给指针变量FunP=MyFun; //将函数地址赋给指针变量 (*MyFun)(10); //函数名MyFun也可以有这样的 调用格式,就如同用(*FunP)(20); 来调用MyFun函数调用格式,就如同用(*FunP)(20); 来调用MyFun函数 }} 真的是可以这样的! 77 依据以往的知识和经验来推理本篇的“新发现”,必 定会推断出以下的结论:定会推断出以下的结论: 1. MyFun的函数名与FunP函数指针都是一样的,即都 是函数指针。MyFun函数名是一个函数指针常量,而是函数指针。 y 函数名是一个函数指针常量,而 FunP是一个函数数指针变量,这是它们的关系。 2. 但函数名调用如果都得如(*MyFun)(10);这样,那2. 但函数名调用如果都得如( MyFun)(10);这样,那 书写与读起来都是不方便和不习惯的。所以C语言的设 计者们才会设计成可允许MyFun(10);这种形式地调用计者们才会设计成可允许MyFun(10);这种形式地调用 (这样方便多了并与数学中的函数形式一样)。 3 为统一起见,FunP函数指针变量也可以FunP(10)的3. 为统一起见,FunP函数指针变量也可以FunP(10)的 形式来调用。 4 赋值时,即可FunP=&MyFun形式,也可 78 4. 赋值时,即可FunP=&MyFun形式,也可 FunP=MyFun。 定义函数的指针类型: 就像自定义数据类型一样,可以先定义一个函数指针类 型,然后再用这个类型来声明函数指针变量。型,然后再用这个类型来声明函数指针变量。 下面先给出一个自定义数据类型的例子: typedef int* PINT; //为int* 类型定义了一个PINT的yp ; 为 类型定义了一个 的 别名 void main()void main() { int x;int x; PINT px=&x; //与int * px=&x;是等价的。PINT类型 其实就是 类型其实就是int * 类型. *px=10; //px就是int*类型的变量 . 79 } 下面来看一下函数指针类型的定义及使用:(请与上对 照!)照!) 此处的声明也可写成:void MyFun(int x); //此处的声明也可写成: void MyFun( int ); typedef void (*FunType)(int ); //这样只是定义一个typedef void ( FunType)(int ); //这样只是定义一个 函数指针类型 FunType FunP; //然后就可以用FunType类型来声明 变量 80 FunP变量 void main(int argc, char* argv[]) {{ FunType FunP; //声明了函数指针变量 MyFun(10);MyFun(10); FunP=&MyFun; (*FunP)(20);(*FunP)(20); } void MyFun(int x) {{ printf(“%d\n”,x); } 81 } 解析: 首先,在void (*FunType)(int ); 前加了一个typedef 。 这只是定义一个名为FunType的函数指针类型,而不是这只是定义一个名为FunType的函数指针类型,而不是 FunType变量。 然后使用FunType FunP; 这句就如PINT px;一样地声然后使用FunType FunP; 这句就如PINT px;一样地声 明了一个FunP变量。 其它相同。整个程序完成了相同的事。其它相同。整个程序完成了相同的事。 这样做法的好处是:这样做法的好处是: 有了FunType类型后,我们就可以同样地、很方便地用 FT 类型来声明多个同类型的函数指针变量了。如FunType类型来声明多个同类型的函数指针变量了。如 下面的代码: FT FP2,FP3 82 FunType FunP2,FunP3; 指向函数的指针的使用 int fun1(char*,int); char arr[ ] = “china!”; int fun2(char* int);int fun2(char ,int); void fun3(double *,int); 是指向函数的指针int(*pfun)(char*,int); // pfun1是指向函数的指针 pfun = fun1; //给函数指针赋值p; int a = (*pfun)(“abcdefg”, 7);//用指向函数的指针进行 函数调用函数调用 pfun = fun2; //指向函数的指针可以被修改 (* f )( 6) // 尽管类型不完全相符,亦可。a = (*pfun)( arr, 6); // 尽管类型不完全相符,亦可。 pfun = fun3; //错,类型不相符 83 pfun = fun2(); // 错。 Why? 观察下面的函数原型: id h ( i t (*FP)( h *) h * );void show ( int (*FP)(char *) , char * ); 其中:第一形参是指向函数的指针,第二形参是指向其中:第一形参是指向函数的指针,第二形参是指向 数据的指针变量。于是可以这样来调用: h(f1);或 h(f );show ( fun1, arr );或 show ( pfun, arr ); 这是一个以函数作为参数的 例题: 84 各种指针 请说明各指针的含义: int *ptr; char *ptr;p; int **ptr; int *ptr[3]; 一维指针数组int ptr[3]; int (*ptr)[3] 一维指针数组 二维指针数组 指向二维数组的指针 int *ptr [3][4]; int *(*ptr)[4]; 指向二维指针数组的指针 二维指针数组 void* (*ptr)(void*); int * pMove(); 指向函数的指针 返回指针的函数int pMove(); int (*p[3]) (int); it(* ) [3] [4] 返回指针的函数 函数指针数组,函数返回int型数据 指向三维数组的指针 85 int (*p) [3] [4]; int (*p[3]) [4]; 指向三维数组的指针 指针数组的每个元素都是指向二维数组的指针 观察总结1: “指向一个元素的指针”是指向一维数组的指针: int *ptr; int a[N];int a[N]; ptr = a; p; “指向一行元素的指针”是指向二维数组的指针: int (*ptr)[3]; i t [M][N]int a[M][N]; ptr = a; 86 p; 观察总结2: int *ptr[3]; int *ptr ; int (*ptr)[3]; int [3] (*ptr); [3] int (*ptr)[3]; int [3] (*ptr); int *(*ptr)[4]; (int *) (*ptr)[4]; i t (* t [3])[4] i t (* t )[4][3]int (*ptr[3])[4]; int (*ptr )[4]; 那么大家考虑: [3] 那么大家考虑: int (* (*func)(int *p) )[5]; char (*(* fun ( ) )[4]) (int *p); int *(*(*func)(int *(*)[10]))[10]; 87 int *(*(*func)(int *(*)[10]))[10]; 复杂指针解析的方法——分解法复杂指针解析的方法——分解法 首先从未定义的标识符起往右看,当遇到圆括号时,就 掉转方向往左看。解析完圆括号里面所有的东西,就整 首先从未定义的标识符起往右看,当遇到圆括号时,就 掉转方向往左看。解析完圆括号里面所有的东西,就整掉转方向往左看。解析完圆括号里面所有的东西,就整 个给它一个命名。将命名与外面的东西再重复这个过程 掉转方向往左看。解析完圆括号里面所有的东西,就整 个给它一个命名。将命名与外面的东西再重复这个过程 直到将整个声明解析完毕。外面的东西其实是类型。直到将整个声明解析完毕。外面的东西其实是类型。 ■■ 第一个例子第一个例子第一个例子第一个例子 int (*func)(int *p); 首先找到未定义的标识符,就是func,它的外面有一对首先找到未定义的标识符,就是func,它的外面有一对 圆括号,而且左边是个*号,这说明func是个指针,然 后跳出这个圆括号,并给它一个命名,比如F 。再看右后跳出这个圆括号,并给它一个命名,比如F 。再看右 边,也是一个圆括号,这说明F是个函数,而func是个 指向这类函数的指针,这类函数具有 *类型的形参, 88 指向这类函数的指针,这类函数具有int*类型的形参, 返回值类型是int。 ■■ 第二个例子 int (*func)(int *p, int (*f)(int*));( )( ( )( )) func被一对括号包含,且左边有个*号,说明func是个 指针,给它一个命名,比如F。其右边也有个括号,那 么func是个指向函数的指针,这类函数具有int *和么 是个指向函数的指针,这类函数具有 和 int (*)(int*)形参,返回值为int类型。再来看形参 int (*f)(int*),类似前面的解释,f也是一个函数指针,int (*f)(int*),类似前面的解释,f也是一个函数指针, 指向的函数具有int*类型的形参,返回值为int 89 ■■ 第三个例子 int (*func[5])(int *p); func右边是一个[]运算符,说明func是具有5个元素的func右边是一个[]运算符,说明func是具有5个元素的 数组,func的左边有个*,说明func的数组元素是指针, 要注意这里的*不是修饰func的,而是修饰func[5]的,要注意这里的*不是修饰func的,而是修饰func[5]的, 原因是[]运算符优先级比*高,func先跟[5]结合。 给它一个命名,比如 。看右边,也是一对圆括号,说给它一个命名,比如F 。看右边,也是一对圆括号,说 明func数组的元素是函数类型的指针,它所指向的函 数具有int*类型的形参,返回值类型为int。 所以func是“每个元素都是指向函数的指针的数组名”。 90 所以func是“每个元素都是指向函数的指针的数组名 。 ■■ 第四个例子 int (* (*func)[5] )(int *p); func被圆括号包含,左边又有*,那么func是个指针, 右边是个 号,那么说明f 是一个指向数组的指针,右边是个[]号,那么说明func是一个指向数组的指针, 给它一个命名,比如F ,那么其余部分就是数组的类 型了。往左看,有个*号,说明这个数组的元素是指针, 跳出括号,右边又有一对括号,说明是个形参。这说 明数组的元素是指向函数的指针。总结一下,就是: func是一个指向二维数组的指针,这个数组的每个元func是一个指向二维数组的指针,这个数组的每个元 素都是函数指针,这些指针指向具有int*形参,返回值 为int类型的函数。 91 为int类型的函数。 从以上解析可以看出,类型是揭示谜底的法宝。 解读: char ( * ( * ( * q ) ( ) ) [ ] ) ( ); ( * q ) ( ) —— k 然后其它的东西都是它的返回类型 ( * k ) [ ] —— m 这个数组的类型在下面 char ( * m)() 这是数组的类型char ( m ) ( ) 这是数组的类型 总结:q是个指向函数的指针,该函数无形参,它的 返回类型是指向二维数组的指针,数组元素的类型返回类型是指向二维数组的指针,数组元素的类型 又是指向函数的指针,无形参,返回值是字符型。 92 char ( * ( * ( * q ) ( ) ) [ 3] ) ( ); char ( * ( *)[3])() ( * q)();char ( ( ) [ 3] ) ( ) ( q ) ( ) ; char ( * ) ( ) ( *) [ 3] ( * q ) ( ) ; 结论:q是个指向函数的指针,该函数无形参,它的返回类型是指向 二维数组的指针,数组元素的类型又是指向函数的指针,无形参 93 二维数组的指针,数组元素的类型又是指向函数的指针,无形参 ,返回值是字符型。 ■■ 第五个例子 int (* (*func)(int *p) )[5];int ( ( func)(int p) )[5]; func是一个函数指针,这类函数具有int*类型的形参 (红的部分)。返回值(黑的部分)是指向数组的指(红的部分)。返回值(黑的部分)是指向数组的指 针,该指针所指向的数组的元素是具有5个int元素的 数组 int (* )[5] 。 94 ■■ 刚才这个例子 int (* (*func)(int *p) )[5];的解析: int (* (*func)(int *p) )[5]; ■■ 刚才这个例子 int ( ( func)(int p) )[5];的解析: int (* (*func)(int *p) )[5]; int (* )[5] (*func)(int *p) ; int (* )[5] (*)(int *p) func ; 95 int ( )[5] ( )(int p) func ; ■■ 第六个例子 char (*(* fun () )[4]) (int *p)char (( fun () )[4]) (int p) 从 看 是个函数,将其命名为 ,得:从fun ()看fun是个函数,将其命名为F,得: char (*(* F )[4]) (int *p) ;(( )[ ]) ( p) 分析(* F )[4],F是指向二维数组的指针,命名为P得 char (*P) (int *p) ;可见P是个指向函数的指针。char (P)(int p) ;可见P是个指向函数的指针。 于是,fun是一个函数,返回值是指向二维数组的指 针。所指向的数组的元素都是指针,每个指针都可以针。所指向的数组的元素都是指针,每个指针都可以 指向函数。这类函数具有返回char类型、形参是一个 96 且类型是整型指针的架构。 ■■ 第六个例子 h(*(*f())[4]) (it*) char (*(* fun ( ) )[4]) (int *p); char (*(* fun () )[4]) (int *p) char (( fun ( ) )[4]) (int p); char (*(* )[4]) (int *p) fun ( ) ; char (*) (int *p) (* )[4] fun ( ) ; 97 ()(p)()[] (); ■■ 第七个例子 int *(*(*func)(int *(*)[10]))[10];int ((func)(int ( )[10]))[10]; func是一个指向函数指针,这类函数用指向二维的指 针数组的指针做参数,返回值的也是同类型指针。针数组的指针做参数,返回值的也是同类型指针。 此句整体定义了一个指向函数的指针数组。 若定义了 后,就可以用若定义了typedef int * (*pa)[10]; 后,就可以用 pa(*func)(pa); 来代替该句。p ( )(p ) 98 ■■ 第七个例子 i*(*(*f )(i *(*)[10]))[10]; it*( *(*f )(i t *(*)[10]) )[10] int *(*(*func)(int *(*)[10]))[10]; int *( *(*func)(int *(*)[10]) )[10]; int *( * )[10] (*func) ( int *(*)[10] ); 此时回想 是什么?此时回想 int *(*ptr)[4] 是什么? 99 指向二维指针数组的指针 解读 1)int (*(*func)[5][6])[7][8]; 2)int (*(*(*func)(int *))[5])(int *);2)int (((func)(int ))[5])(int ); 3)int (*(*func[7][8][9])(int*))[5]; 1) func是指向三维数组的指针,这类数组的大元素是 具有5X6个int元素的二维数组,而指向三维数组的指针具有 个 元素的二维数组,而指向三维数组的指针 又是另一个三维指针数组的元素。 2) func是函数指针,这类函数的返回值是指向数组的) 是函数指针,这类函数的返回值是指向数组的 指针,所指向数组的元素也是函数指针,指向的函数具 有int*形参,返回值为int。有 形参,返回值为 。 3) func是个三维指针数组,数组元素是函数指针,这类 函数具有int*的形参,返回值是指向数组的指针,所指向 100 的数组的元素是具有5个int元素的数组。 指向指针的指针 int i = 20; it*i &iint *pi = &i; int **ppi = πpp p ; 指针变量ppi的内容就是指针变量pi的起始地址。于 是是…… ppi的值是多少呢?——比如10000。是pi的址. *i的值是多少呢 2006 即 i的值 也是i的址。*ppi的值是多少呢——2006,即pi的值,也是i的址。 **ppi的值是多少呢?——20,即i的值,也是*pi的值 。 它可以指向指针,亦可以指向指针数组。 101 它可以指向指针,亦可以指向指针数组。 指向指针的指针应用实例 设计一个函数:void find1(char array[], char hh* )search, char * pa) 要求:这个函数参数中的数组array是以\0值为结束的 字符串,要求在字符串 中查找与参数 h给出字符串,要求在字符串array中查找与参数search给出 的字符相同的字符。如果找到,通过第三个参数(pa) 返回 字符串中首先碰到的字符的地址。如果没找返回array字符串中首先碰到的字符的地址。如果没找 到,则为pa为NULL。 依题意,实现代码如下。依题意,实现代码如下。 102 void find1(char [] array, char search, char * pa) {{ int i; for (i=0;*(array+i)!=‘\0’;i++)(;(y) ;) { if (*(array+i)==search) { pa=array+i // pa得到某元素的地址 break;break; } else if (*(array+i)==0)else if ( (array+i)==0) { pa=0;pa 0; break; } 103 } } 你觉得这个函数能实现所要求的功能吗? 下面调用这个函数试试。 id i ()void main() { char str[]=“afsdfsdfdf”; //待查找的字符串char str[]= afsdfsdfdf ; //待查找的字符串 char a=’d’; //设置要查找的字符 char * p=0; //char p0;// find1(str,a,p); //调用函数以实现查找操作。 if (0==p ) h值为’d’,而 t 字(p) { printf (“没找到!\n”);//如果没找到则输出此句 } ch值为’d’,而str字 符串的第四个字符就 是’d’,应该找得到} else { 是’d ,应该找得到 呀!为何没找到! { printf(“找到了,p=%d”,p); //如果找到则输出此句 } 上面代码,你认为会输出什么呢? 104 } } 上面代码,你认为会输出什么呢? 分析:先看函数定义处:分析:先看函数定义处: void find1(char [] array, char search, char * pa) 再看调用处:find1(str,a,p); 请仔细考虑此时形实结合所发生的事:array得到了数 组名str,search得到了a的值, pa得到了p的值(而非 p的地址)!但p并未得到pa的值(某元素的地址)。p p p 可见尽管使用了指针,也并没实现传址,当实参形参都 是指针时,它们也仅仅是传值——传了别人的地址,没是指针时,它们也仅仅是传值 传了别人的地址,没 有传回来。 画出内存使用图示就清楚了。 105 就是更改形参 的值,而pa=…就是更改形参pa的值,而 p的只并没有被修改。 &p char XXchar char **pa XX*p *pa XX char XX*p *pa=…就是更改形参pa所指向的内 106 p 就是更改形参p 所指向的内 容的值,就是修改了p的值。 修正: void find2(char [] array, char search, char ** ppa) { int i; for (i=0;*(array+i)!=0;i++)for (i=0;*(array+i)!=0;i++) { if (*(array+i)==search)if ( (array+i)==search) { *ppa=array+ipp y break; } else if (*(array+i)==0) { * NULL*ppa=NULL; break; } 107 } } } 主函数的调用处改如下: find2(str a &p); //调用函数以实现操作。find2(str,a,&p); //调用函数以实现操作。 这样形实结合所发生的操作如下: ppa=&p;ppa=&p; ppa是指向指针p的指针。pp p 对*ppa的修改就是对指针p的修改。 看懂了这个例子,也就完全掌握了指针的精华。 108 如果定义了:const int **q ; **q *q 指针变量 指针变量 const 常量 类型是const int * 109 一个有趣的实例 不要将指针的用法照搬到指向指针的指针头上: char * pa 和const char * pa是类型相容的;p 和 p 是类型相容的; 但 char ** pa 和const char ** pa则类型不相容! 看下面代码:看下面代码: #include void fun1(const char *p) { }(p) { } void fun2( const char **p){} void main()() { char *p1; p; fun1(p1); // 非常指针作实参,OK! char **p3 = &p1; 110 pp // fun2(p3); // 非常指向指针的指针针作实参,error } 这是因为: 当 char *p1; const char *p2; 时 p2 = p1; 合法。但 p1 = p2; 就非 法。 对于char ** p3 ;它是指向非常量的二级指针(它所指 的指针也指向非常量:char * q1; p3 = &q1; );的指针也指向非常量:char q1; p3 = &q1; ); 而const char ** p4;则是指向常量的二级指针(即它 所指的指针是指向常量的:const char * q p4 &q )所指的指针是指向常量的:const char * q; p4 = &q;) 也就是说, char * 和 const char *类型不相容(这不 同于 char 和 const char 类型)!所以 p4 = p3是非法同于 char 和 const char 类型)!所以 p4 = p3是非法 的,当然p3 = p4也是非法的。 111 void 类型的指针 又称为“万能型”指针。 所谓“万能”是说该指针没保存类型信息,只所谓“万能”是说该指针没保存类型信息,只 是保存了个地址,不知该咋用这个地址。是保存了个地址,不知该咋用这个地址。 还记得一个故事说的,某人奉命送一封信,交 给对方后,对方掏出枪指着他的脑门说,你给对方后,对方掏出枪指着他的脑门说,你 知道信上说什么吗?让我杀掉来人。 void类型的指针就像这个送信人:只管送信, 不知信上说的是什么。 void类型的指针就这不知信上说的是什么。 void类型的指针就这 么傻!! 于是使用时一定要强制类型转换——对傻瓜的 112 于是使用时一定要强制类型转换——对傻瓜的 使用。 void类型的指针(void * pv; ),是表示行为不确定 的指针,但这不影响它已经被分配了空间,甚的指针,但这不影响它已经被分配了空间,甚 至得到了值——别人的地址。 void类型的指针不能参与算术运算(如 ++,--),不 能参与求值运算(*pv),只能进行被赋值、比较、能参与求值运算(*pv),只能进行被赋值、比较、 sizeof()操作。 113 void 指针的操作 1. 可以与另一个任何类型的指针相比较; 2. 可以向函数的void类型的形参传入指针,但2. 可以向函数的void类型的形参传入指针,但 不可向非void类型的形参传入void指针;不可向非void类型的形参传入void指针; 3. 函数可以返回void类型的指针; 4. 可以向另一个该类型的指针赋值;4. 可以向另一个该类型的指针赋值; 5. 可以个到任何类型的指针的值;5. 可以个到任何类型的指针的值; 6. 不得对其所指向的对象进行任何操作。 114 指针使用过程中需要注意的问题 未分配内存就使用它; 内存分配未成功,却使用了它; 内存分配未成功,却使用了它; 内存分配虽然成功,但是尚未初始化就使用它; 内存分配成功并且已经初始化,但操作越过了边界; 内存分配成功并且已经初始化,但操作越过了边界; 忘记了释放内存或只释放一部分,造成内存泄露; 释放了内存却继续使用它; 程序中的对象调用关系过于复杂,实在难以搞清楚某个程序中的对象调用关系过于复杂,实在难以搞清楚某个 对象究竟是否已经释放; 函数的return语句写错了,返回了栈内存的地址; 函数的return语句写错了,返回了栈内存的地址; 使用free或delete释放了内存后,没有将指针设置为 NULL; 115 NULL; 释放的顺序不对。 常见的错误举例 没有申请空间就使用 f()() { int* p; *p = 0; // 1。压根没申请空间就使用p 0; // 1。压根没申请空间就使用 } 数组或指针越界 这是典型的新手错误。数组或指针越界 void main() { 他没意识到内存分配会 不成功。 { char *a, *b; a = new char[10]; //2。申请了但不见得成功a = new char[10]; //2。申请了但不见得成功 b = a + 20; *b = 0; // 3。指针超出了申请空间的范围 116 *b = 0; // 3。指针超出了申请空间的范围 } 函数结束时忘记释放,导致内存泄漏 void gimme() g() { char *p; p; p = (char * )malloc(10); return; // 没有释放p的空间就返回了return; // 没有释放p的空间就返回了 } void main() {{ gimme(); return; 117 return; } 释放内存顺序不对,导致内存释放了还在使用它释放内存顺序不对,导致内存释放了还在使用它 typedef struct ptrblock { char *ptr; } PB; void main() {{ PB *p; p = (PB )malloc(sizeof(PB)); // 申请了PB变量p( ) ( ( )); 申请了 变量 p->ptr =(char*) malloc(10); ...... free(p); free(p->ptr ); // p的空间已经释放了,还使用它 118 free(p >ptr ); // p的空间已经释放了,还使用它 } 内存已释放,指向它的指针还在使用 char foo(char *p)char foo(char p) { return *(p+1);return (p+1); } main()main() { char *a;char a; a = malloc(10); free(a);free(a); foo(a); t(0) 119 return (0); } 常见的错误举例—思考下面程序的运行结果 void main() { h*it1char *input1; char *input2; input1 = (char*)malloc(20);input1 = (char )malloc(20); strcpy (input1, "this is string1"); printf("%s\n", input1);p( p) free(input1); input2 = (char*)malloc(20); t(it2"thiiti2")strcpy (input2, "this is string2"); printf("%s\n", input2); if( input1 != NULL )if( input1 != NULL ) strcpy (input1, "hello world"); printf("%s\n", input1); 120 printf("%s\n", input2); } 问题表(1) Q1:下面这段程序的执行结果是什么? char *GetString(void) { char p[] = “Hello World”; return p; } void Test(void) { char *str = NULL; str = GetString(); printf(“%s\n”, str); } Q2:char a[] = “Hello World”; 与char *p = “Hello World”; 有区别吗? 能成功吗? 121 a[0] = ‘Y’; 能成功吗? p[0] = ‘Y’ 能成功吗? 问题表(2) Q3:这段代码有问题吗? char *p=(char*)malloc(100);char p = (char )malloc(100); strcpy( p, “hello” ); free(p); … if (p != NULL)if (p != NULL) { strcpy(p, “world”); 122 } 问题表(3) Q4:这段代码有问题吗? #include #include #include #i l d < ll h>#include #define SIZE 4 void main()() { int i,j; char str[100];char str[100]; char *tstr[SIZE]; //创建指针数组 h *t NULL 123 char *tp=NULL; printf("请输入字符串,以回车间隔!\n"); for(i=0;i0)( p( [ ], [j]) ) { tp=tstr[i]; tstr[i]=tstr[j]; tt[j]ttstr[j]=tp; } for(i=0;i
还剩123页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 6 金币 [ 分享pdf获得金币 ] 0 人已下载

下载pdf

pdf贡献者

arkchina

贡献于2014-01-09

下载需要 6 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf