《Effective C++》(总结笔记) 章节一:Accustoming Yourself to C++让自己习惯C++

本文深入探讨C++语言的不同层面,将其视为由C、面向对象、模板和STL四个次语言组成的联邦。强调在编程时应优先使用const、enum和inline替换#define,以提高代码的可读性和安全性。此外,文章阐述了const的多种用法,包括const成员函数,以及如何避免const和non-const成员函数的重复代码。最后,讨论了对象初始化的重要性,特别是针对静态对象的初始化顺序和潜在问题。
摘要由CSDN通过智能技术生成

                章节一:Accustoming Yourself to C++让自己习惯C++

C++是带有众多特性的强大语言,在驾驭其之前,需要先习惯C++的办事方式。

目录

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

View C++ as a federation of languages

次语言一 C:

次语言二 面向对象C++:

次语言三 模板C++:

次语言四 STL:

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

Prefer const,enum,inline to #define

条款03:尽可能使用const

Use const whenever possible

const用法:

const成员函数:

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

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

Make sure that objects are initialized before they're used

对象的初始化何时一定发生,何时不一定发生:

初始化顺序 


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

View C++ as a federation of languages

        起初,C++是C with Classes(“带类的C”),只是C加上了一些面向对象特性,当C++成熟后,它开始接受不同与“带类的C”的观念、特性、编程战略异常对函数结构化带来了不同的做法,模板将我们带到新的设计思考方式,STL定义了全新的伸展性做法。现在C++是一个同时支持过程形式、面向对象形式、函数形式、泛型形式、元编程形式的语言,这使C++成为一个强大的工具。

        如何理解这门语言?->将C++视为一个由相关语言组成的联邦而非单一语言。为理解C++,将其分成四个次语言去理解,在某个次语言中,各种守则与通例倾向简单、直观易懂、容易记住,但当从一个次语言移往另一个次语言,守则可能改变。

次语言一 C:

C++以C为基础,区块、语句、预处理器、内置数据类型、数组、指针等都来自C,当以C++完成C成分工作时,高效编程守则反应出了C语言的局限:没有模板,没有异常,没有重载......

次语言二 面向对象C++:

这部分是“带类的C”的诉求:类(构造函数和析构函数),封装,继承,多态,虚函数(动态绑定)......

次语言三 模板C++:

C++的泛型编程部分,模板相关考虑与设计弥漫整个C++,良好的编程守则中“唯模板适用”的特殊条款不罕见。模板威力强大,带来了崭新的编程范式:模板元编程。

次语言四 STL:

STL是模板程序库,它对容器、迭代器、算法、函数对象的规约有极佳的紧密配合和协调。

        当从一个次语言切换到另一个,导致高效编程守则要求你改变策略时,不用惊讶。例如:在C部分,对内置类型而言传值通常比传引用高效,在面向对象C++部分,由于构造函数和析构函数的存在,传const 引用往往更好,在模板C++中也是如此,但是在STL中迭代器和函数对象是在C指针上塑造出来的,传值守则再次适用。

总结:高效编程守则视情况而变,取决于你用C++的哪一部分。

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

Prefer const,enum,inline to #define

        可将此条款理解为“尽量以编译器替代预处理器”,因为用#define定义的常量未被编译器看到,编译器在处理源码的时候,它就被预处理器移走了。例如:#define A 1.653,当运用a获得编译错误信息,错误信息会提到1.653而不是A,倘若A不定义在你写的头文件,就会因为追踪它而浪费时间,原因是A并未进入记号表。

        解决方法是用常量替代宏,const double a = 1.653; a就会进入记号表,使用常量还可能比使用#define使用更少量的码,因为预处理宏替换会导致目标码出现多份1.653,常量则不会。

        使用常量替换#define有两种特殊情况:

1.定义常量指针:有必要将指针(不只是所指物)声明为const,例如:const char* const p = "Scott Meyers";,因为常量定义式常被放在头文件中。

2.class的专属常量:为确保常量至多一份实体,需让它成为static成员。因为在class中的成员是声明而非定义,还需在类外提供定义(若类内没有初值,还需赋初值)。旧的编译器或许不支持声明时赋初值,倘若如此,可以在类内用enum代替。

注意:#define不能创建一个class的专属常量,因为,#define无视作用域,定义后除非被#undef否则在后面编译过程中都有效。 

        认识enum:

1.enum和#define行为更像,enum和#define一般都不能取地址,const则可以,当不想让人用指针或引用指向常量时,可以用enum。通常“整型const对象”不会有另外的存储空间,除非被指针指向或引用,enum和#define绝不会导致非必要的内存分配。

2.enum hack是模板元编程的基础技术。

        宏函数没有函数调用的额外开销,所有实参需加小括号,但仍然有副作用(不可预料行为)(比如实参有++等操作时),使用模板内联函数可以获得宏的效率同时有一般函数的可预料行为和类型安全性。

总结:

1.单纯变量最好用const对象或enums替换#define。

2.对于类似函数的宏,最好用内联函数替换#define。

条款03:尽可能使用const

Use const whenever possible

const用法:

1.const允许指定一个对象语义约束,编译器会强制执行这项约束。

2.const可以修饰类外全局变量或命名空间中的变量,或修饰文件、函数、区块作用域中被声明为static的对象。也可以修饰类内static和non-static成员变量。对指针,可以指出指针自身、指针所指物、或两者(或都不是)是const,const出现在*左边,表示所指物为常量,出现在*右边,表示指针自身是常量。

3.迭代器的作用像指针,const std::vector<int>::iterator iter,表示这个迭代器不能指向不同的东西,若希望其可以改动,但其所指向的的东西不能改动,应用const_iterator。

4.const可以用在函数返回值,参数,函数自身(如果是成员函数),需要的时候让返回值为const可以减少函数使用错误带来的麻烦。

const成员函数:

对于成员函数而言,只是const属性不同,将构成重载(重要的C++特性),让const对象和non-const对象获得不同的处理。

对于成员函数是const有两个概念:

bitwise const:不更改对象的任何成员变量(static除外),即不改变对象的内的任何一个bit,是C++对常量性的定义,即const成员函数不可以更改任何non-static成员变量。

但是很多成员函数不具备const性质却能通过bitwise测试,比如更改了指针所指物的成员函数,但是对象中只有指针:

class Text{

public:

...

    char& operator[](std::size_t position)const 
        {return pText[position];}

private:

    char*pText;

};

于是通过这个成员函数可以更改所处理的对象内的某些bits,这种情况导出了所谓的logical constness,但只有在客户端侦测不出的情况下才得以如此。

因为编译器坚持bitwise const,如果需要在const成员函数中改变non-static成员变量,可以用mutable释放掉 non-static成员变量 的bitwise constness约束。

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

假设TextBlock内的operator[]不只是返回一个指向字符的引用,也执行边界检验、记录访问信息、数据完整性检验,把所有同时放进const和non-const operator[]中会导致代码冗余:

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[]调用它,但还是重复了代码,例如函数调用,两次return语句等。于是可以实现operator[]一次,并使用两次,令其中一个调用另一个,需要用到常量性转除,如下:

class TextBlock{
public:
    ...
    const char& operator[](std::size_t position)const 
    {
        ...//边界检验
        ...//记录访问信息
        ...//数据完整性检验
        return text[position];
    }
    char& operator[](std::size_t position) 
    {
        //首先将*this加上const属性可以调用const函数,然后将返回值类型去除掉const属性
           
        return const_cast<char&>(
            static_cast<const TextBlock&>(*this)[position]
        );
    }
private:
    std::string text;
}

因为non-const函数调用const函数是安全,故是可行的。

 总结:

1.将某些东西声明为const可以帮助编译器侦测出错误用法。const可以被施加在任何作用域的对象、函数参数、函数返回值、成员函数本体。

2.编译器强制实施bitwise constness,编写程序时应该使用“概念上的常量性”。

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

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

Make sure that objects are initialized before they're used

对象的初始化何时一定发生,何时不一定发生:

        通常使用C part of C++且初始化会招致运行期成本,那么就不保证发生初始化,而进入了non-C parts of C++,规则则不同,数组不保证初始化,而vector有此保证。最佳处理办法,在使用对象前将它初始化,对于无任何成员的内置类型,必须手工完成此事,对于内置类型以外的,初始化责任落在构造函数上,确保每个构造函数将对象的每一个成员初始化。

        注意区分初始化和赋值,C++规定对象的成员变量初始化动作发生在构造函数本体之前,故构造函数的较佳写法是,使用初始化列表替换函数体中的赋值操作,这样做通常效率更高,因为后者需多调用一次成员的默认构造函数。对于内置类型,初始化和赋值的成本一样高,为了一致性也最好用初始化列表初始化。但如果内置类型的成员是const或引用,则它们必须在初始化列表初始化。

        许多类有多个构造函数,每个构造函数都有自己的初始化列表,如果类中有许多成员变量或基类,多份初始化列表造成重复,这时可以合理将初始化列表中赋值效率和初始化效率一样的成员变量改用它们的赋值操作,并将那些赋值操作移至某个函数(通常是private),供所有构造函数调用,这种做法在“成员变量的初值系由文件或数据库读入”是特别有用。

初始化顺序 

      C++有十分固定的成员初始化顺序,先初始化基类,再初始化派生类,类中的成员变量总是以其声明顺序初始化,即使在初始化列表中顺序不同,为避免错误,最好以声明顺序为初始化列表的顺序。

        一旦明确上述初始化顺序和确保基类和成员变量都被初始化,剩下的需要操心的事情是不同编译单元内定义的non-local static对象的初始化顺序。

编译单元:产出单一目标文件的源码,基本是由单一源码文件加上所包含的头文件。

static 对象:生命周期是从构造出来直到程序结束,因此排除stack 和 heap-based(基于堆)对象,包括global对象、static对象(定义与命名空间作用域中、类中、函数中、文件作用域中的被声明为static的对象),函数中的static对象称为local static对象,其他static对象称为non-local static对象

        于是可以这么理解,不同.cpp文件中的非函数体的静态成员变量初始化顺序需要操心。比如一个.cpp文件中的non-local static对象初始化动作使用了另一个.cpp文件中的non-local static对象,它所用到这个对象可能尚未初始化,C++对此初始化顺序无明确定义,原因是很难做到,最常见形式是多个编译单元的non-local static对象由模板隐式具现化形成,不可能决定正确的初始化顺序。

        解决方法是:将 non-local static对象 搬到自己专属的函数内称为local static对象,函数返回指向所含对象的引用,然后用户调用这些函数,而不直接指涉这些对象,这是单例模式的一个常见实现方法。

        这个手法基础在于:C++保证local static对象会在该函数被调用期间首次遇到对象的定义式时被初始化,用函数调用替换直接访问non-local static对象,获得了保证:所获得的的引用指向的对象被初始化,如果没有调用函数则连构造成本和析构成本都没有。这个引用函数十分短小,是内联的绝佳人选。运用引用函数返回解决上述问题的前提是对象有合理的初始化顺序,在单线程程序中没问题。但内含static对象使它们在多线程系统有不确定性。

        任何static对象在多线程环境下等待某事发生具有问题,解决问题的一种麻烦方法是:在程序的单线程启动阶段手动调用所有引用返回的函数,可以消除与初始化有关的“竞速形势”。

总结:

1.为内置类型对象手工初始化,因为C++不保证初始化它们。

2.构造函数最好使用初始化列表初始化成员,且顺序与声明顺序一致。

3.为免除“跨编译单元的初始化次序”问题,请以local static对象替换non-local static对象。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值