C++面向对象的三大特性:封装,继承,多态。现在我们就介绍一下继承。
继承机制是⾯向对象程序设计使代码可以 复⽤ 的最重要的⼿段。我们前面接触到的都是 函数 层次的复用,遇到过的 类 层次的复用有模板,而继承是 类层次 的一种新的复用。 继承允许我们在 保持原有类特性的基础上进⾏扩展 ,增加⽅法(成员函数)和属性(成员变量),这样产⽣新的类,称派⽣类(或子类)。
假如现在我们模拟校园环境,设计老师(Teacher)和学生(Student)两个类。老师和学生都有姓名、电话、地址、年龄等成员变量,都有身份认证相关成员函数。
class Student //学生
{
public:
void identity() //身份认证
{
//...
}
protected:
string _name; //姓名
size_t _age; //年龄
string _add; //住址
string _tel; //电话
};
class Teacher //老师
{
public:
void identity() //身份认证
{
//...
}
protected:
string _name; //姓名
size_t _age; //年龄
string _add; //住址
string _tel; //电话
};
学生特有的变量是学号和学习相关的成员函数。
class Student //学生
{
public:
void identity() //身份认证
{
//...
}
void study() //学习
{
//...
}
protected:
string _name; //姓名
size_t _age; //年龄
string _add; //住址
string _tel; //电话
string _stuid;//学号
};
老师特有的变量是职称和教书相关的成员函数。
class Teacher //老师
{
public:
void identity() //身份认证
{
//...
}
void teaching() //教书
{
//...
}
protected:
string _name; //姓名
size_t _age; //年龄
string _add; //住址
string _tel; //电话
string _title;//职称
};
但是我们会发现这样设计的两个类重复的地方特别多,显得很冗余。
那我们把公共的信息提取出来,放在一个Same类里面,Student和Teacher这两个类复用这个类,就不用重复定义了。
class Same
{
public:
void identity() //身份认证
{
//...
}
protected:
string _name; //姓名
size_t _age; //年龄
string _add; //住址
string _tel; //电话
};
复用这个Same类,就是继承它,怎么继承?写法如下。
//学生
class Student : public Same
{
public:
void study() //学习
{
//...
}
protected:
string _stuid;//学号
};
//老师
class Teacher : public Same
{
public:
void teaching() //教书
{
//...
}
protected:
string _title;//职称
};
这就是继承的意义。接下来我们细说一下继承。
前面定义的Same类就是一个父类,也称作基类;Student类是子类,也称作派生类。
继承方式有三个:public继承、private继承、protected继承。
由上面的表我们可以观察到:
如果我们不写继承方式,使⽤关键字 class 时默认的继承⽅式是 private ,使⽤ struct 时默认的继承⽅式是 public ,不过最好显⽰的写出继承⽅式。
在实际运⽤中 ⼀般使⽤都是public继承 ,⼏乎很少使⽤protetced/private继承,也不提倡使⽤
protetced/private继承,因为protetced/private继承下来的成员都只能在派⽣类的类⾥⾯使⽤,实
际中扩展维护性不强。
之前我们用适配器模式写过栈和队列【C++】栈和队列的模拟实现(适配器模式)
这里我们还可以用 继承来实现栈和队列,以栈stack为例。
template<class T>
class stack : public std::vector<T> //继承vector
{
};
然后再在stack里面实现相关接口。
template<class T>
class stack : public std::vector<T> //继承vector
{
public:
void push(const T& x)
{
vector<T>::push_back(x);
}
void pop()
{
vector<T>::pop_back();
}
const T& top()
{
return vector<T>::back();
}
bool empty()
{
return vector<T>::empty();
}
};
(注意:基类是类模板时,需要指定⼀下类域, 否则编译报错:error C3861: “push_back”: 找不到标识符 相关的错误。)
这里stack的实现就是用了继承来实现。这个代码还可以进行改进,如下。
#define CONTAINER std::vector //宏
template<class T>
class stack : public CONTAINER<T> //继承
{
public:
void push(const T& x)
{
CONTAINER<T>::push_back(x);
}
void pop()
{
CONTAINER<T>::pop_back();
}
const T& top()
{
return CONTAINER<T>::back();
}
bool empty()
{
return CONTAINER<T>::empty();
}
};
用宏替换,就可以改变stack的底层逻辑,可以换成list,deque。
public继承 的派⽣类对象可以赋值给 基类的对象 / 基类的指针 / 基类的引⽤ 。这⾥有个形象的说法叫 切⽚ 或者 切割 。寓意把派⽣类中基类那部分切出来,基类指针或引⽤指向的是派⽣类中切出来的基类那部分。
比如说现在有 Student类的指针ptr1和 Same类指针ptr2,ptr1赋值给ptr2,就是派生类(子类)对象赋值给基类(父类),ptr2只会指向ptr1中基类有的部分。
引用同理。
Student st;//子类对象
Same sa = st; //子类对象赋值给父类对象
Same* psa = &st;//子类对象赋值给父类指针
Same& rsa = sa; //子类对象赋值给父类引用
(这里所有的赋值都不会产生临时变量,因为子类直接做了切片给父类)
父类(基类)对象不能赋值给子类(派生类)。
Student st;//子类对象
Same sa; //父类对象
sa = st;//子类赋值给父类(可以,做切片)
st = sa;//父类赋值给子类(不可以)
子类的 指针或者引⽤可以 通过强制类型转换赋值给父类的指针或者引⽤。但是必须是基类的指针
是指向派⽣类对象时才是安全的。(等以后细说)
下面的类基类和派生类有一个同名的成员变量是_name。
class Same //基类
{
protected:
string _name = "123";
size_t _age;
};
class Teacher : public Same //派生类
{
public:
void Print()
{
cout << _name << endl;
}
protected:
string _name = "456";
};
那我们在派生类中访问_name的时候到底访问的是哪个?
int main()
{
Teacher t;
t.Print();
return 0;
}
结果是显示派生类(子类)里的_name。
如果我们就想访问基类(父类)里的_name,可以直接用 基类::基类成员 显⽰访问,如下。
class Teacher : public Same
{
public:
void Print()
{
cout << Same::_name << endl; //指定作用域访问
}
protected:
string _name = "456";
};
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;
};
答案: B.隐藏 为什么不是重载?因为函数构成重载的要求是两个函数在同一作用域。而父类和子类有独立的作用域。
答案: A.编译报错
如果是下面这样调用,没有传参
int main()
{
B b;
b.fun();
return 0;
};
先看子类里的fun,子类里的fun是需要传参的,匹配不成功,会不会继续去父类里找?不会,子类把父类同名函数隐藏了,直接报错。
如果传参,像下面这样,就会调用子类里的fun函数。
int main()
{
B b;
b.fun(10);
return 0;
};
如果就是想要父类里的fun函数,直接指定定义域调用。
int main()
{
B b;
b.fun(10);
b.A::fun(); //指定
return 0;
};
我们不写,编译器默认生成的构造函数的行为:
1.对内置类型->是否初始化是不确定的。
2.对自定义类型->调用默认构造
3.继承父类成员看作一个整体对象,要求调用父类的默认构造
比如说Same类是基类,我们显示写它的默认构造,Student类为派生类,不显示写。
class Same //基类
{
public:
Same(const char* name = "peter")
: _name(name)
{
cout << "Same()" << endl;
}
protected:
string _name;
};
class Student : public Same //派生类
{
public:
//没有显示写默认构造,编译器自己生成
protected:
int _num;
string _add;
};
按照规则,_num是内置类型,可能初始化也可能不初始化;_add是自定义类型,调用string自己的初始化;继承还要调用父类的默认构造,所以_name应该被初始化为peter。
假如我们不显示写基类的默认构造,就必须在派生类显示调用。
class Same //基类
{
public:
Same(const char* name) //此时基类没有默认构造
: _name(name)
{
cout << "Same()" << endl;
}
protected:
string _name;
};
class Student : public Same
{
public:
Student(const char* string, int num, const char* add)
:Same(string) //在初始化列表阶段显示调用
, _num(num)
,_add(add)
{}
protected:
int _num;
string _add;
};
然后我们传参,就可以了。
int main()
{
Student st("张三", 0, "Chain");
return 0;
}
1.对内置类型 -> 值拷贝
2.对自定义类型 -> 调用自己的拷贝构造函数
3.对于继承成员看作一个整体对象,要求调用父类的拷贝构造
class Same //基类
{
public:
Same(const char* name = "peter") //默认构造
: _name(name)
{
cout << "Same()" << endl;
}
Same(const Same& p) //拷贝构造
: _name(p._name)
{
cout << "Same(const Same& p)" << endl;
}
protected:
string _name;
};
class Student : public Same //派生类
{
public:
Student(const char* string, int num, const char* add)
:Same(string) //在初始化列表阶段显示调用
, _num(num)
,_add(add)
{}
//拷贝构造一般不用自己写
protected:
int _num;
string _add;
};
一般情况下,派生类(子类)默认生成的拷贝构造就够用了,不用自己写,如果有需要深拷贝的资源,才需要自己写。
int main()
{
Student st1("张三", 0, "Chain");
Student st2(st1);
return 0;
}
_num是内置类型,进行值拷贝;_add是自定义类型string,调用string自己的拷贝构造;_name是父类继承成员,调用父类Same的拷贝构造。
如果我们要在派生类(子类)中显示地写拷贝构造,写法如下。
Student(const Student& s) //显示地写子类的拷贝构造
:Same(s)//父类的拷贝构造
,_add(s._add)
,_num(s._num)
{
//假设里面是深拷贝资源的拷贝逻辑
}
Same是父类,Same后面的括号里应该传父类的对象,但是我们没有父类的对象,只有子类的对象s,为什么可以直接传s过去?
这里用到的就是前面说过的 子类和父类对象赋值兼容转换 。我们要拷贝父类的那一部分,就要把父类的那一部分拿出来,我们把子类对象s传给父类Same,Same的拷贝构造函数是引用传参。
这里引用的就是子类对象中切出来的父类的那一部分。
我们先实现一个基类(父类)的赋值重载。
class Same //基类
{
public:
Same(const char* name = "peter") //默认构造
: _name(name)
{
cout << "Same()" << endl;
}
Same(const Same& p) //拷贝构造
: _name(p._name)
{
cout << "Same(const Same& p)" << endl;
}
Same& operator=(const Same& p) //赋值重载
{
cout << "Same& operator=(const Same& p)" << endl;
if (this != &p)
_name = p._name;
return *this;
}
protected:
string _name;
};
class Student : public Same //派生类
{
public:
Student(const char* string, int num, const char* add)
:Same(string) //在初始化列表阶段显示调用
, _num(num)
,_add(add)
{}
//没有实现operator=
protected:
int _num;
string _add;
};
operator=和拷贝构造差不多,我们不在派生类(子类)中显示地写赋值重载时,编译器自动生成的就够用。
int main()
{
Student st1("张三", 0, "Chain");
Student st2("李四", 1, "LA");
st1 = st2;
return 0;
}
也是内置类型值拷贝,自定义类型调用自己的operator=,继承父类成员看作整体,调用父类的operator=。
如果有需要深拷贝的资源,才需要自己实现。自己实现的话,写法如下。
Student& operator=(const Student& s)
{
if (this != &s)
{
Same::operator=(s);//显示调用基类的赋值重载
_num = s._num;
_add = s._add;
//深拷贝逻辑
}
return *this;
}
显示调用父类(基类)的operator=时,要指定类域,因为同名函数子类的会把父类的隐藏,屏蔽基类对同名成员的直接访问,如果不指定类域,会造成栈溢出。
class Same //基类
{
public:
Same(const char* name = "peter") //默认构造
: _name(name)
{
cout << "Same()" << endl;
}
Same(const Same& p) //拷贝构造
: _name(p._name)
{
cout << "Same(const Same& p)" << endl;
}
Same& operator=(const Same& p) //赋值重载
{
cout << "Same& operator=(const Same& p)" << endl;
if (this != &p)
_name = p._name;
return *this;
}
~Same() //析构
{
cout << "~Same()" << endl;
}
protected:
string _name;
};
派生类(子类)默认生成的析构函数就够了,如果有需要资源释放的时候才需要自己实现。
所以我们如果需要在子类(派生类)自己写析构函数时,不可以像下面这样。
~Student()
{
~Same(); //错误的写法
//资源释放逻辑...
}
既然构成隐藏,调用就需要指定类域调用。
~Student()
{
Same::~Same(); //正确的写法
//资源释放逻辑...
}
但是我们会发现,下面的代码明明只有两个对象,却调用了4次析构函数。
而调用析构次数太多会出问题。
~Student()
{
//资源释放逻辑...
//自动调用父类析构
}
⽅法1:将基类的构造函数 私有 ,派⽣类的构成必须调⽤基类的构造函数,但是基类的构成函数私有化以后,派⽣类看不⻅就不能调⽤了,那么派⽣类就⽆法实例化出对象。
⽅法2:C++11新增了⼀个 final 关键字,final修改基类,派⽣类就不能继承了。
class Same final //基类加final后不可被继承
{
public:
//成员函数
protected:
string _name;
};
本次分享见到这里,我们下篇见~