从此章我们即将开始第三部分的学习,之前我们已经学过了两个部分,C++基础和
C++标准库,第三部分为类设计者的工具
也就是我们即将开始传说中的对象对象编程之旅,面向对象程序设计(Object
Oriented Programming)
本章进行学习类如何操控该类型的拷贝,赋值,移动或者销毁,有:拷贝构造函数、移动构造函数、拷贝赋值运算符、移动赋值运算符以及析构函数等重要知识
定义:如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是构造拷贝函数
简单上手
//example1.cpp
class Person
{
public:
int age;
() = default;
Person(int age) : age(age) {}
Person(const Person &person)
Person{
//内容拷贝
this->age = person.age;
}
};
int main(int argc, char **argv)
{
(19);
Person person1= person1;
Person person2 << person2.age << endl; // 19
cout return 0;
}
默认情况下,编译器会定义一个拷贝构造函数,即使在我们提供拷贝构造函数的情况下也仍会自动生成,默认情况下会将每个非 static 成员拷贝到正在创建的对象中
//example2.cpp
class Person
{
public:
int age;
;
string name() = default;
Person(const Person &);
Person(const int age, const string name) : age(age), name(name)
Person{
}
};
//直接使用构造函数初始化列表
//此定义与默认合成拷贝函数相同
::Person(const Person &person) : age(person.age), name(person.name)
Person{
}
int main(int argc, char **argv)
{
(19, "gaowanlu");
Person me= me;
Person other // 19 gaowanlu
<< other.age << " " << other.name << endl;
cout return 0;
}
尝试测试一下编译器默认提供的合成拷贝构造函数,可见存在默认合成拷贝构造函数
如果不想让一个构造函数具有可以赋值转换的功能,则将其定义为 explicit
的
//example3.cpp
class Person
{
public:
;
string nameint age;
(const int age, const string name) : name(name), age(age) {}
Person};
int main(int argc, char **argv)
{
(19, "gaowanlu");
Person me= me;
Person other // 19 gaowanlu
<< other.age << " " << other.name << endl;
cout return 0;
}
重载operator=
方法进行自定义赋值运算符使用时要做的事情
//example4.cpp
class Person
{
public:
int age;
;
string name() = default;
Person(int age, string name) : age(age), name(name) {}
Person&operator=(const Person &);
Person };
&Person::operator=(const Person &person)
Person {
<< "operator =" << endl;
cout this->age = person.age;
this->name = person.name;
return *this;
}
int main(int argc, char **argv)
{
(19, "me");
Person person1;
Person person2= person1; // operator =
person2 << person2.age << " " << person2.name << endl; // 19 me
cout return 0;
}
与合成拷贝构造函数类似,如果没有自定义拷贝赋值运算符,编译器会自动生成
//example5.cpp
class Person
{
public:
int age;
;
string name() = default;
Person(int age, string name) : age(age), name(name) {}
Person};
int main(int argc, char **argv)
{
(19, "me");
Person person1;
Person person2= person1; //使用默认合成拷贝赋值运算符
person2 << person2.age << " " << person2.name << endl; // 19 me
cout return 0;
}
析构函数与构造函数不同,构造函数初始化对象的非 static 数据成员,还可能做一些在对象创建时需要做的事情。析构函数通常释放对象的资源,并销毁对象的非 static 数据成员
~TypeName();
析构函数没有返回值,没有接收参数,所以其没有重载形式
在构造函数中,初始化部分执行在函数体执行前,析构函数则是首先执行函数体,然后按照初始化顺序的逆序销毁。
构造函数被调用的时机
//example6.cpp
class Person
{
public:
int age;
;
string name() = default;
Person(int age, string name) : age(age), name(name) {}
Person~Person()
{
<< "~Person" << endl;
cout }
};
(Person person)
Person func{
return person;
}
int main(int argc, char **argv)
{
(19, "me");
Person person= func(person);
Person person1 //~Person被打印三次
//首先将person拷贝给func的形参,然后形参person作为返回值赋值给person1
//然后func返回值person被销毁
//随着main执行完毕,main内的两个Person被销毁
return 0;
}
当为自定义析构函数时,编译器会自动提供一个合成析构函数,对于某些类作用为阻止该类型的对象被销毁,如果不是则函数体为空
//example7.cpp
class Person
{
public:
int age;
;
string name() = default;
Person(int age, string name) : age(age), name(name) {}
Person~Person() {} //等价于合成析构函数
};
int main(int argc, char **argv)
{
(19, "gaowanlu");
Person person<< person.age << " " << person.name << endl; // 19 gaowanlu
cout return 0;
}
在合成析构函数体执行完毕之后,成员会被自动销毁,对象中的 string
被销毁时,将会调用 string 的析构函数,将 name
的内存释放掉,析构函数自身并不直接销毁成员
,是在析构函数体之后隐含的析构阶段中被销毁的,整个销毁过程,析构函数体是作为成员销毁步骤之外的并一部分而进行的
如果对象的内部有普通指针记录 new 动态内存,在对象析构过程默认只进行指针变量指针本身的释放,而不对申请的内存进行释放,则就需要动态内存章节学习的在析构函数体内手动释放他们,或者使用智能指针,随着智能指针的析构被执行,动态内存会被释放
有三个基本操作可控制类的拷贝操作:拷贝构造函数、拷贝赋值运算符、析构函数。在新标准下还可以通过定义一个移动构造函数、一个移动赋值运算符
我们发现有时赋值运算符与拷贝构造函数会执行相同的功能,通常情况下并不要求定义所有这些操作
使用合成拷贝函数和合成拷贝赋值运算符时可能遇见的问题
//example8.cpp
class Person
{
public:
int age;
*name;
string (const string &name = string()) : name(new string(name)), age(0) {}
Person~Person()
{
delete name;
}
};
int main(int argc, char **argv)
{
{
("me");
Person person1= person1; //使用合成拷贝构造函数
Person person2 //此时的person1.name与person2.name指向相同的内存地址
*person1.name = "he";
<< *person2.name << endl; // he
cout }
<< "end" << endl; // end
cout return 0;
}
在合成拷贝构造函数和合成拷贝赋值运算符,其中的拷贝操作都是简单的指针地址赋值,而不是重新开辟空间,再将原先的 name 赋值到新的内存空间
使用=default
可以显式要求编译器生成合成拷贝构造函数和拷贝赋值运算符
//example9.cpp
class Person
{
public:
() = default; //合成默认构造函数
Person(const Person &) = default; //合成拷贝构造函数
Person&operator=(const Person &); //合成拷贝赋值运算
Person ~Person() = default; //合成析构函数
};
//默认在类内使用=default的成员函数为内联的
//如果不希望是内联函数则应在类外部定义使用=default
&Person::operator=(const Person &person) = default;
Person
int main(int argc, char **argv)
{
;
Person person1= person1;
Person person2 << "end" << endl; // endl
cout return 0;
}
使用=delete
定义删除的函数
//example10.cpp
class Person
{
public:
() = default;
Person(const Person &) = delete; //禁止拷贝构造函数
Person&operator=(const Person &) = delete; //阻止拷贝赋值
Person ~Person() = default;
};
int main(int argc, char **argv)
{
;
Person person1// Person person2 = person1;//错误 不允许拷贝复制赋值
return 0;
}
析构函数不能是删除的成员
,否则就不能销毁此类型,没有析构函数的类型可以使用动态分配方式创建,但是不能被销毁
//example11.cpp
class Person
{
public:
int age;
;
string name(const int age, const string name) : age(age), name(name) {}
Person~Person() = delete;
};
int main(int argc, char **argv)
{
*person = new Person(19, "me");
Person // delete person;//错误 Person没有析构函数
return 0;
}
对于某些情况,编译器会将合成的成员定义为删除的函数
重点:如果一个类有数据成员不能默认构造、拷贝、复制、销毁,则对应的成员函数将被定义为删除的
在新标准之前没有,删除的成员,类是通过将其拷贝构造函数和拷贝赋值运算符声明为 private 的来阻止拷贝的
//example12.cpp
class Person
{
private:
(const Person &person);
Person&operator=(const Person &person);
Person
public:
int age;
;
string name(const int age, const string name) : age(age), name(name) {}
Person~Person() = default;
() = default;
Personvoid test();
};
::Person(const Person &person)
Person{
}
&Person::operator=(const Person &person)
Person {
return *this;
}
void Person::test()
{
*person = new Person(19, "me");
Person = *person; //函数成员或者友元函数可以使用
Person person1 delete person;
}
int main(int argc, char **argv)
{
(19, "me");
Person person1// Person person2 = person1;
// error: 'Person::Person(const Person&)' is private within this context
.test();
person1return 0;
}
这种虽然类的外部不能使用拷贝构造和拷贝赋值,但是类的友元和成员函数仍可使用二者,同时想要阻止友元函数或者成员函数的使用,则只声明 private 成员即可不进行定义
//example13.cpp
class Person
{
private:
(const Person &person); //只声明不定义
Person&operator=(const Person &person); //只声明不定义
Person
public:
int age;
;
string name(const int age, const string name) : age(age), name(name) {}
Person~Person() = default;
() = default;
Personvoid test();
};
int main(int argc, char **argv)
{
(19, "me");
Person person1// Person person2 = person1;
// error: 'Person::Person(const Person&)' is private within this context
// 如果函数成员或友元函数使用拷贝构造或者赋值 也会报错
return 0;
}
总之优先使用=delete 这种新的规范,delete 是从编译阶段直接解决问题
有些类拷贝是值操作,是一份相同得副本
//example14.cpp
class Person
{
public:
int *age;
*name;
string (const int &age, const string &name) : age(new int(age)), name(new string(name)) {}
Person() : age(new int(0)), name(new string("")) {}
Person&operator=(const Person &person);
Person ~Person()
{
delete age, delete name;
}
};
&Person::operator=(const Person &person)
Person {
*age = *person.age;
*name = *person.name;
return *this;
}
int main(int argc, char **argv)
{
(19, "me");
Person person1(20, "she");
Person person2= person2;
person1 << *person1.age << " " << *person1.name << endl; // 20 she
cout << *person2.age << " " << *person2.name << endl; // 20 she
cout *person1.name = "gaowanlu";
<< *person1.age << " " << *person1.name << endl; // 20 gaowanlu
cout << *person2.age << " " << *person2.name << endl; // 20 she
cout //可见之间此类对象像一种值类型
return 0;
}
有些类拷贝是指针指向的操作,也就是不同的类的成员会使用相同的内存
先来看一种简单使用的情况
//example15.cpp
class Person
{
public:
int *age;
*name;
string () : age(new int(0)), name(new string) {}
Person(const int &age, const string &name) : age(new int(age)), name(new string(name)) {}
Person&operator=(const Person &person);
Person };
&Person::operator=(const Person &person)
Person {
if (age)
delete age;
if (name)
delete name;
= person.age;
age = person.name;
name return *this;
}
int main(int argc, char **argv)
{
(19, "me");
Person person1= person1;
Person person2 // person1 person2的内容的内存是相同的
*person2.age = 20;
*person2.name = "gaowanlu";
<< *person1.age << " " << *person1.name << endl; // 20 gaowanlu
cout << *person2.age << " " << *person2.name << endl; // 20 gaowanlu
cout return 0;
}
有意思的例子是我们也可以设计引用计数的机制,通过下面这个例子可以学到很多的编程思想
//example16.cpp
class Person
{
public:
*name;
string int *age;
(const int &age = int(0), const string &name = string("")) : use(new size_t(1)), age(new int(age)), name(new string(name)) {}
Person//拷贝构造时
(const Person &person)
Person{
= person.name;
name = person.age;
age = person.use;
use ++*use; //引用数加一 不能写 *use++哦 因为那是*(use++)
}
//赋值拷贝时
&operator=(const Person &person);
Person //析构时
~Person();
size_t *use; //引用计数器
};
//拷贝赋值
&Person::operator=(const Person &person)
Person {
//递增右边对象的引用系数
++*person.use;
//递减本对象引用计数
--*use;
if (*use == 0)
{
delete age, delete name, delete use;
}
= person.age;
age = person.name;
name = person.use;
use return *this;
}
//析构
::~Person()
Person{
//将引用数减1
--*use;
//判断引用数是否为0
if (*use == 0)
{
delete age, delete name, delete use;
}
}
int main(int argc, char **argv)
{
(19, "me");
Person person1<< *person1.use << endl; // 1
cout {
(person1);
Person person2<< *person1.use << endl; // 2
cout *ptr = new Person(person2);
Person << *ptr->use << endl; // 3
cout delete ptr;
<< *person1.use << endl; // 2
cout }
<< *person1.use << endl; // 1
cout //最后当person1销毁时 析构函数内引用计数变为0 随之将内存释放 达到内存管理的效果
return 0;
}
可以在类上定义一个自己的 swap 函数重载 swap 默认行为
//example17.cpp
class Person
{
//声明为友元函数可访问类私有成员
friend void swap(Person &a, Person &b);
public:
int age;
;
string name(const int &age, const string &name) : age(age), name(name) {}
Person};
//定义函数 void swap(Person &a, Person &b);
inline void swap(Person &a, Person &b)
{
std::swap(a.age, b.age);
std::swap(a.name, b.name);
}
int main(int argc, char **argv)
{
(19, "me");
Person person1(19, "she");
Person person2(person1, person2);
swap<< person1.age << " " << person1.name << endl; // 19 she
cout << person2.age << " " << person2.name << endl; // 19 me
cout return 0;
}
类的 swap 通常用来定义它们的赋值运算符,是一种拷贝并交换的技术
//example18.cpp
class Person
{
friend void swap(Person &a, Person &b);
public:
int age;
;
string name&operator=(Person person);
Person (const int &age, const string &name) : age(age), name(name) {}
Person};
// person为使用合成拷贝构造函数值复制
&Person::operator=(Person person)
Person {
(*this, person); //二者内容交换
swapreturn *this;
}
// Person的swap行为
inline void swap(Person &a, Person &b)
{
.age = b.age;
a.name = b.name;
a}
int main(int argc, char **argv)
{
(19, "me");
Person person1= person1;
Person person2 << person2.age << " " << person2.name << endl; // 19 me
cout return 0;
}
什么是对象移动,也就是将对象移动到某处,即复制,但复制后就将原来的进行对象销毁了(本质上也不是销毁,要到后面的移动语义看一看),不要急先往后面看
标准库函数 std::move
,标准库容器、string、shared_ptr
类即支持移动也支持拷贝,IO 类和 unique_ptr 类可以移动但不能拷贝
//example19.cpp
#include <iostream>
#include <utility>
#include <string>
int main(int argc, char **argv)
{
using namespace std;
= "hello";
string a1 = std::move(a1);
string a2 << a1 << endl; // nothing
cout << a2 << endl; // hello
cout
int b1 = 999;
int b2 = std::move(b1); // int不是对象是基本数据类型不适用
<< b1 << endl; // 999
cout << b2 << endl; // 999
cout return 0;
}
C++中左值一般是指一个指向特定内存的具有名称的值,它有一个相对稳定的内存地址,有一段较长的生命周期,而右值则是不指向稳定内存地址的匿名值,生命周期短,通常是暂时的。可以简单的认为,可以取到左值的地址,但右值取不到地址
int x=1;//x左值 1右值
int y=3;//y左值 3右值
int z=x+y;//z左值 x+y右值
有趣的例子,++x 是左值其返回自身,而 x++将 x 拷贝了一份然后才对 x 递增,最后返回临时复制的内容
#include <iostream>
using namespace std;
int x;
int main(int argc, char **argv)
{
= 0;
x int *p1 = &x;
// int *p2 = &x++;//错误x++返回的是右值 无法取得地址
int *p3 = &++x;
<< p1 << " " << p3 << endl;
cout if (p1 == p3)
{
<< boolalpha << true << endl; // true
cout }
return 0;
}
函数的返回值是左值还是右值,不做特殊处理的话是右值
#include <iostream>
using namespace std;
int x;
int get_set(int val) // val为左值
{
= val;
x return x; // x为左值,但是返回的时候会将x复制一份然后返回,实际返回内容为右值
}
int main(int argc, char **argv)
{
(888); // 888是右值
get_setreturn 0;
}
通常字面量为右值,除字符串字面量以外,编译器会将字符串字面量存储到程序的数据段中,程序加载的时候也会为其开辟内存空间,所以可以使用取地址操作符获得字符串字面量的内存地址
#include <iostream>
using namespace std;
int main(int argc, char **argv)
{
auto str = &"hello world";
// const char (*str)[12] str
// str 指向长度12的char的const数组
<< str << endl; // 0x406045
cout auto str1 = &"hello world";
<< str1 << endl; // 0x406045
cout return 0;
}
什么是右值引用,右值引用为支持移动操作而生,右值引用就是必须绑定到右值的引用,使用&&而不是&来获得右值引用
左值与右值的声明周期,左值有持久的状态直到变量声明到上下文切换内存释放,右值要么是字面量或者求值过程中的临时对象
右值引用特性:
#include <iostream>
using namespace std;
int fun()
{
return 1;
}
int main(int argc, char **argv)
{
int num = 666;
int &ref = num; // 引用
// int &&refref = num; //错误:不能将右值引用绑定到左值上
// int &ref1 = num * 42; //错误:i*42为右值
const int &ref2 = num * 42; // const引用可绑定到右值上
int &&refref1 = num * 10; // 右值引用可以绑定在右值上
<< refref1 << endl; // 6660
cout = 999;
refref1 << refref1 << endl; // 999 而且与使用普通变量没什么区别
cout
// int &&refref2 = refref1; //错误:无法将右值引用绑定到左值
int &&rval_ref = fun();
// int &&rval_ref1 = rval_ref; // 无法将右值引用绑定到左值,即无法将右值引用绑到右值引用
return 0;
}
延长临时对象生命周期、同时往往可以做到减少对象复制提升程序性能
// g++ main.cpp -o main -fno-elide-constructors
#include <iostream>
using namespace std;
class X
{
public:
()
X{
<< "X()" << endl;
cout }
(const X &x)
X{
<< "X(const X &x)" << endl;
cout }
~X()
{
<< "~X()" << endl;
cout }
};
()//在函数fun内发生了两次构造,一次是x1构造时,一次是构造返回的右值时
X fun{
;
X x1return x1;
}
int main(int argc, char **argv)
{
&&x_temp = fun();
X << "hi" << endl;
cout return 0;
}
// X()
// X(const X &x)
// ~X()
// hi
// ~X()
有时后例如,我们自定义的类型,内部使用了动态内存,如果函数内部的一个此类的对象按值返回理应进行复制,但是在进行动态内存申请,然后拷贝操作,这也效率有点低,这时候移动语义就出来了
// g++ main.cpp -o main -fno-elide-constructors
#include <iostream>
#include <string.h>
using namespace std;
class X
{
public:
() : ptr(new char[1024])
X{
<< "X()" << endl;
cout }
(const X &x) : ptr(new char[1024])
X{
<< "X(const X &x)" << endl;
cout (ptr, x.ptr, 1024);
memcpy}
&operator=(const X &x)
X {
<< "operator=(const X &x)" << endl;
cout if (this != &x)
{
if (!ptr)
= new char[1024];
ptr }
(ptr, x.ptr, 1024);
memcpyreturn *this;
}
~X()
{
<< "~X()" << endl;
cout if (ptr)
delete[] ptr;
}
public:
char *ptr{nullptr};
};
()
X func{
;
X x(x.ptr, "hello world");
strcpyreturn x;
}
int main(int argc, char **argv)
{
&&x = func();
X return 0;
}
// X()
// X(const X &x)
//~X()
//~X()
1、X():这是在创建对象 x 时输出的。在 func()函数内部,X x;语句会调用默认构造函数 X::X()来创建对象 x。
2、X(const X &x):这是在执行 return x;语句时输出的。在返回 x 对象时,会调用拷贝构造函数 X::X(const X &x)来创建返回值对象。这是因为返回值是一个新的对象,需要通过拷贝构造函数来初始化它。
3、~X():这是在函数 func()结束时输出的。当函数结束时,局部变量 x 会被销毁,所以会调用析构函数 X::~X()来释放对象 x 的资源。
4、~X():这是在 main()函数结束时输出的。因为 x 是通过绑定到 func()的返回值产生的右值引用,当 main()函数结束时,会调用析构函数 X::~X()来释放对象 x 的资源。
可见尽管我们用了右值引用接受 func 返回值,但还是发生了内存拷贝,而且如果我们用左值引用(常量与非常量)、左值接受就会再增加一次拷贝,有没有可能把 func 内的 x 直接返回不发生拷贝呢,起始函数内返回是进行拷贝这一步并没有什么意义,这就要用移动构造函数与赋值构造函数。
移动构造函数通常在构建新的右值对象时调用。它接收一个右值引用参数,并且用于从该右值引用中接管资源,而无需进行深拷贝。
// g++ main.cpp -o main -fno-elide-constructors
#include <iostream>
#include <string.h>
using namespace std;
class X
{
public:
() : ptr(new char[1024])
X{
<< "X()" << endl;
cout }
(const X &x) : ptr(new char[1024])
X{
<< "X(const X &x)" << endl;
cout (ptr, x.ptr, 1024);
memcpy}
&operator=(const X &x)
X {
<< "operator=(const X &x)" << endl;
cout if (this != &x)
{
if (!ptr)
= new char[1024];
ptr }
(ptr, x.ptr, 1024);
memcpyreturn *this;
}
(X &&x) noexcept
X{
<< "X(X &&x)" << endl;
cout = x.ptr;
ptr .ptr = nullptr;
x}
&operator=(X &&x) noexcept
X {
<< "operator=(X &&x)" << endl;
cout = x.ptr;
ptr .ptr = nullptr;
xreturn *this;
}
~X()
{
<< "~X()" << endl;
cout if (ptr)
delete[] ptr;
}
public:
char *ptr{nullptr};
};
()
X func{
;//X()
X x(x.ptr, "hello world");
strcpyreturn x;//构造返回的右值时优先使用了移动构造函数
}
int main(int argc, char **argv)
{
&&x = func();
X return 0;
}
// X()
// X(X &&x)//根据移动语义,会优先选择使用移动构造函数来构造返回的右值,本质上是隐式调用移动构造函数
// ~X()
// ~X()
上面其实我们已经对左值与右值进行了讨论,但是右值引用绑定的对象是左值还是右值,它属于泛右值范畴为将亡值。
虽然不能将右值引用绑定在左值上,但可以通过 std::move
来实现,也就是将左值变为右值也就是将亡值,除了使用
std::move
还可以使用
static_cast<T&&>(X&)
//example21.cpp
int main(int argc, char** argv)
{
int num = 999;
// int &&rr1 = num; //错误 无法将右值引用绑定到左值
= "hello";
string stra && straRef = std::move(stra);
string<< "1 "<<stra << endl; // hello
cout << "2 "<<straRef << endl; // hello
cout
= "world";
stra << "3 "<<straRef << endl; // world
cout //可见straRef绑定定在了stra上
= "c++";
straRef <<"4 "<< stra << endl;//c++
cout
= "world";
string a = std::move(a); //move返回一个右值引用,调用string的移动构造函数构造b
string b // move函数的表现根据等号左侧的类型的不同随之行为也不同
<<"5 "<< a << endl; // nothing
cout <<"6 "<< b << endl; // world
cout
return 0;
}
话说将左值转为右值有什么用,下面我们来探究以下,首先就是我们可以将左值转为右值然后让一个对象接收,那么会优先调用此类对象的移动构造函数和移动赋值函数,这样一来就可以将一个对象的资源移动走
#include <iostream>
#include <string.h>
using namespace std;
class X
{
public:
() : ptr(new char[1024])
X{
<< "X()" << endl;
cout }
(const X &x) : ptr(new char[1024])
X{
<< "X(const X &x)" << endl;
cout (ptr, x.ptr, 1024);
memcpy}
&operator=(const X &x)
X {
<< "operator=(const X &x)" << endl;
cout if (this != &x)
{
if (!ptr)
= new char[1024];
ptr }
(ptr, x.ptr, 1024);
memcpyreturn *this;
}
(X &&x) noexcept
X{
<< "X(X &&x)" << endl;
cout = x.ptr;
ptr .ptr = nullptr;
x}
&operator=(X &&x) noexcept
X {
<< "operator=(X &&x)" << endl;
cout = x.ptr;
ptr .ptr = nullptr;
xreturn *this;
}
~X()
{
<< "~X()" << endl;
cout if (ptr)
delete[] ptr;
}
public:
char *ptr{nullptr};
};
int main(int argc, char **argv)
{
; // X()
X x1(x1.ptr, "hello world");
strcpy= x1; // X(const X &x)
X x2 = std::move(x1); // std::move(x1)返回右值引用 所以在构造x3时会使用X(X &&x)
X x3 << boolalpha << (x1.ptr == nullptr) << endl; // true
cout << x3.ptr << endl; // hello world
cout //~X() ~X() ~X()
return 0;
}
在函数返回值的应用,也可以使用
static_cast<T&&>
()
X func{
; // X()
X xreturn std::move(x); // X(X&&x)
} //~X()
int main(int argc, char **argv)
{
= func(); // X(X&&x) ~X()
X x1 = static_cast<X &&>(x1); // X(X&&x)
X x2 // ~X()
// ~X()
return 0;
}
在上面有些例子中会发现,当我们定义了移动构造函数时,构造函数临时返回值时会被优先选择,而不是用拷贝构造函数
#include <iostream>
using namespace std;
class X
{
public:
() = default;
X(const X &) = default;
X(X &&)
X{
<< "X(X &&)" << endl;
cout }
};
void func_(X x)
{
}
(X x)
X func{
return x;
}
int main(int argc, char **argv)
{
func_(X{}); // 用的拷贝构造,没用移动构造
<< "tag" << endl;
cout (X{}); // 用了X(X &&)
func// 说明func返回时构造临时返回右值时使用的移动构造函数构造的
// 这里使用了隐式移动
return 0;
}
还有会发现下面的代码,居然没有调用移动构造函数,这是因为 X{}本就是个右值被 func 的 x 又是右值引用,然后将 x 返回,那么返回时构造临时右值时不应该使用移动构造函数吗,当然不会啦,返回的 x 本来不就是右值啊
// g++ -fno-elide-constructors main.cpp -o main
#include <iostream>
using namespace std;
class X
{
public:
() = default;
X(const X &) = default;
X(X &&)
X{
<< "X(X &&)" << endl;
cout }
};
(X &&x)
X func{
return x;
}
int main(int argc, char **argv)
{
(X{});
funcreturn 0;
}
C++20 版本会对返回值返回与 throw 进行隐式的使用移动构造函数,但是其对操作的对象是有要求的,否则还是会用拷贝构造,以下情况会用移动代替复制
// g++ -fno-elide-constructors main.cpp -o main
#include <iostream>
using namespace std;
class X
{
public:
() = default;
X(const X &) = default;
X(X &&)
X{
<< "X(X &&)" << endl;
cout }
};
void func()
{
;
X xthrow x;
}
int main(int argc, char **argv)
{
try
{
();
func}
catch (const X &x)
{
<< "catched x" << endl;
cout }
try
{
;
X x1throw x1;
}
catch (const X &x)
{
<< "catched x" << endl;
cout }
return 0;
}
/*
X(X &&)
catched x
X(X &&)
catched x
*/
右值引用的最大贡献就是将临时变量的声明周期延长,减少临时变量的频繁销毁,内存的利用效率也会变高,当右值表达式被处理后结果存放在会块临时内存空间,右值引用指向它,则可以利用,直到指向它的右值引用全部被销毁,内存才会被释放
//example22.cpp
class Person
{
public:
;
string name(const string &name) : name(name)
Person{
<< "string &name" << endl;
cout }
(string &&name) : name(name)
Person{
<< "string &&name" << endl;
cout }
};
// const引用与右值引用重载时 传递右值时 右值引用的优先级高
int main(int argc, char **argv)
{
//创建临时变量"hello"
("hello"); // string&&name
Person person1
= "world";
string s (s); // string &name
Person person2return 0;
}
在旧标准中,右值可以调用相关成员函数与被赋值
//example23.cpp
int main(int argc, char **argv)
{
= "hello";
string hello = "world";
string world << (hello + world = "nice") << endl; // nice 右值被赋值
cout return 0;
}
怎样限定赋值时右边只能是可修改的左值赋值,引入了引用限定符(reference qualifier),使得方法只有对象为左值时才能被使用
//example24.cpp
#define USE_LIMIT
class Person
{
public:
;
string name#ifdef USE_LIMIT
&operator=(const string &) &; //引用限定符 等号左侧必须为可修改的左值
Person #else
&operator=(const string &);
Person #endif
(string &&name) : name(name)
Person{
}
inline void print()
{
<< this->name << endl;
cout }
};
#ifdef USE_LIMIT
&Person::operator=(const string &name) & //引用限定
Person #else
&Person::operator=(const string &name)
Person #endif
{
this->name = name;
return *this;
}
()
Person func{
return Person("me");
}
int main(int argc, char **argv)
{
() = "hello"; // func()返回右值
func//当define USE_LIMIT时发生错误
//没有define USE_LIMIT时不会发生错误
return 0;
}
一个方法可以同时用 const 和引用限定,引用限定必须在 const 之后
//example25.cpp
class Person
{
public:
int age;
;
string name(const int &age = 19, const string &name = "me") : age(age), name(name)
Person{
}
void print() const &
{
<< age << " " << name << endl;
cout }
};
//func返回右值
()
Person func{
return Person(19, "she");
}
int main(int argc, char **argv)
{
().print();
func//当print不是const&时报错,例如只有引用限定符&,只有const不报错
//很鸡肋没什么卵用
//当有const时 &限定作用消失了
return 0;
}
可以使用&&
进行方法重载,使其为可改变的右值服务
当一个方法名字相同 函数参数列表相同时
有一个有引用限定,全部都应该有引用限定或者全部都没有
//example26.cpp
class Foo
{
public:
() &&;
Foo sort() const &;
Foo sort//当一个方法名字相同 函数参数列表相同时 有一个有引用限定
//全部都应该有引用限定或者全部都没有
};
::sort() &&
Foo Foo{
<< "&&" << endl;
cout return *this;
}
::sort() const &
Foo Foo{
<< "const &" << endl;
cout return *this;
}
// func返回右值
()
Foo func{
return Foo();
}
int main(int argc, char **argv)
{
;
Foo foo1.sort(); // const &
foo1().sort(); //&&
func//如果没有定义Foo Foo::sort() && 二者都会调用 Foo Foo::sort() const &
return 0;
}
资源移动实例,嫖窃其他对象的内存资源,与拷贝构造函数类似,移动构造函数第一个参数也是引用类型,但只不过是右值引用,任何额外参数必须有默认实参
//example27.cpp
class Person
{
public:
int *age;
*name;
string (const int &age, const string &name) : age(new int(age)), name(new string(name))
Person{
}
//移动操作不应抛出任何异常
(Person &&person) noexcept //”盗窃“资源 这是个移动构造函数不是 拷贝构造函数
Person{
delete age, delete name;
= person.age;
age = person.name;
name .age = nullptr;
person.name = nullptr;
person}
void print();
};
void Person::print()
{
if (age && name)
{
<< *age << " " << *name;
cout }
<< endl;
cout }
int main(int argc, char **argv)
{
(19, "me");
Person person1= std::move(person1);
Person person2 .print(); // nothing
person1.print(); // 19 me
person2return 0;
}
与拷贝类似,可以也可以重载赋值运算符来实现对象的移动功能
//example28.cpp
class Person
{
public:
int *age;
*name;
string (const int &age, const string &name) : age(new int(age)), name(new string(name))
Person{
<< "Person(const int &age, const string &name)" << endl;
cout }
&operator=(Person &&person) noexcept;//移动赋值运算符
Person (const Person &person) : age(person.age), name(person.name)
Person{
<< "Person(const Person &person)" << endl;
cout }
void print();
};
&Person::operator=(Person &&person) noexcept
Person {
<< "Person &Person::operator=(Person &&person)" << endl;
cout if (&person != this)
{
delete age, delete name;
= person.age;
age = person.name;
name .age = nullptr;
person.name = nullptr;
person}
return *this;
}
void Person::print()
{
if (age && name)
{
<< *age << " " << *name;
cout }
<< endl;
cout }
//返回右值
()
Person func{
return Person(19, "she"); // Person(const int &age, const string &name) 2
}
int main(int argc, char **argv)
{
(18, "oop"); // Person(const int &age, const string &name) 1
Person person2= func(); // Person &Person::operator=(Person &&person)
person2 .print(); // 19 she
person2
= std::move(person2);//person2移动到person1
Person person1 << *person1.age << " " << *person2.name << endl; // 19 she
cout return 0;
}
只有当一个类没有定义任何自己版本的拷贝控制成员时,且所有数据成员都能进行移动构造或移动赋值时,编译器才会合成移动构造函数或移动赋值运算符
当一定义了拷贝控制成员,如自定义了拷贝构造拷贝拷贝赋值时,将不会提供合成的移动操作
//example29.cpp
class X
{
public:
int i; //内置类型可以移动
; // string定义了自己的移动操作
string s};
class HasX
{
public:
; // X有合成的移动操作
X member};
int main(int argc, char **argv)
{
;
X x1.i = 100;
x1.s = "me";
x1<< x1.i << " " << x1.s << endl; // 100 me
cout // X移动
= std::move(x1);
X x2 << x2.i << " " << x2.s << endl; // 100me
cout << x1.i << " " << x1.s << endl; // 100 nothing
cout // HasX移动
;
HasX hasx.member.i = 99;
hasx.member.s = "me";
hasx= std::move(hasx);
HasX hasx1 << hasx1.member.i << " " << hasx1.member.s << endl; // 99 me
cout return 0;
}
本质上 move 的使用就是调用了拷贝构造函数,但拷贝构造是值拷贝还是指针拷贝有我们自己定义
//example30.cpp
class Y
{
public:
int age;
() = default;
Y(const Y &y) //拷贝构造 则 Y没有合成的移动操作
Y{
this->age = age;
};
// Y(Y &&y)
// {
// age = y.age;
// y.age = 0;
// }
};
class HasY
{
public:
() = default;
HasY;
Y member};
int main(int argc, char **argv)
{
;
HasY hasy.member.age = 999;
hasy= std::move(hasy); //因为Y没有移动操作
HasY hasy1 << hasy1.member.age << endl; //乱码 hasy为一个新对象,为Y添加自定义移动构造函数则输出999
cout << "end" << endl; // end
cout return 0;
}
当一个类既有移动构造函数,也有拷贝构造函数,当我们使用哪一个,会根据函数匹配规则来确定
//example31.cpp
class Person
{
public:
() = default;
Person(const Person &person)
Person{
<< "Person(const Person &person)" << endl;
cout }
(Person &&person)
Person{
<< "Person(Person &&person)" << endl;
cout }
};
int main(int argc, char **argv)
{
;
Person person1(person1); // Person(const Person &person)
Person person2
const Person person3;
(person3); // Person(const Person &person)
Person person4
//而移动构造只接受右值
= std::move(person4); // Person(Person &&person)
Person person5
return 0;
}
要注意的是,当有拷贝构造函数没有移动构造函数时,右值也将被拷贝
//example32.cpp
class Person
{
public:
int age;
;
string name() = default;
Person(const Person &person) : age(person.age), name(person.name)
Person{
<< " Person(const Person &person)" << endl;
cout }
};
int main(int argc, char **argv)
{
;
Person person1// std::move的作用就是将person1作为右值传递
(std::move(person1)); // Person(const Person &person)
Person person2//当存在移动构造时则会优先使用移动构造
return 0;
}
当定义了移动构造函数,且定义了赋值运算符,但无定义移动赋值方法,则将一个右值赋给左值时,将会先使用移动构造函数构造新对象,然后将新对象赋值给原左值,类似地隐含了移动赋值
//example33.cpp
class Person
{
public:
int age;
;
string name() = default;
Person(const int &age, const string &name) : age(age), name(name){};
Person//移动构造函数
(Person &&p) noexcept : age(p.age), name(p.name)
Person{
<< "Person(Person &&p)" << endl;
cout .age = 0;
p.name = "";
p}
//拷贝构造
(const Person &p) : age(p.age), name(p.name)
Person{
<< "Person(const Person &p)" << endl;
cout }
//赋值运算符 也是 移动赋值运算符 也是拷贝赋值运算符
&operator=(Person p)
Person {
<< "Person &operator=(Person p)" << endl;
cout = p.age;
age = p.name;
name return *this;
}
};
int main(int argc, char **argv)
{
(19, "me"); //构造函数
Person person1//显式调用移动构造函数
(std::move(person1)); // Person(Person &&p)
Person person2<< person1.age << " " << person1.name << endl; // 0 nothing
cout
(19, "me"); //构造函数
Person person3; //默认构造函数
Person person4= std::move(person3); //先使用移动构造函数生成新对象 将新对象赋值给person4
person4 // Person(Person &&p) Person &operator=(Person p)
<< person4.age << " " << person4.name << endl; // 19 me
cout return 0;
}
移动迭代器解引用返回一个指向元素的右值引用
通过标准库make_move_iterator
函数可以将一个普通迭代器转换为移动迭代器返回
//example34.cpp
int main(int argc, char **argv)
{
<string> vec = {"aaa", "bbb"};
vectorauto iter = make_move_iterator(vec.begin());
// auto std::move_iterator<std::vector<std::string>::iterator>
<string> allocat;
allocator*ptr = allocat.allocate(10);
string (make_move_iterator(vec.begin()), make_move_iterator(vec.end()), ptr);
uninitialized_copy<< vec[0] << " " << vec[1] << endl; //空字符串
cout << ptr[0] << " " << ptr[1] << endl; // aaa bbb
cout //可见使用移动迭代器进行了移动操作
return 0;
}
本质上是标准库算法在背后使用了移动迭代器解引用,进而相当于
string_a=std::move(stringb),将 stringb 移动到了 stringa_
只有当数据类型支持移动赋值时移动迭代器才显得有意义