什么是继承
继承(inheritance)是面向对象软件技术当中的一个概念,与多态、封装共为面向对象的三个基本特征。继承可以使得子类具有父类的属性和方法或者重新定义、追加属性和方法等。
这种技术使得复用以前的代码非常容易,能够大大缩短开发周期,降低开发费用。子类继承父类的特征和行为,使得子类对象(实例)具有父类的属性和方法,或子类从父类继承方法,使得子类具有父类相同的行为。
注意:
父类 = 基类
子类 = 派生类
基类和派生类对象赋值转换(切片)
并且只能子类切片给父类,多的切掉一部分赋给少的,(因为子类是包括了父类,所以成员数量多于基类)
当然也可以强行父类给子类,但是有很大风险这个过程是天然的,也就是说这个过程是语法支持的行为,中途没有类型转换。而像赋值时中间可能会有临时变量。
class Person//父类(基类)
{
protected:
string _name; // 姓名
string _gender; // 性别
int _age; // 年龄
};
class Student : public Person//子类(派生类)
{
public:
int _No; // 学号
};
int main()
{
Person p;
Student s;
p = s;
return 0;
}
隐藏
区别于函数重载,隐藏是子类和父类中有同名成员(跟参数无关),子类成员屏蔽父类对同名成员的直接访问,是不同作用域,而函数重载是必须要在同一作用域。
最好就是不允许同名成员
派生类中的默认成员函数
类里面我们经常会用到的几个默认成员函数,构造,析构,拷贝构造,赋值。
如果我们在派生类中不写这些成员函数会发生什么呢?
class Person
{
protected:
string _name;
string _gender;
int _age;
Person(const char* name = "Peter")
{
cout << "Person()" << endl;
}
~Person()
{
cout << "~Person()" << endl;
}
Person(const Person& p)
{
cout << "Person(const Person& p)" << endl;
}
};
class Student : public Person
{
public:
int _stdid;
string _s;
};
int main()
{
Student s;
return 0;
}
可以看到我们不写,父类还是调用了自己的构造和析构,子类则是自定义类型调用自己的默认构造函数,内置类型不处理,(子类的处理方式其实就和普通类一样)。
并且, 拷贝构造和赋值也是同理,父类的就调用父类的拷贝构造和赋值,子类的就和普通类一样,内置类型就浅拷贝,自定义类型调用自己的拷贝构造个赋值。
总结起来,其实规律很简单,即父类就调用父类的,子类则按普通类的处理规则处理
那什么情况我们必须自己写呢?
首先,如果父类没有默认构造,我们需要自己写。
class Student : public Person
{
public:
Student(const char* name = "Peter", int stdid = 0)
:Person(name) //注意这里的写法
,_stdid(stdid)
{}
int _stdid = 0;
string _s = "s";
};
如果我们还有空间需要释放,我们需要自己写析构。
~Student()
{
Person::~Person();
}
这里有个小知识,由于析构函数都会被统一处理成destructor()
(这里又和多态有关系,到时候再填坑),因此构成隐藏了,所以这里要加域作用限定。
在这个析构这里,还有一个小问题:即我们实现子类的析构函数不需要显式调用父类析构函数。因为我们定义一个子类变量都是先定义父类,再定义子类,又由于栈里面的变量符合后进先出原则,我们析构时就会先析构子类再析构父类。子类析构函数结束时父类也会跟着析构,所以我们再自己实现一个就会析构两次。
还有就是浅拷贝问题,需要我们自己写拷贝构造和赋值解决。
//s2(s1)
Student(const Student& s)
:Person(s)
// 这里发生了切片,传子类给父类,自动切割,完成拷贝。
, _stdid(s._stdid)
{}
Student& operator=(const Student& s)
{
if (&s != this)
{
Person::operator=(s);
//这里记得加域作用限定符来限定,不然栈溢出
_stdid = s._stdid;
}
return *this;
}
继承与友元
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员 其实很好理解,父亲的朋友不一定是儿子的朋友,所以就不能继承。
菱形继承和菱形虚拟继承
这也许就是很多人说的C++的难点,其实多继承就是一个体现,多继承导致了菱形继承,随之而来就是菱形虚拟继承。因此,我们知道其复杂就不建议设计出菱形继承。
什么是菱形继承?菱形继承的问题是什么?
单继承
多继承
菱形继承是多继承的一种特殊情况,即
菱形继承导致二义性和数据冗余。在Assistant
对象中Person
成员会存两份
class Person
{
public :
string _name ; // 姓名
};
class Student : public Person
{
protected :
int _stdid ; //学号
};
class Teacher : public Person
{
protected :
int _jobid ; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected :
string _major ; // 主修课程
};
void Test ()
{
// 这样会有二义性无法明确知道访问的是哪一个
Assistant a;
a._name = "Peter";
// 需要显示指定访问哪个父类的成员可以解决二义性问题,
// 但是数据冗余问题无法解决
a.Student::_name = "Jack";
a.Teacher::_name = "John";
}
解决,指定类域能解决二义性,但是解决不了数据冗余。
如果像下面这样,Person
类中成员很大。就容易浪费很多空间。
class Person
{
public:
string _name; // 姓名
int arr[1000];
};
虚拟继承
这时候我们就可以使用虚拟继承
virtual
class Teacher : virtual public Person
class Student : virtual public Person
用了之后就既没有二义性,也不会出现数据冗余。
当然最好的解决方法就是不要定义虚继承。
继承和组合
继承是一种is-a
的关系,也就是说派生类都是一个基类对象
组合是一种has-a
的关系,
就好像备胎和轮胎的关系,我们说备胎是轮胎,所以是继承。
还有链表的轮胎和车子的关系,只能说车子有轮胎,所以是组合。
两个都可以用的时候,优先使用组合
继承白盒复用,子类和基类之间的依赖性较强,耦合度高
组合黑盒复用,组合类之间没什么依赖关系,耦合度低
继承总结
这也许就是很多人说的C++的难点,其实多继承就是一个体现,多继承导致了菱形继承,随之而来就是菱形虚拟继承。因此,我们知道其复杂就不建议设计出菱形继承。前车之鉴,后车之师。因此后来的许多面向对象的语言都没有多继承,如JAVA。