More Effective C++----(1)指针与引用的区别 & (2)尽量使用C++风格的类型转换 & (3)不要对数组使用多态

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++风格的类型转换


仔细想想地位卑贱的类型转换功能(cast),其在程序设计中的地位就象goto语句一样令人鄙视。但是它还不是无法令人忍受,因为当在某些紧要的关头,类型转换还是必需的,这时它是一个必需品。

不过C风格的类型转换并不代表所有的类型转换功能。

一来它们过于粗鲁, 能允许你在任何类型之间进行转换。不过如果要进行更精确的类型转换,这会是一个优点。在这些类型转换中存在着巨大的不同,例如把一个指向const对象的指针(pointer-to-const-object)转换成指向非const对象的指针(pointer-to-non-const-object)(即一个仅仅去除const的类型转换),把一个指向基类的指针转换成指向子类的指针(即完全改变对象类型)。传统的C风格的类型转换不对上述两种转换进行区分。(这一点也不令人惊讶,因为 C风格的类型转换是为C语言设计的,而不是为C++语言设计的)。

二来C风格的类型转换 在程序语句中难以识别在语法上,类型转换由圆括号和标识符组成,而这些可以用在C++中的任何地方。这使得回答象这样一个最基本的有关类型转换的问题变得很困难:“在这个程序中是否使用了类型转换?”。这是因为人工阅读很可能忽略了类型转换的语句,而利用象 grep的工具程序也不能从语句构成上区分出它们来。

C++通过引进四个新的类型转换操作符克服了C风格类型转换的缺点,这四个操作符是, static_cast, const_cast, dynamic_cast, 和reinterpret_cast。在大多数情况下,对于这些操作符你只需要知道原来你习惯于这样写,
(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;
这样的类型转换不论是对人工还是对程序都很容易识别。

static_cast在功能上基本上与C风格的类型转换一样强大,含义也一样。它也有功能上限制。例如,你 不能用static_cast象用C风格的类型转换一样把struct转换成int类型或者把double类型转换成指针类型,另外,static_cast 不能从表达式中去除const属性,因为另一个新的类型转换操作符const_cast有这样的功能。

其它新的C++类型转换操作符被用在需要更多限制的地方。 const_cast用于类型转换掉表达式的const或volatileness属性。通过使用const_cast,你向人们和编译器强调你通过类型转换想做的只是改变一些东西的constness或者 volatileness属性。这个含义被编译器所约束。 如果你试图使用const_cast来完成修改constness 或者volatileness属性之外的事情,你的类型转换将被拒绝。下面是一些例子:
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的地方上。,
                    // 不能用在向继承子类进行类型转换。
到目前为止, const_cast最普通的用途就是转换掉对象的const属性

第二种特殊的类型转换符是 dynamic_cast,它被用于安全地沿着类的继承关系向下进行类型转换。这就是说, 你能用dynamic_cast把指向基类的指针或引用转换成指向其派生类或其兄弟类的指针或引用,而且 你能知道转换是否成功 。失败的转换将 返回空指针 (当对指针进行类型转换时)或者 抛出异常 (当对引用进行类型转换时)
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。
如你想在没有继承关系的类型中进行转换,你可能想到static_cast。如果是为了去除const,你总得用const_cast。

这四个类型转换符中的最后一个是reinterpret_cast。使用这个操作符的类型转换,其的 转换结果几乎都是执行期定义(implementation-defined)。因此,使用reinterpret_casts的代码很难移植。

reinterpret_casts的最普通的用途就是在函数指针类型之间进行转换。例如,假设你有一个函数指针数组:
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);
转换函数指针的代码是 不可移植的(C++不保证所有的函数指针都被用一样的方法表示),在一些情况下这样的转换会产生不正确的结果(参见条款M31),所以你应该避免转换函数指针类型,除非你处于着背水一战和尖刀架喉的危急时刻。一把锋利的刀。一把非常锋利的刀。

如果你使用的编译器缺乏对新的类型转换方式的支持,你可以用传统的类型转换方法代替static_cast, const_cast, 以及reinterpret_cast。也可以用下面的宏替换来模拟新的类型转换语法:
#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);
这些模拟不会象真实的操作符一样安全,但是当你的编译器可以支持新的的类型转换时,它们可以简化你把代码升级的过程。

没有一个容易的方法来模拟dynamic_cast的操作,但是很多函数库提供了函数,安全地在派生类与基类之间进行类型转换。如果你没有这些函数而你有必须进行这样的类型转换,你也可以回到C风格的类型转换方法上,但是这样的话你将不能获知类型转换是否失败。当然,你也可以定义一个宏来模拟dynamic_cast的功能,就象模拟其它的类型转换一样:
#define dynamic_cast(TYPE,EXPR)     (TYPE)(EXPR)
请记住,这个模拟并不能完全实现dynamic_cast的功能,它没有办法知道转换是否失败。

我知道,是的,我知道,新的类型转换操作符不是很美观而且用键盘键入也很麻烦。如果你发现它们看上去实在令人讨厌,C风格的类型转换还可以继续使用并且合法。然而,正是因为新的类型转换符缺乏美感才能使它弥补了在含义精确性和可辨认性上的缺点。并且, 使用新类型转换符的程序更容易被解析(不论是对人工还是对于工具程序),它们允许编译器检测出原来不能发现的错误。这些都是放弃C风格类型转换方法的强有力的理由。还有第三个理由:也许让类型转换符不美观和键入麻烦是一件好事。

Item M3: 不要对数组使用多态


类继承的最重要的特性是你可以通过基类指针或引用来操作派生类。这样的指针或引用具有行为的 多态性,就好像它们同时具有多种形态。C++允许你通过基类指针和引用来操作派生类数组。不过这根本就不是一个特性,因为这样的代码几乎从不如你所愿地那样运行。

假设你有一个类BST(比如是搜索树对象)和继承自BST类的派生类BalancedBST:
class BST { ... }; 
class BalancedBST: public BST { ... };
在一个真实的程序里,这样的类应该是模板类,但是在这个例子里并不重要,加上模板只会使得代码更难阅读。为了便于讨论,我们假设BST和BalancedBST只包含int类型数据。

有这样一个函数,它能打印出BST类数组中每一个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];
}
这里的array[I]只是一个指针算法的缩写:它所代表的是*(array)。我们知道array是一个指向数组起始地址的指针,但是array中各元素内存地址与数组的起始地址的间隔究竟有多大呢?它们的间隔是 i*sizeof(一个在数组里的对象),因为在array数组[0]到[I]间有I个对象。编译器为了建立正确遍历数组的执行代码,它必须能够确定数组中对象的大小,这对编译器来说是很容易做到的。参数array被声明为BST类型,所以array数组中每一个元素都是BST类型,因此每个元素与数组起始地址的间隔是i*sizeof(BST)。

至少你的编译器是这么认为的。但是如果你把一个含有BalancedBST对象的数组变量传递给printBSTArray函数,你的编译器就会犯错误。在这种情况下,编译器原先已经假设数组中元素与BST对象的大小一致,但是现在数组中每一个对象大小却与BalancedBST一致。 派生类的长度通常都比基类要长。我们料想BalancedBST对象长度的比BST长。如果如此的话,printBSTArray函数生成的指针算法将是错误的, 没有人知道如果用BalancedBST数组来执行printBSTArray函数将会发生什么样的后果。不论是什么后果都是令人不愉快的。

如果你试图删除一个含有派生类对象的数组,将会发生各种各样的问题。以下是一种你可能采用的但不正确的做法。
//删除一个数组, 但是首先记录一个删除信息
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]的
  }                                           // 析构函数
因为你所编写的循环语句根本不能正确运行,所以当编译成可执行代码后,也不可能正常运行。 语言规范中说通过一个基类指针来删除一个含有派生类对象的数组,结果将是不确定的。这实际意味着执行这样的代码肯定不会有什么好结果。 多态和指针算法不能混合在一起来用,所以数组与多态也不能用在一起。

值得注意的是如果你不从一个具体类(concrete classes)(例如BST)派生出另一个具体类(例如BalancedBST),那么你就不太可能犯这种使用多态性数组的错误。正如条款M33所解释的,不从具体类派生出具体类有很多好处。我希望你阅读一下条款M33的内容。

(WQ加注:VC++中,能够有正确的结果,因为它根本没有数组new/delete函数。)








  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值