C++核心编程
本阶段主要针对C++面向对象编程技术做详细讲解,探讨C++中的核心和精髓。
目录
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; }
核心要点:
new在堆区分配内存,返回地址堆内存不会自动释放,必须手动
delete函数返回堆内存地址是安全的(内存还在)
delete后指针变成野指针,不能再使用堆内存的生命周期由程序员控制,可以跨函数
#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),并且是常量(不可修改)。重载候选:
func(int &a):无法匹配!因为 C++ 规定,普通引用(int&)不能绑定到右值(如字面量、临时对象)。这是为了防止你通过引用修改一个没有内存地址的临时值,从而引发错误。
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访问权限 控制成员的可见性 public,protected,private
成员函数属于类,有
this指针,通过对象调用;全局函数不属于任何类,没有this指针,独立存在。核心区别对比表
特性 成员函数 全局函数 属于谁 属于类 不属于任何类 调用方式 对象.函数()或指针->函数()直接调用 函数()是否有 this指针✅ 有(指向调用对象) ❌ 没有 能否访问私有成员 ✅ 可以(自己是“自己人”) ❌ 不可以(除非声明为友元) 能否被继承 ✅ 可以(子类继承) ❌ 不可以 能否是虚函数 ✅ 可以(实现多态) ❌ 不可以 作用域 类作用域内 全局作用域 声明位置 在类内部 在类外部 成员函数 vs 全局函数的选择
场景 推荐 原因 操作需要访问私有成员 成员函数 只有成员函数和友元能访问私有成员 操作符左侧是自定义类型 成员函数 如 p1 + p2,左侧p1是Person对象操作符左侧是内置类型 全局函数 如 10 + p1,int没有成员函数操作不依赖于特定对象 全局函数 如工具函数 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几乎相同,唯一区别是默认访问权限:
对比项 structclass默认访问权限 publicprivate默认继承权限 publicprivate使用场景 纯数据容器 封装数据和行为 #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);有参构造 创建 p1Person 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)左侧操作数来源 第一个参数 p2this(调用对象)this指针❌ 没有 ✅ 有 调用方式 operator+(p3, 10)p3.operator+(10)使用写法 p3 + 10p3 + 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_A、m_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 + 5operator+(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 + Person,int没有成员函数需要同时支持 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 中,记录了不同子对象(Sheep、Tuo)到同一个虚基类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++ 实现运行时多态需要三个条件:”
继承:派生类继承基类。
虚函数:基类中声明
virtual函数,派生类进行override(重写)。基类指针或引用指向派生类对象:用基类类型的指针或引用来接收派生类对象。
多态的底层依赖于虚函数表(vtable)和虚指针(vptr)。每个带有虚函数的类都有一个虚函数表,存放所有虚函数的地址。每个对象有一个虚指针(vptr),指向该类的虚函数表。调用虚函数时,通过 vptr 找到 vtable,再找到对应函数的地址执行,这个过程叫做动态绑定(Dynamic Binding)。
这也意味着多态有一定的性能开销(通过指针间接寻址),但换来了极大的代码灵活性和可扩展性。
“多态的核心价值在于:”
代码复用:用一份代码(如
makeSound函数)处理多种类型。开闭原则:新增派生类时,原有的代码无需修改(对扩展开放,对修改关闭)。
面向接口编程:调用者只需要知道基类的接口,不需要了解具体的实现细节。
多态是同一个接口在不同对象上表现出不同行为的特性。在 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的vptrBase* 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*),都能找到 vptrvptr 指向派生类的 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();传递的是地址,而不是把整个对象复制给p。p只负责“指向”,并不负责“拥有”,所以它不需要知道子类的全部细节,只需要知道它指向的内存地址上有一块“Base 部分”就够了。总结
用父类类型接收子类对象,是因为每个子类对象都天然包含了一个父类子对象,且内存地址重合;这种“向上转型”在内存层面是合法的,也是 C++ 实现多态的语法基础。
父类有自己的虚函数表(vtable),但父类指针本身只是一个变量,存放在栈上(或作为成员变量),它指向的是某个对象(父类对象或子类对象)的起始地址。
1. 父类有自己的虚函数表和虚函数地址吗?
有。 每个包含虚函数的类都有自己的虚函数表(vtable),存放在只读数据段。
class Base { public: virtual void func() { cout << "Base" << endl; } };内存中:
┌─────────────────────────────────────────────────────────────────┐ │ 只读数据段 │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ Base 的 vtable │ │ │ │ ┌─────────────────────────────────────────────────┐ │ │ │ │ │ Base::func() 的地址 │ │ │ │ │ └─────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘
即使没有创建任何对象,
Base的vtable也已经存在了(编译期生成)每个包含虚函数的类都有自己独立的
vtable2. 父类指针存放在哪里?
父类指针是一个普通的指针变量,存放在栈上(或作为成员变量/全局变量)。
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. 抽象类(纯虚函数)
类 纯虚函数 作用 CPUvirtual void calculate() = 0;定义 CPU 接口规范 VideoCardvirtual void display() = 0;定义显卡接口规范 Memoryvirtual void storage() = 0;定义内存条接口规范 作用:这些类不能被实例化,只能被继承,强制子类实现特定功能。
2. 多态(父类指针指向子类对象)
CPU* cpu = new IntelCPU; // 父类指针指向子类对象 cpu->calculate(); // ★ 调用 IntelCPU::calculate()
关键点 说明 父类指针 CPU*、VideoCard*、Memory*子类对象 IntelCPU、LenovoCPU等动态绑定 运行时根据实际对象类型调用对应函数 3. 组合设计模式
class Computer { CPU* m_cpu; // ★ 组合:包含其他对象 VideoCard* m_vc; Memory* m_mem; };优势:灵活组装,可以自由搭配不同厂商的零件。
4. 虚析构的作用
如果
CPU、VideoCard、Memory的析构函数不是虚的,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*类型,可以指向IntelCPU、LenovoCPU等任何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继承AnimalComputer包含CPU代码 class Cat : public AnimalCPU* 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函数,以二进制方式读数据。
更多推荐
打印结果:



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



4.2.6 初始化列表


















子类继承父类时,会继承父类的 vfptr,但重写虚函数后,子类的 vftable 中对应的函数地址被替换为子类自己的实现,从而实现多态



所有评论(0)