目录
面向对象编程的主要目的之一是提供可重用的代码。再开发一个大型的新项目的时候,复用已经经过测试的代码要比自己重新写代码或直接拷贝源代码要好的多 。因为使用原有的代码可以节约时间,由于已有代码已经被使用或测试,很好的避免出现错误。
C++的类提供了更加高层次的复用。比修改代码更加好,这种方法叫做继承,可以在类原有的属性进行增加扩展其他的功能,它是在从原来的类生成一个派生类(子类),原来的类叫基类(父类),派生类有基类的特征属性。通过继承一个类要比实现一个类要简单得多,更容易设计。就好比继承家产要比自己白手起家使自己更有钱容易。
一个Person的基类。
一个Student继承了Person的派生类。
一个Teacher继承了Person的派生类。
一个Student的类和Teacher的类里面的成员变量都一般要有一个姓名,和一个身份证号,学号或工号,因为姓名和身份证号的属性一致,我们可以统一放在一个类里(就是基类),可以取名为Person。这个时候,我们就不需要再Student和Teacher的类里面各自实现,我们可以直接继承Person(父类)生成Students(派生类)和Teacher(派生类)。
这样,Student和Teacher都有继承父类的属性了。
class Person {
public:
Person(const string& name = "xxx", const string& id = "xxx")
:_name(name)
, _id(id)
{}
void Print() {
cout << "名字:" << _name << "::身份证:" << _id;
}
private:
string _name;
string _id;
};
class Student : public Person
{
public:
Student(const string& name = "xxx", const string& id = "000", const string& stid = "00000")
:Person(name, id)
, _stid(stid)
{}
void PPrint() {
Print();
cout << "::学号" << _stid << endl;
}
private:
string _stid;//学生学号
};
class Teacher : public Person
{
public:
Teacher(const string& name = "xxx", const string& id = "000", const string& jobid = "00000")
:Person(name,id)
,_jobid(jobid)
{}
private:
string _jobid ;//老师工号
};
int main()
{
Student s("张三","666666","12345");
Teacher t("李四","777777","67890");
s.PPrint();
t.Print();
return 0;
}
这里t的对象为什么没有工号输出是因为类没有函数接口打印。
派生类和基类的特殊关系
派生类的对象可以使用基类的方法,只要不是私有的。比如
Student s("张三","666666","12345");
s.PPrint();//派生类对象使用派生类的函数
s.Print();//派生类对象使用基类函数
//但是基类对象不可以使用派生类函数
派生类和基类的赋值
派生类对象可以赋值给基类对象,基类引用,基类的指针,就是把派生类中基类那部分的对象赋值过去。(只能把子类中父类的那一部分赋值给基类的对象)。
Student s("张三","666666","12345");
Person man = s;
Person& rman = s;
Person *pman = &s;
man.Print();
rman.Print();
pman->Print();
这里一个派生类学生的对象s,和一个基类person基类的对象man,引用rman,Preson* 的pman,这里s对象给man对象赋值,就把派生类中基类的对象那部分赋值给man对象,如果是rman引用,就把派生类中基类那部分的对象起别名,如果是指针pman,就指向派生类中基类的那一部分的地址。
这种特殊关系只能把派生类的给基类,不能让基类的对象和地址给派生类的对象,引用和地址。这两个类是相近的,但是他们赋值时不会参数临时对象。因为派生类继承了基类,使用基类可以调用派生类中基类的函数调用方法。
关系继承和访问限定符
派生类继承基类的方式有三种:public继承,protected继承,private继承。
一般来说,我们使用继承都是以public公有的继承方式来继承。
基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面
都不能去访问它。就是父类只要被继承了,不管什么成员都被派生类继承了,只是在派生类不可以访问。
基类的public成员可以在派生类里面使用,也可以在类外面使用。
基类的protected保护成员只能在自己的基类和继承了它的派生类使用。
这里的_a是私有的,_b是公有的,_c保护的。从报错可以看出,A类的私有成员_a只能在类里面访问,不管私有成员的类按什么方式(public,private,protected)继承给派生类,都不允许在自己的基类外访问。_b保护成员可以在继承的派生类进行使用,但在类外面不可使用,说明保护成员只能在自身的类和继承的类中使用,公有类可以在类里面类外面使用,这里就不过多解释。实际情况下,都是使用public的方式继承基类。如果是private继承的出的派生类,即使基类的成员函数和变量是公有的,在基类和他的派生类外边无法访问。
class A {
private:
public:
int _b = 20;
int _a = 10;
void fun()
{
cout << _a << _b << _c << endl;
}
protected:
int _c = 30;
};
class B : private A
{
public:
void Print() {
fun();
}
};
int main()
{
B b;
b.Print();
b.fun();//error因为B是用private继承的A,所以无法把A即使是公有的成员在类外面访问
return 0;
}
继承:is-a关系
C++中有三种继承方式,public,private,protected三种,public公有继承是最主要的方式,这种方式建立了一种叫is-a的关系,即派生类对象也是基类的对象。就比如一个人的一个基类,有名字,身份证明,性别。然后再学校中,学生是人的一种类型,所以就可以再人这个类中派生出一个学生的类,然后学生这个类就继承了人这个基类的属性,在此基础上,可以增加其他的属性。
继承的作用域
在继承中,如果基类和派生类都有同名的函数,同名的函数构成了一种隐藏的关系,只要是函数名相同了,不管类型还是参数的不同,都构成了隐藏,他们不构成重载,因为虽然同样的函数名,但他们却是不同的作用域。
class A {
public:
void fun(){
cout << "class A" << endl;
}
};
class B : public A
{
public:
void fun(int x) {
cout << "class B" << endl;
}
};
int main()
{
B b;
b.fun(1);
b.A::fun();//访问基类的同名函数要加类域
return 0;
}
在继承当中,如果派生类对象使用了函数调用,并且基类也有同名的函数,对象就会调用自己的函数(就近原则)。如果想访问基类的同名函数,就要在同名函数前加上类域。
继承中派生类的默认成员函数
默认构造函数
默认构造函数,要么没有参数,要么所有的参数都有默认值或是全缺省参数。如果没有定义任何默认构造,编译器就会自动生成默认构造函数。派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。如果基类的构造函数私有化,派生类的对象无法生成,无法实例化。
拷贝构造函数
派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化
赋值运算符重函数
派生类的operator=必须要调用基类的operator=完成基类的复制。
析构函数
派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能
保证派生类对象先清理派生类成员再清理基类成员的顺序。
class Person {
public:
Person(const string& name = "xxx", const string& id = "xxx")//构造函数
:_name(name)
, _id(id)
{
cout << "构造:Person()" << endl;
}
Person(const Person& p)//拷贝构造
:_name(p._name)
, _id(p._id)
{
cout << "拷贝:Person(const Person& p)" << endl;
}
Person& operator=(const Person& p)//赋值重载
{
_name = p._name;
_id = p._id;
cout << "赋值:Person& operator=(const Person& p)" << endl;
return *this;
}
~Person(){
cout << "析构 :~Person()" << endl;
}//析构函数
void Print() {
cout << "名字:" << _name << "::身份证:" << _id;
}
private:
string _name;
string _id;
};
class Student : public Person
{
public:
Student(const string& name = "xxx", const string& id = "000", const string& stid = "00000")//构造函数
:Person(name, id)//显示调用
,_stid(stid)
{
cout << "构造:Student()" << endl;
}//构造派生类的时候,我们先要显示调用构造基类的
Student(const Student& s)//拷贝构造
:Person(s)//派生类对象给基类的拷贝构造,基类会把派生类中基类的成员进行拷贝
,_stid(s._stid)
{
cout << "拷贝:Student(const Person& p)" << endl;
}
Student& operator=(const Student& s)//赋值重载
{
_stid = s._stid;
Person::operator=(s);//这里的赋值重载调用基类的赋值重载的时候,要加上类域,如果不加,将会无限递归
cout << "赋值:Student& operator=(const Student& p)" << endl;
return *this;
}
~Student(){
cout << "析构 :~Student()" << endl;
}//析构函数
void PPrint() {
Print();
cout << "::学号" << _stid << endl;
}
private:
string _stid;//学生学号
};
int main()
{
Student t("李四","777777", "67890");
Student t1(t);
Student t2("王五", "8888888", "12345");
t1 = t2;
return 0;
}
再派生类构造函数的时候,先调用基类的构造函数。析构的时候,先析构派生类的然后再析构基类的。
派生类和基类的析构函数其实是构成虚函数的,我们再析构的时候,不需要显示的调用基类的析构函数。为什么要先析构派生类的呢?因为如果先析构基类,如果有一些深拷贝或开辟的空间,先析构基类,然后再派生类析构,可能就会有非法访问的情况。
不可被派生类继承的函数
构造函数
再继承当中,基类的构造函数不会被派生类继承,因为基类的构造函数是创建新对象,而其他函数是已经实例化对象可调用的类方法,所以基类构造函数不可被派生类继承的原因之一。派生类可以使用基类的方法,是因为派生类继承了基类的对象,而构造函数再完成构造的时候才创建给派生类继承的基类对象。所以说如果构造函数是私有成员,继承基类的派生类是无法创建对象的。
友元函数
友元函数不可被继承
多继承
继承可以分为单继承,多继承和多继承中的一种菱形基础。
单继承
有一个基类A,然后用另一个类B继承A类,再把B类做为基类把它继承给C类,这种按一条线路的继承下来的方式就是单继承。C再继承B的时候,也有了A的属性。
多继承
多继承就是有一个派生类类,继承了两个或多个基类,这里的派生类C继承了基类A和B。C就有了A和B的属性。
菱形继承
菱形继承也符号is-a关系。
直观的看,这里A这个基类被派生类B和C继承了,然后D类多继承了B和C,这样D派生类有B类和B继承A类的属性加C类和C继承A类的属性。
如果创建了一个D类的对象d,然后A类的成员是公有的,但如果直接用d对象直接访问A类的成员_a是不允许的,因为d对象继承的B和C类,然后B和C都继承了同一个基类A,就会有两个_a,这样编译器就不知道是_a是B还是C类所继承的,就会存在歧义(二义性)。解决的方法之一就是再有歧义(二义性)的地方加上作用域(类名)让编译器知道是哪个类的继承。这种方法虽然解决了二义性,但是他会数据冗余。
虚继承解决菱形继承
继承的时候,再public之前加入关键字virtual就是虚继承,虚继承解决了二义性和数据冗余。而且不需要再原来有歧义的地方加上类名。
上图是一篇没有问题的代码。对_a没有二义性。
虚继承和加上类名方法的不同
不使用虚继承的菱形继承(在加成员变量前加类名)
如果不使用虚继承,d对象继承B和C的时候,也会把B个C继承A的成员_a继承。虽然这里解决了二义性***(二义性指的是编译器无法区别成员_a是B类还是C类继承的)***,但d对象的_a不需要两个不同的值,所以这种方法不好,且d对象只需一个_a,这里生成了两个,所以会造成数据的冗余。
使用虚继承
这里_a为什么会在_d下一个地址,是因为使用了虚继承,这里的A类的_a就不会存在派生类的B和C。按原来的做法_a应该分别在_b和_c的前一个地址,但是这里存了两个地址,通过查看这两个地址,我们发现了分别有两个不同的16进制的值14和0c,十进制分别代表20和12。这两个其实是代表偏移量,分别表示对应B类偏移20个bit位5个字节,C类偏移12个bit位3个字节,然后找到了_a。
如果生成的无论多少对象都是同样的类型,这两个地址都是一样的。
第一种开辟的4字节,虚函数开辟5字节,看似虚继承开的空间大,但是这是因为每个类只有一个整形的成员变量,使用只有四个字节,如果A类有一个比较大的数组,如果不使用虚继承,则会开辟两个数组,如果使用虚函数,则开辟一个数组。假设A类有一个数组大小位10的整形数组,如果不使用虚继承会开辟92个字节大小,如果使用了则开辟60个字节大小。因为内存要对齐,使用使用虚继承的会在92 - 10 * 4的基础上加上10字节。