三五法则是针对C++中类的成员和类对象的操作函数。
三法则是指:拷贝构造函数、拷贝赋值运算符、析构函数。
五法则是在三法则的基础上增加了:移动构造函数、移动赋值运算符。
定义:如果构造函数的第一个参数是自身类类型的引用,且任何额外的参数都有默认值,则该构造函数时拷贝构造函数。如果没有为类定义拷贝构造函数,则编译器会默认生成拷贝构造函数。
用途:定义已存在的类对象,去初始化该类的新对象的成员初始化过程。
eg:
class Foo
{
public:
Foo(); //默认构造函数
Foo(const Foo&); //拷贝构造函数
//.....
}
拷贝初始化和直接初始化差异:
string dots(9,'.'); //直接初始化
string s(dots); //直接初始化
string s2 = dots; //拷贝初始化
stirng null_book = "hello world"; //拷贝初始化
string nines=string("9",'.'); //拷贝初始化
直接初始化是编译器选择与我们提供参数最匹配的构造函数来进行初始化。拷贝初始化是指将右侧运算对象拷贝到正在创建的对象中。注意两点:正在创建的对象、赋值符号=。
拷贝构造函数调用的情况有以下几种:
(1)使用“=”符号定义变量。
(2)将一个对象作为实参传递给一个非引用类型的形参。
(3)从一个返回类型为非引用类型的函数返回一个对象。
(4)用花括号列表初始化一个数组中的元素或一个聚合类中的成员。
定义:重载类对象赋值操作符“=”的函数,就是拷贝赋值运算符。简单的说就是重载运算符函数。形式如下:
class Foo
{
public:
Foo(); //默认构造函数
Foo(const Foo&); //拷贝构造函数
Foo& operator=(const Foo&); //拷贝赋值运算符
//.....
}
Foo& Foo::operator=(const Foo& org)
{
//.....
return *this; //注意:拷贝赋值运算符应该返回一个指向其左侧预算对象的引用this。
}
定义:释放对象使用的资源,并销毁对象的非static数据成员。
class Foo
{
public:
Foo(); //默认构造函数
Foo(const Foo&); //拷贝构造函数
Foo& operator=(const Foo&); //拷贝赋值运算符
~Foo(); //析构函数
int* ps;
//.....
};
Foo::~Foo()
{
delete ps;
}
注意:一个类中构造函数可以有多个,析构函数只有一个。智能指针时类类型,具有析构函数,所以在析构阶段会自动销毁,不需要手动释放,释放的一般都是new,malloc手动分配的堆内存。
通常,如果类需要定义析构函数,则该类就需要定义:拷贝构造函数和拷贝赋值运算符。
或者说,如果类的成员里面由指针成员变量,则该类需要定义:析构函数、拷贝构造函数、拷贝赋值运算符。(如果类的对象之间不存在赋值和初始化操作,则不用定义拷贝构造函数、拷贝赋值运算符,也应该阻止编译器生成默认的拷贝和赋值函数,阻止的方法是在函数的参数列表后面加上=delete。但该定义析构函数的情况,还是需要定义析构函数释放内存。)
class Foo
{
public:
Foo(); //默认构造函数
Foo(const Foo&)=delete; //阻止类对象的拷贝
Foo& operator=(const Foo&)=delete; //阻止类对象赋值
~Foo(); //析构函数
//.....
};
简单的来说就是将一个原先对象的管理权移动到另一个新对象上。原先被对象则不可再用,因为管理权已移动到新的对象上!!!
移动构造函数和移动赋值运算符的优点是不用深拷贝数据,直接将原先的对象数据移动到新的对象上,提高了效率。
需要定义移动构造函数的场景:自定义的数据如果要使用移动变量内存的方式来获取数据,则需要定义移动构造函数。因为自定义数据不支持移动,需要自定定义移动构造函数。标准库容器、string和shared_ptr类即支持移动也支持拷贝。
demo如下:
#include
#include
class SaleItem
{
public:
//1 如果有多个构造函数时,仍需要编译器默认的构造函数,则可以在声明时,在()后面加default,表示使用系统生成的默认构造函数。
//1 这种在声明时,写=default,表示构造函数为类的内联函数;如果不想用内联函数,在声明和定义分开写。
//1 默认的构造函数的形参为空。
SaleItem(){m_name = new int(10);};
//2 拷贝构造函数用于类类型对象的初始化操作。如果不定义会默认生成。
SaleItem(const SaleItem&);
//3、拷贝赋值运算符
SaleItem& operator=(const SaleItem& orig);
//4、析沟函数
~SaleItem(){if(m_n1 != NULL){delete m_name;m_name = NULL;} std::cout << " ~~~~free memory " << std::endl;};
void PrintMem(){std::cout << "m_n1 " << m_n1 << " m_n2 " << m_n2 << " m_name "<< *m_name << std::endl;}
//5、移动构造函数
SaleItem(SaleItem &&orig);
//6、移动赋值函数
SaleItem& operator=(SaleItem&& orig);
int m_n1=10;
int m_n2 = 1;
int *m_name = NULL;
};
SaleItem::SaleItem(const SaleItem& orig)
{
m_n1 = orig.m_n1+2;
m_n2 = orig.m_n2 *2;
auto newName = new int(*orig.m_name); //创建临时变量保存数据并开辟内存
if(m_name != NULL)
{
delete m_name; //释放原内存的释放
m_name = NULL;
}
m_name = newName;
std::cout << "Copy Constructor " << std::endl;
}
SaleItem& SaleItem::operator=(const SaleItem& orig)
{
std::cout << "Copy Assignment======= " << std::endl;
auto newName = new int(*orig.m_name); //创建临时变量保存待赋值数据并开辟内存,
if(m_name != NULL)
{
delete m_name; //释放原内存的释放
m_name = NULL;
}
m_name = newName;
return *this;
}
SaleItem::SaleItem(SaleItem &&orig)
{
if(m_name != NULL)
{
delete m_name;
m_name = NULL;
}
m_name = orig.m_name;
orig.m_name = NULL;
std::cout << "Move Constructor " << std::endl;
}
SaleItem& SaleItem::operator=(SaleItem&& orig)
{
if(m_name != NULL)
{
delete m_name;
m_name = NULL;
}
m_name = orig.m_name;
orig.m_name = NULL;
std::cout << "Move Assignment======= " << std::endl;
return *this;
}
int main(int argc,char** argv)
{
SaleItem obj1;
SaleItem obj2(obj1); //拷贝构造函---将类对象作为另一个函数的形参
obj2.PrintMem();
SaleItem obj3 = obj1; //拷贝构造函----使用已存在的对象赋值给类新创建的对象!!!
obj3.PrintMem();
obj3 = obj1; //拷贝赋值运算符-----使用已存在的对象赋值给另一个已存在的对象!!!
SaleItem objMo(std::move(obj1)); //移动构造函数
SaleItem objAssg;
objAssg = std::move(obj3); //移动赋值运算符
return 0;
}
运算结果如下:
附加:
可以参考的博客如下:
C++_拷贝赋值运算符详解_深入浅出_图文并茂_Dancing With Bugs的博客-CSDN博客_拷贝赋值运算符实现
C++中拷贝构造函数与赋值构造函数详解_m0_60150025的博客-CSDN博客_c++ 拷贝构造函数和拷贝赋值函数
移动复制构造函数与移动赋值构造函数 - 李兆龙的博客 - 博客园 (cnblogs.com)
C++11新特性:移动构造函数和移动赋值_简单l的博客-CSDN博客_移动构造函数和移动赋值函数
c++ 之 std::move 原理实现与用法总结_ppipp1109的博客-CSDN博客_stdmove
std::move()与移动构造函数_std::move 构造子类_瘦弱的皮卡丘的博客-CSDN博客