C 傻鸟指针

C 傻鸟指针

Lucas Lv4

指针的声明

C语言的指针语法(int *p;*p)在编程语言历史上一直备受争议,被称为“反人类设计”之一。很多人都觉得它容易混淆。 (其中int *p; 是声明指针,*p是取指针指向的对象),如下:

1
int *p;

这个声明其实特别有考究!!!

想想,如果你要声明一个int变量,该怎么写?这样:int a;

一级指针.png

其实指针的声明是完全与之对应的
它声明了p的类型是int *
还声明了*p的类型是int

提示

int *p;int* p;都正确。
p的类型是int *int*也都正确。

然后在调用指针的时候,也就非常清晰了*p是指针指向的对象(在声明时其实也是如此),p是指针(声明时候也是如此)。

这样逻辑就完整了。

二级指针

二级指针.png

这个不仅声明了**p的类型是int
还声明了*p的类型是int *
又声明了p的类型是int **

指针是操作内存的工具

1. 指针也在内存中,指针也有内存地址

我们先打印一下指针的一些信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

int main() {
int a = 10;
int *a_pointer = &a;
int a_size = sizeof(a);
int a_pointer_size = sizeof(a_pointer);
printf("a是%d\n", a);
printf("a在%p\n", &a);
printf("a占%d\n", a_size);
printf("a_pointer是%p\n", a_pointer);
printf("a_pointer在%p\n", &a_pointer);
printf("a_pointer占%d\n", a_pointer_size);
}
1
2
3
4
5
6
a是10
a在0x16fbfaf3c
a占4
a_pointer是0x16fbfaf3c
a_pointer在0x16fbfaf30
a_pointer占8
pointer.png

如图,你看这个 指针 方框更大,也代表着占内存更大,而int只占4字节,这是有些反常识的

记住这个图,每次难以理解指针的时候,就看看这个图

2. 指针的类型及其初始化

首先,一个int *n;,表明了这个指针类型是int *

所以,通用的指针类型就是dataType *

1
2
int a = 10;
int *p = &a;

这里p的类型是int*而不是int

指针的赋值,就是对象的地址

注意这里的&不是引用

引用是 C++ 里的概念,C语言里是没有的

3. 指针通过->调用内部成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <stdlib.h>

typedef struct {
char *name;
} Person;

int main() {
Person *p = (Person *) malloc(sizeof(Person));

if (p == NULL) {
return 1;
}

p->name = "C Language";
printf("Name: %s\n", p->name);
free(p);

return 0;
}

这里这个->其实是一个形象的小语法糖,但是并不像现在很多“先进”的语言的语法糖那么抽象,反而非常直观。

这里p->name(*p).name是等价的。
注意!这里的()不能省略,因为.的优先级要比*更高。

另外,这里的namechar*类型,那为什么没有像一开始我们打印指针内的地址一样打印出来name的存放的地址呢?

因为这里我们在调用printf的时候,填写了%s的格式说明符,这个格式说明符会自动解引用指针类型并循环读出里面的字符,直到遇见结束符\0

4. 作为参数传递

4.1 参数是一级指针

特别重要的提示

对于C语言,函数的参数传递只有值传递
也就是说,所有的参数都只会在传入函数的时候,拷贝一份传到函数内部
不存在Java、C++等语言的引用传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>
#include <stdlib.h>

typedef struct {
char *name;
} Person;

void print_p(Person *p) {
printf("函数内部指针存放的地址:%p\n", p);
printf("函数内部指针的地址:%p\n", &p);
printf("函数内部指针访问的内部对象的地址:%p\n", p->name);
}

int main() {
Person *person_p = (Person *) malloc(sizeof(Person));

if (person_p == NULL) {
return 1;
}

person_p->name = "C Language";

printf("指针存放的地址:%p\n", person_p);
printf("指针的地址:%p\n", &person_p);
printf("指针访问的内部对象的地址:%p\n\n", person_p->name);

print_p(person_p);

free(person_p);

return 0;
}

我们来看输出结果

1
2
3
4
5
6
7
指针存放的地址:0x600003b98030
指针的地址:0x16d516f30
指针访问的内部对象的地址:0x1028e8a15

函数内部指针存放的地址:0x600003b98030
函数内部指针的地址:0x16d516f08
函数内部指针访问的内部对象的地址:0x1028e8a15

我知道有的技术文章讲的天花乱坠,好像很复杂一样
但其实,参数传指针就是在函数内部有一个一样的指针,指向传入参数的所指的对象
意思就是,外面的person_p和函数内的p都指向同一个对象(同一块内存)
自然操作结果会好像被带出来了

4.2 参数是二级指针

为什么要传入二级指针呢?
我们观察刚才的代码,发现是先给指针分配了指向的内存空间,然后再将指针传入,将所需数据写入内存空间

那如果我不知道需要多少内存空间呢?
比如一个字符串。此时就需要二级指针,我们看看实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void copyStringToDynamicMemory(char **str_ptr, char *content) {
unsigned long len = strlen(content) + 1;
*str_ptr = (char *) malloc(sizeof(char) * len);
if (*str_ptr == NULL) {
fprintf(stderr, "Memory allocation failed.\n");
return;
}
strcpy(*str_ptr, content);
}

int main() {
char *my_string = NULL;
copyStringToDynamicMemory(&my_string,"aaaaaaaaaaaaa");

if (my_string != NULL) {
printf("%s\n", my_string);
free(my_string);
}
return 0;
}

// 输出结果
// aaaaaaaaaaaaa

这个代码可能对于一些新手有一些难度了

我们回看一开始的声明,里面写了二级指针声明的含义。
如果参数需要一个一级指针,传入的时候,就要传入对象的地址。
如果参数需要一个二级指针,传入的时候,就要传入一级指针的地址。

一级指针目的是为了操作对象,当然要传入对象的地址。
二级指针目的是为了操作一直指针,当然要传入一级指针的地址。

所以当我们不知道要给当前指针所指的地方申请多少内存空间的时候,就需要使用二级指针,将一级指针的地址传进来,为这个一级指针申请内存空间。

*str_ptr = (char *) malloc(sizeof(char) * len);

我们看下这行代码,*str_ptr就是这二级指针指向的一级指针(我们将一级指针的地址传进来,赋值给了二级指针)

*str_ptr并不是在函数里生成的,所以不会随着函数的退出而消失。

方法栈

了解过方法栈的肯定都知道,方法退出的时候,清空的是方法栈的内存。
而我们将外面的一个内存地址传了进来,再通过*找到了这个内存地址上的指针(不是指向这块内存的指针)

我们之所以对二级指针有很多困惑,其中的一个原因就是p->name这个语法糖
name并不是p指向的对象,而是指向对象的内部成员
p->name等同于(*p).name

4.3 [扩展]黑客写法:整数地址

请思考:既然我们将指针的地址传入函数,是不是意味着我们可以将指针的地址使用整数传进来?
答案是:当然!!!只不过这是一种有点黑客的写法。

1
2
3
4
5
6
void copyStringToDynamicMemory2(unsigned long long str_ptr_address, char *content) {
unsigned long len = strlen(content) + 1;
char **str_ptr = (char **) str_ptr_address;
*str_ptr = (char *) malloc(len * sizeof(char));
strcpy(*str_ptr, content);
}

我们使用了unsigned long long这个超长整数来传递,然后我们在函数内部将这个指针的地址还原成他原本的身份————二级指针,后面的操作就照旧了。

5. 作为返回值

5.1 内存的所有权发生转移

我们直接在函数里申请了一块堆内存,这个最后将指向这块内存的指针返回,这指针本身会被销毁,但是我们返回值接收后,直接使用里面存的堆内存地址即可,这叫做“内存的所有权发生转移”。函数生产,主调函数负责销毁(free)。

1
2
3
4
5
6
7
8
// 类似于面向对象的 new String("...")
char* create_string(const char* src) {
char* new_str = (char*)malloc(strlen(src) + 1);
if (new_str) {
strcpy(new_str, src);
}
return new_str; // 返回堆内存地址,离开函数后依然有效
}

5.2 “定位器”:返回数据结构中的某个节点

1
2
3
4
5
6
7
8
9
10
// 模拟 strchr:查找字符 c 在 str 中第一次出现的位置
char* find_char(char* str, char c) {
while (*str != '\0') {
if (*str == c) {
return str; // 返回指向该字符的指针
}
str++;
}
return NULL; // 没找到
}

返回的指针存放着找到的字符串(内存在函数外面申请)的某个字符的地址。

注意事项

  1. 禁止返回指向局部栈变量的指针

    1
    2
    3
    4
    5
    6
    char* dangerous_func() {
    char buffer[100] = "hello"; // 局部变量,存在栈(Stack)上
    return buffer;
    } // 函数结束 -> 栈帧销毁 -> buffer 所在的内存被标记为可回收

    // 主函数接到这个指针时,指向的是一块“废弃之地”,数据可能已经被覆盖。
  2. 返回 static 变量的地址要小心
    为了避免 malloc 和 free,有人会用 static 局部变量。

    • 好处:static 变量存在静态数据区,函数结束内存不销毁,返回地址是安全的。
    • 坏处:非线程安全,且不支持递归或连续调用。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    char* format_time() {
    static char buffer[100]; // 静态变量,全局只有这一份
    // ... 写入时间到 buffer ...
    return buffer;
    }

    // 问题场景:
    char *t1 = format_time();
    char *t2 = format_time();
    // 此时 t1 和 t2 指向的是同一个地址!t2 的内容会覆盖 t1 的内容。
  3. 明确“谁负责 Free” (Ownership)
    这是 C 语言 API 设计的核心哲学。

    • 如果函数返回的是 malloc 出来的指针,必须在文档中注明:“Caller takes ownership” (调用者负责释放)。
    • 如果函数返回的是内部缓存或静态数据,必须注明:“Do not free”。
  4. 必须处理 NULL
    既然是返回指针,就有可能失败(比如内存耗尽、没找到目标)。

    • 原则:永远不要假设返回的指针是有效的,一定要判空。
  • 标题: C 傻鸟指针
  • 作者: Lucas
  • 创建于 : 2023-10-27 10:14:16
  • 更新于 : 2025-11-24 16:55:40
  • 链接: https://darkflamemasterdev.github.io/2023/10/27/C-傻鸟指针/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论