指针是C语言的灵魂,它赋予了程序员直接操作内存的能力,是实现高效、灵活程序的关键。理解指针,是通往C语言高手之路的必经关卡。
一、指针的本质:内存地址与间接访问
1.1 基本概念
- 内存地址:内存中每个字节的唯一编号,通常以16进制数表示,如0x1254ff。
- 指针变量:一种特殊的变量,其存储的值是另一个变量的内存地址。
- 核心区别:普通变量存储数据值,指针变量存储地址值。
1.2 声明与初始化
指针声明必须指定其指向的数据类型,并使用 * 符号。
// 声明指针
int *int_ptr; // 指向整型
char *char_ptr; // 指向字符
float *float_ptr; // 指向浮点型
// 初始化示例
int main() {
int value = 100;
int *ptr = &value; // & 取地址运算符
printf("value = %d\n", value);
printf("value的地址 = %p\n", &value);
printf("ptr存储的地址 = %p\n", ptr);
printf("*ptr = %d\n", *ptr); // * 解引用运算符
return 0;
}
1,4;2,3行将打印相同的结果
关键点:
&:获取变量的内存地址*:通过地址访问对应变量的值- 指针大小(32位系统4字节,64位系统8字节)与指向类型无关**
二、野指针 vs 空指针
2.1 野指针:
野指针指向无效内存区域,是程序崩溃和数据损坏的常见原因
// 三种典型的野指针场景
int *p1; // 未初始化,随机指向
*p1 = 10; // 危险!操作随机内存
int *p2 = malloc(sizeof(int));
free(p2); // 内存已释放
*p2 = 20; // 危险!操作已释放内存
int arr[5] = {0};
int *p3 = arr + 10; // 指针越界
*p3 = 30; // 危险!操作越界内存
规避策略:
- 声明时立即初始化
- 释放内存后立即置空
- 严格检查指针边界
2.2 空指针:明确的"无指向"状态
空指针是合法的无效指针,通常用 NULL 表示。
int *p = NULL; // 安全的初始化
// 正确用法
if (p != NULL) {
*p = 100; // 仅在指针有效时操作
}
// 动态内存分配的检查
int *ptr = malloc(sizeof(int) * 100);
if (ptr == NULL) {
perror("内存分配失败");
exit(EXIT_FAILURE);
}
| 特性 | 野指针 | 空指针 |
|---|---|---|
| 合法性 | 非法 | 合法 |
| 指向 | 随机地址 | 固定NULL |
| 操作后果 | 未定义行为 | 可预判的异常 |
| 预防 | 需主动避免 | 可主动使用 |
不过,C语言的NULL也有其弊端,这一点在C++中引入的nullptr得到解决,这里不做详细解释。
三、指针运算:内存地址的精确导航
3.1 算术运算:基于类型大小的偏移
指针加减整数不是简单的数值运算,而是按指向类型大小进行偏移。
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr; // 指向arr[0]
printf("arr[0]: %d at %p\n", *p, p);
p++; // 偏移sizeof(int)字节,指向arr[1]
printf("arr[1]: %d at %p\n", *p, p);
// 不同类型的偏移量不同
char *char_ptr = (char*)arr;
char_ptr++; // 仅偏移1字节
3.2 指针比较与关系运算
指针比较仅在同数组或同内存块中有意义。
int arr[10];
int *start = arr;
int *end = arr + 9; // 指向最后一个元素
// 遍历数组的指针版本
while (start <= end) {
printf("%d ", *start);
start++;
}
四、内存视角:大小端字节序
大小端决定了多字节数据在内存中的存储顺序,直接影响跨平台数据交换。
// 检测系统字节序
int a=0x11223344;
char*p=(char*)&a;
if(*p==0x44)
{printf("小端\n");}// 小端:低位在低地址
else
{printf("大端\n");}//大端:低位在高地址
五、指针与数组
5.1 一维数组:数组名即常量指针
int arr[5] = {1, 2, 3, 4, 5};
// 三种等价的访问方式
arr[2] = 100; // 数组下标
*(arr + 2) = 100; // 指针算术
int *p = arr;
p[2] = 100; // 指针下标
// 数组名是常量,不能修改
// arr++; // 错误:数组名不是左值
5.2 二维数组:数组的数组
二维数组需要理解其内存布局和指针类型。
int matrix[2][3] = {{1, 2, 3}, {4, 5, 6}};
// 方法1:行指针
int (*row_ptr)[3] = matrix;
printf("matrix[1][2] = %d\n", *(*(row_ptr + 1) + 2));
// 方法2:普通指针(连续内存视角)
int *flat_ptr = (int*)matrix;
for (int i = 0; i < 6; i++) {
printf("%d ", flat_ptr[i]);}
有关数组与指针的其他内容,可参考C语言:数组2.5,2.6部分。
六、函数指针
6.1 修改外部变量
// 交换两个变量的经典示例
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
// 返回多个值
int divide(int a, int b, int *remainder) {
*remainder = a % b; // 通过指针"返回"余数
return a / b; // 通过返回值返回商
}
6.2 数组参数传递
数组作为函数参数时自动退化为指针。
// 以下三种声明完全等价
void process_array(int arr[], int size);
void process_array(int *arr, int size);
void process_array(int arr[10], int size); // 10被忽略
// 关键:函数内无法通过sizeof获取数组真实大小
void print_array(int arr[], int size) {
// printf("%zd\n", sizeof(arr)); // 错误:返回指针大小,4或8
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
}
七、指针数组
7.1 字符串数组的优雅实现
// 比char strings[3][20]更高效
char *strings[] = {
"Hello",
"World",
"C Language"
};
// 遍历
for (int i = 0; i < 3; i++) {
printf("%s\n", strings[i]);
}
7.2 多数组操作
int arr1[] = {1, 2, 3};
int arr2[] = {4, 5, 6};
int arr3[] = {7, 8, 9};
int *arrays[] = {arr1, arr2, arr3};
int sizes[] = {3, 3, 3};
for (int i = 0; i < 3; i++) {
for (int j = 0; j < sizes[i]; j++) {
printf("%d ", arrays[i][j]);
}
printf("\n");
}
八、const修饰符
const与指针的组合有三种形式,保护对象不同。
int value = 10;
int other = 20;
// 1. 指向常量的指针:保护数据
const int *p1 = &value;
// *p1 = 30; // 错误:不能修改数据
p1 = &other; // 正确:可以改变指向
// 2. 指针常量:保护指针
int *const p2 = &value;
*p2 = 30; // 正确:可以修改数据
// p2 = &other; // 错误:不能改变指向
// 3. 指向常量的指针常量:全保护
const int *const p3 = &value;
// *p3 = 30; p3 = &other;
// 错误,指针与其值均不可修改
// 函数中的典型应用:只读参数
void print_data(const int *data, int size) {
// 函数承诺不修改data指向的内容
for (int i = 0; i < size; i++) {
printf("%d ", data[i]);
}
}
| 类型 | 语法 | 指针可变 | 数据可变 | 用途 |
|---|---|---|---|---|
| 指向常量的指针 | const int *p | ✓ | ✗ | 函数只读参数 |
| 指针常量 | int *const p | ✗ | ✓ | 固定缓冲区指针 |
| 指向常量的指针常量 | const int *const p | ✗ | ✗ | 只读配置数据 |
plus 一道小题:以下代码会打印什么?为什么?
char str1[] = "hello";
char str2[] = "hello";
char* str3= "hello";
char* str4 = "hello";
printf("%d", (str1 == str2));
printf("%d", (str3 == str4));
答案:01
分析:
str1 和 str2 是字符数组,它们分别被初始化为 “hello”,在内存中,它们是两个不同的数组,拥有不同的内存地址,数组名在此情况退化为指向其首元素的指针,str1 == str2 比较的是两个数组的起始地址,它们不同,所以结果为 0;
str3 和 str4 是字符指针,它们都指向字符串字面量 “hello”,编译相同的字符串常量通常存储在RODATA区域的同一个位置,因此 str3 和 str4 指向相同的地址,结果为 1。
前者发生了内存从RODATA到栈的拷贝,后者未发生。
4169

被折叠的 条评论
为什么被折叠?



