(C++) More Effective C++ 笔记:基础议题(Basics)

Posted by

on

从 C 学习到 C++,一开始的初级阶段我仅仅是利用了 C++ 提供的便利特性。很多思想还停留在C初级阶段。所以,要升入学习C++的思想,在好友的推荐下,入手 More Effective C++ (Scott Meyers 著),根据已有经历来逐步体会到C++体系中的深奥思想。

今天总结一下第一章:基础议题(Basics)。

条款一:区分指针(pointers)与引用(references)

C++ 引入了引用(references)的概念。它的作用好似C语言既有的指针(pointers),但区别仍然较大。

指针其实仍然是一个指向具体内存地址的变量。其大小是 size_t (即计算机字长长度)。引用是对既有变量的别名(alias)。

因此:可以有空指针(指向 nullptr),但不可以有空引用。引用从诞生起就必须有它所代表的东西。两者都可以对其指向/代表的数据进行访问和修改。

区分:

std::string s1("iEdon");
std::string s2("Moooc");

std::string& rs = s1;    // rs 代表 s1。从此 rs 就是 s1。rs 是 s1 的别名。
std::string *ps = &s1;   // ps 是指向 s1 所在区域的地址的指针。

rs = s2;    // s1 变成了 "Moooc"
ps = &s2;   // ps 从此指向 s2。

使用方法:

如果我确定以及肯定,(1)我所指向(参考的)的东西从一开始就已经决定,并且永远也不会去改变它。(2)无法由指针实现的功能。此时,我可以放心使用引用(references)。其他情况下,使用指针。

条款二:养成C++式类型转换的好习惯

在C语言中,我们常常使用一组括号,并在括号内填入需要强制转换的类型来进行转型操作。现在我们还是可以这么做,但是这样就像手动管理内存一样,很容易出错。C++给我们提供了新的转型操作关键字:static_cast, dynamic_cast, const_cast, reinterpret_cast。

static_cast<typename>

和旧式C转换作用几乎一样。静态地(statically)将一个类型强制识别为另一个。

dynamic_cast<typename>

允许安全地将基类(base classes)对象的引用或指针转换为具体类(派生类, derived classes)的引用或指针。这种转型是旧式C无法提供的。因为派生类对象往往会比其基类包含更多的私有对象和成员函数等,所以直接粗暴的转换会带来不可预知的行为。

const_cast<typename>

允许对类型做 const 和 非 const 的转换:

std::string loveletter("I love moooc!");
char *str = const_cast<char *>(loveletter.c_str()); // 去除 loveletter.c_str() 返回的 const char * 类型的 const 修饰,等价于:char *str = (char *)loveletter.c_str();
*(str + 7) = 'i';
*(str + 8) = 'E';
*(str + 9) = 'd';
*(str + 10) = 'o';
*(str + 11) = 'n';

reinterpret_cast<typename>

reinterpret_cast 常常用于转型函数指针。这种转型常常用于回调函数(callbacks),DLL/Shared Object 传递过程使用。

条款三:禁止以多态(polymorphically)方式处理数组

即:基类数组与具体类(派生类)数组在传递过程中严禁混用。

假设现在有一个 BST(Binary Search Tree, 二叉树)类以及由它派生的具体类:BalancedBST(平衡二叉树)类。有这样一个函数定义:printBSTArray(ostream& s, const BST array[], int numElements);

如果我们将具体类 BalancedBST 传入此过程,虽然看起来 BalancedBST 是由 BST 派生的,但这是万万不可以的。因为其内部使用了 “指针算术表达式”,即:

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

其中 array[i] 便是“指针算术表达式”。在条款二中我们刚刚提过,“派生类对象往往会比其基类包含更多的私有对象和成员函数等”。编译器在访问数组下标计算内存跨度时,只会带上 BST 类的大小进行计算,而不是 BalancedBST 的。所以会造成不可预知的问题,并很容易崩溃。

条款四:非必要情况下不提供默认构造器(default constructor)

如果类的构造器(class constructors)可以确保类的所有成员正确初始化,就可以避免每个成员函数中对成员变量的检查的成本。如果默认构造器不能提供保证,就不要提供默认构造器了。我们的目标是,让类在产生对象时就完全地被初始化,因为这样更加有效率。

这里我看的不是很透彻,援引其他大佬的总结:

  • 有 default constructor 时,可以避免3个问题,一是:类数组的初始化不支持带参数的构造函数,二是:一些 C++ 模板库,要求被实例化的目标类型必须要有 default constructor,三是类的虚继承体系中,如果基类没有default constructor,那么每一层的子类都必须了解基类的构造函数
  • 反过来看,使用 default constructor 时,可能会增加了类的复杂度,因为不能保证每个字段都有意义(default constructor 导致赋予字段一个缺省值,这个缺省值可能是多余的)。并且,使用这些字段的调用者,都需要做一个“测试”,测试字段是否真的被初始化了。

2 responses to “(C++) More Effective C++ 笔记:基础议题(Basics)”

  1. linhuichao Avatar
    linhuichao

    🙄 加油

Leave a Reply

Your email address will not be published. Required fields are marked *