C++ Primer Plus 学习笔记 - 第4章 复合类型
本章概览
Why This Matters: 基本类型只能存储单个值。但在实际开发中,我们需要处理一组成绩、一段文本、一个用户信息……这些都需要复合类型。指针和动态内存更是 C++ 区别于 Java/Python 的核心能力,也是面试必考重灾区。
前置知识: 学习本章前,建议先掌握第 3 章的基本数据类型(整型、浮点型、const)。
核心目标:
- 掌握数组、字符串、结构体等数据容器的使用
- 理解指针的本质——内存地址的直接操控
- 学会使用
new/delete管理动态内存 - 了解现代 C++ 中更安全的替代方案
复合类型知识体系:
├── 数据容器
│ ├── 数组 (int arr[10])
│ ├── 字符串 (char[] / std::string)
│ ├── 结构体 (struct)
│ ├── 共用体 (union)
│ └── 枚举 (enum)
│
├── 内存操控
│ ├── 指针 (int* p)
│ ├── 动态内存 (new / delete)
│ ├── 动态数组 (new int[10])
│ └── 动态结构体 (new MyStruct)
│
└── 存储管理
├── 自动存储(栈)
├── 静态存储
└── 动态存储(堆)
一、基础知识讲解 (Core Concepts)
1.1 数组 (Arrays)
数组是一种存储多个同类型值的数据格式。
声明与初始化
// 完整示例 - 可直接编译运行
#include <iostream>
int main() {
using namespace std;
// 声明并初始化
int yams[3] = {7, 8, 6};
// 访问元素(下标从 0 开始)
cout << "Total = " << yams[0] + yams[1] + yams[2] << endl;
// ⚠️ 越界访问是未定义行为!
// yams[3] = 10; // DANGER!
return 0;
}
注意:数组大小必须是整型常量,不能是变量。动态大小需用
new或std::vector。
初始化规则速查
int cards[4] = {3, 6, 8, 10}; // 完整初始化
long totals[500] = {0}; // 全部初始化为 0(极常用)
short things[] = {1, 5, 3, 8}; // 自动推断 size=4
// C++11 列表初始化
double earnings[4] {1.2e4, 1.6e4, 1.1e4, 1.7e4}; // 省略等号
unsigned int counts[10] {}; // 空大括号 → 全部为 0
1.2 C 风格字符串
C 风格字符串本质是以空字符 (\0) 结尾的 char 数组。
char dog[8] = {'b', 'e', 'a', 'u', 'x', ' ', 'I', 'I'}; // 不是字符串!缺少 \0
char cat[8] = {'f', 'a', 't', 'e', 's', 's', 'a', '\0'}; // 是字符串
// 更简洁的写法(自动添加 \0)
char bird[11] = "Mr. Cheeps";
char fish[] = "Bubbles"; // 自动计算大小 = 8(含 \0)
输入问题与解决方案
cin >> 遇到空格就停止,无法读取完整句子。
| 方法 | 语法 | 特点 |
|---|---|---|
getline() |
cin.getline(arr, size) |
读取并丢弃换行符 |
get() |
cin.get(arr, size) |
读取并保留换行符在缓冲区 |
const int Size = 20;
char name[Size];
cin.getline(name, Size); // 读取整行,包括空格
1.3 string 类 (std::string)
std::string 让字符串操作像简单变量一样容易。需包含 <string>。
#include <string>
using namespace std;
string str1; // 空字符串
string str2 = "panther";
string str3 = str1 + str2; // 拼接
str1 += " paste"; // 追加
int len = str1.size(); // 获取长度
// 输入整行
string line;
getline(cin, line); // 注意:不是 cin.getline
1.4 混合输入字符串和数字
数字输入后残留的换行符会干扰后续的字符串读取。
int year;
cin >> year; // 换行符残留在缓冲区!
cin.get(); // ✅ 手动丢弃换行符
// 或合并写法:
(cin >> year).get();
char address[80];
cin.getline(address, 80); // 现在可以正常读取了
1.5 结构体 (Structures)
结构体可以存储多个不同类型的数据。
struct Inflatable {
char name[20];
float volume;
double price;
};
int main() {
Inflatable guest = {"Glorious Gloria", 1.88, 29.99};
// 用 . 访问成员
cout << guest.name << ": $" << guest.price << endl;
// 结构体允许直接赋值(数组做不到)
Inflatable choice;
choice = guest; // ✅ 成员逐个复制
return 0;
}
1.6 共用体 (Unions)
所有成员共享同一块内存,同一时间只能存储其中一种类型。
union One4All {
int int_val;
long long_val;
double double_val;
};
One4All pail;
pail.int_val = 15; // 存 int
pail.double_val = 1.38; // 存 double,此时 int_val 的值丢失
用途:节省内存(嵌入式系统)或解析二进制数据格式。
1.7 枚举 (Enumerations)
enum 创建一组命名的整数常量。
enum Spectrum { Red, Orange, Yellow, Green, Blue, Violet };
// Red=0, Orange=1, Yellow=2 ...
Spectrum band = Blue;
int color = Blue; // ✅ 枚举可隐式转为 int
// band = 2000; // ❌ 不能将 int 赋给枚举
// 自定义值
enum Bits { One = 1, Two = 2, Four = 4, Eight = 8 };
1.8 指针 (Pointers)
指针是一个变量,存储的值是内存地址。
int updates = 6;
int *p_updates = &updates; // & 取地址,* 解引用
cout << *p_updates; // 6(通过指针读取值)
*p_updates = 10; // 通过指针修改原变量
cout << updates; // 10
铁律:使用
*之前,指针必须指向一个确定的、合法的地址。未初始化的指针是定时炸弹。
1.9 动态内存 (new / delete)
new 在运行时从堆 (Heap) 分配内存,delete 释放。
// 分配单个变量
int *pn = new int;
*pn = 1001;
delete pn;
// 分配动态数组
int *arr = new int[10];
arr[0] = 100;
delete [] arr; // 注意:数组用 delete[]
new/delete 四条铁律:
- 不要
delete非new分配的内存 - 不要
delete同一块内存两次 new []必须配对delete []- 对
nullptr使用delete是安全的
1.10 动态结构体
Inflatable *ps = new Inflatable;
// 指针访问成员用箭头 ->
cin.get(ps->name, 20);
ps->price = 29.99;
delete ps;
口诀:变量用点 (
.),指针用箭头 (->)。
1.11 指针算术
数组名就是第一个元素的地址。pointer + 1 增加 sizeof(type) 个字节。
double wages[3] = {10000.0, 20000.0, 30000.0};
double *pw = wages;
// 以下四种写法等价:
wages[1] // 数组下标
*(wages + 1) // 数组名 + 偏移
pw[1] // 指针下标
*(pw + 1) // 指针 + 偏移
1.12 存储类型
| 类型 | 存储位置 | 生命周期 | 特点 |
|---|---|---|---|
| 自动存储 | 栈 (Stack) | 函数结束自动销毁 | 最常用,LIFO |
| 静态存储 | 数据段 | 整个程序生命周期 | static 或全局变量 |
| 动态存储 | 堆 (Heap) | 手动 delete |
最灵活也最危险 |
二、进阶应用 (Modern C++ Practice)
2.1 用 nullptr 代替 NULL
// 传统写法 (C++98) - 不推荐
int *p = NULL; // NULL 本质是 0,可能导致重载歧义
// 现代写法 (C++11) - 推荐
int *p = nullptr; // 类型安全的空指针
2.2 用 std::string 代替 C 风格字符串
// 传统 (危险) // 现代 (安全)
char s[20]; string s;
strcpy(s, "hello"); s = "hello"; // 自动管理内存
strcat(s, " world"); s += " world"; // 不会溢出
int len = strlen(s); int len = s.size(); // 直觉接口
2.3 用 vector 和 array 代替原生数组
#include <vector>
#include <array>
// vector: 动态大小,存储在堆上
vector<int> vi;
vi.push_back(10); // 自动扩容
vi.push_back(20);
cout << vi.at(0); // at() 会做边界检查,比 [] 安全
// array (C++11): 固定大小,存储在栈上,比原生数组更安全
array<int, 5> ai = {1, 2, 3, 4, 5};
cout << ai.at(0); // 同样支持边界检查
| 类型 | 大小 | 存储 | 边界检查 | 推荐度 |
|---|---|---|---|---|
int a[10] |
固定 | 栈 | ❌ 无 | ⭐ |
array<int,10> |
固定 | 栈 | ✅ at() |
⭐⭐⭐ |
vector<int> |
动态 | 堆 | ✅ at() |
⭐⭐⭐⭐ |
2.4 用 enum class 代替传统 enum (C++11)
// 传统 enum - 不安全(会隐式转为 int,名字可能冲突)
enum Color { Red, Green, Blue };
int x = Red; // ✅ 隐式转换,可能不是你想要的
// 现代 enum class - 推荐(强类型,不会隐式转换)
enum class Color { Red, Green, Blue };
// int x = Color::Red; // ❌ 编译错误,更安全
int x = static_cast<int>(Color::Red); // ✅ 必须显式转换
Color c = Color::Red; // ✅ 正确用法
三、工程陷阱与避坑指南 (Engineering Pitfalls)
🚨 本章涉及的知识点是实际工程中 Bug 的重灾区。
3.1 缓冲区溢出 (Buffer Overflow)
现象:程序崩溃、数据错乱、甚至被黑客利用执行恶意代码。 原因:C 风格字符串和原生数组不做边界检查,写入超出长度的数据会覆盖相邻内存。
char name[5];
strcpy(name, "Alexander"); // ❌ 11字节写入5字节空间,内存被破坏!
解决方案:
- 优先使用
std::string和std::vector - 若必须用 C 字符串,使用
strncpy(dest, src, size)限制长度
3.2 内存泄漏 (Memory Leak)
现象:程序运行越来越慢,最终耗尽系统内存。
原因:new 分配的内存忘记 delete。
void process() {
int *data = new int[1000];
// ... 处理数据 ...
if (error) return; // ❌ 提前返回,data 永远不会被释放!
delete [] data;
}
解决方案:
- 养成
new和delete成对书写的习惯 - 使用 RAII 原则(智能指针
std::unique_ptr,后续章节详解)
3.3 野指针与悬空指针 (Dangling Pointer)
现象:随机崩溃、数据被莫名篡改(最难排查的 Bug 类型)。
原因:delete 后指针未置空,或指针指向已销毁的栈变量。
int *p = new int(42);
delete p;
// p 仍然指向已释放的地址!
*p = 100; // ❌ 未定义行为:可能崩溃、可能静默破坏数据
// ✅ 正确做法
delete p;
p = nullptr; // 立即置空
3.4 结构体对齐 (Structure Padding)
现象:sizeof(struct) 大于成员大小之和,跨平台数据传输出错。
原因:编译器为 CPU 访问效率自动插入填充字节。
struct Bad {
char a; // 1 字节
int b; // 4 字节
char c; // 1 字节
};
// sizeof(Bad) = 12,而不是 6!(编译器插入了填充)
struct Good {
int b; // 4 字节
char a; // 1 字节
char c; // 1 字节
};
// sizeof(Good) = 8,合理排列成员可以减少浪费
解决方案:网络/文件传输时使用 #pragma pack(1) 或手动序列化。
四、面试高频考点 (Interview Focus)
Q1: 数组名和指针有什么区别?
答:
- 数组名是常量,不能修改指向:
arr = arr + 1;❌ - 指针是变量,可以修改指向:
ptr = ptr + 1;✅ sizeof(arr)返回整个数组大小;sizeof(ptr)返回指针自身大小(4/8 字节)
Q2: new/delete 和 malloc/free 的区别?
答:
| 特性 | new/delete |
malloc/free |
|---|---|---|
| 语言 | C++ 运算符 | C 库函数 |
| 类型安全 | ✅ 自动推断类型 | ❌ 返回 void*,需强转 |
| 构造/析构 | ✅ 调用构造/析构函数 | ❌ 不调用 |
| 异常处理 | 失败抛 std::bad_alloc |
失败返回 NULL |
Q3: 什么是 RAII?为什么重要?
答:RAII (Resource Acquisition Is Initialization) 是 C++ 的核心资源管理思想:在构造函数中获取资源,在析构函数中释放资源。这样即使发生异常,析构函数也会被自动调用,避免资源泄漏。智能指针 unique_ptr、shared_ptr 就是 RAII 的典型应用。
五、总结与回顾 (Summary & Review)
核心记忆点
- 数组:固定大小,下标从 0 开始,越界访问是未定义行为
- string vs char[]:优先用
std::string,自动管理内存,不会溢出 - 结构体:打包不同类型数据,支持直接赋值(数组不行)
- 指针:存储内存地址,用
&取地址,用*解引用 - new/delete:堆内存管理,必须配对使用,
new[]配delete[] - 指针算术:
+1是加一个元素的大小(sizeof(type)字节),不是 1 字节 - 现代替代:
vector>array> 原生数组;string>char[];nullptr>NULL
下一章预告:第 5 章将介绍循环和关系表达式 (
for,while,do while),让我们的数据动起来!