自学内容网 自学内容网

C++多态详解

多态的使用

概念

多态分为静态多态(编译时多态)和动态多态(运行时多态)。
静态多态是函数重载和函数模版,传不同类型的参数可以调不同的函数,通过参数的不同达到了多种形态。静态是因为实参传给形参的参数匹配是在编译时完成的
动态多态是传不同的对象完成不同的行为(函数),达到多种形态,比如买票这个行为,普通人买票时全价,学生买票是半价,军人买票是优先买票。
在这里插入图片描述

多态的构成条件和使用

  • 多态的构成条件:
    多态是一个继承关系下的类对象,调用同一个函数,产生不同的行为

  • 虚函数

class Person
{
public:
// 虚函数
virtual void Func()
{
cout << "买票" << endl;
}
};
  • 虚函数的重写/覆盖
    重写/覆盖的条件:子类必须有一个和父类完全相同的虚函数(返回值类型,函数名,参数列表的类型完全相同
    重写是重写了实现

  • 实现多态的两个重要条件:

  1. 必须是父类的指针或引用调用虚函数
  2. 被调用的函数必须是虚函数,且子类必须对父类虚函数完成重写/覆盖

满足多态指向谁调用谁,不满足多态看函数参数的类型是什么就调用什么

class Person 
{
public:
// 条件3
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};

class Student : public Person 
{
public:
    // 条件3
virtual void BuyTicket() { cout << "买票-打折" << endl; }
};

// 不满足多态的情况下指向谁调用谁,ptr指向Person类都调用Person类
// 满足多态就和指向对象的类型有关是哪个对象类型就调用哪个类
void Func(Person* ptr)// 条件1
{
// 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket
// 但是跟ptr没关系,而是由ptr指向的对象决定的。
ptr->BuyTicket();// 条件2
}

int main()
{
Person ps;
Student st;

Func(&ps);
Func(&st);

return 0;
}

在这里插入图片描述

  • 注意:在重写基类虚函数时,派⽣类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用,很多题有这样的坑。

子类的子类也可以不加virtual,只要父类加了virtual,就能继承下来

一道选择题

B
在这里插入图片描述
重写的本质是重写虚函数的实现部分
绝不重新定义继承而来的缺省值,保留原来的缺省值

在这里插入图片描述

class A
{
public:
virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
virtual void test() { func(); }
};

class B : public A
{
public:
void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};

int main(int argc, char* argv[])
{
B* p = new B;

p->test();
// B->1
p->func();
// B->0
//  没有构成多态,是子类的指针
//  在自己的域里找,找不到到继承的域里找

return 0;
}

协变(了解)

  • 子类重写父类虚函数时,与父类虚函数返回值类型不同。即父类虚函数返回父类对象的指针或者引
    子类虚函数返回子类对象的指针或者引用时,称为协变。
class A {};
class B : public A {};
// 返回的类必须也必须是子类继承父类的关系
// 可以是不同的类或不是自己的类
// 可以是自己的类

class Person 
{
public:
/*virtual A* BuyTicket()
{
cout << "买票-全价" << endl;
return nullptr;
}*/
virtual Person* BuyTicket()
{
cout << "买票-全价" << endl;
return nullptr;
}
};

class Student : public Person 
{
public:
/*virtual B* BuyTicket()
{
cout << "买票-打折" << endl;
return nullptr;
}*/
virtual Student* BuyTicket()
{
cout << "买票-打折" << endl;
return nullptr;
}
};

void Func(Person* ptr)
{
ptr->BuyTicket();
}

int main()
{
Person ps;
Student st;

Func(&ps);
Func(&st);

return 0;
}

析构函数的重写

  • 父类的析构函数为虚函数此时子类析构函数只要定义,无论是否加virtual关键字,都与父类的析构函数构成重写,虽然父类与子类析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor,所以父类的析构函数加了vialtual修饰,子类的析构函数就构成重写。
// 析构函数的名字会被特殊处理为destructor
// 变相的符合了3重
class A
{
public:
virtual ~A()
{
cout << "~A()" << endl;
}
};

class B : public A 
{
public:
~B()
{
cout << "~B()->delete:" << _p << endl;
delete _p;
}
protected:
int* _p = new int[10];
};

// 只有派生类Student的析构函数重写了Person的析构函数,下⾯的delete对象调⽤析构函数,才能
// 构成多态,才能保证p1和p2指向的对象正确的调用析构函数。

int main()
{
// 父类的指针指向子类,根据指向的对象调用析构函数
// 不构成多态,会根据父类的类型调用两次A类
// 不构成多态存在内存泄漏的问题
A* p1 = new A;
A* p2 = new B;

// p1->destructor() + operator delete(free)
delete p1;
delete p2;

return 0;
}

为什么会调用两次A类的析构?
因为B类继承了A类,B类的析构结束会自动调用A类的析构

在这里插入图片描述

所以继承那里构成~A构成函数隐藏,为什么构成函数隐藏,因为处理后函数名相同(destructor),为什么函数名相同,因为多态这里为了防止内存泄漏,为了构成虚函数,指向的对象不同调用A和B类的析构

class A
{
public:
~A()
{
cout << "~A()" << endl;
}
};

class B : public A 
{
public:
~B()
{
cout << "~B()->delete:" << _p << endl;
delete _p;
}
protected:
int* _p = new int[10];
};

在这里插入图片描述

override和final关键字

  • C++11新增的关键字,override用来检查是否构成重写,因为编译器在编译阶段是检查不出来的,在运行时才能检查出来,加override是为了在编译时就检查出来
class Car 
{
public:
virtual void Dirve()
{}
};

class Benz :public Car 
{
public:
virtual void Drive() override { cout << "Benz-舒适" << endl; }
};
  • 如果我们不想在子类重写这个虚函数,用final修饰,不能被重写
class Car
{
public:
virtual void Drive() final {}
};

class Benz :public Car
{
public:
virtual void Drive() { cout << "Benz-舒适" << endl; }
};
  • 继承那里的final,作用是修饰为最终类,不能被继承
class Car final
{
public:
virtual void Drive() final {}
};

重载/重写/隐藏的对比

在这里插入图片描述

纯虚函数和抽象类

  • 在虚函数后面写上=0就是纯虚函数,纯虚函数不需要定义实现(实际上没有什么意义因为要被子类重写,重写之后父类的实现就没用了,但是语法上支持定义实现),只要声明即可包含纯虚函数的类叫做抽象类抽象类不能被实例化出对象如果子类继承后不重写纯虚函数,那么子类也是抽象类。纯虚函数在某种程度上强制了子类重写虚函数,因为不重写实例化不出对象。
class Car
{
public:
virtual void Drive() = 0;
};

class Benz :public Car
{
public:
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};

class BMW :public Car
{
public:
virtual void Drive()
{
cout << "BMW-操控" << endl;
}
};

int main()
{
// 父类无法实例化出对象
// 但是可以用指针和引用来实现多态
// Car c;

Car* pBenz = new Benz;
pBenz->Drive();

Car* pBMW = new BMW;
pBMW->Drive();

return 0;
}

多态是一种相对的概念,B既可以是父类也可以是子类
在这里插入图片描述

多态的原理

虚函数表指针

有几个虚函数就有几个虚函数表指针
虚函数表指针指向一个函数指针数组
在这里插入图片描述
指向谁就调用谁,指向哪个对象就到那个对象的虚函数表中找到对应的函数,进行调用

class Person 
{
public:
virtual void BuyTicket() 
{
cout << "买票-全价" << endl; 
}
private:
string _name;
};

class Student : public Person 
{
public:
virtual void BuyTicket() 
{ 
cout << "买票-打折" << endl; 
}
private:
string _id;
};

class Soldier : public Person 
{
public:
virtual void BuyTicket() 
{ 
cout << "买票-优先" << endl; 
}
private:
string _codename;
};

void Func(Person* ptr)
{
// 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket
// 但是跟ptr没关系,⽽是由ptr指向的对象决定的。
ptr->BuyTicket();
}

int main()
{
// 其次多态不仅仅发生在子类对象之间,多个子类继承父类,重写虚函数后
// 多态也会发生在多个子类之间。
Person ps;
Student st;
Soldier sr;

Func(&ps);
Func(&st);
Func(&sr);

return 0;
}

在这里插入图片描述

动态绑定和静态绑定

  1. 动态绑定是根据指向的对象决定(满足多态)
    满足多态条件的函数调⽤是在运行时绑定,也就是在运行时到指向对象的虚函数表中找到调用函数
    的地址,也就做动态绑定。运行时去找
  2. 静态绑定是根据类型决定(不满足多态)
    对不满足多态条件(指针或者引用+调用虚函数)的函数调用是在编译时绑定,也就是编译时确定调用函数的地址,叫做静态绑定。静态绑定的效率更高一点
    在这里插入图片描述

虚函数表

  1. 有几个虚函数就放几个虚函数的地址到虚函数表中
  2. 同类型的对象虚函数表是一样的,不同类型对象虚表各自独立
    在这里插入图片描述
  3. 子类由两部分构成,继承下来的父类和自己的成员,⼀般情况下,继承下来的父类中有虚函数表
    指针,自己就不会再生成虚函数表指针
    。但是要注意的这⾥继承下来的父类部分虚函数表指针和父
    类对象的虚函数表指针不是同⼀个,就像父类对象的成员和子类对象中的父类对象成员也独立的。
  4. 子类中重写的父类的虚函数,本来是把父类的虚函数的地址拷贝了下来,但是重写了,就覆盖了父类的虚函数的地址
  5. 子类的虚函数表中包含三个部分:
    (1)父类的虚函数地址
    (2)子类重写的虚函数地址完成覆盖
    (3)子类自己的虚函数地址

在这里插入图片描述
6. 虚函数表本质是⼀个存虚函数指针的指针数组(虚函数指针数组),⼀般情况这个数组最后面放了⼀个0x00000000标记。vs有规定,gcc没有规定
7. 虚函数存在哪的?虚函数和普通函数⼀样的,编译好后是⼀段指令,都是存在代码段的,只是虚函数的地址(指针)又存到了虚表中。
8. 虚函数表存在哪的?这个问题严格说并没有标准答案C++标准并没有规定,我们写下面的代码可以对比验证⼀下。vs下是存在代码段(常量区)

class Base 
{
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
void func5() { cout << "Base::func5" << endl; }
protected:
int a = 1;
};

class Derive : public Base
{
public:
// 重写基类的func1
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func1" << endl; }
void func4() { cout << "Derive::func4" << endl; }
protected:
int b = 2;
};

//int main()
//{
//Base b;
//Derive d;
//
//return 0;
//}

int main()
{
int i = 0;
static int j = 1;
int* p1 = new int;
const char* p2 = "xxxxxxxx";

printf("栈:%p\n", &i);
printf("静态区:%p\n", &j);
printf("堆:%p\n", p1);
printf("常量区:%p\n", p2);

Base b;
Derive d;
Base* p3 = &b;
Derive* p4 = &d;

// %p对象就是地址
printf("Person虚表地址:%p\n", *(int*)p3);
printf("Student虚表地址:%p\n", *(int*)p4);
printf("虚函数地址:%p\n", &Base::func1);
printf("普通函数地址:%p\n", &Base::func5);

return 0;
}

在这里插入图片描述


原文地址:https://blog.csdn.net/2301_79722622/article/details/145239927

免责声明:本站文章内容转载自网络资源,如侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!