C++——2.常量与 const、constexpr 初识详解

在 C++ 中,常量(constant)是指值在程序运行期间不可被修改的量。根据"不可修改"的发生时机和约束强度,C++ 提供了多种定义常量的手段。本详解将聚焦两大核心关键字:const 和 constexpr,帮助你从初识走向深层理解。

1. 常量的本质与基础手段

字面量:直接写在代码中的值,如 42、3.14、'a'、"hello"。

宏定义:#define PI 3.14159(预处理器阶段文本替换,无类型检查,现代 C++ 不推荐)。

枚举常量:enum { SIZE = 100 }; 或 enum class,编译期确定值。

const 变量:带类型的常量,运行时或编译期确定。

constexpr 变量/函数:C++11 引入,强制要求在编译期求值,提供真正的"编译期常量"。

我们重点讨论 const 和 constexpr。

2. const:只读的承诺

const 用于声明一个不可修改的变量。它的核心是承诺"我不会通过这个名字去改变它",但不强制要求在编译期就能确定它的值。

2.1 基本用法

cpp 复制代码
const int max_value = 100;          // 编译期常量(用字面量初始化)
int n = 10;
const int size = n;                 // 运行时常量:值在运行时才确定,但之后不可改
const char a = 'a';
const double pi = 3.14159;
const char* ptr;                    // 指向常量字符的指针(所指内容不可改)
char ch = 'A';

char* const ptr2 = &ch;             // 常量指针(指针本身不可改)

const char* const ptr3 = "Hello";   // 两者皆不可改
// 或者:
//const char* const ptr3 = &ch;       // 如果 ch 是 char,它会隐式转换为 const char*

2.2 const 变量的特性

不可修改性:任何试图修改 const 变量的操作都将导致编译错误。

可能为编译期常量:如果用常量表达式(如字面量)初始化,const 变量可作为编译期常量使用,例如数组大小、模板非类型参数等。但这是有条件的。

内部链接性(默认):在全局或命名空间作用域声明的 const 变量默认具有内部链接(static),即每个翻译单元有独立副本。若希望跨文件共享,需用 extern const 声明。

cpp 复制代码
// file1.cpp
extern const int global_const = 10; // 定义,显式 extern

// file2.cpp
extern const int global_const;      // 声明,可访问同一变量

3. constexpr:真正的编译期常量

constexpr 是 C++11 引入的关键字,它显式要求变量或函数的值在编译期就能计算出来。它比 const 更严格,提供了编译期计算的强大能力。

3.1 constexpr 变量

声明为 constexpr 的变量必须用编译期可确定的常量表达式初始化,且它本身隐含 const。

cpp 复制代码
constexpr int max = 100;          // 编译期常量
constexpr int size = max * 2;     // 编译期计算

int n = 10;
// constexpr int s = n;           // 错误:n 不是常量表达式
constexpr int s2 = 42;            // OK

constexpr double pi = 3.14;       // 浮点数也可以
constexpr const char* msg = "Hi"; // OK

onstexpr 变量必定是编译期常量,因此它可以用在任何需要常量表达式的地方,比如数组大小、模板参数、static_assert 等。

cpp 复制代码
constexpr int length = 5;
int arr[length];                  // OK
std::array<int, length> my_arr;   // OK
static_assert(length == 5, "!");  // OK

如果报错 不允许使用不完整的类型 "std::array<int, 5Ui64>"

添加 #include

csharp 复制代码
#include <array>

3.2 constexpr 函数

constexpr 函数是可能在编译期被求值的函数。它并不保证一定在编译期执行------如果调用时传入的参数都是编译期常量,且调用发生在需要常量表达式的地方,编译器就会在编译期计算它;否则,它也可以像普通函数一样在运行期调用。

规则(C++11/14,C++17,C++20 逐步放宽):

函数体只能包含一条 return 语句(C++11),后续标准允许更多语句,如循环、分支,但不可有 goto、try 或静态变量定义(C++20 允许)。

参数和返回值必须是字面类型(标量、引用、聚合体等)。

C++14 起支持大部分控制流。

注意:虽然 square 被声明为 constexpr,且参数 5 是常量,但编译器仍然拒绝将其视为常量表达式。根本原因是:constexpr 函数的完整定义在调用点之前不可见。例如:

cpp 复制代码
#include <iostream>
#include <cstdlib>   // 添加,因为 get_rand 使用了 rand()

int get_rand();   // 可以只声明,定义可以放在后面

int main() {
    constexpr int val = square(5);     // 现在定义可见,编译通过
    int runtime_val = square(get_rand());
    std::cout << runtime_val << std::endl;
    return 0;
}

int get_rand() {
    return rand() % 100;
}
constexpr int square(int x) {
    return x * x;
}

运行报错:

cpp 复制代码
严重性	代码	说明	项目	文件	行	抑制状态	详细信息
错误	C3861	"square": 找不到标识符	StudyCpp	C:\Study\StudyCpp\StudyCpp\main.cpp	7		
错误	C2131	表达式的计算结果不是常数	StudyCpp	C:\Study\StudyCpp\StudyCpp\main.cpp	7		
错误	C3861	"square": 找不到标识符	StudyCpp	C:Study\StudyCpp\StudyCpp\main.cpp	8		

正确操作只需把

cpp 复制代码
onstexpr int square(int x) {
    return x * x;

提前即可。

完整代码:

cpp 复制代码
#include <iostream>
#include <cstdlib>   // 添加,因为 get_rand 使用了 rand()
constexpr int square(int x) {
    return x * x;
}
int get_rand();   // 可以只声明,定义可以放在后面

int main() {
    constexpr int val = square(5);     // 现在定义可见,编译通过
    int runtime_val = square(get_rand());
    std::cout << runtime_val << std::endl;
    return 0;
}

int get_rand() {
    return rand() % 100;
}

3.3 constexpr 与编译期计算

利用 constexpr 可以将大量计算移至编译期,减少运行时开销,并生成常量数据表等。

cpp 复制代码
#include <iostream>
#include <cstdlib>
#include <array> 

constexpr int fib(int n) {
    return (n <= 1) ? n : fib(n - 1) + fib(n - 2);
}

int main() {
    constexpr int fib10 = fib(10);            // 编译期计算 55
    std::array<int, fib(10)> data;            // 可使用,模板参数,编译期求值
    std::cout << data[0] << std::endl;
    return 0;
}

3.4 C++17:constexpr 与 Lambda

C++17 允许将 Lambda 声明为 constexpr(或隐式满足条件时自动成为 constexpr)。

直接用C++20.

cpp 复制代码
#include <iostream>
#include <array>   // 如果仅用于演示,可删除

int main() {
    auto lambda = [](int x) { return x * 2; };   // C++17 自动推断为 constexpr
    static_assert(lambda(3) == 6, "assertion failed"); // 必须提供第二个参数(C++17 单参数也可用)
    std::cout << lambda(3) << std::endl;
    return 0;
}

3.5 C++17:if constexpr

if constexpr 是一种编译期条件分支,用于模板或泛型代码中,根据模板参数在编译期选择代码路径。不成立的分支完全不会被实例化,从而避免了无效代码。

cpp 复制代码
#include <iostream>
#include <type_traits>   // 必须包含!

template<typename T>
auto get_value(T t) {
    if constexpr (std::is_pointer_v<T>) {
        return *t;          // T 为指针时解引用
    }
    else {
        return t;           // 非指针直接返回值
    }
}

int main() {
    int a = 42;
    int* p = &a;

    std::cout << get_value(a) << std::endl;   // 输出 42
    std::cout << get_value(p) << std::endl;   // 输出 42

    return 0;
}

4. const 与 constexpr 核心对比

|

特性 const constexpr
主要语义 只读(不可修改) 编译期求值
初始化 可在运行期初始化 必须用常量表达式初始化
是否隐含常量性 是(不能修改) 是(隐含 const,且编译期确定)
作为编译期常量 有条件(如果初始化器是常量表达式) 无条件保证
用于数组大小、模板参数 仅当初始化器为常量表达式时 总是可以
修饰函数 成员函数(承诺不修改对象) 普通函数/成员函数(可能编译期执行)
内存分配 常规变量,通常有存储 可无运行时存储,完全内联
实际选择指南:

如果仅需承诺"此值不会变",且值可能在运行时才能确定,用 const。

如果需要确保值在编译期就确定,用于模板、数组大小、性能关键处,用 constexpr。

设计接口时,对不可修改的引用/指针参数使用 const;对希望提供编译期计算的工具函数使用 constexpr。

5. 进阶话题简述

5.1 consteval (C++20)

consteval 强制函数必须在编译期执行,即创建"立即函数"。不同于 constexpr 可退化为运行期函数,consteval 函数若不能在编译期求值则会报错。

cpp 复制代码
#include <iostream>
#include <cstdlib>   // 用于 rand()
#include <ctime>     // 用于 time()

constexpr int twice(int x) {
    return x + x;
}

int get_runtime() {
    return rand() % 100;   // 返回一个运行时值
}

int main() {
    constexpr int a = twice(5);         // 编译期求值,OK
    int r = get_runtime();              // 运行时值
    int b = twice(r);                   // 运行时调用,OK(constexpr 函数允许运行时)
    std::cout << "a = " << a << ", b = " << b << std::endl;
    return 0;
}

5.2 constinit (C++20)

constinit 用于保证变量在编译期初始化,但其值仍可在运行时修改(即变量本身非 const)。它主要用于避免静态初始化顺序问题。

cpp 复制代码
constinit static int global = 42; // 编译期初始化,但 global 可被修改

5.3 常量表达式中的 const 与 constexpr

当 const 变量用字面量或 constexpr 函数初始化时,它本身就是一个常量表达式,几乎等同于 constexpr 变量,只是缺少"强制编译期"的保险。

cpp 复制代码
#include <iostream>

// 必须定义为 constexpr,才能在编译期求值
constexpr int square(int x) {
    return x * x;
}

int main() {
    // 这两个初始化器都是常量表达式
    const int a = 10;           // 字面量,常量表达式
    constexpr int b = square(10);   // 明确表示编译时计算 b也位const常量

    // 可以用作数组大小(需要编译期常量)
    int arr[a] = {};            // 大小为 10
    int arr2[b] = {};           // 大小为 100

    std::cout << "a = " << a << ", b = " << b << std::endl;
    std::cout << "Array sizes: " << sizeof(arr) / sizeof(arr[0])
        << " and " << sizeof(arr2) / sizeof(arr2[0]) << std::endl;

    return 0;
}

总结

常量是不可更改的值。

const 是"只读"的契约,值不一定编译期可知。

constexpr 是"编译期常量"的强制要求,是现代 C++ 编译期计算的基石。

善用 constexpr 可以将计算提前到编译期,提升性能与安全性;善用 const 可以明确代码意图,防止意外修改。

掌握 const 和 constexpr 的区别与联系,是深入 C++ 核心、写出高效安全代码的关键一步。

相关推荐
qq_452396232 小时前
第十三篇:《K8s 安全基础:RBAC、ServiceAccount、Pod Security》
java·安全·kubernetes
神仙别闹2 小时前
基于C++ 实现 BP 神经网络
开发语言·c++·神经网络
张某布响丸辣2 小时前
Spring AI 极简入门:Java 开发者快速上手 AI 开发
java·人工智能·spring·springai
java1234_小锋2 小时前
请描述 Spring Boot 的启动流程,包括 SpringApplication 的初始化和 run 方法的核心步骤。
java·数据库·spring boot
疯狂成瘾者2 小时前
Java 集合 LinkedList 详解:链表结构、常用方法和队列使用
java·开发语言·链表
云梦泽࿐้2 小时前
变量与数据类型:Python世界的基石
开发语言·python
QK_002 小时前
C语言 static 关键字三大作用
c语言·开发语言
lanyxp2 小时前
Sentinel 管不到 SQL 这一层——我写了个 MyBatis SQL 熔断器
java