一.什么是继承
1.1 继承的概念
继承机制是使面向对象程序设计使代码可以复用的重要手段,它允许程序员在保留原有类特性的基础上进行扩展,增加功能,这样产生的类,称为派生类。
继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。继承是类设计层次的复用。
1.2 子类和父类的概念
比如说一个类 Human ,另一个类叫Student,Student继承了Human类,Human类即为Student的父类(也叫基类),Student类即为Human的子类(也叫派生类)。
注意:子类和父类的关系是相对的。
1.3 个人的理解
其实继承 就是从父类中提取共性,将类中的属性和方法进行提取,然后子类继承这些属性和方法,从而实现复用。
比如说 :人共有的特性为 年龄,性别,姓名等等。而学生也有这些信息,因此我们便可以复用。
将Human类当作父类,Student当作子类,即Student继承了 年龄,性别,姓名等等东西,但我们不用再写一遍,提高了效率。
二.继承的使用方法
继承格式为:class 子类名 : 访问限定符 父类名
如图:
#include<iostream>
#include<string>
using namespace std;
enum Sex
{
husband,
woman,
Wal_Mart_shopping_bags,
gunship
};
class Human
{
public:
protected:
int _age=18;
Sex _sex= Wal_Mart_shopping_bags;
string _name= "tangmu";
};
class Student:public Human
{
public:
Student()
{
cout << _age << endl;
cout << _sex << endl;
cout << _name <<endl;
}
private:
};
int main()
{
Student s1;
return 0;
}
代码结果为:
这里Student类继承了Human类,Human类中有三属性,因此我们就认为Student类中包含了Human类中的属性,不用再自己定义,以此来达到复用代码的作用,这就是继承。
三.继承关系
3.1 继承中的访问限定符(继承权限)
在这里我们先回顾一下访问限定符的基本概念:
但我们现在将proctected和private进行细分
注意:不光继承关系需要用访问限定符体现,别忘了类中的访问限定符。
3.2 继承关系
关于继承关系,有下面这样的表格:
关键:继承权限决定了子类能继承的父类的最高权限。即public继承不会改变类成员的访问权限;protected继承方式会改变原来访问权限为public的成员;private继承方式会影响原来访问权限为public和protected的成员。
注意:继承实际上还是完全继承,即子类里依旧包含全部父类的内容,但是子类不能访问继承权限之上的父类成员,通过查看子类的大小可以得知,子类中包含继承自父类的全部成员变量。
如:
#include<iostream>
using namespace std;
class A
{
private:
int a;
int b;
};
class B:public A
{
private:
int c;
};
int main()
{
cout << sizeof(A) << endl;
cout << sizeof(B) << endl;
return 0;
}
结果为:
可见:继承只是通过继承关系的区别来使某些元素或者属性在子类中不可使用(或不可访问),但并非子类中没有。
ps: class和struct的区别
- 定义类的默认访问权限不同,class为私有,struct为公有,兼容C语言
- 模板参数列表中可以使用class,不能使用struct
- 继承中的默认继承权限不同,class默认private,struct默认public
四.赋值转换规则
4.1 基本规则
这里的赋值转换规则大致是在public继承的条件下体现的,在实际操作中我们也建议大家多去使用public继承。
大概有以下三个规则:
- 可以使用子类对象给父类对象赋值赋值,但是不能使用父类对象给子类对象赋值。
- 可以使用父类指针指向子类对象,但不能使用子类指针指向父类对象,如果一定要指向,进行强制类型转换后可以,但是会有指针越界访问的问题。
- 可以使用父类的引用去引用子类,不能使用子类的引用引用父类,与指针原理相同。
#include<iostream>
using namespace std;
class A
{
private:
int a;
int b;
};
class B:public A
{
private:
int c;
};
int main()
{
//子类对象可以给父类对象赋值
A a1;
B b1;
a1 = b1;
//父类指针可以指向子类指针
A* a=&a1;
B *b=&b1;
a = &b1;
//父类引用可以引用子类对象
A& a = a1;
B& b = b1;
a = b;
return 0;
}
但是不可以这样: 为什么呢?
4.2 切片
子类 可以赋值给 父类的对象 / 父类的指针 / 父类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
类似这样:
指针和引用原理与上图相同,父类的指针可以指向子类中继承自父类的部分;但是子类的指针如果指向父类,访问_a和_b时不会有问题,当我们访问到_c时就会超出父类对象的范围,越界访问,所以编译器禁止了子类指针指向父类对象。
五.继承中的作用域
- 在继承体系中,父类和子类都有独立的作用域
- 如果父类和子类中有同名成员,子类成员会屏蔽对父类同名成员的直接访问,优先访问自己类中的成员,即同名隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问隐藏内容)
- 对于成员函数,只要函数名相同就构成重定义,与类型无关。
例如:
#include<iostream>
using namespace std;
class A
{
public:
A()
:a(1)
,b (1)
{
}
void Printf()
{
cout << a << ' ' << b << endl;
}
public:
int a;
int b;
};
class B:public A
{
public:
void Printf()
{
cout << a << ' ' <<b<<' '<<c<<endl;
}
B()
:c(2)
,a(2)
{
}
private:
int c;
int a;
};
int main()
{
B b1;
b1.Printf();
b1.A::Printf();
return 0;
}
结果为:
关于这行代码:
我们刚才提到两句话:
在继承体系中,父类和子类都有独立的作用域。
在子类成员函数,中可以使用 基类+ 作用域修饰符 :: +基类成员 显示访问隐藏的内容。
例如:
#include<iostream>
using namespace std;
class A
{
public:
A()
:a(1)
, b(1)
{
}
void Printf()
{
cout << a << ' ' << b << endl;
}
public:
int a;
int b;
};
class B :public A
{
public:
void Printf()
{
cout << A::a << ' ' << b << ' ' << c << endl;
// 这里就使用 父类::父类的方式或元素 访问隐藏
}
B()
:c(2)
, a(2)
{
}
private:
int c;
int a;
};
int main()
{
B b1;
b1.Printf();
return 0;
}
结果为:
同时根据 在继承体系中,父类和子类都有独立的作用域
我们可以推出这样一个图:
显然 A类在一个单独的作用域里面 但是其中的元素和其它元素同级 ,因此我们需要通过作用域限定符先拿到A中的元素 ,再用 .操作符来,因此我们便得出以下代码。
实在不行可以这样理解
b1.A::Printf() ==b1.(A::Printf())
六. 子类的默认成员函数
6.1 构造函数
当父类没有显式定义构造函数或者父类有全缺省的构造函数或者无参的构造函数时,子类可以不显示定义构造函数。
即以下三种情况都可以通过编译:
#include<iostream>
using namespace std;
//父类没有构造函数
class parent1
{
public:
public:
int _a;
int _b;
};
//父类有全缺省构造函数
class parent2
{
public:
parent2(int a = 1, int b = 1)
:_a(a)
,_b(b)
{
}
public:
int _a;
int _b;
};
//父类有无参构造函数
class parent3
{
public:
parent3()
:_a(1)
, _b(1)
{
}
public:
int _a;
int _b;
};
class child1 :public parent1
{
private:
int _c;
int _a;
};
class child2 :public parent2
{
private:
int _c;
int _a;
};
class child3 :public parent3
{
private:
int _c;
int _a;
};
int main()
{
child1 c1;
child2 c2;
child3 c3;
return 0;
}
但是如果父类显式定义了构造函数,且不是无参或者全缺省的,子类必须显式定义构造函数,并在初始化列表显式调用父类的构造函数,因为如果不显式定义,编译器会自动调用父类默认拷贝构造函数,而父类没有默认的拷贝构造函数,便会报错。
如:
其实实际上就是 子类对象在创建的同时,会在初始化的时候调用父类的默认构造函数来 初始化父类对象,同时我们知道
默认构造函数为:
没有显式定义构造函数会自动生成默认构造
全缺省的构造函数
无参的构造函数
因此父类中有默认构造函数时,子类在初始化时会调用父类的默认构造函数,其实这也很好理解:
如果不这样的话,你该怎么初始化父类元素呢?
#include<iostream>
using namespace std;
class parent
{
public:
parent(int a )
:_a(a)
{
}
private:
int _a;
};
class child: public parent
{
public:
child(int c = 1, int a = 1)
: _c(c)
,parent(a)
{
}
private:
int _c;
};
int main()
{
child c1;
return 0;
}
当然我们也可以手动调用其它构造函数完成父类元素初始化,也可以选择自己想调用的构造函数完成构造。
因此完整的子类构造函数应该分为两步:
- 将从父类继承的成员初始化
- 将子类想使用的成员初始化
同时,在默认构造顺序中,先初始化父类,再初始化子类的其它元素。
6.2 拷贝构造函数
子类中调用父类的拷贝构造时,直接在初始化列表传入子类对象即可,父类的拷贝构造会通过“切片”拿到父类的那一部分。
直接这样写就行了:
#include<iostream>
using namespace std;
class parent
{
public:
parent(int a=1)
:_a(a)
{}
parent(const parent& p)
{
_a = p._a;
}
void Printf()
{
cout << _a << endl;
}
private:
int _a;
};
class child: public parent
{
public:
child(int c = 1, int a = 1)
: _c(c)
{}
child(const child& c)
:_c(c._c)
,parent(c)
{
}
void Printf()
{
parent::Printf();
cout << _c << endl;
}
private:
int _c;
};
int main()
{
child c1;
child c2(c1);
c2.Printf();
c1.Printf();
return 0;
}
结果为:
6.3 赋值运算符重载
子类的赋值运算符重载函数必须调用父类的赋值运算符重载完成对 子类中父类元素的赋值。
如:
6.4 析构函数
构造子类对象时,先调用父类的构造函数,再调用子类的构造函数,清理对象时,先调用子类的析构函数,再调用父类的析构函数。
如:
#include<iostream>
using namespace std;
class parent
{
public:
parent(int a =1)
:_a(a)
{
//cout << "父类" << endl;
}
~parent()
{
cout << "~父类" << endl;
}
private:
int _a;
};
class child: public parent
{
public:
child(int c = 1)
: _c(c)
{
//cout << "子类" << endl;
}
~child()
{
cout << "~子类" << endl;
}
private:
int _c;
};
int main()
{
child c1;
return 0;
}
构造顺序和析构顺序总结:
构造子类对象时,先调用父类的构造函数,再调用子类的构造函数,清理对象时,先调用子类的析构函数,再调用父类的析构函数。
如:
#include<iostream>
using namespace std;
class parent
{
public:
parent(int a =1)
:_a(a)
{
cout << "父类" << endl;
}
~parent()
{
cout << "~父类" << endl;
}
private:
int _a;
};
class child: public parent
{
public:
child(int c = 1)
: _c(c)
{
cout << "子类" << endl;
}
~child()
{
cout << "~子类" << endl;
}
private:
int _c;
};
int main()
{
child c1;
return 0;
}
代码结果为:
七 继承与友元,静态成员
7.1 友元关系
友元关系不能继承
举个例子:
杰叔是你父亲的好朋友,但是杰叔对你来说可能是什么呢?
你父亲的遗产会给你,他的遗产会给你吗。
于是有:
在Display函数中可以访问base类的protected成员,但是不能访问其子类derived类成员,因此友元关系不能继承。
7.2 静态成员
父类中声明了静态变量,则整个继承体系中只有一个静态变量。
如:
#include<iostream>
using namespace std;
class A
{
protected:
int _a;
public:
static int _count;//类中声明为静态成员
};
int A::_count = 0;//静态变量必须类外定义=
class B :public A
{
protected:
int _b;
};
class C :public B
{
protected:
int _c;
};
int main()
{
A a;
B b;
C c;
cout << &a._count << endl;
cout << &b._count << endl;
cout << &c._count << endl;
// 验证三个地址用来确认是不是同一个
cout << a._count << endl;
cout << c._count << endl;
c._count = 10;
cout << a._count << endl;
//改变子类的 _count 结果父类也改变了 也用来确认
return 0;
}
代码结果为:
静态成员可以用来记录一共在这个继承体系中创建了多少个类:
如:
#include<iostream>
using namespace std;
class A
{
public:
A()
{
_count++; //只需要写一个就行 因为子类会默认调用父类的构造函数
}
protected:
int _a;
public:
static int _count;//类中声明为静态成员
};
int A::_count = 0;//静态变量必须类外定义=
class B :public A
{
public:
B()
{
}
protected:
int _b;
};
class C :public B
{
public:
C()
{
}
protected:
int _c;
};
int main()
{
A a1;
A a2;
A a3;
B b1;
C c1;
cout << a1._count << endl; //创建了五个类
return 0;
}
结果为:
八 菱形继承和菱形虚拟继承
8.1 单继承与多继承
一个子类只有一个直接父类时称这个继承关系为单继承
类似于
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
8.2 菱形继承
菱形继承:菱形继承是多继承的一种特殊情况。
菱形继承实例:
#include<iostream>
using namespace std;
class Person
{
public:
int _name;
int _age;
int _sex;
};
class Student:public Person
{
public:
int _ID;
};
class Youth :public Person
{
public:
int _birthday;
};
class Student2022:public Student,public Youth
{
public:
};
int main()
{
Student2022 s1;
return 0;
}
8.3 菱形继承的冗余问题
不知道大家光看上面的代码,有没有发现什么问题呢?
我们先来求以下s1的大小
为什么会是32呢?
我们先来了解一下s1类的内存模型:
通过监视来证明:
对于上面图中的菱形继承,存在的问题十分明显,那就是数据冗余和二义性问题。即Youth类和Student类都继承自Person类,那么两个类中都会包含Person类中的成员,Student2022继承这两个类之后,同样的成员便会包含两份,导致数据重复,并且在通过Student对象访问Person类中的成员时,会有二义性。
通过添加作用域限定符可以解决访问二义性的问题:
如:
#include<iostream>
using namespace std;
class Person
{
public:
int _name;
int _age;
int _sex;
};
class Student :public Person
{
public:
int _ID;
};
class Youth :public Person
{
public:
int _birthday;
};
class Student2022 : public Student, public Youth
{
public:
};
int main()
{
Student2022 s1;
s1.Student::_name = 1;
s1.Youth::_name = 18;
cout << s1.Student::_name << endl;
cout << s1.Youth::_name << endl;
return 0;
}
代码结果:
注:通过 作用域限定符 无法从根本解决数据冗余的问题,所以便引入了虚拟继承的概念。 并且在实际情况中,无法解决数据冗余问题,故常用以下两种方式。
8.4 虚拟继承
其实解决菱形继承的方法: 个人觉得有两种。
一是尽量避免多继承(直接解决问题根源)。 建议使用!!!
二便是虚拟继承。
虚拟继承是指在继承权限前面加上一个virtual关键字。
在可能拥有重复元素的类中加上virtual 使其避免因继承带来的元素冗余
#include<iostream>
using namespace std;
class Person
{
public:
int _name;
int _age;
int _sex;
};
//加入virtual 使用虚拟继承
//当两个虚拟继承类被一个类继承时 编译器会自动检测并去除两个类中的相同元素 这也是我们为什么在父类中加入virtual的原因
//我们一般在可能拥有重复元素的类中加入 避免重复
//在这里我们在 Student类和Youth类中加入 避免其
class Student :virtual public Person
{
public:
int _ID;
};
class Youth :virtual public Person
{
public:
int _birthday;
};
class Student2022 : public Student, public Youth
{
public:
};
int main()
{
Student2022 s1;
s1._name = 18;
cout << s1._name << endl;
//现在我们直接打印_name 不用作用域限定符 证明了现在已经没有了数据冗余和二义性
return 0;
}
那我们来看一下现在的s1大小
发现其大小还增大了,这是为什么呢?
我们先来看一个例题了解一下菱形继承的底层吧:
#include<iostream>
using namespace std;
class Base1
{
public:
int _b;
};
class Base2
{
public:
int _b2;
};
class Derive:public Base1,public Base2
{
public:
int _d;
};
int main()
{
Derive d;
Base1* p1 = &d;
Base2* p2 = &d;
Derive* p3 = &d;
return 0;
}
选什么 兄弟们 答案是 C
通过这里我们可以得出p1==p3!=p2
这里我们也可以通过切片和内存模型理解 p2 为Base2类 因此其指针指向Base2 实际内存开始的一部分 , Derive类和Base1类的开头是一样的 所以P3==p1
实际上地址大小应该为 p1==p3<p2 具体看内存模型图
8.5 虚基表
说实话 这部分有点困难,我不会,借用别人博客的一段解释一下
我们用另一个类来解释:
#include<iostream>
using namespace std;
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;
return 0;
}
可以看出D对象将A对象的组成部分放在最底下,那么B对象与C对象如何找到A对象呢?
我们发现在B,C对象部分之前分别多了一个地址,我们对其分别取地址。
在B,C对象前面加的指针称为虚基表指针,虚基表指针指向的表称为虚基表,虚基表中存在两个偏移量,第二个偏移量为与基类的偏移量,通过该偏移量可以找到基类中的数据。