C++基础入门教程(四):枚举和指针

发布时间:2026/10/20 20:13:28
C++基础入门教程(四):枚举和指针 前言枚举和指针是 C 入门阶段两块看似无关、实则都很容易会用但用不对的内容。枚举的坑集中在类型安全传统枚举的枚举值会泄漏到外层作用域还能悄悄转换成整数两个不同枚举之间也能互相比较编译器不会提醒你。指针的坑集中在生命周期和边界未初始化、悬垂、越界三者都是未定义行为undefined behavior, UB程序可能崩溃也可能看起来一切正常。本文按先建立模型、再写代码的顺序讲。枚举部分从传统枚举讲到 C11 的enum class说明底层类型和作用域规则指针部分讲地址、类型、解引用、const修饰位置、指针运算的合法边界最后给一个把两者结合起来的完整示例。示例以 C17 为准涉及 C11 特性的地方会注明。阅读时请记住一条底线本文的代码没有被编译或运行过所有结论都来自语言标准的语义推导。真实行为请以你自己的编译器和标准文档为准。一、传统枚举与枚举类传统枚举unscoped enumeration用enum定义// 适用于 C17enum LegacyColor { kRed, kGreen, kBlue };它有三个特点。第一枚举值kRed、kGreen、kBlue直接进入外层作用域相当于在这里声明了三个常量所以同一作用域内不能再定义同名标识符。第二枚举值可以隐式转换为整数int x kGreen;合法反过来整数转枚举则需要显式转换。第三不同枚举类型的值可以互相比较因为比较前都先转成了整数这让把颜色和状态拿来比较这种错误无法被类型系统拦住。C11 引入的枚举类scoped enumeration解决了这些问题// 适用于 C17enum class Color : std::uint8_t { Red, Green, Blue };枚举值必须写成Color::Red不会污染外层作用域不会隐式转换为整数不同枚举类之间不能直接比较。冒号后的std::uint8_t指定底层类型这在需要固定内存布局、与协议字段对接时很有用。不指定时枚举类的底层类型默认是int传统枚举的底层类型由实现决定只要足以容纳所有枚举值即可。特性传统enumenum class枚举值作用域泄漏到外层限定在枚举名内隐式转整数允许禁止不同枚举间比较允许都转整数禁止指定底层类型C11 起可指定可指定默认int前置声明需指定底层类型可直接前置声明二、枚举的取值、底层类型与位掩码枚举值默认从 0 开始依次加一也可以显式赋值。允许重复值也允许跳过某些值但要注意枚举的合法值范围不是最小到最大这么简单标准规定的是一个可以用若干位表示的区间把任意整数强制转换成枚举类型时落在区间外的值属于未定义行为。因此不要用static_castColor(99)去试探。枚举类做位掩码时要自己提供运算符因为标准不提供按位运算// 适用于 C17enum class Permission : unsigned {None 0u,Read 1u 0,Write 1u 1,Execute 1u 2};constexpr Permission operator|(Permission a, Permission b) {return static_castPermission(static_castunsigned(a) | static_castunsigned(b));}constexpr bool has(Permission set, Permission bit) {return (static_castunsigned(set) static_castunsigned(bit)) ! 0u;}constexpr让这些函数在编译期也能求值。注意operator|返回的是Permission这样has()的第二个参数类型才不会错。位掩码枚举通常把None设为 0并保证各标志位互不重叠。switch配合枚举时建议把每个枚举值都列出来。部分编译器能对遗漏的枚举值给出警告但这依赖编译选项不是标准要求。不要用default草草了事否则将来新增枚举值时编译器帮不上忙。三、指针保存地址的变量指针是一个保存地址的变量它的类型决定了解引用时按什么类型解释这块内存。声明int* p表示p里存的是一个int对象的地址。取地址用解引用用*// 适用于 C17int value 42;int* p value; // p 指向 value*p 43; // 通过 p 修改 valueint copy *p; // 读取 valuevalue的类型是int**p的类型是int左值。指针本身也是一个对象它有自己的地址所以可以有指向指针的指针int**但这在入门阶段很少需要。空指针用nullptr表示C11。NULL在多数实现里是整数常量0在重载解析时可能被当成int从而选错函数nullptr的类型是std::nullptr_t能隐式转换为任意指针类型但不会转成整数。新代码一律用nullptr。未初始化的指针里是 indeterminate 值读取它本身就是未定义行为更不用说解引用。养成声明即初始化的习惯暂时没有目标就写int* p nullptr;。需要区分指针指向的对象是否可改和指针本身是否可改这由const的位置决定写法指向的对象指针本身记忆方法const int* p不可改可改const 在*左边约束所指对象int* const p可改不可改const 在*右边约束指针const int* const p不可改不可改两边都有int const* p不可改可改与第一种等价从右往左读声明通常不会错int* const p读作p 是一个 const 指针指向 int。四、指针运算与数组边界数组名在多数表达式里会退化为指向首元素的指针所以可以用指针遍历数组// 适用于 C17int arr[5] {10, 20, 30, 40, 50};int* begin arr;int* end arr 5; // 指向末尾后一位合法但不可解引用for (int* it begin; it ! end; it) {std::cout *it \n;}指针算术只在同一数组或同一个完整对象内部有定义。arr 5是合法的尾后指针one-past-the-end可以用作循环终止条件但不能解引用。arr 6或arr - 1的运算本身就是未定义行为哪怕不 dereference。两个指针相减只有在指向同一数组时才有定义结果的类型是std::ptrdiff_t。sizeof(arr)得到整个数组的字节数sizeof(begin)得到指针本身的字节数后者由实现决定64 位平台通常是 8。把数组传给函数后形参类型退化为指针函数内部再也拿不到数组长度这是初学者最常见的困惑之一。解决办法是额外传长度或用std::array、std::vector、std::spanC20。完整示例状态机与权限表下面把枚举与指针放在一个示例里用枚举表示状态用指向函数的指针数组做状态处理表。示例只演示语法组织不含平台相关代码。// 适用于 C17#include cstddef#include iostreamenum class State { Idle, Running, Paused, Stopped };const char* to_string(State s) {switch (s) {case State::Idle: return Idle;case State::Running: return Running;case State::Paused: return Paused;case State::Stopped: return Stopped;}return Unknown;}// 函数指针类型接收 State返回 voidusing Handler void (*)(State);void on_idle(State) { std::cout waiting\n; }void on_running(State) { std::cout working\n; }void on_paused(State) { std::cout suspended\n; }void on_stopped(State) { std::cout done\n; }int main() {// 下标与 State 的整数值一一对应Handler table[] {on_idle, on_running, on_paused, on_stopped};State current State::Running;const std::size_t index static_caststd::size_t(current);if (index sizeof(table) / sizeof(table[0])) {table[index](current); // 通过函数指针调用}std::cout to_string(current) \n;int* nothing nullptr;if (nothing ! nullptr) {std::cout *nothing; // 只有非空才解引用}return 0;}这里用static_caststd::size_t把枚举转成下标并且在使用前检查了边界。函数指针数组的类型必须完全一致on_idle的签名是void(State)放进Handler数组才不会出错。sizeof(table) / sizeof(table[0])是 C 风格数组求长度的经典写法对指针不适用。常见坑点❌ 把enum class的值直接赋给int。✅ 必须static_castint(value)这正是枚举类防止误用的设计目的。❌ 用NULL初始化或比较指针。✅ 用nullptrNULL可能是整数0会在重载解析中产生意外。❌ 声明int* p;后直接*p 1;。✅ 未初始化指针的值不确定解引用是 UB声明时就初始化为nullptr或有效地址。❌ 用const int* p试图说明指针不能改。✅ 那是int* const pconst 在*左侧约束所指对象右侧约束指针本身。❌ 返回函数内局部变量的地址。✅ 函数返回后局部对象生命周期结束指针悬垂返回值、返回静态对象或由调用方提供缓冲区。❌ 认为arr n只要不解引用就没事。✅ 指针算术本身必须落在同一数组的[begin, end]闭区间内越界运算即 UB。❌ 对new[]分配的内存用delete释放。✅ 必须delete[]两者不匹配是 UB且通常伴随堆损坏。❌ 在switch里用default吞掉所有未列出的枚举值。✅ 逐个列出枚举值让编译器在新增枚举值时给出警告。总结主题要点传统枚举枚举值泄漏作用域可隐式转整数枚举类强作用域、强类型可指定底层类型枚举转换用static_cast不要越界构造非法值空指针一律nullptr解引用前判空const 位置左约束值右约束指针指针运算仅限同一数组尾后指针可比较不可解引用生命周期悬垂指针解引用是 UB返回局部地址必错枚举负责把一组有限的取值表达成类型指针负责间接访问对象两者本身都不复杂。真正需要训练的是边界意识枚举值是否合法、指针是否有效、下标是否越界。把这三问养成习惯入门阶段的大部分崩溃都可以提前避免。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询