发布于2021-07-24 21:04 阅读(657) 评论(0) 点赞(29) 收藏(3)
1).很多语言并没有赋予类设计者控制,对象拷贝,赋值,移动和销毁时做什么的能力。
1).学习类如何控制该类型对象的拷贝,赋值,移动,销毁。
1).拷贝构造函数。
{
class Foo {
public:
Foo ();//此为默认构造函数
Foo (const Foo &);//此为拷贝构造函数,
// 并且这个拷贝构造函数和 合成的拷贝构造函数hi是一样的。
//等价于
}
}
const
引用的拷贝构造函数,但是此参数几乎总是一个const
的。explicit
的。static
成员拷贝到正在创建的对象中去。{
Sales_data (const Sales_data &);
//以上的拷贝构造函数就等价于,
//与默认构造函数是一样的。
//对每一个成员进行拷贝。
Sales_data (const Sales_data &orig) :
bookNo(orig.bookNo),
unit_sold(orig.unit_sold),
revenue(orig.revenue)
{ }
}
2).什么是拷贝,什么是直接赋值。
{
string null_book = "123";//拷贝初始化,经历完整的两个过程
//等价于
string temp("123");//构造string,隐式转换
null_book = temp;//拷贝
//以下的例子也是一样的
//均调用了构造函数和拷贝构造函数。
string s = string(12,'1');
string s = string("123");
}
{
//两个例子
string s(10,'1');
string s("123");
//这个例子比较特殊(里面就是一个对象)。
string s1(s);
//拷贝(书本有误。)。
}
3).什么时候发生拷贝?
insert
,push
函数成员,容器对元素是拷贝初始化。emplace
创建元素,是直接初始化。4).在拷贝初始化时,编译器跳过拷贝/移动构造函数,直接构造对象。
public
),{
string s = "111";//拷贝初始化
//编译器进行改写
string s("111");//略过拷贝构造函数
}
5).是拷贝还是直接初始化。(总结)
string s = "1234";
)=
还是()
。同类型对象都是拷贝。insert
,emplace
)练习,
=
定义变量,就是拷贝初始化(有可能包含隐式转换。)。{
//构造函数是不是explicit的区别
shared_ptr<int> p = new int(12);//错误,构造函数是只能显式转换的
shared_ptr<int> p(new int(12));//直接构造。
}
shared_ptr
成员时。类的对象进行拷贝时,利用拷贝构造函数,对成员逐一拷贝。那么shared_ptr
的引用计数会加一。上机证明,类使用()
或者=
,引用计数都增加了。()
里面是一个同类型对象时,就是拷贝,调用拷贝初始化。这是特殊的。(其他的调用的是构造函数。)C *p = new C(c);
也是一次拷贝。申请一块空间进行拷贝。args
构造的,才是直接初始化。对于同类对象之间的,或者先类型转换为同类型的,都是拷贝初始化。{
class H {
public:
H(const H ©) {
i = copy.i;
p = new string(*(copy.p));//指向的是自己一个新的空间,只是内容一样。这一点与默认的是不一样的。
}
private:
int i;
string *p;
};
}
1).默认的拷贝赋值运算符。
static
的成员逐一拷贝。(与拷贝构造函数的作用是一样的。){
//等价于合成的拷贝赋值运算符
Sales_data &
Sales_data::operator= (const Sales_data &s) {
bookNo = s.bookNo;//调用的是string::operator=
units = s.units;//调用的是内置的int赋值
revenue = s.revenue;//调用的是内置的double赋值
return *this;//返回的是左侧运算对象的引用。
}
}
2).重载赋值运算符。
operator
加上它要重载的运算符号。operator=
的函数。它也有返回类型,参数列表,函数体。*this即可。
这一点是为了与内置的=
保持一致。)和左侧的运算对象。**如果一个运算符是一个成员函数,其左侧的运算对象就绑定到隐式的this
。并且对于一个二元运算符号,例如=
。其右侧的运算对象就是显式传递参数(形参)。3).拷贝赋值运算符
{
class F {
public:
F &operator=(const F&);
};
}
4).注意事项。
练习,
=
,则两个对象的指针指向的是同一个地方。){
H &
H::operator= (const H&) {
auto newsp = new string(*(H.sp));//构建的指针。
delete sp;//销毁旧得内存地址
sp = newsp;
i = H.i;
return *this;
}
}
1).什么时候进行析构?
delete
时,它所指向的元素会被销毁。如果对象为类类型则会调用析构函数。{
Sales_data *p = new Sales_data;//内置指针
auto p2 = make_shared<Sales_data>();
Sales_data item(*p);//拷贝
vector<Sales_data> vec;
vec.push_back(*p);
delete p;//Sales_data调用析构函数
}
//离开作用域,p2被销毁,引用减一为0,指向的类销毁执行析构,对于string成员,调用string析构函数
//vector被销毁,元素也被销毁,类类型元素调用自身析构函数,对string类型成员调用string析构函数
// 容器,数组,被销毁,它的每一个元素也被销毁。
2).智能指针
delete
,而是直接由于作用域被销毁,那么它所指对象不会销毁,不会调用析构函数。3).自定义的析构函数。
static
成员。static
数据成员。?{
class F {
public:
~F ();
};
}
4).合成析构函数。
{
class F {
public:
~F () {};
};
}
练习,
{
~F(){delete sp;}
}
class(class);
就是拷贝。{
c b;//默认
c *a = new c(b);//拷贝
c *a = new c;//默认
}
1).几个原则。
delete
一个动态内存指针,而一个指针不能重复delete
,所以必然有赋值时,对每一个对象都是不一样的动态指针。而这是合成的拷贝构造函数以及拷贝赋值运算符所不能完成的。练习,
{
void f(C c) {cout << c.num << endl;}
numbered a,b = a,c = a;
f(a),f(b),f(c);//一共进行了5此拷贝。假定num从零开始,由于构造函数时合成的,
// 只有拷贝构造函数是编号的,且从0开始,
// 输出结果是 3,4,5
}
1).几点注意
=default
。因为只有它们是有合成版本的成员函数。=default
显式地要求编译器生成合成的版本。{
class Sales_data {
Sales_data() = default;//隐式内联的
Sales_data(const Sales_data &) = default;//同
Sales_data& operator=(const Sales_data &);//类外,不是内联的。
};
Sales_data& Sales_data::operator = (const Sales_data&) = default;
}
1).阻止拷贝的两种方式。(有些类的有些操作是没有意义的,所以需要阻止。(例如,istream
))
=delete
,(定义为删除函数,虽然声明了,但是不可以使用,同时还阻止了编译器进行合成。)delete
必须出现在第一次出现时,因为这关乎整个操作,这不像default
是只影响单个成员,只为单个成员生成代码。{
struct NoCopy {
NoCopy() = default;
NoCopy(const NoCopy &) = delete;//不允许拷贝初始化。
NoCopy& operator=(const NoCopy &) = delete;//不允许赋值拷贝
~NoCopy() = default;
};
}
private
),并且不定义它,防止定义了它,友元可以进行拷贝或者赋值。**一般只声明一个函数而不定义它是合法的。但是有一个例外。**这样做,如果友元试图拷贝,会编译报错,没有函数定义(链接时错误)。{
class p {
p(const p &);
p& operator=(const p &);
}
}
2).我们可以对任何一个成员函数使用=delete;
3).合成的函数也可能时delete
的。
delete
或者不可以访问的,那么该类对应得函数也是delete
。因为类的操作是需要对每一个成员进行。delete
或者不可访问的,那么类的默认构造,拷贝是delete
的,防止构建一堆不能析构的对象。const
成员或引用,那么合成默认函数是delete
的。const
的或者引用,那么拷贝构造和赋值运算符都是delete
的。因为const
不能拷贝;引用的拷贝和我们的意图相反,这样做会使得所有的引用都指向同一个对象,容易犯无效引用的错。4).析构函数是delete
,我们可以设置它的动态指针。但是不能使用delete
。
{
struct s {
s() = default;
~s() = delete;
};
s *p = new s();//有默认构造函数,如果是而合成的,那么这句也是错误的
}
练习,
static
数据成员来生成一个唯一的编号。{
class s {
static int sn;
public:
s() {num = sn++;}
s(const string &) {name = s;num = sn++;}
private:
string s;
int num;
};
}
1).拷贝形式。
IO
类型和unique_ptr。
1).注意事项。
{
s& s::operator=(const s &a) {
auto newp = new string(*(a.p));
delete p;
p = newp;
return *this;
//而如果是这样
delete p;//string释放
p = new string(*(a.p));//错误,p所指的和自身的一样的,
// 此时,数据已经丢失了,这指针是空悬指针
}
}
1).
shared_ptr
的思想。使用我们自己定义的引用计数。use
使用指针避免发生不统一的情况。C a,b(a),c(a);//如果不是指针,那么b将会无法更新。
shared_ptr
的强大之处。string
的构造函数,有接受对象的拷贝构造函数。{
class HasPtr {
public:
HasPtr(const string &s = string()) :
ps(new string(s)),i(0),use(new size_t(1)) {}
HasPtr(const HasPtr &) :
ps(p.ps),i(p.i),use(p.use) {++(*use);}//拷贝构造函数。是不创建指针的。直接赋值
// 这也说明了拷贝构造函数只用在初始化的事实。
HasPtr& operator=(const HasPtr &);
~HasPtr();
private:
string *ps;
int i;
size_t *use;//使用指针
};
HasPtr::~HasPtr() {
//引用计数变为0
// 释放相应的内存。
if (--(*use) == 0) {
delete ps;
delete use;
}
}
HasPtr& HasPtr::operator= (const HasPtr &d) {
++(*d.use); //为了防止一个对象复制给自身时发生错误。
if (--(*use) == 0) {
delete use;
delete ps;
}
use = d.use;
i = d.i;
ps = d.ps;
return *this;
}
}
练习,
delete
根节点的指针。递归操作。1).为什么要自定义swap
。
swap
,如果我们没有自己定义就会使用标准库里面的版本。而标准库里面的版本,就是三行式,需要建立一个temp
,有些时候我们不需要这样浪费,因为我们只需要交换指针就可以了。swap
声明为inline
,优化代码。swap
,但是是一种重要优化手段。{
class HasPtr {
friend void swap(HasPtr &,HasPtr &);
HasPtr(const HasPtr &d) : ps(new string(*(d.ps))) {}
private:
string *ps;
};
//注意格式
inline
void swap(HasPtr &L,HasPtr &R) {
using std::swap;
swap(L.ps,R.ps);
swap(L.i,R.i);
}
}
2).到底是哪一个swap
。(类中的数据成员)
swap
函数的中的swap
到底是std::
里面的还是类自己定义的,取决于std::
swap
swap
前面指定std
;因为这样做和使用默认的swap
是一样的。**不带有std
,会优先匹配类中的版本,然后在是std
中的版本。(using
语句是否必不可少?)using std::swap;
也可以正常地调用类中的版本?。{
class C {
friend void swap(C &,C &);
HasPtr p;
};
// 定义C中的swap函数
void swap(C &L,C &R) {
using std::swap;
swap(L.p,R.p);//调用的是HasPtr自己定义版本。
// 以下是错误的。
std::swap(L.p,R.p);
// 虽然可以编译通过,正常运行,
// 但是不是我们想要的性能。
// 这就是默认的版本,会进行拷贝初始化。建立不必要的内存。
}
}
3).在赋值运算符中使用swap
,拷贝(实参到形参)并交换(形参和左运算对象)的技术。
new string(*(d.ps));
初始化语句。即使出现异常也是在swap
之前,对原来的没有影响。{
// 注意到这里是一个副本。而不是引用
HasPtr& operator=(HasPtr R) {
swap(*this,R);
return *this;
}
}
1).应用
message
是消息。folders
是目录。message
可以在很多的folders
中,并且一个message
只有一个副本,实现的是共享。{
class Message {
friend class Folder;
public:
// 也是一个默认构造函数
explicit Message (const string &str = "") :
contents(str) {}
Message(const Message &);
Message& operator=(const Message &);
~Message();
void save(Folder &);//添加文本
void remove(Folder &);//删除文本
private:
string contents; //消息文本
set<Folder *> folders; //包含message的folders
void add_to_folders(const Message &);
void remove_form_folders();
};
void Message::save(Folder &f) {
folders.insert(&f); //添加序列,文件的指针
f.addMsg(this); //添加到f中
}
void Message::remove(Folder &f) {
folders.erase(&f); //删除指定关键词(也就是文件指针)
f.remMsg(this); //this也是指针
}
}
2).拷贝控制成员。
folder
中,也就是说,需要遍历右侧的set
的每一个folder
指针。private
中。{
//完成private实现部分
void Message::add_to_folder(const Message &m) {
for (auto f : m.folders)
f->addMsg(this);
}
// 完成拷贝构造函数。
Message::Message(const Message &m) :
contents(m.message),folders(m.folders) {
add_to_folder(m);
}
}
3).析构函数。
message
中的set
的文件删除该message
。{
// 这是不封装的。
Message::~Message() {
for (auto f : this->folders) {
f.remMge(this);
}
}
//进行封装。
void Message::remove_from_folders () {
for (auto f : this->set) {
f.remMsg(this);
}
}
Message::~Message() {
remove_form_folders();
}
}
4).拷贝赋值运算符
message
的set
所以这样操作是合理的。{
Message& Message::operator=(const Message &m) {
remove_form_folders();
// 进行拷贝
this->folders = m.folders;
this->contents = m.contents;
add_to_folders(m);
return *this;
}
}
5).Message
结合swap
函数
string
和set
版本的swap
函数。{
void swap(Message &L,Message &R) {
// using严格来说并不需要,但是这是一个好习惯。
using std::swap;
// 先将每一个消息从它们的folders中删除
for (auto f : L.folders) {
f->remMsg(&L);
}
for (auto f : R.folders) {
f->remMsg(&R);
// 交换指针和内容
}
swap(L.contents,R.contents);
swap(L.folders,R.folders);
// 添加回去。
for (auto f : L.folders) {
f->addMsg(&L);
}
for (auto f : R.folders) {
f->addMsg(&R);
}
}
}
练习,
Folder&
进行&
运算,得到的就是它本体的地址。Folder
的实现也需要将自己添加/删除到每一个message
;方便进行拷贝等。1).如果一个类在运行时需要分配大小可变的内存空间
vector
来实现2).类vector
内存分配思想构造自己的StrVec
类进行动态内存分配。
vector
的仿造。{
class StrVec {
public:
StrVec() :
elements(nullptr),first_free(nullptr),cap(nullptr) {}
StrVec(cosnt StrVec&);
StrVec& operator=(const StrVec &);
~StrVec();
void push_back(const string &);
size_t size() const {
return first_free - elements;
}
size_t capacity() const {
return cap - elements;
}
string* begin() const {
return elements;
}
string* end() const {
return first_free;
}
};
private:
//分配内存空间的元素
Static allocator<string> alloc;
//查询是否需要重新分配内存。
void chk_n_alloc() {
if (size() == capacoty()) reallocate();
}
//工具函数。
pair<string *,string *> alloc_n_copy (const string *,const string *)
void free(); //销毁元素并释放内存
void reallocate(); //获取更多的空间并进行拷贝原有的元素
string *elements; //指向数组的首元素。
string *first_free; //第一个未定义的空间
string *cap; //空间的尾后迭代器
}
3).push_back
操作。
reallocate
allocator.construct()
construct
是进行构造。这里由于是同类型的对象,所以调用的是拷贝构造函数。{
void StrVec::push_back(const string &s) {
ch_n_alloc(); //确保有空间
alloc.construct(first_free++,s);
};
}
4).alloc_n_copy
成员
StrVec
时,需要调用这个函数。vector
类似,它具有类值得行为。所以是拷贝。分配新的内存空间。{
pair<string *,string *>
StrVec::alloc_n_copy(cosnt string *b,const string *e) {
auto data = alloc.allocate(e - b);
return {data,uninitialized_copy(b,e,data)};
// 返回的是一个新的,begin和end指针对
}
}
5).free
成员。
string
元素被销毁会调用自己的析构函数。{
void StrVec::free (){
// 不能给deallocate一个空的指针。
if(elements) {
for (auto p = first_free;p != elements; )
alloc.destroy(--p);
alloc.deallocate(elements,cap-elements);
}
}
}
6).拷贝控制成员。
{
// 拷贝构造函数。
StrVec::StrVec(const StrVec &d) {
auto newData = alloc_n_copy(d.begin(),d.end());
// 注意返回的是一个pair
elements = newData.first;
// 此时first_free和cap在一个位置。
first_free = cap = newData.second;
}
// 析构函数
StrVec::~StrVec() {free();}
// 拷贝赋值运算符
StrVec& StrVec::operator=(const StrVec &d) {
auto newData = alloc_n_copy(d.begin(),d.end());
free();
element = newData.first;
first_free = cap = newData.second;
return *this;
}
}
7).编写reallocate
成员。
{
// 这样做的性能不高。因为需要将原来的数据进行拷贝转移,还要将原来的空间进行释放。
// 如果可以避免拷贝和释放string的额外开销,那将提升性能。
// 注意这里没有做size的判断,是错误的
void StrVec::reallocate() {
auto newSpace = alloc.allocate(end()*2 - begin());
auto newSpace = alloc_n_copy(begin(),end(),newSpace.first);
free();
element = newSpace;
cap = newSpace + end()*2 - begin();
first_free = newData.second;
}
}
8).移动构造函数和std::move
string
,都定义了所谓的**移动构造函数。**具体细节未公开。string
看成是含有c
风格的指针成员的类,它管理着内存,当我们move
时,与前一个版本相比,相当于是进行了指针的移动,而指针所指内容是不移动的。原因是,使用了移动构造函数,string
管理的内存空间不会被拷贝,而是构造的每一个string
都会从原来的string
中接管内存的所有权。原来的string
不再管理原来的内存了。而且更加神奇的是,对于旧的string
,我们执行free
时,会正确执行string
的析构函数(即保持有效的,可以析构的状态);但是我们不知道,经过移动之后旧的string
里里面内容是什么。move
是一种标准库函数,定义在头文件utility
中。reallocate
在新内存中构造string
时,必须调用move
表示我们希望使用的是string
的移动构造函数。如果漏掉了,就会执行string
的拷贝构造函数。move
的使用不需要加上using
声明,但是必须是std::move;
而不是move
。{
void StrVec::reallocate() {
auto newCapacity = size() ? 2 * size() : 1; //当前大小大两倍的空间,注意0的时候。
auto newData = alloc.allocate(newCapacity);
auto dest = newData;
auto elem = elements;
for (size_t i = 0; i != size(); ++i)
alloc.construct(dest++,std::move(*elem++));
free();//移动完成,释放旧的内存
elements = newData;
first_free = dest;
cap = newData + newCapacity;
}
}
1).为什么移动?
IO
类以及unique_ptr
没有拷贝(有些不能共享的资源,IO
缓冲,指针。),怎么解决。share_ptr
,string
,标准库容器,支持拷贝也支持移动;但是IO
类,unique_ptr
,只支持移动,不支持拷贝。1).哪些是左值,哪些是右值。
{
StrVec v1,v1; //默认构造函数
v1 = v2;//拷贝赋值运算
StrVec getVec(istream &);
v2 = getVec(cin); //移动赋值运算
}
2).右值引用特性。
3).非const
左值引用,只能绑定在左值上;const
左值引用可以,右值引用只能绑定在右值上。(const
只是不能改变值,不要求时字面值类型等。)
int &&a;
{
int i = 42;
int &a = i;
int &&b = i;//错误
int &c = i*12;//错误
const int &d = 12;
int &&e = i * 13;
}
4).变量表达式,只有一个变量的表达式,返回的是一个左值。
{
int &&i = 12;
int &&a = i; //错误。它是一个左值。
}
5).调用std::move
,实现左值到右值的转换。
move
和上面介绍的move
是一样的。move
,意味着,我们只能对这个变量进行销毁,或者赋予新值。而不能对他进行任何值的阶段,例如作为=
的右侧运算对象。{
int &&a = std::move(i); //正确。
}
1).为什么移动?需要注意什么。
string
类,很多都同时支持移动和拷贝操作。2).移动构造函数和拷贝构造函数的差异。
{
class f {
f() = default;
f(const f&);
};
f x;
f y(x);//拷贝构造
f k(std::move(y));//还是调用拷贝构造。 这就是函数的匹配。
// 因为f&&可以转换为const f&。
// 并且用拷贝构造函数来代替移动构造函数几乎肯定是安全的。因为拷贝构造函数一般都是满足移动构造函数的要求的,使得源对象处于有效的,安全的状态,而拷贝根本就没有改变源的状态。
}
StrVec
中是将源进行归位,但是一些更加复杂的类,我们不可以预知。)。所以经常的对于指针就是置空,对于set
进行clear
,防止析构函数误伤重要内容(有可能是我们刚刚移动的资源。)。3).移动构造的简单应用。
{
// 例子,移动构造函数。
StrVec::StrVec(StrVec &&r) noexcept :
elements(r.elements),cap(r.cap),first_free(r.first_free) {
s.elements = s.first_free = s.cap = nullptr;
}
// 例子。移动赋值运算符号
StrVec &StrVec::operator=(StrVec &&r) noexcept {
if (this != &r ) {
// 检查是否是自身赋值
free(); //释放内存
elements = r.elements;
first_free = r.first_free;
cap = r.cap;
r.elements = r.cap = r.first_free = nullptr;
//将他置为可以析构的状态,有效,安全。
}
return *this;
}
}
4).**关于noexcept
,**它是一种信号,我们保证这个函数不会抛出异常。(自定义类和标准库的交互。)
vector
保证移动时,不会有异常导致自身内容丢失,才会调用移动构造函数。noexcept
。moexcept
。{
class StrVec {
public:
StrVec(StrVec &&) noexcept;//移动构造函数
};
StrVec::StrVec(StrVec &&s) noexcept : /*成员初始化器*/ {
/*函数体*/
}
}
5).合成移动的操作和拷贝操作的差异
{}
class f {
f() = default;
f(const f&);
};
f x;
f y(x);//拷贝构造
f k(std::move(y));//拷贝构造
}
static
数据成员都可以移动的类,合成移动构造或移动赋值。{
struct x {
int i; //内置类型
string s; //标准库定义
}; //编译器可以为x合成移动操作
struct HasX {
x mem; //有合成的
}; //编译器也可以为这个类合成移动操作。
x a,b = std::move(a);//默认构造,移动构造
HasX hx,hx2 = std::move(hx);//默认构造,移动构造
}
6).合成的移动构造或者移动赋值版本是删除的。
=default
的移动操作,但是编译器不能移动所有的成员时。此时编译器的移动操作就定义为删除的函数。{
// Y是一个类,定义了拷贝但是没有定义移动
struct hasY {
hasY() = default;
hasY(hasY &&) = default;//显式要求,但是编译器不能合成,因为有类成员定义了拷贝而没有定义移动,(函数匹配只能拷贝)
// 如果没有显式要求,那么将是不能合成的。
Y mem;
};
hasY hy,hy1 = std::move(hy);//错误。移动构造函数是删除的
}
const
或是引用,类的移动赋值时删除的。7).如果一个定义了移动操作,那么这个类合成的拷贝操作将会是删除的。
8).综合以上。**五个构造函数应该看成是一个整体。**由于移动构造函数,移动赋值运算,与拷贝的版本有着很多关系(可以重载,不能合成等),为了我们达到预期的目的(只是移动不拷贝,或者只拷贝不移动),这些函数常常一起定义。
9).当我们一起定义这些控制操作时,然后就是一个函数匹配问题。
const
的转换,而strVec&&
是一个亲精确的匹配。{
StrVec v1,v1; //默认构造函数
v1 = v2;//拷贝赋值运算
StrVec getVec(istream &);
v2 = getVec(cin); //移动赋值运算
}
{
C i;//默认构造
C a(i);//调用拷贝构造
C b(std::move(i));//调用移动构造。
}
const
类型)。{
class HasPtr {
public:
// 添加移动构造函数。
HasPtr(HasPtr &&p) noexcept :
ps(p.ps),i(p.i) {p.ps = 0;}
// 添加移动赋值运算符
// 既是拷贝赋值运算符号又是移动赋值运算符号。
HasPtr& operator=(HasPtr r) {
swap(*this,r);
return *this;
}
// 注意上述不是引用。
};
// 类的两个对象。
// 这里都是调用移动(拷贝)赋值运算符
// 此时这两个函数是一样的。
hp = hp2; //这个是报错的
hp = std::move(hp2); //可以运行。
// 但是在传递参数时,还是进行了拷贝。需要拷贝初始化
// 这里的拷贝会调用移动构造函数(实参是左值时),而如果实参是右值,由于定义了移动操作,那么它的合成拷贝操作就是删除的。
}
10).更加优化的Message
拷贝操作。(但是原来的对象是被销毁的。)(右值引用的应用。)
set,string
的移动构造函数,而不是拷贝构造函数。set
会抛出异常bad_alloc;
,我们并不声明是noexcept
的。{
// 工具函数
// 使用指针,因为不想拷贝,从而提升性能。
void Message::move_Folders(Message *m) {
folders = std::move(m->folders); //使用移动操作,移动赋值
for (auto f : folders) {
f->remMsg(m); //移除旧的
f->addMsg(this); //添加新的
}
m->folders.clear(); //确保销毁是无害的。删除所有的元素。
}
// 移动构造函数。
// 这里将m置为右值,我们只能对他赋值或者销毁,对于它的内容,我们不可以进行期望。
// 既对Message进行移动,也对它的成员进行移动。
// m依然还是一个变量。
Message::Message(Message &&m) : contents(std::move(m.contents)) {
move_Folders(&m); //移动folders并更新指针。
}
// 移动赋值运算符
Message& Message::operator=(const Message &&m) {
// 由于是移动,必须检查是不是自己对自己赋值,否则将是混乱的。比其拷贝复杂
// 虽然一个是右值,但是等号依然成立
// if (*this != m) { //这样应该也可以。
if (this != m) {
remove_form_folders(); //将自身的指针移除
contents = std::move(m.contents);//移动
move_Folders(&m);
}
return *this;
}
}
11).移动迭代器
initialized_copy
进行reallocate
操作。问题,它只能进行拷贝。而不能是移动。make_move_iterator
函数,将普通的迭代器转换为移动迭代器。{
void StrVec::recallocate() {
auto newCapacity = size() ? size() * 2 : 1;
auto first = alloc.allocate(newCapacity);
// 看似是copy但是实际上是移动。
auto last = uninitialized_copy(make_move_iterator(begin()),
make_move_iterator(end()),
first);
free(); //销毁原来的空间,
//它会将元素都进行清空,保证销毁时不会误伤。
elements = first;
first_free = last;
cap = first + newCapacity;
}
}
uninitialized_copy
本质上也是对每一个元素调用construct
进行构造。并且,此算法是对**迭代器进行解引用,**所以实际上右值,所以construct
实际上就是用移动构造函数来进行构造元素。是对原来的for
循环的一个简化写法,而工作的实质是一样的。练习,
unique_ptr
是不可以拷贝的,但是当一个将要被销毁的unique_ptr
是可以拷贝的。例如函数返回一个局部的unique_ptr
,此时的“拷贝”,其实是调用了移动构造函数进行了移动。(HasPtr)
。1).形参为以下两种形式的成员函数。
const C &
,接受任意类型的版本。C &&
,接受非const
版本的右值。(精确匹配)const C &&;
因为我们不会去接受一个const
的右值。是否是因为它无法改变,因为当我们使用右值时,意味着源对象的改变。C &;
因为当我们使用左值时,我们执行拷贝,并不会修改源对象。construct
会根据传递给他的第二个及其以后的实参进行判断使用哪一个构造函数。由于是右值,所以使用的是移动构造函数。{
class StrVec {
public:
void push_back(const string&);
void push_back(string &&);
};
void StrVec::push_back(const string &s) {
chk_n_alloc(); //确保有足够的容量
alloc.construct(first++,s);
}
void StrVec::push_back(string &&s) {
chK_n_alloc();
alloc.construct(first_free++,std::move(s)); //再一次进行移动。
}
}
{
StrVec vec;
string s = "this is some string";
vec.push_back(s); //左值,调用const string &
vec.push_back("const &&");//右值
}
2).对象调用函数,对象是左值还是右值并没有限定。
{
string s = "a value";
string t = " another value";
s + t = "woe"; //甚至可以对一个右值赋值,两个string的连接结果。
// 对两个string的连接调用函数。
auto p = (s + t).find('a');
}
3).引用限定符号。解决可以向右值赋值的问题。强制调用的对象是一个左值或者是一个右值。
&
,指定this
是一个左值;&&
,指定this
是一个右值。const
之前。const
声明限定符号,引用限定符号只能用于(非static
)成员函数。而且必须同时出现在函数的声明和定义中。const
限定符号。{
class Foo {
public:
Foo sorted() &&;
Foo sorted() const &;
private:
vector<string> data;
};
// 对于右值,可以直接进行排序,应为传入的参数将要销毁。
Foo Foo::sorted() && {
sort(data.begin().data.end());
return *this;
}
// 对于左值,必须拷贝一个副本,避免对副本进行了修改。
Foo Foo::sorted() const & {
Foo ret(*this); //拷贝一个副本
sort(ret.data.begin().ret.data>end()); //对副本进行sort
return ret;
}
}
{
Foo a();
Foo& b();
a.sorted();//使用的时&&版本
b.sorted();//使用的是const &版本。
}
const
本来就可以依据是否有const
进行重载。这是不一样的地方。{
class Foo {
public:
Foo sorted() &&;
Foo sorted() const;//错误没有带上引用限定符号。
};
}
练习
return ret.sorted();
函数改为这样,那么会陷入死循环。因为一直需要,而左值调用一直是如此。return Foo(*this).sorted();
合法的,会调用右值的版本。因为Foo(*this);
会被当成是右值。1).合成的版本。就是逐一地(非static
的。)构造,移动,拷贝,赋值,销毁等。
2).**copy and swap
。**在赋值运算时,先对右侧拷贝,再交换副本和左侧对象。避免自赋值的错误。
原文链接:https://blog.csdn.net/weixin_50559975/article/details/118917734
作者:小可爱不要爱
链接:http://www.pythonpdf.com/blog/article/279/abb20e0aff0a09d32fa3/
来源:编程知识网
任何形式的转载都请注明出处,如有侵权 一经发现 必将追究其法律责任
昵称:
评论内容:(最多支持255个字符)
投诉与举报,广告合作请联系vgs_info@163.com或QQ3083709327
免责声明:网站文章均由用户上传,仅供读者学习交流使用,禁止用做商业用途。若文章涉及色情,反动,侵权等违法信息,请向我们举报,一经核实我们会立即删除!