EffectiveC++理解

最近在看Effective C++,个人认为这是一本很好的书,因为这是一本好书,并且偏向于实际实用,看着看着里面改善程序的做法,有些特别具有实际意义,但是还有些做法都并不能理解与实际使用,就害怕像大二读《C++Primer Plus》一样花一个月死磕这本书却因为没有去实际使用体会而逐渐淡忘其中细节,所以决定慢慢的读这本书,一个条款一个条款的去领会,尽量做到领会一个条款就一直遵从这条条款去改善代码,所以这篇博客会一直更新至看完这本书为止,并会加入自己在UE4中的体会与理解,与UE4是如何遵从这些条款的案例的。

条款2:尽量用const,enum,inline替换 #define

原因是使用#deine的时候只是单纯的替换,它并不会被编译器编译,报错的时候也不会报宏的定义错误,会增加查BUG的难度,不便于程序调试。(相当于#define处于编译的规则外,想办法使它处在规则内)

关键:
1.对于单纯常量,最耗以const对象或enums 或者enums代替#defines
2.对于形似函数的宏(macros),最好改用inline函数代替
例如:
1.将#defines 常量替换为const
1
#define ASPECT_RATTO 1.653     ------->    const double AspectRotio = 1.653
2.将#defines 常量替换为inlines函数
1
2
3
4
5
6
7
#define CALL_WITH_MAX(a,b) f((a) > (b) ?(a):(b))
-------->
template<typename T>
inline void callWithMax(const T& a, const T& b) //直接调用函数,遵守作用域和访问规则
{
f(a > b ? a : b);
}

条款3:尽可能的使用const

只要当某值不变时你就应该告诉编译器,因为告诉了它将会获得它在编译上的帮助

关键:

1.关键字const出现在星号左边,表示被指物是常量,在星号右边,表示指针自身是常量
2.将不需要修改的引用形参定义为const

注意:

const写在类型之前之后都是一样的意思
1
2
void f1(const Widget* pw);
void f2(Widget const* pw); //都是指向一个不变的对象
如果想要使迭代器所指的对象不被改变,则将迭代器写为const_iterator
1
vector<int>::const_iterator iter     //  *iter = 10; error   // iter ++; ok

条款04:确定对象被使用前已先被初始化

在对象被使用前将其初始化会减少一定的错误情况,和增加一些效率

关键

1.为内置型对象进行手动初始化,即定义时进行初始化
2.使用初始化列表对成员变量在构造函数中进行初始化
3.因为在不同的编译单元,不知道对象的初始化次序,所以使用本地静态对象替换全局静态对象

分析

1.关于这条条款,我翻了一下UE4的源码,发现UE4几乎不会在定义时进行初始化,后面查阅了一下原因,可能是由于这样做会从一定程度上破坏类的抽象性,而且会增加代码编译的时间,所以可能UE4官方就没有这么做。

2.C++对象的成员变量的初始化动作发生在进入构造函数本体之前,所以在构造函数中变量的定义并不是初始化,而是赋值,成员变量在这之前就已经初始化完成了。

3.如果成员变量是const或references,那他们就一定需要在定义时进行初始化,不能被赋值。

4初始化列表以冒号开头,后跟一系列以逗号分隔的初始化字段;

1
2
3
4
5
6
7
class foo
{
public:
foo(string s, int i):name(s), id(i){} ; // 初始化列表
private:
string name ;int id ;
};

对于内置类型,初始化表和在构造函数类初始化差别不大,但是对于类或者struct类型来说,最好使用初始化列表,因为使用初始化列表就不会调用默认构造函数,是较为高效的。所以没有默认构造函数的类类型就可以使用初始化表进行初始化。

条款05:了解C++默默编写并调用了哪些函数

了解:

当你创建一个类的时候,编译器会自动帮你声明一个复制构造函数,一个赋值运算符,一个析构函数,和一个默认构造函数,这些函数都是public和inline的。

相关:

当你声明了一个构造函数或者其他函数时,编译器就不会再为它创建默认构造函数了。

条款06:若不想使用编译器自动生成的函数,那就该明确拒绝

关键:

当你不想一个类被拷贝或者赋值时,你可以声明一个基类,然后将它的复制构造函数和赋值运算符声明为private,这样继承这个类之后派生类就不会玩被拷贝或者赋值了。
这个基类:
1
2
3
4
5
6
7
8
class Uncopyabel{
protected:
Uncopyable(){}
~uncopyable(){}
private:
Uncopyable(const Uncopyables&); // 不写函数参数也行
Uncopyable& operator=(const Uncopyable&);
}

条款07:具有多态性质的基类应该为析构函数添加virtual

因为当删除多态的基类对象指针时,如果不为虚构函数添加virtual就不能够销毁其子类,只销毁了父类对象,会造成资源泄露

关键:

1.具有多态性质的基类应该声明一个virtual虚构函数,如果类带有任何virtual函数,它就应该拥有一个virtual析构函数
2.如果类不具有多态性质就不应该声明virtual析构函数

相关:

1.决定对象被哪一个虚函数调用的信息被一个vptr(函数地址数组指针)指出,它指向一个由函数指针构成的数组,被称为虚函数表,每一个虚函数都有这个虚函数表,当对象调用某虚函数时,编译器会在虚函数表中为该对象选取应该被调用的虚函数。
2.虚函数的调用顺序:先调用最深层的派生类,最后调用父类和基类。

条款8:别让异常逃离析构函数

当在使用析构函数去做一些处理的时候,不要忘记对异常进行处理

关键:

1.析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞掉它们(不传播)或结束程序。
2.如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。

异常处理实例:

1.有异常就结束程序
1
2
3
4
5
6
7
8
DBConn::~DBConn() 
{
try { db.close(); }
catch (...)
{
std::abort();
}
}
2.吞下异常,并记录
1
2
3
4
5
6
7
8
DBConn::~DBConn() 
{
try { db.close(); }
catch (...)
{
//制作运转记录,记下对close的调用失败
}
}
3.将异常检测作为双保险,对用户没有调用的函数,在析构函数中进行调用
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
class DBConn
{
public:
~DBConn()
{
if (!closed)
{
try
{
db.close(); //如果用户没有调用将会在析构函数中调用
}
catch (...)
{
//制作运转记录,记下对close的调用失败
}
}
}
void close()
{
db.close();
closed = true;
}
private:
DBConnection db;
bool closed;
};

条款09:绝不在构造和析构过程中调用virtual函数

关键

在构造和析构期间不要调用virtual函数,因为这类调用从不下降至派生类

原理

1.构造函数的执行顺序:先执行基类的构造函数后执行派生类构造函数

2.因为在基类构造期间,virtual不是virtual,它只会调用基类的的virtual函数,而不会调用派生类的virtual函数

3.而且在执行基类的构造函数时,派生类构造函数还没有被执行,有些变量还未被初始化,所以不允许C++执行派生类中的virtual函数

条款10:令operator=返回一个reference to *this(可以实现连锁赋值)

条款11:在operator=中处理“自我赋值”

关键

1.确保当对象自我赋值operator=有良好行为。
2.确定任何函数如果操作多个对象时,而其中多个对象时同一对象时,其行为仍然正确

原因

在赋值是会经常遇到“自我赋值”的情况,将对象赋值给自己,这样很容易使得自己持有一个指针指向一个已经被删除的对象,这样是十分不安全的。

1
2
// 当i , j 值相同时                   // px 和 py指向同一地址
a[i] = a[j]; // 自我赋值 *px = *py; // 自我赋值

处理

常见“证同测试”:
1
2
3
4
Widget& Widget::operator(const Widget& rhs)
{
if(this = &rhs) return *this; //如果是自我赋值,就不做任何事
}

除此之外还有“异常处理”,copy and swap技术等不同的处理

条款12:复制对象时勿忘其每一个成分

关键

  1. 复制函数应该确保复制所有local变量
  2. 不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数,并由两个coping函数共同调用。

注意:

1.当你为class添加一个成员变量,你必须同时修改复制函数

2.当你为派生类写复制构造函数时,因为父类的成员有些可能是private成员,所以无法直接访问,所有要调用相应的父类函数获取父类成员

条款13:以对象管理资源

所谓资源就是,一旦用了它,将来必须还给系统

关键

1.为了防止资源泄露,应该使用智能指针去管理资源,因为他们会在自动去调用析构函数,释放资源

说明

1.单纯靠用户去调用delete语句是行不通的,因为这也不一定保证资源被释放。

2.智能指针管理对象时一旦对象被销毁或者对象离开作用域时,其析构函数会被自动调用,于是资源被释放。

3.当使用auto_ptr时,如果进行复制和赋值操作,那么被复制物会被置NULL,因为auro_ptr保证资源的唯一拥有权。

4.智能指针的底层是使用引用计数管理资源,用于追踪有多少对象指向某资源,并在无人指向它时自动删除该资源。不同于垃圾回收,引用计数并不能解决循环引用。

条款14:在资源管理类中小心Copying行为

应该根据不同的资源行为与需求定义不提供的复制行为

关键

1.复制RAii对象必须一并复制它所管理的资源,资源的copying行为决定RAII对象的copying行为。
2.普通常见的RAII class copying行为有:禁止复制,使用引用计数,复制底部资源,转移资源的拥有权

说明

1.禁止复制

很多时候RAII不应该被复制。

2.对底部资源使用引用计数
3.可以对其进行深拷贝,拥有任意数量的副本
4.希望只有一个未加工资源(raw recource),复制时将资源的拥有权转移给目标对象。(auto_ptr就是这样做的)

条款15:在资源管理类中提供对原始资源的访问

使用智能指针管理资源虽然挺好,但是有时候会直接访问原始资源(普通指针)的需求,这是后就会产生一些问题

关键

1.有时候有访问原始资源的需求,所以应该为其提供取得其所管理资源的方法

2.对原始资源的访问可能经由显式转换成隐式转换,一般而言显式转换比较安全,当隐式转换对客户比较方便。

解释

需要进行转换的例子:
1
2
3
4
std::tr1::shared_ptr<Investment> pInv(createInvestment());   

int daysHeld(const Investment* pi);
int days = daysHeld(PInv) //错误的调用

编译不通过,是因为deysheld需要的是Investment* 指针,你传递给它的却是个trl::shared_ptr对象

这时候就需要进行转换,一般有显式转换和隐式转换
显式转换
1
2
3
4
5
6
7
class Font{
{
public:
FontHandle get() const {return f};
private:
FontHandle f;
}
隐式转换
1
2


条款16:成对使用new和delete时要采取相同形式

关键

如果你在new表达式中使用[],必须在相应的delete表达式中也使用[].如果你在new表达式中不使用[],一定不要在相应的delete表达式中使用[]

原因

1
2
delete stringPtr1;     //删除一个对象
delete [] stringPtr2; //删除一个由对象组成的数组

如果不加括号,delete认定指针指向单一对象,加括号之后之后只能认定指针指向一个数组

条款17:以独立语句将newed对象置入智能指针

创建智能指针时应该以单独的语句进行声明,而不是在函数调用是进行声明

关键

以独立语句将newed对象存储于智能指针内,如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄露

问题

当编译器:执行“”new Widget“” –>调用prioruty–>trl::shared_otr构造函数次序时,如果对priority函数调用异常,则newWidget返回的指针会遗失,因为它尚未被置入shared_ptr内。

1
processWidget(std::trl::shared_ptr<Widget>(new Widget), priority());

因为priority()调用的顺序是不定的

解决:分离语句

1
2
std::trl::shared_ptr<Widget>(new Widget);
processWidget(pw, priority());

设计与声明

条款18:让接口容易被正确使用,不易被误用

一个没有考虑全面的接口很容易被用户错误的使用

关键

1.你应该在你的所有接口中努力达成容易被正确使用,不容易被误用的目标
2.促进正确使用:1.保证接口的一致性 2.使得内置的类型一致
3.阻止误用:1.通过建立新的类型,然后去限制用户对类型上的操作 2.束缚对象值的范围 3.消除客户的资源管理责任
4.智能指针shared_ptr支持定制删除器,这可以防止DLL问题(不同动态链接库销毁指针问题),可以被用来自动解除互斥锁。

常见用户错误的使用与预防

问题:输入数据类型的不正确?
解决:1.将要输入的类型用struct或者函数进行封装,然后对其调用 2. 限制输入的类型
问题:用户没有删除指针或者删除已经删除过得指针?
解决:使用智能指针对其资源进行管理
问题:用户忘记使用智能指针?
解决:接口设计时就先发制人,令函数返回一个指针指针,这样用户就必须使用指针指针了
问题:用户想要对智能指针下的原始指针进行delete,企图使用错误的资源析构机制?
解决:在接口设计时,令函数返回一个绑定删除器函数的智能指针,这样用户就只能去销毁智能至指针而不是原始指针了

条款19:设计class犹如设计type

在设计一个类时应该考虑的问题

关键

当你定义了一个新的class,你应该严谨来考虑class的设计,尝试着回答下面几个问题:

(1)新type的对象该如何被创建和销毁?

(2)对象的初始化和赋值该有什么区别?

(3)新type对象如果被“值传递”会发生什么?

(4)什么是新type的合法值?

(5)你的新type需要配合某个继承体系吗?

(6)你的新type需要什么样的转换?你的类对象转换为其他对象或者其他类型对象隐式或显式转换为你的对象。

(7)什么样的操作符和函数对此新type是合理的?

(8)什么样的标准函数应该驳回?

(9)谁该取用新type成员?

(10)什么是新type的“未声明接口”?

(11)你的新type有多么一般化?你是定义一个class还是一个新的class template?。

(12)你真的需要一个新的type吗?

这些问题都是十分细节的,不好回答的,但是这种在类设计前就对类进行全面思考的思维是指的学习的

条款20:宁以pass-by-reference-to-const替换pass-by-value

函数类对象传参时引用传递要比值传递效率高

关键

函数参数传递的时候,如果参数是内置类型使用值传递,如果是自定义类型就使用引用传递

说明

1.对于自定义类型而言,引用传递比值传递效率搞。例如有参数是继承的函数中,值传递会调用6次构造函数和6次析构函数,而对于引用传递,没有任何新对象被创建,没有调用任何析构函数和构造函数!从这才知道值传递对于引用传递的效率之差这么大
2.如果函数中不对参数进行修改,应该定义为const类型
3.引用传递还会解决函数“切割”问题(如果在值传递一个派生类,那么派生类将会变成基类,而没有多态性质)

UE4中的使用

在UE4的源码中也遵守者这条条款,但却做了一些改变,UE4中基础数据对象使用值传递,继承自UObject的类使用指针传递(实质上是值传递),大概是UE4官方对UObject类做了一些优化吧,F开头和自定义的类都使用引用传递参数。

条款21:必须返回对象是,别妄想返回其reference

在程序中不能玩盲目的返回引用

关键

不要返回一个指向局部变量的指针或者引用;不能返回一个指向heap_allocated对象的引用;

原因

1.因为局部变量在函数退出前就被销毁了

2.在堆中没有合理的方法让operator* 使用者进行delete调用

条款22:将成员变量声明为private

在成员变量声明的时候应该考虑变量将会被谁使用

关键

1.切记将成员变量声明为private。这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的弹性。
2.protected并不比public更具封装性。

原因

1.接口一致
2.精准的访问控制
3.良好的封装性,可以使得当我们改变这个成员的时候不会影响太多的代码,客户也不会知道类中的变化。

条款23:宁以non-member、non-friend函数替换member函数

当要调用一个类的多个函数时,应该声明为非成员函数,主要从封装性考虑

关键

宁可拿non-member non-friend函数替换member函数.这样做可以增加封装性,包裹弹性和机能扩充性。

解释

如果只是单纯的调用函数,那么就应该使用非成员函数去调用成员函数7,因为成员函数能够访问类中的私有成员,封装性比非成员函数要低

条款25:考虑写出一个不抛出异常的swap函数

swap的使用很多时候是效率不高的,这个条款可以使得swap函数变得效率高

关键

当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常

细节

1.常规的置换将会创建3个原来对象和三个指针对象,效率低,效率很低,实际上,我们只用置换指针的指向就行了。
1
2
3
4
5
6
7
8
namespace std 
{
template<>
void swap<Widget>(Widget& a, Widget& b)
{
swap(a.pImpl, b.pImpl); //交换指针值
}
}
2.我们不能修改std命名空间的任何东西,所以我们要声明一个非成员函数去调用成员函数
1
2
3
4
5
6
7
8
9
class Widget
{
public:
void swap(Widget& other)
{
using std::swap; // 调用成员函数
swap(pImpl, other.pImpl);
}
};

这就实现了对swap交换指针的优化

条款31:将文件的编译依存关系降至最低

单例对某个类进行修改时,如果不将编译的依存关系降至最低,那将会基本重写编译和连接,将会大大影响开发效率

关键

1.为声明和定义提供不同的文件
2.依赖于声明式不依赖与定义式

说明

在UE4中我们平时写代码就已经习惯性将声明和定义分开了,所以可以不用注意这部分,所以需要注意的是:要尽量使用”Class外部声明”代替”#include包含”,这样做的优点是可以不用去编译多余文件

class外部声明使用:

1.在AB类之间相互引用时使用class外部声明

2.在类中只需要其他类的引用而不需要其内部的定义,变量,方法时使用

例如

1
2
3
4
5
6
7
8
9
#include "StrategyGameMode.generated.h"

class AController;

UCLASS(config=Game)
class AStrategyGameMode : public AGameModeBase
{
GENERATED_UCLASS_BODY()
}

UE4中特定的条款

1.自己创建的非继承自UObject的类尽量使用智能指针TSharedPtr/TsharedRef来进行管理

如果你的純C++類是使用new來分配內存,而且你直接傳遞類的指針,那麼你需要意识到:除非你手动删除,否则这一块内存将永远不会被释放,如果忘记了,会造成内存泄露,如果使用智能指针会使用引用技术来完成自动的内存释放。

关键:
注意:TSharedPtr与UObject是互不兼容的,UObject会自动加入GC标记
例如
1.自己定义的Struct结构体
1
TSharedPtr<FActionButtonInfo> GetActionButton(int32 Index) const;
2.自己定义的类
1
TSharedPtr<class SStrategySlateHUDWidget> GetHUDWidget() const;
3.结合使用
1
2
TSharedPtr<TArray<class FStrategyMenuItem>> MainMenu;      //一组自定义类
TArray<TSharedPtr<TArray<class FStrategyMenuItem>>> MenuHistory; //自定义类数组引用的数组