C++多态

目录

一、多态的定义

二、形成多态的条件

三、协变

四、析构函数的重写

五、其他特例

六、重载、重写、隐藏的总结

七、C++11 中的 final 关键字 和 override 关键字

1、final 

2、override

八、抽象类 

九、多态的原理

多态的原理 

虚函数表的存放区域 

打印虚函数表 


想了解继承可以点击这里哦(o´ω`o)ノ

一、多态的定义

        多态就是完成某种行为时,当不同的对象去做就会产生出不同的状态。

        比如买票:普通成年人 和 学生 进行买票时,学生有优惠,而 普通成年人 没有。

 二、形成多态的条件

        多态示例: 

class Person
{
public:
	virtual void BuyTecket()
	{
		cout << "买票 -- 全价\n";
	}
};

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

void func(Person& p)
{
	p.BuyTecket();
}

int main()
{
	Person p;
	Student s;
	func(p);
	func(s);
	return 0;
}

  

        多态的条件: 

        1、虚函数的重写

                重写要求:父子类中的两个虚函数要:三同(函数名、参数、返回值)

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

                往 func() 中传的是父类就调用父类的虚函数,传的是子类就调用子类的虚函数

三、协变

        虚函数的重写要求有特例,协变也满足虚函数重写条件。

        协变:虚函数的返回值可以不同,但必须是父子类关系的指针或引用

                父类中是父类,子类中是子类

class A{}; // 子类
class B : public A{}; // 父类

class Person
{
public:
	virtual A* BuyTecket()
	{
		cout << "买票 -- 全价\n";
		// Person只有返回 A 的 指针/引用 才构成协变,返回 B 的 指针/引用 会报错
		return new A;  	
	}
};

class Student : public Person
{
public:
	virtual B* BuyTecket()
	{
		cout << "买票 -- 半价\n";
		// 同理,Student只能返回 B 的 指针/引用
		return new B;
	}
};

void func(Person& p)
{
	p.BuyTecket();
}

int main()
{
	Person p;
	Student s;
	func(p);
	func(s);
	return 0;
}


    若 Person 中返回 B 的指针或引用,而 Student 返回 A 的指针或引用,则不构成协变,会报错!

class A{}; // 子类
class B : public A{}; // 父类

class Person
{
public:
	virtual B* BuyTecket()
	{
		cout << "买票 -- 全价\n";
		// Person只有返回 A 的 指针/引用 才构成协变,返回 B 的 指针/引用 会报错
		return new B;
	}
};

class Student : public Person
{
public:
	virtual A* BuyTecket()
	{
		cout << "买票 -- 半价\n";
		// 同理,Student只能返回 B 的 指针/引用
		return new A;
	}
};

 四、析构函数的重写

        析构函数的重写是指:只要基类的析构函数加上了 virtual ,那么无论派生类的析构函数加不加 virtual ,它们都构成重写。

        虽然父子类的类名不同,看似违反了重写规则中的 函数名相同,但其实编译器会在底层把析构函数的名字都改为 destruction()。

        析构函数的重写可以防止将 new 出来的子类对象赋值给父类时,在进行 delete 时只调用父类的析构函数 。

如:

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

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

int main()
{
	Person* p = new Person;

	//将 new 的子类对象 切片 赋值给父类
	Person* s = new Student;

	delete p;
	delete s;
	return 0;
}

  

        上述父类析构函数没加 virtual ,导致在 delete s; 时,只调用了 Person 的析构。

        这就会导致内存泄漏。


        加上 virtual 后,就可以正常析构了。

        因为此时析构函数构成多态,用父类的指针 s 调用析构函数就会调用 ~Student()。  

         

五、其他特例

        子类可以不加 virtual

        因为子类是把父类的接口继承下来,重写实现

class Person
{
public:
	virtual void BuyTecket()
	{
		cout << "买票 -- 全价\n";
	}
};

class Student : public Person
{
public:
	void BuyTecket()
	{
		cout << "买票 -- 半价\n";
	}
};

void func(Person& p)
{
	p.BuyTecket();
}

int main()
{
	Person p;
	Student s;

	func(p);
	func(s);

	return 0;
}

        上述代码中,子类重写虚函数没加 virtual ,但依旧构成多态。

        虽然子类重写虚函数可以不加 virtual ,但还是建议加上,这样代码的可读性更高。 


六、重载、重写、隐藏的总结

七、C++11 中的 final 关键字 和 override 关键字

     1、final 

        final 关键字修饰的类为最终类,不能被继承

        final 关键字修饰的函数不能被重写 

        在上述代码中,加入 final 就会报错

   ①、修饰类 


    ②、修饰虚函数 

     2、override

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

八、抽象类 

        抽象类的定义:在虚函数的后面加上 =0 就称为纯虚函数,包含纯虚函数的类就称为抽象类.

        抽象类无法实例化出对象

        派生类继承后也只有重写该纯虚函数才能实例化出对象。 这体现了接口继承。

// 抽象类
class Person
{
public:
    // 纯虚函数
	virtual void func1() = 0;

	int _p = 0;
};

class Student : public Person
{
public:

	int _s = 1;
};

int main()
{
	Person p;
	
	return 0;
}

        上述代码Person 为纯虚函数,因此实例化Person,会报错

        实例化 未重写纯虚函数的Student ,也会报错

         重写后:

        纯虚函数体现出了接口继承。 

九、多态的原理

        多态的原理 

        子类和父类都有一个虚函数表指针,指向的是虚函数的函数指针数组

        可以理解为:子类的虚表指针是先把父类的虚函数表拷贝一份,再判断是否重写,重写了就覆盖父类的虚函数,因此,重写也叫做覆盖。

class Person
{
public:
	virtual void func1()
	{}

	virtual void func2()
	{}

	virtual void func3()
	{}

	int _p = 0;
};

class Student : public Person
{
public:
	virtual void func2()
	{}

	int _s = 1;
};

int main()
{
	Person p;
	Student s;

	return 0;
}

         在用父类的指针或引用调用重写的虚函数时,父类就是在虚函数表中查找;而子类被切片后也可以看做父类,和父类一样在虚函数表中查找,确定函数地址。

        因此,父类调用的就是父类的虚函数,子类调用的就是子类的虚函数。

        多态调用,是在运行时,从指向对象虚表中找到虚函数地址。 

        普通调用,是在编译链接时,从符号表中找,确定函数地址。

虚函数表的存放区域 

         

class Person
{
public:
	virtual void func1()
	{}

	virtual void func2()
	{}

	int _p = 0;
};

class Student : public Person
{
public:
	virtual void func1()
	{
		cout << "重写" << endl;
	}

	virtual void func3()
	{}

	int _s = 1;
};

int ga = 1;

int main()
{
	Person p;
	Student s;

	int sa = 0; // 栈
	int* ha = new int; //  堆
	static int ta = 1;// 静态区
	const char* ca = "aaaaaaaaa"; // 常量区

	Student* ps = &s;
	Person* pp = &p;

	printf("栈: %p\n", &sa);
	printf("堆: %p\n", ha);
	printf("静态区: %p\n", &ta);
	printf("全局数据区: %p\n", &ga);
	printf("常量区: %p\n", ca);
    
    // 取 s 的头四个字节(x86环境)
	printf("虚函数指针: %08x\n", *(int*)ps);

	
	return 0;
}

        由上述代码,我们可以判断出,虚函数指针是存在常量区的。 

        虚表指针是在编译时生成的,在初始化列表的第一个。 

打印虚函数表 

        我们取出对象的头 4/8 个字节就是虚函数表指针,再根据指定虚函数个数,就可以把全部虚函数打印出来。

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

	virtual void func2()
	{
		cout << "Person::func2()" << endl;
	}

	int _p = 0;
};

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

	virtual void func3()
	{
		cout << "Student::func3()" << endl;
	}

	int _s = 1;
};

// 返回值和参数都为空的函数指针数组
typedef void(*VF_PTR)(); 

// 打印指定个数的虚函数
void PrintVFT(VF_PTR* vft, int n)
{
	for(int i = 0; i < n; ++i)
	{
		printf("[%d]:[%p]->", i, vft[i]);
		vft[i]();
	}
}

int main()
{
	Student s;

	// 将 s 的地址强转为 void** ,解引用步长就为一个指针的长度
	// 再将这个值强转为 虚函数表指针的类型,就可以传参了
	PrintVFT((VF_PTR*)(*(void**)(&s)), 3);

	return 0;
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值