类这个概念无非是数据和方法的集合,为什么我一直困惑呢?为什么不弄清楚呢?
这四个函数有它独特的地方,总让人摸不着头脑。这次看《C++ 沉思录》之后有心进行一次整理,这里就分享给大家。
有些类非常简单,完全无需构造函数,所以并非是所有的类都需要构造函数。但是有些复杂的类,他们需要构造函数来隐藏它们内部的工作方式,这个时候就需要创建一个了。
/* 一个例子,说明为什么不使用一个绑定来代替getLength()函数 const引用确实能够实现目的,但是必须在每个构造函数的初始化表里增加一句 略显麻烦 */ class Stick { public: const int &length; // 一个const的引用,能够读取但无法修改 Stick(): true_length(1), length(true_length) {} Stick(Stick &st): true_length(st.true_length), length(true_length) {} Stick(int a): true_length(a), length(true_length) {} void setLength(int b) { true_length = b; } // int getLength() { return true_length; } private: int true_length; };
很多时候,当我写了一个我认为需要的构造函数之后,总会添加一个无参的构造函数,但很少去想为什么我需要这么个无参的构造函数,它给我带来了什么功能呢?事实上一个无参的构造函数,能够让你像这样定义类Stick a; 如果Stick没有无参的构造函数,那么这样的定义就是错的,同样的,当你需要一个Stick的数组的时候,如果没有无参构造函数也是不能成功的,定义数组的时候比较多,比如 Stick a[1024]; 但是很少去想这里居然还有无参构造函数的什么事。
面向对象的设计其实是用类来包装了状态,虽然这种设计也有它自身的缺点,但是对象确实是通过数据成员来反映状态的。是否所有的数据成员都必须有一个初始状态呢?这就不一定了,所以我的看法是,当你在使用之前做了初始化就OK,或者即使不初始化也没有问题就OK。
可能问这个问题比问构造函数更傻,但实际上析构函数是跟类的数据成员息息相关的,不是所有的类都需要析构函数的。只有当你的类申请了资源,并且这些资源不会通过成员函数自动释放,这个时候就需要一个析构函数来擦屁股。
class B { public: int s; }; class D: public B { public: int t; ~D() { printf("~D\n"); } }; int _tmain(int argc, _TCHAR* argv[]) { B *bp = new D; delete bp; return 0; }
这里我定义了一个D的析构函数并进行打印,只是为了显示一下D的析构函数是否被调用了,事实是没有,也就是说如果父类不定义析构函数,那么一个指向子类的父类指针在删除时是不会去调用无论父类还是子类的析构函数的,同样,如果父类的析构函数不是虚函数,那么也不会去调用子类的析构函数,而是直接调用父类的析构函数。所以,为了完整析构函数调用链,让子类的析构函数能够被调用,父类的析构函数只得定义成虚的。但是不是所有父类的析构函数都必须是虚函数呢?这个也不见得。我们公司在测试之间流传着几句话:如果看到父类的析构函数没有定义成虚函数,那么就会得意的笑着说又一个bug;如果你的类里有一个虚函数,那么析构函数也必须是虚的等等。
在《Effective C++》第三版第七条里面,有这么句话“任何class只要带有virtual函数都几乎确定应该也有一个virtual析构函数”,我想上面的口号估计就来自这里。但是如果我的类虽然是父类,但是不会像上面那样使用一个父类的指针去释放子类,也就是不会有多态的特性,那么这个要求父类有虚析构函数的做法就显得不通情理了。在《C++ 沉思录》里面也说了句话“使所有的类都自动包含虚析构函数会亵渎C++‘只为用到的东西付出代价’的哲学”。
最后总结,只有当你想使用多态特性时父类才要求定义一个虚析构函数。关于析构函数的调用顺序这里不谈,其实很简单,自己google吧。
7、什么时候需要一个复制构造函数?
在解释这个问题之前,我们可能需要明白,如果你没有定义一个复制构造函数,那么编译器会自动为你添加一个复制构造函数。其实编译器不光会自动添加复制构造函数,它还会在你没有定义的情况下添加赋值操作符、析构函数,甚至构造函数,关于这点可以参看《Effective C++》。编译器添加的复制构造函数直接复制了数据成员和基类的对象,如果你不是想这样做的话,你就得自己定义一个复制构造函数来防止编译器添加的那个。举个例子:
class String { public: String(); String(const char *s); private: char *data; };
这么一个类的定义,没有定义复制构造函数,那么编译器会添加一个,但是这会造成在复制这个类时,data的地址被保存在了1个以上的对象中,那么在释放data的时候就可能多次释放,所以这个类不定义复制构造函数是极其危险的。如果你想阻止复制对象这种行为,该怎么办呢?《C++ 沉思录》中给出了一种做法,即把复制构造函数定义成私有的,同时保证你的成员函数中不会有用到复制对象就OK了:
class String { public: ...... private: String(const String &); String &operator=(const String &); ...... };
8、什么时候需要一个赋值操作符?
这个理由很简单,当你需要用赋值操作符复制对象时就需要定义一个,这里需要注意一点,赋值操作符只有在对象已经构造完成之后才会被调用,这是什么意思呢?看下面的例子:
class A { public: A() {} ~A() {} A(const A &) { printf("copy construct\n"); } A &operator=(const A &) { printf(" = operator\n"); return *this; } }; int _tmain(int argc, _TCHAR* argv[]) { A a; A b(a); A c = a; A d; d = a; return 0; }
这里只有最后那个d = a;会调用operator=(),A b(a);和A c = a;都调用的是拷贝构造函数,前一个好理解,为什么A c = a;也调用的是拷贝构造函数呢?其实这种写法只是一种语法糖,是为了兼容C的写法。赋值操作符还有什么可说的呢?当然,就是在自己赋值给自己的时候,这个行为你需要注意,你最好在赋值之前先判断是否是自己赋值给自己,然后再进行处理。