首页 > 学院 > 开发设计 > 正文

Effective C++ - Accustoming Yourself to C++

2019-11-14 10:05:19
字体:
来源:转载
供稿:网友

Effective C++ - Accustoming Yourself to C++

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


Effective C - Accustoming Yourself to C构造函数的explicit对象的复制命名习惯TR1和Boost视C为一个语言联邦尽量以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 命名习惯

构造函数和析构函数分别使用缩写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)

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 herevoid fun() ^1 error generated. */#if 0void fun(int a){ printf("fun(int a)/n");}#endifint 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;}

Object-Oriented C++ 这部分就是C with Classes所诉求的:

classes(包括构造函数和析构函数)encapsulation(封装)inheritance(继承)polymorphism(多态)virtual function(虚函数动态绑定)etc.

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

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 dataconst char* p = greeting; // non-const pointer, const datachar* const p = greeting; // const pointer, non-const dataconst 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; // errorstd::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]; }#endifprivate: 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]; }#endifprivate: 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 glength: do strlen... 5Jlength: 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[]() constgconst char& operator[]() consty */

请记住:

将某些东西声明为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


发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表