C++ 学习笔记5

cpp_5

智能指针

智能指针就是类,类中有一个成员管理着原始指针。

unique_ptr

#include <memory>

实际是一个类,在析构函数中释放了内存。

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

类中禁用了拷贝构造和赋值运算符,避免将另一个指针赋值给该智能指针,导致重复释放指针(即释放野指针)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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释放资源。

初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
方法一:
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); // 用已存在的地址初始化。

使用方法

  • 智能指针重载了*和->操作符,可以像使用指针一样使用unique_ptr。

  • 不支持普通的拷贝和赋值。

1
2
3
4
5
6
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 分配的内存。

用于函数的参数

  • 传引用(不能传值,因为unique_ptr没有拷贝构造函数)。

  • 裸指针。

注意

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

简单使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
#include <memory>

using std::cout;
using std::endl;

class Student
{
public:
int m_age;
Student() = default;
Student(int age) : m_age(age){}
};

void func(const std::unique_ptr<Student>& p)
{
cout << p->m_age << endl;
}

int main()
{
std::unique_ptr<Student> ptr(new Student(20));

cout << ptr->m_age << endl;
cout << (*ptr).m_age << endl;
func(ptr);

return 0;
}

更多常用技巧

  1. 将一个unique_ptr赋给另一个时,如果源unique_ptr是一个临时右值,编译器允许这样做;如果源unique_ptr将存在一段时间,编译器禁止这样做。一般用于函数的返回值(函数中的局部变量返回值,如果返回的是全局变量则不行)。

    1
    2
    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()释放对象。

    1
    2
    3
    4
    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,重载了操作符[],操作符[]返回的是引用,可以作为左值使用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // std::unique_ptr<int[]> uptr(std::make_unique<int[]>(3)); 使用 make_unique。
    // 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-4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#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";
}

shared_ptr

shared_ptr共享它指向的对象,多个shared_ptr可以指向(关联)相同的对象,在内部采用计数机制来实现。

当新的shared_ptr与对象关联时,引用计数增加1。

当shared_ptr超出作用域时,引用计数减1。当引用计数变为0时,则表示没有任何shared_ptr与对象关联,则释放该对象。

基本用法

shared_ptr的构造函数也是explicit,但是,没有删除拷贝构造函数和赋值函数。

初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
方法一:
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。

使用方法:

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

用于函数的参数
与unique_ptr的原理相同。

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

更多常用技巧

  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()改变与资源的关联关系。

    1
    2
    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的效率更高,占用的资源更少。

删除器

默认情况下,智能指针过期的时候,用delete原始指针; 释放它管理的资源。

自定义删除器是为了在释放资源时做一些事情。

删除器可以是全局函数、仿函数和Lambda表达式,形参为原始指针。

代码示例:

shared_ptr 和 unique_ptr 指定删除器的方式不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#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);
}

weak_ptr

weak_ptr 是为了配合shared_ptr而引入的,它指向一个由shared_ptr管理的资源但不影响资源的生命周期。也就是说,将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。

不论是否有weak_ptr指向,如果最后一个指向资源的shared_ptr被销毁,资源就会被释放。

weak_ptr更像是shared_ptr的助手而不是智能指针。

shared_ptr内部维护了一个共享的引用计数器,多个shared_ptr可以指向同一个资源。

如果出现了循环引用的情况,引用计数永远无法归0,资源不会被释放。

循环引用情况如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#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):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#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
2
3
4
5
operator=();  // 把shared_ptr或weak_ptr赋值给weak_ptr。
expired(); // 判断它指资源是否已过期(已经被销毁)。
lock(); // 返回shared_ptr,如果资源已过期,返回空的shared_ptr。
reset(); // 将当前weak_ptr指针置为空。
swap(); // 交换。
多线程中的线程安全问题

weak_ptr不控制对象的生命周期,但是,它知道对象是否还活着。

用lock()函数把它可以提升为shared_ptr,如果对象还活着,返回有效的shared_ptr,如果对象已经死了,提升会失败,返回一个空的shared_ptr。

提升的行为(lock())是线程安全的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#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;
}

文件操作

#include <fstream> 包含 <ifstream> <ofstream>

输入输出流继承关系:

pigjBwQ.jpg

向文本文件写入

类:ofstream,输出文件流

打开模式:

  • ios::out 默认值。会截断文件内容。
  • ios::trunc 截断文件内容。
  • ios::app 不截断文件内容,而是在文件末尾追加内容。
  • ios::ate 打开文件时文件指针指向文件末尾,但是,可以在文件中的任何地方写数据。
  • ios::in 打开文件进行读操作,即读取文件中的数据。
  • ios::binary 打开文件为二进制文件,否则为文本文件。

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

使用方式:

  1. 创建 ofstream 对象,如 std::ofstream file;

  2. 使用构造函数打开文件或者使用成员函数 open 打开文件。

    1. 文件名可以使用 string 对象、R 原始字符串、C-style 字符串等
    2. 同时可以指定打开文件的模式

    构造函数:file(R("/data/file.txt"));

    open:file.open(R("/data/file.txt"), ios::out | ios::trunc)

  3. 可以通过 file.is_open() == false 来判断打开文件是否失败。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#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";
}

从文本文件读取

#include <fstream>

实际需要 <ifstream>

对于 ifstream,如果文件不存在,则打开文件失败。

打开文件模式:ios::in,默认值。

从文件读取的三种方法

不用循环的话,只会读取一行。

  1. 使用 string 类中的 getline(),第一个参数是文件流对象,第二个参数是保存到的 string 对象。按行读取,通过循环读取整个文件,读取到文件末尾则返回空。

    getline(filestream, str);

  2. 使用文件流对象的成员 getline(),第一个参数是保存的内存地址对象(如字符数组),第二个参数是读取多少个字节。

    注意:需要确定读取的字节数,不能少。多了还会浪费内存空间。

    filestream.getline(buffer, 128);

  3. 通过右移运算符 >> ,读取到文件末尾则返回空。

    filestream >> buffer;

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#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";
}

写入二进制文件

二进制文件就是将自己设计的一些结构体或类写入文件,只有程序员自己才知道是怎么设计的。如 png、mp3 等。

#include <fstream>

ifstream 类。

写入二进制文件,使用文件流对象的成员函数 write(const char*, std::streamsize n)

写入操作同普通文件一样。

**打开模式需要使用 ios::binary**。

write 函数原型:write(const char*, std::streamsize n);

write 用于向二进制文件写入数据,const char* 参数可以使用强制转换,或者 void*。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <string>
#include <fstream> // ifstream/ofstream

struct Student
{
char m_name[20];
int m_age;
};

int main()
{
Student s1{"stu1", 18};
Student s2{"stu2", 20};

std::ofstream bfile("bfile", std::ios::out | std::ios::trunc | std::ios::binary);

bfile.write((const char*)(&s1), sizeof(Student));
bfile.write((const char*)(&s2), sizeof(Student));

bfile.close();

return 0;
}

读取二进制文件。

需要知道二进制文件的结构,才能读取。

不使用 getline,使用文件流对象的成员函数 read(char_type __s, streamsize __n)。*

使用 ifstream 文件流对象,并且指定打开模式为二进制模式:ios::binary

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
#include <fstream>

using std::cout;
using std::endl;

struct Student
{
char m_name[20]; // 不能是 string,因为是动态数组,写入内存时会出错
int m_age;
};

int main()
{
std::ifstream brfile("bfile", std::ios::in | std::ios::binary);

if(brfile.is_open() == false)
{
cout << "打开文件失败" << endl;
return -1;
}

Student stemp;

while(brfile.read((char*)(&stemp), sizeof(Student)))
{
cout << "name:" << stemp.m_name << " age:" << stemp.m_age << endl;
}

brfile.close();

return 0;
}

操作文本文件和二进制文件的一些细节

  1. 在windows平台下,文本文件的换行标志是”\r\n”(也就是输入 “abc\n” 后,文件的大小为 5 个字节,多了 ‘\r’)。

  2. 在linux平台下,文本文件的换行标志是”\n”。

  3. 在windows平台下,如果以文本方式打开文件,写入数据的时候,系统会将”\n”转换成”\r\n”;读取数据的时候,系统会将”\r\n”转换成”\n”。 如果以二进制方式打开文件,写和读都不会进行转换(也就是说,可以通过显示 ascii 码的形式将 ‘\r’ 显示出来)。

  4. 在Linux平台下,以文本或二进制方式打开文件,系统不会做任何转换。

  5. 以文本方式读取文件的时候,遇到换行符停止,读入的内容中没有换行符;以二制方式读取文件的时候,遇到换行符不会停止,读入的内容中会包含换行符(换行符被视为数据)。

    使用 getlinefin << ifile 的形式读取不到换行符,就停止。

  6. 在实际开发中,从兼容和语义考虑,一般:a)以文本模式打开文本文件,用行的方法操作它;b)以二进制模式打开二进制文件,用数据块的方法操作它;c)以二进制模式打开文本文件和二进制文件,用数据块的方法操作它,这种情况表示不关心数据的内容。(例如复制文件和传输文件)d)不要以文本模式打开二进制文件,也不要用行的方法操作二进制文件,可能会破坏二进制数据文件的格式,也没有必要。(因为二进制文件中的某字节的取值可能是换行符,但它的意义并不是换行,可能是整数n个字节中的某个字节)

文件读取 fstream

最好是读文件用 ifstream,写文件用 ofstream,明确权限。

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

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

文件指针

不管用哪个类操作文件,文件的位置指针只有一个。

一般来说,输入流对象 ifstream 的移动文件指针操作函数是 seekg() ,获取文件指针位置的函数是 tellg(),输出流对象 ofstream 的文件指针操作函数是 seekp(),获取文件指针位置的函数是 tellp()。

移动文件指针 获取文件指针位置
输入流对象 ifstream seekg() tellg()
输出流对象 ofstream seekp() tellp()

移动文件指针操作

方法一:

1
2
3
4
5
std::istream & seekg(std::streampos _Pos); 
fin.seekg(128); // 把文件指针移到第128字节。
fin.seekp(128); // 把文件指针移到第128字节。
fin.seekg(ios::beg) // 把文件指针移动文件的开始。
fin.seekp(ios::end) // 把文件指针移动文件的结尾。

方法二:

1
std::istream & seekg(std::streamoff _Off,std::ios::seekdir _Way);

在ios中定义的枚举类型:

enum seek_dir {beg, cur, end}; // beg-文件的起始位置;cur-文件的当前位置;end-文件的结尾位置。

1
2
3
4
fin.seekg(30, ios::beg);  // 从文件开始的位置往后移30字节。
fin.seekg(-5, ios::cur); // 从当前位置往前移5字节。
fin.seekg( 8, ios::cur); // 从当前位置往后移8字节。
fin.seekg(-10, ios::end); // 从文件结尾的位置往前移10字节。

示例代码:

输出内容到文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>
#include <fstream>
#include <ctime>
#include <cstdlib>

using std::cout;
using std::endl;

// 输出10个随机数到文件
int main()
{
srand(time(NULL));
int num;
std::ofstream ofile("randfile", std::ios::out | std::ios::trunc);

if (ofile.is_open() == false)
{
std::cerr << "打开文件失败" << endl;
return -1;
}

for (int i = 0; i < 10; i++)
{
num = rand()%10; // 0-10
ofile << num << " pos:" << ofile.tellp() << "\n";
}

// 会覆盖原有位置内容
ofile.seekp(std::ios::beg);
ofile << "start";

ofile.close();

return 0;
}

从文件读取内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
#include <fstream>
#include <string>

using std::cout;
using std::endl;

int main()
{
std::ifstream ifile("randfile", std::ios::in);
std::string str;

if (ifile.is_open() == false)
{
std::cerr << "打开文件失败" << endl;
return -1;
}

while (getline(ifile, str))
{
cout << str << " pos:" << ifile.tellg() << "\n";
}

ifile.close();

return 0;
}

输入和输出缓冲区

输出缓冲区

缓冲区就是内存中。

程序向文件写入内容,首先写到输出缓冲区,如果缓冲区满了,那么缓冲区才会向文件中写入一批内容,是一批一批的写入的。

内容先写到缓冲区,再从缓冲区写入到文件的效率更高。但是,对于某些业务场景,内容不能及时写入到文件会出现问题。

刷新缓冲区的方式:

  1. endl 可以换行后刷新缓冲区。
  2. 输出流对象的成员函数 flush() 可以刷新缓冲区,如 fout.flush()
  3. 设置输出流对象,如 fout << unitbuf; ,可以使用 fout << nounitbuf; 取消立即刷新缓冲区内容。

流状态

eofbitbadbitfailbit,取值:1-设置;或0-清除。

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

  1. eofbit:当输入流操作到达文件未尾时,将设置为1,返回 true。
    eof()成员函数检查流是否设置了eofbit。

  2. badbit:无法诊断的失败破坏流时,将设置badbit。(例如:对输入流进行写入;磁盘没有剩余空间)。
    bad()成员函数检查流是否设置了badbit。

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#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()。
}

异常

如果程序中的异常没有被捕获(即抛出异常后没有处理异常的代码),程序将异常中止。

语法:

  1. 捕获全部的异常

    1
    2
    3
    4
    5
    6
    7
    8
    9
    try
    {
    // 可能抛出异常的代码。
    // throw 异常对象;
    }
    catch (...) // ... 代表任意异常类型
    {
    // 不管什么异常,都在这里统一处理。
    }
  2. 捕获指定的异常

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    try
    {
    // 可能抛出异常的代码。
    // throw 异常对象;
    }
    catch (exception1 e)
    {
    // 发生exception1异常时的处理代码。
    }
    catch (exception2 e)
    {
    // 发生exception2异常时的处理代码。
    }

避免异常

C++98标准提出了异常规范,目的是为了让使用者知道函数可能会引发哪些异常。

1
2
3
void func1() throw(A, B, C);   // 表示该函数可能会抛出A、B、C类型的异常。
void func2() throw(); // 表示该函数不会抛出异常。
void func3(); // 该函数不符合C++98的异常规范。

C++11标准弃用了异常规范,使用新增的关键字noexcept指出函数不会引发异常。

1
void func4() noexcept;     // 该函数不会抛出异常。

在实际开发中,大部分程序员懒得在函数后面加noexcept,弃用异常已是共识,没必要多此一举。

关键字noexcept也可以用作运算符,判断表达试(操作数)是否可能引发异常;如果表达式可能引发异常,则返回false,否则返回true。

c++ 标准库异常

pi2AnSg.png

  1. std::bad_alloc

    如果内存不足,调用new会产生异常,导致程序中止;如果在new关键字后面加(std::nothrow)选项,则返回nullptr,不会产生异常。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    #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异常。
    dynamic_cast 转换引用时抛出异常,转换指针时,如果失败会返回空指针,但是转换引用失败,则没有空引用,所以会抛出异常。可以用 typeid 判断,然后再转换。

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

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

    1. std::out_of_range
    2. std::length_error
    3. std::domain_error
    4. std::invalid_argument
  5. 其它异常

    1. std::range_error
    2. std::overflow_error
    3. std::underflow_error
    4. ios_base::failure
    5. std::bad_exception

断言

头文件:<cassert> <assert.h>

头文件中提供了带参数的宏assert,用于程序在运行时进行断言。

语法:assert(表达式);

断言就是判断(表达式)的值,如果为0(false),程序将调用abort()函数中止,如果为非0(true),程序继续执行。

断言可以提高程序的可读性,帮助程序员定位违反了某些前提条件的错误。

注意:

  • 断言用于处理程序中不应该发生的错误,而非逻辑上可能会发生的错误。

  • 不要把需要执行的代码放到断言的表达式中,如避免 assert(num++);

  • 断言的代码一般放在函数/成员函数的第一行,表达式多为函数的形参(判断传入的形参是否合法)。

c++ 11 静态断言

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

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

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

语法:static_assert(常量表达式,提示信息);,常量表达式为 0,则编译时出现错误。

1
2
const int i = 0;
static_assert(i, "static_assert() happend");

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