作为面向对象的语言,c++开发了名为继承的机制,它是c++中代码复用的重要手段;
允许程序员在保持原有特性的基础(基类)上进行扩展,并产生新的类(派生类),这就是继承。
继承的格式
class (派生类名) : (继承方式) (基类名)
class A {
public:
int _a;
};
class B :public A{
public :
int _b;
};
int main()
{
B b;
return 0;
}
在上面的一段代码中,我让 B 类以 public 方式继承了 A 类;
并且通过断点,我们发现,b 对象中包含了 A 类的成员;
而这里的继承方式又起着什么作用呢?
继承方式
public、protected、private;
我们继承一共有三种方式,而这三种方式和类的访问限定符是一样的;
而这三种方式继承分别有什么不同呢?
类成员/继承方式 | public | protected | private |
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 派生类中不可见 | 派生类中不可见 | 派生类中不可见 |
虽然这个表很复杂难记,但是实际上用一句话概括就是:
派生类继承的基类成员,选继承方式和基类成员访问限定符中权限小的那个作为派生类中基类成员的权限大小;
其中比较特殊的就是基类的private成员派生类不可见;
之前在了解类和对象的时候就说过三个访问限定符,其中private和protected的作用类似;
都是在类外面不可见,但是实际上,protected 的范围稍微大点,可以在派生类中看见;
而 private 则只能在基类里面看见;
基类和派生类对象赋值转换
我们都知道,c++中的赋值都是创建一个中间临时变量,然后将这个临时变量给对应的对象;
比如:
这里编译器实际上是先创建一个中间变量记录b的值,然后再把中间变量赋给a;
但是中间变量具有常性,因此我们这样写就会出错;
我们必须加上 const 修饰符才行;
但是基类和派生类对象之间的赋值转换则没有这样的限制;
这就是基类和派生类对象的赋值转换;
但是实际上,这里的赋值转换只传递了 b 对象中基类的那一部分给了 a2;
class A {
public:
int _a;
A(int a = 10)
:_a(a)
{
}
};
class B : public A {
public:
int _b;
B()
:_b(5), A(5)
{
}
};
int main()
{
A a1;
B b;
cout << a1._a << endl;
a1 = b;
cout << a1._a << endl;
return 0;
}
这就是继承的特殊之处——基类和派生类的切割
从字面意思上看,就是指基类对象接收派生类对象的时候,只会接收基类成员的那一部分;
规则:
1.派生类对象可以赋给基类对象 / 指针 / 引用 ;
2.基类对象不能赋值给派生类对象;
3.基类对象的指针和引用可以通过强制类型转换赋值给派生类对象的指针和引用;
通过规则我们可以知道,实际上基类对象的指针和引用是可以通过强制类型转换赋给派生类对象的指针和引用的;
但是也有特殊情况:
根据基类指针或者引用指向的地址不同,可能导致越界访问;
继承的作用域
由于派生类会继承基类的一切成员,当基类和派生类中出现同名成员时,就会构成隐藏关系;
1.基类和派生类有独立的作用域
2.基类和父类出现同名成员时,派生类将屏蔽基类中同名成员的访问——隐藏
3.成员函数只需要函数名相同就构成隐藏
4.继承体系中最好不要定义同名成员
虽然会构成隐藏关系,但是我们也能够显式的访问父类的同名成员;
class A {
public:
int _a;
A(int a = 10)
:_a(a)
{
}
void func()
{
cout << "A::func()" << endl;
}
};
class B : public A {
public:
int _b;
B()
:_b(5), A(5)
{
}
void func()
{
A::func();
cout << "B::func()" << endl;
}
};
int main()
{
B b;
b.func();
return 0;
}
就像上面写的一样,通过 <基类名>::<同名成员> 来访问同名成员;
而若是不显式访问,就只会访问派生类的成员;
派生类的默认成员函数
在派生类的默认成员函数中,我们一般都是先调用基类构建,再派生类构建,先派生类析构再基类析构;
而不管是拷贝构造还是 运算符=重载,都需要先调用基类函数,再使用派生类的函数;
class A {
public:
int _a;
A(int a = 10)
:_a(a)
{
cout << "A::A()" << endl;
}
~A()
{
cout << "A::~A()" << endl;
}
void func()
{
cout << "A::func()" << endl;
}
};
class B : public A {
public:
int _b;
B()
:_b(5), A(5)
{
cout << "B::B()" << endl;
}
~B()
{
cout << "B::~B()" << endl;
}
void func()
{
A::func();
cout << "B::func()" << endl;
}
};
int main()
{
B b;
return 0;
}
继承与友元
友元关系无法继承,因此基类友元无法访问子类私有和保护成员;
继承和静态成员
整个继承体系只有一个这样的成员
class A {
public:
static int cout;
A()
{
cout++;
}
};
int A::cout = 0;
class B :public A {
public :
B()
{
cout++;
}
};
int main()
{
A a1;
cout << A::cout<<endl;
B b1;
cout << B::cout << endl;
return 0;
}
可以发现,cout 的值是始终都是根据 A 类 和 B 类的构造函数来变化的;
复杂的菱形继承和菱形虚拟继承
继承这个机制是允许多继承的,也就是一个子类可以有多个父类;
但是这就造成了菱形继承;
一般的单继承和多继承没有什么问题,但是菱形继承就会导致数据冗余和二义性的问题;
在上图中,D 类就含有两个 A 类的成员,并且当 D 类使用 A 类的成员时,编译器无法区分是用 C类的 A 还是 B 类的 A ,就导致了二义性的问题;
而解决方法就是让 B 类 和 C 类虚拟继承 A 类,这样就解决了数据冗余和二义性的问题;
那么虚拟继承是如何做到解决这些问题的呢?
class A {
public: int _a = 1;
};
class B : public A {
public: int _b = 2;
};
class C : public A {
public:int _c = 3;
};
class D :public B, public C {
public: int _d = 4;
};
int main()
{
D d;
return 0;
}
我们简单的写一段菱形继承的代码;
然后我们通过内存来看看 d 如何存储的;
我们可以看到,普通的菱形继承中有两个 A 的成员。然后最底下是它自身;
而虚拟继承又如何呢?
class A {
public: int _a = 1;
};
class B : virtual public A {
public: int _b = 2;
};
class C :virtual public A {
public:int _c = 3;
};
class D :public B, public C {
public: int _d = 4;
};
int main()
{
D d;
return 0;
}
我们写下菱形虚拟继承的代码;
当我们虚拟继承的时候, D 里面只有一个 A,因此 为了找到这个 A,B和C原来存储 A 的位置改为了一个地址,这个地址指向了一个表,用来记录 B 地址 和 C 地址分别 距离 A 的偏移量,用来访问那一个 A ;