一、深拷贝浅拷贝
1.浅拷贝:
多个数据公用一段内存空间,如果一个数据释放了该空间,其他数据就找不到该数据,从而引发错误。
浅拷贝就像两个人共用一个衣柜,他们分别拥有相同的钥匙,但是某一天如果他们其中某个人偷偷的把锁换了,那么另外一个人去开柜子的时候就会遇到麻烦。这时候如果给两个人分配不同的衣柜(深拷贝),那么这个问题也就不存在了。
2.深拷贝
每一个数据都有各自的内存空间,一个数据释放内存空间不会影响其他数据。
3.构造函数、拷贝构造函数、移动构造函数、重载赋值、重载移动赋值
#include <iostream>
#include <string>
class A
{
private:
int* p = nullptr;
public:
A()
{
std::cout << "construction
";
p = new int(1);
}
A(const A & a) noexcept
{
std::cout << "copy construction
";
this->p = new int(*a.p);
}
//A(const A&& a) noexcept
//{
// std::cout << "move construction
";
// this->p = new int(*a.p);
//}
A& operator=(const A& a) noexcept
{
std::cout << "assignment
";
this->p = new int(*a.p);
return *this;
}
//A& operator=(const A&& a) noexcept
//{
// std::cout << "move assignment
";
// this->p = new int(*a.p);
// return *this;
//}
~A()
{
std::cout << "destruction
";
delete p;
p = nullptr;
}
};
int main()
{
{
A a; //普通构造函数
A b(a); //拷贝构造
A c(std::move(a)); //移动构造,没有移动构造函数则调用拷贝构造
A aa, bb, cc; //普通构造
bb = std::move(aa); //移动赋值,没有移动赋值运算符则调用普通赋值
cc = aa; //普通赋值
A dd = std::move(aa); //移动构造,没有移动构造函数则调用拷贝构造
A ee = aa; //拷贝构造,注意和A a;a = b;的区别
}
return 0;
}
二、析构函数
先执行函数体,然后按照初始化成员的逆序销毁成员。在这里需要注意的是销毁成员的并不是函数体,而是析构部分。
调用析构函数的时刻:
-
变量在离开其作用域时被销毁;
-
当一个对象被销毁时,其成员被销毁。
-
容器被销毁时,成员被销毁。
-
对于动态分配的对象,当对指向它的指针应用delete运算符时被销毁。
-
对于临时对象,当创建它的赛事表达式结束时被销毁。
注意点:
类里面有动态分配内存,一定要自己来定义析构函数(如果没有,一般情况下默认的析构函数就足够了)。
如果一个类需要定义析构函数,那么几乎可以肯定它也需要一个拷贝构造函数和一个赋值操作,类中有指针类型的成员,我们必须防止浅拷贝问题,所以,一定需要拷贝构造函数和赋值操作符,这两个函数是防止浅拷贝问题所必须的。需要拷贝操作的类也需要赋值操作,反之亦然。(三法则)
本质上,当不可能拷贝、赋值、或销毁类的所有成员时,类的合成拷贝控制函数就被定义成删除的了。
三、拷贝构造函数
移动构造函数和移动赋值运算符通常需要被显式声明为 “无异常抛出” ,拷贝函数不能加explicit
关键字
拷贝构造函数形参必须是自身类的引用:
函数实参与形参之间的值传递,是通过拷贝完成的。那么当我们将该类的对象传递给一个函数的形参时,会调用该类的拷贝构造函数,而拷贝构造函数本身也是一个函数,因为是值传递而不是引用,在调用它的时候也需要调用类的拷贝构造函数(它自身),这样无限循环下去,无法完成。
阻止拷贝:
- 阻止拷贝的例子:文件、网络通信连接
iostream类阻止拷贝,以避免多个对象写入或读取相同的IO缓冲,多线程编程中,thread调用函数的参数如果为IO必须使用std::move语义
定义删除的函数,可以通过将拷贝构造函数和拷贝赋值运算符定义为删除的函数来阻止拷贝。删除函数是这样一种函数:我们虽然声明了它们,但不能以任何方式使用它们。在函数的参数列表后面加上=delete来指出我们希望将它定义为删除的:
struct NoCopy
{
NoCopy()=default; //使用合成的默认构造函数
NoCopy(const NoCopy&)=delete; //阻止拷贝
//....
};
NonCopyable实现
class NonCopyable
{
protected:
NonCopyable() = default;
~NonCopyable() = default;
private:
NonCopyable(const NonCopyable&) = delete;
const NonCopyable& operator=( const NonCopyable& ) = delete;
};
构造和析构函数设置为protocted权限,这样就不能直接创建NonCopyable对象,只能由子类构造和析构函数调用它们。拷贝构造函数和拷贝赋值函数声明为private,子类不能进行拷贝操作
四、左值和右值
转载:C++中的左值和右值 | 转载:C++中的右值引用和移动
1.左值(lvalue):
简单的来说,能取地址的变量一定是左值,有名字的变量也一定是左值,最经典的void fun(p&& shit),其中shit也是左值,因为右值引用是左值(所以才会有move,forward这些函数的产生,其中move出来一定是右值,forward保持变量形式和之前的不变,就是为了解决右值引用是左值的问题)。至于为什么不能把等号左边看成左值,因为在C++中,等号是可以运算符重载的,等号完全可以重载成为等号左边为右值的形式。
左值是可以取地址的,这也是区分左值和右值的唯一正确的标志
注意:字符串是左值,字面值常量是右值。例如:"Hello World!"
是左值,666,9.9,true,false,nullptr
都是纯右值
2.右值(rvalue):
当函数返回一个值时编译器会创建一个零时且完整的对象,这个对象就是一个右值
- prvalue(纯右值):
纯右值是传统右值的一部分,纯右值是表达式产生的中间值,不能取地址
- rvalue(右值):
和左值不同,右值不可以取地址,右值不能在内建等号的左边,如果运算符重载右值是可以在等号左边的
3.消亡值(xvalue):
本质上,消亡值就是通过右值引用产生的值。右值一定会在表达式结束后被销毁,比如return x(x被copy以后会被销毁), 1+2(3这个中间值会被销毁)。
4.右值引用
&&
5.引用和std::ref以及std::cref
转载:std::move、std::ref和std::bind
std::ref只是尝试模拟引用传递,并不能真正变成引用,在非模板情况下,std::ref根本没法实现引用传递,只有模板自动推导类型时,ref能用包装类型reference_wrapper来代替原本会被识别的值类型,而reference_wrapper能隐式转换为被引用的值的引用类型。std::ref主要是考虑函数式编程(如std::bind)在使用时,是对参数直接拷贝,而不是引用。比如thread的方法传递引用的时候,必须外层用ref来进行引用传递,否则就是浅拷贝。
std::ref用于包装按引用传递的值。std::cref用于包装按const引用传递的值
五、std::move和std::forward
std::move和std::forward本质就是一个转换函数,std::move执行到右值的无条件转换,std::forward执行到右值的有条件转换,在参数都是右值时,二者就是等价的
完美转发 std::forward
移动语义 std::move
move提供了一个更加智能的传递重量级对象的方法。你只需要创建你的重量级资源一次,然后在任何需要的地方移动即可。就像我之前说的,move不只是用于类,只要在你需要改变一个资源的拥有者时都可以使用move。记住,跟指针不一样的是:move不会分享任何东西,如果对象A从对象B中偷取了数据,对象B中的数据就不再存在了,因此也就不再合法了。我们知道在处理临时对象时这没有问题,但是在从常规对象身上偷取数据时就需要慎重了。
int main()
{
Holder h1(1000); // h1是一个左值
Holder h2(h1); // 调用了拷贝构造函数(因为输入参数是左值)
}
int main()
{
Holder h1(1000); // h1是一个左值
Holder h2(std::move(h1)); // 调用了移动构造函数(因为输入参数是右值)
}
五法则:任何想要移动语义的类必须声明全部五个特殊成员函数:
-
拷贝构造函数(copy constructor)
-
移动构造函数(move constructor)
-
拷贝赋值运算符(copy-assignment operator)
-
移动赋值运算符(move-assignment operator)
-
析构函数 (destructor)
注意和算法库<algorithm>中的std::move的区别
六、常引用、引用、传引用、传值
引用本质只是另一个对象的别名。对引用别名的操作即是对本身变量的操作。
常引用只能指向一个常量对象,不能指向另一个常量对象。