打印
[开发工具]

嵌入式开发中关于const关键字使用场景的一些思考

[复制链接]
60|0
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
luobeihai|  楼主 | 2024-12-14 01:02 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式
本帖最后由 luobeihai 于 2024-12-14 01:04 编辑

#申请原创# @21小跑堂

1. const关键字引入

const 即是英文单词 constant 的缩写,常数、常量(在实际使用中,准确的说,应该是“只读”)的意思。在C语言中,const 用于修饰变量,那么程序员就是希望这个变量变为“常量”,但是这里加了引号,因为对于C语言来说,我们还是可以通过指针间接修改。

2. const修饰普通变量和指针变量

C语言中的 const 修饰变量有如下几个特定:

  • const 修饰的变量只是只读的,本质他还是一个变量,并不是常量;
  • const 修饰的局部变量在栈上分配内存空间;
  • const 修饰的全局变量在只读存储区分配内存空间;
  • const 只是在编译期起作用,在运行期间无效。

总的来说,在C语言中,const修饰的变量不是真的常量,它只是告诉编译器这个变量是不能出现在赋值符号的左边(作为左值使用)。

2.1 const修饰普通变量

const int a = 10;
int const a = 10;

a = 20;

上面两种写法都是一样的,const修饰变量a,那么意味着变量a不能通过赋值被改变,只能通过定义时初始化给一个初值。

2.2 const修饰指针变量

修饰指针变量,才是const**的精髓。有下面几种情况:

/* 这两种写法是一样的,const修饰的是p所指向的内存空间,就是指针p所指向的内存空间的值(或者是内容)是不允许被修改的 */
const int *p;
int const *p;

/* 这里const修饰的是指针p本身,就是指针所指向的地址是不允许被改变的。例如让指针指向其他地址空间,是不被允许的 */
int * const p;

/* 下面这两种写法意思一样。就是结合了上面的两种情况,指针p的指向不允许改变,指向的内存空间的值也不允许改变 */
int const * const p;
const int * const p;

一个阅读小技巧:去掉类型之后(比如上面的 int 类型),然后看 const 最靠近修饰哪个符号,比如先修饰 * 这个符合,就说明指针所指向内存空间的内容是不允许改变的,比如:const int *p;  。const 先修饰指针变量本身,那么就说明修饰的指针不允许改变指向的地址,比如:int * const p;

3. const修饰的变量是否真的不能改变?

前面说过,C语言中 const 修饰的变量具有的只读属性,是编译器通过语法检查来实现的,也就是说,我们在写代码的时候,骗过编译器,可以间接修改const变量的值。

3.1 C语言中const修饰局部变量的情况

#include <stdio.h>

int main(void)
{
    /* 定义一个const局部变量 */
        const int a = 10;
        int *p = (int *)&a;
   
        *p = 20;
        
        printf("a = %d.\n", a);
        printf("*p = %d.\n", *p);

        return 0;
}

编译运行后,输出如下:


很明显,变量a的值被改变为20,说明在C语言中,const修饰的局部变量是可以被改变的。这是因为const修饰的局部变量是存在栈中的,只要骗过了编译器,就可以间接进行改变。

3.2 C语言中const修饰全局变量的情况

C语言中,const修饰的全局变量,如果我们尝试去间接改变该全局变量的值的话,会导致程序运行崩溃。

先看如下代码:

#include <stdio.h>

/* 定义一个const的全局变量 */
const int g_a = 1;

int main(void)
{
        int *p = (int *)&g_a;
   
        *p = 5;
        
        printf("a = %d.\n", a);
        printf("*p = %d.\n", *p);

        return 0;
}

编译运行后,出现了段错误。如下:


原因就是因为const修饰的全局变量,存放的内存区域是只读的全局数据区(read-only-data),既然是保存在只读得全局数据区的,想尝试去修改,那么就只能导致程序崩溃了。

当然,对于比较古老的C语言编译器来说,const修饰的全局变量并不会放在只读存取区,而且也是放在可读可写的存储区,比如 bcc 编译器。

4. C++中对const关键字的进化

C++中,继承了C语言中const的所有特性的基础上,还增加了下面的特性:

  • 当遇见 const 声明的变量时,会把该变量符号和对应的值放入编译器的符号表中;
  • 编译过程中,若发现使用了const修饰的该变量,则直接从符号表中把值取出来进行替换;
  • 编译过程中,若发现有下述情况,那么会对const修饰的变量进行分配储存空间:

    • 对 const 修饰的变量使用了取地址操作符,即 & ;
    • 当 const 变量为全局变量,并且需要在其他文件中使用该变量时(就是用extern关键字修饰)。

注意:C++ 编译器虽然可能会对 const 修饰的变量分配内存空间,但是我们在代码中使用该变量时,不会去该变量的存储空间中取值,而是编译器直接从他的符号常量表中找到对应的值进行替换。我们在代码中进行验证这一点:

#include <stdio.h>

int main(void)
{
    /* 定义一个const局部变量 */
        const int c = 0;
        int *p = (int *)&c;
   
        printf("Begin...\n");
        
        *p = 5;
        
        printf("c = %d.\n", c);
        
        printf("End...\n");

        return 0;
}

编译运行结果如下:


可以看到,打印的变量c的值仍然是他定义时的初值,但是我们明明通过指针p间接改变了变量c存储空间的值了。为什么打印出来c的值还是10?

这就是上面提到的,C++中,const 修饰的变量,C++会把变量c放进符号常量表中,当发现需要使用c的时候(比如这里打印c的值),就会从常量表中找到c的值进行替换。大致过程如下图所示:


所以,就是我们通过指针间接改变了变量c的存储空间的值,但是C++中,使用变量a的时候,并不会从a的存储空间的值,而是使用符号常量表中的值。

也就是说,C++中,const修饰的变量,是可以定义出真正意义上的常量的。

5. const与#define宏定义

在C++中,我们看到了对const修饰的变量,使用了该变量是,是直接从符号常量表中进行替换的,那么这样不就相当于宏定义了吗?

虽然C++中的const常量确实和宏定义很相似,但是并不完全等于宏定义,因为:
  • const常量是有编译器处理的
  • 编译器可以对const常量进行类型检查和作用域检查
  • 宏定义是预处理器处理,仅仅只是进行文本替换而已

下面代码例子:

#include <stdio.h>

void func1(void)
{
        #define a 3
        const int b = 4;
}

void func2(void)
{
        /* 宏没有作用域的概念,不会报错 */
        printf("a = %d\n", a);
        
        /* 会报错,因为const修饰的b是有作用域的 */
        printf("b = %d\n", b);
}

int main(void)
{
    /* 定义一个const局部变量 */
        const int A = 1;
        const int B = 2;
        
        /* C++中,这样定义数组是被允许的,因为是进行常量替换
         * 而在C语言中是不允许的, 因为这时编译器根本不知道要分配多少空间给该数组
         */
        int array[A + B];        
        
        return 0;
}

编译后会报如下错误:


报错说,b没有在函数范围内声明,说明 const 和 宏还是有区别的。

6. C语言中const在函数传参的妙用

我们知道 ,在函数传参时,要想函数参数在函数内部改变时,也把函数参数对应的那个外部变量的值给修改时,就必须传递该变量的地址(指针)作为函数参数。

比如下面的例子:

void swap(int *a, int *b)
{
        int temp;
        
        temp = a;
        a = b;
        b = temp;
}

一个经典的交换两个变量的值的函数,传递的是指针作为函数参数,就可以真正的改变该指针所指向内存空间的值,实现两个变量的值的交换。这里实际上就是函数参数作为了输出型参数来使用,意思就是把两个变量交换后的值通过函数参数返回给调用者。

但是很多时候,我们使用指针作为函数参数传递进来时,并不想要改变指针所指向的内存空间的内容,我们只是想使用这个指针所指向的内存空间的内容进行代码运算而已。因为如果一个变量所占的内存空间很大(比如定义了一个很大的结构体变量),为了节省函数传参过程中的栈内存开销,才传递指针作为函数参数。

比如 strlen 函数的实现:

size_t strlen(const char *str)
{
        const char *sc;

        for (sc = str; *sc != '\0'; sc++);

        return sc - str;
}

函数参数是 const 修饰的 char 型指针。在这个函数里面,其实我们只是想使用这个指针所指向的内容,而并没有想去修改它的内容的意思,所以我们如果加上 const 修饰的话,就可以防止我们在函数内部误修改了该指针变量所指向的内存空间的内容了。

也就是说,我们使用了指针作为函数参数的时候,不加上 const 修饰,可以传递给程序员的信息是这个函数参数是作为输出型参数使用的,就是想要在函数内部改变这个指针参数所指向的内存空间的内容的。而如果加上了 const 修饰指针变量,那就可以向程序员传递一种信息,这个指针所指向的内存空间的内容是不允许被改变的,这个函数仅仅只是想使用该指针所指向的内存空间的内容而已。

Linux 的内核源码中,或者 Linux 提供的一些系统API中,就是遵守这种约定的,指针作为函数参数时,使用 const 修饰时可以传递给程序员一些有用的信息的

7. const作用总结

  • const 修饰的变量,可以利用编译器帮我们进行语法检查,防止我们在写代码过程中无意去修改了那些不想被改变的参数。
  • 在函数传参时,const 修饰的指针变量,可以给程序员传递这个指针参数的一些应用目的,比如是作为输出型参数,还是只是使用该指针所指向的内存空间的内容。
  • 使用关键字 const 修饰变量,可以给编译器提供一些优化代码的信息,这样可能编译产生更紧凑的代码。






使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

19

主题

70

帖子

2

粉丝