:内存地址到数组指针的核心逻辑)
C 语言指针篇上基础核心 数组与指针基础指针是 C 语言的核心与精髓也是入门阶段的重点难点其本质围绕内存地址展开 —— 所有指针操作的底层都是对内存地址的访问和操作。本篇聚焦指针基础核心知识点从内存地址的底层逻辑到指针变量的基础使用再到数组与指针的关联应用同时重点补充assert断言的实用技巧吃透这些才能为后续进阶指针内容打好基础。一、内存和地址指针的本质是地址理解内存与地址的底层逻辑是掌握指针的第一步内存的本质计算机的存储空间用于存放程序运行时的数据最小存储单位是字节Byte1 字节 8 比特bit。地址的概念为了高效访问内存中的每个字节系统会给每个字节分配一个唯一的编号这个编号就是内存地址简称地址。地址的表示以十六进制数形式呈现如0x0060FE00不同系统的地址位数不同32 位系统地址为 32 位64 位系统为 64 位。地址与变量的关系定义变量时系统会为变量分配一块连续的内存空间变量的地址就是其占用内存空间的首字节地址。二、指针变量和地址地址就是指针而指针变量是专门存储内存地址的变量核心操作围绕两个符号展开指针的本质地址 指针指针变量 存储内存地址的变量。核心符号取地址符获取变量 / 数组的内存地址操作数必须是有实体的变量不能是常量 / 表达式。*解引用符 / 间接访问符通过指针变量中存储的地址访问该地址指向的内存空间解引用。指针变量的定义格式数据类型 * 指针变量名;示例int *p; // 定义一个指向 int 类型变量的指针变量 p基础使用示例#include stdio.h #include assert.h // 引入assert头文件 int main() { int a 10; int *p a; // 取a的地址赋值给指针变量p // 解引用前断言确保指针非空 assert(p ! NULL); *p 20; // 解引用p修改p指向的地址对应的内存值即修改a的值 printf(%d, a); // 输出20 return 0; }指针变量的大小指针变量的大小由系统地址位数决定与指向的数据类型无关。32 位系统占 4 字节64 位系统占 8 字节可通过sizeof验证。三、指针变量类型的意义指针变量的类型如int*/char*/double*并非无意义而是决定了指针的两个核心行为关键知识点决定解引用的权限解引用时能访问的内存字节数等于指向的数据类型的大小。示例int* p解引用时访问 4 字节int 占 4 字节char* p解引用时访问 1 字节char 占 1 字节。决定指针运算的步长指针 ± 整数时地址偏移的字节数 整数 × 指向的数据类型的大小。示例int* p执行p地址偏移 4 字节char* p执行p地址偏移 1 字节。核心结论指针的类型决定了指针 “怎么走”运算步长和 “看多少”解引用权限。补充void* 指针指针类型中有一个特殊类型void *可理解为无具体类型的指针泛型指针优点可以接收任意类型的地址局限性不能直接进行指针 ± 整数和解引用运算。四、指针运算指针的运算并非普通算术运算仅支持三种合法运算非法运算会导致程序崩溃或未定义行为指针 ± 整数核心是地址的偏移步长由指针类型决定常用于遍历数组。示例int arr[5] {1,2,3,4,5}; int *p arr; p2;// 地址偏移 8 字节指向 arr [2]指针 - 指针仅当两个指针指向同一个数组时合法结果为两个指针之间的元素个数而非字节数常用于计算数组长度。示例int arr[5] {1,2,3,4,5}; printf(%d, arr[4] - arr[0]);// 输出 4指针的关系运算指针之间可比较大小///仅当指向同一个数组时有实际意义用于判断指针遍历位置如是否到达数组末尾。示例for(int *p arr; p arr5; p) { ... }// 遍历数组p 未到末尾则继续注意指针不支持指针指针、指针×整数、指针÷整数等运算无实际意义且非法。五、使用指针访问数组基于 “数组名是首元素地址” 的特性可通过指针完全替代数组下标访问元素以下是两种常见实现方式加入assert判空方法一指针 ± 整数#define _CRT_SECURE_NO_WARNINGS #include stdio.h #include assert.h // 引入assert头文件 void Print(int *ptr) { // 断言确保传入的指针非空 assert(ptr ! NULL); for (int i 0; i 5; i) { printf(%d , *(ptr i)); // 指针整数偏移访问元素 } printf(\n); } int main() { int arr[] { 10, 20, 30, 40, 50 }; int n sizeof(arr) / sizeof(arr[0]); Print(arr); // 数组名传参本质是传首元素地址 return 0; }方法二指针的关系运算#define _CRT_SECURE_NO_WARNINGS #include stdio.h #include assert.h // 引入assert头文件 void Print(int *ptr, int n) { // 双重断言指针非空 数组长度合法 assert(ptr ! NULL); assert(n 0); int* q ptr; while (ptr q n) { // 指针比较判断是否遍历完成 printf(%d , *ptr); ptr; // 指针自增步长4字节int类型 } printf(\n); } int main() { int arr[] { 10, 20, 30, 40, 50 }; int n sizeof(arr) / sizeof(arr[0]); Print(arr, n); return 0; }六、模拟 strlen指针实现strlen的核心是遍历字符数组直到\0以下是两种指针实现方式加入assert判空方法一计数法#include stdio.h #include assert.h // 引入assert头文件 size_t my_strlen(char *p) { // 断言确保传入的字符串指针非空 assert(p ! NULL); int count 0; while (*p) { // 遍历直到遇到\0*p为0时终止 count; p; } return count; } int main() { char arr[] abcdef; size_t len my_strlen(arr); printf(%zu\n, len); // 输出6 return 0; }方法二指针 - 指针#include stdio.h #include assert.h // 引入assert头文件 size_t my_strlen(char* p) { // 断言确保传入的字符串指针非空 assert(p ! NULL); char* q p; // 保存起始地址 while (*p) { p; } return (p - q); // 指针相减得到元素个数字符串长度 } int main() { char arr[] abcdef; size_t len my_strlen(arr); printf(%zu\n, len); // 输出6 return 0; }七、const 修饰指针const修饰指针的核心是限制修改权限关键规则const靠谁谁就不能改。根据修饰位置不同分为三种情况const 修饰指针指向的内容const 数据类型 * 指针名/数据类型 const * 指针名特点指针变量本身可改可指向其他地址但指向的内容不能通过解引用修改。示例const int *p a; *p 20;错误p b;正确。const 修饰指针变量本身数据类型 * const 指针名特点指针变量本身不可改不能指向其他地址但指向的内容可通过解引用修改。示例int * const p a; p b;错误*p 20;正确。const 既修饰内容又修饰指针const 数据类型 * const 指针名特点指针变量和指向的内容都不可改双重限制。示例const int * const p a; *p 20; p b;两行均错误。八、野指针野指针是指向非法 / 随机 / 已释放内存地址的指针解引用野指针是 C 语言常见致命错误需重点规避野指针的产生原因指针变量未初始化int *p; *p 10;p 为随机地址是野指针指针越界访问int arr[5]; int *p arr; for(int i0; i6; i) p;循环后 p 越界指针指向的内存被释放内存释放后未将指针置空指针仍指向原非法地址。野指针的规避方法指针定义时立即初始化赋值合法地址或NULL指针越界检查遍历内存时严格限制范围内存释放后立即置空p NULL;避免悬空指针解引用前判空if(p ! NULL) { *p 20; }结合assert效果更佳。NULL 的本质C 语言宏定义值为 0代表空指针指向内存地址 0系统保留不可访问空指针解引用也报错但可通过判空规避。九、assert 断言重点补充assert是调试阶段的断言检查用于快速定位指针空值、条件非法等问题是 C 语言调试的 “利器”9.1 assert 核心特性assert()对程序员非常友好核心好处自动标识文件名和出错行号快速定位问题无需修改代码通过宏定义即可开启 / 关闭所有assert()语句仅在调试模式生效不影响发布版本的性能。9.2 基础使用头文件#include assert.h语法assert(表达式);表达式为真程序继续执行无任何影响表达式为假程序立即终止打印错误信息含文件名、行号、断言表达式。9.3 开启 / 关闭 assert启用 assert直接引入assert.h即可默认启用禁用 assert在assert.h前定义宏NDEBUGNo Debug。示例 1启用 assert调试模式#include stdio.h #include assert.h // 直接引入启用assert void Swap(int *x, int *y) { // 断言确保传入的指针非空 assert(x ! NULL); assert(y ! NULL); int temp *x; *x *y; *y temp; } int main() { int a 10, b 20; int *p NULL; // 故意赋值为NULL模拟错误 Swap(p, b); // 触发assert断言 printf(a%d, b%d, a, b); return 0; }运行结果触发断言Assertion failed: (x ! NULL), function Swap, file test.c, line 6.示例 2禁用 assert发布模式#include stdio.h #define NDEBUG // 定义NDEBUG禁用所有assert #include assert.h // 引入assert头文件 void Swap(int *x, int *y) { // 断言被禁用不会执行检查 assert(x ! NULL); assert(y ! NULL); int temp *x; *x *y; *y temp; } int main() { int a 10, b 20; int *p NULL; Swap(p, b); // assert被禁用程序继续执行但会触发野指针错误 printf(a%d, b%d, a, b); return 0; }说明发布版本中assert被完全优化掉无性能损耗若后续程序出问题只需注释 / 删除#define NDEBUG重新编译即可重新启用断言。9.4 assert 最佳实践解引用指针前必加assert(p ! NULL)数组传参时断言数组长度assert(n 0)仅用于 “调试阶段的非法条件检查”不替代正常的业务逻辑判断如if。十、指针的使用和传址调用C 语言函数传参分为传值调用和传址调用指针的核心应用之一是实现传址调用加入assert判空传值调用形参是实参的拷贝形参修改仅在函数内部生效无法修改实参传址调用形参为指针变量接收实参地址函数内通过解引用修改实参的原始内存空间经典示例交换两个整数#include stdio.h #include assert.h // 引入assert头文件 void Swap(int *x, int *y) { // 断言确保指针非空避免野指针 assert(x ! NULL); assert(y ! NULL); int temp *x; *x *y; *y temp; } int main() { int a 10, b 20; Swap(a, b); // 传址传入a和b的地址 printf(a%d, b%d, a, b); // 输出a20, b10 return 0; }传址调用场景需要修改实参、传递大体积数据数组 / 结构体避免拷贝节省内存。十一、数组名的理解数组名的本质是数组首元素的地址常量地址不可修改如arr非法但存在两个例外数组名表示整个数组普通情况数组名 首元素地址。示例int arr[5]; int *p arr;等价于int *p arr[0];例外 1sizeof(数组名)计算整个数组的内存大小字节数。示例int arr[5]; printf(%d, sizeof(arr));输出 205×4例外 2数组名取整个数组的地址地址值与首元素地址相同但类型不同。示例int arr[5]; printf(%p %p, arr, arr);输出两个相同的地址值。十二、一维数组传参的本质一维数组传参时编译器会将数组名解析为首元素的地址因此传参本质是传址调用形参有三种等价写法数组形式void fun(int arr[], int n) { ... }[]内数字可省略编译器忽略指定大小的数组形式void fun(int arr[5], int n) { ... }仅作提示无实际限制指针形式void fun(int *arr, int n) { ... }最推荐明确表示接收地址。核心结论函数内部无法通过sizeof(arr)获取数组总大小只能获取指针大小必须额外传递数组长度且建议在函数开头加assert(arr ! NULL)和assert(n 0)做合法性检查。十三、排序算法的指针实现含 assert 判空指针操作数组的经典应用是排序算法以下是冒泡排序、选择排序、插入排序的指针实现均加入assert断言1. 冒泡排序核心思想相邻元素两两比较大元素后移每轮将最大元素 “冒泡” 到末尾。#include stdio.h #include assert.h // 引入assert头文件 // 冒泡排序形参为指针接收数组首元素地址 数组长度 void BubbleSort(int *arr, int n) { // 断言数组指针非空 长度合法 assert(arr ! NULL); assert(n 0); for(int i0; in-1; i) { int flag 1; // 优化标记是否已有序 for(int j0; jn-1-i; j) { // 指针访问arr[j]和arr[j1]*(arrj) 和 *(arrj1) if(*(arrj) *(arrj1)) { int temp *(arrj); *(arrj) *(arrj1); *(arrj1) temp; flag 0; // 发生交换标记为无序 } } if(flag 1) break; // 无交换直接退出 } } int main() { int arr[6] {5,3,4,1,2,6}; int n sizeof(arr)/sizeof(arr[0]); BubbleSort(arr, n); for(int i0; in; i) { printf(%d , arr[i]); // 输出1 2 3 4 5 6 } return 0; }2. 选择排序核心思想分 “已排序 / 未排序区间”每轮找未排序区间最小值与未排序区间首个元素交换。#include stdio.h #include assert.h // 引入assert头文件 void SelectSort(int *arr, int n) { // 断言数组指针非空 长度合法 assert(arr ! NULL); assert(n 0); for (int i 0; i n - 1; i) { int *min_ptr arr i; // 指针跟踪最小值地址 for (int j i 1; j n; j) { if (*(arr j) *min_ptr) { min_ptr arr j; // 更新最小值地址 } } // 交换最小值与未排序区间首个元素 if (min_ptr ! arr i) { int temp *(arr i); *(arr i) *min_ptr; *min_ptr temp; } } } int main() { int arr[6] {5, 3, 4, 1, 2, 6}; int n sizeof(arr) / sizeof(arr[0]); SelectSort(arr, n); for(int i0; in; i) { printf(%d , arr[i]); // 输出1 2 3 4 5 6 } return 0; }3. 插入排序核心思想分 “已排序 / 未排序区间”逐次取出未排序区间首个元素插入到已排序区间的正确位置。#include stdio.h #include assert.h // 引入assert头文件 void InsertSort(int *arr, int n) { // 断言数组指针非空 长度合法 assert(arr ! NULL); assert(n 0); for (int i 1; i n; i) { int insert_val *(arr i); // 取出未排序区间首个元素 int *pre_ptr arr i - 1; // 指向已排序区间末尾 // 向前找插入位置元素后移 while (pre_ptr arr *pre_ptr insert_val) { *(pre_ptr 1) *pre_ptr; pre_ptr--; } *(pre_ptr 1) insert_val; // 插入元素 } } int main() { int arr[6] {5, 3, 4, 1, 2, 6}; int n sizeof(arr) / sizeof(arr[0]); InsertSort(arr, n); for(int i0; in; i) { printf(%d , arr[i]); // 输出1 2 3 4 5 6 } return 0; }十四、二级指针二级指针是指向指针变量的指针实际开发中最多用到二级指针加入assert判空定义格式数据类型 ** 二级指针名;示例int a 10; int *p a; int **pp p;核心符号使用*pp解引用二级指针得到一级指针p的值即a的地址等价于p**pp二次解引用得到a的值等价于*p和a修改示例#include stdio.h #include assert.h // 引入assert头文件 int main() { int a 10, b 20; int *p a; int **pp p; // 断言一级指针和二级指针均非空 assert(p ! NULL); assert(pp ! NULL); *pp b; // 等价于p b修改一级指针指向 **pp 30; // 等价于*p 30修改b的值 printf(%d, b); // 输出30 return 0; }应用场景修改一级指针的指向、遍历指针数组等。十五、指针数组指针数组是存放指针的数组本质是数组元素为指针定义格式数据类型 * 数组名[数组长度];[]优先级高于*示例int *arr[3];3 个元素每个元素是int*类型指针基础使用存放变量地址#include stdio.h #include assert.h // 引入assert头文件 int main() { int a 1, b 2, c 3; int *pArr[3] {a, b, c}; // 元素存放a、b、c的地址 for(int i0; i3; i) { // 断言指针数组的每个元素非空 assert(pArr[i] ! NULL); printf(%d , *(pArr[i])); // 输出1 2 3 } return 0; }常用场景存放多个字符串的首地址比二维字符数组节省内存示例char *strArr[3] {abc, def, ghi};每个元素是字符串首地址。十六、指针数组模拟二维数组指针数组可通过指向多个一维数组模拟二维数组内存不连续比真正二维数组更灵活#include stdio.h #include assert.h // 引入assert头文件 int main() { // 定义3个一维数组作为行 int row1[3] {1,2,3}; int row2[3] {4,5,6}; int row3[3] {7,8,9}; // 指针数组每个元素指向一维数组首地址 int *pArr[3] {row1, row2, row3}; // 模拟二维数组访问pArr[i][j] 等价于 *(*(pArri)j) for(int i0; i3; i) { // 断言指针数组的行指针非空 assert(pArr[i] ! NULL); for(int j0; j3; j) { printf(%d , pArr[i][j]); } printf(\n); } return 0; }核心区别真正二维数组内存连续所有行紧密排列指针数组模拟内存不连续每行是独立一维数组指针数组仅存行地址。上篇小结本篇围绕指针基础核心展开核心要点总结指针的本质是地址指针类型决定了解引用权限看多少和运算步长怎么走assert(p ! NULL)是规避野指针的关键技巧调试阶段启用、发布阶段可通过NDEBUG禁用无性能损耗数组名的本质是首元素地址常量仅sizeof(数组名)和数组名表示整个数组一维数组传参本质是传首元素地址函数内需额外传递数组长度且建议通过assert做指针和长度的合法性检查指针的核心应用包括传址调用修改实参、遍历数组、实现排序算法等二级指针和指针数组是基础拓展需重点理解 “指向关系”。吃透以上基础才能顺利进入指针进阶内容如函数指针、二维数组与指针、动态内存分配等。那我们下期再见