- 13.1 13.2 13.3 13.4 13.5 13.6 13.7
- 13.8 13.9 13.10 13.11 13.12 13.13 13.14
- 13.15 13.16 13.17 13.18 13.19 13.20 13.21
- 13.22 13.23 13.24 13.25 13.26 13.27 13.28
- 13.29 13.30 13.31 13.32 13.33
- 总结
拷贝构造函数:本身是一个构造函数,其参 数是一个自身类类型的引用,且任何额外参数皆有默认值。
这是一个类的拷贝构造函数声明,需要使用引用类型的参数进行初始化。若使用非引用类型参数初始化时,会调用拷贝构造函数拷贝实参,但为了拷贝实参又需要拷贝构造函数,会进行无限循环。
StrBlob拷贝,创建新的智能指针指向原StrBlob类的vector<string>
,智能指针计数器+1。
StrBlobPtr是弱智能指针,计数器不增加。
foo_bar
函数的参数为非引用类型,需拷贝。- 在函数体中
arg
拷贝到local
对象。 local
拷贝到*heap
为拷贝赋值运算符local
、*heap
拷贝到pa[4]
中皆使用拷贝构造函数。- 使用拷贝构造函数、函数的返回类型非引用,也需要进行拷贝,使用拷贝构造函数。
class HasPtr
{
public:
HasPtr(const string &s = string()): ps(new string(s)), i(0){}
HasPtr(const HasPtr &ptr):ps(new string(*ptr.ps)), i(ptr.i) {}
private:
string *ps;
int i;
};
int main()
{
HasPtr a = HasPtr("1111");
HasPtr b (a);
cout << "数据" << endl;
cout << *a.ps << "\t" << *b.ps << endl; //1111 1111
cout << "地址" << endl;
cout << a.ps << "\t" << b.ps << endl; //011BEC78 011BF020
system("pause");
return 0;
}
结果相同,地址不同。
- 拷贝赋值运算符:即重载
=
运算符。 - 何时调用:使用
=
将对象的值复制给一个已经存在的实例。 - 合成拷贝赋值完成工作:将右侧运算对象的每个非static`成员赋值给左侧运算对象的对应成员。
- 什么时候生成合成拷贝赋值运算符: 如果一个类未定义自己的拷贝赋值运算符。
StrBlob
:创建一个新的智能指针指向同一个vector<string>
,计数器+1。StrBlobPtr
:同上,由于是弱智能指针,计数器不增加。
class HasPtr
{
friend int main();
public:
HasPtr(const string &s = string()) : ps(new string(s)), i(0) {}
HasPtr(const HasPtr &ptr) :ps(new string(*ptr.ps)), i(ptr.i) {}
HasPtr& operator = (const HasPtr &ptr)
{
delete ps; //删除原先所指的string
ps = new string(*ptr.ps);
i = ptr.i;
return *this;
}
private:
string *ps;
int i;
};
int main()
{
HasPtr a = HasPtr("1111");
HasPtr b(a);
cout << "数据" << endl;
cout << *a.ps << "\t" << *b.ps << endl; //1111 1111
cout << "地址" << endl;
cout << a.ps << "\t" << b.ps << endl; //011BF460 011BF268
system("pause");
return 0;
}
结果相同,地址不同。
#13.1.3 析构函数
- 析构函数:释放对象使用的资源,并销毁对象的非static数据成员。
- 合成析构函数:合成析构函数按对象创建时的逆序撤销每个非static成员,因此,它按成员在类中声明次序的逆序撤销成员。对于类类型的成员,合成析构函数调用该成员的析构函数来撤销对象。
- 合成析构函数生成时间:当一个对象被销毁时。
- 当对象的引用或指针超出作用域时,不会运行析构函数,只有删除指向动态分配对象的指针或实际对象(而不是对象的引用)超出作用域时,才会运行析构函数。(也就是说当一个对象的引用或者未释放空间的指针离开作用域时,析构函数不会执行)。
StrBlob
:所有数据成员被销毁,智能指针的计数器-1。StrBlobPtr
:同上,由于是弱智能指针,计数器不减少。
~HasPtr()
{
delete ps;
}
3次,accum
,item1
,item2
class X
{
public:
X(int a):val(a) { cout << "默认构造函数" << endl; }
X(const X &x):val(x.val){ cout << "拷贝构造函数" << endl; }
X& operator=(const X &x)
{
val = x.val;
cout << "赋值构造函数" << endl;
return *this;
}
~X()
{
cout << "析构函数" << endl;
}
int val;
};
int main()
{
X x1(10); //默认构造函数
X x2(x1); //拷贝构造函数
X x3 = x1; //拷贝构造函数
X x4(4); //默认构造函数
x4 = x1; //拷贝赋值运算符
system("pause");
return 0;
}
会输出一样的值,因为合成的拷贝构造函数。
会改变,三个不一样的数字。
与15题结果不同,但仍然是不一样的数字。
class numbered
{
public:
numbered() //默认构造函数
{
static int s_val = 0;
val = s_val++;
}
numbered(const numbered &n) //拷贝构造函数
{
val = n.val+10;
}
numbered& operator=(const numbered &n) //拷贝赋值运算符
{
val = n.val + 3;
return *this;
}
int val;
};
void f(numbered s)
{
cout << s.val << endl;
}
void f1(const numbered &s)
{
cout << s.val << endl;
}
int main()
{
numbered a1, a2;
f(a1); f(a2); //10 11
f1(a1); f1(a2); //0 1
numbered a3 = a1;
f(a3); //20
f1(a3); //10
numbered a4;
a4 = a1;
f(a4); //13
f1(a4); //3
system("pause");
return 0;
}
class Employee
{
public:
Employee(string n)
{
static size_t s_num = 1;
num = s_num ++;
name = n;
}
size_t num;
string name;
};
不需要拷贝控制成员,创建一个员工类的时候你会去找一个名字相同的员工? 而且编号的处理也会混乱。
所有成员(包括智能指针和容器)被拷贝或销毁。
因为使用的是智能指针,不需要自己定义析构函数。
TextQuery
类是保存数据,且没用到指针所以不需要定义拷贝控制成员。
QueryResult
类有指向TextQuery
成员的智能指针,这个类的目的就是读取对应TextQuery
类的文本,虽然这里使用拷贝构造没有意义,但是使用合成拷贝构造也是可以的。所以没有必要再定义拷贝控制成员。
见13.8 增加一个析构函数
~HasPtr()
{
delete ps;
}
如果没有析构函数,指针不会释放而导致内存泄漏。若未定义拷贝构造函数,合成拷贝构造函数会直接拷贝指针,而非指针所指向的值。
拷贝构造函数和拷贝赋值运算符需要为智能指针data
分配空间。
因为合成析构函数会销毁智能指针,而智能指针会释放空间。所以不需要析构函数。
StrBlob::StrBlob(const StrBlob &s)
{
data = make_shared<vector<string>>(*s.data);
}
StrBlob& StrBlob::operator=(const StrBlob &s)
{
data = make_shared<vector<string>>(*s.data);
return *this;
}
class HasPtr
{
public:
HasPtr(const string &s = string()) :ps(new string(s)),use(new size_t(1)) {}
HasPtr(const HasPtr &s) :ps(s.ps), use(s.use) { ++*use; }
~HasPtr()
{
if (--*use == 0)
{
delete ps;
delete use;
}
}
HasPtr& operator=(const HasPtr &s)
{
if (--*use == 0)
{
delete use;
delete ps;
}
use = s.use;
ps = s.ps;
++*use;
return *this;
}
void OutUse()
{
cout << *this->use << endl;;
}
private:
string *ps;
size_t *use;
};
int main()
{
//构造
HasPtr p1;
p1.OutUse(); //1
HasPtr p2;
p2.OutUse(); //1
//拷贝构造
HasPtr p3(p1);
p1.OutUse(); //2
p3.OutUse(); //2
//拷贝赋值
p2 = p1;
p2.OutUse(); //3
system("pause");
return 0;
}
类似于27题
#13.3 交换操作
因为swap
函数中的swap
是std版本中的,而第一个swap
是HasPtr
的。
类值版本的HasPtr
编写Swap
函数
class Hasptr
{
friend void swap(Hasptr&, Hasptr&);
public:
//默认构造函数
Hasptr(const string &s, int a=0):ps(new string(s)),i(a) {}
//拷贝构造函数,完成string 指针指向内容的拷贝和i值的拷贝
Hasptr(const Hasptr& p) :ps(new string(*p.ps)), i(p.i) {}
//拷贝赋值运算符
Hasptr& operator= (const Hasptr& p)
{
auto new_ps = new string(*p.ps);
delete ps;
ps = new_ps;
return *this;
}
//析构函数
~Hasptr() { delete ps; }
//输出ps
void OutPs()
{
cout << *ps << endl;
}
private:
string *ps;
int i;
};
inline void swap(Hasptr& a, Hasptr& b)
{
using std::swap;
swap(a.ps, b.ps);
std::swap(a.i, b.i);
cout << "swap" << endl;
}
int main()
{
Hasptr p1("p1"); Hasptr p2("p2");
p1.OutPs(); //p1
p2.OutPs(); //p2
swap(p1, p2);
p1.OutPs(); //p2
p2.OutPs(); //p1
system("pause");
return 0;
}
在上题代码中添加
//<运算符
bool operator< (const Hasptr &p)
{
cout << "<" << endl;
return ps->size() < p.ps->size();
}
以及输出检验函数
int main()
{
vector<Hasptr> vec;
vec.push_back(Hasptr("11111"));
vec.push_back(Hasptr("222"));
vec.push_back(Hasptr("333"));
vec.push_back(Hasptr("4444"));
vec.push_back(Hasptr("55555"));
sort(vec.begin(),vec.end());
for(auto pt : vec)
{
cout << *pt.ps << endl;
}
system("pause");
return 0;
}
类指针版本的HasPtr
直接使用自带的swap()
即可,自己写一个效果也是一样的。
- 不能使用
(Folder)
: 如果不引用的话会拷贝一个Folder对象,而我们在往folders
set中添加指针时指向的对象就不是我们希望的对象了,而是拷贝的那个对象。 - 不能使用
(const Folder &)
: 因为在save
中使用了Folder
的成员函数addMsg
,这会对Folder
的set进行更改,所以不能用const修饰。
##13.34
#include "main.h"
using namespace std;
void Message::save(Folder &f)
{
folders.insert(&f);
f.addMsg(this);
}
void Message::remove(Folder &f)
{
folders.erase(&f);
f.remMsg(this);
}
//将本Message添加到指向m的Folder中
void Message::add_to_Folders(const Message &m)
{
for (auto i : m.folders)
{
i->addMsg(this);
}
}
//移除指向本Message的Folder
void Message::remove_from_Folders()
{
for (auto i : folders)
{
i->remMsg(this);
}
}
//拷贝控制函数
Message::Message(const Message &m):contents(m.contents),folders(m.folders)
{
add_to_Folders(m);
}
Message::~Message()
{
remove_from_Folders();
}
Message& Message::operator=(const Message &m)
{
//通过先删除指针再插入它们来处理自赋值情况
remove_from_Folders();
contents = m.contents;
folders = m.folders;
add_to_Folders(m);
return *this;
}
void Folder::addMsg(Message *m)
{
messages.insert(m);
}
void Folder::remMsg(Message *m)
{
messages.erase(m);
}
Folder::~Folder()
{
for (auto i : messages)
{
i->remove(*this);
}
}
int main()
{
Folder f1("f1");
Message m1("m1");
m1.save(f1);
system("pause");
return 0;
}
#include <memory>
#include <string>
#include <set>
#include <stdlib.h>
using namespace std;
class Message
{
friend class Folder;
public:
//folders被隐式初始化为空集合
Message(const string &str = ""):contents(str) {}
//拷贝控制成员,用来管理指向本Message的指针
Message(const Message&);
Message& operator=(const Message&);
~Message();
//从给定Folder集合中添加/删除本Message
void save(Folder &);
void remove(Folder &);
private:
string contents; //实际消息文本
set<Folder*> folders; //包含本Message的Folder
//拷贝构造函数、拷贝赋值运算符和析构函数所用的工具函数
//将本Message添加到指向参数的Folder中
void add_to_Folders(const Message&);
//从folders中的每个Folder中删除本Message
void remove_from_Folders();
};
class Folder
{
friend class Message;
public:
Folder(const string str = ""):name(str) {}
Folder(const Folder &) = delete;
~Folder();
Folder operator=(const Folder&) = delete;
void addMsg(Message*); //往messages中添加
void remMsg(Message*); //在messages中删除
private:
string name;
set<Message*> messages;
};
在Folder
的set中将不会添加新的Message
地址。
见 13.34
void Message::addFolder(Folder *f)
{
folders.insert(f);
}
void Message::remFolder(Folder *f)
{
folders.erase(f);
}
当涉及到动态分配内存时,拷贝并交换是一个完成该功能的精简的方式. ,但是在Message类中,并未涉及到动态分配内存,这种方法并不会产生任何益处,同时还会因为很多指针操作让程序变得更复杂难难以实现。
#include "main.h"
using namespace std;
void StrVec::push_back(const string &s)
{
chk_n_alloc(); //确保有空间容纳元素
// 在first_free指向的元素中构造s的副本
alloc.construct(first_tree++, s);
}
pair<string*, string*> StrVec::alloc_n_copy(const string *b, const string*e)
{
//分配空间保存给定范围内的元素
auto data = alloc.allocate(e - b);
//初始化并返回一个pair,该pair由data和uninitialized_copy的返回值构成
return { data, uninitialized_copy(b,e,data) };
}
void StrVec::free()
{
//不能传递给deallocate一个空指针,如果ellement为0,函数什么也不做
if (elements)
{
for (auto p = first_tree; p != elements;)
alloc.destroy(--p);
alloc.deallocate(elements, cap - elements);
}
}
StrVec::StrVec(const StrVec &s)
{
//调用alloc_n_copy分配空间以容纳与s中一样多的元素
auto newdata = alloc_n_copy(s.begin(), s.end());
elements = newdata.first;
first_tree = newdata.second;
}
StrVec::~StrVec()
{
free();
}
StrVec &StrVec::operator=(const StrVec &rhs)
{
//释放原内存
free();
auto newdata = alloc_n_copy(rhs.begin(), rhs.end());
elements = newdata.first;
first_tree = newdata.second;
return *this;
}
void StrVec::reallocate()
{
//我们将分配当前大小两倍的空间
auto newcapacity = size() ? 2 * size() : 1;
//分配新内存
auto newdata = alloc.allocate(newcapacity);
//将数据从旧内存移到新内存中
auto dest = newdata; //指向新数组中下一个空闲位置
auto elem = elements; //指向旧数组中下一个元素
for (size_t i = 0; i != size(); ++i)
alloc.construct(dest++, move(*elem++));
//释放旧内存空间
free();
elements = newdata;
first_tree = dest;
cap = elements + newcapacity;
}
int main()
{
StrVec s1;
s1.push_back("aaa");
system("pause");
return 0;
}
#include <allocators>
#include <memory>
#include <string>
#include <stdlib.h>
using namespace std;
// 类vector类内存分配策略的简化实现
class StrVec
{
public:
StrVec() :elements(nullptr), first_tree(nullptr), cap(nullptr){} //默认初始化
StrVec(const StrVec&); //拷贝构造函数
~StrVec(); //析构函数
StrVec &operator=(const StrVec&); //拷贝赋值运算符
void push_back(const string&); //拷贝元素
size_t size() const { return first_tree - elements; }
size_t capacity() const { return cap - elements; }
string *begin() const { return elements; }
string *end() const { return first_tree; }
private:
allocator<string> alloc; //分配元素
//被添加元素的函数使用
void chk_n_alloc()
{
if (size() == capacity())
reallocate();
}
// 工具函数,被拷贝构造函数、赋值运算符和析构函数所使用
pair<string*, string*> alloc_n_copy(const string*, const string*);
void free(); //销毁元素并释放内存
void reallocate(); //获得更多内存并拷贝已有元素
string *elements; //指向数组首元素的指针
string *first_tree; //指向数组第一个空闲元素的指针
string *cap; //指向数组尾后指针的位置
};
StrVec::StrVec(initializer_list<string> lst)
{
auto newdata = alloc_n_copy(lst.begin(), lst.end());
elements = newdata.first;
first_tree = newdata.second;
}
如果后置会在第一个位置没有数据,并在最后在未分配的内存中插入数据,会出错。
StrVec
包含了string
的基本功能,可以替换使用。
##13.43 for_each(elements, first_tree, [this](string &s) {alloc.destroy(&s);});
##13.44 #include #include #include #include <stdlib.h> #include #include
using namespace std;
class String
{
friend int main();
public:
String() :begin(nullptr), end(nullptr) {}
String(const char *s, size_t size = 0)
{
auto newdata = alloc_n_copy(s, s+size);
begin = newdata.first;
end = newdata.second;
}
String(const String &s) //拷贝构造
{
auto newdata = alloc_n_copy(s.begin, s.end);
begin = newdata.first;
end = newdata.second;
}
String &operator=(const String &s)
{
free();
auto newdata = alloc_n_copy(s.begin, s.end);
begin = newdata.first;
end = newdata.second;
return *this;
}
~String()
{
free();
}
void free()//释放内存
{
if (begin)
{
for_each(begin, end, [this](char &rhs) {alloc.destroy(&rhs); });
alloc.deallocate(begin, end - begin);
}
}
private:
allocator<char> alloc; //分配元素
pair<char*, char*> alloc_n_copy(const char*b, const char*e)
{
//分配空间保存给定范围内的元素
auto data = alloc.allocate(e - b);
//初始化并返回一个pair,该pair由data和uninitialized_copy的返回值构成
return { data, uninitialized_copy(b,e,data) };
}
char *begin;
char *end;
};
#13.6 对象移动
-
左值引用,也就是“常规引用”,不能绑定到要转换的表达式,字面常量,或返回右值的表达式。而右值引用恰好相反,可以绑定到这类表达式,但不能绑定到一个左值上。
-
右值引用就是必须绑定到右值的引用,通过&&获得。右值引用只能绑定到一个将要销毁的对象上,因此可以自由地移动其资源。
-
返回左值的表达式包括返回左值引用的函数及赋值,下标,解引用和前置递增/递减运算符,返回右值的包括返回非引用类型的函数及算术,关系,位和后置递增/递减运算符。可以看到左值的特点是有持久的状态,而右值则是短暂的。
int f()
:非引用函数为右值,所以可以用右值引用
vector<int> vi(100)
:为左值,用左值引用
(a) int &&r1 = f();
(b) int &r2 = vi[0];
(c) int &r3 = r1;
(d) int &&r4 = vi[0]*f();
当push
一个时会拷贝一次,如果内存不够则会开辟一块新内存,再把vector
中已有的进行拷贝,再继续push
。
因此
push
一次就拷贝了一次。
push
两次就是1+1+1=3次,第一个1为push
第一个数; 第二个1是在push
第二个数时发现空间不够,将现有的一个数拷贝到更大的空间; 第三个1是push
第二个数。可以从图中看到三个1是如何增加的。
push
3个string就是3+2+1=6,4个就是6+3+1=10,5个就是10+4+1=15 ... 以此类推
Message &Message::operator=(Message &&m)
{
if (&m != this)
{
remove_from_Folders();
contents = move(m.contents);
move_Folders(&m);
}
return *this;
}
StrVec &operator = (StrVec &&s)
{
if(&s!=this)
{
free();
elements = move(s.elements);
first_tree = move(s.first_tree);
cap = move(s.cap);
}
return *this;
}
String &operator=(String &&s)
{
if(s != this)
{
free();
begin = move(s.begin);
end = move(s.end);
}
return *this;
}
按照13.48push
两次,会有两次移动构造,一次拷贝构造。
P418已经说的很清楚,使用的是移动操作,因为返回值相当于一个表达式,为右值
rhs是一个非引用的参数,所以需要进行拷贝初始化,依赖于实参的类型,拷贝初始化要么使用拷贝构造函数要么使用移动构造函数,左值被拷贝,右值被移动
hp的第一个赋值中,右侧为左值,需要进行拷贝初始化,分配一个新的string,并拷贝hp2所指向的string
hp的第二个赋值中,直接调用std::move()将一个右值绑定到hp2上,虽然移动构造函数和拷贝构造函数皆可行,但是移动构造函数是精确匹配且不会分配任何内存
HasPtr
的拷贝赋值运算符会重新分配空间,而移动赋值运算符直接将指针赋值就行。
具有更快的赋值速度,但是原对象会被销毁。 Hasptr& operator=(Hasptr &&p) { ps = p.ps; p.ps = nullptr; return *this; }
void push_back(string &&s) { data->push_back(move(s)); }
sorted不断调用本身,无限循环,造成堆栈溢出
#include <vector>
#include <iostream>
#include <algorithm>
#include <stdlib.h>
using std::vector;
using std::sort;
class Foo {
public:
Foo sorted() && ;
Foo sorted() const&;
private:
vector<int> data;
};
Foo Foo::sorted() &&
{
sort(data.begin(), data.end());
std::cout << "&&" << std::endl; // debug
return *this;
}
Foo Foo::sorted() const &
{
// Foo ret(*this);
// sort(ret.data.begin(), ret.data.end());
// return ret;
std::cout << "const &" << std::endl; // debug
// Foo ret(*this);
// ret.sorted(); //13.56
// return ret;
return Foo(*this).sorted(); //13.57
}
int main()
{
Foo().sorted(); // call "&&"
Foo f;
f.sorted(); // call "const &"
system("pause");
return 0;
}
-
主要是拷贝构造、拷贝赋值运算符、析构函数、移动构造、移动赋值运算符。
-
若没有自己定义拷贝构造、拷贝赋值运算符、析构函数,会自动进行合成。
-
移动操作会析构掉原来的对象。