目录
预备知识:
1.派生类的默认成员函数
1.1四个常见默认成员函数
在C++初阶的时候我讲过C++的默认成员函数,如果不知道的可以去看:
6个默认成员函数,默认的意思就是指我们不写,编译器会变我们⾃动⽣成⼀个,那么在派⽣类中,这⼏个成员函数是如何⽣成的呢?
(1)派⽣类的构造函数必须调⽤基类的构造函数初始化基类的那⼀部分成员。如果基类没有默认的构造函数,则必须在派⽣类构造函数的初始化列表阶段显式调⽤。
#include<iostream>
using namespace std;
class Person
{
public:
Person(const char* name = "peter")
: _name(name)
{
cout << "Person()" << endl;
}
Person(const Person& p)
: _name(p._name)
{
cout << "Person(const Person& p)" << endl;
}
Person& operator=(const Person& p)
{
cout << "Person operator=(const Person& p)" << endl;
if (this != &p)
_name = p._name;
return *this;
}
~Person()
{
cout << "~Person()" << endl;
}
protected:
string _name; // 姓名
};
//子类中的父类对象看成当场整体对象,作为子类自定义类型成员看待
//也就意味着父类的成员会调用父类的默认构造
class Student :public Person
{
public:
protected:
int _num;
};
int main()
{
Student s1;
return 0;
}
这样的运行结果是:
如果我们改成这样:
using namespace std;
class Person
{
public:
//Person(const char* name = "Perter")
Person(const char* name )
: _name(name)
{
cout << "Person()" << endl;
}
Person(const Person& p)
: _name(p._name)
{
cout << "Person(const Person& p)" << endl;
}
Person& operator=(const Person& p)
{
cout << "Person operator=(const Person& p)" << endl;
if (this != &p)
_name = p._name;
return *this;
}
~Person()
{
cout << "~Person()" << endl;
}
protected:
string _name; // 姓名
};
//子类中的父类对象看成当场整体对象,作为子类自定义类型成员看待
//也就意味着父类的成员会调用父类的默认构造
class Student :public Person
{
public:
protected:
int _num;
};
int main()
{
Student s1;
return 0;
}
那么就会出现:
这句话的意思就是:找不到基类的构造函数导致没法完成构造。
那这个时候可以把代码改成这样(其他的不变):
class Student :public Person
{
public:
Student(const char* name, int num)
:_name(name)
,_num(num)
{}
protected:
int _num = 0;
};
但是,这样会报错:
正确写法是:
//正确
Student(const char* name = "张三", int num)
:Person(name)
,_num(num)
{ }
这样Person(name)这个和构造匿名对象一样,如果有多个成员变量要进行初始化,那么就要按照声明顺序进行初始化,如:
#include<iostream>
using namespace std;
class Person
{
public:
//Person(const char* name = "Perter")
Person(const char* name , int age )
: _name(name)
,_age(age)
{
cout << "Person()" << endl;
}
Person(const Person& p)
: _name(p._name)
{
cout << "Person(const Person& p)" << endl;
}
Person& operator=(const Person& p)
{
cout << "Person operator=(const Person& p)" << endl;
if (this != &p)
_name = p._name;
return *this;
}
~Person()
{
cout << "~Person()" << endl;
}
protected:
string _name; // 姓名
int _age; // 年龄
};
//子类中的父类对象看成当场整体对象,作为子类自定义类型成员看待
//也就意味着父类的成员会调用父类的默认构造
class Student :public Person
{
public:
//错误
/* Student(const char* name, int num)
:_name(name)
,_num(num)
{}*/
//正确
Student(const char* name = "张三", int num = 0, int age = 1)
:Person(name , age)
,_num(num)
{ }
protected:
int _num ;
};
int main()
{
Student s1;
return 0;
}
那么运行结果为:
记得每个参数给个缺省值,以满足无参的构造!
综上,最好在要继承的类里面自己写一个默认构造函数,否则很容易报错。
(2)派⽣类的拷⻉构造函数必须调⽤基类的拷⻉构造完成基类的拷⻉初始化。
自定义类型会调用自定义类型的拷贝构造,内置类型则直接完成值的拷贝
若把主函数改成:
int main()
{
Student s1("Jack", 13);
Student s2(s1);
return 0;
}
则运行结果为:
一般情况下,编译器自动生成的拷贝构造就已经足够了,但是在有内存的申请的时候就需要自己写了,因为值拷贝是浅拷贝,如果是自动生成的无法自动申请内存,最终会导致析构函数被调用的时候调用两次导致运行出错,这个时候我们就需要自己写了,比如(其他代码不变):
Student(const Student& s)
:Person(s)
,_num(s._num)
{
// 深拷贝 需要自己写,否则默认生成的就可以够了
//...
}
这个就相当于把子类的特有对象切割出来了,对Person(父类)就是调用Person(父类)自己的拷贝构造,另外的子类成员则需要自己显式写出来。
(3)派⽣类的operator=必须要调⽤基类的operator=完成基类的复制。需要注意的是派⽣类的
operator=隐藏了基类的operator=,所以显式调⽤基类的operator=,需要指定基类作⽤域。
只有深拷贝需要自己显式写,比如(其他的不变):
Student& operator=(const Student& s)
{
// 深拷贝 需要自己写,否则默认生成的就可以够了
if (this != &s)
{
operator=(s);
_num = s._num;
}
return *this;
}
当我们这样写,运行结果为:
说明该代码肯定是有问题的,这个问题就是:
派生类的operator=与基类的operator=构成隐藏,所以我们这样operator(s)最终的结果就是一直调用自己的,我们想要调用的是Person里面的operator=,所以我们需要改成这样:
Student& operator=(const Student& s)
{
// 深拷贝 需要自己写,否则默认生成的就可以够了
if (this != &s)
{
Person::operator=(s);
_num = s._num;
}
return *this;
}
用下面代码运行:
int main()
{
Student s1("jack", 18);
Student s2(s1);
Student s3("rose", 17);
s1 = s3;
return 0;
}
结果为:
(4)派⽣类的析构函数会在被调⽤完成后⾃动调⽤基类的析构函数清理基类成员。因为这样才能保证派⽣类对象先清理派⽣类成员再清理基类成员的顺序。
如果在没有内存的申请和释放的情况下是不用写析构函数的,之前代码用这个main函数运行有:
int main()
{
Student s1("jack", 18);
//Student s2(s1);
//Student s3("rose", 17);
//s1 = s3;
return 0;
}
则运行结果为:
但是如果派生类的成员变量变成这样:
protected:
int _num;
int* ptr = new int[10];
则需要显式写析构函数。
按照正常的写法我们应该这样写:
~Student()
{
~Person();
delete[] ptr;
}
但是这样写就会出现编译错误:
所以这样是不行的。
正确写法是这样的:
~Student()
{
Person::~Person();
delete[] ptr;
}
为什么要指定类域?
在这里其实是因为:~Student和~Person两个函数构成了隐藏:
(5)因为多态中⼀些场景析构函数需要构成重写,重写的条件之⼀是函数名相同(这个我们多态章节会讲解)。那么编译器会对析构函数名进⾏特殊处理,处理成destructor(),所以基类析构函数不加virtual的情况下,派⽣类析构函数和基类析构函数构成隐藏关系。
这个原因就是所有析构函数名都会被重载成函数名为destructor,所以导致了隐藏,这个具体需要到下一讲-C++进阶-多态,去讲解。
(6)派⽣类对象初始化先调⽤基类构造再调派⽣类构造。
(7)派⽣类对象析构清理先调⽤派⽣类析构再调基类的析构。
但是有一个比较重要的问题是:因为编译器对构造要保持先父后子,对析构要保持先子后父的调用顺序,但是编译器在派生类的析构结束时会自动调用父类的析构函数,所以我们不用显式写Person::~Person();,析构函数都是系统自动调用的,不需要我们显式调用!
所以最终代码为:
#include<iostream>
using namespace std;
class Person
{
public:
//Person(const char* name = "Perter")
Person(const char* name, int age)
: _name(name)
, _age(age)
{
cout << "Person()" << endl;
}
Person(const Person& p)
: _name(p._name)
{
cout << "Person(const Person& p)" << endl;
}
Person& operator=(const Person& p)
{
cout << "Person operator=(const Person& p)" << endl;
if (this != &p)
_name = p._name;
return *this;
}
~Person()
{
cout << "~Person()" << endl;
}
protected:
string _name; // 姓名
int _age; // 年龄
};
//子类中的父类对象看成当场整体对象,作为子类自定义类型成员看待
//也就意味着父类的成员会调用父类的默认构造
class Student :public Person
{
public:
//错误
/* Student(const char* name, int num)
:_name(name)
,_num(num)
{}*/
//正确
Student(const char* name = "张三", int num = 0, int age = 1)
:Person(name, age)
, _num(num)
{
cout << "Student(const char* name = \"张三\", int num = 0, int age = 1)" << endl;
}
Student(const Student& s)
:Person(s)
,_num(s._num)
{
// 深拷贝 需要自己写,否则默认生成的就可以够了
//...
cout << "Student(const Student& s)" << endl;
}
Student& operator=(const Student& s)
{
// 深拷贝 需要自己写,否则默认生成的就可以够了
if (this != &s)
{
Person::operator=(s);
_num = s._num;
}
return *this;
cout << "Student& operator=(const Student& s)" << endl;
}
~Student()
{
//Person::~Person();
delete[] ptr;
cout << "~Student()" << endl;
}
protected:
int _num;
int* ptr = new int[10];
};
int main()
{
Student s1("jack", 18);
//Student s2(s1);
//Student s3("rose", 17);
//s1 = s3;
return 0;
}
那么运行结果为:
这是因为有如下关系:
总结一下就是:构造函数一般都要自己写,拷贝构造函数、赋值、析构函数都只有深拷贝的时候就要自己写,否则自动生成的就够了!
1.2实现一个不能被继承的类
⽅法1:基类的构造函数私有,派⽣类的构成必须调⽤基类的构造函数,但是基类的构成函数私有化以后,派⽣类看不⻅就不能调⽤了,那么派⽣类就⽆法实例化出对象。
但方法一不能从根本上解决问题,因为这样还是能继承其他的函数,虽然这个构造函数不可见了,但是构造还是存在的。
⽅法2:C++11新增了⼀个final关键字,final修改基类,派⽣类就不能继承了。
//实现一个不可被继承的类
//C++11的方法
class Base final
{
public:
void func() { cout << "Base::func()" << endl; }
protected:
int a = 1;
};
//报错
class Drive :public Base
{};
这样处理的结果就是:
2.多继承及其菱形继承问题
2.1 继承模型
单继承:⼀个派⽣类只有⼀个直接基类时称这个继承关系为单继承。
比如这种就是单继承:
单继承定义就是单纯的class关键字+类名+继承方式(public/protected+private)+一个被继承的类名(基类名)。
多继承:⼀个派⽣类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型是:先继承的基类在前⾯,后⾯继承的基类在后⾯,派⽣类成员在放到最后⾯。
比如下面的就是一个多继承:
菱形继承:菱形继承是多继承的⼀种特殊情况。菱形继承的问题,从下⾯的对象成员模型构造,可以看出菱形继承有数据冗余和⼆义性的问题,在Assistant的对象中Person成员会有两份。⽀持多继承就⼀定会有菱形继承,像Java就直接不⽀持多继承,规避掉了这⾥的问题,所以实践中我们也是不建议设计出菱形继承这样的模型的。
比如以下类型的继承:
菱形继承会存在这种情况,但是有数据冗余和二义性的问题。(出现的情况基本上是public继承的时候,如果是其他类型的继承可能就不会)
数据冗余:假设Person有一个变量_a,那么如果是在Student和Teacher中都是存了一个_a,而在Assistant就会存放两个_a,而且这个时候并不代表这个就构成隐藏了,因为隐藏有个条件:隐藏的前提是派生类自己定义了同名成员,而Student和Teacher只是继承了Person::_a,没有覆盖它。Assistant 继承的是Assistant::Student::_a和Assistant::Teacher::_a,它们是 两个不同的副本(来自不同的继承路径),而不是隐藏关系。虽然Student::_a和Teacher::_a理论上应该是同一个变量,但实际上它们是 两个独立的副本,导致存储浪费。从逻辑上来看是没必要存储两份的。
二义性:还是刚刚的例子,由于Assistant继承了两份_a,如果直接访问Assistant._a,编译器无法确定应该使用Student继承的_a还是Teacher继承的_a,导致 二义性错误。
假设有以下菱形继承:
//菱形继承
class Person
{
public:
string _name; // 姓名
};
class Student : public Person
{
protected:
int _num; //学号
};
class Teacher : public Person
{
protected:
int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; // 主修课程
};
那么想要访问到Student的_name和Teacher的_name则需要:
int main()
{
Assistant A;
//访问Student的_name
cout << A.Student::_name << endl;
//访问Teacher的_name
cout << A.Teacher::_name << endl;
return 0;
}
这样很麻烦,如果不是菱形继承,我们直接:A._name即可,所以说:尽量别写菱形继承。
2.2虚继承
很多⼈说C++语法复杂,其实多继承就是⼀个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂,性能也会有⼀些损失,所以最好不要设计出菱形继承。多继承可以认为是C++的缺陷之⼀,后来的⼀些编程语⾔都没有多继承,如Java。
还是上面的例子,我们要解决这个问题,就需要用到虚继承:
就是在一个类被多个类继承的时候在继承方式前加virtual,这个就可以避免了菱形继承的数据冗余和二义性的问题!
class Person
{
public:
string _name; // 姓名
};
class Student : virtual public Person
{
protected:
int _num; //学号
};
class Teacher : virtual public Person
{
protected:
int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; // 主修课程
};
这个也算菱形继承,对于这种写虚继承只要在B和C继承的时候加上:virtual即可。
2.3多继承中指针偏移问题
下⾯说法正确的是( )
A:p1 == p2 == p3
B:p1 < p2 < p3
C:p1 == p3 != p2
D:p1 != p2 != p3
//指针偏移的问题
class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main()
{
Derive d;
Base1* p1 = &d;
Base2* p2 = &d;
Derive* p3 = &d;
return 0;
}
有一个特性:先继承的类的成员先放到前面,其次再依次往后放,最后才放派生类独有的类的成员,但在继承的切割这一部分,我讲过,继承是先放基类的成员再放派生类的那个部分。
按照这个说法最终结果类似于:
所以这个题目选C。
3.继承与组合
(1)public继承是⼀种is-a的关系。也就是说每个派⽣类对象都是⼀个基类对象。
(2)组合是⼀种has-a的关系。假设B组合了A,每个B对象中都有⼀个A对象。(比如:学生是一个特殊的人,学生具备了所有人的性质,也就是说学生组合了人;栈是一个特殊的顺序表/链表,也就是说stack组合了vector/list)
组合这个概念可能很多人都不是很了解,我们在实现Mystack的时候,如:
//原写法
template<class T,class Container = vector<T>>
class MyStack
{
public:
void push(const T& x)
{
_con.push_back(x);
}
void pop()
{
_con.pop_back();
}
const T& top()
{
return _con.back();
}
bool empty()
{
return _con.empty();
}
private:
Container _con;
};
其中的vector<T>与Mystack类相结合,并把一个类放到模板参数那里就算组合,如果我们Mystack<int,list<int>>,的话,那就是list与Mystack组合,也就是说两个类可以组合,组合另外一个类的类可以调用被组合的类的函数等等,这个我们只要了解一下即可。
(3)继承允许你根据基类的实现来定义派⽣类的实现。这种通过⽣成派⽣类的复⽤通常被称为⽩箱复⽤(white-box reuse)。术语“⽩箱”是相对可视性⽽⾔:在继承⽅式中,基类的内部细节对派⽣类可⻅ 。继承⼀定程度破坏了基类的封装,基类的改变,对派⽣类有很⼤的影响。派⽣类和基类间的依赖关系很强,耦合度⾼。
一般来说,耦合度高的两个东西,如果一个出了问题,另外一个也有可能出问题。
(4)对象组合是类继承之外的另⼀种复⽤选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接⼝。这种复⽤⻛格被称为⿊箱复⽤(black-box reuse),因为对象的内部细节是不可⻅的。对象只以“⿊箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使⽤对象组合有助于你保持每个类被封装。
比如如果Stack要用容器的push_back,那么用没实现push_back的那么就没法作为组合。
(5)优先使⽤组合,⽽不是继承。实际尽量多去⽤组合,组合的耦合度低,代码维护性好。不过也不太那么绝对,类之间的关系就适合继承(is-a)那就⽤继承,另外要实现多态,也必须要继承。类之间的关系既适合⽤继承(is-a)也适合组合(has-a),就⽤组合。
在C++的STL部分经常喜欢使用的是组合而非继承,继承一般只有iostream这些继承和多态需要继承了。
4.总结
在现在的学习中,继承确实不是那么好用,因为继承的规则太多了,还涉及的地方比较多,不过,继承在后面的工作中用得地方也还是比较多的,学好继承多一个技能,继承被设计出来也就是供我们使用的,希望你们能通过我这两篇博客的讲解把继承的知识应用到往后的学习中!
好了,这讲就讲到这里,下讲将进行讲解:C++进阶-多态1。喜欢的可以一键三连哦,下讲再见!