effective c++的老笔记(四)

第三十二条款

public继承意味着is-a关系,并且基类的每一个功能都应该在派生类上发挥同样效果,否则is-a关系不成立,如基类为矩形,派生为正方形,在代码上就不属于is-a关系。

第三十三条款

不要遮掩基类的成员(public继承来的)

在使用public继承的时候,结合using声明和重载(override)来替换不要的版本并保留需要的其他版本(using声明放在public)。

至于private继承,想要保留,某个版本可以使用转接函数,即在同名的派生类函数里调用基类函数。(不使用using是因为这些函数已经在派生类可见了,但是继承在派生类的private域之中,而且using会把所有的重载版本引入。注意这里指的是private继承的基类的public中的函数,基类中private的函数对派生类是不可见的。)

Derived Class{

public:

         同名函数()

          {

                   基类::同名函数() 

          }

}

如上,完成转接。

条款三十四

区分不同的继承方式,如只继承接口,继承接口与缺省实现,继承接口和强制实现

只继承接口->纯虚函数

若需要纯虚函数的缺省实现,则使用Base::name()在派生类中调用纯虚函数定义的实现,但这么做就意味着这个缺省为公有。

继承接口和缺省实现->一般virtual。

意味着希望派生类拥有出现错误时可调用的函数,即缺省实现。问题是当派生类继承了接口,但是使用默认的实现会出现错误时。

一个解决方法是定义一个protected的成员函数来实现缺省功能,如果派生类需要缺省的行为,则调用这个函数(而且这个函数还可以inline调用),同时定义一个纯虚函数要求派生类必须实现接口。问题是增加了一个名字。

继承接口与强制实现->一般成员函数

并不意味着不能重写,只是在语意上表示这是一个不该更改的函数,派生类也应该使用一样的实现。

条款三十五

考虑virtual以外的方法

NVI方法(non-virtual-interface)实现template method(模板方法模式)

NVI方法(non-virtual-interface)实现template method(模板方法模式)
namespace NVI {
	class GameCharacter {
	public:
		GameCharacter() :health(100) {}
		GameCharacter(int h) :health(h) {}
		int healthCalc();

	private:
		int health;

		virtual int calcHealth(int num);
	};

	int GameCharacter::healthCalc()
	{
		//prepare
		int healthPoint = calcHealth(health);//执行实际功能
											 //after
		return healthPoint;
	}//定义非虚拟的成员函数来取代virtual函数,隐式inline

	int GameCharacter::calcHealth(int num)
	{
		std::cout << "gamecharacter" << std::endl;
		return num;
	}//私有的virtual函数决定实际的功能


	class EvilBadGuy : public GameCharacter {
	public:
		EvilBadGuy() :GameCharacter() {}
		EvilBadGuy(int h) :GameCharacter(h) {}

	private:
		virtual int calcHealth(int num) override;
	};

	int EvilBadGuy::calcHealth(int num)
	{
		std::cout << "evilbadguy" << std::endl;
		return num * 2;
	}//不同的计算策略
}

/*
//执行代码 结果为 gamecharacter 100 evilbadguy 200
GameCharacter g;
EvilBadGuy e;
cout << g.healthCalc() << endl;
cout << e.healthCalc() << endl;
*/
NVI的前提是virtual函数不能是public。
Function Pointer实现strategy(策略模式)
namespace strategy {
	int defaultCalc(int num);
	class GameCharacter {
	public:
		using calHealthType = int(int);//定义函数指针类型
		GameCharacter() :calMethod(defaultCalc), health(100) {}
		GameCharacter(calHealthType *p, int hp) :calMethod(p), health(hp) {}
		int CalcHealth() const;

	private:
		calHealthType * calMethod;
		int health;

	};
	int defaultCalc(int num)
	{
		std::cout << "defaultCalc" << std::endl;
		return num;
	}

	int GameCharacter::CalcHealth() const
	{
		//...准备
		int healthPoint = calMethod(health);//使用函数指针完成工作
		//...善后
		return healthPoint;
	}

	int EvilBadGuyHealthCal(int num)
	{
		std::cout << "EvilBadGuyCalc" << std::endl;
		return num * 2;
	}
}
//以下使用function生成的可调用对象实现同样的strategy模式
namespace funStrategy {
	int defaultCalc(int num);
	class GameCharacter
	{
	public:
		using calcType = std::function<int(int)>;//定义可调用对象类型
		GameCharacter() :calcFun(defaultCalc), health(100) {}
		GameCharacter(calcType cal, int hp) :calcFun(cal), health(hp) {}
		virtual ~GameCharacter() {}
		int CalcHealth();
		int MemHealthCal(int num)
		{
			std::cout << "MemHealthCal" << std::endl;
			std::cout << "this->health: " << this->health << std::endl;
			//返回的这个值是调用者的health * 3,但是这个函数并不是调用者的
			//故this->Health将会不同于调用者的this->health
			return num * 3;
		}

	private:
		calcType calcFun;//存储可调用对象
		int health;

	};
	int GameCharacter::CalcHealth()
	{
		//...某些准备
		int healthPoint = calcFun(health);//实际使用int(int)形式的可调用对象来计算
		//...善后
		return healthPoint;
	}

	int defaultCalc(int num)
	{
		std::cout << "defaultCalc" << std::endl;
		return num;
	}
	
	float defaultEBGcal(short num);//注意返回类型和参数类型,short,float分别可以提升/降级到int
	class EvilBadGuy :public GameCharacter
	{
	public:
		EvilBadGuy() :GameCharacter(defaultEBGcal,100) {}//non-member函数
		//EvilBadGuy() :GameCharacter(evilHealthCal2, 100) {}//函数对象(结构体)
		EvilBadGuy(calcType cal, int hp) :GameCharacter(cal, hp) {}
		virtual ~EvilBadGuy() override{};

		struct EvilHealthCal2 {
			int operator()(int num)
			{
				std::cout << "EvilHealthCal2" << std::endl;
				return num / 2;
			}
		}evilHealthCal2;//其实应该放private,但是这样外部就用不了了
	private:

	};
	float  defaultEBGcal(short num)
	{
		std::cout << "defaultEBGcal" << std::endl;
		return num * 2;
	}
}

//测试:
int main()
{
	funStrategy::GameCharacter a;
	funStrategy::EvilBadGuy eb;//默认构造,使用外部函数
	funStrategy::EvilBadGuy el(
		[/*捕获列表*/](int num) {cout << "lambda" << endl;return num * 5; }
	, 200);//lambda表达式
	funStrategy::EvilBadGuy e(std::bind(&funStrategy::GameCharacter::MemHealthCal, el, placeholders::_1),
		100);//可调用对象,绑定成员函数,注意这里需要绑定一个实际的类型,一个能调用这个类的类型
	funStrategy::EvilBadGuy e1(e.evilHealthCal2, 100);//函数对象

	

	cout << a.CalcHealth() << endl;
	cout << eb.CalcHealth() << endl;
	cout << el.CalcHealth() << endl;
	cout << e.CalcHealth() << endl;
	cout << e1.CalcHealth() << endl;
}
/*
结果
defaultCalc
100
defaultEBGcal
200
lambda
1000
MemHealthCal
this->health: 200 //这里200的原因是使用labmda的对象health为200
			   //而bind绑定的this正是这个对象
300            //但调用函数时传入参数num的是e的health = 100
EvilHealthCal2
50
*/
古典实现strategy模式
namespace classic {
	class HealthCalcFunc {
	public:
		int calcHealth(int num);
	}defaultCalcFunc;//计算策略类
	int HealthCalcFunc::calcHealth(int num)
	{
		return num;
	}

	class GameCharacter
	{
	public:
		GameCharacter() : healthCalFunc(&defaultCalcFunc), health(100) {}
		GameCharacter(HealthCalcFunc *hcf, int hp) :healthCalFunc(hcf), health(hp) {}
		~GameCharacter();
		int CalcHealth();

	private:
		HealthCalcFunc *healthCalFunc;
		int health;
	};

	int GameCharacter::CalcHealth()
	{
		healthCalFunc->calcHealth(health);//使用策略类方法计算
	}
}

结论

不要总是使用virtual函数,其实有很多选择,可以带来更好的可控性和封装性。

NVI:使用non-virtual函数来调用virtual函数以在调用前后进行检测、控制。

Function Pointer/function 实现的strategy模式:把具体的计算转移到了类外的可调用对象,优点是可以随时进行方法的切换(即使是运行时),而且也可以在计算前后进行一定控制。

classic的strategy模式:一眼就可以看出来是strategy模式,使用不同的类和类指针控制方法,与NVI相似的是在non-virtual中调用了virtual函数,不过NVI调用的是本类的private virtual函数,方法切换通过继承本类,经典的方法是调用方法类中的public virtual函数,切换通过继承方法类。

 

 

 

 

 

 

条款三十六

绝不重新定义继承来的non-virtual函数

这样会使同一个对象通过不同的指针/引用调用的non-member函数出现不同。比如用基类的引用调用一个派生类对象的non-virtual函数,调用的会是基类的函数,用派生类引用存放则调用派生类版本,如果需要这样的功能,那么就用virtual函数来代替,否则就不用重新定义non-member。

 

 

 

 

 

条款三十七

绝对不要重新定义继承来的函数的缺省值,因为缺省值是绑定静态类型的。

class Shape {
public:
	virtual void draw(string color = "red")const { cout << color << endl; }
};

class Rectangle : public Shape {
public:
	virtual void draw(string color = "green") const override { cout << color << endl; }
};
int main()
{
	Shape shape;
	Rectangle rectangle;
	Shape *s = &shape;
	Shape *s2 = &rectangle;
	Rectangle *R = &rectangle;

	shape.draw();
	rectangle.draw();
	s->draw();//基类指针基类对象
	s2->draw();//基类指针派生对象
	R->draw();//派生指针派生对象
}

结果:
red                  基类对象
green                派生类对象
red                  基类指针指向基类
red                  指向派生类
green                派生类指针指向派生类

结论

缺省参数由静态类型决定(即使是虚函数),但virtual函数版本却又由动态类型决定,所以不要更改virtual的缺省函数。

一定要使用缺省的时候定义一个非虚拟的接口函数,把虚拟函数放到private中,派生类修改private函数即可。

class Shape {
public:
	void draw(string color = "red")const { doDraw(color); }

private:
	virtual void doDraw(string color) const { cout << color << endl;; }
};

class Rectangle : public Shape {
public:
	virtual void doDraw(string color) const override { cout << color << endl; }
};
这样保证每个派生类的缺省参数就是”red”。

条款三十八

通过复合来实现"has - a"关系,或者根据某物来实现当前类以实现“has-a”关系(private继承/通过某类的对象/指针/引用)

复合!=public继承

复合表示的是一个类中有其他类对象

比如Person有(has-a)name birthday address等类

而根据某物来实现也是一个类中有其他类,不同的是,这个类的技能依赖于其他类

比如定义一个自定的空间需求更小的Set

namespace clause38 {
	template<typename T>
	class Set {
	public:
		bool member(const T&) const;
		void insert(const T&);
		void remove(const T&);
		std::size_t size();

	private:
		std::list<T> rep;//使用list实现set

	};
	template<typename T>
	bool Set<T>::member(const T& mem) const
	{
		//使用泛型算法
		return std::find(rep.begin(),rep.end(),mem) != rep.end();
	}
	template<typename T>
	void Set<T>::insert(const T& mem)
	{
		//不存在则加入(set是不能有同样成员的)
		if (!member(mem))rep.push_back(mem);
	}
	template<typename T>
	void Set<T>::remove(const T& mem)
	{
		typename std::list<T>::iterator it
			= std::find(rep.begin(), rep.end(), mem);//获取迭代器
			if (it != rep.end())//对象在内
				rep.erase(it);//删除
	}
	template<typename T>
	std::size_t Set<T>::size() { return rep.size(); }
}

条款三十九

谨慎使用private继承

private继承意味着被继承的成员进入派生类的private域,private继承则意味着无法发生派生类->基类的转换,也就不能把私有继承的“派生类”地址传递给一个“基类“引用/指针。这也说明private不是用来做is-a关系的。

private继承通常发生在一个类需要使用另一个类的protected成员,但是又和另一个类不具有“is-a”关系时发生

比如需要使用Timer记录Person类的调用次数等,则让Person继承private Timer(事实上你应该不会这么做)。然后Timer中的virtual就可以在Person中重写(即使是Timer的virtual private成员函数也是可以被重写的,NVI就是基于这一点实现的),而这些函数又可以调用Person的成员

同样的,可以使用经典的strategy模式实现这一点。定义一个计数类(策略类)继承public Timer,然后在Person类private中放一个计数类的指针就好了。

这样做的好处有二:

一是防止Person的派生类重写Timer的virtual函数,也就是说派生类不需要计数时,使用经典的strategy可以阻止可以阻止派生类继承计数体制,若需要计数体制只要为派生类也加上计数类指针即可,相当于实现了java中的final。(问题是现在c++也有final了,所以加上final也可以将解决这个问题,但是继承的方式任然让派生类拥有了计数器的某些public继承到基类的函数,这些函数派生类可能根本不需要)

而是可以减少编译的依赖性(针对指针的情况),只放一个指针的做法不需要了解Timer的具体定义,而是知道Timer的声明即可,而继承则需要全部的定义才行。

EBO:

一个完全没有任何数据的类的对象空间至少为1,因为编译器会强制放一个char进去,如果这个类的对象被放进其他类,则为了“齐位”,则可能会扩充成更大的类型(但并不代表其中真的有个这种类型的变量);所以为了使用某个EBO的方法,应当使用private继承,这使得派生类的大小完全不会改变,因为无数据的EBO不是独立的对象,所以编译器就不再需要像其中加入某个不存在的成员来占位,这是private继承的完美用地。但是这样的继承要保证EBO的确不占任何空间,也就是也不能含有virtual函数或者virtual的基类。

条款四十

谨慎使用多重继承。

多重继承首先要提到的就是有多少个基类的部分,派生类的两个直接基类可能又有同样的间接基类,那么就会让派生类拥有n个间接基类部分,那么直接调用间接基类的成员时就会出现多种版本,来自每个直接基类的间接基类部分。要使用特定版本则需要加上类作用域限定符。故一般来说我们不想要多个,故应该使用virtual继承,但virtual继承的问题是会减慢速度加大体积,并且派生类需要初始化直接基类。故大部分的场合可以的话就不要使用virtual继承,或者你可以使用一个没有数据成员的base class,这样继承就不会增加任何空间问题,调用的方法也不会用到不同版本的成员,更不用在派生类初始化在哪里的基类。

合理的多重继承情况是public继承接口、private继承实现。正好做了他们该做的事。由public继承一个纯粹只有函数的基类,在当前的派生类实现,再private继承一个用以实现接口的private类,这样public继承的接口只有声明,private继承的类也被封装在当前派生类中,用户只需要接口的声明即可以使用,很好得区分了声明与定义,解决了编译上的麻烦。

条款四十一

注意隐式接口和编译器多态

编译器多态->使用不同的template参数会调用不同的template函数,这是在编译期决定的,实际上是Template衍生出了一系列的重载函数,不同参数调用不同重载的版本,这是template实现多态的方法。而运行期多态则是派生类基类指针的动态绑定决定的不同virtual函数

隐式接口

template参数需要实现template函数中的操作/操作符。但实际上不是硬性的,比如operator>,template的参数可能并不实现>操作,然后template参数可以隐式转换成另一端的参数,那么template参数便也可以实现这一版本的函数,虽然它也可能根本不支持其中的任何操作。这些操作视为参数要实现的隐式接口,与显示不同的是显示接口有具体的声明定义,隐式就不一定了,可以说隐式接口意味的是参数类型可以完成操作组成的表达式,因此借助其他操作数的操作与转换也ok。

猜你喜欢

转载自blog.csdn.net/qq_37051430/article/details/83352204