《More Effective C++》读书笔记第一章:基础议题

第一章:基础议题

条款一.仔细区分pointers和references

首先,没有NULL reference(空引用),但是可以有NULL pointer(空指针)。考虑这样的情况:

char *pc = 0;
char& rc = *pc;

这难道就是空引用了吗?错,这是未定义的行为,编译器可能产生任何输出,要杜绝这种行为,所以千万不要考虑让reference成为NULL。
Reference不能成为NULL,必然就有初值,必须初始化;但是指针可以为NULL,所以可以不用初始化(未初始化的指针虽然有效,但是风险高)。所以在使用引用时,不用测试其有效性,但是使用指针,就必须测试其是否为NULL:

void print(const double* pd) {
    if(pd) cout << *pd; //必须检查NULL
}

指针的指向是可以改变的,而引用即别名,引用是无法改变指向的,引用其实很像一个常量指针

string s1("Nancy"), s2("Clancy");
string& rs = s1;
string* ps = &s1;
rs = s2;    //rs还是代表s1,这其实是改变s1为“Clancy”
ps = &s2;   //将ps指向s2

还有一个引用应用的地方,operator[]通常返回的是引用

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

在c语言中的隐式转型通常是下面这种表达式:

(type) expression;

在C++中,应该这样写:

xxx_cast<type>(expression);

C++中的四个转型操作符包括:

static_cast, const_cast, dynamic_cast, reinterpret_cast;
  1. static_cast和c旧式转型有着相同的威力、意义以及限制,static_cast是强迫隐式转换
    无法将一个struct转化为一个int,或者将一个double转化为pointer,这些本来就是c旧式转型也不可能完成的任务。
  2. const_cast:通常用来去除表达式的常量性(constness)或者变易性(volatileness)。
    const和volatile都是类型限定符,volatile设计用来修饰被不同线程访问和修改的变量。volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值(直接从内存中读取)。用法与const相同,并且可以与const连用。
  3. dynamic_cast:用来执行自上而下的安全的转型,也就是说可以利用dynamic_cast将指向基类的指针或者引用转型为指向派生类的指针或者引用。dynamic_cast无法用在缺乏虚函数的类身上,也不能改变其常量型。
  4. reinterpret_cast,最常见的用途就是函数指针类型的转换,例如将一个函数指针放到另一个函数指针数组里面:
typedef void (*FuncPtr)();
FuncPtr funcPtrArray[10];
int doSomething();
funcPtrArray[0] = &doSomething; //编译报错
funcPtrArray[0] = reinterpret_cast<FuncPtr>(&doSomething);  //正确

最后,使用C++转型操作符的好处在于:辨识度高,且编译器能够诊断出错误

条款三:绝对不要以多态的方式来处理数组

假设基类base 派生类derived,考虑一个函数的形参是const base array[]即函数形参是基类数组,如果实参传递的是派生类的数组,比如

derived array[10];

将array2传递给该函数,函数有访问array[i]这样的操作是很正常的,在这一步就是出问题的地方:
在函数体中,array[i]实际代表的是*(array + i)array是一个地址,array + i也是一个地址,且两者相距i * sizeof(base);但是实际传递的派生类sizeof(derived)通常都要比sizeof(base)大,因此这会造成不可预期的错误。
此外,可能最后会调用删除数组的代码,例如:

delete [] array;

实际编译器产生的代码可能是这样的:

for(int i = array.size(); i >= 0; --i) {
    array[i].base::~base();    //依次调用析构函数
}

这个时候行为未定义的原因依然是在于array[i]的地址是错误的。
所以数组和多态不要混用

条款四:非必要不提供默认构造函数

默认构造函数是一种”无中生有”的行为,没有任何外来的信息就能将对象初始化。对于那些“必须要有外来信息才能生成对象”的类,则不必拥有默认构造函数。
对于有其它构造函数,却没有默认构造函数的类,产生数组是错误的:

class EquipmentPiece {
public:
    EquipmentPiece(int IDNumber);
};
EquipmentPiece Piece[10];   //错误,无法调用ctors

解决办法是:使用指针数组

typedef EquipmentPiece* PEP;
PEP Piece[10];  //可行
PEP *Piece =  new PEP[10];  //这样也可行

这两种做法的缺点在于:
1. 必须将数组的所有对象都删除,资源都及时释放,以免出现资源泄露问题。
2. 需要多余的空间来放置指针。

另外的解决办法就是使用“raw memory”,然后使用“placement new”:

void *rawMemory = operator new[](10 * sizeof(EquipmentPiece));  //只分配内存而不构造对象
EquipmentPiece* Piece = static_cast<EquipmentPiece*> rawMemory;
for(int i = 0; i < 10; ++i) {
    new (&Piece[i]) EquipmentPiece(IDNumber);
}

最后删除rawMemory,不能直接这样:

delete [] rawMemory;

正确的做法是:先调用析构函数,在调用operator delete;

for(int i = 0; i < 10; ++i) {
    Piece[i].~EquipmentPiece();
}
operator delete[](rawMemory);

关于new和delete的一些底层的东西,见执行期语意学
缺乏默认构造函数的类的另一个缺点在于,不能适用于template-based container constructors。对于template而言,被实例化的目标类型必须有一个默认构造函数。
到底要不要默认构造函数呢?就像最早所说的:只有对于那些“必须要有外来信息才能生成对象”的类,才不必拥有默认构造函数。 添加无意义的默认构造函数,也会影响classes的效率。

主要参考自侯捷老师翻译的《More Effective C++》中文版

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值