(转载请注明原创于潘多拉盒子)
一本典型的C语言教科书的厚度大约是200页左右,而一本典型的C++教科书的厚度至少要500页。比如K&R的《The C Programming Language》的厚度是272页,而权威性于此大致相当的Stroustrup的C++教科书《The C++ Programming Language》的厚度是1019页,后者是前者的3.75倍。这给C++工作者带来了沉重的负担,纯记忆这些内容就已非易事,能深入理解则消耗更多的实践,如果想把C++的每一个特性熟练恰当的运用,则近乎天方夜谭。
这源于C++的特性之复杂。为了在底层上兼容C并保持较高的运行效率,C++尽管号称对语言本身的特性进行了大大的限制,然而实际的结果并不理想。这里有大量的特性是程序员无法准确把握的,或者会造成误解,容易引起错误的。比如C++的默认参数特性,这个特性设计的本意是为了让C++的接口调用者使用起来更简单,或者对接口保持向下兼容,然而实际的效果并不理想。需要考虑的情况实在是太多了。对于一个含有默认参数的调用者来说,他需要搞清楚默认的参数是哪个,默认的情况下参数取值是什么,这些取值很多情况下并不是显而易见的,每到这样的一个地方就需要仔细的检查,看看是不是与接口设计的一致。非常容易发生错误。默认参数的个数还可能超过一个,这种情况下还夹杂类型隐式转换,实在是让人头疼不已。
因此不难看出,C++并非每一个特性都适合在实际中使用,而其中大量的特性,其实是实际中不适合使用的。因此很有必要有一个详细的介绍,能将C++中的优秀特性圈出来,让程序员在实际开发时优先选择这些优秀特性,而对其它C++特性的使用则采取谨慎的态度。
如果你之前用过别的高级语言,如Java/Python之类的,那么C++的引用特性可能有一部分是容易理解的,而有一部分则是有所不同的。
int a = 0; int& b = a; // b是a的引用,对b的所有操作等同于作用到a上,包括作为左值和右值 b = 10; // 此时a = 10,b = 10 int* c = &b; // c持有的是a的地址,也是b的地址
这里的int& b定义了一个引用,需要立刻将被引用的对象作为右值赋值。
这段程序至少可以得到以下几个结论:
- 引用实际上是一个别名,相当于对变量换了个名字,其余不变(包括变量的地址)。
- 引用一旦指向一个对象,则这种指向关系不能变更。
- 对引用的操作等同于对它引用的对象进行操作。
- 引用的生命期包含于被引用对象的生命期(引用生命期开始晚于被引用对象生命期开始,结束则更早)。
- 引用不同于指针之处在于,引用关系是不能变更的;而指针的指向关系是可以变更的。实际上,一个引用大致相当于定义了一个const指针(int* const b = &a;)。
- 引用不能指向一个空对象(null)。
这里的引用和Python相同的点是#1,#3,#4;不同之处是#2,#5,#6。
引用出了可以定义变量之外,还可以作为形参。当引用作为形参时,传入的对象不会被拷贝,二是直接拷贝了地址。如:
int createFile(const std::string& filePath) { // 以filePath作为文件名创建文件
return 0; }
这里的filePath是一个std::string类型的对象,这个对象作为行参传入时,不会被复制,从而提高了效率。实际上,往往比这一点更重要的是,有些对象是无法复制的,比如锁、单例对象等。
当使用引用时,一个const修饰符往往是必要的。只要对象不需要在后续的代码中修改(mutate),那么就可以给该对象的引用加上const修饰符。关于const修饰符使用的场景,后面会介绍。
了解了一个特性之后,就有一个很重要的问题:什么场景下使用这个特性?总结下来看,对引用的使用,可以归结为以下几点:
1. 对复杂表达式创建别名,提高可读性,降低思维成本。
std::vector<std::string> fields; // 创建fields,填充值,这往往来自于一个parse操作,或者是一个split操作 const std::string& username = fields[0]; // 这里定义了一个别名,相比记忆fields[0]这种带脚标的表达式,实在是容易多了。 const std::string& age = fields[1]; // 还可以定义更多的别名 // 使用username和age,显著降低了思维成本,提高了代码的可读性
2. 在形参中引用复杂的对象,避免对象拷贝。
3. 返回一个对象的引用,避免拷贝。根据结论#4,返回的对象的生命期需要包含调用者取得引用的生命期。因此返回一个局部变量是不允许的。但可以返回一个成员变量、static对象、全局对象。
4. 作为形参传入,用于保存函数对该形参的修改。这通常适合需要多返回值的情况,或者返回值是复杂对象,切不满足上述第3条,不是成员变量、static对象、全局对象。
然而,好的特性并不是可以被滥用的,如果定义一个下述的形参:
int nextNumber(const int& n) { return n + 1; }
则是完全不必要的,因为int型是一个基本类型,不是复杂类型,这样做就是“画蛇添足”。
为了让读者能专注在C++的特性上,这里的例子都是尽可能简单的。