返回顶部

C++继承 学习笔记

C++继承

继承就是类之间的一种关系,子类拥有父类的一切,也能够完成父类的所有可以完成的事务。父类也即基类,子类也即派生类。(子类和基类是相对而言的)。

继承的方式一般写成:

class 子类:继承权限 父类,…,继承权限 父类

因为C++支持多继承,所以继承列表可以有多个父类,以逗号分隔。

继承权限

继承关键字

C++提供三个权限关键字:publicprotectedprivate。(一般使用第一种。)

  • 公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有和保护成员来访问。
  • 保护继承(protected):当一个类派生自保护基类时,基类的公有和保护成员将成为派生类的保护成员。
  • 私有继承(private):当一个类派生自私有基类时,基类的公有和保护成员将成为派生类的私有成员。

访问控制

继承权限 public protected private
public public protected private
protected protected protected private
private private private private
访问 public protected private
同一个类
派生类 ×
外部的类或函数 × ×

一个派生类继承了所有的基类方法,以下情况除外:

  • 基类的构造函数、析构函数和拷贝构造函数
  • 基类的重载运算符
  • 基类的友元函数

示例:

//基类
class Shape {
protected:
	int x, y;
public:
	Shape(int x, int y) {
		this->x = x;
		this->y = y;
	}
};
//基类
class volume {
protected:
	int calVolume(int x, int y, int z) {
		return x * y * z;
	}
};
//派生类
class Cube:public Shape , public volume {
private:
	int z;
public :
	Cube(int x, int y, int z):Shape(x , y){
		this->z = z;
	}
	int cal() {
		return calVolume(x, y, z);
	}
};
int main() {
	Cube cube(1, 2, 3);
	cout << "立方体的体积为:" << cube.cal() << endl;
	return 0;
}

输出:立方体的体积为:6

继承的构造与析构

在生成子类对象过程中会首先调用父类构造函数,然后再调用子类构造函数,这样就让父类构造函数把父类部分数据初始化了一遍,然后通过调用子类构造函数初始化子类成员。而对于析构函数就刚好相反了,秉承着先进后出的堆栈思想。

有如下示例:

class A {
public:
	A() {cout << "A" << endl;}
	~A() { cout << "DA" << endl; }
};
class B {
public:
	B() { cout << "B" << endl; }
	~B() { cout << "DB" << endl; }
};
class C :public A, public B {
public:
	C() { cout << "C" << endl; }
	~C() { cout << "DC" << endl; }
};

int main() {
	C* c = new C();
	delete c;
	return 0;
}

输出:类名表示调用了构造函数,在类名前加D表示调用了析构函数

A
B
C
DC
DB
DA

因为在C++中存在多继承,对于上述程序,若B也继承于A,则输出为:

A
A
B
C
DC
DB
DA
DA

发现对象A被构造了两次,为了解决这一问题,可以在继承关键字前加上virtual关键字,即:

class A {
public:
	A() {cout << "A" << endl;}
	~A() { cout << "DA" << endl; }
};
class B:virtual public A {
public:
	B() { cout << "B" << endl; }
	~B() { cout << "DB" << endl; }
};
class C :virtual public A, public B {
public:
	C() { cout << "C" << endl; }
	~C() { cout << "DC" << endl; }
};

int main() {
	C* c = new C();
	delete c;
	return 0;
}

输出:

A
B
C
DC
DB
DA

对于上述代码,若删去CB继承的virtual关键字都会导致编译失败(VS2022)。

上述添加virtual关键字的继承方式称为虚继承,虚继承在创建父类对象的时候会创建一个虚表。

改变访问权限

使用 using 关键字可以改变基类成员在派生类中的访问权限,例如将 public 改为 private、将 protected 改为 public

注意:using 只能改变基类中 publicprotected 成员的访问权限,不能改变 private 成员的访问权限,因为基类中 private 成员在派生类中是不可见的,根本不能使用,所以基类中的 private 成员在派生类中无论如何都不能访问。

实例:

class A {
public:
	A(){}
	~A(){}
	void func() {};
protected:
	int a, b, c;
};
class B : public A {
public:
	B(){}
	~B(){}
	using A::a;//将protected改为public
	using A::b;
	int d;
private:
	using A::func;//将public 改为 private
};

虚函数

虚函数:即被virtual修饰的类成员函数称为虚函数。

基本概念和实例

在某基类中声明为 virtual 并在一个或多个派生类中被重新定义的成员函数,用法格式为:virtual 函数返回类型 函数名(参数表) {函数体};实现多态性,通过指向派生类的基类指针或引用,访问派生类中同名覆盖成员函数。

#include<iostream>
using std::cout;
using std::endl;

class Base {
private:
public:
	Base() {
		cout << "Base::constructor run " << endl;
	}
	virtual void fun1() {
		cout << "Base::fun1 run" << endl;
		return;
	}
	virtual void fun2() {
		cout << "Base::fun2 run" << endl;
		return;
	}
	virtual ~Base() {
		cout << "Base::desconstructor run" << endl;
	}
};
class Child : public Base {
private:
public:
	Child() {
		cout << "Child::constructor run" << endl;
	}
	Child(Base*) {

	}
	void fun1() override {
		cout << "Child::fun1 run" << endl;
	}
	~Child() {
		cout << "Child::desconstructor run" << endl;
	}
};

int main() {
	Base* base = new Child();
	base->fun1();
	base->fun2();
	delete base;
	return 0;
}
/**
Base::constructor run
Child::constructor run
Child::fun1 run
Base::fun2 run
Child::desconstructor run
Base::desconstructor run
*/

通过上述代码可以看出,在子类重写了fun1。在主函数中创建Base*指针base指向Child对象。因为派生类Child成员函数fun1重写了基类Base的成员函数fun1,故通过base调用fun1实际调用的是派生类Child的成员函数,而fun2没有被重写,故base调用fun2实际调用的是基类Base的成员函数fun2。最后使用delete释放内存使用基类指针base,会调用派生类析构函数和基类析构函数,不会造成内存泄露。

虚函数的使用

  • 在基类用virtual关键字声明成员函数为虚函数。

  • 在派生类中重新定义此函数,要达到改写的目的,有如下要求:

    • 基类中的函数必须是虚函数。

    • 基类和派生类中的函数名字必须完全相同(析构函数除外)。

    • 基类和派生类中的函数形参型别必须完全相同。

    • 基类和派生类中的函数常量性必须完全相同。

    • 基类和派生类中的函数返回值和异常规格必须兼容。

      此处返回值存在一个例外,那就是协变:派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。实例:

      class A {
      public:
      	virtual A* fun() {
      		return new A;
      	}
      };
      class B : public A {
      public:
      	virtual B* fun() override {//此处返回值与基类的返回值不同
      		return new B;
      	}
      };
      
    • 基类和派生类中的函数引用修饰词必须完全相同。(C++11增加)。(成员函数引用修饰词是C++11中的语言特性,它们是为了实现限制成员函数仅用于左值或右值。带有引用修饰词的成员函数,不必是虚函数。示例如下:

      //以下内容来自Effective Modern C++
      class Widget {
      public:
      	//...
      	void doWork()&{};//这个版本的doWork仅在*this是左值时调用
      	void doWork()&&{};//这个版本的doWork仅在*this是右值时调用
      	//...
      };
      Widget makeWidget();//工厂函数 返回右值
      Widget w;//普通对象(左值)
      int main() {
      	w.doWork();//以左值调用Widget::doWork
      			  //即Widget::doWork &
      	makeWidget().doWork();//以右值调用Widget::doWork
      						 //即Widget::doWork &&
      }
      
  • 定义指向基类对象的指针变量,并使它指向同一类族中需要调用该函数的对象。如前面实例中的Base* base = new Child();

  • 通过该指针变量调用此虚函数,此时调用的是指针变量指向的对象的同名函数。

定义虚函数的限制

  • 非类的成员函数不能定义为虚函数,类的成员函数中静态成员函数和构造函数也不能定义为虚函数,但析构函数可以定义为虚函数。而将析构函数定义为虚函数是比较推荐的,因为将基类的析构函数定义为虚函数后,当利用delete删除一个指向派生类定义的对象指针时,系统会调用相应的类的析构函数,而不将析构函数定义为虚函数时,只调用基类的析构函数。

  • 只需要在声明函数的类体中使用关键字virtual将函数声明为虚函数,而定义函数时不需要使用关键字virtual。因为在C++中推荐将类的声明和实现分开放在.h文件和.cpp文件中。故上述限制内容可以表示成:

    //在Base.h文件中声明类
    class Base {
    private:
    public:
    	Base();
    	virtual void fun1();
    	virtual ~Base();
    };
    
    //在Base.cpp中实现函数
    #include "Base.h"
    void Base::fun1() {//在此处不需要在使用关键字virtual
    	...
    	return;
    }
    
  • 当将基类中的某一成员函数声明为虚函数后,派生类中的同名函数(函数名相同、参数列表完全一致、返回值类型相关)自动成为虚函数,故在派生类声明该虚函数时可以加virtual也可不加。同时,在有些编译器中,若你在派生类中意图改写基类的虚函数,但参数或返回值与基类中的出现偏差,可能不会产生报错信息(这跟编译器有关),为了保证改写符合自己的预期和其正确性,C++11提供了显式地标明派生类中函数是为了改写基类版本:为其加上override声明。该关键字只有在对应的位置采用其含义,故你仍然可以定义函数名为override的函数。

    class A {
    public:
    	virtual void fun1(int x){};
    	virtual void fun2(int& x) {};
    };
    class B: public A{
    public:
        //添加override关键字后,强制编译器进行检查
    	void fun1(int t) override {};
        void fun2(int c) override {};//error
    	void fun2(int& c) override {};//correct
    };
    
  • 如果声明了某个成员函数为虚函数,则在该类中不能出现和这个成员函数同名并且返回值、参数个数、类型都相同的非虚函数。在以该类为基类的派生类中,也不能出现和这个成员函数同名并且返回值、参数个数、类型都相同的非虚函数。

其他注意点

  • 改写(override)是针对于虚函数的,即子类的同名函数改写父类的同名虚函数;而重载是针对一般函数而言的。

  • 虚指针可用于函数成员而不能用于数据成员

  • 虚函数不能是内联函数。

  • C++ 11关键字final

    修饰虚函数,表示该虚函数不能再被继承。

虚表

带有虚函数的类称为虚基类,子类继承虚基类。在C++中虚基类有一个虚函数表指针保存虚函数表地址,而虚函数表保存函数地址,虚函数表并不在虚基类里,但是虚函数表指针在虚基类里,子类继承虚基类,子类也就有了虚函数表指针。

虚表(虚拟函数表)是一个列表,存放父类中虚函数的地址,当创建对象时如果存在派生类对基类虚函数的改写,就将子类的虚函数的地址覆盖原来父类虚函数的地址。拥有虚函数的类才有虚函数表。

虚表建立在编译过程中,虚函数指针是在运行阶段确定的。类对象空间最开始四个字节就是虚表的地址,也即虚指针,C++中虚指针是放在类对象的开头,虚指针指向一个虚表,虚表中记录了虚基类与本类的地址偏移,通过这个地址偏移可以找到虚基类的成员指针的地址。

纯虚函数

纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加 =0;:

virtual void func() = 0;

声明了纯虚函数的类是一个抽象类。所以,用户不能创建类的实例,只能创建它的派生类的实例。纯虚函数最显著的特征是:它们必须在继承类中重新声明函数(不要后面的=0,否则该派生类也不能实例化),而且它们在抽象类中往往没有定义。

抽象类

称带有纯虚函数的类为抽象类。抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体的类。

参考资料

C++ 继承

C++三种继承方式

posted @ 2022-04-15 11:02  cherish-lgb  阅读(50)  评论(0编辑  收藏  举报