C++- 基于多设计模式下的同步异步日志系统
第一个项目:13万字,带源代码和详细步骤
目录
第一个项目:13万字,带源代码和详细步骤
1. 项目介绍
2. 核心技术
3. 日志系统介绍
3.1 为什么需要⽇志系统
3.2 ⽇志系统技术实现
3.2.1 同步写⽇志
3.2.2 异步写⽇志
4.知识点和单词补充
4.1单词补充
4.2知识点补充
4.2.1完美转发forward
4.2.3 override
4.2.4 虚函数和纯虚函数
4.2.5智能指针中的reset
4.2.6继承和多态中,函数返回类型是父类的,返回子类的类型
4.2.7继承和多态中,函数返回类型是父类的,怎么样才可以返回子类的类型
4.2.8 stat(文件存不存在)
4.2.9 文件路径查找
4.3.0 c++线程
4.3.1 using的用法
4.3.2 ostream将数据写入
4.3.3 using和智能指针一起控制类对象
4.3.4使用 strftime 格式化时间
4.3.5 stringstream
4.3.6 终止一个程序的执行abort
4.3.7 cout.write
4.3.8 ofstream中的接口
4.3.9 stringstream
4.4.0 enum clss
4.4.1 atomic原子操作的库
4.4.2 unique_lock
4.4.3 智能指针中get接口
4.4.4 copy
4.2.5 ifstream的成员函数
4.2.6 seekg
4.2.7 condition_variable(多线程)
4.2.8 回调函数和智能指针
4.2.9 c++中的线程
4.3.0 线程的接口
4.3.1 bind绑定
5. 相关技术知识补充
5.1 不定参函数
不定参宏函数
打印文件名和行号
宏函数
编辑
c语言中不定参函数的使用,不定参数据的访问
模拟一个printf
C++风格不定参函数
5.2 设计模式
六⼤原则:
单例模式
• 饿汉模式:
懒汉模式
编辑
⼯⼚模式
抽象工厂模式(开闭原则没有遵循)
建造者模式:
代理模式
6. 日志系统框架设计
6.1模块划分
6.2 模块关系图
7. 代码设计
7.1 实⽤类(常用)设计
7.2 日志等级类设计
7.3 日志消息类设计
7.4 日志输出格式化类设计
7.5日志落地类的设计
7.6 日志器类(Logger)设计(建造者模式)
7.7 双缓冲区异步任务处理器(AsyncLooper)设计(实现异步日志缓冲区)
7.8 异步⽇志器(AsyncLogger)设计
7.9 单例日志器管理类设计(单例模式)
7.10日志宏&全局接口设计 (代理模式)
8.目前为止代码基本完成
buffer.hpp
format.hpp
level.hpp
ljwlog.h
logger.hpp
loop.hpp
makefile
message.hpp
sink.hpp
test.cc
util.hpp
1. 项目介绍
本项目主要实现⼀个日志系统,其主要⽀持以下功能:
• ⽀持多级别日志消息
• ⽀持同步日志和异步日志
• ⽀持可靠写⼊日志到控制台、⽂件以及滚动⽂件中
• ⽀持多线程程序并发写日志
• ⽀持扩展不同的日志落地⽬标地
2. 核心技术
• 类层次设计(继承和多态的应⽤)
• C++11(多线程、auto、智能指针、右值引⽤等)
• 双缓冲区
• ⽣产消费模型
• 多线程
• 设计模式(单例、⼯⼚、代理、建造者等)
本项⽬不依赖其他任何第三⽅库,只需要安装好CentOS/Ubuntu+vscode/vim(vscode写)环境即可开发。
3. 日志系统介绍
3.1 为什么需要⽇志系统
• ⽣产环境的产品为了保证其稳定性及安全性是不允许开发⼈员附加调试器去排查问题,可以借助日志系统来打印⼀些日志帮助开发⼈员解决问题
• 上线客户端的产品出现bug无法复现并解决,可以借助⽇志系统打印⽇志并上传到服务端帮助开发⼈员进⾏分析
• 对于⼀些⾼频操作(如定时器、⼼跳包)在少量调试次数下可能⽆法触发我们想要的⾏为,通过断点的暂停⽅式,我们不得不重复操作⼏⼗次、上百次甚⾄更多,导致排查问题效率是⾮常低下,可以借助打印⽇志的⽅式查问题
• 在分布式、多线程/多进程代码中,出现bug⽐较难以定位,可以借助⽇志系统打印log帮助定位 bug
• 帮助⾸次接触项⽬代码的新开发⼈员理解代码的运⾏流程
3.2 ⽇志系统技术实现
⽇志系统的技术实现主要包括三种类型:
• 利⽤printf、std::cout等输出函数将⽇志信息打印到控制台
• 对于⼤型商业化项目,为了⽅便排查问题,我们⼀般会将日志输出到⽂件或者是数据库系统⽅便查询和分析日志,主要分为同步⽇志和异步⽇志⽅式
◦ 同步写日志
◦ 异步写日志
3.2.1 同步写⽇志
同步⽇志是指当输出⽇志时,必须等待⽇志输出语句执⾏完毕后,才能执⾏后⾯的业务逻辑语句,⽇志输出语句与程序的业务逻辑语句将在同⼀个线程运⾏。每次调⽤⼀次打印⽇志API就对应⼀次系统调 ⽤write写⽇志⽂件。
在⾼并发场景下,随着⽇志数量不断增加,同步⽇志系统容易产⽣系统瓶颈:
• ⼀⽅⾯,⼤量的⽇志打印陷⼊等量的write系统调⽤,有⼀定系统开销.
• 另⼀⽅⾯,使得打印⽇志的进程附带了⼤量同步的磁盘IO,影响程序性能.
3.2.2 异步写⽇志
异步⽇志是指在进⾏⽇志输出时,⽇志输出语句与业务逻辑语句并不是在同⼀个线程中运⾏,⽽是有专⻔的线程⽤于进⾏⽇志输出操作。业务线程只需要将⽇志放到⼀个内存缓冲区中不⽤等待即可继续执⾏后续业务逻辑(作为⽇志的⽣产者),⽽⽇志的落地操作交给单独的⽇志线程去完成(作为⽇志的消费者),这是⼀个典型的⽣产-消费模型。
这样做的好处是即使⽇志没有真的地完成输出也不会影响程序的主业务,可以提⾼程序的性能:
• 主线程调⽤⽇志打印接口成为⾮阻塞操作
• 同步的磁盘IO从主线程中剥离出来交给单独的线程完成
4.知识点和单词补充
4.1单词补充
单例模式:Singleton pattern
实例:instance
工厂:factory
指针:pointer(ptr的由来)(智能指针shared_ptr等等)
建造者:Builder
主板:Motherboard(_board)
显示器:Display(_display)
操作系统:Operating System(_os)
参数:paramater
指挥者:director
建造(组建):construct
房东:landlord
中介:intermediary
实用类:Utility class(util)
存在:exist
文件:file
路径:path
目录:directory
格式:format
项:item
模式:pattern
格式化程序:Formatter
有效载荷:payload
下沉(落地):sink
基础:base
限制:limit
同步:synchronization
异步:asynchronous
序列化:serialization
同步:synchronization
异步:asynchronous
缓冲:buffer
默认的:default
阈值:threshold
增量:increment
循环:loop
消费者:consumer
生产者:producer
日志记录器:logger
管理者:manager
根,根源:root
整体的全局的:global
4.2知识点补充
4.2.1完美转发forward
在C++中,完美转发(Perfect Forwarding)是一种用于在函数模板中保持参数原有类型的技巧,包括它们的const和volatile修饰符以及引用属性。这在编写模板代码时非常有用,特别是在创建工厂函数或者封装其他函数调用时。
以下是对完美转发的一些总结:
基本概念
-
转发:将函数的参数原封不动地传递给另一个函数。
-
完美转发:在转发过程中保持参数的左值或右值属性。
关键字和操作符
-
std::forward<T>(u)
:条件性转发,如果u
是左值,则返回左值引用;如果u
是右值,则返回右值引用。 -
T&&
:通用引用,在模板参数中用来捕获任意类型的参数。
完美转发的实现
在函数模板中,通常结合使用T&&
和std::forward
来实现完美转发:
template<typename T>
void func(T&& arg) {
someOtherFunc(std::forward<T>(arg));
}
这里,T&&
是一个通用引用,可以绑定到任何类型的参数上,std::forward<T>(arg)
则确保了参数的左右值属性在转发时得以保持。
注意事项
-
通用引用:只有当
T&&
是模板参数时,它才是通用引用。否则,它可能是一个右值引用。 -
引用折叠:当将一个引用类型绑定到另一个引用类型上时,会发生引用折叠,例如,
T& &
会折叠成T&
,而T&& &
会折叠成T&
。 -
转发时的const保持:如果参数是const的,完美转发也会保持const属性。
完美转发的用途
-
在模板库中,如工厂模式、函数适配器等,允许用户以最有效率的方式传递参数。
-
在函数重载和模板特化中,确保参数的值类别不变。
示例
#include <iostream>
#include <utility> // for std::forward
void print(int& x) {
std::cout << "lvalue: " << x << std::endl;
}
void print(int&& x) {
std::cout << "rvalue: " << x << std::endl;
}
template<typename T>
void wrapper(T&& arg) {
print(std::forward<T>(arg));
}
int main() {
int a = 5;
wrapper(a); // 转发为左值
wrapper(5); // 转发为右值
return 0;
}
在这个例子中,wrapper
函数模板使用完美转发来保持参数arg
的值类别,并将它传递给print
函数。
总结
完美转发是C++11中引入的一个高级特性,它使得模板代码更加灵活和高效。理解并正确使用完美转发,对于编写可重用和高效的C++模板代码至关重要。
4.2.2 shared_ptr和make_shared
在C++中,shared_ptr
是一种智能指针,它提供了对动态分配的对象的共享所有权。make_shared
是一个标准库函数模板,用于创建一个shared_ptr
对象,同时管理动态分配的对象。以下是shared_ptr
和make_shared
之间的关系:
shared_ptr
:
-
shared_ptr
是一个模板类,定义在<memory>
头文件中。 -
它用于管理动态分配的对象,通过引用计数机制来确保对象在不再被需要时自动被销毁。
-
当最后一个
shared_ptr
被销毁或重置时,它所管理的对象会被自动删除(调用delete
)。
make_shared
:
-
make_shared
是一个模板函数,同样定义在<memory>
头文件中。 -
它用于创建一个
shared_ptr
对象,并分配和管理一个动态对象。 -
make_shared
通常比直接使用new
和shared_ptr
的构造函数更高效,因为它可以一次性分配共享的控制块和对象内存,减少了内存分配的次数。
shared_ptr
和make_shared
的关系:
-
内存分配:
-
使用
new
和shared_ptr
的构造函数时,内存分配分为两步:首先分配对象内存,然后分配共享的控制块(用于引用计数和弱引用计数)。 -
使用
make_shared
时,内存分配通常是一步完成的,即同时分配对象内存和控制块内存,这减少了内存分配的开销。
-
-
异常安全性:
-
make_shared
提供了更强的异常安全性保证。在构造对象和控制块时,如果抛出异常,不会产生内存泄漏,因为内存分配是原子操作。
-
-
性能:
-
make_shared
可能比直接使用shared_ptr
的构造函数更快,因为它减少了内存分配的次数。 -
make_shared
返回的shared_ptr
可以直接使用,无需额外的步骤。
-
-
使用方式:
-
使用
make_shared
时,不需要显式指定对象类型,它会根据传递的参数自动推导。
-
示例:
以下是使用shared_ptr
和make_shared
的例子:
#include <memory>
#include <iostream>
class MyClass {
public:
MyClass() { std::cout << "MyClass constructed\n"; }
~MyClass() { std::cout << "MyClass destroyed\n"; }
};
int main() {
// 使用 make_shared 创建 shared_ptr
std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>();
// 直接使用 shared_ptr 构造函数
std::shared_ptr<MyClass> ptr2(new MyClass());
return 0;
}
在这个例子中,ptr1
和ptr2
都是shared_ptr
,但ptr1
是通过make_shared
创建的,而ptr2
是通过new
和shared_ptr
的构造函数创建的。
结论:
make_shared
是创建shared_ptr
对象的首选方式,因为它更高效且提供更好的异常安全性。然而,在某些情况下,例如需要自定义删除器或者初始化对象时,可能需要直接使用shared_ptr
的构造函数。
4.2.3 override
在C++中,override
关键字用于明确表示派生类中的函数意在重写基类中的虚函数。这是C++11标准中引入的一个特性,旨在提高代码的可读性和可维护性。
下面是使用override
关键字的一个简单示例:
class Base {
public:
virtual void doSomething() {
// 基类的实现
}
};
class Derived : public Base {
public:
void doSomething() override { // 使用override关键字
// 派生类的实现,重写基类的虚函数
}
};
在这个例子中,Derived
类中的doSomething
函数使用了override
关键字,这表明它重写了Base
类中的doSomething
虚函数。如果基类中不存在这样的虚函数,或者签名不匹配,编译器将会报错。
使用override
关键字的几个好处:
-
明确性:清楚地表明函数的意图是重写基类中的虚函数。
-
安全性:如果基类中的虚函数签名改变了,而派生类没有更新,编译器将会报错,因为不再满足重写的条件。
-
维护性:有助于其他开发者理解代码,特别是在大型项目中。
需要注意的是,override
关键字不会改变函数的任何行为,它仅仅是一个指示器,告诉编译器这个函数应该重写基类中的某个虚函数。
4.2.4 虚函数和纯虚函数
在 C++ 中,虚函数和纯虚函数是实现多态性的重要机制,尤其是在面向对象编程中。它们的主要区别在于它们的定义和用途。下面将详细介绍这两者的区别及其用法。
1. 虚函数(Virtual Function)
虚函数是基类中声明为 virtual
的成员函数,允许在派生类中重写(override)。虚函数的主要目的是实现运行时多态性。
特点:
-
在基类中使用
virtual
关键字声明。 -
可以在派生类中被重写。
-
可以有实现(即可以在基类中定义函数体)。
-
通过基类指针或引用调用时,会根据对象的实际类型调用相应的函数。
示例:
#include <iostream>
class Base {
public:
virtual void show() { // 虚函数
std::cout << "Base class show function called." << std::endl;
}
};
class Derived : public Base {
public:
void show() override { // 重写虚函数
std::cout << "Derived class show function called." << std::endl;
}
};
int main() {
Base* b; // 基类指针
Derived d; // 派生类对象
b = &d; // 指向派生类对象
b->show(); // 调用派生类的 show(),输出 "Derived class show function called."
return 0;
}
2. 纯虚函数(Pure Virtual Function)
纯虚函数是没有实现的虚函数,通常用于定义接口。它在基类中声明为 virtual
,并在函数声明后加上 = 0
。包含纯虚函数的类称为抽象类,不能实例化。
特点:
-
在基类中声明为
virtual
,并且在声明后加上= 0
。 -
不可以在基类中有实现(没有函数体)。
-
派生类必须重写纯虚函数才能实例化。
-
用于定义接口,强制派生类实现特定功能。
示例:
#include <iostream>
class AbstractBase {
public:
virtual void show() = 0; // 纯虚函数
};
class ConcreteDerived : public AbstractBase {
public:
void show() override { // 重写纯虚函数
std::cout << "ConcreteDerived class show function called." << std::endl;
}
};
int main() {
// AbstractBase ab; // 错误:不能实例化抽象类
ConcreteDerived cd; // 可以实例化派生类
AbstractBase* ab = &cd; // 基类指针指向派生类对象
ab->show(); // 调用派生类的 show(),输出 "ConcreteDerived class show function called."
return 0;
}
3. 总结
特性 |
虚函数 |
纯虚函数 |
---|---|---|
定义 |
使用 |
使用 |
实现 |
可以有实现 |
没有实现 |
抽象类 |
不一定是抽象类 |
是抽象类 |
实例化 |
可以实例化包含虚函数的类 |
不能实例化包含纯虚函数的类 |
派生类要求 |
可选重写 |
必须重写 |
用法区别
-
虚函数用于需要在基类中提供默认实现的情况,允许派生类选择性地重写。
-
纯虚函数用于定义接口,强制派生类实现特定的功能,确保派生类提供具体的实现。
通过理解虚函数和纯虚函数的区别及其用法,你可以更好地利用 C++ 的面向对象特性,设计出灵活和可扩展的程序。
4.2.5智能指针中的reset
在 C++ 中,reset
通常与智能指针(如 std::unique_ptr
和 std::shared_ptr
)有关,用于管理动态分配的内存。下面将详细介绍 reset
的用法及其总结。
1. std::unique_ptr
的 reset
std::unique_ptr
是 C++11 引入的一种智能指针,确保对动态分配内存的独占拥有权。当 unique_ptr
被销毁时,它会自动释放指向的内存。reset
方法用于更改 unique_ptr
所管理的对象。
用法:
-
释放当前管理的对象:调用
reset
会释放当前指针所管理的内存。 -
管理新对象:可以将
reset
用于分配新的对象。
示例:
#include <iostream>
#include <memory>
int main() {
std::unique_ptr<int> ptr(new int(10)); // 创建一个 unique_ptr 管理的对象
std::cout << "Value: " << *ptr << std::endl; // 输出值
ptr.reset(new int(20)); // 释放原来的对象并管理新的对象
std::cout << "New Value: " << *ptr << std::endl; // 输出新的值
ptr.reset(); // 释放当前对象
// 现在 ptr 是 nullptr,不能再解引用
return 0;
}
std::shared_ptr
也是一种智能指针,允许多个指针共享同一个对象。它会自动管理引用计数,当最后一个指向该对象的 shared_ptr
被销毁时,内存才会被释放。
用法:
-
释放当前管理的对象:调用
reset
会减少引用计数,并可能释放内存。 -
管理新对象:可以将
reset
用于指向新的对象。
示例:
#include <iostream>
#include <memory>
int main() {
std::shared_ptr<int> sptr(new int(30)); // 创建一个 shared_ptr 管理的对象
std::cout << "Value: " << *sptr << std::endl; // 输出值
sptr.reset(new int(40)); // 释放原来的对象并管理新的对象
std::cout << "New Value: " << *sptr << std::endl; // 输出新的值
sptr.reset(); // 释放当前对象
// 现在 sptr 是 nullptr,不能再解引用
return 0;
}
3. 总结
-
功能:
-
reset
方法用于更改智能指针所管理的对象,无论是std::unique_ptr
还是std::shared_ptr
。 -
当调用
reset
时,当前指针管理的对象会被销毁(如果存在),并且指针会被更新为新的对象。
-
-
适用场景:
-
用于动态分配内存的智能指针,使得内存管理更安全,避免内存泄漏。
-
特别适合在资源管理和生命周期控制上需要精细管理的场景。
-
-
注意事项:
-
确保在使用
reset
之前,指针不应为nullptr
,否则会导致解引用错误。 -
使用
reset
时,确保理解对象的所有权转移,特别是在shared_ptr
中,确保引用计数的管理。
-
通过使用智能指针及其 reset
方法,C++ 程序员能够更安全地管理动态分配的内存,减少内存泄漏的风险。
4.2.6继承和多态中,函数返回类型是父类的,返回子类的类型
在 C++ 中,函数返回类型为父类的确可以返回子类的对象,这是面向对象编程中的多态性的一部分。具体来说,当一个函数声明的返回类型是父类时,你可以返回一个子类的实例。这种行为称为向上转型(upcasting),允许你通过父类的指针或引用来操作子类对象。
示例代码
#include <iostream>
using namespace std;
// 基类
class Animal {
public:
virtual void sound() { // 使用虚函数实现多态
cout << "Animal sound" << endl;
}
};
// 派生类
class Dog : public Animal {
public:
void sound() override { // 重写基类的方法
cout << "Bark" << endl;
}
};
// 返回类型为父类指针的函数
Animal* getAnimal() {
return new Dog(); // 返回子类的实例
}
int main() {
Animal* animal = getAnimal(); // 获取子类对象
animal->sound(); // 输出: Bark
delete animal; // 释放动态分配的内存
return 0;
}
解释
-
基类和派生类:
-
Animal
是基类,其中定义了一个虚函数sound()
。 -
Dog
是从Animal
派生出的子类,重写了sound()
方法。
-
-
返回类型为父类指针:
-
getAnimal()
函数的返回类型是Animal*
,它返回的是一个Dog
的实例。 -
这里的关键点是,尽管返回类型是
Animal*
,但实际返回的是Dog
类的对象。
-
-
多态性:
-
在
main()
函数中,通过父类指针animal
调用sound()
方法时,实际执行的是Dog
类中的sound()
方法,这展示了运行时多态性。 -
这使得程序可以在不修改代码的情况下扩展新的子类,只需实现适当的函数重写即可。
-
小结
因此,在 C++ 中,函数返回类型为父类是可以返回子类的对象的,这种特性支持多态性,使得代码更灵活且易于扩展。
4.2.7继承和多态中,函数返回类型是父类的,怎么样才可以返回子类的类型
在 C++ 中,如果一个函数的返回类型是父类,你可以通过返回一个子类的实例来实现多态性。为了成功返回子类的类型,你需要确保以下几点:
1. 使用指针或引用
确保函数返回的是父类的指针或引用,以便能够指向任何子类的对象。这是实现多态性的关键。
2. 使用虚函数
在基类中使用 virtual
关键字来声明函数,以便在子类中重写该函数。这可以确保调用该函数时执行的是子类的实现,而不是基类的实现。
示例代码
下面是一个示例,展示如何在 C++ 中实现这一点:
#include <iostream>
using namespace std;
// 基类
class Animal {
public:
virtual void sound() { // 声明虚函数
cout << "Animal sound" << endl;
}
virtual ~Animal() {} // 虚析构函数
};
// 派生类
class Dog : public Animal {
public:
void sound() override { // 重写基类的虚函数
cout << "Bark" << endl;
}
};
// 另一个派生类
class Cat : public Animal {
public:
void sound() override { // 重写基类的虚函数
cout << "Meow" << endl;
}
};
// 返回类型为父类指针的函数
Animal* getAnimal(const string& type) {
if (type == "dog") {
return new Dog(); // 返回子类 Dog 的实例
} else if (type == "cat") {
return new Cat(); // 返回子类 Cat 的实例
}
return nullptr; // 如果没有匹配类型,返回 nullptr
}
int main() {
Animal* animal1 = getAnimal("dog"); // 获取 Dog 对象
animal1->sound(); // 输出: Bark
delete animal1; // 释放内存
Animal* animal2 = getAnimal("cat"); // 获取 Cat 对象
animal2->sound(); // 输出: Meow
delete animal2; // 释放内存
return 0;
}
解释
-
基类和派生类:
-
Animal
是基类,其中定义了一个虚函数sound()
。 -
Dog
和Cat
是从Animal
派生的类,它们重写了sound()
方法。
-
-
返回父类指针:
-
getAnimal(const string& type)
函数根据传入的参数返回不同子类的对象。返回类型是Animal*
,这样可以指向Dog
或Cat
的实例。
-
-
多态性:
-
在
main()
函数中,通过父类指针animal1
和animal2
调用sound()
方法,实际执行的是子类Dog
和Cat
中的sound()
方法。
-
-
内存管理:
-
使用
new
动态分配内存时,确保在使用完对象后调用delete
来释放内存,以避免内存泄漏。
-
小结
要返回子类的类型,确保函数返回的是父类的指针或引用,并在子类中重写虚函数。这样可以利用多态性,实现灵活的对象管理和动态行为。
4.2.8 stat(文件存不存在)
在 C++ 中,stat
函数用于获取文件的状态信息,例如文件的大小、类型、权限和时间戳等。它通常用于 POSIX 兼容的系统,如 Linux 和 macOS,包含在 <sys/stat.h>
头文件中。
stat
函数的原型
int stat(const char *path, struct stat *buf);
-
**
path
**:要查询状态的文件的路径。 -
**
buf
**:指向stat
结构的指针,用于存放文件状态信息。
如果成功,stat
返回 0;如果失败,返回 -1 并设置 errno
。
struct stat
结构
struct stat
结构包含有关文件的各种信息,定义如下:
struct stat {
dev_t st_dev; // 文件所在设备的 ID
ino_t st_ino; // 文件的 inode number
mode_t st_mode; // 文件类型和权限
nlink_t st_nlink; // 硬链接数量
uid_t st_uid; // 文件所有者的用户 ID
gid_t st_gid; // 文件所有者的组 ID
dev_t st_rdev; // 特殊文件的设备 ID
off_t st_size; // 文件大小(字节数)
time_t st_atime; // 最近访问时间
time_t st_mtime; // 最近修改时间
time_t st_ctime; // 最近状态改变时间
};
示例代码
以下是一个简单的示例,展示如何使用 stat
函数获取文件状态信息:
#include <iostream>
#include <sys/stat.h>
#include <unistd.h>
#include <ctime>
int main() {
const char *filePath = "example.txt"; // 要检查的文件路径
struct stat fileInfo; // 用于存储文件状态信息
// 获取文件状态
if (stat(filePath, &fileInfo) == -1) {
std::cerr << "Error getting file status." << std::endl;
return 1;
}
// 输出文件信息
std::cout << "File Size: " << fileInfo.st_size << " bytes" << std::endl;
std::cout << "File Permissions: " << std::oct << (fileInfo.st_mode & 0777) << std::dec << std::endl;
std::cout << "Last Modified: " << ctime(&fileInfo.st_mtime); // 打印最后修改时间
std::cout << "Last Accessed: " << ctime(&fileInfo.st_atime); // 打印最后访问时间
// 判断文件类型
if (S_ISREG(fileInfo.st_mode)) {
std::cout << "File Type: Regular file" << std::endl;
} else if (S_ISDIR(fileInfo.st_mode)) {
std::cout << "File Type: Directory" << std::endl;
} else if (S_ISCHR(fileInfo.st_mode)) {
std::cout << "File Type: Character device" << std::endl;
} else if (S_ISBLK(fileInfo.st_mode)) {
std::cout << "File Type: Block device" << std::endl;
} else if (S_ISFIFO(fileInfo.st_mode)) {
std::cout << "File Type: FIFO" << std::endl;
} else if (S_ISLNK(fileInfo.st_mode)) {
std::cout << "File Type: Symbolic link" << std::endl;
} else if (S_ISSOCK(fileInfo.st_mode)) {
std::cout << "File Type: Socket" << std::endl;
}
return 0;
}
解释
-
包含头文件:引入
sys/stat.h
以使用stat
和struct stat
,并引入其他相关头文件。 -
定义文件路径:指定要获取信息的文件的路径。
-
调用
stat
函数:获取文件的状态信息并存储在fileInfo
中。若失败,输出错误信息并返回。 -
输出文件信息:
-
打印文件大小(字节数)。
-
打印文件权限(以八进制格式)。
-
打印最后修改时间和最后访问时间(使用
ctime
转换为可读格式)。
-
-
判断文件类型:使用
S_IS*
宏判断文件类型,输出相应的信息。
注意事项
-
路径有效性:确保提供的文件路径是有效的,否则
stat
会返回错误。 -
文件权限:文件的权限可以通过
st_mode
字段获取,使用按位与运算符可以提取特定权限。 -
类型判断:可以使用
S_ISREG
,S_ISDIR
,S_ISCHR
,S_ISBLK
,S_ISFIFO
,S_ISLNK
,S_ISSOCK
等宏来判断文件类型。
总结
stat
函数是一个强大的工具,用于获取文件的各种状态信息,适用于文件管理和分析。在开发中,理解和使用 stat
函数是非常重要的。
4.2.9 文件路径查找
size_t pos = pathname.find_last_of("/\\");
的作用是查找字符串 pathname
中最后一个出现的 /
或 \
字符的位置,并将该位置的索引赋值给 pos
。
例子:
4.3.0 c++线程
在 C++ 中,std::this_thread
和 std::thread
是与线程相关的两个重要组件,分别提供线程操作和线程管理的功能。以下是对它们及其接口的详细介绍。
1. std::thread
std::thread
是 C++11 中引入的类,用于创建和管理线程。
1.1. 构造函数
-
**
std::thread::thread()
**:默认构造函数,创建一个未关联的线程。 -
**
template<class Function, class... Args> explicit thread(Function&& f, Args&&... args)
**:创建一个新线程,执行函数f
,并将参数args
传递给它。
1.2. 成员函数
-
**
void join()
**:等待线程完成执行,阻塞当前线程,直到被调用的线程执行结束。 -
**
void detach()
**:分离线程,使其在后台独立运行,不再与主线程关联。 -
**
bool joinable() const
**:检查线程是否可加入。如果线程已结束或被分离,返回false
。 -
**
std::thread::~thread()
**:析构函数。若线程仍在运行且未分离,会调用std::terminate()
。
示例
#include <iostream>
#include <thread>
void threadFunction(int id) {
std::cout << "Thread " << id << " is running." << std::endl;
}
int main() {
std::thread t1(threadFunction, 1); // 创建线程 t1
std::thread t2(threadFunction, 2); // 创建线程 t2
t1.join(); // 等待线程 t1 完成
t2.join(); // 等待线程 t2 完成
return 0;
}
2. std::this_thread
std::this_thread
提供了与当前线程相关的操作,如获取线程 ID、休眠等。
2.1. 成员函数
-
**
static void sleep_for(const std::chrono::duration& rel_time)
**:使当前线程休眠指定的时间段。 -
**
static void sleep_until(const std::chrono::time_point<Clock, Duration>& abs_time)
**:使当前线程休眠直到指定的时间点。 -
**
static std::thread::id get_id()
**:获取当前线程的 ID。 -
**
static bool yield()
**:提示调度程序当前线程希望让出 CPU 控制权。
示例
#include <iostream>
#include <thread>
#include <chrono>
void threadFunction(int id) {
std::cout << "Thread " << id << " is running." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1)); // 休眠 1 秒
}
int main() {
std::thread t(threadFunction, 1);
std::cout << "Current thread ID: " << std::this_thread::get_id() << std::endl;
t.join(); // 等待线程完成
return 0;
}
3. 总结
-
std::thread
是用于创建和管理线程的类,提供了创建、等待和分离线程的功能。 -
std::this_thread
提供了与当前线程相关的操作,如休眠和获取线程 ID。 -
使用
std::thread
和std::this_thread
,可以有效地实现多线程编程,确保线程的创建、管理和同步。
4. 常用功能示例
使用 sleep_for
#include <iostream>
#include <thread>
void threadFunction() {
std::cout << "Thread is going to sleep." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2)); // 休眠 2 秒
std::cout << "Thread has woken up." << std::endl;
}
int main() {
std::thread t(threadFunction);
t.join();
return 0;
}
使用 yield
#include <iostream>
#include <thread>
void threadFunction(int id) {
std::cout << "Thread " << id << " is running." << std::endl;
std::this_thread::yield(); // 提示调度程序让出 CPU
}
int main() {
std::thread t1(threadFunction, 1);
std::thread t2(threadFunction, 2);
t1.join();
t2.join();
return 0;
}
通过 std::thread
和 std::this_thread
的组合,C++ 程序员可以灵活地实现多线程应用,充分利用多核处理器的优势。
4.3.1 using的用法
在 C++ 中,using
关键字不仅用于简化类型名称和引入命名空间,还可以在与回调函数结合使用时,提供更清晰的语法。回调函数是一种通过函数指针或函数对象传递的函数,以便在特定事件发生时被调用。以下是对 using
的用法以及如何实现回调函数的详细说明。
1. using
关键字的用法
在回调函数的上下文中,using
主要用于定义函数指针类型或类型别名,使得代码更简洁和易于理解。
示例:使用 using
定义函数指针类型
#include <iostream>
// 定义一个函数指针类型
using Callback = void(*)(int);
void callbackFunction(int value) {
std::cout << "Callback called with value: " << value << std::endl;
}
// 函数接受回调函数
void performAction(Callback cb, int value) {
cb(value); // 调用回调函数
}
int main() {
performAction(callbackFunction, 42); // 传递回调函数
return 0;
}
2. 使用函数对象(可调用对象)
除了使用函数指针,C++ 还支持使用函数对象(如 std::function
)作为回调函数。这种方式提供了更大的灵活性和可读性。
示例:使用 std::function
作为回调
#include <iostream>
#include <functional> // 引入 std::function
// 使用 std::function 定义回调类型
using Callback = std::function<void(int)>;
void performAction(Callback cb, int value) {
cb(value); // 调用回调函数
}
int main() {
// 使用 lambda 表达式作为回调函数
performAction([](int value) {
std::cout << "Lambda callback called with value: " << value << std::endl;
}, 100);
// 使用普通函数作为回调
performAction(callbackFunction, 200);
return 0;
}
3. 使用类中的回调
回调函数也可以是类中的成员函数,这时需要使用 std::bind
或 lambda 表达式来绑定对象。
示例:使用类成员函数作为回调
#include <iostream>
#include <functional>
class MyClass {
public:
void memberFunction(int value) {
std::cout << "Member function called with value: " << value << std::endl;
}
};
using Callback = std::function<void(int)>;
void performAction(Callback cb, int value) {
cb(value); // 调用回调函数
}
int main() {
MyClass obj;
// 使用 std::bind 绑定成员函数
Callback cb = std::bind(&MyClass::memberFunction, &obj, std::placeholders::_1);
performAction(cb, 300); // 传递绑定的回调函数
return 0;
}
总结
-
using
关键字 在 C++ 中可用于简化函数指针或可调用对象的类型定义,特别在回调函数的上下文中,可以使代码更加清晰易懂。 -
回调函数 可以通过函数指针、函数对象(如
std::function
)或 lambda 表达式实现。 -
C++ 的灵活性允许使用类成员函数作为回调,这时可以利用
std::bind
或 lambda 进行绑定。
4.3.2 ostream将数据写入
在 C++ 中,将 std::ostream
作为参数传递的主要目的是为了实现灵活的输出,允许函数将输出结果写入不同的输出流(如控制台、文件等)。以下是 std::ostream
作为参数的用法,包括函数定义、调用及示例。
1. 函数定义
可以将 std::ostream
的引用或指针作为函数参数,以便在函数内部进行输出操作。
示例:使用 std::ostream
引用作为参数
#include <iostream>
#include <string>
// 定义一个接受 ostream 引用的函数
void printMessage(std::ostream& os, const std::string& message) {
os << message << std::endl; // 将消息写入输出流
}
int main() {
printMessage(std::cout, "Hello, World!"); // 输出到控制台
std::ofstream outFile("output.txt"); // 创建文件输出流
if (outFile.is_open()) {
printMessage(outFile, "Writing to a file!"); // 输出到文件
outFile.close(); // 关闭文件流
} else {
std::cerr << "Unable to open file!" << std::endl; // 输出错误信息
}
return 0;
}
2. 通过 std::ostream
实现灵活的日志记录
通过将 std::ostream
作为参数,可以实现灵活的日志记录功能,支持不同的输出目标。
示例:日志记录函数
#include <iostream>
#include <fstream>
#include <string>
void logMessage(std::ostream& os, const std::string& level, const std::string& message) {
os << "[" << level << "] " << message << std::endl; // 输出日志
}
int main() {
logMessage(std::cout, "INFO", "Application started."); // 输出信息到控制台
std::ofstream logFile("log.txt"); // 创建文件输出流
if (logFile.is_open()) {
logMessage(logFile, "ERROR", "An error occurred!"); // 输出错误日志到文件
logFile.close(); // 关闭文件流
} else {
std::cerr << "Unable to open log file!" << std::endl; // 输出错误信息
}
return 0;
}
3. 使用 std::ostream
的指针
虽然使用引用更为常见,但也可以使用指针作为参数。
示例:使用 std::ostream
指针
#include <iostream>
#include <fstream>
void outputToStream(std::ostream* os, const std::string& message) {
if (os) { // 检查指针是否有效
*os << message << std::endl; // 将消息写入输出流
}
}
int main() {
outputToStream(&std::cout, "Hello from pointer!"); // 输出到控制台
std::ofstream outFile("output.txt");
if (outFile.is_open()) {
outputToStream(&outFile, "Writing to file via pointer!"); // 输出到文件
outFile.close();
} else {
std::cerr << "Unable to open file!" << std::endl;
}
return 0;
}
e.close(); } else { std::cerr << "Unable to open file!" << std::endl; } return 0; }
4. 总结
-
std::ostream
作为参数使得函数具有更高的灵活性,可以输出到不同的流(如控制台、文件等)。 -
可以使用引用或指针来传递
std::ostream
,通常推荐使用引用。 -
通过这种方式,可以实现通用的输出函数,简化代码和提高可重用性。
这种方法在需要动态选择输出目标(例如调试、日志记录等)时特别有用,能够使代码更加模块化和清晰。
4.3.3 using和智能指针一起控制类对象
在 C++ 中,结合 using
和智能指针可以有效地管理类对象的生命周期和内存。这种方法不仅简化了代码,还提高了安全性,避免了内存泄漏。以下是如何使用 using
和智能指针控制类对象的几种常见方式。
1. 使用 using
定义智能指针类型别名
通过 using
定义智能指针类型别名,可以使代码更加简洁,并且便于管理和使用。
示例:定义智能指针类型别名
#include <iostream>
#include <memory>
class MyClass {
public:
void display() const {
std::cout << "MyClass instance." << std::endl;
}
};
class Container {
public:
using MyClassPtr = std::unique_ptr<MyClass>; // 定义智能指针类型别名
void createInstance() {
instance = MyClassPtr(new MyClass()); // 使用类型别名创建对象
}
void showInstance() const {
if (instance) {
instance->display(); // 调用成员函数
} else {
std::cout << "Instance not created." << std::endl;
}
}
private:
MyClassPtr instance; // 使用智能指针作为成员变量
};
int main() {
Container container;
container.showInstance(); // 输出: Instance not created.
container.createInstance();
container.showInstance(); // 输出: MyClass instance.
return 0;
}
在需要多个对象共享同一个类实例时,可以使用 std::shared_ptr
结合 using
。
#include <iostream>
#include <memory>
class MyClass {
public:
MyClass(int id) : id(id) {}
void display() const {
std::cout << "MyClass instance with ID: " << id << std::endl;
}
private:
int id;
};
class Manager {
public:
using MyClassPtr = std::shared_ptr<MyClass>; // 定义智能指针类型别名
void createInstance(int id) {
instance = MyClassPtr(new MyClass(id)); // 创建实例并使用智能指针管理
}
void showInstance() const {
if (instance) {
instance->display(); // 调用成员函数
} else {
std::cout << "No instance created." << std::endl;
}
}
private:
MyClassPtr instance; // 使用智能指针作为成员变量
};
int main() {
Manager manager;
manager.showInstance(); // 输出: No instance created.
manager.createInstance(1);
manager.showInstance(); // 输出: MyClass instance with ID: 1
{
Manager anotherManager;
anotherManager.createInstance(2);
anotherManager.showInstance(); // 输出: MyClass instance with ID: 2
} // anotherManager 作用域结束,智能指针会自动释放内存
manager.showInstance(); // 输出: MyClass instance with ID: 1
return 0;
}
3. 结合 using
、智能指针与工厂模式
使用工厂模式结合 using
和智能指针可以使对象的创建和管理更加灵活。
示例:使用工厂模式和智能指针创建对象
#include <iostream>
#include <memory>
class MyClass {
public:
MyClass(int value) : value(value) {}
void display() const {
std::cout << "MyClass value: " << value << std::endl;
}
private:
int value;
};
class MyClassFactory {
public:
using MyClassPtr = std::unique_ptr<MyClass>; // 定义智能指针类型别名
static MyClassPtr create(int value) {
return MyClassPtr(new MyClass(value)); // 返回智能指针
}
};
int main() {
auto obj = MyClassFactory::create(42); // 使用工厂创建对象
obj->display(); // 输出: MyClass value: 42
return 0;
}
总结
-
使用
using
定义类型别名:可以简化智能指针的使用,增强代码的可读性。 -
选择合适的智能指针:根据需求选择
std::unique_ptr
或std::shared_ptr
,以便在对象的生命周期和共享上进行合理管理。 -
结合工厂模式:通过工厂方法创建和返回智能指针,能够有效地管理对象的生命周期,避免内存泄漏。
通过结合 using
和智能指针,您可以更灵活地控制类对象,同时提高代码的安全性和可读性。
4.3.4使用 strftime
格式化时间
以下示例演示如何将当前时间戳转换为 std::tm
结构,并使用 strftime
格式化为可读的时间字符串。
#include <iostream>
#include <ctime>
#include <iomanip>
int main() {
// 获取当前时间戳
std::time_t timestamp = std::time(nullptr); // 获取当前时间戳
// 将时间戳转换为 std::tm 结构
std::tm timeinfo;
#ifdef _WIN32
localtime_s(&timeinfo, ×tamp); // 在 Windows 上使用 localtime_s
#else
localtime_r(×tamp, &timeinfo); // 在 Linux/Unix 上使用 localtime_r
#endif
// 定义输出字符串的缓冲区
char buffer[100];
// 使用 strftime 格式化时间
strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &timeinfo);
// 输出格式化的当前时间
std::cout << "当前时间: " << buffer << std::endl;
return 0;
}
代码解析
-
获取当前时间戳:
-
使用
std::time(nullptr)
获取当前的 Unix 时间戳。
-
-
转换为
std::tm
结构:-
在 Windows 上,使用
localtime_s
进行线程安全的转换;在其他平台,使用localtime_r
。
-
-
定义输出缓冲区:
-
使用字符数组
buffer
存储格式化后的时间字符串。
-
-
格式化时间:
-
使用
strftime
将std::tm
结构格式化为字符串。格式字符串"%Y-%m-%d %H:%M:%S"
指定输出的格式。
-
-
输出结果:
-
使用
std::cout
输出格式化后的时间字符串。
-
常用格式化标志
在 strftime
中使用的格式化标志包括:
-
%Y
:四位年份 -
%m
:月份(01-12) -
%d
:日(01-31) -
%H
:小时(00-23) -
%M
:分钟(00-59) -
%S
:秒(00-59)
4.3.5 stringstream
在 C++ 中,std::stringstream
是一个非常有用的类,它允许你在内存中处理字符串输入和输出。通过 stringstream
,你可以方便地将不同类型的数据格式化为字符串,也可以从字符串中提取数据。这种方式类似于使用 std::cin
和 std::cout
,但是是在字符串上操作。
基本用法
以下是 std::stringstream
的一些常见用法示例,包括创建 stringstream
对象、写入数据和从中读取数据。
示例代码
#include <iostream>
#include <sstream> // 包含 stringstream 的头文件
#include <string>
int main() {
// 创建一个 stringstream 对象
std::stringstream ss;
// 向 stringstream 中写入数据
ss << "Hello, World! ";
ss << 42; // 写入整数
ss << " is the answer.";
// 获取 stringstream 中的字符串
std::string result = ss.str(); // 转换为字符串
std::cout << "写入后的字符串: " << result << std::endl;
// 清空 stringstream
ss.str(""); // 清空内容
ss.clear(); // 清空状态标志
// 向 stringstream 中写入新的数据
ss << "The value of pi is approximately: ";
ss << 3.14159;
// 从 stringstream 中读取数据
std::string piString;
double piValue;
ss >> piString >> piValue; // 读取字符串和浮点数
// 输出读取到的数据
std::cout << piString << " " << piValue << std::endl;
return 0;
}
代码解析
-
包含头文件:
-
#include <sstream>
用于引入std::stringstream
的定义。
-
-
创建
stringstream
对象:-
使用
std::stringstream ss;
创建一个stringstream
实例。
-
-
写入数据:
-
使用
<<
操作符将字符串和其他类型的数据写入stringstream
对象。
-
-
获取字符串:
-
使用
ss.str()
方法将stringstream
中的内容转换为std::string
。
-
-
**清空
stringstream
**:-
使用
ss.str("")
清空内容,使用ss.clear()
清空状态标志,以便重新使用。
-
-
读取数据:
-
使用
>>
操作符从stringstream
中读取数据。可以读取不同类型的数据(如字符串和浮点数)。
-
-
输出结果:
-
使用
std::cout
输出最终的结果。
-
常见应用场景
-
数据格式化:可以将不同类型的数据格式化为一个字符串,比如将数值、日期等转换为字符串形式。
-
解析字符串:可以将格式化的字符串解析为不同类型的数据,例如从字符串中提取整数、浮点数等。
-
构建字符串:通过串联多个数据片段来构建一个完整的字符串。
注意事项
-
使用
stringstream
时要注意清空状态和内容,以防止后续操作受到影响。 -
在处理输入输出时,
stringstream
提供了更大的灵活性,尤其是当需要处理复杂格式时。
4.3.6 终止一个程序的执行abort
在 C++ 中,如果你想要终止一个程序的执行,可以使用 abort()
函数,这个函数会引发一个异常并终止程序的运行。使用方式如下:
#include <cstdlib> // 包含 abort 函数
int main() {
// 某些条件下需要中止程序
if (/* 某些条件 */) {
abort(); // 终止程序
}
// 其他代码
return 0;
}
4.3.7 cout.write
用到了cout.write,从data位置开始,写入len长度的数据
std::cout.write 是 C++ 标准库中 iostream 类的一部分,它允许你将一个字符序列直接写入到输出流中。与使用插入运算符 << 不同,write 方法不会格式化数据,而是将原始字节序列输出到流中。
cout.write 方法介绍
write 方法是 std::ostream 类的一个成员函数,其原型如下:
basic_ostream<charT, traits>& write(const charT* s, streamsize n);
charT 是字符类型,对于 std::cout 来说,它是 char。
traits 是字符类型的特征类,通常不需要显式指定。
s 是指向要写入的字符序列的指针。
n 是要写入的字符数。
write 方法返回对调用它的 std::ostream 对象的引用,这使得你可以连续调用多个输出操作。
cout.write 方法使用
下面是如何使用 std::cout.write 方法的一些示例:
示例 1:写入一个 C 风格字符串的一部分
#include <iostream>
int main() {
const char* str = "Hello, World!";
std::cout.write(str, 5); // 写入 "Hello"
std::cout << std::endl; // 添加换行
return 0;
}
在这个例子中,我们只写入字符串的前5个字符。
示例 2:写入一个 std::string 对象
#include <iostream>
#include <string>
int main() {
std::string str = "Hello, World!";
std::cout.write(str.c_str(), str.size()); // 写入整个字符串
std::cout << std::endl; // 添加换行
return 0;
}
这里,我们使用 c_str() 方法获取 std::string 的 C 风格字符串,并写入整个字符串。
示例 3:连续写入
#include <iostream>
int main() {
const char* str1 = "Hello, ";
const char* str2 = "World!";
std::cout.write(str1, 7) << " "; // 写入 "Hello, " 然后添加一个空格
std::cout.write(str2, 6); // 写入 "World!"
std::cout << std::endl; // 添加换行
return 0;
}
在这个例子中,我们使用 write 方法的返回值来连续调用输出操作。
注意事项
write 方法不会在写入后添加任何额外的字符,比如换行符或字符串结束符 \0。
如果 n 指定的字符数超过了 s 指向的字符串的实际长度,write 方法将写入尽可能多的字符,但不会导致未定义行为。
如果 n 为 0,write 方法不会执行任何操作。
使用 write 方法时,需要确保不会超出缓冲区的界限,以避免未定义行为。
4.3.8 ofstream中的接口
std::ofstream
接口和成员函数的参数介绍
std::ofstream
是 C++ 中用于处理文件输出的类,提供了多种接口和成员函数。以下是主要接口、成员函数及其参数的详细介绍。
构造函数
-
std::ofstream()
-
参数:无
-
描述:创建一个
std::ofstream
对象,但不打开任何文件。
-
-
std::ofstream(const char* filename)
-
参数:
const char* filename
- 要打开的文件名的 C 风格字符串。 -
描述:创建并打开指定名称的文件。
-
-
std::ofstream(const std::string& filename)
-
参数:
const std::string& filename
- 要打开的文件名的字符串对象。 -
描述:创建并打开指定名称的文件。
-
成员函数
-
void open(const char* filename)
-
参数:
const char* filename
- 要打开的文件名。 -
描述:打开指定的文件。如果文件已存在,默认情况下会清空文件内容。
-
-
void open(const std::string& filename)
-
参数:
const std::string& filename
- 要打开的文件名。 -
描述:打开指定的文件,功能与上面的重载相同。
-
-
bool is_open() const
-
参数:无
-
描述:检查流是否成功打开,返回
true
表示成功,false
表示失败。
-
-
std::ofstream& operator<<(const T& value)
-
参数:
const T& value
- 要写入的数据,可以是任意类型(如整数、浮点数、字符串等)。 -
描述:重载插入运算符,用于将数据写入文件。
-
-
void write(const char* s, std::streamsize n)
-
参数:
-
const char* s
- 指向要写入的字符数组的指针。 -
std::streamsize n
- 要写入的字符数量。
-
-
描述:将指定数量的字符写入文件。
-
-
void put(char c)
-
参数:
char c
- 要写入的单个字符。 -
描述:写入一个字符到文件。
-
-
void close()
-
参数:无
-
描述:关闭打开的文件,释放相关资源。
-
-
void flush()
-
参数:无
-
描述:刷新输出缓冲区,确保所有数据都写入文件。
-
-
状态检查:
-
**
bool good() const
**:检查流的状态是否良好。 -
**
bool eof() const
**:检查是否到达文件末尾。 -
**
bool fail() const
**:检查流是否处于失败状态。 -
**
bool bad() const
**:检查流是否处于错误状态。
-
open()
的详细用法
open()
成员函数用于打开一个文件。可以使用两种重载形式:
-
void open(const char* filename)
-
参数:
const char* filename
- 要打开的文件名。 -
使用示例:
cpp
std::ofstream outFile; outFile.open("example.txt");
-
-
void open(const std::string& filename)
-
参数:
const std::string& filename
- 要打开的文件名。 -
使用示例:
cpp
std::ofstream outFile; outFile.open(std::string("example.txt"));
-
在调用 open()
之后,可以使用 is_open()
检查文件是否成功打开。打开文件时,可以同时指定打开模式,如下所示:
打开模式总结
打开模式用于指定打开文件时的行为,可以使用逻辑或(|
)运算符组合多种模式。以下是常用的打开模式:
-
std::ios::out
-
描述:以输出模式打开文件(默认模式)。
-
效果:如果文件已存在,内容会被清空(除非使用
std::ios::app
)。
-
-
std::ios::app
-
描述:以追加模式打开文件。
-
效果:写入的数据会被添加到文件末尾,原有内容不被覆盖。
-
-
std::ios::trunc
-
描述:以截断模式打开文件。
-
效果:如果文件已经存在,内容会被清空。
-
-
std::ios::binary
-
描述:以二进制模式打开文件。
-
效果:适用于处理二进制数据,避免文本模式下的换行符转换等。
-
示例代码
以下示例展示了如何使用 std::ofstream
和 open()
方法:
#include <iostream>
#include <fstream>
int main() {
// 创建 ofstream 对象
std::ofstream outFile;
// 打开文件并指定打开模式
outFile.open("example.txt", std::ios::out | std::ios::app); // 以追加模式打开文件
// 检查文件是否成功打开
if (!outFile.is_open()) {
std::cerr << "无法打开文件!" << std::endl;
return 1; // 返回错误代码
}
// 写入数据
outFile << "Hello, World!" << std::endl; // 使用 << 运算符写入
outFile.write("This is a binary data test.", 30); // 使用 write() 写入字符数组
// 关闭文件
outFile.close();
return 0;
}
总结
std::ofstream
提供了多种接口和成员函数,使得文件写入操作简单易用。open()
方法允许通过指定文件名和打开模式来打开文件,确保满足不同的需求。- 各种打开模式(如
std::ios::app
和std::ios::trunc
等)使用户能够灵活控制文件的读写行为。
4.3.9 stringstream
std::stringstream
是 C++ 标准库中用于字符串输入输出的流类,属于 sstream
头文件。它允许程序员以流的方式读写字符串,使得字符串的处理更加灵活和方便。下面是 std::stringstream
的接口介绍以及相关总结。
1. 接口介绍
1.1 构造函数
-
std::stringstream()
:默认构造函数,创建一个空的stringstream
对象。 -
std::stringstream(const std::string& str)
:使用给定字符串初始化stringstream
。 -
std::stringstream(std::ios_base::openmode mode)
:以指定模式打开流。
1.2 成员函数
-
写入数据
-
template<typename T> std::stringstream& operator<<(const T& val)
:将数据写入字符串流。
-
-
读取数据
-
template<typename T> std::stringstream& operator>>(T& val)
:从字符串流中读取数据。
-
-
获取和设置内容
-
std::string str() const
:获取当前字符串流的内容。 -
void str(const std::string& s)
:设置字符串流的内容为给定字符串。
-
-
流状态管理
-
void clear()
:清除流的状态标志。 -
bool eof() const
:检查流是否到达文件末尾。 -
bool fail() const
:检查流是否出现错误。 -
bool good() const
:检查流的状态是否良好。 -
bool bad() const
:检查流是否处于坏状态。
-
-
格式设置
-
std::streamsize precision(std::streamsize n)
:设置或获取浮点数的精度。 -
std::ios_base& setf(std::ios_base::fmtflags flags)
:设置格式标志。
-
1.3 其他功能
-
清空内容
-
void clear()
:清除流的状态,但不清空内容。 -
void str(const std::string& s)
:设置流的内容,可以用来清空内容。
-
2. 示例代码
以下是使用 std::stringstream
的一些基本操作示例:
#include <iostream>
#include <sstream>
#include <string>
int main() {
// 创建 stringstream 对象
std::stringstream ss;
// 写入数据
ss << "Hello, " << "world! " << 42;
// 获取当前字符串内容
std::string result = ss.str();
std::cout << "String content: " << result << std::endl;
// 清空 stringstream 内容
ss.str(""); // 清空内容,流的状态保持不变
// 从字符串中读取数据
ss.str("123 456 789");
int a, b, c;
ss >> a >> b >> c; // 读取整数
std::cout << "Read values: " << a << ", " << b << ", " << c << std::endl;
return 0;
}
3. 总结
-
功能:
std::stringstream
提供了将不同数据类型与字符串之间转换的能力,方便进行格式化输出和解析输入。 -
灵活性:可以在流中读写多种数据类型,适合处理复杂字符串操作。
-
性能:由于使用内存中的字符串而非磁盘 I/O,性能较好,尤其适用于需要频繁字符串操作的场景。
-
易用性:接口简单易用,代码可读性高,适合调试和记录日志信息。
4. 使用场景
-
格式化输出:将多个变量格式化为字符串进行输出。
-
解析输入:从字符串中提取数据,特别是在处理用户输入或文件读取时。
-
调试:在调试过程中,可以将复杂的状态信息写入
stringstream
,方便查看。
通过 std::stringstream
,开发者可以更灵活地处理字符串数据,提升代码的清晰度和维护性。
4.4.0 enum clss
在C++中,enum class
是一种强类型枚举,提供了更好的类型安全性和命名空间管理。与传统的枚举不同,enum class
不会隐式转换为整数类型,这有助于防止错误并增强代码的可读性。
定义和使用 enum class
以下是enum class
的基本定义和使用示例:
#include <iostream>
// 定义一个强类型枚举
enum class Color {
RED,
GREEN,
BLUE
};
enum class Direction {
NORTH,
SOUTH,
EAST,
WEST
};
int main() {
// 使用强类型枚举
Color myColor = Color::RED;
Direction myDirection = Direction::NORTH;
// 打印枚举值的整数值
std::cout << "Color value: " << static_cast<int>(myColor) << std::endl; // 输出:Color value: 0
std::cout << "Direction value: " << static_cast<int>(myDirection) << std::endl; // 输出:Direction value: 0
return 0;
}
enum class
的特点
-
类型安全:
-
enum class
不允许隐式转换为整数,必须使用static_cast
进行显式转换。 -
不同的
enum class
类型之间的值不能直接比较。
-
-
命名空间:
-
enum class
的枚举值具有作用域,因此可以使用相同的名称定义不同的枚举。例如,可以同时有Color::RED
和Direction::RED
。
-
-
增强的可读性:
-
enum class
提高了代码的可读性,因为枚举值使用类名作为前缀,清楚地表明了它们的来源。
-
示例
下面是一个完整的示例,演示了如何定义和使用 enum class
:
#include <iostream>
enum class Color {
RED,
GREEN,
BLUE
};
enum class Fruit {
APPLE,
ORANGE,
BANANA
};
void printColor(Color color) {
switch (color) {
case Color::RED:
std::cout << "Color is Red" << std::endl;
break;
case Color::GREEN:
std::cout << "Color is Green" << std::endl;
break;
case Color::BLUE:
std::cout << "Color is Blue" << std::endl;
break;
}
}
int main() {
Color myColor = Color::GREEN;
printColor(myColor);
// Error: cannot implicitly convert enum class to int
// int value = myColor; // This will cause a compilation error
return 0;
}
在这个示例中,我们定义了两个强类型枚举 Color
和 Fruit
,并使用 switch
语句来打印颜色。尝试将 myColor
直接赋值给整数将导致编译错误,这体现了 enum class
的类型安全性。
4.4.1 atomic
原子操作的库
在C++中,atomic
是一个提供原子操作的库,主要用于多线程编程。原子操作是不可分割的操作,即在执行过程中不会被其他线程中断。这有助于避免数据竞争和确保数据的一致性。
1. 头文件和基本概念
要使用 atomic
,需要包含头文件 <atomic>
。原子类型提供了一种高效的方式来在多线程环境中共享数据。
2. 原子类型
C++标准库提供了一些原子类型,如 std::atomic
和 std::atomic<T>
,其中 T
是基本数据类型,如 int
、bool
等。常用的原子类型包括:
-
std::atomic<int>
-
std::atomic<bool>