首页 > 编程语言 >C++——多态

C++——多态

时间:2024-10-27 16:16:52浏览次数:3  
标签:函数 子类 多态 virtual class C++ 重写 public

1.概念

多态:通俗来说,就是 多种形态。具体说就是去完成某个行为时,不同的对象去完成会 产生出不同的状态,这就是多态。 比如,同样是 买票,当 普通人买票时,是全价买票;而 学生买票时,是半价买票; 军人买票时,是优先买票。

2.构成条件

C++里,在继承中要 构成多态两个条件: 1. 虚函数重写 2. 父类的指针或者引用去调用虚函数

举例:

class Person 
{
public:
    //注意:这里的virtual和虚继承的virtual关系不大

	virtual void BuyTicket() //虚函数
	{ 
		cout << "普通人买票-全价" << endl;
	}

};

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

};

void Func(Person& p)
{
	p.BuyTicket();
	//和之前的调用方式已经完全不一样了
	//之前的普通调用,是看类型是什么,就去调用对应的函数,没有对应函数就会报错
	//而现在是多态调用,p指向谁,就去调用谁的函数
    //p指向父类就调用父类的函数,指向子类,就调用子类的函数
}

int main()
{
	Person ps;
	Student st;

	// 因为Func函数的参数是父类的引用,
	// 所以这里可以传父类对象,也可以传子类对象
	Func(ps);
	Func(st);

	return 0;
}

2.1虚函数

何为虚函数:

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

注意:virtual只能修饰成员函数

2.2虚函数重写

如何 构成虚函数重写: 在继承关系中,父、子类的 两个虚函数,要求 三同
1.返回值   2.函数名   3.参数类型
虚函数的重写(覆盖): 子类中 有一个跟父类 完全相同的虚函数, 即子类虚函数与父类虚函数的 返回值类型、函数名字、参数列表完全相同, 此时称 子类的虚函数重写了父类的虚函数

2.3虚函数重写的两个例外

2.3.1协变

协变是指,父类与子类虚函数的返回值类型可以在不相同的情况下,构成虚函数重写,但此时,父子类虚函数的返回值类型必须同时为某个父类类型的指针或者引用。

派生类 重写基类虚函数时,与基类虚函数 返回值类型不同。此时基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
class Person 
{
public:
	virtual Person* BuyTicket()
	//virtual Person& BuyTicket()
	{ 
		cout << "普通人买票-全价" << endl;
		return nullptr;

		//Person p;
		//return p;
	}

};

class Student : public Person 
{
public:
	virtual Student* BuyTicket()
	//virtual Student& BuyTicket()
	{
		cout << "学生买票-半价" << endl;
		return nullptr;

		//Student s;
		//return s;
	}

};

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

int main()
{
	Person ps;
	Student st;

	Func(ps);
	Func(st);

	return 0;
}

返回值类型是其它父子类的指针或者引用时,也是可以构成协变的。

class A
{
public:
	int _a;

};

class B: public A
{
public:
	int _b;

};


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;
}

2.3.2子类虚函数前可以不加virtual

在重写父类虚函数时,子类的虚函数前不加virtual关键字时,是可以构成重写的。

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;
}

注意:

1.子类重写的虚函数前不加virtual时,虽然也可以构成重写,因为继承后,父类的虚函数被继承下来了,在子类中依旧保持虚函数属性,但是这种写法不是很规范,不建议使用。

2.只有父类函数前加了virtual可以构成虚函数重写,但如果只有子类函数前加了virtual是不构成虚函数重写的

2.3.3析构函数的重写

析构函数的使用,是使得C++的多态要设计子类虚函数前可以不加virtual的这种用法的重要原因

通过了解析构函数,可以更深刻地理解到,为什么C++的多态要设计子类虚函数前可以不加virtual的这种用法。

2.3.3.1普通的析构函数
class Person 
{
public:
	virtual void BuyTicket()
	{ 
		cout << "普通人买票-全价" << endl;
	}


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

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



	~Student()
	{
		cout << "~Student()" << endl;
	}
};

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

int main()
{ 
	Person p;
	Student s;

    return 0;
}

日常使用中,这样书写析构函数是没有问题的。

2.3.3.2析构要写成虚函数的情景
class Person 
{
public:
	virtual void BuyTicket()
	{ 
		cout << "普通人买票-全价" << endl;
	}


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

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



	~Student()
	{
		cout << "~Student()" << endl;
	}
};

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

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

	p = new Student;
	delete p;

    return 0;
}

所以,只有析构是虚函数时,才能正确调用析构函数,去释放空间

否则,如果派生类的析构去进行了资源清理,但它不是虚函数,就不会去调用它,此时就会出现内存泄露。


父类加virtual,子类不加virtual就可以构成虚函数重写
此时只要保证父类析构函数是虚函数,子类就算不写virtual,也完成了虚函数的重写,这样就大大降低了内存泄露的可能。

正常情况下,不需要析构函数是虚函数,但是父子类对象是new出来的情况下,就需要析构函数是虚函数了。

2.3.3.3正确的析构虚函数写法
class Person 
{
public:
	virtual void BuyTicket()
	{ 
		cout << "普通人买票-全价" << endl;
	}


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

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



	~Student()
	{
		cout << "~Student()" << endl;
	}
};

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

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

	p = new Student;
	delete p;

    return 0;
}

析构函数的函数名不相同,不能构成虚函数重写,编译器为了解决这个问题,把析构函数统一处理为了destructor

2.4对比重载、重写(覆盖)和隐藏(重定义)

三个概念的对比:

一、重载

要求两个函数在同一个作用域中,函数名相同、参数不同(类型、个数、顺序)时,构成重载。

二、重写(覆盖)

要求两个函数分别在父类和子类的作用域中,必须都是虚函数,

并且要求  1.返回值   2.函数名   3.参数类型  都要相同,协变例外。

三、隐藏(重定义)

同样要求两个函数分别在父类和子类的作用域中,但只要求二者的函数名相同,就构成隐藏。

两个父类与子类里的同名函数,不构成重写,那就是隐藏。

2.5 C++11的final和override

2.5.1 final

一、final可以修饰类使得这个类不能被继承。

二、final还可以 修饰虚函数表示该虚函数不能再被重写。
class Car
{
public:
 virtual void Drive() final {}//此时这个函数就不能被重写了
};

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

2.5.2 override

override用来修饰子类的虚函数,来检查是否完成重写

如果没有重写会编译报错。

class Car {
public:
	virtual void Drive() {}
};

class Benz :public Car {
public:
	//override修饰子类的虚函数,用来检查是否完成重写。
	virtual void Drive() override { cout << "Benz-舒适" << endl; }
};

3.抽象类

3.1概念

虚函数的后面写上 =0 ,则这个函数为 纯虚函数包含纯虚函数的类叫做 抽象类,也叫 接口类

class Car
{
public:
	//纯虚函数
	virtual void Drive() = 0;
};

//类中有纯虚函数,该类就被称为抽象类

3.2特点

抽象类的特点,就是 不能实例化出对象,定义不出对象。但是 可以定义指针。 子类继承后 也不能实例化出对象,除非去 重写纯虚函数,子类才能实例化出对象。 纯虚函数规范了子类虚函数必须重写,另外纯虚函数更体现出了 接口继承

class Car
{
public:
	//纯虚函数
	virtual void Drive() = 0;
};

class Benz :public Car
{
public:

};

int main()
{
	//Car c;
	//不可行

	Car* c1;
	//可行

	Benz b;
	//子类也实例化不出对象
	//可以认为子类也包含纯虚函数,因为它继承了父类
	//所以子类也是抽象类

	return 0;
}

重写虚函数后,子类就可以实例化对象了。

class Car
{
public:
	//纯虚函数
	virtual void Drive() = 0;
};

class Benz :public Car
{
public:
	//除非把这个纯虚函数进行重写
	virtual void Drive()
	{
		cout << "Benz-舒适" << endl;
	}

};


int main()
{
	Benz b;
	//此时子类就可以实例化对象了

	return 0;
}

这也展现了多态的另一种形态

class Car
{
public:
	virtual void Drive() = 0;
};

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

};

class BMW :public Car
{
public:
	virtual void Drive()
	{
		cout << "BMW-操控" << endl;
	}
};

void func(Car* ptr)
{
	ptr->Drive();
}

int main()
{
	func(new Benz);
	func(new BMW);

	return 0;
}

3.3总结

关于纯虚函数

一、纯虚函数,或者说抽象类,在某种程度上,间接强制了子类去重写虚函数
因为如果子类不重写虚函数,就实例化不出对象,这样的话就没什么价值了

二、纯虚函数描述的是抽象类,抽象类不能实例化出对象

什么时候把类设计成抽象类比较好呢?
当某个类指的是现实世界中一些抽象的表示,但是它又不对应某些具体的实体,只是公共特征抽象出来的表达时,可以认为它就是抽象类。

比如说人这个类就可以定义为抽象类,在定义各种职业类时去继承人这个类
比如,医生、老师、程序员这些类去继承人这个类
人不是一个具体的职业,人也不需要去实例化出对象

又比如说动物类,动物没有具体的对象,牛、马、羊才是具体的动物

如果给一个类加上纯虚函数,定义为抽象类,就说明了,这个类不能实例化出对象,它可能在现实生活中不对应具体的实体


抽象类的另一层意义是,它拥有多个子类,这里实现的多态,是想在多个子类之间实现。

虚函数的意义就是实现重写、实现多态,在这里就是多个子类去重写虚函数

3.4补充:接口继承和实现继承

普通函数的继承是一种实现继承,子类继承了父类函数,可以使用函数,因为 继承的是函数的实 是一种复用。 而 虚函数的继承是一种接口继承,子类继承的是 父类虚函数的接口,目的是为了重写函数的实现,达成多态,继承的是接口,所以子类不加virtual也是可以的。 所以 如果不实现多态,就不要把函数定义成虚函数。

4.多态的原理

4.1虚函数表

这里以一道常考的笔试题来引入。

//常考的一道笔试题:sizeof(Base)是多少?
class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
};


int main()
{
	cout << sizeof(Base) << endl;
	// 32位下是8
	// 64位下是16

	return 0;
}

为什么是这样的?

这里就涉及到一个原理:C++中会把虚函数的地址,存储在一个叫做虚函数表的地方。
只要有虚函数就会有虚表

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

class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
	char ch;
};

//此时sizeof(Base)的值 
//32位下是12,依旧遵循类对齐规则
//64位下是16

再次观察

class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}

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

	virtual void Func3()
	{
		cout << "Func3()" << endl;
	}
private:
	int _b = 1;
	char _ch;
};


int main()
{
	cout << sizeof( & (Base::Func1)) << endl;//32位是4
	Base b;

	return 0;
}

虚表实际就是一个虚函数指针数组

虚函数编译后,存储在内存中的哪个位置?
答案:代码段

//编译好之后,是一串指令
//最开始是建立栈帧,然后执行中间的动作,最后销毁栈帧

//只是说虚函数又单独做了一个动作,它的地址被拿出来放在了虚函数表中
//函数编译完是一串指令,表中不可能说去把所有的指令全部存进去(太多了),只是存储了虚函数的地址

4.2探究多态的原理

class Person 
{
public:
	virtual void BuyTicket() { cout << "普通人买票-全价" << endl; }
	virtual void func() {}
private:
	int _a=0;
};

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

private:
	int _b=1;
};

void Func(Person* ptr)
{
	ptr->BuyTicket();
    //是运行起来后,去对应的虚表中去找虚函数
}

int main()
{
	Person p;
	Student s;

	Func(&p);
	Func(&s);

	return 0;
}

形象的的说法,就是从语法的角度这样说更好理解。

比如说成员函数的this指针,我们不能显式写,但实参、形参会把this指针加上,但实际上编译器并不是先把this这个实参、形参加上,它会直接编译成汇编指令,this通过ecx直接就传递了。

再比如引用,表面说引用就是别名,没有开空间,但底层上,它就是指针,也开了空间

由此也可以看出,赋值兼容规则,实际上也是在为多态做准备

4.2.1汇编代码

4.2.2为什么对象调用不构成多态?

这个函数在传参时,如果传的对象是子类对象,会把子类对象中父类的那一部分的成员拷贝给ptr对象,调用拷贝构造。

但是!此时不会把虚函数表指针拷贝过去,所以ptr中没有虚表指针。

4.2.3为什么不允许拷贝虚函数表指针呢?

如果允许拷贝虚表指针,看似可以构成多态了,但实际上会导致许多的连锁反应。

综上,在拷贝时,不能把虚表指针拷贝过去,可以认为这是个特例。

4.3补充

如果子类不重写虚函数,父类和子类的虚表是否一样?

但是,同类对象的虚表指针是一样的。

同一个类对象,共用一张虚表。不同的类,不会共用虚表。

练习题

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();//打印B->1

	p->func();//打印B->0
	注意:这里不构成多态,所以就是正常的函数调用

	这也说明了,只有构成多态时,调用的子类虚函数,才会是继承父类的接口来重写的
	是接口继承,使用的是父类的接口、声明,重写的是函数的实现

	return 0;
}

通过这道题我们也可以看出,多态这里设计的过于复杂,

比如子类可以不加virtual这个用法,如果可以报警告,也许是更好的处理方法

警告不需要处理,但是要评估下它有没有什么影响

标签:函数,子类,多态,virtual,class,C++,重写,public
From: https://blog.csdn.net/2301_80342122/article/details/143193723

相关文章

  • Chromium base库 环境变量类使用说明c++
    1、环境变量获取和设置定义参考:base\environment.hbase\environment.cc//Copyright2011TheChromiumAuthors//UseofthissourcecodeisgovernedbyaBSD-stylelicensethatcanbe//foundintheLICENSEfile.#ifndefBASE_ENVIRONMENT_H_#defineBASE_EN......
  • VC++ __declspec(dllexport) 和 __declspec(dllimport)
    头文件中声明了方法,在提供者那里方法应该被声明为__declspec(dllexport),在使用者那里,方法应该被声明为__declspec(dllimport)。Class中含有一个静态变量,生成dll的时候只采用了__declspec(dllexport),使用的时候__declspec(dllimport)就派上用场了,他会告诉使用dll的工程去lib中找......
  • C++/C电子宠物1.0
    使用的是一个在线编程的网站,是linux环境https://www.onlinegdb.com/#include<iostream>#include<stdlib.h>#include<time.h>#include<unistd.h>#include<pthread.h>#include<string>usingnamespacestd;intwindow();intea......
  • 【C++ 真题】B2099 矩阵交换行
    矩阵交换行题目描述给定一个5×55\times55×5的矩阵(数学上,一个......
  • C++ 静态变量什么时候完成初始化
    C语言在编译器就完成静态变量的内存分配和初始化;始化发生在任何代码执行之前,属于编译期初始化。C++全局或静态对象当且仅当对象首次用到时才进行构造,并通过atexit()来管理对象的生命期;静态变量初始化是线程安全的。全局变量、文件域的静态变量和类的静态成员变量在m......
  • C++ Vector介绍及应用
    一.引言在C++编程中, std::vector 是一个非常重要的容器,它提供了动态数组的功能。这意味着它可以根据需要自动调整大小,存储一系列相同类型的元素,并允许快速随机访问。本文将详细介绍std::vector的基本概念、特性、常用操作以及一些实际应用场景。二.什么是std::vector? std:......
  • C++中的函数重载
    前言    在给函数命名的时候,我们通常会遇到这类问题,这类函数都是解决一个类型的问题的,例如两个数相加,两个int类型的整数相加,我们起名add1,然后两个double类型的浮点数相加,我们起名为add2......在一些小型项目中还行,但是在一些大型的项目中,这显然是不可取的。那么能不能......
  • c++数据封装
    C++ 数据封装数据封装(DataEncapsulation)是面向对象编程(OOP)的一个基本概念,它通过将数据和操作数据的函数封装在一个类中来实现。这种封装确保了数据的私有性和完整性,防止了外部代码对其直接访问和修改。所有的C++程序都有以下两个基本要素:程序语句(代码):这是程序中执行动作......
  • C++的继承和多态
    继承继承的本质意义是复用(不用写就有)父类的某些东西可以直接使用eg.但是注意:被继承的成员是新的成员,不是共用同一个成员(实例化的成员变量不同)         但是调用的函数是同一个函数继承基类成员访问方式的变化(重点)private访问 "在派......
  • C++11新标准の右值引用
    一、什么是左值、右值?专业的说法:左值是指表达式结束后依然存在的持久化对象;右值是指表达式结束后就不再存在的临时对象。通俗的说法:有名字的对象都是左值,右值没有名字。区分左右值得便捷方法:看能不能对表达式取地址,如果能,则为左值,否则为右值Tips:C++11把右值分为纯右值和......