More Effective C++ 读书笔记

More Effective C++ 读书笔记

01 区分pointer和reference

使用reference的好处是:

  • 没有null reference,这意味着不需要进行空校验。
  • 可以避免歧义:比如operatr[]返回指针需要进行就地修改时,必须解引用,这个解引用就会造成歧义。
    1
    2
    vector<int> v(10);
    *v[5] = 10; // 这看起来v好像是一个指针形成的vecotr

03 不要以多态处理数组

我们知道指针和引用可以表现动态多态,但如果是以值传递方式,将派生类数组赋值给一个基类数组,就会出现对象截断现象。

1
2
3
4
5
6
7
8
9
10
11
class BST{...};
class BalancedBST{...};
void printBSTArray(const BST array[])
{
for(int i=0; i<size(); ++i){
cout<<array[i];
}
}

BalancedBST BBSTArray[10];
printBSTArray(BBSTArray); // what happened?

我们知道array是个指针,array[i]表示array+i,计算的是i*sizeof(对象)。你给编译器声明的数组元素是BST,实际给编译器的是BBST,这两者大小不一致时
将出现未定义行为。
同样地,如果你删除数组时,会出现下列调用

1
2
3
4
5
6
7
8
9
void deleteBSTArray(BST array[])
{
delete [] array;
// equals to this
// 调用其中每个元素的析构函数
for(int i=0; i<size; ++i){
array[i].BST::~BST();
}
}

04 非必要不提供默认构造函数

什么时候需要默认构造函数?

  1. 产生局部对象数组时
  2. 模板类中产生局部对象数组时

05 对定制的“类型转换函数”保持警觉

警惕隐式类型转换,这包括:

  • 单自变量构造函数
  • 类型转换操作符。
    所谓类型转换操作符:关键词operator之后加上类型名称。取代类型转换操作符,替代以普通成员函数asFloat等。
    单自变量构造函数可以通过添加关键字explicit避免隐式类型转换。

还有一个解决办法称之为:嵌套类
利用规则:编译器不能做两次及以上的类型转换。构造中间类,将构造函数的变量替换为中间类。

08 了解不同意义的new和delete

区分 new operator 和 operator new

new operator就是最常见的new操作符号,比如

1
string *ps = new string("Memmory Managenmt");

它分为两步:

  1. 分配内存
  2. 调用类的构造函数
    其中分配内存的函数名称为:operator new。你可以重载该函数,但第一个参数必须为size_t。
    1
    void* operator new(size_t size);

new和delte要成对使用

1
2
3
void* buffer = operator new(50*sizeof(char));
...
operator delete(buffer);

10 在构造函数内阻止资源泄露

C++只会析构已经构造完成的对象。

为什么呢?当构造进行到一半时,对象处于一种中间态,如果此时调用析构函数,将无法正确释放资源。
因此,构造期间抛出异常的对象,必须设计一种方法,使得它们抛出一场时能够自我清理。

效率

80-20 法则

使用lazy evaluation

  • 引用计数
  • 区分读写
  • lazy fetching 缓式取出
  • lazy expression evaluation 表达式缓评估
  • 分期瘫还 over-eager evaluation

19 了解临时对象的来源

什么是临时对象?

1
2
3
4
5
6
7
template<class T>
void swap(T& object1, T& object2)
{
T tmp = object1;
object1 = object2;
object2 = tmp;
}

以上示例代码中,tmp虽然在意义上是“临时对象“,但是在编译器眼中,它只是函数局部对象。
C++所谓临时对象是不可见的,它不会在你的源代码中出现。只要你产生了一个non-heap object而没有为它命名,便产生一个临时对象。
通常有两种情况:

  • 隐式类型转换
  • 函数返回对象时
    C++禁止为non-const reference 对象产生临时对象,原因是non-const修改的是临时变量,修改不会起作用。

20 协助返回值优化

当有些函数硬是要返回对象时,采用 constructor arguments 取代对象,可以让编译器消除临时对象的成本。

1
2
3
4
5
6
7
8
9
10
const Rational& operator*(cosnt Rational& lhs, const Rational& rhs)
{
Rational result(lhs.number() * rhs.number());
return result;
}

const Rational& operator*(cosnt Rational& lhs, const Rational& rhs)
{
return Rational(lhs.number() * rhs.number());
}

26 限制class能产生的对象数量

允许0个或1个对象

【单例模式】阻止class产出对象最简单的办法就是将其构造函数声明为private。

从语法上看,namespcae就像一个class,只不过没有public等访问控制符,里面都是public的。

static

(1)初始化顺序

  • “class”拥有一个static对象,即使从未被使用,他也会被构造以及析构。
  • 函数拥有一个static对象,此对象在函数内第一次调用时才产生。

cpp对于不同编译单元内的static对象的初始化顺序没有任何说明,因此使用该方法时需要特别注意。

(2)static 与 inline

1
2
3
4
5
Printer& thePrinter()
{
static Printer p;
return p;
}

如果将此函数声明为inline,会发生什么?

如果是非成员函数,它可能有内部连接。而带有内部连接的函数,可能会在程序中被复制。所以你的程序可能带有多份local static对象的副本。

限定产生K个对象

  • 构造函数私有,包括拷贝构造
  • 添加static类成员计数、最大类数量常数
  • 创建工厂函数,调用构造函数,构造函数对数量进行验证

自动模版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
template<class BeingCounted>
class Counted
{
public:
class TooManyObjects{}; // 用来抛出异常
static int objectCount() { return numObjects; } // 返回当前的对象个数

protected:
Counted();
Counted(const Counted &rhs);
~Counted() { --numObjects; }

private:
static int numObjects;
static const size_t maxObjects;
void init(); // 避免构造函数的代码重复
};

// 构造函数
template<class BeingCountered>
Counted<BeingCountered>::Counted()
{
init();
}
// 拷贝构造函数
template<class BeingCountered>
Counted<BeingCountered>::Counted(const Counted &)
{
init();
}

template<class BeingCountered>
void Counted<BeingCountered>::init()
{
if (numObjects >= maxObjects)
throw TooManyObjects();
++numObjects;
}

模版使用:

  • private继承Counted,模版参数为自己的类(奇异递归模版模式)
  • 采用using声明符,使得private的Counted函数成为public

有了这个模版,Printer就无需自己计数,就好像有人专门为它服务似的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Printer: private Counted<Printer>{
public:
// 伪构造函数
static Printer * makePrinter();
static Printer * makePrinter(const Printer& rhs);

~Printer();
void submitJob(const PrintJob& job);
void reset();
void performSelfTest();
...

using Counted::objectCount; // 让Counted的private成为Public
using Counted::TooManyObjects;

private:
Printer();
Printer(const Printer& rhs);
};

27 禁止对象产生于heap之中

参考:https://blog.desirer233.fun/2025/05/25/C++/约定对象创建限制/

30 代理类

实现二维数组

1
2
3
4
5
Template<class T>
class Array2D {
public:
T& operator[][](int index1, int index2);
}

由于我们无法重载两个方括号,因此我们采用代理类的设计方式,重载operator[],令其返回1维数组的代理,然后再重载代理类的operator[]。

1
2
3
4
5
6
7
8
9
10
11
Template<calss T>
class Array2D {
public:
class Array1D {
public:
T& operator[](int index);
...
};
Array1D operator[](int index);
};

Array2D 的用户不需要知道Array1D的存在,用户好像是在使用真正的二维数组。

区分operator[]的读写

遗憾的是,编译器无法告诉我们针对operator[]的动作是读还是写,因此我们必须悲观地假设 non-const opeartor[]均为写动作。

1
2
3
4
5
6
7
8
9
10
// COW String类设计
char& String::operator[](int index)
{
// 必须假设这是一个写动作,当有两份共享时,必须拷贝一份出来改变
if (value->refCount > 1){
--value->refCount;
value = new StringValue(value->data);
}
return value->data[index];
}

但是,我们可以利用代理类的设计方式,为字符设计代理,从而解决这个问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class String{
public:
class CharProxy {
public:
CharProxy(String& str, int index);
CharProxy& ooperator=(const CharProxy& rhs);
CharPRoxy& operaotr=(char c);
operator char() const; // 类型转换操作符
private:
String& theString;
int charIndex;
};
const CahrProxy operator[](int index) const;
CharProxy operator[](int index);
...
friend class CharProxy;
private:
RCPtr<StringValue> value;
};

const String::CharProxy String::operator[](int index) const
{
return CharProxy(const_cast<String&>(*this), index);
}

String::CharProxy String::operator[](int index)
{
return CharProxy(*this), index);
}

Const operator[]返回的是const proxy,而 const proxy并不能作为赋值的对象。所有的“写”动作都将移动到CharProxy的赋值操作符中。

1
2
3
4
5
6
7
8
String::CahrProxy& String::CharProxy::operator=(const CharProxy& rhs)
{
if (theString.value->isShared){
theString.value = new StringValue(theString.value->data);
}
theString.value->data[charIndex] = rhs.theString.value->data[charIndex];
return *this;
}

31 让函数根据一个以上的对象类型来决定虚化

参考:https://www.cnblogs.com/reasno/p/4872147.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class SpaceShip; // 前置声明
class SpaceStation;
class Asteroid;
class GameObject {
public:
virtual void collide(GameObject& otherObject) = 0;
virtual void collide(SpaceShip& otherObject) = 0;
virtual void collide(SpaceStation& otherObject) = 0;
virtual void collide(Asteroid& otherobject) = 0;
...
};
class SpaceShip: public GameObject {
public:
virtual void collide(GameObject& otherObject);
virtual void collide(SpaceShip& otherObject);
virtual void collide(SpaceStation& otherObject);
virtual void collide(Asteroid& otherobject);
...
};

void SpaceShip::collide(GameObject& otherObject)
{
otherObject.collide(*this); //this一定为SpaceShip
}

看起来有些像递归调用,其实并非这样,在该函数内部,*this实际上已经对应该函数的动态类型,因此otherObject调用的将不再是collide(GameObject& otherObject),而是collide(SpaceShip& otherObject)

这种方法不需要使用RTTI,但却有和RTTI一样的缺点:一旦有新的class假如,代码就必须修改——含入一个新的虚函数,这涉及到类定义得修改,然而修改类定义会引起包含这些类定义的文件的重新编译,在很多情况下成本相当大.

作者

Desirer

发布于

2025-07-13

更新于

2025-07-20

许可协议