深入理解指针2

深入理解指针(二):const、野指针、assert与传址调用

指针是C语言的灵魂,也是无数初学者的噩梦。上一篇我们聊了指针的基础概念,这一篇我们深入四个关键话题:const修饰指针、野指针、assert断言,以及指针的使用和传址调用。掌握这些,你的指针功力至少提升一个档次。


目录

  • 一、const修饰指针
    • [1. const 在 * 左边](#1. const 在 * 左边)
    • [2. const 在 * 右边](#2. const 在 * 右边)
    • [3. 两边都有 const](#3. 两边都有 const)
    • [4. 为什么要用 const 修饰指针?](#4. 为什么要用 const 修饰指针?)
  • 二、野指针
    • [2.1 野指针的常见成因](#2.1 野指针的常见成因)
    • [2.2 如何规避野指针?](#2.2 如何规避野指针?)
  • 三、assert断言
    • [1. assert 是什么?](#1. assert 是什么?)
    • [2. 为什么需要 assert?](#2. 为什么需要 assert?)
    • [3. 在指针中的典型应用](#3. 在指针中的典型应用)
    • [4. assert 的使用原则](#4. assert 的使用原则)
    • [5. 补充:C11 的静态断言](#5. 补充:C11 的静态断言)
  • 四、指针的使用和传址调用
    • [4.1 经典例子:交换两个数](#4.1 经典例子:交换两个数)
    • [4.2 通过指针返回多个值](#4.2 通过指针返回多个值)
    • [4.3 数组传参的本质](#4.3 数组传参的本质)
  • 总结

一、const修饰指针

const 是C语言中的常量修饰符,当它和指针结合时,位置不同,含义天差地别。很多人被 const int *p、int * const p、const int * const p 绕晕,其实记住一句话就行:

左定值,右定向。

const 在 * 左边,修饰的是指针指向的内容,内容不可改;

const 在 * 右边,修饰的是指针本身,指针指向不可改。


1. const 在 * 左边

c 复制代码
#include <stdio.h>

int main(void)
{
    int a = 10;
    int b = 20;

    const int *p = &a;   /* 等价于 int const *p */
    // *p = 30;          /* 错误:不能通过 p 修改 a 的值 */
    printf("初始: *p = %d\n", *p);

    p = &b;              /* 正确:p 本身可以指向别处 */
    printf("改变指向后: *p = %d\n", *p);

    return 0;
}

此时 p 指向的内容是只读的,但 p 自己可以改变指向。

2. const 在 * 右边

c 复制代码
#include <stdio.h>

int main(void)
{
    int a = 10;
    int b = 20;

    int * const p = &a;
    *p = 30;             /* 正确:可以修改 a 的值 */
    // p = &b;           /* 错误:p 本身是常量,不能改变指向 */

    printf("a = %d, *p = %d\n", a, *p);

    return 0;
}

此时 p 是一个常量指针,必须在定义时初始化,之后不能指向别处,但可以通过它修改所指内容。

3. 两边都有 const

c 复制代码
#include <stdio.h>

int main(void)
{
    int a = 10;
    const int * const p = &a;

    // *p = 20;          /* 错误:内容不可改 */
    // p = &b;           /* 错误:指向不可改 */

    printf("*p = %d\n", *p);
    return 0;
}

既不能改内容,也不能改指向,完全"锁死"。

4. 为什么要用 const 修饰指针?

· 提高代码可读性:看到 const int *p,调用者就知道函数不会修改 p 指向的数据。

· 安全性:防止无意中修改不该修改的数据。

· 接口约定:在函数参数中常用,比如 void print(const char *str),表明函数只读字符串。

小技巧:const 离变量名近,修饰的就是变量名;离类型近,修饰的就是类型。多写多练,自然就记住了。

二、野指针

野指针(Wild Pointer)是指向"不可知"内存位置的指针。它不像空指针那样有明确的值(NULL),而是随机指向某块内存,解引用它会导致未定义行为------可能崩溃,可能篡改关键数据,甚至看起来"正常运行"但埋下隐患。


2.1野指针的常见成因

  1. 指针未初始化
c 复制代码
int *p;      // p是野指针,指向随机地址
*p = 10;     // 危险!可能写入非法内存
  1. 指针越界访问
c 复制代码
int arr[5] = {0};
int *p = arr;
for (int i = 0; i <= 5; i++) {
    *(p + i) = i;   // 当i=5时越界,p+5已超出数组范围
}
  1. 指针指向的空间已释放
c 复制代码
int *p = (int *)malloc(sizeof(int));
free(p);
// p现在成了野指针,再解引用就是灾难
*p = 20;    // 错误
  1. 返回局部变量的地址
c 复制代码
int *func() {
    int a = 10;
    return &a;   // a是局部变量,函数结束后栈帧销毁,返回的地址无效
}

2.2如何规避野指针?

(1)初始化时置 NULL

指针在定义时就赋初值,不知道指向哪里就先置 NULL,用之前再赋值。

c 复制代码
#include <stdio.h>

int main(void)
{
    int a = 100;
    int *p = NULL;      /* 明确初始化为空指针 */

    if (p != NULL) {
        *p = 10;        /* 不会执行 */
    }
    printf("初始 p = %p (NULL)\n", (void *)p);

    p = &a;             /* 真正需要时再指向有效内存 */
    if (p != NULL) {
        *p = 10;
        printf("赋值后 *p = %d, a = %d\n", *p, a);
    }

    return 0;
}

(2)使用前检查有效性

尤其是作为函数参数传入的指针,一定要先判断:

c 复制代码
#include <stdio.h>

void print_array(int *arr, int len)
{
    if (arr == NULL || len <= 0) {
        printf("[print_array] 无效输入,直接返回\n");
        return;              /* 无效输入直接返回,避免解引用 */
    }
    printf("[print_array] ");
    for (int i = 0; i < len; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int main(void)
{
    int a[3] = {1, 2, 3};
    print_array(a, 3);       /* 正常 */
    print_array(NULL, 3);    /* 不会崩溃,直接返回 */
    return 0;
}

(3)小心数组越界

循环边界要严格,< 不要写成 <=:

c 复制代码
#include <stdio.h>

int main(void)
{
    int arr[5] = {0};
    int *p = arr;

    /* 错误写法:i <= 5 会访问 arr[5],越界 */
    // for (int i = 0; i <= 5; i++) { *(p + i) = i; }

    /* 正确写法 */
    for (int i = 0; i < 5; i++) {
        *(p + i) = i + 1;
    }

    printf("数组内容: ");
    for (int i = 0; i < 5; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    return 0;
}

(4)free 之后立即置 NULL

动态内存释放后,指针变量本身不会自动变成 NULL,它依然保留原来的地址,此时再解引用就是野指针:

c 复制代码
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int *p = (int *)malloc(sizeof(int) * 10);
    if (p == NULL) {
        printf("malloc 失败\n");
        return 1;
    }

    *p = 100;
    printf("使用中 *p = %d\n", *p);

    free(p);
    p = NULL;               /* 关键一步:置空,避免野指针 */

    if (p != NULL) {
        *p = 200;           /* 不会执行 */
    }
    printf("free 后 p = %p (已置 NULL,安全)\n", (void *)p);

    return 0;
}

(5)不要返回局部变量的地址

局部变量存放在栈上,函数返回后栈帧销毁,地址不再有效。需要返回数据时,用 malloc 动态分配,或者让调用者传入缓冲区:


c 复制代码
#include <stdio.h>
#include <stdlib.h>

/* 错误示例(不要打开):
int *bad_func(void) {
    int a = 10;
    return &a;       // a 随函数结束而销毁,返回的指针是野指针
}
*/

/* 正确方式一:动态分配,由调用者负责释放 */
int *func1(void)
{
    int *p = (int *)malloc(sizeof(int));
    if (p != NULL) {
        *p = 10;
    }
    return p;            /* 调用者用完需要 free */
}

/* 正确方式二:传入缓冲区,由调用者提供存储空间 */
void func2(int *out)
{
    if (out != NULL) {
        *out = 10;       /* 直接写入调用者提供的地址 */
    }
}

int main(void)
{
    /* 方式一 */
    int *p1 = func1();
    if (p1 != NULL) {
        printf("方式一 malloc: *p1 = %d\n", *p1);
        free(p1);
        p1 = NULL;       /* 释放后置空 */
    }

    /* 方式二 */
    int val = 0;
    func2(&val);
    printf("方式二 传缓冲区: val = %d\n", val);

    return 0;
}

(6)局部指针作用域尽量小

c 复制代码
#include <stdio.h>
#include <stdlib.h>

void process(void)
{
    int *p = NULL;       /* 定义时置 NULL */
    p = (int *)malloc(sizeof(int) * 5);
    if (p == NULL) {
        return;          /* 分配失败处理 */
    }

    for (int i = 0; i < 5; i++) {
        p[i] = i;
    }

    printf("局部指针内容: ");
    for (int i = 0; i < 5; i++) {
        printf("%d ", p[i]);
    }
    printf("\n");

    free(p);
    p = NULL;            /* 释放后置空,作用域结束前清理干净 */
}

int main(void)
{
    process();
    return 0;
}

野指针就像一颗定时炸弹,养成好习惯,把它扼杀在摇篮里。

三、assert断言

1. assert 是什么?

assert 是C标准库中的宏,定义在 <assert.h> 中。它不是普通函数,而是一个"运行时断言"工具。它接收一个表达式:

· 如果表达式为真(非0),程序继续执行,什么都不做;

· 如果表达式为假(0),程序会立即终止,并向标准错误输出诊断信息,通常包括:失败的表达式、源文件名、行号。

例如:

c 复制代码
#include <stdio.h>
#include <assert.h>

int divide(int a, int b)
{
    assert(b != 0);   /* 如果 b 为 0,程序在这里终止并报错 */
    return a / b;
}

int main(void)
{
    printf("10 / 2 = %d\n", divide(10, 2));

    /* 取消下面注释,会看到 assert 触发终止程序 */
    // printf("10 / 0 = %d\n", divide(10, 0));

    return 0;
}

当 b == 0 时,你会看到类似这样的错误信息:

text 复制代码
Assertion failed: b != 0, file test.c, line 5

然后程序调用 abort() 终止。这就是 assert 的核心机制:在错误发生的第一现场停下,而不是等到后面数据被破坏后再崩溃。

2. 为什么需要 assert?

在开发阶段,bug 越早暴露越好。很多指针错误、非法参数、逻辑不变量被破坏,如果不检查,程序可能继续运行,然后在一段毫不相关的代码处崩溃,调试难度极大。

assert 的作用就是:把"本不该发生"的情况变成"立刻可见的失败"。

例如,一个函数要求指针不能为空、长度必须大于0,这些属于函数的前置条件。用 assert 检查,可以快速定位调用者传错参数的问题。

3. 在指针中的典型应用

c 复制代码
#include <stdio.h>
#include <assert.h>

void print_array(int *arr, int len)
{
    assert(arr != NULL);   /* 确保指针有效 */
    assert(len > 0);       /* 确保长度合理 */

    printf("[print_array] ");
    for (int i = 0; i < len; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int main(void)
{
    int a[5] = {1, 2, 3, 4, 5};
    print_array(a, 5);

    /* 取消下面注释会触发 assert */
    // print_array(NULL, 5);   /* Assertion failed: arr != NULL */

    return 0;
}

这里用 assert 表达的是:调用 print_array 时,arr 不应该为 NULL,len 不应该小于等于 0。如果违反了,说明程序逻辑有问题,应该立即修复。

4. assert 的使用原则

  1. 只检查"绝不应该发生"的情况
    比如内部不变量、函数前置条件、后置条件。不要用它处理用户输入错误、文件打开失败、网络断开、malloc 失败等可预期错误。
  2. 不要用于错误恢复
    assert 失败会直接终止程序,它不能像 if 那样让程序优雅处理并继续运行。需要恢复的错误,必须用 if 判断并处理。
  3. 不要有副作用
    像 assert(i++ > 0) 这种写法非常危险。因为在发布版本中,assert 可能被完全移除,i++ 就不会执行,导致逻辑不一致。
  4. 发布版本可能禁用 assert
    如果定义了 NDEBUG 宏(通常在 Release 版本中),assert 会被替换成空操作。因此,不能依赖 assert 来完成必须执行的逻辑。
c 复制代码
#define NDEBUG
#include <assert.h>
  1. 一个 assert 只检查一个条件
    这样失败时信息更明确,便于定位问题。
  2. assert 不是错误处理,而是调试工具
    它用于捕捉程序员自己的错误,而不是用户或外部环境造成的错误。

5. 补充:C11 的静态断言

C11 提供了 _Static_assert,用于编译期检查。例如:

c 复制代码
_Static_assert(sizeof(int) == 4, "int must be 4 bytes");

如果条件不满足,编译就会失败。它适合检查类型大小、常量表达式等,和运行时的 assert 互补。

合理使用 assert,能让bug在开发阶段就暴露出来,而不是等到上线后才发现。

四、指针的使用和传址调用

C语言函数参数默认是传值调用,也就是说,形参是实参的一份临时拷贝。在函数内修改形参,不会影响外面的实参。


4.1经典例子:交换两个数

错误版(传值):

c 复制代码
void swap(int x, int y) {
    int temp = x;
    x = y;
    y = temp;
}

int main() {
    int a = 10, b = 20;
    swap(a, b);
    printf("a=%d, b=%d\n", a, b);  // 输出 a=10, b=20,交换失败
    return 0;
}

正确版(传址):

c 复制代码
void swap(int *px, int *py) {
    int temp = *px;
    *px = *py;
    *py = temp;
}

int main() {
    int a = 10, b = 20;
    swap(&a, &b);
    printf("a=%d, b=%d\n", a, b);  // 输出 a=20, b=10,成功
    return 0;
}

原理:传址调用时,函数接收到的是变量的地址,通过 * 解引用就能直接操作原变量。

(对比传值与传址):

c 复制代码
#include <stdio.h>

/* 错误版:传值,交换失败 */
void swap_wrong(int x, int y)
{
    int temp = x;
    x = y;
    y = temp;
}

/* 正确版:传址,交换成功 */
void swap_right(int *px, int *py)
{
    int temp = *px;
    *px = *py;
    *py = temp;
}

int main(void)
{
    int a = 10, b = 20;

    swap_wrong(a, b);
    printf("[传值调用] a = %d, b = %d (未交换)\n", a, b);

    swap_right(&a, &b);
    printf("[传址调用] a = %d, b = %d (已交换)\n", a, b);

    return 0;
}

4.2通过指针返回多个值

函数只能有一个返回值,但通过指针参数,可以"返回"多个结果:

c 复制代码
#include <stdio.h>
#include <assert.h>

void find_min_max(int arr[], int len, int *min, int *max)
{
    assert(arr != NULL && min != NULL && max != NULL);
    assert(len > 0);

    *min = *max = arr[0];
    for (int i = 1; i < len; i++) {
        if (arr[i] < *min) *min = arr[i];
        if (arr[i] > *max) *max = arr[i];
    }
}

int main(void)
{
    int arr[] = {3, 1, 7, 2, 9, 4};
    int len = sizeof(arr) / sizeof(arr[0]);
    int min = 0, max = 0;

    find_min_max(arr, len, &min, &max);
    printf("min = %d, max = %d\n", min, max);

    return 0;
}

4.3数组传参的本质

数组名在传参时会"退化"为指向首元素的指针:

c 复制代码
#include <stdio.h>

void func(int arr[])
{
    /* 这里 arr 等价于 int *arr */
    printf("[函数内] sizeof(arr)      = %zu 字节(指针大小)\n", sizeof(arr));
    printf("[函数内] sizeof(int *)    = %zu 字节\n", sizeof(int *));
}

int main(void)
{
    int arr[10] = {0};
    printf("[main 中] sizeof(arr)     = %zu 字节(真正的数组大小)\n", sizeof(arr));
    func(arr);
    return 0;
}

所以在函数内部无法用 sizeof 求数组长度,必须额外传入长度参数。

使用指针参数的注意事项

· 调用前确保传入的地址有效。

· 在函数内用 assert 或 if 检查指针是否为 NULL。

· 明确指针是输入、输出还是输入输出,必要时加 const 保护输入数据。

总结

核心要点:

const修饰指针 左定值,右定向;提高安全性与可读性

野指针 未初始化、越界、释放后未置NULL、返回局部变量地址;初始化置NULL,free后置NULL

assert断言 运行时检查,Debug有效,不能替代错误处理,不要有副作用

传址调用 通过指针修改外部变量,实现交换、返回多值;数组传参退化为指针


指针是一把双刃剑,用好了效率极高,用不好程序崩溃。理解 const、避开野指针、善用 assert、掌握传址调用,是写出健壮C代码的必经之路。


下一篇我们聊聊指针与数组的微妙关系。敬请期待!


相关推荐
quantdash_cc1 小时前
股票历史数据为什么比实时行情更重要?回测结果失真的根源可能就在 K 线数据
开发语言·python·数据分析·量化交易·股票数据·quantdash
光电的一只菜鸡1 小时前
为什么调整LSC会对AF产生影响
android·开发语言·kotlin
zhangjw341 小时前
第47篇:Java综合实战:电商秒杀系统(高并发场景)
java·开发语言
FOAF-lambda2 小时前
uiautomation-new 控件方位的使用
开发语言·python
OKkankan2 小时前
LangChain能力详解!:从工具调用到 LangSmith——让聊天模型具备实时交互能力!
开发语言·python·langchain
richard_yuu2 小时前
AOI 实战第五篇:c_broken 漏检严重,V4 迭代背后的权衡
开发语言·深度学习·yolo
那年窗外下的雪.2 小时前
AIDC 学习日志|第 22 天|All-Active/Single-Active 故障演练设计
服务器·网络·学习·算法·哈希算法
m0_734571762 小时前
深入理解C++ 析构函数<一>基本概念
开发语言·c++