Effective C++ 第二版 48)编译器警告 49)标准库 50)总结

56 篇文章 0 订阅

条款48 重视编译器警告

不注意编译器警告的后果可能很严重; 

1
2
3
4
5
6
7
8
class B {
public:
    virtual void f() const;
};
class D: public B {
public:
    virtual void f();
};

>本来是想用D::f重新定义虚函数B::f, 但有个错误: 在B中f是一个const成员函数, 而D中没有声明为const; 

有些编译器会提示: warning: D::f() hides virtual B::f(); [有些根本不提示] 对于这样的警告, 很多程序员会认为D::f当然会隐藏B::f, 但是编译器想说的是: 声明在B中的f没有在D中重新声明, 它被完全隐藏了(条款50); 忽视这条警告可能会导致错误的程序行为;

在对某个编译器的警告积累经验之后, 你会真正来理解不同的信息所表示的含义; 具备经验后对很多警告可以不必处理, 但重要的是在忽略警告之前, 先要理解它的含义;

警告和编译器是紧密相关的, 不同的编译器可能会有不同的警告, 有的情况没有警告; 编译器是用来将C++代码转换成可执行的格式, 不保证完善的安全性; (或许Ada可以: http://www.ha97.com/4181.html)


条款49 熟悉标准库

C++标准库很大, 规格说明就有300多页, C库只是作为参考包含在内;

标准库中的一切都放在名字空间std中; 但这带来新问题, 无数现有的C++代码依赖于使用了多年的伪标准库中的功能, 例如<istream.h><complex.h><limits.h>等头文件中的功能; 如果现有软件没有针对名字空间进行设计, 用std来包装标准库会导致现有代码不能用;

标准委员会因此决定为包装了std的那部分标准库构建创建新的头文件名; 生成新头文件的方法是将现有C++头文件名中的.h去掉; 所以<iostream.h>变成了<iostream>... 对于C头文件, 采用同样的方法, 但在每个名字前添加一个c; 所以C的<string.h>变成了<cstring>, <stdio.h>变成<cstdio>... 最后, 旧的C++头文件是官方反对使用的(不再支持), 但旧的C头文件依旧支持(保持对C的兼容性); 实际上, 编译器制造商不会停止对客户现有软件提供支持, 所以很多C++头文件还是被支持;

旧的C++头文件名如<iostream.h>将会继续被支持, 尽管不再官方标准中, 这些头文件的内容不在std中; [有些新的编译器已经不支持 iostream.h这样的头文件了~]

新的C++头文件如<iostream>博阿含的基本功能和对应的旧头文件相同, 但内容在std中(在标准化的过程中, 库中有些部分的细节被修改了, 所以旧的和新的头文件中的实体不一定完全对应);

标准C头文件如<stdio.h>继续支持, 头文件内容不在std中; 具有C库功能的新C++头文件具有如<cstdio>这样的名字; 内容和相应的旧C头文件相同, 只是内容在std中;

理清字符串头文件: <string.h>是旧的C头, 对应基于char*的字符串处理函数; <string>是包装了std的C++头文件, 对应新的string类; <cstring>是对应旧的C头的std版本;


标准库中的一切几乎都是模板; iostream帮助操作字符流, 但字符没有规定是char, wchar_t还是Unicode字符, 而是让用户选择; 所有的流类stream class实际上是类模板, 在实例化流类的时候指定字符类型; 例如, 标准库将cout类型定义为ostream, 但ostream实际上是一个basic_ostream<char>类型定义(typedef);

e.g. string不是类, 是类模板, 类型参数限定了每个string类中的字符类型; complex类模板的类型参数限定了实数部分和虚数部分的类型; vector也是类模板; 

如果只是和char类型的流和字符串打交道, 通常可以忽略模板; 因为对这些组件的char实例, 标准库都定义了typedef, 用户可以在编程时继续使用cin, cout, cerr等对象, 以及istream, ostream, string等类型, 不必担心cin的真实类型是basic_istream<char>或者string的真实类型是basic_string<char>;

标准库中很多组件的模板化和上面建议的大不相同, 例如string, 可以基于包含的字符类型确定参数, 但不同的字符集在细节上不同; 特殊的文件结束字符, 拷贝数组的最有效方式... 这些特征在标准中被称为traits, 它们在string实例中通过另外一个模板参数指定; 此外, string对象要执行动态内存分配和释放; string模板有一个Allocator参数, Allocator类型的对象被用来分配和释放string对象使用的内存;

e.g. basic_string模板的完整声明, 以及建立在它之上的string类型定义typedef (可以在<string>头文件中找到相关的信息):

1
2
3
4
5
6
7
8
namespace std {
template<class charT,
    class traits = char_traits<charT>,
    class Allocator = allocator<charT> >
class basic_string;
 
typedef basic_string<char> string;
}

>basic_string的traits和Allocator参数有缺省值; 这在标准库中是很典型的做法, 为使用者提供了灵活性, 相对于灵活性带来的复杂性, 那些进行普通操作的用户可以避开; 如果只想使用C字符串那样的对象, 可以使用string对象, 不用在意实际上是在用basic_string<char, char_traits<char>, allocator<char>>类型的对象;


e.g. 声明string类型的错误方法:

1
class string; // 会通过编译,但你不会这么做

>不考虑名字空间, 问题在于: string不是一个类, 而是一个typedef;

如果使用下面的方法:

1
typedef basic_string<char> string;

 >这不能通过编译; 无法识别basic_string;

为了声明string, 首先要声明它所依赖的所有模板:

1
2
3
4
5
6
7
8
template<class charT> struct char_traits;
template<class T> class allocator;
template<class charT,
    class traits = char_traits<charT>,
    class Allocator = allocator<charT> >
class basic_string;
 
typedef basic_string<char> string;

但是不应该声明string, 因为标准库的实现者声明的string(或std中的其他)可以和标准中所指的的有所不同, 只要最终提供的行为符合标准; 例如basic_string的实现可以增加第四个模板参数, 但是这个参数的缺省值所产生的代码的行为要和标准中的原始bashic_string一致;

-->不要手工声明string(或标准库中的其他); 只要包含一个适当的头文件, 如<srting>;


标准C++库中的主要组件:

标准C库;
小修改;

iostream; 
模板化, 继承层次, 抛异常, 支持string和国际化locales; 支持缓冲区, 格式化标识符, 操作文件; cin, cout, cerr, clog; 可以把string或文件当作流, 对流进行控制, 包括缓冲和格式化;

string; 
string对象在大多数应用中被用来消除对char*指针的使用[作为程序接口, 还是会选用基本类型如char, 因为标准库版本不一致也会导致链接/内存/跨平台问题]; string支持大多数操作(字符串连接, operator[]对单个字符进行常量时间级的访问...), 可以转换成char*以保持兼容性, 还自动处理了内存管理; 一些string的实现采用了引用计数(M29), 这会比基于char*的字符串有更好的性能(时间和空间);

容器;
不用写自己的容器类, 标准库提供了高效的实现: vector(动态可扩充的数组), list(双链表), queue, stack, deque, map, set, bitset; 没有hash table[现在有了], string也是容器;


标准库规定了每个类的接口, 每条接口规范中的部分是一套性能保证; 例如: 无论vector如何实现, 仅仅提供对元素的访问是不够的, 还必须提供 常量时间 内的访问;

很多C++程序中, 动态分配字符串和数组导致大量使用new和delete; 没有delete掉new出来的内存泄漏经常发生; 如果使用string和vector对象(自身内存管理), 而不使用char*和动态分配的数组的指针, 很多new/delete可以免于使用, 隐藏的new/delete问题也会减少(条款6, 11);


算法; 
标准库提供了大量简易方法(预定义函数, 官方称为算法algorithm)--实际上是函数模板, 其中大多数适用于所有的容器, 以及内建数组built-in arrays;

算法将容器的内容当作序列sequence, 每个算法可以应用于一个容器中所有值所对应的序列, 或者子序列subsequence; 标准算法有for_each(为序列中的每个元素调用某个函数), find(在序列中查找包含某个值的第一个位置), count_if(计算序列中使得某个判定为真的所有元素的数量), equal(确定两个序列包含的元素值是否完全相同), search(在一个序列中找出某个子序列的起始位置), copy(拷贝一个序列到另一个), unique(在序列中删除重复值), roate(旋转序列中的值)[位置], sort(对序列中的值排序)......

和容器操作一样, 算法也有性能保证; 例如stable_sort算法执行时要求不超过0比较级(NlogN) (性能必须和最高效的通用排序算法在同一个级别);


对国际化的支持; 
和C库一样, C++库提供很多特性有助于开发国际化软件; 例如, 为支持国际化广泛使用了模板, 利用了继承和虚函数...

主要构件是facets和locales; 
facets描述的是对一种文化要处理哪些特性, 包括排序规则(某地区字符集中字符的排序规则), 日期和时间的表示, 怎样将信息标识符映射成(自然)明确的语言信息...
locales将多组facets捆绑在一起; 例如, 一个关于美国的locale将包括很多facets, 描述美国英语字符串排序方式, 美国人读写日期时间的方式, 货币, 数值... C++允许单个程序中同时存在多个locales, 所以一个应用中的不同部分可能采用不同的规范;


对数字处理的支持:

C++库为复数类(实数和虚数部分的精度可以是float, double或long double)和专门针对数值编程而设计的特殊数组提供了模板; 例如valarray类型的对象可保持任意混叠aliasing的元素; 使得编译器可以更充分优化, 尤其对矢量计算; 标准库还对两种不同类型的数组片提供了支持, 计算内积 inner product, 部分和 partial sum, 临差 adjacent difference...


诊断支持; 
标准库支持三种报错方式: C的断言, 错误号, 例外exception; 标准库定义了例外类exception class层次结构:

exception - logic_error     -invalid_argument    -domain_error/ length_error/ out_of_range

               - runtime_error -underflow_error    -range_error/ overflow_error

logic_error或其子类, 表示的是软件中的逻辑错误; 理论上说是可以通过仔细的程序设计来防止的; runtime_error或其子类表示的是只有在运行时才能发现的错误; 通过继承他们可以创建自己的例外类;


标准库中容器和算法部分一般称为标准模板库STL; STL第三个构件---迭代子Iterator, 是类似指针的对象, 让STL算法哦容器共同工作;

STL的体系结构具有扩展性: 用户可以对STL进行添加; 标准库的组件本身是固定的, 遵守STL构件规范, 可以写自己的容器, 算法和迭代子, 使它们可以和标准STL一起工作, 就像标准组件自身之间相互工作一样; 也可以使用别人写的符合STL规范的容器, 算法, 迭代子; STL组件体现了遵循规范convention带来的好处;

通过使用标准库中的组件, 通常可以避免从头到尾地设计IO流, string, 容器, 国际化, 数值数据结构以及诊断等机制, 给了用户更多时间和精力关注业务部分;


条款50 提高对C++的认识

C++包含很多: C, 重载, 面向对象, 模板, 例外[异常], 名字空间;

C++的设计目标:

1) 和C的兼容性; C++利用C的基础---平衡在这一基础上;

2) 效率; 和C相近的效率;

3) 和传统开发工具及环境的兼容; 各种编译器, 链结器, 编辑器, 各种开发环境; 要移植C++, 实际上移植的只是语言, 并利用了目标平台上的工具;

4) 解决真实问题的可应用性; C++没有被设计为一种完美, 纯粹的语言, 不适合用来教学编程; 它是专业程序员的强大工具, 用来解决各种领域中的真实问题;


为什么隐式生成的拷贝构造函数和赋值运算符会象这样工作, 尤其是指针(条款11, 45)? 因为这是C对struct进行拷贝和赋值的方式, 和C兼容很重要;

为什么析构函数不自动被声明为virtual(条款14), 为什么实现细节必须出现在类的定义中(条款34)? 因为不这样就会带来性能损失, 效率很重要;

为什么C++不能检测非局部静态对象之间的初始化依赖关系(条款47)? 因为C++支持单独编译(分开编译源模块, 然后将多个目标文件链接起来, 形成可执行程序), 依赖现有的链结器, 不与程序数据库相关; 所以C++编译器不会知道整个程序的一切的情况;

为什么C++不让程序员从繁杂事务如内存管理(条款5-10)和低级指针操作中解放出来? 因为一些程序员需要这些处理能力, 程序员的需求很重要;


关于C++的设计目标和语言行为的形成, 可以参考Stroustrup的书 "The Design and Evolution of C++" (Addison-Wesley, 1994), 简称D&E, 里面描述了哪些特性被加到C++中, 以什么顺序以及为什么; 还有哪些特性被放弃了, 幕后的故事, 如dynamic_cast(条款39, M2)被考虑, 放弃, 又考虑, 最后接受的过程原因; D&E描述了C++如何成为现状, 但它不是正式语言规格说明, C++国际标准提供了诸如这些:
一个虚函数调用所使用的缺省参数是表示对象的指针或引用的静态类型所决定的虚函数所声明的缺省参数; 派生类中的重载函数不获取它重载的函数中的缺省值; ---这段话是条款38的基础, 

C++标准是解决争议时可提供的权威信息; 官方名称International Standard for Information Systems---Programing Language C++; 它由International Organization for Standardization(ISO)第21工作组颁布(ISO/IEC JTC1/SC22/WG21) 可以从国家标准机构(美国是ANSI--American National Standards Institute)订购正式C++标准的副本, 在互联网上有免费提供最新草稿副本: the Cygnus Solutions Draft Standard C++ Page;

D&E帮助了解C++语言设计思想, C++标准则明确了语言的具体细节; 


ARM--The Annotated C++ Reference Manual(Addison-Wesley 1990) 作者是Margaret Ellis和Bjarne Stroustrup; 国际标准就是基于ARM(和C标准)开始制定的; ARM真正有用的部分是A--the annotations 注释; 针对C++的很多特性, ARM提供了全面的解释; 

e.g.

1
2
3
4
5
6
7
8
9
10
class Base {
public:
    virtual void f(int x);
};
class Derived: public Base {
public:
    virtual void f(double *pd);
};
Derived *pd = new Derived;
pd->f(10); // 错误!

>问题在于Derived::f隐藏了Base::f, 即使它们参数类型不同; 编译器要求对f取double*, 而数字10不符合;

ARM的解释: 假设调用f时, 用户真的是想调用Derived中的版本, 但不小心用错了参数类型; 假设Derived是在继承层次结构的下层, 你不知道Derived间接继承了基类BaseClass, 而且BaseClass中声明了一个带int参数的虚函数f; 这种情况下, 用户会无意调用了BaseClass::f, 你甚至不知道它存在; 在使用大型类层次结构的情况下, 这种错误时常发生; 为了防患未然, Stroustrup决定让派生类成员按名字隐藏掉基类成员; [用户肯定可以查看基类/头文件, 在继承之前当然要查看基类, 所以这是不是有点多此一举了...而且有点限制了用户的行动范围]

如果想让Derived的用户访问Base::f, 可很容易地通过using声明来完成:

1
2
3
4
5
6
7
8
class Derived: public Base {
public:
    using Base::f; // 将Base::f 引入到
// Derived 的空间范围
    virtual void f(double *pd);
};
Derived *pd = new Derived;
pd->f(10); // 正确,调用 Base::f

[...觉得行为更奇怪了]

对于不支持using声明的编译器, 可以采用内联函数:

1
2
3
4
5
6
7
class Derived: public Base {
public:
    virtual void f(int x) { Base::f(x); }
    virtual void f(double *pd);
};
Derived *pd = new Derived;
pd->f(10); // 正确,调用 Derived::f(int) 间接调用了 Base::f(int)

借助于D&E和ARM, 可以对C++的设计和实现获得透彻的理解; 看似巴洛克风格的建筑外观之后, 是合理严肃的结构设计; 理解结合C++标准的细节, 基于软件开发的坚实基础, 才能真正走向有效的C++程序设计之路;

---杂项The End---YC

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值