转换构造函数

将其它类型转换为当前类类型需要借助转换构造函数(Conversion constructor)

拷贝构造函数

拷贝是在初始化阶段进行的,也就是用其它对象的数据来初始化新对象的内存。

  1. ​ Student stu2 = stu1; //调用拷贝构造函数
  2. ​ Student stu3(stu1); //调用拷贝构造函数

拷贝构造函数只有一个参数,它的类型是当前类的引用,而且一般都是 const 引用。

为什么是 const 引用呢?

添加 const 限制后,可以将 const 对象和非 const 对象传递给形参了,因为非 const 类型可以转换为 const 类型。

拷贝赋值构造函数

移动构造


demo get_demo(){
return demo();
}
int main(){
demo a = get_demo();
return 0;
}
// 1 执行 get_demo() 函数内部的 demo() 语句,即调用 demo 类的默认构造函数生成一个匿名对象;
// 2 执行 return demo() 语句,会调用拷贝构造函数复制一份之前生成的匿名对象,并将其作为 get_demo() 函数的返回值(函数体执行完毕之前,匿名对象会被析构销毁);
// 3 执行 a = get_demo() 语句,再调用一次拷贝构造函数,将之前拷贝得到的临时对象复制给 a(此行代码执行完毕,get_demo() 函数返回的对象会被析构);
// 4 程序执行结束前,会自行调用 demo 类的析构函数销毁 a。

目前多数编译器都会对程序中发生的拷贝操作进行优化,因此如果我们使用 VS 2017、codeblocks 等这些编译器运行此程序时,看到的往往是优化后的输出结果:

-fno-elide-constructors

利用拷贝构造函数实现对 a 对象的初始化,底层实际上进行了 2 次拷贝(而且是深拷贝)操作

所谓移动语义,指的就是以移动而非深拷贝的方式初始化含有指针成员的类对象。简单的理解,移动语义指的就是将其他对象(通常是临时对象)拥有的内存资源“移为已用”。

可以看到,在之前 demo 类的基础上,我们又手动为其添加了一个构造函数。和其它构造函数不同,此构造函数使用右值引用形式的参数,又称为移动构造函数。并且在此构造函数中,num 指针变量采用的是浅拷贝的复制方式,同时在函数内部重置了 d.num,有效避免了“同一块对空间被释放多次”情况的发生。

  1. ​ demo(demo &&d):num(d.num){
  2. ​ d.num = NULL;
  3. ​ cout<<”move construct!”<<endl;
  4. ​ }

当为 demo 类添加移动构造函数之后,使用临时对象初始化 a 对象过程中产生的 2 次拷贝操作,都转由移动构造函数完成。

当类中同时包含拷贝构造函数和移动构造函数时,如果使用临时对象初始化当前类的对象,编译器会优先调用移动构造函数来完成此操作。只有当类中没有合适的移动构造函数时,编译器才会退而求其次,调用拷贝构造函数。

image-20220819003402549


https://zhuanlan.zhihu.com/p/335994370

左值:左值可以取地址,位于等号左边,具有名字的.包括

  • 变量名
  • 返回左值引用的函数调用
  • int a = 0; int b = a;
  • 前置自增。
  • 赋值运算或复合赋值运算 ( i = 9) = 100; ( i += 10 ) = 1000;
  • 解引用

右值:右值只能在等号右边,不能取地址,不具有名字。

  1. 纯右值

    • 字面值
    • 返回非引用类型的函数调用
    • 后置自增
    • 算术表达式/比较表达式/逻辑表达式
  2. 将亡值(c11新引入的 与右值引用(移动语义)相关的值类型) 将亡值用来触发移动构造或移动赋值构造,并进行资源转移,之后值将调用析构函数

A a = A();
  • 同样的,a可以通过 & 取地址,位于等号左边,所以a是左值。
  • A()是个临时值,没法通过 & 取地址,位于等号右边,所以A()是个右值。

左值引用是对左值的引用;右值引用是对右值的引用。

引用是变量的别名,由于右值没有地址,没法被修改,所以左值引用无法指向右值。

const左值引用是可以指向右值的,const左值引用不会修改指向值,因此可以指向右值。但是引用又需要修改变量值,因此引出右值引用。

右值引用,右值引用的标志是&&,可以指向右值,不能指向左值。

int &&ref_a_right = 5; // ok

int a = 5;
int &&ref_a_left = a; // 编译不过,右值引用不可以指向左值

ref_a_right = 6; // 右值引用的用途:可以修改右值

右值引用有办法指向左值吗?std::move

int a = 5; // a是个左值
int &ref_a_left = a; // 左值引用指向左值
int &&ref_a_right = std::move(a); // 通过std::move将左值转化为右值,可以被右值引用指向

cout << a; // 打印结果:5

看上去是左值a通过std::move移动到了右值ref_a_right中,但事实上std::move移动不了什么,唯一的功能是把左值强制转化为右值,让右值引用可以指向左值。其实现等同于一个类型转换:static_cast<T&&>(lvalue)

同样的,右值引用能指向右值,本质上也是把右值提升为一个左值,并定义一个右值引用通过std::move指向该左值:

int &&ref_a = 5;
ref_a = 6;

等同于以下代码:

int temp = 5;
int &&ref_a = std::move(temp);
ref_a = 6;

被声明出来的左、右值引用都是左值,std::move会返回一个右值引用int &&,它是左值还是右值呢? 从表达式int &&ref = std::move(a)来看,右值引用ref指向的必须是右值,所以move返回的int &&是个右值。所以右值引用既可能是左值,又可能是右值吗? 确实如此:右值引用既可以是左值也可以是右值,如果有名称则为左值,否则是右值

或者说:作为函数返回值的 && 是右值,直接声明出来的 && 是左值。 这同样也符合第一章对左值,右值的判定方式:其实引用和普通变量是一样的,int &&ref = std::move(a)int a = 5没有什么区别,等号左边就是左值,右边就是右值。


最后,从上述分析中我们得到如下结论:

  1. 从性能上讲,左右值引用没有区别,传参使用左右值引用都可以避免拷贝。左值引用和右值引用都是左值。
  2. 右值引用可以直接指向右值,也可以通过std::move指向左值;而左值引用只能指向左值(const左值引用也能指向右值)。
  3. 作为函数形参时,右值引用更灵活。虽然const左值引用也可以做到左右值都接受,但它无法修改,有一定局限性。const右值可以修改。

3. 右值引用和std::move的应用场景

在实际场景中,右值引用和std::move被广泛用于在STL和自定义类中实现移动语义,避免拷贝,从而提升程序性能。 在没有右值引用之前,一个简单的数组类通常实现如下,有构造函数拷贝构造函数赋值运算符重载析构函数等。深拷贝/浅拷贝在此不做讲解。

在STL的很多容器中,都实现了以右值引用为参数移动构造函数移动赋值重载函数,或者其他函数,最常见的如std::vector的push_backemplace_back。参数为左值引用意味着拷贝,为右值引用意味着移动。

完美转发 std::forward

与move相比,forward更强大,move只能转出来右值,forward都可以。

std::forward(u)有两个参数:T与 u。 a. 当T为左值引用类型时,u将被转换为T类型的左值; b. 否则u将被转换为T类型右值。

void B(int&& ref_r) {
ref_r = 1;
}

// A、B的入参是右值引用
// 有名字的右值引用是左值,因此ref_r是左值
void A(int&& ref_r) {
B(ref_r); // 错误,B的入参是右值引用,需要接右值,ref_r是左值,编译失败

B(std::move(ref_r)); // ok,std::move把左值转为右值,编译通过
B(std::forward<int>(ref_r)); // ok,std::forward的T是int类型,属于条件b,因此会把ref_r转为右值
}

int main() {
int a = 5;
A(std::move(a));
}

左值引用和右值引用功能差异

  1. 左值引用避免对象拷贝:函数传参和函数返回值。

    T& f():返回类的引用可以作为左值,并且返回的类类型引用可以直接调用成员函数来修改,返回的类类型不会调用复制构造函数。

  2. 右值引用:实现移动语义和完美转发。

    移动语义:

    • 对象赋值的时候,避免资源的重新分配。
    • 移动构造以及移动拷贝构造
    • stl应用 alist.push_back(A()) 用了移动构造
    • std::unique_ptr

    实现完美转发:

    template<typename T>
    void revoke(T &&t) {
    func(forward<T>(t));
    }
    • 函数模板可以将自己的参数完美地转发给内部调用的其他函数。
    • 完美指不仅能转发参数的值,还能保证被转发的参数的左右值属性不变。
    • 借用万能引用,通过引用的方式接收左右值。
    • 引用折叠规则,参数为左值或者左值引用T&&会转化为int &,右值或右值引用转为int &&
    • std::forwad(v) T为左值引用,v将转为T类型的左值;T为右值引用,V将转为T类型的右值。forward作用是解引用。

    image-20220819101639616


https://www.bilibili.com/video/BV1bG411n79V?spm_id_from=333.880.my_history.page.click&vd_source=6c92aa3e5d0f2e0347ec135013a906d8


core dump(核心转储/吐核):是操作系统在进程收到某些信号而终止运行时,将此时进程地址空间的内容以及有关进程状态的其他信息写出的一个磁盘文件。这种信息往往用于调试。
C/C++程序常见coredump总结:

无效指针引起的程序coredump,大致可以分为4种原因引起异常。
(1)对空指针进行了操作。
(2)对一个未初始化的指针进行了操作。
(3)对一个已经调用了delete释放了内存的指针再次调用了delete去重复释放。
(4)多线程访问全局变量,导致内存值异常。

注:

  1. 存放Coredump的目录即进程的当前目录。(1)/proc/sys /kernel/core_uses_pid可以控制core文件的文件名中是否添加pid作为扩展。(2)proc/sys/kernel/core_pattern可以控制core文件保存位置和文件名格式。
  2. core文件的生成开关和大小限制:ulimit
  3. 用gdb查看core文件:gdb [exec file] [core file]

指针管理的困境

  1. 资源释放了,指针没有置空:
    • 野指针:如果未来接着使用这个内存,会判断存在。指针没初始化,不确定指向哪。未初始化的指针被称为野指针,可能是null,也可能指向合法内存(野)。
    • 指针悬挂:多个指针指向同一个资源,其中一个指针将资源删除且置空了,但其他指针不知道,还在使用。指针所指的内存空间已经删除,指针指向空间就不确定了。当指针所指向的对象被释放,但是该指针没有任何改变,以至于其仍然指向已经被回收的内存地址,这种情况下该指针被称为悬挂指针;
    • 踩内存:访问了不应该访问的内存
  2. 没有释放内存导致内存泄漏
  3. 重复释放资源引发coredump

怎么解决:RAII