您的位置:首页 > 编程语言 > C语言/C++

Effective C++ - Accustoming Yourself to C++

2017-02-04 21:49 351 查看

Effective C++ - Accustoming Yourself to C++

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

Effective C - Accustoming Yourself to C
构造函数的explicit

对象的复制

命名习惯

TR1和Boost

视C为一个语言联邦
1 C

2 Object-Oriented C

3 Template C

4 STL

尽量以const enum inline替换define

尽量使用const

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

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 命名习惯

构造函数和析构函数分别使用缩写
ctor
dtor
代替。

使用
lhs
(left-hand side)和
rhs
(right-hand side)表示参数名称。

4 TR1和Boost

TR1
(Technical Report 1)是一份规范,描述加入C++标准程序库的诸多新机能。这些机能以新的
class templates
function 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
);

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
*/


const
non-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
,但你编写程序时应该使用“概念上的常量性”。

const
non-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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: