构造/析构/赋值运算
一、尽量用const eunm inline 替代define
-
静态类内常量的类内初始化只支持整型(即是整数类型,int/long/uint64_t都可以),如是浮点类型等其他类型,需使用类外初始化,初始化时不需要添加static关键字。
-
非const的静态成员,一律需要类外初始化
二、尽可能用const
-
例如重载一个对象的*运算符时,如果不是反回一个常量对象(即返回值是const),那么就可以在重载函数外对这个对象赋值,在笔误的情况下有可能完成难以排查的非预期错误
-
const成员函数,可以读取所有数据成员,但要取地址、写修改时,只能修改静态成员变量,且不允许调用其他non-const成员函数。另外,non-const对象实体,可以调用const成员函数和non-const成员函数,但const对象实体仅能调用const成员函数,这是因为const对象不允许改变对象内的任何一个bit。
-
变量声明时,const修饰指针,取决于从右到左,const先结合的部分,const和指针名结合,指针保存的地址不能被修改,const和*结合,说明指针指向的变量的值不能修改。
-
STL容器中 const T::iterator是一个T* const,其指向的值可以修改,但本身不能做++之类的修改;除非我们使用T::const_iterator,可实现指向的值不能修改,但能++修改,类似一个const T *
-
mutable修饰成员变量,可以使得这些成员变量能在const成员函数中修改。
-
事实上可以通过const_cast<T>()将cosnt特性移除,但不推荐这么做
-
类成员函数的常量性不同(和返回值无关,就是相同参数但末尾是否有const),就可以被重载;可以根据对象实体的常量性,会调用不同的重载函数
char & operator[] (std::size_t position) {
cout << "execute non-const operator[]" << endl;
return string_base[position];
}
// 如果只是返回值const,将不满足重载条件,需作为const成员函数
const char & operator[] (std::size_t position) const {
cout << "execute const operator[]" <<endl;
return string_base[position];
}
output:
cs Text string null
cs Text string : Happy new year
execute const operator[]
y
execute non-const operator[]
ds Text
ds Text
三、确定对象被使用前已先被初始化
-
内置类型变量必须手工初始化
-
在类构造函数的初始化列表中,列出所有成员变量,以免记忆和遗漏变量初始化,最好初始化的次序和声明的次序一致
-
两个编译单元(例如不同文件)中的non-local static对象(函数外面声明的static对象),多个non-local static的初始化次序是无法确定的,如果遇到需要彼此依赖的场合,可以参考Singleton模式的方法:在函数内部声明静态对象,通过reference的方式返回给调用方,以此将non-local static转化为local static对象,该函数称为reference-returning函数。
-
任何non-const static对象,在多线程系统环境下都需要特别注意!初始化时,可以在程序的单线程初始化阶段,手工调用reference-returning函数一次。但执行过程中的务必考虑多线程数据访问问题。
四、了解c++默默编写并调用哪些函数
-
默认构造、默认拷贝、默认析构(非virtual的)、默认赋值
-
基类的析构为virtual的情况下,默认的析构函数也具有virtualness属性;其实这个无所谓,因为原本默认析构函数内也没做任何事,能否调用到问题都不大
-
默认拷贝构造、默认赋值构造函数,都是将对象每一个non-static成员拷贝到目标对象
-
成员变量有const和reference、base class中的拷贝、赋值构造为private时,编译器无法生产默认的拷贝、赋值构造函数
五、若不想使用编译器自动生成的函数,就该明确拒绝
-
拒绝方式,可以将拷贝、赋值构造函数放置在private且不提供实现;如何赋值和拷贝时就能报编译错误,而内部函数、friend函数调用时,在linker时会报错
-
额外的,可以写一个Uncopyable作为空的基类,把拷贝、赋值构造函数放置在private且不提供实现;需要屏蔽赋值、拷贝的类继承它即可,boost也提供了类似功能的类:noncopyable
六、为多态基类声明virtual析构函数
-
一个类中没有virtual函数,即不需要派生其他类且用作多态使用时,不建议对析构函数virtual,因为虚函数表会增大对象的体积
-
STL容器都是non-vitrual的,不要傻逼到派生它们
-
要多态的基类可以只将析构函数声明为pure virtural,以实现一个抽象类,但我们需要提供一个析构函数的实现,否则linker会报错
七、别让异常逃离析构函数
-
异常会导致析构函数非正常退出,中断资源的回收而导致资源泄漏,不一定是指内存,例如于服务端通信的连接等资源
-
可以在析构函数中通过try catch(...)转std::abort()中断程序或捕获作后续处理
-
较佳的选择是提供一个额外的回收函数如close(),供用户手动关闭,并结合1、2两点作收尾
八、绝不在构造和析构过程中调用virtual函数
事实上base class构造调用的virtual函数就是base class自己的版本,derive class调用的也是自己的版本,如果两边都对virtual做了实现,那么实例化derive对象的时候,两个virtual函数都会调用到。这个似乎符合逻辑,感觉作者过于担心了,一般也不会这么使用。如果调用的函数是pure virtual的函数,那当然会发生链接错误。
class Transaction {
public:
Transaction() {
logTransaction();
}
virtual void logTransaction() {
cout << "Transaction log execute!" << endl;
}
};
class BuyTransaction : public Transaction {
public:
BuyTransaction() {
logTransaction();
}
virtual void logTransaction() {
cout << "BuyTransaction log execute!" << endl;
}
};
BuyTransaction bt;
output:
[root@centos74-dev Demo]# g++ main.cc -o main --std=c++11 && ./main
=============== effective c++ Demo ===============
Transaction log execute!
BuyTransaction log execute!
九、令operator= 返回一个reference to *this
主要是为了方便连锁赋值:a = b = c = 15,STL容器的赋值也是使用这个形式,其实不止=,例如+=、-=等和赋值相关的都可以作这样的标准。
Transaction& operator+(const Transaction& rhs) {
return *this;
}
十、在operator= 中处理“自我赋值”
内置类型的自我赋值一般没什么问题,如果你管理着一些资源,例如堆内存就需要注意。
-
使用if (this == &rhs) return *this; 判断自我赋值(主要是这点)
-
需要考虑new新的资源失败,是否会影响两个赋值对象的原本值?可以先用一个指针临时保持旧的地址。
-
资源的转移,推荐使用copy and swap技术。(说白了是用rhs的资源对象,拷贝构造一个新的资源对象,然后交换这个临时对象)
十一、复制对象时勿忘其每一个成分(重要)
每一个成分,指对象自己和它的base class。即我们自定义拷贝、赋值构造函数时,需要手动调用所有base class的适当copying函数。例如:
class BuyTransaction {
BuyTransaction(const BuyTransaction &rhs) : Transaction(rhs) { // 初始化基类
m_id = rhs.m_id;
}
BuyTransaction& operator= (const BuyTransaction &rhs) {
if (&rhs == this)
return *this;
Transaction::operator=(rhs); // 手动调用基类的operator=
m_id = rhs.m_id;
return *this;
}
};
copying函数需确保:
-
复制所有local变量
-
调用所有baseclasses内适当的copying函数
资源管理
十二、以对象管理资源
不只是堆内存,文件句柄、互斥锁、socket等都是需要不使用需要释放的资源。
-
使用工厂函数生成对象资源(暂定,需要细化实现接口)
-
资源申请后立即放入管理对象
-
依赖管理对象的析构函数释放资源,而不是使用delete操作符
-
需要注意析构函数执行过程中可能导致的异常,从而造成资源泄漏
-
std::auto_ptr不能多个管理一个资源对象,当auto_ptr发送赋值、拷贝时,源ptr会被置null
-
使用RCSP(引用计数只能指针),类似shared_ptr替代auto_ptr
-
注意std的智能指针在析构时调用的是delete而不是delete[],这在析构数组的时候需要注意,对于可变数组可以用vector替代
十三、在资源管理类中小心copying行为
其实这里就是注意。因为资源管理类的拷贝行为是和业务相关的,行为主要有:
-
禁止复制
-
对底层资源使用“引用计数法”,对于不需要delete的对象,可以使用shared_ptr的自定义删除器(deleter)
-
复制底层资源,即深拷贝
-
转移底部资源所有权,类似auto_ptr
别忘了复制管理对象的时候,必须一同处理其管理的资源,无论使用哪种行为。
十四、在资源管理类中提供对原始资源的访问
即使用get方法或隐性转换(可能导致错误,不推荐)方式,面向用于提供原始资源访问的接口。
十五、成对使用new和delete时采取相同形式
即new、delete和new []、delete[]应成对使用。
应当注意使用typedef时,最后注释标注new和delete的使用类型。
十六、以独立语句将newed对象置入智能指针
如要传递智能指针做函数参数时,推荐先在外部独立一条语句,new出对象绑定智能指针,而后再传入函数参数。这是因为一条语句内的调用顺序编译器可能会优化,因此不排除语句内new是否会应异常导致资源泄漏。
设计与声明
十七、让接口容易被正确使用,不易被误用
部分方法:
-
保持接口统一一致,例如统一用size表达容器个数
-
在创建新类型、限制类型上做一些约束
-
使用shared_ptr的定制器deleter,避免夸动态库调用的一系列问题,也可以用来解锁mutex(第十三点
十八、设计class犹如设计type
把class的设计当成是语言设计者最初设计内置类型一样,需要考虑多个方面,主要体现在以下几个问题;
-
新的类型如何被创建和析构
-
对象的初始化和赋值会有什么差别,毕竟这对应不同的函数调用
-
新类型的对象以值传递时,意味着什么?需要记住用copy构造函数定义类型的以值传递
-
什么是新类型的合法值,在成员变量、构造函数、赋值操作符和成员变量的set函数时,应该考虑其约束
-
新的类型需要配合某个继承体系吗?例如成员函数特别是析构函数的virtual属性
-
什么样的操作符、函数对新类型是合理的?(如何定义成员函数这种废话)
-
什么样的标准函数应该驳回?(啥函数放private)
-
谁该取用新type的成员:设计啥函数是public、protected、private,啥类型和函数是friends
-
什么是新类型的“未声明的接口”:它对效率、异常安全性以及资源运用(多任务锁定和动态内存)有什么保障
-
你的新类型有多么一般化:如果我的类型是一个类型家族,可以考虑做成类模板
-
你真的需要一个新类吗?如果只是定义一个新的derived class 派生类,可以考虑能否通过增加一个成员函数实现
十九、宁以pass-by-reference-to-const替换pass-by-value
即对自定义类型,用于函数参数时尽量使用(const T &t)的方式。
额外小tips:
-
const T &可以通过base类引用derived类对象,编译器底层貌似把引用转换为指针处理的
-
内置类型使用by-value比reference效率更高
注意:内置类型、STL迭代器和函数对象,使value传递更好。
二十、必须返回对象时,别妄想返回其reference
其实就是注意函数返回值为reference或指针指向local变量时,会发生找不到实体的异常,在必要的时候可以考虑以对象的形式返回,不必拘泥于pass-by-reference
二十一、将成员变量声明为private
-
将成员变量声明为private,通过函数接口进行访问,可以减少成员变量改变时对类外代码的影响。
-
protected并不比public更有封装性,因为会影响derived class的代码。
二十二、尽可能用non-member、non-friend替换member函数
尽量使用non-member、non-friend调用组合调用member的方式来实现业务逻辑。整节我能理解的就是,因为类不能跨文件,而命名空间可以,我们就可以针对不同功能模块编写对应功能函数,将这些函数归到相同的命名空间下,使用时,用户针对需要使用的子模块,include对应的头文件即可编译和指定子模块相关的功能函数,方便扩展,降低编译依赖性。按我感觉,就是减少非核心函数在类中的容量。本节并没让我感觉这样做的优势有多大,可能巨型工程代码才能体现优势吧。
至于封装性,能访问成员变量的函数越少,封装性就越高,可能基于此,non-member、non-friend函数的封装性才显得高一些吧。。
二十三、若所有参数皆需要类型转换,请为此采用non-member函数
这里主要指运算符重载时,例如:
class Rational {
public:
Rational (int numerator = 0, int denominator = 1); //non-explicit
int numerator () const;
int denominator () const;
const Rational operator* (const Rational & rhs) const;
};
Rational a(1, 8);
Rational b(1, 8);
Rational result = a * b; //ok
result = a * 2; //ok
result = 2 * b; // error
上例a * 2,a可以知道自己是Rational故调用operator* (const Rational & rhs),但2并不知道自己要转换到什么类型中,如发生编译错误。要解决该问题,需要使用non-member函数实现运算符重载:
const Rational operator* (const Rational & rhs, const Rational & lhs);
并且这个函数最好不用声明成Rational的friend吗,而是利用numerator、denominator等函数访问数据成员。
二十四、考虑写出一个不抛异常的swap函数
pimpl(pointer to implementation)手法:在类的内部使用一个指针,指向真实保存数据的impl对象,ceph代码中大量应用。
一个STL提供的swap算法如下:
namespace std {
template<typename T>
void swap (T& a, T&b)
{
T temp(a);
a = b;
b = temp;
}
}
以上方法,只要我们的自定义类支持拷贝和赋值操作,就能实现类型的swap。但在某些类内部数据有指针时,如我们自己的拷贝函数,会对指针指向的对象进行拷贝(设计原理上我们应该如此),而标准的swap函数将会带来额外的赋值开销,不符合swap高效的初衷。因此我们可以做swap的一个特化版本,专门为我们的类服务例如Weight,通常我们不能给std命名空间添加任何东西,但可以为标准templates(如swap)构造特化版本:
//首先,在Weight类中,定义一个public的swap函数,用于交换内部指针,然后在std命名空间特化swap,专属Weight服务
namespace std {
template<>
void swap<Weight> (Weight& a, Weight& b)
{
a.swap(b);
}
}
如上实现,其实就是STL各个容器中的swap实现原理。
如果默认的std::swap版本对你的class的效率是可接受的,我们不需要做任何事情,直接使用即可。但如类内部使用形如pimpl手法的内部类实现时,就需要考虑用一下方法高效的实现一个定制化的swap:
-
写一个public的swap成员函数,让他高效的swap两个对象,且保证不会抛出异常
-
在class或template所在的命名空间中提供一个non-member的swap,并调用上面的swap成员函数
-
如果我们写的是class而不是class template,为class特化std::swap,并调用上面的swap成员函数
最好,如果我们调用swap,需要包含一个声明:"using std::swap",以便让std::swap在函数内部曝光,而调用swap时不加任何修饰符,这样当编译器找不到你对std::swap的全特化后,可以找到默认的std swap实现
最最后,不要尝试在std namespace重载,这会为std增加新的东西,这是不合法的,重载可以实现在其他namespace中。
感觉,通常只要实现一个成员swap,即可,这样做可能是为了让自定义类型能够在stl容器中套用swap算法吧。。。唉麻烦
实现
二十五、尽可能延后变量定义式的出现时间
尽可能延后对象的定义,直到能给他赋予初值为止,因为通常使用default构造后再赋初值效率比较低下。
二十六、尽量减少做转型的动作(需补充新转型的知识)
C++新提供是4种转换方法:
-
const_cast<T>:用来将对象的常量性移除
-
dynamic_cast<T>:执行“向下安全性转型”,用来决定某对象是否归属继承体系中的某个类型。
-
reinterpret_cast<T>:执行低级转型,例如将一个pointer to int 转型为一个int
-
static_cast<T>:用来强迫隐式转换,(常用!!)可将non-const转为const,int转double,void*转typed指针,基类指针转到派生类指针,但无法做const转non-const
总结:
-
尽可能避免转型,使用其他设计规避转型,在注重效率的代码中避免使用dynamic_cast
-
转型必要时,尽量隐藏在函数后,用户调用函数就行
-
使用新的c++类型转换,而避免使用旧的,主要是新式容易被识别出来,或者grep
二十七、避免返回handles指向对象内部的成分
其实就是成员函数应该避免传递内部成员变量的引用、指针和迭代器出来,不仅会破坏封装性,更可能因为无法掌握声明周期而导致handles空悬,程序crash。
二十八、为“异常安全”而努力是值得的
程序函数应该考虑出现异常后,对数据、对类内部的影响情况,最好能保证异常抛出后,能回到函数调用前的状态,再不济你应该保证数据回到合法的状态;或者可使用不抛出异常的函数或代码。通常来说,一个函数中如果再调用一个不是异常安全的函数,那么整个函数也都不是异常安全的了。
针对异常抛出后,数据回到调用前的状态,可以使用copy-and-swap方式实现,但并不是所有函数的实现都能靠这样的方法。
总之,就是写代码时,考虑异常的退回机制咯
二十九、透彻了解inlining的里里外外
inline函数注意!inline函数无法随着程序库的升级而升级,因为inline函数是将函数体替换进程序代码中的,inline函数千万不能应用于接口!!否则客户进程也需要重新编译才生效。
inline函数内部,大部分调试器是无法设置断点的,在使用inline时必须考虑使用inline对日后调试的影响。
注意:
-
将inlining限制在小型、被频繁调用的函数身上。
-
不要只因为function templates出现在头文件,就将它们声明为inline,因为这样的话,它的具象化的函数,也都是inline,容易造成代码体积增大
-
类中定义的成员函数隐含inline,如果friend函数也在类内搞,那它也是inline的
三十、将文件间的编译依存关系降至最低
对外接口,尽量提供Impl,即指针或者内部impl的方式,否则修改实现类将使得用户端代码重新编译。当然也可以使用接口类的方式对外提供接口,在派生类中提供具体实现,也能起到分离编译的作用。
总结:
-
依赖声明而不是依赖定义式,方法就是上面说的两种
-
程序库的头文件应该尽量只有声明
三十一、确定你的public继承塑模出is-a关系
is-a的定义:Derived对象都是一个Base对象,反之则不成立。
每一个适用于Base对象的事情,也同样适用于derived对象。
三十二、避免遮盖继承而来的名称
如果你继承 base class并加上重载函数,而你又希望重新定义或覆写(推翻)其中一部分,那么你必须为那些原本会被遮掩的每个名称引入一个 using声明式,否则某些你希望继承的名称会被遮掩。
总结:
-
derived classes内的名称会遮掩 base classes内的名称。在 public继承下从来没有人希望如此。
-
为了让被遮掩的名称再见天日,可使用 using声明式或转交函数( forwarding functions)。
三十三、区分接口继承和实现继承
1,接口继承和实现继承不同。在 public继承之下, derived classes总是继承 base class 的接口
2,pure virtual函数只具体指定接口继承
3,简朴的(非纯) impure virtual函数具体指定接口继承及缺省实现继承
4,non- virtual函数具体指定接口继承以及强制性实现继承。
三十四、考虑virtual函数意外的其他选择
感觉根据需要使用即可,强记这些方法意义不大
-
non-virtual interface NVI方法:使用public非虚函数,包裹private的虚函数
-
strategy策略:使用函数指针成员变量传递要调用的方法
-
function 成员变量来包装一个可调用对象(如2中的某个函数)
-
将derived类中的虚函数,替换成另一个dervied类中的虚函数(感觉是两个类的对象进行耦合,需要从实际问题中看,例如每个角色的生命值类,生命值类派生出level1、level99两种类,计算生命值的虚函数方法在levelX中实现,传递levelX进角色类中)
三十五、绝不重新定义继承而来的虚函数
额,伦理问题,其实只需记住,需要重新定义的情况,请记得把base类的函数声明为virtual
三十六、绝不重新定义继承而来的缺省参数值
virtual函数是动态绑定(运行时决定的),而缺省参数却是静态绑定的。意味着,使用Base的指针调用重新定义缺省参数的Dervied类时,是Base类的缺省参数生效。解决办法,使用上面说的NVI方法,用public 非虚函数包裹private的虚函数,用public函数实现缺省参数重新定义。
本文深入探讨C++编程中的关键原则,包括构造/析构/赋值运算的优化,合理使用const,确保对象初始化,理解编译器自动生成的函数,资源管理策略,设计与声明的考量,以及实现细节。文章强调了避免异常逃逸析构函数,禁止自我赋值,复制对象时考虑所有成分,使用对象管理资源,以及在资源管理类中谨慎对待copying行为的重要性。
746

被折叠的 条评论
为什么被折叠?



