临时对象
1、概念:
没有对象名,用完立即释放
2、示例代码:
#include <iostream>
#include <string.h>
using namespace std;
class Person
{
private:
char* name = nullptr;
int age;
public:
// 默认构造
Person()
{
cout << "调用 默认构造函数" << endl;
}
// 带参构造
Person(const char* name,int age):name(new char[strlen(name)+1]),age(age)
{
strcpy(this->name,name);
cout << "调用 name-age 带参构造函数" << endl;
}
// 析构函数
~Person()
{
if(this->name != nullptr)
{
delete []this->name;
this->name = nullptr;
}
cout << "调用 析构函数" << endl;
}
// 深拷贝
Person(const Person & p):name(new char[strlen(p.name)+1]),age(p.age)
{
strcpy(this->name,p.name);
cout << "调用 深拷贝" << endl;
}
};
Person get_Person()
{
return Person{"小龙",18};
}
int main()
{
Person p = get_Person();
return 0;
}
3.运行结果:
g++ 1-临时对象.cpp -std=c++11 -fno-elide-constructors // 禁止优化
执行: Person p = GetPerson(); 这段代码会调用 2 次拷贝构造函数 和一次构造函数
g++ 1-临时对象.cpp -std=c++11
执行: Person p = GetPerson(); 这段代码会调用 1 次构造函数
-fno-elide-constructors:编译选项,用于禁用编译器的返回值优化和复制消除,
在C++中,编译器会尝试优化返回值的创建,尤其是在临时对象的情况下,
通过省略拷贝构造函数的调用来避免不必要的对象复制。这种优化称为返回值优化,它可以显著提高程序的性能,减少内存消耗。
Person p = GetPerson();过程分析
1.进入 GetPerson() 函数,执行 Person{"小龙",18}; 这段代码将会调用 name-age 带参构造函数,定义初始化一个临时对象
2.return 这个临时对象: 此时,刚刚创建的临时对象 会被拷贝构造函数将他赋值给 GetPerson() 函数的返回值对象(又一个临时对象)
3.此时,第一次通过 name-age 带参构造函数 创建的这个临时对象作用结束,进行消亡,执行它的析构函数
4.Person p = get_Person(); 当函数返回了一个临时对象时,又调用了一次拷贝构造函数,将返回的这个临时对象赋值给 新的对象p。
5.赋值结束后,被返回的这个临时对象的作用结束,进行消亡,执行该对象的析构函数。
6.对象p 的生命周期结束,调用 p 的析构函数。
4.编译器的返回值优化和复制消除
实质: 尽可能减少或消除不必要的对象复制操作,从而提高程序的性能和效率。
具体实现: 通过在函数返回值的内部直接构造调用者要接收的对象,避免了中间对象的创建和拷贝,减少了不必要的内存和性能开销。
而复制消除是指编译器在编译过程中识别出不必要的对象复制,然后将其消除,避免了不必要的拷贝构造函数调用。
以 Person p = GetPerson(); 这段代码为例
eg:没有进行优化
①:
调用get_Person(),创建临时对象1
临时对象1.name = (char*)malloc(strlen("小龙")+1);
strcpy(临时对象1.name,"小龙");
临时对象1.age = 18;
②:
进入return 创建临时对象2
临时对象2.name = (char*)malloc(strlen(临时对象1.name)+1);
strcpy(临时对象2.name, 临时对象1.name);
临时对象2.age = 临时对象1.age;
③
析构临时对象1
free(临时对象1.name);
④
main 函数中 p 对象用来接收返回值
p.name = (char*)malloc(strlen(临时对象2.name)+1);
strcpy(p.name, 临时对象2.name);
p.age = 临时对象2.age;
⑤
析构临时对象2
free(临时对象2.name);
⑥
析构对象 p
free(p.name);
eg:使用编译器优化
①
调用 get_Person() 函数,不会产生临时对象的拷贝,返回的对象直接在 main 函数中的对象 p 的内存空间中构造。
②
在 main 函数中,对象 p 的内存空间中直接存储了字符串 "小龙" 和年龄 18。
③
在 main 函数结束时,对象 p 的析构函数被调用,释放 p 中的内存空间,不会有额外的析构步骤。
5.C++中右值引用
1.左值引用:
指向左值的引用,使用符号 '&' 声明;
左值: 指可以放在赋值运算符左边的表达式;
左值引用可以绑定到左值,并且可以修改器所引用的对象。
2.右值引用:
指向右值的引用,使用符号 '&&' 声明;
右值:是临时性的,通常是临时对象、字面量、函数返回的临时结果等...;
右值引用可以绑定到右值,但通常不可以修改其所引用的对象。
3.C++中左值引用和右值引用的区别:
1.左值引用指向左值,绑定到左值,右值引用指向右值,绑定到右值;
2.左值引用通常可以修改其引用的对象,用于传递可修改的对象,右值引用通常不可以修改其所引用的对象,用于支持移动语义和完美转发;
4.移动语义和完美转发:
移动语义和完美转发是 C++11 引入的两个重要概念,它们都涉及到改进和优化对象的传递和使用方式,提高了代码的效率和灵活性。
(1)移动语义:
①允许在不进行深拷贝的情况下转移对象的资源所有权。
②通过右值引用(Rvalue References)实现,允许程序员识别和利用临时对象(右值)。
③移动构造函数和移动赋值运算符是实现移动语义的关键。通过这两个特殊的成员函数,对象可以从临时对象“窃取”资源,而不是进行昂贵的深拷贝操作。
(2)完美转发:
①完美转发是一种技术,允许将参数以原样转发给其他函数,而不会丢失其值类别(左值还是右值)和常量性质。
②在 C++ 中,使用模板和引用折叠等特性可以实现完美转发。通常结合模板函数或模板类来实现。
③完美转发可以用于创建通用接口,例如,编写接受任意参数的函数或类模板时,可以使用完美转发来将参数转发给其他函数,从而保留其原始类型和特性。
移动语义使得资源管理更高效,而完美转发则允许编写更通用的代码,同时保留参数的类型信息和常量属性。
6.移动构造
移动构造的实质就是值传递,移动构造的过程中可能会改变当前对象中的值,所以,在移动构造中一般很少使用 const
右值引用就是为了去服务移动构造的
移动构造满足以下条件之后,编译器会给它写一个
1.没有用户声明的拷贝构造函数
2.没有用户声明赋值运算符重载
3.没有用户声明的移动构造函数
4.没有用户声明的析构函数
5.用户没有显示的声明移除移动构造
7.移动构造函数代码示例
#include <iostream>
#include <string.h>
using namespace std;
class Person
{
private:
char* name = nullptr;
int age;
public:
// 默认构造
Person()
{
cout << "调用 默认构造函数" << endl;
}
// 带参构造
Person(const char* name,int age):name(new char[strlen(name)+1]),age(age)
{
strcpy(this->name,name);
cout << "调用 name-age 带参构造函数" << endl;
}
// 析构函数
~Person()
{
if(this->name != nullptr)
{
delete []this->name;
this->name = nullptr;
cout << "释放堆空间" << endl;
}
cout << "调用 析构函数" << endl;
}
// 深拷贝
Person(const Person & p):name(new char[strlen(p.name)+1]),age(p.age)
{
strcpy(this->name,p.name);
cout << "调用 深拷贝" << endl;
}
// 移动构造函数
Person(Person && p)
{
this->name = p.name;
this->age = p.age;
p.name = nullptr;
cout << "调用移动构造函数" << endl;
}
};
Person get_Person()
{
return Person{"小龙",18};
}
int main()
{
Person p = get_Person();
return 0;
}
8.移动构造函数执行结果
9.结果分析
1.使用了移动构造函数:
(1)调用带参构造函数创建了临时对象,然后临时对象被移动构造函数转移给了变量 p,因此会调用移动构造函数。
(2)在移动构造函数中,指针 name 被转移,新对象的 name 指针指向旧对象 name 指针指向的空间,旧指针的 name 在使用完后直接指向 nullptr,
通过两次调用移动构造函数,最终 p->name指向了最初的临时变量开辟的 name 空间。
(3)最后,变量 p 的析构函数被调用,释放了 p 的资源。
2.未使用移动构造函数:
(1)先调用带参构造函数创建了临时对象。
(2)但由于没有移动构造函数,所以在返回临时对象时,会调用拷贝构造函数创建一个新的对象,这里开辟空间,进行深拷贝。
(3)接着临时对象的析构函数被调用释放了临时对象的资源。
(4)最后,变量 p 的析构函数被调用,释放了 p 的资源。
移动构造函数在使用过程中只是在更换指向堆空间的指针变量,而不需要每次都额外开辟堆空间,从而避免了不必要的深拷贝,提高程序的性能。
注意:在编写移动构造函数并涉及到堆空间的开辟时,一定要给该成员变量赋初值为 nullptr,并在临时变量使用完毕后将该成员变量指向 nullptr,
然后再 执行析构函数的时,对该成员变量进行 nullptr 值判断。否则,可能会造成调用析构函数,多次释放同一个空间!因为临时变量用完即销毁,
通过移动构造函数拿到的空间就是一个越界的空间!
移动构造函数在使用过程中只是在更换指向堆空间的指针变量,而不需要每次都额外开辟堆空间,从而避免了不必要的深拷贝,提高程序的性能。
注意:在编写移动构造函数并涉及到堆空间的开辟时,一定要给该成员变量赋初值为 nullptr,并在临时变量使用完毕后将该成员变量指向 nullptr,
然后再 执行析构函数的时,对该成员变量进行 nullptr 值判断。否则,可能会造成调用析构函数,多次释放同一个空间!
因为临时变量用完即销毁,通过移动构造函数拿到的空间就是一个越界的空间!