C语言指针到底是个什么东西

未分类
1.8k 词

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; // 取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; // 这里指针类型变成了char*
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码解析):

1
*pointer对应的字符: {

因为{的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[])
{
// 字符数组:连续存储8个字节
char s[8] = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'};
int* pointer = (int*)s; // 用int*指针指向数组首地址

while (*pointer) // 简单判断(实际应避免用0作为结束符的情况)
{
printf("%c\n", *pointer); // 解读4字节中的第一个字符
pointer++; // 偏移4个字节
}
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; // 用int*指针指向函数

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; // 用int*指向函数
*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); // 11在short范围内,正常输出

p = (short*)&b;
printf("b的低2字节: %d\n", *p); // 111111超出short范围,结果"奇怪"
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>
// 定义一个16位的位域结构(模拟short)
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; // 16个1位字段
};
int main(int argc, char const *argv[])
{
struct num a;
// 前15位设为1,最后1位设为0
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; // 用short*读16位
printf("16位解读: %d\n", *sp); // 0111111111111111 → 32767

char* cp = (char*)&a; // 用char*读低8位
printf("低8位解读: %d\n", *cp); // 11111111 → -1(补码规则)
return 0 ;
}

运行结果:

1
2
16位解读: 32767
低8位解读: -1

指针类型决定了内存的解读方式——同样的二进制,用16位读是32767,用8位读(且视为有符号数)就是-1。

指针的”手动分配”技巧

理解了指针的本质,我们甚至可以手动指定变量的存储区域。比如给结构体”分配”栈上的缓冲区:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
struct a{}; // 空结构体(大小为0,但可以作为指针类型)
int main(int argc, char const *argv[])
{
char buffer[128]; // 栈上的128字节缓冲区
struct a* sa = (struct a*)buffer; // 让结构体指针指向缓冲区

// 之后操作sa,就相当于操作buffer中的内存
return 0;
}

这种技巧在嵌入式开发或需要精确控制内存的场景中很常用。

总结:指针的核心是”瞎搞”的自由

C语言指针的魅力,在于它打破了很多”规则”:

  • 类型不限制存储的地址,只影响解读方式
  • 任何指针都能指向任何地址(函数、变量、数组…)
  • 可以手动控制内存的读写范围和方式

当然,这种自由也伴随着风险,但是不得不说这玩意儿确实很好玩。