《C++沉思录》:类设计者的核查表——有关class的11问
来源:互联网 发布:java视频文件上传 编辑:程序博客网 时间:2024/04/27 14:45
本文的11个问题提取自《C++沉思录》第四章。所有问题的说明均为自己补充。
1 你的类需要一个构造函数吗?
——正确的定义构造函数,把握好构造函数的职能范围
- 有些类太简单,它们的结构就是它们的接口,所以不需要构造函数。
class print{ void print1(){cout<<"1"<<endl;} void print2(){cout<<"2"<<endl;} void print3(){cout<<"3"<<endl;}};
- 有些类很复杂,构造过程就有很复杂的行为,所以必须设计一个构造函数,甚至多个重载构造函数。
class string{public: string(){_buf_ptr = new char [15];} string(const string &); string(const char *);private: char *_buf_ptr;};
- 我常常困扰在这样的问题上:
“我应该把这部分操作放在构造函数中,还是另外声明一个函数单独执行?”
例如:
class socket_connect{public: socket_connect(); PowerOn();private: int sock_fd;};socket_connect::socket_connect(){ sock_fd = socket(AF_INET,SOCK_STREAM,0); init_sockaddr_in();}void socket_connect::PowerOn(){ int ret = bind(sock_fd,(const sockaddr*)&servaddr,sizeof(servaddr)); PERROR(ret,"bind failed!"); ret = listen(sock_fd,BACK_LOG); PERROR(ret,"listen failed!");}
在这里,根据我们的需求,socket本身在初始化的时候就应该开始监听了吧?所以我们完全可以把bind()
和listen()
放到构造函数里面去嘛!
但是我还是单独提出来了一个函数PowerOn()
,这是有理由的。因为我倾向于以全局变量的方式创建socket,而且不希望在我完成一系列socket之外的初始化前,就有客户端试图连接。
嗯,听起来都很有道理。第一种做法赋予了构造函数更全面的“构造”功能;第二种做法将操作细化,可以更好地控制类的行为。
那么。构造函数究竟该执行到哪一步呢?
- 一种说法是:
PowerOn()
或者单独的init()
这样的函数容易被忘记调用,我们应该让class在构造后就能愉快的跑起来! - 另一种声音说:太过复杂的构造函数是不可靠的!你不该过分的信任构造函数!如果你在调用构造函数时出现了问题,后果会很严重的!
- 一种说法是:
目前来看。我还没有找到一个严格的限定。但是对于复杂的类来说,下面的两条一定没有错!
- 空的构造函数是愚蠢的行为!你至少应该把数据赋值成0嘛
- 实现太多功能的构造函数同样是愚蠢的行为!过于复杂的构造函数会让class看起来很凌乱
这额外的一条是未经过考究的:
- 如果有init()
这样的一个函数作为构造函数的补充,起码应该保证当我忘记调用init()
时会通过某种方式给出警告或提醒!
2 你的数据成员是私有的吗?
——pubic
, private
还是 protected
?
在逐渐熟练的使用C++的过程中,我越来越倾向于将所有的数据成员都隐藏起来这样的做法。看这样一个例子:
class SOURCE{public: void init(const char*,unsigned int, unsigned int,void*); inline void increase() {source_amount++;}; inline int decrease(unsigned int val){source_amount -= val}; unsigned int get_sleeptime() const {return sleep_time;} unsigned int get_amount() const {return source_amount;} unsigned int get_speed() const {return source_speed; } char* get_name() const { return name; }private: Mutex source_lock char name[20]; unsigned int source_amount; unsigned int sleep_time; unsigned int source_speed;};
看!我把所有的数据成员的访问权限都设定为private了!
如果你很懒。将数据成员直接暴露在public也并不是错误的做法,但是这样的话有两个致命缺点
失去了对数据成员变化的完全控制
. 我们不知道在何处,在哪里,数据成员被修改了!暴露数据成员意味着会发生原本希望进行一次++操作,却意外的被清零这样的类似问题!
. 所以说,这就相当于你把root权限给了所有用户,多可怕的一件事!不易于修改
. 假设:今天我们的需求是: 每次把这个东西+1,明天需求可能就变成了: 每次把这个东西-1!
. 难道说每一次你都要在所有可能出现修改类成员的地方都把++改为–吗?这显然是不现实的!
. 如果我们把数据成员隐藏,仅仅提供一个访问接口,那事情就变得简单了!我只需要修改这个函数的行为就可以了!
. 在上面的例子中,我可以在接口内进行mutex_lock()
和mutex_unlock()
操作,修改成线程安全的自增操作!这很酷!
因此藏起来所有希望保护的数据成员可能是一个不错的习惯!(尽管定义各种代替访问行为的函数接口会增加工作量)
3 你的类需要一个无参的构造函数吗?
这个问题和问题1相近,构造函数执行到什么程度算好呢?
显然这个问题没有标准答案,决定因素是:你对这个class的设计意图!
class Example{ Example(int p,int q){cout<<p+q<<endl;}};class Example_2{ Expamle_2() {cout<<"nothing"<<endl;}};
如果只提供一个带参数的constructor,会有哪些损失?
Example eg;Example_2 eg_2;
- 显然第一个式子是错误的。作为类的设计者,你了解关于这个类的一切,可能不会犯这样的错误。但是如果别人需要使用你的类呢?这样的创建对象显然会带来一些小问题。
- 考虑
Example a[100]
,同样的道理,只提供一个有参构造函数有时候会带来小麻烦。
好吧,到底该怎么做呢?
- 如果你的设计意图很明确:我不希望编译器给我创建默认构造函数,我希望严格控制这个class对象的构造行为
- 那么放心的仅仅提供有参构造函数,一定没错!(但是注意把这个要求传达给使用者)
- 如果你思考后,觉得提供一个无参构造函数并不被你的设计意图所排斥,而且不会因为没有赋给对象一个有意义的初值导致程序崩溃
- 那么提供一个无参构造函数会让你的类使用起来更方便!
看看STL吧,string s; string s(const string&);
这是一种很好的参考模型。
- 另外,提供默认值的有参构造函数也可以被用于无参的调用!这也不失为好的解决办法
class A{public: A(int a = 10) {}};...int main(){ A a(11);//合法 A b;//合法}
4 是不是构造函数应该初始化所有的数据成员?
看起来这个问题似乎有些奇怪,如果用下面的这种问法,应该会好回答一些:
是不是每个数据成员都应该被构造函数初始化?
显然应该对每个出现的成员进行一个合理的初始化操作。不然出现未定义的行为会让程序出现意料之外的错误。
几个常用的例子有: int data = 0;
char *ptr = NULL;
我想大多数人都知道像上面这样做以避免“未初始化”这样的错误!那么,构造函数中也理应如此。
不过,也别做的那么绝对。书中提到:
有时,类会有一些数据成员,它们只在它们的对象存在了一定时间后才有意义。
对于这种成员,要不要初始化?这就需要留到实际问题中去思考了!不过,养成把指针初始化成NULL的习惯一定不会有错!
5 类需要析构函数吗?
这个问题不难回答,我常在析构函数中做的就是使用delete
来释放new
创建的对象。(同理malloc
和free
也是如此!)
保证一个new
匹配一个delete
,不要多也不要少。
有时候唯一需要注意的是,用delete
还是delete []
。
6 类需要虚析构函数吗?
这个问题比问题5有意义多了!
首先应该知道:
绝不会用作基类的类是不需要虚析构函数的!
那么,为什么析构函数有时候需要被声明为virtual?
什么时候应该声明析构函数为virtual?
class B {};class D: public B {};int main(){ B *b_ptr = new D; //这是正确的 delete b_ptr; //可能会造成错误}
只要有人可能会对实际指向基类D对象的、但类型确实B*类型的指针执行delete表达式,就应该给B添加一个虚析构函数!
为什么这样做?这是我的解读
- 首先,你一定知道:派生类内部是存在一个完整的基类对象的,派生类做的一切变化都是在这个基类对象后面添加的。
- 可以这样理解:基类
B
是1楼,派生类D
是1楼+2楼。现在我们的指针是B *b_ptr
,它的作用范围只有1楼;但是因为B *b_ptr = new D;
,所以这个指针实际指向的对象拥有1楼+2楼。 - 如果我们
delete b_ptr
,很显然,因为指针被编译器限制作用范围为1楼,所以只会是1楼被delete
了!但是别忘了,我们的对象是有2楼的! - 那么1楼被拆掉,2楼会怎样呢?当然是直接悬空!不用怀疑,这是相当危险的!残余的2楼不仅没有被安全释放,还因为1楼已经消失,使得我们无法再通过1楼上到2楼。
因此,虚析构函数是有必要的!对于一个使用不恰当的指针(上面的b_ptr
),我们一旦发现这个指针是不合理的,就通过动态绑定的方式,给他一个假的楼层让它去析构。如此就避免了2楼还在1楼却没了的情况!
虚析构函数通常是空的
7 你的类需要一个赋值操作符吗?
等号=
太常用了,以至于我们经常忘记确定class是否有=
操作就去使用它。
这的后果是什么呢?
class A{public: A(){name = new char[100];} ~A(){delete [] name;}//注意这里private: char *name;};void function(const A& base){ A copy = base;}//function结束后调用析构函数int main(){ A base; function(base);}//main结束后调用析构函数
上面的代码中,function()
函数内,创建了一个名为copy
的对象,并使用了=
操作。尝试运行一下,会发生什么?
————————崩溃啦!!!
为什么?
- 因为默认的
=
操作符只是简单地赋值了一份内存中的。copy
和base
的数据成员char *name
值是相等的,指向同一片内存! - 这就造成,在
function()
调用结束后,析构copy
,name
指向的内存已被删除。 - main函数结束后,还需要析构
base
一次。而此时name已经被释放了!再次delete必然崩溃!
给我们的启示:
- 动态分配的资源,一定要考虑重载
=
。
这里可以这样修改:
A& A::operator=(const A&base){ char *temp = new char [100]; strcpy(temp,base.name); name = temp; //others = base.others; return *this;}
- 良好的指针习惯可以让你在类设计不完善时避免程序崩溃!
~A(){delete [] name;name = NULL;}
——拒绝悬垂指针,从我做起!
上面的操作虽然没有达到预期的目的,但是至少不会崩溃嘛!
TIPS:
通常
operator=
应该返回一个引用class&,并且由return *this
结束以保证与内建的复制操作符一致。
8 你的类需要一个复制构造函数吗?
如果需要一个复制构造函数,那么你多半也需要定义一个=
吧。即使你不需要一个=
,它们的实现方法也是一致的。所以请参见问题7
(当然,如果你的复制构造函数(copy constructor)设计的足够好的话,你完全可以用copy constructor
来实现operator=
而不是用=
来实现copy constructor
)
9 你的赋值操作符能正确的将对象赋给对象本身吗?
——复制自己带来的麻烦
记得《剑指offer》最开始的面试题吗?
如下类型A的声明,为该类添加一个赋值运算符函数。
class A{public: A(char *pData = NULL);//pData一般用new动态分配 A(const A&str); ~A();private: char *pData;};
还记得问题7,8吗?
我们的思路很明确:在赋值运算符函数内,先调用delete,释放原来的字符串,然后再new一个字符串,用strcpy
拷贝过来!(再次强调:delete后先赋值为NULL是好习惯)
那么想一想,下面的语句,会发生什么呢?
A origin;origin = origin;
origin 先执行delete,然后拷贝自己。很显然,origin玩脱了!复制自己的时候,已经delete过了,显然无法完成正确的赋值!
核心问题就是这个啦——防止复制自身!
解决方案如下:
A& A::operator =(const A& origin){ if(this==&origin) return *this; delete [] pData; pData = NULL; pData = new char [strlen(origin.pData)+1]; strcpy(pData,origion.pData); return *this;}
另一个可行,且较好的方案是,用temp临时保存起来origin的值。(调换语句顺序)
A& A::operator =(const A& origin){ char *temp = new char[strlen(origion.pData)+1]; strcpy(temp,origin.pData); delete [] pData; pData = temp; return *this;}
既然说到了这里,同样要提到copy constructor里面的坑——下面哪个复制构造函数是正确的?
class A{public: A(A origion);//1 A(A& origion);//2 A(const A& origion);//3};
第一个通不过编译!
A的复制构造函数禁止带有和A类型的参数!
这是一个值得思考的问题。先有鸡还是先有蛋呢。。。第二个可以通过编译,但是不好,下个问题会详细说明。
- 第三个是推荐的!
10 const总是很重要!
在问题9的末尾,已经提到了 在赋值运算符和复制构造函数中使用const限定符 有关的东西。
我们使用const是为了防止该变量被修改
原则上来讲,凡是不希望改变数据成员的函数我们都给它声明为const
那么,当你真的使用const限定符后,对于某些成员函数,添加const同样也是必须的!
假如一个class被限定为const,编译器会判定一切非const的成员函数的调用为非法!
因为这些函数可能会拥有改变数据成员值的行为!
即使它实际上并没有改变数据成员的想法。
这个例子是一个很好的说明:
class Vec{public: int len_const() const {return len;} int len(){return len;} int len;};void use_Vec(const Vec& origin){ int ret1 = origin.len_const();//合法的 int ret2 = len();//error:不兼容的类型限定符}
以上的问题常常被忽略,所幸IDE自带语法检查,让我们能够及时发现这类错误!
11 删除数组时你记住了用delete []吗?
——new和delete的匹配
这条实在是简单的很,但是实际应用起来却不知忘记了多少次!
每一个new都要匹配一个delete。除非你确认程序马上就会结束。
每一个new [ ] 都应该匹配一个delete[ ]。遵循对称原则总没有错!
完
- 《C++沉思录》:类设计者的核查表——有关class的11问
- 《C++沉思录》笔记:类设计者的核查表
- 《C++沉思录》-第四章-类设计者的核查表
- 类设计者的核查表----c++沉思录笔记
- 类设计者的核查表----摘自C++沉思录
- cpp沉思录笔记1---类设计者的核查表
- 《C++沉思录》读书笔记之类设计者的核查表
- C++沉思录笔记之一(类设计者的核查表)
- 类设计者的核查表
- 类设计者的核查表
- 类设计者的核查表
- 类设计者的核查表
- 类设计者的核查表
- 类设计者的核查表
- 类设计者的核查表
- 类设计者的核查表(转自C++rumination)
- C++类设计者的核查表
- 摘录:类设计者的核查表
- Struts2 常用的常量配置
- 端口被占怎么解决
- 如何包容青春期叛逆的孩子?
- Java的内存泄漏
- C++之继承与多态
- 《C++沉思录》:类设计者的核查表——有关class的11问
- 在linux服务器上搭建XMPP服务器
- Data Structure Alignment 2
- 10 个很有用的高级 Git 命令
- jogl:在SWT中使用OpenGL的模板(SWT_AWT桥接)
- Android检测版本更升级
- OnMouseHover()和OnMouseLeave()(让窗口捕获并响应WM_MOUSEHOVER和WM_MOUSELEAVE消息)
- Struts2 乱码问题
- C基础知识复习(上)