大多数情况下,适当提出你的classes(和class template)定义以及functions(和function templates)声明,是花费最多心力的两件事。一旦正确完成它们,相应的实现大多直接了当。尽管如此,还是需要注意某些东西:
- 太快定义变量可能造成效率上的拖延
2.过渡使用转型(casts)可能导致代码变慢又难维护
3.返回对象“内部数据之号码牌”可能会破坏封装并留给客户虚吊号码牌
4.未考虑异常带来的冲击则可能导致资源泄露和数据败坏
5.过度热心地inling可能引起代码膨胀
6.过度耦合(coupling)则可能导致让人不舒服的冗长建置时间(build times)
条款二十六:尽可能延后变量定义式的出现时间
看这一段代码,我们调用CreateStr函数。返回一个res
但是,如果temp的长度太小的话,就会因为异常导致res后续不处理。
相当于res无端的进行了构造和析构,但是没有被使用,这非常浪费
#include <iostream>
using namespace std;
string CreateStr(const string& temp )
{
static int minLength = 10;
string res(temp);
if (temp.size() < minLength)
{
throw logic_error("too short");
}
cout << "处理res" << endl;
res = temp + "123456";
return res;
}
int main()
{
try
{
string str = CreateStr("123456");
}
catch (exception ex)
{
cout << ex.what() << endl;
}
return 0;
}
/*
运行结果:
too short
*/
所以我们尽可能要在我们需要使用的时候再用它。
即改成:
string CreateStr(const string& temp )
{
static int minLength = 10;
if (temp.size() < minLength)
{
throw logic_error("too short");
}
cout << "处理res" << endl;
string res(temp);
res = temp + "123456";
return res;
}
你应该尝试延后定义直到你能够给他初值实参为止。
那么循环怎么办?
#include <iostream>
using namespace std;
class A
{
public:
A():value(0) {}
A(int x) :value(x) {}
const A operator=(int x)
{
return A(x);
}
int value;
};
int main()
{
//方1
A a;
for (int i = 0; i < 10; i++)
{
a = i;
}
//方2
for (int i = 0; i < 10; i++)
{
A a1(i);
}
return 0;
}
方1:1个构造函数+1个析构函数+n个赋值操作
方2:n个构造函数+n个析构函数
所以具体哪个好需要看构造析构的成本和赋值的成本谁大
请记住:
尽可能延后变量定义式的出现,这样做可增加程序的清晰度并改善程序效率。
条款二十七:尽量少做转型动作
旧式转型:
(T) expression //将expresssion转型为T
T(expression) //将expression转型为T
新式转型:
const_cast<T> (expression)
dynamic_cast<T> (expression)
reinterpret_cast<T> (expression)
static_cast<T> (expressiion)
1.const_cast通常被用来将对象的常量性转出。它也是唯一有此能力的C+±style转型操作符;
2.dynamic_cast主要用来执行“安全向下转型”,也就是用来决定某对象是否归属继承体系中的某个类型。它是唯一无法由旧式语法执行的动作,也就是唯一可能耗费重大运行成本的转型动作;
3.reinterpret_cast意图执行低级转型,实际动作(及结果)可能取决于编译器,这也就表示它不可移植。例如将一个pointer to int转型为一个int。
4.static_cast用来强迫隐式转换,例如将non-const对象转为const对象(条款3),或将int转为double等。它也可以用来执行上述多种转换的反向转换,例如将void*指针转为typed指针,将pointer-to-base转为pointer-to-derived,但无法将const转为non-const(仅仅const_cast可做到);
我们尽可能使用新式转型而不是旧式的,因为旧式的不直观。
dynamic_casts本人没用过,而且不怎么用,就不举例了。
请记住:
如果可以,尽量避免转型,特别实在注重效率的代码中避免dynamic_casts。如果有个设计需要转型动作,试着发展无需转型的替代方案。
如果转型是必须的,试着将他隐藏于某个函数背后。客户随后可一个调用该函数,而不需要将转型放进他们自己的代码内。
宁可使用C+±style(新式)转型,不要使用旧式转型。前者很容易被辨识出来。
条款二十八:避免返回handles指向对象内部成分
看如下代码:
upperLeft()的本意应该是只读的,但是此时用户却可以写了。
#include <iostream>
using namespace std;
class Point {
public:
Point(int x, int y):x(x),y(y){}
void setX(int newVal) { x = newVal; }
void setY(int newVal) { y = newVal; }
private:
int x;
int y;
};
struct RectData {
Point ulhc;//左上角点
Point lrhc;//右下角点
};
class Rectangle {
public:
Rectangle(Point ulhc, Point lrhc) :pData(new RectData({ulhc, lrhc})) {}
Point& upperLeft()const { return pData->ulhc; }
Point& lowerLeft()const { return pData->lrhc; }
private:
shared_ptr<RectData> pData;
};
int main()
{
Point p1(0, 0);
Point p2(0, 0);
Rectangle rec(p1, p2);
rec.upperLeft().setX(50);//我们本意应该是不能让其更改的,但是他现在可以更改了。
return 0;
}
/*
运行结果:
*/
这里的例子立刻给我们带来两个教训:
成员函数的封装性最多只等于”返回 其reference“的函数的访问级别。例如本例中的ulhc和lrhc是private但是实际上却是public的,因为成员函数upperLeft传出了他们的引用。
如果const成员函数传出一个reference,后者所指数据与对象自身有关联,而他又被储存于对象之外,那么这个函数的调用者可以需改这个数据。
References,Pointer,Iterators都是所谓的handles(号码牌,用来取得某个对象),而返回一个代表对象内部数据的handle,随之而来的是降低对象封装性的风险。
只需要把返回值改成const就行
const Point& upperLeft()const { return pData->ulhc; }
const Point& lowerLeft()const { return pData->lrhc; }
但依然改变了虚吊的情况。
#include <iostream>
using namespace std;
class Point {
public:
Point(int x, int y):x(x),y(y){}
void setX(int newVal) { x = newVal; }
void setY(int newVal) { y = newVal; }
private:
int x;
int y;
};
struct RectData {
Point ulhc;//左上角点
Point lrhc;//右下角点
};
class Rectangle {
public:
Rectangle(Point ulhc, Point lrhc) :pData(new RectData({ulhc, lrhc})) {}
const Point& upperLeft()const { return pData->ulhc; }
const Point& lowerLeft()const { return pData->lrhc; }
private:
shared_ptr<RectData> pData;
};
const Rectangle GetRect()
{
Point p1(0, 0);
Point p2(0, 0);
Rectangle tempRect(p1,p2);
return tempRect;
}
int main()
{
const Point* p=&(GetRect().upperLeft());
return 0;
}
该调用中,GetRect取得一个临时对象指向Rectangle对象。我们暂且称之为temp。然后upperLeft再作用其上,得到临时的点数据。但是当这条语句指向结束,这个temp就消失了。所谓的Pointer也就是成了(虚吊)dangling handles。
请记住:
避免返回handle(包括references,指针,迭代器)指向对象内部。遵守这个条款可增加封装性,帮助const成员函数的行为像个const,并将发生”虚吊号码牌”的可能性降至最低
条款二十九:为”异常安全“而努力是值得的
先看如下代码,
#include <iostream>
#include<mutex>
using namespace std;
class A
{
public:
void Func(const int& i)
{
mutex.lock();
delete x;
++cnt;
x = new int(i);
mutex.unlock();
}
private:
mutex mutex;
int* x;
int cnt;//用于计数
};
int main()
{
A a;
a.Func(10);
return 0;
}
每次调用Func函数,会先删除x,然后再new一个x,并使cnt++表示Func的执行次数。
但是这个函数不符合“异常安全性”:
1.不泄露任何资源。如果new int(i)导致异常,则会使unlock不执行,于是mutex就会永远锁住。
2.不允许数据败坏。如果new int(i)导致异常,那么x就指向了一个删除的对象,并且cnt也会被无端的++。
泄漏资源的问题的解决方案很简单,就是使用之前学的资源管理
如shared_ptr去管理锁,使得他能自动控制lock和unlock时机。
#include <iostream>
#include<mutex>
using namespace std;
void unlock(mutex* x)
{
x->unlock();
}
class Lock
{
public:
Lock(mutex* pm) :mutex(pm, unlock) { mutex.get()->lock(); }
private:
shared_ptr<mutex> mutex;
};
class A
{
public:
void Func(const int& i)
{
Lock m(&mutex);
delete x;
++cnt;
x = new int(i);
cout << (*x) << " " << cnt << endl;
}
private:
mutex mutex;
int* x;
int cnt;//用于计数
};
int main()
{
A a;
a.Func(10);
a.Func(20);
return 0;
}
然后就是解决数据败坏问题。
我们先看看异常安全函数(Exception-safe functions)提供以下三个保证之一:
1.基本承诺:如果异常抛出,程序内任何事物仍然保持在有效状态下。没有任何对象或数据结构会因此而败坏,所有对象都处于一种内部前后一致的状态。
2.强烈保证:如果异常被抛出,程序状态不改变。调用这样的函数需有这样的认知:如果函数成功,就完全成功,如果函数失败,程序会回到“调用函数之前”的状态。
3.不抛异常(nothrow)保证:承诺绝不抛出异常,因为它们总是能够完成它们原先承诺的功能。作用于内置类型(例如ints,指针等)身上的所有操作都提供nothrow保证。这是异常安全码中一个必不可少的关键基础材料。
nothrow类型是最好的情况,但是我们很难做到,比如int& i如果出了异常我们就没办法处理。
但我们勉强可以做到强烈保证型
数据败坏主要原因是,我们delete了数据,但在准备重新new的时候出现了问题,解决方案有两种,
1.使用资源管理去处理
class A
{
public:
void Func(const int& i)
{
Lock m(&mutex);
x.reset(new int(i));
++cnt;
}
private:
mutex mutex;
shared_ptr<int> x;
int cnt;//用于计数
};
2.使用copy and swap方式去处理
这里就不举代码例子了。后面章节会详细谈copy and swap。
当你写代码时,请仔细想想是否具备异常安全性,在实际开发中,我们应该避免内存泄漏和数据被破坏的情况,但是要做到上述所说的不抛异常型函数很难,所以我们至少需要了解这些问题,才能写出优质代码。
请记住:
异常安全函数即使发生异常也不会泄漏资源或允许任何数据结构败坏。这样的函数区分三种可能的保证:基本型,强烈型,不抛异常型。
”强烈保证”往往能够以“copy and swap”实现出来,但”强烈保证”并非对所有函数都可实现或具备实现意义。
函数提供的“异常安全保证”通常最高只等于其所调用之各函数的”异常安全保证”中的最弱者。
条款三十:透彻了解inlineing的里里外外
1.inline函数背后整体观念是,将"对此函数的每一个调用"都以函数本体替换之
2.Inlining在大多数C++程序中是编译行为
3.大部分编译器拒绝将太过复杂(例如循环或递归)的函数inlining,而所有对virtual函数的调用也会使inlining落空,因为virtual意味着"等待,知道运行期才确定调用那个函数",而inling意味着"执行前,先将调用动作替换为被调用函数的本体";
4.编译器通常不对"通过函数指针而进行的调用"实施inlining,这意味着对inline函数的调用有可能被inlined,也有可能不被inlined
5.构造函数和析构函数往往是inlining的糟糕候选人。C++对于“对象被创建和销毁时发生什么事”做了各式各样的保证。C++描述了什么一定会发生,但没有说如何发生。”事情如何发生”是编译器实现者的权责,不过至少我们知道他不会凭空发生。所以编译器可能会帮你偷偷的制造一些代码,但是如果你用inline的话,会影响编译器。
6.大部分调试器对inline函数都束手无策,因为你无法在一个并不存在的函数内打断点调试。
请记住:
将大多数inlining限制在小型,被频繁调用的函数身上。这可使日后的调试过程和二进制升级更容易,也可使潜在代码膨胀问题最小化,使程序的速度提升机会最大化。
不要只因为function templates出现在头文件,就将 他们声明为inline。
条款三十一:将文件间的编译依存关系降至最低
当你对代码某个内容进行改变都会重新编译源文件,所以我们需要想办法降低耦合度。
本人理解浅薄,未看懂书上内容,所以找了一篇高质量文章代替