一、decltype(RTTI)


RTTI 即 run time type identification(程序运行对对象的类型识别)
auto和decltype

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//类型推导RTTI : run time type identification
//程序运行起来对对象的类型识别
int a{ 10 }, b{ 20 };
decltype(a + b) c = 30; //定一个c和a+b是一个类型的对象
cout << typeid(c).name() << endl;
cout << c << endl;

auto d = a+b;
cout << typeid(d).name() << endl;
cout << d << endl;


//auto不能作为形参和返回值
/*auto func(auto e)
{
return e;
}
cout << func(a) << endl;*/

虽然auto不能作为函数的形参和返回值
但是在C++14的标准中就有了lambda表达式内可以有auto


二、C++11中的新容器


c++98容器:
string / vector / list / deque / map / set / bitset + stack / queue / priority_queue
c++11新容器:
array(定长数组) / forward_list(单链表)
array: #include< array> 缺点:定长+存储数据的空间在 栈上,栈的空间本来就不大
forward_list: #include< forward_list > 缺点:不支持尾插尾删+insert数据也是在当前位置的后面
unordered_map/unordered_set : 效率高于map / set,推荐


三、默认成员函数的控制


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class B
{
public:
//1.要生成默认构造函数
B() = default; //加入default关键字,即可仍然生成默认构造函数
B(const int& b) //构造函数
:_b(b)
{}

//2.不能拷贝和赋值(防拷贝)
//c++98
B(const B& b);
B& operator=(const B& b);
//1.通过只声明,不定义的方式,使得链接失败,但是可以在外部定义
//2.private限定声明

//c++11
B(const B& b) = delete;
B& operator=(const B& b) = delete;
private:
//B(const B& b);
//B& operator=(const B& b); //private限定声明
int _b = 10;
};

B::B(const B& b) //可以在外部定义
{} //delete删除仍然可以在外部定义,但是拷贝构造时就会报错

void test4()
{
B b1; //因为有显示构造函数了,没有生成默认构造函数
B b2(b1); //内部只声明,外部定义,仍然可以使用拷贝构造
b1 = b2;
}

四、右值引用


1.右值引用的基本使用

左右不是方向,和左移右移一样,是C留下的坑
左值通常是变量
右值通常是常量,表达式或者函数返回值等临时常量
c++11将右值分为:纯右值 , 将亡值
纯右值:基本类型的常量或者临时对象
将亡值:自定义类型的临时对象

总结
右值引用做参数和做返回值减少拷贝的本质是利用了移动构造和移动赋值
左值引用和右值引用本质的作用都是减少拷贝,右值引用本质可以认为是弥补左值引用不足的地方,他们相辅相成

左值引用: 解决的是传参过程中和返回值过程中的拷贝
做参数 : void push(T x) -> void push(T& x) 解决的是传参过程中减少拷贝
做返回值 : T f2() -> T& f2() 解决的是返回值过程中的拷贝
注意: 这里有限制,如果返回对象出了作用域不在了就不能传引用,这个左值引用无法解决,等待C++11右值引用解决

右值引用 : 解决的是传参后,push/insert 函数内部将对象移动到容器空间上的问题 + 传值返回接受返回值的拷贝
做参数 : void push(T&& x) 解决的push内部不再使用拷贝构造x到容器空间上,而是移动构造过去
做返回值: T&& f2() 解决的外面调用接收f2() 返回对象的拷贝,T ret = f2() , 这里就是右值引用的移动给构造,减少了拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
template<class T>
void fun(const T& a)
{
cout << "void fun(const T& a)" << endl;
}

template<class T>
void fun(const T&& a)
{
cout << "void fun(const T&& a)" << endl;
}

void test5()
{
int x = 1, y = 2, a = 10;

int& b = a;

int& e = 10;
int& f = x + y;//左值引用不能用右值初始化,这两段会出错

//加上const,左值引用可以引用右值
const int& e = 10;
const int& f = x + y;

//右值引用
int&& c = 10;
int&& d = x + y;

//引用move后右值引用可以引用左值
int&& m = a; //右值引用无法正常引用左值
int&& m = move(a);

fun(x); //调左值引用
fun(10); //调右值引用
}

2.右值引用的作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
class String
{
public:
String(const char* str = "")
{
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}

//s2(s1)
String(const String& s)
{
cout << "String(const String& s) - 深拷贝" << endl;
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
}

String(String&& s) //传递进一个将亡值,直接交换
:_str(nullptr)
{
cout << "String(const String&& s) - 移动拷贝" << endl; //相对于深拷贝,效率高
swap(_str, s._str);
}

String& operator=(const String& s)
{
if (this != &s)
{
char* newstr = new char[strlen(s._str) + 1];
strcpy(newstr, s._str);

delete[] _str;
_str = newstr;
}
return *this;
}

String& operator=(String&& s)
{
cout << "String& operator=(String&& s) - 移动赋值 - 高效" << endl;
swap(_str, s._str);
return *this;
}

~String()
{
delete[] _str;
}


String& operator+=(const String& s)
{
//this->append(s);
return *this; //返回的是左值
}

String operator+(const String& s)
{
String ret(*this);
//ret.append(s);
return ret; //返回的是右值
}

private:
char* _str;
};

String f(const char* str)
{
String tmp(str);
return tmp; //返回tmp临时拷贝对象
}

int test1() //右值引用
{
String s1("左值"); //"左值""构造为string,性质为左值
String s2(s1);
String s3(move(s2)); //这里将s2转换为将亡值,移动拷贝
String s4(f("右值,将亡值")); //移动拷贝

String s5(s1);
s5 = s1;
s5 = f("rightval-dyingv"); //tmp的临时拷贝对象就是将亡值
//在string的标准库中同样有右值引用,类似的list,vector等

String s6 = s1 += s3;//拷贝构造
String s7 = s1 + s2; //移动构造

//右值引用的应用场景在于深拷贝大型类,会导致大量效率的浪费
//eg: vector<vector<int>> ...
return 0;
}

3.emplace_back


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
int test2() //emplace_back
{
//像vector,list等的插入基本都是两个重载实现,一个左值引用,一个右值引用
vector<string> v;
string s1("left val");
int val = 123;
v.push_back(s1);
v.push_back("right val"); //右值引用
v.push_back(to_string(val));//右值引用


//对于emplace_back
//template<class Arg>
//void emplace_back(Arg && arg); //emplace_back只有右值引用,但由于模板的存在(模板的可变参数),可以传左值引用.

v.emplace_back(s1); //实际上并没有对其进行右值引用处理
v.emplace_back(move(s1)); //此时才有右值处理
v.emplace_back("right val"); //右值引用
//综上发现,emplace_back和push_back用法和作用完全一样,没有孰优孰劣

//但是emplace_back的特征在于以下
vector<pair<string, string>> vp;
pair<string, string> kv("left v", "left v");

vp.push_back(make_pair("right v", "right v"));
vp.emplace_back(make_pair("right v", "right v"));
vp.push_back(kv);
vp.emplace_back(kv);

vp.emplace_back("right v", "right v"); //体现emplace_back模板的可变参数特点的地方(template)

return 0;
}

4.完美转发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void fun(int& x) { cout << "leftv ref" << endl; }
void fun(int&& x) { cout << "rightv ref" << endl; }
void fun(const int& x) { cout << "const leftv ref" << endl; }
void fun(const int&& x) { cout << "const rightv ref" << endl; }

template<typename T>
void PerfectFoward(T&& t)
{
//fun(t); //右值引用会在第二次之后参数传递过程中属性丢失,所以结果并不为我们所想
fun(std::forward<T>(t)); //加入forward关键字即可保留其属性,完美转发解决
}

int test3()
{
PerfectFoward(10); //rightv ref
int a;
PerfectFoward(a); //leftv ref
PerfectFoward(std::move(a)); //rightv ref

const int b = 1;
PerfectFoward(b); //const leftv ref
PerfectFoward(std::move(b)); //const rightv ref
return 0;
}

五、lambda表达式


lambda表达式的轮廓为

1
2
[capture - list] (parameters) mutable -> return-type {statement}
捕捉列表 参数列表 取消常性 返回类型 函数体
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
int add1(int a, int b)
{
return a + b;
}
int main()
{
//最简单的lambda表达式 : []{}
int a = 1, b = 2;

auto add1 = [](int x, int y)->int {return x + y; };
add1(a, b); //调用的是lambda表达式(匿名函数)
::add1(a, b); //调用的是顶上的函数

//auto add2 = [=]() {return a + b; }; //=代表捕捉作用域内所有的值,同样的,&捕捉作用域内所有数据的引用
auto add2 = [a, b]() {return a + b; }; //捕捉到a,b , 返回类型其实会自动推导,可以不写
add2();

auto add3 = [=]() {return add1(a, b); }; //匿名函数调用匿名函数实现
auto add4 = [=]() {return ::add1(a, b); };//匿名函数调用函数实现


auto swap1 = [&]() {int x = a; a = b; b = x; };
swap1();
cout << a << b << endl;

auto swap2 = [&a,&b]() {int x = a; a = b; b = x; }; //捕捉的引用,而不是地址
swap2();
cout << a << b << endl;

auto swap3 = [](int& x,int& y) {int z = x; x = y; y = z; };
swap3(a,b);
cout << a << b << endl;


//mutable
auto test = [a, b]()mutable{int c = a, a = 20, b = c; };
test();
//默认情况下,lambda函数总是const函数,mutable可以取消其常量性,使用该修饰符时,参数列表不可省略(即使参数为空)

//lambda表达式可以作为仿函数(其原理就是仿函数的operator()成员)(匿名函数)
int arr[] = {1, 5, 4, 23, 5, 76, 5, 342, 4};
sort(arr, arr + sizeof(arr) / sizeof(arr[0]), [](auto a, auto b){return a > b; }); //默认升序,改为降序
sort(arr, arr + sizeof(arr) / sizeof(arr[0]), std::greater<int>()); //默认升序,改为降序
for (auto& e : arr)
cout << e << " ";
cout << endl;

//底层还是依靠仿函数来实现
//也就是定义lambda表达式,实际上是编译器会生成一个lambda_uuid类,仿函数的operator() 的参数和实现就是我们写的lambda
return 0;
}

六、异常


1.异常简介

传统处理错误的方式有: 1.返回错误码 2.终止程序:eg:exit()
其缺点是: 1.拿到错误码,需要查找错误码表才知道具体错误
2.如果一个函数是通过返回值拿数据,发生错误很难处理(让传一个自定义类型,却传了个-1之类的)
3.如果调用的函数栈很深,一层层返回错误码,处理很复杂

异常的优缺点:
优点:
1.清晰的包含错误信息
2.面对 T operator[](int i)这类函数越界错误,异常可以很好的解决
3.多层调用时,里面发生错误,不再需要层层处理,最外层直接捕获即可
4.很多第三方库都是用异常,使用异常可以更好的使用他们.eg:boost,gtest.gmock

缺点:
1.异常会导致执行流乱飞,会使得调试分析程序bug带来一些困难
2.异常可能导致资源泄露等异常安全问题,要学会RAII来解决
3.C++库里的异常体系不好用,常自己定义
4.C++异常语言可以抛任意类型异常,项目若没有规范管理,会非常混乱

总的来说,异常利大于弊,实际小项目不使用


2.异常的基本使用

STL库中有的自带抛异常

try和catch必须带有花括号,否则无法识别
如果try了没有catch到,程序会直接终止

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main() //STL中自带抛异常
{
try
{
vector<int> v{ 1,2,3,4 };
for (size_t i = 0; i <= v.size(); ++i)
{
//cout << v[i] << " ";
cout << v.at(i) << " "; //自带的抛出异常
}
cout << endl;
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
int Div(int m, int n)
{
if (n == 0) //除零错误
{
throw string("/0 error"); //throw 可以抛出任意类型的对象
//直接跳转到catch匹配的地方
}
return m / n;
}

int Fun1(int m, int n)
{
//异常是可以截胡的,比如
try
{
return Div(m, n);
}
catch (const string& err)
{
cout << __LINE__ << err << endl;
}

//return Div(m, n);
}

int main()
{
int m, n; cin >> m>> n;
try
{
cout << Fun1(m, n) << endl;
}
catch (const string& err)
{
cout << __LINE__ << err<< endl;
}
catch (...) //当throw的类型匹配不上以上的catch,就会都跳转到这里,避免异常没捕获时程序直接终止
{
cout << "none matched catch" << endl;
}
return 0;
}

异常的底层逻辑

首先检查throw本身是否在try内部,如果是再查找匹配的catch.
没有则退出当前函数栈,继续在调用函数的栈中寻找匹配的catch
如果main函数的栈中还没有,则终止程序


3.异常的重新抛出,自定义异常类

异常是很危险的,非常容易导致内存泄漏

,throw后直接跑到catch那里不再回来了,导致throw后可能有的释放内存没有进行

解决的方法就是让异常重新抛出
一个实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
class Exception
{
public:
Exception(const char* errmsg,int errid)
:_errid(errid)
,_errmsg(errmsg)
{}

virtual string what() = 0; //纯虚函数,构成多态

protected:
int _errid; //错误码
string _errmsg; //错误信息
};

class SqlException : public Exception
{
public:
SqlException(const char* errmsg, int errid)
:Exception(errmsg, errid)
{}

virtual string what()
{
return "sql error : " + _errmsg;
}
};

class NetworkException : public Exception
{
public:
NetworkException(const char* errmsg, int errid)
:Exception(errmsg,errid)
{}

virtual string what()
{
return "internet error : " + _errmsg;
}
};


void ServeStart()
{
//if (rand() % 6 == 0)
// throw SqlException("sql open error", 1);
//if (rand() % 7 == 0)
// throw NetworkException("internet connect error", 3);
//cout << "sucessful" << endl;

//模拟一下出现问题抛异常报错
int* arr = new int[10];

try
{
if (rand() % 6 == 0)
throw SqlException("sql open error", 1);
if (rand() % 7 == 0)
throw NetworkException("internet connect error", 3);
cout << "sucessful" << endl;
}
catch (const Exception& err) //如果抛出异常
{
delete[] arr;
throw; //重新抛出异常,以正确输出所需内容
//截胡处理一下,再把路让出来过去
}
delete[] arr;
}

int main() //自定义异常类(继承体系)
{
for (size_t i = 0; i < 10; ++i)
{
try
{
ServeStart();
}
catch (Exception& err)
{
cout << err.what() << endl;
}
catch (...)
{
cout << "catch error" << endl;
}
}
return 0;
}

4.异常规范

throw(A,B)
函数会抛出A,B类型某个类的异常
throw(A)
函数只会抛出A类型异常
throw()
函数不会抛出异常
noexcept
函数不会抛出异常

void* operator new(std::size_t size) throw(std::bad_alloc); //表示这个函数只会抛出bad_alloc异常
void* operator delete(std::size_t size, void* ptr) throw(); //表示这个函数不会抛出异常
void* operator delete(std::size_t size, void* ptr) noexcept; //表示这个函数不会抛出异常


七、类型转换


1.隐式类型转换和显示类型转换

隐式类型转换:相近类型,也就是意义相似的类别之间的转换
显示类型转换:不相近类型,意义差别很大的类型的转换

1
2
3
4
5
6
7
8
int i = 1;
double d = 3.33;
i = d; //C语言支持相近类型的隐式类型转换(相近类型,也就是意义相似的类别)
cout << i << endl;

int* p = nullptr;
p = (int*)i; //C语言支持不相近类型强制类型转换(不相近类型,意义差别很大的类型)
cout << p << endl;

2.C++11中的类型转换操作符

1
2
3
4
static_cast 		//隐式类型转换
reinterpret_cast //强制类型转换
const_cast //强制类型转换,并去掉const属性
dynamic_cast //父子类转换

①static_cast,reinterpret_cast

1
2
3
4
5
6
7
8
9
10
11
12
13
int main()
{
int i = 1;
double d = 3.33;
int* p = nullptr;

d = static_cast<double>(i); //对应C的隐式类型转换(相近类型)
p = reinterpret_cast<int*>(i); //对应C的强制类型转换(不相近类型)

//如果用错操作符是没办法编译通过的
//d = reinterpret_cast<double>(i);
//p = static_cast<int*>(i);
}

②const_cast

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main()
{
//const int ci = 10;
volatile const int ci = 10;
int* pi = const_cast<int*>(&ci); //强制类型转换,并去掉const属性
*pi = 20;
cout << *pi << endl; //20
cout << ci << endl; //10

//为什么监视窗口中(内存中)得到的ci是20,但是输出却是10呢?
//ci的内存中存储的值确实被改成了20,但是ci放进了寄存器,这里去寄存器中取,还是10
//本质上是编译器对const对象存取优化机制导致
//可以加入volatile关键字去内存中取值,可以禁止编译器的优化
}

③dynamic_cast

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class A
{
public:
virtual void Fun()
{}
int _a;
};

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

void Fun_cast(A* pa)
{
//如果想区分pa是指向父类还是子类
//pa指向子类对象则转换成功
//pa指向父类对象就转换失败,返回nullptr
B* pb = dynamic_cast<B*>(pa);
//B* pb = (B*)pa; //只会根据情况成功或 失败(程序崩溃)
if (pb != nullptr)
{
cout << "translated pa->parent" << endl;
pb->_a = 10;
pb->_b = 20;
}
else
{
cout << "translat error" << endl;
}
}

void main()
{
A a;
B b;

//子类对象可以赋值给父类的对象、指针、引用,这个过程叫做切片,这是天然支持的,称为 向上转换
//父类的对象、指针、引用传给子类的指针...,这个过程叫做向下转换,根据实际情况有可能成功

A* pa = &a;
Fun_cast(pa); //pa指向父类

pa = &b;
Fun_cast(pa); //pa指向子类

//dynamic_cast向下转换只能针对继承中多态类型(父类必须含有虚函数)
//这就涉及到 dynamic_cast 的原理: 其如何实现识别父类子类的呢?
//dynamic_cast通过去虚表上方存储的标识信息,来判断指向的是父类还是子类
}

④explicit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class AA
{
public:
explicit AA(int a) //加入explicit可以阻止隐式转换地构造
{
cout << "AA(int a)" << endl;
}

AA(const AA& a)
{
cout << "AA(const AA& a)" << endl;
}

private:
int _a;
};


void test5()
{
AA a1(1); //构造

//隐式转换 : AA tmp(1) -> AA a2(tmp) 先构造再拷贝构造被优化为只有构造,优化取决于编译器
//以防有些没有优化的编译器导致的低效,加入explicit可以阻止这种隐式转换
//AA a2 = 1;
}

八、IO流


1.IO流 简单的使用

文件打开方式 作用
ios::in 为读文件打开文件
ios::out 为写文件打开文件
ios::ate 初始位置:文件尾
ios::app 追加方式写文件
ios::trunc 如果文件存在先删除,再创建
ios::binary 二进制方法

ofstream 写操作
ifstream 读操作
fstream 读写操作

文本文件写文件

1
2
3
4
5
6
7
8
9
#include<fstream>
int main()
{
ofstream ofs; //其实前两行可以合并为 ofstream ofs("文件.txt",ios::out);
ofs.open("文件.txt",ios::out);
ofs<<"内容"<<endl;
ofs.close();
return 0;
}

文本文件读文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include<fstream>
int main()
{
ifstream ifs("文件.txt",ios::in);
if(!ifs.is_open())
{
cout<<"文件打开失败"<<endl;
exit(-1);
}
//读数据 多种方法
//1
char buf[1024] = {0};
while(ifs>>buf)
cout<<buf<<endl;
//2
char buf[1024] = {0};
while(ifs.getline(buf,sizeof(buf)))
cout<<buf<<endl;
//3
string buf;
while(getline(ifs,buf))
cout<<buf<<endl;
//4
char c;
while((c=ifs.get()) != EOF)
cout<<c;
ifs.close();
return 0;
}

二进制写文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<fstream>
class Person
{
public:
char name[64];
int age;
};
int main()
{
ofstream ofs; //ofstream ofs("文件.txt",ios::out | ios::binary);
ofs.open("文件.txt",ios::out | ios::binary); //二进制方法写文件

Person p = {"name",18};
ofs.write((const char*)&p,sizeof(Person));
ofs.close();
return 0;
}

二进制读文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<fstream>
class Person
{
public:
char name[64];
int age;
};
int main()
{
ifstream ifs("文件.txt",ios::in | ios::binary);
if(!ifs.is_open())
{
cout<<"文件打开失败"<<endl;
exit(-1);
}
Person p;
ifs.read((char*) &p,sizeof(Person));
cout<<"name:"<<p.name<<"age" << p.age<<endl;
ifs.close();
return 0;
}

其他写法读写文件

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<fstream>
int main()
{
ofstream ofs("text.txt"); //fopen( ,"W");
ofs.put('x'); //fputc
ofs.write("jack", 5); //fwrite
ofs.close(); //fclose

ifstream ifs("text.txt"); //fopen( , "R");
//cout << (char)ifs.get() << endl; //fgetc
cout<<ifs.rdbuf(); //输出文件缓冲区外所有内容,如果前面已经输出了,就不再输出了
//ifs.read //fread
}

2.读写结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include<fstream>
struct Msg
{
string name;
int age;
};

int main()
{
//使用起来和cin cout类似
Msg per1{ "jack",18 };

ofstream ofs("text.txt");
ofs << per1.name << endl; //要给上换行或者空格,否则在读文件时会无法分割
ofs << per1.age << endl;
ofs.close();

Msg per2;
ifstream ifs("text.txt");
//ifs >> per2.name;
//ifs >> per2.age;
cout << ifs.rdbuf();
}

3.序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include<sstream>
struct Msg
{
string name;
int age;
};
int main()
{
Msg per1{ "jack",18 };
//序列化字符串
ostringstream ost;
ost << per1.name << endl;
ost << per1.age << endl;

string str1 = ost.str();
cout << str1 << endl; //网络中可以把这个str1发送给另一端

//网络另一端接收后就可以开始解析数据
//反序列化
istringstream ist;
Msg per2;
ist >> per2.name;
ist >> per2.age;
}