【C语言】指针

指针是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;                // 危险!操作越界内存

规避策略

  1. 声明时立即初始化
  2. 释放内存后立即置空
  3. 严格检查指针边界

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到栈的拷贝,后者未发生。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值