const 常量与常量

在 C 语言中,通过内联方式直接写到源代码中的字面量值一般被称为“常量”。比如这里的 -10,‘c’, 2.0。

int x = -10;  
char y = 'c';  
double z = 2.0; 

还有一种常量,是用 const 关键字按照与定义变量相同语法定义的量。比如:

const int vx = 10;
const int* px = &vx;

那这两种常量有什么区别呢?

通常来说,在 C 语言中,使用 const 关键字修饰的变量定义语句,表示对于这些变量,我们无法在后续的程序中修改其对应或指针指向的值。因此,我们更倾向于称它们为“只读变量”,而非常量。当然,在程序的外在表现上,二者有一点是相同的:其值在第一次出现时便被确定,且无法在后续程序中被修改。

只读变量与字面量常量的一个最重要的不同点是,使用 const 修饰的只读变量不具有“常量表达式”的属性,因此无法用来表示定长数组大小,或使用在 case 语句中。常量表达式本身会在程序编译时被求值,而只读变量的值只能够在程序实际运行时才被得知。并且,编译器通常不会对只读变量进行内联处理,因此其求值不符合常量表达式的特征。

案例一

我们来做一个测试:

#include <stdio.h>
int main(void) {
  const int vx = 10;
  const int vy = 10;
  int arr[vx] = {1, 2, 3}; 
  switch(vy) {
    case vx: {  
      printf("Value matched!");
      break;
    }
  }
}

使用gcc编译,结果如下:

root@hecs-270451192.168.0.179 09:58:54 [pwd:~]# gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-redhat-linux/8/lto-wrapper
OFFLOAD_TARGET_NAMES=nvptx-none
OFFLOAD_TARGET_DEFAULT=1
Target: x86_64-redhat-linux
Configured with: ../configure --enable-bootstrap --enable-languages=c,c++,fortran,lto --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enable-shared --enable-threads=posix --enable-checking=release --enable-multilib --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --enable-linker-build-id --with-gcc-major-version-only --with-linker-hash-style=gnu --enable-plugin --enable-initfini-array --with-isl --disable-libmpx --enable-offload-targets=nvptx-none --without-cuda-driver --enable-gnu-indirect-function --enable-cet --with-tune=generic --with-arch_32=x86-64 --build=x86_64-redhat-linux
Thread model: posix
gcc version 8.5.0 20210514 (Red Hat 8.5.0-4) (GCC)
root@hecs-270451192.168.0.179 09:59:03 [pwd:~]# gcc test_constant.c -o test_constant
test_constant.c: In function ‘main’:
test_constant.c:5:3: error: variable-sized object may not be initialized
   int arr[vx] = {1, 2, 3};
   ^~~
test_constant.c:5:18: warning: excess elements in array initializer
   int arr[vx] = {1, 2, 3};
                  ^
test_constant.c:5:18: note: (near initialization for ‘arr’)
test_constant.c:5:21: warning: excess elements in array initializer
   int arr[vx] = {1, 2, 3};
                     ^
test_constant.c:5:21: note: (near initialization for ‘arr’)
test_constant.c:5:24: warning: excess elements in array initializer
   int arr[vx] = {1, 2, 3};
                        ^
test_constant.c:5:24: note: (near initialization for ‘arr’)
test_constant.c:7:5: error: case label does not reduce to an integer constant
     case vx: {
     ^~~~

使用g++编译, 结果如下:

root@hecs-270451192.168.0.179 10:00:34 [pwd:~]# g++ -v
Using built-in specs.
COLLECT_GCC=g++
COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-redhat-linux/8/lto-wrapper
OFFLOAD_TARGET_NAMES=nvptx-none
OFFLOAD_TARGET_DEFAULT=1
Target: x86_64-redhat-linux
Configured with: ../configure --enable-bootstrap --enable-languages=c,c++,fortran,lto --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enable-shared --enable-threads=posix --enable-checking=release --enable-multilib --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --enable-linker-build-id --with-gcc-major-version-only --with-linker-hash-style=gnu --enable-plugin --enable-initfini-array --with-isl --disable-libmpx --enable-offload-targets=nvptx-none --without-cuda-driver --enable-gnu-indirect-function --enable-cet --with-tune=generic --with-arch_32=x86-64 --build=x86_64-redhat-linux
Thread model: posix
gcc version 8.5.0 20210514 (Red Hat 8.5.0-4) (GCC)
root@hecs-270451192.168.0.179 10:00:37 [pwd:~]# g++ test_constant.c -o test_constant
root@hecs-270451192.168.0.179 10:00:47 [pwd:~]# ./test_constant
Value matched!root@hecs-270451192.168.0.179 10:00:50 [pwd:~]#

可以看到使用非常量表达式定义定长数组或者用于case 语句,GCC编译时是不通过的。

但是g++编译时通过了。这是因为内联常量的处理在 C 和 C++ 中存在一些差异。

在 C++ 中,const 变量默认是隐式内联的,这意味着编译器会尝试将其内联展开。因此,在 C++ 中,对于内联常量的编译行为通常是符合预期的,而且编译器会更倾向于将其内联展开。

然而,在 C 中,并没有类似于 C++ 的内联机制。虽然在某些情况下,编译器可能会选择将常量进行内联展开,但这是完全由编译器决定的。对于 C 语言而言,常量的内联展开并不是 C 语言标准所要求的行为。

由于 C 和 C++ 是两种不同的语言,它们的编译器在处理代码时有不同的行为和优化策略。因此,GCC 在 C 和 C++ 的编译过程中可能会有不同的行为,导致对内联常量的处理有所差异。

所以,如果在 C 代码中尝试进行内联常量,GCC 可能会在编译时报错,因为它将严格遵循 C 语言标准。而在 C++ 中,它更倾向于将常量进行内联展开,因此可以通过编译。

案例二

类似地,来看看其他编译器。以下是一个DMSQL语言案例。

CREATE OR REPLACE PACKAGE PKG_CONSTANTS IS
	DATE_FMT CONSTANT VARCHAR2(8):='YYYYMMDD';
END;

DROP TABLE IF EXISTS TEST_TABLE;
CREATE TABLE test_table (
  id INT,
  event_name VARCHAR(50),
  event_time TIMESTAMP
);
--初始化数据
BEGIN 
	FOR I IN 1..1000000 LOOP
        INSERT INTO test_table VALUES(I,'AAA'||I,SYSDATE-I%1000);  
	END LOOP;
	COMMIT;
END;
--创建索引
CREATE INDEX IDX_DATE ON test_table(TO_CHAR(EVENT_TIME,'YYYYMMDD'));
CREATE TABLE test_table1 AS SELECT * FROM test_table WHERE 1=0;

CREATE OR REPLACE PROCEDURE TEST_CONSTANT() AS
	DATE_FMT CONSTANT VARCHAR2(8):='YYYYMMDD';
BEGIN
  INSERT INTO test_table1 SELECT * FROM test_table WHERE  TO_CHAR(EVENT_TIME,DATE_FMT) BETWEEN '20230605' AND TO_CHAR(SYSDATE,DATE_FMT);
END ;

这里存储过程中的SQL从执行计划看是不使用索引的。这是因为存储过程编译器无法对只读变量内联处理。如果使用常量值替换只读变量会带来很大性能提升。

小结

对于常量的内联处理,不同编程语言和编译器的支持程度可能有所不同。C++通常对常量的内联处理提供良好的支持,而C语言则一般不提供显式支持。这取决于编译器的实现和语言的规范。

资料参考自极客时间