C++ 多态

文章目录

多态场景的一个选择题

如果能作对这个选择题,那就没必要看这篇文章里,恭喜你,你已经搞懂了多态!!!
问: 以下程序的输出结果是什么()

A: A->0    B: B->1    C: A->1    D: B->0    E: 编译出错    F: 以上都不正确

cpp 复制代码
class A {
public:
	virtual void func(int val = 1) {
		cout << "A->" << val << endl;
	}
	virtual void test() {
		func();
	}
};

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

void test5() {
	B* p = new B;
	p->test();  // 这里答案是什么
	p->func();  //这里答案是什么
}

解释:p->test()

1.p指向的对象是B

2.由于B类没有重写test(),所以实际指向是A*->test()

3.A*调用满足多态(多态必须通过指针或引用来实现)。

4.调用B类的func()函数,重写是重写实现。

cpp 复制代码
virtual void func(int val = 1) {
	 cout << "B->" << val << endl; 
}

5.答案:B->1

解释:p->func()

1.p指向对象B

2.由于B重写了func(),所以实际指向B*->func()

3.答案:B->0

虚函数

类成员函数前面加virtual修饰,那么这个成员函数被称为虚函数。注意非成员函数不能加virtual修饰。

cpp 复制代码
class Person {
public:
	virtual void BuyTicket() {
		cout << "买票-全价" << endl;
	}
};

多态的概念

通俗的说,就是多种形态。

多态的构成条件

多态是一个继承关系下的类对象,去调用同一个函数,产生了不同的行为。

必须是父类的指针或者引用调用虚函数。

被调用的函数必须是虚函数,并且完成了虚函数的重写/覆盖。

要实现多态效果,第一类必须是基类的指针或引用,因为只有基类的指针或引用才能指向基类对象又指向派生类对象。

派生类必须对基类的虚函数完成重写/覆盖,重写或者覆盖了,基类和派生类之间才能有不同的函数,多态的不同形态效果才能达到,

cpp 复制代码
class Person {
public:
	virtual void BuyTicket() {
		cout << "买票-全价" << endl;
	}
};

class Student : public Person {
	//虚函数的重写 返回类型,函数名,参数列表完全相同 
	//但是在子类中重写的虚函数可以不加virtual,尤其注意
	virtual void BuyTicket() {
		cout << "买票-半价" << endl;
	}
};

void Func(Person* ptr) {
	//这里虽然看到是Person指针ptr在调用BuyTickrtet
	//但是这里跟ptr没有关系,而是由ptr指向的对象决定的
	ptr->BuyTicket();
}

void test1() {
	Person ps;
	Student st;
	Func(&ps);  //买票-全价
	Func(&st);  //买票-半价
}

在上述代码中,Student继承了Person。Person对象买票全价,Student对象买票半价。

cpp 复制代码
class Person {
public:
	virtual void BuyTicket() {
		cout << "买票-全价" << endl;
	}
};

class Student : public Person {
	//虚函数的重写 返回类型,函数名,参数列表完全相同 
	//但是在子类中重写的虚函数可以不加virtual,尤其注意
	virtual void BuyTicket() {
		cout << "买票-半价" << endl;
	}
};

void Func(Person* ptr) {
	//这里虽然看到是Person指针ptr在调用BuyTickrtet
	//但是这里跟ptr没有关系,而是由ptr指向的对象决定的
	ptr->BuyTicket();
}

void Func(Person ptr) {
	ptr.BuyTicket();
}

void test2() {
	//不满足多态 会进行切割
	Person ps;
	Student st;
	Func(ps);  //买票-全价
	Func(st);  //买票-全价
}

子上述代码中直接使用的是对象,不满足多态,输出的全是买票-全价。

虚函数的重写/覆盖

子类中有一个和父类完全相同的虚函数(即子类虚函数和父类虚函数的返回值类型,函数名字,参数列表完全相同)称子类的虚函数重写了父类的虚函数。

在重写父类虚函数时,子了的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后父类的虚函数被继承下来了在子类依旧保持虚函数属性)但是这种写法不是很规范,不建议这样写,在笔试中的选择题上经常会故意留下这个坑,让我们判断是否构成多态。

协变

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

cpp 复制代码
class A1 {

};

class B1 : public A1 {

};

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

class Student1 : public Person1 {
public:
	virtual B1* BuyTicket() {
		cout << "买票-打折" << endl;
		return nullptr;
	}
};

void func(Person1* ptr1) {
	ptr1->BuyTicket();
}

void test6() {
	Person1 ps1;
	Student1 st1;

	func(&ps1);
	func(&st1);
}

析构函数的重写

父类的析构函数为虚函数,此时子类析构函数只有定义,无论是否加virtual关键字,都与父类的析构函数构成重写,虽然父类与子类的析构函数名字不同,看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理为destructor ,所以父类的析构函数加了vialtual修饰,子类的析构函数就构成重写。

cpp 复制代码
class A2 {
public:
	virtual ~A2() {
		cout << "~A2" << endl;
	}
};

class B2 : public A2 {
public:
	//构成重写
	// destructor  + operator delete
	virtual ~B2() {
		cout << "~B2->delete" << _p << endl;
		delete _p;
	}
protected:
	int* _p = new int[10];
};


void test7() {
	A2* p1 = new A2;
	A2* p2 = new B2;
	delete p1;
	delete p2;
	//如果~A2()中不加virtual,那么delete p2时只调用A的析构函数,没有调用B的析构函数
	//就会导致内存泄露,因为B在释放资源
}

override & final

C++对虚函数重写的要求比较严格,但是有些情况下由于疏忽,比如函数名写错等无法构成重写,这种错误在编译期间是不会报出来的,因此C++11提供了override,可以帮助用户检测是否构成重写。

cpp 复制代码
class Car {
public:
	virtual void Dirve() {

	}
};

class Benz : public Car {
public:
	//override 编译时检查是否完成重写
	//final final修饰的函数不能重写
	virtual void Dirve() override {
		cout << "Benz-舒适" << endl;
	}
};

如果我们不想子类重写这个虚函数,那么可以用final去修饰。

cpp 复制代码
class Car {
public:
	virtual void Dirve() final{

	}
};

class Benz : public Car {
public:
	//final final修饰的函数不能重写
	//这里就会报错
	virtual void Dirve() {
		cout << "Benz-舒适" << endl;
	}
};

重载/重写/隐藏的对比

重载

两个函数在同一作用域。

函数名相同,参数不同,参数类型或者个数不同,返回值可同,可不同。

重写/覆盖

两个函数分别在继承体系的父类和子类的不同作用域。

函数名,参数,返回值都必须相同,协变例外。

两个函数都必须是虚函数。

隐藏

两个函数分别在继承体系和父类和子类不同作用域。

函数名相同。

两个函数只有不构成重写,就是隐藏。

父子类的成员变量相同也叫隐藏。

纯虚函数和抽象类

在虚函数的后面写上=0,则这个函数为纯虚函数,纯虚函数不需要定义实现(实现没啥意义,因为要被派生类重写,但是语法上可以实现)。

包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派生类继承后不重写纯虚函数,那么派生类也是抽象类。

纯虚函数某种程度上强制了派生类重写虚函数,因为不重写实例化不出对象。

cpp 复制代码
class Car1 {
public:
	//抽象类不能实例化出对象
	virtual void Drive() = 0;
};
//继承后不重写 纯虚函数 子类任然是抽象类

多态的原理

虚函数表指针

问: 下面编译为32位程序的运行结果是什么()

A: 编译报错    B: 运行报错    C: 8    D: 12

cpp 复制代码
class Base {
public:
	virtual void Func1() {
		cout << "Func1" << endl;
	}
protected:
	int _b = 1;
	char _c = 'x';
};
void test8() {
	Base b;
	cout << sizeof(b) << endl;
}

根据过去的知识,计算大小计算的就是成员变量的大小,成员变量的大小是8。

但是在多态中,有虚函数表,虚函数表==_vfptr== 占4字节。

除了_b和_c成员,还多了一个_vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为一个类所以虚函数的地址要被放到这个类对象的虚函数表中,虚函数表也简称虚表。

多态是如何实现的

当满足多态条件后,底层不再是编译时通过调用对象确定函数地址的,而是在运行时到指向对象的虚表中确定对应的虚函数的地址,这样就实现了指针或引用指向基类就调用基类的虚函数,指向派生类就调用派生类对应的虚函数。

cpp 复制代码
class Person3 {
public:
	virtual void BuyTicket() {
		cout << "买票-全价" << endl;
	}
protected:
	string _name;
};

class Student3 : public Person3 {
	virtual void BuyTicket() {
		cout << "买票-半价" << endl;
	}
protected:
	int _id;
};

class Soldier3 : public Person3 {
public:
	virtual void BuyTicket() {
		cout << "买票-优先" << endl;
	}
protected:
	string _codename;
};

void Func3(Person3* ptr) {
	//指向谁,调用谁
	//指向那个对象,运行时,到指向对象的虚函数表中找到对应虚函数的地址,进行调用
	ptr->BuyTicket();
}

void test9() {
	Person3 ps3;
	Student3 st3;
	Soldier3 sr3;

	Func3(&ps3);
	Func3(&st3);
	Func3(&sr3);

}

虚函数表

父类对象的虚函数表中存放父类所有虚函数的地址。同类型的对象共用一张虚表,不同类型的对象各自有独立的虚表,所以父类和子类各有独立的虚表。

子类由两部分构成,继承下来的父类和自己的成员,一般情况下,继承下来的父类中有虚函数表指针,自己就不会在生成虚函数表指针。但是要注意的这里继承下来的父类部分虚函数表指针和基类对象的虚函数表指针不是同一个,就像基类对象的成员和派生类对象中的基类成员也独立的。

子类中重写的父类虚函数,子类的虚函数表中对应的虚函数就会被覆盖成子类重写的虚函数地址。

子类的虚函数中包含,(1)父类的虚函数地址,(2)子类重写的虚函数地址完成覆盖,(3)子类自己的虚函数地址三个部分。

虚函数表本质是一个存虚函数指针的指针数组。

cpp 复制代码
class Base3 {
public:
	virtual void func1() {
		cout << "Base3::func1" << endl;
	}
	virtual void fucn2() {
		cout << "Base3::func2" << endl;
	}
	void func5() {
		cout << "Base3::func5" << endl;
	}
protected:
	int a = 1;
};

class Derive3 : public Base3 {
	virtual void func1() {
		cout << "Derive3::func1" << endl;
	}

	virtual void fucn3() {
		cout << "Derive3::func3" << endl;
	}
	void func4() {
		cout << "Derive3::func4" << endl;
	}
};
void test10(){
	//同类型对象的虚函数表是一样的,不同类型的虚函数表各自独立
	Base3 b1;
	Base3 b2;

	//在虚函数表中会对func1进行地址覆盖
	Derive3 d;
}

觉得我回答有用的话,记得点个关注哟!谢谢支持!

相关推荐
你的冰西瓜2 小时前
C++ STL算法——数值算法
开发语言·c++·算法·stl
青山是哪个青山2 小时前
现代C++特性
开发语言·c++
tankeven2 小时前
自创小算法00:数据分组
c++·算法
苏宸啊3 小时前
OS环境变量
linux·c++
33三 三like3 小时前
高精度计算
开发语言·c++·算法
sycmancia3 小时前
C++——二阶构造模式
c++
郝学胜-神的一滴3 小时前
Python中的“==“与“is“:深入解析与Vibe Coding时代的优化实践
开发语言·数据结构·c++·python·算法
zmzb01033 小时前
C++课后习题训练记录Day109
开发语言·c++
sycmancia3 小时前
C++——类的静态成员变量、静态成员函数
c++