Effective C++读书笔记 第1章

    xiaoxiao2026-03-09  5

    文章目录

    Ⅰ、 让自习惯C++条款01:视C++为一个语言连邦条款02:尽量以 const, enum, inline 替换 #define2.1、采用const替换#define2.2、采用enum替换#define2.3、采用 template替换 #define 条款03:尽可能使用const3.1、const成员函数3.2、bitwise constness 和 logical constness3.3、在 const 和 non-const 成员函数中避免重复 条款04:确定对象使用前已被初始化4.1、区分赋值和初始化4.2、不同编译单元内定义的 non-local static 对象的初始化次序

    Ⅰ、 让自习惯C++

    条款01:视C++为一个语言连邦


    C++是一个多重泛型编程语言,同时支持过程形式、面向对象形式、泛型形式、元编程形式的语言。

    将C++是为一个联邦,主要的此语言为4个:

    procedural-based C。C语言的面向过程Object-Oriented C++。面向对象Template C++。 C++ 的泛型 (generics) 编程和由 template 的强大功能带来 template metaprogramming (TMP,模板元编程)STL。template程序库,主要包括 容器、迭代器、算法以及函数对象

    tips:编程过程中在前面3个次语言之间切换时,可能导致高效编程守则要求的改变

    条款02:尽量以 const, enum, inline 替换 #define


    2.1、采用const替换#define

    此处用宏定义一个常量

    #define ASPECT_RATIO 1.653

    记号名称ASPECT_RATIO可能未被登记到记录表(symbol table), 因为可能在编译前的预处理阶段被替换。 导致发生错误时不容易追踪该信息。

    采用常量替换:

    const double AspectRatio = 1.653 //大写名称通常用于宏,此处改变名称写法

    常量定义式通常放在头文件

    定义一个常量的 char*based字符串(不可改变指向不可改变内容)

    const char* const autorName = "Scott Meyers"; //根据c++ primer观点,第一个const为底层const,保证指针指向内容不变 //第二个const为顶层const,保证指针指向地址不变 //采用string const std::string autoirName("Scott Meyers");

    定义一个class的专属常量

    class GamePlayer{ private: static const int NumTurns = 5; //这里只是常量声明式,声明式,声明式 int scores[NumTurns]; //使用它 ... };

      C++中 class专属常量又是static且为整数类型(eg: int,char,bool).需要特殊处理,如果未取地址,可以只声明,否则需要另外提供定义式。

    const int GamePlayer::NumTurns; //类内已经初始化赋值,无须也无法重新赋值。

    #define无法为一个 class 定义一个专属变量,因为 #define 并不重视作用域。 也就是不存在所谓的 private #define

    2.2、采用enum替换#define

      旧式编译器不支持 static 成员在声明式上获得初值,“in class” 初值设定也只允许对整数常量进行。 此时,需要将初值放在定义式:

    class CostEstimate{ private: static const double FudgeFactor; //static class 常量声明 ... }; const double CostEstimate::FudgeFactor = 1.35; //位于实现文件内

      类内的数组声明式,编译器坚持必须在编译期间知道数组的大小。如果编译器不允许 “static 整数型class常量” 完成 "in class"初值设定,可改用所谓的 “the enum hack” 补偿做法。

    class GamePlayer{ private: enum { NumTurns = 5}; //"the enum hack" //令 NumTurns成为5的一个记号名称 int socres[NumTurns]; ... };

    注意:enum不能取地址

    2.3、采用 template替换 #define

    另一个常见的 #define 误用情况是以它实现宏(macros)。

    //用宏比较a和b的较大值来调用f #define CALL_WITH_MAX(a,b) f(a)>f(b) ? (a) : (b) )

    缺点: 要为实参添加小括号防止错误,还有一些不容易发现的错误

    如下:

    int a = 5,b = 0; CALL_WITH_MAX(++a, b); //a被累加了两次 CALL_WITH_MAX(++a, b+10); //a被累加了一次

    采用template函数替代

    template<typename T> inline void callWithMax(const T& a,const T& b){ //采用 pass by reference-to-const //可以传递所有类型. f(a > b ? a : b); }

    条款03:尽可能使用const


    3.1、const成员函数

    目的:

    使class接口比较容易被理解,那些可以改动,那些不行使“操作const对象”称为可能。pass-by-reference-to-const方式传递对象。

    const函数可被重载 1.const对象只能调用 const版本 2.non-const对象可以调用 const和non-const版本,默认为non-const版本。

    3.2、bitwise constness 和 logical constness

    1、bitwise const阵营的人相信,成员函数只有在不改变对象的任何成员变量(static除外)时 才可以说是 const。即不改变对象内的任何一个 bit 。 这种论点好处是很容易侦测违反点。 bitwise constness正是 C++ 对常量性的定义,因此 const 成员函数不可以更改对象内的 任何non-static成员变量。

    不幸的是很多不具备const性质的函数却可以通过 bitwise测试。 实例:

    class CTextBlock{ public: ... char& operator[](std::size_t position) const //bit const声明 { return pText[position]; } //但实际没起到效果 private: char* pText; }; //此时 const CTextBlock cctb("Hello"); //声明一个常量对象 char *pc = &cctb[0]; *pc = 'J'; //此时cctb变为 “Jello”

    2、这种情况导出了所谓的logical constness。他们主张,一个 const成员函数 可以修改它所处理的对象内的某些 bits, 但只有在客户端侦测不出的情况下才得如此。

    例如: CTextBlock class有可能高速缓存文本区域的长度以便询问:

    class CTextBlock{ public: ... std::size_t length() const; private: char *pText; std::size_t textLength; //最近一次计算的文本区域长度 bool lengthIsValid; //当前长度是否有效 }; std::size_t CTextBlock::length() const { if(!lengthIsValid) { textLength = std::strlen(pText); //error!!! const函数内 lengthIsValid = true; //不允许修改类成员 } return textLength; }

    解决方案 mutable释放 non-static成员变量的bitwise constness约束

    class CTextBlock{ public: ... std::size_t length() const; private: char *pText; mutable std::size_t textLength; //mutable可能总是会被更改, mutable bool lengthIsValid; //即使在const函数内 }; std::size_t CTextBlock::length() const { if(!lengthIsValid) { textLength = std::strlen(pText); //现在valid lengthIsValid = true; } return textLength; }

    3.3、在 const 和 non-const 成员函数中避免重复

    假设TextBlock的 operator[] 不单单返回一个 reference,还要做各种检测, 这样会导致写出的 const 和 non-const operator[] 冗余。

    解决方案 令 non-const版本调用 const版本

    class TextBlock{ public: ... const char& operator[](std::size_t position) const //一如既往 { ... ... ... return text[position]; } char& operator[](std::size_t position) //调用 const版本 { return const_cast<char>&( //将op[]返回值的const转除 static_cast<const TextBlock&>(*this) //为*this加上const [position]; //调用 const op[]; ); } ... };

    注意:反向操作将 const函数调用 non-const是不可取的

    条款04:确定对象使用前已被初始化

    定义类Point

    class Point{ int x,y; }; ... Point p; //p成员变量有时候会被初始化(为0),有时候不会 使用 C part of C++ 而且初始化可能招致运行期成本,那么不保证初始化non-C parts of C++,则有所变化,如 vector(STL part of C++)保证初始化, array(C part of C++) 不保证内容初始化。

    最佳处理办法: 永远在使用对象前先将其初始化。对于无任何成员的内置类型,你必须手工完成此事。例如:

    int x = 0; //对int初始化 const char* text = "A C-style string //对指针初始化 double d; std::cin >> d; //调用输入流初始化

    4.1、区分赋值和初始化

    class PhoneNumber { ...}; class ABEntry{ public: ABEntry(const std::string&name, const std::string& address, //构造函数 const std::list<PhoneNumber>& phones); private: std::string theName; std::string theAddress; std::list<PhoneNumber> thePhones; int numTimesConsulted; }; ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones) { theName = name; //这些都是赋值 theAddress = address; //而非初始化 thePhone = phones; numTimesConsulted = 0; }

      对象的初始化发生在进入构造函数之前,故以上的 theName, theAddress 和 thePhones均不是初始化,而是赋值。初始化发生在某些对象的default构造函数调用时,此时未进入ABEntry构造函数体,而对于 numTimesConsulted这类内置类型,赋值动作的时间点不确定。

    采用 **member initialization list (成员初值列)**来为对象的成员初始化。

    ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones) :theName(name), //此处均为初始化,如果 theAddress(address), //类成员没有在指定,编译器 thePhones(phones), //调用默认构造函数 numTimesCosulted(0) {}

    如果成员变量为 const 或 references,则一定需要初值,不能被赋值。

    C++成员初始化次序 base classes 更早由于其 drived classes 被初始化,而 class 的成员变量按照成员声明次序被初始化

    4.2、不同编译单元内定义的 non-local static 对象的初始化次序

    local static对象: 寿命从构造直至程序结束为止,因此不包括 stack和heap-based对象。这种对象包括 global对象 、定义于namespace作用域内的对象、在classed内、在函数内、以及在 file作域内被声明为 static的对象。 函数内的 static对象称为为 local static对象(它们相对函数而言为 local),其他 static对象称为 non-local static对象。它们的析构函数会在 main()结束时自动调用。

    编译单源:是指产出单一目标文件的那些源码。基本上它是单一源码文件加上其所含的头文件。

    问题在于C++对于定义于不同编译单元内的 non-local static对象的初始化次序并无明确定义

    实例:

    class FileSystem{ //编译单元A public: ... std::size_t numDisk() const; ... }; extern FileSystem tfs; //预备给用户使用的对象 /*----------------------------------------------------------*/ class Directory{ public: Directory( params); //编译单元B ... }; Directory::Directory( params) { ... std::size_t disks = tfs.numDisks(); ... }

    假设,采用 创建一个 Driectory对象

    Directory tempDir( params);

      此时 tempDir构造函数调用了 tfs函数对象,故 tfs必须先于 tempDir初始化,否则后果很严重。以下是解决方案:

    采用 local static对象替换 non-local static对象 这是 Singleton模式的一个常见手法

    这个手法基础在于:C++保证,函数内的local static对象会在“该函数被调用期间” “首次遇上该对象的定义式”时被初始化。如果函数从未调用,则不会生成对象, 也免去了多余的构造函数析构函数的调用。

    class FileSystem{...}; //同前 FileSystem& tfs() //定义一个local static对象 { //返回一个reference static FileSystem fs; return fs; } class Directory{...}; //同前 Directory::Directory( params) { ... std::size_t disk = tfs().numDisks(); ... } Directory& tempDir() //同前,返回一个 reference to tfs { static Directory td; return td; }

    以上的 reference-returing 函数往往十分单纯:定义并初始化一个local static,然后返回它。

    注意:这些内含 static对象的事实使他们在多线程有不确定性。任何一种 non-const static对象,无论是local还是non-local,多线程环境下的等待某事发生都会有麻烦。

    解决方案:在程序单线程启动阶段手工调用所有reference-returing函数,可以消除与初始化有关的“竞速形势”。

    最新回复(0)