1 变量的访问方式
内存是计算机中非常重要的存储设备,程序的运行过程都是在内存中完成的。为了高效地管理和使用内存,通常会将内存划分为一个个小的存储单元,每个单元占据 1 个字节(Byte)的空间。
在程序中定义的变量,都会在内存中分配相应的存储空间。不同类型的变量占用的空间大小不同,例如 int 类型通常占用 4 个字节,而 char 类型则只占 1 个字节。
那么,我们如何访问这些存储在内存中的变量数据呢?主要有两种方式:
直接访问:这是我们之前最常用的方式,即通过变量名来直接访问其对应的值。例如:
int a = 10;
printf("%d", a); // 直接通过变量名 a 访问其值
间接访问:这种方式是通过指针来实现的。
2 程序内存、RAM 与存储设备的对比
类别程序内存运行内存(RAM)存储设备(硬盘/SSD)本质虚拟内存空间物理硬件内存长期存储硬件存储内容临时数据(变量、栈等)程序运行时加载的代码/数据操作系统、文件、用户数据访问速度极快(直接 CPU 访问)极快(纳秒级)较慢(毫秒级)数据持久性程序退出后丢失断电后丢失断电后保留容量大小MB~GB 级GB 级(如 8GB、16GB)GB~TB 级分配方式编译器/运行时动态分配操作系统管理(虚拟内存)用户/程序手动或自动分配典型用途程序运行时中间数据运行程序、缓存数据长期存储文件和程序与程序的关系程序直接操作的对象程序运行的物理基础数据来源和存储目标
程序内存 vs RAM:
程序内存是逻辑抽象,依赖 RAM 的物理支持。程序通过指针操作虚拟内存,实际数据存储在 RAM 中。
RAM vs 存储设备:
RAM 临时存储,断电丢失;存储设备永久保存。RAM 用于快速访问当前数据,存储设备用于长期归档。
程序内存 vs 存储设备:
程序内存是动态的、临时的;存储设备是静态的、持久的。程序从存储设备加载数据到 RAM,再由程序内存操作。
3 内存地址
为了能够准确地访问每一个内存单元,计算机系统会给每个字节分配一个唯一的编号,这个编号称为内存地址。每个内存地址对应一个字节的数据存储位置。
变量在内存中是存储在一个或多个连续的内存单元中的,即变量也有自己的地址。例如,定义一个 int 类型的变量 num,它通常会在内存中占用 4 个连续的字节(具体大小与平台有关),这 4 个字节中第一个字节的地址,就是整个变量 num 的地址。
如下图所示,变量在内存中是以连续的方式存放的,其地址表示的是该变量起始字节的位置。
在不同架构的系统中,内存地址的表示方式也有所不同:
在 32 位系统中:内存地址是一个 32 位的二进制数(即 4 个字节),理论上可寻址的内存范围为 2³² 个地址,对应约 4GB 的内存空间。在 64 位系统中:内存地址是一个 64 位的二进制数(即 8 个字节),理论上可寻址的内存空间极大,达到 2⁶⁴ 个地址,远远超过目前实际应用的需求。
4 指针的概念
指针(pointer)是一种特殊的变量,专门用于存储内存地址。通过指针,我们可以间接访问内存中的其他数据。
简单来说,如果一个变量用来存放另一个变量的内存地址,那么这个变量就被称为指针变量,通常简称为指针。
如下图所示,假设我们定义了一个 int 类型的变量 num 并赋值为 5。在内存中,num 占用连续的 4 个字节(以 32 位系统为例),其起始地址为 0x0012FF44。接下来,我们使用一个指针变量 ptr,它存储的是 num 的内存地址 0x0012FF44。因此,我们可以说 ptr 指向 num,并通过 ptr 可以间接访问 num 的值。
5 指针的定义
5.1 定义格式
在定义指针时,必须明确指定指针所指向的数据类型。指针的定义格式如下:
数据类型 *指针变量名 [=初始地址值];
数据类型:表示指针所指向地址处存储的数据类型,例如 int(整型)、char(字符型)、float(浮点型)等。不同的数据类型决定了指针每次操作内存时步长的大小,比如 int 类型指针在大多数系统中每次移动 4 个字节(假设 int 占 4 字节),而 char 类型指针每次移动 1 个字节。符号 *:这是一个关键符号,用于告知编译器正在定义的是一个指针变量。* 符号通常放置在变量名前面,以此来表明该指针所指向的数据类型。例如,char * 表示这是一个指向字符类型数据的指针,float * 表示指向浮点类型数据的指针。指针变量名:这是指针在程序中的标识符,通过它可以引用和操作该指针。命名规则与普通变量类似,应遵循见名知意的原则,以提高代码的可读性。初始地址值(可选):可以为指针变量赋予一个初始的内存地址值,这个地址通常是另一个变量的地址,通过取地址运算符 & 来获取。例如,int var = 10; int *ptr = &var; 这里 ptr 指针被初始化为指向变量 var 的地址。
5.2 指针定义的三种形式
在 C 语言中,以下三种定义指针的写法在语法上都是正确的,功能也等价:
int* ptr;
int * ptr;
int *ptr; // 推荐写法
虽然这三种写法都能正常编译和运行,但从代码的可读性和一致性角度考虑,建议选择一种统一的风格并贯穿整个项目。常见的推荐写法是将 * 放在变量名前面,这种写法的好处在于,当需要定义多个指针变量时,能够更清晰地表达每个变量的类型。例如:
int *ptr1, *ptr2; // 推荐写法,明确 ptr1 和 ptr2 都是 int 类型指针
如果将 * 符号紧随数据类型,当定义多个指针变量时,可能会产生误解。例如:
int* ptr1, ptr2;
// ptr1 是一个指向 int 类型的指针(int * 类型)
// ptr2 仅仅是一个普通的 int 变量
实际上,在这个例子中,ptr1 是一个指向 int 类型的指针(int * 类型),而 ptr2 仅仅是一个普通的 int 变量,并非指针。这就容易导致代码理解上的偏差,尤其是在复杂的代码环境中。
在 VS Code 中,我们可以方便地查看变量的数据类型。将鼠标放在变量名上,就能显示其所属的数据类型,这有助于我们准确理解代码中变量的性质,避免因指针定义写法不当而产生的错误理解。
6 取址运算符和取值运算符
6.1 取址运算符(&)
符号:&作用:取址运算符的主要功能是获取变量的内存地址。在程序运行过程中,每个变量都占据着内存中的一个特定位置,取址运算符能够帮助我们找到这个位置的地址。用法:使用方式非常简单,只需将 & 符号放置在变量名前面,就可以得到该变量的内存地址。例如,对于变量 num,&num 就会返回变量 num 的地址。下面是一个具体的代码示例:
int num = 250;
int *ptr = # // 使用取址运算符获取变量 num 的内存地址,并将其赋值给指针 ptr
格式化输出地址:如果需要在 printf 函数中格式化输出地址,应该使用 %p 格式占位符。同时,为了符合标准且避免编译器警告,最好将地址强制转换为 void* 类型。示例代码如下:
printf("num 的地址: %p\n", (void*)&num);
6.2 取值运算符(*)
符号:*作用:取值运算符也被称为解引用运算符或间接引用运算符,它的作用是获取指针所指向的内存地址处的数据值。通过指针,我们可以间接访问和操作其指向的变量。用法:将 * 符号放置在指针变量名前面,就可以访问该指针所指向的变量的值。例如,若 ptr 是一个指向 int 类型的指针,那么 *ptr 就表示 ptr 所指向的 int 变量的值。以下代码展示了其具体用法:
int num = 250;
int *ptr = # // 使用 取址运算符 获取变量 num 的内存地址,并将其赋值给指针 ptr
printf("%d", *ptr); // 使用 取值运算符 通过指针 ptr 间接访问变量 num,输出 250
通过取址运算符和取值运算符的结合使用,我们可以灵活地操作内存中的数据,实现诸如动态内存分配、函数间数据传递等强大的功能,这也是指针在 C 语言中重要性的体现。
7 指针的基本操作
7.1 变量地址获取与指针数据访问
本案例旨在展示指针的基本操作,创建一个 int 类型的变量,使用取址运算符获取其地址并赋值给指针,然后分别打印该变量的值、地址,指针的值、地址以及指针指向的值,以此帮助理解指针与变量地址之间的关系及指针的基本用法。
#include
int main()
{
// 定义一个整型变量 num 并初始化为 100
int num = 100;
// 定义一个整型指针 ptr,使用取址运算符 & 获取变量 num 的地址,并将其赋值给指针 ptr
// 此时 ptr 指向 num 变量所在的内存地址
int *ptr = #
// 打印变量 num 的值,使用 %d 格式占位符
printf("num 的值是 %d \n", num);
// 打印变量 num 的地址,使用 %p 格式占位符,并使用 & 运算符获取地址
printf("num 的地址是 %p \n\n", &num);
// 打印指针 ptr 的值,ptr 中存储的是 num 的地址,使用 %p 格式占位符
printf("ptr 的值是 %p \n", ptr);
// 打印指针 ptr 自身的地址,使用 & 运算符获取 ptr 的地址,%p 格式占位符
printf("ptr 的地址是 %p \n", &ptr);
// 打印指针 ptr 指向的值,使用取值运算符 * 获取 ptr 所指向内存地址中的数据,%d 格式占位符
printf("ptr 指向的值是 %d \n", *ptr);
return 0;
}
程序在 VS Code 中的运行结果如下所示:
7.2 通过指针修改指向变量的值
本案例主要展示如何使用指针来修改其所指向变量的值。首先创建一个 double 类型的变量,然后定义两个指针,使它们都指向该变量。接着分别通过这两个指针修改该变量的值,并打印出每次修改后的结果,以此加深对指针操作的理解。
#include
int main()
{
// 创建一个 double 类型的变量 num 并初始化为 2.88
double num = 2.88;
// 创建一个 double 类型的指针 p1,使用取址运算符 & 获取变量 num 的地址,并将其赋值给 p1
// 此时 p1 指向变量 num
double *p1 = #
// 创建一个 double 类型的指针 p2,将 p1 的值(即 num 的地址)赋值给 p2
// 此时 p1 和 p2 都指向变量 num
double *p2 = p1;
// 打印变量 num 的初始值,使用 %.2f 格式占位符保留两位小数
printf("变量 num 的初始值: %.2f \n", num); // 输出:变量 num 的初始值: 2.88
// 使用取值运算符 * 通过指针 p1 修改 num 的值为 3.88
*p1 = 3.88;
// 打印修改后 num 的值
printf("通过指针 p1 修改后 num 的值: %.2f \n", num); // 输出:通过指针 p1 修改后 num 的值: 3.88
// 使用取值运算符 * 通过指针 p2 修改 num 的值,这里采用 += 运算符将 num 的值增加 10
*p2 += 10;
// 打印再次修改后 num 的值
printf("通过指针 p2 再次修改后 num 的值: %.2f \n", num); // 输出:通过指针 p2 再次修改后 num 的值: 13.88
return 0;
}
程序在 VS Code 中的运行结果如下所示:
8 指针的长度
在 C 语言中,指针的长度(即指针变量占用的内存大小)完全由系统架构决定,而与指针所指向的数据类型无关。这是因为指针的本质是存储内存地址的变量。具体规则如下:
32 位系统:指针长度固定为 4 字节(32 位),因为 CPU 最大寻址空间为 2³²(4GB)。64 位系统:指针长度固定为 8 字节(64 位),支持更大的寻址空间(2⁶⁴)。
#include
int main()
{
int *int_ptr; // 定义一个指向 int 类型的指针
char *char_ptr; // 定义一个指向 char 类型的指针
float *float_ptr; // 定义一个指向 float 类型的指针
double *double_ptr; // 定义一个指向 double 类型的指针
// 指针长度与系统架构有关,32 位系统上指针长度为 4 字节,64 位系统上指针长度为 8 字节
// sizeof 运算符直接作用于指针变量时,返回指针本身的长度(而非指向对象的大小)
printf("int 指针长度: %zu 字节\n", sizeof(int_ptr));
printf("char 指针长度: %zu 字节\n", sizeof(char_ptr));
printf("float 指针长度: %zu 字节\n", sizeof(float_ptr));
printf("double 指针长度: %zu 字节\n", sizeof(double_ptr));
return 0;
}
程序在 VS Code 中的运行结果如下所示:
提示:sizeof 运算符与指针变量
当 sizeof 直接作用于指针变量时,返回的是指针本身的长度(即指针占用的内存大小),而非指针所指向对象的大小。
指针变量 → 返回指针长度(系统相关)。具体对象 → 返回对象大小(类型相关)。
9 指针运算
9.1 指针与整数的加减运算
在 C 语言中,指针与整数的加减运算用于调整指针指向的内存地址。其核心规则如下:
运算本质:
加法:指针 + n 表示指针向后移动 n 个元素(非字节)。减法:指针 - n 表示指针向前移动 n 个元素(非字节)。
步长规则:
移动的字节数 = n × sizeof(指针指向的数据类型)。例如:
int * 指针(假设 int 占 4 字节):+1 移动 4 字节,-2 移动 8 字节。char * 指针(char 占 1 字节):+1 移动 1 字节,-2 移动 2 字节。
典型应用:遍历数组时,通过指针加减快速访问连续内存元素。
#include
int main()
{
// 定义包含 5 个整数的数组
int nums[] = {10, 20, 30, 40, 50};
// 初始化指针指向数组首元素
int *ptr = &nums[0]; // 等价于 int *ptr = nums;
// 初始状态
printf("初始状态: ptr=%p, *ptr=%d\n", (void *)ptr, *ptr); // 10
// 指针加 3:移动 3 个 int 元素(3 × 4 = 12 字节)
ptr += 3;
printf("指针加 3 后: ptr=%p, *ptr=%d\n", (void *)ptr, *ptr); // 40
// 指针减 2:移动 2 个 int 元素(2 × 4 = 8 字节)
// *(ptr - 2) 表示访问 ptr 指针前 2 个 int 元素的位置
printf("指针减 2 后: ptr=%p, *ptr=%d\n", (void *)ptr, *(ptr - 2)); // 20
return 0;
}
程序在 VS Code 中的运行结果如下所示:
9.2 指针自增与自减
指针的自增(++)和自减(--)操作本质是通过加减整数实现的,用于移动指针指向的内存地址。其核心规则如下:
运算本质:
自增(ptr++):指针向后移动 1 个元素(非字节)。自减(ptr--):指针向前移动 1 个元素(非字节)。
步长规则:
移动的字节数 = 1 × sizeof(指针指向的数据类型)。例如:
short * 指针(假设 short 占 2 字节):ptr++ 移动 2 字节。int * 指针(假设 int 占 4 字节):ptr-- 移动 4 字节。
边界问题:指针越界后需显式重置(如 ptr-- 或重新赋值),否则访问非法内存可能导致未定义行为。
#include
int main()
{
// 定义包含 5 个 short 类型元素的数组
short nums[] = {10, 20, 30, 40, 50};
const int len = sizeof(nums) / sizeof(nums[0]); // 计算数组长度
// 正序遍历:使用指针自增
short *ptr = &nums[0]; // 指向首元素
printf("正序输出:\n");
for (int i = 0; i < len; i++, ptr++)
{
printf("索引: %d, 地址: %p, 值: %hd\n", i, (void *)ptr, *ptr);
}
// 重置指针:方法 1(自减)或方法 2(直接赋值)
// 方法 1:通过自减回退到最后一个元素
ptr--; // 抵消循环结束时的多余自增
// 方法 2:直接指向末尾(更安全)
// ptr = &nums[len - 1];
// 倒序遍历:使用指针自减
printf("\n倒序输出:\n");
for (int i = len - 1; i >= 0; i--)
{
printf("索引: %d, 地址: %p, 值: %hd\n", i, (void *)ptr, *ptr);
ptr--; // 向前移动
}
return 0;
}
程序在 VS Code 中的运行结果如下所示:
9.3 同类型指针相减
同类型指针相减用于计算两个指针之间的元素距离(非字节差),返回值为 ptrdiff_t 类型(带符号整数)。规则如下:
运算规则:
高位地址 - 低位地址:返回正值(如 ptr2 - ptr1)。低位地址 - 高位地址:返回负值(如 ptr1 - ptr2)。
结果含义:
表示两个指针之间相隔的元素个数,而非字节差。例如:int * 指针相减结果为 3,表示两者间隔 3 个 int 元素。
格式占位符:使用 %td 输出 ptrdiff_t 类型(如 printf("%td", ptr2 - ptr1))。
#include
int main()
{
// 示例 1:整型数组指针相减
int nums[] = {10, 20, 30, 40, 50};
int *ptr1 = &nums[0]; // 指向首元素
int *ptr2 = &nums[3]; // 指向第 4 个元素
printf("ptr1 地址: %p\n", (void *)ptr1);
printf("ptr2 地址: %p\n", (void *)ptr2);
// 计算指针距离(相隔 3 个 int 元素)
printf("ptr2 - ptr1 = %td\n", ptr2 - ptr1); // 输出 3
printf("ptr1 - ptr2 = %td\n\n", ptr1 - ptr2); // 输出 -3
// 示例 2:double 变量指针相减
double d1 = 1.0, d2 = 2.0;
double *p1 = &d1; // 指向 d1
double *p2 = &d2; // 指向 d2
printf("p1 地址: %p\n", (void *)p1);
printf("p2 地址: %p\n", (void *)p2);
// 注意:C 标准未规定连续变量地址的间隔,结果依赖编译器和平台!
// 以下操作可能无意义或引发未定义行为!
printf("p1 - p2 = %td\n", p1 - p2); // 未定义行为
printf("p2 - p1 = %td\n", p2 - p1); // 未定义行为
return 0;
}
程序在 VS Code 中的运行结果如下所示:
提示:C 标准未规定连续变量地址的相邻性
在 C 语言中,连续定义的变量(如 double d1, d2;)的地址是否相邻是由编译器和平台决定的,C 标准并未强制规定这一点。因此,直接对不同类型变量的指针进行算术运算(如 p1 - p2)可能是无意义的,甚至会导致未定义行为。
9.4 指针的比较运算
指针之间支持比较运算(==、!=、<、<=、>、>=),用于判断指针指向的内存地址关系。规则如下:
运算规则:
比较的是指针的地址值(如 0x100000 与 0x100004),而非存储的内容。返回值为 int 类型(1 表示 true,0 表示 false)。
注意事项:
同类型指针:比较有意义(如两个 int * 指针)。不同类型指针:比较可能导致未定义行为或编译器警告(应避免)。
#include
int main()
{
// 定义整型数组和 double 变量
int nums[] = {10, 20, 30, 40, 50};
double n = 1.0;
// 初始化指针
int *ptr1 = &nums[0]; // 指向首元素
int *ptr2 = &nums[3]; // 指向第 4 个元素
int *ptr3 = &nums[0]; // 同样指向首元素
double *ptr4 = &n; // 指向 double 变量
// 打印指针地址
printf("ptr1地址: %p\n", (void *)ptr1);
printf("ptr2地址: %p\n", (void *)ptr2);
printf("ptr3地址: %p\n", (void *)ptr3);
printf("ptr4地址: %p\n\n", (void *)ptr4);
// 同类型指针比较
printf("ptr1 > ptr2: %d\n", ptr1 > ptr2); // 0(ptr1 地址更小)
printf("ptr1 < ptr2: %d\n", ptr1 < ptr2); // 1(ptr1 地址更小)
printf("ptr1 == ptr3: %d\n", ptr1 == ptr3); // 1(指向同一地址)
// 不同类型指针比较(可能引发警告)
printf("ptr4 > ptr1: %d\n", ptr4 > ptr1); // 结果依赖编译器实现(不推荐)
return 0;
}
程序在 VS Code 中的运行结果如下所示:
注意:不同类型的指针进行比较会引发编译器警告,如下图所示: