C++ 引用c 类成员初始化化列表后输出为什么无法读取内存

C++对象模型——成员初始化列表(第二章)
成员初始化列表 (Member Initialization List)
当编写一个类的构造函数时,有可能设定类成员的初始值,或者通过成员初始化列表初始化,或者在构造函数内初始化,除了四种情况,其实任何选择都差不多。
本节中,首先澄清何时使用初始化列表才有意义,然后解释初始化列表内部的真正操作是什么,然后再看一些微妙的陷阱。
下列情况中,为了让程序能够被顺利编译,必须使用成员初始化列表(不能在构造函数内初始化):
当初始化一个引用的成员时
当初始化一个常量成员( const member)时
当调用一个基类的构造函数,而它拥有一组参数时
当调用一个成员类对象的构造函数,而它拥有一组参数时
在这四种情况中,程序可以被正确编译并执行,但是效率不佳。例如:class Word {
// 没有错误,但是太naive
_name = 0;
在这里,Word构造函数会先产生一个临时性的String对象,然后将它初始化,再以一个赋值运算符将临时对象指定给_name,然后再销毁临时对象。以下是构造函数可能的内部扩张结果:// C++伪代码
Word::Word( /* This pointer goes here */) {
// 调用String的default constructor
_name.String::String();
// 产生临时对象
String temp = String(0);
// "memberwise"地拷贝_name
_name.String::operator=(temp);
// 销毁临时对象
temp.String::~String();
对程序代码反复审查并修正,得到一个明显更有效率的实现方法:// 较佳的方式
Word::Word : _name(0) {
它会被扩张成这样子:
// C++伪代码
Word::Word( /* This pointer goes here */ ) {
// 调用String(int) constructor
_name.String::String(0);
顺便一提,陷阱最可能发生在这种形式的template code中:template &class type&
foo&type&::foo(type t) {
// 可能是也可能不是个好主意
// 视type的真正类型而定
这会引导某些程序员十分积极进取地坚持所有的成员初始化操作必须在成员初始化列表中完成,甚至即使是一个行为良好的成员如_cnt:// 坚持此种代码风格
Word::Word()
: _cnt(0), _name(0)
成员初始化列表中到底发生了什么事情?许多人对list的语法感到迷惑,误以为它是一组函数调用,当然不是!
编译器会一一操作初始化列表,以适当地次序在构造函数内插入初始化操作,并且在任何显式用户代码之前。例如,先前的Word构造函数被扩张为:// C++伪代码
Word::Word( /* this pointer goes here */ ) {
_name.String::String(0);
它看起来很像是在构造函数中指定_cnt的值,事实上,有一些微妙的地方要注意:列表中的项目次序是由类中成员的声明次序决定的,而不是初始化列表中的排列顺序决定的。在本例的Word类中,_name被声明在_cnt之前,所以它的初始化比_cnt早。
"初始化顺序"和"初始化列表中的项目排列顺序"之间的差异,会导致下面意想不到的危险:class X {
X (int val)
: j(val), i(j)
上述程序代码看起来好像要把j设初值为val,然后把i设初值为j。但是由于声明顺序的原因,初始化列表中的i(j)比j(val)更早执行。而j开始并没有初始值,所以i(j)的执行结果导致i会被初始化为一个无法预测的值。
这个bug的困难度在于它很不容易被观察出来,编译器应该发出一个警告消息。但是目前只有一个编译器(g++)做到这一点。(测试VS2010确实没有给出警告消息,Lippman确实牛掰,2000年提的问题,VS10都没解决),X x(0);然后输出x的i,j的值,结果如下图所示:
i的值不正常,j值正常.VS10中并未给出任何警告信息.
还有一个有趣的问题,初始化列表中的项目插入到构造函数中,会继续保存声明次序吗?也就是说,已知:// 一个有趣的问题
X::x(int val) : j(val) {
j的初始化顺序会插入在显式用户赋值操作(i=j)之前还是之后呢?
如果声明次序继续被保存,则这段代码会出现很大问题(因为先要将i初始化,再将j初始化).事实上,这段代码是正确的,因为初始化列表的项目被插入在显式用户代码之前.
另一个常见的问题是,是否能够如下所示,调用一个成员函数设置成员的初始值:// X::xfoo()被调用
X::X(int val) : i(xfoo(val)), j(val)
其中xfoo()是X的一个成员函数,答案是yes.但是最好使用"存在于构造函数提内的一个成员",而不要使用"存在于成员初始化列表中的成员",来为另一个成员设定初始值.并不确定xfoo()对X object的依赖性有多高,如果把xfoo()放在构造函数体内,那么对于"到底哪一个member在xfoo()执行时被设置初始值"这件事,就可以给出确定的答案.
成员函数的使用是合法的,因为和此object相关的this指针已经被建构妥当,而构造函数大约被扩张为:// constructor扩张后的结果
X::X( /* this pointer, */ int val) {
i = this-&xfoo(val);
如果一个派生类成员函数被调用,其返回值被当作基类构造函数的一个参数,将会如何?// 调用FooBar::fval()可以吗?
class FooBar : public X {
int fval() { return _ }
// derived class member function
FooBar(int val) : _fval(val), X(fval())
// fval()作为base class constructor的参数
下面是它可能的扩张结果:// C++伪代码
FooBar::FooBar( /* this pointer goes here */ )
X::X(this, this-&fval());
它的确不是一个好主意.总结
编译器对初始化列表一一处理并可能重新排序,以反映出成员的声明次序,它会插入一些代码到构造函数体内,并且插入在任何显式用户代码之前.
没有更多推荐了,
加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!c++中冒号(:)、双冒号(::)、初始化列表详解
最近学习了C++中一些特殊符号的使用规则,查阅了一些资料和博客,对初始化列表、(:),(::)的用法进行了梳理,如有理解不周的地方欢迎大家指正
初始化列表
初始化列表其实就是类成员初始化列表(Member
Initialization List)的简单说法。
1) 基本概念,初始化列表的形式
&span style="font-family:KaiTi_GB2312;"&#include &iostream&
class MemberInitializationList
MemberInitializationList(int val) : j(val), i(j)
// j(val), i(j)就是所谓的成员初始化列表
inline void printInfo()
cout && "i = " && i && ", j = " && j &&
int main(void)
MemberInitializationList MIL(10);
MIL.printInfo();
运行结果:
&span style="font-family:KaiTi_GB2312;"&
j = 10&/span&
j如愿以偿被初始化为10,但是i的值为什么是一个奇怪的数字,而不是意想中的10呢?
答案是有些细微的地方需要注意:成员初始化列表的初始化顺序是由类中的成员声明次序决定的,而不是由initialization list中的排列次序决定的。在本例中,先初始化i然后再初始化j。initialization list中的i(j),表明将j的值赋给i,而此时j还没有被初始化,其值不确定,所以i的值也就不能确定,这就是运行结果中为什么i的值比较奇怪的原因了。
在任何explicit user code之前,编译器会一一操作initialization list,以适当次序在构造函数内安插初始化操作。
需要说明的是,据说除了g++编译器会对这种情况给予warning外,其它的编译器都不会给出相关的警告信息。
如果把本例中的构造函数改成:
&span style="font-family:KaiTi_GB2312;"&MemberInitializationList(int val) : i(val), j(i)
再运行的结果就正确了:
i = 10 j = 10
2) 为什么要使用member initialization list?
根据Stanley Lippman的Inside C++ Object Model,采用member initialization list的方法的效率比较高,即
MemberInitializationList(int val) : i(val), j(i)
的效率要比
MemberInitializationList(int val)
高。理由是后者会产生临时性的变量,然后要调用赋值运算符赋给真正的变量,再然后摧毁那个临时性的变量。是否真的是这样呢?我们来做一个试验证明之。验证程序如下:
#include &iostream&
#include &time.h&
class MemberInitializationList
MemberInitializationList(int val) : i(val), j(i)
MemberInitializationList(int val)
inline void printInfo()
cout && "i = " && i && ", j = " && j &&
int main(void)
//cout && CLOCKS_PER_SEC &&
clock_t start = clock();
for(int i = 0; i & 3000000; i++)
MemberInitializationList* pMIL = new MemberInitializationList(i);
delete pMIL;
clock_t finish = clock();
cout && finish - start && " ms elapsed." &&
结论:在VC6和VC2005的编译器上,两种方式似乎没有什么区别。或许在别的编译器上有所区别。也就是说,要么微软的编译器对于两种构造函数都使用了临时变量,或者都没有使用临时变量。
3) 调用一个成员函数设定成员变量的初值
#include &iostream&
#include &time.h&
class MemberInitializationList
MemberInitializationList(int val) : i(setI(val)), j(i)
// 用成员函数设定成员变量的初始值也是可以的
inline int setI(int i)
inline void printInfo()
cout && "i = " && i && ", j = " && j &&
int main(void)
MemberInitializationList MIL(10);
MIL.printInfo();
每个成员在成员初始化表中只能出现一次初始化的顺序不是由名字在初始化表中的顺序决定而是由成员在类中被声明的顺序决定的,但是在初始化表中出现或者在被隐式初始化的成员类对象中的成员总是在构造函数体内成员的赋值之前被初始化,初始化列表,跟在{}里面的初始化没有什么不同,但在非静态const类型以及引用型成员变量必须在初始化列表里面初始化在使用C++编程的过程当中,常常需要对类成员进行初始化,常用的2种方法:
第一种方法:
CMYClass::CSomeClass() { x=0; y=1; }
第二种方法:
CSomeClass::CSomeClass() : x(0), y(1) { }
下面探讨这两种方法的异同以及如何使用这两种方法。
从技术上说,第二种方法比较好,但是在大多数情况下,两者实际上没有什么区别。第二种语法被称为成员初始化列表,之所以要使用这种语法有两个原因:一个原因是必须这么做,另一个原因是出于效率考虑。
让我们先看一下第一个原因——必要性。设想你有一个类成员,它本身是一个类或者结构,而且只有一个带一个参数的构造函数。
&span style="font-family:KaiTi_GB2312;"&class CMember { public: CMember(int x) { ... } };&/span&
因为CMember有一个显式声明的构造函数,编译器不产生一个缺省构造函数(不带参数),所以没有一个整数就无法创建CMember的一个实例。
&span style="font-family:KaiTi_GB2312;"&CMember* pm = new CM // 出错!! CMember* pm = new CMember(2); // OK&/span&
如果CMember是另一个类的成员,你怎样初始化它呢?答案是你必须使用成员初始化列表。
&span style="font-family:KaiTi_GB2312;"&class CMyClass { CMember m_ public: CMyClass(); }; // 必须使用初始化列表来初始化成员 m_member CMyClass::CMyClass() : m_member(2) { ooo }&/span&
没有其它办法将参数传递给m_member,如果成员是一个常量对象或者引用也是一样。根据C++的规则,常量对象和引用不能被赋值,它们只能被初始化。
使用初始化列表的第二个原因是出于效率考虑,当成员类具有一个缺省的构造函数和一个赋值操作符时。MFC的CString提供了一个完美的例子。假定你有一个类CMyClass具有一个CString类型的成员m_str,你想把它初始化为"Hi,how are you."。你有两种选择:
&span style="font-family:KaiTi_GB2312;"&CMyClass::CMyClass() { // 使用赋值操作符 // CString::operator=(LPCTSTR); m_str = _T("Hi,how are you."); }
// 使用初始化列表 // 和构造函数 CString::CString(LPCTSTR) CMyClass::CMyClass() : m_str(_T("Hi,how are you.")) { }&/span&
在它们之间有什么不同吗?是的。编译器总是确保所有成员对象在构造函数体执行之前被初始化,因此在第一个例子中编译的代码将调用CString::Cstring来初始化m_str,这在控制到达赋值语句前完成。在第二个例子中编译器产生一个对CString::
CString(LPCTSTR)的调用并将"Hi,how are you."传递给这个函数。结果是在第一个例子中调用了两个CString函数(构造函数和赋值操作符),而在第二个例子中只调用了一个函数。
在CString的例子里这是无所谓的,因为缺省构造函数是内联的,CString只是在需要时为字符串分配内存(即,当你实际赋值时)。但是,一般而言,重复的函数调用是浪费资源的,尤其是当构造函数和赋值操作符分配内存的时候。在一些大的类里面,你可能拥有一个构造函数和一个赋值操作符都要调用同一个负责分配大量内存空间的Init函数。在这种情况下,你必须使用初始化列表,以避免不要的分配两次内存。
在内建类型如ints或者longs或者其它没有构造函数的类型下,在初始化列表和在构造函数体内赋值这两种方法没有性能上的差别。不管用那一种方法,都只会有一次赋值发生。有些程序员说你应该总是用初始化列表以保持良好习惯,但我从没有发现根据需要在这两种方法之间转换有什么困难。在编程风格上,我倾向于在主体中使用赋值,因为有更多的空间用来格式化和添加注释,你可以写出这样的语句:
memset(this,0,sizeof(this));
注意第二个片断绝对是非面向对象的。
当我考虑初始化列表的问题时,有一个奇怪的特性我应该警告你,它是关于C++初始化类成员的,它们是按照声明的顺序初始化的,而不是按照出现在初始化列表中的顺序。
&span style="font-size:12"&class CMyClass { CMyClass(int x, int y); int m_x; int m_y; }; CMyClass::CMyClass(int i) : m_y(i), m_x(m_y) { }&/span&
你可能以为上面的代码将会首先做m_y=i,然后做m_x=m_y,最后它们有相同的值。但是编译器先初始化m_x,然后是m_y,,因为它们是按这样的顺序声明的。结果是m_x将有一个不可预测的值。这个例子是故意这样设计来说明这一点的,然而这种bug会很自然地出现。有两种方法避免它,一个是总是按照你希望它们被初始化的顺序来声明成员,第二个是,如果你决定使用初始化列表,总是按照它们声明的顺序罗列这些成员。这将有助于消除混淆。冒号(:)用法
(1)表示机构内位域的定义(即该变量占几个bit空间)&span style="font-family:KaiTi_GB2312;"&typedef struct _XXX{
unsigned char a:4;
} ; XXX&/span&(2)构造函数后面的冒号起分割作用,是类给成员变量赋值的方法,初始化列表,更适用于成员变量的常量const型。&span style="font-family:KaiTi_GB2312;"&struct _XXX{
_XXX() : y(0xc0) {}
};&/span&(3) public:和private:后面的冒号,表示后面定义的所有成员都是公有或私有的,直到下一个"public:”或"private:”出现为止。"private:"为默认处理。(4)类名冒号后面的是用来定义类的继承。&span style="font-family:KaiTi_GB2312;"&class 派生类名 : 继承方式 基类名
派生类的成员
};&/span&继承方式:public、private和protected,默认处理是public。下面重点讲一下构造函数后的(:)
构造函数后加冒号是初始化表达式:有四种情况下应该使用初始化表达式来初始化成员:1:初始化const成员2:初始化引用成员3:当调用基类的构造函数,而它拥有一组参数时
4:当调用成员类的构造函数,而它拥有一组参数时。
在程序中定义变量并初始化的机制中,有两种形式,一个是我们传统的初始化的形式,即赋值运算符赋值,还有一种是括号赋值,如:
 &span style="font-family:KaiTi_GB2312;"& int a=10;
char b='r';//赋值运算符赋值
int a(10);/
char b('r');//括号赋值 &/span&
以上定义并初始化的形式是正确的,可以通过编译,但括号赋值只能在变量定义并初始化中,不能用在变量定义后再赋值,
冒号初始化是给数据成员分配内存空间时就进行初始化,就是说分配一个数据成员只要冒号后有此数据成员的赋值表达式(此表达式必须是括号赋值表达式),那么分配了内存空间后在进入函数体之前给数据成员赋值,就是说初始化这个数据成员此时函数体还未执行。 对于在函数中初始化,是在所有的数据成员被分配内存空间后才进行的。这样是有好处的,有的数据成员需要在构造函数调入之后函数体执行之前就进行初始化如引用数据成员,常量数据成员和对象数据成员
&span style="font-family:KaiTi_GB2312;color:#330033;"&class student
  {public :
    student ()
     .
     .
     .
    protected:
    int &b;
  student ::student (int i,int j)
    a=i;
    b=j;
  } &/span&在Student类中有两个数据成员,一个是常量数据成员,一个是引用数据成员,并且在构造函数中初始化了这两个数据成员,但是这并不能通过编译,因为常量初始化时必须赋值,它的值是不能再改变的,与常量一样引用初始化也需要赋值,定义了引用后,它就和引用的目标维系在了一起,也是不能再被赋值的。所以C ++":"后初始化的机制,使引用和常量数据成员变为可能的,Student类的构造函数应为:
student ::student(int i,int j):a(i),b(j){}常见的三种情况
1、对含有对象成员的对象进行初始化,例如,
类line有两个私有对象成员startpoint、endpoint,line的构造函数写成:
line(int sx,int sy,int ex,int ey):startpoint(sx,sy),endpoint(ex,ey){……}
初始化时按照类定义中对象成员的顺序分别调用各自对象的构造函数,再执行自己的构造函数
2、对于不含对象成员的对象,初始化时也可以套用上面的格式,例如,
类rectangle有两个数据成员length、width,其构造函数写成:
rectangle():length(1),width(2){}
rectangle(int x,int y):length(x),width(y){}
3、对父类进行初始化,例如,
CDlgCalcDlg的父类是MFC类CDialog,其构造函数写为:
CDlgCalcDlg(CWnd* pParent ): CDialog(CDlgCalcDlg::IDD, pParent)
其中IDD是一个枚举元素,标志对话框模板的ID
使用初始化成员列表对对象进行初始化,有时是必须的,有时是出于提高效率的考虑
双冒号(::)用法
1)表示“域操作符”例:声明了一个类A,类A里声明了一个成员函数void f(),但没有在类的声明里给出f的定义,那么在类外定义f时, 就要写成void A::f(),表示这个f()函数是类A的成员函数。2)直接用在全局函数前,表示是全局函数 例:在VC里,你可以在调用API 函数里,在API函数名前加::3)表示引用成员函数及变量,作用域成员运算符例:System::Math::Sqrt() 相当于System.Math.Sqrt()VC中如下
::是C++里的“作用域分解运算符”。比如声明了一个类A,类A里声明了一个成员函数voidf(),但没有在类的声明里给出f的定义,那么在类外定义f时,就要写成voidA::f(),表示这个f()函数是类A的成员函数。  :: 一般还有一种用法,就是直接用在全局函数前,表示是全局函数。当类的成员函数跟类外的一个全局函数同名时,考试,大提示在类内定义的时候,打此函数名默认调用的是本身的成员函数;如果要调用同名的全局函数时,就必须打上::以示区别。比如在VC里,你可以在调用API函数时,在API函数名前加::
没有更多推荐了,
加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!18:07 提问
C++11 vector使用初始化列表会造成内存泄漏
std::vector&std::vector&std::string&& itemText =
//初始化列表会造成内存泄漏
上面的代码会造成内存泄漏,Win7+VS2013
换了下面的代码就没问题了
std::vector&std::string& vRow1 = { "1" };
std::vector&std::string& vRow2 = { "2" };
std::vector&std::string& vRow3 = { "3" };
std::vector&std::string& vRow4 = { "4" };
std::vector&std::string& vRow5 = { "5" };
std::vector&std::string& vRow6 = { "6" };
std::vector&std::string& vRow7 = { "7" };
std::vector&std::string& vRow8 = { "8" };
std::vector&std::string& vRow9 = { "9" };
std::vector&std::string& vRow10 = { "0" };
TreeItemsText itemText = { vRow1, vRow2, vRow3, vRow4, vRow5, vRow6, vRow7, vRow8, vRow9, vRow10 };
这是为什么?
按赞数排序
你是怎么判断有内存泄漏的?用的工具么?一些工具判断不准确,它们会把在堆上没有发生回收的数据都当成“泄漏”。
怎么会内存泄露呢! 感觉这不太可信
这应该是编译器bug 你用vs2015试试
准确详细的回答,更有利于被提问者采纳,从而获得C币。复制、灌水、广告等回答会被删除,是时候展现真正的技术了!
其他相关推荐effecttive c++ 笔记 - 简书
effecttive c++ 笔记
字数 10643
1. 让自己习惯C++
条款01:视C++为一个语言联邦
为了更好的理解C++,我们将C++分解为四个主要次语言:
C。说到底C++仍是以C为基础。区块,语句,预处理器,内置数据类型,数组,指针统统来自C。
Object-Oreinted C++。这一部分是面向对象设计之古典守则在C++上的最直接实施。类,封装,继承,多态,virtual函数等等...
Template C++。这是C++泛型编程部分。
STL。STL是个template程序库。容器(containers),迭代器(iterators),算法(algorithms)以及函数对象(function objects)...
请记住:这四个次语言,当你从某个次语言切换到另一个,导致高效编程守则要求你改变策略。C++高效编程守则视状况而变化,取决于你使用C++的哪一部分。
条款02:尽量以const,enum,inline替换#define
这条可理解为**让编译器代替预处理器做更多的事情。
预处理过程扫描源代码,对其进行初步的转换,产生新的源代码提供给编译器。检查包含预处理指令的语句和宏定义,并对源代码进行相应的转换。预处理过程还会删除程序中的注释和多余的空白字符。可见预处理过程先于编译器对源代码进行处理。预处理指令是以#号开头的代码行。
例:#define ASPECT_RATIO 1.653
记号名称ASPECT_RATIO也许从未被编译器看见,也许在编译器开始处理源代码之前它就被预处理器移走了。即编译源代码时ASPECT_RATIO已被1.653取代。ASPECT_RATIO可能并未进入记号表(symbol table)。
替换:const double AspectRatio = 1.653;
好处应该有:多了类型检查,因为#define 只是单纯的替换,而这种替换在目标码中可能出现多份1.653;改用常量绝不会出现相同情况。
请记住:对于单纯常量,最好以const对象或enums替换#defines;对于形似函数的宏,最好改用inline函数替换#defines。
条款03:尽可能使用const
const的多才多艺:
在classes外部修饰global或namespace作用域中的常量,或修饰文件、函数、或被声明为static的对象。
修饰classes内部的static和non-static成员变量。
可以指出指针自身所指事物或两者都(或都不)是const。
在一个函数声明式内,const可以修饰函数返回值、各参数、函数自身(如果是成员函数)
char greeting[] = "Hello";
//指针p及所指的字符串都可改变;
const char *p =
//指针p本身可以改变,如p = &Anyother;p所指的字符串不可改变;
char * cosnt p =
//指针p不可改变,所指对象可改变;
const char * const p =
//指针p及所致对象都不可改变;
请记住:如果关键字const出现在星号左边,表示被指物事常量。const char *p和char const *p两种写法意义一样,都说明所致对象为常量;如果关键字const出现在星号右边,表示指针自身是常量。
将const实施于成员函数,是为了1)判断哪个函数可以改动对象内容而哪个函数不行,2)使操作const对象称为可能。
常量对象不能调用非常量成员函数,只能调用常量成员函数;常量成员函数不能改变对象的成员变量,除非该变量声明为mutable。
将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体;
编译器强制实施bitwise constness,但你编写程序时应该使用“概念上的车辆”(conceptual constness);
当cosnt和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。
条款04:确定对象被使用前已先被初始化
永远在使用对象之前先将它初始化。对于无任何成员的内置类型,你必须手工完成此事。至于内置类型以外的任何其它东西,初始化责任落在构造函数身上,确保每一个构造函数都将对象的每一个成员初始化。
赋值和初始化:
C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前。所以应将成员变量的初始化置于构造函数的初始化列表中。
ABEntry::ABEntry(const std::string& name, const std::string& address,
const std::list&PhoneNumber&& phones)
//这些都是赋值,而非初始化
theAddress =
//这些成员变量在进入函数体之前已调用默认构造函数,接着又调用赋值函数,
thePhones =
//即要经过两次的函数调用。
numTimesConsulted = 0;
ABEntry::ABEntry(const std::string& name, const std::string& address,
const std::list&PhoneNumber&& phones) :
theName(name),
//这些才是初始化
theAddress(address),
//这些成员变量只用相应的值进行拷贝构造函数,所以通常效率更高。
thePhones(phones),
numTimesConsulted(0)
所以,对于非内置类型变量的初始化应在初始化列表中完成,以提高效率。而对于内置类型对象,其初始化和赋值的成本相同,但为了一致性最好也通过成员初始化表来初始化。如果成员变量时const或reference,它们就一定需要初值,不能被赋值。
C++有着十分固定的“成员初始化次序”。基类总是在派生类之前被初始化,而类的成员变量总是以其说明次序被初始化。所以:当在成员初始化列表中列各成员时,最好总是以其声明次序为次序。
为内置对象进行手工初始化,因为C++不保证初始化它们;
构造函数最好使用成员初始化列表,而不要在构造函数本体内使用赋值操作。初始化列表列出的成员变量,其排列次序应该和它们在类中的声明次序相同;
为免除“跨编译单元之初始化次序”问题,请以local static对象替换non-local static对象。
2. 构造、析构、赋值运算
条款05:了解C++默默编写并调用哪些函数
如果你在类声明时没有声明拷贝构造函数、拷贝赋值操作符、析构函数、构造函数,编译器会自动为你提供一份声明。惟有当这些函数被需要(被调用),它们才会被编译器创建出来。
默认构造函数和析构函数主要是给编译器一个地方用来放置“藏身幕后”的代码,像是调用基类和非静态成员变量的构造函数和析构函数;至于拷贝构造函数和拷贝赋值操作符,编译器创建的版本只是单纯地将来源对象的每一个非静态成员变量拷贝到目标对象。
编译器可以暗自为类创建默认构造函数、拷贝构造函数、拷贝赋值操作符,以及析构函数。
如一个类声明了一个构造函数(无论有没参数),编译器就不再为它创建默认构造函数。
编译器生成的拷贝赋值操作符:对于成员变量中有指针,引用,常量类型,我们都应考虑建立自己“合适”的拷贝赋值操作符。因为指向同块内存的指针是个潜在危险,引用不可改变,常量不可改变。
条款06:若不想使用编译器自动生成的函数,就该明确拒绝
通常如果你不希望类支持某一特定技能,只要不说明对应函数就是了。但这个策略对拷贝构造函数和拷贝赋值操作符却不起作用。因为编译器会“自作多情”的声明它们,并在需要的时候调用它们。
由于编译器产生的函数都是public类型,因此可以将拷贝构造函数或拷贝赋值操作符声明为private,来阻止人们在外部调用它;但是类中的成员函数和友元函数还是可以调用private函数,解决方法可能是在一个专门为了阻止拷贝动作而设计的基类。(Boost提供的那个类名为noncopyable)。
条款07:为多态基类声明virtual析构函数
当基类的指针指向派生类的对象的时候,当我们使用完,对其调用delete的时候,其结果将是未有定义——基类成分通常会被销毁,而派生类的充分可能还留在堆里。这可是形成资源泄漏、败坏之数据结构、在调试器上消费许多时间。
消除以上问题的做法很简单:给基类一个virtual析构函数。此后删除派生类对象就会如你想要的那般。
任何类只要带有virtual函数都几乎确定应该也有一个virtual析构函数。如果一个类不含virtual函数,通常表示它并不意图被用做一个基类,当类不企图被当做基类的时候,令其析构函数为virtual往往是个馊主意。因为实现virtual函数,需要额外的开销(指向虚函数表的指针vptr)。
条款08:别让异常逃离析构函数
C++并不禁止析构函数吐出异常,但它不鼓励你这样做。C++不喜欢析构函数吐出异常。
如果可能导致异常:
如果抛出异常,就结束程序。(强迫结束程序是个合理选项,毕竟它可以阻止异常从析构函数传播出去。)
捕获异常,但什么也不做。
如果某个操作可能在失败时抛出异常,而又存在某种需要必须处理该异常,那么这个异常必须来自析构函数以外的某个函数。
析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么类应该提供一个普通函数(而非在析构函数中)执行该操作。
条款09:决不在构造和析构过程中调用virtual函数
你不该在构造函数和析构函数中调用virtual函数,因为这样的调用不会带来你预想的结果。
基类的构造函数的执行要早于派生类的构造函数,当基类的构造函数执行时,派生类的成员变量尚未初始化。派生类的成员变量没初始化,即为指向虚函数表的指针vptr没被初始化又怎么去调用派生类的virtual函数呢?析构函数也相同,派生类先于基类被析构,又如何去找派生类相应的虚函数?
唯一好的做法是:确定你的构造函数和析构函数都没有调用虚函数,而它们调用的所有函数也不应该调用虚函数。
解决的方法可能是:既然你无法使用虚函数从基类向下调用,那么我们可以使派生类将必要的构造信息向上传递至基类构造函数。即在派生类的构造函数的成员初始化列表中显示调用相应基类构造函数,并传入所需传递信息。
条款10:令operator= 返回一个reference to *this
对于赋值操作符,我们常常要达到这种类似效果,即连续赋值:
x = y = z = 15;
为了实现“连锁赋值”,赋值操作符必须返回一个“引用”指向操作符的左侧实参。
Widget & operator = (const Widget &rhs)
所有内置类型和标准程序库提供的类型如string,vector,complex或即将提供的类型共同遵守。
条款11:在operator =中处理“自我赋值”
看下面的例子:
Widget& Widget::operator=(const Widget& rhs)
//这里对pb指向内存对象进行delete,试想 *this == rhs?情况会如何
pb = new Bitmap(*rhs.pb);
//如果*this == rhs,那么这里还能new吗?“大事不妙”。
也许以下代码能解决以上问题:
Widget& Widget::operator=(const Widget& rhs)
if (this == &rhs)
//解决了自我赋值的问题。
pb = new Bitmap(*rhs.pb);
“许多时候一群精心安排的语句就可以导出异常安全(以及自我赋值安全)的代码”,以上代码同样存在异常安全问题。如果new Bitmap导致异常,Widget最终会指向一块被删除的Bitmap。
Widget& Widget::operator=(const Widget& rhs)
Bitmap *pOrig =
//记住原先的pb
pb = new Bitmap(*rhs.pb);
//令pb指向*pb的一个复本
//删除原先的pb
//这样既解决了自我赋值,又解决了异常安全问题。自我赋值,将pb所指对象换了个存储地址。
条款12:复制对象时勿忘其每一个成员
如果你在类中添加一个成员变量,你必须同时修改相应的copying函数(所有的构造函数,拷贝构造函数以及拷贝赋值操作符)。在派生类的构造函数,拷贝构造函数和拷贝赋值操作符中应当显示调用基类相对应的函数,否则编译器可能又“自作聪明了”。
当你编写一个copying函数,请确保:
复制所有local成员变量;
调用所有基类内的适当copying函数。
但是,我们不该令拷贝赋值操作符调用拷贝构造函数,也不该令拷贝构造函数调用拷贝赋值操作符。想想,一个是拷贝(建立对象),一个是赋值(对象已经存在)
3. 资源管理
所谓资源就是,一旦用了它,将来必须还给系统。C++程序中最常使用的资源就好似动态分配内存(如果你new了,却忘了delete,会导致内存泄露),但内存只是你必须管理的众多资源之一。其它常见的有文件描述符(file descriptors)、互斥器(mutex)、图形界面中的字形和画刷。数据库连接以及网络sockets。当你不使用它们时,记得还给系统。
条款13:以对象管理资源
把资源放进对象内,我们便可依赖C++的“析构函数自动调用机制”确保资源被释放。
许多资源被动态分配于堆内而后被用于单一区块或函数内。它们应该在控制流离开那个区块或函数时被释放。标准程序库提供的auto_ptr正是针对这种形势而设计的特制产品。auto_ptr是个“类指针对象”,也就是所谓的“智能指针”,其析构函数自动对其所指对象调用delete。
std::auto_ptr&Investment& pInv(createInvestment());
//函数退出,auto_ptr调用析构函数自动调用delete,删除pInv;无需显示调用delete。
“以对象管理资源”的两个关键想法:
获得资源后立刻放进管理对象内(如auto_ptr)。每一笔资源都在获得的同时立刻被放进管理对象中。“资源取得时机便是初始化时机”(Resource Acquisition Is Initialization;RAII)。
管理对象运用析构函数确保资源被释放。即一旦对象被销毁,其析构函数被自动调用来释放资源。
由于auto_ptr被销毁时会自动删除它所指之物,所以不能让多个auto_ptr同时指向同一对象。所以auto_ptr若通过copying函数复制它们,它们会变成NULL,而复制所得的指针将取得资源的唯一拥有权!
std::auto_ptr&Investment& pInv1(createInvestment()); //pInv1指向createInvestment()返回物;
std::auto_ptr&Investment& pInv2(pInv1);
//现在pInv2指向对象,而pInv1被设为NULL;
pInv1 = pInv2;
//现在pInv1指向对象,而pIn2被设为NULL;
受auto_ptr管理的资源必须绝对没有一个以上的auto_ptr同时指向它。即“有你没我,有我没你”。auto_ptr的替代方案是“引用计数型智能指针”(reference-counting smart pointer;SCSP)、它可以持续跟踪共有多少对象指向某笔资源,并在无人指向它时自动删除该资源。
TR1的tr1::shared_ptr就是一个"引用计数型智能指针"。
std::tr1::shared_ptr&Investment&
pInv1(createInvestment()); //pInv1指向createInvestment()返回物;
std::tr1::shared_ptr&Investment&
pInv2(pInv1);
//pInv1,pInv2指向同一个对象;
pInv1 = pInv2;
//同上,无变化
//函数退出,pInv1,pInv2被销毁,它们所指的对象也竟被自动释放。
auto_ptr和tr1::shared_ptr都在其析构函数内做delete而不是delete[],也就意味着在动态分配而得的数组身上使用auto_ptr或tr1::shared_ptr是个潜在危险,资源得不到释放。也许boost::scoped_array和boost::shared_array能提供帮助。还有,vector和string几乎总是可以取代动态分配而得的数组。
为防止资源泄漏,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源。
两个常被使用的RAII类分别是auto_ptr和tr1::shared_ptr。后者通常是较佳选择,因为其拷贝行为比较直观。若选择auto_ptr,复制动作会使他(被复制物)指向NULL。
条款14:在资源管理类中小心拷贝行为
我们在条款13中讨论的资源表现在堆上申请的资源,而有些资源并不适合被auto_ptr和tr1::shared_ptr所管理。可能我们需要建立自己的资源管理类。
例:假设我们使用Mutex类型的互斥器对象,共有lock和unlock两函数可用
void lock(Mutex *pm);
//锁定pm所指的互斥量
unlock(Mutex *pm);
//将pm解除锁定
我们建立的资源管理类可能会是这样:
class Lock
explicit Lock(Mutex *pm)
: mutexPtr(pm)
lock(mutexPtr);
unlock(mutexPtr);
Mutex *mutexP
“当一个RAII对象被复制,会发生什么事?”大多数时候你会选择一下两种可能:
禁止复制。如果复制动作对RAII类并不合理,你便应该禁止之。禁止复制类的copying函数参见条款6。
对底层资源使用”引用计数法“。有时候我们又希望保有资源,直到它的最后一个使用者被销毁。这种情况下复制RAII对象时,应该将资源的”被引用计数“递增。tr1::shared_ptr便是如此。通常只要内含一个tr1::shared_ptr成员变量,RAII类便可实现”引用计数“行为。
条款15:在资源管理类中提供对原始资源的访问
前几个条款提到的资源管理类很棒。它们是你对抗资源泄漏的堡垒。但这个世界并不完美,许多APIs直接指涉资源,这时候我们需要直接访问原始资源。这时候需要一个函数可将RAII对象(如tr1::shared_ptr)转换为其所内含之原始资源。有两种做法可以达成目标:显示转换和隐式转换。
tr1::shared_ptr和auto_ptr都提供一个get成员函数,用来执行显示转换,也就是返回智能指针内部的原始指针(的复件)。就像所有智能指针一样, tr1::shared_ptr和auto_ptr也重载了指针取值操作符(operator-&和operator),它们允许隐式转换至底部原始指针。(即在对智能指针对象实施-&和操作时,实际被转换为被封装的资源的指针。)
class Font
FontHandle get() const
//FontHandle 是资源;
显示转换函数
operator FontHandle() const
//隐式转换
这个值得注意,可能引起“非故意之类型转换”
是否该提供一个显示转换函数(例如get成员函数)将RAII类转换为其底部资源,或是应该提供隐式转换,答案主要取决于RAII类被设计执行的特定工作,以及它被使用的情况。显示转换可能是比较受欢迎的路子,但是需要不停的get,get;而隐式转换又可能引起“非故意之类型转换”。
条款16:成对使用new和delete时要采取相同形式
当我们使用new,有两件事情发生:第一,内存被分配出来;第二,针对此内存会有一个(或更多)构造函数被调用。当你使用delete,也有两件事发生:针对此内存会有一个(或多个)析构函数被调用,然后内存才被释放。delete的最大问题在于:即将被删除的内存之内究竟有多少对象?这个问题的答案决定了有多少个析构函数必须被调用起来。
解决以上问题事实上很简单:如果你调用new时使用[],你必须在对应调用delete时也使用[]。如果你调用new时没有使用[],那么也不该在对应调用delete时使用[]。
条款17:以独立语句将newed对象置入智能指针
为了避免资源泄漏的危险,最好在单独语句内以智能指针存储newed所得对象。
int priority();
void processWidget(std::tr1::shared_ptr&Widget& pw, int priority);
std::tr1::shared_ptr&Widget& pw(new Widget);
//即在传入函数之前对智能指针初始化,而不是在传入参数中
//对其初始化,因为那样可能引起操作序列的问题。
processWidget(pw, priority());
请记住:以独立语句将newed对象存储于(置入)智能指针内。如果不这样做,一旦异常抛出,有可能导致难以察觉的资源泄漏。
4. 设计与声明
所谓软件设计,是“令软件做出你希望它做的事情”的步骤和做法,通常以颇为一般性的构想开始,最终变成十足的细节,以允许特殊接口的开发。
条款18:让接口容易被正确使用,不易被误用
好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质。
“促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容。
“阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
tr1::shared_ptr支持定制删除器。这可防范DLL问题,可被用来自动解除互斥量等等。
条款19:设计class犹如设计type
设计一个良好的类,或者称作类型,考虑一下设计规范:
新类型的对象应该如何被创建和销毁?
对象的初始化和对象的赋值该有什么样的差别?
新类型的对象如果被passed by value(值传递),意味着什么?
什么是新类型的“合法值”?
你的新类型需要配合某个继承图系吗?
你的新类型需要什么样的转换?
什么样的操作符和函数对此新类型而言是合理的?
什么样的标准函数应该驳回?
谁该取用新类型的成员?
什么是新类型的“未声明接口”?
你的新类型有多么一般化?
你真的需要一个新类型吗?
条款20:宁以pass-by-reference-to-const替代psss-by-value
缺省情况下C++以by value方式传递对象至函数。除非你另外指定,否则函数参数都是以实际实参的副本为初值,而调用端所获得的亦是返回值的一个副本。这些副本由对象的拷贝构造函数产生。所以在以对象为by value时,可能会调用相应的构造函数(成员对象的构造、基类对象的构造),然后调用对应的析构函数。所以以by value的形式开销还是比较大的。
如果我们用pass-by-reference-to-const,例如:bool validateStudent(const Student& s);,这种传递方式效率高得多:没有任何构造函数或析构函数被调用,因为没有任何新对象被创建。以传引用方式传递参数也可以避免对象切割问题:即当一个派生类对象以传值的方式传递并被视为一个基类对象,基类对象的拷贝构造函数会被调用,而“造成此对象的行为像个派生类对象”的那些特化性质全被切割掉了,仅仅留下了基类对象。这一般不是你想要的。
所以我们一般的做法应该是这样:内置对象和STL的迭代器和函数对象,我们一般以传值的方式传递,而其它的任何东西都以传引用的方式传递。
条款21:必须返回对象时,别妄想返回其reference
不要返回一个指向局部变量的指针或引用,因为局部变量在函数返回后就没有存在的意义。
不要返回一个在堆上对象的引用,这会埋下了资源泄漏的危险。谁该对这对象实施delete呢?别把这种对资源的管理寄托完全寄托于用户。
不要返回一个被定义于函数内部的静态对象的引用。
“必须返回新对象”的函数的正确写法是:就让那个函数返回一个新对象。
条款22:将成员变量声明为private
将成员变量隐藏在函数接口的背后,可以为“所有可能的实现”提供弹性。例如,这可使得成员变量被读或写时轻松通知其它对象、可以验证calss的约束条件以及函数的前提和事后状态、可以在多线程环境中执行同步控制......
不封装意味不可改变!成员变量的封装性与“成员变量的内容改变时所坏量的代码数量”成反比。
条款23:宁以non-member、non-friend替换member函数
宁可拿non-member non-friend函数替代member函数。这样做可以增加封装性、包裹弹性和机能扩充性。
一般我们相当然以为类中的成员函数更具封装性,而实际上并不是那么一回事,因为成员函数不仅可以访问private成员变量,也可以取用private函数、enums、typedefs等等。而非成员非友元函数能实现更大的封装性,因为它只能访问public函数。
将所有便利函数放在多个头文件内但隶属同一个命名空间,意味客户可以轻松扩展这一组便利函数。需要做的就是添加更多non-member non-friend函数到此命名空间内。
条款24:若所有参数皆需类型转换,请为此采用non-member函数
通常,令类支持隐式类型转换通常是个糟糕的主意。当然这条规则有其例外,最常见的例外是在建立数值类型时。
const Rational operator*(const Rational& rhs)
//如果定义一个有理数类,并实现*操作符为成员函数,如上所示;那么考虑一下调用:
Rational oneHalf(1, 2);
result = oneHalf * 2; // 正确,2被隐式转换为Rational(2,1)
//编译器眼中应该是这样:const Rational temp(2); result = oneHalf *
result = 2 * oneH // 错误,2,可不被认为是Rational对象;因此无法调用operator*
可见,这样并不准确,因为乘法应该满足交换律,不是吗?所以,支持混合式算术运算的可行之道应该是:让operator*成为一个non-member函数,允许编译器在每一个实参上执行隐式类型转换:
class Rational
... // contains no operator*
const Rational operator*(const Rational& lhs,
Rational& rhs)
return Rational(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
Rational oneFourth(1, 4);
result = oneFourth * 2;
result = 2 * oneF
//这下两个都工作的很好,通过隐式转换实现
成员函数的方面是非成员函数,而不是友元函数。可以用类中的public接口实现的函数,最好就是非成员函数,而不是采用友元函数。
请记住:如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member。
条款25:考虑写出一个不抛异常的swap函数
当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。
如果你提供一个member swap,也该提供一个non-member swap用来调用前者。对于class(而非templates),也请特化std::swap。
调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何“命名空间资格修饰”。
为“用户定义类型”进行std templates全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西。
大多数情况下,适当提出你的类定义及函数声明,是花费最多心力的两件事。尽管如此,还是有很多东西需要小心:太快定义变量可能造成效率上的拖延;过度使用转型(casts)可能导致代码变慢又难维护,又招来微妙难解的错误;返回对象“内部数据之号码牌(handls)”可能会破坏封装并留给客户虚吊号码牌;为考虑异常带来的冲击则可能导致资源泄漏和数据败坏;过度使用inline function可能引起代码膨胀;过度耦合则可能导致让人不满意的冗长build times。
条款26:尽可能延后变量定义式的出现时间
只要你定义了一个变量而其类型带有一个构造函数或析构函数,那么当程序的控制流到达这个变量定义式时,你便得承受构造成本;当这个变量离开其作用域时,你便得承受析构成本。即使这个变量最终并未被使用,仍需耗费这些成本,所以应该尽量避免这种情形。因此,最好延后该变量的定义式,直到非得使用该变量的前一刻为止,甚至应该尝试延后这份定义直到能够给它初值实参为止。
std::string encryptPassword(const std::string& password)
if (password.length() & MinimumPasswordLength)
throw logic_error("Password is too short");
//注意:可能抛出异常
如上代码,encrypted在2处定义是个不错的选择,因为如果抛出异常,那么encrypted的构造和析构可是做了无用功啊!
条款27:尽量少做转型动作
C++规则的设计目标之一是,保证“类型错误”绝不可能发生。不幸的是,转型(casts)破坏了类型系统。那可能导致任何种类的麻烦,有些容易辨识,有些非常隐晦。
两种旧式转型:
(T)expression
//C风格的转型动作
T(expression)
//函数风格的转型动作
C++还提供四种新式转型:
const_cast:通常被用来将对象的常量性转除;即去掉const。
dynamic_cast:主要用来执行“安全向下转型”,也就是用来决定某对象是否归属继承体系中的某个类型。
reinterpret_cast:意图执行低级转型,实际动作可能取决于编译器,这也就表示它不可移植。
static_cast:用来强迫隐式转换,例如将non-const转型为const,int转型为double等等。
尽量使用新式转型:它们很容易在代码中被辨识出来,因而得以简化“找出类型系统在哪个地点被破坏”的过程;各转型动作的目标愈窄化,编译器愈可能诊断出错误的运用。
如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_casts。如果有个设计需要转型动作,试着发展无需转型的替代设计。
如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需将转型放进他们自己的代码内。
宁可使用C++-style(新式)转型,不要使用旧式转型。前者很容易辨识出来,而且也比较有着分门别类的执掌。
条款28:避免返回handls指向对象内部成分
struct RectData
class Rectangle
Point& upperLeft() const { return pData-& }//1
const只对函数内进行保护,函数返回后呢??
Point& lowerRight() const { return pData-& } //2
const只对函数内进行保护,函数返回后呢??
std::tr1::shared_ptr&RectData& pD
1,2两函数都返回引用,指向private内部数据,调用者于是可通过这些引用更改内部数据!这严重破坏了数据的封装性,对私有成员进行直接操作?太不可思议了!
const Point& upperLeft() const { return pData-& } //3
const Point& lowerRight() const { return pData-& } //4
或者将1,2改为3,4,这就限制了客户的“涂改权”,只有“读取权”。但终究“返回一个handle代表对象内部成分”总是危险的。特别是将返回的指针或引用赋值给其它指针或引用,那么久造成了“悬空”。
请记住:避免返回handles(包括reference、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助const成员函数的行为像个const,并将发生“虚吊号码牌”(dangling handles)的可能性降至最低。
条款29:为“异常安全”而努力是值得的
异常安全函数(Exception-safe functions)即使发生异常也不会泄漏资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。
“强烈保证”往往能够以copy-and-swap实现出来,但“强烈保证”并非对所有函数都可实现或具备现实意义。
函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者。
条款30:透彻了解inlining的里里外外
Inline函数看起来像函数,动作像函数,比宏好得多,可以调用它们又不需蒙受函数调用所招致的额外开销。你实际获得的比想象的还多,编译器有能力对执行语境相关最优化。然而编写程序就像现实生活一样,没有白吃的午餐。inline函数也不例外,这样做可能增加你的目标码。
如果inline函数的本体很小,编译器针对“函数本体”所产生的码可能比针对“函数调用”所产出的码更小。果真如此,将函数inlining确实可能导致较小的目标码和较高的指令高速缓存装置击中率。
Inline函数通常一定被置于头文件内,因为大多数建置环境在编译过程中进行inlining,而为了将一个“函数调用”替换为“被调用函数的本体”,编译器必须知道那个函数长什么样子。Template通常也被置于头文件内,因为它一旦被使用,编译器为了将它具现化,需要知道哦啊它长什么样子。Template的具现化与inlining无关。如果你正在写一个template而你认为所有根据此template具现出来的函数都应该inlined,请将此template声明为inline;但如果你写的template煤油理由要求它所具现的每一个函数都是inlined,就应该避免将这个template声明为inline。
一个表面上看似inline的函数是否真实inline,主要取决于编译器。有时候虽然编译器有意愿inlining某个函数,还是可能为该函数生成一个函数本体(函数指针,构造函数,析构函数)。对程序开发而言,将上述所有考虑牢记在新很是重要,但若从纯粹实用观点出发,有一个事实比其它因素更重要:大部分调试器面对inline函数都束手无策。
这使我们在决定哪些函数该被声明为inline而哪些函数不该时,掌握一个合乎逻辑的策略。一开始先不要将任何函数声明为inline,或至少将inlining施行范围局限在那些“一定成为inline”或“十分平淡无奇”的函数身上。
条款31:将文件间的编译依存关系降至最低
6 继承与面对对象设计
条款32:确定你的public继承塑模出is-a关系
以C++进行面向对象编程,最重要的一个规则是:public inheritance(公有继承)意味is-a(是一种)的关系。如果你令class D以public形式继承class B,你便是告诉C++编译器(以及你的代码读者)说,每一个类型为D的对象同时也是一个类型为B的对象,反之不成立。B对象可派上用场的任何地方,D对象一样可以派上用场。
在C++领域中,任何函数如果期望获得一个类型为基类的实参(而不管是传指针或是引用),都也愿意接受一个派生类对象(而不管是传指针或是引用)。(只对public继承才成立。)
条款33:避免遮掩继承而来的名称
C++的名称遮掩规则所做的唯一事情就是:遮掩名称。至于名称的类型是否相同并不重要。即只要名称相同就覆盖基类相应的成员,类型和参数个数都无关紧要。
派生类的作用域嵌套在基类的作用域内。C++的继承关系的遮掩名称也并不管成员函数是纯虚函数,非纯虚函数或非虚函数等。只和名称有关。 如果你真的需要用到基类的被名称遮掩的函数,可以使用using声明式或转交函数(forwarding function),引入基类的成员函数。若是基类中存在函数重载的情况,在派生类中应注意同名函数覆盖的问题!
条款34:区分接口继承和实现继承
表面上直截了当的public继承概念,经过更严密的检查之后,发现它由两部分组成:函数接口继承和函数实现继承。
成员函数的接口总是会被继承。
声明一个纯虚函数的目的是为了让派生类只继承函数接口。
声明一个虚函数的目的是让派生类继承该函数的接口和缺省实现。
声明一个非虚函数的目的是为了令派生类继承函数的接口及一份强制性实现。
由于不同类型的声明意味着本质并不相同的事情,当你声明你的函数成员时,必须谨慎选择。如果将所以函数都声明为非虚函数,这将使得派生类没有余裕空间进行特化工作,而基类的非虚析构函数尤其会带来问题,实际上任何被当作基类来使用的类都会拥有若干虚函数。将所有成员函数声明为虚函数也是一种常见的错误,当然有时也是正确的,如果你的不变性凌驾于特异性,别害怕说出来。
条款35:考虑virtual函数以外的其它选择
条款36:绝不重新定义继承而来的non-virtual函数
假设基类B中有一个非虚函数mf,派生类D在继承实现时定义了自己的mf版本,将会导致基类的mf被隐藏。如果定义一个基类指针指向一个派生类对象并调用mf函数时,其结果将会调用基类版本的mf函数,而不是派生类版本的mf函数!这与虚函数的动态绑定并不一样,非虚函数是静态绑定的。
条款37:绝不重新定义继承而来的缺省参数值
对于non-virtual函数,上一条款说到,“绝不重新定义继承而来的non-virtual函数”,而对于继承一个带有缺省参数值的virtual函数,也是如此。即绝不重新定义继承而来的缺省参数值。因为:virtual函数是动态绑定,而缺省参数值却是静态绑定。
一个指向派生类的基类指针,如果两者的一个虚函数指定了不同的缺省参数值 ,那么,利用基类指针调用派生类虚函数值时,其缺省参数值将是基类指定的!这是因为缺省参数值却是静态绑定,而该指针是一个基类对象。
条款38:通过复合塑模出has-a或“根据某物实现出”
复合(composition)的意义和public继承(is-a)完全不同。当你的对象是你所塑造的世界中的某些事物,复合意味着has-a的关系,如人拥有名字、地址、电话等对象;当你的对象纯粹是实现细节上的人工制品(如链表、集合、互斥器等),复合则是表现的“由某某实现出”的关系,比如由链表实现出集合。
条款39、40:明智而审慎地使用私有继承和多重继承
私有继承意味着“由某物实现出”的关系,但是实现中,尽可能使用复合而不是私有继承来实现这一关系。
愤怒的面包,落草的火子
前言 把《C++ Primer》读薄系列笔记全集。 目录 第I部分:C++基础 开始学习C++ 变量和基本类型 字符串、向量和数组 表达式 语句 函数 类 第II部分:C++标准库 IO库 顺序容器 范型算法 关联容器 动态内存 第III部分:类设计者的工具 拷贝控制 重载...
C++文件 例:从文件income. in中读入收入直到文件结束,并将收入和税金输出到文件tax. out。 检查文件是否成功打开 常量 C++中的const变量能在任何常数可以出现的地方使用,例如数组的大小、case标号中的表达式。 bool data type C++新...
PART0、前言 TOPIC运用c++进行高效编程 收获了解c++如何行为为什么那样行为如何运用其行为形成优势 PART1、导读 定义式的任务 对象:为此对象拨发内存的地点 函数:提供代码本体 类:列出他们的成员 c++没有接口的关键字,定义接口就是定义一个全是纯虚函数的类...
一个博客,这个博客记录了他读这本书的笔记,总结得不错。《深度探索C++对象模型》笔记汇总 1. C++对象模型与内存布局 参考资料 C++对象模型 C++对象模型之简述C++对象的内存布局 在C++中有两种类的数据成员:static和nonstatic,以及三种类的成员函数...
1、利用构造函数限制对象的创建 在上一节构造函数的例1中,我们提到,如果不声明默认构造函数book(),而只声明带参构造函数book(char *a, double p)的时候,语句book A是无法创建对象的。无法创建的原因在上一节已经讲明,这节就不再赘述了。 ...
&你放下他了把?& &哈哈哈放下了!& &我还没说他是谁呢…&
清晨五点的机场显得有些空旷,一切都像刚苏醒!而就在刚刚,大熊与兔子结束了它们相爱相杀的虐恋 他们两人,被我们这些朋友一致评为最奇葩的情侣 大熊是一个神经及其大条的人,永远无法读懂女人心里在想什么 兔子性...
顾筱宁站在陈小易学校的门口,给他打电话。她昨天想了一晚上,她觉得自己真的有些无理取闹了。她想来和他赔礼道歉,想到这,顾筱宁想狠狠的抽自己。从小到大,她都没有向谁低过头,但是面对陈小易她没有办法捍卫她的高傲,在他面前,她永远是个需要被人疼爱的小女孩。 “小易哥哥,你在干嘛?我...
在一个月后,终于从医院获得了证实怀孕的报告,也从这时起,我开始了职场孕妇生活。至今怀孕已5周了,孕吐现象逐渐显现,虽然胃口还是很好,但是从早到晚胃里一直有种排山倒海的感觉,甚是不爽。也从这时候起,当年选工作时脑子进的水统统变成了现在的汗水。
1)离家远。当所有人...
因为专注,所以专业 所有任课教师均为10年以上教龄,6年以上广州初三毕业班代班经验。 我们只做初中课辅。 分层达标,精品小班 制定学科目标及达标计划,每周上课每天跟进确保效果,梯度目标达标奖励,给孩子一个看得着的明确方向。 精品小班8-10人 强化提升班2-6人 个性细分,...

我要回帖

更多关于 c 引用成员 初始化 的文章

 

随机推荐