mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5mobile wallpaper 6
3253 字
9 分钟
一篇文章通杀c指针
2026-04-12

一篇文章通杀c指针#

一、引言#

指针对初学者来说,通常是一个难以跨越的大山,但是当你理解本质,学会抽丝剥茧,那么使用起来也会称心如意😎

今天就由我来,带大家吃透c指针。

a

二、人生若只如初见#

1.指针长什么样子?#

int *p;

喏,就是这个样子,这里声明了一个int类型的指针p。

声明一个指针的方式就是类型 *变量名。记住!指针不过也是一种数据类型!

2.指针的作用#

指针是用来存放内存地址的,并且他可以通过解引来操作内存地址里的数据。

int a;//假设a的内存地址是204
int *p;//指针变量p的地址是64
p = &a;
a=5;
printf(p);//204
printf(&a);//204
printf(&p);//64
printf(*p);//5

p里面存放了a的地址,&a的意思是取a的地址,他完全等于p。

这里*的用法新手容易混淆,我来解释一下。

在第二行,我们利用*声明了指针变量p,而在第八行,我们的星号的作用是解引(拿到p里面的地址所存放的值)!

对于指针来说,星号就上面两个作用。

也就是说,我们常常所说的指针就是p,单独一个p!星号只在解引和声明时起作用。

三、指针运算#

对于指针类型他是可以运算的!

int a=10;//假设a地址为2006
int *p;
p=&a;//可以合并成int *p=&a;
printf(p);//2006
printf(p+1);//2010
printf(*p);//10
printf(*(p+1));//6489464116

p+1的地址是2010?为什么?首先p被声明成了一个int类型的指针。

p+1相当于操作内存地址,在原先地址上加了sizeof(int),对于int类型来说也就是4。

这里的*(p+1)打印出来的是一个随机值,有点类似于野指针(未初始化,释放后未滞空,越界的指针),本质上来说,就是指着一块未被初始化的地址,地址里面是随机数。

四、指针类型转换#

什么是指针类型转换,简单一句话说就是强制改变编译器的解读方式。

最常见的情况就是修改通用指针void *类型。

我们来看一段程序。

int a=1025;//假设a的地址是100
int *p;
p=&a;
printf("size of integer is %d bytes",sizeof(int));//4
printf("Address = %d,value=%d\n",p,*p);//Address = 100,value=1025
char *p0;
p0 = (char*)p;
printf("size of char is %d bytes",sizeof(char));//1
printf("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的地址是100
int *p;
p=&a;
printf("size of integer is %d bytes",sizeof(int));//4
printf("Address = %d,value=%d\n",p,*p);//Address = 100,value=1025
printf("Address = %d,value=%d\n",p+1,*(p+1));//Address = 104,value=-256298591851
char *p0;
p0 = (char*)p;
printf("size of char is %d bytes",sizeof(char));//1
printf("Address = %d,value=%d\n",p0,*p0);//Address = 100,value=1
printf("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的地址是100
int *p;
p=&a;
printf("size of integer is %d bytes",sizeof(int));//4
printf("Address = %d,value=%d\n",p,*p);//Address = 100,value=1025
void *p0;
p0 = p;
printf("Address = %d,value=%d\n",p0,*p0);//Address = 100,value=1

这样子写会编译错误!!!💢

void* 不能直接解引用,这是 C 语言的铁律!

六、指向指针的指针#

放轻松,我们来玩个套娃游戏吧。

int x=5;//假设x地址为100
int *p=&x;//假设p地址为200
*p=6;
int **q=&p;//假设q地址为300
int ***r=&q;//假设r地址为400
printf(*p);//6
printf(*q);//200
printf(*(*q));//6
printf(*(*r));//200
printf(*(*(*r)));//6

我们来画个直观的图理解一下

b

指针指向指针就是后一个指针装着前一个指针的地址,如此简单😎

七、函数传值——传引用#

我们知道,在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);//200
printf(*p);//2
printf(p+1);//204

c

我们利用指针,可以获得数组的地址(数组地址通常用首地址表示!),还能操作里面的元素!

语法糖:利用数组名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”字符串就长这样

d

末尾是一个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"会报错!

十、指针和二维数组#

二维数组在内存中长这样。忽略我的格子数,这里没做兼容,无视他!🫡

e

int B[2][3];
int *p=B;//这是不被允许的!是类型错误!因为B是包含三个int类型的一维数组的首地址!
int (*p)[3] = B;//这样才合理!
print B or &B[0]//400
print *B or B[0] or &B[0][0]//400
print B+1 or &B[1] //412
print *(B+1) or B[1] or &B[1[0] //412
print *(B+1)+2 or B[1]+2 or &B[1][2] //420
print *(*B+1) //输出一维数组里的第二个元素,也就是404地址存储的元素

int B[2][3];声明了两个含有三个int类型数据的一维数组!

数组名B依旧返回首地址,相当于B[0]。

这里B[i][j]=*B(B[i]+j)=*(*(B+i)+j)

十一、指针和三维数组#

三维数组在内存中长这样,在下面的代码与图片中是一个包含三个二维数组的一维数组(内存里的样子哦!)。

f

int C[3][2][2];
int (*p)[2][2] = C;
print c;//800
print *c or c[0] or &c[0][0] //800
print *(c[0][1]+1) or C[0][1][1]//9
print *(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接受,这是一个好习惯!

这里的修改类型语法就是括号加类型加星号哦!

我们不能直接操作堆里面的数据,但是可以通过指针来利用内存地址和解引来操作修改数据!

g

注意,这里的指针p在栈里面哦!

释放堆里面的内存利用free即可。

十四、内存分配#

这里可以使用的函数有mallocrealloccalloc。使用频率最高的肯定是malloc,另外两个各有特点。

函数原型功能是否初始化典型用途
mallocvoid *malloc(size_t size);分配 size 字节的未初始化内存❌ 不初始化(内容随机)通用动态分配
callocvoid *calloc(size_t nmemb, size_t size);分配 nmemb × size 字节内存,并全部初始化为 0✅ 初始化为 0需要清零的场景(如数组、结构体)
reallocvoid *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,world
15668491561

为什么*ptr的值是随机数?

我们来看看栈里面发生了什么?

h

这里分别入栈了两个函数,一切看上去都很好!

但是当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 后面 → 需要交换 → 返回 1
int 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整个数组的地址(如 0x1000int (*)[5]
*p整个数组(等价于 arrint [5]
(*p)[i]数组第 i 个元素的值int
p + 1下一个「5元素int数组」的地址int (*)[5]
分享

如果这篇文章对你有帮助,欢迎分享给更多人!

一篇文章通杀c指针
https://fatdog.20060113.xyz/posts/c-pointer/
作者
神秘大胖狗
发布于
2026-04-12
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

封面
Sample Song
Sample Artist
封面
Sample Song
Sample Artist
0:00 / 0:00