指针
撰写时间:2024-08-24
修订时间:2024-09-25
学好C语言指针的要点有二:一是需掌握指针的语法与特点;二是需学习掌握计算机内存存储机制及寻址方式的基本原理。
医学课上,学生必须先了解人体结构,才能进一步学习手术知识。很难想像,一个医生有高超的手术技能,却对人体结构一无所知。您敢让他给您动手术吗?
然而在学习C语言的学生中,却存在许多这样的医生。
内存存储的细节
对于普通的变量:
int num = 10;
我们所看到的是该变量的类型、名称,及其数值。但对于任何一个变量,其数值的存储均占用一定的空间。sizeof操作符可返回特定变量所占用的内存空间大小,以字节 (byte)为单位。
unsigned char num = 25;
printf("%zu\n", sizeof num); // 1
上面表示变量num的数量类型为unsigned char,其所占用的内存空间的大小为1个字节。
因此,衡量某个变量占有多少存储空间,我们通常称为字长(bytes length)。故称,类型为unsigned char的变量num,其字长为1字节。
计算机内部使用二进制来存储数据。二进制只能表示0或1这两种数值。C语言中使用位(bit)来表示每个二进制数值。因此,每一位只能存储一个二进制中的0或1的数值。
每个字节由8位构成。因此,变量num的数值,十进制是25,转换为二进制后为00011001,则该变量的数值在计算机内存中实际物理存储示意图如下:
digraph {
mem_addr [shape=plain, fillcolor=invis, label=<
>];
}
灰色部分的单元格代表这些邻近的表示存储单元的单元格可能存储了其他数值。
我们注意到,十进制数值25,其二进制11001的有效位数只有5位,在存储时其值前面自动添补了3个0。这是因为num的字长为1字节,也即8位,故它一次需要至少8个存储单元格,前面未占用的存储单元格需以0来填充。这就是数据类型在存储时的意义所在,它不仅表示需要多少内存空间,同时也会自动覆盖相应字长的内存空间的内容。
我们可以将每个字节都想像为一户住户,每户住户均至多只能居住8个人。在实际生活中,每户住户都有一个唯一的门牌号码;同样,在计算机存储系统中,每个字节都有一个唯一的地址。因此我们也称,字节是计量内存空间大小的最小单位,或称,字节是最小的寻址单位。
可通过使用&操作符来取得变量的地址。
unsigned char num1 = 25;
unsigned char num2 = 26;
printf("%p\n", &num1); // 0x7FF7B14047DF
printf("%p\n", &num2); // 0x7FF7B14047DE
则这两个变量的数值在内存中存储状态如下:
digraph {
mem_addr [shape=plain, fillcolor=invis, label=<
Address | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | digit value |
...... | | | | | | | | |
0x7FF7B14047DF | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 1 | 25 |
0x7FF7B14047DE | 0 | 0 | 0 | 1 | 1 | 0 | 1 | 0 | 26 |
...... | | | | | | | | |
>];
}
笔者所用的系统是64位的操作系统,可看出其表示地址的十六进制的数值最大共有16位,十六进制每两位数为1个字节,这意味着在64位操作系统中,共使用8个字节的长度来寻址。这也是64位的由来:8 * 8 = 64位。则其可寻址范围数量为264个地址。即使严格按上面所显示的使用6个字节的长度来寻址,则寻址范围也为248个地址。都是天文数字般的数字。
寻址能力与内存大小是两个不同的概念。寻址能力与操作系统是32位或64位有关,操作系统的位数越高,其寻址能力越强。而内存是用户安装在特定电脑上的内存条的总容量,即使操作系统有较强的寻址能力,但如果内存较小,可分配的内存数量也较小,则会白白浪费寻址能力。
上面,0x7FF7B14047DF的地址使用1字节存储了25这个值,0x7FF7B14047DE的地址使用1字节存储了26这个值。因为两个数值的类型均为字长为1字节的unsigned char,因此这两个地址的编号都紧邻排在一起。
变量名称num1、num2呢?它们在内存中保存在哪个地址?
变量名称不在寻址范围内,不占用可寻址的存储空间。相反,编译器在编译过程中通过建立起一个符号表,将变量名指向相应的内存地址。
digraph {
symbol_table [shape=plain, fillcolor=invis, label=<
Variable Name | Data Type | Address |
num1 | unsigned char | 0x7FF7B14047DF |
num2 | unsigned char | 0x7FF7B14047DE |
>];
mem_addr [shape=plain, fillcolor=invis, label=<
Address | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | digit value |
...... | | | | | | | | |
0x7FF7B14047DF | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 1 | 25 |
0x7FF7B14047DE | 0 | 0 | 0 | 1 | 1 | 0 | 1 | 0 | 26 |
...... | | | | | | | | |
>];
symbol_table:f0 -> mem_addr:f0;
symbol_table:f1 -> mem_addr:f1;
}
我们注意到,可寻址空间范围内是不包含任何数据类型的数据信息的,它仅是纯粹地连续存储了任意内容的数值,并负责以字节为单位对外展示其可寻址的地址。而如何取出相应的数据,则由符号表根据相应变量的数据类型,精准取出特定范围内的数据。
由此,在已经清晰地知道变量的数据类型的字长的情况下,我们可以将上图简化为下面的图示:
digraph {
mem_addr [shape=plain, fillcolor=invis, label=<
Variable | Address | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | digit value |
...... | ...... | | | | | | | | |
num1 | 0x7FF7B14047DF | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 1 | 25 |
num2 | 0x7FF7B14047DE | 0 | 0 | 0 | 1 | 1 | 0 | 1 | 0 | 26 |
...... | ...... | | | | | | | | |
>];
}
变量名、内存地址,及其所存储的数值,这3个内容,成为我们现在应关注的对象。
指针的声明
int num = 10;
int *ptr = #
printf("%p\n", &ptr); // 0x7ff7b1bbe7d0
printf("%p\n", ptr); // 0x7ff7b1bbe7dc
printf("%d\n", *ptr); // 10
声明了一个名为ptr的指针变量,并在声明时,直接将num的地址赋值于它。
出现了一个新的操作符*,在两个地方使用。第一个地方是在声明指针变量时使用:
int *ptr;
int * ptr;
int* ptr;
声明时不管*操作符的左右两边是否出现空格,均为有效的声明方式。
对于指针,正确的阅读方法是从右往左阅读。即ptr是一个指针,它指向一个类型为int的变量。
*操作符还可用于访问指针变量所指向的地址所存储的数值。
查看指针的内部存储状态
int num = 10;
int *ptr = #
printf("num Address: %p, Value: %d\n", &num, num);
printf("ptr Address: %p, Value: %p, Target value: %d", &ptr, ptr, *ptr);
终端输出:
num Address: 0x7ff7bcece7d8, Value: 10
ptr Address: 0x7ff7bcece7d0, Value: 0x7ff7bcece7d8, Target value: 10
此时内存地址示意图如下:
digraph {
mem_addr [shape=plain, fillcolor=invis, label=<
| Address | Value |
num | 0x7ff7bcece7d8 | 10 |
ptr | 0x7ff7bcece7d0 | 0x7ff7bcece7d8 |
>];
mem_addr:src -> mem_addr:dst;
}
通过上面的代码及示意图,可以看出:
- 对于任意一个变量,不管其类型是否指针,均有变量名、内存地址以及该地址所存储的值这3个部分。
- 在实际存储中,只有内存地址,以及该地址所存储的值这两个部分。变量名只不是编译器为方便我们引用地址而添加的内存地址的别名。
- 不管是普通变量,还是指针变量,要查看存储该变量数值的内存地址,均应使用
&变量名
的格式。
- 要想查看指针变量所存储的值,与普通变量一样,使用
变量名
的格式即可。但在实际编码中,由于使用变量名可以快速帮助我们寻址,因此,查看指针变量所存储的内存地址的值没有太大的价值及意义。我们只需记住,这是一个指向其他变量的地址的指针即可。但也有例外,因为指针可以进行加减的操作,即从所引用的变量的地址进行了特定数量的偏移,此时,查看指针变量所存储的内存地址的值变成有意义了。
- 指针变量存储的值是一个有效的内存地址。要查看该内存地址所存储的值,须使用
*指针变量名
的解引方式。
当我们学习指针时,经常说,指针变量指向了其他变量的地址。并且,以上面已经出现过的代码为佐证:
int num = 10;
int *ptr = #
说明指针变量ptr指向了普通变量num的地址。但眼见并非为实。看上面的图示,num哪有地址?正确的说法是,num所存储的数值10有地址,而作为变量标识符,num指向了这个地址。不是指针类型的变量指向了某个地址?没错,回头再看第一节的图示就能明白,任何变量都通过符号表指向了存储其值的地址。确切的说法是,num和ptr均指向了存储其值的地址。这也正是我们均可通过&num
及&ptr
来取得它们各自存储的数值的地址原因。
printf("%p", &num);
printf("%p", &ptr);
此外,普通变量与指针变量都存储了相应的数值,因此,当我们调用:
printf("%d", num);
printf("%p", ptr);
均可直接打印出这两个变量所存储的值(但因数值的数据类型不一样,所需的限定符也不一样:需分别使用%d
与%p
)。
这是它们相同之处。
不同的地方在于,普通变量直接存储程序所需的数据,而指针变量存储的是某个数据的地址。普通变量可直接访问存储该数值的地址并读取出数据。但指针变量不同,它要访问该数据,共有2步。第一步,根据指针变量所存储的地址,访问该地址。第二步,提取在该地址中所存储的数据。而使用了操作符*的代码*ptr
干的就是这两步的活,让我们像使用普通变量一样,可直接提取指针变量的数据,从而减轻了程序员的负担。
下表列出了普通变量与指针变量在何时使用何种操作符的规律:
变量类型 | 变量名 | 查看存储值的地址 | 查看值 | 值类型 | 查看类型为地址的值中该地址所存储的值 |
普通变量 | num | &num | num | 常规数据 | |
指针变量 | ptr | &ptr | ptr | 地址 | *ptr |
void *指针
基本使用
void *指针是一种通用类型的指针,可用于存储任何数据类型的地址。
void *pData;
int a = 5;
pData = &a;
printf("address: %p\n", &pData);
printf("value: %p\n", pData);
/*
address: 0x7ffc290bd9f0
value: 0x7ffc290bd9ec
*/
因为void *指针不记忆所存储数值的数据类型,因此我们不能直接解引用。
printf("pData value: %d\n", *pData);
/*
warning: dereferencing ‘void *’ pointer
error: invalid use of void expression
*/
可使用以下代码,先将指针转换为特定数据类型的指针后,再解引用。
printf("pData value: %d\n", *((int *)pData));
/*
pData value: 5
*/
指针的指针
下面,我们准备在堆上分配一块内存区域,然后在该块内存区域存储一个特定的地址。
void *pHeapData = malloc(sizeof(void *));
printf("\nInfo of local variable pHeapData:\n");
printf("address: %p\n", &pHeapData);
printf("point to: %p (heap memory address) \n\n", pHeapData);
int a = 10;
int *ptr = &a;
printf("Info of local variable ptr:\n");
printf("point to: %p\n\n", ptr);
free(pHeapData);
终端显示:
Info of local variable pHeapData:
address: 0x7ffd77c751b8
point to: 0xf43010 (heap memory address)
Info of local variable ptr:
point to: 0x7ffd77c751b4
00 01 02 03 04 05 06 07
-- -- -- -- -- -- -- --
0XF43010 B4 51 C7 77 FD 7F 00 00 .Q.w....
即,我们准备在0xf43010的地址处保存0x7ffd77c751b4的地址值。看起来需求比较简单。但这里有雷容易被踩到。
首先,pHeapData是一个本地的指针变量,指向了在堆上已经分配内存空间的地址。其主要作用是在程序的最后,用其来释放内存。
因此,pHeapData
指向0xf43010,若想修改该地址的值,让其保存另一地址的值,需使用*pHeapData
作为左值。
但,如前所述,上面的代码其实是对void *指针进行解引,将导致警告及失败。
printf("%p\n", *pHeapData);
/*
warning: dereferencing ‘void *’ pointer
error: invalid use of void expression
*/
故而,我们不能使用解引的方式,但可通过调用string.h库中的memcpy函数来达到目的。
void *pHeapData = malloc(sizeof(void *));
printf("\nInfo of local variable pHeapData:\n");
printf("address: %p\n", &pHeapData);
printf("point to: %p (heap memory address) \n\n", pHeapData);
int a = 10;
int *ptr = &a;
printf("Info of local variable ptr:\n");
printf("point to: %p\n\n", ptr);
memcpy(pHeapData, &ptr, sizeof(void *));
dump_mem(pHeapData, sizeof(void *), 1, true);
free(pHeapData);
对于memcpy,我们分别传入目标与源的地址,该函数负责将源地址的内容复制到目标地址的存储空间。
终端显示:
Info of local variable pHeapData:
address: 0x7ffd77c751b8
point to: 0xf43010 (heap memory address)
Info of local variable ptr:
point to: 0x7ffd77c751b4
00 01 02 03 04 05 06 07
-- -- -- -- -- -- -- --
0XF43010 B4 51 C7 77 FD 7F 00 00 .Q.w....
内存地址的打印输出
%n
int num = 25;
int *ptr = #
signed int prt_char_nums = 0;
printf("%p%n\n", ptr, &prt_char_nums);
printf("%d\n", prt_char_nums);
限定符%n
将当前打印进终端流的字符数量保存进一个指针变量中。
%p
尽管%x
或%X
可打印十六进制,但这两个限定符要求参数类型为unsigned int。而%p
要求参数的类型为void *指针类型。因此,若需打印指针,只应使用%p
限定符。
int num = 25;
int *ptr = #
printf("%p\n", ptr); // 0x7ff7b01d77dc
但%p
的问题是只能显示小写的十六进制,另外,不能按需求定制。
编写内存打印工具
memutils.h的内容:
#ifndef memutils_h
#define memutils_h
#include <stddef.h>
#include <stdbool.h>
#define PTR_SIZE sizeof(void *)
#define ADDR_STR_BUF_SIZE (2 * PTR_SIZE + 2 + 1)
void get_mem_str(char *buffer, void *addr, bool isFull);
void dump_mem(const void *addr, size_t bytes_per_row, int rows, bool isFull);
#endif /* memutils_h */
memutils.c的内容:
#include "memutils.h"
#include <stdio.h>
#include <ctype.h>
#include <string.h>
void get_mem_str(char *buffer, void *addr, bool isFull) {
int len = sprintf(buffer, "%p", addr);
char *ptr = buffer;
while(*ptr) {
*ptr = toupper(*ptr);
ptr++;
}
if (isFull) {
return;
}
size_t half_addr_len = PTR_SIZE;
size_t start_index = len - half_addr_len - 2;
ptr = buffer;
ptr += start_index;
memmove(ptr, "0X", 2);
memmove(buffer, ptr, strlen(ptr) + 1);
}
void dump_mem(const void *addr, size_t bytes_per_row, int rows, bool isFull) {
const unsigned char *pc = (unsigned char *)addr;
const unsigned char *pc_in_row = NULL;
char buffer[ADDR_STR_BUF_SIZE];
const char COL_GAP = 2;
get_mem_str(buffer, (void *)pc, isFull);
char first_col_width = strlen(buffer) + COL_GAP;
printf("\n%*s", first_col_width, "");
for (int i = 0; i < bytes_per_row; i++) {
printf("%-3.2X", i);
}
printf("\n");
printf("%*s", first_col_width, "");
for (int i = 0; i < bytes_per_row; i++) {
printf("%-3.2s", "--");
}
printf("\n");
for (int i = 0; i < rows; i++) {
pc_in_row = pc + bytes_per_row * i;
get_mem_str(buffer, (void *)pc_in_row, isFull);
printf("%-*s", first_col_width, buffer);
for (int j = 0; j < bytes_per_row; j++) {
printf("%-3.2X", *pc_in_row);
pc_in_row++;
}
printf("%*s", COL_GAP - 1, "");
pc_in_row = pc + bytes_per_row * i;
for (int j = 0; j < bytes_per_row; j++) {
if (!isprint(*pc_in_row)) {
printf(".");
} else {
printf("%c", *pc_in_row);
}
pc_in_row++;
}
printf("\n");
}
printf("\n");
}
dump_mem函数可定制在每行上显示的字节数量,以及需显示多少行。此外,长地址及短地址均支持。
打印数组的内存情况:
int nums[5] = {1, 2, 3, 4, 5};
int ele_size = sizeof(nums[0]);
int ele_num = sizeof(nums) / ele_size;
dump_mem(&nums, ele_size, ele_num, false);
终端显示:
00 01 02 03
-- -- -- --
0XBBB9B7C0 01 00 00 00 ....
0XBBB9B7C4 02 00 00 00 ....
0XBBB9B7C8 03 00 00 00 ....
0XBBB9B7CC 04 00 00 00 ....
0XBBB9B7D0 05 00 00 00 ....
左栏为地址,用十六进制表示。中间一栏为左栏地址所存储的数值,也用十六进制表示。中间一栏的每一列为一个字节的内容。上面每一行中仅显示4个字节的内容,因此左栏每个地址均相差4个字节。右边一栏将中间一栏每个字节的内容视为ASCII字符而依序打印出来,如遇不可打印字符则用.
来代替。
如果换成每行显示2个int的字长,共显示3行,且显示长地址:
int nums[5] = {1, 2, 3, 4, 5};
int ele_size = sizeof(nums[0]);
int ele_num = sizeof(nums) / ele_size;
dump_mem(&nums, ele_size * 2, 3, true);
终端显示:
00 01 02 03 04 05 06 07
-- -- -- -- -- -- -- --
0X7FF7BD7E67C0 01 00 00 00 02 00 00 00 ........
0X7FF7BD7E67C8 03 00 00 00 04 00 00 00 ........
0X7FF7BD7E67D0 05 00 00 00 F7 7F 00 00 ........
字符指针
char *s = "abc";
dump_mem(s, 16, 1, true);
终端显示:
00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
0X1008BBF6C 61 62 63 00 25 70 00 30 58 00 0A 25 2A 73 00 00 abc.%p.0X..%*s..
字符指针是指向字符类型的指针。在C语言中,这是一种很特殊的数据类型。
我们之前使用指针,是先声明一个其他变量,待其有了存储空间及地址后,再创建指向该地址的指针变量:
int num = 10;
int *ptr = #
但上面字符指针的创建却很奇怪,无需先创建其他变量,而是直接创建并赋值:
char *s = "abc";
再查看其地址,上面我们打印的是其完整地址,该地址与上面的例子所显示出来的地址不一样。
原因是字符指针所指向的数据,存储于一处称为字符串字面量池的区域。编译器自动帮我们在该区域上申请了一片内存空间,并将其内容设置为abc
,最后向我们返回该指针。
我们不能修改存储在字面量池中的内容。
char *s = "abc";
*s = 'D'; // error
char *ptr = s;
*ptr = 'D'; // error
上面试图修改其值的代码,均会导致出现:
Bus error: 10
的错误。我们无此权限。
字符指针与字符数组也不一样。字符数组不在字面量池上创建,我们可以修改其值。字符数组有2种创建方式。一种是自动以字符串的形式来创建:
char s[] = "abc";
dump_mem(s, 8, 1, true);
终端显示:
00 01 02 03 04 05 06 07
-- -- -- -- -- -- -- --
0X7FF7B76BA7DC 61 62 63 00 00 AA 6B B7 abc...k.
这种方式,数组的最后以ASCII码表中的NUL字符(经常写为'\0'字符,其ASCII值为0)来结尾。另一种是以指定单独的字符的形式来创建:
char t[] = {'e', 'f', 'g'};
dump_mem(t, 8, 1, true);
终端显示:
00 01 02 03 04 05 06 07
-- -- -- -- -- -- -- --
0X7FF7B76BA7D9 65 66 67 61 62 63 00 00 efgabc..
这种方式,数组的最后没有'\0'字符。
而字符指针,则会永远跟着一个'\0'字符来结尾。
字符指针,就是大名鼎鼎的C字符串
。基于上面所述的特点,我们在与C字符串打交道时,需注意其内部存储的细节。
对于'\0'字符,正确的解读是,它是一个表示空值的字符,常放在C字符串之后,表示C字符串的结尾。虽然其ASCII值为0,但并不意味着它就是阿拉伯数字中的0。实际上,阿拉伯数字中的0,其在ASCII码表中的ASCII值为48。它们两个不是一回事。而在表示C字符串的结尾时,我们通常说C字符串的结尾会有一个NUL字符,或'\0'字符,以免与阿拉伯数字中的0相混淆。
动态分配内存
指针可以动态地跟踪内存地址,此特点一旦结合C标准库中所提供的功能,指针将变成瑞士军刀般的利器。且不可或缺。
malloc
#include <stdio.h>
#include <stdlib.h>
#include "memutils.h"
int main() {
int *pi = (int *)malloc(sizeof(int));
*pi = 125;
dump_mem(pi, sizeof(int) * 2, 1, true);
free(pi);
pi = NULL;
}
malloc函数位于stdlib库中。其作用是在堆(heap)上动态地申请分配一块内存区域,其参数是要分配的字节数,返回类型为void *的地址。
上面的代码,先将malloc函数所返回的地址显式地转换为int *的指针并赋值于变量pi。也可以使用隐式转换:
int *pi = malloc(sizeof(int));
然后使用解引操作符*将该地址的值设置为125,并打印出其内存状态。
00 01 02 03 04 05 06 07
-- -- -- -- -- -- -- --
0X600000264040 7D 00 00 00 00 00 00 00 }.......
使用完后,一定要调用free函数来释放这块动态分配的内存,否则将造成内存泄露。
现在,pi所指向的内存区域已由系统接管,但pi仍指向原地址,以后若不小心再次通过该变量来访问该地址甚至修改该地址的内容,将造成极大的风险。pi已成为迷途指针。因此,上面将其值置为NULL值,以绝后患。
realloc
realloc函数可重新分配内存。
int *pi = malloc(sizeof(int));
*pi = 125;
dump_mem(pi, sizeof(int) * 2, 1, true);
int *newpi = realloc(pi, sizeof(int) * 5);
pi = NULL;
dump_mem(newpi, sizeof(int), 5, true);
free(newpi);
newpi = NULL;
终端显示:
00 01 02 03 04 05 06 07
-- -- -- -- -- -- -- --
0X6000014B8040 7D 00 00 00 00 00 00 00 }.......
00 01 02 03
-- -- -- --
0X6000016BD220 7D 00 00 00 }...
0X6000016BD224 00 00 00 00 ....
0X6000016BD228 00 00 00 00 ....
0X6000016BD22C 00 00 00 00 ....
0X6000016BD230 00 00 00 00 ....
pi先分配了1个int所需的字节数即4个字节的内存空间,然后再调用realloc重新分配了5个int所需的字节的内存。
realloc函数做了以下几件事情:
- 根据重新申请的字节数,分配一块内存区域(新内存区域的起始地址,视情况,可能与原地址相同)。
- 将原地址的数据复制到新地址上。
- 释放原地址的内存空间。
- 返回新地址。
由于realloc函数会自动释放原地址的内存空间,因此在调用该函数后,我们不可以再调用free函数来释放其内存空间了。但此时,上面的pi立即变成迷途指针,故我们及时将其置为空值。
明白了上面的细节,我们可以编写更高效的代码:
int *pi = malloc(sizeof(int));
*pi = 125;
dump_mem(pi, sizeof(int) * 2, 1, true);
pi = realloc(pi, sizeof(int) * 5);
dump_mem(pi, sizeof(int), 5, true);
free(pi);
pi = NULL;
其逻辑原因是:如果新地址与旧地址一样,则pi仍指向有效地址而无需置于空值;如果新地址与旧地址不一样,旧内存已被收回,pi成为迷途指针,但我们将所返回的新地址仍赋值于pi,它重新变为不再迷途。最后,就像之前一样,依据它来释放内存并将其置空即可。其结果是,无需再引入一个新的变量,以免增加不必要的负担。
动态分配内存需注意的问题
看下面代码:
int *pi = malloc(sizeof(int));
pi++;
free(pi);
pi = NULL;
pi持有动态分配内存的地址后,向前移了1个单位,从而指向新的地址。此时再调用free函数准备释放内存时,将报错:
Abort trap: 6
即出现了越界错误。因为所要释放的地址与原地址不一样。
因此,在动态分配内存后,一定要妥善保管好指向动态内存的地址指针。如果我们需要移动指针,可通过其他指针来进行。
int *pi = malloc(sizeof(int) * 2);
int *ptr = pi;
ptr++;
*ptr = 10;
dump_mem(pi, 8, 1, true);
free(pi);
pi = NULL;
ptr = NULL;
终端显示:
00 01 02 03 04 05 06 07
-- -- -- -- -- -- -- --
0X600003A20040 00 00 00 00 0A 00 00 00 ........
因为ptr也指向了动态分配内存区域,因此最后也需同时置空。
参考资源
- C11 Standard, §7.21.6.1, The fprintf function, P309
- https://developer.mozilla.org/en-US/docs/Web/Events/Creating_and_triggering_events