读书笔记

作者: 计算机网络技术  发布:2019-11-09

Effective C++介绍,effective介绍

1 让自己习惯 C++

p0:导读

1:构造函数声明为explict,可以禁止编译器执行非预期的类型转换(阻止隐式转换,但可以显式转换)。

2:copy构造函数 用来”以同类型对象初始化自我对象”

copy赋值操作符 用来”从另一个同类型对象拷贝其值到自我对象”。

两者区别在于如果一个新对象是被定义,一定会调用一个构造函数,不可能是赋值操作,

如果没有新对象被定义,那就是赋值操作被调用。

    Widget w1; 
    Widget w2(w1); //copy构造函数
    Widget w3 = w2;//copy构造函数
    w1 = w2;   //copy assignment操作符

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

将C++视为一个由相关语言组成的联邦而非单一语言。在某个次语言(sublanguage)中,各种守则与通例都倾向简单、直观易懂、并且容易记住。然而当你从一个次语言移往另一个次语言,守则可能改变。

  • C:说到底C++仍是以C为基础。区块,语句,预处理器,内置数据类型,数组,指针统统来自C。
  • Objective-Oriented C++:C with Classes所诉求的。这一部分是面向对象设计之古典守则在C++上的最直接实施。类,封装,继承,多态,virtual函数等等...
  • Template C++:C++的泛型编程部分
  • STL:template程序库。容器(containers),迭代器(iterators),算法(algorithms)以及函数对象(function objects)...

** note: **
C++高效编程守则视状况而改变,取决于你使用C++的哪一部分。


p1:视C++为一个语言联邦

四个模块:C, object-oriented C++, template C++, STL

高效:内置类型传递使用值传递,自定义类型使用引用传递。

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

C++ 编译过程:预处理 --> 编译 --> 链接
预处理过程扫描源代码,对其进行初步的转换,产生新的源代码提供给编译器。检查包含预处理指令的语句和宏定义,并对源代码进行相应的转换。预处理过程还会删除程序中的注释和多余的空白字符。

“宁可以编译器替换预处理器”。就是尽量少用预处理。

  • 预处理器#define ASPECT_RATIO 1.653将所有出现ASPECT_RATIO的地方替换为1.653,ASPECT_RATIO可能并未进入记号表(symbol table)。因此,当出现错误时报的是1.653而不是ASPECT_RATIO,导致目标定位有问题,问题追踪有困难。如果使用变量,则可轻易地判断。
    此外,盲目地把ASPECT_RATIO替换为1.653可能会在目标码中出现多份1.653,改用常量绝不会出现相同情况。所以尽量定义为常量,const double ASPECT_RATIO = 1.653

  • 如果在数组初始化的时候,编译器需要知道数组的大小,且编译器(错误地)不允许使用“static整数型class常量”进行数组初始化,这时可以使用枚举类型enum来替代define。

class GamePlays{
private:
  static const int NumTurns = 5;      // static整数型class常量
  enum { NumTurns = 5 };             // 枚举
  int scores[NumTurns];
... ...
}
  • 宏看起来像函数,但不会招致函数调用带来的额外开销。如果你想获得高效,建议使用inline内联函数。

有了consts 、enums 和inlines,我们对预处理器(特别是#define) 的需求降低了,但并非完全消除。#include 仍然是必需品,而 #ifdef / #ifndef 也继续扮演控制编译的重要角色。目前还不到预处理器全面引退的时候,但我们要尽量限制预处理器的使用。

** note: **

  1. 对于单纯常量,最好以const对象或enum替换#define。
  2. 对于形似函数的宏,最好改用inline函数替换#define。

p2:尽量用 const ,enum , inline 替换 #define

1:定义常量指针 const char* const authorName = “Sping”;

const std::string authorName(“Sping”);

2:class 专属常量

    //.h
    class GamePlayer
    {
        private:
            static const int NumTurns = 5;  //1:常量声明
            static const int NumTurn;       //2:常量声明
            int scores[NumTurns];
    };
    //.cpp
    const int GamePlayer::NumTurns;   //1:常量定义  //对于class专属常量,如果是整数类型并且不需要取地址的话可以不定义。
    const int GamePlayer::NumTurn = 5;//2:常量定义

3:一个属于枚举类型的数值可权充ints使用, 比如常见的对话框资源ID。

    class Game
    {
        private:
            enum { NumTurns = 5 };
            int scores[NumTurns];
    };

4:使用 template 替换宏定义

    //如以a和b的较大值调用f函数
     #define CALL_WITH_MAX(a,b) f( (a)>(b) ? (a):(b) ) 
    template 
    inline void callWithMax(const T& a, const T& b)
    {
        f(a > b ? a : b);
    }

条款03:尽可能使用 const

const允许你告诉编译器和其他程序员某值应保持不变,只要“某值”确实是不该被改变的,那就该确实说出来。如果const修饰变量,则表示这个变量不可变;如果const修饰指针,表示指针指向的位置不可改变。

  • 和指针有关的const判断:
  1. 如果关键字const出现在星号左边,表示被指物事常量。const char *pchar const *p两种写法意义一样,都说明所致对象为常量。
  2. 如果关键字const出现在星号右边,表示指针自身是常量。
const char * p = "hello"; // *p的hello不可变, 与char const * p = "hello"等价
char * const p = "hello"; // 表示p的值不可变,即p不能指向其它位置
  • STL迭代器的const:
  1. 声明迭代器为const就像声明指针为const一样(即声明一个T* const指针),表示这个迭代器不得指向不同的东西,但它所指的东西的值可以改变。
  2. 如果想要迭代器所指的东西不可改变(即模拟一个const T*指针),使用const_iterator。
std::vector<int> vec;
const std::vector<int>::iterator iter = vec.begin(); //类似T* const
*iter = 10;  //没问题,改变iter所指物
++iter;      //错误!iter是const
std::vector<int>const_iterator cIter = vec.begin();  //类似const T*
*iter = 10;  //错误,*iter是const
iter++;      //没问题,可以改变iter
  • 令函数返回一个常量值,可以避免意外错误。
    如下代码,错把==写成=,一般程序对*号之后进行赋值会报错,但在自定义操作符面前不会(因为自定义*号后返回的是Rational对象实例的引用,可以拿来赋值,不会报错)。如果*不写成const,则下面的程序完全可以通过,但写成const之后,再对const进行赋值就出现问题了。函数的参数,如果无需改变其值,尽量使用const,这样可以避免函数中错误地将==等于符号误写为=赋值符号,而无法察觉。
class Rational { ... };
const Rational operator* (const Rational& lhs, const Rational& rhs);
...
Rational a, b, c;
if(a * b = c)...  //把==错写成=,比较变成了赋值
  • const作用于成员函数,有两个作用:
  1. 可以知道哪些函数可以改变对象内容,哪些函数不可以。
  2. 改善C++效率,通过pass by reference_to_const(const对象的引用)方式传递对象可改善C++效率。
    下面是常量函数与非常量函数的形式:
class TextBlock{
    public:
        ...
        const char& operator[] (std:size_t position) const
        {    return text[position];    }
        char& operator[] (std:size_t position) 
        {    return text[position];    }
    private:
        std::string text;
};
/* 使用operator[] */
TextBlock tb("hello");              //non-const 对象
cout<<tb[0]<<endl;    //调用的是non-const TextBlock::operator[]
tb[0] = 'x';          //没问题,写一个non-const对象
const TextBlock cTb("hello");    //const 对象
cout<<cTb[0]<<endl;   //调用的是const TextBlock:operator[]
cTb[0] = 'x';          //错误,写一个const对象

在C++中,只有被声明为const的成员函数才能被一个const类对象调用。

  • 成员函数是const意味着什么?
  1. bitwise const主张const成员函数不可以改变对象内任何non-static成员变量。但一个更改了“指针所指物”的成员函数虽然不能算const,但如果只有指针(而非其所指物)隶属于对象,那么称此函数为bitwise const不会引发编译器异议。
  2. logical const主张成员函数可以修改它所处理的对象内的某些bits,但要在客户端侦测不出的情况下才得如此。
    编译器默认执行bitwise。如果想要在const函数中修改non-static变量,需将变量声明为mutable(可变的)。
class TextBlock{
    private:
        char* pText;
        mutable std::size_t textLength;    // 即使在const成员函数内,
        mutable bool lengthIsValid;        // 这些成员变量也可能会被更改。
    public:
        ...
        std::size_t length() const; 
};
std::size_t TextBlock::length() const{
    if (!lengthIsValid){
        textLength = std::strlen(pText);  //加上mutable修饰后,便可以修改其值
        lengthIsValid = true;
    }
}
  • 避免const和non-const成员函数重复
    如果const和non-const成员函数功能相当、代码重复,编译时间、维护等会是一个大问题,这时就用non-const函数去调用const函数,但不能反过来。这是因为non-const函数可能会改变对象,const函数承诺不改变对象,const调用non-const就不安全了
class TextBlock{
    public:
        const char& operator[](std:size_t position) const
            ...
            return text[position];
        }

        char& operator[] (std:size_t position){
            return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]);
        } 
};

上面代码进行了两次转型:

  1. 第一次用static_cast来为*this添加const,这使接下来调用operator[ ]时得以调用const版本;
  2. 第二次则是用const_cast从const operator[]的返回值转除const,以符合non-const返回值类型。

** note: **

  1. 将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。
  2. 编译器强制实施bitwise constness,但你编写程序时应该使用“概念上的常量性”;
  3. 当cosnt和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。

p3:尽可能使用const

1:指针指向常量 与 常量的指针

const在左边,表示被指物是常量;const在右边,表示指针自身是常量;

    char greeting[] = "Hello";
    const char* p = greeting; //no-const *p, const data
    char* const p = greeting; //const *p, no-const data

2:声明为const可帮助编译器侦测出错误用法,const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。

3:成员函数如果只是常量性不同,可以被重载

4:mutable(可变的),关键字mutable释放掉non-static成员变量的bitwise constness约束。

5:当const和 non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。

    class TextBlock
    {
        public:
            const char& operator[](std::size_t position) const{
                return text[position];
            }
            char& operator[](std::size_t positon){
                return const_cast( 
                    static_cast(*this)[position] 
                    ) //1:为*this加上const调用const op[];
            }         //2:将op[]返回值的const转除;
        private:
            std::string text;

        public:
            std::size_t length() const;
        private:
            char* pText;
            mutable std::size_t textLength; //最近一次计算的文本长度
            mutable bool lengthIsValid; //目前长度是否有效

    };
    std::size_t TextBlock::length() const
    {
        if(!lengthIsValid){
            textLength = std::strlen(pText);    
            lengthIsValid = true;
        }
    }

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

  • 对内置类型(基本类型)手动进行初始化。

  • 内置类型以外的类型,初始化要靠构造函数,要确保每一个构造函数都将对象的每一个成员初始化。
    类的构造函数使用成员初值列(member initialization list),而不是在构造函数中进行赋值操作,这样通常效率更高。因为赋值的版本其实是先进行初始化再进行赋值,而成员初值列版本是直接进行初始化,这对于非内置类型(std::string等)来说显然后者效率更高。对于内置类型,其初始化和赋值的成本相同,但为了一致性最好也通过成员初值列来初始化。对于const和reference类型必须是初始化,赋值操作是不允许的。

  • base classes更早于derived classes被初始化,class的初值列成员变量的排列顺序与其声明顺序相同。

  • “不同编译单元内定义之non-local-static对象”的初始化次序。
    static对象,其寿命从被构造出来直到程序结束为止,包括global对象,定义于namespace作用域内的对象,在classes内、在函数内、以及在file作用域内被声明为static的对象。函数内的static对象被称为local static对象(因为它们对函数而言是local),其他static对象称为non-local static对象。

// FileSystem源文件 class FileSystem{ public: ... std::size_t numDisks() const; };
extern FileSystem tfs;
// Directory源文件,与FileSystem处于不同的编译单元
class Directory{
    public:
        Directory(params);
        ...
};
Directory::Directory(params){
    ...
    //调用未初始化的tfs会出现错误
    std::size_t disks = tfs.numDisks();
}

C++对“定义于不同编译单元内的non-local static对象”的初始化相对次序并无明确定义,因此,为防止A的初始化需要B,但B尚未初始化的错误,将每个non-local static对象搬到自己的专属函数内(该对象在此函数内被声明为static),然后用户调用这些函数,而不直接涉及这些对象。

class FileSystem { ... };
FileSystem& tfs(){
    static FileSystem fs;
    return fs;
}
class Directory { ... };
Directory::Directory(params){
    std::size_t disks = tfs().numberDisks();
}
Directory& tempDir(){
    static Directory td;
    return td;
}

经过上面的处理,将non-local转换了local对象,这样做的原理是:函数内的local static 对象会在"该函数被调用期间","首次遇上该对象之定义式"时被初始化,这样就保证了对象被初始化。使用函数返回的“指向static对象”的reference,而不再使用static对象本身。这样做的好处是不调用函数时,不会产生对象的构造和析构。但对多线程这样的方法会有问题。

** note: **

  1. 为内置对象进行手工初始化,因为C++不保证初始化它们;
  2. 构造函数最好使用成员初始化列表,而不要在构造函数本体内使用赋值操作。初始化列表列出的成员变量,其排列次序应该和它们在类中的声明次序相同;
  3. 为免除“跨编译单元之初始化次序”问题,请以local static对象替换non-local static对象。

p4:确定对象呗使用前已先被初始化

1:C++不保证初始化内置类型的对象。

2:对象的成员变量的初始化发生在进入构造函数之前。

3:构造函数最好使用成员初值列,而不要在构造函数中使用赋值操作,初值列的初始化次序只与class中的声明次序一致。

4:为免除”跨编译单元之初始化次序”问题,应以local static对象替换non-local static对象。

    FileSystem& tfs()
    {
        static FileSystem fs;  //函数内的static对象称为local static。
        return fs;
    } //也即是设计模式中的单例模式

C++介绍,effective介绍 p0:导读 1:构造函数声明为explict,可以禁止编译器执行非预期的类型转换(阻止隐式转换,但可以显式转换)。...

本文由今晚买四不像发布于计算机网络技术,转载请注明出处:读书笔记

关键词:

上一篇:hadoop安装详解,Hadoop跨域集群构建实验
下一篇:没有了