Item M1:指针与引用的区别
指针与引用看上去完全不同(指针用操作符“*”和“->”,引用使用操作符“. ”),但是它们似乎有相同的功能。指针与引用都是让你间接引用其他对象。你如何决定在什么时候使用指针,在什么时候使用引用呢?
首先,要认识到在任何情况下都不能使用指向空值的引用。一个引用必须总是指向某些对象。因此如果你使用一个变量并让它指向一个对象,但是该变量在某些时候也可能不指向任何对象,这时你应该把变量声明为指针,因为这样你可以赋空值给该变量。相反,如果变量肯定指向一个对象,例如你的设计不允许变量为空,这时你就可以把变量声明为引用。
“但是,请等一下”,你怀疑地问,“这样的代码会产生什么样的后果?”char *pc = 0; // 设置指针为空值
char& rc = *pc; // 让引用指向空值
这是非常有害的,毫无疑问。结果将是不确定的(编译器能产生一些输出,导致任何事情都有可能发生)。应该躲开写出这样代码的人,除非他们同意改正错误。如果你担心这样的代码会出现在你的软件里,那么你最好完全避免使用引用,要不然就去让更优秀的程序员去做。我们以后将忽略一个引用指向空值的可能性。
因为引用肯定会指向一个对象,在C++里,引用应被初始化。
string& rs; // 错误,引用必须被初始化
string s("xyzzy");
string& rs = s; // 正确,rs指向s
指针没有这样的限制。
string *ps; // 未初始化的指针
// 合法但危险
不存在指向空值的引用这个事实意味着使用引用的代码效率比使用指针的要高。因为在使用引用之前
不需要测试它的合法性
。
void printDouble(const double& rd)
{
cout << rd; // 不需要测试rd,它
} // 肯定指向一个double值
相反,指针则应该
总是被测试
,
防止其为空
:
void printDouble(const double *pd)
{
if (pd) { // 检查是否为NULL
cout << *pd;
}
}
指针与引用的另一个重要的不同是
指针可以被重新赋值以指向另一个不同的对象。但是引用则总是指向在初始化时被指定的对象,以后不能改变。
string s1("Nancy");
string s2("Clancy");
string& rs = s1; // rs 引用 s1
string *ps = &s1; // ps 指向 s1
rs = s2; // rs 仍旧引用s1,
// 但是 s1的值现在是
// "Clancy"
ps = &s2; // ps 现在指向 s2;
// s1 没有改变
总的来说,在以下情况下你应该使用指针,一是你考虑到存在不指向任何对象的可能(在这种情况下,你能够设置指针为空),二是你需要能够在不同的时刻指向不同的对象(在这种情况下,你能改变指针的指向)。如果总是指向一个对象并且一旦指向一个对象后就不会改变指向,那么你应该使用引用。
还有一种情况,就是当你 重载某个操作符时,你应该使用引用。最普通的例子是操作符[]。这个操作符典型的用法是返回一个目标对象,其能被赋值。vector<int> v(10); // 建立整形向量(vector),大小为10;
// 向量是一个在标准C库中的一个模板(见条款M35)
v[5] = 10; // 这个被赋值的目标对象就是操作符[]返回的值
如果操作符[]返回一个指针,那么后一个语句就得这样写:
*v[5] = 10;
但是这样会使得v看上去象是一个向量指针。因此你会选择让操作符返回一个引用。(这有一个有趣的例外,参见条款M30)
当你知道你必须指向一个对象并且不想改变其指向时,或者在重载操作符并为防止不必要的语义误解时,你不应该使用指针。而在除此之外的其他情况下,则应使用指针。Item M2: 尽量使用C++风格的类型转换
(type) expression
而现在你总应该这样写:
static_cast<type>(expression)
例如,假设你想把一个int转换成double,以便让包含int类型变量的表达式产生出浮点数值的结果。如果用C风格的类型转换,你能这样写:
int firstNumber, secondNumber;
...
double result = ((double)firstNumber)/secondNumber;
如果用上述新的类型转换方法,你应该这样写:
double result = static_cast<double>(firstNumber)/secondNumber;
class Widget { ... };
class SpecialWidget: public Widget { ... };
void update(SpecialWidget *psw);
SpecialWidget sw; // sw 是一个非const 对象。
const SpecialWidget& csw = sw; // csw 是sw的一个引用
// 它是一个const 对象
update(&csw); // 错误!不能传递一个const SpecialWidget* 变量
// 给一个处理SpecialWidget*类型变量的函数
update(const_cast<SpecialWidget*>(&csw));
// 正确,csw的const被显示地转换掉(
// csw和sw两个变量值在update
//函数中能被更新)
update((SpecialWidget*)&csw);
// 同上,但用了一个更难识别
//的C风格的类型转换
Widget *pw = new SpecialWidget;
update(pw); // 错误!pw的类型是Widget*,但是
// update函数处理的是SpecialWidget*类型
update(const_cast<SpecialWidget*>(pw));
// 错误!const_cast仅能被用在影响
// constness or volatileness的地方上。,
// 不能用在向继承子类进行类型转换。
Widget *pw;
...
update(dynamic_cast<SpecialWidget*>(pw));
// 正确,传递给update函数一个指针
// 是指向变量类型为SpecialWidget的pw的指针
// 如果pw确实指向一个对象,
// 否则传递过去的将使空指针。
void updateViaRef(SpecialWidget& rsw);
updateViaRef(dynamic_cast<SpecialWidget&>(*pw));
//正确。 传递给updateViaRef函数
// SpecialWidget pw 指针,如果pw
// 确实指向了某个对象
// 否则将抛出异常
dynamic_casts在帮助你浏览继承层次上是
有限制的。
它
不能被用于缺乏虚函数的类型上
(参见条款M24),也不能用它来转换掉constness:
int firstNumber, secondNumber;
...
double result = dynamic_cast<double>(firstNumber)/secondNumber;
// 错误!没有继承关系
const SpecialWidget sw;
...
update(dynamic_cast<SpecialWidget*>(&sw));
// 错误! dynamic_cast不能转换
// 掉const。
typedef void (*FuncPtr)(); // FuncPtr is 一个指向函数
// 的指针,该函数没有参数
// 返回值类型为void
FuncPtr funcPtrArray[10]; // funcPtrArray 是一个能容纳
// 10个FuncPtrs指针的数组
让我们假设你希望(因为某些莫名其妙的原因)把一个指向下面函数的指针存入funcPtrArray数组:
int doSomething();
你不能不经过类型转换而直接去做,因为doSomething函数对于funcPtrArray数组来说有一个错误的类型。在FuncPtrArray数组里的函数返回值是void类型,而doSomething函数返回值是int类型。
funcPtrArray[0] = &doSomething; // 错误!类型不匹配
reinterpret_cast可以让你迫使编译器以你的方法去看待它们:
funcPtrArray[0] = // this compiles
reinterpret_cast<FuncPtr>(&doSomething);
#define static_cast(TYPE,EXPR) ((TYPE)(EXPR))
#define const_cast(TYPE,EXPR) ((TYPE)(EXPR))
#define reinterpret_cast(TYPE,EXPR) ((TYPE)(EXPR))
你可以象这样使用:
double result = static_cast(double, firstNumber)/secondNumber;
update(const_cast(SpecialWidget*, &sw));
funcPtrArray[0] = reinterpret_cast(FuncPtr, &doSomething);
#define dynamic_cast(TYPE,EXPR) (TYPE)(EXPR)
Item M3: 不要对数组使用多态
class BST { ... };
class BalancedBST: public BST { ... };
void printBSTArray(ostream& s,
const BST array[],
int numElements)
{
for (int i = 0; i < numElements; ) {
s << array[i]; //假设BST类
} //重载了操作符<<
}
当你传递给该函数一个含有BST对象的数组变量时,它能够正常运行:
BST BSTArray[10];
...
printBSTArray(cout, BSTArray, 10); // 运行正常
然而,请考虑一下,当你把含有BalancedBST对象的数组变量传递给printBSTArray函数时,会产生什么样的后果:
BalancedBST bBSTArray[10];
...
printBSTArray(cout, bBSTArray, 10); // 还会运行正常么?
你的编译器将会毫无警告地编译这个函数,但是再看一下这个函数的循环代码:
for (int i = 0; i < numElements; ) {
s << array[i];
}
//删除一个数组, 但是首先记录一个删除信息
void deleteArray(ostream& logStream, BST array[])
{
logStream << "Deleting array at address "
<< static_cast<void*>(array) << '\n';
delete [] array;
}
BalancedBST *balTreeArray = // 建立一个BalancedBST对象数组
new BalancedBST[50];
...
deleteArray(cout, balTreeArray); // 记录这个删除操作
这里面也掩藏着你看不到的指针算法。当一个数组被删除时,每一个数组元素的析构函数也会被调用。当编译器遇到这样的代码:
delete [] array;
它肯定象这样生成代码:
// 以与构造顺序相反的顺序来
// 解构array数组里的对象
for ( int i = 数组元素的个数 1; i >= 0;--i)
{
array[i].BST::~BST(); // 调用 array[i]的
} // 析构函数