看到了Effective c++的最后一章,最开始的那章---内存管理还没搞清楚,准备那章搞清楚完也写篇博客,不管怎样,有好的开始就应该让它有个完美的结束,杂项讨论这章是作者将那些分到哪章都不合适的就索性放到了最后讨论,我看完后从中摘出自己认为重要的坐下笔记,如果能帮得到大家,那就更荣幸了哈!
1.当我们定义一个类时,编译器会自动给我产生哪些成员函数?
解析:我们都知道,当我们定义类时,如果我们没有定义某些成员函数的话,编译器会总会给我们自动合成,这就是编译器默默为我们完成和调用函数,这些函数主要有以下几个
构造函数、析构函数、拷贝构造函数、赋值运算符、(non-const)取地址运算符和(const)取地址运算符这六个函数,其代码形式如下
如果我们定义一个空类,如下所示
//空类 class Empty{ };
其实这个Empty类相当于如下:
//空类 class Empty{ //编译器会在我们用到这些函数时,如果类中没有定义,会调用自己生产的 Empty(); ~Empty(); Empty(const Empty &rhs); Empty& operator==(const Empty &rhs); Empty* operator&(); const Empty* operator&() const; };
下面就以个例子来说明这些编译器默默为我们产生的函数什么时候会正常工作什么时候会失效
下面是个NameObejct的模板类
template <typename T> class NameObject{ public: NameObject(const char *name , const T &value); NameObject(cosnt string &name , cosnt T &value); private: string nameValue; T objectValue; }; //当我们自己定义的类中有了编译器为我们生成的函数,那么它便不会给我们生产 //NameObject类有了构造函数,编译器不会再给我们生成 NameObject<int> no_1("Smallest Prime Number" , 2); //类中没有拷贝构造函数,所以调用编译器为我们产生的拷贝构造函数 NameObject<int> no_2(no_1);
由于我们的模板类中没有定义拷贝构造函数,所以调用编译器为我们产生的拷贝构造函数以no_1.nameValue 和 no_1.objectValue为初值来讲no_2对象的成员进行初始化,因为nameValue成员是string类型,而string类型有自己的拷贝构造函数,所以调用string的拷贝构造函数进行复制,而objectValue是int类型的,而int类型是内置类型,所以只能bitwise一位一位的进行复制。
上面大致讲述了,编译器为我们产生的拷贝构造函数是怎么工作的,下面就谈谈赋值运算符是怎么工作的,
我们稍微改下上面的模板类,来演示一下,赋值运算符不能工作的情境。
template <typename T> class NameObject{ public: NameObject(string name , const T &value); private: string &nameValue; const T objectValue; }; string newDog("xiaoa"); string oldDog("xiaob"); NameObject<int> p(newDog , 2); NameObject<int> s(oldDog , 29); p = s;
当指向p=s时,由于定义的模板类中没有定义赋值运算符,所以采用编译器为我们产生的赋值运算符函数
当执行赋值运算符时,p的数据成员的nameValue的引用应该绑定s.nameValue绑定的string对象,但是我们都知道定义引用时应该给予绑定的初始对象并且不能再更改,同时类中的另一个数据成员是const,不能被写入,所以编译器所产生的赋值运算符函数将会拒绝执行,如果想完成,必须自己定义赋值运算符函数。
对于上述这种情况,我们应该显示的拒绝不能执行的函数,在前面的条款中提到这点,当类中不可以执行编译器为我们默默生成的函数时,应该显式的拒绝它,怎么显式的拒绝呢?其实看了前面条款的同学都知道,只要将这个函数private即可,如下所示:
template <typename T> class NameObject{ public: NameObject(string &name , const T &value); private: NameObject& operator=(const NameObject &rhs); string &nameValue; const T objectValue; };
当然如果我们想实现上面的赋值运算符的功能,必然要自己写代码实现,这样当类中定义了赋值运算符后,编译器也不会给我们产生默认的了。
2.宁愿编译链接时出错,也不要再执行时出错!
答:自我感觉这点没什么好讲的,因为在编译链接时出错我们根据编译器提示的错误查找,一般情况下都可以搞定,然而,在执行期出错的原因就很多,很难断定,所以Effective c++就提出了,尽量让错误都在编译期显现出来,也就是改变类的设计方法,添加新的类型来检查,这仅仅是提供了思路,书上也给出了例子,但是感觉在实际的开发中很难去做,所以在此只是稍微提下记录,有兴趣的可以详细看书哈!
3.在使用非局部静态对象之前要先确定它已被初始化。
答:什么是非局部静态对象(non-local-static-object)呢?
定义于global 或者 namespace scope
在某个class 类中声明为static
在某个文件范围内定义为static
还是举个例子
class Test{ }; //非局部静态对象 Test test;
另外就要谈初值的问题了,我们在写程序的时候,往往会犯的错误,定义了变量没有初始化就来用,这是大忌,在前面的条款中也提到过,尽量延迟变量的定义时间,最好在该变量初始化的时候去定义它。
非局部静态对象的问题在于,如果这个非局部对象的初始化不依赖于其他对象的初始化,那么没什么问题,或者说如果是单一的编译单元的话也没什么问题,但是如果是不同的编译单元,这个编译单元中对象的初始化依赖另一个编译单元的初始化时那么此时就会产生问题,因为c++是支持分离的编译单元(不同编译单元的代码生成目标文件,然后链接起来形成可执行文件),因为c++的这种特性就会导致这种初始化动作时发生的问题。
如下例所示
class FileSystem{ }; FileSystem theFileSystem;//非局部静态对象 class Directory{ public: Directory(); }; Directory::Directory() { //该构造函数中依赖theFileSystem的初始化 } Directory tempDir;
如上面的情况,初始化顺序的问题就显现出来了,Directory构造函数初始化tempDir时依赖于theFileSystem的初始化,我们利用调用函数来解决,返回静态对象的引用,这样就一定保证已经初始化了。
class FileSystem{ }; FileSystem& theFileSystem() { static FileSystem tfs;//定义并且初始化静态对象 return tfs;//传回的引用指向tfs } class Directory{ public: Directory(); }; Directory::Directory() { //该构造函数中依赖theFileSystem的初始化 } Directory& tempDir { static Directory td; return td; };
这样一来,对象的初始化顺序就得到了合理的安排,这样就会避免上面的情况。