[c++]类与对象学习笔记(二)

4. 友元

在程序中 ,有些私有属性需要让类外的一些函数或者类进行访问,就需要运用友元技术。

友元的目的是让一个函数或者类访问领一个类中私有成员。它的关键字是friend。

友元的三种实现:

  • 全局函数做友元
  • 类做友元
  • 成员函数做友元

例1:全局函数做友元

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

//建筑物类
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;
    //若没有前面的friend,则下一行代码会报错。
    cout << "好基友全局函数正在访问:" << building->m_BedRoom << endl;
    
}


//常对象
void test01()
{
    Building building;
    goodGay(&building);
}

int main()
{
    test01();

    return 0;
}

例2:类做友元

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

//建筑物类
class Building;//声明
class GoodGay
{
public:
    GoodGay();//声明

    void visit();//参观函数 访问Building中的属性

    Building * building;
};

class Building
{    
    friend class GoodGay;//类做友元

public:
    Building();

    string m_SittingRoom;//客厅

private:
    string m_BedRoom;//卧室
};

//可以类内声明,类外写成员函数
GoodGay::GoodGay()
{   //由于GoodGay内的Building变量是指针,所以要用new的方法返回该对象的指针
    building = new Building;
}

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

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

int main()
{
    GoodGay gg;
    gg.visit();

    return 0;
}

例3:成员函数做友元

class Building
{
    friend void visit();
}

5. 运算符重载

5.1 加号运算符重载

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

例1:成员函数重载+号

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

class Person
{
public:
    Person operator+(Person &p)
    {
        Person temp;
        temp.m_A = this->m_A + p.m_A;//this指的是.operator+(p)前面的对象
        temp.m_B = this->m_B + p.m_B;
        return temp;//不能返回引用,因为temp会被销毁,此时引用是空指针
    }

    int m_A;
    int m_B;

};

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 = p1 + p2;
    
    cout << "p3.m_A=" << p3.m_A << endl;
    cout << "p3,m_B=" << p3.m_B << endl;
}

int main()
{
    test01();  

    return 0;
}

例2:全局函数重载+号

//本质:Person p3 = operator+(p1, p2);
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 x)。
  • 内置的运算符不可改变
  • 不要乱写重载的内容。比如加法重载里面写个减法的内容,造孽啊!

5.2 左移运算符重载

作用:输出自定义数据类型。

例:

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

class Person
{
    friend ostream & operator<<(ostream &cout, Person &p);

public:
    
    //利用成员函数重载 左移运算符
    //一般不会这么做,因为无法实现cout在左侧
    //void operator<<(cout){};

    int m_A;
    int m_B;

private:
    int m_C = 10;
};

//利用全局函数重载左移运算符
//本质:operator<<(cout,p);,简化成cout << p
//一定要用引用传入cout,因为cout对象只能有一个。
//如果想用链式编程思想在后面继续输出,<<重载就必须返回cout,重载函数前面不能写成void
ostream & operator<<(ostream &cout, Person &p)
{
    cout << "m_A = " << p.m_A << " m_B = " << p.m_B
        << " m_C = " << p.m_C;
    return cout;
}

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

    //重载<<之后以下语句就能正常执行
    cout << p << endl;
}

int main()
{
    test01();  

    return 0;
}

注:

  • 1、cout用于在计算机屏幕上显示信息,是C++中ostream类型的对象,C++的输出是用“流” (stream)的方式实现的,流运算符的定义等信息是存放在C++的输入输出流库中的,因此如果在程序中使用cout和流运算符,就必须使用预处理命令把头文件stream包含到本文件中,即< iostream >库,该库定义的名字都在命名空间std中,所以·cout全称是std::cout 。

  • 2、对于ostream & operator<<(ostream &cout, Person &p),用函数指针的原因是:ostream对象通常不允许通过拷贝构造函数进行复制,而直接返回ostream对象时会触发拷贝构造函数的调用。

  • 3、所有的重载都最好是全局函数重载,这样便于规范代码,否则会有局限性。

5.3 自增运算符重载

作用:实现自己的整型数据。

例1:重载前置++运算符

class MyInteger
{
public:
    MyInteger()
    {
        m_Num = 0;
    }

    int m_Num;
}

//重载前置++运算符
MyInteger & operator++()
{
    //先进行++运算
    ++m_Num;
    
    //再将自身返回
    return *this;
}

例2:重载后置++运算符

class MyInteger
{
public:
    MyInteger()
    {
        m_Num = 0;
    }

    int m_Num;
}

//重载后置++运算符
//int代表占位参数,用于区分前置和后置递增
MyInteger operator++(int)
{
    //先记录当时结果
    MyInteger temp = *this;
    //后递增
    m_Num++;
    //最后记录的结果返回
    return temp;
}

注:
两个函数的返回类型不同。前置++返回的是引用,而后置++返回的是值。

在c++中,

  • ++a表示取a的地址,增加它的内容,然后把值放在寄存器中;
  • a++表示取a的地址,把它的值装入寄存器,然后增加内存中的a的值;

假设a=0,那么(a++)++可以通过编译,但是它输出的值是1,具体原因已在章节3.2解释过。

5.4 赋值运算符重载

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

  • 默认构造函数
  • 默认析构函数
  • 默认拷贝构造函数
  • 赋值运算符operator=,对属性进行值拷贝

使用这个默认的赋值运算符操作类对象时,该运算符会把这个类的所有数据成员都进行一次赋值操作。

如果类中有属性指向堆区(如new了一个指针),那么会出现堆区内存重复释放的问题,如:

#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;
        }
    }

    int *m_Age;
};
 
int main()
{
    Person p1(10);
    Person p2(20);
    p2 = p1;
    
    cout << "p1的年龄是:" << *p1.m_Age << endl;
    cout << "p2的年龄是:" << *p2.m_Age << endl;
    return 0;
}

在默认的赋值运算符函数中,它会进行浅拷贝操作,即只是简单地将p1的成员变量值复制给p2的成员变量。由于m_Age是一个指向动态分配内存的指针,浅拷贝只会复制指针的值,

所以,在这段代码中,p2的m_Age指针与p1的m_Age指针指向了同一块内存地址。当p2的析构函数被调用时,会释放这块内存,而p1的m_Age指针仍然指向已经被释放的内存地址。

因此,当尝试打印p1.m_Age和p2.m_Age的值时,由于p1.m_Age指向的内存已经无效,访问它会导致程序崩溃。

解决方法:利用赋值运算符重载进行深拷贝

例:

Person & operator=(Person &p)
{
    //先判断是否有属性在堆区,若有,则先释放干净,后深拷贝
    if (m_Age != NULL)
    {
        delete m_age;
        m_Age = NULL;
    }

    m_Age = new int(*p.m_Age);   
    
    return *this; 
}

5.5 关系运算符重载

作用:让两个自定义对象进行对比操作。

例:

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

class Person
{
public:
    Person(string name, int age)
    {
        m_Name = name;
        m_Age = age;
    }

    string m_Name;
    int m_Age;

    //请思考:为什么这里不用引用
    bool operator==(Person &p)
    {
        if(m_Name == p.m_Name && m_Age == p.m_Age)
        return true;
        return false;
    }
};


void test01()
{
    Person p1("Tom", 18);
    Person p2("Jerry", 18);

    if (p1 == p2)
    {
        cout << "p1 equals to p2." << endl;
    }
    else
    {
        cout << "They are not equal." << endl;
    }
}

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

类似地,也可以写bool operator!=(Person &p)

5.6 函数调用运算符重载

  • 函数调用运算符()也可以重载
  • 由于重载后使用的方式非常像函数的调用,因此称为仿函数
  • 仿函数没有固定写法

例:

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

//打印输出类
class MyPrint
{
public:

    //重载函数调用运算符
    void operator()(string text)
    {
        cout << text << endl;
    }

};

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

void test01()
{
    MyPrint print;
    print("Hello world!");//仿函数
    
    MyPrint02("Hello world!");//函数
}

int main()
{
    test01();

    return 0;
}

当然,你也可以这样调用:

cout << MyPrint()("Hello world!");

这个被称为匿名函数对象,执行完这一行后就会被销毁。

6. 继承

就像生物学的物种分类一样,有些类与类之间存在一些关系:下级成员拥有上一级的共性,还有自己的特性。

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

6.1 继承的基本语法

class 子类 : 继承方式 父类

错误示范:

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

//普通实现页面

//Java教程页面
class Java 
{
public:
    void header()
    {
        cout << "首页、公开课、登录、注册...(公共header)" << endl;
    }
    void footer()
    {
        cout << "帮助中心、交流合作、站内地图...(公共footer)" << endl;
    }
    void left()
    {
        cout << "Java、Python、C++...(公共分类列表)" << endl;
    }
    void content()
    {
        cout << "Java学科视频" << endl;
    }
};

//Python页面
class Python 
{
public:
    void header()
    {
        cout << "首页、公开课、登录、注册...(公共header)" << endl;
    }
    void footer()
    {
        cout << "帮助中心、交流合作、站内地图...(公共footer)" << endl;
    }
    void left()
    {
        cout << "Java、Python、C++...(公共分类列表)" << endl;
    }
    void content()
    {
        cout << "Python学科视频" << endl;
    }
};

//C++页面
class Cpp 
{
public:
    void header()
    {
        cout << "首页、公开课、登录、注册...(公共header)" << endl;
    }
    void footer()
    {
        cout << "帮助中心、交流合作、站内地图...(公共footer)" << endl;
    }
    void left()
    {
        cout << "Java、Python、C++...(公共分类列表)" << endl;
    }
    void content()
    {
        cout << "C++学科视频" << endl;
    }
};

void test01()
{
    cout << "Java下载视频页面如下:" << endl;
    Java jvav;
    jvav.header();
    jvav.footer();
    jvav.left();
    jvav.content();

    cout << "---------------------------" << endl;
    cout << "Python下载视频页面如下:" << endl;
    Python py;
    py.header();
    py.footer();
    py.left();
    py.content();

    cout << "---------------------------" << endl;
    cout << "C++下载视频页面如下:" << endl;
    Cpp cpp;
    cpp.header();
    cpp.footer();
    cpp.left();
    cpp.content();
}

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

cv工程师,你好

正确示范:使用继承

class BasePage
{
public:
    void header()
    {
        cout << "首页、公开课、登录、注册...(公共header)" << endl;
    }
    void footer()
    {
        cout << "帮助中心、交流合作、站内地图...(公共footer)" << endl;
    }
    void left()
    {
        cout << "Java、Python、C++...(公共分类列表)" << endl;
    }
};

//Java页面
class Java:public BasePage//继承
{
public:
    void content()
    {
        cout << "Java学科视频" << endl;
    }
};
//其余同理

6.2 继承方式

public, protected, private.

  • 1、子类无法访问父类private内容
  • 2、公有继承:父类的public和protected都原封不动地继承至子类
  • 3、保护继承:父类的public和protected继承至子类后都是protected
  • 4、私有继承:父类的public和protected继承至子类后都是private

6.3 继承中的对象模型

父类中所有非静态成员属性都会被子类继承。

父类中的私有成员属性被编译器隐藏了,因此无法访问,但是会被继承下去

可以用sizeof(ClassName)来验证。

6.4 继承中构造和析构顺序

父构->子构->子析->父析

遵循FILO(先进后出)(doge

6.5 继承同名成员处理方式

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

答:

  • 访问子类同名成员 直接访问即可
  • 访问父类同名成员 需要加作用域
#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;
    cout << "Base m_A = " << s.Base::m_A << endl;
}

//同名成员函数处理
void test02()
{
    Son s;
    s.func();//子类成员函数
    s.Base::func();//父类成员函数1
    s.Base::func(114514);//父类成员函数2
}

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

6.6 多继承语法

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

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

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

实际开发中不建议用多继承

例:

#include <iostream>
using namespace std;

class Base1
{
public:

    Base1()
    {
        m_A = 100;
    }
    int m_A;
};

class Base2
{
public:

    Base2()
    {
        m_B = 200;
    }
    
    int m_A;
    int m_B;
};

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 << "sizeof(s) = " << sizeof(s) << endl;
    cout << "m_A = " << s.m_A << endl;
    cout << "m_A = " << s.Base1::m_A << endl;
}


int main()
{
    test01();

    return 0;
}

6.7 菱形继承

概念:两个子类(派生类)继承同一个父类(基类),又有一个类同时继承两个子类。又称钻石继承。

例:

#include <iostream>
using namespace std;

//动物类
class Animal
{
public:

    int m_Age;
};

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

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

//羊驼类
class Alpaca:public Sheep, public Camel{};

void test01()
{
    Alpaca alp;
    alp.Sheep::m_Age = 18;
    alp.Camel::m_Age = 28;

    cout << "alp.Sheep::m_Age = " << alp.Sheep::m_Age << endl;
    cout << "alp.Camel::m_Age = " << alp.Camel::m_Age << endl;
}


int main()
{
    test01();

    return 0;
}

(注:生物学上,羊驼和羊几乎没有亲缘关系,而与骆驼有很很大关系。本例仅以字面意义来演示菱形继承)(逃

在上面的示例代码中,Sheep类和Camel类都继承了Animal类的m_Age属性,导致两个子类的m_Age重复了,使得资源浪费。

为了避免此类浪费,我们可以使用关键字virtual,即虚继承,可解决菱形继承带来的资源浪费。

  • 此时Animal类称为虚基类
class Sheep:virtual public Animal{};
class Camel:virtual public Animal{};

访问方式直接写成如下形式

cout << alp.m_Age;

原理: 虚继承后,派生类多了一个vbptr(virtual base pointer),称为虚基类指针,它指向虚基类表(vbtable)。vbtable包含了偏移量信息,这些信息指示了虚基类相对于派生类对象起始地址的偏移量。通过这种机制,即使在复杂的继承体系中,程序也能正确地定位和访问虚基类的成员。

7. 多态

7.1 多态的基本概念

多态指的是同一个方法调用,由于对象不同可能会有不同的行为。

多态分为两类:

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

两者区别:

  • 1、静态多态的函数地址早绑定————编译阶段确定函数地址
  • 2、动态多态的函数地址晚绑定————运行阶段确定函数地址

例1:

#include <iostream>
using namespace std;

//动物类
class Animal
{
public:
    void sound()
    {
        cout << "动物在叫" << endl;
    }
};

//猫
class Cat :public Animal
{
public:
    void sound()
    {
        cout << "猫在叫" << endl;
    }
};


//如果想执行“猫在叫”,则这个函数地址需要在运行阶段进行绑定。
void makeSound(Animal &animal)
{
    animal.sound();
}

void test01()
{
    Cat cat;
    makeSound(cat);
}

int main()
{
    test01();

    return 0;
}

其中,void makeSound(Animal &animal)是执行动物“叫唤”的函数。它的地址早绑定(在编译阶段确定函数地址)。调用makeSound时传入的是Cat类,这是合法的,因为c++允许父子之间的类型转换(父类可转子类,而子类不可转父类),然而此时调用的是父类Animal的sound()函数。

如果想执行“猫在叫”,则这个函数地址晚绑定(在运行阶段进行绑定)。也就是说,要利用虚函数来构成动态多态。

例2:

//将原cpp文件的Animal类修改为如下:
class Animal
{
public:
    virtual void sound()
    {
        cout << "动物在叫" << endl;
    }
};
//即可输出“猫在叫”
(点击展开)ChatGPT解释 当函数makeSound被调用时,它使用的是动态绑定(即运行时绑定)机制。由于sound()函数在Animal类中被声明为virtual,编译器会根据实际对象的类型来确定调用的是哪个版本的sound()函数。因此,当你传入Cat对象时,实际上调用的是Cat类中重写的sound()函数,输出的是"猫在叫"。

如果将sound()函数声明为非虚函数(即没有virtual关键字),那么无论你传入什么类型的对象,都会调用Animal类中的sound()函数。因此,不管你传入的是Cat对象还是其他类型的对象,输出都是"动物在叫"。这是因为在编译阶段,函数的地址已经被确定下来,不会根据实际对象类型而改变。


当子类重写父类的虚函数时,子类的虚函数表内部会替换成子类虚函数的地址。



总结:

  • 动态多态满足条件:
    • 1、有继承关系
    • 2、子类重写父类的虚函数
  • 动态多态的使用:
    • 父类的指针或引用指向子类对象

7.2 多态案例一:计算器类

案例描述:分别利用普通写法和多态技术,设计一个计算器类,实现两个操作数进行运算。

多态的优点:

  • 代码组织结构清晰,可读性强
  • 便于前期和后期的扩展及维护

例1:普通实现

#include <iostream>
#include <string>
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, m_Num2;
};

void test01()
{
    Calculator c;
    c.m_Num1 = 10;//操作数1
    c.m_Num2 = 10;//操作数2

    cout << c.m_Num1 << "+" << c.m_Num2 << "=" << c.getResult("+") << endl;
    cout << c.m_Num1 << "-" << c.m_Num2 << "=" << c.getResult("-") << endl;
    cout << c.m_Num1 << "*" << c.m_Num2 << "=" << c.getResult("*") << endl;
}

int main()
{
    test01();

    return 0;
}

这里,我们没有写除法。如果想扩展新的功能,就要修改getResult()的源码。

在真正的开发中,这不是我们想要的结果。有时候一个方法包含上千行代码,如果直接从核心修改,那么修改量很大。我们可以从外面新增功能。

开发提倡开闭原则

  • 开闭原则:对扩展进行开放,对修改进行关闭。

例2:多态实现

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

//计算器抽象类
class AbsCalculator
{
public:

    virtual int getResult()
    {
        return 0;
    }
    int m_Num1;
    int m_Num2;

};

//加法计算器类
class AddCalculator :public AbsCalculator
{
public:

    int getResult()
    {
        return m_Num1 + m_Num2;
    }

};

//减法计算器类
class SubCalculator :public AbsCalculator
{
public:

    int getResult()
    {
        return m_Num1 - m_Num2;
    }

};

//减法计算器类
class MulCalculator :public AbsCalculator
{
public:

    int getResult()
    {
        return m_Num1 * m_Num2;
    }

};

void test01()
{
    //多态使用条件:父类指针或引用指向子类对象
    //这里我们用指针

    //创建加法计算器对象
    AbsCalculator * abc = new AddCalculator;
    abc->m_Num1 = 100;
    abc->m_Num2 = 100;
    cout << abc->m_Num1 << "+" << abc->m_Num2 << "=" << abc->getResult() << endl;
    delete abc;

    //减法运算
    //还是这个父类的指针,只不过改变指向的对象
    abc = new SubCalculator;
    abc->m_Num1 = 100;
    abc->m_Num2 = 100;
    cout << abc->m_Num1 << "-" << abc->m_Num2 << "=" << abc->getResult() << endl;
    delete abc;

    abc = new MulCalculator;
    abc->m_Num1 = 100;
    abc->m_Num2 = 100;
    cout << abc->m_Num1 << "*" << abc->m_Num2 << "=" << abc->getResult() << endl;
}

int main()
{
    test01();

    return 0;
}

如果要新增减法,我们可以新建减法类,然后重载getResult()函数。

总结:多态的优点:

  • 代码组织结构清晰,可读性强
  • 便于前期和后期的扩展及维护

7.3 纯虚函数和抽象类

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

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

当类中有这样的函数,那么这个类也被称为抽象类

抽象类的特点:

  • 无法实例化对象
  • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类

例:

#include <iostream>
using namespace std;

class Base
{
public:
    virtual void func() = 0;
};

class Son :public Base
{
public:

    virtual void func()//有没有virtual都可以
    {
        cout << "func()调用" << endl;
    }

};

void test01()
{
    //Base b; new Base;
    //会报错,因为抽象类不允许实例化对象
    Son s;
    Base * base = &s;
    base->func();
}



int main()
{
    test01();

    return 0;
}

7.4 多态案例二:制作饮品

案例描述:

流程为:煮水-冲泡-导入杯中-加入辅料

#include <iostream>
using namespace std;

class AbstractDrinks
{
public:
    //煮水
    virtual void Boil() = 0;

    //冲泡
    virtual void Brew() = 0;

    //倒入杯中
    virtual void pourIn() = 0;

    //加入辅料
    virtual void putIn() = 0;
    
    void makeDrink()
    {
        Boil();
        Brew();
        pourIn();
        putIn();
    }
};

//制作咖啡
class Coffee :public AbstractDrinks
{
public:
    //煮水
    virtual void Boil()
    {
        cout << "煮农夫山泉" << endl;
    }

    //冲泡
    virtual void Brew()
    {
        cout << "冲泡咖啡" << endl;
    }

    //倒入杯中
    virtual void pourIn()
    {
        cout << "倒入杯中" << endl;
    }

    //加入辅料
    virtual void putIn()
    {
        cout << "加入糖和牛奶" << endl;
    }
};

//制作茶
class Tea :public AbstractDrinks
{
public:
    //煮水
    virtual void Boil()
    {
        cout << "煮怡宝" << endl;
    }

    //冲泡
    virtual void Brew()
    {
        cout << "冲泡茶叶" << endl;
    }

    //倒入杯中
    virtual void pourIn()
    {
        cout << "倒入茶杯中" << endl;
    }

    //加入辅料
    virtual void putIn()
    {
        cout << "加入枸杞" << endl;
    }
};

//由于抽象类不能实例化,故这里用指针
void doWork(AbstractDrinks * abs)
{
    abs->makeDrink();
    delete abs;
}

//AbstractDrinks * abs = new Coffee
//自己写项目千万别像下面这样写new,除非你想内存泄漏
void test01()
{
    //制作咖啡
    doWork(new Coffee);

    //制作茶
    doWork(new Tea);
}

int main()
{
    test01();

    return 0;
}

7.5 虚析构和纯虚析构

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

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




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

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

二者的共性:

  • 可以解决父类指针释放子类对象
  • 都需要有具体的函数实现(纯虚析构要在类内声明,类外实现)

二者的区别:

  • 纯虚析构所属类为抽象类

例:

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

class Animal
{
public:

    Animal()
    {
        cout << "Animal的构造函数调用" << endl;
    }

    virtual ~Animal()
    {
        cout << "Animal的析构函数调用" << endl;
    }
    virtual void speak() = 0;
};

class Cat :public Animal
{
public:

    Cat(string name)
    {
        cout << "Cat的构造函数调用" << endl;
        m_Name = new string(name);
    }
    
    ~Cat()
    {
        if(m_Name != NULL)
        {
            cout << "Cat的析构函数调用" << endl;
            delete m_Name;
            m_Name = NULL;
        }
    }

    virtual void speak()
    {
        cout << *m_Name << "猫在说话" << endl;
    }

    string *m_Name;
};

void test01()
{
    Animal * animal = new Cat("Tom");
    animal->speak();
    //父类指针在析构的时候,不会调用子类的析构函数
    //导致子类如果有堆区属性,会出现内存泄漏
    //所以我们要在Animal类的析构函数前加virtual,使其变为虚析构
    //如果不理解,可以尝试把virtual去掉,看运行结果有什么不同
    delete animal;
}

int main()
{
    test01();

    return 0;
}

总结:

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