------指针
目录
一.初识指针:
1.基础指针知识归纳:
-
指针就是地址,地址就是指针。(举例:一把钥匙对应一个房间,一个房间对应一把钥匙)
-
指针变量就是存放地址的变量。
-
如果一个指针变量指向一个普通变量,则 *指针变量 就完全等普通变量。
-
地址就是内存单元的编号。
-
指针是一个操作受限的非负整数(只能减,不能加、乘除)
所以指针的值就是某一个变量的内存地址,指针变量就是用来存放某个变量的内存地址的变量,和广义的变量没有什么区别。
2.如何定义和使用指针:
2.1指针变量的定义和使用:
定义格式:
类型 *指针变量名 指针的格式字符串%p:表示输出一个变量对应得内存地址编号,通常是十六进制的形式
例:
#include <stdio.h> int main() { int a = 2, b = 4, c; int *p1, *p2, *p3; p1 = &a;//看总结第1点,地址就是指针,指针就是地址,所以a的地址&a与指针p1等价,所以我们让指针p1指向&a p2 = &b;//同理 p3 = &c; *p3 = 6;//看总结第3点,如果一个指针变量指向一个普通变量,则 *指针变量 就完全等普通变量 printf("a = %d\tb = %d\n", a, b);//正常输出 printf("*p1 = %d\t*p2 = %d\n", *p1, *p2);//看总结第3点,如果一个指针变量指向一个普通变量,则 *指针变量 就完全等普通变量 //在输出时我们也可以认为*p1就是取值,将指针p1指向的变量的值取出来 printf("*p3 = %d\n", *p3); return 0; }
总结:如何使用指针变量呢?第一,定义;第二,指向;第三,使用。
2.2实例练习:
输入a,b的值,从小到大输出。
#include <stdio.h> int main() { int a, b, temp; int *p1, *p2; //定义 scanf("%d %d", &a, &b); p1 = &a; //引用 p2 = &b; //引用 if(a > b) //等价于if(*p1 > *p2) { //把指针当作普通变量使用 temp = *p1; //使用 *p1 = *p2; *p2 = temp; } printf("%d %d\n", *p1, *p2); return 0; }
#include <stdio.h> int main() { int a, b, temp; int *p1, *p2, *p3; //定义 scanf("%d %d", &a, &b); p1 = &a; //引用 p2 = &b; //引用 if(*p1 > *p2) { //正常使用指针 p3 = p1;//使用 p1 = p2; p2 = p3; } printf("%d %d\n", *p1, *p2); return 0; }
3.通过指针引用数组:
3.1讲解:
在第五次培训中我们学习了数组,现在我们需要对数组有进一步的了解;
在初学C语言时,我们可以认为数组就是指针,指针就是数组,数组是一段连续的空间的地址,而指针就是地址。所以两者可以等价。
首先我们需要了解一个概念:
#include <stdio.h> int main() { int *p1, *p2; int a[3] = {1, 2, 3}; p1 = &a[0]; p2 = a; printf("%d %p\n", *p1, p1); printf("%d %p\n", *p2, p2); return 0; }
数组a[0]就为数组的首地址,也就是我们的数组名
然后我们便可以进一步了解数组就是指针,指针就是数组:
#include <stdio.h> int main() { int a[5] = {168, -2, 0, 4, -500}; for(int i = 0; i < 5; i++) { printf("%-5d", *(a+i)/*a[i]*/);//我们认为数组就是指针,即a[0]就为数组的首地址,也就是我们的数组名 } putchar('\n'); for(int i = 0; i < 5; i++) { printf("%-5d", a[i]/*(*(a+i))*/); } putchar('\n'); //编译器为数组a分配了一段连续的5个空间,每个空间对应一个地址编号 printf("%p\n", a[0]); printf("%p\n", a[1]); printf("%p\n", a[2]); printf("%p\n", a[3]); printf("%p\n", a[4]); return 0; } /* 代码输出: 168 -2 0 4 -500 168 -2 0 4 -500 00000000000000A8 00000000FFFFFFFE 0000000000000000 0000000000000004 00000000FFFFFE0C */
代码中我们可以看到,我们认为*(a+i)就是指针,并且随着变量i的增加,数组的地址也在增加,也就是说a是一个地址,+i是一个地址偏移量, a+i也是一个地址。所以*(a+i)就是取a+i这个地址里面的值。同时我们可以可以看到,在两个for循环中*(a+i)与a[i]等价,所以这里引出一个概念:
我们已经强调过,初学C语言时,数组就是指针,指针就是数组,所以我们可以这样理解:
因为*(a+i)与a[i]等价,所以我们通过简单的加法结合律,就可以变形得到:
*(a + i) = a[i]; *(i + a) = a[i]; *(a + i) = i[a]; *(i + a) = i[a];
大家可以自己试一试,敲一敲代码,看看结果是否全部一样。
3.2实例练习:
1.通过引用指针修改数组a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}里的奇数项为n的平方。
#include <stdio.h> #include <string.h> int main() { int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int *p; p = a;//等价于p = &a[0] for(int i = 0; i < 10; i++) { if((i+1) % 2 != 0) { /*(p[i])*/*(p+i) = (i+1)*(i+1);//其实等价于*(a+i) = (i+1)*(i+1); } } for(int i = 0; i < 10; i++) { printf("%-3d", *(a+i));//a[i] } return 0; } /*效率更高 int main() { int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int *p; for(p = a; p < a+10; p++) { if(*p % 2 != 0) { *p = (*p)*(*p);//其实等价于*(a+i) = (i+1)*(i+1); } } for(p = a; p < a+10; p++) { printf("%-3d", *p); } return 0; } */ /*代码输出: 1 2 9 4 25 6 49 8 81 10 */
2.写一个函数,将上一题的数组作为参数,将每项变成它的阶乘。
#include <stdio.h> #include <string.h> int factorial(int n) { int result; if(n < 0) { result = -1; } if(n == 1 || n==0) result = 1; else { result = n*factorial(n-1); } return result; } void fac(int a[], int n) { int *p; p = a; for(int i = 0; i < n; i++) { /*(p[i])*/*(p+i) = factorial(*(p+i));//其实等价于*(a+i) = factorial(*(a+i)); } for(int i = 0; i < n; i++) { printf("%d\t", *(p+i)); } } /*效率更高 void fac(int a[], int n) { int *p; for(p = a; p < a + n; p++) { *p = factorial(*p);//其实等价于*(a+i) = factorial(*(a+i)); } for(p = a; p < a + n; p++) { printf("%d\t", *p); } } */ int main() { int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; fac(a, 10); return 0; } /*代码输出: 1 2 6 24 120 720 5040 40320 362880 3628800 */
4.通过指针引用字符串
4.1回顾:
在 C 程序中,字符串是存放在字符数组中的。想引用一个字符串,可以用以下两种方法。
(1)用字符数组存放一个字符串,可以通过数组名和下标引用字符串中一个字符,也可以通过数组名和格式声明 “ %s ” 输出该字符串。
举例:定义一个字符数组,在其中存放字符串 " I love China! " ,输出该字符串和第8个字符。
#include <stdio.h> int main() { char string[] = "1 love China!"; //定义字符数组string printf("%s\n", string); //用%s格式声明输出string,可以输出整个字符串 printf("%c\n", string[7]); //用%c格式输出一个字符数组元素 return 0; }
在定义字符数组 string 时未指定长度,由于对它初始化,因此它的长度是确定的,长度应为14,其中 13 个字节存放 " l love China! " 13个字符,最后一个字节存放字符串结束符 '\0'。数组名 string 代表字符数组首元素的地址。题目要求输出该字符串第 8 个字符,由于数组元素的序号从 0 起算,所以应当输出 string[7],它代表数组中序号 7 的元素的值(它的值是字母 C )。实际上 string[7] 就是 *(string+7),string+7 是一个地址,它指向字符 “ C ”。
(2)用字符指针变量指向一个字符串常量,通过字符指针变量引用字符串常量。<已弃用>
[Warning] deprecated conversion from string constant to 'char*' [-Wwrite-strings]
举例:通过字符指针变量输出一个字符串。
#include <stdio.h> int main() { char *string = "1 love China!"; //定义指针变量string printf("%s\n", string); //用%s格式输出整个字符串 printf("%c\n", *(string + 7)); //用%c格式输出一个元素 return 0; }
在程序中没有定义字符数组,只定义了一个 char* 型的指针变量(字符指针变量) string,用字符串常量 " I love China! " 对它初始化。C 语言对字符串常量是按字符数组处理的,在内存中开辟了一个字符数组用来存放该字符串常量,但是这个字符数组是没有名字的,因此不能通过数组名来引用,只能通过指针变量来引用。
对字符指针变量 string 初始化,实际上是把字符串第 1 个元素的地址(即存放字符串的字符数组的首元素地址)赋给指针变量 string,使 string 指向字符串的第 1 个字符,由于字符串常量 " I love China! " 已由系统分配在内存中连续的 14 个字节中,因此,string 就指向了该字符串的第一个字符。在不致引起误解的情况下,为了简便,有时也可说 string 指向字符串 " I love China! " ,但应当理解为 “ 指向字符串的第 1 个字符 ”。
注意:
string 被定义为一个指针变量,基类型为字符型。请注意它只能指向一个字符类型数据,而不能同时指向多个字符数据,更不是把 " I love China! " 这些字符存放到 string 中(指针变量只能存放地址),也不是把字符串赋给 *string。只是把 " I love China! " 的第 1 个字符的地址赋给指针变量。
%s 是输出字符串时所用的格式符,在输出项中给出字符指针变量名 string,则系统会输出 string 所指向的字符串第 1 个字符,然后自动使 string 加 1,使之指向下一个字符,再输出该字符……如此直到遇到字符串结束标志 '\0' 为止。在内存中,字符串的最后被自动加了一个 '\0' ,因此在输出时能确定输出的字符到何时结束。
4.2指针引用字符串数组:
字符串数组的引用同指针引用数组差不多,需要特别注意的是结束条件。
举例:将字符串 a 复制为字符串 b,然后输出字符串b。
#include <stdio.h> int main() { char a[] = "I am a student.", b[20];//定义字符数组 int i; for (i = 0; *(a + i) != '\0'; i++) *(b + i) = *(a + i); //将a[i]的值赋给b[i] *(b + i) = '\0'; //在b数组的有效字符之后加'\0' printf("string a is:%s\n", a); //输出a数组中全部有效字符 printf("string b is:"); for (i = 0; b[i] != '\0'; i++) printf("%c", b[i]); //逐个输出b数组中全部有效字符 //printf("string b is:%s\n", b); //输出b数组中全部有效字符 printf("\n"); return 0; }
注意:字符数组与数字型数组的区别,字符数组可以用printf输出整个数组,而数字型数组则不行。在前面的培训中我们讲过。
4.3实例练习:
1.输入一串字符串,统计数字个数
复习fgets
#include <stdio.h> #include <string.h> #define MAXSIZE 100 int main() { char str[MAXSIZE]; int n = 0; fgets(str, MAXSIZE, stdin); //int x = strlen(str); //printf("%s\n%d\n", str, x); for(int i=0;*(str+i) != '\n'; i++) if(*(str+i)>='0' && *(str+i)<='9') n++; printf("%d\n", n); return 0; }
复习getchar
#include <stdio.h> #define MAXSIZE 100 int main() { char str[MAXSIZE], s; int n = 0, i=0; char *p;//定义字符指针 while((s = getchar()) != '\n')//输入字符串 { *(str+i) = s; i++; } str[i] = '\0';//添加末尾结束标志 for(p = str;*p!='\0';p++)//判断是否为数字 if(*p>='0' && *p<='9') n++; printf("%d\n", n); return 0; }
5.自增,自减与指针:
口诀:自增,自减与指针的优先级相同,所以当两个放一起时自右向左运行。
*p++ = *(p++) = p[i++] *++p = *(++p) = p[++i] *p-- = *(p--) = p[i--] *--p = *(--p) = p[--i]
#include <stdio.h> #include <string.h> int main() { int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int *p, i; for(p = a; p < a+10;) { printf("%-3d", *p++);//*p++ = *(p++) = p[i++] } putchar('\n'); for(p = a, i = 0; i<10;) { printf("%-3d", p[i++]);//*p++ = *(p++) = p[i++] } return 0; }
#include <stdio.h> #include <string.h> int main() { int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int *p, i; for(p = a; p < a+9;)//因为开始已经跳过了一个元素,所以p<a+9而不是p<a+10 { printf("%-3d", *++p);//*++p = *(++p) = p[++i] } putchar('\n'); for(p=a, i=0; i<9;) { printf("%-3d", p[++i]);//*++p = *(++p) = p[++i] } return 0; }
自减同理;
#include <stdio.h> #include <string.h> int main() { int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int *p, i; for(p = a+9; p >= a;) { printf("%-3d", *p--);//*p-- = *(p--) = p[i--] } putchar('\n'); for(p=a, i = 9; i >= 0;) { printf("%-3d", p[i--]);//*p-- = *(p--) = p[i--] } return 0; }
#include <stdio.h> #include <string.h> int main() { int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int *p, i; for(p = a+9; p >= a+1;)//因为开始已经跳过了一个元素,所以p>=a+1而不是p>=a { printf("%-3d", *--p);//*--p = *(--p) = p[--i] } putchar('\n'); for(p = a, i=9; i >= 1;) { printf("%-3d", p[--i]);//*--p = *(--p) = p[--i] } return 0; }
二.指针的大小:(自行深入了解)
在同一CPU构架下,不同类型的指针变量所占用的存储单元长度是相同的。这是因为操作系统的位数与其所能支持的最大内存有直接的关系。由于计算机是按照字节寻址的,如在 32 位操作系统下,32位比特位一共能描述 232 个状态,一个状态标记大小为 1B(一般定义8位(bit,比特)为一字节),所以一共有 2321B = 4GB。因此 32 位系统所能支持的最大内存为 4GB。而对于 64 位操作系统(目前主流操作系统),所能支持的最大内存为 2641B = 17179869184GB,这是一个很大的数,基本上可以支持任何现实中任意存在的内存。
而指针最小就是以字节为单位进行指引,因此对于 32 位操作系统,它也要是 32 位的,即 4 字节大小;而对于 64 位操作系统,它就得是 64 位 即 8 字节大小。
例:
#include <stdio.h> int main() { int a = 4; int *p = &a; printf("%zu\n", sizeof(p)); // 指针变量占据 8 个字节 printf("%p\n", p); // 变量 a 存放在以内存地址为000000000062FE14开头的内存单元中 } /* 代码输出: 8 000000000062FE14 */
注意:在 C/C++语言中,指针一般被认为是指针变量,指针变量的内容存储的是其指向的对象的首地址,指向的对象可以是变量(指针变量也是变量),数组,函数等占据存储空间的实体。
#include <stdio.h> int main() { int a = 0, b = 1; int *p1 = &a, *p2 = &b; printf("%p\n%p\n", p1, p2); printf("%zu\n%zu\n", sizeof(p1), sizeof(p2)); printf("%zu\n%zu\n", sizeof(a), sizeof(b)); return 0; } /* 代码输出: 000000000062FE0C 000000000062FE08 8 8 4 4 */
这是一个很简单的例子,程序中创建了两个局部变量,然后利用指针输出它们的地址。其中&是取地址符,取出来以后赋给两个指针变量p1、p2,并将其打印出来。 细心的话可以发现这两个地址由大到小,相差为 4。(16进制,C为12,比8大4)这说明栈中的这两个变量恰好相邻,且一个int类型的变量占用 4B大小空间,同理char类型的变量占用1B大小空间,float类型的变量占用8B大小空间。而且还说明栈空间的增长方向是由大到小的,当然我们的主题是指针,这里不再赘述。
#include <stdio.h> #include <string.h> int main() { int a[] = {1, 2, 3, 4}; printf("%d\n", sizeof(a+0)); //数组名默认首元素地址:加0还是首元素地址,地址为一个指针,因此结果为8字节(64位) printf("%d\n", sizeof(*a));//数组名默认首元素地址,解引用也就是数组中的1,其为整型数据,所以为4字节 printf("%d\n", sizeof(a+1));//数组名默认首元素地址:加1是数组中第二个元素2的地址,地址8字节 (64位) printf("%d\n", sizeof(a[1]));//数组中第一个元素1的大小,其为整型所以为4字节 printf("%d\n", sizeof(&a));//&a:为整个数组的地址,既然为地址,就占8个字节(64位) printf("%d\n", sizeof(*&a));//对整个数组进行解引用,因此大小为4*4=16字节 printf("%d\n", sizeof(&a+1));//整个数组的下一位元素的大小:8字节 (64位) printf("%d\n", sizeof(*&a[0]));//取出第一个元素的地址并解引用,整型数据1,4字节 printf("%d\n", sizeof(*&a[0]+1));//取出第一个元素的地址并解引用,整型数据1,再加1为2,整型2的大小为4字节 return 0; } //*的作用是引用指针指向的变量值,引用其实就是引用该变量的地址,“解”就是把该地址对应的东西解开,解出来,就像打开一个包裹一样,那就是该变量的值了,所以称为“解引用”。也就是说,解引用是返回内存地址中对应的对象。
%d输出int型。 %zu输出size_t型。size_t在库中定义为unsigned int。//计算时间更长 一个是整型,一个是无符号整型。 补充:如果%zu不能使用,可以用%u取代。%zu不能输出负数。
sieof是求一个数据的所占用的内存空间的函数
,其单位为字节
。
#include<stdio.h> int main() { char a[] = {'a', 'b', 'v', 'c', 't'}; printf("%d\n", sizeof(a)); //数组名放入sizeof内,其代表整个数组,每个字符占一个字节,因此,一共5个字节 printf("%d\n", sizeof(a+0));//此时a不再代表整个数组,而是数组首元素的地址,加0 还是首元素地址,地址即为8字节(64位) printf("%d\n", sizeof(*a));//数组名代表首元素地址,也引用即为字符a,其大小为1字节 printf("%d\n", sizeof(a+1));//数组名代表首元素地址,加1代表字符b的地址,地址即为8字节(64位) printf("%d\n", sizeof(a[1]));//此时表示数组中字符b的大小,1字节 printf("%d\n", sizeof(&a));//&a代表整个数组的地址,地址即为8字节(64位) printf("%d\n", sizeof(*&a));//&a代表整个数组的地址,整个数组解引用即5个字节 printf("%d\n", sizeof(&a+1));//&a代表整个数组的地址,加1代表此数组后的下一个元素的地址,地址即为8字节(64位) printf("%d\n", sizeof(*&a[0]));//取出数组中字符a的地址并解引用,也就是字符a,1字节 printf("%d\n", sizeof(&a[0]+1));//取出数组中字符a的地址,加1表示字符b的地址,地址即为8字节(64位) return 0; }
strlen
所作的仅仅是计数器一个的工作,它从内存的某个位置,可以是字符串开头,中间某个位置,甚至是某个不确定的内存区域开始扫描,直到碰到第一个字符串结束'\0为止,然后返回计数器值,长度不包含\0。
#include<stdio.h> #include<string.h> int main() { char a[] = {'a','b','v','c','t','r'}; //在内存中实际存储: 'a','b','v','c','t','r','\0' printf("%d\n",strlen(a)); //由于上述这种字符串定义方式系统会默认补充字符串结束标志'\0' //因此,其将会直至找到\0位置 printf("%d\n",strlen(a+0));//数组名默认为书元素地址,加0仍然是首元素地址,然后去找'\0' printf("%d\n",strlen(a+1));//首元素地址加1,则变为了字符数组中字符b的地址,因此将从b开始寻找结束标志'\0' 因此,长度减一 //printf("%d\n",strlen(a[1]));//访问数组中的第二个元素,则是字符b,因此将会其对应的ASCII码值的地址开始寻找\0,会产生错误 printf("%d\n",strlen(*&a));//&a代表整个数组的地址,从此地址开始找\0,整个数组解引用即长度为6 printf("%d\n",strlen(*&a+1));//从整个数组后的下一个字节开始寻找\0, 数组解引用即长度为5 printf("%d\n",strlen(&a[0]+1));//&a[0]取出第一个元素的地址,再加1为第二个元素的地址,从此开始寻找\0,数组解引用即长度为5 return 0; }
三.深入指针:
1.三种指针的运算:(掌握)
1.1指针 +, - 整数:
//指针 + 整数 #include <stdio.h> int main() { int a[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; int i = 0; int sz = sizeof(a) / sizeof(a[0]); //printf("%d\n", sz); int *p = a; for (i = 0; i < sz; i++) { //printf("%d ", *(p + i)); printf("%d ", *p); p = p + 1; //指针加1 //p++; } }
//指针 - 整数 #include <stdio.h> int main() { int a[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; int i = 0; int sz = sizeof(a) / sizeof(a[0]); int* p = &a[9]; //p指针指向数组a第九个元素 for (i = 0; i < sz; i++) { printf("%d ", *p); p = p - 1; //p指针-1 } return 0; }
通过两个例子我们可以发现,指针加减整数就是指针的移动。
1.2指针 - 指针:
(1)数组中指针减指针得到的是数组元素的个数:
举例如下:
#include <stdio.h> int main() { int a[6] = { 1, 2, 3, 4, 5, 6 }; printf("%d\n", &a[6] - &a[0]); /* int* p = &a[6]-&a[0]; printf("%d\n", p); */ return 0; }
(2)字符串数组中指针减指针得到的是字符串的个数:
举例如下:
//模拟strlen()函数,用指针的方式求字符串的长度 #include <stdio.h> int my_strlen(char* str)//用字符指针p接收字符数组a的首地址,返回int类型 { char* start = str;//定义开始指针,指向字符元素的首地址 char* end = str;//定义结束指针,遇到\0就结束 while (*end != '\0') //*end等于'\0'后就跳出循环 { end++; } return end - start; } int main() { char a[] = "nice"; // n i c e \0 int len = my_strlen(a); //传的是字符数组a的首地址 printf("%d\n", len); return 0; }
2.指针引用多维数组:
先定义一个二维数组
int a[3][4] = { {1,3,5,7},{9,11,13,15},{17,19,21,23} };
a 是二维数组名,a 数组包含 3 行,即 3 个行元素:a[0],a[1],a[2]。而每一个行元素又是一个一维数组,它包含 4 个元素(即 4 个列元素)。例如, a[0] 所代表的一维数组又包含 4 个元素:a[0] [0],a[0] [1],a[0] [2],a[0] [3]。可以认为二维数组是 “数组的数组”,即二维数组 a 是由 3 个一维数组所组成的。
从二维数组的角度来看,a 代表二维数组首元素的地址,现在的首元素不是一个简单的整型元素,而是由 4 个整型元素所组成的一维数组,因此 a 代表的是首行(即序号为 0 的行)的起始地址。a+1代表序号为 1 的行的起始地址。如果二维数组的首行的起始地址为2000,一个整型数据占 4 个字节,则 a+1 的值应该是 2000+4×4=2016 (因为第 0 行有 4 个整型数据)。a+1 指向a[1],或者说,a+1 的值是 a[1] 的起始地址。a+2 代表 a[2] 的起始地址,它的值是2032。
a[0],a[1],a[2] 既然是一维数组名,从前面已知,数组名代表数组首元素地址,因此 a[0] 代表一维数组 a[0] 中第 0 列元素的地址,即 &a[0] [0]。同理,a[1] 的值是 &a[1] [0],a[2] 的值是 &a[2] [0]。
a[0] 是一维数组名,该一维数组中序号为 1 的元素的地址显然应该用 a[0]+1 来表示。此时 “a[0]+1” 中的 1 代表 1 个列元素的字节数,即4个字节。a[0] 的值是2000,a[0]+1 的值是2004(而不是2016)。这是因为现在是在一维数组范围内讨论问题,正如有一个一维数组x,x+1 是其第 1 个元素x[1] 的地址一样。a[0]+0,a[0]+1,a[0]+2,a[0]+3 分别是a[0] [0],a[0] [1],a[0] [2],a[0] [3] 元素的地址(即 &a[0] [0],&a[0] [1],&a[0] [2],&[0] [3])。
在上面我们说过*(a+i)与a[i]等价,因此,a[0]+1和 *(a+0)+1 都是 &a[0] [1] (即地址都为2004)。a[1]+2 和 *(a+1)+2 的值都是 &a[1] [2] (即地址为2024)。请注意不要将*(a+1)+2 错写成 *(a+1+2),后者变成 *(a+3) 了,相当于 a[3] 。进一步分析,欲得到 a[0] [1] 的值,用地址法怎么表示呢?既然 a[0]+1 和 (a+0)+1 是 a[0] [1] 的地址,那么,(a[0]+1) 就是 a[0] [1] 的值。同理,((a+0)+1) 或 (a+1) 也是 a[0] [1] 的值。*(a[i]+j) 或 ((a+i)+j)是 a[i] [j] 的值。务请记住 *(a+i) 和 a[i] 是等价的。
注意:a[i] 从形式上看是 a 数组中序号为 i 的元素。如果a是一维数组名,则 a[i] 代表 a 数组序号为 i 的元素的存储单元。a[i] 是一个有确定地址的存储单元。但如果 a 是二维数组,则 a[i] 是一维数组名,它只是一个地址,并不代表一个存储单元,也不代表存储单元中的值(如同一维数组名只是一个指针常量一样)。a,a+i,a[i],* (a+i),* (a+i)+j,a[i]+j 都是地址。而 *(a[i]+j) 和 ((a+i)+j) 是二维数组元素 a[i] [j] 的值。
简单来说除了*(a[1]+2),*(*(a+1)+2),a[1] [2]以外的其他形式都是地址。
2.1地址举例说明:
表现形式 | 含义 | 值 |
---|---|---|
a | 二维数组名,指向行,即一维数组a[0],即0行的起始地址 | 2000 |
a[0] | 一维数组名,指向列,即整型常量a[0] [0],即0行0列元素的地址 | 2000 |
*(a+0),*a | 0 行 0 列元素的地址 | 2000 |
a+1,&a[1] | 第 1 行 (a[1]) 的起始地址 | 2016 |
a[1],*(a+1) | 第 1 行 0 列元素 a[1] [0] 的地址 | 2016 |
a[1]+2,*(a+1)+2,&a[1] [2] | 第 1 行 2 列元素 a[1] [2] 的地址 | 2024 |
*(a[1]+2),*(*(a+1)+2),a[1] [2] | 第 1 行 2 列元素 a[1] [2] 的值 | 13 |
注意:2000只是随便写的地址作用是方便理解,具体是多少得看实际情况。
//代码讲解 #include<stdio.h> int main() { int a[3][4] = { {1,3,5,7},{9,11,13,15},{17,19,21,23} }; //输出的是地址 printf("%d\n", a); printf("%d\n", a[0]); printf("%d\n", *a); printf("%d\n", *(a+0)); putchar('\n'); printf("%d\n", *(a+1)); printf("%d\n", &a[1]); printf("%d\n", a+1); printf("%d\n", a[1]); putchar('\n'); printf("%d\n", a[1]+2); printf("%d\n", *(a+1)+2); printf("%d\n", &a[1][2]);//*(a[1]+2),*(*(a+1)+2),a[1][2] putchar('\n'); printf("%d\n", *(a[1]+2)); printf("%d\n", *(*(a+1)+2)); printf("%d\n", a[1][2]); } /*代码输出 6487536 6487536 6487536 6487536 6487552 6487552 6487560 6487560 6487560 13 13 13 */
关于a+1与a[1]的讲解:
a+1 是二维数组 a 中序号为 1 的行的起始地址(序号从 0 起算),而 (a+1) 并不是 a+1 单元的内容(值),因为 a+1 并不是一个数组元素的地址,也就谈不上存储单元的内容了。(a+1) 就是 a[1],而a[1] 是一维数组名,所以也是地址,它指向a[1] [0]。a[1]和 *(a+1) 都是二维数组元素 a[1] [0] 的地址的不同的表示形式。
2.2地址详解:(做了解)
2.3指向数组元素的指针变量
实列:有一个3×4的二维数组,要求用指向元素的指针变量输出二维数组各元素的值。
#include<stdio.h> int main() { int a[3][4] = { 1,3,5,7,9,11,13,15,17,19,21,23 }; int *p; //p是int型的指针 for (p = a[0]; p < a[0] + 12; p++)//使p依次指向下一个元素 { if (p != a[0] && ((p - a[0]) % 4 == 0)) printf("\n");//p移动4次后换行 printf("%4d ", *p); //输出p所指向元素的值 } } /*代码输出: 1 3 5 7 9 11 13 15 17 19 21 23 */
如何理解代码呢?相信大家会对for循环和if的理解有点困难。
首先需要记住计算 a[i] [j] 在数组中的相对位置的计算公式为:i * m + j
其中,m 为二维数组的列数(二维数组大小为 n×m )。例如,对上述 3×4 的二维数组,它的 2 行 3 列元素 a[2] [3] 对 a[0] [0] 的相对位移量为 2×4+3=11 元素。如果一个元素占 4 个字节,则 a[2] [3] 对 a[0] [0]的地址差为 11×4=44 字节。若开始时指针变量 p 指向 a[0] [0],a[i] [j] 的地址为 “&a[0] [0]+(i*m+j)” 或 “p+(i*m+j)” 。a[2] [3] 的地址是 (p+2*4+3),即 (p+11)。a[2] [3] 的值为 *(p+11)。
从图中可以看到在 a[i] [j] 元素之前有 i 行元素(每行有 m 个元素),在 a[i] [j] 所在行,a[i] [j] 的前面还有 j 个元素,因此 a[i] [j] 之前共有 i×m+j 个元素。例如,a[2] [3]的前面有两行,共 2×4=8 个元素,在它本行内还有 3 个元素在它前面,故共有 8+3=11 个元素在它之前,可用 p+11 表示其相对位置。
2.4实例练习:
在示例的基础上,如果二维数组a[i] [j]的下标i+j值为偶数则变为1,输出格式不变。
//常规思路: #include<stdio.h> int main() { int a[3][4] = { { 1, 3, 5, 7 }, { 9, 11, 13, 15 }, { 17, 19, 21, 23 } }; int i, j; int *p, *ptr; printf("原始数组 a:\n"); for (p = a[0]; p < a[0] + 12; p++) //使p依次指向下一个元素 { if (p != a[0] && ((p - a[0]) % 4 == 0)) printf("\n"); //p移动4次后换行 printf("%-5d ", *p); //输出p所指向元素的值 } putchar('\n'); printf("处理后的数组 a:\n"); for (i = 0; i < 3; i++) { for (j = 0; j < 4; j++) { if (((i + j) % 2 == 0)) { a[i][j] = 1; } printf("%-5d ", a[i][j]); } printf("\n"); } return 0; }
//使用指针: #include<stdio.h> int main() { int a[3][4] = { { 1, 3, 5, 7 }, { 9, 11, 13, 15 }, { 17, 19, 21, 23 } }; int i, j; int *p, *ptr; printf("原始数组 a:\n"); for (p = a[0]; p < a[0] + 12; p++) //使p依次指向下一个元素 { if (p != a[0] && ((p - a[0]) % 4 == 0)) printf("\n");//p移动4次后换行 printf("%-5d ", *p);//输出p所指向元素的值 } putchar('\n'); printf("处理后的数组 a:\n"); for (ptr = &a[0][0]; ptr <= &a[2][3]; ptr++) { if (((ptr - &a[0][0]) / 4 + (ptr - &a[0][0]) % 4) % 2 == 0) { *ptr = 1; } printf("%-5d ", *ptr); if ((ptr - &a[0][0] + 1) % 4 == 0)//每行输出完成后换行 { printf("\n"); } } return 0; } //(ptr - &a[0][0])计算了指针相对于数组起始地址的偏移量。这个偏移量告诉我们当前指针位于数组中的哪个位置。 //(ptr - &a[0][0]) / 4计算了当前元素的行号(i值),因为在这个数组中,每行有4个元素。这个操作会将偏移量除以4,得到行号。 //(ptr - &a[0][0]) % 4计算了当前元素的列号(j值),因为每行有4个元素,所以取偏移量除以4的余数即可得到列号。 //最后,计算了当前元素的值。这个值表示了元素在二维数组中的位置之和。((ptr - &a[0][0]) / 4 + (ptr - &a[0][0]) % 4)i + j
3.指向函数的指针:
3.1什么是函数的指针:
如果在程序中定义了一个函数,在编译时会把函数的源代码转换为可执行代码并分配一段存储空间。这段内存空间有一个起始地址,也称为函数的入口地址。每次调用函数时都从该地址入口开始执行此段函数代码。 函数名就是函数的指针,它代表函数的起始地址。
总结:简单来说,函数的指针就是指向函数首地址的一个指针,通过访问指针来找到并使用函数。
例如:
可以定义一个指向函数的指针变量,用来存放某一函数的起始地址,这就意味着此指针变量指向该函数。
int (*p)(int,int); //定义p是一个指向函数的指针变量,可以指向函数类型为整型且有两个整型参数的函数。指针变量p的类型用int (*)(int,int)表示。
3.2定义和使用函数指针:
如何定义并使用指针呢?我们通过一个例子来学习:输入a,b,c的值,输出最大者:
#include <stdio.h> int max(int a, int b, int c) { int temp; if(a > b) { if(a > c) temp = a; else temp = c; } else { if(b > c) temp = b; else temp = c; } return temp; } int main() { int a, b, c, t; int (*p)(int, int, int); //定义指向函数的指针变量p scanf("%d%d%d", &a, &b, &c); p = max; //引用指针,让它指向max函数的起始地址,也就是将max函数的起始地址赋值给指针变量p t = (*p)(a, b, c); //使用指向max函数的指针变量p(*p替换函数名) printf("max = %d\n", max(a, b, c)); printf("max = %d\n", t); return 0; }
首先,我们先编写好我们的max函数,然后我们便可以在主函数里定义我们的函数指针:
//格式:类型名 (*指针变量名)(函数参数表列) int (*p)(int,int);
注意事项:
(1) 定义指向函数的指针变量,并不意味着这个指针变量可指向任何函数,它只能指向在定义时指定的类型的函数。
(2) 如果要用指针调用函数,必须先使指针变量指向该函数。
(3) 在给函数指针变量赋值时,只须给出函数名而不必给出参数。
(4) 用函数指针变量调用函数时,只须将(*p)代替函数名即可(p为指针变量名),在(*p)之后的括号中根据需要写上实参。
(5) 对指向函数的指针变量不能进行算术运算,如p+n,p++,p--等运算是无意义的。
(6) 用函数名调用函数,只能调用所指定的一个函数,而通过指针变量调用函数比较灵活,可以根据不同情况先后调用不同的函数。
易错点:
(1)指针必须加括号,表示p先与*结合,是指针变量,再与后面的()结合,()表示函数部分,即该指针变量不是指向一般的变量,而是指向函数,如果
int*p(int, int);
则表示声明了一个名字为p的函数,返回值为指向整型变量的指针。
总结:如何使用指向函数的指针呢,和常规的使用差不多:
1.定义;2.引用;3.替换使用。
3.3实例练习:
输入一个小于100的十进制数,编写一个函数将十进制数转换为二进制并输出,要求通过指针来引用函数
#include <stdio.h> void dec_bin(int x) { int a[10]; for(int j=0; j<10; j++) a[j] = -1; while(x) { a[i] = x % 2; x = x / 2; i++; } for(int i = 10; i>=1; i--) if(a[i-1]!=-1) printf("%-2d", a[i-1]); } int main() { int dec; scanf("%d", &dec); void (*p)(int); p = dec_bin; (*p)(dec); return 0; }
4.返回指针值的函数:
4.1使用返回指针值的函数:
格式:
类型名 *函数名(参数表列)
我们通过一个例子来学习:
输入x, y的值,编写一个函数,将x, y作为参数,若不相同则返回指针输出最大者,否则指针为空。
#include <stdio.h> int *max(int x, int y) { int *p, max;//定义int类型的指针变量p和int类型的整形变量max p =NULL;//将指针p置空 if(x != y) { max = x>y?x:y; p = &max; } return p; } int main() { int x, y,*q; scanf("%d%d", &x, &y); q = max(x, y); if(q)//q != NULL { printf("max = %d\n", *q); } else { printf("x and y is same.\n"); } return 0; }
强化理解1:
存有字母A到Z,26个字母的字符串数组a[27],我们随机输入一个数字n(1~26),通过访问数组a来输出对应的字母A~Z。
#include <stdio.h> //我们也可以这样定义函数 //char* search(char a[], int n) //原因:我们讲过数组就是指针,指针就是数组 char *search(char *a, int n)//定义一个char*类型的search函数,参数为数组首地址和所需的字母位数 { char *c; c = (a+(n-1));//a为数组首地址,我们认为是指针,然后加上(n-1)就是指针的移动 //如果不好理解,我们可以这样想: // for(int i; i<n; i++) // { // c = (a+i); // } return c;//返回指针 } int main() { int n; char a[27] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";//定义存有26个字母的数组a char *x;//定义一个指针变量x scanf("%d", &n); x = search(a, n);//调用函数search,把返回的指针赋值给指针变量x printf("%c", *x);//输出指针x对应的值 return 0; }
在刚才的练习下加深理解:
强化理解2:
存有字母A到Z,26个字母的字符串数组a[27],我们随机输入一个数字n(1~26),通过访问数组a来输出第n个字母后面的所有字母。
#include <stdio.h> //我们也可以这样定义函数 //char* display(char a[], int n) //原因:我们讲过数组就是指针,指针就是数组 char *display(char *a, int n)//定义一个char*类型的display函数,参数为数组首地址和字母位数 { char *c; c = NULL;//先让指针为空 if(n<=25)//如果为前25个字母,则表示后面有字母,指针不为空 { c = (a+n);//a为数组首地址,我们认为是指针,然后加上n就是指针的移动,移动到第n个字母后面 // //如果不好理解,我们可以这样想: // for(int i; i<=n; i++) // { // c = (a+i); // } } return c;//返回指针 } int main() { int n; char a[27] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";//定义存有26个字母的数组a char *x;//定义一个指针变量x scanf("%d", &n); x = display(a, n);//调用函数display,把返回的指针赋值给指针变量x if(x)//x != NULL 指针不为空 { for(int i=0; x+i<&a[26]; i++)//输出第n个字母后面的所有字母 { printf("%-2c", *(x+i)); } putchar('\n'); } else//指针为空 { printf("无字母\n"); } return 0; }
5.指针数组与数组指针:
5.1定义和使用指针数组:
指针数组,顾名思义就是指针类型的数组,首先是一个数组,数组元素存储的是指针。那么如何使用呢?
格式:
类型名 * 数组名[数组长度]
注意:(注意优先级:( )>[ ]> *),所以:
int* p[n];
[ ]> * ,所以p是数组,是一个由n个指针类型元素组成的指针数组,或者说这个当一个数组里含有的元素为指针类型的时候,它就被成为指针数组。当p+1时,则p指向下一个数组元素。(需注意,p=a;这种赋值方法是错的,因为p是一个不可知变量,只存在p[0],p[1],p[2],但可以这样 p=a; 这里p表示指针数组第一个元素的值,a的首地址的值) 将二维数组赋值给指针数组:
#include <stdio.h> int main(void) { int *p[3], i; //定义指针数组 int a[3][4]; for(i=0;i<3;i++) p[i]=a[i]; //通过循环将a数组每行的首地址分别赋值给p里的元素 return 0; }
这里int * p[3]表示一个一维数组内存放三个指针变量,分别是p[1],p[2],p[3]。如果要引用二维数组时,可以有多种表达方式:如表示数组中i行j列一个元素:
*(* (p+i)+j)、(* (p+i) )[j]、p[i] [j]、 * (p[i]+j)
指针数组最重要的用途是对多个字符串进行处理操作<已弃用>,因为字符指针比二维数组更快更有效。
原因:
[Warning] deprecated conversion from string constant to 'char*' [-Wwrite-strings]
[警告]已弃用从字符串常量到'char*'的转换[-Wwrite-strings]
例:
#include <stdio.h> void func(char **p) { char *t; t = (p += 3)[-1]; printf("%s\n",*p); printf("%s\n",t); } int main() { char *arr[] = {"ab","ce","ef","gh","ij","kl"};//指针数组 func(arr); return 0; }
5.2定义和使用数组指针:
数组指针也称为行指针,格式:
类型名 (*数组名)[数组长度]
注意:(注意优先级:( )>[ ]> *),所以:
int (*p)[n];
( )>[ ],所以首先p 和*结合,说明 p是一个指针,然后int 和[n]都是修饰这个指针的,意思是,这个指针指向了一个里面有n个int型元素的数组。
当数组指针指向一个一维数组时:
#include <stdio.h> #define n 10 int main() { int (*p)[n]; //定义了指向含有n个元素的一维数组的指针 int a[n]; //定义数组 p=a; //将一维数组首地址赋值给数组指针p return 0; }
()优先级高,说明p是指针,指向一个整型的一维数组。这个一维数组的长度是n,也可以说p的步长为n。当p+1时,p指针会跨过n个整型数据的长度。
当数组指针指向一个二维数组时:
#include <stdio.h> int main() { int (*p)[4]; //定义了指向含有4个元素的一维数组的指针 int a[3][4]; p=a; //将二维数组的首地址赋值给p,也可是a[0]或&a[0][0] p++; //表示p跨过行a[0][],指向了行a[1][] return 0; }
所以,数组指针也成为指向一维数组的指针,也就是行指针。
#include <stdio.h> int main() { int arr[][3] = {1,2,3,4,5,6}; int (*ptr)[3] = arr; printf("%d,%d\n",(*ptr)[0],(*ptr)[1]); ptr++; //ptr = ptr + 1;ptr跨过了a[0][],指向了a[1][] printf("%d,%d\n",(*ptr)[0],(*ptr)[1]); return 0; }
ok,到这里,数组指针和指针数组的区别就很明了,数组指针(行指针)是一个指针变量,似乎是C语言里专门用来指向二维数组的,它的p+1表示到下一行。指针数组是指针类型的数组,里面的元素是指针类型的数据。
关于它们的使用有点困难就不举例了。
6.多重指针:
我们简单讲一下:
#include<stdio.h> int main() { int a = 10; int *p, **p1; p = &a; p = &p; printf("%d\n", **p1); return 0; }
代码中 *p 就是正常的指针,**p 就表示这是一个指向指针的指针。*p 是指向变量,而**p1 就是表示指向指针。其实指针也是一个变量,他也有一个地址,**p1 就表示他将会指向一个指针,而指向的那个指针则会指向一个变量,操作 **p1就等于操作那个最终指向的变量。
也就是说,多重指针就是套娃式的操作。
#include<stdio.h> int main() { int a = 10; int *p, **p1, ***p2; p = &a; p1 = &p; p2 = &p1; printf("%d\n", ***p2); return 0; }
我们看这个代码就如同上面一样的意思。
更深入的知识就需要自己去了解了。
四.作业:
1.输入两个小写字母,通过引用指针输出更后面的字母;
2.通过引用指针输出数组a[9] = "ntaiiatn";的偶数项;
3.输入n个1~24的数字,通过引用指针和函数将他们转换为对应的字母输出后并排序输出。
第一题:
#include <stdio.h> int main() { char c1, c2; char *p1, *p2, *p3; scanf("%c %c", &c1, &c2); p1 = &c1; p2 = &c2; p3 = (*p1 - *p2) > 0 ? p1 : p2; printf("%c\n", *p3); return 0; }
第二题:
#include <stdio.h> int main() { char a[9] = "ntaiiatn"; char *p; p=a; for(int i = 0;i<8;i++) { if((i+1) % 2 == 0) { printf("%c",*(p+i)); } } return 0; }
第三题:
#include <stdio.h> // 定义一个函数,将数字映射为字母 char mapNumberToLetter(int num) { if (num >= 1 && num <= 26) { return 'A' + num - 1; // A对应1,B对应2,以此类推 } return '?'; // 如果数字不在1~26范围内,返回? } // 定义一个函数,接受一个整数数组和其大小,将数字转换为字母并排序输出 void convertAndSortNumbers(int* numbers, int size) { char letters[size]; // 用于存储转换后的字母 // 遍历输入的数字数组,将数字转换为字母并存储在动态分配的内存中 for (int i = 0; i < size; ++i) { letters[i] = mapNumberToLetter(numbers[i]); } printf("排序前的字母:"); for (int i = 0; i < size; ++i) { printf("%c ", letters[i]); } printf("\n"); // 对字母进行排序 for (int i = 0; i < size - 1; ++i) { for (int j = 0; j < size - i - 1; ++j) { if (letters[j] > letters[j + 1]) { char temp = letters[j]; letters[j] = letters[j + 1]; letters[j + 1] = temp; } } } // 输出排序后的字母 printf("排序后的字母:"); for (int i = 0; i < size; ++i) { printf("%c ", letters[i]); } printf("\n"); } int main() { int n; printf("请输入数字个数n:"); scanf("%d", &n); int numbers[n]; printf("请输入%d个1~24之间的数字:\n", n); for (int i = 0; i < n; ++i) { scanf("%d", &numbers[i]); } convertAndSortNumbers(numbers, n); return 0; }