has-a关系
类之间除了有is-a关系,还有has-a关系,它有三种方式实现
- 包含类对象
- 私有继承
- 保护继承
包含对象成员的类
我们可以在类private字段声明一个类类型,这样两个类之间就是has-a关系,比如Student类中有名为name的string类。
这样的包含方式与共有继承不同,它是只继承实现而不继承接口。我们可以利用name对象使用string类的接口函数,但是不能直接在Student类外面使用string的接口。
私有继承
私有继承也是has-a关系,即通过私有继承string类实现Student,同样因为我们使用私有继承,外部依然不能调用string类的接口函数。它也是只继承实现而不继承接口。
因为私有继承不再有像上面的name的实例对象,无法通过对象调用string接口,所以在函数编写方面有一些改动。
在Student类中,大部分情况我们使用域解析运算符直接调用string类接口,比如说构造函数中:
Student::Student(const string& name,int n):std::string(name),score(n){}
但是假如你需要基类的对象,比如返回Student的名字,它需要返回一个string对象,这部分使用this指针的强制类型转换。比如定义各Name方法返回Student名字:
const string& Name() const{
return (const string&) *this;
}
那么友元函数呢?
友元函数无法使用域解析,因为它不是类中的函数,所以需要使用强制类型转换。
ostream& operator<<(ostream& os, const Student& stu){
os << (const string&) stu << endl;
}
在私有继承中,派生类的引用或指针无法直接赋值给基类的引用和指针,必须使用显式类型转换
什么时候使用私有继承,什么时候使用包含?大多数情况都是用包含关系,因为继承管理起来很不方便,而且容易引起多重继承的诸多问题。总之,一般使用包含,如果派生类需要使用基类的保护成员或需要重新定义虚方法,那么使用私有继承。
## 保护继承
保护继承的特点在第三代派生类中显现,使用保护继承的基类,它的共有接口在第三代中可以访问。但私有继承不可以。
多重继承
多重继承带来了两个问题:
- 从两个不同的基类继承的同名方法
- 从两个或更多的基类那里继承通过一个类的多个实例
继承来的同名方法
以一个例子讲述,假如我们定义一个虚基类worker,派生出singer类 和waiter类,然后用这两个类派生出singingwaiter类
class worker{
...
public:
virtual ~wokrer() = 0;
virtual void Show() const;
virtual void Set();
};
class waiter:public worker{
...
public:
void Set();
void Show() const;
};
class singer:public worker{
...
public:
void Set();
void Show() const;
};
class singingwaiter:private waiter,private singer{
...
public:
void Set();
//void Show() const; //Not defined
};
创建singingwaiter实例,调用Show方法,程序将产生二义性而报错,当然我们可以显式的指出用那么类中的Show方法
singingwaiter.singer::Show();
但更好的方法是在singingwaiter中定义自己Show方法,但是如果只调用singer的Show,那么waiter相关的信息就无法显示。如果同时调用waiter和singer的Show,那么基类的信息将显示两边。
这样你就不得不在各自的类中提供额外的辅助输出函数,只输出自己类中新定义的信息,而不输出基类信息,然后再singingwaiter类中分别调用。
多个实例
继续上述的距离,singingwaiter中实际存在两个worker类,这将引起问题。
singingwaiter ed;
worker* pw = &ed;
实际上,这样的赋值将派生类中的基类部分的地址赋给基类指针,但是singingwaiter中有两个worker,程序会产生二义性。当然,我们可以使用强制类型转换指出指针应指向哪一个基类。
worker* pw = (waiter*)&ed;
C++使用虚基类使得从多个基类相同的类派生出的新类只含有一个基类对象。
class waiter:virtual public worker{...
class singer:public virtual worker{...
本质上来说,singingwaiter含有一个worker类也代表着waiter和singer共用同一个worker对象。
虚基类使得派生类的编程方式有所改变。这体现在构造函数。
现在构造函数必须显示调用虚基类的构造函数
singingwaiter():worker(),waiter(),singer(){}
以前的初始化列表会使用来初始化worker的信息从两条渠道获得,为了避免这种冲突,C++直接禁止了从中间类传递信息给虚基类
如果A,B有一个虚基类,C,D公有继承基类,E类私有继承A,B,C,D。那么因为A,B共用一个,C,D各有一个,E中将有三个基类对象。
类模板
-
类模板中原型声明可以省略模板参数,但是在定义中需要将实际的模板函数定义将类型定义出来
template <class Type> class stack{ ... public: stack& operator=(const stack& st); } template <class Type> stack<Type>& stack<Type>::operator=(const stack<Type>& st){ ... }
-
模板的非类型参数:使用模板非类型参数可以在隐式实例化的时候携带额外的信心。比如下列例子中使用n来表示数组的大小
template <class T, int n> class Array{ ... }; Array<int,12> int_a;
另外,由于编译器会在定义时使用12替换n,所以在类的内部无法使用n++或&n等表达式,它应当被视为常量,所以在传入的时候也只允许传入常量表达式。
模板多功能性
-
你可以递归定义模板,比如使用stl定义二维数组
vector<vector<int>> n;
-
你可以使用多个类型参数,另外下面的例子还使用了默认类型模板参数,额外一点,你可为类模板提供默认类型参数,但是不可以为模板函数提供默认类型模板参数。
template <class T1,class T2 = int> class Pair{ ... };
模板具体化
-
隐式实例化,使用这样的形式都是隐式实例化类模板
vector<int>
-
显式实例化,和模板函数一样,使用template关键字指出程序生成某种特定类型的类模板实例。
template class vector<int>;
这种情况并没有生成vector<int>的实例。而且,声明必须位于模板定义所在的名称空间。
-
显式具体化:它是特定类型的类定义,可以为某种特定类型的方法进行修改。
template <> class vector<bool>{ ... };
这样将会为bool类型提供专门的vector类定义,程序将使用该定义生成vector<bool>对象。
-
部分具体化
template <class T1,class T2> class TT{...}; template <class T1> class TT<T1,int>{...}; //template参数只需要指出没有被具体化的参数 template <class T1> class TT<T1*, int>{...};
编译器在定义时会优先选择最靠近的版本。比如TT<char*,int>会选择第三个版本,而不是第二个。
成员模板
模板可用作结构、类或模板类的成员。比如在类模板嵌套模板类,成员函数使用模板函数等。
template <class T>
class beta{
private:
template <class V>
class hold{...}; //模板参数不要重名
hold<T> q;
public:
template<class U>
U balb(U u,T t){...}
};
上面有几个可说的点,首先注意模板与模板之间不要使用相同模板类型参数。
另一点是我们在私有成员中创建了T类型的hold类,这是使用了外部参数定义内部类模板的实例。
之后就是balb函数,注意它的第二个参数是由传入T决定的,而非和第一个参数一样是由传入的参数决定的,也就是说:
beta<int> ddb;
ddb.balb(34,12.1);
第一个参数是34是int类型,使得生成返回类型为int的返回值,即便在内部可能结果是double类型。第二个参数是double,但是他的参数类型是由上述定义指出的,应该是int,所以首先需要隐式类型转换将double转成int在进行计算。
你可以发现上面的定义全部都在类内部首先,如果你想让嵌套类和成员函数在类外面实现,则应该这样写。
template<class T>
template<class V>
class beta<T>::hold{...};
template<class T>
template<class U>
U beta<T>::balb(U u,T t){...}
因为模板是嵌套的,所以定义也要跟着嵌套,而不是template<class T,class V>这样指出。
另一方面,因为外部定义需要使用域解析符,所以显式的指出成员应所属的类类型,就是beta<T>。
模板用作参数
我们可以给模板传入一个模板。
template<template <class T> class Thing>
class Crab{...};
上述给Crab类传入的类型是template<class T> class,是一个模板类,参数为Thing。
假设我们成功使用
Crab<King> legs;
这代表着King是一个模板类
template<class T>
class King{...};
让我们扩充一下Crab
template<template<class T> class Thing>
class Crab{
private:
Thing<int> s1;
Thing<double> s2;
...
};
这样我们知道,上述传入的king模板类会在Crab类中创建两个类实例,King<int>和king<double>
额,这样做有什么用?
比如说数组类,栈类,队列类都有一个方法push,那么可以定义一个容器类模板类,根据传入的不同模板使用不容的push。
template<template<class T> class para>
class Container{
private:
para<int> _container;
public:
bool push(const int a){return _container.push(a);}
};
但是很明显,上述定义只能用于int的容器类。所以我们一般混合模板参数使用
template<template<class T> class para,class V>
class Container{
private:
para<V> _container;
public:
bool push(const V& data){return _container.push(data);}
};
模板类和友元
模板的友元分成三类:
- 非模板友元
- 约束模板友元
- 非约束模板友元
非模板友元
template<class T>
class HasFriend{
public:
friend void count();
};
上述count函数成为所有模板所有实例化的友元函数。它不是通过对象的调用的,也没有对象参数,如果你想传入某个实例化的对象,则应该是:
friend void count(HasFriend<T>&);
但是有一个问题,你必须为所有可能传入的类型参数定义函数,因为它本身不是模板函数,就是说:
void count(HasFriend<int>& a){...}
void count(HasFriend<double>& a){...}
...
约束模板友元
可以用约束模板友元解决上述问题,即将友元函数定义为模板函数,在传入参数时传入相同的类型参数。
template<class T> void report(T&);
template<class T> void count();
template<class TT>
class HasFriend{
...
friend void count<TT>();
friend void report<>(HasFriend<TT>& )
};
对于没有参数的模板友元,需要显式具体化,而report可以根据参数生成具体化。
非约束模板友元
非约束模板友元是约束模板友元的扩展,友元依旧是模板函数,但是可以和类类型不同。
template<class T>
class Many{
...
template<class C,class D> friend void show(C&,D&);
};
由于它有更强的通用性,所以和非模板友元一样,它也是所有类模板具体化的友元。