Skip to content

Latest commit

 

History

History
728 lines (609 loc) · 20.5 KB

07.第七章:深入IO.md

File metadata and controls

728 lines (609 loc) · 20.5 KB

第七章:深入IO

// 常用的类型实际上是类模板实例化的结果
#include <iostream>

// 表示形式的变化:使用格式化 / 解析在数据的内部表示与字符序列间转换
int main()
{
    int x = 100;	// 00..00 01100100  
    // 将二进制的值格式化成 三个字符: 1 0 0 ; 输出流
    std::cout << x << std::endl;
    
    std::cin 
}

////////////////////////////////////////////////////

int main()
{
    union	// 联合体
    {		// 使用一块内存既表示 x , 又表示 y
        // int 和 float 都占了四个字节
        // 这四个字节可以同时用来表示 x 和 y
        int x;
        float y;
    };
    
    x = 100;
    
    std::cout << x << std::endl;	// -> 100
    std::cout << y << std::endl;	// -> 1.4013e-43
    // 用不同的方式解释同一块内存, 相应的格式化处理方式不同,
    // 类型不同输出流转换出来的字符序列不同
}
////////////////////////////////////////////////////
/*
template<
    class CharT,
    class Traits = std::char_traits<CharT>
> class basic_ifstream : public std::basic_istream<CharT, Traits>
*/

#include <fstream>	// 对文件进行输入输出操作

int main()
{
    std::ifstream x;
    std::basic_ifstream<char,std::char_traits<char>> x; //同上面等价
    // ifstream  -> basic_ifstream<char>
    //char 是一个模板的实参
    // wifstream -> basic_ifstream<wchar_t>
    
    // -->
    std::ifstream x = std::basic_ifstream<char>();
}

输入与输出

非格式化 I/O

#include <iostream>

int main()
{
    // 格式化
    int x;
    std::cin >> x;	// 格式化操作,读入字符解析成二进制表示,并保存在int中
    std::cout << x << std::endl; // 把 x 中二进制表示进行格式化,输出
    
    // 非格式化输入
    // basic_istream& read( char_type* s, std::streamsize count );
    std::cin.read(reinterpret_cast<char*>(&x), sizeof(x));
    std::cout << x << std::endl;
    // 输入 100 输出  170930225
    // 原因:希望读入四个字符,100和回车;每个字符都对应一个ASCII码,
    // 每个ASCII码是一串二进制的数值,read会把每个字符对应的ASCII码读出来
    // 把这个数值不作任何解析操作和变换,直接放到 x 的某个字节当中
    // 会把四个字符对应的ASCII码原封不动放到 x 的字符中
    // 放完后再尝试打印 x ,使用格式化的输出系统尝试把这样二进制的序列进行格式化解析
    // 二进制 -> int 转换; 因此转换完之后并不与 100 对应
    
    // 输入 1 ; 还在等待输入; 在输入 2 ; 输出  171051569
    // 因为采用非格式化输入,需要输入四个字符;1 + 回车 + 2 + 回车
}


// 非格式化优势
int main()
{
    float y = 3.14;  // 类型确定,对象所占的尺寸基本确定
    // 但字符个数不同,在寄存器内部所占宽度不同:3 是一个字符; 3.14是四个字符
    std::cout << y << std::endl; // 对人友好
    
    // 在一堆 float 数里找某一个字符输出,长度不可控
    // 非格式化:每个类型占的字符个数确定,可以定位到具体的字符,对计算机友好,人不可读
}

格式化 I/O

#include <iostream>

// C++ 通过操作符重载以支持内建数据类型的格式化 I/O
// 触发函数重载

int main()
{
    char x = '0';
    // 重载了移位操作符进行输入输出
    std::cout << x << std::endl;	// -> 0
    // std::cout << x 触发重载
    int y = static_cast<int>(x);
    std::cout << y << std::endl;	// -> 48
    // 根据传入的类型选择适当的调用以格式化
}

// 格式控制
// 可接收位掩码类型( showpos )
// C++ 基本数据单位是字节,一个字节占 8 位,
// 在一些特殊情况下,仅需要对 1 位或几位数据进行操作
// showpos 用来修改某一个字节的某一位
int main()
{
    char x = '0';
    // setf -> set flag   flag 一位
    std::cout.setf(std::ios_base::showpos); // positive + 
    // 需要显示正号时
    // 相当于改变了格式化的行为
    std::cout << x << std::endl;	// -> 0
    // 此处打印字符,并不是一个整数,不存在正负,所以没有正号
    int y = static_cast<int>(x);
    std::cout << y << std::endl;	// -> +48
}


// 取值相对随意( width )
// 字符类型( fill )
int main()
{
    char x = '0';
    std::cout.setf(std::ios_base::showpos); 
    std::cout.width(10);	// 要让整个输出占 10 个字符
    std::cout.fill('.');	// 用 . 填充字符
    std::cout << x << std::endl;	// -> "         0"
    int y = static_cast<int>(x);
    // 注意 width 方法的特殊性:触发后被重置
    std::cout << y << std::endl;	// -> +48
}

操纵符

#include <iostream>
#include <string>
#include <iomanip>

int main()
{
    char x = '0';
    int y = static_cast<int>(x);
    
    std::cout.setf(std::ios_base::showpos); 
    std::cout.width(10);	
    std::cout.fill('.');	
    std::cout << x << std::endl;	
	std::cout.width(10);
    std::cout << y << std::endl;	
    
    std::cout << x << '\n' << y << '\n';
}

#include <iomanip>

// 操纵符
int main()
{
    char x = '0';
    int y = static_cast<int>(x);
    
    std::cout << std::showpos 
              << std::setw(10) << std::setfill('.') << x << '\n'
              << std::setw(10) << y << std::endl;
}

// 提取会放松对格式的限制, 部分数据类型有效
int x;
char x; // 行为不一样,只取第一个字符
std::string x;
std::cin >> x; // +10;    10  -> 10

//提取 C 风格字符串时要小心内存越界
int main()
{
    char x[5];	// abcdefg \0
    std::cin >> x;
    std::cout << x << std::endl;
    
    std::cin >> std::setw(5) >> x; // abcdefg \0
    std::cout << x << std::endl;   // abcd
    
}

文件与内存操作

文件操作

// basic_ifstream / basic_ofstream / basic_fstream
#include <iostream>
#include <fstream>
#include <string>

int main()
{
    std::ofstream outFile("my_file");
    std::cout << outFile.is_open() << '\n'; // 判断文件流是否打开
    outFile << "Hello\n";  // 输出到文件里,cout 输出到终端
    
    std::ofstream outFile;	// 缺省构造,此时outFile没有和指定文件相关联
    std::cout << outFile.is_open() << '\n'; // 此时关闭,无关联
    outFile.open("my_file");	// 使 outFile 与文件关联
    std::cout << outFile.is_open() << '\n'; // 此时打开
    outFile.close();   // 解除关联
    std::cout << outFile.is_open() << '\n';  // 关闭
    
    // 读取文件
    std::ifstream inFile("my_file");
    std::string x;
    inFile >> x;
    std::cout << x << '\n';  // -> Hello
}

// 文件流可以处于打开 / 关闭两种状态
// 处于打开状态时无法再次打开,只有打开时才能 I/O


int main()
{
    // 类模板:1.构造函数,构造对象  2.析构函数,销毁对象
    // 销毁对象时会调用析构函数,此处的析构函数包含了关闭数据流的逻辑
    // 因此没有显示包含outFile.close()也能把缓存中的内容放入文件
	std::ofstream outFile;	// ofstream 抽象数据函数,使用类模板实现
    
    outFile.open("my_file");
    outFile << "Hello\n";	// 写到缓存
    outFile.close();  	// 把缓存中的内容放到文件里,再关闭
    // 确保缓存中的内容全部放到文件中
    
	// 如果想在某一行之前执行完销毁
    // 构造域  即可
    {
        std::ofstream outFile("my_file");
        outFile << "Hello\n";
    }
    // 此时 outFile 已经被关闭
}

文件流的打开模式

// 每种文件流都有缺省的打开方式
#include <iostream>
#include <fstream>

int main()
{
    std::ofstream outFile("my_file", std::ios_base::out);
    outFile << "Hello\n";
    
    std::ifstream inFile("my_file", std::ios_base::in);
    
    std::ios_base::in;		// 0010
    std::ios_base::ate;		// 0001
    std::ios_base::in | std::ios_base::ate  // 按位或 不是逻辑或
    // 按位或  0011  -> 表示既能实现 in 的功能又能实现 ate 的功能
        
    // 如果文件存在,会把原始文件清了,再写入  
    std::ofstream outFile("my_file", std::ios_base::out | std::ios_base::trunc);
    outFile << "World\n";
    
    // 附加
    std::ofstream outFile("my_file", std::ios_base::out | std::ios_base::app);
}

内存流

//	basic_istringstream
//	basic_ostringstream
//	basic_stringstream
#include <iostream>
#include <sstream>

// 输出流
int main()
{
    std::ostringstream obj1;	// 构造输出的内存流对象
    obj1 << 1234;	// 格式化方式输出,注意 这里输入是 1234 整数--> 字符串
    // obj1 << true;  // -> 1  最终是把数据放到 res 内;不是终端不是文件
    // 把 1234 放到一块内存中
    // 需要找到对应的内存读取相应的数据
    // 用 str 获取底层所对应的内存
    // 返回 std::basic_string<CharT,Traits,Allocator>  其实就是string
    auto res = obj1.str();
    // 可以直接写成
    std::string res = obj1.str();  // obj1 内部保存的对应的信息
    std::cout << res << std::endl; // -> 1234 字符串
}

#include <iomanip>

int main()
{
    std::ostringstream obj1;
    obj1 << std::setw(10) << std::setfill('.') << 10; // -> ........10
    std::string res = obj1.str();  
    std::cout << res << std::endl;
}
// 输入流
int main()
{
    std::ostringstream obj1;
    obj1 << 10;
    std::string res = obj1.str();  
    
    std::istringstream obj2(res); // 构造输入流时需把读取的内存给出
    int x;
    obj2 >> x;	// std::cin >> x 
    std::cout << x << std::endl;
}

// 也会受打开模式: in / out / ate / app 的影响

int main()
{
    // 输出流在 append 模式 (C++11)
    std::ostringstream buf2("test", std::ios_base::ate);
    buf2 << '1'; // 位于结尾处 + 1  // 可以自动扩大内存
    std::cout << buf2.str() << '\n';
    
    std::ostringstream buf2("test");
    buf2 << '1'; // 输出 1est ; 会把 t 替换掉
    std::cout << buf2.str() << '\n';
}

// 使用 str() 方法获取底层所对应的字符串
// 小心 避免使用 str().c_str() 的形式获取 C 风格字符串
int main()
{
    std::ostringstream buf2("test", std::ios_base::ate);
    buf2 << '1';
    std::string res = buf2.str();
    auto c_res = res.c_str();  // C 风格字符串
    // c_res 是一个指针,指向 字符串 开头地址
    std:: cout << c_res << std::endl;	// 合法
    // 这样也行,但要 小心销毁内存 导致 程序未定义 的情况发生
    std:: cout << buf2.str().c_str() << std::endl;
    
    // 不能这么写,程序未定义
    auto c_res = buf2.str().c_str();
    // buf2.str()返回的是右值 字符串,c_str()指向了这块字符串内部的首地址,此处还没问题
    // 因为执行了右值,此处会被释放掉
    std:: cout << c_res << std::endl;
    // 可能会指向一块已经被释放掉的内存,导致行为未定义
}

// 基于字符串流的字符串拼接优化操作
int main()
{
    std::string x;   // string 
    x += "Hello";	 // 插入字符串,先看string 是否有足够的缓冲区 保存
    x += "World";	 // 插入新的字符,基本上要建立新的缓冲区
    x += "Hello";	 // 把原始缓冲区的内容拷到新的缓冲区中
    x += "World";	 // 再把插入的字符添加到新的缓冲区中,再把原始的缓冲区释放
    // 涉及到缓冲区开辟,内存拷贝,缓冲区释放过程
    std::cout << x << std::endl;
    // 这样性能较差
    
    // 行为一样,但较上述性能要好
    // 基于字符串流的字符串拼接优化操作
    // 原理:数据先放到缓冲区而不是直接输出到设备;
    // 此处缓冲区较大,且在缓冲区满了的时候一次性把内容输出到对应设备
    // 此时才会设计到内存开辟、拷贝、释放; 减少了内存的消耗成本。因此性能更好
    std::ostringstream obj;
    obj << "Hello";
    obj << "World";
    obj << "Hello";
    obj << "World";
    std::cout << obj.str() << std::endl;
    // obj.str()触发缓冲区刷新,把内容输出到目标内存
}

流的状态

// iostate
typedef /*implementation defined*/ iostate;
static constexpr iostate goodbit = 0;
static constexpr iostate badbit = /*implementation defined  实现定义*/
static constexpr iostate failbit = /*implementation defined*/
static constexpr iostate eofbit = /*implementation defined*/
// 编译器定义的

/*    
常量   	解释
goodbit	无错误
badbit	不可恢复的流错误
failbit	输入/输出操作失败(格式化或提取错误)
eofbit	关联的输出序列已抵达文件尾*/
    
#include <iostream>

int main()
{
    //指定流状态标志。它是位掩码类型 (BitmaskType) 
    iostate -- char
        badbit  -- 0000,0001	// badbit | failbit = 0000,0011
        failbit -- 0000,0010
        eofbit  -- 0000,0100
}

#include <iostream>
#include <fstream>
//  badbit 不可恢复
int main()
{
    std::ofstream outFile; // 构造outFile
    outFile << 10;  // 没有对应文件,把outFile由一个正常状态变成异常状态
    // 异常状态 -> badbit 不可恢复
}

// failbit 可恢复
int main()
{
    // 输入流
    int x;
    std::cin >> x;
    // 输入 Hello 因为不是 int型 因此会 failbit
    
    // 输出流
    std::ofstream outFile;// 当前状态即是关闭状态
    outFile.close();// 再次进行关闭  failbit
}

// eofbit : end of file 读到文件尾没得读了会产生eofbit
// 对文件流、内存流以及终端流都有效
// 只对输入序列有关
int main()
{
    int x;
    std::cin >> x;
    // Ubuntu下 ctrl + D   --> eofbit
}

// 检测流的状态
// good() / fail() / bad() / eof()
int main()
{
    int x;
    std::cin >> x;
    
    std::cout << std::cin.good()
   	          << std::cin.fail()
              << std::cin.bad()
              << std::cin.eof()		// 检测异常状态eofbit
              << static_cast<bool>(std::cin) << std::endl;
}

//注意区分 fail 与 eof
// char 时 eof 和 fail 会被同时设置
int main()
{
    char x;
    // 先输入 a
    // Ubuntu下按两次 ctrl + D 
    std::cin >> x;
    std::cout << std::cin.fail() << ' ' << std::cin.eof() << std::endl;
    // 读完 a 已经读取完了系统不会再往下读取,因此 eof 输出为 0
    std::cin >> x;
    // 因为已经读到了结尾,eof 输出为 1,fail 输出 1
    std::cout << std::cin.fail() << ' ' << std::cin.eof() << std::endl;
    // 输出 0 0; 1 1
}
// int 时 eof 和 fail 不会被同时设置
int main()
{
    int x;	
    std::cin >> x;
    // 输入 10 ; 第一个字符 1 第二个字符 0,读到输入结尾
    // 输出 0 1   
    std::cout << std::cin.fail() << ' ' << std::cin.eof() << std::endl;
}
// 通常来说,只要流处于某种错误状态时,插入 / 提取操作就不会生效
// 复位流状态
// clear :设置流的状态为具体的值(缺省为 goodbit )
void clear( std::ios_base::iostate state = std::ios_base::goodbit );
int main()
{
    int x;	
    std::cin >> x;
    std::cout << std::cin.fail() << ' ' << std::cin.eof() << std::endl;
    std::cin.clear(); // 传入了 goodbit
    // 相当于把所有错误的状态给清空了,复位了流的状态
    // 也可以传入 failbit、badbit、eofbit,设置流的状态为具体的值   
}
// setstate :将某个状态附加到现有的流状态上
// 通常不会调用上述方法,一般不太需要对流的状态进行处理

// 捕获流异常:exceptions方法
// 程序处于非正常状态时 抛出异常,使程序跳转到异常处理
#include <iostream>
#include <fstream>
 
int main() 
{
    int ivalue;
    try {
        std::ifstream in("in.txt");
        in.exceptions(std::ifstream::failbit);
        in >> ivalue;
    } catch (std::ios_base::failure &fail) {
        // 此处处理异常
    }
}

流的定位

// 获取流位置
#include <iostream>

// tellg() / tellp() 可以用于获取输入 / 输出流位置 (pos_type 类型 )
// gap  拿       put 放
// 若 fail()==true ,则返回 pos_type(-1)  流错误则不能使用
#include <iostream>
#include <sstream>
int main()
{
    std::ostringstream s;	// 输出流
    std::cout << s.tellp() << '\n'; // 返回的是 当前 可以写入的位置
    s << 'h';
    std::cout << s.tellp() << '\n';
    s << "ello, world ";
    std::cout << s.tellp() << '\n';
    s << 3.14 << '\n';
    std::cout << s.tellp() << '\n' << s.str();
}
// 输出
0	// 当前可以写入的位置是 0 
1	// 写入位置往后挪一位
13
18
hello, world 3.14
    
    
//  tellg()
#include <iostream>
#include <string>
#include <sstream>
 
int main()
{
    std::string str = "Hello, world";
    std::istringstream in(str);  // 输入流
    std::string word;
    in >> word;	// 用 in 读取字符串并保存至 word
    std::cout << "After reading the word \"" << word
              << "\" tellg() returns " << in.tellg() << '\n';
    // 标准输入处理字符串时,按顺序依次处理,直到遇到分隔符
    // tellg 打印出 接下来 要读取或写入的字符
}
// 输出
After reading the word "Hello," tellg() returns 6
    
    
// 设置流位置
// seekg()  用于设置输入流的位置
basic_istream& seekg( pos_type pos );
basic_istream& seekg( off_type off, std::ios_base::seekdir dir);
// off_type 偏移量
pos	-	设置输入位置指示器到的绝对位置。
off	-	设置输入位置指示器到的相对位置。
dir	-	定义应用相对偏移到的基位置。它能为下列常量之一:
        常量	解释
        beg	流的开始
        end	流的结尾
        cur	流位置指示器的当前位置
    
#include <iostream>
#include <string>
#include <sstream>
 
int main()
{
    std::string str = "Hello, world";
    std::istringstream in(str);	// 定义输入流
    std::string word1, word2;
 
    in >> word1;
    in.seekg(0); // 回溯  //调用了第一个方法
    // 流的当前位置是6,回溯到了0
    in >> word2;
 
    std::cout << "word1 = " << word1 << '\n'
              << "word2 = " << word2 << '\n';
}
// 输出:
word1 = Hello,
word2 = Hello,

// seekp() 用于设置输出流的位置
basic_ostream& seekp( pos_type pos );
basic_ostream& seekp( off_type off, std::ios_base::seekdir dir );

#include <sstream>
#include <iostream>
 
int main()
{
    std::ostringstream os("hello, world");
    os.seekp(7);
    os << 'W';	// 小 w 换成 W
    os.seekp(0, std::ios_base::end);// 挪到最后一个位置
    os << '!';
    os.seekp(0);// 挪到开头
    os << 'H';
    std::cout << os.str() << '\n';
}
// 输出
Hello, World!

流的同步

#include <sstream>
#include <iostream>
 
// 输出流的同步
int main()
{
    std::cout << "What's your name\n";
    // 缓冲区满了在一次性输出
    // 若是基于缓冲区的用法,这句话还在缓冲区中,而系统还在等待输入
    // 不能采用缺省行为(缓冲区满输出),需要主动刷新缓冲区——流的同步
    // 方法1
    std::cout.flush();
    // 方法2
    std::cout << "What's your name\n" << std::flush;
    // 方法3
    std::cout << "What's your name" << std::endl;
    // endl  回车和刷新放在一起
    std::string name;
    std::cin >> name;
    // cin 绑定到了 cout 上
    // cin 在做任何操作之前会对 cout 的缓冲区进行刷新
    // 因此此处 cout 的内容一定能打印出来
    
}


// 输入流的同步
// sync() 用于输入流同步,其实现逻辑是编译器所定义的


// 输出流可以通过设置 unitbuf 来保证每次输出后自动同步
// std::cout << std::unitbuf; // 启用自动冲入
// 好处:信息立即显示
// 坏处:影响程序性能(与外部设备交互次数变多)


int main()
{
    // 区别
    std::cout << "What's your name" << std::endl;
    // 标准输出
    // cout 一般会设置缓冲区
    std::cerr << "What's your name" << std::endl;
    // 标准错误输出
    // 缺省情况会被设置成unitbuf,
    // 即cerr中的内容会被直接显示出来
}

// 基于绑定 (tie) 的同步
// 流可以绑定到一个输出流上,这样在每次输入 / 输出前可以刷新输出流的缓冲区
   A --> C // 将输出流 A 绑定到 C 上,即 A 记录了 C 中的内容
         D // 若再将 A 绑定到 D 上, A  不再绑定 C
   B --> C
       // A、B 可以同时绑定到一个输出流

// 绑定后, A 无论是输入还是输出流,A 在做输入输出操作之前一定会刷新 C 的缓冲区

       
// 缺省情况下, C++ 的输入输出操作会与 C 的输入输出函数同步
#include <iostream>
#include <cstdio>
 
int main()
{
    // 可以通过 sync_with_stdio 关闭该同步
    std::ios::sync_with_stdio(false); // 解除同步
       // 除非特别注重性能,一般不用关闭此同步
    std::cout << "a\n";
    std::printf("b\n");
    std::cout << "c\n";
}

// Possible output:
b
a
c