码文不易,觉得内容不错点个关注吧~
在C++的类中有这么一些成员函数是自动定义的,具体如下所示:
1.默认构造函数
2.默认析构函数
3.复制构造函数(即拷贝构造函数)
4.赋值运算符
5.地址运算符
C++11提供了另外的两个特殊成员函数:移动构造函数和移动赋值运算符,在后讨论。
本文仅讨论1,3,4的问题
一.默认构造函数
在不定义构造函数时,会自动定义一个默认构造函数。反之则不会定义。如果想在创建对象的时候不显式地初始化,就必须自己显式地定义默认构造函数。这种构造函数没有参数,有两种方式:1.不带任何参数的默认构造。2.带default值的有参构造函数。
注意:这两种定义不能同时存在,不然的话在用默认构造创建对象的时候会有二义性。
Klunk(int n = 0){klunk_ct=n};//定义1
Klunk(){klunk_ct=0};//定义2
二.复制构造函数(拷贝构造函数)
2.1何为拷贝构造函数,何时调用
拷贝构造函数用于将一个对象复制到新建对象中。一言以蔽之,初始化过程中。
常见原型 Class_name(const Class_name &)
它接受一个指向类对象的常量作为参数,以下为调用拷贝构造函数的4种声明
Stringbad copy_1(initial)// Stringbad是类名,copy_n为新对象名,initial为旧对象名
Stringbad copy_2 = initial;
Stringbad copy_3 = Stringbad(initial);
Stringbad * pcopy_3 = new Stringbad(initial);
2.2默认拷贝构造函数的功能与缺陷
默认拷贝构造函数逐个复制非静态成员,复制的是成员的值。(这就是浅拷贝)
注意:静态成员不受影响,因为它们属于整个类,所有的对象都共享同一份,因此不会被操作。
为了说明浅拷贝的危害,下面举一个例子来说明。这是一个有缺陷的Stirng类的实现,因此将他命名为Stringbad。
class Stringbad {
private:
char *str; //声明一个char*而不具体的定义多大空间,这样能够根据需要来,而不会浪费。
int len;// 字符串的长度
static int num_strings;//一个静态成员,注意只有在这里声明才需要static,待会使用的时候是没有static的
public:
Stringbad(const char *a);//带参构造
Stringbad();//默认构造
~Stringbad();//析构函数
friend std::ostream & operator<<(std::ostream &os, const Stringbad &st);//<<运算符重载,为了能以<<对象的方式就打印。
};
Stringbad::Stringbad(const char *a) {
len =std::strlen(a);
str = new char[len+1];//because it ignore the last null char.
std::strcpy(str,a);
num_strings++;
cout<<num_strings<<": "<<str<<" "<<"object created\n";
}
Stringbad::Stringbad() {
len = 4;
str = new char[len];
std::strcpy(str,"C++");
num_strings++;
cout<<num_strings<<": "<<str<<" "<<"object created\n";
}
Stringbad::~Stringbad() {
cout<<num_strings<<": "<<str<<" "<<"object delted\n";
--num_strings;
cout<<"Now has "<<num_strings<<" left\n";
delete []str;
}
std::ostream & operator<<(std::ostream &os,const Stringbad &st)
{
os<<st.str;
return os;
}
接着我们在main函数中调用以下这个测试:
void test01(){
Stringbad a1 = "ddd";
Stringbad a2 = "mmm";
Stringbad copy_1 = a1;
cout<<copy_1;
}
1.我们先通过构造函数创建了两个对象a1,a2,在构造函数中我们每次都会让num_string这个值+1,因此为2,说明有两个对象了。
2.然后我们使用默认拷贝构造将a1拷贝给了copy_1,但是默认拷贝构造只是简单的将成员的值复制了一下而已,并没有使静态成员发生变化,因此我们的num_string还是2.(缺陷1)
3.由于对象的创建顺序是a1,a2,copy_1因此析构的顺序也应该是copy_1,a2,a1。我们观禅控制台的输出,1.发现先是ddd被delete了,这个是copy_1的成员变量str被delete了。2.接着是mmm,也正常地被析构了3.然后是a1的mmm,但是并没有正常地打印出内容这是为什么呢??因为浅拷贝只是简单的赋值而已,它将指向a1的str的指针直接拿给了copy_1,相当于这两个对象都指向了同一片区域,因此对copy的str删除,将使a1指向的str也没有了ps:就是同一个(缺陷2)
总结:1.不能拷贝静态变量
2.在存在指针的情况下,并没有给新对象独立的区域而是简单的赋值。
3.3解决方案:深拷贝
深拷贝就是在上述的缺陷中提出来的,代码实现如下。非常简单,就是将C++没有给我们考虑好的拷贝要素自己写好就OK了,这就是深拷贝,这样就是真正独立的一个对象了,并不依靠于别的对象。
Stringbad::Stringbad(const Stringbad &t) {
num_strings++;
len = t.len;
str = new char [len+1];
std::strcpy(str,t.str);
cout<<num_strings<<": "<<str<<" "<<"object created\n";
}
三.赋值运算符
3.1赋值运算符的来由,我们为何能隐式构造
现在我们来关注一下这个写法Stringbad copy_2 = initial;
显然这就是拷贝构造嘛,前面讲的隐式构造。但是!为什么这样是可以的呢?C语言没有类,当然不会支持它,C++为什么支持呢?其实这里是C++通过为我们类重载赋值运算符实现的,这种运算符的原型如下:
Class_name & Class_name::operator=(const Class_name &);
那你可能会奇怪了,那我们直接重写一个赋值运算符就好了,不用写拷贝构造了,因为像Stringbad copy_2 = intial这样的写法,能够直接被匹配。
实际上初始化对象时并不一定会使用赋值运算符。
上述语句实现的时候可能会分两布处理该语句:1.使用拷贝构造函数创建一个临时变量2.通过=赋值重载复制到新对象中。
也就是说初始化总会调用靠背构造,而使用=运算符也允许调用赋值运算符。
3.2赋值的错误
赋值的错误如下所示,a1用a2赋值了,从而先析构a2的时候str内容已经消失了,再析构a1的时候以及该地质以及没有内容了,会出现数据受损的危害。
void test01(){
Stringbad a1 = "ddd";
Stringbad a2 = "mmm";
a1 = a2;
cout<<a1;
}
3.3解决赋值问题(深赋值)
与深拷贝一样,深赋值就是把浅操作的问题都弥补一下,但是是有所差别的需要注意如下:
1.由于目标对象可能引用了以前分配的数据,所以需要使用delete[]来释放这些数据防止内存泄漏。
2.函数应该避免赋给自己;否则在给对象重新赋值前会将该对象的内容删除。
3.函数返回一个指向调用对象类型的引用。
Stringbad &Stringbad::operator=(const Stringbad &st) {
if(this == &st)
{
return *this;
}
delete []str;
len = st.len;
str = new char[len+1];
std::strcpy(str,st.str);
return *this;
}