连续看了两遍的C++ Primer的第十三章, 个人觉得这章应该一看再看。
下面用一个简单的例子来说明类的这几种特殊的构造函数的使用。
class hasPtr{
private:
int val;
int *ptr;
public:
hasPtr(int *ptr, int i):ptr(p),val(i){}
~hasPtr(){delete ptr;}
};
下面的讨论会用到这个看似简单却很难缠 的类。
首先,还是讲讲各个函数的基本定义以及基本用法:
构造函数:是在创建对象的时候,要调用的函数。
下面的两种创建对象的方式:(以标准库的string对象为例)
string str1 = "123456"; //调用string类的以C风格字符串作为形参的构造函数,(这里存在隐式转换)
string str2(5,‘*’); //调用string的另一个构造函数,(另一个重载版本,接受一个数字与字符的版本)
string str3 = string(); //右边调用string的默认构造函数创建一个临时对象,然后调用拷贝构造函数将该临时对象赋给str3
string str4;//调用string的默认构造函数
上述四种情况中,值得一提的是,
1 str1中存在隐式转换,这依赖于string的该构造函数没有使用expecilit关键字。
2 临时对象的概念,临时对象是创建在栈中,在执行完复制操作之后,就会销毁了,就如在函数调用的时候,如果你传入一个string对象,那么函数会在栈上临时存储一个该对象的副本,这里就用到了临时对象。
拷贝构造函数也是一种构造函数。
我不得不说,我对构造函数的理解还太少,既然是构造函数,那么也就是在创建对象的时候可能(因为有多个)用到的函数,自然就应该包括拷贝构造函数和构造函数了。
拷贝构造函数还用在,如果函数的参数或者是返回值是类类型的时候,那么这个时候会调用拷贝构造函数,这对与用户来说是透明的
对于类的对象数组而言,例如:
vector sVec(5);//首先会调用string的默认构造函数生成临时对象,然而使用拷贝构造函数来初始化sVec。
如果用户没有定义自己的拷贝构造函数, 那么编译器会自动生成一个,称为合成拷贝构造函数。逐个成员初始化,功能大致为:
为类类型成员调用该类的拷贝构造函数。为内置类型直接复制,数组成员则是反常的可以复制,并且是一个一个元素的复制。
定义自己的拷贝构造函数:由于合成拷贝构造函数的局限性,对于只包含类成员以及内置数组类型的成员,可以无需定义自己的。合成构造函数就足够。
一般在类的成员有指针,或者是在类的构造函数中分配了资源,那么在复制时,就必须定义自己的拷贝构造函数。
形参:该类型对象的引用。通常为const,这个其实很好想,一般的复制操作不会改变赋值对象的值。至于为什么是引用,其实也很简单,如果是传值的话,在一个函数的参数如果是该对象的话,那么就会调用该拷贝构造函数,由于是传值,就会出现栈溢出。(具体可参见剑指offer。)
无返回值。(与构造函数要求一致)。
这里提到一点,非常重要!!如果我定义了一个类,但是我不允许该类的对象之间可以复制。首先应该想到的是,不允许对象调用复制构造函数,声明为私有即可。
但是,更进一步的想,既然不允许复制,那在任何地方都不允许,包括成员函数和友元(这些是可以访问私有成员的),那么我们就只需要在将复制构造函数以及后面会提到的赋值操作符都声明为私有,并且不定义它。
补充:其实在C++里面已经有一个Uncopyable类,如果想让自己的类不被复制的话,那么可以继承该类。
赋值操作符:在类类型之间赋值,一般都需要定义自己的赋值操作符的含义,重载=操作符。大致为如下形式:
const &operator=(const myClass &)
{
//body
}
注:当该函数作为成员函数时,=操作符是二元操作符,它的另一个操作数即为当前调用它的对象的this指针。并且根据赋值运算符的定义,应该返回的是左操作数的引用。
定义类的拷贝构造函数以及重载赋值运算符,函数实现其实都很简单,关键是什么时候我们必须定义他们。PS:如果类需要复制构造函数,那么它也会需要重载=。
析构函数:
析构函数是不需要用户显示调用的,因此我们需要更加谨慎,要使得该对象在被撤销的时候,无影无踪,不会还保留下什么残骸。
[修改]:析构函数不需要用户显示调用并不意味着用户不能显示调用,显示调用也是可以的,比如内存是由operator new分配的,由placement new初始化的,那么不能直接使用delete,要显示调用析构函数,并且调用operator delete)。
记住:当对象本身超出作用域,或者是删除对象的指针的时候,才会自动调用析构函数。
对于对象数组而言,析构函数的调用方式是逆序撤销的。并且对于每一个对象,非static成员的撤销相对于成员定义的方式也是逆序的。
感谢LJ_SHOU的提醒,这儿理解错了,这里的逆序撤销针对的是单个对象本身的成员而言的,就是是按照成员定义的顺序(也就是构造函数初始化的顺序)的逆序来撤销的。
何时编写析构函数:三法则:如果需要析构函数,那么也需要定义类的拷贝构造函数以及重载赋值运算符。
析构函数是没有形参并且没有返回值的,因此不能重载,所有对象共用一个。
好,介绍到这儿,相信大家对这三种函数也有一定的理解了,对于上面的那个难缠的类,下面给出详细的解释:
class hasPtr{
private:
int val;
int *ptr;
public:
hasPtr(int *ptr, int i):ptr(p),val(i){}
~hasPtr(){delete ptr;}
};
对于上述这个类,如果我们定义该类的对象,会出现什么呢?
int iVal = 7;
hasPrt obj1 = (&iVal,100);
hasPtr obj2(obj1);
对象obj1与对象obj2的ptr指针指向了内存中的同一块区域,那么对obj1的修改将会直接影响到obj2。更为严重的是,析构函数中的delete会带来大问题,如果obj1调用了析构函数,那么obj2的ptr指针将会不知道指向了。
为了解决这样的问题:有以下方法,
第一:使用值型类,很简单的说,就是修改构造函数即可,为对象的ptr指针每次都创建新的空间,那么所有的对象的值就不会出现指向同一块内存的情况了。
第二:智能指针,简言之,就是在释放指针的时候,看是不是还有其他的指针指向这块区域,如果有,那么就不释放,直到计数为0,就释放改指针.
对上述类的改造见如下源代码:
#include
using namespace std;
class U_ptr{
private:
friend class hasPtr;
int *ip;
int count; //count the number of object sharing this ptr
U_ptr(int *p):ip(p),count(1){}
~U_ptr(){if(count == 0) delete ip;}
};
class hasPtr{
private:
int val;
U_ptr *ptr;
public:
hasPtr(int *p, int i);
~hasPtr();
hasPtr &operator=(const hasPtr &rhs);
hasPtr(const hasPtr &rhs);
int *get_ptr() const;
int get_val()const;
};
hasPtr::hasPtr(int *p, int i):ptr(new U_ptr(p)),val(i)
{}
hasPtr::~hasPtr()
{
if(ptr->count == 0)
{
delete ptr;//the destruction of U_ptr invoked
}
}
hasPtr &hasPtr::operator=(const hasPtr &rhs)
{
(rhs.ptr->count)++;
if(--ptr->count==0)
{
delete ptr;
}
ptr = rhs.ptr;
val = rhs.val;
return *(this);
}
hasPtr::hasPtr(const hasPtr &rhs):ptr(rhs.ptr),val(rhs.val)
{
ptr->count++;
}
int *hasPtr::get_ptr()const
{
return ptr->ip;
}
int hasPtr::get_val() const
{
return val;
}
int main()
{
int iVal= 7;
hasPtr obj1(&iVal, 12);
hasPtr obj2(obj1);
cout << obj1.get_ptr()<< "\t"<< obj1.get_val()<< endl;
cout << obj2.get_ptr()<< "\t"<< obj2.get_val()<< endl;
hasPtr *obj3 = new hasPtr(&iVal,13);
cout << obj3->get_ptr()<< "\t"<< obj3->get_val()<< endl;
delete obj3;
cout << obj1.get_ptr()<< "\t"<< obj1.get_val()<< endl;
cout << obj2.get_ptr()<< "\t"<< obj2.get_val()<< endl;
}
在删除obj3之后,obj1以及obj2还是依然能够访问。
C++ Primer这一章是特别特别的重要,对于一个完整的类,一个良好设计的类,我们何时定义它的构造函数,拷贝构造函数以及赋值操作符。
补充:关于对象撤销顺序的一个简单的例子说明:
#include
#include
using namespace std;
class Test
{
private:
int val;
public:
Test(int x = 0):val(x){}
~Test()
{
cout << "val = " << val << endl;
}
void set_val(int x)
{
val = x;
}
};
int main()
{
vector iVec(5);
cout << "begin set val..." << endl;
for(int i = 0; i < 5; ++i)
{
(iVec[i]).set_val(i);
}
return 0;
}
结果为: