本文的11个问题提取自《C++沉思录》第四章。所有问题的说明均为自己补充。
——正确的定义构造函数,把握好构造函数的职能范围
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()
时会通过某种方式给出警告或提醒!
——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()
操作,修改成线程安全的自增操作!这很酷!
因此藏起来所有希望保护的数据成员可能是一个不错的习惯!(尽管定义各种代替访问行为的函数接口会增加工作量)
这个问题和问题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]
,同样的道理,只提供一个有参构造函数有时候会带来小麻烦。好吧,到底该怎么做呢?
看看STL吧,string s; string s(const string&);
这是一种很好的参考模型。
class A{
public:
A(int a = 10) {}
};
...
int main(){
A a(11);//合法
A b;//合法
}
看起来这个问题似乎有些奇怪,如果用下面的这种问法,应该会好回答一些:
是不是每个数据成员都应该被构造函数初始化?
显然应该对每个出现的成员进行一个合理的初始化操作。不然出现未定义的行为会让程序出现意料之外的错误。
几个常用的例子有:
int data = 0;
char *ptr = NULL;
我想大多数人都知道像上面这样做以避免“未初始化”这样的错误!那么,构造函数中也理应如此。
不过,也别做的那么绝对。书中提到:
有时,类会有一些数据成员,它们只在它们的对象存在了一定时间后才有意义。
对于这种成员,要不要初始化?这就需要留到实际问题中去思考了!不过,养成把指针初始化成NULL的习惯一定不会有错!
这个问题不难回答,我常在析构函数中做的就是使用delete
来释放new
创建的对象。(同理malloc
和free
也是如此!)
保证一个new
匹配一个delete
,不要多也不要少。
有时候唯一需要注意的是,用delete
还是delete []
。
这个问题比问题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楼的!因此,虚析构函数是有必要的!对于一个使用不恰当的指针(上面的b_ptr
),我们一旦发现这个指针是不合理的,就通过动态绑定的方式,给他一个假的楼层让它去析构。如此就避免了2楼还在1楼却没了的情况!
虚析构函数通常是空的
等号=
太常用了,以至于我们经常忘记确定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
指向的内存已被删除。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
结束以保证与内建的复制操作符一致。
如果需要一个复制构造函数,那么你多半也需要定义一个=
吧。即使你不需要一个=
,它们的实现方法也是一致的。所以请参见问题7
(当然,如果你的复制构造函数(copy constructor)设计的足够好的话,你完全可以用copy constructor
来实现operator=
而不是用=
来实现copy constructor
)
——复制自己带来的麻烦
记得《剑指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类型的参数!
这是一个值得思考的问题。先有鸡还是先有蛋呢。。。
第二个可以通过编译,但是不好,下个问题会详细说明。
在问题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自带语法检查,让我们能够及时发现这类错误!
——new和delete的匹配
这条实在是简单的很,但是实际应用起来却不知忘记了多少次!
每一个new都要匹配一个delete。除非你确认程序马上就会结束。
每一个new [ ] 都应该匹配一个delete[ ]。遵循对称原则总没有错!
《C++沉思录》:类设计者的核查表——有关class的11问
原文地址:http://blog.csdn.net/u011497904/article/details/44940595