前言
面向对象有三大特点:封装、继承、多态。封装可以把属性和方法封装在一个类中,这样当类对象做函数参数时即可以属性也可以使用方法;继承可以实现对代码的复用;而多态则实现了接口和功能的解耦合。
一、问题引出,为什么要有多态?
当我们在子类和父类定义了同名函数时(函数重写),并把子类对象传给父类指针或引用时,我们调用该同名函数,最终都是调用父类的成员函数,这是由于类型兼容性原则导致的(使用public继承时,子类可以当作父类使用)。但是我们现在希望编译器能够自动判断我们传入的是子类还是父类,并调用相应的成员函数。
class Parent { public: void func() { ; } }; class Child { public: void func() { ; } }; //void test(Parent& p) void test(Parent* p) { //p.func(); p->func(); //传入子类对象,依然调用父类的 func 成员 } int main() { Child c; // test(&c); test(c); return 0; }
二、多态的基础知识
1.类型兼容性原则
类型兼容性原则是指:当派生类是公有制继承时,派生类的对象可以当作基类的对象使用(但是基类不能当派生类使用)。这包括
子类对象传递给父类的引用;
子类对象传递给父类的指针;
用子类对象给父类对象赋值;
子类对象初始化父类;
#include <iostream> using namespace std; class A { public: void ptint_a() { cout << "===a===" << endl; } protected: private: }; class B : public A { public: void ptint_b() { cout << "===b===" << endl; } protected: private: }; void func1(A* a) { a->ptint_a(); } void func2(A& a) { a.ptint_a(); } /* void func2(B& b) { b.ptint_b(); }*/ void func3(B& b) { b.ptint_b(); } int main() { A a1; B b1; A a2 = b1; //子类对象初始化父类//会调用拷贝构造函数 func1(&b1); //子类对象传递给父类的指针 func2(b1);//会优先匹配 func2(B& b)//子类对象传递给父类的引用 //func3(a1); 基类不能当派生类用 a1 = b1; //可以直接用子类对象给父类对象赋值 system("pause"); return 0; }
2.重载重写重定义
- 重载
同一个类中,函数名相同,参数类型、个数不同; - 重写
在子类与父类中,父类与子类的函数原型完全相同;
有 virtual 关键字,虚函数重写,多态;
无 virtual 关键字,重定义; - 名称覆盖
当父类和子类有相同的函数名、变量名出现,发生名称覆盖,子类的函数名,覆盖了父类的函数名。
#include <iostream> using namespace std; class MyClassA { public: void PrintFunc() { cout << "MyClassA 无参函数" << endl; } void PrintFunc(int a) { cout << "MyClassA 一个参数" << endl; } virtual void PrintFunc(int a, int b) { cout << "MyClassA 两个参数" << endl; } }; class MyClassB : public MyClassA { public: void PrintFunc(int a) { cout << "MyClassB 一个参数" << endl; } virtual void PrintFunc(int a, int b) { cout << "MyClassB 两个参数" << endl; } }; /* MyClassA 和 MyClassB 的函数 void PrintFunc(int a) 属于重写(重定义) * MyClassA 和 MyClassB 的函数 virtual void PrintFunc(int a, int b) 属于虚函数重写(多态) * MyClassA 的 void PrintFunc() 在子类中被名称覆盖,子类中无法直接引用 * MyClassA 中的三个 PrintFunc() 属于函数重载 * MyClassB 中的两个 PrintFunc() 属于函数重载 */ int main() { MyClassB b1; //b1.PrintFunc(); //错误 C2661 “MyClassB::PrintFunc”: 没有重载函数接受 0 个参数 //因为子类中也有名为 PrintFunc 的函数,所以他把父类中的同名函数覆盖了,编译器只会在 //子类中查找 PrintFunc 函数,当找不到匹配的参数时,就会报没有重载函数 b1.MyClassA::PrintFunc(); system("pause"); return 0; }
3.动态联编与静态联编
C++与C语言都是静态编译型语言,在编译时,编译器自动根据指针的类型判断指向的是一个什么样的对象;所以编译器认为父类指针指向的是父类对象。由于程序没有运行,所以不可能知道父类指针指向的具体是父类对象还是子类对象。从程序安全的角度,编译器假设父类指针只指向父类对象,因此编译的结果为调用父类的成员函数。这种特性就是静态联编。
联编是指一个程序模块、代码之间互相关联的过程。
静态联编(static binding),是程序的匹配、连接在编译阶段实现,也称为早期匹配。比如重载函数使用静态联编(在编译的时候就决定了怎么执行,函数怎么调用)。
动态联编是指程序联编推迟到运行时进行,所以又称为晚期联编(迟绑定)。比如switch 语句和 if 语句是动态联编的例子(在程序执行的过程中才知得调用哪一个,根据类型决定)。
通过对重写函数加 virtual 关键字来实现动态联编。
三、多态案例
#include <iostream> using namespace std; class GetArea //抽象类//抽象类不能建立对象 { public: virtual double get_area() = 0; //纯虚函数 }; class Square : public GetArea { public: Square(int a, int b) { this->a = a; this->b = b; } public: virtual double get_area() { return a * b; } private: int a; int b; }; class Circular : public GetArea { public: Circular(int r) { this->r = r; } public: virtual double get_area() { return 3.14 * r * r; } private: int r; }; class Triangle : public GetArea { public: Triangle(int h, int l) { this->h = h; this->l = l; } public: virtual double get_area() { return 0.5 * h * l; } private: int h; int l; }; void PrintArea(GetArea& obj)//同一调用语句表现出不同状态 { cout << obj.get_area() << endl; } int main() { Square s(3, 4); Circular c(10); Triangle t(5, 2); PrintArea(s); PrintArea(c); PrintArea(t); system("pause"); return 0; }
C语言实现多态效果的案例可见
四、虚函数
1.虚函数表与 vptr 指针
如果类中有虚函数,那么在使用该类定义对象的时候,会创建一个虚函数表,虚函数表存放了虚函数的入口地址,而 vptr 指向这个虚函数表,父类的vptr指针指向父类的虚函数表,子类的vptr指针指向子类的虚函数表(虚函数的入口地址都在这个虚函数表中)。动态联编就是这么实现的,根据vptr指针决定调用哪个虚函数,当遇到虚函数调用时,会根据vptr指针找到虚函数表,并在虚函数表中查找函数原型。
vptr指针初始化是分步实现的
1.使用子类定义一个子类对象,会初始化子类的vptr指针
2.当父类的构造函数执行时,子类的 vptr 指针会指向父类的虚函数表
3.当父类的构造函数执行完毕时,子类的 vptr 指针指向子类的虚函数表
通过一个例子可以证明vptr指针的分步初始化
#include <iostream> using namespace std; class Class1 { public: Class1() { print_func(); cout << "class 1 构造" << endl; } virtual void print_func() { cout << "class 1" << endl; } }; class Class2 : public Class1 { public: Class2() { print_func(); cout << "class 2 构造" << endl; } virtual void print_func() { cout << "class 2" << endl; } }; int main() { Class2 c2; system("pause"); return 0; }
编译运行
可以看到,在Class1构造函数中调用了Class1的print_func函数,这说明此时vptr指针指向了Class1的虚函数表;在Class2构造函数中调用了Class2的print_func函数,说明此时vptr指针指向了Class2的虚函数表。
2.虚析构函数
虚析构函数是指,在父类析构函数加 virtual 关键字,通过父类指针,执行所有子类的析构函数,释放所有子类内存。
应用场景:一个函数的参数是基类指针,在主调函数 new 分配了内存,需要在被调函数中使用完该对象后 delete
释放内存,我在调用这个函数时传入了一个派生类对象,如果基类中析构函数没有加 virtual 关键字,就会静态联编,delete
时只执行基类的析构函数,而没有执行派生类析构函数,造成了派生类对象中内存泄漏。虚析构函数会执行动态联编,把基类和派生类的析构函数都执行一遍。
#define _CRT_SECURE_NO_WARNINGS #include <iostream> using namespace std; class MyClassA { public: MyClassA(const char* str) { this->pA = new char[strlen(str) + 1]; strcpy(this->pA, str); cout << "A 构造函数" << endl; } //~MyClassA() virtual ~MyClassA()//虚析构函数 { if (this->pA != NULL) { delete[] this->pA; } this->pA = NULL; cout << "A 析构函数 " << endl; } private: char* pA; }; class MyClassB : public MyClassA { public: MyClassB(const char* str) : MyClassA(str) { this->pB = new char[strlen(str) + 1]; strcpy(this->pB, str); cout << "B 构造函数" << endl; } ~MyClassB() { if (this->pB != NULL) { delete[] this->pB; } this->pB = NULL; cout << "B 析构函数 " << endl; } private: char* pB; }; class MyClassC : public MyClassB { public: MyClassC(const char* str) : MyClassB(str) { this->pC = new char[strlen(str) + 1]; strcpy(this->pC, str); cout << "C 构造函数" << endl; } ~MyClassC() { if (this->pC != NULL) { delete[] this->pC; } this->pC = NULL; cout << "C 析构函数 " << endl; } private: char* pC; }; void FuncTest(MyClassA* p) { delete p; } int main() { MyClassC* c = new MyClassC("hello C++"); FuncTest(c); //delete c; //也会调用 C B A 的析构函数 system("pause"); return 0; }
正常析构函数只会执行A(做函数参数的类)的析构函数,因为静态联编,编译器编译时并不知道具体要用哪个类的析构函数,所以只能选择函数参数的类的析构函数执行
将基类的析构函数加 virtual 关键字,即虚析构函数,将会依次调用 C B A(基类和所有派生类)的析构函数;动态联编,在执行时根据指针具体传入哪个类的对象决定如何调用