面向对象程序设计将对象作为程序的基本单元,将程序和数据封装其中,以提高软件的重用性、灵活性和扩展性。
C++面向对象的三大特性为:封装、继承、多态

万事万物皆为 对象(object) ,对象上有其属性行为

具有相同性质的对象,可以抽象为 类(class)


封装 (encapsulation)

封装的意义

  • 将属性和行为作为一个整体表现事物
  • 将属性和行为加以权限控制

封装意义一:

​ 在设计类的时候,属性和行为写在一起,表现事物

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

const double PI = 3.14;

class Circle{
    public:
    
    //属性:半径
    int m_r;
    
    //行为:计算周长
    double ZC(){return 2 * PI * m_r;}
};

int main(){
    //实例化(通过一个类创建一个对象)
    Circle c1;
    c1.m_r = 10;
    
    cout<<"圆c1的周长为:"<<c1.ZC()<<endl;
    
    return 0;
}

封装意义二:

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

  • 访问权限有三种:

    • public 类内可访问 类外可访问 子类可访问
    • protected 类内可访问 类外不可访问 子类可访问
    • private 类内可访问 类外不可访问 子类不可访问

struct 和 class 的区别

struct 的默认权限是 public

class 的默认权限是 private

成员(member)属性设置为私有

优点1: 将所有成员属性设置为私有,可以自己控制读写权限

优点2: 对于写权限,可以检测数据的有效性

class person
{
public:
    //写姓名
    void setName(string name)
    {
        m_Name = name;
    }
    //读姓名
    string getName()
    {
        return m_Name;
    }
    
    //读年龄
    int getAge()
    {
        return m_Age;
    }
    
    //写情人
    void setLover(string lover)
    {
        m_Lover = lover;
    }
    
private:
    string m_Name;   //姓名,可读可写
    int m_Age;       //年龄,只读
    string m_Lover;  //情人,只写
};

对象的初始化和清理

构造函数(constructor)和析构函数(destructor)

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

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

  1. 函数名称和类名相同
  2. 构造函数可以有参数,因此可以发生重载

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

  1. 函数名称和类名相同,在前面加上~
  2. 析构函数不可以有参数,不能发生重载
class Person
{
public:
    Person()
    {
        cout<<"构造函数的调用"<<endl;
    }
    ~Person()
    {
        cout<<"析构函数的调用"<<endl;
    }
};
void test01()
{
    Person P;
}
int main()
{
    test01();
    
    return 0;
}

构造函数的分类和调用

  • 两种分类方式:

    • 按参数分为:有参构造和无参构造
    • 按类型分为:普通构造和复制构造
  • 三种调用方式:

    • 括号法
    • 显示法
    • 隐式转换法
class Person
{
public:
    //构造函数
    Person()
    {
        cout<<"Person的无参构造函数调用"<<endl;
    }
    Person(int a)
    {
        age = a;
        cout<<"Person的有参构造函数调用"<<endl;
    }
    //复制构造函数
    Person(const Person &p)
    {
        age = p.age;
        cout<<"Person的复制构造函数调用"<<endl;
    }
    //析构函数
    ~Person()
    {
        cout<<"Person的析构函数调用"<<endl;
    }
   
    int age;
};
//调用
void test01()
{
    //1.括号法
    Person p1;       //无参构造函数调用
    Person p2(10);   //有参构造函数调用
    Person p3(p2);   //复制构造函数调用
    //2.显示法
    Person p4;
    Person p5 = Person(10);  //Person(10): 匿名对象,当前行执行结束后,系统会立即回收掉匿名对象
    Person p6 = Person(p5);
    //3.隐式转换法
    Person p7 = 10;
    Person p8 = p7;
}

复制构造函数的使用时机

  • 使用一个已经创建完毕的对象来初始化一个新对象
  • 值传递的方式给函数参数传值
  • 以值方式返回局部对象(由于编译优化可能不会调用)
class Person
{
public:
    Person()
    {
        cout<<"Person默认构造函数调用"<<endl;
    }
    Person(int age)
    {
        cout<<"Person有参构造函数调用"<<endl;
        m_Age = age;
    }
    Person(const Person &p)
    {
        cout<<"Person复制构造函数调用"<<endl;
        m_Age = p.m_Age;
    }
    ~Person()
    {
        cout<<"Person析构函数调用"<<endl;
    }
    
    int m_Age;
};
//1.使用一个已经创建完毕的对象来初始化一个新对象
void test01()
{
    Person p1(20);
    Person p2(p1);
}
//2.值传递的方式给函数参数传值
void doWork(Person p)
{}
void test02()
{
    Person p;
    doWork(p);
}
//3.以值方式返回局部对象
Person doWork2()
{
    Person p1;
    return p1;
}
void test03()
{
    Person p = doWork2();
}

构造函数调用规则

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

  1. 默认构造函数(无参,函数体为空)
  2. 默认析构函数(无参,函数体为空)
  3. 默认复制构造函数,对所有属性值进行复制
  4. 赋值运算符 operator=,对属性值进行复制

构造函数调用规则如下:

  • 如果用户定义了有参构造函数,则默认无参构造函数不再提供
  • 如果用户定义了复制构造函数,则其他构造函数都不会被提供

shallowCopy和deepCopy

class Person{
public:
    //无参构造函数
    Person(){}
    //有参构造函数
    Person(int age, int height){
        m_Age = age;
        m_Height = new int(height);
    }
    //复制构造函数
    Person(const Person &p){
        m_Age = p.m_Age;
        
        //shallow copy
        //m_Height = p.m_Height;
        //shallow copy仅仅复制了m_Height这个指针,而没有复制堆区的空间,会导致堆区的重复delete
        
        //deep copy
        m_Height = new int(*p.m_Height);
    }
    
    //析构函数
    ~Person(){
        if(m_Height != NULL){
            delete m_Height;
            m_Height = NULL;
        }
    }
public:
    int m_Age;
    int* m_Height;
};

void test01()
{
    Person p1(18,180);
    Person p2(p1);
    cout<<"p1的年龄:"<<p1.m_Age<<"  身高:"<<*p1.m_Height<<endl;
    cout<<"p2的年龄:"<<p2.m_Age<<"  身高:"<<*p2.m_Height<<endl;
}
int main(){
    test01();
    return 0;
}

总结:如果属性有在堆区开辟的,要自己提供复制构造函数,防止shallowCopy带来的问题

初始化列表

语法:构造函数(): 属性1(值1), 属性2(值2) ... {}

class Person{
public:
    Person(int a, int b, int c): m_A(a), m_B(b), m_C(c) {}
private:
    int m_A;
    int m_B;
    int m_C;
};
int main(){
    Person p(10,20,30);
    return 0;
}

类对象作为类成员

例如:

class A{};
class B{
    A a;
};

构造顺序:先调用对象成员的构造,再调用本类构造

析构顺序:先析构本类,再析构对象成员

静态成员

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

静态成员分为:

  • 静态成员变量:

    • 所有对象共享同一份数据
    • 在编译阶段分配内存
    • 类内声明,类外初始化
  • 静态成员函数

    • 所有对象共享同一个函数
    • 静态成员函数只能访问静态成员变量

示例1:静态成员变量

class Person
{
public:
    //类内声明
    static int m_A;
private:
    static int m_B;
};

//类外初始化
int Person::m_A = 100;
int Person::m_B = 300;

void test01(){
    Person p1;
    cout<<p1.m_A<<endl;    //输出100
    
    Person p2;
    p2.m_A = 200;
    
    cout<<p1.m_A<<endl;    //输出200,所有对象共享
}

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

示例2:静态成员函数

class Person
{
public:
    static void func()
    {
        m_A = 100;
        cout<<"static void func调用"<<endl;
    }
    static int m_A = 10;
};
void test01()
{
    //1.通过对象访问
    Person p;
    p.func();
    
    //2.通过类名访问
    Person::func();
}

C++对象模型和this指针

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

在C++中,类内的成员变量和成员函数分开存储

只有 非静态成员变量 才属于类的对象上

空类的对象占1个字节

this指针

在C++中成员变量和成员函数是分开存储的

每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码

那么问题是:这一块代码是如何区分哪个对象调用自己的呢?

C++通过提供特殊的对象指针---- this 指针,解决上述问题

this 指针指向被调用的成员函数所属的对象

this指针的特性:

  • 隐含于每一个非静态成员函数之内
  • 不需要定义,直接使用
  • 本质是指向不可修改的指针

this指针的用途:

  • 当形参和成员变量同名时,可以使用 this 指针来区分
  • 在类的非静态成员函数中返回对象本身
class Apple
{
public:
    Apple(int num){
        //1.解决命名重复
        this->num=num;
    }
    
    Apple& myAdd(Apple& p){
        num+=p.num;
        //2.返回本体
        return *this;
    }
    
    int num;
};
int main(){
    Apple a1(10);
    Apple a2(20);
    //链式编程思想
    a2.myAdd(a1).myAdd(a1);
    cout<<a2.num<<endl;
}

空指针访问成员函数

C++中空指针是可以调用成员函数的,但要注意有没有用到this指针

如果成员函数没有用到this指针,那么空指针可以直接访问成员函数

如果成员函数用到this指针,就要判断指针是否为空,防止崩溃

class Person
{
public:
    void showClassName()
    {
        cout<<"This is a Person class"<<endl;
    }
    void showAge()
    {
        if(this==NULL){return;}  //防止崩溃
        cout<<"age= "<<m_Age<<endl;
    }
    
    int m_Age;
};
void test01()
{
    Person* p = NULL;
    p->showClassName();  //正常调用
    p->showAge();        //无法调用
}

常函数和常对象

常函数:

  • 成员函数 加const, 我们称为这个函数为常函数

    • const实质上修饰的是this指针
  • 常函数内不可以修改成员属性
  • 成员属性声明时前面加关键字mutable,在常函数中依然可以修改

    • mutable int m_B;

常对象:

  • 声明对象前加const称该对象为常对象
  • 常对象的属性不可修改,但是mutable属性仍然可以修改
  • 常对象只能调用常函数

友元

生活中,家里有客厅(Public),有卧室(Private)

客厅所有来的客人都可以进去,但是卧室是私有的,也就是说只有自己能进去。但是呢,你也可以允许你的好朋友进去。

在程序里,有些私有属性也想让类外特殊的一些函数或者进行访问,就需要用到友元的技术

友元的目的就是让一个函数或者类,访问另一个类中的私有成员

关键字: friend

友元的三种实现:

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

全局函数做友元

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

class Building
{
    //好友认证
    friend void goodBoy(Building& building);
public:
    Building()
    {
        m_SittingRoom = "客厅";
        m_BedRoom = "卧室";
    }
public:
    string m_SittingRoom;
private:
    string m_BedRoom;
};

void goodBoy(Building& building)
{
    cout<<"好朋友全局函数正在访问:"<<building.m_SittingRoom<<endl;
    cout<<"好朋友全局函数正在访问:"<<building.m_BedRoom<<endl;
}
int main()
{
    Building b1;
    goodBoy(b1);
}

类做友元

class Building;
class GoodBoy
{
public:

    GoodBoy();
    void visit();  //参观

private:
    Building* building;
};
class Building
{
    //好友认证
    friend class GoodBoy;

public:
    Building();

public:
    string m_SittingRoom;
private:
    string m_BedRoom;
};

//类外写成员函数
Building::Building() {
        m_SittingRoom = "客厅";
        m_BedRoom = "卧室";
    }
GoodBoy::GoodBoy() {
    //创建建筑物对象
    building = new Building;
}
void GoodBoy::visit() {
    cout<<"好朋友类正在访问:"<<building->m_SittingRoom<<endl;
    cout<<"好朋友类正在访问:"<<building->m_BedRoom<<endl;
}

int main(){
    GoodBoy g1;
    g1.visit();
}

成员函数做友元

class Building;
class GoodBoy
{
public:

    GoodBoy();
    void visit();   //让visit函数可以访问Building中的私有成员
    void visit2();  //让visit2访问不到

private:
    Building* building;
};
class Building
{
    //好友认证
    friend void GoodBoy::visit();

public:
    Building();
public:
    string m_SittingRoom;
private:
    string m_BedRoom;
};
Building::Building() {
        m_SittingRoom = "客厅";
        m_BedRoom = "卧室";
    }
GoodBoy::GoodBoy() {
    building = new Building;
}
void GoodBoy::visit() {
    cout<<"visit函数正在访问:"<<building->m_SittingRoom<<endl;
    cout<<"visit函数正在访问:"<<building->m_BedRoom<<endl;
}
void GoodBoy::visit2() {
    cout<<"visit2函数正在访问:"<<building->m_SittingRoom<<endl;
    //cout<<"visit函数正在访问:"<<building->m_BedRoom<<endl;
}
int main(){
    GoodBoy g1;
    g1.visit();
    g1.visit2();
}

运算符重载

运算符重载概念:对已有的运算符进行重新定义,赋予其另一种功能,以适应不同的数据类型

加号运算符重载

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

class Building
{
public:
    Building(int a, int b):m_A(a), m_B(b){};

    //通过成员函数重载+号
    Building operator+(Building& b) const
    {
        Building temp(0,0);
        temp.m_A = m_A + b.m_A;
        temp.m_B = m_B + b.m_B;
        return temp;
    }


    int m_A, m_B;
};

//通过全局函数重载+号
Building operator+(Building& b1, Building& b2)
{
    Building temp(0,0);
    temp.m_A = b1.m_A + b2.m_A;
    temp.m_B = b1.m_B + b2.m_B;
    return temp;
}
//运算符重载 也可以发生函数重载
Building operator+(Building& b, int a)
{
    Building temp(0,0);
    temp.m_A = b.m_A + a;
    temp.m_B = b.m_B + a;
    return temp;
}

int main()
{
    Building b1(10,20), b2(20,10);
    //Building b3 = b1.operator+(b2);或者Building b3 = operator+(b1, b2);简化为:
    Building b3 = b1 + b2;
    b3 = b3 + 10;
    cout<<"b3: "<<b3.m_A<<" "<<b3.m_B<<endl;
}

注意:

  1. 对于内置数据类型的表达式的运算符是不能改变的
  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<<cout,而不是 cout<<p

private:
    int m_A;
    int m_B;
};

//只能利用全局函数重载左移运算符,并返回cout,实现连续输出
ostream& operator<<(ostream& cout, Person& p)  //本质 operator<<(cout,p) 简化为 cout<<p
{
    cout<<"m_A = "<<p.m_A<<" m_B = "<<p.m_B;
    return cout;
}
int main(){
    Person p(10,20);
    cout<<p<<" (<<重载测试)"<<endl;  //链式编程思想
}

递增运算符重载

class MyInteger {

    friend ostream& operator<<(ostream& cout, MyInteger a);

public:
    MyInteger() { m_Num =0; }

    //重载前置递增运算符,前置递增运算符可以连用(++(++a))
    MyInteger& operator++(){
        ++m_Num;
        return *this;
    }
    //重载后置递增运算符,后置递增运算符无法连用
    MyInteger operator++(int){
        MyInteger temp = *this;
        m_Num++;
        return temp;
    }

private:
    int m_Num;
};

ostream& operator<<(ostream& cout, MyInteger a)
{
    cout<<"m_Mum = "<<a.m_Num;
    return cout;
}

int main(){
    MyInteger a;
    cout << a << endl;
    cout << ++(++a)<<endl;
    cout << a++ <<endl;
    cout << a <<endl;
}

赋值运算符重载

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

  1. 默认构造函数(无参,函数体为空)
  2. 默认析构函数(无参,函数体为空)
  3. 默认复制构造函数,对所有属性值进行复制
  4. 赋值运算符 operator=,对属性值进行复制

如果类中有属性指向堆区,做赋值操作时也会出现shallow/deep copy问题

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;  //shallowCopy

        //应该先判断是否有属性在堆区,如果有,先释放,再deepCopy
        if(m_Age != NULL){
            delete m_Age;
            m_Age = NULL;
        }
        m_Age = new int (*p.m_Age);

        return *this;
    }

    int* m_Age;
};
int main(){
    Person p1(18);
    Person p2(20);
    Person p3(22);
    p1 = p2 = p3;  //赋值操作,不重载"="会导致堆区内存重复释放
    cout<<"p1的年龄为:"<<*p1.m_Age<<endl;
}

关系运算符重载

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

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

    //重载"=="号  //"!="号重载同理
    bool operator==(Person& p) const{
        if(m_Age==p.m_Age && m_Name==p.m_Name)return true;
        else return false;
    }

    string m_Name;
    int m_Age;
};
void test01(){
    Person p1("Tom",18);
    Person p2("Tom",18);
    if(p1==p2)cout<<"相等"<<endl;
    else cout<<"不相等";
}
int main(){test01();return 0;}

函数调用运算符重载

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

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

//加法类
class MyAdd
{
public:
    int operator()(int num1, int num2){
        return num1 + num2;
    }
};

void test01()
{
    MyPrint myPrint;
    myPrint("Hello world");
}
void test02(){
    MyAdd myAdd;
    int ret = myAdd(3,4);
    cout<<ret<<endl;

    //匿名函数对象,当前行执行完毕后被释放
    cout<<MyAdd()(100,100)<<endl;
}
int main(){test01();test02();return 0;}

继承 (inheritance)

继承是面向对象三大特性之一
有些类与类之间存在着特殊的关系,例如下图中:

flowchart TB classDef node fill:#a0eee115,stroke:#333; 动物---猫 & 狗 猫---加菲猫 & 布偶猫 & A & 波斯猫 狗---哈士奇 & 京巴 & B & 德国牧羊犬 A["……"] B["……"]

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

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

继承的基本语法

class 子类(派生类) : 继承方式 父类(基类);

派生类的成员,包含两大部分:

一类是从基类继承过来的,一类是自己增加的成员

从基类继承过来的表现其共性,而新增的成员体现了其个性

继承方式

继承方式有三种:

  • public
  • protected
  • private
graph TB classDef node fill:#a0eee115,stroke:#333; A["public:
int a;
protected:
int b;
private:
int c; "]--公共继承---B[" public:
int a;
protected:
int b;
//不可访问:
int c;
"] A--保护继承---C[" protected:
int a;
int b;
//不可访问:
int c; "] A--私有继承---D[" private:
int a;
int b;
//不可访问:
int c;
"]

继承中的对象模型

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

父类中的私有成员,被编译器隐藏,所以无法访问

利用visual studio的开发人员命令提示工具查看对象模型:

在文件路径下,输入 cl /d1 reportSingleClassLayout类名 文件名

继承中的构造和析构顺序

构造子类时:先调用父类构造函数,再调用子类构造函数

析构子类时:先调用子类析构函数,再调用父类析构函数

继承中的同名成员处理

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

  • 访问子类同名成员 直接访问即可
  • 访问父类同名成员 需要加作用域
  • 当子类与父类拥有同名的成员函数,子类会隐藏掉父类中的所有同名成员函数(包括重载的函数),加作用域可以访问到父类中的同名成员函数
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;
};

int main(){
    Son s;
    cout<<s.m_A<<endl;
    cout<<s.Base::m_A<<endl;
    s.func();
    s.Base::func();
    s.Base::func(0);
    return 0;
}
  • 同名静态成员的访问方式

    • 通过对象访问

    子类:s.m_A

    父类:s.Base::m_A

    • 通过类名访问

    子类:Son::m_A

    父类:Son::Base::m_A

多继承、菱形继承与虚继承

多 继 承 语 法

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

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

多继承可能会出现父类中有同名成员的情况,子类使用时要加作用域

菱 形 继 承

flowchart TB classDef node fill:#a0eee115,stroke:#333; 动物 --- 水生动物 & 陆生动物 --- 两栖动物

菱形继承的问题:

  1. 动物的数据经过菱形继承出现了两份,使用时会产生二义性
  2. 实际上只需要一份数据,产生了资源浪费

解决方法:虚继承

虚 继 承

关键字:virtual

class Base
{
public:
    int m_A;
};

class A : virtual public Base {}; 
class B : virtual public Base {};
class AB : public A, public B {};

ABA , B 继承下来的是 vbptr

vbptr 即 virtual base pointer ,虚基类指针

graph TB subgraph class AB subgraph A["(base class A)
{vbptr}"] B["(base class B)
{vbptr}"] end Ba["(virtual base Base)
m_A"] end

 
虚继承时,虚基类指针指向虚基类表(vbtable),虚基类表中存放的是数据相对于虚基类指针的偏移,从而根据偏移找到数据

graph LR A["{vbptr}"] T["(vbtable)
0    |0         
1    |偏移量"] M["m_A"] A --> T --> M

多态 (polymorphism)

多态的基本概念

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

多态分为两类

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

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

  • 静态多态:函数地址早绑定,编译阶段确定函数地址
  • 动态多态:函数地址晚绑定,运行阶段确定函数地址

动态多态:使用虚函数,实现函数地址晚绑定

关键字: virtual

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;
    }
};
void DoSpeak(Animal& animal){
    animal.speak();
}

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

    Dog dog;
    DoSpeak(dog);
}
int main(){test01();return 0;}

多态满足条件:

  1. 有继承关系
  2. 子类重写(override)父类的虚函数

多态使用条件:

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

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

多态的原理

vfptr:virtual function pointer,虚函数(表)指针

vftable:virtual function table,虚函数表

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

当父类的指针或引用指向子类对象的时候,发生多态

Animal& animal = cat;

animal.speak();

flowchart TB subgraph s1["Animal 类内部结构"] v2["vfptr"] subgraph s11["vftable"] v3["&Animal::speak"] end end subgraph s2["Cat 类内部结构"] v4["vfptr"] subgraph s21["vftable"] v5["&Animal::speak"] end end subgraph s3["Cat 类内部结构"] v6["vfptr"] subgraph s31["vftable"] v7["&Cat::speak"] end end v2 --> s11 v4 --> s21 v6 --> s31 s1 -.继承.-> s2 s2 -.重写.-> s3

纯虚函数和抽象类

在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容

因此可以将虚函数改成纯虚函数

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

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

抽象类特点:

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

虚析构和纯虚析构

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

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

虚析构和纯虚析构共性:

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

虚析构和纯虚析构区别:

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

虚析构语法:

virtual ~类名(){}

纯虚析构语法:

virtual ~类名() = 0;

类名::~类名(){}

注:如果子类中没有堆区数据,可以不写为虚析构或纯虚析构