# 第十章 自定义类型（类）

> Arietids Learn · 个人课程复习笔记
> 在线阅读：https://learn.arietids.net/courses/programming/chapter-10/

## 关键考点总览

1. 类的定义格式（class vs struct 默认访问权限差异）
2. 访问权限（private/public）
3. 构造函数（默认/重载/复制构造）
4. 析构函数（调用时机与顺序）
5. 初始化列表的使用场景（必须使用的典型情况与初始化顺序）
6. this 指针机制
7. 静态成员（数据成员/成员函数的定义与访问）
8. const 与类（常量对象/常量成员函数/常量数据成员）
9. 友元（友元函数/友元类及其特性）

---

## 详细知识点

### 10.1 面向对象程序设计概述

#### 定义/语法

- **面向过程 vs 面向对象**：
  - 面向过程：数据与数据操作是**分离**的，通过函数调用解决问题
  - 面向对象：数据和数据操作**封装**在一起构成对象，通过对象间交互解决问题
  [来源：第10章 p5，面向过程 vs 面向对象]

- **类与对象的关系**：类是具有相同属性的事物的抽象；对象是类的一个具体实例
  [来源：第10章 p6，类与它的对象]

#### 规则/规范

- **面向对象三大特点**：
  1. **封装**：将数据和基于数据的操作封装在一起，数据被隐藏在类型内部，通过对外接口与外部联系
  2. **继承**：用已存在的类（基类）建立新的类（派生类），派生类继承基类的可访问成员（受访问控制影响），并可扩展新成员/新行为
  3. **多态**：同一接口在不同对象上表现出不同的行为（典型：虚函数实现的运行时多态；也可理解为重载/模板的编译期多态）
  [来源：第10章 p8，面向对象的程序设计的特点]

---

### 10.2 类的定义

#### 定义/语法

- **类定义格式**：
  ```cpp
  class 类名 {
  private:
      私有数据成员和成员函数;
  public:
      公有数据成员和成员函数;
  };  // 注意分号
  ```
  [来源：第10章 p10，类的定义]

- **对象定义方式**：
  ```cpp
  类名 对象名;                    // 普通定义
  类名 *p = new 类名;             // 动态分配
  delete p;                       // 释放动态对象
  ```
  [来源：第10章 p13，对象的定义]

#### 规则/规范

- **成员默认访问权限**：class 默认是 **private**；struct 默认是 **public**
  [来源：第10章 p10，Notes]

- **私有成员**：只能被本类的成员函数（含 static 成员函数）以及友元（友元函数/友元类）访问；同类成员函数可访问任意同类对象的私有成员
  [来源：第10章 p10]

- **公有成员**：是类的对外接口，可以被外部访问
  [来源：第10章 p10]

#### 代码模式

- **对象访问数据成员**：
  ```cpp
  对象名.数据成员名       // 通过对象名访问
  对象指针->数据成员名    // 通过指针访问
  ```
  [来源：第10章 p14，对象的操作]

- **对象调用成员函数**：
  ```cpp
  对象名.成员函数名(实参表)
  对象指针->成员函数名(实参表)
  ```
  [来源：第10章 p14，对象的操作]

- **成员函数在类外定义**：
  ```cpp
  返回类型 类名::函数名(形参表) { 函数体 }
  ```
  [来源：第10章 p12，示例 - DoubleArray]

#### 易错点与辨析

- ⚠️ 私有数据成员不能通过 `对象名.成员名` 直接访问，必须通过成员函数
  [来源：第10章 p14，Notes]

- ⚠️ 同类对象之间默认可以相互赋值（若拷贝赋值运算符可用）；默认赋值是**成员逐个赋值**。若成员包含裸指针/共享资源，赋值后可能仍指向同一资源（浅拷贝风险），需自定义拷贝控制（Rule of Three/Five）或使用 RAII 类型（如 `std::string/std::vector/std::unique_ptr` 等）
  [来源：第10章 p14，Notes]

---

### 10.3 this 指针

#### 定义/语法

- **this 指针**：成员函数有一个隐藏的指向本类型的指针形参 `this`，指向当前调用该成员函数的对象
  [来源：第10章 p17，this指针]

#### 规则/规范

- 所有对象**共享同一个成员函数**定义，但**各有各自的数据成员**
  [来源：第10章 p16，this指针]

- 定义对象时，系统**只为其数据成员分配空间**，不为成员函数分配空间
  [来源：第10章 p16]

- 成员函数里可以显式使用 `this` 指针（如 `this->成员名`），编译器会自动添加
  [来源：第10章 p17]

---

### 10.4 对象的构造与析构

#### 10.4.1 构造函数

##### 定义/语法

- **构造函数定义**：
  ```cpp
  类名::类名(形参表) { 函数体 }
  ```
  [来源：第10章 p21，构造函数的特点]

- **对象定义时调用构造函数**：
  ```cpp
  类名 对象名(实参列表);
  类名 *p = new 类名(实参列表);
  ```
  [来源：第10章 p21]

##### 规则/规范

- 构造函数**名字必须与类名相同**
  [来源：第10章 p21，构造函数的特点]

- 构造函数**不需要返回值**，**没有返回类型**（不加 void）
  [来源：第10章 p21]

- 构造函数**可以重载**，可以定义多个具有不同参数的构造函数
  [来源：第10章 p22，构造函数的特点]

- **默认构造函数**：不带参数的构造函数
  [来源：第10章 p22]

- 如果没有定义任何构造函数，编译器会**隐式声明/定义默认构造函数**（可写作 `类名() = default;`）。其行为是对成员/基类执行默认初始化并调用其默认构造，是否“空函数体/平凡(trivial)”取决于成员/基类类型；且一旦用户声明了任意构造函数，编译器将不再自动生成默认构造函数（需手动提供或 `= default`）
  [来源：第10章 p22，Notes]

##### 代码模式

- **构造函数重载示例**：
  ```cpp
  class Timer {
  private:
      int second;
  public:
      Timer(int t) { second = t; }
      Timer(int min, int sec) { second = 60 * min + sec; }
      Timer(int h, int min, int sec) { second = sec + 60*min + 3600*h; }
  };
  Timer a(20), b(1, 20), c(1, 1, 10);
  ```
  [来源：第10章 p23，重载构造函数示例]

##### 易错点与辨析

- ⚠️ 如果定义对象时没有初始化参数（如 `Timer t;`），必须有默认构造函数，否则编译错误
  [来源：第10章 p23，Notes]

---

#### 10.4.2 初始化列表

##### 定义/语法

- **初始化列表格式**：位于函数头和函数体之间，以冒号开头，逗号分隔
  ```cpp
  类名::类名(形参表): 成员1(值1), 成员2(值2), ... { 函数体 }
  ```
  [来源：第10章 p24，初始化列表方法]

##### 执行逻辑

- **对象构造过程**：
  1. 先初始化（构造）**基类子对象**与**成员子对象**，然后才进入构造函数体
  2. 成员初始化顺序由“**基类声明顺序** + **成员在类中声明顺序**”决定，与初始化列表的书写顺序无关
  3. 未出现在初始化列表中的成员，会使用类内初始值（若有）或执行默认初始化
  [来源：第10章 p24]

##### 规则/规范

- **必须使用初始化列表（或等价的成员初始值）的常见情况**：
  1. **const 数据成员**（必须在初始化阶段给值）
  2. **引用数据成员**（reference 必须初始化，不能后赋值）
  3. **成员对象或基类没有可用的默认构造函数**，或需要显式指定其构造方式（调用特定构造函数）
  [来源：第10章 p25-26，初始化列表方法]

##### 代码模式

- **初始化列表示例**：
  ```cpp
  class A {
  private:
      const int a;
  public:
      A(int b): a(b) { }  // 必须用初始化列表初始化const成员
  };
  ```
  [来源：第10章 p26]

##### 易错点与辨析

- ⚠️ const 数据成员**不能在构造函数体中赋值**，必须在初始化阶段完成初始化：用构造函数初始化列表（传统写法），或 C++11 起使用类内成员初始值（in-class member initializer）
  [来源：第10章 p26，Notes]

---

#### 10.4.3 复制构造函数

##### 定义/语法

- **复制构造函数格式**：
  ```cpp
  类名(const 类名 &other);
  //（不推荐）类名(类名 &other);  // 不能绑定到 const 对象/临时对象
  ```
  [来源：第10章 p27，复制构造函数]

##### 规则/规范

- 如果没有定义，编译器**自动生成默认复制构造函数**，将实参对象的数据成员对应赋值给新对象
  [来源：第10章 p27]

- 若类使用**裸指针**等自行管理动态资源（`new/delete`），默认拷贝/赋值往往会产生浅拷贝、重复释放等问题。应遵循“**三法则/五法则**”：至少自定义（或禁用）拷贝构造、拷贝赋值、析构（必要时再加移动构造/移动赋值），或改用 RAII 成员（如 `std::vector/std::string/std::unique_ptr`）管理资源
  [来源：第10章 p27，Notes]

##### 执行逻辑

- **复制构造函数可能被调用的常见情况**：
  1. **用一个对象初始化另一个对象**：`A a2(a1);` 或 `A a2 = a1;`
  2. **对象作为函数参数（值传递）**：`void f(A arr);`
  3. **对象作为函数返回值**：`A f() { A a; return a; }`
     - 注：现代 C++（尤其 C++17 起）很多返回值场景会发生/强制发生**拷贝省略（copy elision）**，因此可能不调用拷贝/移动构造；若要观察调用需在特定编译器上关闭省略或降低优化等级
  [来源：第10章 p28，调用复制构造函数的情景]

##### 易错点与辨析

- ⚠️ 默认复制构造/默认赋值是**成员逐个拷贝/赋值**。若成员是**裸指针**，就只会复制地址（典型“浅拷贝”），可能导致多个对象指向同一块内存并引发重复释放；但若成员是具有值语义的类型（如 `std::string`、`std::vector`），其自身拷贝通常是安全的
  [来源：第10章 p27，Notes]

- ⚠️ **引用传递不调用复制构造函数**：`void f(A &arr);`
  [来源：第10章 p32，Notes]

---

#### 10.4.4 析构函数

##### 定义/语法

- **析构函数格式**：
  ```cpp
  类名::~类名() { 函数体 }
  ```
  [来源：第10章 p29，析构函数]

##### 规则/规范

- 析构函数名字是 **~类名**
  [来源：第10章 p29]

- 析构函数**没有参数**，**不能重载**
  [来源：第10章 p29]

- 析构函数**没有返回值**
  [来源：第10章 p29]

- 若没有显式定义，编译器会生成**默认析构函数**：它会依次调用成员与基类的析构函数（并非简单“空函数体”）。但如果你用**裸指针**在构造中 `new` 了内存，默认析构不会自动 `delete`，需要你在析构中释放（或改用 RAII 成员）
  [来源：第10章 p29]

- 对象撤销时，系统**自动调用**析构函数
  [来源：第10章 p29]

---

#### 10.4.5 对象生命周期与构造/析构顺序

##### 执行逻辑

- **不同存储类型对象的生命周期**：
  - **全局对象（静态存储期对象）**：在进入 `main` 之前完成初始化，在程序结束时析构；注意不同源文件（不同翻译单元）间的初始化/析构顺序一般**未指定**
  - **静态局部对象**：第一次执行到时构造（只构造一次），程序结束后析构
  - **自动局部对象**：每次进入作用域时构造，离开作用域时析构
  - **动态对象**：`new` 时构造，`delete` 时析构
  [来源：第10章 p30，验证对象的生命周期]

- **析构顺序**：同一作用域内遵循“**后构造先析构**”（LIFO）；对全局/静态对象在同一翻译单元内也是先构造后析构，但不同翻译单元之间的顺序一般未指定
  [来源：第10章 p30，Notes]

##### 例题结论

- **【执行结果分析】** p30 示例程序输出：
  ```
  constructor 0    // 全局对象
  constructor 3    // main局部对象
  f1:
  constructor 1    // f1静态对象（只构造一次）
  constructor 2    // f1局部对象
  destructor 2     // f1局部对象析构
  f1:
  constructor 2    // f1局部对象再次构造
  destructor 2     // f1局部对象析构
  destructor 3     // main局部对象析构
  destructor 1     // f1静态对象析构
  destructor 0     // 全局对象析构
  ```
  [来源：第10章 p30，Notes]

- **【执行结果分析】** p31 习题输出：
  ```
  Construct 7
  Destruct 7
  7
  ```
  要点：`p.check()` 返回的是 int 值，不是对象，所以返回时不调用复制构造
  [来源：第10章 p31，Notes]

- **【执行结果分析】** p32 习题输出：
  ```
  Constructor 0    // 全局
  Constructor 2    // main obj2
  Constructor 3    // main obj3
  Copy constructor 2   // f1形参（值传递）
  Copy constructor 2   // f1内static对象
  Destructor 2     // f1形参析构
  Copy constructor 3   // f2内obj1（引用不复制，但内部用复制构造）
  Destructor 3     // f2内obj1析构
  Destructor 3     // main obj3析构
  Destructor 3     // main obj2析构（值已被改为3）
  Destructor 2     // f1 static对象析构
  Destructor 0     // 全局析构
  ```
  [来源：第10章 p32，Notes]

- **【执行结果分析】** p44 习题输出：
  ```
  Object 1(x:1 y:2) begins:    // a1
  Object 2(x:5 y:10) begins:   // a2（使用默认参数）
  Object 3(x:5 y:10) begins:   // p指向的动态对象（注意：a3(a1)用复制构造但没输出）
  Object 1 ends:    // a1（注意：动态对象未delete所以不析构）
  Object 2 ends:    // a2
  Object 1 ends:    // a3（ID与a1相同因为复制了）
  ```
  要点：复制构造函数未定义时使用默认复制构造，不会调用用户定义的构造函数
  [来源：第10章 p44，Notes]

---

### 10.5 const 与类

#### 10.5.1 常量对象

##### 定义/语法

- **常量对象定义**：
  ```cpp
  const 类名 对象名(参数表);
  ```
  [来源：第10章 p34，常量对象]

##### 规则/规范

- 常量对象（`const 类名 obj;`）表示该对象被 **const 限定**：不能通过该对象修改其状态（非 `mutable` 成员不可修改）。对象成员仍在构造阶段完成初始化；并非“数据成员类型都变成 const”，而是“通过 const 对象只能以只读方式访问成员”
  [来源：第10章 p34]

- **常量对象只能调用常量成员函数**
  [来源：第10章 p34]

---

#### 10.5.2 常量成员函数

##### 定义/语法

- **常量成员函数定义**：声明和定义都必须在函数头后加 `const`
  ```cpp
  返回类型 函数名(参数表) const { 函数体 }
  ```
  [来源：第10章 p34，常量成员函数]

##### 规则/规范

- 常量成员函数**不能修改非 `mutable` 的数据成员**（但可以修改 `mutable` 成员，且可修改静态数据成员）
  [来源：第10章 p34]

- 常量成员函数中**不能对当前对象（`*this`）调用非常量的非静态成员函数**（可调用静态成员函数，也可对非 const 对象调用其非常量成员函数）
  [来源：第10章 p34]

- 在常量成员函数中修改数据成员或调用非常量成员函数，**编译器会报错**
  [来源：第10章 p34]

---

#### 10.5.3 常量数据成员

##### 定义/语法

- **常量数据成员定义**：
  ```cpp
  class A {
      const int SIZE;
  };
  ```
  [来源：第10章 p36，常量数据成员]

##### 规则/规范

- 常量数据成员必须在**初始化阶段**完成初始化：可通过构造函数初始化列表初始化；C++11 起也可使用类内成员初始值（in-class member initializer）
  [来源：第10章 p36]

- **不能**在构造函数体中再赋值；是否能在类定义处给初值取决于语言标准：C++11 起允许类内成员初始值；C++98/03 下非静态 const 成员只能用初始化列表（静态 const 整型/枚举等另论）
  [来源：第10章 p36]

- 不同对象的常量数据成员可以有不同的值（通过不同的构造函数调用初始化）
  [来源：第10章 p36，Notes]

##### 代码模式

```cpp
class A {
private:
    const int SIZE;
public:
    A(int size): SIZE(size) { }
};
A a(100);  // a的SIZE为100
A b(200);  // b的SIZE为200
```
[来源：第10章 p36]

---

### 10.6 静态成员

#### 10.6.1 静态数据成员

##### 定义/语法

- **静态数据成员声明**（类内）：
  ```cpp
  static 类型 成员名;
  ```
  [来源：第10章 p38，静态数据成员]

- **静态数据成员定义**（传统写法：类外提供一次定义；C++17 起可用 `inline static` 在类内定义，视编译环境而定）：
  ```cpp
  类型 类名::成员名 = 初值;
  ```
  [来源：第10章 p38]

##### 规则/规范

- 静态数据成员是**所有对象共享**的数据，属于类而不属于具体某个对象
  [来源：第10章 p38]

- 静态数据成员需要**单独定义一次**，并**只分配一次空间**
  [来源：第10章 p38]

- 为对象分配空间时，**不包括静态数据成员**的空间
  [来源：第10章 p38]

- **静态数据成员的定义不能在函数体内**
  [来源：第10章 p38，Notes]

##### 代码模式

- **访问静态数据成员**：
  ```cpp
  类名::静态成员名
  对象名.静态成员名
  ```
  [来源：第10章 p38]

---

#### 10.6.2 静态常量成员

##### 定义/语法

- **静态常量成员声明与初始化**（传统规则，常见于整型/枚举型）：
  ```cpp
  // 例：整型静态常量成员可在类内给出常量表达式初值
  static const int N = 100;
  ```
  - 补充：若该成员被 ODR-used（如取地址），仍可能需要类外定义；C++11+ 推荐 `static constexpr`，C++17+ 可用 `inline static`（视课程/编译环境而定）
  [来源：第10章 p39，静态的常量成员]

##### 规则/规范

- 静态常量成员是**整个类所有对象的共享常量**
  [来源：第10章 p39]

- **非 const 的静态成员**按传统规则不能在类定义内赋值（需类外定义）；C++17 起可用 `inline static` 在类内定义并初始化（视编译环境支持）
  [来源：第10章 p39]

##### 易错点与辨析

- ⚠️ **常量数据成员 vs 静态常量数据成员**：
  - 常量数据成员：属于每个对象，不同对象可以有不同的值
  - 静态常量数据成员：属于类，所有对象共享同一个值
  [来源：第10章 p39]

---

#### 10.6.3 静态成员函数

##### 定义/语法

- **类内声明**：
  ```cpp
  static 返回类型 函数名(参数表);
  ```
  [来源：第10章 p40，静态成员函数]

- **类外定义**：**不能**加 static
  ```cpp
  返回类型 类名::函数名(参数表) { 函数体 }
  ```
  [来源：第10章 p40]

##### 规则/规范

- 静态成员函数没有 `this`，因此**不能直接访问非静态成员**（需要显式传入对象/指针/引用才能访问）；但可以访问静态成员以及其他静态成员函数
  [来源：第10章 p40]

- 静态成员函数**没有 this 指针**
  【整理自 PPT】（由静态成员函数不能访问非静态成员推得）

##### 代码模式

- **调用静态成员函数**：
  ```cpp
  类名::静态成员函数()
  对象名.静态成员函数()
  对象指针->静态成员函数()
  ```
  [来源：第10章 p40]

##### 例题结论

- **【执行结果分析】** p43 示例程序输出：
  ```
  总对象数：0	存活对象数：0
  总对象数：2	存活对象数：2
  总对象数：4	存活对象数：4
  总对象数：4	存活对象数：3
  总对象数：4	存活对象数：2
  ```
  [来源：第10章 p43，Notes]

---

### 10.7 友元

#### 10.7.1 友元函数

##### 定义/语法

- **友元函数声明**（类内）：
  ```cpp
  friend 返回类型 函数名(参数表);
  ```
  [来源：第10章 p47，友元]

- 友元函数可以定义在类内或类外；**不是成员函数**，调用时不需要 `类名::`
  [来源：第10章 p48，Notes]

##### 规则/规范

- 友元函数可以**访问类的私有成员**
  [来源：第10章 p47]

---

#### 10.7.2 友元类

##### 定义/语法

- **友元类声明**：
  ```cpp
  friend class 类名;
  ```
  [来源：第10章 p47]

##### 规则/规范

- 如果 B 是 A 的友元类，则 B 的所有成员函数都可以访问 A 的私有成员
  [来源：第10章 p47]

---

#### 10.7.3 友元的特性

##### 规则/规范

- **友元关系是授予的而不是索取的**：类 A 必须显式声明函数 f 是它的友元，f 不能自称是 A 的友元
  [来源：第10章 p49，友元特点]

- **友元关系不是对称关系**：A 声明 B 是友元，不意味着 A 也是 B 的友元
  [来源：第10章 p49]

- **友元关系不是传递关系**：A 是 B 的友元，B 是 C 的友元，不意味着 A 是 C 的友元
  [来源：第10章 p49]

##### 代码模式

- **将类 B 的成员函数定义为类 A 的友元**（特殊语法）：
  ```cpp
  class A;                    // 1. 先前向声明 A
  class B {                   // 2. 定义 B
      void f(const A &x);     //    此处先声明；若要访问 A 的成员，定义需在 A 完整定义之后
  };
  class A {                   // 3. 定义 A
      friend void B::f(const A &x);
      // ...
  };
  void B::f(const A &x) { }   // 4. 最后在类外定义 B 的友元函数
  ```
  [来源：第10章 p49，Notes；p53，习题]

---

### 10.8 习题答案汇总

#### 填空题答案

- **p45 习题答案**：
  ```cpp
  CConst(int d = 0): len(d) { size += d; }
  void print() const
  static void show()
  int CConst::size = 2;
  ```
  [来源：第10章 p45，Notes]

- **p50 习题答案**：
  ```cpp
  friend float Dis(Point &p, Point &q)
  Point(float a, float b): x(a), y(b)
  result = sqrt((q.x-p.x)*(q.x-p.x) + (q.y-p.y)*(q.y-p.y))
  ```
  [来源：第10章 p50，Notes]

- **p52 习题答案**：
  ```cpp
  A(int a, int b): y(b) { x = a; }
  friend void disp(const A &a);
  void print() const { cout << x << " " << y << endl; }
  ```
  [来源：第10章 p52，Notes]

- **p53 习题答案**：
  ```cpp
  class SeaAnemone;
  void disp(const SeaAnemone &x);
  friend class Clownfish;
  SeaAnemone sa1("Tony", 4);
  SeaAnemone sa2("Tim", 2);
  Clownfish cl("Nemo", 4);
  ```
  [来源：第10章 p53，Notes]

---

## 总结（来源：p54）

1. 面向对象程序设计的基本思想是如何根据应用的需求**创造合适的类型**，用该类型的对象来解决特定的问题
2. 本章主要介绍了：
   - 如何**定义一个类**，了解**封装**的概念
   - 如何**定义类的对象**
   - 如何实现**对象的初始化**
   - 如何**操作对象**
