1. 基础规则
1.1 main函数
入门c++代码如下,main函数是一个程序的入口,每个程序都必须有这么一个函数,并且有且只有一个。
#include<iostream>
using namespace std;
int main() {
std::cout << "hello C++" << endl;
std::cin.get();//pause
//system("pause"); 不同系统可能用不了
return 0;
}1.2 编译过程

- 预处理 预编译把一些
#define的宏定义完成文本替换,然后将#include的文件里的内容复制到.cpp文件里,如果.h文件里还有.h文件,就递归展开。在预处理这一步,代码注释直接被忽略,不会进入到后续的处理中,所以注释在程序中不会执行。生成文件
main.i - 编译
编译把我们写的代码转为汇编代码,它的工作是检查词法和语法规则,所以,如果程序没有词法或则语法错误,那么不管逻辑是怎样错误的,都不会报错。编译不是指程序从源文件到二进制程序的全部过程,而是指将经过预处理之后的程序转换成特定汇编代码(assembly
code)的过程。生成文件
main.s - 汇编 汇编过程将上一步的汇编代码(main.s)转换成机器 (machine
code),这一步产生的文件叫做目标文件
main o,是二进制格式。生成最终的可执行文件(Executable file)。 - 链接 链接(Link)其实就是一个“打包”的过程,汇编只会对单个cpp文件进行分开处理,还需要标准库、动态链接库和其他cpp文件等结合起来,生成最终的可执行文件(Executable file)。
1.3 基本变量
1.3.1变量类型
| 类型 | 含义 | 最小尺寸 |
|---|---|---|
bool |
布尔类型 | 8bits |
char |
字符 | 8bits |
wchar_t |
宽字符 | 16bits |
char16_t |
Unicode字符 | 16bits |
char32_t |
Unicode字符 | 32bits |
short |
短整型 | 16bits |
int |
整型 | 16bits (在32位机器中是32bits) |
long |
长整型 | 32bits |
long long |
长整型 | 64bits (是在C++11中新定义的) |
float |
单精度浮点数 | 4bits,6位有效数字 |
double |
双精度浮点数 | 8bits,15位有效数字 |
long double |
扩展精度浮点数 | 8bits,15位有效数字 |
1.3.2 三种初始化方法
int a = 2;
int c (222);
int b {1};
// int a1 = 2.1; //不会报错,涉及类型转换
// int b1{2.1}; //会报错,建议用着这种
char character1 {'a'};
char value = 65;
fmt::println("value(int): {}", static_cast<unsigned int>(value));1.4 枚举(enum)
枚举类型(enumeration):是C++中的一种派生数据类型,它是由用户定义的若干枚举常量的集合。
Enum 枚举类型名 {变量值列表};
enum Weekday{ SUM,MON,TUE,WED,THU,FRSAT }; //定义枚举类型
//SUM = 0; //SUM是枚举类型,此语句非法
Weekday day1=WED, day2= SAT;
if (day1 > day2)
cout << day1;
else
cout << day2;
cin.get();
- 枚举元素具有默认值,依次为:0,1,2,3。。。
- 声明时可以另行定义枚举元素的值
Enum Weekday {SUM=7,MON=1,TUE,WED,THU,FRI,SAT}; //后面从TUE依次为23456- 枚举值可以进行关系运算
- 整数值不能直接赋值给枚举变量,如需将整数值给枚举类型,需要进行强制转换
1.5 结构体(struct)
结构体,通俗讲就像是打包封装,把一些变量和函数封装在内部,通过一定方法访问修改内部变量。与class在技术上的区别是struct默认public,class默认private
#include <iostream>
#include <string>
//自定义数据类型,一些类型的集合组成一个类型
//语法 struct 类型名称 { 成员列表 }
struct Student
{
//成员列表
std::string name;
int age;
int score;
char other[20];
void Log1(){cout <<score;}
};
int main()
{
Student s1;
Student s2 = { "李四",19,80 };
//给s1属性赋值,通过点.访问结构体变量中的属性
s1.age = 18;
s1.score = 100;
cout << "age1:" << s1.age <<endl;
cout << "score1:" << s2.score <<endl;
std::cin.get();//pause
return 0;
}1.6 const
constexpr 可以在编译期间定义常量
constexpr int age{20};
constexpr int age1{age+10};
//此时可以使用static_assert在编译期间做检查
static_assert(age==20)constexpr只能使用字面量或者constexpr的变量进行赋值
如果是常规的变量,则会报错
// 此时函数可以在编译时使用,也可以在运行时使用
constexpr int add(int a,int b){
return a+b;
}
int main()
{
constexpr int value1{22};
constexpr int value2{33};
auto result{add(value1,value2)}; // Compile time
int factor{2}; // Run time variable
auto result2{add(value1, factor)}; // Run time
return 0;
}consteval是严格版本的constexpr,如果相关计算不能在编译期间完成,会直接报错,而不是移动到runtime执行
constinit的作用在于显式地指定变量的初始化方式为静态初始化,在编译时初始化,但可以修改值
constinit int a{8};
a=12;
const constinit int a1{8}; //可以额外指定变量const1.7 控制语句
1.7.1 for
有两种方法
//常规
for(i ; i < 10 ; ++i){
fmt::println( " {}: I love C++", i );
}// 迭代器模式
std::vector<int> values{ 1, 2, 3, 4, 5 };
for (auto value : values) {
fmt::println(" value : {}", value);
}1.8 位运算
查看各个进制的表示
unsigned short int data {15};
fmt::println("data (dec): {0:d}", data); // Decimal
fmt::println("data (oct): {0:o}", data); // Octal
fmt::println("data (hex): {0:x}", data); // Hexadecimal
fmt::println("data (bin): {:016b}", data); // Binary (16 digits)Shift运算符
unsigned short int value {0xff0u};
value = static_cast<unsigned short int> (value << 1);
fmt::println("binary value: {:016b}, decimal value: {}", value, value);
value = static_cast<unsigned short int> (value << 4);
fmt::println("binary value: {:016b}, decimal value: {}", value, value);
value = static_cast<unsigned short int> (value >> 1);
fmt::println("binary value: {:016b}, decimal value: {}", value, value);逻辑运算
unsigned int value1 {0x3}; // 0000 0011
unsigned int value2 {0x5}; // 0000 0101
fmt::println("Bitwise and:");
fmt::println("value1 & value2: {:032b}", (value1 & value2));
fmt::println("Bitwise or:");
fmt::println("value1 | value2: {:032b}", (value1 | value2));
fmt::println("Bitwise NOT :");
fmt::println("~value1: {:032b}", ~value1);
fmt::println("~value2: {:032b}", ~value2);
fmt::println("Bitwise xor:");
fmt::println("value1 ^ value2: {:032b}", (value1 ^ value2));当作mask使用。从RGBA提取单通道的值
unsigned int red_mask{ 0xFF000000 };
unsigned int green_mask{ 0x00FF0000 };
unsigned int blue_mask{ 0x0000FF00 };
unsigned int alpha_mask{ 0x000000FF };
unsigned int color {0xAABCDE00};
fmt::println("Red is: {0:d}", ((my_color & red_mask) >> 24));
fmt::println("Green is: {0:d}", ((my_color & green_mask) >> 16));
fmt::println("Blue is: {0:d}", ((my_color & blue_mask) >> 8));
fmt::println("Alpha is: {0:d}", ((my_color & alpha_mask) >> 0));1.9 字符串
1.9.1 字符数组
这种方式不常用不建议使用。数组的定义方式是
char name[10]={'s','t','r','i','n','g','\0'};//需要手动加上'\0'
char a[]="hello world"; 这种情况非常适合使用cctype标准库
该库主要是用来判断单个字符是否是数字、字母或其他字符,可以用来判断是否是大小写并转换‘’
比如:
int isalnum(int c);
int isalpha(int c);
int isblank(int c);
int islower(int c);
int isupper(int c);
int tolower(int c);
int toupper(int c);1.9.2 c_string
这种是比较常见,写法是用指针来定义
const char* message2 {"The sky is blue."};
const char message1 [] {"The sky is blue."};本质是用一个指针来存储字面量的地址,由于是字面量,所以必须使用const来申明
此时可以借助cstring标准库,该库提供了实用的字符串操作工具,比如:
char* strcat(char* s1, const char* s2);
int strcmp(const char* s1, const char* s2);
size_t strlen(const char* s);
char* strchr(char* s, int c); //查找字符的第一次出现
char* strstr(char* s1, const char* s2); //查找字符子字符串的第一次出现1.9.2.1 字面量(literal)
字面量是指在源代码中直接写出的固定值,字符串字面量通常存储在程序的只读内存区域中
原始字符串字面量Raw String Literals是 C++11
引入的一种新的字符串表示方式,它允许你在字符串中直接使用不需要转义的字符(如反斜杠、换行符等),使得处理一些包含特殊字符的字符串更加方便。
原始字符串字面量的语法为:
R"delimiter(string_contents)delimiter"delimiter:是可选的标识符,用于确定字符串的开始和结束位置。可以是空字符串,也可以是自定义的字符串,目的是避免与字符串内容中的括号冲突。
string_contents:表示字符串的内容,可以包含换行符、引号、反斜杠等,而不需要转义。
例子:
const char* str = R"(This is a raw string literal)";
const char* path = R"(C:\Program Files\MyApp)"; // 路径中无需转义反斜杠
const char* json = R"({
"name": "John Doe",
"age": 30,
"city": "New York"
})"; // JSON 字符串字面量1.9.3 std_string
标准库string提供了一种更好的字符串容器,并且附带数据转换的接口,这个容器可以使用c_string和字符数组来初始化自己
数据初始化:
std::string planet{ "Earth. Where the sky is blue" };
std::string prefered_planet{ planet };
std::string result{"planet",1,3}; //切片
std::string result1(4, 'e'); //长度为4填充'e'的字符串字符串赋值
// 和构造函数类似,但这是赋值过程
string& assign(const char *s)
string& assign(const char *s, int n,int m)
string& assign(const string &s)
string& assign(int n, char c)数据转换:
std::to_string;
std::stoi;
std::stof;字符串拼接
//字符串拼接
string a = string("asd")+"asd";
using namespace std::string_literals;
// `"S` 是来自上一行的命名空间的语法,正式名称叫‘用户自定义字面量(User-Defined Literals, UDL)’
//C++11引入
string b = "asd"s+"asd";
string c = a+"123"+b //可以直接用
std::string str11 {"Hello "};
std::string str12 {str11.append(5,'?')}; // Appends 5 '?' characters
std::string str13{str12.append(str11,2,3)};查找和替换
string a="hello world";
//int find(const char* s, int pos=0, int n=0)
//从pos开始查看指定字符子串,如果n>0,就存在查找范围。
int f1 = a.find('ll');
int l1 = a.rfinf('o');
//查到字符
//find_first_of
//find_first_not_of
//find_last_of
//find_last_not_of
string& replace(int pos, int n, const string& str); //替换从pos开始n个字符为字符串str
string& replace(int pos, int n,const char* s); //替换从pos开始的n个字符为字符串sstd::basic_string - cppreference.cn - C++参考手册
//字符子串
//starts_with
//end_with
//compare
//contains
//substr
//push_back
//pop_back
//erase
//clear如果用这个容器,可以使用全部stl算法
1.9.4 string_view
来自c++17,
提供一种只用来看,不能编辑的字符串类型。
特点:不会发生复制,只是用指针指向要看的地址
const char * c_string1 { "The animals have left the region" };
std::string_view sv10{c_string1};
// 不看前两个字符
sv14.remove_prefix(2);
// 不看最后两个字符
sv14.remove_suffix(2);可以通过提供的接口完成基本的字符串处理,比如
find;
contains;
starts_with;
find_first_of;
find_last_of;
compare;1.10 函数
1.10.1 入口函数
通常程序入口是带参数的
void process_arguments(int argc, char* argv[]){
fmt::println("number of arguments: {}", argc);
for(size_t i{0}; i < argc; ++i){
fmt::println("arg [{}]: {}", i, argv[i]);
}
}
int main(int argc, char* argv[]){
process_arguments(argc, argv);
}这个argv可以继续传递给下一个函数
1.10.2 lambda
lambda的本质是一个普通函数,但不像普通函数一样做声明。它是我们的代码在过程中生成的,用完即弃的函数。
#include<iostream>
#include<vector>
void ForEach(const std::vector<int>& values, void(*func)(int))
{
for (int value : values)
func(value);
}
int main()
{
std::vector<int> values = { 1,5,4,2,3 };
ForEach(values, [](int value) {std::cout << " Value:" << value << std::endl;});
std::cin.get();
}
这里的[]叫做捕获方式,lambda可以把上下文变量以值或引用的方式捕获,在body中直接使用。int value是我们的参数,后面就和PrintValue函数体一样了。 [] 什么也不捕获; [=] 按值的方式捕获所有变量 ; [&] 按引用的方式捕获所有变量; [=, &a] 除了变量a之外,按值的方式捕获所有局部变量,变量a 使用引用的方式来捕获。这里可以按引用捕获多个,例如 [=, & a, &b,&c]。这里注意,如果前面加了=,后面加的具体的参数必 须以引用的方式来捕获,否则会报错; [&, a] 除了变量a之外,按引用的方式捕获所有局部变量,变量 a使用值的方式来捕获。这里后面的参数也可以多个,例如 [&, a, b, c]。这里注意,如果前面加了&,后面加的具体的参数必 须以值的方式来捕获; [a, &b] 以值的方式捕获a,引用的方式捕获b,也可以捕获多个; [this] 在成员函数中,也可以直接捕获this指针,其实在成员 函数中,[=]和[&]也会捕获this指针。
int x = 1; int y = 2;
auto plus = [=] (int a, int b) -> int { return x + y + a + b; };
int c = plus(1, 2);
//把x,y按值捕获可以把lambda赋值给一个auto类型变量,然后将lambda变量传入函数。
int main()
{
std::vector<int> values = { 1,5,4,2,3 };
auto lambda = [](int value) {std::cout << " Value:" << value << std::endl;};
ForEach(values, lambda);
std::cin.get();
}当我们试图传入某些变量时,不管是通过值还是引用来捕获变量,这里的ForEach都会出错,因为我们正在使用原始函数指针。若转变成std::function,返回void,有一个int参数叫做func就可以了。
我们有一个可选的修饰符mutable,它允许函数体修改通过拷贝传递捕获的参数。若我们在lambda中给a赋值会报错,需要写上mutable。

还可以写一个lambda接受vector的整数元素,遍历这个vector找到比3大的整数,然后返回它的迭代器,也就是满足条件的第一个元素。
#include<iostream>
#include<vector>
#include<algorithm>
int main()
{
std::vector<int> values = { 1,5,4,2,3 };
auto it = std::find_if(values.begin(), values.end(), [](int value) {return value > 3;});
std::cout << *it << std::endl;
std::cin.get();
}2. 进阶基础
2.1 指针
2.1.1介绍
指针是存储内存地址的变量,引用是指针的语法糖,两者会转为同一汇编代码
//声明
int *p1,*p2;//p1和p2是指向int对象的指针
double *dp1, *dp2;//dp1和dp2是指向double对象的指针
//定义
int a = 1;
int *p = &a;//P中存放变量a的地址,或者说p是指向a的指针
*p = 3;//pi的指向没有改变,但a的值发生了改变
int &p1=a; //创建引用p1,必须赋值
//引用初始化后不能被改变,指针可以改变所指的对象
//不存在指向空值的引用,但是存在指向空值的指针。
//空指针
int *p1 = nullptr;
int *p2 = 0;
int *p3 = NULL;//三种方法等价
范例
#include<iostream>
using namespace std;
int Log(int *num)
{
std::cout <<*num;
return 0;
}
int main() {
int num = 1;
int* p = #
int& p1 = num;
Log(p);
Log(&num);
Log(&p1);
std::cout <<std::endl;
num=num+1;
*p=*p+1;
p1=p1+1;
std::cout <<num;
std::cout <<*p;
std::cout <<p1;
std::cin.get();//pause
return 0;
}
//输出结果:
//111
//444对于数组的指针来说,直接打印就可以,但是对于char* 或者char[],则必须用static_cast做转换,更safe的做法是统一使用fmt::ptr
const char *a = "hello";
std::cout<<&a<<std::endl; //打印是a的地址而不是a存储的地址 const char**
std::cout << static_cast<const void*>(a) << std::endl;
char c[]{"hello"};
std::cout << static_cast<const void*>(c) << std::endl;
int b[]{1,2,3};
std::cout<<b<<std::endl;
std::cout<<fmt::ptr(a)<<std::endl;2.1.2 指针的用法和含义
指向常量的指针,不能修改值,
int a{1};
const int* b{&a} ;不能用变量指针指向常量
const int a{1};
int* b{&a}; //报错
const int* b1{&a} ;指针本身是常量
int a{1};
int* const b{&a} ;指针是常量,指向的也是常量
const int a{1};
const int* const b{&a} ;2.2 动态内存
2.2.1 new
一般声明的变量存放在栈上,new则会在堆上开辟空间。new出来的是一段空间的首地址,需要用指针来存放这段地址
int *p(nullptr);
p = new int; //在堆上开辟了一个4字节内存,并用p指向它
*p=22; //解引用
delete p; //释放内存回os
p = nullptr; //重置指针指向
// 如果初始化失败,会抛出异常2.2.2 new使程序崩溃的情况
- 未初始化
int * p_number;
fmt::println( "p_number: {}", fmt::ptr(p_number));
fmt::println( "*p_number: {}" , *p_number); //CRASH!- 访问被释放的指针
int * p_number1 {new int{67}};
fmt::println( "*p_number1(after delete): {}", *p_number1 );
delete p_number1;
fmt::println( "*p_number1(after delete): {}", *p_number1 );//CRASH!- 多个指针指向同个地址
int *p_number3 {new int{83}};
int *p_number4 {p_number3};
delete p_number3;
fmt::println( "p_number4(after deleting p_number3) -{} : {}", fmt::ptr(p_number4) , *p_number4 );//CRASH!- new失败
int* data = new int[10000000] //crash,内存不够解决方案如下
1.声明时就进行初始化
int *p_number5{nullptr};
int *p_number6{new int(87)};
//在使用时进行检查
if(p_number6!= nullptr){
fmt::println( "p_number6 - {} - {}", fmt::ptr(p_number6), *p_number6 );
}else{
fmt::println( "Invalid address" );
}- 在释放内存后重置指针
int *p_number7{ new int{ 82 } };
delete p_number7;
p_number7 = nullptr;// Reset the pointer- 应该规定一个主指针来管理内存,其他指针必须在主指针可用时才能使用
int *p_number8{ new int{ 382 } };// Let's say p_number8 is the master pointer
int *p_number9{ p_number8 };
delete p_number8;// Master releases the memory
p_number8 = nullptr;
if (!(p_number8 == nullptr)) {// Only use slave pointers when master pointer is valid
fmt::println("p_number9 - {} : {}", fmt::ptr(p_number9), *p_number9);
} else {
fmt::println("WARNING : Trying to use an invalid pointer");
}4.使用catch-except语法或者std::nothrow
try{
int * data = new int[100000000000000];
for(size_t i{0} ; i < 10000000 ; ++i){
int * data = new int[100000000000000];
// int *data = new (std::nothrow) int[1000000000000000]; //有std::nothrow关键字,如果有异常,则会赋值nullptr,不会程序崩溃
}
}catch(std::exception& ex){
fmt::println("Something went wrong: {}", ex.what());
}
2.2.3 高级用法
- 可以把分配内存和创建对象分开来进行
#include <new>
struct A {
A() { std::cout << "A born\n"; }
~A() { std::cout << "A dead\n"; }
};
//栈上
char buffer[sizeof(A)]; // 预分配内存
A* p = ::new (buffer) A(); // 在指定内存构造
p->~A(); // 需要显式调用析构函数
/*
void* buffer = operator new(sizeof(A)); //堆上分配
A* p = ::new (buffer) A();
p->~A();
operator delete(buffer); //堆上释放
*/ - 自定义new、delete
class A {
public:
static void* operator new(size_t size) {
std::cout << "分配 " << size << " 字节\n";
return ::operator new(size);
}
static void operator delete(void* p) {
std::cout << "释放内存\n";
::operator delete(p);
}
};::operator new 是 全局作用域里的
operator new 函数
2.3 智能指针
手动管理堆上的指针很容易出问题,所以有了能自动管理的智能指针
2.3.1.unique_ptr
唯一指针,只能有一个指针指向同一内存,使用这个就不用手动delete
#include <memory>
int* p_int3 = new int(30);
std::unique_ptr<int> up_int4{p_int3};
std::unique_ptr<int> up_int5 {new int(50)};在使用了std::unique_ptr后最好不要使用原始指针p_int3了
c++14后有更好的方法,以避免直接使用new
std::unique_ptr<int> up_int7 = std::make_unique<int>(70);
up_int7.reset() //重置为空指针,内存被释放注意:如果初始化两个unique_ptr指向p_int3,程序会崩溃,因为会自动delete两次
如果要转移指针的控制权,可以用std::move,
int* p_int3 = new int(30);
std::unique_ptr<int> up_int4{p_int3};
std::unique_ptr<int> up_int5{std::move(up_int4)};这种情况下,up_int4会被自动重置为空指针
2.3.2 shared_ptr
和unique_ptr不同在于,可以把自身指针信息复制出去。然后进行一个计数,复制一次加1,重置一次减1,use_count()归0的话,内存会被释放
std::shared_ptr<int> int_ptr_1{ new int{20} };
std::shared_ptr<int> int_ptr_2 = int_ptr_1;
std::println(stdout, "Use count for int_ptr_1: {}", int_ptr_1.use_count());
std::println(stdout, "Use count for int_ptr_2: {}", int_ptr_2.use_count());
int_ptr_2.reset();
std::println(stdout, "Use count for int_ptr_1: {}", int_ptr_1.use_count());
但是仍然不能使用下面方式,会崩溃
int* p = new int{ 20 };
std::shared_ptr<int> int_ptr_1{ p };
std::shared_ptr<int> int_ptr_2{ p };c++14后可以使用make_shared
std::shared_ptr<int> int_ptr_1 = std::make_shared<int>(20);
std::shared_ptr<int> int_ptr_2{ nullptr };
int_ptr_2 = int_ptr_1;
int_ptr_2.reset();2.3.3 weak_ptr
weak_ptr被称为弱指针,可以和shared_ptr一起使用。但是它不会增加引用计数。当我们将一个shared_ptr赋值给另外一个shared_ptr时它会增加引用计数。但是当把一个shared_ptr赋值给一个weak_ptr时不会增加引用计数。如果不想要Entity的所有权,例如在排序一个Entity列表时不关心它们是否有效,只需要存储一个它们的引用就可以了。
class A {
public:
weak_ptr<B> b_ptr; // 改为weak_ptr(不增加引用计数)
~A() { cout << "A析构" << endl; }
};
class B {
public:
weak_ptr<A> a_ptr; // 改为weak_ptr
~B() { cout << "B析构" << endl; }
};
int main() {
shared_ptr<A> a = make_shared<A>();
shared_ptr<B> b = make_shared<B>();
a->b_ptr = b; // weak_ptr不增加引用计数(b的计数仍为1)
b->a_ptr = a; // weak_ptr不增加引用计数(a的计数仍为1)
return 0;
} // a、b析构,引用计数=0 → A、B均析构(无内存泄漏)2.4 类(class)
与结构体类似,class默认private
范例
#include <iostream>
using namespace std;
const double PI = 3.14;
class Circle
{
public:
int m_r;
double calculateZC()
{
return 2 * PI * m_r;
}
};
int main()
{
Circle c1;
c1.m_r = 10;
cout << "圆的周长为:" << c1.calculateZC() << endl;
std::cin.get();//pause
return 0;
}2.4.1封装权限
公共权限 public,类内可以访问成员,类外可以访问成员
保护权限 protected,类内可以访问成员,类外不可以访问成员 子类可以访问父类中的保护内容
私有权限 private,类内可以访问权限,类外不可以访问成员 子类不可以访问父类中的私有内容
2.4.2构造函数
构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。 构造函数语法:类名 () {}
- 构造函数,没有返回值也不写void。
- 函数名称与类名相同。
- 构造函数可以有参数,因此可以重载。
- 程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次。
#include <iostream>
#include <string>
class Person
{
public:
// 内部赋值方式
Person(int x1)
{
x = x1;
std::cout << "Person 构造函数1的调用" << endl;
}
// 初始化列表方式
Person(int x1,string y1):
x(x1),
y(y1)
{
std::cout << "Person 构造函数2的调用" << endl;
}
//编译器会自动创建一个,但是里面是空语句
Person()=default;
/*
//Not recommended in modern C++
Pixel(){
}
*/
private:
int x;
string y;
};
int main()
{
Person x_entiy(2);
/*=》等价写法
Person x_entiy=2;//隐式转换法
Person x_entiy = Person(2) //显式法
*/
Person y_entiy(1,"asd");
std::cout << y_entiy.y<<endl;
std::cout << x_entiy.x<<endl;
std::cin.get();
return 0;
}2.4.3 析构函数
析构函数,没有返回值也不写void,程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次。
如果类中有new出的堆上的空间,就需要在结束时调用析构函数来释放。如果没有手动管理的,则不需要定义析构函数
class Person
{
public:
Person()
{
cout << "Person 构造函数的调用" << endl;
}
~Person()
{
delete a;
cout << "Person 析构函数的调用" << endl;
}
private:
a = new int(8);
};注意:如果使用的是智能指针管理内存,则不需要使用析构
2.4.4 隐式转换和显式转换
explicit:该构造函数是显示的。
implicit:该构造函数是隐式的(默认)
其用处是修饰类构造函数
如果是显示的会被阻止,在编译时无法自动转为对象。
如果是隐式的会被放行。
#include <iostream>
using namespace std;
class Cat
{
public :
Cat(int num):n(num){} // 默认为 implicit
private:
int n;
};
class Mouse
{
public :
explicit Mouse(int num):n(num){}
private:
int n;
};
int main()
{
Cat t1 = 12; // 编译通过12会自动转为Cat对象赋值到构造函数
Mouse t2(13); // 编译通过,
Mouse t3 = 14; // 在编译时,这个地方会报错,因为这个时显示的,因此不能直接转为Test2对象
return 0;
}2.4.5 符号重载
- 运算符重载:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。
- 对于内置的数据类型的表达式的运算符是不可能改变的。
加号重载
Person operator+(Person &p1, Person &p2)
{
Person temp;
temp.m_A = p1.m_A + p2.m_A;
temp.m_B = p1.m_B + p2.m_B;
return temp;
}左移重载
#include <iostream>
class Person
{
private:
int m_A;
int m_B;
public:
Person(int a, int b) : m_A(a), m_B(b) {}
friend std::ostream& operator<<(std::ostream &cout, Person &p);
};
// 如果返回类型为void,那么就无法无限追加,也没有办法在后面添加换行符
std::ostream &operator<<(std::ostream &cout, Person &p)
{
cout << "m_A= " << p.m_A << " m_B=" << p.m_B;
return cout;
}
int main()
{
Person p(10,10);
std::cout << p << " hello world" << std::endl;
return 0;
}2.4.6 拷贝构造函数
c++默认提供一个拷贝构造函数。它的作用是内存复制,将other对象的内存浅层拷贝进这些成员变量。形如以下:
Entity(const Entity& other)//拷贝构造函数
{
memcpy(this,&other,sizeof(other));
}
// Entity a{};
// Entity b{a};这会带来一个问题,如果有类中有使用指针,或者在堆上开辟内存,那么拷贝后,两个实例共享同一块内存。
且不说普通指针,堆上的空间会被释放两次,程序会直接崩溃(包括智能指针)。加上我们不想数据共享。
所以,在这种情况下,需要手动定义拷贝构造函数,进行深拷贝
下面的例子是拷贝字符串的例子
#include <iostream>
#include <fmt/format.h>
class String
{
public:
String(const char *string)
{
m_Size = strlen(string);
m_Buffer = new char[m_Size + 1]; //+1是为了复制'\0'
memcpy(m_Buffer, string, m_Size + 1);
}
String(const String &other)
{
m_Size = other.m_Size;
m_Buffer = new char[m_Size + 1];
memcpy(m_Buffer, other.m_Buffer, m_Size + 1);
}
~String()
{
std::cout << "Destructor called" << std::endl;
delete[] m_Buffer;
}
const char *get()
{
return m_Buffer;
}
private:
char *m_Buffer;
unsigned int m_Size;
};
int main()
{
String string1 = "Cherno";
String string2 = string1;
std::cout << fmt::ptr(string2.get()) << std::endl;
std::cout << fmt::ptr(string1.get()) << std::endl;
return 0;
}如果我们把上面的拷贝函数注释掉。可以发现打印的两个地址相同,然后程序崩溃了。
2.4.7 拷贝赋值函数
与上面的函数不同的是,上面是发生了构造过程,这个则是在构造完想进行赋值
String string1 = "Cherno";
String string2;
string2 = string1; //区别在这这时候需要定义一个关于=的重载函数
String& &operator=(const String &other)
{
if (this == &other)
return *this;
delete[] m_Buffer; //先释放已有的空间
m_Size = other.m_Size;
m_Buffer = new char[m_Size + 1];
memcpy(m_Buffer, other.m_Buffer, m_Size + 1);
return *this;
}如果m_Buffer开辟空间失败,结果就是原来的数据损坏,然后该实例则进入半死状态。
而且手动写了这些函数,就必须把析构、拷贝、移动5个函数都写上,非常繁琐
最后就是delete和new必须严格配对。总之这种写法已经落后
2.4.8 拷贝函数(智能指针)
上一节是通过new开辟的空间,很繁琐很容易出错。下面用智能指针重构
#include <memory>
#include <cstring>
#include <fmt/format.h>
class String {
public:
String() = default;
String(const char* str);
String(const String& other);
String& operator=(const String& other);
String(String&& other) = delete; //下面章节会补上
String& operator=(String&& other) = delete;
const char* get() const { return m_Buffer.get(); }
private:
size_t m_Size = 0;
std::unique_ptr<char[]> m_Buffer;
};
String& String::operator=(const String& other)
{
if (this == &other)
return *this;
// 1️⃣ 先分配新内存(异常安全)
auto newBuffer = std::make_unique<char[]>(other.m_Size + 1);
std::memcpy(newBuffer.get(), other.m_Buffer.get(), other.m_Size + 1);
// 2️⃣ 再整体替换
m_Size = other.m_Size;
m_Buffer = std::move(newBuffer);
return *this;
}
String::String(const char* str)
{
m_Size = std::strlen(str);
m_Buffer = std::make_unique<char[]>(m_Size + 1);
std::memcpy(m_Buffer.get(), str, m_Size + 1);
}
String::String(const String& other)
{
m_Size = other.m_Size;
m_Buffer = std::make_unique<char[]>(m_Size + 1);
std::memcpy(m_Buffer.get(), other.m_Buffer.get(), m_Size + 1);
}
int main()
{
String s1("Hello");
String s2(s1);
String s3;
s3 = s2;
s1 = s2;
fmt::println("s1: {}\n", fmt::ptr(s1.get()));
fmt::println("s2: {}\n", fmt::ptr(s2.get()));
fmt::println("s3: {}\n", fmt::ptr(s3.get()));
}2.4.9 移动构造、移动赋值
这个主要是用来 把“资源的所有权”从一个对象转移到另一个对象,避免深拷贝。
如果只是单纯的转移资源,可以采用默认的函数,智能指针能够自动处理这个事
class String {
public:
std::unique_ptr<char[]> m_Buffer;
size_t m_Size = 0;
String(String&&) noexcept = default;
String& operator=(String&&) noexcept = default;
};但如果想精准控制转移后的情况,则可以:
String::String(String&& other) noexcept
: m_Size(other.m_Size),
m_Buffer(std::move(other.m_Buffer))
{
other.m_Size = 0;
}
String& String::operator=(String&& other) noexcept
{
if (this != &other) {
m_Buffer = std::move(other.m_Buffer);
m_Size = other.m_Size;
other.m_Size = 0;
}
return *this;
}new版本
String::String(String&& other) noexcept
: m_Size(other.m_Size),
m_Buffer(other.m_Buffer)
{
other.m_Size = 0; // 可以忽略
other.m_Buffer = nullptr; //必须重置,防止释放资源
}
String& String::operator=(String&& other) noexcept
{
if (this != &other) {
delete[] m_Buffer; // 释放旧资源
m_Size = other.m_Size;
m_Buffer = other.m_Buffer;
other.m_Buffer = nullptr;
}
return *this;
}
/* 更专业的写法
String& String::operator=(String&& other) noexcept
{
std::swap(m_Size, other.m_Size);
std::swap(m_Buffer, other.m_Buffer);
return *this;
}
*/
// 因为传进来的是右值,swap后,右值会被丢弃,所以效果等价实际使用时
String s1("Hello");
String s2(std::move(s1)); //移动构造
String s3;
s3 = std::move(s2); //移动赋值2.4.10 继承
范例:
#include <iostream>
using namespace std;
class Ball
{
public:
int diameter;
void hit()
{
std::cout<<"ball has been hited"<<std::endl;
}
void print()
{
std::cout<<"this is a ball"<<std::endl;
}
};
class Football:public Ball
{
public:
int color[3];
}三种继承改变权限
- 继承的语法:class 子类:继承方式 父类
- 继承方式一共有三种:
- 公共继承
- 保护继承
- 私有继承
- 不同的继承方式,父类中的变量被继承后,权限相应的得到了改变,如下图所示。
继承后同名函数的处理
- 子类对象可以直接访问到子类中同名成员。
- 子类对象加作用域可以访问到父类同名成员。
- 当子类与父类拥有同名的成员函数,子类会隐藏父类中所有同名成员函数(有参、无参),加作用域才可以访问到父类中同名函数。
- 静态函数下,子类和分类不会共用同一份数据
//同名成员属性处理方式
void test01()
{
Son s;
cout << "Son 下 m_A=" << s.m_A << endl;
//如果通过子类对象访问到父类中同名成员,需要加作用域
cout << "Base 下 m_A=" << s.Base::m_A << endl;
}多继承语法
- C++运行一个类继承多个类。
- 语法:
class 子类:继承方式 父类1,继承方式 父类2,..... - 多继承可能会引发父类中有同名成员出现,需要加作用域区分。
- C++实际开发中不建议用多继承。
菱形继承
菱形继承概念:
- 两个派生类继承同一个基类
- 又有某个类同时继承两个派生类
- 这种继承被称为菱形继承
2.4.11 多态、虚函数
静态的多态:函数重载,看起来调用同一个函数却有不同的行为。静态:原理是编译时实现。 动态的多态:一个父类的引用或指针去调用同一个函数,传递不同的对象,会调用不同的函数。动态:原理是运行时实现。
在继承中要构成多态两个条件:
- 必须通过基类的指针或者引用调用虚函数。
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。
虚函数:即被virtual修饰的类成员函数称为虚函数。一旦定义了虚函数,该基类的派生类中同名函数也自动成为了虚函数。也就是说在派生类中有一个和基类同名的函数,只要基类加了virtual修饰,派生类不加virtual修饰也是虚函数。虚函数只能是类中的一个成员函数,不能是静态成员或普通函数。
如下面代码中,doSpeak函数入口是基类,但是可以输入派生类,且因为虚函数的覆写,可以产生不同的结果
#include <iostream>
using namespace std;
class Animal
{
public:
virtual void speak()
{
cout << "动物在说话" << endl;
}
};
class Cat:public Animal
{
public:
//重写 函数返回值类型、函数名、参数列表都完全相同才叫重写
void speak() override//子类virtual,override可写可不写,也可以写 virtual void speak()
{
cout << "小猫在说话" << endl;
}
};
class Dog:public Animal
{
public:
virtual void speak() override
{
cout << "小狗在说话" << endl;
}
};
void doSpeak(Animal &animal) // Animal & animal = cat
{
animal.speak();
}
int main()
{
Cat cat;
doSpeak(cat);
Dog dog;
doSpeak(dog);
}纯虚函数和抽象类
- 在多态中,通常父类中虚函数的实现时毫无意义的,主要都是调用子类重写的内容。因此,可以将虚函数改为纯虚函数。
- 纯虚函数语法:
virtual 返回值类型 函数名 (参数列表) = 0; - 当类中有了纯虚函数,这个类也称为抽象类。
④ 抽象类特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类。
#include <iostream>
using namespace std;
//纯虚函数和抽象类
class Base
{
public:
virtual void func() = 0;
};
class Son : public Base
{
public:
virtual void func()
{
cout << "func函数调用" << endl;
}
};虚析构函数
核心问题:当基类指针指向派生类对象时,若基类析构函数非虚函数,销毁对象时只会调用基类析构,派生类析构不会执行,导致资源泄漏;
核心作用:确保派生类对象通过基类指针销毁时,先调用派生类析构,再调用基类析构;
语法:在基类析构函数前加
virtual;示例(对比非虚析构与虚析构):
// 基类 class Base { public: // 非虚析构(错误示范) ~Base() { cout << "Base析构" << endl; } }; // 派生类(含堆资源) class Derived : public Base { private: int* data; public: Derived() { data = new int(10); } ~Derived() { delete data; // 释放派生类的堆资源 cout << "Derived析构" << endl; } }; // 测试:基类指针指向派生类对象 Base* ptr = new Derived; delete ptr; // 仅调用Base析构,Derived析构未执行 → 内存泄漏!修正(虚析构):
class Base { public: virtual ~Base() { // 基类析构设为virtual cout << "Base析构" << endl; } }; Base* ptr = new Derived; delete ptr; // 正确执行顺序:Derived析构 → Base析构(无内存泄漏)面试考点:虚析构的必要性——只要类可能被继承,且派生类有堆资源,基类析构必须设为
virtual;抽象类(含纯虚函数)的析构也需设为虚析构(可提供默认实现)。
与重载的区别(重点)
基类为虚函数下,子类重写虚函数,用基类指针调用子类,会使用子类函数
基类不是虚函数下,子类重写函数,用基类指针调用子类,会使用父类函数
#include <iostream>
class Base {
public:
void show() {
std::cout << "Base class show" << std::endl;
}
};
class Derived : public Base {
public:
void show() { // 这里不是虚函数的重写,只是覆盖
std::cout << "Derived class show" << std::endl;
}
};
int main() {
Base* basePtr = new Derived(); // 基类指针指向派生类对象
basePtr->show(); // 输出 "Base class show",而不是 "Derived class show"
delete basePtr;
return 0;
}2.4.12 const成员函数、mutable
常函数:
- 成员函数后加const后我们称这个函数为常函数。
- 常函数内不可以修改成员属性。
- 成员属性声明时加关键字mutable后,在常函数中依然可以修改,mutable主要用于debug。
常对象:
- 声明对象前加const称该对象为常对象。
- 常对象只能调用常函数。
class TestClass
{
public:
mutable int count = 0;
int count1 = 0;
void print() const
{
std::cout << count << std::endl;
++count;
//++count1;//报错
}
};2.4.13 this指针
- 每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会公用一块代码。
- C++通过提供特殊的对象指针,this指针指向被调用的成员函数所属的对象。
- this指针是隐含每一个非静态成员函数内的一种指针。
- this指针不需要定义,直接使用即可。
this指针的用途:
- 当形参和成员变量同名时,可用this指针来区分。
- 在类的非静态成员函数中返回对象本身,可使用return * this。
#include <iostream>
using namespace std;
class Person
{
public:
Person(int age)
{
this->age = age;
//如果这里是 age = age;那么编译器会将这两个age和上面的形参age当做同一个age,因此age并没有赋值
}
//如果用值的方式返回,Person PersonAddAge(Person& p){},它返回的是本体拷贝的对象p',而不是本体p
Person& PersonAddAge(Person& p) //要返回本体的时候,要用引用的方式返回
{
this->age += p.age;
return *this;
}
int age;
};
int main()
{
Person p1(10);
Person p2(10);
//链式编程思想
p2.PersonAddAge(p1).PersonAddAge(p1).PersonAddAge(p1);
cout << "p2的年龄为:" << p2.age << endl;
std::cin.get();
return 0;
}2.5 IO
文件IO用于实现“数据持久化”(将数据存储到磁盘文件,或从文件读取数据),C++提供两种方式:C风格文件IO(<cstdio>)
和
C++风格文件IO(<fstream>),推荐使用C++风格(面向对象、类型安全)。
| 模式标志 | 含义 |
|---|---|
ios::in |
读模式(默认用于ifstream) |
ios::out |
写模式(默认用于ofstream),若文件存在则清空,不存在则创建 |
ios::app |
追加模式(写模式的补充),数据写入文件末尾,不覆盖原有内容 |
ios::binary |
二进制模式(默认是文本模式),用于读写二进制文件(如图片、视频) |
ios::trunc |
截断模式(默认用于ios::out),文件存在则清空内容 |
ios::ate |
打开文件后,文件指针直接指向文件末尾 |
2.5.1 字符流
#include <vector>
#include <fstream>
#include <filesystem>
#include <fmt/format.h>
#include <vector>
void write_txt(){
std::filesystem::path file_path("test.txt");
std::ofstream file(file_path,std::ios::out | std::ios::app);
std::vector<std::string> content = {"Hello", "World"};
if (!file.is_open()) {
fmt::println("Failed to open file for writing: {}", file_path.filename().string());
}
for (const auto& line : content) {
file << line << '\n';
}
file.close();
}
void read_txt(){
std::filesystem::path file_path("test.txt");
std::ifstream file(file_path,std::ios::in);
std::vector<std::string> content;
if (!file.is_open()) {
fmt::println("Failed to open file for reading: {}", file_path.filename().string());
}
std::string line;
while (std::getline(file, line)) {
content.push_back(line);
}
for (const auto& line : content) {
fmt::println("{}", line);
}
}2.5.2 字节流
二进制文件存储的是字节序列(人类不可读),适用于存储结构体、图片、视频等数据,读写时需指定ios::binary模式,且不会处理换行符。
下面是读写字符的案例
int main()
{
std::filesystem::path file_path = "example_binary_file.bin";
const char *content{"hello world\n123"};
std::ofstream file(file_path, std::ios::out | std::ios::binary); // Binary mode
if (!file.is_open())
{
fmt::print("Failed to open file for writing: {}\n", file_path.filename().string());
return 1;
}
file.write(content, strlen(content));
file.close();
std::ifstream file1(file_path, std::ios::in | std::ios::binary);
if (!file1.is_open())
{
fmt::print("Failed to open file for reading: {}\n", file_path.filename().string());
return 1;
}
std::streamsize size = std::filesystem::file_size(file_path);
// Get the file size by seeking to the end
// file.seekg(0, std::ios::end);
// std::streamsize size = file.tellg();
// file.seekg(0, std::ios::beg);
std::vector<char> buffer(size);
if (!file1.read(buffer.data(), size))
{
return 1;
}
file1.close();
fmt::println("Content: {}", std::string(buffer.begin(), buffer.end()));
return 0;
}2.5.3 文件指针操作
文件指针(文件位置指示器)指向当前读写的位置,可通过seekg()(读指针)和seekp()(写指针)调整位置,配合tellg()/tellp()获取当前位置。
核心函数
| 函数名 | 功能 |
|---|---|
seekg(pos, mode) |
移动读指针到指定位置:pos是偏移量,mode是基准(ios::beg:文件开头;ios::cur:当前位置;ios::end:文件末尾) |
seekp(pos, mode) |
移动写指针(用法同seekg) |
tellg() |
返回读指针当前位置(字节数,从文件开头算起) |
tellp() |
返回写指针当前位置 |
#include <fstream>
#include <iostream>
using namespace std;
int main() {
ifstream ifs("test.txt", ios::in | ios::binary);
if (!ifs.is_open()) return -1;
// 1. 移动读指针到文件末尾,获取文件大小
ifs.seekg(0, ios::end);
int fileSize = ifs.tellg(); // 文件总字节数
// 2. 移动读指针到文件末尾前10个字节(若文件小于10字节则从开头开始)
int offset = (fileSize > 10) ? -10 : 0;
ifs.seekg(offset, ios::end);
// 3. 读取剩余字节
char buf[11] = {0}; // 存储10个字符+结束符
ifs.read(buf, 10);
cout << "文件最后10个字节:" << buf << endl;
ifs.close();
return 0;
}注意事项:
- 文本文件读写时,
string类型可直接用<</>>,但二进制文件不建议存储string(动态内存导致结构不固定),推荐用固定长度字符数组; - 打开文件后必须检查是否成功(
is_open()),避免因路径错误、权限问题导致后续操作失败; - 操作完成后手动关闭文件(
close()),或使用RAII封装(如自定义文件类,析构时自动关闭); - 二进制文件的跨平台兼容性:不同平台的字节序(大端/小端)、结构体对齐方式可能不同,需注意兼容性处理。
2.5.4 POD序列化
namespace pod_types
{
struct PODType
{
int id;
double value;
};
// Function to write POD to a binary file (returns bool for success)
bool write_pod(const std::filesystem::path &filename, const PODType &pod)
{
std::ofstream out_file(filename, std::ios::binary);
if (!out_file)
{
return false;
}
out_file.write(reinterpret_cast<const char *>(&pod), sizeof(PODType));
return out_file.good();
}
// Function to read POD from a binary file (returns bool for success)
bool read_pod(const std::filesystem::path &filename, PODType &pod)
{
std::ifstream in_file(filename, std::ios::binary);
if (!in_file)
{
return false;
}
in_file.read(reinterpret_cast<char *>(&pod), sizeof(PODType));
return in_file.good();
}
// Function to write a vector of PODType objects to a file
bool write_pod_vector(const std::filesystem::path &file_path,
const std::vector<pod_types::PODType> &pod_vector)
{
std::ofstream out_file(file_path, std::ios::binary);
if (!out_file)
{
fmt::print("Failed to open file for writing.\n");
return false;
}
for (const auto &pod : pod_vector)
{
out_file.write(reinterpret_cast<const char *>(&pod), sizeof(pod_types::PODType));
}
if (!out_file.good())
{
fmt::print("Failed to write PODType objects to file.\n");
return false;
}
out_file.close();
return true;
}
// Function to read a vector of PODType objects from a file
bool read_pod_vector(const std::filesystem::path &file_path,
std::vector<pod_types::PODType> &pod_vector)
{
std::ifstream in_file(file_path, std::ios::binary);
if (!in_file)
{
fmt::print("Failed to open file for reading.\n");
return false;
}
pod_types::PODType read_pod;
while (in_file.read(reinterpret_cast<char *>(&read_pod), sizeof(pod_types::PODType)))
{
pod_vector.push_back(read_pod);
}
if (!in_file.eof() && !in_file.good())
{
fmt::print("Failed to read PODType objects from file.\n");
return false;
}
in_file.close();
return true;
}
} // namespace pod_types2.5.5 非POD序列化
#include <vector>
#include <fstream>
#include <filesystem>
#include <fmt/format.h>
#include <vector>
namespace non_pod_types
{
class Person
{
public:
std::string name;
int age;
// Serialization function for a single person
void serialize(std::ofstream &ofs) const
{
size_t name_size = name.size();
ofs.write(reinterpret_cast<const char *>(&name_size), sizeof(name_size));
ofs.write(name.data(), name_size);
ofs.write(reinterpret_cast<const char *>(&age), sizeof(age));
}
// Deserialization function for a single person
void deserialize(std::ifstream &ifs)
{
size_t name_size;
ifs.read(reinterpret_cast<char *>(&name_size), sizeof(name_size));
name.resize(name_size);
// Read the name and age from the file and store in the object's members: name and age
ifs.read(name.data(), name_size);
ifs.read(reinterpret_cast<char *>(&age), sizeof(age));
}
};
// Function to write a vector of person objects to a file
void write_persons_to_file(const std::vector<Person> &persons, const std::filesystem::path &file_path)
{
std::ofstream ofs(file_path, std::ios::binary);
if (!ofs)
{
fmt::println("Failed to open file for writing: {}", file_path.string());
return;
}
size_t vector_size = persons.size();
ofs.write(reinterpret_cast<const char *>(&vector_size), sizeof(vector_size));
for (const auto &p : persons)
{
p.serialize(ofs);
}
fmt::println("Serialized {} person(s) to file: {}", vector_size, file_path.string());
}
// Function to read a vector of person objects from a file
std::vector<Person> read_persons_from_file(const std::filesystem::path &file_path)
{
std::ifstream ifs(file_path, std::ios::binary);
if (!ifs)
{
fmt::println("Failed to open file for reading: {}", file_path.string());
return {};
}
size_t vector_size;
ifs.read(reinterpret_cast<char *>(&vector_size), sizeof(vector_size));
std::vector<Person> persons(vector_size);
for (auto &p : persons)
{
p.deserialize(ifs);
}
fmt::println("Deserialized {} person(s) from file: {}", vector_size, file_path.string());
return persons;
}
} // namespace non_pod_types
int main()
{
non_pod_types::Person p1{"John", 30};
non_pod_types::Person p2{"Mike", 25};
// write_to_file
non_pod_types::write_persons_to_file({p1, p2}, "persons.bin");
// read_from_file
auto persons = non_pod_types::read_persons_from_file("persons.bin");
for (const auto &p : persons)
{
fmt::println("Name: {}, Age: {}", p.name, p.age);
}
}2.5.6 类的序列化
其实还是属于非POD的内容,关键在于手动转换。不过这种转换风险太大,最好使用专门的库
#include <vector>
#include <fstream>
#include <filesystem>
#include <fmt/format.h>
#include <vector>
namespace class_with_non_pod_types
{
// Position struct definition
struct Position
{
unsigned int x{0};
unsigned int y{0};
Position() = default;
Position(unsigned int x, unsigned int y) : x(x), y(y) {}
};
// Pixel class definition
class Pixel
{
public:
Pixel() = default;
Pixel(uint32_t color, unsigned int x, unsigned int y)
: m_color(color), m_pos(x, y) {}
uint32_t get_color() const { return m_color; }
const Position &get_position() const { return m_pos; }
void set_color(uint32_t color) { m_color = color; }
void set_position(unsigned int x, unsigned int y)
{
m_pos.x = x;
m_pos.y = y;
}
void serialize(std::ofstream &out) const
{
out.write(reinterpret_cast<const char *>(&m_color), sizeof(m_color));
out.write(reinterpret_cast<const char *>(&m_pos.x), sizeof(m_pos.x));
out.write(reinterpret_cast<const char *>(&m_pos.y), sizeof(m_pos.y));
}
void deserialize(std::ifstream &in)
{
in.read(reinterpret_cast<char *>(&m_color), sizeof(m_color));
in.read(reinterpret_cast<char *>(&m_pos.x), sizeof(m_pos.x));
in.read(reinterpret_cast<char *>(&m_pos.y), sizeof(m_pos.y));
}
private:
uint32_t m_color{0xFF000000};
Position m_pos; // Now directly embedded, no heap allocation
};
void save_pixels(const std::vector<Pixel> &pixels, const std::filesystem::path &filepath)
{
std::ofstream out(filepath, std::ios::binary);
if (!out)
{
fmt::println("Error opening file for writing: {}", filepath.string());
return;
}
for (const auto &pixel : pixels)
{
pixel.serialize(out);
}
}
std::vector<Pixel> load_pixels(const std::filesystem::path &filepath)
{
std::vector<Pixel> pixels;
std::ifstream in(filepath, std::ios::binary);
if (!in)
{
fmt::println("Error opening file for reading: {}", filepath.string());
return pixels;
}
while (true)
{
Pixel temp;
temp.deserialize(in);
if (in.eof())
break;
pixels.push_back(temp);
}
return pixels;
}
} // namespace class_with_non_pod_types
int main()
{
using namespace class_with_non_pod_types;
std::vector<Pixel> pixels = {Pixel(0xFF000000, 0, 0), Pixel(0xFF0000FF, 1, 1)};
save_pixels(pixels, "pixels.bin");
auto loaded_pixels = load_pixels("pixels.bin");
fmt::println("Loaded {} pixels", loaded_pixels.size());
return 0;
}