c++多态

130 阅读8分钟

多态的概念

不同类型的对象 ( 这些类型具有继承关系 ),去做同一件事情时,会产生不同的结果.

以人买票为例:

image.png

代码实现多态

( 1 ) 虚函数

被virtual修饰的 类成员函数 叫虚函数.

( 2 ) 虚函数的重写

子类有一个和父类完全相同的虚函数(返回类型、函数名、参数类型列表完全相同),

则子类重写了父类的虚函数

class Person
{
public:
    //父类的虚函数
    virtual void buy_ticket()
    {
        cout << "全价买票" << endl;
    }
};
class Student:public Person
{
public:
    //子类重写了父类的虚函数
    virtual void buy_ticket()
    {
        cout << "半价买票" << endl;
    }
};

( 3 ) 父类的指针或引用去调用虚函数

父类的指针/引用可以指向父类对象,也可以指向子类对象(切片).

父类指针 指向 父类对象,调用父类的虚函数;

父类指针 指向 子类对象,调用子类的虚函数.

void test1()
{
    //父类指针指向父类对象
    Person* p1 = new Person;

    //父类指针指向子类对象(切片现象)
    Person* p2 = new Student;

    p1->buy_ticket();  //调用父类的虚函数
    p2->buy_ticket();  //调用子类的虚函数
}

--特例

( 1 ) 子类要重写虚函数,可以不加virtual

class Person
{
public:
    //父类的虚函数
    virtual void buy_ticket()
    {
        cout << "全价买票" << endl;
    }
};

class Student:public Person
{
public:
    //子类不加virtual,仍然重写了父类的虚函数
    void buy_ticket()
    {
        cout << "半价买票" << endl;
    }
};

( 2 ) 协变:返回值可以不同,但必须是具有父子关系的指针/引用.

父类的虚函数返回父类指针或引用;

子类的虚函数返回子类指针或引用.

只要是具有父子关系的指针/引用都可以,不限类型

class Person
{
public:
    //协变
    //返回类型是父子关系的指针/引用 
    virtual Person* buy_ticket()
    {
        cout << "全价买票" << endl;
        return nullptr;
    }
};

class Student:public Person
{
public:
    Student* buy_ticket()
    {
        cout << "半价买票" << endl;
        return nullptr;
    }
};

--接口继承和实现继承

( 1 ) 普通函数的继承是实现继承,把父类整个函数全部继承下来,包括函数接口和具体实现.

class A
{
public:
	void ordinary_func()
	{
		cout << "普通函数继承:" << "this指针类型:" << typeid(this).name() << endl;
	}
};
class B:public A
{public:};

void test1()
{
	B b;
	b.ordinary_func();
}

b.ordinary_func()--编译器处理为b.ordinary_func(&b)

将 B* 传递给this指针,由于接口使用的是父类,

父类的this指针是A* const类型,中间会发生切片.

( 2 ) 构成多态是接口继承,把父类的虚函数接口声明拿下来

class A
{
public:
	virtual void virtual_func(int a = 10)
	{
		cout << "参数a= " << a << endl;
	}
};
class B:public A
{
public:
	void virtual_func(int a = 5)
	{
		cout << "参数a= " << a << endl;
	}
};

void test2()
{
	A* ptr = new B;
	ptr->virtual_func();//构成多态,接口继承 a = 10
	
	B b;
	b.virtual_func();//不构成多态,普通继承 a = 5
}

image.png

多态的原理

为什么能实现:

父类指针/引用 指向 父类对象,调用父类的虚函数

父类指针/引用 指向 子类对象,调用子类的虚函数

--虚函数表

( 1 ) 对于一个类,编译器会把虚函数的地址,放进虚函数表中

一般而言,每个类都有各自的虚函数表.

( 2 ) 类对象,会多一个成员变量,保存虚函数表的地址

同类对象会共享一个虚函数表.

class A
{
public:
	virtual void func1(){}
	virtual void func2(){}
};
class B
{
public:
	virtual void func3(){}
	virtual void func4(){}
};

void test2()
{
	A a;
  cout << sizeof(A);        
	B b;
}

image.png

( 3 ) 在单继承中,若父类有虚函数,例:

class A
{
public:
	virtual void func1(){}
	virtual void func2(){}
public:
	int _a;
};

class B:public A
{
public:
	int _b;
};

image.png

B类对象和A类对象都只有一个虚表指针,但指向的虚表是不一样的.

( 4 ) 若子类没有完成虚函数的重写,

那 子类虚表的对应虚函数地址 和 父类虚表的对应虚函数地址 相同;

若子类完成虚函数的重写,

那 子类虚表的对应虚函数地址 和 父类虚表的对应虚函数地址 不同

class A
{
public:
	virtual void func1(){}
	virtual void func2(){}
public:
	int _a;
};

class B:public A
{
public:
	//重写func1(),不重写func2()
	virtual void func1(){}
public:
	int _b;
};

image.png

完整结论

编译器检查构成多态,运行时就会到 父类指针/引用 指向的对象中,

找到对象的虚函数表指针 -> 去对象的虚函数表中找到对应虚函数地址

image.png

( 5 ) 虚函数表存在哪里?

一个类的虚函数表是存在常量区/代码段的,

在编译阶段就生成.

image.png

--多态调用特例

若父类有虚函数,没有完成重写,此时用父类指针/引用去调用该虚函数时,

仍然会到虚表中找虚函数地址.

class A
{
public:
	virtual void func1(){}
};

class B:public A
{public:};

void test4()
{
	A* a = new B;
	a->func1();
}

image.png

编译器不会具体看有没有完成重写,

调用函数在父类中是否是虚函数?是否是父类指针或引用去调用?

只要满足如上两个条件,就去指向对象的虚表中找到虚函数地址.

但是没完成重写,父类和子类的虚表里,存的都是父类的虚函数地址.

--模拟调用虚函数

以32位平台为例,因为指针长度较短.

以下面的代码为例:

class A
{
public:
	virtual void func1()
	{
		cout << "A::func1()" << endl;
	}
	virtual void func2()
	{
		cout << "A::func2()" << endl;
	}

public:
	int _a;
};

class B:public A
{
public:
	virtual void func1()
	{
		cout << "B::func1()" << endl;
	}
	virtual void func2()
	{
		cout << "B::func2()" << endl;
	}

public:
	int _b;
};

虚函数表本质是一个函数指针数组,

即一个数组,每个元素都是函数地址,每个元素的类型都是函数指针类型.

typedef void(*VFPTR)();//将【无返回值无参的函数】指针类型 typedef 成 VFPTR

具体步骤

( 1 ) 拿到对象中,虚函数表的指针(一般在前4/8byte).

void test5()
{
	A a;
	*(int*)&a;//拿到虚函数表的地址,但是用int表示

   (VFPTR*)*(int*)&a;//该虚函数表存的都是VFPTR类型元素,强转成VFPTR*
}

image.png

( 2 ) 用虚函数表指针找到虚函数表.

( 3 ) 打印虚函数表的内容,并调用虚函数表里的虚函数.

//显示传虚函数数目
void printVFTable(VFPTR vftable[], int num)
{
	for (int i = 0; i < num; ++i)
	{
		cout << vftable[i] << endl;
		vftable[i]();//函数地址+()可以直接调用,已经越过了正常的调用方式
	}
}
void test5()
{
	A a;
	B b;

	printVFTable((VFPTR*)*(int*)&a, 2);
	cout << endl;
	printVFTable((VFPTR*)*(int*)&b, 2);
}

image.png

多继承里的多态

--对象模型

会出现多个虚表的情况.

例:

class A1
{
public:
	virtual void func1(){}
	virtual void func2(){}
public:
	int _a1 = 1;
};

class A2
{
public:
	virtual void func3(){}
	virtual void func4(){}
public:
	int _a2 = 2;
};

class B:public A1,public A2
{
public:
	int _b = 3;
};

image.png

--特殊情况

若子类单独增加一个虚函数,只会被放进第一张虚表中.

class B:public A1,public A2
{
	virtual void func5(){}
public:
	int _b = 3;
};
void test7()
{
	B b;
	//打印第一张虚表
	printVFTable((VFPTR*)*(int*)&b, 3);
	cout << endl;
	//打印第二张虚表
	
	//切片
	A2* a2 = &b;
	printVFTable((VFPTR*)*(int*)a2, 3);//在某些编译器会崩溃,因为第二张虚表里只有2个虚函数地址
}

image.png

构造函数/析构函数与虚函数

--构造函数不能是虚函数

对象的虚函数表指针,是在构造函数里初始化列表阶段,进行初始化的.

( 1 ) 若虚函数表指针没有初始化,无法找到对象的虚函数表

( 2 ) 构造函数若作为虚函数,多态调用时,就会先找虚函数表,再调用构造函数.

但此时虚函数表指针没有初始化,无法找到虚函数表,也无法调用构造函数.

--析构函数建议为虚函数

( 1 ) 析构函数名会被编译器统一处理为destructor,

因此父类析构函数加了virtual,子类都完成了虚函数重写.

( 2 ) delete释放空间时,会先调用析构函数清理对象,再调用operator delete()释放堆区空间.

( 3 ) 父类指针可以指向 new出来的父类对象,也可以指向 new出来的子类对象.

在delete时,只有把父类的析构函数设为虚函数,才能正确调用析构函数

【即去指针指向的对象中,找到虚函数表指针,找到对应虚函数进行调用】

否则,一律只调用父类的析构函数,因为指针类型是父类的指针.

class A
{
public:
	~A() { cout << "~A()" << endl; }
};
class B:public A
{
public:
	~B() { cout << "~B()" << endl; }
};
void test8()
{
	A* a = new A;
	A* b = new B;
	delete a;
	delete b;
}

image.png

抽象类

( 1 ) 在虚函数声明后加上=0,则该虚函数为纯虚函数.

( 2 ) 包含纯虚函数的类叫抽象类,抽象类无法实例化出对象.

子类继承后,也不能实例化出对象,只有重写纯虚函数,子类才能实例化出对象.

class A
{
public:
	virtual void func1() = 0;
};
class B:public A
{
public:
	virtual void func1(){}
public:
	int _b = 3;
};
void test8()
{
	B b;
}

间接要求抽象类的子类必须重写纯虚函数.