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语言则一般不提供显式支持。这取决于编译器的实现和语言的规范。
资料参考自极客时间