条款1:仔细区别 pointers 和 references
- 引用应该被初始化,指针可以不被初始化。
- 不存在指向空值的引用这个事实意味着使用引用的代码效率比使用指针的要高。因为在使用引用之前不需要测试它的合法性。
- 指针与引用的另一个重要的不同是指针可以被重新赋值以指向另一个不同的对象。但是
引用则总是指向在初始化时被指定的对象,以后不能改变。
std::string s1("Nancy");
std::string s2("Clancy");
std::string& rs = s1; // rs引用s1
std::string* ps = &s1; // ps指向s1
rs = s2; // rs仍旧引用s1,但是s1的值现在是"Clancy"
条款2:最好使用C++转型操作符
这四个操作符是:static_cast、const_cast、dynamic_cast、reinterpret_cast。
- const_cast 最普通的用途就是转换掉对象的 const 属性
- dynamic_cast,它被用于安全地沿着类的继承关系向下进行类型转换。这就是说,你能用 dynamic_cast 把指向基类的指针或引用转换成指向其派生类或其兄弟类的指针或引用,而且你能知道转换是否成功。失败的转换将返回空指针(当对指针进行类型转换时)或者抛出异常(当对引用进行类型转换时)。它不能被用于缺乏虚函数的类型上。
- 如你想在没有继承关系的类型中进行转换,你可能想到 static_cast。
- reinterpret_cast,使用这个操作符的类型转换,其 的 转 换 结 果 几 乎 都 是 执 行 期 定 义 ( implementation-defined )。 因此,使用reinterpret_casts 的代码很难移植。reinterpret_casts 的最普通的用途就是在函数指针类型之间进行转换。
double result = static_cast<double>(firstNumber)/secondNumber;
条款3:绝对不要以多态(polymorphically)方式处理数组
在对数组进行传参使用多态时,程序会crash; 因为数组在移位至下一数据时,步长是形参(基类)的size,而不是指针实际指向数据类型(派生类)的size,所以会数组会移位至一个非法的地址 。
#include <iostream>
using namespace std;
class Base
{
public:
virtual void test()
{
cout<<"Base::test()"<<endl;
}
int a;
};
class Derived: public Base
{
public:
void test()
{
cout<<"Derived::test()"<<endl;
}
int b, c;
};
void testArray(Base bArray[], int n)
{
for(int i =0; i<n; i++)
bArray[i].test(); //i = 1时,程序crash; 编译器原先已经假设数组中元素
//与Base对象的大小一致,但是现在数组中每一个对象大小却与Derived一致,
//派生类的长度比基类要长,数组将移动到一个非法位置。
}
int main()
{
Base *p = new Derived[2];
testArray(p, 2);
}
条款4:非必要不提供 default construcor
提供无意义的缺省构造函数也会影响类的工作效率。如果成员函数必须测试所有的部分是否都被正确地初始化,那么这些函数的调用者就得为此付出更多的时间。而且还得付出更多的代码,因为这使得可执行文件或库变得更大。它们也得在测试失败的地方放置代码来处理错误。如果一个类的构造函数能够确保所有的部分被正确初始化,所有这些弊病都能够避免。缺省构造函数一般不会提供这种保证,所以在它们可能使类变得没有意义时,尽量去避免使用它们。
class EquipmentPiece {
public:
EquipmentPiece(int IDNumber) {}
virtual ~EquipmentPiece() {}
int a = 1;
float b = 2.0;
};
//避免无用的缺省构造函数
int ID1 = 1, ID2 = 2;
EquipmentPiece bestPieces3[] = { EquipmentPiece(ID1), EquipmentPiece(ID2) };
// 正确,提供了构造函数的参数
// 利用指针数组来代替一个对象数组
typedef EquipmentPiece* PEP; // PEP指针指向一个EquipmentPiece对象
PEP* bestPieces5 = new PEP[10]; // 也正确
// 在指针数组里的每一个指针被重新赋值,以指向一个不同的EquipmentPiece对象
for (int i = 0; i < 10; ++i)
bestPieces5[i] = new EquipmentPiece(ID1);
for (int i = 0; i < 10; ++i)
delete bestPieces5[i];
delete bestPieces5;
利用指针数组代替一个对象数组这种方法有两个缺点:第一你必须删除数组里每个指针所指向的对象。如果忘了,就会发生内存泄漏。第二增加了内存分配量,因为正如你需要空间来容纳EquipmentPiece对象一样,你也需要空间来容纳指针.
解决办法:
//分配足够的 raw memory,给一个预备容纳 10 个EquipmentPiece objects 的
//数组使用
void* rawMemory = operator new[](10 * sizeof(EquipmentPiece));
//让 basePiece 指向此块内存,使这块内存被视为一个 EquipmentPiece 数组
EquipmentPiece* bestPieces6 = static_cast<EquipmentPiece*>(rawMemory);
//利用 “placement new”构造这块内存中的 EquipmentPiece objects
for (int i = 0; i < 10; ++i)
new(&bestPieces6[i]) EquipmentPiece(i);
//将 basePieces 中的各个对象,以其构造顺序的相反顺序析构掉
for (int i = 9; i >= 0; --i)
bestPieces6[i].~EquipmentPiece();
// 如果使用普通的数组删除方法,程序的运行将是不可预测的
//因为 basePieces 并非来自 new operator
//释放 raw memory
operator delete[](rawMemory);
条款5:对定制的“类型转换函数”保持警觉
单自变量 constructors 是指能够以单一自变量成功调用的 constructors。如此的 constructor 可能声明拥有单一参数,也可能声明拥有多个参数,并且除了第一参数之外都有默认值。
class Name{
public:
Name(const string& s); //可以把string转换成Name
...
};
class Rational{
public:
Rational(int numerator = 0,int denominator = 1);
//可以把 int 转换成 Rational
...
};
有两种函数允许编译器进行这些的转换:单参数构造函数(single-argument constructors)和隐式类型转换运算符。
隐式类型转换运算符只是一个样子奇怪的成员函数:operator关键字,其后跟一个类型符号。你不用定义函数的返回类型,因为返回类型就是这个函数的名字。
class Rational {
public:
Rational(int numerator = 0, int denominator = 1) // 转换int到有理数类
{
n = numerator;
d = denominator;
}
operator double() const // 转换Rational类成double类型
{
return static_cast<double>(n) / d;
}
double asDouble() const
{
return static_cast<double>(n) / d;
}
private:
int n, d;
};
//谨慎定义类型转换函数
Rational r(1, 2); // r的值是1/2
double d = 0.5 * r; // 转换r到double,然后做乘法
fprintf(stdout, "value: %f\n", d);
std::cout << r << std::endl; // 应该打印出"1/2",但事与愿违,是一个浮点数,
而不是一个有理数,隐式类型转换的缺点
一般来说,越有经验的 C++程序员就越喜欢避开类型转换运算符。例子,在打印Rational类实例时,你忘了为 Rational 对象定义 operator<<。你可能想打印操作将失败,因为没有合适的的 operator<<被调用。但是你错了。当编译器调用 operator<<时,会发现没有这样的函数存在,但是它会试图找到一个合适的隐式类型转换顺序以使得函数调用正常运行。类型转换顺序的规则定义是复杂的,但是在现在这种情况下,编译器会发现它们能调用Rational::operator double 函数来把 r 转换为 double 类型。所以上述代码打印的结果是一个浮点数,而不是一个有理数。这样的函数有时候会引起预料之外的调用。可以用显示的转换函数替代
构造函数用 explicit 声明,如果这样做,编译器会拒绝为了隐式类型转换而调用构造函数。显式类型转换依然合法。
条款6:区别 increament/decrement 操作符的前置(prefix)和后置(postfix)形式
参考文章:《More Effective C++》笔记_more effective c++ pdf github-CSDN博客
More Effective C++-CSDN博客