在C/C++中有个叫指针的玩意存在感极其强烈,而说到指针又不得不提到内存管理。现在时不时能听到一些朋友说指针很难,实际上说的是内存操作和管理方面的难。(这篇笔记咱也会结合自己的理解简述一些相关的内存知识)
最近在写C程序使用指针的时候遇到了几个让我印象深刻的地方,这里记录一下,以便今后回顾。

“经一蹶者长一智,今日之失,未必不为后日之得。” – 王阳明《与薛尚谦书》
指针和二级指针
简述下指针的概念。
指针
一个指针可以理解为一条 内存地址。

这里先定义了一个整型变量
test
,接着用取址运算符&
取得这个变量的内存地址并打印出来。
可以看到该变量的内存地址是000000000061FE1C
指针变量
指针变量就是存放 指针(也就是存放内存地址)的变量,使用 数据类型* 变量名
进行定义。
值得注意的是指针变量内储存的指针(内存地址) 所代表的变量的数据类型,比如 int*
定义的指针变量就只能指向 int
类型的变量。
int test = 233;
int* ptr = &test;
test
变量的类型是整型int
,所以test
存放的就是一个整形数据。
而ptr
变量的类型是整型指针类型int*
,存放则的是 整性变量test
的指针(内存地址)。
二级指针
二级指针指的是 一级指针变量的地址。
int main() {
int test = 233;
printf("%p\n", &test);
int *ptr = &test;
printf("%p", &ptr);
return 0;
}
/* stdout
000000000061FE1C
000000000061FE10
*/
这个例子中二级指针就是
ptr
变量的地址000000000061FE10
。
二级指针变量
二级指针变量就是存放 二级指针(二级指针的地址)的变量,使用 数据类型** 变量名
进行定义。
int main() {
int test = 233;
int *ptr = &test;
int **ptr2 = &ptr;
return 0;
}
ptr
变量的类型是整型指针类型int*
,存放的是 整性(int
)变量test
的指针(内存地址),
ptr2
变量的类型是二级整型指针类型int**
,存放的是 整性指针(int*
)变量ptr
的内存地址。

多级指针变量
虽然二级以上的指针变量相对来说不太常用,但我觉得基本的辨别方法还是得会的:
[En]
Although pointer variables above level 2 are relatively uncommonly used, I think the basic method of discrimination still has to be:
通过观察发现,指针变量的数据类型定义其实就是在 其所指向的数据类型名后加一个星号,
比如说:
- 指针
ptr
指向整型变量int test
,那么它的定义写法就是int* ptr
。(数据类型在int
后加了一个星号) - 指针
ptr2
指向一级指针变量int* ptr
,那么它的定义写法就是int** ptr2
。(数据类型在int*
后加了一个星号)
再三级指针变量 int*** ptr3
,乍一看星号这么多,实际上”剥”一层下来就真相大白了:
(int**)*
实际上三级指针变量指向的就是 二级指针变量的地址。

其他更多级的指针变量可以依此类推。
栈内存和堆内存
指针和内存操作关系紧密,提到指针总是令人情不自禁地想起内存。
[En]
Pointers are closely related to memory operations, and the mention of pointers is always reminiscent of memory.
程序运行时占用的内存空间会被划分为几个区域,其中和这篇笔记息息相关的便是 栈区(Stack)和 堆区(Heap)。
栈区 (Stack)
栈区的操作方式正如数据结构中的栈,是 LIFO后进先出的。这种操作模式的一个很经典的应用就是 递归函数了。
每个函数被调用时需要从 栈区划分出一块栈内存用来存放调用相关的信息,这块栈内存被称为函数的 栈帧。
栈帧存放的内容 主要是(按入栈次序由先至后):
- 返回地址,也就是 函数被调用处的下一条指令的内存地址(内存中专门有代码区用于存放),用于函数调用结束返回时能接着原来的位置执行下去。
- 函数调用时的 参数值。
- 函数调用过程中定义的 局部变量的值。
- and so on…
由LIFO后进先出可知一次函数调用完毕后相较而言 局部变量先出栈,接着是 参数值,最后栈顶指针指向 返回地址,函数返回,接着下一条指令执行下去。
栈区的特性:
- 交由系统(C语言这儿就是编译器参与实现) 自动分配和释放,这点在函数调用中体现的很明显。
- 分配速度较快,但并不受程序员控制。
- 相对来说空间较小,如果申请的空间大于栈剩余的内存空间,会引发 栈溢出问题。(栈内存大小限制因操作系统而异)
比如递归函数控制不当就会导致栈溢出问题,因为每层函数调用都会形成新的栈帧”压到”栈上,如果递归函数层数过高,栈帧迟迟得不到”弹出”,就很容易挤 爆栈内存。
- 栈内存占用大小随着函数调用层级升高而 增大,随着函数调用结束逐层返回而 减小;也随着 局部变量的定义而增大,随着局部变量的销毁而减小。
栈内存中储存的数据的 生命周期很清晰明确。
- 栈区是一片 连续的内存区域。
堆区 (Heap)
堆内存就真的是”一堆”内存,值得一提的是,这里的堆 和数据结构中的堆没有关系。
相对栈区来说,堆区可以说是一个更加灵活的大内存区,支持按需进行动态分配。
[En]
Compared with the stack area, the stack area can be said to be a more flexible large memory area, which supports dynamic allocation on demand.
堆区的特性:
- 交由 程序员或者垃圾回收机制进行管理,如果不加以回收,在整个程序没有运行完前,分配的堆内存会一直存在。(这也是容易造成内存泄漏的地方)
在C/C++中,堆内存需要程序员 手动申请分配和回收。
- 分配速度 较慢,系统需要依照算法搜索(链表)足够的内存区域以分配。
- 堆区 空间比较大,只要还有可用的物理内存就可以持续申请。
- 堆区是 不连续(离散)的内存区域。(大概是依赖 链表来进行分配操作的)
- 现代操作系统中,在程序运行完后会 回收掉所有的堆内存。
要养成不用就释放的习惯,不然运行过程中进程占用内存可能越来越大。
简述C中堆内存的分配与释放
分配
这里咱就直接报菜名吧!

这一部分的函数的原型都定义在头文件 stdlib.h
中。
void* malloc(size_t size)
用于请求系统从 堆区中分配一段 连续的内存块。void* calloc(size_t n, size_t size);
在和malloc
一样申请到连续的内存块后,将所有分配的内存全部 初始化为0。void* realloc(void* block, size_t size)
修改已经分配的内存块的大小(具体实现是重新分配),可以放大也可以缩小。
malloc
可以记成Memory Allocate 分配内存
;
calloc
可以记成Clear and Allocate 分配并设置内存为0
;
realloc
可以记成Re-Allocate 重分配内存
。
简单来说原理大概是这样:
malloc
内存分配依赖的数据结构是 链表。简单说来就是所有空闲的内存块会被组织成一个 空闲内存块链表。- 当要使用
malloc
分配内存时,它首先会依据算法扫描这个链表,直到找到 一个大小满足需求的空闲内存块为止,然后将这个空闲内存块传递给用户(通过指针)。
(如果这块的大小 大于用户所请求的内存大小,则将多余部分”切出来”接回链表中)。 - 在不断的分配与释放过程中,由于内存块的”切割”,大块的内存可能逐渐被切成许多小块内存存在链表中,这些便是 内存碎片。当
malloc
找不到合适大小的内存块时便会尝试 合并这些内存碎片以获得大块空闲的内存。 - 实在找不到空闲内存块的情况下,
malloc
会返回NULL
指针。
释放
释放手动分配的堆内存需要用到 free
函数:
void free(void* block)
只需要传入 指向分配内存始址的指针变量作为实参传入即可。
在
C/C++
中,对于 手动申请分配的堆内存在使用完后一定要及时释放,
不然在运行过程中进程占用内存 可能会越来越大,也就是所谓的内存泄漏。不过在现代操作系统中,程序运行完毕后OS会自动回收对应进程的内存, 包括泄露的内存。内存泄露指的是在程序运行过程中 无法操作的内存。
free
为什么知道申请的内存块大小?

简单来说,就是在 malloc
进行内存分配时会把内存大小 分配地略大一点,多余的内存部分用于储存一些头部数据(这块内存块的信息),这块头部数据内就 包括分配的内存的长度。
但是在返回 指针的时候, malloc
会将其 往后移动,使得指针代表的是 用户请求的内存块的起始地址。
头部数据占用的大小通常是 固定的(网上查了一下有一种说法是 16
字节,也有说是 sizeof(size_t)
的),在将指针传入 free
后, free
会将指针 向前移动指定长度以获得头部数据,读取到 分配的内存长度,然后 连同头部数据和所分配长度的内存一并释放掉。
内存释放可以理解为 这块内存被重新接到了空闲链表上,以备后面的分配。
(实际上内存释放后的情况其实挺复杂的,得要看具体的算法实现和运行环境)
[En]
(in fact, the situation after memory release is actually quite complicated, depending on the specific algorithm implementation and running environment.)
二维数组
定义和初始化
C语言中二维数组的定义:
数据类型 数组名[行数][列数];
初始化则可以使用 大括号:
int a[3][4]={
{1,2,3,4},
{5,6,7,8},
{9,10,11,12}
};
int b[3][4]={ // 内层不要大括号也是可以的,具体为什么后面再说
1,2,3,4,
5,6,7,8,
9,10,11,12
};
char str[2][6]={
"Hello",
"World"
};
此外,在 有初始化值的情况下,定义二维数组时的一维长度(行数)是 可以省略的:
int a[][4]={ // 如果没有初始化,则一维长度不可省略
1,2,3,4,
5,6,7,8,
9,10,11,12
}
在内存中
按上述语句定义的数组,在进程内存中一般储存于:
- 栈区 – 在函数内部定义的 局部数组变量。
- 静态储存区 – 当用
static
修饰数组变量或者在 全局作用域中定义数组。
数组在内存中是 连续且呈 线性储存的, 二维数组也是不例外的。
虽然在使用过程中二维数组发挥的是”二维”的功能,但其在内存中是被映射为一维线性结构进行储存的。
实践验证一下:
int i, j;
int a[][4] = { // 如果没有初始化,则一维长度不可省略
1, 2, 3, 4,
5, 6, 7, 8,
9, 10, 11, 12
};
size_t len1 = sizeof(a) / sizeof(a[0]);
size_t len2 = sizeof(a[0]) / sizeof(a[0][0]);
for (i = 0; i < len1; i++) {
for (j = 0; j < len2; j++)
printf(" [%d]%p ", a[i][j], &a[i][j]);
printf("\n");
}
输出:

第一维有 3行,第二维有 4列。
一个 int
类型数据占用 4
个字节,从上面的图可以看出来:
[1]000000000061FDD0
->[2]000000000061FDD4
相隔4字节,说明这两个数组元素相邻,同一行中数组元素储存连续。[4]000000000061FDDC
->[5]000000000061FDE0
同样相隔4字节,这两个数组元素在内存中也是相邻的。- 从
[1]000000000061FDD0
到[12]000000000061FDFC
正好相差44
个字节,整个二维数组元素在内存中是 连续储存的。
这样一看,为什么 定义并初始化的时候 二维数组的第一维可以省略已经不言而喻了:
在初始化的时候编译器通过数组 第二维的大小对元素进行”分组”,每一组可以看作是一个一维数组,这些一维数组在内存中从低地址到高地址连续排列储存形成二维数组:

在上面例子中大括号中的元素
{1,2,3,4,5,6,7,8,9,10,11,12}
被按第二维长度4
划分成了{1,2,3,4}
,{5,6,7,8}
,{9,10,11,12}
三组,这样程序也能知道第一维数组长度为3
了。
二维数组名代表的地址
一维数组名代表的是数组的起始地址(也是第一个元素的地址)。
二维数组在内存中也是映射为一维进行连续储存的,
既然如此,二维数组名代表的地址其实也是 整个二维数组的起始地址,在上面的例子中相当于 a[0][0]
的地址。
在上面的示例最后加一行:
printf("Arr address: %p", a);
打印出来的地址和 a[0][0]
的地址完全一致,是 000000000061FDD0
。
二维数组和二级指针
二维数组不等于二级指针
首先要明确一点: 二维数组 ≠ 二级指针
刚接触C语言时我总是想当然地把这两个搞混了,实际上根本不是一回事儿。
- 二级指针变量储存的是 一级指针变量的 地址。
- 二维数组是内存中连续储存的一组数据,二维数组名相当于一个 一级指针(二维数组的起始地址)。
int arr[][4]={
{1,2},{1},{3},{4,5}
};
int** ptr=arr; // 这样写肯定是不行的!,ptr储存的是一级指针变量的地址
int* ptr=arr; // 这样写是可以的,但是不建议
int* ptr=&arr[0][0]; // 这样非常ok, ptr储存的是数组起始地址(也就是首个变量的地址)
可以把之前二维数组的例子改一下:
int i;
int a[][4] = { // 如果没有初始化,则一维长度不可省略
1, 2, 3, 4,
5, 6, 7, 8,
9, 10, 11, 12
};
size_t len1 = sizeof(a) / sizeof(a[0]);
size_t len2 = sizeof(a[0]) / sizeof(a[0][0]);
size_t totalLen = len1 * len2; // 整个二维数组的长度
int *ptr = &a[0][0]; // ptr指向二维数组首地址
for (i = 0; i < totalLen; i++) {
// 一维指针操作就是基于一维的,所以整个二维数组此时会被当作一条连续的内存
printf(" [%d]%p ", ptr[i], &ptr[i]);
// printf(" [%d]%p ", *(ptr + i), ptr + i);
if (i % len2 == 3) // 换行
printf("\n");
}
printf("Arr address: %p", ptr);
输出结果和之前遍历二维数组的是一模一样的。
指针数组
实现”二维数组”
既然二级指针变量不能直接指向二维数组,那能不能依赖二级指针来实现一个类似的结构呢?当然是可以的啦!
[En]
Since secondary pointer variables cannot point directly to a two-dimensional array, can you rely on secondary pointers to implement a similar structure? Of course you can!
整型变量存放着整型 int
数据,整型数组 int a[]
中存放了整型数据;
如果是用申请堆内存来实现的整型数组:
int* arr = (int*)malloc(sizeof(int) * 3);
指针 int*
变量 arr
此时指向的是 连续存放整型(int
)数据的内存的起始地址,相当于一个一维数组的起始地址。
代码实现
二级指针 int**
变量存放着一级指针变量的地址,那么就可以构建二级指针数组来存放二级指针数据(也就是每个元素都是 一级指针变量的地址)。

具体代码实现:
int rows = 3; // 行数/一维长度
int cols = 4; // 列数/二维长度
int **ptr = (int **) malloc(rows * sizeof(int *));
// 分配一段连续的内存,储存int*类型的数据
int i, j, num = 1;
for (i = 0; i < rows; i++) {
ptr[i] = (int *) malloc(cols * sizeof(int));
// 再分配一段连续的内存,储存int类型的数据
for (j = 0; j < cols; j++)
ptr[i][j] = num++; // 储存一个整型数据1-12
}
其中
ptr[i] = (int *) malloc(cols * sizeof(int));
这一行,等同于
*(ptr+i) = ...
也就是利用间接访问符 *
让一级指针变量 指向在堆内存中分配的一段连续整形数据,这里相当于初始化了第二维。
而在给整型元素赋值时和二维数组一样用了中括号进行访问:
ptr[i][j] = i * j;
其实就等同于:
*(*(ptr+i)+j) = i * j;
- 第一次访问第一维元素,用第一维起始地址
ptr
加上第一维下标i
,取出对应的 一级指针变量中 存放的地址:*(ptr+i)
这个地址是第二维中 一段连续内存的起始地址。 - 第二次访问第二维元素,用1中取到的地址
*(ptr+i)
加上 第二维下标j
,再用间接访问符*
访问对应的元素,并赋值。
在内存中的存放
指针数组在内存中的存放 不同于普通定义的二维数组,它的 每一个维度是连续储存的,但是 维度和维度之间在内存中的存放是 离散的。
用一个循环打印一下每个元素的地址:
for (i = 0; i < rows; i++) {
for (j = 0; j < cols; j++)
printf(" [%d]%p ", ptr[i][j], *(ptr + i) + j);
printf("\n");
}
输出:

可以看到第二维度的地址是连续的,但是第二维度”数组” 之间并不是连续的。比如元素 4
和元素 5
的地址相差了 20
个字节,并不是四个字节。

其在内存中的存放结构大致如上,并无法保证 *(ptr+0)+3
和 *(ptr+1)
的地址相邻,也无法保证 *(ptr+1)+3
和 *(ptr+2)
的地址相邻。
这种非连续的存放方式可以说是和二维数组相比 很大的一个不同点了。
释放对应的堆内存
通常指针数组实现的”二维数组”是在 堆内存中进行存放的,既然申请了堆内存,咱也应该养成好习惯,使用完毕后将其释放掉:
for (i = 0; i < rows; i++)
free(ptr[i]);
free(ptr);
先利用一个循环释放掉每一个 一级指针变量指向的 连续内存块(储存整型数据),最后再把二级指针变量指向的 连续内存块(储存的是一级指针变量的地址)释放掉。
sizeof的事儿
sizeof()
是C语言中非常常用的一个 运算符,而 二级指针和 二维数组的区别在这里也可以很好地展现出来。
对于直接定义的数组
对于 非变量长度定义的数组, sizeof
在 编译阶段就会完成求值运算,被替换为对应数据的大小的常量值。
int arr[n];
这种定义时数组长度为变量的即为 变量长度数组(C99标准开始支持),不过还是不太推荐这种写法。
直接固定长度 定义二维数组时,编译器是 知道这个变量是数组的,比如:
int arr[3][4];
size_t arrSize = sizeof(arr);
在编译阶段,编译器知道数组 arr
是一个整型 int
二维数组:
- 每个 第二维数组包含四个
int
数据,长度为sizeof(int)*4=16
个字节。 - 第一维数组包含三个 第二维数组,每个第二维数组长度为
16
字节,整个二维数组总长度为16*3=48
个字节。
即 sizeof(arr) = 48
。
对于指针数组
指针变量储存的是指针,也就是一个地址。内存地址在运算的时候会存放在CPU的 整数寄存器中。
64位计算机中整数寄存器宽度有 64
bit(位),而指针数据要能存放在这里。
目前来说 1
字节(Byte) = 8
位(bit),那么 64
位就是 8
个字节,
所以 64位系统中指针变量的长度是 8
字节。
int rows = 3; // 行数/一维长度
int **ptr = (int **) malloc(rows * sizeof(int *));
size_t ptrSize = sizeof(ptr); // 8 Bytes
size_t ptrSize2 = sizeof(int **); // 8 Bytes
size_t ptrSize3 = sizeof(int *); // 8 Bytes
size_t ptrSize4 = sizeof(char *); // 8 Bytes
虽然上面咱通过申请分配堆内存实现了二维数组(用二级指针变量 ptr
指向了指针数组起址),
但其实在编译器眼中, ptr
就 单纯是一个二级指针变量,占用字节数为 8 Bytes
(64位),储存着一个地址,因此在这里是 无法通过sizeof获得这块连续内存的长度的。
通过上面的例子很容易能观察出来:
sizeof(指针变量) = 8 Bytes
(64位计算机)
无论 指针变量指向的是什么数据的地址,它储存的 单纯只是一个内存地址,所以所有指针变量的占用字节数 是一样的。
函数传参与返回
得先明确一点:C语言中不存在所谓的 数组参数,通常让函数接受一个数组的数据需要通过 指针变量参数传递。
传参时数组发生退化
int test(int newArr[2]) {
printf(" %d ", sizeof(newArr)); // 8
return 0;
}
int main() {
int arr[5] = {1, 2, 3, 4, 5};
test(arr);
return 0;
}
在上面这个例子中 test
函数的定义中声明了”看上去像数组的”形参 newArr
,然而 sizeof
的运算结果是 8
。
实际上这里的形参声明是等同于 int* newArr
的,因为把数组作为参数进行传递的时候, 实际上传递的是数组的首地址(因为数组名就代表数组的首地址)。
这种情况下就发生了 数组到 指针的退化。
在编译器的眼中, newArr
此时就 被当作了一个指针变量,指向 arr
数组的首地址,因此声明中数组的长度怎么写都行: int newArr[5]
, int newArr[]
都可以。
为了让代码更加清晰,我觉得最好还是声明为 int* newArr
,这样一目了然能知道这是一个指针变量!
函数内运算涉及到数组长度时
当函数内运算涉及到数组长度时,就需要在函数定义的时候 另声明一个形参来接受数组长度:
int test(int *arr, size_t rowLen, size_t colLen) {
int i;
size_t totalLen = rowLen * colLen;
for (i = 0; i < totalLen; i++) {
printf(" %d ", arr[i]);
if (i % colLen == colLen - 1) // 每个第二维数组元素打印完后换行
printf("\n");
}
return 0;
}
int main() {
int arr[3][3] = {
1, 2, 3,
4, 5, 6,
7, 8, 9
};
test(arr, sizeof(arr) / sizeof(arr[0]), sizeof(arr[0]) / sizeof(arr[0][0]));
return 0;
}
输出:

这个例子中 test
函数就多接受了 二维数组的一维长度 rowLen
和二维长度 colLen
,以对二维数组元素进行遍历打印。
返回”数组”
经常有应用场景需要函数返回一个”数组”,说是数组,实际上函数 并无法返回一个局部定义的数组,哪怕是其指针(在下面一节有写为什么)。
取而代之地,常常会返回一个 指针指向分配好的一块连续的 堆内存。
(在 算法题中就经常能遇到要求返回指针的情况)
int *test(size_t len) {
int i;
int *arr = (int *) malloc(len * sizeof(int));
for (i = 0; i < len; i++)
arr[i] = i + 1;
return arr;
}
int main() {
int i = 0;
int *allocated = test(5);
for (; i < 5; i++)
printf(" %d ", allocated[i]);
free(allocated); // 一定要记得释放!
return 0;
}
这个示例中, test
函数的 返回类型是整型指针。当调用了 test
函数,传入要分配的连续内存长度后,其在函数内部定义了一个局部指针变量,指向分配好的内存,在内存中存放数据后将该指针返回。
在主函数中, test
返回的整型指针被赋给了指针变量 allocated
,所以接下来可以通过一个循环打印出这块连续内存中的数据。
再次提醒,申请堆内存并使用完后,一定要记得使用 free
进行 释放!
生疏易犯-函数返回局部变量
错误示例
记得初学C语言的时候,我曾经犯过一个错误:将 函数内定义的数组的数组名作为返回值:
int *test() {
int arr[4] = {1, 2, 3, 4};
return arr;
}
int main() {
int i = 0;
int *allocated = test();
for (; i < 4; i++)
printf(" %d ", *(allocated + i));
return 0;
}
这个例子中直到for循环前进程仍然正常运行,但是一旦尝试使用 *
运算符取出内存中的数据 *(allocated + i)
,进程立马接收到了系统发来的 异常信号 SIGSEGV
,进而终止执行。
原因简述
SIGSEGV
是比较常见的一种异常信号,代表Signal Segmentation Violation
,也就是内存分段冲突
造成异常的原因通常是进程 试图访问一段没有分配给它的内存,” 野指针“总是伴随着这个异常出现。
上面简述栈区的时候提到了 栈帧,每次调用函数时会在栈上给函数分配一个栈帧用来储存 函数调用相关信息。
函数调用完成后,先把运算出来的 返回值存入寄存器中,接着会在 栈帧上进行 弹栈操作,在这个过程中 分配的局部变量就会被回收。
最后,程序在 栈顶中取到函数的返回地址,返回上层函数继续执行余下的指令。 栈帧销毁,此时 局部变量相关的栈内存已经被回收了。
然而此时寄存器中仍 存着函数的返回值,是一个内存地址,但是 内存地址代表的内存部分已经被回收了。
当将返回值赋给一个 指针变量时, 野指针就产生了——此时这个指针变量 指向一片未知的内存。
所以当进程 试图访问这一片不确定的内存时,就容易引用到无效的内存,此时系统就会发送 SIGSEGV
信号让进程终止执行。
教训
教训总结成一句话就是:
- 程序中请不要让函数返回代表 栈内存的 局部变量的地址。
延伸:返回 静态局部变量是可以的,因为静态局部变量是 储存在静态储存区的。
int *test() {
static int arr[4] = {1, 2, 3, 4};
return arr;
}
👆 如果之前例子中的 test
函数内这个局部数组变量声明为局部的 静态变量,程序就可以正常执行了。
实参结构体中的指针
改变指针变量指向的变量
用一个拥有指针变量的结构体作为实参传入函数:
struct Hello {
int num;
int *ptr;
};
int test(struct Hello testStruct) {
printf(" [test]testStruct-Ptr: %p \n", ++testStruct.ptr);
*testStruct.ptr = 2;
return 1;
}
int main() {
int *testPtr = (int *) calloc(4, sizeof(int));
struct Hello testStruct = {
.num=5,
.ptr=testPtr
};
printf(" [main]testStruct-Ptr: %p \n\tptr[1]=%d\n", testStruct.ptr, testStruct.ptr[1]);
test(testStruct);
printf(" [main]testStruct-Ptr: %p \n\tptr[1]=%d\n", testStruct.ptr, testStruct.ptr[1]);
free(testPtr);
return 0;
}
输出:
[main]testStruct-Ptr: 0000000000A71420
ptr[1]=0
[test]testStruct-Ptr: 0000000000A71424
[main]testStruct-Ptr: 0000000000A71420
ptr[1]=2
在 test
函数中,通过自增操作和 *
运算符给 testStruct.ptr
指向的下一个元素赋值为 2
。
通过输出可以看到, test
函数内结构体中指针变量的自增操作并没有影响到 main
函数中结构体的指针变量,这是因为 结构体作为参数传入时实际上是被 拷贝了一份作为局部变量以供操作。
之所以能赋值是因为 testStruct.ptr
是指针变量,存放着一个内存地址。无论怎么拷贝, 变量储存的内存地址是没有变的,所以通过 *
运算符仍然能直接对相应数据进行赋值。
改变原结构体的指针变量指向
如果要在 test
函数中改变原结构体中指针变量的指向,就需要把原结构体的地址传入函数:
int test(struct Hello *testStruct) {
printf(" [test]testStruct-Ptr: %p \n", ++testStruct->ptr);
*testStruct->ptr = 2;
return 1;
}
int main() {
int *testPtr = (int *) calloc(4, sizeof(int));
struct Hello testStruct = {
.num=5,
.ptr=testPtr
};
printf(" [main]testStruct-Ptr: %p \n\t*ptr=%d\n", testStruct.ptr, *testStruct.ptr);
test(&testStruct);
printf(" [main]testStruct-Ptr: %p \n\t*ptr=%d\n", testStruct.ptr, *testStruct.ptr);
free(testPtr);
return 0;
}
输出:
[main]testStruct-Ptr: 00000000001A1420
*ptr=0
[test]testStruct-Ptr: 00000000001A1424
[main]testStruct-Ptr: 00000000001A1424
*ptr=2
可以看到通过在函数内通过地址访问到对应结构体,能直接修改结构体中指针变量的指向。这个例子中通过自增运算符让指针变量指向的内存地址后移了一个 int
的长度。
通过指针访问结构体时使用 箭头运算符
->
获取属性。
最近摔了一跤的地方
被自己绕进去
最近写的一个小工具中有个自动扩大堆内存以容纳数据的需求,最开始我写成了这个样:
[En]
A recently written gadget has a need to automatically expand heap memory to accommodate data, which I initially wrote like this:
#include
#include
#include
#define SIZE_PER_ALLOC 10
void extend(int *arr, int arrPtr, int *arrMax) {
*arrMax += SIZE_PER_ALLOC; // 新分配这么多
arr = (int *) realloc(arr, (*arrMax) * sizeof(int));
memset(arr + arrPtr, 0, SIZE_PER_ALLOC * sizeof(int)); // 将新分配的部分初始化为0
}
int main() {
int i;
int arrPtr = 0;
int arrMax = 10; // 当前最多能容纳多少元素
int *flexible = (int *) calloc(arrMax, sizeof(int));
for (i = 0; i < 95; i++) { // 模拟push 95 个元素
flexible[arrPtr++] = i + 1;
if (arrPtr >= arrMax) // 数组要容纳不下了,多分配一点
extend(flexible, arrPtr, &arrMax);
}
for (i = 0; i < 95; i++) // 打印所有元素
printf("%d ", flexible[i]);
return 0;
}
本来预期是 95
个元素能顺利推入 flexible
这个”数组”,”数组”大小也会扩展为足够容纳 100
个元素。
然而程序运行未半而中道崩殂,这个例子中系统送来了 SIGSEGV
信号(调试器Debugger可能会显示因为 SIGTRAP
而终止进程)。根据上面写到的 SIGSEGV
产生原因,很明显我又访问到了未分配给进程的无效内存(产生了野指针)。
为什么呐
观察一下函数的声明和调用时的传参:
void extend(int *arr, int arrPtr, int *arrMax);
extend(flexible, arrPtr, &arrMax);
后面的 arrPtr
整型变量参数接受 main
函数传入的 arrPtr
的值,用以确定当前”数组”的下标指向哪;而 arrMax
指针变量参数接受 main
函数传入的 arrMax
的地址,用以修改当前”数组”的大小。这两个参数没有引发任何问题。
很明显了,问题就出现在 arr
参数这儿!
实际上,当我将指针变量 flexible
作为参数传入时也只是传入了一个 地址,而不是指针本身。因此在 extend
里调用 realloc
重分配内存后, 新的内存块的地址会被赋给 局部变量 arr
,此时外部的指针变量 flexible
的指向 没有任何改变。
realloc()
在重分配内存时,会尽量在原有的内存块上进行扩展/缩减,尽量不移动数据,这种时候返回的地址 和原来一样。
但是一旦 原有内存块及其后方相邻的空闲内存不足以提供分配,就会找到一块足够大的新内存块,并将原内存块的数据” 移动“过去,此时 realloc()
返回的地址 和原来的不同,并且 原来的地址所代表的内存 已经被回收。
也就是当 realloc()
移动了 数据在内存中的位置时,外面的 flexible
指针变量还 指向着原来的地址,原来地址代表的内存已经被回收了。
因此, extend
函数调用结束后的 flexible
指针变量就变成了 野指针,指向了一片无效内存,所以试图访问这片内存时,就导致了 SIGSEGV
异常。
怎么解决
根本原因在于我传入函数的是 一个地址而不是 指针变量本身,所以把 指针变量的地址传入就能解决了!
#include
#include
#include
#define SIZE_PER_ALLOC 10
void extend(int **arr, int arrPtr, int *arrMax) {
*arrMax += SIZE_PER_ALLOC; // 多分配这么多
*arr = (int *) realloc(*arr, (*arrMax) * sizeof(int));
memset(*arr + arrPtr, 0, SIZE_PER_ALLOC * sizeof(int)); // 将新分配的部分初始化为0
}
int main() {
int i;
int arrPtr = 0;
int arrMax = 10; // 当前最多能容纳多少元素
int *flexible = (int *) calloc(arrMax, sizeof(int));
for (i = 0; i < 95; i++) { // 模拟push 95 个元素
flexible[arrPtr++] = i + 1;
if (arrPtr >= arrMax) // 数组要容纳不下了,多分配一点
extend(&flexible, arrPtr, &arrMax);
}
for (i = 0; i < 95; i++) // 打印所有元素
printf("%d ", flexible[i]);
free(flexible);
return 0;
}
因为 二级指针变量存放一级指针变量的地址,所以在声明形参 arr
的时候需要声明为二级指针:
void extend(int **arr, int arrPtr, int *arrMax);
调用函数的时候,将指针变量 flexible
的 地址传入:
extend(&flexible, arrPtr, &arrMax);
接下来在函数 extend
内部通过 *
运算符访问指针变量 flexible
以做出修改即可。
这样一来程序就能成功运行完成了,输出:
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
教训
说到最开始遇到这个问题的时候,我真的是找了半天都没找着,因为push元素和数组扩展我分开写在了两个源文件中,而这个部分又涉及到其他内存分配的代码。我甚至查了 realloc
是怎么导致 SIGSEGV
的,结果就…打断点调试了好多次才发现是这个问题。
涉及到指针变量和内存操作的时候,一定要牢记 指针变量的指向,也一定要 步步谨慎,不然一旦出现问题,很可能难以定位。
总结
C语言的内存管理很灵活,但正是因为灵活,在编写相关操作的时候要十分小心。
在接触这类和底层接壤的编程语言时对基础知识的要求真的很高…感觉咱还有超长的路要走呢。
那么就是这样,感谢你看到这里,也希望这篇笔记能对你有些帮助!再会~
[En]
So that’s it. Thank you for seeing this. I hope this note can be of some help to you. Goodbye ~

相关文章
Original: https://www.cnblogs.com/somebottle/p/Pointers_and_2D_Arrays.html
Author: SomeBottle
Title: 【小记】与指针和二维数组过几招
原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/115946/
转载文章受原作者版权保护。转载请注明原作者出处!