宣传旅游网站建设的重点是什么,wordpress tag列表页,织梦怎么修改网站标题,百度搜索优化关键词排名春风若有怜花意#xff0c;可否许我再少年。 文章目录 一、C11线程库1.thread类介绍2.mutex互斥锁 和 CAS原子操作#xff08;compare and set#xff09;3.lock_guard和unique_lock4.两个线程交替打印#xff0c;一个打印奇数#xff0c;一个打印偶数#xff08;线程同步…春风若有怜花意可否许我再少年。 文章目录 一、C11线程库1.thread类介绍2.mutex互斥锁 和 CAS原子操作compare and set3.lock_guard和unique_lock4.两个线程交替打印一个打印奇数一个打印偶数线程同步 二、CIO流1.C标准IO流自定义类型到内置类型的隐式类型转换2.C文件IO流2.1 二进制读写string作为二进制读写要谨慎否则把你坑的死死的2.2 文本读写类设计层次的代码复用i/ostream类的和重载 3.C字符串流 一、C11线程库
1.thread类介绍
1. C11的线程库实际封装了windows和linux底层的原生线程库接口在不同的操作系统下运行时C11线程库可以通过条件编译的方式来适配的使用不同的接口比如在linux下就用封装POSIX线程库的接口来进行多线程编程在windows下就用封装WinAPI线程库的接口来进行多线程编程。所以C11线程库为我们带来了可移植性编程。
下面是thread类的默认成员函数与POSIX不同的是利用无参构造创建出来的线程并不会运行而是只有给线程分配可调用对象之后该线程才会运行而POSIX中只要你调用了pthread_create接口线程就会立马运行起来。经常使用的thread构造函数就是传一个可调用对象然后可以选择给可调用对象传参或者不传参数都行也就是第二个构造函数该函数不允许隐式类型转换所以我们应该用()的方式构造出对象而不是用的方式来构造对象。第二个参数是可变参数模板专门用来给调用对象传参用的。 thread不允许拷贝和赋值这两个函数都被delete掉了但thread允许移动构造和移动赋值。 this_thread是std中的一个子命名空间其中包含了当前线程的相关属性接口例如get_id获取线程tid值yield让出当前线程的CPU时间片yield仅仅是一种提示操作系统是否执行这个提示这是不确定的操作系统可以选择忽略也可以选择执行其余两个接口是让线程休眠可以休眠一段时间也可以休眠到指定的时间点为止。 2. 在编写线程代码之前再来回顾重新认识一下进程和线程的关系以及linux下的进程结构等知识站在上层的角度把知识串一串。 线程和进程最大的区别就是分配资源和通信这两个方面linux下的线程是一种轻量级进程分配的资源是较轻的只需要分配一个PCB结构体即可多个线程之间共享地址空间页表等内核结构。而进程分配的资源是较重的进程之间具有独立性每个进程都有自己独立的内核数据结构。 所以由于分配资源的不同进而导致了通信成本的不同线程由于共享地址空间所以天然的就可以看到同一份资源因为我们知道地址空间是资源的窗口无论是线程还是进程他们都无法直接操纵物理地址只能通过资源窗口来访问所以共享地址空间本身就是共享大部分资源。而通信的前提是让不同的进程或线程看到同一份资源线程天然的就完成了这个工作自然线程间通信的成本就会低很多而进程之间具有独立性无法天然的完成这个工作所以进程间通信的成本一定是要比线程高的。 实际上linux下的进程就是一个族谱从0号进程开始一直fork出进程所以整个linux下的进程都是父子关系。0号进程通常指的是操作系统内核1号进程是由内核创建出的第一个用户空间进程1号进程也叫init进程他是所有其他用户进程的祖先进程。 3. 下面代码是经典的利用C11线程库实现的线程池即用一个vector来管理创建出的多个线程除直接存放线程对象外我们也可以new出来thread对象然后把指向对象的指针存到vector里面存指针的方式POSIX比较偏爱但今天我们就用vector来直接存储线程对象。在对线程扩容的时候有个坑我们不能显示的写出来thread的无参构造函数因为vector的resize接口对于第二个参数thread()匿名对象会进行拷贝而我们知道线程是不允许被拷贝的所以在调用resize初始化vector里面的每个线程时不要显示的给resize传第二个参数而是直接用resize的缺省参数即可。 为了给每个线程一个可调用对象我们遍历threads数组进行移动赋值将匿名的具有可调用对象的线程移动赋值给vector里面的线程对象。可调用对象除了下面使用lambda这样的方式之外还可以用包装器函数指针仿函数对象等等下面让num个线程打印cnt次自己的线程id获取线程id就可以通过this_thread命名空间中的get_id接口来获取。 如果未detach线程那么一定要join线程如果不join线程线程资源就得不到回收此时程序会异常终止。如果设置线程为detach该线程会被分离出地址空间操作系统会立即回收该线程的资源。所以要保证创建出线程之后线程运行完之后一定要join或detach线程否则会导致程序异常崩溃。
int main()
{//C11线程库封装了windows和linux的线程库通过条件编译来区分用封装linux的还是windows的接口//C11线程库面向对象int num, cnt;cin num cnt;vectorthread threads;//threads.resize(num, thread());//不要显示的传匿名对象因为resize的第二个参数会调用拷贝构造threads.resize(num);for (auto t : threads){t thread([cnt]() {for (int i 0; i cnt; i){//这里无法通过线程对象调用get_id()通过this_thread命名空间来调用get_id()cout std::this_thread::get_id() - i endl;}} );//这里直接调用移动赋值}for (auto t : threads){t.join();//阻塞式的回收线程资源 }return 0;
}2.mutex互斥锁 和 CAS原子操作compare and set
1. 当多个线程操作同一个共享资源时会出现线程不安全而造成的数据不一致等问题在下面的打印结果中当增大操作的次数过后(左图)可以明显看到val的值出现了问题没有达到30000的预期结果那么在这样的情况下为了保证线程安全一般需要加锁即让所有线程互斥式的访问这份共享资源这个操作在linux下的时候我们早就习以为常了所以互斥锁不是重点CAS原子操作才是重点。 2. C提供了线程安全的原子操作支持- -按位与按位或等等操作的原子性以保证线程安全下面贴了一个atomic的链接详细信息可以转过去看一下。 那CAS的原理是什么呢CAS实现主要是依靠三个操作数内存位置预期原值新值。每个线程会先将内存中的共享资源值拿到并将这个值设置为预期原值然后对其进行修改得到新值然后对比当前内存中的共享资源值是否与预期原值相同如果相同则将新值写回内存如果不相同则写回操作失败重新读取内存的值重新修改重新拿新的预期原值进行比对看是否满足写入要求。所以当多个线程在写回内存的时候操作系统将时间粒度缩的足够小那肯定是有先后顺序的当某一个线程写入工作完成之后其余线程在写入之前会进行内存值和预期原值的比对现在内存中的值是新值所以比对肯定是失败的那么其他线程的写入操作都会失败则需要重新while循环执行再一次的读取修改比对写回的工作而CAS就是compare and swap但也有人叫做compare and set我觉得compare and set更加形象一些拿线程的预期原值和当前内存位置中的值进行compare如果相同则将修改后的新值set到内存里面如果不相同则此次CAS操作失败重新while循环执行新的CAS操作。 这就是CAS操作的原理当多个线程在修改共享资源的值的时候由于CAS操作的约束则可以保证只有一个线程能够修改成功其余线程需要重新进行新一轮的CAS操作这就是线程安全的原子操作。 C中atomic类的介绍
3. 下面代码中也是演示了全局互斥锁和全局原子操作的使用方式保证了共享资源的线程安全但实际项目当中比较忌讳用全局变量因为全局变量工程的所有文件都可以看到链接时容易造成链接属性的问题所以我们一般都用局部的锁和原子。
int val 0;
mutex mtx;
atomicint atoval(0);//实际项目当中不太推荐用全局变量因为全局多个文件之间都可以看到会有链接属性的问题void Func1(int n)//每个线程都有自己的私有栈每个线程都会在私有栈建立线程函数栈帧
{for (int i 0; i n; i){//mtx.lock();atoval;//mtx.unlock();}
}
void Func2(int n)
{mtx.lock();for (int i 0; i n; i){//加锁和解锁也是有消耗的如果放里面则会频繁的申请锁释放锁这会导致效率降低阻塞到运行还需要线程上下文的保存和恢复这很废时间。//mtx.lock();val;//mtx.unlock();}mtx.unlock();for (int i 0; i n; i){atoval;//让变成原子操作}
}
int main()
{int m 100000;//200 100发生错误的概率不大稍微多线程操作的次数多一点那就会出错了thread t1(Func1, 2 * m);thread t2(Func2, m);t1.join();t2.join();cout atoval endl;return 0;
}3.lock_guard和unique_lock
1. lock_guard和unique_lock都是RAII的锁但unique_lock较为特殊一些他除了RAII外又主动实现了lock和unlock这也正是条件变量wait的时候只需要互斥锁的原因因为线程在条件变量中等待和被唤醒的时候需要释放锁和加锁而lock_guard只有RAII无法实现这样主动加锁和释放锁的功能所以条件变量wait的时候必须使用unique_lock。
unique_lock
2. 下面代码中我们不再使用全局的锁和原子而是使用局部的方式通过lambda捕捉原子和互斥锁的方式来实现线程安全使用RAII的锁对象时一般配合代码块来进行使用因为对象的生命周期随代码块儿所以有RAII对下的代码块就是所谓的临界区我们想让多线程串行打印cout语句。 除此之外引入了chrono类该类有多个创建出时间段duration的静态方法这可以让线程休眠一段指定的时间休眠函数可以用this_thread命名空间中的sleep_for接口。
int main()
{int m 100000;atomicint atoval 0;mutex mtx;auto func [](int cnt) {for (int i 0; i cnt; i){{lock_guardmutex lock(mtx);//可以搞一个代码块来控制临界区的粒度cout this_thread::get_id() - atoval endl;}atoval;//这是原子操作this_thread::sleep_for(chrono::milliseconds(1000));}};thread t1(func, 2 * m);thread t2(func, m);t1.join();t2.join();cout atoval endl;return 0;
}3. 还有一些其他杂七杂八的锁比较乱然后平常中我们也用不到因为我们并不清楚某一个线程被操作系统调度的具体情况无法做出准确的加锁或解锁某一段时间所以一般我们就用普通的互斥锁就够了但是这些杂七杂八的还是说一下比较好平常需要使用这些锁的时候直接去查文档就OK了看看原理看看使用样例就懂了。 try_lock是一种非阻塞式申请锁的接口如果锁状态未就绪则该函数直接返回可以让线程去做别的工作你也可以使用try_lock来轮询检测锁的状态。 另一个锁是recursive_mutex即递归互斥锁通过线程id则可以判断是否该线程能够进入临界区如果同一个线程多次进入临界区则递归锁是允许的其余线程想要进入临界区递归锁会互斥式的拒绝除非等锁就绪。 4.两个线程交替打印一个打印奇数一个打印偶数线程同步
1. 条件变量是配合互斥锁来进行使用的所以多线程访问条件变量的操作本身就是线程不安全的所以使用条件变量之前需要加锁并且条件变量的wait接口只允许使用unique_lock有两点原因一是unique_lock相比原生的mutex更为灵活且安全因为他是RAII的二是条件变量需要等待和唤醒操作这两个操作是在临界区中执行的那么就需要主动的申请和释放锁这点lock_guard做不到所以只能用unique_lock。
2. 通过条件变量来实现两个线程分别打印奇数和偶数是一种非常安全且经典的操作当条件不满足时让线程去条件变量内部维护的等待队列进行等待当条件满足时唤醒对应条件变量中等待的线程C11线程库提供了两个wait接口第二个接口不怎么好用因为有点绕所以一般都是直接用第一个接口让线程进行wait等待我们自己手动设置等待和唤醒的条件唤醒的接口是notify_one和notify_all分别对应POSIX中的pthread_cond_signal和pthread_cond_broadcast即唤醒一个线程和唤醒多个线程。 3. 代码实现并不复杂老铁们可以自己看一下。推荐使用第一个wait接口下面是程序的打印结果通过条件变量实现了线程的同步。 int main()
{int i 0;mutex mtx;condition_variable cond;//如果想要做到你打印完通知我我打印完通知你那就需要各自用while循环条件不满足就wait满足就notify//如果是for循环一个遍历奇数一个遍历偶数的话无法利用条件变量进行wait因为时时刻刻都是满足条件的。这样不行//打印奇数thread t1([]() {while (i 100){unique_lockmutex ulock(mtx);//必须用unique_lock因为它可以主动加锁和解锁//solution 1while (i % 2 0){cond.wait(ulock);}//solution 2cond.wait(ulock, []() { return !(i % 2 0); });//当i是偶数的时候那就阻塞返回false才会阻塞cout t1: this_thread::get_id() - i endl;i;cond.notify_one();}});//打印偶数thread t2([]() {while (i 100){unique_lockmutex ulock(mtx);// 必须用unique_lock因为它可以主动加锁和解锁//因为unique_lock可以手动加锁和解锁,那就可以满足条件变量的需求,当wait的时候unlock,当被notify时申请锁lock//而lock_guard不能手动加锁和解锁只能在创建和销毁的时候lock和unlock锁//solution 1while (i % 2 ! 0){cond.wait(ulock);//推荐使用这个wait接口下面那个wait接口太绕了}//solution 2cond.wait(ulock, []() { return !(i % 2 ! 0); });//当i是奇数的时候发生阻塞返回false才会阻塞cout t2: this_thread::get_id() - i endl;i;cond.notify_one();}});t1.join();t2.join();
}二、CIO流
1.C标准IO流自定义类型到内置类型的隐式类型转换
1. C标准库提供了四个全局流对象分别为cin cout cerr clog分别为将数据从键盘流向内存中的程序数据从内存程序流向显示器文件标准错误输出到显示器文件输出日志信息但cout、cerr、clog是ostream类的三个不同的对象这三个对象现在基本没有区别只是应用场景不同罢了。 cin是从缓冲区中拿数据我们键盘输入的数据会先存放到缓冲区中输入的数据以换行符为结束符cin读取时以空格和换行符作为数据的间隔。 C实现了一个庞大的输入输出流库其中ios为基类其他类都直接或间接的是ios类的派生类。 2. cin和cout支持所有内置类型的输入和输出其实就是因为运算符的函数重载cin和cout重载了所有的内置类型的流插入和流提取而自定义类型想要支持cin和cout也很简单只要类里面重载了自定义类型对象的和运算符的重载函数即可。 在很多在线OJ题目中有很多IO类型的题这些题往往都要求循环cin输入我们知道cin返回的对象是一个istream类的对象那为什么istream类对象能够做逻辑判断呢其实是因为隐式类型转换自定义类型对象可以隐式转换为内置类型这里的隐式类型转换的实现也是通过运算符重载来实现的不过严格意义上讲不能叫做运算符重载因为void *和bool不能算是运算符。 ios基类中实现了operator void *和operator bool函数这样的函数支持istream和ostream对象隐式类型转换为bool值之后作为while循环逻辑条件判断的值。当其他内置类型比如intint *double等类型作为逻辑条件判断时都是隐式类型转换为了bool值进行判断的。 3. 在下面代码中我们实现了A类的operator int函数则A类对象便可以隐式类型转换成内置类型int同理只要我实现了operator bool函数则A类对象也可以隐式类型转换为内置类型bool。 结束while循环的cin流提取可以通过ctrlc发送信号杀死进程或者是ctrlz将istream流对象转换为的bool类型值设置成false这样就可以结束while循环的cin流提取了。
class A
{
public:A(int a):_a1(1),_a2(2){}operator int(){return _a1 _a2;}
private:int _a1;int _a2;
};
int main()
{//cout 1111111111 endl;//cerr 1111111111 endl;//clog 1111111111 endl;string str;while (cin str)//表达式的返回值是流提取对象调用cin.operator(str)cin为什么能做逻辑条件判断呢//cin的父类ios重载了operator bool和operator void*void*作条件逻辑判断时还是会隐式的转为bool值//所以cin对象在作逻辑条件判断的时候可以隐式的转换为bool进行判断{cout str endl;}A aa1 1;// 内置类型隐式类型转换成自定义类型int a aa1;// 自定义类型隐式类型转换成内置类型cout a endl;return 0;
}4. 下面是用经典的日期类来演示自定义类型转换为内置类型的场景可以实现多种重载下面代码中实现了operator void */int/bool等三种支持日期类对象转换为对应内置类型的函数。 支持这样的函数过后C便可以让内置类型和自定义类型的对象都支持流插入和流提取并且还支持内置类型隐式类型转换到自定义类型(通过构造函数实现)自定义类型隐式类型转换到内置类型(通过operator 内置类型实现)。
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){}//operator bool()//给日期类重载一个operator bool这样日期类对象也可以隐式类型转换为bool//{// // 这里是随意写的假设输入_year为0则结束// if (_year 0)// return false;// else// return true;//}//operator void* ()//{// if (_year 0)// return nullptr;// else// return (void*)1;//}operator int(){if (_year 0)return 0;elsereturn 1;}
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;return out;
}
// C IO流使用面向对象运算符重载的方式
// 能更好的兼容自定义类型流插入和流提取
int main()
{// cout自动识别类型的本质--函数重载// 内置类型可以直接使用--因为库里面ostream类型已经实现了对应类型的运算符重载int i 1;double j 2.2;cout i endl;cout j endl;// 自定义类型则需要我们自己重载 和 Date d(2022, 4, 10);cout d endl;while (d)//直接让自定义类型作为while的判断条件年为0返回false不为0就一直输入{cin d;cout d;}return 0;
}2.C文件IO流
2.1 二进制读写string作为二进制读写要谨慎否则把你坑的死死的
1. C提供了文件IO的类分别是ifstream和ofstream提供了一套面向对象的写入和读取文件的接口C语言的面向过程就是需要先打开文件然后对文件进行读写操作而C只要创建好对应的istream/ostream对象则对应文件就会被打开当对象析构的时候则对应文件就会被关闭这也是面向对象和面向过程的不同。 2. 二进制读写的接口使用我简单说一下构造对象的接口需要文件名和open mode的两个参数我们用的文件名_filename是string类型而构造对象的接口是const char *类型由于string类内部提供了c_str接口所以string类型是可以隐式类型转换为const char *的。而打开文件的openmode早在ios_base类实现了所以其余所有的派生类都可以直接用openmode默认的ifstream和ofstream的openmode是in和out并且是文本读写。 调用ifstream和ofstream对象的类成员函数read和write时read是将二进制文件的内容读到char *的缓冲区当中write是将const char *缓冲区中的二进制内容写到文件里面。读取之后可能对缓冲区内容做出修改所以是缓冲区是非const修饰的写入过程中缓冲区的内容不应发生改动所以缓冲区是const修饰的。 3. 下面是二进制将结构体ServerInfo内容写到文件中的结果当结构体ServerInfo成员变量为char[32]数组时二进制写入和读取都是没有问题的而当结构体ServerInfo的char[32]数组改为string的时候二进制写入并读取而且读到的内容也是正确的但程序却异常退出了这是为什么呢 要想知道原因需要先知道什么是二进制写入二进制写入你可以简单理解为将数据的二进制表示形式原模原样的写入到文件中例如某个指针的二进制表示形式为0x0032447b3a(我自己编的)那在二进制写入时就会将数据的二进制表示形式原封不动的写到文件中所以二进制文件最终保存的是原始的二进制数据。而文本写入则是将所有类型先转换为字符类型将转换后的字符写入到文本文件当中所以文本文件最终保存的是字符数据。 当换了长一点的字符串后二进制写入的工作确实完成了但二进制读取的时候这回却什么都读不到读取和写入的过程是这个进程分开执行的用注释的方式将二进制写入和读取过程分开并且程序依旧是异常退出了。 4. 出现上面的现象主要和vs下string的结构有关系vs下的string在存储字符字节数小于等于15时会将内容存储到内部的一个buf数组里面这个buf数组的生命周期随string对象的生命周期结束而结束当存储字符字节数大于15时string内部有一个ptr指针此时会在堆上动态开辟一块内存用于存放大于15字节的内容而这个ptr指针存储的内容就是这块堆内存空间的地址。 而当string在作为二进制读写的时候会将ptr这个指针的二进制表示写入到文件而ptr指向的堆空间的内容并不会写入到文件中也就是原封不动的将结构体写入到二进制文件中当string存储字符串长度较短时其实就是将string的buf数组整体写入到文件里面那么读取的时候自然也会将文件中的内容读回到rinfo结构体中string的buf数组里面所以这个写入和读取的过程是没有问题的但还有一个容易忽略的因素就是ptr字符串内容较短时buf存储有效内容而ptr则会分配一个随机的野指针此时就出大问题了winfo结构体和rinfo结构体中各自的string对象里面的ptr指针都是相同的野指针而两个string对象在析构时ptr指针相同并且都是野指针所以就会出现析构野指针的情况这就会导致程序异常退出。 而当存储内容字节数较大时就会用ptr分配堆空间来存储但如果分开两次也就是注释读取让进程单执行写入然后再注释写入让进程单执行读取这样就是不同的进程来进行二进制读取和写入此时也会出问题因为原来的ptr指针确实指向有效的堆空间并且能够通过ptr虚拟地址访问到这个堆空间但是当换了进程之后原来的虚拟地址对于当前进程的地址空间来说是无效的通过原来进程的虚拟地址让当前进程继续访问虚拟地址指向的空间的话那就是野指针访问程序必然会出错所以这样也会出问题。 那如果是一个进程执行写入和读取呢并且string存储内容是内部ptr开辟堆空间来进行存储的这是否会出现问题呢这回可以读取内容成功因为虚拟地址还是有效的当前进程的地址空间没有发生改变但是在对象析构时还是会出问题原因很简单还是因为winfo和rinfo结构体内部string的ptr指针相同此时这两个指针虽然不是随机分配的指针而是指向有效堆空间的指针但谁让他们指向的堆空间是相同的呢一块空间被释放两次必然会出现野指针访问的问题这就是为什么进程会异常退出的原因。 析构两次string对象堆空间释放两次出现野指针访问的问题 5. 在上面分析了一大堆情况过后就知道为什么用string来进行二进制读写很坑了吧最主要还是因为指针的原因一旦指针作为二进制写入和读取就会出现写入缓冲区winfo和读取缓冲区rinfo的指针内容相同的情况那么此时在两个对象析构的时候就一定会出现野指针访问的情况所以用string来作为二进制读取和写入要谨慎防止野指针问题的出现。但光防止还是不够推荐的做法就是不要用string对象来进行二进制写入和读取而是直接使用char数组来进行二进制读取和写入这一定不会出现问题。 因为每个ServerInfo结构体在构造的时候都会分配各自的char数组所以各自的char数组占用的 内存空间都是不同的在进行二进制读取和写入的时候会将char中的所有内容的二进制表示形式写到内存里面读取的时候也会这么做但不同结构体的char数组内存位置不同所以在析构的时候大家都各自析构各自的并不会出现野指针问题这也是char数组作为二进制读写的优势所在。所以以后在进行二进制读写的时候用char数组就对了不要问为什么因为前人已经踩过坑了。
struct ServerInfo
{char _address[64];//表示结构体信息的时候没有用string用string的时候不能用二进制读写。string _address;//二进制读写要谨慎的用string否则会把你坑的死死的int _port;//Date _date;
};struct ConfigManager
{
public:ConfigManager(const char* filename):_filename(filename){}void WriteBin(const ServerInfo info)//二进制写入{//创建对象的时候会自动调用open函数析构对象会自动调用close函数ofstream ofs(_filename, std::ofstream::out | std::ios_base::binary);//ios_base就已经定义了modeofs.write((const char*)info, sizeof(info));//ofs.close();}void ReadBin(ServerInfo info)//二进制读取{//创建对象的时候会自动调用open函数析构对象会自动调用close函数ifstream ifs(_filename, std::ifstream::in | std::ios_base::binary);//ios_base就已经定义了modeifs.read((char*)info, sizeof(info));//ifs.close();}private:string _filename; // 配置文件
};
int main()
{ConfigManager cm(test.txt);ServerInfo winfo { 192.0.0, 80};//测试数据1ServerInfo winfo { 192.0.0.111111111111111111, 80};//测试数据2cm.WriteBin(winfo);ServerInfo rinfo;cm.ReadBin(rinfo);cout rinfo._address endl;cout rinfo._port endl;return 0;
}2.2 文本读写类设计层次的代码复用i/ostream类的和重载
1. 进行文本读写时用string或是用char数组都是无所谓的因为不管你是什么类型在进行文本读写时都会先将类型转为字符类型然后将字符写入到文件当中。 比较牛的一点是i/ofstream的对象都可以使用和来进行数据向文件插入和数据从文件提取只不过数据流动的对象换了以前是针对于显示器和键盘现在可以是所有文件包括键盘和显示器文件。 所以上面的二进制读写除了使用read和write接口外也可以使用流插入和流提取来进行二进制读写只不过二进制模式下和会直接将内容写到内存里面不会对字符串做解析比如说文本读写会以空格和换行符作为间隔但二进制读写不会这么做的你给什么他就直接写什么不会做任何额外的处理。至于选择调用运算符重载还是调用read和write接口选择权在于你。 2. 为什么i/ofstream对象可以直接用流插入和流提取呢因为类设计层次的代码复用说白了就是继承带来的效果基类重载的成员函数派生类都可以直接调用所以在使用i/ofstream对象进行读写时除了调用read和write接口外也可以直接用流插入和流提取。 如果日期类对象也实现了流插入和流提取那么i/ofstream对象也就可以直接将日期类对象写到文件和从文件中读取日期类对象这其实是因为派生类对象赋值给基类对象是天然的切割赋值过程所以i/ofstream对象是可以直接调用日期类对象的i/ostream流插入和流提取的。 所以除了标准IO外对于文件的IO也是可以使用流插入和流提取的。包括内置类型和自定义类型都是可以进行流插入和流提取只要重载了对应的和函数即可。
struct ServerInfo
{//文本读写用string或者是用数组都是无所谓的char _address[64];//string _address;int _port;Date _date;
};struct ConfigManager
{
public:ConfigManager(const char* filename):_filename(filename){}void WriteText(const ServerInfo info){ofstream ofs(_filename);//用ofstream自带的第二个缺省参数mode::outofs info._address endl info._port endl;//以空格或换行符作为分隔依据//遇到整型就会将其转成字符串写到文件里比如以前我们cout输出信息到显示器文件里面时显示器文件放的都是字符串ofs info._date endl;//重载了日期类对象的流插入和流提取}void ReadText(ServerInfo info){ifstream ifs(_filename);ifs info._address info._port info._date;}private:string _filename; // 配置文件
};int main()
{ConfigManager cm(test.txt);ServerInfo winfo { 192.0.0.11111111111111111111111111, 80, {2023, 5, 22} };cm.WriteText(winfo);ServerInfo rinfo;cm.ReadText(rinfo);cout rinfo._address endl;cout rinfo._port endl;cout rinfo._date endl;return 0;
}3.C字符串流
1. C标准库还实现了istringstream和ostringstream类用于进行多种类型序列化为字符串类型和将字符串类型反序列化为其他多种类型。 i/ostringstream对象内部维护了一个string对象用于存储序列化之后的结果和从中提取结果进行反序列化。可以调用i/ostringstream对象内部的str()接口来返回其内部维护的string对象。 stringstream内部使用string类对象代替字符数组可以避免缓冲区溢出的危险而且其会对参 数类型进行推演不需要格式化控制也不会出现格式化失败的风险因此使用更方便更 安全。 //stringstream既有istringstream的功能也有ostringstream的功能int main()
{//把Date转成一个字符串int i 999;double dou1 13.14;Date d1 { 2023, 5, 22 };ostringstream oss;oss i dou1 d1;string str oss.str();cout str endl;int j;double dou2;Date d2;istringstream iss(str);iss j dou2 d2;cout str;return 0;
}2. 在进行多次转换时需要调用clear()函数将状态标志位设置为允许进行新一轮的转换但clear并不会清空stringstream内部维护的string对象内容所以如果仅调用clear()接口重置标志位的话则新一轮的序列化内容会重复累积到string尾部。 所以如果想要进行全新一轮的转换则可以先调用str()接口将string底层内容设置为空(只有’\0’)然后再调用clear重置状态标志位当然顺序也可以反过来。
int main()
{int a 12345678;string sa;// 将一个整形变量转化为字符串存储到string类对象中stringstream s;s a;s sa;cout sa endl;// clear()// 注意多次转换时必须使用clear将上次转换状态清空掉// stringstreams在转换结尾时(即最后一个转换后),会将其内部状态设置为badbit// 因此下一次转换是必须调用clear()将状态重置为goodbit才可以转换// 但是clear()不会将stringstreams底层字符串清空掉// s.str();// 将stringstream底层管理string对象设置成, // 否则多次转换时会将结果全部累积在底层string对象中s.str();s.clear(); // 清空s, 不清空下一轮的转换是无效的string存储的还是上一轮的结果double d 12.34;s d;s sa;string sValue;sValue s.str(); // str()方法返回stringsteam中管理的string类型cout sValue endl;return 0;
}3. 下面这段代码就是直接使用stringstream来进行序列化和反序列化使用的方式也非常简单直接复用i/ostream类的operator 和operator 重载函数即可所以你可以看到C的这一套继承体系带来很大的便捷无论是标准IO还是文件IO还是字符串IO都可以使用统一的一套标准来实现即通过operator 和operator 重载函数来完成IO的过程。 不过使用stringstream来进行序列化和反序列化格式控制过于单一所以大部分公司都不喜欢用stringstream而是用一些第三方库例如jsonxml等来进行序列化和反序列化。
struct ChatInfo
{string _name; // 名字int _id; // idDate _date; // 时间string _msg; // 聊天信息
};
//下面是简单的序列化和反序列化
int main()
{// stringstream作序列化和反序列化只能作简单的分割例如用空格或\n来作为分隔符难一点的分隔他做不到//尤其面对复杂数据的时候。ChatInfo winfo { 张三, 123456, { 2023, 5, 22 }, 晚上一起看电影吧};//序列化stringstream oss;oss winfo._name : winfo._id winfo._date winfo._msg ;cout oss.str() endl endl;cout 网络发送 endl endl;// 我们通过网络这个字符串发送给对象实际开发中信息相对更复杂// 一般会选用Json、xml等方式进行更好的支持// 字符串解析成结构信息//反序列化ChatInfo rInfo;stringstream iss(oss.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;
}