1. 基础议题 摘录

基础议题。是的,pointer、reference、cast、arrays、constructors。

条款1: 仔细区分pointers和reference

首先必须认知一点,没有所谓的null reference。一个refrence必须总代表某个对象。所以如果你有一个变量,其目的是用来指向(代表)另一个对象,但是也有可能它不指向任何对象,那么你应该使用pointer。换个角度,如果这个变量必须代表一个对象,也就是说如果你的设计并不允许这个变量为null,那么你应该使用reference。

char* pc = 0;       // 将pointer设置为null
// 其结果未定义
char& rc = *pc;     // 让reference代表null pointer

由于reference一定得代表某个对象,C++因此要去reference必须有初值:

string& rs;         // wrong, reference必须被初始化
string s("xyhhs");
string& rs = s;     //没问题

但是pointer没有这样的限制:

string* ps; 

没有所谓的null reference意味着使用reference可能会比ponter效率更高,这是因为使用refernce之前不需要测试其有效性。

void printData(const double& rd)
{
    cout << rd;
}

void printData(const double* rd)
{
    if (pd)
    cout << *pd;
}

pointer和reference之间的另一个重要性差异就是,pointer可以被重新赋值,指向另一个对象,reference却总是指向它最初获得的那个对象。

string s1("Nancy");
string s2("Clanncy");

string& rs = s1;
string* = &s1;

rs = s2;        // rs仍然代表s1,只是s1的值为s2的值
ps = &s2;       // ps现在指向s2

当你需要考虑“不指向任何对象”的可能性时,或是考虑“在不同时间指向不同对象”的能力时,你就应该考虑采用pointer,前一种情况你可以将pointer设为null,后一种情况你可以改变pointer所指对象。而当你确定“总会代表某个对象”,而且“一旦代表了该对象就不能在改变”,那么你应该选用reference。

还有其他情况也需要使用reference,例如当你实现某些操作时候,最常见的例子是operator[]。

vector<int> v(10);
v[5] = 12;

// 如果operator[]返回指针,则
*v[5] = 12;

但这使得v看起来好像是以指针形成的vector,事实上它不是。

因此,让我做一下结论:当你知道需要指向某个东西,而且绝不会改变指向其他东西,或是当你实现一个操作符而其语法需求无法由pointer达成,你就应该选择reference,任何其他时候,请采用pointer。

条款2: 最好使用C++转型操作符

旧式C转型方式并非唯一选择, 它几乎允许你讲任何类型转换为任何其他类型,这是十分拙劣的。

旧式转型第二个问题是它们难以辨识。旧式转型语法结构是由一对小括号加上一个对象名称(标识符)组成,而小括号和对象名称在C++的任何地方都有可能被使用。因此,我们简直无法回答最基本的转型相关问题“这个程序中有使用任何转型动作吗?”

为了解决C旧式转型的缺点,C++导入4个新的转型操作符(cast operator)。将过去(type)expression改写成static_cast<type>(expression)。

int firstNumber, secondNumber;
double result = (double) firstNumber / secondNumber;

double result = static_cast<double>(firstNumber) / secondNumber;

static_cast基本上拥有与C旧式转型相同的威力和意义,以及相同的限制。

其他新式C++转型操作符适用于更狭窄的目的。const_cast用来改变表达式的常量性(constness)或变易性(volatileness)。

class Widget {...};
class SpecialWidget : public Widget {...};

void updata(SpecialWidget* psw);
SpecialWidget sw;
const SpecialWidget& csw = sw;

updata(&csw); // wrong, 不能将const类型传递给非const参数
updata(const_cast<SpecialWidget*>(&csw)); // OK

const_cast最常见的用途就是将某个对象的常量性去除。

第二个特殊化的转型操作符是dynamic_cast,用来执行继承体系中“安全向下转型或跨系类型转换”。可以使用dynamic_cast,将指向base class objects的pointer或reference转型为derived class objects的pointer或reference,并得知转型是否成功。如果转型失败,会以一个null指针或一个exception。

Widget* pw;
...
updata(pw); // wrong,不能将Widget类型传递给SpecialWidget参数

updata(dynamic_cast<SpecialWidget*>(PW));   // ok

dynamic_cast只能用来协助你巡航与继承体系之中。它无法应用在缺乏虚函数的类型上,也不能改变类型的常量性。

如果你香味一个不涉及继承机制的类型执行转型动作,可使用static_cast,要改变constness,可使用const_cast。

reinterpret_cast操作符的转换结果几乎总是与编译平台息息相关。所以其不具备移植性。reinterpret_cast最常见的用途是转换“函数指针”类型。假设有一个数组。存储的都是函数指针,有特定的类型:

typedef void (*FunctPtr)();
FunctPtr funcPtrArray[10];

int doSomething();

funcPtrArray[0] = doSomething; //wrong,类型不符合
funcPtrArray[0] = reinterpret_cast<FunctPtr>(doSomething); // OK!

函数指针的转型动作,并不具有移植性(C++不保证所有的函数指针都能以此方式重新呈现),某些情况下这样的转型可能会导致不正确的结果。

如果你的编译器尚未支持这些新式转型动作,可以使用宏来仿真这些新语法。

#define static_cast(TYPE, EXPR)     ((TYPE)(EXPR))
#define const_cast(TYPE, EXPR)     ((TYPE)(EXPR))
#define dynamic_cast(TYPE, EXPR)     ((TYPE)(EXPR))

至于可能的第三个因素是:让转型动作既丑陋又不易键入,或许未尝不是件好事。

条款3: 绝对不要以多态形式处理数组

继承最重要的性质之一:可以通过指向base class objects的pointer或reference,来操作derived class objects。

class BST {};
class BalancedBST : public BST {};

void printBSTArray(ostream& s, const BST array[], int numElements)
{
    for (int i = 0; i < numElements; i++)
        s << array[i];
}

BST BSTArray[10];
...
printBSTArray(cout, BSTArray, 10);

当我们将一个BST组成的数组传递给此函数,没问题。但是当我们将一个BallancedBST对象所组成的数组交个printBSTArray。编译器会毫无怨言的接受它。

BalancedBST bBSTArray[10];
...
// 此时错误来了
printBSTArray(cout, bBSTArray, 10);

此时编译器仍假设数组中每一个元素大小是BST的大小,但其实每一个元素的大小是BalancedBST的大小。如此编译器为printBSTArray函数所产生的指针算术表达式,对于BalancedBST对象所组成的数组就是错误的。

如果你尝试通过一个base class指针,删除一个有derived class objects组成的数组,那么上述问题还会以另一个不同的面貌出现。

void deleteArray(ostream& logStream, BST array[])
{
    logStream << " Deleting array at address " 
              << static_cast<void*>(array) << '\n';
    delete [] array;
}

BalancedBST* balTreeArray = new BalancedBST(50);
...
deleteArray(cout, balTreeArray);

C++语言规范说,通过base class指针删除一个由derived class objects构成的数组,其结果未定义。简单地说,多态(polymorphism)和指针算术不能混用。数组对象几乎总是会涉及指针的算术运算,所以数组和多态不要混用。

条款4:非必要不要提供default constructor

constructor用来将对象初始化,所以default constructor的意思是在没有任何外来信息的情况下降对象初始化。

有许多对象,如果没有外来信息,就没办法执行一个完全的初始化动作。

在一个完美的世界中,方可以“合理地从无到有生成对象的”classes,都应该内含deault constructor。

class EquipmentPiece
{
public:
    EquipmentPiece(int IDNumber);
    ...
};

EquipmentPiece缺乏default constructor,其运行可能在3种情况下出现问题。第一个情况是产生数组时候。一般而言没有任何办法可以为数组中的对象指定constructor自变量,所以几乎不可能产生一个由EquipmentPiece objects构成的数组。

EquipmentPiece bestPieces[10];  // 错误! 无法调用ctors
EquipmentPiece* bestPieces = new EquipmentPiece[10] // 错误!另有一些问题

有3个方法可以侧面解决这个束缚。第一个方法是使用non-heap数组,于是便能够在定义数组时提供必要的自变量:

int ID1, ID2, ID3,...,ID10;
EquipmentPiece bestPieces[] = {EquipmentPiece(ID1), EquipmentPiece(ID2),...};

不幸的是此法无法延伸至heap数组。

更一般的做法是使用“指针数组”而非“对象数组”。

typedef EquipmentPiece* PEP;
PEP bestPieces[10]; // 行
PEP* bestPieces = new PEP[10];  // 也行

for (int i = 0; i < 10; i++)
    bestPieces[i] = new EquipmentPiece(ID number);

此法有两个缺点,第一,你必须记得将数组所指的所有对象删除。如果忘了,则会出现resource leak。第二,你需要的内存总量比较大,因为你需要一些空间来放置指针,还需要一些空间来放置EquipmentPieces objects。

// 分配足够的raw memory,给一个预备容纳10个EquipmentPieces objects
// 的数组使用。
void* rawMemory = operator new[] (10 * sizeof(EquipmentPiece));

// 让bestPieces指向此内存,使这块内存
// 被视为一个EquipmentPieces数组
EquipmentPiece* bestPieces = static_cast<EquipmentPiece*>(rawMemory);

// 利用“placement new”构造这块
// 内存中的EquipmentPieces objects
for (int i = 0; i < 10; i++)
    new (&bestPieces[i]) EquipmentPiece(IDNumber);

placement new的缺点是:大部程序员不怎么熟悉它,维护起来比较困难。此外,你还得在数组内的对象结束生命时,以手动的方式调用其destructor,最后还得以调用operator delelte[]的方式释放raw memory。

// 将bestPieces中的各个对象,以其构造顺序的相反顺序析构掉
for (int i = 9; i >= 0; --i)
    bestPieces[i].~EquipmentPiece();

operator delete[](rawMemory);

delete [] bestPieces;   // 没有定义,因为bestPieces并非来自new operator

class 如果缺乏default constructor带来的第二个缺点就是:它们将不适用于许多template-based container classes。对那些template而言,被实例化的“目标类型”必须得有一个default constructor。例如一个Array类:

template <class T>
class Array
{
public:
    Array(int size);
    // ...
private:
    T* data;
};

template<class T>
Array<T>::Array(int size)
{
    data = new T[size];
    // ...
}

virtual base classes如果缺乏default constructor,与之合作将是一种刑罚。因为virtual base class constructor的自变量必须由欲产生的对象的派生层次最深的class提供。于是,一个缺乏default constructor的virtual base class,要求其所有的derived class--不论距离多么遥远--都必须知道、了解其意义,并且提供virtual base class的constructor自变量。

由于缺乏default constructor带来的诸多束缚,有些人便认为所有class都应该提供default constructor--甚至即使其default constructor没有足够信息将对象做完整的初始化。此时的EquipmentPieces可能被修改如下;

class EquipmentPiece
{
public:
    EquipmentPiece(INT IDNumber = UNSPECIFIED);
    // ...
private;
    static const int UNSPECIFIED;
};

会造成class内的其他member function变得复杂,因为这不保证一个EquipmentPieces object的所有字段都是富有意义的初值。

一个无ID值的EquipmentPieces object竟然是可以存在的。这部分的实现策略并不明朗,许多编译器选择的解决方法是:声明都不做,仅提供便利性,它们抛出一个exception,或是调用某函数将程序结束掉。

添加无意义的default constructor,也会影响class的效率。如果class constructor可以确保对象的所有字段都会被正确的初始化,上述的成本便可以免除。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值