当前位置:   article > 正文

C++核心编程:类和对象 笔记

C++核心编程:类和对象 笔记

4.类和对象

  • C++面向对象的三大特性为:封装,继承,多态
  • C++认为万事万物都皆为对象,对象上有其属性和行为

例如:

  • 人可以作为对象,属性有姓名、年龄、身高、体重...,行为有走、跑、跳、说话...
  • 车可以作为对象,属性有轮胎、方向盘、车灯...,行为有载人、放音乐、开空调...
  • 具有相同性质的对象,我们可以对其进行抽象,抽象为类,人属于人类,车属于车类

4.1 封装

4.1.1 封装的意义

  • 封装是C++面向对象三大特性之一
  • 封装的意义:
    • 将属性和行为作为一个整体,表现生活中的事物
    • 将属性和行为加以权限控制

封装意义一:在设计类的时候,属性和行为写在一起,表现事物

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

类中的属性和行为 我们统一称为成员

  1. 属性 -> 成员属性/成员变量
  2. 行为 -> 成员函数/成员方法
  • 示例1:设计一个圆类,求圆的周长
  • 类和对象-封装-属性和行为作为整体.cpp
  1. #include <iostream>
  2. using namespace std;
  3. // 圆周率
  4. const double PI = 3.1415926;
  5. // 设计一个圆类,求圆的周长
  6. // 圆求周长的公式 : 2 * PI * 半径
  7. class Circle {
  8. // 访问权限
  9. // 公共权限
  10. public:
  11. // 行为
  12. // 获取圆的周长
  13. double calculatePerimeter(double radius) {
  14. return 2 * PI * radius;
  15. }
  16. // 属性:半径
  17. int m_radius;
  18. };
  19. int main() {
  20. // 通过圆类,创建具体的圆(对象)
  21. // 实例化(通过一个类 创建一个对象的过程)
  22. Circle c1;
  23. c1.m_radius = 10;
  24. cout<<"圆的周长为 : "<<c1.calculatePerimeter(c1.m_radius)<<""<<endl;
  25. return 0;
  26. }
  • 示例2:设计学生类
  1. #include <iostream>
  2. using namespace std;
  3. #include <string>
  4. // 设计一个学生类,属性有姓名和学号
  5. // 可以给姓名和学号赋值,可以显示学生的姓名和学号
  6. // 设计学生类
  7. class Student{
  8. public://公共权限
  9. // 类中的属性和行为 我们统一称为成员
  10. // 属性 -> 成员属性/成员变量
  11. // 行为 -> 成员函数/成员方法
  12. string m_name; // 姓名
  13. int m_Id; // 学号
  14. // 行为
  15. void setName(string name){ // 设置姓名
  16. m_name = name;
  17. }
  18. void setId(int id){ // 设置学号
  19. m_Id = id;
  20. }
  21. void display(){ // 显示姓名和学号
  22. cout << "姓名:" << m_name << endl;
  23. cout << "学号:" << m_Id << endl;
  24. }
  25. };
  26. int main() {
  27. // 创建一个具体学生 实例化对象
  28. Student s1;
  29. // 给s1对象 进行属性赋值操作
  30. s1.m_name = "张三";
  31. s1.m_Id = 2019001;
  32. // 显示学生信息
  33. s1.display();
  34. s1.setName("李四");
  35. s1.setId(2019002);
  36. s1.display();
  37. }

运行结果:

  1. PS D:\Work\c++\build\bin> ."D:/Work/c++/bin/app.exe"
  2. 姓名:张三
  3. 学号:2019001
  4. 姓名:李四
  5. 学号:2019002
  6. PS D:\Work\c++\build\bin>

封装意义二:

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

访问权限有三种:

  1. public 公共权限
  2. protected 保护权限
  3. private 私有权限
  1. #include <iostream>
  2. using namespace std;
  3. /*
  4. 三种访问权限:
  5. 公共权限 public 成员 类内可以访问 类外可以访问
  6. 保护权限 protected 成员 类内可以访问 类外不可以访问(儿子可以访问父亲中的保护内容)
  7. 私有权限 private 成员 类内可以访问 类外不可以访问(儿子不可以访问父亲中的私有内容)
  8. */
  9. class Person{
  10. public:
  11. void func() {
  12. m_Name = "张三";//公共权限
  13. m_Car = "拖拉机";//保护权限
  14. m_Password = 123456;//私有权限
  15. }
  16. public:
  17. // 公共权限
  18. string m_Name;//姓名
  19. protected:
  20. // 保护权限
  21. string m_Car;//汽车
  22. private:
  23. // 私有权限
  24. int m_Password;//密码
  25. };
  26. int main() {
  27. // 实例化具体对象
  28. Person p1;
  29. p1.m_Name = "呵呵哒";//类外可以访问(public)
  30. // p1.m_Car = "保时捷";//保护权限内容,在类外访问不到 error:成员"Person::m_Car"不可访问
  31. // p1.m_Password = 123456;//私有权限内容,在类外访问不到 error:成员"Person::m_Password"不可访问
  32. p1.func();//类外可以访问(public)
  33. return 0;
  34. }

4.1.2 structclass区别

  • 在C++中structclass唯一的区别就是默认的访问权限不同

区别:

  • struct默认权限是公共权限
  • class默认权限是私有权限
  1. #include <iostream>
  2. using namespace std;
  3. class C1{
  4. int m_A;// 默认权限 是私有
  5. };
  6. struct C2{
  7. int m_A;// 默认权限 是公共
  8. };
  9. int main() {
  10. /*
  11. 在C++中struct和class唯一的区别就是默认的访问权限不同
  12. 区别:
  13. struct默认权限是公共权限
  14. class默认权限是私有权限
  15. */
  16. C1 c1;
  17. // c1.m_A = 10;// error:成员"C1::m_A"不可访问
  18. C2 c2;
  19. c2.m_A = 100;// ok
  20. return 0;
  21. }

4.1.3 成员属性设置为私有

  • 优点1:将所有成员属性设置为私有,可以自己控制读写权限
  • 优点2:对于写权限,我们可以检测到数据的有效性

演示:控制读写权限 

  1. #include <iostream>
  2. using namespace std;
  3. /*
  4. 成员属性设置私有
  5. 优点1:将所有成员属性设置为私有,可以自己控制读写权限
  6. 优点2:对于写权限,我们可以检测到数据的有效性
  7. */
  8. // 人类
  9. class Person{
  10. public:
  11. // 设置姓名
  12. void setName(string name) {
  13. m_Name = name;
  14. }
  15. // 获取姓名
  16. string getName() const {// 返回类型为const,表示返回的是常量,不可修改返回值。
  17. return m_Name;// 返回m_Name的值。
  18. }
  19. // 获取年龄
  20. int getAge() const {// 返回类型为const,表示返回的是常量,不可修改返回值。
  21. return m_Age;// 返回m_Age的值。
  22. }
  23. // 设置偶像
  24. void setIdol(string idol) {
  25. m_Idol = idol;
  26. }
  27. private:
  28. string m_Name;// 姓名 可读可写
  29. int m_Age = 18;// 年龄 只读
  30. string m_Idol;// 偶像 只写
  31. };
  32. int main() {
  33. Person p;
  34. // 姓名设置
  35. p.setName("张三");
  36. cout<<"姓名:"<<p.getName()<<endl;// 姓名:张三
  37. // 获取年龄
  38. cout<<"年龄:"<<p.getAge()<<endl;// 输出年龄:18
  39. // 偶像设置
  40. p.setIdol("迪丽热巴");
  41. return 0; // 程序执行成功,返回0
  42. }

演示:检测到数据的有效性,例如年龄设置为0-150之间

  1. #include <iostream>
  2. using namespace std;
  3. /*
  4. 成员属性设置私有
  5. 优点1:将所有成员属性设置为私有,可以自己控制读写权限
  6. 优点2:对于写权限,我们可以检测到数据的有效性
  7. */
  8. // 人类
  9. class Person{
  10. public:
  11. // 设置姓名
  12. void setName(string name) {
  13. m_Name = name;
  14. }
  15. // 获取姓名
  16. string getName() const {// 返回类型为const,表示返回的是常量,不可修改返回值。
  17. return m_Name;// 返回m_Name的值。
  18. }
  19. // 设置年龄 0-150
  20. void setAge(int age) {
  21. if (age < 0 || age > 150) {
  22. cout << "年龄: "<< age << ",输入错误!" << endl;// 输出错误信息。
  23. return;
  24. }
  25. m_Age = age;
  26. }
  27. // 获取年龄
  28. int getAge() const {// 返回类型为const,表示返回的是常量,不可修改返回值。
  29. return m_Age;// 返回m_Age的值。
  30. }
  31. // 设置偶像
  32. void setIdol(string idol) {
  33. m_Idol = idol;
  34. }
  35. private:
  36. string m_Name;// 姓名 可读可写
  37. int m_Age = 18;// 年龄 只读
  38. string m_Idol;// 偶像 只写
  39. };
  40. int main() {
  41. Person p;
  42. // 姓名设置
  43. p.setName("张三");
  44. cout<<"姓名:"<<p.getName()<<endl;// 姓名:张三
  45. // 获取年龄
  46. p.setAge(250);
  47. cout<<"年龄:"<<p.getAge()<<endl;// 输出年龄:18
  48. // 偶像设置
  49. p.setIdol("迪丽热巴");
  50. return 0; // 程序执行成功,返回0
  51. }

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

  • 设计立方体类(Cube)
  • 求出立方体的面积和体积
  • 分别用全局函数和成员函数判断两个立方体是否相等

1.类和对象-封装-设计案例1-立方体类 

  1. #include <iostream>
  2. using namespace std;
  3. /*
  4. 立方体类设计
  5. 1.创建立方体类
  6. 2.设计属性
  7. 3.设计行为 获取立方体面积和体积
  8. 4.分别利用全局函数和成员函数 判断两个立方体是否相等
  9. */
  10. class Cube{
  11. public:
  12. // 设置长
  13. void setL(int l){m_L=l;}
  14. // 获取长
  15. int getL(){return m_L;}
  16. // 设置宽
  17. void setW(int w){m_W=w;}
  18. // 获取宽
  19. int getW(){return m_W;}
  20. // 设置高
  21. void setH(int h){m_H=h;}
  22. // 获取高
  23. int getH(){return m_H;}
  24. // 获取立方体面积
  25. int getArea(){return 2*(m_L*m_W+m_L*m_H+m_W*m_H);}
  26. // 获取立方体体积
  27. int getVolume(){return m_L*m_W*m_H;}
  28. // 利用成员函数判断两个立方体是否相等
  29. bool isSameByClass(Cube &c){
  30. if(getL() == c.getL() && getW() == c.getW() && getH() == c.getH()) {
  31. return true;
  32. }else{
  33. return false;
  34. }
  35. }
  36. private:
  37. int m_L;//长
  38. int m_W;//宽
  39. int m_H;//高
  40. };
  41. // 利用全局函数判断 两个立方体是否相等
  42. bool isSame(Cube &c1,Cube &c2) {
  43. if(c1.getL() == c2.getL() && c1.getW() == c2.getW() && c1.getH() == c2.getH())
  44. return true; // 判断长宽高是否相等,如果相等则返回true,否则返回false
  45. else return false;
  46. }
  47. int main() {
  48. Cube c1,c2;
  49. c1.setL(5);
  50. c1.setW(4);
  51. c1.setH(3);
  52. c2.setL(10);
  53. c2.setW(10);
  54. c2.setH(10);
  55. cout<<"c1的面积为:"<<c1.getArea()<<endl;
  56. cout<<"c1的体积为:"<<c1.getVolume()<<endl;
  57. cout<<"***************************"<<endl;
  58. cout<<"c2的面积为:"<<c2.getArea()<<endl;
  59. cout<<"c2的体积为:"<<c2.getVolume()<<endl;
  60. // 判断c1和c2是否相等
  61. // 利用全局函数判断
  62. bool ret = isSame(c1,c2);
  63. if(ret) cout<<"利用全局函数判断:c1和c2相等"<<endl;
  64. else cout<<"利用全局函数判断:c1和c2不相等"<<endl;
  65. // 利用成员函数判断
  66. ret = c1.isSameByClass(c2);
  67. if(ret) cout<<"利用成员函数判断:c1和c2相等"<<endl;
  68. else cout<<"利用成员函数判断:c1和c2不相等"<<endl;
  69. return 0;
  70. }

2.类和对象-封装-设计案例2-点和圆关系案例

  1. #include <iostream>
  2. using namespace std;
  3. // 点和圆关系案例
  4. // 点类
  5. class Point{
  6. public:
  7. // 设置x
  8. void setX(int x) {m_X = x;}
  9. // 获取x
  10. int getX() {return m_X;}
  11. // 设置y
  12. void setY(int y) {m_Y = y;}
  13. // 获取y
  14. int getY() {return m_Y;}
  15. private:
  16. int m_X;
  17. int m_Y;
  18. };
  19. // 圆类
  20. class Circle {
  21. public:
  22. // 设置半径
  23. void setR(int r) {m_R = r;}
  24. // 获取半径
  25. int getR() {return m_R;}
  26. // 设置圆心
  27. void setCenter(Point center) {m_Center = center;}
  28. // 获取圆心
  29. Point getCenter() {return m_Center;}
  30. private:
  31. int m_R;// 半径
  32. // 在类中可以让另一个类 作为本类中的成员
  33. Point m_Center;// 圆心
  34. };
  35. // 判断点和圆关系
  36. void isInCircle(Circle& c, Point& p) {
  37. // 计算两点之间距离 平方
  38. int distance =
  39. (c.getCenter().getX() - p.getX()) * (c.getCenter().getX() - p.getX()) +
  40. (c.getCenter().getY() - p.getY()) * (c.getCenter().getY() - p.getY());
  41. // 计算半径的平方
  42. int rDistance = c.getR() * c.getR();
  43. // 判断关系
  44. if(distance == rDistance) {
  45. cout<<"点在圆上"<<endl;
  46. }
  47. else if(distance < rDistance) {
  48. cout<<"点在圆内"<<endl;
  49. }
  50. else {
  51. cout<<"点在圆外"<<endl;
  52. }
  53. }
  54. int main() {
  55. // 创建圆
  56. Circle c;
  57. c.setR(10);
  58. Point center;
  59. center.setX(10);
  60. center.setY(0);
  61. c.setCenter(center);
  62. // 创建点
  63. Point p;
  64. p.setX(10);
  65. p.setY(9);
  66. // 判断关系
  67. isInCircle(c,p);
  68. return 0;
  69. }

 进一步完善项目

  • point.h
  1. #pragma once
  2. // 点类
  3. class Point{
  4. public:
  5. // 设置x
  6. void setX(int x);
  7. // 获取x
  8. int getX();
  9. // 设置y
  10. void setY(int y);
  11. // 获取y
  12. int getY();
  13. private:
  14. int m_X;
  15. int m_Y;
  16. };
  • point.cpp
  1. #include "point.h"
  2. // 设置x
  3. void Point::setX(int x) {m_X = x;}
  4. // 获取x
  5. int Point::getX() {return m_X;}
  6. // 设置y
  7. void Point::setY(int y) {m_Y = y;}
  8. // 获取y
  9. int Point::getY() {return m_Y;}
  •  circle.h
  1. #pragma once
  2. #include "point.h"
  3. // 圆类
  4. class Circle {
  5. public:
  6. // 设置半径
  7. void setR(int r);
  8. // 获取半径
  9. int getR();
  10. // 设置圆心
  11. void setCenter(Point center);
  12. // 获取圆心
  13. Point getCenter();
  14. private:
  15. int m_R;// 半径
  16. // 在类中可以让另一个类 作为本类中的成员
  17. Point m_Center;// 圆心
  18. };
  • circle.cpp
  1. #include "circle.h"
  2. // 设置半径
  3. void Circle::setR(int r) {m_R = r;}
  4. // 获取半径
  5. int Circle::getR() {return m_R;}
  6. // 设置圆心
  7. void Circle::setCenter(Point center) {m_Center = center;}
  8. // 获取圆心
  9. Point Circle::getCenter() {return m_Center;}

4.2 对象的初始化和清理

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

4.2.1 构造函数和析构函数

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

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

C++利用了构造函数和析构函数解决上述问题,这两个函数将会被编译器自动调用,完成对象初始化和清理工作。对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供编译器提供的构造函数和析构函数是空实现

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

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

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

析构函数写法:~类名(){}

  1. 析构函数,没有返回值也不写void
  2. 函数名称与类名相同,在名称前加上符号~
  3. 析构函数不可以有参数,因此不可以发生重载
  4. 程序在对象销毁时候会自动调用析构,无须手动调用,而且只会调用一次
  1. #include <iostream>
  2. using namespace std;
  3. // 对象的初始化和清理
  4. // 1.构造函数 进行初始化操作
  5. class Person {
  6. public:
  7. // 1. 构造函数
  8. // 没有返回值 不用写void
  9. // 函数名 与类名相同
  10. // 构造函数可以有参数,可以发生重载
  11. // 创建对象的时候,构造函数会自动调用,而且只调用一次
  12. Person() {cout<<"Person 构造函数的调用"<<endl;}
  13. // 2.析构函数 进行清理的操作
  14. // 没有返回值 不写void
  15. // 函数名和类名相同 在名称前加~
  16. // 析构函数不可以有参数的,不可以发生重载
  17. // 对象在销毁前,会自动调用析构函数,而且只会调用一次
  18. ~Person () {cout<<"Person 析构函数的调用"<<endl;}
  19. };
  20. // 构造和析构都是必须有的实现,如果我们不提供,编译器会提供一个空实现的构造和析构
  21. void test01() {
  22. Person p;// 在栈上的数据,test01执行完毕后,释放这个对象
  23. }
  24. int main() {
  25. test01();
  26. return 0;
  27. }

执行结果:

  1. PS D:\Work\c++\bin> .\app
  2. Person 构造函数的调用
  3. Person 析构函数的调用
  4. PS D:\Work\c++\bin>

4.2.2 构造函数的分类及调用

两种分类方式:

  • 按参数分为:有参构造和无参构造
  • 按类型分为:普通构造和拷贝构造

三种调用方式:

  • 括号法
  • 显示法
  • 隐式转换法
  1. #include <iostream>
  2. using namespace std;
  3. // 1.构造函数的分类及调用
  4. // 分类
  5. // 按照参数分类 无参构造(默认构造)和有参构造
  6. // 按照类型分类 普通构造 拷贝构造
  7. class Person {
  8. public:
  9. // 无参构造函数
  10. Person(){cout<<"Person的构造函数调用"<<endl;}
  11. // 有参构造函数
  12. Person(int age) {
  13. m_age=age;
  14. cout<<"Person的构造函数调用"<<endl;
  15. }
  16. // 拷贝构造函数
  17. Person(const Person& p) {
  18. // 将传入的人身上的所有属性,拷贝到我身上
  19. m_age = p.m_age;
  20. cout<<"Person的拷贝构造函数调用"<<endl;
  21. }
  22. ~Person(){cout<<"Person的析构函数调用"<<endl;}
  23. int m_age;
  24. };
  25. void test01() {
  26. // 1. (括号法)
  27. Person p1;// 默认构造调用
  28. Person p2(10);// 有参构造调用
  29. Person p3(p2);// 拷贝构造调用
  30. cout<< "p2的年龄是: "<<p2.m_age<<endl;
  31. cout<< "p3的年龄是: "<<p3.m_age<<endl;
  32. // 注意事项1
  33. // 调用默认构造函数时候,不要加()
  34. // 因为下面这行代码,编译器会认为是一个函数的声明,不会认为在创建对象
  35. // Person p1();
  36. // void func();
  37. }
  38. void test02() {
  39. // 2.显示法
  40. Person p1;
  41. Person p2 = Person(10);// 有参构造
  42. Person p3 = Person(p2);// 拷贝构造
  43. Person(20);// 匿名对象 特点:当前行执行结束后,系统会立即回收掉匿名对象
  44. cout<<"asasasa"<<endl;
  45. // 注意事项2
  46. // 不要利用拷贝构造函数 初始化匿名对象
  47. // 编译器会认为Person(p3) == Person p3;
  48. // 会认为这是一个对象的声明
  49. // Person(p3); // 此时重定义了
  50. }
  51. void test03() {
  52. // 3,隐式转换法
  53. Person p4 = 10;// 相当于 写了 Person p4 = Person(10); 有参构造
  54. Person p5 = p4;// 拷贝构造
  55. }
  56. // 调用
  57. int main() {
  58. test02();
  59. return 0;
  60. }

4.2.3 拷贝构造函数调用时机

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

  1. 使用一个已经创建完毕的对象来初始化一个新对象
  2. 值传递的方式给函数参数传值
  3. 以值方式返回局部对象
  1. #include<iostream>
  2. using namespace std;
  3. // 拷贝构造函数调用时机
  4. class Person {
  5. public:
  6. Person() {cout<<"Person默认构造函数调用"<<endl;}
  7. Person(int age) : m_Age(age) {cout<<"Person有参构造函数调用"<<endl;}
  8. Person(const Person& p) {
  9. m_Age = p.m_Age;
  10. cout<<"Person拷贝构造函数调用"<<endl;
  11. }
  12. ~Person() {cout<<"Person析构函数调用"<<endl;}
  13. int m_Age;
  14. private:
  15. };
  16. // 1.使用一个已经创建完毕的对象来初始化一个新对象
  17. void test01() {
  18. Person p1(20);
  19. Person p2(p1);
  20. cout<<"p2: "<<p2.m_Age<<endl;
  21. /*
  22. Person有参构造函数调用
  23. Person拷贝构造函数调用
  24. p2: 20
  25. Person析构函数调用
  26. Person析构函数调用
  27. */
  28. }
  29. // 2.值传递的方式给函数参数传值
  30. void doWork(Person p) {// 值传递会拷贝一个临时的副本出来,在调用它的拷贝构造函数,
  31. }
  32. void test02() {
  33. Person p;
  34. doWork(p);
  35. /*
  36. Person默认构造函数调用
  37. Person拷贝构造函数调用
  38. Person析构函数调用
  39. Person析构函数调用
  40. */
  41. }
  42. // 3.以值方式返回局部对象
  43. Person doWork2() {
  44. Person p1;
  45. cout<<(int*)&p1<<endl;
  46. return p1;
  47. }
  48. void test03() {
  49. Person p = doWork2();
  50. cout<<(int*)&p<<endl;
  51. }
  52. int main() {
  53. test03();
  54. return 0;
  55. }

4.2.4 构造函数调用规则

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

  1. 默认构造函数(无参,函数体为空)
  2. 默认析构函数(无参,函数体为空)
  3. 默认拷贝构造函数,对属性进行值拷贝

构造函数调用规则如下:

  • 如果用户定义有参构造函数,C++不再提供默认无参构造,但是会提供默认拷贝构造
  • 如果用户定义拷贝构造函数,C++不会再提供其他构造函数
  1. #include <iostream>
  2. using namespace std;
  3. // 构造函数的调用规则
  4. // 1.创建一个类,C++编译器会给每个类都添加至少3个函数
  5. // 默认构造(空实现)
  6. // 析构函数(空实现)
  7. // 拷贝构造(值拷贝)
  8. // 2.如果我们写了有参构造函数,编译器就不再提供默认构造,依然提供拷贝构造
  9. // 如果我们写了拷贝构造函数,编译器就不再提供其他普通构造函数了
  10. class Person {
  11. public:
  12. // 默认构造
  13. Person() { cout << "Person的默认构造函数调用" << endl; }
  14. // 有参构造
  15. Person(int age) {
  16. m_Age = age;
  17. cout << "Person的有参构造函数调用" << endl;
  18. }
  19. // 拷贝构造
  20. // Person(const Person& p) {
  21. // m_Age = p.m_Age;
  22. // cout << "Person的拷贝构造函数调用" << endl;
  23. // }
  24. // 析构函数
  25. ~Person() { cout << "Person的析构函数调用" << endl;}
  26. int m_Age;
  27. };
  28. void test01() {
  29. Person p;
  30. p.m_Age = 18;
  31. Person p2(p);
  32. cout<<"p2的年龄为: "<<p2.m_Age<<endl;
  33. }
  34. int main() {
  35. test01();
  36. return 0;
  37. }

执行结果:

  1. PS D:\Work\c++\bin> ."D:/Work/c++/bin/app.exe"
  2. Person的默认构造函数调用
  3. p2的年龄为: 18
  4. Person的析构函数调用
  5. Person的析构函数调用
  • 如果我们写了拷贝构造函数,编译器就不再提供其他普通构造函数了
  1. #include <iostream>
  2. using namespace std;
  3. // 构造函数的调用规则
  4. // 1.创建一个类,C++编译器会给每个类都添加至少3个函数
  5. // 默认构造(空实现)
  6. // 析构函数(空实现)
  7. // 拷贝构造(值拷贝)
  8. // 2.如果我们写了有参构造函数,编译器就不再提供默认构造,依然提供拷贝构造
  9. // 如果我们写了拷贝构造函数,编译器就不再提供其他普通构造函数了
  10. class Person {
  11. public:
  12. // 默认构造
  13. Person() { cout << "Person的默认构造函数调用" << endl; }
  14. // 有参构造
  15. Person(int age) {
  16. m_Age = age;
  17. cout << "Person的有参构造函数调用" << endl;
  18. }
  19. // 拷贝构造
  20. Person(const Person& p) {
  21. m_Age = p.m_Age;
  22. cout << "Person的拷贝构造函数调用" << endl;
  23. }
  24. // 析构函数
  25. ~Person() { cout << "Person的析构函数调用" << endl;}
  26. int m_Age;
  27. };
  28. void test01() {
  29. Person p;
  30. p.m_Age = 18;
  31. Person p2(p);
  32. cout<<"p2的年龄为: "<<p2.m_Age<<endl;
  33. }
  34. int main() {
  35. test01();
  36. return 0;
  37. }

执行结果:

  1. Person的默认构造函数调用
  2. Person的拷贝构造函数调用
  3. p2的年龄为: 18
  4. Person的析构函数调用
  5. Person的析构函数调用
  •  如果我们写了有参构造函数,编译器就不再提供默认构造,依然提供拷贝构造
  1. #include <iostream>
  2. using namespace std;
  3. // 构造函数的调用规则
  4. // 1.创建一个类,C++编译器会给每个类都添加至少3个函数
  5. // 默认构造(空实现)
  6. // 析构函数(空实现)
  7. // 拷贝构造(值拷贝)
  8. // 2.如果我们写了有参构造函数,编译器就不再提供默认构造,依然提供拷贝构造
  9. // 如果我们写了拷贝构造函数,编译器就不再提供其他普通构造函数了
  10. class Person {
  11. public:
  12. // 默认构造
  13. Person() { cout << "Person的默认构造函数调用" << endl; }
  14. // 有参构造
  15. Person(int age) {
  16. m_Age = age;
  17. cout << "Person的有参构造函数调用" << endl;
  18. }
  19. // 拷贝构造
  20. // Person(const Person& p) {
  21. // m_Age = p.m_Age;
  22. // cout << "Person的拷贝构造函数调用" << endl;
  23. // }
  24. // 析构函数
  25. ~Person() { cout << "Person的析构函数调用" << endl;}
  26. int m_Age;
  27. };
  28. void test02() {
  29. Person p(18);
  30. Person p2(p);
  31. }
  32. int main() {
  33. test02();
  34. return 0;
  35. }

执行结果:

  1. Person的有参构造函数调用
  2. Person的析构函数调用
  3. Person的析构函数调用

4.2.5 深拷贝与浅拷贝

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

  • 浅拷贝:简单的赋值拷贝操作
  • 深拷贝:在堆区重新申请空间,进行拷贝操作
  1. #include<iostream>
  2. using namespace std;
  3. // 深拷贝与浅拷贝
  4. class Person {
  5. public:
  6. Person() { cout<<"Person的默认构造函数调用"<<endl; }
  7. Person(int age,int height) {
  8. m_Age = age;
  9. m_Height = new int(height);
  10. cout<<"Person的有参构造函数调用"<<endl;
  11. }
  12. // 自己实现拷贝构造函数,解决浅拷贝带来的问题
  13. Person(const Person& p) {
  14. cout<<"Person的拷贝构造函数调用"<<endl;
  15. m_Age = p.m_Age;
  16. // m_Height = p.m_Age; // 编译器默认实现就是这行代码
  17. // 深拷贝操作
  18. m_Height = new int(*p.m_Height); // 在堆区创建一块内存
  19. }
  20. ~Person(){
  21. // 析构代码,将堆区开辟数据做释放操作
  22. if(m_Height != NULL) {
  23. delete m_Height;
  24. m_Height = NULL;
  25. }
  26. cout<<"Person的析构函数调用"<<endl;
  27. }
  28. int m_Age;
  29. int* m_Height;// 身高
  30. };
  31. void test01() {
  32. // 深拷贝,p1走p1的析构,p2走p2的析构.注意:堆栈是先进后出的
  33. // 浅拷贝,有交叉重复释放的问题
  34. Person p1(18,160);
  35. cout<<"p1的年龄: "<<p1.m_Age<<"身高: "<<*p1.m_Height<<endl;
  36. Person p2(p1);// 默认的拷贝构造函数(浅拷贝)
  37. cout<<"p2的年龄: "<<p2.m_Age<<"身高: "<<*p2.m_Height<<endl;
  38. }
  39. int main() {
  40. test01();
  41. return 0;
  42. }

执行结果:

  1. Person的有参构造函数调用
  2. p1的年龄: 18身高: 160
  3. Person的拷贝构造函数调用
  4. p2的年龄: 18身高: 160
  5. Person的析构函数调用
  6. Person的析构函数调用

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

4.2.6 初始化列表

  • 作用:C++提供了初始化列表语法,用来初始化属性
  • 语法:构造函数():属性1(值1),属性2(值2),…{}
  1. #include <iostream>
  2. using namespace std;
  3. // 初始化列表
  4. class Person {
  5. public:
  6. // 传统初始化操作
  7. // Person(int a,int b,int c) {
  8. // m_A = a;
  9. // m_B = b;
  10. // m_C = c;
  11. // }
  12. // 初始化列表初始化属性
  13. Person(int a,int b,int c):m_A(a),m_B(b),m_C(c) { }
  14. int m_A;
  15. int m_B;
  16. int m_C;
  17. };
  18. void test01() {
  19. Person p(1,2,3);
  20. cout << p.m_A << " " << p.m_B << " " << p.m_C << endl; // 输出:1 2 3
  21. }
  22. int main() {
  23. return 0;
  24. }

4.2.7 类对象作为类成员

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

例如:

  1. class A{};
  2. class B{A a};

B类中有对象A作为成员,A为对象成员。那么当创建B对象时,A与B的构造和析构的顺序是谁先谁后呢?

  • 当其他类对象作为本类成员,构造时候先构造类对象,再构造自身,析构的顺序与构造相反
  1. #include <iostream>
  2. using namespace std;
  3. #include <string>
  4. // 类对象作为类成员
  5. // 手机类
  6. class Phone {
  7. public:
  8. Phone(string pName) {
  9. m_PName = pName;
  10. cout<<"Phone有参构造调用"<<endl;
  11. }
  12. ~Phone() { cout<<"Phone的析构函数调用"<<endl; }
  13. string m_PName;// 品牌名称
  14. };
  15. // 人类
  16. class Person {
  17. public:
  18. // Phone m_Phone = pName 隐式转换法
  19. Person(string name, string pName) : m_name(name), m_phone(pName) {
  20. cout<<"Person有参构造调用"<<endl;
  21. }
  22. ~Person() { cout<<"Person的析构函数调用"<<endl; }
  23. // 姓名
  24. string m_name;
  25. // 手机
  26. Phone m_phone;
  27. private:
  28. };
  29. // 当其他类对象作为本类成员,构造时候先构造类对象,再构造自身,析构的顺序与构造相反
  30. void test01() {
  31. Person p("张三", "苹果");
  32. cout << "姓名:" << p.m_name << " 手机品牌:" << p.m_phone.m_PName << endl;
  33. }
  34. int main() {
  35. test01();
  36. return 0;
  37. }

执行结果:

  1. PS D:\Work\c++\bin> ."D:/Work/c++/bin/app.exe"
  2. Phone有参构造调用
  3. Person有参构造调用
  4. 姓名:张三 手机品牌:苹果
  5. Person的析构函数调用
  6. Phone的析构函数调用
  7. PS D:\Work\c++\bin>

4.2.8 静态成员

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

静态成员分为:

静态成员变量

  • 所有对象共享同一份数据
  • 在编译阶段分配内存
  • 类内声明,类外初始化
  1. #include <iostream>
  2. using namespace std;
  3. // 静态成员变量
  4. class Person {
  5. public:
  6. // 1.所有对象都共享同一份数据
  7. // 2.编译阶段就分配内存
  8. // 3.类内声明,类外初始化操作
  9. static int m_A; // 静态成员变量
  10. // 静态成员变量也是有访问权限的
  11. private:
  12. static int m_B; // 静态成员变量
  13. };
  14. int Person::m_A = 100; // 类外初始化操作
  15. int Person::m_B = 200; // 类外初始化操作
  16. void test01() {
  17. Person p;
  18. cout << "m_A = " << p.m_A << endl; // 100
  19. Person p2;
  20. p2.m_A = 400;
  21. cout << "m_A = " << p.m_A << endl; // 400
  22. }
  23. void test02() {
  24. // 静态成员变量,不属于某个对象上,所有对象都共享同一份数据
  25. // 因此静态成员变量有两种访问方式
  26. // 1.通过对象进行访问
  27. // Person p;
  28. // cout<<"p.m_A = "<<p.m_A<<endl; // 100
  29. // 2.通过类名进行访问
  30. cout << "m_A = " << Person::m_A << endl; // 100
  31. // 类外访问不到私有静态成员变量
  32. // cout << "m_B = " << Person::m_B << endl; // error:成员"Person::m_B"不可访问
  33. }
  34. int main() {
  35. test02();
  36. return 0;
  37. }

静态成员函数

  • 所有对象共享同一个函数
  • 静态成员函数只能访问静态成员变量
  1. #include <iostream>
  2. using namespace std;
  3. // 静态成员函数
  4. // 所有对象共享同一个函数
  5. // 静态成员函数只能访问静态成员变量
  6. /*
  7. 因为m_B必须通过创建对象才能访问,得创建一个对象,
  8. 才能够去读/写这块内存.当你去调用静态成员函数func()
  9. 这个函数体的内部不知道改变的是哪个对象的m_B
  10. 非静态成员变量属于特定对象的成员变量
  11. */
  12. class Person {
  13. public:
  14. // 静态成员函数
  15. static void func() {
  16. m_A = 100;//静态成员函数是可以访问静态成员变量
  17. // m_B = 200;//静态成员函数是不可以访问非静态成员变量的,无法区分到底是哪个对象的m_B属性
  18. cout<<"static void func调用"<<endl;
  19. }
  20. static int m_A;// 静态成员变量
  21. int m_B;// 非静态成员变量
  22. // 静态成员函数也是有访问权限的
  23. private:
  24. static void func2() { cout<<"static void func2调用"<<endl; }
  25. };
  26. int Person::m_A = 0;
  27. // 有两种访问方式
  28. void test01() {
  29. // 1.通过对象访问
  30. Person p;
  31. p.func();
  32. // 2.通过类名访问(类外访问不到私有静态成员函数)
  33. Person::func();
  34. // Person::func2();// 错误,不可访问
  35. }
  36. int main() {
  37. test01();
  38. return 0;
  39. }

因为m_B必须通过创建对象才能访问,得创建一个对象,才能够去读/写这块内存.当你去调用静态成员函数func()。这个函数体的内部不知道改变的是哪个对象的m_B,非静态成员变量属于特定对象的成员变量。静态成员函数是不可以访问非静态成员变量的,无法区分到底是哪个对象的m_B属性。

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

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

在C++中,类内的成员变量和成员函数分开存储,只有非静态成员变量才属于类的对象上。

  1. #include <iostream>
  2. using namespace std;
  3. // 成员变量和成员函数是分开存储的
  4. class Person {
  5. public:
  6. int m_A;// 非静态成员变量 属于类的对象上
  7. static int m_B;// 静态成员变量 不属于类的对象上
  8. void func(){} // 非静态成员函数 不属于类的对象上
  9. static void func2(){} // 静态成员函数 不属于类的对象上
  10. };
  11. int Person::m_B = 100;// 初始化静态成员变量
  12. void test01() {
  13. Person p;
  14. // 空对象占用内存空间为:1
  15. // C++编译器会给每个空对象特分配一个字节空间,是为了区分空对象
  16. // 占内存的位置
  17. // 每个空对象也应该有一个独一无二的内存地址
  18. cout<<"size of p = " <<sizeof(p)<<endl; // 1
  19. }
  20. void test02() {
  21. Person p;
  22. cout<<"size of p = " <<sizeof(p)<<endl; // 4
  23. }
  24. int main() {
  25. test02();
  26. return 0;
  27. }

4.3.2 this指针概念

通过4.3.1 我们知道在C++中成员变量和成员函数是分开存储的。每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码。那么问题是:这一块代码是如何区分那个对象调用自己的呢?C++通过提供的对象指针,this指针,解决上述问题,this指针指向被调用的成员函数所属的对象

  • this指针是隐含每一个非静态成员函数内的一种指针
  • this指针不需要定义,直接使用即可

this指针的用途:

  • 当形参和成员变量同名时,可用this指针来区分
  • 在类的非静态成员函数中返回对象本身,可使用return *this
  1. #include <iostream>
  2. using namespace std;
  3. class Person {
  4. public:
  5. Person(int age) {
  6. // this指针指向的是被调用的成员函数 所属的对象
  7. this->m_Age = age;
  8. }
  9. Person& PersonAddPerson(Person& p) {
  10. this->m_Age += p.m_Age;
  11. // this指向p2的指针,而*this指向的就是p2这个对象本体
  12. return *this; // 返回对象本身
  13. }
  14. int m_Age;
  15. };
  16. // 1.解决名称冲突
  17. void test01() {
  18. Person p1(18); // 创建一个Person对象,年龄为18
  19. cout <<"p1的年龄为: "<<p1.m_Age << endl; // 输出Person对象的年龄
  20. }
  21. // 2.返回对象本身用*this
  22. void test02() {
  23. Person p1(10); // 创建一个Person对象,年龄为10
  24. Person p2(20); // 创建另一个Person对象,年龄为20
  25. // 链式编程思想
  26. p2.PersonAddPerson(p1).PersonAddPerson(p1).PersonAddPerson(p1);
  27. cout<<"p2的年龄为: "<<p2.m_Age<<endl;
  28. }
  29. int main() {
  30. test02();
  31. return 0;
  32. }

4.3.3 空指针访问成员函数

  • C++中空指针也是可以调用成员函数的,但是也要注意有没有用到this指针
  • 如果用到this指针,需要加以判断保证代码的健壮性
  1. #include <iostream>
  2. using namespace std;
  3. // 空指针调用成员函数
  4. class Person {
  5. public:
  6. void showClassName() {
  7. cout<<"this is Person Class"<<endl;
  8. }
  9. void showPersonAge() {
  10. // 常见报错原因:传入的指针是NULL
  11. if(this == NULL) return;
  12. cout<<"Age = "<<this->m_Age<<endl;
  13. }
  14. int m_Age;
  15. };
  16. void test01() {
  17. Person *p = NULL;
  18. p->showClassName();
  19. p->showPersonAge();
  20. }
  21. int main() {
  22. test01();
  23. return 0;
  24. }

4.3.4 const修饰成员函数

常函数:

  1. 成员函数后加const称此函数为常函数
  2. 常函数内不可以修改成员属性
  3. 成员属性声明时加关键字mutable,则表示在常函数中依然可以修改

常对象:

  1. 声明对象前加const称此对象为常对象
  2. 常对象只能调用常函数

const 和 this 

  • this指针的本质 是指针常量 指针的指向是不可以修改的,指针指向的值是可以修改的
  • Person * const this;
  • const Person * const this; 指针的指向的值也不可以修改了
  • 在成员函数后面加const,修饰的是this指向,让指针指向的值也不可以修改 

注意:

  • 特殊变量,即使在常函数中,也可以修改这个值.需要加上关键字mutable 
  • 常对象不可以调用普通成员函数,因为普通成员函数可以修改属性
  1. #include <iostream>
  2. using namespace std;
  3. // 常函数
  4. class Person {
  5. public:
  6. Person();
  7. // this指针的本质 是指针常量 指针的指向是不可以修改的,指针指向的值是可以修改的
  8. // Person * const this;
  9. // const Person * const this; 指针的指向的值也不可以修改了
  10. // 在成员函数后面加const,修饰的是this指向,让指针指向的
  11. // 值也不可以修改
  12. void showPerson() const {// 常函数
  13. // this->m_A = 10;
  14. // this 指针是不可以修改指针的指向的
  15. // this = NULL;// error 分配到"this"(记时错误)
  16. this->m_B = 100;
  17. }
  18. void func() {
  19. m_A = 100;
  20. }
  21. int m_A;
  22. mutable int m_B;// 特殊变量,即使在常函数中,也可以修改这个值.需要加上关键字mutable
  23. };
  24. void test01() {
  25. Person p;
  26. p.showPerson();
  27. }
  28. // 常对象
  29. void test02() {
  30. const Person p;// 在对象前加上const,变为常对象
  31. // p.m_A = 100;//error
  32. p.m_B = 100;// m_B是特殊值,在常对象也可以修改
  33. // 常对象只能调用常函数
  34. p.showPerson();
  35. // p.func();// error:常对象不可以调用普通成员函数,因为普通成员函数可以修改属性
  36. }
  37. int main() {
  38. return 0;
  39. }

4.4 友元

生活中你的家有客厅(Public),有你的卧室(Private)客厅所有来的客人都可以进去,但是你的卧室是私有的,也就是说只有你能进去。但是呢,你也可以允许你的好闺蜜好基友进去。在程序里,有些私有属性,也想让类外特殊的一些函数或者类进行访问,就需要用到友元的技术,友元的目的就是让一个函数或者类访问另一个类中私有成员。友元的关键字为friend

友元的三种实现

  1. 全局函数做友元
  2. 类做友元
  3. 成员函数做友元

全局函数做友元 

  1. #include <iostream>
  2. using namespace std;
  3. #include <string>
  4. // 建筑物
  5. class Building {
  6. // 告诉编译器 goodGay全局函数是 Building好朋友,可以访问Building中私有成员
  7. friend void goodGay(Building *building);
  8. public:
  9. Building() {
  10. m_SittingRoom = "客厅";
  11. m_BedRoom = "卧室";
  12. }
  13. public:
  14. string m_SittingRoom; // 客厅
  15. private:
  16. string m_BedRoom; // 卧室
  17. };
  18. // 全局函数
  19. void goodGay(Building *building) {
  20. cout<<"好基友全局函数 正在访问 : "<<building->m_SittingRoom<<endl;
  21. cout<<"好基友全局函数 正在访问 : "<<building->m_BedRoom<<endl;
  22. }
  23. void test01() {
  24. Building building;
  25. goodGay(&building); // 调用全局函数
  26. }
  27. int main() {
  28. test01();
  29. return 0; // 返回0表示正常退出
  30. }

执行结果:

  1. PS D:\Work\c++\bin> ."D:/Work/c++/bin/app.exe"
  2. 好基友全局函数 正在访问 : 客厅
  3. 好基友全局函数 正在访问 : 卧室

类做友元

  1. #include <iostream>
  2. using namespace std;
  3. #include <string>
  4. // 类做友元
  5. class Building;
  6. class GoodGay
  7. {
  8. public:
  9. GoodGay();
  10. void visit();// 参观函数 访问Building中的属性
  11. Building *building;
  12. };
  13. class Building{
  14. // 告诉编译器 GoodGay 类是本来的好朋友,可以访问本类中私有成员
  15. friend class GoodGay;
  16. public:
  17. Building();
  18. public:
  19. string m_SittingRoom;// 客厅
  20. private:
  21. string m_BedRoom;// 卧室
  22. };
  23. // 类外写成员函数
  24. Building::Building() {
  25. m_SittingRoom = "客厅";
  26. m_BedRoom = "卧室";
  27. }
  28. GoodGay::GoodGay() {
  29. // 创建建筑物对象
  30. building = new Building;
  31. }
  32. void GoodGay::visit() {
  33. cout << "好基友正在访问" << building->m_SittingRoom << endl;
  34. cout << "好基友正在访问" << building->m_BedRoom << endl;
  35. }
  36. void test01() {
  37. GoodGay gg;
  38. gg.visit();
  39. }
  40. int main() {
  41. test01();
  42. return 0;
  43. }

成员函数做友元 

  1. #include <iostream>
  2. using namespace std;
  3. #include <string>
  4. class Building;
  5. class GoodGay{
  6. public:
  7. GoodGay();
  8. void visit();// 让visit函数可以访问Building中私有成员
  9. void visit2();// 让visit2函数不可以访问Building中私有成员
  10. Building* building;
  11. };
  12. class Building{
  13. // 告诉编译器 GoodGay类下的visit成员函数作为本类的好朋友,可以访问私有成员
  14. friend void GoodGay::visit();
  15. public:
  16. Building();
  17. string m_SittingRoom; // 客厅
  18. private:
  19. string m_BedRoom; // 卧室
  20. };
  21. // 类外实现成员函数
  22. Building::Building(){
  23. m_SittingRoom = "客厅";
  24. m_BedRoom = "卧室";
  25. }
  26. GoodGay::GoodGay(){
  27. building = new Building;
  28. }
  29. void GoodGay::visit() {
  30. cout<<"visit 函数正在访问: "<<building->m_SittingRoom<<endl;
  31. cout<<"visit 函数正在访问: "<<building->m_BedRoom<<endl;
  32. }
  33. void GoodGay::visit2() {
  34. cout<<"visit 函数正在访问: "<<building->m_SittingRoom<<endl;
  35. }
  36. void test01() {
  37. GoodGay gg;
  38. gg.visit();
  39. gg.visit2();
  40. }
  41. int main() {
  42. test01();
  43. return 0;
  44. }

4.5  运算符重载

4.5.1 加号运算符重载

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

总结:

  • 对于内置的数据类型的表达式的运算符是不可能改变的
  • 不要滥用运算符重载

  • 加号运算符重载 

  1. #include <iostream>
  2. using namespace std;
  3. // 加号运算符重载
  4. class Person {
  5. public:
  6. // 1. 成员函数重载 + 号
  7. Person operator+(Person &p) {
  8. Person temp;
  9. temp.m_A = this->m_A + p.m_A;
  10. temp.m_B = this->m_B + p.m_B;
  11. return temp;
  12. }
  13. int m_A;
  14. int m_B;
  15. };
  16. // 2.全局函数重载+号
  17. // Person operator+(Person &p1,Person &p2) {
  18. // Person temp;
  19. // temp.m_A = p1.m_A + p2.m_A;
  20. // temp.m_B = p1.m_B + p2.m_B;
  21. // return temp;
  22. // }
  23. // 函数重载的版本
  24. Person operator+(Person &p1,int num) {
  25. Person temp;
  26. temp.m_A = p1.m_A + num;
  27. temp.m_B = p1.m_B + num;
  28. return temp;
  29. }
  30. void test01() {
  31. Person p1;
  32. p1.m_A = 10;
  33. p1.m_B = 10;
  34. Person p2;
  35. p2.m_A = 10;
  36. p2.m_B = 10;
  37. // 成员函数重载本质调用
  38. // Person p3 = p1.operator+(p2);
  39. // 全局函数重载本质调用
  40. // Person p3 = operator+(p1,p2);
  41. Person p3 = p1 + p2;
  42. // 运算符重载,也可以发生函数重载
  43. Person p4 = p1 + 100;
  44. cout<<"p3.m_A = " << p3.m_A<<endl;
  45. cout<<"p3.m_B = " << p3.m_B<<endl;
  46. cout<<"p4.m_A = " << p4.m_A<<endl;
  47. cout<<"p4.m_B = " << p4.m_B<<endl;
  48. }
  49. int main() {
  50. test01();
  51. return 0;
  52. }

4.5.2 左移运算符重载

  1. #include <iostream>
  2. using namespace std;
  3. // 左移运算符重载
  4. class Person {
  5. friend ostream & operator<<(ostream &cout,Person &p);
  6. public:
  7. Person(int a,int b):m_A(a),m_B(b){}
  8. // 利用成员函数重载 左移运算符 p.operator<<(cout) 简化版本 p<<cout
  9. // 不会利用成员函数重载<<运算符,因为实现cout在左侧
  10. // void operator<<(Person &p) {
  11. // }
  12. private:
  13. int m_A;
  14. int m_B;
  15. };
  16. // 只能利用全局函数重载左移运算符
  17. ostream & operator<<(ostream &cout,Person &p) // 本质 operator<<(cout,p) 简化 cout<<p
  18. {
  19. cout<<"m_A = "<<p.m_A<<" , m_B = "<<p.m_B;
  20. return cout;
  21. }
  22. void test01() {
  23. Person p(10,20);
  24. cout<<p<<", hello,world!"<<endl;
  25. }
  26. int main() {
  27. test01();
  28. return 0;
  29. }

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

4.5.3 递增运算符重载

  • 作用:通过重载递增运算符,实现自己的整型数据
  1. #include <iostream>
  2. using namespace std;
  3. // 重载递增运算符
  4. // 自定义整型
  5. class MyInterger {
  6. friend ostream& operator<<(ostream& cout,MyInterger myint);
  7. public:
  8. MyInterger() {
  9. m_Num = 0;
  10. }
  11. // 重载前置++运算符 返回引用为了一直对一个数据进行递增
  12. MyInterger& operator++() {
  13. // 先进行++运算
  14. m_Num++;
  15. // 再将自身做返回
  16. return *this;
  17. }
  18. // 重载后置++运算符
  19. // void operator++(int) int代表占位参数,可以用于区分前置和后置递增
  20. MyInterger operator++(int) {
  21. // 先 记录当时结果
  22. MyInterger temp = *this;
  23. // 后 递增
  24. m_Num++;
  25. // 最后将记录结果做返回
  26. return temp;
  27. }
  28. private:
  29. int m_Num;
  30. };
  31. // 重载<<运算符
  32. ostream& operator<<(ostream& cout,MyInterger myint) {
  33. cout<<myint.m_Num;
  34. return cout;
  35. }
  36. void test01() {
  37. MyInterger myint;
  38. cout<<++(++myint)<<endl;
  39. cout<<myint<<endl;
  40. }
  41. void test02() {
  42. MyInterger myint;
  43. cout<<myint++<<endl;
  44. cout<<myint;
  45. }
  46. int main() {
  47. test02();
  48. return 0;
  49. }
  • 总结:前置递增返回引用,后置递增返回值 

4.5.4 赋值运算符重载

C++编译器至少给一个类添加4个函数

  1. 默认构造函数(无参,函数体为空)
  2. 默认析构函数(无参,函数体为空)
  3. 默认拷贝构造函数,对属性进行值拷贝
  4. 赋值运算符operator=,对属性进行值拷贝
  • 如果类中有属性指向堆区,做赋值操作时会出现深浅拷贝的问题

  1. #include <iostream>
  2. using namespace std;
  3. // 赋值运算符重载
  4. class Person {
  5. public:
  6. Person(int age) {
  7. m_Age = new int(age);
  8. }
  9. ~Person() {
  10. if(m_Age!=NULL) {
  11. delete m_Age;
  12. m_Age = NULL;
  13. }
  14. }
  15. // 重载 赋值运算符
  16. Person& operator=(Person &p) {
  17. // 编译器是提供浅拷贝
  18. // m_Age = p.m_Age;
  19. // 应该先判断是否有属性在堆区,如果有先释放干净,然后再深拷贝
  20. if(m_Age!=NULL) {
  21. delete m_Age;
  22. m_Age = NULL;
  23. }
  24. // 深拷贝
  25. m_Age = new int(*p.m_Age);
  26. // 返回对象本身 而不是返回副本
  27. return *this;
  28. }
  29. int *m_Age;
  30. };
  31. void test01() {
  32. Person p1(18);
  33. Person p2(20);
  34. Person p3(30);
  35. p3 = p2 = p1;// 赋值操作
  36. cout<<"p1 的年龄为: "<<*p1.m_Age<<endl;
  37. cout<<"p2 的年龄为: "<<*p2.m_Age<<endl;
  38. cout<<"p3 的年龄为: "<<*p3.m_Age<<endl;
  39. }
  40. int main() {
  41. test01();
  42. return 0;
  43. }

执行结果:

  1. PS D:\Work\c++\bin> ."D:/Work/c++/bin/app.exe"
  2. p1 的年龄为: 18
  3. p2 的年龄为: 18
  4. p3 的年龄为: 18

4.5.5 关系运算符重载

  • 作用:重载关系运算符,可以让两个自定义类型对象进行对比操作
  1. #include <iostream>
  2. using namespace std;
  3. // 作用:重载关系运算符,可以让两个自定义类型对象进行对比操作
  4. class Person{
  5. public:
  6. Person(string name,int age) {
  7. m_Name = name;
  8. m_Age = age;
  9. }
  10. // 重载 == 号
  11. bool operator==(Person& p) {
  12. if(this->m_Name == p.m_Name && this->m_Age == p.m_Age) {
  13. return true;
  14. }
  15. return false;
  16. }
  17. // 重载 != 号
  18. bool operator!=(Person& p) {
  19. if(this->m_Name == p.m_Name && this->m_Age == p.m_Age) {
  20. return false;
  21. }
  22. return true;
  23. }
  24. string m_Name;
  25. int m_Age;
  26. };
  27. void test01() {
  28. Person p1("Tom",18);
  29. Person p2("Tom",18);
  30. Person p3("Jerry",18);
  31. if(p1 == p2) {
  32. cout<<"p1 和 p2 是相等的"<<endl;
  33. }
  34. if(p1 != p3) {
  35. cout<<"p1 和 p3 是不相等的"<<endl;
  36. }else{
  37. cout<<"p1 和 p3 是相等的"<<endl;
  38. }
  39. }
  40. int main() {
  41. test01();
  42. return 0;
  43. }

执行结果: 

  1. PS D:\Work\c++\bin> ."D:/Work/c++/bin/app.exe"
  2. p1 和 p2 是相等的
  3. p1 和 p3 是不相等的
  4. PS D:\Work\c++\bin>

4.5.6 函数调用运算符重载

  • 函数调用运算符() 也可以重载
  • 由于重载后使用的方式非常像函数的调用,因此称为仿函数
  • 仿函数没有固定写法,非常灵活
  1. #include <iostream>
  2. using namespace std;
  3. #include <string>
  4. // 函数调用运算符重载
  5. // 打印输出类
  6. class MyPrint {
  7. public:
  8. // 重载函数调用运算符
  9. void operator()(string test){
  10. cout<<test<<endl;
  11. }
  12. };
  13. void MyPrint02(string test) {
  14. cout<<test<<endl;
  15. }
  16. void test01() {
  17. MyPrint myPrint;
  18. myPrint("hello,world"); // 由于使用起来非常类似于函数调用,因此称为仿函数
  19. MyPrint02("hello,heheda");
  20. }
  21. // 仿函数非常灵活,没有固定的写法
  22. class MyAdd{
  23. public:
  24. int operator()(int num1,int num2){
  25. return num1+num2;
  26. }
  27. };
  28. void test02() {
  29. MyAdd myadd;
  30. int ret = myadd(100,200);
  31. cout<<"ret = "<<ret<<endl;
  32. // 匿名函数对象
  33. cout<<MyAdd()(100,10)<<endl;
  34. }
  35. int main() {
  36. test02();
  37. return 0;
  38. }

4.6 继承

  • 继承是面向对象三大特性之一

有些类与类之间存在特殊的关系,例如下图中:

我们发现,定义这些类时,下级别的成员除了拥有上一级的共性,还有自己的特性。

这个时候我们就可以考虑利用继承的技术,减少重复代码

 

  1. #include <iostream>
  2. using namespace std;
  3. // 继承方式
  4. // 公共继承
  5. class Base1{
  6. public:
  7. int m_A;
  8. protected:
  9. int m_B;
  10. private:
  11. int m_C;
  12. };
  13. class Son1:public Base1{
  14. public:
  15. void func() {
  16. m_A = 10;// 父类中的公共权限成员 到子类中依然是公共权限
  17. m_B = 10;// 父类中的保护权限成员 到子类中依然是保护权限
  18. // m_C = 10;// 父类中的私有权限成员 子类访问不到
  19. }
  20. };
  21. void test01(){
  22. Son1 s1;
  23. s1.m_A = 100;
  24. // s1.m_B = 20; // 到Son1中 m_B是保护权限 类外访问不到
  25. }
  26. // 保护继承
  27. class Base2{
  28. public:
  29. int m_A;
  30. protected:
  31. int m_B;
  32. private:
  33. int m_C;
  34. };
  35. class Son2:protected Base2{
  36. public:
  37. void func(){
  38. m_A = 100; // 父类中公共成员,到子类中变为保护权限
  39. m_B = 200; // 父类中保护成员,到子类中变为保护权限
  40. // m_C = 400;// 父类中私有成员 子类访问不到
  41. };
  42. };
  43. void test02() {
  44. Son2 s2;
  45. // s2.m_A = 1000;// 在Son2中 m_A变为保护权限,因此类外访问不到
  46. // s2.m_B = 120; // 在Son2中 m_B为保护权限,因此类外访问不到
  47. }
  48. // 私有继承
  49. class Base3{
  50. public:
  51. int m_A;
  52. protected:
  53. int m_B;
  54. private:
  55. int m_C;
  56. };
  57. class Son3:private Base3{
  58. public:
  59. void func(){
  60. m_A = 100; // 父类中公共成员,到子类中变为私有权限
  61. m_B = 200; // 父类中保护成员,到子类中变为私有权限
  62. // m_C = 400;// 父类中私有成员 子类访问不到
  63. };
  64. };
  65. void test03() {
  66. Son3 s3;
  67. // s2.m_A = 1000;// 在Son3中 m_A变为私有权限,因此类外访问不到
  68. // s2.m_B = 120; // 在Son3中 m_B变为私有权限,因此类外访问不到
  69. }
  70. class GrandSon3:public Son3 {
  71. public:
  72. void func() {
  73. // m_A = 100; // 到了Son3中 m_A变为私有,即使是儿子,也是访问不到
  74. // m_B = 20; // 到了Son3中 m_B变为私有,即使是儿子,也是访问不到
  75. }
  76. };

4.6.3 继承中的对象模型

  • 问题:从父类继承过来的成员,哪些属于子类对象中?
  1. #include<iostream>
  2. using namespace std;
  3. // 继承中的对象模型
  4. class Base{
  5. public:
  6. int m_A;
  7. protected:
  8. int m_B;
  9. private:
  10. int m_C;
  11. };
  12. class Son:public Base{
  13. public:
  14. int m_D;
  15. };
  16. // 利用开发人员命令提示工具查看对象模型
  17. // 跳转盘符 F:
  18. // 跳转文件路径 cd 具体路径下
  19. // 查看命名
  20. // c1 /d1 reportSingleClassLayout类名 文件名
  21. void test01() {
  22. // 16
  23. // 父类中所有非静态成员属性都会被子类继承下去
  24. // 父类中私有成员属性 是被编译器给隐藏了,因此是访问不到,但是确实被继承了
  25. cout<<"size of Son = "<<sizeof(Son)<<endl;
  26. }
  27. int main() {
  28. return 0;
  29. }

 

打开工具窗口后,定位到当前CPP文件的盘符

然后输入:c1 /d1 reportSingleClassLayout查看的类名 所属文件名

4.6.4 继承中构造和析构顺序

子类继承父类后,当创建子类对象,也会调用父类的构造函数

问题:父类和子类的构造和析构顺序是谁先谁后?

继承中的构造和析构顺序如下:先构造父类,再构造子类,析构的顺序与构造的顺序相反

  1. #include <iostream>
  2. using namespace std;
  3. // 继承中的构造和析构顺序
  4. class Base{
  5. public:
  6. Base() {cout << "Base constructor" << endl;}
  7. ~Base() {cout << "Base destructor" << endl;}
  8. private:
  9. };
  10. class Son:public Base{
  11. public:
  12. Son() {cout << "Son constructor" << endl;}
  13. ~Son() {cout << "Son destructor" << endl;}
  14. };
  15. void test01() {
  16. // Base b;
  17. // 继承中的构造和析构顺序如下:
  18. // 先构造父类,再构造子类,析构的顺序与构造的顺序相反
  19. Son s;
  20. }
  21. int main() {
  22. test01();
  23. return 0;
  24. }

执行结果:

  1. PS D:\Work\c++\bin> ."D:/Work/c++/bin/app.exe"
  2. Base constructor
  3. Son constructor
  4. Son destructor
  5. Base destructor
  6. PS D:\Work\c++\bin>

4.6.5 继承同名成员处理方式

问题:当子类与父类出现同名的成员,如何通过子类对象,访问到子类或父类中同名的数据呢?

  • 访问子类同名成员,直接访问即可
  • 访问父类同名成员,需要加作用域

总结:

  1. 子类对象可以直接访问到子类中同名成员
  2. 子类对象加作用域可以访问到父类同名成员
  3. 当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数
  1. #include <iostream>
  2. using namespace std;
  3. // 继承中同名成员处理
  4. class Base{
  5. public:
  6. Base() {
  7. m_A = 100;
  8. }
  9. void func() {
  10. cout << "Base func()" << endl;
  11. }
  12. void func(int a) {
  13. cout << "Base func(int a)" << endl;
  14. }
  15. int m_A;
  16. };
  17. class Son:public Base{
  18. public:
  19. Son() {
  20. m_A = 200;
  21. }
  22. void func() {
  23. cout << "Son func()" << endl;
  24. }
  25. int m_A;
  26. };
  27. // 同名成员属性处理
  28. void test01() {
  29. Son s;
  30. cout << "Son下的 m_A = " << s.m_A << endl;//200
  31. // 如果通过子类对象 访问到父类中同名成员,需要加作用域
  32. cout << "Base下的 m_A = " << s.Base::m_A << endl;//100
  33. cout << "Son::m_A = " << s.Son::m_A << endl;//200
  34. }
  35. // 同名成员函数处理
  36. void test02() {
  37. Son s;
  38. s.func();//Son func() 直接调用 调用的是子类中的同名成员
  39. // 如何调用到父类中同名成员函数?
  40. s.Base::func();//Base func()
  41. // 如果子类中出现和父类同名的成员函数,子类的同名成员会隐藏掉父类中所有的同名成员函数
  42. // s.func(100); // error
  43. // 如果想访问到父类中被隐藏的同名成员函数,需要加作用域
  44. s.Base::func(100);//Base func(int a)
  45. }
  46. int main() {
  47. // test01();
  48. test02();
  49. return 0;
  50. }

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

问题:继承中同名的静态成员在子类对象上如何进行访问?

静态成员和非静态成员出现同名,处理方式一致

  • 访问子类同名成员,直接访问即可
  • 访问父类同名成员,需要加作用域
  1. #include <iostream>
  2. using namespace std;
  3. // 继承中的同名静态成员处理方式
  4. class Base {
  5. public:
  6. static int m_A;
  7. static void func() {
  8. cout << "Base func()" << endl;
  9. }
  10. static void func(int a) {
  11. cout<<"Base func(int a)"<<endl;
  12. }
  13. };
  14. int Base::m_A = 100;
  15. class Son:public Base{
  16. public:
  17. static int m_A;
  18. static void func() {
  19. cout<<"Son func()"<<endl;
  20. }
  21. };
  22. int Son::m_A = 200;
  23. // 同名的静态成员属性
  24. void test01() {
  25. Son s;
  26. // 1.通过对象访问
  27. cout<<"通过对象访问: "<<endl;
  28. cout<<"Son 下的 m_A = "<<s.m_A<<endl;
  29. cout<<"Base 下的 m_A = "<<s.Base::m_A<<endl;
  30. // 2.通过类名访问
  31. cout<<"通过类名访问: "<<endl;
  32. cout<<"Son 下的 m_A = "<<Son::m_A<<endl;
  33. // 第一个::代表通过类名方式访问
  34. // 第二个::代表访问父类作用域下
  35. cout<<"Base 下的 m_A = "<<Son::Base::m_A<<endl;
  36. }
  37. // 同名的静态成员函数
  38. void test02() {
  39. Son s;
  40. // 1.通过对象访问
  41. cout<<"通过对象访问: "<<endl;
  42. s.func();//Son func()
  43. s.Base::func();//Base func()
  44. // 2.通过类名访问
  45. cout<<"通过类名访问: "<<endl;
  46. Son::func();//Son func()
  47. Son::Base::func();//Base func()
  48. // 子类出现和父类同名静态成员函数,也会隐藏父类中所有同名成员函数
  49. // 如果想访问父类中被隐藏同名成员,需要加作用域
  50. Son::Base::func(1);//Base func(int a)
  51. }
  52. int main() {
  53. test02();
  54. return 0;
  55. }

总结:同名静态成员处理方式和非静态处理方式一样,只不过有两种访问方式(通过对象 和 通过类名)

4.6.7 多继承语法

C++允许一个类继承多个类

语法:class 子类:继承方式1 父类1,继承方式2 父类2...

多继承可能会引发父类中有同名成员出现,需要加作用域区分

C++实际开发中不建议多继承

  1. #include <iostream>
  2. using namespace std;
  3. // 多继承语法
  4. class Base1 {
  5. public:
  6. Base1() {
  7. m_A = 100;
  8. }
  9. int m_A;
  10. };
  11. class Base2 {
  12. public:
  13. Base2() {
  14. m_A = 200;
  15. }
  16. int m_A;
  17. };
  18. // 子类 需要继承Base1和Base2
  19. // 语法:class 子类:继承方式1 父类1,继承方式2 父类2...
  20. class Son:public Base1,public Base2 {
  21. public:
  22. Son() {
  23. m_C = 300;
  24. m_D = 400;
  25. }
  26. int m_C;
  27. int m_D;
  28. };
  29. void test01() {
  30. Son s;
  31. cout<<"size of Son: "<<sizeof(s)<<endl;
  32. // 当父类中出现同名成员需要加作用域区分
  33. cout<<"Base1::m_A = "<<s.Base1::m_A<<endl;//100
  34. cout<<"Base2::m_A = "<<s.Base2::m_A<<endl;//200
  35. }
  36. int main() {
  37. test01();
  38. return 0;
  39. }

总结:多继承中如果父类中出现了同名情况,子类使用时候要加作用域

4.6.8 菱形继承

菱形继承概念:

  1. 两个派生类继承同一个基类
  2. 又有某个类同时继承这两个派生类
  3. 这种继承称为菱形继承,或者钻石继承

典型的菱形继承案例:

菱形继承问题:

  1. 羊继承了动物的数据,驼同样继承了动物的数据,当草泥马使用数据时,就会产生二义性
  2. 草泥马继承自动物的数据继承了两份,其实我们应清楚,这份数据只需要一份即可
  1. #include <iostream>
  2. using namespace std;
  3. // 动物类
  4. class Animal{
  5. public:
  6. int m_Age;
  7. };
  8. // 羊类
  9. class Sheep:public Animal{};
  10. // 驼类
  11. class Tuo:public Animal{};
  12. // 羊驼类
  13. class SheepTuo:public Sheep,public Tuo{};
  14. void test01() {
  15. SheepTuo st;
  16. st.Sheep::m_Age = 18;
  17. st.Tuo::m_Age = 28;
  18. // 当菱形继承,两个父类拥有相同数据,需要加以作用域区分
  19. cout<<"st.Sheep::m_Age = "<<st.Sheep::m_Age<<endl;//18
  20. cout<<"st.Tuo::m_Age = "<<st.Tuo::m_Age<<endl;//28
  21. // 这份数据我们知道,只要有一份就可以,菱形继承导致数据有两份,资源浪费
  22. }
  23. int main() {
  24. test01();
  25. return 0;
  26. }

  • 利用虚继承 解决菱形继承的问题,继承之前 加上关键字 virtual 变为虚继承
  1. #include <iostream>
  2. using namespace std;
  3. // 动物类
  4. class Animal{
  5. public:
  6. int m_Age;
  7. };
  8. // 利用虚继承 解决菱形继承的问题
  9. // 继承之前 加上关键字 virtual 变为虚继承
  10. // Animal类称为 虚基类
  11. // 羊类
  12. class Sheep:virtual public Animal{};
  13. // 驼类
  14. class Tuo:virtual public Animal{};
  15. // 羊驼类
  16. class SheepTuo:public Sheep,public Tuo{};
  17. void test01() {
  18. SheepTuo st;
  19. st.Sheep::m_Age = 18;
  20. st.Tuo::m_Age = 28;
  21. // 当菱形继承,两个父类拥有相同数据,需要加以作用域区分
  22. cout<<"st.Sheep::m_Age = "<<st.Sheep::m_Age<<endl;//28
  23. cout<<"st.Tuo::m_Age = "<<st.Tuo::m_Age<<endl;//28
  24. cout<<"st.m_Age = "<<st.m_Age<<endl;//28
  25. // 这份数据我们知道,只要有一份就可以,菱形继承导致数据有两份,资源浪费
  26. }
  27. int main() {
  28. test01();
  29. return 0;
  30. }

总结:

  • 菱形继承带来的主要问题是子类继承两份相同的数据,导致资源浪费以及毫无意义
  • 利用虚继承可以解决菱形继承问题

4.7 多态

4.7.1 多态的基本概念

多态是C++面向对象三大特性之一

多态分为两类

  • 静态多态:函数重载和运算符重载属于静态多态,复用函数名
  • 动态多态:派生类和虚函数实现运行时多态

静态多态和动态多态区别:

  • 静态多态的函数地址早绑定 - 编译阶段确定函数地址
  • 动态多态的函数地址晚绑定 - 运行阶段确定函数地址
  1. #include <iostream>
  2. using namespace std;
  3. // 多态
  4. // 动物类
  5. class Animal {
  6. public:
  7. // 虚函数
  8. virtual void speak() {
  9. cout<<"动物在说话"<<endl;
  10. }
  11. };
  12. // 猫类
  13. class Cat:public Animal{
  14. public:
  15. // 重写 函数返回值类型 函数名 参数列表 完全相同
  16. void speak() {
  17. cout<<"小猫在说话"<<endl;
  18. }
  19. };
  20. // 狗类
  21. class Dog:public Animal{
  22. public:
  23. void speak() {
  24. cout<<"小狗在说话"<<endl;
  25. }
  26. };
  27. // 执行说话的函数
  28. // 地址早绑定 在编译阶段确定函数地址
  29. // 如果想执行让猫说话,那么这个函数地址就不能提前绑定,需要在运行阶段进行绑定,
  30. // 地址晚绑定
  31. // 动态多态满足条件
  32. // 1.有继承关系
  33. // 2.子类要重写父类的虚函数
  34. // 动态多态使用
  35. // 父类的指针或者引用,指向子类对象
  36. void doSpeak(Animal &animal) { // Animal &animal = cat;
  37. animal.speak();
  38. }
  39. void test01() {
  40. Cat cat;
  41. doSpeak(cat);
  42. Dog dog;
  43. doSpeak(dog);
  44. }
  45. int main() {
  46. test01();
  47. return 0;
  48. }

总结:

多态满足条件:

  • 有继承关系
  • 子类重写父类的虚函数

多态使用条件:

  • 父类指针或引用指向子类对象

重写:函数返回值类型 函数名 参数列表 完全一致称为重写

多态的原理剖析:

 

当子类重写父类的虚函数,子类中的虚函数表内部会替换成子类的虚函数地址。当父类的指针或者引用指向子类对象时候,发生多态。

  1. Animal& animal = cat;
  2. animal.speak()

 

4.7.2 多态案例一.计算器类

案例描述:

  • 分别利用普通写法和多态技术,设计实现两个操作数进行运算的计算器类

多态的优点:

  1. 代码组织结构清晰
  2. 可读性强
  3. 利于前期和后期的扩展以及维护
  1. #include <iostream>
  2. using namespace std;
  3. // 分别利用普通写法和多态技术实现计算器
  4. // 普通写法
  5. class Calculator {
  6. public:
  7. int getResult(string oper) {
  8. if(oper == "+") {
  9. return m_Num1 + m_Num2;
  10. }
  11. else if(oper == "-") {
  12. return m_Num1 - m_Num2;
  13. }
  14. else if(oper == "*") {
  15. return m_Num1 * m_Num2;
  16. }
  17. // 如果想扩展新的功能,需要修改源码
  18. // 在真实开发中 提倡 开闭原则
  19. // 开闭原则: 对扩展进行开放,对修改进行关闭
  20. }
  21. int m_Num1;// 操作数1
  22. int m_Num2;// 操作数2
  23. };
  24. void test01() {
  25. // 创建计算器对象
  26. Calculator cal;
  27. cal.m_Num1 = 10;// 操作数1为10
  28. cal.m_Num2 = 10;// 操作数2为10
  29. cout<<cal.getResult("+")<<endl;// 输出20
  30. cout<<cal.getResult("-")<<endl;// 输出0
  31. cout<<cal.getResult("*")<<endl;// 输出100
  32. }
  33. // 利用多态实现计算器
  34. // 多态好处:
  35. // 1.组织结构清晰
  36. // 2.可读性强
  37. // 3.对于前期和后期扩展以及维护性高
  38. // 实现计算器抽象类
  39. class AbstractCalculator {
  40. public:
  41. virtual int getResult() {
  42. return 0;
  43. }
  44. int m_Num1;// 操作数1
  45. int m_Num2;// 操作数2
  46. };
  47. // 加法计算器类
  48. class AddCalculator:public AbstractCalculator {
  49. public:
  50. int getResult() {
  51. return m_Num1 + m_Num2;
  52. }
  53. };
  54. // 减法计算器类
  55. class SubCalculator:public AbstractCalculator {
  56. public:
  57. int getResult() {
  58. return m_Num1 - m_Num2;
  59. }
  60. };
  61. // 乘法计算器类
  62. class MulCalculator:public AbstractCalculator {
  63. public:
  64. int getResult() {
  65. return m_Num1 * m_Num2;
  66. }
  67. };
  68. void test02() {
  69. // 多态使用条件
  70. // 父类指针或者引用指向子类对象
  71. // 加法运算
  72. AbstractCalculator* abc = new AddCalculator;
  73. abc->m_Num1 = 10;// 操作数1为10
  74. abc->m_Num2 = 100;// 操作数2为100
  75. cout<<abc->m_Num1 <<"+"<<abc->m_Num2<<"="<<abc->getResult()<<endl;// 输出20
  76. // 用完后记得销毁
  77. delete abc;
  78. abc = nullptr;
  79. // 减法运算
  80. abc = new SubCalculator;
  81. abc->m_Num1 = 10;// 操作数1为10
  82. abc->m_Num2 = 100;// 操作数2为100
  83. cout<<abc->m_Num1 <<"-"<<abc->m_Num2<<"="<<abc->getResult()<<endl;
  84. delete abc;
  85. abc = nullptr;
  86. // 乘法运算
  87. abc = new MulCalculator;
  88. abc->m_Num1 = 10;// 操作数1为10
  89. abc->m_Num2 = 100;// 操作数2为100
  90. cout<<abc->m_Num1 <<"*"<<abc->m_Num2<<"="<<abc->getResult()<<endl;
  91. delete abc;
  92. abc = nullptr;
  93. }
  94. int main() {
  95. test02();
  96. return 0;
  97. }

4.7.3 纯虚函数和抽象类

在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容。因此可以将虚函数改为纯虚函数

  • 纯虚函数语法:virtual 返回值类型 函数名(参数列表) = 0;

当类中有了纯虚函数,这个类也称为抽象类

抽象类特点:

  • 无法实例化对象
  • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
  1. #include <iostream>
  2. using namespace std;
  3. // 纯虚函数和抽象类
  4. class Base{
  5. public:
  6. // 纯虚函数
  7. // 只要有一个纯虚函数,这个类称为抽象类
  8. // 抽象类特点:
  9. // 1.无法实例化对象
  10. // 2.抽象类的子类 必须要重写父类中的纯虚函数,否则也属于抽象类
  11. virtual void func() = 0; // 纯虚函数
  12. };
  13. class Son:public Base{
  14. public:
  15. void func() { cout << "Son::func()" << endl; }// 重写父类的纯虚函数
  16. };
  17. void test01() {
  18. // Base b;// 抽象类无法实例化对象
  19. // new Base;//抽象类是无法实例化对象
  20. Son s;// 子类必须重写父类中的纯虚函数,否则无法实例化对象
  21. Base* base = new Son;
  22. base->func();// Son::func()
  23. delete base;
  24. base = NULL;
  25. }
  26. int main() {
  27. test01();
  28. return 0;
  29. }

4.7.4 多态案例二-制作饮品

案例描述:制作饮品大致流程:煮水-冲泡-倒入杯中-加入辅料

  • 利用多态技术是实现本案例,提供抽象制作饮品基类,提供子类制作咖啡和茶叶
  1. #include <iostream>
  2. using namespace std;
  3. // 多态案例2: 制作饮品
  4. class AbstractDrinking{
  5. public:
  6. // 制作饮品
  7. virtual void boil() = 0;
  8. // 冲泡
  9. virtual void brew() = 0;
  10. // 倒入杯中
  11. virtual void pourInCup() = 0;
  12. // 加入辅料
  13. virtual void putSomething() = 0;
  14. // 制作饮品
  15. void makeDrink() {
  16. boil();
  17. brew();
  18. pourInCup();
  19. putSomething();
  20. }
  21. };
  22. // 制作咖啡
  23. class Coffee:public AbstractDrinking{
  24. public:
  25. // 煮水
  26. void boil() {
  27. cout<<"煮农夫山泉"<<endl;
  28. }
  29. // 冲泡
  30. void brew() {
  31. cout<<"冲泡咖啡"<<endl;
  32. }
  33. // 倒入杯中
  34. void pourInCup() {
  35. cout<<"倒入咖啡杯中"<<endl;
  36. }
  37. // 加入辅料
  38. virtual void putSomething() {
  39. cout<<"加入牛奶和糖"<<endl;
  40. }
  41. };
  42. // 制作红茶
  43. class RedTee:public AbstractDrinking{
  44. public:
  45. // 煮水
  46. void boil() {
  47. cout<<"煮怡宝"<<endl;
  48. }
  49. // 冲泡
  50. void brew() {
  51. cout<<"冲泡茶叶"<<endl;
  52. }
  53. // 倒入杯中
  54. void pourInCup() {
  55. cout<<"倒入茶杯中"<<endl;
  56. }
  57. // 加入辅料
  58. virtual void putSomething() {
  59. cout<<"加入冰糖和柠檬"<<endl;
  60. }
  61. };
  62. // 制作函数
  63. // AbstractDrinking* drinkings = new Coffee
  64. void doWork(AbstractDrinking *drinking) {
  65. drinking->makeDrink();
  66. delete drinking;// 释放
  67. drinking = nullptr;
  68. }
  69. void test01() {
  70. // 制作咖啡
  71. doWork(new Coffee);
  72. cout<<"================"<<endl;
  73. // 制作红茶
  74. doWork(new RedTee);
  75. }
  76. int main() {
  77. test01();
  78. return 0;
  79. }

执行结果:

  1. PS D:\Work\c++\bin> ."D:/Work/c++/bin/app.exe"
  2. 煮农夫山泉
  3. 冲泡咖啡
  4. 倒入咖啡杯中
  5. 加入牛奶和糖
  6. ================
  7. 煮怡宝
  8. 冲泡茶叶
  9. 倒入茶杯中
  10. 加入冰糖和柠檬
  11. PS D:\Work\c++\bin>

4.7.5 虚析构和纯虚析构

多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码

解决方式:将父类中的析构函数改为虚析构或者纯虚析构

虚析构和纯虚析构共性:

  • 可以解决父类指针释放子类对象
  • 都需要有具体的函数实现

虚析构和纯虚析构区别:  

  • 如果是纯虚析构,该类属于抽象类,无法实例化对象

虚析构语法: virtual ~ 类名() {}

纯虚析构语法:virtual ~ 类名() = 0;

类名::类名(){}

  1. #include <iostream>
  2. using namespace std;
  3. #include <string>
  4. // 虚析构和纯虚析构
  5. class Animal{
  6. public:
  7. // 纯虚函数
  8. Animal() { cout<<"Animal 构造函数调用" <<endl; }
  9. virtual void speak() = 0;
  10. // 利用虚析构可以解决 父类指针释放子类对象的时不干净的问题
  11. // virtual ~Animal(){ cout<<"Animal 虚析构函数调用" <<endl; }
  12. // 纯虚析构 需要声明也需要实现
  13. // 有了纯虚析构之后,这个类也属于抽象类,无法实例化对象
  14. virtual ~Animal() = 0;
  15. };
  16. Animal::~Animal() {
  17. cout<<"Animal 纯虚析构函数调用" <<endl;
  18. }
  19. class Cat:public Animal{
  20. public:
  21. Cat(string name) {
  22. cout<<"Cat 构造函数调用" << endl;
  23. m_Name = new string(name); // 动态分配内存
  24. }
  25. void speak(){
  26. cout << *m_Name <<"小猫在说话" << endl;
  27. }
  28. ~Cat(){
  29. cout << "Cat 析构函数调用" << endl;
  30. if(m_Name != NULL) {
  31. delete m_Name;
  32. m_Name = NULL;
  33. cout<<"释放m_Name内存"<<endl;
  34. }
  35. }
  36. string* m_Name;
  37. };
  38. void test01() {
  39. Animal* animal = new Cat("Tom");
  40. animal->speak();
  41. // 父类指针在析构时候,不会调用子类中析构函数,导致子类如果有堆区属性,出现内存泄漏
  42. delete animal; // 父类指针指向子类对象,调用子类的析构函数
  43. animal = NULL; // 防止野指针
  44. }
  45. int main() {
  46. test01();
  47. return 0;
  48. }

执行结果:

  1. PS D:\Work\c++\bin> ."D:/Work/c++/bin/app.exe"
  2. Animal 构造函数调用
  3. Cat 构造函数调用
  4. Tom小猫在说话
  5. Cat 析构函数调用
  6. 释放m_Name内存
  7. Animal 纯虚析构函数调用
  8. PS D:\Work\c++\bin>

总结:

  1. 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象
  2. 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构
  3. 拥有纯虚析构函数的类也属于抽象类

4.7.6 多态案例三-电脑组装

案例描述:电脑主要组成部件为CPU(用于计算),显卡(用于显示),内存条(用于存储)。将每个零件封装出抽象基类,并且提供不同的厂商生产不同的零件,例如Intel厂商和Lenovo厂商。创建电脑类提供让电脑工作的函数,并且调用每个零件工作的接口,测试时组装三台不同的电脑进行工作。

  1. #include <iostream>
  2. #include <string>
  3. using namespace std;
  4. // 目的:抽象不同的零件类
  5. // 抽象的CPU类
  6. class CPU {
  7. public:
  8. // 抽象的计算函数
  9. virtual void calculate() = 0;
  10. };
  11. // 抽象的显卡类
  12. class VideoCard {
  13. public:
  14. // 抽象的显示函数
  15. virtual void display() = 0;
  16. };
  17. // 抽象的内存条类
  18. class Memory {
  19. public:
  20. // 抽象的存储函数
  21. virtual void storage() = 0;
  22. };
  23. // 具体零件厂商 Intel厂商
  24. class IntelCPU : public CPU {
  25. public:
  26. void calculate() {
  27. cout << "IntelCPU 计算中..." << endl;
  28. }
  29. };
  30. class IntelVideoCard : public VideoCard {
  31. public:
  32. void display() {
  33. cout << "IntelVideoCard 显示中..." << endl;
  34. }
  35. };
  36. class IntelMemory : public Memory {
  37. public:
  38. void storage() {
  39. cout << "IntelMemory 存储中..." << endl;
  40. }
  41. };
  42. // 具体零件厂商 Lenovo厂商
  43. class LenovoCPU : public CPU {
  44. public:
  45. void calculate() {
  46. cout<<"LenovoCPU 计算中..." << endl;
  47. }
  48. };
  49. class LenovoVideoCard : public VideoCard {
  50. public:
  51. void display() {
  52. cout << "LenovoVideoCard 显示中..." << endl;
  53. }
  54. };
  55. class LenovoMemory : public Memory {
  56. public:
  57. void storage() {
  58. cout << "LenovoMemory 存储中..."<<endl;
  59. }
  60. };
  61. // 抽象的电脑类
  62. class Computer {
  63. public:
  64. // 电脑需要CPU、显卡、内存条
  65. // 构造函数中,传入三个零件指针
  66. Computer(CPU* cpu, VideoCard* videoCard, Memory* memory){
  67. m_cpu = cpu;
  68. m_videoCard = videoCard;
  69. m_memory = memory;
  70. }
  71. // 提供工作的函数,调用每个零件工作的接口
  72. void doWork() {
  73. m_cpu->calculate();
  74. m_videoCard->display();
  75. m_memory->storage();
  76. }
  77. // 提供析构函数 释放三个电脑零件
  78. ~Computer() {
  79. cout << "Computer 析构" << endl;
  80. if(m_cpu != NULL) { // 释放CPU零件
  81. delete m_cpu;
  82. m_cpu = NULL;
  83. cout<<"释放m_cpu"<<endl;
  84. }
  85. if(m_videoCard != NULL) { // 释放显卡零件
  86. delete m_videoCard;
  87. m_videoCard = NULL;
  88. cout<<"释放m_videoCard"<<endl;
  89. }
  90. if(m_memory != NULL) { // 释放内存条零件
  91. delete m_memory;
  92. m_memory = NULL;
  93. cout<<"释放m_memory"<<endl;
  94. }
  95. }
  96. private:
  97. CPU* m_cpu; // CPU的零件指针
  98. VideoCard* m_videoCard; // 显卡零件指针
  99. Memory* m_memory; // 内存条零件指针
  100. };
  101. void test01() {
  102. // 第一台电脑组装和工作~
  103. cout<<"第一台电脑组装和工作~~"<<endl;
  104. CPU* cpu = new LenovoCPU;
  105. VideoCard* videoCard = new LenovoVideoCard;
  106. Memory* memory = new LenovoMemory;
  107. // 创建一个Lenovo电脑
  108. Computer* computer1 = new Computer(cpu, videoCard, memory);
  109. computer1->doWork();
  110. delete computer1;
  111. computer1 = NULL;
  112. cout<<"===================================="<<endl;
  113. // 第二台电脑组装和工作~
  114. cout<<"第二台电脑组装和工作~"<<endl;
  115. cpu = new IntelCPU;
  116. videoCard = new IntelVideoCard;
  117. memory = new IntelMemory;
  118. // 创建一个Intel电脑
  119. Computer* computer2 = new Computer(cpu, videoCard, memory);
  120. computer2->doWork();
  121. delete computer2;
  122. computer2 = NULL;
  123. cout<<"===================================="<<endl;
  124. // 第三台电脑组装和工作~
  125. cout<<"第三台电脑组装和工作~"<<endl;
  126. cpu = new IntelCPU;
  127. videoCard = new LenovoVideoCard;
  128. memory = new IntelMemory;
  129. // 创建一个Intel电脑
  130. Computer* computer3 = new Computer(cpu, videoCard, memory);
  131. computer3->doWork();
  132. delete computer3;
  133. computer3 = NULL;
  134. }
  135. int main() {
  136. test01();
  137. return 0;
  138. }

执行结果:

  1. PS D:\Work\c++\bin> ."D:/Work/c++/bin/app.exe"
  2. 第一台电脑组装和工作~~
  3. LenovoCPU 计算中...
  4. LenovoVideoCard 显示中...
  5. LenovoMemory 存储中...
  6. Computer 析构
  7. 释放m_cpu
  8. 释放m_videoCard
  9. 释放m_memory
  10. ====================================
  11. 第二台电脑组装和工作~
  12. IntelCPU 计算中...
  13. IntelVideoCard 显示中...
  14. IntelMemory 存储中...
  15. Computer 析构
  16. 释放m_cpu
  17. 释放m_videoCard
  18. 释放m_memory
  19. ====================================
  20. 第三台电脑组装和工作~
  21. IntelCPU 计算中...
  22. LenovoVideoCard 显示中...
  23. IntelMemory 存储中...
  24. Computer 析构
  25. 释放m_cpu
  26. 释放m_videoCard
  27. 释放m_memory
  28. PS D:\Work\c++\bin>

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/运维做开发/article/detail/864304
推荐阅读
相关标签
  

闽ICP备14008679号