第四章:复合类型 | C++ Primer Plus 重点带看

数组下标的有效性检查

下标有效性必须由程序员手动保证,C++ 本身不提供自动越界检查。

  • 编译阶段:编译器不会对数组下标是否越界进行检查,即使下标超出数组范围,代码仍可正常编译。
  • 运行阶段:若使用无效下标访问数组,会导致未定义行为,而非一定会抛出错误:可能表现为程序崩溃、数据篡改、逻辑异常等;部分系统 / 编译器可能会触发段错误(Segmentation Fault)或内存访问违规。
int arr[5] = {1,2,3,4,5};
cout << arr[10]; // 编译通过,但运行时行为未定义

sizeof 运算符与数组名

当 sizeof 直接作用于数组名时,返回的是整个数组占用的总字节数,而非单个元素或指针大小。

int arr[5];
cout << sizeof(arr);  // 输出 5 * sizeof(int) = 20(假设 int 占 4 字节)

绝大多数场景下,数组名会被隐式转换为指向数组首元素的指针(即 &arr[0]),此时它的行为与普通指针一致:

int arr[5];
int* p = arr;  // arr 隐式转换为 int*,指向 arr[0]
cout << sizeof(p);  // 输出指针大小(如 8 字节,64 位系统)

只有在两种情况下数组名不会退化为指针:

  • 作为 sizeof 的操作数。
  • 作为 &(取地址)的操作数(&arr 得到指向整个数组的指针,类型为 int(*)[5])。
  • int arr[5];
    cout << sizeof(arr);     // 20(数组总大小)
    cout << sizeof(&arr);    // 8(指向数组的指针大小,64位)
    cout << sizeof(arr[0]);  // 4(单个 int 元素大小)
    

    C++11 数组初始化方法

    初始化数组时,= 号可以省略,写法更紧凑。

    大括号内为空时,数组所有元素会被自动初始化为 0(数值类型)。

    列表初始化严格禁止缩窄转换(即目标类型无法完整保存原值的转换),编译器会直接报错。

    double earnings[4] {1.2e4, 1.6e4, 1.1e4, 1.7e4}; // C++11 允许省略 =
    
    unsigned int counts[10] = {}; // 全部元素设为 0
    float balances[100] {};       // 全部元素设为 0(也可省略 =)
    
    long plifs[] = {25, 92, 3.0};     // ❌ 非法:3.0 是 double,转 long 属于缩窄
    char slifs[4] {'h', 'i', 1122011, '\0'}; // ❌ 非法:1122011 超出 char 取值范围
    char tlifs[4] {'h', 'i', 112, '\0'};     // ✅ 合法:112 在 char 取值范围内
    

    strlen( )

    strlen() 只计算字符串中可见的有效字符不包含末尾的空字符 \0,并且遇到 \0 停止计算

    char s[] = "hello";
    strlen(s) == 5   // 只算 h e l l o
    sizeof(s) == 6   // 算 h e l l o + \0
    

    cin 读取字符串

    使用 cin 读取字符串时,会以空白符(空格、换行、制表符)作为读取结束的标志,读到空白符即停止读取,并自动在已读取字符的末尾添加空字符 ’\0‘,形成合法的 C 风格字符串空白符本身以及其后的所有内容并不会被丢弃,而是保留在输入缓冲区中,等待后续的输入操作继续读取。

    注意:cin 会在读取前自动跳过前导空白符,所以连续使用 cin >> 不会出问题。

    std::string a, b;
    std::cin >> a;          // 输入: hello world↵  → a = "hello"," world\n" 留在缓冲区
    std::cin >> b;          // 自动跳过空白符,读到 "world"  ✅ 也没问题
    

    面向行的输入 getline( )

    与 cin 的区别:

    cin

    getline

    遇到空白符

    停止读取,不消费(留在缓冲区)

    继续读取,作为内容的一部分

    遇到 \n

    停止读取,不消费(留在缓冲区)

    停止读取,消费掉 \n(丢弃)

    前导空白符

    自动跳过

    不跳过,原样读取

    int age;
    std::string name;
    
    std::cin >> age;                    // 输入: 20↵
    // 缓冲区: '\n'                     ← cin >> 不消费 '\n'
    std::getline(std::cin, name);      // 读到 '\n',立即停止,name = ""
    std::cout << "name: " << name;     // 输出: name:      ← 空的!
    
    // 解决办法
    std::cin >> age;
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');  // 遇到 \n 停止丢弃,但最多可以丢弃的字符数为第一个参数设置的大小。
    std::getline(std::cin, name);   // ✅ 正常读取
    // std::cin.ignore( 参数1, 参数2 );
    //                    │     │
    //                    │     └── 遇到这个字符就停止丢弃
    //                    └── 最多丢弃多少个字符
    
    

    结构体部分初始化

    C++ 中对结构体进行初始化时,如果提供的初始值少于成员个数,剩余成员会被默认初始化为 0(对于整型是 0,指针是 nullptr,布尔是 false)。

    // C++17 — 支持指定初始化器,但必须按声明顺序;C++20 和 C 语言一样,允许乱序
    struct Point { int x, y, z; };
    
    Point p{.x = 1, .y = 2, .z = 3};   // ✅ 按声明顺序
    Point p{.x = 1, .z = 3};           // ✅ 跳过了 y,y 自动补 0,顺序正确
    Point p{.z = 3, .x = 1};           // ❌ C++ 17 编译报错!z 在 y 后面,必须先写 x 再写 y 再写 z。(C++ 20 不会报错)
    

    标准

    是否支持指定初始化器

    是否运行乱序

    示例

    C99

    支持

    允许乱序

    {.z = 3, .x = 1} ✅

    C++20

    支持

    允许乱序

    {.z = 3, .x = 1} ✅

    C++17

    支持

    不允许乱序

    {.x = 1, .z = 3} ❌ 编译报错

    结构体位域是什么?有什么用?

    结构体位域允许指定每个成员占用的二进制位数,目的是在满足需求的前提下,尽可能压缩内存占用

    注意事项:

    1. 不能取地址 — 位域成员没有独立的内存地址,&flags.SYN 编译报错
    2. 位数不能超过类型宽度 — int a : 33 是非法的(int 通常是 32 bit)。
    3. 跨平台布局不确定 — 位域的排列顺序由编译器决定,不保证可移植性。
    4. 实际对齐由编译器优化 — 编译器可能插入填充位,不一定紧密排列。
    // 语法格式
    struct BitFieldDemo {
        类型 成员名 : 位数;
    };
    
    // 不用位域:每个 bool 占 1 字节,总共 4 字节
    struct Flags {
        bool SYN;
        bool ACK;
        bool FIN;
        bool RST;
    };
    std::cout << sizeof(Flags);   // 输出 4
    
    // 使用位域:4 个标志挤在 1 个字节里
    struct FlagsCompact {
        bool SYN : 1;
        bool ACK : 1;
        bool FIN : 1;
        bool RST : 1;
    };
    std::cout << sizeof(FlagsCompact);   // 输出 1
    

    共用体

    共用体(union)是一种特殊的复合数据类型,它和结构体语法相似,但有一个关键区别:结构体的每个成员各自占用独立的内存,而共用体的所有成员共享同一块内存。因此,同一时刻只能存储一个成员的值

    union One4all {
        int i;
        long l;
        double d;
    };
    
    One4all pail;
    pail.i = 15;              // 存储了 int
    pail.d = 1.38;            // int 被覆盖,现在存储的是 double
    
    std::cout << pail.i;      // ⚠️ 未定义行为,i 已经被覆盖了
    std::cout << sizeof(pail); // 输出 8(double 的大小,最大成员的长度)
    

    匿名共用体

    匿名共用体没有名称,其成员直接作为外部作用域的变量使用,共享同一地址

    注意事项:

    1. 同一时刻只有一个成员有效:写入一个成员后,读取其他成员是未定义行为(C++ 标准术语为 active member 规则),C++17 之前技术上属于未定义行为,C++17 后在某些受限场景下有明确规范
    2. C++ 限制比 C 更严格:C++ 的共用体成员不能是非平凡构造函数的类型(如 std::string),除非自定义构造/析构函数来管理。C 语言没有这个限制。
    3. 不能取不同成员的地址进行比较:所有成员地址相同,比较没有意义
    4. 匿名共用体的成员不能和外部作用域同名:否则会产生名称冲突
    struct Prize {
        int id_num;
        char id_char[20];
    };
    
    // 不用匿名共用体:需要一个中间变量
    Prize prize1;
    prize1.id_num = 123;       // 通过变量名访问
    
    // 使用匿名共用体:成员直接暴露
    struct Prize2 {
        union {
            int id_num;
            char id_char[20];
        };
        // 没有 union 名称,id_num 和 id_char 直接作为 Prize2 的成员
    };
    
    Prize2 prize2;
    prize2.id_num = 123;           // ✅ 直接访问
    prize2.id_char;                // ✅ 和 id_num 共享同一块内存
    

    枚举

    C++ 有三种枚举:C 风格枚举(也叫无作用域枚举)、C++11 引入的 enum class(有作用域枚举)、以及指定底层类型的 enum。

    • C 风格枚举:无作用域枚举。会污染外层作用域;并且可以隐式转为 int;底层类型也不确定。
    // 语法
    enum Color {
        RED,
        GREEN,
        BLUE
    };
    Color c = RED;
    
    // 冲突示例
    enum Color { RED, GREEN, BLUE };
    enum TrafficLight { RED, YELLOW, GREEN };   // ❌ 编译报错,RED 和 GREEN 冲突了
    
    • enum class(有作用域枚举):不怕名字冲突。类型安全,不能隐式转换为 int;底层默认类型为 int,但可以更改
    enum class Color {
        RED,
        GREEN,
        BLUE
    };
    
    Color c = Color::RED;         // ✅ 必须加作用域前缀
    Color c2 = RED;               // ❌ 编译报错,RED 不存在于当前作用域
    int x = Color::RED;           // ❌ 不会隐式转换为 int
    int y = static_cast<int>(Color::RED);  // ✅ 显式转换,y = 0
    
    • 给枚举指定底层类型:可以保证跨平台一致。
    // C 风格枚举指定底层类型
    enum Color : uint8_t { RED, GREEN, BLUE };     // 占 1 字节
    
    // enum class 指定底层类型(默认就是 int)
    enum class Size : uint32_t { SMALL, MEDIUM, LARGE };  // 占 4 字节
    

    数组的静态联编与动态联编

    "联编"(binding)在这里的含义是数组大小和内存的绑定时机。静态联编在编译期确定,动态联编在运行期确定。

    使用数组名是静态联编,即在编译期就必须确定数组的长度,长度必须是编译时常量;使用 new[] 创建数组可以在运行期进行长度确定。

    • 静态数组:不能调整大小。
    int arr[5];              // 静态数组,编译期确定大小
    
    const int N = 10;
    int arr[N];              // ✅ const 变量是编译期常量
    
    constexpr int M = 20;
    int arr2[M];             // ✅ constexpr 是 C++11 的编译期常量
    
    int n = 5;
    int arr3[n];             // ❌ n 是运行时变量(VLA,C99 支持但 C++ 标准不支持)
    
    • 动态数组:大小可以在运行期调整
    int n;
    std::cin >> n;           // 运行时输入大小
    int* arr = new int[n];   // ✅ 运行时确定大小,动态联编
    delete[] arr;            // 用完必须释放
    

    特性

    静态数组 int arr[N]

    动态数组 new int[n]

    大小确定时机

    编译期

    运行期

    内存位置

    大小可变

    不可变

    创建时确定,之后不可变

    内存管理

    自动释放(作用域结束)

    必须 delete[]

    性能

    分配/释放极快(栈操作)

    分配/释放较慢(堆分配)

    栈溢出风险

    大数组容易溢出

    不存在栈溢出问题

    数组的替代品:std::array、std::vector

    替代原因:原生数组(int arr[5])存在不安全的隐患:没有边界检查、不支持 size()、不能拷贝、容易退化为指针。C++11 引入了 std::array,C++98 就有 std::vector,它们提供了更安全、更方便的接口。

    • std::array 栈上的安全数组

    特点:

    1、大小在编译期确定,存储在栈上。

    2、和原生数组效率完全一样(零开销抽象)。

    3、提供 size()、at()、迭代器等安全接口。

    #include <array>
    
    std::array<int, 5> arr = {1, 2, 3, 4, 5};
    
    arr[0] = 10;                // 和原生数组一样用下标访问
    arr.size();                 // ✅ 返回 5,原生数组做不到
    arr.at(0);                  // ✅ 带边界检查的访问
    for (auto& e : arr) {}      // ✅ 支持范围 for 循环
    
    • std::vector 堆上的动态内存,但效率稍微低点(因为可能涉及到:系统调用、内存管理器查找、碎片整理等)

    特点:

    1、大小在运行期确定,存储在堆上。

    2、可以动态扩容(push_back)。

    3、自动管理内存(离开作用域自动释放)。

    #include <vector>
    
    std::vector<int> v = {1, 2, 3};    // 初始大小为 3
    v.push_back(4);                      // ✅ 动态扩容,大小变为 4
    v.size();                            // ✅ 返回 4
    v.capacity();                        // ✅ 返回已分配的容量(可能 > size)
    v.pop_back();                        // ✅ 删除最后一个元素
    
    std::vector<int> v2(10, 0);         // 10 个 0
    std::vector<int> v3;                // 空的,大小为 0
    
    • [ ] 和 at( ) 的区别

    特性

    [ ] 运算符

    at( ) 成员函数

    边界检查

    ❌ 不检查

    ✅ 运行时检查

    越界行为

    未定义行为

    抛出 std::out_of_range

    效率

    更高(无额外开销)

    略低(每次都要检查索引)

    适用场景

    性能敏感、确定不越界

    安全优先、索引可能不确定

    • 三者对比

    特性

    int arr[N]

    std::array<int, N>

    std::vector<int>

    大小确定时机

    编译期

    编译期

    运行期

    存储位置

    能否扩容

    size()

    边界检查

    at( ) 可以

    at( )可以

    支持拷贝

    支持迭代器

    ❌(退化为指针)

    内存管理

    自动

    自动

    自动

    效率

    最快

    和原生数组一样

    堆分配,稍慢

    自动变量与静态变量

    自动变量:函数内定义的普通局部变量,存储在栈上,函数结束即销毁。

    静态变量 :生命周期贯穿整个程序运行的变量,存储在静态存储区。

    • 局部静态变量:static 修饰的局部变量,延长生命周期 + 限制作用域。
    • 全局静态变量:static 修饰的全局变量,限制作用域为当前文件。
    • 普通全局变量:函数外定义,无 static,跨文件可见。
    C++ Primer Plus 文章被收录于专栏

    C++ Primer Plus 精读|从入门到面试,重点内容全程带看。 本专栏以《C++ Primer Plus》为蓝本,逐章提炼必考知识点、易错点、面试高频考点,跳过冗余示例,直击语法本质与工程实践,帮你高效吃透 C++ 基础,夯实底层开发必备能力。

    全部评论
    mark收藏
    1 回复 分享
    发布于 04-02 20:51 河北
    求问编译器
    点赞 回复 分享
    发布于 04-01 23:15 河北
    欢迎订阅专栏《C++/嵌入式开发 秋招面经》:https://www.nowcoder.com/creation/manager/columnDetail/MKaoll
    点赞 回复 分享
    发布于 03-30 23:00 河北

    相关推荐

    评论
    6
    2
    分享

    创作者周榜

    更多
    牛客网
    牛客网在线编程
    牛客网题解
    牛客企业服务