上面,我们看到没有继承之前我们设计了两个类 Student 和 Teacher, S t u d e n t Student Student 和 T e a c h e r Teacher Teacher 都有 姓名 / 地址 / 电话 / 年龄 等成员变量,都有 i d e n t i t y identity identity ⾝份认证的成员函数,设计到两个类里面就是冗余的。当然他们也有⼀些独有的成员变量和函数,比如老师独有成员变量是职称,学生的独有成员变量是学号;学⽣的独有成员函数是学习,⽼师的独有成员函数是授课。
既然 S t u d e n t Student Student 和 T e a c h e r Teacher Teacher 两个类的设计有些冗余,那我们能不能把公共的信息提取出来呢?
下面我们公共的成员都放到 Person 中,Student 和 Teacher 都继承 Person,就可以复⽤这些成员,就不需要重复定义了,省去了很多麻烦。
cpp复制代码
class Person
{
public:
// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
void identity()
{
cout << "void identity()" << _name << endl;
}
protected:
string _name = "张三"; // 姓名
string _address; // 地址
string _tel; // 电话
int _age = 18; // 年龄
};
class Studen : public Person
{
public:
// 学习
void study()
{
// ...
}
protected:
int _stuid; // 学号
};
class Teacher : public Person
{
public:
// 授课
void teaching()
{
//...
}
protected:
string title; //职称
};
虽然 S t u d e n t Student Student类 的成员变量看起来只有int _stuid;,但它继承了Person类,它还有string _name 、string _address;等等成员变量。成员函数也不止void study(),还有void identity()
1.2 继承定义
1.2.1 定义格式
下面我们看到 Person 是父类,也称作基类。Student 是子类,也称作派生类。(因为翻译的原因,所以既叫父类/子类,也叫基类/派生类)
子类想访问父类的 p r i v a t e private private 成员虽然不能直接访问,但能间接访问。虽然在子类中是不能访问,但在父类中并没有相关限制,只用父类提供相关访问 p r i v a t e private private 成员变量的成员函数,子类调用其函数就能间接访问。
父类 p r i v a t e private private 成员在子类中是不能被访问的,如果父类成员不想在类外直接被访问,但需要在子类中能访问 ,就定义为 protected。可以看出保护成员限定符是因继承才出现的。
实际上面的表格我们进行一下总结会发现,父类的私有成员在子类都是不可见。父类其他成员在子类的访问方式为: M i n Min Min(成员在父类的访问限定符, 继承方式), p u b l i c public public > p r o t e c t e d protected protected > p r i v a t e private private。
使用关键字 class 时默认的继承方式是private,使用 struct 时默认的继承方式是 public,不过最好显式的写出继承方式
class Student:Person //默认为private继承、struct Student:Person //默认为public继承
在实际运用中一般使用的都是 p u b l i c public public 继承,几乎很少使用 p r o t e c t e d protected protected / p r i v a t e private private 继承,也不提倡使用 p r o t e c t e d protected protected / p r i v a t e private private 继承,因为 p r o t e c t e d protected protected / p r i v a t e private private 继承下来的成员都只能在子类的类里面使用,实际中扩展维护性不强。这里可以认为是 C++ 过度设计了。
ganyu::stack<int> st;这句代码实例化栈,将 T T T 实例化成 i n t int int,也间接将 v e c t o r vector vector 实例化(严格来说只实例化了栈的构造函数)。但我们将 v e c t o r vector vector 实例化时不会把 v e c t o r vector vector 中所有的成员函数都实例化,我们调用谁才实例化谁。
我们调用 p u s h push push 函数时,编译器去找 p u s h push push_ b a c k back back 函数,在子类和父类中都找不到,因为还没有实例化。所以我们要指定类域去访问,表示调用的是 v e c t o r vector vector< T T T> 中的 p u s h push push_ b a c k back back,此时编译器看到 T T T 已经被实例化成 i n t int int 了,就会将 v e c t o r vector vector< T T T> 中的 p u s h push push_ b a c k back back 实例化出一份 i n t int int 版本的出来。
我们可以结合 #define,能灵活更改 s t a c k stack stack 的底层容器,达到类似适配器模式的效果
p u b l i c public public继承的前提下,子类对象可以赋值给父类的对象 / 父类的指针 / 父类的引用 。这里有个形象的说法叫切片 或者切割。寓意把子类中父类那部分切割开来赋值给父类对象/指针/引用
但反过来就不成立:父类对象不能赋值给子类对象
例如:现在有一个 S t u d e n t Student Student 对象, S t u d e n t Student Student 对象可以赋值给父类对象 P e r s o n Person Person,当然,指针和引用也是可以的;但反过来就不成立(总不能无中生有出一个 _ N o No No 成员吧)。
cpp复制代码
class Person
{
protected :
string _name; // 姓名
string _sex; // 性别
int _age; // 年龄
};
class Student : public Person
{
public :
int _No; // 学号
};
int main()
{
Student sobj;
// 1.派⽣类对象可以赋值给基类的对象/指针/引⽤
Person pobj = sobj;
Person* pp = &sobj;
Person& rp = sobj;
//2.基类对象不能赋值给派⽣类对象,这⾥会编译报错
//sobj = pobj;
return 0;
}
这里并没有发生类型转换 。
虽然我们前面讲过不同类型的对象之间进行赋值,支持的是类型转换
cpp复制代码
int i = 0;
double d = i;
将 i i i 赋值给 d d d 走的就是类型转换,中间会生成一个临时对象 。
但是切片并不是类型转换,中间并没有产生临时变量,这是一种特殊处理。
基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(Run-Time Type Information)的 dynamic_cast 来进行识别后进行安全转换。(ps:这个我们后面再单独专门介绍,这里先提⼀下)
class Person
{
protected :
string _name = "小帅"; // 姓名
int _num = 111; // ⾝份证号
};
class Student : public Person
{
public :
void Print()
{
cout << " 姓名:" << _name << endl;
cout << " 身份证号:" << Person::_num << endl;//指定父类的类域进行访问
cout << " 学号:" << _num << endl;
}
protected:
int _num = 999; // 学号
};
int main()
{
Student s1;
s1.Print();
return 0;
}
运行结果:
如果是成员函数的隐藏 ,只需要函数名相同就构成隐藏
注意:在实际中在继承体系里面最好不要定义重名的成员或函数
3.2 例题
cpp复制代码
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);
b.fun();
return 0;
};
A A A 和 B B B 类中的两个 f u n c func func函数 构成什么关系()
A. 重载 B. 隐藏 C.没关系
下面程序的编译运行结果是什么()
A. 编译报错 B. 运行报错 C. 正常运行
第一题:第一眼看上去,他们构成重载关系:函数名相同,参数类型不同。但如果选 A 就错了,这题选B。别忘了,只有在同一作用域的函数才构成函数重载 ,而隐藏是父类和子类中的函数名相同就构成隐藏
第二题:选A,因为子类和父类的 f u n c func func函数 构成隐藏,除非指定父类的作用域去调用,否则同名成员或函数是不会去父类中查找的。b.fun(); 没有传递参数,编译报错。
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 num, const char* sex)
:Person(name)
, _num(num)
, _sex(sex)
{}
//未写拷贝构造
//···
protected:
int _num; //学号
string _sex; //性别
};
int main()
{
Student s1("张三", 1, "男");
Student s2 = s1;
return 0;
}
严格来说, S t u d e n t Student Student类是不用我们自己写拷贝构造的,默认生成的拷贝构造已经完成了我们的需求。前面,我们通过学习知道拷贝构造、赋值重载、析构是一体的。一个不需要写,三个都不需要写;一个要写,三个都要写。因此 S t u d e n t Student Student类 的赋值重载和析构函数都不需要自己写
如果有需要深拷贝的资源,才需要自己实现
4.2.2 自己显式写
那假设 S t u d e n t Student Student 类中有指向的资源,需要我们自己写拷贝构造,又该怎么写呢?
cpp复制代码
class Student : public Person
{
public:
protected:
int _num; //学号
string _sex; //性别
int* _ptr = new int[10];//假设有指向的资源
};
我们说过,所有成员都会走初始化列表,父类 P e r s o n Person Person 没有显示调用,也会走初始化列表。但此时编译器会调用 P e r s o n Person Person 的默认构造 ,虽然编译能通过,但很可能不符合你的需求 ;如果 P e r s o n Person Person 没有默认构造,那么编译报错
4.3 赋值重载
和拷贝构造一样, S t u d e n t Student Student 类严格来说不需要写赋值。
但如果我们需要显式写要怎么写呢
派生类的 o p e r a t o r operator operator= 必须要调用基类的 o p e r a t o r operator operator =。
总结:派生类的 o p e r a t o r operator operator= 必须要调用基类的 o p e r a t o r operator operator= 完成基类的赋值。需要注意的是派生类的 o p e r a t o r operator operator= 屏蔽了基类的 o p e r a t o r operator operator=,所以显式调用基类的 operator=,需要指定基类作用域
4.4 析构函数
首先,严格来说 S t u d e n t Student Student 并不需要我们显式写析构函数
那如果有需要显式释放的资源,析构函数又该怎么写呢?
4.4.1 重载
我们还是以 S t u d e n t Student Student类 为例
首先,如果显式实现析构函数,_ n u m num num 和 _ s e x sex sex 是不用管的。因为int _num是内置类型,而 string _sex会自己调用其析构。我们只需要管父类即可
cpp复制代码
~Student()
{
~Person();
}
但这样会报错
析构是可以显示调用的,但为什么这里调不动呢?
这里有个小知识点:子类的析构会和父类的析构构成隐藏关系。
因为一些特殊的原因,析构函数的函数名会被特殊处理成 d e s t r u c t o r destructor destructor(),所以父类的析构函数和子类的析构函数构成隐藏关系。实际上并没有什么 ~ S t u d e n t Student Student() 和 ~ P e r s o n Person Person(),只有 d e s t r u c t o r destructor destructor()。
所以我们要指定类域调用
cpp复制代码
~Student()
{
Person::~Person();
}
4.4.2 顺序
我们来尝试调用一下析构函数
cpp复制代码
class Person
{
public:
//成员函数
//···
~Person()
{
cout << "~Person()" << endl;
}
protected:
string _name; // 姓名
};
class Student : public Person
{
public:
//成员函数
//···
~Student()
{
Person::~Person();
}
protected:
int _num; //学号
string _sex; //性别
};
int main()
{
Student s1("张三", 1, "男");
Student s2("李四", 2, "未知");
return 0;
}
class Base
{
public :
void func5() { cout << "Base::func5" << endl; }
protected:
int a = 1;
private:
// C++98的⽅法
Base()
{}
};
class Derive :public Base
{
void func4() { cout << "Derive::func4" << endl; }
protected:
int b = 2;
};
为什么呢?因为子类的构造函数,不论是自动生成还是我们自己显式实现,都必须调用父类的构造函数 。但是父类的 p r i v a t e private private成员在子类中是不可见的,因此子类调不到父类的构造函数。
但是这种方式不够明显,如果不调用子类的对象编译器是不会报错的
4.5.2 法二:final
C++11中新增了一个关键字: f i n a l final final
用 f i n a l final final 修饰一个类,表示该类是最终类,无法再被继承。
这种方式更直观一些,不管子类定不定义,直接报错
cpp复制代码
class Base final
{
public :
Base()
{}
void func5() { cout << "Base::func5" << endl; }
protected:
int a = 1;
};
class Derive :public Base
{
void func4() { cout << "Derive::func4" << endl; }
protected:
int b = 2;
};
派生类的 o p e r a t o r operator operator= 必须要调用基类的 o p e r a t o r operator operator= 完成基类的复制。需要注意的是派生类的 o p e r a t o r operator operator= 隐藏了基类的 o p e r a t o r operator operator=,所以显示调用基类的 operator=,需要指定基类作用域