virtual override final

virtual关键字

virtual使用简介

virtual在基类中确定了一个虚拟函数,在派生类中使用重写虚拟函数来实现对基类虚拟函数的覆盖

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

class Derived : public Base{
	Public:
		void print(){
			cout<<"Derived";
		}
};

当类型为基类Base的指针指向了一个Drived型的对象时,调用Print()函数,此时调用的是Drived类中的print函数而不是Base类中的函数。

这是面向对象中多态性的一种体现。

函数重载和覆盖的不同点

  • 重载的函数位于同一类中,覆盖(override)的函数应位于有继承关系的不同类中
  • 重载的函数要求函数名称相同,但是参数不同,由此编译器在可以通过参数来判断调用的是哪个函数。而覆盖的几个函数必须要求函数名、参数、返回值都相同。
  • 重载和关键词virtual无关。覆盖的函数在前面必须加上关键词virtual

关于函数覆盖的一些隐藏规则:

  1. 如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
  2. 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。

纯虚函数virtual ··· = 0,可以用来定义接口

当定义纯虚函数时,可以指定让继承类所实现的接口。但是基类并不能被基类调用,纯虚函数也不需要实现,只需要声明即可。

纯虚函数的声明如下所示:

class Asd{
	public:
		virtual ostream& print(ostream&=cout) = 0;//函数声明后面紧跟赋值为0
};

如果一个类包含纯虚拟函数,那么就是一个抽象基类,如果你试图创建一个抽象基类的对象会报错

C++中的接口通常被定义为一个抽象基类,也就是只包含纯虚函数的类。纯虚函数是一种特殊的虚函数,它没有实现,只有函数原型。

它的作用就是要求实现该接口的具体类必须提供该函数的具体实现。

虚析构

如果一个类用作基类,我们通常需要virtual来修饰它的析构函数,这点很重要。如果基类的析构函数不是虚析构,当我们用delete来释放基类指针(它其实指向的是派生类的对象实例)占用的内存的时候,只有基类的析构函数被调用,而派生类的析构函数不会被调用,这就可能引起内存泄露。如果基类的析构函数是虚析构,那么在delete基类指针时,继承树上的析构函数会被自低向上依次调用,即最底层派生类的析构函数会被首先调用,然后一层一层向上直到该指针声明的类型

使用虚析构是防止只调用了基类的析构函数,而没有调用继承类的析构函数。

虚继承 virtual public

举个例子,首先我们定义基类A,再从基类A中派生出两种不同的继承类B和C。

class A{
	public:
		fun();
};

class B : public A{```};
class C : public A{```};

而当我们使用B和C作为基类派生出D时,就出现了问题。每个D的对象中包含了B 和 C的基类对象数据,即包含两个A对象。

而我们实际上只需要一个A对象,但实际上存储了两个,浪费了存储区。此外在这个过程中,A的构造函数被调用了两次(B和C各一次)

最严重的引起的二义性,当调用D中的A成员时到底是哪个?由此引入了虚拟继承。

我们可以使用关键字Virtual修正,一个基类的声明可以将它指定为被虚拟派生。

// 这里关键字 public 和 virtual的顺序不重要
class Bear : public virtual ZooAnimal { ... };
class Raccoon : virtual public ZooAnimal { ... };

虚拟派生不是基类本身的一个显式特性,而是它与派生类的关系。如前面所说明的,虚拟继承提供了“按引用组合”。也就是说,对于子对象及其非静态成员的访问是间接进行的。这使得在多继承情况下,把多个虚拟基类子对象组合成派生类中的一个共享实例,从而提供了必要的灵活性。同时,即使一个基类是虚拟的,我们仍然可以通过该基类类型的指针或引用,来操纵派生类的对象。

override关键字

override 简介

C++ override从字面意思上,是覆盖的意思,实际上在C++中它是覆盖了一个方法并且对其重写,从而达到不同的作用。override是C++11中的一个继承控制关键字。override确保在派生类中声明的重载函数跟基类的虚函数有相同的声明。

override明确地表示一个函数是对基类中一个虚函数的覆盖。更重要的是,它会检查基类虚函数和派生类中重载函数的签名不匹配问题。如果签名不匹配,编译器会发出错误信息。

在我们C++编程过程中,最熟悉的就是对接口方法的实现,在接口中一般只是对方法进行了声明,而我们在实现时,就需要实现接口声明的所有方法。还有一个典型应用就是在继承中也可能会在子类覆盖父类的方法。

实际在基类和派生类的作用

首先定义一个Person类,含有纯虚函数、虚函数以及普通函数

class Person{
public:
	virtual void creat() const = 0;               // 1.纯虚函数
	virtual void say(const std::string& msg);   // 2.普通虚函数
	int name() const;                           // 3.非虚函数
};

class Student : public Person{
	public:
	protected:
	private:
};

class Teacher : public Person{
	public:
	protected:
	private:
};

纯虚函数

纯虚函数,继承的是基类成员函数的接口必须在派生类中重写该函数的实现:

class Student : public Person{
	public: void creat(){
		Person* s1 = new Student; 
	}// calls Student::creat();
	protected:
	private:
};

class Teacher : public Person{
	public: void creat(){
		Person* s1 = new Teacher; 
	}// calls Teacher::eat();
	protected:
	private:
}; 

并且基类的eat为纯虚函数不可被调用

普通虚函数

普通虚函数,对应在基类中定义一个缺省的实现 (default implementation),表示继承的是基类成员函数的接口和缺省的实现,由派生类自行选择是否重写该函数。

实际上,允许普通虚函数同时继承接口和缺省实现是危险的。 如下, CarA 和 CarB 是 Car的两种类型,且二者的运行方式完全相同。

class Car{
public:
	virtual void run(const Car& destination);

};

class CarA : public Car{
  public:
  protected:
  private:
};

class CarB : public Car{
  public:
  protected:
  private:
};

这是典型的面向对象设计,两个类共享一个特性 – run,则 run可在基类中实现,并由两个派生类继承。


现增加一个新的飞机型号 CarC,其飞行方式与 CarA,CarB 并不相同,假如不小心忘了在 CarC 中重写新的 fly 函数.

class CarC : public Car
{
public:
	... // no fly function is declared
};

则调用 CarC 中的 run 函数,就是调用 Car::run,但是 CarC的运行方式和缺省的并不相同

Car * car1 = new CarC;
car1->run(China);  // calls Car::run!!

这就是前面所说的,普通虚函数同时继承接口和缺省(默认参数)实现是危险的,最好是基类中实现缺省行为 (behavior),但只有在派生类要求时才提供该缺省行为.

纯虚函数 + 缺省实现

一种方法是 纯虚函数 + 缺省实现,因为是纯虚函数,所以只有接口被继承,其缺省的实现不会被继承。派生类要想使用该缺省的实现,必须显式的调用:

class Car
{
public:
	virtual void run(const Car& destination) = 0;//纯虚函数

};

void Car::run(const Car& destination)
{
	// a pure virtual function default code for run a Car to the given destination
}//纯虚函数其实都不需要写实现

class CarA : public Car
{
public:
	virtual void run(const Car& destination)
	{
		Car::run(destination);
	}

};

这样在派生类 CarC 中,即使一不小心忘记重写 Run函数,也不会调用 Car的缺省实现.

class CarC : public Car
{
public:
	virtual void run(const Car& destination);
};

void CarC::run(const Car& destination)
{
	// code for run a CarC Car to the given destination
}
使用override

可以看到,上面问题的关键就在于,一不小心在派生类 CarC中忘记重写 run函数,C++11 中使用关键字 override,可以避免这样的“一不小心”。

非虚函数:
非虚成员函数没有virtual关键字,表示派生类不但继承了接口,而且继承了一个强制实现(mandatory implementation),既然继承了一个强制的实现,则在派生类中,无须重新定义继承自基类的成员函数,如下:

使用指针调用 name 函数,则都是调用的 Person::name()

Student s1;  // s1 is an object of type Student

Person* p1 = &s1;  // get pointer to s1
p1->name();        //call name() through pointer

Student* s2 = &s1;   // get pointer to s1
s2->name();          // call name() through pointer

如果在派生类中重新定义了继承自基类的成员函数 name :

class Student : public Person
{
public:
   int name() const; // hides Person::name();

};

p1->name();        //call name() through pointer
s2->name();          // call name() through pointer

此时,派生类中重新定义的成员函数会 “隐藏” (hide) 继承自基类的成员函数。

这是因为非虚函数是 “静态绑定” 的,p1被声明的是 Person* 类型的指针,则通过 p1调用的非虚函数都是基类中的,即使 指向的是派生类。

与“静态绑定”相对的是虚函数的“动态绑定”,即无论 p1被声明为 Person* 还是 Student* 类型,其调用的虚函数取决于p1实际指向的对象类型。

使用原则

  • 基类函数没加virtual,子类有相同函数,实现的是覆盖。用基类指针调用时,调用到的是基类的函数;用子类指针调用时,调用到的是子类的函数。
  • 基类函数加了virtual时,实现的是重写。用基类指针或子类指针调用时,调用到的都是子类的函数。
  • 函数加上override,强制要求基本相同函数需要是虚函数,否则会编译报错。相当于是一个保险,防止基类没写virtual?
  • 子类的virtual可加可不加,建议加override不加virtual。
  • C++11 中的 override 关键字,可以显式的在派生类中声明哪些成员函数需要被重写,如果没被重写,则编译器会报错。

常见用法

  • virtual一般用在继承的关系中。
  • 如果这个方法无需子类定义,则该方法不用virtual进行修饰。
  • 如果这个方法需要子类重写,但有默认实现,则该方法需要virtual进行修饰。
  • 如果这个方法只需要子类实现,父类无需处理,则该方法可以定义为纯虚方法:virtual fun()=0
  • 在继承的关系中,基类的析构方法,需要定义为虚函数,以避免子类无法析构。
  • 构造函数不存在虚函数,因为在创建对象时,需要确切地知道是那个类(静态绑定),而需函数时动态绑定,在构造类时不能确定类的信息是错误的。

final关键字

如果某个类/函数已做好了充分的准备并可供其他类使用的话(即其接口已明确定义且以后不会修改),那么该类/函数就是封闭(你可以称之为完整)的,这时可以使用final

最重要的两个作用

  • 禁止虚函数被重写
  • 禁止基类被继承

final:指定不能在派生类中重写虚函数不能从中继承类

在虚函数声明或定义中使用时,final确保函数是虚函数,并指定它不能被派生类重写。否则程序格式错误(生成编译时错误)。

在类定义中使用时,final指定该类不能出现在另一个类定义的基说明符列表中(换句话说,不能从中派生)。否则程序格式错误(生成编译时错误)。final也可以与union定义一起使用,在这种情况下,它没有任何影响(除了std::is_final的结果),因为联合不能从派生)

final是在成员函数声明或类头中使用时具有特殊含义的标识符。在其他上下文中,它不是保留的,可以用来命名对象和函数。

C++ 11还增加了防止继承类或简单地防止派生类中重写方法的能力。这是用特殊标识符final完成的。

final阻止类的进一步派生阻止虚函数的进一步重写

C++ 11关键字的最终目的有两个。它阻止从类继承,并禁止重写虚函数

有时,你不想让派生类重写基类的虚函数。C++ 11允许内置的工具防止使用最终说明符重写虚函数。

C++ 11中的最后关键字可以应用于整个类或方法。当应用于一个类时,它表示该类不允许派生;也就是说,不能创建从最终类派生的类。第二种方法是将final应用于方法时,这样可以防止方法被派生类重写(尽管仍然可以创建子类)。

例子

阻止基类函数的覆盖

#include <iostream>
 
class Base {//最底层的类
public:
	Base() {
 
	}
public:
	virtual void func()  = 0 ;
	virtual void func2() = 0;
};
 
class Son :public Base{//第二层类
public:
	Son()
	{
 
	}
public:
	void func()
	{
		std::cout << "son func "<< std::endl;
	}
	void func2() final//定义func2 为final 不能被重写覆盖
	{
		std::cout << "son func2 " << std::endl;
 
	}
 
};
 
class Son2 :public Son{//继承自第二层的类,不能重写final函数func2()
	void func()
	{
		std::cout << "son2 func " << std::endl;
	}
};
 
 
int main()
{
	Base* p = new Son2;
	p->func2();//son func2
}

阻止类的继承

#include <iostream>
 
class Base final {//final类,不能被继承 断子绝孙类
public:
	Base() {
 
	}
public:
	virtual void func()  = 0 ;
	virtual void func2() = 0;
};