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

news2024/11/16 19:27:53

4.类和对象

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

例如:

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

4.1 封装

4.1.1 封装的意义

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

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

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

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

  1. 属性 -> 成员属性/成员变量
  2. 行为 -> 成员函数/成员方法
  • 示例1:设计一个圆类,求圆的周长
  • 类和对象-封装-属性和行为作为整体.cpp
#include <iostream>
using namespace std;
// 圆周率
const double PI = 3.1415926;
// 设计一个圆类,求圆的周长
// 圆求周长的公式 : 2 * PI * 半径
class Circle {
    // 访问权限
    // 公共权限
public:
    // 行为
    // 获取圆的周长
    double calculatePerimeter(double radius) {
        return 2 * PI * radius;
    }
    // 属性:半径
    int m_radius;
};
int main() {
    // 通过圆类,创建具体的圆(对象)
    // 实例化(通过一个类 创建一个对象的过程)
    Circle c1;
    c1.m_radius = 10;
    cout<<"圆的周长为 : "<<c1.calculatePerimeter(c1.m_radius)<<""<<endl;
    return 0;
}
  • 示例2:设计学生类
#include <iostream>
using namespace std;
#include <string>
// 设计一个学生类,属性有姓名和学号
// 可以给姓名和学号赋值,可以显示学生的姓名和学号

// 设计学生类
class Student{
public://公共权限
    // 类中的属性和行为 我们统一称为成员
    // 属性 -> 成员属性/成员变量
    // 行为 -> 成员函数/成员方法
    string m_name; // 姓名
    int m_Id; // 学号

    // 行为
    void setName(string name){ // 设置姓名
        m_name = name;
    }
    void setId(int id){ // 设置学号
        m_Id = id;
    }
    void display(){ // 显示姓名和学号
        cout << "姓名:" << m_name << endl;
        cout << "学号:" << m_Id << endl;
    }
};

int main() {
    // 创建一个具体学生 实例化对象
    Student s1;
    // 给s1对象 进行属性赋值操作
    s1.m_name = "张三";
    s1.m_Id = 2019001;
    // 显示学生信息
    s1.display();

    s1.setName("李四");
    s1.setId(2019002);
    s1.display();
}

运行结果:

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

封装意义二:

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

访问权限有三种:

  1. public 公共权限
  2. protected 保护权限
  3. private 私有权限
#include <iostream>
using namespace std;

/*
    三种访问权限:
    公共权限 public        成员  类内可以访问   类外可以访问
    保护权限 protected     成员  类内可以访问   类外不可以访问(儿子可以访问父亲中的保护内容)
    私有权限 private       成员  类内可以访问   类外不可以访问(儿子不可以访问父亲中的私有内容)
*/

class Person{
public:
    void func() {
        m_Name = "张三";//公共权限
        m_Car = "拖拉机";//保护权限
        m_Password = 123456;//私有权限
    }
public:
    // 公共权限
    string m_Name;//姓名
protected:
    // 保护权限
    string m_Car;//汽车
private:
    // 私有权限
    int m_Password;//密码
};

int main() {
    // 实例化具体对象
    Person p1;
    p1.m_Name = "呵呵哒";//类外可以访问(public)
    // p1.m_Car = "保时捷";//保护权限内容,在类外访问不到 error:成员"Person::m_Car"不可访问
    // p1.m_Password = 123456;//私有权限内容,在类外访问不到 error:成员"Person::m_Password"不可访问
    p1.func();//类外可以访问(public)
    return 0;
}

4.1.2 structclass区别

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

区别:

  • struct默认权限是公共权限
  • class默认权限是私有权限
#include <iostream>
using namespace std;

class C1{
    int m_A;// 默认权限 是私有
};

struct C2{
    int m_A;// 默认权限 是公共
};

int main() {
    /*
        在C++中struct和class唯一的区别就是默认的访问权限不同
        区别:
            struct默认权限是公共权限
            class默认权限是私有权限
    */

    C1 c1;
    // c1.m_A = 10;// error:成员"C1::m_A"不可访问
    C2 c2;
    c2.m_A = 100;// ok 
    return 0;
}

4.1.3 成员属性设置为私有

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

演示:控制读写权限 

#include <iostream>
using namespace std;
/*
    成员属性设置私有
        优点1:将所有成员属性设置为私有,可以自己控制读写权限
        优点2:对于写权限,我们可以检测到数据的有效性
*/
// 人类
class Person{
public:
    // 设置姓名
    void setName(string name) {
        m_Name = name;
    }
    // 获取姓名
    string getName() const {// 返回类型为const,表示返回的是常量,不可修改返回值。
        return m_Name;// 返回m_Name的值。
    }
    // 获取年龄
    int getAge() const {// 返回类型为const,表示返回的是常量,不可修改返回值。
        return m_Age;// 返回m_Age的值。
    }
    // 设置偶像
    void setIdol(string idol) {
        m_Idol = idol;
    }
private:
    string m_Name;// 姓名 可读可写
    int m_Age = 18;// 年龄  只读
    string m_Idol;// 偶像 只写
};

int main() {
    Person p;
    // 姓名设置
    p.setName("张三");
    cout<<"姓名:"<<p.getName()<<endl;// 姓名:张三

    // 获取年龄
    cout<<"年龄:"<<p.getAge()<<endl;// 输出年龄:18

    // 偶像设置
    p.setIdol("迪丽热巴");
    return 0; // 程序执行成功,返回0
}

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

#include <iostream>
using namespace std;
/*
    成员属性设置私有
        优点1:将所有成员属性设置为私有,可以自己控制读写权限
        优点2:对于写权限,我们可以检测到数据的有效性
*/
// 人类
class Person{
public:
    // 设置姓名
    void setName(string name) {
        m_Name = name;
    }
    // 获取姓名
    string getName() const {// 返回类型为const,表示返回的是常量,不可修改返回值。
        return m_Name;// 返回m_Name的值。
    }
    // 设置年龄 0-150
    void setAge(int age) {
        if (age < 0 || age > 150) {
            cout << "年龄: "<< age << ",输入错误!" << endl;// 输出错误信息。
            return;
        }
        m_Age = age;
    }
    // 获取年龄
    int getAge() const {// 返回类型为const,表示返回的是常量,不可修改返回值。
        return m_Age;// 返回m_Age的值。
    }
    // 设置偶像
    void setIdol(string idol) {
        m_Idol = idol;
    }
private:
    string m_Name;// 姓名 可读可写
    int m_Age = 18;// 年龄  只读
    string m_Idol;// 偶像 只写
};

int main() {
    Person p;
    // 姓名设置
    p.setName("张三");
    cout<<"姓名:"<<p.getName()<<endl;// 姓名:张三

    // 获取年龄
    p.setAge(250);
    cout<<"年龄:"<<p.getAge()<<endl;// 输出年龄:18

    // 偶像设置
    p.setIdol("迪丽热巴");
    return 0; // 程序执行成功,返回0
}

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

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

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

#include <iostream>
using namespace std;

/*
    立方体类设计
    1.创建立方体类
    2.设计属性
    3.设计行为 获取立方体面积和体积
    4.分别利用全局函数和成员函数 判断两个立方体是否相等
*/
class Cube{
public:
    // 设置长
    void setL(int l){m_L=l;}
    // 获取长
    int getL(){return m_L;}
    // 设置宽
    void setW(int w){m_W=w;}
    // 获取宽
    int getW(){return m_W;}
    // 设置高
    void setH(int h){m_H=h;}
    // 获取高
    int getH(){return m_H;}
    // 获取立方体面积
    int getArea(){return 2*(m_L*m_W+m_L*m_H+m_W*m_H);}

    // 获取立方体体积
    int getVolume(){return m_L*m_W*m_H;}

    // 利用成员函数判断两个立方体是否相等
    bool isSameByClass(Cube &c){
        if(getL() == c.getL() && getW() == c.getW() && getH() == c.getH()) {
            return true;
        }else{
            return false;
        }
    }
private:
    int m_L;//长
    int m_W;//宽
    int m_H;//高
};

// 利用全局函数判断 两个立方体是否相等
bool isSame(Cube &c1,Cube &c2) {
    if(c1.getL() == c2.getL() && c1.getW() == c2.getW() && c1.getH() == c2.getH()) 
        return true; // 判断长宽高是否相等,如果相等则返回true,否则返回false
    else return false;
}

int main() {
    Cube c1,c2;
    c1.setL(5);
    c1.setW(4);
    c1.setH(3);
    c2.setL(10);
    c2.setW(10);
    c2.setH(10);
    cout<<"c1的面积为:"<<c1.getArea()<<endl;
    cout<<"c1的体积为:"<<c1.getVolume()<<endl;
    cout<<"***************************"<<endl;
    cout<<"c2的面积为:"<<c2.getArea()<<endl;
    cout<<"c2的体积为:"<<c2.getVolume()<<endl;

    // 判断c1和c2是否相等
    // 利用全局函数判断
    bool ret = isSame(c1,c2);
    if(ret) cout<<"利用全局函数判断:c1和c2相等"<<endl;
    else cout<<"利用全局函数判断:c1和c2不相等"<<endl;

    // 利用成员函数判断
    ret = c1.isSameByClass(c2);
    if(ret) cout<<"利用成员函数判断:c1和c2相等"<<endl;
    else cout<<"利用成员函数判断:c1和c2不相等"<<endl;
    return 0;
}

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

#include <iostream>
using namespace std;

// 点和圆关系案例

// 点类
class Point{
public:
    // 设置x
    void setX(int x) {m_X = x;}
    // 获取x
    int getX() {return m_X;}
    // 设置y
    void setY(int y) {m_Y = y;}
    // 获取y
    int getY() {return m_Y;}
private:
    int m_X;
    int m_Y;
};

// 圆类
class Circle {
public: 
    // 设置半径
    void setR(int r) {m_R = r;}
    // 获取半径
    int getR() {return m_R;}
    // 设置圆心
    void setCenter(Point center) {m_Center = center;}
    // 获取圆心
    Point getCenter() {return m_Center;}
private:
    int m_R;// 半径
    // 在类中可以让另一个类 作为本类中的成员
    Point m_Center;// 圆心
};
// 判断点和圆关系
void isInCircle(Circle& c, Point& p)  {
    // 计算两点之间距离 平方
    int distance = 
    (c.getCenter().getX() - p.getX()) * (c.getCenter().getX() - p.getX()) +
    (c.getCenter().getY() - p.getY()) * (c.getCenter().getY() - p.getY());
    // 计算半径的平方
    int rDistance = c.getR() * c.getR();

    // 判断关系
    if(distance == rDistance) {
        cout<<"点在圆上"<<endl;
    }
    else if(distance < rDistance) {
        cout<<"点在圆内"<<endl;  
    }
    else {
        cout<<"点在圆外"<<endl;
    }
}
int main() {
    // 创建圆
    Circle c;
    c.setR(10);
    Point center;
    center.setX(10);
    center.setY(0);
    c.setCenter(center);

    // 创建点
    Point p;
    p.setX(10);
    p.setY(9);
    // 判断关系
    isInCircle(c,p);
    return 0;
}

 进一步完善项目

  • point.h
#pragma once
// 点类
class Point{
public:
    // 设置x
    void setX(int x);
    // 获取x
    int getX();
    // 设置y
    void setY(int y);
    // 获取y
    int getY();
private:
    int m_X;
    int m_Y;
};
  • point.cpp
#include "point.h"

// 设置x
void Point::setX(int x) {m_X = x;}
// 获取x
int Point::getX() {return m_X;}
// 设置y
void Point::setY(int y) {m_Y = y;}
// 获取y
int Point::getY() {return m_Y;}
  •  circle.h
#pragma once
#include "point.h"
// 圆类
class Circle {
public: 
    // 设置半径
    void setR(int r);
    // 获取半径
    int getR();
    // 设置圆心
    void setCenter(Point center);
    // 获取圆心
    Point getCenter();
private:
    int m_R;// 半径
    // 在类中可以让另一个类 作为本类中的成员
    Point m_Center;// 圆心
};
  • circle.cpp
#include "circle.h"
// 设置半径
void Circle::setR(int r) {m_R = r;}
// 获取半径
int Circle::getR() {return m_R;}
// 设置圆心
void Circle::setCenter(Point center) {m_Center = center;}
// 获取圆心
Point Circle::getCenter() {return m_Center;}

4.2 对象的初始化和清理

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

4.2.1 构造函数和析构函数

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

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

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

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

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

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

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

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

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

// 构造和析构都是必须有的实现,如果我们不提供,编译器会提供一个空实现的构造和析构
void test01() {
    Person p;// 在栈上的数据,test01执行完毕后,释放这个对象
}

int main() {
    test01();
    return 0;
}

执行结果:

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

4.2.2 构造函数的分类及调用

两种分类方式:

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

三种调用方式:

  • 括号法
  • 显示法
  • 隐式转换法
#include <iostream>
using namespace std;

// 1.构造函数的分类及调用
// 分类
//    按照参数分类      无参构造(默认构造)和有参构造
//    按照类型分类      普通构造 拷贝构造
class Person {
public:
    // 无参构造函数
    Person(){cout<<"Person的构造函数调用"<<endl;}
    // 有参构造函数
    Person(int age) {
        m_age=age;
        cout<<"Person的构造函数调用"<<endl;
    }
    // 拷贝构造函数
    Person(const Person& p) {
        // 将传入的人身上的所有属性,拷贝到我身上
        m_age = p.m_age;
        cout<<"Person的拷贝构造函数调用"<<endl;
    }
    ~Person(){cout<<"Person的析构函数调用"<<endl;}
    int m_age;
};

void test01() {
    // 1. (括号法)
    Person p1;// 默认构造调用
    Person p2(10);// 有参构造调用
    Person p3(p2);// 拷贝构造调用
    cout<< "p2的年龄是: "<<p2.m_age<<endl;
    cout<< "p3的年龄是: "<<p3.m_age<<endl;

    // 注意事项1
    // 调用默认构造函数时候,不要加()
    // 因为下面这行代码,编译器会认为是一个函数的声明,不会认为在创建对象
    // Person p1();
    // void func();
}

void test02() {
    // 2.显示法
    Person p1;
    Person p2 = Person(10);// 有参构造
    Person p3 = Person(p2);// 拷贝构造

    Person(20);// 匿名对象 特点:当前行执行结束后,系统会立即回收掉匿名对象
    cout<<"asasasa"<<endl;

    // 注意事项2
    // 不要利用拷贝构造函数 初始化匿名对象
    // 编译器会认为Person(p3) == Person p3;
    // 会认为这是一个对象的声明
    // Person(p3); // 此时重定义了
}

void test03() {
    // 3,隐式转换法
    Person p4 = 10;// 相当于 写了 Person p4 = Person(10); 有参构造
    Person p5 = p4;// 拷贝构造
}

// 调用
int main() {
    test02();
    return 0;
}

4.2.3 拷贝构造函数调用时机

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

  1. 使用一个已经创建完毕的对象来初始化一个新对象
  2. 值传递的方式给函数参数传值
  3. 以值方式返回局部对象
#include<iostream>
using namespace std;

// 拷贝构造函数调用时机
class Person {
public:
    Person() {cout<<"Person默认构造函数调用"<<endl;}
    Person(int age) : m_Age(age) {cout<<"Person有参构造函数调用"<<endl;}
    Person(const Person& p) {
        m_Age = p.m_Age;
        cout<<"Person拷贝构造函数调用"<<endl;
    }
    ~Person() {cout<<"Person析构函数调用"<<endl;}
    int m_Age;
private:

};

// 1.使用一个已经创建完毕的对象来初始化一个新对象
void test01() {
    Person p1(20);
    Person p2(p1);

    cout<<"p2: "<<p2.m_Age<<endl;
    /*
        Person有参构造函数调用
        Person拷贝构造函数调用
        p2: 20
        Person析构函数调用
        Person析构函数调用
    */
}

// 2.值传递的方式给函数参数传值
void doWork(Person p) {// 值传递会拷贝一个临时的副本出来,在调用它的拷贝构造函数,

}

void test02() {
    Person p;
    doWork(p);
    /*
        Person默认构造函数调用
        Person拷贝构造函数调用
        Person析构函数调用
        Person析构函数调用
    */
}

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

int main() {
    test03();
    return 0;
}

4.2.4 构造函数调用规则

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

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

构造函数调用规则如下:

  • 如果用户定义有参构造函数,C++不再提供默认无参构造,但是会提供默认拷贝构造
  • 如果用户定义拷贝构造函数,C++不会再提供其他构造函数
#include <iostream>
using namespace std;

// 构造函数的调用规则
// 1.创建一个类,C++编译器会给每个类都添加至少3个函数
// 默认构造(空实现)
// 析构函数(空实现)
// 拷贝构造(值拷贝)

// 2.如果我们写了有参构造函数,编译器就不再提供默认构造,依然提供拷贝构造
// 如果我们写了拷贝构造函数,编译器就不再提供其他普通构造函数了
class Person {
public:
    // 默认构造
    Person() { cout << "Person的默认构造函数调用" << endl; }
    // 有参构造
    Person(int age) {
        m_Age = age; 
        cout << "Person的有参构造函数调用" << endl; 
    }
    // 拷贝构造
    // Person(const Person& p) {
    //     m_Age = p.m_Age;
    //     cout << "Person的拷贝构造函数调用" << endl;
    // }
    // 析构函数
    ~Person() { cout << "Person的析构函数调用" << endl;}
    int m_Age;
};

void test01() {
    Person p;
    p.m_Age = 18;
    Person p2(p);
    cout<<"p2的年龄为: "<<p2.m_Age<<endl;
}


int main() {
    test01();
    return 0;
}

执行结果:

PS D:\Work\c++\bin> ."D:/Work/c++/bin/app.exe"
Person的默认构造函数调用
p2的年龄为: 18
Person的析构函数调用
Person的析构函数调用
  • 如果我们写了拷贝构造函数,编译器就不再提供其他普通构造函数了
#include <iostream>
using namespace std;

// 构造函数的调用规则
// 1.创建一个类,C++编译器会给每个类都添加至少3个函数
// 默认构造(空实现)
// 析构函数(空实现)
// 拷贝构造(值拷贝)

// 2.如果我们写了有参构造函数,编译器就不再提供默认构造,依然提供拷贝构造
// 如果我们写了拷贝构造函数,编译器就不再提供其他普通构造函数了
class Person {
public:
    // 默认构造
    Person() { cout << "Person的默认构造函数调用" << endl; }
    // 有参构造
    Person(int age) {
        m_Age = age; 
        cout << "Person的有参构造函数调用" << endl; 
    }
    // 拷贝构造
    Person(const Person& p) {
        m_Age = p.m_Age;
        cout << "Person的拷贝构造函数调用" << endl;
    }
    // 析构函数
    ~Person() { cout << "Person的析构函数调用" << endl;}
    int m_Age;
};

void test01() {
    Person p;
    p.m_Age = 18;
    Person p2(p);
    cout<<"p2的年龄为: "<<p2.m_Age<<endl;
}

int main() {
    test01();
    return 0;
}

执行结果:

Person的默认构造函数调用
Person的拷贝构造函数调用
p2的年龄为: 18
Person的析构函数调用
Person的析构函数调用
  •  如果我们写了有参构造函数,编译器就不再提供默认构造,依然提供拷贝构造
#include <iostream>
using namespace std;

// 构造函数的调用规则
// 1.创建一个类,C++编译器会给每个类都添加至少3个函数
// 默认构造(空实现)
// 析构函数(空实现)
// 拷贝构造(值拷贝)

// 2.如果我们写了有参构造函数,编译器就不再提供默认构造,依然提供拷贝构造
// 如果我们写了拷贝构造函数,编译器就不再提供其他普通构造函数了
class Person {
public:
    // 默认构造
    Person() { cout << "Person的默认构造函数调用" << endl; }
    // 有参构造
    Person(int age) {
        m_Age = age; 
        cout << "Person的有参构造函数调用" << endl; 
    }
    // 拷贝构造
    // Person(const Person& p) {
    //     m_Age = p.m_Age;
    //     cout << "Person的拷贝构造函数调用" << endl;
    // }
    // 析构函数
    ~Person() { cout << "Person的析构函数调用" << endl;}
    int m_Age;
};

void test02() {
    Person p(18);
    Person p2(p);
}

int main() {
    test02();
    return 0;
}

执行结果:

Person的有参构造函数调用
Person的析构函数调用
Person的析构函数调用

4.2.5 深拷贝与浅拷贝

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

  • 浅拷贝:简单的赋值拷贝操作
  • 深拷贝:在堆区重新申请空间,进行拷贝操作
#include<iostream>
using namespace std;

// 深拷贝与浅拷贝
class Person {
public:
    Person() { cout<<"Person的默认构造函数调用"<<endl; }
    Person(int age,int height) { 
        m_Age = age;
        m_Height = new int(height);
        cout<<"Person的有参构造函数调用"<<endl; 
    }
    // 自己实现拷贝构造函数,解决浅拷贝带来的问题
    Person(const Person& p) {
        cout<<"Person的拷贝构造函数调用"<<endl;
        m_Age = p.m_Age;
        // m_Height = p.m_Age; // 编译器默认实现就是这行代码
        
        // 深拷贝操作
        m_Height = new int(*p.m_Height); // 在堆区创建一块内存
    }
    ~Person(){ 
        // 析构代码,将堆区开辟数据做释放操作
        if(m_Height != NULL) {
            delete m_Height;
            m_Height = NULL;
        }
        cout<<"Person的析构函数调用"<<endl; 
    }
    int m_Age;
    int* m_Height;// 身高
};

void test01() {
    // 深拷贝,p1走p1的析构,p2走p2的析构.注意:堆栈是先进后出的
    // 浅拷贝,有交叉重复释放的问题
    Person p1(18,160);
    cout<<"p1的年龄: "<<p1.m_Age<<"身高: "<<*p1.m_Height<<endl;
    Person p2(p1);// 默认的拷贝构造函数(浅拷贝)
    cout<<"p2的年龄: "<<p2.m_Age<<"身高: "<<*p2.m_Height<<endl;
}

int main() {
    test01();
    return 0;
}

执行结果:

Person的有参构造函数调用
p1的年龄: 18身高: 160
Person的拷贝构造函数调用
p2的年龄: 18身高: 160
Person的析构函数调用
Person的析构函数调用

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

4.2.6 初始化列表

  • 作用:C++提供了初始化列表语法,用来初始化属性
  • 语法:构造函数():属性1(值1),属性2(值2),…{}
#include <iostream>
using namespace std;

// 初始化列表
class Person {
public:
    // 传统初始化操作
    // Person(int a,int b,int c) {
    //     m_A = a;
    //     m_B = b;
    //     m_C = c;
    // }
    // 初始化列表初始化属性
    Person(int a,int b,int c):m_A(a),m_B(b),m_C(c) { }
    int m_A;
    int m_B;
    int m_C;
};

void test01() {
    Person p(1,2,3);
    cout << p.m_A << " " << p.m_B << " " << p.m_C << endl; // 输出:1 2 3
}

int main() {

    return 0;
}

4.2.7 类对象作为类成员

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

例如:

class A{};

class B{A a};

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

  • 当其他类对象作为本类成员,构造时候先构造类对象,再构造自身,析构的顺序与构造相反
#include <iostream>
using namespace std;
#include <string>
// 类对象作为类成员

// 手机类
class Phone {
public:
    Phone(string pName) {
        m_PName = pName;
        cout<<"Phone有参构造调用"<<endl;
    }
    ~Phone() { cout<<"Phone的析构函数调用"<<endl; }
    string m_PName;// 品牌名称
};

// 人类
class Person {
public:
    // Phone m_Phone = pName 隐式转换法
    Person(string name, string pName) : m_name(name), m_phone(pName) {
        cout<<"Person有参构造调用"<<endl;
    }
    ~Person() { cout<<"Person的析构函数调用"<<endl; }
    // 姓名
    string m_name;
    // 手机
    Phone m_phone;
private:

};

// 当其他类对象作为本类成员,构造时候先构造类对象,再构造自身,析构的顺序与构造相反
void test01() {
    Person p("张三", "苹果");
    cout << "姓名:" << p.m_name << " 手机品牌:" << p.m_phone.m_PName << endl;
}

int main() {
    test01();
    return 0;
}

执行结果:

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

4.2.8 静态成员

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

静态成员分为:

静态成员变量

  • 所有对象共享同一份数据
  • 在编译阶段分配内存
  • 类内声明,类外初始化
#include <iostream>
using namespace std;

// 静态成员变量
class Person {
public:
    // 1.所有对象都共享同一份数据
    // 2.编译阶段就分配内存
    // 3.类内声明,类外初始化操作
    static int m_A; // 静态成员变量

    // 静态成员变量也是有访问权限的
private:
    static int m_B; // 静态成员变量
};

int Person::m_A = 100; // 类外初始化操作
int Person::m_B = 200; // 类外初始化操作

void test01() {
    Person p;
    cout << "m_A = " << p.m_A << endl; // 100

    Person p2;
    p2.m_A = 400;
    cout << "m_A = " << p.m_A << endl; // 400
}

void test02() {
    // 静态成员变量,不属于某个对象上,所有对象都共享同一份数据
    // 因此静态成员变量有两种访问方式
    // 1.通过对象进行访问
    // Person p;
    // cout<<"p.m_A = "<<p.m_A<<endl; // 100
    // 2.通过类名进行访问
    cout << "m_A = " << Person::m_A << endl; // 100
    // 类外访问不到私有静态成员变量
    // cout << "m_B = " << Person::m_B << endl; // error:成员"Person::m_B"不可访问
}

int main() {
    test02();
    return 0;
}

静态成员函数

  • 所有对象共享同一个函数
  • 静态成员函数只能访问静态成员变量
#include <iostream>
using namespace std;

// 静态成员函数
// 所有对象共享同一个函数
// 静态成员函数只能访问静态成员变量
/*
    因为m_B必须通过创建对象才能访问,得创建一个对象,
    才能够去读/写这块内存.当你去调用静态成员函数func()
    这个函数体的内部不知道改变的是哪个对象的m_B
    非静态成员变量属于特定对象的成员变量
*/
class Person {
public:
    // 静态成员函数
    static void func() { 
        m_A = 100;//静态成员函数是可以访问静态成员变量
        // m_B = 200;//静态成员函数是不可以访问非静态成员变量的,无法区分到底是哪个对象的m_B属性
        cout<<"static void func调用"<<endl;
    }
    static int m_A;// 静态成员变量
    int m_B;// 非静态成员变量

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

int Person::m_A = 0;

// 有两种访问方式
void test01() {
    // 1.通过对象访问
    Person p;
    p.func();

    // 2.通过类名访问(类外访问不到私有静态成员函数)
    Person::func();
    // Person::func2();// 错误,不可访问
}

int main() {
    test01();
    return 0;
}

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

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

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

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

#include <iostream>
using namespace std;

// 成员变量和成员函数是分开存储的
class Person {
public:
    int m_A;// 非静态成员变量 属于类的对象上
    static int m_B;// 静态成员变量 不属于类的对象上
    void func(){} // 非静态成员函数 不属于类的对象上
    static void func2(){} // 静态成员函数 不属于类的对象上
};

int Person::m_B = 100;// 初始化静态成员变量

void test01() {
    Person p;
    // 空对象占用内存空间为:1
    // C++编译器会给每个空对象特分配一个字节空间,是为了区分空对象
    // 占内存的位置
    // 每个空对象也应该有一个独一无二的内存地址
    cout<<"size of p = " <<sizeof(p)<<endl; // 1
}

void test02() {
    Person p;
    cout<<"size of p = " <<sizeof(p)<<endl; // 4
}

int main() {
    test02();
    return 0;
}

4.3.2 this指针概念

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

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

this指针的用途:

  • 当形参和成员变量同名时,可用this指针来区分
  • 在类的非静态成员函数中返回对象本身,可使用return *this
#include <iostream>
using namespace std;
class Person {
public:
    Person(int age) {
        // this指针指向的是被调用的成员函数 所属的对象
        this->m_Age = age;
    }
    Person& PersonAddPerson(Person& p) {
        this->m_Age += p.m_Age;

        // this指向p2的指针,而*this指向的就是p2这个对象本体
        return *this; // 返回对象本身
    }
    int m_Age;
};
// 1.解决名称冲突
void test01() {
    Person p1(18); // 创建一个Person对象,年龄为18
    cout <<"p1的年龄为: "<<p1.m_Age << endl; // 输出Person对象的年龄
}

// 2.返回对象本身用*this
void test02() {
    Person p1(10); // 创建一个Person对象,年龄为10
    Person p2(20); // 创建另一个Person对象,年龄为20
    // 链式编程思想
    p2.PersonAddPerson(p1).PersonAddPerson(p1).PersonAddPerson(p1); 
    cout<<"p2的年龄为: "<<p2.m_Age<<endl; 
}

int main() {    
    test02();
    return 0;
}

4.3.3 空指针访问成员函数

  • C++中空指针也是可以调用成员函数的,但是也要注意有没有用到this指针
  • 如果用到this指针,需要加以判断保证代码的健壮性
#include <iostream>
using namespace std;

// 空指针调用成员函数
class Person {
public:
    void showClassName() {
        cout<<"this is Person Class"<<endl;
    }
    void showPersonAge() {
        // 常见报错原因:传入的指针是NULL
        if(this == NULL) return;
        cout<<"Age = "<<this->m_Age<<endl;
    }
    int m_Age;
};

void test01() {
    Person *p = NULL;
    p->showClassName();
    p->showPersonAge();
}

int main() {
    test01();
    return 0;
}

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 
  • 常对象不可以调用普通成员函数,因为普通成员函数可以修改属性
#include <iostream>
using namespace std;

// 常函数
class Person {
public:
    Person();
    // this指针的本质 是指针常量 指针的指向是不可以修改的,指针指向的值是可以修改的
    // Person * const this;
    // const Person * const this; 指针的指向的值也不可以修改了
    // 在成员函数后面加const,修饰的是this指向,让指针指向的
    // 值也不可以修改
    void showPerson() const {// 常函数
        // this->m_A = 10;
        // this 指针是不可以修改指针的指向的
        // this = NULL;// error 分配到"this"(记时错误)

        this->m_B = 100;
    }

    void func() {
        m_A = 100;
    }
    int m_A;
    mutable int m_B;// 特殊变量,即使在常函数中,也可以修改这个值.需要加上关键字mutable
};

void test01() {
    Person p;
    p.showPerson();
}

// 常对象
void test02() {
    const Person p;// 在对象前加上const,变为常对象
    // p.m_A = 100;//error
    p.m_B = 100;// m_B是特殊值,在常对象也可以修改

    // 常对象只能调用常函数
    p.showPerson();
    // p.func();// error:常对象不可以调用普通成员函数,因为普通成员函数可以修改属性
}

int main() {

    return 0;
}

4.4 友元

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

友元的三种实现

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

全局函数做友元 

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

// 建筑物
class Building {
    // 告诉编译器 goodGay全局函数是 Building好朋友,可以访问Building中私有成员
    friend void goodGay(Building *building);
public:
    Building() {
        m_SittingRoom = "客厅";
        m_BedRoom = "卧室";
    }
public:
    string m_SittingRoom; // 客厅
private:
    string m_BedRoom; // 卧室
};

// 全局函数
void goodGay(Building *building) {
    cout<<"好基友全局函数 正在访问 : "<<building->m_SittingRoom<<endl;
    cout<<"好基友全局函数 正在访问 : "<<building->m_BedRoom<<endl;
}

void test01() {
    Building building;
    goodGay(&building); // 调用全局函数
}

int main() {
    test01();
    return 0; // 返回0表示正常退出
}

执行结果:

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

类做友元

#include <iostream>
using namespace std;
#include <string>
// 类做友元
class Building;
class GoodGay
{
public:
    GoodGay();
    void visit();// 参观函数 访问Building中的属性
    Building *building;
};

class Building{
    // 告诉编译器 GoodGay 类是本来的好朋友,可以访问本类中私有成员
    friend class GoodGay;
public:
    Building();
public:
    string m_SittingRoom;// 客厅
private:
    string m_BedRoom;// 卧室
};

// 类外写成员函数
Building::Building() {
    m_SittingRoom = "客厅";
    m_BedRoom = "卧室";
}

GoodGay::GoodGay() {
    // 创建建筑物对象
    building = new Building;
}

void GoodGay::visit() {
    cout << "好基友正在访问" << building->m_SittingRoom << endl;
    cout << "好基友正在访问" << building->m_BedRoom << endl;
}

void test01() {
    GoodGay gg;
    gg.visit();
}

int main() {
    test01();
    return 0;
}

成员函数做友元 

#include <iostream>
using namespace std;
#include <string>
class Building;
class GoodGay{
public:
    GoodGay();
    void visit();// 让visit函数可以访问Building中私有成员
    void visit2();// 让visit2函数不可以访问Building中私有成员
    Building* building;
};

class Building{
    // 告诉编译器 GoodGay类下的visit成员函数作为本类的好朋友,可以访问私有成员
    friend void GoodGay::visit();
public:
    Building();
    string m_SittingRoom; // 客厅
private:
    string m_BedRoom; // 卧室
};

// 类外实现成员函数
Building::Building(){
    m_SittingRoom = "客厅";
    m_BedRoom = "卧室";
}

GoodGay::GoodGay(){
    building = new Building;
}

void GoodGay::visit() {
    cout<<"visit 函数正在访问: "<<building->m_SittingRoom<<endl;
    cout<<"visit 函数正在访问: "<<building->m_BedRoom<<endl;
}

void GoodGay::visit2() {
    cout<<"visit 函数正在访问: "<<building->m_SittingRoom<<endl;    
}

void test01() {
    GoodGay gg;
    gg.visit();
    gg.visit2();
}

int main() {
    test01();
    return 0;
}

4.5  运算符重载

4.5.1 加号运算符重载

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

总结:

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

  • 加号运算符重载 

#include <iostream>
using namespace std;

// 加号运算符重载
class Person {
public:
    // 1. 成员函数重载 + 号
    Person operator+(Person &p) {
        Person temp;
        temp.m_A = this->m_A + p.m_A;
        temp.m_B = this->m_B + p.m_B;
        return temp;
    }
    int m_A;
    int m_B;
};

// 2.全局函数重载+号
// Person operator+(Person &p1,Person &p2) {
//     Person temp;
//     temp.m_A = p1.m_A + p2.m_A;
//     temp.m_B = p1.m_B + p2.m_B;
//     return temp;
// }

// 函数重载的版本
Person operator+(Person &p1,int num) {
    Person temp;
    temp.m_A = p1.m_A + num;
    temp.m_B = p1.m_B + num;
    return temp;
}

void test01() {
    Person p1;
    p1.m_A = 10;
    p1.m_B = 10;
    Person p2;
    p2.m_A = 10;
    p2.m_B = 10;

    // 成员函数重载本质调用
    // Person p3 = p1.operator+(p2);
    // 全局函数重载本质调用
    // Person p3 = operator+(p1,p2);
    Person p3 = p1 + p2;
    // 运算符重载,也可以发生函数重载
    Person p4 = p1 + 100;
    cout<<"p3.m_A = " << p3.m_A<<endl;
    cout<<"p3.m_B = " << p3.m_B<<endl;

    cout<<"p4.m_A = " << p4.m_A<<endl;
    cout<<"p4.m_B = " << p4.m_B<<endl;
}


int main() {
    test01();
    return 0;
}

4.5.2 左移运算符重载

#include <iostream>
using namespace std;

// 左移运算符重载
class Person {
    friend ostream & operator<<(ostream &cout,Person &p);
public:
    Person(int a,int b):m_A(a),m_B(b){}
    // 利用成员函数重载 左移运算符  p.operator<<(cout) 简化版本 p<<cout
    // 不会利用成员函数重载<<运算符,因为实现cout在左侧
    // void operator<<(Person &p) {

    // }

private:
    int m_A;
    int m_B;
};

// 只能利用全局函数重载左移运算符
ostream & operator<<(ostream &cout,Person &p) // 本质 operator<<(cout,p) 简化 cout<<p
{
    cout<<"m_A = "<<p.m_A<<" , m_B = "<<p.m_B;
    return cout;
}

void test01() {
    Person p(10,20);
    cout<<p<<", hello,world!"<<endl;
}

int main() {
    test01();
    return 0;
}

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

4.5.3 递增运算符重载

  • 作用:通过重载递增运算符,实现自己的整型数据
#include <iostream>
using namespace std;

// 重载递增运算符

// 自定义整型
class MyInterger {
    friend ostream& operator<<(ostream& cout,MyInterger myint);
public:
    MyInterger() {
        m_Num = 0;
    }
    // 重载前置++运算符 返回引用为了一直对一个数据进行递增
    MyInterger& operator++() {
        // 先进行++运算
        m_Num++;
        // 再将自身做返回
        return *this;
    }
    // 重载后置++运算符 
    // void operator++(int) int代表占位参数,可以用于区分前置和后置递增
    MyInterger operator++(int) {
        // 先 记录当时结果
        MyInterger temp = *this;
        // 后 递增
        m_Num++;
        // 最后将记录结果做返回
        return temp;
    }
private:
    int m_Num;
};


// 重载<<运算符
ostream& operator<<(ostream& cout,MyInterger myint) {
    cout<<myint.m_Num;
    return cout;
}


void test01() {
    MyInterger myint;
    cout<<++(++myint)<<endl;
    cout<<myint<<endl;
}

void test02() {
    MyInterger myint;
    cout<<myint++<<endl;
    cout<<myint;
}

int main() {
    test02();
    return 0;
}
  • 总结:前置递增返回引用,后置递增返回值 

4.5.4 赋值运算符重载

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

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

#include <iostream>
using namespace std;

// 赋值运算符重载
class Person {
public:
    Person(int age) {
        m_Age = new int(age);
    }
    ~Person() {
        if(m_Age!=NULL) {
            delete m_Age;
            m_Age = NULL;
        }
    }

    // 重载 赋值运算符
    Person& operator=(Person &p) {
        // 编译器是提供浅拷贝
        // m_Age = p.m_Age;
        
        // 应该先判断是否有属性在堆区,如果有先释放干净,然后再深拷贝
        if(m_Age!=NULL) {
            delete m_Age;
            m_Age = NULL;
        }
        // 深拷贝
        m_Age = new int(*p.m_Age);
        // 返回对象本身 而不是返回副本
        return *this;
    }

    int *m_Age;
};

void test01() {
    Person p1(18);
    Person p2(20);
    Person p3(30);
    p3 = p2 = p1;// 赋值操作
    cout<<"p1 的年龄为: "<<*p1.m_Age<<endl;
    cout<<"p2 的年龄为: "<<*p2.m_Age<<endl;
    cout<<"p3 的年龄为: "<<*p3.m_Age<<endl;
}

int main() {
    test01();
    return 0;
}

执行结果:

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

4.5.5 关系运算符重载

  • 作用:重载关系运算符,可以让两个自定义类型对象进行对比操作
#include <iostream>
using namespace std;
// 作用:重载关系运算符,可以让两个自定义类型对象进行对比操作
class Person{
public:
    Person(string name,int age) {
        m_Name = name;
        m_Age = age;
    }
    // 重载 == 号
    bool operator==(Person& p) {
        if(this->m_Name == p.m_Name && this->m_Age == p.m_Age) {
            return true;
        }
        return false;
    }

    // 重载 != 号
    bool operator!=(Person& p) {
        if(this->m_Name == p.m_Name && this->m_Age == p.m_Age) {
            return false;
        }
        return true;
    }

    string m_Name;
    int m_Age;
};

void test01() {
    Person p1("Tom",18);
    Person p2("Tom",18);
    Person p3("Jerry",18);
    if(p1 == p2) {
        cout<<"p1 和 p2 是相等的"<<endl;
    }
    if(p1 != p3) {
        cout<<"p1 和 p3 是不相等的"<<endl;
    }else{
        cout<<"p1 和 p3 是相等的"<<endl;
    }
}

int main() {
    test01();
    return 0;
}

执行结果: 

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

4.5.6 函数调用运算符重载

  • 函数调用运算符() 也可以重载
  • 由于重载后使用的方式非常像函数的调用,因此称为仿函数
  • 仿函数没有固定写法,非常灵活
#include <iostream>
using namespace std;
#include <string>
// 函数调用运算符重载

// 打印输出类
class MyPrint {
public:
    // 重载函数调用运算符
    void operator()(string test){
        cout<<test<<endl;
    }
};

void MyPrint02(string test) {
    cout<<test<<endl;
}

void test01() {
    MyPrint myPrint;
    myPrint("hello,world"); // 由于使用起来非常类似于函数调用,因此称为仿函数
    MyPrint02("hello,heheda");
}

// 仿函数非常灵活,没有固定的写法
class MyAdd{
public:
    int operator()(int num1,int num2){
        return num1+num2;
    }
};

void test02() {
    MyAdd myadd;
    int ret = myadd(100,200);
    cout<<"ret = "<<ret<<endl;

    // 匿名函数对象
    cout<<MyAdd()(100,10)<<endl;
}

int main() {
    test02();
    return 0;
} 

4.6 继承

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

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

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

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

 

#include <iostream>
using namespace std;

// 继承方式

// 公共继承
class Base1{
public:
    int m_A;
protected:
    int m_B;
private:
    int m_C;
};

class Son1:public Base1{
public:
    void func() {
        m_A = 10;// 父类中的公共权限成员 到子类中依然是公共权限
        m_B = 10;// 父类中的保护权限成员 到子类中依然是保护权限
        // m_C = 10;// 父类中的私有权限成员 子类访问不到
    }
};
void test01(){
    Son1 s1;
    s1.m_A = 100;
    // s1.m_B = 20; // 到Son1中 m_B是保护权限 类外访问不到
}

// 保护继承
class Base2{
public:
    int m_A;
protected:
    int m_B;
private:
    int m_C;
};
class Son2:protected Base2{
public:
    void func(){
        m_A = 100; // 父类中公共成员,到子类中变为保护权限
        m_B = 200; // 父类中保护成员,到子类中变为保护权限
        // m_C = 400;// 父类中私有成员 子类访问不到
    };
};

void test02() {
    Son2 s2;
    // s2.m_A = 1000;// 在Son2中 m_A变为保护权限,因此类外访问不到
    // s2.m_B = 120; // 在Son2中 m_B为保护权限,因此类外访问不到
}

// 私有继承
class Base3{
public:
    int m_A;
protected:
    int m_B;
private:
    int m_C;
};
class Son3:private Base3{
public:
    void func(){
        m_A = 100; // 父类中公共成员,到子类中变为私有权限
        m_B = 200; // 父类中保护成员,到子类中变为私有权限
        // m_C = 400;// 父类中私有成员 子类访问不到
    };
};

void test03() {
    Son3 s3;
    // s2.m_A = 1000;// 在Son3中 m_A变为私有权限,因此类外访问不到
    // s2.m_B = 120; // 在Son3中 m_B变为私有权限,因此类外访问不到
}

class GrandSon3:public Son3 {
public:
    void func() {
        // m_A = 100; // 到了Son3中 m_A变为私有,即使是儿子,也是访问不到
        // m_B = 20; // 到了Son3中 m_B变为私有,即使是儿子,也是访问不到
    }
};

4.6.3 继承中的对象模型

  • 问题:从父类继承过来的成员,哪些属于子类对象中?
#include<iostream>
using namespace std;

// 继承中的对象模型
class Base{
public:
    int m_A;
protected:  
    int m_B;
private:
    int m_C;
};

class Son:public Base{
public:
    int m_D;
};

// 利用开发人员命令提示工具查看对象模型
// 跳转盘符 F:
// 跳转文件路径 cd 具体路径下
// 查看命名
// c1 /d1 reportSingleClassLayout类名 文件名

void test01() {
    // 16
    // 父类中所有非静态成员属性都会被子类继承下去
    // 父类中私有成员属性 是被编译器给隐藏了,因此是访问不到,但是确实被继承了
    cout<<"size of Son = "<<sizeof(Son)<<endl;
}

int main() {

    return 0;
}

 

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

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

4.6.4 继承中构造和析构顺序

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

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

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

#include <iostream>
using namespace std;
// 继承中的构造和析构顺序
class Base{
public:
    Base() {cout << "Base constructor" << endl;}
    ~Base() {cout << "Base destructor" << endl;}
private:

};

class Son:public Base{
public:
    Son() {cout << "Son constructor" << endl;}
    ~Son() {cout << "Son destructor" << endl;}
};

void test01() {
    // Base b;

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

int main() {
    test01();
    return 0;
}

执行结果:

PS D:\Work\c++\bin> ."D:/Work/c++/bin/app.exe"
Base constructor
Son constructor
Son destructor
Base destructor
PS D:\Work\c++\bin> 

4.6.5 继承同名成员处理方式

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

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

总结:

  1. 子类对象可以直接访问到子类中同名成员
  2. 子类对象加作用域可以访问到父类同名成员
  3. 当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数
#include <iostream>
using namespace std;

// 继承中同名成员处理
class Base{
public:
    Base() {
        m_A = 100;
    }
    void func() {
        cout << "Base func()" << endl;
    }
    void func(int a) {
        cout << "Base func(int a)" << endl;
    }
    int m_A;
};

class Son:public Base{
public:
    Son() {
        m_A = 200;
    }
    void func() {
        cout << "Son func()" << endl;
    }
    int m_A;
};

// 同名成员属性处理
void test01() {
    Son s;
    cout << "Son下的 m_A = " << s.m_A << endl;//200
    // 如果通过子类对象 访问到父类中同名成员,需要加作用域
    cout << "Base下的 m_A = " << s.Base::m_A << endl;//100
    cout << "Son::m_A = " << s.Son::m_A << endl;//200

}

// 同名成员函数处理
void test02() {
    Son s;
    s.func();//Son func() 直接调用 调用的是子类中的同名成员

    // 如何调用到父类中同名成员函数?
    s.Base::func();//Base func()

    // 如果子类中出现和父类同名的成员函数,子类的同名成员会隐藏掉父类中所有的同名成员函数
    // s.func(100); // error

    // 如果想访问到父类中被隐藏的同名成员函数,需要加作用域
    s.Base::func(100);//Base func(int a)
}

int main() {
    // test01();
    test02();
    return 0;
}

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

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

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

  • 访问子类同名成员,直接访问即可
  • 访问父类同名成员,需要加作用域
#include <iostream>
using namespace std;

// 继承中的同名静态成员处理方式
class Base {
public:
    static int m_A;
    static void func() {
        cout << "Base func()" << endl;
    }
    static void func(int a) {
        cout<<"Base func(int a)"<<endl;
    }
};

int Base::m_A = 100;

class Son:public Base{
public:
    static int m_A;
    static void func() {
        cout<<"Son func()"<<endl;
    }
};
int Son::m_A = 200;

// 同名的静态成员属性
void test01() {
    Son s;
    // 1.通过对象访问
    cout<<"通过对象访问: "<<endl;
    cout<<"Son 下的 m_A = "<<s.m_A<<endl;
    cout<<"Base 下的 m_A = "<<s.Base::m_A<<endl;

    // 2.通过类名访问
    cout<<"通过类名访问: "<<endl;
    cout<<"Son 下的 m_A = "<<Son::m_A<<endl;
    // 第一个::代表通过类名方式访问
    // 第二个::代表访问父类作用域下
    cout<<"Base 下的 m_A = "<<Son::Base::m_A<<endl;
}

// 同名的静态成员函数
void test02() {
    Son s;
    // 1.通过对象访问
    cout<<"通过对象访问: "<<endl;
    s.func();//Son func()
    s.Base::func();//Base func()

    // 2.通过类名访问 
    cout<<"通过类名访问: "<<endl;
    Son::func();//Son func()
    Son::Base::func();//Base func()
    // 子类出现和父类同名静态成员函数,也会隐藏父类中所有同名成员函数
    // 如果想访问父类中被隐藏同名成员,需要加作用域
    Son::Base::func(1);//Base func(int a)
}


int main() {
    test02();
    return 0;
}

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

4.6.7 多继承语法

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

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

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

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

#include <iostream>
using namespace std;

// 多继承语法
class Base1 {
public:
    Base1() {
        m_A = 100;
    }
    int m_A;
};

class Base2 {
public:
    Base2() {
        m_A = 200;
    }
    int m_A;
};

// 子类 需要继承Base1和Base2
// 语法:class 子类:继承方式1 父类1,继承方式2 父类2...
class Son:public Base1,public Base2 {
public:
    Son() {
        m_C = 300;
        m_D = 400;
    }
    int m_C;
    int m_D;
};

void test01() {
    Son s;
    cout<<"size of Son: "<<sizeof(s)<<endl;
    // 当父类中出现同名成员需要加作用域区分
    cout<<"Base1::m_A = "<<s.Base1::m_A<<endl;//100
    cout<<"Base2::m_A = "<<s.Base2::m_A<<endl;//200
}

int main() {
    test01();
    return 0;
}

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

4.6.8 菱形继承

菱形继承概念:

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

典型的菱形继承案例:

菱形继承问题:

  1. 羊继承了动物的数据,驼同样继承了动物的数据,当草泥马使用数据时,就会产生二义性
  2. 草泥马继承自动物的数据继承了两份,其实我们应清楚,这份数据只需要一份即可
#include <iostream>
using namespace std;

// 动物类
class Animal{
public:
    int m_Age;
};

// 羊类
class Sheep:public Animal{};

// 驼类
class Tuo:public Animal{};

// 羊驼类
class SheepTuo:public Sheep,public Tuo{};

void test01() {
    SheepTuo st;
    st.Sheep::m_Age = 18;
    st.Tuo::m_Age = 28;
    // 当菱形继承,两个父类拥有相同数据,需要加以作用域区分
    cout<<"st.Sheep::m_Age = "<<st.Sheep::m_Age<<endl;//18
    cout<<"st.Tuo::m_Age = "<<st.Tuo::m_Age<<endl;//28

    // 这份数据我们知道,只要有一份就可以,菱形继承导致数据有两份,资源浪费
    
}

int main() {
    test01();
    return 0;
}

  • 利用虚继承 解决菱形继承的问题,继承之前 加上关键字 virtual 变为虚继承
#include <iostream>
using namespace std;

// 动物类
class Animal{
public:
    int m_Age;
};

// 利用虚继承 解决菱形继承的问题
// 继承之前 加上关键字 virtual 变为虚继承
// Animal类称为 虚基类

// 羊类
class Sheep:virtual public Animal{};

// 驼类
class Tuo:virtual public Animal{};

// 羊驼类
class SheepTuo:public Sheep,public Tuo{};

void test01() {
    SheepTuo st;
    st.Sheep::m_Age = 18;
    st.Tuo::m_Age = 28;
    // 当菱形继承,两个父类拥有相同数据,需要加以作用域区分
    cout<<"st.Sheep::m_Age = "<<st.Sheep::m_Age<<endl;//28
    cout<<"st.Tuo::m_Age = "<<st.Tuo::m_Age<<endl;//28
    cout<<"st.m_Age = "<<st.m_Age<<endl;//28

    // 这份数据我们知道,只要有一份就可以,菱形继承导致数据有两份,资源浪费

}

int main() {
    test01();
    return 0;
}

总结:

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

4.7 多态

4.7.1 多态的基本概念

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

多态分为两类

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

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

  • 静态多态的函数地址早绑定 - 编译阶段确定函数地址
  • 动态多态的函数地址晚绑定 - 运行阶段确定函数地址
#include <iostream>
using namespace std;

// 多态

// 动物类
class Animal {
public:
    // 虚函数
    virtual void speak() {
        cout<<"动物在说话"<<endl;
    }
};

// 猫类
class Cat:public Animal{
public:
    // 重写 函数返回值类型 函数名 参数列表 完全相同
    void speak() {
        cout<<"小猫在说话"<<endl;
    }
};

// 狗类
class Dog:public Animal{
public:
    void speak() {
        cout<<"小狗在说话"<<endl;
    }
};

// 执行说话的函数
// 地址早绑定 在编译阶段确定函数地址
// 如果想执行让猫说话,那么这个函数地址就不能提前绑定,需要在运行阶段进行绑定,
// 地址晚绑定

// 动态多态满足条件
// 1.有继承关系
// 2.子类要重写父类的虚函数

// 动态多态使用
// 父类的指针或者引用,指向子类对象

void doSpeak(Animal &animal) { // Animal &animal = cat;
    animal.speak();
}

void test01() {
    Cat cat;
    doSpeak(cat);

    Dog dog;
    doSpeak(dog);
}

int main() {
    test01();    
    return 0;
}

总结:

多态满足条件:

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

多态使用条件:

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

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

多态的原理剖析:

 

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

Animal& animal = cat;
animal.speak()

 

4.7.2 多态案例一.计算器类

案例描述:

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

多态的优点:

  1. 代码组织结构清晰
  2. 可读性强
  3. 利于前期和后期的扩展以及维护
#include <iostream>
using namespace std;

// 分别利用普通写法和多态技术实现计算器
// 普通写法
class Calculator {
public:
    int getResult(string oper) {
        if(oper == "+") {
            return m_Num1 + m_Num2;
        }
        else if(oper == "-") {
            return m_Num1 - m_Num2;
        }
        else if(oper == "*") {
            return m_Num1 * m_Num2;
        }
        // 如果想扩展新的功能,需要修改源码
        // 在真实开发中 提倡 开闭原则
        // 开闭原则: 对扩展进行开放,对修改进行关闭
    }
    int m_Num1;// 操作数1
    int m_Num2;// 操作数2
};

void test01() {
    // 创建计算器对象
    Calculator cal;
    cal.m_Num1 = 10;// 操作数1为10
    cal.m_Num2 = 10;// 操作数2为10

    cout<<cal.getResult("+")<<endl;// 输出20
    cout<<cal.getResult("-")<<endl;// 输出0
    cout<<cal.getResult("*")<<endl;// 输出100
}

// 利用多态实现计算器
// 多态好处:
// 1.组织结构清晰
// 2.可读性强
// 3.对于前期和后期扩展以及维护性高

// 实现计算器抽象类
class AbstractCalculator {
public:
    virtual int getResult() {
        return 0;
    }
    int m_Num1;// 操作数1
    int m_Num2;// 操作数2
};

// 加法计算器类
class AddCalculator:public AbstractCalculator {
public:
    int getResult() {
        return m_Num1 + m_Num2;
    }
};

// 减法计算器类
class SubCalculator:public AbstractCalculator {
public:
    int getResult() {
        return m_Num1 - m_Num2;
    }
};

// 乘法计算器类
class MulCalculator:public AbstractCalculator {
public:
    int getResult() {
        return m_Num1 * m_Num2;
    }
};

void test02() {
    // 多态使用条件
    // 父类指针或者引用指向子类对象

    // 加法运算
    AbstractCalculator* abc = new AddCalculator;
    abc->m_Num1 = 10;// 操作数1为10
    abc->m_Num2 = 100;// 操作数2为100

    cout<<abc->m_Num1 <<"+"<<abc->m_Num2<<"="<<abc->getResult()<<endl;// 输出20 
    // 用完后记得销毁
    delete abc;
    abc = nullptr;

    // 减法运算
    abc = new SubCalculator;
    abc->m_Num1 = 10;// 操作数1为10
    abc->m_Num2 = 100;// 操作数2为100
    cout<<abc->m_Num1 <<"-"<<abc->m_Num2<<"="<<abc->getResult()<<endl;  
    delete abc;
    abc = nullptr;

    // 乘法运算
    abc = new MulCalculator;
    abc->m_Num1 = 10;// 操作数1为10
    abc->m_Num2 = 100;// 操作数2为100
    cout<<abc->m_Num1 <<"*"<<abc->m_Num2<<"="<<abc->getResult()<<endl;
    delete abc;
    abc = nullptr;
}

int main() {
    test02();
    return 0;
}

4.7.3 纯虚函数和抽象类

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

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

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

抽象类特点:

  • 无法实例化对象
  • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
#include <iostream>
using namespace std;

// 纯虚函数和抽象类
class Base{
public:
    // 纯虚函数
    // 只要有一个纯虚函数,这个类称为抽象类
    // 抽象类特点:
    // 1.无法实例化对象
    // 2.抽象类的子类 必须要重写父类中的纯虚函数,否则也属于抽象类
    virtual void func() = 0; // 纯虚函数
};

class Son:public Base{
public:
    void func() { cout << "Son::func()" << endl; }// 重写父类的纯虚函数
};

void test01() {
    // Base b;// 抽象类无法实例化对象
    // new Base;//抽象类是无法实例化对象
    Son s;// 子类必须重写父类中的纯虚函数,否则无法实例化对象

    Base* base = new Son;
    base->func();// Son::func()
    delete base;
    base = NULL;
}

int main() {
    test01();
    return 0;
}

4.7.4 多态案例二-制作饮品

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

  • 利用多态技术是实现本案例,提供抽象制作饮品基类,提供子类制作咖啡和茶叶
#include <iostream>
using namespace std;

// 多态案例2: 制作饮品
class AbstractDrinking{
public:
    // 制作饮品
    virtual void boil() = 0;
    // 冲泡
    virtual void brew() = 0;
    // 倒入杯中
    virtual void pourInCup() = 0;
    // 加入辅料
    virtual void putSomething() = 0;
    // 制作饮品
    void makeDrink() {
        boil();
        brew();
        pourInCup();
        putSomething();
    }
};

// 制作咖啡
class Coffee:public AbstractDrinking{
public:
    // 煮水
    void boil() {
        cout<<"煮农夫山泉"<<endl;
    }
    // 冲泡
    void brew() {
        cout<<"冲泡咖啡"<<endl;
    }
    // 倒入杯中
    void pourInCup() {
        cout<<"倒入咖啡杯中"<<endl;
    }
    // 加入辅料
    virtual void putSomething() {
        cout<<"加入牛奶和糖"<<endl;
    }
};

// 制作红茶
class RedTee:public AbstractDrinking{
public:
    // 煮水
    void boil() {
        cout<<"煮怡宝"<<endl;
    }
    // 冲泡
    void brew() {
        cout<<"冲泡茶叶"<<endl;
    }
    // 倒入杯中
    void pourInCup() {
        cout<<"倒入茶杯中"<<endl;
    }
    // 加入辅料
    virtual void putSomething() {
        cout<<"加入冰糖和柠檬"<<endl;
    }
};

// 制作函数
// AbstractDrinking* drinkings = new Coffee
void doWork(AbstractDrinking *drinking) { 
    drinking->makeDrink();
    delete drinking;// 释放
    drinking = nullptr;
}

void test01() {
    // 制作咖啡
    doWork(new Coffee);
    cout<<"================"<<endl;
    // 制作红茶
    doWork(new RedTee);
}

int main() {
    test01();
    return 0;
}

执行结果:

PS D:\Work\c++\bin> ."D:/Work/c++/bin/app.exe"
煮农夫山泉
冲泡咖啡
倒入咖啡杯中
加入牛奶和糖
================
煮怡宝
冲泡茶叶
倒入茶杯中
加入冰糖和柠檬
PS D:\Work\c++\bin>

4.7.5 虚析构和纯虚析构

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

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

虚析构和纯虚析构共性:

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

虚析构和纯虚析构区别:  

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

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

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

类名::类名(){}

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

// 虚析构和纯虚析构
class Animal{
public:
    // 纯虚函数
    Animal() { cout<<"Animal 构造函数调用" <<endl; }
    virtual void speak() = 0;
    // 利用虚析构可以解决 父类指针释放子类对象的时不干净的问题
    // virtual ~Animal(){ cout<<"Animal 虚析构函数调用" <<endl; }
    
    // 纯虚析构 需要声明也需要实现
    // 有了纯虚析构之后,这个类也属于抽象类,无法实例化对象
    virtual ~Animal() = 0;
};

Animal::~Animal() {
    cout<<"Animal 纯虚析构函数调用" <<endl;
}

class Cat:public Animal{
public:
    Cat(string name) {
        cout<<"Cat 构造函数调用" << endl;
        m_Name = new string(name); // 动态分配内存
    }
    void speak(){
        cout << *m_Name <<"小猫在说话" << endl;
    }
    ~Cat(){
        cout << "Cat 析构函数调用" << endl;
        if(m_Name != NULL) {
            delete m_Name;
            m_Name = NULL;
            cout<<"释放m_Name内存"<<endl;
        }   
    }
    string* m_Name;
};

void test01() {
    Animal* animal = new Cat("Tom"); 
    animal->speak();
    // 父类指针在析构时候,不会调用子类中析构函数,导致子类如果有堆区属性,出现内存泄漏
    delete animal; // 父类指针指向子类对象,调用子类的析构函数
    animal = NULL; // 防止野指针
}

int main() {
    test01();
    return 0;
}

执行结果:

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

总结:

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

4.7.6 多态案例三-电脑组装

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

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

// 目的:抽象不同的零件类

// 抽象的CPU类
class CPU {
public:
    // 抽象的计算函数
    virtual void calculate() = 0;
};

// 抽象的显卡类
class VideoCard {
public:
    // 抽象的显示函数
    virtual void display() = 0;
};

// 抽象的内存条类
class Memory {
public:
    // 抽象的存储函数
    virtual void storage() = 0;
};

// 具体零件厂商 Intel厂商
class IntelCPU : public CPU {
public:
    void calculate() {
        cout << "IntelCPU 计算中..." << endl;
    }
};

class IntelVideoCard : public VideoCard {
public:
    void display() {
        cout << "IntelVideoCard 显示中..." << endl;
    }
};

class IntelMemory : public Memory {
public:
    void storage() {
        cout << "IntelMemory 存储中..." << endl;
    }
};

// 具体零件厂商 Lenovo厂商
class LenovoCPU : public CPU {
public:
    void calculate() {
        cout<<"LenovoCPU 计算中..." << endl;
    }
};

class LenovoVideoCard : public VideoCard {
public:
    void display() {
        cout << "LenovoVideoCard 显示中..." << endl;
    }
};

class LenovoMemory : public Memory {
public:
    void storage() {
        cout << "LenovoMemory 存储中..."<<endl;
    }
};

// 抽象的电脑类
class Computer {
public:
    // 电脑需要CPU、显卡、内存条
    // 构造函数中,传入三个零件指针
    Computer(CPU* cpu, VideoCard* videoCard, Memory* memory){
        m_cpu = cpu;
        m_videoCard = videoCard;
        m_memory = memory;
    }
    // 提供工作的函数,调用每个零件工作的接口
    void doWork() {
        m_cpu->calculate();
        m_videoCard->display();
        m_memory->storage();
    }
    // 提供析构函数 释放三个电脑零件
    ~Computer() {
        cout << "Computer 析构" << endl;
        if(m_cpu != NULL) { // 释放CPU零件
            delete m_cpu;
            m_cpu = NULL;
            cout<<"释放m_cpu"<<endl;
        }
        if(m_videoCard != NULL) { // 释放显卡零件
            delete m_videoCard;
            m_videoCard = NULL;
            cout<<"释放m_videoCard"<<endl;
        }
        if(m_memory != NULL) { // 释放内存条零件
            delete m_memory;
            m_memory = NULL;
            cout<<"释放m_memory"<<endl;
        }
    }
private:
    CPU* m_cpu;              // CPU的零件指针
    VideoCard* m_videoCard;  // 显卡零件指针
    Memory* m_memory;        // 内存条零件指针
};

void test01() {
    // 第一台电脑组装和工作~
    cout<<"第一台电脑组装和工作~~"<<endl;
    CPU* cpu = new LenovoCPU;
    VideoCard* videoCard = new LenovoVideoCard;
    Memory* memory = new LenovoMemory;

    // 创建一个Lenovo电脑
    Computer* computer1 = new Computer(cpu, videoCard, memory);
    computer1->doWork();
    delete computer1;
    computer1 = NULL;
    cout<<"===================================="<<endl;
    // 第二台电脑组装和工作~
    cout<<"第二台电脑组装和工作~"<<endl;
    cpu = new IntelCPU;
    videoCard = new IntelVideoCard;
    memory = new IntelMemory;
    // 创建一个Intel电脑
    Computer* computer2 = new Computer(cpu, videoCard, memory); 
    computer2->doWork();
    delete computer2;
    computer2 = NULL;
    cout<<"===================================="<<endl;
    // 第三台电脑组装和工作~
    cout<<"第三台电脑组装和工作~"<<endl;
    cpu = new IntelCPU;
    videoCard = new LenovoVideoCard;
    memory = new IntelMemory;
    // 创建一个Intel电脑
    Computer* computer3 = new Computer(cpu, videoCard, memory); 
    computer3->doWork();
    delete computer3;
    computer3 = NULL;
}

int main() {
    test01();
    return 0;
}

执行结果:

PS D:\Work\c++\bin> ."D:/Work/c++/bin/app.exe"
第一台电脑组装和工作~~
LenovoCPU 计算中...
LenovoVideoCard 显示中...
LenovoMemory 存储中...
Computer 析构
释放m_cpu
释放m_videoCard
释放m_memory
====================================
第二台电脑组装和工作~
IntelCPU 计算中...
IntelVideoCard 显示中...
IntelMemory 存储中...
Computer 析构
释放m_cpu
释放m_videoCard
释放m_memory
====================================
第三台电脑组装和工作~
IntelCPU 计算中...
LenovoVideoCard 显示中...
IntelMemory 存储中...
Computer 析构
释放m_cpu
释放m_videoCard
释放m_memory
PS D:\Work\c++\bin>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1419994.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

万兆网络数据传输-scp加速

在万兆甚至更高的网络带宽场景下 scp 的传输效率并不如人意。毕竟 scp 是旧时代的产物&#xff0c;那时千兆网络都很罕见。以下通过修改压缩方式的方法提升数据的传输速度。同时也采用 nc &#xff0c; bbcp 和 rsync 进行了对比测试。 目录 scp采用默认方式更改压缩算法为 aes…

seata 分布式

一、下载安装seata 已经下载好的朋友可以跳过这个步骤。这里下载的是seata1.6.1这个版本。 1、进入seata官网 地址&#xff1a; https://seata.io/zh-cn/index.html 2、进入下载 3、点击下载地址 下载地址&#xff1a; https://github.com/seata/seata 二、配置seata 进入c…

vue3项目中让echarts适应div的大小变化,跟随div的大小改变图表大小

目录如下 我的项目环境如下利用element-resize-detector插件监听元素大小变化element-resize-detector插件的用法完整代码如下&#xff1a;结果如下 在做项目的时候&#xff0c;经常会使用到echarts&#xff0c;特别是在做一些大屏项目的时候。有时候我们是需要根据div的大小改…

2023年全国职业院校技能大赛(高职组)“云计算应用”赛项赛卷9

某企业根据自身业务需求&#xff0c;实施数字化转型&#xff0c;规划和建设数字化平台&#xff0c;平台聚焦“DevOps开发运维一体化”和“数据驱动产品开发”&#xff0c;拟采用开源OpenStack搭建企业内部私有云平台&#xff0c;开源Kubernetes搭建云原生服务平台&#xff0c;选…

redis—Zset有序集合

目录 前言 1.常见命令 2.使用场景 3.渐进式遍历 4.数据库管理 前言 有序集合相对于字符串、列表、哈希、集合来说会有一-些陌生。它保留了集合不能有重复成员的 特点&#xff0c;但与集合不同的是&#xff0c;有序集合中的每个元素都有-个唯- -的浮 点类型的分数(score) …

c语言常量详解 全

c语言常量详解 全 一 常量的基本概念及分类二 常量的存储方式三 c语言常量和变量的具体区别四 字面常量详解4.1 常见类型的字面常量及其示例&#xff1a;4.2 字面常量的使用情况4.3 字面常量的优点 五 const 关键字常量详解5.1 const关键字在C语言中的使用情况&#xff1a;5.2 …

山海鲸可视化大屏:引领企业洞悉市场,提升客户价值的秘密武器

随着大数据时代的到来&#xff0c;企业面临着前所未有的机遇与挑战。如何从海量数据中挖掘出有价值的信息&#xff0c;洞察市场趋势&#xff0c;提升客户价值&#xff0c;成为了企业发展的重要课题。山海鲸可视化企业客户价值分析大屏&#xff0c;为企业提供了一个全新的解决方…

利用外卖系统源码构建高效的在线订餐平台

在当今数字化时代&#xff0c;外卖服务已成为人们日常生活中不可或缺的一部分。为了满足用户需求&#xff0c;许多创业者和企业都希望搭建自己的在线订餐平台。利用现有的外卖系统源码&#xff0c;可以快速构建一个高效、安全的在线订餐平台。本文将介绍如何利用外卖系统源码来…

播报 | 天空卫士入围FreeBuf《CCSIP 2023中国网络安全产业全景图》16个细分领域

2024年1月24&#xff0c;国内安全行业门户FreeBuf旗下FreeBuf咨询正式发布《CCSIP 2023中国网络安全产业全景图》&#xff08;第六版&#xff09;。 天空卫士成功入围SASE、数据防泄露&#xff08;DLP&#xff09;、分类分级、数据安全治理(解决方案)、数据安全管控&#xff08…

Django问题报错:Cannot resolve keyword ‘name‘ into field. Choices are: course, id

笔者在进行登录注册实验用户名已经注册过的操作时报错 一、错误位置 二、问题原因 使用Student模型时参数名错误 三、解决办法 修改为与Student模型中对应的参数名,问题解决

每日一题 力扣514自由之路

514. 自由之路 题目描述&#xff1a; 电子游戏“辐射4”中&#xff0c;任务 “通向自由” 要求玩家到达名为 “Freedom Trail Ring” 的金属表盘&#xff0c;并使用表盘拼写特定关键词才能开门。 给定一个字符串 ring &#xff0c;表示刻在外环上的编码&#xff1b;给定另一…

插值(Python)

插值 插值是数学和计算机科学领域中的一种技术&#xff0c;用于在给定一些离散数据点的情况下&#xff0c;估计在这些点之间的数值。插值的目标是通过某种函数&#xff08;插值函数&#xff09;来逼近或拟合这些离散数据&#xff0c;从而使得在原始数据点之间的数值也有合理的估…

物流信息网

技术架构&#xff1a; JSPMySQL 功能描述&#xff1a; 物流信息网主要用于实现网上自主物流&#xff0c;基本功能包括&#xff1a;登录、查询、时效查询、价格查询、注册等。本系统结构如下&#xff1a; &#xff08;1&#xff09;普通用户&#xff1a; 登录&#xff1a…

GIS毕业的那10000人,你们都在做什么?

根据阳光高考网&#xff08;教育部指定高校招生平台&#xff09;的数据显示&#xff0c;截止到2022年12月31日&#xff0c;全国高校GIS&#xff08;地理信息科学&#xff09;本科毕业生人数为&#xff1a;9000-10000人。 我们还可以看到&#xff0c;地信专业的男女比例为&#…

强化学习原理python篇06(拓展)——DQN拓展

强化学习原理python篇06&#xff08;拓展&#xff09;——DQN拓展 n-steps代码结果 Double-DQN代码结果 Dueling-DQN代码结果 Ref 拓展篇参考赵世钰老师的教材和Maxim Lapan 深度学习强化学习实践&#xff08;第二版&#xff09;&#xff0c;请各位结合阅读&#xff0c;本合集只…

nginx负载均衡案例

大家好今天给大家带来nginx负载均衡实验案例,首大家先看一下我的各类版本信息。&#xff08;还有两台设备信息相同就不展示了&#xff09; 一&#xff0c;搭建nginx环境 ❶首先创建Nginx的目录并进入&#xff1a; [rootlocalhost]# mkdir /soft && mkdir /soft/nginx…

Python qt.qpa.xcb: could not connect to display解决办法

遇到问题&#xff1a;qt.qpa.xcb: could not connect to display 解决办法&#xff0c;在命令行输入&#xff1a; export DISPLAY:0 然后重新跑python程序&#xff0c;解决&#xff01; 参考博客&#xff1a;qt.qpa.xcb: could not connect to displayqt.qpa.plugin: Could …

内核和进程的内存管理,内核从buddy到alloc到slab到kmalloc,内核的内核栈和中断处理程序栈,进程的虚拟内存到页表

内核中的内存管理 内核把物理页作为内存管理的基本单位&#xff0c;尽管处理器最小寻址单位为字&#xff0c;但是MMU&#xff08;管理内存并且把虚拟地址转换为物理地址的硬件&#xff09;通常以页为单位进行处理。 每个物理页面都由一个相应的 struct page 结构来表示&#…

打造高效经营:开发连锁餐饮管理系统的技术深度解析

为了适应市场的快速发展和提高经营效率&#xff0c;许多连锁餐饮企业纷纷投入开发连锁餐饮管理系统。 一、数字化转型的动力 传统的餐饮经营面临着诸多挑战&#xff0c;如订单管理、库存控制、人力资源等问题。在这样的背景下&#xff0c;连锁餐饮企业迫切需要一种全面而高效…

jenkins pipeline配置maven可选参数

1、在Manage Jenkins下的Global Tool Configuration下对应的maven项添加我们要用得到的不同版本的maven安装项 2、pipeline文件内容具体如下 我们maven是单一的&#xff0c;所以我们都是配置单选参数 pipeline {agent anyparameters {gitParameter(name: BRANCH_TAG, type: …