本阶段主要针对C++面向对象编程技术做详细讲解,探讨C++中的核心和精髓。

目录

1 内存分区模型

1.1 程序运行前

1.2 程序运行后

1.3 new操作符

2 引用

2.1 引用的基本使用

2.2 引用注意事项

​2.3 引用做函数参数

2.4 引用做函数返回值

2.5 引用的本质

2.6 常量引用

3 函数提高

3.1 函数默认参数

3.2 函数占位参数

3.3 函数重载

3.3.1 函数重载概述

3.3.2 函数重载注意事项

4 类和对象

4.1 封装

4.1.1 封装的意义

4.1.2 struct和class区别

4.1.3 成员属性设置为私有

4.2 对象的初始化和清理

4.2.1 构造函数和析构函数

4.2.2 构造函数的分类及调用

4.2.3 拷贝构造函数调用时机

4.2.4 构造函数调用规则

4.2.5 深拷贝与浅拷贝

​4.2.6 初始化列表

4.2.7 类对象作为类成员

4.2.8 静态成员

4.3 C++对象模型和this指针

4.3.1 成员变量和成员函数分开存储

4.3.2 this指针概念

4.3.3 空指针访问成员函数

4.3.4 const修饰成员函数

4.4 友元

4.4.1 全局函数做友元

4.4.2 类做友元

4.4.3 成员函数做友元

4.5 运算符重载

4.5.1 加号运算符重载

4.5.2 左移运算符重载

4.5.3 递增运算符重载

4.5.4 赋值运算符重载

4.5.5 关系运算符重载

4.5.6 函数调用运算符重载

4.6 继承

4.6.1 继承的基本语法

4.6.2 继承方式

4.6.3 继承中的对象模型

4.6.4 继承中构造和析构顺序

4.6.5 继承同名成员处理方式

4.6.6 继承同名静态成员处理方式

4.6.7 多继承语法

4.6.8 菱形继承

4.6.9. 继承的本质

4.7 多态

4.7.1 多态的基本概念

4.7.2 多态案例一-计算器类

4.7.3 纯虚函数和抽象类

4.7.4 多态案例二-制作饮品

4.7.5 虚析构和纯虚析构

4.7.6 多态案例三-电脑组装

5 文件操作

5.1文本文件

5.1.1写文件

5.1.2读文件

5.2 二进制文件

5.2.1 写文件

5.2.2 读文件


1 内存分区模型

C++程序在执行时,将内存大方向划分为**4个区域**

* 代码区:存放函数体的二进制代码,由操作系统进行管理的
* 全局区:存放全局变量和静态变量以及常量
* 栈区:由编译器自动分配释放, 存放函数的参数值,局部变量等
* 堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收

**内存四区意义:**

不同区域存放的数据,赋予不同的生命周期, 给我们更大的灵活编程方式。

1.1 程序运行前

​ 在程序编译后,生成了exe可执行程序,未执行该程序前分为两个区域

​ 代码区:

​ 存放 CPU 执行的机器指令

​ 1.代码区是**共享**的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可

​ 2.代码区是**只读**的,使其只读的原因是防止程序意外地修改了它的指令

全局区

​ 全局变量和静态变量存放在此.

​ 全局区还包含了常量区, 字符串常量和其他常量也存放在此.

​ 该区域的数据在程序结束后由操作系统释放.

**示例:

#include <iostream>
using namespace std;

//全局区
// 
//全局变量
int g_a = 10;
int g_b = 10;
//全局常量
const int c_g_a = 10;
const int c_g_b = 10;
int main() {

	//创建普通局部变量
	int a = 10;
	int b = 10;
	cout << "局部变量a的地址为:" << (int)&a << endl;
	cout << "局部变量b的地址为:" << (int)&b << endl;

	//全局变量
	cout << "全局变量a的地址为:" << (int)&g_a << endl;
	cout << "全局变量b的地址为:" << (int)&g_b << endl;

	//静态变量
	static int s_a = 10;
	static int s_b = 10;
	cout << "静态变量s_a地址为: " << (int)&s_a << endl;
	cout << "静态变量s_b地址为: " << (int)&s_b << endl;

	//常量
	//字符串常量
	cout << "字符串常量地址为: " << (int)&"hello world" << endl;
	cout << "字符串常量地址为: " << (int)&"hello world1" << endl;
	//全局常量
	cout << "全局常量c_g_a地址为: " << (int)&c_g_a << endl;
	cout << "全局常量c_g_b地址为: " << (int)&c_g_b << endl;
	//局部常量
	const int c_l_a = 10;
	const int c_l_b = 10;
	cout << "局部常量c_l_a地址为: " << (int)&c_l_a << endl;
	cout << "局部常量c_l_b地址为: " << (int)&c_l_b << endl;

	system("pause");
	return 0;
}


#include <iostream>
using namespace std;

// ★★★★★ 全局区(Global/Static Area)★★★★★

// 1️⃣ 全局变量(Global Variables)
int g_a = 10;      // ✅ 全局区(.data 段)
int g_b = 10;      // ✅ 全局区(.data 段)

// 2️⃣ 全局常量(Global Constants)
const int c_g_a = 10;  // ✅ 常量区(Constants)
const int c_g_b = 10;  // ✅ 常量区(Constants)

int main() {
    // ★★★★★ 栈区(Stack)★★★★★
    
    // 3️⃣ 局部变量(Local Variables)
    int a = 10;        // ✅ 栈区
    int b = 10;        // ✅ 栈区
    
    // 4️⃣ 局部常量(Local Constants)
    const int c_l_a = 10;  // ✅ 栈区(注意!不是常量区!)
    const int c_l_b = 10;  // ✅ 栈区
    
    // ★★★★★ 全局/静态区(Static Area)★★★★★
    
    // 5️⃣ 静态变量(Static Variables)
    static int s_a = 10;  // ✅ 全局区(.data 段)
    static int s_b = 10;  // ✅ 全局区(.data 段)
    
    // ★★★★★ 常量区(Constants)★★★★★
    
    // 6️⃣ 字符串常量(String Literals)
    cout << "hello world";   // ✅ 常量区
    cout << "hello world1";  // ✅ 常量区
    
    // ... 输出地址
}


局部变量a的地址为:  15182748    ← 栈区(高地址)
局部变量b的地址为:  15182744    ← 栈区(相差4字节,相邻)

全局变量a的地址为:  4221232     ← 全局区(低地址)
全局变量b的地址为:  4221236     ← 全局区(相差4字节,相邻)

静态变量s_a地址为:  4221240     ← 全局区(和全局变量相邻)
静态变量s_b地址为:  4221244     ← 全局区(和全局变量相邻)

字符串常量地址为:  4349556     ← 常量区
字符串常量地址为:  4349568     ← 常量区(和全局常量相邻)

全局常量c_g_a地址为:4349580     ← 常量区
全局常量c_g_b地址为:4349584     ← 常量区

局部常量c_l_a地址为:15182736    ← 栈区(和局部变量相邻)
局部常量c_l_b地址为:15182732    ← 栈区

打印结果:

* C++中在程序运行前分为全局区和代码区
* 代码区特点是共享和只读
* 全局区中存放全局变量、静态变量、常量
* 常量区中存放 const修饰的全局常量 和 字符串常量

记忆口诀

全局静态放一起,程序开始到结束
局部变量在栈上,函数结束就回收
常量字符串只读区,全局常量也在那
局部常量在栈上,记住这个别搞错
栈上地址比较大,全局区地址比较小

重要结论总结

变量类型 存储区域 生命周期 地址特征
全局变量 全局区 程序启动→结束 地址小,固定
静态变量 全局区 程序启动→结束 地址小,固定
局部变量 栈区 函数开始→结束 地址大,每次运行可能不同
局部常量 栈区 函数开始→结束 地址大,和局部变量相邻
全局常量 常量区 程序启动→结束 地址中,只读
字符串常量 常量区 程序启动→结束 地址中,只读

1.2 程序运行后

​ 栈区:

​ 由编译器自动分配释放, 存放函数的参数值,局部变量等

​ 注意事项:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放

**示例:

#include<iostream>
using namespace std;

//栈区数据注意事项--不要返回局部变量的地址
//栈区的数据由编译器管理开辟和释放

int* func(int b) //形参数据也会放在栈区
{
	b = 100;

	int a = 10;//局部变量 存放在栈区,栈区的数据在函数执行完后自动释放
	return &a;
}

int main() {
	//接收func函数的返回值

	int * p = func(1);
	cout << *p << endl;//第一次可以打印正确的数字,是因为编译器做了保留
	cout << *p << endl;//第二次这个数据就不再保留了



	system("pause");
	return 0;
}

正确做法

方法1:使用静态变量

int* func() {
    static int a = 10;  // 静态变量在全局区,程序结束才释放
    return &a;          // ✅ 安全!
}

int main() {
    int* p = func();
    cout << *p << endl;  // 输出 10
    cout << *p << endl;  // 输出 10(安全)
    return 0;
}

方法2:使用动态内存(堆)

int* func() {
    int* a = new int(10);  // 在堆上分配
    return a;              // ✅ 安全!但需要手动释放
}

int main() {
    int* p = func();
    cout << *p << endl;  // 输出 10
    cout << *p << endl;  // 输出 10
    delete p;            // ⚠️ 必须手动释放!
    return 0;
}

方法3:返回值而不是地址

int func() {
    int a = 10;
    return a;  // ✅ 返回值(复制),不是返回地址
}

int main() {
    int value = func();
    cout << value << endl;  // 输出 10
    cout << value << endl;  // 输出 10
    return 0;
}

方法4:使用引用参数

void func(int& result) {
    result = 10;  // 通过引用修改外部变量
}

int main() {
    int a = 0;
    func(a);
    cout << a << endl;  // 输出 10
    return 0;
}

堆区:

​ 由程序员分配释放,若程序员不释放,程序结束时由操作系统回收

​ 在C++中主要利用new在堆区开辟内存

**示例:

int* func() {
    int* a = new int(10);  // 在堆上分配
    return a;              // ✅ 安全!但需要手动释放
}
 
int main() {
    int* p = func();
    cout << *p << endl;  // 输出 10
    cout << *p << endl;  // 输出 10
    delete p;            // ⚠️ 必须手动释放!
    return 0;
}

1.3 new操作符

​ C++中利用new操作符在堆区开辟数据

​ 堆区开辟的数据,由程序员手动开辟,手动释放,释放利用操作符 delete

​ 语法:new 数据类型

利用new创建的数据,会返回该数据对应的类型的指针

**示例1: 基本语法

int* func() {
    int* a = new int(10);  // ① 在堆区分配内存
    return a;              // ② 返回堆内存地址
}

int main() {
    int *p = func();       // ③ p 指向堆内存
    cout << *p << endl;    // ④ 输出 10
    cout << *p << endl;    // ⑤ 再次输出 10
    delete p;              // ⑥ 释放堆内存
    // cout << *p << endl; // ⑦ 错误!内存已释放
    return 0;
}

核心要点:

  1. new 在堆区分配内存,返回地址

  2. 堆内存不会自动释放,必须手动 delete

  3. 函数返回堆内存地址是安全的(内存还在)

  4. delete 后指针变成野指针,不能再使用

  5. 堆内存的生命周期由程序员控制,可以跨函数

#include <iostream>
using namespace std;

int main() {
    // 1. 基本类型
    int* pInt = new int(10);        // new int → 返回 int*
    double* pDouble = new double(3.14); // new double → 返回 double*
    char* pChar = new char('A');    // new char → 返回 char*
    
    // 2. 数组
    int* pArray = new int[5];       // new int[5] → 返回 int*(首元素地址)
    
    // 3. 结构体/类
    struct Student { string name; int age; };
    Student* pStu = new Student{"张三", 20}; // new Student → 返回 Student*
    
    // 4. 使用指针访问
    cout << *pInt << endl;          // 10
    cout << pArray[0] << endl;      // 数组方式访问
    cout << pStu->name << endl;     // 结构体成员访问
    
    // 释放内存
    delete pInt;
    delete pDouble;
    delete pChar;
    delete[] pArray;
    delete pStu;
    
    return 0;
}

**示例2:开辟数组

int main() {
    // 1. 开辟堆区数组
    int* arr = new int[10];

    // 2. 使用数组
    for (int i = 0; i < 10; i++) {
        arr[i] = i + 100;
    }

    // 3. 输出数组
    for (int i = 0; i < 10; i++) {
        cout << arr[i] << endl;
    }

    // 4. ★ 释放堆区数组
    delete[] arr;
    arr = nullptr;  // ✅ 强烈建议置空,防止野指针

    system("pause");
    return 0;
}

完整代码:

#include<iostream>      // 引入输入输出流头文件
using namespace std;     // 使用标准命名空间

// ========== 1. new 的基本语法(单个变量) ==========
// 函数功能:在堆区创建并返回一个 int 类型的数据
int* func() {
    // 在堆区创建整型数据
    // new返回的是该数据类型的指针(int*)
    //指针本质也是局部变量,存放在栈上,指针保存的数据存放在堆区中
    int* p = new int(10);  // 在堆区分配 4 字节,存储值 10,返回地址
    return p;              // 返回堆内存地址给调用者
}

// 测试函数:演示单个变量的 new 和 delete
void test01()
{
    int* p = func();       // p 接收堆内存地址(假设是 0x1000)

    // 多次访问堆内存中的数据(多次解引用)
    cout << *p << endl;    // 第一次:输出 10
    cout << *p << endl;    // 第二次:输出 10(内存还在)
    cout << *p << endl;    // 第三次:输出 10
    cout << *p << endl;    // 第四次:输出 10

    // 堆区的数据由程序员管理开辟,程序员管理释放
    // 如果想释放堆区的数据,利用关键字 delete
    delete p;              // 释放 0x1000 地址的内存

    // cout << *p << endl; // ❌ 内存已被释放,再次访问是非法操作,会报错
}

// ========== 2. 在堆区利用 new 开辟数组 ==========
void test02() {
    // 创建容量为 10 的整型数组,在堆区分配
    int* arr = new int[10];  // 10 代表数组有 10 个元素
    // new int[10] 返回 int*,指向数组首元素地址

    // 给数组元素赋值
    for (int i = 0; i < 10; i++)
    {
        arr[i] = i + 100;    // 给 10 个元素赋值:100-109
        // arr[i] 等价于 *(arr + i)
    }

    // 打印数组元素
    for (int i = 0; i < 10; i++)
    {
        cout << arr[i] << endl;  // 输出 100 到 109
    }

    // 释放数组:delete 后加 []
    delete[] arr;            // 释放整个数组内存
}

int main() {

    test01();   // 测试单个变量
    test02();   // 测试数组

    system("pause");
    return 0;
}

在现代 C++ 中,建议使用 std::vector 或 std::array 来替代手动管理堆区数组,它们会自动管理内存,更加安全便捷:

#include <vector>
#include <iostream>
using namespace std;

int main() {
    vector<int> arr(10);  // ★ 自动在堆上分配,但无需手动释放

    for (int i = 0; i < 10; i++) {
        arr[i] = i + 100;
    }

    for (int i = 0; i < 10; i++) {
        cout << arr[i] << endl;
    }

    // ★ 离开作用域时,vector 自动释放内存

    system("pause");
    return 0;
}

2 引用

2.1 引用的基本使用

**作用: **给变量起别名

**语法:** `数据类型 &别名 = 原名`

在语法层面,引用没有自己的地址,它就是你绑定的那个变量。

**示例:

int main() {

    int a = 10;
    int &b = a;

    cout << "a = " << a << endl;
    cout << "b = " << b << endl;

    b = 100;

    cout << "a = " << a << endl;
    cout << "b = " << b << endl;

    system("pause");

    return 0;
}

2.2 引用注意事项

* 引用必须初始化
* 引用在初始化后,不可以改变

示例:

int main() {

    int a = 10;
    int b = 20;
    //int &c; //错误,引用必须初始化
    int &c = a; //一旦初始化后,就不可以更改
    c = b; //这是赋值操作,不是更改引用

    cout << "a = " << a << endl;
    cout << "b = " << b << endl;
    cout << "c = " << c << endl;

    system("pause");

    return 0;
}

2.3 引用做函数参数

**作用:**函数传参时,可以利用引用的技术让形参修饰实参

**优点:**可以简化指针修改实参

#include <iostream>
using namespace std;

// ========== 1. 值传递 ==========
// 形参是普通变量,接收实参的副本
void mySwap01(int a, int b) {
    // a 和 b 是实参的副本,在栈上新分配内存
    int temp = a;   // temp = 10
    a = b;          // a = 20(修改的是副本)
    b = temp;       // b = 10(修改的是副本)
    // 函数结束,a、b、temp 全部销毁
    // 实参不受影响!
}

// ========== 2. 地址传递 ==========
// 形参是指针,接收实参的地址
void mySwap02(int* a, int* b) {
    // a 和 b 是存储地址的指针变量
    int temp = *a;   // temp = *a = 10(解引用获取值)
    *a = *b;         // *a = 20(修改 a 指向的内存)
    *b = temp;       // *b = 10(修改 b 指向的内存)
    // 通过地址间接修改了实参!
}

// ========== 3. 引用传递 ==========
// 形参是引用,是实参的别名
void mySwap03(int& a, int& b) {
    // a 和 b 是实参的别名(引用),直接操作实参
    int temp = a;   // temp = 10(直接读取实参 a)
    a = b;          // a = 20(直接修改实参 a)
    b = temp;       // b = 10(直接修改实参 b)
    // 直接修改了实参!
}

int main() {
    int a = 10;
    int b = 20;

    // 1. 值传递:传入变量值
    mySwap01(a, b);
    cout << "值传递后 a:" << a << " b:" << b << endl;  // a:10 b:20(不变)

    // 2. 地址传递:传入变量地址
    mySwap02(&a, &b);
    cout << "地址传递后 a:" << a << " b:" << b << endl;  // a:20 b:10(已交换)

    // 3. 引用传递:传入变量本身(引用)
    mySwap03(a, b);
    cout << "引用传递后 a:" << a << " b:" << b << endl;  // a:10 b:20(又换回来了)

    system("pause");
    return 0;
}

总结:通过引用参数产生的效果同按地址传递是一样的。引用的语法更清楚简单。

2.4 引用做函数返回值

作用:引用是可以作为函数的返回值存在的

注意:**不要返回局部变量引用**

用法:函数调用作为左值

**示例:

#include <iostream>
using namespace std;

// ========== 1. 返回局部变量的引用(危险!) ==========
// 函数功能:返回局部变量的引用
int& test01() {
    int a = 10;      // 局部变量,在栈上分配
    return a;        // ❌ 危险!返回局部变量的引用
    // 函数结束时,a 被销毁,返回的引用变成"悬空引用"
}

// ========== 2. 返回静态变量的引用(安全) ==========
// 函数功能:返回静态变量的引用
int& test02() {
    static int a = 20;  // 静态变量,在全局区分配
    return a;           // ✅ 安全!静态变量生命周期是整个程序
    // 函数结束,a 依然存在
}

int main() {
    
    // ========== 测试1:返回局部变量引用 ==========
    cout << "===== 测试 test01(局部变量) =====" << endl;
    int& ref = test01();  // ref 是 test01 中局部变量 a 的引用
    // 此时 a 已经被销毁,ref 是"悬空引用"
    
    cout << "ref = " << ref << endl;  // 第一次:可能输出 10(侥幸)
    cout << "ref = " << ref << endl;  // 第二次:可能输出随机值(危险!)
    // ⚠️ 这是未定义行为!
    
    // ========== 测试2:返回静态变量引用 ==========
    cout << "\n===== 测试 test02(静态变量) =====" << endl;
    int& ref2 = test02();  // ref2 是 test02 中静态变量 a 的引用
    // ✅ 安全!静态变量一直存在
    
    cout << "ref2 = " << ref2 << endl;  // 输出:20
    cout << "ref2 = " << ref2 << endl;  // 输出:20
    
    // ========== 函数调用作为左值 ==========
    cout << "\n===== 函数调用作为左值 =====" << endl;
    test02() = 1000;  // ✅ 函数返回引用,可以作为左值修改静态变量
    // test02() 返回的是静态变量的引用,所以可以赋值
    
    cout << "ref2 = " << ref2 << endl;  // 输出:1000(已被修改)
    cout << "ref2 = " << ref2 << endl;  // 输出:1000
    
    system("pause");
    return 0;
}

2.5 引用的本质

本质:引用的本质在c++内部实现是一个指针常量.

讲解示例:

//发现是引用,转换为 int* const ref = &a;
void func(int& ref){
    ref = 100; // ref是引用,转换为*ref = 100
}
int main(){
    int a = 10;

    //自动转换为 int* const ref = &a; 指针常量是指针指向不可改,也说明为什么引用不可更改
    int& ref = a; 
    ref = 20; //内部发现ref是引用,自动帮我们转换为: *ref = 20;

    cout << "a:" << a << endl;
    cout << "ref:" << ref << endl;

    func(a);
    return 0;
}

结论:C++推荐用引用技术,因为语法方便,引用本质是指针常量,但是所有的指针操作编译器都帮我们做了

2.6 常量引用

**作用:**常量引用主要用来修饰形参,防止误操作

在函数形参列表中,可以加const修饰形参,防止形参改变实参

**示例:

#include <iostream>
using namespace std;

// ========== const 引用作为形参 ==========
// 功能:只读访问,不能修改实参
void showValue(const int& v) {  // v 是 const 引用
    // v += 10;  // ❌ 错误!const 引用不能修改
    cout << v << endl;  // ✅ 只能读取
}

int main() {
    
    // ========== 1. const 引用绑定字面量 ==========
    // int& ref = 10;   // ❌ 错误!引用本身需要一个合法的内存空间
    // 原因:10 是字面量(右值),没有内存地址
    
    // ✅ 加入 const 就可以了
    const int& ref = 10;  
    // 编译器优化:int temp = 10; const int& ref = temp;
    // 创建临时变量 temp,ref 引用这个临时变量
    
    // ref = 100;  // ❌ 错误!const 引用不能修改
    
    cout << ref << endl;  // 输出:10
    
    // ========== 2. const 引用保护实参 ==========
    int a = 10;
    showValue(a);  // 传递 a 的引用,但函数内不能修改
    
    system("pause");
    return 0;
}

笔记:

“表达式必须是可修改的左值”这句报错,是 C++ 编译器在你试图修改一个“临时值”或“常量”时,对你的提醒。

要理解它,关键是要明白编译器在检查什么。我们可以把一个变量看作一个盒子。

 用“盒子”理解三个概念

左值 (Lvalue):一个有名字、有明确内存地址的盒子。比如你定义的变量 int a = 10;,a 就是一个左值。

可修改的左值 (Modifiable Lvalue):一个不是常量的、你可以打开往里放新东西的盒子。比如 int a 是可修改的,而 const int b = 20; 就不是(它是常量)。

右值 (Rvalue):一个临时的、没有名字的“值”本身,就像一个飞在空中的数字 10。它没有可以存放东西的“盒子”,用完就消失了。

⚙️ 表达式为什么会有“左值/右值”属性?

“表达式必须是可修改的左值”这个报错,本质是 C++ 在保护你不会误改一个不该改的值。

编译器在检查时,看的不是你写了什么字面量,而是整个表达式计算出的结果是什么“身份”。

a = 10:左边的 a 是一个左值表达式,代表那个盒子,所以可以往里写东西。

a + b = 10:左边的 a + b 是一个右值表达式,计算结果是一个临时的数字,没有可以存放数据的“盒子”,所以不能作为赋值的目标。

 常见报错场景

试图给字面量赋值:10 = a;(10 是右值)。

给常量赋值:const int a = 10; a = 20;(a 是左值但不是可修改的)。

给临时对象赋值:string("hello") = "world";(string("hello") 是右值)。

给表达式结果赋值:(a + b) = 100;((a + b) 是右值)。

 总结

当编译器报错说“表达式必须是可修改的左值”时,它实际上是在告诉你:

“你赋值符号 ‘=’ 左边的这个东西,计算结果不是一个我可以往里放数据的‘盒子’。”

你需要检查,确保赋值号左边的是一个可以存放数据的内存位置(变量),而不是一个临时的计算结果或常量。

常量引用(const T&)是一个指向只读数据的引用,它既避免拷贝(高效),又保护数据不被修改(安全),还能绑定到临时对象(灵活)。

#include <iostream>
using namespace std;

int main() {
    int a = 10;

    // ============================================================
    // 1. 绑定普通变量
    // ============================================================
    const int& ref1 = a;
    // ref1 = 20;   // ❌ 错误:不能通过常量引用修改
    cout << ref1;   // ✅ 可以读取

    // ============================================================
    // 2. 绑定字面量
    // ============================================================
    const int& ref2 = 100;   // ✅ 合法
    cout << ref2;            // 输出 100

    // ============================================================
    // 3. 绑定表达式结果
    // ============================================================
    const int& ref3 = a + 5;   // ✅ 合法(绑定到临时结果)
    cout << ref3;              // 输出 15

    // ============================================================
    // 4. 绑定临时对象(如函数返回值)
    // ============================================================
    string getString() { return "hello"; }
    const string& ref4 = getString();   // ✅ 合法
    cout << ref4;                       // 输出 "hello"

    return 0;
}

3 函数提高

3.1 函数默认参数

在C++中,函数的形参列表中的形参是可以有默认值的。

语法:` 返回值类型 函数名 (参数= 默认值){}`

**示例:

#include <iostream>        // 1. 包含输入输出流头文件,提供 cout、endl 等工具
using namespace std;       // 2. 使用标准命名空间 std,可以直接写 cout、endl

// ============================================================
// 3. 函数定义:带有默认参数的函数
//    参数:a 无默认值(必须传),b 默认 10,c 默认 10
//    规则:如果某个参数有默认值,则它右边的所有参数都必须有默认值
// ============================================================
int func(int a, int b = 10, int c = 10) {
    return a + b + c;      // 4. 返回三个参数的和
}

// ============================================================
// 5. ★ 默认参数的声明与实现分离规则
//    规则1:如果某个位置参数有默认值,那么从这个位置往后,从左向右,必须都要有默认值
//    规则2:如果函数声明有默认值,函数实现的时候就不能有默认参数
//    规则3:函数声明和函数实现只能有一个有默认参数
// ============================================================

// 6. 函数声明(在头文件或代码前部):带有默认参数
int func2(int a = 10, int b = 10);

// 7. 函数实现(定义):不再重复写默认参数
//    注意:如果这里再写 int a = 10 会报错(重复默认参数)
int func2(int a, int b) {
    return a + b;          // 8. 返回两个参数的和
}

// ============================================================
// 主函数
// ============================================================
int main() {
    
    // 9. 调用 func:传入 a=20,b=20,c 使用默认值 10
    //    结果:20 + 20 + 10 = 50
    cout << "ret = " << func(20, 20) << endl;
    
    // 10. 调用 func:传入 a=100,b 和 c 都使用默认值 10
    //     结果:100 + 10 + 10 = 120
    cout << "ret = " << func(100) << endl;
    
    system("pause");       // 11. 暂停控制台窗口,等待用户按键
    return 0;              // 12. 程序正常退出,返回 0 表示执行成功
}

普通引用 vs 常量引用

特性 普通引用 int& 常量引用 const int&
能否修改绑定的数据 ✅ 可以 ❌ 不可以
能否绑定变量 ✅ 可以 ✅ 可以
能否绑定字面量(如 10 ❌ 不可以 ✅ 可以
能否绑定临时对象 ❌ 不可以 ✅ 可以
能否绑定表达式结果 ❌ 不可以 ✅ 可以
使用场景 需要修改实参时 只读访问、函数参数

常量引用 vs 指针

特性 常量引用 const T& 指向常量的指针 const T*
语法 使用 . 访问成员 使用 -> 或 * 访问成员
是否能为空 ❌ 不能(必须绑定有效对象) ✅ 可以(nullptr
能否改变指向 ❌ 不能(引用一旦绑定不能改) ✅ 可以(指针本身可变)
使用场景 函数参数、只读访问 需要判空或改变指向的场合

常量引用(const T&)是 C++ 中实现“只读访问”的最佳方式,既高效(不拷贝)又安全(不能修改),还能兼容临时对象,是函数参数的黄金标准。

3.2 函数占位参数

C++中函数的形参列表里可以有占位参数,用来做占位,调用函数时必须填补该位置

语法:返回值类型 函数名 (数据类型){}

在现阶段函数的占位参数存在意义不大,但是后面的课程中会用到该技术

**示例:

#include <iostream>
using namespace std;

// ========== 函数占位参数 ==========
// 语法:在参数列表中只写类型,不写参数名
void func(int a, int) {  // 第一个参数有名字 a,第二个参数是占位参数
    cout << "this is func" << endl;
    // 注意:占位参数在函数体内无法使用(因为没有名字)
}

int main() {
    // 调用时:占位参数必须传值
    func(10, 10);  // 第一个参数 10 给 a,第二个 10 给占位参数
    
    // func(10);   // ❌ 错误!占位参数必须填补
    // func(10, 20, 30);  // ❌ 错误!参数数量不匹配
    
    system("pause");
    return 0;
}

占位参数 是在函数参数列表中只声明类型,不声明变量名的参数。

void func(int a, int) {
    // 第一个参数有名:a,可以使用
    // 第二个参数无名:占位参数,函数体内不能使用它
}

占位参数的特点

特点 说明
只有类型,没有名字 在参数列表中写 int 但不写变量名
函数体内无法使用 因为没有名字,所以无法访问它的值
调用时必须传值 占位参数也是参数,调用时必须传入对应类型的值
主要用途 用于函数重载、运算符重载、以及保持接口兼容性

为什么需要占位参数?

1. 保持接口兼容性

当函数需要升级时,可以添加占位参数而不影响已有的调用代码。

// 旧版本
void log(const string& msg) { ... }

// 新版本(增加日志级别,但保持旧调用兼容)
void log(const string& msg, int = 0) { ... }

log("hello");      // ✅ 仍然可以调用,第二个参数使用默认值 0
log("hello", 1);   // ✅ 也可以指定日志级别

2. 运算符重载中的后置 ++

class Counter {
public:
    // 前置++:++c
    Counter& operator++() { ... }
    
    // 后置++:c++(占位参数 int 用于区分前置和后置)
    Counter operator++(int) { ... }
};

3. 函数重载的区分

void process(int a) { ... }
void process(int a, int) { ... }   // 用占位参数区分重载

普通参数 vs 占位参数

对比 普通参数 占位参数
写法 int a(有名字) int(只有类型,无名字)
函数体内使用 ✅ 可以(通过名字访问) ❌ 不可以(没有名字)
调用时必须传值 ✅ 必须 ✅ 必须
有默认值 ✅ 可以 ✅ 可以(int = 0

        占位参数是函数参数列表中只有类型、没有名字的参数,主要用于保持接口兼容、区分函数重载和运算符重载。调用时仍需传值,但函数体内无法使用它。

3.3 函数重载

3.3.1 函数重载概述

**作用:**函数名可以相同,提高复用性

**函数重载满足条件:**

* 同一个作用域下
* 函数名称相同
* 函数参数**类型不同** 或者 **个数不同** 或者 **顺序不同**

**注意:** 函数的返回值不可以作为函数重载的条件

**示例:

#include <iostream>        // 1. 包含输入输出流头文件,提供 cout、endl 等工具
using namespace std;       // 2. 使用标准命名空间 std,可以直接写 cout、endl

// ============================================================
// 函数重载演示
// 条件:函数名相同,但参数列表不同(参数个数、类型或顺序不同)
// 注意:函数返回值类型不能作为重载的条件
// ============================================================

// 3. ★ 函数重载:参数列表为空的版本
void func()
{
    cout << "func 的调用!" << endl;      // 4. 输出提示信息
}

// 5. ★ 函数重载:参数列表为 int 的版本
void func(int a)
{
    cout << "func (int a) 的调用!" << endl;   // 6. 输出提示信息
}

// 7. ★ 函数重载:参数列表为 double 的版本
void func(double a)
{
    cout << "func (double a)的调用!" << endl; // 8. 输出提示信息
}

// 9. ★ 函数重载:参数列表为 int, double 的版本
void func(int a, double b)
{
    cout << "func (int a, double b) 的调用!" << endl;  // 10. 输出提示信息
}

// 11. ★ 函数重载:参数列表为 double, int 的版本(参数顺序不同,也算重载)
void func(double a, int b)
{
    cout << "func (double a, int b)的调用!" << endl;   // 12. 输出提示信息
}

// ============================================================
// 13. ★ 以下代码被注释,因为返回值类型不同不能作为重载条件
//     如果取消注释,编译器会报错:函数重载中,仅返回值类型不同是不允许的
// ============================================================
// int func(double a, int b)
// {
//     cout << "func (double a ,int b)的调用!" << endl;
//     return 0;
// }

// ============================================================
// 主函数
// ============================================================
int main() {
    
    // 14. 调用无参版本 → 匹配 void func()
    func();
    
    // 15. 调用 int 版本 → 匹配 void func(int a)
    func(10);
    
    // 16. 调用 double 版本 → 匹配 void func(double a)
    func(3.14);
    
    // 17. 调用 int, double 版本 → 匹配 void func(int a, double b)
    func(10, 3.14);
    
    // 18. 调用 double, int 版本 → 匹配 void func(double a, int b)
    func(3.14, 10);
    
    system("pause");       // 19. 暂停控制台窗口,等待用户按键
    return 0;              // 20. 程序正常退出,返回 0 表示执行成功
}

3.3.2 函数重载注意事项

* 引用作为重载条件
* 函数重载碰到函数默认参数

**示例:

#include <iostream>
using namespace std;

// ========== 1. 引用作为重载条件 ==========

// 重载版本1:int&(普通引用)
void func(int &a)  // 参数:int 类型的引用
{
    cout << "func (int &a) 调用 " << endl;
}

// 重载版本2:const int&(常量引用)
void func(const int &a)  // 参数:const int 类型的引用
{
    cout << "func (const int &a) 调用 " << endl;
}

// ========== 2. 函数重载碰到默认参数 ==========

// 重载版本1:两个参数,第二个有默认值
void func2(int a, int b = 10)
{
    cout << "func2(int a, int b = 10) 调用" << endl;
}

// 重载版本2:一个参数
void func2(int a)
{
    cout << "func2(int a) 调用" << endl;
}

int main() {
    int a = 10;
    
    // ========== 测试引用重载 ==========
    func(a);   // ✅ 调用无 const 版本:func(int &a)
    func(10);  // ✅ 调用有 const 版本:func(const int &a)
    
    // ========== 测试默认参数重载 ==========
    // func2(10); // ❌ 错误!产生二义性,编译失败
    func2(10,20);//正确可运行
    
    system("pause");
    return 0;
}

笔记:

这个例子非常经典,它完美地展示了 C++ 中 const 引用如何影响函数重载决议,以及普通引用和常量引用在绑定规则上的根本区别

我们可以从“变量”和“字面量”在内存中的不同角色来理解:

一、func(a):为什么匹配 func(int &a)

int a = 10;      // a 是一个可修改的变量(左值)
func(a);         // 调用 func(int &a)
  • a 的身份a 是一个在栈上分配内存的左值(lvalue),它有明确的地址,并且内容可以被修改。

  • 重载候选

    func(int &a):需要一个 int 类型的普通引用。普通引用必须绑定到一个可修改的左值a 完全符合这个要求,所以这是一个精确匹配
  • func(const int &a):需要一个 const int 类型的常量引用。虽然它也能绑定到 a,但编译器在匹配时,会优先选择最精确、限制最少的版本。普通引用比常量引用更匹配(因为它允许修改),所以编译器选择了版本1。

核心:普通引用(int&)是“可修改的左值”的最佳匹配。只要实参是可修改的左值,就会优先匹配普通引用。

二、func(10):为什么匹配 func(const int &a)

func(10);        // 调用 func(const int &a)
  • 10 的身份10 是一个字面量(literal),它没有自己的内存地址,是一个右值(rvalue),并且是常量(不可修改)。

  • 重载候选

    1. func(int &a)无法匹配!因为 C++ 规定,普通引用(int&不能绑定到右值(如字面量、临时对象)。这是为了防止你通过引用修改一个没有内存地址的临时值,从而引发错误。

    2. func(const int &a)完美匹配!因为常量引用(const int&)有一个特殊能力:它可以绑定到右值。这背后是编译器创建了一个临时变量来存储 10,然后让这个常量引用指向它,从而延长了临时变量的生命周期。

核心:常量引用(const int&)是“右值”和“临时对象”的“万能接收器”。当实参是字面量或临时对象时,只有常量引用能绑定它。


4 类和对象

C++面向对象的三大特性为:封装、继承、多态

C++认为万事万物都皆为对象,对象上有其属性和行为

Q1:类和对象有什么区别?

类是对一类事物的抽象描述,是模板;对象是类的具体实例,是模板生产出来的产品。类是静态的(定义),对象是动态的(在内存中存在)。

Q2:一个类可以创建多少个对象?

可以创建任意多个。就像一张图纸可以建造无数栋房子一样。

Q3:对象在内存中如何存储?

每个对象存储自己的成员变量(数据),但共享同一个类的成员函数(代码)。不同的对象占据不同的内存空间。

类的大小只取决于成员变量

类是一种抽象的定义,对象是类的实例化结果。

成员属性是“数据”,成员函数是“操作数据的行为”。它们共同构成了类的“封装体”,彼此紧密关联

核心理解

  • 成员属性(Member Variables):用来描述对象“有什么”(状态)。比如 Person 类中的 m_Name(姓名)、m_Car(汽车)、m_Password(密码)。

  • 成员函数(Member Functions):用来描述对象“能做什么”(行为)。比如 func(),它可以读取、修改,或者基于这些属性执行逻辑。

它们的关系是:成员函数通常围绕成员属性工作,操作和改变对象的状态。

  • 属性是类的“数据仓库”,函数是类的“操作中心”。

  • 成员函数是为成员属性服务的,它是改变或获取属性值的唯一途径(特别是对于私有属性)。

  • 你可以把类想象成一个智能设备属性是设备内部的零件(你看不到),成员函数是设备表面的按钮(你按按钮来操作设备)。

**例如:**

​ 人可以作为对象,属性有姓名、年龄、身高、体重...,行为有走、跑、跳、吃饭、唱歌...

​ 车也可以作为对象,属性有轮胎、方向盘、车灯...,行为有载人、放音乐、放空调...

​ 具有相同性质的对象,我们可以抽象称为类,人属于人类,车属于车类

4.1 封装

4.1.1 封装的意义

封装是C++面向对象三大特性之一

封装的意义:

* 将属性和行为作为一个整体,表现生活中的事物
* 将属性和行为加以权限控制

**封装意义一:**

​ 在设计类的时候,属性和行为写在一起,表现事物

**语法:** class 类名{ 访问权限: 属性 / 行为 };

**示例1:**设计一个圆类,求圆的周长

**示例代码:**

//圆周率
const double PI = 3.14;

//1、封装的意义
//将属性和行为作为一个整体,用来表现生活中的事物

//封装一个圆类,求圆的周长
//class代表设计一个类,后面跟着的是类名
class Circle
{
public:  //访问权限  公共的权限

    //属性
    int m_r;//半径

    //行为
    //获取到圆的周长
    double calculateZC()
    {
        //2 * pi  * r
        //获取圆的周长
        return  2 * PI * m_r;
    }
};

int main() {

    //通过圆类,创建圆的对象
    // c1就是一个具体的圆
    Circle c1;
    c1.m_r = 10; //给圆对象的半径 进行赋值操作

    //2 * pi * 10 = = 62.8
    cout << "圆的周长为: " << c1.calculateZC() << endl;

    system("pause");

    return 0;
}
#include <iostream>
#include <cmath>
using namespace std;
//圆周率
const double pi = 3.14;

//设计一个圆类,求圆的周长
//圆求周长的公式:2*pi*半径

//class代表设计一个类,类后面紧跟着的就是类名称
class Circle {

	//访问权限
	// 公共权限
public:

	//属性
	//半径
	int m_r;

	//行为
	//获取圆的周长
	double calculateZC() {
		return 2 * pi * m_r;
	}
};
int main() {

	//通过圆类创建具体的圆(对象)
	//实例化 (通过一个类,创建一个对象的过程)
	Circle c1;
	//给圆对象的属性进行赋值
	c1.m_r = 10;
	cout << "圆的周长为:" << c1.calculateZC() << endl;
	system("pause");
	return 0;
}

**示例2:**设计一个学生类,属性有姓名和学号,可以给姓名和学号赋值,可以显示学生的姓名和学号

**示例2代码:

//学生类
class Student {
public:
    void setName(string name) {
        m_name = name;
    }
    void setID(int id) {
        m_id = id;
    }

    void showStudent() {
        cout << "name:" << m_name << " ID:" << m_id << endl;
    }
public:
    string m_name;
    int m_id;
};

int main() {

    Student stu;
    stu.setName("德玛西亚");
    stu.setID(250);
    stu.showStudent();

    system("pause");

    return 0;
}
#include <iostream>    // 输入输出流,用于 cout、cin
#include <cmath>       // 数学函数库(此代码未使用,可删除)
#include <string>      // 字符串库,提供 string 类型
using namespace std;   // 使用标准命名空间

// ========== 设计学生类 ==========
// 类是一种用户自定义的数据类型,封装了属性和行为
class Student
{
    // 公共权限:类外部可以访问
public:
    // 类中的属性和行为,我们统一称为成员
    // 属性 = 成员属性 = 成员变量(存储数据)
    // 行为 = 成员函数 = 成员方法(操作数据)

    // ---------- 属性(成员变量) ----------
    string m_Name;   // 学生姓名,m_ 前缀表示 member(成员变量)
    int m_Id;        // 学生学号

    // ---------- 行为(成员函数) ----------

    // 显示学生信息
    void showStudent() {
        // 直接访问成员变量 m_Name 和 m_Id
        cout << "姓名:" << m_Name << " 学号:" << m_Id << endl;
    }

    // 给姓名赋值(设置器 Setter)
    void setName(string name) {
        m_Name = name;   // 将传入的参数赋值给成员变量
    }

    // 给学号赋值(设置器 Setter)
    void setId(int id) {
        m_Id = id;       // 将传入的参数赋值给成员变量
    }
};

int main() {
    // ========== 创建一个具体学生(实例化对象) ==========
    // Student 是类(图纸),s1 是根据图纸创建的对象(房子)
    Student s1;

    // ---------- 方式1:通过成员函数设置属性 ----------
    s1.setName("张三");   // 调用 setName 成员函数
    s1.setId(1);         // 调用 setId 成员函数
    // 注释掉的直接赋值方式:s1.m_Name = "张三";
    // 虽然 public 权限允许直接访问,但推荐使用成员函数封装

    // 显示学生信息
    s1.showStudent();     // 输出:姓名:张三 学号:1

    // ---------- 方式2:直接访问 public 属性 ----------
    Student s2;           // 创建第二个对象
    s2.m_Name = "李四";   // 直接修改 public 成员变量
    s2.m_Id = 2;
    s2.showStudent();     // 输出:姓名:李四 学号:2

    system("pause");
    return 0;
}

封装意义二:

类在设计时,可以把属性和行为放在不同的权限下,加以控制

访问权限有三种:

1. public 公共权限
2. protected 保护权限
3. private 私有权限

三种访问权限:

访问权限 关键字 类内访问 类外访问 用途
公共 public ✅ 可以 ✅ 可以 对外接口
保护 protected ✅ 可以 ❌ 不可以 继承时使用
私有 private ✅ 可以 ❌ 不可以 隐藏内部数据

常见术语总结

术语 含义 示例
类(Class) 自定义数据类型 class Student { ... }
对象(Object) 类的实例 Student s1;
成员变量 类中定义的变量 string m_Name;
成员函数 类中定义的函数 void showStudent()
实例化 创建对象的过程 Student s1;
封装 隐藏内部实现细节 通过 private + public
访问权限 控制成员的可见性 publicprotectedprivate

        

        成员函数属于类,有 this 指针,通过对象调用;全局函数不属于任何类,没有 this 指针,独立存在。

核心区别对比表

特性 成员函数 全局函数
属于谁 属于类 不属于任何类
调用方式 对象.函数() 或 指针->函数() 直接调用 函数()
是否有 this 指针 ✅ 有(指向调用对象) ❌ 没有
能否访问私有成员 ✅ 可以(自己是“自己人”) ❌ 不可以(除非声明为友元)
能否被继承 ✅ 可以(子类继承) ❌ 不可以
能否是虚函数 ✅ 可以(实现多态) ❌ 不可以
作用域 类作用域内 全局作用域
声明位置 在类内部 在类外部

成员函数 vs 全局函数的选择

场景 推荐 原因
操作需要访问私有成员 成员函数 只有成员函数和友元能访问私有成员
操作符左侧是自定义类型 成员函数 如 p1 + p2,左侧 p1 是 Person 对象
操作符左侧是内置类型 全局函数 如 10 + p1int 没有成员函数
操作不依赖于特定对象 全局函数 如工具函数 printMessage()
需要多态(虚函数) 成员函数 只有成员函数可以是虚函数
需要被继承和重写 成员函数 子类可以重写父类的成员函数

运算符重载中的选择

运算符 推荐方式 原因
+-*/ 成员函数 或 全局函数 都可以,看左侧操作数类型
=[]()-> 必须成员函数 这些运算符要求必须是成员函数
<<>>(输入输出) 必须全局函数 左侧是 ostream/istream,不是自定义类
+(混合类型) 全局函数 如 int + Person,左侧是 int

**示例:

#include <iostream>    // 输入输出流,用于 cout
#include <string>      // 字符串库,提供 string 类型
using namespace std;   // 使用标准命名空间

// ========== 访问权限 ==========
// 三种访问权限:
// 1. public(公共)   :类内可以访问,类外也可以访问
// 2. protected(保护) :类内可以访问,类外不可以访问,子类(派生类)可以访问
// 3. private(私有)   :类内可以访问,类外不可以访问,子类(派生类)不可以访问

class Person
{
    // ---------- 公共权限 ----------
public:
    string m_Name;   // 姓名,public:类外可以直接访问和修改

    // ---------- 保护权限 ----------
protected:
    string m_Car;    // 汽车,protected:类外不能直接访问,但子类可以

    // ---------- 私有权限 ----------
private:
    int m_Password;  // 银行卡密码,private:类外不能访问,子类也不能访问

    // ---------- 公共成员函数 ----------
public:
    void func()
    {
        // ✅ 类内可以访问所有权限的成员
        m_Name = "张三";      // public:可以访问
        m_Car = "拖拉机";     // protected:可以访问
        m_Password = 123456;  // private:可以访问
    }
};

int main() {
    // 实例化具体对象
    Person p1;
    
    // ✅ public:类外可以访问
    p1.m_Name = "李四";   // 合法,可以修改 public 成员
    
    // ❌ protected:类外不可以访问
    // p1.m_Car = "奔驰";   // 编译错误!'m_Car' 是 protected 成员
    
    // ❌ private:类外不可以访问
    // p1.m_Password = 123;  // 编译错误!'m_Password' 是 private 成员
    
    system("pause");
    return 0;
}

4.1.2 struct和class区别

在C++中 struct和class唯一的**区别**就在于 **默认的访问权限不同**

区别:

* struct 默认权限为公共
* class 默认权限为私有

类与结构体的区别

在 C++ 中,class 和 struct 几乎相同,唯一区别是默认访问权限

对比项 struct class
默认访问权限 public private
默认继承权限 public private
使用场景 纯数据容器 封装数据和行为
#include <iostream>    // 输入输出流
#include <string>      // 字符串库
using namespace std;   // 使用标准命名空间

// ========== 使用 class 定义 ==========
// class 的成员默认是 private(私有)
class C1
{
    int m_A;   // 没有写访问权限关键字 → 默认是 private
    // 等价于:private: int m_A;
};

// ========== 使用 struct 定义 ==========
// struct 的成员默认是 public(公共)
struct C2
{
    int m_A;   // 没有写访问权限关键字 → 默认是 public
    // 等价于:public: int m_A;
};

int main() {
    // ========== 测试 class ==========
    C1 c1;
    // c1.m_A = 10;   // ❌ 错误!m_A 是 private,类外不能访问
    
    // ========== 测试 struct ==========
    C2 c2;
    c2.m_A = 10;      // ✅ 正确!m_A 是 public,类外可以访问
    
    system("pause");
    return 0;
}

4.1.3 成员属性设置为私有

**优点1:**将所有成员属性设置为私有,可以自己控制读写权限

**优点2:**对于写权限,我们可以检测数据的有效性

**示例:

#include <iostream>    // ① 包含输入输出流头文件,提供 cout、endl 等工具
#include <string>      // ② 包含字符串头文件,提供 string 类型支持
using namespace std;   // ③ 使用标准命名空间 std,简化代码书写

// ============================================================
// ④ ★ 成员属性私有化 —— 封装的核心体现
// ============================================================
// 私有化的好处:
// 1. 可以自己控制读写权限(只读、只写、可读可写)
// 2. 对于写操作可以检测数据有效性(如年龄范围检查)
// ============================================================

// ⑤ 定义 Person 类
class Person {
public:
    // ============================================================
    // ★ 姓名:可读可写(提供 getter 和 setter)
    // ============================================================

    // ⑥ 设置姓名(setter)
    void setName(string name) {
        m_Name = name;   // ⑦ 将参数 name 赋值给私有成员 m_Name
    }

    // ⑧ 获取姓名(getter)
    string getName() {
        return m_Name;   // ⑨ 返回私有成员 m_Name 的值
    }

    // ============================================================
    // ★ 年龄:可读可写,且带有效性检查
    // ============================================================

    // ⑩ 获取年龄(getter)
    int getAge() {
        return m_Age;    // ⑪ 返回私有成员 m_Age 的值
    }

    // ⑫ 设置年龄(setter),带数据验证
    void setAge(int age) {
        // ⑬ ★ 数据有效性检查:年龄必须在 0~150 之间
        if (age < 0 || age > 150) {
            cout << "你个老妖精!" << endl;  // ⑭ 数据无效,拒绝修改并提示
            return;      // ⑮ 提前返回,不执行后面的赋值
        }
        m_Age = age;     // ⑯ 数据有效,允许修改
    }

    // ============================================================
    // ★ 情人:只写(只提供 setter,不提供 getter)
    // ============================================================

    // ⑰ 设置情人(setter)
    void setLover(string lover) {
        m_Lover = lover;   // ⑱ 将参数 lover 赋值给私有成员 m_Lover
        // ⑲ ★ 注意:没有提供 getLover() 方法,所以外部无法读取
    }

private:
    // ============================================================
    // ★ 私有成员变量(外部无法直接访问)
    // ============================================================

    string m_Name;   // ⑳ 姓名:提供 get/set,可读可写
    int m_Age;       // ㉑ 年龄:提供 get/set,但 set 有数据验证
    string m_Lover;  // ㉒ ★ 情人:只提供 set,只写(外部无法读取)
};

// ============================================================
// ㉓ 主函数
// ============================================================
int main() {
    Person p;   // ㉔ 实例化 Person 对象

    // ============================================================
    // ★ 测试姓名(可读可写)
    // ============================================================
    p.setName("张三");          // ㉕ 写操作:设置姓名
    cout << "姓名: " << p.getName() << endl;  // ㉖ 读操作:获取姓名

    // ============================================================
    // ★ 测试年龄(可读可写 + 数据验证)
    // ============================================================
    p.setAge(50);               // ㉗ 写操作:设置年龄(有效数据 50)
    cout << "年龄: " << p.getAge() << endl;  // ㉘ 读操作:获取年龄

    // ㉙ ★ 测试年龄有效性检查(取消注释可测试)
    // p.setAge(200);   // 如果执行,会输出"你个老妖精!",年龄不会被修改

    // ============================================================
    // ★ 测试情人(只写)
    // ============================================================
    p.setLover("牢大");        // ㉚ 写操作:设置情人(只能设置)
    // ㉛ ❌ 错误!m_Lover 是 private,外部无法直接访问
    // cout << "情人: " << p.m_Lover << endl;
    // ㉜ ★ 因为没有提供 getLover(),所以无法读取情人信息
    //     这就是"只写"的实现方式

    system("pause");   // ㉝ 暂停控制台窗口
    return 0;          // ㉞ 程序正常退出
}

**练习案例1:设计立方体类**

设计立方体类(Cube)

求出立方体的面积和体积

分别用全局函数和成员函数判断两个立方体是否相等。

#include <iostream>    // 1. 包含输入输出流头文件,提供 cout、endl 等工具
#include <string>      // 2. 包含字符串库(本代码虽未直接使用,但保留以备扩展)
using namespace std;   // 3. 使用标准命名空间 std

// ============================================================
// 立方体类设计
// 设计步骤:
// 1. 创建立方体类
// 2. 设计属性(长、宽、高)
// 3. 设计行为(获取面积和体积)
// 4. 分别利用全局函数和成员函数判断两个立方体是否相等
// ============================================================

class Cube
{
public:
    // ===== 长(Length)的 Getter/Setter =====
    void setL(int l) { m_L = l; }     // 4. 设置长的值
    int getL() { return m_L; }        // 5. 获取长的值

    // ===== 宽(Width)的 Getter/Setter =====
    void setW(int w) { m_W = w; }     // 6. 设置宽的值
    int getW() { return m_W; }        // 7. 获取宽的值

    // ===== 高(Height)的 Getter/Setter =====
    void setH(int h) { m_H = h; }     // 8. 设置高的值
    int getH() { return m_H; }        // 9. 获取高的值

    // ===== 计算表面积 =====
    // 公式:2 × (长×宽 + 长×高 + 宽×高)
    int calculateS() {
        return 2 * m_L * m_W + 2 * m_L * m_H + 2 * m_H * m_W;  // 10. 返回表面积
    }

    // ===== 计算体积 =====
    // 公式:长 × 宽 × 高
    int calculateV() {
        return m_L * m_W * m_H;       // 11. 返回体积
    }

    // ===== ★ 成员函数判断两个立方体是否相等 =====
    // 当前对象(this)与传入的 c 进行比较
    bool isSameByClass(Cube &c) {
        // 12. 直接访问自己的私有成员(m_L, m_W, m_H)
        //     通过 getter 访问传入对象的私有成员(因为 c 是外部对象,不能直接访问其私有成员)
        if (m_L == c.getL() && m_W == c.getW() && m_H == c.getH()) {
            return true;              // 13. 三个维度都相等,返回 true
        }
        return false;                 // 14. 否则返回 false
    }

private:
    int m_L;   // 15. ★ 私有成员:长
    int m_W;   // 16. ★ 私有成员:宽
    int m_H;   // 17. ★ 私有成员:高
};

// ============================================================
// ★ 全局函数判断两个立方体是否相等
// 通过 getter 访问两个对象的私有成员(因为全局函数不是类的成员,不能直接访问私有成员)
// ============================================================
bool isSame(Cube &c1, Cube &c2) {
    // 18. 分别获取两个立方体的长、宽、高,逐个比较
    if (c1.getL() == c2.getL() &&
        c1.getW() == c2.getW() &&
        c1.getH() == c2.getH()) {
        return true;                  // 19. 三个维度都相等,返回 true
    }
    return false;                     // 20. 否则返回 false
}

// ============================================================
// 主函数
// ============================================================
int main() {
    // ===== 创建第一个立方体 c1 =====
    Cube c1;                          // 21. 创建 Cube 对象 c1
    c1.setL(10);                      // 22. 设置长 = 10
    c1.setW(10);                      // 23. 设置宽 = 10
    c1.setH(10);                      // 24. 设置高 = 10

    cout << "c1的面积为:" << c1.calculateS() << endl;   // 25. 输出表面积:600
    cout << "c1的体积为:" << c1.calculateV() << endl;   // 26. 输出体积:1000

    // ===== 创建第二个立方体 c2 =====
    Cube c2;                          // 27. 创建 Cube 对象 c2
    c2.setL(10);                      // 28. 设置长 = 10
    c2.setW(10);                      // 29. 设置宽 = 10
    c2.setH(11);                      // 30. ★ 高不同(11),所以两个立方体不相等

    // ===== 方式1:全局函数判断 =====
    bool res = isSame(c1, c2);        // 31. 调用全局函数判断 c1 和 c2 是否相等
    if (res) {
        cout << "c1和c2是相等的" << endl;    // 32. 相等时输出
    } else {
        cout << "c1和c2是不相等的" << endl;  // 33. 不等时输出
    }

    // ===== 方式2:成员函数判断 =====
    res = c1.isSameByClass(c2);       // 34. ★ c1 调用成员函数,c2 作为参数传入
    if (res) {
        cout << "成员函数判断:c1和c2是相等的" << endl;   // 35. 相等时输出
    } else {
        cout << "成员函数判断:c1和c2是不相等的" << endl; // 36. 不等时输出
    }

    system("pause");                  // 37. 暂停控制台窗口
    return 0;                         // 38. 程序正常退出
}

**练习案例2:点和圆的关系**

设计一个圆形类(Circle),和一个点类(Point),计算点和圆的关系。

#include <iostream>    // 输入输出流
#include <string>      // 字符串库
using namespace std;   // 使用标准命名空间

// ========== 点类 ==========
// 表示二维平面上的一个点,包含 X 和 Y 坐标
class Point
{
public:
    // 设置 X 坐标
    void setX(int x) { m_X = x; }
    // 获取 X 坐标
    int getX() { return m_X; }

    // 设置 Y 坐标
    void setY(int y) { m_Y = y; }
    // 获取 Y 坐标
    int getY() { return m_Y; }

private:
    int m_X;   // X 坐标(私有)
    int m_Y;   // Y 坐标(私有)
};

// ========== 圆类 ==========
// 包含半径和圆心(Point 类型)
class Circle
{
public:
    // 设置半径
    void setR(int r) { m_R = r; }
    // 获取半径
    int getR() { return m_R; }

    // 设置圆心(传入 Point 对象)
    void setCenter(Point center) { m_Center = center; }
    // 获取圆心(返回 Point 对象)
    Point getCenter() { return m_Center; }

private:
    int m_R;          // 半径(私有)

    //在类中可以让另一个类作为本类中的成员
    Point m_Center;   // 圆心(私有,Point 类型)
};

// ========== 判断点和圆的关系 ==========
// 通过距离公式判断点在圆内、圆上还是圆外
void isInCircle(Circle& c, Point& p)
{
    // 计算两点之间距离的平方
    // 公式:d² = (x₁ - x₂)² + (y₁ - y₂)²
    int distance = (c.getCenter().getX() - p.getX()) * (c.getCenter().getX() - p.getX())
        + (c.getCenter().getY() - p.getY()) * (c.getCenter().getY() - p.getY());

    // 计算半径的平方
    int rDistance = c.getR() * c.getR();

    // 判断关系
    if (distance == rDistance) {
        cout << "点在圆上" << endl;
    }
    else if (distance > rDistance) {
        cout << "点在圆外" << endl;
    }
    else {
        cout << "点在圆内" << endl;
    }
}

int main() {
    // ===== 步骤1:创建圆 =====
    // 圆心 (10, 0),半径 10
    Circle c;
    c.setR(10);

    Point center;
    center.setX(10);
    center.setY(0);
    c.setCenter(center);

    // ===== 步骤2:测试点1:在圆上 (10, 10) =====
    Point p;
    p.setX(10);
    p.setY(10);
    isInCircle(c, p);   // 输出:点在圆上

    // ===== 步骤3:复用 p,修改为 (10, 9) =====
    p.setX(10);
    p.setY(9);
    isInCircle(c, p);   // 输出:点在圆内

    // ===== 步骤4:复用 p,修改为 (10, 11) =====
    p.setX(10);
    p.setY(11);
    isInCircle(c, p);   // 输出:点在圆外

    system("pause");
    return 0;
}
// ===== circle.h =====
#pragma once
#include <iostream>
#include "point.h"   // 需要包含 point.h,因为使用了 Point 类型
using namespace std;

class Circle
{
public:
    // 函数声明
    void setR(int r);
    int getR();
    void setCenter(Point center);
    Point getCenter();

private:
    int m_R;          // 半径
    Point m_Center;   // 圆心(Point 类型)
};
// ===== circle.cpp =====
#include "circle.h"   // 包含头文件

// 设置半径
void Circle::setR(int r) {
    m_R = r;
}

// 获取半径
int Circle::getR() {
    return m_R;
}

// 设置圆心
void Circle::setCenter(Point center) {
    m_Center = center;
}

// 获取圆心
Point Circle::getCenter() {
    return m_Center;
}
// ===== point.h =====
// #pragma once 防止头文件被多次包含
#pragma once

#include <iostream>
using namespace std;

// ========== 点类 ==========
// 表示二维平面上的一个点
class Point
{
public:
    // 函数声明(只声明,不实现)
    void setX(int x);
    int getX();
    void setY(int y);
    int getY();

private:
    int m_X;   // X 坐标(私有)
    int m_Y;   // Y 坐标(私有)
};
//作用:
//实现函数:提供函数的具体代码
//Point:::作用域解析运算符,表示这些函数属于 Point 类
/*// 在类外定义成员函数
返回值类型 类名::函数名(参数) {
    // 函数体
}
void Point::setX(int x) { m_X = x; }
//  ↑      ↑
//  类名   作用域解析运算符
*/

// ===== point.cpp =====
#include "point.h"   // 包含头文件,让编译器知道函数声明

// 设置 X 坐标(实现)
void Point::setX(int x) {
    m_X = x;
}

// 获取 X 坐标(实现)
//Point:::作用域解析运算符,表示这些函数属于 Point 类
int Point::getX() {
    return m_X;
}

// 设置 Y 坐标(实现)
void Point::setY(int y) {
    m_Y = y;
}

// 获取 Y 坐标(实现)
int Point::getY() {
    return m_Y;
}
// ===== main.cpp =====
#include <iostream>     // 标准输入输出
#include <string>       // 字符串库
#include "circle.h"     // 包含圆类
#include "point.h"      // 包含点类
using namespace std;

// 判断点和圆的关系(全局函数)
void isInCircle(Circle& c, Point& p) {
    // 计算距离的平方
    int distance = (c.getCenter().getX() - p.getX()) * (c.getCenter().getX() - p.getX())
        + (c.getCenter().getY() - p.getY()) * (c.getCenter().getY() - p.getY());
    int rDistance = c.getR() * c.getR();

    if (distance == rDistance)
        cout << "点在圆上" << endl;
    else if (distance > rDistance)
        cout << "点在圆外" << endl;
    else
        cout << "点在圆内" << endl;
}

int main() {
    // 创建圆
    Circle c;
    c.setR(10);
    Point center;
    center.setX(10);
    center.setY(0);
    c.setCenter(center);

    // 测试三个点
    Point p;
    p.setX(10);
    p.setY(10);
    isInCircle(c, p);   // 点在圆上

    p.setY(9);
    isInCircle(c, p);   // 点在圆内

    p.setY(11);
    isInCircle(c, p);   // 点在圆外

    system("pause");
    return 0;
}

总结:头文件声明类的接口,源文件实现函数功能,主程序通过包含头文件使用类;编译时分别编译,链接时合并为可执行文件。

4.2 对象的初始化和清理

* 生活中我们买的电子产品都基本会有出厂设置,在某一天我们不用时候也会删除一些自己信息数据保证安全
* C++中的面向对象来源于生活,每个对象也都会有初始设置以及对象销毁前的清理数据的设置。

4.2.1 构造函数和析构函数

对象的初始化和清理也是两个非常重要的安全问题

​一个对象或者变量没有初始状态,对其使用后果是未知的,​同样的使用完一个对象或变量,没有及时清理,也会造成一定的安全问题。

c++利用了**构造函数**和**析构函数**解决上述问题,这两个函数将会被编译器自动调用,完成对象初始化和清理工作。

对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供。

编译器提供的构造函数和析构函数是空实现。


* 构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
* 析构函数:主要作用在于对象**销毁前**系统自动调用,执行一些清理工作。


**构造函数语法:**`类名(){}`

1. 构造函数,没有返回值也不写void
2. 函数名称与类名相同
3. 构造函数可以有参数,因此可以发生重载
4. 程序在调用对象时会自动调用构造,无须手动调用,而且只会调用一次


**析构函数语法:** `~类名(){}`

1. 析构函数,没有返回值也不写void
2. 函数名称与类名相同,在名称前加上符号 ~
3. 析构函数不可以有参数,因此不可以发生重载
4. 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次


构造函数的三大核心目的

目的 说明
初始化成员变量 确保对象一诞生就有明确的初始值
保证对象有效性 防止因未初始化而导致程序行为不可预测
资源获取 在对象创建时获取所需资源(如内存、文件句柄、网络连接)

#include <iostream>    // 输入输出流
#include <string>      // 字符串库
using namespace std;

// ========== 对象的初始化和清理 ==========
// 1. 构造函数:进行初始化操作
// 2. 析构函数:进行清理操作

class Person
{
public:
    // ===== 1. 构造函数(Constructor) =====
    // 特点:
    // - 没有返回值,不用写 void
    // - 函数名和类名相同
    // - 构造函数可以有参数,可以发生重载
    // - 创建对象的时候,构造函数会自动调用,而且只调用一次
    
    Person()
    {
        cout << "Person构造函数的调用" << endl;
    }
    
    // ===== 2. 析构函数(Destructor) =====
    // 特点:
    // - 没有返回值,不写 void
    // - 函数名和类名相同,在名称前加 ~
    // - 析构函数不可以有参数,不可以发生重载
    // - 对象在销毁前,会自动调用析构函数,而且只会调用一次
    
    ~Person() {
        cout << "Person的析构函数调用" << endl;
    }
};

// 构造和析构都是必须有的实现
// 如果我们自己不提供,编译器会提供一个空实现的构造和析构

void test01() {
    Person p;   // 在栈上的数据,test01 执行完毕后,释放这个对象
    // 函数结束时,p 会被销毁,析构函数自动调用
}

int main() {
    // test01();   // 如果调用,会看到构造和析构的输出
    
    Person p;   // 创建对象 → 调用构造函数
    
    system("pause");
    return 0;   // main 函数结束 → p 被销毁 → 调用析构函数
}

        创建对象时自动调用构造函数进行初始化,销毁对象时自动调用析构函数进行清理;如果不自己提供,编译器会生成空实现的版本。

        创建对象的核心本质就是调用构造函数。

对象的创建过程

在 C++ 中,创建对象有以下几种方式,但所有方式都会调用构造函数

// ===== 1. 栈上创建 =====
Person p;              // ✅ 调用默认构造函数
Person p(10);          // ✅ 调用有参构造函数
Person p(p1);          // ✅ 调用拷贝构造函数

// ===== 2. 堆上创建(new) =====
Person* p = new Person();     // ✅ 调用默认构造函数
Person* p = new Person(10);   // ✅ 调用有参构造函数
Person* p = new Person(p1);   // ✅ 调用拷贝构造函数

// ===== 3. 全局/静态区创建 =====
static Person p;       // ✅ 调用默认构造函数
Person p;              // 全局变量,调用默认构造函数

// ===== 4. 数组创建 =====
Person arr[5];         // ✅ 每个元素调用默认构造(5次)
Person* arr = new Person[5];   // ✅ 每个元素调用默认构造(5次)

创建对象 = 分配内存 + 调用构造

步骤 说明
① 分配内存 在栈/堆/全局区上分配对象所需的内存空间
② 调用构造 调用对应的构造函数初始化该内存
③ 对象诞生 构造函数执行完后,才成为真正的对象

代码验证

#include <iostream>
using namespace std;

class Person {
public:
    int age;
    
    // 默认构造
    Person() {
        cout << "默认构造被调用" << endl;
        age = 0;
    }
    
    // 有参构造
    Person(int a) {
        cout << "有参构造被调用,age = " << a << endl;
        age = a;
    }
    
    // 拷贝构造
    Person(const Person& p) {
        cout << "拷贝构造被调用,age = " << p.age << endl;
        age = p.age;
    }
};

int main() {
    cout << "--- 栈上创建 ---" << endl;
    Person p1;          // 默认构造
    Person p2(10);      // 有参构造
    Person p3(p2);      // 拷贝构造
    
    cout << "\n--- 堆上创建 ---" << endl;
    Person* p4 = new Person();      // 默认构造
    Person* p5 = new Person(20);    // 有参构造
    Person* p6 = new Person(p2);    // 拷贝构造
    
    delete p4;
    delete p5;
    delete p6;
    
    cout << "\n--- 数组创建 ---" << endl;
    Person arr[2];      // 默认构造 × 2
    
    return 0;
}

各种创建方式汇总

创建方式 语法 调用的构造函数
栈上对象 Person p; 默认构造
栈上对象(带参) Person p(10); 有参构造
栈上对象(拷贝) Person p(p1); 拷贝构造
堆上对象 new Person(); 默认构造
堆上对象(带参) new Person(10); 有参构造
堆上对象(拷贝) new Person(p1); 拷贝构造
全局对象 Person p;(全局) 默认构造
对象数组 Person arr[5]; 默认构造 × N

总结

所有创建对象的方式,最终都会调用构造函数。构造函数的调用是对象诞生的标志——分配内存只是提供空间,构造才是让对象“活过来”的关键步骤。

4.2.2 构造函数的分类及调用

两种分类方式:

        ​ 按参数分为: 有参构造和无参构造

         按类型分为: 普通构造和拷贝构造

三种调用方式:

        ​ 括号法

        ​ 显式法

​         隐式转换法

匿名对象

·        匿名对象是没有名字的临时对象,它“用完即焚”——在当前行执行结束后,立即被销毁。

匿名对象的本质

匿名对象是临时对象的一种。它没有名字,所以:

  • 无法在后续代码中引用它(因为没有名字)。

  • 生命周期由编译器管理(在当前行结束时自动销毁)。

  • 主要用于临时计算或传参

匿名对象 vs 有名对象

特性 匿名对象 有名对象
是否有名字 ❌ 没有 ✅ 有
是否可重复使用 ❌ 不可以(行结束即销毁) ✅ 可以(在整个作用域内)
生命周期 极短(当前行结束) 较长(直到作用域结束)
典型用途 临时计算、传参 长期持有、反复使用

匿名对象是没有名字的临时对象,它只在当前行有效,行结束立即销毁。常用于临时传参或作为返回值。

**示例:

//1、构造函数分类
// 按照参数分类分为 有参和无参构造   无参又称为默认构造函数
// 按照类型分类分为 普通构造和拷贝构造

class Person {
public:
    //无参(默认)构造函数
    Person() {
        cout << "无参构造函数!" << endl;
    }
    //有参构造函数
    Person(int a) {
        age = a;
        cout << "有参构造函数!" << endl;
    }
    //拷贝构造函数
    Person(const Person& p) {
        age = p.age;
        cout << "拷贝构造函数!" << endl;
    }
    //析构函数
    ~Person() {
        cout << "析构函数!" << endl;
    }
public:
    int age;
};

//2、构造函数的调用
//调用无参构造函数
void test01() {
    Person p; //调用无参构造函数
}

//调用有参的构造函数
void test02() {

    //2.1  括号法,常用
    Person p1(10);
    //注意1:调用无参构造函数不能加括号,如果加了编译器认为这是一个函数声明
    //Person p2();

    //2.2 显式法
    Person p2 = Person(10); 
    Person p3 = Person(p2);
    //Person(10)单独写就是匿名对象  当前行结束之后,马上析构

    //2.3 隐式转换法
    Person p4 = 10; // Person p4 = Person(10); 
    Person p5 = p4; // Person p5 = Person(p4); 

    //注意2:不能利用 拷贝构造函数 初始化匿名对象 编译器认为是对象声明
    //Person p5(p4);
}

int main() {

    test01();
    //test02();

    system("pause");

    return 0;
}

#include <iostream>     // 标准输入输出
#include <string>       // 字符串库
using namespace std;

// ========== 构造函数的分类及调用 ==========
// 分类方式:
// 1. 按参数分类:无参构造(默认构造)和有参构造
// 2. 按类型分类:普通构造 和 拷贝构造

class Person
{
public:
    // ===== 1. 无参构造函数(默认构造) =====
    Person()
    {
        cout << "Person的无参构造函数调用" << endl;
    }
    
    // ===== 2. 有参构造函数 =====
    Person(int a)
    {
        age = a;
        cout << "Person的有参构造函数调用,age = " << age << endl;
    }
    
    // ===== 3. 拷贝构造函数 =====
    // 用已有对象初始化新对象
    Person(const Person& p)
    {
        age = p.age;
        cout << "Person的拷贝构造函数调用,age = " << age << endl;
    }

    // ===== 析构函数 =====
    ~Person()
    {
        cout << "Person的析构函数调用" << endl;
    }

    int age;   // 年龄
};

void test01() {
    // ========================================
    // 1. 括号法(最常用)
    // ========================================
    cout << "\n=== 括号法 ===" << endl;
    
    Person p1;        // ✅ 无参构造
    Person p2(10);    // ✅ 有参构造
    Person p3(p2);    // ✅ 拷贝构造
    
    cout << "p2的年龄为:" << p2.age << endl;
    cout << "p3的年龄为:" << p3.age << endl;

    // ⚠️ 注意事项1:调用默认构造函数时,不要加 ()
    // Person p1();   // ❌ 错误!编译器认为是函数声明
    // 这行会被解释为:声明一个函数 p1,返回 Person 类型,无参数

    // ========================================
    // 2. 显式法
    // ========================================
    cout << "\n=== 显式法 ===" << endl;
    
    Person p4;              // ✅ 无参构造
    Person p5 = Person(20); // ✅ 有参构造
    Person p6 = Person(p5); // ✅ 拷贝构造

    // ----- 匿名对象 -----
    Person(10);   // 匿名对象:当前行执行结束后,系统会立即回收
    cout << "aaaa" << endl;
    // 输出顺序:
    // Person的有参构造函数调用,age = 10
    // Person的析构函数调用  ← 匿名对象立即销毁
    // aaaa

    // ⚠️ 注意事项2:不要用拷贝构造初始化匿名对象
    // Person(p3);   // ❌ 错误!编译器会认为这是 Person p3; 的重复声明
    // 这行会被解释为:Person p3; 导致重复定义错误

    // ========================================
    // 3. 隐式转换法
    // ========================================
    cout << "\n=== 隐式转换法 ===" << endl;
    
    Person p7 = 30;   // ✅ 等价于 Person p7 = Person(30); 有参构造
    Person p8 = p7;   // ✅ 拷贝构造
}

int main() {
    test01();
    
    system("pause");
    return 0;
}
//Person(10);//匿名对象 特点:当前行执行结束后,系统会立即回收掉匿名对象
//cout << "aaaa" << endl;

4.2.3 拷贝构造函数调用时机

C++中拷贝构造函数调用时机通常有三种情况

* 使用一个已经创建完毕的对象来初始化一个新对象
* 值传递的方式给函数参数传值
* 以值方式返回局部对象

三种调用时机一览

时机 说明 代码示例
1. 用已有对象初始化新对象 对象在创建时被初始化 Person p2(p1);
2. 值传递传参 函数形参是值传递 void doWork(Person p)
3. 值方式返回局部对象 函数返回局部对象 return p1;

情况1:用已有对象初始化新对象

void test01() {
    Person p1(20);      // ① 有参构造
    Person p2(p1);      // ② 拷贝构造(括号法)
    Person p3 = p1;     // ③ 拷贝构造(隐式转换法)
}

关键判断:等号左边有类型声明Person),说明是在创建新对象

代码 调用 说明
Person p1(20); 有参构造 创建 p1
Person p2(p1); 拷贝构造 用 p1 初始化 p2(括号法)
Person p3 = p1; 拷贝构造 用 p1 初始化 p3(隐式转换法)
p3 = p1; 赋值操作 p3 已存在,不是拷贝构造!

注意Person p3 = p1; 是拷贝构造,因为 p3 正在被创建。
p3 = p1; 是赋值操作,因为 p3 已经存在。

情况2:值传递传参

void doWork(Person p) {   // ① 形参是值传递
    cout << "doWork 函数内部" << endl;
}

void test02() {
    Person p;             // ② 无参构造
    doWork(p);            // ③ 值传递 → 调用拷贝构造
}
① 创建实参 p(无参构造)
② 调用 doWork(p)
③ 形参 p1 是实参 p 的副本 → 调用拷贝构造
④ 函数结束 → 形参 p1 销毁(析构)
⑤ 继续执行

为什么会有拷贝构造?

因为函数参数是值传递,编译器需要把实参 p 的值复制一份给形参 p1。这个“复制”过程就是调用拷贝构造函数。

情况3:以值方式返回局部对象

Person doWork2() {
    Person p1;            // ① 无参构造
    cout << &p1 << endl;
    return p1;            // ② 返回值 → 调用拷贝构造
}

void test03() {
    Person p = doWork2(); // ③ 接收返回值 → 可能调用拷贝构造
    cout << &p << endl;
}

执行顺序

① 进入 doWork2()
② 创建局部对象 p1(无参构造)
③ return p1 → 创建临时对象(调用拷贝构造),复制 p1 的数据
④ 局部对象 p1 销毁(析构)
⑤ 返回临时对象,用来初始化 p(可能调用拷贝构造)
⑥ 临时对象销毁(析构)
⑦ test03 结束 → p 销毁(析构)

现代编译器的优化(RVO/NRVO)

实际运行中,编译器通常会进行返回值优化(RVO),直接构造 p,避免多余的拷贝构造。所以实际打印结果可能与理论略有不同。

test03() 调用
    │
    ▼
进入 doWork2()
    │
    ▼
创建 p1(无参构造)→ 输出 "Person默认构造函数调用"
    │
    ▼
return p1 → 拷贝构造临时对象 → 输出 "Person拷贝构造函数调用"
    │
    ▼
p1 销毁 → 输出 "Person析构函数调用"
    │
    ▼
返回临时对象 → 拷贝构造 p → 输出 "Person拷贝构造函数调用"
    │
    ▼
临时对象销毁 → 输出 "Person析构函数调用"
    │
    ▼
test03 结束 → p 销毁 → 输出 "Person析构函数调用"

总结:拷贝构造函数在“对象被创建并同时用另一个对象初始化”时调用,包括:新对象初始化、值传递传参、值返回局部对象。

**示例:

class Person {
public:
    Person() {
        cout << "无参构造函数!" << endl;
        mAge = 0;
    }
    Person(int age) {
        cout << "有参构造函数!" << endl;
        mAge = age;
    }
    Person(const Person& p) {
        cout << "拷贝构造函数!" << endl;
        mAge = p.mAge;
    }
    //析构函数在释放内存之前调用
    ~Person() {
        cout << "析构函数!" << endl;
    }
public:
    int mAge;
};

//1. 使用一个已经创建完毕的对象来初始化一个新对象
void test01() {
    //拷贝构造发生在“对象创建时”,赋值操作发生在“对象创建后”
    Person man(100);        // ① 有参构造

    // ============================================================
    // ★ 拷贝构造:对象在创建时被初始化
    // ============================================================
    Person newman(man);     // ② 拷贝构造(括号法)
    Person newman2 = man;   // ③ 拷贝构造(隐式转换法)

    // ============================================================
    // ★ 赋值操作:对象已经存在,进行赋值
    // ============================================================
    Person newman3;         // ④ 无参构造(对象已存在)
    newman3 = man;          // ⑤ 赋值操作(不是拷贝构造!)
}

//2. 值传递的方式给函数参数传值
//相当于Person p1 = p;
void doWork(Person p1) {}
void test02() {
    Person p; //无参构造函数
    doWork(p);
}

//3. 以值方式返回局部对象
Person doWork2()
{
    Person p1;
    cout << (int *)&p1 << endl;
    return p1;
}

void test03()
{
    Person p = doWork2();
    cout << (int *)&p << endl;
}


int main() {

    //test01();
    //test02();
    test03();

    system("pause");

    return 0;
}
#include <iostream>     // 标准输入输出
#include <string>       // 字符串库
using namespace std;

/* =================================================================
 * C++ 中拷贝构造函数调用时机通常有三种情况:
 * 1. 使用一个已经创建完毕的对象来初始化一个新对象
 * 2. 值传递的方式给函数参数传值
 * 3. 以值方式返回局部对象
 * ================================================================= */

class Person
{
public:
    // 无参构造(默认构造)
    Person()
    {
        cout << "Person默认构造函数调用" << endl;
    }

    // 有参构造
    Person(int age)
    {
        cout << "Person有参构造函数调用" << endl;
        m_Age = age;
    }

    // 拷贝构造
    Person(const Person& p)
    {
        cout << "Person拷贝构造函数调用" << endl;
        m_Age = p.m_Age;
    }

    // 析构函数
    ~Person()
    {
        cout << "Person析构函数调用" << endl;
    }

    int m_Age;
};

// ===== 情况1:使用已有对象初始化新对象 =====
void test01()
{
    cout << "\n=== 情况1:使用已有对象初始化新对象 ===" << endl;

    Person p1(20);      // 有参构造
    Person p2(p1);      // 拷贝构造(用 p1 初始化 p2)

    cout << "p2的年龄为:" << p2.m_Age << endl;
}

// ===== 情况2:值传递方式给函数传参 =====
void doWork(Person p)   // 形参是值传递,会调用拷贝构造
{
    cout << "doWork 函数内部" << endl;
}

void test02()
{
    cout << "\n=== 情况2:值传递方式给函数传参 ===" << endl;

    Person p;           // 默认构造
    doWork(p);          // 值传递 → 调用拷贝构造
}

// ===== 情况3:以值方式返回局部对象 =====
Person doWork2()
{
    cout << "进入 doWork2 函数" << endl;
    Person p1;          // 默认构造
    cout << (int*) & p1 << endl;
    cout << "准备返回 p1" << endl;
    return p1;          // 返回值 → 调用拷贝构造(可能被编译器优化)
}

void test03()
{
    cout << "\n=== 情况3:以值方式返回局部对象 ===" << endl;

    Person p = doWork2();  // 接收返回值 → 调用拷贝构造
    cout << &p << endl;
    cout << "doWork2 返回后" << endl;
}

int main() {
    // test01();   // 测试情况1
    // test02();   // 测试情况2
    test03();       // 测试情况3

    system("pause");
    return 0;
}

4.2.4 构造函数调用规则

默认情况下,c++编译器至少给一个类添加3个函数

1.默认构造函数(无参,函数体为空)

2.默认析构函数(无参,函数体为空)

3.默认拷贝构造函数,对属性进行值拷贝

构造函数调用规则如下:

* 如果用户定义有参构造函数,c++不再提供默认无参构造,但是会提供默认拷贝构造

* 如果用户定义拷贝构造函数,c++不会再提供其他构造函数

示例:

class Person {
public:
    //无参(默认)构造函数
    Person() {
        cout << "无参构造函数!" << endl;
    }
    //有参构造函数
    Person(int a) {
        age = a;
        cout << "有参构造函数!" << endl;
    }
    //拷贝构造函数
    Person(const Person& p) {
        age = p.age;
        cout << "拷贝构造函数!" << endl;
    }
    //析构函数
    ~Person() {
        cout << "析构函数!" << endl;
    }
public:
    int age;
};

void test01()
{
    Person p1(18);
    //如果不写拷贝构造,编译器会自动添加拷贝构造,并且做浅拷贝操作
    Person p2(p1);

    cout << "p2的年龄为: " << p2.age << endl;
}

void test02()
{
    //如果用户提供有参构造,编译器不会提供默认构造,会提供拷贝构造
    Person p1; //此时如果用户自己没有提供默认构造,会出错
    Person p2(10); //用户提供的有参
    Person p3(p2); //此时如果用户没有提供拷贝构造,编译器会提供

    //如果用户提供拷贝构造,编译器不会提供其他构造函数
    Person p4; //此时如果用户自己没有提供默认构造,会出错
    Person p5(10); //此时如果用户自己没有提供有参,会出错
    Person p6(p5); //用户自己提供拷贝构造
}

int main() {

    test01();

    system("pause");

    return 0;
}
class Person {
public:
    int age;

    // ★ 返回引用:返回对象本身,不拷贝
    Person& getSelf() {
        return *this;   // 返回当前对象的引用
    }

    // ★ 返回引用:可以修改传入的对象
    Person& setAge(int a) {
        this->age = a;
        return *this;   // 支持链式调用
    }

    // ★ 返回值:返回副本(拷贝)
    Person copyPerson() {
        return *this;   // 返回当前对象的副本
    }
};

这三者的核心区别在于返回的是“对象本身”还是“对象的副本”

函数 返回类型 返回内容 是否拷贝 能否修改原对象
getSelf() Person& 当前对象本身(引用) ❌ 不拷贝 ✅ 可以
setAge(int) Person& 当前对象本身(引用) ❌ 不拷贝 ✅ 可以
copyPerson() Person 当前对象的副本 ✅ 拷贝 ❌ 不可以(修改的是副本)
#include <iostream>
using namespace std;

class Person {
public:
    int age;

    // ① 返回引用:返回对象本身
    Person& getSelf() {
        return *this;   // 返回当前对象
    }

    // ② 返回引用:修改并返回自身(支持链式调用)
    Person& setAge(int a) {
        this->age = a;
        return *this;   // 返回当前对象,允许连续调用
    }

    // ③ 返回值:返回副本(拷贝)
    Person copyPerson() {
        return *this;   // 返回当前对象的副本
    }
};

int main() {
    Person p1;
    p1.age = 10;

    cout << "=== 测试 getSelf() ===" << endl;
    Person& ref = p1.getSelf();   // ★ ref 是 p1 的引用
    ref.age = 20;                 // ✅ 修改 ref = 修改 p1
    cout << "p1.age = " << p1.age << endl;  // 20

    cout << "\n=== 测试 setAge() 链式调用 ===" << endl;
    p1.setAge(30).setAge(40).setAge(50);   // ★ 链式调用
    cout << "p1.age = " << p1.age << endl;  // 50

    cout << "\n=== 测试 copyPerson() ===" << endl;
    Person p2 = p1.copyPerson();   // ★ p2 是 p1 的副本
    p2.age = 100;                  // ✅ 只修改 p2
    cout << "p1.age = " << p1.age << endl;  // 50(不变)
    cout << "p2.age = " << p2.age << endl;  // 100

    return 0;
}

返回引用 Person&

调用 p1.setAge(30).setAge(40)

① setAge(30) 返回 p1 的引用
       ↓
② p1.setAge(40) 在同一个对象上继续调用
       ↓
③ 最终 p1.age = 40

内存中始终只有 p1 一个对象!

返回值 Person

调用 Person p2 = p1.copyPerson()

① copyPerson() 返回 p1 的副本
       ↓
② 这个副本被复制给 p2
       ↓
③ 内存中有两个独立对象:p1 和 p2

修改 p2 不影响 p1!

        "返回引用 Person& 返回的是对象本身,不产生拷贝,可以用于链式调用和修改原对象。返回值 Person 返回的是对象的副本,会产生拷贝,修改副本不影响原对象。返回引用更高效,但需要注意返回的引用必须指向生命周期足够长的对象(如 *this、成员变量、静态变量),不能返回局部变量的引用。"

总结

Person& 返回对象本身(引用,不拷贝),可用于链式调用和修改原对象;Person 返回对象的副本(拷贝),修改副本不影响原对象。返回引用更高效,但必须确保对象生命周期足够长。

4.2.5 深拷贝与浅拷贝

深浅拷贝是面试经典问题,也是常见的一个坑

浅拷贝:简单的赋值拷贝操作

深拷贝:在堆区重新申请空间,进行拷贝操作

**示例:**

class Person {
public:
    //无参(默认)构造函数
    Person() {
        cout << "无参构造函数!" << endl;
    }
    //有参构造函数
    Person(int age ,int height) {

        cout << "有参构造函数!" << endl;

        m_age = age;
        m_height = new int(height);

    }
    //拷贝构造函数  
    Person(const Person& p) {
        cout << "拷贝构造函数!" << endl;
        //如果不利用深拷贝在堆区创建新内存,会导致浅拷贝带来的重复释放堆区问题
        m_age = p.m_age;
        m_height = new int(*p.m_height);

    }

    //析构函数
    ~Person() {
        cout << "析构函数!" << endl;
        if (m_height != NULL)
        {
            delete m_height;
        }
    }
public:
    int m_age;
    int* m_height;
};

void test01()
{
    Person p1(18, 180);

    Person p2(p1);

    cout << "p1的年龄: " << p1.m_age << " 身高: " << *p1.m_height << endl;

    cout << "p2的年龄: " << p2.m_age << " 身高: " << *p2.m_height << endl;
}

int main() {

    test01();

    system("pause");

    return 0;
}
#include <iostream>     // 标准输入输出
#include <string>       // 字符串库
using namespace std;

// ========== 深拷贝与浅拷贝 ==========
// 浅拷贝:简单的赋值拷贝,只复制指针的值(地址),不复制指针指向的数据
// 深拷贝:复制指针指向的数据,在堆区重新开辟内存

class Person
{
public:
    // 默认构造函数
    Person()
    {
        cout << "Person的默认构造函数调用" << endl;
    }

    // 有参构造函数
    Person(int age, int height)
    {
        m_Age = age;
        // ⚠️ 在堆区开辟内存,存储 height 的值
        m_Height = new int(height);
        cout << "Person的有参构造函数调用" << endl;
    }

    // ===== 自己实现拷贝构造函数,解决浅拷贝带来的问题 =====
    Person(const Person& p)
    {
        cout << "Person的拷贝构造函数调用" << endl;

        // 浅拷贝(编译器默认实现):
        // m_Height = p.m_Height;  // ❌ 只复制指针地址,两个指针指向同一块内存

        // ✅ 深拷贝操作:重新在堆区开辟内存
        m_Age = p.m_Age;
        m_Height = new int(*p.m_Height);  // 复制指针指向的数据
    }

    // 析构函数
    ~Person()
    {    
        //析构代码,将堆区开辟的数据做释放操作
        if (m_Height != NULL) {
            delete m_Height;   // 释放堆区内存
            m_Height = NULL;   // 指针置空,防止野指针
        }
        cout << "Person的析构函数调用" << endl;
    }

    int m_Age;      // 年龄(栈区)
    int* m_Height;  // 身高指针(指向堆区)
};

void test01()
{
    Person p1(18, 160);     // 创建 p1,堆区分配内存
    cout << "p1的年龄为:" << p1.m_Age
        << " 身高为:" << *p1.m_Height << endl;  // ✅ 解引用输出值

    Person p2(p1);          // 拷贝构造 p2(深拷贝)
    cout << "p2的年龄为:" << p2.m_Age
        << " 身高为:" << *p2.m_Height << endl;  // ✅ 解引用输出值
}

int main() {
    test01();
    system("pause");
    return 0;
}

> 总结:如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题

4.2.6 初始化列表

**作用:**

C++提供了初始化列表语法,用来初始化属性

**语法:**`构造函数():属性1(值1),属性2(值2)... {}`

**示例:**

#include <iostream>     // 标准输入输出
#include <string>       // 字符串库
using namespace std;

class Person {
public:
    // ===== 初始化列表方式(推荐) =====
    // 语法:构造函数名(参数列表) : 成员1(值1), 成员2(值2), ...
    Person(int a, int b, int c) : m_A(a), m_B(b), m_C(c) 
    {
        // 构造函数体可以为空,初始化工作在初始化列表中完成
    }

    // ===== 传统赋值方式(不推荐) =====
    // Person(int a, int b, int c) {
    //     m_A = a;   // 这是赋值,不是初始化
    //     m_B = b;
    //     m_C = c;
    // }

    void PrintPerson() {
        cout << "mA:" << m_A << endl;
        cout << "mB:" << m_B << endl;
        cout << "mC:" << m_C << endl;
    }

private:
    int m_A;
    int m_B;
    int m_C;
};

int main() {
    Person p(1, 2, 3);   // 调用构造函数体,参数 1,2,3 分别初始化 m_A, m_B, m_C
    p.PrintPerson();

    system("pause");
    return 0;
}
#include <iostream>     // 1. 包含输入输出流头文件,提供 cout、endl 等工具
#include <string>       // 2. 包含字符串库(本代码虽未直接使用,但保留以备扩展)
using namespace std;    // 3. 使用标准命名空间 std

// ============================================================
// Person 类:演示初始化列表和传统赋值的区别
// ============================================================
class Person {
public:
    // ============================================================
    // ★ 方式1:初始化列表(推荐方式)
    // 语法:构造函数名(参数列表) : 成员1(值1), 成员2(值2), ...
    // 特点:在进入函数体之前,成员变量就已经完成初始化
    // ============================================================
    Person(int a, int b, int c) : m_A(a), m_B(b), m_C(c) 
    {
        // 4. 构造函数体可以为空,因为初始化工作已在初始化列表中完成
        //    这里可以写其他逻辑,比如日志输出、验证等
    }

    // ============================================================
    // ★ 方式2:传统赋值方式(不推荐,仅供对比)
    // 特点:先调用默认构造函数创建成员变量(已初始化),再赋值
    // 性能稍差,且对 const 成员和引用成员无效
    // ============================================================
    // Person(int a, int b, int c) {
    //     m_A = a;   // 这是赋值,不是初始化
    //     m_B = b;
    //     m_C = c;
    // }

    // 5. 打印三个成员变量的值
    void PrintPerson() {
        cout << "mA:" << m_A << endl;   // 6. 输出 m_A 的值
        cout << "mB:" << m_B << endl;   // 7. 输出 m_B 的值
        cout << "mC:" << m_C << endl;   // 8. 输出 m_C 的值
    }

private:
    int m_A;   // 9. 私有成员变量 A
    int m_B;   // 10. 私有成员变量 B
    int m_C;   // 11. 私有成员变量 C
};

// ============================================================
// 主函数
// ============================================================
int main() {
    // 12. 创建 Person 对象,传入 1, 2, 3,分别初始化 m_A, m_B, m_C
    Person p(1, 2, 3);
    
    // 13. 调用成员函数打印信息
    p.PrintPerson();

    system("pause");   // 14. 暂停控制台窗口
    return 0;          // 15. 程序正常退出
}

4.2.7 类对象作为类成员

C++类中的成员可以是另一个类的对象,我们称该成员为对象成员

例如:

class A {}
class B
{
    A a;
}

B类中有对象A作为成员,A为对象成员

那么当创建B对象时,A与B的构造和析构的顺序是谁先谁后?

**示例:

#include <iostream>     // 标准输入输出
#include <string>       // 字符串库
using namespace std;

// ========== Phone 类 ==========
class Phone
{
public:
    // 有参构造函数
    Phone(string name)
    {
        m_PhoneName = name;
        cout << "Phone构造" << endl;
    }

    // 析构函数
    ~Phone()
    {
        cout << "Phone析构" << endl;
    }

    string m_PhoneName;  // 手机品牌名
};

// ========== Person 类 ==========
class Person
{
public:
    // 初始化列表:告诉编译器调用哪一个构造函数
    // m_Name(name)        → 用 name 初始化 m_Name(string 类型)
    // m_Phone(pName)      → 用 pName 初始化 m_Phone(Phone 类型,调用 Phone 的有参构造)
    Person(string name, string pName) : m_Name(name), m_Phone(pName)
    {
        cout << "Person构造" << endl;
    }

    // 析构函数
    ~Person()
    {
        cout << "Person析构" << endl;
    }

    // 玩游戏的方法
    void playGame()
    {
        cout << m_Name << " 使用" << m_Phone.m_PhoneName << " 牌手机! " << endl;
    }

    string m_Name;     // 姓名(普通成员)
    Phone m_Phone;     // 手机(对象成员)
};

void test01()
{
    // 当类中成员是其他类对象时,我们称该成员为"对象成员"
    // 构造的顺序是:先调用对象成员的构造,再调用本类构造
    // 析构顺序与构造相反
    Person p("张三", "苹果X");
    p.playGame();
}

int main() {
    test01();
    system("pause");
    return 0;
}

4.2.8 静态成员

静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员

静态成员分为:

* 静态成员变量
  * 所有对象共享同一份数据
  * 在编译阶段分配内存
  * 类内声明,类外初始化
* 静态成员函数
  * 所有对象共享同一个函数
  * 静态成员函数只能访问静态成员变量

**示例1 :**静态成员变量

#include <iostream>      // ① 包含输入输出流头文件
using namespace std;     // ② 使用标准命名空间

// ============================================================
// ★ Person 类
// ============================================================
class Person
{
public:
    // ③ ★ 静态成员变量 m_A
    //    特点1:在编译阶段分配内存(全局/静态区)
    //    特点2:类内声明,必须在类外初始化
    //    特点3:所有对象共享同一份数据
    static int m_A;

private:
    // ④ ★ 静态成员变量 m_B(私有)
    //    同样是静态成员变量,但访问权限是 private
    //    只能在类内部访问,类外无法访问
    static int m_B;
};

// ⑤ ★ 静态成员变量类外初始化(必须!)
//    语法:类型 类名::变量名 = 初始值;
int Person::m_A = 10;
int Person::m_B = 10;

// ============================================================
// ⑥ 测试函数
// ============================================================
void test01()
{
    // ============================================================
    // ★ 访问方式1:通过对象访问
    // ============================================================

    Person p1;           // ⑦ 创建对象 p1
    p1.m_A = 100;        // ⑧ 通过 p1 修改静态成员变量 m_A
    cout << "p1.m_A = " << p1.m_A << endl;   // 输出 100

    Person p2;           // ⑨ 创建对象 p2
    p2.m_A = 200;        // ⑩ 通过 p2 修改静态成员变量 m_A
    cout << "p1.m_A = " << p1.m_A << endl;   // ⑪ 输出 200(共享数据!)
    cout << "p2.m_A = " << p2.m_A << endl;   // ⑫ 输出 200

    // ============================================================
    // ★ 访问方式2:通过类名访问(推荐)
    // ============================================================

    // ⑬ ★ 通过 类名::变量名 直接访问
    cout << "m_A = " << Person::m_A << endl;   // 输出 200

    // ⑭ ❌ 私有静态成员变量无法在类外访问
    // cout << "m_B = " << Person::m_B << endl; // 编译错误!
}

// ============================================================
// ⑮ 主函数
// ============================================================
int main() {
    test01();            // ⑯ 调用测试函数
    system("pause");
    return 0;
}

1. 静态成员变量的特点

特点 说明
存储位置 全局/静态区,不存储在对象中
生命周期 程序启动时分配,程序结束时释放
共享性 所有对象共享同一份数据
初始化 必须在类外初始化(类内只是声明)
访问方式 可通过对象访问,也可通过类名访问
访问权限 受 public/private/protected 限制

2. 静态成员变量的内存布局

┌─────────────────────────────────────────────────────────────────┐
│  全局/静态区                                                   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  Person::m_A = 200  ← 所有对象共享!                   │    │
│  │  Person::m_B = 10   ← 私有,类外不能访问              │    │
│  └─────────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────────┘
                    ↑                    ↑
                    │                    │
┌───────────────────┼────────────────────┼───────────────────┐
│                   │                    │                   │
│  Person 对象 p1   │   Person 对象 p2   │                   │
│  (无 m_A 和 m_B)│  (无 m_A 和 m_B)│                   │
│  ★ 静态成员不占   │  ★ 静态成员不占   │                   │
│     对象空间       │     对象空间       │                   │
└───────────────────┴────────────────────┴───────────────────┘

静态成员 vs 非静态成员

对比项 静态成员变量 非静态成员变量
存储位置 全局/静态区 对象内存中
属于谁 属于类(所有对象共享) 属于每个对象(独立)
初始化 必须在类外初始化 构造函数初始化列表
访问方式 类名::变量名 或 对象.变量名 只能通过对象访问
对象大小 不占用对象空间 占用对象空间
生命周期 程序启动到结束 对象创建到销毁

为什么 p1.m_A 和 p2.m_A 都是 200?

p1.m_A = 100;   // 修改共享数据 → m_A = 100
p2.m_A = 200;   // 修改共享数据 → m_A = 200
// p1.m_A 和 p2.m_A 访问的是同一块内存,所以都是 200

为什么必须在类外初始化?

// 类内只是声明
static int m_A;

// ★ 必须类外定义(分配内存)
int Person::m_A = 10;

因为静态成员变量不属于任何对象,不能在构造函数中初始化,必须在类外单独分配内存。

总结

静态成员变量属于类而非对象,所有对象共享一份数据,在编译期分配内存,必须在类外初始化。可通过对象或类名访问,受访问权限控制。 

静态成员变量确实存储在全局/静态区,但 private 限制的是“访问权限”,而不是“存储位置”。

这是两个完全不同的概念:

概念 作用 影响
存储位置 决定变量在内存中的哪里 全局/静态区
访问权限 决定谁能访问这个变量 private 限制类外访问

总结

问题 答案
静态成员变量在全局区吗? ✅ 是,存储在全局/静态区
private 影响存储位置吗? ❌ 不影响,只控制访问权限
为什么类外能初始化 private 静态成员? 因为那是定义(分配内存),不是访问
private 限制了什么? 限制类外代码(如 main 函数、其他类)的访问

        静态成员变量在全局区存储,但 private 是编译期的访问控制。存储位置决定“在哪里”,访问权限决定“谁能用”,两者互不干扰。

**示例2:**静态成员函数

class Person
{

public:

    //静态成员函数特点:
    //1 程序共享一个函数
    //2 静态成员函数只能访问静态成员变量

    static void func()
    {
        cout << "func调用" << endl;
        m_A = 100;
        //m_B = 100; //错误,不可以访问非静态成员变量
    }

    static int m_A; //静态成员变量
    int m_B; // 
private:

    //静态成员函数也是有访问权限的
    static void func2()
    {
        cout << "func2调用" << endl;
    }
};
int Person::m_A = 10;


void test01()
{
    //静态成员变量两种访问方式

    //1、通过对象
    Person p1;
    p1.func();

    //2、通过类名
    Person::func();


    //Person::func2(); //私有权限访问不到
}

int main() {

    test01();

    system("pause");

    return 0;
}

4.3 C++对象模型和this指针

4.3.1 成员变量和成员函数分开存储

在C++中,类内的成员变量和成员函数分开存储

空对象占用内存空间为:1
原因:C++ 编译器会给每个空对象分配 1 个字节空间
是为了让每个空对象都有一个独一无二的内存地址

#include <iostream>     // ① 包含输入输出流头文件,提供 cout、endl 等工具
#include <string>       // ② 包含字符串头文件(本代码未直接使用,但保留)
using namespace std;    // ③ 使用标准命名空间 std,简化代码书写

// ============================================================
// ④ ★ 定义一个空类 Person
//     类中没有任何成员变量和成员函数(空的)
// ============================================================
class Person
{

};   // ⑤ 注意:类定义以分号结尾

// ============================================================
// ⑥ 测试函数
// ============================================================
void test01()
{
    Person p;   // ⑦ 创建 Person 对象(空对象)

    // ⑧ ★ 输出空对象占用的内存大小
    //     输出结果为 1
    //     原因:C++ 编译器会给每个空对象分配 1 个字节空间
    //     是为了让每个空对象都有一个独一无二的内存地址
    cout << "size of p = " << sizeof(p) << endl;
}

// ============================================================
// ⑨ 主函数(程序入口)
// ============================================================
int main() {
    test01();                // ⑩ 调用测试函数

    system("pause");         // ⑪ 暂停控制台窗口
    return 0;                // ⑫ 程序正常退出
}
#include <iostream>     // ① 包含输入输出流头文件
#include <string>       // ② 包含字符串头文件
using namespace std;    // ③ 使用标准命名空间

// ============================================================
// ★ 成员变量和成员函数是分开存储的
// ============================================================
class Person
{
    // ============================================================
    // ④ int m_A —— 非静态成员变量
    //    特点:属于类的每个对象,每个对象都有一份独立的拷贝
    //    存储位置:在对象的内存空间中
    //    大小:占 4 字节(int 类型)
    // ============================================================
    int m_A;

    // ============================================================
    // ⑤ static int m_B —— 静态成员变量
    //    特点:不属于任何对象,所有对象共享同一份数据
    //    存储位置:在全局/静态区
    //    大小:不占用对象的内存空间
    // ============================================================
    static int m_B;

    // ============================================================
    // ⑥ void func() —— 非静态成员函数
    //    特点:不属于类的对象,所有对象共享同一份函数代码
    //    存储位置:在代码段(只读区域)
    //    大小:不占用对象的内存空间
    // ============================================================
    void func() {}

    // ============================================================
    // ⑦ static void func2() —— 静态成员函数
    //    特点:不属于任何对象,所有对象共享同一份函数代码
    //    存储位置:在代码段(只读区域)
    //    大小:不占用对象的内存空间
    // ============================================================
    static void func2() {}
};

// ⑧ ★ 静态成员变量:类内声明,类外初始化(必须)
int Person::m_B = 0;

// ============================================================
// ⑪ test:演示有成员变量的对象的大小
// ============================================================
void test()
{
    Person p;
    // ⑫ ★ 输出对象大小:4(int m_A 占 4 字节)
    //     静态成员和成员函数都不占用对象空间
    cout << "size of p = " << sizeof(p) << endl;  // 输出 4
}

int main() {
    test();   // ⑬ 调用 test,输出:size of p = 4
    system("pause");
    return 0;
}

只有非静态成员变量才属于类的对象上

#include <iostream>     // ① 包含输入输出流头文件,提供 cout、endl 等工具
using namespace std;    // ② 使用标准命名空间 std

// ============================================================
// ★ Person 类
// ============================================================
class Person {
public:
    // ③ 构造函数(无参构造)
    Person() {
        mA = 0;          // ④ 初始化成员变量 mA
    }

    // ============================================================
    // ⑤ ★ 非静态成员变量 —— 占对象空间
    //    每个 Person 对象都有自己独立的 mA
    //    类型:int → 占 4 字节
    // ============================================================
    int mA;

    // ============================================================
    // ⑥ ★ 静态成员变量 —— 不占对象空间
    //    所有 Person 对象共享同一个 mB
    //    存储在全局/静态区,不属于任何对象
    // ============================================================
    static int mB;

    // ============================================================
    // ⑦ ★ 非静态成员函数 —— 不占对象空间
    //    所有 Person 对象共享同一份函数代码
    //    存储在代码段,不属于任何对象
    //    函数内通过 this 指针区分调用者
    // ============================================================
    void func() {
        cout << "mA:" << this->mA << endl;   // ⑧ 使用 this 指针访问当前对象的 mA
    }

    // ============================================================
    // ⑨ ★ 静态成员函数 —— 不占对象空间
    //    所有 Person 对象共享同一份函数代码
    //    存储在代码段,不属于任何对象
    //    静态函数没有 this 指针,不能访问非静态成员
    // ============================================================
    static void sfunc() {
    }
};

// ⑩ ★ 静态成员变量:类内声明,类外初始化(必须)
int Person::mB = 0;

// ============================================================
// ⑪ 主函数(程序入口)
// ============================================================
int main() {
    // ⑫ ★ 输出 Person 类对象的大小
    //     计算结果:只有 int mA 占 4 字节
    //     静态成员 mB、函数 func()、静态函数 sfunc() 都不算
    //     所以 sizeof(Person) = 4
    cout << sizeof(Person) << endl;

    system("pause");   // ⑬ 暂停控制台窗口
    return 0;          // ⑭ 程序正常退出
}

4.3.2 this指针概念

通过4.3.1我们知道在C++中成员变量和成员函数是分开存储的

每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码

那么问题是:这一块代码是如何区分哪个对象调用自己的呢?

c++通过提供特殊的对象指针,this指针,解决上述问题。this指针指向被调用的成员函数所属的对象

this指针是隐含每一个非静态成员函数内的一种指针

this指针不需要定义,直接使用即可

this指针的用途:

* 当形参和成员变量同名时,可用this指针来区分
* 在类的非静态成员函数中返回对象本身,可使用return *this

#include <iostream>     // 标准输入输出
#include <string>       // 字符串库
using namespace std;

class Person
{
public:
    Person(int age)
    {
        this->age = age;
    }

    // ✅ 方法1:参数用值传递(会拷贝)
    Person& PersonAddPersonByValue(Person p)
    {
        this->age += p.age;
        cout << "值传递版本,age = " << age << endl;
        return *this;
    }

    // ✅ 方法2:参数用引用传递(推荐,不拷贝)
    Person& PersonAddPersonByRef(Person& p)
    {
        this->age += p.age;
        cout << "引用传递版本,age = " << age << endl;
        return *this;
    }

    // ✅ 方法3:返回副本(不支持链式编程)
    Person PersonAddPersonCopy(Person p)
    {
        this->age += p.age;
        cout << "返回副本版本,age = " << age << endl;
        return *this;   // 返回当前对象的副本
    }

    int age;
};

void test01()
{
    Person p1(10);
    Person p2(10);

    // ===== 测试1:值传递 + 返回引用 =====
    cout << "=== 值传递 + 返回引用 ===" << endl;
    p2.PersonAddPersonByValue(p1);
    // 可以链式调用
    p2.PersonAddPersonByValue(p1).PersonAddPersonByValue(p1);
    cout << "p2.age = " << p2.age << endl;

    // ===== 测试2:引用传递 + 返回引用(推荐) =====
    cout << "\n=== 引用传递 + 返回引用 ===" << endl;
    Person p3(10), p4(10);
    p4.PersonAddPersonByRef(p3).PersonAddPersonByRef(p3).PersonAddPersonByRef(p3);
    cout << "p4.age = " << p4.age << endl;

    // ===== 测试3:返回副本 =====
    cout << "\n=== 返回副本(不支持链式) ===" << endl;
    Person p5(10), p6(10);
    Person result = p6.PersonAddPersonCopy(p5);  // result 是副本
    // result.PersonAddPersonCopy(p5);  // ❌ 修改的是副本,不是 p6
    cout << "p6.age = " << p6.age << endl;
}

int main() {
    test01();
    system("pause");
    return 0;
}

四种组合对比表

编号 参数传递 返回方式 函数签名 是否拷贝参数 是否拷贝返回值 能否修改原对象 链式调用
值传递 返回引用 Person& func(Person p) ✅ 拷贝 ✅ 可以(操作的是 this ✅ 支持
引用传递 返回引用 Person& func(Person& p) ✅ 可以 ✅ 支持
值传递 返回值 Person func(Person p) ✅ 拷贝 ✅ 拷贝 ❌ 不能(操作的是副本) ❌ 不支持
引用传递 返回值 Person func(Person& p) ✅ 拷贝 ✅ 可以 ❌ 不支持

详细分析

组合①:值传递 + 返回引用 Person& func(Person p)

Person& PersonAddPersonByValue(Person p) {
    this->age += p.age;   // p 是副本,this->age 是当前对象
    return *this;         // 返回当前对象的引用
}
分析点 说明
参数 p 从实参拷贝一份,修改 p 不影响原对象
操作对象 this->age 是当前对象,所以当前对象被修改
返回 返回当前对象的引用,支持链式调用
效率 ⚠️ 有拷贝开销(参数拷贝)
p2.PersonAddPersonByValue(p1);   // p1 被拷贝到 p,p2 被修改
p2.PersonAddPersonByValue(p1).PersonAddPersonByValue(p1);  // ✅ 链式

组合②:引用传递 + 返回引用 Person& func(Person& p) ✅ 推荐

Person& PersonAddPersonByRef(Person& p) {
    this->age += p.age;   // p 是引用,直接操作原对象
    return *this;         // 返回当前对象的引用
}
分析点 说明
参数 p 传入引用,不拷贝
操作对象 this->age 是当前对象,当前对象被修改
返回 返回当前对象的引用,支持链式调用
效率 ✅ 最高(无任何拷贝)
p4.PersonAddPersonByRef(p3).PersonAddPersonByRef(p3).PersonAddPersonByRef(p3);
// ① 第一次:p4.age = 20
// ② 第二次:p4.age = 30(因为返回的是 p4 本身)
// ③ 第三次:p4.age = 40

组合③:值传递 + 返回值 Person func(Person p)

Person PersonAddPersonCopy(Person p) {
    this->age += p.age;
    return *this;   // ★ 返回当前对象的副本
}
分析点 说明
参数 p 从实参拷贝一份
操作对象 this->age 是当前对象,当前对象被修改
返回 返回当前对象的副本不支持链式调用(链式操作的是副本)
效率 ⚠️ 有拷贝开销(参数 + 返回值)
Person result = p6.PersonAddPersonCopy(p5);   // p6 被修改,result 是副本
result.PersonAddPersonCopy(p5);   // ❌ 操作的是 result,不是 p6
cout << p6.age << endl;   // 还是 20(原对象没变)

组合④:引用传递 + 返回值 Person func(Person& p)

Person func(Person& p) {
    this->age += p.age;
    return *this;   // 返回当前对象的副本
}
分析点 说明
参数 p 传入引用,不拷贝
操作对象 this->age 是当前对象,当前对象被修改
返回 返回当前对象的副本不支持链式调用
效率 ⚠️ 有返回值拷贝开销(参数无拷贝)

总结

组合 参数拷贝 返回值拷贝 效率 修改原对象 链式调用
① 值传递 + 返回引用 ✅ 有 ❌ 无 ⭐⭐⭐
② 引用传递 + 返回引用 ❌ 无 ❌ 无 ⭐⭐⭐⭐⭐
③ 值传递 + 返回值 ✅ 有 ✅ 有
④ 引用传递 + 返回值 ❌ 无 ✅ 有 ⭐⭐⭐

选择建议

场景 推荐组合 原因
需要修改并返回当前对象 ② 引用传参 + 返回引用 ✅ 最高效,支持链式
需要保护原对象不被修改 ③ 值传参 + 返回值 操作的是副本,原对象不变
只读访问,不需要修改 const T& 传参 避免拷贝,保护数据
赋值运算符 Person& operator=(const Person&) 标准写法,必须返回引用

总结

参数加引用 = 避免拷贝(高效),不加引用 = 拷贝参数(低效)。返回值加引用 = 返回对象本身(支持链式),不加引用 = 返回副本(不支持链式)。组合② Person& func(Person& p) 是最优写法。

4.3.3 空指针访问成员函数

C++中空指针也是可以调用成员函数的,但是也要注意有没有用到this指针

如果用到this指针,需要加以判断保证代码的健壮性

**示例:

#include <iostream>     // 标准输入输出
#include <string>       // 字符串库
using namespace std;

//空指针调用成员函数
class Person {
public:

    void ShowClassName() {
        cout << "我是Person类!" << endl;
    }

    void ShowPersonAge() {
        //报错原因是因为传入的指针是为空NULL
        cout << this->mAge << endl;
    }

    void ShowPerson() {
        if (this == NULL) {
            return;
        }
        cout << mAge << endl;
    }

public:
    int mAge;
};

void test01()
{
    Person* p = NULL;
    p->ShowClassName(); //空指针,可以调用成员函数
    p->ShowPerson();  //但是如果成员函数中用到了this指针,就不可以了
}

int main() {

    test01();

    system("pause");

    return 0;
}
#include <iostream>     // 标准输入输出
#include <string>       // 字符串库
using namespace std;

// ========== 空指针调用成员函数 ==========
class Person {
public:
    // ===== 成员函数1:不访问成员变量 =====
    void ShowClassName() {
        cout << "我是Person类!" << endl;
        // ⚠️ 没有使用 this 指针,没有访问成员变量
        // 即使 this 为 NULL,也可以正常运行
    }

    // ===== 成员函数2:访问成员变量(不安全版本) =====
    void ShowPersonAge() {
        // ❌ 报错原因:this 指针为 NULL
        // 尝试通过 this->mAge 访问成员变量
        // 相当于访问 NULL->mAge,非法内存访问
        cout << this->mAge << endl;
    }

    // ===== 成员函数3:访问成员变量(安全版本) =====
    void ShowPerson() {
        // ✅ 先判断 this 是否为空
        if (this == NULL) {
            return;  // 如果为空,直接返回,不执行后续代码
        }
        // this 不为空,可以安全访问成员变量
        cout << mAge << endl;  // 等价于 this->mAge
    }

public:
    int mAge;  // 成员变量
};

void test01()
{
    Person* p = NULL;  // 空指针

    // ✅ 空指针可以调用成员函数(只要不访问成员变量)
    p->ShowClassName();  // 输出:我是Person类!

    // ✅ 安全版本:有判空保护,不会崩溃
    p->ShowPerson();     // 不输出任何内容(因为 this == NULL,直接返回)

    // ❌ 不安全版本:会崩溃
    // p->ShowPersonAge();  // 如果调用,程序会崩溃
}

int main() {
    test01();
    system("pause");
    return 0;
}

4.3.4 const修饰成员函数

**常函数:**

* 成员函数后加const后我们称为这个函数为常函数
* 常函数内不可以修改成员属性
* 成员属性声明时加关键字mutable后,在常函数中依然可以修改

**常对象:**

* 声明对象前加const称该对象为常对象
* 常对象只能调用常函数

**示例:**

#include <iostream>     // 标准输入输出
#include <string>       // 字符串库
using namespace std;

// ========== 常函数 ==========
class Person {
public:
    // 构造函数
    Person() {
        m_A = 0;
        m_B = 0;
    }

    // ===== 常函数:在成员函数后面加 const =====
    // this 指针的本质是一个指针常量(Person* const this),指针的指向不可修改
    // 加 const 后,变成 const Person* const this,指向的内容也不能修改
    void ShowPerson() const {
        // ❌ this = NULL;     // 错误:不能修改指针的指向(this 是 const 指针)
        // ❌ this->m_A = 100; // 错误:不能修改成员变量(const 修饰了 this 指向的内容)
        
        // ✅ 但是 mutable 修饰的变量可以修改
        this->m_B = 100;  // m_B 是 mutable,即使在常函数中也可以修改
    }

    // 另一个常函数示例
    void MyFunc() const {
        // ❌ m_A = 10000;   // 错误:不能修改普通成员变量
        // ✅ 可以读取
        cout << m_A << endl;
    }

public:
    int m_A;              // 普通成员变量
    mutable int m_B;      // mutable 成员变量:在常函数中也可以修改
};

// ========== 常对象 ==========
void test01() {
    // const 修饰对象 → 常对象
    const Person person;   // 常量对象

    // ✅ 可以读取成员变量
    cout << person.m_A << endl;

    // ❌ person.m_A = 100;  // 错误:常对象不能修改普通成员变量

    // ✅ 可以修改 mutable 成员变量
    person.m_B = 100;      // 常对象可以修改 mutable 成员

    // ❌ 常对象不能调用普通成员函数(会修改对象)
    // person.ShowPerson();   // 错误!
    
    // ✅ 常对象可以调用常函数(保证不修改对象)
    person.MyFunc();       // 常函数,可以调用
}

int main() {
    test01();
    system("pause");
    return 0;
}

4.4 友元

生活中你的家有客厅(Public),有你的卧室(Private)

客厅所有来的客人都可以进去,但是你的卧室是私有的,也就是说只有你能进去

但是呢,你也可以允许你的好闺蜜好基友进去。

在程序里,有些私有属性也想让类外特殊的一些函数或者类进行访问,就需要用到友元的技术

友元的目的就是让一个函数或者类访问另一个类中私有成员

友元的关键字为 friend

友元的三种实现

* 全局函数做友元
* 类做友元
* 成员函数做友元

4.4.1 全局函数做友元

#include <iostream>     // ① 包含输入输出流头文件,提供 cout、endl 等工具
#include <string>       // ② 包含字符串头文件,提供 string 类型支持
using namespace std;    // ③ 使用标准命名空间 std,简化代码书写

// ============================================================
// ★ Building 类(建筑类)
// ============================================================
class Building
{
    // ============================================================
    // ④ ★ 友元声明(核心!)
    //     告诉编译器:goodGay 这个全局函数是 Building 类的好朋友
    //     它可以访问 Building 类中的所有成员(包括 private 和 protected)
    //     即使 goodGay 不是类的成员函数,也能访问私有成员
    // ============================================================
    friend void goodGay(Building* building);

public:
    // ⑤ 构造函数:初始化成员变量
    Building()
    {
        this->m_SittingRoom = "客厅";   // ⑥ 初始化公共成员
        this->m_BedRoom = "卧室";       // ⑦ 初始化私有成员
    }

public:
    string m_SittingRoom;   // ⑧ 公共成员 → 谁都可以访问

private:
    string m_BedRoom;       // ⑨ ★ 私有成员 → 只有类内部和友元可以访问
};

// ============================================================
// ★ 全局函数 goodGay(好基友)
// ============================================================
// ⑩ ★ 注意:这个函数不是 Building 的成员函数,它只是一个普通全局函数
//     但因为在 Building 类中被声明为友元,它拥有“特权”
//     可以访问 Building 类的私有成员
// ============================================================
void goodGay(Building* building)
{
    // ⑪ ✅ 访问公共成员(任何人都可以)
    cout << "好基友正在访问: " << building->m_SittingRoom << endl;

    // ⑫ ✅ 访问私有成员(因为是友元,所以可以!)
    //     如果没有 friend 声明,这行代码会报编译错误
    cout << "好基友正在访问: " << building->m_BedRoom << endl;
}

// ============================================================
// ⑬ 测试函数
// ============================================================
void test01()
{
    Building b;          // ⑭ 创建 Building 对象
    goodGay(&b);         // ⑮ 调用友元函数,传入对象的地址
}

// ============================================================
// ⑯ 主函数(程序入口)
// ============================================================
int main()
{
    test01();            // ⑰ 运行测试
    system("pause");     // ⑱ 暂停控制台窗口
    return 0;            // ⑲ 程序正常退出
}

4.4.2 类做友元

类做友元的目的是让一个类可以访问另一个类中的私有成员

类做友元的目的是什么?  在不破坏封装的前提下,允许特定类访问私有成员

friend class goodGay; 让 goodGay 类的所有成员函数都能访问 Building 类的私有成员,两个类通过指针 Building* building 建立关联,形成紧密协作的关系

#include <iostream>     // 标准输入输出
#include <string>       // 字符串库
using namespace std;

// ========== 前置声明 ==========
// 告诉编译器:Building 是一个类,稍后定义
// 因为 goodGay 类中有一个 Building* 指针
//原因: goodGay 类中有一个 Building* 指针,编译器需要知道 Building 是一个类型名。
//在 goodGay 定义时,Building 的完整定义还没有出现
//所以需要提前告诉编译器"Building 是一个类"。
class Building;

// ========== goodGay 类 ==========
class goodGay
{
public:
    goodGay();          // 构造函数声明
    void visit();       // 成员函数声明

private:
    Building* building; // 指向 Building 对象的指针
};

// ========== Building 类 ==========
class Building
{
    // ⭐ 友元类声明(核心!)
    // 告诉编译器:goodGay 类是 Building 类的好朋友
    // 所以 goodGay 类中的所有成员函数都可以访问 Building 的私有成员
    friend class goodGay;

public:
    Building();         // 构造函数声明

public:
    string m_SittingRoom;   // 客厅(公共)

private:
    string m_BedRoom;       // 卧室(私有)
};

// ========== Building 构造函数的实现 ==========
//类外写成员函数
Building::Building()
{
    this->m_SittingRoom = "客厅";
    this->m_BedRoom = "卧室";
}

// ========== goodGay 构造函数的实现 ==========
goodGay::goodGay()
{
    // 在堆区创建 Building 对象
    // building 指针指向这个对象
    building = new Building;
}

// ========== goodGay::visit 成员函数的实现 ==========
void goodGay::visit()
{
    // ✅ 可以访问 Building 的公共成员
    cout << "好基友正在访问" << building->m_SittingRoom << endl;
    
    // ✅ 可以访问 Building 的私有成员(因为是友元类)
    cout << "好基友正在访问" << building->m_BedRoom << endl;
}

// ========== 测试函数 ==========
void test01()
{
    goodGay gg;     // 创建 goodGay 对象
    gg.visit();     // 调用 visit 成员函数
}

int main()
{
    test01();
    system("pause");
    return 0;
}

4.4.3 成员函数做友元

#include <iostream>     // 标准输入输出
#include <string>       // 字符串库
using namespace std;

// ========== 前置声明 ==========
// 因为 GoodGay 类中有 Building* 指针,需要知道 Building 是一个类
class Building;

// ========== GoodGay 类 ==========
class GoodGay
{
public:
    GoodGay();              // 构造函数声明
    void visit();           // 声明为友元的成员函数
    void visit2();          // 普通成员函数(不是友元)

private:
    Building* building;     // 指向 Building 对象的指针
};

// ========== Building 类 ==========
class Building
{
public:
    Building();             // 构造函数声明
    
    // ⭐ 关键点:只让 GoodGay 的 visit 成员函数成为友元
    // 不是整个 GoodGay 类,只是 visit 这一个函数!
    //告诉编译器  goodGay类中的visit成员函数 是Building好朋友,可以访问私有内容
    friend void GoodGay::visit();

public:
    string m_SittingRoom;   // 客厅(公共)

private:
    string m_BedRoom;       // 卧室(私有)
};

// ========== Building 构造函数实现 ==========
Building::Building()
{
    m_SittingRoom = "客厅";
    m_BedRoom = "卧室";
}

// ========== GoodGay 构造函数实现 ==========
GoodGay::GoodGay()
{
    // 在堆区创建 Building 对象
    building = new Building;
}

// ========== visit(友元成员函数) ==========
void GoodGay::visit()
{
    // ✅ 可以访问公共成员
    cout << "visit正在访问:" << building->m_SittingRoom << endl;
    
    // ✅ 可以访问私有成员(因为 visit 是 Building 的友元!)
    cout << "visit正在访问:" << building->m_BedRoom << endl;
}

// ========== visit2(普通成员函数) ==========
void GoodGay::visit2()
//GoodGay::visit2() 中的 GoodGay:: 是作用域解析运算符(Scope Resolution Operator)
//它的作用是指明 visit2 这个函数是属于 GoodGay 这个类的成员函数。


{
    // ✅ 可以访问公共成员
    cout << "visit2正在访问:" << building->m_SittingRoom << endl;
    
    // ❌ 不能访问私有成员(visit2 不是友元)
    // cout << building->m_BedRoom;  // 编译错误!
}

// ========== 测试函数 ==========
void test01()
{
    GoodGay gg;
    gg.visit();     // visit 有权限访问私有成员
    // gg.visit2(); // visit2 没有权限访问私有成员
}

int main()
{
    test01();
    system("pause");
    return 0;
}

4.5 运算符重载

运算符重载概念:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型

4.5.1 加号运算符重载

作用:实现两个自定义数据类型相加的运算

#include <iostream>     // ① 包含输入输出流头文件,提供 cout、endl 等工具
using namespace std;    // ② 使用标准命名空间 std

// ============================================================
// ★ Person 类
// ============================================================
class Person {
public:
    // ③ 默认构造函数(空实现)
    Person() {}

    // ④ 有参构造函数(使用 this 指针区分成员变量和参数)
    Person(int a, int b)
    {
        this->m_A = a;   // ⑤ 用参数 a 初始化成员变量 m_A
        this->m_B = b;   // ⑥ 用参数 b 初始化成员变量 m_B
    }

    // ============================================================
    // ⑦ ★ 方式1:成员函数实现 + 号运算符重载
    //     调用方式:p1 + p2 → p1.operator+(p2)
    //     返回值:新的 Person 对象(值返回)
    // ============================================================
    Person operator+(const Person& p) {
        Person temp;                    // ⑧ 创建临时对象
        temp.m_A = this->m_A + p.m_A;   // ⑨ 当前对象的 m_A + 传入对象的 m_A
        temp.m_B = this->m_B + p.m_B;   // ⑩ 当前对象的 m_B + 传入对象的 m_B
        return temp;                    // ⑪ 返回临时对象
    }

public:
    int m_A;   // ⑫ 成员变量 A
    int m_B;   // ⑬ 成员变量 B
};

// ============================================================
// ⑭ ★ 方式2:全局函数重载 + 号:Person + int
//     调用方式:p + 10 → operator+(p, 10)
//     为什么需要全局函数?因为 int 是内置类型,不能添加成员函数
// ============================================================
Person operator+(const Person& p2, int val)
{
    Person temp;                     // ⑮ 创建临时对象
    temp.m_A = p2.m_A + val;         // ⑯ p2 的 m_A + val → 存到 temp.m_A
    temp.m_B = p2.m_B + val;         // ⑰ p2 的 m_B + val → 存到 temp.m_B
    return temp;                     // ⑱ 返回临时对象
}

// ============================================================
// ⑲ 测试函数
// ============================================================
void test() {
    Person p1(10, 10);   // ⑳ 创建 p1:m_A=10, m_B=10
    Person p2(20, 20);   // ㉑ 创建 p2:m_A=20, m_B=20

    // ============================================================
    // ㉒ ★ 测试1:Person + Person(成员函数)
    //     p2 + p1 等价于 p2.operator+(p1)
    //     计算:m_A = 20 + 10 = 30, m_B = 20 + 10 = 30
    // ============================================================
    Person p3 = p2 + p1;
    cout << "mA:" << p3.m_A << " mB:" << p3.m_B << endl;  // ㉓ 输出:mA:30 mB:30

    // ============================================================
    // ㉔ ★ 测试2:Person + int(全局函数)
    //     p3 + 10 等价于 operator+(p3, 10)
    //     计算:m_A = 30 + 10 = 40, m_B = 30 + 10 = 40
    // ============================================================
    Person p4 = p3 + 10;
    cout << "mA:" << p4.m_A << " mB:" << p4.m_B << endl;  // ㉕ 输出:mA:40 mB:40
}

// ============================================================
// ㉖ 主函数(程序入口)
// ============================================================
int main() {
    test();                  // ㉗ 调用测试函数
    system("pause");         // ㉘ 暂停控制台窗口
    return 0;                // ㉙ 程序正常退出
}
#include <iostream>     // ① 包含输入输出流头文件
using namespace std;    // ② 使用标准命名空间

// ============================================================
// ★ Person 类
// ============================================================
class Person
{
public:
    // ③ 无参构造函数
    Person() {}

    // ④ 有参构造函数(使用初始化列表)
    Person(int a, int b) : m_A(a), m_B(b) {}

    // ============================================================
    // ⑤ ★ 成员函数重载 + 号:Person + Person
    //    返回新的 Person 对象,不修改当前对象
    // ============================================================
    Person operator+(const Person& p)
    {
        Person temp;   // ⑥ 创建临时对象
        temp.m_A = this->m_A + p.m_A;   // ⑦ 当前对象的 m_A + 传入对象的 m_A
        temp.m_B = this->m_B + p.m_B;   // ⑧ 当前对象的 m_B + 传入对象的 m_B
        return temp;   // ⑨ 返回新对象
    }

    // ============================================================
    // ⑩ ★ 成员函数重载 + 号:Person + int
    //    支持 Person p + 5 这种写法
    // ============================================================
    Person operator+(int val)
    {
        Person temp;
        temp.m_A = this->m_A + val;   // ⑪ 当前对象的 m_A + 整数
        temp.m_B = this->m_B + val;   // ⑫ 当前对象的 m_B + 整数
        return temp;
    }

    // ⑬ 打印成员变量
    void show() {
        cout << "mA:" << m_A << " mB:" << m_B << endl;
    }

private:
    int m_A;   // ⑭ 私有成员变量 A
    int m_B;   // ⑮ 私有成员变量 B
};

// ============================================================
// ⑯ ★ 全局函数重载 + 号:int + Person
//    支持 5 + Person 这种写法
//    注意:成员函数无法实现这种写法(因为 int 类型没有重载 + 的能力)
// ============================================================
Person operator+(int val, const Person& p)
{
    Person temp;
    temp.m_A = val + p.m_A;   // ⑰ 整数 + 对象的 m_A
    temp.m_B = val + p.m_B;   // ⑱ 整数 + 对象的 m_B
    return temp;
}

// ============================================================
// ⑲ 主函数
// ============================================================
int main()
{
    Person p1(10, 10);   // ⑳ 创建 p1:mA=10, mB=10
    Person p2(20, 20);   // ㉑ 创建 p2:mA=20, mB=20
    Person p3(30, 30);   // ㉒ 创建 p3:mA=30, mB=30

    // ============================================================
    // ★ 链式相加:Person + Person + Person
    // ============================================================
    Person p4 = p1 + p2 + p3;
    // 等价于:(p1.operator+(p2)).operator+(p3)
    // ① p1 + p2 → 返回临时对象 temp1(mA=30, mB=30)
    // ② temp1 + p3 → 返回临时对象 temp2(mA=60, mB=60)
    // ③ temp2 拷贝构造 p4

    cout << "p4: ";
    p4.show();  // ㉓ 输出:mA:60 mB:60

    // ============================================================
    // ★ 混合链式相加:Person + int + Person + int
    // ============================================================
    Person p5 = p1 + 5 + p2 + 10;
    // 执行顺序:((p1 + 5) + p2) + 10
    // ① p1 + 5 → Person(15, 15)(调用 Person::operator+(int))
    // ② (15,15) + p2 → Person(35, 35)
    // ③ (35,35) + 10 → Person(45, 45)

    cout << "p5: ";
    p5.show();  // ㉔ 输出:mA:45 mB:45

    // ============================================================
    // ★ 反向混合:int + Person + int + Person
    // ============================================================
    Person p6 = 5 + p1 + 10 + p2;
    // 执行顺序:(((5 + p1) + 10) + p2)
    // ① 5 + p1 → Person(15, 15)(调用全局函数 operator+(int, Person))
    // ② (15,15) + 10 → Person(25, 25)(调用 Person::operator+(int))
    // ③ (25,25) + p2 → Person(45, 45)(调用 Person::operator+(Person))

    cout << "p6: ";
    p6.show();  // ㉕ 输出:mA:45 mB:45

    return 0;
}

#include <iostream>     // ① 包含输入输出流头文件
using namespace std;    // ② 使用标准命名空间

// ============================================================
// ★ Person 类
// ============================================================
class Person
{
public:
    int m_A;   // ③ 成员变量 A
    int m_B;   // ④ 成员变量 B

    // ============================================================
    // ⑤ ★ 方式1:普通成员函数(未使用运算符重载)
    //     函数名:PersonAddPerson(普通函数名)
    //     调用方式:p1.PersonAddPerson(p2)
    //     优点:功能清晰,一眼就能看懂是“把两个 Person 相加”
    //     缺点:写法不直观,不如直接用 + 符号自然
    // ============================================================
    Person PersonAddPerson(Person &p)
    {
        Person temp;                       // ⑥ 创建临时对象
        temp.m_A = this->m_A + p.m_A;      // ⑦ 当前对象的 m_A + 传入对象的 m_A
        temp.m_B = this->m_B + p.m_B;      // ⑧ 当前对象的 m_B + 传入对象的 m_B
        return temp;                       // ⑨ 返回临时对象
    }

    // ============================================================
    // ⑩ ★ 方式2:运算符重载(成员函数版)
    //     函数名:operator+(运算符重载)
    //     调用方式:p1 + p2 → 编译器转为 p1.operator+(p2)
    //     优点:语法直观,像内置类型一样使用 +
    //     缺点:需要学习运算符重载的语法
    // ============================================================
    Person operator+ (Person &p)
    {
        Person temp;
        temp.m_A = this->m_A + p.m_A;
        temp.m_B = this->m_B + p.m_B;
        return temp;
    }
};

// ============================================================
// ⑪ ★ 方式3:运算符重载(全局函数版)
//     函数名:operator+(运算符重载)
//     调用方式:p1 + p2 → 编译器转为 operator+(p1, p2)
//     优点:左侧可以是任何类型(如 int + Person)
//     缺点:如果 m_A、m_B 是私有成员,需要声明为友元
// ============================================================
Person operator+ (Person &p1, Person &p2)
{
    Person temp;
    temp.m_A = p1.m_A + p2.m_A;    // ⑫ 直接用形参 p1 和 p2
    temp.m_B = p1.m_B + p2.m_B;
    return temp;
}

// ============================================================
// ⑬ 主函数
// ============================================================
int main()
{
    Person p1, p2;          // ⑭ 创建两个 Person 对象
    p1.m_A = 10; p1.m_B = 10;   // ⑮ 初始化 p1
    p2.m_A = 20; p2.m_B = 20;   // ⑯ 初始化 p2

    // ============================================================
    // ⑰ ★ 方式1:普通成员函数
    //     写法:p1.PersonAddPerson(p2)
    //     直观但不够“自然”
    // ============================================================
    Person p3 = p1.PersonAddPerson(p2);
    cout << p3.m_A << ", " << p3.m_B << endl;  // ⑱ 输出:30, 30

    // ============================================================
    // ⑲ ★ 方式2:成员函数运算符重载
    //     写法:p1 + p2
    //     编译器自动转换为:p1.operator+(p2)
    //     语法自然,像使用 int 加法一样
    // ============================================================
    Person p4 = p1 + p2;   // 调用 Person::operator+(p2)
    cout << p4.m_A << ", " << p4.m_B << endl;  // ⑳ 输出:30, 30

    // ============================================================
    // ㉑ ★ 方式3:全局函数运算符重载
    //     写法:p1 + p2
    //     编译器自动转换为:operator+(p1, p2)
    //     和方式2的写法完全相同,编译器根据上下文自动选择
    // ============================================================
    Person p5 = p1 + p2;   // 调用全局函数 operator+(p1, p2)
    cout << p5.m_A << ", " << p5.m_B << endl;  // ㉒ 输出:30, 30

    return 0;
}
Person operator+(const Person& p2, int val)
{
    Person temp;
    temp.m_A = p2.m_A + val;
    temp.m_B = p2.m_B + val;
    return temp;
}
Person operator+(int val)
    {
        Person temp;
        temp.m_A = this->m_A + val;
        temp.m_B = this->m_B + val;
        return temp;
    }

第一个是“全局函数”,支持 Person + int 的写法;第二个是“成员函数”,也支持 Person + int 的写法。它们功能相同,但调用方式不同,且不能同时存在(会产生二义性)。

对比表

对比项 全局函数版本 成员函数版本
定义位置 类外部 类内部
参数个数 2 个(Person& + int 1 个(int
左侧操作数来源 第一个参数 p2 this(调用对象)
this 指针 ❌ 没有 ✅ 有
调用方式 operator+(p3, 10) p3.operator+(10)
使用写法 p3 + 10 p3 + 10(语法相同)
能否访问私有成员 ❌ 不能(除非友元) ✅ 能(自己是成员)

本质区别

全局函数版

Person operator+(const Person& p2, int val)
{
    Person temp;
    temp.m_A = p2.m_A + val;    // p2 是形参,不是 this
    temp.m_B = p2.m_B + val;
    return temp;
}
  • 它是一个独立的全局函数

  • 它通过形参 p2 获取左侧操作数的值。

  • 不属于任何类,所以如果 m_A 是私有成员,它无法访问(除非声明为友元)。

成员函数版

Person operator+(int val)
{
    Person temp;
    temp.m_A = this->m_A + val;    // this 指向调用对象
    temp.m_B = this->m_B + val;
    return temp;
}
  • 它是 Person 类的成员函数

  • 它通过 this 指针获取左侧操作数的值。

  • 属于 Person 类,所以可以直接访问私有成员 m_Am_B

调用方式对比


Person p(10, 10);

// ★ 两种函数写法的实际调用方式不同

// 全局函数版本:
Person result1 = operator+(p, 5);   // 直接调用函数名
Person result2 = p + 5;             // 语法糖,编译器自动转为 operator+(p, 5)

// 成员函数版本:
Person result3 = p.operator+(5);    // 通过对象调用成员函数
Person result4 = p + 5;             // 语法糖,编译器自动转为 p.operator+(5)

编译器的处理

表达式 全局函数版本 成员函数版本
p + 5 operator+(p, 5) p.operator+(5)

⚠️ 二义性警告

如果同时定义两个版本,编译器会报错:

// ❌ 错误:二义性
class Person {
    Person operator+(int val) { ... }  // 成员函数
};
Person operator+(const Person& p, int val) { ... }  // 全局函数

Person p;
p + 5;   // ❌ 编译器不知道该调用哪一个!
原因p + 5 既可以匹配成员函数 p.operator+(5),也可以匹配全局函数 operator+(p, 5)。

💡 语言表述方法

描述全局函数版

“这是一个全局函数,重载了 + 运算符,用于支持 Person 对象 + int 值 的运算。它接收一个 Person 常引用和一个 int 值作为参数,返回一个新的 Person 对象。该函数不属于 Person 类,因此如果 m_A 和 m_B 是私有成员,需要将函数声明为友元才能访问。”

描述成员函数版

“这是一个 Person 类的成员函数,重载了 + 运算符,用于支持 Person 对象 + int 值 的运算。它接收一个 int 值作为参数,通过 this 指针访问当前对象的成员变量,返回一个新的 Person 对象。因为它是成员函数,所以可以直接访问类的私有成员。”

📋 使用建议

场景 推荐 原因
操作数左侧是自定义类型 成员函数 更自然,能直接访问私有成员
操作数左侧是内置类型 全局函数 如 int + Personint 没有成员函数
需要同时支持 Person + int 和 int + Person 成员函数 + 全局函数 成员函数支持 Person + int,全局函数支持 int + Person

> 总结1:对于内置的数据类型的表达式的运算符是不可能改变的

> 总结2:不要滥用运算符重载

4.5.2 左移运算符重载

作用:可以输出自定义数据类型

#include <iostream>     // ① 包含输入输出流头文件(其中定义了 ostream 类)
using namespace std;    // ② 使用标准命名空间

// ============================================================
// ★ Person 类
// ============================================================
class Person {
    // ③ ★ 友元声明:让 operator<< 可以访问私有成员
    //     因为 operator<< 是全局函数,不是成员函数
    //     没有友元声明就无法访问 m_A 和 m_B
    friend ostream& operator<<(ostream& out, Person& p);

public:
    // ④ 有参构造函数
    Person(int a, int b)
    {
        this->m_A = a;
        this->m_B = b;
    }

private:
    int m_A;   // ⑤ 私有成员变量 A
    int m_B;   // ⑥ 私有成员变量 B
};

// ============================================================
// ⑦ ★ 全局函数实现左移运算符重载
//     参数1:ostream& out → 输出流对象的引用(如 out)
//     参数2:Person& p    → 要输出的 Person 对象
//     返回值:ostream&    → 返回输出流引用,支持链式编程(cout << p << endl)
// ============================================================
ostream& operator<<(ostream& cout, Person& p)
{
    // ⑧ ★ 输出格式化内容
    cout << "a:" << p.m_A << " b:" << p.m_B;
    return cout;   // ⑨ ★ 返回 out(也就是 cout)本身,支持链式调用
}

// ============================================================
// ⑩ 测试函数
// ============================================================
void test()
{
    Person p1(10, 20);   // ⑪ 创建 Person 对象

    // ⑫ ★ 链式编程:cout << p1 << "hello world" << endl;
    //     执行顺序:
    //     1. cout << p1      → 调用 operator<<(cout, p1),返回 cout
    //     2. cout << "hello" → 调用内置的 operator<<(cout, "hello"),返回 cout
    //     3. cout << endl    → 调用内置的 operator<<(cout, endl),返回 cout
    cout << p1 << "hello world" << endl;
}

// ============================================================
// ⑬ 主函数
// ============================================================
int main()
{
    test();                  // ⑭ 调用测试函数
    system("pause");         // ⑮ 暂停控制台窗口
    return 0;                // ⑯ 程序正常退出
}
#include <iostream>
#include <string>
using namespace std;

// ========== Person 类 ==========
class Person
{
    // ⭐ 友元声明(关键!)
    // 让 operator<< 全局函数可以访问 Person 的私有成员
    friend ostream& operator<<(ostream& cout, Person& p);

public:
    // 有参构造函数
    Person(int a, int b)
    {
        m_A = a;
        m_B = b;
    }

    // ❌ 成员函数方式(注释掉,不可行)
    // 如果写成成员函数,调用方式会是 p << cout
    // 但我们想要 cout << p
    /*
    void operator<<(cout)
    {
        // 这样写是错误的
    }
    */

private:
    int m_A;
    int m_B;
};

// ===== 全局函数重载 << 左移运算符 =====
// 参数1:ostream 引用(cout)
// 参数2:要输出的 Person 对象
// 返回值:ostream 引用(支持链式编程)
ostream& operator<<(ostream& cout, Person& p)
//      ↑              ↑        ↑
//   返回引用      第一个参数  第二个参数
//   本质:operator<<(cout, p) 简化为 cout << p
{
    // ✅ 可以访问私有成员(因为是友元)
    cout << "m_A = " << p.m_A << " m_B = " << p.m_B << endl;
    return cout;  // 返回 cout 本身,支持链式调用
}

void test01()
{
    Person p(10, 10);  // 使用有参构造函数

    // ✅ 调用全局函数 operator<<
    cout << p << endl;
    // 等价于:operator<<(cout, p) << endl;
}

int main()
{
    test01();
    system("pause");
    return 0;
}

总结:重载左移运算符配合友元可以实现输出自定义数据类型

4.5.3 递增运算符重载

作用: 通过重载递增运算符,实现自己的整型数据

#include <iostream>
using namespace std;

class MyInteger {
    // 友元:让 operator<< 可以访问私有成员 m_Num
    friend ostream& operator<<(ostream& out, MyInteger myint);

public:
    // 构造函数:初始化为 0
    MyInteger() {
        m_Num = 0;
    }

    // ===== 前置++ =====
    // 语法:operator++(),没有参数
    // 特点:先自增,再返回
    // 返回值:引用(返回自身,支持链式操作)
    MyInteger& operator++() {
        // 1. 先自增
        m_Num++;
        // 2. 再返回自身(引用)
        return *this;
    }

    // ===== 后置++ =====
    // 语法:operator++(int),int 是占位参数(区分前置和后置)
    // 特点:先返回旧值,再自增
    // 返回值:值(返回旧值的副本)
    MyInteger operator++(int) {
        // 1. 先保存当前值(旧值)
        MyInteger temp = *this;
        // 2. 再自增
        m_Num++;
        // 3. 返回旧值(副本)
        return temp;
    }

private:
    int m_Num;
};

// 左移运算符重载:输出 MyInteger 对象
ostream& operator<<(ostream& out, MyInteger myint) {
    out << myint.m_Num;
    return out;
}

// 测试前置++
void test01() {
    MyInteger myInt;
    cout << ++myInt << endl;  // 先++,再输出 → 1
    cout << myInt << endl;    // 输出当前值 → 1
}

// 测试后置++
void test02() {
    MyInteger myInt;
    cout << myInt++ << endl;  // 先输出旧值,再++ → 0
    cout << myInt << endl;    // 输出当前值 → 1
}

int main() {
    test01();  // 测试前置++
    // test02(); // 测试后置++
    system("pause");
    return 0;
}
#include <iostream>
#include <string>
using namespace std;

class MyInteger
{
    // 友元:让 operator<< 可以访问私有成员 m_Num
    friend ostream& operator<<(ostream& cout, MyInteger myint);

public:
    // 构造函数:初始化为 0
    MyInteger()
    {
        m_Num = 0;
    }

    // ===== 前置++ =====
    // 特点:先自增,再返回
    // 返回值:MyInteger&(引用),必须返回引用!
    // 原因:
    // 1. 效率高(不拷贝)
    // 2. 支持链式调用 ++(++myInt)
    // 3. 返回的是修改后的对象本身
    MyInteger& operator++()
    {
        m_Num++;        // ① 先自增
        return *this;   // ② 再返回自己(引用)
    }

    // ===== 后置++ =====
    // 特点:先保存旧值,再自增,最后返回旧值
    // 返回值:MyInteger(值),必须返回值!
    // 原因:
    // 1. 必须返回修改前的旧值
    // 2. 旧值必须与当前对象分离(不能返回引用)
    // 3. int 是占位参数,用于区分前置和后置
    MyInteger operator++(int)
    {
        MyInteger temp = *this;  // ① 保存旧值(拷贝)
        m_Num++;                 // ② 自增
        return temp;             // ③ 返回旧值(再次拷贝)
    }

private:
    int m_Num;
};

// ===== 左移运算符重载 =====
// 用于输出 MyInteger 对象
ostream& operator<<(ostream& cout, MyInteger myint)
{
    cout << myint.m_Num;  // 访问私有成员(友元)
    return cout;          // 返回 cout 引用(支持链式)
}

// ===== 测试前置++ =====
void test01()
{
    MyInteger myint;
    cout << ++myint << endl;  // 输出 1
}

// ===== 测试后置++ =====
void test02()
{
    MyInteger myint;
    cout << myint++ << endl;  // 输出 0(旧值)
    cout << myint << endl;    // 输出 1(新值)
}

int main()
{
    // test01();  // 测试前置++
    test02();     // 测试后置++
    system("pause");
    return 0;
}

总结: 前置递增返回引用,后置递增返回值

递减运算符重载

#include <iostream>
#include <string>
using namespace std;

class MyInteger {
public:
    // 友元:让 operator<< 可以访问私有成员 m_Num
    friend ostream& operator<<(ostream& cout, MyInteger myint);

    // 构造函数:初始化为 0
    MyInteger() {
        m_Num = 0;
    }

    // ===== 前置递减 -- =====
    // 特点:先自减,再返回
    // 返回值:MyInteger&(引用),必须返回引用!
    // 原因:
    // 1. 效率高(不拷贝)
    // 2. 支持链式调用 --(--myInt)
    // 3. 返回的是修改后的对象本身
    MyInteger& operator--()
    {
        m_Num--;        // ① 先自减
        return *this;   // ② 再返回自己(引用)
    }

    // ===== 后置递减 -- =====
    // 特点:先保存旧值,再自减,最后返回旧值
    // 返回值:MyInteger(值),必须返回值!
    // 原因:
    // 1. 必须返回修改前的旧值
    // 2. 旧值必须与当前对象分离(不能返回引用,否则悬空)
    // 3. int 是占位参数,用于区分前置和后置
    MyInteger operator--(int)
    {
        MyInteger temp = *this;  // ① 保存旧值(拷贝)
        m_Num--;                 // ② 自减
        return temp;             // ③ 返回旧值(再次拷贝)
    }

private:
    int m_Num;
};

// ===== 左移运算符重载 =====
// 用于输出 MyInteger 对象
ostream& operator<<(ostream& cout, MyInteger myint)
{
    cout << myint.m_Num << endl;  // 输出 m_Num 并换行
    return cout;                  // 返回 cout 引用(支持链式)
}

// ===== 测试前置-- =====
void test01()
{
    MyInteger myint;
    cout << --myint << endl;  // 输出 -1
}

// ===== 测试后置-- =====
void test02()
{
    MyInteger myint;
    cout << myint-- << endl;  // 输出 0(旧值)
    cout << myint;            // 输出 -1(新值)
}

int main()
{
    test01();  // 测试前置--
    test02();  // 测试后置--
    system("pause");
    return 0;
}

4.5.4 赋值运算符重载

c++编译器至少给一个类添加4个函数

1. 默认构造函数(无参,函数体为空)
2. 默认析构函数(无参,函数体为空)
3. 默认拷贝构造函数,对属性进行值拷贝
4. 赋值运算符 operator=, 对属性进行值拷贝

如果类中有属性指向堆区,做赋值操作时也会出现深浅拷贝问题

**示例:

class Person
{
public:

    Person(int age)
    {
        //将年龄数据开辟到堆区
        m_Age = new int(age);
    }

    //重载赋值运算符 
    Person& operator=(Person &p)
    {
        if (m_Age != NULL)
        {
            delete m_Age;
            m_Age = NULL;
        }
        //编译器提供的代码是浅拷贝
        //m_Age = p.m_Age;

        //提供深拷贝 解决浅拷贝的问题
        m_Age = new int(*p.m_Age);

        //返回自身
        return *this;
    }


    ~Person()
    {
        if (m_Age != NULL)
        {
            delete m_Age;
            m_Age = NULL;
        }
    }

    //年龄的指针
    int *m_Age;

};


void test01()
{
    Person p1(18);

    Person p2(20);

    Person p3(30);

    p3 = p2 = p1; //赋值操作

    cout << "p1的年龄为:" << *p1.m_Age << endl;

    cout << "p2的年龄为:" << *p2.m_Age << endl;

    cout << "p3的年龄为:" << *p3.m_Age << endl;
}

int main() {

    test01();

    //int a = 10;
    //int b = 20;
    //int c = 30;

    //c = b = a;
    //cout << "a = " << a << endl;
    //cout << "b = " << b << endl;
    //cout << "c = " << c << endl;

    system("pause");

    return 0;
}

#include <iostream>
#include <string>
using namespace std;

// ========== 赋值运算符重载 ==========
class Person
{
public:
    // 构造函数:在堆区分配内存
    Person(int age)
    {
        // new int(age) 在堆区创建 int,存储 age,返回地址
        m_Age = new int(age);
    }

    // 析构函数:释放堆区内存
    ~Person()
    {
        if (m_Age != NULL)
        {
            delete m_Age;
            m_Age = NULL;  // 置空,防止野指针
        }
    }

    // ===== 重载赋值运算符 =====
    // 返回值:Person&(引用),支持链式赋值
    // 参数:const Person& p(引用,避免拷贝)
    Person& operator=(Person& p)
    {
        // ❌ 编译器默认的浅拷贝(只复制地址)
        // m_Age = p.m_Age;  // 如果这样,两个指针指向同一块内存

        // ✅ 第1步:释放自身原有的堆内存
        // 防止内存泄漏(如果不释放,旧内存就永远丢失了)
        if (m_Age != NULL)
        {
            delete m_Age;      // 释放旧内存
            m_Age = NULL;      // 指针置空
        }

        // ✅ 第2步:深拷贝
        // 在堆区分配新内存,复制 p 的数据
        m_Age = new int(*p.m_Age);  // *p.m_Age 是 p 指向的值

        // ✅ 第3步:返回自身(支持链式赋值)
        return *this;
    }

    int* m_Age;  // 指向堆区的指针
};

void test01()
{
    Person p1(18);
    Person p2(20);
    Person p3(30);

    // 链式赋值:从右向左执行
    p3 = p2 = p1;

    cout << "p1的年龄为:" << *p1.m_Age << endl;  // 18
    cout << "p2的年龄为:" << *p2.m_Age << endl;  // 18
    cout << "p3的年龄为:" << *p3.m_Age << endl;  // 18
}

int main()
{
    test01();
    system("pause");
    return 0;
}

4.5.5 关系运算符重载

**作用:**重载关系运算符,可以让两个自定义类型对象进行对比操作

**示例:

#include <iostream>
#include <string>
using namespace std;

class Person
{
public:
    // 构造函数
    Person(string name, int age)
    {
        this->m_Name = name;
        this->m_Age = age;
    }

    // ===== 重载 == 运算符 =====
    // 返回值:bool(true/false)
    // 作用:比较两个 Person 对象是否相等
    bool operator==(Person& p)
    {
        // 如果姓名相同 AND 年龄相同 → 相等
        if (this->m_Name == p.m_Name && this->m_Age == p.m_Age)
        {
            return true;
        }
        else
        {
            return false;
        }
    }

    // ===== 重载 != 运算符 =====
    // 返回值:bool(true/false)
    // 作用:比较两个 Person 对象是否不相等
    bool operator!=(Person& p)
    {
        // 如果姓名相同 AND 年龄相同 → 相等 → 返回 false(不相等为假)
        if (this->m_Name == p.m_Name && this->m_Age == p.m_Age)
        {
            return false;  // 相等,所以 != 返回 false
        }
        else
        {
            return true;   // 不相等,所以 != 返回 true
        }
    }

    string m_Name;
    int m_Age;
};

void test01()
{
    Person a("孙悟空", 18);
    Person b("孙悟空", 18);

    // 测试 ==
    if (a == b)
    {
        cout << "a和b相等" << endl;
    }
    else
    {
        cout << "a和b不相等" << endl;
    }

    // 测试 !=
    if (a != b)
    {
        cout << "a和b不相等" << endl;
    }
    else
    {
        cout << "a和b相等" << endl;
    }
}

int main()
{
    test01();
    system("pause");
    return 0;
}

4.5.6 函数调用运算符重载

* 函数调用运算符 () 也可以重载
* 由于重载后使用的方式非常像函数的调用,因此称为仿函数
* 仿函数没有固定写法,非常灵活

**示例:

#include <iostream>
#include <string>
using namespace std;

// ===== 仿函数1:打印字符串 =====
class MyPrint
{
public:
    // 重载函数调用运算符 ()
    // 参数:string text
    // 返回值:void
    // 作用:让对象像函数一样使用
    void operator()(string text)
    {
        cout << text << endl;
    }
};

void test01()
{
    // 重载的 () 操作符也称为仿函数
    MyPrint myFunc;          // 创建对象
    myFunc("hello world");   // 像函数一样调用!
    // 等价于:myFunc.operator()("hello world");
}

// ===== 仿函数2:两数相加 =====
class MyAdd
{
public:
    // 重载函数调用运算符 ()
    // 参数:int v1, int v2
    // 返回值:int
    int operator()(int v1, int v2)
    {
        return v1 + v2;
    }
};

void test02()
{
    // 方式1:通过对象调用
    MyAdd add;                // 创建对象
    int ret = add(10, 10);    // 像函数一样调用!
    cout << "ret = " << ret << endl;  // 20

    // 方式2:匿名对象调用
    // MyAdd() 创建临时对象,然后直接调用 operator()
    cout << "MyAdd()(100,100) = " << MyAdd()(100, 100) << endl;
    // 等价于:MyAdd temp; temp(100, 100);
}

int main()
{
    test01();
    test02();
    system("pause");
    return 0;
}

4.6 继承

**继承是面向对象三大特性之一**

有些类与类之间存在特殊的关系,例如下图中:

我们发现,定义这些类时,下级别的成员除了拥有上一级的共性,还有自己的特性。

这个时候我们就可以考虑利用继承的技术,减少重复代码

4.6.1 继承的基本语法

例如我们看到很多网站中,都有公共的头部,公共的底部,甚至公共的左侧列表,只有中心内容不同

接下来我们分别利用普通写法和继承的写法来实现网页中的内容,看一下继承存在的意义以及好处

**普通实现:

//Java页面
class Java 
{
public:
    void header()
    {
        cout << "首页、公开课、登录、注册...(公共头部)" << endl;
    }
    void footer()
    {
        cout << "帮助中心、交流合作、站内地图...(公共底部)" << endl;
    }
    void left()
    {
        cout << "Java,Python,C++...(公共分类列表)" << endl;
    }
    void content()
    {
        cout << "JAVA学科视频" << endl;
    }
};
//Python页面
class Python
{
public:
    void header()
    {
        cout << "首页、公开课、登录、注册...(公共头部)" << endl;
    }
    void footer()
    {
        cout << "帮助中心、交流合作、站内地图...(公共底部)" << endl;
    }
    void left()
    {
        cout << "Java,Python,C++...(公共分类列表)" << endl;
    }
    void content()
    {
        cout << "Python学科视频" << endl;
    }
};
//C++页面
class CPP 
{
public:
    void header()
    {
        cout << "首页、公开课、登录、注册...(公共头部)" << endl;
    }
    void footer()
    {
        cout << "帮助中心、交流合作、站内地图...(公共底部)" << endl;
    }
    void left()
    {
        cout << "Java,Python,C++...(公共分类列表)" << endl;
    }
    void content()
    {
        cout << "C++学科视频" << endl;
    }
};

void test01()
{
    //Java页面
    cout << "Java下载视频页面如下: " << endl;
    Java ja;
    ja.header();
    ja.footer();
    ja.left();
    ja.content();
    cout << "--------------------" << endl;

    //Python页面
    cout << "Python下载视频页面如下: " << endl;
    Python py;
    py.header();
    py.footer();
    py.left();
    py.content();
    cout << "--------------------" << endl;

    //C++页面
    cout << "C++下载视频页面如下: " << endl;
    CPP cp;
    cp.header();
    cp.footer();
    cp.left();
    cp.content();

}

int main() {

    test01();

    system("pause");

    return 0;
}

继承实现:

#include <iostream>  // 包含输入输出流头文件,用于使用 cout 和 endl
#include <string>    // 包含字符串头文件(虽然这里没用到,但通常C++项目中会保留)
using namespace std; // 使用标准命名空间,避免每次写 std::cout

//===========================================
// 基类(父类):公共页面
// 作用:定义所有页面共有的成员函数
//===========================================
class BasePage
{
public:  // 公有访问权限,所有地方都可以访问
    // 公共头部函数:显示页面的顶部导航栏
    void header()
    {
        // cout 是标准输出流,endl 是换行符
        cout << "首页、公开课、登录、注册...(公共头部)" << endl;
    }

    // 公共底部函数:显示页面的底部信息
    void footer()
    {
        cout << "帮助中心、交流合作、站内地图...(公共底部)" << endl;
    }
    
    // 公共左侧栏函数:显示页面的分类列表
    void left()
    {
        cout << "Java,Python,C++...(公共分类列表)" << endl;
    }
    // 注意:这里没有写析构函数,编译器会自动生成默认的
};

//===========================================
// 子类(派生类):Java页面
// 继承方式:public(公有继承)
// 含义:Java 是一种 BasePage(is-a 关系)
//===========================================
class Java : public BasePage  // 冒号表示继承,public 是继承方式
{
public:  // 公有访问权限
    // 子类特有的成员函数:显示Java学科的内容
    void content()
    {
        cout << "JAVA学科视频" << endl;  // 只输出Java特有的内容
    }
    // 子类自动拥有基类的 header()、footer()、left() 三个函数
    // 这就是代码复用!不需要重复写这三个函数
};

//===========================================
// 子类(派生类):Python页面
// 继承方式和 Java 类完全相同
//===========================================
class Python : public BasePage  // 公有继承 BasePage
{
public:
    void content()
    {
        cout << "Python学科视频" << endl;  // Python特有的内容
    }
};

//===========================================
// 子类(派生类):C++页面
//===========================================
class CPP : public BasePage  // 公有继承 BasePage
{
public:
    void content()
    {
        cout << "C++学科视频" << endl;  // C++特有的内容
    }
};

//===========================================
// 测试函数:演示继承的效果
//===========================================
void test01()
{
    // ---------- 测试 Java 页面 ----------
    cout << "Java下载视频页面如下: " << endl;  // 输出标题
    Java ja;  // 创建 Java 类的对象(实例化)
    // 以下调用都是从 BasePage 继承来的公共成员函数
    ja.header();  // 调用基类的 header() 函数
    ja.footer();  // 调用基类的 footer() 函数
    ja.left();    // 调用基类的 left() 函数
    ja.content(); // 调用自己的 content() 函数(子类特有的)
    cout << "--------------------" << endl;  // 分隔线

    // ---------- 测试 Python 页面 ----------
    cout << "Python下载视频页面如下: " << endl;
    Python py;   // 创建 Python 类的对象
    py.header();  // 继承自基类
    py.footer();  // 继承自基类
    py.left();    // 继承自基类
    py.content(); // 自己的特有函数
    cout << "--------------------" << endl;

    // ---------- 测试 C++ 页面 ----------
    cout << "C++下载视频页面如下: " << endl;
    CPP cp;      // 创建 C++ 类的对象
    cp.header();  // 继承自基类
    cp.footer();  // 继承自基类
    cp.left();    // 继承自基类
    cp.content(); // 自己的特有函数
    // 注意:最后一个对象后面没有分隔线,但代码是完整的
}

//===========================================
// 主函数:程序入口
//===========================================
int main()   // main 函数返回 int 类型
{
    test01();  // 调用测试函数,执行所有测试代码

    system("pause");  // Windows 系统命令:暂停控制台窗口,方便查看输出结果
    // 效果:按任意键继续...

    return 0;  // 返回 0 表示程序正常结束
}
#include <iostream>
#include <string>
using namespace std;

// ===== 基类(父类) =====
// 定义所有页面共有的公共部分
class BasePage
{
public:
    // 公共头部
    void header()
    {
        cout << "首页、公开课、登录、注册...(公共头部)" << endl;
    }

    // 公共底部
    void footer()
    {
        cout << "帮助中心、交流合作、站内地图...(公共底部)" << endl;
    }

    // 公共分类列表
    void left()
    {
        cout << "Java,Python,C++...(公共分类列表)" << endl;
    }
};

// ===== 继承 =====
// 继承的好处:减少重复代码,继承的本质是代码复用
// 语法:class 子类 : 继承方式 父类
// 子类 也称为 派生类
// 父类 也称为 基类

// ===== Java 页面(子类) =====
// 继承方式:public(公共继承)
// Java 类拥有了 BasePage 的所有 public 成员
class Java : public BasePage
{
public:
    // 子类特有的内容
    void content() {
        cout << "Java学科视频" << endl;
    }
};

// ===== Python 页面(子类) =====
class Python : public BasePage
{
public:
    void content() {
        cout << "Python学科视频" << endl;
    }
};

// ===== C++ 页面(子类) =====
class CPP : public BasePage
{
public:
    void content() {
        cout << "C++学科视频" << endl;
    }
};

void test01()
{
    // ----- Java 页面 -----
    cout << "Java下载视频页面如下: " << endl;
    Java ja;
    ja.header();   // 继承自 BasePage
    ja.footer();   // 继承自 BasePage
    ja.left();     // 继承自 BasePage
    ja.content();  // 自己的

    // ----- Python 页面 -----
    cout << "--------------------" << endl;
    cout << "Python下载视频页面如下: " << endl;
    Python py;
    py.header();   // 继承自 BasePage
    py.footer();   // 继承自 BasePage
    py.left();     // 继承自 BasePage
    py.content();  // 自己的

    // ----- C++ 页面 -----
    cout << "--------------------" << endl;
    cout << "C++下载视频页面如下: " << endl;
    CPP cp;
    cp.header();   // 继承自 BasePage
    cp.footer();   // 继承自 BasePage
    cp.left();     // 继承自 BasePage
    cp.content();  // 自己的
}

int main()
{
    test01();
    system("pause");
    return 0;
}

**总结:**

继承的好处:==可以减少重复的代码==

class A : public B;

A 类称为子类 或 派生类

B 类称为父类 或 基类

**派生类中的成员,包含两大部分**:

一类是从基类继承过来的,一类是自己增加的成员。

从基类继承过来的表现其共性,而新增的成员体现了其个性。

4.6.2 继承方式

继承的语法:`class 子类 : 继承方式 父类`

**继承方式一共有三种:**

* 公共继承public
* 保护继承protected
* 私有继承private

三种继承方式对比表

基类成员权限 public 继承 protected 继承 private 继承
public 成员 子类中变为 public 子类中变为 protected 子类中变为 private
protected 成员 子类中变为 protected 子类中变为 protected 子类中变为 private
private 成员 ❌ 永远不可访问 ❌ 永远不可访问 ❌ 永远不可访问

1. 公共继承(public)

class Son1 : public Base1
基类成员 在子类中的权限 类外能否访问
m_A(public) public ✅ 可以
m_B(protected) protected ❌ 不可以
m_C(private) 不可访问 ❌ 不可以

特点:基类的访问权限在子类中保持不变

2. 保护继承(protected)

class Son2 : protected Base2
基类成员 在子类中的权限 类外能否访问
m_A(public) protected ❌ 不可以
m_B(protected) protected ❌ 不可以
m_C(private) 不可访问 ❌ 不可以

特点:基类的所有可访问成员在子类中都变成 protected(类外不能访问,但子类可以)。

3. 私有继承(private)

class Son3 : private Base3
基类成员 在子类中的权限 类外能否访问
m_A(public) private ❌ 不可以
m_B(protected) private ❌ 不可以
m_C(private) 不可访问 ❌ 不可以

特点:基类的所有可访问成员在子类中都变成 private(连子类的子类都不能访问)。

总结

        public 继承保持原权限;protected 继承把 public 降为 protected;private 继承把所有可访问成员降为 private。基类的 private 成员在任何继承方式下都不可访问。 

继承方式像一个“过滤器”或“上限值”,它限制了基类成员在子类中能拥有的最高访问权限。

核心规则

继承方式决定了基类成员在子类中“最多能有多大的访问权限”。

  • public 继承:权限不变(原是什么,还是什么)

  • protected 继承:权限最高只能是 protected

  • private 继承:权限最高只能是 private

示例:

#include <iostream>
using namespace std;

// ========== 1. 公共继承(public) ==========
class Base1
{
public:
    int m_A;    // 公共权限:任何人都能访问
protected:
    int m_B;    // 保护权限:类内和子类能访问
private:
    int m_C;    // 私有权限:只有类内能访问
};

// Son1 公共继承 Base1
class Son1 : public Base1
{
public:
    void func()
    {
        m_A;    // ✅ 可访问:Base1::m_A 在 Son1 中仍是 public
        m_B;    // ✅ 可访问:Base1::m_B 在 Son1 中是 protected
        // m_C; // ❌ 不可访问:Base1::m_C 是 private,永远不能访问
    }
};

void myClass1()
{
    Son1 s1;
    s1.m_A;     // ✅ 可访问:public 成员类外可以访问
    // s1.m_B;  // ❌ 不可访问:protected 成员类外不能访问
    // s1.m_C;  // ❌ 不可访问:private 成员类外不能访问
}

// ========== 2. 保护继承(protected) ==========
class Base2
{
public:
    int m_A;
protected:
    int m_B;
private:
    int m_C;
};

// Son2 保护继承 Base2
class Son2 : protected Base2
{
public:
    void func()
    {
        m_A;    // ✅ 可访问:Base2::m_A 在 Son2 中变成 protected
        m_B;    // ✅ 可访问:Base2::m_B 在 Son2 中仍是 protected
        // m_C; // ❌ 不可访问
    }
};

void myClass2()
{
    Son2 s2;
    // s2.m_A;  // ❌ 不可访问:protected 成员类外不能访问
    // s2.m_B;  // ❌ 不可访问
    // s2.m_C;  // ❌ 不可访问
}

// ========== 3. 私有继承(private) ==========
class Base3
{
public:
    int m_A;
protected:
    int m_B;
private:
    int m_C;
};

// Son3 私有继承 Base3
class Son3 : private Base3
{
public:
    void func()
    {
        m_A;    // ✅ 可访问:Base3::m_A 在 Son3 中变成 private
        m_B;    // ✅ 可访问:Base3::m_B 在 Son3 中变成 private
        // m_C; // ❌ 不可访问
    }
};

// GrandSon3 继承 Son3
class GrandSon3 : public Son3
{
public:
    void func()
    {
        // ❌ 全部不可访问!
        // 因为 Son3 是私有继承 Base3,所以 Base3 的成员在 Son3 中是 private
        // GrandSon3 无法访问 Son3 的 private 成员
        // m_A;  // ❌ 不可访问
        // m_B;  // ❌ 不可访问
        // m_C;  // ❌ 不可访问
    }
};

4.6.3 继承中的对象模型

**问题:**从父类继承过来的成员,哪些属于子类对象中?

**示例:

#include <iostream>
#include <string>
using namespace std;

class Base
{
public:
    int m_A;     // 公共成员:4 字节
protected:
    int m_B;     // 保护成员:4 字节
private:
    int m_C;     // 私有成员:4 字节
    // ⚠️ 重要:私有成员只是被隐藏了,但是确实会被继承下去!
    // 子类虽然无法访问 m_C,但它确实存在于子类对象中
};

// 公共继承
class Son : public Base
{
public:
    int m_D;     // 子类自己的成员:4 字节
};

void test01()
{
    //父类中的所有非静态成员属性都会被子类继承下去
    //私有成员虽然不能访问,但确实会被继承,并占用子类对象的内存空间
    cout << "sizeof Son = " << sizeof(Son) << endl;
    // 输出:16
    // 计算:m_A(4) + m_B(4) + m_C(4) + m_D(4) = 16
}

int main()
{
    test01();
    system("pause");
    return 0;
}

利用工具查看:

打开工具窗口后,定位到当前CPP文件的盘符

然后输入: cl /d1 reportSingleClassLayout查看的类名 所属文件名

效果如下图:

结论: 父类中私有成员也是被子类继承下去了,只是由编译器给隐藏后访问不到

4.6.4 继承中构造和析构顺序

子类继承父类后,当创建子类对象,也会调用父类的构造函数

问题:父类和子类的构造和析构顺序是谁先谁后?

**示例:

#include <iostream>     // 输入输出流
using namespace std;    // 标准命名空间

// ===== 基类(父类) =====
class Base 
{
public:
    Base()  // ① 构造函数:对象创建时自动调用
    {
        cout << "Base构造函数!" << endl;
    }
    ~Base() // ② 析构函数:对象销毁时自动调用
    {
        cout << "Base析构函数!" << endl;
    }
};

// ===== 派生类(子类) =====
class Son : public Base  // ③ 继承语法:class 子类 : 继承方式 父类
{
public:
    Son()   // ④ 子类构造函数
    {
        cout << "Son构造函数!" << endl;
    }
    ~Son()  // ⑤ 子类析构函数
    {
        cout << "Son析构函数!" << endl;
    }
};

void test01()
{
    // ⑥ ⭐ 核心:创建子类对象时,构造顺序 = 先父类 → 后子类
    //    析构顺序 = 先子类 → 后父类(完全相反)
    Son s;  // ⑦ 在栈上创建对象,函数结束自动销毁
}

int main() 
{
    test01();           // ⑧ 调用测试函数
    system("pause");    // ⑨ 暂停程序
    return 0;           // ⑩ 程序结束
}

总结:继承中 先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反

构造顺序:先父类,后子类         析构顺序:先子类,后父类

4.6.5 继承同名成员处理方式

问题:当子类与父类出现同名的成员,如何通过子类对象,访问到子类或父类中同名的数据呢?

* 访问子类同名成员 直接访问即可
* 访问父类同名成员 需要加作用域

**示例:

#include <iostream>
using namespace std;

// ===== 基类(父类) =====
class Base {
public:
    // 构造函数:初始化 m_A = 100
    Base()
    {
        m_A = 100;
    }

    // 父类的 func() 无参版本
    void func()
    {
        cout << "Base - func()调用" << endl;
    }

    // 父类的 func(int) 有参版本(函数重载)
    void func(int a)
    {
        cout << "Base - func(int a)调用" << endl;
    }

public:
    int m_A;   // 父类的成员变量
};

// ===== 派生类(子类) =====
class Son : public Base {
public:
    // 子类构造函数:初始化 m_A = 200
    Son()
    {
        m_A = 200;  // ⚠️ 这里修改的是子类的 m_A,不是父类的
    }

    // ⭐ 核心规则:当子类与父类拥有同名的成员函数
    //    子类会隐藏父类中所有版本的同名成员函数
    //    如果想访问父类中被隐藏的同名成员函数,需要加父类的作用域
    void func()   // 子类的 func() 无参版本
    {
        cout << "Son - func()调用" << endl;
    }

public:
    int m_A;   // 子类的成员变量(与父类同名)
};

void test01()
{
    Son s;   // 创建子类对象

    // ----- 同名成员变量 -----
    // 访问子类的 m_A(默认)
    cout << "Son下的m_A = " << s.m_A << endl;           // 200
    
    // 访问父类的 m_A(需要加作用域)
    cout << "Base下的m_A = " << s.Base::m_A << endl;    // 100

    // ----- 同名成员函数 -----
    // 调用子类的 func()(默认)
    s.func();              // "Son - func()调用"
    
    // 调用父类的 func() 无参版本(需要加作用域)
    s.Base::func();        // "Base - func()调用"
    
    // 调用父类的 func(int) 有参版本(需要加作用域)
    s.Base::func(10);      // "Base - func(int a)调用"
}

int main()
{
    test01();
    system("pause");
    return EXIT_SUCCESS;
}

总结:

1. 子类对象可以直接访问到子类中同名成员
2. 子类对象加作用域可以访问到父类同名成员
3. 当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数

4.6.6 继承同名静态成员处理方式

问题:继承中同名的静态成员在子类对象上如何进行访问?

静态成员和非静态成员出现同名,处理方式一致

* 访问子类同名成员 直接访问即可
* 访问父类同名成员 需要加作用域

**示例:

#include <iostream>
using namespace std;

// ===== 基类(父类) =====
class Base {
public:
    // 父类的静态成员函数(无参版本)
    static void func()
    {
        cout << "Base - static void func()" << endl;
    }
    
    // 父类的静态成员函数(有参版本)—— 重载
    static void func(int a)
    {
        cout << "Base - static void func(int a)" << endl;
    }

    // 父类的静态成员变量(声明)
    static int m_A;
};

// ✅ 静态成员变量必须在类外单独定义和初始化
int Base::m_A = 100;

// ===== 派生类(子类) =====
class Son : public Base {
public:
    // 子类的静态成员函数(与父类同名)
    // ⭐ 会隐藏父类所有同名的静态成员函数
    static void func()
    {
        cout << "Son - static void func()" << endl;
    }
    
    // 子类的静态成员变量(与父类同名)
    static int m_A;
};

// ✅ 子类的静态成员变量也必须类外初始化
int Son::m_A = 200;

// ===== 测试同名静态成员变量 =====
void test01()
{
    cout << "===== 静态成员变量测试 =====" << endl;
    
    // ----- 1. 通过对象访问 -----
    cout << "\n【通过对象访问】" << endl;
    Son s;
    cout << "Son  下 m_A = " << s.m_A << endl;              // 200(子类的)
    cout << "Base 下 m_A = " << s.Base::m_A << endl;        // 100(父类的)

    // ----- 2. 通过类名访问 -----
    cout << "\n【通过类名访问】" << endl;
    cout << "Son  下 m_A = " << Son::m_A << endl;           // 200(子类的)
    cout << "Base 下 m_A = " << Son::Base::m_A << endl;     // 100(父类的)
}

// ===== 测试同名静态成员函数 =====
void test02()
{
    cout << "===== 静态成员函数测试 =====" << endl;
    
    // ----- 1. 通过对象访问 -----
    cout << "\n【通过对象访问】" << endl;
    Son s;
    s.func();                     // ⭐ 调用子类的 func()(默认)
    s.Base::func();               // ⭐ 调用父类的 func() 无参版本(加作用域)

    // ----- 2. 通过类名访问 -----
    cout << "\n【通过类名访问】" << endl;
    Son::func();                  // ⭐ 调用子类的 func()
    Son::Base::func();            // ⭐ 调用父类的 func() 无参版本
    
    // ⭐ 核心:子类同名函数会隐藏父类所有同名函数
    // 父类的 func(int) 被隐藏了,必须加作用域才能访问
    Son::Base::func(100);         // ⭐ 调用父类的 func(int) 有参版本
}

int main()
{
    // test01();  // 测试静态成员变量
    test02();     // 测试静态成员函数

    system("pause");
    return 0;
}

总结:同名静态成员处理方式和非静态处理方式一样,只不过有两种访问的方式(通过对象 和 通过类名)

4.6.7 多继承语法

C++允许**一个类继承多个类**

语法:` class 子类 :继承方式 父类1 , 继承方式 父类2...`

多继承可能会引发父类中有同名成员出现,需要加作用域区分

**C++实际开发中不建议用多继承**

**示例:

#include <iostream>
using namespace std;

// ===== 基类1 =====
class Base1 {
public:
    Base1()
    {
        m_A = 100;   // 初始化 m_A = 100
    }
public:
    int m_A;         // 成员变量 m_A
};

// ===== 基类2 =====
class Base2 {
public:
    Base2()
    {
        m_A = 200;   // 初始化 m_A = 200
        // ⚠️ 注意:如果这里写 m_B,不会出问题
        // 但写 m_A,就会和 Base1 的 m_A 同名
    }
public:
    int m_A;         // 成员变量 m_A(和 Base1 同名!)
};

// ===== 多继承语法 =====
// class 子类 : 继承方式 父类1, 继承方式 父类2
// 注意:继承顺序会影响构造顺序
class Son : public Base2, public Base1
//          ↑              ↑
//       先构造 Base2   再构造 Base1
{
public:
    Son()
    {
        m_C = 300;
        m_D = 400;
    }
public:
    int m_C;
    int m_D;
};

// ===== 多继承容易产生成员同名的情况 =====
// ✅ 通过使用类名作用域可以区分调用哪一个基类的成员
void test01()
{
    Son s;  // 创建子类对象

    // sizeof(Son) = 4 + 4 + 4 + 4 = 16 字节
    cout << "sizeof Son = " << sizeof(s) << endl;

    // ✅ 通过类名作用域区分同名成员
    cout << s.Base1::m_A << endl;  // 100(Base1 的 m_A)
    cout << s.Base2::m_A << endl;  // 200(Base2 的 m_A)
}

int main()
{
    test01();
    system("pause");
    return 0;
}

总结: 多继承中如果父类中出现了同名情况,子类使用时候要加作用域

多继承的关键规则

规则1:构造顺序 = 继承声明顺序

class Son : public Base2, public Base1
//         ↑          ↑
//      先构造      后构造

构造顺序: Base2() → Base1() → Son()

规则2:同名成员 = 需要加作用域区分

// 两个父类都有 m_A
s.m_A;           // ❌ 错误!不明确(二义性)
s.Base1::m_A;    // ✅ 100(Base1 的 m_A)
s.Base2::m_A;    // ✅ 200(Base2 的 m_A)

规则3:对象大小 = 所有成员大小之和

sizeof(Son) = sizeof(Base1) + sizeof(Base2) + sizeof(Son自己的成员)
            = 4 + 4 + 4 + 4 = 16 字节

4.6.8 菱形继承

**菱形继承概念:**

​ 两个派生类继承同一个基类

​ 又有某个类同时继承这两个派生类

​ 这种继承被称为菱形继承,或者钻石继承

**典型的菱形继承案例:

菱形继承问题:

1. 羊继承了动物的数据,驼同样继承了动物的数据,当草泥马使用数据时,就会产生二义性。
2. 草泥马继承自动物的数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。

示例:

#include <iostream>
using namespace std;

// ===== 基类(动物) =====
// 这个类将被虚继承
class Animal
{
public:
    int m_Age;   // 年龄成员
};

// ===== 虚继承 =====
// 在继承前加 virtual 关键字,变为虚继承
// 此时公共的父类 Animal 称为虚基类

// ⭐ Sheep 虚继承 Animal
// 加上 virtual,表示 Sheep 和 Animal 之间是"共享继承"
class Sheep : virtual public Animal {};

// ⭐ Tuo 虚继承 Animal
// Tuo 也虚继承 Animal
class Tuo : virtual public Animal {};

// ===== 多继承 =====
// SheepTuo 同时继承 Sheep 和 Tuo
// 因为 Sheep 和 Tuo 都是虚继承 Animal
// 所以 SheepTuo 中只有一份 Animal 的成员
class SheepTuo : public Sheep, public Tuo {};

void test01()
{
    SheepTuo st;  // 创建 SheepTuo 对象

    // 通过 Sheep 路径设置 m_Age
    st.Sheep::m_Age = 100;
    
    // 通过 Tuo 路径设置 m_Age
    // ⭐ 因为是虚继承,Sheep 和 Tuo 共享同一个 Animal
    // 所以这里会把 m_Age 从 100 改为 200
    st.Tuo::m_Age = 200;

    // 输出结果:
    // 1. 通过 Sheep 路径访问 → 200(被覆盖了)
    cout << "st.Sheep::m_Age = " << st.Sheep::m_Age << endl;
    
    // 2. 通过 Tuo 路径访问 → 200(同一个变量)
    cout << "st.Tuo::m_Age = " << st.Tuo::m_Age << endl;
    
    // 3. 直接访问 → 200(只有一份)
    cout << "st.m_Age = " << st.m_Age << endl;
}

int main()
{
    test01();
    system("pause");
    return 0;
}

总结:

* 菱形继承带来的主要问题是子类继承两份相同的数据,导致资源浪费以及毫无意义
* 利用虚继承可以解决菱形继承问题

虚继承的内存布局详细解析

  st 对象中已经包含了 Sheep 和 Tuo 子对象,这些子对象内部各自有虚基类指针(vbptr),它们都指向 SheepTuo 类的虚基类表(vbtable)。而 SheepTuo 的 vbtable 中,记录了不同子对象(SheepTuo)到同一个虚基类 Animal 的偏移量,通过偏移量就能定位到唯一的 Animal 子对象。

class SheepTuo size(12):

说明: SheepTuo 对象在内存中占 12 字节

内存布局图示

各部分详细拆解

vbptr 和 vbTable 的工作原理

vbptr 的作用:

  • 通过 vbptr 找到 vbTable

  • vbTable 中存储了 Animal 部分在对象中的偏移量(8)

  • 通过偏移量找到共享的 Animal 数据

总结:虚继承通过在子类中增加 vbtptr(虚基类表指针),让 Sheep 和 Tuo 共享同一个 Animal 基类,避免了数据冗余和二义性,但增加了内存开销。

虚继承的底层原理(简化理解)

        编译器会为虚继承的类生成一个虚基类指针(vbptr),指向一个虚基类表(vbtable)。这张表记录了共享的虚基类成员在当前对象中的偏移量。因此,无论通过哪条路径访问,最终都会定位到同一块内存。


虚继承的设计是为了解决菱形继承(钻石继承)带来的数据冗余二义性问题

1. 问题起源:菱形继承(没有虚继承)

假设你有这样一个继承结构:

class Animal { public: int age; };
class Sheep : public Animal {};
class Tuo : public Animal {};
class SheepTuo : public Sheep, Tuo {};

        这时 SheepTuo 对象里会有两份 age 的拷贝(一份来自 Sheep,一份来自 Tuo)。访问 age 时必须指明路径(s.Sheep::age),否则编译器会报二义性。这就是数据冗余和二义性的问题。

2. 解决方案:虚继承(Virtual Inheritance)

为了解决这个问题,我们在继承时加上 virtual 关键字:

class Sheep : virtual public Animal {};
class Tuo : virtual public Animal {};

这样 Animal 就成了 虚基类Sheep 和 Tuo 对 Animal 的继承就是虚继承。最终 SheepTuo 对象中只会保留一份 age 的共享拷贝,既节省了内存,也解决了二义性。

3. 底层的实现机制:vbptr 和 vbtable

编译器通过虚基类指针(vbptr)虚基类表(vbtable)来管理这个共享的 Animal 子对象。

  • vbptr(虚基类指针):这是一个隐藏的指针,位于每个派生类对象的开头,它不指向虚函数表(vtable),而是指向虚基类表(vbtable)

  • vbtable(虚基类表):这个表记录了当前对象与它共享的那个虚基类子对象之间的偏移量。通过这个偏移量,无论 SheepTuo 通过哪条路径访问 age,程序都能在运行时动态计算出 Animal 子对象的准确位置,并定位到那同一份数据上。

4. 整个过程用一张图来理解

当 SheepTuo 对象需要访问 age 时,实际的运行逻辑是这样的:

5. 核心知识点小结

概念 说明
虚基类 在继承链中被 virtual 继承的基类,最终派生类中只保留一份拷贝
虚继承 解决菱形继承问题的继承方式,使用 virtual 关键字
虚基类表(vbtable) 存储虚基类子对象相对于当前对象偏移量的数据结构(编译期生成)
虚基类指针(vbptr) 对象中指向 vbtable 的隐藏指针,用于运行时计算虚基类的位置
  • 虚基类表(vbtable):存放在可执行文件的数据段(只读常量区),编译时生成,所有对象共享

  • 虚基类指针(vbptr):存放在每个对象的开头(对象内存中),每个对象都有自己的 vbptr

存放位置对比

概念 存放位置 属于谁 生成时机
虚基类表(vbtable) 数据段(只读区) 整个类(所有对象共享一份) 编译期
虚基类指针(vbptr) 对象内存的头部 每个对象(每个对象有自己的 vbptr) 运行期(对象创建时)

        “多继承时,如果两个中间类继承同一个基类,派生类会同时继承基类的两份副本,形成‘菱形继承’,导致数据冗余和二义性。
        解决方法是使用虚继承:在中间类继承基类时加上 virtual 关键字,让基类在派生类中只保留一份共享拷贝。
        编译器通过虚基类指针(vbptr)和虚基类表(vbtable)来实现这一点,保证所有访问路径最终都指向同一块内存。”

        “在 C++ 中,多继承会引发‘菱形继承’问题,即派生类会从不同路径继承同一个基类的多个副本,导致数据冗余和访问二义性。C++ 通过虚继承(virtual inheritance) 来解决这个问题,其核心是在派生类中只保留基类的一份共享拷贝。”

步骤1:描述问题(菱形继承是什么)

“假设有两个类 Sheep 和 Tuo 都继承自同一个基类 Animal,然后 SheepTuo 又同时继承了 Sheep 和 Tuo。这样 SheepTuo 中就会包含两份 Animal 的成员,一份来自 Sheep,一份来自 Tuo,形成了‘菱形’结构。这就带来了两个问题:数据冗余(两份拷贝浪费内存)和访问二义性(访问时不知道该用哪一份)。”

步骤2:引出虚继承(如何解决)

“为了解决这个问题,C++ 引入了虚继承机制。只需在 Sheep 和 Tuo 继承 Animal 时加上 virtual 关键字——class Sheep : virtual public Animal {};——它们对 Animal 的继承就变成了‘共享继承’。编译器会保证在最终的派生类 SheepTuo 中只保留一份 Animal 的成员拷贝。”

步骤3:解释底层原理(加分项)

“虚继承的底层实现依赖于虚基类指针(vbptr) 和虚基类表(vbtable)。编译器会在派生类对象中插入一个隐藏的指针(vbptr),指向一个虚基类表。这张表记录了共享的基类成员在当前对象中的偏移量,确保无论通过 Sheep 还是 Tuo 的路径访问 Animal,最终都能定位到同一块内存。”

步骤4:描述虚继承的效果

“有了虚继承,最派生类 SheepTuo 中只有一个 Animal 子对象。通过 Sheep 路径修改 m_Age,通过 Tuo 路径访问到的也是同一个 m_Age,彻底消除了多份拷贝和二义性问题。”


“所以,虚继承的核心目的就是解决菱形继承带来的数据冗余和二义性问题。它通过共享基类副本,让最终派生类只包含一份基类成员。”

4.6.9 继承的本质

继承的本质:包含关系

class Animal {
public:
    int age;
    void eat() { cout << "eating" << endl; }
};

class Dog : public Animal {
public:
    void bark() { cout << "barking" << endl; }
};

当Dog 继承 Animal 时,在内存中:

Dog 对象的内存布局:
┌─────────────────────────────────────────────────────────────────┐
│  Animal 子对象(基类部分)                                    │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  age                                                   │    │
│  │  eat()                                                 │    │
│  └─────────────────────────────────────────────────────────┘    │
│  Dog 自己的成员                                               │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  bark()                                                │    │
│  └─────────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────────┘

继承的本质:派生类对象中“包含”了一个基类子对象。

访问方式的对比

1. 直接访问(通过子对象访问自己)

Dog d;
d.age = 10;   // ✅ 直接访问继承来的成员
d.eat();      // ✅ 直接调用继承来的函数
d.bark();     // ✅ 直接调用自己的函数
  • 为什么可以直接访问? 因为 age 在 Dog 对象中,Dog 对象包含 Animal 子对象,所以可以直接访问。

2.通过作用域访问(显式指明基类)

Dog d;
d.Animal::age = 10;   // ✅ 显式指定访问 Animal 子对象的 age
  • 什么时候需要用?

    • 当派生类有同名成员时(重名冲突)

    • 菱形继承时解决二义性

3. 同名成员时,作用域的作用

class Animal {
public:
    int age = 10;
};

class Dog : public Animal {
public:
    int age = 20;   // ★ 同名成员
};

int main() {
    Dog d;
    cout << d.age << endl;            // 20(默认访问 Dog 自己的 age)
    cout << d.Animal::age << endl;    // 10(指定访问 Animal 子对象的 age)
}

默认访问的是子类自己的成员,通过作用域可以访问基类的隐藏成员。

访问方式总结

访问方式 语法 访问的是 使用场景
直接访问 d.age 当前类自己的成员(或继承来的,如果没被隐藏) 大多数情况
作用域访问 d.Animal::age 显式指定 Animal 子对象中的成员 同名成员、二义性、菱形继承

        继承的本质是派生类对象中包含基类子对象。访问继承来的成员时,默认直接访问,同名冲突时可以用 基类名::成员 指定访问基类子对象中的成员。

4.7 多态

4.7.1 多态的基本概念

**多态是C++面向对象三大特性之一**

总结:

        多态是同一个行为(接口),在不同的对象上有不同的具体实现。它让父类的指针或引用可以指向子类的对象,调用同一个函数时,执行的是子类的版本。

“在 C++ 中,多态分为编译时多态(静态多态)和运行时多态(动态多态):”

多态类型 实现方式 绑定时机 典型例子
编译时多态 函数重载、运算符重载、模板 编译期 void print(int) 和 void print(double)
运行时多态 虚函数(virtual)+ 继承 运行期 基类指针指向派生类对象,调用虚函数

“我们通常说的多态,主要指运行时多态。”

多态的实现条件

“C++ 实现运行时多态需要三个条件:”

  1. 继承:派生类继承基类。

  2. 虚函数:基类中声明 virtual 函数,派生类进行 override(重写)。

  3. 基类指针或引用指向派生类对象:用基类类型的指针或引用来接收派生类对象。

多态的底层依赖于虚函数表(vtable)和虚指针(vptr)。每个带有虚函数的类都有一个虚函数表,存放所有虚函数的地址。每个对象有一个虚指针(vptr),指向该类的虚函数表。调用虚函数时,通过 vptr 找到 vtable,再找到对应函数的地址执行,这个过程叫做动态绑定(Dynamic Binding)。

这也意味着多态有一定的性能开销(通过指针间接寻址),但换来了极大的代码灵活性和可扩展性。

“多态的核心价值在于:”

  1. 代码复用:用一份代码(如 makeSound 函数)处理多种类型。

  2. 开闭原则:新增派生类时,原有的代码无需修改(对扩展开放,对修改关闭)。

  3. 面向接口编程:调用者只需要知道基类的接口,不需要了解具体的实现细节。

        多态是同一个接口在不同对象上表现出不同行为的特性。在 C++ 中,运行时多态通过虚函数和继承实现:基类声明 virtual 函数,派生类重写它,然后通过基类指针或引用指向派生类对象来调用。它的底层依赖虚函数表(vtable)和虚指针(vptr),在运行期动态绑定。多态的价值在于让代码更灵活、更易扩展,是面向对象编程的核心思想之一。

Q1:多态和重载有什么区别?

“重载是编译时多态,同名函数参数不同,编译器在编译期决定调用哪个。多态是运行时多态,函数名相同、参数相同,但根据对象的实际类型在运行期决定调用哪个。”

Q2:为什么需要 virtual 析构函数?

“如果基类的析构函数不是 virtual 的,通过基类指针删除派生类对象时,只会调用基类的析构函数,派生类的析构函数不会被调用,导致派生类中动态分配的资源无法释放,造成内存泄漏。将基类析构函数声明为 virtual,会先调用派生类析构函数,再调用基类析构函数,确保所有资源都被正确释放。”

Q3:virtual 函数和纯 virtual 函数的区别?

“虚函数有默认实现,派生类可以选择重写或不重写。纯虚函数没有实现(=0),派生类必须重写它。含有纯虚函数的类叫抽象类,不能实例化。”

特性 基类指针指向子类对象 虚基类 / 虚继承
核心目的 实现运行时多态(同一接口,不同行为) 解决菱形继承带来的二义性和数据冗余
关键语法 virtual 函数(成员函数前加 virtual virtual 继承(继承时在基类名前加 virtual
核心机制 虚函数表(vtable) + 虚指针(vptr) 虚基类表(vbtable) + 虚基类指针(vbptr)
解决什么问题 让代码更灵活、可扩展,面向抽象编程 保证公共基类在派生类中只有一份共享拷贝
什么时候用 需要根据对象实际类型执行不同操作时 出现“菱形继承”(如 SheepTuo 继承 Sheep 和 Tuo)时

        多态的底层机制是:每个类有自己的虚函数表(vtable),每个对象有自己的虚指针(vptr)。子类重写虚函数后,子类的 vtable 中函数地址被覆盖。通过基类指针调用时,实际通过对象的 vptr 找到子类的 vtable,执行子类的版本。

Derived d;      // ① 创建 Derived 对象,vptr 指向 Derived 的 vtable
Base* p = &d;   // ② 定义 Base* 指针 p,指向 Derived 对象的 Base 子对象部分

关键

  • p 是一个独立的指针变量,类型是 Base*

  • Base 是一个类型

  • p 指向的是 Derived 对象中的 Base 子对象,但 vptr 在整个对象开头,所以仍然是 Derived 的 vptr

Base* p = new Derived();
p->func();
步骤 发生了什么
new Derived() 创建 Derived 对象,vptr 指向 Derived 的 vtable
Base* p = ... 定义独立指针变量,存储 Derived 对象的地址
调用 p->func() 时,通过 p 找到对象的 vptr
vptr 指向 Derived 的 vtable,取出 Derived::func() 的地址
调用 Derived::func()

  

关键点

关键点 说明
vptr 在对象开头 无论指针类型是什么(Base* 或 Derived*),都能找到 vptr
vptr 指向派生类的 vtable 对象创建时,vptr 被初始化为指向实际类型的 vtable
基类指针指向基类子对象 Base* p = &d; 时,p 指向的是对象中的 Base 子对象部分
但 vptr 是共享的 基类子对象部分没有自己的 vptr,整个对象共享一个 vptr

总结

父类指针指向子类对象中的父类子对象,通过该对象共享的 vptr(指向子类的 vtable)找到重写后的虚函数地址,实现多态。

        父类指针可以指向子类对象,这是 C++ 语法允许的“向上转型”(Upcasting),也是多态能够实现的基础。

1. 内存视角:子类对象里“住着”一个父类子对象

当你用 public 方式继承时,子类对象的内存布局中,基类的那部分数据是完整地包含在派生类对象开头的

class Base { int a; };
class Derived : public Base { int b; };

创建 Derived 对象时,它的内存结构是:

[  Base部分 (int a)  ][  Derived部分 (int b)  ]
↑
这个地址既是 Derived 对象的起始地址,也是 Base 子对象的起始地址。

        因为子类对象的起始地址和它内部 Base 子对象的起始地址完全重合,所以把一个子类对象的地址赋值给一个父类指针,在内存上是天然合法且自洽的,编译器根本不需要做任何转换,直接拷贝地址即可。

2. 语法视角:C++ 的设计原则

“能用父类指针接收子类对象”是 C++ 多态的语法基础。

  • 如果你不允许这样赋值,那多态就无从谈起——因为只有用统一类型(基类)去接收,才能让一段函数(比如 void doWork(Base* p))处理不同的子类对象,然后针对不同对象调用不同版本的虚函数。

  • 反过来,如果你试图用子类指针去接收父类对象,编译器会报错,因为父类对象里没有子类的那部分数据,内存不完整。


3. 和“传引用”同理,本质是地址传递

Base* p = new Derived(); 传递的是地址,而不是把整个对象复制给 pp 只负责“指向”,并不负责“拥有”,所以它不需要知道子类的全部细节,只需要知道它指向的内存地址上有一块“Base 部分”就够了。

总结

用父类类型接收子类对象,是因为每个子类对象都天然包含了一个父类子对象,且内存地址重合;这种“向上转型”在内存层面是合法的,也是 C++ 实现多态的语法基础。


        父类有自己的虚函数表(vtable),但父类指针本身只是一个变量,存放在栈上(或作为成员变量),它指向的是某个对象(父类对象或子类对象)的起始地址。

1. 父类有自己的虚函数表和虚函数地址吗?

有。 每个包含虚函数的类都有自己的虚函数表(vtable),存放在只读数据段。

class Base {
public:
    virtual void func() { cout << "Base" << endl; }
};

内存中:

┌─────────────────────────────────────────────────────────────────┐
│  只读数据段                                                    │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  Base 的 vtable                                        │    │
│  │  ┌─────────────────────────────────────────────────┐    │    │
│  │  │  Base::func() 的地址                           │    │    │
│  │  └─────────────────────────────────────────────────┘    │    │
│  └─────────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────────┘
  • 即使没有创建任何对象,Base 的 vtable 也已经存在了(编译期生成)

  • 每个包含虚函数的类都有自己独立的 vtable

2. 父类指针存放在哪里?

父类指针是一个普通的指针变量,存放在栈上(或作为成员变量/全局变量)。

int main() {
    Base* p = new Base();
    //  ↑
    //  p 是一个局部变量,在栈上占 8 字节(64位系统)
    return 0;
}
栈内存:
┌─────────────────────────────────────────────────────────────────┐
│  p(8 字节)                                                   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  指向对象的地址(如 0x1000)                           │    │
│  └─────────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────────┘

3. 父类指针指向哪里?

父类指针指向某个对象的起始地址。

有两种情况:

情况1:指向父类对象

Base* p = new Base();   // p 指向 Base 对象的起始地址
堆内存(Base 对象):
┌─────────────────────────────────────────────────────────────────┐
│  0x1000: vptr(指向 Base 的 vtable)                          │
│  0x1008: Base 的成员变量                                      │
└─────────────────────────────────────────────────────────────────┘
          ↑
          │
    Base* p = 0x1000

情况2:指向子类对象

Derived d;          // 创建 Derived 对象
Base* p = &d;       // p 指向 Derived 对象的起始地址
栈/堆内存(Derived 对象):
┌─────────────────────────────────────────────────────────────────┐
│  0x1000: vptr(指向 Derived 的 vtable)                       │
│  0x1008: Base 的成员变量                                      │
│  0x100C: Derived 的成员变量                                   │
└─────────────────────────────────────────────────────────────────┘
          ↑
          │
    Base* p = 0x1000(指向对象开头)

        父类有自己的虚函数表(vtable),但父类指针本身只是一个变量,存放在栈上,指向某个对象的起始地址。它不知道自己指向的是父类对象还是子类对象,但对象里的 vptr 知道,所以调用虚函数时能正确找到对应的实现。 

        子类对象在构造时,其内部的 vptr 被初始化为指向子类自己的 vtable,而不是父类的。

关于多态,我的理解是这样的:

        先定义一个基类,在基类中声明一个虚函数。然后由不同的子类来继承这个基类。

        在底层机制中,每个包含虚函数的类都有自己的虚函数表(vtable),表中存放了该类所有虚函数的地址。每个对象在创建时,内部会生成一个虚指针(vptr),指向它所属类的虚函数表。

        子类继承基类后,重写(override) 继承来的虚函数,这时子类的虚函数表中对应函数的地址会被覆盖为子类自己的版本。

        由于子类对象中包含了一个基类子对象,所以用基类指针接收子类对象是合法的——基类指针指向的是子类对象中的基类子对象部分。

        当通过基类指针调用虚函数时,实际会通过对象内的 vptr 找到子类的虚函数表,进而调用子类重写后的虚函数版本,从而实现多态。

多态分为两类

* 静态多态: 函数重载 和 运算符重载属于静态多态,复用函数名
* 动态多态: 派生类和虚函数实现运行时多态

静态多态和动态多态区别:

* 静态多态的函数地址早绑定 - 编译阶段确定函数地址
* 动态多态的函数地址晚绑定 - 运行阶段确定函数地址

下面通过案例进行讲解多态

#include <iostream>
using namespace std;

// ===== 基类(动物) =====
class Animal
{
public:
    // ⭐ Speak 函数是虚函数
    // 函数前面加上 virtual 关键字,变成虚函数
    // 那么编译器在编译的时候就不能确定函数调用了
    // 而是在运行时根据实际对象类型来决定调用哪个函数
    virtual void speak()
    {
        cout << "动物在说话" << endl;
    }
};

// ===== 派生类:Cat(猫) =====
class Cat : public Animal
{
public:
    // ⭐ 重写(Override)父类的虚函数
    // 子类重写父类中的虚函数(函数名、参数、返回值都相同)
    void speak()
    {
        cout << "小猫在说话" << endl;
    }
};

// ===== 派生类:Dog(狗) =====
class Dog : public Animal
{
public:
    // ⭐ 重写父类的虚函数
    void speak()
    {
        cout << "小狗在说话" << endl;
    }
};

// ===== 多态使用 =====
// 父类指针或引用指向子类对象
// 如果函数地址在编译阶段就能确定 → 静态联编
// 如果函数地址在运行阶段才能确定 → 动态联编
void DoSpeak(Animal& animal)  // 父类引用接收子类对象
{
    animal.speak();  // 调用虚函数,运行时动态绑定
}

//
// 多态满足条件:
// 1、有继承关系
// 2、子类重写父类中的虚函数
//
// 多态使用:
// 父类指针或引用指向子类对象

void test01()
{
    Cat cat;
    DoSpeak(cat);  // 传入 Cat 对象 → 输出 "小猫在说话"

    Dog dog;
    DoSpeak(dog);  // 传入 Dog 对象 → 输出 "小狗在说话"
}

int main()
{
    test01();
    system("pause");
    return 0;
}
#include <iostream>
using namespace std;

//多态

//动物类
class Animal
{
public:
    //虚函数
    virtual void speak()
    {
        cout << "动物在说话:" << endl;
    }
};

//猫类
class Cat :public Animal {
public:
    //重写:函数返回值类型 函数名 参数列表 完全一致称为重写
    void speak()
    {
        cout << "小猫在说话:" << endl;
    }
};

//狗类
class Dog :public Animal {
public:
    //重写:函数返回值类型 函数名 参数列表 完全一致称为重写
    void speak()
    {
        cout << "小狗在说话:" << endl;
    }
}; 

//执行说话的函数
//地址早绑定 在编译阶段就确定函数地址
//如果想执行让猫说话 ,那么这个函数地址就不能提前绑定,需要在运行阶段绑定,地址晚绑定

//动态多态满足条件
//1、有继承关系
//2、子类重写父类的虚函数

//动态多态使用
//父类的指针或者引用 执行子类对象



void doSpeak(Animal& animal)
{
    animal.speak();
}

void test01()
{
    Cat cat;
    doSpeak(cat);

    Dog dog;
    doSpeak(dog);

}

int main()
{
    test01();
    system("pause");
    return 0;
}

多态通过虚函数实现,当用父类引用或指针指向子类对象时,调用虚函数会执行子类的版本,从而实现"同一个接口,不同实现"的效果

多态的原理剖析:

子类继承父类时,会继承父类的 vfptr,但重写虚函数后,子类的 vftable 中对应的函数地址被替换为子类自己的实现,从而实现多态

总结:

多态满足条件

* 有继承关系
* 子类重写父类中的虚函数

多态使用条件

* 父类指针或引用指向子类对象

重写:函数返回值类型 函数名 参数列表 完全一致称为重写

虚函数通过“虚函数表(vtable)”和“虚函数指针(vfptr)”实现动态绑定,让父类指针调用子类的函数。

总结

4.7.2 多态案例一-计算器类

案例描述:

分别利用普通写法和多态技术,设计实现两个操作数进行运算的计算器类

多态的优点:

* 代码组织结构清晰
* 可读性强
* 利于前期和后期的扩展以及维护

**示例:

#include <iostream>      // ① 包含输入输出流头文件,提供 cout、endl 等工具
#include <string>        // ② 包含字符串头文件,提供 string 类型支持
using namespace std;     // ③ 使用标准命名空间 std

// ============================================================
// ★ 方式1:普通实现(面向过程)
//    优点:代码简单直接
//    缺点:每次增加新运算都要修改 getResult 函数,违背开闭原则
// ============================================================
class Calculator {
public:
    // ④ ★ getResult:根据运算符字符串执行对应的运算
    //    参数:oper("+"、"-"、"*" 等)
    //    返回值:int 类型的计算结果
    int getResult(string oper)
    {
        if (oper == "+") {
            return m_Num1 + m_Num2;    // ⑤ 加法
        }
        else if (oper == "-") {
            return m_Num1 - m_Num2;    // ⑥ 减法
        }
        else if (oper == "*") {
            return m_Num1 * m_Num2;    // ⑦ 乘法
        }
        // ⑧ ⚠️ 如果要增加除法、取余等新运算,必须修改这个函数
        //    这违背了"开闭原则"(对扩展开放,对修改关闭)
    }
public:
    int m_Num1;   // ⑨ 第一个操作数
    int m_Num2;   // ⑩ 第二个操作数
};

// ⑪ 测试普通实现
void test01()
{
    Calculator c;           // ⑫ 创建计算器对象
    c.m_Num1 = 10;          // ⑬ 设置操作数1
    c.m_Num2 = 10;          // ⑭ 设置操作数2

    // ⑮ 分别测试加、减、乘
    cout << c.m_Num1 << " + " << c.m_Num2 << " = " << c.getResult("+") << endl;
    cout << c.m_Num1 << " - " << c.m_Num2 << " = " << c.getResult("-") << endl;
    cout << c.m_Num1 << " * " << c.m_Num2 << " = " << c.getResult("*") << endl;
}

// ============================================================
// ★ 方式2:多态实现(面向对象)
//    优点:符合开闭原则,新增运算只需新增子类,不需要修改已有代码
//    缺点:代码量稍多,需要理解继承和多态
// ============================================================

// ⑯ ★ 抽象计算器类(基类)
//    定义接口:getResult() 是虚函数,由子类重写
class AbstractCalculator
{
public:
    // ⑰ ★ 虚函数:子类必须重写
    //    如果子类没有重写,调用时会执行基类版本(返回 0)
    virtual int getResult()
    {
        return 0;
    }

    int m_Num1;   // ⑱ 操作数1(子类继承)
    int m_Num2;   // ⑲ 操作数2(子类继承)
};

// ⑳ ★ 加法计算器(子类)
class AddCalculator : public AbstractCalculator
{
public:
    // ㉑ ★ 重写父类的虚函数
    //    添加 override 关键字让编译器检查是否正确重写
    int getResult() override
    {
        return m_Num1 + m_Num2;   // ㉒ 加法实现
    }
};

// ㉓ ★ 减法计算器(子类)
class SubCalculator : public AbstractCalculator
{
public:
    int getResult() override
    {
        return m_Num1 - m_Num2;   // ㉔ 减法实现
    }
};

// ㉕ ★ 乘法计算器(子类)
class MulCalculator : public AbstractCalculator
{
public:
    int getResult() override
    {
        return m_Num1 * m_Num2;   // ㉖ 乘法实现
    }
};

// ㉗ ★ 测试多态实现
void test02()
{
    // ============================================================
    // ★ 加法
    // ============================================================
    AbstractCalculator* abc = new AddCalculator;   // ㉘ ★ 父类指针指向子类对象
    abc->m_Num1 = 10;                               // ㉙ 设置操作数1
    abc->m_Num2 = 10;                               // ㉚ 设置操作数2
    cout << abc->m_Num1 << " + " << abc->m_Num2 << " = " << abc->getResult() << endl;
    delete abc;                                     // ㉛ ★ 释放堆内存(防止内存泄漏)

    // ============================================================
    // ★ 减法
    // ============================================================
    abc = new SubCalculator;   // ㉜ ★ 父类指针重新指向子类对象
    abc->m_Num1 = 10;
    abc->m_Num2 = 10;
    cout << abc->m_Num1 << " - " << abc->m_Num2 << " = " << abc->getResult() << endl;
    delete abc;

    // ============================================================
    // ★ 乘法
    // ============================================================
    abc = new MulCalculator;   // ㉝ ★ 父类指针重新指向子类对象
    abc->m_Num1 = 10;
    abc->m_Num2 = 10;
    cout << abc->m_Num1 << " * " << abc->m_Num2 << " = " << abc->getResult() << endl;
    delete abc;
}

// ============================================================
// ㉞ 主函数(程序入口)
// ============================================================
int main()
{
    // test01();   // ㉟ 普通实现(被注释掉)
    test02();      // ㊱ 多态实现(当前执行)
    system("pause");
    return 0;
}
#include <iostream>      // ① 包含输入输出流头文件,提供 cout、endl 等工具
#include <string>        // ② 包含字符串头文件,提供 string 类型支持
using namespace std;     // ③ 使用标准命名空间 std

// ============================================================
// ★ 计算器类(普通写法,不使用多态)
// ============================================================
class Calculator
{
public:
    // ④ ★ getResult 函数:根据运算符执行对应运算
    //    参数:oper(运算符字符串),如 "+"、"-"、"*"
    //    返回值:int 类型的计算结果
    int getResult(string oper)
    {
        // ⑤ ★ switch 判断运算符(取字符串的第一个字符)
        //    只比较单个字符,效率比 if-else 高
        switch (oper[0])
        {
        case '+':              // ⑥ 加法
            return m_Num1 + m_Num2;
        case '-':              // ⑦ 减法
            return m_Num1 - m_Num2;
        case '*':              // ⑧ 乘法
            return m_Num1 * m_Num2;
        default:               // ⑨ 不支持的运算符
            cout << "暂不支持该运算符!" << endl;
            return 0;
        }
    }

public:
    int m_Num1;   // ⑩ 第一个操作数
    int m_Num2;   // ⑪ 第二个操作数
};

// ============================================================
// ⑫ 测试函数
// ============================================================
void test01()
{
    Calculator cc;          // ⑬ 创建计算器对象
    cc.m_Num1 = 10;         // ⑭ 设置第一个操作数为 10
    cc.m_Num2 = 10;         // ⑮ 设置第二个操作数为 10

    // ⑯ ★ 分别测试加、减、乘三种运算
    cout << cc.m_Num1 << "+" << cc.m_Num2 << "=" << cc.getResult("+") << endl;
    cout << cc.m_Num1 << "-" << cc.m_Num2 << "=" << cc.getResult("-") << endl;
    cout << cc.m_Num1 << "*" << cc.m_Num2 << "=" << cc.getResult("*") << endl;
}

// ============================================================
// ⑰ 主函数(程序入口)
// ============================================================
int main()
{
    test01();                // ⑱ 调用测试函数
    system("pause");         // ⑲ 暂停控制台窗口
    return 0;                // ⑳ 程序正常退出
}

总结:C++开发提倡利用多态设计程序架构,因为多态优点很多

4.7.3 纯虚函数和抽象类

在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容

因此可以将虚函数改为**纯虚函数**

纯虚函数语法:`virtual 返回值类型 函数名 (参数列表)= 0 ;`

当类中有了纯虚函数,这个类也称为抽象类

**抽象类特点**:

* 无法实例化对象
* 子类必须重写抽象类中的纯虚函数,否则也属于抽象类

**示例:

#include <iostream>
using namespace std;

// ===== 基类(抽象类) =====
class Base
{
public:
    // ⭐ 纯虚函数:在虚函数后面加 = 0
    // 类中只要有一个纯虚函数,这个类就成为抽象类
    // 抽象类无法实例化对象(不能 new Base)
    // 子类必须重写父类中的纯虚函数,否则子类也属于抽象类
    virtual void func() = 0;  // = 0 表示纯虚函数,没有实现
};

// ===== 子类 =====
class Son : public Base
{
public:
    // ⭐ 子类必须重写父类的纯虚函数
    // 如果不重写,Son 也是抽象类,无法实例化
    virtual void func() override
    {
        cout << "func调用" << endl;
    };
};

void test01()
{
    Base* base = NULL;      // 父类指针可以定义
    // base = new Base;     // ❌ 错误!抽象类无法实例化对象
    base = new Son;         // ✅ 父类指针指向子类对象
    base->func();           // 调用子类的 func()
    delete base;            // 记得销毁
}

int main()
{
    test01();
    system("pause");
    return 0;
}

4.7.4 多态案例二-制作饮品

**案例描述:**

制作饮品的大致流程为:煮水 - 冲泡 - 倒入杯中 - 加入辅料

利用多态技术实现本案例,提供抽象制作饮品基类,提供子类制作咖啡和茶叶

示例:

#include <iostream>
using namespace std;

// ============================================================
// 抽象基类:定义制作饮品的流程模板
// ============================================================
class AbstractDrinking {
public:
    // ----- 纯虚函数:子类必须实现 -----
    
    // 1. 烧水(子类决定用什么水)
    virtual void Boil() = 0;
    
    // 2. 冲泡(子类决定冲泡什么)
    virtual void Brew() = 0;
    
    // 3. 倒入杯中(子类决定倒入什么容器)
    virtual void PourInCup() = 0;
    
    // 4. 加入辅料(子类决定加什么)
    virtual void PutSomething() = 0;

    // ⭐ 模板方法:定义固定的制作流程
    // 这个流程对所有饮品都适用,但每个步骤由子类具体实现
    void MakeDrink() {
        Boil();         // 第1步:烧水
        Brew();         // 第2步:冲泡
        PourInCup();    // 第3步:倒入杯中
        PutSomething(); // 第4步:加入辅料
    }
};

// ============================================================
// 咖啡类:实现制作咖啡的具体步骤
// ============================================================
class Coffee : public AbstractDrinking {
public:
    // 烧水:用农夫山泉
    //override 让编译器帮你检查:这个函数是否真的在重写父类的虚函数
    //如果写错了(函数名、参数、返回值不匹配),编译器会报错,而不是静默地创建一个新函数。
    void Boil() override {
        cout << "煮农夫山泉!" << endl;
    }
    
    // 冲泡:冲泡咖啡粉
    void Brew() override {
        cout << "冲泡咖啡!" << endl;
    }
    
    // 倒入杯中:倒入咖啡杯
    void PourInCup() override {
        cout << "将咖啡倒入杯中!" << endl;
    }
    
    // 加入辅料:加牛奶
    void PutSomething() override {
        cout << "加入牛奶!" << endl;
    }
};

// ============================================================
// 茶类:实现制作茶的具体步骤
// ============================================================
class Tea : public AbstractDrinking {
public:
    // 烧水:用自来水
    void Boil() override {
        cout << "煮自来水!" << endl;
    }
    
    // 冲泡:冲泡茶叶
    void Brew() override {
        cout << "冲泡茶叶!" << endl;
    }
    
    // 倒入杯中:倒入茶杯
    void PourInCup() override {
        cout << "将茶水倒入杯中!" << endl;
    }
    
    // 加入辅料:加枸杞
    void PutSomething() override {
        cout << "加入枸杞!" << endl;
    }
};

// ============================================================
// 业务函数:制作饮品
// ============================================================
void DoWork(AbstractDrinking* drink) {
    drink->MakeDrink();  // 执行模板流程
    delete drink;        // 释放内存
}

void test01() {
    DoWork(new Coffee);  // 制作咖啡
    cout << "--------------" << endl;
    DoWork(new Tea);     // 制作茶
}

int main() {
    test01();
    system("pause");
    return 0;
}

4.7.5 虚析构和纯虚析构

多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码

解决方式:将父类中的析构函数改为**虚析构**或者**纯虚析构**

虚析构和纯虚析构共性:

* 可以解决父类指针释放子类对象
* 都需要有具体的函数实现

虚析构和纯虚析构区别:

* 如果是纯虚析构,该类属于抽象类,无法实例化对象

        虚析构通过将析构函数放入虚函数表,实现动态绑定,让 delete 父类指针时能够根据对象的实际类型调用正确的析构函数(先子类后父类),从而保证所有资源都被正确释放,解决“析构不干净”的问题。

虚析构语法:

`virtual ~类名(){}`

纯虚析构语法:

` virtual ~类名() = 0;`

`类名::~类名(){}`

**示例:

#include <iostream>      // ① 包含输入输出流头文件
#include <string>        // ② 包含字符串头文件
using namespace std;     // ③ 使用标准命名空间

// ============================================================
// ★ 基类 Animal
// ============================================================
class Animal {
public:
    // ④ 构造函数
    Animal()
    {
        cout << "Animal 构造函数调用!" << endl;
    }

    // ⑤ ★ 纯虚函数:让 Animal 成为抽象类
    //    子类必须重写此函数,否则子类也是抽象类
    virtual void Speak() = 0;

    // ============================================================
    // 方案1:虚析构函数(注释掉的部分)
    // ============================================================
    // virtual ~Animal()
    // {
    //     cout << "Animal虚析构函数调用!" << endl;
    // }

    // ============================================================
    // 方案2:纯虚析构函数(当前使用的) ★ 核心知识点
    // ============================================================
    // ⑥ ★ 纯虚析构函数:声明为 = 0
    //    作用:让 Animal 成为抽象类(不能被实例化)
    //    注意:纯虚析构函数必须在类外提供实现!
    virtual ~Animal() = 0;
};

// ⑦ ★ 纯虚析构函数的类外实现(必须!)
//    即使声明为纯虚,也必须提供实现体
//    因为析构函数最终一定会被调用
Animal::~Animal()
{
    cout << "Animal 纯虚析构函数调用!" << endl;
}

// ⑧ ★ 包含纯虚析构函数的类,和包含普通纯虚函数的类一样
//     都是抽象类,不能被实例化

// ============================================================
// ★ 子类 Cat
// ============================================================
class Cat : public Animal {
public:
    // ⑨ 构造函数:在堆区分配内存
    Cat(string name)
    {
        cout << "Cat构造函数调用!" << endl;
        m_Name = new string(name);  // ⑩ ★ 在堆区分配内存
    }

    // ⑪ ★ 重写纯虚函数 Speak
    virtual void Speak() override
    {
        cout << *m_Name << "小猫在说话!" << endl;
    }

    // ⑫ ★ 析构函数:释放堆区内存
    //    如果不调用,会造成内存泄漏
    ~Cat()
    {
        cout << "Cat析构函数调用!" << endl;
        if (this->m_Name != NULL) {
            delete m_Name;   // ⑬ 释放堆区内存
            m_Name = NULL;
        }
    }

public:
    string* m_Name;  // ⑭ 指向堆区的指针(需要手动释放)
};

// ============================================================
// ⑮ ★ 测试函数:演示虚析构的必要性
// ============================================================
void test01()
{
    // ⑯ 父类指针指向子类对象
    Animal* animal = new Cat("Tom");
    animal->Speak();   // ⑰ 调用子类的 Speak(多态)

    // ⑱ ★ 通过父类指针释放子类对象
    //    问题:如果基类析构函数不是虚函数,子类析构函数不会被调用
    //    后果:子类堆区内存无法释放 → 内存泄漏!
    //    
    //    ✅ 解决方法:给基类增加虚析构函数或纯虚析构函数
    //    虚析构函数用来解决通过父类指针释放子类对象的问题
    delete animal;
    // ⑲ 执行顺序:
    //    ① Cat 析构函数(释放 m_Name 堆内存)
    //    ② Animal 纯虚析构函数(父类析构)
}

int main()
{
    test01();
    system("pause");
    return 0;
}

总结:

​ 1. 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象

​ 2. 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构

​ 3. 拥有纯虚析构函数的类也属于抽象类

        为了解决多态下的资源泄漏问题,将父类析构函数设为虚析构(可实例化)或纯虚析构(抽象类,需类外实现)。两者都能确保 delete 父类指针时,子类和父类的析构函数都被正确调用。

        delete掉的是指针指向的堆区内存。指针自己没被delete。

纯虚析构函数的作用

作用 说明
让类成为抽象类 阻止实例化 Animal 对象
保证多态析构 通过父类指针删除子类对象时,正确调用子类析构
释放子类资源 子类析构函数被执行,释放堆区内存

        父类指针无法释放子类对象的核心原因是:默认情况下,通过父类指针调用析构函数时,编译器只知道这个指针是父类类型,所以只会调用父类的析构函数,不会调用子类的析构函数。子类的析构函数没有被执行,导致子类中堆区分配的资源没有被释放,造成内存泄漏。

1. 没有虚析构时发生了什么?

class Animal {
public:
    ~Animal() { cout << "Animal 析构" << endl; }
};

class Cat : public Animal {
public:
    string* m_Name;
    ~Cat() { 
        delete m_Name;   // ★ 释放堆内存
        cout << "Cat 析构" << endl; 
    }
};

int main() {
    Animal* p = new Cat();
    delete p;   // ★ 只调用 Animal 的析构函数
    return 0;
}

问题Cat 的析构函数没有被调用,m_Name 指向的堆内存没有被释放 → 内存泄漏

2. 为什么只调用了 Animal 的析构函数?

┌─────────────────────────────────────────────────────────────────┐
│  编译器的决定(静态绑定)                                      │
│                                                                 │
│  Animal* p = new Cat();                                        │
│  delete p;                                                     │
│      │                                                         │
│      ▼                                                         │
│  编译器看到的:p 的类型是 Animal*                              │
│  编译器决定的:调用 Animal 的析构函数                          │
│                                                                 │
│  ★ 因为析构函数不是虚函数,编译器在编译时就确定了调用哪个版本  │
└─────────────────────────────────────────────────────────────────┘

加了虚析构之后发生了什么?

class Animal {
public:
    virtual ~Animal() { cout << "Animal 虚析构" << endl; }
};

class Cat : public Animal {
public:
    string* m_Name;
    ~Cat() { 
        delete m_Name;   // ★ 成功执行
        cout << "Cat 析构" << endl; 
    }
};

int main() {
    Animal* p = new Cat();
    delete p;   // ★ 先调用 Cat 析构,再调用 Animal 析构
    return 0;
}

为什么虚析构能解决?

┌─────────────────────────────────────────────────────────────────┐
│  运行时的决定(动态绑定)                                      │
│                                                                 │
│  Animal* p = new Cat();                                        │
│  delete p;                                                     │
│      │                                                         │
│      ▼                                                         │
│  运行时检查:p 实际指向的是 Cat 对象                           │
│  运行时决定:先调用 Cat 的析构函数                             │
│             再调用 Animal 的析构函数                           │
│                                                                 │
│  ★ 因为析构函数是虚函数,在运行时根据实际对象类型决定调用版本  │
└─────────────────────────────────────────────────────────────────┘
没有虚析构(静态绑定):
┌─────────────────────────────────────────────────────────────────┐
│  Animal* p = new Cat();                                        │
│  delete p;                                                     │
│      │                                                         │
│      ▼                                                         │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  Cat 对象                                               │    │
│  │  ┌─────────────────────────────────────────────────┐    │    │
│  │  │  Animal 部分                                    │    │    │
│  │  └─────────────────────────────────────────────────┘    │    │
│  │  ┌─────────────────────────────────────────────────┐    │    │
│  │  │  Cat 自己的成员(m_Name 指向堆内存)            │    │    │
│  │  └─────────────────────────────────────────────────┘    │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                 │
│  delete p → 只释放 Animal 部分                                  │
│  ★ Cat 部分的内存没有被释放 → 内存泄漏!                        │
└─────────────────────────────────────────────────────────────────┘

有虚析构(动态绑定):
┌─────────────────────────────────────────────────────────────────┐
│  Animal* p = new Cat();                                        │
│  delete p;                                                     │
│      │                                                         │
│      ▼                                                         │
│  ① 通过 vptr 找到 Cat 的虚函数表                              │
│  ② 调用 Cat 的析构函数(释放 m_Name)                         │
│  ③ 调用 Animal 的析构函数                                     │
│                                                                 │
│  ★ 所有内存都被正确释放!                                      │
└─────────────────────────────────────────────────────────────────┘

总结

情况 析构调用顺序 子类资源是否释放 结果
没有虚析构 只调用父类析构 ❌ 未释放 内存泄漏
有虚析构 先子类、后父类 ✅ 释放 安全
有纯虚析构 先子类、后父类 ✅ 释放 安全 + 抽象类

        没有虚析构时,通过父类指针删除子类对象只会调用父类析构,子类析构被跳过,导致子类堆内存无法释放。虚析构让编译器在运行时根据对象实际类型调用正确的析构顺序,先子类后父类,保证所有资源都被释放。

        虚析构的原理是:将析构函数放入虚函数表,通过 vptr 在运行时动态绑定,确保 delete 父类指针时先调用子类析构函数释放子类资源,再调用父类析构函数释放父类资源,保证完整的析构链。

关于普通析构和虚析构的区别

        普通析构是静态绑定的,父类指针 delete 时只调用父类析构函数,子类析构不会被调用,子类堆内存无法释放,造成内存泄漏。

        虚析构是动态绑定的。父类析构函数被声明为 virtual 后,它的地址会放入虚函数表(vtable)。当通过父类指针 delete 子类对象时,编译器通过对象的虚函数表指针(vptr)找到子类的析构函数并执行,释放子类资源,然后再自动调用父类析构函数,释放父类资源。完整的析构链是:先子类、后父类。

普通析构 vs 虚析构对比

对比项 普通析构 虚析构
绑定方式 静态绑定(编译期) 动态绑定(运行期)
通过父类指针删除子类对象 只调父类析构 ❌ 先调子类、再调父类 ✅
子类堆资源释放 ❌ 不释放 ✅ 释放
是否在 vtable 中 ❌ 不在 ✅ 在
多态析构 ❌ 不支持 ✅ 支持

关键点

要点 说明
虚析构的本质 将析构函数放入虚函数表,实现动态绑定
执行顺序 先子类析构 → 后父类析构(和构造顺序相反)
为什么先子类后父类 子类析构可能依赖父类成员,所以父类必须最后释放
纯虚析构 同样实现动态绑定,同时让类抽象化

4.7.6 多态案例三-电脑组装

**案例描述:**

电脑主要组成部件为 CPU(用于计算),显卡(用于显示),内存条(用于存储)

将每个零件封装出抽象基类,并且提供不同的厂商生产不同的零件,例如Intel厂商和Lenovo厂商

创建电脑类提供让电脑工作的函数,并且调用每个零件工作的接口

测试时组装三台不同的电脑进行工作

**示例:

#include<iostream>      // ① 标准输入输出流头文件
using namespace std;     // ② 使用标准命名空间

// ============================================================
// ★ 抽象 CPU 类(定义接口规范)
// ============================================================
class CPU
{
public:
    // ③ ★ 纯虚函数:定义计算能力的接口
    //    = 0 表示纯虚函数,让 CPU 类成为抽象类
    //    所有 CPU 子类必须实现 calculate()
    virtual void calculate() = 0;
};

// ============================================================
// ★ 抽象显卡类(定义接口规范)
// ============================================================
class VideoCard
{
public:
    // ④ ★ 纯虚函数:定义显示能力的接口
    virtual void display() = 0;
};

// ============================================================
// ★ 抽象内存条类(定义接口规范)
// ============================================================
class Memory
{
public:
    // ⑤ ★ 纯虚函数:定义存储能力的接口
    virtual void storage() = 0;
};

// ============================================================
// ★ 电脑类:组装和运行所有零件
//    体现"组合"设计模式:Computer 包含 CPU、显卡、内存
// ============================================================
class Computer
{
public:
    // ⑥ 构造函数:接收 CPU、显卡、内存的指针
    //    参数是父类指针,可以接收任何子类对象(多态的核心!)
    Computer(CPU* cpu, VideoCard* vc, Memory* mem)
    {
        m_cpu = cpu;   // ⑦ 保存 CPU 指针
        m_vc = vc;     // ⑧ 保存显卡指针
        m_mem = mem;   // ⑨ 保存内存条指针
    }

    // ⑩ ★ work 函数:让电脑开始工作
    //    通过父类指针调用虚函数,实现多态
    //    运行时动态绑定到具体厂商的实现
    void work()
    {
        m_cpu->calculate();   // ⑪ 调用 CPU 的计算功能
        m_vc->display();      // ⑫ 调用显卡的显示功能
        m_mem->storage();     // ⑬ 调用内存条的存储功能
    }

    // ⑭ ★ 析构函数:释放所有零件
    //    通过父类指针释放子类对象,基类析构必须是虚函数
    //    不加 virtual,子类析构不会被调用 → 内存泄漏!
    ~Computer()
    {
        if (m_cpu != NULL) {
            delete m_cpu;    // ⑮ 释放 CPU
            m_cpu = NULL;
        }
        if (m_vc != NULL) {
            delete m_vc;     // ⑯ 释放显卡
            m_vc = NULL;
        }
        if (m_mem != NULL) {
            delete m_mem;    // ⑰ 释放内存条
            m_mem = NULL;
        }
    }

private:
    CPU* m_cpu;          // ⑱ CPU 零件指针(父类指针)
    VideoCard* m_vc;     // ⑲ 显卡零件指针(父类指针)
    Memory* m_mem;       // ⑳ 内存条零件指针(父类指针)
};

// ============================================================
// ★ Intel 系列配件
// ============================================================
class IntelCPU : public CPU
{
public:
    // ㉑ ★ 重写纯虚函数,实现 Intel CPU 的计算
    virtual void calculate() override
    {
        cout << "Intel的CPU开始计算了!" << endl;
    }
};

class IntelVideoCard : public VideoCard
{
public:
    // ㉒ ★ 重写纯虚函数,实现 Intel 显卡的显示
    virtual void display() override
    {
        cout << "Intel的显卡开始显示了!" << endl;
    }
};

class IntelMemory : public Memory
{
public:
    // ㉓ ★ 重写纯虚函数,实现 Intel 内存条的存储
    virtual void storage() override
    {
        cout << "Intel的内存条开始存储了!" << endl;
    }
};

// ============================================================
// ★ Lenovo 系列配件
// ============================================================
class LenovoCPU : public CPU
{
public:
    virtual void calculate() override
    {
        cout << "Lenovo的CPU开始计算了!" << endl;
    }
};

class LenovoVideoCard : public VideoCard
{
public:
    virtual void display() override
    {
        cout << "Lenovo的显卡开始显示了!" << endl;
    }
};

class LenovoMemory : public Memory
{
public:
    virtual void storage() override
    {
        cout << "Lenovo的内存条开始存储了!" << endl;
    }
};

// ============================================================
// ★ 测试函数
// ============================================================
void test01()
{
    // ---- 第一台:全部 Intel ----
    CPU* intelCpu = new IntelCPU;
    VideoCard* intelCard = new IntelVideoCard;
    Memory* intelMem = new IntelMemory;

    cout << "第一台电脑开始工作:" << endl;
    Computer* computer1 = new Computer(intelCpu, intelCard, intelMem);
    computer1->work();   // ㉔ ★ 多态调用:执行 Intel 系列
    delete computer1;    // ㉕ ★ 虚析构保证完整释放

    cout << "-----------------------" << endl;
    cout << "第二台电脑开始工作:" << endl;

    // ---- 第二台:全部 Lenovo ----
    Computer* computer2 = new Computer(new LenovoCPU, new LenovoVideoCard, new LenovoMemory);
    computer2->work();   // ㉖ ★ 多态调用:执行 Lenovo 系列
    delete computer2;

    cout << "-----------------------" << endl;
    cout << "第三台电脑开始工作:" << endl;

    // ---- 第三台:混搭(Lenovo CPU + Intel 显卡 + Lenovo 内存) ----
    // ㉗ ★ 体现多态灵活性:自由组合不同厂商零件
    Computer* computer3 = new Computer(new LenovoCPU, new IntelVideoCard, new LenovoMemory);
    computer3->work();
    delete computer3;
}

int main()
{
    test01();
    system("pause");
    return 0;
}

核心知识点

1. 抽象类(纯虚函数)

纯虚函数 作用
CPU virtual void calculate() = 0; 定义 CPU 接口规范
VideoCard virtual void display() = 0; 定义显卡接口规范
Memory virtual void storage() = 0; 定义内存条接口规范

作用:这些类不能被实例化,只能被继承,强制子类实现特定功能。

2. 多态(父类指针指向子类对象)

CPU* cpu = new IntelCPU;   // 父类指针指向子类对象
cpu->calculate();          // ★ 调用 IntelCPU::calculate()
关键点 说明
父类指针 CPU*VideoCard*Memory*
子类对象 IntelCPULenovoCPU 等
动态绑定 运行时根据实际对象类型调用对应函数

3. 组合设计模式

class Computer {
    CPU* m_cpu;          // ★ 组合:包含其他对象
    VideoCard* m_vc;
    Memory* m_mem;
};

优势:灵活组装,可以自由搭配不同厂商的零件。

4. 虚析构的作用

如果 CPUVideoCardMemory 的析构函数不是虚的,delete m_cpu 只会释放父类部分,子类堆资源无法释放 → 内存泄漏。

总结

这段代码演示了“抽象类 + 多态 + 组合”的经典用法:通过纯虚函数定义接口规范,子类实现具体功能,Computer 类通过父类指针组合各种零件,利用多态实现灵活组装,虚析构保证资源完整释放。

class Computer {
    CPU* m_cpu;          // ★ 组合:包含其他对象
    VideoCard* m_vc;
    Memory* m_mem;
};

        “组合”(Composition)设计原则:一个类可以包含其他类的对象(或指针),作为自己的成员变量。Computer 类通过包含 CPU*VideoCard*Memory* 这些指针,实现了“电脑由多个零件组成”的语义。

1. 组合的本质:一个类拥有另一个类

class Computer {
    CPU* m_cpu;          // ★ Computer 拥有(包含)一个 CPU
    VideoCard* m_vc;     // ★ Computer 拥有一个显卡
    Memory* m_mem;       // ★ Computer 拥有一个内存条
};
关系 说明
组合(Composition) 一个类包含另一个类的对象作为成员
“has-a”关系 Computer 有一个 CPU有一个 VideoCard有一个 Memory
对比继承 继承是“is-a”(Cat 是 Animal),组合是“has-a”(Computer 有 CPU

2. 为什么用指针而不是对象?

// 方式1:直接用对象(不灵活)
class Computer {
    IntelCPU m_cpu;      // ❌ 只能使用 Intel CPU,不能换
};

// 方式2:用指针(灵活)
class Computer {
    CPU* m_cpu;          // ✅ 可以指向 IntelCPU、LenovoCPU、任何子类
};
使用对象 使用指针
类型固定,不能换 类型灵活,可以指向任何子类
编译时就确定 运行时动态绑定(多态)
不支持多态 ✅ 支持多态

3. 指针在这里的作用

Computer* computer = new Computer(new LenovoCPU, new IntelVideoCard, new LenovoMemory);
//                                 ↑                ↑                  ↑
//                            传入的都是子类对象,但通过父类指针接收

工作原理

┌─────────────────────────────────────────────────────────────────┐
│  Computer 对象                                                 │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  m_cpu(CPU*) → 指向 LenovoCPU 对象                   │    │
│  │  m_vc(VideoCard*)→ 指向 IntelVideoCard 对象          │    │
│  │  m_mem(Memory*)→ 指向 LenovoMemory 对象              │    │
│  └─────────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────────┘
  • m_cpu 是 CPU* 类型,可以指向 IntelCPULenovoCPU 等任何 CPU 子类

  • 调用 m_cpu->calculate() 时,根据实际对象类型执行对应的函数(多态)

4. 为什么能够“自由组合”?

// 1. Intel 全家桶
Computer* c1 = new Computer(new IntelCPU, new IntelVideoCard, new IntelMemory);

// 2. Lenovo 全家桶
Computer* c2 = new Computer(new LenovoCPU, new LenovoVideoCard, new LenovoMemory);

// 3. 混搭(Lenovo CPU + Intel 显卡 + Lenovo 内存)
Computer* c3 = new Computer(new LenovoCPU, new IntelVideoCard, new LenovoMemory);

原因Computer 不关心具体的零件类型,只要求传入的是 CPU*VideoCard*Memory* 类型。任何满足这个接口的零件都可以被组装进去。

📊 组合 vs 继承

对比项 继承(is-a) 组合(has-a)
关系 子类是父类的一种 一个类包含另一个类
示例 Cat 继承 Animal Computer 包含 CPU
代码 class Cat : public Animal CPU* m_cpu;
优点 代码复用,多态 灵活,低耦合
灵活性 较低(继承链固定) 高(可以自由组合)

总结

Computer 类可以通过包含 CPU*VideoCard*Memory* 这些父类指针,实现“电脑由零件组成”的语义。因为使用了指针和多态,可以自由组合不同厂商的零件,体现了“组合优于继承”的设计原则。

5 文件操作

程序运行时产生的数据都属于临时数据,程序一旦运行结束都会被释放

通过文件可以将数据持久化

C++中对文件操作需要包含头文件 <fstream>

文件类型分为两种:

1. **文本文件** - 文件以文本的**ASCII码**形式存储在计算机中
2. **二进制文件** - 文件以文本的**二进制**形式存储在计算机中,用户一般不能直接读懂它们

操作文件的三大类:

1. ofstream:写操作
2. ifstream: 读操作
3. fstream : 读写操作

5.1文本文件

5.1.1写文件

写文件步骤如下:

1. 包含头文件 #include <fstream>
2. 创建流对象ofstream ofs;
3. 打开文件ofs.open("文件路径",打开方式);
4. 写数据ofs << "写入的数据";
5. 关闭文件ofs.close();​

ios = Input/Output Stream(输入/输出流)的缩写,是 C++ 中所有流类的基类

文件打开方式:

打开方式 解释
ios::in 为读文件而打开文件
ios::out 为写文件而打开文件
ios::ate 初始位置:文件尾
ios::app 追加方式写文件
ios::trunc 如果文件存在先删除,再创建
ios::binary 二进制方式

**注意:** 文件打开方式可以配合使用,利用|操作符

**例如:**用二进制方式写文件 ios::binary | ios:: out

**示例:

#include <fstream>      // 引入文件操作头文件,提供 ofstream、ifstream 等
#include <iostream>     // 输入输出流,提供 cout、endl
using namespace std;    // 使用标准命名空间

void test01()
{
    // ===== 1. 创建文件输出流对象 =====
    // ofstream = output file stream(输出文件流)
    // 用于向文件中写入数据
    ofstream ofs;

    // ===== 2. 打开文件 =====
    // open(文件名, 打开模式)
    // "test.txt" - 文件名(相对路径,在程序同目录下生成)
    // ios::out   - 写入模式(默认会清空文件)
    // 如果文件不存在,会自动创建
    // 如果文件存在,会清空原有内容
    ofs.open("test.txt", ios::out);

    // ===== 3. 写入数据 =====
    // 使用 << 运算符向文件写入数据(和 cout 用法一样)
    // endl 会插入换行符并刷新缓冲区
    ofs << "姓名:张三" << endl;
    ofs << "性别:男" << endl;
    ofs << "年龄:18" << endl;

    // ===== 4. 关闭文件 =====
    // 释放文件资源,确保数据完整写入磁盘
    // 如果不关闭,程序结束时也会自动关闭,但手动关闭是好习惯
    ofs.close();

    // ⚠️ 如果不调用 close(),析构函数会自动关闭
    // 但手动 close() 可以:
    // 1. 立即保存数据(防止程序崩溃丢失数据)
    // 2. 释放文件资源供其他程序使用
    // 3. 明确表示写入完成
}

int main()
{
    test01();          // 调用测试函数
    system("pause");   // 暂停程序
    return 0;          // 程序正常结束
}

总结:

* 文件操作必须包含头文件 fstream
* 读文件可以利用 ofstream ,或者fstream类
* 打开文件时候需要指定操作文件的路径,以及打开方式
* 利用<<可以向文件中写数据
* 操作完毕,要关闭文件

5.1.2读文件

读文件与写文件步骤相似,但是读取方式相对于比较多

读文件步骤如下:

1. 包含头文件 #include <fstream>
2. 创建流对象ifstream ifs;
3. 打开文件并判断文件是否打开成功ifs.open("文件路径",打开方式);
4. 读数据四种方式读取
5. 关闭文件ifs.close();

**示例:

#include <fstream>      // 文件操作头文件(ifstream、ofstream)
#include <iostream>     // 输入输出流(cout、endl)
#include <string>       // 字符串库(string)
using namespace std;

void test01()
{
    // ===== 1. 创建文件输入流对象 =====
    // ifstream = input file stream(输入文件流)
    // 用于从文件中读取数据
    ifstream ifs;
    
    // ===== 2. 打开文件 =====
    // open(文件名, 打开模式)
    // "test.txt" - 要读取的文件名
    // ios::in   - 读取模式
    ifs.open("test.txt", ios::in);

    // ===== 3. 检查文件是否打开成功 =====
    // is_open() 返回 bool,true 表示打开成功
    if (!ifs.is_open())
    {
        cout << "文件打开失败" << endl;
        return;  // 直接返回,不继续执行
    }

    // ========================================
    // 方式1:使用 >> 运算符(按单词读取)
    // ========================================
    // char buf[1024] = { 0 };  // 字符数组缓冲区
    // while (ifs >> buf)       // 每次读取一个单词
    // {
    //     cout << buf << endl; // 输出每个单词
    // }
    // 特点:自动跳过空格和换行,按单词分割
    // 缺点:无法读取包含空格的整行

    // ========================================
    // 方式2:使用 getline()(C风格,按行读取)
    // ========================================
    // char buf[1024] = { 0 };  // 字符数组缓冲区
    // while (ifs.getline(buf, sizeof(buf)))  // 每次读取一行
    // {
    //     cout << buf << endl; // 输出整行
    // }
    // 特点:读取整行,包括空格
    // 缺点:需要指定缓冲区大小,可能截断

    // ========================================
    // 方式3:使用 getline()(C++风格,按行读取)⭐ 推荐
    // ========================================
    // string buf;              // string 对象(自动管理内存)
    // while (getline(ifs, buf)) // 每次读取一行到 string
    // {
    //     cout << buf << endl; // 输出整行
    // }
    // 特点:读取整行,自动管理内存,不会截断
    // 优点:最常用,最安全

    // ========================================
    // 方式4:使用 get()(逐个字符读取)
    // ========================================
    char c;  // 存储每个字符
    // get() 读取一个字符,EOF 表示文件结束(End Of File)
    while ((c = ifs.get()) != EOF)
    {
        cout << c;  // 输出每个字符(不换行,保留原格式)
    }
    // 特点:逐字符读取,保留所有格式
    // 优点:可以处理任何文本,包括特殊字符

    // ===== 4. 关闭文件 =====
    ifs.close();
}

int main()
{
    test01();
    system("pause");
    return 0;
}

总结:

* 读文件可以利用 ifstream ,或者fstream类
* 利用is_open函数可以判断文件是否打开成功
* close 关闭文件

四种读取方式对比

方式 代码 特点 适用场景
>> 运算符 ifs >> buf 以空白为分隔,自动跳过空格和换行 读取单词或数字
getline(数组) ifs.getline(buf, size) 读取一整行,包含空格 读取带空格的文本行
getline(string) getline(ifs, buf) 读取一整行,使用 string 类型 更安全,无长度限制
get() c = ifs.get() 逐个字符读取 需要精确控制每个字符

注意事项

要点 说明
文件路径 "test.txt" 是相对路径,文件在程序运行目录下
打开失败原因 文件不存在、路径错误、权限不足
is_open() 必须检查,否则可能操作无效文件流
close() 必须关闭,释放文件句柄
缓冲区大小 确保 buf 足够大,避免溢出

文件内容示例

假设 test.txt 内容为:

Hello World
C++ File IO

运行结果:

Hello
World
C++
File
IO

因为 >> 以空白为分隔,所以 Hello World 被分成 Hello 和 World 两次输出。

总结

这段代码演示了 C++ 文件读取的基本流程:创建 ifstream 对象 → open() 打开文件 → 检查 is_open() → 使用 >> 读取数据 → close() 关闭文件。>> 运算符以空白字符为分隔符读取数据。

>> 运算符

char buf[1024] = { 0 };
while (ifs >> buf) {
    cout << buf << endl;
}

特点:

  • 自动跳过空白字符(空格、换行、Tab)

  • 每次读取一个连续的字符串

  • 遇到空格或换行停止

  • 适用于读取以空格分隔的数据

文件内容:Hello World 123
读取顺序:Hello → World → 123

getline()(C风格)

char buf[1024] = { 0 };
while (ifs.getline(buf, sizeof(buf))) {
    cout << buf << endl;
}

特点:

  • 读取整行,包括空格

  • 需要指定缓冲区大小

  • 如果行过长,会被截断

  • C 风格代码

getline()(C++风格)⭐ 推荐

string buf;
while (getline(ifs, buf)) {
    cout << buf << endl;
}

特点:

  • 读取整行,包括空格

  • string 自动管理内存,不会截断

  • 最常用、最安全

  • 推荐使用

get() 逐字符

char c;
while ((c = ifs.get()) != EOF) {
    cout << c;
}

特点:

  • 每次读取一个字符

  • 保留所有格式(包括空格、换行)

  • 适合逐字符处理

  • EOF = End Of File(文件结束标志)

总结:C++ 读取文件有四种方式——>> 按单词读取、getline 按行读取(推荐)、get() 逐字符读取,最推荐使用 getline(ifs, buf) 配合 string 按行读取

5.2 二进制文件

以二进制的方式对文件进行读写操作

打开方式要指定为 ios::binary

5.2.1 写文件

二进制方式写文件主要利用流对象调用成员函数write

函数原型 :ostream& write(const char * buffer,int len);

参数解释:字符指针buffer指向内存中一段存储空间,len是读写的字节数。

**示例:

#include <fstream>      // 文件操作头文件(ofstream、ifstream)
#include <iostream>     // 输入输出流(cout、endl)
#include <string>       // 字符串库(string)
using namespace std;

// ===== 定义 Person 结构体 =====
// 用于存储人员信息,包含姓名和年龄
class Person
{
public:
    char m_Name[64];    // 姓名:字符数组,64 字节
    int m_Age;          // 年龄:整数,4 字节
    // 总大小:64 + 4 = 68 字节(可能因内存对齐而变大)
};

// ===== 二进制文件写入 =====
void test01()
{
    // 1、包含头文件(已包含)

    // 2、创建输出流对象 + 打开文件
    // 直接在构造函数中指定文件名和打开模式
    // ios::out   → 写入模式
    // ios::binary → 二进制模式(不进行文本转换)
    ofstream ofs("person.txt", ios::out | ios::binary);
    // 等价于:
    // ofstream ofs;
    // ofs.open("person.txt", ios::out | ios::binary);

    // 3、准备数据
    // 创建 Person 对象并初始化
    Person p = {"张三", 18};
    // 等价于:
    // Person p;
    // strcpy(p.m_Name, "张三");
    // p.m_Age = 18;

    // 4、写文件(二进制写入)
    // write(要写入的内存地址, 要写入的字节数)
    // (const char*)&p → 将结构体的地址转换为 char* 类型
    // sizeof(p)       → 计算结构体占用的字节数(68 字节)
    ofs.write((const char*)&p, sizeof(p));

    // 5、关闭文件
    ofs.close();
}

int main()
{
    test01();
    system("pause");
    return 0;
}

总结:

* 文件输出流对象可以通过write函数,以二进制方式写数据。

5.2.2 读文件

二进制方式读文件主要利用流对象调用成员函数read

函数原型:

istream& read(char *buffer,int len);

参数解释:字符指针buffer指向内存中一段存储空间。len是读写的字节数。

示例:

#include <fstream>      // 文件操作头文件(ifstream、ofstream)
#include <iostream>     // 输入输出流(cout、endl)
#include <string>       // 字符串库(string)
using namespace std;

// ===== 定义 Person 结构体 =====
// 必须和写入时完全一致!
// 这样才能正确读取之前写入的数据
class Person
{
public:
    char m_Name[64];    // 姓名:64 字节(必须和写入时一样)
    int m_Age;          // 年龄:4 字节
    // 总大小:64 + 4 = 68 字节(必须和写入时完全一致!)
};

// ===== 二进制文件读取 =====
void test01()
{
    // ===== 1. 创建输入流对象并打开文件 =====
    // 在构造函数中直接指定文件名和打开模式
    // ios::in     → 读取模式
    // ios::binary → 二进制模式(必须与写入时一致!)
    ifstream ifs("person.txt", ios::in | ios::binary);
    // 等价于:
    // ifstream ifs;
    // ifs.open("person.txt", ios::in | ios::binary);

    // ===== 2. 检查文件是否打开成功 =====
    // is_open() 返回 bool
    // true  → 打开成功
    // false → 打开失败(文件不存在或权限问题)
    if (!ifs.is_open())
    {
        cout << "文件打开失败" << endl;
        return;  // 直接返回,不继续执行
    }

    // ===== 3. 读取二进制数据 =====
    // 创建一个空的 Person 对象
    Person p;
    
    // read(存储地址, 读取字节数)
    // (char*)&p → 将结构体的地址转换为 char* 类型
    // sizeof(p) → 读取的字节数(必须和写入时一致!)
    ifs.read((char*)&p, sizeof(p));
    
    // 解释:从文件中读取 68 字节数据,直接填入 p 的内存中
    // 这完全还原了之前写入的 Person 对象

    // ===== 4. 输出读取的数据 =====
    cout << "姓名: " << p.m_Name << " 年龄: " << p.m_Age << endl;
    // 输出:姓名:张三 年龄:18

    // ===== 5. 关闭文件 =====
  
    ifs.close();  
}

int main()
{
    test01();
    system("pause");
    return 0;
}

文件输入流对象可以通过read函数,以二进制方式读数据。

更多推荐