第一章、让自己习惯C++

    xiaoxiao2022-07-07  173

    Accustoming Yourself to C++

    条款01、视C++为一个语言联邦(View C++ as a federation of languages)条款02、尽量以const、enum、inline替换#define(Prefer consts,enum,and inlines to #define)条款03、尽可能使用const(User const whenever possible)条款04、确定对象被使用前已被初始化(Make sure that objects are initlialized before they’re used)

    条款01、视C++为一个语言联邦(View C++ as a federation of languages)

    今天的C++是一个多重范型编程语言,一个同时支持过程形式,面向对象形式,函数形式,泛型形式,元编程形式的语言。在C++中,主要分为了4个次语言,分别是C、Object-Oriented C++、Template C++、STL。它们的语言特性如下: 次语言基础特性C块、语句、预处理器、内置数据类型、数组、指针没有模板、没有异常、没有重载Object-Oriented C++classes(包含构造函数和析构函数)、封装、继承、多态、virtual函数(动态绑定)等面向对象,运行期多态Template C++泛型编程、TMP(模板元编程)泛型编程,编译期多态STL容器、迭代器、算法、函数对象对容器、迭代器、算法、函数对象的规约有极佳的紧密配合和协调 C++的高效编程守则需要视情况而定。例如对内置类型而言。pass-by-value通常比pass-by-reference高效;而在Object-Oriented C++中,由于构造函数和析构函数的存在,所以pass-by-reference-to-const往往更加高效,对于Template C++也和Object-Oriented C++类似;而对于STL来说,由于迭代器和函数对象都是在指针上塑造出来的,所以对于STL的迭代器和函数对象而言,pass-by-value的方式通常比较高效。

    条款02、尽量以const、enum、inline替换#define(Prefer consts,enum,and inlines to #define)

    编译器在处理源码前,宏定义中的记号名称可能就已经被预处理器移走了(使用的宏名称无法进入记号表),这时如果出现编译错误,错误信息中出现的可能是宏的值而不是宏名称。

    例如 #define ASPECT_RATIO 1.653 记号名称ASPECT_RATIO 也许从未被编译器看见;也许在编译器开始处理源码前就被预处理器移走了。于是记号名称ASPECT_RATIO 可能没进入记号表内。于是当你运用此常量但获得一个编译错误信息时,可能会带来困惑,因为错误信息也许会提到1.653而不是ASPECT_RATIO 。如果ASPECT_RATIO 被定义在一个不是你写的头文件中,你可能对1.653以及它来自何处毫无概念,于是你将因为追踪它而浪费时间。这个问题也可能出现在记号调试器中,原因相同:你使用的名称可能未进入记号表。

    当定义一个常量时,常量名称肯定会被编译器看到并进入记号表。对于浮点常量而言,使用常量可能比使用#define导致较小量的码,因为预处理器将宏盲目的替换成值有可能导致目标码出现多份宏的值,若改用常量则不会出现这种情况。当我们用常量代替#define时,需要注意两种特殊情况:第一是定义常量指针;第二是定义class专属常量。 当我们定义常量指针时,有必要将指针也声明为const。对于class的专属常量而言,为了将常量的作用域限制在class内,必须让他成为class的一个成员;而为了确保此常量至多只有一份实体,必须让他成为static成员: class Temp { static const int NUM = 5; }; 通常C++要求你对你所使用的的任何东西提供一个定义式,但如果它是一个class专属常量又是static有是一个整数类型(例如int,char等),则需特殊处理。只要不取它们的地址,你可以声明并使用它们,而无需提供定义式。但如果你需要取其地址或你的编译器必须要看到定义,那么你就必须提供一个定义式(const int Temp::NUM;)。请注意,要把这个式子放入实现文件而非头文件。 对于enum来说,enum hack的行为比较像#define,例如对一个enum取地址是不合法的,即无法使用pointer或reference指向一个enum。the enum hack补偿做法的理论基础是:一个属于枚举类型的数值可以权冲一个ints使用。enum hack是template metaprogramming的基础技术。有一些看起来像函数的宏,可以完成想要的实现,又可以避免函数调用带来的额外开销。

    以a和b的较大值调用函数f #define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b)) 无论何时,对类似于这种宏中的参数都应该加上小括号,以免他人在表达式中调用该宏时遇到麻烦。但纵使你为所有实参都带上小括号,依然有可能发生不可思议的事情: int a = 5, b = 0; CALL_WITH_MAX(++a, b); //a被累加两次 CALL_WITH_MAX(++a, b+10); //a被累加一次

    对于那些行为类似于函数的宏而言,如果可以写出对应的template inline函数,就可以在获得宏带来的效率的同时,也可以获得一般函数的所有可预料行为和类型安全性。

    条款03、尽可能使用const(User const whenever possible)

    const允许你指定一个语义约束,而编译器会强制实施这项约束。将某些对象声明为const可以让编译器帮助侦测错误用法。const可被施加于任何作用域内的对象,函数参数,函数返回类型和成员函数本体。对于指针而言,如果const出现在*号左边,则表示被指物是常量;如果出现在指针右边,则表示指针自身是常量。如果被指物是常量,则把const写在类型前后都是合法的。

    const int* a; int const* a;

    声明迭代器为const表示这个迭代器不能指向不同的东西,但它所指的东西的值是可以改变的。如果你希望这个迭代器指向的对象不可被改变,应该使用const_iterator。在函数声明时,另函数的返回值类型为const往往可以降低因客户错误而导致的意外,而又不至于放弃安全性和高效性。将const实施于成员函数的目的,是为了确认该成员函数可作用于const对象身上。这类函数的重要性主要体现在两方面:第一、它们使class接口比较容易理解。这是因为,我们可以通过const属性直接得知哪个函数可以改动对象内容而哪个函数不行。第二、它们使操作const对象成为可能。这对编写高效代码是个关键,因为改善C++程序效率的一个根本办法是以pass by reference-to-const方式传递对象,而此技术可行的前提是,我们有const成员函数可用来处理取得(并经修饰而成)的const对象。对于成员函数来说,如果两个成员函数只是常量性不同的话,是可以被重载的。当const和non-const成员函数有着实质等价的实现时,另non-const版本调用const版本可以避免代码重复。关于const成员函数的含义,主要有两个流行概念:bitwise constness(又称physical constness)和logical constness。bitwise constness阵营的人相信,成员函数只有在不更改对象之任何成员变量时才可以说是const,也就是说它不会改变对象内的任意一个bit。logical constness阵营的人则认为,一个const成员函数可以修改它所处理的对象内的某些bits,但只有客户端侦测不出的情况下才可以。mutable可以 释放掉non-static成员变量的bitwise constness约束。

    条款04、确定对象被使用前已被初始化(Make sure that objects are initlialized before they’re used)

    在类中,应该确保每一个构造函数都会初始化该对象的每一个成员(使用所谓的member initialization list进行初始化),以免出现在部分场景下未被初始化的成员被使用而导致错误的情况。但是切勿混淆初始化和赋值。为避免在对象初始化之前过早的使用它们,需要从多方面进行保证:第一、手工初始化内置型non-member对象。第二、使用成员初始值列初始化所以成员。第三、在初始化次序不确定的情况下,加强你的设计。在初始化列表中进行成员初始化时,可指定无物作为某成员的初始化实参,此时对于该成员,将调用其default构造函数进行初始化操作。对于const对象和reference对象而言,他们必须被初始化。对于某些赋值像初始化的表现一样好的成员变量,可以在初始化列表中遗漏它,并在构造函数中给它赋值。C++对于成员的初始化顺序是固定的,在base class初始化结束后才会进行derived class初始化,而class成员的初始化顺序总是和它们被声明的顺序一致。所以在初始化成员时,最好以它们的声明次序为次序。C++对于定义于不同编译单元内的non-local static对象的初始化的相对次序并没有明确定义。我们可以用Singleton模式来处理不同编译单元内的non-local static对象,以避免在不同编译单元内使用它们时它们还未被初始化。

    函数内的static对象被称为是local static对象(因为它们对于函数而言是local),其他static对象被称为是non-local static对象。 所谓编译单元是指产出单一目标文件的那些源码。基本上它是单一源码文件加上它所包含的头文件。

    Singleton模式(单例模式)的一个常见手法是将每个non-local static对象搬到它们的一个专属函数内(该对象在该函数内被声明为static),这些函数返回一个reference指向该对象。这个手法的基础在于:函数内的static对象会在首次遇上该对象的定义式时进行初始化。而且当你以函数调用方式代替直接访问方式时,可以保证你所获得的那个reference指向的一定是一个历经了初始化的对象;若你从未调用这个static的仿真函数,也不会引发构造和析构成本。对于所有的non-const static对象(不管是local或non-local),如果在多线程环境下等待某事发生都会有麻烦发生。处理这个麻烦的一种做法是:在程序的单线程启动阶段,手工调用所有reference-returning函数,这可以消除与初始化有关的竞速形势。
    最新回复(0)