C++:多态

多态的概念

多态的概念:通俗来说,就是多种形态, 具体点就是去完成某个行为,当不同的对象去完成时会 产生出不同的状态
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如 Student 继承了 Person。 Person 对象买票全价, Student 对象买票半价。

多态的定义及实现

那么在继承中要 构成多态还有两个条件

  1. 必须通过基类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

不同的对象去做同一件事情,多种形态,结果不一样
多态:指向谁,调用谁的虚函数

1、父子类完成虚函数重写(三同:函数名、参数、返回值)

2、父类的指针或者引用去调用虚函数

虚函数

虚函数:即被 virtual 修饰的类成员函数称为虚函数

虚函数的重写

虚函数的重写 ( 覆盖 ) : 派生类中有一个跟基类完全相同的虚函数 ( 即派生类虚函数与基类虚函数的 返回值类型、函数名字、参数列表完全相同 ) ,称子类的虚函数重写了基类的虚函数。

cpp 复制代码
class Person {
public:

 virtual void BuyTicket() { cout << "买票-全价" << endl; }

};

class Student : public Person {
public:

 virtual void BuyTicket() { cout << "买票-半价" << endl; }

};

void Func(Person& p)
{ p.BuyTicket(); }
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
 return 0;
}

虚函数重写的两个例外:

  1. 协变 ( 基类与派生类虚函数返回值类型不同 )

派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指 针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。

cpp 复制代码
class A{};
class B : public A {};
class Person {
public:
 virtual A* f() {return new A;}
};
class Student : public Person {
public:
 virtual B* f() {return new B;}
};
  1. 析构函数的重写 ( 基类与派生类析构函数的名字不同 )
    如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,
    都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,
    看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处
    理,编译后析构函数的名称统一处理成destructor。
cpp 复制代码
class Person {
public:
 virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:
 virtual ~Student() { cout << "~Student()" << endl; }
};
// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函
//数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{
 Person* p1 = new Person;
 Person* p2 = new Student;
 
// p1->destructor + operator delete(p1)
// p2->destructor + operator delete(p2)
// 多态
// 期望:指向父类调用父类析构
// 期望:指向子类调用子类析构
// 结论:建议析构函数定义为虚函数,防止发生内存泄漏

 delete p1;
 delete p2;
 return 0;
}

3.派生类的虚函数virtual可省略

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

cpp 复制代码
class Person {
public:

 virtual void BuyTicket() { cout << "买票-全价" << endl; }

};

class Student : public Person {
public:

void BuyTicket() { cout << "买票-半价" << endl; }

};

void Func(Person& p)
{ p.BuyTicket(); }
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
 return 0;
}

C++11 overridefinal

从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,**因此:**C++11提供了override和final两个关键字,可以帮助用户检测是否重写。

1. final:修饰虚函数,表示该虚函数不能再被重写 。也可修饰一个类,无法被继承。

cpp 复制代码
// 一个类不能被继承
//class Car
// C++11的方法: final修饰的类叫最终类,不能继承
class Car final
{
public:

private:
	// C++98的方法:父类的构造函数私有
	// 子类的构造无法生成和实现,导致子类对象无法实例化
	Car()
	{}
};

class Benz :public Car
{
public:

};

2. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

重载、覆盖**(重写)、隐藏(重定义)**的对比

多态的原理

虚函数表

通过观察测试我们发现 b 对象是 8bytes , 除了 _b 成员,还多一个 __vfptr 放在对象的前面 ( 注意有些 平台可能会放到对象的最后面,这个跟平台有关 ) ,对象中的这个指针我们叫做虚函数表指针 (v virtual f 代表 function) 。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。

// 针对上面的代码我们做出以下改造
// 1. 我们增加一个派生类 Derive 去继承 Base
// 2.Derive 中重写 Func1
// 3.Base 再增加一个虚函数 Func2 和一个普通函数 Func3
class Base
{
public :
virtual void Func1 ()
{
cout << "Base::Func1()" << endl ;
}
virtual void Func2 ()
{
cout << "Base::Func2()" << endl ;
}
void Func3 ()
{
cout << "Base::Func3()" << endl ;
}
private :
int _b = 1 ;
};
class Derive : public Base
{
public :
virtual void Func1 ()
{
cout << "Derive::Func1()" << endl ;
}
private :
int _d = 2 ;
};
int main ()
{
Base b ;
Derive d ;
return 0 ;
}

  1. 派生类对象 d 中也有一个虚表指针, d 对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
  2. 基类 b 对象和派生类 d 对象虚表是不一样的,这里我们发现 Func1 完成了重写,所以 d 的虚表 中存的是重写的 Derive::Func1 ,所以虚函数的重写也叫作覆盖 ,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
  3. 另外 Func2 继承下来后是虚函数,所以放进了虚表, Func3 也继承下来了,但是不是虚函 数,所以不会放进虚表。
  4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一 nullptr 。
  5. 总结一下派生类的虚表生成:
    a. 先将基类中的虚表内容拷贝一份到派生类虚表中
    b. 如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
    c. 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
  6. 注意 虚表存的是虚函数地址,不是虚函数 ,虚函数和普通函数一样的,都是存在代码段的,只是它 的地址又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针
    虚表存在哪的 呢?实际我们去验证一下会发现 vs 下是存在代码段的
cpp 复制代码
class Person {
public:
	void BuyTicket() { cout << "买票-全价" << endl; }

private:
	int _i = 1;
};

class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }

	int _j = 2;
};

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

int main()
{
	Person Mike;
	Func(&Mike);

	Person p1;
	Func(&p1);

	Student Johnson;
	Func(&Johnson);

	return 0;
}
cpp 复制代码
class Base {
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
private:
	int a = 1;
};

class Derive :public Base {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
	virtual void func4() { cout << "Derive::func4" << endl; }
private:
	int b = 2;
};

// 打印对象虚表
typedef void(*VFPTR)();

// 打印函数指针数组
// virtual function table
//void PrintVFT(VFPTR vft[])
void PrintVFT(VFPTR* vft)
{
	for (size_t i = 0; i < 4; i++)
	{
		printf("%p->", vft[i]);

		VFPTR pf = vft[i];
		(*pf)();
		//pf();
	}
}

int main()
{
	Base b;
	Derive d;

	//int ptr = (int)d; // 不支持转换,只有有关联的类型才能互相转
	VFPTR* ptr = (VFPTR*)(*((int*)&d));
	PrintVFT(ptr);

	// 函数指针
	//void (*p1)();
	VFPTR p2;

	// 函数指针数组
	void (*pa1[10])();
	VFPTR pa2[10];


	return 0;
}

相关例题

1.以下程序输出结果是什么()
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 ();
return 0 ;
}
A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确
答案:B

2.多继承中指针偏移问题?下面说法正确的是( )
class Base1 { public : int _b1 ; };
class Base2 { public : int _b2 ; };
class Derive : public Base1 , public Base2 { public : int _d ; };
int main (){
Derive d ;
Base1 * p1 = & d ;
Base2 * p2 = & d ;
Derive * p3 = & d ;
return 0 ;
}
A : p1 == p2 == p3 B : p1 < p2 < p3 C : p1 == p3 != p2 D : p1 != p2 != p3
选C

p1和p2的顺序是由Base1和Base2继承顺序决定的

相关推荐
hopetomorrow9 分钟前
学习路之PHP--使用GROUP BY 发生错误 SELECT list is not in GROUP BY clause .......... 解决
开发语言·学习·php
小牛itbull19 分钟前
ReactPress vs VuePress vs WordPress
开发语言·javascript·reactpress
怀澈12221 分钟前
高性能服务器模型之Reactor(单线程版本)
linux·服务器·网络·c++
请叫我欧皇i27 分钟前
html本地离线引入vant和vue2(详细步骤)
开发语言·前端·javascript
闲暇部落30 分钟前
‌Kotlin中的?.和!!主要区别
android·开发语言·kotlin
GIS瞧葩菜39 分钟前
局部修改3dtiles子模型的位置。
开发语言·javascript·ecmascript
chnming198744 分钟前
STL关联式容器之set
开发语言·c++
威桑1 小时前
MinGW 与 MSVC 的区别与联系及相关特性分析
c++·mingw·msvc
熬夜学编程的小王1 小时前
【C++篇】深度解析 C++ List 容器:底层设计与实现揭秘
开发语言·数据结构·c++·stl·list
yigan_Eins1 小时前
【数论】莫比乌斯函数及其反演
c++·经验分享·算法