第一章 习惯C++

Effective C++的讨论内容:

  • 一般性的设计策略
  • 带有具体细节的特性语言特性
    例如:
  • inheritance or templates?
  • public or private?
  • private inheritance or composition?
  • pass-by-value or pass-by-reference?
  • return type of assignment?
  • when to use virtual destructor?

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

view c++ as a federation of languages

最初的C with Classes -> Exceptions, templates
如今发展到多重范型编程语言(multiparadigm programming language),同时支持多种形式:

  • 过程形式:procedural
  • 面向对象形式:object-oriented
  • 函数形式:functional
  • 泛型形式:generic
  • 元编程形式:metaprogramming

因此应该将C++视为多个次语言(sublanguage)形成的联邦。

  • C:区块blocks,语句statement,预处理器preprocessor,内置数据类型built-in data types,数组arrays,指针porinters均来自C。但是C没有模板template,没有异常exceptions,没有重载overloading。
  • Object-Oriented C:C with classes中的classes,constructor&destructor,封装encapsulation,继承inheritance,多态polymorphism,virtual,动态绑定等。
  • Template C++:泛型编程genneric programming部分。包含Template metaprogramming(TMP,模板元编程)。
  • Standard Template Library:containers,iterators,algorithms,function objects

C-like中,pass-by-value更高效;
Object-Oriented和Template中pass-by-reference-to-const更高效。
STL中,实质上都是基于指针,因此pass-by-value再次适用。

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

Prefer consts, enums and inlines to #defines.

i.e.“宁可以编译器替换预处理器”。

#define语句并非语言的一部分。预处理器在编译前将宏定义的值移走,因此该内容对编译器不可见,报错时也只能报具体数值而不是宏名称(原因:宏的名称不进入记号表symbol table)。

const替换#define的两个特殊情况:

  1. const指针:应该将指向常量的指针本身也设为const:
    const T* const p;
    
  2. class中的常量:为了将其作用域限制在class内,应该定义为class的一个成员;而为了保证只有一份,应该定义为static成员。
    static const int MaxValue = 10;
    

注意:某些旧编译器在类内只能进行const static成员的声明,而定义必须在类外进行。若编译期编译器不能找到该值(如需要使用该值创建一个数组),则可以使用“enum hack”方法:

enum{ MaxValue = 10};
int a[MaxValue];

enum和const的区别:

  • enum不允许取地址,因此不能使用指针或引用
  • enum不会导致非必要的内存分配,但某些编译器可能为static const对象分配另外的空间

另一种#define误用情况

#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))

int a = 5, b = 0;
CALL_WITH_MAX(++a, b);
CALL_WITH_MAX(++a, b+10);

应该改为template inline函数:

template<typename T>
inline void callWithMax(const T& a, const T& b){
    f(a>b ? a:b);
}

小结

  • #define依然是必需品,#ifdef和#ifndef也在编译控制中有重要作用。
  • 对于单纯常量,最好以const或enums替换#define
  • 对于类似函数的宏,最好改用inline函数替换#define

条款03:尽可能使用const

Use const whenever possible

const用途广泛,可修饰:

  • class外部的global或namespace作用域中的常量
  • 文件、函数、区块作用域中static的对象
  • class内部的static和非static成员变量
  • 指针的顶层和底层const

STL中基于迭代器的算法都采用T*形式的参数,在大多条件下应该将其设为const。

const成员函数

const用于成员函数是为了确认该成员函数可作用于const对象。这类函数成员十分重要,比如:

  • 使class接口容易被理解:得知哪些函数可以改变对象内容
  • 实现“操作const对象”:pass by reference-to-const

一个重要的C++特性:如果两个成员函数只是常量性不同,可以被重载。

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

const Text text1("hello");
Text text2("world");
cout<<text1[0]<<text2[0]<<endl;

//Text1返回的对常量的引用不能被赋值。

对constness的两大解释:

  • bitwise constness(physical constness)
  • logical constness

bitwise constness是C++对常量性的定义,指成员函数只有在不更改对象任何非static成员变量时才是const。

问题在于不具备const性质的成员函数也可以通过bitwise测试。即:更改了指针所指对象的成员函数虽然不是const,但如果只有指针本身属于对象,则不会引发编译错误,如为了对应C而将string改为char*的程序:

class CTextBlock{
public:
    char& operator[](std::size_t position)const{
        return pText[position];
    }
private:
    char* pText;
}

此代码中只有pText是成员,而operator[]返回一个指向对象内值的引用:

const CTextBlock cctb("hello");
char* pc = &cctb[0];
*pc = 'y';  //不会报错,内容变为yello

因此仅有bitwise const不够,延伸出了logical constness:const成员函数可以在客户端侦测不出的情况下更改所处理对象的某些bits。

只要对可能会被改变的成员变量使用mutable关键字即可:

class CTextBlock{
public:
    size_t length() const;
private:
    char* pText;
    mutable size_t textLength;
    mutable bool lengthIsValid;
};
size_t CtextBlock::length() const {
    if(!lengthIsValid){
        //下列赋值可行
        textLength = std::strlen(pText);
        lengthIsValid = true;
    }
    return textLength;
}

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

若同时定义了const T T::foo() constT T::foo(),有时二者都需要进行的检测如边界检验(bounds cheking)、日志数据访问(log access data)、检验数据完整性(verify data integrity),会导致大量代码重复,应尝试不同时使用两个,而是用一个调用另一个:

使用常量性转除casting away constness

即:首先定义const版本,再利用const_cast和static_cast

const char& operator[](size_t position)const{
    ...
    return text[position];
}
char& operator[](size_t position){
    return const_cast<char&>{     //去除返回值的常量性
        static_cast<const TextBlock&>(*this) //*this加const,为了确认调用的是const版本,而不是递归调用自己
            [position];            //调用const的operator[]
    }
}

小结

  • const作用广泛,可用于任何作用域
  • 编译器强制bitwise constness,但编写时注意conceptunal constness
  • const和non-const成员函数有实质等价的实现时,可以使用非const版本调用const版本,注意两次转换。

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

Make sure that objects are initialized before they are used

使用未初始化的变量可能会发生ub。大多发生在C++语言联邦的C语言部分,如内置数组。但除此之外的部分,如vector则不会发生这些问题。

唯一的解决方法是永远在使用对象之前将其初始化。对于无任何成员的内置类型,必须手工完成初始化。如int、指针等。

区分赋值和初始化

若在构造函数体内部为类成员赋值,其实再构造函数体执行之前已经调用过一次默认构造函数,因此构造函数体内部对成员变量是赋值操作而不是初始化。也就是说,该过程先调用了默认构造函数,然后调用了拷贝构造函数。

Foo::Foo(const int a){
    memberA = a;    //赋值而非初始化
}

应该更改为在构造函数初始化列表(函数名字后函数体前)进行成员函数的初始化。

Foo::Foo():memberA(a)){}

如果成员变量是const或reference就需要初值,而不能被赋值。最简单的方法是:应该在构造函数初始化列表写上所有成员变量。同时,C++具有十分固定的成员初始化顺序,因此应该按照类中声明顺序列出各成员变量

static对象的UB

static对象:

  • 一定不属于stack和heap-based对象,
  • 一般包括global对象、namespace作用域的对象
  • class、函数、file作用域内的对象
  • 函数内的static对象称为local static对象,其他是non-static对象。

编译单元translation unit:产出单一目标文件的源码。基本上是单一源码文件加上含的头文件。

若两个源码文件中,每个内含至少一个non-local static对象,如果其中一个初始化时使用了另一个,初始化次序是不定的,会出现UB。

解决方式:将每个non-static对象搬到其自己的专属函数内。该对象在该函数内被声明为static。这些函数返回一个指向其所含对象的reference,用户调用这些函数而不是static对象本身。

这是一种Singleton模式。在函数中声明的static变量其实已经被初始化了,不会出现使用未初始化static变量的问题。可以理解为,通过函数将a的使用转为了fa(){static A a; return a}中对a的初始化再使用。

小结

  • 为内置类型对象手动初始化
  • 构造函数中使用成员初始化列表,而不是在构造函数体中进行赋值。
  • 为免除“跨编译单元初始化次序”问题,以local static对象替换non-local static对象。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容