一篇文章通杀c指针
一、引言
指针对初学者来说,通常是一个难以跨越的大山,但是当你理解本质,学会抽丝剥茧,那么使用起来也会称心如意😎
今天就由我来,带大家吃透c指针。

二、人生若只如初见
1.指针长什么样子?
int *p;喏,就是这个样子,这里声明了一个int类型的指针p。
声明一个指针的方式就是类型 *变量名。记住!指针不过也是一种数据类型!
2.指针的作用
指针是用来存放内存地址的,并且他可以通过解引来操作内存地址里的数据。
int a;//假设a的内存地址是204int *p;//指针变量p的地址是64p = &a;a=5;printf(p);//204printf(&a);//204printf(&p);//64printf(*p);//5p里面存放了a的地址,&a的意思是取a的地址,他完全等于p。
这里*的用法新手容易混淆,我来解释一下。
在第二行,我们利用*声明了指针变量p,而在第八行,我们的星号的作用是解引(拿到p里面的地址所存放的值)!
对于指针来说,星号就上面两个作用。
也就是说,我们常常所说的指针就是p,单独一个p!星号只在解引和声明时起作用。
三、指针运算
对于指针类型他是可以运算的!
int a=10;//假设a地址为2006int *p;p=&a;//可以合并成int *p=&a;printf(p);//2006printf(p+1);//2010printf(*p);//10printf(*(p+1));//6489464116p+1的地址是2010?为什么?首先p被声明成了一个int类型的指针。
p+1相当于操作内存地址,在原先地址上加了sizeof(int),对于int类型来说也就是4。
这里的*(p+1)打印出来的是一个随机值,有点类似于野指针(未初始化,释放后未滞空,越界的指针),本质上来说,就是指着一块未被初始化的地址,地址里面是随机数。
四、指针类型转换
什么是指针类型转换,简单一句话说就是强制改变编译器的解读方式。
最常见的情况就是修改通用指针void *类型。
我们来看一段程序。
int a=1025;//假设a的地址是100int *p;p=&a;printf("size of integer is %d bytes",sizeof(int));//4printf("Address = %d,value=%d\n",p,*p);//Address = 100,value=1025char *p0;p0 = (char*)p;printf("size of char is %d bytes",sizeof(char));//1printf("Address = %d,value=%d\n",p0,*p0);//Address = 100,value=1第七行,p0被初始化,相当于把p转换成char类型的指针然后赋值给p0。
所以在第八行我们仍然可以看到p0的地址是100,因为他和p一样装着a的地址。
但是value为什么是1呢??难道对他解引不是a的值吗?你可能会这样想。
实际上,他里面装的确实还是a的值!但是因为指针类型的转换,导致编译器读取方式不同,我们来写出1025的32位的二进制表达。
也就是1025=00000000 00000000 00000100 00000001
不熟悉的老铁可以补一下二进制的知识,这里总共是32个数字,每8个为一组(对应一个byte字节数)。
我们都知道,int类型占4个字节,char类型占一个字节。
p0是p从int类型转换为char类型,那么编译器就会从原来读取4字节(也就是上面完整32个数字)变成读取1字节(8个数字),而00000001代表2**0也就是1,所以会输出1。
既然转换了类型,那原来的p会被转换吗?
一嘴吻醒你💢:类型转换只是“换个角度看同一个地址”,原指针 p 完全不受影响!它还是那个正经的 int*,稳如老狗!
我们来修改一下,让你再理解理解
int a=1025;//假设a的地址是100int *p;p=&a;printf("size of integer is %d bytes",sizeof(int));//4printf("Address = %d,value=%d\n",p,*p);//Address = 100,value=1025printf("Address = %d,value=%d\n",p+1,*(p+1));//Address = 104,value=-256298591851char *p0;p0 = (char*)p;printf("size of char is %d bytes",sizeof(char));//1printf("Address = %d,value=%d\n",p0,*p0);//Address = 100,value=1printf("Address = %d,value=%d\n",p0+1,*(p0+1));//Address = 101,value=4新增了第六行和第11行,第六行在三、指针运算说过,再次不做赘述
p0+1操作内存地址,属于指针运算,而p0是char类型,对应1字节,所以地址只加了一位。
而编译器读取char类型是1字节,所以这次会读到00000100也就是4。
五、通用指针
✅ void* = 通用指针(能存任何地址) ✅ 不能直接解引用/算术运算(必须强转) ✅ 核心用途:泛型函数、动态内存分配、跨类型传参
缺点是自己不能直接用!
int a=1025;//假设a的地址是100int *p;p=&a;printf("size of integer is %d bytes",sizeof(int));//4printf("Address = %d,value=%d\n",p,*p);//Address = 100,value=1025void *p0;p0 = p;printf("Address = %d,value=%d\n",p0,*p0);//Address = 100,value=1这样子写会编译错误!!!💢
void* 不能直接解引用,这是 C 语言的铁律!
六、指向指针的指针
放轻松,我们来玩个套娃游戏吧。
int x=5;//假设x地址为100int *p=&x;//假设p地址为200*p=6;int **q=&p;//假设q地址为300int ***r=&q;//假设r地址为400printf(*p);//6printf(*q);//200printf(*(*q));//6printf(*(*r));//200printf(*(*(*r)));//6我们来画个直观的图理解一下

指针指向指针就是后一个指针装着前一个指针的地址,如此简单😎
七、函数传值——传引用
我们知道,在c语言中,像一个函数传变量,他只是做到值传递,拷贝了原变量的一个副本,做不到修改变量,那么如何传入函数并对变量进行修改呢?
答案是传引用,栗子如下
void Increment(int *p){ *p = (*p) + 1;}int main(){ int a=10; Increment(&a); printf(a);//输出11}八、指针和一维数组
int A[5];int *p;p = &A[0];//可以直接写成p=A;printf(p);//200printf(*p);//2printf(p+1);//204
我们利用指针,可以获得数组的地址(数组地址通常用首地址表示!),还能操作里面的元素!
语法糖:利用数组名A,可以直接拿到数组的首地址(这是个常量指针!)。
利用这个特性,我们可以得到:
- address——
&A[i]=(A+i) - value——
A[i]=*(A+i)
他们完全等价!
但是切记!这个特性不能做指针运算!
A++;//这是错误的!int *p = A;p++;//这个合理!在编译器视角下的数组,如果作为参数被传到函数里,会变成指针的形式。
int sumofarray(int A[]){//会被数组解析成int *A int size=sizeof(A)/sizeof(A[0]); int sum = 0; for(int i=0;i<size;i++){ sum+=A[i]; } return sum;}int main(){ A={1,2,3,4,5}; int total=sumofarray(A); printf(total);}猜猜结果是什么??
结果是1!
为什么会这样!
答案其实已经写出来了,int A[]会被数组解析成int *A。也就是说,在sumofarray函数的栈帧里,收到的参数是A的首地址!所以求出来的size根本不是数组的大小!
如何避免这种情况?只能通过在main函数里求出size再传入到sumofarray函数里了!
九、字符数组与指针
c里面的字符串本质就是一个数组。
比如“JOHN”字符串就长这样

末尾是一个NULL也就是\0,用于标志这一串字符串的结束,如果没有他,你可以想象一下如果再写一个字符串,刚好内存被分配到4这个地址会怎么样👌
我们依然可以利用索引解出元素
char C[10];c[0]= "J";巴拉巴拉,懒得写了表示字符数组的方式如下
char C[20] = "hello";在栈(stack)上分配一个长度为 20 的字符数组。编译时或运行时(取决于上下文)分配空间。字符串字面量 "hello" 被复制到这个数组中。数组内容是可修改的。
char *C="hello";定义一个指向字符串字面量的指针。"hello" 存储在只读数据段(如 .rodata) 中。C 指向该只读内存地址。不能修改指向的内容(否则未定义行为,通常 crash)。
所以我们修改C[0]="A"会报错!十、指针和二维数组
二维数组在内存中长这样。忽略我的格子数,这里没做兼容,无视他!🫡

int B[2][3];int *p=B;//这是不被允许的!是类型错误!因为B是包含三个int类型的一维数组的首地址!int (*p)[3] = B;//这样才合理!print B or &B[0]//400print *B or B[0] or &B[0][0]//400print B+1 or &B[1] //412print *(B+1) or B[1] or &B[1[0] //412print *(B+1)+2 or B[1]+2 or &B[1][2] //420print *(*B+1) //输出一维数组里的第二个元素,也就是404地址存储的元素int B[2][3];声明了两个含有三个int类型数据的一维数组!
数组名B依旧返回首地址,相当于B[0]。
这里B[i][j]=*B(B[i]+j)=*(*(B+i)+j)
十一、指针和三维数组
三维数组在内存中长这样,在下面的代码与图片中是一个包含三个二维数组的一维数组(内存里的样子哦!)。

int C[3][2][2];int (*p)[2][2] = C;print c;//800print *c or c[0] or &c[0][0] //800print *(c[0][1]+1) or C[0][1][1]//9print *(c[1]+1) or c[1][1] or &c[1][1][0]//824十二、多维数组作为参数
void func(int A[][3]){//传入二维数组,包含的一维数组必须有三个int类型,前面可以不用传任何参数//其实会被编译器解释成 int (*A)[3]}int main(){ int C[3][2][2]={ {{2,5},{7,9}}, {{3,4},{6,1}}, {{0,8},{11,13}} }; int A={1,2}; int B[2][3] = {{2,4,6},{5,8,7}}; int x[2][4]; func(x)//传入x会报错!4超出了一维数组边界}要传三维数组,就得这么写参数
void func(A[][2][3])//或者(*A)[2][3]十三、指针和动态内存
int total;int square(int x){ return x*x;}int sqofsum(int x,int y){ int z = square(x+y); return z;}int main(){ int a= 4,b=8; total = sqofsum(a,b); printf(total);}上面的栗子是函数调用栈
在操作系统的内存分区的栈里面,依次压入main,sqofsum,square函数。
当函数执行完成之后,操作系统就会回收栈里面使用的函数以及变量,这就意味着,这三个函数里面所有的变量和操作都会被操作系统自动回收。但是这并不意味着栈不会爆满,他虽然是动态增长的,但是也有极限!超过就会栈溢出!
我们再来看看堆——heap
他就不会被自动回收了!堆里面装的是是你通过malloc或者cpp里的new出来的内存地址,这需要你手动回收!要不然也会内存泄漏!
在堆中申请的内存通常会持久存在,除非你自己释放掉。
int *p;p=(int *)malloc(sizeof(int));//在堆里面申请了一块内存用于存放int类型数据!*p=10;free(p);malloc函数会返回一个void *类型的通用指针,但是我们需要用(int *)来做类型转换来让p接受,这是一个好习惯!
这里的修改类型语法就是括号加类型加星号哦!
我们不能直接操作堆里面的数据,但是可以通过指针来利用内存地址和解引来操作修改数据!

注意,这里的指针p在栈里面哦!
释放堆里面的内存利用free即可。
十四、内存分配
这里可以使用的函数有malloc,realloc,calloc。使用频率最高的肯定是malloc,另外两个各有特点。
| 函数 | 原型 | 功能 | 是否初始化 | 典型用途 |
|---|---|---|---|---|
malloc | void *malloc(size_t size); | 分配 size 字节的未初始化内存 | ❌ 不初始化(内容随机) | 通用动态分配 |
calloc | void *calloc(size_t nmemb, size_t size); | 分配 nmemb × size 字节内存,并全部初始化为 0 | ✅ 初始化为 0 | 需要清零的场景(如数组、结构体) |
realloc | void *realloc(void *ptr, size_t new_size); | 调整已有内存块大小(可扩大或缩小) | 扩大后的新部分不初始化 | 动态扩容(如动态数组) |
十五、函数返回指针
void print(){ printf("hello,world");}int *Add(int *a,int *b){ int c= (*a) + (*b); return &c;}int main(){ int a=2,b=4; int *ptr=Add(&a,&b); print(); printf(*ptr);}猜猜会输什么?
hello,world15668491561为什么*ptr的值是随机数?
我们来看看栈里面发生了什么?

这里分别入栈了两个函数,一切看上去都很好!
但是当print函数入栈时,他就可能会一屁股坐在Add函数的地址上,将原先的数据覆盖掉。
原理就是当Add函数执行完成之后,他的栈帧会被操作系统回收销毁,并被标记为可用,随时可能被后续代码覆盖!
这也就意味着,ptr保存了一个失效的地址,此时成了一个野指针!
有时候会正常输出结果的原因是,print函数没有占用原先add函数的地址,而是利用了其他地址,导致数据仍然看似正常。
解决方法就是利用malloc将Add里的c挂到堆上面!
十六、函数指针
int Add(int a,int b){ return a+b;}int main(){ int c; int (*p)(int ,int); p=&Add; c=(*p)(2,3)//调用函数add printf(c);}这里利用了指针p来存储函数地址,注意p的声明方式!
c其实给了语法糖,第七行可以直接用函数名p=Add,函数名会直接返回地址
同时第八行也可以不解引用,直接c=p(2,3)这也是语法糖!
十七、函数指针使用案例——回调函数
它的核心思想是:把一个函数的地址(即函数指针)作为参数传递给另一个函数,让后者在“合适的时候”调用前者。
标准库里的qsort就是很经典的栗子
#include <stdio.h>#include <stdlib.h>
// 比较函数(回调函数)int compare(const void *a, const void *b) { return (*(int*)a - *(int*)b); // 升序}
int main() { int arr[] = {5, 2, 9, 1, 5}; int n = sizeof(arr) / sizeof(arr[0]);
// qsort 接收 compare 函数指针,在内部调用它来比较元素 qsort(arr, n, sizeof(int), compare);
for (int i = 0; i < n; i++) { printf("%d ", arr[i]); // 输出: 1 2 5 5 9 } return 0;}再来个栗子吧,让ai写个冒泡排序的栗子,我已经燃尽了😭
#include <stdio.h>#include <stdlib.h> // for abs()
// 定义回调函数类型:接收两个 int,返回 int// 返回 1 表示需要交换,0 表示不需要typedef int (*CompareCallback)(int a, int b);
// 冒泡排序函数,接受回调函数作为比较规则void bubble_sort_with_callback(int arr[], int n, CompareCallback should_swap) { for (int i = 0; i < n - 1; i++) { for (int j = 0; j < n - 1 - i; j++) { // 调用回调函数判断是否需要交换 if (should_swap(arr[j], arr[j + 1])) { // 交换 int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } }}
// 回调函数:按绝对值升序排列// 如果 |a| > |b|,说明 a 应该在 b 后面 → 需要交换 → 返回 1int compare_abs(int a, int b) { return abs(a) > abs(b); // C 中 true 是 1,false 是 0}
// 辅助函数:打印数组void print_array(int arr[], int n) { for (int i = 0; i < n; i++) { printf("%d ", arr[i]); } printf("\n");}
// 主函数int main() { int arr[] = {-5, 3, -1, 8, -2, 0, 4}; int n = sizeof(arr) / sizeof(arr[0]);
printf("原始数组: "); print_array(arr, n);
// 调用带回调的冒泡排序 bubble_sort_with_callback(arr, n, compare_abs);
printf("按绝对值升序排序后: "); print_array(arr, n);
return 0;}彻底燃尽了!
十八、指针数组和数组指针
这个漏了,来补一下😋
指针数组 = 一个数组,里面每个元素都是「指针」! 这些指针可以指向 任意类型的数据(整数、字符、结构体、甚至其他数组!) 本质:数组的每个格子存的是 内存地址(不是数据本身!)
int a = 10, b = 20, c = 30;int *ptr_arr[3] = {&a, &b, &c}; // 指针数组,存了3个int指针ptr_arr[0] 存的是 a 的地址(比如 0x1000)ptr_arr[1] 存的是 b 的地址(比如 0x1004)ptr_arr[2] 存的是 c 的地址(比如 0x1008)用数组名和索引会拿到什么?
| 操作 | 结果 | 类型 |
|---|---|---|
ptr_arr | 数组首元素的地址(即 &ptr_arr[0]) | int(二级指针) |
ptr_arr[i] | 第 i 个指针的值(即某个变量的地址) | int*(一级指针) |
*ptr_arr[i] | 第 i 个指针指向的变量的值 | int(实际数据) |
区别
指针数组 vs 数组指针
- 指针数组:int arr[10] → 数组,含10个int指针
- 数组指针:int (*arr)[10] → 指针,指向一个含10个int的数组
int arr[5] = {1, 2, 3, 4, 5};int (*p)[5] = &arr; // p 是数组指针,指向整个 arr 数组给你一个表放在这里,希望你能熬过这个痛苦学习过程😋
| 操作 | 结果 | 类型 |
|---|---|---|
p | 整个数组的地址(如 0x1000) | int (*)[5] |
*p | 整个数组(等价于 arr) | int [5] |
(*p)[i] | 数组第 i 个元素的值 | int |
p + 1 | 下一个「5元素int数组」的地址 | int (*)[5] |
部分信息可能已经过时









