C 语言指针进阶教程:const 修饰、野指针规避与传址调用

🏠个人主页:黎雁

🎬作者简介:C/C++/JAVA后端开发学习者

❄️个人专栏:C语言数据结构(C语言)EasyX游戏规划

✨ 从来绝巘须孤往,万里同尘即玉京

文章目录

    • [前景回顾:上一篇指针核心知识点速记 📝](#前景回顾:上一篇指针核心知识点速记 📝)
    • [一、const 修饰指针:限制指针的修改权限 🔒](#一、const 修饰指针:限制指针的修改权限 🔒)
    • [二、野指针:C语言中的危险隐患 💣](#二、野指针:C语言中的危险隐患 💣)
      • [1. 野指针的定义](#1. 野指针的定义)
      • [2. 野指针的三大成因 🚫](#2. 野指针的三大成因 🚫)
        • [① 指针未初始化](#① 指针未初始化)
        • [② 指针越界访问](#② 指针越界访问)
        • [③ 指针指向的空间被释放](#③ 指针指向的空间被释放)
      • [3. 规避野指针的四大方法 ✅](#3. 规避野指针的四大方法 ✅)
        • [① 指针必须初始化](#① 指针必须初始化)
        • [② 避免指针越界访问](#② 避免指针越界访问)
        • [③ 指针不用时及时置`NULL`,使用前检查有效性](#③ 指针不用时及时置NULL,使用前检查有效性)
        • [④ 避免返回局部变量的地址](#④ 避免返回局部变量的地址)
    • [三、assert 断言:程序运行时的检查工具 🛂](#三、assert 断言:程序运行时的检查工具 🛂)
      • [1. assert 的基本概念](#1. assert 的基本概念)
      • [2. assert 的使用示例](#2. assert 的使用示例)
      • [3. assert 的优势](#3. assert 的优势)
        • [① 精准定位问题](#① 精准定位问题)
        • [② 可以灵活开启和关闭](#② 可以灵活开启和关闭)
    • [四、指针的使用和传址调用:指针的实际应用 💪](#四、指针的使用和传址调用:指针的实际应用 💪)
      • [1. 模拟实现 strlen 函数](#1. 模拟实现 strlen 函数)
      • [2. 值调用与传址调用:交换两个整数的对比](#2. 值调用与传址调用:交换两个整数的对比)
        • [① 值调用:无法实现交换功能](#① 值调用:无法实现交换功能)
        • [② 传址调用:成功实现交换功能](#② 传址调用:成功实现交换功能)
    • [写在最后 📝](#写在最后 📝)

继上一篇的指针基础内容后,本篇将继续深入讲解指针的进阶知识,主要包含const修饰指针、野指针、assert断言以及指针的使用和传址调用四个核心模块,内容详实且条理清晰,帮助大家彻底掌握这些关键知识点。

前景回顾:上一篇指针核心知识点速记 📝

上一篇文章链接:【C语言指针精讲】从内存到运算,吃透指针核心逻辑

要学好本篇的进阶内容,需要先巩固上一篇的关键知识点,打好扎实的基础:

  1. 内存与地址 :内存以1字节为基本存储单元,每个单元都有唯一的编号,这个编号就是地址 ,也被称为指针
  2. 指针变量 :是专门用于存储地址的变量,定义格式为类型* 变量名,使用&可以获取变量的地址,使用*可以解引用指针,从而操作目标变量。
  3. 指针大小:仅与系统位数相关,32位系统下指针变量占4字节,64位系统下占8字节,和指针指向的变量类型没有关系。
  4. 指针类型的意义 :决定了解引用时操作的字节数,以及指针进行±整数运算时的步长,例如int*类型指针步长为4,char*类型指针步长为1。
  5. 指针运算:支持±整数、指针减指针(要求两个指针指向同一块连续内存)以及关系运算,是高效遍历数组的重要方式。

一、const 修饰指针:限制指针的修改权限 🔒

在C语言中,const的作用是限制变量或指针的修改权限,让程序的逻辑更加严谨。我们可以分为两种情况来理解它的用法。

1. const 修饰变量:常变量的特性

const修饰的变量会成为常变量,它的本质仍然是变量,但无法直接对其进行修改操作。

c 复制代码
#include <stdio.h>
int main()
{
    const int n = 10; // n为常变量,不能直接修改
    // n=100; // 报错,直接修改常变量的操作不被允许
    printf("%d\n", n); // 输出:10
    return 0;
}
补充方法:使用指针间接修改常变量

虽然不能直接修改常变量的值,但可以借助指针,间接修改常变量对应的内存数据。

c 复制代码
#include <stdio.h>
int main()
{
    const int n = 10;
    int *p = &n; // 让指针指向常变量n的地址
    *p = 100; // 通过解引用指针,间接修改n的值
    printf("%d\n", n); // 输出:100
    return 0;
}

2. const 修饰指针变量:不同位置的不同效果

const修饰指针变量时,放置的位置不同,产生的限制效果也截然不同,核心判断依据是const*的左侧还是右侧。

const的位置 格式示例 限制规则
*左边 const int *p / int const *p 限制的是指针指向的内容*p的值不能修改,但指针变量p本身可以指向其他地址
*右边 int *const p 限制的是指针变量本身p不能指向其他地址,但指针指向的内容*p可以修改
*两边 const int *const p 对指针和指针指向的内容都有限制,p不能更换指向,*p的值也不能修改
代码验证示例
c 复制代码
#include <stdio.h>
int main()
{
    const int n = 10;
    int b = 20;

    // 情况1:const在*左边
    const int *p1 = &n;
    // *p1 = 100; // 报错,无法修改指针指向的内容
    p1 = &b; // 合法,指针可以指向其他地址

    // 情况2:const在*右边
    int *const p2 = &n;
    *p2 = 100; // 合法,可以修改指针指向的内容
    // p2 = &b; // 报错,指针不能指向其他地址

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

二、野指针:C语言中的危险隐患 💣

野指针是C语言编程中需要重点防范的问题,一旦出现野指针,很容易导致程序崩溃或出现不可预期的错误。

1. 野指针的定义

野指针指的是指向位置不可知的指针,具体来说,就是指针指向的内存空间不属于当前程序,对这类指针进行访问或操作,属于非法的内存操作。

2. 野指针的三大成因 🚫

① 指针未初始化

局部指针变量如果在定义时没有进行初始化,编译器会为其分配一个随机的内存地址,这个指针就会成为野指针。

c 复制代码
#include <stdio.h>
int main()
{
    int *p; // 局部指针变量未初始化,指向随机地址
    *p = 20; // 操作野指针,会导致程序异常
    return 0;
}
② 指针越界访问

当指针的操作范围超出了目标内存的合法边界时,指针就会变成野指针,最常见的场景是数组遍历的越界。

c 复制代码
#include <stdio.h>
int main()
{
    int arr[10] = {0};
    int *p = &arr[0];
    for (int i = 0; i <= 11; i++) // 循环次数超出数组元素个数,导致越界
    {
        *(p++) = i; // 指针越界后,转变为野指针
    }
    return 0;
}
③ 指针指向的空间被释放

函数内部的局部变量,其生命周期仅限于函数运行期间,函数执行结束后,局部变量的内存空间会被系统回收。如果函数返回局部变量的地址,得到的指针就是野指针。

c 复制代码
#include <stdio.h>
int* test()
{
    int n = 100; // n是局部变量,函数结束后内存会被释放
    return &n; // 返回局部变量的地址,得到野指针
}
int main()
{
    int *p = test();
    printf("%d\n", *p); // 非法访问已释放的内存,结果不可预期
    return 0;
}

3. 规避野指针的四大方法 ✅

① 指针必须初始化
  • 如果明确知道指针的指向,直接将目标变量的地址赋值给指针;
  • 如果暂时不确定指针的指向,就将NULL赋值给指针。NULL是C语言定义的常量,本质是地址0,这个地址无法进行读写操作。
c 复制代码
int a = 10;
int *p1 = &a; // 明确指向,初始化方式安全
int *p2 = NULL; // 暂时无指向,赋值为NULL
② 避免指针越界访问

指针只能访问当前程序已经申请过的内存空间,在编写代码时,要严格控制指针的操作范围,杜绝越界情况的发生。

③ 指针不用时及时置NULL,使用前检查有效性

当指针使用完毕后,将其赋值为NULL,后续再次使用这个指针前,先判断它是否为NULL,确认非空后再进行操作。

c 复制代码
#include <stdio.h>
int main()
{
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    int *p = &arr[0];
    for (int i = 0; i < 10; i++)
    {
        *(p++) = i;
    }
    p = NULL; // 指针使用完毕,置为NULL
    // 重新使用前进行检查
    p = &arr[2];
    if (p != NULL) // 确认指针非空
    {
        // 执行安全的操作
    }
    return 0;
}
④ 避免返回局部变量的地址

局部变量的作用域仅限于函数内部,函数执行结束后其内存会被回收,因此不要将局部变量的地址作为函数的返回值。

三、assert 断言:程序运行时的检查工具 🛂

assert断言是C语言中用于程序调试的重要工具,能够帮助开发者在程序运行过程中,及时发现隐藏的问题。

1. assert 的基本概念

assert是定义在<assert.h>头文件中的宏,它接收一个表达式作为参数,运行时的判断规则如下:

  • 如果表达式的结果为真(非0),程序正常执行,assert不会产生任何影响;
  • 如果表达式的结果为假(0),程序会立刻终止运行,并在标准错误流中输出错误信息,包括未通过的表达式、所在的文件名和行号。

2. assert 的使用示例

c 复制代码
#include <stdio.h>
#include <assert.h>
int main()
{
    int *p = NULL;
    assert(p != NULL); // 检查指针是否为空
    printf("程序正常运行\n"); // 表达式为假,此句不会执行
    return 0;
}

程序运行后会输出报错信息,示例如下:
Assertion failed: p != NULL, file xxx.c, line 5

assert的检查范围不局限于指针,任何表达式都可以作为它的参数:

c 复制代码
#include <stdio.h>
#include <assert.h>
int main()
{
    int a = 10;
    assert(a == 5); // 检查变量a的值是否为5
    printf("程序正常运行\n");
    return 0;
}

3. assert 的优势

① 精准定位问题

当程序出现问题时,assert能够直接指出错误所在的文件和行号,无需开发者手动添加调试代码,大大提升调试效率。

② 可以灵活开启和关闭

如果确认程序没有问题,不需要再进行断言检查,可以在引入<assert.h>头文件之前,定义宏NDEBUG,这样编译器就会禁用当前文件中所有的assert语句。

c 复制代码
#define NDEBUG // 定义该宏,禁用assert
#include <assert.h>

一般情况下,Debug版本的程序默认开启assert,Release版本的程序默认禁用assert

四、指针的使用和传址调用:指针的实际应用 💪

学习指针的最终目的是将其运用到实际编程中,接下来通过两个经典案例,讲解指针的实用技巧。

1. 模拟实现 strlen 函数

strlen是C语言的库函数,定义在<string.h>头文件中,功能是统计字符串中\0之前的字符个数。函数原型为size_t strlen(const char *str);

在模拟实现这个函数时,constassert都能发挥重要作用:

c 复制代码
#include <stdio.h>
#include <assert.h>
size_t my_strlen(const char *p) // const修饰,保证不会修改字符串内容
{
    assert(p != NULL); // 断言检查,防止空指针解引用
    size_t count = 0;
    while (*p != '\0') // 遍历字符串,直到遇到结束符
    {
        count++;
        p++; // 指针移动,指向下一个字符
    }
    return count;
}

int main()
{
    char arr[] = "abcdef";
    size_t len = my_strlen(arr);
    printf("%zd\n", len); // 输出:6
    return 0;
}

关键点解析:const char *p的写法可以避免函数内部意外修改字符串内容;assert(p != NULL)能够防止传入空指针导致程序崩溃。

2. 值调用与传址调用:交换两个整数的对比

我们以编写交换两个整数的函数为例,对比值调用和传址调用的区别。

① 值调用:无法实现交换功能

值调用时,函数的形参是实参的临时拷贝,形参拥有独立的内存空间,对形参的修改不会影响到实参。

c 复制代码
#include <stdio.h>
void Swap(int x, int y) // x、y是实参的临时拷贝
{
    int z = x;
    x = y;
    y = z;
}
int main()
{
    int a = 10, b = 20;
    printf("交换前:a=%d b=%d\n", a, b); // 输出:交换前:a=10 b=20
    Swap(a, b); // 值调用,传递的是变量的值
    printf("交换后:a=%d b=%d\n", a, b); // 输出:交换后:a=10 b=20
    return 0;
}
② 传址调用:成功实现交换功能

传址调用时,函数接收的是实参的地址,通过指针解引用,可以直接操作实参对应的内存空间,从而实现修改实参的目的。

c 复制代码
#include <stdio.h>
void Swap2(int *pa, int *pb) // 接收实参的地址
{
    int c = *pa; // c获取a的值
    *pa = *pb; // 将b的值赋给a
    *pb = c; // 将原来a的值赋给b
}
int main()
{
    int a = 10, b = 20;
    printf("交换前:a=%d b=%d\n", a, b); // 输出:交换前:a=10 b=20
    Swap2(&a, &b); // 传址调用,传递变量的地址
    printf("交换后:a=%d b=%d\n", a, b); // 输出:交换后:a=20 b=10
    return 0;
}

核心逻辑:传址调用突破了函数的局部限制,让函数内部能够直接对主函数中的变量进行修改。

写在最后 📝

本篇的指针进阶知识点,核心围绕安全实用两个关键词展开:

  1. const的合理使用,能够限制指针和变量的修改权限,让程序逻辑更加严谨;
  2. 认清野指针的成因,并掌握对应的规避方法,是保证程序稳定运行的关键;
  3. assert断言是高效的调试工具,能够帮助开发者快速定位程序问题;
  4. 传址调用是指针的核心应用场景之一,能够实现函数对外部变量的修改操作。

指针的学习需要循序渐进,建议大家多编写代码、多调试运行,观察指针的指向变化,加深对知识点的理解。下一篇我们将继续讲解指针与,敬请关注。

相关推荐
lsx2024062 小时前
ASP TextStream
开发语言
cike_y2 小时前
JSP标签&JSTL标签&EL表达式
java·开发语言·jsp
秃然想通2 小时前
Java继承详解:从零开始理解“父子关系”编程
java·开发语言
嘻嘻嘻开心2 小时前
List集合接口
java·开发语言·list
源码获取_wx:Fegn08952 小时前
基于springboot + vue物业管理系统
java·开发语言·vue.js·spring boot·后端·spring·课程设计
cike_y2 小时前
JavaWeb-JDBC&事务回滚
java·开发语言·javaweb
青啊青斯2 小时前
python markdown转word【包括字体指定】
开发语言·python·word
corpse20102 小时前
trae下载依赖包特别慢!!!
开发语言·python
rainFFrain2 小时前
QT显示类控件---QSlider
开发语言·qt