C++ 构造函数
- 默认构造函数
- 普通构造函数
- 复制构造函数
- 移动构造函数
- 转换构造函数
1. 默认构造函数
- 什么是默认构造函数?没有参数或者所有参数都有默认值的构造函数。
- 如果类没有定义任何构造函数,编译器会提供一个默认构造函数。
- 如果定义了非默认构造函数,而不显式定义默认构造函数,则编译器会自动禁用默认构造函数,此时创建未经初始化的对象(实例)的语句会引发编译器错误。
- 通常情况下,不要显式定义默认构造函数,这种做法是禁止用户创建未初始化的对象。
#include <iostream>
using std::cout;
using std::endl;
class tString
{
private:
char *m_str;
public:
/**
* @brief 默认构造函数
* @note 没有参数或者所有参数都有默认值的构造函数,称为默认构造函数
* 作用:
* 定义了默认构造函数后,允许创建未经初始化的对象(实例),
* 如 tString ts; // 如果没定义默认构造函数,这句代码会引发编译器报错
*/
//tString();
};
//tString::tString() {}
int main()
{
// 把所有构造函数注释掉等价于未定义任何构造函数,此时以下语句是合法的,因为编译器提供了默认构造函数
tString ts;
return 0;
}
class tString
{
private:
char *m_str;
public:
/**
* @brief 默认构造函数
* @note 没有参数或者所有参数都有默认值的构造函数,称为默认构造函数
* 作用:
* 定义了默认构造函数后,允许创建未经初始化的对象(实例),
* 如 tString ts; // 如果没定义默认构造函数,这句代码会引发编译器报错
*/
// tString();
/**
* @brief 非默认构造函数
* @note 定义非默认构造函数之后,如果不显式定义默认构造函数,不允许创建未经初始化的对象(实例)
* 如 tString ts; // 触发编译器错误
*
* @param src -
* @param len -
*/
tString(const char *src, int len);
};
// tString::tString() {}
tString::tString(const char *src, int len) : m_str(new char[len + 1]) {}
int main()
{
// 此时该语句触发编译器错误,但如果将默认构造函数解注释,则可通过编译
tString ts;
return 0;
}
2. 普通构造函数
3. 复制构造函数
- 复制构造函数/拷贝构造函数通常用于“左值复制”。
#include <iostream>
#include <string.h> // C standard lib
using std::cout;
using std::endl;
class tString
{
private:
char *m_str;
int m_len;
public:
/**
* @brief 非默认构造函数
*/
tString(const char *src, int len) : m_len(len), m_str(new char[len + 1])
{
for (int i = 0; i < m_len; i++)
{
m_str[i] = src[i];
}
m_str[m_len] = '\0';
cout << "general constructor." << endl;
}
/**
* @brief 复制构造函数/拷贝构造函数
* @note C++默认的复制构造函数只进行“浅拷贝”,自己实现复制构造函数的目的
* 通常都是将默认的浅拷贝转换为“深拷贝”
*
* @attention 这里有个有趣的现象,复制构造函数可以访问不同对象的私有成员!!!
*
* @param _ts -
*/
tString(const tString &_ts)
{
m_len = _ts.m_len; // 直接访问_ts对象的私有成员变量
m_str = new char[m_len + 1];
char *tmp = _ts.m_str; // 直接访问_ts对象的私有成员变量
int i = 0;
while(*tmp)
{
m_str[i] = *tmp;
i++;
tmp++;
}
m_str[m_len] = '\0';
cout << "copy constructor called." << endl;
}
~tString()
{
if (m_str)
{
delete[] m_str;
}
cout << "destructor called." << endl;
}
void showAddr()
{
cout << (void *)m_str << endl;
}
};
tString testCopyTime(tString ts1)
{
return ts1;
}
int main()
{
char src[20] = "Hello, world.";
tString ts1 = tString(src, strlen(src));
tString ts2 = ts1;
ts1.showAddr();
ts2.showAddr();
// 为了测试不适用左值引用作为形参,以及返回临时对象时,复制构造函数被调用的次数
// 根据gdb调试,调用testCopyTime函数会调用两次复制构造函数,同一份数据会在一瞬间存在于三个对象中(三份拷贝)
tString ts3 = testCopyTime(ts2);
return 0;
}
# 打印结果
general constructor. # ts1对象构建时调用一般构造函数
copy constructor called. # ts2对象构建时调用复制构造函数
0xeb6fd0 # 使用自定义复制构造函数进行深拷贝,ts1和ts2内容一致,但是内部指针指向两个不同的内存块
0xeb6fe8
copy constructor called. # 调用testCopyTime()创建形参变量时调用复制构造函数一次
copy constructor called. # testCopyTime()赋值给返回值时,触发复制构造函数一次,调用一次testCopyTime触发两次复制构造函数!!
destructor called. destructor called. destructor called. destructor called.
如果把自定义的复制构造函数注释掉,c++也会提供默认的复制构造函数。
class tString
{
private:
char *m_str;
int m_len;
public:
/**
* @brief 非默认构造函数
*/
tString(const char *src, int len) : m_len(len), m_str(new char[len + 1])
{
for (int i = 0; i < m_len; i++)
{
m_str[i] = src[i];
}
cout << "general constructor." << endl;
}
/**
* @brief 复制构造函数/拷贝构造函数
* @note C++默认的复制构造函数只进行“浅拷贝”,自己实现复制构造函数的目的
* 通常都是将默认的浅拷贝转换为“深拷贝”
*
* @attention 这里有个有趣的现象,复制构造函数可以访问不同对象的私有成员!!!
*
* @param _ts -
*/
#if 0
tString(const tString &_ts)
{
m_len = _ts.m_len; // 直接访问_ts对象的私有成员变量
m_str = new char[m_len];
char *tmp = _ts.m_str; // 直接访问_ts对象的私有成员变量
int i = 0;
while(*tmp)
{
m_str[i] = *tmp;
i++;
tmp++;
}
cout << "copy constructor called." << endl;
}
#endif
~tString()
{
if (m_str)
{
delete[] m_str;
}
cout << "destructor called." << endl;
}
void showAddr()
{
cout << (void *)m_str << endl;
}
};
tString testCopyTime(tString ts1)
{
return ts1;
}
int main()
{
char src[20] = "Hello, world.";
tString ts1 = tString(src, strlen(src));
// 此语句触发的是“默认的复制构造函数”,而自定义的复制构造函数被注释掉了
tString ts2 = ts1;
ts1.showAddr();
ts2.showAddr();
// 为了测试不适用左值引用作为形参,以及返回临时对象时,复制构造函数被调用的次数
// 根据gdb调试,调用testCopyTime函数会调用两次复制构造函数,同一份数据会在一瞬间存在于三个对象中(三份拷贝)
//tString ts3 = testCopyTime(ts2);
return 0;
# 打印结果
general constructor. 0xee6fd0 0xee6fd0 destructor called. destructor called.
可以看到ts2的m_str指针和ts1的m_str指针指向同一内存处,这种情况下,当ts2和ts1析构时,就会造成***“double free”***的问题。
4. 移动构造函数
C++11引入移动语义。所谓移动,就是把原来对象A所拥有的数据转移给对象B,这种情况就像是有两颗二叉树,需要把二叉树A的一个结点移到二叉树B上,移动节点后,树A原有的结点可以置空、也可以释放。
在C++代码层面:
#include <iostream>
#include <string.h> // C standard lib
using std::cout;
using std::endl;
class tString
{
private:
char *m_str;
int m_len;
public:
/**
* @brief 默认构造函数
*/
// tString() : m_str(nullptr), m_len(0) {}
/**
* @brief 非默认构造函数
*/
tString(const char *src, int len) : m_len(len), m_str(new char[len + 1])
{
// ..
}
/**
* @brief 复制构造函数/拷贝构造函数
*/
tString(const tString &_ts)
{
// ...
}
/**
* @brief 移动构造函数
* @param ts_ -
*/
tString(tString &&ts_) : m_len(ts_.m_len), m_str(ts_.m_str)
{
// 为了防止double free,将ts_源对象的值置空
ts_.m_str = nullptr;
ts_.m_len = 0;
cout << "Move constructor called." << endl;
}
~tString()
{
if (m_str)
{
delete[] m_str;
}
cout << "destructor called." << endl;
}
void showAddr()
{
cout << (void *)m_str << endl;
}
};
int main()
{
char src[20] = "Hello, world.";
tString ts3 = tString(src, strlen(src));
// 触发移动构造函数:三种方式
ts3.showAddr();
// 方式1 - 强制类型转换
tString ts4 = tString(static_cast<tString &&>(ts3));
ts3.showAddr();
ts4.showAddr();
// 方式2 - std::move()
tString ts5 = std::move(ts4);
ts4.showAddr();
ts5.showAddr();
// 方式三,重载+/=等运算符,使其支持移动语义
return 0;
}
移动构造函数中使用到**右值引用作为形参,要点是如果源对象有指针成员,转移后,需置空源对象指针,否则容易导致double free。
5. 转换构造函数 - 单参构造函数
#include <iostream>
#include <string.h> // C standard lib
using std::cout;
using std::endl;
class tString
{
private:
char *m_str;
int m_len;
public:
/**
* @brief 默认构造函数
*/
// tString() : m_str(nullptr), m_len(0) {}
/**
* @brief 非默认构造函数
*/
tString(const char *src, int len) : m_len(len), m_str(new char[len + 1])
{
// ...
}
/**
* @brief 转换构造函数/单参构造函数
* @note 主要作用由两点:
* 1. tString对象仅需初始化单个参数
* 2. 作为类型转换,如将int转换为tString
* @param num -
*/
explicit tString(int num) : m_str(nullptr), m_len(num)
{
// 这里并没有作类型转换,简单例子
cout << "type convert constructor called." << endl;
}
/**
* @brief 复制构造函数/拷贝构造函数
*/
tString(const tString &_ts)
{
// ...
}
/**
* @brief 移动构造函数
* @param ts_ -
*/
tString(tString &&ts_) : m_len(ts_.m_len), m_str(ts_.m_str)
{
// ...
}
~tString()
{
if (m_str)
{
delete[] m_str;
}
cout << "destructor called." << endl;
}
void showAddr()
{
cout << (void *)m_str << endl;
}
};
tString testCopyTime(tString ts1)
{
return ts1;
}
int main()
{
tString ts6(100);
return 0;
}
6.构造函数的显式(explicit)和隐式(implicit)
加了explicit关键字的构造函数,只能被显式调用,而且***一般只用于修饰单参构造函数***,可以禁止编译器执行非预期的类型隐式转换。
7. 构造函数小结
- 复制构造函数应当保证每一个对象实例化时,都拥有自己独立的内存地址,不与其他对象共享内存地址,避免double free or multiple free。
- 移动构造函数实际上是同一个内存地址空间换了个对象表示,但必须把源对象的指针/引用置空,也是为了防止double free。
C++深复制和浅复制
1. 什么是深拷贝、浅拷贝(什么是深复制、浅复制)?
2. 深拷贝和浅拷贝的区别
- 对于基本类型数据(非指针、非引用),深拷贝和浅拷贝效果基本一致
- 但当对象中含有指针、引用时,如果使用浅拷贝,会导致多个对象中的指针指向同一块内存空间,当对象析构时,会对同一地址多次delete,造成***“double free or multiple free”***。
- 如果使用深拷贝,每一个对象中的指针/引用都指向独立的内存空间,于是避免了"对同一地址多次delete"的问题。
完整地练习源码
#include <iostream>
#include <string.h> // C standard lib
using std::cout;
using std::endl;
class tString
{
private:
char *m_str;
int m_len;
public:
/**
* @brief 默认构造函数
* @note 没有参数或者所有参数都有默认值的构造函数,称为默认构造函数
* 作用:
* 定义了默认构造函数后,允许创建未经初始化的对象(实例),
* 如 tString ts; // 如果没定义默认构造函数,这句代码会引发编译器报错
*/
// tString() : m_str(nullptr), m_len(0) {}
/**
* @brief 非默认构造函数
* @note 定义非默认构造函数之后,如果不显式定义默认构造函数,不允许创建未经初始化的对象(实例)
* 如 tString ts; // 触发编译器错误
*
* @param src -
* @param len -
*/
tString(const char *src, int len) : m_len(len), m_str(new char[len + 1])
{
for (int i = 0; i < m_len; i++)
{
m_str[i] = src[i];
}
m_str[m_len] = '\0';
cout << "general constructor." << endl;
}
/**
* @brief 转换构造函数/单参构造函数
* @note 主要作用由两点:
* 1. tString对象仅需初始化单个参数
* 2. 作为类型转换,如将int转换为tString
* @param num -
*/
explicit tString(int num) : m_str(nullptr), m_len(num)
{
// 这里并没有作类型转换,简单例子
cout << "type convert constructor called." << endl;
}
/**
* @brief 复制构造函数/拷贝构造函数
* @note C++默认的复制构造函数只进行“浅拷贝”,自己实现复制构造函数的目的
* 通常都是将默认的浅拷贝转换为“深拷贝”
*
* @attention 这里有个有趣的现象,复制构造函数可以访问不同对象的私有成员!!!
*
* @param _ts -
*/
tString(const tString &_ts)
{
m_len = _ts.m_len; // 直接访问_ts对象的私有成员变量
m_str = new char[m_len + 1];
char *tmp = _ts.m_str; // 直接访问_ts对象的私有成员变量
int i = 0;
while(*tmp)
{
m_str[i] = *tmp;
i++;
tmp++;
}
m_str[m_len] = '\0';
cout << "copy constructor called." << endl;
}
/**
* @brief 移动构造函数
* @param ts_ -
*/
tString(tString &&ts_) : m_len(ts_.m_len), m_str(ts_.m_str)
{
// 为了防止double free,将ts_源对象的值置空
ts_.m_str = nullptr;
ts_.m_len = 0;
cout << "Move constructor called." << endl;
}
~tString()
{
if (m_str)
{
delete[] m_str;
}
cout << "destructor called." << endl;
}
/**
* @brief 重载赋值运算符,支持深拷贝
* @param ts_ -
* @return tString& -
*/
tString & operator=(const tString &ts_)
{
// 如果是将自己赋值给自己
if (this == &ts_)
{
return *this;
}
delete[] m_str;
m_len = ts_.m_len;
m_str = new char[m_len + 1];
int i = 0;
while (i < m_len)
{
m_str[i] = ts_.m_str[i];
i++;
}
m_str[m_len] = '\0';
cout << "copy assigned function called." << endl;
}
/**
* @brief 支持移动语义的赋值运算符重载
* @param ts_ -
* @return tString& -
*/
tString & operator=(tString &&ts_)
{
if (this == &ts_)
{
return *this;
}
delete[] m_str;
m_len = ts_.m_len;
m_str = ts_.m_str;
ts_.m_len = 0;
ts_.m_str = nullptr;
return *this;
cout << "move assign function called." << endl;
}
void showAddr()
{
cout << (void *)m_str << endl;
}
void val()
{
cout << m_str << endl;
}
};
tString testCopyTime(tString ts1)
{
return ts1;
}
int main()
{
char src[20] = "Hello, world.";
tString ts1 = tString(src, strlen(src));
ts1.val();
tString ts2 = ts1;
ts1.showAddr();
ts2.showAddr();
ts2.val();
tString ts3 = testCopyTime(ts2);
// 触发移动构造函数:三种方式
ts3.showAddr();
ts3.val();
// 方式1 - 强制类型转换
tString ts4 = tString(static_cast<tString &&>(ts3));
ts3.showAddr();
ts4.showAddr();
// 方式2 - std::move()
tString ts5 = std::move(ts4);
ts4.showAddr();
ts5.showAddr();
// 方式三,重载+/=等运算符,使其支持移动语义
tString ts6(100);
ts6 = tString(src, strlen(src));
ts6.showAddr();
return 0;
}
不知不觉已经工作快一年了,好久没有写过博客,再次记录一下本周学习,偷得浮生半日闲,又摘桃花换酒钱,纯属个人笔记,如有错误,欢迎大佬讨论!!!