1、declaration是告诉编译器某个东西的名称和类型,而definition是编译器为此对象分配内存。对function而言,definition提供函数本体。
2、鼓励奖构造函数声明为explicit,因为他可以禁止编译器执行非预期的类型转换。
3、copy构造函数是实现“以同型对象初始化自我对象”,从“另一个同型对象中拷贝其值到自我对象”。
4、尽量以const、enum、inline替换掉#define,why?
解答:(1)#define myName 1.1415927;类似这样的一条定义,如果运用该常量并且发生编译错误,那么编译器也许会提到1.1415927而不是myName。所以,可能会给我们带来莫名其妙的错误信息提示。
解决之道: const double myName = 1.1415927;
(2)#define不仅不能用来定义class,也不能提供任何封闭性,除非在某处被定义为#undef。
(3)我们也可以使用enum { myName = 1.1415927 };来替换宏。而且enum有一个好处:就是不会让别人获得一个enum的地址。这是const无法企及的。
5、关键字const多才多艺,应尽可能使用它:
(1)
std::vector<int>myvector; const std::vector<int>::iterator myIter = myvector.bedin(); //如果执行 *myIter = 10;// 是正确的 myIter++ ;//错误的 std::vector<int>::const_iterator myIter = myvector.bedin(); *myIter = 10;// 是错误的 myIter++;//没问题的
(2)const成员函数。
两个函数如果其中一个const函数以及返回值是const,二另一个同名函数参数列表一致但是是non-const,这个时候这两个函数是可以实现重载的。(当然其实重载跟返回值没有太大关系)。遮会引发一个问题:当一个类中如果需要处理某一类动作,而这个动作(成员函数)需要分const和non-const对象考虑,那么首先如果分别定义两个这样的函数则会出现代码重复,解决的办法可以是:零non-const成员函数调用const成员函数即可,但是要记得在non-const内部调用const函数时要指出当前调用的对象如果是non-const首先应该将其转换成const(static_cast<const 函数名&>(*this)),之后再将该函数的结果返回值的const属性移植(const_cast)即可。切记,不要因为不小心犯了non-const函数调用自己导致内存泄露的情况发生。
6、确定对象被使用之前先被初始化。这是因为读取未初始化的值会导致不明确行为的发生。
(1)首先,我们要分清楚初始化和赋值。
C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前,即对成员的初始化应该是放在member initialization list 处进行初始化。
并且这个处理方法的效率会比在构造函数体内赋值要高得多。
(2)对于大多数时候来说,比起先调用default构造函数然后再调用copy assignment,单次的copy构造函数效率要高很多。
(3)C++有着十分固定的”成员初始化次序“,即:base class总是比derive class先被初始化,而class成员变量的初始化顺序总是以其声明次序被初始化。因此,如果在初始化的时候涉及两个成员变量初始化顺序具有次序关系,我们应该在初始化成员列表中注意这些关系。
(4)non-local static对象的初始化顺序:
首先我们来看什么是static对象:其寿命从被构造出来到程序结束为止,因此stack和heap-based对象都被排除。这种对象包括global 对象、定义域namespace作用于内的对象、在class内函数内以及在file内被声明为static的对象。他们的析构函数会在main()结束时自动被调用。
对于不同编译单元(产自单一目标文件的源码)内无法确定初始化顺序的情况下,可以这样设计non-local static对象初始化:将non-local static 对象转化为local static对象,即在函数内定义局部静态变量,然后再返回该对象的reference。详见effective C++条款4.
7 如果不想使用编译器自动生成的函数,就应该主动拒绝。
例如,若要防止对象拷贝或实现对象副本的保留,应该明确告诉编译器拒绝调用copy 构造函数和copy assignment函数。要令对象无法调用这些函数,首先比较直接的做法是在class内声明其为private,那么对象不能直接调用了。但是这样做其实不够安全,因为member函数和friend函数仍然可以调用,此时可以创造一个base class,当前class以private形式继承该base class,并且在base class中将copy 构造函数和copy assignment函数声明为private即可,这样的话derive class的member或friend也不能实现对象拷贝了。
8 我们要记得多态的基类应该声明virtual 析构函数。为什么呢?
首先,我们来理解下virtual的含义:声明一个函数为virtual,是为了实现在子类中的重新定义基类中同名的一个函数,从而实现多态。
如果忘记将基类中的析构函数定义为virtual,那么将会带来灾难:即在销毁对象释放内存时无法正常调用子类的析构函数,结果造成只有base的对象被摧毁,但是子类的对象并没有被摧毁,即“局部销毁”,容易导致内存泄露、败坏数据结构、在调试器上浪费很多时间等。
需要注意的是:任何class只要存在一个virtual函数,几乎都可以确定应该也有一个virtual的析构函数。
另一方面,如果一个class不含virtual函数,通常意味它并不准备用作一个base class,当一个不准备做base class却将其析构函数声明为virtual往往是个馊主意,why?
如果一个class中含有virtual函数,则对象会比实际的大一些,其携带一些信息用来记录在运行期该调用哪个virtual函数。这份信息通常由一个所谓的virtual table pointer简称vptr指针指出。vptr指向一个由函数指针构成的数组,成为vtbl,每一个带有virtual的class都有一个对应的vtbl。当对象调用某一个virtual函数,实际被调用的函数取决于该对象的vptr所指的哪个vtbl——编译器在其中寻找适当的函数指针。于是,这会导致对象体积变大,有可能导致无法移植。
总结:只有当class内至少含有一个virtual函数时才为他声明一个virtual的析构函数。
9 能否在构造函数或析构函数中调用virtual函数?
答案是绝不这么做。why?因为这会引来一些意想不到的后果。
首先,假设存在这样的一个base类,该类中定义一个virtual函数,并且在base类中的constructor 调用了该virtual。有多个derive继承base。那么有趣的事情来了:
当我们创建一个derive类的对象实例的时候,我们都知道base的constructor会更早被调用,即derive class对象内的base class成分会在derive class 自身成分被创建之前先构造妥当,而在构造base成分的过程中会调用virtual函数,这时候被调用的virtual函数是base class内的版本,不是derive内的版本,但是别忘了我们目前需要创建的是derive版本的呀!这或许不该是我们想要的结果把!或者我们可以这么说:在base 构造期间,virtual函数不是virtual函数。
相同道理也是用与析构函数。
那么,反过来思考下,我们为什么会想到要在构造函数中调用virtual函数呢?或许你的出发点是:为了确保每次有一个base继承体系上的对象被创建,就会有适当版本的对应函数(被定义为virtual的那个)被调用。那么,怎么做才是正确的呢?
这里有一个方案可以解决!
一种做法是将base中上面被我们声明为virtual的那个函数改为non-virtual,然后要求derive构造函数传递必要信息给base构造函数,于是便安全了,大概例子如下:
class base
{
public:
explicit base(const std::string & mybaseInfo);
void myNonVirtual(const std::string & mybaseInfo)const;
......
}
base::base(const std::string & mybaseInfo)
{
myNonVirtual( mybaseInfo );
};
class derive:public base
{
public :
derive(parameters):base(createMyNonVirtual (parameters) ){.....}
......
private:
static std::string createMyNonVirtual (parameters) ;
};
10、在operator =中处理“自我赋值”
自我赋值中需要注意的是:当实现自我赋值后,对该对象作出删除,需要先做判断赋值的目的端和源端是否是同一个对象,因为在
calss Biemap{......}
class Widget
{
private:
Bitmap *pb;
}
widget::operator=(const Widget& rhs)
{
delete pb;
pb=new Bitmap(*rhs.pb);
return *this;
} //不安全的版本需要加一个证同测试。即在释放pb所指的内存空间时应该先判断 if(this==&rhs){return *this;}
//即如果是自我赋值,那么就不做任何事情。
11、复制对象时勿忘记其每一个成分(主要是针对声明自己的copy constructor而言)
当你自己定义一个copy constructor、copy assignment时,在这些构造函数中会有对应的变量赋值;而问题就出现在后期你又自己在该类中增加一个数据成员的时候,则如果没有在对应的copy constructor、copy assignment函数中增加对该成员的拷贝处理,那么就会出现局部拷贝的现象出现。更郁闷的是,这时候局部拷贝的话编译器不会发出任何抱怨,
或许可以认为编译器在调皮:“你自己写的东西,如果你的代码不完全,我为什么要告诉你?”结论很明显:如果在class中增加一个数据成员,我们就必须同时修改copy constructor、copy assignment函数。如果忘记了,编译器也不会主动提醒你。
更严重的则是,在上述情况下,一旦发生继承,辣么,稍不小心就会引发一场潜在的危机!
定义一个base class,在该class内自定义copy 构造函数和copy assignment函数,然后有一个derive class继承base class,我们可能很容易就忘记要在derive class中的copy 构造函数和copy assignment函数中指明对base class成分进行赋值动作,否则就会造成在derive class中没法构造base class的成分,当然编译器也不会提醒你。所以,我们必须要确保(1)复制所有local成员变量(2)调用所有base class内的适当的copy 函数。
12、以对象管理资源
(1)获得资源后立即放进管理对象内。
(2)管理对象运用析构函数确保资源被释放。不论控制流如何离开区块,一旦对象被摧毁(例如对象离开作用域)其析构函数自然会被自动调用,于是资源被释放。
需要注意的是:auto_ptr被摧毁时会自动删除它所指的内存,所以一定要注意别让多个auto_ptr窒息那个同一对象,否则,对象会被删除多次,那么程序就会走上“未定义明确行为”的列车了!
13、成对使用new和delete时要采取相同形式
首先先来看下一个小小例子:
std::string *strArray = new std::string[100];
....
delete strArray;
上面的代码看起来有什么错误的吗?当然有错啦,不然我贴它干嘛?错就错在:new 和delete没有配对使用。strArray 含有100个string对象,当你使用delete strArray 的那一刻只能删除其中一个,剩下的99个对象并不能被适当删除,因为他们的析构函数根本没有被调用。正确的方式应该是delete strArray[];
我们需要注意的是,当使用new,那么有两件事会发生:(1)内存被分配(operator new);(2)针对此内存会有一个或更多构造函数被调用。当我们使用delete的时候,相应的也会发生两件事情。
当你使用delete来删除一个指针时,你必须告诉编译器内存中是否存在一个“数组大小记录”。
delete[]则会确定指针指向的是一个数组,而单纯的delete则是确认指向单一对象。
14、让接口容易被正确使用,不易被误用