C++学习(5)STL算法、智能指针、文件操作

在学习STL算法之前先复习函数指针和回调函数(C++学习(2)基础知识点整理 60)、重载括号运算符(C++学习(3)面向对象OOP 118)。

目录

STL算法

foreach 

 findif

  findif(仿函数)

bsort(冒泡排序) 

一、函数对象

二、预定义的函数对象

三、算法函数

​编辑

四、学习要领(重要)

五、常用函数(遍历、排序、查找)

智能指针 

193、智能指针unique_ptr

一、基本用法

二、更多技巧

194、智能指针shared_ptr

一、基本用法

二、更多细节

195、智能指针的删除器

196、智能指针weak_ptr

一、shared_ptr存在的问题

二、weak_ptr是什么

三、如何使用weak_ptr

文件操作 

201、文件操作-写入文本文件

202、文件操作-读取文本文件

203、文件操作-写入二进制文件

204、文件操作-读取二进制文件

205、文件操作-随机存取

一、fstream类

二、文件的位置指针

三、随机存取

206、文件操作-缓冲区及流状态

一、文件缓冲区

二、流状态

C++异常、断言 

209、C++异常

一、异常的语法

二、栈解旋

三、异常规范

四、C++标准库异常

五、重点关注的异常

六、逻辑错误异常

七、其它异常

210、C++断言

一、断言

二、C++11静态断言


STL算法

STL提供了很多处理容器的函数模板,它们的设计是相同的,有以下特点:

        1)用迭代器表示需要处理数据的区间。(前两个参数)

        2)返回迭代器放置处理数据的结果(如果有结果)。

        3)接受一个函数对象参数(结构体模板),用于处理数据(如果需要)。(第三个参数)

foreach 

#include <iostream>
#include <vector>
#include <list>
#include <algorithm>
using  namespace std;

template<typename T>
void zsshow(const T& no)    // 张三的个性化表白函数。
{
	cout << "亲爱的" << no << "号:我是一只傻傻鸟。\n";
}

template<typename T>
class czs   // 张三的个性化表白仿函数。
{
public:
	void operator()(const T& no) {  
		cout << "亲爱的" << no << "号:我是一只傻傻鸟。\n";
	}
};

template<typename T1, typename T2>
void foreach(const T1 first, const T1 last, T2 pfun)
{
	for (auto it = first; it != last; it++)
		pfun(*it);        // 以超女编号为实参调用类的operator()函数。
}

int main()
{
	vector<int> bh = { 5,8,2,6,9,3,1,7 };   // 存放超女编号的容器。
	//list<string> bh = { "05","08","02","06","09","03","01","07" };   // 存放超女编号的容器。

	// 写一个函数,在函数中遍历容器,向超女表白,表白的方法可自定义。
	foreach(bh.begin(), bh.end(), zsshow<int>);  // 第三个参数是模板函数。
	
	foreach(bh.begin(), bh.end(), czs<int>());       // 第三个参数是仿函数。
}

 findif

#include <iostream>
#include <vector>
#include <list>
#include <algorithm>
using  namespace std;

template<typename T>
bool zsshow(const T& no,const T & in_no)    // 张三的个性化表白函数。
{
	if (no != in_no) return false;
	cout << "亲爱的" << no << "号:我是一只傻傻鸟。\n";
	return true;
}

template<typename T>
class czs   // 张三的个性化表白仿函数。
{
public:
	bool operator()(const T& no, const T& in_no) {
		if (no != in_no) return false;
		cout << "亲爱的" << no << "号:我是一只傻傻鸟。\n";
		return true;
	}
};

template<typename T1, typename T2, typename T3>
T1 findif(const T1 first, const T1 last, T2 pfun,T3 in_no)
{
	for (auto it = first; it != last; it++)
		if (pfun(*it, in_no) ==true)  return it;        // 用迭代器调用函数对象。

	return last;
}

int main()
{
	vector<int> bh = { 5,8,2,6,9,33,1,7 };   // 存放超女编号的容器。
	//list<string> bh = { "05","08","02","06","09","03","01","07" };   // 存放超女编号的容器。

	auto it1=findif(bh.begin(), bh.end(), zsshow<int>,2);  // 第三个参数是模板函数。
	if (it1 == bh.end()) cout << "查找失败。\n";
	else cout << "查找成功:" << *it1 << endl;

	auto it2=findif(bh.begin(), bh.end(), czs<int>(),33);       // 第三个参数是仿函数。
	if (it2 == bh.end()) cout << "查找失败。\n";
	else cout << "查找成功:" << *it2 << endl;
}

  findif(仿函数)

#include <iostream>
#include <vector>
#include <list>
#include <algorithm>  // STL算法函数头文件。
using  namespace std;

template<typename T>
bool zsshow(const T& no)    // 张三的个性化表白函数。
{
	if (no != 3) return false;
	cout << "亲爱的" << no << "号:我是一只傻傻鸟。\n";
	return true;
}

template<typename T>
class czs   // 张三的个性化表白仿函数。
{
public:
	T m_no;     // 存放张三喜欢的超女编号。
	czs(const T& no) : m_no(no) {}  // 构造函数的参数是张三喜欢的超女编号。
	bool operator()(const T& no) {
		if (no != m_no) return false;
		cout << "亲爱的" << no << "号:我是一只傻傻鸟。\n";
		return true;
	}
};

template<typename T1, typename T2>
T1 findif(const T1 first, const T1 last, T2 pfun)
{
	for (auto it = first; it != last; it++)
		if (pfun(*it) ==true)  return it;        // 用迭代器调用函数对象。

	return last;
}

int main()
{
	vector<int> bh = { 5,8,2,6,9,33,1,7 };   // 存放超女编号的容器。
	//list<string> bh = { "05","08","02","06","09","03","01","07" };   // 存放超女编号的容器。

	auto it1=find_if(bh.begin(), bh.end(), zsshow<int>);  // 第三个参数是模板函数。
	if (it1 == bh.end()) cout << "查找失败。\n";
	else cout << "查找成功:" << *it1 << endl;

	auto it2=find_if(bh.begin(), bh.end(), czs<int>(8));       // 第三个参数是仿函数。
	if (it2 == bh.end()) cout << "查找失败。\n";
	else cout << "查找成功:" << *it2 << endl;
}

bsort(冒泡排序) 

#include <iostream>
#include <vector>
#include <list>
#include <algorithm>   // STL算法函数。
#include <functional>   // STL仿函数。
using  namespace std;

template<typename T>
bool compasc(const T& left, const T& right)   {     // 普通函数,用于升序。
	return left < right;
}

template<typename T>
struct _less
{
	bool operator()(const T& left, const T& right) { // 仿函数,用于升序。
		return left < right;
	}
};

template<typename T>
bool compdesc(const T& left, const T& right) {     // 普通函数,用于降序。
	return left > right;
}

template<typename T>
class _greater
{
public:
	bool operator()(const T& left, const T& right) { // 仿函数,用于降序。
		return left > right;
	}
};

template<typename T, typename compare>
void bsort(const T first, const T last, compare comp)  // 冒泡排序。
{
	while(true)
	{
		bool bswap = false;        // 本轮遍历已交换过元素的标识,true-交换过,false-未交换过。
		for (auto it = first; ; )
		{
			auto left = it;                  // 左边的元素。
			it++;
			auto right = it;               // 右边的元素。
			if (right == last) break;  // 表示it1已经是最后一个元素了。

			//if (*left > *right)             // 如果左边的元素比右边大,交换它们的值。
			//if (*left < *right)             // 如果左边的元素比右边小,交换它们的值。
			// 排序规则:如果comp()返回true,left排在前面(升序),否则right排在前面(降序)。
			if (comp(*left, *right) == true)  continue;

			// 交换两个元素的值。
			auto tmp = *right;
			*right = *left;
			*left = tmp;
			bswap = true;        // 一轮遍历已交换过元素的标识。
		}

		if (bswap == false) break;  // 如果在for循环中不曾交换过元素,说明全部的元素已有序。
	}
}

int main()
{
	vector<int> bh = { 5,8,2,6,9,33,1,7 };   // 存放超女编号的容器。
	//list<string> bh = { "05","08","02","06","09","03","01","07" };   // 存放超女编号的容器。

	//bsort(bh.begin(), bh.end(),compasc<int>);        // 普通函数(升序)。
	//bsort(bh.begin(), bh.end(), compdesc<int>);     // 普通函数(降序)。

	//bsort(bh.begin(), bh.end(),_less<int>());             // 仿函数(升序)。
	//bsort(bh.begin(), bh.end(), _greater<int>());      // 仿函数(降序)。

	//bsort(bh.begin(), bh.end(), less<int>());             // STL提供的仿函数(升序)。
	//bsort(bh.begin(), bh.end(), greater<int>());       // STL提供的仿函数(降序)。

	//sort(bh.begin(), bh.end(),_less<int>());             // 仿函数(升序)。
	sort(bh.begin(), bh.end(), _greater<int>());      // 仿函数(降序)。

	for (auto val : bh)
		cout << val << " ";
	cout << endl;
}

一、函数对象

很多STL算法都使用函数对象,也叫函数符(functor),包括函数名、函数指针和仿函数(C++)

函数符的概念:

1)生成器(generator):不用参数就可以调用的函数符。

2)一元函数(unary function):用一个参数可以调用的函数符。

3)二元函数(binary function):用两个参数可以调用的函数符。

改进的概念:

1)一元谓词(predicate):返回bool值的一元函数。

2)二元谓词(binary predicate):返回bool值的二元函数。

二、预定义的函数对象

STL定义了多个基本的函数符,用于支持STL的算法函数。

如果使用预定义的函数对象需要包含头文件:#include <functional>

下面的这些函数都是通过仿函数(结构体)实现的

三、算法函数

STL将算法函数分成四组:

1)非修改式序列操作:对区间中的每个元素进行操作,这些操作不修改容器的内容。

2)修改式序列操作:对区间中的每个元素进行操作,这些操作可以容器的内容(可以修改值,也可以修改排列顺序)。

3)排序和相关操作:包括多个排序函数和其它各种函数,如集合操作。

4)通用数字运算:包括将区间的内容累积、计算两个容器的内部乘积、计算小计、计算相邻对象差的函数。通常,这些都是数组的操作特性,因此vector是最有可能使用这些操作的容器。

前三组在头文件#include <algorithm>中,第四组专用于数值数据,在#include <numeric>中。

详见《C++ Primer plus》,第六版,从886页开始。

四、学习要领(重要)

1)如果容器有成员函数,则使用成员函数,如果没有才考虑用STL的算法函数。

2)把全部的STL算法函数过一遍,知道大概有些什么东西。不要浪费太多时间

3)如果打算采用某算法函数,一定要搞清楚它的原理,关注它的效率。

4)不要太看重这些算法函数,自己写一个也就那么回事。

5)不是因为简单,而是因为不常用。

五、常用函数(遍历、排序、查找)

遍历

1)for_each()遍历

2)find()遍历

3)find_if()遍历

4)find_not_if()遍历

排序

5)sort()排序

STL的sort算法,数据量大时采用QuickSort(快速排序),分段归并排序。一旦分段后的数据量小于某个门槛(16),为避免QuickSort的递归调用带来过大的额外负荷,就改用InsertSort(插入排序)。如果递归层次过深,还会改用HeapSort(堆排序)。

适用于数组容器vector、string、deque(list容器有sort成员函数,红黑树和哈希表没有排序的说法)。

查找,一般查找都是使用红黑树或者哈希表

6)二分查找


智能指针 

193、智能指针unique_ptr

unique_ptr独享它指向的对象,也就是说,同时只有一个unique_ptr指向同一个对象,当这个unique_ptr被销毁时,指向的对象也随即被销毁

包含头文件:#include <memory>

explict 关键字是表明不能隐式转换 

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

不支持普通的拷贝和赋值。unique就说明了不能指向同一个对象

       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)用于函数的参数

  1. 传引用不能传值,因为unique_ptr没有拷贝构造函数)。
  2. 裸指针。

4不支持指针的运算(+、-、++、--)

二、更多技巧

1)将一个unique_ptr赋给另一个时,如果源unique_ptr是一个临时右值,编译器允许这样做;如果源unique_ptr将存在一段时间,编译器禁止这样做。一般用于函数的返回值。

unique_ptr<AA> p0;

p0 = unique_ptr<AA>(new AA ("西瓜"));

#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"; }
};

unique_ptr<AA> func()
{
    unique_ptr<AA>pp(new AA("西施3"));      //在函数中创建对象pp,之后返回pp
    return pp;
}

int main()
{
    unique_ptr<AA>pu1(new AA("西施1"));   //pu1初始化

    unique_ptr<AA>pu2;                    //pu2没有初始化
    //pu2 = pu1;              //错误,pu1是局部变量,代码执行后仍然存在
    pu2 = unique_ptr<AA>(new AA("西施2"));    //使用匿名对象对pu2赋值,匿名对象赋值后将消失

    cout << "调用func之前" << endl;
    pu2 = func();           //使用函数返回值给pu2赋值
    cout << "调用func之前" << endl;
    
}

用匿名对象给pu赋值结果 

全部代码运行结果 

2)用nullptr给unique_ptr赋值将释放对象,空的unique_ptr==nullptr。

#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"; }
};

unique_ptr<AA> func()
{
    unique_ptr<AA>pp(new AA("西施3"));
    return pp;
}

int main()
{
	unique_ptr<AA>pu(new AA("西施"));
    cout<<"赋值前"<<endl;
    if(pu!=nullptr)cout<<"pu不为空"<<endl;
    pu=nullptr;
    cout<<"赋值后"<<endl;
    if(pu==nullptr)cout<<"pu为空"<<endl;
}

3)release()释放对原始指针的控制权,将unique_ptr置为空,返回裸指针。(可用于把unique_ptr传递给子函数,子函数将负责释放对象)

4)std::move()可以转移对原始指针的控制权。(可用于把unique_ptr传递给子函数,子函数形参也是unique_ptr)

#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";
}

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也可象普通指针那样,当指向一个类继承体系的基类对象时,也具有多态性质,如同使用裸指针管理基类对象和派生类对象那样。

#include <iostream>         // 包含头文件。
#include<memory>
using namespace std;        // 指定缺省的命名空间。

class Hero                        // 英雄基类
{
public:
	int viability;      // 生存能力。
	int attack;         // 攻击伤害。
	virtual void skill1() { cout << "英雄释放了一技能。\n"; }
	virtual void skill2() { cout << "英雄释放了二技能。\n"; }
	virtual void uskill() { cout << "英雄释放了大绝招。\n"; }
};

class XS :public Hero       // 西施派生类
{
public:
	void skill1() { cout << "西施释放了一技能。\n"; }
	void skill2() { cout << "西施释放了二技能。\n"; }
	void uskill() { cout << "西施释放了大招。\n"; }
};

class HX :public Hero       // 韩信派生类
{
public:
	void skill1() { cout << "韩信释放了一技能。\n"; }
	void skill2() { cout << "韩信释放了二技能。\n"; }
	void uskill() { cout << "韩信释放了大招。\n"; }
};

class LB :public Hero       // 李白派生类
{
public:
	void skill1() { cout << "李白释放了一技能。\n"; }
	void skill2() { cout << "李白释放了二技能。\n"; }
	void uskill() { cout << "李白释放了大招。\n"; }
};

int main()
{
	// 根据用户选择的英雄,施展一技能、二技能和大招。
	int id = 0;     // 英雄的id。
	cout << "请输入英雄(1-西施;2-韩信;3-李白。):";
	cin >> id;

	// 创建基类指针,让它指向派生类对象,用基类指针调用派生类的成员函数。
	//Hero* ptr = nullptr;
	unique_ptr<Hero>ptr;
	if (id == 1) {             // 1-西施
		//ptr = new XS;
		ptr = unique_ptr<Hero>(new XS);
	}
	else if (id == 2) {      // 2-韩信
		//ptr = new HX;
		ptr = unique_ptr<Hero>(new HX);
	}
	else if (id == 3) {      // 3-李白
		//ptr = new LB;
		ptr = unique_ptr<Hero>(new LB);
	}

	if (ptr != nullptr) {
		ptr->skill1();
		ptr->skill2();
		ptr->uskill();
		//delete ptr;
	}
}

8)unique_ptr不是绝对安全,如果程序中调用exit()退出,全局的unique_ptr可以自动释放,但局部的unique_ptr无法释放。

#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"; }
};

unique_ptr<AA> pu1(new AA ("西施全局"));

int main()
{
    unique_ptr<AA> pu2(new AA("西施局部"));
    return 0;
}

 当将最后的return 0; 改为exit(0); 局部的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;

#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;
}

194、智能指针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)使用方法

  1. 智能指针重载了*和->操作符,可以像使用指针一样使用shared_ptr
  2. use_count()方法返回引用计数器的值。
  3. unique()方法,如果use_count()为1,返回true,否则返回false
  4. shared_ptr支持赋值,左值的shared_ptr的计数器将减1,右值shared_ptr的计算器将加1。
  5. get()方法返回裸指针。
  6. 不要用同一个裸指针初始化多个shared_ptr
  7. 不要用shared_ptr管理不是new分配的内存。

3)用于函数的参数

  1. 传引用不能传值,因为unique_ptr没有拷贝构造函数)。
  2. 裸指针。

4)不支持指针的运算(+、-、++、--)

二、更多细节

1)将一个unique_ptr赋给另一个时,如果源unique_ptr是一个临时右值,编译器允许这样做;如果源unique_ptr将存在一段时间,编译器禁止这样做。一般用于函数的返回值。

2)用nullptrshared_ptr赋值将把计数减1,如果计数为0,将释放对象,空的shared_ptr==nullptr

3release()释放对原始指针的控制权,将unique_ptr置为空,返回裸指针。

4std::move()可以转移对原始指针的控制权。还可以将unique_ptr转移成shared_ptr

5reset()改变与资源的关联关系。

pp.reset();        // 解除与资源的关系,资源的引用计数减1。

pp. reset(new AA("bbb"));  // 解除与资源的关系,资源的引用计数减1。关联新资源。

6swap()交换两个shared_ptr的控制权。

void swap(shared_ptr<T> &_Right);

7shared_ptr也可象普通指针那样,当指向一个类继承体系的基类对象时,也具有多态性质,如同使用裸指针管理基类对象和派生类对象那样。

8shared_ptr不是绝对安全,如果程序中调用exit()退出,全局的shared_ptr可以自动释放,但局部的shared_ptr无法释放。

9shared_ptr提供了支持数组的具体化版本。

数组版本的shared_ptr,重载了操作符[],操作符[]返回的是引用,可以作为左值使用。

10shared_ptr的线程安全性:

  1. shared_ptr的引用计数本身是线程安全(引用计数是原子操作)。
  2. 多个线程同时读同一个shared_ptr对象是线程安全的。
  3. 如果是多个线程对同一个shared_ptr对象进行读和写,则需要加锁。
  4. 多线程读写shared_ptr所指向的同一个对象,不管是相同的shared_ptr对象,还是不同的shared_ptr对象,也需要加锁保护。

11)如果unique_ptr能解决问题,就不要用shared_ptr。unique_ptr的效率更高,占用的资源更少。

195、智能指针的删除器

在默认情况下,智能指针过期的时候,用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);
}

196、智能指针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;
}

二、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;
}

三、如何使用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;
}

文件操作 

201、文件操作-写入文本文件

文本文件一般以行的形式组织数据。

包含头文件:#include <fstream>

类:ofstream(output file stream)

ofstream打开文件的模式(方式):

对于ofstream,不管用哪种模式打开文件,如果文件不存在,都会创建文件。

创建的文件跟代码在一个目录下,如果想指定目录,就传入具体路径

文件打开模式:

ios::out              缺省值:会截断文件内容。

ios::trunc           截断文件内容。(truncate)

ios::app              不截断文件内容,只在文件未尾追加文件。(append)

    //ofstream fout;    //创建文件输出流对象

    string filename = R"文件的具体路径"    //创建字符串接受文件路径

    //这三种的演示结果相同,效果也一样,可以在创建对象时直接传入文件路径打开
    //当文件不存在就创建文件,当文件存在就覆盖
	ofstream fout(filename);
	ofstream fout(filename, ios::out);
	ofstream fout(filename, ios::trunc);

    //在文件末尾追加
	ofstream fout(filename, ios::app);
#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";
}

必须检查文件是否打开成功,当文件成功打开后再对文件操作

202、文件操作-读取文本文件

包含头文件:#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);
	
    //第二种打开方法,先创建对象,再使用对象的open函数打开文件
	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";
}

203、文件操作-写入二进制文件

二进制文件以数据块的形式组织数据,把内存中的数据直接写入文件。

包含头文件:#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;

    //二进制文件的写入要使用输出流的write函数
	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";
}

使用TXT读取二进制文件里面是乱码 

数据写入文本文件和二进制文件的区别

1 写入数据时需要使用输出流ofstream的write函数 write(结构体指针,结构体大小)

2 打开文件的时候需要使用二进制模式

204、文件操作-读取二进制文件

包含头文件:#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";
}

二进制文件的读取

二进制文件以数据类型的方式组织文件,怎么写进去就怎么读出来,定义跟写入时相同的struct结构体,使用输入流ifstream的read函数,read(结构体指针,结构体大小),read函数一次只读取一块数据,想要读取全部数据使用while循环。

205、文件操作-随机存取

一、fstream类

fstream类既可以读文本/二进制文件,也可以写文本/二进制文件。

fstream类的缺省模式是ios::in | ios::out,如果文件不存在,则创建文件;但是,不会清空文件原有的内容。

普遍的做法是:

1)如果只想写入数据,用ofstream;如果只想读取数据,用ifstream;如果想写和读数据,用fstream,这种情况不多见。不同的类体现不同的语义。

2)在Linux平台下,文件的写和读有严格的权限控制。(需要的权限越少越好)

二、文件的位置指针

对文件进行读/写操作时,文件的位置指针指向当前文件读/写的位置。

很多资料用“文件读指针的位置”和“文件写指针的位置”,容易误导人。不管用哪个类操作文件,文件的位置指针只有一个。

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字节。

写入后会覆盖之前的数据,换行符也算在数据里面

三、随机存取

随机存取是指直接移动文件的位置指针,在指定位置读取/写入数据。

#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";
}

202、文件操作-打开文件的模式(方式)

一、写文件

如果文件不存在,各种模式都会创建文件。

ios::out              1)会截断文件;2)可以用seekp()移动文件指针。

ios:trunc            1)会截断文件;2)可以用seekp()移动文件指针。

ios::app              1)不会截断文件;2)文件指针始终在文件未尾,不能用seekp()移动文件指针。

ios::ate               打开文件时文件指针指向文件末尾,但是,可以在文件中的任何地方写数据。

ios::in                 打开文件进行读操作,即读取文件中的数据。

ios::binary         打开文件为二进制文件,否则为文本文件。

注:ate是at end的缩写,trunc是truncate(截断)的缩写,app是append(追加)的缩写。

206、文件操作-缓冲区及流状态

一、文件缓冲区

文件缓冲区(缓存)是系统预留的内存空间,用于存放输入或输出的数据。

根据输出和输入流,分为输出缓冲区和输入缓冲区。

注意,在C++中,每打开一个文件,系统就会为它分配缓冲区。不同的流,缓冲区是独立的

程序员不用关心输入缓冲区,只关心输出缓冲区就行了。

在缺省模式下,输出缓冲区中的数据满了才把数据写入磁盘,但是,这种模式不一定能满足业务的需求。

输出缓冲区的操作:

1)flush()成员函数

刷新缓冲区,把缓冲区中的内容写入磁盘文件。

2)endl

换行,然后刷新缓冲区。

3)unitbuf

fout << unitbuf;

设置fout输出流,在每次操作之后自动刷新缓冲区。

4)nounitbuf

fout << nounitbuf;

设置fout输出流,让fout回到缺省的缓冲方式。

二、流状态

流状态有三个:eofbit、badbit和failbit,取值:1-设置;或0-清除。

当三个流状成都为0时,表示一切顺利,good()成员函数返回true。

eof和good成员函数用的较多 

1)eofbit

当输入流操作到达文件未尾时,将设置eofbit。只对输入流有意义ifs.eof()

eof()成员函数检查流是否设置了eofbit。

2)badbit

无法诊断的失败破坏流时,将设置badbit。(例如:对输入流进行写入;磁盘没有剩余空间)。

bad()成员函数检查流是否设置了badbit。

3)failbit

当输入流操作未能读取预期的字符时,将设置failbit(非致命错误,可挽回,一般是软件错误,例如:想读取一个整数,但内容是一个字符串;文件到了未尾)I/O失败也可能设置failbit。

fail()成员函数检查流是否设置了failbit。

4)clear()成员函数清理流状态。

5)setstate()成员函数重置流状态。


C++异常、断言 

209、C++异常

一、异常的语法

现在学习异常的作用就是帮助理解之前编写的存在异常的代码 

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语句块中。

如果程序中的异常没有被捕获,程序将异常中止。

二、栈解旋

异常被抛出后,从进入try语句块开始,到异常被抛出之前,这期间在上构造的所有对象,都会被自动析构。析构的顺序与构造的顺序相反。这一过程称为栈的解旋。

也就是在执行throw前,在try执行期间构造的所有对象被自动析构后,才会进入catch匹配。

上构造的对象肿么办?

三、异常规范

简单了解即可,没必要深入研究

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。

四、C++标准库异常

五、重点关注的异常

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异常。

#include <iostream>         // 包含头文件。
using namespace std;        // 指定缺省的命名空间。

class Hero                        // 英雄基类
{
public:
	int viability;      // 生存能力。
	int attack;         // 攻击伤害。
	virtual void skill1() { cout << "英雄释放了一技能。\n"; }
	virtual void skill2() { cout << "英雄释放了二技能。\n"; }
	virtual void uskill() { cout << "英雄释放了大绝招。\n"; }
};

class XS :public Hero       // 西施派生类
{
public:
	void skill1() { cout << "西施释放了一技能。\n"; }
	void skill2() { cout << "西施释放了二技能。\n"; }
	void uskill() { cout << "西施释放了大招。\n"; }
	void show() { cout << "我是天下第一美女。\n"; }
};

class HX :public Hero       // 韩信派生类
{
public:
	void skill1() { cout << "韩信释放了一技能。\n"; }
	void skill2() { cout << "韩信释放了二技能。\n"; }
	void uskill() { cout << "韩信释放了大招。\n"; }
};

class LB :public Hero       // 李白派生类
{
public:
	void skill1() { cout << "李白释放了一技能。\n"; }
	void skill2() { cout << "李白释放了二技能。\n"; }
	void uskill() { cout << "李白释放了大招。\n"; }
};

int main()
{
	// 以下代码演示把基类引用转换为派生类引用时发生异常的情况。
	HX hx;
	Hero& rh = hx;
	try{
		XS & rxs= dynamic_cast<XS &>(rh);
	}
	catch (bad_cast) {
		cout << "出现了bad_cast异常。\n";
	}
}

3)std::bad_typeid

假设有表达式typeid(*ptr)ptr是空指针时,如果ptr是多态的类型,将引发std::bad_typeid异常。

六、逻辑错误异常

程序的逻辑错误产生的异常std::logic_error,通过合理的编程可以避免。

1)std::out_of_range

Defines a type of object to be thrown as exception. It reports errors that are consequence of attempt to access elements out of defined range.

It may be thrown by the member functions of std::bitset and std::basic_string, by std::stoi and std::stod families of functions, and by the bounds-checked member access functions (e.g. std::vector::at and std::map::at).

2)std::length_error

Defines a type of object to be thrown as exception. It reports errors that result from attempts to exceed implementation defined length limits for some object.

This exception is thrown by member functions of std::basic_string and std::vector::reserve.

3)std::domain_error

Defines a type of object to be thrown as exception. It may be used by the implementation to report domain errors, that is, situations where the inputs are outside of the domain on which an operation is defined.

The standard library components do not throw this exception (mathematical functions report domain errors as specified in math_errhandling). Third-party libraries, however, use this. For example, boost.math throws std::domain_error if boost::math::policies::throw_on_error is enabled (the default setting).

4)std::invalid_argument 

Defines a type of object to be thrown as exception. It reports errors that arise because an argument value has not been accepted.

This exception is thrown by std::bitset::bitset, and the std::stoi and std::stof families of functions.

数组越界

#include <iostream>
#include <vector>  
using namespace std; 

int main()
{
	try{
		vector<int> vv = { 1,2,3 };  // 容器vv中只有三个元素。
		vv.at(3) = 5;                        // 将引发out_of_range异常。
	}
	catch (out_of_range) {
		cout << "出现了out_of_range异常。\n";
	}
}

 转化失败

#include <stdexcept>
#include <iostream>
#include <string>
using namespace std;

int main()
{
	string str = "123";  // 不会抛出异常。   
	//string str = "";     // 将抛出Invalid_argument异常。
	//string str = "253647586946334221002101";  // 将抛出out_of_range异常。

	try {
		int x = stoi(str);        // 把string字符串转换为整数。
		cout << "x=" << x << endl;
	}
	catch (invalid_argument&) {
		cout << " invalid_argument. \n";
	}
	catch (out_of_range&) {
		cout << " out of range. \n";
	}
	catch (...) {
		cout << " something else…" << endl;
	}
}

七、其它异常

1)std::range_error

Defines a type of object to be thrown as exception. It can be used to report range errors (that is, situations where a result of a computation cannot be represented by the destination type).

The only standard library components that throw this exception are std::wstring_convert::from_bytes and std::wstring_convert::to_bytes.

The mathematical functions in the standard library components do not throw this exception (mathematical functions report range errors as specified in math_errhandling).

2)std::overflow_error     

Defines a type of object to be thrown as exception. It can be used to report arithmetic overflow errors (that is, situations where a result of a computation is too large for the destination type)

The only standard library components that throw this exception are std::bitset::to_ulong and std::bitset::to_ullong.

The mathematical functions of the standard library components do not throw this exception (mathematical functions report overflow errors as specified in math_errhandling). Third-party libraries, however, use this. For example, boost.math throws std::overflow_error if boost::math::policies::throw_on_error is enabled (the default setting).

3)std::underflow_error   

Defines a type of object to be thrown as exception. It may be used to report arithmetic underflow errors (that is, situations where the result of a computation is a subnormal floating-point value)

The standard library components do not throw this exception (mathematical functions report underflow errors as specified in math_errhandling). Third-party libraries, however, use this. For example, boost.math throws std::underflow_error if boost::math::policies::throw_on_error is enabled (the default setting

4)ios_base::failure

这个异常,程序员不主动找它就没事。

#include <iostream>
#include <fstream>

int main()
{
    using namespace std;
    fstream file;
    file.exceptions(ios::failbit);    // 设置如果出现ios::failbit,就引发异常。
    try
    {
        file.open("rm.txt", ios_base::in);  // 如果打开的文件不存在,就会引发异常。
    }
    catch (ios_base::failure f)
    {
        cout << caught an exception: " << f.what() << endl;
    }
}

210、C++断言

一、断言

断言(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中。
}

二、C++11静态断言

assert宏是运行时断言,在程序运行的时候才能起作用。

C++11新增了静态断言static_assert,用于在编译时检查源代码。

使用静态断言不需要包含头文件。

语法:static_assert(常量表达式,提示信息);

注意:static_assert的第一个参数是常量表达式。而assert的表达式既可以是常量,也可以是变量。

#include <iostream>
using  namespace std;

int main()
{
    int ii=0;
    static_assert(ii,"ii的值不合法");

}
  • 29
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值