继承
1.继承的概念
继承是面向对象软件技术当中的一个概念,与多态、封装共为面向对象的三个基本特征。继承可以使得子类具有父类的属性和方法或者重新定义、追加属性和方法等。
2.继承的格式和分类
(1)格式
class 子类:继承方式 父类
示例:
class Student :public Person
{}
(2)继承方式
private:父类的所有成员在子类中都是不可见的;
protected:在子类中,父类的公有成员变为保护成员,保护成员权限不会发生变化;
public:父类的公有和保护成员权限在子类中不会发生变化;
(3)总结
1)三种继承方式的强弱:
private>protected>public
2)采用private的继承方式,尽管在在子类中成员不可见,但实际上子类继承了父类的成员;
3)使用关键字class定义类时默认的访问权限是private,使用struct定义类时默认的访问权限是public;
4)私有成员不可见,访问权限取最小;
5)protected成员在类外不可访问,在类中可以访问;
3.基类和派生类对象赋值转换
(1)派生类对象、指针、引用可以将自身和基类部分共有的成员赋值给基类对象、指针、引用,称之为切片;
(2)基类对象不能赋值给派生类对象,因为基类对象中没有派生类对象中的自定义成员;
(3)基类的指针可以通过强制类型转换赋值给派生类的指针,但是必须是基类的指针是指向派生类对象时才是安全的;同理基类的引用也可以强转。
示例:
切片:
#include<iostream>
using namespace std;
class Parent{
private:
int _a=2048;
};
class Son:public Parent{
private:
int _b=1024;
};
int main()
{
Parent p;
Son s;
p = s;
Parent* p1;
Son* s1;
p1 = s1;
Parent& i1=p;
Son& i2 = s;
i1 = i2;
return 0;
}
基类指针赋值给派生类的指针:
#include<iostream>
using namespace std;
class Parent{
private:
int _a=2048;
};
class Son:public Parent{
private:
int _b=1024;
};
int main()
{
Parent p;
Son s;
s = p;//error
Parent* p1=&s;
Son* s1=&s;
s1 = (Son*)p1;//this is safe
Parent* p2=&p;
Son* s2=&s;
s2=(Son*)p2;//this is not safe
return 0;
}
运行监视:
4.继承中的作用域
(1)父类和子类各自有自己的作用域;
(2)当父类和子类有同名的成员时,子类会隐藏(也叫重定义)父类的成员;
(3)如果是成员函数的隐藏,只要函数名相同即可;
(4)继承体系中最好不要定义同名成员;
代码示例:
#include<iostream>
class A{
private:
int _a = 100;
public:
void Print()
{
std::cout << _a << std::endl;
}
};
class B:public A{
private:
int _a = 1000;
public:
//函数的同名隐藏,只要函数名相同即可
void Print(int )
{
std::cout << _a << std::endl;
}
};
void test()
{
B b;
b.Print(0);//结果为1000
}
int main()
{
test();
return 0;
}
5.派生类的默认成员函数
(1). 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用积基类的构造函数。
(2). 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝构造函数初始化。
(3). 派生类的operator=必须要调用基类的operator=完成基类的赋值。
(4). 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
(5). 派生类对象初始化先调用基类构造再调用派生类构造。
6). 派生类对象析构清理先调用派生类析构再调用基类的析构。
代码演示:
#include<iostream>
using namespace std;
class A{
public:
A(int a=10000)
:_a(a)
{
cout << "A(int)" << endl;
}
A(const A& a1)
:_a(a1._a)
{
cout << "A(const A&)" << endl;
}
A& operator=(const A& a1)
{
if (this != &a1)
_a = a1._a;
cout << "A& operator=" << endl;
return *this;
}
~A()
{
cout << "~A()" << endl;
}
protected:
int _a=996;
};
class B :public A{
public:
B(int a, int b)
:A(a) //基类的构造函数需要写在初始化列表中
,_b(b)
{
cout << "B(int,int)" << endl;
}
B(const B& b1)
:A(b1)
,_b(b1._b)
{
cout << "B(const B&)" << endl;
}
//显式定义的赋值运算符重载函数,不会自动调用父类的赋值运算符重载函数
B& operator=(const B& b)
{
if (this != &b)
{
//调用父类的赋值运算符,需要加上父类的作用域
//因为父类的赋值运算符和子类的赋值运算符构成了同名隐藏
A::operator=(b);
//_a = b._a;
_b = b._b;
}
cout << "B& operator=" << endl;
return *this;
}
//显式定义的子类析构,会在函数体执行完时,自动调用父类析构
~B()
{
cout << "~B()" << endl;
//不需要显示调用父类析构,编译器会自动调用,显式调用会造成内存二次释放
//A::A();
}
protected:
int _b = 1024;
};
void test()
{
B b(1,2);//先调用基类的构造,再调用派生类的构造
B copy(b);
B b2(0, 0);
b2 = b;
}
int main()
{
test();
return 0;
}
运行结果:
6.继承与友元
友元关系不能继承,也就是说基类友元只能访问基类中的成员,不能访问子类私有和保护成员
7.继承与静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员,静态成员是共享的。无论派生出多少个子类,都只有一个static成员实例.
代码演示:
#include<iostream>
using namespace std;
struct A{
static int _i;
};
int A::_i = 0;
struct B :public A{
};
int main()
{
A a;
B b;
a._i = 1;
b._i = 2;
A::_i = 3;
B::_i = 4;
return 0;
}
运行监视:
8.菱形继承和菱形虚拟继承
(1)菱形继承的定义:
某个类的多个直接父类中有共同的基类成员,称这样的继承关系为菱形继承
举个例子:
两个子类B,C继承了同一个父类A,另一个子类D同时继承了这两个子类B和C,在类D中会有两份A中的成员。
在实际应用中,应该尽量少使用菱形继承;
菱形继承示意图:
(2)菱形继承存在的问题
数据冗余和二义性:
数据冗余即相同的数据被存放了多份;
二义性指的是当访问A中的成员时,因为有两份数据,编译器不知道该访问继承自B的那部分A,还是访问继承自C的那部分A;
(3)菱形继承存在问题的解决办法----菱形虚拟继承
在B类和C类继承A类的继承方式之前加上virtual关键字
1)菱形虚拟的原理:
对于相同的数据,派生类中并没有存储多份,而是将这份数据保存了一份。在派生类的成员中有一个虚基表指针,可以通过虚基表指针找到虚基表,再从虚基表中找到实际存储数据的地址距离虚基表指针的偏移量,通过偏移找到实际存储的数据。
2)菱形虚拟继承的优点:
消除了二义性;
避免数据冗余,当公共的基类成员比较大时,优势很明显;
3)图形解析菱形虚拟继承:
4)代码示例:
struct A{
int _a = 1;
};
struct B:virtual public A{
int _b=2;
};
struct C :virtual public A{
int _c=3;
};
struct D : public B,public C{
int _d=4;
};
9.继承与组合的区别
(1)继承是一种"是"的关系,也就是说每个派生类对象都是一个基类对象,继承是一种突破封装的语法,耦合性比较高,并且是一种白盒复用。
(2)组合是一种"有"的关系。假设B组合了A,每个B对象中都有一个A对象,组合是一种黑盒复用。
(3)当有继承和组合两种选择时,优先使用组合;
(4)继承主要用来扩展类的功能模块;