C/C++指针核心知识总结:从内存地址到安全传参
一、先建立一个核心模型:指针就是地址值的载体
计算机内存通常按字节编址,每个字节单元都有唯一编号,这个编号就是地址。C 语言中的指针变量,就是专门保存地址的变量。
&用于取地址,*用于解引用:通过指针保存的地址找到目标对象。
#include<stdio.h>intmain(void){inta=10;int*p=&a;printf("a = %d\n",a);printf("address = %p\n",(void*)&a);printf("through pointer = %d\n",*p);*p=20;printf("a = %d\n",a);return0;}注意:打印地址应使用%p,并将地址转换为void *。地址的具体数值由平台和运行时决定,不应写死。
二、指针大小和指针类型不是一回事
在常见平台上,32 位程序的指针通常占 4 字节,64 位程序的指针通常占 8 字节。同一平台上,char *、int *、double *通常等宽,但指针类型依然非常重要:它决定编译器如何解释目标内存。
1. 指针类型决定解引用尺度
char *解引用通常访问 1 个字节,int *解引用则按int的大小访问。也就是说,类型决定了一次读写的“权限范围”。
2. 指针加减按元素步进
p + 1不是地址数值简单加 1,而是向后移动一个所指向元素的大小:
char *通常前进 1 字节;int *通常前进sizeof(int)字节;double *通常前进sizeof(double)字节。
数组连续存储,因此可以用指针遍历数组:
intarr[]={1,2,3,4,5};intsize=(int)(sizeof(arr)/sizeof(arr[0]));for(int*p=arr;p<arr+size;++p){printf("%d ",*p);}两个指针相减表示它们之间相差多少个元素,通常用于计算数组区间长度。arr + size是尾后指针,可以比较,不能解引用。
三、void *:通用地址,但不是万能指针
void *可以接收任意对象类型的地址,常用于通用接口。在 C 中它不能直接解引用,也不能直接按元素进行指针运算,使用前要转换回正确的类型。
voidprint_int(constvoid*data){constint*p=(constint*)data;printf("%d\n",*p);}在 C++ 中,建议使用更明确的类型设计和显式转换,不要把void *当作类型安全的替代品。
四、const修饰指针:左管内容,右管指针
| 声明 | 约束对象 | 允许的操作 |
|---|---|---|
int *p | 无 const 限制 | 可改*p,可改p |
const int *p | 指向的内容 | 不可通过p改内容,可改p的指向 |
int *const p | 指针变量本身 | 可改内容,不可改p的指向 |
const int *const p | 内容和指针 | 两者都不能改 |
阅读技巧:先找到变量名,再向两侧看。const在*左边,约束目标内容;在*右边,约束指针变量。
不要通过错误的指针类型去修改const对象。修改 const 对象会产生未定义行为,不能把“编译器暂时没报错”当成正确性证明。
五、野指针:最常见的三个来源
- 局部指针未初始化:
int *p;中的p可能是随机值。 - 指针越界:走出数组或动态内存的有效边界后还继续读写。
- 指向的空间已经失效:例如返回局部变量地址,函数结束后局部对象的生命周期已经结束。
int*bad_function(void){intlocal=100;return&local;// 错误:返回失效的局部地址}安全习惯:能确定指向哪里就立即初始化为地址;不知道目标时初始化为NULL;使用前检查有效性;不再使用时可置空;严格控制数组边界和对象生命周期。C++ 代码优先使用nullptr。
六、assert:把“必须成立”的条件写出来
assert定义在assert.h中。条件为真时继续,条件为假时输出错误信息并终止程序。它适合在 Debug 版本中捕捉程序员错误。
#include<assert.h>#include<stddef.h>size_tmy_strlen(constchar*str){assert(str!=NULL);size_tcount=0;while(*str!='\0'){++count;++str;}returncount;}定义NDEBUG后,assert通常会被禁用。因此不要把有副作用的业务逻辑放进 assert 表达式。
七、传值调用与传址调用
C 的参数传递是传值。下面的函数只交换了形参副本:
voidswap_wrong(intx,inty){inttmp=x;x=y;y=tmp;}如果函数需要修改调用者中的变量,就传入地址:
voidswap_int(int*px,int*py){inttmp=*px;*px=*py;*py=tmp;}intmain(void){inta=3;intb=5;swap_int(&a,&b);}传值适合只依赖输入值的计算;传址适合需要修改调用者对象或避免复制较大对象的场景。
八、最后的四问自检法
每次看到*p,先问:
p是否初始化?p是否为NULL/nullptr?p指向的对象是否仍在有效生命周期内?- 指针是否落在合法边界内,并且目标空间足够大?
把这四个问题变成习惯,指针代码的稳定性会明显提高。
编译建议
GCC 可以开启警告和调试信息:
gcc-Wall-Wextra-gpointer_demo.c-opointer_demo警告不是错误的替代品,但它能尽早暴露类型不匹配、未使用变量和可疑表达式。