一、让自己习惯C++

写在前面

第一遍看《Effective C++》时,在准备暑期实习生的招聘,没有时间好好地捋一下,将一些要点记录下来。现在实习回来,重读此书,并记录一些要点,为今后的复习亦或是学习铺垫。

这篇介绍第一章的4个条款。

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

  1. C++是一个多重范型编程语言:
  • 支持过程形式
  • 支持面向对象形式
  • 支持函数/泛型形式
  • 支持元编程形式
  1. 理解C++,必须首先认识其主要的次语言:
  • C语言。 C++ 以C为基础,区块,语句,预处理器,内置数据类型,数组,指针等全部都来自于C。C++是C的高级解法揭露了C语言的局限性:
    • 没有模板。
    • 没有异常处理。
    • 没有重载。
    • ...(以及封装,继承等面向对象的特性等其它)
  • Object-Oriented C++: 简单总结就是面向对象的特点。
    • 类。每个类都有构造函数,析构函数。
    • 封装,继承,多态,虚函数(动态绑定).
  • Template C++: C++范型编程的部分。有了模板可以带来崭新的编程范型。
  • STL: Standard Template Library。对容器,迭代器,算法以及函数对象的规约有极佳的机密配合与协调。
  1. 对于内置类型,pass-by-value通常比pass-by-reference更加高效,但是对于用户自定义(user-defined)类型,由于构造函数和析构函数的存在,pass-by-reference-to-const往往更好。

作者总结:

C++的高效编程守则视状况而变化,取决于你使用C++的哪一部分。

个人总结:

此条款介绍了C++的组成部分,在开发过程中,要高效利用C++的这几个“次语言”特性,在使用不同的“次语言”的时候,要选择相应的高效的编程方式。

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

首先要明白使用#define的缺点:

  • 如#define NUM 1.2,调试的时候出现的是1.2而不是NUM,如果这个宏定义不是自己写的就更难定位问题了。

  • 宏定义#define MAX(a,b) ((a) > (b) ? (a) : (b))看似可行的一个宏定义函数,但是考虑以下情况:

    int a = 5,b = 0;
    MAX(a++,b); // a被累加两次
    MAX(++a,b + 10); // a被累加一次
    a的累加次数取决于a和b的大小,显然不是调用者所期待的情形。

class的专属常量

假定我们在GamePlayer类中有个常量成员,有个数组,数组大小使用该常量表示。

class GamePlayer
{
public:
    static const int iNum = 5;  //常量声明式
    int iScores[iNum];
    。。。              // 其它成员
}

要明确一点:上述const是一个声明式,并不是一个定义式。

为什么要声明为static?

如果不是static,在该类还未构造时,iNum是不存在的,编译器也就无法知道数组iScores的大小。编译器会坚持要求知道数组的大小。

旧式的编译器中,不允许static在声明的时候不允许被赋初值。如果不支持声明时候赋初值,就应该改为:

class GamePlayer
{
public:
    static const int iNum;
    ...
}

在函数体外再赋初值:

int GamePlayer::iNum = 5;

但是采取这种写法就无法在类中定义一个常量大小的数组。

采用enum解决

声明enum常量,就可以防止不同编译器对const能否赋初值所带来的不便之处。

class GamePlayer
{
public:
    enum { iNum = 5 };
    int iScores[iNum];
}

使用enum的更多好处

enum声明的常量是一个右值。如果不想别人用一个pointer或者reference指向一个整型常量,使用enum即可。引用和指针都无法绑定在一个枚举常量上。

enum
{
	first = 1,
};
//int &First = first;   无法通过编译
//int *pFirst = &first; 无法通过编译

作者总结

对于单纯常量,最好以const对象或enum替换#define.

对于形似函数的宏,最好改用inline function替换#define.

条款03:尽可能使用const

先写一下老生常谈的const和pointer的不同组合的效果。

char hello[] = "hell0";
char *p1 = hello;               // non-const pointer,non-const data
const char *p2 = hello;         // non-const pointer,const data
char* const p3 = hello;         // const pointer,non-const data
const char* const p = hello;    // const pointer,const data

初学者不容易记住,其实只要记住const后面是什么(数据类型不看),什么就不变就对了。

比如说,const char *p,const 后面是 *p, p是指针所指向的数据,所以是data不变。又比如 char const p; const后面是p,p是一个指针,所以是个const pointer,指针指向的地址不能改变。

由此延申出const在STL迭代器中的使用

假设我们用一个迭代器指针去操作一个vector容器:
如果用const显式修饰:

vector<int> vct(10,1);
const vector<int>::iterator it = vct.begin(); // 此迭代器指针是一个non-const data,const pointer类型。
*it = 9;    // 正确。
++it;       // 错误,const pointer不能改变指向的位置。

上述代码中,const 修饰的迭代器指向的地址不可变,所以只能指向vct.begin()位置。

此外,STL的迭代器中有一个const_iterator,是一个non-const pointer,const data类型的迭代器。

vector<int> vct(10,1);
vector<int>::const_iterator cIt;
*cIt = 10;      // 错误,const data,不可改变其值。
++cIt;          // 正确。non-const pointer.可以改变其指向。

令函数返回一个常量值可以降低错误发生的概率

const Rational operator *(const Rational &lhs, const Rational &rhs);

如果返回的不是const,那么很可能写成

if(a * b = c)

这个是程序员写错的时候的情形,如果返回const那么就会提示报错,就能立即定位错误。而如果不是const类型,那么这个错误就可能很难被发现。

const修饰成员函数

const修饰的成员函数,在函数体内不能修改任何一个成员变量。如果是可能被修改的成员变量,那么这些成员变量应该是使用mutable关键词来修饰。mutable关键词可以去掉non-static成员变量的bitwise constness约束

注意:两个成员函数的常量性不同,是可以被重载的。

例如:

class TextBlock
{
public:
    const char &operator[](std::size_t position) const
    {
        ... // 记录数据1
        ... // 记录数据2
        ... // 记录数据3
        return text[position];
    }
    char & operator[](std::size_t position)
    {
        ... // 记录数据1
        ... // 记录数据2
        ... // 记录数据3
        return text[position];
    }
}

如果这两个版本实现了相同的函数体,只是返回值的常量性不同,那么可以将non-const版本改成以下版本:

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

这语句有两个转型的动作:

(1) static_cast<const TextBlock&>.将当前对象转成const的对象。

(2) const_cast<char &>是去掉const属性,恢复成原来的非const对象。

我们重载了[]运算符,const和非const版本都有。当对象为const 属性的时候调用的是const版本,非const属性对象就调用非const版本。

这样写可以避免代码冗余。

注意:只能用非const去调用const,如果使用const去调用非const,那么就先要将const属性去掉,那么原本const函数体中的数据就不被保证不会被修改,也就失去了我们一开始使用const修饰的初衷。

作者总结

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

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

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

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

先搞清楚赋值和初始化是不一样的:
假设有一个类ABEntry:

class ABEntry
{
public:
    ABEntry(const string name,const string addr);
private:
    string theName;
    string theAddr;
}

以下的构造函数中是赋值给私有成员变量,而不是初始化私有成员变量。

ABEntry::ABEntry(const string name,const string addr)
{
    theName = name;     // 赋值
    theAddr = addr;     // 赋值
}

真正的初始化:

ABEntry::ABEntry(const string name,const string addr)
:theName(name),theAddr(addr)
{
    
}

二者的不同:
第一个构造函数中:

(1) 在赋值之前,theName和theAddr先执行了它们各自的默认构造函数,也就是string类中的默认构造函数,有了一个初值(为空)。

(2) 进行赋值的时候,调用了copy assignment操作符。将name和addr复制给theName和theAddr.

所以它充其量只是一个赋值,并不能说是初始化,第一小步就已经初始化成一个空string了。

而在第二个构造函数中,只调用了一个copy构造函数去构造初始值。《C++ Primer》中将这种初始化方式叫做成员列表初始化。

单单使用一个copy构造函数显然是比较高效的。在内置类型中,不需要调用默认构造函数,二者的效率是差不多的。

const和reference初始化

由于const和reference一定需要初值,而不能被赋值改变,所以需要采用成员列表初始化的方式来进行初始化操作。

成员变量的初始化顺序

在C++中,成员变量的初始化顺序严格遵守变量的声明顺序。

class Text
{
public:
    ...
private:
    string strAddr;
    string strName;
    int iCall;
}

在上述类之中,如果采用成员列表初始化,那么初始化顺序依此为strAddr,strName,iCall.如果需要使用strName的值去初始化strAddr, 那么是错误的做法,因为strAddr先于strName初始化,strName这个时候尚未有值。

作者总结:

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

原文地址:https://www.cnblogs.com/love-jelly-pig/p/9612936.html