C++继承 学习笔记
C++继承
继承就是类之间的一种关系,子类拥有父类的一切,也能够完成父类的所有可以完成的事务。父类也即基类,子类也即派生类。(子类和基类是相对而言的)。
继承的方式一般写成:
class 子类:继承权限 父类,…,继承权限 父类
因为C++
支持多继承,所以继承列表可以有多个父类,以逗号分隔。
继承权限
继承关键字
C++
提供三个权限关键字:public
、protected
、private
。(一般使用第一种。)
- 公有继承(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
对于上述代码,若删去C
或B
继承的virtual
关键字都会导致编译失败(VS2022
)。
上述添加virtual
关键字的继承方式称为虚继承,虚继承在创建父类对象的时候会创建一个虚表。
改变访问权限
使用 using
关键字可以改变基类成员在派生类中的访问权限,例如将 public
改为 private
、将 protected
改为 public
。
注意:using
只能改变基类中 public
和 protected
成员的访问权限,不能改变 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
,否则该派生类也不能实例化),而且它们在抽象类中往往没有定义。
抽象类
称带有纯虚函数的类为抽象类。抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体的类。
参考资料
作者:cherish.
出处:https://home.cnblogs.com/u/cherish-/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。