欢迎光临
我们一直在努力

C语言从入门到进阶——第12讲:深入理解指针(2)

文章目录

  • 1. const修饰指针
    • 1.1 const修饰变量
    • 1.2 const修饰指针变量
  • 2. 野指针
    • 2.1 野指针成因
      • 2.1.1 指针未初始化
      • 2.1.2 指针越界访问
      • 2.1.3 指针指向的空间释放
    • 2.2 如何规避野指针
      • 2.2.1 指针初始化
      • 2.2.2 小心指针越界
      • 2.2.3 指针不用时置NULL,使用前检查有效性
      • 2.2.4 避免返回局部变量的地址
  • 3. assert断言
    • 3.1 assert的基本使用
    • 3.2 assert的开关机制
    • 3.3 assert的优缺点
  • 4. 指针的使用和传址调用
    • 4.1 strlen的模拟实现
    • 4.2 传值调用和传址调用
      • 4.2.1 传值调用
      • 4.2.2 传址调用
      • 4.2.3 传值调用与传址调用的使用场景

1. const修饰指针

1.1 const修饰变量

普通变量的值可直接修改,也可通过指向它的指针间接修改;被const修饰的变量,在语法层面被限制直接修改,成为只读变量,但仍可通过指针绕开const限制间接修改。 示例代码:

#include <stdio.h>
int main()
{
const int n = 0;
printf("n = %d\\n", n);
int* p = &n;
*p = 20;//绕开const修改n的值
printf("n = %d\\n", n);
return 0;
}

为避免指针打破const对变量的修饰限制,需将const修饰指针变量,限制指针的操作权限。

1.2 const修饰指针变量

const修饰指针变量时,根据放在*左侧或右侧,有不同的修饰效果,也可同时修饰*的左右两侧,实现双重限制。通过以下代码分析不同修饰方式的效果:

#include <stdio.h>
//代码1 – 无const修饰
void test1()
{
int n = 10;
int m = 20;
int* p = &n;
*p = 20;//ok
p = &m; //ok
}
//代码2 – const放在*的左边
void test2()
{
int n = 10;
int m = 20;
const int* p = &n;
*p = 20;//error
p = &m; //ok
}
//代码3 – const放在*的右边
void test3()
{
int n = 10;
int m = 20;
int * const p = &n;
*p = 20; //ok
p = &m; //error
}
//代码4 – *的左右两边都有const
void test4()
{
int n = 10;
int m = 20;
int const * const p = &n;
*p = 20; //error
p = &m; //error
}
int main()
{
test1();
test2();
test3();
test4();
return 0;
}

核心结论:

  • const放在*左侧:修饰指针指向的内容,保证指针指向的内容不能通过指针修改,但指针变量本身的地址值可修改;
  • const放在*右侧:修饰指针变量本身,保证指针变量的地址值不能修改,但指针指向的内容可通过指针修改;
  • *左右两侧都加const:指针变量的地址值和指向的内容,均不能通过该指针修改。
  • 2. 野指针

    概念:野指针是指指针指向的位置不可知(随机的、不正确的、没有明确限制的),使用野指针会导致程序运行异常,是C语言中常见的错误。

    2.1 野指针成因

    2.1.1 指针未初始化

    局部指针变量若未显式初始化,其值为随机的内存地址,此时解引用操作该指针,会访问非法内存空间,形成野指针。

    #include <stdio.h>
    int main()
    {
    int *p;//局部指针未初始化,值为随机值
    *p = 20;//非法访问内存,p为野指针
    return 0;
    }

    2.1.2 指针越界访问

    指针指向数组等连续内存空间时,若访问的范围超出该空间的合法边界,会形成野指针。

    #include <stdio.h>
    int main()
    {
    int arr[10] = {0};
    int *p = &arr[0];
    int i = 0;
    for(i = 0; i <= 11; i++)
    {
    *(p++) = i;//i=10、11时,指针超出数组范围,成为野指针
    }
    return 0;
    }

    2.1.3 指针指向的空间释放

    函数中的局部变量存放在栈区,函数执行结束后,局部变量的内存空间会被系统释放,若函数返回该局部变量的地址,接收该地址的指针会指向已释放的无效空间,形成野指针。

    #include <stdio.h>
    int* test()
    {
    int n = 100;//局部变量,函数结束后空间释放
    return &n;
    }
    int main()
    {
    int*p = test();//p指向已释放的空间,成为野指针
    printf("%d\\n", *p);
    return 0;
    }

    2.2 如何规避野指针

    2.2.1 指针初始化

    指针定义时必须显式初始化:

  • 若明确知道指针指向的目标,直接将目标地址赋值给指针;
  • 若暂时不知道指针指向哪里,将其赋值为NULL。
  • NULL是C语言定义的标识符常量,本质是地址((void *)0),该地址无法读写,是指针的合法初始值。

    #include <stdio.h>
    int main()
    {
    int num = 10;
    int*p1 = &num;//明确指向,直接赋值地址
    int*p2 = NULL;//暂时无指向,赋值NULL
    return 0;
    }

    2.2.2 小心指针越界

    指针只能访问程序向内存申请的合法空间,严格控制指针的访问范围,避免超出空间边界,尤其在遍历数组时,注意循环结束条件。

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

  • 当指针不再访问指定内存空间时,及时将其置为NULL,避免其成为野指针;
  • 每次使用指针前,先判断指针是否为NULL,仅当指针非NULL时,才进行解引用等操作。
  • int main()
    {
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    int *p = &arr[0];
    int i = 0;
    for(i = 0; i < 10; i++)
    {
    *(p++) = i;
    }
    p = NULL;//指针越界后,及时置NULL
    p = &arr[0];//重新获取地址
    if(p != NULL) //使用前检查有效性
    {
    //执行指针操作
    }
    return 0;
    }

    2.2.4 避免返回局部变量的地址

    栈区的局部变量随函数执行结束而释放,其地址会变为无效地址,因此函数不要返回局部变量的地址,若需返回值,可直接返回变量本身,或使用全局变量、动态内存分配。

    3. assert断言

    assert是C语言的断言宏,定义在<assert.h>头文件中,用于运行时检查程序的指定条件是否成立,是排查程序错误、规避野指针的重要工具。

    3.1 assert的基本使用

    assert接收一个表达式作为参数:

  • 若表达式为真(返回值非0),程序正常继续运行,assert无任何作用;
  • 若表达式为假(返回值为0),程序立即报错终止运行,并在标准错误流stderr中输出错误信息,包含未通过的表达式、所在文件名和行号。
  • 核心用法:使用指针前,断言指针非NULL,避免解引用野指针。

    assert(p != NULL);//检查指针p是否为NULL,非NULL才继续执行

    3.2 assert的开关机制

    assert的检查可通过宏NDEBUG灵活开启或关闭,无需修改代码:

  • 启用assert:默认状态下,未定义NDEBUG时,assert生效,执行条件检查;
  • 禁用assert:在#include <assert.h>前定义#define NDEBUG,编译器会禁用所有assert语句,不再执行检查。
  • #define NDEBUG //定义后,禁用assert
    #include <assert.h>

    3.3 assert的优缺点

    优点

  • 自动标识错误的表达式、文件名和行号,方便程序员快速定位问题;
  • 开关机制灵活,可在调试阶段启用,发布阶段禁用,不影响代码结构。
  • 缺点:引入额外的条件检查,会轻微增加程序的运行时间,因此通常在Debug版本中使用,Release版本中禁用。主流集成开发环境(如VS)会在Release版本中自动优化掉assert语句。

    4. 指针的使用和传址调用

    4.1 strlen的模拟实现

    库函数strlen的功能是统计字符串中\\0之前的字符个数,函数原型为size_t strlen ( const char * str ),参数str用const修饰,保证指针不会修改字符串内容。

    模拟实现思路:

  • 定义计数器,从0开始计数;
  • 用assert断言指针非NULL,避免野指针;
  • 遍历字符串,每访问一个非\\0字符,计数器+1,指针后移;
  • 遍历到\\0时停止,返回计数器值。
  • 参考代码:

    #include <stdio.h>
    #include <assert.h>
    int my_strlen(const char * str)
    {
    int count = 0;
    assert(str);//断言指针非NULL
    while(*str)//判断当前字符是否为'\\0'
    {
    count++;
    str++;
    }
    return count;
    }
    int main()
    {
    int len = my_strlen("abcdef");
    printf("%d\\n", len);//输出:6
    return 0;
    }

    4.2 传值调用和传址调用

    4.2.1 传值调用

    概念:调用函数时,将实参的值传递给形参,编译器会为形参单独创建临时空间接收实参值,形参和实参是两个独立的内存空间。 特点:对形参的修改不会影响实参的值,因此传值调用无法实现修改主调函数中变量的需求。

    反例:传值调用交换两个整型变量(失败)

    #include <stdio.h>
    void Swap1(int x, int y)//x、y为形参,接收实参值
    {
    int tmp = x;
    x = y;
    y = tmp;//仅交换形参x、y的值,与实参无关
    }
    int main()
    {
    int a = 10, b = 20;
    printf("交换前:a=%d b=%d\\n", a, b);
    Swap1(a, b);//传值调用
    printf("交换后:a=%d b=%d\\n", a, b);//a、b值未改变
    return 0;
    }

    4.2.2 传址调用

    概念:调用函数时,将实参的地址传递给形参,形参为指针变量,接收实参的地址,通过指针可间接访问并修改实参的内存空间。 特点:让被调函数和主调函数建立真正的内存联系,被调函数可通过指针修改主调函数中的变量值,是指针的核心应用场景。

    正例:传址调用交换两个整型变量(成功)

    #include <stdio.h>
    void Swap2(int*px, int*py)//px、py接收a、b的地址
    {
    int tmp = 0;
    tmp = *px;//通过指针访问a的值
    *px = *py;//将b的值赋给a
    *py = tmp;//将原a的值赋给b
    }
    int main()
    {
    int a = 10, b = 20;
    printf("交换前:a=%d b=%d\\n", a, b);
    Swap2(&a, &b);//传址调用,传递a、b的地址
    printf("交换后:a=%d b=%d\\n", a, b);//a、b值成功交换
    return 0;
    }

    4.2.3 传值调用与传址调用的使用场景

  • 传值调用:函数仅需要使用主调函数中变量的值进行计算,无需修改变量本身时使用;
  • 传址调用:函数需要修改主调函数中变量的值,或需要减少大对象的拷贝开销时使用(如数组、结构体)。
  • 赞(0)
    未经允许不得转载:171主机测评 » C语言从入门到进阶——第12讲:深入理解指针(2)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址