C语言指针从入门到精通(含源码示例)
本文适合希望从零系统掌握 C 指针的学习者。
文章将由浅入深,从地址与变量,到函数指针、数组指针、回调函数与 qsort 实现。
一、内存、地址与指针的本质在计算机中,内存就像一栋有门牌号的宿舍楼。每个“房间”(字节)都有编号(地址),CPU 通过地址快速访问数据。
代码语言:javascript复制#include
int main()
{
int a = 10;
printf("a 的地址是:%p\n", &a);
return 0;
}输出类似:
代码语言:javascript复制a 的地址是:006FFD70💡 结论:
地址即内存单元的编号;在 C 语言中,指针是保存地址的变量,地址是指针的值。& 操作符用于取地址,* 操作符用于解引用访问地址中的内容。二、指针变量与类型指针变量是用来存放地址的变量。
代码语言:javascript复制int a = 10;
int *pa = &a; // pa 保存 a 的地址
*pa = 0; // 解引用,修改 a 的值📘 要点:
指针类型决定了解引用时访问的字节数;int * 每次操作 4 字节,char * 每次操作 1 字节;32 位系统下int *指针大小为 4 字节,64 位为 8 字节。三、指针类型的意义代码语言:javascript复制int n = 0x11223344;
int *pi = &n;
char *pc = (char *)&n;
*pi = 0; // 修改全部字节
*pc = 0; // 仅修改最低字节🧭 结论:
指针类型决定了解引用时“访问的步长”;也是指针运算(p+1、p-1)时的跳跃长度依据。四、指针与数组数组名在大多数情况下等价于首元素地址。
代码语言:javascript复制int arr[5] = {1,2,3,4,5};
int *p = arr;
printf("%d %d\n", arr[2], *(p+2)); // 输出相同🔍 例外情况:
sizeof(arr):得到整个数组大小;也就是“整个数组”的字节数。&arr:表示整个数组的地址(与 arr 类型不同,其类型升级成数组指针)。五、指针与函数参数传递当数组作为函数参数传递时,实际上传递的是 首元素的地址。
代码语言:javascript复制void test(int arr[]) // 实际是指针
{
printf("%d\n", sizeof(arr));
} ⚠️ 注意:
在函数内部无法用 sizeof 得到数组总大小:
1、数组名是数组⾸元素的地址;那么在数组传参的时候,传递的是数组名,也就是说本质上数组传参传递的是数组⾸元素的地址。
2、所以函数形参的部分理论上应该使⽤指针变量来接收⾸元素的地址。
3、那么在函数内部我们写sizeof(arr) 计算的是⼀个地址的⼤⼩(单位字节)⽽不是数组的⼤⼩(单位字节)。正是因为函数的参数部分是本质是指针,所以在函数内部是没办法求的数组元素个数的。
六、字符指针的本质代码语言:javascript复制#include
int main()
{
char ch = 'w';
char *pc = &ch;
*pc = 'W';
printf("%c\n", ch);
return 0;
}说明pc 是一个指向 char 类型的指针,存放 ch 的地址。通过 *pc 可修改 ch 的值。字符串常量指针代码语言:javascript复制const char *pstr = "hello bit.";
printf("%s\n", pstr);⚠️ 注意:
pstr 并不是存放字符串本身,而是存放字符串首字符的地址。字符串常量一般在只读存储区,不能被修改。七、数组指针与二维数组传参代码语言:javascript复制int *p1[10]; // 指针数组
int (*p2)[10]; // 数组指针p1 是数组,每个元素是 int*p2 是指针,指向一个含 10 个 int 的数组。 int *p1[10]:
先结合 p1[10] → 这是一个长度 10 的数组,再拿 int * 修饰元素类型 → 元素是指向 int 的指针。
读作:“p1 是含 10 个 int * 的数组”。
int (*p2)[10];
括号强制 *p2 先结合 → p2 是一个指针,再下标 [10] 说明它指向“长度 10 的 int 数组”。
读作:“p2 是指向 int[10] 这一整个数组的指针”。
初始化代码语言:javascript复制int arr[10] = {0};
int (*p)[10] = &arr;二维数组传参代码语言:javascript复制#include
void print2D(int (*p)[5], int r, int c)
{
for(int i = 0; i < r; i++) {
for(int j = 0; j < c; j++) {
printf("%d ", *(*(p+i)+j));
}
printf("\n");
}
}
int main()
{
int arr[3][5] = {{1,2,3,4,5}, {2,3,4,5,6}, {3,4,5,6,7}};
print2D(arr, 3, 5);
}代码语言:javascript复制int (*p)[5] 是“指向含 5 个 int 的一行”的指针,加 1 跳过 20 B;八、函数指针与回调机制代码语言:javascript复制int Add(int x, int y) { return x + y; }
int main()
{
int (*pf)(int, int) = Add;
printf("%d\n", pf(2, 3));
return 0;
}📘 函数名即地址,可直接赋给函数指针。
类型系统:函数指针的声明与初始化写法
类型
含义
int Add(int,int)
函数类型
只在声明/定义处出现
&Add
int (*)(int,int)
显式取地址,得到函数指针
Add
退化后同样为 int (*)(int,int)
隐式退化
因此三条初始化语句完全等价:
代码语言:javascript复制int (*pf)(int, int) = Add; // 最常用
int (*pf)(int, int) = &Add; // 等价
int (*pf)(int, int) = ******Add; // 任意层 * 也等价,因为函数名先退化九、函数指针数组与转移表代码语言:javascript复制#include
int add(int a,int b){return a+b;}
int sub(int a,int b){return a-b;}
int mul(int a,int b){return a*b;}
int divi(int a,int b){return a/b;}
int main()
{
int (*ops[5])(int,int) = {0,add,sub,mul,divi};
int choice, x, y;
while(1){
printf("\n1:add 2:sub 3:mul 4:div 0:exit\nChoose: ");
scanf("%d",&choice);
if(choice==0) break;
printf("输入操作数:");
scanf("%d%d",&x,&y);
printf("结果: %d\n", ops[choice](x,y));
}
}✅ 使用函数指针数组可快速实现函数跳转逻辑,省去冗余的switch。
十、回调函数与 qsort 示例代码语言:javascript复制#include
#include
int cmp_int(const void *a, const void *b)
{
return *(int*)a - *(int*)b;
}
int main()
{
int arr[] = {9,3,7,1,5};
qsort(arr, 5, sizeof(int), cmp_int);
for(int i=0; i<5; i++)
printf("%d ", arr[i]);
}qsort 的最后一个参数是函数指针,由用户提供比较逻辑。
函数地址可作为参数传入另一个函数,由被调用函数在合适时“回调”。
模拟实现 qsort其中主要实现的是qsort函数的最后一个参数,后续结构体排序时也是做好类型转换,写好比较逻辑。
代码语言:javascript复制#include
#include
void swap(void *p1, void *p2, int size)
{
for(int i=0; i char tmp = *((char*)p1+i); *((char*)p1+i) = *((char*)p2+i); *((char*)p2+i) = tmp; } } void bubble(void *base, int count, int size, int(*cmp)(void*,void*)) { for(int i=0;i for(int j=0;j char *p1=(char*)base+j*size; char *p2=(char*)base+(j+1)*size; if(cmp(p1,p2)>0) swap(p1,p2,size); } } int cmp_int(void *a, void *b){ return *(int*)a - *(int*)b; } int main() { int arr[]={4,1,3,9,2}; bubble(arr,5,sizeof(int),cmp_int); for(int i=0;i<5;i++) printf("%d ",arr[i]); }十一、sizeof 与 strlen 区别sizeof strlen 1. sizeof是操作符2. sizeof计算操作数所占内存的⼤⼩,单位是字节3. 不关注内存中存放什么数据 1. strlen是库函数,使⽤需要包含头⽂件 string.h2. srtlen是求字符串⻓度的,统计的是 \0 之前字符的隔个数3. 关注内存中是否有 \0 ,如果没有 \0 ,就会持续往后找,可能会越界 代码语言:javascript复制char arr1[3] = {'a','b','c'}; char arr2[] = "abc"; printf("%d %d\n", sizeof(arr1), strlen(arr2));十二、结语指针是 C 语言的灵魂。掌握指针不仅能写出更高效的程序,也能理解底层内存的运作原理。 建议读者: 理解地址、类型与内存模型;学会用函数指针与回调函数提高代码通用性;使用调试器观察地址变化;尝试自己实现 qsort 与函数回调。模拟标准库函数培养底层思维。