More Effective C++ 第五部分 技术
Posted zhangqixiang5449
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了More Effective C++ 第五部分 技术相关的知识,希望对你有一定的参考价值。
本部分内容比较高深,涉及很多较高端的技术。笔记会有许多不足的地方,若复习可以在看一遍书。只看笔记恐怕无法彻底理解。
25.将constructor和non-member function虚化
virtual constructor
virtual constructor是并非是constructor,其根据输入可产生不同类型的对象(以多态的方式返回),常用的使用情况如在磁盘读取数据,产生一个或为音乐,或为视频的对象。
virtual copy constructor
virtual copy constructor 返回一个指针,指向其调用者的一个新副本,通常命名为colne().使用virtual copy constructor可以用一个基类指针调用clone()得到一个派生类的返回值。
virtual函数返回类型可与基类中虚函数的类型不一致。
#include <iostream>
#include <vector>
class Base
public:
virtual Base* clone() const = 0;
;
class Derived1: public Base
public:
//virtual copy constructor返回类型不为基类类型
virtual Derived1* clone() const
return new Derived1(*this);
int a = 1;
;
class Derived2: public Base
public:
virtual Derived2* clone() const
return new Derived2(*this);
int b = 2;
;
int main(int argc, const char * argv[])
std::vector<Base*> v;
v.push_back(new Derived1());
v.push_back(new Derived2());
std::vector<Base*> v2;
for (auto i = v.begin(); i!=v.end(); i++)
v2.push_back((*i)->clone());//调用virtual copy constructor
//此时v2第一个内容应该是derived1类型的,第二个是derived2类型的
printf("%d %d",
dynamic_cast<Derived1*>(*(v2.begin()))->a,
dynamic_cast<Derived2*>(*(v2.begin()+1))->b
);
return 0;
输出:1 2Program ended with exit code: 0
将non-member function的行为虚化
指在一个non-member function中调用一个虚函数。不做其他的事情。出现情况例如下边的例子:
若使用虚函数operator<<将会导致t << cout;
这样的奇怪写法,因为operator<<接受一个类型为ostream&的右边的值。而不能接受左边的值。
class NLComponent
public:
// 对输出操作符的不寻常的声明
virtual ostream& operator<<(ostream& str) const = 0; ...
;
class TextBlock: public NLComponent
public:
// 虚拟输出操作符(同样不寻常)
virtual ostream& operator<<(ostream& str) const;
;
class Graphic: public NLComponent
public:
// 虚拟输出操作符
virtual ostream& operator<<(ostream& str) const;
;
TextBlock t;
Graphic g;
...
t << cout;//不能写cout<<t,因为类内声明的operator<<接受一个右边的ostream&
g << cout;
其解决办法是
class NLComponent
public:
virtual ostream& print(ostream& s) const = 0;
;
class TextBlock: public NLComponent
public:
virtual ostream& print(ostream& s) const;
;
class Graphic: public NLComponent
public:
virtual ostream& print(ostream& s) const;
;
//虚化的non-member function
inline
ostream& operator<<(ostream& s, const NLComponent& c)
//仅调用虚函数
return c.print(s);
TextBlock t;
Graphic g;
cout << t;
cout << g;//可以使用正常的写法
return 0;
26.限制某个class所能产生的对象数量
阻止产出对象
将各constructor声明为private
仅产生一个对象
使用单例模式,但书中提出一个观点是将static对象声明在function内。
class Printer
public:
static Printer& thePrinter();
private:
Printer();
Printer(const Printer&);
;
Printer& Printer::thePrinter()
static Printer p;//使用Function static
return p;
使用Function static的好处是:
1.Function static在函数调用时才会构造对象,而class static则在程序开始时被创建。也就意味着即使没有使用到也会被创建。
2.C++对不同编译单元内的static对象初始化顺序不提供任何说明,使用class static可能会导致一些问题。
使用Function static不可将其inline,因为目标代码可能会复制多份local static对象。
不同的对象构造状态
在三种情况下会调用构造函数:
1.正常的构造。
2.派生类的base class部分
3.内嵌于其他对象内。
在控制对象产生的数量时应注意。
限制对象数量的方法
在class内声明一个static成员,用来指定生成对象最多的数量,在产生对象的函数内进行判断,若超过最大限制,则抛出异常:例如tooManyObj.
我们可以将这部分功能分离出来形成一个base class,将其template化,在使用的时候使用private继承,利用base class的构造函数在derived class构造函数内被调用的特性实现自动的管理。实现如下:
template<class BeingCounted>
class Counted
public:
class TooManyObjects; //用来抛出异常
static int objectCount() return numObjects;
protected:
Counted();
Counted(const Counted& rhs);
~Counted() --numObjects;
private:
static int numObjects;
static const size_t maxObjects;
void init(); // 避免构造函数的代码重复
;
template<class BeingCounted>
Counted<BeingCounted>::Counted()
init();
template<class BeingCounted>
Counted<BeingCounted>::Counted(const Counted<BeingCounted>&)
init();
template<class BeingCounted>
void Counted<BeingCounted>::init()
if(numbObjects >= maxObjects) throw TooManyObjects();
++numObjects;
//初始化static对象
template<class BeingCounted>
int Counted<BeingCounted>::numObjects;
使用方法:
class Printer: private Counted<Printer>
public:
//若Printer类的用户想知道当前有多少个对象,可以把让objectCount成为public的。
using Counted<Printer>::objectCount;
;
//初始化static对象
const size_t Counted<Printer>::maxObjects = 10;
27.要求(或禁止)对象产生于heap之中
要求对象产生于heap之中
产生于heap 对象必须使用new.
限制constructors和destructor其中之一为非public即可,选用限制destructor的原因是一个class有多个constructor,若由编译器合成便为public,所以我们选用限制destructor。将destructor声明为protected而非private的原因是在继承和内涵的情况下,不可以声明private。若要调用destructor需要一个member function间接调用。
class UPNumber
public:
void destory() const delete this;
protected:
~UPNumber();
;
若以产生非heap对象将会在自动析构时报错,因为无法访问destructor.
阻止对象产生于heap之中
使用new产生的对象需要调用operator new,可以把operator new声明为private以阻止。
class UPNumber
private:
static void* operator new(size_t size);
static void operator delete(void* ptr);//最好声明为与new同一访问层级.
//还可以声明operator new[]
;
这样继承自该类的对象也无法产生heap内的对象,但内含该class的可以,因为其调用的是自身的operator new.
28.智能指针
智能指针包含一个普通指针数据,通过构造析构重载操作符等方法实现“智能”并且与原指针使用方法类似。
实现解引操作符
解引操作符包含operator*和operator->
operator*需要返回一个引用。若返回T类型对象,若装入的是T类型的派生类则会发生切割问题。
template<class T>
T& SmarePtr<T>::operator*() const
....
return *pointee;//pointee是原始指针
operator->返回一个普通指针即可,因为
//pt是智能指针,重载了operator->
pt->fun();
//会变为
(pt.operator->())->fun();
所以operator->实现如下
template<class T>
T* SmarePtr<T>::operator->() const
....
return pointee;//pointee是原始指针
测试智能指针是否为空
希望可以使用传统指针判空的方式:
if(pt == 0);
if(pt);
if(!pt);
采用的办法是重载operator void*(),提供隐式转换为void指针
template<class T>
class SmartPtr
public:
operator void*();//如果普通指针为空返回0,否则返回非零值。
;
其缺点是可以比较两个不同类型的智能指针。因为都将其转为void*.
不要提供对普通指针的隐式转换
提供对普通指针的隐式转换将会导致可以轻易的对普通指针操作,从而回避了智能指针的设计目的。
智能指针与“与继承有关的”类型转换
使用template function实现
template<class newType>
operator SmartPtr<newType>()
return SmartPtr<newType>(pointee);
该方法的缺点是若有一个函数接收基类的智能指针,还有一个重载版本,接收派生类的智能指针。将会导致二义性,不能通过编译。
29.引用计数Reference counting
引用计数带来两个好处:
1.实现垃圾回收机制
2.使等值对象共享一份实值
等值对象共享一份实值
使等值对象共享一份实质,需要记录被共享的实值被共享的次数。
做法是使用一个struct将引用计数与实值关联起来
class String
public:
String(const char *initValue = "")
: value(new StringValue(initValue))
String(const String& rhs)
:value(rhs.value)
++value->refCount;
~String()
if (--value->refCount == 0) delete value;
String& operator=(const String& rhs)
if (value == rhs.value)
return *this;
if (--value->refCount == 0)
delete value;
value = rhs.value;
++value->refCount;
return *this;
//const string的operator[]假设只读,详细见下章
const char& operator[](int index) const
return value->data[index];
//非const string的operator[]可读可写
char& operator[](int index)
if (value->refCount > 1)
--value->refCount;//减少引用1个
value = new StringValue(value->data);//创建一个新的stringvalue
return value->data[index];
private:
struct StringValue
int refCount;
char *data;
StringValue(const char *initValue);
~StringValue();
;
StringValue *value;
;
String::StringValue::StringValue(const char *initValue)
:refCount(1)
data = new char[strlen(initValue) + 1];
strcpy(data, initValue);
String::StringValue::~StringValue()
delete [] data;
这个做法有一个缺点:
string s = "fuck";
char* p = &s[1];
auto s2 = s;//s2和s共享一个实值
*p = 'b';//检测不到实值被改变。s2也被改变
解决方法是使用一在StringValue内设置一个shareable标志,一旦non-const operator[]作用于对象值身上便将此标志设为false,并且不可改变。所有其他的member function都应检查sharedable。实现将在下面的基类给出。
引用计数基类
class RCObject
public:
RCObject();
RCObject(const RCObject& rhs);
RCObject& operator=(const RCObject& rhs);
virtual ~RCObject() = 0;//设计为base class的析构函数为virtual
void addReference();
void removeReference();
void markUnshareable();
bool isShareable() const;
bool isShared() const;
private:
int refCount;
bool shareable;
;
RCObject::RCObject()
: refCount(0), shareable(true)
//当RCObject被复制的时候会产生一个新的实值
RCObject::RCObject(const RCObject&)
: refCount(0), shareable(true)
RCObject& RCObject::operator=(const RCObject&)
return *this;
RCObject::~RCObject()
void RCObject::addReference() ++refCount;
void RCObject::removeReference()
if (--refCount == 0) delete this;
void RCObject::markUnshareable()
shareable = false;
bool RCObject::isShareable() const
return shareable;
bool RCObject::isShared() const
return refCount > 1;
class String
private:
struct StringValue: public RCObject
char *data;
StringValue(const char *initValue);
StringValue();
;
...
;
自动引用计数
各对象通过一个指针共享一个实值,我们可以修改指针(28章)以侦测指针的构造copy等操作,来自动设置共享了实值的对象。
被共享的对象生成在heap内,需要使用27章的方法强制其生成在heap中。
整合
将所有部分整合在一起,结果如图:
各部分详细代码和整合代码参考书203-209页
使用这种技术会导致代码维护难度极大的增加,应确保这样做可以大幅优化程序性能才使用这种方法。
30.替身类、代理类 Proxy classes
Proxy classes用来代表一个观念上并不存在的类。可以完成一些十分困难的任务,但需要从与真实的对象合作,转变到与替身对象合作,其行为有些差异。
例如希望实现一个二维数组,可通过A[9][8]
这样的方式访问。但是并没有operator[][]。所以我们建立一个替身类,二维数组重载operator[],返回该替身类的对象,而该替身类也重载operator[],返回在二维数字内的真正的值。这样operator[][]便看起来的合法的。
Proxy classes区分operator[]左值运用和右值运用
在上一章我们假设const char& operator[](int index) const
只做读操作,在这章我们用Proxy classes解决区分左值运用和右值运用。
我们令string的operator[]返回一个proxy class对象,然后等待其被运用,在确定operator[]是读还是写操作。若对proxy object做赋值动作将会是写操作,其余为读操作。
class String
public:
class CharProxy
public:
CharProxy(String& str, int index);
CharProxy& operator=(const CharProxy& rhs);//左值运用
CharProxy& operator=(char c);//左值运用
operator char() const;//右值运用
private:
String& theString;
int charIndex;
;
const CharProxy operator[](int index) const; // 返回替身对象
CharProxy operator[](int index); // 返回替身对象
friend class CharProxy;
private:
RCPtr<StringValue> value;
;
考虑以下调用方式
String s1,s2;
cout<<s1[5];
//s1[5]产生一个CharProxy对象,但该对象并无<<操作符,
//但是可以提供到char的隐式转换,所有可通过该隐式转换判定为右值运用
s2[5] = 'x';
//s2[5]产生一个CharProxy对象,调用operator=,可判定为左值运用
限制
1.在判断左值与右值引用时,可能不止一种赋值方式,可以使用+=,-=,++……这意味着若希望proxy class能和返回其对象的类合作,便要支持这些operator。
2.通过proxy class调用原对象member function将会失败,这将导致下边这样的代码报错。除非重载。
s[4].memberFun();
3.无法返回reference to non-const objects对象。
void swap(char& a, char& b);
String s = "+C+";
swap(s[0], s[1]);
String::operator[]返回一个 CharProxy 对象,但 swap()函数要求它所参数是 char & 类型。一个 CharProxy 对象可以印式地转换为一个 char,但没有转换为 char &的转换函数。 而它可能转换成的 char 并不能成为 swap 的 char &参数,因为这个 char 是一个临时对象。
4.无法提供原对象的隐式类型转换。
31.让函数根据一个以上的对象类型来决定如何虚化
传统虚函数只依据一个对象的动态类型决定调用的函数是哪个,本章将以2或以上个参数的动态类型来决定调用哪个函数,也就是说根据1个以上的对象类型来决定如何虚化函数。
例如构造一个星体碰撞的程序,碰撞的结果由碰撞的两个对象的类型决定。
class GameObject...;
class SpaceShip: public ... ;
class SpaceStation: public GameObject ... ;
class Asteroid: public GameObject ... ;
processCollision(GameObject& object1,GameObject& object2);//处理碰撞结果的函数
虚函数+运行期类型识别RTTI
在base class内定义虚函数,在派生类改写的虚函数内利用RTTI和if else判断两个object的类型
class GameObject
public:
virtual void collide(GameObject& otherObject) = 0;
;
void SpaceShip::collide(GameObject& otherObject)
const type_info& objectType = typeid(otherObject);
if (objectType == typeid(SpaceShip))
SpaceShip& ss = static_cast<SpaceShip&>(otherObject);
//处理碰撞
else if((objectType == typeid(SpaceStaion))
//下面省略
//若不是三种类型之一就抛出一个异常
使用这种方法导致代码维护性增加,类之间的耦合性增大。
只用虚函数
做法是在base class内重载虚函数。
class GameObject
public:
virtual void collide(GameObject& otherObject);
virtual void collide(SpaceShip& otherObject);
vrtuall void collide(SpaceSttion& otherObject);
virtual void collide(Asteroid& otherObject);
...
;
//处理非三种类型的参数,不像第一种会抛出异常
void SpaceShip::collide(GameObject& otherObject)
otherObject.collide(*this);
仿真虚函数表virtual function table
在class内声明一个函数指针,并实现一个函数lookup,其功能是根据不同的输入对象类型,使函数指针指向处理与该类型对象碰撞的函数。
class SpaceShip: pulic GameObject
private:
typedef void (SpaceShip::*HitFunctionPtr)(GameObject&);
typedef map<string,HitFunctionPtr> HitMap;//虚函数表的typedef
static HitFunctionPtr lookup(const GameObject& whatWeHit);
static HitMap initializeCollisionMap();//初始化map
//接受GameObject&类型参数。在每个函数的实现内将其dynamic——cast为正确的类型
virtual void hitSpaceShip(GameObject& spaceShip);
virtual void hitSpaceStation(GameObject& spaceStation);
virtual void hitAsteroid(GameObject& asteroid);
;
//base class定义的纯虚函数
void SpaceShip::collide(GameObject& otherObject)
HitFunctionPtr hfp = lookup(otherObject);
if (hfp)
(this->*hfp)(otherObject);
else
throw CollisionWithUnknownObject(otherObject);
SpaceShip::HitFunctionPtr SpaceShip::lookup(const GameObject& whatWeHit)
static auto_ptr<HitMap> collisionMap(initializeCollisionMap());//初始化,见下面
HitMap::iterator mapEntry = collisionMap.find(typeid(whatWeHit).name());
if(mapEntry == collisionMap.end()) return 0;
return (*mapEntry).second;
SpaceShip::HitMap * SpaceShip::initializeCollisionMap()
HitMap *phm = new HitMap;
(*phm)["SpaceShip"] = &hitSpaceShip;
(*phm)["SpaceStation"] = &hitSpaceStation;
(*phm)["Asteroid"] = &hitAsteroid;
return phm;
使用non-member函数
使用non-member函数的好处是当修改时(增加或删除等)不必修改类,直接修改本文件即可。
#include "SpaceShip.h"
#include "SpaceStation.h"
#include "Asteroid.h"
namespace // 使用你们namespace,使得其内的内容对编译单元为私有的
// 主要的碰撞处理函数
void shipAsteroid(GameObject& spaceShip,
GameObject& asteroid);
void shipStation(GameObject& spaceShip,
GameObject& spaceStation);
void asteroidStation(GameObject& asteroid,
GameObject& spaceStation);
//次要的碰撞处理函数,只为了实现对称性,将参数位置对调。
void asteroidShip(GameObject& asteroid,
GameObject& spaceShip)
shipAsteroid(spaceShip, asteroid);
void stationShip(GameObject& spaceStation,
GameObject& spaceShip)
shipStation(spaceShip, spaceStation);
void stationAsteroid(GameObject& spaceStation,
GameObject& asteroid)
asteroidStation(asteroid, spaceStation);
//HitFunctionPtr现在是指向non-member function的指针
typedef void (*HitFunctionPtr)(GameObject&, GameObject&);
//HitMap变为pair<string,string>, HitFunctionPtr型
typedef map< pair<string,string>, HitFunctionPtr > HitMap;
//可使用标准库make_pair代替
pair<string,string> makeStringPair(const char *s1,
const char *s2);
//需要修改以适应新HitMap类型,原理不变
HitMap * initializeCollisionMap();
//需要修改以适应新HitMap类型,原理不变
HitFunctionPtr lookup(const string& class1,
const string& class2);
// end namespace
void processCollision(GameObject& object1,
GameObject& object2)
HitFunctionPtr phf = lookup(typeid(object1).name(),
typeid(object2).name());
if (phf) phf(object1, object2);
else throw UnknownCollision(object1, object2);//查找不到对应的函数则抛出一个异常
当需要修改时,直接在匿名的namespace加上处理的函数,在 initializeCollisionMap()中添加对应的map即可。
缺点
上述的所有方法都是静态的,没有解耦合,若想增加或删除都需要更改已将写好的函数。使用动态方法的思路是:
我们可以将存储撞击函数的map放入一个类,并由它提供动态修改映射关系的成员函数。
以上是关于More Effective C++ 第五部分 技术的主要内容,如果未能解决你的问题,请参考以下文章