C++11学习笔记 12.docx
- 文档编号:24886167
- 上传时间:2023-06-02
- 格式:DOCX
- 页数:23
- 大小:23.84KB
C++11学习笔记 12.docx
《C++11学习笔记 12.docx》由会员分享,可在线阅读,更多相关《C++11学习笔记 12.docx(23页珍藏版)》请在冰豆网上搜索。
C++11学习笔记12
拷贝构造函数的第一个参数必须是引用类型,此参数几乎总是const的引用。
拷贝构造函数在几种情况下会隐式地使用。
因此,拷贝构造函数不应该是explicit的
即使我们定义了其他构造函数,在没有拷贝构造函数时,编辑器也会为我们合成的。
编辑器从给定对象中依次将每个非static成员拷贝到创建的对象中。
每个成员决定了它使用何种方式进行拷贝。
类调用拷贝构造函数,数组逐个拷贝,内置类型直接拷贝
stringdots(10,'.') //直接初始化
stringnoll_book="999999"; //拷贝初始化
当我们使用拷贝初始化时,我们要去编译器将右侧运算对象拷贝到正在创建的对象中,如果需要可能进行类型转化
拷贝初始化通常使用的是拷贝构造函数。
但是,如果一个类有一个移动构造函数,则拷贝初始化有时会使用移动构造函数。
拷贝初始化不仅在我们用=定义变量时发生,在以下情况也会发生:
将一个对象作为实参传递给一个非引用类型的形参
从一个返回类型为非引用类型的函数返回一个对象
用花括号列表初始化一个数组中的元素或一个聚合类中的成员
某些类类型还会对他们所分配的对象使用拷贝初始化。
如,当我们初始化标准库容器或是调用insert或push成员时,容器会对其元素进行拷贝初始化。
与之相对的,emplace用的是直接初始化。
如果拷贝构造函数的参数不是引用,那么我们将会再次调用拷贝构造函数。
如此循环
如果拷贝构造函数声明为explicit那么,不能进行隐式转换
vector
vector
voidf(vector
f(10);//错误:
f(vector
编辑器可以绕过拷贝构造函数,但不是必须。
而且拷贝/移动构造函数必须是可访问的
stringnull_book="9999";
编辑器可改写为:
stringnull_book("9999");
某些运算符,包括赋值运算符,必须定义为成员函数。
如果一个运算符是一个成员函数,其左侧运算对象就绑定到隐式的this参数。
对于二元运算符,例如赋值运算符,其右侧运算对象作为显示参数传递。
为了与内置类型保持一致,赋值运算符通常返回一个指向其左侧运算对象的引用。
另外值得注意的是,标准库通常要求保存在容器中的类型要具有赋值运算符,且返回值是左侧对象的引用。
如果未定义自己的拷贝赋值运算符,编译器会为它自动生成一个合成赋值运算符,会将右侧对象的每个非static成员赋予左侧。
//等价于合成拷贝赋值运算符
Sales_data&Sales_data:
:
operator=(constSales_data&rhs)
{
bookNo=rhs.bookNo;
units_sold=rhs.units_sold;
revenue=rhs.revenue;
return*this;
}
析构函数释放对象使用的资源,并销毁对象的非static数据成员。
由于析构函数不接有参数也没有返回值,对一个给定的类,只有唯一一个。
在一个析构函数中,首先执行函数体,然后销毁成员。
成员按初始化顺序的逆序销毁。
在对象最后一次使用之后,析构函数的函数体可执行类设计者希望执行的任何收尾工作。
通常,会释放对象在生存期分配的所有资源
析构部分是隐式的。
成员销毁时依赖成员的类型。
销毁类会调用自己的析构函数。
内置类型没有,什么也不做。
隐式销毁一个内置指针类型的成员不会delete掉它所指的对象。
但是智能指针会调用自己的析构函数,在析构阶段会自动销毁
无论何时一个对象被销毁,就会自动调用其析构函数:
变量离开其作用域时被销毁
当一个对象被销毁时,其成员被销毁
容器(无论是标准库容器还是数组)被销毁时,其元素被销毁
对于动态分配的对象,当对指向它的指针引用delete运算符时被销毁
对于临时对象,当创建它的完整表达式结束时别销毁。
析构函数时自动运行的,我们无须担心资源何时被释放。
当指向一个对象的引用或指针离开作用域时,析构函数不会被执行。
当类未定义自己的析构函数时,编辑器会自己定义一个合成析构函数。
类似拷贝构造函数和拷贝赋值运算符,对于某些类,合成析构函数被用来阻止该类型的对象被销毁。
如果不是这种情况,课程析构函数的函数体就为空。
在(空)析构函数执行完毕后,成员会被自动销毁。
析构函数自身并不直接销毁成员。
成员实在析构函数体之后隐含的析构阶段中被销毁的。
析构函数体是作为成语销毁之外的另一部分而进行的
我们可以从析构函数开始判断:
如果一个类需要一个析构函数,我们几乎可以肯定它也需要一个拷贝构造函数和一个拷贝赋值运算符。
如果一个雷需要一个拷贝构造函数,几乎可以肯定它也需要一个拷贝赋值运算符。
反之亦然,如果一个类需要一个拷贝赋值运算符,几乎可以肯定它也需要一个拷贝构造函数。
然而,无论是需要拷贝构造函数还是拷贝赋值运算符都不必然意味着也需要析构函数。
~Sales_data()=default;//显示的要求编辑器生成合成的版本;
当我们在雷内使用=default时,合成的函数将隐式的声明为内联函数,如果我们不需要内联应该只在定义是使用。
大多数类应该定义默认构造函数、拷贝构造函数和拷贝赋值运算符,无论是显示的还是隐式的。
有时候类必须采取某种机制阻止拷贝和赋值,如,iostream类就阻止了拷贝,以避免多个对象写入或读取相同IO缓冲。
在新标准下,我们可以通过将拷贝构造函数和拷贝运算符定义成删除的函数来阻止拷贝。
删除函数是这样的,我们虽然声明了它们,但不能以任何方式使用它们。
NoCopy(constNoCapy&)=delete;//阻止拷贝
与=default不同,=delete必须是在函数第一次声明出现的时候。
我们可以对任何函数指定delete,我们只能对编辑器可以合成的默认构造函数或拷贝控制成员使用=default。
虽然删除函数主要是阻止拷贝,担当我们希望引导函数匹配过程时,删除函数有时也是很有用的。
值得注意的是我们不能定义析构函数时删除函数。
如果将析构函数定义成删除函数,我们不能定义该类的变量或临时对象。
可以定义动态分配这种类型的对象。
但是,不能释放这些对象。
对某些类来说,编辑器将合成的成员定义为删除的函数:
如果类的某个成员的析构函数时删除的或不可访问的(如,private),则类的合成析构函数被定义为删除的。
如果类的某个成员的拷贝构造函数时删除的或不可访问的,则合成的拷贝构造函数被定义为删除的。
如果类的某个成员的析构函数是删除的或不可访问的,则类合成的拷贝构造函数也被定义为删除的。
如果类的某个成员的拷贝赋值运算是删除的或不可访问的,或类有一个const或引用成员,则类的合成拷贝赋值运算符被定义为删除的
如果类的某个成员的析构函数是删除的或不可访问的,或一个类有一个引用成员,它么有类内初始化器,或是类有一个const成员,它没有类内初始化器且其未显示定义默认构造函数,则该类的默认构造函数被定义为删除的。
这些规则的含义是:
如果一个类有数据成员不能默认构造,拷贝,复制或销毁,则对应的成员函数被定义为删除的。
一个成员有删除的或不可访问的析构函数会导致合成默认和拷贝构造函数被定义为删除的。
虽然我们可以将一个新值赋予一个引用成员,但这样做改变的是引用指向的对象的值,而不是引用本身。
在新标准发布之前,类是通过将其拷贝构造函数和拷贝赋值运算符声明为private来阻止拷贝的、为了阻止友元和成员函数进行拷贝,我们将这些拷贝控制成员声明为private的,但不定义他们。
如果访问就会在编译阶段或链接阶段报错。
声明但不定义一个成员函数是合法的,对此只有一个例外。
应使用新版的=delete。
拷贝控制和资源管理有两种方式,令类的行为像一个值,令类的行为像一个指针。
//行为像值得类
classHasPtr{
public:
HasPtr(conststd:
:
string&s=std:
:
string()):
ps(newstd:
:
string(s),i(0){}
HasPtr(constHasPtr&p):
ps(newstd:
:
string(*p.ps)),i(p.i){}
HasPtr&operator=(constHasPtr&);
~HasPtr(){deleteps;}
private:
std:
:
string*ps;
int i;
};
赋值运算符通常组合了析构函数和构造函数的操作。
要保证正确的顺序执行,即使将一个对象赋予它自身,也保证正确。
而且,如果有可能,我们编写赋值运算还应该是异常安全的,当异常发生时能将左侧对象置于一个有意义的状态。
//类值拷贝赋值运算符
HasPtr&HasPtr:
:
operator=(constHasPtr&rhs)
{
autonewp=newstring(*rhs.ps); //在销毁左侧运算对象资源之前拷贝右侧运算对象
deleteps;
ps=newp;
i=rhs.i;
return*this;
}
如果写成deleteps; ps=newstring(*(rhs.ps)); 当是同一个对象时将报错。
引用计数的工作方式:
除了初始化对象外,每个构造函数(拷贝构造函数除外)还要创建一个引用计数,用来记录有多少对象正在创建的对象共享状态。
当我们创建一个对象时,只有一个对象共享状态,因此将技术器初始化为1.
拷贝构造函数不分配新的计数器,而是拷贝给定对象的数据成员,包括计数器。
拷贝构造函数递增共享的计数器,指出给定对象的状态又被一个新的用户所共享。
析构函数递减计数器,指出共享状态的用户少了一个。
如果计数器变为0,则析构函数释放状态。
拷贝赋值运算符递增右侧运算对象的计数器,递减左侧运算对象的计数器。
如果左侧运算对象的计数器变为0,意味着它的共享状态没有用户了,拷贝赋值运算符就必须销毁状态。
引用计数应该保存在哪里,一种方法是保存在动态内存中。
//定义行为像指针的类,我们也可以用shared_ptr实现数据的共享。
classHasPtr{
public:
HasPtr(conststd:
:
string&s=std:
:
string()):
ps(newstd:
:
string(s),i(0),use(newstd:
:
size_t
(1)){}
HasPtr(constHasPtr&p):
ps(p.ps),i(p.i),use(p.use){++*use;}
HasPtr&operator=(constHasPtr&);
~HasPtr();
private:
std:
:
string*ps;
int i;
std:
:
size_t *use;
};
HasPtr:
:
~HasPtr()
{
if(--*use==0)
{
deleteps;
deleteuse;
}
}
HasPtr&HasPtr:
:
operator=(constHasPtr&rhs)
{
++*rhs.use;
if(--*use==0)
{
deleteps;
deleteuse;
}
ps=rhs.ps;
i=rhs.i;
use=rhs.use;
return*this;
}
如果一个类定义了自己的swap,那么算法将使用类自定义版本。
否则,算法将使用标准库定义的swap。
我们的版本应该是通过改变指针来实现数据交换。
classHasPtr{
friendvoidswap(HasPtr&,HasPtr&);
};
inlineswap(HsaPtr&lhs,HasPtr&rhs)
{
usingstd:
:
swap;
swap(lhs.ps,rhs.ps);
swap(lhs.i,rhs.i);
}
与拷贝控制成员不同,swap并不是必要的。
但是,对于分配了资源的类,定义swap可能是一种重要的优化手段。
对于swap,如果存在类型中特定的swap版本,swap调用会与之匹配。
如果不存在,则会调用std中的版本(假定作用域中有using声明)
在定义了swap的类中,通常使用swap实现赋值运算符。
使用的是一种名为拷贝交换的技术。
HasPtr&HasPtr:
:
operator=(HasPtrrhs)
{
swap(*this,rhs);
return *this; //rhs被销毁
}
使用拷贝和交换的赋值运算符自动就是异常安全的。
且能正确处理自赋值。
它保证异常安全的方法与原来的赋值运算符实现一样。
代码中唯一可能抛出异常的是拷贝构造函数中的new表达式。
如果真的发生了异常,它也会在我们改变左侧运算对象之前发生。
每个message对象可以出现在多个Folder中。
析构函数和拷贝运算都必须包含一条Message的所有Folder中删除它。
拷贝赋值运算符通常执行拷贝构造函数和析构函数中也要只有的工作。
这种情况下,公共的工作应该放在private的工具函数中完成。
我们假定Folder类包含名为addMsg和remMsg,分别完成在给定Folder对象的结合中添加和删除Message的工作。
//拷贝控制示例
classMessage{
friendclassFolder;
public:
explicitMessage(conststd:
:
string&str=""):
contents(str){}
Message(constMessage&);
Message&operator=(constMessage&);
~Message();
voidsave(Folder&);
voidremove(Folder&);
private:
std:
:
stringcontents;
std:
:
set
voidadd_to_Folders(constMessage&);
voidremove_from_Folders();
};
voidMessage:
:
save(Folder&f)
{
folders.insert(&f);
f.addMsg(this);
}
voidMessage:
:
remove(Folder&f)
{
folders.erase(&f);
f.remMsg(this);
}
voidMessage:
:
add_to_Floders(constMessage&m)
{
for(autof:
m.folders)
f->addMsg(this);
}
Message:
:
Message(constMessage&m):
contents(m.constents),foders(m.folders)
{
add_to_Folders(m);
}
voidMessage:
:
remove_from_Folders()
{
for(autof:
folders)
f->remMsg(this);
}
Message:
:
~Message()
{
remove_from_Folders();
}
Message&Message:
:
operator=(constMessage&rhs)
{
//通过先删除指针在插入它们来处理自赋值情况
remove_from_Folder();
constents=rhs.contents;
folders=rhs.folders;
add_to_Folders(rhs);
return*this;
}
voidswap(Message&lhs,Message&rhs)
{
usingstd:
:
swap;
for(autof:
lhs.folders)
f->remMsg(&lhs);
for(autof:
rhs.folders)
f->remMsg(&rhs);
swap(lhs.folders,rhs.folders);
swap(lhs.contents,rhs.constents);
for(autof:
lhs.folders)
f->addMsg(&lhs);
for(autof:
rhs.folders)
f->addMsg(&rhs);
}
//动态内存管理类
classStrVec{
public:
StrVec():
elements(nullptr),first_free(nullptr),cap(nullptr){}
StrVec(constStrVec&);
StrVec&operator=(constStrVec&);
~Strvec();
voidpush_back(conststd:
:
stirng&);
size_tsize()const{returnfirst_free-elements;}
size_tcapacity()const{returncap-elements;}
std:
:
string*begin()const{returnelements;}
std:
:
string*end()const{returnfirst_free;}
private:
staticstd:
:
allocator : string>alloc; voidchk_n_alloc() {if(size()==capacity())reallocate();} std: : pair : string*,std: : string*>alloc_n_copy(conststd: : string*,conststd: : string*); voidfree(); voidreallocate(); std: : string*elements; std: : string*first_free; std: : string*cap; }; voidStrVec: : push_back(conststring&s) { chk_n_alloc(); alloc.construct(first_free++,s); } pair : alloc_n_copy(conststring*b,conststring*e) { autodata=alloc.allocate(e-b); return{data,uninitialized_coyp(b,e,data)}; } voidStrVec: : free() { if(elements) { for(autop=first_free;p! =elements;) alloc.destroy(--p); alloc.deallocate(elements,cap-elements); } } StrVec: : StrVec(constStrVec&s) { autonewdata=alloc_n_copy(s.begin(),s.end()); elements=newdata.first; first_free=cap=newdata.second; } StrVec: : ~StrVec() { free(); } StrVec&StrVec: : operator=(constStrVec&rhs) { autodata=alloc_n_copy(rhs.begin(),rhs.end()); free(); elements=data.first; first_free=cap=data.second; return*this; } voidStrVec: : reallocate() { autonewcapacity=size()? 2*size(): 1; autonewdata=alloc.allocate(newcapacity); autodest=newdata; autoelem=elements; for(size_ti=0;i! =size();++i) alloc.construct(dest++,std: : move(*elem++)); free(); elements=newdata; first_free=dest; cap=elements+newcapacity; } 上述使用了避免拷贝的两种机制,使用移动构造函数,通常是将资源从给定对象“移动”而不是拷贝到正在创建的对象。 而且标准库保证“移后源”string仍然保持一个有效的、可析构的状态。 另一种机制,使用move,如果遗漏将使用拷贝构造函数,其次,我们通常不为move提供一个using声明,而是直接调用std: : move; io类或unique_ptr。 这些类都包含不能被共享的资源。 因此,这些类型的对象不能拷贝但可移动 右值引用: 必须绑定到右值的引用。 通过&&来获得右值的引用。 而且只能绑定到一个将要销毁的对象。 因此,我们可以自由地将一个右值引用的资源“移动”到另一个对象中。 一般而言,一个左值表达式表示的是一个对象的身份,而一个右值表达式表示的是对象的值。 对常规引用(左值引用)我们不能将其绑定到要求转
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- C+11学习笔记 12 C+ 11 学习 笔记