目录
STL六大组件:容器、算法、迭代器、仿函数、适配器、空间配置器
一、标准IO库
cerr:ostream 对象,向标准错误写入数据(没有缓冲,直接发给屏幕)
getline 函数:从 istream 对象读取一行数据,写入 string 对象。
宽字符版本的IO类型和函数的名字以 w 开始(wcin→wistream)
头文件 iostream 定义了用于读写流的基本类型,fstream 定义了读写 命名文件的类型,sstream 定义了读写内存中 string 对象的类型。
1、IO流
IO对象无拷贝或赋值,所以不能将形参或返回类型设置为流类型,而通常以引用方式传递。且读写一个IO对象会改变其状态,因此也不能是const
1.1条件状态
只有当流处于无错状态时,才能从它写入或读取数据
while(cin>>i) 的终止条件:遇到了文件结束符,或者遇到了IO流错误,或者读入了无效数据。
badbit 表示系统级错误,如不可恢复的读写错误。通常情况下,一旦 badbit 被置位,流就无法继续使用了。在发生可恢复错误后,failbit 会被置位,如期望读取数值却读出一个字符。如果到达文件结束位置, eofbit 和 failbit 都会被置位。如果流未发生错误,则 goodbit 的值为0。 如果 badbit、failbit 和 eofbit 任何一个被置位,检测流状态的条件都会 失败。
while(in>>v, !in,eod()) //直到遇到文件结束符才停止读取
管理条件状态P281:
1.2 管理输出缓冲
由于设备的读写操作可能很耗时,允许操作系统将多个输出操作组合为单一的设备写操作可以带来很大的性能提升
cout << "hi!" << endl; // 输出hi和一个换行,然后刷新缓冲区
cout << "hi!" << flush; // 输出hi,然后刷新缓冲区,不附加任何额外字符
cout << "hi!" << ends; // 输出hi和一个空字符,然后刷新缓冲区
cout << unitbuf; // 所有输出操作后都会立即刷新缓冲区,cerr是设置unitbuf
cout << nounitbuf; // 回到正常的缓冲方式 如果程序异常终止,输出缓冲区是不会被刷新的。
当一个程序崩溃后, 它所输出的数据很可能停留在输出缓冲区中等待打印。
关联输入和输出流 tie
2、文件输入输出
头文件 fstream 定义了三个类型来支持文件IO:ifstream 从给定文件读取数据,ofstream 向指定文件写入数据,fstream 可以同时读写指定文件。
分别继承自istream、ostream、iostream
ios_base <- ios <- istream <- ifstream
所以对于:std::istream::operator>> 输入终端 cin 和 ifstream 都是 istream 的子类,所以输入操作符 >> 用法相同
istream& getline (istream& is, string& str);
从流对象is
中读取一行存到字符串str
直到遇到截止字符,如果遇到截止字符,则把它从流中取出来,然后丢弃(它不被存储,下一个操作的起点在它之后)函数调用前str
中的内容将被覆盖。
2.1 文件流对象
ifstream in(infile); //构造一个ifstream并打开给定文件(提供了文件名,则open自动调用)
检查文件是否被打开:
if(fin.fail())
if(!fin)
ifstream对象与istream对象一样,被放在需要bool类型的地方时,将被转换为bool值
if(!fin.is_open()) 最好的方式
同样,当一个fstream对象被销毁时,close会自动被调用,而关闭其关联的文件
2.2 文件模式
默认情况下为trunc
ofstream app(“file1”,ofstream::out | ofstream::app) //此处ofstream::out可不要,因为与 ofstream关联的文件默认以(out|trunc)模式打开
保留被 ofstream 打开的文件中已有数据的唯一方法是显式指定 app 或 in 模式。
3、string 流
#include<sstream>
istringstream可以从string读取数据,ostringstream向string写入数据,stringstream可读可写
二、顺序容器
1、容器库概览
- forward_list 和 array 是C++11新增类型。与内置数组相比,array 更安全易用。 array 对象的大小是固定的,forward_list 没有 size 操作。
- 如果程序有很多小的元素,且空间的额外开销很重要,则不要使用 list 或 forward_list。
- 不确定应该使用哪种容器时,可以先只使用 vector 和 list 的公共操作:使用迭代器,不使用下标操作,避免随机访问。这样在必要时选择 vector 或 list 都很方便。
C++11新增cbegin和cend函数
auto it3 = v.cbegin(); // it3的类型是vector<int>::const_iterator
array<int, 10>::size_type i; array相比于内置数组类型,一个很大的区别就是array可以进行拷贝或对象赋值操作
1.1 容器初始化
1.2 赋值(swap与assign)
v1.swap(v2);
swap(v1, v2);
将v2赋值给v1,此时v2变成了v1 将vec与本身的元素互换。实质上只是交换vector中用于指示空间的三个指针而已,也就是空间的交换实际是指针指向的交换,所以其速度比v2向v1拷贝元素快得多
但是对于array,执行swap操作将真正交换他们的元素
assign,仅顺序容器(array除外)
允许我们从一个不同但相容的类型赋值,或者从容器的一个子序列赋值,以替换左边容器中的所有元素
list<string> names;
vector<const char*> oldstyle;
names = oldstyle; // 错误: 容器类型不匹配,注意将一个容器初始化为另一个容器的拷贝时,两个容器的容器类型和元素类型都必须相同
// 正确:可以将const char*转换为string
①names.assign(oldstyle.cbegin(), oldstyle.cend());
②names.assign(10, “Hiya!”);
所有容器支持关系运算(==和!=)
除无序关联容器外,其他容器都支持关系运算符(>、>=、<、<=)
关系运算符两边的运算对象必须是相同类型、相同元素类型。其比较方法与string类似
2、顺序容器操作
2.1 添加元素和删除元素
//返回指向新添加的第一个元素的迭代器
c.insert(p,t) c.insert(p, n, t) //在p指向的元素前插入n个t
c.insert(p, b, e) //在p所指元素前插入b,e迭代器范围内的元素
c.insert(p, il) //il是一个花括号包起来的元素值列表 {“I”, “am”, “a”, “teacher”}
push_front (还有pop_front) 但是vector和string都不支持push_front
svec.push_front(“Hello!”) 相当于:
vector<string> svec;
svec.insert(serv.begin() , “Hello!”); //当然插入到vector末尾之外的任何位置都可能很慢
earse() 返回指向最后一个被删元素之后为止的迭代器
emplace 将参数传递给元素类型的构造函数,直接在容器的内存空间中构造元素。
c.emplace_back("978-0590353403", 25, 15.99); //三个参数将传递给Sales_data的构造函数
//直接初始化
相当于 c.push_back(Sales_data("978-0590353403", 25, 15.99)); //拷贝初始化
2.2 访问元素
2.3 容器大小操作
resize的第二个参数指定当容器变长时填充在新位置上的值
reserve():只修改capacity的大小,不修改size的大小(修改后capacity大于或等于传递给reserve的参数)
resize():既修改capacity的大小,也修改size的大小 不适用于array
forward_list 支持 max_size 和 empty,但不支持size
max_size()返回一个大于或等于该类型容器所能容纳的最大元素数的值
2.4 特殊的forward_list操作
由于删除或添加的元素之前的那个元素的后继会发生改变,所以要访问这个前驱,以改变它的链接,但是单向链表没有简单的方法来获取一个元素的前驱
forward_list 并未定义 insert、emplace 和 erase,而是定义了名为 insert_after、emplace_after和 erase_after 的操作
p = lst.before_begin() //返回指向dummyhead的迭代器 P313
lst.erase_after(p) //删除首元素
2.5 vector是如何让增长的
容器必须分配新的内存空间来保存已有元素和新元素,将已有元素从旧位置移动到新空间,然后添加新元素,释放旧内存空间,这样每添加一个新元素,就要执行这样的一次内存释放和分配操作,性能极低。
所以, vector 和 string 的实现通常会分配比新空间需求更大的内存空间,容器预留这些空间作为备用,可用来保存更多新元素 (capacity>=size)
当size超过capacity时,再次分配新的内存空间
在C++11中可以使用 shrink_to_fit 函数来要求 deque、vector 和 string 退回不需要的内存空间(并不保证退回)
清空vector容器
int main(){
vector <int> vecInt;
for (int i=0;i<50;i++)
{
vecInt.push_back(i);
}
cout<<"capacity:"<<vecInt.capacity(); //j=64
cout<<"size:"<<vecInt.size(); //i=50
cout<<endl;
//1、使用clear ,清空元素,不回收空间
vecInt.clear();
cout<<"capacity:"<<vecInt.capacity(); //j=64
cout<<"size:"<<vecInt.size(); //i=50
cout<<endl;
//2、erase循环删除,不回收空间
for (int i=0;i<50;i++)
{
vecInt.push_back(i);
}
for ( vector <int>::iterator iter=vecInt.begin();iter!=vecInt.end();)
{
iter=vecInt.erase(iter);
}
cout<<"capacity:"<<vecInt.capacity(); //j=64
cout<<"size:"<<vecInt.size(); //i=50
cout<<endl;
//3、使用swap,清除元素并回收内存
vector <int>().swap(vecInt); //清除容器并最小化它的容量,
// vecInt.swap(vector<int>()) ; 另一种写法
cout<<"capacity:"<<vecInt.capacity(); //j=0
cout<<"size:"<<vecInt.size(); //i=0
cout<<endl;
}
思考:为什么swap可以回收内存?
3、额外的string操作
getline(str,’:’) //读到‘:’,抛弃: 与 cin.getline(info,100,’:’); 效果相同
size()、length()返回字符串的字符数
s.substr(pos, n) 默认是拷贝pos开始的所有字符 P321
string s5 = "hiya"; // 拷贝初始化
string s6("hiya") ; // 直接初始化
string s7(10, 'c'); // 直接初始化,s7的内容是cccccccccc
在执行读取操作时,string 对象会自动忽略开头的空白(空格符、换行符、制表符等)并从第一个真正的字符开始读取,直到遇见下一处空白为止。
string的构造、substr(截断其中一部分)、insert、erase、assign(替换s本身)、append(尾部追加)、replace(替换s中的一部分)、find(搜索字符或者字符串)、compare(相当于strcmp)、to_string(数值转换)
4、适配器
一个容器适配接受一种已有的容器类型,使其行为看起来像一种不同的类型。
默认情况下,stack 和 queue 是基于 deque 实现的,priority_queue 是基于 vector 实现的。三者的相关操作见 P330(push、pop、top)
priority_queue
priority_queue允许我们为队列中的元素建立优先级。新加入的元素会排在所有优先级比它低的已有元素之前
优先队列具有队列的所有特性,包括基本操作,只是在这基础上添加了内部的一个排序,它本质是一个堆实现的
priority_queue<Type, Container, Functional>
//升序队列
priority_queue <int,vector<int>,greater<int> > q;
//降序队列
priority_queue <int,vector<int>,less<int> >q;
默认是大顶堆
所有适配器都要求容器具有添加和删除元素的能力,因此适配器不能构造在 array 上。适配器还要求容器具有添加、删除和访问尾元素的能力,因此也不能用 forward_list 构造适配器。
stack<string, vector<string>> str_stk2(svec); // strstk2在vector上实现,初始化时保存svec的拷贝
三、泛型算法
泛型:可用于不同类型的元素和多种容器类型(及类型的序列)
算法:实现了一些经典算法的公共接口
迭代器的使用令算法不依赖于容器类型
1、初始泛型算法
1.1 只读算法
find
前两个参数用迭代器表示元素范围,第三个参数为一个元素值
#include<algorithm>
find(vec.cbegin(), vec.cend(), val) //从vector对象vec中查找val
其中底层实现包含着各种类型元素之间的比较操作(==),运算符重载
C++11在头文件 iterator 中定义了两个名为 begin 和 end 的函数,功能与容器中的两个同名成员函数类似,参数是一个数组。
#include<iterator>
int ia[] = {0,1,2,3,4,5,6,7,8,9}; // ia是一个含有10个整数的数组
int *beg = begin(ia); // 指向ia首元素的指针
int *last = end(ia); // 指向arr尾元素的下一位置的指针
int* result = find(begin(ia), end(ia), val);
accumulate
#include<numeric>
accumulate(v.cbegin(), v.cend, string(“ ”)); //对vector中的元素求和,第三个参数是和的初值
底层实现(+),这里则被重载为字符串拼接,序列中元素类型必须与第三个参数相匹配
accumulate(v.cbegin(), v.cend, “ ”); 编译出错,字符串字面值将对应于cosnt char *,这种类型不存在“+”运算符
equal
前两个参数用迭代器表示一个子序列,第三个参数表示第二个序列的首元素
因为要求第一个序列的每个元素,在第二个序列中都有一个与之对应的元素,所以第二序列需要不短于第一序列
equal(roster1.cbegin(), roster.cend(), roster2.cbegin());
底层实现(==),两个序列的元素可以不一样,只要能用==来比较就可以了
1.2 写容器元素的算法
fill
可见,执行泛型算法时,修改容器元素甚至需要扩大容器的情况下,需要借助插入迭代器
拷贝算法
copy(begin(a1), end(a1), a2) //将前两个参数指定的输入范围中的元素拷贝到第三个参数指
//定的目标序列中
replace(ilst.begin(), ilst.end(), 0, 42); //将序列中所有等于0的元素替换为42
replace_copy(ilst.cbegin(), ilst.cend(), back_inserter(ivec), 0, 42);
从cbegin和cend也可以看出,原序列ilst并未改变,而是将replace后的ilst的拷贝放在了ivec中
_copy版本的算法,都是写到额外目的空间
排序算法
sort(words.begin(), words.end()) 底层(<)
unique(words.begin(), words.end()) //不重复元素会排在前面(相邻重复元素被覆盖,返回的迭代器指向最后一个不重复元素之后的位置,再后面的元素将未知)
容器大小不会变,算法不能添加(或删除)元素,标准库算法是对迭代器而不是容器进行操作。
remove_copy_if(v.begin(), v.end(), back_inserter(v1), [](int i { return i%2; }));
2、定制操作
向算法传递函数
谓词是一个可调用的表达式,其返回结果是一个能用做条件的值
接受谓词参数的算法对输入序列中的元素调用谓词,因此,元素类型必须能转换为谓词的参数类型
sort(words.begin(), words.end(), isShorter);
2.1 lambda表达式
_if版本的算法,查找使得可调用对象返回非零值的元素
(这里的捕获问题到P507学习了生成类后再理解)
transform(vi.begin(), vi.end(), vi.begin(), [](int i) { return i<0 ? -I : i; });
算法对输入序列(前两个参数)中的每个元素调用可调用对象,并将结果写到目的位置(第三个参数)
for_each接受两个参数、一个可调用对象
2.2 参数绑定
除了lambda表达式,还有一个方法调用bind可将函数变成一个可调用对象
bind
#include<functional> //bind函数
using namespace std::placeholders; //占位符
auto check6 = bind(check_size, _1, 6);
bool b1 = check6(s) //check6(s)会调用check_size(s, 6)
auto wc = find_if(words.begin(), words.end(), bind(check_size, _1, sz)); //对比lambda表达式
_1对应string类型参数,sz是被绑定的参数
3、迭代器
3.1 插入迭代器
back_inserter(push_back)、front_inserter(push_front)、inserter( insert(t,p) )
插入器是一种迭代器适配器,它们接受一个容器,生成一个迭代器
back_inserter接受一个指向容器的引用,返回一个与容器绑定的插入迭代器
auto it = inserter(vec);
*it = val 相当于:
- it = vec.insert(it, val); //it指向新加入的元素(注意insert的val是插入在it所指向的元素之前的)
- ++it; //递增it使它指向原来的元素
3.2 iostream迭代器
istream_iterator<int> in_iter(cin); //从cin读取int型数据
ostream_iterator<int>out_iter(cout, “ ”); //输出数据到cout,且每输出一个int加一个空格
out_iter可看成cout的一个元素
当然这里更好的方法是调用copy:
copy(vec.begin(), vec.end(), out_iter);
3.3 反向迭代器
sort(vec.rbegin(), vec.rend()) ;
4、一些特定容器算法
链表定义的几种成员函数形式的算法 merge lst.merge(lst2, comp) 会改变容器,lst2中的元素会被删除,而通用版本的merge不会
remove、reverse、sort、unique
lst.splice(p, lst2) 或 flst.splice_after(p,lst2,p2) //将lst2的全部或者其中一个元素(p所指)或者一段元素范围(b、e指定),移动到(相当于剪切)lst中p迭代器所指的元素的前面或者后面
四、关联容器
// 统计每个单词在输入中出现的次数
map<string, size_t> word_count; // string到size_t的空map
string word;
while (cin >> word)
++word_count[word]; // 提取word的计数器并将其加1
for (const auto &w : word_count) // 对map中的每个元素
// 打印结果
cout << w.first << " occurs " << w.second
<< ((w.second > 1) ? " times" : " time") << endl;
1、关联容器概述
map 和 multimap 类型定义在头文件 map 中;
set 和 multiset 类型定义在头文件 set 中;
无序容器定义在头文件 unordered_map 和 unordered_set 中。
允许重复保存关键字的容器名字都包含单词 multi;无序保存元素的容器名字都以单词 unordered 开头。
map类型通常称为关联数组,与普通数组的差别在于它是通过一个关键字而不是位置来查找值
初始化
map<string, size_t> word_count; // 空容器
// 列表初始化
set<string> exclude = { "the", "but", "and" };
// 三个元素; authors将姓映射为名
map<string, string> authors =
{
{"Joyce", "James"},
{"Austen", "Jane"},
{"Dickens", "Charles"}
};
1.1 关键字类型的要求
对于有序容器,关键字类型必须定义元素比较的方法,默认情况下为<
如果需要使用自定义的比较操作,则必须在定义关联容器类型时提供此操作的类型。操作类型在尖括号中紧跟着元素类型给出。 P379
multiset<Sales_data, decltype(compareIsbn)*> bookstore(compareIsbn);
这里decltype想通过一个函数名获得函数指针类型,必须加上*
1.2 pair类型
- pair的默认构造函数对数据成员进行值初始化
- pair的数据成员是public的
- pair对象是map的元素
#include<utility>
pair<string, int> process(vector<string> &v)
{
// 处理v
if (!v.empty())
// 列表初始化
return { v.back(), v.back().size() };// 隐式构造返回值
return pair<string, int>();//显式构造返回值
return pair<string, int>(v.back, v.back().size());
//make_pair
return make_pair(v.back(), v.back().size());
}
2、关联容器操作
与顺序容器一样,使用作用域运算符来提取一个类型的成员
set<string>::value_type v1; // v1是一个string
set<string>::key_type v2; // v2是一个string
map<string, int>::value_type v3; // v3是一个pair<const string, int>
map<string, int>::key_type v4; // v4是一个string
map<string, int>::mapped_type v5; // v5是一个int
一个map的value_type是一个pair,可以改变pair的值,但不能改变pair的关键字成员的值(关键字是const的)
同样的,因为set容器中保存的值就是关键字,所以也是只读的,set的迭代器就是const的
2.1 关联容器和算法
由于关键字是const这一特性,意味着不能使用修改或重排容器元素的算法,可用于只读取元素的算法,但使用关联容器定义的find成员又会比调用泛型find快得多
一般只当作一个原序列,或当作一个目的位置(copy、inserter)时,可以使用算法
添加元素(insert)
可以是insert(b, e) //插入迭代器所指范围的元素
// 向word_count插入word的4种方法
word_count.insert({word, 1});
word_count.insert(make_pair(word, 1));
word_count.insert(pair<string, size_t>(word, 1));
word_count.insert(map<string, size_t>::value_type(word, 1));
insert的返回值
对于map:
pair<map<string, size_t>::iterator, bool> ret = word_count.insert(make_pair(word, 1)); //注意现今的编译器,ret前面复杂的声明部分已经不需要了
++ret.first->second; P385
可见它的返回值是一个pair类型,pair的first成员是一个迭代器,它指向word_count中具有关键字word的元素;pair的second成员是一个bool类型,如果关键字word已在容器中,则insert什么也不做,bool为false,否则插入该元素,bool值为true
对于mutimap和mutiset:
直接插入,返回一个指向新元素的迭代器
删除元素 erase
与顺序容器一样,支持一个迭代器参数和两个迭代器范围参数;不同的是:
word_count.erase(val); //删除所有匹配给定关键字val的元素,返回实际删除的元素的数量
map的下标操作 []、at()
(只对map可用)
map
下标运算符接受一个关键字,获取与此关键字相关联的值。如果关键字不在容器中,下标运算符会向容器中添加该关键字,并值初始化关联值。
hashtable[nums[i]] = i //(nums[i],i)由于不存在该关键字的话会直接添加,所以可视为一种插入数据的方法
由于下标运算符可能向容器中添加元素,所以只能对非 const
的 map
使用下标操作。
at
· 如果存在:返回key对应的值,可以直接修改,和[]操作一样。
· 如果不存在:抛出 out_of_range 异常.
mymap.at(“Mars”) = 3396; //mymap[“Mars”] = 3396
下标运算返回左值,可读可写:
访问元素
P391
P392的单词转换程序
3、无序容器
使用一个哈希函数和关键字类型的==运算符来组织元素
五、动态内存
静态内存:局部static对象、类static数据成员、定义在任何函数之外的变量
栈内存:定义在函数内的非static对象
栈对象仅在其定义的程序块运行时才存在,static对象在使用之前分配,在程序结束时销毁
自由空间或堆(内存池):动态分配的对象,即在程序运行时分配的对象
1、动态内存与智能指针
1.1 shared_ptr ***
可见内置指针(new分配)可作为参数构造出一个shared_ptr智能指针,见1.2节“new与shard_ptr”
p.get() //返回p中保存的内置指针,但是要谨慎使用,若智能指针释放了其对象,则返回的指针所指向的对象也就消失了
每个shared_ptr都会记录有多少个shared_ptr指向相同的对象,一旦一个shared_ptr的计数器变为0,它就会自动释放自己所管理的对象
#include<memory>
shared_ptr<list<int>> p1; //shared_ptr,可以指向int的list,默认初始化保存一个空指针
make_shared函数
在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr
#include<memory>
可见make_shared传递的参数,跟emplace一样,能直接与<>中的某个构造函数相匹配(或者能用来初始化一个对象),直接构造一个对象,并用shared_ptr指向它
shared_ptr的拷贝和赋值
无论何时我们拷贝一个shared_ptr ,计数器都会递增
-
一个对象初始化另外一个对象时
-
作为参数传递给一个函数时
-
作为函数的返回值时
当我们给shared_ptr赋予一个新值或是shared_ptr被销毁(例如一个局部的shared_ptr离开其作用域时,计数器就会递减。
智能指针是类类型,它是通过析构函数(destructor)完成销毁工作的
StrBlob类的设计
使用shared_ptr管理动态内存的类设计示例,它是利用一个标准库容器来管理一组可变大小的内存空间 P405
1.2 直接管理内存 new
在自由空间分配的内存是无名的,因此new无法为其分配的对象命名,而是返回一个指向该对象的指针
动态分配的对象是默认初始化的(内置类型或组合类型的对象的值将是未定义的)
int *pi = new int; //pi指向一个未初始化的int
而类类型对象将用默认构造函数进行初始化:
int *ps = new string; //初始化为空string
可以使用直接初始化的方式来初始化一个动态分配的对象:
int *pi = new int(1024); //pi指向的对象的值为1024
int *pi = new int(); //值初始化为0,*pi为0
auto p1 = new auto(obj) //auto类型由obj类型推断,所以括号中的obj显然只能有一个
定位new
当内存耗尽时,new会失败,它会抛出一个bad_alloc异常
使用定位new,可阻止抛出异常,而是返回一个空指针
#include<new>
int *p1 = new int; //分配失败,则new抛出std::bad_alloc
int *p2 = new (nothrow) int; //分配失败,返回空指针
①p1 = new (buffer) int[20]; //从buffer中分配内存空间给一个包含20个元素的int数组,再将其地
址赋给指针p1
注意delete可与常规new运算符配合使用,但不能与定位new运算符配合使用
②pc1 = new (buffer) JustTesting; //pc1是一个指向JustTesting类的指针,buffer是一个指向字符
数组的指针
这种情况下,要删除该语句创建的类对象(默认构造函数创建),必须显式地调用析构函数: pc1 -> ~ JustTesting( );
new与delete
instance_ = new Singleton;
这条语句实际上做了三件事,第一件事申请一块内存,第二件事调用构造函数,第三件是将该内存地址赋给instance_。
注意delete与new一样,是包括两个动作:销毁给定的指针指向的对象;释放对应的内存
传递给delete的指针可以是一个空指针;如果释放了一块非new分配的内存,或者将相同的指针值释放多次,结果是未定义的(编译器无法分辨这两种情况是错误的)。
P410考虑一种情况,一个函数内new了一块动态内存,定义指针p指向它,但是当p离开作用域被释放以后,就再也没有指针指向这块内存了,也就没有办法释放这块内存了。这很容易导致空悬指针的产生
new与shared_ptr
int *p(new int(42)); //直接初始化,p指向动态内存
shared_ptr<int> p2(new int(1024)); //只能直接初始化,而不能使用一个内置指针拷贝初始化的方式
shared_ptr<int> p2 = new int(1024); //错误,要完成这个拷贝初始化,意味着右值必须是一个shared_ptr,也就是new int(1024)实际上一个指向值为1024的int *,它必须隐式转换为一个智能指针,而接受指针参数的智能指针构造函数是explicit的,所以无法完成这个隐式转换
P414 p.get()用法注意事项
if(!p.unique) //p.use_count()返回与p共享对象的智能指针数量,大于等于1则返回true
p.reset(new int(1024)); //可用于修改p的指向,因为它释放了p原来指向的对象,又令p指向参数q,这里的q是new int(1024)返回的指针
修改shared_ptr默认的delete操作
可以想到,如果在new与delete之间发生异常,内存将不被释放,所以需要使用shared_ptr
当然这个"q"也可以是一个shared_ptr,这样p就是q的拷贝
d的示例(当然改变delete操作,一般被用在智能指针管理的资源不是new分配的内存的情况,更进一步,当一个类没有定义析构函数,可能导致内存泄漏(忘记或者发生异常)的情况也是会借助shared_ptr指针的,这个时候shared_ptr所管理的资源就不是new分配的内存)P415:
void end_connection(connection *p) { disconnect(*p); }
shared_ptr<connection> p(&c, end_connection);
可见此时的可调用对象end_connection已经取代了delete,同样完成connection对象资源的释放操作
智能指针陷阱 P417
1.3 unique_ptr
1、定义一个unique_ptr时,需要将其绑定到new返回的指针,类似shared_ptr必须采用直接初始化:
unique_ptr<int> p(new int(42)); //p指向一个值为42的int
2、由于一个unique_ptr“拥有”它指向的对象,它持有对对象的独有权,即两个 unique_ptr 不能指向一个对象,因此unque_ptr不支持普通拷贝或赋值操作,只能进行移动操作;但是也有例外,可以拷贝或赋值一个将要被销毁的unique_ptr:
std::unique_ptr<int>p1(new int(5));
std::unique_ptr<int>p2=p1;// 编译会出错
std::unique_ptr<int>p3=std::move(p1);// 转移所有权, 现在那块内存归p3所有, p1成为无效的针.
p3.reset();//释放p3指向的内存,p3置为空
p1.reset();//无效
分析:std::move()返回绑定到左值上的右值引用,即返回的是一个右值引用,可见它是一个具体的值,这个引用绑定到了一个左值,即p1,p1是一个指针,保存的是一块内存的地址,所以这个右值引用不就是这块内存的地址,然后p3指向这个右值引用,也就是指向了这块内存
所以整个p3=std::move(p1)语句,并没有发生任何的拷贝,只是将指针指向了这块内存,称之为移动这块内存
但是对于移后源对象p1的值不做任何假设,所以可以销毁它,也可以赋予它新值,但不能使用这个值。
3、结合p.release()和p.reset()完成unique的“拷贝与赋值”操作 P418
p1.reset(p2.release());
分析:这里的release会切断unique_ptr和它原来管理的对象间的联系,p2置空,同时返回一个指针
此时应该让一个新指针来保存它,否则就没有指针指向这块内存了,导致“内存泄漏”:
auto p3 = p2.release(); //其实就是智能指针p放弃了对内存的控制权,同时返回一个指向这块内存的内置指针,接住它就好了
而p1.reset(p3)的作用是,p1先释放它指向的内存,然后重新指向p3,所以结果就是p1指向p2所指向的内存,完成了赋值操作
修改unique_ptr的默认删除器delete
其方法类似重载关联容器的比较操作 P419
unique_ptr<connection, decltype(end_connection)*> p(&c, end_connection);
unique_ptr与shread_ptr管理删除器的方式的比较 P599
1.4 weak_ptr
一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象
用途:由于它不影响shared_ptr所管理对象的生存周期,所以可以用于查看与它共享对象的其他shared_ptr 的情况
w.lock() 函数的妙用
2、内存管理
详解new与delete关键字
全局new
使用new关键字时,会被编译器转为三个步骤,其中第一步是operator new的函数调用,该函数内部会使用malloc申请内存
new的源码中,如果malloc调用失败(内存耗尽),将会执行_callnewh,自定义的一个函数,用于释放一些不需要的内存,释放后又能回到malloc调用,此时可能成功
内存分配
使用new
容器的内存分配:
3、动态数组
array new
在执行左上角的new Foo[N]语句时,编译器将自动执行try中的语句,其中第一步是先执行operator new,申请一块内存,该语句调用的版本,如果Foo类中重载了new,则调用重载版本,否则调用全局::operator new()。接着强制转换指针类型,然后调用N次构造函数创建N个类对象
typedef int arrT[42];
int *p = new arrT; //new分配要求数量的对象并(假定分配成功后)返回指向第一个对象的指针
//注意这里的动态数组并非真的是一个数组类型的对象,而是一个数组元素类型(int)的指针,
int *pia = new int[get_size()] //方括号中大小必须是整型,但不必是常量
//get_size甚至可以是0,此时返回一个合法的非空指针,当然它不指向任何元素,类似于尾后指针
从上图中可以看出,动态数组将执行多次对象的构造
上图为例,一个Foo类对象占12字节(int、long、string各占4字节),执行operator new()以后,内存为64字节(12*5,另外4字节是一个计数器,其值为5,表示new分配的这个动态数组中有5个Foo对象),之后执行5次ctor(构造函数),也因此必须搭配delete[] 使用,才能对应执行5次dtor(析构函数),从图中可见这五次构造与析构的执行顺序。
以下是调用一次malloc申请的内存情况:
头尾cookie记录着这块内存块的大小,这是在new调用malloc时自动生成的,当要释放这段内存而调用 delete [] pac 时,pca指针也是凭借这个cookie才知道具体要释放多大的内存:
内存管理之内存池设计
使用一个单向链表,将::operator new(BOLCK_SIZE*sizeof(Airplane))申请的一整块内存,按sizeof(Airplane),即8字节为一块小内存,通过next指针串接在一起
newBlock[i].next=&newBlock[i+1]; //其中newBolck是Airplane *
可以减少malloc的调用次数(尽管malloc很快),可以提高空间使用率(因为每次调用malloc申请的内存块的两端都需要各自耗费四字节保存一个cookie)
这里的union设计节省了next指针的使用空间,因为两者只存其一(Airplane本身因占5字节,对齐后占8个字节,而新添加的指针next是为了管理内存池的连接使用的新增数据,占四字节),如果不用union,这个类占用字节将达到16字节。现在只需要8字节:
embedded指针的实现是借助union联合体,union的所有成员相对于基地址的偏移量都为0,且联合体中的所有成员共享这一段内存,其中的成员在同一时间只有一个是被初始化的
当调用malloc分配一大块内存以后,先按照类对象的大小将内存分片,然后利用每块分片后的内存的前四个字节作为embedded指针,将这些空闲内存分片串接在一起方便管理。当要构建实例化一个类对象时,就从这个free list中取出一块并存储类的成员数据(此时由于union机制,embedded失效)
对于内存的回收,是在析构完这块内存所存储的对象以后,前四个字节重新为embedded指针,并接回free list中
以上解释了很多问题:(为什么C开发的结构体定义中,结构体成员最后要定义一个char data[0] ,为什么C++类,即使没有成员,它也是占用四个字节的)就是因为即使没有数据成员,一个类至少要占四个字节,以供embedded指针使用,方便内存管理
上图中carcass是Airplane*指针,指向准备释放的那块内存;但是这里并不是真的释放,而是将其收回插入到free list的前端,可用于重复使用,同时更新free list的头指针到carcess。
但是如果用到一个需要内存管理的类就设计一个operator new,以执行以上操作是很复杂的事情,所以将这些操作(内存池的分配与回收)封装成一个独立的allocator类:
封装的allocate类如下:
当然可以借助宏定义更进一步降低类的设计复杂度,并且达到内存管理的目的:
allocator类
new将内存分配和对象构造组合在了一起,类似的delete则将对象析构和内存释放组合在了一起。而allocator将内存分配和对象构造分离
#include<memory>
allocator<string> alloc;
auto const p = alloc.allocate(n); //分配未构造的内存,用于保存n个类型为string的对象
alloc.construct(p++,args); //在给定内存位置构造一个元素args
alloc.destroy(--p); //释放构造的string对象
alloc.deallocate(p, n); //释放这块大小为n的内存
其他一些拷贝和填充未初始化内存的算法 P429
标准分配器都是简单调用::operator new(),每次调用都有两个cookie,空间利用率不高
std::alloc
前面是使用一条自由链表,专门为一个类中类对象的内存分配服务
现在是同时维护16条大小不一的自由链表,每种相差8个字节,一直到128字节(如果元素类型超过128字节则调用malloc,不在此服务范围内)
当容器第一次申请一个元素所占内存,将调用一次malloc,一次申请的字节数为:
sizeof(容器元素)*(1~20)*2+RoundUp(已申请内存大小>>4) //其中RoundUp是字节对齐,分片数量要尽量大,但是最大20
其中内存池pool大小就是sizeof(容器元素)*(1~20)+RoundUp(已申请内存大小>>4)
当由该容器继续存储元素时,是从剩下的19个free list上的内存分片中取出并用于存储,headofFreeList移动
而当有新的容器(存储的元素类型不一样),则从内存池pool中同样申请sizeof(容器元素)*20新构成一个free-list:
此时内存池容量为0,如果再有新的容器要求存储器元素,就必须再次调用malloc分配新的内存了
当然如果这里还有一点点余量,但是又不足以应对之后新容器新元素的大小,此时所剩内存即为内存碎片。
假设之前是在0号链表,内存池中剩余80字节,现在新容器申请104字节,解决方法如下,将80字节直接接到9号链表等待使用
当然所剩空间足够应对下一个新元素类型的话,就分配尽量多的内存分片就行了(前面说的一次分片是1~20)
如果堆内存用尽,malloc失败,此时会从右边找比自己尺寸大,但是尽量与自己相同的空闲内存块,供当前链表使用。一直找到16号链表还是没有空闲内存块的话,malloc就失败了。
部分源码,可以看到deallocate并没有调用free,只是将内存块重新接回my_free_list的头部
所以这里也是一个需要改进的问题,一个用户程序在不断申请堆内存,调用deallocate却没有归还内存
std::alloc总结:
上面的Foo(1)是栈上的一个临时对象,通过push_back()存储到list容器中,容器通过alloc分配内存(以上可知调用new,通过malloc在堆上分配了内存),用于保存该元素。
而下面是直接通过new在堆上建立一个Foo(2)对象,调用malloc,所以它是带cookie的,但是存储到list中,管理内存的free_lists上以后,就没有cookie了
malloc
上面已经给出了malloc申请到的内存情况
2.1 智能指针和动态数组
unique_ptr<int []> up(new int[10]);
可以使用下标来访问数组中的元素 up[i]
当up销毁它管理的指针时,会自动使用delete[]
shared_ptr不支持管理动态数组,它默认使用delete,所以除非自定义删除器:
shared_ptr<int> sp(new int[10], [](int *p) { delete[] p; });
shared_ptr未定义下标操作,但是可以使用get获取与它指向相同对象的内置指针来访问数组元素
六、STL源码剖析
string其实是typedef basic_string,其内部就有重载operator new,并且接受两个参数,第二个参数是一个extra(跟引用计数有关),使用的时候就是placement new