Faker :: Lost
REALITY IS FAKE
0%

C++ Primer Plus 学习笔记 - 第4章 复合类型

预计阅读 17 分钟

本章概览

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;
}

注意:数组大小必须是整型常量,不能是变量。动态大小需用 newstd::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 四条铁律

  1. 不要 deletenew 分配的内存
  2. 不要 delete 同一块内存两次
  3. new [] 必须配对 delete []
  4. 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 用 vectorarray 代替原生数组

#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::stringstd::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;
}

解决方案

  • 养成 newdelete 成对书写的习惯
  • 使用 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/deletemalloc/free 的区别?

特性 new/delete malloc/free
语言 C++ 运算符 C 库函数
类型安全 ✅ 自动推断类型 ❌ 返回 void*,需强转
构造/析构 ✅ 调用构造/析构函数 ❌ 不调用
异常处理 失败抛 std::bad_alloc 失败返回 NULL

Q3: 什么是 RAII?为什么重要?

:RAII (Resource Acquisition Is Initialization) 是 C++ 的核心资源管理思想:在构造函数中获取资源,在析构函数中释放资源。这样即使发生异常,析构函数也会被自动调用,避免资源泄漏。智能指针 unique_ptrshared_ptr 就是 RAII 的典型应用。


五、总结与回顾 (Summary & Review)

核心记忆点

  1. 数组:固定大小,下标从 0 开始,越界访问是未定义行为
  2. string vs char[]:优先用 std::string,自动管理内存,不会溢出
  3. 结构体:打包不同类型数据,支持直接赋值(数组不行)
  4. 指针:存储内存地址,用 & 取地址,用 * 解引用
  5. new/delete:堆内存管理,必须配对使用,new[]delete[]
  6. 指针算术+1 是加一个元素的大小(sizeof(type) 字节),不是 1 字节
  7. 现代替代vector > array > 原生数组;string > char[]nullptr > NULL

下一章预告:第 5 章将介绍循环和关系表达式 (for, while, do while),让我们的数据动起来!

💬 评论区

感谢阅读!如有任何问题或建议,欢迎交流。