什么是继承
继承(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() (这里又和多态有关系,到时候再填坑),因此构成隐藏了,所以这里要加域作用限定。
在这个析构这里,还有一个小问题:即我们实现子类的析构函数不需要显式调用父类析构函数。因为我们定义一个子类变量都是先定义父类,再定义子类,又由于栈里面的变量符合后进先出原则,我们析构时就会先析构子类再析构父类。子类析构函数结束时父类也会跟着析构,所以我们再自己实现一个就会析构两次。
还有就是浅拷贝问题,需要我们自己写拷贝构造和赋值解决。
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。
|