[C.C++] 学习笔记:从C语言到C++

441 0
Honkers 2026-3-12 23:37:49 | 显示全部楼层 |阅读模式

C语言到C++是很多同学的学习流程,但是网上贴合这一学习路径的教学并不多,于是在此将我的学习过程加以总结,凝练成这篇笔记,(主要是面向对象编程这一部分)供大家参考。

一、C++基础与C语言的区别

1.c++的基础框架

  1. #include<iostream>
  2. using namespace std;
  3. int main() {
  4. system("pause");
  5. return 0;
  6. }
复制代码

命名空间:

命名空间提供了一种在大项目中避免名字冲突的方法 。即:在命名空间块内声明的符号被放入一个具名的作用域中,避免这些符号被误认为其他作用域中的同名符号。

效果:多个命名空间块中的名字可以相同。 命名空间名::成员名(叫做有限定名) 链接里的这篇文章讲的不错: https://blog.csdn.net/2301_80026901/article/details/156806562?fromshare=blogdetail&sharetype=blogdetail&sharerId=156806562&sharerefer=PC&sharesource=2501_94259244&sharefrom=from_link

本地优先:(查找顺序)

  1. 当前作用域(函数内部、代码块内部)

  2. 命名空间作用域(当前所在的命名空间)

  3. 全局作用域

  4. 外部命名空间(包括通过 using 导入的命名空间)

当多个候选函数在同一查找层级时(如自定义max和std::max都在全局可见层级),会产生冲突

2.标准输入输出流

标头定义了一些有参的操纵符函数,包括

setw(n), 更改下个输入/输出域的宽度,宽度为 n setprecision(n) ,更改浮点精度
  1. #include<iostream>
  2. #include<iomanip>
  3. using namespace std;
  4. int main() {
  5. int age = 25; double salary = 500.50; string name = "张三";
  6. cout << "姓名:" << name << endl<<"年龄" << age <<endl<< "工资:" << salary << endl;
  7. cout << "第一行" << endl; cout << "第二行\n"; cout << "第三行" << endl;
  8. //设置宽度和对齐iomanip输出宽度为8
  9. int a = 10; int b = 100;
  10. cout << setw(8) << a << endl << setw(8) << b << endl << left << setw(8) << a << endl;
  11. double pi = 3.1415926;
  12. cout << pi << endl; cout << setprecision(2) << pi << endl;
  13. cout << setprecision(4) << pi << endl;
  14. cout <<"单引号:\'" << endl;
  15. system("pause");
  16. return 0;
  17. }
复制代码

输出结果:

  1. 姓名:张三
  2. 年龄25
  3. 工资:500.5
  4. 第一行
  5. 第二行
  6. 第三行
  7. 10
  8. 100
  9. 10
  10. 3.14159
  11. 3.1
  12. 3.142
  13. 单引号:'
复制代码

3.C++引用(C语言中没有)

引用的本质是已经存在变量的别名,必须要在定义时初始化,一旦初始化,永不改变绑定关系,

不能通过const引用修改对象,但可以直接修改原变量。

  1. void swap(int& a, int& b) {
  2. //引用形参引用的就是实参变量
  3. int temp = a;
  4. a = b;
  5. b = temp;
  6. }
  7. void process(int& x) {
  8. cout << "处理左值" << x << endl;
  9. }
  10. void process(int&& x) {
  11. cout << "处理右值" << x << endl;
  12. }
  13. int main() {
  14. int x = 10;
  15. int& ref = x;
  16. const int &cref = x;
  17. //cref = 20;(错误)
  18. //只能绑定到左值(有名字地址的对象),不能绑定到右值
  19. int var = 10;
  20. int& lref = var;
  21. //const左值引用可以绑定右值
  22. const int& clref1 = 10;
  23. const int& clref2 = var + 1;
  24. //右值引用可以绑定右值
  25. int&& rref = 10;
  26. //swap函数
  27. int y = 10;
  28. swap(x, y);
  29. //函数重载
  30. process(x);
  31. process(10);
  32. system("pause");
  33. return 0;
  34. }
复制代码

引用

指针
空值不能为空可以为NULL
初始化必须先初始化可以不初始化
重绑定不能重新绑定可以重新指向
内存占用通常不占额外内存占用指针大小内存
语法更简洁需要解引用
安全性更高更低

函数返回引用 要求:返回的引用所引用的变量必须是存在的(局部变量在函数结束后会被销毁,因而不能返回局部变量的引用)

  1. double& f(double x){
  2. static double y;
  3. y=sin(x);
  4. return y;
  5. }
复制代码

4.函数重载

        函数重载(Function Overloading)是C++中的一种多态形式,它允许在同一作用域内定义多个同名函数,但这些函数的参数列表必须不同(参数类型、参数个数或参数类型顺序不同)。

eg:void show(int a,int b)与void show(int b,int a)不算函数重载!!!

不构成重载情况:

1.仅参数名不同 2.仅返回类型不同 3.对于按值传递的参数,仅const限定符不同

第三点额外说明:对于指针/引用传递:const 影响调用者能传入什么参数(如能否传入常量),所以可以用于重载(其余一般不可)

  1. // 整数版本
  2. void display(int num) {
  3. cout << "整数: " << num << endl;
  4. }
  5. // 浮点数版本
  6. void display(double num) {
  7. cout << "浮点数: " << num << endl;
  8. }
  9. // 字符串版本
  10. void display(string str) {
  11. cout << "字符串: " << str << endl;
  12. }
复制代码

函数模板

  1. #include<iostream>
  2. using namespace std;
  3. template<typename T>
  4. //std命名空间中有std::max
  5. T my_max(T a, T b) { return a > b ? a : b; }
  6. int main() {
  7. double x, y;
  8. cin >> x >> y;
  9. cout << my_max(x, y) << endl;
  10. system("pause");
  11. return 0;
  12. }
复制代码

不改函数名如何处理重名问题:删除using语句,改用有限定名。

5.C++函数支持默认参数,C语言不支持

  1. void print(string msg, int times = 2) { // times有默认值
  2. for (int i = 0; i < times; i++) {
  3. cout << msg << endl;
  4. }
  5. }
复制代码

6.函数内联(inline关键字)

内联函数(Inline Function)是C++中一种特殊的函数,通过在编译时将函数调用处替换为函数体代码,从而减少函数调用的开销。它是通过关键字 inline 声明的函数。

优点:

  1. 消除函数调用开销:提高程序执行效率

  2. 可优化性:编译器可以对内联后的代码进行更多优化

  3. 类型安全:比宏函数更安全

缺点:

  1. 代码膨胀:每次调用都会复制函数体代码

  2. 可能增加编译时间

  3. 调试困难:在调试器中不易跟踪

    1. // 类外定义
    2. inline int add(int a, int b) {
    3. return a + b;
    4. }
    5. // 类内定义(隐式内联)
    6. class Math {
    7. public:
    8. int multiply(int a, int b) { // 隐式内联
    9. return a * b;
    10. }
    11. inline int subtract(int a, int b); // 显式声明
    12. };
    13. // 类外定义内联函数
    14. inline int Math::subtract(int a, int b) {
    15. return a - b;
    16. }
    复制代码

    4.适合内联的情况

    • 函数体很小(1-5行)

    • 频繁调用的小函数

    • getter/setter方法

    • 简单的数学运算

7.Bool类型

C++中的bool类型不再是int的别名,而是独立的类型,只有true和false,大小通常是一个字节

  1. cout << true; // 显示1(为了兼容)false显示0
  2. cout << boolalpha << true; // 显示"true"
复制代码

会自动类型转换,类型安全

  1. bool b = 5; // 警告!应该写:bool b = (5 != 0);
  2. int i = true; // 可以,true变1
复制代码

8.更加严格的const

在C++中,非extern的const对象默认具有内部链接,需要显式使用extern来使其具有外部链接。

  1. // file1.cpp
  2. const int x = 10; // 默认内部链接,仅在本文件可见
  3. // file2.cpp
  4. extern const int x; // 链接错误!需要声明为 extern const
复制代码

C++要求const变量必须在声明时初始化。

C++不允许指针隐式去除const限定,类型检查更为严格

  1. const int x = 10;
  2. int* p = &x; // 错误!不能将const int*转换为int*
  3. int* q = (int*)&x; // 需要显式强制转换
  4. *q = 20; // 未定义行为,不会修改x的值
复制代码

constexpr是C++11引入的关键字,用于指定变量或函数可以在编译时求值,从而成为编译期常量。(详细询问AI)

成员函数const限定符:

  1. class MyClass {
  2. int data;
  3. public:
  4. int get() const { // const成员函数
  5. return data; // 此函数只读,不修改成员变量
  6. // data = 10; // 错误!
  7. }
  8. void modify() { // 非const成员函数
  9. data = 20;
  10. }
  11. };
  12. const MyClass obj;
  13. obj.get(); // 合法
  14. obj.modify(); // 错误!不能在const对象上调用非const成员函数
复制代码

9.new/delete

new 类型:返回该类型的指针,指向新建对象(与malloc不同,返回的不是万能指针void*)(从堆内分配内存)

访问该对象(*p) 释放该对象 delete p(调用析构函数)

new 类型[]{初始化列表}

1.若没有初始化列表,调用默认构造函数(一般是不确定值)

2.常数初始化列表,不足补零

3.数组长度小于初始化列表长度,抛出异常

  1. // 分配单个int
  2. int* pInt = new int;
  3. // 分配并初始化
  4. int* pInt2 = new int(42);
  5. //若为类类型,则初始化参数相当于将实际参数传递给该类的构造函数
  6. // 分配数组
  7. int* arr = new int[10];
  8. // 分配并初始化数组
  9. int* arr2 = new int[5]{1, 2, 3, 4, 5};
  10. // 分配对象
  11. class MyClass {
  12. public:
  13. MyClass(int x) : data(x) {}
  14. private:
  15. int data;
  16. };
  17. MyClass* obj = new MyClass(10);
复制代码
  1. int* p = new int(42);
  2. delete p; // 释放单个对象
  3. int* arr = new int[10];
  4. delete[] arr; // 释放数组
  5. // 注意:delete nullptr是安全的
  6. int* p2 = nullptr;
  7. delete p2; // 安全,无操作
复制代码

创建和删除二维数组

  1. #include<iostream>
  2. using namespace std;
  3. int main() {
  4. int row,column;
  5. cin >> row >> column;
  6. int** x = new int* [row];
  7. for (int i = 0; i < row; i++) {
  8. x[i] = new int[column];
  9. }
  10. for (int i = 0;i < row; i++) {
  11. for (int j = 0; j < column; j++) {
  12. cin >> x[i][j];
  13. }
  14. }
  15. //删除数组
  16. for (int i = 0; i < row; i++) {
  17. delete[]x[i];
  18. }
  19. delete[]x;
  20. system("pause");
  21. return 0;
  22. }
复制代码

10.string类型

string类型的创建和初始化

  1. // 默认构造 - 空字符串
  2. std::string s1;
  3. // 用C风格字符串初始化
  4. std::string s2 = "Hello";
  5. std::string s3("World");
  6. // 用字符重复构造
  7. std::string s4(5, 'A'); // "AAAAA"
  8. // 拷贝构造
  9. std::string s5 = s2;
  10. // 从子串构造
  11. std::string s6("Hello World", 5); // "Hello"
  12. std::string s7(s2, 1, 3); // "ell" (从位置1开始取3个字符)
  13. // 使用初始化列表(C++11)
  14. std::string s8 = {'H', 'i'};
  15. // 移动构造(C++11)
  16. std::string s9 = std::move(s2);
复制代码

访问元素

  1. std::string str = "Hello";
  2. str[0]; // 'H'
  3. str.at(1); // 'e' (会进行边界检查)
  4. str.front(); // 'H' (C++11)
  5. str.back(); // 'o' (C++11)
  6. // 获取C风格字符串
  7. const char* cstr = str.c_str();
  8. const char* data = str.data(); // C++17前需要确保以null结尾
复制代码

字符串长度

  1. std::string str = "Hello";
  2. str.length(); // 返回字符数: 5
  3. str.size(); // 与length()相同: 5
  4. str.empty(); // 是否为空: false
复制代码

对于字符串的基础操作

  1. std::string str = "Hello";
  2. // 追加
  3. str += " World"; // "Hello World"
  4. str.append("!!!"); // "Hello World!!!"
  5. str.push_back('!'); // 追加单个字符
  6. // 插入
  7. str.insert(6, "Beautiful "); // "Hello Beautiful World!!!"
  8. // 删除
  9. str.erase(5, 10); // 从位置5删除10个字符
  10. str.clear(); // 清空字符串
  11. str.pop_back(); // 删除最后一个字符(C++11)
  12. // 替换
  13. str.replace(0, 5, "Hi"); // 将位置0开始的5个字符替换为"Hi"
  14. // 交换
  15. std::string str2 = "Bye";
  16. str.swap(str2);
复制代码

提起str的子串

  1. std::string str = "Hello World";
  2. std::string sub = str.substr(6, 5); // "World" (从位置6取5个字符)
复制代码

查找操作:

  1. std::string str = "Hello World";
  2. str.find("World"); // 返回6 (找到的位置)
  3. str.find('o'); // 返回4
  4. str.find("xyz"); // 返回std::string::npos
  5. //std::string::npos 是一个特殊的静态常量,表示 "未找到" 或 "无效位置"
  6. // 在64位系统上通常输出:
  7. // npos = 18446744073709551615
  8. str.rfind('o'); // 从后向前查找: 7
  9. str.find_first_of("aeiou"); // 查找任意一个字符出现的位置: 1('e')
  10. str.find_last_of("aeiou"); // 从后向前查找: 7('o')
  11. //查找给定字符集中的任意字符在在字符串中可能出现的最早位置,与字符集顺序无关
  12. str.find_first_not_of("Helo "); // 查找不在集合中的字符: 6('W')
  13. //也与字符集的顺序无关
复制代码

比较(compare按字典序比较)

  1. std::string s1 = "apple";
  2. std::string s2 = "banana";
  3. s1.compare(s2); // 返回负值(s1 < s2)
  4. s1.compare("apple"); // 返回0(相等)
  5. s1.compare(0, 2, "ap"); // 比较子串
复制代码

数值转换#include

  1. // 字符串转数值
  2. std::string s1 = "123";
  3. int i = std::stoi(s1); // 转int
  4. double d = std::stod("3.14"); // 转double
  5. long l = std::stol("1000000"); // 转long
  6. //均是原值转化,分别为123,3.14,1000000
  7. // 数值转字符串
  8. std::string s2 = std::to_string(123); // "123"
  9. std::string s3 = std::to_string(3.14159); // "3.14159"
复制代码

相关操作

  1. #include <algorithm>
  2. #include <cctype>
  3. std::string str = "Hello World";
  4. // 转换大小写
  5. std::transform(str.begin(), str.end(), str.begin(), ::toupper);
  6. std::transform(str.begin(), str.end(), str.begin(), ::tolower);
  7. // 删除特定字符
  8. str.erase(std::remove(str.begin(), str.end(), ' '), str.end());
  9. // 反转字符串
  10. std::reverse(str.begin(), str.end());
复制代码

输入

  1. cin<<str;//遇到空格停止
  2. getline(cin,str);//遇到换行符停止
  3. //cin和getline混用要注意清除换行符
  4. // 方法1:清除直到下一个换行符
  5. std::cin.ignore(); // 默认清除1个字符
  6. // 方法2:清除直到换行符,包括换行符
  7. std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
  8. // numeric_limits<std::streamsize>::max() 表示非常大的数
  9. // 方法3:清除指定数量字符
  10. std::cin.ignore(1000, '\n'); // 最多清除1000个字符,直到换行符
复制代码

C语言和C++的差异就铺垫到这里,接下来会进入到C++的核心内容:类与对象的学习当中

二、类与对象:

类(class)是一个自定义的数据类型,包含成员变量(属性)和成员函数(行为)对象依据类所创建的具体实例。

特点:

1.封装:类可以把数据和操作绑定在一起

2.对象之间相互独立,没有影响

在没有任何访问修饰符的前提下,默认为私有成员

  1. #include<iostream>
  2. using namespace std;
  3. class Car {
  4. private:
  5. double fuel;//私有成员变量
  6. //外部不能直接car1.fuel访问
  7. public:
  8. string color;
  9. string brand;
  10. void addfuel(double amount) {
  11. //公有函数可以访问私有变量
  12. fuel += amount;
  13. cout << "Fuel:" << fuel << endl;
  14. }
  15. void setfuel(double amount) {
  16. fuel = amount;
  17. }
  18. void drive(double distance) {
  19. fuel -= distance * 0.05;
  20. cout << "行驶了:" << distance << "公里,剩余油量:" << fuel << endl;
  21. }
  22. void indicate() {
  23. cout << color << brand << endl;
  24. }
  25. };
  26. int main() {
  27. Car car1;
  28. car1.color = "红色";
  29. car1.brand = "丰田";
  30. car1.indicate();
  31. car1.setfuel(100);
  32. car1.drive(25);
  33. car1.addfuel(10);
  34. Car car2;
  35. car2.color = "蓝色";
  36. car2.brand = "宝马";
  37. car2.indicate();
  38. car2.setfuel(50);
  39. car2.addfuel(20);
  40. car2.drive(50);
  41. system("pause");
  42. return 0;
  43. }
复制代码

构造函数:

类的构造函数是类的一种特殊的成员函数,每次创建类的新对象时执行它完成初始化等逻辑。 构造函数的 名称与类的名称完全相同,并且不会返回任何类型,也不会返回void 如果用户没有自定义构造函数,则编译器会自动生成一个默认构造函数 自定义构造函数

传统写法——函数体内赋值

  1. // 传统写法 - 在函数体内赋值
  2. Student(int s) {
  3. score = s; // 这是赋值,不是初始化!
  4. }
复制代码

推荐写法——初始化器列表——效率更高

初始化器列表以冒号开头,后面跟一系列以逗号分割的成员初始化器

  1. class Example {
  2. private:
  3. std::string name; // string 有构造函数
  4. int age;
  5. public:
  6. // 初始化列表写法(效率高)
  7. Example(std::string n, int a) : name(n), age(a) {
  8. }
  9. };
复制代码

对于常量成员,引用成员,没有默认构造函数的类类型成员,必须使用初始化列表。

对于类的组合,即不同类的对象组合形成一个新的类,如果某个对象成员的类类型没有无参数的构造函数,则要使用成员初始化器调用有参的构造函数进行初始化

(所以平常尽量使用初始化列表就好了~)

重要:初始化顺序由成员在类中声明的顺序决定,而不是初始化列表中的顺序!

  1. class Example {
  2. private:
  3. int a; // 先声明
  4. int b; // 后声明
  5. public:
  6. // 危险:虽然列表中是 b(a+1), a(val),但实际执行顺序是:
  7. // 1. 先初始化 a (a = val)
  8. // 2. 再初始化 b (b = a+1,此时 a 已有值)
  9. Example(int val) : b(a+1), a(val) {} // 能正常工作,但容易混淆
  10. // 建议:保持声明顺序和初始化顺序一致
  11. Example(int val) : a(val), b(a+1) {} // 更清晰
  12. };
复制代码

析构函数:

析构函数是类的一种特殊成员函数,在对象生命周期结束时自动执行(局部变量离开作用域,delet对象指针,程序结束时全局/静态对象),名称为:~类名(),函数不返回任何值也不带有任何参数,主要任务为关闭/释放。若用户没有自定义析构函数,则编译器自动生成默认析构函数(大部分程序不需要手动编写)

当自己手动分配内存时(new),则需要手动编写析构函数delete。

访问私有变量的方法:

1.公共成员函数Getter/Setter(最推荐)

2.友元函数:外部函数可以直接访问私有成员

  1. class Student {
  2. private:
  3. int score;
  4. public:
  5. Student(int s) : score(s) {}
  6. // 声明外部函数为友元
  7. friend void checkScore(Student s);
  8. };
  9. // 这个函数可以直接访问 Student 的私有成员
  10. void checkScore(Student s) {
  11. if (s.score >= 60) { // 直接访问私有变量 score
  12. cout << "及格,分数:" << s.score << endl;
  13. } else {
  14. cout << "不及格,分数:" << s.score << endl;
  15. }
  16. }
  17. int main() {
  18. Student stu(85);
  19. checkScore(stu); // 输出:及格,分数:85
  20. return 0;
  21. }
复制代码

3.友元类:一个类可以访问另一个类的私有成员

  1. class Teacher; // 前向声明
  2. class Student {
  3. private:
  4. int score;
  5. public:
  6. Student(int s) : score(s) {}
  7. // 声明 Teacher 类为友元类
  8. friend class Teacher;
  9. };
  10. class Teacher {
  11. public:
  12. void modifyScore(Student& stu, int newScore) {
  13. stu.score = newScore; // 可以直接修改学生的私有分数
  14. cout << "老师修改了分数为:" << stu.score << endl;
  15. }
  16. };
  17. int main() {
  18. Student stu(70);
  19. Teacher t;
  20. t.modifyScore(stu, 90);
  21. return 0;
  22. }
复制代码

简单实操

接下来实际操练一下,实现三维向量。

  1. //头文件vector3.hpp
  2. #pragma once//头文件守卫,避免重复定义
  3. #include<cmath>
  4. #include<iostream>
  5. using namespace std;
  6. class vector {
  7. public:
  8. float x, y, z;
  9. vector(float x, float y, float z) :x(x), y(y), z(z) {
  10. }
  11. void getlength()const;
  12. void set(float new_x, float new_y, float new_z);
  13. void indicate()const;
  14. };
复制代码
  1. //函数定义,vector.cpp
  2. #include"vector3.hpp"
  3. #include<iomanip>
  4. void vector::getlength()const {
  5. float length = sqrt(x * x + y * y + z * z);
  6. cout << "length:" << length << endl;
  7. }
  8. void vector::set(float new_x, float new_y, float new_z) {
  9. x = new_x;
  10. y = new_y;
  11. z = new_z;
  12. }
  13. void vector::indicate()const {
  14. cout << setprecision(2) << "(" << x << "," << y << "," << z << ")" << endl;
  15. }
复制代码
  1. //客户端 point.cpp
  2. #include"vector3.hpp"
  3. int main(){
  4. vector p(1, 1, 1);
  5. int newx, newy, newz;
  6. cin >> newx >>newy >> newz;
  7. p.set(newx, newy, newz);
  8. p.getlength();
  9. p.indicate();
  10. system("pause");
  11. return 0;
  12. }
复制代码

this指针:

c++中,每一个对象的成员函数都能通过this指针访问自己的地址,换言之,this指针是成员函数的隐藏参数,因此在成员函数内部,this指针可以用来指向调用对象。

  1. class Box {
  2. public:
  3. // 构造函数定义
  4. Box(double l=2.0, double b=2.0, double h=2.0) {
  5. cout <<"Constructor called." << endl;
  6. length = l;
  7. breadth = b;
  8. height = h;
  9. }
  10. double Volume() {
  11. return length * breadth * height;
  12. }
  13. int compare(Box box) {
  14. return this->Volume() > box.Volume();
  15. }
  16. private:
  17. double length; // Length of a box
  18. double breadth; // Breadth of a box
  19. double height; // Height of a box
  20. };
复制代码

友元函数没有this指针,因为友元函数不是类的成员。

当成员函数参数和参数重名时,必须使用this指针

  1. class Person {
  2. private:
  3. string name;
  4. int age;
  5. public:
  6. // 构造函数:参数名与成员名相同,必须用 this
  7. Person(string name, int age) {
  8. // 如果不用 this
  9. name = name; // 参数赋值给自己,成员 name 没变!
  10. age = age; // 参数赋值给自己,成员 age 没变!
  11. // ✅ 正确写法:用 this 区分
  12. this->name = name; // 成员 = 参数
  13. this->age = age; // 成员 = 参数
  14. }
复制代码

注意:static成员函数属于整个类,而不是单独属于某个对象,没有this指针,需要使用类名::成员访问成员

static修饰:(类的静态成员)

定义方式:

1.在数据成员类型前加保留字static声明静态数据成员

2.在成员函数返回类型前加保留字static声明静态成员函数

静态成员是类的组成部分但不是任何对象的组成部分

访问方式:

1.基于类名。使用类名和::,不通过对象

2.通过对象

静态数据成员

与非静态数据成员不同,静态数据成员并不是通过构造静态成员进行初始化,必须在类定义体的外部(类的实现文件)再定义一次,且恰好一次(此时不再用static修饰)。

  1. class 示例 {
  2. int 普通成员; // 每个对象都有自己的一份
  3. static int 静态成员; // 所有对象共享同一份
  4. public:
  5. 示例() {
  6. 普通成员 = 100; // 在构造函数中初始化普通成员
  7. // 静态成员 = 200; // 不能在构造函数中初始化静态成员
  8. }
  9. };
  10. //必须:在类外定义和初始化静态成员
  11. int 示例::静态成员 = 200; // 注意:这里不能写 static
复制代码
静态成员函数

没有this指针,不属于任何对象

重点:静态成员函数不能直接访问类的非静态成员,只能直接访问类的静态成员

可以通过传递对象作为参数访问

=default

令编译器为某个类生成默认的特殊成员函数的显式指令(只能用于特殊成员函数)

特殊成员函数

1.默认构造函数T() 2.析构函数 ~T() 3.复制构造函数T(const T&) 4.赋值运算operator=(const T&)

以=default声明以后,不能再自定义实现(重复定义导致出错)

  1. class A
  2. {
  3. public:
  4. A(int x) {
  5. cout << "This is a
  6. parameterized constructor";
  7. }
  8. A() =default;
  9. };
复制代码

=delete弃置函数

删除或者禁用函数的功能,必须要在函数第一次声明的时候删除。

  1. class MyClass {
  2. public:
  3. // 正确:声明时立即删除
  4. MyClass(const MyClass&) = delete;
  5. // MyClass& operator=(const MyClass&);
  6. };
  7. // MyClass& MyClass::operator=(const MyClass&) = delete; // 不允许
复制代码

在删除后函数签名仍然存在

  1. class HasDeleted {
  2. public:
  3. void func(int) = delete;
  4. // 这个函数仍然参与重载决议
  5. void func(double) { /* ... */ }
  6. };
  7. HasDeleted h;
  8. h.func(42); // 编译错误:选择被删除的int版本
  9. h.func(3.14); // 调用double版本
复制代码

程序内存布局

1.代码区:所有类的成员函数和非成员函数代码存放在代码区内(字符串字面量存储在代码区)

2.全局数据区:存放全局变量,静态数据和常量

3.栈区(stack):函数栈,为运行函数分配的内存,存储函数中的局部变量,函数参数,返回参数返回地址等。(大小较小)

4。堆区(heap):余下的空间都叫堆,new就是在堆区申请内存。

类的对象不存储指向成员函数的指针

如果是函数中定义的类变量(对象),则在栈内存区。如果是new出来的对象,则在堆内存区,同时引用会保存在栈里。

对象方法的静态联编(编译时确定)

指在编译阶段,就能直接使用代码段函数地址调用动态对象的方法,该方式仅需要向非静态成员函数传送this指针,即可用静态函数调用实现动态调用的效果。

优势:内存中对象便于与其他语言程序兼容    高效率高性能

  1. DATE date1();
  2. date1.f()
  3. //C++翻译:栈上分配空间,构造函数初始化
  4. //date1.f()翻译为DATE::f(&date1,....)
复制代码

拷贝构造函数

创建对象时用同类型的对象初始化新对象

  1. TYPE a;//调用TYPE的无参构造函数
  2. TYPE b(a);//或TYPE b=a;(这里不是赋值运算,而是函数操作)
复制代码

如果TYPE有明确定义的拷贝构造函数,则调用这个拷贝构造函数,如果没有,则调用默认拷贝构造函数

TYPE::TYPE(const TYPE& obj);

形式:形参类型为该类型本身且参数传递方式为按引用传递。

若用户未提供,默认构造函数按照初始化顺序,对对象的各基类和非静态成员进行完整的逐成员赋值,完成新对象的初始化。

对于不含指针成员的类,使用默认拷贝构造函数即可,但默认拷贝构造函数使用浅复制策略,不能满足对含指针输出据成员的类的需求。

对象作为值返回的编译优化

  1. TYPE func(){
  2. TYPE t;
  3. return t;
  4. }
  5. b=func();
复制代码

不再生成临时对象保存函数的返回结果,而是在离开func时不撤销t,调用拷贝构造函数用t初始化b。

含指针成员的类重写默认函数

含指针成员的类通常重写:1.构造函数(拷贝构造函数)中分配内存,深复制策略。2. =操作重写,完成对象深复制策略。3.析构函数释放内存。

浅复制无法满足的原因:浅复制只复制成员指针的值,而不复制指向的对象实体,导致新旧对象成员指针指向同一块内存。因此,深复制要求指针指向对象也要复制,新对象与原对象指向的不是同一个内存,修改新对象也不会影响原对象。

  1. class NAME {
  2. public:
  3. NAME();
  4. ~NAME();
  5. void show();
  6. void set(char* s);
  7. private:
  8. char* str;
  9. };
  10. void NAME::set(const char* s){
  11. strcpy(str,s);
  12. }
  13. void NAME::show()
  14. {
  15. cout << str << "\n";
  16. }
  17. void Print(NAME name) {
  18. name.show();
  19. }
  20. int main(){
  21. NAME *myname = new NAME;
  22. myname->set("Leetcode");
  23. Print(*myname);
  24. myname->show();
  25. }
复制代码

上述代码在进入print函数后,由于函数是传值,调用默认构造函数,依靠*myname初始化name,函数结束后,对象name调用析构函数,销毁name.str所指向的字符串,也就是"Leetcode\0",再次调用myname->show时,str指针悬垂,产生错误。

定义新的拷贝构造函数和赋值运算符

拷贝构造函数:

  1. NAME::NAME(const NAME& other){
  2. if(other.str==nullptr){
  3. str=nullptr;
  4. return;
  5. }
  6. str=new char[strlen(other.str)+1];
  7. strcpy(str,other.str);
  8. //新对象和原对象指向的内存不同
  9. }
复制代码

赋值运算符

  1. NAME& NAME::operator=(const NAME& other){
  2. if(this==&other) return *this;//不能自身赋值
  3. if(str!=NULL) delete[]str;//清空对象
  4. if(other.str==NULL){
  5. str=NULL;
  6. return *this;
  7. }
  8. str=new char[strlen(other.str)+1];
  9. if(str!=NULL) strcpy(str,other.str);
  10. return *this;
  11. }
复制代码

const再述:

const引用左值对象:const int& size=i;为变量取一个别名,不能修改。

const引用右值对象:const int& size=15/i+7;为值取一个别名(为右值对象开品临时空间,并赋予别名),不能修改

  1. int const_ref_with_casting() {
  2. //const引用右值 – 特例:隐式转型的值
  3. double b = 3.14;
  4. const int &a = b; //等于: int temp = b; const int& a=temp;
  5. int &a = temp
  6. b = 11;
  7. printf("a=%d,b=%lf\n", a, b);
  8. }
复制代码

在上述代码中,const引用右值,由于发生隐式转换,a绑定到了另一个变量temp上,所以对b的修改不会影响到a,所以最后的输出结果a=3,b=11.000.

临时对象(右值)的绑定能力
  1. void const_ref_in_para(const string& s) {
  2. cout << s << endl;
  3. }
  4. void no_const_ref_in_para(string& s) {
  5. cout << s << endl;
  6. }
  7. int const_ref_in_para(const int& s) {
  8. cout << s << endl;
  9. }
  10. int no_const_ref_in_para(int& s) {
  11. cout << s << endl;
  12. }
  13. int main() {
  14. const_ref_in_para("I love c++"); //equal to:
  15. // string s_temp="I..."; 创建临时变量
  16. // const_ref_in_para(string(s_temp));const引用可以绑定到临时变量
  17. // no_const_ref_in_para("I love c++");
  18. //error:临时对象是只读的,非const引用可能修改它,这是不允许的
  19. const_ref_in_para(3.14);
  20. // no_const_ref_in_para(3.14); //error同理
  21. //解决方法
  22. string str = "I love c++";
  23. no_const_ref_in_para(str); // 可以,因为str是左值
  24. }
复制代码

操作符重载

操作符重载可以视为特殊名称的函数重载,也具有返回类型,函数名和参数列表。

操作重载符的函数名是由关键字operator和要重载的运算符符号构成

参数数量:

1.重载二元运算符时,成员运算符只需要显式传递1个参数(A+B——>A.operator+(b))

2.重载一元运算符时,成员运算符函数没有参数,操作数时该类对象本身,通过this指针隐式传递。(-A——>operator -())

左操作数与返回值

重载后的左操作数必须是*this,eg:e=4+a则不正确

返回对象可以是左操作数类型的临时对象T,也可以是左操作数的引用T&

大多数运算符都可以通过成员或者非成员函数进行重载,但是=赋值运算符,()函数调用运算符,[]下标运算符,->通过指针访问类成员的的运算符,这些运算符只能通过成员函数重载。

不能重载的运算符:

1.作用域解析::   2. .成员访问.    3.成员指针访问预算符号.*  4.三元条件?:

5.sizeof运算符   6.预处理运算符#

操作符重载限制

1.不能创建新的运算符。

2.运算符&&和||的重载将失去短路求值原则。

3.不可更改运算符的优先级,结合方向和操作数的数量。

4.重载运算符->必须要么返回罗指针,要么(用引用或值)返回同样重载了运算符->的对象

例子解释如下:

  1. class Data {
  2. public:
  3. void func() { std::cout << "Data::func()" << std::endl; }
  4. };
  5. class Wrapper {
  6. private:
  7. Data* ptr;
  8. public:
  9. Wrapper(Data* p) : ptr(p) {}
  10. // 重载 operator->,返回裸指针
  11. Data* operator->() {
  12. return ptr;
  13. }
  14. };
  15. int main() {
  16. Data d;
  17. Wrapper w(&d);
  18. w->func(); // 输出: Data::func()
  19. }
复制代码
  1. w->func();被编译器解释为(w.operator->())->func()

  2. w.operator()->返回Data*     

  3. Data*->调用func()函数

    1. class DeepData {
    2. public:
    3. void deepFunc() { std::cout << "DeepData::deepFunc()" << std::endl; }
    4. };
    5. class MiddleLayer {
    6. private:
    7. DeepData* ptr;
    8. public:
    9. MiddleLayer(DeepData* p) : ptr(p) {}
    10. // 重载 operator->,返回 DeepData*(裸指针)
    11. DeepData* operator->() {
    12. return ptr;
    13. }
    14. };
    15. class TopLayer {
    16. private:
    17. MiddleLayer* middle;
    18. public:
    19. TopLayer(MiddleLayer* m) : middle(m) {}
    20. MiddleLayer operator->() {
    21. return *middle; // 返回对象本身
    22. }
    23. };
    24. int main() {
    25. DeepData d;
    26. MiddleLayer m(&d);
    27. TopLayer t(&m);
    28. t->deepFunc(); // 输出: DeepData::deepFunc()
    29. }
    复制代码

1.t->deepFunc();被编译器解释为(t.operator->())->deepFunc()

2.(t.operator->())->deepFunc()返回对象本身变为m->deepFunc();

3.m->deepFunc();思路与上一个例子类似,最后同样是通过裸指针访问deepFunc()函数

双目单目运算符重载
  1. class Integer {
  2. int x;
  3. public:
  4. Integer(int x=0):x(x) {
  5. }
  6. Integer operator + (const Integer& Int){
  7. return Integer(x+Int.x);
  8. }
  9. Integer operator - (const Integer& Int){
  10. return Integer(x-Int.x);
  11. }
  12. Integer operator - () {
  13. return Integer(-x);
  14. }
  15. void print() {
  16. cout<<x<<endl;
  17. }
  18. };
  19. int main() {
  20. Integer a=3,b=4;
  21. a.print();
  22. b.print();
  23. Integer c=a+b;
  24. c.print();
  25. Integer d=a-b;
  26. d.print();
  27. Integer e=-a;
  28. e.print();
  29. return 0;
  30. }
复制代码
自增运算符
重置前置++x:

保持语义:返回加一之后的值

返回对象引用:返回引用能够减少构造新对象的开销,运算结果可以作为左值,eg:++++x。

类内定义:T& T::operator++();

重置后置x++:

保持语义:x的值加一,返回加一之前的值

不能返回对象引用:当前对象存储加一以后的值,只能返回新建对象(加一以前的值)

不能重载为const函数:对象值发生变动(+1)

类内定义:T operator++(int);

  1. class Integer{
  2. int x;
  3. public:
  4. Integer(int x=0):x(x){
  5. }
  6. Integer& operator++(){
  7. ++x;
  8. return *this;
  9. }
  10. Integer operator++(int){
  11. return Integer(x++);
  12. }
  13. };
  14. int main(){
  15. Integar a(3);
  16. Integer b=++a;
  17. Integer c=a++;
  18. return 0;
  19. }
复制代码
赋值运算符重载

保持语义:右侧表达式的值赋给左侧变量

返回值:使用引用返回值是为了效率

支持用法:(c=a).print();

类内定义:T operator=(const T2& b);

  1. class Integer{
  2. int x;
  3. public:
  4. Integer(int x=0):x(x){
  5. }
  6. Integer& operator=(const Integer& Int){
  7. x=Int.x;
  8. return *this;
  9. }
  10. Integer& operator+=(const Integer& Int){
  11. this->x+=Int.x;
  12. return *this;
  13. }
  14. void print(){
  15. cout<<x<<endl;
  16. }
  17. };
  18. int main(){
  19. Integer a=3,b;
  20. a.print();
  21. b=a;
  22. b.print();
  23. return 0;
  24. }
复制代码
赋值的三种语义:

浅拷贝赋值(缺省的赋值运算符):被赋值对象与赋值对象共用同一个资源

深拷贝赋值(用户定义赋值运算符):被赋值对象拥有赋值对象资源的副本,对于非指针成员,不需要再自定义代码块中逐一完成非指针成员的赋值。

移动赋值:(自定义operator=T(T&&)赋值运算符完成,默认赋值运算被弃置):释放被赋值对象的已拥有资源,并指向赋值对象对应的资源,同时赋值相关成员你。复制对象指针赋值空,并被禁用。即复制对象资源转移到被复制对象。(b赋值对象,a被赋值对象)

浅拷贝赋值问题分析

  1. class IntArray {
  2. int *a;
  3. int n;
  4. public:
  5. IntArray(int n=1):n(n) {
  6. a=new int[n];
  7. for (int i=0;i<n;++i) a[i]=i;
  8. }
  9. ~IntArray() {
  10. cout << "release p=" << a <<endl;
  11. delete[] a;
  12. }
  13. void print() {
  14. for (int i=0;i<n;++i)
  15. cout<<a[i]<<" ";
  16. cout<<endl;
  17. }
  18. };
  19. int main() {
  20. IntArray a(4), b;
  21. a.print();
  22. b=a;
  23. b.print();
  24. a.print();
  25. return 0;
  26. }
复制代码

程序存在两个问题,当对象b创建的时候,由于n=1,b.a指向了一个大小为一的数组,然而再浅拷贝过程中,指针被覆盖,这个大小为1的数组没有被释放,导致了内存泄露。另一个问题是,在程序结束时,b,a先后调用析构函数,导致同一片内存被释放两次。

深拷贝策略修改:

  1. IntArray& operator=(const IntArray& other){
  2. if(this!=&other){
  3. delete[] a;//这里a一定不为空
  4. n=other.n;
  5. a=new int[n];
  6. memcpy(a,other.a,sizeof(int)*n);
  7. }
  8. return *this;
  9. }
复制代码

移动赋值(了解)

  1. //T&&作为右值引用,只能绑定到右值上,字面量或临时变量
  2. IntArray(IntArray&& other) { //移动构造
  3. cout << "copy move... p=" << other.a <<endl;
  4. a = other.a;n = other.n;
  5. other.a = nullptr; other.n=0;
  6. }
  7. IntArray& operator = (IntArray&& other) {//移动赋值
  8. cout << "operator = move p=" << other.a <<endl;
  9. if (this != &other) {
  10. if (a) delete[] a;
  11. a = other.a;n = other.n;
  12. other.a = nullptr; other.n=0;
  13. }
  14. return *this;
  15. }
复制代码

下标运算符重载

C++规定,下标运算符[]必须以成员函数的形式进行重载。

声明格式:R& T::operator[](S b);(可以访问和修改元素)

                  const R& T::operator[](S b) const;(只能访问元素)

实际开发中提供两种形式,因为const对象只能调用const成员函数,如果不提供第二种形式,呢么将无法访问任何元素

  1. int& Intarry::operator[](int i) {
  2. if (i>=0 && i < n) {
  3. return a[i];
  4. }
  5. throw std::out_of_range("out of range");
  6. }
  7. const int& Intarry::operator[](int i) const{
  8. if (i>=0 && i < n) {
  9. return a[i];
  10. }
  11. throw std::out_of_range("out of range");
  12. }
  13. int main(){
  14. Intarry a(4);
  15. for(int i=0;i<n;i++) a[i]=i+1;
  16. )
复制代码

函数调用运算符

函数对象:函数对象就是一个既可以当作函数来调用,同时又可以像对象一样拥有属性(变量)和方法(函数)的东西。可类似函数使用f(...),此时类也叫函数类(callable)

函数对象既有对象持有状态的特征,又有函数可调用特征。

那么如何才能实现函数对象呢?    通过对operator()的重载实现!

函数调用运算符的实现只能在类内实现

  1. class adder{
  2. private:
  3. int val;
  4. public:
  5. adder(int val):val(val);
  6. //重载()运算符
  7. int operator()(int x) const{
  8. return val+x;
  9. }
  10. //能拥有自己的函数
  11. void setval(int x){
  12. val=x;
  13. }
  14. };
  15. int main(){
  16. adder f(10);
  17. cout<<f(3)<<endl;
  18. return 0;
  19. }
复制代码

当operator()使用对象的成员时,依赖对象本身内部状态,相同的函数调用,结果不同

  1. class GreaterThan {
  2. int baseline;
  3. public:
  4. GreaterThan(int x):baseline(x) { }
  5. bool operator () (const int& x) {
  6. return x>baseline;
  7. }
  8. };
  9. int main() {
  10. GreaterThan g1(10), g2(20);
  11. cout << g1(15) << endl;
  12. cout << g2(15) << endl;
  13. return 0;
  14. }
复制代码

谓词函数是一种返回bool类型的判断函数

标准库中许多函数(eg:find_if)需要提供谓词函数作为参数,且支持任何可调用函数:普通函数,函数对象,lambda函数

类外函数运算符重载

  1. struct Integer {
  2. int x;
  3. public:
  4. Integer(int x=0):x(x) {}
  5. Integer operator- (const Integer& Int) {
  6. return Integer(x-Int.x);
  7. }
  8. Integer operator - () {
  9. return Integer(-x);
  10. }
  11. void print() {
  12. cout<<x<<endl;
  13. }
  14. };
  15. Integer operator+(const Integer& lhs, const Integer& rhs) { // 全局+运算符
  16. return Integer(lhs.x + rhs.x);
  17. }
  18. int main() {
  19. Integer a=3, b, c;
  20. b = a + 4;
  21. c = 5 + a;
  22. b.print();
  23. c.print();
  24. return 0;
  25. }
复制代码

函数调用情况:

1.通过构造函数Intege(int)将4隐式转换成Integer类型。

2.调用operator+运算符函数将a和临时对象相加,返回新的临时对象

3.调用默认拷贝赋值运算符将临时对象赋值给b。

类外定义输入输出重载

  1. istream& operator>>(istream& is,Integer& Int){
  2. is>>Int.x;
  3. return is;
  4. }
  5. ostream& operator<<(ostream& os,const Integer& Int){
  6. os<<Int.x;
  7. return os;
  8. }
复制代码

当类的成员是private时,外部函数无法访问。

友元函数

友元函数时在类内部用friend声明的函数,不属于类,但是可以访问类的私有变量及函数。

友元函数需要在类外实现,其他类的成员函数也可以时友元函数。

  1. class Integer{
  2. int x;
  3. friend istream& operator>>(istream& is,Integer& Int);
  4. friend ostream& operator<<(ostream& os,const Integer& Int);
  5. };
复制代码

友元类

利用friend T语句可以声明友元类,效果:如果类A将类B声明为自己的友元类,那么类B的所有成员函数就都可以访问类A的私有成员(注意:没有双向性!!!类A不可以访问类B的私有成员)

对于运算符=   ,()   ,[]   ,->   ,必须是成员函数。

对于运算符<<,>>,必须是非成员函数。

对于+,-,推荐使用非成员函数(以处理3+a的情况)

类型转换

如果类T存在构造函数T(T1),则可以称之为类型转换构造函数。

则支持T1类型对象(实参)隐式转换为T类型对象

重载协议

重载函数参数类型一样,一个有const,一个没有const。

const类型实参只能匹配const版本 非const实参优先匹配非const类型

  1. class Integer {
  2. int x;
  3. public:
  4. Integer(int x=0):x(x) {}
  5. friend ostream& operator<<(ostream& o, const Integer &hs) {
  6. o << "const " << hs.x;
  7. return o;
  8. }
  9. friend ostream& operator<<(ostream& o, Integer &hs) {
  10. o << "no_const " << hs.x ;
  11. return o;
  12. }
  13. };
  14. int main() {
  15. Integer i1(1);
  16. const Integer i2(2);
  17. cout << i1 << "," << 3 << "," << i2 << endl;
  18. }
  19. //输出结果:no_const,3,const
复制代码

nullptr

始终使用nullptr而不是NULL或0表示空指针

explicit

explicit修饰类的构造函数,被修饰后不能进行自动的隐式类型转换

三、继承和派生

使用继承的情况

在C++中,当定义一个新的类B时,如果发现类B拥有莫格已经写好的类A的全部特征,此外还有类A没有的特征,那么就不必从头重写类B,可以采用继承/派生方法,即把类A作为一个“基类”,把类B设计为基类A的一个派生类(子类),那么就可以说是类A派生出了类B,也可以说是类B继承了类A。类B中除了自己定义的成员之外,还包括了类A中定义的数据成员与成员函数,这些自动继承下来的成员称为类B的继承成员。

效果:

基类的所有成员自动成为派生类的成员

派生类是通过对基类进行扩充和修改得到的

继承代表的是从属关系

继承和派生是面向对象编程的基础,提供了一种通过修改(演化)一个或多个现有类来构造新事物的方法

继承关系分类:

单继承:派生类只有一个直接基类          

多重继承:派生类具有两个以上的直接基类

多重继承特殊形式:派生类两次以上重复继承某个间接基类

多层继承:存在间接基类

单重继承的定义形式

继承访问控制和成员访问控制均由保留字public,private,protected来定义,缺省为private

成员访问控制和继承

private:私有成员只能通过本类的成员函数来访问(派生类也不能)

public:公有成员用于描述一个类与外部世界的接口类的外部(程序的其它部分的代码)可以访问公有成员

protected:具有private和public的双重角色,即protected成员只能由本类及其派生类成员函数访问。本类和派生类对象无法直接访问。

影响继承成员的访问控制方式的两个因素:定义派生类时指定的继承访问控制,该成员在基类中所具有的成员访问控制,具体如下表

大多数情况均使用public继承

派生类对象存储

派生类的对象总是在前面存放基类中定义的非静态数据成员,然后存放派生类中定义的非静态数据成员。 可以认为派生类对象包含了一个基类子对象。换言之,派生类对象中都有一个BASE实例对象。 (虽然private成员无法访问,但是派生类对象中仍有private成员的内存)

派生类对象的初始化和基类的构造函数

派生类总是先进行基类成员的初始化,默认使用基类无参构造。

派生类需要自己定义构造函数,需要对新增成员进行初始化

对于继承成员,调用基类的构造函数

隐式调用:编译时在派生类构造函数初始化器中自动调用基类的无参构造函数(或拷贝构造)如果没有默认构造函数,则编译错误。

显式调用:在派生类构造函数初始化器中显示调用基类的有参构造函数,不能在构造函数内调用

若基类的构造函数带参数,则定义派生类构造函数时,必须通过初始化列表显式调用基类构造函数,并向基类构造函数传递实参。(见代码中的manager函数)

  1. class Person {
  2. public:
  3. Person(string n) : name(n) {}
  4. private:
  5. string name;
  6. };
  7. class Employee : public Person {
  8. public:
  9. Employee(string n, string d) : Person(n), dept(d)//构造函数初始化器 {
  10. //构造函数体
  11. }
  12. private:
  13. string dept;
  14. };
  15. class Manager : public Employee {
  16. public:
  17. Manager(string n, string d, int level)
  18. : Employee(n, d), managerLevel(level) {}
  19. private:
  20. int managerLevel;
  21. };
复制代码

创建派生类对象时构造函数调用顺序

首先调用基类构造函数(多重继承:调用顺序按照基类被继承是的声明顺序从左到右)

然后调用本类对象成员的构造函数(声明顺序)

最后调用本类的构造函数(即构造函数体内的指令)

撤销派生类对象时析构函数的调用次序与构造函数调用次序相反。

  1. class C {
  2. public:
  3. C( ); //构造函数
  4. ~C( ); //析构函数
  5. };
  6. class BASE {
  7. public:
  8. BASE( ); // 构造函数
  9. ~BASE( ); //析构函数
  10. }
  11. C::C( ) //构造函数
  12. { cout << "Constructing C object.\n"; }
  13. C:: ~C( ) //析构函数
  14. { cout << "Destructing C object.\n"; }
  15. BASE::BASE( ) // 构造函数
  16. { cout << "Constructing BASE object.\n"; }
  17. BASE:: ~BASE( ) // 析构函数
  18. { cout << "Destructing BASE object.\n";}
  19. class DERIVED: public BASE {
  20. public:
  21. DERIVED() // 构造函数
  22. ~DERIVED() // 析构函数
  23. private:
  24. C mOBJ;
  25. };
  26. DERIVED::DERIVED() // 构造函数
  27. { cout << "Constructing derived object.\n"; }
  28. DERIVED:: ~DERIVED() // 析构函数
  29. { cout << "Destructing derived object.\n"; }
  30. int main()
  31. {
  32. DERIVED obj; // 声明一个派生类的对象
  33. // 什么也不做,仅完成对象obj的构造与析构
  34. return 0;
  35. }
  36. /*运行结果
  37. Constructing BASE object.
  38. Constructing C object.
  39. Constructing derived object.
  40. Destructing derived object.
  41. Destructing C object.
  42. Destructing BASE object. */
复制代码

重载和隐藏

重载在同一个类中,函数名相同,参数不同时发生的,重载函数的作用域相同

隐藏是在派生类中,出现和基类中同名的函数,当参数列表有差异(或参数列表相同,但基类函数没有virtual关键字)时发生的,导致基类函数被隐藏,无法直接访问,其余的函数正常继承。

访问基类被隐藏的函数成员

  1. class Father {
  2. public:
  3. void work() {
  4. cout << "父亲在工作" << endl;
  5. }
  6. void work(string task) { // 重载版本
  7. cout << "父亲在做" << task << endl;
  8. }
  9. void work(int hours) { // 重载版本
  10. cout << "父亲工作" << hours << "小时" << endl;
  11. }
  12. };
  13. class Son : public Father {
  14. public:
  15. void work() { // 隐藏了父类所有的work函数
  16. cout << "儿子在工作" << endl;
  17. }
  18. };
  19. int main() {
  20. Son son;
  21. SOn* pson=son;
  22. son.work(); // OK:调用自己的work()
  23. // son.work("作业"); //错误!父类的work(string)被隐藏
  24. // son.work(8); //错误!父类的work(int)被隐藏
  25. // 通过对象调用父类函数
  26. son.Father::work("作业"); // OK:父亲在做作业
  27. pson->Father::work(8); // OK:父亲工作8小时
  28. }
复制代码

直接调用隐藏函数成员

  1. Base::HiddenFuc();
  2. this->Base::HiddenFuc();
复制代码

利用隐藏,因为派生类中的名字屏蔽基类重点名字,所以可以修改继承成员函数的功能,或赋予派生类成员新的功能。

  1. void POINT::show()
  2. {
  3. if (! is_visible()) {
  4. visible = TRUE;
  5. putpixel(x_pos, y_pos, getcolor());
  6. }
  7. }
  8. void CIRCLE::show()
  9. {
  10. if (! is_visible())
  11. {
  12. visible = TRUE;
  13. circle(x_pos, y_pos, radius);
  14. }
  15. }
复制代码

改变继承成员的访问控制方式(访问声明)

派生类中可以使用访问声明随意设置可访问继承成员在派生类中的访问控制方式(对基类中的private不管用)

形式:using 基类名::成员名(放在适当的成员访问控制后)

  1. class BASE {
  2. public:
  3. void set_i(int x)
  4. {
  5. i = x;
  6. }
  7. int get_i()
  8. {
  9. return i;
  10. }
  11. protected:
  12. int i;
  13. };
  14. class DERIVED: private BASE {
  15. public:
  16. using BASE::set_i; // 访问声明
  17. using BASE::i;
  18. void set_j(int x)
  19. {
  20. j = x;
  21. }
  22. int get_ij()
  23. {
  24. return i + j;
  25. }
  26. protected:
  27. int j;
  28. }
  29. int main()
  30. {
  31. DERIVED obj; // 声明一个派生类的对象
  32. obj.set_i(5); // set_i()已从私有的转为public
  33. obj.set_j(7); // set_j()本来就是公有的
  34. cout << obj.get_ij() << "\n"; // get_ij()本来就是公有的
  35. return 0;
  36. }
复制代码

访问声明除了可以改变访问控制方式,还可以将隐藏的重载函数引入作用域,简化构造函数等。

  1. class Base {
  2. public:
  3. void func() { cout << "Base func()" << endl; }
  4. void func(int x) { cout << "Base func(int)" << endl; }
  5. void func(string s) { cout << "Base func(string)" << endl; }
  6. };
  7. class Derived : public Base {
  8. public:
  9. void func() { // 这个会隐藏基类的所有func
  10. cout << "Derived func()" << endl;
  11. }
  12. // 把基类的其他func版本引入
  13. using Base::func; // 引入func(int)和func(string)
  14. };
  15. int main() {
  16. Derived d;
  17. d.func(); // 自己的
  18. d.func(10); // 现在可以了!
  19. d.func("hi"); // 现在可以了!
  20. }
复制代码
  1. class Base {
  2. public:
  3. Base(int x) : data(x) { cout << "Base(int)" << endl; }
  4. Base(string s) : data(0) { cout << "Base(string)" << endl; }
  5. Base(double d) : data(static_cast<int>(d)) { cout << "Base(double)" << endl; }
  6. private:
  7. int data;
  8. };
  9. class Derived : public Base {
  10. public:
  11. using Base::Base; // 继承基类的所有构造函数
  12. // 还可以添加自己的构造函数
  13. Derived() : Base(0) { cout << "Derived()" << endl; }
  14. };
  15. int main() {
  16. Derived d1(10); // 使用继承的Base(int)
  17. Derived d2("hello"); // 使用继承的Base(string)
  18. Derived d3(3.14); // 使用继承的Base(double)
  19. Derived d4; // 使用自己的Derived()
  20. }
  21. //如果不使用using,则需要在初始化列表中显示调用
复制代码

类型兼容性

可以将后代类的对象赋值给祖先类对象,反之不行

原因:每个派生类对象包含一个基类子对象,这意味着可以将派生类对象当作基类对象使用

  1. BASE obj1;
  2. Y1 obj2;
  3. obj1=obj2;//将obj2中基类部分的内容赋给obj1
  4. obj2=obj1;//error
复制代码

基类对象的指针或引用也可以指向或引用公有派生类的对象,反之不行(class Derived::public BASE)

只适用于公有派生!!!

效果:引用/指向的是派生类对象重的基类子对象

  1. BASE* p;
  2. Y1* p1;
  3. p=&obj1;
  4. p=&obj2;
  5. p=p1;
  6. p1=&obj1;//error
  7. p1=&obj2;
复制代码
  1. class Base {
  2. public:
  3. void display() ;
  4. };
  5. class D1: public Base {
  6. public:
  7. void display() ;
  8. };
  9. class D2: public D1 {
  10. public:
  11. void display() ;
  12. };
  13. void Base::display()
  14. {
  15. cout<<"Base::display()"<<endl;
  16. }
  17. void D1:: display()
  18. {
  19. cout<<"D1::display()"<<endl;
  20. }
  21. void D2:: display()
  22. {
  23. cout<<"D2::display()"<<endl;
  24. }
  25. void fun(Base *ptr)
  26. {
  27. ptr->display();
  28. }
  29. int main()
  30. {
  31. Base b; //声明Base类对象
  32. D1 d1; //声明D1类对象
  33. D2 d2; //声明D2类对象
  34. Base *p; //声明Base类指针
  35. p=&b; //Base类指针指向Base类对象
  36. fun(p);
  37. p=&d1; //Base类指针指向D1类对象
  38. fun(p);
  39. p=&d2; //Base类指针指向D2类对象
  40. fun(p);
  41. }
复制代码

运行结果:Base::display() Base::display(); Base::display();(指向基类子对象)

对象的类型转换

向上转型upcasting:将派生类对象视为基类对象,将派生类的对象/指针赋值给基类的对象/指针(允许隐式转换)

向下转型downcasting:将基类对象视为派生类对象(需要显式转型)一般使用运算符dynamic_cast(类型转换运算符,具有动态类型检查的功能,比static_cast更安全)

  1. base *p=new derived(1);
  2. derived *p1=new derived(2);
  3. p=p1;
  4. p1=dynemic_cast<derived*>(p);
复制代码

Casts

static_cast静态类型转换

是一种在编译期间完成的类型转换操作,用于讲义中类型的值显示地转换为另一种类型,也同时提供比C语言中的强制类型转换更多的类型检查和安全性

缺点:没有运行时类型检查保证转换安全性

使用情况:基本类型转化,将派生类值或指针赋值给基类

const_cast

用于去除常指针/引用的常量性

const常变量:

可以通过指向这个常变量的指针改变常变量的值,所以通过指针访问该变量得到的是改变以后的值

直接使用变量名,得到的依旧是原来的常量值(这是因为在编译时变量名被自动替换成常量值,运行时的改动对此无影响)

  1. #include <iostream>
  2. using namespace std;
  3. int main() {
  4. const int i = 100; //常变量
  5. const int *p = &i;
  6. int *q = const_cast<int*>(p);
  7. int j = i;
  8. *q=1;
  9. cout << i << endl << j<<endl
  10. << *p << endl << *q <<endl;
  11. return 0;
  12. }
  13. //输出结果100 100 1 1
复制代码
reinterpret_cast

任意类型的重新解释,可以随意转换但不安全。

dynamic_cast

动态类型转换,在运行时检查类型安全(失败返回NULL),支持向下转型,常用于对含有虚函数的抽象类进行向下转型

多重继承

对于前文提到的多重继承关系,有这么一个要求:同一个概念不必直接继承多次,但允许间接继承多次。

格式: class 派生类名:继承访问控制1 基类名,继承访问控制2 基类名2,...

  1. #include <iostream>
  2. using namespace std;
  3. // 基类1:打印机
  4. class Printer {
  5. public:
  6. void print() {
  7. cout << "正在打印文件..." << endl;
  8. }
  9. };
  10. // 基类2:扫描仪
  11. class Scanner {
  12. public:
  13. void scan() {
  14. cout << "正在扫描文件..." << endl;
  15. }
  16. };
  17. // 派生类:多功能一体机(同时继承打印机和扫描仪)
  18. class AllInOne : public Printer, public Scanner {
  19. public:
  20. void copy() {
  21. cout << "正在复印文件..." << endl;
  22. scan(); // 先扫描
  23. print(); // 再打印
  24. }
  25. };
  26. int main() {
  27. AllInOne device;
  28. device.print(); // 来自Printer
  29. device.scan(); // 来自Scanner
  30. device.copy(); // 自己的功能
  31. return 0;
  32. }
复制代码

虚基类

在菱形继承中,派生类会两次间接继承同一基类,导致派生类中存在两个基类子对象,产生数据冗余和二义性,而虚基类就是用来解决这个问题

操作:在继承访问控制前加保留字virtual

效果:派生类中虚基类的对应成员只有一个副本

构造函数调用次序

最先调用虚基类的构造函数,其次调用普通基类的构造函数(声明顺序),再调用对象成员的构造函数,最后执行派生类的构造函数

析构函数的调用顺序相反

  1. class Base {public: int i;};
  2. class Base1: virtual public Base {
  3. public: int j;
  4. };
  5. class Base2: virtual public Base {
  6. public: int k;
  7. };
  8. class Derived: public Base1, public Base2 {
  9. public: int sum;
  10. };
  11. int main() {
  12. Derived obj; // 创建一个派生类对象
  13. obj.i = 3; // 正确:从Base继承的i在Derived中只有一份
  14. obj.j = 5; // 正确:使用从Base1继承的j
  15. obj.k = 7; // 正确:使用从Base2继承的k
  16. return 0;
  17. }
复制代码

四、多态

让我们来看这样一个情景

  1. #include<iostream>
  2. using namespace std;
  3. class Animal{
  4. public:
  5. void eat(){
  6. cout<<"I like food!"<<endl;
  7. }
  8. };
  9. class Cat: public Animal{
  10. public:
  11. void eat(){
  12. cout<<"I like fish!"<<endl;
  13. }
  14. };
  15. class Dog: public Animal{
  16. public:
  17. void eat(){
  18. cout<<"I like meat!"<<endl;
  19. }
  20. };
  21. void prefer(Animal& animal){
  22. animal.eat();
  23. }
  24. int main(){
  25. Animal &&cat=Cat();
  26. Animal &&dog=Dog();
  27. prefer(cat);
  28. prefer(dog);
  29. return 0;
  30. }
复制代码

然而输出结果并非如我们所愿,是两个"I like food!",而我们可以通过虚函数解决这个问题

虚函数

虚函数是前面有关键字virtual的成员函数,作用是:

提醒在公有继承中的一个或多个派生类要对虚函数进行重定义

且当使用基类的指针或者引用调用派生类对象的此函数时,将调用该对象的重定义版本

所以对代码进行如下简单修改,我们就能得到想要的结果

  1. class Animal{
  2. public:
  3. virtual void eat(){
  4. cout<<"I like food!"<<endl;
  5. }
  6. };
复制代码

注意事项:

在公有继承层次中,某个类的成员函数声明为virtual。则其直接或间接派生类函数签名的函数都是虚函数,不论是否用virtual再次声明(当函数名相同但参数列表不同时,不会覆盖基类版本,而是隐藏基类同名函数)

任何虚函数,既可以被继承,也可能被其派生类再次重定义,重定义后依然是虚函数

覆盖和隐藏

覆盖override:修改基类虚函数定义

形式:派生类定义函数必须要有与基类虚函数同样的函数签名,即函数名,参数类型,顺序,数量相同)

效果:与同名虚函数语义相关,将修改/重定义虚函数的实现语义

隐藏:屏蔽基类的函数定义

形式:函数名相同但参数列表有差异,函数签名相同但基类函数没有virtual关键字

效果:与同名的继承函数是语义无关的,将屏蔽继承成员(重新声明)

差异:

基类的指针或者引用调用派生类对象的此函数,覆盖调用的是虚函数版本(语义相关,重定义),隐藏调用的是基类函数(语义无关,重声明)

将派生类对象赋值给基类对象时效果相同,都是按基类行为调用函数,因为都只赋值了派生类对象中的基类子对象部分。

多态

多态特指相同的一个标识符指代不同的行为/不同类型的数据,而多态标识符的语义要由语境上下文(具体的数据类型)决定。

eg:对于不同的动物,eat这一行为是不同的,即dog.eat()和cat.eat()的调用结果是不一样的

满足多态定义的标识符

1.函数重载  

2.覆盖,即虚函数

3.泛型/模板,即参数化类型

绑定:多态标识符必须指派具体的函数或方法以事先规定的语义

静态绑定:在运行前由编译完成指派,成为静态绑定

动态绑定:运行期间完成指派,称为动态绑定

函数重载操作符重载,模板是静态绑定,覆盖是动态绑定

多态——函数重载

  1. class Apple{ /*可以随意填入成员*/ };
  2. class Banana{ /*可以随意填入成员*/ };
  3. class Cherry{ /*可以随意填入成员*/ };
  4. void say(Apple fruit) {printf("I'm an apple!\n");}
  5. void say(Banana fruit){printf("I'm a banana!\n");}
  6. void say(Cherry fruit){printf("I'm a cherry!\n");}
  7. int main(){
  8. Apple a;
  9. Banana b;
  10. Cherry c;
  11. say(a);
  12. say(b);
  13. say(c);
  14. }
复制代码
  1. int abs(int x) { // 整数类型数据的绝对值函数
  2. cout << "Using integer version of abs().\n";
  3. return (x >= 0 ? x : -x);
  4. }
  5. double abs(double x) { // 浮点类型数据的绝对值函数
  6. cout << "Using floating-point version of abs().\n";
  7. return (x >= 0.0 ? x : -x);
  8. }
  9. int main() {
  10. cout << abs(-5) << "\n"; // 调用abs()的整数版本
  11. cout << abs(3.14) << "\n"; // 调用abs()的浮点版本
  12. return 0;
  13. }
复制代码

静态类型与动态类型

  1. //Animal父类,Dog子类
  2. Animal mydog=new Dog();
复制代码

静态类型:编译时确定的类型,是声明变量时写的类型(左侧Animal)

动态类型:运行时确定的类型,是对象真正在内存中创建时的类型。(动态绑定,右边Dog())

静态类型的静态绑定

对于下面这两段代码,编译时编译器会沿Apple(a的静态类型)继承树向上找到最近的say成员函数定义

  1. class Fruit{
  2. public:
  3. void say() {
  4. printf("I'm a fruit!\n");
  5. }
  6. };
  7. class Apple : public Fruit{
  8. };
  9. int main(){
  10. Apple a;
  11. a.say();
  12. }
复制代码
  1. class Fruit{
  2. public:
  3. void say() {
  4. printf("I'm a fruit!\n");
  5. }
  6. };
  7. class Apple : public Fruit{
  8. public:
  9. void say() {
  10. printf("I'm an apple!\n");
  11. }
  12. };
  13. int main(){
  14. Apple a;
  15. a.say();
  16. }
复制代码

对于如下代码,编译器的编译过程如下(基类指针指向派生类对象)

检查fptr的静态类型Fruit,检查say是否是虚函数:不是

既然不是虚函数,则使用静态绑定,编译器在编译阶段确定:fptr是Fruit类型的指针,调用的say不是虚函数,则调用Fruit::say()

  1. class Fruit{
  2. public:
  3. void say() {
  4. printf("I'm a fruit!\n");
  5. }
  6. };
  7. class Apple : public Fruit {
  8. public:
  9. void say() {
  10. printf("I'm an apple!\n");
  11. }
  12. };
  13. int main(){
  14. Apple a;
  15. Fruit *fPtr = &a;
  16. fPtr->say();
  17. }
复制代码

动态类型的动态绑定(在基类成员函数前加上virtual关键字)

该类及其子类apple都是多态类型

当多态类指针或引用调用虚函数时,则产生多态现象,即调用指针所指向的对象的成员函数

  1. class Fruit{
  2. public:
  3. virtual void say() { printf("I'm a fruit!\n"); }
  4. };
  5. class Apple : public Fruit {
  6. public:
  7. void say() override { printf("I'm an apple!\n"); }
  8. };
  9. int main(){
  10. Apple a;
  11. Fruit *fPtr = &a;
  12. fPtr->say();
  13. }
复制代码

抽象类

对于许多基类,比如Animal,Person,其概念往往是抽象的,这就意味着我们很难编写其成员函数,即使编写出来,也不符合认知逻辑,纯虚函数可以解决这个问题

纯虚函数:用“=0”作为虚函数声明后缀,表示该函数是纯虚函数、

纯虚函数不建议给定义,若纯虚函数是析构函数则必须提供

抽象类(Abstract class):定义继承了至少一个虚函数的类

要点:

1.抽象类不能实例化

仅能作为基类指针/引用,因为不可能存在抽象对象,也不要能显示转为抽象类对象

能声明指针或引用,指代自己的派生类对象

不能作为函数参数类型或者返回值的类型

2.抽象类的子类依旧是抽象类,除非所有虚函数都被覆盖为非纯虚函数

  1. class Animal {
  2. public:
  3. virtual void eat() = 0;
  4. };
复制代码

接口

抽象类天然适合定义接口,接口只说明“做什么”,而不关心“怎么做”,换言之,接口只声明不实现,具有抽象性、契约性、多态性,是实现多态的基础。

虚析构函数

在处理多态时,我们常常用基类指针指向派生类对象,但这么做其实会有内存泄露的风险,我们来看下面这个例子。

  1. #include <iostream>
  2. class Base {
  3. public:
  4. Base() { std::cout << "Base Constructor\n"; }
  5. // 注意:析构函数不是虚函数
  6. ~Base() { std::cout << "Base Destructor\n"; }
  7. };
  8. class Derived : public Base {
  9. public:
  10. Derived() {
  11. std::cout << "Derived Constructor\n";
  12. // 假设派生类申请了一些资源,比如动态内存
  13. data = new int[100];
  14. }
  15. ~Derived() {
  16. std::cout << "Derived Destructor\n";
  17. delete[] data; // 释放资源
  18. }
  19. private:
  20. int* data;
  21. };
  22. int main() {
  23. Base* ptr = new Derived(); // 基类指针指向派生类对象
  24. delete ptr; // 通过基类指针删除对象
  25. return 0;
  26. }
复制代码

在delete ptr这步就出现了问题,由于Base类的析构函数不是虚函数,编译器直接依据静态类型Base*调用析构函数,而不是调用Derived类的析构函数,这就导致Derived中申请的内存没有被释放,发生内存泄漏。

  1. virtual ~Base() { std::cout << "Base Destructor\n"; }
复制代码

对语句做以上修改后,就能正确调用析构函数,完全的释放内存

调用顺序为:首先调用derived类的虚构函数,在成功调用后,调用Base类析构函数,完成对整个对象的释放。

RTTI

RTTI(Run TImeType Identification)即通过运行时类型识别,程序能用基类的指针或引用来检查这些指针或引用所指的对象的实际派生类型

RTTI提供了两个操作符:

typeid操作符:返回指针和引用所指的实际类型

dynamic_cast:将基类类型的指针或引用安全的转换为派生类类型的指针或引用

typeid运算符

必须包含头文件

typeid(...)指代一个表示对应类型的std::type_info对象,若传入引用,则为被引用的类型

  1. int x = 10;
  2. int& ref = x; // ref 是 x 的引用
  3. // typeid 会怎么识别?
  4. cout << typeid(x).name(); // 输出:int
  5. cout << typeid(ref).name(); // 输出:int(与ref 无关,只看本质是 int)
复制代码

在有虚函数的前提下,typeid返回指针所指向的实际类型

  1. class Animal {
  2. public:
  3. virtual ~Animal() {} // 虚函数是关键!
  4. };
  5. class Dog : public Animal {};
  6. class Cat : public Animal {};
  7. Animal* animal1 = new Dog(); // 包装上写的是"Animal",但里面实际是狗
  8. Animal* animal2 = new Cat(); // 包装上写的是"Animal",但里面实际是猫
  9. std::cout << typeid(*animal1).name(); // 输出:Dog(不看"Animal"标签,看出是狗)
  10. std::cout << typeid(*animal2).name(); // 输出:Cat(不看"Animal"标签,看出是猫)
复制代码

输出类型名的方法

  1. cout << typeid(*animal1).name();
复制代码

比较方法(==,!=)

  1. void handleAnimal(Animal* animal) {
  2. if (typeid(*animal) == typeid(Dog)) {
  3. cout << "这是一只狗,让它摇尾巴";
  4. Dog* dog = (Dog*)animal; // 安全转换
  5. dog->wagTail();
  6. }
  7. else if (typeid(*animal) == typeid(Cat)) {
  8. cout << "这是一只猫,让它爬树";
  9. Cat* cat = (Cat*)animal;
  10. cat->climbTree();
  11. }
  12. }
复制代码

指针与解引用的区别!!!

  1. Animal* animal = new Dog();
  2. typeid(animal).name() // 输出:Animal*
  3. typeid(*animal).name() // 输出:Dog
复制代码

当表达式指向一个多态类型的对象,typeid会对这个对象求值(不能为空),并返回对象实际类型

  1. Animal& ref = dog; // dog 是 Dog 类型
  2. typeid(ref) // ✅ 会去检查 ref 真正指向谁 → 发现是 Dog
复制代码

当指向普通类型时,typeid不会对该对象求值,直接根据编译时类型信息返回(静态类型)

  1. int x = 10;
  2. int& ref = x;
  3. typeid(ref) // 不求值,直接返回 int(因为 int 没有虚函数)
  4. // 甚至可以对不存在的对象使用
  5. int* p = nullptr;
  6. typeid(p) // 安全!因为 p 没有虚函数,不求值,直接返回 int*
复制代码
dynamic_cast类型转换运算符

  1. int main() {
  2. //向上 派生类到基类
  3. Animal *a = new Donald();
  4. //向下 基类到派生类
  5. Duck *d = dynamic_cast<Duck*>(a);
  6. //侧向
  7. Actor *c = dynamic_cast<Actor*>(d);
  8. delete c;
  9. }
复制代码
final说明符

final用来禁止任何进一步的继承或重写

final类

  1. class Base final { // 这个类被标记为 final
  2. };
  3. class Derived : public Base { //错误,不可继承
  4. };
复制代码

final函数

  1. class Base {
  2. public:
  3. virtual void func() final { // 这个虚函数被标记为 final
  4. cout << "Base::func\n";
  5. }
  6. virtual void other() {
  7. cout << "Base::other\n";
  8. }
  9. };
  10. class Derived : public Base {
  11. public:
  12. // void func() override { //错误!不能重写 final 函数
  13. // cout << "Derived::func\n";
  14. // }
  15. void other() override { //可以重写,因为没加 final
  16. cout << "Derived::other\n";
  17. }
  18. };
复制代码

五、模板template

泛型编程

泛型编程是编写独立于任何特定数据类型的程序,从而可以用相同的代码处理不同类型的数据(对象)

在C++中,使用模板(template)来泛型编程,包括:函数模板,类模板

泛型编程是一种编译时多态性(静态多态),其中,数据的数据类型本身是参数化的,因而程序具有多态特征,当用模板代码创建实例代码时,具体的数据类型才被确定,这也叫泛型程序的实例化

函数模板

函数模板通过使用相同的处理过程,以处理不同类型的数据,从而实现减少代码,甚至处理编程时未知数据类型的作用

函数模板的一般形式

  1. template <模板形参表>
  2. 返回值类型 函数名(形式参数列表){
  3. 函数语句体
  4. }
  5. template <typename T>
  6. void Swap( T& v1, T& v2)
  7. {
  8. T temp;
  9. temp = v1;
  10. v1 = v2;
  11. v2 = temp;
  12. }
复制代码

函数模板的调用形式与普通函数调用相同,这就是函数模板的实例化

  1. int main()
  2. {
  3. std::string s1("rabbit"), s2("bear");
  4. int iv1 = 3, iv2 = 5;
  5. double dv1 = 2.8, dv2 = 8.5;
  6. Swap(s1, s2);
  7. Swap(iv1, iv2);
  8. Swap(dv1, dv2);
  9. }
复制代码

函数模板的调用过程:

1.推断模板实参:编译器根据函数调用中实参类型,确定好相应的模板实参

2.模板实参确定之后,编译器就使用模板实参代替相应的模板形参,产生并编译函数模板的一个特定版本/实例,此过程不进行常规隐式类型转换

模板特化

在函数模板中有特殊类型,需要对特殊类型进行特殊处理,这个时候要用到函数模板的特化

特化方法:在正常函数模板下面接着编写代码,写一个空的template<>,然后写具体的函数定义

  1. template <typename T>
  2. void Swap( T& v1, T& v2)
  3. {
  4. T temp;
  5. temp = v1;
  6. v1 = v2;
  7. v2 = temp;
  8. }
  9. template <>
  10. void Swap( int & v1, int & v2)
  11. {
  12. int temp;
  13. temp = v1;
  14. v1 = v2;
  15. v2 = temp;
  16. v1 += 10;
  17. v2 += 10;
  18. }
复制代码
函数模板重载

1.定义名字相同而函数形参表不同的函数模板

2.定义与函数模板同名的非模板函数(正常函数),在其函数体中完成不同的行为

函数调用的静态绑定规则(重载协议)

step1:如果一同名的非模板函数的形参类型正好与函数调用实参类型匹配,则调用该函数,否则进入step2

step2:首先匹配函数模板的特化,再匹配函数模板的主体,若匹配不成功,则进入step3

step3:对函数调用实参进行隐式类型转化后与非模板函数再次进行匹配,若能找到匹配函数则调用,否则进入step4

step4:编译错误

  1. // 函数模板demoPrint(两个参数)
  2. template <typename T>
  3. void demoPrint(const T v1, const T v2){
  4. cout << "the first version of demoPrint()" << endl;
  5. cout << "the arguments: " << v1 << " " << v2 << endl;
  6. }
  7. // 函数模板demoPrint的指定特化-char类型
  8. template <>
  9. void demoPrint(const char v1, const char v2){
  10. cout << "the specify special of demoPrint()" << endl;
  11. cout << "the arguments: " << v1 << " " << v2 << endl;
  12. }
  13. // 函数模板demoPrint重载的函数模板(一个参数)
  14. template <typename T>
  15. void demoPrint(const T v){
  16. cout << "the second version of demoPrint()" << endl;
  17. cout << "the argument: " << v << endl;
  18. }
  19. // 非函数模板demoPrint (两个参数)
  20. void demoPrint(const double v1, const double v2){
  21. cout << "the nonfunctional template version of demoPrint()" << endl;
  22. cout << "the arguments: " << v1 << " " << v2 << endl;
  23. }
  24. int main(){
  25. string s1("rabbit"), s2("bear");
  26. char c1('k'), c2('b');
  27. int iv1 = 3, iv2 = 5;
  28. double dv1 = 2.8, dv2 = 8.5;
  29. // 调用第一个函数模板
  30. demoPrint(iv1, iv2);
  31. // 调用第一个函数模板的特化
  32. demoPrint(c1, c2);
  33. // 调用第二个函数模板
  34. demoPrint(iv1);
  35. // 调用非函数模板
  36. demoPrint(dv1, dv2);
  37. // 隐式转换后调用非函数模板
  38. demoPrint(iv1, dv2);
  39. return 0;
  40. }
  41. /* 结果
  42. the first version of demoPrint()
  43. the arguments: 3 5
  44. the specify special of demoPrint()
  45. the arguments: k b
  46. the second version of demoPrint()
  47. the argument: 3
  48. the nonfunctional template version of demoPrint()
  49. the arguments: 2.8 8.5
  50. the nonfunctional template version of demoPrint()
  51. the arguments: 3 8.5 */
复制代码

类模板

使用情景:定义可以存放任意类型对象的通用容器类,eg:栈,队列

类模板的一般形式

  1. template <模板形参表>
  2. class 类模板名
  3. {
  4. 类成员声明
  5. ...
  6. }
复制代码

在类模板外定义成员函数一般形式

  1. template <模板形参表>
  2. 返回值类型 类模板名<模板形参名列表>::函数名(函数形参表)
  3. {
  4. 函数实现
  5. ...
  6. }
复制代码

举个例子:类模板用链表实现栈

  1. template <typename ElementType>
  2. class Stack {
  3. public:
  4. Stack();
  5. ~Stack();
  6. void push(ElementType obj);
  7. void pop();
  8. ElementType getTop() const;
  9. bool isEmpty() const;
  10. private:
  11. /* 栈节点类型 */
  12. struct Node
  13. {
  14. ElementType element; // 结点中存放的元素
  15. Node* next; // 指向下一结点的指针
  16. };
  17. Node* top; // 栈顶节点
  18. };
  19. template <typename ElementType>
  20. void Stack<ElementType>::push( ElementType obj )
  21. {
  22. Node* temp;
  23. temp = new Node;
  24. temp -> element = obj;
  25. temp -> next = top;
  26. top = temp;
  27. }
  28. template <typename ElementType>
  29. void Stack<ElementType>::pop()
  30. {
  31. Node* temp;
  32. if (top != NULL) {
  33. temp = top;
  34. top = top -> next;
  35. delete temp;
  36. }
  37. }
  38. template <typename ElementType>
  39. ElementType Stack<ElementType>::getTop() const
  40. {
  41. if (top != NULL)
  42. {
  43. return top->element;
  44. }
  45. }
  46. template <typename ElementType>
  47. bool Stack<ElementType>::isEmpty() const
  48. {
  49. return top == NULL;
  50. }
复制代码

主函数部分

  1. int main()
  2. {
  3. Stack<int> stack; // 实例化一个保存int型元素的栈
  4. for (int i = 1; i < 9; i++) // 向栈中压入8个元素
  5. stack.push(i);
  6. while (!stack.isEmpty()) { // 栈不为空时循环
  7. cout << stack.getTop() << " "; // 显示栈顶元素
  8. stack.pop(); // 弹出栈顶元素
  9. }
  10. }
复制代码

类模板实例化一般形式:类模板名<模板实参表>

当运用模板编程时如果使用原来声明与实现分开的方式(声明.hpp,实现.cpp),由于模板需要实例化,在链接时才能访问实现太晚了,编译器必须能够访问模板定义的源代码

大部分情况使用将声明和实现合并一起写在.hpp中的方法

非类型模板形参

非类型模板形参相当于模板内部常量(类型固定),形式上类似普通的函数邢丹,其对应的模板实参必须是编译时常量表达式

  1. /* 类模板:使用数组实现的栈*/
  2. template <typename ElementType, int N//非类型模板参数>
  3. class Stack {
  4. public:
  5. Stack();
  6. ~Stack();
  7. void push(ElementType obj);
  8. void pop();
  9. ElementType getTop() const;
  10. bool isEmpty() const;
  11. private:
  12. /* 栈节点类型 */
  13. ElementType elements[N];
  14. int count;
  15. };
  16. int main()
  17. {
  18. Stack<int, 10> stack;
  19. for (int i = 1; i < 9; i++)
  20. stack.push(i);
  21. while (!stack.isEmpty()) {
  22. cout << stack.getTop() << " ";
  23. stack.pop();
  24. }
  25. }
复制代码

数组引用和数组指针

  1. template <typename T, int N>
  2. void printValues(T (&arr)[N]) { // arr 是对数组 T[N] 的引用
  3. for (int i = 0; i != N; ++i)
  4. cout<< arr[i] << endl;
  5. }
  6. int main()
  7. {
  8. int intArr[6] = {1, 2, 3, 4, 5, 6};
  9. double dblArr[4] = {1.2, 2.3, 3.4, 4.5};
  10. // 生成函数实例printValues(int (&) [6])
  11. printValues(intArr);
  12. // 生成函数实例printValues(double (&) [4])
  13. printValues(dblArr);
  14. return 0;
  15. }
复制代码
  1. template <typename T, int N>
  2. void printValues(T (*arr)[N]) { // arr 是指向数组 T[N] 的指针
  3. for (int i = 0; i != N; ++i)
  4. cout<< (*arr)[i] << endl;
  5. }
  6. int main()
  7. {
  8. int intArr[6] = {1, 2, 3, 4, 5, 6};
  9. double dblArr[4] = {1.2, 2.3, 3.4, 4.5};
  10. // 生成函数实例printValues(int (&) [6])
  11. printValues(&intArr);
  12. // 生成函数实例printValues(double (&) [4])
  13. printValues(&dblArr);
  14. return 0;
  15. }
复制代码

六、STL

STL(Standard Template Library,标准模板库)是C++标准库的核心组成部分,它提供了一套通用的模板类和函数,实现了常用的数据结构和算法。STL的设计理念是将数据操作分离,通过迭代器作为桥梁连接二者。

STL的六大核心组件

容器、算法、迭代器、函数对象、适配器、分配器

用我们经常涉及到的vector举一个例子

  1. #include <vector>
  2. #include <iostream> // exp1.cpp
  3. using namespace std;
  4. int main()
  5. {
  6. vector<int> ivec = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; // ivec包含10个元素,值分别为0~9
  7. ivec.front() = 100; // 将第0个元素修改为100
  8. cout << "the first element: " << ivec[0] << endl; // 输出第0个元素的值
  9. ivec[1] = 102; // 将第1、第2个元素修改为102、103
  10. ivec.at(2) = 103;
  11. cout << "the second element: " << ivec.at(1) <<endl; //输出第1、第2个元素的值
  12. cout << "the third element: " << ivec[2] << endl;
  13. ivec.back() = 999; //将最后一个元素修改为999
  14. cout << "the last element: " << ivec[9] << endl; // 输出最后一个元素的值
  15. }
复制代码

vector是动态的连续数组(长度可变,内存连续)

vector实现了两种元素访问方式:

操作符[]:不检查下表是否越界       

访问元素函数:返回元素左值(返回的是元素的引用,可以直接修改对应的函数值)

front ,at(int) ,back     (at检查下标越界,抛出异常)

  1. #include <iostream>
  2. #include <vector>
  3. using std::vector;
  4. int main()
  5. {
  6. vector<int> ivec(10, 2); // 创建含10个值为2的元素的vector容器
  7. vector<int>::iterator iter; // 声明迭代器对象-正向
  8. vector<int>::reverse_iterator riter; // 声明迭代器对象-反向
  9. iter = ivec.begin(); // 获取指向第0个元素的迭代器
  10. *iter += 10; // 将第0个元素的值加10
  11. iter = ivec.end(); // iter指向最后一个元素的下一位置
  12. *(iter - 1) = 100; // 将最后一个元素的值改为100
  13. riter = ivec.rbegin(); // riter指向最后一个元素
  14. *riter -= 20; // 将最后一个元素的值减20
  15. riter = ivec.rend(); // riter指向第0个元素的前一位置
  16. *(riter - 1) += 10; // 将第0个元素的值加10
  17. }
复制代码

迭代器是用于遍历容器元素的指针的包装

vertor提供正向和反向的迭代器:通过成员函数begin(),end()返回正向迭代器对象实例,通过成员函数rbegin(),rend()返回反向迭代器对象实例

单项迭代器中对运算符进行了重载:自增运算符,算术运算符是指针的移动,而关系运算符比较的是指针地址(后面的元素指针地址大)

容器

容器时存储其他对象的对象,每个容器提供一种适配大部分类型的数据结构

容器分为四种:顺序容器,关联式容器,无序关联式容器,容器适配器

迭代器是每种容器个自定义的一个或多个不同于容器的类,换言之,每个容器都有属于自己的迭代器,像vector::iterator,主要用于访问修改增加删除容器中的元素

顺序容器
array

静态连续数组

vector动态连续数组
deque双端队列
forward_list单链表
list双向链表
顺序容器的构造函数

仅创建空容器

  1. C(容器类型) <T(存储数据类型)> c;
复制代码

复制另一个容器,换言之,创建容器c作为容器cx的副本要求c和cx必须是同类型而且存储元素类型也相同

  1. C(容器类型) <T(存储数据类型)> c(cx);
复制代码

c中存放迭代器b和e范围内的元素的副本(b到e左闭右开,即e所指向的元素不计入副本,只计入到前一个元素)不要求容器类型相同,存储元素类型可转化相同

  1. C(容器类型) <T(存储数据类型)> c(begin,end);
复制代码

c中存储n个值为t的元素,t必须是T类型或者可以转换为T类型

  1. C(容器类型) <T(存储数据类型)> c(n,t);
复制代码

c中存放n个元素,每个元素都是T类型的初始化值

  1. C(容器类型) <T(存储数据类型)> c(n);
复制代码

c中存储初始化列表中的元素

  1. C(容器类型) <T(存储数据类型)> c({...});
  2. C(容器类型) <T(存储数据类型)> c={...};
复制代码
访问元素
形式参数返回值
c.back()最后一个元素的引用备注容器为空时操作无意义
c.front()第0个元素的引用备注容器为空时操作无意义
c[index]元素下标下标index元素的引用

list不提供该操作

下标越界无意义

c.at(index)元素下标下标index元素的引用

list不提供该操作

进行越界检查

迭代器
返回值

c.begin()

c.cbegin()

迭代器,指向容器c的第零个元素

c.end()

c.cend()

迭代器,指向容器c中最后一个元素的下一个位置

c.rbegin()

c.crbegin()

逆向迭代器,指向容器c的最后一个元素

c.rend()

c.crend()

逆向迭代器,指向容器c中第零个元素的前一个位置
插入元素
形式参数返回值
c.insert(iter,t)iter常迭代器,t为元素值返回指向新插入元素的迭代器在iter指向元素之前插入值为t的元素
c.insert(iter,n,t)iter常迭代器,t为元素值在iter指向元素之前插入n个值为t的元素
c.insert(iter,begin,end)

iter常迭代器

begin,end迭代器

在iter指向元素之前插入b和e所指范围内元素(不包括e)
c.push_back(t)t为元素值在c的尾端加入值为t的元素
c.push_front(t)t为元素值在c的头端加入值为t的元素
删除元素
形式参数返回值效果
c.clear()删除所有元素
c.erase(iter)迭代器返回指向被删除元素的下一元素的迭代器删除iter指向元素iter=c.end()操作无意义
c.erase(b,e)迭代器返回指向被删除元素段的下一元素的迭代器删除b和e范围内的所有元素(不包括e)
c.pop_back()删除容器最后一个元素容器空无意义
c.pop_front()删除容器第一个元素

容器空无意义

vector不提供该操作

比较操作

所有比较操作要求左右容器类型必须相同

==:若两个容器元素个数相同,对应位置每个元素与相等,返回true,否则false  (!=相反)

<,<=,>,>=:若一个容器中所有元素与另一容器中开头一段元素对应相等,则阶段的容器小于另一容器,否则第一对不相等元素比较结果就是容器比较结果

容量操作

c.empty():容器为空返回true,否则返回false

c.size():返回容器中目前所存放的元素的数目

c.max_size():返回容器中可存放元素的最大数目

c.resize(n):将容器的大小调整为可以存放n个元素

当n删除多余元素,否则在尾端加入相应数目新元素,新元素采用值初始化

c.resize(n,t):新增元素的值为t,其余相同

赋值与交换
形式参数效果
c1=c2容器先删除c1中的所有元素,然后将c2中元素复制给c1c1c2同类型容器,元素类型相同
c.assign(b,e)迭代器先删除c中的所有元素,然后将b和e范围内元素复制到c中(不包括e)b和e不能指向c中元素,元素类型需要兼容
c.assign(n,t)n元素数目,t元素值先删除c1中的所有元素,然后在c中存放n个值为t的元素
c1.swap(c2)容器交换c1,c2所有元素,实际上是交换名称c1c2同类型容器,元素类型相同
关联容器

这里的数据结构我们以前都并未涉及,所以我在这里选择先行跳过,等到了解了《数据结构与算法》后再对这一部分进行学习

容器适配器

容器适配器基于混旭日能勾起,提供了一个不同的,有特殊功能的接口,以此定义一种新的,具有特定行为的数据结构

stack栈

特点:后进先出(LIFO)

限制:你只能操作栈顶的元素,不能遍历整个栈,也不能随机访问中间的某个元素。

底层容器:默认使用deque,但也可以使用vector和list

基础操作:pop(移除栈顶元素)push(栈顶插入元素)top(访问栈顶元素)

queue队列

特点:先进先出(FIFO)

限制:只能操作队首和队尾,不能访问中间元素。

底层容器:默认使用deque,也可以使用list(不能用vector,由于队列需要批犯进行头部操作)

基础操作:push(队尾插入元素)pop(移除队首元素)front(访问队首元素)back(访问队尾元素)

priority_queue优先队列

特点:优先级最高的元素总是最先被取出,内部结构是由堆数据结构实现

限制:元素按优先级排序,而非插入顺序。无法直接访问中间的特定元素。

底层容器:默认使用vector,也可以使用deque(支持随机访问实现对堆内部的调整)

基础操作:push(按顺序插入元素)pop(删除优先级最高的元素)top(访问优先级最高的元素)

当使用默认底层容器时,不需要包含底层容器的头文件

  1. #include <stack>
  2. std::stack<int> s; // 默认用 deque,deque 的头文件会被 stack 间接包含
复制代码

指定非默认容器时,需要包含头文件

  1. #include <stack>
  2. #include <vector>
  3. #include <list>
  4. std::stack<int, std::vector<int>> s1;
  5. std::stack<int, std::list<int>> s2;
复制代码

举一个大家比较熟悉的例子,用栈检查回文序列

  1. #include<iostream>
  2. #include<stack>
  3. using namespace std;
  4. int main() {
  5. char s[100];
  6. cin >> s;
  7. stack<char> t;
  8. for (int i = 0; s[i] != '\0'; i++) {
  9. if (t.empty()!=true&&s[i] == t.top()) {
  10. t.pop();
  11. }
  12. else {
  13. t.push(s[i]);
  14. }
  15. }
  16. if (t.empty()) {
  17. cout << 1;
  18. }
  19. else {
  20. cout << 0;
  21. }
  22. return 0;
  23. }
复制代码

迭代器

输入迭代器:只读,解引用操作的结果只能作为右值使用

输出迭代器:可写,解引用操作结果可以作为左值使用

  1. int main() {
  2. // DEMO1: read only iterator
  3. const vector<int> iv {1,2,3,4,5,6,7};
  4. auto it1 = iv.begin();
  5. // *it1 = 3; // error: assignment of read-only location
  6. set<int> is {1,2,1,3,1,4};
  7. auto it2 = is.begin();
  8. // *it2 = 5; // error: set迭代器是只读的
  9. }
复制代码

还有正向迭代器,双向迭代器,随机访问迭代器

auto

auto关键字:当不能明确写出类型时,编译器回根据初始化表达式自行判断类型,减少对类型的依赖,确保类型匹配,避免类型名过长

注意:必须立即初始化,不能用于函数参数,不能用于推导数组类型

遍历容器新语法:for(auto x:container){}

container要求:有begin()和end()方法,支持!=,++,*操作

x为每次遍历容器中的各个元素

for(auto x:container)拷贝元素,适用于基本类型或你明确需要副本时

for(auto& x:container)修改元素,或避免拷贝大对象

for(const auto& x:container)只读访问,避免拷贝

七、异常处理

程序错误

编译错误:语法错误,链接错误(未定义引用,重复定义,库文件缺失),语义错误

静态分析错误:潜在bug,安全问题,性能问题

运行错误:逻辑错误(程序正常运行,未得到期望结果),内存分配,指针越界...

健壮性

健壮性是指一个计算机系统在执行过程中处理错误,以及算法在遭遇输入运算异常时保持正常运行的能力

程序终止

正常终止:执行正常结束而终止

异常终止:程序执行中发生错误或特殊事件而终止(可预测错误,用户自定义错误,难以预测错误)

异常处理

异常处理机制的基本思想:采用结构化方法对程序运行时错误(可预测或特殊事件)进行显式管理

效果:将程序中正常处理代码与异常处理代码显式区别开,提高可读性

结构化方法

结构化定义异常:将异常种类定义为树状结构(将程序中可能出现的各种错误(异常),按照它们的性质和关系,组织成一个像“树”一样的分类体系。)

结构化处理异常(异常检测与异常处理分离)

异常检测:异常检测部分检测到异常存在时,抛出一个异常对象交给异常处理代码

异常处理:在程序或函数特定未知,集中捕获异常对象,再处理异常

抛出异常程序段

  1. throw 表达式;
复制代码

捕获并处理异常程序段

  1. try{
  2. 保护段
  3. }catch(异常类型声明){
  4. 异常处理语句
  5. }
复制代码
  1. #include <iostream>
  2. #include <stdexcept>//标准异常库
  3. #include <vector>
  4. using namespace std;
  5. void testFunction(int choice) {
  6. if (choice == 1) {
  7. throw out_of_range("索引越界异常");
  8. }
  9. else if (choice == 2) {
  10. throw bad_alloc(); // 内存分配异常
  11. }
  12. else if (choice == 3) {
  13. throw runtime_error("运行时错误");
  14. }
  15. }
  16. int main() {
  17. for (int i = 1; i <= 4; i++) {
  18. try {
  19. cout << "\n测试选择 " << i << ":" << endl;
  20. testFunction(i);
  21. cout << "执行成功" << endl;
  22. }
  23. catch (const out_of_range& e) {
  24. cout << "越界异常: " << e.what() << endl;
  25. }
  26. catch (const bad_alloc& e) {
  27. cout << "内存分配异常: " << e.what() << endl;
  28. }
  29. catch (const runtime_error& e) {
  30. cout << "运行时异常: " << e.what() << endl;
  31. }
  32. }
  33. return 0;
  34. }
复制代码

注:catch(...)负责处理前面未截获的其他异常,必须放在最后

Try-Catch执行机制

若程序有异常,则通过throw关键字创建一个异常对象,并抛出

将可能抛出异常的长须段嵌再try子句的保护段之中,当执行到try语句时,就执行保护段语句

若保护段语句执行期间没有引发异常,则跳过所有的catch语句,执行try-catch语段后面的语句

若出现异常,catch子句按其再try子句后出现的顺序检查,类型匹配的catch子句将捕获并处理异常(或继续抛出异常)若找不到匹配catch语句,将自动调用标准库函数terminate,默认调用abort()终止程序

备注:检查出一个异常就直接进入catch,不会全部运行保护段检查所有异常

throw关键字
  1. throw;//用于当前异常再抛出
复制代码

throw会将捕获到的语句再次抛出,有函数调用链中更上层的函数处理异常沿函数调用栈反向追溯,从当前函数返回调用它的函数)

  1. throw 表达式;//表达式可以是任意类型的对象
复制代码

执行throw语句时,先创建抛出对象副本,在函数返回前原始对象被析构,而副本作为异常对象传递,进入catch语句,catch语句结束后副本被析构

如果再当前函数的try语句块中,则终止try子句执行,执行catch语句捕获(若当前函数无法捕获处理异常对象,则将异常向上层传递)

  1. #include <iostream>
  2. #include <stdexcept>
  3. void func2() {
  4. std::cout << "func2:抛出异常" << std::endl;
  5. throw std::runtime_error("func2 的错误"); // 抛出异常
  6. }
  7. void func1() {
  8. std::cout << "func1:调用 func2" << std::endl;
  9. func2(); // 没有 try-catch,异常继续向上传播
  10. std::cout << "func1:这行不会执行" << std::endl;
  11. }
  12. int main() {
  13. try {
  14. std::cout << "main:调用 func1" << std::endl;
  15. func1(); // 异常从这里向上传播到 main
  16. } catch (const std::exception& e) {
  17. std::cout << "main 捕获到异常:" << e.what() << std::endl;
  18. //what()返回描述异常信息的字符串
  19. }
  20. std::cout << "main:程序继续执行" << std::endl;
  21. return 0;
  22. }
复制代码
C++异常结构树

先分清楚两个头文件

中不包含std::exception定义,但内部包含头文件;换言之,日常使用即可而特殊需求(自定义异常基类或终止处理)需要

基类std::exception的三大分支:logic_error,runtimr_error,bad_系列

logic_error(头文件

std::domain_error(参数超出定义域)        std::invalid_argument(参数无效)        std::out_of_range(下标越界)        std::length_error (操作超出对象长度限制)

runtime_error(头文件

std::range_error(范围错误)        std::overflow_error(数值太大超出表示范围)        std::underflow_error(数值太小失去精度)

bad_系列

std::bad_alloc(头文件,内存分配失败)

std::bad_cast(头文件,动态转换失败,仅在引用类型dynamic_cast失败抛出)

atd::bad_typeid(头文件,当typeid操作符的参数是空指针的解引用时抛出)

std::bad_exception(头文件,在异常处理遇到未预期异常)

在异常类中使用explicit关键字,禁止隐式类型转换,强制显示构造,确保异常类型明确,不被混淆。

以下是一个out_of_range的例子

  1. // out_of_range example
  2. #include <iostream> // std::cerr
  3. #include <stdexcept> // std::out_of_range
  4. #include <vector> // std::vector
  5. int main (void) {
  6. std::vector<int> myvector(10);
  7. try {
  8. myvector.at(20)=100; // vector::at throws an out_of_range
  9. }
  10. catch (const std::out_of_range& oor) {
  11. std::cerr << "Out of Range error: " << oor.what() << '\n';
  12. }
  13. return 0;
  14. }
复制代码
 异常匹配机制     

按catch顺序与异常声明匹配,如果异常对象类型是声明类型或其子类,则匹配成功,若异常接收变量是值,则复制异常到接受变量,若异常接收变量是指针/引用,则创建引用/指针

要点:子类catch必须要优先于基类catch,(...)最后catch

           只有捕捉引用类型的基类异常,且基类存在虚函数,才能实现多态

所以,异常捕获一般使用引用

异常传播

从异常抛出到转移到合适的异常处理子句的过程就是异常传播

异常传播过程:当前函数未处理异常,则交给调用函数(向上层传递)

  1. #include <iostream>
  2. #include <stdexcept>
  3. // 三个简单的函数,展示异常如何向上传播
  4. void funcC() {
  5. std::cout << "C开始执行" << std::endl;
  6. throw std::runtime_error("C出错了!"); // 异常在这里产生
  7. std::cout << "C结束执行" << std::endl; // 这行不会执行
  8. }
  9. void funcB() {
  10. std::cout << "B开始执行" << std::endl;
  11. funcC(); // 调用C
  12. std::cout << "B结束执行" << std::endl; // 这行不会执行
  13. }
  14. void funcA() {
  15. std::cout << "A开始执行" << std::endl;
  16. funcB(); // 调用B
  17. std::cout << "A结束执行" << std::endl; // 这行不会执行
  18. }
  19. int main() {
  20. std::cout << "main开始执行" << std::endl;
  21. try {
  22. funcA(); // 调用A
  23. }
  24. catch (const std::exception& e) {
  25. std::cout << "捕获到异常: " << e.what() << std::endl;
  26. }
  27. std::cout << "main结束执行" << std::endl;
  28. return 0;
  29. }
复制代码
noexcept关键字
  1. void f() noexcept{}//函数不会抛出异常
复制代码
  1. void f() noexcept(false){}//函数可能抛出异常
复制代码

必须使用noexcept的函数:

1.移动构造函数和移动赋值运算符(否则容器会使用拷贝导致性能降低)

2.析构函数

3.swap函数

4.getter函数

5.简单计算函数

用户自定义异常
  1. #include<stdexcept>
  2. #include<iostream>
  3. using namespace std;
  4. class Divided_By_Zero:public runtime_error{
  5. public:
  6. explicit Divided_By_Zero(const string& s = "Divide By Zero"):runtime_error(s){};
  7. explicit Divided_By_Zero(const char* s):runtime_error(string(s)){};
  8. };
  9. int main() {
  10. double a = 10, b = 0, res;
  11. char Operator = '/';
  12. try {
  13. if (b == 0) throw Divided_By_Zero();
  14. res = a / b;
  15. cout << a << " / " << b << " = " << res;
  16. }
  17. catch(const exception& e) {
  18. cout << e.what() << endl;
  19. }
  20. return 0;
  21. }
复制代码
对象构造异常:

当对象构造异常时,会导致对象本身无法被完全构造,成员对象由于是独立对象,已将完成构造,析构函数会在异常传播时被正常调用,而对于已经分配内存的原始指针(eg:char*),由于依赖析构函数释放内存,而此时对象没有完成构造就抛出异常导致无法调用析构函数,就会发生内存泄露

  1. #include <iostream>
  2. #include <stdexcept>
  3. class Resource {
  4. public:
  5. Resource(const char* name) : name_(name) {
  6. std::cout << "Resource构造: " << name_ << std::endl;
  7. }
  8. ~Resource() {
  9. std::cout << "Resource析构: " << name_ << std::endl;
  10. }
  11. private:
  12. const char* name_;
  13. };
  14. class ComplexClass {
  15. private:
  16. Resource r1{"r1"}; // 成员对象1
  17. Resource r2{"r2"}; // 成员对象2
  18. Resource* r3; // 原始指针成员
  19. public:
  20. ComplexClass(){
  21. std::cout << "ComplexClass构造开始" << std::endl;
  22. r3 = new Resource("r3"); // 手动分配
  23. throw std::runtime_error("构造失败");
  24. }
  25. ~ComplexClass() {
  26. std::cout << "ComplexClass析构" << std::endl;
  27. delete r3; // 需要手动释放
  28. }
  29. };
  30. int main() {
  31. try {
  32. ComplexClass obj;
  33. }
  34. catch (const std::exception& e) {
  35. std::cout << "异常: " << e.what() << std::endl;
  36. }
  37. }
  38. /*
  39. Resource构造: r1
  40. Resource构造: r2
  41. ComplexClass构造开始
  42. Resource构造: r3 ← 这里分配了
  43. Resource析构: r2 ← 成员对象析构
  44. Resource析构: r1 ← 成员对象析构
  45. 异常: 构造失败
  46. */
复制代码

RAII(Resource Acquisition Is Initialization)

资源获取即初始化,即利用RAII类(eg:vector,string)或智能指针实现自动的资源管理,避免忘记管理导致资源的泄露

RAII本质:让对象的构造函数和析构函数来管理资源的生命周期,利用C++的自动销毁机制,确保资源永远被正确释放!

原理:利用对象生命周期特征,构造函数申请资源,析构函数释放资源,在对象构造和生命周期结束时自动调用

RAII类的要求

有构造函数申请资源,有析构函数释放资源,不能允许简单的浅拷贝(会导致多次释放)

智能指针

智能指针也是RAII技术中的一种

在这里我们学习三种智能指针:unique_ptr,shared_ptr,weak_ptr

  1. #include <iostream>
  2. #include <memory>
  3. #include <vector>
  4. // ===== 1. unique_ptr(独占所有权)=====
  5. void unique_ptr演示() {
  6. std::cout << "\n=== unique_ptr(独占)===" << std::endl;
  7. // 创建
  8. std::unique_ptr<int> p1 = std::make_unique<int>(42);
  9. std::cout << "p1指向的值: " << *p1 << std::endl;
  10. // 不能拷贝,只能移动
  11. // std::unique_ptr<int> p2 = p1; // 编译错误!
  12. std::unique_ptr<int> p2 = std::move(p1); // 转移所有权
  13. // p1现在为空
  14. if (!p1) {
  15. std::cout << "p1已空" << std::endl;
  16. }
  17. std::cout << "p2指向的值: " << *p2 << std::endl;
  18. // 自动释放内存
  19. } // p2析构,释放内存
  20. // ===== 2. shared_ptr(共享所有权)=====
  21. void shared_ptr演示() {
  22. std::cout << "\n=== shared_ptr(共享)===" << std::endl;
  23. // 创建
  24. std::shared_ptr<int> p1 = std::make_shared<int>(100);
  25. std::cout << "p1引用计数: " << p1.use_count() << std::endl; // 1
  26. // 可以拷贝,引用计数增加
  27. std::shared_ptr<int> p2 = p1;
  28. std::cout << "拷贝后引用计数: " << p1.use_count() << std::endl; // 2
  29. std::shared_ptr<int> p3 = p1;
  30. std::cout << "再拷贝后引用计数: " << p1.use_count() << std::endl; // 3
  31. // 所有指针指向同一内存
  32. *p1 = 200;
  33. std::cout << "p2指向的值: " << *p2 << std::endl; // 200
  34. std::cout << "p3指向的值: " << *p3 << std::endl; // 200
  35. // 离开作用域时,引用计数递减
  36. // 最后一个shared_ptr析构时才释放内存
  37. }
复制代码

当两个对象相互引用时,会产生引用计数均不为1的情况,这个时候由于相互持有,导致两个对象都无法释放,我们把这种现象叫做循环引用,weak_ptr就是用来解决这种情况的

  1. #include <iostream>
  2. #include <memory>
  3. using namespace std;
  4. class Son;
  5. class Father {
  6. public:
  7. shared_ptr<Son> son; // 爸爸拥有儿子
  8. ~Father() { cout << "爸爸走了\n"; }
  9. };
  10. class Son {
  11. public:
  12. weak_ptr<Father> father; // 儿子知道爸爸,但不拥有(弱引用)
  13. ~Son() { cout << "儿子走了\n"; }
  14. void callDad() {
  15. // 先用 expired() 检查
  16. if (father.expired()/*检查是否被销毁*/) {
  17. cout << "[expired检查] 爸爸已经不在了\n";
  18. } else {
  19. cout << "[expired检查] 爸爸还在\n";
  20. // 再用 lock() 使用
  21. if (auto dad = father.lock()) {
  22. //若对象存活,则返回其shared_ptr,否则返回空指针
  23. cout << " 找到爸爸了,可以和他说话\n";
  24. }
  25. }
  26. }
  27. void checkDadStatus() {
  28. cout << "爸爸的状态: "
  29. << (father.expired() ? "已去世" : "还活着")
  30. << " (引用计数: " << father.use_count() << ")\n";
  31. }
  32. };
  33. int main() {
  34. cout << "=== 程序开始 ===\n\n";
  35. // 创建关系
  36. auto dad = make_shared<Father>();
  37. auto son = make_shared<Son>();
  38. dad->son = son;
  39. son->father = dad;
  40. cout << "1. 刚开始时:\n";
  41. son->checkDadStatus();
  42. son->callDad();
  43. cout << "\n2. 爸爸走后:\n";
  44. dad.reset(); // 引用计数减1,销毁对象
  45. son->checkDadStatus();
  46. son->callDad();
  47. cout << "\n=== 程序结束 ===\n";
  48. return 0;
  49. }
复制代码

八、结语

是的没错,到这里,这篇C++面向对象编程的学习笔记就接近尾声了,这篇5万字左右的文章,是我这个懵懵懂懂的初学者,靠着AI和自己愚昧的理解一点点写出来的,当然,在关联容器部分甚至还有缺失,后续还会有所补充和改动,如果你读到这里,是我这个小小作者的荣幸,如果你只是放在收藏夹里吃灰,我也要感谢你的捧场,希望将来有一天他可能会对你有所帮助,但是朋友,总而言之言而总之,我们汇聚在这里,我们没有去追随大部分人走的捷径,而是选择静下心来一点点啃这些C++的底层知识,是繁杂,是难懂,但是,我想,在AI也能够写代码的年代里,需要有人有坐下来一点点学技术、一行行读代码的勇气,这是AI做不到的,也是很多人做不到的,还是那句圣经:计算机没有黑魔法,代码的世界很大,是我这篇小小文章包含不了的,希望我们都能保持着最淳朴的初心,在追求技术的道路上走下去

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

中国红客联盟公众号

联系站长QQ:5520533

admin@chnhonker.com
Copyright © 2001-2026 Discuz Team. Powered by Discuz! X3.5 ( 粤ICP备13060014号 )|天天打卡 本站已运行