C 语言第 17 天学习笔记:从二级指针到内存布局的进阶指南
C语言进阶:从二级指针到内存布局的深度解析
一、指针进阶:二级指针的应用与特性
1.1 二级指针的定义与基本用法
二级指针(多重指针)用于存储一级指针的地址,需要两次次解引用才能访问原始数据。在实际开发中,二级指针是最常见的多重指针类型。
int a = 10; // 普通变量,原始数据
int *p = &a; // 一级指针,指向a,一次解引用可获取a的值
printf("%d\n", *p); // 输出:10int **w = &p; // 二级指针,指向p,两次解引用可获取a的值
printf("%d\n", **w); // 输出:10int ***k = &w; // 三级指针,指向w,三次解引用可获取a的值
printf("%d\n", ***k); // 输出:10
1.2 二级指针的语法与特点
语法格式:
数据类型 **指针变量名 = 指针数组的数组名 | 一级指针的地址
核心特点:
-
与指针数组的等效性
二级指针与指针数组在某些场景下可以等效使用,但与二维数组不等效。// 指针数组示例 int arr[] = {11, 22, 33}; int *arr_[] = {&arr[0], &arr[1], &arr[2]};// 二级指针接收指针数组 char *str[3] = {"abc", "aaa034", "12a12"}; char **p = str; // p存储数组首地址,*p访问列地址,**p访问列元素
-
与二维数组的差异
二维数组名是数组指针类型(如int (*)[3]
),直接赋值给二级指针会导致类型不匹配:int arr[2][3] = {{1, 3, 5}, {11, 33, 55}}; int (*p)[3] = arr; // 正确:数组指针指向二维数组 int **k = arr; // 错误:类型不兼容(int(*)[3] 与 int**)
1.3 二级指针的解引用操作
字符型二级指针可直接遍历字符串数组,操作类似一维数组:
#include <stdio.h>void fun2() {char *arr[] = {"orange", "apple", "grape", "banana", "kiwi"};int len = sizeof(arr) / sizeof(arr[0]);char **p = arr; // 二级指针等价于指针数组for (int i = 0; i < len; i++) {printf("%s\n", *(p + i)); // 指针法访问}
}
其他类型二级指针需要两次解引用访问数据,常用于操作指针数组:
#include <stdio.h>int main() {int arr1[] = {11, 22, 33, 44, 55, 66};int *arr[] = {&arr1[0], &arr1[1], &arr1[2], &arr1[3], &arr1[4], &arr1[5]};int **p = arr; // 二级指针接收指针数组for(int i = 0; i < sizeof(arr)/sizeof(arr[0]); i++){printf("%-6d", **(p+i)); // 指针法访问}
}
二、main函数原型解析
main函数作为程序入口,有多种定义格式,其中标准写法为:
int main(int argc, char *argv[]) { ... }
// 或等价形式
int main(int argc, char **argv) { ... }
2.1 参数含义与用法
-** argc :存储命令行参数的个数,默认值为1(程序名本身)
- argv **:存储所有参数的字符串形式,是一个字符型指针数组
示例代码:
#include <stdio.h>int main(int argc, char **argv) {printf("参数个数: %d\n", argc);for(int i = 0; i < argc; i++){printf("参数%d: %s\n", i, argv[i]);}return 0;
}
三、常量指针与指针常量
3.1 常量指针(指向常量的指针)
定义:指向常量数据的指针,指针指向的数据不可修改,但指针本身的指向可以改变。
语法:
const 数据类型 *变量名;
// 或
const 数据类型* 变量名;
示例:
int a = 10, b = 20;
const int *p = &a;
// *p = 30; // 错误:不能修改指向的数据
p = &b; // 正确:可以改变指向
3.2 指针常量(指针本身是常量)
定义:指针本身是常量,指向固定地址,但指向的数据可以修改。
语法:
数据类型* const 变量名;
// 或
数据类型 *const 变量名;
示例:
int a = 10, b = 20;
int* const p = &a;
*p = 30; // 正确:可以修改指向的数据
// p = &b; // 错误:不能改变指向
3.3 常量指针常量
定义:指针指向和指向的数据都不可改变。
语法:
const 数据类型* const 变量名;
示例:
int a = 10, b = 20;
const int* const p = &a;
// *p = 30; // 错误
// p = &b; // 错误
3.4 总结对比
类型 | 语法 | 指向可变 | 数据可变 |
---|---|---|---|
常量指针 | const int *p | ✔️ | ❌ |
指针常量 | int *const p | ❌ | ✔️ |
常量指针常量 | const int *const p | ❌ | ❌ |
四、野指针、空指针与空悬指针
4.1 野指针
定义:指向无效内存区域的指针(未初始化、已释放或越界访问)。
产生场景:
-
指针未初始化
int *p; // 野指针 printf("%d\n", *p); // 危险操作
-
指针指向已释放的内存
int *p = malloc(sizeof(int)); free(p); printf("%d\n", *p); // 危险操作
-
返回局部变量的地址
int* fun() {int sum = 0;return ∑ // 危险:返回局部变量地址 }
避免方法:
- 初始化指针为NULL
- 释放内存后立即置为NULL
- 避免返回局部变量地址
- 使用前检查指针有效性
4.2 空指针
定义:值为NULL的指针,指向地址0x00000000(系统保留,不可访问)。
作用:明确表示指针当前不指向有效内存,用于指针初始化。
int *p = NULL; // 初始化为空指针
4.3 空悬指针
定义:指针指向的内存已被释放,但未重新赋值,是野指针的一种特例。
char *p = malloc(100);
free(p); // 释放内存后,p成为空悬指针
五、void与void*的区别
5.1 void类型
表示"无类型/空类型",用于函数返回类型或参数:
void func(void); // 无返回值,无参数
5.2 void*类型(通用指针)
可指向任意类型数据,但需要强制类型转换后才能解引用:
void* ptr = malloc(4); // 分配4字节内存// 存储int类型
int *p = (int*)ptr;
*p = 10;// 存储float类型
float* p1 = (float*)ptr;
*p1 = 12.5f;
注意:void*
只能与具体类型指针(int*
、double*
等)之间进行转换。
六、C语言内存管理
6.1 进程内存布局
每个C语言进程拥有结构相同的虚拟内存,包含以下区域:
-** 栈(stack):存储环境变量、命令行参数、局部变量
- 堆(heap):动态内存区域,可由开发者管理
- 数据段 :包含.bss段(未初始化静态数据)、.data段(已初始化静态数据)、.rodata段(常量数据)
- 代码段 **:包含.text段(用户代码)、.init段(系统初始化代码)
6.2 各内存区域特性
栈内存:
- 空间有限,自动分配和释放
- 函数调用时栈向下增长,函数退出时栈向上缩减
静态数据:
- 包括全局变量和static修饰的局部变量
- 程序启动时分配,程序退出时释放
- 未初始化时自动初始化为0
堆内存:
- 大小受限于物理内存
- 从下往上增长
- 需要开发者手动申请和释放
- 相关API:
malloc()
、calloc()
、realloc()
、free()
示例:
// 堆内存操作示例
int *p = malloc(sizeof(int)); // 申请内存
*p = 100; // 存储数据
free(p); // 释放内存
p = NULL; // 避免空悬指针// 连续内存申请
double *k = calloc(3, sizeof(double)); // 已清零
k[0] = 0.618;
free(k);
七、总结
本文深入探讨了C语言中的二级指针、main函数原型、常量指针与指针常量、各类特殊指针以及内存布局等进阶知识。掌握这些概念对于理解C语言的内存管理机制、编写高效安全的代码具有重要意义。特别是内存管理部分,需要开发者手动管理堆内存,这既是C语言的灵活性所在,也是容易出现bug的地方,需要格外注意。