C++中的继承,是C++三大特性之一,在其中占着非常重要的地位,今天,就让我们来说说继承。
一、基本概念
1.定义:继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能(用已存在的类来构建新类)。这样产生的新类称为派生类(子类),继承呈现了面向对象程序设计的层次结构,提高了程序的利用率以及代码的管理,大大提高了程序员的工作效率。
2.形式:class 派生类名:继承权限 基类名
继承权限:public、protected、private
3.继承的作用:实现代码复用以及多态。
4.类之间的关系:is_a(继承体现) has_a (组合关系)
5.如果不给出继承权限,则编译器默认为private继承关系
6.继承将给出除构造函数与析构函数外,将继承基类中的所有成员
7.基类的成员函数只能访问基类的成员,不能访问派生类中新增加的成员。
8.友元关系不能继承(因为友元函数不属于类中的成员函数,因此不能继承)
二、有关各种继承权限
公有继承(public)
class Base
{
public:
int a;
protected:
int b;
private:
int c;
};
class Devided:public Base
{
public:
void getum1()
{
cin >> _a >> " " >> _b >> " " >> _c ;
}
void display()
{
cout << a<<endl;
cout << b << endl;
cout << c << endl;
cout << _a << endl;
cout << _b << endl;
cout << _c << endl;
//总结:在派生类中不可以访问基类中的私有成员,公有继承条件下,其他成员均可以其在基类中的访问属性存在
}
public:
int _a;
protected:
int _b;
private:
int _c;
};
int main()
{
Devided d;
d._a = 1;//在类外可以访问公有继承中派生类的公有成员
d.a = 2;//在类外可以访问公有继承下来中基类的公有成员
//d.b = 3;//在类外不可以访问公有继承下来中基类的保护成员
//d._b = 4;//在类外不可以访问公有继承中派生类中保护成员
//d.c = 5;//在类外不可以访问公有继承下来中基类的私有成员
//d._c = 6;//在类外不可以访问公有继承中派生类中的私有成员
return 0;
}
//验证保护类成员,在公有继承之后仍为保护的访问属性,且在类外不能调用
class C :public Devided
{
void displaynum()
{
cout << a << endl;
cout << b << endl;
}
};
int main()
{
C c;
c.b = 3;//不可访问
return 0;
}
保护继承(protected)
class Base
{
public:
int a;
protected:
int b;
private:
int c;
};
class Devided:protected Base
{
public:
void getum1()
{
cin >> _a >> " " >> _b >> " " >> _c ;
}
void display()
{
cout << a<<endl;
cout << b << endl;
cout << c << endl;//私有成员不可访问
cout << _a << endl;
cout << _b << endl;
cout << _c << endl;
//总结:在派生类中不可以访问基类中的私有成员,保护成员继承下,其他成员的访问属性均变为protected访问属性,不可在类外访问
}
public:
int _a;
protected:
int _b;
private:
int _c;
};
int main()
{
Devided d;
d._a = 1;//在类外可以访问保护继承中派生类的公有成员
d.a = 2;//在类外不可以访问保护继承下来中基类的公有成员
d.b = 3;//在类外不可以访问保护继承下来中基类的保护成员
d._b = 4;//在类外不可以访问保护继承中派生类中保护成员
d.c = 5;//在类外不可以访问保护继承下来中基类的私有成员
d._c = 6;//在类外不可以访问保护继承中派生类中的私有成员
return 0;
}
//验证保护的继承方式:验证同上
私有继承(private)
class Base
{
public:
int a;
protected:
int b;
private:
int c;
};
class Devided:private Base
{
public:
void getum1()
{
cin >> _a >> " " >> _b >> " " >> _c ;
}
void display()
{
cout << a<<endl;
cout << b << endl;
cout << c << endl;//私有成员不可访问
cout << _a << endl;
cout << _b << endl;
cout << _c << endl;
//总结:在派生类中不可以访问基类中的私有成员,保护成员继承下,其他成员的访问属性均变为private访问属性,不可在类外访问
}
public:
int _a;
protected:
int _b;
private:
int _c;
};
int main()
{
Devided d;
d._a = 1;//在类外可以访问私有中派生类的公有成员
d.a = 2;//在类外不可以访问私有继承下来中基类的公有成员
d.b = 3;//在类外不可以访问私有继承下来中基类的保护成员
d._b = 4;//在类外不可以访问私有继承中派生类中保护成员
d.c = 5;//在类外不可以访问私有继承下来中基类的私有成员
d._c = 6;//在类外不可以访问私有继承中派生类中的私有成员
return 0;
}
//验证私有的继承方式:新创建一个类,在这个类中的基类的所有成员均不可访问
图示如下:
总结:1.基类的private成员在派生类中是不能被访问的,如果基类成员不想在类外直接被访问,但需要在派生类派生类中能访问,就定义为protected。
2.public继承是一个接口继承,保持is_a原则,每个父类可用的成员对子类也可用,因为每个子类对象也都是一个父类对象。
3.protected\private继承是一个实现继承,基类的部分成员并非子类接口的一部分,是has_a的关系原则
4.不管是哪种继承方式,在派生类内部都可以访问基类的公有成员和保护成员,基类的私有成员存在,但在子类中不可见(不能访问)
5.使用关键字class时,默认的继承方式是private,使用struct时,默认的继承方式是public
三、派生类中的构造函数与析构函数
(一)构造函数
class A
{
public:
A()
{
cout << "这是基类" << endl;
}
A(int a)
:_a(a)
{
cout << "这是基类" << endl;
}
A(const A& a)
{
_a = a._a;
cout << "这是基类" << endl;
}
A& operator=(const A& a)
{
cout << "A& operator=(const A& a)" << endl;
cout << "这是基类" << endl;
return *this;
}
~A()
{
cout << "这是基类" << endl;
}
private:
int _a;
};
class B:public A
{
public:
B()
{
cout << "这是派生类" << endl;
}
B(int b)
:_b(b)
{
cout << "这是派生类" << endl;
}
B(const B& b)
{
_b = b._b;
cout << "这是派生类" << endl;
}
B& operator=(const B& a)
{
cout << "B& operator=(const B& a)" << endl;
cout << "这是派生类" << endl;
return *this;
}
~B()
{
cout << "这是派生类" << endl;
}
private:
int _b;
};
int main()
{
B a;
B b(10);
B c;
c = b;
return 0;
}
由此可知,在调用派生类的构造函数之时,先调用的是基类的构造函数,从而完成对基类数据成员的初始化。
再进一步观察调用的反汇编
由上面也可知道,在继承中构造函数是不继承下来的。
说明:1.基类没有缺省的构造函数,派生类必须要在初始化列表中显示给出基类名和参数列表
2.基类没有定义构造函数,则派生类也可以不用定义,全部使用缺省构造函数(也可以是编译器自动合成的构造函数)
3.基类定义了带有形参表的构造函数,派生类就一定定义构造函数。
调用顺序:
(二)析构函数
代码同上
由此可以看出,在调用析构函数中,先调用派生类的析构函数,在调用基类的构造函数,这个恰好与构造函数的调用相反,与函数压栈出栈的顺序一致。
调用顺序:
四、继承体系中的作用域
1.在继承体系中基类和派生类是两个不同的作用域
2.子类和父类中有同名成员,子类成员将屏蔽父类成员的直接访问(在子类成员中可以使用 基类::基类成员 访问)
3.注意在实际中在继承体系里面最好不要定义同名函数。
#include<iostream>
using namespace std;
class A
{
public:
A(int a = 10, int b = 20)
:_a(a)
, _b(b)
{}
void display()
{
cout << _a << " " << _b;
}
private:
int _a;
int _b;
};
class B :public A
{
public:
B(int c = 0)
{
_c = c;
}
void display()//发生同名隐藏
{
cout << _c;
}
private:
int _c;
};
int main()
{
B b;
b.display();
return 0;
}
接下来看一下调用过程:
由上可以看出在调用相同名字的函数之时,往往会调用派生类中的函数,而将基类函数给隐藏掉,这也就是我们所说的同名隐藏。若要访问基类中的成员,则需要通过以下方法访问
b.A::display();
五、继承与转换—–赋值兼容规则—-(public继承)
先来看基类与派生类的对象模型
1.子类对象可以赋值给父类对象(切割\切片):即在赋值之时,将派生类中基类成员只赋值给基类,将派生类中的自己特有成员以切片形式省略掉,
2.父类对象不能赋值给子类对象。原因:基类的对象只是派生类的一部分
3.父类的指针\引用可以指向子类对象
4.子类的指针\引用不能指向父类对象(可以通过强制类型转换)//强制类型转换,及其不安全,通过这种方法,扩大了父类的内存空间
#include<iostream>
using namespace std;
class A
{
public:
A(int a = 10, int b = 20)
:_a(a)
, _b(b)
{}
void display()
{
cout << _a << " " << _b;
}
private:
int _a;
int _b;
};
class B :public A
{
public:
B(int c = 0)
{
_c = c;
}
void display()//发生同名隐藏
{
cout << _c;
}
private:
int _c;
};
int main()
{
A a;
B b;
a = b;//子类对象可以赋值给父类
b = a;//错误
A*pa = &a;//创建A类型的指针
pa = &b;//利用A类型的对象,指向派生类的对象
A&paa = a;//创建A类型的引用
paa = b;//利用A类型的对象,指向派生类的对象
b.display();
b.A::display();
return 0;
}
六、继承与静态成员
class A
{
public:
A()
{
count++;
}
public:
static int count;
protected:
int a;
};
class B:public A
{
protected:
int b;
};
int A::count = 0;
class C :public A
{
protected:
int c;
};
int main()
{
A a1;
A a2;
A a3;
B b;
cout << "count=" << A::count << endl;
cout << sizeof(B)<<endl;//8
A::count = 0;
cout << "count=" << A::count << endl;
return 0;
}
基类定义了静态成员,则整个继承体系中只有一个这样的成员,无论派生出多少个子类,就只有一个static成员实例。
七、单继承&多继承&菱形继承
(一)单继承
模型
基本形式:
class A
{
protected:
int a;
};
class B:public A
{
protected:
int b;
};
class C :public B
{
protected:
int c;
};
(二)多继承
模型
对象模型:
基本形式:
class A
{
protected:
int a;
};
class B
{
protected:
int b;
};
class C :public A,public B
{
protected:
int c;
};
(三)菱形继承
模型:
对象模型:
基本形式:
class A
{
protected:
int a;
};
class B:public A
{
protected:
int b;
};
class C :public A
{
protected:
int c;
};
class D :public B, public C
{
protected:
int _d;
};
二义性问题
由上面的对象模型可以看出,菱形继承中有两份_a的数据,因此存在二义性与数据冗余的问题,想要解决这个问题,接下来将要引入虚继承的概念
八、虚继承–解决菱形继承的二义性和数据冗余的问题
关键字: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.a = 1;//解决了数据的二义性问题
d.b = 2;
d.c = 3;
d._d = 4;
cout << sizeof(D) << endl;
return 0;
}
相同代码,仅仅在使用了虚继承之后,所占用的内存反而多了4个字节,究竟是什么占用了这四个字节,现在我们来一探究竟。
由图中可以直观性的发现,在解决二义性的问题下,vs所使用的是偏移量表格,从而更好的解决这个问题。
总结:1. 虚继承解决了在菱形继承体系里面子类对象包含多份父类对象的数据冗余&浪费空间的问题。
2. 虚继承体系看起来好复杂,在实际应用我们通常不会定义如此复杂的继承体系。一般不到万不得已都不要定义菱形结构的虚继承体系结构,因为使用虚继承解决数据冗余问题也带来了性能上的损耗。
下面,再来谈下普通继承与虚继承之间的区别,从而让我们更好的理解与学习
区别:1.书写形式:虚拟关键字(virtual)
2.对象模型区别——>多了4个字节——>保存地址——>偏移量表格
3.对于继承成员访问形式
普通继承:直接访问
虚拟继承偏移量表格地址——>相对于基类对象的偏移量——>访问基类成员
4.构造函数不同
派生类——>合成构造函数——>将偏移量表格的地址放入对象的前4个字节中
构造函数多了一个参数——>push 1——>检测是否为虚拟继承