导图社区 C指针思维导图
C语言的指针既简单又有趣。通过指针,可以简化一些 C编程任务的执行,还有一些任务,如动态内存分配,没有指针是无法执行的。
编辑于2021-07-30 19:56:04C指针
指针变量
定义
一种存放另一种变量的地址的特殊变量
存放数据的变量称为指针变量所指向的目标变量
示例
我白天发现老婆把钱放在了3010的抽屉里; 我偷偷打开屉子,方向里面有张字条写着: 钱被我放在了2000的屉子里了,于是我在 2000屉子里,找到了123块钱
123块钱是目标变量
2000是指针变量
指针变量的取值存放的是目标变量的地址
可以通过指针变量中的地址直接访问它指向的目标变量
2000是目标变量的地址 3010是指针变量的地址 2000是指针变量的值 我们通过指针变量存储的 值去找到目标变量的地址 123是我们目标变量所存储的值
定义方法
类型名 * 指针变量名
int * point_1,* point_2;
基类型为 int 的指针变量 point_1 和 point_2 只能用来指向整型的变量。
在定义指针变量时要注意,指针变量前面的 “*” 表示该变量的类型为指针型变量。
指针变量名是 point_1 和 point_2,而不是 *point_1 和 *point_2。这是和定义整型或实型变量不同的。
赋值
#include<stdio.h> int main() { //定义普通变量 float a = 99.5, b = 10.6; char c = '@', d = '#'; //定义指针变量同时赋值 float *p1 = &a; char *p2 = &c; //修改指针变量的值 p1 = &b; p2 = &d; // 定义指针变量 char *p3; float *p4; // 赋值指针变量 p3 = &c; p4 = &a; // 打印目标变量的地址 printf("p1=%p, p2=%p, p3=%p, p4=%p\n",&b, &d, &c, &a); // 打印指针变量的值,也即所指向的目标变量的地址 printf("p1=%p, p2=%p, p3=%p, p4=%p\n",p1, p2, p3, p4); // 打印 a, b, c, d变量的值 printf("a=%.1lf, b=%.1lf, c=%c, d=%c\n", a, b, c, d); // 打印 a, b, c, d变量的值,也就是*所指向的变量的值 printf("a=%.1lf, b=%.1lf, c=%c, d=%c\n", *p4, *p1, *p3, *p2); return 0; }
*是一个特殊符号,表明一个变量是指针变量, 定义 p1、p2 时必须带*。而给 p1、p2 赋值时, 因为已经知道了它是一个指针变量,就没必要多 此一举再带上*,后边可以像使用普通变量一样来 使用指针变量。也就是说,定义指针变量时必须 带*,给指针变量赋值时不能带*。
需要强调的是,p1、p2 的类型分别是float*和char*,而不是float和char, 它们是完全不同的数据类型,读者要引起注意。
p1=0x7ffda1bc0974, p2=0x7ffda1bc096f, p3=0x7ffda1bc096e, p4=0x7ffda1bc0970 p1=0x7ffda1bc0974, p2=0x7ffda1bc096f, p3=0x7ffda1bc096e, p4=0x7ffda1bc0970 a=99.5, b=10.6, c=@, d=# a=99.5, b=10.6, c=@, d=#
命名
与一般变量命名相同,遵循C语言标志符的规则
类型
是指针变量所指向的目标变量的类型,而不是自身的类型
如123是目标变量int
float*
char*
int*
示例
问题:通过指针变量访问整数类型
#include<stdio.h> int main() { int a = 100; int b = 10; int *point_1, *point_2; //定义指向整型数据的指针变量 point_1 = &a; point_2 = &b; printf("a=%d, b=%d\n", a, b); printf("%p, %p\n", point_1, point_2); //输出 a 和 b 在内存中的地址 printf("*point_1=%d, *point_2=%d\n", *point_1, *point_2); //输出变量 a 和 b 的值 return 0; }
%p是打印地址的, %x是以十六进制形式打印, 完全不同!另外在64位下结果会不一样, 所以打印指针老老实实用%p .
a=100, b=10 0x7ffdc9ff3e40, 0x7ffdc9ff3e44 *point_1=100, *point_2=10
程序分析
在开头处定义了两个指针变量 point_1 和 point_2。但此时他们并未指向任何一个变量,只是提供两个指针变量,规定他们可以指向整形变量,至于指向哪一个整形变量,要在程序语句中指定。
程序第 6,7 两行的作用就是使 point_1 指向 a,point_2 指向 b,此时 point_1 的值为 &a(即 a 的地址),point_2 的值为 &b。
第 10 行输出 *point_1 和 *point_2 的值,其中的 “*” 表示“指向”。
其中 *point_1 表示指针变量 point_1 所指向的变量,也就是变量 a。 *point_2 表示指针变量 point_2 所指向的变量,也就是变量 b。从运 行结果来看他们也就是 100 和 10。
程序中有两处出现 *point_1 和 *point_2,但是两者含义不同。
程序第 4 行的 *point_1 和 *point_2 表示定义两个指针变量 *point_1 和 *point_2。 它们前面的 “*” 只是表示该变量是指针变量。
程序最后 10 行中的 printf 函数中的 *point_1 和 *point_2 则表示指针变量 point_1 和 point_2 所指向的变量
上面程序 6 行和 7 行是不能写成 * point_1=&a; 和 *point_2=&b; 的。因为 a 的地址是赋给 指针变量 point_1,而不是赋值给 *point_1(即变量 a)。
引用指针变量
给指针变量赋值
p=&a; //把 a 的地址赋给指针变量 p
指针变量 p 的值是变量 a 的地址,p 指向 a。
引用指针变量指向的变量
printf("%d",*p);
如果已经执行 p=&a; 即指针变量 p 指向了整型变量 a
其作用是以整数形式输出指针变量 p 所指向的变量的值,即变量 a 的值。
*p = 1;
表示将整数 1 赋给 p 当前所指向的变量,如果 p 指向变量 a,则相当于把 1 赋给 a,即 a=1;。
引用指针变量的值
printf("%o",p);
用是以八进制数形式输出指针变量 p 的值,如果 p 指向了 a,就是输出了 a 的地址,即 &a。
printf("%p",p);
printf("%p",&a);
地址运算符 & 和指针运算符 *
示例
输入两个整数,按先大后小的顺序输出 a 和 b。
#include<stdio.h> /* 输入两个整数,按照先大后小的顺序输出 a 和 b */ int main() { int *p1, *p2, *p, a, b; printf("Please enter two integer numbers:"); scanf("%d %d", &a, &b); //scanf这个格式控制中不要有任何空格或者逗号 p1 = &a; p2 = &b; // 注意这里是交换指针的值 // 即指针的指向发生改变 // 但是两个变量a, b 实际不发生改变 if(a < b) { p = p1; p1 = p2; p2 = p; //互换p1和p2的值 } printf("a=%d, b=%d\n", a, b); printf("max=%d, min=%d\n", *p1, *p2); return 0; }
程序分析
a 和 b 的值并未交换,他们仍然保持原值,但 p1 和 p2 的值改变了。 p1 的值原来为 &a,后来变为 &b。这样输出 *p1 和 *p2 时,实际上 输出变量 b 和 a 的值,所以先输出 42,然后输出 23。 程序中 p=p1;p1=p2;p2=p; 这个语句是我们之前使用过的方法, 两个变量的值交换要利用第三个变量。实际上学到指针我们可以用更加 简洁的方法把 p=p1;p1=p2;p2=p; 改为 p1=&b,p2=&a,这样就不需 要定义中间变量 p,使得程序更加简洁。
指针变量作为函数参数
函数的参数不仅可以是整形,浮点型等数据,也可以是指针类型。他的作用是将一个变量的地址传送到另一个函数中。
示例1
#include<stdio.h> int main() { void swap(int * point_1, int * point_2); int *p1, *p2, a, b; printf("please enter two integer numbers:"); scanf("%d%d", &a, &b); p1 = &a; p2 = &b; if(a<b) { swap(p1, p2); } // 如果 a < b, 那么 a 和 b 的值进行了交换 // 如果打印了 *p1和 *p2 我们会发现, p1 扔指向 a, p2 扔指向 b printf("max=%d, min=%d", a, b); return 0; } void swap(int * point_1, int * point_2) { int temp; /* 交换指针所指变量的值,指针的指向不发生变化,但是变量的值发生了变化 */ temp = * point_1; * point_1 = * point_2; * point_2 = temp; }
程序分析
swap 是用户自定义函数,它的作用是交换两个变量(a 和 b)的值。
swap 函数的两个形参 point_1 和 point_2 是指针变量。程序运行时, 先执行 main 函数,输入 a 和 b 的值(我们输入的是 23 和 34),然后将 a 和 b 的地址分别赋给 int 指针变量 p1 和 p2,使 p1 指向 a,p2 指向 b, 见下图 a,接着执行 if 语句,由于 a<b,因此执行 swap 函数。
注意实参 p1 和 p2 是指针变量,在函数调用时,将实参变量的值传送给形参变量, 采取的依然是“值传递”方式。因此虚实结合后形参 point_1 的值是 &a,point_2 的值为 &b,见下图 b。这时 p1 和 point_1 都指向变量 a,p2 和 point_2 都指向 变量 b。 接着执行 swap 函数的函数体,使 * point_1 和 * point_2 的值互换,也就 是使 a 和 b 的值互换。互换后的情况见图 c。
函数调用结束后,形参 point_1 和 point_2 不复存在(已释放),情况如图 d,最后 在 main 函数中输出的 a 和 b 的值已经是经过交换的值。
大家可以考虑下能否通过下面的函数实现 a 和 b 互换。
void swap(int x ,int y){ int temp; temp = x; x = y; y = temp; }
在函数结束时,变量 x 和 y 释放了,main 函数中的 a 和 b 并未互换。 也就是说,这种单向的值传递,形参值的改变不能使实参的值随之改变。
为了使在函数中改变了的变量值能被主调函数 main 所用, 不能采取上述的把要改变的变量作为参数的办法,而应该 用指针变量作为函数参数,在函数执行过程中使指针变量 所指向的变量值发生变化,函数调用结束后,这些变量值 依然保留了下来。
示例2
解题:求三个数的按最大到最小排序
#include<stdio.h> int main() { void exchange(int * q1, int * q2, int * q3); int a, b, c, *p1, *p2, *p3; printf("please enter 3 integer numbers:"); scanf("%d%d%d", &a, &b, &c); p1 = &a; p2 = &b; p3 = &c; exchange(p1, p2, p3); printf("the order is : %d, %d, %d\n", a, b, c); return 0; } void exchange(int * q1, int * q2, int * q3) { void swap(int * p1, int * p2); if (* q1 < * q2) { swap(q1, q2); } if(* q1 < * q3) { swap(q1, q3); } if(* q2 < * q3) { swap(q2, q3); } } void swap(int * p1, int * p2) { int temp; temp = * p1; *p1 = * p2; *p2 = temp; }
通过指针引用数组
可以用一个指针变量指向一个数组元素
指针变量 p 指向 a 数组的第 0 号元素
int a[10]={1,2,3,4,5,6,7,8,9,10}; int *P; p = &a[0];
在 C 语言中,数组名(不包括形参数组名,形参数组并不占据实际的内存单元)代表数组中首元素(即序号为 0 的元素)的地址。
p = &a[0]; //p 的值是 a[0] 的地址 p = a; //p 的值是数组 a 首元素(即 a[0])的地址
数组名不代表整个数组,只代表数组首元素的地址。上述 p=a; 的作用是“把 a 数组的首元素的地址赋给指针变量 p”,而不是 “把数组 a 各元素的值赋给 p”。
举例
#include<stdio.h> int main() { int a[10]; int i; printf("please enter 10 integer numbers:"); for(i = 0; i < 10; i++) { scanf("%d", &a[i]); } for(i = 0; i < 10; i++) { printf("%d\t", a[i]); } return 0; }
#include<stdio.h> int main() { int a[10]; int i; printf("please enter 10 integer numbers:"); for(i = 0; i < 10; i++) { scanf("%d", &a[i]); } for(i = 0; i < 10; i++) { // printf("%d\t", a[i]); printf("%d\t", *(a + i)); } return 0; }
#include<stdio.h> int main() { int a[10]; int i; int *p; printf("please enter 10 integer numbers:"); for(i = 0; i < 10; i++) { scanf("%d", &a[i]); } for(p = a; p < (a+ 10) ; p++) { // printf("%d\t", a[i]); // printf("%d\t", *(a + i)); printf("%d\t", *p); } return 0; }
方法对比
第 1 种和第 2 种方法的执行效率是相同的。C 编译系统是将 a[i] 装换为 *(a+i) 处理的,即先计算元素的地址。因此用第 1 和第 2 种方法找数组元素费时较多。
第 3 种方法比第 1、第 2 种方法快,用指针变量直接指向元素,不必每次都重新计算地址,像 p++ 这样的自加操作是比较快的。这种有规律的改变地址值 p++ 能大大提高执行效率。
用下标比较直观,能直接知道是第几个元素。例如,a[5] 是数组中序号为 5 的元素(注意序号是从 0 算起)。用地址发或者指针变量的方法不直观,难以很快的判断出当前处理的是哪一个元素。
如果不用 p 变化的方法而用数组名 a 变化的方法行不行呢?
for(p=a;p<(a+10);p++) printf("%d\t",*p);
for(p=a;a<(p+10);a++) printf("%d",*a);
这样是不行的。因为数组名 a 代表数组首元素的地址,它是一个指针型常量,它的值在程序运行期间是固定不变的。 既然 a 是常量,所以 a++ 是无法实现的。值得注意的它的值虽然不能改变,但是在程序中我们可以使用 a+i 表示 a[i], 这里 a+i 与 a++ 不同在于它并没有修改常量 a 的值(单独使用时,a++ 是等价于 a=a+1,它对 a 的值进行了修改)。
指针变量指向数组元素注意指针变量的当前值。
下标法,如 a[i],p[i] 的形式;直观,不易出错。 指针法,如 *(a+1) 或 *(p+i)。其中 a 是数组名,p 是指向数组元素的指针变量,其初值 p=a。
p[i],*(p+i) 的形式必须先使 p = a。
通过指针对数组元素进行操作
加减运算
P+1
如果指针变量 p 已指向数组中的一个元素,则 p+1 指向同一数组中的下一个元素
P-1
p-1 指向同一数组中的上一个元素
注意:执行 p+1 时并不是将 p 的值(地址)简单地加 1,而是加上一个数组元素所占用的字节数。
例如,数组元素是 float 型,每个元素占 4 个字节,则 p+1 意味着使 p 的值加 4 个字节, 以使它指向下一个元素。p+1 所代表的地址实际上是 p+1*d,d 是一个数组元素所占的 字节数。若 p 的值是 2000,则 p+1 的值不是 2001 而是 2004。
如果 p 的初值为 &a[0],则 p+i 和 a+i 就是数组元素 a[i] 的地址
*(p+i)和 *(a+i) 是 p+i 或 a+i 所指向的元素, 即 a[i]。例如 *(p+5) 和 *(a+5) 就是 a[5],三者等价。
自增运算
P++
#include<stdio.h> int main() { int i, *p, a[10]; p = a; printf("please enter 10 integer numbers:"); for(i=0; i <10; i++) { scanf("%d", p++); } p = a; //指针变量指向数组元素注意指针变量的当前值。 for(i=0; i<10; i++, p++) { printf("%d", *p); } printf("\n"); return 0; }
可能大家会觉得上面的程序没有什么问题,即使已被告知此程序有问题, 还是找不出问题出在哪里。问题出在指针变量 p 的指向。指针变量 p 的初始值为 a 数组首元素的地址,但经过第一个 for 循环读入数据后, p 已指向 a 数组的末尾。因此,在执行第 2 个 for 循环时,p 的起始 值不是 &a[0] 了,而是 a+10。由于执行第 2 个 for 循环时,每次要 执行 p++,因此 p 指向的是 a 数组下面的 10 个存储单元。
p++ 使 p 指向下一个元素 a[1]。然后若在执行 * p,则得到下一个元素 a[1] 的值。
p++; * p;
++p
由于 ++ 和 * 同优先级,结合方向为自右而左,因此它等价于 *(p++)。先引用 p 的值,实现 *p 的运算,然后再使 p 自增 1。
*p++;
*(p++) 与 *(++p) 作用是否相同?答案肯定是不相同的。前者是先取 *p 的值,然后使 p 加 1。后者是先使 p 加 1,再取 *p。
自减运算
P--
--P
遍历数组的成员可用
1. 下标法,如 a[2];
2. 通过数组名计算数组元素地址,找出元素的值;
3. 用指针变量指向数组元素。
用数组名做函数参数
int main(){ void fun(int arr[],int n); int array[10]; . . . fun(array,10); //用数组名作为函数的参数 return 0; } void fun(int arr[],int n){ . . . }
array 是实参数组名,arr 为形参数组名。 从前两节我们应该知道,当用数组名作为 参数时,如果形参数组中各元素的值发生 变化,实参数组元素的值随之变化。
程序分析
先看数组元素为实参时的情况。如果已经定义一个函数, 其原型为void swap(int x,int y);,假设函数的作用是将 两个形参 (x,y) 的值交换,现有以下的函数调用swap(a[1],a[2]);。 用数组元素 a[1] 和 a[2] 作为实参的情况,与用变量作实参时一样, 是“值传递”的方式,将 a[1] 和 a[2] 的值单向传递给 x 和 y。 当 x 和 y 的值改变时 a[1] 和 a[2] 的值并没有改变。
我们再来看数组名作为函数参数的情况, 实参数组名代表该数组首元素的地址,而形参是用来接收 从实参传递过来的数组首元素地址的。因此,形参应该是 一个指针变量。 实际上 C 编译系统是将形参数组名作为指针变量来处理的。
上一节说的实参数组名是一个指针常量
例如 fun(int arr[],int n) ,在程序编译时是将 arr 按指针变量处理的,相当于将函数 fun 的首部写成 fun(int *arr,int n);。 上面的两种写法是等价的。在该函数被调用时,系统会在 fun 函数中建一个指针变量 arr,用来存放从主调函数传递过来的实参数组首元素的地址。当 arr 接收了实参数组的首元素地址后,arr 就指向实参数组首元素,也就是指向 array[0]。因此 *arr 就是 array[0],*(arr+1) 就是 array[1]。
我们可以这样来理解,在函数调用期间,形参数组从实参数组那里得到起始地址,因此形参数组与实参数组共占同一段内存单元,在调用函数期间,如果改变了形参数组的值,也就是改变了实参数组的值。
示例1
题目:将数组 a 中 n 个整数按相反的顺序存放
代码实现
数组名作为形参
#include<stdio.h> int main() { void change(int x[], int n); int i; int a[10] = {3, 4, 5, 6,7, 8, 9, 0, 2, 1}; printf("the orginal array : \n"); for(i=0; i<10; i++) { printf("%d", a[i]); } printf("the inverted array is :\n"); change(a, 10); for(i=0; i<10; i++) { printf("%d\t", a[i]); } return 0; } void change(int x[], int n) { int temp; int i; int j; int m = (n-1) / 2; for(i=0; i<=m; i++) { j = n-1-i; temp = x[i]; x[i] = x[j]; x[j] = temp; } }
指针变量名作为形参
#include<stdio.h> int main() { void change(int x[], int n); int i; int a[10] = {3, 4, 5, 6,7, 8, 9, 0, 2, 1}; int *p = a; printf("the orginal array : \n"); for(i=0; i<10; i++) { printf("%d", a[i]); } printf("the inverted array is :\n"); change(p, 10); for(i=0; i<10; i++) { printf("%d\t", a[i]); } return 0; } void change(int *x, int n) { int temp; int *i; int *j; int *p; int m = (n-1) / 2; i = x; j = x + n -1; p = x + m; for(; i<=p; i++, j--) { temp = *i; *i = *j; *j = temp; } }
程序分析
将函数 change 中的形参 x 改成指针变量。相应的实参仍为数组名 a(即数组 a 首元素的地址), 将它传给形参指针变量 x,这时 x 就指向 a[0]。x+m 是 a[m] 元素的地址。设 j 和 i 以及 p 都 是指针变量,用它们指向有关元素。i 的初值 x,j 的初值为 x+n-1,见下图。使得 *i 与 *j 交换就是 a[i] 和 a[j] 交换
归纳分析
如果有一个实参数组,要想在函数中改变数组中的元素的值,实参和形参的对应关系有以下 4 种情况。
形参和实参都用数组名
int main(){ int a[10]; . . . f(a,10); } int f(int x[],int n){ . . . }
由于形参数组名 x 接收了实参数组首元素 a[0] 的地址,因此可以认为在函数调用期间,形参数组与实参数组共用一段内存单元,这种形式比较好理解。
形参用指针名,实参用数组名
int main(){ int a[10]; . . . f(a,10); } void f(int *x,int n){ . . . }
实参 a 为数组名,形参 x 为 int * 型的指针变量,调用函数开始后,形参 x 指向 a[0],即 x=&a[0],通过 x 的值的改变,可以指向 a 数组的任一元素。例 11-2.c 就属于此类。
形参和实参都用指针名
int main(){ int a[10],*p=a; . . . f(p,10); . . . } void f(int *x,int n){ . . . }
实参 p 和形参 x 都是 int * 型的指针变量。先使实参指针变量 p 指向数组 a[0],p 的值是 &a[0]。然后将 p 的值传给形参指针变量 x,x 的初始值也是 &a[0],通过 x 的值的改变可以使 x 指向数组 a 的任意元素。
形参用数组名,实参用指针名
int main(){ int a[10],*p=a; . . . f(p,10); } void f(int x[],int n){ . . . . }
实参 p 为指针变量,它指向 a[0]。形参为数组名 x, 编译系统把 x 作为指针变量处理,今将 a[0] 的地址 传给形参 x,使 x 也指向 a[0]。也可以理解为形参数 组 x 和 a 数组共用同一段内存单元。在执行过程中可 以使 x[i] 的值变化,而 x[i] 就是 a[i]。这样,main 函 数可以使用变化了的数组元素的值。