二十一、STL算法
STL提供了很多处理容器的函数模板,它们的设计是相同的,有以下特点:
1)用迭代器表示需要处理数据的区间。
2)返回迭代器放置处理数据的结果(如果有结果)。
3)接受一个函数对象参数(结构体模板),用于处理数据(如果需要)。
1、函数对象
很多STL算法都使用函数对象,也叫函数符(functor),包括函数名、函数指针和仿函数。
函数符的概念:
1)生成器(generator):不用参数就可以调用的函数符。
2)一元函数(unary function):用一个参数可以调用的函数符。
3)二元函数(binary function):用两个参数可以调用的函数符。
改进的概念:
1)一元谓词(predicate):返回bool值的一元函数。
2)二元谓词(binary predicate):返回bool值的二元函数。
2、预定义的函数对象
STL定义了多个基本的函数符,用于支持STL的算法函数。
包含头文件:#include <functional>
3、算法函数
STL将算法函数分成四组:
1)非修改式序列操作:对区间中的每个元素进行操作,这些操作不修改容器的内容。
2)修改式序列操作:对区间中的每个元素进行操作,这些操作可以容器的内容(可以修改值,也可以修改排列顺序)。
3)排序和相关操作:包括多个排序函数和其它各种函数,如集合操作。
4)通用数字运算:包括将区间的内容累积、计算两个容器的内部乘积、计算小计、计算相邻对象差的函数。通常,这些都是数组的操作特性,因此vector是最有可能使用这些操作的容器。
前三组在头文件#include <algorithm>中,第四组专用于数值数据,在#include <numeric>中。
all_of( ): 检查范围内的所有元素是否都满足给定的条件。如果所有元素都满足条件,则返回
true
;否则返回false
。any_of( ): 检查范围内是否至少有一个元素满足给定的条件。如果至少有一个元素满足条件,则返回
true
;否则返回false
。none_of( ): 检查范围内是否没有任何元素满足给定的条件。如果所有元素都不满足条件,则返回
true
;否则返回false
。for_each( ): 对范围内的每个元素执行指定的操作。这个函数通常用于遍历容器并对每个元素应用某个函数或操作。
find( ): 在范围内查找等于给定值的第一个元素。如果找到,返回指向该元素的迭代器;否则返回范围的结束迭代器。
find_if( ): 在范围内查找第一个满足给定条件的元素。如果找到,返回指向该元素的迭代器;否则返回范围的结束迭代器。
find_if_not( ): 在范围内查找第一个不满足给定条件的元素。如果找到,返回指向该元素的迭代器;否则返回范围的结束迭代器。
find_end( ): 在范围内查找另一个范围的最后一次出现的位置。返回指向找到的子范围第一个元素的迭代器,如果未找到则返回范围的结束迭代器。
find_first_of( ): 在范围内查找属于另一个范围中的任意一个元素的位置。返回指向找到的第一个元素的迭代器,如果未找到则返回范围的结束迭代器。
adjacent_find( ): 在范围内查找一对相邻的元素,这对元素相等或者满足给定的二元谓词。返回指向这对元素的第一个元素的迭代器,如果未找到则返回范围的结束迭代器。
count( ): 计算范围内等于给定值的元素个数。
count_if( ): 计算范围内满足给定条件的元素个数。
mismatch( ): 比较两个范围,返回指向第一个不匹配元素的迭代器对。如果两个范围在某个点上不相等(或者一个范围比另一个短),则返回这对迭代器。如果所有元素都匹配,则返回两个范围的结束迭代器。
equal( ): 检查两个范围是否相等。如果两个范围内的所有对应元素都相等,则返回
true
;否则返回false
。is_permutation( ): 检查两个范围是否包含相同数量的相同元素,但顺序可能不同。如果两个范围是彼此的排列,则返回
true
;否则返回false
。search( ): 在范围内查找另一个范围的第一次出现。返回指向找到的子范围第一个元素的迭代器,如果未找到则返回范围的结束迭代器。
search_n( ): 在范围内查找等于给定值的连续n个元素的第一次出现。返回指向找到的序列第一个元素的迭代器,如果未找到则返回范围的结束迭代器。
二十二、智能指针
1、智能指针unique_ptr
unique_ptr独享它指向的对象,也就是说,同时只有一个unique_ptr指向同一个对象,当这个unique_ptr被销毁时,指向的对象也随即被销毁。
包含头文件:#include <memory>
template <typename T, typename D = default_delete<T>>
class unique_ptr
{
public:
explicit unique_ptr(pointer p) noexcept; // 不可用于转换函数。
~unique_ptr() noexcept;
T& operator*() const; // 重载*操作符。
T* operator->() const noexcept; // 重载->操作符。
unique_ptr(const unique_ptr &) = delete; // 禁用拷贝构造函数。
unique_ptr& operator=(const unique_ptr &) = delete; // 禁用赋值函数。
unique_ptr(unique_ptr &&) noexcept; // 右值引用。
unique_ptr& operator=(unique_ptr &&) noexcept; // 右值引用。
// ...
private:
pointer ptr; // 内置的指针。
};
第一个模板参数T:指针指向的数据类型。
第二个模板参数D:指定删除器,缺省用delete释放资源。
测试类AA的定义:
class AA
{
public:
string m_name;
AA() { cout << m_name << "调用构造函数AA()。\n"; }
AA(const string & name) : m_name(name) { cout << "调用构造函数AA("<< m_name << ")。\n"; }
~AA() { cout << m_name << "调用了析构函数~AA(" << m_name << ")。\n"; }
};
基本用法
1)初始化
方法一:
unique_ptr<AA> p0(new AA("西施")); // 分配内存并初始化。
方法二:
unique_ptr<AA> p0 = make_unique<AA>("西施"); // C++14标准。
unique_ptr<int> pp1=make_unique<int>(); // 数据类型为int。
unique_ptr<AA> pp2 = make_unique<AA>(); // 数据类型为AA,默认构造函数。
unique_ptr<AA> pp3 = make_unique<AA>("西施"); // 数据类型为AA,一个参数的构造函数。
unique_ptr<AA> pp4 = make_unique<AA>("西施",8); // 数据类型为AA,两个参数的构造函数。
方法三(不推荐):
AA* p = new AA("西施");
unique_ptr<AA> p0(p); // 用已存在的地址初始化。
2)使用方法
-
智能指针重载了*和->操作符,可以像使用指针一样使用unique_ptr。
-
不支持普通的拷贝和赋值。
AA* p = new AA("西施");
unique_ptr<AA> pu2 = p; // 错误,不能把普通指针直接赋给智能指针。
unique_ptr<AA> pu3 = new AA("西施"); // 错误,不能把普通指针直接赋给智能指针。
unique_ptr<AA> pu2 = pu1; // 错误,不能用其它unique_ptr拷贝构造。
unique_ptr<AA> pu3;
pu3 = pu1; // 错误,不能用=对unique_ptr进行赋值。
-
不要用同一个裸指针初始化多个unique_ptr对象。
-
get()方法返回裸指针。
-
不要用unique_ptr管理不是new分配的内存。
3)用于函数的参数
-
传引用(不能传值,因为unique_ptr没有拷贝构造函数)。
-
裸指针。
4)不支持指针的运算(+、-、++、--)
更多技巧
1)将一个unique_ptr赋给另一个时,如果源unique_ptr是一个临时右值,编译器允许这样做;如果源unique_ptr将存在一段时间,编译器禁止这样做。一般用于函数的返回值。
unique_ptr<AA> p0;
p0 = unique_ptr<AA>(new AA ("西瓜"));
2)用nullptr给unique_ptr赋值将释放对象,空的unique_ptr==nullptr。
3)release()释放对原始指针的控制权,将unique_ptr置为空,返回裸指针。(可用于把unique_ptr传递给子函数,子函数将负责释放对象)
4)std::move()可以转移对原始指针的控制权。(可用于把unique_ptr传递给子函数,子函数形参也是unique_ptr)
5)reset()释放对象。
void reset(T * _ptr= (T *) nullptr);
pp.reset(); // 释放pp对象指向的资源对象。
pp.reset(nullptr); // 释放pp对象指向的资源对象
pp.reset(new AA("bbb")); // 释放pp指向的资源对象,同时指向新的对象。
6)swap()交换两个unique_ptr的控制权。
void swap(unique_ptr<T> &_Right);
7)unique_ptr也可象普通指针那样,当指向一个类继承体系的基类对象时,也具有多态性质,如同使用裸指针管理基类对象和派生类对象那样。
8)unique_ptr不是绝对安全,如果程序中调用exit()退出,全局的unique_ptr可以自动释放,但局部的unique_ptr无法释放。
9)unique_ptr提供了支持数组的具体化版本。
数组版本的unique_ptr,重载了操作符[],操作符[]返回的是引用,可以作为左值使用。
// unique_ptr<int[]> parr1(new int[3]); // 不指定初始值。
unique_ptr<int[]> parr1(new int[3]{ 33,22,11 }); // 指定初始值。
cout << "parr1[0]=" << parr1[0] << endl;
cout << "parr1[1]=" << parr1[1] << endl;
cout << "parr1[2]=" << parr1[2] << endl;
unique_ptr<AA[]> parr2(new AA[3]{string("西施"), string("冰冰"), string("幂幂")});
cout << "parr2[0].m_name=" << parr2[0].m_name << endl;
cout << "parr2[1].m_name=" << parr2[1].m_name << endl;
cout << "parr2[2].m_name=" << parr2[2].m_name << endl;
示例1:
#include <iostream>
#include <memory>
using namespace std;
class AA
{
public:
string m_name;
AA() { cout << m_name << "调用构造函数AA()。\n"; }
AA(const string & name) : m_name(name) { cout << "调用构造函数AA("<< m_name << ")。\n"; }
~AA() { cout << "调用了析构函数~AA(" << m_name << ")。\n"; }
};
// 函数func1()需要一个指针,但不对这个指针负责。
void func1(const AA* a) {
cout << a->m_name << endl;
}
// 函数func2()需要一个指针,并且会对这个指针负责。
void func2(AA* a) {
cout << a->m_name << endl;
delete a;
}
// 函数func3()需要一个unique_ptr,不会对这个unique_ptr负责。
void func3(const unique_ptr<AA> &a) {
cout << a->m_name << endl;
}
// 函数func4()需要一个unique_ptr,并且会对这个unique_ptr负责。
void func4(unique_ptr<AA> a) {
cout << a->m_name << endl;
}
int main()
{
unique_ptr<AA> pu(new AA("西施"));
cout << "开始调用函数。\n";
//func1(pu.get()); // 函数func1()需要一个指针,但不对这个指针负责。
//func2(pu.release()); // 函数func2()需要一个指针,并且会对这个指针负责。
//func3(pu); // 函数func3()需要一个unique_ptr,不会对这个unique_ptr负责。
func4(move(pu)); // 函数func4()需要一个unique_ptr,并且会对这个unique_ptr负责。
cout << "调用函数完成。\n";
if (pu == nullptr) cout << "pu是空指针。\n";
}
调用构造函数AA(西施)。
开始调用函数。
西施
调用了析构函数~AA(西施)。
调用函数完成。
pu是空指针。
示例2:
#include <iostream>
#include <memory>
using namespace std;
class AA
{
public:
string m_name;
AA() { cout << m_name << "调用构造函数AA()。\n"; }
AA(const string & name) : m_name(name) { cout << "调用构造函数AA("<< m_name << ")。\n"; }
~AA() { cout << "调用了析构函数~AA(" << m_name << ")。\n"; }
};
int main()
{
//AA* parr1 = new AA[2]; // 普通指针数组。
AA* parr1 = new AA[2]{ string("西施"), string("冰冰") };
//parr1[0].m_name = "西施1";
//cout << "parr1[0].m_name=" << parr1[0].m_name << endl;
//parr1[1].m_name = "西施2";
//cout << "parr1[1].m_name=" << parr1[1].m_name << endl;
//delete [] parr1;
unique_ptr<AA[]> parr2(new AA[2]); // unique_ptr数组。
//unique_ptr<AA[]> parr2(new AA[2]{ string("西施"), string("冰冰") });
parr2[0].m_name = "西施1";
cout << "parr2[0].m_name=" << parr2[0].m_name << endl;
parr2[1].m_name = "西施2";
cout << "parr2[1].m_name=" << parr2[1].m_name << endl;
}
调用构造函数AA()。
调用构造函数AA()。
parr2[0].m_name=西施1
parr2[1].m_name=西施2
调用了析构函数~AA(西施2)。
调用了析构函数~AA(西施1)。
2、智能指针shared_ptr
shared_ptr共享它指向的对象,多个shared_ptr可以指向(关联)相同的对象,在内部采用计数机制来实现。
当新的shared_ptr与对象关联时,引用计数增加1。
当shared_ptr超出作用域时,引用计数减1。当引用计数变为0时,则表示没有任何shared_ptr与对象关联,则释放该对象。
基本用法
shared_ptr的构造函数也是explicit,但是,没有删除拷贝构造函数和赋值函数。
1)初始化
方法一:
shared_ptr<AA> p0(new AA("西施")); // 分配内存并初始化。
方法二:
shared_ptr<AA> p0 = make_shared<AA>("西施"); // C++11标准,效率更高。
shared_ptr<int> pp1=make_shared<int>(); // 数据类型为int。
shared_ptr<AA> pp2 = make_shared<AA>(); // 数据类型为AA,默认构造函数。
shared_ptr<AA> pp3 = make_shared<AA>("西施"); // 数据类型为AA,一个参数的构造函数。
shared_ptr<AA> pp4 = make_shared<AA>("西施",8); // 数据类型为AA,两个参数的构造函数。
方法三:
AA* p = new AA("西施");
shared_ptr<AA> p0(p); // 用已存在的地址初始化。
方法四:
shared_ptr<AA> p0(new AA("西施"));
shared_ptr<AA> p1(p0); // 用已存在的shared_ptr初始化,计数加1。
shared_ptr<AA> p1=p0; // 用已存在的shared_ptr初始化,计数加1。
2)使用方法
-
智能指针重载了*和->操作符,可以像使用指针一样使用shared_ptr。
-
use_count()方法返回引用计数器的值。
-
unique()方法,如果use_count()为1,返回true,否则返回false。
-
shared_ptr支持赋值,左值的shared_ptr的计数器将减1,右值shared_ptr的计算器将加1。
-
get()方法返回裸指针。
-
不要用同一个裸指针初始化多个shared_ptr。
-
不要用shared_ptr管理不是new分配的内存。
3)用于函数的参数
与unique_ptr的原理相同。
4)不支持指针的运算(+、-、++、--)
更多细节
1)将一个unique_ptr赋给另一个时,如果源unique_ptr是一个临时右值,编译器允许这样做;如果源unique_ptr将存在一段时间,编译器禁止这样做。一般用于函数的返回值。
2)用nullptr给shared_ptr赋值将把计数减1,如果计数为0,将释放对象,空的shared_ptr==nullptr。
3)release()释放对原始指针的控制权,将unique_ptr置为空,返回裸指针。
4)std::move()可以转移对原始指针的控制权。还可以将unique_ptr转移成shared_ptr。
5)reset()改变与资源的关联关系。
pp.reset(); // 解除与资源的关系,资源的引用计数减1。
pp. reset(new AA("bbb")); // 解除与资源的关系,资源的引用计数减1。关联新资源。
6)swap()交换两个shared_ptr的控制权。
void swap(shared_ptr<T> &_Right);
7)shared_ptr也可象普通指针那样,当指向一个类继承体系的基类对象时,也具有多态性质,如同使用裸指针管理基类对象和派生类对象那样。
8)shared_ptr不是绝对安全,如果程序中调用exit()退出,全局的shared_ptr可以自动释放,但局部的shared_ptr无法释放。
9)shared_ptr提供了支持数组的具体化版本。
数组版本的shared_ptr,重载了操作符[],操作符[]返回的是引用,可以作为左值使用。
10)shared_ptr的线程安全性:
-
shared_ptr的引用计数本身是线程安全(引用计数是原子操作)。
-
多个线程同时读同一个shared_ptr对象是线程安全的。
-
如果是多个线程对同一个shared_ptr对象进行读和写,则需要加锁。
-
多线程读写shared_ptr所指向的同一个对象,不管是相同的shared_ptr对象,还是不同的shared_ptr对象,也需要加锁保护。
11)如果unique_ptr能解决问题,就不要用shared_ptr。unique_ptr的效率更高,占用的资源更少。
示例1:
#include <iostream>
#include <memory>
using namespace std;
class AA
{
public:
string m_name;
AA() { cout << m_name << "调用构造函数AA()。\n"; }
AA(const string & name) : m_name(name) { cout << "调用构造函数AA("<< m_name << ")。\n"; }
~AA() { cout << "调用了析构函数~AA(" << m_name << ")。\n"; }
};
int main()
{
shared_ptr<AA> pa0(new AA("西施a")); // 初始化资源西施a。
shared_ptr<AA> pa1 = pa0; // 用已存在的shared_ptr拷贝构造,计数加1。
shared_ptr<AA> pa2 = pa0; // 用已存在的shared_ptr拷贝构造,计数加1。
cout << "pa0.use_count()=" << pa0.use_count() << endl; // 值为3。
shared_ptr<AA> pb0(new AA("西施b")); // 初始化资源西施b。
shared_ptr<AA> pb1 = pb0; // 用已存在的shared_ptr拷贝构造,计数加1。
cout << "pb0.use_count()=" << pb0.use_count() << endl; // 值为2。
pb1 = pa1; // 资源西施a的引用加1,资源西施b的引用减1。
pb0 = pa1; // 资源西施a的引用加1,资源西施b的引用成了0,将被释放。
cout << "pa0.use_count()=" << pa0.use_count() << endl; // 值为5。
cout << "pb0.use_count()=" << pb0.use_count() << endl; // 值为5。
}
调用构造函数AA(西施a)。
pa0.use_count()=3
调用构造函数AA(西施b)。
pb0.use_count()=2
调用了析构函数~AA(西施b)。
pa0.use_count()=5
pb0.use_count()=5
调用了析构函数~AA(西施a)。
3、智能指针的删除器
在默认情况下,智能指针过期的时候,用delete原始指针; 释放它管理的资源。
程序员可以自定义删除器,改变智能指针释放资源的行为。
删除器可以是全局函数、仿函数和Lambda表达式,形参为原始指针。
示例:
#include <iostream>
#include <memory>
using namespace std;
class AA
{
public:
string m_name;
AA() { cout << m_name << "调用构造函数AA()。\n"; }
AA(const string & name) : m_name(name) { cout << "调用构造函数AA("<< m_name << ")。\n"; }
~AA() { cout << "调用了析构函数~AA(" << m_name << ")。\n"; }
};
void deletefunc(AA* a) { // 删除器,普通函数。
cout << "自定义删除器(全局函数)。\n";
delete a;
}
struct deleteclass // 删除器,仿函数。
{
void operator()(AA* a) {
cout << "自定义删除器(仿函数)。\n";
delete a;
}
};
auto deleterlamb = [](AA* a) { // 删除器,Lambda表达式。
cout << "自定义删除器(Lambda)。\n";
delete a;
};
int main()
{
shared_ptr<AA> pa1(new AA("西施a"), deletefunc);
//shared_ptr<AA> pa2(new AA("西施b"), deleteclass());
//shared_ptr<AA> pa3(new AA("西施c"), deleterlamb);
//unique_ptr<AA,decltype(deletefunc)*> pu1(new AA("西施1"), deletefunc);
// unique_ptr<AA, void (*)(AA*)> pu0(new AA("西施1"), deletefunc);
//unique_ptr<AA, deleteclass> pu2(new AA("西施2"), deleteclass());
//unique_ptr<AA, decltype(deleterlamb)> pu3(new AA("西施3"), deleterlamb);
}
调用构造函数AA(西施a)。
自定义删除器(全局函数)。
调用了析构函数~AA(西施a)。
4、智能指针weak_ptr
shared_ptr存在的问题
shared_ptr内部维护了一个共享的引用计数器,多个shared_ptr可以指向同一个资源。
如果出现了循环引用的情况,引用计数永远无法归0,资源不会被释放。
示例:
#include <iostream>
#include <memory>
using namespace std;
class BB;
class AA
{
public:
string m_name;
AA() { cout << m_name << "调用构造函数AA()。\n"; }
AA(const string & name) : m_name(name) { cout << "调用构造函数AA("<< m_name << ")。\n"; }
~AA() { cout << "调用了析构函数~AA(" << m_name << ")。\n"; }
shared_ptr<BB> m_p;
};
class BB
{
public:
string m_name;
BB() { cout << m_name << "调用构造函数BB()。\n"; }
BB(const string& name) : m_name(name) { cout << "调用构造函数BB(" << m_name << ")。\n"; }
~BB() { cout << "调用了析构函数~BB(" << m_name << ")。\n"; }
shared_ptr<AA> m_p;
};
int main()
{
shared_ptr<AA> pa = make_shared<AA>("西施a");
shared_ptr<BB> pb = make_shared<BB>("西施b");
pa-> m_p = pb;
pb->m_p = pa;
}
调用构造函数AA(西施a)。
调用构造函数BB(西施b)。
weak_ptr是什么
weak_ptr 是为了配合shared_ptr而引入的,它指向一个由shared_ptr管理的资源但不影响资源的生命周期。也就是说,将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。
不论是否有weak_ptr指向,如果最后一个指向资源的shared_ptr被销毁,资源就会被释放。
weak_ptr更像是shared_ptr的助手而不是智能指针。
示例:
#include <iostream>
#include <memory>
using namespace std;
class BB;
class AA
{
public:
string m_name;
AA() { cout << m_name << "调用构造函数AA()。\n"; }
AA(const string & name) : m_name(name) { cout << "调用构造函数AA("<< m_name << ")。\n"; }
~AA() { cout << "调用了析构函数~AA(" << m_name << ")。\n"; }
weak_ptr<BB> m_p;
};
class BB
{
public:
string m_name;
BB() { cout << m_name << "调用构造函数BB()。\n"; }
BB(const string& name) : m_name(name) { cout << "调用构造函数BB(" << m_name << ")。\n"; }
~BB() { cout << "调用了析构函数~BB(" << m_name << ")。\n"; }
weak_ptr<AA> m_p;
};
int main()
{
shared_ptr<AA> pa = make_shared<AA>("西施a");
shared_ptr<BB> pb = make_shared<BB>("西施b");
cout << "pa.use_count()=" << pa.use_count() << endl;
cout << "pb.use_count()=" << pb.use_count() << endl;
pa->m_p = pb;
pb->m_p = pa;
cout << "pa.use_count()=" << pa.use_count() << endl;
cout << "pb.use_count()=" << pb.use_count() << endl;
}
调用构造函数AA(西施a)。
调用构造函数BB(西施b)。
pa.use_count()=1
pb.use_count()=1
pa.use_count()=1
pb.use_count()=1
调用了析构函数~BB(西施b)。
调用了析构函数~AA(西施a)。
如何使用weak_ptr
weak_ptr没有重载 ->和 *操作符,不能直接访问资源。
有以下成员函数:
1)operator=(); // 把shared_ptr或weak_ptr赋值给weak_ptr。
2)expired(); // 判断它指资源是否已过期(已经被销毁)。
3)lock(); // 返回shared_ptr,如果资源已过期,返回空的shared_ptr。
4)reset(); // 将当前weak_ptr指针置为空。
5)swap(); // 交换。
weak_ptr不控制对象的生命周期,但是,它知道对象是否还活着。
用lock()函数把它可以提升为shared_ptr,如果对象还活着,返回有效的shared_ptr,如果对象已经死了,提升会失败,返回一个空的shared_ptr。
提升的行为(lock())是线程安全的。
示例:
#include <iostream>
#include <memory>
using namespace std;
class BB;
class AA
{
public:
string m_name;
AA() { cout << m_name << "调用构造函数AA()。\n"; }
AA(const string& name) : m_name(name) { cout << "调用构造函数AA(" << m_name << ")。\n"; }
~AA() { cout << "调用了析构函数~AA(" << m_name << ")。\n"; }
weak_ptr<BB> m_p;
};
class BB
{
public:
string m_name;
BB() { cout << m_name << "调用构造函数BB()。\n"; }
BB(const string& name) : m_name(name) { cout << "调用构造函数BB(" << m_name << ")。\n"; }
~BB() { cout << "调用了析构函数~BB(" << m_name << ")。\n"; }
weak_ptr<AA> m_p;
};
int main()
{
shared_ptr<AA> pa = make_shared<AA>("西施a");
{
shared_ptr<BB> pb = make_shared<BB>("西施b");
pa->m_p = pb;
pb->m_p = pa;
shared_ptr<BB> pp = pa->m_p.lock(); // 把weak_ptr提升为shared_ptr。
if (pp == nullptr)
cout << "语句块内部:pa->m_p已过期。\n";
else
cout << "语句块内部:pp->m_name=" << pp->m_name << endl;
}
shared_ptr<BB> pp = pa->m_p.lock(); // 把weak_ptr提升为shared_ptr。
if (pp == nullptr)
cout << "语句块外部:pa->m_p已过期。\n";
else
cout << "语句块外部:pp->m_name=" << pp->m_name << endl;
}
调用构造函数AA(西施a)。
调用构造函数BB(西施b)。
语句块内部:pp->m_name=西施b
调用了析构函数~BB(西施b)。
语句块外部:pa->m_p已过期。
调用了析构函数~AA(西施a)。
二十三、C++文件操作
1、文件操作-写入文本文件
文本文件一般以行的形式组织数据。
包含头文件:#include <fstream>
类:ofstream(output file stream)
ofstream打开文件的模式(方式):
对于ofstream,不管用哪种模式打开文件,如果文件不存在,都会创建文件。
ios::out 缺省值:会截断文件内容。
ios::trunc 截断文件内容。(truncate)
ios::app 不截断文件内容,只在文件未尾追加文件。(append)
示例:
#include <iostream>
#include <fstream> // ofstream类需要包含的头文件。
using namespace std;
int main()
{
// 文件名一般用全路径,书写的方法如下:
// 1)"D:\data\txt\test.txt" // 错误。
// 2)R"(D:\data\txt\test.txt)" // 原始字面量,C++11标准。
// 3)"D:\\data\\txt\\test.txt" // 转义字符。
// 4)"D:/tata/txt/test.txt" // 把斜线反着写。
// 5)"/data/txt/test.txt" // Linux系统采用的方法。
string filename = R"(D:\data\txt\test.txt)";
//char filename[] = R"(D:\data\txt\test.txt)";
// 创建文件输出流对象,打开文件,如果文件不存在,则创建它。
// ios::out 缺省值:会截断文件内容。
// ios::trunc 截断文件内容。(truncate)
// ios::app 不截断文件内容,只在文件未尾追加文件。(append)
//ofstream fout(filename);
//ofstream fout(filename, ios::out);
//ofstream fout(filename, ios::trunc);
//ofstream fout(filename, ios::app);
ofstream fout;
fout.open(filename,ios::app);
// 判断打开文件是否成功。
// 失败的原因主要有:1)目录不存在;2)磁盘空间已满;3)没有权限,Linux平台下很常见。
if (fout.is_open() == false)
{
cout << "打开文件" << filename << "失败。\n"; return 0;
}
// 向文件中写入数据。
fout << "西施|19|极漂亮\n";
fout << "冰冰|22|漂亮\n";
fout << "幂幂|25|一般\n";
fout.close(); // 关闭文件,fout对象失效前会自动调用close()。
cout << "操作文件完成。\n";
}
2、文件操作-读取文本文件
包含头文件:#include <fstream>
类:ifstream
ifstream打开文件的模式(方式):
对于ifstream,如果文件不存在,则打开文件失败。
ios::in 缺省值。
示例:
#include <iostream>
#include <fstream> // ifstream类需要包含的头文件。
#include <string> // getline()函数需要包含的头文件。
using namespace std;
int main()
{
// 文件名一般用全路径,书写的方法如下:
// 1)"D:\data\txt\test.txt" // 错误。
// 2)R"(D:\data\txt\test.txt)" // 原始字面量,C++11标准。
// 3)"D:\\data\\txt\\test.txt" // 转义字符。
// 4)"D:/tata/txt/test.txt" // 把斜线反着写。
// 5)"/data/txt/test.txt" // Linux系统采用的方法。
string filename = R"(D:\data\txt\test.txt)";
//char filename[] = R"(D:\data\txt\test.txt)";
// 创建文件输入流对象,打开文件,如果文件不存在,则打开文件失败。。
// ios::in 缺省值。
//ifstream fin(filename);
//ifstream fin(filename, ios::in);
ifstream fin;
fin.open(filename,ios::in);
// 判断打开文件是否成功。
// 失败的原因主要有:1)目录不存在;2)文件不存在;3)没有权限,Linux平台下很常见。
if (fin.is_open() == false)
{
cout << "打开文件" << filename << "失败。\n"; return 0;
}
第一种方法。
//string buffer; // 用于存放从文件中读取的内容。
文本文件一般以行的方式组织数据。
//while (getline(fin, buffer))
//{
// cout << buffer << endl;
//}
第二种方法。
//char buffer[16]; // 存放从文件中读取的内容。
注意:如果采用ifstream.getline(),一定要保证缓冲区足够大。
//while (fin.getline(buffer, 15))
//{
// cout << buffer << endl;
//}
// 第三种方法。
string buffer;
while (fin >> buffer)
{
cout << buffer << endl;
}
fin.close(); // 关闭文件,fin对象失效前会自动调用close()。
cout << "操作文件完成。\n";
}
3、文件操作-写入二进制文件
二进制文件以数据块的形式组织数据,把内存中的数据直接写入文件。
包含头文件:#include <fstream>
类:ofstream(output file stream)
ofstream打开文件的模式(方式):
对于ofstream,不管用哪种模式打开文件,如果文件不存在,都会创建文件。
ios::out 缺省值:会截断文件内容。
ios::trunc 截断文件内容。(truncate)
ios::app 不截断文件内容,只在文件未尾追加文件。(append)
ios::binary 以二进制方式打开文件。
操作文本文件和二进制文件的一些细节:
1)在windows平台下,文本文件的换行标志是"\r\n"。
2)在linux平台下,文本文件的换行标志是"\n"。
3)在windows平台下,如果以文本方式打开文件,写入数据的时候,系统会将"\n"转换成"\r\n";读取数据的时候,系统会将"\r\n"转换成"\n"。 如果以二进制方式打开文件,写和读都不会进行转换。
4)在Linux平台下,以文本或二进制方式打开文件,系统不会做任何转换。
5)以文本方式读取文件的时候,遇到换行符停止,读入的内容中没有换行符;以二制方式读取文件的时候,遇到换行符不会停止,读入的内容中会包含换行符(换行符被视为数据)。
6)在实际开发中,从兼容和语义考虑,一般:
a)以文本模式打开文本文件,用行的方法操作它;
b)以二进制模式打开二进制文件,用数据块的方法操作它;
c)以二进制模式打开文本文件和二进制文件,用数据块的方法操作它,这种情况表示不关心数据的内容。(例如复制文件和传输文件)
d)不要以文本模式打开二进制文件,也不要用行的方法操作二进制文件,可能会破坏二进制数据文件的格式,也没有必要。(因为二进制文件中的某字节的取值可能是换行符,但它的意义并不是换行,可能是整数n个字节中的某个字节)
示例:
#include <iostream>
#include <fstream> // ofstream类需要包含的头文件。
using namespace std;
int main()
{
// 文件名一般用全路径,书写的方法如下:
// 1)"D:\data\bin\test.dat" // 错误。
// 2)R"(D:\data\bin\test.dat)" // 原始字面量,C++11标准。
// 3)"D:\\data\\bin\\test.dat" // 转义字符。
// 4)"D:/tata/bin/test.dat" // 把斜线反着写。
// 5)"/data/bin/test.dat" // Linux系统采用的方法。
string filename = R"(D:\data\bin\test.dat)";
//char filename[] = R"(D:\data\bin\test.dat)";
// 创建文件输出流对象,打开文件,如果文件不存在,则创建它。
// ios::out 缺省值:会截断文件内容。
// ios::trunc 截断文件内容。(truncate)
// ios::app 不截断文件内容,只在文件未尾追加文件。(append)
// ios::binary 以二进制方式打开文件。
//ofstream fout(filename, ios::binary);
//ofstream fout(filename, ios::out | ios::binary);
//ofstream fout(filename, ios::trunc | ios::binary);
//ofstream fout(filename, ios::app | ios::binary);
ofstream fout;
fout.open(filename, ios::app | ios::binary);
// 判断打开文件是否成功。
// 失败的原因主要有:1)目录不存在;2)磁盘空间已满;3)没有权限,Linux平台下很常见。
if (fout.is_open() == false)
{
cout << "打开文件" << filename << "失败。\n"; return 0;
}
// 向文件中写入数据。
struct st_girl { // 超女结构体。
char name[31]; // 姓名。
int no; // 编号。
char memo[301]; // 备注。
double weight; // 体重。
}girl;
girl = { "西施",3,"中国历史第一美女。" ,45.8 };
fout.write((const char *)& girl, sizeof(st_girl)); // 写入第一块数据。
girl = { "冰冰",8,"也是个大美女哦。",55.2};
fout.write((const char*)&girl, sizeof(st_girl)); // 写入第二块数据。
fout.close(); // 关闭文件,fout对象失效前会自动调用close()。
cout << "操作文件完成。\n";
}
4、文件操作-读取二进制文件
包含头文件:#include <fstream>
类:ifstream
ifstream打开文件的模式(方式):
对于ifstream,如果文件不存在,则打开文件失败。
ios::in 缺省值。
ios::binary 以二进制方式打开文件。
示例:
#include <iostream>
#include <fstream> // ifstream类需要包含的头文件。
using namespace std;
int main()
{
// 文件名一般用全路径,书写的方法如下:
// 1)"D:\data\bin\test.dat" // 错误。
// 2)R"(D:\data\bin\test.dat)" // 原始字面量,C++11标准。
// 3)"D:\\data\\bin\\test.dat" // 转义字符。
// 4)"D:/tata/bin/test.dat" // 把斜线反着写。
// 5)"/data/bin/test.dat" // Linux系统采用的方法。
string filename = R"(D:\data\bin\test.dat)";
//char filename[] = R"(D:\data\bin\test.dat)";
// 创建文件输入流对象,打开文件,如果文件不存在,则打开文件失败。。
// ios::in 缺省值。
// ios::binary 以二进制方式打开文件。
//ifstream fin(filename , ios::binary);
//ifstream fin(filename , ios::in | ios::binary);
ifstream fin;
fin.open(filename, ios::in | ios::binary);
// 判断打开文件是否成功。
// 失败的原因主要有:1)目录不存在;2)文件不存在;3)没有权限,Linux平台下很常见。
if (fin.is_open() == false)
{
cout << "打开文件" << filename << "失败。\n"; return 0;
}
// 二进制文件以数据块(数据类型)的形式组织数据。
struct st_girl { // 超女结构体。
char name[31]; // 姓名。
int no; // 编号。
char memo[301]; // 备注。
double weight; // 体重。
}girl;
while (fin.read((char*)&girl, sizeof(girl)))
{
cout << "name=" << girl.name << ",no=" << girl.no <<
",memo=" << girl.memo << ",weight=" << girl.weight << endl;
}
fin.close(); // 关闭文件,fin对象失效前会自动调用close()。
cout << "操作文件完成。\n";
}
5、文件操作-随机存取
1)fstream类
fstream类既可以读文本/二进制文件,也可以写文本/二进制文件。
fstream类的缺省模式是ios::in | ios::out,如果文件不存在,则创建文件;但是,不会清空文件原有的内容。
普遍的做法是:
1)如果只想写入数据,用ofstream;如果只想读取数据,用ifstream;如果想写和读数据,用fstream,这种情况不多见。不同的类体现不同的语义。
2)在Linux平台下,文件的写和读有严格的权限控制。(需要的权限越少越好)
2)文件的位置指针
对文件进行读/写操作时,文件的位置指针指向当前文件读/写的位置。
很多资料用“文件读指针的位置”和“文件写指针的位置”,容易误导人。不管用哪个类操作文件,文件的位置指针只有一个。
1)获取文件位置指针
ofstream类的成员函数是tellp();ifstream类的成员函数是tellg();fstream类两个都有,效果相同。
std::streampos tellp();
std::streampos tellg();
2)移动文件位置指针
ofstream类的函数是seekp();ifstream类的函数是seekg();fstream类两个都有,效果相同。
方法一:
std::istream & seekg(std::streampos _Pos);
fin.seekg(128); // 把文件指针移到第128字节。
fin.seekp(128); // 把文件指针移到第128字节。
fin.seekg(ios::beg) // 把文件指针移动文件的开始。
fin.seekp(ios::end) // 把文件指针移动文件的结尾。
方法二:
std::istream & seekg(std::streamoff _Off,std::ios::seekdir _Way);
在ios中定义的枚举类型:
enum seek_dir {beg, cur, end}; // beg-文件的起始位置;cur-文件的当前位置;end-文件的结尾位置。
fin.seekg(30, ios::beg); // 从文件开始的位置往后移30字节。
fin.seekg(-5, ios::cur); // 从当前位置往前移5字节。
fin.seekg( 8, ios::cur); // 从当前位置往后移8字节。
fin.seekg(-10, ios::end); // 从文件结尾的位置往前移10字节。
3)随机存取
随机存取是指直接移动文件的位置指针,在指定位置读取/写入数据。
示例:
#include <iostream>
#include <fstream> // fstream类需要包含的头文件。
using namespace std;
int main()
{
string filename = R"(D:\data\txt\test.txt)";
fstream fs;
fs.open(filename, ios::in | ios::out);
if (fs.is_open() == false)
{
cout << "打开文件" << filename << "失败。\n"; return 0;
}
fs.seekg(26); // 把文件位置指针移动到第26字节处。
fs << "我是一只傻傻的小菜鸟。\n";
/*string buffer;
while (fs >> buffer)
{
cout << buffer << endl;
}*/
fs.close(); // 关闭文件,fs对象失效前会自动调用close()。
cout << "操作文件完成。\n";
}
6、文件操作-缓冲区及流状态
1)文件缓冲区
文件缓冲区(缓存)是系统预留的内存空间,用于存放输入或输出的数据。
根据输出和输入流,分为输出缓冲区和输入缓冲区。
注意,在C++中,每打开一个文件,系统就会为它分配缓冲区。不同的流,缓冲区是独立的。
程序员不用关心输入缓冲区,只关心输出缓冲区就行了。
在缺省模式下,输出缓冲区中的数据满了才把数据写入磁盘,但是,这种模式不一定能满足业务的需求。
输出缓冲区的操作:
1)flush()成员函数
刷新缓冲区,把缓冲区中的内容写入磁盘文件。
2)endl
换行,然后刷新缓冲区。
3)unitbuf
fout << unitbuf;
设置fout输出流,在每次操作之后自动刷新缓冲区。
4)nounitbuf
fout << nounitbuf;
设置fout输出流,让fout回到缺省的缓冲方式。
2)流状态
流状态有三个:eofbit、badbit和failbit,取值:1-设置;或0-清除。
当三个流状成都为0时,表示一切顺利,good()成员函数返回true。
1)eofbit
当输入流操作到达文件未尾时,将设置eofbit。
eof()成员函数检查流是否设置了eofbit。
2)badbit
无法诊断的失败破坏流时,将设置badbit。(例如:对输入流进行写入;磁盘没有剩余空间)。
bad()成员函数检查流是否设置了badbit。
3)failbit
当输入流操作未能读取预期的字符时,将设置failbit(非致命错误,可挽回,一般是软件错误,例如:想读取一个整数,但内容是一个字符串;文件到了未尾)I/O失败也可能设置failbit。
fail()成员函数检查流是否设置了failbit。
4)clear()成员函数清理流状态。
5)setstate()成员函数重置流状态。
示例1:
#include <iostream>
#include <fstream> // ofstream类需要包含的头文件。
#include <unistd.h>
using namespace std;
int main()
{
ofstream fout("/oracle/tmp/bbb.txt"); // 打开文件。
fout << unitbuf;
for (int ii = 0; ii < 1000; ii++) // 循环1000次。
{
fout << "ii=" << ii << ",我是一只傻傻傻傻傻傻傻傻傻傻傻傻傻傻的鸟。\n";
//fout.flush(); // 刷新缓冲区。
usleep(100000); // 睡眠十分之一秒。
}
fout.close(); // 关闭文件。
}
示例2:
#include <iostream>
#include <fstream> // ifstream类需要包含的头文件。
#include <string> // getline()函数需要包含的头文件。
using namespace std;
int main()
{
ifstream fin(R"(D:\data\txt\test.txt)", ios::in);
if (fin.is_open() == false) {
cout << "打开文件" << R"(D:\data\txt\test.txt)" << "失败。\n"; return 0;
}
string buffer;
/*while (fin >> buffer) {
cout << buffer << endl;
}*/
while (true) {
fin >> buffer;
cout << "eof()=" << fin.eof() << ",good() = " << fin.good() << ", bad() = " << fin.bad() << ", fail() = " << fin.fail() << endl;
if (fin.eof() == true) break;
cout << buffer << endl;
}
fin.close(); // 关闭文件,fin对象失效前会自动调用close()。
}
二十四、C++异常、断言
1、C++异常
1)异常的语法
1)捕获全部的异常
try
{
// 可能抛出异常的代码。
// throw 异常对象;
}
catch (...)
{
// 不管什么异常,都在这里统一处理。
}
2)捕获指定的异常
try
{
// 可能抛出异常的代码。
// throw 异常对象;
}
catch (exception1 e)
{
// 发生exception1异常时的处理代码。
}
catch (exception2 e)
{
// 发生exception2异常时的处理代码。
}
在try语句块中,如果没有发生异常,执行完try语句块中的代码后,将继续执行try语句块之后的代码;如果发生了异常,用throw抛出异常对象,异常对象的类型决定了应该匹配到哪个catch语句块,如果没有匹配到catch语句块,程序将调用abort()函数中止。
如果try语句块中用throw抛出异常对象,并且匹配到了catch语句块,执行完catch语句块中的代码后,将继续执行catch语句块之后的代码,不会回到try语句块中。
如果程序中的异常没有被捕获,程序将异常中止。
示例:
#include <iostream>
using namespace std;
int main(int argc, char* argv[])
{
try
{
// 可能抛出异常的代码。
int ii = 0;
cout << "你是一只什么鸟?(1-傻傻鸟;2-小小鸟)";
cin >> ii;
if (ii==1) throw "不好,有人说我是一只傻傻鸟。"; // throw抛出const char *类型的异常。
if (ii==2) throw ii; // throw抛出int类型的异常。
if (ii==3) throw string("不好,有人说我是一只傻傻鸟。"); // throw抛出string类型的异常。
cout << "我不是一只傻傻鸟,哦耶。\n";
}
catch (int ii)
{
cout << "异常的类型是int=" << ii << endl;
}
catch (const char* ss)
{
cout << "异常的类型是const char *=" << ss << endl;
}
catch (string str)
{
cout << "异常的类型是string=" << str << endl;
}
//catch (...) // 不管什么异常,都在这里处理。
//{
// cout << "捕获到异常,具体没管是什么异常。\n";
//}
cout << "程序继续运行......\n"; // 执行完try ... catch ...后,将继续执行程序中其它的代码。
}
2)栈解旋
异常被抛出后,从进入try语句块开始,到异常被抛出之前,这期间在栈上构造的所有对象,都会被自动析构。析构的顺序与构造的顺序相反。这一过程称为栈的解旋。
也就是在执行throw前,在try执行期间构造的所有对象被自动析构后,才会进入catch匹配。
3)异常规范
C++98标准提出了异常规范,目的是为了让使用者知道函数可能会引发哪些异常。
void func1() throw(A, B, C); // 表示该函数可能会抛出A、B、C类型的异常。
void func2() throw(); // 表示该函数不会抛出异常。
void func3(); // 该函数不符合C++98的异常规范。
C++11标准弃用了异常规范,使用新增的关键字noexcept指出函数不会引发异常。
void func4() noexcept; // 该函数不会抛出异常。
在实际开发中,大部分程序员懒得在函数后面加noexcept,弃用异常已是共识,没必要多此一举。
关键字noexcept也可以用作运算符,判断表达试(操作数)是否可能引发异常;如果表达式可能引发异常,则返回false,否则返回true。
4)C++标准库异常
5)重点关注的异常
1)std::bad_alloc
如果内存不足,调用new会产生异常,导致程序中止;如果在new关键字后面加(std::nothrow)选项,则返回nullptr,不会产生异常。
示例:
#include <iostream>
using namespace std;
int main()
{
try {
// 如果分配内存失败,会抛出异常。
//double* ptr = new double[100000000000];
// 如果分配内存失败,将返回nullptr,会抛出异常。
double* ptr = new (std::nothrow) double[100000000000];
if (ptr == nullptr) cout << "ptr is null.\n";
}
catch (bad_alloc& e)
{
cout << "catch bad_alloc.\n";
}
}
2)std::bad_cast
dynamic_cast可以用于引用,但是,C++没有与空指针对应的引用值,如果转换请求不正确,会出现std::bad_cast异常。
3)std::bad_typeid
假设有表达式typeid(*ptr),当ptr是空指针时,如果ptr是多态的类型,将引发std::bad_typeid异常。
2、C++断言
1)断言
断言(assertion)是一种常用的编程手段,用于排除程序中不应该出现的逻辑错误。
使用断言需要包含头文件<cassert>或<assert.h>,头文件中提供了带参数的宏assert,用于程序在运行时进行断言。
语法:assert(表达式);
断言就是判断(表达式)的值,如果为0(false),程序将调用abort()函数中止,如果为非0(true),程序继续执行。
断言可以提高程序的可读性,帮助程序员定位违反了某些前提条件的错误。
注意:
-
断言用于处理程序中不应该发生的错误,而非逻辑上可能会发生的错误。
-
不要把需要执行的代码放到断言的表达式中。
-
断言的代码一般放在函数/成员函数的第一行,表达式多为函数的形参。
示例:
#include <iostream>
#include <cassert> // 断言assert宏需要包含的头文件。
using namespace std;
void copydata(void *ptr1,void *ptr2) // 把ptr2中的数据复制到ptr1中。
{
assert(ptr1&&ptr2); // 断言ptr1和ptr2都不会为空。
cout << "继续执行复制数据的代码......\n";
}
int main()
{
int ii=0,jj=0;
copydata(&ii, &jj); // 把ptr2中的数据复制到ptr1中。
}
2)C++11静态断言
assert宏是运行时断言,在程序运行的时候才能起作用。
C++11新增了静态断言static_assert,用于在编译时检查源代码。
使用静态断言不需要包含头文件。
语法:static_assert(常量表达式,提示信息);
注意:static_assert的第一个参数是常量表达式。而assert的表达式既可以是常量,也可以是变量。