条款1 数据抽象
当我们在某个问题领域中识别对象时,首先考虑的问题是“可以用这个对象来做什么”而不是“这个对象是如何实现的”。因此,如果某个问题的自然描述涉及到雇员、合同和薪水记录,那么用来解决该问题的编程语言就应该包含Employee、Contract和PayrollRecord类型。这样就允许在问题领域和解决方案领域之间进行双向、高效地转换,用这种方式编写的软件才能尽量避免产生“转换噪音”,从而达到更简洁、更准确。
如何识别问题领域中的对象?如何根据问题领域中的对象识别解决方案中的对象?这两个问题其实在我的UML那本书里有介绍,但是我一直没有领悟。比如,在SIP呼叫器插件中,我连问题领域的概念都没有,谈何识别,这样解决方案中的对象识别也就无从谈起了。具体一点,场景文件是否是一个对象,初始化配置文件是否是一个对象?
在C++这样的通用编程语言中,不会有像Employee这样特定于应用的类型,但是,它允许我们创建像Employee这样的抽象数据类型。从本质上说,抽象数据类型的用途在于将编程语言扩展到一个特定的问题领域。
C++中不存在针对抽象数据类型设计的公认方案,这方面的编程依然需要灵感和艺术才能,不过许多成功的途径都遵循下面这组类似的步骤。
看似目前我还不具有这方面的才能,咋办?
(1)为类型选择一个描述性的名字。如果难以为这个类型命名,那就说明你还不知道你想要实现什么,你需要多开动脑筋。一个抽象数据类型应该表示一个单纯的、有着良好定义的概念,而且为该概念所取的名字应该是显而易见的。
我经常还在为取名而烦恼,说明我设计抽象数据类型上还很欠缺,咋办呢?
(2)列出类型所能执行的操作。定义一个抽象数据类型的依据是能用它做什么。要避免在实现时简单地为数据成员提供一串get/set操作-那不叫数据抽象,而是懒惰且缺乏想象力的表现。
(3)为类型设计接口。正如Scott Meyers告诉我们的那样,一个类型应该做到“易于正确使用、难以错误使用”。你要为类型的用户设身处地地想一想,并且编写一些使用类型接口的代码。良好的接口设计除需考虑技术的威力外,心理学和情感方面的问题同样需要加以考虑。(这条印证了设计模式解析中说的从背景设计原则)
我完全还达不到这个层次,比如我写的查找对话框类,我的接口就是公有成员变量,自己用还可以,别人一看根本不知道怎么用,这说明这个查找对话框类还不具备让别人复用的水平,如何提高?
(4)实现类型。不要让实现影响类型的接口。要实现类型的接口所承诺的约定。记住,大多数情况下,对抽象数据类型的实现的改动,远比对其接口的改动来得频繁。
但是我的接口经常发生变动,比如告警处理模块的OnRecv函数接口,由传入lTaskID改为传入pTask,这说明什么问题?告警处理模块不应该抽象成一个类?还是其他的?
这些步骤仅仅是从实现抽象数据类型的具体操作来说的,对于识别对象几乎没有有价值的建议,而识别对象才是我们真正需要掌握的能力(也是面向对象设计中最难的),看来还是得从UML那本书取经了。
条款2 多态
看了三遍不知道作者究竟想让我明白什么?下次有空再读几遍
条款3 设计模式
......,然而,设计模式的确是职业C++程序员工具箱中不可或缺的组件。
但是我对设计模式还很菜!郁闷啊
不管哪种描述方式,模式均包含以下4个必不可少的部分。
首先,每个模式必须具有一个毫无歧义的名字,比如Bridge,Strategy,Facade等。
其次,每个模式必须定义它所能解决的问题。
再次,每个模式必须提供该问题的解决方案。
最后,每个模式必须说明应用该模式后,会带来哪些好处,哪些坏处。
模式除了用来解决问题,还有利于程序员之间的顺畅交流。
上面说的这些东西,和四人帮写的那本设计模式以及模式重构那本书上讲的完全一致(即每个模式都是按这4个必不可少的部分进行描述的),看来这个作者肯定看过四人帮那本书。
......,当然,选择适当的模式并有效地对其进行组合,也是需要设计方面的专家经验和天资禀赋的。
我连基本的设计模式都不熟,谈什么组合啊,自我感觉寒碜,呜呜!
条款4 STL
STL并不仅仅是一个库,它更是一种优秀的思想以及一套约定。
STL包含三大组件:容器、算法和迭代器。容器用于容纳和组织元素;算法执行操作;迭代器则用于访问容器中的元素。这些都不是什么新东西,许多传统的程序库也都包含类似的组件,并且许多程序库也都采用模板实现而成。STL的优秀思想体现在:容器与在容器上执行的算法之间无需彼此了解,这种戏法是通过迭代器实现的。
......,采用这种方式,容器和算法可以紧密地协作,同时还可以保持彼此不知情(这种“不知情”的好处,乃是C++高级编程领域反复强调的主题)。
STL对约定有着很强的依赖。容器和函数对象必须通过一套标准的嵌套类型名字对其自身进行描述。
怎么我就不能领会这种思想和约定的实践意义呢?
最后,作者说到STL的效率也是不错的,和专家写的媲美,比普通人写的要明显胜出一筹,一句话,让我们学习并使用STL。(这点我赞同,否则我还真的不会写程序了,呵呵)
条款5 引用是别名而非指针
对于这个条款其他好多书上也讨论,但在实践中并没有多大的意义,反正引用是采用指针的方式实现的的,作者也承认。
对于作者提出的引用和指针的三大区别,完全站不住脚,因为应该理解为引用相当于常量指针。如果理解为常量指针,一下子就把他的三大区别推翻了,常量指针也必须初始化,当然就不存在指向空的情况了,指向一个对象后同样不能再改变。
int* const p; //error C2734: 'p' : const object must be initialized if notextern
const int i; //error C2734: 'i' :const object must be initialized if not extern
只要是常量的东东,必须得初始化否则编译同样不能通过。(已验证)
本质上是一样的,但是在个别语义上还是有差别,比如,引用就意味着它一定得指向存在的对象,程序员应该保证这个,例如:
Employee& anEmployee = *getAnEmployee(); // 糟糕的代码,程序员的错
if (&anEmployee == 0) // 未定义行为
你应该使用一个指针来存放getAnEmployee返回的结果。
当一个指向常量的引用采用一个字面值来初始化时,该引用实际上被设置为指向“采用该字面值初始化”的一个临时变量。下面的汇编可以证明:
const int& i = 10;
00401028 mov dword ptr[ebp-8],0Ah // 将10放入一个临时分配的空间中
0040102F lea eax,[ebp-8] // 将临时空间的地址放入eax寄存器
00401032 mov dword ptr[ebp-4],eax // 将寄存器中的值放入变量i的空间中,同时证明引用是由指针实现的
但是为什么就不能int& i = 10;呢?并且强转也不行,为什么?
答:通过
int fun()
{
return 1;
}
int main(int argc, char* argv[])
{
int&i = fun();
return 0;
}
的编译告警
error C2440: 'initializing' : cannot convert from'int' to 'int &'
A reference that is not to 'const' cannot be bound to anon-lvalue
我们可以确定,虽然从物理上可以实现非const引用绑定到一个左值,但是从语义上是不支持的,当然我们也就没办法了,除非你自己写个编译器,呵呵!
条款6 数组形参
就是讲如何传递数组,大致有四个办法:1.带一个元素个数的参数 2.数组的引用 3.模板+引用 4.带一个表示结束的元素值5.使用vector或string代替数组
这个条款我以前就已经完全理解,这儿算是复习。其实2和3是类似的,2是数组元素个数是写死的(比如11,如果需要多个元素个数的函数版本,得显式写多个函数定义),3是通过模板参数实现数组元素个数可变,等实例化模板的时候函数原型再确定,不需要写多个,实例化多个就行了。我建议采用1和5,最简单,谁都能看懂。
条款7 常量指针与指向常量的指针
我已经能够区分,略
条款8 指向指针的指针
在C++中,几乎总是首选指向指针的引用作为函数参数,而不是指向指针的指针。
我已经能够区分,略
条款9 新式转型操作符(记住,不管怎么转,对被转变量类型i都不会有影响)
const_cast
const int i = 10;
int& ri = const_cast<int&>(i); // 用法1
int* pi = const_cast<int*>(&i); // 用法2,它只有这两种用法。
static_cast(针对void*和两者之间有关系的转换,否则编译不过)
D* pd = static_cast
D* pd2 = static_cast
ch = static_cast
dynamic_cast(仅针对多态的类型,即被转换的类型必须是带有虚函数的,转换到的可以不是多态类型,否则编译不过)
带运行时检查的指针或引用转换,注意,在VC中如果转换失败会抛异常,而VS2005中不抛(已证实)
#include "stdafx.h"
#include "stdlib.h"
class B{
public:
virtual void fun(){}
int i;
};
class C{
public:
int i;
};
class D : public B, public C{
int i;
};
int main(int argc, char* argv[])
{
D d;
B* pb = &d;
C* pc = dynamic_cast
// C* pc = (C*)(pb); // 错误!C风格的转换并不能让pc指向C的那部分
system("pause");
return 0;
}
reinterpret_cast(仅针对指针相关的转换,否则编译不过)
char* pch = reinterpret_cast<char*>(pv); // Allows any pointer tobe converted into any other pointer type
int* pi = reinterpret_cast<int*>(12346);// The reinterpret_cast operator also allowsany integral type to be converted into any pointer type and vice versa.
这儿主要是掌握上面的转型操作符的用法。作者说得很有道理,用它们的目的就是让转型看起来很丑陋,让写代码的人很痛苦(所以我老是用老的转换),它们易于发现,因为无论何时发生bug时,人们应该首先检查转型这个嫌犯。
我的观点:以我几年的编程经验,我发现,在实际编程中,确实经常会出现类型不一致的情况,比如有符号和无符号,char和int,int和long等,要是都用新式的转换,我不累死才怪!再说,有多少bug是由类型转换引起的,我是没有碰到过,所以,我仍然才用旧式转型(因为我们在写代码的时候已经知道能不能转换了,我们唯一需要注意,即可能出错的例外情况是当我们想依赖运行时类型识别时,即有不止一种转换结果时,比如水平方向转换等,dynamic_cast才有可能派上用场),再说了,就算转换错了,也是非常容易发现的,当然,要是能消除转换那就更好了,反正我是不习惯用新式的!
扩展:无多态的水平方向如何转换?
答:
int main(int argc, char* argv[])
{
D d;
// 从目前看来,唯一的办法就是先向下转换,然后再向上转换。
// 如果直接从B到C,那么这种直接硬转换不是我们想要的结果。
B* pb= &d;
D* pd = (D*)pb;
C* pc = (C*)(pd);
system("pause");
return 0;
}
条款10 常量成员函数的含义
为什么常量成员函数中不能修改成员变量呢?那是因为此时的this指针的类型为const X*const this,而非常量成员函数的this指针的类型为X* const this。
当成员变量是指针的时候,虽然我们在常量成员函数中不能修改指针的指向,但是我们可以修改指针指向对象的值。比如
......
private:
int* p;
};
在常量成员函数中,我们可以
p[2] = 4;
这种做法是合法的,但很不道德。就像那些口口声声说尊重法律条文实际上却在违背其本意的奸诈律师一样,一个编写可以改变对象逻辑状态(即改变p是改变物理状态,改变p指向的对象是改变逻辑状态)的常量成员函数的C++程序员,即使编译器未宣判他有罪,他的同事也会判他有罪,因为这种做法很不厚道!
话又说回来。有时一个真的应该被声明为常量的成员函数必须要修改其对象。这常见于利用“缓式求值(lazy evaluation)”机制来计算一个值时。你的想法可能是将this转型,像这样
int GetValue() const
{
if(!IsComputed)
{
X* const aThis =const_cast
aThis->computedValue =expensiveOperation();
aThis->isComputed = true;
}
return computedValue;
}
但这种做法是糟糕的!处理这种情况的正确方式是将有关数据成员声明为mutable,如
private:
//......
mutable bool isComputed; // 现在可以被常量成员函数修改了
mutable int computedValue; // 现在可以被常量成员函数修改了
};
说不定我目前正在写的场景文件解析模块中,对是否是主叫,节点名都可以用缓式求值呢?应用场景岂不是和上面讲的一样:)
class X
{
public:
int& operator[](int index)
{
return a[index];
}
const int& operator[](intindex) const
{
return a[index];
}
int a[10];
};
int main(int argc, char* argv[])
{
X x;
x[1]; // 当两个函数存在时,优先匹配第一个函数,this是x*const,因为x是非常量
const Xy;
y[1]; // 肯定调用的是第二个,this是constX* const,因为y是常量
return 0;
}
对于下面这个重载不太理解
class X
{
public:
//...
Xoperator+(const X &rightArg); // 左边的常数是非常量
Xoperator+(const X &rightArg) const; // 左边的常数是常量
};
终于理解了,它的意思和上面例子一样,即当X是非常量时,优先匹配第一个,当X是常量时,只能匹配第二个。例
class X
{
public:
int operator+(const int&rightArg)
{
return i + rightArg;
}
int operator+(const int&rightArg) const
{
return i + rightArg;
}
int i;
};
int main(int argc, char* argv[])
{
X x;
x + 1; //因为x是非常量,而第一个函数的this是是x* const,故优先匹配第一个函数
const X y;
y + 1;
return 0;
}
条款11 编译器会在类中放东西
这个条款是说编译器可能会在我们的类对象中插入虚表指针等玩意儿,并且不同编译器位置可能不是一样的。
在不同的平台上,高层的操作将会做相同的事情,但它们的底层实现可能并不相同。例如,如果希望复制一个类对象,那么永远不要使用memcpy这样的标准内存块复制函数,而应该使用对象的初始化或赋值操作。
我才不会傻得用底层操作去实现对象复制!所以这个条款对我的实际工作没有多大意义。
条款12 赋值和初始化并不相同
本条款主要说明赋值和初始化的区别: 赋值时,必须先清除掉左值拥有的资源再赋值,而初始化时,左值没拥有资源的可以直接赋值。
感觉这没有什么好讲的,对工作没什么实际意义。换句话说,后者是调用构造函数的过程,前者是调用赋值运算符的过程。因为构造函数是没拥有资源的,所以在构造时不用释放之前的资源,而赋值需要,所以,当一个对象可以用构造函数一步到位比先默认构造再赋值的效率更高,比如
CString str(_T("123"));就比CString str;str =_T("123");效率高!
条款13 赋值操作
提到了一下在写赋值函数时,需要判断参数和它自身是不是同一个地址,这种对自身赋值所执行的检查往往是为了正确性(有时也是出于效率方面的考虑)。
我们在写赋值函数的时候容易忘掉检查这个步骤,注意啦!这个知识点以前就知道,对工作没什么实际意义,BUG不会出在这种地方。
条款14 函数指针
这儿主要讲了三点,一、函数名前加不加&都可以获得函数的地址,用函数指针调用函数时前面加不加*都可以(具体参考TheC++ Programming Language,那儿也有介绍)。二、注意,一个函数指针指向内联函数是合法的。然而,通过函数指针调用内联函数讲不会导致内联式的函数调用,因为编译器通常无法在编译期精确地确定将会调用什么函数。三、函数指针的一个传统用途是实现回调。
对于第三点,以前还真的没想过,不过想想,好像还真的是这样。这个条款都是旧知识,对我没多大帮助。
条款15 指向类成员的指针并非指针
与常规指针不同,一个指向成员的指针并不指向一个具体的内存位置,它指向的是一个类的特定成员,而不是指向一个特定对象里的特定成员。通常最清晰的做法,是将指向数据成员的指针看做为一个偏移量。当然,事情未必一定如此,因为C++标准对于一个指向数据成员的指针究竟该如何实现只字未提。成员指针例子:
class C
{
public:
int i;
int j;
};
int main(int argc, char* argv[])
{
int C::* pimC; // 定义了一个指向类C中的一个int(i)成员(m)的指针(p)
C aC;
C* pC = &aC;
pimC = &C::j; // 注意,指向的是类的成员,而不是某个类对象的某个成员. pimC为,表示j的偏移量是
aC.*pimC = 10; // 成员指针使用方式一
int b = pC->*pimC; // 成员指针使用方式二,两种形式都表示找到从对象开始偏移多少的成员
return 0;
}
理解了成员指针是一个偏移,你就不难理解: 存在从指向基类成员的指针到指向公有派生类成员的指针的隐式转换,反过来就不行(可能偏移过头了,当然不行了)。
指向数据成员的指针的使用场景在哪儿?好像从来没见过,对实际工作用处不大,拿来忽悠人还差不多。
条款16 指向成员函数的指针并非指针
class Shape
{
private:
int i;
public:
void moveTo(){}
void validate() const{}
virtual void draw() const{}
private:
int j;
};
int _tmain(int argc, _TCHAR* argv[])
{
// 定义语法
void (Shape::*mf1)() = &Shape::moveTo;
void (Shape::*mf2)() const = &Shape::validate;
void (Shape::*mf3)() const = &Shape::draw;
Shape shape;
Shape* pShape =&shape;
// 调用语法
(shape.*mf1)();
(pShape->*mf1)();
// 我打印了一下,不管怎样,全部都是1
cout << &Shape::moveTo<< endl;
cout << &Shape::validate<< endl;
cout << &Shape::draw<< endl;
system("pause");
return 0;
}
注意,不存在什么指向“虚拟”成员函数的指针。虚拟性是成员函数自身的属性,而不是指向它的指针所具有的属性。也就是说用普通的指向成员函数的指针也可以指向虚拟成员函数。
这就是为何一个指向成员函数的指针,通常不能被实现为一个简单的指向函数的指针。一个指向成员函数的指针的实现自身必须存储一些信息,诸如它所指向的成员函数是虚拟的还是非虚拟的,到哪里去找到适当的虚表指针,从函数的this指针加上或减去一个偏移量,以及可能还有其他一些信息。指向成员函数的指针通常实现为一个小型结构(为什么我sizeof它却等于4呢?),其中包含这些信息。当然,也可以使用其他一些实现。
和指向数据成员的指针一样,指向成员函数的指针也表现出一种逆变性,即存在从指向基类成员函数的指针到指向派生类成员函数指针的隐式转换,反之则不行。
指向成员函数的指针有什么使用场景?我在工作中从来没见过
条款17 处理函数和数组声明
函数指针数组的声明方式,是将数组名字和简单的函数指针声明放在一起。因此,可以声明一个装有这些东西的数组:
int (*afp2[N])(); // 一个具有N个元素的数组,其元素类型为指向“返回值为int”的函数指针
至此,事情开始变得笨拙,typedef闪亮登场的时机到了:
typedef int (*FP)();
FP afp3[N]; // 功能同上
使用tpyedef可以简化复杂的声明语法,这也是对你的代码维护者的关爱。使用typedef甚至标准的set_new_handler函数的声明都变得简单多了:
typedef void(*new_handler)();
new_handler set_new_handler(new_handler);
如果不使用typedef,你的声望在那些维护你的代码的人中将急剧下跌:
void (*set_new_handler(void (*)()))(); // 语法没错,但邪恶
除了使用函数指针,还可以使用函数引用,但函数引用很少使用,其应用程度跟常量函数指针差不多。
都不知道这个条款主要讲什么,讲的内容和标题感觉有点不搭配,更像是介绍typedef的使用,很好地证明了使用typedef的必要性,以前我不喜欢typedef的,现在有点扭转观念了。
条款18 函数对象
有时需要一些行为类似于函数指针的东西,但函数指针显得笨拙、危险而且过时(让我们承认这一点)。通常最佳方式是使用函数对象取代函数指针。
A function object, or functor, is any typethat implements operator(). (记住这个定义,否则老觉得函数对象高深)
Function objects provide two main advantages over a straight functioncall. The first is that a function object can contain state. The second is thata function object is a type and therefore can be used as a template parameter.
对于第一个优点,如果不用函数对象,你就得用全局或局部静态对象或其他一些基本的技巧,以便在多次调用同一函数的时候保持某些状态。甚至,使用全局或静态局部变量使函数成为不可重入的,说不定采用函数对象可以解决这个问题?找个工作中例子看看能不能证明这个猜想。
对于第二个优点,我还得证明一下函数指针为什么不能作为模板参数?
答:从find_if来看,都是可以做模板参数的。
以上优点是MSDN中提出的,第一个优点书上也有,书上另外一个优点是: 使用函数对象还有可能并且常见的是获得虚函数指针的效果,这是通过创建一个带有虚拟operator()的函数对象层次结构而实现的。
对于第三个优点,书上有个例子,但是我想在真实的工作中找一个例子?
为什么使用函数对象比使用函数指针有那么多优点,但在工作中我们还是习惯使用函数指针呢?
答:函数对象其实就是类,有时一件很简单的事情,我们干嘛一定要搞个类来实现呢,函数指针多直接啊,一定要因地制宜,不要有函数对象就比函数高级的思想
对于类中的一个功能,我们可能只需要一个接口函数,但是因为代码多,我们可能需要多个工具函数去实现这个接口函数,造成类中函数过多,比如在CScenarioXML类中,黄建写的和我写的分别占好多个函数,是否可以通过函数对象封装一下呢?
答:我觉得完全可以,这样应该说功能更加内聚。
假如函数对象可以用来封装操作,而普通的类也可以,我们何时用普通类,何时用函数对象呢?
答:应该说普通类和函数对象没有任何本质区别,一般情况下,如果你的类只提供一个对外功能接口的时候,此时用函数对象大概可以省掉一个名字。普通类需要一个类名和接口函数名,函数对象仅仅需要一个类名而接口函数名变成了operator(),并且调用时看起来像调函数。
总结:只要你把函数对象看做是普通的类,什么东西你都就好理解了。
条款19 Command模式与好莱坞法则
当一个函数对象用作回调时,就是一个Command模式的实例。
从另外的设计模式书中理解Command模式?
Button的用户设置回调函数,然后将Button移交给框架代码,后者可以侦测Button何时被点击了,并执行指定的动作。
Button* b = new Button("Anoko no mamaewa");
b->setAction(playMusic);
registerButtonWithFramework(b);
这种责任的分割通常被称为“好莱坞法则”,即"不要call我们,我们会call你".
感觉MFC中的消息映射函数的就是这种方式?好处就是将变与不变的东西分离,即MFC框架不变,变得仅仅是要执行的动作。如果代码全部是我们写的话,可能就会把变与不变的混合起来,这样代码重用性就不高了,在我们的代码中找找看看能否找到这样的案例?
然而,使用一个简单的函数指针作为回调的做法有一些严格的限制。我们知道,函数往往需要一些数据才能工作,但一个函数指针没有相关联的数据。在上面的例子中,函数playMusic如何知道播放什么歌曲?快速修复这个问题的常见做法有二.其一,将函数的作用域大幅缩小,如下
extern void playAnokoNoNamaewa();
//...
b->setAction(playAnokoNoNamaewa);
也就是函数名里带上歌曲信息,相当于写死了。
其二,借助于一些声名狼藉的、危险的编程实践,例如使用一个全局变量:
extern const MP3* theCurrentSong = 0;
//...
const MP3 anokoNoNamaewa("AnokoNoNamaewa.mp3");
theCurrentSong = &anokoNoNamaewa;
b->setAction(playMusic);
为什么不选择带参数列表的回调函数呢?MFC中的消息处理函数是如何实现的?
Command模式和好莱坞法则感觉没什么区别?
一种具有代表性的更好方式是使用函数对象代替函数指针。将一个函数对象(典型情况为函数对象层次结构)与“好莱坞法则”相结合使用,即为Command模式的一个实例。
class Action{ //Command
public:
virtual ~Action();
virtual void operator()() = 0;
virutal Action* clone() const= 0;
};
class PlayMusic : public Action{
public:
PlayMusic(const string&songFile) : song_(songFile){};
void operator()(); // 播放音乐
private:
MP3 song_;
};
Button* b = new Button("Anoko no mamaewa");
auto_ptr
b->setAction(playMusic);
registerButtonWithFramework(song.get());
好好体会一下这种方式的好处?看看平常的工作中是否能够用得上
答:其实上面的需求啊,可以简单的理解为我们想设置一个回调函数,但光一个函数指针还不够,还需要设置若干个参数,这些参数将会在回调时被回调函数使用。脑筋能转过弯的话,很容易想到办法,我们完全可以把函数指针和参数封装到一个类中,直接把这个类设置过去,那不就得了,还用得着搞个屁的全局变量啊!解决这种需求,其实用不用函数对象、叫不叫Action已经不重要了,道理通的。
体会一下函数对象的命名方式,我觉得有时我不愿用函数对象是不好取名
答:函数名是什么就取什么,完全不用再遵照旧的类名规则,比如以C开头等
条款20 STL函数对象
在这个例子中,从binary_function派生下来的popLess类型允许我们找到函数对象的参数和返回值类型。不过在这里我们并没有利用这种能力,但是可以打赌肯定有人需要这样的能力,而且希望我么弄得popLess类型可以为其他人利用。
使用函数对象作为比较器的一个额外好处,就是比较操作将被内联处理,而使用函数指针则不允许内联。
复习一下为什么find_if这样的算法函数,我么既可以传递一个函数对象也可以传递一个函数指针进去?我记得是可以的
答:
struct greater10
{
greater10()
{
cout << "greater10"<< endl;
}
bool operator()( int value )
{
return value >10;
}
};
int main( )
{
list <int> L;
L.push_back( 5 );
L.push_back( 10 );
L.push_back( 15 );
find_if(L.begin(), L.end(), greater10());// 构造了一个greater10类型的临时对象。记住要加括号,我们需要的可是对象实例,不是类型
}
template<class_InIt,
class _Pr> inline
_InIt _Find_if(_InIt _First, _InIt _Last, _Pr _Pred)
{ // find firstsatisfying _Pred
for (; _First != _Last; ++_First)
if (_Pred(*_First))
break;
return (_First);
}
因为_Pr是一个模板参数,所以,如果你传函数指针进去,那么类型就是函数类型;如果你传函数对象进去,那么_Pr就是函数对象类型。正好,函数对象是重载了()的,当然两者都是可以的。
对于STL中的函数对象,我们得好好理解与使用。
答:
class Functor
{
public:
int operator()(int a, int b)
{
return a < b;
}
};
int main()
{
Functor f;
int a = 5;
int b = 7;
int ans = f(a, b);
}
其实我们可以在Functor中添加任何东西,比如构造函数、私有函数等等,只要operator()存在就行,这样我们就可以像函数一样操作。STL中的函数对象是同样道理,看看源代码就好理解了。
template<class_Ty>
struct less
: public binary_function<_Ty, _Ty, bool>
{ // functor foroperator<
bool operator()(const _Ty& _Left, const _Ty& _Right) const
{ // applyoperator< to operands
return (_Left < _Right);
}
};
说白了,函数对象一点都不神秘,和普通的类没有任何区别,正如MSDN上说的,无非就是重载了operator()而已,一般类名就是函数名。我完全可以把类名另外取,把operator()换成具体的函数名,这样就和我们以前普通的干法一样了。
为什么我们的SIP呼叫器程序中出现了那么多用for遍历容器的代码,怎么就没有用for_each呢?
答:在SIP+RTP中,其实我已经尝试过了,但感觉不好使:本来针对每个元素的操作是很简单的事,比如delete元素,但是我们却要去写个全局函数,名字还不好取,麻烦得很!
条款21 重载和重写并不相同
重载和重写彼此之间没有任何关系。它们是两个完全不同的概念。对它们之间区别的不了解,以及对这个两个术语马虎的使用,业已导致数不清的混乱和不计其数的bug.
能导致bug吗?我在工作中遇到过吗?
答:我觉得导致不了,至少我在工作中是没遇到过。
重载发生于同一个作用域内有两个或多个函数具有相同的名字但签名(参数列表不同就会导致不同的签名)不同时。
重写发生于派生类函数和基类虚函数具有相同的名字和签名时。
我再重复一遍,重载和重写是两个不同的概念,如果希望洞悉关于高级基类接口设计方面的建议和忠告,在技术上透彻理解这两个概念之间的区别是必不可少的。
条款22 Template Method模式
Template Method模式和C++模板一点关系都没有。
一个基类的成员函数是否应该为非虚拟的、虚拟的或纯虚拟的,这样的决策主要是基于该函数的行为如何被派生类定制。如果基类成员是非虚拟的,那么基类设计者就为以该基类为根所确立的层次结构指明了一个不变式(invariant)。派生类不应该用同名的派生类成员去隐藏基类非虚函数。如果不喜欢基类所指定的契约,可以(应该)去寻找另一个合乎心意的基类,而不要试图去改写基类的契约。虚函数和纯虚函数指定的操作,其实现可以又派生类通过重写机制定制。一个非纯虚函数提供了一个默认实现,并且不强求派生类一定重写它,而一个纯虚函数则必须在具体派生类中进行重写。这两种虚函数都允许派生类插入并取代其整个实现,同时保持接口不变。
Template Method模式赋予基类设计者一种中级控制机制,该控制机制介于非虚函数提供的“占有它或离开它”和虚函数提供的“如果你不喜欢就替换掉所有东西”这两种机制之间。Template Method确立了其实现的整体架构,同时将部分实现延迟到派生类中进行。通常来说,TemplateMethod被实现为一个公有非虚函数,它调用被保护的虚函数。派生类必须接受它所继承的非虚基类函数所指明的全部实现,同时还可以通过重写该公有函数所调用的被保护的虚函数,以有限的方式来定制其行为。
class App
{
public:
virtual ~App();
//...
void startup() // TemplateMethod
{
initialize();
if(!validate())
altInit();
}
protected:
virtual bool validate() const =0;
virtual void altInit();
private:
void initialize();
//...
};
class MyApp : public App
{
public:
//...
private:
bool validate() const;
void altInit();
//...
};
Template Method是一个“好莱坞法则”的例子,即“不要...,我们...”.startup函数的整体流程由基类确定,客户通过基类的接口来调用strartup,因此派生类设计者不知道validate或altInit何时会被调用。但他们知道当这两个方法被调用时,它们各自应该做些什么。因此我们说,基类和派生类同心协力打造了一个完整的函数实现。
其实这种所谓的TemplateMethod模式在工作中我们经常用到,只是可能不知道这个名字而已,比如在DMR中就用到了。不知道这个条款主要的目的是什么?
条款23 名字空间
显然,连续使用显式的限定符太乏味。缓解这种无趣状况的方式之一是使用using指令。
许多C++程序员(甚至很多还算有头脑的家伙)建议将using指令放在全局作用域中,
#include "iostream"
using namespace std;
这是个馊主意!现在我们基本上又退回起点了。在头文件中这么做尤其糟糕,因为所有包含该头文件的文件都会受到这个糟糕决策的影响。在头文件中,我们通常坚持使用显式的限定,并且仅将using指令局限于较小的作用域中(例如函数体或函数体内的某个代码块),这样,它们的效用就会受到限制并易于控制。基本来说,在头文件中要坚持表现最好,在源文件中要表现得足够好,而在函数内大可放松一下。
为什么我就以及我的同事都喜欢将using namespacestd;放到全局去,也没见出什么问题呀?
答:其实出不了问题,因为我们一般是不会产生和STL相同标识的类等。
使用using指令一个有趣的方面在于,它使得一个名字空间中的名字可用,但这种可用又不能算是绝对的可用。例,
略
using声明通常是介于冗长乏味的显式限定和不受限制地使用using指令之间的一种折衷。下面这种情况尤其适合:一个给定的代码段,只使用来自两、三个名字空间中有限的几个名字,但反复使用它们:
void aFun()
{
using std::cout;
using std::endl;
cout << a << b<< c << endl;
// 等等...
}
另一种对付冗长乏味的名字空间名字的方式是使用别名。例
namespace S = org_semantics;
于using指令一样,最好避免在头文件中使用名字空间别名。
让我们瞥一眼匿名名字空间,
namespace
{
int anInt = 12;
int aFunc() {return anInt;}
}
相当于
namespace __compiler_generated_name__
{
int anInt = 12;
int aFunc() {return anInt;}
}
using namespace __compiler_generated_name__;
这是一种避免声明具有静态连接的函数和变量的新潮方式。在上面的匿名名字空间中,anInt和aFunc都具有外部链接,但它们只能在其所在出现的翻译单元内被访问,就像静态对象一样。
这样做有什么好处呢?
我们程序中出现的bug,目前为止还没有发现是因为名字空间引起的,故此条款不重要。
条款24 成员函数查找
class B
{
public:
//...
void f(double){};
};
class D : public B
{
void f(int){};
};
int _tmain(int argc, _TCHAR* argv[])
{
D d;
d.f(12.3);
system("pause");
return 0;
}
这个条款中,只要知道上面这个例子就行了(理解隐藏)。再说,我们不会写这样的代码的,否则会被人鄙视!就算写了,编译不过也不会导致什么问题。
条款25 实参相依的查找
例
namespace org_semantics
{
class X{};
void f(const X&){};
void g(X*){};
}
int g(org_semantics::X*){return 0;}
int main(int argc, char* argv[])
{
org_semantics::X a;
f(a); // 调用org_semantics::f
g(&a); // 错误!调用具有歧义性
return 0;
}
普通的查找是不会发现函数semantics::f的,因为它被嵌套在一个名字空间内,并且对f的使用需要以该名字空间的名字加以限定。然而,由于实参a的类型被定义于semantics名字空间中,因此,编译器也会到该名字空间中检查候选函数。
以上在VS2005中通过验证,但是在VC6.0中却不是这样,即不具有(ArgumentDependent Lookup,ADL)。
像这样怪异的特性,我估计在工作中永远都碰不到,不用太关注,知道有这回事就行。
条款26 操作符函数查找
没搞懂这个条款究竟有什么用,反正这种奇怪的特性以后是用不上,最算遇到,也是编译错误,不用太关注。
条款27 能力查询
#include "assert.h"
class A
{
public:
virtual ~A(){};
};
class B
{
public:
virtual ~B(){};
};
class C : public A, public B
{
};
class D : public A
{
};
int main(int argc, char* argv[])
{
C c;
D d;
A* pA = &c;
B* pB = dynamic_cast(pA);
assert(pB);
pA = &d;
pB = dynamic_cast(pA);
assert(pB == NULL);
return 0;
}
这个条款的意思是说,我们偶尔会遇到这种横向转换的情况,也就是所谓的能力查询,上面相当于就是查询对象是否具有B的能力。
能力查询只是偶尔需要,但它们往往被过度使用。它们通常是糟糕设计的“指示器”。因此,除非找不到其他合理方式的困难境地,最好避免对一个对象的能力进行运行期查询。
在VC6.0中用到dynamic_cast就会抛出异常(在VS2005中不会),为什么?
答:参考条款9 新式转型操作符
TCC当中好像还到处都用到了dynamic_cast,有这个必要吗?我反正是不会用它,一是我水平没那么高,二是我也不愿意用,搞成那样我还不如用简单的办法解决问题呢
条款28 指针比较的含义
#include "assert.h"
class A
{
public:
virtual ~A(){};
};
class B
{
public:
virtual ~B(){};
};
class C : public A, public B
{
};
int main(int argc, char* argv[])
{
C* pC = new C;
A* pA = pC;
B* pB = pC;
assert(pA == pC);
assert(pB == pC);
//assert(pA == pB); // error C2446: “==”: 没有从“B *”到“A *”的转换
return 0;
}
这个条款的意思是,虽然pA和pC,pB和pC的绝对地址可能不一样,但是,编译器通过将参与比较的指针值之一调整一定的偏移量来完成这种比较。(合法行为,因为它们之间有继承关系,没继承关系的当然不行,如pA == pB,编译不能通过)
...
从以上的观察中,我们可以得到一个非常重要的经验:一般而言,当我们处理指向对象的指针或引用时,必须小心避免丢失类型信息。指向void的指针是常见的错误:转成void*就会丢失类型信息,编译器别无他法,只好用原始地址比较,此时
assert((void*)pB == pC); // 断言失败
这个条款在工作中估计用不到,理解就行,不用深究。
条款29 虚构造函数与Prototype模式(重点理解)
有两个主要的原因需要使用“克隆”:你必须(或者更喜欢)对正在处理的对象的“不知情”,并且不希望改变被克隆的原始对象,也不希望受原始对象改变的影响。
在C++中,提供了这种克隆对象的能力的成员函数,从传统上说,被称为“虚构造函数”。当然,并不存在什么虚构造函数,但是生成对象的一份复制品通常涉及到通过一个虚函数对其基类的构造函数的间接调用,因此,即使不是真的虚构造函数,效果上也算是虚构造函数了(说得简单点就是通常相同的接口,但可以随具体对象变化的一份拷贝)。最近,这种技术被称为Prototype模式的一个实例。例
class Meal
{
public:
virtual ~Meal(){};
virtual void eat() = 0;
virtual Meal* clone() const = 0;
//...
};
class Spaghetti : public Meal
{
public:
Spaghetti(const Spaghetti& ); // 在clone中会使用到该拷贝构造函数
void eat();
Spaghetti* clone()
{
return new Spaghetti(*this);
}
};
有了这个简单的框架,就可以生成任何类型的Meal的复制品,并且无需知道正在复制的Meal的实际类型的精确知识。注意,以下代码没有提及具体派生类,因而代码不会和任何当前(或将来出现)的Meal派生类相耦合。
这个例子例证了软件设计中"不知情"的一些优点,尤其是在用于定制和扩展的框架结构软件设计中。记住:有些事情你知道的越少越好。
上面的克隆技术在工作和学习中用过吗?举一个实际的历史
条款30 Factory Method模式重点理解
一个常见、但总是错误的方式是使用“类型编码”和switch语句:
class Employee
{
public:
enum Type{SALARY, HOURLY, TEMP};
Type type const(){return type_;};
//...
private:
Type type_;
//...
};
HRInfo* genInfo(const Employee& e)
{
switch(e.Type())
{
case SALARY:
case HOURLY:
return StdInfo(e);
case TEMP:
return new TempInfo(static_cast<const Temp*>(e));
default:
return 0; //未知的类型编码
}
}
genInfo的主要缺点:首先是必须知道的东西太多(耦合性强),不管是雇员的种类还是HRInfo的种类有变化,或者是雇员和HRInfo的对应关系有变化,你都得修改代码。另一个问题是,可能无法成功识别Employee&e实参的确切类型,从而要求调用genInfo的代码提供对错误情况的处理。
以前写代码经常遇到这种需要处理异常分支的情况,是否可以参考这儿的办法解决?
根据上面的分析,正确的方式是去考虑从每一种Employee类型到对应的HRInfo类型的映射应该置于何处。换句话说,谁最清楚一个TempEmployee需要何种HRInfo对象?理所当然,是Temp Employee自己:
class Employee
{
public:
//...
virtual HRInfo* genInfo(); // 使用FactoryMethod
//...
};
class Temp:public Employee
{
public:
//..
virtual HRInfo* genInfo() const
{
return TempInfo(*this);
}
//...
};
这是一个Factory Method模式的实例。实际上,我们不是向employee询问一系列生硬的私人问题,而是说“不管你是什么类型的employee,请为自己生成适当的HRInfo对象”:
Employee* e = getAnEmployee();
//...
HRInfo* info = e->genInfo(); // 使用Factory Method
Factory Method的本质在于,基类提供一个虚函数“挂钩”,用于产生适当的“产品”。每一个派生类可以重写继承的虚函数,为自己产生适当的产品。实际上,我们具备了使用一个未知类型的对象(例如某种Employee对象)来产生另一个未知类型对象(例如适当的HRInfo对象)的能力。
使用Factory Method模式通常意味着一个高级设计需要基于一个对象的确切类型产生另一个“适当”的对象,这样的需要往往发生于存在多个平行或几乎平行的类层次结构的情况下。FactoryMethod模式通常是治疗一系列运行期类型查询问题的良方。
从29和30两个条款,可以看出,为什么别人老是喜欢搞虚基类,老是用基类去操作具体类提供的功能呢?难道任务管理,任务那种做法真的是好吗?
对于解析场景文件那个地方的命令节点,是否可以用上面的这种号称抽象的方式搞一下?
条款31 协变返回类型
一般来说,一个重写的函数与被它重写的函数必须具有相同的返回类型:
class Shape
{
public:
virtualdouble area() const = 0;
};
class Circle : public Shape
{
public:
floatarea() const; // 错误!返回类型不同
};
然而,这个规则对于“协变返回类型”的情形来说有所放松。也就是说,如果B是一个类类型,并且一个基类虚函数返回B*,那么一个重写的派生类函数可以返回D*,其中的D公有派生于B。对于引用类似。
class Shape
{
public:
virtualShape* clone() const = 0;
};
class Circle : public Shape
{
public:
virtualCircle* clone() const;
};
当直接操纵派生类型而不是通过其基类接口来操纵它们时,使用协变返回类型的优势就体现出来了:
Circle* c1 = getACircle();
Circle* c2 = c1->clone();
如果没有协变类型,你就只得被迫强转。
协变返回类型的优势在于,总是可以在适当程度的抽象层面工作。如果我们是在处理Shape,将获得一个抽象的Shape;如果正在处理某种具体的形状类型,比如Circle,我们就可以直接获得Circle。(也就是你想要哪个抽象层面的对象,你就用哪个抽象层面的变量去接收返回值)
在现实代码中能找到使用这个技术的场景吗?
条款32 禁止赋值
即将拷贝构造函数和赋值操作符函数声明为私有的,并且可以不提供定义。
好多书上都讲了这个东西,我熟悉,从经验来看,就算我不这样做,大家都知道该不该复制,基本上不会因为这个问题出现缺陷,故此处不用太关注。
条款33 制造抽象基类
抽象基类通常用于表示目标问题领域的抽象概念,创建这种类型的对象没有什么意义的。我们通过至少声明一个纯虚函数(或者从别的类继承一个纯虚函数并且不予实现)使得一个基类成为抽象的,编译器将会确保无人能够创建该抽象基类的任何对象。
其实下面的办法就是为了实现“确保无人能够创建该抽象基类的任何对象”,只能创建继承它后的类对象。
然而,有时找不到一个可以成为纯虚函数的合理候选者,但仍然希望类的行为像个抽象基类。办法有两个
第一个是通过构造函数实现,第二个是通过析构函数实现(书: 受保护的析构函数和受保护的构造函数发挥的效果基本相同,不过是第一个报错发生在对象创建时,第二个报错发生在对象离开作用域或被显式销毁时)。
class ABC
{
public:
virtual ~ABC();
protected:
ABC();
ABC(const ABC&);
};
ABC::~ABC()
{
}
ABC::ABC()
{
}
ABC::ABC(const ABC&)
{
}
class ABC
{
//...
protected:
~ABC();
};
ABC::~ABC()
{
}
现实中有上面的这种场景吗?
另一种使一个类成为抽象基类的方式需要人为地将该类的一个虚函数指定为纯虚的。通常来说,析构函数是最佳候选者。
class AB
{
public:
virtual ~AB() = 0;// 需要提供函数实现
};
AB::~AB()
{
}
条款34 禁止或强制使用堆分配
有时候,指明一些特定类的对象不应该被分配到堆上是个好主意。通常这是为了确保该对象的析构函数一定会得到调用(比如局部对象,静态对象)。
比如呢?我在工作中怎么一次都没见到过呢。我觉得没多大意义,既然你都知道一定要调用析构,就算是在堆上,你照样能做到。
指明对象不应该分配到堆上的方式之一,是将其堆内存分配定义为不合法:
class NoHeap
{
public:
//...
protected:
void* operator new(size_t){return 0;}
void operator delete(void*){}
};
这样,new NoHeap或者new从NoHeap继承的类都编译不能通过。之所以给出operator new和operator delete 的定义,是因为在一些平台上它们可能会被构造函数和析构函数隐式地调用(也就是说,不给出定义的话,会搞得局部对象或静态对象都没法产生)。
同时,还要注意阻止在堆上分配NoHeap对象的数组。在这种情况下,只要将array new和arraydelete声明为private且不予定义即可。
class NoHeap
{
public:
//...
protected:
void* operator new(size_t){return 0;}
void operator delete(void*){}
private:
void* operator new[](size_t);
void operator delete[](void*);
};
当然,某些场合,我们可能鼓励而非阻止使用堆分配,为此,只需将析构函数声明为private即可:
class OnHeap
{
~OnHeap();
public:
void destroy()
{
delete this;
}
};
有这种使用场景吗?我很怀疑,对工作基本上没有任何意义。
条款35 placement new
直接调用构造函数是行不通的,然而,可以通过使用placement new来“哄骗”编译器调用构造函数。这个东东简单一点说就是把对象创建在一个已经分配好的内存上,不需要调用delete而是需要自己显式调用析构。
这个条款在其他书上见过,此处略。知道有这回事就行。
这种方式快速而灵活,它并不指望被普通大众所知晓。这一基本技术(以更高级的形式)广泛应用于大多数标准库容器的实现。
能不能在标准库中找个具体的例子?
条款36 特定于类的内存管理
如果不喜欢标准的operator new和operator delete对某个类类型的处置方式,不必郁闷地忍受。该类型完全可以拥有量身定制的operatornew和operator delete,以满足其特殊要求。
更确切地说,成员operator new和operator delete之所以是静态成员函数,是因为前者被调于类对象构造之前,而后者则被调用类对象析构之后,在两种情况下,对象均不处于有效的状态,因此也就无this指针可用。
如果在基类中定义了成员operator new和operator delete,要确保基类析构函数是虚拟的。如果基类析构函数不是虚拟的,那么通过一个基类的指针来删除一个派生类对象的结果就是未定义的。
还要注意的是,我么使用了一个带有两个参数的operator delete而不是普通的单参数版本的operatordelete。
class Handle
{
public:
~Handle();
void* operator new(size_t);
void operator delete(void*);
};
class MyHandle : public Handle
{
public:
void* operator new(size_t);
void operator delete(void*, size_t); // 注意第二个参数
};
一个常见的误解是以为使用new和delete操作符就意味着使用堆内存,其实并非如此。使用new操作符唯一的暗示是名为operator new的函数将被调用,且该函数返回一个指向内存块的指针。没错,标准、全局的operatornew和operator delete的确是从堆上分配内存,但成员operator new和operator delete可以做它们想做的任何事情。对于分配的内存到底从哪儿来没有任何限制:它可能来自一个特殊的堆,也可能来自一个静态分配的块,也可能来自一个标准容器内部,也可能来自某个函数范围的局部存储区。要说对于这个内存从哪儿来确实存在什么限制因素的话,那就是你的创造力和判断力了。
条款37 数组分配
从逻辑上来说,只要声明了非数组形式的函数(即operator new和operator delete),就应该为这些函数声明数组的形式,这是个好主意(然而奇怪的是,在日常编程实践中,这一点往往被人们所忽视)。如果目的是调用全局的数组分配操作,那么,定义“仅仅转发对全局形式的调用”的局部形式,可以让事情变得更清晰:
class Handle
{
public:
void* operator new(size_t);
void operator delete(void*);
void* operator new[](size_t n)
{
return ::operator new(n);
}
void operator delete[](void* p)
{
::operator delete(p);
}
};
如果目的是不鼓励分配Handle数组,那么可以将数组形式的函数声明为私有的并且不提供定义。
有不鼓励分配数组的情况吗?
int* p1= new int; // 隐式调用调用operator new,相当于operator new(sizeof(T)),由编译器决定需要多少内存
int* p2= static_cast<int*>(operator new(sizeof(int))); // 显式调用,和上面的是等价的,但我们得明确指明分配的字节数
// 同理
int* p3= new int[4]; // 请求内存为sizeof(int)* 4 + delta字节
int* p4= static_cast<int*>(operator new(sizeof(int) * 4));
所请求的额外空间一般用于运行期内存管理器记录关于数组的一些信息,这些信息对于以后回收内存是必不可少的。不过,事情远没有这么简单,编译器未必为每一个数组分配都请求额外的空间,并且对于不同的数组分配而言,额外请求的内存空间大小也会发生变化。
请求内存数量上的区别通常只在编写非常底层的代码时才需要考虑,在这种情况下,数据的存储区被直接处理。如果打算编写底层代码,通常最简单的做法是避免直接调用arraynew以及编译器所执行的有关干预,取而代之的是,使用平凡朴素的operator new。
条款38 异常安全公理
公理1:异常是同步的
这段连翻译者都没完全明白,翻译者的理解如下:归纳起来,这段话的中心思想是函数调用可能会抛出异常。我对异常的同步性的理解是,如果程序在某一点抛出了一个异常,那么在该异常被处理之前程序将不会继续执行下去,即程序执行流必须等待异常处理完成。
公理2:对象的销毁是异常安全的
该公理并非建立于技术基础上,而是建立于“社会”基础上。按照惯例,析构函数、operator delete以及operator delete[]不会抛出异常。也就是说
X::~X
{
try
{
delete ptr1;
delete ptr2;
}
catch (...){}
};
这种做法是不必要的,也是不可取的,道理很简单,这种对“为社会所不容”的畏惧心理同样也会影响到ptr1和ptr2所指向的对象的析构函数以及operator delete的作者。可以利用这种普遍的畏惧心理让代码变得更简洁一些:
X::~X
{
delete ptr1;
delete ptr2;
};
要是析构函数中调用了其他的函数会抛出异常咋办呢?难道析构函数中只能做释放对象的工作?
公理3:交换操作不会抛出异常
这同样是一个建立于C++社群共识之上的公理。......,尤其是在STL的实现中。无论何时只要执行一个sort、reverse、partion以及其他许多操作,都会涉及到交换操作。一个异常安全的交换对于保证这些操作同样是异常安全的大有助益。
条款39 异常安全的函数
首先做任何可能会抛异常的事情(但不会改变对象重要的状态),然后使用不会抛出异常的操作作为结束。例
新手写的代码
void Button::setAction(const Action* newAction)
{
delete action_;
try
{
action_ = newAction->clone();
}
catch (...)
{
action_ = 0;
throw;
}
}
异常安全的代码
void Button::setAction(const Action* newAction)
{
Action* temp = newAction->clone();// 首先做可能会抛出异常的事情,但不改变action_的状态
delete action_; // 然后改变状态
action_ = temp; // 虽然多用到一个临时变量temp,但这个更好
}
有必要提醒一下:编写正确的异常安全的代码其实很少使用try语句!一个新手在尝试编写异常安全的代码时,往往会使用不必要的甚至有害的try和catch语句块,从而把代码搞得乱七八糟。
只要可能,尽量少用try语句块是一个好主意。主要在这种地方使用它们:确实希望检查一个传递的异常的类型,为的是对它做一些事情。在实践中,这些地方通常是代码和第三方的库之间、以及代码和操作系统之间的模块分界处。
这种方式运用在我们的代码中说不定能简化代码?
条款40 RAII
其实在<
RAII技术在C++编程中的影响是如此深远,以至于很难发现有哪个库组件或大型代码块未以某种风格使用它们。(这句话我同意)
class Trace
{
public:
Trace(const char* msg):msg_(msg)
{
std::cout << "Entering " << msg_ << std::endl;
}
~Trace()
{
std::cout << "Leaving " << msg_ << std::endl;
}
private:
std::string msg_;
};
在SIP呼叫器的代码中,黄建搞了这个,说不定是看了这本书搞的,呵呵!估计在调试死锁时还是有些作用。
条款41 new、构造函数和异常
为了编写完美的异常安全代码,保持对任何分配的资源的跟踪并且时刻准备着当异常发生时释放它们,是必不可少的。这个过程很简单,我们既可以将代码组织成无需回收资源的方式(参见条款39),也可以使用资源句柄来自动回收资源(参见RAII)。在极端情况下,还可以采用try,但这应该视作一个例外,而不应当被当作应该奉行的规则。
然后作者提出了一个问题,就是当
String* title = newString("Kicks"); // 使用成员operatornew,如果提供了的话
String* title = ::new String("Kicks");// 使用全局的new
时,我们搞不清楚究竟是分配内存出的异常还是构造函数出的异常。作者搞了一个将内存分配和调用构造函数分开的办法,显然有时不能搞定。还说,这是一个完美的反例,告诉我们,过于聪明的代码一开始或许可以工作,但在将来可能会因一些细微的改变而失败,而且失败的原因颇为微妙。
幸运的是,编译器可以帮我们处理这种情形,并且生成执行方式与上面手工编码方式相同的代码。不过有一个例外,它将会调用与执行分配任务的operatornew相对应的operator delete。
特别地,如果分配时采用了成员operator new,那么,如果String构造函数抛出了一个异常,则相应的成员operatordelete将会被调用,进行存储区的回收工作。这又是一个好理由:如果声明了一个成员operator new,那么最好也声明一个成员operatordelete。
对于全局的new和数组new也是同样的道理。
这个条款我们只要知道道理就行了,在工作中用处不大,因为编译器已经帮我们搞定了。
条款42 智能指针
学会使用和熟悉STL中auto_ptr的用法就行了。
为什么我老是害怕智能指针出错不肯用它呢?用它能得到什么好处呢?
答:只要你看了源代码你就敢大胆的用了
template
classauto_ptr {
public:
typedef_Ty element_type;
explicitauto_ptr(_Ty *_P = 0) _THROW0()
:_Owns(_P != 0), _Ptr(_P) {}
auto_ptr(constauto_ptr<_Ty>& _Y) _THROW0() // 拷贝构造和赋值都会引起源auto_ptr对象释放拥有的资源
:_Owns(_Y._Owns), _Ptr(_Y.release()) {}
auto_ptr<_Ty>&operator=(const auto_ptr<_Ty>& _Y) _THROW0() //这是我老担心的一个操作,现在不怕了
{if(this != &_Y)
{if(_Ptr != _Y.get()) // 如果目的和源指向的是同一个对象,则源对象不用delete,否则就重复delete了
{if(_Owns)
delete_Ptr;
_Owns= _Y._Owns; }
elseif (_Y._Owns)
_Owns= true;
_Ptr= _Y.release(); }
return(*this); }
~auto_ptr()
{if(_Owns)
delete_Ptr; }
_Ty&operator*() const _THROW0()
{return(*get()); }
_Ty*operator->() const _THROW0()
{return(get()); }
_Ty*get() const _THROW0()
{return(_Ptr); }
_Ty*release() const _THROW0() // 释放自己拥有的指针,并返回原来的指针
{((auto_ptr<_Ty>*)this)->_Owns = false;
return(_Ptr); }
private:
bool_Owns; // 代表是否拥有一个合法的指针
_Ty*_Ptr; // 所拥有的指针
};
实际上,_Owns主要是用来记录是否可以delete _Ptr,因为C++标准是允许deleteNULL的,所以,在VS2005中,已经去掉了_Owns成员,在原来需要判断_Owns的地方,都是直接操作了。
条款43 auto_ptr非同寻常
auto_ptr有很多好处。首先它非常高效。你不可能使用内建指针编写一个比auto_ptr性能还好的解决方案。其次,当auto_ptr离开作用域时,其析构函数将会释放它指向的任何东西。auto_ptr的第三个好处是,在类型转换方面,其行为酷似内建指针。
不过看起来,除第二点外,其他的都不算好处。
然而,有两种常见的场合应该避免使用auto_ptr。首先,它们永远都不应该被用作容器元素。其次,一个auto_ptr应该指向单个元素,而不应该指向一个数组。原因在于,当auto_ptr所指向的对象被删除时,它使用opertaordelete而array delete来执行删除操作。
条款44 指针算术
已知晓,略。
条款45 模板术语
精确地使用术语对于任何技术领域来说都很重要,对于程序设计领域尤其如此,对于C++程序设计领域更是这样,而在C++模板编程领域则达到极致。
尤其要区分模板参数(template parameter)和模板实参(templateargument),前者用于模板的声明中,后者则用在模板的特化中:
template
class Heap{...};
Heap
还要小心区分模板名字(template-name)和模板id(template-id)之间的区别,前者只是一个简单的标识符,后者则是指附带有模板实参列表的模板名称。
Heap 模板名字
Heap
为数众多的C++程序员将术语实例化(instantiation)和特化(specialization)混为一谈。对模板的特化是指当你以一套模板实参供应给一个模板时所得到的东西。特化可以显式进行,也可以隐式进行。举个例子,当我们写Heap
我的理解:通俗一点讲,实例化就是指编译器帮我们生成了具体的类,这个具体类是我们不能看见的。而特化呢,指类中不再有模板参数的时候。
对于实例化和特化我一直都是混为一谈。丢人!
条款46 类模板显式特化
// Heap主模板,也就是平常我们用得最多的
template<typename T>
class Heap
{
public:
void push(const T& val);
T pop();
bool empty() const{ return h_.empty(); }
private:
std::vector
};
这个类模板实现对于许多类型的值来说都工作得很好,但在处理指向字符的指针时碰了钉子。我们可以提供一个针对指向字符的显式特化版本(针对Heap主模板而言)来解决这个特别的难题:
template<>
class Heap<const char*>
{
public:
void push(const char* val);
const char* pop();
bool empty() const{ return h_.empty(); }
private:
std::vector<const char*> h_;
};
这样,除了类模板名字相同外,其它的函数之类的,咱都可以全部重新定义了,这样便解决了主模板不能处理类型const char*的问题。
其中的模板参数列表是空的,但要特化的模板实参则附在模板名字后面。让人惊讶的是,这个类模板显式特化版本并不是一个模板,因为此时没有剩下任何未指定的模板参数了。出于这个原因,类模板显式特化通常被称为“完全特化”,以便与局部特化区分开,后者是一个模板。
记得以前在代码中见到过template<>形式的,类模板,出于一直没搞懂这是什么语法,现在明白了。多在库代码中看看完全特化的应用场景都在哪些地方?
使用这个技术的好处有哪些?不会是仅仅因为这样可以使用同一个名字,客户代码调用方式可以保持一直吧?
在完全特化时,因为所有的代码都被我们写出来了,当然编译器不用再帮我们实例化什么了,这也就是作者说的,特化不一定会被实例化。
条款47 模板局部特化
不能对函数模板进行局部特化。C++语言目前还不支持这个特性(尽管有朝一日可能会支持)。所能做的就是重载它们。因此,在这个条款中,我们只考虑类模板。
类模板局部特化的工作方式很简单。就像完全特化一样,首先需要一个通用的主模板来进行特化。
// 局部特化
template<typename T>
class Heap
{
public:
void push(const T* val);
T* pop();
bool empty() const{ return h_.empty(); }
private:
std::vector
};
什么时候会用到这个技术?
条款48 类模板成员特化
关于类模板显式特化和局部特化一个常见的误解是,一个特化版本会以某种方式从主模板那里“继承”一些东西。事实并非如此。对于主模板而言,类模板的完全特化或局部特化全然是单独的实体,它们不从主模板“继承”任何接口或实现。然而,从非技术的意义来说,一个特化版本确实从主模板那里继承了一套有关接口和行为的“期望”,因为用户在根据主模板的接口编写泛型代码时,通常期望所编写代码同样可以处理特化的情形。
这就意味着一个完全特化或局部特化通常必须重新实现主模板的具备的所有能力,即使只有部分实现需要定制也得如此。一个替代的方式是只去特化主模板成员函数的一个子集。举个例子,考虑主模板:略
针对从const char* 的完全特化Heap替代了主模板的全部实现,其实对于字符指针的堆来说,主模板Heap的私有成员和empty成员函数已经完全够用了,我们真正要做的全部事情就是特化push和pop成员函数。
template<>
void Heap
{
h_.push_back(pval);
std::push_heap(h_.begin(),h_.end(), strLess);
}
pop略
类模板成员特化用得多吗,在哪儿有代码例子?
条款49 利用typename消除歧义
template<typename Cont>
void fill(Cont& c, Cont::ElemT a[], int len)
{
for(int i = 0; i < len; i++)
{
c.insert(a[i]);
}
}
上面这个代码是编译通不过的。嵌套的名字Cont::ElemT并不被识别为一个类型名字!其中的问题在于,在fill模板的上下文中,编译器没有足够的信息来决定嵌套的名字ElemT到底是一个类型名字,还是一个非类型的名字(比如说一个枚举值)。C++标准约定,在这种情形下,嵌套的名字被假定为一个非类型的名字。
为了对付这种情形,我们有时必须明确地通知编译器,某个嵌套的名字是一个类型名字:
template<typename Cont>
void fill(Cont& c, typename Cont::ElemT a[], int len)
{
for(int i = 0; i < len; i++)
{
c.insert(a[i]);
}
}
在这里,使用typename关键字明确地告诉编译器,接下来的限定名字是一个类型名字,从而允许编译器去正确解析模板。
但为啥
template<typename T>
void aFuncTemplate(T&arg)
{
T::ElemT var;
cout << var<< endl;
};
编译能通过呢?
条款50 成员模板
在了解成员模板之前,先了解一下模板成员的概念:
template
bool SList
{ return head_ == 0;}
template
struct SList
{
Node*next_;
T el_;
};
成员empty和Node是模板成员的例子。
但一个类模板(甚至一个非模板的类)还可以有成员模板。遵照同义反复的传统,我们说,一个成员模板就是一个自身是模板的成员:
template <typename T>
class SList
{
public:
template<typename In>SList(In begin, In end);
};
template<typename T> // 这一个针对SList
template<typename In> // 这一个针对成员
SList
{
while(begin != end)
{
push_front(*begin++);
}
reverse();
}
使用时
float rds[] = {4.5f, 7.8f, 0.0f};
const int size = sizeof(rds) / sizeof(rds[0]);
std::vector
//...
SList
SList
在STL中,这是一个常见的构造函数模板应用,以便允许一个容器可以通过从任一个数据源中抽取的一系列值进行初始化。关于成员模板的另一个常见应用是生成类似复制操作的构造函数以及赋值操作符:
template<typename T>
class SList
{
public:
SList(const SList& that); // 复制构造函数
SList& operator=(const SList&rsh); // 复制赋值操作符
template<typename S>
SList(const SList&that);
template<typename S>& SList&
operator=(const SList&rhs);
};
请注意上面描述的“类似复制构造函数”和“类似复制操作”这两个无聊的短语。这是因为模板成员永远不会被用于实例化一个赋值操作。确切地说,如果上面T和S是同样的类型,那么编译器将不会实例化成员模板,它自己将会“编写”一个复制操作。在这种情形下,为了明确地防止编译器多管闲事(往往是在帮倒忙),通常最好明确地定义复制操作,如上所示。
任何非虚的成员都可以写成模板(成员模板不能是虚拟的,因为这些特性的组合会导致在其实现中存在难以克服的技术问题)。
条款51 采用template消除歧义
在条款“采用typename消除歧义[条款49]”中,我们看到有时必须明确地告诉编译器,某个嵌套的名字是一个类型名字,这样编译器才能正确地执行解析工作。对于嵌套的模板名字来说,存在同样的情况。
这方面典型的例子出现于STL配置器的实现中。
找时间好好看看STL?
配置器是一种类类型,用于为STL容器定制内存管理操作。配置器通常采用类模板实现:
template<class T>
class AnAlloc
{
public:
//...
template<class Other>
class rebind
{
public:
typedef AnAlloc
};
//...
};
template<typename T, class A = std::allocator
class SList
{
//...
struct Node
{
//...
};
//typedef A::rebind
typedef typename A::template rebind
//...
};
再一次,问题出在此时编译器没有关于类型名字A的信息。那个typedef必须改写如下
typedef typename A::template rebind
关键字template 的使用等于告诉编译器,rebind是一个模板名字,而typename的使用则等于告诉编译器,整个这一坨东西表示的是一个类型名字。很简单,不是吗?
条款52 针对类型信息的特化
类模板显式特化和局部特化通常用于生成主模板的一些版本,这些版本根据具体的模板实参或模板实参的类定制而成。然而,这些语言常常也被以相反的样式使用,即,不是基于类型的属性生成特化版本,而是从一个特化版本中推导出类型的属性。让我们看一个简单的例子:
template<typename T>
struct IsInt
{
enum {result = false};
};
template<>
struct IsInt<int>
{
enum {result = true};
};
template<typename X>
void aFunc(X& arg)
{
IsInt
};
也就是说,只要以int去特化IsInt就会采用template<>版本,那么其成员result就等于true了,否则都是false。这是一个简单的众所周知的模板元编程的例子,确切地说,就是利用模板实例化机制,在编译器执行一部分计算。
在编译器向类型询问此类问题,是许多重要的优化机制和错误检查技术的基础。当然,知道一个特定类型是否恰好是一个int,其作用很有限。但是,知道一个类型是否为一个指针应该更有普遍的意义,因为一些实现往往根据所处理的类型到底是指向对象的指针还是对象本身而采取不同的形式。
这么高级的语言特性有人使用吗,比如在STL中?
条款53 嵌入的类型信息
我们怎么才能知道一个容器中元素的类型呢?
例
template
class Seq
{
public:
typedefT Elem; // 元素的类型
typedefT Temp; // 临时对象的类型
//...
};
通过typedef,我们就可以Seq
template
typename Container::Elemprocess(Container& c, int size)
{
//...
}
像vector
条款54 traits
没看懂,先看看STL中的trait吧
条款55 模板的模板参数
在标准容器中,经常都是这种形式
template
class Stack
{
//...
};
在使用时
Stack
但是,当我们不小心搞错了的时候
Stack
这样就会出现问题,有没有更好的方式呢?解决办法如下
template
class Stack
{
//...
private:
Cont
};
这样,我们在使用时,就可以
Stack
Stack
不用再担心int和unsigned不一致的情况了。
其实我倒挺喜欢Stack
template<class _Ty,
class _Container = deque<_Ty>>
class stack
{ // LIFO queueimplemented with a container
条款56 policy
这个条款说得简单一点,就是将可能发生变化的操作封装成一个模板参数,将变和不变分离。和
template
InputIterator find_if(
InputIterator _First,
InputIterator _Last,
Predicate _Pred
);
最后一个模板参数的道理几乎是一摸一样的。
唯一不同的就是对于find_if,如果调用_Pred形式是固定的,需要我们去适应,即我们只能搞成全局函数或重载()操作符。
而这个地方呢,代码全是用户写的,不管怎么调用策略函数都可以,所以我们的策略函数可以是任何形式,比如静态成员函数,重载()操作符,估计全局函数也是可以的,随便怎么整。
例
template <typename T, template<typename>class DeletionPolicy>
class Stack
{
public:
~Stack()
{
for (I i(s_.begin()); i != s_.end(); ++i)
{
DeletionPolicy
}
}
private:
typedef std::vector
typedef typename C::iterator I;
C s_;
};
template <typename T>
struct PtrDeletePolicy
{
static void doDelete(T ptr)
{
delete ptr;
}
};
// 可以在这儿定义不同的策略
// ...
Stack
实际上,find_if就是一种策略处理方式,好好体会!再者,感觉模板这玩意,除了在标准模板库中(我们自己写的库也有可能)有大量应用场景外,其他具体到某方面的业务时,我们根本不需要这种灵活性,反正代码都没什么重用性,直接写死可能更简单。换句话说,模板可能主要用在重用性较高的库中!平常我们用不到,不要以为自己水平不行,因为我们根本就不需要模板技术。我们应该根据业务去选择技术,而不是为了技术而将业务复杂化了。
条款57 模板实参推导
类模板必须显式地予以特化。例如
Heap
函数模板也可以被显式特化。例如
int a = cast
然而,典型的做法(同时也是更方便的做法)是让编译器根据函数调用中的实参类型推导出模板实参。这在情理之中,这个过程称为模板实参推导(templateargument deduction).
template
T min(const T&a, const T& b)
{
return a< b ? a : b;
}
int a = min(12, 13); // T 是int
double d = min('\b', '\a'); // T是char
char c = min(12.3, 4); // 错误!T不能既是double又是int
上面错误的那一行代码,原因在于编译器无法在模棱两可的情形下推导出模板实参。在这种情形下,我们总是可以用显式的方式告诉编译器,模板实参究竟是什么:
d = min
与重载解决方案一样,编译器在模板实参推导期间只检查函数实参的类型,对可能的返回类型并不作检查。因此,要想让编译器知道返回类型,除了明确告诉它,别无他法。
这个条款没什么重要的,仅仅就是讲模板实参推导这个概念,不过,以前还真不知道函数模板可以显式特化。
条款58 重载函数模板
一个函数模板可以被其他函数模板和非模板函数重载。这种能力非常有用,但容易被滥用。
我们见过这种用法吗?
函数模板和非函数模板之间的主要区别在于对实参隐式转换的支持度。非模板函数允许对实参进行广泛的隐式转换,从内建转换(例如整形提升)到用户自定义转换(借助未标记以explicit的单参构造函数和转换操作符)。对于函数模板来说,由于编译器必须基于调用的实参类型来执行模板实参推倒,因此只支持一些琐细的隐式转换,这包括涉及外层修饰(例如从T到constT,或从const T到T)、引用(例如从T到T&)以及从数组和函数退化的指针(例如从T[42]到T*)等情形。例
template
void g(T a, T b) // 这个g是一个模板
{}
void g(char a, char b)
{
}
g(12.3, 45.6); //使用模板的g
g(12.3, 45); // 使用非模板的g
条款60 泛型算法
略,感谢没讲什么实质性东西
条款61 只实例化要用的东西
在C和C++中,如果没有调用一个已声明的函数(或试图获取其地址),那么就不需要定义它。对于类模板的成员函数来说存在类似情形。如果实际上并没有调用一个模板的成员函数,那么该成员就不会被实例化。
从这个规则可以得出的一个更重要的结论:用来特化一个类模板的模板实参,并不一定要使得该类模板的所有成员函数都能被合法地实例化。有了这个规则,我们就能够编写灵活的类模板,它可以处理范围广泛的实参,即使一些实参可能导致某些成员函数进行错误的实例化。如果没有真正调用这些错误的函数,它们就不会被实例化,也就不会导致出现错误。
这项技术通常用于类模板的设计中,从而使类模板尽可能灵活,但又不会灵活得过了头。
在现实的代码中,有这种用法吗?
条款62 包含哨位
这个条款其实就是我们熟悉的头文件重复包含问题,略。
条款63 可选关键字
在一个重写的派生类函数中,省略virtual关键字是否为一个良好的编程实践,意见分为两派。一些权威人士声称使用本质上并不必要的virtual关键字,有助于为人们提供有关派生类函数性质的文档。另外一些专家则声称这种做法纯粹是浪费精力,并可能导致一个非重写的派生类函数一不留神被误标为virtual。不管赞成哪一种意见,最好保持统一,即要么对每一个重写的派生类函数使用virtual关键字,要么省略不写。
我个人还是觉得第一种好,第二种观点非常不实际,误标,算什么事嘛
当声明operator new、operator delete、array new以及arraydelete成员时,static关键字是可选的,因为这些函数是隐式静态的:
class Handle
{
public:
void* operator new(size_t);
void operator delete(void*);
void* operator new[](size_t n);
void operator delete[](void* p);
};
一些权威人士声称,在这一点上应该尽可能明确,即总是将这些函数显式地声明为static。另外一些人士则认为,如果使用或维护此类C++代码的人不知道这些函数是隐式静态的,那么它们根本不配使用或维护这些代码。作者的观点比较中庸,即要么全不加,要么全加。
我赞成第二种,即全都不加。
在一个模板的头部,关键字typename和class可以互换使用,表示一个模板参数是一个类型名字,在这种上下文中,二者含义无任何区别。然而,许多专家级的C++程序员使用typename向人们指明该模板实参可以是任何类型,而使用class关键字来指明该类型实参必须是一个类类型。
但是我以前看的有书上说,应该完全用typename代替class,究竟那种更好呢?
...
不过我坦诚这一点:register和auto可以用在一些模糊难解的场合下,借以消除一些编写得及其糟糕的代码之语法歧义。不过话又说回来,在这些情况下,正确的做法是编写更好的代码并且避免使用这些关键字。
我也不能十分确定该怎么使用register和auto?有空到别的库代码中搜一下,看看能不能找到什么蛛丝马迹。
参考MSDN:
The register keyword specifies that the variableis to be stored in a machine register, if possible.
However, all other semantics associated with theregister keyword are honored(vt.兑现).
The auto storage-class specifier declares an automatic variable, avariable with a local lifetime. It is the default storage-class specifier forblock-scoped variable declarations.