面向对象语言的三大特性:封装,继承和多态
根据目前学到的知识,对于封装的理解,大致有两层:
- 将数据和方法封装,不想让外面看到用
private/protected
修饰,想让外面看到用public
修饰 - 类型的行为不满足我们的需求,将类型封装,自主规定类型的行为,比如
list
迭代器,反向迭代器
从现在开始,进入继承的学习
1. 继承的概念和定义
1.1 继承的概念
有这样的场景,你要完成一个学生管理系统,必然需要描述很多的对象,于是构建很多类,每个类代表不同的群体,比如学生类、老师类、宿管类…定义出来后,发现每个类中都有某些属性是相同的,比如大家都有名字、年龄、性别这样的属性,在每个类中都定义了一遍,显然代码冗余,于是就有了继承
将每个类的公共属性提取出来,单独作为一个类,称为父类/基类;每个群体中持有它们独有的属性,叫做子类/派生类,通过继承的方式,将父类继承给子类,这样子类就有父类的属性和自身独有的属性
继承是面向对象语言中代码复用的一种重要手段,它允许我们保持原有类的特性,增加新的功能,这样产生的类叫做派生类
1.2 继承的定义
三种的继承方式+访问限定符决定了在派生类中访问基类成员的方式
规律十分简单,如果是基类的private
成员,那么不管何种继承方式,都不能在派生类中直接使用
其他情况,按照public > protected > private
的顺序,基态成员在派生类中的修饰方式,按照继承方式和基态类成员修饰符当中最小值
类成员/继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
基类public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类private成员 | 派生类中不可见 | 派生类中不可见 | 派生类中不可见 |
在实践中,一般不会对基类成员进行private修饰和private继承
struct的默认继承方式和限定符都是共有;class的默认继承方式和限定符都是私有
2. 基类和派生类赋值转换
C语言中,相关类型之间可以发生隐式类型转换,中间会产生临时变量,C++中延续了这种语法
不相关类型间不能隐式类型转换,但对于基类和派生类,可以发生赋值转换,中间不会产生临时变量,它由编译器特殊处理
对于public继承,每一个派生类对象都是一个特殊的基类对象,这种赋值转换也叫做切割/切片
- 派生类对象可以赋值给基类对象/引用/指针
- 基类对象不能赋值给派生类对象
int main()
{Student s;Person p = s;Person* ptr = &s;Person& ref = s;// 没有产生临时变量,因此可以不加constptr->_name += 'x';ref._age = 1;return 0;
}
3. 继承中的作用域
- 继承体系中,基类和派生类有自身独立的作用域
- 如果基类和派生类有同名成员变量,派生类中默认访问的是自身的,可以通过显示调用访问基类的;该同名变量构成隐藏,也叫重定义
- 如果是成员函数的隐藏,只要函数名相同就构成隐藏
class Person
{
protected:string _name;int _num = 111;
};class Student : public Person
{
public:void func(){cout << _num << endl;// 默认是自身的成员变量cout << Person::_num << endl;// 指定父类中的成员变量}protected:int _num = 222;
};int main()
{Student s;s.func();return 0;
}
class Person
{
public:void func(int i = 1){cout << "fun(int i)" << endl;}protected:string _name;int _num = 111;
};class Student : public Person
{
public:void func(){Person::func(10);cout << "fun()" << endl;}protected:int _num = 222;
};int main()
{Student s;s.func();// 调用子类中的func()s.Person::func();// 在子类中调用父类中的func()Person p;p.func();// 调用父类中的func()return 0;
}
4. 派生类的默认成员函数
派生类的成员变量分为两部分:
- 父类的成员(看作一个整体)
- 自身的内置类型和自定义类型按照跟以前一样的方式
-
默认构造函数会调用父类的默认构造函数初始化父类的成员,如果父类没有默认构造函数,则必须在初始化列表中显示调用
class Person { public:Person(const char* name):_name(name){cout << "Person()" << endl;}protected:string _name; };class Student : public Person { public:Student(const char* name, int num):Person(name),_num(num){}protected:int _num; };int main() {Student s("zhangsan", 4);return 0; }
-
拷贝构造调用父类的拷贝构造完成父类成员的拷贝初始化
class Person { public:Person(const char* name):_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 num):Person(name),_num(num){cout << "Student()" << endl;}// s1(s2);Student(const Student& s):Person(s),_num(s._num){cout << "Student(const Student& s)" << endl;}protected:int _num; };int main() {Student s1("zhangsan", 4);Student s2(s1);return 0; }
-
赋值重载调用父类的赋值重载完成父类成员的初始化
class Person { public: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){if (this != &p){_name = p._name;cout << "Person& operator=(const Person& p)" << endl;}return *this;}protected:string _name; };class Student : public Person { public:Student(const char* name = "xxxx", int num = 3):Person(name),_num(num){cout << "Student()" << endl;}// s1(s2);Student(const Student& s):Person(s),_num(s._num){cout << "Student(const Student& s)" << endl;}//s1 = s2Student& operator=(const Student& s){if (this != &s){Person::operator=(s);_num = s._num;cout << "Student& operator=(const Student& s)" << endl;}return *this;}protected:int _num; };int main() {Student s1("zhangsan", 4);Student s2;s2 = s1;return 0; }
-
析构函数调用时,会先析构子类的成员,再析构父类的成员;这是为了防止在子类的析构中访问父类的成员,如果先析构父类,就会造成访问非法空间的问题;因此,编译器在构造时,先构造父类,再构造子类;再析构时,会保证先析构子类,再析构父类
class Person { public: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){if (this != &p){_name = p._name;cout << "Person& operator=(const Person& p)" << endl;}return *this;}~Person(){cout << "~Person()" << endl;}protected:string _name; };class Student : public Person { public:Student(const char* name = "xxxx", int num = 3):Person(name),_num(num){cout << "Student()" << endl;}// s1(s2);Student(const Student& s):Person(s),_num(s._num){cout << "Student(const Student& s)" << endl;}//s1 = s2Student& operator=(const Student& s){if (this != &s){Person::operator=(s);_num = s._num;cout << "Student& operator=(const Student& s)" << endl;}return *this;}~Student(){Person::~Person();cout << "~Student()" << endl;}protected:int _num; };int main() {Student s1;return 0; }
5. 继承与友元
基类的友元函数不是派生类的友元函数,也就是说友元不能继承,基类友元函数不能访问派生类的私有和保护成员
class B;
class A
{
public:friend void func(const A& a, const B& b);protected:int _a;
};class B : public A
{
protected:int _b;
};void func(const A& a, const B& b)
{cout << a._a << endl;cout << b._b << endl;// 编译器报错
}int main()
{A a;B b;func(a, b);return 0;
}
6. 继承与静态成员
基类中的静态成员,不管该基类派生出多少个类,静态成员只有一份,所有派生类使用的都是同一个静态成员
可以根据这个特性,计算基类及其派生类一共创建的个数
class Person
{
public:static int _count;Person(){_count++;}protected:string _name;
};int Person::_count = 0;class Student : public Person
{
protected:int _stdid;
};class Other : public Student
{
protected:int _num;
};int main()
{Person p;Student s;Other o;cout << p._count << endl;// 3cout << s._count << endl;// 3cout << o._count << endl;// 3return 0;
}
7.棱形继承
单继承:一个子类只有一个直接父类时称这种继承关系为单继承
多继承:一个子类有两个及以上的直接父类时称这种继承关系为多继承
多继承一般用于一个对象同时是两种类别,比如西红柿,它既是水果,又是蔬菜,继承两个父类是很合理的
但是,有多继承就意味着会出现棱形继承
棱形继承会导致派生类包含了Other
包含了两份Person
,产生数据冗余和二义性的问题
class Person
{
protected:string _name;
};class Student : public Person
{
protected:int _stdid;
};class Teacher : public Person
{
protected:int _jobid;
};class Other : public Student, public Teacher
{
public:void func(){cout << _name << endl;// 编译器报错,不知道访问的是Student还是Teacher中的_namecout << _other << endl;}protected:int _other;
};int main()
{Other o;o.func();return 0;
}
C++早期设计时,认为多继承很合理,但在后续使用中就出现了棱形继承的问题,该如何解决呢?
使用虚拟继承,让基类的第一级的派生类继承时加上virtual
关键字,表示虚拟继承
class Person
{
public:int _name;
};class Student : virtual public Person
{
public:int _stdid;
};class Teacher : virtual public Person
{
public:int _jobid;
};class Other : public Student, public Teacher
{
public:int _other;
};int main()
{Other o;o.Student::_name = 6;o.Teacher::_name = 7;o._stdid = 1;o._jobid = 2;o._other = 3;return 0;
}
使用棱形虚拟继承,在内存中,基类被放到了最下面,变成公共的,同时第一级的派生类中多了指针,该指针指向一个数,表示该类到基类的偏移量
棱形虚拟继承中,基类被叫做虚基类,派生类中的指针叫做虚基表指针,指向一个虚基表,里面存放着基类的偏移量
发生切割/切片时,会有指针偏移,指针指向自身的对象
8.继承总结
关于多继承的面试题:
-
C++有多继承,为什么java没有?
C++比java先设计,在当时,多继承看起来十分合理,于是就设计了出来,但是没想到出现了很多问题,导致解决非常麻烦,而且生活中也很少使用;而java吸收了这个教训,在设计时就舍弃了多继承
-
多继承的问题是什么?
多继承本身没有任何问题,但有多继承就可能会写出棱形继承
-
棱形继承的问题?如何解决?
数据冗余,二义性;对第一级的派生类使用虚拟继承
-
底层角度是如何解决数据冗余和二义性的?
将基类放到第一级派生类的后面,派生类中加入虚函数指针,指向虚函数表,存放基类的偏移量
继承和组合:
public继承是一种is-a
的关系,比如学生和人,学生是人
组合是一种has-a
的关系,比如汽车和轮胎,汽车有轮胎
如果使用继承,基类对象对于派生类是可见的,一定程度上破坏了基类的封装,导致基类和派生类耦合度高
而对于组合,自定义对象成员在其他对象中不可见,类和类之间耦合度低
开发软件时,尽量做到类和类之间低耦合,高内聚,因此如果一个对象既能使用继承描述,又能使用组合组合描述,优先使用组合