C++初阶--多态

目录

1.多态的概念

2.多态的定义和实现

        2.1多态的构成条件

        2.2虚函数

        2.3重写(覆盖)

3.派生类的析构函数

4.override和final

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

6.抽象类

7.接口继承和实现继承

8.多态的原理

        8.1虚函数表

        8.2打印虚函数表中内容


1.多态的概念

        多态:多种形态,不同的对象去完成同一件事,结果不同。

2.多态的定义和实现

2.1多态的构成条件

1.必须通过基类的引用或指针调用虚函数

2.被调用的函数必须是虚函数,且派生类必须对基类虚函数完成重写

2.2虚函数

        被virtual修饰的类非静态成员函数

2.3重写(覆盖)

        派生类中有一个跟基类完全相同的虚函数(返回值类型、函数名、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
//多态:多种形态
//静态的多态:函数重载,看起来调用同一个函数有不同行为。静态:原理是编译时
//动态的多态:一个父类的引用或指针去调用同一个函数,传递不同的对象,会调用不同的函数。动态:原理是运行时实现
//本质:不同人去做同一件事情,结果不同
class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票-全价" << endl;
	}
};

class Student : public Person
{
public:
	//子类中满足三同(函数名、参数、返回值)(协变除外) 、 虚函数,  叫做  重写(覆盖)
	virtual void BuyTicket()
	{
		cout << "买票-半价" << endl;
	}
};

//构成多态,跟p的类型没有关系,传的哪个类型的对象,调用的就是这个类型的虚函数 -- 跟对象有关
//不构成多态,调用的就是p类型的函数 -- 跟类型有关

void Func(Person& p)
{
	p.BuyTicket();     //多态
}

int main()
{
	Person ps;
	Student st;

	Func(ps);
	Func(st);

	return 0;
}

        重写要求返回值相同有一个例外:协变--要求返回值是父子关系的引用或指针

//重写要求返回值相同有一个例外:协变--要求返回值是父子关系的指针或引用
class A{};
class B : public A{};

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

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

void Func(Person& p)
{
	p.BuyTicket();     
}

int main()
{
	Person ps;
	Student st;

	Func(ps);
	Func(st);

	return 0;
}

3.派生类的析构函数

        动态申请的父子对象,如果给了父类指针管理,那么需要析构函数是虚函数完成重写,构成多态,那么才能正确调用析构函数。析构函数名被特殊处理了,处理成了destructor。

//析构函数名被特殊处理了,处理成了destructor
class Person
{
public:
	virtual ~Person()
	{
		cout << "~Person()" << endl;
	}
};

class Student : public Person
{
public:
	virtual ~Student()
	{
		cout << "~Student()" << endl;
	}
};

int main()
{
	//普通对象,析构函数是否虚函数,是否完成重写,都正确调用了
	//Person p;
	//Student s;

	//动态申请的父子对象,如果给了父类指针管理,那么需要析构函数是虚函数完成重写,构成多态,那么才能正确调用析构函数
	Person* p1 = new Person;         //operator new + 构造函数
	Person* p2 = new Student;

	//析构函数 + operator delete                
	delete p1;      //p1->destructor()
	delete p2;		//p2->destructor()				

	return 0;
}

        虚函数的重写允许两个都是虚函数或者父类是虚函数,再满足三同,就构成重写

//虚函数的重写允许两个都是虚函数或者父类是虚函数,再满足三同,就构成重写
class Person
{
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl;}

	virtual ~Person() { cout << "~Person()" << endl;}
};

class Student : public Person
{
	//虽然子类没写virtual,但是他是先继承了父类的虚函数的属性,再完成重写。也算虚函数
	void BuyTicket() { cout << "买票-半价" << endl;}
public:
	~Student() { cout << "~Student()" << endl;}
};

void Func(Person& p)
{
	p.BuyTicket();     
}

4.override和final

        设计一个不能被继承的类 (C++98)

//设计一个不能被继承的类
//C++98

class A
{
private:
	A(int a = 0)
		:_a(a)
	{}

public:
	static A CreatOBj(int a = 0)
	{
		//new A;
		return A(a);
	}

protected:
	int _a;
};

//间接限制,子类构成函数无法调用父类构造函数初始化成员,没办法实例化对象

class B : public A
{};

int main()
{
	A aa = A::CreatOBj(10);

	return 0;
}

        final修饰类  该类不能被继承

//C++11 直接限制
class A final
{
protected:
	int _a;
};

//class B : public A
//{
//
//};

        final修饰虚函数  该虚函数不能被子类虚函数重写

//C++11 final 修饰虚函数,限制他不能被子类中的虚函数重写
class C
{
public:
	virtual void f() final
	{
		cout << "C::f()" << endl;
	}
};

class D : public C
{
public:
	//virtual void f() final
	//{
	//	cout << "D::f()" << endl;
	//}
};

        override:放在子类重写的虚函数后面,检查是否完成重写,没有重写就报错

//override放在子类重写的虚函数的后面,检查是否完成重写
//没有重写就报错
class Car
{
public:
	void Drive()
	{}
};

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

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

重载

1.两个函数在同一作用域

2.函数名相同,参数不同

覆盖(重写)

1.两个函数分别在基类和派生类作用域

2.函数名,参数,返回值都相同(协变例外)

3.两个函数是虚函数

隐藏(重定义)

1.两个函数分别在基类和派生类作用域

2.函数名相同

3.两个基类和派生类同名的函数,不构成重写就是重定义

6.抽象类

        在虚函数后面加上=0,这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数要求派生类必须重写,更体现出了接口继承。

class Car
{
public:
	//纯虚函数一般只声明,不实现
	//实现没有价值
	//virtual void Drive() = 0;
	virtual void Drive() = 0
	{
		cout << "virtual void Drive() = 0" << endl;
	}

	void f()
	{
		cout << "void f()" << endl;
	}
};

int main()
{
	Car* p = nullptr;
	//p->Drive();

	return 0;
}

7.接口继承和实现继承

        普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成
多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

8.多态的原理

        8.1虚函数表

//sizeof(Base)是多少?--12
class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}

	virtual void Func2()
	{
		cout << "Func2()" << endl;
	}

private:
	int _b = 1;
	char _ch = 'A';
};

int main()
{
	cout << sizeof(Base) << endl;
	Base bb;

	return 0;
}

        对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)

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

	void f(){cout << "f()" << endl;}

protected:
	int _a = 0;
};

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

protected:
	int _b = 0;
};

void Func(Person& p)
{
	//多态调用,在编译时,不能确定调用的是哪个函数
	//运行时,去p指向对象的虚表中找到虚函数的地址
	p.BuyTicket();
	p.f();
}

//普通函数与虚函数存储的位置是否一样?
//一样,都在代码段。虚函数要把地址存到虚表,方便实现多态

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

	Student Johnson;
	Func(Johnson);

	Person& r1 = Johnson;
	Person p = Johnson;

	//Person p1 = Mike;
	//Person p2 = Johnson;
	 不是多态,编译时确定地址
	//p1.BuyTicket();
	//p2.BuyTicket();

	return 0;
}
1. 派生类对象有一个虚表指针,由两部分构成,一部分是父类继承下来的成员。
2. 基类对象和派生类对象虚表不一样,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数
的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
5. 派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
6. 虚函数存在哪?虚表存在哪?虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?vs下是存在代码段的。 

        8.2打印虚函数表中内容

typedef void(*VF_PTR)();

//打印虚函数表中内容
void PrintVFTable(VF_PTR* table)
{
	for (int i = 0; table[i] != nullptr; ++i)
	{
		printf("vft[%d]:%p->", i, table[i]);
		VF_PTR f = table[i];
		f();
	}
	cout << endl;
}
typedef void(*VF_PTR)();

//打印虚函数表中内容
void PrintVFTable(VF_PTR* table)
{
	for (int i = 0; table[i] != nullptr; ++i)
	{
		printf("vft[%d]:%p->", i, table[i]);
		VF_PTR f = table[i];
		f();
	}
	cout << endl;
}

//单继承
class Base
{
public:
	Base()
	{
		a = 0;
	}

	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; }
	void fun4(){ cout << "Derive::func4" << endl; }

private:
	int b;
};

int main()
{
	Base b;
	//b.func1();

//#ifdef _WIN64
//	PrintVFTable((VF_PTR*)(*(long long*)&b));
//#else
//	PrintVFTable((VF_PTR*)(*(int*)&b));
//#endif
//

	PrintVFTable((VF_PTR*)(*(void**)&b));

	return 0;
}
1. inline函数可以是虚函数,不过编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去。调用时,如果不构成多态,这个函数保持inline属性,如果构成多态,这个函数就没有inline属性了。
2. 静态成员不可以是虚函数,因为静态成员函数没有this指针,使用类型::成员函数 的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
3. 构造函数不可以是虚函数。因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
4. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:可以,并且最好把基类的析构函数定义成虚函数。
5. 对象访问普通函数快还是虚函数更快?答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
6. 虚函数表是在什么阶段生成的,存在哪的?虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。
7.抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系。

  • 25
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值