江小南
2023/01/08阅读:51主题:萌绿
聊聊指针
阅读本文,需要对C语言的基础知识有所了解。C语言基础,我给大家准备了详细的学习资料,微信公众号主页回复“C语言”即可领取。
引言
如果在程序中定义了一个变量,那么在对程序进行编译时,系统就会给这个变量分配内存单元,按变量地址存取变量值的方式我们称之为“直接访问”,如:printf(“%d”,i);scanf(“%d”,&i);等。
另外,如果将变量i的地址存放到另一个变量中,我们通过一个变量拿到另一个变量的地址,这种方式叫做“间接访问”,我么也把它称之为“指针”。
在C语言中,指针变量是一种特殊的变量,它用来存放变量地址。
1. 定义格式
基类型 *指针变量名;
例如:
int *i_pointer;
说明:指针与指针变量是两个概念,一个变量的地址称为该变量的“指针”。

如图:2000是地址,即变量i的指针。i_pointer存放的是2000,那么i_pointer称为指针变量。
2. 取地址操作符与取值操作符
取地址操作符为&,也称引用。通过该操作符我们可以获取一个变量的地址值;取值操作符为*,也称解引用。通过该操作符我们可以得到一个地址对应的数据。
指针变量的初始化一定是某个变量的地址。
例如:
#include <stdio.h>
int main() {
int i=5;
int *p=&i; // 指针变量初始化
printf("i=%d\n",i); // 直接访问
printf("*p=%d\n",*p); // 直接访问
return 0;
}
F:\Computer\Project\practice\6\6.1-pointer\cmake-build-debug\6_1_pointer.exe
i=5
*p=5
进程已结束,退出代码为 0
注意点:
-
指针变量前面的* 表示该变量的类型为指针变量。指针变量名为p,而不是*p。 -
在定义指针变量时必须指定其类型,指针变量类型和变量类型要保持一致。例如:int i;那么指针变量应为int *p,都是int类型。 -
&和*两个运算符的优先级别相同,但要按自右向左的方向结合。 -
如果要同时定义多个指针变量,需要使用:int *a,*b,*c…
3. 指针的简单应用
指针的使用场景通常只有两种,传递和偏移。
指针的传递
#include <stdio.h>
void change(int j){
j=5;
}
int main() {
int i=10;
printf("before change i=%d\n",i);
change(i);
printf("after change i=%d\n",i);
return 0;
}
F:\Computer\Project\practice\6\6.2-transmit\cmake-build-debug\6_2_transmit.exe
before change i=10
after change i=10
进程已结束,退出代码为 0
疑问:i=10经过了change函数,值应当变为5才对,为何没有改变?

分析:当main函数开始执行时,系统会为main函数开辟函数栈空间,当走到int i时,main函数的栈空间就会为变量i分配4个字节大小的空间,调用change函数时,系统会为change函数重新分配新的函数栈空间,并为形参j分配4个字节大小的空间,所以i和j对应空间地址是不同的。在调用change(i)时,实际上是将i的值赋给了j。我们把这种效果称为值传递(C语言的函数调用均为值传递)。因此,当我们在change函数的函数栈空间内修改变量j值后,change函数执行结束,其栈空间会释放,j就不存在了,i的地址和它不同,并没有被修改。
我们将以上程序做一些修改:
#include <stdio.h>
void change(int *j){
*j=5; // 注意这里不是j=5,而是*j=5
}
int main() {
int i=10;
printf("before change i=%d\n",i);
change(&i); // 传递变量i的地址
printf("after change i=%d\n",i);
return 0;
}
F:\Computer\Project\practice\6\6.2-transmit\cmake-build-debug\6_2_transmit.exe
before change i=10
after change i=5
进程已结束,退出代码为 0

很明显,当我们把i的地址传递给chang函数,那么它取到的值就是i的实际地址对应的值。当然修改的就是i的值了。
指针的偏移
指针的另一种场景就是对其进行加减。在实际中,我们把对指针的加减称为指针的偏移,加就是向后偏移,减就是向前偏移。
#include <stdio.h>
#define N 5
int main() {
int a[N]={1,2,3,4,5};
int *p=a; // 保证等号两边的数值类型一致。一维数组本身存放的就是首地址,所以没有必要写&取地址
int i;
for(i=0;i<N;i++){ // 正序输出
printf("%3d",*(p+i));
}
printf("\n------------------\n");
p=&a[N-1]; // 让P指向最后一个元素,注意有&
for(i=0;i<N;i++){ // 逆序输出
printf("%3d",*(p-i));
}
printf("\n");
return 0;
}
F:\Computer\Project\practice\6\6.3-deviation\cmake-build-debug\6_3_deviation.exe
1 2 3 4 5
------------------
5 4 3 2 1
进程已结束,退出代码为 0
说明:数组本身就是存放的就是指针,不需要加&,但是里面的具体数据需要通过&取值。

同时,偏移的长度是其基类型的长度,也就是偏移sizeof(int),这样通过*(p+1)就可以得到元素a[1]的值。
4. 指针与一维数组
疑问:为什么一维数组在函数调用进行传递时,它的长度子函数无法知道呢?
#include <stdio.h>
void change(char *d){ // 形参实际上接收到的是一个地址(数组的首地址)
*d='H'; // 指针法
d[1]='E'; // 下标法
*(d+2)='L'; // 指针法
}
int main() {
char c[10]="hello";
puts(c);
change(c); // 数组名作为参数传递给子函数时,是弱化为指针的
puts(c);
return 0;
}
F:\Computer\Project\practice\6\6.3-pointer_arr\cmake-build-debug\6_3_pointer_arr.exe
hello
HELlo
进程已结束,退出代码为 0
解析:因为一维数组名中存储的是数组的首地址。所以子函数change中其实传入了一个地址。定义一个指针变量时,指针变量类型要和数组类型保持一致,通过取值操作,就可以将"h"改为"H",这种方法称为指针法。获取数组元素时,也可以通过取下标的方式来获取数组元素并修改,这种方法称为下标法。
5. 动态指针与内存申请
平时我们定义的整型、浮点型、字符型变量、数组变量都是在栈空间中,而栈空间的大小在编译时确定的,不会改变。如果使用的空间大小不确定,就要使用堆空间。
堆和栈我们在后面的文章中会详细讲解。
#include <stdio.h>
#include <stdlib.h> // malloc需要的头文件
#include <string.h>
int main() {
int i;
char *p;
char q[50]; // 也可以写成char *q
scanf("%d",&i);
p=(char*) malloc(i); // 使用malloc动态申请堆空间,注意强制类型转换。堆空间刚申请下来是void类型,需要转换
strcpy(p,"malloc success");
strcpy(q,p);
puts(p);
free(p); // free时必须使用malloc申请时返回的指针值,同时不能进行任何偏移
puts(p); // 可以看到释放完后是乱码
puts(q);
return 0;
}
F:\Computer\Project\practice\6\6.4-apply\cmake-build-debug\6_4_apply.exe
20
malloc success
鄅
malloc success
进程已结束,退出代码为 0
说明:在执行void *malloc(size_t size)
时,需要给malloc传递的参数是一个整型变量。
当确定了用来存储什么类型的数据后,需要将void*强制转换为对应的类型。
同时需要注意指针本身的大小,和其指向空间的大小是两回事。

如图所示,定义的整型变量i,指针变量p均在main函数的栈空间中,通过malloc申请的空间会返回一个堆空间的首地址,我们把首地址存入变量p,知道了首地址,就可以通过strcpy函数往对应的空间存储字符数据。
堆的效率要比栈低得多。
栈空间由系统自动管理,而堆空间的申请和释放需要自行管理。
其传入的参数为void类型指针,任何指针均可自动转换为void*类型指针,所以p传递给free函数时,不需要强制类型转换。p的地址值必须是malloc当时返回的地址值,不能进行偏移等操作。
作者介绍