C++的那些事:类的拷贝控制
1,什么是类的拷贝控制
当我们定义一个类的时候,为了让我们定义的类类型像内置类型(char,int,double等)一样好用,我们通常需要考下面几件事:
Q1:用这个类的对象去初始化另一个同类型的对象。
Q2:将这个类的对象赋值给另一个同类型的对象。
Q3:让这个类的对象有生命周期,比如局部对象在代码部结束的时候,需要销毁这个对象。
因此C++就定义了5种拷贝控制操作,其中2个移动操作是C++11标准新加入的特性:
拷贝构造函数(copy constructor)
移动构造函数(move constructor)
拷贝赋值运算符(copy-assignment operator)
移动赋值运算符(move-assignment operator)
析构函数 (destructor)
前两个构造函数发生在Q1时,中间两个赋值运算符发生在Q2时,而析构函数则负责类对象的销毁。
但是对初学者来说,既是福音也是灾难的是,如果我们没有在定义的类里面定义这些控制操作符,编译器会自动的为我们合成一个版本。这有时候看起来是好事,但是编译器不是万能的,它的行为在很多时候并不是我们想要的。
所以,在实现拷贝控制操作中,最困难的地方是认识到什么时候需要定义这些操作。
2,拷贝构造函数
拷贝构造函数是构造函数之一,它的参数是自身类类型的引用,且如果有其他参数,则任何额外的参数都有默认值。
class Foo{
public:
Foo();
Foo(const Foo&);
};
我们从上面代码中可以注意到几个问题:
1,我们把形参定义为const类型,虽然我们也可以定义非const的形参,但是这样做基本上没有意义的,因为函数的功能只涉及到成员的复制操作。
2,形参是本身类类型的引用,而且必须是引用类型。为什么呢?
我们知道函数实参与形参之间的值传递,是通过拷贝完成的。那么当我们将该类的对象传递给一个函数的形参时,会调用该类的拷贝构造函数,而拷贝构造函数本身也是一个函数,因为是值传递而不是引用,在调用它的时候也需要调用类的拷贝构造函数(它自身),这样无限循环下去,无法完成。
3,拷贝构造函数通过不是explict的。
如果我们没有定义拷贝构造函数,编译器会为我们定义一个,这个函数会从给定的对象中依次将每个非static成员拷贝到正在创建的对象中。成员自身的类型决定了它是如何被拷贝的:类类型的成员,会使用其拷贝构造函数来拷贝;内置类型则直接拷贝;数组成员会逐元素地拷贝。
区分直接初始化与拷贝初始化:
string name("name_str"); //直接初始化
string name = string("name_str"); // 拷贝初始化
string name = "name_str"; // 拷贝初始化
直接初始化是要求编译器使用普通的函数匹配来选择与我们提供的参数最匹配的构造函数;当我们使用拷贝初始化时,我们要求编译器将右侧运算对象拷贝到正在创建的对象中,如果需要的话还要进行类型转换(第三行代码隐藏了一个C风格字符串转换为string类型)。
3,拷贝赋值运算符
拷贝赋值运算符是一个对赋值运算符的重载函数,它返回左侧运算对象的引用。
class Foo
{
public:
Foo& operator=(const Foo&);
};
与拷贝构造函数一样,如果没有给类定义拷贝赋值运算符,编译器将为它合成一个。
4,析构函数
析构函数是由波浪线接类名构成,它没有返回值,也不接受参数。因为没有参数,所以它不存在重载函数,也就是说一个类只有一个析构函数。
析构函数做的事情与构造函数相反,那么我们先回忆一个构造函数都做了哪些事:
1,按成员定义的顺序创建每个成员。
2,根据成员初始化列表初始化每个成员。
3,执行构造函数函数体。
而析构函数中不存在类似构造函数中初始化列表的东西来控制成员如何销毁,析构部分是隐式的。成员如何销毁依赖于成员自身的类型,如果是类类型则调用本身的析构函数,如果是内置类型则会自动销毁。而如果是一个指针,则需要手动的释放指针指向的空间。与普通指针不同的是,智能指针是一个类,它有自己的析构函数。
那么什么时候会调用析构函数呢?在对象销毁的时候:
- 变量在离开其作用域时被销毁;
- 当一个对象被销毁时,其成员被销毁。
- 容器被销毁时,成员被销毁。
- 对于动态分配的对象,当对指向它的指针应用delete运算符时被销毁。
- 对于临时对象,当创建它的赛事表达式结束时被销毁。
值得注意的析构函数是自动运行的。析构函数的函数体并不直接销毁成员,成员是在析构函数体之后隐含的析构阶段中被销毁的。在整个对象销毁过程中,析构函数体是作为成员销毁步骤之外的另一部分而进行的。
5,定义拷贝控制操作的原则
在第1点里有提过,在定义类的时候处理拷贝控制最困难的在于什么时候需要自己定义,什么时候让编译器自己合成。
那么我们可以有下面2点原则:
如果一个类需要定义析构函数,那么几乎可以肯定它也需要一个拷贝构造函数和一个拷贝赋值函数,反过来不一定成立。
如果一个类需要一个拷贝构造函数,几乎可以肯定它也需要一个拷贝赋值函数,反之亦然。
为什么析构函数与拷贝构造函数与赋值函数关系这么紧密呢,或者说为什么我们在讨论拷贝控制(5种)的时候要把析构函数一起放进来呢?
首先,我们思考什么时候我们一定要自己来定义析构函数,比如:类里面有动态分配内存。
class HasPtr
{
public:
HasPtr(const string&s = string()) :ps(new string(s), i()){}
~HasPtr(){ delete ps; }
private:
int i;
string* ps;
};
我们知道如果是编译器自动合成的析构函数,则不会去delete指针变量的,所以ps指向的内存将无法释放,所以一个主动定义的析构函数是需要的。那么如果没有给这个类定义拷贝构造函数和拷贝赋值函数,将会怎么样?
编译器自动合成的版本,将简单的拷贝指针成员,这意味着多个HasPtr对象可能指向相同的内存。
HasPtr p("some values");
f(p); // 当f结束时,p.ps指向的内存被释放
HasPtr q(p);// 现在p和q都指向无效内存
6,使用=default和=delete
我们可以使用=default来显式地要求编译器生成合成的版本。合成的函数将隐式地声明为内联的,如果我们不希望合成的成员是内联的,应该只对成员的类外定义使用=default。
有的时候我们定义的某些类不需要拷贝构造函数和拷贝赋值运算符,比如iostream类就阻止拷贝,以避免多个对象写入或读取相同的IO缓冲。
新的标准里,我们可以在拷贝构造函数和拷贝赋值运算符函数的参数列表后面加上=delete用来指出我们希望将它定义为删除的,这样的函数称为删除函数。
class NoCopy
{
NoCopy() = default; // 使用合成的默认构造函数
NoCopy(const NoCopy&) = delete; // 删除拷贝
NoCopy& operator=(const NoCopy&) = delete; // 删除赋值
~NoCopy() = default; // 使用合成的析构函数
};
注意:析构函数不能是删除的成员,因为这样的类是无法销毁的。
如果一个类有const成员或者有引用成员,则这个类合成拷贝赋值运算符是被定义为删除的。
在新的标准出来之前,类是通过将其拷贝构造函数的拷贝赋值运算符声明为private来阻止拷贝,而且为了防止成员被友元或其他成员访问,会对这些成员函数只声明,但不定义。
7,右值引用
所谓的右值引用就是必须绑定在右值上的引用,我们可以通过&&来获得右值引用,右值引用一个很重要的性质是只能绑定到一个将要销毁的对象,所以我们可以自由地将一个右值引用的资源“移动”到另一个对象中。
我们可以将一个右值引用绑定到表达式上,但不能将右值引用绑定到一个左值上:
int i = ;
int &r = i; // 正确:r引用i
int &&rr = i; // 错误:不能将一个右值引用绑定到一个左值上
int &r2 = i * ; // i*42是一具右值
const int& r3 = i * ; // 可以将一个const的引用绑定到一个右值上
int && rr2 = i * ; // 正确:将rr2绑定到乘法结果上
总体来说:左值有持久的状态,而右值要么是字面常量,要么是表达式求值过程中创建的临时对象。
从而我们得知,关于右值引用:1)所引用的对象将要销毁;2)该对象没有其他用户。
标准库提供了一个std::move函数,让我们可以获得左值上的右值引用:
int &&r3 = std::move(rr1); // rr1是一个变量
move调用告诉编译器:我们有一个左值,但是我们希望像一个右值一个处理它。在上面的代码后,要么销毁rr1,要么对rr1进行赋值,否则我们不能使用rr1。
另外一点值得注意的是,我们使用std::move而不是move,即使我们提供了using声明。
8,移动构造函数和移动赋值运算符
与拷贝一样,移动操作同样发生在我们一个类的对象去初始化或赋值同一个类类型的对象时,但是与拷贝不同的是,对象的内容实际上从源对象移动到了目标对象,而源对象丢失了内容。移动操作一般只发生在当这个源对象是一个uname的对象的时候。
一个uname object意思是一个临时对象,还没有被赋予一个名字,例如一个返回该类型的函数返回值或者一个类型转换操作返回的对象。
MyClass fn(); // function returning a MyClass object
MyClass foo; // default constructor
MyClass bar = foo; // copy constructor
MyClass baz = fn(); // move constructor
foo = bar; // copy assignment
baz = MyClass(); // move assignment
上面的代码中由fn()返回的对象和由MyClass构造出来的对象都是unnamed,用这样的对象给MyClass赋值或初始化时,并不需要拷贝,因为源对象只有很短的生命周期。
移动构造函数与移动赋值函数的定义形式上与拷贝操作一样,只是将拷贝函数的形参的引用换成右值引用。
MyClass (MyClass&&); // move-constructor
MyClass& operator= (MyClass&&); // move-assignment
移动操作对那些需要管理存储空间的类是非常有用的,比如我们下面定义的这个类
// move constructor/assignment
#include <iostream>
#include <string>
using namespace std; class Example6 {
string* ptr;
public:
Example6 (const string& str) : ptr(new string(str)) {}
~Example6 () {delete ptr;}
// move constructor
Example6 (Example6&& x) : ptr(x.ptr) {x.ptr=nullptr;}
// move assignment
Example6& operator= (Example6&& x) {
delete ptr;
ptr = x.ptr;
x.ptr=nullptr;
return *this;
}
// access content:
const string& content() const {return *ptr;}
// addition:
Example6 operator+(const Example6& rhs) {
return Example6(content()+rhs.content());
}
}; int main () {
Example6 foo ("Exam");
Example6 bar = Example6("ple"); // move-construction foo = foo + bar; // move-assignment cout << "foo's content: " << foo.content() << '\n';
return ;
}
C++的那些事:类的拷贝控制的更多相关文章
- OOP3(继承中的类作用域/构造函数与拷贝控制/继承与容器)
当存在继承关系时,派生类的作用域嵌套在其基类的作用域之内.如果一个名字在派生类的作用域内无法正确解析,则编译器将继续在外层的基类作用域中寻找该名字的定义 在编译时进行名字查找: 一个对象.引用或指针的 ...
- [C++]类的设计(2)——拷贝控制(拷贝控制和资源管理)
1.类的行为分类:看起来像一个值:看起来想一个指针. 1)类的行为像一个值,意味着他应该有自己的状态.当我们拷贝一个像值的对象时,副本和原对象是完全独立的.改变副本不会对原有对象有任何影响 ...
- [C++]类的设计(2)——拷贝控制(阻止拷贝)
1.阻止拷贝的原因:对于某些类来说,拷贝构造函数和拷贝赋值运算符没有意义.举例:iostream类阻止了拷贝,以避免多个对象写入或者读取相同的IO缓冲. 2.阻止拷贝的方法有两个:新标准中可以将成 ...
- [C++] 类的设计(2)——拷贝控制(1)
1.一个类通过定义五种特殊的成员函数来控制此类型对象的拷贝.移动.赋值和销毁:拷贝构造函数.拷贝赋值运算符.移动构造函数.移动赋值运算符和析构函数.(拷贝.移动.析构) 2.拷贝和移动构造函数定义 ...
- C++ Primer : 第十三章 : 拷贝控制之对象移动
右值引用 所谓的右值引用就是必须将引用绑定到右值的引用,我们通过&&来绑定到右值而不是&, 右值引用只能绑定到即将销毁的对象.右值引用也是引用,因此右值引用也只不过是对象的别名 ...
- C++ Primer : 第十三章 : 拷贝控制之拷贝控制和资源管理
定义行为像值的类 行为像值的类,例如标准库容器和std::string这样的类一样,类似这样的类我们可以简单的实现一个这样的类HasPtr. 在实现之前,我们需要: 定义一个拷贝构造函数,完成stri ...
- C++ Primer : 第十三章 : 拷贝控制之拷贝、赋值与销毁
拷贝构造函数 一个构造函数的第一个参数是自身类类型的引用,额外的参数(如果有)都有默认值,那么这个构造函数是拷贝构造函数.拷贝构造函数的第一个参数必须是一个引用类型. 合成的拷贝构造函数 在我们没 ...
- Chapter13:拷贝控制
拷贝控制操作:拷贝构造函数.拷贝赋值运算符.移动构造函数.移动赋值运算符.析构函数. 实现拷贝控制操作的最困难的地方是首先认识到什么时候需要定义这些操作. 拷贝构造函数: 如果一个构造函数的第一个参数 ...
- C++ 拷贝控制和资源管理,智能指针的简单实现
C++ 关于拷贝控制和资源管理部分的笔记,并且介绍了部分C++ 智能指针的概念,然后实现了一个基于引用计数的智能指针.关于C++智能指针部分,后面会有专门的研究. 通常,管理类外资源的类必须定义拷贝控 ...
随机推荐
- 12个常用的js正则表达式
在这篇文章里,我已经编写了12个超有用的正则表达式,这可是WEB开发人员的最爱哦. 1.在input框中只能输入金额,其实就是只能输入最多有两位小数的数字 //第一种在input输入框限制 <i ...
- C# JavascriptSerializer与匿名对象打造Json的完美工具
一:背景 在web项目中经常需要生成json数据,返回给前端ajax. 无论是ashx,还是WebMethod,可以人工的用字符串去拼接,最终得到json数据. 有没有更好的方法呢?我个人推荐使用Ja ...
- upc.2219: A^X mod P(打表 && 超越快速幂(in some ways))
2219: A^X mod P Time Limit: 5 Sec Memory Limit: 128 MB Submit: 417 Solved: 68 [Submit][Status][Web ...
- To the Max
To the Max --------------------------------------------------------------------------------Time Limi ...
- springmvc 定时器
CronTrigger配置格式: 格式: [秒] [分] [小时] [日] [月] [周] [年] 序号 说明 是否必填 允许填写的值 允许的通配符 1 秒 是 0-59 , - * ...
- Action的动态调用方法
Action执行的时候并不一定要执行execute方法,我们可以指定Action执行哪个方法: 1. 方法一(通过methed属性指定执行方法): 可以在配置文件中配置Action的时候用method ...
- http://www.highcharts.com/
MAKE YOUR DATA COME ALIVE HIGHCHARTS CLOUD Online charts for non-techies. Create smashing, interacti ...
- 在eclipse中进行Struts2项目的配置
Struts2是一个比较出色的基于MVC设计模式的框架,是由Struts1和WebWork发展而来的,性能也比较稳定,现在是Apache软件基金会的一个项目,下面就来配置Struts2进行初始化的开发 ...
- Java for LeetCode 061 Rotate List
Given a list, rotate the list to the right by k places, where k is non-negative. For example: Given ...
- Netbeans快捷键
一.常用快捷键:1.在文件中查找指定内容 Ctrl+F2.在文件中替换指定内容 Ctrl+H3.在整个项目中查找指定内容 Ctrl+Shift+f4.自动复制整行代码 Ctrl+Shift+上/下方向 ...