C++ day25 代码重用(一) 包含,私有继承,保护继承
C++的各种先进理念中包含了一条:代码重用。上一章学习的公有继承是实现代码重用理念的方式之一,但是还有多种机制可以实现这个理念:
- 私有继承
- 保护继承
- 包含/组合/层次化(containment/composition/layering):把一个类的对象作为另一个类的成员
- 多重继承multiple inheritance,MI:可以是多重私有继承,也可以是多重公有继承(容易出问题)
类模板
前面三个都用于实现has文章目录
- 包含
- 示例 学生类
- 在编译阶段出现错误优于在运行阶段出现错误
- 公有继承中,类可以继承接口;包含不可以继承接口
- 代码
- 私有继承:基类的公有成员和保护成员都成为派生类的私有成员
- 私有继承 VS 包含
- 示例 重新设计Student类
- 私有继承怎么访问基类方法和对象
- 代码
- 保护继承(私有继承的变体)
- 保护继承 VS 私有继承
- 公有继承 VS 保护继承 VS 私有继承
- 重新定义保护派生和私有派生的访问权限(即希望让派生类对象可以在类外部调用基类方法)
- 把函数调用包装在另一个函数调用中
- 像名称空间一样,使用using声明指出派生类可以使用基类的特定成员
包含
示例 学生类
我们用最后一个定义来表示学生,因为这样才适合计算机表示。即把学生简化和抽象为一个姓名和一组分数。
姓名怎么表示?可以用C语言的字符数组,或者用字符指针char*和动态内存分配,或者直接用string类。当然最简单的选择是string类啦,因为字符数组的长度很难确定,char指针需要多写一些代码,而string类是C++库提供的,库里有所有的实现代码,而且都是很好的实现,经过了严格测试。
分数也可以用一个定长的double数组,或者使用new的动态数组,或者C++提供的valarray类。同样的原因,当然是选择valarray类。
valarray类:用于处理数值的模板类
这是个模板类,即类型不确定,由实现决定,也就可以处理各种不同的数据类型。他支持一些算术运算:把数组所有元素相加以及在数组中找到最大值和最小值等操作。
#include <valarray>
#include <iostream>
int mian()
{
using std::cout;
using std::valarray;
valarray<int> q_values; //int array,长度为0的空数组
valarray<double> weights; //double array
double gpa[5] = { 2.1, 5.2, 4.1, 6.3, 8.4};
valarray<double> v1;//size 0
valarray<int> v2(8); //指定长度的空数组,size 8,可放8个int元素,用的是圆括号哦
valarray<int> v3(10, 8); //size是8,所有元素被初始化为指定值10,第一个参数是初始值,第二个参数是size
valarray<double> v4(gpa, 4);//size是4,把gpa数组的前四个元素的值作为初始值,用常规数组的值去初始化
valarray<int> v5 = { 12, 45, 78, 96};//使用初始化列表
cout << v3[0](0);
return 0;
}
由于学生拥有名字和分数,所以Student类和string类,valarray类都是has-a关系,而不是is-a关系,不能用公有继承,要用包含。
在编译阶段出现错误优于在运行阶段出现错误
所以C++要用explicit,const等关键字去做一些限制。
公有继承中,类可以继承接口;包含不可以继承接口
公有继承一般是处理is-a关系,派生类可以继承基类的接口(即基类的纯虚函数)和实现,获得接口是is-a关系的一部分,因为既然是一种,那么就要有一样的接口和实现。
但是包含关系中,has-a关系,类并不能获得自己包含的对象所属的类的接口,只能在方法代码中调用一下对象所属类的方法,实现代码重用,这在下面的代码中有体现。
对于has-a关系,类对象不能获得被包含对象的接口是合理的,还是件好事。比如Student类包含了string类的对象,但Student类并不需要获得string类的各种方法和接口啊,比如把两个string对象串接起来,就不适用于Student类嘛。
代码
//Student.h
#ifndef STUDENT_H_
#define STUDENT_H_
#include <iostream>
#include <string>
#include <valarray>
class Student{
private:
typedef std::valarray<double> arrayDb;//typedef放在类的私有部分则只有Student类的实现中可以使用arrayDb,Student类外不可以
std::string name;
arrayDb scores;//没指定size
std::ostream & arr_out(std::ostream & os) const;
public:
Student()
:name("Null Null"),scores(){ }//分数可以留着括号
//由于单参数的构造函数可以用作从参数类型到类类型的隐式类型转换函数,但是把int转换为Student类毫无意义,所以要用explicit关闭隐式转换
explicit Student(const std::string & s)
:name(s), scores(){ }//只传入一个参数(名字),避免隐式自动类型准换
explicit Student(int n):name("Null"), scores(n){ }//只传入一个参数(分数数组的元素个数),避免隐式自动类型准换
Student(const std::string & s, int n)
:name(s), scores(n){ }
Student(const std::string & s, const arrayDb & a)
:name(s), scores(a){ }
Student(const char * str, const double * pd, int n)
:name(str),scores(pd, n){ }
~Student(){ }
double Average() const;
const std::string & Name() const;
double & operator[](int i);
double operator[](int i) const;
//friends
//输入
friend std::istream & operator>>(std::istream & is, Student & stu);// 1 word
friend std::istream & getline(std::istream & is, Student & stu);//1 line
//输出
friend std::ostream & operator<<(std::ostream & os, const Student & stu);
};
#endif
//Student.cpp
#include <iostream>
#include "Student.h"
//私有方法(当做辅助函数来用)
//输出分数,通过把输出分数的凌乱细节包装在一个私有函数里,可以使得友元函数代码更简洁整洁,也隐藏了实现
std::ostream & Student::arr_out(std::ostream & os) const
{
int i;
int lim = scores.size();//虽然被包含对象的接口不是公有的,但是在类方法中可以使用他们
if (lim > 0)
{
for (i = 0; i < lim; ++i)
{
os << scores[i] << ' ';
if (i % 5 == 4)//一行5个数
os << '\n';
}
if (i % 5 != 0)
os << std::endl;
}
else
os << "empty array!\n";
return os;
}
double Student::Average() const
{
if (scores.size() > 0)
return scores.sum()/scores.size();
else
return 0;
}
const std::string & Student::Name() const
{
return name;
}
double & Student::operator[](int i)
{
//按引用返回
return scores[i];//这里用的是valarray<double>::operator[]()函数
}
double Student::operator[](int i) const
{
return scores[i];//按值返回
}
//输入
std::istream & operator>>(std::istream & is, Student & stu)// 1 word
{
is >> stu.name;
return is;
}
std::istream & getline(std::istream & is, Student & stu)//1 line
{
getline(is, stu.name);//调用string类的友元函数getline(istream &, const string &)
return is;
}
//输出
std::ostream & operator<<(std::ostream & os, const Student & stu)
{
os << "Scores for " << stu.name << ": "; //友元函数可以访问name成员, stu.name是string类对象,所以会调用string类的operator<<(ostream &, const string &)
return stu.arr_out(os);
}
主程序
#include <valarray>
#include <iostream>
#include "Student.h"
using std::cout;
void set(Student & sa, int n);
int main()
{
using std::valarray;
const int pupils = 3;
const int quizzes = 5;
Student ada[pupils] = { Student(quizzes), Student(quizzes), Student(quizzes)};
for (int i = 0; i < pupils; ++i)
set(ada[i], quizzes);
cout << "Student list:\n";
for (int i = 0; i < pupils; ++i)
cout << ada[i] << std::endl;
return 0;
}
void set(Student & sa, int n)
{
using std::cin;
cout << "Enter the student's name: ";
getline(cin, sa);//Student类的友元函数
cout << "Enter " << n << " quiz scores:\n";
for (int i = 0; i < n; ++i)
cin >> sa[i];//??把对象名当做数组名用?
//清空输入队列
while (cin.get() != '\n')
;
}
输出
Enter the student's name: Gil Bayts Enter 5 quiz scores: 78 56 79 85 69 Enter the student's name: Pat Roone
Enter 5 quiz scores:
98.5 45.6 86.2 96.3
52.3
Enter the student's name: Fleur Oday
Enter 5 quiz scores:
78.1 45.2 85 69 36.5 78.6 21
Student list:
Scores for Gil Bayts: 78 56 79 85 69
Scores for Pat Roone: 98.5 45.6 86.2 96.3 52.3
Scores for Fleur Oday: 78.1 45.2 85 69 36.5
私有继承:基类的公有成员和保护成员都成为派生类的私有成员
即基类的公有方法和保护方法都成了派生类的私有方法。
所以基类的方法并不会成为派生类对象的公有接口的一部分,但是可以在派生类的成员函数中使用他们。
因此私有继承也(包含不继承接口)不会继承基类的接口。
私有继承 VS 包含
- 相同:都只获得实现,不获得接口
- 不同:
- 对象是否命名,包含把一个命名了的子对象(成员对象)添加在类中;私有继承把对象作为一个没有命名的继承对象添加在类中,即只继承组件,而不是创建成员对象。
由于私有继承没有显式命名对象,所以代码会有不同,比如内联构造函数中的初始化成员列表使用的是类名,而包含使用的对象名。
- 怎么选择:
包含和私有继承都可以实现has-a关系,但是更倾向于使用包含实现has-a关系。因为:
- 怎么选择:
包含优点 :易于理解,可以用显式命名被包含类(这里不叫基类)的对象,代码好写。
私有继承缺点:可能会很麻烦:从多个基类进行多重私有继承时,也许多个基类有同名函数,或者基类之间共享了一个祖先基类。另外,私有继承最多只可以用到一个基类对象(因为对象没有名称难以区分),但是包含可以创建多个基类对象。
私有继承的优点:提供的特性比包含多。
比如,包含类和被包含类不是继承关系,所以包含类不在继承链条中,所以包含类不可以访问被包含类的保护成员(函数或数据),但是继承链条中的派生类就可以访问保护成员。
- 必须使用私有继承而不是包含的情况:
- 需要访问原类的保护成员
- 新类需要重新定义虚函数,因为只有派生类可以重新定义虚函数,包含类不可以。
示例 重新设计Student类
改变并不大,只是以前包含中使用了对象名的地方需要改
私有继承怎么访问基类方法和对象
访问基类的公有方法:用类名和作用域解析运算符(包含使用对象名调用基类的公有方法)
double Student::Average() const
{if (ArrayDb::size() > 0)
return ArrayDb::sum() / ArrayDb::size();//用类名和作用域解析运算符调用基类的方法
else
return 0;
}
访问基类对象:强制类型转换
Student类是string类派生的,所以可以强制类型转换为string类对象,即继承来的string类对象const std::string & Student::Name() const
{return (const std::string &) *this;//通过强制类型转换创建一个引用指向继承来的基类对象
//(把Student对象强制转换为基类string类对象),以访问基类对象
}
访问基类的友元函数
答案是:仍然是强制类型转换。由于友元函数并不属于类,所以肯定不能用类名和作用域解析运算符。
私有继承中,如果不显示类型转换,则不可以把派生类的指针或者引用赋给基类的指针或引用。
std::istream & operator>>(std::istream & is, Student & stu)
{
is >> (std::string &)stu;//强制类型转换得到基类对象以调用基类的友元函数operator<<(ostream &, const string &)
return is;
}
代码
//student.h
#ifndef STUDENT_H_
#define STUDENT_H_
#include <iostream>
#include <valarray>
#include <string>
//私有继承
class Student : private std::string, private std::valarray<double>
{
private:
typedef std::valarray<double> ArrayDb;
std::ostream & arr_out(std::ostream & os) const;
public:
//0个参数,即只声明
Student():std::string("Null Null"), ArrayDb(){ }//括号空着
//单个参数
explicit Student(const std::string & s):std::string(s), ArrayDb(){ }
explicit Student(int n):std::string("Null Null"), ArrayDb(n){ }
//两个参数,只传入分数数组的元素个数
Student(const std::string & s, int n):std::string(s), ArrayDb(n){ }
//两个参数,传入分数数组的引用
Student(const std::string & s, const ArrayDb & ar):std::string(s), ArrayDb(ar){ }
~Student(){ }
double Average() const;
double & operator[](int i);
double operator[](int i) const;
const std::string & Name() const;
//friends
//输入
friend std::istream & operator>>(std::istream & is, Student & stu);//输入一个词语
friend std::istream & getline(std::istream & is, Student & stu);//输入一行
//输出
friend std::ostream & operator<<(std::ostream & os, const Student & stu);
};
#endif
//student.cpp
#include <iostream>
#include <string>
#include "studenti.h"
//public
double Student::Average() const
{
if (ArrayDb::size() > 0)
return ArrayDb::sum() / ArrayDb::size();//用类名和作用域解析运算符调用基类的方法
else
return 0;
}
const std::string & Student::Name() const
{
return (const std::string &) *this;//通过强制类型转换创建一个引用(把Student对象强制转换为基类string类对象),以访问基类对象
}
double & Student::operator[](int i)
{
return ArrayDb::operator[](i);//调用基类方法
}
double Student::operator[](int i) const
{
return ArrayDb::operator[](i);
}
//private
std::ostream & Student::arr_out(std::ostream & os) const
{
int i;
int lim = ArrayDb::size();
if (lim > 0)
{
for (i = 0; i < lim; ++i)
{
os << ArrayDb::operator[](i) << ' ';
if (i % 5 == 4)//一行5个数
os << '\n';
}
if (i % 5 != 0)
os << std::endl;
}
else
os << "empty array!\n";
return os;
}
//friends
//输入
std::istream & operator>>(std::istream & is, Student & stu)
{
is >> (std::string &)stu;//强制类型转换得到基类对象以调用基类的友元函数operator<<(ostream &, const string &)
return is;
}
std::istream & getline(std::istream & is, Student & stu)
{
getline(is, (std::string &)stu);//由于参数类型,会调用string类的getline函数
return is;
}
//输出
std::ostream & operator<<(std::ostream & os, const Student & stu)
{
os << "Scores for " << (const std::string &)stu << ":\n";//把Student对象转换为string对象,从而使用了string类的友元函数operator<<(std::ostream &, const std::string &)
//os << stu;//千万别这么写!必须使用显式强制转换,否则一是会导致无限递归调用;
//二是因为这里是多重继承,两个基类都提供了operator<<函数,编译器不知道该自动转换为哪一个的
stu.arr_out(os);
return os;
}
#include <valarray>
#include <iostream>
#include "Studenti.h"
using std::cout;
void set(Student & sa, int n);
int main()
{
using std::valarray;
const int pupils = 3;
const int quizzes = 5;
Student ada[pupils] = { Student(quizzes), Student(quizzes), Student(quizzes)};
for (int i = 0; i < pupils; ++i)
set(ada[i], quizzes);
cout << "Student list:\n";
for (int i = 0; i < pupils; ++i)
cout << ada[i] << std::endl;
return 0;
}
void set(Student & sa, int n)
{
using std::cin;
cout << "Enter the student's name: ";
getline(cin, sa);//Student类的友元函数
cout << "Enter " << n << " quiz scores:\n";
for (int i = 0; i < n; ++i)
cin >> sa[i];//??把对象名当做数组名用?
//清空输入队列
while (cin.get() != '\n')
;
}
输出
Enter the student's name: aa bb Enter 5 quiz scores: 45 45 45 52 66 Enter the student's name: cc dd
Enter 5 quiz scores:
78 56 12 46 53 98
Enter the student's name: ee ff
Enter 5 quiz scores:
78 45 56 23 12
Student list:
Scores for aa bb:
45 45 45 52 66
Scores for cc dd:
78 56 12 46 53
Scores for ee ff:
78 45 56 23 12
保护继承(私有继承的变体)
基类的公有成员和保护成员都成为派生类的保护成员。
保护继承 VS 私有继承
- 相同:
基类(第一代类)的接口在派生类(第二代类)都可用。
- 不同:
当从派生类再派生一个类时:
私有继承中,第三代类不可以用基类(第一代类)的接口,因为基类的公有方法在派生类(第二代类)已经成为了私有方法。
保护继承中,第三代类还可以使用第一代类的接口。因为第一代类的公有方法(接口)在第二代类中成为了保护方法。
公有继承 VS 保护继承 VS 私有继承
隐式向上转换implicit upcasting:不需要显式强制类型转换就可以把基类指针或引用指向派生类对象。
重新定义保护派生和私有派生的访问权限(即希望让派生类对象可以在类外部调用基类方法)
由于私有派生和保护派生会使基类的公有方法在派生类成了私有方法或保护方法,在类外部,即外部程序中,不能通过派生类对象调用这些方法(类对象在类外部只能调用公有方法)。那怎么使得这样做变成现实呢?有这么几个办法:
把函数调用包装在另一个函数调用中
即在派生类声明一个同名方法,在实现代码中直接调用基类的该方法
比如,如果想让Student类使用valarray类的sum()方法,则可以给Student类定义一个sum()
double Student::sum() const//Student类的公有方法
{
return std::valarray<double>::sum();//调用私有继承得来的私有方法或者保护继承得来的保护方法
//但是注意这个方法并不是作用于this对象,而是作用于this对象内部包含的valarray对象
}
可以看到,虽然std::sum()是私有继承得来的私有方法或者保护继承得来的保护方法,但是还是必须用类名限定才就可以访问哦
像名称空间一样,使用using声明指出派生类可以使用基类的特定成员
这种using指令只可以用于继承,不可以用于包含
即使是私有派生,也是可以把通过using指令声明的函数像公有接口那样来使用的
注意using声明写在派生类的公有部分
class Student:private std::string, private std::valarray<double>
{
public:
using std::valarray<double>::max;//只写成员函数名,不写圆括号,特征标以及返回类型
using std::valarray<double>::min;
}
如果基类有两个同名函数,那么using声明会使得这两个函数都可以像派生类的公有方法一样被使用
还没有评论,来说两句吧...