C++类的继承
Posted 正义的伙伴啊
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++类的继承相关的知识,希望对你有一定的参考价值。
继承的概念和定义
概念
我们知道C++是一个面向对象的语言,继承是面向对象程序设计使代码可以复用的最重要的手段,使在逻辑上是继承关系的对象在用类来描述对象语言层面实现了代码复用,这样产生的新的类叫做派生类
例如用学生系统来举例
图中的学生管理系统建立了四个类用来描述 人、学生、老师、助教,但是我们发现学生、助教、老师这三个类都有人这个类的属性,在这个属性上才会多出来一些不同的特征,例如学生可能会有学号、班级…,于是学生从人继承部分属性就是类中的继承,人这个类叫做 父类 或者 基类 ,学生这个类别叫做 子类 或派生类。
class person
public:
void print()
cout << name << endl;
cout << _age << endl;
protected:
string name = "peter";
int _age = 18;
;
class student:public person //student类继承了基类person 中的所有属性
private:
int stuid; //student中新增的属性
;
定义
student叫做 子类 或 派生类,person叫做 父类 或 基类,public叫做继承方式,不同的继承方式会导致继承的属性在子类中的访问权限不同
属性访问权限 和 继承方式
以前在学习类的时候 不同的访问限定符 : public
、private
、protected
,成员变量或函数声明在不同的访问限定符内会导致访问权限大不相同,在继承的时候也会出现这个问题, 不同访问限定符的属性(成员函数或变量)和 继承方式 会导致父类中这些属性在子类中的访问权限不同
public继承 | protected继承 | private继承 | |
---|---|---|---|
父类属性在prublic访问限定符内 | 继承到子类prublic访问限定符内 | 继承到子类protected访问限定符内 | 继承到子类private访问限定符内 |
父类属性在protected访问限定符内 | 继承子类protected访问限定符内 | 继承到子类protected访问限定符内 | 继承到子类private访问限定符内 |
父类属性在private访问限定符内 | 无法被子类继承 | 无法被子类继承 | 无法被子类继承 |
注意
- 基类的private成员无论以什么继承方式继承都是不可见的(这里的不可见是权限上的“不可见”,实际物理内存上是继承到派生类里面的,但是派生类没有权限去读取基类的private成员)
- 这里其实区分了
private
和protected
之间的用法上的区别,如果基类的成员不想被类外直接访问,而能被派生类访问就可以使用protected关键字来定义成员。protected是因为继承才出现的 - 实际运用过程中一般都是用
public
继承方式,很少取用private
和protetcted
基类和派生类对象赋值转换
由于派生类和基类中都含有共同的成员变量和函数,他们之间满足一定的单向赋值关系!
派生类对象 可以赋值给 基类的对象、基类的指针、基类的引用。这里如上图所示就是将派生类除继承类以外的所有成员赋值给基类成员,形象的说法就是叫做切片。但是基类不能赋值给派生类!
class person
public:
void print()
cout << name << endl;
cout << _age << endl;
protected:
string name = "peter";
int _age = 18;
;
class student:public person
private:
int stuid;
;
int main()
student s;
person p1 = s; //基类的对象
person* p2 = &s; //基类的指针
person& p3 = s; //基类的引用
继承中的作用域
在继承的过程中另一个问题就会出现,如果从基类继承的成员变量名或者成员函数名与派生类的成员变量名或函数名重名的情况。这时候就要讨论一下 基类 和 派生类的作用域的问题
- 基类 和 派生类 是两个独立的类,拥有独立的作用域,同一个作用域里面是不允许出现同名变量,但是不同作用域是可以出现相同的变量名。但是要注意要区分函数重载(同名但是参数不同在同一作用域)和 函数隐藏(同名在不同作用域)
- 子类和父类有同名成员,子类成员将屏蔽对父类同名成员变量的访问,这种情况叫隐藏,也叫重定义
- 注意:只要函数名相同就可以构成 隐藏
- 注意:尽量不要定义重名成员
变量重名
class person
protected:
string name = "peter";
int _age = 18;
int num = 10;
;
class student:public person
public:
void print()
cout << num << endl; //打印的是student类中的num
cout << person::num << endl; //打印的是person类中的num ,注意要加限定符::
private:
int stuid;
int num = 100;
;
int main()
student s;
s.print();
函数名重名
class person
public:
void fun()
cout << "this is person" << endl;
protected:
string name = "peter";
int _age = 18;
;
class student:public person
public:
void fun()
cout << "this is student" << endl;
private:
int stuid;
;
int main()
student s;
s.fun();
s.person::fun(); //同名函数想要访问必须显示访问
派生类的默认成员函数
构造函数 | 子类中从父类继承的成员会被看成一个整体,在构造的时候会整体调用基类的构造函数进行初始化,而且基类的构造函数会在派生类的构造函数之前调用。如果派生类继承于多个基类,基类函数的构造函数调用顺序是继承声明时的顺序 |
---|---|
拷贝构造函数 | 拷贝构造函数由于本质上是一种特殊的构造函数,所以调用规则和上面一样 |
赋值运算符函数 | 派生类的 赋值运算符函数 同理要显示的调用 父类的 赋值构造函数,特别注意 这里要显示调用基类的函数(在函数名前加上类的作用域,否则会构成函数隐藏) |
析构函数 | 析构函数虽然函数名看似不相同,但是编译器处理所有类的析构函数时都会认为函数名相同,所以这里继承的时候就会构成隐藏,必须显示调用。还有要注意一下调用顺序:构造函数时先 基类 后 派生类 ,根据FILO的原则:析构函数的顺序就是:先派生类 后 基类 |
class person //基类
public:
person();
person(string s)
:name(s)
cout << "person" << endl;
;
person(person& p)
:name(p.name)
;
person& operator=(person& p)
name = p.name;
return *this;
~person()
cout << "~person" << endl;
protected:
string name;
;
class student :public person //派生类,如果这里声明了多个基类,那么构造函数的调用顺序就是这里基类的声明顺序
public:
student(string s, int n)
:person(s)
, stuid(n)
cout << "student" << endl;
;
student(student& s)
:person(s)
stuid = s.stuid;
student& operator=(student& s)
person::operator=(s);
stuid = s.stuid;
return *this;
~student()
//person::~person();
cout << "~student" << endl;
private:
int stuid;
;
int main()
student s("sht",191);
继承与静态成员
继承中中的静态成员 基类 和 派生类 共享同一个静态成员
代码证明:
class person
public:
person();
person(string s)
:name(s)
count++;
;
static int count;
protected:
string name;
;
int person::count = 0;
class student :public person
public:
student(string s, int n)
:person(s)
, stuid(n)
;
private:
int stuid;
;
int main()
person s1("jack");
student s2("tony", 10);
student s3("henry", 20);
cout << s3.count << endl;
复杂的菱形继承及菱形虚拟继承
线性继承
菱形继承
菱形继承代码:
class A
public:
int a;
;
class B :virtual public A
public:
int b;
;
class C : virtual public A
public:
int c;
;
class D : public B,public C
public:
int d;
;
int main()
D d;
d.a = 1;
d.b = 2;
d.c = 3;
d.d = 4;
如果我们没有使用虚继承的时候,打开内存监视面板,查看d的内存应该是如下:
int main()
D d;
d.B::a = 1;
d.C::a = 10;
d.b = 2;
d.c = 3;
d.d = 4;
如果加上虚拟继承的话,d的内存结构就会发生改变,B::a
和C::a
的存储位置就不会存储a的值了,而是存储了一个地址,这地址指向一块内存这块内存我们把它叫做虚基表,这个表上第一行存储的是多态的虚表,后面学习多态的时候会提到。第二个存储的值是虚基表相对于虚基类存储位置的偏移量。
注意
虚继承不仅会改变D类的存储结构,同时还会改变B类和C类的存储结构
继承的总结
学完了继承之后,一下两个类之间的关系要重点辨析
class B :public A // 关系一:is-a关系
;
class B //关系二:has-a关系
A a;
;
继承是一种 is-a关系,逻辑上如果B继承了A那么B应该包含A,A中的所有成员和都会暴露给B使用,B可以直接对A中的 成员变量直接进行修改,所以这里就会造成一定的风险。本质上是一种白箱操作,两个类如果是继承关系,那么关联度就会很高,耦合度就会很高
关系二是一种has-a关系,B只能调用A中想要暴露给外界的接口,成员变量是无法访问的,是一种黑箱操作,因为只是调用接口,所以两个类之间的关联度很低,耦合度也很低
所以我们在编写程序的时候尽量用高内聚,低耦合的组织方法,尽量使用黑箱操作少暴露类里面的成员,使用对外暴露的接口
以上是关于C++类的继承的主要内容,如果未能解决你的问题,请参考以下文章