一、赋值运算符重载和拷贝构造函数区别
#include <iostream>
using namespace std;
class Element
{
private:
int *m_pdata;
public:
Element(int data):m_pdata(NULL) // 指针的成员函数一定在初始化列表中赋值为初始化为NULL
{
cout << "构造函数" << endl;
m_pdata = new int(data);
}
Element(const Element& e)
{
cout << "拷贝构造" << endl;
}
void operator=(const Element& e) // 返回值是void,有啥问题呢?
{
cout << "运算符重载" << endl;
}
~Element()
{
delete m_pdata;
}
};
int main()
{
Element e1(1);
Element e2(2);
Element e3 = e1;
e2 = e1;
system("pause");
return 0;
}
运行上面的程序后,输出如下:
所以最后可以得出结论:
(1)拷贝构造函数是在初始化并赋值的时候调用的。比如说例子中的Element e3 = e1,或者stl容器vector在添加元素的时候都会调用拷贝构造函数。
(2)赋值运算符重载只会在单纯的赋值操作中调用,被赋值的对象已经创建过了。
二、可能引发的问题
如果没写自定义的拷贝构造函数或者赋值运算符重载的话,调用时会默认使用浅拷贝,当成员函数中有指针的话会出大问题
上面的成员变量就是指针,把上面的代码稍微修改一下,去掉自定义的拷贝构造函数或者赋值运算符重载。
#include <iostream>
using namespace std;
class Element
{
private:
int *m_pdata;
public:
Element(int data):m_pdata(NULL) // 指针的成员函数一定在初始化列表中赋值为初始化为NULL
{
cout << "构造函数" << endl;
m_pdata = new int(data);
}
~Element()
{
cout << "被释放了" << "pdata:" << m_pdata << endl;
// 开始认为加上判空以及delete后赋值为空可以避免崩溃,但是想多了,还是对指针理解不到位啊.
// 给一个对象的成员变量修改后不会修改其他对象中成员变量的引用。
if (m_pdata) //
{
delete m_pdata;
m_pdata = NULL;
}
}
};
// 放到函数中,变量生存空间变成了函数内部,结束后可以直接调用析构函数
void test()
{
Element e1(1);
Element e2(2);
Element e3 = e1;
e2 = e1;
}
int main()
{
test();
system("pause");
return 0;
}
上面的程序在运行结果:
在第二次释放的时候就崩溃了。因为三个对象e1,e2,e3中成员变量m_pdata的值都为00035450,第一个对象做析构的时候,把00035450这个地址的内存区域delete掉了,其他两个对象的m_pdata却还指向被释放的内存区域00035450,也就变成了野指针,然后其他两个对象调用析构的时候对野指针进行delete就崩溃了。
所以说,,当成员变量有指针的时候,还是自己写拷贝构造和赋值运算符重载来进行深拷贝吧,不然出问题的几率还是很大的。
BTW:我在赋值运算符重载的时候,返回的值是void(这样是可以的,因为赋值运算符本质是对this的修改,只要在重载函数中改了this就可以了,返回值是什么都可以),但是这样的话就不能进行a=b=c这样的连续赋值操作了,所以还是建议返回值为该类型的引用(用引用减少一次临时的拷贝构造)
修改后的代码:
#include <iostream>
using namespace std;
class Element
{
private:
int *m_pdata;
public:
Element(int data):m_pdata(NULL) // 指针的成员函数一定在初始化列表中赋值为初始化为NULL
{
cout << "构造函数" << endl;
m_pdata = new int(data);
}
Element(const Element& e)
{
cout << "拷贝构造" << endl;
m_pdata = new int(*(e.m_pdata));
}
Element& operator=(const Element& e) // 返回值是void,有啥问题呢?
{
cout << "运算符重载" << endl;
// 标准写法还需要进行一次判断this!=&e,防止a=a这种无意义的赋值
m_pdata = new int(*(e.m_pdata));
return *this;
}
~Element()
{
cout << "被释放了" << "pdata:" << m_pdata << endl;
// 开始认为加上判空以及delete后赋值为空可以避免崩溃,但是想多了,还是对指针理解不到位啊.
// 给一个对象的成员变量修改后不会修改其他对象中成员变量的引用。
if (m_pdata) //
{
delete m_pdata;
m_pdata = NULL;
}
}
};
// 放到函数中,变量生存空间变成了函数内部,结束后可以直接调用析构函数
void test()
{
Element e1(1);
Element e2(2);
Element e3 = e1;
e2 = e1;
}
int main()
{
test();
system("pause");
return 0;
}
三、再探stl中的交互
#include <iostream>
#include <map>
#include <vector>
using namespace std;
class Element
{
private:
int *m_pdata;
public:
Element():m_pdata(NULL) // 指针的成员函数一定在初始化列表中赋值为初始化为NULL
{
cout << "默认构造函数" << endl;
m_pdata = new int(-1);
}
Element(int data):m_pdata(NULL) // 指针的成员函数一定在初始化列表中赋值为初始化为NULL
{
cout << data << "构造函数" << endl;
m_pdata = new int(data);
}
Element(const Element& e)
{
cout << *e.m_pdata << "拷贝构造" << endl;
this->m_pdata = new int(*e.m_pdata+2000000);
}
void operator=(const Element& e) // 返回值是void,有啥问题呢?
{
cout << *e.m_pdata << "运算符重载" << endl;
this->m_pdata = new int(*e.m_pdata+3000000);
}
~Element()
{
cout << *m_pdata << "析构" << endl;
delete m_pdata;
}
}
int main()
{
cout << "------vector test--------" << endl;
std::vector<Element>vt ;
vt.push_back(Element(10));
cout << "------map test--------" << endl;
std::map<int,Element>mmap;
mmap[10]=Element(100);
cout << "------map test2--------" << endl;
mmap.insert(std::pair<int,Element>(100, Element(200)));
cout << "end" << endl;
return 0;
}
输出结果
分析:
- vector的插入和第一部分说的一样,容器插入的时候会调用拷贝构造函数。同时map的insert也是一样的。
- c++ 的map的赋值时,如果是map[key]这种方式使用map的话,如果map中没有这个key对应的value也会调用value类型的默认构造函数自动创建一个,如果没有默认构造函数只有初始化构造函数会报错。所以可以抽象为两步:
所以后面调用的也是赋值函数。最后那个临时变量还会析构,这种情况下极大的增大了开销,所以c++11中引入了move语义,直接把右值的临时变量内存指向左值中,不需要两次创建对象。map[key]() map[key]=value