老生常谈的话题,面向对象三大特性:封装继承和多态,封装前边已经介绍了部分了,比如你把成员变量访问修饰限定符设置为private或者什么其他的就是封装的一种,这一讲我们开始介绍继承。
首先,为什么要继承?
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <list>
#include <deque>
#include <string>
using namespace std;
class Student
{
public:
// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
void identity()
{
}
// 学习
void study()
{
// ...
}
protected:
string _name = "peter"; // 姓名
string _address; // 地址
string _tel; // 电话
int _age = 18; // 年龄
int _stuid; // 学号
};
class Teacher
{
public:
// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
void identity()
{
// ...
}
// 授课
void teaching()
{
//...
}
protected:
string _name = "张三"; // 姓名
int _age = 18; // 年龄
string _address; // 地址
string _tel; // 电话
string _title; // 职称
};
int main()
{
return 0;
}
一个老师一个学生,这两位成员函数成员方法均高度重合,定义成两个独立类过于冗余,因此我们采用一个父类Person,由老师学生充当其子类。
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <list>
#include <deque>
#include <string>
using namespace std;
class Person
{
public:
// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
void identity()
{
cout << "void identity()" << _name << endl;
}
protected:
string _name = "张三"; // 姓名
string _address; // 地址
string _tel; // 电话
int _age = 18; // 年龄
};
class Student : public Person
{
public:
// 学习
void study()
{
// ...
}
protected:
int _stuid; // 学号
};
class Teacher:public Person
{
public:
// 授课
void teaching()
{
//...
}
protected:
string _title; // 职称
};
int main()
{
Student s;
Teacher t;
s.identity();
t.identity();
return 0;
}

这就是一个最为简单的继承代码,采用的继承方式是public。
在上述程序中,我们称Person类为父类或基类,称Student和Teacher为子类或派生类。
虽然继承方式有三种但其实和访问修饰限定符并不是同一个东西,继承继承的是基类的成员,简单来讲,继承基类访问方式的变化是取基类成员访问修饰限定符和继承方式中权限最小的那个,比如基类中有一个protected成员,你用public方式继承,到你子类这还是protected。
既然讲到了继承,就不得不填一下之前埋下的坑,曾经我们提过,protected在之前和private可以理解为一个东西,都是不能在类外访问,但如今引入继承概念后,private修饰的成员甚至是不能在继承子类中访问的而protected却可以。
但是这里又有一个误区,private成员他只是不能被子类访问,并不是没有继承



使用关键字class时默认的继承方式是private,使用struct关键字时默认的继承方式是public,但是最好显式使用,而且,一般来说,public继承用的是最多的。
接下来讲一下模板的继承,之前我们实现stack是利用了组合的方式,直接包含一个vector或者其他容器成员,我们同样可以通过继承的方式实现stack等其他依赖于其他容器的类。
cpp
template<class T>
class stack :public std::vector<T>
{
public:
void push(const T& x)
{
vector<T>::push_back(x);
}
void pop()
{
vector<T>::pop_back();
}
bool empty()
{
return vector<T>::empty();
}
const T& top()
{
return vector<T>::back();
}
};
这样写固然没问题,但仍然形成不了stack可以依赖多个容器的特性,我们可以用vector实现stack,也可以用list和deque,但是这种写法只能用vector。
cpp
//#define Container std::vector<T>
//#define Container std::list<T>
#define Container std::deque<T>
template<class T>
class stack :public Container
{
public:
void push(const T& x)
{
Container::push_back(x);
}
void pop()
{
Container::pop_back();
}
bool empty()
{
return Container::empty();
}
const T& top()
{
return Container::back();
}
};
利用#define定义宏的方式即可实现上述我们想达到的效果。
关于类模板的继承还有两个关键点要提,首先,在继承语法中,若基类是类模板需要指定类域,再者,由于模板是按需实例化,我们在调用该类模板的成员函数时不能直接调用,因为它还没有实例化该方法,需要用vector<int>指定。
接下来讲一下基类和派生类之间的转换。
public继承的派生类对象可以赋值给基类的指针/基类的引用,我们一般称之为切片,可以形象的理解成将子类中父类那部分剥离出来赋给父类。
但父类对象并不能赋值给子类,因为你可能会有一些方法没有实现。
不过,虽然父类对象不能直接赋值给子类,但可以通过强制类型转换将父类的指针或引用赋值给子类的指针或引用。
而且,将子类赋值给基类的操作并不是类型转换。
cpp
Person p1 = s;
Person* pp = &s;
Person& p2 = s;
Person p3 = t;
s = p1;

cpp
Student* ps = (Student*)&p1;
Teacher& tt = (Teacher&)p1;
这种转化是不安全的,因为本身子类就可能包含父类所没有的方法,因此当基类是多态类时可以用dynamic_cast来识别后进行安全转换,这里不再赘述,后续再提。
接下来讲一讲继承中的作用域。
首先,基类和派生类都有自己的作用域。
再者,基类和派成类的成员变量名可以重复,但基类的同名成员变量会被隐藏。
cpp
class A
{
protected:
string _name = "asd";
};
class B :public A
{
public:
void print_name()
{
cout << _name << endl;
}
protected:
string _name = "sdhkf";
};
cpp
int main()
{
//A a;
B b;
b.print_name();
return 0;
}

来看一看隐藏中的易错点。
cpp
class A
{
public:
void fun()
{
cout << "fun(void)" << endl;
}
protected:
string _name = "asd";
};
class B :public A
{
public:
void fun(int c)
{
cout << "fun(int)" << endl;
}
void print_name()
{
cout << _name << endl;
}
protected:
string _name = "sdhkf";
};
请问这两个fun函数构成什么关系?
我觉得很多人第一反应就是重载,因为二者函数名相同,参数列表不同,完美符合重载的定义,但重载的前提就是需要在同一作用域,显然不满足条件,因此这里是构成隐藏。
在派生类中只要函数名与基类一致就构成隐藏。
cpp
b.fun(1);
b.fun();

由于隐藏,这种方式调用也会报错。
倘若就要调用父类的同名方法,必须指定类域。
cpp
b.fun(1);
//b.fun();
b.A::fun();

需要注意,一般不要在基类和派生类中定义同名的变量或方法,但是特殊情况也确实避免不了,这个特殊情况马上就要提到。
接下来讲一讲派生类的四个默认成员函数。
分别是构造、拷贝构造、赋值运算符重载、析构。
先讲构造,不知道你是否观察到了,上面写的基类和派生类的例子,我都没有写构造函数,在派生类中,我们可以把成员视为三部分,内置类型、自定义类型、基类类型,针对这三者,在派生类默认生成的默认构造方法中,对于内置类型不确定,对于自定义类型调用自定义类型的构造方法,针对父类类型,自动调用父类的默认构造,但如果父类没有默认构造,就需要我们在初始化列表显式调用其构造方法。
cpp
class Animal
{
public:
Animal(const string& name = "xieajin")
:_name(name)
{
}
protected:
string _name;
};
class Dog :public Animal
{
public:
void bark()
{
cout << _name << " is " << _speak << endl;
}
protected:
int _age;
string _speak = "wolf~wolf";
};

cpp
class Animal
{
public:
Animal(const string& name)
:_name(name)
{
}
protected:
string _name;
};
class Dog :public Animal
{
public:
Dog()
:Animal("daidai")
,_age(12)
,_speak("wangwang")
{
}
void bark()
{
cout << _name << " is " << _speak << endl;
}
protected:
int _age;
string _speak = "wolf~wolf";
};

我们前边就讲过,初始化列表的初始顺序和其在类中的前后顺序相关而和初始化列表初始化的顺序无关。因此这里不论怎么写,他都是先初始化基类类型。
接下来讲一讲拷贝构造
cpp
Animal(const string& name)
:_name(name)
{
cout << "Animal(const string& name)" << endl;
}
Animal(const Animal& a)
:_name(a._name)
{
cout << "Animal(const Animal& a)" << endl;
}
cpp
Dog()
:Animal("daidai")
,_age(12)
,_speak("wangwang")
{
cout << "Dog()" << endl;
}
Dog(const Dog& d)
:Animal(d)
, _age(d._age)
,_speak(d._speak)
{
cout << "Dog(const Dog& d)" << endl;
}
cpp
Dog d;
Dog d1 = d;

可以看到,我们调用派生类的拷贝构造时会去调用基类的拷贝构造,并由子类进行一个赋值兼容转化,然后再进行子类的拷贝构造逻辑,但其实这些操作是会自动进行的,倘若我们没有需要动态开辟的成员变量或不需要深拷贝,就没有必要自己实现拷贝构造。
下面是赋值运算符重载函数
cpp
Animal& operator=(const Animal& a)
{
cout << "operator=(const Animal& a)" << endl;
if (this != &a)
{
_name = a._name;
}
return *this;
}
cpp
Dog& operator=(const Dog& d)
{
cout << "operator=(const Dog& d)" << endl;
if (this != &d)
{
_name = d._name;
_age = d._age;
_speak = d._speak;
}
return *this;
}
针对赋值运算符重载函数还是和拷贝构造一样,若无动态开辟的成员或不需要深拷贝,就不需要自己实现,按值得注意的是,基类和派生类的赋值运算符重载函数构成隐藏关系。
最后是析构函数,和拷贝构造以及赋值运算符重载一样,未涉及到空间释放就不需要自己实现。
cpp
~Animal()
{
cout << "~Animal()" << endl;
}
cpp
~Dog()
{
cout << "~Dog()" << endl;
}

在调用子类析构函数后编译器会自动调用父类的析构函数,并且子类析构函数也不需要手动调用。
值得注意的是,编译器会对析构函数进行特殊处理,在上述情况下,基类析构函数和派生类析构函数构成隐藏关系。
所以,初始化并销毁一个派生类对象调用函数的顺序是先基类构造,再子类构造,再子类析构,最后父类析构。