More Effective C++ 读书笔记(1)

条款1:指针与引用的区别

1.任何情况下都不能引用指向空值,相反指针可以,引用应该被初始化

当然,引用不能为空也意味着使用引用的效率高于指针,因为在使用前无需判空

void func(const int &x)
{
    cout << x;
}
void func(const int *x)
{
    if(x){  //需要判空
        cout <<x;
    }
}

2.指针可以被重新赋值以指向另一个不同的对象,但引用总是指向初始化时指定的对象,以后不能改变

条款2:尽量使用C++风格的类型转换

static_cast

const_cast 

转换掉对象的const属性

dynamic_cast

被用于安全地沿着类的继承关系向下进行类型转换 

dynamic_cast 把指向基类的指针或引用转换成指向其派生类或其兄弟类的指针或引用,而且你能知道转换是否成功。失败的转换将返回空指针(当对指针进行类型转换时)或者抛出异常(当对引用进行类型转换时)

class B;
class D:public B
{};
B *b;
//b经过转换后指向派生类D
func(dynamic_cast<B*>(b));

reinterpret_cast

 转换结果几乎都是执行期定义( implementation-defined ),难移植

常见用途是函数指针之间的转换

typedef void (*Func)();//Func是函数指针,无参数,返回类型void
Func f[10]; //f是一个数组,容纳10个Func函数指针

//现在希望把doSomething函数的指针存入f数组中
int doSomething();
//由于返回类型不一样,因此需要转换
f[0] = &doSomething; //ERROR!类型不匹配
f[0] = reinterpret_cast<Func>(&doSomething);

条款3:不要对数组使用多态

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 BSTArray[10];
printBSTArray(cout, BSTArray, 10); // 运行正常
BalancedBST bBSTArray[10];
printBSTArray(cout, bBSTArray, 10); //ERROR

这里的 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 函数将会发生什么样的后果。不论是什么后果都是令人不愉快的。

条款4:避免无用的缺省构造函数

如果class中没有default构造函数,那么在下面几种情况下可能会出现问题

1.建立类对象数组时

class B
{
public:
    B(int x);
};
B b1[10]; //ERROR
B *b = new B[10]; //ERROR

PS:解决方法:(1)使用非堆数组时,定义时可以一一赋值

(2)利用指针数组来代替

typedef B* Pb;
Pb b1[10]; //OK
Pb *b2 = new Pb[10]; //OK

PS:此方法缺点:必须删除数组里每个指针所指向的对象//增加内存分配量

2.无法在许多基于模版的容器里使用

因为实例化一个模板时,模板的类型参数应该提供一个缺省构造函数,这是一个常见的要求。

条款5:谨慎定义类型转换函数

条款6:自增、自减操作符前缀形式与后缀形式的区别

class A
{
public:
    //前缀
    A &operator ++(); 
    A &operator --();
    //后缀
    const A operator ++(int);
    const A operator --(int);
};

注意:后缀返回的是const对象。因为返回的是增加前的值

const A A::operator ++(int)
{
    A oldValue = *this;
    ++(*this);
    return oldValue;
}
 
 

条款7:不要重载“&&”,“||”或“,”

C++通常运用布尔表达式短路求值法来确定真假,如果重载“&&“或“||”,那就是用函数调用法来替代短路求值,两者之间会有所区别:首先当函数被调用时,需要运算其所有参数,所以调用函数 functions operator&& 和 operator||时,两个参数都需要计算,换言之,没有采用短路计算法。第二是 C++语言规范没有定义函数参数的计算顺序,所以没有办法知道表达式 1 与表达式 2 哪一个先计算。完全可能与具有从左参数到右参数计算顺序的短路计算法相反。

条款8:理解不同含义的new和delete

你使用的 new 是 new 操作符。这个操作符就象 sizeof 一样是语言内置的,你不能改变它的含义,它的功能总是一样的。它要完成的功能分成两部分。第一部分是分配足够的内存以便容纳所需类型的对象。第二部分是它调用构造函数初始化内存中的对象。new 操作符总是做这两件事情,你不能以任何方式改变它的行为。

你所能改变的是如何为对象分配内存 。new 操作符为分配内存所调用函数的名字是 operator new。

函数 operator new 通常这样声明:
void * operator new(size_t size);

返回值类型是 void*,因为这个函数返回一个未经处理(raw)的指针,未初始化的内存。

参数 size_t 确定分配多少内存。你能增加额外的参数重载函数 operator new,但是第一个参数类型必须是 size_t。

placement new

在一个已存在的对象上调用构造函数是没有意义的,因为构造函数用来初始化对象,而一个对象仅仅能在给它初值时被初始化一次。但是有时你有一些已经被分配但是尚未处理的(raw)内存,你需要在这些内存中构造一个对象。你可以使用一个特殊的 operator new ,它被称为 placement new。

class Widget
{
public:
    Widget(int widgetSize);
};
Widget * constructWidgetInBuffer(void *buffer,
                                 int widgetSize)
{
    return new (buffer) Widget(widgetSize);
}

在 constructWidgetInBuffer 里面,返回的表达式是:new (buffer) Widget(widgetSize)这初看上去有些陌生,但是它是 new 操作符的一个用法,需要使用一个额外的变量(buffer),当 new 操作符隐含调用 operator new 函数时, 把这个变量传递给它。 被调用的 operator new函数除了待有强制的参数 size_t 外,还必须接受 void*指针参数,指向构造对象占用的内存空间。这个 operator new 就是 placement new,它看上去象这样:

void * operator new(size_t, void *location)
{
    return location;
}

operator new和new operator的区别

你想在堆上建立一个对象,应该用 new 操作符。它既分配内存又为对象调用构造函数。如果你仅仅想分配内存,就应该调用 operator new 函数;它不会调用构造函数。如果你想定制自己的在堆对象被建立时的内存分配过程,你应该写你自己的 operator new 函数,然后使用 new 操作符,new 操作符会调用你定制的 operator new。如果你想在一块已经获得指针的内存里建立一个对象,应该用 placement new。

Deletion and Memory Deallocation

如果你只想处理未被初始化的内存(raw),你应该绕过 new 和 delete操作符,而调用 operator new 获得内存和 operator delete 释放内存给系统。
// 分配足够的内存以容纳 50  char没有调用构造函数
void *buffer = operator new(50*sizeof(char)); 
operator delete(buffer); // 释放内存 没有调用析构函数

条款9:使用析构函数防止资源泄漏

void F()
{
    B *b = new B(x);
    b->func();
    delete b;
}

若func()函数出现异常,那么delete b语句将不会执行,则内存泄漏,虽然try..catch可以解决,但是语句容易重复不简洁,更好的方法是将delete类似的操作放入析构函数或使用智能指针,这样就无需担心异常的出现了。

条款10: 在构造函数中防止资源泄漏  
C++仅仅能删除被 完全构造的对象(fully contructed objects), 只有一个对象的构造函数完全运行完毕,这个对象才被完全地构造。
C++拒绝为没有完成构造操作的对象调用析构函数是有一些原因的,而不是故意为你制造困难。原因是:在很多情况下这么做是没有意义的,甚至是有害的。如果为没有完成构造操作的对象调用析构函数,析构函数如何去做呢?仅有的办法是在每个对象里加入一些字节来指示构造函数执行了多少步?然后让析构函数检测这些字节并判断该执行哪些操作。这样的记录会减慢析构函数的运行速度,并使得对象的尺寸变大。C++避免了这种开销,但是代价是不能自动地删除被部分构造的对象。

条款11:禁止异常信息(exceptions)传递到析构函数外

在有两种情况下会调用析构函数:
1.在正常情况下删除一个对象,例如对象超出了作用域或被显式地 delete。
2.异常传递的堆栈辗转开解(stack-unwinding)过程中,由异常处理系统删除一个对象。
禁止异常传出析构函数的原因
(1)如果在一个异常被激活的同时,析构函数也抛出异常,并导致程序控制权转移到析构函数外,C++将调用 terminate 函数。这个函数的作用正如其名字所表示的:它终止你程序的运行,而且是立即终止,甚至连局部对象都没有被释放。
(2)如果一个异常被析构函数抛出而没有在函数内部捕获住,那么析构函数就不会完全运行(它会停在抛出异常的那个地方上)。如果析构函数不完全运行,它就无法完成希望它做的所有事情。





猜你喜欢

转载自blog.csdn.net/sinat_25394043/article/details/80134967