Effective C++ 26 ~ 31 条款 实现 包括 尽可能延后变量定义式的出现时间 尽量少做转型动作 避免返回handle指向对象内部成分 为“异常安全”而努力是值得的

大多数情况i下,提出class和声明函数是最花费时间的地方,但有些实现上的东西还需要注意

条款26:尽可能延后变量定义式的出现时间

尽可能延后变量定义式的出现,这样可以增加程序的清晰度并改善程序效率。

问题A (效率问题):

// 这个函数过早定义变量“encrypted”
std::string encryptPassword(const std::string& password)
{
	using namespace std;
	string encrypted;
	if(password.length() < MinimumPasswordLength)
	{
		throw logic_error("Password is too short");
	}
	... // 必要动作
	return encrypted;
}

如果出现异常,那么string encrypted 则不会使用,就会付出构造成本和析构成本

解决(初步)

//
std::string encryptPassword(const std::string& password)
{
	if(password.length() < MinimumPasswordLength)
	{
		throw logic_error("Password is too short");
	}
	std::string encrypted; // 定义并初始化
		// 必要动作
		// 将password置入变量encrypted 内 
	return encrypt;
	...
}

这里延后了encrypted的定义式,但不够好,因为encrypted虽然获得定义,但无任何实参,意味着调用的是default构造函数,后面才进行赋值,效率不高

解决(最终)

//定义并初始化encrypted最佳做法
std::string encryptPassword(const std::string& password)
{
	... //检查length 如前
	std::string encrypted(password); // 定义并初始化
	encrypt(encrypted);
	...
}

这里在定义时就用password作为实参构造对象,跳过了default构造,做到了效率最大化
由此可以得出

尽可能延后的意义:

你不止应该延后变量的定义,直到非得使用该变量的前一刻为止,甚至应该尝试延后这份定义直到能够给他初值实参为止

问题B (清晰度问题):

// 方法A:定义于循环外
Widget w;
for(int i = 0; i < n; i++)
{
	w = 取决于i的某个值;
	...
}

// 方法B:定义于循环内
for(int i = 0; i < n; i++)
{
	Widget w(取决于i的某个值);
	...
}

使用 方法一或者方法二 ?

成本分析

一. 1个构造函数 + 1个析构函数 + n 个 赋值操作(Widget对象)

二. n个构造函数 + n个析构函数

结论

一般而言,二的做法比一好,原因是可理解性和易维护好

除非
  • 已知赋值成本比构造+析构低
  • 对于效率高度敏感
    则使用方法一,否则使用方法二

条款 27 :尽量少做转型动作

-------------------------------------------- 转型破坏了类型系统,那可能导致任何种类的麻烦

1. 如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_cast。如果有个设计需要转型动作,试着发展无需转型的替代设计。

2. 如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需要将转型放进他们自己的代码中。

3. 宁可使用C++ -style(新式)转型,不要使用旧式转型。前者很容易辨识出来,而且也比较有着分门别类的职掌。

旧式转型

  1. (类型说明符)表达式
  2. 类型说明符(表达式)

两个形式并无差别

新式转型

static_cast<类型说明符>(表达式)

用来强迫隐式转换,例如将non-const 转化成const 对象,int -> double 等等

dynamic_cast<类型说明符>(表达式)

  • 用于安全向下转型
  • 唯一无法由旧式转换执行
  • 唯一可能耗费重大成本

const_cast<类型说明符>(表达式)

  • 用于去除对象的常量性
  • 唯一有此能力发C++操作符

reinterpret_cast<类型说明符>(表达式)

  • 执行低级转型,例如pointer to int 的

新式转型的优势

  • 容易被辨识
  • 各转型的目标比较单一,编译器可以快速找出错误

唯一使用旧式转型的时机是:

当要调用一个explicit构造函数将一个对象传递给一个函数时

后面没太看懂,后期再补吧

条款 28:避免返回handle指向对象内部成分

避免返回handle(句柄:用来取得某个对象,包括reference、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助const成员函数的行为像个const,并将发生“虚吊号码牌”的可能性降至最低。

问题

class Point // 表示点
{
public:
	Point(int x, int y);
	...
	void setX(int newVal);
	void setY(int newVal);
	...
};

struct RectData // 这些点数据用来表示一个矩阵
{
	Point ulhc;
	Point lrhc;
};

class Rectangle
{	
	...
private:
	std::shared_ptr<RectData> pData; // 智能指针
};

用户需要能计算Rectangle 的 范围,所以这个class 提供upperLeft函数和lowerRight 函数,根据条例20(尽量用pass-by-reference-to-const(const引用)替换pass-by-value(传值)),可以实现以下函数

class Rectangle
{	
public:
	...
	Point& upperLeft() const {return pData->ulhc;}
	Point& lowerRight() const {return pData->lrhc;}
	...
};

问题所在 : 这两个函数返回指向对象的引用,那么用户可以使用这个指针修改成员而不被发现,那么就没有封装性了

教训(同样适用指针和迭代器):

  • 成员变量的封装性最多只等于“返回其reference”的函数的访问级别。本例中虽然pData的ulhc和lrhc都是private,但实际上是public,由于成员函数传出他们的reference。
  • 虽然是const成员函数,但是函数的调用者可以修改这笔数据

解决 :

class Rectangle
{
public:
	...
	const Point& upperLeft() const {return pData->ulhc;}
	const Point& lowerRight() const {return pData->lrhc;}
};

只要对它们的返回类型上加上const即可,这样用户可以读矩形的Points 但不能修改。

进一步的问题:

但即使如此, 还是返回了"代表对象内部"的handle,有可能在其他场合带来问题。更确切的说,可能导致dangling handle(虚吊号码牌):这种handle 所指的东西不复存在

class GUIObject {...};
const Rectangle
	boundingBox{const GUIOject & obj};

现在可能用户会使用这个函数;

GUIObject * pgo;
...
const Point * p = &(boudningBox(*pgo).upperLeft()); // 取得一个指针指向外框左上角

对boudningBox的调用获得新的Rectangle对象,当这个对象销毁时,间接导致里面的Points析构,最终导致 p 指针指向一个不存在的对象,从而p指针变成dangling虚吊的

这并不意味你绝不可以让成员函数返回handle,有时候你还必须那么做。例如:operator[] 。尽管如此,这样函数毕竟是例外,不是常态

条款29:为“异常安全”而努力是值得的

1. 异常安全函数(Exception-safe functions)即使发生异常也不会泄漏资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。

异常安全性函数三大保证 :
  • 基本承诺如果异常被抛出,程序内的任何事务仍然保持在有效状态下。没有任何对象或数据结构会因此而败坏,所有对象都处于一种内部前后一致的状态(例如所有的class约束条件都继续获得满足)
    • 强烈保证如果异常抛出,程序状态不改变。调用这样的函数需要有这样的认知:如果函数成功,就是完全成功,如果函数失败,程序会回复到“调用函数之前”的状态
    • 不抛异常保证承诺不抛出异常,因为他们总是能够完成他
  • 们原先承诺的功能。作用于内置类型(如指针等等)身上的所有操作都提供nothrow保证

2. "强烈保证"往往能够以copy-and-swap实现出来,但“强烈保证”并非对所有函数都可实现或具备现实意义。

3. 函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者。

条款 30:透彻了解inline 的里里外外

1. 将大多数inline限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。

2. 不要只因为function template出现在头文件,就将它们声明为inline。

inline的思想 :

将“对此函数的每一个调用”都以函数本体替换之

inline的优势 :

  • 免除函数调用成本
  • 编译器就因此有能力对它执行语境相关最优化。
  • 如果inline函数的本体很小,编译器针对”函数本体“所产出的代码可能比函数调用产生的代码更小

inline的缺点:

  • 可能增加目标码的大小,过度使用inline会导致程序体积过大,引发换页行为
  • 无法随着程序库的升级而升级,如果f是程序库内的一个inline函数,客户将“f函数本体”编进其程序中,一旦程序库设计者决定改变f,所有用到f的客户端程序都必须重新编译。
    然而如果f是non-inline函数,一旦它有任何修改,客户端只需重新连接就好,远比重新编译的负担少很多。
  • 构造函数和析构函数往往是inline的糟糕候选人

inline 提出的特点:

inline只是对编译器的一个申请,不是强制命令。既可以隐喻提出,也可以明确提出

  • 隐喻提出
    成员函数和friend函数 在class内定义都被隐喻声明为inline函数。
class Person
{
public:
	...
	int age() const // 一个隐喻的inline申请
	{
		return theAge;
	}
private:
	int theAge;
};
  • 显示提出

定义式前加上关键字inline

template<typename T>
inline const T& std::max(const T& a, const T& b) // 明确申请inline
{
	return a < b ? b : a; 
}

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

支持”编译依存性最小化“的一般构想是 : 相依于声明式,不要相依于定义式,基于此构想的两个手段是Handle classes 和 Interface classes 。

程序库头文件应该以“完全且仅有声明式”形式存在,这种做法不论是否涉及templates都适用。

能力有限,实在没什么经验,看不太懂。。。
有机会回来补完

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值