C++ primer 学习笔记

C++ primer 分为四个部分

C++基础、C++标准库、类设计者的工具、高级主题

 

第一部分、C++基础

 

第二章:变量和基本类型

2.1、基本内置类型:

(1)算数类型:(bool 、 void 、int 、 short 、 long 、 double 、  char 、 float 、 undefined )

(2)有符号及无符号

1、未定义(undefined):引发难以追踪的运行时的错误、安全问题、移植性问题

2、有无符号类型混用:表达式中同时包含带符号和无符号类型,带符号数会自动转为无符号数

3、整型字面值:十进制字面值默认为带符号数,是int/long/long long 中能容纳其值且尺寸最小。八进制(0开头)和十六进制(0x开头)则是int/unsigned int/unsigned long/long long/unsigned long long中尺寸最小者

4、字符串字面值:编译器在每个字符串结尾添加空字符'\0’(算大小)

5、字符前缀与类型:u(char16_t),U(char32_t),L(wchar_t),u8(char)

6、泛化的转义序列:\x后跟十六进制数字,或\后跟1到3位八进制数字,可像普通字符一样使用

 

2.2、变量(初始化、定义、声明)

1、初始化:定义在任何函数之外的变量被初始化为0,每个类决定各自的初始化对象方式。而定义在函数体内部的内置变量类型将不初始化,试图访问未初始化的值将引发未定义行为

2、定义:负责创建与名字关联的实体,并申请存储空间和赋初始值

3、声明:规定变量的类型和名字,只声明不定义可在变量名前添加extern并不显式初始化,包含显式初始化即为定义

 

2.3、复合类型(引用、指针)

1、引用:为对象起的别名,定义时必须初始化,不是对象,不可定义引用的引用

2、指针:指向某个对象,定义时无需赋值,本身就是对象,对指针使用解引用符*可访问该对象

3、指针值状态:指向一个对象、指向对象的下一个紧邻空间位置、空指针、无效指针(访问或拷贝都会出错)

4、空指针(nullptr):不指向任何对象,可以转换成任何其他的指针类型

5、初始化指针:建议用已定义对象或nullptr或0初始化所有指针,把任何int型变量(即使值为0)赋值给指针是错误的

6、void*指针:可存放任意对象的地址,但不能直接操作其所指对象

7、引用/指针类型匹配:除两种特例(const可绑定非const,基类可绑定派生类)外,指针和引用的类型都需要与之绑定的对象严格匹配

8、复合类型判断:从右向左阅读复杂的指针或引用的声明语句,离变量名越近的符号对变量的类型有越直接的影响

9、多文件共享const:不管声明还是定义都要添加关键字extern

10、初始化对const的引用:允许任何表达式作为初始值,只要该表达式结果可以转化为引用类型的临时量对象

11、指向常量的指针:自觉不去改变所指对象的值,而该对象若不是常量对象则其值通过其他方式改变

 

2.4、const限定符

1、const指针:必须初始化且不能修改,书写上直接在变量名之前,表示不变的是指针本身而非指针指向的对象

2、顶层/底层const:顶层const表示本身是常量,底层const表示绑定的对象是常量;执行拷贝操作时,拷入拷出对象必须具有相同的底层const资格,或能够强制转换

3、常量表达式:数据类型和初始值都需要是常量类型,值不会改变并在编译过程就能得到计算结果

4、constexpr变量:一定是常量,必须用常量表达式(字面值类型,包括算术类型、引用、指针)或constexpr函数(足够简单编译时可计算结果)初始化

5、constexpr指针:初始值是nullptr或0,或存储于某个固定地址中的对象

6、指针、常量与类型别名:typedef char *pstring; const pstring cstr = 0;,与const char *cstr不等价,前者的cstr是指向char的常量指针,后者中cstr是指向常量char的指针

 

2.5、处理类型(auto、decltype)

1、类型说明符auto:让编译器通过初始值推算变量类型,并赋诸该值;

2、类型指示符decltype:从表达式的类型推断出要定义的变量的类型

       decltype (f()) sum =x;

3、预处理器:在编译前执行的一段程序,功能有替换#include的头文件,以及头文件保护符避免重复定义实体

4、预处理变量:#define将一个名字设置为预处理变量,#ifndef及#ifdef则判断名字是否已被定义过,无视作用域规则

 

2.6、自定义数据结构(自己定义类)

 

 

 

第三章:字符串、向量和数组

3.1、字符串

1、using声明:每个using声明引入命名空间的一个成员;头文件中的代码一般不应使用using声明

2、string初始化:用数字和字符初始化,则string对象内容是将给定字符连续重复给定次数得到的序列

3.2、向量

3.3、数组

3.4、迭代器的介绍

 

 

第四章:表达式

1、左值右值:左值用的是对象的身份,右值用的是对象的值

2、整数相除:商向0取整;取余时m%(-n)=m%(n),-m%n=-(m%n)

3、真值测试:比较运算中除非比较对象是bool型,否则不使用布尔字面值

4、复合赋值运算符:只求值1次,普通运算符需要2次(右边表达式和赋值)

5、移位运算符:右侧的移动位数必须非负且小于结果的位数;移出位被舍弃,符号位视机器而定

6、sizeof运算符:可使用作用域来获取类成员大小,可使用无效的指针获取指针指向的对象所占空间,不会把数组当指针处理,对string或vector返回固定部分的大小;sizeof的返回值是常量表达式

 

第五章:语句

5.1、条件语句(if 、 switch)

1、switch语句:对括号内表达式求值转换为整数类型,然后与case后的每个常量表达式比较

2、default标签:当其他case都不满足时执行,位置自由,若为空也必须跟上一个空语句( ;)或空块({})

5.2、迭代语句(while 、 for 、 do while)

1、范围for语句:语法形式是for (declaration: expression) statement;expression表示一个可以返回迭代器的begin()和end()的序列,不允许增删容器元素改变范围for中预存的end()值

5.3、跳转语句(break、continue、goto)

1、控制流跳转:在switch和goto中,不允许跨过变量的初始化语句直接跳转到该变量作用域内的另一个位置

5.4、try 语句块和异常处理(throw、try)

异常是指存在于运行时的反常行为,这些行为超出了函数正常功能的范围。典型的异常包括市区数据库连接以及遇到意外输入等。处理反常行为可能是涉及所有系统最难的一部分。

C++ primer 学习笔记_第1张图片

1、异常:throw引发异常,try中跑出的异常通过会被某个catch处理,在throw与catch间通过异常类传递信息

2、寻找异常处理代码:寻找异常处理代码的过程与函数调用相反,逐层回退直到找到适当类型的catch子句;若最终未找到则会转到terminate的标准库函数,全无try语句的程序会直接调用terminate

3、异常安全:在异常发生期间,正确执行了”清理“工作的程序被称作异常安全的代码。

4、异常类:(用于报告标准库函数遇到的问题)头文件exception中的exception类、stdexcept中定义了几种常见的异常类、new中的bad_alloc、type_info中的bad_cast

 

第六章:函数

6.1、函数基础

1、局部静态对象:不同于只存在于块执行期间的自动对象,局部静态对象直到程序终止才被销毁;没有显式初始值时会执行值初始化

2、函数声明:函数可以只能声明一次,但可以定义多次,如果一个函数永远不会被用到,可以只声明不定义

 

6.2、参数传递

1、const参数:实参初始化形参时会忽略顶层const,所以有无顶层const版本等价,都定义则属于重复定义

2、数组实参:不允许拷贝数组,传递数组时实际上是传递指向首元素的指针;不同大小的数组是不同类型;若形参是引用,数组不会转换为指针

3、传多维数组:数组第二维(以及后面所有维度)的大小都是数组类型的一部分,不能省略

4、initializer_list:编写能处理不同数量实参的函数,实参数量未知但类型都相同,initializer_list可作形参;提供的操作类似容器,但对象中的元素永远是常量(C++ 11)

C++ primer 学习笔记_第2张图片

 

6.3、返回类型和return语句

1、返回引用:不要返回局部对象的引用或指针,调用一个返回引用的函数得到左值

2、列表初始化返回值:函数可以返回花括号包围的值的列表;若返回为内置类型,则花括号内至多一个值,而且所占空间不应大于目标类型空间

3、主函数main的返回值:当控制达到了main函数的结尾处而且没有return 语句,编译器将隐式的插入一条返回0的return语句。

返回数组指针:形如Type (*function(parameter_list))[dimension],函数返回类型是指向大小为dimension的数组的指针

 

6.4、函数重载

1、函数参数:实参是函数中形参的初始值,存在对应关系,但并没有规定实参的求值顺序

2、main函数:不能调用自己,不能重载

3、const_cast和重载:常用const_cast修改const参数属性的性质,将const和非const版本的重载函数关联

C++ primer 学习笔记_第3张图片

4、形参默认值:被赋予了默认值的形参,后面所有形参都必须有默认值;在给定的作用域中一个形参只能被赋予一次默认值;可多次声明函数给不同形参赋予默认值

 

6.5、特殊用途的语言特性(默认实参、内联函数、constexpr)

1、默认实参:在函数的多次调用中它们都被赋予一个相同的值,此时,我们把这个反复出现的值称为函数的默认实参

2、内联函数:一般只有当函数规模小、流程直接、无递归时,内联函数的请求才会被编译器接受

3、constexpr函数:能用于常量表达式的函数,但返回值可以是非常量;返回和形参类型都是字面值类型,函数只执行一句return;被隐式指定为内联函数;

4、内联定义:和其他函数不同,内联和constexpr函数可以多次定义,但每次必须完全一致,通常定义在头文件中

 

6.6、调试帮助

5、预处理宏assert:根据提供的表达式判断是否要输出错误信息并终止程序,可定义预处理变量NDEBUG禁用assert的效果

6、NDEBUG:可在#ifndef NDEBUG和#endif之间编写自己的调试代码;有5个编译器预定义的名字变量

__func__/__FILE__/__LINE__/__TIME__/__DATE__,用于输出调试信息

C++ primer 学习笔记_第4张图片

 

 

第七章:类

7.1、定义抽象数据类型

1、数据抽象:定义数据成员和函数成员,抽象数据类型依赖封装

2、封装:分离接口(用户所能执行的操作)和实现(数据成员、实现接口的函数体、私有函数)

3、成员函数:声明必须在类内部,但定义可在类内或外

4、this:成员函数通过一个名为this 的额外的隐式参数来访问调用它的那个对象。当我们用一个成员函数时,用请求该函数的对象地址初始化this。

5、常量成员函数:参数列表后带const的函数叫常量成员函数,它可将this设置为指向常量的指针,使得常量对象可访问常量成员函数,但不可写入新值;若以引用形式返回*this,则返回类型是常量引用

6、合成的默认构造函数:会尽可能使用类内初始值初始化数据成员,当类不存在任何构造函数时编译器创建的默认构造函数;既需要其他形式又需要合成的默认构造函数,可在默认构造函数的参数列表后面加上= default

7、类的动态内存:当类需要分配类对象之外的资源时,合成的版本常常会失效;需要动态内存的类应该使用vector或string对象管理存储空间,避免分配和释放内存带来的复杂性

 

 

7.2、访问控制和封装

1、class和struct:唯一区别是默认的访问权限,class为private,struct为public

2、友元:允许其他类或函数访问非公有成员,则可在类的开头用friend关键字将其声明为友元;友元的声明仅仅限定了权限,可在真正的函数声明之后;友元函数可以定义在类的内部,但在类的外部仍然需要提供相应的声明使得函数可见

4、可变数据成员:添加mutable关键字的数据成员,即使在const成员函数中,依旧可以被改变

5、类内初始值:必须使用符号或花括号的直接初始化形式

6、重载const:根据是否是常量对象,决定调用是否是常量版本的成员函数

 

7.3、类的其他特性

1、类的声明:仅声明不定义叫做前向声明:类在声明之后、定义完成之前是不完全类型,可以定义其指针或引用,作为函数的参数或返回值;只有当类全部完成后类才算被定义,所以类的成员中不能包含自己,但可以出现指针或引用

2、类的定义:编译所有成员的声明之后才会编译函数体

3、类型名的定义:内层作用域可以重新定义外层作用域的名字,但若外层作用域中的某个名字表示类型,则类中不能再重新定义该名字

4、访问类成员:使用this->或类名::可强制访问类成员,无视作用域的名字查找规则;无类名加::表示全局对象

 

7.4、构造函数再探

3、默认构造函数:当对象被默认初始化或值初始化时,自动执行默认构造函数;不那么明显的一种情况是类的某些数据成员缺少默认构造函数

4、默认初始化场景:无任何初始值定义非静态变量或数组,本身含有类成员且使用合成的默认构造函数,类类型成员没有在构造函数初始值列表中显式初始化

6、默认构造对象:使用默认构造函数定义对象的格式为类名+对象名,无之后空的圆括号对,否则将定义为函数而非对象

7、类类型转换:构造函数只接受一个实参,则其定义了一种从构造函数的参数类型到类类型的隐式转换规则;编译器只会自动执行一步类型转换

8、显式构造函数:在类内添加explicit关键字的构造函数只能用于直接初始化,抑制隐式转换;编译器不会在自动转换过程中使用它,但可用于显式的强制转化

9、聚合类:有以下几点称之为聚合类:(所有成员public、未定义构造函数、无类内初始值、无基类或virtual函数)

使用花括号初始化,花括号内的初始值顺序与声明顺序一致;

 

10、字面值常量类:数据成员都是字面值类型的聚合类;或数据成员都是字面值类型、至少含有一个constexpr构造函数、类内初始值是常量表达式或拥有自己的constexpr构造函数、使用析构函数的默认定义

11、constexpr构造函数:为保证构造函数的不包含返回语句,和constexpr函数唯一可执行语句即返回函数,其函数体一般为空

 

7.5、类的静态成员

12、静态成员:独立于任何对象之外,不包含this指针;静态成员函数不能声明为const,也不能在内部使用this指针

C++ primer 学习笔记_第5张图片

13、静态数据成员:必须在类的外部定义和初始化,方式和类外部定义成员函数类似;

若静态数据成员为constrexpr类型,且其只限于编译器替换值的使用场景,则可以在类内定义,否则必须在类外再重新定义

14、静态成员的优势:类内可包含自身类型的静态数据成员,但普通成员只能是指针或引用;

静态成员可以作为默认实参,非静态数据成员的值属于对象的一部分,不能作为默认实参

C++ primer 学习笔记_第6张图片

 

 

第二部分、C++标准库

第八章:IO库

8.1、IO类

(1)  IO库类型和头文件

C++ primer 学习笔记_第7张图片

 "如果程序崩溃,输出缓冲区不会被刷新"

(2)缓存刷新的原因

1)程序正常结束
2)缓冲区满
3)endl
4)用unitbuf。默认情况下,对cerr是设置unitbuf,因此写到cerr的内容是立即刷新的
5)一个输出流可能被关联到另一个流,关联到的流的缓冲区会被刷新。

 

8.2、文件输入输出

(1)文件模式

     以out的方式打开文件会丢弃掉已有的数据

     每次调用open时都要确定文件模式

8.3、string 流

1)iostream 处理控制台IO
2)fstream处理命名文件IO
3)stringstream处理内存stringIO
并且fstream和stringstream都继承自类iostream。

 

 

第九章:顺序容器

 

9.1、容器库概览

(1)除了固定大小的array外,其他容器都提供高效、灵活的内存管理

(2)string 和vector 将元素保存在连续的内从空间中

(3)list 和 forward_list 可以令容器的添加和删除更加快速

(4)deque支持快速随机访问,两端增加删除快,但是中间位置添加删除成本很高

(4)新版本标准库的容器比旧版本便准库快得多

C++ primer 学习笔记_第8张图片

 

选择容器的要求:

C++ primer 学习笔记_第9张图片

 

9.2、顺序容器操作

C++ primer 学习笔记_第10张图片

C++ primer 学习笔记_第11张图片

C++ primer 学习笔记_第12张图片

 

9.3、vector对象时如何增长的

C++ primer 学习笔记_第13张图片

size是指容器已经保存的元素的数量
capacity是在不分配新的内存空间的前提下最多可以保存多少元素。

 

9.4、额外的String操作

C++ primer 学习笔记_第14张图片

(1)append 和replace函数

append 操作时在string 末尾进行插入操作的一种简写形式:

replace操作时调用erase 和insert 的一种简写形式:

C++ primer 学习笔记_第15张图片

 

(2)string 搜索操作

find函数完成最简单的搜索。它查找参数指定的字符串,找到则返回第一个匹配位置的下标,否则返回npos:

 

(3)compare 函数

C++ primer 学习笔记_第16张图片

 

第十章:泛型算法

10.1、初识泛型算法

(1)标准库容器定义了很少的操作(添加、删除元素,访问首尾元素等),有更多的操作(如查找特定元素、重排元素等)需要通过算法实现。迭代器令算法不依赖于容器,但算法依赖于元素类型的操作

(2)算法:find、count、accumulate、equal、fill、replace、sort、unique、stable_sort、for_each、transform

fill 算法:

fill接受一对迭代器表示一个范围,还接受一个值作为第三个参数,将第三个参数给定的值赋予输入序列中的每个元素。

unique算法:(将相邻的重复项删除)

C++ primer 学习笔记_第17张图片

C++ primer 学习笔记_第18张图片

 

10.2、定制操作(待完成)

(1)lambda表达式:

 

10.3、泛型算法结构(待查看,看讲不讲)

迭代器类别

输入迭代器                只读,不写;单遍扫描,只能递增

输出迭代器                只写,不读;单遍扫描,只能递增

前向迭代器                可读写,多遍扫描,只能递增

双向迭代器                可读写,多遍扫描,可递增递减

随机访问迭代器         可读写,多遍扫描,支持全部迭代器运算

 

 

第十一章:关联容器

11.1、关联容器概述及其操作

(1)关联容器支持高效的关键字查找和访问。

主要的关联容器是:map 和 set ,map是键值对,而set是光只有键

关联容器不支持顺序容器的操作,原因我们通过键值来访问,这些操作对关联容器没有意义。

关联容器的迭代器都是双向的。

(2)使用map:

C++ primer 学习笔记_第19张图片

(3)使用set:

C++ primer 学习笔记_第20张图片

 

 

11.2、无序容器

c++11定义了新的4个无序关联容器,这些容器不是使用比较运算符来组织元素的,而是使用一个hash函数和关键字类型==运算符。

(1)unordered_map:用hash函数组织的map 
(2)unordered_set:用hash函数组织的set 
(3)unordered_multimap:hash组织的map:关键字可重复出现
(4)unordered_multiset:hash组织的set:关键字可重复出现

 

 

第十二章:动态内存

12.1、动态内存与智能指针

(1)shared_ptr类:

1.智能指针也是模板,定义方式一样
2.make_shared<>最安全的分配和调用方法

auto p=make_shared>();//指向一个动态分配的空vector

3.引用计数

auto r=make_shared(42);
r=q;//递增q指向的对象的引用计数;递减r指向的引用计数

 

(2)直接管理内存

1.使用new动态分配和初始化对象

int *p1=new int;//p指向未初始化int对象
int *p2=new int(2);//指,向2
string *s=new string(9,'9');//指向"9999999999"
vector *pv=new vector{1,2,3};

2.使用auto,只有括号仅有单一初始化器才能使用

auto p=new auto(obj);//p指向obj类型
auto p=new auto{a,b,c};//错误

3.动态分配const

const int *pci=new const int(1024);
const string *psc=new const string;

4.内存耗尽

int *p2=new (nothrow) int;//如果分配失败,new返回空指针,bad_alloc和nothrow都定义在new头文件中

5.使用new和delete三个常见问题

  • 忘记delete内存,导致内存泄漏
  • 使用已经释放掉的内存
  • 同一块内存释放两次

(3)智能指针和异常

防止智能指针陷阱规范

  • 不使用相同的内置指针初始化多个智能指针
  • 不delete get()返回的指针
  • 不使用get()初始化或reset另一个智能指针
  • 如果使用了get(),记住当最后一个对应的智能指针销毁后,你的指针就变为无效了
  • 如果使用只能指针管理的资源不是new分配的内存,记住传递给它一个删除器

 

12.2、动态数组

 

 

12.3、使用标准库:文本查询程序

 

 

第三部分、类设计者的工具

第十三章:

13.1、

13.2、

13.3、

 

第十四章:

14.1、

14.2、

14.3、

 

第十五章:面向对象的程序设计

OOP概述:(面向对象程序设计Object-oriented Programming)

核心思想:数据抽象、继承和动态绑定

(1)数据抽象:类的接口与实现分离(见第七章)

(2)继承:

(3)动态绑定:

          分解为四个概念:对象的静态类型、对象的动态类型、静态绑定、动态绑定

          动静态类型这两个概念一般发生在基类和派生类之间

1、对象的静态类型(static type):就是它在程序中被声明时所采用的类型(或理解为类型指针或引用的字面类型),在编译期确定;

2、对象的动态类型(dynamic type):是指“目前所指对象的类型”(或理解为类型指针或引用的实际类型),在运行期确定;

           动静态绑定只有当编译器产生的代码直到运行时才能确定应该调用哪个版本的函数

3、静态绑定(statically bound):又名前期绑定(eraly binding),绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译期;

4、动态绑定(dynamically bound):又名后期绑定(late binding),绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行期;

 

15.1、定义基类和派生类

(1)基类对函数将会分成两种:(基类的函数被派生类覆盖)(派生类直接继承不进行覆盖)

(2)基类通过对其成员函数加上关键字virtual,使得该函数执行动态绑定,

15.2、虚函数

1.只有当我们使用基类的引用或指针调用一个虚函数时会执行动态绑定,动态绑定的对象依赖于绑定的对象;
2.派生类可省略virtual;

final关键字:定义了final关键字的函数,之后任何尝试覆盖该函数的操作都会引发错误

 

15.3、抽象基类

 

15.4、访问控制与继承

(public 、 private 、 protected )

15.5、继承中的类作用域

15.6、构造函数与拷贝控制

15.7、容器与继承

15.8、文本查询程序再探

 

 

 

第十六章:模板与泛型编程

16.1、定义模板

1、函数模板

模板成宿应该尽量减少对实参类型的要求。

(1)类型模板参数:

template 
void test(T i, U j)
{
}

(2)非类型模板参数:

表示一个值,而非类型

template 
int compare(const char (&p1)[M], const char(&p2)[N])
{
    return strcmp(p1, p2);
}

compare("test", "abc");    // test被转换成M=5,abc被转换成N=4 p1就是test数组的引用,p2就是abc数组的引用

实例出如下版本:

int compare(const char (&p1)[5],const char (&p2)[4])

算上 \0 字符串结束标志

(3)编译参数模板会在这三个阶段出现错误报告

*、编译模板本身时:检查语法是否有错误

*、编译器遇到模板使用时:参数数目、参数类型是否匹配

*、模板实例化时:检查类型是否有实例化

Sales_data data1,data2;

cout << compare(data1,data2)<

(先得实例化了类型才能使用)

 

2、类模板

编译器从模板类中实例化出一个类时,它会重写类模板,将模板参数T的每个实例替换为给定的模板实例。

一个类模板的成员函数只有当程序用到它时才进行实例化,所以下面的T,虽然是在一个类中,但是都代表不同的类型。

template 
class test
{
public:
    test();
    test& operator=(const test& t) { this->m_vec = t.m_vec; return *this; }
    test copy(const test& t) { test tmp; tmp.m_vec = t.m_vec; return tmp; }    // 在类的内部可以简化使用类名

    template     // 可以嵌套其他的模板函数
    U add(U i)
    {
        return i++;
    }

    std::vector m_vec;   
};

template     // 定义可以在类外
test::test()
{
}

test t;
test t1 = t;
t.add(1);    // 返回2

实例化test 时,

test t      ---->         std::vector  t;

test t           ---->         std::vector  t;

 

(2)、类模板成员函数

(3)、类模板成员函数的实例化

 

3、模板参数

 

4、成员模板

 

5、控制实例化

 

6、效率与灵活性

 

16.2、模板实参推断

编译器利用调用中的函数实参来确定其模板参数,从函数实参来确定模板实参的过程被称为模板实参推断

1、类型转换与模板类型参数

2、

16.3、重载与模板

 

16.4、可变参数模板

 

16.5、模板特列化

 

 

 

 

第四部分、高级主题

第十七章:标准库特殊设施

17.1、

17.2、

17.3、

17.4、

17.5、

17.6、

 

 

第十八章:用于大型程序的工具

大规模应用程序的特殊要求包括:

(1)在独立开发的子系统之间协同处理错误的能力

(2)使用各种库(可能包含独立开发的库)进行协同开发的能力

(3)对比较复杂的应用概念建模的能力

18.1、异常处理

 

18.1、命名空间

18.1、多重继承与虚继承

 

第十六章:特殊工具和技术

19.1、定义模板

19.1、定义模板

19.1、定义模板

19.1、定义模板

 

 

 

 

 

 

 

 

你可能感兴趣的:(C)