[C++ 基础核心] 为什么 constexpr 指针编译报错?打破 declarations 结构与右左法则的语法高墙 | CppCon 精华

发布时间:2026/7/14 17:17:46

[C++ 基础核心] 为什么 constexpr 指针编译报错?打破 declarations 结构与右左法则的语法高墙 | CppCon 精华
导读摘要在 C 开发中我们经常遇到难以读懂的复杂声明或令人困惑的编译报错。比如为什么constexpr int *p;会直接报错而const int *p;却能顺利通过本博文基于 CppCon 2022 Ben Saks 的演讲“Back to Basics: Declarations in C”用**“修饰性零件”与“地基”**等生动比喻从零剖析 C 声明的底层解构式。文章详细拆解了decl-specifier-seq与declarator-list的爱恨情仇详解了读懂复杂声明的“右左法则”并以专家视角深度扩展了typename模板消除歧义、万能引用与引用折叠等现代 C 前沿机制。核心收获彻底搞懂 C 声明的底层编译逻辑免受编译报错折磨 前言那个让人百思不得其解的报错在写 C 代码时你是否写过类似这样的代码constint*p1;// 正常编译无任何警告constexprint*p2;// 编译报错error: default initialization of an object of const type const int * (or similar)这时候你的脑海中一定有很多小问号const和constexpr不都代表常量吗为什么一个不需要初始值而另一个必须初始化它们修饰的到底是指针本身还是指针指向的数据今天我们就跟着 C 标准委员会专家 Ben Saks一起拆解 C 声明Declarations的底层原理打碎这堵语法高墙️ 第一章声明 vs 定义——变量的“户口登记”与“分房入住”在进入复杂语法前我们先理清两个最容易混淆的词声明Declaration与定义Definition。[!TIP]生活类比声明就像是给孩子上户口登记名字。你跟派出所说“我们家要多一个叫张三的人了。” 此时派出所知道了这个名字但并没给张三分配具体的住房。定义则是真正盖楼并分房入住。不仅登记了名字还分配了实在的物理内存空间。// 仅声明 externintx;// 登记“有一个整型变量 x 在别处我们先用它”voidfoo(inta);// 登记“有一个函数 foo 接收 int具体实现待会儿说”structWidget;// 登记“有一个叫 Widget 的结构体类型”// 同时也定义 intx;// 盖楼在内存里划出 4 字节给 xvoidfoo(inta){}// 盖楼为 foo 分配函数体代码区structWidget{intid;};// 盖楼明确了 Widget 里面长啥样[!IMPORTANT]C 黄金法则在 C 中所有定义都是声明但并不是所有声明都是定义。编译器在编译期只需要知道名称的“声明”就可以编译但连接器在最后打包时必须找到“定义”否则就会报unresolved external symbol错误。 第二章解构 C 声明的底层结构C 的声明语法源自 C 语言。Ben Saks 指出几乎所有 C 声明都可以被拆解为两个核心组件decl-specifier-seq declarator-list ; \text{decl-specifier-seq} \quad \text{declarator-list};decl-specifier-seqdeclarator-list;换句话说声明就是【地基基类型说明符】【各种修饰性零件声明符】。int const *p1, *const p2; └────────┘ ──┬── ───┬── │ │ │ decl-specifier-seq │ │ (地基) │ declarator 2 (零件) │ declarator 1 (零件)2.1 地基decl-specifier-seq声明说明符序列这部分定义了变量的基础类型以及存储属性。它们会作用于后面逗号分隔的所有变量。基本类型int、double、Widget等CV 限定符const、volatile存储属性static、extern、thread_local常量级别修饰constexpr、consteval2.2 零件declarator-list声明符列表这部分用来决定我们声明的到底是一个普通的变量还是它的指针、引用或数组。修饰符*指针、左值引用、右值引用、[]数组、()函数标识符变量的名称。[!CAUTION]最经典的新手陷阱int*p,q;很多人以为p和q都是指针。但实际上int是地基应用于全部。*p是第一个零件声明p是指针。q是第二个零件由于没有*修饰它只是普通的int变量。正确的读法是p是int*而q是int。 第三章East Const 哲学与“右左法则”3.1 为什么推荐 “East Const”因为decl-specifier-seq里的说明符可以任意排列所以下面两行是等价的constintx10;// West Const (左派)intconstx10;// East Const (右派)Ben Saks 强烈推荐使用East Const将const写在类型的右边。因为这能极大地帮助我们使用从右向左Right-to-Left的规则来读懂声明。3.2 终极阅读密码右左法则 (Right-to-Left Rule)想读懂任何复杂的 C 声明只需要像剥洋葱一样从标识符开始先向右看再向左看。实战演练 1int const *p;找到标识符p。往右看什么都没有。往左看遇到*⟹p是一个指针pointer to…。继续往左看遇到const⟹ 指向一个常量const…。继续往左看遇到int⟹ 常量是int。翻译p是一个指向常量整型的指针。指针可变内容不可变。实战演练 2int *const p;找到标识符p。往右看什么都没有。往左看遇到const⟹p本身是常量const…。继续往左看遇到*⟹ 这个常量是一个指针pointer…。继续往左看遇到int⟹ 指向int。翻译p是一个指向整型的常量指针。指针不可变但内容可变。int const * p ; vs int * const p ; (4) (3) (2) (1) (4) (3) (2) (1) └───────┬─────┘ │ │ └─────┬─────┘ │ │ │ │ const int 指针 int const指针 第四章解密为什么constexpr int *p;报错回到前言的痛点问题为什么const int *p;能过而constexpr int *p;报错通过第三章的拆解我们来看看编译器是怎么理解这两个声明的const int *p1;const是类型限定符它修饰的是基类型int。p1是一个普通的指针它指向const int。既然p1只是普通变量它在声明时不需要赋初值。constexpr int *p2;重点来了constexpr不是常规的类型限定符它是非类型说明符它修饰的是整个被声明的变量本身。因此无论你把constexpr写在左边还是右边它都等价于将指针本身声明为常量即int *const p2;。在 C 中所有常量对象在声明时都必须初始化如果不给初始值编译器就不知道它的常量值是什么自然会无情报错。// 修复方法staticintval42;constexprint*p3val;// ✅ 成功编译p3 本身在编译期是固定地址的常量指针 第五章专家深潜——超越视频的延伸硬核知识以下内容是站在 C 专家视角为你补充的高阶声明技巧与底层机制。5.1 消除编译歧义的typename在模板泛型编程中编译器经常会遇到“傻傻分不清”的尴尬templatetypenameTvoidrun(){T::NestedType*p;// ⚠️ 编译器抓狂了}编译器的两难处境如果T::NestedType是一个类型那这一行是声明p是一个指向该类型的指针。如果T::NestedType是一个静态变量那这一行是乘法表达式T::NestedType乘以p。C 标准规定默认情况下依赖于模板参数的嵌套名字Dependent Names一律被视为非类型即变量/值。所以为了让声明成立我们必须加上typenametemplatetypenameTvoidrun(){typenameT::NestedType*p;// ✅ 明确告诉编译器这真的是个类型声明}5.2 万能引用Forwarding References与引用折叠在声明中通常代表右值引用但如果配合类型推导它就变成了“万能引用”templatetypenameTvoidprocess(Targ);// T 是万能引用既能接左值也能接右值当左值传入时T会被推导为左值引用如int此时声明中就会出现“引用的引用”int arg。C 通过**引用折叠Reference Collapsing**来强行平展这种多重引用 (左值引用) (左值引用) (左值引用) (右值引用)[!TIP]极简口诀见左即左——只要有一个左值引用参与折叠结果必定折叠为单。只有当全部为双时结果才是双。 第六章一图胜千言——C 声明全景思维导图C 声明结构decl-specifier-seq(地基 / 属性与基类型)declarator-list(零件 / 修饰符与名称)基本类型 (int, double)CV限定符 (const, volatile)非类型说明符 (constexpr, static)* (指针) / (引用)[ ] (数组)( ) (函数)constexpr 修饰变量本身constexpr 指针等价于 *const 指针常量必须当场初始化万能引用 (T / auto)引用折叠规则 总结三步读懂任何 C 声明寻找标识符从变量名开始出发。右看再左看先看右边有没有数组[]或函数()再看左边有没有指针*或引用。分清地基与零件基类型如int const是所有人共有的而*或仅仅属于绑在它身边的那个变量。记住用 East Const 的视角看世界复杂声明也不过是各种积木零件的组合 推荐阅读 系列文章本文是CppCon “Back to Basics” 系列精讲的第 6 篇SEO 长尾关键词布局C 声明与定义区别、constexpr 指针编译报错、C 右左法则读复杂声明、decl-specifier-seq 与 declarator 区别、East Const 优缺点、typename Dependent Names 消除歧义、万能引用与引用折叠原理视频来源CppCon 2022 - Back to Basics: Declarations in C - Ben Saks本文作者立场以 C 标准专家视角结合视频内容与实际开发经验撰写。如有疑问欢迎评论区讨论

相关新闻

定时/计数器:从原理到实战,解锁单片机精准时序控制

定时/计数器:从原理到实战,解锁单片机精准时序控制

2026/7/14 17:17:46

1. 定时/计数器:单片机的时间指挥官第一次接触51单片机时,你可能用过delay()函数做延时——让CPU空转循环来消耗时间。这种简单粗暴的方式就像让厨师亲自数秒煮鸡蛋,不仅效率低下,还会耽误其他工作。而定时/计数器就是为解决这个问…

GDB调试实战:从连接错误到堆栈分析,系统化解决C++程序崩溃问题

GDB调试实战:从连接错误到堆栈分析,系统化解决C++程序崩溃问题

2026/7/14 17:17:46

1. 项目概述:当C程序在GDB中“崩溃”时,我们到底在看什么?调试C程序,尤其是处理那些只在特定环境下才触发的、难以复现的崩溃或异常时,GDB(GNU Debugger)几乎是每个C开发者绕不开的终极武器。但…

256K上下文长度:NVIDIA Kimi-K2.7-Code-NVFP4长文本处理完全指南

256K上下文长度:NVIDIA Kimi-K2.7-Code-NVFP4长文本处理完全指南

2026/7/14 17:17:46

256K上下文长度:NVIDIA Kimi-K2.7-Code-NVFP4长文本处理完全指南 【免费下载链接】Kimi-K2.7-Code-NVFP4 项目地址: https://ai.gitcode.com/hf_mirrors/nvidia/Kimi-K2.7-Code-NVFP4 NVIDIA Kimi-K2.7-Code-NVFP4是一款基于Moonshot AI的Kimi-K2.7-Code模型…

工业负载控制方案:TPD2017FN与PIC18F46K22实战应用

工业负载控制方案:TPD2017FN与PIC18F46K22实战应用

2026/7/14 18:37:54

1. 工业负载控制的核心挑战与选型思路在工业自动化产线上,电感和电阻负载的控制一直是电气工程师面临的棘手问题。去年我在某包装机械项目中就遇到过这样的场景:当电磁阀快速切换时,产生的反向电动势导致整个控制板频繁重启;而电阻…

Unity UI -- (7) 世界空间UI:从Canvas渲染模式到3D场景交互设计

Unity UI -- (7) 世界空间UI:从Canvas渲染模式到3D场景交互设计

2026/7/14 18:37:54

1. Canvas渲染模式的核心差异 第一次接触Unity UI系统时,我也被Canvas的三种渲染模式搞得一头雾水。直到做了几个VR项目后,才真正理解它们的本质区别。简单来说,这三种模式决定了UI元素在游戏世界中的存在方式。 Screen Space-Overlay是最简…

历史VaR实战指南:不假设分布的可审计风险度量方法

历史VaR实战指南:不假设分布的可审计风险度量方法

2026/7/14 18:37:54

1. 项目概述:为什么一个真实的、不假设分布的VaR才值得放进你的风险报告里我做量化风控这行快十二年了,从给银行做巴塞尔III合规系统,到后来带团队给私募基金搭实时风险引擎,踩过的坑比读过的论文还多。今天这篇不是教科书复述&am…

C# DirectX视频播放器开发:从零构建高性能渲染管线

C# DirectX视频播放器开发:从零构建高性能渲染管线

2026/7/14 18:37:54

1. 项目概述与核心价值 如果你正在用C#开发一个视频播放器,并且对Windows Media Player控件或者基于GDI的简单渲染感到性能瓶颈,或者需要更底层的控制来处理高清、高帧率视频,那么直接使用DirectX进行渲染几乎是必经之路。我最近刚完成一个用…

基于JSR380规范的外卖API接口参数校验链:自定义约束注解开发实践

基于JSR380规范的外卖API接口参数校验链:自定义约束注解开发实践

2026/7/14 18:37:54

基于JSR380规范的外卖API接口参数校验链:自定义约束注解开发实践 在构建外卖CPS(Cost Per Sale)返利平台时,API接口的健壮性是系统稳定运行的第一道防线。面对海量的外部请求,如果依赖传统的if-else进行参数校验&#…

ClaudeSkills:垂直领域AI模块化解决方案解析

ClaudeSkills:垂直领域AI模块化解决方案解析

2026/7/14 18:27:53

1. ClaudeSkills的核心价值与定位ClaudeSkills作为新一代AI辅助工具,其核心价值在于填补了传统AI模型在特定垂直领域的应用空白。不同于通用型AI助手,ClaudeSkills通过模块化技能包的设计,实现了对专业场景的深度适配。我在实际使用中发现&am…

Unity游戏文本翻译架构深度解析:XUnity.AutoTranslator的技术实现与工程实践

Unity游戏文本翻译架构深度解析:XUnity.AutoTranslator的技术实现与工程实践

2026/7/14 10:03:09

Unity游戏文本翻译架构深度解析:XUnity.AutoTranslator的技术实现与工程实践 【免费下载链接】XUnity.AutoTranslator 项目地址: https://gitcode.com/gh_mirrors/xu/XUnity.AutoTranslator XUnity.AutoTranslator作为Unity游戏社区中最成熟的文本翻译解决方…

openEuler Raspberry Pi Kernel设备驱动开发指南:为树莓派硬件添加支持

openEuler Raspberry Pi Kernel设备驱动开发指南:为树莓派硬件添加支持

2026/7/13 20:43:19

openEuler Raspberry Pi Kernel设备驱动开发指南:为树莓派硬件添加支持 【免费下载链接】raspberrypi-kernel It provides openEuler kernel source for Raspberry Pi 项目地址: https://gitcode.com/openeuler/raspberrypi-kernel 前往项目官网免费下载&…

openEuler系统集成测试实战:基于smoke-test套件的环境验证技巧

openEuler系统集成测试实战:基于smoke-test套件的环境验证技巧

2026/7/13 20:43:10

openEuler系统集成测试实战:基于smoke-test套件的环境验证技巧 【免费下载链接】integration-test The repo contains test suits for system integration test 项目地址: https://gitcode.com/openeuler/integration-test 前往项目官网免费下载:…

XUnity.AutoTranslator 游戏实时翻译插件:从原理到实战的完整指南

XUnity.AutoTranslator 游戏实时翻译插件:从原理到实战的完整指南

2026/7/14 0:06:37

1. 项目概述:当游戏语言成为一堵墙作为一名玩了十几年日系、欧美独立游戏的“老油条”,我太懂那种面对一款心仪已久、画风玩法都戳中G点的游戏,却因为语言不通而望而却步的痛了。尤其是那些基于Unity引擎开发的、体量不大但内容精良的作品&am…

2026普通文员学数据分析的价值

2026普通文员学数据分析的价值

2026/7/14 0:06:37

一、2026年普通文员学习数据分析的必要性随着数字化转型加速,数据分析技能正逐渐成为职场基础能力。普通文员学习数据分析可以提升工作效率、增强竞争力,并为职业转型提供更多可能性。二、数据分析对文员的价值自动化办公:通过数据分析工具&a…

2026从计划员到主管,生产管理者学数据分析有用吗?

2026从计划员到主管,生产管理者学数据分析有用吗?

2026/7/14 0:06:37

一、生产管理领域的职业发展路径 从计划员到主管的角色转变,是生产管理者职业发展的典型路径。计划员主要负责生产排程、库存管理和资源协调等基础工作,而主管则需要承担团队管理、决策支持和效率优化等更高级别的职责。这种转变不仅仅是职位的提升&…