├── .gitignore
├── C++
├── README.md
├── 样例
│ ├── README.md
│ ├── codes
│ │ └── nothing.cpp
│ ├── docs
│ │ └── nothing.md
│ └── imgs
│ │ └── img_1.jpg
├── 第一讲
│ ├── C++_Google_Style简介
│ │ ├── README.md
│ │ ├── codes
│ │ │ ├── base.cpp
│ │ │ ├── base.h
│ │ │ └── main.cpp
│ │ ├── docs
│ │ │ └── Google Coding Style.md
│ │ └── imgs
│ │ │ └── 01.png
│ └── C++_多文件组织简介
│ │ ├── README.md
│ │ ├── codes
│ │ ├── main.cpp
│ │ ├── occupation.cpp
│ │ ├── occupation.h
│ │ ├── salary.cpp
│ │ └── salary.h
│ │ ├── docs
│ │ └── 多文件组织.md
│ │ └── imgs
│ │ ├── img01.png
│ │ └── img02.png
├── 第七讲
│ ├── 继承与多态总结
│ │ ├── codes
│ │ │ ├── pointer_reference_to_base.cpp
│ │ │ ├── virtual_destructor.cpp
│ │ │ ├── virtual_func_abcd.cpp
│ │ │ └── virtual_func_animal.cpp
│ │ ├── docs
│ │ │ └── 继承与多态总结.md
│ │ └── readme.md
│ └── 虚函数
│ │ ├── README.md
│ │ ├── codes
│ │ ├── dynamic_cast.cpp
│ │ └── 虚函数.cpp
│ │ └── docs
│ │ └── 虚函数.md
├── 第三讲
│ ├── 左值与右值简介
│ │ ├── README.md
│ │ ├── docs
│ │ │ └── 左值与右值.md
│ │ └── imgs
│ │ │ └── 01.png
│ └── 移动语义简介
│ │ ├── README.md
│ │ ├── codes
│ │ └── 复制构造函数.cpp
│ │ └── docs
│ │ └── 移动语义.md
├── 第九讲
│ ├── 桥接模式简介
│ │ ├── codes
│ │ │ ├── bridge_pattern.h
│ │ │ └── main.cpp
│ │ ├── docs
│ │ │ └── 桥接模式简介.md
│ │ └── readme.md
│ └── 迭代器模式简介
│ │ ├── README.md
│ │ ├── codes
│ │ └── iterator.cpp
│ │ ├── docs
│ │ └── 迭代器模式简介.md
│ │ └── imgs
│ │ └── img01.png
├── 第二讲
│ ├── C++ 指针用法总结
│ │ ├── README.md
│ │ ├── codes
│ │ │ ├── Examples_1.cpp
│ │ │ └── Examples_2.cpp
│ │ └── docs
│ │ │ └── C++指针用法.md
│ └── C++ 智能指针简介
│ │ ├── README.md
│ │ ├── codes
│ │ ├── Examples_1.cpp
│ │ ├── Examples_2.cpp
│ │ └── Examples_3.cpp
│ │ └── docs
│ │ └── 三类智能指针.md
├── 第五讲
│ ├── C++11语法糖杂讲
│ │ ├── README.md
│ │ ├── codes
│ │ │ └── type_name.cpp
│ │ └── docs
│ │ │ └── C++11语法糖.md
│ └── C++_const
│ │ ├── README.md
│ │ ├── codes
│ │ ├── constants.h
│ │ └── draft.cpp
│ │ └── docs
│ │ └── Sast tutor 2022-const.md
├── 第八讲
│ ├── 工厂模式
│ │ ├── README.md
│ │ ├── codes
│ │ │ ├── 工厂方法模式.cpp
│ │ │ └── 简单工厂模式.cpp
│ │ └── docs
│ │ │ └── 工厂模式.md
│ └── 设计模式原则简介
│ │ ├── README.md
│ │ ├── codes
│ │ ├── OCP_1.cpp
│ │ ├── OCP_2.cpp
│ │ ├── OCP_3.cpp
│ │ └── SRP.cpp
│ │ └── docs
│ │ └── design pattern.md
├── 第六讲
│ └── 函数对象、lambda与参数绑定
│ │ ├── README.md
│ │ ├── codes
│ │ ├── 1FunctionObject.cpp
│ │ ├── 2lambda.cpp
│ │ └── 3bind.cpp
│ │ ├── docs
│ │ └── 第六讲:函数对象、lambda表达式与参数绑定.md
│ │ └── imgs
│ │ └── image1.png
├── 第十讲
│ └── README.md
└── 第四讲
│ ├── static修饰符用法总结
│ ├── README.md
│ ├── codes
│ │ ├── linkage
│ │ │ ├── 1.cpp
│ │ │ └── 2.cpp
│ │ ├── mem_fun.cpp
│ │ └── zoo.cpp
│ └── docs
│ │ └── Static.md
│ └── 命名空间简介
│ ├── README.md
│ ├── codes
│ ├── aliases.cpp
│ ├── namespace1.cpp
│ ├── namespace2.cpp
│ └── 多文件namespace
│ │ ├── add.h
│ │ ├── main.cpp
│ │ ├── math.cpp
│ │ └── subtract.h
│ └── docs
│ └── Namespace.md
├── MATLAB
├── 2022MatPyFly
│ ├── 第一讲
│ │ ├── README.md
│ │ ├── codes
│ │ │ ├── MATLAB_hw_week1.mlx
│ │ │ ├── MATLAB_optional_week1.mlx
│ │ │ └── MATLAB_tutor_week1.mlx
│ │ └── docs
│ │ │ └── MATLAB_安装指南.pdf
│ ├── 第三讲
│ │ ├── MATLAB_hw_week3.mlx
│ │ ├── MATLAB_tutor_week3.mlx
│ │ └── README.md
│ └── 第二讲
│ │ ├── README.md
│ │ └── codes
│ │ ├── MATLAB_hw_week2.mlx
│ │ ├── MATLAB_tutor_week2.mlx
│ │ ├── circle.m
│ │ ├── data
│ │ ├── data1.mat
│ │ ├── data2.txt
│ │ ├── data3.xlsx
│ │ ├── data4.csv
│ │ ├── image1.bmp
│ │ └── image2.mat
│ │ ├── data_hw
│ │ ├── data1.mat
│ │ ├── data2.xlsx
│ │ ├── data3.mat
│ │ ├── image1.png
│ │ └── image_22
│ │ │ ├── image1.png
│ │ │ ├── image10.png
│ │ │ ├── image11.png
│ │ │ ├── image12.png
│ │ │ ├── image2.png
│ │ │ ├── image3.png
│ │ │ ├── image4.png
│ │ │ ├── image5.png
│ │ │ ├── image6.png
│ │ │ ├── image7.png
│ │ │ ├── image8.png
│ │ │ └── image9.png
│ │ ├── fmax.m
│ │ └── fun1.m
├── README.md
└── 快速入门
│ ├── MATLAB_QuickStart_Array.mlx
│ ├── MATLAB_QuickStart_Char_and_String.mlx
│ └── test_image
│ ├── img1.jpg
│ ├── img2.jpg
│ ├── img3.jpg
│ └── img4.jpg
├── Python
├── 2022MatPyFly
│ ├── 第一讲
│ │ ├── README.md
│ │ ├── codes
│ │ │ ├── bin.txt
│ │ │ ├── dec.txt
│ │ │ ├── python_assignment_01.ipynb
│ │ │ └── python_tutorial_01.ipynb
│ │ └── docs
│ │ │ ├── Python安装指南(Windows版).pdf
│ │ │ └── Python安装指南(mac版本) .pdf
│ ├── 第三讲
│ │ ├── Dataset.rar
│ │ ├── README.md
│ │ ├── codes
│ │ │ ├── Common
│ │ │ │ ├── DataLoader.py
│ │ │ │ └── __pycache__
│ │ │ │ │ └── DataLoader.cpython-38.pyc
│ │ │ ├── LogisticRegression.py
│ │ │ ├── python_assignment_03.ipynb
│ │ │ └── python_tutorial_03.ipynb
│ │ └── docs
│ │ │ ├── 2021年夏季学期Python程序设计大作业指引.pdf
│ │ │ ├── Python安装指南(Windows版).pdf
│ │ │ └── python安装指南(mac版本).pdf
│ └── 第二讲
│ │ ├── README.md
│ │ └── codes
│ │ ├── files
│ │ ├── bin.txt
│ │ ├── dec.txt
│ │ └── example.txt
│ │ ├── python_assignment_02.ipynb
│ │ └── python_tutorial_02.ipynb
├── README.md
└── 其它
│ ├── 文档查阅
│ ├── myfile.py
│ └── 文档查阅.ipynb
│ └── 迭代_推导_生成基础
│ ├── data.txt
│ ├── 推导.ipynb
│ └── 迭代.ipynb
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | /.vscode
2 |
--------------------------------------------------------------------------------
/C++/README.md:
--------------------------------------------------------------------------------
1 | # 文件清单
2 |
3 | ---
4 |
5 | - 样例/
:仅仅为了说明该如何出C++的教程
6 | - 第一讲/
:C++多文件组织简介,C++ Google Style简介
7 | - 第二讲/
:C++指针用法总结,C++智能指针简介
8 | - 第三讲/
:左值与右值简介,移动语义简介
9 | - 第四讲/
:static修饰符用法总结,命名空间简介
10 | - 第五讲/
:const用法总结,C++11语法糖杂讲
11 | - 第六讲/
:函数对象与Lambda表达式简介,参数绑定简介
12 | - 第七讲/
:继承与多态总结,虚函数深入理解
13 | - 第八讲/
:设计模式原则简介,工厂模式简介
14 | - 第九讲/
:桥接模式简介,迭代器模式简介
15 | - 第十讲/
:泛型编程与STL简介
16 |
--------------------------------------------------------------------------------
/C++/样例/README.md:
--------------------------------------------------------------------------------
1 | # 样例
2 |
3 | ## 简介
4 | 这里写本教程的内容,之后出推送的时候可以直接把它复制到推送里。
5 |
6 | ## 文件清单
7 | 一般来说,一个C++教程单元中有以下三个子文件夹:
8 | - docs/
:用来存放.md
教程文件
9 | - imgs/
:用来存放docs/
中教程所需图片(注意路径)
10 | - codes/
:用来存放能运行的代码示例
11 |
12 |
13 | 各位在写这一块的时候,只要说明下各文件夹下有什么文件、有什么作用就行。
14 |
15 | ## 参考资料
16 | 咱们C++教程的主要参考来源于以下讲义、书籍与网站:
17 | - 清华大学计算机系姚海龙老师程序设计基础课件
18 | - 《C++ Primer》(第五版)
19 | - [cplusplus reference](https://www.cplusplus.com/reference/)
20 | - [learn Cpp](https://www.learncpp.com/)
21 | - [Microsoft C++ docs](https://docs.microsoft.com/en-us/cpp/cpp/?view=msvc-170)
22 | - [GeeksforGeeks C++ tutorials](https://www.geeksforgeeks.org/c-plus-plus/)
23 |
24 | 各位在写这部分的时候把自己参考的资料列出来就行。
25 |
26 | ---
27 |
28 | 下面是一些你们写README.md
的时候不必出现的东西:
29 |
30 | ## 有关类图的绘制
31 |
32 | 第六至十讲可能要手画一些简单的UML类图,有关C++类间关系及类图的参考资料如下:
33 | - [UML Class Diagram Explained With C++ samples](https://cppcodetips.wordpress.com/2013/12/23/uml-class-diagram-explained-with-c-samples/)
34 | - [learn Cpp](https://www.learncpp.com/)第16章
35 | - [ C++ OOD and OOP - Class Diagram in UML](https://www.youtube.com/watch?v=thbxWbneJ6o)
36 |
37 | 关于如何绘制类图,这里有两个方案:
38 |
39 | ### 方案一、PlantUML
40 | 这里推荐[PlantUML](http://www.plantuml.com/plantuml/uml/SyfFKj2rKt3CoKnELR1Io4ZDoSa70000)。有关如何使用PlantUML画UML类图的参考资料如下:
41 | - [PlantUML类图介绍](https://plantuml.com/zh/class-diagram)
42 |
43 | 生成UML类图后,只需在.md
文件中附上图片链接,并在链接前加上//https:
,即可显示。简单示例如下:
44 |
45 |

46 |
47 |
48 | 源代码如下:
49 | ```html
50 |
51 |

52 |
53 | ```
54 | - 优点:不必自己截图,且比较好看
55 | - 缺点:如果使用网站,类图加载速度慢,而且需要不断复制粘贴代码
56 |
57 | ### 方案二、
58 | mermaid
59 | Typora
中有支持,可以直接渲染。但是很遗憾,GitHub中不支持。因此,一种比较快的解决方法是直接截图(统一采用默认主题),截图后把图片存在imgs/
文件夹下。
60 |
61 | mermaid
绘制类图教程如下:
62 | - [csdn中的Mermaid类图入门教程](https://blog.csdn.net/u012787240/article/details/112847071)
63 | - [mermaid类图官方教程](https://mermaid-js.github.io/mermaid/#/classDiagram)
64 |
65 | 取得的效果如下图:
66 |
67 |

68 |
69 |
70 | 源代码如下图(若在Typora
中显示,应当能够渲染出来,但在GitHub上还是代码块):
71 | ```mermaid
72 | classDiagram
73 | classA <|-- classB : implements
74 | classC *-- classD : composition
75 | classE o-- classF : association
76 | ```
77 |
78 | - 优点:所见即所得,能在本地快速编辑
79 | - 缺点:比较朴素,并且修改的话,得重新截图
80 |
81 | ## 有关.md
文件中图片显示的问题
82 |
83 | 为了使得图片能够在非本地(如GitHub)能正常显示,你可能需要手动写下图片路径:
84 |
85 | 例如现在我处于C++/样例/README.md
,想要去显示位于MATLAB/快速入门/test_image/
中的img1.jpg
,一个恰当的做法如下:
86 |
87 |

88 |
89 |
90 | 源代码如下:
91 | ```html
92 |
93 |

94 |
95 | ```
96 |
97 | 亦即,你需要把绝对路径改成相对路径。
98 |
99 |
100 |
101 |
--------------------------------------------------------------------------------
/C++/样例/codes/nothing.cpp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ksc999/THUEEXP-SAST-Tutor/d01ad77a614a8ff9eefa3c4a64fc7e8bf7b47f08/C++/样例/codes/nothing.cpp
--------------------------------------------------------------------------------
/C++/样例/docs/nothing.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ksc999/THUEEXP-SAST-Tutor/d01ad77a614a8ff9eefa3c4a64fc7e8bf7b47f08/C++/样例/docs/nothing.md
--------------------------------------------------------------------------------
/C++/样例/imgs/img_1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ksc999/THUEEXP-SAST-Tutor/d01ad77a614a8ff9eefa3c4a64fc7e8bf7b47f08/C++/样例/imgs/img_1.jpg
--------------------------------------------------------------------------------
/C++/第一讲/C++_Google_Style简介/README.md:
--------------------------------------------------------------------------------
1 | # 第一讲
2 |
3 | ## 简介
4 |
5 | - 内容主题:`C++ Google Coding Style `
6 | - 你将学到什么:
7 | - 我们为什么需要一个统一的、优秀的代码风格?
8 | - `Google Coding Style`初步:如何让你的代码更规范?
9 |
10 | ---
11 |
12 | ## 文件清单
13 |
14 | - `docs/`
15 | - Google Coding Style.md【教程的核心内容】
16 | - `imgs/`
17 | - 01.png【教程所含的图片】
18 | - `codes/`
19 | - example.cpp【以公司人事管理系统(部分)为例体现 Coding Style 的示例代码】
20 |
21 | ---
22 |
23 | ## 参考资料
24 |
25 | - [Google C++ 风格指南--格式](https://zh-google-styleguide.readthedocs.io/en/latest/google-cpp-styleguide/formatting/)
26 | - 十分详细的中文版说明文档
27 | - 同时包含了除`C++`以外的其他`Google`开源项目的说明介绍
28 |
29 |
--------------------------------------------------------------------------------
/C++/第一讲/C++_Google_Style简介/codes/base.cpp:
--------------------------------------------------------------------------------
1 | /**
2 | * @file base.cpp
3 | * @author Zsbyqx2020
4 | * @brief implementation of all the functions defined in the .h file.
5 | * @version 0.1
6 | * @date 2022-01-14
7 | *
8 | * @copyright Copyright (c) 2022
9 | *
10 | */
11 |
12 | #include "base.h"
13 |
14 | #include
15 | #include
16 | #include
17 | using namespace std;
18 |
19 | /**
20 | * @brief Construct a new Worker object
21 | *
22 | * @param wname in "string" type, the worker's name.
23 | * @param wnum in "int" type, the worker's working number/ID.
24 | * @param wsa in "double" type, the worker's base salary.
25 | */
26 | Worker::Worker(string wname, int wnum, double wsa) {
27 | name_ = wname;
28 | num_ = wnum;
29 | base_salary_ = wsa;
30 | }
31 |
32 | /**
33 | * @brief Print the worker's basic information.
34 | */
35 | void Worker::Print_info() {
36 | cout << "Basic Information:" << endl
37 | << setw(22) << setiosflags(ios::left) << "Name" << setw(3) << ":"
38 | << name_ << endl
39 | << setw(22) << "ID" << setw(3) << ":" << num_ << endl;
40 | }
41 |
42 | /**
43 | * @brief Construct a new Employee object
44 | *
45 | * @param ename in "string" type, the employee's name.
46 | * @param enum in "int" type, the employee's working number/ID.
47 | * @param esa in "double" type, the employee's base salary.
48 | * @param ewh in "double" type, the employee's working hours during a week.
49 | * @param edep in "string" type, the employee's working department.
50 | */
51 | Employee::Employee(string emname, int emnum, double esa, double ewh,
52 | string edep)
53 | : Worker(emname, emnum, esa) {
54 | weekly_working_hours_ = ewh;
55 | department_ = edep;
56 | }
57 |
58 | /**
59 | * @brief Print the employee's basic information.
60 | */
61 | void Employee::Print_info() {
62 | Worker::Print_info();
63 | cout << setw(22) << setiosflags(ios::left) << "Weekly working hours"
64 | << setw(3) << ":" << weekly_working_hours_ << endl
65 | << setw(22) << "Working Department" << setw(3) << ":" << department_
66 | << endl
67 | << setw(22) << "Salary" << setw(3) << ":" << Salary() << endl;
68 | }
69 |
70 | /**
71 | * @brief Calculate the salary of the employee.
72 | *
73 | * @return double
74 | */
75 | double Employee::Salary() {
76 | return base_salary_ * (1 + weekly_working_hours_ / 10);
77 | }
78 |
79 | /**
80 | * @brief Construct a new Administrator object
81 | *
82 | * @param aname in "string" type, the admin's name.
83 | * @param anum in "int" type, the admin's working number/ID.
84 | * @param asa in "double" type, the admin's base salary.
85 | * @param aen in "int" type, the number of employees the admin has.
86 | * @param awy in "int" type, the admin's working years.
87 | */
88 | Administrator::Administrator(string aname, int anum, double asa, int aen,
89 | int awy)
90 | : Worker(aname, anum, asa) {
91 | employee_num_ = aen;
92 | working_years_ = awy;
93 | }
94 |
95 | /**
96 | * @brief Print the admin's basic information.
97 | */
98 | void Administrator::Print_info() {
99 | Worker::Print_info();
100 | cout << setw(22) << setiosflags(ios::left) << "Employee number" << setw(3) << ":"
101 | << employee_num_ << endl
102 | << setw(22) << "Working years" << setw(3) << ":" << working_years_ << endl
103 | << setw(22) << "Salary" << setw(3) << ":" << Salary() << endl;
104 | }
105 |
106 | /**
107 | * @brief Calculate the salary of the admin.
108 | *
109 | * @return double
110 | */
111 | double Administrator::Salary() {
112 | return base_salary_ * (1 + employee_num_ / 20 + working_years_ / 10);
113 | }
--------------------------------------------------------------------------------
/C++/第一讲/C++_Google_Style简介/codes/base.h:
--------------------------------------------------------------------------------
1 | /**
2 | * @file base.h
3 | * @author Zsbyqx2020
4 | * @brief defines all the classes and their menber functions
5 | * @version 0.1
6 | * @date 2022-01-14
7 | *
8 | * @copyright Copyright (c) 2022
9 | *
10 | */
11 |
12 | #include
13 |
14 | /**
15 | * @brief The base class.
16 | * Defines the name, the working ID and the base salary of the workers.
17 | */
18 | class Worker {
19 | public:
20 | /**
21 | * @brief Construct a new Worker object.
22 | */
23 | Worker(std::string, int, double);
24 | /**
25 | * @brief Print the basic information of the worker.
26 | */
27 | void Print_info();
28 |
29 | protected:
30 | /**
31 | * @brief name of the worker in type "string"
32 | */
33 | std::string name_;
34 | /**
35 | * @brief working number of the worker in type "int"
36 | */
37 | int num_;
38 | /**
39 | * @brief base salary of the worker in type "double"
40 | */
41 | double base_salary_;
42 | };
43 |
44 | /**
45 | * @brief Subclass of Worker.
46 | * Defines an employee of the company,
47 | * with his working hours and Department added.
48 | */
49 | class Employee : public Worker {
50 | public:
51 | /**
52 | * @brief Construct a new Employee object
53 | */
54 | Employee(std::string, int, double, double, std::string);
55 | /**
56 | * @brief Print the basic information of the employee.
57 | */
58 | void Print_info();
59 | /**
60 | * @brief Calculate the employee's salary.
61 | * @return double
62 | */
63 | double Salary();
64 |
65 | private:
66 | /**
67 | * @brief The working hours of the employee during one week.
68 | */
69 | double weekly_working_hours_;
70 | /**
71 | * @brief The department this employee works in.
72 | */
73 | std::string department_;
74 | };
75 |
76 | class Administrator : public Worker {
77 | public:
78 | /**
79 | * @brief Construct a new Administrator object
80 | */
81 | Administrator(std::string, int, double, int, int);
82 | /**
83 | * @brief Print the basic information of the administrator.
84 | */
85 | void Print_info();
86 | /**
87 | * @brief Calculate the employee's salary.
88 | * @return double
89 | */
90 | double Salary();
91 |
92 | private:
93 | /**
94 | * @brief The number of employees in the administrator's team.
95 | */
96 | int employee_num_;
97 | /**
98 | * @brief The working years of the administrator.
99 | */
100 | int working_years_;
101 | };
--------------------------------------------------------------------------------
/C++/第一讲/C++_Google_Style简介/codes/main.cpp:
--------------------------------------------------------------------------------
1 | /**
2 | * @file main.cpp
3 | * @author Zsbyqx2020
4 | * @brief the main program file.
5 | * @version 0.1
6 | * @date 2022-01-14
7 | *
8 | * @copyright Copyright (c) 2022
9 | *
10 | */
11 |
12 | #include
13 | #include
14 |
15 | #include "base.h"
16 | using namespace std;
17 |
18 | int main() {
19 | // generate an employee obj
20 | Employee emp = Employee("Little Red Hat", 20210103, 3000, 40, "Staff");
21 | // generate an administrator obj
22 | Administrator adm = Administrator("Nightfall Duke", 20200109, 4000, 30, 10);
23 | // test the Print_info() function of both objs.
24 | emp.Print_info();
25 | cout << endl;
26 | adm.Print_info();
27 | return 0;
28 | }
--------------------------------------------------------------------------------
/C++/第一讲/C++_Google_Style简介/imgs/01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ksc999/THUEEXP-SAST-Tutor/d01ad77a614a8ff9eefa3c4a64fc7e8bf7b47f08/C++/第一讲/C++_Google_Style简介/imgs/01.png
--------------------------------------------------------------------------------
/C++/第一讲/C++_多文件组织简介/README.md:
--------------------------------------------------------------------------------
1 | # 第一讲
2 |
3 | ## 简介
4 |
5 | - 内容主题:`C++ 多文件组织简介`
6 | - 你将学到什么:
7 | - 什么是项目?什么是文件
8 | - 源文件变成可执行文件的过程
9 | - 多文件组织的原因
10 | - 多文件组织规范
11 |
12 | ---
13 |
14 | ## 文件清单
15 |
16 | - `docs/`
17 | - 多文件组织.md【教程的核心内容】
18 | - `imgs/`
19 | - img01.png【教程所含的图片】
20 | - img02.png【教程所含的图片】
21 | - `codes/`
22 | - main.cpp【以公司人事管理系统(部分)为例体现**多文件组织**的示例代码】
23 | - occupation.cpp【类的实现代码】
24 | - occupation.h【类的定义代码】
25 | - salary.cpp【函数的声明代码】
26 | - salary.h【函数的定义代码】
27 |
28 | ---
29 |
30 | ## 参考资料
31 |
32 | - [GCC编译过程(预处理->编译->汇编->链接) - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/111500914)
33 | - [Compiling and Linking - Cprogramming.com](https://www.cprogramming.com/compilingandlinking.html)
34 | - [2.10 — Header files – Learn C++ (learncpp.com)](https://www.learncpp.com/cpp-tutorial/header-files/)
35 | - [2.11 — Header guards – Learn C++ (learncpp.com)](https://www.learncpp.com/cpp-tutorial/header-guards/)
36 | - [12.11 — Class code and header files – Learn C++ (learncpp.com)](https://www.learncpp.com/cpp-tutorial/class-code-and-header-files/)
37 |
38 |
--------------------------------------------------------------------------------
/C++/第一讲/C++_多文件组织简介/codes/main.cpp:
--------------------------------------------------------------------------------
1 | #include "occupation.h"
2 | #include "salary.h"
3 |
4 | #include
5 | #include
6 | using namespace std;
7 |
8 | int main() {
9 | Coder cod1 = Coder(111, 3000);
10 | Coder cod2 = Coder(222, 5000);
11 | cout << "woker_num:" << cod1.Get_num()
12 | <<" year_salary:" << Cou_year_salary(cod1.Get_month_salary()) << endl;
13 | cout << "woker_num:" << cod2.Get_num()
14 | <<" year_salary:" << Cou_year_salary(cod2.Get_month_salary()) << endl;
15 | system("pause");
16 | return 0;
17 | }
--------------------------------------------------------------------------------
/C++/第一讲/C++_多文件组织简介/codes/occupation.cpp:
--------------------------------------------------------------------------------
1 | #include "occupation.h"
2 |
3 | #include
4 | using namespace std;
5 |
6 | Coder::Coder(int wnum, double wsa) {
7 | num_ = wnum;
8 | month_salary_ = wsa;
9 | }
10 |
11 | int Coder::Get_num() {
12 | return num_;
13 | }
14 |
15 | double Coder::Get_month_salary() {
16 | return month_salary_;
17 | }
--------------------------------------------------------------------------------
/C++/第一讲/C++_多文件组织简介/codes/occupation.h:
--------------------------------------------------------------------------------
1 | #ifndef OCCUPATION_H
2 | #define OCCUPATION_H
3 |
4 | class Coder {
5 | public:
6 | Coder(int, double);
7 | int Get_num();
8 | double Get_month_salary();
9 |
10 | private:
11 | int num_;
12 | double month_salary_;
13 | };
14 |
15 | #endif
--------------------------------------------------------------------------------
/C++/第一讲/C++_多文件组织简介/codes/salary.cpp:
--------------------------------------------------------------------------------
1 | #include "salary.h"
2 |
3 | #include
4 | using namespace std;
5 |
6 | double Cou_day_salary(double month_salary) {
7 | double day_salary = month_salary / 30;
8 | return day_salary;
9 | }
10 |
11 | double Cou_year_salary(double month_salary) {
12 | double year_salary = month_salary * 12;
13 | return year_salary;
14 | }
--------------------------------------------------------------------------------
/C++/第一讲/C++_多文件组织简介/codes/salary.h:
--------------------------------------------------------------------------------
1 | #ifndef SALARY_H
2 | #define SALARY_H
3 |
4 | double Cou_day_salary(double);
5 |
6 | double Cou_year_salary(double);
7 |
8 | #endif
--------------------------------------------------------------------------------
/C++/第一讲/C++_多文件组织简介/docs/多文件组织.md:
--------------------------------------------------------------------------------
1 | # 第一讲:多文件组织
2 |
3 | **什么是项目?什么是文件?**这些问题的答案或许没有出现在程设的课堂上,但我们其实一直在和它们打交道:使用Visual Studio完成作业的同学都知道,打开Visual Studio后会首先进入“创建新项目”的界面,选择“空项目”之后便会来到我们熟悉的操作界面,然后点击源文件→添加→新建项,最后创建“源.cpp”就可以愉快coding了。
4 |
5 |
6 |

7 |
8 | **通过以上这波操作,我们其实已经创建了项目和文件。**如上图所示,我创建了名为“mult_file”的项目,又在该项目下创建了一个名为“源.cpp”的文件。通俗来讲,文件就是某一形式的代码信息的集合,其命名会以.cpp、.h等结尾,为了实现某一功能而将文件组合起来,便形成项目,一个项目中往往包含多个文件。为了进一步了解使用多文件的原因以及多文件组织的规范,我们有必要先了解一下代码变成可以被计算机执行的程序之前,都经历了什么。
9 |
10 |
11 |
12 | ## 1.预处理、编译、汇编和链接
13 |
14 | c++代码会放在源文件(命名后缀.cpp)或头文件(命名后缀.h)中,它们经编译器处理后生成可执行文件(命名后缀.exe),整个过程包括**预处理、编译、汇编、链接**四个步骤。负责进行这些操作的程序被分别称作**预处理器、编译器、汇编器、链接器**,再次我们把它们四个的集合成为**编辑器**。
15 |
16 |
17 |
18 | ### 1.1预处理(Preprocessing)
19 |
20 | 预处理操作是针对预处理指令的操作。所谓**预处理指令**是指以“#”开头的指令,包括`#include`、宏定义命令`#define`等。在进行预处理时,会将预处理指令对应的内容插到源程序中,例如将`#include`指定的内容插入程序中、将程序中使用的宏定义进行替换。
21 |
22 | 预处理会将.cpp、.h文件转换成.i文件,由于插入了预处理指令对应的内容,.i文件相比对应的.cpp文件会大很多。.i文件仍旧是文本文件,可以通过文本编辑器打开。
23 |
24 | 将try1.cpp进行预处理得到try1.i的命令为:`g++ -E try1.c -o try1.i`
25 |
26 | > 此命令是GCC编译器的命令,对于Windows安装MinGW后即可在命令行中使用。Mac OS内置clang编辑器,基本语法命令与GCC编译器类似,可参考[clang常用语法介绍 - 简书 (jianshu.com)](https://www.jianshu.com/p/96058bf1ecc2)或直接百度。
27 |
28 |
29 |
30 | ### 1.2编译(Compilation)
31 |
32 | 编译是指将经过预处理得到的文件转换成特定汇编代码的过程,编译后会生成对应的.s文件,这个文件不会像.o文件那么臃肿,也是文本文件,可以通过文本编辑器查看。
33 |
34 | 将try1.i进行编译得到try1.s的命令为:`g++ -S try1.i -o try1.s`
35 |
36 |
37 |
38 | ### 1.3汇编(Assemble)
39 |
40 | 汇编步骤会把编译得到的汇编代码转换成机器码,产生.o\\.obj文件,该文件是二进制格式。
41 |
42 | 将try1.s进行汇编得到try1.o的命令为:`g++ -c try1.s -o try1.o`
43 |
44 |
45 |
46 | ### 1.4链接(Linking)
47 |
48 | 汇编得到的二进制文件并不是操作系统可以使用的可执行文件,还需要通过链接来将.o文件转换成.exe\\.out文件。链接可以视作整个过程最终的组合环节,项目中的若干.h、.cpp文件经过一步步处理产生.o文件后,链接步骤将这些.o文件组合起来,生成一个可执行文件。
49 |
50 | 在前3个步骤中编辑器都只会**查看一个文件**,而在链接时编辑器会同时**查看多个文件**。在编译时,如果编辑器找不到特定函数的定义,编辑器会假定该函数已经在另一个文件中被定义而不会报错;而在链接时,编辑器会查找未提及的函数的引用并报错。通常情况下,编译错误是由语法导致,如符号中英文出错,将main写成mian;而链接错误与缺少定义或存在多个定义有关。
51 |
52 | 将try1.o进行链接得到try1.exe的命令为:`g++ try1.o -o try1.exe`
53 |
54 |
55 |
56 | 4个步骤的先后顺序和涉及的文件如下图:
57 |
58 |
59 |

60 |
61 |
62 |
63 | ## 2.使用多文件的原因
64 |
65 | 站在**代码读者**的角度,一个项目使用多个文件就像一本书分成多个章节一样。一般的c++项目动辄上万行代码,如果把它们全部放在一个文件中,即使注释再好也不便于读者阅读;而且往往读者只会关注项目中涉及某个特定功能的代码,在庞大的单个文件中查找费时费力。所以一个项目会把实现某一功能的代码放在一个文件中,这样仅通过文件的命名就可以快速找到目标代码,预览整个项目时也会有主有次。
66 |
67 | 站在**代码作者**的角度,一个项目使用多个文件有助于编程**模块化**,例如有些代码负责运行逻辑,有些代码负责函数实现,将这些代码放在不同的文件中,分批次进行模块化编写,可以让整个编写过程更有逻辑。同时一个项目使用多个文件有助于代码的**修改和维护**,在修改和维护代码时只需要改动定义和声明部分,其余代码无需修改即可正常工作,这样修改代码后无需对整个项目重新进行预处理、编译、汇编和链接,而只需要对修改过的文件进行这些操作即可。
68 |
69 |
70 |
71 | ## 3.多文件组织规范
72 |
73 | 与单一文件相比,多文件组织的区别在于引入了多个源文件,以及引入了头文件。多个源文件意味着多文件组织必须处理文件之间的共用的变量\函数\类,这就涉及声明和定义的问题。引入头文件意味着我们必须进一步了解头文件的作用和编写规范。
74 |
75 |
76 |
77 | ### 3.1声明(Declaration)和定义(Definition)
78 |
79 | **声明**是给出变量、函数或类的名字(而不指明其内容),而**定义**是具体说明一个变量、函数或类的内容。声明变量的语法是“extern+变量类型+变量名”,例如`extern int a`,其余的声明和定义的语法都会在程设课程中提及。
80 |
81 | 多文件中声明和定义的规范只有一条,就是**函数\变量\类只能定义一次,但可以声明许多次**。例如可以在一个源文件try.cpp中定义变量`int num = 30;`,然后在源文件try1.cpp中声明此变量`extern int num;`,在源文件try2.cpp中声明此变量`extern int num;`,这样便可以在源文件try1.cpp和try2.cpp中使用源文件try.cpp中的int型变量num。也正是由于声明仅是给出变量的名字而非变量的内容,所以代码`extern int num = 20;`是非法的。
82 |
83 |
84 |
85 | ### 3.2头文件
86 |
87 | **头文件**是后缀为.h\\.hpp的文件,和源文件.cpp一样是多文件组织中极其重要的文件类型,头文件的**主要用途**是将声明传播到源文件中。头文件允许我们将声明放在一个位置,然后将其导入到任何需要的位置。这可以节省多文件程序中的大量键入。
88 |
89 | 可以通过下面这个简单程序来理解头文件的作用:
90 |
91 | ```c++
92 | #include
93 | int main()
94 | {
95 | cout<<"Hello World!"<`引入了头文件“iostream”,`cout`和`<<`已经在该头文件中声明。
100 |
101 | > iostream和iostream.h是不同的头文件,这两个文件的产生有一定的历史原因,在此不详细说明。
102 |
103 |
104 |
105 | 关于头文件的一些具体规范如下:
106 |
107 | #### 3.2.1变量和函数尽量不要在头文件中定义
108 |
109 | 在预处理时,预处理器会将头文件中的内容导入源文件中,由于在源文件中往往会包含变量和函数的定义,因此如果头文件中也含有变量和函数的定义,那么预处理后的代码中会出现对同一变量\函数的多次定义,进而会在编译时导致**重定义**的错误。因而头文件通常不应包含函数和变量定义。
110 |
111 |
112 |
113 | #### 3.2.2类可以在头文件中定义
114 |
115 | 一个类往往附带若干成员函数,如下面代码所示,c++允许在类的定义之外定义类的成员函数,这样可以避免类的定义过分臃肿,也方便代码的管理。
116 |
117 | ```c++
118 | class Coder {
119 | public:
120 | Coder(int, double);
121 | int Get_num();
122 | double Get_month_salary();
123 |
124 | private:
125 | int num_;
126 | double month_salary_;
127 | };
128 |
129 |
130 | Coder::Coder(int wnum, double wsa) {
131 | num_ = wnum;
132 | month_salary_ = wsa;
133 | }
134 |
135 | int Coder::Get_num() {
136 | return num_;
137 | }
138 |
139 | double Coder::Get_month_salary() {
140 | return month_salary_;
141 | }
142 | ```
143 |
144 | 这种编写方式逐渐成为规范,如此一来,类的定义无论从形式还是作用上看都越来越类似于声明,允许类在头文件中定义也因此顺理成章。当然这样做大大增加了重定义的风险,这个问题可以通过下面的头文件保护机制来解决。
145 |
146 |
147 |
148 | #### 3.2.3头文件保护
149 |
150 | 在实际的代码编写中,由于编写者的疏忽等原因,往往会由于头文件而产生的重定义的错误;在某些情况下,我们必须把类的定义放在头文件中,这就进一步增加了重定义的可能。可以引入头文件保护机制来处理这一问题。所谓**头文件保护机制**,就是按照如下的格式来编写头文件:
151 |
152 | ```c++
153 | #ifndef XX
154 | #define XX
155 |
156 | //body
157 |
158 | #endif
159 | ```
160 |
161 | 其中`//body`就是原来的头文件的内容,`XX`按照惯例是头文件的全名(全部字母大写,标点符号用`_`代替),如对于try.h文件,它对应的`XX`就是TRY_H。
162 |
163 | 在预处理后,如果同一个头文件出现了两次,则第二次出现时`XX`已经被`#define XX `代码定义,所以第二次出现时`#ifndef XX `不再满足,这就保证了相同的内容不会重复出现两次,进而降低了重定义发生的可能性。
164 |
165 | `#pragma once`指令也可以起到相同的作用,它的使用方法更加简单,在头文件最开始的地方加上`#pragma once`即可。
166 |
167 |
168 |
169 | #### 3.2.4将头文件导入源文件的方法
170 |
171 | - `#include<>`:常用来导入系统文件
172 |
173 | - `#include""`:先找与编译文件同文件夹下的头文件,再像`#include<>`一样找系统文件
174 |
175 | > 值得注意的是,头文件不仅可以导入源文件中,也可以导入头文件中,即在头文件中引入头文件也是合法的(在许多情况下甚至是必要的),将头文件导入头文件的方法与将头文件导入源文件的方法一致。
176 |
177 |
178 |
179 | 此外,关于头文件还有一些细节知识\规范\建议:
180 |
181 | 1. 头文件的名称应尽量与相对应的源文件的名称相同,例如:grades.h 与 grades.cpp。
182 | 2. 各个”头文件——源文件“组合应尽可能彼此独立。
183 | 3. “#include”之后不能跟.cpp文件
184 |
185 |
186 |
187 | ------
188 |
189 | 为了使大家对多文件机制能够有更深刻的理解,我编写了一份实例代码放在`codes/`文件夹内,大家可以自行阅读和运行尝试。祝大家学习愉快!
190 |
191 | > 将`codes/`文件夹内的文件进行编译生成名为main的可执行文件的命令是:
192 | >
193 | > `g++ -o main occupation.h occupation.cpp salary.h salary.cpp main.cpp`
194 |
195 |
--------------------------------------------------------------------------------
/C++/第一讲/C++_多文件组织简介/imgs/img01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ksc999/THUEEXP-SAST-Tutor/d01ad77a614a8ff9eefa3c4a64fc7e8bf7b47f08/C++/第一讲/C++_多文件组织简介/imgs/img01.png
--------------------------------------------------------------------------------
/C++/第一讲/C++_多文件组织简介/imgs/img02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ksc999/THUEEXP-SAST-Tutor/d01ad77a614a8ff9eefa3c4a64fc7e8bf7b47f08/C++/第一讲/C++_多文件组织简介/imgs/img02.png
--------------------------------------------------------------------------------
/C++/第七讲/继承与多态总结/codes/pointer_reference_to_base.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | class Base
3 | {
4 | protected:
5 | int m_value;
6 | public:
7 | Base(int value) :m_value(value) {}
8 | std::string getName() const { return "Base"; }
9 | int getValue() const { return m_value; }
10 | };
11 | class Derived : public Base
12 | {
13 | public:
14 | Derived(int value) :Base(value) {}
15 | std::string getName() const { return "Derived"; }
16 | int getValueDoubled() const { return m_value * 2; }
17 | };
18 | //这是基类与派生类的定义
19 |
20 | int main()
21 | {
22 | Derived derived{ 5 };
23 | Base& rBase{ derived };
24 | Base* pBase{ &derived };//派生类对象生成基类的指针和引用
25 |
26 | std::cout << "derived is a " << derived.getName();
27 | std::cout << " and has value " << derived.getValue() << '\n';
28 | std::cout << "rBase is a " << rBase.getName();
29 | std::cout << " and has value " << rBase.getValue() << '\n';
30 | std::cout << "pBase is a " << pBase->getName();
31 | std::cout << " and has value " << pBase->getValue() << '\n';
32 | return 0;
33 | }
--------------------------------------------------------------------------------
/C++/第七讲/继承与多态总结/codes/virtual_destructor.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | using namespace std;
3 |
4 | class Base{
5 | public:
6 | Base(){cout<<"Base()"<print();
21 | delete b;
22 | return 0;
23 | }
24 |
--------------------------------------------------------------------------------
/C++/第七讲/继承与多态总结/codes/virtual_func_abcd.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | class A
3 | {
4 | public:
5 | virtual std::string getName() const { return "A"; }
6 | };
7 | class B : public A
8 | {
9 | public:
10 | virtual std::string getName() const { return "B"; }
11 | };
12 | class C : public B
13 | {
14 | public:
15 | virtual std::string getName() const { return "C"; }
16 | };
17 | class D : public C
18 | {
19 | public:
20 | virtual std::string getName() const { return "D"; }
21 | };
22 |
23 | int main()
24 | {
25 | C c;
26 | A& rBase{ c };
27 | std::cout << "rBase is a " << rBase.getName() << '\n';
28 | return 0;
29 | }
--------------------------------------------------------------------------------
/C++/第七讲/继承与多态总结/codes/virtual_func_animal.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | class Animal
4 | {
5 | protected:
6 | std::string m_name;
7 | Animal(const std::string& name) : m_name(name) {}
8 | public:
9 | virtual std::string speak() const { return "???"; }
10 | };
11 | class Cat : public Animal
12 | {
13 | public:
14 | Cat(const std::string& name) : Animal(name) {}
15 | virtual std::string speak() const { return "Meow"; }
16 | };
17 | class Dog : public Animal
18 | {
19 | public:
20 | Dog(const std::string& name) : Animal(name) {}
21 | virtual std::string speak() const { return "Woof"; }
22 | };
23 | void report(const Animal& animal)
24 | {
25 | std::cout << animal.speak() << '\n';
26 | }
27 | int main()
28 | {
29 | Cat cat{ "Fred" };
30 | Dog dog{ "Garbo" };
31 | report(cat);
32 | report(dog);
33 | return 0;
34 | }
--------------------------------------------------------------------------------
/C++/第七讲/继承与多态总结/docs/继承与多态总结.md:
--------------------------------------------------------------------------------
1 | # 第七讲 继承与多态总结
2 |
3 | 继承与多态的内容是C++程序设计理论中较为重要的一部分。在《计算机程序设计基础(2)》课程对这一部分基础知识比较细致的讲解的基础上,本教程将回顾一部分重点并对“继承与多态”的内在本质与外在实现进行拓展讲述。
4 |
5 | ## 三种继承方式下基类成员的访问属性
6 |
7 | 在C++语言中,类的成员有三种访问属性:私有继承(private)、保护继承(protected)和公有继承(public)。在继承的过程中,派生类会将基类作为它自身组成的一部分,因而在派生类中基类成员会具有怎样的访问属性是值得关心的。这一问题可以用一张表格概括,其中首行表示继承方式,首列表示基类成员在基类中的访问属性,$3\times3$的交叉部分表示基类成员在派生类中的访问属性:
8 |
9 | | | 私有继承(private) | 保护继承(protected) | 公有继承(public) |
10 | | :-----------------------: | :-----------------: | :-------------------: | :----------------: |
11 | | 基类私有成员 (private) | 不可访问 | 不可访问 | 不可访问 |
12 | | 基类保护成员(protected) | 私有成员 | 保护成员 | 保护成员 |
13 | | 基类公有成员 (public) | 私有成员 | 保护成员 | 公有成员 |
14 |
15 | 这一知识点是简单的,但略显琐碎,在实际编程中应当加以注意。
16 |
17 | ## 多态的利用目的
18 |
19 | 在面向对象的程序设计中,多态被称为是基本特性之一,正确把握它的应用目的是重要的。
20 |
21 | **多态的利用目的是实现接口的重用**,也就是说利用多态的情况下,不同对象收到相同消息时可以产生不同行为。它分为两种形式:静态多态和动态多态。**静态多态**下,在**编译**的环节中不同对象收到相同消息时产生的不同行为就被确定了,而**动态多态**下不同对象收到相同消息时产生的不同行为要在程序**运行**期间根据基本规则和实际情况来决定。
22 |
23 | 这也许仍然是抽象的,但我们现在对上面的阐述产生一个简单印象就可以了——这部分暂时的不透彻了解并不会对下面的阅读造成太大影响。在下面的阅读过程中,它的内核会更加清晰地呈现在我们面前。
24 |
25 | ## 动态多态的实现
26 |
27 | 在多态的两种实现形式中,静态多态主要通过函数重载和函数模板的使用实现,这两者或比较简单或在教程的其他章节有所阐述,因此本节教程中我们主要关心动态多态这一形式。如果只用一句话概括的话,**动态多态是通过虚函数和抽象基类实现的**。
28 |
29 | ### 指向基类的指针和引用的使用
30 |
31 | 在具体讲解动态多态的实现方式之前,为了更好地理解虚函数的作用,我们最好先引入一个基本知识:指向基类的指针和引用的使用。
32 |
33 | #### 使用规则
34 |
35 | 根据指针与引用的用法,我们显然可以用派生类对象生成派生类的指针和引用,但在此之外,**我们也可以用派生类对象生成基类的指针和引用**。但我们应当注意的是,用派生类对象生成派生类的指针和引用时,涉及到的指针和引用建立在派生类之上,由它们能够访问派生类的全部成员,无论这些成员来自基类抑或是后来派生出来的。然而,用派生类对象生成基类的指针和引用时,涉及到的指针和引用建立在基类之上,由它们只能访问基类的部分——换句话说,用派生类对象生成基类的指针和引用后,通过基类的指针和引用能够“看到”的只有派生类对象的基类部分,派生部分在这里是“看不见”的。
36 |
37 | 我们不妨来看这样一个实例:
38 |
39 | ```C++
40 | #include
41 | class Base
42 | {
43 | protected:
44 | int m_value;
45 | public:
46 | Base(int value) :m_value(value) {}
47 | std::string getName() const { return "Base"; }
48 | int getValue() const { return m_value; }
49 | };
50 | class Derived : public Base
51 | {
52 | public:
53 | Derived(int value) :Base(value) {}
54 | std::string getName() const { return "Derived"; }
55 | int getValueDoubled() const { return m_value * 2; }
56 | };
57 | //这是基类与派生类的定义
58 |
59 | int main()
60 | {
61 | Derived derived{ 5 };
62 | Base& rBase{ derived };
63 | Base* pBase{ &derived };//派生类对象生成基类的指针和引用
64 |
65 | std::cout << "derived is a " << derived.getName();
66 | std::cout << " and has value " << derived.getValue() << '\n';
67 | std::cout << "rBase is a " << rBase.getName();
68 | std::cout << " and has value " << rBase.getValue() << '\n';
69 | std::cout << "pBase is a " << pBase->getName();
70 | std::cout << " and has value " << pBase->getValue() << '\n';
71 | return 0;
72 | }
73 | ```
74 |
75 |
76 | 这个程序的结果是:
77 |
78 | ```
79 | derived is a Derived and has value 5
80 | rBase is a Base and has value 5
81 | pBase is a Base and has value 5
82 | ```
83 |
84 | 后两行输出与第一行形成了明显的对比:指向派生类`Derived`对象`derived`的基类`Base`指针`pBase`和引用`rBase`只能“看到”派生类对象的基类部分,那么通过`pBase`与`rBase`调用函数`getName()`与`getvalue()`时,所调用的函数体应当是基类部分的定义;而直接通过派生类`Derived`对象`derived`调用函数`getName()`与`getvalue()`时,由同名覆盖原则,所调用的函数体应当是派生部分的定义。
85 |
86 | #### 这种手段有什么用呢?
87 |
88 | 我们不妨再看一个实例:
89 |
90 | ```C++
91 | #include
92 | #include
93 | #include
94 | //Animal为基类,Cat,Dog类为派生类
95 | class Animal
96 | {
97 | protected:
98 | std::string m_name;
99 | Animal(std::string_view name):m_name(name){}
100 | public:
101 | std::string_view getName() const { return m_name;}
102 | };
103 |
104 | class Cat: public Animal
105 | {
106 | public:
107 | Cat(std::string_view name):Animal(name){}
108 | };
109 |
110 | class Dog: public Animal
111 | {
112 | public:
113 | Dog(std::string_view name):Animal(name){}
114 | };
115 | ```
116 |
117 | 如果我们想输出`Cat`或`Dog`对象的,来自基类的成员`m_name`时,如果不使用指向基类的指针或引用时,我们需要写出这样的输出函数:
118 |
119 | ```C++
120 | void report(const Cat& cat)
121 | {
122 | std::cout << cat.getName() << '\n';
123 | }
124 |
125 | void report(const Dog& dog)
126 | {
127 | std::cout << dog.getName() << '\n';
128 | }
129 | ```
130 |
131 | 这两个函数唯一的实质性不同在于函数参数的种类,代码复用性较差。如果派生类从`Cat`和`Dog`两个变成二十个或者二百个,代码的编写和维护成本就会急剧攀升。
132 |
133 | 然而,如果使用指向基类的指针或引用,这一情况会大大简化。无论存在多少派生类,我们只需要写出一个输出函数:
134 |
135 | ```C++
136 | void report(const Animal& rAnimal)
137 | {
138 | std::cout << rAnimal.getName() << '\n';
139 | }
140 | ```
141 |
142 | 这一输出函数的参数类型是基类`Animal`的引用。因而无论此函数被调用时的实参是哪种派生类的对象,函数的形参都会是由派生类实参产生的指向基类的引用。由这一形参,输出函数可以输出各种派生类对象中共有的基类成员`m_name`的值。
143 |
144 | 因而**总的来说,指向基类的指针和引用为实现多态提供了一个渠道**:利用指向基类的指针和引用的时候,**人们能够以相同的接口对不同的派生类对象的基类部分进行操作**,进而提高代码的复用度,降低程序的设计和维护成本。
145 |
146 |
147 |
148 | ### 虚函数
149 |
150 | 在“指向基类的指针和引用”这一部分,我们知道了借助指向基类的指针和引用,人们能够以相同的接口对不同的派生类对象的**基类部分**进行操作,那么我们能否以相同的接口对不同的派生类对象的**派生类部分**进行操作呢?答案是肯定的,此时我们就需要借助虚函数了。
151 |
152 | #### 使用规则
153 |
154 | 有关虚函数具体实现和逻辑细节的知识在《计算机程序设计基础(2)》中已经有了一定讲解。在研究虚函数对多态的作用之前,我们应当回顾一些重要的基础知识:虚函数是基类中一种被`virtual`特殊标记的函数,它发挥作用需要与派生类中与它函数特征(函数名、返回类型、const类型及参数数量、类型、顺序)完全相同的函数配合。当虚函数被调用时,实际上被调用的函数体是派生类中与它函数特征相同且从派生关系上看距离基类最远的函数。
155 |
156 | 回忆过这条知识,我们来看一个实例:
157 |
158 | ```C++
159 | #include
160 | #include
161 | class A
162 | {
163 | public:
164 | virtual std::string_view getName() const { return "A"; }
165 | };
166 | class B: public A
167 | {
168 | public:
169 | virtual std::string_view getName() const { return "B"; }
170 | };
171 | class C: public B
172 | {
173 | public:
174 | virtual std::string_view getName() const { return "C"; }
175 | };
176 | class D: public C
177 | {
178 | public:
179 | virtual std::string_view getName() const { return "D"; }
180 | };
181 |
182 | int main()
183 | {
184 | C c;
185 | A& rBase{ c };
186 | std::cout << "rBase is a " << rBase.getName() << '\n';
187 | return 0;
188 | }
189 | ```
190 |
191 | 这个程序的输出结果将是`rBase is a C`。
192 |
193 | 在分析中可以发现,四个类中由基类开始的派生顺序是`A-->B-->C-->D`。在主函数中,我们利用了一个`C`类对象`c`生成了一个指向基类`A`的引用`rBase`,当通过`rBase`调用函数`getname()`时,程序会首先访问基类`A`中的函数`getname()`,而它已被标记成虚函数。这样,程序就会寻找到派生类`C`中的`getname()`的函数体并执行。
194 |
195 | 为什么不是其他的`getname()`被调用呢?因为在这些派生类中,虽然`B`,`C`,`D`三个类都是派生类,但`rBase`是由`C`类对象生成的,不能借助虚函数访问到由`C`类派生出的`D`类的函数,且派生关系上看`C`类距`A`类远于`B`类,故调用的是`C`中的`getname()`。
196 |
197 | #### 这种手段有什么用呢?
198 |
199 | 我们不妨再看一个实例(它曾在前面出现过一次,但有一些差异):
200 |
201 | ```C++
202 | #include
203 | #include
204 | class Animal
205 | {
206 | protected:
207 | std::string m_name;
208 | Animal(const std::string& name) : m_name(name) {}
209 | public:
210 | virtual std::string speak() const { return "???"; }
211 | };
212 | class Cat : public Animal
213 | {
214 | public:
215 | Cat(const std::string& name) : Animal(name) {}
216 | virtual std::string speak() const { return "Meow"; }
217 | };
218 | class Dog : public Animal
219 | {
220 | public:
221 | Dog(const std::string& name) : Animal(name) {}
222 | virtual std::string speak() const { return "Woof"; }
223 | };
224 | void report(const Animal& animal)
225 | {
226 | std::cout << animal.speak() << '\n';
227 | }
228 | int main()
229 | {
230 | Cat cat{ "Fred" };
231 | Dog dog{ "Garbo" };
232 | report(cat);
233 | report(dog);
234 | return 0;
235 | }
236 | ```
237 |
238 | 输出结果是
239 |
240 | ```
241 | Meow
242 | Woof
243 | ```
244 |
245 | 我们可以发现,在利用指向基类的引用的基础上,**对虚函数的使用使我们能以相同的接口对不同的派生类对象的派生部分进行操作**。在这个程序中,外部的`report`函数利用了其形参即指向基类`Animal`的引用`animal`,调用了基类成员函数`speak()`。由于基类中`speak()`被标记成虚函数,外部实际调用的是`Cat`和`Dog`类中派生部分重载的`speak()`的函数体,实现了通过相同的接口对不同派生类派生部分的操作。如果基类中`speak()`没有虚函数标记,那么输出将变成两行`???`。
246 |
247 | 这样,虚函数的重要作用就得到了体现:如果没有虚函数,只借助指向基类的指针或引用,相同接口对不同派生类的派生部分就无法进行操作,我们只能为之制造多个接口,导致性能降低。只有基类中的接口函数被标记成了虚函数,它才能在被调用的时候转接到不同派生类派生部分中的、与基类虚函数对应的重载函数——指向基类的指针或引用是哪个派生类对象产生的,就会具体转接到哪个派生类——这样实现了相同接口下进行不同操作的目的。
248 |
249 | 由此,虚函数对于动态多态的作用是明显的。在接口统一的情况下,结构类似的派生类的数量变化不会造成太大影响,发挥出了多态的效用。
250 |
251 | #### 关于虚函数的一些注意事项
252 |
253 | 1.在派生类中,与基类虚函数函数特征相同的重载函数也会被系统自动判定为虚函数,无论它本身是否有`virtual`显式标记。但为代码含义明确起见,最好还是予以显式标记。
254 |
255 | 2.虚函数有其优势,但它会降低程序运行效率,所以只建议在必要的时候使用它。
256 |
257 |
258 |
259 | ### 虚析构函数
260 |
261 | 为了更好地实现多态,在实际编程中,人们也常把析构函数标记为虚函数。在本节我们要通过一个实例来认识虚析构函数的作用。
262 |
263 | 先看这个例子,其中基类的析构函数并非虚函数:
264 |
265 | ```c++
266 | #includeusing namespace std;class Base{public: Base(){cout<<"Base()"<print(); delete b; return 0;}
267 | ```
268 |
269 | 我们会得到这样的结果:
270 |
271 | ```
272 | Base()subBase()virtual Base.print()~Base()
273 | ```
274 |
275 | 从结果可以看出,当通过基类指针`b`删除派生类对象时,派生类的析构函数没有被调用,造成内存的不完全释放。原因是派生类重写了基类析构函数,这时无论派生类和基类的析构函数名称是否相同,编译器内部都会把两者名称视作相同的。所以当基类的析构函数为非虚函数时,就不能构成多态,通过基类指针只能使用基类析构函数。
276 |
277 | 如果我们只是把上面例子中基类析构函数改为虚函数,输出结果将是:
278 |
279 | ```
280 | Base()subBase()virtual Base.print()~subBase()~Base()
281 | ```
282 |
283 | 可以看出,基类定义了虚析构函数后,用基类指针删除派生类对象时,派生类的析构函数也被调用了。原因是当基类析构函数定义为虚函数后,删除对象时会直接调用派生类的析构函数。由于子类析构时会先调用父类的析构函数,子类和继承的父类就全部析构了。
284 |
285 |
286 |
287 | ### 抽象基类
288 |
289 | 抽象基类的定义与建立方式比较简单,在《计算机程序设计基础(2)》课程中已有足够多的说明。在本教程中,我们着重分析其实际意义。为了更好地分析,我们可以想象有这样一种情景:公司有一套工种查询系统,程序内部每个工种都有对应的一个类,如何组织这些类?
290 |
291 | 根据我们上面的各种讲解,组织好这些类必须要充分发挥“多态”的作用,否则成本就太大了。如果仿照“虚函数”一节有关“动物”的实例,我们可以设立一个“工种”基类,在其中保留“车工”“铣工”“钳工”等各个具体类所需的函数接口,之后通过派生产生各个具体类。
292 |
293 | 然而,此时我们要注意到一个细节:基类“工种”只是一个抽象概念,它所产生的对象是无意义的,这一基类的存在只是为了包含接口。因此,人们发明了“纯虚函数”,即一种只有形式上的接口而无实际函数体的虚函数,它的存在只是为了派生类继承它的接口并补写函数体,包含纯虚函数的类即成为“抽象基类”。由于纯虚函数毫无实质内容,抽象基类也就无法产生对象了,自然地消灭了“无意义对象”这种现象。
294 |
295 | 这样我们看出,抽象基类的发明目的仍是为了实现多态,即抽象基类的存在是为了把自身作为由它产生的诸多派生类的统一接口的集合。虽然不借助抽象基类仍能将接口统一,但抽象基类能够更好地凸显统一接口的地位,避免后续编程出现混乱。
296 |
--------------------------------------------------------------------------------
/C++/第七讲/继承与多态总结/readme.md:
--------------------------------------------------------------------------------
1 | ## 继承与多态总结
2 | ### 内容介绍
3 |
4 | 本次教程回顾了三种继承方式下基类成员的访问属性,在此基础上进一步介绍了多态的本质,重点介绍了动态多态的实现:指向基类的指针和引用的使用、虚函数、虚析构函数和抽象基类。
5 |
6 | ### 文件目录
7 |
8 | * `\docs`:包含教程文档“继承与多态总结.md”。
9 | * `\codes`:包含示例代码`pointer_reference_to_base.cpp`,`virtual_destructor.cpp`,`virtual_func_abcd.cpp`,`virtual_func_animal.cpp`。
10 |
11 | ### 参考资料
12 |
13 | - 清华大学计算机系姚海龙老师程序设计基础课件
14 | - 《C++ Primer》(第五版)
15 | - [learn Cpp](https://www.learncpp.com/)
16 | - Chapter 18.1-18.4
17 |
--------------------------------------------------------------------------------
/C++/第七讲/虚函数/README.md:
--------------------------------------------------------------------------------
1 | ## 简介:虚函数(virtual function)
2 |
3 | 1、虚函数的使用回顾
4 |
5 | 2、虚函数的原理简述
6 |
7 | 前期/静态绑定(early/static binding) 与 后期/动态绑定(late/dynamic binding)
8 |
9 | 虚函数表(VTABLE)与 虚函数表指针(VPTR)
10 |
11 | 3、RTTI简介(Run-time type Information),运行时类型识别
12 |
13 | 上行转换(upcasting)与下行转换(downcasting)
14 |
15 | 静态转换(static_cast)与动态转换(dynamic_cast)。
16 |
17 | 4、纯虚函数(pure virtual function)与抽象类(abstract class)
18 |
19 | 5、补充
20 |
21 | ----
22 |
23 |
24 |
25 | ## 参考资料:
26 |
27 | [C++ 虚函数表剖析 - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/75172640)
28 |
29 | [18.5 — Early binding and late binding – Learn C++ (learncpp.com)](https://www.learncpp.com/cpp-tutorial/early-binding-and-late-binding/)
--------------------------------------------------------------------------------
/C++/第七讲/虚函数/codes/dynamic_cast.cpp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ksc999/THUEEXP-SAST-Tutor/d01ad77a614a8ff9eefa3c4a64fc7e8bf7b47f08/C++/第七讲/虚函数/codes/dynamic_cast.cpp
--------------------------------------------------------------------------------
/C++/第七讲/虚函数/codes/虚函数.cpp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ksc999/THUEEXP-SAST-Tutor/d01ad77a614a8ff9eefa3c4a64fc7e8bf7b47f08/C++/第七讲/虚函数/codes/虚函数.cpp
--------------------------------------------------------------------------------
/C++/第七讲/虚函数/docs/虚函数.md:
--------------------------------------------------------------------------------
1 | ## 一、虚函数(virtual function) ##
2 |
3 | 1、虚函数的使用回顾
4 |
5 | 2、虚函数的原理简述
6 |
7 | 前期/静态绑定(early/static binding) 与 后期/动态绑定(late/dynamic binding)
8 |
9 | 虚函数表(VTABLE)与 虚函数表指针(VPTR)
10 |
11 | 3、RTTI简介(Run-time type Information),运行时类型识别
12 |
13 | 上行转换(upcasting)与下行转换(downcasting)
14 |
15 | 静态转换(static_cast)与动态转换(dynamic_cast)。
16 |
17 | 4、纯虚函数(pure virtual function)与抽象类(abstract class)
18 |
19 |
20 |
21 | ___
22 |
23 | ### 1、虚函数的使用回顾
24 |
25 | 在引入虚函数之前,我们先来回顾一下派生类基类的关系:简而言之,是“is-a”关系,即:派生类继承基类的所有成员,包含了基类的全部信息。
26 |
27 | 从而,我们自然可以把基类指针或引用指向派生类对象。当我们这样做时,目的往往是希望便利地使用**派生类**的函数。
28 |
29 | 但是,当派生类存在**基类函数的同名函数**时,将出现问题:程序并没有调用派生类函数,而是调用了基类函数。这是因为,指针(或引用)本来是基类的,当其指向派生类后,也只能使用派生类中的基类部分,其他部分是不可见的。
30 |
31 | 下面的代码可以看作一个例子:tune函数的形参为基类对象的引用,而实参是派生类wind的对象flute。我们期望调用flute的play函数,但事与愿违。
32 |
33 | ```c++
34 | #include
35 | using namespace std;
36 |
37 | enum note { middleC, Csharp, Eflat }; //这个枚举类型并不必要
38 | class Instrument
39 | {
40 | public:
41 | void play(note)const
42 | {
43 | cout << "Instrument::play" << endl;
44 | }
45 | };
46 |
47 | class Wind :public Instrument
48 | {
49 | public:
50 | void play(note)const
51 | {
52 | cout << "Wind::play" << endl;
53 | }
54 | };
55 |
56 | void tune(Instrument& i)
57 | {
58 | i.play(middleC);
59 | }
60 |
61 | int main()
62 | {
63 | Wind flute;
64 | tune(flute);
65 | }
66 |
67 | //运行结果为:Instrument::play。
68 | ```
69 |
70 | 为此,C++提供了解决方法,即虚函数。
71 |
72 | 虚函数使用的方法,可以简单记作“三步法”:**virtual、override、call**。
73 |
74 | 即:
75 |
76 | 首先在基类同名函数前加virtual关键字(此时派生类的同名函数自动为虚函数);
77 |
78 | 派生类应复写(override)该函数(同名,同类型,同参数);(如没定义,则继承其基类的)
79 |
80 | 最后用**指针或引用**调用成员函数。(例如,本例中删掉引用符号,则即使加virtual亦无用)
81 |
82 | #注:以下均认为是公有继承。
83 |
84 | ___
85 |
86 | ### 2、虚函数的原理简述
87 |
88 | 了解了虚函数的使用方法,下面我们来看一下虚函数的原理。虚函数简而言之是通过**后期(动态)绑定**、借助**虚函数表**实现的。
89 |
90 | 所谓绑定,即指将标识符(如变量和函数名称)转换为地址的过程,下面重点介绍函数绑定。
91 |
92 | 绑定分为前期绑定与后期绑定。
93 |
94 | 前期绑定,又称静态绑定,在编译过程中即完成。即编译器遇到函数调用时,直接将函数名称与计算机地址相关联,将函数调用替换为机器语言指令,该指令告诉CPU跳转到函数的地址。我们通常见到的函数调用,都是前期绑定。
95 |
96 | 后期绑定,又称动态绑定,特点是在编译时不知道将调用哪个函数,只有在其后的运行时才能做到。经过虚函数表调用虚函数的过程,即称为后期绑定。与前期绑定相比,后期绑定的效率会低一些,但更加灵活。
97 |
98 | 那么,动态绑定的具体流程是什么呢?下面是一个简述,有兴趣的同学们可以了解一下。
99 |
100 | 首先,每个包含了虚函数的类都包含一个**虚函数表(虚表)**。当然,若一个类继承了包含虚函数的基类,那么这个派生类类也将拥有自己的虚表。虚表是一个指针数组,其元素就是虚函数的指针,即每个元素对应一个指向虚函数的函数指针。普通的函数(非虚函数)的调用并不需要经过虚表。在代码的编译阶段,虚表就可以构造出来了。一个类只需要一个虚表即可,同一个类的所有对象都使用同一个虚表。每个对象内部会包含一个虚表的指针,来指向自己所使用的虚表。
101 |
102 | 简而言之,即:**对象的虚表指针用来指向自己所属类的虚表,虚表中的指针会指向其继承的最近的一个类的虚函数。如果类重写了虚函数,那么指针会指向重写的虚函数。**
103 |
104 | 此时,假设我们定义一个派生类B的对象b,再声明一个基类A的指针p指向对象b,那么,p可以访问b的虚表指针(派生类对象的虚表指针亦属于基类部分)从而p可以访问类B的虚表。然后,p将在B的虚表中查找所调用的虚函数对应的条目,最后,就可以根据虚表中找到的函数指针成功调用所需的函数。
105 |
106 |
107 |
108 | ___
109 |
110 | ### 3、RTTI 简介
111 |
112 | 由于使用虚函数时往往涉及到了指向基类的指针或引用向派生类的转换,C++为此提供了一个机制,即“RTTI”(运行时类型识别),能够让程序获取由基指针或引用所指向的对象的实际派生类型。
113 |
114 | 下面首先介绍两种转换方式:**上行转换**与**下行转换**。
115 |
116 | 上行转换(upcasting):子类的指针或引用转成父类,即把派生类型“向上”视为其基类型。由于子类包含父类全部信息,甚至不用强制转换,直接赋值即可。
117 |
118 | 下行转换(upcasting):父类的指针或引用转成子类,不能直接去赋值,必须先强制转换。
119 |
120 | C++中,有两种强制转换的方式:static_cast(静态转换)与dynamic_cast(动态转换)。
121 |
122 | dynamic_cast:
123 |
124 | (1)使用前提:类中含有至少一个虚函数。
125 |
126 | (2)特点:在运行而非编译时进行转换。
127 |
128 | (3)安全性:
129 |
130 | 上行转换一定成功;
131 |
132 | 下行转换时,有报错机制。对于指针,如果转换失败将返回 NULL;对于引用,如果转换失败将抛出`std::bad_cast`异常。
133 |
134 | 第一种情况:当待转换的基类指针(或引用)指向(或引用)**派生类**对象时,转换成功;
135 |
136 | 第二种情况:当待转换的基类指针(或引用)指向(或引用)**基类**对象时,转换失败。
137 |
138 | (4)缺点:安全检查将花费一定开销,将降低程序效率。
139 |
140 | (5)注:除了上行转换、下行转换,还有一个交叉转换(两个类没有继承关系),也可以用dynamic_cast完成。
141 |
142 | ```c++
143 | #include
144 | using namespace std;
145 |
146 | class Base
147 | {
148 | public:
149 | Base() {};
150 | virtual void Show() { cout << "This is Base calss"; }
151 | };
152 | class Derived :public Base
153 | {
154 | public:
155 | Derived() {};
156 | void Show() { cout << "This is Derived class"; }
157 | };
158 | int main()
159 | {
160 | //第一种情况
161 | Base* base = new Derived;
162 | if (Derived* der = dynamic_cast(base))
163 | {
164 | cout << "第一种情况转换成功" << endl;
165 | der->Show();
166 | cout << endl;
167 | }
168 | //第二种情况
169 | Base* base1 = new Base;
170 | if (Derived* der1 = dynamic_cast(base1))
171 | {
172 | cout << "第二种情况转换成功" << endl;
173 | der1->Show();
174 | }
175 | else
176 | {
177 | cout << "第二种情况转换失败" << endl;
178 | }
179 |
180 | delete(base);
181 | delete(base1);
182 | system("pause");
183 | }
184 | /*
185 | //程序结果:
186 | 第一种情况转换成功
187 | This is Derived class
188 | 第二种情况转换失败
189 | */
190 | ```
191 |
192 | static_cast:
193 |
194 | 在运行时期间不执行任何检查,以确保要转换的对象实际上是目标类型的完整对象。因此,必须由程序员自己来确保转换是安全的。另一方面,它不会产生安全检查的开销。
195 |
196 | RTTI的另一内容是typeid操作符,它能够获取一个表达式的类型信息。如果表达式的类型是类类型且至少包含有一个虚函数,则typeid操作符返回表达式的动态类型,需要在运行时计算;否则,typeid操作符返回表达式的静态类型,在编译时就可以计算。
197 |
198 | //具体输出未必就是类的名字,这个还与编译器有关。以下是用vs2019得到的。
199 |
200 | ```c++
201 | int main()
202 | {
203 | Instrument ia;
204 | Wind wb;
205 | Instrument& ic = wb;
206 | Instrument* id = &wb;
207 | cout << typeid(double).name() << endl;//double
208 | cout << typeid(ia).name() << endl;//Instrument
209 | cout << typeid(wb).name() << endl;//Wind
210 | cout << typeid(ic).name() << endl;//Wind(若不加virtual,为Instrument)
211 | cout << typeid(id).name() << endl;//Instrument*(只是指针,不是类类型)
212 | cout << typeid(*id).name() << endl;//Wind(若不加virtual,为Instrument)
213 | }
214 | ```
215 |
216 | ___
217 |
218 | ### 4、纯虚函数和抽象类
219 |
220 | 我们还可以定义纯虚函数,一旦一个类含有至少一个纯虚函数,其成为抽象类。抽象类不能实例化对象,只能作为(其派生类的)接口使用。
221 |
222 | **为什么要用纯虚函数**:避免对象切片。
223 |
224 | 什么是对象切片:将一个派生类转换为基类的过程中,派生类的一部分将被基类接收不到,只能留下基类大小的对象。
225 |
226 | 怎么避免对象切片:上行转换时,一律使用指针和引用形式。
227 |
228 | 因此,当用了纯虚函数,由于基类不能实例化对象,就强制用户使用指针与引用,从而解决问题。
229 |
230 | ----
231 |
232 |
233 |
234 | ### 5、补充:
235 |
236 | 1、override关键字:写在派生类同名函数前,让编译器帮助用户检查是不是正确地override了该函数。
237 |
238 | 2、final关键字:写在类名称后边,阻止被继承;写在成员函数列表后边,阻止被复写。
239 |
240 | 3、构造函数不可声明为虚函数;析构函数应尽量声明为虚函数。
241 |
242 |
243 |
244 | ----
245 |
246 | ## 参考资料:
247 |
248 | [C++ 虚函数表剖析 - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/75172640)
249 |
250 | [18.5 — Early binding and late binding – Learn C++ (learncpp.com)](https://www.learncpp.com/cpp-tutorial/early-binding-and-late-binding/)
251 |
--------------------------------------------------------------------------------
/C++/第三讲/左值与右值简介/README.md:
--------------------------------------------------------------------------------
1 | # 第三讲
2 |
3 | ## 简介
4 |
5 |
6 | - 内容主题:`左值与右值`
7 | - 你将学到的内容:
8 | - 值类别简介
9 | - 值类别的相关定义
10 | - 常见表达式所属的值类别
11 | - 右值引用
12 |
13 | ---
14 |
15 | ## 文件清单
16 |
17 | - `docs/`
18 | - 第三讲:左值与右值.md【本讲教程核心内容】
19 | - `imgs/`
20 | - 01.png
21 |
22 | ---
23 |
24 | ## 参考资料
25 |
26 | - 清华大学计算机系姚海龙老师程序设计基础课件
27 | - 《C++ Primer》(第五版)
28 | - [learn Cpp](https://www.learncpp.com/)
29 | - M.2 — R-value references
30 | - [cppreference-值类别](https://en.cppreference.com/w/cpp/language/value_category)
31 | - [Microsoft C++ docs](https://docs.microsoft.com/en-us/cpp/cpp/?view=msvc-170)
32 | - [GeeksforGeeks C++ tutorials](https://www.geeksforgeeks.org/c-plus-plus/)
33 | - https://blog.csdn.net/z_yu_yun/article/details/58149590
34 |
--------------------------------------------------------------------------------
/C++/第三讲/左值与右值简介/docs/左值与右值.md:
--------------------------------------------------------------------------------
1 | # 第三讲:左值与右值
2 |
3 | ## 1. 值类别
4 |
5 | 每一个C++表达式(带有其操作数的运算符、文本、变量名称等)都具有两个独立属性的特征:`类型`和`值类别`。
6 | 每个表达式都有一些非引用类型,并且每个表达式正好属于三个主要值类别之一:`prvalue`、`xvalue` 和 `lvalue`。
7 |
8 | 具体的类别分类可以参考下图:
9 |
10 |
11 |

13 |
14 |
15 | 注:
16 | 1. `glvalue`(泛左值)= `lvalue`(传统意义上的左值)+ `xvalue`(消亡值,通过右值引用产生)
17 | - `glvalue`("广义"左值)是一个表达式,其计算决定了对象或函数的恒等式。
18 | - `lvalue`(历史上称为左值,因为左值可能出现在赋值表达式的左侧)指不是 `xvalue` 的`glvalue`。
19 | - `xvalue` 是一个 `glvalue`,表示其资源可以重用的对象。
20 | 2. `rvalue` (传统意义上的右值) = `prvalue`(纯右值) + `xvalue`(消亡值,通过右值引用产生)
21 | - `rvalue`(历史上称为右值,因为右值可能出现在赋值表达式的右侧)指一个 `prvalue` 或 `xvalue`。
22 | - `prvalue`("pure"rvalue)是其计算的表达式
23 | - 计算内置运算符的操作数的值(此类 `prvalue` 没有结果对象)
24 | - 初始化对象(这样的 `prvalue` 被称为具有结果对象)。
25 |
26 |
27 | ## 2. 值类别的相关定义
28 |
29 | 1. `lvalues`(左值)
30 | - 当且仅当表达式E指的是一个**已经拥有**一个标识(地址、名称或别名),可以在E之外访问它的实体。
31 |
32 | 2. `xvalues`
33 |
34 | 表达式E属于`xvalue`类别当且仅当表达式E为:
35 | - 隐式或显式调用函数的结果,该函数的返回类型是返回对象类型的右值引用
36 | - 转换为对象类型的右值引用
37 | - 一个类成员访问表达式,指定一个非引用类型的非静态数据成员,其中对象表达式是`xvalue`
38 | - 一个指针到成员的表达式,其中第一个操作数是`xvalue`,第二个操作数是数据成员的指针。
39 |
40 | 注意,上面规则的效果是,对对象的命名右值引用被视为左值,而对对象的未命名右值引用被视为x值;对函数的右值引用被视为左值,无论是否命名。
41 |
42 | 3. `prvalues`
43 | - 当且仅当E既不属于左值也不属于`xvalue`类别时,表达式E属于`prvalue`类别。
44 |
45 | 另外有一种比较简单的判断方法:
46 | 1. 如果有标识符,且不可移动,则被称为`lvalue`
47 | 2. 如果有标识符,且可移动,被称为`xvalue`
48 | 3. 如果没有标识符,且可移动,称为`prvalue`
49 |
50 |
51 | ## 3. 常见的表达式的值类别
52 |
53 | ### 以下表达式是 lvalue 表达式:
54 |
55 | - `变量`、`函数`、`模板参数对象(自 C++20 起)`或 `数据成员的名称`,无论其类型如何,例如`std::cin`或`std::endl`。即使变量的类型是右值引用,由其名称组成的表达式也是左值表达式。
56 | - `函数调用`或`运算符重载`表达式,其返回类型为左值引用,例如`std::getline`(`std::cin`, `str`),`std::cout << 1`,`str1 = str2`或`++it`。
57 | - `a = b`, `a += b`, `a %= b`,以及所有其他内置赋值和复合赋值表达式。
58 | - `++a`和`--a`,内置的预自增和预自减表达式。
59 | - `*p`,内置间接寻址表达式。
60 | - `a[n]`和`p[n]`、内置下标表达式,其中一个操作数位于`a[n]`是数组左值 (自 `C++11` 起)。
61 | - `a.m`,对象表达式的成员,除非m是成员枚举器或非静态成员函数,或者a是右值并且是对象类型的非静态数据成员。
62 | - `p->m`,指针表达式的内置成员,除非m是成员枚举器或非静态成员函数。
63 | - `a.*mp`,指向对象表达式成员的指针,其中a是左值并且是指向数据成员的指针。
64 | - `p->*mp`,是指向指针表达式成员的内置指针,其中mp是指向数据成员的指针。
65 | - `a, b`,内置逗号表达式,其中b是左值。
66 | - `a ? b : c`,三元条件表达式(当b、c都是相同类型的左值时)。
67 | - 字符串字面量,例如`"hello world!"`。
68 | - 转换为左值引用类型的表达式,例如`static_cast(x)`。
69 |
70 | ### 以下表达式是 prvalue 表达式:
71 |
72 | - 文本(字符串文本除外),例如`42`,`true`或`nullptr`
73 | - 函数调用或重载运算符表达式,其返回类型为非引用,例如`str.substr(1, 2)`,`str1 + str2`或`it++`
74 | - `a++`和`a--`,内置的后自增和后自减表达式
75 | - `a + b`, `a % b`, `a & b`, `a << b`,以及所有其他内置的算术表达式
76 | - `a && b`, `a || b`, `!a`,内置逻辑表达式
77 | - `a < b`, `a == b`, `a >= b`,以及所有其他内置的比较表达式
78 | - 和,表达式的内置地址
79 | - `a.m`,对象表达式的成员,其中是成员枚举器或非静态成员函数,或者 a 是右值,m 是非引用类型的非静态数据成员(直到` C++11`)
80 | - `p->m`,指针表达式的内置成员,其中是成员枚举器或非静态成员函数
81 | - `a.*mp`,指向对象表达式成员的指针,其中是指向成员函数的指针,或者其中 a 是右值,mp 是指向数据成员的指针(直到 `C++11`)
82 | - `p->*mp`,指向指针表达式成员的内置指针,其中是指向成员函数的指针
83 | - `a, b`,内置逗号表达式,其中a、b为右值
84 | - `a ? b : c`,三元条件表达式(当b、c都是相同类型的纯右值时)
85 | - 将表达式转换为非引用类型,例如`static_cast(x)`,`std::string{}`或`(int)42`
86 | - this指针
87 | - 枚举器
88 | - 非类型模板参数,除非其类型是类或(自 C++20 起)左值引用类型
89 | - 一个 `lambda 表达式`,例如`[](int x){return x * x;}`
90 | (自 `C++11` 起)
91 | - a require-expression,例如`require (T i) {typename T::type;}`
92 | - 概念的专业化,例如`std::equality_comparable`.(自 `C++20` 起)
93 |
94 |
95 | ### 以下表达式是 xvalue 表达式:
96 |
97 | - 函数调用或重载运算符表达式,其返回类型是对对象的右值引用,例如`std::move(x)`
98 | - `a[n]`,内置的下标表达式,其中一个操作数是数组右值
99 | - `a.m`,对象表达式的成员,其中是右值并且是非引用类型的非静态数据成员
100 | - `a.*mp`,指向对象表达式成员的指针,其中是右值,是指向数据成员的指针
101 | - `a ? b : c`,三元条件表达式(当b、c都是相同类型的xvalue时)
102 | - 一个强制转换表达式,用于对对象类型的引用进行右值转换,例如`static_cast(x)`
103 | - 在临时具体化后指定临时对象的任何表达式。(自 `C++17` 起)
104 |
105 |
106 | ## 4. 右值引用(&&)
107 |
108 | 在`c++11`之前,引用仅限于左值引用,即只能对左值做引用。左值引用也就是我们课上都学过的那种引用方式。
109 | 以孙甲松老师的课件为例:
110 |
111 | ```c++
112 | void swap(int*& a,int*& b) //利用引用来进行交换
113 | {
114 | int *temp;
115 | temp = a;
116 | a = b;
117 | b = temp
118 | }
119 | ```
120 |
121 | 而在`C++11`中,**右值引用**(&&)被首次提出,右值引用的主要作用是保证了在销毁临时变量之前对它们的值的**安全访问**
122 |
123 | 关于左右值引用的例子可参考如下样例:
124 | ```c++
125 | int lval {2}; // lval is an lvalue
126 | int f() {...} //f() is rvalue
127 | int&& ref_rval {5}; // OK: binds to an rvalue 5
128 | int&& ref_rval2 {lval}; // Error: rvalue ref cannot bind to lvalue
129 | int&& ref_rval3 {lval+5}; // OK: binds to an rvalue
130 | int&& ref_rval4 {f()}; // OK: binds to an rvalue
131 | ```
132 |
133 | 右值引用常用作函数参数。当希望对左值和右值参数具有不同的行为时,这对于函数重载最有用。
134 | ```c++
135 | void fun(const int& lref) // l-value arguments will select this function
136 | {
137 | std::cout << "l-value reference to const\n";
138 | }
139 |
140 | void fun(int &&rref) // r-value arguments will select this function
141 | {
142 | std::cout << "r-value reference\n";
143 | }
144 |
145 | int main()
146 | {
147 | int x{ 5 };
148 | fun(x); // l-value argument calls l-value version of function
149 | fun(5); // r-value argument calls r-value version of function
150 |
151 | return 0;
152 | }
153 | ```
154 | 以上程序的输出结果为:
155 | l-value reference to const
156 | r-value reference
--------------------------------------------------------------------------------
/C++/第三讲/左值与右值简介/imgs/01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ksc999/THUEEXP-SAST-Tutor/d01ad77a614a8ff9eefa3c4a64fc7e8bf7b47f08/C++/第三讲/左值与右值简介/imgs/01.png
--------------------------------------------------------------------------------
/C++/第三讲/移动语义简介/README.md:
--------------------------------------------------------------------------------
1 | # 第三讲
2 |
3 | ## 简介
4 |
5 | - 内容主题:`移动语义`
6 | - 你将学到的内容:
7 | - 复制构造和复制赋值回顾
8 | - 移动构造函数与移动赋值函数的使用
9 | - std::move的基本操作
10 |
11 | ---
12 |
13 | ## 文件清单
14 |
15 | - `docs/`
16 | - 第三讲:移动语义.md【本讲教程核心内容】
17 | - `codes/`
18 | - 复制构造函数.cpp【样例程序】
19 |
20 | ---
21 |
22 | ## 参考资料
23 |
24 | - 清华大学计算机系姚海龙老师程序设计基础课件
25 | - 《C++ Primer》(第五版)
26 | - [learn Cpp](https://www.learncpp.com/)
27 | - M.3 — Move constructors and move assignment
28 | - M.4 — std::move
29 | - [Microsoft C++ docs](https://docs.microsoft.com/en-us/cpp/cpp/?view=msvc-170)
30 | - [GeeksforGeeks C++ tutorials](https://www.geeksforgeeks.org/c-plus-plus/)
31 |
--------------------------------------------------------------------------------
/C++/第三讲/移动语义简介/codes/复制构造函数.cpp:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | template
4 | class Auto_ptr3
5 | {
6 | T* m_ptr;
7 | public:
8 | Auto_ptr3(T* ptr = nullptr):m_ptr(ptr){}
9 |
10 | ~Auto_ptr3()
11 | {
12 | delete m_ptr;
13 | }
14 |
15 | // Copy constructor
16 | // Do deep copy of a.m_ptr to m_ptr
17 | Auto_ptr3(const Auto_ptr3& a)
18 | {
19 | m_ptr = new T;
20 | *m_ptr = *a.m_ptr;
21 | }
22 |
23 | // Copy assignment
24 | // Do deep copy of a.m_ptr to m_ptr
25 | Auto_ptr3& operator=(const Auto_ptr3& a)
26 | {
27 | // Self-assignment detection
28 | if (&a == this)
29 | return *this;
30 |
31 | // Release any resource we're holding
32 | delete m_ptr;
33 |
34 | // Copy the resource
35 | m_ptr = new T;
36 | *m_ptr = *a.m_ptr;
37 |
38 | return *this;
39 | }
40 |
41 | T& operator*() const { return *m_ptr; }
42 | T* operator->() const { return m_ptr; }
43 | bool isNull() const { return m_ptr == nullptr; }
44 | };
45 |
46 | class Resource
47 | {
48 | public:
49 | Resource() { std::cout << "Resource acquired\n"; }
50 | ~Resource() { std::cout << "Resource destroyed\n"; }
51 | };
52 |
53 | Auto_ptr3 generateResource()
54 | {
55 | Auto_ptr3 res(new Resource);
56 | return res; // this return value will invoke the copy constructor
57 | }
58 |
59 | int main()
60 | {
61 | Auto_ptr3 mainres;
62 | mainres = generateResource(); // this assignment will invoke the copy assignment
63 |
64 | return 0;
65 | }
66 |
67 | //引用自:https://www.learncpp.com/cpp-tutorial/move-constructors-and-move-assignment/
--------------------------------------------------------------------------------
/C++/第三讲/移动语义简介/docs/移动语义.md:
--------------------------------------------------------------------------------
1 | # 第三讲:移动语义
2 |
3 | ## 1、复制构造和复制赋值回顾
4 |
5 | 在程设2中,我们学习了关于复制构造函数和复制赋值(对 `=` 进行重载)。其作用为使用一个已经存在的对象去构造并初始化同类的一个新对象。
6 |
7 | ```c++
8 | Point::Point(const Point& p)
9 | {
10 | x = p.x;
11 | y = p.y;
12 | }
13 | ```
14 |
15 | 复制构造函数用于通过创建同一类的对象的副本来初始化类。复制赋值用于将一个类对象复制到另一个现有类对象。
16 | 默认情况下,C++将提供复制构造函数,如果未显式提供复制赋值运算符,则提供复制赋值运算符。这些编译器提供的函数执行**浅层复制**,这可能会导致分配动态内存的类出现问题。因此,处理动态内存的类应重写这些函数以执行**深度复制**。
17 |
18 |
19 | ## 2、移动构造函数与移动赋值函数简介
20 |
21 | 我们先来看一个关于变量交换的例子:
22 | ```c++
23 | template
24 | void swap(T& a, T& b)
25 | {
26 | // 一个是a自身,一个是tmp
27 | T tmp(a); // now we have two copies of a
28 | // 一个是b自身,一个是a
29 | a = b; // now we have two copies of b
30 | //同理……
31 | b = tmp; // now we have two copies of tmp
32 | }
33 | ```
34 |
35 | 然而,事实上,`tmp(a)`之后,相当于将`a`复制给了`tmp`,其实`a`所占内存可以被立即释放掉,不必占用内存。
36 | 同理,`a = b`之后,`b`所占内存也可以被立即释放掉`b = tmp`时也一样。
37 |
38 | 另外一个例子是codes/复制构造函数.cpp,这个程序虽然不长,但是实际上其反复的申请和释放内存,造成资源上的浪费。
39 | ```c++
40 | #include
41 |
42 | template
43 | class Auto_ptr3
44 | {
45 | T* m_ptr;
46 | public:
47 | Auto_ptr3(T* ptr = nullptr):m_ptr(ptr){}
48 |
49 | ~Auto_ptr3()
50 | {
51 | delete m_ptr;
52 | }
53 |
54 | // Copy constructor
55 | // Do deep copy of a.m_ptr to m_ptr
56 | Auto_ptr3(const Auto_ptr3& a)
57 | {
58 | m_ptr = new T;
59 | *m_ptr = *a.m_ptr;
60 | }
61 |
62 | // Copy assignment
63 | // Do deep copy of a.m_ptr to m_ptr
64 | Auto_ptr3& operator=(const Auto_ptr3& a)
65 | {
66 | // Self-assignment detection
67 | if (&a == this)
68 | return *this;
69 |
70 | // Release any resource we're holding
71 | delete m_ptr;
72 |
73 | // Copy the resource
74 | m_ptr = new T;
75 | *m_ptr = *a.m_ptr;
76 |
77 | return *this;
78 | }
79 |
80 | T& operator*() const { return *m_ptr; }
81 | T* operator->() const { return m_ptr; }
82 | bool isNull() const { return m_ptr == nullptr; }
83 | };
84 |
85 | class Resource
86 | {
87 | public:
88 | Resource() { std::cout << "Resource acquired\n"; }
89 | ~Resource() { std::cout << "Resource destroyed\n"; }
90 | };
91 |
92 | Auto_ptr3 generateResource()
93 | {
94 | Auto_ptr3 res(new Resource);
95 | return res; // this return value will invoke the copy constructor
96 | }
97 |
98 | int main()
99 | {
100 | Auto_ptr3 mainres;
101 | mainres = generateResource(); // this assignment will invoke the copy assignment
102 |
103 | return 0;
104 | }
105 | ```
106 | 该程序中共有6个关键步骤(每个打印的消息一个):
107 |
108 | - 在`generateResource()`中,使用动态分配的内存创建并初始化局部变量 `res`,这会导致第一个`"Resource acquired"`。
109 |
110 | - `Res`值返回到`main()`。我们在这里按值返回,因为`res`是一个局部变量,它不能通过地址或引用返回,因为当`genegoryResource()`结束时`res`将被销毁。因此,`res`被复制构造到一个临时对象中。由于我们的复制构造函数执行深度复制,因此在此处分配了一个新的资源,这会导致第二个`"Resource acquired"`。
111 |
112 | - `Res`超出其作用域,销毁最初创建的资源,从而导致第一个`“Resource destroyed”`。
113 |
114 | - 临时对象通过复制分配给`mainres`。由于拷贝也会执行深度复制,因此会分配一个新资源,从而导致另一个`"Resource acquired"`。
115 |
116 | - 赋值表达式结束,临时对象超出表达式范围并被销毁,从而导致`“Resource destroyed”`。
117 |
118 | - 程序的最后,`mainres`的作用域结束,`“Resource destroyed”`被打印出来
119 |
120 |
121 | 移动构造函数就是用来解决这上述过程中的内存浪费问题的。
122 |
123 | 这时候,使用类似于`other = move(lvalue)`的语句,`move()`函数能将一个左值转成右值,
124 | 然后通过`"="`触发移动赋值(`C++11`中出现),直接将`other`指向`lvalue`所指内存,而此后`lvalue`不再指向这一块内存
125 | 由此一来,就避免了“复制”,直接实现了内容在变量名之间的“转移”功能,也省下了内存空间。
126 |
127 | `C++11` 定义了两个为移动语义服务的新功能:**移动构造函数**和**移动赋值运算符**。复制构造函数和复制赋值的目标是将一个对象复制到另一个对象,而移动构造函数和移动赋值的目标是**将资源的所有权从一个对象移动到另一个对象**(这通常比进行复制更加节省时间和空间)。
128 |
129 |
130 | ## 3、std::move 简介
131 |
132 | 在 `C++11` 中,`std::move` 是一个标准库函数,它将其参数转换为右值引用,以便可以调用移动语义。因此,我们可以使用 `std::move` 将左值强制转换为一个类型,该类型更喜欢被移动而不是被复制。
133 |
134 | 它的效率要高得多。当 `tmp` 被初始化时,我们不是创建 `x` 的副本,而是使用 `std::move` 将左值表达式 `x` 转换为右值。由于参数是右值,因此调用移动语义,并将 `x` 移动到 `tmp` 中。
135 |
136 | `std::move` 在对元素数组进行排序时也很有用。许多排序算法(如选择排序和气泡排序)通过交换元素对来工作。在前面的课程中,我们不得不求助于复制语义来进行交换。现在我们可以使用移动语义来完成,这可以提高程序执行的效率。
137 |
138 | 如果我们想将一个智能指针管理的内容移动到另一个智能指针,`std::move` 也很有用。
139 |
140 | 总之,每当我们想要将左值视为右值以调用移动语义而不是复制语义时,都可以使用`std::move`。
--------------------------------------------------------------------------------
/C++/第九讲/桥接模式简介/codes/bridge_pattern.h:
--------------------------------------------------------------------------------
1 | #ifndef __BRIDGE_PATTERN_H__
2 | #define __BRIDGE_PATTERN_H__
3 | #include
4 | #include
5 | #include
6 | using namespace std;
7 |
8 | // 实现类接口:工种
9 | class Job
10 | {
11 | public:
12 | Job() {}
13 | virtual ~Job() {}
14 | virtual void begin() = 0;
15 | private:
16 | };
17 |
18 | // 具体实现类:炉前工
19 | class luqian :public Job
20 | {
21 | public:
22 | luqian() {}
23 | void begin() {
24 | cout << "luqian begin" << endl;
25 | }
26 | };
27 |
28 | // 具体实现类:吊车工
29 | class diaoche :public Job
30 | {
31 | public:
32 | diaoche() {}
33 | void begin() {
34 | cout<<"diaoche begin"<job = ijob;
67 | }
68 | void begin() {
69 | this->job->begin();
70 | }
71 | private:
72 | Job* job;
73 | };
74 |
75 | // 扩充抽象类:平炉
76 | class pinglu :public Place
77 | {
78 | public:
79 | pinglu() {}
80 | void setup(Job* ijob) {
81 | this->job = ijob;
82 | }
83 | void begin() {
84 | this->job->begin();
85 | }
86 | private:
87 | Job* job;
88 | };
89 |
90 | // 扩充抽象类:转炉
91 | class zhuanlu :public Place
92 | {
93 | public:
94 | zhuanlu() {}
95 | void setup(Job* ijob) {
96 | this->job = ijob;
97 | }
98 | void begin() {
99 | this->job->begin();
100 | }
101 | private:
102 | Job* job;
103 | };
104 |
105 |
106 | #endif //__BRIDGE_PATTERN_H__
--------------------------------------------------------------------------------
/C++/第九讲/桥接模式简介/codes/main.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include "bridge_pattern.h"
3 |
4 | int main()
5 | {
6 | Place *place;
7 | Job *job;
8 |
9 | //在高炉前启动指导炉前工工作的程序
10 | place = new gaolu();
11 | job = new luqian();
12 | place->setup(job);
13 | place->begin();
14 |
15 | //改变,在平炉前启动指导吊车工工作的程序
16 | delete place;
17 | delete job;
18 | place = new pinglu();
19 | job = new diaoche();
20 | place->setup(job);
21 | place->begin();
22 |
23 | delete place;
24 | delete job;
25 | return 0;
26 | }
27 |
--------------------------------------------------------------------------------
/C++/第九讲/桥接模式简介/docs/桥接模式简介.md:
--------------------------------------------------------------------------------
1 | # 第九讲:桥接模式简介
2 |
3 | 在这部分的教程中,我们将向大家介绍一种经典的OOP设计模式——桥接模式。
4 |
5 | ## 桥接模式基本原理
6 |
7 | 桥接模式是一种很好地体现了OOP设计中一大基本原则“针对接口编程,而非针对实现编程”的设计模式。它涉及到一个作为桥接的接口,使得实现类的功能独立于接口类,这样可以把抽象化和实现化解耦,使得二者可以独立变化。
8 |
9 | 上述文段也许是抽象的,但下面我们可以通过一个实例来认识桥接模式的本质。
10 |
11 | ## 桥接模式实例
12 |
13 | 想象一个情景:钢铁公司生产一线有炉前工、吊车工、备料工等工种,有高炉、平炉、转炉等工作场景,现需要设计一个程序,让程序能够指导特定场景中的特定工种工作。
14 |
15 | ### 无模式法
16 |
17 | 接到这个问题,思维难度最低的思路也许是直接勤劳地构建各场景与各工种之间的联系,形成如下的网络结构:
18 |
19 | ```mermaid
20 | graph LR;
21 | 高炉---炉前工;
22 | 高炉---吊车工;
23 | 高炉---备料工;
24 | 平炉---炉前工;
25 | 平炉---吊车工;
26 | 平炉---备料工;
27 | 转炉---炉前工;
28 | 转炉---吊车工;
29 | 转炉---备料工;
30 | ```
31 |
32 | 通过图片我们能够直接发现这种做法的局限性:我们需要构建太多从场景到工种的联系,极大增加了维护成本(这种结构复杂到连比较智能的mermaid语法在构建对应的结构图时都很难把它画得好看一些)。
33 |
34 | ### 桥接模式法
35 |
36 | 面对“无模式法”的困难,我们能否运用“桥接模式法”呢?
37 |
38 | 仔细观察不难发现,平炉、高炉和转炉是以“场景”的面貌出现在各工种之前的,而炉前工、吊车工、备料工又是以“工种”的面貌出现在各场景之前的。按桥接模式的思想,我们可以独立地构建“场景”和“工种”两个接口类,把三个场景和三个工种之间的联系全部统一到这两个接口类之下,各实现类只需和对应的接口类建立联系即可。按这样的思路,我们可以画出另一种程序实现结构:
39 |
40 | ```mermaid
41 | graph LR;
42 | 高炉---场景;
43 | 平炉---场景;
44 | 转炉---场景;
45 | 场景---工种;
46 | 工种---炉前工;
47 | 工种---吊车工;
48 | 工种---备料工;
49 | ```
50 |
51 | 同“无模式法”的结构图做对比,我们能够很明显地看出采用了桥接模式的程序结构有很大简化。这种结构下抽象化和实现化实现了解耦,某一实现类变化时只需修改与对应的接口类之间的联系,而无需与其他实现类发生纠缠,降低了程序的维护成本。
52 |
53 | ### 桥接模式法代码示例
54 |
55 | 在下面的示例中我们能够具体体会借助抽象基类、虚函数和指向基类的指针(或引用)实现桥接模式的方法。
56 |
57 | ```C++
58 | #ifndef __BRIDGE_PATTERN_H__
59 | #define __BRIDGE_PATTERN_H__
60 | #include
61 | #include
62 | #include
63 | using namespace std;
64 |
65 | // 实现类接口:工种
66 | class Job
67 | {
68 | public:
69 | Job(){}
70 | virtual ~Job(){}
71 | virtual void begin() = 0;
72 | private:
73 | };
74 |
75 | // 具体实现类:炉前工
76 | class luqian:public Job
77 | {
78 | public:
79 | luqian(){}
80 | void begin(){
81 | printf("炉前工工作开始\n");
82 | }
83 | };
84 |
85 | // 具体实现类:吊车工
86 | class diaoche:public Job
87 | {
88 | public:
89 | diaoche(){}
90 | void begin(){
91 | printf("吊车工工作开始\n");
92 | }
93 | };
94 |
95 | // 具体实现类:备料工
96 | class beiliao:public Job
97 | {
98 | public:
99 | beiliao(){}
100 | void begin(){
101 | printf("备料工工作开始\n");
102 | }
103 | };
104 |
105 | //抽象类:场景
106 | class Place
107 | {
108 | public:
109 | Place(){}
110 | virtual ~Place(){}
111 | virtual void setup(Job *job) = 0;
112 | virtual void begin() = 0;
113 | private:
114 | Job *job;
115 | };
116 |
117 | // 扩充抽象类:高炉
118 | class gaolu:public Place
119 | {
120 | public:
121 | gaolu(){}
122 | void setup(Job *ijob){
123 | this->job = ijob;
124 | }
125 | void begin(){
126 | this->job->begin();
127 | }
128 | private:
129 | Job *job;
130 | };
131 |
132 | // 扩充抽象类:平炉
133 | class pinglu:public Place
134 | {
135 | public:
136 | pinglu(){}
137 | void setup(Job *ijob){
138 | this->job = ijob;
139 | }
140 | void begin(){
141 | this->job->begin();
142 | }
143 | private:
144 | Job *job;
145 | };
146 |
147 | // 扩充抽象类:转炉
148 | class zhuanlu:public Place
149 | {
150 | public:
151 | zhuanlu(){}
152 | void setup(Job *ijob){
153 | this->job = ijob;
154 | }
155 | void begin(){
156 | this->job->begin();
157 | }
158 | private:
159 | Job *job;
160 | };
161 |
162 |
163 | #endif //__BRIDGE_PATTERN_H__
164 | ```
165 |
166 | ```C++
167 | #include
168 | #include "BridgePattern.h"
169 |
170 | int main()
171 | {
172 | Place *place;
173 | Job *job;
174 |
175 | //在高炉前启动指导炉前工工作的程序
176 | place = new gaolu();
177 | job = new luqian();
178 | place->setup(job);
179 | place->begin();
180 |
181 | //改变,在平炉前启动指导吊车工工作的程序
182 | delete place;
183 | delete job;
184 | place = new pinglu();
185 | job = new diaoche();
186 | place->setup(job);
187 | place->begin();
188 |
189 | delete place;
190 | delete job;
191 | return 0;
192 | }
193 | ```
194 |
195 | 输出为:
196 |
197 | ```
198 | luqian begin
199 | diaoche begin
200 | ```
201 |
202 |
--------------------------------------------------------------------------------
/C++/第九讲/桥接模式简介/readme.md:
--------------------------------------------------------------------------------
1 | ## 桥接模式简介
2 | ### 内容
3 | 本次教程是对一种程序设计模式——桥接模式的介绍,主要介绍了桥接模式的含义、优势和相关实例。
4 |
5 | ### 文件
6 | * `\docs`:包含教程文档“桥接模式简介.md”。
7 | * `\codes`:包含示例代码`main.cpp`,`bridge_pattern.h`。
8 |
--------------------------------------------------------------------------------
/C++/第九讲/迭代器模式简介/README.md:
--------------------------------------------------------------------------------
1 | # 第九讲
2 |
3 | ## 简介
4 |
5 | - 内容主题:`迭代器模式`
6 | - 你将学到什么:
7 | - 什么是迭代器模式
8 | - STL中的迭代器
9 |
10 | ---
11 |
12 | ## 文件清单
13 |
14 | - `docs/`
15 | - 迭代器模式.md【教程的核心内容】
16 | - `imgs/`
17 | - img01.png【教程所含的图片】
18 | - `codes/`
19 | - iterator.cpp【实例使用迭代器和反向迭代器对vector进行遍历的代码】
20 |
21 | ---
22 |
23 | ## 参考资料
24 |
25 | - [Iterators: The link between Data Structures and Algorithms (hashnode.dev)](https://dcode.hashnode.dev/iterators-the-link-between-data-structures-and-algorithms)
26 | - 《C++ Primer 中文版(第5版)》9.2.1 10.4
27 |
28 |
--------------------------------------------------------------------------------
/C++/第九讲/迭代器模式简介/codes/iterator.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 | #include
5 | using namespace std;
6 |
7 | int main() {
8 | vector vec = {1, 2, 3, 4, 5};
9 |
10 | cout << "use iterator:" << endl;
11 | for (auto iter1 = vec.begin(); iter1 != vec.end(); iter1++)
12 | cout << *iter1 << " ";
13 |
14 | cout << endl << "use reverse iterator:" << endl;
15 | for (auto iter2 = vec.crbegin(); iter2 != vec.crend(); iter2++)
16 | cout << *iter2 << " ";
17 | }
--------------------------------------------------------------------------------
/C++/第九讲/迭代器模式简介/docs/迭代器模式简介.md:
--------------------------------------------------------------------------------
1 | #第九讲:迭代器模式简介
2 |
3 | 通过上一讲的内容,相信大家已经简要了解了设计模式的原则;在本讲中,我们将学习一个具体的设计模式——**迭代器模式**,并了解迭代器模型的一个实现——**STL中的迭代器**。
4 |
5 | ## 迭代器模式
6 |
7 | **迭代器模式**是一种特殊的设计模式,它允许人们**遍历(或迭代)**数据结构。其实迭代(遍历)的思想我们一直在使用,例如下面这个遍历数组中元素并打印的例子:
8 |
9 | ```c++
10 | int a[5] = { 1,2,3,4,5 };
11 | for (int i = 0; i < 5; i++)
12 | cout << a[i] << endl;
13 | ```
14 |
15 | 对于每一种数据结构,无论是像数组这样的**线性数据结构**还是像二叉树这样的**非线性数据结构**,都有具体的方法实现对数据结构中元素的遍历。但是如果对于每种数据结构都采用不同的方法进行遍历,就会导致最终的算法实现与数据结构之间有很强的**关联性**,这并不是我们所期望的。为了将数据结构与算法进行**解耦**,迭代器模式因运而生。
16 |
17 | 简单来讲,我们可以将迭代器理解为**可以对任意数据结构的元素进行迭代的工具**,正是由于它的通用性,使得算法的呈现形式与具体的数据结构无关,进而实现了算法与数据结构的**解耦**。迭代器有许多种类,如可以顺序访问数据的**前向迭代器**、可以逆序访问数据的**反向迭代器**、可以随机访问数据的**随机迭代器**……
18 |
19 | 总之,迭代器**抽象了接口**,使我们的数据结构在算法中更加灵活。使用迭代器可以使算法与数据结构解耦,因而更符合程序设计的规范。
20 |
21 | ## STL中的迭代器
22 |
23 | 在**C++的标准模板库(STL)**中实现了迭代器,所有的标准库容器都支持迭代器。除此之外,`string`(不属于容器类型)也支持迭代器。
24 |
25 | > **容器**是STL中的一个重要的概念,在这里可以简单理解为**可以存储数据元素的数据结构的统称**。
26 |
27 | 迭代器提供了对对象的**间接访问**,使用迭代器可以访问容器中的某个元素,迭代器也能从一个元素移动到另一个元素。迭代器有**有效**和**无效**之分:**有效**的迭代器或者指向某个元素,或者指向容器中尾元素的下一位置;其他所有情况都属于**无效**。
28 |
29 | ### 迭代器范围
30 |
31 | 一个**迭代器范围**由一对迭代器表示,两个迭代器分别指向同一个容器中的元素或者是尾元素之后的位置。这两个迭代器通常被称为`begin`和`end`,它们标记了容器中元素的一个范围。
32 |
33 | 这种元素范围被称为**左闭合区间**,其数学表述为[begin , end)。迭代器`begin`和`end`必须指向相同的容器,`end`可以与`begin`指向相同的位置,但不能指向`begin`之前的位置。其中`end`通常被称为**尾后迭代器**。
34 |
35 | ### 迭代器支持的操作
36 |
37 | 1. `*iter`:返回迭代器`iter`所指元素的引用
38 | 2. `iter->mem`:解引用`iter`并获取该元素的名为`mem`的成员,等价于`(*iter).mem`
39 | 3. `++iter`:令`iter`指示容器中的下一个元素
40 | 4. `--iter`: 令`iter`指示容器中的上一个元素
41 | 5. `iter1 == iter2`/`iter1 != iter2`:判断两个迭代器是否相等(不相等),如果两个迭代器指示的是同一个元素或者它们是同一个容器的尾后迭代器,则相等;反之,不相等
42 |
43 | ### 迭代器支持的算术运算
44 |
45 | 下面给出迭代器支持的算术运算,注意并非所有容器的迭代器都支持这些运算。通常这些运算只能应用于`string`、`vector`、`deque`和`array`的迭代器。
46 |
47 | 1. `iter + n`:
48 |
49 | 迭代器加上一个整数值仍得一个迭代器,迭代器指示的新位置与原来相比向前移动了若干个元素。结果迭代器或者指示容器内的一个元素,或者指示容器尾元素的下一位置。
50 |
51 | 2. `iter - n`:
52 |
53 | 迭代器减去一个整数值仍得一个迭代器,迭代器指示的新位置与原来相比向后移动了若干个元素。结果迭代器或者指示容器内的一个元素,或者指示容器尾元素的下一位置。
54 |
55 | 3. `iter1 += n`:
56 |
57 | 迭代器加法的复合赋值语句,将`iter1`加`n`的结果赋给`iter1`
58 |
59 | 4. `iter1 -= n`:
60 |
61 | 迭代器减法的复合赋值语句,将`iter1`减`n`的结果赋给`iter1`
62 |
63 | 5. `iter1 - iter2`:
64 |
65 | 两个迭代器相间的结果是他们之间的距离,也就是说,将运算符右侧的迭代器向前移动差值个元素后将得到左侧的迭代器。参与运算的两个迭代器必须指向的是同一个容器中的元素或者尾元素的下一位置。
66 |
67 | 6. `>`、`>=`、`<`、`<=`:
68 |
69 | 迭代器的关系运算符,如果某迭代器指向的容器位置在另一个迭代器所指位置之前,则说前者小于后者。参与运算的两个迭代器必须指向的是同一个容器中的元素或者尾元素的下一位置。
70 |
71 | ### 常迭代器
72 |
73 | ```c++
74 | vector::iterator it1; //it1能读写vector的元素
75 | string::iterator it2; //it2能读写string对象中的字符
76 |
77 | vector::const_iterator it3; //it3只能读元素,不能写元素
78 | string::const_iterator it4; //it4能读字符,不能写字符
79 | ```
80 |
81 | 如上面的例子所示,**常迭代器(const_iterator)**和常量指针类似,能读取但不能修改它所指的元素值。而普通的迭代器(iterator)的对象可读可写。如果`vector`对象或`string`对象是一个常量,只能使用const_iterator;如果`vector`对象或`string`对象不是常量,那么既能使用iterator也能使用const_iterator。
82 |
83 | ### 反向迭代器
84 |
85 | 除了为每个容器定义迭代器之外,标准库在头文件`iterator`中还定义了额外几种迭代器,分别是**插入迭代器(insert iterator)**、**流迭代器(stream iterator)**、**反向迭代器(reverse iterator)**、**移动迭代器(move iterator)**。下面将详细介绍反向迭代器。
86 |
87 | **反向迭代器**就是在容器中从尾元素向首元素反向移动的迭代器。对于反向迭代器,递增(以及递减)操作的含义会颠倒过来。递增一个反向迭代器`++it`会移动到前一个元素;递减一个迭代器`--it`会移动到下一个元素。
88 |
89 | 除了`forward_list`之外,其他容器都支持反向迭代器。我们可以通过调用`rbegin`、`rend`、`crbegin`、`crend`成员函数来获得反向迭代器。下图以一个名为vec的`vector`为例来说明这4种迭代器的位置分布:
90 |
91 |
92 |

93 |
94 |
95 | 需要注意的是,我们只能从即支持`++`也支持`--`的迭代器来定义反向迭代器。毕竟反向迭代器的目的是在序列中反向移动。除了`forward_list`之外,标准容器上的其他迭代器都即支持递增运算又支持递减运算。但是,**流迭代器不支持递减运算**,因为不可能在一个流中反向移动。因此,不可能从一个`forward_list`或一个流迭代器创建反向迭代器。
96 |
97 |
98 |
99 | ------
100 |
101 | 为了使大家对迭代器模式能够有更深刻的理解,我编写了一份实例代码放在`codes/`文件夹内,该例子实现了**使用迭代器和反向迭代器对vector中的元素进行遍历**;大家可以自行阅读和运行尝试。祝大家学习愉快!
102 |
103 |
--------------------------------------------------------------------------------
/C++/第九讲/迭代器模式简介/imgs/img01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ksc999/THUEEXP-SAST-Tutor/d01ad77a614a8ff9eefa3c4a64fc7e8bf7b47f08/C++/第九讲/迭代器模式简介/imgs/img01.png
--------------------------------------------------------------------------------
/C++/第二讲/C++ 指针用法总结/README.md:
--------------------------------------------------------------------------------
1 | # 第三讲
2 |
3 | ## 简介
4 | - 内容主题:**C++ 指针用法总结**
5 | - 你将学到什么:
6 | - 指针的基本操作
7 | - 部分区别与注意事项
8 | - 关于几类特殊指针
9 | - 空指针
10 | - 函数指针
11 | - 类成员指针
12 |
13 |
14 | ## 文件清单
15 | - docs/
:
16 | - C++ 指针用法.md **—** 教程主要内容
17 |
18 | - codes/
:
19 | - Examples_1.cpp **—** 基本操作相关代码
20 | - Examples_2.cpp **—** 函数指针相关代码
21 |
22 | ## 参考资料
23 | - 《C++ Primer》(第五版)
24 | - [关于C++中的函数指针](https://www.geeksforgeeks.org/function-pointer-in-c/)
25 | - [C++类成员指针](https://blog.csdn.net/K346K346/article/details/49471287)
26 |
--------------------------------------------------------------------------------
/C++/第二讲/C++ 指针用法总结/codes/Examples_1.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | int main(){
3 | int x{ 7 };
4 | int y{ 3 };
5 | int* ptr{ &x };
6 |
7 | std::cout << &x << std::endl;
8 | std::cout << x << std::endl;
9 | std::cout << ptr << std::endl;
10 | std::cout << *ptr << std::endl;
11 |
12 | *ptr = 9;
13 |
14 | std::cout << &x << std::endl;
15 | std::cout << x << std::endl;
16 | std::cout << ptr << std::endl;
17 | std::cout << *ptr << std::endl;
18 |
19 | ptr = &y;
20 |
21 | std::cout << &y << std::endl;
22 | std::cout << y << std::endl;
23 | std::cout << ptr << std::endl;
24 | std::cout << *ptr << std::endl;
25 |
26 | std::cout << sizeof(ptr) << std::endl;
27 | std::cout << sizeof(*ptr) << std::endl;
28 |
29 | return 0;
30 | }
--------------------------------------------------------------------------------
/C++/第二讲/C++ 指针用法总结/codes/Examples_2.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | void add(int a, int b)
3 | {
4 | std::cout << "加和为 " << a + b;
5 | }
6 | void sub(int a, int b)
7 | {
8 | std::cout << "相差为 " << a - b;
9 | }
10 | void mul(int a, int b)
11 | {
12 | std::cout << "相乘为" << a * b;
13 | }
14 | void fun1() {
15 | std::cout << "Fun1";
16 | }
17 | void fun2() {
18 | std::cout << "Fun2";
19 | }
20 | void funx(void (*fun)()){
21 | fun();
22 | }
23 |
24 | int main()
25 | {
26 |
27 | //关于函数指针数组的主程序部分
28 |
29 | /*
30 | void (*fun_ptr_arr[])(int, int) = { add, sub, mul };
31 | int x, a = 1, b = 2;
32 |
33 | std::cout << "请选择: 输入0让两数相加, 输入1让两数相减,输入2让两数相乘\n";
34 | std::cin >> x;
35 |
36 | if (x > 2)
37 | return 0;
38 | (*fun_ptr_arr[x])(a, b);
39 | return 0;
40 | */
41 |
42 | //关于函数指针作为参数传递的主程序部分
43 |
44 | /*
45 | funx(fun1);
46 | funx(fun2);
47 | return 0;
48 | */
49 | }
--------------------------------------------------------------------------------
/C++/第二讲/C++ 指针用法总结/docs/C++指针用法.md:
--------------------------------------------------------------------------------
1 | # C++指针用法
2 |
3 | ### 目录
4 |
5 | 1. 指针的基本操作
6 |
7 | 2. 部分区别与注意事项
8 |
9 | 3. 关于几类特殊指针
10 |
11 | - 空指针
12 |
13 | - 函数指针
14 |
15 | - 类成员指针
16 |
17 | ### 指针的基本操作
18 |
19 | 在准备开始了解指针之前,让我们先来讨论一些更基本的东西。
20 |
21 | 我们定义一个变量(如`char a`),当执行对应的代码时,一块内存空间将会被分配给该对象,我们为对象所赋的值将会被储存在这块内存空间中。在我们使用`a`这个变量时,程序需要去访问内存空间中储存的值,而为了区分不同对象的内存空间,每块内存空间都会有一个标识符,也称作该对象的地址。这个地址将会通过**地址运算符(&)**来进行访问,例如:
22 |
23 | ```c++
24 | #include
25 | int main(){
26 | int x{ 5 };
27 | std::cout << x << ' ' << &x << std::endl;
28 | return 0;
29 | }
30 | ```
31 |
32 | 在笔者的电脑上打印出的结果为:
33 |
34 | ``` c++
35 | 5 0000008359EFF7A4
36 | ```
37 |
38 | 地址通常是以十六进制的形式进行打印,并且省略了前缀`0x`
39 |
40 | 与获取变量地址的运算符&相对应的是**解引用运算符(*)**,其操作数是一个地址,功能是将给定地址所储存的值返回,例如`*(&x)`便是获取变量x的值。
41 |
42 | 现在,我们可以开始来聊一聊指针的相关内容了。
43 |
44 | 首先需要知道的一点是,指针是一个对象,该对象的值是另一个常规对象的地址,其定义方式也与常规对象有所区别:
45 |
46 | ```c++
47 | int a{ 5 };//声明一个整型变量a,其值为5
48 | int* ptr{ &a };//声明一个整数指针(指向整数变量地址的指针),其值为整数变量a的地址
49 | ```
50 |
51 | 也就是说,通过在类型名旁添加一个*符号,我们就能够声明一个指针变量,当然,这时候\*并不是作为解引用运算符被使用。
52 |
53 | **注意!**当在同一行中声明多个指针变量时,需要在每个指针变量前都添加一个*符号:
54 |
55 | ```c++
56 | int* a, b, c;//声明了一个整数指针a,和两个整型变量b、c
57 | int* x, * y, * z; //声明了三个整数指针x、y和z
58 | ```
59 |
60 | 在定义了一个指针变量后,该指针在默认情况下不会被初始化,**未初始化的指针被称作野指针**,直接使用野指针的情况通常是不被允许的:
61 |
62 | ```c++
63 | int* x;
64 | std::cout << x;//非法语句
65 | std::cout << *x;//非法语句
66 | std::cout << &x;//有效语句,x是一个指针对象,其本身也有自己的地址,地址用来存放指向对象的地址
67 | ```
68 |
69 | 一个指针变量的初始化与使用可以为:
70 |
71 | ```c++
72 | #include
73 | int main(){
74 | int x{ 5 };
75 | std::cout << x << ' ';
76 | int* ptr{ &x };
77 | std::cout << *ptr;
78 | }
79 | ```
80 |
81 | 在该例子中,指针变量`ptr`被初始化为指向变量`x`的地址,也即`ptr = &x`,所以使用解引用运算符`*`也就是获取变量`x`的值。
82 |
83 | 一个简单的事实是,指针变量的类型必须与其所指向对象的类型相匹配:
84 |
85 | ```c++
86 | int x{ 1 };
87 | double y{ 0.2 };
88 | int* xptr1{ &x };//合法
89 | int* xPtr2{ &y };//非法
90 | double* yptr1{ &x };//非法
91 | double* yptr2{ &y };//合法
92 | ```
93 |
94 | 指针对象在初始化后,可以通过赋值语句来改变其所指向的对象,从而获取不同对象所储存的值:
95 |
96 | ```c++
97 | int x{ 3 };
98 | int y{ 5 };
99 | int* ptr{ &x };
100 | std::cout << *ptr;//此时打印变量x的值
101 | ptr = &y;
102 | std::cout << *ptr;//此时打印变量y的值
103 | ```
104 |
105 | ### 部分区别与注意事项
106 |
107 | 在了解了关于指针的声明、初始化和赋值这些基本操作后,我们有必要对一些细节做出处理。
108 |
109 | #### 指针与引用
110 |
111 | 来看一个例子:
112 |
113 | ```c++
114 | #include
115 | int main(){
116 | int x{ 5 };
117 | int& y{ x };
118 | int* ptr{ &x };
119 |
120 | std::cout << x;
121 | std::cout << y;
122 | std::cout << *ptr;
123 |
124 | y = 2;
125 | std::cout << x;
126 | std::cout << y;
127 | std::cout << *ptr;
128 |
129 | *ptr = 3;
130 | std::cout << x;
131 | std::cout << y;
132 | std::cout << *ptr;
133 | return 0;
134 | }
135 | ```
136 |
137 | 上述程序的打印结果为:
138 |
139 | ```c++
140 | 111222333
141 | ```
142 |
143 | 在例子中我们可以发现,指针和对象的引用都能够改变对象本身的值,并且两者之间能够互通,从这个角度来看,似乎指针是通过地址运算符`&`显式获取了指向对象的地址,又通过解引用运算符`*`显式获取了所指向地址的值,而引用则只是将操作转化为隐式,两者之间没有什么差别,但其实不然,两者之间实际上有不少值得指出的区别:
144 |
145 | - 引用在定义时必须进行初始化,而指针不用(当然,我们应该要初始化,但至少不会直接报错)
146 | - 引用并没有重新创建一个对象,而指针则创建了一个对象
147 | - 引用是对于一个改定对象的别名,并且指向的对象不能改变,但是指针可以
148 | - 引用必须总是指向一个对象,而指针不用
149 | - 通常情况下,引用是安全的,但是指针是**危险的**
150 |
151 | 关于最后两点,我们将在对于空指针的讨论中看到。
152 |
153 | #### 地址操作符
154 |
155 | 通过`&`获取一个对象的地址(如`&a`)时,`&a`并不是将`a`的地址作为一个字符串返回,它返回的是一个包含操作数地址的指针,并且该指针的类型来自于其操作数本身,这能够解释我们在程序中不能通过一串地址来直接对一个指针进行赋值:
156 |
157 | ```c++
158 | int a{ 5 };//定义变量a,并且&a = 0x0012FF7C
159 | int* ptr1{ a };//合法,指针ptr1指向变量a的地址
160 | int* ptr2{ 0x0012FF7C };//非法,尽管0x0012FF7C是一个地址,但是编译器将0x0012FF7C视作一个十六进制下的整数表示
161 | ```
162 |
163 | #### 指针的大小
164 |
165 | 指针的大小与指针类型无关,只与编译可执行文件的体系结构(32位:4字节;64位:8字节)有关系:
166 |
167 | ```c++
168 | #include
169 | int main(){//假设为64位操作系统
170 | char* a;
171 | int* b;
172 | long long* c;
173 | std::cout << sizeof(a);
174 | std::cout << sizeof(b);
175 | std::cout << sizeof(c);
176 | return 0;
177 | }
178 | ```
179 |
180 | 打印语句的结果均为`8`。
181 |
182 | ### 关于几类特殊指针
183 |
184 | #### 空指针
185 |
186 | 在之前的讨论中,我们知道指针在语法上可以不用指向任何对象,但是在使用时需要事先将其初始化,那么,在我们定义一个指针的时候,如果没有一个已知的有效对象用来作用指向目标,这种情况下,我们该如何处理呢?
187 |
188 | 事实上,我们的确不需要让指针指向一个具体的对象:
189 |
190 | ```c++
191 | int* ptr{};
192 | ```
193 |
194 | 这个语句是合法的,此时指针`ptr`没有指向一个具体对象,而是储存了一个`null`值,在此之后,我们可以重新通过赋值语句来让`ptr`指向一个具体对象并进行使用,或者,我们还可以通过赋值语句来让一个已经指向具体对象的指针变成`null`指针,这需要用到关键字`nullptr`:
195 |
196 | ```c++
197 | int main(){
198 | int a{ 5 };
199 | int* ptr1{ &a };//初始化指针ptr1,并让其指向对象a
200 | int* ptr2{ nullptr };//初始化指针ptr2,并让其指向null
201 | ptr2 = nullptr;//让指针ptr2重新成为null指针
202 | }
203 | ```
204 |
205 | **注意!**与野指针类似的是,我们应当尽量避免直接对`null`指针使用解引用运算符`*`,尽管有时编译器会为`null`指针赋予一个全为`0`的空地址,但有时直接使用也会让应用程序无法运行。
206 |
207 | 为了避免以上情况的发生,我们可以通过像布尔值`true`和`false`那样来对空指针和非空指针进行检测:
208 |
209 | ```c++
210 | #include
211 | int main(){
212 | int x { 5 };
213 | int* ptr { &x };
214 | if (ptr)
215 | std::cout << "ptr is non-null\n";
216 | else
217 | std::cout << "ptr is null\n";
218 | return 0;
219 | }
220 | ```
221 |
222 | 在以上例子中,我们可以发现,指针可以隐式地转化为布尔值`true`和`false`,其中空指针被转化为`false`,而非空指针则被转化为`true`。
223 |
224 | 通过对于空指针的简单讨论,我们应该认识到,尽管指针比较灵活,但是使用指针来访问对象会给我们带来一些附加的危险,所以,在没有足够的把握时,我们**尽可能优先使用引用而不是指针**。
225 |
226 | #### 函数指针
227 |
228 | 就像指向普通对象的指针那样,在**C++**中,我们还可以声明指向函数的指针:
229 |
230 | ```c++
231 | #include
232 | void fun(int a){
233 | std::cout << "Value of a is " << a;
234 | }
235 | int main(){
236 | void (*fun_ptr)(int) = &fun;
237 | (*fun_ptr)(10);
238 | return 0;
239 | }
240 | ```
241 |
242 | 程序的运行结果为:
243 |
244 | ```c++
245 | Value of a is 10
246 | ```
247 |
248 | 但是,可以发现一个有趣的事实是,`fun`的函数指针`fun_ptr`在声明的时候添加了一个额外的括号,如果我们去掉括号(也就是`void *fun_ptr(int)`),这会发生什么呢:
249 |
250 | ```c++
251 | #include
252 | void fun(int a){
253 | std::cout << "Value of a is " << a;
254 | }
255 | int main(){
256 | void *fun_ptr(int) = &fun;
257 | (*fun_ptr)(10);
258 | return 0;
259 | }xxxxxxxxxx #include void fun(int a){ std::cout << "Value of a is " << a;}int main(){ void (*fun_ptr)(int) = &fun; (*fun_ptr)(10); return 0;}void *fun_ptr(int) = &fun;
260 | ```
261 |
262 | 在我们将上述程序中的对应语句更换后,我们会沮丧地发现这个程序将无法正常执行,原因就在于**C++**中运算符的优先级在这个语句中依旧生效:
263 |
264 | ```c++
265 | void *fun_ptr(int);
266 | ```
267 |
268 | 在上述语句中,运算符()将优先于运算符*,所以这个语句实际意味着我们声明了一个具有一个int类型参数和void\*返回值的函数`fun_ptr`。为避免这个烦恼,我们需要将运算符`*`与`fun_ptr`绑定,因此在原本正确的程序中,我们才会看到那个括号的出现。
269 |
270 | > 在这里,我们看到了`void*`,这是一个`void`类型的指针,这种指针可以被任意数据类型的指针赋值,但是不能获取对象的值,我们可以通过以下的例子简单地理解:
271 | >
272 | > ```c++
273 | >#include
274 | > int main(){
275 | > int a{ 5 };
276 | > double b{ 0.6 };
277 | >
278 | > int* ptr1{ &a };
279 | > double* ptr2{ &b };
280 | > void* ptr3;
281 | >
282 | > ptr3 = ptr1;//合法
283 | > std::cout << *ptr3 << std::endl;//非法
284 | > ptr3 = ptr2;//合法
285 | > std::cout << *ptr2 << std::endl;//非法
286 | >
287 | > return 0;
288 | > }
289 | > ```
290 | > 另外,当一个函数的参数或者返回值中有`void*`类型的数据时,如果数据的类型无法确定,那么我们就可以使用`void`类型来代替。
291 |
292 | 除了上述基本应用外,函数指针还有一些其他的使用方法。比如,我们可以通过函数名来直接获取函数的地址,以最开始的程序为例:
293 |
294 | ```c++
295 | #include
296 | void fun(int a){
297 | std::cout << "Value of a is " << a;
298 | }
299 | int main(){
300 | void (*fun_ptr)(int) = fun;
301 | fun_ptr(10);
302 | return 0;
303 | }
304 | ```
305 |
306 | 注意到,上述程序与最开始那个程序的区别有两点:1.我们删除了`void (*fun_ptr)(int) = &fun;`语句中`fun`前面的地址运算符`&`;2.`(*fun_ptr)(10);`语句中我们同样删去了解引用运算符`*`更改了函数调用,这样程序依然有效。
307 |
308 | 此外,我们还可以创建函数指针数组,并让用户来作出决策决定指针执行的具体函数,或者将函数指针作为一个具体函数的参数进行传递,这些应用的举例我们都会在附加的`cpp`文件中给出。
309 |
310 | #### 类成员指针
311 |
312 | 类成员指针主要分为**数据成员指针**和**成员函数指针**两种,使用方法与普通指针类似,但需要使用类限定符,但需要注意的两点是:1.为成员函数指针赋值时,需要显示使用&运算符,不能直接将“类名::成员函数名”赋给成员函数指针(即通过函数名直接获取函数地址);2.使用数据成员指针时,被访问的成员往往是类的公有成员,如果是类的私有成员,容易出错。
313 |
--------------------------------------------------------------------------------
/C++/第二讲/C++ 智能指针简介/README.md:
--------------------------------------------------------------------------------
1 | # 第三讲
2 |
3 | ## 简介
4 | - 内容主题:**C++ 智能指针简介**
5 | - 你将学到什么:
6 | - 为什么要引入智能指针(动态分配new & delete简介)
7 | - 三类智能指针的简单介绍
8 | - `std::unique_ptr`
9 | - `std::shared_ptr`
10 | - `std::weak_ptr`
11 |
12 | ## 文件清单
13 | - docs/
:
14 | - 三类智能指针.md
15 | - codes/
:
16 | - Examples_1.cpp**—**智能指针类的简单思想
17 | - Examples_2.cpp**—**`std::make_unique()`的例子
18 | - Examples_3.cpp**—**`std::make_shared()`的例子
19 |
20 |
21 |
22 | ## 参考资料
23 | - 《C++ Primer》(第五版)
24 | - [learn Cpp](https://www.learncpp.com/)
25 | - [GeeksforGeeks C++ tutorials](https://www.geeksforgeeks.org/c-plus-plus/)
26 | - [C++11 创建和使用`std::unique_ptr`](https://www.cnblogs.com/DswCnblog/p/5628195.html)
27 | - [C++11 创建和使用`std::weak_ptr`](https://blog.csdn.net/Xiejingfa/article/details/50772571)
28 |
29 |
--------------------------------------------------------------------------------
/C++/第二讲/C++ 智能指针简介/codes/Examples_1.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | template
3 | class SmartPtr {
4 | T* ptr;
5 | public:
6 | explicit SmartPtr(T* p = NULL) { ptr = p; }
7 | ~SmartPtr() { delete (ptr); }
8 | T& operator*() { return *ptr; }
9 | T* operator->() { return ptr; }
10 | };
11 |
12 | int main(){
13 | SmartPtr ptr(new int());
14 | *ptr = 1;
15 | std::cout << *ptr;
16 | return 0;
17 | }
18 |
--------------------------------------------------------------------------------
/C++/第二讲/C++ 智能指针简介/codes/Examples_2.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | class Fraction{
4 | int m_num{ 0 };
5 | int m_den{ 1 };
6 | public:
7 | Fraction(int num = 0, int den = 1):m_num{ num }, m_den{ den }{
8 | }
9 |
10 | friend std::ostream& operator<<(std::ostream& out, const Fraction& f1){
11 | out << f1.m_num << '/' << f1.m_den;
12 | return out;
13 | }
14 | };
15 |
16 | int main(){
17 | auto f1{ std::make_unique(3, 5) };
18 | std::cout << *f1 << std::endl;
19 | auto f2{ std::make_unique(4) };
20 | std::cout << f2[0] << std::endl;
21 | return 0;
22 | }
--------------------------------------------------------------------------------
/C++/第二讲/C++ 智能指针简介/codes/Examples_3.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | class Resource{
4 | public:
5 | Resource() { std::cout << "对象生成" << std::endl; }
6 | ~Resource() { std::cout << "对象摧毁" << std::endl; }
7 | };
8 |
9 | int main(){
10 | auto ptr1{ std::make_shared() };
11 | {
12 | auto ptr2{ ptr1 };
13 | std::cout << "销毁一个指针" << std::endl;
14 | }
15 | std::cout << "销毁另一个指针" << std::endl;
16 | return 0;
17 | }
--------------------------------------------------------------------------------
/C++/第二讲/C++ 智能指针简介/docs/三类智能指针.md:
--------------------------------------------------------------------------------
1 | # 三类智能指针
2 |
3 | ### 目录
4 |
5 | 1. 为什么要引入智能指针?
6 | 2. 三类智能指针简介
7 | - `std::unique_ptr`
8 | - `std::shared_ptr`
9 | - `std::weak_ptr`
10 |
11 | ### 为什么要引入智能指针?
12 |
13 | 在上一讲中,我们简单介绍了`C++`中指针的各种用法,那里所提到的指针又被叫做**原始指针**,这是因为自`C++11`以后,引入了一类新的指针——**智能指针**,它们原本有四位成员:
14 |
15 | ```c++
16 | std::auto_ptr std::unique_ptr std::shared_ptr std::weak_ptr
17 | ```
18 |
19 | 但是第一位成员因为一些缺陷,从`C++17`开始就不再被允许,所以被留下来且被广泛应用的主要是后三位,它们也正是我们本讲的重要成员。不过呢,与上一讲类似的,我们也不着急直接与这三位见面,在此之前,我们先抛出一个问题,那就是:我现在`C++`目前已有的指针整得好好的,为什么要费心费力地去使用这几个用法相对繁琐,还有一套自己独特成员函数体系的三种指针呢?
20 |
21 | 嗯……好吧,这的确是一个需要去回答的问题,在回答这一个问题的同时,我们顺带讲一讲上一讲中没有提到的`C++`原始指针的一种重要用途:**内存的动态分配与释放**。
22 |
23 | 当我们创建数组时,需要给定数组的长度:
24 |
25 | ```c++
26 | #include
27 | class Animal{
28 | ...
29 | };
30 |
31 | int main(){
32 | int a[100];
33 | char str[50];
34 | Animal cat[1000];
35 | ...
36 | return 0;
37 | }
38 | ```
39 |
40 | 为了保证我们声明的内存足够后续的使用,我们能做的最好的就是将各种数组的长度分配为我们可能使用的最大数量,并且希望不会发生意外。
41 |
42 | 但是显然,这并非是一个好方法,原因至少有三点:
43 |
44 | - 在实际应用过程中,大多数情况下并不会使用被分配内存的大部分空间,这容易导致较大的内存浪费。
45 | - 我们平时所声明的普通变量(包含基本类型变量、类对象变量等),其内存是在**栈(Stack)**上进行分配,程序的栈空间通常不会很大,比如如果申请一个`int a[1000000]`可能就会导致栈溢出的问题。
46 | - 尽管我们期望意外不会发生,但是数组溢出的情况难免还是会存在,这会给用户的使用带来不小的限制。
47 |
48 | 为了避免固定内存分配所带来的各种麻烦,`C++`提出了一个解决方案:**动态内存分配**,其与指针的使用密切相关:
49 |
50 | ```c++
51 | int* a;
52 | int n;
53 | ```
54 |
55 | 我们申明一个整数指针`a`和整型变量`n`,现在我们想要创建一个整形数组,其大小经由用户决定,那么我们该怎么做呢?
56 |
57 | 首先,当然是由用户先输入想要创建的数组的长度,这个长度由`n`来决定:
58 |
59 | ```c++
60 | std::cin >> n;
61 | ```
62 |
63 | 接下来,我们需要用到一个新的关键字:`new`:
64 |
65 | ```c++
66 | a = new int[n];
67 | ```
68 |
69 | 上面几个语句是什么意思呢,大概可以等效为:
70 |
71 | ```c++
72 | int a[n];
73 | ```
74 |
75 | 当然,这个语句是非法的,因为在`C++`的语法中数组的固定分配内存不允许为一个变量。
76 |
77 | 现在,我们通过用户决定的长度创建了一个整形数组`a[n]`,其他类型包括类对象数组(`classA* a = new classA[n]`)也是被允许的,接下来我们**几乎**把它们当成一个普通数组来进行使用就可以了。
78 |
79 | 另外,如果想要在动态分配的时候进行初始化,当然也是可以的。
80 |
81 | ```c++
82 | a = new int[5]{1, 2, 3, 4, 5};
83 | ```
84 |
85 | 而且可以指出的是,动态分配的内存是在堆(Heap)上进行的,空间很大,所以基本上不会出现开不出`int a[1000000]`这样的数组的情况。
86 |
87 | 但是,还有一点小问题,我们之前提到了“几乎”,也就是说事实上动态内存分配和普通数组还是有不同的,这体现在哪里呢?
88 |
89 | 在我们使用普通数组时,当对象生命周期结束的时候,程序会自动帮我们执行销毁工作,但是动态分配不同,我们需要明确地告诉`C++`释放内存以供重新使用,这个工作将由关键字`delete`来完成:
90 |
91 | ```c++
92 | int* a{ new int };
93 | delete a;
94 | a = nullptr;
95 | ```
96 |
97 | 通过关键字`delete`,我们将指针被分配的内存返还给了操作系统,操作系统能够另将这份内存用作他用,但是指针本身依旧存在,它可以再次被分配给一个新的内存。
98 |
99 | 到此,我们已经学会了如何简单地使用`new`和`delete`来动态分配和释放内存,对指针的应用又多了一种领会,可喜可贺,可喜可贺。
100 |
101 | 但,这与一开始我们所提出的问题有什么关系呢?让我们来看看下面这个例子:
102 |
103 | ```c++
104 | #include
105 | class A{
106 | int a1;
107 | int a2;
108 | };
109 | void Func(){
110 | A* ptr = new A();
111 | }
112 | int main(){
113 | while(1){
114 | Func();
115 | }
116 | return 0;
117 | }
118 | ```
119 |
120 | 非常简单明了,程序只是在不断地创造和销毁A类对象`ptr`,仅此而已。
121 |
122 | 但是,真的只是如此吗?我们来仔细分析一下,函数`Func`创建一个A类指针,并通过动态分配对指针分配了一个A类对象的空间,当函数结束时,对象`ptr`是一个局部变量,所以它会被销毁,但是由于我们的疏忽,没能够对对象使用`delete`,所以对象本身虽然被销毁,但内存却没有被释放,这份内存不能被其他资源使用,而在主函数体内,无效内存在不断被分配,这也就是所说的**内存泄漏**现象。由于这个原因,整个堆上的内存可能会变得无法使用,而且即使是使用了`delete`,也会有无数种方法使得内存无法被释放,例如提前退出:
123 |
124 | ```c++
125 | #include
126 | void Func(){
127 | ClassA* ptr = new ClassA();
128 | int a;
129 |
130 | std::cin >> a;
131 | if(a == 0)
132 | return;
133 | delete ptr;
134 | }
135 | ```
136 |
137 | 或者异常处理:
138 |
139 | ```c++
140 | #include
141 | void Func(){
142 | ClassA* ptr = new ClassA();
143 | int a;
144 |
145 | std::cin >> a;
146 | if(a == 0)
147 | throw 0;
148 | delete ptr;
149 | }
150 | ```
151 |
152 | 还有种种原因都可能导致这种后果,从本质上来讲,这些问题的发生是因为原始指针缺少内在机制(就像类的析构函数那样)来自行处理。
153 |
154 | 于是,为了解决这样的问题,`C++`提出了它的方案——**智能指针**。
155 |
156 | ### 三类智能指针简介
157 |
158 | 如上所说,智能指针利用了类的思想(事实上它就是一个管理动态分配对象的类),它管理动态分配的资源,并确保在适当的时间(通常在智能指针超出范围时)正确清理动态分配的对象。一种简单的智能指针类被放在了样例程序中,具体的实现方式这里不做讨论,当我们自己使用智能指针时,需要引入一个头文件:`#include`,现在我们对常用的三种智能指针进行介绍。
159 |
160 | #### `std::unique_ptr`
161 |
162 | `std::unique_ptr`在三种智能指针中被应用得最广泛,以下是一个简单的实例:
163 |
164 | ```c++
165 | #include
166 | #include
167 | int main(){
168 | std::unique_ptr ptr1(new int(5));
169 | std::cout << *ptr1;
170 | return 0;
171 | }
172 | ```
173 |
174 | 正如指针名字所指出的那样,`std::unique_ptr`被用于管理不被其他对象共享的动态分配的对象,换句话说,一个`std::unique_ptr`指针完全享有它所指向的对象的管理权限,所以复制构造和赋值操作都是不被允许的:
175 |
176 | ```c++
177 | #include
178 | #include
179 | int main(){
180 | std::unique_ptr ptr1(new int(5));
181 | std::unique_ptr ptr2(ptr1); //Error!
182 | std::unique_ptr ptr3 = ptr1; //Error!
183 | return 0;
184 | }
185 | ```
186 |
187 | 但也有一个例外,那就是从函数中返回指针:
188 |
189 | ```c++
190 | #include
191 | #include
192 | std::unique_ptr Func(int ptr)
193 | {
194 | std::unique_ptr ptr1(new int(ptr));
195 | return ptr1; // 返回unique_ptr
196 | }
197 |
198 | int main() {
199 | int p = 1;
200 | std::unique_ptr ptr = Func(p);
201 | std::cout << *ptr;
202 | }
203 | ```
204 |
205 | 另外,管理权的转移是被允许的,这将通过`std::move()`来实现:
206 |
207 | ```c++
208 | #include
209 | #include
210 | int main(){
211 | std::unique_ptr ptr1(new int(5));
212 | std::unique_ptr ptr2(std::move(ptr1));
213 |
214 | std::cout << *ptr1; //Error! ptr1 has been removed!
215 | std::cout << *ptr2;
216 | return 0;
217 | }
218 | ```
219 |
220 | 如果要判断一个`std::unique_ptr`指针是否正管理一个对象,可以将指针直接作为布尔值进行判断:
221 |
222 | ```c++
223 | #include
224 | #include
225 | int main(){
226 | std::unique_ptr ptr(new int(5));
227 | if(ptr)
228 | std::cout << *ptr;
229 | return 0;
230 | }
231 | ```
232 |
233 | 另外,从`C++14`开始,我们可以通过`std::make_unique()`来创建`std::unique_ptr`对象,关于这一点的例子我们将在样例中给出。
234 |
235 | 在固定数组、动态数组和字符串的处理上,`std::array`、`std::vector`和`std::string`一般是更好的选择,`std::unique_ptr`指针可以用于以下几种情况:
236 |
237 | - 提供异常处理安全保证
238 |
239 | 先附上本讲中出现过的一段代码:
240 |
241 | ```c++
242 | #include
243 | void Func(){
244 | ClassA* ptr = new ClassA();
245 | int a;
246 |
247 | std::cin >> a;
248 | if(a == 0)
249 | throw 0;
250 | delete ptr;
251 | }
252 | ```
253 |
254 | 通过智能指针,这段代码可以更改为:
255 |
256 | ```c++
257 | #include
258 | #include
259 | void Func(){
260 | std::unique_ptr ptr(new ClassA());
261 | int a;
262 |
263 | std::cin >> a;
264 | if(a == 0)
265 | throw 0;
266 | }
267 | ```
268 |
269 | 这样就可以保证动态资源能够被释放。
270 |
271 | - 返回函数内动态资源的所有权(从函数中返回智能指针)
272 | - 在容器中储存指针
273 | ```c++
274 | #include
275 | #include
276 | #include
277 | int main()
278 | {
279 | std::vector> vec;
280 | std::unique_ptr ptr(new int(1));
281 | vec.push_back(std::move(ptr));
282 | return 0;
283 | }
284 | ```
285 |
286 | #### `std::shared_ptr`
287 |
288 | 与`std::unique_ptr`正相反,`std::shared_ptr`指针主要用于需要多个智能指针共同享有一个资源的情况,并且可以通过成员函数`use_count()`来跟踪有多少个智能指针指向同一个对象,只要至少有一个`std::shared_ptr`指向某一个资源,即使部分指向该资源的`std::shared_ptr`被销毁,该资源也不会被立即释放:
289 |
290 | ```c++
291 | #include
292 | #include
293 |
294 | int main(){
295 | std::shared_ptr ptr1(new int(1));
296 | std::shared_ptr ptr2(ptr1);
297 | std::cout << *ptr2 << std::endl;
298 | std::cout << ptr2.use_count();
299 | return 0;
300 | }
301 | ```
302 |
303 | 然后我们来看看下面的例子:
304 |
305 | ```c++
306 | #include
307 | #include
308 | int main(){
309 | int *a = new int;
310 | std::shared_ptr ptr1 { a };
311 | {
312 | std::shared_ptr ptr2 { a };
313 | std::cout << "销毁一个智能指针 ";
314 | }
315 |
316 | std::cout << "销毁另一个智能指针";
317 | return 0;
318 | }
319 | ```
320 |
321 | 语句被打印出来——然后程序崩溃。
322 |
323 | 原因在于两个智能指针之前没有通过复制操作正确建立联系,也就是说它们各自都认为自己独立享有同一个资源,当`ptr2`超出生命周期后,它会尝试销毁资源,然后`ptr1`超出生命周期时它也会这样做,于是寄。
324 |
325 | 为了避免这样的情况,请通过复制操作使得指针之间建立联系:
326 |
327 | ```c++
328 | #include
329 | #include
330 | int main(){
331 | int *a = new int;
332 | std::shared_ptr ptr1 { a };
333 | {
334 | std::shared_ptr ptr2 { ptr1 };
335 | std::cout << "销毁一个智能指针 ";
336 | }
337 |
338 | std::cout << "销毁另一个智能指针";
339 | return 0;
340 | }
341 | ```
342 |
343 | 类似地,我们也有`std::make_shared()`用来创建`std::shared_ptr`对象,实例被放在样例之中。
344 |
345 | #### `std::weak_ptr`
346 |
347 | 前两个指针中,`std::unique_ptr`用来解决单一所有权问题,而`std::shared_ptr`用于共享所有权问题,似乎不再需要其他指针,那么`std::weak_ptr`存在的意义是什么呢?让我们来看看下面的例子:
348 |
349 | ```c++
350 | #include
351 | #include
352 | #include
353 | class Person {
354 | std::string m_name;
355 | std::shared_ptr m_partner;
356 |
357 | public:
358 | Person(const std::string& name) : m_name(name) {
359 | std::cout << m_name << " 生成" << std::endl;
360 | }
361 | ~Person() {
362 | std::cout << m_name << " 摧毁" << std::endl;
363 | }
364 | friend bool partnerUp(std::shared_ptr& p1, std::shared_ptr& p2) {
365 | if (!p1 || !p2)
366 | return false;
367 | p1->m_partner = p2;
368 | p2->m_partner = p1;
369 | std::cout << p1->m_name << " 现在与 " << p2->m_name << "成为搭档" << std::endl;
370 | return true;
371 | }
372 | };
373 |
374 | int main() {
375 | auto Zhangsan{ std::make_shared("Zhang San") };
376 | auto Lisi{ std::make_shared("Li Si") };
377 | partnerUp(Zhangsan, Lisi);
378 | return 0;
379 | }
380 | ```
381 |
382 | 程序的执行结果如下:
383 |
384 | ```c++
385 | Zhang San 生成
386 | Li Si 生成
387 | Zhang San 现在与 Li Si成为搭档
388 | ```
389 |
390 | 在上面的示例中,在调用`partnerUp()`后,有两个共享指针指向`Zhang San`(`Zhang San`和`Li Si`的`m_partner`)和两个共享指针指向`Li Si`(`Li Si`和`Zhang San`的`m_partner`)。
391 |
392 | 在`main()`结束时,`Zhang San`共享指针首先超出范围。发生这种情况时,`Zhang San`检查是否有任何其他共享指针共同拥有`Zhang San`这个人。有(`Li Si`的`m_partner`)。正因为如此,它不会释放`Zhang San`(如果它释放了,那么`Li Si`的`m_partner`将最终成为一个悬空指针)。我们现在有一个指向`Zhang San`的共享指针(`Li Si`的 m_partner)和两个指向`Li Si`的共享指针(`Li Si`和`Zhang San`的`m_partner`)。
393 |
394 | 接下来`Li Si`共享指针超出范围,同样的事情发生了。共享指针`Li Si`检查是否有任何其他共享指针共同拥有`Li Si`这个人。有(`Zhang San`的`m_partner`),所以`Li Si`没有被释放。此时,有一个指向`Li Si`的共享指针(`Zhang San`的`m_partner`)和一个指向`Zhang San`的共享指针(`Li Si`的`m_partner`)。
395 |
396 | 然后程序结束,`Li Si`或`Zhang San`这两个对象都没有被释放——`Li Si`最终阻止了`Zhang San`被摧毁,而`Zhang San`最终阻止了`Li Si`被摧毁。
397 |
398 | 实际上,当`std::shared_ptr`形成循环引用的时候就有可能产生这种情况。而`std::weak_ptr`便是为了解决这种状况而出现。
399 |
400 | `std::weak_ptr`指向一个由`std::shared_ptr`管理的对象而不影响所指对象的生命周期,也就是将一个`std::weak_ptr`绑定到一个`std::shared_ptr`不会改变`std::shared_ptr`的引用计数。不论是否有`std::weak_ptr`指向,一旦最后一个指向对象的`std::shared_ptr`被销毁,对象就会被释放。从这个角度看,`std::weak_ptr`更像是`std::shared_ptr`的一个助手而不是智能指针。因此,我们在使用`std::weak_ptr`的时候需要依靠`std::shared_ptr`:
401 |
402 | ```c++
403 | int main() {
404 | std::shared_ptr ptr1(new int(1));
405 | cout << "创建前ptr1的引用计数:" << ptr1.use_count(); // use_count = 1
406 |
407 | std::weak_ptr ptr2(ptr1);
408 | cout << "创建后ptr1的引用计数:" << ptr1.use_count(); // use_count = 1
409 | }
410 | ```
411 |
412 | 我们利用`std::weak_ptr`来解决之前所遇到的问题:
413 |
414 | ```c++
415 | #include
416 | #include
417 | #include
418 | class Person{
419 | std::string m_name;
420 | std::weak_ptr m_partner;
421 |
422 | public:
423 |
424 | Person(const std::string &name): m_name(name){
425 | std::cout << m_name << " 生成" << std::endl;
426 | }
427 | ~Person(){
428 | std::cout << m_name << " 摧毁" << std::endl;
429 | }
430 |
431 | friend bool partnerUp(std::shared_ptr &p1, std::shared_ptr &p2){
432 | if (!p1 || !p2)
433 | return false;
434 | p1->m_partner = p2;
435 | p2->m_partner = p1;
436 | std::cout << p1->m_name << " 现在与 " << p2->m_name << "成为搭档" << std::endl;
437 | return true;
438 | }
439 | };
440 |
441 | int main(){
442 | auto Zhangsan{ std::make_shared("Zhang San") };
443 | auto Lisi{ std::make_shared("Li Si") };
444 | partnerUp(Zhangsan, Lisi);
445 | return 0;
446 | }
447 | ```
448 |
449 | 于是循环引用的问题便被我们解决了。
450 |
--------------------------------------------------------------------------------
/C++/第五讲/C++11语法糖杂讲/README.md:
--------------------------------------------------------------------------------
1 | # 第五讲
2 |
3 | ## 简介
4 |
5 | - 内容主题:**C++ 11 语法糖杂讲**
6 | - 你将学到什么:
7 | - `constexpr`变量
8 | - `auto`类型指示符
9 | - 范围`for`语句
10 | - 列表初始化
11 |
12 | ## 文件清单
13 |
14 | - `docs\`
15 | - C++ 11 语法糖.md:教程的主要内容
16 |
17 | - `codes\`
18 | - type_name.cpp:相关代码
19 |
20 |
21 | ## 参考资料
22 |
23 | - 《C++ Primer》第五版
24 |
25 | - [StackOverflow上关于C++打印数据类型的讨论](https://stackoverflow.com/questions/81870/is-it-possible-to-print-a-variables-type-in-standard-c)
26 |
27 |
--------------------------------------------------------------------------------
/C++/第五讲/C++11语法糖杂讲/codes/type_name.cpp:
--------------------------------------------------------------------------------
1 | /**
2 | * @file type_name.cpp
3 | * @author Zsbyqx2020
4 | * @brief Some related code in the tutorial.
5 | * @version 0.1
6 | * @date 2022-02-15
7 | *
8 | * @copyright Copyright (c) 2022
9 | *
10 | */
11 | #define _GLIBCXX_USE_CXX11_ABI 0
12 | #include
13 | #include
14 | #ifndef _MSC_VER
15 | #include
16 | #endif
17 | #include
18 |
19 | #include
20 | #include
21 | #include
22 | #include
23 | template
24 | void type_name(std::string name) {
25 | typedef typename std::remove_reference::type TR;
26 | std::unique_ptr own(
27 | #ifndef _MSC_VER
28 | abi::__cxa_demangle(typeid(TR).name(), nullptr, nullptr, nullptr),
29 | #else
30 | nullptr,
31 | #endif
32 | std::free);
33 | std::string r = own != nullptr ? own.get() : typeid(TR).name();
34 | if (std::is_const::value) r += " const";
35 | if (std::is_volatile
::value) r += " volatile";
36 | if (std::is_lvalue_reference::value)
37 | r += "&";
38 | else if (std::is_rvalue_reference::value)
39 | r += "&&";
40 | std::cout << "delctype of " << name << " is: " << r << std::endl;
41 | }
42 | int main() {
43 | // About 'constexpr'
44 | // 指向整型常量的指针
45 | const int *p1 = nullptr;
46 | type_name("p1");
47 |
48 | // 指向整数的常量指针
49 | int *const p2 = nullptr;
50 | constexpr int *q1 = nullptr;
51 | type_name("p2");
52 | type_name("q1");
53 |
54 | // 指向整形常量的常量指针
55 | const int *const p3 = nullptr;
56 | constexpr const int *q2 = nullptr;
57 | type_name("p3");
58 | type_name("q2");
59 |
60 | // About 'auto'
61 | // 关于引用
62 | int i = 0, &r = i;
63 | auto a = r;
64 | type_name("a");
65 |
66 | // 忽略顶层const
67 | const int ci = 0, &cr = ci;
68 | auto b = ci;
69 | auto c = cr;
70 | type_name("b");
71 | type_name("c");
72 |
73 | // 保留底层const
74 | auto d = &i;
75 | auto e = &ci;
76 | const auto f = ci;
77 | type_name("d");
78 | type_name("e");
79 | type_name("f");
80 |
81 | // const引用绑定
82 | auto &g = ci;
83 | const auto &j = 42;
84 | type_name("g");
85 | type_name("j");
86 |
87 | // 范围for语句
88 | std::vector v = {1, 2, 3, 4, 5, 6};
89 | for (auto &r : v) r *= 2;
90 | for (auto i : v) std::cout << i << " ";
91 | }
--------------------------------------------------------------------------------
/C++/第五讲/C++11语法糖杂讲/docs/C++11语法糖.md:
--------------------------------------------------------------------------------
1 | # 第五讲:C++ 11 语法糖杂讲
2 |
3 | ## 5.1 `constexpr` & 常量表达式
4 |
5 | > 我们在之前的编程学习以及在 **const的基本用法** 的介绍中,已经对 C++ 中的**常量表达式**有了初步的认识。在本节的学习中,我们会针对**常量表达式**这一重要概念继续深入,并介绍 C++ 11 新引入的 `constexpr` 机制。
6 |
7 | 那么首先,什么是常量表达式呢?
8 |
9 | > **常量表达式** 是指 **值不会改变** 并且 **在编译过程就能得到计算结果** 的表达式。
10 |
11 |
12 |
13 | 根据上述的定义,显然我们可以知道:
14 |
15 | - 字面值属于常量表达式;
16 | - 用常量表达式初始化的 `const` 对象也是常量表达式;
17 |
18 | - 一个对象是不是常量表达式,由它的**数据类型**和**初始值**共同决定。
19 |
20 |
21 |
22 | 接下来我们为大家准备了一些实例,供大家学习参考。
23 |
24 | ```c++
25 | const int max_files = 20; // max_files是常量表达式
26 | const int limit = max_files + 1; // limit是常量表达式
27 | int staff_size = 27; // staff_size不是常量表达式
28 | const int sz = get_size(); // sz不是常量表达式
29 | ```
30 |
31 | - 尽管`staff_size`的初始值是字面常量,但由于其**数据类型**只是普通的`int`而不是`const int`,所以它并不是常量表达式;
32 | - 尽管`sz`本身是一个常量,但是它的**具体值**需要在**运行时**才能获取到,所以它也不是常量表达式。
33 |
34 |
35 |
36 | 在对常量表达式这一概念有了更为深入的认知之后,我们继续介绍关于 `constexpr` 变量的相关知识。
37 |
38 | > 在一个复杂系统中,很难(几乎肯定不能)分辨一个初始值到底是不是常量表达式。
39 |
40 | 基于上述情形,C++ 11 新标准规定,允许将变量声明为 **constexpr** 类型,以便于编译器验证变量的值是否属于常量表达式。声明为 `constexpr` 的变量一定是一个常量,且必须用常量表达式初始化。
41 |
42 | ```c++
43 | constexpr int mf = 20; // 20是常量表达式
44 | constexpr int limit = mf + 1; // mf + 1是常量表达式
45 | constexpr int sz = size(); // 只有当size()是一个constexpr函数时才是正确语句
46 | ```
47 |
48 |
49 |
50 | 我们已经知道,常量表达式的值需要在**编译**时就进行计算,因此我们必须对声明 `constexpr` 时用到的类型进行限制(因为并不是每一种类型都能在编译时得到结果)。能够被声明的类型一般比较简单,值也显而易见、容易得到,它们被称为“**字面值类型**”。
51 |
52 | > 到目前为止接触过的数据类型中,算术类型、引用和指针都属于字面值类型。
53 | >
54 | > 自定义类、IO 库、string 类型则不属于字面值类型。
55 |
56 |
57 |
58 | 值得一提的是,尽管指针和引用属于字面值类型,它们的初始值却受到**严格限制**。一个 **constexpr** 指针的初始值必须是 **nullptr** 或 **0**,或者是**存储于某个固定地址的对象**。
59 |
60 | > **函数体内**定义的变量一般来说并非存储于固定地址,因此不能声明为 constexpr 指针所指的变量;
61 | >
62 | > 相反地,定义于**所有函数体外**的对象地址固定不变,可以用于初始化 constexpr 指针。
63 |
64 |
65 |
66 | 此外,在 constexpr 声明中如果定义了一个指针,限定符 constexpr **仅对指针有效**,而和指针所指的对象无关。
67 |
68 | ```c++
69 | const int *p = nullptr; // p是一个指向整型常量的指针
70 | constexpr int *q = nullptr; // q是一个指向整数的常量指针
71 | ```
72 |
73 |
74 |
75 | 同样与其他常量指针类似的是,constexpr 指针不一定指向常量。
76 |
77 | ```c++
78 | constexpr int *np = nullptr; // np是一个指向整数的常量指针,值为空
79 | int j = 0;
80 | constexpr i = 42; // i的类型是整形常量
81 | // i和j都必须定义于函数体之外
82 | constexpr const int *p = &i; // p是常量指针,指向整型常量i
83 | constexpr int *p1 = &j; // p1是常量指针,指向整数j
84 | ```
85 |
86 |
87 |
88 | ## 5.2 `auto` 类型指示符
89 |
90 | 编程时,我们需要把表达式的值赋给变量,这就要求在声明变量的时候知道表达式的类型。然而在某些情况下,我们可能很难知道甚至不可能知道某些表达式的类型。C++ 11 引入的 `auto` 类型说明符就可以让**编译器代替我们**分析表达式所属的类型。
91 |
92 | > 和原先那些只对应一种特定类型的说明符(double、float)不同,auto 会让编译器通过**初始值**来判断变量的类型。
93 |
94 | - auto 定义的变量必须有初始值
95 | - 使用 auto 也能在一条语句中声明多个变量
96 | - 因为一条声明语句只能有一个基本数据类型,所以该语句中所有变量的基本数据类型应该相同
97 |
98 |
99 |
100 | > 在实际运用中,编译器推断得到的数据类型可能与初始值**不相同**,编译器会适当**改变结果类型**使之更符合**初始化规则**。
101 |
102 | - 例 1 :
103 |
104 | ```c++
105 | int i = 0, &r = i;
106 | auto a = r;
107 | // a是一个整数(r是i的别名,而i是一个整数)
108 | // 在本例中,编译器遵循“使用引用本质上是使用引用的对象”原则
109 | // 使用引用对变量进行初始化时,初始值应该是被引用的对象的值
110 | // 因而a的类型被推断为整型而不是一个引用
111 | ```
112 |
113 | - 例 2 :
114 |
115 | ```c++
116 | const int ci = i, &cr = ci;
117 | auto b = ci;
118 | // b是一个整数(ci的顶层const特性被忽略)
119 | auto c = cr;
120 | // c是一个整数(cr是ci的别名,ci的顶层const特性被忽略)
121 | auto d = &i;
122 | // d是一个整型指针(整数的地址就是指向整数的指针)
123 | auto e = &ci;
124 | // e是一个指向整型常量的指针(对常量对象取地址是底层const)
125 | const auto f = ci;
126 | // ci的推演类型是int,这种声明方式下f就可以保留const
127 |
128 | // 本例体现的是auto忽略顶层const而保留底层const的特性
129 | ```
130 |
131 | - 例 3 :
132 |
133 | ```c++
134 | auto &g = ci;
135 | // g是一个整型常量引用,绑定到ci
136 | // auto &h = 42;
137 | // 这种写法错误,不能为非常量引用绑定字面值
138 | const auto &j = 42;
139 | // 正确,可以为常量引用绑定字面值
140 |
141 | // 当引用的类型被设置为auto时,原先的初始化规则仍然适用
142 | // 设置一个类型为auto的引用时,初始值中的顶层const属性仍然保留
143 | ```
144 |
145 |
146 |
147 | ## 5.3 范围 `for` 语句
148 |
149 | `for`循环语句作为 C++ 中十分多见的语法运用,想必大家都对此比较熟悉。在 C++ 11 的新标准中,引入了一种更为简单的`for`语句,这种语句可以遍历**容器或其他序列**的所有元素。
150 |
151 | 范围`for`语句的语法形式如下:
152 |
153 | ```c++
154 | for (declaration : expression)
155 | statement
156 | ```
157 |
158 | - expression 表示的必须是一个**序列**;
159 | - declaration 定义一个**变量**,序列中的每一个元素都应该**能被转换成**该变量的类型;
160 | - 确保类型相容最简单的办法是使用`auto`类型说明符
161 | - 每次迭代都会重新定义循环控制变量,并将其**初始化**为序列中的下一个值,之后执行 statement;
162 | - statement 可以是一条单独的语句,也可以是一个块。
163 |
164 |
165 |
166 | 接下来我们会以 C++ 的`vector`为例来展示范围`for`语句的使用方法。
167 |
168 | ```c++
169 | vector v = {0,1,2,3,4,5,6,7,8,9};
170 | // 需要对元素执行写操作时,范围变量需要设置为引用类型
171 | for (auto &r : v)
172 | r *= 2;
173 | ```
174 |
175 |
176 |
177 | ## 5.4 列表初始化
178 |
179 | C++ 语言定义了初始化的很多种形式。例如要想定义一个名为`units_sold`的`int`变量并将其初始化为 0,以下的 4 条语句都可以做到这一点:
180 |
181 | ```c++
182 | int units_sold = 0;
183 | int units_sold = {0};
184 | int units_sold{0};
185 | int units_sold(0);
186 | ```
187 |
188 | > 作为 C++ 11 新标准的一部分,用`{}`来初始化变量得到了全面应用。
189 | >
190 | > 这种初始化的方式被称为列表初始化。
191 |
192 | 当用于内置类型的变量时,这种初始化形式有一个重要特点:如果我们使用列表初始化且初始值存在**丢失信息**的风险,则编译器将报错。
193 |
194 | ```c++
195 | long double ld = 3.1415926536;
196 | int a{ld}, b = {ld};
197 | // 报错:转换未执行,因为存在丢失信息的风险
198 | int c(ld), d = ld;
199 | // 正确转换:自动丢失部分值
200 | ```
201 |
202 | 此外,列表初始化也可用于初始化`vector`对象。此时,用`{}`括起来的 0 个或多个元素将会被赋值给 vector 对象。
203 |
204 | ```c++
205 | vector articles = {"a", "an", "the"};
206 | // 上述vector对象中包含列表内的3个元素
207 | ```
208 |
209 |
210 |
211 | ## 备注
212 |
213 | 本部分教程主要为大家介绍了一部分我们日常编程过程中经常能够用到的 C++ 11 的新标准和新用法,希望能为大家日后的编程学习提供一定的帮助和参考。
214 |
--------------------------------------------------------------------------------
/C++/第五讲/C++_const/README.md:
--------------------------------------------------------------------------------
1 | # 第五讲 const
2 |
3 | ## 简介
4 |
5 | - 内容主题:`C++ const简介`
6 | - 你将学到什么:
7 | - 为什么要使用const?(为什么不用别的?)
8 | - const的基本用法
9 | - const在多文件中的使用
10 |
11 | ---
12 |
13 | ## 文件清单
14 |
15 | - `docs/`
16 | - Sast tutor 2022-const.md【教程】
17 | - `codes/`
18 | - main.cpp 【示例程序】
19 | - constants.h 【示例程序】
20 |
21 | ---
22 |
23 | ## 参考资料
24 |
25 | - 清华大学计算机系姚海龙老师程序设计基础课件
26 | - learncpp [4.15](https://www.learncpp.com/cpp-tutorial/const-constexpr-and-symbolic-constants/), [6.9](https://www.learncpp.com/cpp-tutorial/sharing-global-constants-across-multiple-files-using-inline-variables/), [12.12](https://www.learncpp.com/cpp-tutorial/const-class-objects-and-member-functions/)
27 | - [const (C++) | Microsoft Docs](https://docs.microsoft.com/en-us/cpp/cpp/const-cpp?view=msvc-170)
28 | - [Runtime and Compile-time constants in C++ - GeeksforGeeks](https://www.geeksforgeeks.org/runtime-and-compile-time-constants-in-c/)
--------------------------------------------------------------------------------
/C++/第五讲/C++_const/codes/constants.h:
--------------------------------------------------------------------------------
1 | #ifndef CONSTANTS_H
2 | #define CONSTANTS_H
3 |
4 | namespace constants {
5 | const double pi = 3.1416927;
6 | const double avogadro = 6.022e23;
7 | }
8 |
9 | #endif // !CONSTANTS_H
10 |
--------------------------------------------------------------------------------
/C++/第五讲/C++_const/codes/draft.cpp:
--------------------------------------------------------------------------------
1 | #include"constants.h"
2 |
3 | #include
4 | using namespace std;
5 |
6 | class rectangle { //定义一个矩形类
7 | public:
8 | rectangle(double l, double w) : length(l), width(w) {} //构造函数
9 | double length; //长
10 | double width; //宽
11 | double get_area() /*注意此处的const*/ const { return length * width; } //求矩形的面积
12 | bool operator < (const rectangle& r) { //比较两个矩形面积的大小
13 | return get_area() < r.get_area();
14 | }
15 | };
16 | int main() {
17 | //const变量
18 | const int a = 10; //静态绑定
19 | int b;
20 | cin >> b;
21 | const int c = b; //动态绑定
22 | //a++; //常量不能修改
23 | //c++;
24 | char txt0[10];
25 | char txt1[a];
26 | //char txt2[b];
27 | //char txt3[c]; //运行时绑定的常量不能作为数组大小
28 |
29 |
30 | //const作为函数参数
31 | void func(const int* p);
32 |
33 | //类中的const
34 | const rectangle re(6, 7); //构造一个常对象
35 | re.get_area(); //可以调用常成员函数
36 | //re.length = 2; //不能修改常对象的数据
37 |
38 | rectangle a(2, 5), b(3, 4); //构造两个矩形
39 | if (a < b) cout << "矩形a的面积小于b" << endl;
40 |
41 |
42 | //多文件下的const
43 | cout << "please enter the radius:";
44 | double r;
45 | cin >> r;
46 | cout << "the area of the circle is: " << constants::pi * r * r << endl;
47 |
48 | return 0;
49 | }
50 | void func(const int* p) {
51 | int a = *p;
52 | //*p = a++; //不能修改
53 | }
--------------------------------------------------------------------------------
/C++/第五讲/C++_const/docs/Sast tutor 2022-const.md:
--------------------------------------------------------------------------------
1 | # const
2 |
3 | ### 目录
4 |
5 | 1.为什么要使用const?(为什么不用别的?)
6 |
7 | 2.const的基本用法
8 |
9 | 3.const在多文件中的使用
10 |
11 | ### 1.为什么要使用const?(为什么不用别的?)
12 |
13 | 写代码时,我们需要将**数据(*data*)**存放在**变量(*variables*)**中,通过对变量进行各种运算和操作,实现功能或解决问题。然而,随着变量的不断增多和发量的不断减少,我们有时会发现:所有数据都可以存放在变量中,但单从字面上看,有些数据比其他数据更像**变**量。
14 |
15 | 为什么这么说呢?进行某些数学计算时,我们需要用到圆周率Pi;下五子棋时,我们需要知道棋盘的大小N*N。在一段程序运行的过程中,圆周率Pi或棋盘边长N往往不会发生改变(否则,我们需要操心的恐怕是一些超出这段程序的东西,比如数学大厦是不是摇摇欲坠或者某选手是不是在作弊)。但是,如果我们在声明这些变量时仅仅使用普通的double或int类型,我们便(1)很难保证我们的代码不会在某个地方稀里糊涂地修改了这些变量,尤其是当我们同时使用n,m,i,j,k,l,p,q……以及它们的大写作为变量名的时候。同时,(2)把一堆压根不会改变的数字专门用一堆变量来存放,对总体的程序而言也是不必要的开销。为了解决这些问题,我们需要使用一些新的手段。
16 |
17 | “简单简单!这有什么难的?”小鱼儿看也不看,在程序的开头加了几行代码,它看起来好像是:
18 |
19 | ```c++
20 | #define Pi 3.1415927
21 | #define N 15
22 | #define 秋季学期假期天数 0
23 | //...
24 | ```
25 |
26 | 你也是这么想的吗?的确,我们之前学习过**宏(*macro*)**的概念和用法,也曾经在试卷上见过它,知道它会让预处理器在预处理阶段(也就是编译之前的阶段)将程序中的一些字段替换为另一些字段。但是,且不提宏命令在程序中多么容易导致错误——这个理由或许已经很充分了——还有更多原因让我们在面对那些**较为复杂的问题**时不去使用它。
27 |
28 | 1.因为它发挥作用的途径是替换文本,宏命令中的“变量”不是真正的变量,在debug时很难确定它的值。
29 |
30 | 2.宏可能在代码的其他部分引起冲突,尤其当引入的头文件个数多且宏命名不规范时。
31 |
32 | “够了够了!我直接敲数字,总不会出错了吧?”小鱼儿不服气。
33 |
34 | 对于**简单**的程序,敲数字当然没有问题。但是当程序规模变大,尤其是这些数字需要调整时,这样做就会带来很多困难。设想新人程序员小E在上班第一天就被叫去修改这样的代码:
35 |
36 | ```c++
37 | total_num = 5 * 2; //5是行数,2是列数
38 | set_rows(5);
39 | set_cols(2);
40 | get_content(total_num);
41 | show(screen);
42 | funcxxx(2);
43 | ```
44 |
45 | 小E耐着性子一个个更换这些数字时,突然犯了难:这个奇怪函数funcxxx(2)里的数字要不要改?这个2是啥???此刻,小E只想打电话给那个被优化了的前辈,请他在送外卖之余为自己好好解释一下代码里的这些数各自究竟都是些什么意思。然后,小E要给这些数字**起好名字,写好注释**~~,让自己的后辈以后能更好地接手自己的工作~~。
46 |
47 | “行吧行吧!其实我还可以用枚举类型enum……”小鱼儿好像还有话要说。
48 |
49 | 确实,还有其他方式来表示我们所说的“常量”,但现在是时候展示一个更加常见和广泛的用法了:**const**。
50 |
51 | ### 2.const的基本用法
52 |
53 | #### (1)常变量
54 |
55 | const在代码中的作用类似于形容词或副词在句子中的作用,比方说,int a是整型变量,const int a则是**常**整型变量(它也可以写作int const a,但我们不会这样写),const变量在声明之后不允许被改变,因此,我们必须在**声明的同时定义**它(给它赋值)。如果直接使用数字或表达式定义,在编译阶段就可以确定它的值,称为**静态绑定/编译时绑定**,如果只有运行过程中才能确定,则称为**动态绑定/运行时绑定**,静态绑定和动态绑定相比消耗的资源更少,但没那么灵活。
56 |
57 | ```c++
58 | const int a = 2022;
59 | const int b {2022}; //另一种写法
60 | int c;
61 | cin >> c; //键盘中输入2022,回车
62 | const int d {c}; //不直接用数字定义const变量的方法,称为运行时绑定/动态绑定
63 | ```
64 |
65 | 在C++中,静态绑定的常变量还可以用来确定数组大小:
66 |
67 | ```c++
68 | const int n = 100;
69 | char text[n]; //不会报错!
70 | ```
71 |
72 | #### (2)常·函数参数
73 |
74 | 当函数参数为指针或引用时,我们可以通过用const修饰参数来保护参数(为什么不考虑直接传递参数值的情况?请你思考),这样做可以使得指针或引用所指向的内存在函数体内无法修改,就像这样:
75 |
76 | ```c++
77 | int max(const int* a,const int* b) {
78 | //...
79 | *a = 111; //报错!不允许修改*a的值
80 | }
81 | ```
82 |
83 | 为指针添加const修饰时,const的位置决定了它的含义,理解它的关键在于看const后面紧跟的是什么,如下:
84 |
85 | ```c++
86 | const int* a; //const修饰的是int,所以a指向的内存空间中的数据不允许修改。
87 | int* const b; //const修饰的是a,所以a指向的地址不允许修改。
88 | *a = *b; //报错!
89 | b = a; //报错!
90 | ```
91 |
92 | #### (3)常 in class
93 |
94 | 学习了类的知识后,我们知道const也可以用来修饰对象,得到**常对象**。与前面类似,常对象自身的内容不能改变。请你阅读以下代码:
95 |
96 | ```c++
97 | #include
98 | using namespace std;
99 | class rectangle { //定义一个矩形类
100 | public:
101 | rectangle(double l, double w) : length(l), width(w) {} //构造函数
102 | double length; //长
103 | double width; //宽
104 |
105 | double get_area() { return length * width; } //求矩形的面积
106 | bool operator < (const rectangle& r) { //比较两个矩形面积的大小
107 | return get_area() < r.get_area();
108 | }
109 | };
110 | int main() {
111 | rectangle a(2, 5), b(3, 4); //构造两个矩形
112 | if (a < b) cout << "矩形a的面积小于b" << endl;
113 | return 0;
114 | }
115 | ```
116 |
117 | 在这段代码中,我们构造了rectangle类,并重载了它的小于号运算符<。其中虽然没有出现常对象,但出现了**对象的常引用**,在向函数传递参数时使用常引用,在函数体内这个引用就被理解为常对象,其内部数据就不会被更改。但是,如果我们把这段代码复制进编译器中,编译器会报错。**仔细看看,你能发现问题在哪吗?**
118 |
119 | 问题在于:在重载小于号时,我们调用了常引用r的get_area()函数。由于成员函数有可能修改对象内的数据,C++不允许常对象调用普通的成员函数,为此,我们需要使用的是**常成员函数**。因为get_area()函数确实没有修改成员,在本例中我们只需作如下修改就可消除bug:
120 |
121 | ```c++
122 | double get_area() { return length * width; } //普通成员函数,需要被替换
123 | double get_area() const { return length * width; } //常成员函数,可以被常对象调用,注意const的位置
124 | ```
125 |
126 | 常成员函数不能修改对象内的任何数据,不仅可以被常对象调用,也可以被普通对象调用,是不是很方便呢?有人说,代码出bug时不妨往里面加几个const,这话的确有几分道理呢。
127 |
128 | ### 3.const在多文件中的使用
129 |
130 | 当我们面临较大项目的编程时,可能会遇到这样的情况:有些常数在各个地方都要用,但是如果在每个文件里都定义一遍就太浪费了,要是它们定义时的精度不一样,还可能造成更大的问题。对此,**方法一**是利用我们在**上一讲**学过的有关**命名空间(*namespace*)**的知识,把所有常数放在同一个头文件**constants.h**里,如下(constexpr的用法我们会在稍后接触,现在只需把它理解为const):
131 |
132 | ```c++
133 | #ifndef CONSTANTS_H
134 | #define CONSTANTS_H
135 | namespace constants
136 | {
137 | constexpr double pi { 3.1415927 };
138 | constexpr double avogadro { 6.0221413e23 };
139 | // ... 其他常数
140 | }
141 | #endif
142 | ```
143 |
144 | 之后,在其他文件中,可以通过“命名空间”+“::”使用这些常数,比如在main.cpp中:
145 |
146 | ```c++
147 | #include "constants.h" // 把含常数的头文件加入这个文件
148 | #include
149 | int main()
150 | {
151 | std::cout << "Enter a radius: ";
152 | int radius{};
153 | std::cin >> radius;
154 | std::cout << "The circumference is: " << 2.0 * radius * constants::pi << '\n';
155 | return 0;
156 | }
157 | ```
158 |
159 | 如果这样做,一个好处是编译器在编译时往往会直接对这些常量进行优化(用数字直接代替它)。但是,因为使用#include会导致每个文件中都有一个constants.h(使用#ifndef等命令只能保证它在每个文件**内部**只出现一次),这种方法仍然有其不足:
160 |
161 | 1.修改一个常量值(比如调整某个参数)会要求重新编译每个包含constants.h的文件,如果项目很大,这将需要很长的时间。
162 |
163 | 2.如果常量的个数太多使得编译器无法把它们优化掉,这些“变量”会消耗许多内存。
164 |
165 | 为了避免这些问题,我们可以采取**方法二**,同样利用**上一讲**中的知识,把这些常量变成**外部变量(*external variables*)**。在方法二中,我们在**constants.cpp**而不是constants.h中实例化(赋值):
166 |
167 | ```c++
168 | #include "constants.h"
169 | namespace constants
170 | {
171 | extern const double pi { 3.1415927 };
172 | extern const double avogadro { 6.0221413e23 };
173 | // ... 其他常数
174 | }
175 | ```
176 |
177 | 而在**constants.h**中,我们只是简单地声明了这些常量(这种声明被称为**前向声明(*forward declaraion*)**):
178 |
179 | ```c++
180 | #ifndef CONSTANTS_H
181 | #define CONSTANTS_H
182 | namespace constants
183 | {
184 | extern const double pi;
185 | extern const double avogadro;
186 | // ... 其他常数(只有声明)
187 | }
188 | #endif
189 | ```
190 |
191 | (注意,这里使用的是const而不是constexpr,在学习相关知识后,你能说出为什么这里不能使用constexpr吗?)
192 |
193 | 而其他文件中的代码保持不变:
194 |
195 | ```c++
196 | #include "constants.h" // 把含常数的头文件加入这个文件
197 | #include
198 | int main()
199 | {
200 | std::cout << "Enter a radius: ";
201 | int radius{};
202 | std::cin >> radius;
203 | std::cout << "The circumference is: " << 2.0 * radius * constants::pi << '\n';
204 | return 0;
205 | }
206 | ```
207 |
208 | 在这种方法下,每个包含constants.h的文件内实际上并不包含常量们实际的取值。因此,修改常量值时只需要重新编译constants.cpp一个文件。但是,这种做法同样有缺陷:因为除了constants.cpp之外的文件在编译时只能看到constants.h中的前向声明,所以这些常量都被视为**动态绑定/运行时绑定**,无法实现静态绑定的常量的功能,编译和运行时也可能会消耗更多资源。两个方法各有长短,总而言之,这是一个权衡利弊的问题。
209 |
210 |
--------------------------------------------------------------------------------
/C++/第八讲/工厂模式/README.md:
--------------------------------------------------------------------------------
1 | ## 简介:工厂模式(factory pattern)
2 |
3 | 1、简单工厂模式(simple factory pattern)
4 |
5 | 2、工厂方法模式(factory method pattern)
6 |
7 | 3、抽象工厂模式(abstract factory pattern)
8 |
9 | ----
10 |
11 | ## 参考资料:
12 |
13 | [DesignPattern/01.SimpleFactory.md at master · FengJungle/DesignPattern · GitHub](https://github.com/FengJungle/DesignPattern/blob/master/01.SimpleFactory/01.SimpleFactory.md)
14 |
15 | [Factory Patterns - Simple Factory Pattern - CodeProject](https://www.codeproject.com/Articles/1131770/Factory-Patterns-Simple-Factory-Pattern)
16 |
17 |
--------------------------------------------------------------------------------
/C++/第八讲/工厂模式/codes/工厂方法模式.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | using namespace std;
3 |
4 | class AbstractEmployee
5 | {
6 | public:
7 | AbstractEmployee() {
8 |
9 | }
10 | virtual ~AbstractEmployee() {};
11 | virtual void nice_work() = 0;
12 | virtual void bad_work() = 0;
13 | };
14 |
15 | class Teacher :public AbstractEmployee
16 | {
17 | public:
18 | Teacher() {
19 |
20 | }
21 | ~Teacher() {};
22 | void nice_work()
23 | {
24 | cout << "The teacher works well." << endl;
25 | }
26 | void bad_work()
27 | {
28 | cout << "The teacher works badly." << endl;
29 | }
30 | };
31 |
32 | class Professor :public AbstractEmployee
33 | {
34 | public:
35 | Professor() {
36 |
37 | }
38 | ~Professor() {};
39 | void nice_work()
40 | {
41 | cout << "The professor works well." << endl;
42 | }
43 | void bad_work()
44 | {
45 | cout << "The professor works badly." << endl;
46 | }
47 | };
48 |
49 | class AbstractFactory
50 | {
51 | public:
52 | virtual shared_ptr getEmployeeEvaluation(string evaluation) = 0;
53 | virtual ~AbstractFactory() {}
54 | };
55 |
56 | class TeacherEnvaluationFactory: public AbstractFactory
57 | {
58 | public:
59 | shared_ptr getEmployeeEvaluation(string evaluation)
60 | {
61 | shared_ptr emp;
62 | emp = shared_ptr(new Teacher());
63 | if (evaluation == "Good")
64 | emp->nice_work();
65 | else if (evaluation == "Bad")
66 | emp->bad_work();
67 | return emp;
68 | }
69 | };
70 |
71 | class ProfessorEnvaluationFactory : public AbstractFactory
72 | {
73 | public:
74 | shared_ptr getEmployeeEvaluation(string evaluation)
75 | {
76 | shared_ptr emp;
77 | emp = shared_ptr(new Professor());
78 | if (evaluation == "Good")
79 | emp->nice_work();
80 | else if (evaluation == "Bad")
81 | emp->bad_work();
82 | return emp;
83 | }
84 | };
85 |
86 | int main()
87 | {
88 | shared_ptr efac = make_shared();
89 | shared_ptr emp = efac->getEmployeeEvaluation("Good");
90 | efac = make_shared();
91 | emp = efac->getEmployeeEvaluation("Good");
92 | }
93 |
--------------------------------------------------------------------------------
/C++/第八讲/工厂模式/codes/简单工厂模式.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | using namespace std;
3 |
4 | class AbstractEmployee
5 | {
6 | public:
7 | AbstractEmployee() {
8 |
9 | }
10 | virtual ~AbstractEmployee() {};
11 | virtual void nice_work() = 0;
12 | virtual void bad_work() = 0;
13 | };
14 |
15 | class Teacher :public AbstractEmployee
16 | {
17 | public:
18 | Teacher() {
19 |
20 | }
21 | ~Teacher() {};
22 | void nice_work()
23 | {
24 | cout << "The teacher works well." << endl;
25 | }
26 | void bad_work()
27 | {
28 | cout << "The teacher works badly." << endl;
29 | }
30 | };
31 |
32 | class Professor :public AbstractEmployee
33 | {
34 | public:
35 | Professor() {
36 |
37 | }
38 | ~Professor() {};
39 | void nice_work()
40 | {
41 | cout << "The professor works well." << endl;
42 | }
43 | void bad_work()
44 | {
45 | cout << "The professor works badly." << endl;
46 | }
47 | };
48 |
49 | class EnvaluationFactory
50 | {
51 | public:
52 | std::shared_ptr getEmployeeEvaluation(string employeename,string evaluation)
53 | {
54 | std::shared_ptr emp;
55 | if (employeename == "Teacher") {
56 | emp = std::shared_ptr(new Teacher());
57 | }
58 | else if (employeename == "Professor") {
59 | emp = std::shared_ptr(new Professor());
60 | }
61 | if (evaluation == "Good")
62 | emp->nice_work();
63 | else if (evaluation == "Bad")
64 | emp->bad_work();
65 | return emp;
66 | }
67 | };
68 |
69 | int main()
70 | {
71 | shared_ptr efac = std::make_shared();
72 | shared_ptr emp = efac->getEmployeeEvaluation("Teacher","Good");
73 | emp = efac->getEmployeeEvaluation("Professor", "Bad");
74 | }
75 |
--------------------------------------------------------------------------------
/C++/第八讲/工厂模式/docs/工厂模式.md:
--------------------------------------------------------------------------------
1 | ## 二、工厂模式(factory pattern)
2 |
3 | 1、简单工厂模式(simple factory pattern)
4 |
5 | 2、工厂方法模式(factory method pattern)
6 |
7 | 3、抽象工厂模式(abstract factory pattern)
8 |
9 |
10 |
11 | ----
12 |
13 | ### 1、简单工厂模式
14 |
15 | 简单工厂模式是最基础的设计模式之一,应用也十分频繁。
16 |
17 | 该模式具体框架如下:定义一个简单工厂类,它可以根据参数的不同返回不同类的实例对象,被创建的实例通常都具有共同的父类。
18 |
19 | 在简单工厂模式中,大体上有3个角色:**工厂类、抽象产品类、具体产品类**。即,工厂类可以看作一个总处理器,负责接受用户的参数,然后“制造不同的产品”,即创建各具体产品类的对象、调用函数等。各个具体产品类则是由一个抽象产品类派生出来的,这体现了OOP的“针对接口编程”等原则。
20 |
21 | 下面,我们可以通过一个例子来了解一下:就以“公司职员评价系统”为例。用户希望设计一款对不同职员的评价系统,为简单起见,设有Teacher、Professor两种职员,对职员的评价有好评与差评两种。那么,若以简单工厂模式设计代码,框架大致如下:
22 |
23 | 首先设计一个抽象职员类AbstractEmployee、由此派生出Teacher、Professor两个职员类,这两个类中有评价相关函数;
24 |
25 | 随后设计一个工厂类EnvaluationFactory,负责接收用户输入的数据,进而生成Teacher或Professor类对象以进行评价。
26 |
27 | ```c++
28 | #include
29 | using namespace std;
30 |
31 | class AbstractEmployee
32 | {
33 | public:
34 | AbstractEmployee() {
35 |
36 | }
37 | virtual ~AbstractEmployee() {};
38 | virtual void nice_work() = 0;
39 | virtual void bad_work() = 0;
40 | };
41 |
42 | class Teacher :public AbstractEmployee
43 | {
44 | public:
45 | Teacher() {
46 |
47 | }
48 | ~Teacher() {};
49 | void nice_work()
50 | {
51 | cout << "The teacher works well." << endl;
52 | }
53 | void bad_work()
54 | {
55 | cout << "The teacher works badly." << endl;
56 | }
57 | };
58 |
59 | class Professor :public AbstractEmployee
60 | {
61 | public:
62 | Professor() {
63 |
64 | }
65 | ~Professor() {};
66 | void nice_work()
67 | {
68 | cout << "The professor works well." << endl;
69 | }
70 | void bad_work()
71 | {
72 | cout << "The professor works badly." << endl;
73 | }
74 | };
75 |
76 | class EnvaluationFactory
77 | {
78 | public:
79 | std::shared_ptr getEmployeeEvaluation(string employeename,string evaluation)
80 | {//shared_ptr为智能指针
81 | std::shared_ptr emp;
82 | if (employeename == "Teacher") {
83 | emp = std::shared_ptr(new Teacher());
84 | }
85 | else if (employeename == "Professor") {
86 | emp = std::shared_ptr(new Professor());
87 | }
88 | if (evaluation == "Good")
89 | emp->nice_work();
90 | else if (evaluation == "Bad")
91 | emp->bad_work();
92 | return emp;
93 | }
94 | };
95 |
96 | int main()
97 | {
98 | shared_ptr efac = std::make_shared();
99 | shared_ptr emp = efac->getEmployeeEvaluation("Teacher","Good");
100 | emp = efac->getEmployeeEvaluation("Professor", "Bad");
101 | }
102 |
103 | /*运行结果如下:
104 | The teacher works well.
105 | The professor works badly.
106 | */
107 |
108 |
109 | ```
110 |
111 | 简单工厂模式的优点如下:
112 |
113 | 工厂类提供创建具体产品的方法,并包含一定判断逻辑,客户不必参与产品的创建过程,只需要知道对应产品的简单参数即可。
114 |
115 | 但简单工厂模式也存在明显的不足:
116 |
117 | 如前文例子,若还想再添加一类职员,我们必须再从抽象产品类派生出一个职员类,同时在工厂类的方法中增加条件分支。但是,在工厂类中的改动将违背OOP三大原则中的开闭原则(对扩展开放,对修改关闭),即在扩展功能时修改了既有的代码。另一方面,简单工厂模式所有的判断逻辑都在工厂类中实现,一旦工厂类设计故障,则整个系统都受之影响。
118 |
119 | 为此,简单工厂模式被进行了改进,形成了“工厂方法模式”。
120 |
121 | ----
122 |
123 | ### 2、工厂方法模式
124 |
125 | 简单工厂模式中,每新增一个具体产品,就需要修改工厂类内部的判断逻辑。为了不修改工厂类,遵循开闭原则,工厂方法模式中不再使用**一个**工厂类统一创建所有的具体产品,而是针对不同的产品设计了**不同**的工厂,每一个工厂只生产特定的产品。
126 |
127 | 在工厂方法模式中,大体上有4个角色:**抽象工厂类、具体工厂类、抽象产品类、具体产品类**。即,在简单工厂模式的基础上,把一个工厂变成了从一个抽象类派生出的多个工厂,各司其职,负责相应的产品“生产”。
128 |
129 | 回到上文中的职员评价系统例子,我们可以在前文的代码基础上稍加改动,部分代码如下:
130 |
131 | (只显示了改动后的部分,完整代码可以在附件中查阅)
132 |
133 | ```c++
134 | class AbstractFactory
135 | {
136 | public:
137 | virtual shared_ptr getEmployeeEvaluation(string evaluation) = 0;
138 | virtual ~AbstractFactory() {}
139 | };
140 |
141 | class TeacherEnvaluationFactory: public AbstractFactory
142 | {
143 | public:
144 | shared_ptr getEmployeeEvaluation(string evaluation)
145 | {
146 | shared_ptr emp;
147 | emp = shared_ptr(new Teacher());
148 | if (evaluation == "Good")
149 | emp->nice_work();
150 | else if (evaluation == "Bad")
151 | emp->bad_work();
152 | return emp;
153 | }
154 | };
155 |
156 | class ProfessorEnvaluationFactory : public AbstractFactory
157 | {
158 | public:
159 | shared_ptr getEmployeeEvaluation(string evaluation)
160 | {
161 | shared_ptr emp;
162 | emp = shared_ptr(new Professor());
163 | if (evaluation == "Good")
164 | emp->nice_work();
165 | else if (evaluation == "Bad")
166 | emp->bad_work();
167 | return emp;
168 | }
169 | };
170 |
171 | int main()
172 | {
173 | shared_ptr efac = make_shared();
174 | shared_ptr emp = efac->getEmployeeEvaluation("Good");
175 | efac = make_shared();
176 | emp = efac->getEmployeeEvaluation("Good");
177 | }
178 |
179 | /*运行结果如下:
180 | The teacher works well.
181 | The professor works well.
182 | */
183 |
184 | ```
185 |
186 | 如果用户希望增加一类职员,只需要增加一个相应的工厂类而不用改动原有代码。由此可以看到,相较简单工厂模式,工厂方法模式更加符合开闭原则。工厂方法模式是使用频率最高的设计模式之一,也是很多开源框架和API类库的核心模式。
187 |
188 | 但这种模式也有着相应的不足:添加新产品时需要同时添加新的产品工厂,系统中类的数量成对增加,增加了系统的复杂度,更多的类需要编译和运行,增加了系统的额外开销;工厂和产品都引入了抽象层,增加了系统的抽象层次和理解难度。
189 |
190 | ----
191 |
192 | ### 3、抽象工厂模式
193 |
194 | 抽象工厂模式可以看作对工厂方法模式进一步的拓展,即**一个工厂不只是生产一种产品,而是生产一类产品**。其原理与前文基本相同,只是增加了抽象产品类的个数(即增加了产品种类数)等,感兴趣的同学们可以尝试探究。
195 |
196 | ----
197 |
198 | ## 参考资料:
199 |
200 | [DesignPattern/01.SimpleFactory.md at master · FengJungle/DesignPattern · GitHub](https://github.com/FengJungle/DesignPattern/blob/master/01.SimpleFactory/01.SimpleFactory.md)
201 |
202 | [Factory Patterns - Simple Factory Pattern - CodeProject](https://www.codeproject.com/Articles/1131770/Factory-Patterns-Simple-Factory-Pattern)
203 |
204 |
--------------------------------------------------------------------------------
/C++/第八讲/设计模式原则简介/README.md:
--------------------------------------------------------------------------------
1 | # 第二讲 设计模式简介
2 |
3 | ## 内容介绍
4 |
5 | 1. **设计模式为什么是必要的?**
6 |
7 | 2. **OOP编程中的三大基本原则**
8 |
9 | 1. 针对接口编程,而非针对实现编程
10 |
11 | 2. 开闭原则
12 |
13 | 3. 单一职责原则
14 |
15 | 3. **扩展知识:里氏替换原则 & 迪米特法则**
16 |
17 |
18 |
19 | ## 文件清单
20 |
21 | docs:内含第二讲教程的核心文字内容和重点代码块;
22 |
23 | codes:内含关于开闭原则和单一职责原则的示例程序源代码;
24 |
25 |
26 |
27 | ## 参考文献
28 |
29 | 1. 清华大学计算机科学与技术系姚海龙老师《面向对象的程序设计》课程文件Lec10&11;
30 | 2. CSDN博文:开闭原则 https://blog.csdn.net/niu2212035673/article/details/77803672?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522164475222816780366530694%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=164475222816780366530694&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_ecpm_v1~rank_v31_ecpm-5-77803672.first_rank_v2_pc_rank_v29&utm_term=开闭原则+代码+C%2B%2B&spm=1018.2226.3001.4187
31 | 3. CSDN博文:单一职责原则(C++) https://blog.csdn.net/niu2212035673/article/details/77752316?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522164475951016780255268472%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=164475951016780255268472&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_ecpm_v1~rank_v31_ecpm-1-77752316.first_rank_v2_pc_rank_v29&utm_term=单一职责原则+C%2B%2B&spm=1018.2226.3001.4187110521613.first_rank_v2_pc_rank_v29&spm=1018.2226.3001.4187
32 | 4. 知乎专栏文章:里氏替换原则 https://zhuanlan.zhihu.com/p/280765580
33 | 5. CSDN博文:C++设计原则——迪米特法则 https://blog.csdn.net/qq_42956179/article/details/115372247
--------------------------------------------------------------------------------
/C++/第八讲/设计模式原则简介/codes/OCP_1.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 |
4 | //计算器类
5 | class Calculator
6 | {
7 | public:
8 | Calculator()
9 | {
10 |
11 | }
12 | ~Calculator() {};
13 |
14 | double getompute(char c)
15 | {
16 | switch (c)
17 | {
18 | case '+':
19 | return mA + mB;
20 | break;
21 | case '-':
22 | return mA - mB;
23 | break;
24 | default:
25 | break;
26 | }
27 | }
28 | public:
29 | double mA;
30 | double mB;
31 | };
32 |
33 | //客户端
34 | int main(void)
35 | {
36 | Calculator calculator;
37 |
38 | while (true)
39 | {
40 | std::cout << "请输入数字:";
41 | std::cin >> calculator.mA;
42 | std::cout << "请输入数字:";
43 | std::cin >> calculator.mB;
44 | std::cout << "进行计算:";
45 | char c = '0';
46 | std::cin >> c;
47 |
48 | std::cout << "计算结果:" << calculator.getompute(c) << std::endl;
49 | }
50 |
51 | return 0;
52 | }
--------------------------------------------------------------------------------
/C++/第八讲/设计模式原则简介/codes/OCP_2.cpp:
--------------------------------------------------------------------------------
1 | //计算器类
2 | class Calculator
3 | {
4 | public:
5 | Calculator()
6 | {
7 |
8 | }
9 | ~Calculator() {};
10 |
11 | double getompute(char c)
12 | {
13 | switch (c)
14 | {
15 | case '+':
16 | return mA + mB;
17 | break;
18 | case '-':
19 | return mA - mB;
20 | break;
21 | case '*':
22 | return mA * mB;
23 | break;
24 | case '/':
25 | return mA / mB;
26 | break;
27 | default:
28 | break;
29 | }
30 | }
31 | public:
32 | double mA;
33 | double mB;
34 | };
--------------------------------------------------------------------------------
/C++/第八讲/设计模式原则简介/codes/OCP_3.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 |
4 | //计算器类
5 | class Calculator
6 | {
7 | public:
8 | Calculator()
9 | {
10 |
11 | }
12 | ~Calculator() {};
13 |
14 | //抽象接口类,子类实现
15 | virtual double getompute()
16 | {
17 | return 0;
18 | }
19 | public:
20 | double mA;
21 | double mB;
22 | };
23 |
24 | //除
25 | class Division : public Calculator
26 | {
27 | public:
28 | virtual double getompute()
29 | {
30 | return mA / mB;
31 | }
32 | };
33 | //乘
34 | class Multiplication : public Calculator
35 | {
36 | public:
37 | virtual double getompute()
38 | {
39 | return mA * mB;
40 | }
41 | };
42 | //减
43 | class Subtraction : public Calculator
44 | {
45 | public:
46 | virtual double getompute()
47 | {
48 | return mA - mB;
49 | }
50 | };
51 | //加
52 | class Addition : public Calculator
53 | {
54 | public:
55 | virtual double getompute()
56 | {
57 | return mA + mB;
58 | }
59 | };
60 |
61 | //工厂,根据不同的计算方式生产类
62 | Calculator* CreateCalculator(char c)
63 | {
64 | switch (c)
65 | {
66 | case '+':
67 | return new Addition;
68 | break;
69 | case '-':
70 | return new Subtraction;
71 | break;
72 | case '*':
73 | return new Multiplication;
74 | break;
75 | case '/':
76 | return new Division;
77 | break;
78 |
79 | default:
80 | return NULL;
81 | break;
82 | }
83 | }
84 |
85 | //客户端
86 | int main(void)
87 | {
88 | Calculator *calculator = NULL;
89 |
90 | calculator = CreateCalculator('-');
91 |
92 | calculator->mA = 10;
93 | calculator->mB = 5;
94 | std::cout << "计算结果:" << calculator->getompute() << std::endl;
95 |
96 | while (true) {};
97 | return 0;
98 | }
99 |
100 |
--------------------------------------------------------------------------------
/C++/第八讲/设计模式原则简介/codes/SRP.cpp:
--------------------------------------------------------------------------------
1 | //方块形状类
2 | class Shape
3 | {
4 | public:
5 | Shape();
6 | ~Shape();
7 |
8 | virtual void setShape();
9 | private:
10 | };
11 |
12 | //"一"形
13 | class Shape1 : public Shape
14 | {
15 | public:
16 | Shape1();
17 | ~Shape1();
18 |
19 | virtual void setShape();
20 | private:
21 | };
22 |
23 | //"十"形
24 | class Shape2 : public Shape
25 | {
26 | public:
27 | Shape2();
28 | ~Shape2();
29 |
30 | virtual void setShape();
31 | private:
32 | };
33 |
34 | //"L"形
35 | class Shape3 : public Shape
36 | {
37 | public:
38 | Shape3();
39 | ~Shape3();
40 |
41 | virtual void setShape();
42 | private:
43 | };
44 |
45 | //"其他"形
46 | class Shape4: public Shape
47 | {
48 | public:
49 | Shape4();
50 | ~Shape4();
51 |
52 | virtual void setShape();
53 | private:
54 | };
55 |
56 | //方块位置类
57 | class Place
58 | {
59 | public:
60 | Place();
61 | ~Place();
62 |
63 | void setPlace();
64 | private:
65 | };
66 |
67 | //游戏界面类
68 | class GameUI
69 | {
70 | public:
71 | GameUI();
72 | ~GameUI();
73 |
74 | void showViewCcon(); //游戏方块显示
75 | private:
76 | Shape *mShape; //形状
77 | Place *mPlace; //位置
78 | int mStartButton; //开始按钮
79 | int mEndButton; //结束按钮
80 | };
81 |
82 |
--------------------------------------------------------------------------------
/C++/第八讲/设计模式原则简介/docs/design pattern.md:
--------------------------------------------------------------------------------
1 | # 第二讲:设计模式简介
2 |
3 | ## 设计模式为什么是必要的?
4 |
5 | 在面向对象的程序设计中,一段好的程序必须具有以下三个特点:可读性、高效性和可扩展性。其中,我们在前面所介绍关于代码风格的教程中,解决的就是有关可读性的相关问题;而在这一讲中,我们所介绍的设计模式则可以帮助提高程序的可扩展性。这在编写大型程序或多人协作编程中是至关重要的,有助于提高代码编写的效率。
6 |
7 |
8 |
9 | ## OOP编程中的三大基本原则
10 |
11 | ### 原则一:针对接口编程,而非针对实现编程
12 |
13 | 要想弄明白这一原则的内容,我们首先要明白什么是接口类和实现类。
14 |
15 | 接口这一概念是由Java继承而来,可以与C++中的抽象类类比。而接口类则是指一个抽象类仅通过纯虚函数向外提供接口的类,通过重载这些虚函数,我们就可以得到实现具体功能的实现类。这一原则中,强调的就是在OOP编程中,我们应当尽量使用接口类而非实现类,这样可以保证未来程序的可扩展性。
16 |
17 | 两种编程方式的区别可以用以下的示例程序清楚地展现:
18 |
19 | ```C++
20 | // 针对实现编程
21 | Dog d = new Dog();
22 | d.bark();
23 |
24 | //针对接口编程
25 | Animal animal = new Dog();
26 | animal.makesound();
27 | ```
28 |
29 | 我们想实现创建狗的对象并实现狗吠的功能。当采用针对实现的编程原则时,我们必须新建一个具体的Dog类,然后调用bark方法;然而,如果采用针对接口的编程原则,我们就可以利用Dog类的构造函数生成一个Animal对象。由于多态性的保证,当Animal对象调用makesound方法时会自动调用Dog类的方法,从而实现了对bark()函数的调用。
30 |
31 | 两种方法殊途同归,然而其效用却并不相同。采用前者,虽然代码含义清晰明确,但对于每类动物,即使他们相互之间有很多共同的元素,也必须在他们相应的类中分别加以实现,完全不能代码复用,更重要的是不能反应出这些Animal的共同特征。而采用后者,我们就可以更好地实现代码的重用,并且有助于隔离变化,程序的扩展性和灵活性都要略胜一筹;
32 |
33 |
34 |
35 | ### 原则二:开闭原则
36 |
37 | 这一原则理解起来就较为简单了。所谓开闭原则,指的是代码编写过程中,应当对扩展开放,对修改封闭。这样一来,当需求改变时,我们可以进行代码复用,而不用对原始代码进行改动。
38 |
39 | 使用这一编程原则可以至少在两个层面上提升代码质量。首先,通过扩展已有代码,可以提供新的行为和功能,以满足对软件的新需求,使得变化中的程序有一定的适应性和灵活性。此外,开闭原则保证了已有的软件模块,特别是最重要的抽象模块不再修改;这就使变化中的软件系统有一定的稳定性和延续性。
40 |
41 | 为了更好地理解这一原则,我们可以用下面的程序作为例子(可参见OCP.cpp源代码)。假设我们想在Calculator类中完成加减法的运算,我们可能会这么写:
42 |
43 | ```c++
44 | class Calculator
45 | {
46 | public:
47 | Calculator()
48 | {
49 |
50 | }
51 | ~Calculator() {};
52 |
53 | double getompute(char c)
54 | {
55 | switch (c)
56 | {
57 | case '+':
58 | return mA + mB;
59 | break;
60 | case '-':
61 | return mA - mB;
62 | break;
63 | default:
64 | break;
65 | }
66 | }
67 | public:
68 | double mA;
69 | double mB;
70 | };
71 | ```
72 |
73 | 然而,现在客户提出了新的需求,计算器需要完成乘除法等新的运算,那么一个尚未经过专业OOP培训的程序员可能会对getompute() 函数进行如下的改动:
74 |
75 | ```cpp
76 | double getompute(char c)
77 | {
78 | switch (c)
79 | {
80 | case '+':
81 | return mA + mB;
82 | break;
83 | case '-':
84 | return mA - mB;
85 | break;
86 | case '*':
87 | return mA * mB;
88 | break;
89 | case '/':
90 | return mA / mB;
91 | break;
92 | default:
93 | break;
94 | }
95 | }
96 | ```
97 |
98 | 这样一来,虽然功能得到了满足,但是由于修改了类的核心代码,违背了开放封闭原则:代码没有对修改封闭。这样一来,当后期要求的功能越来越多时,程序员都需要对类的getompute() 函数进行直接修改,这极大地增加了代码出错的概率。一个符合开放封闭原则的写法则是在Calculator类中定义抽象接口类,并通过子类继承的方式增加新的功能。
99 |
100 | ```C p
101 | class Calculator
102 | {
103 | public:
104 | Calculator()
105 | {
106 |
107 | }
108 | ~Calculator() {};
109 |
110 | //抽象接口类,子类实现
111 | virtual double getompute()
112 | {
113 | return 0;
114 | }
115 | public:
116 | double mA;
117 | double mB;
118 | };
119 |
120 | //除
121 | class Division : public Calculator
122 | {
123 | public:
124 | virtual double getompute()
125 | {
126 | return mA / mB;
127 | }
128 | };
129 | //乘
130 | class Multiplication : public Calculator
131 | {
132 | public:
133 | virtual double getompute()
134 | {
135 | return mA * mB;
136 | }
137 | };
138 | ```
139 |
140 | 在这种编程规范下,功能的修改既不需要修改Calculator类的任何代码,实现新功能时又仅需要进行新的类的独立编写并继承在Calculator类下,这将极大方便了程序应对需求变化的情形,也将为多人合作下的代码编写提供了更多的方便。
141 |
142 |
143 |
144 | ### 原则三:单一职责原则
145 |
146 | 为了更好地理解什么是单一职责原则,我们首先来考虑下面这个例子:假设在工厂中,一款产品从无到有需要经过10台机器。作为工厂的管理者,我们是雇佣10名员工,让每个人拿着原材料从第一台操作到第十台比较快;还是让每个机器有一个单独的人专门负责会比较快?我想,答案是显而易见的。
147 |
148 | 工厂通过流水线式的分工,让每个人仅负责一道工序,效率有了大幅度的提高。当我们类比面向对象设计时,这种原则则被叫做单一职责原则。顾名思义,所谓单一职责原则就是一个类应该只有一个引起其变化的原因。这是一个最简单,最容易理解,但是却最不容易做到的一个设计原则。说的简单一点,就是怎样设计类以及类的方法界定的问题。
149 |
150 | 例如,我们考虑一个公司的人事管理系统。如果我们将工程师、销售人员、销售经理等等都放在职员类里考虑,其结果将会非常混乱。在这个假设下,职员类里的每个方法(例如:发工资、招投标等)都需要用if else 判断是哪种职员类型,不论从类结构或是代码实现层面上来说都会十分臃肿。更严重的是,任何职员类型,不论哪一种发生需求变化,都会改变职员类,这是我们不愿意看到的。
151 |
152 | 此外,我们还可以通过一个具体的代码示例来理解。假设我们要完成俄罗斯方块游戏的编写,里面会涉及到各种各样方块的位置和形状。如果我们不采用单一职责原则而将所有的功能都集成在一个类中,那么这个类就会因为耦合度太高而造成可维护性降低。相反,如果我们对不同类型的方块分开编写,降低了类与类之间的耦合性,那么在编程层面就可以提升效率,具体情形可以参考如下代码:
153 |
154 | ```cpp
155 | //方块形状类
156 | class Shape
157 | {
158 | public:
159 | Shape();
160 | ~Shape();
161 |
162 | virtual void setShape();
163 | private:
164 | };
165 |
166 | //"一"形
167 | class Shape1 : public Shape
168 | {
169 | public:
170 | Shape1();
171 | ~Shape1();
172 |
173 | virtual void setShape();
174 | private:
175 | };
176 |
177 | //"十"形
178 | class Shape2 : public Shape
179 | {
180 | public:
181 | Shape2();
182 | ~Shape2();
183 |
184 | virtual void setShape();
185 | private:
186 | };
187 |
188 | //"L"形
189 | class Shape3 : public Shape
190 | {
191 | public:
192 | Shape3();
193 | ~Shape3();
194 |
195 | virtual void setShape();
196 | private:
197 | };
198 |
199 | //"其他"形
200 | class Shape4: public Shape
201 | {
202 | public:
203 | Shape4();
204 | ~Shape4();
205 |
206 | virtual void setShape();
207 | private:
208 | };
209 |
210 | //方块位置类
211 | class Place
212 | {
213 | public:
214 | Place();
215 | ~Place();
216 |
217 | void setPlace();
218 | private:
219 | };
220 |
221 | //游戏界面类
222 | class GameUI
223 | {
224 | public:
225 | GameUI();
226 | ~GameUI();
227 |
228 | void showViewCcon(); //游戏方块显示
229 | private:
230 | Shape *mShape; //形状
231 | Place *mPlace; //位置
232 | int mStartButton; //开始按钮
233 | int mEndButton; //结束按钮
234 | };
235 |
236 |
237 | ```
238 |
239 | 因此,我们可以总结出,所谓的“单一职责原则”有两层含义:单一职责有两个含义:**一个是避免相同的职责分散到不同的类中,另一个是避免一个类承担太多的职责。**遵循这样的规则进行类和类的方法界定,将会使得类的结构更为清晰,便于在需求改变时对类进行合理的修改和扩展。可以说,单一职责原则既减少了类与类之间的耦合,又提高了类的可复用性,极大的提升了面向对象编程的效率。
240 |
241 |
242 |
243 | ## 扩展知识
244 |
245 | ### 里氏替换原则
246 |
247 | 里氏替换原则也是OOP的基本原则之一,所强调的是在任何基类可以出现的地方,派生类一定可以出现。因此我们可以说,它是继承复用的基石,只有当派生类可以替换掉基类,软件的功能不受到影响时,基类才能真正被复用,而派生类也能够在基类的基础上增加新的行为。在这一意义上,我们可以说里氏替换原则是对“开闭”原则的补充。而要想规范地遵从里氏替换原则,我们需要做到以下五点:
248 |
249 | 1. 子类必须完全实现父类的抽象方法,但不能覆盖父类的非抽象方法
250 |
251 | 2. 子类可以实现自己特有的方法
252 |
253 | 3. 当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
254 |
255 | 4. 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
256 |
257 | 5. 子类的实例可以替代任何父类的实例,但反之不成立
258 |
259 | 我们可以用下面的代码示例进行说明:
260 |
261 | ```cpp
262 | #include
263 | //鸟
264 | class Bird{
265 | public:
266 | Bird(){}
267 | virtual ~Bird(){}
268 | virtual void Fly() {
269 | std::cout << "I am a bird and I am flying" << std::endl;
270 | }
271 | };
272 | //燕子
273 | class Swallow : public Bird{
274 | public:
275 | Swallow(){}
276 | ~Swallow(){}
277 | void Fly() override {
278 | std::cout << "I am a Swallow and I am flying" << std::endl;
279 | }
280 | };
281 | //大雁
282 | class WildGoose : public Bird{
283 | public:
284 | WildGoose(){}
285 | ~WildGoose(){}
286 | void Fly() override {
287 | std::cout << "I am a Wild Goose and I am flying" << std::endl;
288 | }
289 | };
290 | //模拟鸟的飞行
291 | void Fly(Bird& b){
292 | b.Fly();
293 | }
294 |
295 | int main(int argc,char * argv[]){
296 | WildGoose goose;
297 | Swallow s;
298 | Fly(s);
299 | Fly(goose);
300 | }
301 | ```
302 |
303 | 在这一段示例代码中,我们看到:定义的全局函数的参数为Bird类的对象,这使得不论我们传入的是燕子还是大雁,程序都能顺利进行。然而,如果将Fly函数改为下面的定义:
304 |
305 | ```cpp
306 | void Fly(WildGoose& wg){
307 | wg.Fly();
308 | }
309 | ```
310 |
311 | 那么我们就无法用Fly函数模拟燕子的飞行。根据里氏替换原则,在基类出现的地方都可以用派生类代替,而反之则不行。
312 |
313 |
314 |
315 | ### 迪米特法则
316 |
317 | 与之前介绍的各种原则不同,迪米特法则关注的是的是类之间的耦合性大小。事实上,这一法则并非是程序设计领域中的专属,美国人就在航天系统的设计中同样采用了这一法则。
318 |
319 | 迪米特法则又叫作最少知识原则,一个类对于其他类知道的越少越好,就是说一个对象应当对其他对象有尽可能少的了解:只和朋友通信,不和陌生人说话。在编程层面,其具有以下几个特点:
320 |
321 | 1. **如果两个类不直接通信,那么这两个类就不应该发生直接的相互作用**。
322 |
323 | 2. 在类的结构设计上,每一个类都应该尽量降低成员的访问权限
324 |
325 | 3. 该法则在适配器模式、解释模式等中有强烈的体现
326 |
327 | 4. 强调类之间的松耦合,类之间的耦合越弱,越有利于复用
328 |
329 |
330 |
--------------------------------------------------------------------------------
/C++/第六讲/函数对象、lambda与参数绑定/README.md:
--------------------------------------------------------------------------------
1 | # 第六讲
2 |
3 | ## 简介
4 |
5 | - 主题:函数对象、lambda表达式与参数绑定
6 | - 你将学到什么:
7 | - 函数对象的定义、优点、使用方法
8 | - lambda表达式的相关知识
9 | - 参数绑定、bind函数的知识
10 |
11 | ------
12 |
13 | ## 文件清单
14 |
15 | - `docs/`
16 | - 第六讲:函数对象、lambda表达式与参数绑定.md【本讲教程】
17 | - `imgs/`
18 | - image1.png
19 | - `codes/`
20 | - 1FunctionObject.cpp
21 | - 2lambda.cpp
22 | - 3bind.cpp
23 |
24 | ------
25 |
26 | ## 参考资料
27 |
28 | - 清华大学计算机系姚海龙老师程序设计基础课件
29 | - 《C++ Primer》(第五版)
30 | - [learn Cpp](https://www.learncpp.com/)
31 | - [Microsoft C++ docs](https://docs.microsoft.com/en-us/cpp/cpp/?view=msvc-170)
32 | - [GeeksforGeeks C++ tutorials](https://www.geeksforgeeks.org/c-plus-plus/)
--------------------------------------------------------------------------------
/C++/第六讲/函数对象、lambda与参数绑定/codes/1FunctionObject.cpp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ksc999/THUEEXP-SAST-Tutor/d01ad77a614a8ff9eefa3c4a64fc7e8bf7b47f08/C++/第六讲/函数对象、lambda与参数绑定/codes/1FunctionObject.cpp
--------------------------------------------------------------------------------
/C++/第六讲/函数对象、lambda与参数绑定/codes/2lambda.cpp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ksc999/THUEEXP-SAST-Tutor/d01ad77a614a8ff9eefa3c4a64fc7e8bf7b47f08/C++/第六讲/函数对象、lambda与参数绑定/codes/2lambda.cpp
--------------------------------------------------------------------------------
/C++/第六讲/函数对象、lambda与参数绑定/codes/3bind.cpp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ksc999/THUEEXP-SAST-Tutor/d01ad77a614a8ff9eefa3c4a64fc7e8bf7b47f08/C++/第六讲/函数对象、lambda与参数绑定/codes/3bind.cpp
--------------------------------------------------------------------------------
/C++/第六讲/函数对象、lambda与参数绑定/docs/第六讲:函数对象、lambda表达式与参数绑定.md:
--------------------------------------------------------------------------------
1 | ## 第六讲:函数对象、lambda表达式与参数绑定
2 |
3 | ### 一、函数对象
4 |
5 | #### 1、函数对象的定义与特点
6 |
7 | **函数对象**是可以实现操作符`()`的任何类型,简单来说,就是指一个重载了括号操作符`()`的类的对象。
8 |
9 | 这种对象使用起来如同使用一个函数。需要注意的是,重载`()`的函数,必须为该类的成员函数。
10 |
11 | 与直接函数调用相比,函数对象具有两个主要优点:
12 |
13 | - 函数对象可以包含状态;
14 | - 函数对象是一种类型,因此可以用作模板参数。
15 |
16 | #### 2、函数对象简单使用示例
17 |
18 | ###### 例1.1:生成并使用一个简单的函数对象
19 |
20 | ~~~c++
21 | #include
22 | using namespace std;
23 |
24 | class Functor {
25 | public:
26 | int operator() (int a, int b) { //重载()操作符,是成员函数
27 | return a < b;
28 | }
29 | };
30 |
31 | int main() {
32 | Functor f; //声明一个函数对象
33 | int a = 5;
34 | int b = 7;
35 | int ans = f(a,b); //此处f类似一个函数,但实际是在调用重载的()操作符
36 | cout << ans; //ans=1
37 | }
38 | ~~~
39 |
40 | ###### 例1.2:函数对象可以包含状态,即存储和传递数据(优点一)
41 |
42 | ~~~c++
43 | #include
44 | using namespace std;
45 |
46 | class F {
47 | public:
48 | F(int val) : data{val} {}
49 | bool operator() (int x) { return (x > data); }
50 | private:
51 | int data;
52 | };
53 |
54 | void Disp(int a[], int len, F op) {
55 | for (int i=0; i,
94 | class Allocator=allocator>
95 | class set
96 | ~~~
97 |
98 | 第二个模板参数是函数对象 。比如`less`中的重载函数会比较两个`int`类型的参数,如果第一个参数小于第二个,则此函数对象返回true。通过这样一个函数对象便可以实现对容器内的元素排序。
99 |
100 | ###### 函数对象用于算法中
101 |
102 | std::sort 算法默认使用元素类型的 `<` 运算符,但可能我们希望的顺序与<所定义的顺序不同,或者我们要排序的是自己定义的元素类型,此时我们需要向sort函数中传递第三个参数,此参数称为**谓词**。
103 |
104 | 注:谓词是一个可调用表达式,其返回结果是一个能用作条件的值。其分为**一元谓词**(接受单一参数)、**二元谓词**(两个参数)。接受谓词参数的算法对输入序列的元素调用谓词。
105 |
106 | 例1.3:在下面的程序中,我们给sort算法传入不同的(二元)谓词,实现特定的排序
107 |
108 | ~~~c++
109 | #include
110 | #include
111 | #include
112 | using namespace std;
113 |
114 | bool myfunction (int i,int j) { return (i>j); } //自定义比较函数
115 |
116 | class myclass { //重载()的类
117 | public:
118 | bool operator() (int i,int j) { return (i>j);}
119 | } myobject; //自定义一个函数对象myobject
120 |
121 | int main () {
122 | int myints[] = {32,71,12,45,26,80,53,33};
123 | // 将该数组的8个元素装入容器
124 | vector myvector (myints, myints+8);
125 | vector::iterator it; //迭代器,用来访问元素
126 | // 使用默认的比较函数(operator <)排列前四个元素:
127 | //(12 32 45 71) 26 80 53 33
128 | sort (myvector.begin(), myvector.begin()+4);
129 |
130 | // 使用自定义的比较函数排列后四个元素
131 | // 12 32 45 71 (80 53 33 26)
132 | sort (myvector.begin()+4, myvector.end(), myfunction);
133 |
134 | // 使用三种函数对象排序(效果一样,同operator >)
135 | //(80 71 53 45 33 32 26 12)
136 | sort (myvector.begin(), myvector.end(), myobject);
137 | sort (myvector.begin(), myvector.end(), myclass()); //myclass是类名,后面要加()
138 | sort (myvector.begin(), myvector.end(), greater()); //greater是标准库里的类型
139 |
140 | cout << "myvector contains:";
141 | //使用迭代器遍历容器内元素输出
142 | for (it=myvector.begin(); it!=myvector.end(); ++it)
143 | cout << " " << *it;
144 |
145 | return 0;
146 | }
147 | //结果:myvector contains: 80 71 53 45 33 32 26 12
148 | ~~~
149 |
150 | ### 二、lambda表达式
151 |
152 | #### 1、lambda表达式的定义
153 |
154 | - **lambda表达式**是一个能够**捕获**作用域中**变量**的未命名**函数对象**。
155 |
156 | - 一个lambda表达式表示一个可调用的代码单元,我们也可以理解为一个未命名的内联函数。
157 |
158 | 注:对于一个对象或者表达式,如果它可以使用`()`运算符,我们称之为是可调用的。即如果e是一个可调用的表达式,我们可以编写代码 `e(args);`其中`args`是一个逗号分隔的一个或多个参数列表)
159 |
160 | 函数、函数指针、重载了`()`运算符的类、lambda表达式,这四类都是可调用对象。
161 |
162 | - lambda表达式具有一个`捕获列表`、一个`参数列表`、一个`返回类型`和一个`函数体`,可以被定义在一个函数的内部。
163 |
164 | 一个lambda表达式有如下形式:
165 |
166 | ~~~c++
167 | [capture list] (paramter list) -> return type { funciton body }
168 | ~~~
169 |
170 | 其中,参数列表与返回类型可以忽略,捕获列表与函数体必须有(捕获内容可为空)。
171 |
172 | 例如下面这个lambda表达式(赋给f):
173 |
174 | ~~~c++
175 | auto f=[] { return 26; };
176 | cout << f() ; //打印26
177 | ~~~
178 |
179 | #### 2、传递参数
180 |
181 | 与一个普通函数类似,调用一个lambda时给定的实参被用来初始化lambda的形参。但是与普通函数不同的是,lambda表达式**不能有默认参数**,调用的实参数目必须与形参数目相同。
182 |
183 | 例如,我们可以这样编写一个lambda表达式用于两个字符串的比较:
184 |
185 | ~~~c++
186 | auto f=[] (const string &a, const string &b)
187 | { return a.size() < b.size();};
188 | ~~~
189 |
190 | lambda表达式是一个函数对象,上面的`f` 类似于下面这个类的对象:
191 |
192 | ~~~c++
193 | class ShorterString{
194 | public:
195 | bool operator() (const string& a,const string& b) const{
196 | return a.size() < b.size();
197 | }
198 | };
199 | ~~~
200 |
201 | 我们可将其运用在排序算法中(设words是一个string容器)(例2.1)
202 |
203 | ~~~c++
204 | sort(words.begin(),words.end(),
205 | [](const string &a, const string &b)
206 | { return a.size() < b.size();});
207 | //这相当于
208 | sort(words.begin(),words.end(),f);
209 | //也相当于
210 | sort(words.begin(),words.end(),ShorterString());
211 | ~~~
212 |
213 | #### 3、使用捕获列表
214 |
215 | - 捕获内容:**局部非static变量**
216 |
217 | - 捕获意义:一个lambda只有在其捕获列表中捕获一个它所在函数中的局部变量,才能在函数体中使用该变量。
218 |
219 | 注:局部static变量与lambda表达式所在函数之外声明的变量可以直接使用,无需捕获。
220 |
221 | ~~~c++
222 | [sz](const string &a) {return a.size()>=sz;}; //ok!(设sz是函数内局部变量)
223 | [](const string &a) {return a.size()>=sz;}; //error! (没有捕获sz)
224 | ~~~
225 |
226 | #### 4、捕获与返回
227 |
228 | 当我们定义一个lambda表达式时,编译器自动生成一个与lambda表达式对应的新的(未命名)类类型。当向一个函数传递一个lambda时,就同时定义了一个新类型与该类型的一个对象。类似的,当用auto定义一个用lambda初始化的变量时(见上上个例子),定义了一个从lambda生成的类型对象。
229 |
230 | 从lambda生成的类包含了lambda所捕获的变量作为其数据成员,该数据成员在lambda对象创建时被初始化。
231 |
232 | 类似传递参数,lambda的捕获分为值捕获和引用捕获两种方式,编写时还可以使用简单的隐式捕获方式。
233 |
234 | ##### 值捕获
235 |
236 | 类似函数中的值传递,但要注意被捕获的变量的值是在lambda创建时拷贝,而不是调用时拷贝。(例2.2)
237 |
238 | ~~~c++
239 | void func1()
240 | {
241 | int t=42; //函数内的局部变量
242 | //将t拷贝到名为f的可调用对象 值捕获
243 | //此时传入了值是42,不会随着之后t的改变而改变
244 | auto f=[t] { return t;};
245 | t=0;
246 | auto j=f(); //j=42
247 | }
248 | ~~~
249 |
250 | ##### 引用捕获
251 |
252 | 引用捕获某变量时,实际使用的是引用所绑定的对象。
253 |
254 | ~~~c++
255 | void func2()
256 | {
257 | int t=42; //局部变量
258 | //将t拷贝到名为f的可调用对象
259 | //引用捕获,结果会随着之后t的改变而改变
260 | auto f=[&t] { return t;};
261 | t=0;
262 | auto j=f(); //j=0
263 | }
264 | ~~~
265 |
266 | 注意: 引用捕获时,必须保证在lambda执行时变量是存在的。并且我们应该尽量减少捕获的数据量。
267 |
268 | ##### 隐式捕获
269 |
270 | 由编译器自动判断捕获哪些变量
271 |
272 | [=] 表示采用值捕获
273 |
274 | [&] 表示采用引用捕获
275 |
276 | 更多的捕获方式可参见下表:
277 |
278 | 
279 |
280 | ##### 可变lambda
281 |
282 | 默认情况下,lambda不会改变一个值被拷贝的变量。如果我们希望改变它,需要在参数列表后使用关键字mutable
283 |
284 | 例2.3:
285 |
286 | ~~~c++
287 | #include
288 | int main() {
289 | int ammo{ 10 };
290 | //用lambda定义一个可调用对象shoot
291 | auto shoot{
292 | [ammo]() mutable { //捕获ammo
293 | --ammo; //使用mutable后可以更改lambda中ammo的值
294 | std::cout << "Pew! " << ammo << " shot(s) left.\n";
295 | }
296 | };
297 | //调用两次
298 | shoot();
299 | shoot();
300 | //打印主函数中的ammo
301 | std::cout << ammo << " shot(s) left";
302 |
303 | return 0;
304 | }
305 | //输出:
306 | //Pew! 9 shot(s) left.
307 | //Pew! 8 shot(s) left.
308 | //10 shot(s) left
309 | ~~~
310 |
311 | 由于是值传递,所以主函数的ammo并没有发生改变。如果改为引用传递,则无需使用mutable,可以直接改变ammo,最后会输出8。
312 |
313 | #### 5、返回类型
314 |
315 | 返回类型可以被忽略,此时,如果函数体只有一个return语句,则系统自动从返回表达式类型来判断返回类型;否则,返回类型为void。
316 |
317 | 因此,当函数体包含return语句之外的其他语句时,应该指定返回类型。
318 |
319 | 例如下面的这个lambda表达式:
320 |
321 | ~~~c++
322 | [](int i) ->int //指定返回int类型
323 | { if(i<0) return -i; else return i;};
324 |
325 | //示例:lambda用于标准库里的transform算法
326 | transform(v.begin(), v.end(), v.begin(),
327 | [](int i)->int {if(i<0) return -i; else return i;});
328 | ~~~
329 |
330 | #### 6、使用小结
331 |
332 | lambda表达式允许我们在另一个函数中定义一个匿名函数。嵌套很重要,因为它允许我们避免命名空间命名污染,并定义尽可能靠近使用它的位置的函数。
333 |
334 | 对于那种只在一两个地方使用的简单操作,lambda表示式是最有用的。
335 |
336 | 如果捕获列表为空,通常可用函数代替它。
337 |
338 | lambda表达式常用在算法的谓词上,这里也体现了lambda捕获列表的优势:根据算法接受一元谓词还是二元谓词,我们传递给算法的谓词必须严格接受一个或两个参数,但有时我们又希望获得更多的参数,便可以通过捕获列表实现。
339 |
340 | 例2.4:`find_if`算法(只接受一元谓词):
341 |
342 | ~~~c++
343 | #include
344 | #include
345 | #include
346 | using namespace std;
347 | bool comp(int a, int sz){ //比较函数,接受两个参数
348 | return a > sz;
349 | }
350 | int main() {
351 | int myints[] = { 32,71,12,45,26,80,53,33 };
352 | vector myvector(myints, myints + 8);
353 | vector::iterator it;
354 |
355 | // 使用默认的比较函数(operator <)排列所有元素:
356 | //12 26 32 33 45 53 71 80
357 | sort(myvector.begin(), myvector.end());
358 | //定义比较标准sz
359 | int sz = 50;
360 |
361 | //获取一个迭代器wc,指向第一个>sz的元素
362 | //lambda表达式只接受一个参数,正确
363 | auto wc = find_if(myvector.begin(), myvector.end(),
364 | [sz](int a) {return a > sz; });
365 |
366 | //comp函数接受两个参数,不符合find_if的要求,错误
367 | //auto wc2= find_if(myvector.begin(), myvector.end(),comp);
368 |
369 | //使用迭代器输出大于50的元素
370 | for (it = wc; it != myvector.end(); ++it)
371 | cout << *it << " ";
372 |
373 | return 0;
374 | }
375 | //输出:53 71 80
376 | ~~~
377 |
378 | ### 三、参数绑定
379 |
380 | #### 1、std::bind函数
381 |
382 | 在上面的例子中,传递给`find_if`的可调用对象必须接受单一参数,自定义的comp函数就不适用了,但我们可以使用**bind**函数(定义在头文件`functional`中),来用comp函数取代lambda表达式。
383 |
384 | 调用`bind`的一般形式如下:
385 |
386 | ~~~c++
387 | auto newCallable = bind(Callable, arg_list);
388 | ~~~
389 |
390 | 可以将 bind 函数看作一个通用的函数适配器,它接受一个可调用对象`Callable`,生成一个新的可调用对象`newCallable`来 "适应" 原对象的参数列表。
391 |
392 | `arg_list `是一个逗号分隔的参数列表,当我们调用`newCallable `时,`newCallable` 会调用 `Callable`,并传递给它 `arg_list` 中的参数。
393 |
394 | `arg_list` 中的参数可能包含形如 `_n` 的名字,其中 `n` 是一个整数。这些参数是 "占位符",表示 `newCallable` 的参数,它们占据了传递给 `newCallable `的参数的 "位置"。数值 `n` 表示 生成的可调用对象中参数的位置:`_1` 为 `newCallable` 的第一个参数,`_2` 为第二个参数,依此类推。
395 |
396 | `_n` 都定义在命名空间 `placeholders` 中,且此命名空间又定义在命名空间 `std` 中。因此使用`bind`与`_n`之前应该声明:
397 |
398 | ~~~c++
399 | #include
400 | using namespace std;
401 | using namespace std::placeholders;
402 | ~~~
403 |
404 | #### 2、bind的使用
405 |
406 | 作为一个简单的例子,我们将使用`bind`生成一个调用上一个例子中的`comp`函数的对象,如下所示
407 |
408 | ~~~c++
409 | auto comp1 = bind(comp, _1, 50);
410 | //comp1是一个可调用对象,接受一个int参数(_1处)
411 | //并用此参数与50调用comp函数
412 | ~~~
413 |
414 | 之前用lambda表达式写的wc可以更换为:(例3.1)
415 |
416 | ~~~c++
417 | auto wc = find_if(myvector.begin(), myvector.end(),
418 | bind(comp, _1, sz));
419 | ~~~
420 |
421 | 这便将`comp`的第二个参数绑定到了`sz`的值(50),只接受一个参数,满足了`find_if`的要求。
422 |
423 | ##### 调整参数顺序
424 |
425 | 如前文所示,我们可以用`bind`修正参数的值,也可以重新安排参数的顺序,获得更多新的函数。
426 |
427 | 例如,`f`是一个有5个参数的可调用对象,我们生成一个新可调用对象`g`,只有两个参数,用两个占位符表示
428 |
429 | ~~~c++
430 | auto g=bind(f, a, b, _2, c, _1);
431 | ~~~
432 |
433 | 如此,a、b、c分别与`f`的第1、2、4个参数绑定起来,`g`的第1、2个参数分别与`f`的第5、3个参数绑定。
434 |
435 | 即,调用g(x, y),就是调用f(a, b, y, c, x)
436 |
437 | ##### 绑定成员函数
438 |
439 | 除了绑定普通函数,`bind`还可以绑定成员函数,这时,第一个参数表示对象的成员函数的指针,第二个参数表示对象的地址,后面是参数列表。
440 |
441 | 例3.2:
442 |
443 | ~~~c++
444 | struct Foo {
445 | void func(int x, int y)
446 | {
447 | std::cout << x-y << '\n';
448 | }
449 | int data = 10;
450 | };
451 | int main() {
452 | Foo foo;
453 | auto f = std::bind(&Foo::func, &foo, 25, std::placeholders::_1);
454 | f(5); // 20
455 | }
456 | ~~~
457 |
458 | ##### 绑定引用参数
459 |
460 | 默认情况下, `bind` 的那些不是占位符的参数被拷贝到 `bind` 返回的可调用对象中。但是, 与 lambda 类似,有时对有些绑定的参数我们希望以**引用方式传递**, 或是要绑定参数的类型无法拷贝。这时,我们必须使用标准库**`ref`函数**(头文件`functional`中)。
461 |
462 | 例3.3:输出流对象`os`无法拷贝时,用`ref`返回引用
463 |
464 | ~~~c++
465 | #include
466 | #include
467 | #include
468 | #include
469 | using namespace std;
470 | using namespace std::placeholders;
471 |
472 | ostream& disp(ostream& os, const int a, char c) {
473 | return os << a << c;
474 | }
475 |
476 | int main() {
477 | int myints[] = { 32,71,12,45,26,80,53,33 };
478 | vector myvector(myints, myints + 8);
479 | vector::iterator it;
480 | sort(myvector.begin(), myvector.end());
481 |
482 | char cc = '*';
483 | ostream &os = cout;
484 | for_each(myvector.begin(), myvector.end(),
485 | bind(disp, ref(os), _1, cc)); //使用ref返回引用
486 | return 0;
487 | }
488 | //输出:12*26*32*33*45*53*71*80*
489 | ~~~
490 |
491 |
492 |
493 |
--------------------------------------------------------------------------------
/C++/第六讲/函数对象、lambda与参数绑定/imgs/image1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ksc999/THUEEXP-SAST-Tutor/d01ad77a614a8ff9eefa3c4a64fc7e8bf7b47f08/C++/第六讲/函数对象、lambda与参数绑定/imgs/image1.png
--------------------------------------------------------------------------------
/C++/第十讲/README.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ksc999/THUEEXP-SAST-Tutor/d01ad77a614a8ff9eefa3c4a64fc7e8bf7b47f08/C++/第十讲/README.md
--------------------------------------------------------------------------------
/C++/第四讲/static修饰符用法总结/README.md:
--------------------------------------------------------------------------------
1 | # 第4讲第1节 Static修饰符用法总结
2 |
3 | ## 简介
4 |
5 | - 内容主题:Static
6 | - 你将学到什么:
7 | - static修饰符的功能
8 |
9 | ---
10 |
11 | ## 文件清单
12 |
13 | - `docs/`
14 |
15 | - Static.md【本讲教程】
16 | - `imgs/`
17 |
18 | - 无图片(只是为了统一格式)
19 | - `codes/`
20 |
21 | static部分
22 |
23 | - zoo.cpp
24 | - 1.cpp**与**2.cpp(linkage文件夹中)
25 | - mem_fun.cpp
26 |
27 |
28 | ---
29 |
30 | ## 参考资料
31 |
32 | - 清华大学计算机系姚海龙老师程序设计基础课件
33 | - 《C++ Primer》(第五版)
34 | - [learn Cpp](https://www.learncpp.com/)
35 | - [Microsoft C++ docs](https://docs.microsoft.com/en-us/cpp/cpp/?view=msvc-170)
36 | - [GeeksforGeeks C++ tutorials](https://www.geeksforgeeks.org/c-plus-plus/)
37 | - 由于上述三个网站均为英文网站,将表述转换为中文时,参考了部分CSDN上的文章,由于篇幅过多,不在此一一列出
38 |
39 |
--------------------------------------------------------------------------------
/C++/第四讲/static修饰符用法总结/codes/linkage/1.cpp:
--------------------------------------------------------------------------------
1 | #include