C++ Primer Plus读书笔记
代码重用
- C++的一个主要目标是促进代码重用,公有继承是实现这种目标的机制之一,但不是唯一的机制。有一些类成员,本身是另一个类的对象,这种方法称为包含、组合或层次化。还有一些其他的继承方法是私有继承或保护继承。通常,包含、私有、保护继承方法用于实现has-a关系,即新的类将包含另一个类的对象。多重继承使得能够使用两个或更多的基类派生出来的新的类,将基类的功能组合在一起。
1.包含对象成员的类
valarray类
用来处理数值,是一个模板类,用来处理不同类型的数据
使用方法:与array类似,
valarray<double> val1; valarray<int> val2;
当一个类中包含其他现有类或者以其他类作为基类时,执行构造、析构函数的顺序
#include<iostream> using namespace std; class A { public: A() { cout << "A's constructor." << endl; } ~A() { cout << "A's destructor." << endl; } }; class B { public: B() { cout << "B's constructor." << endl; } ~B() { cout << "B's destructor." << endl; } }; class C { private: B bInC; public: C() { cout << "C's constructor." << endl; } ~C() { cout << "C's destructor." << endl; } A aInC; }; class D:public C { public: D() { cout << "D's constructor." << endl; } ~D() { cout << "D's destructor." << endl; } A aInD; private: B bInD; }; int main(void) { D d; return 0; }
上面的代码就很清晰,在D类中,以C为基类,因此需要看C类,C类中按照顺序有B类和A类,因此先看B类,B类没有基类也不包含其他类,因此直接执行B类的构造函数,然后执行A类的构造函数。这里要注意:虽然在用到A类之前已经声明了C类的构造函数析构函数,但是还是需要先执行A类的构造函数。所有的都执行结束后,D类对象才被声明完成。
至于析构函数的执行顺序,与构造函数完全相反。下面是程序的运行结果:
B's constructor. A's constructor. C's constructor. A's constructor. B's constructor. D's constructor. D's destructor. B's destructor. A's destructor. C's destructor. A's destructor. B's destructor.
(1)存在继承关系时,先执行父类的构造函数,再执行子类的构造函数;
(2)当一个类中含有对象成员时,在启动本类的构造函数之前,先分配对象空间,按对象成员的声明顺序执行他们各自的构造函数,再继续执行本类的构造函数;
(3)对于非静态的局部对象,他们的析构函数的执行顺序与构造函数相反。
初始化顺序:
- 在一个类中,在构造函数后面加一个初始化列表,先被声明的变量先被初始化
2.私有继承
在C++中,私有继承可以实现一种has-a的关系。使用私有继承,基类的公有成员和保护成员都将称为派生类的私有成员。这意味着基类方法将不会成为派生对象公有接口的一部分,但可以在派生类的成员函数中使用它们。
如果使用私有继承,基类的公有方法将称为派生类的私有方法,派生类不能继承基类的接口。
下面是详细的使用方法:
class ZhuMeng : private std::string{};
1.初始化基类组件如果是在公有继承中,ZhuMeng中定义一个string型的变量叫name,构造函数应该这样写:
ZhuMeng::ZhuMeng():name("yanyiquan"){}
- 但是在私有继承中,就应该写成这样:
ZhuMeng::ZhuMeng():std::string("yanyiquan"){}
解释一下,隐式的继承组件而不是成员对象将影响代码的编写,因为再也不能使用name来描述对象了。而必须使用用于公有继承的技术。
- 访问基类方法:
使用私有继承时,只能在派生类的方法中使用基类的方法。私有继承使得能够使用类名和作用域解析符来调用基类的方法。
//这里找不到合适的例子了 double ZhuMeng::average() const{ return Array::sum()/Array::size(); }
- 访问基类对象:
使用作用域解析符可以访问基类的方法,但是如果要使用基类对象本身,就需要强制类型转换。由于ZhuMeng类是从stirng派生而来,因此可以通过强制类型转换,将ZhuMeng对象转换为string对象。
const string & Student::Name() const{ return (const string &)*this; }
- 访问基类友元函数
- 使用类名显式的限定函数名不适合于友元函数,这是因为友元不是类的成员。然而我们可以显式的转换为基类来正确的调用。
ostream & operator<<(ostream &os ,const ZhuMeng &stu){ os<<(const string &)stu<<endl; }
3.包含和私有继承的对比
- 对于has-a的关系,我们既可以使用包含,也就是前面的第一条去实现,也可以使用私有继承实现,那么我们应该使用继承还是包含?
如果使用包含,这样代码层次清晰,耦合度比继承低,是一种良好的设计模式。并且,继承会引起许多的问题,尤其是从多个基类继承时,需要多次的转型操作。然而,私有继承提供的特性比包含多。另一种情况是私有继承需要重新定义虚拟函数。派生类可以重定义虚函数,但包含不可以。
4.保护继承
- 保护继承是私有继承的一种变体,使用关键字protected继承,像下面这样写:
class ZhuMeng : protected std::string{};
使用保护继承时,基类的公有成员和保护成员都成为派生类的保护成员。和私有继承一样,基类的接口在派生类中也是可用的,但是在继承层次结果之外是不可用的。
区别:从派生类中再派生出一个类时,如果使用私有继承,第三代类将不能够使用基类的接口,这是因为基类的公有方法在派生类中将变成私有方法,使用保护继承的时候,基类的公有方法在第二代中还是保护类型,因此在第三代的派生类中仍然可以使用。
如果想要重新定义访问权限,可以通过下面两种方式进行,
1.包装
double ZhuMeng::sum() const{ return std::Array::sum(); }
2.使用using
“`
class ZhuMeng : private std::Array{
…
public:
using std::Array::sum();
using std::Array::average();
…
};
“`
特征 | 公有继承 | 私有继承 | 保护继承 |
---|---|---|---|
公有成员变成 | 派生类的公有成员 | 派生类的保护成员 | 派生类的私有成员 |
保护成员变成 | 派生类的保护成员 | 派生类的保护成员 | 派生类的私有成员 |
私有成员变成 | 不可访问 | 不可访问 | 不可访问 |
能够隐式向上转型 | 能 | 能(只能在派生类内部) | 不能 |
5.虚基类
当在多条继承路径上有一个公共的基类,在这些路径中的某几条汇合处,这个公共的基类就会产生多个实例(或多个副本),若只想保存这个基类的一个实例,可以将这个公共基类说明为虚基类。
虚基类的基本原则是在内存中只有基类成员的一份拷贝。这样,通过把基类继承声明为虚拟的,就只能继承基类的一份拷贝,从而消除歧义。用virtual限定符把基类继承说明为虚拟的。
class x1:virtual public x
{
};
class x2:public virtual x
{
//virtual和public的顺序随意
};
初始化
- 虚基类的初始化与一般多继承的初始化在语法上是一样的,但构造函数的调用次序不同。派生类构造函数的调用次序有三个原则:
- 虚基类的构造函数在非虚基类之前调用;
- 若同一层次中包含多个虚基类,这些虚基类的构造函数按它们说明的次序调用;
- 若虚基类由非虚基类派生而来,则仍先调用基类构造函数,再调用派生类的构造函数。
- 虚基类的初始化与一般多继承的初始化在语法上是一样的,但构造函数的调用次序不同。派生类构造函数的调用次序有三个原则:
代码实例
#include <iostream>
using namespace std;
class B0// 声明为基类B0
{
int nv;//默认为私有成员
public://外部接口
B0(int n){ nv = n; cout << "Member of B0" << endl; }//B0类的构造函数
void fun(){ cout << "fun of B0" << endl; }
};
class B1 :virtual public B0
{
int nv1;
public:
B1(int a) :B0(a){ cout << "Member of B1" << endl; }
};
class B2 :virtual public B0
{
int nv2;
public:
B2(int a) :B0(a){ cout << "Member of B2" << endl; }
};
class D1 :public B1, public B2
{
int nvd;
public:
D1(int a) :B0(a), B1(a), B2(a){ cout << "Member of D1" << endl; }// 此行的含义,参考下边的 “使用注意5”
void fund(){ cout << "fun of D1" << endl; }
};
int main(void)
{
D1 d1(1);
d1.fund();
d1.fun();
return 0;
}
注意:
- (1) 一个类可以在一个类族中既被用作虚基类,也被用作非虚基类。
- (2) 在派生类的对象中,同名的虚基类只产生一个虚基类子对象,而某个非虚基类产生各自的子对象。
- (3) 虚基类子对象是由最远派生类的构造函数通过调用虚基类的构造函数进行初始化的。
- (4) 最远派生类是指在继承结构中建立对象时所指定的类。
- (5) 派生类的构造函数的成员初始化列表中必须列出对虚基类构造函数的调用;如果未列出,则表示使用该虚基类的缺省构造函数。
- (6) 从虚基类直接或间接派生的派生类中的构造函数的成员初始化列表中都要列出对虚基类构造函数的调用。但仅仅用建立对象的最远派生类的构造函数调用虚基类的构造函数,而该派生类的所有基类中列出的对虚基类的构造函数的调用在执行中被忽略,从而保证对虚基类子对象只初始化一次。
- (7) 在一个成员初始化列表中同时出现对虚基类和非虚基类构造函数的调用时,虚基类的构造函数先于非虚基类的构造函数执行。
- 上面代码执行结果:
Member of B0
Member of B1
Member of B2
Member of D1
fun of D1
fun of B0
- 这里D1在B1,B2上继承,间接继承B0,D1继承的成员变量有nv、nv1、nv2,并且只继承一次,若不是由虚基类继承而来,那么nv会被D1从B1和B2各继承一次,造成冗余。
6.使用方法
- 通常情况下,除了修改构造函数之外,多重继承还需要调整其他代码,比如说:在使用基类的方法时可能会产生二义性,MI(多重继承)如果继承了两个基类,而这两个基类中都有同名的方法,如果调用这个方法,则会出现二义性。
/*假设SingingWaiter继承singer和waiter两个基类,
并且这两个基类中都有Show()方法,则会出现矛盾*/
SingingWaiter val1;;
val1.Show();//二义性
- 如果是单继承,则会选择最近的祖宗基类中的方法,多类继承则会出现问题。可以用下面的方法来解决:
void SingingWaiter::Show()
{
singer::Show();
}
//也就是在新的派生类中重新定义Show方法,指出使用哪一个Show
7.关于多重继承的问题
- 混合使用虚基类和非虚基类
- 如果基类不是虚基类,派生类包含多个子对象。
- 例如:假设类B被用作类C和D的虚基类,同时被用作类X和Y的非虚基类,而类M是从C、D、X、Y派生而来的,这时候M类会从虚派生祖先那里共同继承一个B类子对象,从XY处各继承一个B类子对象,因此它将包含3个B类子对象。
8.MI小结
如果不使用虚基类,则这种形式的MI不会引入新规则,但是如果一个类从两个不同的类那里继承了两个同名的成员,则需要在派生类中使用类限定符来区分它们。
当使用virtual限定符来指示派生时,基类就成了虚基类。
class marketing : public virtual reality{};
- 主要变化,也就是说使用虚基类的原因是:从虚基类的一个或多个实例派生而来的类将只继承一个基类对象。
9.类模板
- 类模板存在的意义也就是类中某些变量是可以不确定的,使用模板的方式可以不必写重复的代码,只有变量类型是不同的。
- 类模板的声明与定义:
template <class T>
class ZhuMeng
{
public:
void input(T x);
};
//定义的时候,需要在类名后面加尖括号T
//注意别忘了返回类型
T ZhuMeng<T>::input(T x)
{
//...
}
- 使用的时候需要在类后面加上尖括号即可
ZhuMeng<int> val1;
- 也可能会存在下面这种情况:
template<class T, int n>
class ZhuMeng
{
private:
T val1;
//...其他的省略
}
ZhuMeng<int, 12> val;
上面那个声明中n是一个int型变量,可以直接在类里面使用。
成为模板的类,可以作为基类被继承,在被继承的时候需要和声明对象一样把模板的类型写上即可。
//接着上面那一段,ZhuMeng类是一个模板类,YanYiQuan将会继承ZhuMeng类
class YanYiQuan : public ZhuMeng<int, 3>{};
//这里是将模板定义成int型,但实际上可以将这个新的类也作为一个模板类,
template<class T>
class YanYiQuan : public ZhuMeng<T, 5>{};
- 模板类可以递归使用
ZhuMeng<ZhuMeng<int, 3>, 5> val;
//上面的递归调用就相当于创建了一个二维数组。
- 如果使用多个模板参数,则写两个类型即可
template<class T1, class T2>
class ZhuMeng
{
private:
T1 val1;
T2 val2;
// ...
};
//调用如下:
ZhuMeng<int, double> val;
//STL库中的map就是一个二维的模板类。
模板的具体化
1.隐式具体化(implicit instantiation):他们声明一个或多个对象,指出所需要的类型
ZhuMeng<int, 12> val1;
2.显式实例化:这种情况下没有创建或提及类对象。
template class ZhuMeng<int, 12>;
3.显式具体化:就是特定类型的定义
template<class T> class ZhuMeng { //... };
4.部分具体化:就是可以给部分类型参数制定一个具体的类型
template<class T1, class T2> class Pair{}; template<class T1> class Pair<T1, int>{}; template<> class Pair<int, int>{};
成员模板
- 这种模板是在定义某个类的同时,在类中又引入新的模板,第一种方式是直接在类中进行定义(声明与定义同处)
template<class T>
class ZhuMeng
{
private:
template<class T2>
class ZhuMeng2
{
T2 val_T2;
};
public:
ZhuMeng2<T> val;
};
//这里的成员模板定义为private型,因此只能在该类内进行访问。
- 第二种是将定义放在原来的模板类之外(下面是一个类和一个方法在外面进行定义的)
template<class T>
class ZhuMeng
{
private:
template<class V>
class ZhuMeng2;
ZhuMeng2<T> val1;
ZhuMeng2<int> val2;
public:
template<class U>
U mem(U , V);
};
template<class T>
template<class V>
class ZhuMeng<T>::ZhuMeng2
{
private:
V val;
};
template<class T>
template<class U>
U ZhuMeng<T>::mem(U val1, V val2)
{
}
- 因为上面的T V U是不同层次的模板进行嵌套的,并不是同级关系,所以应该写成嵌套的关系而不是放在一起写
//正确的写法
template<class T>
template<class V>
//错误的写法
template<class T, class V>
- 模板别名
- 模板别名就是用typedef将一个模板的具体化写成另外一种形式,从而来简化代码,实质上与其他的别名没什么不同。
typedef std::array<int, 12> val;
val val1;
- 与友元相关的内容在下一章友元部分一并总结。