条款二十二
1、务必对成员变量使用private,然后用函数读取它们,看似多次一举,实际上使用函数读取可以保证每次都进行正确的值检测,并且可以在用户无需了解的情况下计算/读取方式、可以细微得划分只读/只写。
type getNum(){}
这个函数返回的可以是一个成员的值,这个成员储存了需要的值(在内存次要时)
return member;
也可以在函数内调用相应的成员进行计算(在内存重要,调用次数不多时)
{
//计算
return result;
}
2、protected的封装性并没有比public好,protected中改变一个成员,破坏的是所有派生类,public则是,鬼晓得!只有private(封装)与其他(不封装),这两种封装性。
条款二十三
使用non-member函数来代替member(前提是可以)
把需要使用private成员的函数放在类里,调用一系列成员函数完成某种功能的“便利函数”定义成non-member会更好。
这样做可以避免用户去编译他们不需要的功能。若把所有的“便利函数”放到类里,那么就意味着用户要编译全部的功能。
可行的做法是把“仅仅调用成员函数的函数”放在和定义的类所在的同一个命名空间中。然后把命名空间在不同的文件展开,把不同的功能的函数放在不同的头文件中。这么做用户便只需要编译他们想要的功能部分,和必须编译的类部分。
故使用non-member函数代替member函数,可以增加封装性、包装弹性、机能扩充性。
条款二十四
如果一个函数的全部参数都可以进行类型转换,那么它只能是一个非成员函数(这些类型转换通常是单一参数的构造函数定义的隐式转换,也就是其他类型到本类型的转换)
比如operator*定义成成员函数则会导致“数值 * 类型”无法通过编译,因为operator*的第一个参数绑定了“类型”,所以只能以 “类型 * 数值”的方式使用operator*,此时就应当改成非成员函数(通过友元等实现)。
条款二十五
使用一个编写正确,不抛出异常的swap
标准库的swap实现
template<typename T>
void swap(T& lhs, T& rhs)
{
T temp(lhs);
lhs = rhs;
rhs = temp;
}//拷贝并交换
简单的拷贝并交换,可能引起三次拷贝交换,因为operator=通常都需要“拷贝并交换”。也就是说,当你swap时,虽然只有三句代码,但是第一句调用了拷贝构造函数进行一次拷贝,后两句调用了operator=进行了“拷贝并交换”又发生两次拷贝(额外的临时对象)。
那么当你不需要这样的交换时,定义一个自己的版本,比如只交换内置指针,而不是构造三次对象。
对于非template类,即非模板类
定义一个swap()成员函数,再定义一个非成员函数(接受两个参数的)来调用这个函数,接着再定义一个对std::swap的偏特化版本,最好和这个类放在一起,因为使用这个类的客户应该都会需要它的偏特化swap。
namespace ForSwapStuff {
class ForSwap {
public:
void swap(ForSwap& rhs)
{
int *temp = pointer;
pointer = rhs.pointer;
rhs.pointer = temp;
}//成员函数交换private成员
private:
int *pointer;
};
void swap(ForSwap& lhs, ForSwap& rhs)
{
lhs.swap(rhs);
}//非成员函数调用public swap函数保证封装
}//放入namespace,在名字查找时会代替std版本
namespace ForSwapStuff {
//特化了std的版本,在使用std::swap时也会使用特化版本
class ForSwap {
public:
void swap(ForSwap& rhs)
{
int *temp = pointer;
pointer = rhs.pointer;
rhs.pointer = temp;
}//仅交换指针
private:
int *pointer;
};
void swap(ForSwap& lhs, ForSwap& rhs)
{
lhs.swap(rhs);
}
}
namespace std {
template<>
void swap<ForSwap>(ForSwapStuff::ForSwap& lhs, ForSwapStuff::ForSwap& rhs)
{
lhs.swap(rhs);
cout << "swap" << endl;
}
}
//泛型模板的版本
namespace ForSwapStuff {
template<typename T>
class ForSwap {
public:
void swap(ForSwap& rhs)
{
int *temp = pointer;
pointer = rhs.pointer;
rhs.pointer = temp;
}
private:
T * pointer;
};
template<typename T>
void swap(ForSwap<T>& lhs, ForSwap<T>& rhs)
{
lhs.swap(rhs);
cout << "swap" << endl;
}
}
结论:
1、成员版的swap不要抛出异常,当swap进行到一部分的时候,对象的值发生改变,此时却发生了异常,会使得对象破坏。
2、如果提供了成员版的swap那么就再提供一个非成员版的swap去调用成员版的,非成员版的介绍两个此类的引用,最好再提供一个偏特化的swap,防止客户使用std:swap。事实上调用swap时应当先加上using std::swap,根据名字查找规则,编译器优先考虑自定义的版本,然后是特例化版本,最后才是系统版本。
3、可以特例化std中的函数,但是绝对不要扩充std。
条款二十六
尽量延后变量出现的位置。
最好是在变量定义时就可以为变量赋值,或者延后到变量使用的前一刻才进行定义,因为定义到使用的这个区间可能阻止了这个变量的使用,那就只是定义变量,然后释放。
对于循环中的变量,耗费的是一组构造/析构,如果不是明确知道“构造 + 析构”的效率低于一个赋值操作,那么直接在这个循环中使用它的位置中定义这个变量会更好。因为这样可以让程序的结构更加清晰(况且你的拷贝赋值运算符很可能是用拷贝并交换的方式实现的)
故
type a;
for(...){ use a }
for(...){ type a;use a}
条款二十七
1、尽量避免转型的发生,尤其是dynamic_cast,在多继承体系中它不可避免得需要调用多次strcmp来拷贝每个类名,还有其他的各种比较,在效率上非常不理想,也无法保证类型安全。尽量使用继承体系的virtual函数来控制器发生,比如定义一个什么也不做的virtual缺省函数,有时就可以避免一些dynamic_cast。
2、不要尝试用static_cast来转换成基类后调用基类的某个版本的函数,应该使用域作用符的方式来调用,使用static_cast转换类型后调用相当于在一个副本上调用成员函数,会造成“基类不改变,派生类被改变”的奇怪状态
Derived &d;
Base &b = d;
//根据多态,这是可行的,但是c++与java,c,c#不同的一点是,此时的d相当于有了两个不
//同的地址,一个地址是Derived(派生类)的,一个是Base(基类)的,这也是为啥//static_cast<Base>(d).成员函数(),是错的,因为这把d的基类部分“取出”,构造了一个副
//本,调用成员函数的是副本,不是d中的Base部分。
3、可以使用保存了智能指针的容器来代替cast,这样做的话一般来说你可能需要多个容器(存放基类/派生类)然后使用相应容器存放相应指针
4、尽量使用新式的转换而不是c风格的转换,甚至是在你构造一个对象传递给函数的时候,也可以用static_cast注意reinterpret_cast(其实更应该说是类型指定)是编译器相关的,不可移植。
fun( (type(...)) ) == fun( static_cast<type>(...) )
int a,b; double c = a/b == double c = static_cast<double>(a)/b
使用类似这样的显示新型转换可以让你得到更多的错误信息(当然是在你真的有出错的时候)。
5、如果你无论如何都需要转型了,那么把这个操作包装进函数,让用户调用这个函数。更好的或许还是由你自己调用这个函数,尽量不要把这个类型转换的责任交给你的客户(你并不知道你的客户会不会记得要转换)
6、绝对不要连写多个dynamic_cast、
if(Derived1 *d1 = dynamic_cast<Derived1*>(基类指针))
{ 使用Derived1的成员 }
else if(Derived2 *d2 = dynamic_cast<Derived2*>(基类指针))
{ 使用Derived2的成员 }
以此类推
这样做的效率极低(而且要是你的派生类够多。。。),每次有新的派生类还要添加新的条件分支,用virtual函数代替他们。
条款二十八
避免返回一个内部成员的handle(引用或指针或迭代器)
当返回了一个成员的handle的时候,首先是你可能对一个函数返回值赋值,也可以通过一个const成员函数返回的引用来修改private成员的值。这一点通过定义const的返回值可以解决,但并不意味着就是安全的。
class Mem {
public:
Mem() :mem(1), memp(&mem) {}
int *memp;
int mem;
};
class ConstReturn {
public:
ConstReturn() :mem(){}
const Mem &getMp() const
{
return mem;
}
~ConstReturn() { cout << "析构执行,此时*memp=" << *mem.memp << endl; }
private:
Mem mem;
};
const ConstReturn tempConstReturn()//返回临时创建的对象
{
return ConstReturn();
}
const Mem *a;
void test()
{
a = &(tempConstReturn().getMp());//取一个临时对象的地址
//其他
cout << "创建语句后a:" << a->mem << endl;
}
int main()
{
test();
cout << "main函数中a:" << a->mem;
//cout << "a->memp" << *a->memp << endl;//运行时错误,memp为空指针
int z;
cin >> z;
}
结果为
析构执行,此时*memp=1
创建语句后a:1
main函数中a:1
这里的*a指向一个无名的ConstResult的Mem成员,但事实上,这个无名的ConstResult对象在这句结束后就应该被摧毁(析构函数已经调用),也就意味着它的Mem成员也会一并摧毁,这让a成为了一个空悬指针(虽然实际上在单线程的编译运行时这段代码不会有任何问题)。
总之,返回一个类内部成员的handle是非常不明智的,应该尽量避免,即使返回也应该返回const引用来避免外部的修改。
其他:
关于析构函数,调用析构函数并不意味着对象就被摧毁了(特别当你显示调用的时候)。只是对象的那部分内存变为了可用的,析构函数其实还是像其他的成员函数一样,执行了里面的操作而已,当你使用delete的时候才是删除了内存。也就意味着,你使用析构函数后任然可以访问数据成员,单线程的话几乎不会出错,但是delete后则不行,可能是乱码也可能挂掉。
记住,析构函数执行之后才是内存回收(系统的默认删除法)。构造函数之前进行内存分配,构造函数体执行的时候,所有成员已经初始化完成。
条款二十九
保证函数的异常安全性
函数的异常安全性分三种
- 基本保证,保证异常发生后,类的结构,各种数据等会处于一个有效状态。比如替换图片时发生异常,那么就把原图片放回,或把某缺省图片放入,至于具体怎么做的,用户可能需要调用一个成员函数才知道。
- 强烈保证,保证异常发生后,数据保持发生前的状态,就像没调用这个函数一样。
- 无异常保证,函数根本不会抛出异常。意味着这个函数总是能完成它的工作。用于内置类型的所有函数操作都是nothrow保证的(内置指针、数值等)。
class Mutex {//测试用的Mutex
public:
Mutex():locked(false) { }
~Mutex() { }
void lock() { locked = true; }
void unlock() { locked = false; }
private:
bool locked;
};
class Lock {//管理Mutex的RAII类
public:
Lock():mutex() { mutex->lock(); }
Lock(Mutex *mu) :mutex(mu) { mutex->lock(); }
~Lock() { mutex->unlock(); }
private:
Mutex *mutex;
};
struct Picture {
};
struct MenuBar {
int changeTimes;
shared_ptr<Picture> pic;
};
class Menu {
//两个change函数强烈异常安全的前提是Picture的构造不会出现改变其他
//状态的问题,假如两个函数的参数改为流对象,那么Picture构造失败时
//很可能就改变了stream的读取位的状态,这样虽然Menu状态不变,但是
//其他读取流的用户受到影响。
public:
void changePicture(Picture *p)
{
Lock lock(&mutex);//使用对象管理Mutex
pic.reset(new Picture(*p));
//使用reset便不用再删除原对象,因为reset已经帮我们做了
//并且在new创建的Picture成功前,是不会reset的
changeTimes++;
}//普通的强烈异常安全
void copyAndSwapPicture(Picture *p)
{
using std::swap;
Lock lock(&mutex);
shared_ptr<MenuBar> newPic(new MenuBar(*mb));//复制原对象
newPic->pic.reset(p);//改变副本对象
newPic->changeTimes++;
swap(mb, newPic);//交换原对象与副本
}//使用copy and swap实现强烈异常安全,对象修改成功后才会交换
private:
shared_ptr<Picture> pic;
shared_ptr<MenuBar> mb;
int changeTimes;
Mutex mutex;
};
- 异常保证有三种类别
- 强烈保证并不是对所有的函数都有效/可行,毕竟需要复制对象(若使用swap and copy),这可能会带来非常大的消耗。
- 函数提供的异常安全保证强弱,只能等于函数调用的函数中最低的那个
条款三十
谨慎选择你的inline函数,只有真正频繁使用的函数才应该是inline函数。
inline函数通常一定在头文件内,因为inline函数在编译期就需要让编译器看到他的全部定义(为了把函数调用替换成函数本体),除非你的建制环境支持。
不要因为template函数定义在头文件中就把它定义成inline。对于模板函数,只有你相信所有的种类都需要inline时才定义成inline否则别这么做
对于inlin函数,他们会增加源码的大小,在每个inline调用的地方。但不是说inline每次调用都是inline的,函数指针调用inlin通常都不是inline另外所有virtual都阻止inline,因为virtual意味着运行时。
对于类,把构造/析构函数声明成inline是很糟糕的选择,因为它们之中包含:基类的构造,每个成员的构造,构造每个成员时还有相应的try{}catch{}语句保证一旦有异常抛出就会销毁之前构造的部分。
关于inline造成的影响,修改inline函数需要重新编译每个inline的位置,是非常大的开销;而如果这个函数不是inline,则只需要重新连接即可
条款三十一
让文件之间的编译依赖性最小
使得依赖依存性最小的原则是,让用户的使用依赖于声明函数/类的文件,而不是定义函数/类的文件,即提供仅有声明的头文件,再把实施放到另一个地方。使用Handle Classes或者Interface Classes来实现这一点
1、Handle Classes
声明一个返回实际类各个成员的Handle Classes在一个头文件中,这个头文件不需要包含实际类的头文件,只需要提供实际类的前置声明;然后在另一个头文件中同时包含这个Handle Class的头文件和实际类的头文件,并在此提供Handle Class的各个接口函数实现。
实际类Personimpl.h
#include<string>
struct PersonImpl
{
PersonImpl(const std::string& n, int a) :name(n), age(a) {}
~PersonImpl();
std::string name;
int age;
};//提供各个成员和构造,不需要是class,因为外部接口提供了封装
接口类Handle Class所处的personfwd.h
#include<string>
#include<memory>
//不包含实际类的头文件,所以实际类若更改,这里不需要编译,使用这个接口的客户也不用重新编译
struct PersonImpl;//前置声明,以便之后的声明使用
class Person
{
public:
Person(const std::string &name, const int age);//提供一样的构造函数
Person(std::shared_ptr<PersonImpl> p);
~Person();
std::string name();
int age();//提供和实际类的成员完全一致的接口函数
private:
std::shared_ptr<PersonImpl> pImpl;//使用智能指针防止内存泄漏
};
最后是实现接口的头文件Person.h
#include<string>
#include<memory>
#include"personfwd.h"
#include"PersonImpl.h"
//因为这里是具体的实现,故需要包含实际类的头文件
//调用相应的构造函数
Person::Person(const std::string &name, const int age) :pImpl(new PersonImpl(name, age)) {}
Person::Person(std::shared_ptr<PersonImpl> p) : pImpl(p) {}
Person::~Person() {}
//返回同名成员
std::string Person::name() { return pImpl->name; }
int Person::age() { return pImpl->age; }
Interface Class
使用除了析构函数以外全部都是虚函数的一个接口,以及相应的static工厂函数和继承来实现声明定义的分离。用户必须通过指针/引用来使用(事实上,出于内存安全的考虑,用户得用智能指针)。
接口类的头文件Person.h
#include<memory>
#include<string>
class Person {
public:
virtual ~Person() {}//析构不能是纯虚,或是提供了定义的纯虚
virtual std::string name() const = 0;
virtual int age() const = 0;//成员接口
static std::shared_ptr<Person> create(const std::string &name,int age);//工厂函数
};
实际类的头文件RealPerson.h
#include"Person_Interface.h"
class RealPerson : public Person//实现接口的函数
{
public:
RealPerson(const std::string& n, int a) :pname(n), page(a) {}
~RealPerson() override {};
std::string name() const override { return pname; }
int age() const override { return page; }
private:
std::string pname;
int page;
};
std::shared_ptr<Person> Person::create(const std::string &name, int age)
{
std::shared_ptr<Person> p = std::make_shared<Person>(new RealPerson(name, age));
return p;
}//提供了实际类定义后就可以实现接口程序
可见interface class返回的是智能指针,这强迫用户使用智能指针来防止内存泄漏。
使用方式:
shared_ptr<Person> p = Person::create("asdf",5);
//可以给Handle Class也定义一个类似的工厂函数在声明文件里,这样就也能使用智能指针
//保障自动释放,防止用户使用这个类的指针时忘记delete等
代价:
Handle Class
需要额外的空间,给implementetion pointer即指向实际类的shared_ptr,而且这也使得取得成员需要使用指针,增加了一层间接性(如果再添加工厂函数,那就是两层了!)。另外你必须初始化这个指针,那么就需要承受new内存不足的后果以及使用动态内存带来的额外开销。
Interface Class
首先,由于是继承体系,所以派生类需要一个vptr(虚指针),这可能会增加空间需求的数量(取决于除了interface class以外还有没有virtual函数);另外调用virtual函数本来就需要做出一个间接跳跃(indirect jump)的成本。
结论:
能够使用object reference/pointers时就使用,尽量少用objects(对象),这一点是因为不需要知道类的具体定义就可以使用一个类的引用/指针,但具体类型则需要完整的定义。
为声明和定义类提供不同的头文件,让用户包含声明的头文件即可工作,防止改动类后需要大量的重新编译。也就是说,相比依赖定义,更好的是依赖声明。