第13章 拷贝控制
从此章我们即将开始第三部分的学习,之前我们已经学过了两个部分,C++基础和C++标准库,第三部分为类设计者的工具 也就是我们即将开始传说中的对象对象编程之旅,面向对象程序设计(Object Oriented Programming)
本章进行学习类如何操控该类型的拷贝,赋值,移动或者销毁,有:拷贝构造函数、移动构造函数、拷贝赋值运算符、移动赋值运算符以及析构函数等重要知识
拷贝构造函数
定义:如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是构造拷贝函数
简单上手
class Person
{
public:
int age;
Person() = default;
Person(int age) : age(age) {}
Person(const Person &person)
{
this->age = person.age;
}
};
int main(int argc, char **argv)
{
Person person1(19);
Person person2 = person1;
cout << person2.age << endl;
return 0;
}
合成拷贝构造函数
默认情况下,编译器会定义一个拷贝构造函数,即使在我们提供拷贝构造函数的情况下也仍会自动生成,默认情况下会将每个非static成员拷贝到正在创建的对象中
class Person
{
public:
int age;
string name;
Person() = 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)
{
}
int main(int argc, char **argv)
{
Person me(19, "gaowanlu");
Person other = me;
cout << other.age << " " << other.name << endl;
return 0;
}
尝试测试一下编译器默认提供的合成拷贝构造函数,可见存在默认合成拷贝构造函数 如果不想让一个构造函数具有可以赋值转换的功能,则将其定义为explicit的
class Person
{
public:
string name;
int age;
Person(const int age, const string name) : name(name), age(age) {}
};
int main(int argc, char **argv)
{
Person me(19, "gaowanlu");
Person other = me;
cout << other.age << " " << other.name << endl;
return 0;
}
重载赋值运算符
重载operator= 方法进行自定义赋值运算符使用时要做的事情
class Person
{
public:
int age;
string name;
Person() = default;
Person(int age, string name) : age(age), name(name) {}
Person &operator=(const Person &);
};
Person &Person::operator=(const Person &person)
{
cout << "operator =" << endl;
this->age = person.age;
this->name = person.name;
return *this;
}
int main(int argc, char **argv)
{
Person person1(19, "me");
Person person2;
person2 = person1;
cout << person2.age << " " << person2.name << endl;
return 0;
}
合成拷贝赋值运算符
与合成拷贝构造函数类似,如果没有自定义拷贝赋值运算符,编译器会自动生成
class Person
{
public:
int age;
string name;
Person() = default;
Person(int age, string name) : age(age), name(name) {}
};
int main(int argc, char **argv)
{
Person person1(19, "me");
Person person2;
person2 = person1;
cout << person2.age << " " << person2.name << endl;
return 0;
}
析构函数
析构函数与构造函数不同,构造函数初始化对象的非static数据成员,还可能做一些在对象创建时需要做的事情。析构函数通常释放对象的资源,并销毁对象的非static数据成员
~TypeName(); 析构函数没有返回值,没有接收参数,所以其没有重载形式
在构造函数中,初始化部分执行在函数体执行前,析构函数则是首先执行函数体,然后按照初始化顺序的逆序销毁。
构造函数被调用的时机
- 变量在离开其作用域时被销毁
- 当一个对象被销毁时,其成员被销毁
- 容器(无论标准容器还是数组)被销毁时,其元素被销毁
- 动态内存分配,当对它的指针使用delete时被销毁
- 对于临时对象,当创建它的完整表达式结束时被销毁
class Person
{
public:
int age;
string name;
Person() = default;
Person(int age, string name) : age(age), name(name) {}
~Person()
{
cout << "~Person" << endl;
}
};
Person func(Person person)
{
return person;
}
int main(int argc, char **argv)
{
Person person(19, "me");
Person person1 = func(person);
return 0;
}
合成析构函数
当为自定义析构函数时,编译器会自动提供一个合成析构函数,对于某些类作用为阻止该类型的对象被销毁,如果不是则函数体为空
class Person
{
public:
int age;
string name;
Person() = default;
Person(int age, string name) : age(age), name(name) {}
~Person() {}
};
int main(int argc, char **argv)
{
Person person(19, "gaowanlu");
cout << person.age << " " << person.name << endl;
return 0;
}
在合成析构函数体执行完毕之后,成员会被自动销毁,对象中的string被销毁时,将会调用string的析构函数,将name的内存释放掉,析构函数自身并不直接销毁成员 ,是在析构函数体之后隐含的析构阶段中被销毁的,整个销毁过程,析构函数体是作为成员销毁步骤之外的并一部分而进行的
如果对象的内部有普通指针记录new动态内存,在对象析构过程默认只进行指针变量指针本身的释放,而不对申请的内存进行释放,则就需要动态内存章节学习的在析构函数体内手动释放他们,或者使用智能指针,随着智能指针的析构被执行,动态内存会被释放
三/五法则
有三个基本操作可控制类的拷贝操作:拷贝构造函数、拷贝赋值运算符、析构函数。在新标准下还可以通过定义一个移动构造函数、一个移动赋值运算符 我们发现有时赋值运算符与拷贝构造函数会执行相同的功能,通常情况下并不要求定义所有这些操作
使用合成拷贝函数和合成拷贝赋值运算符时可能遇见的问题
class Person
{
public:
int age;
string *name;
Person(const string &name = string()) : name(new string(name)), age(0) {}
~Person()
{
delete name;
}
};
int main(int argc, char **argv)
{
{
Person person1("me");
Person person2 = person1;
*person1.name = "he";
cout << *person2.name << endl;
}
cout << "end" << endl;
return 0;
}
在合成拷贝构造函数和合成拷贝赋值运算符,其中的拷贝操作都是简单的指针地址赋值,而不是重新开辟空间,再将原先的name赋值到新的内存空间
使用=default
使用=default 可以显式要求编译器生成合成拷贝构造函数和拷贝赋值运算符
class Person
{
public:
Person() = default;
Person(const Person &) = default;
Person &operator=(const Person &);
~Person() = default;
};
Person &Person::operator=(const Person &person) = default;
int main(int argc, char **argv)
{
Person person1;
Person person2 = person1;
cout << "end" << endl;
return 0;
}
=delete阻止拷贝
使用=delete 定义删除的函数
class Person
{
public:
Person() = default;
Person(const Person &) = delete;
Person &operator=(const Person &) = delete;
~Person() = default;
};
int main(int argc, char **argv)
{
Person person1;
return 0;
}
析构函数不能是删除的成员 ,否则就不能销毁此类型,没有析构函数的类型可以使用动态分配方式创建,但是不能被销毁
class Person
{
public:
int age;
string name;
Person(const int age, const string name) : age(age), name(name) {}
~Person() = delete;
};
int main(int argc, char **argv)
{
Person *person = new Person(19, "me");
return 0;
}
编译器将成员处理为删除的
对于某些情况,编译器会将合成的成员定义为删除的函数
重点:如果一个类有数据成员不能默认构造、拷贝、复制、销毁,则对应的成员函数将被定义为删除的
private拷贝控制
在新标准之前没有,删除的成员,类是通过将其拷贝构造函数和拷贝赋值运算符声明为private的来阻止拷贝的
class Person
{
private:
Person(const Person &person);
Person &operator=(const Person &person);
public:
int age;
string name;
Person(const int age, const string name) : age(age), name(name) {}
~Person() = default;
Person() = default;
void test();
};
Person::Person(const Person &person)
{
}
Person &Person::operator=(const Person &person)
{
return *this;
}
void Person::test()
{
Person *person = new Person(19, "me");
Person person1 = *person;
delete person;
}
int main(int argc, char **argv)
{
Person person1(19, "me");
person1.test();
return 0;
}
这种虽然类的外部不能使用拷贝构造和拷贝赋值,但是类的友元和成员函数仍可使用二者,同时想要阻止友元函数或者成员函数的使用,则只声明private成员即可不进行定义
class Person
{
private:
Person(const Person &person);
Person &operator=(const Person &person);
public:
int age;
string name;
Person(const int age, const string name) : age(age), name(name) {}
~Person() = default;
Person() = default;
void test();
};
int main(int argc, char **argv)
{
Person person1(19, "me");
return 0;
}
总之优先使用=delete这种新的规范,delete是从编译阶段直接解决问题
行为像值的类
有些类拷贝是值操作,是一份相同得副本
class Person
{
public:
int *age;
string *name;
Person(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()
{
delete age, delete name;
}
};
Person &Person::operator=(const Person &person)
{
*age = *person.age;
*name = *person.name;
return *this;
}
int main(int argc, char **argv)
{
Person person1(19, "me");
Person person2(20, "she");
person1 = person2;
cout << *person1.age << " " << *person1.name << endl;
cout << *person2.age << " " << *person2.name << endl;
*person1.name = "gaowanlu";
cout << *person1.age << " " << *person1.name << endl;
cout << *person2.age << " " << *person2.name << endl;
return 0;
}
行为像指针的类
有些类拷贝是指针指向的操作,也就是不同的类的成员会使用相同的内存
先来看一种简单使用的情况
class Person
{
public:
int *age;
string *name;
Person() : 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)
{
if (age)
delete age;
if (name)
delete name;
age = person.age;
name = person.name;
return *this;
}
int main(int argc, char **argv)
{
Person person1(19, "me");
Person person2 = person1;
*person2.age = 20;
*person2.name = "gaowanlu";
cout << *person1.age << " " << *person1.name << endl;
cout << *person2.age << " " << *person2.name << endl;
return 0;
}
实现引用计数
有意思的例子是我们也可以设计引用计数的机制,通过下面这个例子可以学到很多的编程思想
class Person
{
public:
string *name;
int *age;
Person(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)
{
name = person.name;
age = person.age;
use = person.use;
++*use;
}
Person &operator=(const Person &person);
~Person();
size_t *use;
};
Person &Person::operator=(const Person &person)
{
++*person.use;
--*use;
if (*use == 0)
{
delete age, delete name, delete use;
}
age = person.age;
name = person.name;
use = person.use;
return *this;
}
Person::~Person()
{
--*use;
if (*use == 0)
{
delete age, delete name, delete use;
}
}
int main(int argc, char **argv)
{
Person person1(19, "me");
cout << *person1.use << endl;
{
Person person2(person1);
cout << *person1.use << endl;
Person *ptr = new Person(person2);
cout << *ptr->use << endl;
delete ptr;
cout << *person1.use << endl;
}
cout << *person1.use << endl;
return 0;
}
编写swap函数
可以在类上定义一个自己的swap函数重载swap默认行为
class Person
{
friend void swap(Person &a, Person &b);
public:
int age;
string name;
Person(const int &age, const string &name) : age(age), name(name) {}
};
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)
{
Person person1(19, "me");
Person person2(19, "she");
swap(person1, person2);
cout << person1.age << " " << person1.name << endl;
cout << person2.age << " " << person2.name << endl;
return 0;
}
拷贝赋值运算中使用swap
类的swap通常用来定义它们的赋值运算符,是一种拷贝并交换的技术
class Person
{
friend void swap(Person &a, Person &b);
public:
int age;
string name;
Person &operator=(Person person);
Person(const int &age, const string &name) : age(age), name(name) {}
};
Person &Person::operator=(Person person)
{
swap(*this, person);
return *this;
}
inline void swap(Person &a, Person &b)
{
a.age = b.age;
a.name = b.name;
}
int main(int argc, char **argv)
{
Person person1(19, "me");
Person person2 = person1;
cout << person2.age << " " << person2.name << endl;
return 0;
}
对象移动
什么是对象移动,也就是将对象移动到某处,即复制,但复制后就将原来的进行对象销毁了
标准库函数 std::move ,标准库容器、string、shared_ptr类即支持移动也支持拷贝,IO类和unique_ptr类可以移动但不能拷贝
#include <iostream>
#include <utility>
#include <string>
int main(int argc, char **argv)
{
using namespace std;
string a1 = "hello";
string a2 = std::move(a1);
cout << a1 << endl;
cout << a2 << endl;
int b1 = 999;
int b2 = std::move(b1);
cout << b1 << endl;
cout << b2 << endl;
return 0;
}
右值引用
什么是右值引用,右值引用为支持移动操作而生,右值引用就是必须绑定到右值的引用,使用&&而不是&来获得右值引用
左值与右值的声明周期,左值有持久的状态直到变量声明到上下文切换内存释放,右值要么是字面量或者求值过程中的临时对象 右值引用特性:
- 所引用的对象将要被销毁
- 该对象没有其他用户
- 无法将右值引用绑定到右值引用
int main(int argc, char **argv)
{
int num = 666;
int &ref = num;
const int &ref2 = num * 42;
int &&refref1 = num * 10;
cout << refref1 << endl;
refref1 = 999;
cout << refref1 << endl;
return 0;
}
move与右值引用
虽然不能将右值引用绑定在左值上,但可以通过std::move来实现
int main(int argc, char **argv)
{
int num = 999;
string stra = "hello";
string &&straRef = std::move(stra);
cout << stra << endl;
cout << straRef << endl;
stra = "world";
cout << straRef << endl;
string a = "world";
string b = std::move(a);
cout << a << endl;
cout << b << endl;
return 0;
}
右值引用做参数
右值引用的最大贡献就是将临时变量的声明周期延长,减少临时变量的频繁销毁,内存的利用效率也会变高,当右值表达式被处理后结果存放在会块临时内存空间,右值引用指向它,则可以利用,直到指向它的右值引用全部被销毁,内存才会被释放
class Person
{
public:
string name;
Person(const string &name) : name(name)
{
cout << "string &name" << endl;
}
Person(string &&name) : name(name)
{
cout << "string &&name" << endl;
}
};
int main(int argc, char **argv)
{
Person person1("hello");
string s = "world";
Person person2(s);
return 0;
}
右值和左值引用成员函数
在旧标准中,右值可以调用相关成员函数与被赋值
int main(int argc, char **argv)
{
string hello = "hello";
string world = "world";
cout << (hello + world = "nice") << endl;
return 0;
}
&左值限定符
怎样限定赋值时右边只能是可修改的左值赋值,引入了引用限定符(reference qualifier),使得方法只有对象为左值时才能被使用
#define USE_LIMIT
class Person
{
public:
string name;
#ifdef USE_LIMIT
Person &operator=(const string &) &;
#else
Person &operator=(const string &);
#endif
Person(string &&name) : name(name)
{
}
inline void print()
{
cout << this->name << endl;
}
};
#ifdef USE_LIMIT
Person &Person::operator=(const string &name) &
#else
Person &Person::operator=(const string &name)
#endif
{
this->name = name;
return *this;
}
Person func()
{
return Person("me");
}
int main(int argc, char **argv)
{
func() = "hello";
return 0;
}
const与&左值限定符
一个方法可以同时用const和引用限定,引用限定必须在const之后
class Person
{
public:
int age;
string name;
Person(const int &age = 19, const string &name = "me") : age(age), name(name)
{
}
void print() const &
{
cout << age << " " << name << endl;
}
};
Person func()
{
return Person(19, "she");
}
int main(int argc, char **argv)
{
func().print();
return 0;
}
&&右值引用限定符
可以使用&& 进行方法重载,使其为可改变的右值服务 当一个方法名字相同 函数参数列表相同时 有一个有引用限定,全部都应该有引用限定或者全部都没有
class Foo
{
public:
Foo sort() &&;
Foo sort() const &;
};
Foo Foo::sort() &&
{
cout << "&&" << endl;
return *this;
}
Foo Foo::sort() const &
{
cout << "const &" << endl;
return *this;
}
Foo func()
{
return Foo();
}
int main(int argc, char **argv)
{
Foo foo1;
foo1.sort();
func().sort();
return 0;
}
移动构造函数和移动赋值运算符
资源移动实例,嫖窃其他对象的内存资源,与拷贝构造函数类似,移动构造函数第一个参数也是引用类型,但只不过是右值引用,任何额外参数必须有默认实参
class Person
{
public:
int *age;
string *name;
Person(const int &age, const string &name) : age(new int(age)), name(new string(name))
{
}
Person(Person &&person) noexcept
{
delete age, delete name;
age = person.age;
name = person.name;
person.age = nullptr;
person.name = nullptr;
}
void print();
};
void Person::print()
{
if (age && name)
{
cout << *age << " " << *name;
}
cout << endl;
}
int main(int argc, char **argv)
{
Person person1(19, "me");
Person person2 = std::move(person1);
person1.print();
person2.print();
return 0;
}
移动赋值运算符
与拷贝类似,可以也可以重载赋值运算符来实现对象的移动功能
class Person
{
public:
int *age;
string *name;
Person(const int &age, const string &name) : age(new int(age)), name(new string(name))
{
cout << "Person(const int &age, const string &name)" << endl;
}
Person &operator=(Person &&person) noexcept;
Person(const Person &person) : age(person.age), name(person.name)
{
cout << "Person(const Person &person)" << endl;
}
void print();
};
Person &Person::operator=(Person &&person) noexcept
{
cout << "Person &Person::operator=(Person &&person)" << endl;
if (&person != this)
{
delete age, delete name;
age = person.age;
name = person.name;
person.age = nullptr;
person.name = nullptr;
}
return *this;
}
void Person::print()
{
if (age && name)
{
cout << *age << " " << *name;
}
cout << endl;
}
Person func()
{
return Person(19, "she");
}
int main(int argc, char **argv)
{
Person person2(18, "oop");
person2 = func();
person2.print();
Person person1 = std::move(person2);
cout << *person1.age << " " << *person2.name << endl;
return 0;
}
合成的移动操作
只有当一个类没有定义任何自己版本的拷贝控制成员时,且所有数据成员都能进行移动构造或移动赋值时,编译器才会合成移动构造函数或移动赋值运算符 当一定义了拷贝控制成员,如自定义了拷贝构造拷贝拷贝赋值时,将不会提供合成的移动操作
class X
{
public:
int i;
string s;
};
class HasX
{
public:
X member;
};
int main(int argc, char **argv)
{
X x1;
x1.i = 100;
x1.s = "me";
cout << x1.i << " " << x1.s << endl;
X x2 = std::move(x1);
cout << x2.i << " " << x2.s << endl;
cout << x1.i << " " << x1.s << endl;
HasX hasx;
hasx.member.i = 99;
hasx.member.s = "me";
HasX hasx1 = std::move(hasx);
cout << hasx1.member.i << " " << hasx1.member.s << endl;
return 0;
}
本质上move的使用就是调用了拷贝构造函数,但拷贝构造是值拷贝还是指针拷贝有我们自己定义
class Y
{
public:
int age;
Y() = default;
Y(const Y &y)
{
this->age = age;
};
};
class HasY
{
public:
HasY() = default;
Y member;
};
int main(int argc, char **argv)
{
HasY hasy;
hasy.member.age = 999;
HasY hasy1 = std::move(hasy);
cout << hasy1.member.age << endl;
cout << "end" << endl;
return 0;
}
拷贝构造与移动构造的匹配
当一个类既有移动构造函数,也有拷贝构造函数,当我们使用哪一个,会根据函数匹配规则来确定
class Person
{
public:
Person() = default;
Person(const Person &person)
{
cout << "Person(const Person &person)" << endl;
}
Person(Person &&person)
{
cout << "Person(Person &&person)" << endl;
}
};
int main(int argc, char **argv)
{
Person person1;
Person person2(person1);
const Person person3;
Person person4(person3);
Person person5 = std::move(person4);
return 0;
}
要注意的是,当有拷贝构造函数没有移动构造函数时,右值也将被拷贝
class Person
{
public:
int age;
string name;
Person() = default;
Person(const Person &person) : age(person.age), name(person.name)
{
cout << " Person(const Person &person)" << endl;
}
};
int main(int argc, char **argv)
{
Person person1;
Person person2(std::move(person1));
return 0;
}
拷贝并交换赋值运算符和移动操作
当定义了移动构造函数,且定义了赋值运算符,但无定义移动赋值方法,则将一个右值赋给左值时,将会先使用移动构造函数构造新对象,然后将新对象赋值给原左值,类似地隐含了移动赋值
class Person
{
public:
int age;
string name;
Person() = default;
Person(const int &age, const string &name) : age(age), name(name){};
Person(Person &&p) noexcept : age(p.age), name(p.name)
{
cout << "Person(Person &&p)" << endl;
p.age = 0;
p.name = "";
}
Person(const Person &p) : age(p.age), name(p.name)
{
cout << "Person(const Person &p)" << endl;
}
Person &operator=(Person p)
{
cout << "Person &operator=(Person p)" << endl;
age = p.age;
name = p.name;
return *this;
}
};
int main(int argc, char **argv)
{
Person person1(19, "me");
Person person2(std::move(person1));
cout << person1.age << " " << person1.name << endl;
Person person3(19, "me");
Person person4;
person4 = std::move(person3);
cout << person4.age << " " << person4.name << endl;
return 0;
}
移动迭代器
移动迭代器解引用返回一个指向元素的右值引用
通过标准库make_move_iterator 函数可以将一个普通迭代器转换为移动迭代器返回
int main(int argc, char **argv)
{
vector<string> vec = {"aaa", "bbb"};
auto iter = make_move_iterator(vec.begin());
allocator<string> allocat;
string *ptr = allocat.allocate(10);
uninitialized_copy(make_move_iterator(vec.begin()), make_move_iterator(vec.end()), ptr);
cout << vec[0] << " " << vec[1] << endl;
cout << ptr[0] << " " << ptr[1] << endl;
return 0;
}
本质上是标准库算法在背后使用了移动迭代器解引用,进而相当于 stringa=std::move(stringb),将stringb移动到了stringa 只有当数据类型支持移动赋值时移动迭代器才显得有意义
|