C++ 拷贝构造函数详解
下面的讲解将以C++标准库的string类作为讲解对象,string类:class with pointer member(s)
1、拷贝构造函数和拷贝赋值函数
1.1引入
下面是给出的测试函数,也是我们要能在自己设计的myString类中实现的功能:
int main()
{
myString s1(); //无参数构造函数
myString s2("Hello world!"); //传入字符串的构造函数
myString s3(s1); //拷贝构造
cout<<s3<<endl; //操作符重载,对<<重载
s3 = s2; //拷贝赋值
cout<<s3<<endl;
}
当我们没有显式写出拷贝构造函数和拷贝赋值函数时,编译器会给我们默认提供拷贝构造和拷贝赋值函数,这两个函数做的都是逐字节地将一个对象的内容拷贝到另一个对象中。
对于成员没有指针的类,默认的拷贝构造函数一般不需要再重写。但是对于成员中含有指针的类,那么拷贝构造函数必须要进行重写。不能使用默认的拷贝构造函数。
#ifndef COPYCONSTRUCTOR_MYSTRING_H
#define COPYCONSTRUCTOR_MYSTRING_H
class myString {
private:
char* m_data;//动态分配的方式
public:
myString(const char* cstr = 0);
myString(const myString& str);//拷贝构造函数
myString& operator=(const myString& str);//拷贝赋值
~myString();//析构函数,类死亡的时候调用
char* get_c_char()const{return m_data;};//inline function
};
#endif //COPYCONSTRUCTOR_MYSTRING_H
下面我们先对普通的构造函数和析构函数进行创建:
inline
myString::myString(const char* cstr) {
if(cstr){
m_data = new char[strlen(cstr)+1];//别忘了结束符要占用长度
strcpy(m_data, cstr);
}else{//未指定初值
m_data = new char[1];
*m_data = '\0';
}
}
inline
myString::~myString() {
delete[] m_data;
}
上述的创建分别使用了array new和array delete,即array[]和delete[]的写法。两者一定要搭配使用,不然会造成内存泄漏:
可以进行这样的测试:
{
myString s1();
myString s2("hello");
myString* p = new myString("hello");
delete p;
}
使用new的关键字进行动态创建字符串,离开作用域时,必须写出delete p,删除指针对象。
未使用new关键字创建的字符串会自动调用析构函数。
1.2 拷贝构造函数
- class with pointer members 必须有 copy cstr 和 copy op=
对于内含指针的构造函数,若使用默认的构造函数,则是**“浅拷贝”**memory leak,有可能造成内存泄漏。
**内存泄漏(Memory Leak)**是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
如下图,我们希望的是a和b各自有一个指针指向各自的字符串Hello\0
,假使我们使用默认的拷贝构造,那么原先的World\0
将不会有指针指向:造成了内存泄漏
alias:别名,两个指针指向同一个字符串也是非常危险的,a修改a的m_data
,结果b的m_data
也被修改了。这是我们所不希望的。
-
拷贝构造函数的设置
下面的写法就避免了内存泄漏,称为深拷贝。
inline
myString::myString(const myString &str) {
//创建出足够的空间放蓝本
m_data = new char[strlen(str.m_data) + 1];//直接取另一个对象的private:兄弟之间互为友元
strcpy(m_data, str.m_data);
}
可以看到在拷贝构造函数中,我们使用了另一个object对象的private成员,可以直接调用,这是因为同一个类的不同对象之间互为友元。
可编写测试函数:
{
myString s1("Hello");
myString s2(s1);
myString s3 = s1;
//第三行和第四行是不同的操作,
//一个是利用拷贝构造函数创建出一个新的对象
//另一个是使用了拷贝赋值函数
}
1.3拷贝赋值函数
- 赋值的过程
- 销毁自己
- 重分配空间
- 返回*this
inline
myString & myString::operator=(const myString &str) {
//检测自我赋值
//self assignment
if(this == &str){//?
return *this;
}
delete[] m_data; //①
m_data = new char[strlen(str.m_data)+1];//加上结束符的长度②
strcpy(m_data, str.m_data);//③
return *this;
}
上述的①②③即分别对应上述赋值过程。特别要注意的是自我赋值,自我赋值的检测不仅关系到效率,还关系到下面程序的正确性。
2.new 和 delete
2.1对象的生命——堆空间和栈空间
对象一定存储在内存中,但可以是存储在栈空间,也可是在堆空间。
s1,s2,s3的内存空间在栈中,称为stack object,又叫做local object,因为其声明在作用域结束的时候就结束了,又被称为auto object,因为他被自动清理——析构函数被自动调用。
- static变量
{
myString s1 = myString(); //无参数构造函数
static myString s2 = myString("Hello world!"); //传入字符串的构造函数
return 0;
}
假如s2对象设定为static,那么这个statck object就会变成static对象,其生存期为程序的生存期,声明在作用域结束之后仍存在。
要注意的是static变量只会初始化一次。即重复调用函数修改static变量的值也只会修改一次。
- 全局变量
class Complex{...};
...
Complex c3(1,2);
int main()
{
……
}
像c3这样的变量称为全局变量,其作用域和static变量一样。
2.2 new的正确使用方法
new 必须搭配delete使用,不然可能造成内存泄漏。
而new运算符会被分解成三个操作:
- 分配内存:使用operator new函数,内部调用malloc,为对象分配内存
- 转型:把void转型成为Complex
- 构造函数:通过转型得到的指针调用其构造函数
即整个new的动作是:先分配内存,再调用构造函数
2.3 delete的使用
delete ps;
编译器会将其转换为:
myString::~myString(ps); //析构函数
operator delete(ps); //释放内存
即delete被转化为两个动作:先调用析构函数然后释放内存。
而调用析构函数需要做什么?这需要我们自己定义:
myString::~myString() {
delete[] m_data;
}
在我们定义的字符串的析构函数中,我们对动态申请的字符串的空间进行了删除,比如删除了字符串"hello word"。而字符串的m_data本身只是一个指针,此时还没有被删除。
operator delete(ps);则是内部调用free函数的一个函数,将指针删除。
而调用析构函数需要做什么?这需要我们自己定义:
myString::~myString() {
delete[] m_data;
}
在我们定义的字符串的析构函数中,我们对动态申请的字符串的空间进行了删除,比如删除了字符串"hello word"。而字符串的m_data本身只是一个指针,此时还没有被删除。
operator delete(ps);则是内部调用free函数的一个函数,将指针删除。