详解const申明

Last Updated: 2024-03-13 12:53:06 Wednesday

-- TOC --

C编译器能够在编译期间,发现对有const申明的变量的赋值,并进行错误提示。但它的作用比较有限!不过,能用还是尽量用,即可提高代码可读性,也可能提高编译后的代码性能。

在C中,const的语义只有一个,即readonly!

const申明

哪怕是赋同样的值,编译器也会报错!哪怕是不可能被执行到的语句,也会报错!

const申明的另一个作用,在函数接口处,表明传入的参数值不会在函数内部被改变。很多C语言标准库中的函数接口参数,都有const申明,一般他们都是 const type * 这种指针,表明安全,可放心传入,函数不会改变指针指向的内容。这也是const的一个主要的用法。定义全局常量,我们更倾向于使用#define。这也说明了,在 call by value 的传参机制上,一个 int 型的参数,申明 const 没有实际意义。

const char *p1;  // I preferred
char const *p2;

以上两个定义含义相同,*p结合在一起是const的。我一般喜欢把const关键词放在前面。此申明表示,p1和p2指向的内存块是const,不可以修改,但p1和p2本身可以修改

char* const p3;

p3的申明表示,指针本身(即地址)为readonly。(只有p3是const的)

const char* const p4;
char const* const p5; // == char const * const p5;

p4和p5的申明,表示指针本身,以及指向的数据都为readonly,不可修改。

非const向const转换

非const申明,可以顺利转换成const,函数接口参数的const申明,大多是这样的。C标准库中大量的接口参数,都是const char*这种写法。但去掉const就会有warning:

const char *p;
char *q = p;
...
warning: initialization discards ‘const’ qualifier from pointer target type [-Wdiscarded-qualifiers]
706 | char *q = p;

因为const申明转换成非const,破坏了原const语义,原来不能修改的,突然变成可以修改了,warning一下。

拿掉const

下面这种写法,可以合法的将const拿掉(强类型转换,转成非const的),没有warning:

$ cat test.c
#include <stdio.h>
#include <stdint.h>


void unconst(const char *p) {
    char *q = (char*)p;
    *q = 'b';
    printf("q is %p %s\n", q, q);
}


int main(void) {
    char a[] = {'a'};
    printf("a is %p %s\n", a, a);
    unconst(a);
    return 0;
}
$ gcc -Wall -Wextra test.c -o test
$ ./test
a is 0x7fff012ff5df a
q is 0x7fff012ff5df b

据说这种写法在Windows上会导致crash!未测试...

再来一个测试,以下代码用gcc编译和g++编译都可以,但是运行结果不同:

#include <stdio.h>

int main(void) {
    const int a = 123;
    *(int*)&a = 234;
    printf("%d\n", a);
    return 0;
}

输出:

$ gcc -Wall -Wextra test2.c -o test
$ ./test
234
$ g++ -Wall -Wextra test2.c -o test
$ ./test
123

均无告警!

可能的原因是,在C++中,const除了readonly,还有常量的含义,见下文更详细的说明。

诡异的const入参

下面这段代码,在编译的时候,会有一个warning:

$ cat test_2.c
void foo(const char **argv) {
    //
}

int main(int argc, char **argv) {
    foo(argv);
    return 0;
}
$ gcc test_2.c
test_2.c: In function main:
test_2.c:7:9: warning: passing argument 1 of foo from incompatible pointer type [-Wincompatible-pointer-types]
    7 |     foo(argv);
      |         ^~~~
      |         |
      |         char **
test_2.c:1:23: note: expected const char ** but argument is of type char **
    1 | void foo(const char **argv) {
      |          ~~~~~~~~~~~~~^~~~

why?

这两个argv一开始就是incompatible pointer type!

修正也很容易,将foo函数的定义修改为:

void foo(char * const *argv)

这样定义,就相当于将char *p,升级为const char *p,这是符合标准的。 getopt接口的定义就是如此,可查看man 3 getopt。

英文文档中,const是一个qualifier,char *p叫做pointer to a quanlified type。升级到const是合法的,在C标准中有这样的描述:Both operands are pointers to qualified or unqualified versions of compatible types, and the type pointed to by the left has all the qualifiers of the type pointed to by the right.说得好绕...标准文档就是这样吧...

C++对const申明的扩展

const不是constant常量(C)

这个细节C与C++不同,请看下面这段测试代码:

$ cat test3.c
#include <stdio.h>

int main(void) {
    const int a = 100;
    char b[a] = {0};
    return 0;
}

这段代码用gcc编译会有错误,但是用g++就可以编译通过。

$ gcc test3.c
test3.c: In function main’:
test3.c:7:5: error: variable-sized object may not be initialized
    7 |     char b[a] = {0};
      |     ^~~~
$ g++ test3.c
$

这是C++与C的不同的细节。

后来测试发现,没有const,g++也一样能编译!C++就是支持这种语法。

gcc并没有把const量当做constant常量。

C++中的const,具有双重语义:

  1. readonly
  2. constant

readonly的量,不能在编译期间,当做常量使用。因为readonly的量,可以在运行时去掉const,改变值。C++在编译期间,需要使用的常量,应使用constexpr来申明。

常量constant和只读readonly的区别

C中的const,可通过强类型转换来修改,它是readonly type,换一个type,就可以write了!

C++中的const,既可以readonly,也可以是常量,常量就可以用来初始化template,而readonly不行。因为readonly的值无法在编译期间确定,比如某个函数的入参是const的,这个值在函数内部是readonly,但具体值是多少,要在运行时才能确定。如下:

#include <cstdint>
#include <cstdio>

template<int N> class bob{};

void func(const int a){
    bob<a> b;  // compile error
}

int main(void) {
    const int a {24};
    bob<a> b;  // no compile error

    return 0;
}

因此,C++11建议代码中的常量,统一用constexpr来申明。(一个constant也是一个expression)

头文件中的const变量

如果在某个头文件中,定义了一个const申明的变量,这个头文件在多个代码文件中被include。C语言场景下,这种情况算multiple definition,

/usr/bin/ld: /tmp/ccBFZzVf.o:(.rodata+0x0): multiple definition of `you'; /tmp/ccujWF49.o:(.rodata+0x0): first defined here
collect2: error: ld returned 1 exit status

C++语言场景下,一切OK。

在C++中,const是隐式声明为static,所以每一个#include了这个头文件的代码文件,都会定义一份自己的拷贝,这份拷贝在自己的符号表中,所以不会有重复定义的错误。当然,这也导致了同一个const量存在多份copy。解决多份copy的方案,是C++17引入的inline variable,在这样的const量前使用inline,将保证不会存在多份copy。由于在C++中更应该使用constexpr,因此,在头文件中,下面这样的定义很常见:

template<typename T, typename U>
inline constexpr bool in_same_v = is_same<T,U>::value;

const与代码性能

个人认为,如果能够给编译器多一些信息,总是好的。编译器行为各有各的不同,多给点信息也许能够得到更快的代码,反正也可以提供代码可读性,何乐不为呢!

本文链接:https://cs.pynote.net/sf/c/202111262/

-- EOF --

-- MORE --