C++ 学习笔记——八、代码重用

文章探讨了C++中实现has-a关系的两种方式:包含对象成员和私有继承。通过示例展示了如何使用这两种方法,并分析了它们的优缺点。私有继承可以使基类的成员变为派生类的私有成员,而包含则保持对象之间的独立性。文章还讨论了多重继承带来的问题,如菱形问题,以及如何通过虚基类来解决。最后,介绍了类模板的概念,以及如何处理模板类的类型参数和非类型参数,包括模板的隐式实例化、显式实例化和具体化。
摘要由CSDN通过智能技术生成

目录:点我

一、包含对象成员的类

由于公有继承是 is - a 的关系,即班长首先是一个学生,因此班长类可以从学生类中继承而来。但很多情况下需要 has - a 的关系,即学生有姓名和成绩组成,而姓名和成绩可以是两个不同的类。此时可以让学生类中包含姓名类和成绩类的对象来实现,这种方式叫做包含:

class Student {
private:
	string name;  // 姓名类的对象
	valarray<double> scores;  // 成绩类的对象
};

由于声明为私有成员,此时对于姓名和成绩的管理只能通过成员函数利用姓名类和成绩类的接口来实现,注意,此时学生类并没有获得姓名类和成绩类的接口,而只是通过对象调用他们。因此对于 has - a 关系来说,类兑现不能自动获得被包含对象的接口。从实现上来说,两个姓名类之间进行加减拼接操作是没有意义的,因此也不应该继承接口。

但是使用姓名来排序是一件有意义的事情,因此没有继承接口的情况下就需要编写这个成员函数,在函数中通过姓名对象调用它的接口来实现。

由于构造函数用于初始化对象,如果类中存在对象成员,那么是否使用初始化列表就有了区别:

Student(const char * str, const double * pd, int n) : name(str), scores(pd, n) {}
  • 使用初始化列表:对于 namescores 来说将分别使用 String(const char *)ArrayDb(const double *, int) 构造函数来初始化;
  • 不使用初始化列表:对于 namescores 来说将先使用默认的构造函数来生成对象,然后在构造函数中根据相应的代码对其进行赋值。

二、私有继承

1. 初始化基类组件

除了上一种实现 has - a 关系的方法,还可以采用私有继承来实现,这样基类中的公有成员和保护成员都将变为派生类的私有成员,这意味着基类方法将不会成为派生类对象公有接口的一部分,但是派生类的成员函数却可以使用它们:

class Student : private String, private valarray<double> {
public:
	...
};

使用多个基类的继承被称为多重继承(multiple inheritance, MI),对于这个继承类,因为提供了两个无名称的子对象成员,所以需要采用新的构造函数:

Student(const char * str, const double * pd, int n) : String(str), ArrayDb(pd, n) {}

类定义与上一个版本的区别在于省略了显式对象名称,并在内联构造函数中使用了类名,而不是成员名:

#include <iostream>
#include <valarray>
#include <string>
using namespace std;
class Student : private String, private valarray<double> {
private:
	typedef valarray<double> ArrayDb;
	ostream & arr_out(ostream & os) const;
public:
	Student() : string("Null Student"), ArrayDb() {}
	explicit Student(const char * s) : String(s), ArrayDb() {}
	explicit Student(int n) : String("Nully"), ArrayDb(n) {}
	Student(const char * str, int n) : String(str), ArrayDb(n) {}
	Student(const char * str, const ArrayDb & a) : String(str), ArrayDb(a) {}
	Student(const char * str, const double * pd, int n) : String(str), ArrayDb(pd, n) {}
	~Student() {}
	double Average() const;
	double & operator[](int i);
	double operator[](int i) const;
	const string & Name() const;
	// friends
	friend istream & operator>>(istream & is, Student & stu);
	friend istream & getline(istream & is, Student & stu);
	friend ostream & operator<<(istream & os, const Student & stu);
};

2. 访问基类的方法

如果用上一个版本写一个求均值的方法:

double Student::Average() const {
	if(scores.size() > 0)
		return scores.sum() / scores.size();
	return 0;
}

但是私有继承版本的实现如下,即通过在派生类方法中用作用域解析符号来实现:

double Student::Average() const {
	if(ArrayDb::size() > 0)
		return ArrayDb::sum() / ArrayDb::size();
	return 0;
}

3. 访问基类对象

若要访问基类对象(姓名 string),由于基类对象没有名称,因此需要通过强制类型转换来将 Student 对象转换为 string 对象:

const string & Student::Name() const {
	return (const string &) *this;
}

4. 访问基类的友元函数

用类名显式地限定函数名不适合友元函数,这是因为友元不属于类。然而,可以通过显示地转换为基类来调用正确的函数:

cout << stu;  // 通过派生类友元函数输出
os << "Scores for" << (const string &) stu << ":\n";  // 显式地将stu转换为string对象引用,进而调用operator<<(ostream &, const string &)

引用 stu 不会自动转换为 string 对象引用,这是因为在私有继承中,在不显式类型转换的情况下,不能将指向派生类的引用或指针赋给基类引用或指针;第二个原因则是这个类使用多重继承,编译器无法确定应转换成哪个基类。

不过在这个例子中,即便使用公有继承,第一条语句仍然会与派生类的友元函数匹配,从而导致递归调用。

5. 选择包含还是私有继承

  • 包含:首先包含易于理解,而使用继承将使关系更抽象;其次继承会引起很多问题,特别是多重继承。
  • 私有继承:特性更多,访问控制更全面,可以重新定义虚函数。
  • 结论:通常采用包含;如果新类需要访问原有类的保护成员或需要重新定义虚函数则采用私有继承。

6. 保护继承

保护继承是私有继承的变体,基类的公有成员和保护成员都将称为派生类的保护成员。二者的主要区别在于派生类派生出另一个类时:

  • 私有继承:第三代类将不能使用基类的接口,因为基类的公有方法在派生类中变为了私有方法;
  • 保护继承:基类的公有方法在子子孙孙的保护继承中仍可用。

假设派生类想要使用基类的私有方法,除了可以在成员方法中使用作用域解析符,还可以使用 using 关键字进行声明,这将使(const 和非 const 版本均可用):

class Student : private String, private valarray<double> {
public:
	using valarray<double>::min;
	using valarray<double>::max;
	...
};

该语句只适用于继承,而不适用于包含。

三、多重继承

多重继承即子类同时继承多个父类的特性,举个栗子:

class className : public Dad1, public Dad2 {...};

多重继承看起来很美好,但也有它自己的新问题,举个栗子:

class Grandpa {
private:
	string name;
};
class Dad : public Grandpa {
	...
};
class Mom : public Grandpa {
	...
};
class Son : public Dad, public Mom {
	...
};

上述代码中儿砸继承了它的父亲和母亲,父亲和母亲继承了祖父。那么问题来了,儿砸到底有几个名字?从理论上来看,同时继承父亲和母亲的私有成员变量,那么就是两个名字,但这与显示世界的实际情况不符。

为了解决这个问题,C++引入了一种新技术——虚基类。

虚基类使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象,举个栗子:

class Dad : virtual public Grandpa { // virtual的先后顺序不影响作用效果
	...
};
class Mom : public virtual Grandpa {
	...
};
class Son : public Dad, public Mom {
	...
};

此时儿砸就只有一个名字了!

使用虚基类会对构造函数的初始化列表产生影响:

Son(const Grandpa & grandpa, int a = 0, int b = 1) : Dad(grandpa, a), Mom(grandpa, b) {}

由于 grandpa 会从 Dad 和 Mom 两条路径上进行传递,因此出现了冲突,为避免这种冲突,虚基类会禁止中间类传递信息给基类,此时只有 a 和 b 两个变量会被传递并用于初始化。但是要想创建派生类,首先必须先创建基类,因此虚基类会调用默认的构造函数来对 grandpa 进行初始化。要想选择特定的构造函数对 grandpa 进行初始化,需要显式的指定:

Son(const Grandpa & grandpa, int a = 0, int b = 1) : Grandpa(grandpa), Dad(grandpa, a), Mom(grandpa, b) {}

注意,虚基类必须这样做,但是对于非虚基类则是非法的。

再来看另一种情况,加入 son 调用了一个自己没有重新定义的函数,而此时 dad 和 mom 中均有该函数的定义,此时调用是二义性的(单继承会调用最近祖先的同名方法)。为解决这个问题可以使用作用域解析运算符来表示:

Son son;
son.show();  // 二义性
son.Dad::show();  // allow

当然,更好的办法是在类中重新定义 show 方法:

void Son::show() {
	Dad::show();
}

四、类模板

像 Stack 这样的类来说,内部变量类型可能是多样化的,而为此定义多个相同功能的类显然过于冗余,因此可以使用类模板:

template <class Type>  // 早期版本,因为class易混淆,所以新版本换为了typaname
template <typaname Type>  // 新版本
class Stack {
	...
	Item items[Max];  // 老版本,Item表示数据类型,即typedef unsigned long Item;
	Type items[Max];  // 模板化
	...
};

bool Stack::push(const Item & item) {...}  // 老版本
bool Stack<Type>::push(const Type & item) {...}  // 模板化

Stack<int> kernels;  // 使用方法

Type 为泛型标识符,被称为类型参数(type parameter),这意味着它类似于变量,但赋给它们的不能是数字,而只能是类型,因此最后一句的参数 Type 的值为 int 。

现在考虑一个问题,是否可以在模板类中使用指针类型?答案是可以的,但是如果不对程序做重大修改,将无法很好的工作。

对于模板还可以加入一些非类型参数(或表达式):

template <class T, int n> 
ArrayTP<double, 12> eggweights;

n 称为非类型(non-type)或表达式(expression)参数,如第二句所示,编译器定义了一个名为 ArrayTP<double, 12> 的类,并创建一个 eggweights 对象,定义类的过程中会使用 double 替换 T ,使用 12 替换 n 。

表达式参数有一些限制,它可以是整型、枚举、引用或指针,而浮点类型是非法,但是浮点指针是合法的。另外模板代码中不能修改参数的值,也不能使用参数的地址,也就是说 ArrayTP 模板中不能使用 n++&n 这样的表达式。实例化模板时,用作表达式参数的值必须是常量表达式。

模板中还可以使用模板进行嵌套,例如:

Array<Stack<int> > asi;  // 一个栈类型的数组

值得注意的是,在 C++ 98 中,要求两个 > 符号之间添加空格,否则会与运算符 >> 混淆。

部分模板希望能够使用多个类型参数:

map<int, double> mp;  // 使用了两个类型
template<class T1, class T2>  // 可以采用该定义实现

也可以设定默认的模板参数:

template<class T = int>  // 默认为int

类模板的具体化与函数模板相似,分为隐式实例化、显式实例化以及显式具体化:

ArrayTP<int, 100> stuff;  // 隐式实例化
ArrayTP<double, 30> * pt;  // 生成对象之前不会生成类的隐式实例化
pt = new ArrayTP<double, 30>;  // 编译器生成类定义,并根据定义创建一个对象

template class ArrayTP<string, 100>;  // 显式实例化,即使未创建对象也进行类声明

template <> class SortedArray<const char *> { ... };  // 显式具体化,具体的指出const char *类型的版本

template <class T1> class Pair<T1, int> { ... };  // 部分具体化,即Pair类第一个参数为模板,后一个参数指定为int,若所有参数均被指定具体类型则为显式具体化

// 下面这种情况表示定义多个版本,前者是通用版本,后者是通用指针版本,编译器会根据需要自动选择
template<class T>
class Feeb {...};
template<class T*>
class Feeb {...};

模板除了可用作类、结构、模板类,还可用作它们的成员。

模板类也可以有友元,分为 3 类:

  • 非模板友元:
    template<class T>
    class HasFriend {
    public:
    	friend void counts();  // 普通友元
    	friend void report(HasFriend &);  // 不可行,因为没给出具体的类型
    	friend void report(HasFriend<T> &);  // 可行,因为给出了具体的类型
    };
    
  • 约束模板友元:
    // 首先定义模板函数
    template<typename T> void counts();
    template<typename T> void report(T &);
    
    // 然后声明友元
    template<class TT>
    class HasFriendT {
    public:
    	friend void counts<TT>();
    	friend void report<>(HasFriend<TT> &);  // 此处为空表示从函数参数中推断
    	friend void report<HasFriendT<TT>>(HasFriend<TT> &);  // 也可以显式的指定
    };
    
    // 最后,由于为友元提供了模板定义,因此也可以根据参数自动推断
    
  • 非约束模板友元:
    template<class T>
    class ManyFriend {
    public:
    	template <typename C, typename D> friend void show(C &, D &);  // 函数可根据参数自动推断
    };
    

还可以使用 typedef 为模板添加别名:

typedef array<int, 12> arri;  // 别名
arri days;  // 使用别名

C++ 11 还提供了新的方法:

template<typename T>  // 定义类型名
using arrtype = array<T, 12>;  // 定义别名
arrtype<int> days;  // 使用别名

using pc = const char *;  // 允许using用于非模板,此时与typedef等价
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BeZer0

打赏一杯奶茶支持一下作者吧~~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值