第四章:复合类型 | 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 的操作数。
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} ❌ 编译报错 |
结构体位域是什么?有什么用?
结构体位域允许指定每个成员占用的二进制位数,目的是在满足需求的前提下,尽可能压缩内存占用。
注意事项:
- 不能取地址 — 位域成员没有独立的内存地址,&flags.SYN 编译报错。
- 位数不能超过类型宽度 — int a : 33 是非法的(int 通常是 32 bit)。
- 跨平台布局不确定 — 位域的排列顺序由编译器决定,不保证可移植性。
- 实际对齐由编译器优化 — 编译器可能插入填充位,不一定紧密排列。
// 语法格式
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 的大小,最大成员的长度)
匿名共用体
匿名共用体没有名称,其成员直接作为外部作用域的变量使用,共享同一地址
注意事项:
- 同一时刻只有一个成员有效:写入一个成员后,读取其他成员是未定义行为(C++ 标准术语为 active member 规则),C++17 之前技术上属于未定义行为,C++17 后在某些受限场景下有明确规范
- C++ 限制比 C 更严格:C++ 的共用体成员不能是非平凡构造函数的类型(如 std::string),除非自定义构造/析构函数来管理。C 语言没有这个限制。
- 不能取不同成员的地址进行比较:所有成员地址相同,比较没有意义
- 匿名共用体的成员不能和外部作用域同名:否则会产生名称冲突
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++ 基础,夯实底层开发必备能力。
