More Effective C++ 笔记
时间:2010-12-27 来源:苹果君
没有空引用,但是有空指针。
用指针的情况:
有可能什么也不指向。
不同时候指向不同对象。
使用引用的情况:
实现某一些操作符的时候,操作符由于语义要求使得指针不可行,这时就使用引用。
指向固定不变的时候用引用。
其他情况用指针。
M2 C++风格的类型转换
static_cast
等同于C风格的类型转换,有诸如结构体不能转为整型,double不能转为指针等限制。
const_cast
用于去除变量的const或者volatile属性。
dynamic_cast
用来针对一个继承体系做向下或者横向的安全转换(即只能用来操纵继承体系,不能横向)。
不能用于没有虚函数的类型(如果没有虚函数会提示类型不是多态类型)。
目标类型必须为指针或者引用。
还有一点很关键,如果是向上类型转换那么用static_cast就够了,如果是向下类型转换,必须保证源类型跟目标类型本来就是一致的,否则转换失败。比如一个父类指针指向一个子类,我们可以将父类指针转换为子类指针,父类指针如果指向父类对象或者其他类型的子类对象都会使转换不成功。他与static_cast的区别是,转换失败不会抛出异常,而是返回NULL指针,即所谓的安全向下转型(safedowncasting)。
这个函数使用的是RTTI机制,所以编译器必须打开这个选项才能编译。
reinterpret_cast
具体有编译器决定,通常拿来在函数指针之间进行类型转换,诸如将int*转换为int等。
很容易转换失败,不建议使用。
M3 决不要把多态应用于数组
其实当对象的大小是一样的时候,是不会有问题的。
主要是因为数组应用到了指针运算,而多态跟指针运算是不能用在一起的。
但是如果我们设计软件的时候,不要让具体类继承具体类的话,就不太可能犯这种错误。(理由是,一个类的父类一般都会是一个抽象类,抽象类不存在数组)
M4 避免不必要的默认构造函数
没有默认构造函数带来的限制:
不能创建该类的对象数组。
对于不在堆上分配的数组可以这样: A a[] ={A(2), A(3), …};
也可以用指针数组。
使用placementnew。
无法作为许多基于模板的容器类的类型参数使用。
像Vector容器就可以,因为它的底层是采用placement new实现的。
虚基类构造函数的参数必须由被创建的对象所属的最远的派生类提供。
有默认构造函数带来的限制:
类的其他函数必须确保对象已被有意义地初始化了。如果发现没有被有意义地初始化,通常也不能够确定应该怎么去处理这个错误(抛异常或者终止程序都是权宜之计)。
类的其他函数对对象是否有意义的测试也对效率有所影响。
M5 小心用户自定义的转换函数
隐式的类型转换运算符会带来意想不到的效果。
一个重载隐式类型转换运算符的例子:
class A
{
public:
operator double() const{return x;}
double x;
};
cout<<A(5)<<endl;
出现意想不到的类型转换,输出5。
可行的方法是,定义类似功能的函数,而抛弃隐式类型转换,使得类型转换必须显示调用。
例如 String类没有定义对Char*的隐式转换,而是用c_str函数来实施这个转换。
拥有单参数构造函数的类,很容易被隐式类型转换。
可行的方法是采用explicit修饰构造函数以防止构造函数被用来做隐式类型转换。
另一种可行的方法是建立一个代理类,其目的只有代替单参数构造函数的参数。注意该代理类为了表示跟被代理类的密切关系,会声明在被代理类里面,然后声明为public,使外部也能够用它。
M6 区分自增运算符和自减运算符的前缀形式和后缀形式
后缀形式以int为参数,后缀通过前缀来实现。基本数据类型的后缀自增不能为左值(例如i++= 3;是错的),所以后缀自增返回const的副本。
标准形式:
前缀:
const A& operator++()
{
x++;
return *this;
}
后缀:
const A operator ++ (int)
{
const A a = *this;
++*this;
return a;
}
M7 不要重载“&&”、“||”、“,”
主要是我们不能控制表达式的求解优先级,所以不能真正模仿这些运算符。
M8 理解new和delete在不同情形下的含义
不要重载new运算符(new operator),而是重载operator new函数。原因是程序员不可以显示调用构造函数,从而模仿new的行为,所以对象不会正确地被创建。
void* operatornew(size_t size);
这个函数的职责是分配内存,分配内存可以通过调用真正的operator new完成。
通过以下代码,将会调用placementnew,将对象构造到指定内存区域。
byte* buffer = new byte[sizeof(A)];
A* a = new(buffer)A;
此时,如果类中有重载operatornew函数的话,则必须是placementnew:
void* operator new(size_t size, void*x)
{
cout<<"creatingmemory"<<endl;
return x;
}
placement new其实是operator new,只不过参数不一样,而且,必须返回传入的void指针。简而言之placementnew就是不用分配内存的operatornew,因为内存已经由调用方提供。
operator delete实现的功能与operator new刚好相反。释放内存。
所以这两个函数其实跟malloc与free是差不多的。注意析构函数可以显示调用。
void * buffer = operator new(sizeof(A)); //相当于调用malloc
A* a = new(buffer)A; //相当于显示调用构造函数
a->~A(); //显示调用析构函数
operator delete(buffer); //相当于free
以上代码可以编译并运行而不会出错。Cool。
M19 了解临时对象的来源
出现临时对象主要有两种情况。
为了使函数调用成功而进行隐式类型转换,以及函数返回对象而进行的隐式类型转换。
C++禁止为非常量引用(reference-to-non-const)产生临时对象,所以涉及到这一步的隐式类型转换都是不可行的。
M20 协助编译器实现返回值优化
C++规则允许编译器针对超出生存周期的临时对象进行优化。
比如,函数调用方在调用函数的时候同时将函数返回值赋予一个变量,那么当函数调用完成产生临时对象返回时,它可以将临时对象构造在被赋值的变量上,是产生临时对象带来的开销变成零。
M25 使构造函数和非成员函数具有虚函数行为
如果虚函数的返回值是一个指向某个基类的指针,那么派生类重定义虚函数时可以返回一个指向上述基类的派生类的指针。
构造函数跟拷贝构造函数都不能为虚函数。他们只能用inline修饰。
使拷贝构造函数拥有虚函数行为的方法是采用Template Method模式。
构造函数不能采用TemplateMethod模式。
M26 限制类对象的个数
类的唯一一个实例被放在了友元函数内部作为静态局部变量。
这样做的话,类的实例会在函数被第一次调用的时候构造,如果放在类里面,则会在程序开始运行的时候就构造了。
该友元函数不可以被声明为内联,原因是非成员内联函数在链接的时候在目标文件中会产生多个副本,其中的静态局部变量也会有多个副本。
通过将构造函数设为私有以禁止类被继承。
子类通过使用using可以改变从父类继承而来的函数的访问控制。
class D
{
protected:
void print(){}
};
class E : public D
{
public:
using D::print;
};