we need one reference count per string value, not one reference count per string object. 因此需要把引用计数与特定的值绑定在一起, 如下面的 StringValue.
1 2 3 4 5 6 7 8 9
classString { public: ... // the usual String member functions go here private: structStringValue { ... }; // holds a reference count and a string value StringValue *value; // value of this String };
String::~String() { if (--value->refCount == 0) delete value; }
String& String::operator=(const String& rhs) { if (value == rhs.value) { // do nothing if the values are already the same; return *this; // this subsumes the usual test of this against &rhs } if (--value->refCount == 0) { // destroy *this’s value if no one else is using it delete value; } value = rhs.value; // have *this share rhs’s value
同时还有一个假设: To implement the non-constoperator[] safely, we must ensure that no other String object shares the StringValue to be modified by the presumed write.
1 2 3 4 5 6 7 8 9 10
char& String::operator[](int index) { // if we’re sharing a value with other String objects, // break off a separate copy of the value for ourselves if (value->refCount > 1) { --value->refCount; // decrement current value’s refCount, because we won’t be using that value any more value = newStringValue(value->data); // make a copy of the value for ourselves } return value->data[index]; // return a reference to a character inside our unshared StringValue object }
String::String(const String& rhs) { //所有的 member function 都应该这样检查是否可以共享 if (rhs.value->shareable) { value = rhs.value; ++value->refCount; } else { //如果不能共享就 new 一个一模一样的值出来, 可以共享. value = newStringValue(rhs.value->data); } }
char& String::operator[](int index) { if (value->refCount > 1) { --value->refCount; value = newStringValue(value->data); } // non-const operator [] 是唯一把 flag 置为 false 的函数 value->shareable = false; // add this return value->data[index]; }
一个引用计数的基类
Rewriting a class to take advantage of reference counting can be a lot of work. 因此考虑更好的方法: write (and test and document) the reference counting code in a context-independent manner, then just graft it onto classes when needed.
constructors 里将 refCount 设为 0 而不是 1. It simplifies things for the creators of RCObjects to set refCount to 1 themselves.
copy constructor always sets refCount to 0. That’s because we’re creating a new object representing a value, and new values are always unshared and referenced only by their creator.
assignment operator looks downright subversive(颠覆性的): 这么做的原因: In our case, we don’t expect StringValue objects to be assigned to one another, we expect only String objects to be involved in assignments. In such assignments, no change is made to the value of a StringValue — only the StringValue reference count is modified. 这里我表述一下我的理解, 真实的字符串数值, 是在一个”字符串池”里产生的, String 里有的只是其指针以及统计其引用计数的 StringValue, 而管理计数的部分被抽象为 RCObject. 只是涉及到对 StringValue 以及 RCObject 的复制并不会影响计数, 因为计数这一现象是从 String 层级(较高的用户层级)产生的. 这样才可以理解下面的话:
Given StringValue objects sv1 and sv2, what should happen to sv1’s and sv2’s reference counts in an assignment? sv1 = sv2; Before the assignment, some number of String objects are pointing to sv1. That number is unchanged by the assignment, because only sv1’s value changes(其实相当于进行换壳, 而壳是 String 类, 至于为什么可以换壳, 是因为 String 里有的只是对真实字符串数值的指针). Similarly, some number of String objects are pointing to sv2 prior to the assignment, and after the assignment, exactly the same String objects point to sv2. sv2’s reference count is also unchanged. When RCObjects are involved in an assignment, then, the number of objects pointing to those objects is unaffected, hence RCObject::operator= should change no reference counts.
思路: If we could somehow make the pointer itself detect these happenings and automatically perform the necessary manipulations of the refCount field, we’d be home free.
// template class for smart pointers-to-T objects. T must // support the RCObject interface, typically by inheriting from RCObject template<classT> classRCPtr { public: RCPtr(T* realPtr = 0); RCPtr(const RCPtr& rhs); ~RCPtr(); RCPtr& operator=(const RCPtr& rhs); T* operator->() const; T& operator*() const; private: T *pointee; // dumb pointer voidinit(); // common initialization, 减少代码重复 };
template<classT> void RCPtr<T>::init() { if (pointee == 0) { // if the dumb pointer is null, so is the smart one return; } if (pointee->isShareable() == false) { // if the value isn’t shareable, copy it pointee = newT(*pointee);//浅拷贝 } pointee->addReference(); // note that there is now a new reference to the value }
template<classT> RCPtr<T>& RCPtr<T>::operator=(const RCPtr& rhs) { if (pointee != rhs.pointee) { // skip assignments where the value doesn’t change if(pointee){ Pointee->removeReference(); init(); //如果可能, 共享, 否则做一份属于自己的副本 } return *this; }
注意开头的注释模板里的 T 必须是继承 RCObject 的类型 or at least that T provide all the functionality that RCObject does.
pointee = new T(*pointee); 会导致浅拷贝, 也就是只拷贝指针本身, 深层次的数据内容没有被拷贝过去(automatically generated copy constructors in C++, copy only StringValue’s data pointer; it will not copy the char* string data points to). 我们需要在继承的具体类 StringValue 中实现深拷贝的操作.
You should get into the habit of writing a copy constructor (and an assignment operator) for all your classes that contain pointers.
char& String::operator[](int index) { if (value->isShared()) { value = newStringValue(value->data); } value->markUnshareable(); return value->data[index]; }
与不使用智能指针的 String 相比, the only changes are in operator[], where we call isShared instead of checking the value of refCount directly and where our use of the smart RCPtr object eliminates the need to manually manipulate the reference count during a copy-on-write.
加入智能指针最关键的是 The String interface has not changed. not one line of client code needs to be changed.
classRCWidget { public: RCWidget(int size) : value(newWidget(size)) {} voiddoThis() { if (value.getRCObject().isShared()) { value = newWidget(*value);// do COW if Widget is shared } value->doThis(); } intshowThat()const{ return value->showThat(); }
private: RCIPtr<Widget> value; };
评估
究竟什么时候应该使用引用计数?
相对较多的对象共享相对少量的实值. The higher the objects/values ratio, the better the case for reference counting.
对象实值的产生或者销毁成本很高, 或者它们使用许多内存.
使用引用计数可能带来问题的场景:
Some data structures (e.g.directed graphs) lead to self-referential or circular dependency structures.
补充使用时的注意点: RCObjects 只能通过 heap 产生这一点需要客户保证. StringValue we limit its use by making it private in String. Only String can create StringValue objects, so it is up to the author of the String class to ensure that all such objects are allocated via new.
Item 30: Proxy classes.
二维数值对象实现
不使用 [][] 而是使用 ()() 进行索引的简单方法:
1 2 3 4 5 6
template<classT> classArray2D { public: Array2D(int dim1, int dim2); ... };
使用:
1 2 3 4 5 6 7
Array2D<int> data(10, 20); // fine Array2D<float> *data = new Array2D<float>(10, 20); // fine voidprocessInput(int dim1, int dim2) { Array2D<int> data(dim1, dim2); // fine ... }
类似地使用 () 进行索引:
1 2 3 4 5 6 7 8 9 10
template<classT> classArray2D { public: // declarations that will compile T& operator()(int index1, int index2); const T& operator()(int index1, int index2)const; ... }; //使用: cout << data(3, 6);//don’t look like built-in arrays any more. like a function call.
overloading operator[] to return an object of a new class, Array1D. 执着于 [][] 索引.
classString { public: constchar& operator[](int index) const; // for reads char& operator[](int index); // for writes ... };
但是无法实现, 重载的区分点在调用函数的对象是否是 const 为基准, 而目的. Compilers choose between const and non-const member functions by looking only at whether the object invoking a function is const. No consideration is given to the context in which a call is made.
1 2 3 4 5
String s1, s2; ... cout << s1[5];// calls non-const operator[], because s1 isn’t const s2[5] = ’x’;// also calls non-const operator[]: s2 isn’t const s1[3] = s2[8];// both calls are to non-const operator[], because both s1 and s2 are non-const objects
There are only three things you can do with a proxy:
创建, Create it.
左值, Use it as the target of an assignment, in which case you are really making an assignment to the string character it stands for. When used in this way, a proxy represents an lvalue use of the string on which operator[] was invoked.
右值, Use it in any other way. When used like this, a proxy represents an rvalue use of the string on which operator[] was invoked.
//Because CharProxy::operator= isn’t a const member function, //such proxies can’t be used as the target of assignments. //实现了 const 重载的本意(防止了 CharProxy 类偷走一个 char 然后修改之) const CharProxy operator[](int index) const; // for const Strings,return CharProxy objects.
const String::CharProxy String::operator[](int index) const { //const_cast is necessary to satisfy the constraints of the CharProxy constructor, //which accepts only a non-const String. returnCharProxy(const_cast<String&>(*this), index); // CharProxy object returned by operator[] is itself const, //so there is no risk the String containing the character //to which the proxy refers will be modified. }
String::CharProxy::CharProxy(String& str, int index): theString(str), charIndex(index) {}
//Conversion of a proxy to an rvalue //return a copy of the character represented by the proxy: String::CharProxy::operatorchar()const { return theString.value->data[charIndex]; }
//Conversion of a proxy to an lvalue String::CharProxy& String::CharProxy::operator=(const CharProxy& rhs) { if (theString.value->isShared()) { //检查可共享性 COW theString.value = newStringValue(theString.value->data); } //创建新的 string data 然后进行赋值 theString.value->data[charIndex] = rhs.theString.value->data[rhs.charIndex]; return *this; }
String::CharProxy& String::CharProxy::operator=(char c) { if (theString.value->isShared()) { theString.value = newStringValue(theString.value->data); } theString.value->data[charIndex] = c; return *this; }
限制
operator 重载后返回的类型
In general, taking the address of a proxy yields a different type of pointer than does taking the address of a real object.
String 的 operator [] 返回值都是 CharProxy 类 而不是 char&.
1 2
String s1 = "Hello"; char *p = &s1[1]; // error!
因此需要 overload the address-of operators for the CharProxy class:
To make proxies behave like the objects they stand for, you must overload each function applicable to the real objects so it applies to proxies, too.
proxies fail to replace real objects is when being passed to functions that take references to non-const objects
1 2 3
voidswap(char &a, char &b);// swaps the value of a and b String s = "+C+";// oops, should be "C++" swap(s[0], s[1]);// this should fix the problem, but it won’t compile
the char to which it may be converted can’t be bound to swap’s char& parameters, because that char is a temporary object (it’s operator char’s return value) and, as Item 19 explains, there are good reasons for refusing to bind temporary objects to non-const reference parameters.
implicit type conversions
只能对 user-defined 的 implicit type conversion 进行一层, 存在限制.
As Item 5 explains, compilers may use only one user-defined conversion function when converting a parameter at a call site into the type needed by the corresponding function parameter.
但如上有时候反而可以利用这一点实现压抑隐式转换的功能.
评估
proxy class 的三个应用举例:
多维数组
左右值区分
压抑隐式转换
disadvantages:
As function return values, proxy objects are temporaries (see Item 19), so they must be created and destroyed. That’s not free.
increases the complexity of software systems.
shifting from a class that works with real objects to a class that works with proxies often changes the semantics of the class.
What you need is a kind of function whose behavior is somehow virtual on the types of more than one object. C++ offers no such function.
其他语言里此特性: You could turn to CLOS, for example, the Common Lisp Object System. CLOS supports what is possibly the most general object-oriented function-invocation mechanism one can imagine: multi-methods.
As new classes are added, the code must be updated. 一旦有新加入的同级类, base 类以及所有 derived 类都要修改, 全部需要重新编译.
自行仿真虚函数表格(virtual function tables)
放弃重载, 换成使用关系型数组的方式, 在运行时判断类型然后在关系数组中寻找匹配相应类型的函数指针. create an associative array that, given a class name, yields the appropriate member function pointer.
//collide 调用 lookup 的过程 voidSpaceShip::collide(GameObject& otherObject) { HitFunctionPtr hfp = lookup(otherObject); // find the function to call if (hfp) { // if a function was found call it (this->*hfp)(otherObject); } else { throwCollisionWithUnknownObject(otherObject); } }
// 具体的初始化过程 // 所有的 hit* member function 的参数类型是一样的, 基类 GameObject 的引用 classSpaceShip:public GameObject { public: virtualvoidcollide(GameObject& otherObject); // these functions now all take a GameObject parameter virtualvoidhitSpaceShip(GameObject& spaceShip); virtualvoidhitSpaceStation(GameObject& spaceStation); virtualvoidhitAsteroid(GameObject& asteroid); ... };
下面是对具体执行函数的实现, 注意因为大家接受的入参类型都是 base 类, 需要用 dynamic_cast 转成各自需要的 derived 类.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
voidSpaceShip::hitSpaceShip(GameObject& spaceShip) { SpaceShip& otherShip=dynamic_cast<SpaceShip&>(spaceShip); process a SpaceShip-SpaceShip collision; }
voidSpaceShip::hitSpaceStation(GameObject& spaceStation) { SpaceStation& station=dynamic_cast<SpaceStation&>(spaceStation); process a SpaceShip-SpaceStation collision; }
voidSpaceShip::hitAsteroid(GameObject& asteroid) { Asteroid& theAsteroid = dynamic_cast<Asteroid&>(asteroid); process a SpaceShip-Asteroid collision; }
用 non-member function 实现执行函数
有 2 个动机使用 non-member function 指针替代 member function 指针作为 std::map 的元素.
新增一个元素会导致 recompilation, 即便那些不关心新加元素的用户.
哪里放入管理这些函数? 上面是放在一个子类中, 那问题是也可以放在另一个子类中. in which class should collisions between objects of different types be handled? 放在一个中立的位置? Wouldn’t it be better to design things so that collisions between objects of types A and B are handled by neither A nor B but instead in some neutral location outside both classes?
使用 non-member function, give clients header files that contain class definitions without any hit or collide functions.
your only practical recourse is to fall back on the double-virtual-function-call mechanism.
That implies you’ll also have to put up with everybody recompiling when you add to your inheritance hierarchy.
Initializing Emulated Virtual Function Tables (Reprise)
std::map 是静态的, Once we’ve registered a function for processing collisions between two types of objects, that’s it; we’re stuck with that function forever. What if we’d like to add, remove, or change collision-processing functions as the game proceeds? There’s no way to do it.
One way to do this is to express design constraints in C++ instead of (or in addition to) comments or other documentation. 其实就是 C++20 里面的 concept 的思想.
Handle assignment and copy construction in every class, even if “nobody ever does those things.” If these functions are difficult to implement, declare them private . That way no one will inadvertently call compiler-generated functions that do the wrong thing.
strive to provide classes whose operators and functions have a natural syntax and an intuitive semantics. Preserve consistency with the behavior of the built-in types: when in doubt, do as the ints do.
Recognize that anything somebody can do, they will do.
Strive for portable code. 除非 performance be significant enough to justify unportable constructs.
Design your code so that when changes are necessary, the impact is localized. Encapsulate as much as you can; make implementation details private.
Future-tense thinking
Provide complete classes, even if some parts aren’t currently used.
Design your interfaces to facilitate common operations and prevent common errors.
If there is no great penalty for generalizing your code, generalize it.
Lizard liz; Chicken chick; Animal *pAnimal1 = &liz; Animal *pAnimal2 = &chick; ... *pAnimal1 = *pAnimal2; // assign a chicken to a lizard!
为了解决异形赋值问题:
利用 dynamic_cast 强制类型匹配
1 2 3 4 5 6
Lizard& Lizard::operator=(const Animal& rhs) { // make sure rhs is really a lizard const Lizard& rhs_liz = dynamic_cast<const Lizard&>(rhs); proceed with a normal assignment of rhs_liz to *this; }
It forces the introduction of a new abstract class only when an existing concrete class is about to be used as a base class, i.e., when the class is about to be (re)used in a new context. Such abstractions are useful.
It is unlikely you could design a satisfactory abstract packet class unless you were well versed in many different kinds of packets and in the varied contexts in which they are used. Given your limited experience in this case, my advice would be not to define an abstract class for packets, adding one later only if you find a need to inherit from the concrete packet class(though recompile).
实在无法按照此建议做的时候该怎么做?
无法应用此建议的场景的确存在, 例如, 第三方的 C++ 库里的实体类只有读权, 无法修改其继承自一个抽象类.
作者给出了偏重于各种角度的做法如下:
从已存在的实体类派生出你的实体类, 自己多加小心部分赋值与异性赋值.
试图在类库的继承树的更高处找到一个完成了你所需的大部分功能的抽象类, 从它进行继承. 缺点是重复造轮子, you may have to duplicate a lot of effort that has already been put into the implementation of the concrete class whose functionality you’d like to extend.
以”你所希望继承的那个程序库类”来实现你的新类. 例如, 把库里的类的对象变成你的新类的 data member, 然后在新类中重新实现接口. 缺点是
update your class each time the library vendor updates the class on which you’re dependent.
forgo the ability to redefine virtual functions declared in the library class, because you can’t redefine virtual functions unless you inherit them.
classWindow { // this is the library class public: virtualvoidresize(int newWidth, int newHeight); virtualvoidrepaint()const; intwidth()const; intheight()const; };
classSpecialWindow { // this is the class you wanted to have inherit public: ... // from Window pass-through implementations of nonvirtual functions intwidth()const{ return w.width(); } intheight()const{ return w.height(); } // new implementations of "inherited" virtual functions virtualvoidresize(int newWidth, int newHeight); virtualvoidrepaint()const; private: Window w; };
手上有什么就用什么, 包括使用 non-member functions.
Item 34 如何在同一个程序中结合 C++ 和 C
确定 C++ 和 C 编译器产出兼容的目标文件
这是下面讨论的前提条件.
将双方都会使用的函数声明为 extern "C"
Name Mangling
In C you can’t overload function names ==> no name mangling.
linkers usually insist on all function names being unique. ==> C++ has name mangling.
因此, you need a way to tell your C++ compilers not to mangle certain function names. ==> extern “C” directive:
extern “C”的用法:
声明 C 函数.
声明 a function in assembler.
declare C++ functions, 方便 debug 出函数名字: your clients can use the natural and intuitive names you choose instead of the mangled names your compilers would otherwise generate.
多个函数一起声明.
1 2 3 4 5 6
extern"C" { voiddrawLine(int x1, int y1, int x2, int y2); voidtwiddleBits(unsignedchar bits); voidsimulate(int iterations); ... }
区分 C/C++ 编译器使用 preprocessor symbol __cplusplus
1 2 3 4 5 6 7 8 9 10 11
#ifdef __cplusplus extern"C" { #endif voiddrawLine(int x1, int y1, int x2, int y2); voidtwiddleBits(unsignedchar bits); voidsimulate(int iterations); ... #ifdef __cplusplus } #endif
static initialization 与 static destruction: 在 main 函数之前, 编译器会执行一些函数, 执行构造 static class 对象, 全局对象, namespace 内的对象以及文件范围(file scope) 内的对象. 在 main 后执行析构这些 static 性质的对象.
static initialization: 例如 the constructors of static class objects and objects at global, namespace, and file scope are usually called before the body of main is executed. 同样地, objects that are created through static initialization must have their destructors called during static destruction; that process typically takes place after main has finished executing.
例如一些编译器的下面的做法(插入代码式的):
1 2 3 4 5
intmain(int argc, char *argv[]) { performStaticInitialization(); // generated by the implementation the statements you put in main go here; performStaticDestruction(); // generated by the implementation }
沿袭这种思想: Just rename the main you wrote in C to be realMain, then have the C++ version of main call realMain:
1 2 3 4 5 6 7 8
extern"C" // implement this intrealMain(int argc, char *argv[]); // function in C
intmain(int argc, char *argv[])// write this in C++ { returnrealMain(argc, argv); }
Dynamic Memory Allocation
动态内存分配时, delete ~ new, malloc ~ free 配对
你没法判断给到的指针指向的对象到底是 new 出来的还是 malloc 出来的.
1
char * strdup(constchar *ps); // return a copy of the string pointed to by ps
数据结构兼容性
C 能接受哪些 C++ 的数据类型?
内建数据类型
一般指针, functions in the two languages can safely exchange pointers to objects and pointers to non-member or static functions.
trivial struct(memory layout should not change, POD). 例如没有 non-virtual functions. 没有 base class.
Item 35 让自己习惯于标准 C++ 语言
标准的重要意义: The ISO/ANSI standard for C++ is what vendors will consult when implementing compilers, what authors will examine when preparing books, and what programmers will look to for definitive answers to questions about C++.