【C++完结篇】:深入“次要”但关键的知识腹地

前言:终于到最后一篇了,这篇是一些C++的扩展内容,包括的东西看下面目录,看这篇之前,最好是把前面C++的内容都看了,快快快,话不多说,赶紧把最后一点点学完!

目录

一、类型转换

[1. C语言中的类型转换](#1. C语言中的类型转换)

[2. C++自定义与内置类型的转换](#2. C++自定义与内置类型的转换)

(1)内置类型转其他内置类型

(2)内置类型转自定义类型

(3)自定义类型转内置类型

(4)自定义类型转自定义类型

[3. C++显示强制类型转换](#3. C++显示强制类型转换)

(1)类型安全

(2)static_cast

(3)reinterpret_cast

(4)const_cast

(5)dynamic_cast

[4. RTTI](#4. RTTI)

typeid

二、IO流

[1. IO继承家族类](#1. IO继承家族类)

[2. IO流的状态标识](#2. IO流的状态标识)

[3. 标准IO流](#3. 标准IO流)

[4. 缓冲区](#4. 缓冲区)

[5. 文件IO流](#5. 文件IO流)

[6. string IO流](#6. string IO流)

三、反向迭代器的模拟实现

[1. 分析](#1. 分析)

[2. 实现](#2. 实现)

四、计算器的实现



一、类型转换

1. C语言中的类型转换

在C语言中,如果赋值运算符左右两侧类型不同,或者形参与实参类型不匹配,或者返回值类型与接收返回值类型不一致时等场景,就需要发生类型转化,C语言中总共有两种形式的类型转换:隐式类型转换和显式强制类型转换。

  • 隐式类型转化:编译器在编译阶段自动进行,能转就转,不能转就编译失败

  • 显式强制类型转化:需要用户自己去显示在变量前用括号指定要转换的类型

并不是任意类型之前都支持转换,两个类型支持转换需要有一定关联性,也就是说转换后要有一定的意义,两个毫无关联的类型是不支持转换的。

cpp 复制代码
void test_c()
{
	int i = 1;
	// 隐式类型转换 
	// 隐式类型转换主要发生在整形和整形之间,整形和浮点数之间,浮点数和浮点数之间 
	double d = i;
	printf("%d, %.2f\n", i, d);
	int* p = &i;
	// 显示的强制类型转换 
	// 强制类型转换主要发生在指针和整形之间,指针和指针之间 
	int address = (int)p;
	printf("%p, %d\n", p, address);
	// malloc返回值是void*,被强转成int* 

	int* ptr = (int*)malloc(8);
	// 编译报错:类型强制转换: 无法从"int *"转换为"double"
	// d = (double)p;
	// 指针是地址的编号,也是一种整数,所以可以和整形互相转换 
	// 但是指针和浮点数毫无关联,强转也是不支持的 
}

2. C++自定义与内置类型的转换

(1)内置类型转其他内置类型

cpp 复制代码
// 数值类型之间的隐式转换
int i = 10;
double d = i;        // int → double
float f = 3.14;
int j = f;           // float → int (截断)

// 指针转换
int* ptr = nullptr;
bool valid = ptr;    // 指针 → bool (nullptr为false)

// C风格转换
double d = 3.14;
int i = (int)d;      // 截断小数部分

// C++风格转换
int j = static_cast<int>(d);
char c = static_cast<char>(i);

// 处理符号问题
unsigned int u = 100;
int k = static_cast<int>(u);  // 无符号 → 有符号

// 提升(无信息丢失)
short s = 100;
int i = s;           // short → int

// 截断(可能丢失信息)
double d = 9.99;
int i = d;           // 丢失小数部分,i=9

// 符号扩展
char c = -10;
int i = c;           // 符号扩展,i=-10

(2)内置类型转自定义类型

  • 单参数类型,支持隐式类型转换;
  • 多参数类型,需要{ ... };
  • 前面两个实际上都依赖构造函数,其他的就是运算符重载有可能会支持,需要自己实现。

(3)自定义类型转内置类型

其实在讲智能指针的时候已经用过了,就一个方法:使用类型转换运算符,在类里写成重载函数

cpp 复制代码
class A
{
public:
	operator 内置类型 () { }
};
cpp 复制代码
class Meters {
private:
    double value;
public:
    Meters(double val) : value(val) {}
    
    // 转换运算符:Meters → double
    operator double() const {
        return value;
    }
    
    // 转换运算符:Meters → int
    operator int() const {
        return static_cast<int>(value);
    }
    
    // 转换运算符:Meters → string
    operator std::string() const {
        return std::to_string(value) + " meters";
    }
};

// 使用
Meters distance(5.5);
double d = distance;           // Meters → double
int i = distance;              // Meters → int
std::string s = distance;      // Meters → string

// 在表达式中自动转换
double total = distance + 2.5; // distance自动转为double

explicit转换运算符

cpp 复制代码
class SmartBool
{
private:
    int value;
public:
    SmartBool(int val) : value(val) {}

    // explicit operator bool 可以在条件语句中隐式使用
    explicit operator bool() const
    {
        return true;
    }

    explicit operator double() const = delete;  // 禁止转换
};

void test_3()
{
    SmartBool sb(666);

    //bool c = sb;                    // 错误:explicit阻止直接赋值
    bool b = (bool)sb;    // 正确:显式转换

    // 但在条件语句中,explicit operator bool 可以被隐式调用
    if (sb)                          // 正确:条件语句中允许
        std::cout << "sb is true" << std::endl;
}

(4)自定义类型转自定义类型

C++还支持自定义类型到自定义类型之间的转换,需要对应类型的构造函数支持即可,比如A类型对象想转成B类型,则支持一个形参为A类型的B构造函数即可支持。

cpp 复制代码
class A
{
public:
    A(int a)
        :_a1(a)
        , _a2(a)
    {}

    int _a1 = 1;
    int _a2 = 1;
};

class B
{
public:
    B(int b)
        :_b1(b)
    {}
    // 支持A类型对象转换为B类型对象 
    B(const A & aa)
        :_b1(aa._a1)
    {}
private:
    int _b1 = 1;
};

list 的具体应用

cpp 复制代码
DaYuan::list<int>::const_iterator cit = lt.begin();

如果还记得list的实现,这样写是不行的,因为begin是 iterator 类型的,而接收它是 const_iterator 类型的。我们必须在迭代器类里加转换函数:

cpp 复制代码
template<class T, class Ref, class Ptr>
struct ListIterator
{
    typedef ListNode<T> Node;
    typedef ListIterator<T, Ref, Ptr> Self;
    Node* _node;
    ListIterator(Node* node)
        :_node(node)
    {}

    typedef ListIterator<T, T&, T*> iterator;
    typedef ListIterator<T, const T&, const T*> const_iterator;

    // ListIterator实例化为iterator时,这个函数是拷贝构造 
    // ListIterator实例化为const_iterator时,这个函数支持iterator转换为const_iterator构造函数
    ListIterator(const ListIterator<T, T&, T*>& it)
        :_node(it._node)
    {}

    //......
};

3. C++显示强制类型转换

(1)类型安全

类型安全是指编程语言在编译和运行时提供保护机制,避免非法的类型转换和操作,导致出现一个内存访问错误等,从而减少程序运行时的错误。C/C++不是类型安全的语言,C/C++允许隐式类型转换,一些特殊情况下就会导致越界访问的内存错误,其次不合理的使用强制类型转换也会导致问题,比如一个int的指针强转成double访问就会出现越界。

cpp 复制代码
void insert(size_t pos, char ch)
{
    // 这里当pos==0时,就会引发由于隐式类型转换 
    // end跟pos比较时,提升为size_t导致判断结束逻辑出现问题 
    // 在数组中访问挪动数据就会出现越界,经典的类型安全问题 
    int end = 10;
    while (end >= pos)
    {
        // ...

        cout << end << endl;
        --end;
    }
}

int main()
{
    insert(5, 'x');
    //insert(0, 'x');

    // 这里会本质已经出现了越界访问,只是越界不一定能被检查出来 
    int x = 100;
    double* p1 = (double*)&x;
    cout << *p1 << endl;
    const int y = 0;
    int* p2 = (int*)&y;
    (*p2) = 1;
    // 这⾥打印的结果是1和0,也是因为我们类型转换去掉了const属性 
    // 但是编译器认为y是const的,不会被改变,所以会优化编译时放到 
    // 寄存器或者直接替换y为0导致的 
    cout << *p2 << endl;
    cout << y << endl;
    return 0;
}

于是,C++提出4个显示的命名强制类型转换 static_cast / reinterpret_cast / const_cast / dynamic_cast 就是为了让类型转换相对而言更安全:

(2)static_cast

static_cast 是C++中最常用的显式类型转换运算符,用于在编译时进行相对安全的类型转换。

cpp 复制代码
static_cast<new_type>(expression)

特点:

  • 编译时进行类型检查

  • 不能移除 const/volatile 限定符

  • 不能进行不相关指针类型之间的转换

  • 比C风格转换更安全,优先使用static_cast替代C风格转换

cpp 复制代码
class Base {
public:
    virtual ~Base() = default;
    void base_func() { std::cout << "Base function" << std::endl; }
};

class Derived : public Base {
public:
    Derived(int a= 0):b(a){}
    void derived_func() { std::cout << "Derived function" << std::endl; }
    int b;
};

void conversions()
{
    // 数值类型转换
    double d = 3.14159;
    int i = static_cast<int>(d);        // 3,截断小数部分
    float f = static_cast<float>(d);    // 3.14159f

    // 符号转换
    unsigned int u = 100;
    int s = static_cast<int>(u);        // 无符号 → 有符号

    // 大小转换
    long long big = 1000LL;
    short small = static_cast<short>(big);  // 可能丢失数据

    // 向上转换:Derived* → Base* (安全)
    Derived derived;
    Base* base_ptr = static_cast<Base*>(&derived);
    base_ptr->base_func();

    int x = 42;
    // 任意指针 → void*
    void* void_ptr = static_cast<void*>(&x);
    // void* → 具体指针类型
    int* int_ptr = static_cast<int*>(void_ptr);

    // 自定义类型 → 内置类型
    int num(100);
    Derived p = static_cast<Derived>(num);

    //Base* real_base = new Base();
    //Derived* bad_cast = static_cast<Derived*>(real_base);   编译通过,但运行时错误!
    // bad_cast->derived_func();  // 未定义行为!

}

(3)reinterpret_cast

reinterpret_cast 是C++中最底层的强制类型转换,它执行简单的二进制位模式重新解释,不进行任何类型检查。

cpp 复制代码
reinterpret_cast<new_type>(expression)

特点:

  • 最危险的类型转换运算符,应该尽量避免使用

  • 不进行任何运行时类型检查

  • 只是简单地重新解释底层比特模式

  • 几乎可以转换任何指针类型

cpp 复制代码
void conversions_2()
{
	int x = 984535;
	double* double_ptr = reinterpret_cast<double*>(&x);
	//double* double_ptr2 = static_cast<double*>(&x);
	//error C2440: "static_cast": 无法从"int *"转换为"double *"
    // 整数 → 指针
    int* ptr_restored = reinterpret_cast<int*>(x);

}

(4)const_cast

const_cast 是专门用于添加或移除 constvolatile 限定符的类型转换运算符。

cpp 复制代码
const_cast<new_type>(expression)

特点:

  • 唯一可以移除 const 限定符的C++转换

  • 不能改变实际的基础类型

  • 使用不当会导致未定义行为

cpp 复制代码
void const_test() {
    const int ci = 100;
    const std::string cs = "hello";

    // 错误:不能直接修改const对象
    // ci = 200;
    // cs.append(" world");

    // 使用const_cast移除const
    int mi = const_cast<int&>(ci);
    mi = 200;  

    std::string& ms = const_cast<std::string&>(cs);
    ms.append(" world");

    std::cout << "ci: " << ci << std::endl;  // 可能还是100(编译器优化)
    std::cout << "mi: " << mi << std::endl;  // 200

    int i = 100;
    // 添加const限定符(通常不需要,有隐式转换)
    const int& ci1 = const_cast<const int&>(i);
    // 更常见的写法(隐式转换)
    const int& ci2 = i;

    const A a;
    //a.b();    error C2662:"void A::b(void)":不能将"this"指针从"const A"转换为"A&"
    const_cast<A&>(a).b();
}

注意点:

我们在用const_cast时本意是修改原对象,所以转换必须是引用或者指针,但是因为编译器的优化以及系统存const变量位置等问题,原变量可能不会改,只修改新变量。

(5)dynamic_cast

dynamic_cast用于将基类的指针或者引用安全的转换成派生类的指针或者引用。如果基类的指针或者引用是指向派生类对象的,则转换回派生类指针或者引用时可以成功的,如果基类的指针指向基类对象,则转换失败返回nullptr,如果基类引用指向基类对象,则转换失败,抛出bad_cast异常。

cpp 复制代码
class A
{
public:
    virtual void f() {}
    int _a = 1;
};

class B : public A
{public:   int _b = 2;};

void fun1(A* pa)
{
    // 指向父类转换时有风险的,后续访问存在越界访问的风险 
    // 指向子类转换时安全 
    B* pb1 = (B*)pa;
    cout << "pb1:" << pb1 << endl;
    cout << pb1->_a << endl;
    cout << pb1->_b << endl;
    pb1->_a++;
    pb1->_b++;
    cout << pb1->_a << endl;
    cout << pb1->_b << endl;
}

void fun2(A* pa)
{
    // dynamic_cast会先检查是否能转换成功(指向子类对象),能成功则转换, 
    // (指向父类对象)转换失败则返回nullptr 
    B* pb1 = dynamic_cast<B*>(pa);
    if (pb1)
    {
        cout << "pb1:" << pb1 << endl;
        cout << pb1->_a << endl;
        cout << pb1->_b << endl;
        pb1->_a++;
        pb1->_b++;
        cout << pb1->_a << endl;
        cout << pb1->_b << endl;
    }
    else
    {
        cout << "转换失败" << endl;
    }
}

void fun3(A& pa)
{
    // 转换失败,则抛出bad_cast异常 
    try {
        B& pb1 = dynamic_cast<B&>(pa);
        cout << "转换成功" << endl;
    }
    catch (const exception& e)
    {
        cout << e.what() << endl;
    }
}

4. RTTI

RTTI的英文全称是"Runtime Type Identification",中文称为"运行时类型识别",它指的是程序在运行的时候才确定需要用到的对象是什么类型的 。用于在运行时(而不是编译时)获取有关对象的信息。RTTI主要由两个运算符实现,typeid和dynamic_cast;typeid主要用于返回表达式的类型,dynamic_cast前面已经讲过了,主要用于将基类的指针或者引用安全的转换成派生类的指针或者引用。下面讲另一个:

typeid

typeid(e)中e可以是任意表达式或类型的名字,typeid(e)的返回值是typeinfo或typeinfo派生类对象的引用

成员函数 功能描述
operator== 比较两个类型是否完全相同
operator!= 比较两个类型是否不同
before 比较类型在实现定义的内部排序中的先后顺序(也能传比较器,了解一下)
name 获取类型的名称字符串(只是字符串,他和before不同编译器结果可能不同)
hash_code(C++11) 获取类型的哈希值
cpp 复制代码
class A
{
public:   virtual void func() {}
protected:   int _a1 = 1;
};

class B : public A
{
protected:   int _b1 = 2;
};

int main()
{
    cout << typeid(string).name() << endl;
    try
    {

        B* pb = new B;    // pb 静态类型是 B*,动态类型是 B*
        A* pa = (A*)pb;   // pa 静态类型是 A*,动态类型是 B*
        cout << typeid(pa).name() << endl;
        cout << typeid(pb).name() << endl;
        if (typeid(*pb) == typeid(B))
            cout << "1.typeid(*pb) == typeid(B)" << endl;

        // 如果A和B不是继承关系,则会抛bad_typeid异常 
        if (typeid(*pa) == typeid(B))
            cout << "2.typeid(*pa) == typeid(B)" << endl;

        // 这里pa和pb是A*和B*,不是类类型对象,他会被当做编译时求值的静态类型运算
        // typeid(pa) 在编译时就能确定返回 A* 的类型信息
        // 不涉及运行时多态(忘了的复习一下多态)
        // 所以这里始终是不相等的 
        if (typeid(pa) == typeid(pb))
            cout << "3.typeid(pa) == typeid(B)" << endl;

    }
    catch (const std::exception& e)
    {
        cout << e.what() << endl;
    }
    return 0;
}

二、IO流

1. IO继承家族类

从一学习C++,我们就用上了"cin>>""cout<<",但具体是什么,我们没有讲过,其实它是IO流的其中一种!

2. IO流的状态标识

IO操作的过程中,可能会发生各种错误,IO流对象中给了四种状态标识错误。goodbit表示流没有错误 / eofbit表示流到达文件结束 / failbit表示IO操作失败了 / badbit表示流崩溃了出现了系统级错误。

  • 一个常见的IO流错误是cin>>i,i是一个int类型的对象,如果我们在控制台输入一个字符,cin对象的failbit状态位 就会被设置,cin就进入错误状态,一个流一旦发生错误,后续的IO操作都会失败,我们可以调用cin.clear()函数来恢复cin的状态为goodbit

  • badbit表示系统级错误,如不可恢复的读写错误,通常情况下,badbit一旦被设置了,流就无法再使用了。

  • failbit表示一个逻辑错误 ,如期望读取一个整形,但是却读取到一个字符,failbit被设置了,流是可以恢复的,恢复以后可以继续使用

  • 如果到达文件结束位置eofbit和failbit都会被置位。如果想再次读取当前文件,可以恢复一下流的状态,同时重置一个文件指针位置。

  • goodbit表示流未发生错误。

  • 也可以用setstate和rdstate两个函数来控制流状态,eofbit / failbit / badbit / goodbit 是ios_base基类中定义的静态成员变量,可以直接使用的,并且他们是可组合的位运算值,具体使用细节可以参考文档。

函数名称 作用描述 返回值说明
good() 检查流的状态是否良好,即所有错误标志位(eofbit, failbit, badbit)均未被设置 若流处于良好状态,返回 true;否则返回 false
eof() 检查是否设置了文件结束标志位(eofbit 若遇到文件结束(如输入流读取到结尾),返回 true;否则返回 false
fail() 检查是否设置了失败标志位(failbitbadbit 若发生可恢复错误(如格式错误)或严重错误,返回 true;否则返回 false
bad() 检查是否设置了严重错误标志位(badbit 若发生不可恢复的错误(如流损坏),返回 true;否则返回 false
operator! 逻辑非运算符重载,用于检查流是否出错 fail()true,则返回 true;否则返回 false
operator bool 布尔转换运算符重载,用于检查流是否有效 !fail()true,则返回 true;否则返回 false
rdstate() 获取当前流的所有错误状态标志位 返回一个 iostate 类型的值,包含当前设置的错误标志位组合
setstate() 设置指定的错误状态标志位,不影响其他已设置的标志位 无返回值(void),但会更新流的错误状态
clear() 设置(或清除)流的错误状态标志位 无返回值(void),可传入参数指定新的错误状态,默认参数为 goodbit(清除所有错误)
cpp 复制代码
int main()
{
	cout << cin.good() << endl;
	cout << cin.eof() << endl;
	cout << cin.bad() << endl;
	cout << cin.fail() << endl << endl;
	int i = 0;
	// 输入一个字符或多个字符,cin读取失败,流状态被标记为failbit 
	cin >> i;
	cout << i << endl;
	cout << cin.good() << endl;
	cout << cin.eof() << endl;
	cout << cin.bad() << endl;
	cout << cin.fail() << endl << endl;
	if (cin.fail())
	{
		// clear可以恢复流状态位goodbit 
		cin.clear();
		// 我们还要把缓冲区中的多个字符都读出来,读到数字停下来,否则再去cin>>i还是会失败
		char ch = cin.peek();   //peek()用于查看下一个字符而不从流中提取它。
		while (!(ch >= '0' && ch <= '9'))
		{
			ch = cin.get();
			cout << ch;
			ch = cin.peek();
		}
		cout << endl;
	}
	cout << cin.good() << endl;
	cout << cin.eof() << endl;
	cout << cin.bad() << endl;
	cout << cin.fail() << endl << endl;
	cin >> i;
	cout << i << endl;
	return 0;
}

3. 标准IO流

cpp 复制代码
#include<iostream>
#include<fstream>
#include<string>
using namespace std;
int main()
{
	// 持续的输入,要结束需要输入Ctrl+Z换行,Ctrl+Z用于告诉程序输入已经完成,类似于在文件末尾添加一个标记。
    // istream& operator>>(int i),>>运算符重载的返回值是istream对象,istream对象可以调用operator bool转换为bool值
    // 本质在底层是将cin的eofbit和failbit标志位设置了,cin调用operator bool函数语法逻辑上实现转换为bool值
	int i = 0, j = 1;
	while (cin >> i >> j)
	{
		cout << i << ":" << j << endl;
	}
	cout << cin.good() << endl;
	cout << cin.eof() << endl;
	cout << cin.bad() << endl;
	cout << cin.fail() << endl << endl;
	// 流一旦发生错误就不能再用了,清理重置一下再能使用 
	cin.clear();
	string s;
	while (cin >> s)
	{
		cout << s << endl;
	}
}
cpp 复制代码
#include <iostream>
#include <iomanip>

void printDate(int year, int month, int day) {
    std::cout << year << "."
              << std::setw(2) << std::setfill('0') << month << "."
              << std::setw(2) << std::setfill('0') << day << std::endl;
}

int main() {
    printDate(2025, 1, 1);    // 输出: 2025.01.01
    printDate(2025, 12, 25);  // 输出: 2025.12.25
    
    return 0;
}

4. 缓冲区

  • 任何输出流都管理着一个缓冲区,用来保存程序写的数据。如果我们执行 os<<"hello world"; 字符串可能立即输出,也可能被操作系统保存在缓冲区中,随后再输出。有了缓冲区机制,操作系统就可能将多个输出操作组合成为一个单一的系统级写操作。因为设备的写操作通常是很耗时的,允许操作系统将多个输出操作组合为单一的设备写操作可能带来很大的性能提升。

  • 会触发缓冲区刷新,将数据真正的写到输出设备或文件的原因有很多,如:

    <1> 程序正常结束;

    <2> 缓冲区满了;

    <3> 输出了操纵符endl或flush会立即刷新缓冲区;

    <4> 我们使用了操纵符unitbuf设置流的内部状态,来清空缓冲区,cerr就设置了unitbuf,所以cerr输出都是立即刷新的;

    <5> 一个输出流关联到另一个流时,当这个流读写时,输出流会立即刷新缓冲区。例如默认情况下cerr和cin都被关联到cout,所以读cin或写cerr时,都会导致cout缓冲区会被立即刷新。

cpp 复制代码
void func(ostream& os)
{
	os << "hello world";
	os << "hello bit";
	// "hello world"和"hello bit"是否输出不确定 
	system("pause");
	// 遇到endl,"hello world"和"hello bit"一定刷新缓冲区输出了 
	//os << endl;
	//os << flush;
	//int i;
	//cin >> i;
	os << "hello cat";
	// "hello cat"是否输出不确定 
	system("pause");
}

int main()
{
	ofstream ofs("test.txt");
	//func(cout);
	// unitbuf设置后,ofs每次写都直接刷新 
	// ofs << unitbuf;
	// cin绑定到ofs,cin进行读时,会刷新ofs的缓冲区 
	// cin.tie(&ofs);
	func(ofs);
	return 0;
}

tie()

tie() 是 C++ 标准库中用于绑定输入输出流的函数,主要作用是实现流之间的自动同步。 tie() 将一个输出流与输入流绑定,当对输入流进行操作时,会自动刷新绑定的输出流。cin 默认绑定到 cout.

endl

endl 是 C++ 中一个常用的输出流操纵器,插入换行符 (\n)、刷新输出缓冲区,即

cpp 复制代码
std::cout << '\n' << std::flush;
cpp 复制代码
int main()
{
	// 在io需求比较高的地方,如部分大量输入的竞赛题中,加上以下几行代码可以提高C++IO的效率 
	// 并且建议⽤'\n'替代endl,因为endl会刷新缓冲区 

	// 关闭标准 C++ 流是否与标准 C 流在每次输入/输出操作后同步。 
	ios_base::sync_with_stdio(false);

	// 关闭同步后,以下程序可能顺序为b a c 
	// std::cout << "a\n";
	// std::printf("b\n");
	// std::cout << "c\n";

	// 解绑cin和cout关联绑定的其他流 
	cin.tie(nullptr);
	cout.tie(nullptr);

	return 0;
}

5. 文件IO流

(1)文件流的特化

文件 I/O 专用的类定义在 <fstream> 中,它们是标准 IO 类的派生类:

  • ifstream (Input File Stream): 继承自 istream,专用于读取文件。

  • ofstream (Output File Stream): 继承自 ostream,专用于写入文件。

  • fstream (File Stream): 继承自 iostream,支持读写操作。

(2)打开文件

打开文件有构造时打开和调用 open() 成员函数两种方式:

cpp 复制代码
#include <fstream>
// 方式一:构造时打开
std::ofstream outFile("data.txt"); 

// 方式二:使用 open() 函数,并指定模式
outFile.open("data.txt", std::ios::out | std::ios::trunc);

(3)文件打开模式 (File Modes)

文件打开模式通过 std::ios 的枚举常量定义,它们是独立的二进制位值,可以组合使用(通过位运算 |):

模式常量 描述 关键行为 默认类
ios::in 输入模式 为读取而打开。ifstream 默认。 ifstream
ios::out 输出模式 为写入而打开。如果文件存在,内容会被覆盖 ofstream
ios::app 追加模式 所有输出操作都在文件末尾进行。
ios::trunc 截断模式 强制清空文件内容。 ofstream 默认包含
ios::binary 二进制模式 不进行字符转换(如换行符),用于非文本数据。
ios::ate 定位到结尾 打开后立即将指针移动到文件末尾,但允许移动指针。

注意点:

  • ios::outios::trunc 都会在打开时清空现有内容。ios::trunc 的存在意义是更明确地表达"截断"这一行为,提升代码可读性。

  • ios::appios::ate 都是在文件末尾开始操作。但 app 不能 移动文件指针,永远在末尾追加;ate 可以移动指针,只是初始位置在末尾。

  • ofstream 默认模式是 ios::out | ios::trunc

  • ifstream 默认模式是 ios::in

(4)关闭文件

文件流对象会在析构时自动关闭文件,但建议主动调用 close() 释放资源,尤其是当您需要立即检查操作是否成功时。

cpp 复制代码
outFile.close();

(5)二进制模式

对于非文本数据(如图片、结构体对象),应使用二进制模式 (ios::binary)。此时,数据按内存中的字节原样读写,不会进行任何字符集或换行符转换。

  • 写入: stream.write(const char* s, std::streamsize n)

  • 读取: stream.read(char* s, std::streamsize n)

(6) 自定义类型的读写

对于自定义结构体,可以直接使用 <<>> 重载来实现文本读写

cpp 复制代码
#include <iostream>
#include <fstream>
#include <cstring>

using namespace std;

// 定义一个适用于二进制 I/O 的结构体
// 关键:使用固定大小的 char 数组而非 std::string
struct ProductRecord {
    int id;
    char name[20]; 
    double price;
};

// 文件路径
const char* FILENAME = "products.bin";

// 函数:写入数据到文件
void writeBinaryFile() {
    // 使用 ios::out | ios::binary | ios::trunc 模式打开文件进行写入
    ofstream outFile(FILENAME, ios::out | ios::binary | ios::trunc);

    if (!outFile.is_open()) {
        cerr << "错误:无法打开文件进行写入!" << endl;
        return;
    }

    // 准备要写入的记录
    ProductRecord p1 = {101, "Laptop", 999.99};
    ProductRecord p2 = {102, "Mouse", 25.50};
    
    // 使用 strncpy 安全地复制字符串到 char 数组
    // strncpy(p1.name, "Laptop", 19); p1.name[19] = '\0'; // 规范做法
    // strncpy(p2.name, "Mouse", 19); p2.name[19] = '\0';

    cout << "开始写入二进制数据..." << endl;
    
    // 写入第一条记录
    // 必须将结构体地址转换为 const char* 类型
    outFile.write(reinterpret_cast<const char*>(&p1), sizeof(ProductRecord));

    // 写入第二条记录
    outFile.write(reinterpret_cast<const char*>(&p2), sizeof(ProductRecord));
    
    if (outFile.fail()) {
        cerr << "警告:写入操作失败!" << endl;
    }

    outFile.close();
    cout << "二进制数据写入完成。文件大小: " << 2 * sizeof(ProductRecord) << " 字节" << endl;
}

// 函数:从文件读取数据
void readBinaryFile() {
    // 使用 ios::in | ios::binary 模式打开文件进行读取
    ifstream inFile(FILENAME, ios::in | ios::binary);

    if (!inFile.is_open()) {
        cerr << "错误:无法打开文件进行读取!" << endl;
        return;
    }

    ProductRecord readRecord;
    int count = 0;
    
    cout << "\n开始读取二进制数据:" << endl;

    // 循环读取,直到文件结束或发生错误
    while (inFile.read(reinterpret_cast<char*>(&readRecord), sizeof(ProductRecord))) {
        count++;
        // 检查流状态是否良好,尽管 while 条件已经检查,但这是良好的习惯
        if (inFile.good() || inFile.eof()) { 
            cout << "记录 " << count << ": ID=" << readRecord.id 
                 << ", 名称=" << readRecord.name 
                 << ", 价格=" << readRecord.price << endl;
        }
    }
    
    // 检查循环结束的原因
    if (inFile.eof()) {
        cout << "读取完毕,到达文件末尾 (EOF)。" << endl;
    } else if (inFile.fail() && !inFile.eof()) {
        // 读取失败,但不是因为 EOF,可能是文件损坏或读写错误
        cerr << "警告:读取操作提前失败!" << endl;
    }

    inFile.close();
}

int main() {
    // 1. 写入数据
    writeBinaryFile();

    // 2. 读取数据
    readBinaryFile();

    return 0;
}

⚠️ 二进制读写陷阱: 在进行二进制读写时,结构体中不应该包含 std::string 或其他动态内存分配的对象。这是因为二进制读写只复制对象在内存中的原始字节,如果复制了 std::string,只会复制其内部指向堆内存的指针,而不会复制指针指向的实际字符串内容,导致读取时得到"野指针"或错误数据。因此,需要使用固定大小的 char 数组代替 std::string

6. string IO流

ostringstream是string的写入流,ostringstream是ostream的派生类;istringstream是string的读出流,istringstream是istream的派生类;stringstream是ostringstream和istringstream的派生类,既可以读也可以写。这里使用stringstream会很方便。

  • stringstream系列底层维护了一个string类型的对象用来保存结果,使用方法跟上面的文件流类似,只是数据读写交互的都是底层的string对象。

  • stringstream最常用的方式还是使用<<和>>重载,进行数据和string之间的IO转换。

  • string流使用str函数获取底层的string对象,或者写入底层的string对象,具体细节参考下面代码理解。

cpp 复制代码
#include<iostream>
#include<sstream>
#include<string>
using namespace std;

int main()
{
	int i = 123;
	Date d = { 2025, 4, 10 };
	ostringstream oss;
	oss << i << endl;
	oss << d << endl;
	string s = oss.str();
	cout << s << endl;
	//stringstream iss(s);
	//stringstream iss;
	//iss.str("100 2025 9 9");
	istringstream iss("100 2025 9 9");
	int j;
	Date x;
	iss >> j >> x;
	cout << j << endl;
	cout << x << endl;
	int a = 1234;
	int b = 5678;
	string str;
	// 将一个整形变量转化为字符串,存储到string类对象中 
	stringstream ss;
	ss << a << " " << b;
	ss >> str;
	cout << str << endl;
	cout << ss.fail() << endl;
	cout << ss.bad() << endl;
	// 注意多次转换时,必须使用clear将上次转换状态清空掉 
	// stringstreams在转换结尾时(即最后一个转换后),会将其内部状态设置为badbit和failbit 
	// 因此下一次转换是必须调用clear()将状态重置为goodbit才可以转换 
	// 但是clear()不会将stringstreams底层字符串清空掉,str给一个空串可以清掉底层的字符串
	ss.clear();
	ss.str("");
	double dd = 12.34;
	ss << dd;
	ss >> str;
	cout << str << endl;
	return 0;
}
cpp 复制代码
#include<iostream>
#include<sstream>
#include<string>
using namespace std;

class Date
{
	friend ostream& operator << (ostream& out, const Date& d);
	friend istream& operator >> (istream& in, Date& d);

public:
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{}

private:
	int _year;
	int _month;
	int _day;
};

istream& operator >> (istream& in, Date& d)
{
	in >> d._year >> d._month >> d._day;
	return in;
}
ostream& operator << (ostream& out, const Date& d)
{
	out << d._year << " " << d._month << " " << d._day << endl;
	return out;
}

struct ChatInfo
{
	string _name; // 名字 
	int _id; // id

	Date _date; // 时间 
	string _msg; // 聊天信息 
};

int main()
{
	// 结构信息序列化为字符串 
	ChatInfo winfo = { "张三", 135246, { 2022, 4, 10 }, "晚上一起看电影吧" };
	ostringstream oss;
	oss << winfo._name << " " << winfo._id << " " << winfo._date << " " <<
		winfo._msg;
	string str = oss.str();
	cout << str << endl << endl;
	// 我们通过网络这个字符串发送给对象,实际开发中,信息相对更复杂, 
	// 一般会选用Json、xml等方式进行更好的支持 
	// 字符串解析成结构信息 
	ChatInfo rInfo;
	istringstream iss(str);
	iss >> rInfo._name >> rInfo._id >> rInfo._date >> rInfo._msg;
	cout << "-------------------------------------------------------" << endl;
	cout << "姓名:" << rInfo._name << "(" << rInfo._id << ") ";
	cout << rInfo._date << endl;
	cout << rInfo._name << ":>" << rInfo._msg << endl;
	cout << "-------------------------------------------------------" << endl;
	return 0;
}

三、反向迭代器的模拟实现

1. 分析

翻一下源码,我们可以看到reverse_iterator实现了两个版本,通过__STL_CLASS_PARTIAL_SPECIALIZATION条件编译控制使用哪个版本。简单点说就是支持偏特化的迭代器萃取以后,反向迭代器使用的是版本1,之前使用的是版本2:

cpp 复制代码
//版本1
template <class Iterator> class reverse_iterator;

//版本2
template <class BidirectionalIterator, class T, class Reference, class Distance>
class reverse_bidirectional_iterator;

template <class RandomAccessIterator, class T, class Reference, class Distance>
class reverse_iterator;

//迭代器萃取的本质上是一个特化,这里我们就不讲解了,有兴趣且基础功底好的同学可以查一下
  • 反向迭代器本质上是一个适配器,使用模板实现,传递哪个容器的迭代器就可以封装适配出对应的反向迭代器。因为反向迭代器的功能跟正向的迭代器功能高度相似,只是遍历的方向相反,类似operator++底层调用迭代器的operator--等,所以封装一下就可以实现。

  • 比较奇怪的是operator*的实现,内部访问的是迭代器当前位置的前一个位置。这个要结合容器中rbegin和rend实现才能看懂,rbegin返回的是封装end位置的反向迭代器,rend返回的是封装begin位置迭代器的反向迭代器,这里是为了实现出一个对称,所以解引用访问的是当前位置的前一个位置。

2. 实现

cpp 复制代码
#pragma once
 
template<class Iterator, class Ref, class Ptr>
class ReverseIterator
{
	typedef ReverseIterator<Iterator, Ref, Ptr> Self;
public:
	ReverseIterator(Iterator it)
		:_it(it)
	{}
 
	Ref operator*()
	{
		Iterator tmp = _it;
		return *(--tmp); //取的是上一个的数据
	}
 
	Ptr operator->()
	{
		return &(operator*());//取的是上一个的数据的地址
	}
 
	Self& operator++()
	{
		--_it;
		return *this;
	}
 
	Self& operator--()
	{
		++_it;
		return *this;
	}
 
	bool operator!=(const Self& s)
	{
		return _it != s._it;
	}
	
private:
	Iterator _it;
};

template<class T>
class list
{
    //反向迭代器
    typedef ReverseIterator<iterator, T&, T*> reverse_iterator;
    typedef ReverseIterator<const_iterator, const T&, const T*> const_reverse_iterator;

    reverse_iterator rbegin()
    {
	    return reverse_iterator(end());
    }
 
    reverse_iterator rend()
    {
	    return reverse_iterator(begin());
    }
}

四、计算器的实现

主要用到此算法(该文档为国外Gemini 2.5 的AI工具生成)

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;

int oPre(string ch)
{
	map<string, int> m = { {"+", 1}, { "-",1 }, { "*",2 }, { "/",2 } };
	return m[ch];
}

int evalRPN(vector<string>& v)
{
	map<string, function<int(int, int)>> m =
	{
		{"+",[](int a,int b) {return a + b; }},
		{"-",[](int a,int b) {return a - b; }},
		{"*",[](int a,int b) {return a * b; }},
		{"/",[](int a,int b) {return a / b; }},
	};
	
	stack<int> st;
	for (size_t i = 0; i < v.size(); i++)
	{
		if (m.find(v[i]) != m.end())
		{
			int right = st.top();
			st.pop();
			int left = st.top();
			st.pop();

			int ret = m[v[i]](left, right);
			st.push(ret);
		}
		else
			st.push(stoi(v[i]));
	}

	return st.top();
}


void toRPN(vector<string>& v, size_t& i, vector<string>& v2)
{
	stack<string> st;
	set<string> s = { "+","-","*","/","(",")"};
	while (i < v.size())
	{
		if (s.find(v[i]) == s.end())
		{
			v2.push_back(v[i]);
			i++;
		}
		else
		{
			if (v[i] == "(")
			{
				i++;
				toRPN(v, i, v2);
			}
			else if (v[i] == ")")
			{
				i++;
				while (!st.empty())
				{
					v2.push_back(st.top());
					st.pop();
				}
				return;
			}
			else
			{
				// 持续弹出所有优先级大于等于当前运算符的运算符
				while (!st.empty() && oPre(st.top()) >= oPre(v[i]))
				{
					v2.push_back(st.top());
					st.pop();
				}
				st.push(v[i]); // 当前运算符入栈
				i++;
			}
		}
	}
	while (!st.empty())
	{
		v2.push_back(st.top());
		st.pop();
	}
}

int calculate(string s) 
{
	//1.处理空格
	string news;
	for (auto e : s)
	{
		if (e != ' ')  news += e;
	}
	s.swap(news);
	news.clear();

	//2.处理负数
	for (size_t i = 0; i < s.length(); i++)
	{
		if (s[i] == '-')
		{
			if (i == 0 ||(!isdigit(s[i-1])&& s[i - 1]!=')'))
			{
				news += "0-";
			}
			else
				news += s[i];
		}
		else
			news += s[i];
	}
	s.swap(news);
	news.clear();

	//3.整数合并
	vector<string> v; // 中缀表达式 tokens
	for (size_t i = 0; i < s.length(); )
	{
		if (isdigit(s[i]))
		{
			news.clear();
			while (i < s.length() && isdigit(s[i]))
			{
				news += s[i];
				i++;
			}
			v.push_back(news);
		}
		else
		{
			v.push_back(string(1, s[i]));
			i++;
		}
	}
	size_t i = 0;
	vector<string> v2;
	toRPN(v, i, v2);

	int ret = evalRPN(v2);

	return ret;
}


后记:本来想总结一下,但我真的不想写了,谁懂写到最后一点时候的急躁啊,真不想写了,这C++是真的难,下面休憩几天更新高阶数据结构,就这吧,快结束,C++主线任务,拍板!结束!

相关推荐
weixin_462446231 小时前
使用 Python + Tkinter + openpyxl 实现 Excel 文本化转换
开发语言·python·excel
少许极端1 小时前
算法奇妙屋(十六)-BFS解决边权为1的多源最短路径问题
算法·bfs·队列·图解算法·边权为1的多源最短路径问题·宽度优先遍历
韩立学长1 小时前
基于协同过滤算法的宠物收养系统f27ny63s(程序、源码、数据库、调试部署方案及开发环境)系统界面展示及获取方式置于文档末尾,可供参考。
数据库·算法·宠物
明洞日记1 小时前
【数据结构手册006】映射关系 - map与unordered_map的深度解析
数据结构·c++
凌康ACG1 小时前
Sciter设置图标、设置进程名称
c++·sciter
廋到被风吹走1 小时前
【JDK版本】JDK1.8相比JDK1.7 JVM(Metaspace 与 G1 GC)
java·开发语言·jvm
冬夜戏雪1 小时前
【java学习日记】【2025.12.4】【4/60】
java·开发语言·学习
浅川.251 小时前
xtuoj Prime Twins
算法