一、指针是什么 指针是C语言的重要概念,也是C语言及其扩展语言(C++等)的重要特色。指针可以使程序变得简洁、紧凑、高效。每一个学习C语言的人都应当深入学习和掌握指针。可以说,没有掌握指针,就是没有学会C语言。 指针的概念复杂也比较灵活,因此初学者经常会犯错。请同学们务必多思考、多练习、谨慎使用。 /********地址与指针的关系与区别*********/ 在计算机内存中,每个存储单元都有一个特定的唯一的编号,称为地址。 在32位计算机中,地址是由32位(bit)构成的2进制数,占4字节(byte),通常由一个8位的16进制数来表示。 示例:地址0x00110023(16进制)-------->0000 0000 0001 0001 0000 0000 0010 0011(2进制) 在C语言中,专门用来保存地址的变量,称为“指针变量”,简称“指针”。在不影响理解的前提下,“指针”、“指针变量”和“地址”不做区分,统称“指针”。 /********地址与指针的关系与区别end******/ 二、指针变量 从上文我们知道:存放地址的变量是指针变量,它可以指向另一个对象(如变量、数组、函数等)。 1、定义一个指针变量 用法: <数据类型名> *<指针变量名> 说明: 1.<数据类型名>:指针指向的数据类型,是指针的目标的数据类型,而非指针本身的数据类型 2.*<指针变量名>:指针变量的变量名。前面加*代表这是一个指针变量 注意:定义指针时必须指定指针目标的数据类型名,这样才能够正确地按照数据类型取出数据。 /*******指针的数据类型*******/ 在C语言的四类数据类型中,指针是单独的一类数据类型。指针数据类型无法独立存在,必须配合其他数据类型(基本数据类型,构造数据类型,空类型)一起使用,表示指向该类型的指针。例如: int a;表示int类型变量a int *p;表示该指针是指向int类型的指针 /*******指针的数据类型end****/ 如: char *s;//定义一个指向char类型数据的指针变量s int *a;//定义一个指向int类型数据的指针变量a float *f;//定义一个指向float类型数据的指针变量f 注意:在C语言的官方文档内,推荐定义指针时指针运算符*要紧贴指针变量名,而不要挨着数据类型名。 2、引用一个指针变量 在这里需要熟练掌握两个与指针有关的运算符:&和* &:取地址运算符 *:指针运算符(或称“间接访问运算符”) 1)给指针变量赋值 如果我们已经定义了一个int型变量a,和一个int型指针p int a; int *p; 则我们可以使用&给指针变量赋值,即让指针变量p指向变量a p = &a;//将a的地址取出赋值给指针变量p 2)引用指针变量所指向的内容 如果已执行p = &a,表示指针p已指向变量a,则我们可以使用指针p来引用变量a。如: printf("%d\n",*p);//*p表示将指针变量p的内容取出 或者可以通过指针对指向的变量进行操作。如: *p = 1;//相当于a = 1 3)打印指针变量 指针变量也是可以输出的,用格式控制%p printf("%p",p);//打印指针p所存放的地址 4)指针变量的初始化 我们可以在定义指针变量的时候对指针变量进行初始化。如: int *p = &a;//初始化指针变量p指向变量a 示例:输入a和b两个整数,按先大后小的顺序输出这两个整数。要求使用指针。 #include int main() { int *p1,*p2,*tmp; int a,b; printf("请输入两个整数:\n"); scanf("%d%d",&a,&b); p1 = &a;//让p1指向a p2 = &b;//让p2指向b if(*p1 < *p2)//相当于比较a int main() { int a,b,c,tmp; int *p1,*p2,*p3; printf("请输入3个整数:\n"); scanf("%d%d%d",&a,&b,&c); p1 = &a; p2 = &b; p3 = &c; if(*p1 > *p2 && *p1>*p3) { tmp = *p1; *p1 = *p3; *p3 = tmp; } if(*p2 > *p1 && *p2>*p3) { tmp = *p2; *p2 = *p3; *p3 = tmp; } //经过这两个判断语句后*p3(即变量c)中存的是3个数的最大值。再比较*p1(即变量a)和*p2(即变量b)的大小即可 if(*p1>*p2) { tmp = *p1; *p1 = *p2; *p2 = tmp; } printf("%d %d %d\n",*p1,*p2,*p3); return 0; } 答案2: //答案2是比较指针中的引用内容并交换指针的指向对象而未改变变量中的数值 //即程序执行后变量a、b、c的值未发生改变而分别指向它们的指针可能发生了改变 #include int main() { int a,b,c; int *p1,*p2,*p3,*tmp; printf("请输入3个整数:\n"); scanf("%d%d%d",&a,&b,&c); p1 = &a; p2 = &b; p3 = &c; if(*p1 > *p2 && *p1>*p3) { tmp = p1; p1 = p3; p3 = tmp; } if(*p2 > *p1 && *p2>*p3) { tmp = p2; p2 = p3; p3 = tmp; } //经过这两个判断语句后*p3中存的是3个数的最大值。再比较*p1和*p2的大小即可。注意此时指针p1、p2、p3的指向对象可能改变 if(*p1>*p2) { tmp = p1; p1 = p2; p2 = tmp; } printf("%d %d %d\n",*p1,*p2,*p3); return 0; } 5)指针变量占内存空间大小 指针变量存储的是一个32位2进制数(8位16进制数),因此占内存4字节。 注意指针变量存储的是一个地址值,而地址是4字节,因此指针大小就是4字节。指针占内存空间大小与指针类型无关。 示例: #include int main() { int a,*p; char c,*q; p = &a; q = &c; printf("sizeof(p) is %d\n",sizeof(p));//指针大小 printf("sizeof(*p) is %d\n",sizeof(*p));//指针指向的内容(int类型)大小 printf("sizeof(q) is %d\n",sizeof(q));//指针大小 printf("sizeof(*q) is %d\n",sizeof(*q));//指针指向的内容(char类型)大小 return 0; } 输出结果:4、4、4、1 三、使用指针变量 1、使用指针变量引用一般变量 上文已讲过,不再赘述 /***************大端序和小端序*******************/ 思考:若有int类型变量0x12345678,能否使用char*类型指针指向该变量并使用指针取出该内存内存储的值?若可以,读取的值是什么? int a; char *p; p = (char*)&a; 大端序:高地址存储数据低位,低地址存储数据高位 小端序:高地址存储数据高位,低地址存储数据低位 大端序常用于网络通信领域,小端序常用于操作系统领域 /***************大端序和小端序end****************/ 2、使用指针变量引用数组 1)数组元素的指针 数组存放在内存中,一个数组包含若干元素,每个数组元素都在内存中占据存储单元,都有相应的地址。我们可以使用指针来访问或操作一个数组。例如: int a[10]={1,2,3,4,5,6,7,8,9,10}; int *p; 则让指针p指向数组首地址(即数组下标为0的元素的地址) p = &a[0]; 或可以直接写成: p = a;//注意不要写成p = &a; 因为数组名即为数组的首地址(即数组下标为0的元素的地址),因此我们可以直接把这个地址赋值给指针。 同样对于指向数组的指针的初始化可以写成: int *p = &a[0]; 或: int *p = a; 2)对指向数组元素的指针进行运算 如果指针已经指向了数组元素,则该指针可以进行如下运算: 1.加一个整数(用+或+=)。如p += 1; 2.减一个整数(用-或-=)。如p -= 1; 3.自增。如p++;和++p; 4.自减。如p--;和--p; 5.两个指针相减。如p1-p2;(只有p1和p2都指向同一数组元素才有意义) 说明: 1.如果指针变量p已经指向数组中的一个元素,则p+1指向这个元素的下一个元素,p-1指向这个元素的上一个元素。p++与p--同理 注意:C语言不检查数组越界的问题,因此操作指针时要小心数组的边界,不要越界 2.执行p+1时不是简单地将p的值(所存储的地址)加1,而是加上一个数组元素所占的字节数。例如,如果数组是int型或float型,则p+1相当于地址+4(因为int型和float型占4字节);如果数组是char型,则p+1相当于地址+1(因为char型占1字节) 示例: #include int main() { int a[5]={1,2,3,4,5}; char c[]="Hello World"; int *p = a; char *q = c; printf("p的初始地址是%p\nq的初始地址是%p\n",p,q); p++; q++; printf("p++后的地址是%p\nq++后的地址是%p\n",p,q); return 0; } 3.*p表示当前指针指向的元素的值,*(p+i)代表指针在当前位置的第i个之后的元素的值。例如,如果p现在指向数组首地址(即a[0]),则*(p+5)就是a[5]的值。 思考:*(p+i)和*p+i的意思一样吗? 4.如果指针p1和p2都指向同一数组,如果执行p1-p2,则表示两个指针的元素个数之差。这样的运算是有意义的,可以得到这两个指针所指的元素间的相对位置。如: p1指向a[1],p2指向a[4],则 p2-p1=3,即a[1]偏移3个地址可以找到a[4] 注意:p1和p2不能相加,两个指针相加是无意义运算。 3)通过指针引用数组元素 如果一个指针已经指向了数组,那么引用数组元素可以有2种方法: 下标法:使用数组下标的形式。如a[i]的形式 指针法:先让指针偏移到适当位置,然后使用指针运算符。如*(p+i)的形式 练习:自定义一个整型数组有10个元素,使用下标法和指针法分别输出这10个元素 答案: //下标法 #include int main() { int a[10]={5,4,2,3,1,8,7,9,10,6}; int i; for(i=0;i<10;i++) { printf("%d ",a[i]); } printf("\n"); return 0; } //指针法1 #include int main() { int a[10]={5,4,2,3,1,8,7,9,10,6}; int *p = a; int i; for(i=0;i<10;i++) { printf("%d ",*(p+i)); } printf("\n"); return 0; } //指针法2 #include int main() { int a[10]={5,4,2,3,1,8,7,9,10,6}; int *p; int i; for(p=a;p<(a+10);p++) { printf("%d ",*p); } printf("\n"); return 0; } 思考:指针法1和指针法2的指针移动是否相同? /*************** *p++的用法****************/ 有指针p是指向了数组a[]的指针。我们考虑这种用法:*p++ 示例: #include int main() { int a[10]={5,4,2,3,1,8,7,9,10,6}; int i = 1; int *p = a; while(i<=10) { printf("%d ",*p++);//在这里使用*p++ i++; } printf("\n"); return 0; 该程序的执行结果等同于上面的练习题答案。 对于*p++运算,因为*与++的运算优先级相同,则根据结合性,相当于*(p++) 对于*p++的运算,C语言编译器做了4步操作 1.复制一个p的副本 2.将原始的p进行++运算 3.输出p的副本指向的内容值,即计算*p(副本) 4.删除p的副本 实际上第3步与第4步是一起完成的。 初学者在使用*p++时一定要理解它的计算流程,谨慎使用 思考: *p++ 和 (*p)++两种用法结果相同吗? /*************** *p++的用法end***************/ 3、用指针代替数组名做函数参数 在“函数”部分的学习中我们已经接触过,将数组作为函数的形参和实参的写法: void fun(int arr[],int n)//形参 { …… } int main() { int a[10]; fun(a,10);//实参 } 针对函数fun()的第一个形参,我们还可以使用指针的形式。即写成: void fun(int *arr,int n) 这样在fun()函数中引用arr数组的第i个元素可以有2种形式: arr[i] 或 *(arr+i) 这样做的原因是因为,无论形参是int arr[]还是int *arr,编译器都把它当做指针处理(即都视为int *arr),这样来说int arr[]和int *arr在形参列表中二者是等价的。 练习1:自定义一个int型数组,将数组中所有元素反序存放。要求使用指针传参。 答案: #include #define MAX 10 void reserve(int *a,int n) { int *p,*q; int tmp; p = a;//p指向数组第一个元素 q = a+(n-1);//q指向数组最后一个元素 while(p!=q && (p-1)!=q)//需考虑奇数个元素个数和偶数个元素个数不同的情况 { tmp = *p; *p = *q; *q = tmp; p++; q--; } } int main() { int a[MAX]={55,11,99,77,33,22,88,44,66,110}; int i; reserve(a,MAX); for(i=0;i #define MAX 10 void Bubble(int *a,int n) { int i,j,tmp; for(i=0;ia[j+1])//如果前一个元素比后一个元素大,则向后移动 { tmp = a[j]; a[j] = a[j+1]; a[j+1] = tmp; } } } } int main() { int a[MAX]={55,11,99,77,33,22,88,44,66,110}; int i; Bubble(a,MAX); for(i=0;i #define ROW 3 #define COL 4 void average_total(int (*p)[COL],int n)//注意第二个下标不可省略 { int i,j,sum = 0; float avg = 0; for(i=0;i int main() { char *p = "I love China!"; printf("字符串是%s\n",p); printf("字符是%c\n",*(p+7));//注意第8个字符是向后移动7位 return 0; } 不过要注意,使用这种方法来引用字符串,字符串是只读的,不能修改。例如: #include int main() { char *s1 = "Hello World!"; char *s2; s2 = s1; s2 += 2; *s2 = 'L';//试图修改Hello World的第三个字符为'L',语法报错 printf("%s\n",s1); return 0; } 编译发现语法报错。 错误的原因:使用字符数组存放字符串,字符串(或者说字符数组)是存放在堆栈区内的,存放在堆栈区的数据是可读可写的(可修改)。而直接让指针指向一个字符串常量,字符串是存放在常量区内的。常量区所存放的数据是只读的(不可修改),因此试图修改只读数据,语法报错。 虽然让字符串存放在常量区可以节省部分堆栈区空间,但是这样做数据就不可修改。因此初学者一定要谨慎使用,选择合适的存放字符串的方式。 练习1:自定义一个字符串,不使用C库函数中的strcpy()函数,复制这个字符串。要求使用指针。 答案: #include char *my_strcpy(char *dst,const char *src) { if((NULL==dst)||(NULL==src)) { printf("数据无效!\n"); return NULL; } char *dstCopy = dst; //第一种写法 /*int i=0; while(src[i]!='\0') { dst[i] = src[i]; i++; } dst[i]='\0';*/ //第二种写法 /*while(*src!='\0') { *dst = *src; dst++; src++; } *dst='\0';*/ //第三种写法 /*while(*dst++=*src++);*/ return dstCopy; } int main() { char str1[]="Hello World!"; char str2[32];//足够大的空间 my_strcpy(str2,str1); printf("str2 is %s\n",str2); return 0; } 练习2:加密一个字符串。加密规则是:字符串中所有字母都循环向后4个字母。如a--->e、b--->f……x--->b、y--->c、z--->d。A--->E、B--->F……X--->B、Y--->C、Z--->D。例如: "Hello World!"--->"Lipps Asvph!",注意非字母字符不转换。 其中函数的数组形参使用指针。 答案: #include void change_word(char *p, int n) { int i; for (i = 0; i < n; i++) { if ((p[i] >= 'a' && p[i] <= 'v') || (p[i] >= 'A' && p[i] <= 'V')) { p[i] += 4; } else if ((p[i] >= 'w' && p[i] <= 'z') || (p[i] >= 'W' && p[i] <= 'Z')) { *(p + i) = *(p + i) - 26 + 4; } } } int main() { char a[] = "Hello World!"; change_word(a, sizeof(a) / sizeof(a[0])); printf("字符串a变成 %s\n", a); return 0; }