C++继承
文章目录
继承的基本介绍
继承的作用
面向对象程序设计中最重要的一个概念是继承。继承允许我们依据另一个类来定义一个类,这使得创建和维护一个应用程序变得更容易。这样做,也达到了重用代码功能和提高执行效率的效果。
继承是代码可以复用的重要手段,在保持原有类的基础下,继承原有类的基本性质,再对新类进行扩展,很好的实现代码复用。
#include<iostream>
using namespace std;
class Animal {
public:
void print() {
cout << _name <<" " << _age << endl;
}
protected:
string _name="小白";
int _age=5;
};
//Dog类继承Animal类
class Dog :public Animal {
protected:
string bark;
};
//Cat类继承Animal类
class Cat :public Animal {
protected:
string bark;
};
int main() {
Dog d;
//Dog类并没有print()方法,但是继承了Animal类的变量和方法,所以可以调用。
d.print();
Cat c;
c.print();
return 0;
}
继承的定义
继承的格式
Dog类也就是子类,继承了父类,采用的是共有继承的方法,继承了父类的所有变量和方法。
继承的关系和访问限定符
继承的方式和访问限定符一样,都分为三种,public . protected . private 公有 保护 和 私有。
不同的访问限定符被不同的继承方式所继承,都会有不同的访问效果。
类成员/继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
看去来很复杂,其实稍微总结一下就很清楚:
- 如果基类的成员或方法是private属性,那么派生类不管用什么方式继承这些成员都是不能访问的。(虽然子类继承了父类的private成员,但是没有访问权限)
- 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
- 看起来两种关系组合起来很复杂,但是可以看出,不管是访问限定符还是继承方式,我们要判断派生类中能不能访问,只要看限定符和继承方式中权限小的那个,就能确定成员的访问权限。
- class后面也可以不写继承方式,那么和作用域限定符一样,如果不写,默认为private,而struct继承方式默认为public
基类和派生类之间的对象的赋值转换
- 派生类的对象可以给基类的对象/指针/引用赋值
- 基类对象不能给派生类对象赋值
派生类是继承了基类的所有成员变量和方法,而且派生类有自己特有的成员和方法,如果用派生类给基类赋值,那么只要拷贝从基类继承来的那一部分给基类,特有的部分不会赋值,这个行为也成为切割/切片
但是反过来,如果用基类对象给派生类对象赋值,那么派生类中特有的成员变量和方法就无法初始化,所以不支持基类对象给派生类对象赋值
基类的指针可以通过强制类型转换赋值给派生类的指针。但是必须是基类的指针是指向派生类对象时才是安全的。
继承的作用域
-
继承中基类和子类有不同的作用域,那就说明,派生类中的成员方法,可以和基类中的拥有相同的方法名。
那么我们知道,派生类继承了基类的所有变量和方法,那么我们用派生类对象调用这个同名方法时,就会产生产生歧义,所以为了解决这个问题,C++中规定,如果子类和父类中有相同的方法名,在访问这个同名方式时,父类中的方法会被直接屏蔽,这种情况也叫隐藏/重定义。
#include<iostream>
using namespace std;
class Animal {
public:
void print() {
cout << "Animal" << endl;
}
protected:
string _name="小白";
int _age=5;
};
class Dog :public Animal {
public:
//和父类相同的方法名
void print(int a) {
cout << "Dog" << endl;
}
protected:
string bark;
};
class Cat :public Animal {
public:
//和父类相同的方法名
void print(int b) {
cout << "Cat" << endl;
}
protected:
string bark;
};
int main() {
Dog d;
d.print();
Cat c;
c.print();
return 0;
}
可以看出,子类调用和父类相同的方法,并没有调用到父类的方法,而是用的自己的。
- 如果想调用父类中的同名方法,那么必须在给出方法的作用域。
int main() {
Dog d;
d.print();
//给出方法的限定作用域
d.Animal::print();
Cat c;
c.print();
c.Animal::print();
return 0;
}
派生类的默认成员函数
- 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用
- 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化
- 派生类的operator=必须要调用基类的operator=完成基类的复制
- 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
- 派生类对象初始化先调用基类构造再调派生类构造
- 派生类对象析构清理先调用派生类析构再调基类的析构
class Animal {
public:
Animal(const char* name,int age)
:_name(name)
,_age(age)
{
cout << "Animal()" << endl;
}
Animal(const Animal& a)
:_name(a._name)
,_age(a._age)
{
cout << "Animal(const Animal& a)" << endl;
}
Animal& operator=(const Animal& a)
{
if (this != &a) {
_name = a._name;
_age = a._age;
cout << "operator=(const Animal& a)" << endl;
}
return *this;
}
~Animal() {
cout << "~Animal()" << endl;
}
protected:
string _name="小白";
int _age=5;
};
class Dog :public Animal {
public:
Dog(const char* name, int age, const char* bark)
//调用父类的构造函数构造父类的成员
:Animal(name,age)
,_bark(bark)
{
cout << "Dog()" << endl;
}
Dog(const Dog& d)
//调用父类的拷贝构造方法拷贝父类的成员
:Animal(d)
,_bark(d._bark)
{
cout << "Dog(const Dog& d)" << endl;
}
Dog& operator=(const Dog& d)
{
if (this != &d) {
//调用父类的赋值,由于与父类方法形成重定义,所以加上作用域限定符
Animal::operator=(d);
_bark = d._bark;
cout << "operator=(const Dog& d)" << endl;
}
return *this;
}
~Dog() {
cout << "~Dog()" << endl;
}
//为保证对象先实例化先析构,子类对象在调完析构函数后,会自动调用父类的析构函数,所以我们在实现子类的析构函数时,不需要主动调用父类析构函数,
protected:
string _bark;
};
int main() {
Animal a("小黑", 5);
cout << "//" << endl;
Dog d1("旺财", 3, "旺旺");
cout << "//" << endl;
Dog d2(d1);
cout << "//" << endl;
Dog d3("小白",5,"旺旺");
cout << "//" << endl;
d3 = d2;
cout << "//" << endl;
return 0;
}
继承的友元和静态
- 友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员
- 基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。
class Dog;
class Animal {
public:
friend void Friend( Animal& a, Dog& d);
Animal(const char* name,int age)
:_name(name)
,_age(age)
{
cout << "Animal()" << endl;
_conut++;
}
Animal(const Animal& a)
:_name(a._name)
,_age(a._age)
{
cout << "Animal(const Animal& a)" << endl;
}
Animal& operator=(const Animal& a)
{
if (this != &a) {
_name = a._name;
_age = a._age;
cout << "operator=(const Animal& a)" << endl;
}
return *this;
}
~Animal() {
cout << "~Animal()" << endl;
}
//给父类定义一个静态变量
static int _conut;
protected:
string _name="小白";
int _age=5;
};
//静态变量类外初始化
int Animal::_conut = 0;
class Dog :public Animal {
public:
Dog(const char* name, int age, const char* bark)
:Animal(name,age)
,_bark(bark)
{
cout << "Dog()" << endl;
_conut++;
}
Dog(const Dog& d)
//调用父类的拷贝构造方法
:Animal(d)
,_bark(d._bark)
{
cout << "Dog(const Dog& d)" << endl;
}
Dog& operator=(const Dog& d)
{
if (this != &d) {
//调用父类的赋值,由于与父类方法形成重定义,所以加上作用域限定符
Animal::operator=(d);
_bark = d._bark;
cout << "operator=(const Dog& d)" << endl;
}
return *this;
}
~Dog() {
cout << "~Dog()" << endl;
}
//为保证对象先实例化先析构,子类对象在调完析构函数后,会自动调用父类的析构函数,所以我们在实现子类的析构函数时,不需要主动调用父类析构函数,
protected:
string _bark;
};
void Friend(Animal& a, Dog& d) {
cout << a._name << endl;
cout << a._age << endl;
cout << d._name << endl;
cout << d._age << endl;
}
int main() {
Animal a("小黑", 5);
Dog d1("旺财", 3, "旺旺");
cout << "///" << endl;
Friend(a, d1);
cout << "///" << endl;
//此时可以看出父类定义的静态变量是两个类共有的。
cout <<"Animal::_conut:" << Animal::_conut << endl;
cout <<"Dog::_conut:" << Dog::_conut << endl;
return 0;
}
多继承和菱形继承
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
就好比上面的Dog类继承了Animal类,Dog类除了Animal类并没有别的父类,就叫单继承。
多继承
一个子类有两个以上的父类,那么这个继承关系就称为多继承
class A
{};
class B
{};
//C同时继承A和B 这个继承关系就叫做多继承
class C :public A, public B
{};
菱形继承
菱形继承实际上是多继承的一种特殊情况
class A
{};
class B :public A
{};
class C :public A
{};
//B C继承自A D又同时继承B C
class D :public B, public C
{};
菱形继承会产生很多问题,其中就有数据冗余和二义性
class Person {
public:
string _name; //姓名
};
class Worker :public Person {
protected:
int _num; //工人职工编号
};
class Docter :public Person {
protected:
int _id; //医生职工编号
};
class Spouse :public Worker, public Docter {
protected:
string child_name; //夫妻孩子名称
};
int main() {
Spouse s;
//这样访问不会成功,因为Spouse继承了两份_address 这么调用就会造成二义性
s._name = "张三";
return 0;
}
要想解决二义性问题,在访问_address时指定作用域
int main() {
Spouse s;
s.Worker::_address = "三楼";
s.Doctor::_address = "三楼";
return 0;
}
虚拟继承
上面的菱形继承,可以看出Spouse继承了来自Worker和Doctor的两个相同的_address属性,但是我们只需要一个_address就行,多出来的我们用不到,这就造成了数据冗余,而且这个address有继承自两个不同的父类,会造成二义性,为了解决这两个问题,我们就要用到虚拟继承。
class Person {
public:
string _address; //住址
};
//在继承Person类时,使用virtual类来虚继承
class Worker :virtual public Person {
protected:
int _num; //工人职工编号
};
//在继承Person类时,使用virtual类来虚继承
class Doctor :virtual public Person {
protected:
int _id; //医生职工编号
};
class Spouse :public Worker, public Doctor {
protected:
string child_name; //夫妻孩子名称
};
int main() {
Spouse s;
//虚拟继承Person类后,就不会产生二义性和数据冗余
s._address = "三楼";
return 0;
}
虚继承解决数据冗余和二义性的原理
我们先设计一个简单的菱形继承类,然后用内存窗口看&d 也就是D 类实例化后的内存情况
class A{
public:
int _a;
};
//class B :virtual public A
class B :public A{
public:
int _b;
};
//class C :virtual public A
class C :public A {
public:
int _c;
};
class D :public B, public C{
public:
int _d;
};
int main() {
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
可以看出_a的数据有两份,出现了数据冗余
B C 类改用virtual继承
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.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
- 可以看出D的内存分布,A被放到了最下面而B和C原来的A位置变成了一个指针,指向了另一块内存,而这两个指针就叫做虚基表指针,虚基表指针指向两个表,这两个表叫做虚基表,而虚基表中存放着两个偏移量,通过偏移量计算就能找到A的位置。
这样,A的数据不管有多少,都可以通过这个虚基表和偏移量存在同一个位置,B C中只需要保存虚基表指针就能找到虚基表的位置,解决了数据冗余的问题,同时数据只有一份,自然也就没有二义性的问题了。