继承(inheritance)是一种面向对象编程的概念,它允许一个类(称为子类或派生类)继承另一个类(称为父类或基类)的特征和行为。子类可以获得父类的成员函数和变量,而不需要重新编写它们。子类还可以添加自己的成员函数和变量,以扩展其功能。
可以说,继承机制 是在面向对象的程序设计中,使代码可以复用的最重要的手段。以前我们接触的复用都是函数复用,而继承是类设计层次的复用。
我们通过下面的例子来说明什么是继承。假设要录入全校师生的信息,那要根据学校中的不同角色,涉及不同的类,如学生、老师、职工……
#include
using namespace std;
class Student
{
string name;
int age;
string tele;
string StudentID;
};
class Teacher
{
string name;
int age;
string tele;
string TeacherID;
};
class Staff
{
string name;
int age;
string tele;
string StaffID;
};
int main() {
return 0;
}
但实际上,我们会发现,这些类的基本成员(姓名、年龄、电话)是一致的,仅仅是ID不一样。那这样写,是否会造成代码的冗余呢?
Yes。为了避免这种冗余,C++中用“继承”的机制来复用类中的代码。
我们先定义出一个包含基本成员的Person类,然后复用这个Person类,让各个类都能继承Person类中的基本成员:
#include
using namespace std;
class Person
{
string name;
int age;
string tele;
};
class Student:public Person //让Student继承Person类
{
string StudentID;
};
class Teacher :public Person
{
string TeacherID;
};
class Staff :public Person
{
string StaffID;
};
int main() {
Student s;
return 0;
}
这就是继承,Student类继承了Person类中的所有成员。我们打开监视窗口看看s的当前成员:
可见,Perosn的成员已经被Student对象包含进来了。
格式:class 子类名 : 继承方式 父类名
在下面的例子中,Person是父类,也称作基类。Student是子类,也称作派生类。
Student和Person是“子承父业”的关系,子类在获得父类全部成员的基础上,又拓展了自己的属性,添加了“学号”“专业”这种 自己的成员变量。
之前学访问限定符时,就剩了个protected没提。因为protected是为继承而设计的。
三种访问方式:
public:公有,类外可直接访问
private:私有,仅类里能访问,类外不可访问
protected:保护。通常用于父类中,子类能访问父类的protected成员,而类外不能访问。
举个栗子,如果我们把父类成员设为private,那类外和子类都被拦在类域之外,子类无法访问父类成员:
#include
using namespace std;
class Person //把Person成员设为private
{
private:
string name="zhangsan";
int age=20;
string tele="1234567";
};
class Student:public Person
{
public:
void Print() {
cout << name << endl; //访问父类成员
cout << age << endl;
cout << tele << endl;
}
string StudentID;
};
int main() {
Student s;
s.Print();
return 0;
}
这种情况下,Student是访问不了Person成员的:
如果我们设父类成员为protected,那就对子类开放了访问窗口(类外仍不可访问):
class Person
{
protected:
string name="zhangsan";
int age=20
string tele="1234567";
};
class Student:public Person
{
……
};
int main() {
Student s;
s.Print();
return 0;
}
这就是protected访问方式。
其实,public、protected、private不仅是三种访问方式,也可以是三种继承方式。
继承方式,分public、protected、private这三种情况。
3种父类的访问方式,与3种子类的继承方式,结合一下就是9种情况:
这个表老师一般都会要求背诵,我们不要死记硬背,这张表其实是很有规律的:两个权限结合,我们取权限更小的那个。从权限来看,public>protected>private,如果是protected继承与基类的private成员,那继承到派生类中的权限就是private(小的那个)。
几点说明:
1.基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指:基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
2.使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
3.在实际运用中,绝大部分情况都用public继承,很少使用protetced/private继承,也不提倡使用protetced/private继承,protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
4.如果不指定继承方式,那默认为private。
1.子类对象可以赋值给父类的对象 / 指针 / 引用。这里有个形象的说法叫切片or切割。寓意把子类中父类那部分切来赋值过去。
#include
using namespace std;
class Person
{
public:
string name="zhangsan";
int age=20;
string tele="1234567";
};
class Student:public Person
{
public:
string StudentID;
};
int main() {
Student s;
Person p;
p = s; //子类对象赋值给父类对象
return 0;
}
2.父类对象不能赋值给子类对象。
3.当父类指针指向子类对象时,如何避免切片现象?
刚刚我们说了,子类对象可以赋值给父类的指针。但当子类对象的大小大于父类对象的大小时,则会切片,使子类对象的部分信息丢失。
这种情况下,为了避免切片现象,处理方法是:将父类指针强制转换为子类指针。
示例:
class A
{
public:
int _a=10;
};
class B:public A
{
public:
int _b=20;
};
int main() {
B b;
A* pa = &b;
B* pb = (B*)pa; //强转
return 0;
}
在继承体系中子类和父类都有独立的作用域。
如果子类和父类中有同名成员,子类成员将屏蔽 父类的同名成员 的直接访问,这种情况叫隐藏,也叫重定义。
例:
#include
using namespace std;
class Person
{
public:
string name="zhangsan";
int age=20;
string tele="Person中的tele";
};
class Student:public Person
{
public:
void Print() {
cout << tele << endl;
}
string tele="Student中的tele";
};
int main() {
Student s;
s.Print(); //此时从Person中继承来的tele被隐藏
return 0;
}
但隐藏并不是删除,从父类那里继承来的同名成员 依然存在于子类中。在子类的成员函数中,可以使用 “父类::父类成员”的方式访问。我们来访问一下看看:
class Person
{
public:
……
string tele="Person中的tele";
};
class Student:public Person
{
public:
void Print() {
cout << Person::tele << endl;
}
string tele="Student中的tele";
};
int main() {
Student s;
s.Print();
return 0;
}
如果是成员函数的隐藏,只需要函数名相同就构成隐藏。不过,在实际中,继承体系里面最好不要定义同名的成员。
区分隐藏与函数重载
小练习:A和B的func构成什么关系? A、重载 B、重定义 C、重写
class A { public: void func() { cout << "func()" << endl; } }; class B : public A { public: void func(int i) { A::func(); cout << "func(int i) -> " << i << endl; } }; int main(void) { B b; b.func(10); }
answer:B重定义,即隐藏。这里很容易误选A。函数重载的前提是,得在同一个作用域里。而子类和父类是两个不同的类域,所以这俩类域中相同的函数名构成隐藏,而非函数重载。
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员。
class Student;
class Person
{
friend void Print(Person& p, Student& s);
protected:
string _name;
};
class Student :public Person
{
protected:
string _ID;
};
void Print(Person& p, Student& s) {
cout << p._name << endl; //可以访问
cout << s._ID << endl; //不可访问
}
int main() {
Person p;
Student s;
Print(p, s);
return 0;
}
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。
#include
using namespace std;
class Student;
class Person
{
public:
static int count;
Person()
:_name("")
{
count++;
}
protected:
string _name;
};
int Person::count = 0;
class Student :public Person
{
protected:
string _ID;
};
int main() {
Person p1;
Student s;
cout << Person::count <<" "<< Student::count << endl; //原本count为2
Person::count = 0; //将Person的count置空
cout << Student::count << endl; //发现Student的count也被清空了,说明这俩count就是同一个
return 0;
}
静态成员始终是放在静态区的,而不是储存在对象的内存分配中。静态成员是一定不被包含在对象中的。
判断正误
1.基类的所有成员变量都会被子类继承。
√,包括静态成员变量,都会被子类继承。
2.基类对象包含了所有的基类成员变量。
×,静态成员变量跟普通变量存放的位置不一样,前者存放在静态区,后者存放在对象的内存空间中。所以不包含。
3.子类对象中不仅包含了所有基类成员变量,也包含了所有子类成员变量。
×,静态成员变量就不被包含
当实例化出子类对象时,会先调用父类的构造函数,再调用子类的构造函数。
示例:
#include
using namespace std;
class Person
{
public:
Person(string n="",int a=0,string tel="")
:name(n)
, age(a)
, tele(tel)
{
cout << "调用了基类的构造" << endl;
}
string name;
int age;
string tele;
};
class Student :public Person
{
public:
Student(string n="", int a=0, string tel="", string teleNum="")
:tele(teleNum) //这里没有显示调用父类的构造函数,但编译器会自觉调用父类的默认构造函数
{
cout << "调用了派生类的构造" << endl;
}
string tele;
};
int main() {
Student s;
return 0;
}
子类的构造函数必须调用 父类的构造函数 初始化父类的那一部分成员。那假如父类没有默认构造函数,要怎么办呢?
class Person
{
public:
Person(string n,int a,string tel) //父类的构造函数
:name(n)
, age(a)
, tele(tel)
{
cout << "调用了基类的构造" << endl;
}
string name;
int age;
string tele;
};
这时,得在子类的构造函数中,显示调用父类的构造函数:
class Student:public Person
{
public:
Student(string n, int a, string tel,string teleNum)
:tele(teleNum)
,Person(n,a,tel) //需显式调用
{
cout << "调用了派生类的构造" << endl;
}
string tele;
};
总之,父类的构造函数是一定要调用 并且是先调用的。假如你不想让某类被继承,那就把它的构造函数私有化。
析构的顺序是固定的:先析构子类对象,再析构父类对象。为了保证一定是这个析构顺序,子类的析构函数在调用完成后,会自动调用父类的析构函数。
#include
using namespace std;
class Person
{
public:
~Person() {
cout << "析构父类" << endl;
}
string name;
int age;
string tele;
};
class Student:public Person
{
public:
~Student() {
cout << "析构子类" << endl;
}
string tele;
};
int main() {
Student s;
return 0;
}
要注意,析构函数不要再显示调用了!来看下面这个错误示例:
class Person
{
public:
~Person() {
cout << "析构父类" << endl;
}
……
};
class Student:public Person
{
public:
~Student() {
Person::~Person(); //在析构函数中显示调用基类的析构
cout << "析构子类" << endl;
}
string tele;
};
int main() {
Student s;
return 0;
}
可见,这样会导致基类对象被析构两次。
因为父类的析构会被自动调用,所以不要再显示调用父类的析构函数了。不然如果有指针变量,析构两次就造成释放野指针的问题。
子类的拷贝构造函数中,初始化 从父类那儿继承来的成员 时,必须得调用父类的拷贝构造函数。
#include
using namespace std;
class Person
{
public:
Person(string n="",int a=0,string tel="")
:name(n)
, age(a)
, tele(tel)
{
cout << "调用了基类的构造" << endl;
}
//拷贝构造
Person(const Person& p)
:name(p.name)
,age(p.age)
,tele(p.tele)
{
cout << "调用了父类的拷贝构造" << endl;
}
string name;
int age;
string tele;
};
class Student :public Person
{
public:
Student(string n = "", int a = 0, string tel = "",string teleNum ="")
:tele(teleNum)
,Person(n,a,tel)
{
cout << "调用了派生类的构造" << endl;
}
//拷贝构造
Student(const Student& s)
:Person(s) //s中,属于Person的那部分会“切片”,拷贝过去
,tele("")
{
cout << "调用了zi类的拷贝构造" << endl;
}
string tele;
};
int main() {
Student s1("zhangsan",20,"111","");
Student s2(s1);
return 0;
}
子类的operator=必须调用父类的operator=,完成父类成员的赋值。
#include
using namespace std;
class Person
{
public:
Person(const char* name="")
:_name(name)
{}
protected:
string _name;
};
class Student :public Person
{
public:
Student(const char* name="",string ID = "")
:_ID(ID)
,Person(name)
{}
Student& operator=(const Student& s) {
if (this != &s) {
Person::operator=(s); //调用父类的operator=
_ID = s._ID;
}
return *this;
}
protected:
string _ID;
};
int main() {
Student s1("111","000");
Student s2;
s2 = s1;
return 0;
}