本章内容是继承,主要用于实现C++的思想之一:代码重用。
文章目录
代码重用
代码重用是一个很美丽,大家都很喜欢,也极其符合自然需求的特性。避免重复劳动这么美好的事情,谁不想做呢?可以节省开发时间,提升效率,还能让人专注于当下的重点,关注程序的整体逻辑,策略等,而不是聚焦于很多细节。并且,使用已经经过测试的使用过的代码,基本不会引入错误。
C通过 函数库
C的代码重用是通过函数库实现的。因为C只有函数,函数是他的building block,一个函数就是一个独立的功能块,当然要用函数重用代码啦,事实上,也只有这一种办法。
C的库函数,比如stdio.h, math.h, ctype.h, stdlib.h, string.h等头文件提供了很多函数原型,这些原型的函数定义的编译后的二进制代码在编译器中集成了,我们写程序可以直接使用这些库函数,比如strcpy,rand()等。
除此之外,很多厂商还提供了一些专用C库,大多和一些实际应用相关,比如硬件,嵌入式应用等。但是厂商一般也只是提供机器码,并不提供源代码。
所以C通过函数库实现代码重用的缺点是:
- 不能自己根据需要修改这些函数,只能通过修改自己的代码去迎合库函数的接口和功能。
- 就算库函数是源代码,那修改源代码也是极其危险和不妥的做法,毕竟本来正确安全的代码,修改后的安全性,可靠性等都完全是另外一回事了。
C++通过 类库 和 类继承
C重用的两个缺点在C++面前都不是事儿,因为C++的重用是通过类库和类的继承特性实现的。这两个工具帮助C++提供更高层次的重用。
类库
- 包括类声明和类实现,每个类都组合了自己的全部数据和方法。
- 大多数厂商提供的类库都是源码。但是我们不直接修改源码(修改源码这种事情在任何场合都是不能干的),而是通过继承来扩展和修改类。
继承
-
类继承允许我们从一个类(基类,base class)派生出一个新的类(派生类, derived class)。
-
派生类继承了基类的所有方法和数据。
-
可以在基类基础上添加数据成员,添加方法。比如给数组类添加求和运算,合并方法。给字符串类添加表示显示颜色的数据成员。
-
可以修改基类的方法。比如头等舱乘客类(派生类)的服务(方法)要在普通乘客类(基类)的方法上有所变化。
-
不需要复制基类代码来修改以实现上述工作,只需要提供新特性,即新的数据成员和新的方法。
如果不需要修改基类方法的话,根本不需要访问源代码。
就算厂商只提供了类接口和方法的编译后代码,我们仍然可以派生出新类。
所以也可以把自己写的类分发给别人,但是并不公开自己的实现,别人仍然可以好好地使用我的类,并继承我的类。
示例1(简单,不对基类方法做任何修改)
基类(普通玩家类)
没想到这么简单一个小程序,也检查出了我的一个掌握不好的地方:即传递引用上,不能传递指向函数中临时对象的引用
类声明和类定义
方法全部使用内联形式
//TableTennisPlayer.h
#include <string>
#ifndef TABLEPLAYER_H_
#define TABLEPLAYER_H_
class TableTennisPlayer{
private:
std::string firstname;
std::string lastname;
bool hasTable;
public:
//用初始化成员列表的构造函数
TableTennisPlayer(const std::string & fn = "none", const std::string & ln = "none", bool ht = false):firstname(fn), lastname(ln), hasTable(ht)
{}
/*
//不用初始化成员列表的构造函数
TableTennisPlayer(const std::string & fn = "none", const std::string & ln = "none", bool ht = false)
{
firstname = fn;
lastname = ln;
hasTable = ht;
}
*/
~TableTennisPlayer()
{}
bool HasTable() const {return hasTable;}
void ResetTable(bool v){hasTable = v;}
const std::string name(){return (firstname + " " + lastname);}
};
#endif
Amy Green: has a table.
Fiona Gallagar: hasn't a table.
我最开始把const std::string name(){return (firstname + " " + lastname);}的返回类型写的是引用类型,但是return后面的明显是个临时对象!于是错误,艾
主程序
//main.cpp
#include <iostream>
#include "TableTennisPlayer.h"
int main()
{
TableTennisPlayer amy("Amy", "Green", true);
TableTennisPlayer fiona = TableTennisPlayer("Fiona", "Gallagar", false);
if (amy.HasTable())
std::cout << amy.name() << ": has a table.\n";
else
std::cout << amy.name() << ": hasn't a table.\n";
if (fiona.HasTable())
std::cout << fiona.name() << ": has a table.\n";
else
std::cout << fiona.name() << ": hasn't a table.\n";
return 0;
}
主程序中有一个点,很容易被忽视,但应该知道,即TableTennisPlayer amy(“Amy”, “Green”, true);中,调用构造函数时输入的前两个参数都是const char *类型,即传统C风格字符串的类型,但是头问价的参数类型是const string &,所以这里实际上做了类型转换,即把const char
∗
*
∗转换为const string。这个类型转换实际上是通过string类的一个参数类型为const char *的构造函数实现的。
我们上次自己写string类时也是写了参数为const char
∗
*
∗类型的构造函数的哦,就是出于会遇到这种情况的考虑
派生类(参赛玩家类)公有派生
本例使用公有派生,即派生类的冒号后面紧跟public和基类名。
派生类会继承基类的所有接口和实现
派生类的对象包含了基类的对象。即基类的公有成员会成为派生类的公有成员,且基类的私有成员也成为了派生类的一部分,但是仍然只可以通过基类的公有方法或者保护方法去访问这部分。
为什么说派生类的对象包含了基类的对象?
创建派生类对象时,程序要先调用基类构造函数,以初始化基类继承来的数据成员;再调用派生类构造函数,以初始化新增的数据成员。
派生类的构造函数一定会调用一个基类构造函数。
如果不永初始化列表说明要使用的基类构造函数,就会使用基类的默认构造函数。
派生类对象过期被销毁时,程序会首先调用派生类的析构函数,再调用基类的析构函数。
派生类继承了一大笔财产之后,还需要自己做点事情
派生类可以使用基类的除私有方法以外的所有方法(公有方法,保护方法),但是自己还是要根据需求额外做点事情:
- 根据需要添加数据成员和方法成员。尤其是一定要添加自己的构造函数。
- 派生类的构造函数必须给新数据成员和旧数据成员(基类的数据成员)赋值。但是这里有个问题:派生类不能直接访问基类的私有数据成员,要怎么给他们赋值呢?
——答案是:派生类会调用基类的公有方法来访问基类的私有数据成员。在设置初始值上,就是调用基类的构造函数。所以,创建派生类对象一定要先创建基类对象。后者在进入派生类的构造函数的构造函数的函数体之前就被创建了。怎么做到呢?
——通过初始化成员列表把基类信息传递给基类构造函数。通过之前写队列模拟时介绍的初始化成员列表的方式,就可以做到在进入函数体之前做点事情。
初始化成员列表(只可以用于构造函数)
一般派生类的构造函数要使用初始化成员列表这种特殊的语法进行传参,把参数的值传递给基类构造函数。
derived是派生类类名,base是基类。x, y的值会传给接受type1, type2类型参数的基类构造函数。
类声明和类定义
//RatedPlayer.h
#ifndef RATEDPLAYER_H_
#define RATEDPLAYER_H_
#include <string>
#include "TableTennisPlayer.h"
typedef unsigned int uint;
class RatedPlayer : public TableTennisPlayer
{
private:
uint rating;//比分
public:
//习惯把新成员写在前面
RatedPlayer(uint rate = 0, const std::string & fn = "none", const std::string & ln = "none", bool ht = false):TableTennisPlayer(fn, ln, ht),rating(rate) {}
RatedPlayer(uint rate, TableTennisPlayer & ttp):TableTennisPlayer(ttp), rating(rate){}
uint Rating() const {return rating;}
void Reset_rating(uint r){rating = r;}
};
#endif
主程序
//main.cpp
#include <iostream>
#include "TableTennisPlayer.h"
#include "RatedPlayer.h"
int main()
{
TableTennisPlayer amy("Amy", "Green", true);
TableTennisPlayer fiona = TableTennisPlayer("Fiona", "Gallagar", false);
RatedPlayer mona(12, "Mona", "White", true);
RatedPlayer andy(2, amy);
if (amy.HasTable())
std::cout << amy.name() << ": has a table.\n";
else
std::cout << amy.name() << ": hasn't a table.\n";
if (fiona.HasTable())
std::cout << fiona.name() << ": has a table.\n";
else
std::cout << fiona.name() << ": hasn't a table.\n";
if (mona.HasTable())//使用基类的公有方法
std::cout << mona.name() << ": has a table. Rating: " << mona.Rating() << '\n';//使用基类的公有方法,使用自己的新增方法
else
std::cout << mona.name() << ": hasn't a table. Rating: " << mona.Rating() << '\n';//使用基类的公有方法,使用自己的新增方法
if (andy.HasTable())//使用基类的公有方法
std::cout << andy.name() << ": has a table. Rating: " << andy.Rating() << '\n';
else
std::cout << andy.name() << ": hasn't a table. Rating: " << andy.Rating() << '\n';
return 0;
}
输出
Amy Green: has a table.
Fiona Gallagar: hasn't a table.
Mona White: has a table. Rating: 12
Amy Green: has a table. Rating: 2
两个错误
-
函数名不可以和变量名相同,其实我知道这一点,之前专门写程序测试过,今天又试试,
-
派生类构造函数的初始化成员列表的参数顺序!!!!!
错误版(引发警告,但程序复杂的话也可能引发错误)
RatedPlayer(uint rate = 0, const std::string & fn = "none", const std::string & ln = "none", bool ht = false):rating(rate), TableTennisPlayer(fn, ln, ht) {}
警告中在不断提醒要reorder,重新排序,并且由于没初始化到rating,所以程序报警以为会later再初始化rating
正确版
RatedPlayer(uint rate = 0, const std::string & fn = "none", const std::string & ln = "none", bool ht = false):TableTennisPlayer(fn, ln, ht),rating(rate) {}
这纯粹是无意之间发现的,没想到参数顺序必须是先把基类的构造函数写在前面,然后才可以写派生类的新数据成员!!!否则就不会初始化派生类的新数据成员。
我想了一下 ,确实应当如此。因为前面说了,派生类初始化数据对象时要先调用基类构造函数以初始化旧成员,然后才初始化自己新增的成员。所以我如果再初始化成员列表中,把基类构造函数写在新成员后面,那么派生类构造函数看到函数头屁股后的冒号就直接调用基类构造函数,调用完基类构造函数后看构造函数后面没东西,就进入函数体了,他以为要在函数体中初始化自己的新成员,没想到函数体是空的!!它发现自己的新成员落空了,于是警告。
当基类和派生类遇上指针和引用
基类和派生类的地位并不是那么平等,感觉上,派生类还要强势一些,当然这是不科学不官方的表述啦,为什么我这么说呢,主要是因为当涉及到指针或者引用时:
- 派生类指针不可以指向基类对象
- 派生类引用不可以引用基类对象
即不能把基类对象的地址赋给派生类的引用或指针。因为若这样做,派生类指针或引用会使得基类对象可以访问派生类的成员,这就破坏了数据隐藏和封装等OOP思想,是绝对不被允许的。
- 基类指针可以在不显示类型转换下(即隐式地)就指向派生类对象
- 基类的引用也可以在不显示类型转换下就引用派生类对象
这两点正好反映了is-a关系的真相。
这样,基类的引用可以使得派生类对象调用基类的方法。
一般情况下,C++要求指针和引用的类型要和赋给的类型匹配的,但是这里可以看到,继承提供了一种单向的例外。但是基类指针和引用只能调用基类方法。
参数是基类引用的函数可用于派生类
比如
给上面的例子加一个显示名字和有无球桌的友元方法,参数是const TableTennisPlayer &
//TableTennisPlayer.cpp
#include <iostream>
#include "TableTennisPlayer.h"
void show(const TableTennisPlayer & ttp)
{
std::cout << "Name: " << ttp.name() << "\tTable: ";
if (ttp.HasTable())
std::cout << "yes\n";
else
std::cout << "no\n";
}
//和上面完全一样,只是改为了指针
void show(const TableTennisPlayer * ttp)
{
std::cout << "Name: " << ttp->name() << "\tTable: ";
if (ttp->HasTable())
std::cout << "yes\n";
else
std::cout << "no\n";
}
原型:
const std::string name() const {return (firstname + " " + lastname);}//这里必须设置为const成员函数!!!
friend void show(const TableTennisPlayer & ttp);//派生类对象也可传入
friend void show(const TableTennisPlayer * ttp);//派生类对象也可传入,重载
由于show方法需要把待显示对象作为参数从圆括号传入,所以不能定义为成员函数,因为用成员函数显示一个对象的内容不需要显式从圆括号传,直接使用隐式地this对象就行,所以这里为了使用const TableTennisPlayer &的形参,应当设计为友元函数。
测试程序
//main.cpp
#include <iostream>
#include "TableTennisPlayer.h"
#include "RatedPlayer.h"
int main()
{
TableTennisPlayer amy("Amy", "Green", true);
TableTennisPlayer fiona = TableTennisPlayer("Fiona", "Gallagar", false);
RatedPlayer mona(12, "Mona", "White", true);
RatedPlayer andy(2, amy);
show(amy);//传入基类对象
show(fiona);//传入基类对象
show(mona);//传入派生类对象
show(andy);//传入派生类对象
std::cout << '\n';
show(&amy);
show(&fiona);
show(&mona);
show(&andy);
return 0;
}
输出
Name: Amy Green Table: yes
Name: Fiona Gallagar Table: no
Name: Mona White Table: yes
Name: Amy Green Table: yes
Name: Amy Green Table: yes
Name: Fiona Gallagar Table: no
Name: Mona White Table: yes
Name: Amy Green Table: yes
写这个方法我遇到一个错误:用const对象调用非const的成员函数,程序报错如下,把const TableTennisPlayer作为this参数传入,会丢失限定符。
刚开始我一头雾水,逐渐明朗,const是限定符,错误说会丢失const,也就是在show方法调用name()函数时(std::cout << "Name: " << ttp.TableTennisPlayer::name() << "\tTable: ";),我把const传给非const啦。所以只要把name()成员方法设置为const成员函数,即表明name()方法不会修改传入的this对象,即相当于把this对象设置为const的,于是就类型匹配,const传给const,错误解除。
可以把基类对象初始化为派生类对象
要使用到基类的复制构造函数TableTennisPlayer(const TableTennisPlayer &);
//main.cpp
#include <iostream>
#include "TableTennisPlayer.h"
#include "RatedPlayer.h"
int main()
{
TableTennisPlayer amy("Amy", "Green", true);
RatedPlayer andy(2, amy);
TableTennisPlayer debbie(amy);//调用隐式复制构造函数TableTennisPlayer(const TableTennisPlayer &);
TableTennisPlayer liam(andy);//调用隐式复制构造函数TableTennisPlayer(const TableTennisPlayer &);把基类对象liam初始化为派生类对象andy
show(amy);
show(andy);
show(debbie);
show(liam);
return 0;
}
实际上,liam被初始化为Andy对象中嵌套的或者说包含的那个拥有firstname, lastname, hasTable成员的TableTennisPlayer类的对象。
Name: Amy Green Table: yes
Name: Amy Green Table: yes
Name: Amy Green Table: yes
Name: Amy Green Table: yes
把派生类对象赋给基类对象
调用隐式重载赋值运算符函数TableTennisPlayer & operator=(const TableTennisPlayer &)const;
//main.cpp
#include <iostream>
#include "TableTennisPlayer.h"
#include "RatedPlayer.h"
int main()
{
TableTennisPlayer amy("Amy", "Green", true);
RatedPlayer andy(2, amy);
TableTennisPlayer debbie;
TableTennisPlayer liam;
debbie = amy;//调用隐式重载赋值运算符函数TableTennisPlayer & operator=(const TableTennisPlayer &)const;
liam = andy;//把派生类对象给基类对象,调用隐式重载赋值运算符函数
show(amy);
show(andy);
show(debbie);
show(liam);
return 0;
}
Name: Amy Green Table: yes
Name: Amy Green Table: yes
Name: Amy Green Table: yes
Name: Amy Green Table: yes