个人主页:@Weraphael
✍作者简介:目前学习C++和算法
✈️专栏:C++航路
希望大家多多支持,咱一起进步!
如果文章对你有帮助的话
欢迎 评论 点赞 收藏 加关注✨
假设需要设计学校教务系统,可以简单划分为:老师和学生,但如果更深层次继划分的话,还可以划分出:校领导、各专业院长、辅导员等,那么就要对每个角色写一个类,那么代码量也未免太大了。
为了复用代码、提高开发效率,可以从各种角色中选出共同点,组成基类(父类),比如一个人都有姓名、年龄、性别、联系方式等基本信息,而老师与学生的区别就在于学号和工号。于是,继承允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类(子类)。
子类继承父类后,子类可以享有父类中的所有公开public/保护protected
属性,除了私有private
内容外。为什么是这样定义的,在【继承的定义】会讲解
#include
using namespace std;
class Person // 父类
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "张三"; // 姓名
int _age = 18; // 年龄
};
class Student : public Person // 子类
{
protected:
int _stuid; // 学号
};
class Teacher : public Person // 子类
{
protected:
int _jobid; // 工号
};
int main()
{
Student s;
Teacher t;
s.Print();
t.Print();
return 0;
}
继承的格式很简单,格式为class 子类 : 继承方式 父类
下面我们看到Person
是基类,也称作父类。Student
是派生类,也称作子类。
在类和对象我们说过,protected
和private
是没有区别的,但在继承中,它们的区别大了!
访问权限和继承权限都是三种,根据排列组合,可以列出以下多种搭配方案:
【子类的可访问情况】
访问权限 / 继承权限 | public |
protected |
private |
---|---|---|---|
父类的public 成员 |
派生类变成public 成员 |
派生类变成protected 成员 |
派生类变成private 成员 |
父类的 protected 成员 |
派生类变成protected 成员 |
派生类变成protected 成员 |
派生类变成private 成员 |
父类的 private 成员 |
不可见 | 不可见 | 不可见 |
【总结】
父类的private
成员在子类中无论以什么方式继承都是不可见的。这里的不可见是指父类的私有成员还是被继承到了子类对象中,但是语法上限制子类对象不能访问父类的private
成员。
如果父类成员需要在子类中能访问,就将父类的成员定义为protected
或者public
。
无论是哪种继承方式,父类中的私有成员始终不可被访问;当子类成员可访问父类成员时,最终权限将会变为访问权限与继承权限中的较小者(public > protected> private
)
注意:使用关键字class
时默认的继承方式是private
,使用struct
时默认的继承方式是public
,不过最好显示的写出继承方式
在实际运用中一般使用都是public
继承,几乎很少使用protetced/private
继承。还有的就是父类的访问权限一般都是public/protected
。因为继承的本质就是代码复用,要是访问权限是private
,子类还访问个pi
啊。
在继承中,允许将子类对象直接赋值给父类,但不允许父类对象赋值给子类。
class A //父类
{
protected:
int a = 666;
};
class B : public A //子类
{
private:
int b = 999;
};
int main()
{
A aa; // 实例化
B bb; // 实例化
aa = bb; // 子给父
bb = aa; //非法,erro
return 0;
}
并且这种赋值兼容转化是非常自然,虽然类型不一样,但没有发生隐式类型转换(表达式中自动进行的类型转换,无需进行特殊的语法操作。这种转换是由编译器根据类型兼容性自动完成的)
那么如何证明没有发生隐式类型转化呢?
int main()
{
int i = 0;
double d = i; // 发生隐式类型转化
// 对i取别名
//double& c = i; //erro
// 正确
const double& c = i;
return 0;
}
如果对于整型变量取别名,并且别名的类型和整型不同,那么就会发生报错。因为i
是整型,当i
赋值给c
时,中间会产生一个临时变量,并且这个临时变量具有常性,因此只要在double
前加上const
就不会发生报错。
那么我们可以用类似于以上的方法来查看子类赋值给父类时有没有发生隐式类型转化:
还有的就是,子类对象可以赋值给·父类的对象 / 父类的指针 / 父类的引用。这里有个形象的说法叫切片(切割)。就是它会把子类中属于父类的成员切割出来赋值给父类。
子类虽然继承父类,但他们有独立的作用域。因此可以出现同名成员。但是默认会将父类的同名成员隐藏,进而执行子类的同名成员,这种情况叫隐藏,也叫重定义。
#include
#include
using namespace std;
class Person
{
protected:
string _name = "张三";
int _id = 111;
};
class Student : public Person
{
public:
void print()
{
cout << "姓名:" << _name << endl;
cout << "身份证:" << _id << endl;
}
protected:
int _id = 999; // 子类和父类出现同名变量
};
int main()
{
Student s1;
s1.print(); // 若出现同名变量,输出的是子类成员
return 0;
}
【输出结果】
出现同名成员时,默认会将父类的同名成员隐藏,进而执行子类的同名成员
若出现同名成员,但就想用父类的成员,该怎么办?
方法:指定作用域,加个域作用限定符::
成员函数也是一样的,需要在调用时指定作用域。
#include
using namespace std;
class A
{
public:
void fun()
{
cout << "A::func()" << endl;
}
};
class B : public A
{
public:
void fun()
{
cout << "B::fun()" << endl;
}
};
int main()
{
B b;
b.fun();
b.A::fun();
return 0;
}
【输出结果】
虽然子类和父类可以有同名成员,但注意在实际中,继承体系里面最好不要定义同名的成员。不然就是自己坑自己!
问:子类中的fun
和父类中的fun
构成什么关系?
#include
using namespace std;
class A
{
public:
void fun()
{
cout << "A::func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
cout << "B::fun(int i)" << endl;
}
};
很多人都以为是函数重载(函数名相同,参数个数不同)。在这我想说,不是构成重载,因为 构成函数重载的前提是同一作用域。
答案:B
中的fun
和A
中的fun
还是构成隐藏
注意:在继承中,只要是命名相同,都构成隐藏 ,与返回值、参数无关。
子类也是类,程序员未显示定义的情况下,同样会生成六个默认成员函数
不同于单一的类,子类是在父类的基础之上创建的,因此它在进行相关操作时,需要为父类进行考虑
大家有没有想过一个问题,当子类实例化后可以调用它的默认构造函数来进行初始化(初始化列表)?那么父类该怎么初始化呢?
有的类和对象的基础,大家可能就会向下面一样:
#include
#include
using namespace std;
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 = 111)
:_name(name)
,_stuid(num)
{
cout << "Student()" << endl;
}
protected:
int _stuid;
};
但是以上的方法是错的,报错结果如下:
因此,C++规定:子类对象创建后,必须先调用父类的默认构造函数初始化父类成员
#include
#include
using namespace std;
class Person
{
public:
Person(const char* name = "张三")
:_name(name)
{
cout << "Person()" << endl;
}
protected:
string _name;
};
class Student : public Person
{
public:
Student(int num = 111)
:_stuid(num)
{
cout << "Student()" << endl;
}
protected:
int _stuid;
};
int main()
{
// 子类对象在实例化后,会先调用父类的默认构造函数初始化其成员变量
Student s1;
return 0;
}
【输出结果】
注意:自动调用是由编译器完成的,前提是父类存在对应的默认成员函数;如果不存在默认构造,会报错。
那么问题来了,父类没有默认构造函数该怎么初始化呢?
父类的默认构造函数其实是在子类的初始化列表调用的,如果父类没有默认的构造函数,则必须在子类构造函数的初始化列表阶段显示调用
但是显示调用不能向一开始那样,在子类的构造函数显示写出父类的成员变量
正确方法:类似定义一个匿名对象
而我们知道初始化列表是有初始化顺序的,其顺序是和声明的顺序有关,C++规定:默认父类的成员是在子类成员前面。
和默认构造函数一样,子类的拷贝构造函数必须先调用父类的拷贝构造完成父类的拷贝初始化
#include
#include
using namespace std;
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(int num = 111)
:_stuid(num)
{
cout << "Student()" << endl;
}
// 拷贝构造本质也是一个构造函数,所以也要写初始化列表
Student(const Student& s)
:Person(s)
,_stuid(s._stuid)
{
cout << "Student(const Student& s)" << endl;
}
protected:
int _stuid;
};
int main()
{
Student s1;
// 拷贝构造
Student s2(s1);
return 0;
}
【输出结果】
注意:和默认构造不同的区别是,拷贝构造的初始化列表要显示写出Person(s)
。如果不写,编译器会默认调用默认构造构造,那么就可能导致构造的对象内容不一样,因此最好显性给出拷贝构造。例如以下案例
#include
#include
using namespace std;
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 = 111)
:Person(name)
,_stuid(num)
{
cout << "Student()" << endl;
}
// 拷贝构造本质也是一个构造函数,所以也要写初始化列表
Student(const Student& s)
//未显示写出Person(s)
:_stuid(s._stuid)
{
cout << "Student(const Student& s)" << endl;
}
protected:
int _stuid;
};
int main()
{
Student s1;
// 拷贝构造
Student s2(s1);
return 0;
}
【监视结果】
C++规定:子类的operator=
必须要先调用父类的operator=
完成父类的复制
因此不难可以写出以下代码
#include
#include
using namespace std;
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)
{
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 num = 111)
:Person(name)
,_stuid(num)
{
cout << "Student()" << endl;
}
Student(const Student& s)
:Person(s)
,_stuid(s._stuid)
{
cout << "Student(const Student& s)" << endl;
}
Student& operator=(const Student& s)
{
if (this != &s)
{
// 调用父类的运算符重载
operator=(s);
_stuid = s._stuid;
}
return *this;
}
protected:
int _stuid;
};
int main()
{
Student s1;
// 拷贝构造
Student s2(s1);
return 0;
}
但是程序发生栈溢出了(可以查看调用堆栈)
为什么会发生栈溢出呢?我们可以先来分析子类的operator=
这下我们知道了,子类和父类出现了同名函数(隐藏/重定义),当要调用父类的operator=
,根据就近原则,子类会优先调用子类里的运算符重载函数。
因此,解决办法也很简单,指定类域(父域)就行。(以下给出核心代码)
Student& operator=(const Student& s)
{
if (this != &s)
{
// 调用父类的运算符重载
Person::operator=(s);
_stuid = s._stuid;
}
return *this;
}
根据以上经验,我们直接让子类的析构调用父类的析构就行
#include
#include
using namespace std;
class Person
{
public:
// 父类默认构造
Person(const char* name = "张三")
:_name(name)
{
cout << "Person()" << endl;
}
~Person()
{
cout << "~Person()" << endl;
}
protected:
string _name;
};
class Student : public Person
{
public:
Student(const char* name = "李四", int num = 111)
:Person(name)
, _stuid(num)
{
cout << "Student()" << endl;
}
// 派生类对象析构清理先调用派生类析构再调基类的析构
~Student()
{
~Person();
}
protected:
int _stuid;
};
int main()
{
Student s1;
Student s2;
return 0;
}
当代码运行起来后,竟然报错了
为什么会报错呢?父类的析构和子类的析构并没有重名,按常理来看这是可以的。但是这里牵扯另一个知识,由于多态的原因,析构函数名被特殊处理了,处理成destructor
(后期会讲解),解决办法就是指定类域(父类)
虽然代码运行起来,但结果还是不对,只创建了两个对象却析构4次,
而我们把代码屏蔽后析构的次数却刚刚好
这么看来 编译器会自动调用析构函数。
友元关系不能继承
#include
using namespace std;
class B;
class A
{
public:
// 父类的友元
friend void print(const A& a, const B& b);
protected:
int i = 1;
};
class B : public A
{
public:
protected:
char x = 'x';
};
void print(const A& a, const B& b)
{
cout << a.i << endl;
cout << b.x << endl;
}
int main()
{
A a;
B b;
print(a, b);
return 0;
}
Display
函数是Person
的友元。所以在Display
函数中访问Person
类成员变量是没问题的,由于友元关系不能继承,因此Display
中直接使用Student
的成员变量会报错。
也就是说,父亲的朋友不一定是孩子的朋友。若想和父亲的朋友成为朋友,就在子类加上友元
父类定义了static
静态成员,则在整个继承体系里面只有一个这样的成员。无论有多少个子类,都只有一个static
成员
#include
#include
using namespace std;
class Person
{
public:
Person()
{
++_count;
}
protected:
string _name;
public:
// 父类的静态成员变量_count
static int _count;
};
// 静态成员变量的初始化(类外)
int Person::_count = 0;
class Student : public Person
{
protected:
int _stuid;
};
int main()
{
Student s1;
Student s2;
Student s3;
cout << "人数 :" << Person::_count << endl;
return 0;
}
【输出结果】
再次提醒:不是static
修饰的成员,每个对象都是独有一份,是staic
修饰的成员,每个对象共用一份。
一个对象可以有多个角色,使其基础信息更为丰富,但凡事都有双面性,多继承在带来巨大便捷性的同时,也带来了个巨大的坑:菱形继承问题。
菱形继承有什么问题呢?从下面的对象成员模型构造,在Assistant
的对象中Person
成员会有两份名字,因此可以看出菱形继承有数据冗余(浪费空间)和二义性(访问谁)的问题。
【代码样例】
#include
#include
using namespace std;
class Person
{
protected:
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;
};
int main()
{
Assistant at;
at._name = "李四";
return 0;
}
【程序结果】
程序提示_name
不明确,因此我们可以指定为其指定类域
int main()
{
Assistant at;
at.Student::_name = "李四";
at.Teacher::_name = "李老师";
return 0;
}
指定访问只能解决二义性问题,但这没有从本质上解决问题!而且还没有解决数据冗余问题
真正的解决方法:虚拟继承
虚拟继承可以解决菱形继承的二义性和数据冗余的问题。加上virtual
关键字修饰被继承的父类(如下代码所示),即可解决问题。但需要注意的是,虚拟继承不要在其他地方去使用
【调试结构】
那么虚拟继承是如何解决二义性和数据冗余的问题?为了研究虚拟继承原理,我们给出了一个简化的菱形继承继承体系,再借助内存窗口观察对象成员的模型(对象在内存中的布局)。
首先我们先看普通的菱形继承为什么会存在冗余和二义性
代码如下:
#include
using namespace std;
class A
{
public:
int _a;
};
class B : public A
{
public:
int _b;
};
class C : public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
int main()
{
D d;
// B和C继承了A
// 因此它们都有_a
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
再来看看菱形虚拟继承
代码如下(稍加改动):
#include
using namespace std;
class A
{
public:
int _a;
};
class B : virtual public A
{
public:
int _b;
};
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;
//最后将_a全部改为0
d._a = 0;
return 0;
}
总结:
虚继承底层是如何解决菱形继承问题的?
- 对于冗余的数据位,改存指针,该指针指向相对距离
- 对于冗余的成员,合并为一个,放置后面(可能前面,取决于编译器),假设想使用公共的成员(冗余成员),可以通过相对距离(偏移量)进行访问。
public
继承是一种is-a
的关系。也就是说每个子类对象都是一个父类对象。例如,花是植物。has-a
的关系。假设B组合了A,每个B对象中都有一个A对象。例如,汽车有轮胎。class A
{
public:
int _a;
};
// 继承
class B : public A
{
// ...
};
// 组合
class C
{
private:
A _c;
};
菱形继承是指在面向对象编程中,当一个类同时继承自两个类,并且这两个类又继承自同一个父类时所产生的继承结构。
菱形形继承会引发一个问题,即菱形继承的二义性和冗余,当子类需要调用父类的某个成员时,由于存在多条继承路径,系统无法确定应该调用哪个父类的成员,从而导致歧义。这会导致编译器无法解析成员的访问路径,进而产生编译错误。
为了解决菱形继承的冗余和二义性,C++引入了虚拟继承。虚拟继承可以通过在中间类的承声明前加上关键字virtual
来实现。通过使用虚拟继承,可以确保子类类只包含一份来自基类的数据成员,避免了数据几余。同时,通过虚拟继承,可以解决二义性问题,子类可以明确指定使用哪个基类的成员。
点击跳转
OO
语言(面向对象语言)都没有多继承,如Java。