目录
某些类可能会用到同样的成员变量和成员函数,所以我们可以把类中相同的特性抽取出来,形成的类就是父类/基类/超类,继承它的类就叫做子类/派生类, 所以继承本质是对代码的复用!
继承的语法格式
继承后访问方式的变化
我们知道,类的访问限定符有3种,而类的继承关系也有3种,所以组合后就有9种, 而访问权限的大小关系: public > protected > private
总结:
1.最终的访问方式 = min{成员在基类中的访问限定符, 继承方式}
2.只要父类是private, 无论子类以什么方式继承,子类里面与外面均无法访问!
3.继承部分就可以体现protected与private的区别了,两个的特点都是类里面可以使用,类外面不能使用,但是如果想让子类继承,父类就不能用private, 要用protected或public
4.实际场景中,9种用的最多是两种,父类成员是public/protected, 子类以public方式继承
5.继承的语法格式中,继承方式可以不写,子类是class定义,继承方式默认是私有,子类是struct定义,继承方式默认是公有, 但建议把继承方式写出来
6.不可见指的是类里面和类外面都不能用,私有是指类里面可以使用,类外面不能用
赋值兼容规则
之前博客提到过,相近的类型之间进行赋值或者引用会产生临时对象,临时对象(内置类型/自定义类型)具有常性,所以引用对象时如果不加const, 可能编译就不通过, 但是下面代码可以编译通过!
#include <iostream>
using namespace std;
class Person
{
protected:
string _name;
string _sex;
int _age;
};
class Student : public Person
{
public:
int _No; // 学号
};
int main()
{
Student s;
Person p = s;
Person& rp = s;
Person* ps = &s;
}
所以结论就是 在子类以public方式继承父类的前提下(我们称子类和父类是is_a的关系), 子类对象赋值给父类对象/父类指针/父类引用,我们认为是天然的,不产生临时对象, 这就叫做父子类兼容赋值规则, 也叫做切割/切片
Student s;
Person p = s;
Student s;
Person& rp = s;
Student s;
Person* ps = &s;
继承中的作用域
1.在继承体系中基类和派生类都有独立的作用域
#include <iostream>
using namespace std;
class Person
{
protected:
string _name = "小李子";
int _num = 111;
};
class Student : public Person
{
public:
void Print()
{
cout << " 姓名:" << _name << endl; // 姓名:小李子
cout << " 身份证号:" << _num << endl; // 身份证号:999
cout << " 身份证号:" << Person::_num << endl; // 身份证号:111
cout << " 学号:" << _num << endl; // 学号:999
}
protected:
int _num = 999; // 学号
};
int main()
{
Student s1;
s1.Print();
};
#include <iostream>
using namespace std;
class A
{
public:
void fun()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
cout << "func(int i)->" << i << endl;
}
};
int main()
{
B b;
b.fun(10); //调的是子类的fun函数
b.A::fun(); //指定作用域调的是父类的fun函数
};
子类的默认成员函数
1.子类不写构造函数,编译器默认生成的构造函数会去调用父类的构造函数; 如果我们要写子类的构造函数,就必须把父类看成一个完整的成员去初始化, 复用父类的构造函数
注意:无论初始化列表写的顺序如何,父类的析构函数比子类的析构函数先调用,因为初始化列表执行的顺序是按照声明顺序走的!
#include <iostream>
using namespace std;
class Person
{
public:
Person(const char* name = "peter")
: _name(name)
{
cout << "Person()" << endl;
}
protected:
string _name;
};
class Student : public Person
{
public:
Student(const char* name, int id)
:Person(name) //调用父类的构造函数
, _id(id)
{
cout << "Student(const char* name, int id)" << endl;
}
protected:
int _id; //学号
};
int main()
{
Student s("张三", 18);
//打印结果:
//Person()
//Student(const char* name, int id)
return 0;
}
2.子类不写拷贝构造函数,编译器默认生成的拷贝构造函数会去调用父类的拷贝构造函数; 如果我们要写子类的拷贝构造函数, 就要去调用父类的拷贝构造函数去拷贝父类的那一部分成员
#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;
}
protected:
string _name;
};
class Student : public Person
{
public:
Student(const char* name, int id)
:Person(name) //调用父类的构造函数
,_id(id)
{
cout << "Student(const char* name, int id)" << endl;
}
Student(const Student& s)
:Person(s) //赋值兼容规则
,_id(s._id)
{
cout << "Student(const Student& s)" << endl;
}
protected:
int _id; //学号
};
int main()
{
Student s1("张三", 18);
Student s2(s1);
//打印结果:
//Person()
//Student(const char* name, int id)
//Person(const Person & p)
//Student(const Student & s)
return 0;
}
3.子类不写赋值重载函数,编译器默认生成的赋值重载函数会去调用父类的赋值重载函数; 如果我们要写子类的赋值重载函数, 由于子类的赋值重载和父类的赋值重载构成隐藏关系,因此我们要显式调用父类的赋值重载函数
#include <iostream>
using namespace std;
class Person
{
public:
Person(const char* name = "peter")
: _name(name)
{
cout << "Person()" << endl;
}
Person& operator=(const Person& p)
{
cout << "Person operator=(const Person& p)" << endl;
if (this != &p)
_name = p._name;
return *this;
}
protected:
string _name;
};
class Student : public Person
{
public:
Student(const char* name, int id)
:Person(name) //调用父类的构造函数
, _id(id)
{
cout << "Student(const char* name, int id)" << endl;
}
Student& operator=(const Student& s)
{
if (this != &s)
{
Person::operator=(s);
_id = s._id;
}
cout << "Student& operator=(const Student& s)" << endl;
return *this;
}
protected:
int _id; //学号
};
int main()
{
Student s1("张三", 18);
Student s2("李四", 19);
s1 = s2;
//打印结果:
//Person()
//Student(const char* name, int id)
//Person()
//Student(const char* name, int id)
//Person operator=(const Person & p)
//Student& operator=(const Student & s)
return 0;
}
4. 析构函数注意事项:
4.1 由于多态的原因,析构函数会被统一处理成为destructor, 因此父子的析构函数构成隐藏关系,因此需要显式调用父类的析构函数
#include <iostream>
using namespace std;
class Person
{
public:
~Person()
{
cout << "~Person()" << endl;
}
protected:
string _name;
};
class Student : public Person
{
public:
~Student()
{
//~person(); //err
Person::~Person();
cout << "~Student()" << endl;
}
protected:
int _id; //学号
};
int main()
{
Student s;
//打印结果:
//~Person()
//~Student()
//~Person()
return 0;
}
显式调用父类的析构函数之后,发现父类的析构函数被调用了两次,而屏蔽掉我们自己调用的父类的析构函数,父类的析构函数刚好被调用一次
#include <iostream>
using namespace std;
class Person
{
public:
~Person()
{
cout << "~Person()" << endl;
}
protected:
string _name;
};
class Student : public Person
{
public:
~Student()
{
//Person::~Person();
cout << "~Student()" << endl;
}
protected:
int _id; //学号
};
int main()
{
Student s;
//打印结果:
//~Student()
//~Person()
return 0;
}
4.2 构造要求是先父后子,而析构要求是先子后父,原因如下:
如果是析构是先父后子,父类已经把成员给释放了,而子类在析构函数内部可能还会去访问父类成员(比如将数据写到文件等场景), 就会存在风险!所以析构要求先子后父
而我们手动无法保证析构先子后父,所以为了保证析构安全,保证析构先子后父,父类的析构函数不需要显式调用,子类析构函数结束时会自动调用父类析构,保证了先子后父
实现不能被继承的类
由于子类定义对象时必定要调用子类的构造函数,而子类的构造函数又会去调用父类的构造函数,因此我们可以考虑让父类的构造函数私有化即可, 这样子类就无法定义对象了~
class A
{
private:
A()
{}
};
class B : public A
{
public:
B()
:A() //err, 无法调用
{}
};
int main()
{
B bb;
return 0;
}
C++11新增了关键字final, 被final修饰的类直接不能被继承
class A final
{
private:
A()
{}
};
class B : public A //err,无法继承被final修饰的类
{
};
int main()
{
B bb;
return 0;
}
继承与友元
友元关系不能被继承, 也就是说父类的友元函数不能去访问子类的私有成员变量或私有成员函数
#include <iostream>
using namespace std;
class Student; //声明Student类,因为下面友元的声明要用到Student类
class Person
{
public:
friend void Display(const Person& p, const Student& s);
protected:
string _name; // 姓名
};
class Student : public Person
{
protected:
int _stuNum; // 学号
};
//Display是父类的朋友,但是不能去访问子类的私有成员变量
void Display(const Person& p, const Student& s)
{
cout << p._name << endl; //可以访问
//cout << s._stuNum << endl; //err, 不能访问
}
int main()
{
Person p;
Student s;
Display(p, s);
}
继承与静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子 类,都只有一个static成员实例, 所以可以认为继承了,继承的是static成员的使用权(类似于成员函数的继承,也可以认为没有继承,因为始终只有1份static成员
#include <iostream>
using namespace std;
class Person
{
public:
Person() { ++_count; }
protected:
string _name; // 姓名
public:
static int _count; // 统计人的个数。
};
int Person::_count = 0;
class Student : public Person
{
protected:
int _stuNum; // 学号
};
int main()
{
//打印的地址是一样的, 说明_count静态成员只有1份
cout << &Person::_count << endl;
cout << &Student::_count << endl;
}
我们就可以借助static成员变量来统计在继承体系中一共创建了多少个类
#include <iostream>
using namespace std;
class Person
{
public:
Person() { ++_count; }
protected:
string _name; // 姓名
public:
static int _count; // 统计人的个数。
};
int Person::_count = 0;
class Student : public Person
{
protected:
int _stuNum; // 学号
};
class Graduate : public Student
{
protected:
string _seminarCourse; // 研究科目
};
int main()
{
Person p;
Graduate g;
Student s;
cout << Student::_count << endl; //3
}
单继承
多继承
多继承本身是没有问题的,也是有实际价值的(比如现实生活中,一人身兼多职本质就是多继承),但是有了多继承之后,就可能会导致菱形继承问题
菱形继承
如上图所示,最终Assistant会有两份Person中的成员,所以菱形继承带来了下面两个问题
1.数据二义性问题
#include <iostream>
using namespace std;
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; // 主修课程
};
void Test()
{
// 这样会有二义性无法明确知道访问的是哪一个
Assistant a;
//a._name = "peter"; //err
// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
}
2.数据冗余问题
指定类域可以解决数据二义性问题,但是没有解决数据冗余问题, 数据冗余问题要靠虚继承解决
规定在菱形继承的"中间部分"在继承方式前加关键字virtual,此时被继承的成员就是同一份!
#include <iostream>
using namespace std;
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; // 主修课程
};
void Test()
{
Assistant a;
a._name = "peter";
}
int main()
{
Test();
return 0;
}
如果菱形继承如下所示, 那么virtual应该加在如图所示位置:
虚拟继承解决数据二义性和冗余的原理
class A
{
public:
int _a;
};
class B : public A
//class B : virtual public A
{
public:
int _b;
};
class C : public A
//class C : virtual public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
d.B::_a = 0;
return 0;
}
我们通过调试之后观察内存布局来解释一下虚继承解决问题的原理:
没有使用虚继承:
可以明显的看到,_a成员在d对象中存储了两份,所以有数据冗余的问题
使用了虚继承:
可以看到,使用了虚继承之后,d对象中_a成员只存储了一份,并且当前编译器的实现是把_a放在了最下面,可问题是如果要实现切片呢??
比如, B* bb = &d; 由于bb的类型是B*, B类中本来有的成员是_b和_a, 如上图所示,_b成员就在起始地址处,而如果要访问_a成员呢?? 比如B->_a = 6;
所以如图所示,还存储了一个地址00067bdc,我们通过内存2可以看到该地址处的内容,第一行是与多态有关的,暂时忽略,第二行是个偏移量,也就是_a成员变量和存储B类成员的起始地址的差距,我们在图上也演示了,起始地址加上偏移量之后就是_a的地址
C类成员也是如此,就不赘述了~
当A类中的成员所占空间比较大时,虚继承的方式所节省的空间就很多了~
而之所以不直接在d对象中存储偏移量或者_a的地址,是因为虚基表中不仅要存偏移量,还要存储其他东西,如果全部放在对象中,对象就比较大了~
继承与组合
相同点:都是代码复用
//继承(is_a)
class A
{
protected:
int _a;
};
class B : public A
{
protected:
int _b;
};
//组合(has_a)
class C
{
protected:
int _c;
};
class D
{
protected:
C _c;
int _d;
};
int main()
{
cout << sizeof(B) << endl; //8
cout << sizeof(D) << endl; //8
return 0;
}
不同点: 权限不同
#include <iostream>
using namespace std;
//继承(is_a)
class A
{
public:
void func()
{}
protected:
int _a;
};
class B : public A
{
public:
void f()
{
func();
_a++;
}
protected:
int _b;
};
//组合(has_a)
class C
{
public:
void func()
{}
protected:
int _c;
};
class D
{
public:
void f()
{
_cc.func();
//_cc._c++; //组合类的保护无法使用
}
protected:
C _cc;
int _d;
};
int main()
{
D dd;
//dd.func(); //dd无法直接调用func函数, 必须间接调用
B bb;
bb.func(); //√
return 0;
}
总结
1. 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称 为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的 内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很 大的影响。派生类和基类间的依赖关系很强,耦合度高。
2. 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象 来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复 用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。
3.优先使用对象组合有助于你保持每个类被封装。 实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。