☰
constexpr const char* 探秘:字符串字面量与编译期常量指针
2026/10/13 1:17:03 网站建设 项目流程

1. 看到constexpr const char*时,我为什么第一反应是“这能过编译?”

大概每个刚开始接触现代 C++ 的人,第一次看到constexpr const char* kTag = "auth";都会愣一下。老实说,我当年在代码审查里第一次看到类似写法时,脑子里第一反应就是:这东西真能编译通过?constexpr不是要求在编译期就能算出值吗?一个字符串的地址,难道不是要等程序加载运行之后才知道它落在内存的哪个位置?这个疑问后来把我带进了存储期和常量表达式的细节里绕了好几圈,也是我在给同事讲这个知识点时积累下来的一套比较顺的解释路径。

先说结论:在 C++11 及以后的标准里,constexpr const char*是合法且非常常见的一种写法。它表示“一个可以在编译期求值的、指向 const char 的指针变量”。之所以很多人觉得它不该合法,是因为把两个维度的事混在了一起:一个是“指针变量本身的值(也就是地址)是不是常量表达式”,另一个是“被指向的那串字符内容是否在编译期固定”。后者的答案取决于存储期,前者的答案取决于constexpr的初始化规则。下面把这两层逐一拆开。

1.1 一个常见直觉的误区

很多人会把constexpr对象理解成“编译期就得有的纯值”,就像constexpr int n = 42;那样。既然指针也能加constexpr,那它指向的地址到底从哪来?栈上?堆上?程序不跑起来怎么知道?

这里其实忽略了一个事实:程序里存在一类对象,它们的存储位置在链接期就已经确定了,根本不需要等到程序运行。字符串字面量就属于这类对象。再说直白一点,字符串字面量不是“运行时临时拼出来的值”,它是被编译器放进可执行文件某个数据段里的数组对象。只要可执行文件本身不改变布局,这个数组的地址就是一个编译期、链接期可定位的常量,而不是凭空冒出来的运行期数字。

我再举一个更直观的例子:constexpr const int* p = &global_value;同样是合法的,只要global_value是一个全局对象。如果你在函数里写constexpr const int* p = &local_value;,编译器会直接拒绝,因为局部变量的栈地址在每次函数调用时才会分配,不可能在编译期给出来。字符串字面量属于前一种场景,不是后一种。搞清楚了这一层,这个语法点的很多困惑都会自然消失。

2. 字符串字面量的静态存储期:地址为什么能在编译期确认

要真正理解constexpr const char*,绕不开一个概念:存储期。C++ 里的对象按存储期可以分为自动、静态、线程和动态四类,它们最大的区别之一就是“地址什么时候能确定”。

2.1 四种存储期,谁能在编译期拿到地址

存储期生命周期起点地址何时确定典型对象
自动存储期进入作用域/函数调用时栈帧创建后,运行时确定普通的局部变量
静态存储期程序加载期间链接期基本确定全局变量、静态成员、字符串字面量
线程存储期线程创建时线程私有存储区分配时thread_local变量
动态存储期new/malloc调用时堆分配成功后堆对象

从这个表就能看出,只有静态存储期对象才有资格在编译期被“指着”。自动存储期对象每次函数调用地址都可能变,动态存储期对象更是完全看运行时堆的状态,这些都不属于常量表达式的范畴。

2.2 “hello” 不只是一段字符,它还是一个数组对象

很多人意识不到一个细节:字符串字面量不仅是一串字符,它本身是一个带类型的数组对象。在 C++ 里:

decltype("hello") // const char(&)[6],注意是一个引用类型 sizeof("hello") // 6,包含末尾的 '\0'

这说明"hello"是const char[6]类型的左值。它有真实的存储、真实的地址,和整数常量42这种纯右值完全不同。42没有地址可言,但"hello"有。当我们写:

constexpr const char* p = "hello";

实际上发生的是数组到指针的退化:"hello"这个数组对象退化成指向其首元素的const char*指针值。而因为原数组是静态存储期对象,这个指针值就是一个地址常量表达式,自然可以被constexpr接受。

这里顺带提一个常见的报错场景。很多人会写:

void foo() { const char local[] = "hello"; constexpr const char* p = local; // 编译错误 }

为什么错?因为local是自动存储期的局部数组,虽然它用字符串字面量初始化,但数组本身在栈上,地址要等foo()被调用之后才存在。字符串字面量是静态的,local不是。这是两个完全不同的东西,初学者特别容易混淆。

2.3 静态存储期地址为什么能算常量表达式

从标准的角度看,指向静态存储期对象的指针可以出现在地址常量表达式中。这个规则允许的值包括:nullptr、指向静态存储期对象的指针值、指向函数的指针值,以及由满足条件的表达式经数组到指针退化、函数到指针退化等转换得到的指针值。

你可能会问:现代操作系统不是有地址随机化吗?程序每次加载,全局变量和字符串字面量的实际地址都不一样,这还算“编译期常量”吗?

答案是:C++ 的抽象机只看链接期和编译期可确定的地址,不关心操作系统加载到哪个虚拟地址。地址随机化是加载器的事,运行期重定位之后,操作系统会保证这些符号之间的相对关系不变。而constexpr要的是一个能在编译阶段被当作常量处理的地址值,不是非得等于运行时那串物理地址。这一点理解到位,就不会再因为 ASLR 之类的东西产生无谓的困惑。

3. 指针型constexpr变量到底能装哪些值:规则与直觉对齐

讲完存储期,接下来可以正式看constexpr指针的取值规则了。很多人以为“只要初始化成一个常量就行”,但具体什么算常量,标准里其实划了明确的边界。

3.1constexpr变量对初始化器的要求

constexpr变量有一个硬性要求:初始化器必须是常量表达式。普通变量没有这个要求,所以const char* p = "hello";人人会写,但不代表const char* p本身能被当作常量表达式使用。只有加上constexpr,这个指针值才具备出现在模板实参、数组大小、static_assert等编译期上下文中的资格。

注意,constexpr并不要求变量只存在于编译期。它的意思是:这个变量一定可以被编译期求值,同时在运行期它也仍然是一个真实存在的普通变量,只是由于constexpr隐含的const,你在运行时不能再去修改它。这对指针来说同样成立。

3.2 合法的初始化器清单

概括一下,能用于初始化constexpr const char*的常见值包括:

  • nullptr,空指针值;
  • 字符串字面量,如"hello",前提是数组退化为指针后指向静态存储期对象;
  • 其他全局数组、全局变量取地址的结果,如&global_array[0];
  • 一个constexpr函数返回的、满足上述条件的指针。

反过来,下面这些都不行:

  • 自动存储期局部变量的地址;
  • 动态分配的内存指针;
  • reinterpret_cast产生的地址;
  • 运行时才能确定值的任意表达式。

这个清单其实和“哪些地址是编译期可定位的”是同一件事。你只要把对象存储期判断对了,constexpr指针能不能写就基本不会判断错。

3.3constexpr char*和constexpr const char*为什么都在,但你最好只用后者

这里有一个历史包袱。C 语言里,字符串字面量类型一直被定义为char[N],而不是const char[N],允许你把它直接赋给char*。C++ 虽然把底层类型改成了const char[N],但为了兼容 C,仍然保留了字符串字面量到char*的隐式转换路径。

所以你会看到:

constexpr char* p = "hello"; // 能编译,但这是历史遗留的“危险”写法 constexpr const char* cp = "hello"; // 推荐,指向 const char constexpr const char* const ccp = "hello"; // 显式写出指针自身 const,也更明确

constexpr char* p = "hello";能编译,但如果你试图p[0] = 'H';,代码也能编译,运行结果却属于未定义行为。因为字符串字面量的底层类型是const char[6],修改const对象是违法的,只是历史兼容让类型检查漏了一条口子。

在实际项目里,我强烈建议一律使用constexpr const char* const。虽然constexpr已经隐含了指针变量自身的const,再写一个顶层const属于重复约束,但这样写最直白:指针不可变、指向内容也不可变。代码审查时也更省口舌。

3.4 别拿static_assert去比较两个字符串字面量的地址

还有一个很容易踩的坑:有人会用static_assert(kA == kB)来判断两个constexpr const char*是否指向同一个字符串。这里比较的是指针地址,而不是内容。标准并不保证两个内容相同的字符串字面量一定共享存储,编译器完全可以选择各自存放一份。所以:

constexpr const char* a = "hello"; constexpr const char* b = "hello"; static_assert(a == b); // 很可能 true,但不是标准保证的!

如果确实需要比较内容,用std::string_view、编译期遍历字符,或者干脆按字节处理。依赖字符串驻留机制来写比较逻辑,早晚会翻车。

4. 从能编译到不能编译:几个真实场景的边界验证

只看规则容易记不住,我带你把几个典型场景跑一遍。这些场景我基本都在真实项目里遇到过,每一个都是很好的边界测试。

4.1 基本合法场景:字符串字面量初始化

constexpr const char* kLogTag = "auth"; static_assert(kLogTag[0] == 'a'); // 可在编译期访问字符 constexpr char kFirst = kLogTag[1]; // 'u'

这段代码在 C++11 以后都能编译。kLogTag指向"auth"的首元素,整个数组是静态存储期,编译期访问kLogTag[0]、kLogTag[1]都没有问题。

4.2 constexpr 函数返回字符串字面量

在 C++14 之后,constexpr函数体里可以有循环了,于是能写出编译期计算字符串长度这种工具:

#include <cstddef> constexpr std::size_t cstrlen(const char* s) { std::size_t n = 0; while (s[n] != '\0') { ++n; } return n; } constexpr const char* kMsg = "hello"; static_assert(cstrlen(kMsg) == 5);

这个cstrlen在编译期对字符串字面量求值时,参数s指向的就是静态存储期数组,所以循环里的每次读取都能在编译期完成。它也可以用于运行期调用,那时候就是一份普通函数代码,完全没问题。这正是constexpr函数的特性:既能编译期求值,也能运行期调用。

4.3 不能编译的场景:自动存储期对象地址

void demo() { const char local_name[] = "alice"; constexpr const char* p = local_name; // 错误:局部数组地址不是常量表达式 const char* q = local_name; // 合法:普通指针可以在运行期指向栈对象 }

这是我在实际答疑时最常被弄混的一条。很多初学者问:“local_name内容里的"alice"不是字符串字面量吗?为什么加上constexpr就报错?”原因就是local_name是自动存储期的局部数组,地址要在运行时栈帧创建后才能确定,和底下的初始化字符串是不是字面量没有关系。字面量的静态存储期不会传递给局部数组变量。

4.4 能用于编译期上下文:数组边界、哈希、模板实参

指针是constexpr的真正意义,在于它能把字符串内容带进编译期计算。比如编译期算一个 FNV-1a 哈希:

#include <cstdint> constexpr std::uint32_t fnv1a(const char* s) { std::uint32_t h = 2166136261u; while (*s) { h ^= static_cast<unsigned char>(*s); h *= 16777619u; ++s; } return h; } constexpr const char* kType = "user"; constexpr std::uint32_t kTypeHash = fnv1a(kType); static_assert(kTypeHash == fnv1a("user"));

编译期就把哈希值算好,运行时拿去查表时就是零成本。再比如用constexpr字符串长度作为数组边界:

constexpr const char* kName = "alice"; constexpr int kNameLen = cstrlen(kName); char replica[kNameLen + 1]{};

replica的数组大小在编译期就确定下来,不再需要std::strlen调用来折腾运行期缓冲区。

4.5 一个容易忽视的边界:constexpr 函数的运行期参数

有一点值得提醒:constexpr函数不一定非要在编译期执行。如果你在运行期传入一个自动存储期的char数组,cstrlen也会在运行期正常工作。反过来,如果你希望某个调用一定在编译期失败,而不希望它静默落到运行期,可以用std::is_constant_evaluated()之类的工具去判断。在 C++20 里这种需求尤其常见,不过大部分业务代码用不到,知道有这回事即可。

5. 当字符编码遇上constexpr:u8 字面量与编译期字节处理

标题里既然带了“字符编码”这个标签,就必须讲讲编码和constexpr const char*交织时的那些细节。最典型的就是u8"..."前缀字面量。

5.1 各种前缀字面量的底层类型差异

前缀字面量类型说明
无前缀const char[N]执行编码,通常是 UTF-8,但实现定义
u8(C++17 及以前)const char[N]UTF-8 编码
u8(C++20 及以后)const char8_t[N]UTF-8 编码,char8_t是独立类型
uconst char16_t[N]UTF-16 编码
Uconst char32_t[N]UTF-32 编码
Lconst wchar_t[N]宽字符,不同平台宽度不一样

这个表格里最值得注意的坑是u8字符串的类型在 C++20 发生了变化。C++17 里,你可以写:

constexpr const char* p = u8"hello"; // C++17 合法

但到了 C++20,u8"hello"的类型是const char8_t[N],char8_t和char是不同类别,不能隐式转换,上面的初始化就会编译失败。必须改成:

constexpr const char8_t* p = u8"hello"; // C++20 合法

如果你的项目有一批constexpr const char*的旧代码,升级 C++20 后遇到u8字面量报类型错误,先检查这里。

5.2 编译期遍历 UTF-8 字节序列

有了constexpr,我们可以在编译期对 UTF-8 字符串做字节级处理。下面写一个统计 UTF-8 编码字符个数的函数,它对char和char8_t都适用:

template<class CharT> constexpr bool is_utf8_continuation(CharT c) { const auto u = static_cast<unsigned int>(static_cast<unsigned char>(c)); return (u & 0xC0u) == 0x80u; } template<class CharT> constexpr int utf8_chars(const CharT* s) { int count = 0; while (*s != CharT(0)) { if (!is_utf8_continuation(*s)) { ++count; } ++s; } return count; }

调用方式:

constexpr const char* s = "你好世界"; static_assert(utf8_chars(s) == 4); // C++20 下也可以用 char8_t constexpr const char8_t* u8s = u8"你好世界"; static_assert(utf8_chars(u8s) == 4);

原理是:UTF-8 编码中,每个多字节字符除首字节外,其余字节都以10xxxxxx的形式开头。所以统计所有“不是连续字节”的字节数,就能得到字符数量。整个过程在编译期完成,不依赖任何运行期状态机或第三方库。这在做一些协议解析、嵌入式日志前缀、资源表预校验时非常实用。

5.3 源码编码和执行编码的坑

最后提醒一个和“字符编码”直接相关的坑:普通无前缀字符串字面量的编码是“执行字符集”,由编译器实现决定。现在的桌面级编译器普遍默认源码和执行字符集都是 UTF-8,但这在标准层面并不是强制保证。如果你们项目还在用其他编码保存源码,或者编译选项里指定了别的执行字符集,那么"你好世界"的实际字节序列可能就不是 UTF-8,utf8_chars统计出来的结果也就不是你期望的值。

所以生产代码里,如果对字符编码有明确要求,尽量使用u8"..."前缀,并且在 C++20 下配合const char8_t*使用。这样字节序列从语言层面就和 UTF-8 绑定,不会因为执行字符集的变化而漂移。constexpr加在这里还有一个额外好处:编码相关的问题会在编译期就暴露出来,比如写错了多字节序列的结构,static_assert能第一时间拦住,而不是等程序跑到某个业务分支才出乱子。

每次帮同事解释完这个知识点,我都会补一句:把constexpr const char*理解成“指针本身和指向内容都编译期确定,内容只读”最稳妥。遇到想修改字符串内容的需求,第一时间考虑std::string_view或std::array<char, N>,不要跟字面量较劲。如果你也曾对这个语法点困惑过,按这个思路给自己讲一遍,应该比直接背一句“标准就是这么写的”要容易接受得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询