http://blog.163.com/leonary_dy/blog/static/405528602009122103416862/
虽然很多人一再强调语言细节不重要,可我还是要花时间重读经典。上次我认认真真读这本书还要追溯到两年前,现在我对C++的理解更深了一层,可以从书中读到一些两年前无法领悟的东西。
声明:本笔记只记录我以前不了解的部分,请初学者不要作为书籍摘要
我看的是TCPL电子版第三版,June 1997第一次印刷。这是网上最常见的版本,前两页书皮虽然写着special版,其实后面版权信息那里写的是第三版,书皮应该是有人后加上去的。电子书虽便宜,不过因为是初版,错误在所难免,类似auto_ptr的错误这种地方(详见下面链接)。如果以前没看过这本书,我建议买一本新版的纸质书,从权威、厚度、质量 来看性价比都远超Effective 系列
http://blog.163.com/leonary_dy/blog/static/40552860200891123358999/
2009-2-22
前面三章随便翻翻跳过,太基础了
4.4 节开头,“把unsigned int作为一个(32位的:我注)bit数组很合适,但是为了获得更大的正数范围而使用无符号整型并不是好办法,这可能会因为隐式类型转换带来一些莫名其妙的问题”。其实这是有道理的,毕竟最大的unsigned int比signed int只大一倍而已,如果signed int可能溢出,unsigned同样很危险。可是标准库里面用的size_t公然定义为unsigned int,给代码编译带来了N多warning,不爽啊不爽
0开头的表示八进制,0x开头表示16进制
4.6节,BS爷爷说char至少有8bit,我分明记得NetMM在水木说过标准规定的下限是7bit,鉴于此问题不严重,哪天无聊了再去翻翻最新的标准吧。wchar_t在C当中是一个宏,C++当中是一个关键字。在VC9当中有一个编译选项可以开关
头文件<limits>中可以找到各种上下限
枚举类型是有长度限制的,对其做算术运算的时候需要留意一下。不过我在VC9上试了一下,/01 参数编译出来的一个{0,1} 枚举sizeof是4,/0d 当然也是4了。
4.9.3 讲了一段命名规则的问题,使用范围广的名字应该长一些,很local的名字应该短一些,或许我们可以考虑通过某个名字的使用范围和频率的哈夫曼编码确定其长度进行重构
4.9.6 Objects和左值,这一段有些莫名其妙,两者没有太大关系呀
附录C3.4的最后结论我并不同意,全部使用char来避免char之间的赋值转换并不是好办法,移植的时候还是char的不确定性带来的问题更多。实际上还是应该彻底避免用char进行运算,在java游戏的时代内存很紧张,不得不省吃俭用,遗留代码中落下了很多不必要的麻烦。
5.6节最后 "Pointers to functions (§7.7) and pointers to members (§15.5) cannot be assigned to void*s." 为什么?为什么??
2009-2-24
6.1 节BS为我们展示一个计算器,实质上是一个词法分析器,结构的层次化很严谨,从语法的角度来讲我看这段代码应该富富有余,但是实际上没那么简单。如何把一个繁琐并不复杂的东西写的简洁清晰,这需要深厚的功底,这类的东西我向来是比较头大的。
6.2.2 节提到了赋值顺序的问题,这其实是一个很严肃的问题,BS说类似
v[i] = i++;
这样的代码编译器应该作出警告,但是我把VC9开到W4也没有警告。
, && || 这三个操作符可以保证左操作数的赋值先于右操作数,这其实是一个序列点概念(sequence point),BS在这里没有进一步的展开讲解,考虑到这本书的定位和目的。操作符优先级曾经是N多脑残考官的最爱,大多数都不是问题,例外在于&& 和 || 之间是有顺序的,并不像我以前想象的那样同级关系从左至右依次计算。在a || b && c的时候,该加括号的地方要加括号。
2009-2-25
6.2.8节,提到了 T(v) 相当于static_cast<T>(e),而上面讲的(T)e 则是根据上下文选择三种cast之中的一种(不会dynamic_cast),我一度还认为T(v) 和(T) v 是等价的
6.3.3 BS爷爷说他也跟我一样不喜欢do-while循环
7.2.1节又是轻描淡写的提到了一个很诡异的地方,和参数类型不一致的常量在参数传递的时候可以绑定到const引用类型的临时变量上,这是临时变量的用途之一,涉及到临时变量的生命周期、隐式类型转换等问题。
前两天发现临时对象的一个问题: 对于T a = v ; 在C++标准当中要求语义上生成一个临时对象再拷贝构造a,而不是T a(0)这种直接构造a。然而VC扩展当中不检查T的拷贝构造函数,即使拷贝构造的参数非const也可以编译通过。打开/za选项就是按照标准的说法给出error
隐式类型转换当中的类型提升不包括int=》long的转换,需要注意。BS爷爷在前面说过如果没事只用int做整型就好,除非历史遗留问题,不要用short long之类
2009-2-28
8.2. 类名也是namespace的一种,由于类的声明、定义先入为主,导致后来我接触namespace时在这一点上我的理解不够深。namespace 定义的scope和class定义的scope有很多相似之处,声明必须在scope标记的 {} 当中,定义可以用namespace::name 形式。同一scope内的名字引用时不需要namespace:: 前缀
using namespace xxxxxx; 是个不太好的习惯,失去了namespace应有的意义
using xxxxspace::xxxx;才是正确的做法。
BS在8.2.3节肯定了我的看法,但是很遗憾我们现在的代码中充斥了这样的用法,也许对于一个十万行量级的项目来说,这种用法带来的方便足以抵消引入的问题。在8.2.8节写到:
理想情况下,namespace应该:
1 xxxxx
2 xxxxx
3 不应该让用户使用太过繁琐
如果很繁琐,那实际上暴露出设计上的缺陷
8.2.6 很通俗、简要的介绍了ADL(Argument Dependent Lookup),也叫做Keoniglookup,在C++标准当中唯一一个以人名命名的规则,Andrew Koenig。这个规则实际上在模板部分有着更为深刻的应用背景,但在本节没办法讲的那么复杂。详细情况请自行谷歌,譬如下面这个
http://blog.pfan.cn/vfdff/35154.html
2009-3-4
最近两天看的东西开始让我怀疑以前究竟有没有读过TCPL,很多明明在书上写着的东西我到现在才知道
9.2 节讲了Linkage的一些规则,9.2.1小标题前面提了一句说在C和早期的C++当中static的意思是“internal linkage”,新写代码就不要把static这样用了。非extern的const变量就可以保证internal linkage,如果需要变量可以使用匿名的namespace,当然使用全局变量这本身就是一个很土的做法,很容易被人鄙视的。水木精华区x-5-6-38提到匿名namespace 中的变量是external linkage
http://www.newsmth.net/bbsanc.php?path=%2Fgroups%2Fcomp.faq%2FCPlusPlus%2Ffaq%2Flinker%2FM.1149790415.l0
ODR(one-definition-rule),本应该规定自定义类型、模板在一个程序中只能定义一次。但是由于头文件的问题,这个规则在C++当中就变成了同一类型的定义在一个编译单元中只有一次,在不同编译单元中必须一致。
2009-3-7
10.2.4节 静态成员变量可以是自身类型,也就是说:
class Date{
static Date default_data;
};
这样的代码看起来很新颖,我以前知道的只有通过指针嵌套,不过这种用法的使用范围似乎没有指针那么多。
10.4.6.3 节“Note that the default copy constructor leaves a reference member referring to the same object in both the original and the copied object. This can be a problem if the object referred to is supposed to be deleted.” 就是我前面博客中写的“关于引用类型的成员变量”
http://blog.163.com/leonary_dy/blog/static/40552860200871811858224/
看到这里的时候我应该感叹英雄所见略同,还是自己以前读TCPL太过肤浅呢。
10.4.9节最后讲的使用一个静态函数初始化,通过一个静态变量标记是否已经初始化。可是这句话“the really difficult case is the one in which the first operation may be time- critical so that the overhead of testing and possible initialization can be serious.” 我没看明白这句话说的是什么问题,以及21.5.2相比10.4.9这个办法优越在哪里。留待以后再说吧,看了一下VC的初始化全局变量cin 的时候使用的也是编译器提供的特性,21.5.2也说编译器的实现要更为简单可靠。
11.2.4 最后这里的描述似乎有些问题,成员函数的操作符重载和全局函数的操作符重载是可以ambiguous的,并不存在成员函数高于全局函数的问题。并且一视同仁也避免了同名函数之间的“隐藏”。
但是有一组operator是例外,那就是new和delete系列,成员operator new和delete的优先级高于全局的new、delete,内存分配操作符还有一点与众不同之处在于他们都是静态的,即使没有显式static声明。
11.3.4 节,为什么拷贝构造的参数必须是引用? 因为参数传递本身就会涉及到一次拷贝,如果不是引用类型那拷贝构造将成为一个无限递归调用!!
下面关于complex x=2;的描述可能有些问题,详见水木C++版111文,太细节,不看也罢
11.4 Conversion Operators 隐式类型转换,比较容易出问题的一个地方,但是用于写一些底层工具的话又非常方便,也不可避免,用的时候需要多加小心。11.3.5最后提到成员操作符只 能用于左值变量,并不会通过隐式类型转换生成一个临时对象,这大约是为了避免隐式类型转换不小心带来的问题吧。
2009-3-14
11.4.1 最后的那个例子,可以实现某种程度上的按返回值重载函数,详情见RC的这个帖子
http://www.newsmth.net/bbsanc.php?path=%2Fgroups%2Fcomp.faq%2FCPlusPlus%2Fcodeandtrick%2FM.1037702476.F0
11.5.1 最后那个例子似乎有些问题
class X {
friend void f();
int i;
};
void f() {
X a;
a.i = 0;
}
TCPL上说f() 必须要在声明为友元之前声明一次,否则不视为X的友元函数,也就是说a.i =0那里应该编译错误。但是我查了一下标准中friend一节并未提及这一限制,在VC9和Comeau 4.3.10上编译也没有遇到问题。
12.4.2 “Since Ival_box provides the interface for the derived class, it is derived
using public. Since BBwindow is only an implementation aid, it is derived using protected ”
这里解释了什么时候应该public继承,什么时候应该protected继承。protected继承和private继承的时候外界是无法通过子类对象访问其父类成员的,同时从子类向父类的指针、引用的隐式类型转换也被禁止。
2009-3-21
进入第13章,发现脑细胞有些不够用。模板和继承是C++中代码复用的两大法宝,很重要的一点是需要搞清楚什么场合用什么技术。13.6.1简单的分析了一下“我们操作一堆具有共同属性的对象时,如果这些对象之间有某种继承关系,就应该使用虚函数完成,反之应该使用模板。”这一段是我进入13章 以来第一次记起来上次曾经看过,前面的特化、偏特化、模板继承的用法通通没有印象了。但是这也应该是使用模板最为关键的一个问题了,在现实当中,我目前的 做法是:没有很充分的理由需要使用模板的时候,优先考虑通过继承的方式抽象。模板带来的可读性上的代价必须要考虑进去。对于我来说,同样的一段代码用模板 表达出来要比用继承表达要难懂的多。也许随着水平的提高模板相对继承的使用成本会降低一些。
这一部分无疑是C++当中最耗费脑细胞的环节,完全没有前面几章一马平川的感觉。我不得不放弃了前面一边看一边写评语的习惯,只能先看过一遍再返回来慢慢理解。
13.2.3节模板参数可以是模板,还可以是常量,通过常量表达式或external linkage的对象/函数的地址表达。常量参数一个很强大的武器,通过模板函数的重载、递归某些吃饱了撑着的人开发了各种各样的编译期运算的tricks,这里仅举一个小例子,一个可以返回任意类型数组元素个数的函数
template<class T, int n>
int Func(T (&X)[n])
{
return n;
}
更多的例子可以去boost里面找,或者看看那本什么模板元编程的书
13.3.2节 在函数重载的时候普通函数比一个模板函数的优先级要高。在进行模板参数推导的时候,涉及推导的参数是不允许进行隐式类型转换的。但是不涉及推导的部分可以转化譬如本节最后的例子:
template<class T> class B{};
template<class T> classD : public B<T> {};
template<class T>void Func( B<T>*);// 这里可以进行D<int>*到B<int>*的转换
// 只要能推导出一个确定的T类型即可
与之相反的例子在13.6.3节,模板和类的继承两者混用会导致严重的问题,一个是运行时一个是编译期重用。D是B的子类,D*可以隐式转换到B*并不意味着vector<D*>可以隐式转换到vector<B*>。如果有这种需求,我们可以使用13.6.2节给出的带模板的构造函数来进行转换。
13.4.1节,这里书上出现了一个比较大的问题,模板参数的默认值只能用于构建模板类,而不能用于模板函数或者模板类的成员函数,也就是说本节这个compare函数是编不过的。在VC9上编译时报告error C4519,查了一下潘爱民版的依然存在这个问题。至于原因,我在这里找到了线索
http://www.open-std.org/JTC1/sc22/wg21/docs/cwg_defects.html#226
BS爷爷于2000年提交了一份提案,建议加上模板函数的默认参数,但是被否决了。原因是参数可以推导、函数可以重载,如果还可以默认类型的话问题将变得非常复杂。
13.5 模板特化有一个顺序问题需要注意一下。如果某个模板在特化的声明之前已经进行过实例化,会产生一个编译错误。如果在偏特化之前进行过实例化,VC9当中可编译通过,按照未偏特化的版本进行实例化。
另外GCC实例化的时候还有一个不讨人喜欢的"feature",pragma pack按照实例化位置的pack进行pack,而不是定义模板类的pack,具体情况看这里:
http://chen3feng.spaces.live.com/blog/cns!FEF0D083246BBED0!1880.entry
因为实例化可能发生在各种地方,这种顺序带来的问题可能还有。
13.5.1最后那句话翻来覆去看不懂…… 另外还有两个问题:
1. 模板函数是否可以偏特化?(ms不可以)
2. 刚才在哪儿看见一眼,模板函数的返回类型也是其signature的一部分,ms还可以通过返回值deduce模板参数(这是在标准上看到的)
13章大体上看完了两遍,现在知道了不少可以怎么用的语法,但还有很多不知可不可以用的语法没有搞清楚。
13.2
·通过某个实例化的模板来理解、调试代码比较方便,不至于太抽象
·编译器保证同样的模板参数只产生一个实例化的类型/函数
·除了类型,还可以用几种编译期常量作为模板参数(但是string literal不可以)
13.3
·函数模板的参数可以在调用的时候做推导
13.5
·模板类可以通过偏特化+继承减小生成的代码量
2009-03-28
14章 异常处理
14.1.1 C++异常处理只用于同步的异常,硬件中断等异步的异常不在考虑之内,譬如除以0等情况。
异常处理的设计原则是为了分离错误检测代码和错误处理代码
通常情况下使用指针或引用类型catch异常,这样不会在拷贝的时候丢失信息。
14.4.1就是大名鼎鼎的RAII原则,尽可能的利用构造函数和析构函数来管理资源的获取和释放,避免构造函数抛出异常时未调用析构函数造成的内存泄漏等问题,这也就是通常所说的异常安全。
14.4.2 Auto_ptr这一节的电子版有问题,中文版部分纠正,详情见我那篇《关于auto_ptr的三个问题》
问题:
譬如说标准库当中的异常,使用的代码当中没有进行catch,会出现未捕获的异常吗?(Yes)
声明了throw类型的函数和没声明的函数是同一个函数吗?如果不是的话如何重载?(必须保证同一函数的所有声明、定义的Exception Specification一致,否则将是一个编译错误。但是这个限制比较鸡肋,标准规定跨编译单元的声明检查不是必须,否则严重影响编译成本,因此在VC9当中即使同一编译单元默认也不做这一检查,打开/Za选项才认为是一个编译错误)
14.6.3未捕获异常的映射,在函数的exception specification中加一个std::bad_exception,如果出现指定以外的异常抛出,则自动转换为一个std::bac_exception而不是直接terminate()
14.11 建议,第四条,并不是每个程序都需要考虑异常安全,不要拿着锤子就想到处都敲一敲,KISS也很重要,一定要因地制宜,考虑实际情况编写合适的代码。
15.2.5
曾经听人说起在虚基类当中不应该放任何成员变量,但是原因并不能令人信服。这一节提到如果认为虚继承的性能开销不可接受的话可以把不含有成员变量的虚基类改成非虚的,不会有任何问题。在设计C++的时候没有把所有的基类继承规定为虚的是出于历史原因的考虑,不应该为不需要的东西付出额外的开销。现在来看,这个原因造成性能瓶颈的可能性太低了。如果有,这样的应用基本也还在用C,没有转移到C++阵营。
15.3.2
对于private继承,子类向父类的指针转换并非完全禁止,在类的内部或者友元函数当中还是允许的。
15.3.2.1 如果多重继承当中有多条路径可以访问某个父类的成员,则允许访问的规则优先于禁止访问。
15.4.1 本节开始的例子指出dynamic_cast在downcast类型不对的时候返回空指针,在upcast访问权限不够的时候也返回空指针。不过我在VC里面试了一下,例子给出的那段代码直接就给出了编译错误。这里可能是委员会后来作了修订,upcast权限不足的直接给error,看下面的例子
class Base{
};
class D : virtual protected Base{};
class DD : public D, virtual public Base
{};
int main()
{
DD d;
D* pD = &d;
Base* pB = dynamic_cast<Base*>(pD);
return 0;
}
因为是私有继承,通常情况下的D*是不可以转换为Base*的,但是DD从Base公有继承了一次,因此可以由DD*转换为Base*。pD实际指向的是一个DD对象,也就是说按照RTTI的规则来看转换为Base*比返回0更为合理。然而访问权限这个东西其实只是一个编译期的概念,并不存在于运行期,如果到了运行期是无法判断出是否有足够的访问权限。但是在编译期又无法决定pD究竟是一个D的指针还是DD的指针。
我能想到的就是这个原因,谁有兴趣可以去翻一下C++标准或者提案,我不打算深究了。
btw:本节例子之前的那句话描述有些问题,应该是这样子" if p is of type T* or T is an accessible base class of p"
15.4.1 本节后面一个例子的trick有点儿意思,用多继承的方式把一个concrete类型包在polymorphic类型当中然后再转回来,这是解决concrete类型不能dynamic_cast的一个workaround,看起来总比static_cast要类型安全一些。
15.4.2 dynamic_cast失败除了因为类型不对,还有可能是多继承当中virtual 和非virtual并存出现ambiguity。如果在upcast当中出现这种情况,实际上在编译期就可以知道的,我认为完全没必要搞到编译期返回空指针,这个设计欠妥。
15.4.2.1 从virtual base做downcast不能用static_cast,既然dynamic_cast可以做这件事,既然你已经不怕麻烦用了static,不妨改成dynamic吧。但有个问题,如果是非polymorphic基类被virtual继承的话,那就两种cast都搞不定了。当然基类不polymorphic也是比较失败的设计,没事不需要用罢了。
15.6.2 构造函数不可以virtual,但是也有办法作出同样的效果,那就是本节代码示范的通过virtual函数clone自身对象,这貌似是设计模式中的一种。