C语言指针到底是个什么东西
指针,恐怕是C语言中最让人又爱又恨的东西。有人觉得它很难懂,但是不得不说,它真的很好用!!!。
指针是什么?—— 本质是地址
你可以把指针理解为一种特殊的变量类型——没错,int*
、char*
本质上都是指针类型。它们的核心功能只有一个:存储内存地址。
先看个最基础的例子:
1 2 3 4 5 6 7 8 9 10
| #include <stdio.h> int main(int argc, char const *argv[]) { int a = 123; int* pointer = &a; printf("a的值: %d\n", a); printf("pointer存储的地址: %p\n", pointer); printf("pointer指向的内容: %d\n", *pointer); return 0; }
|
运行结果显而易见:
1 2 3
| a的值: 123 pointer存储的地址: 0x7ffff91d4d2c // 每次运行可能不同 pointer指向的内容: 123
|
这里的*pointer
就是”解引用”——通过指针存储的地址,找到并取出该地址中存放的数据。
指针的”无类型”本质
上面的例子太常规了。要体会到真正的指针,要从”抛开类型限制”开始。
看这段略有不同的代码:
1 2 3 4 5 6 7 8 9 10
| #include <stdio.h> int main(int argc, char const *argv[]) { int a = 123; char* pointer = (char*)&a; printf("a的值: %d\n", a); printf("pointer存储的地址: %p\n", pointer); printf("pointer指向的内容: %d\n", *pointer); return 0; }
|
运行结果和之前几乎一样:
1 2 3
| a的值: 123 pointer存储的地址: 0x7ffff842f12c // 地址随机分配,和上次不同很正常 pointer指向的内容: 123
|
这说明什么?无论指针声明为int*
还是char*
,它本质上都只是存储一个地址。类型只是告诉编译器:”我该如何解读这个地址里的数据”。
如果我们用%c
输出*pointer
(把123当ASCII码解析):
因为{
的ASCII码正好是123——指针存储的地址没变,只是解读方式变了。
类型影响什么?—— 指针的偏移量
既然类型不影响指针存储的地址,那不同类型的指针有什么区别?答案是:加减运算时的偏移量。
char*
指针:加减1,偏移1个字节(因为char
占1字节)
int*
指针:加减1,偏移4个字节(在32位环境下,int
通常占4字节)
看这个例子就明白了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| #include <stdio.h> int main(int argc, char const *argv[]) { char s[8] = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'}; int* pointer = (int*)s; while (*pointer) { printf("%c\n", *pointer); pointer++; } return 0; }
|
运行结果:
1 2
| a // 指向s[0] e // 偏移4字节后指向s[4]
|
完全符合预期——int*
指针每次+1跳过4个字节,正好从第一个字符”a”跳到第五个字符”e”。
函数也能被指针指?
C语言中,函数名和数组名一样,本质上都是地址。既然是地址,那普通指针能不能指向函数?
先看个对比:
1 2 3 4 5 6 7 8 9 10 11
| void function(){} #include <stdio.h> int main(int argc, char const *argv[]) { void (*fp)() = function; int *p = (int *)function; printf("函数指针fp的值: %p\n", fp); printf("普通指针p的值: %p\n", p); return 0; }
|
运行结果:
1 2
| 函数指针fp的值: 0x7f3722b95149 普通指针p的值: 0x7f3722b95149
|
两个指针存储的地址完全相同!这说明:任何指针类型都能存储函数的地址。
但别急着高兴,我们来个”骚操作”:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| #include <stdio.h> void function() { printf("Hello Pointer\n"); } int main(int argc, char const *argv[]) { function(); int *p = (int *)function; *p = 0; function(); return 0 ; }
|
运行结果:
1 2
| Hello Pointer [1] 1127 segmentation fault (core dumped) ./demo
|
程序崩溃了!为什么?
用调试工具查看会发现:函数的机器码存放在只读内存区域(代码段),当我们试图用*p = 0
修改时,会触发内存保护机制,导致段错误。这也从侧面证明:指针确实指向了函数的真实地址,我们没办法强制进行修改。
指针与数据的”部分解读”
指针的类型不仅影响偏移,还决定了”一次读多少字节”。比如short
占2字节,int
占4字节,当用short*
指向int
变量时,会只读取低2字节。
看这个例子:
1 2 3 4 5 6 7 8 9 10 11
| #include <stdio.h> int main(int argc, char const *argv[]) { int a = 11 , b = 111111; short* p = (short*)&a; printf("a的低2字节: %d\n", *p); p = (short*)&b; printf("b的低2字节: %d\n", *p); return 0 ; }
|
运行结果:
1 2
| a的低2字节: 11 b的低2字节: -19961
|
为什么b
的结果是-19961?这涉及到二进制存储和补码规则。111111
的二进制超过了2字节(16位),short*
只读取低16位,而这16位恰好是-19961的补码(感兴趣的话可以自己换算验证)。
用指针”解剖”内存:位域实验
我们可以用指针配合位域,更直观地看内存存储:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| #include <stdio.h>
struct num{ short a:1,b:1,c:1,d:1,e:1,f:1,g:1,h:1, i:1,j:1,k:1,l:1,m:1,n:1,o:1,p:1; }; int main(int argc, char const *argv[]) { struct num a; a.a=a.b=a.c=a.d=a.e=a.f=a.g=a.h=1; a.i=a.j=a.k=a.l=a.m=a.n=a.o=1; a.p=0; short* sp = (short*)&a; printf("16位解读: %d\n", *sp); char* cp = (char*)&a; printf("低8位解读: %d\n", *cp); return 0 ; }
|
运行结果:
指针类型决定了内存的解读方式——同样的二进制,用16位读是32767,用8位读(且视为有符号数)就是-1。
指针的”手动分配”技巧
理解了指针的本质,我们甚至可以手动指定变量的存储区域。比如给结构体”分配”栈上的缓冲区:
1 2 3 4 5 6 7 8 9 10
| #include <stdio.h> struct a{}; int main(int argc, char const *argv[]) { char buffer[128]; struct a* sa = (struct a*)buffer; return 0; }
|
这种技巧在嵌入式开发或需要精确控制内存的场景中很常用。
总结:指针的核心是”瞎搞”的自由
C语言指针的魅力,在于它打破了很多”规则”:
- 类型不限制存储的地址,只影响解读方式
- 任何指针都能指向任何地址(函数、变量、数组…)
- 可以手动控制内存的读写范围和方式
当然,这种自由也伴随着风险,但是不得不说这玩意儿确实很好玩。