Effective C++ - Accustoming Yourself to C++

Effective C++ - Accustoming Yourself to C++

前言:如何有效运用C++,包括一般性的设计策略,以及带有具体细节的特定语言特性。知道细节很重要,否则如果疏忽几乎总是导致不可预期的程序行为(undefined behavior)。本文总结对于如何使用C++的一些建议,从而让你成为一个有战斗力的C++程序员。


1 构造函数的explicit

被声明为explicit的构造函数通常比non-explicit更受欢迎,因为它们禁止编译器执行非预期的类型转换。除非有一个好理由允许构造函数被用于隐式类型转换,否则把它声明为explicit

class foo {
public:
    explicit foo(int x);
};

2 对象的复制

copy构造函数被用来“以同型对象初始化自我对象”copy assignment操作符被用来“从另一个同型对象中拷贝其值到自我对象”。

class Widget {
public:
    Widget();                               // default构造函数
    Widget(const Widget& rhs);              // copy构造函数
    Widget& operator=(const Widget& rhs);   // copy assignment操作符
};

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

copy构造和copy赋值的区别:如果一个新对象被定义,一定会有一个构造函数被调用,不可能调用赋值操作。如果没有新对象被定义,就不会有构造函数被调用,那么就是赋值操作被调用。

3 命名习惯

构造函数和析构函数分别使用缩写ctordtor代替。
使用lhs(left-hand side)和rhs(right-hand side)表示参数名称。

4 TR1和Boost

TR1(Technical Report 1)是一份规范,描述加入C++标准程序库的诸多新机能。这些机能以新的class templatesfunction templates形式体现。所有TR1组件都被置于命名空间tr1内。
Boost是个组织,亦是一个网站,提供可移植,源代码开放的C++程序库。大多数TR1机能是以Boost的工作为基础。

5 视C++为一个语言联邦

今天的C++已经是个多重范型编程语言(multiparadigm programming language),一个同时支持以下特性的语言:
* 过程形式(procedural)
* 面向对象形式(object-oriented)
* 函数形式(functional)
* 泛型形式(generic)
* 元编程形式(metaprogramming)

为了理解C++,你必须认识其主要的次语言(sublanguage)

5.1 C

说到底C++仍是以C为基础。blocks, statements, preprocessor, built-in data types, arrays, pointers等统统来自C。许多时候C++对问题的解法其实不过就是较高级的C解法,但是C++提供了C没有的templates, exceptions, overloading(重载)等功能。

C语言可以重载吗

// http://www.cplusplus.com/reference/cstdlib/qsort/
/* qsort example */
#include <stdio.h>      /* printf */
#include <stdlib.h>     /* qsort */

int values[] = { 40, 10, 100, 90, 20, 25 };

int compare (const void * a, const void * b)
{
    return ( *(int*)a - *(int*)b );
}

void fun()
{
    printf("fun()\n");
}

/*
$gcc -o overload_test overload_test.c 
overload_test.c:18:6: error: redefinition of 'fun'
void fun(int a)
     ^
overload_test.c:13:6: note: previous definition is here
void fun()
     ^
1 error generated.
 */
#if 0
void fun(int a)
{
    printf("fun(int a)\n");
}
#endif

int main ()
{
    // 测试C语言是否支持overload重载
    fun();

    // C语言可以通过不同的函数指针来模拟overload重载
    int n;
    qsort (values, 6, sizeof(int), compare);
    for (n=0; n<6; n++)
        printf ("%d ",values[n]);

    return 0;
}

5.2 Object-Oriented C++

这部分就是C with Classes所诉求的:
* classes(包括构造函数和析构函数)
* encapsulation(封装)
* inheritance(继承)
* polymorphism(多态)
* virtual function(虚函数动态绑定)
* etc.

5.3 Template C++

这是C++的泛型编程(generic programming)部分,也是大多数程序员经验最少的部分。

5.4 STL

STL是个template程序库,它对containers, iterators, algorithms以及function objects的规约有极佳的紧密配合与协调。

6 尽量以const, enum, inline替换#define

宁可以编译器替换预处理器。当你做出这样的事情:

#define ASPECT_RATIO 1.653

记号名称ASPECT_RATIO也许从未被编译器看见,也许在编译器开始处理源码之前就被预处理器替换了,于是记号名称有可能没有进入记号表(symbol table)内,当你运用此常量但获得一个编译错误时可能会带来困惑,因为这个错误信息提到的是1.653而不是ASPECT_RATIO。尤其是如果ASPECT_RATIO被定义在一个非你所写的头文件内,你肯定对1.653来自何处毫无概念。解决的方法是:以一个常量替换上述的宏(#define)

const double AspectRatio = 1.653;   // 大写名称通常用于宏

好处是:

  • 作为一个语言常量,AspectRatio肯定会被编译器看到,当然就会进入记号表内。
  • 使用常量可能比使用#define导致较小量的目标代码,因为预处理器盲目地将宏名称进行替换会导致目标代码出现多份1.653,而若改用常量则不会出现。
  • 字符串常量,string对象通常比char*-based合适。
const char* const authorName = "gerry";
const std::string authorName("gerry");
  • class专属常量。为了将常量的作用域(scope)限制在class内,你必须让它成为class的一个成员(member),另外为了保证此常量至多只有一份实体,必须让它成为一个static成员
#include<stdio.h>

class GamePlayer {

public:
    void set_scores() {
        for (int i = 0; i != NumTurns; ++i) {
            scores[i] = i;
        }
    }
    void get_scores() {
        for (int i = 0; i != NumTurns; ++i) {
            printf("%d ", scores[i]);
        }
        printf("\n");
    }

    static int get_numturns() {
        //printf("addr GamePlayer::NumTurns[%p]\n", &GamePlayer::NumTurns);
        return GamePlayer::NumTurns;
    }

private:
    static const int NumTurns = 5;   // 常量声明
    int scores[NumTurns];            // 使用该常量
};

int main()
{
    printf("GamePlayer::NumTurns[%d]\n", GamePlayer::get_numturns());

    GamePlayer player;
    player.set_scores();
    player.get_scores();

    GamePlayer player2;
    printf("player.NumTurns[%d] player2.NumTurns[%d]\n", 
            player.get_numturns(), player2.get_numturns());

    return 0;
}
/*
GamePlayer::NumTurns[5]
0 1 2 3 4 
player.NumTurns[5] player2.NumTurns[5]
 */

然而,上面你所看到的是NumTurns的声明式,而非定义式。通常C++要求所使用的任何东西提供一个定义式,但如果它是class专属常量且又是static整数类型,只要不取它们的地址,你可以声明并使用它们而无须提供定义式

但是,如果你需要取某个class专属常量的地址,或者编译器要求(比如,老编译器)需要看到一个定义式,那么需要另外提供定义式

#include<stdio.h>

class GamePlayer {

public:
    void set_scores() {
        for (int i = 0; i != NumTurns; ++i) {
            scores[i] = i;
        }
    }
    void get_scores() {
        for (int i = 0; i != NumTurns; ++i) {
            printf("%d ", scores[i]);
        }
        printf("\n");
    }

    static int get_numturns() {
        printf("addr GamePlayer::NumTurns[%p]\n", &GamePlayer::NumTurns);
        return GamePlayer::NumTurns;
    }

private:
    static const int NumTurns = 5;   // 常量声明
    int scores[NumTurns];            // 使用该常量
};

const int GamePlayer::NumTurns;   // NumTurns的定义

int main()
{
    printf("GamePlayer::NumTurns[%d]\n", GamePlayer::get_numturns());

    GamePlayer player;
    player.set_scores();
    player.get_scores();

    GamePlayer player2;
    printf("player.NumTurns[%d] player2.NumTurns[%d]\n", 
            player.get_numturns(), player2.get_numturns());

    return 0;
}
/*
addr GamePlayer::NumTurns[0x102092f30]
GamePlayer::NumTurns[5]
0 1 2 3 4 
addr GamePlayer::NumTurns[0x102092f30]
addr GamePlayer::NumTurns[0x102092f30]
player.NumTurns[5] player2.NumTurns[5]
*/

通过提供定义式,我们就可以获取class专属常量的地址。

注意:

  • NumTurns的定义式中没有赋值是因为,class常量已在声明时获得了初值,因此定义时不可以再设置初值。
  • 我们无法利用#define创建一个class专属常量,因为#define并不能限制作用域(scope),一旦宏被定义,它就在其后的编译过程中有效,除非在某处被#undef。因此,#define不仅不能用来定义class专属常量,也不能提供任何封装性。

如果想具备作用域,但又不想取地址,可以使用enum来实现这个约束。

class GamePlayer {

public:
    void set_scores() {
        for (int i = 0; i != NumTurns; ++i) {
            scores[i] = i;
        }
    }
    void get_scores() {
        for (int i = 0; i != NumTurns; ++i) {
            printf("%d ", scores[i]);
        }
        printf("\n");
    }

    static int get_numturns() {
        //printf("addr GamePlayer::NumTurns[%p]\n", &GamePlayer::NumTurns);
        return GamePlayer::NumTurns;
    }

private:
    //static const int NumTurns = 5;   // 常量声明
    enum {
        NumTurns = 5,    // 令NumTurns成为5的一个记号名称
    };
    int scores[NumTurns];            // 使用该常量
};

预处理器和宏的陷阱:

宏看起来像函数,但是不会招致函数调用(function call)带来的额外开销。
糟糕的做法:(有效率,但不安全)

// 以a和b的较大值调用f函数
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))

好的做法:(效率和安全同时得到保证)

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

这个template根据实例化可以产出一整群函数,每个函数都接受两个同类型对象,并以其中较大的调用f。这里不需要在函数本体中为参数加上括号,也不需要操心参数被计算的次数,同时,由于callWithMax是个真正的函数,它遵守作用域和访问规则,因此可以写出一个class内的private inline函数,而对于宏是无法完成的。

请记住:

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

7 尽量使用const

const允许你指定一个语义约束,也就是指定一个“不该被改动”的对象,而编译器会强制实施该项约束。

char greeting[] = "Hello";

char* p = greeting;             // non-const pointer, non-const data
const char* p = greeting;       // non-const pointer, const data
char* const p = greeting;       // const pointer, non-const data
const char* const p = greeting; // const pointer, const data

如果关键字const出现在星号左边,表示被指物是常量;如果出现在星号右边,表示指针自身是常量;如果出现在星号两边,表示被指物和指针两者都是常量。

注意:如果被指物是常量,将关键字const写在类型之前,和写在类型之后星号之前,这两种写法的意义相同。

void f1(const Widget* pw);
void f2(Widget const * pw);

STL迭代器系以指针为根据塑模出来,所以迭代器的作用就像个T*指针。如果你希望迭代器所指的东西不可被改变,则需要使用const_iterator

std::vector<int> vec;

const std::vector<int>::iterator iter = vec.begin();
*iter = 10;     // ok
++iter;         // error

std::vector<int>::const_iterator citer = vec.begin();
*citer = 10;    // error
++citer;        // ok 

const成员函数

const实施于成员函数的目的,是为了确认该成员函数可作用于const对象身上。这一类成员函数之所以重要,是因为:

  • 它们使class接口比较容易被理解,可以得知哪个函数可以改动对象内容,而哪个函数不行。
  • 它们使“操作const对象”成为可能,这对编写高效代码是个关键,比如,改善程序效率的一个根本方法是以pass by reference-to-const方式传递对象,而此技术可行的前提是,我们有const成员函数可用来处理取得的const对象。

注意:两个成员函数如果只是常量性不同,可以被重载(overload)。只有返回值类型不同的两个函数不能重载(functions that differ only in their return type cannot be overloaded)。

#include<stdio.h>
#include<iostream>
#include<string>

class TextBlock {
public:
    TextBlock()
    {
    }
    TextBlock(const char* lhs)
    {
        text = lhs;
    }
public:
    // operator[] for const object
    const char& operator[] (std::size_t position) const 
    {
        return text[position];
    }

    // operator[] for non-const object
    char& operator[] (std::size_t position)
    {
        return text[position];
    }

private:
    std::string text;
};

int main()
{
    TextBlock tb("gerry");
    std::cout << tb[0] << std::endl;         // 调用non-const TextBlock::operator[]

    const TextBlock ctb("yang");             // 调用const TextBlock::operator[]
    std::cout << ctb[0] << std::endl;

    return 0;
}

成员函数如果是const意味着什么?—— bitwise constness或者physical constness VS logical constness

bitwise const指的是,成员函数只有在不更改对象之任何成员变量(static除外)时才可以说是const,即,const成员函数不可以更改对象内任何non-static成员变量。

注意:许多成员函数虽然不完全具备const性质,却能通过bitwise测试。比如,一个更改了”指针所指物”的成员函数,如果只有指针隶属于对象,那么此函数为bitwise const不会引发编译器异议,但是实际不能算是const

下面这段代码,可以通过bitwise测试,但是实际上改变了对象的值。

#include<stdio.h>
#include<iostream>
#include<string>

class TextBlock {
public:
    TextBlock()
    {
    }
    TextBlock(char* lhs)
    {
        pText = lhs;
    }
public:
    // operator[] for const object
    char& operator[] (std::size_t position) const 
    {
        return pText[position];
    }

#if 0
    // operator[] for non-const object
    char& operator[] (std::size_t position)
    {
        return pText[position];
    }
#endif

private:
    char* pText;
};

int main()
{
    char name[] = "gerry";
    const TextBlock ctb(name);
    std::cout << ctb[0] << std::endl;         // 调用const TextBlock::operator[]

    char* pc = &ctb[0];
    *pc = 'J';
    std::cout << ctb[0] << std::endl;         // 调用const TextBlock::operator[]

    return 0;
}

logical constness主张,一个const成员函数可以修改它所处理的对象的某些bits,但只有在客户端侦测不出的情况才可以(即,对客户端是透明的,但是实际上对象的某些值允许改变)。正常情况下,由于bitwise const的约束,const成员函数内是不允许修改non-static成员变量的,但是通过将一些变量声明为mutable则可以躲过编译器的bitwise const约束。

#include<stdio.h>
#include<iostream>
#include<string>
#include<string.h>

class TextBlock {
public:
    TextBlock() : lengthIsValid(false)
    {
    }
    TextBlock(char* lhs) : lengthIsValid(false)
    {
        pText = lhs;
    }
public:
    std::size_t length() const
    {
        if (!lengthIsValid) {
            printf("do strlen... ");
            textLength = std::strlen(pText);    // error? 在const成员函数内不能修改non-static成员变量
            lengthIsValid = true;               // 同上
        }
        return textLength;
    }

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

#if 0
    // operator[] for non-const object
    char& operator[] (std::size_t position)
    {
        return pText[position];
    }
#endif

private:
    char* pText;

    mutable std::size_t textLength;    // 最近一次计算的文本区域块长度
    mutable bool lengthIsValid;        // 目前的长度是否有效
};

int main()
{
    char name[] = "gerry";
    const TextBlock ctb(name);
    std::cout << ctb[0] << std::endl;         // 调用const TextBlock::operator[]
    std::cout << "length: " << ctb.length() << std::endl;

    char* pc = &ctb[0];
    *pc = 'J';
    std::cout << ctb[0] << std::endl;         // 调用const TextBlock::operator[]
    std::cout << "length: " << ctb.length() << std::endl;

    return 0;
}
/*
$./mutable 
g
length: do strlen... 5
J
length: 5
 */

constnon-const成员函数中避免重复

方法是:运用const成员函数实现出其non-const孪生兄弟。

不好的做法(因为有重复代码):

    // operator[] for const object
    const char& operator[] (std::size_t position) const 
    {
        // bounds checking
        // log access data
        // verify data integrity
        // ...

        return text[position];
    }

    // operator[] for non-const object
    char& operator[] (std::size_t position)
    {
        // bounds checking
        // log access data
        // verify data integrity
        // ...

        return text[position];
    }

好的做法(实现operator[]的机能一次并使用它两次,令其中一个调用另一个):

#include<stdio.h>
#include<iostream>
#include<string>

class TextBlock {
public:
    TextBlock()
    {
    }
    TextBlock(const char* lhs)
    {
        text = lhs;
    }
public:
    // operator[] for const object
    const char& operator[] (std::size_t position) const 
    {
        // bounds checking
        // log access data
        // verify data integrity
        // ...

        std::cout << "const char& operator[]() const\n";
        return text[position];
    }

#if 0
    // operator[] for non-const object
    char& operator[] (std::size_t position)
    {
        // bounds checking
        // log access data
        // verify data integrity
        // ...

        return text[position];
    }
#endif

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

private:
    std::string text;
};

int main()
{
    TextBlock tb("gerry");
    std::cout << tb[0] << std::endl;         // 调用non-const TextBlock::operator[]

    const TextBlock ctb("yang");             // 调用const TextBlock::operator[]
    std::cout << ctb[0] << std::endl;

    return 0;
}
/*
char& operator[]()
const char& operator[]() const
g
const char& operator[]() const
y 
 */

请记住:

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

8 确定对象被使用前已先被初始化

关于“将对象初始化”这事,C++似乎反复无常(对象的初始化动作何时一定发生,何时不一定发生)。针对这种复杂的规则,最佳的处理方法是:永远在使用对象之前先将它初始化

对于内置类型,必须手工完成初始化;对于内置类型以外的其他类型,初始化责任落在构造函数(constructors)身上,即,确保每一个构造函数都将对象的每一个成员初始化。

构造函数初始化的正确方法是:使用member initialization list(成员初值列),而不是在构造函数中的赋值。因为第一种方法的执行效率通常较高(对于大多数类型而言,比起先调用default构造函数,然后再调用copy assignment操作符,单只调用一次copy构造函数是比较高效的。对于内置类型,其初始化和赋值的成本相同,但为了一致性最好也通过成员初值列来初始化)。

ABEntry:ABEntry(const std::string& name, const std::string& address,
                 const std::list<PhoneNumber>& phones)
: theName(name),        // 成员初值列表,这些都是初始化
  theAddress(address),
  thePhones(phones),
  numTimesConsulted(0)
{ }

ABEntry::ABEntry()
: theName(),            // 调用theName的`default`构造函数
  theAddress(),         // 同上
  thePhones(),          // 同上
  numTimesConsulted(0)  // 将内置类型int显示初始化为0
{ }

C++有着十分固定的”成员初始化次序”:总是base classes更早于其derived classes被初始化。而class的成员变量总是以其声明次序被初始化,而和它们在成员初始值列中的出现次序无关。建议,当你在成员初值列中初始化各个成员时,最好总是和其声明的次序一致

最后一个问题:不同编译单元内定义的non-local static对象的初始化顺序是怎么样的?

函数内的static对象称为local static对象,其他static对象称为non-local static对象。

C++对定义于不同编译单元内的non-local static对象的初始化次序并无明确定义。因此,如果某编译单元内的某个non-local static对象的初始化动作依赖另一编译单元内的某个non-local static对象,那么它所用到的这个对象可能尚未被初始化。

针对上面这个问题的解决方法是
将每个non-local static对象搬到自己的专属函数内,这些函数返回一个reference指向它所含的对象。即,non-local static对象被local static对象替换了。

class FileSystem { ... };
FileSystem& tfs()
{
    static FileSystem fs;
    return fs;
}

注意:这些函数内含static对象的事实使它们在多线程系统中带有不确定性。处理这种麻烦的方法是,在程序的单线程启动阶段,手工调用所有reference-returning函数,这可消除与初始化有关的race conditions(竞速形势)

请记住

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

下一篇:
Effective C++ - Constructors, Destructors, and Assignment Operators

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值