导图社区 CPlusPlus编程语言基础
本资源名为“CPlusPlus编程语言基础”,由袁宵归纳整理出五千余条C 知识点,几乎涵盖了C 基础的所有知识,希望此资源能帮助C 初学者入门和C 使用者参考。
编辑于2020-05-31 14:02:37本资源名为“CPlusPlus编程语言基础”,由袁宵归纳整理出五千余条C 知识点,几乎涵盖了C 基础的所有知识,希望此资源能帮助C 初学者入门和C 使用者参考。
计算机专业同学福利!总结了6年的JavaScript 高级程序设计思维导图,非常全面非常细致。需要拿去!Java具有简单性、面向对象、分布式、健壮性、安全性、平台独立与可移植性、多线程、动态性等特点。Java可以编写桌面应用程序、Web应用程序、分布式系统和嵌入式系统应用程序等。还不快收藏学起来!
本资源名为“CPlusPlus入门”,系统性地介绍了有关C 编程语言的入门知识。希望此资源能帮助C 编程初学者入门,本资源旨在培养C 编程兴趣。
社区模板帮助中心,点此进入>>
本资源名为“CPlusPlus编程语言基础”,由袁宵归纳整理出五千余条C 知识点,几乎涵盖了C 基础的所有知识,希望此资源能帮助C 初学者入门和C 使用者参考。
计算机专业同学福利!总结了6年的JavaScript 高级程序设计思维导图,非常全面非常细致。需要拿去!Java具有简单性、面向对象、分布式、健壮性、安全性、平台独立与可移植性、多线程、动态性等特点。Java可以编写桌面应用程序、Web应用程序、分布式系统和嵌入式系统应用程序等。还不快收藏学起来!
本资源名为“CPlusPlus入门”,系统性地介绍了有关C 编程语言的入门知识。希望此资源能帮助C 编程初学者入门,本资源旨在培养C 编程兴趣。
CPlusPlus编程语言基础
本资源名为“CPlusPlus编程语言基础”,由袁宵归纳整理出五千余条C++知识点,几乎涵盖了C++基础的所有知识,希望此资源能帮助C++初学者入门和C++使用者参考。 注: C++是在C语言基础上开发的一种集面向对象编程、泛型编程和过程化编程于一体的编程语言,是C语言的超集。 本资源内容主要来自C++经典入门巨著C++ Primer Plus 第六版(使用C++11标准)。 若读者发现该作品的错误或者有自己的建议,欢迎联系作者wangzichaochaochao@gmail.com
变量与常量
变量
变量命名规范
标识符
在计算机编程语言中,标识符是用户编程时使用的名字,用于给变量、常量、函数、语句块等命名,以建立起名称与使用之间的关系。标识符通常由字母和数字以及其它字符构成。
关键字typedef
typedef是在计算机编程语言中用来为复杂的声明定义简单的别名,它与宏定义有些差异。它本身是一种存储类的关键字,与auto、extern、mutable、static、register等关键字不能出现在同一个表达式中。
变量声明
定义与声明的本质区别: 定义:给变量分配存储空间。 声明:不给变量分配存储空间。
定义声明(定义)
单定义规则
引用声明(引用)
关键字extern
初始化
列表初始化
大括号初始化器:{ }
关键字auto
仅用于单个值的变量类型推断,不能用于列表初始化的变量类型推断。
存储信息的基本属性
存储信息的基本属性:信息存储的位置、信息的值、信息值的类型
运算符sizeof
sizeof运算符返回类型或变量的长度,单位为字节
取地址运算符&
常量
常量定义并初始化
创建常量的通用格式: const type name = value;
关键字const
算数类型
整型字面值
第一位是1~9的整数是十进制
第一位是0第二位是1~7的整数是八进制
前两位是0x或0X的整数是十六进制
进制
整数不同进制的表示方法只是为了表达上的方便。例如,如果视频内存段为十六进制B000,则不必在程序中使用之前将它转换为十进制数45056,而只需要使用0xB000即可。但是不管书写为10、012还是0xA,都讲以相同的方式存储在计算机中,被存储为二进制数(以2为基数)。
以l或L结尾的整数是long型
以ll或LL结尾的整数是long long型
以u或U结尾的整数是unsigned型
类型
字母U与L可以组合(不区分大小写和顺序),比如UL表示unsiged long int 类型的整数。
字符型字面值
char字面值
字符编码
转义字符
bool型字面值:true、false
浮点型字面值
小数点表示法
科学计数法
表示方法
以f或F结尾的浮点数是float型
以L结尾的浮点数是long double型
类型
没有后缀的浮点数默认就是double型
枚举
关键字enum
作用域内枚举
一种枚举。这种枚举使用class或struct定义: enum old1 {yes, no, maybe}; //traditional form enum class Newi (never, sometimes, often, always}; // new form enum struct New2 {never, lever, sever}; // new form 新枚举要求进行显式限定,以免发生名称冲突。因此,引用特定枚举时,需要使用Newl:never和New2:never等。
枚举量
默认值
显式赋值
符号常量
符号常量是在C语言中,可以用一个标识符来表示一个常量,这个标识符称之为符号常量。其特点是编译后写在代码区,不可寻址,不可更改,属于指令的一部分。
数据类型
类型
类型 (程序开发语言)
类型,是编程语言中的一个很重要的概念,它在强类型语言中尤其重要,比如C语言,C++语言,Java语言中...它定义了一个变量的内存布局和这个这个变量可以实用的运算符。 C和C++开发语言中,基本类型有:int,char,double,class等
指定基本类型完成了三项工作
指定基本类型完成了三项工作: 决定数据对象需要的内存数量; 决定如何解释内存中的位(long和float在内存中占用的位数相同,但将它们转换为数值的方法不同); 决定可使用数据对象执行的操作或方法。 对于内置类型来说,有关操作的信息被内置到编译器中。但在C+中定义用户自定义的类型时,必须自己提供这些信息。付出这些劳动换来了根据实际需要定制新数据类型的强大功能和灵活性。
不同数据类型占用的字节数根系统有关
#include <iostream> using namespace std; // Test by Visual Studio 2019 int main(void) { // x86: 1 2 4 4 8 x64: 1 2 4 4 8 std::cout << sizeof(char) << std::endl; std::cout << sizeof(short) << std::endl; std::cout << sizeof(int) << std::endl; std::cout << sizeof(long) << std::endl; std::cout << sizeof(long long) << std::endl; // x86: 4 8 x64: 4 8 std::cout << sizeof(float) << std::endl; std::cout << sizeof(double) << std::endl; // x86: 4 4 4 x64: 8 8 8 std::cout << sizeof(int *) << std::endl; std::cout << sizeof(char *) << std::endl; std::cout << sizeof(double *) << std::endl; return 0; }
分类
内置类型
基本类型
整型
5种整型关键字:char、int、long、long long
函数库
无符号类型关键字:unsigned
字符类型扩展:char -> wchar_t、char16_t、char32_t
浮点型
float、double、long double
单位类型
单位类型用来度量类型的大小,本质上是整型。
size_t
size_t 类型定义在cstddef头文件中,该文件是C标准库的头文件stddef.h的C++版。它是一个与机器相关的unsigned类型,其大小足以保证存储内存中对象的大小。 不同平台的size_t会用不同的类型实现,比如: typedef unsigned int size_t; // x86 typedef unsigned long long size_t; //x64
sizeof( )
sizeof() 是一个判断数据类型或者表达式长度的运算符。
size_type
size_type由string类类型和vector类类型定义的类型,用以保存任意string对象或vector对象的长度,标准库类型将size_type定义为unsigned类型。 string抽象意义是字符串, size()的抽象意义是字符串的尺寸, string::size_type抽象意义是尺寸单位类型。 理解size_type抽象意义是尺寸单位类型。 比如: string s = "中国人"; u16string su16 = u"中国人"; u32string su32 = U"中国人"; cout << s.length() << endl; // 6 cout << su16.length() << endl; // 3 cout << su32.length() << endl; // 3
复合类型
数组
数组声明
数组声明的通用格式: typeName arrayName[arraySize]
数组名与数组的地址
数组名被解释为数组第一个元素的地址,但是还具有以下两个特点: 对数组名应用地址运算符&时,得到的是整个数组的地址。 对数组名使用sizeof时,得到整个数组的长度(以字节为单位)。 例如: int tell[10]; tell 等价于 &tell[0] &tell 整个数组的地址 虽然这两个值相等,但是意义不同。&tell[0](即tell)是一个4字节内存块的地址,而&tell是一个40字节内存块的地址。因此,表达式tell+1将地址加4,指向下一个数组元素;而表达式&tell+1将地址加40,指向下一个包含10个int元素的数组。
运算符[ ]
数组初始化规则
二维数组
理解 arr[M][N]
typeName arr[M][N]; // 一个M行N列的二维数组arr,其元素类型时typeName。 arr // 指向arr数组第0行的指针 arr + r // 指向arr数组第r行的指针 arr[r] == *(arr + r) //arr数组的第r行(该行数组含有N个元素),因此arr[r]指向这一行的第0个元素 arr[r] + c == *(arr + r) + c //指向arr数组第r行第c列元素的指针 arr[r][c] == *(*(arr + r) + c) //arr数组第r行第c列元素的值
char二维数组、字符串指针数组、string对象数组
char二维数组、字符串指针数组、string对象数组对比 char二维数组: char cities[M][100] = {"XXX", ...,"XXT"}; 字符串指针数组: const char * cities[M] = {"XXX", ...,"XXT"}; string对象数组: const string cities[M] = {"XXX", ...,"XXT"}; 可以用相同的方式访问这三者的内容:cities[i],i是想要访问的数组元素的序号。
函数指针数组
函数指针数组的声明方法为: 返回值类型 ( * 数组名[数组大小]) ([形参列表]);
数组的替代品
静态数组:模板类array
声明创建一个名为arr的array对象,它包含_elem个类型为typename的元素: array<typeName, n_elem> arr; 与创建vector对象不同的是, n_elem不能是变量。
动态数组:模板类vector
一般而言,下面的声明创建一个名为vt的vector对象,它可存储n_elem个类型为typeName的元素: vector<typeName> vt (n_elem); 其中参数n_elem可以是整型常量,也可以是整型变量。
面相数值计算的数组:模板类valarray
对比
您可能会问, C++为何提供三个数组模板: vector、valarray和array。这些类是由不同的小组开发的,用于不同的目的。vector模板类是一个容器类和算法系统的一部分,它支持面向容器的操作,如排序、插入、重新排列、搜索、将数据转移到其他容器中等。而valarray类模板是面向数值计算的,不是STL的一部分。例如,它没有push_back( )和insert( )方法,但为很多数学运算提供了一个简单、直观的接口。最后,array是为替代内置数组而设计的,它通过提供更好、更安全的接口,让数组更紧凑,效率更高.Array表示长度固定的数组,因此不支持push_back( )和insert( ),但提供了多个STL方法,包括begin( )、 end( )、rbegin( )和rend( ),这使得很容易将STL算法用于array对象。 valarray没有定义下标超过尾部一个元素的行为,为解决这种问题, C++11提供了接受valarray对象作为参数的模板函数begin( )和end( )。因此,您将使用begin(vad)而不是vad.begin。这些函数返回的值满足STL区间需求: sort (begin(vad), end(vad)); // c++11 fix! 经验: 模板类vector, forward list, list, deque和array都是序列容器,它们都前面列出的方法,但forward_lis不是可反转的。序列容器以线性顺序存储一组类型相同的值。 如果序列包含的元素数是固定的,通常选择使用array; 否则,应首先考虑使用vector,它让array的随机存取功能以及添加和删除元素的功能于一身。 然而,如果经常需要在序列中间添加元素,应考虑使用list或forward_list。 如果添加和删除操作主要是在序列两端进行的,应考虑使用deque.
字符串
分类
string类
操作
拼接+
复制=
字符数size()、length()
C风格字符串
C风格字符串:以空字符结尾的char 类型数组
初始化方法
双引号法
数组法
字符串字面值
在cout和多数C++表达式中。char数组名、char指以及引号括起来的字符串常量都被解释为字符串第一个字符的指针。
空字符:\0
C库函数<string.h>
拼接strcat()
复制strcpy()、strncpy()
大小strlen()
比较strcmp()
原始字符串
R"字符串"
其它形式
wchar_t、char16_t、char32_t
u16string、u32string
结构
关键字struct
成员运算符.
结构数组
结构中的位字段
共用体
关键字union
指针
解除引用*
*运算符被称为间接值或解除引用
分配内存
运算符 new
为一个数据对象(可以是结构、也可以是基本类型等)获得并指定分配内存的通用格式: typeName * pointer_name = new typeName;
堆
运算符delete
delete pointer_name; 这将释放指针pointer_name指向的内存(数据对象),但不会删除指针本身。
内存泄漏
数据对象
术语"数据对象”比"变量”更通用,它指的是为数据项分配的内存块。因此,变量也是数据对象。
管理数据内存的方式
动态存储
静态存储
自动存储
根据用于分配内存的方法
局部变量、栈
static、静态区
new、堆
指针运算
递增(减)运算符和指针: *++pt、++*pt
指针的应用
指针与数组
创建与删除
创建动态数组: type_name * pointer_name = new type_name [num_elements]; 比如, int * psome = new int [10]; 释放整个动态数组: delete [ ] pointer_name; 比如,delete [ ] psome; 注意:delete和之间的方括号
使用动态数组
int * p = new int[3]; 使用动态数组:把指针名当做数组名使用即可。
遍历数组等价式
arr[i] == *(arr + i)
&arr[i] == arr + i
指针与字符串
指针与结构
箭头成员运算符->
指针与类
指针和关键字const
指向const对象的指针
指向const对象的指针:目的是防止使用该指针来修改所指向的值,有一个微妙的问题是,这并不意味着指针指向的对象实际上就是一个常量,而只是意味着对该指针而言,这个对象是常量。单方面宣告! 例如: int age = 39; const int * pt = &age; 该声明指出,pt指向一个const int(这里是39),因此不能使用pt来修改这个值。等价于*pt的值为const,不能被修改。但是age本身不是const,所以可以直接通过age变量来修改age的值。 如果是一级间接关系(比如指针指向基本数据类型),则可以将非const指针赋值给const指针: int age = 39; int * pd = &age; const int * pt = pd; 使用指向常量指针的好处: 可以避免由于无意间修改数据导致的编程错误; 使用const使得函数能够处理const和非const实参,否则只能接受非const数据。
const指针
const指针:即无法修改指针值的指针,指针的值变为常量,这使得无法改变指针指向的对象。 int sloth = 3; int * const finger = &sloth;
指向const对象的const指针
分类
函数指针
函数指针声明
函数指针的声明方法为: 返回值类型 ( * 指针变量名) ([形参列表]); “返回值类型”说明函数的返回类型,“(指针变量名 )”中的括号不能省,括号改变了运算符的优先级。若省略整体则成为一个函数说明,说明了一个返回的数据类型是指针的函数,后面的“形参列表”表示指针变量指向的函数所带的参数列表。 简单理解:函数指针声明类似于函数声明,仅仅是将函数声明中的函数名改为( * 指针变量名)即可。在函数指针声明之后可以直接把( * 指针变量名)当做函数名使用即可。
函数名不等于函数指针
函数名和函数指针的区别: 第一:函数名与FunP函数指针都是函数指针。fun是一个函数指针常量,funP是一个函数数指针变量。 虽然通过常量与变量来解释函数名无法赋值可以帮助理解,但是我们发现对fun赋值时编译器给的错误提示并不是说对常量进行赋值,而是告诉我们=号两端格式不匹配。对此,第二种理解更合理。 第二:函数名和数组名实际上都不是指针,但是,在使用时可以退化成指针,即编译器可以帮助我们实现自动的转换。 这也可以解释为什么当我们在=号右侧使用函数名时,无论是取值还是取地址都没有问题,因为编译替我们做了相当于强制类型转换的工作,而在当函数名在=号左侧时,右侧的函数指针并没有这个功能,毕竟他们俩不是同一种结构。 补充说明: 假设有如下函数声明 void MyFun(int ); void (*FunP)(int ); 1. 其实,MyFun的函数名与FunP函数指针都是一样的,即都是函数指针。MyFun函数名是一个函数指针常量,而FunP是一个函数数指针变量,这是它们的关系。 2. 但函数名调用如果都得如(*MyFun)(10);这样,那书写与读起来都是不方便和不习惯的。所以C语言的设计者们才会设计成又可允许MyFun(10);这种形式地调用(这样方便多了并与数学中的函数形式一样,不是吗?)。 3. 为统一起见,FunP函数指针变量也可以FunP(10)的形式来调用。 4. 赋值时,即可FunP=&MyFun形式,也可FunP=MyFun。
作用:调用函数和做函数的参数
函数指针作为某个函数的参数 既然函数指针变量是一个变量,当然也可以作为某个函数的参数来使用的。所以,你还应知道函数指针是如何作为某个函数的参数来传递使用的。 一个实例:要设计一个CallMyFun函数,这个函数可以通过参数中的函数指针值不同来分别调用MyFun1、MyFun2、MyFun3这三个函数(注:这三个函数的定义格式应相同)。 实现代码如下: //自行包含头文件 void MyFun1(int x); void MyFun2(int x); void MyFun3(int x); typedef void (*FunType)(int ); //②. 定义一个函数指针类型FunType,与①函数类型一至 void CallMyFun(FunType fp,int x); int main(int argc, char* argv[]) { CallMyFun(MyFun1,10); //⑤. 通过CallMyFun函数分别调用三个不同的函数 CallMyFun(MyFun2,20); CallMyFun(MyFun3,30); } void CallMyFun(FunType fp,int x) //③. 参数fp的类型是FunType。 { fp(x);//④. 通过fp的指针执行传递进来的函数,注意fp所指的函数是有一个参数的 } void MyFun1(int x) // ①. 这是个有一个参数的函数,以下两个函数也相同 { printf(“函数MyFun1中输出:%d\n”,x); } void MyFun2(int x) { printf(“函数MyFun2中输出:%d\n”,x); } void MyFun3(int x) { printf(“函数MyFun3中输出:%d\n”,x); }
C++实现多态性的虚函数表是通过函数指针实现
指向函数指针数组的指针
空指针
关键字nullptr
智能指针:帮助管理动态内存分配
悬挂指针(野指针)
指针指向非法的内存地址,那么这个指针就是悬挂指针,也叫野指针。意为无法正常使用的指针。
广义指针
迭代器
引用
声明引用
引用的初始化声明方法: 类型标识符 &引用名=目标变量名; 能且只能通过初始化声明来设置引用(包括按引用传递函数参数时)。
引用的特点
主要适应对象
结构和类
主要作用
作为函数参数
尽量使用const
将引用参数声明为常量数据的引用的理由有三个: 使用const可以避免无意中修改数据的编程错误。 使用const使函数能够正处理const和非const实参,否则只能接受非const数据。 使用const引用使函数能够正确生成并使用匿名的临时变量。 注意:如果函数调用的参数不是左值或与相应的const引用参数的类型不匹配,则C++将创建类型正确的匿名变量,将函数调用的参数值传递给该匿名变量,并让参数来引用该变量。
从函数中返回左值
函数返回引用
左值引用与右值引用
左值引用& 右值引用&& 一般而言,将左值传递给const左值引用参数时,参数将被初始化为左值。将右值传递给函数时, const左值引用参数将指向右值的临时拷贝 一般而言,将左值传递给非const左值引用参数时,参数将被初始化为左值;但非const左值形参不能受右值实参。 总之,非const左值形参与左值实参匹配,非const右值形参与右值实参匹配: const左值形参可与左值或右值形参匹配,但编译器优先选择前两种方式(如果可供选择的话)。 代码分析 #include <iostream> using namespace std; double up(double x) { return 2.0 * x; } void r1(const double &rx) { cout << rx << endl; } void r2(double &rx) { cout << rx << endl; } void r3(double &&rx) { cout << rx << endl; } int main() { double w = 10.0; r1(w); r1(w + 1); r1(up(w)); r2(w); r2(w + 1); // 错误!非常量引用的初始值必须为左值 r2(up(w)); // 错误!非常量引用的初始值必须为左值 r3(w); // 错误!无法将右值引用绑定到左值 r3(w + 1); r3(up(w)); return 0; }
分类
左值引用&
拷贝语义
常量左值引用使全能引用类型(可以引用所有值类型),可以用于拷贝语义。即:一般而言,将左值传递给const左值引用参数时,参数将被初始化为左值。将右值传递给函数时, const左值引用参数将指向右值的临时拷贝
右值引用&&
非常量右值引用:用于移动语义、完美转发。 对大多数程序员来说,右值引用带来的主要好处并非是让他们能够编写使用右值引用的代码,而是能够使用利用右值引用实现移动语义的库代码。例如, STL类现在都有复制构造函数、移动构造函数、复制赋值运算符和移动赋值运算符。
移动语义和右值引用
强制移动
std::move( )
// useless.cpp -- an otherwise useless class with move semantics #include"stdafx.h" // stdmove.cpp -- using std::move() #include <iostream> #include <utility> // use the following for g++4.5 // #define nullptr 0 // interface class Useless { private: int n; // number of elements char * pc; // pointer to data static int ct; // number of objects void ShowObject() const; public: Useless(); explicit Useless(int k); Useless(int k, char ch); Useless(const Useless & f); // regular copy constructor Useless(Useless && f); // move constructor ~Useless(); Useless operator+(const Useless & f)const; Useless & operator=(const Useless & f); // copy assignment Useless & operator=(Useless && f); // move assignment void ShowData() const; }; // implementation int Useless::ct = 0; Useless::Useless() { ++ct; n = 0; pc = nullptr; } Useless::Useless(int k) : n(k) { ++ct; pc = new char[n]; } Useless::Useless(int k, char ch) : n(k) { ++ct; pc = new char[n]; for (int i = 0; i < n; i++) pc[i] = ch; } Useless::Useless(const Useless & f) : n(f.n) { ++ct; pc = new char[n]; for (int i = 0; i < n; i++) pc[i] = f.pc[i]; } Useless::Useless(Useless && f) : n(f.n) { ++ct; pc = f.pc; // steal address f.pc = nullptr; // give old object nothing in return f.n = 0; } Useless::~Useless() { delete[] pc; } Useless & Useless::operator=(const Useless & f) // copy assignment { std::cout << "copy assignment operator called:\n"; if (this == &f) return *this; delete[] pc; n = f.n; pc = new char[n]; for (int i = 0; i < n; i++) pc[i] = f.pc[i]; return *this; } Useless & Useless::operator=(Useless && f) // move assignment { std::cout << "move assignment operator called:\n"; if (this == &f) return *this; delete[] pc; n = f.n; pc = f.pc; f.n = 0; f.pc = nullptr; return *this; } Useless Useless::operator+(const Useless & f)const { Useless temp = Useless(n + f.n); for (int i = 0; i < n; i++) temp.pc[i] = pc[i]; for (int i = n; i < temp.n; i++) temp.pc[i] = f.pc[i - n]; return temp; } void Useless::ShowObject() const { std::cout << "Number of elements: " << n; std::cout << " Data address: " << (void *)pc << std::endl; } void Useless::ShowData() const { if (n == 0) std::cout << "(object empty)"; else for (int i = 0; i < n; i++) std::cout << pc[i]; std::cout << std::endl; } // application int main() { using std::cout; { Useless one(10, 'x'); Useless two = one + one; // calls move constructor cout << "object one: "; one.ShowData(); cout << "object two: "; two.ShowData(); Useless three, four; cout << "three = one\n"; three = one; // automatic copy assignment cout << "now object three = "; three.ShowData(); cout << "and object one = "; one.ShowData(); cout << "four = one + two\n"; four = one + two; // automatic move assignment cout << "now object four = "; four.ShowData(); cout << "four = move(one)\n"; four = std::move(one); // forced move assignment cout << "now object four = "; four.ShowData(); cout << "and object one = "; one.ShowData(); } std::cin.get(); } /* 运行结果 object one: xxxxxxxxxx object two: xxxxxxxxxxxxxxxxxxxx three = one copy assignment operator called: now object three = xxxxxxxxxx and object one = xxxxxxxxxx four = one + two move assignment operator called: now object four = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx four = move(one) move assignment operator called: now object four = xxxxxxxxxx and object one = (object empty) */
左值、左值引用、右值、右值引用的比较
// 来源:https://zhuanlan.zhihu.com/p/95628575 左值和右值 C++中每个表达式要么是左值要么是右值。关于左值和右值的概念C++ Prime中的一句话是“当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值时,用的是对象的身份(在内存中的位置)”。在需要右值的地方,可以用左值来代替,实际使用是它的内容,但是右值不能当做左值来使用。 我们平时见到的大部分都是左值,赋值等号左边是左值,其运算结果也是左值。解引用运算符、下标运算符、递增、递减返回的都是左值。在代码中,你能看到有名字的变量基本都是左值。 取地址符作用于左值,返回的是右值。字面常量也可以当做右值来使用。 当然这是一种右值形式。可以理解右值是一次性的对象。临时对象算右值,它们在创建的地方不能被操作,并且很快就会销毁。其实,字面常量也是创建的临时变量。对于右值不太好理解的,就记住右值是一次性的。 左值引用和右值引用 左值引用就是绑定在左值上的引用,右值引用就是绑定在右值上的引用。不过有一点比较饶人,就是右值引用它本身是左值。 int && rr1 = 32; // 正确, 字面常量是右值 int && rr2 = rr1; // 错误, 右值引用rr1本身是一个左值。(它也是有名字的变量) 模板和右值引用 template <typename T> void f(T&& x) {} 模板参数推导会发生以下情况: 如果 f 接受一个左值参数,那么x是一个左值引用 如果 f 接受一个右值参数,那么x是一个右值引用 若想知道详细的推导机制,可以搜搜模板实参推断和引用、引用折叠等概念。关于上面这段代码还有一点要提的,就是右值引用本身是左值,右值是右值,右值引用是右值引用,不要搞混了,假设调用f时,实参是一个右值,在函数f体内,x是一个右值引用,右值引用就是左值,是左值就可以做对应的改动。比如: template<typename T> void f(T&& x) { cou << x++; } f(2); // 3 再看下面这段代码: template<typename T> void g(T&& x) {} template<typename T> void f(T&& x) { g(x); } 上面这段代码有一个问题就是,在调用 f 的时候,不管给 f 传的是左值还是右值,在函数 f 体内,x 就是一个左值(不管是左值引用还是右值引用),所以在左值传参给函数 g 时,在 g 的函数体内,x 就是是一个左值引用。如果想在 g 的函数体内的 x 是右值引用,那么需要使用标准库函数 std::forward 来转发引用。将 f 函数改为: tempalte<typename T> void f(T&& x) { g(std::forward<T>(x)); } std::forward函数保持了 x 的引用类型,所以: 如果 x 是一个右值引用,那么std::forward作用和std::move一样。 如果 x 是一个左值引用,那么std::forward没做啥事。 std::move 虽然我们不能直接将右值引用绑定到左值上,但可以显示的将左值转换为对应的右值引用类型。通过std::move函数来获得绑定到左值上的右值引用。 int i = 3; int && rr = std::move(i); 经过std::move函数操作后的对象内容是未定义的,可以认为内容被移走了。std::move本身接受任何类型参数,包括左值右值。 int i = 3, j; j = std::move(2); // 从一个右值移动数据 j = std::move(i); //从一个左值移动数据,但是i的值之后是不确定的。
自定义类型
类型 (技术名词)
声明一个类或者一个接口都同时向编译器注册了一个新的类型,而此类或者接口以及类型都是共享同样的一个名字。也就是说。编译器所能理解的全部都是类型,而程序员的工作是把现实中的类概念转化为设计中的接口概念,而编译器对应于上两种概念都有直接的支持,那就是一个类声明或者接口声明在编译器的理解来看就是一个类型声明。但是反过来却不一定成立。一个类可以有多个接口,(一个类完全有可能实现了设计人员的多个契约条件),同时也就可能有多个类型(因为类型不过是接口这个设计域内的概念在编译器中的实现)。
类
void类型
关键字void
类型转换
按数据类型分类
标准转换
基本数据类型之间的转换
指针、引用、指向成员的指针派生类型的转换
基本类型->自义定类型(类对象)
方法1.构造函数(只能是单参数值)
只有一个参数的类构造函数用于将类型与该参数相同的值转换为类类型。例如,将int值赋给Stonewt对象时,接受int参数的Stonewt类构造函数将自动被调用。然而,在构造函数声明中使用explicit可防止隐式转换,而只允许显式转换。
方法2.重载赋值运算符
对比
构造函数(只能是单参数值)需要创建和删除临时对象 重载赋值运算符直接处理基本类型,处理效率更高
自义定类型(类对象)->基本类型
转换函数(特殊的运算符函数)
被称为转换函数的特殊类成员运算符函数,用于将类对象转换为其他类型。要把类对象转换为typeName类型,需要使用这种形式的转换函数: operator typeName ( ); 请注意以下几点: 转换函数必须是类方法; 转换函数不能指定返回类型; 转换函数不能有参数。 例如,转换为double类型的函数的原型如下:operator double( ); typeName (这里为double)指出了要转换成的类型,因此不需要指定返回类型。转换函数是类方法意味着:它需要通过类对象来调用,从而告知函数要转换的值。因此,函数不需要参数。
转换函数、友元函数、重载函数
在两个不同类型的数据类型进行某个操作符定义的运算时,需要类型转换,下面以实现double类型和Stonewt类型加法运算时为例讲解不同的运算方案。 实现加法时的选择要将double量和Stonewt两种选择: 第一种方法是下面的定义为友元函数(并且依赖于参数类型的隐式转换),让Stonewt(double)构造函数将double类型的参数转换为Stonewt类型的参数: operator+ (const stonewt &. const stonewt &) // 这样A+B时,无论A或B是double类型还是Stonewt类型都能处理(已经有double类型->Stonewt类型的构造函数) 第二种方法是,将加法运算符重载为一个显式使用double类型参数的函数: stonewt operator+ (double x); // member function // Stonewt类的成员函数,只处理 Stonewt类型 + double类型 friend stonewt operator+ (double x, stonewt &s); // Stonewt类的友元函数,只处理 double类型 + Stonewt类型 这样,下面的语句将与成员函数operator + (double x)完全匹配: cotal = jennyst + kennyp; // stonewt + double 而下面的语句将与友元函数operator + (double x, Stonewt &s)完全匹配: total = pennyp +jennyst; // double + stonewt 每一种方法都有其优点。第一种方法(依赖于隐式转换)使程序更简短,因为定义的函数较少。这也意味程序员需要完成的工作较少,出错的机会较小。这种方法的缺点是,每次需要转换时,都将调用转换构造函数,这增加时间和内存开销。第二种方法(增加一个显式地匹配类型的函数)则正好相反。它使程序较长,程序员需要完成的工作更多,但运行速度较快。如果程序经常需要将double值与Stonewt对象相加,则重载加法更合适;如果程序只是偶尔使用这种加法,则依赖于自动转换更简单,但为了更保险,可以使用显式转换。
按是否强制分类
自动类型转换
自动类型转换发生的时机: 将一种算数类型的值赋值给另一种算数类型的变量时。 表达式中含有不同的类型时。 将参数传递给函数时。
存在问题
强制类型转换
强制类型转换的通用格式: typename ( value ) 或者 (typename) value
显式强制类型转换
显式转换运算符
关键字explicit
隐式强制类型转换
按是否隐式分类
隐式转换
显式转换
按语言分类
C语言中
C++中
dynamic_cast
指向基类的指针向下转型为指向派生类的指针
const_cast
const_cast不是万能的。它可以修改指向一个值的指针,但修改const值的结果是不确定的。
限定符const 和volatile之间转换
static_cast
基类和派生类之间的显式转换、数值类型之间转换
reinterpret_cast
指针、引用、整数三种类型之间的转换
表达式与语句
表达式
C++表达式是值或值与运算符的组合,每个表达式都有值。
值
左值与右值
在C++11中可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值(将亡值或纯右值)。举个例子,int a = b+c, a 就是左值,其有变量名为a,通过&a可以获取该变量的地址;表达式b+c、函数int func()的返回值是右值,在其被赋值给某一变量前,我们不能通过变量名找到它,&(b+c)这样的操作则不会通过编译。
分类
左值
以下常用的运算符用到左值: 赋值运算符需要一个(非常量)左值作为左侧运算对象,得到的结果仍然是左值。 取地址符作用于一个左值运算符对象,返回一个指向该运算对象的指针,这个指针是右值。 内置解引用运算符、下标运算符、迭代器解引用运算符、string 和vector 的下标运算符的求值结果都是左值。 内置类型的迭代器的递增递减运算符作用于左值运算对象,其前置版本所得的结果也是左值。
可修改左值
不可修改左值
特殊类左值:函数、数组
右值
右值包括字面常量(C-风格字符串除外,它表示地址)、诸如x+y等表达式、有返回值的函数(条件是该函数返回的不是引用): int x = 10; int y = 23; int && r1 = 13; int && r2 = x+ y; double && r3 = std::sqrt(2.0); 上面的13、x+y、std::sqrt(2.0)都是右值。r1、r2、r3是右值引用。
纯右值
纯右值指的是临时变量值、不跟对象关联的字面量值。临时变量指的是非引用返回的函数返回值、表达式等,例如函数int func()的返回值,表达式a+b;不跟对象关联的字面量值,例如true,2,”C”等。
将亡值
转换
左值->右值
运算符取地址 &
右值->左值
运算符解引用 *
表达式的值
运算符
运算符用于执行程序代码运算,会针对一个以上操作数项目来进行运算。
运算符分类
运算符性质
优先级 名称 运算符 可重载性 所需变量个数 结合性 1 作用域运算符 :: 否 自左向右 2 成员访问运算符 . 否 双目运算符 自左向右 指向成员运算符 -> 下标运算符 [ ] 括号 / 函数运算符 () 3 自增运算符 ++ 单目运算符 自右向左 自减运算符 -- 按位取反运算符 ~ 逻辑非运算符 ! 正号 + 负号 - 取地址运算符 & 地址访问运算符 * 强制类型转换运算符 (Type) 类型长度运算符 sizeof() 否 内存分配运算符 new 取消分配内存运算符 delete 类型转换运算符 castname_cast<type> 否 4 成员指针运算符 .* 否 双目运算符 自左向右 ->* 5 乘号 * 双目运算符 自左向右 除号 / 取余运算符 % 6 加号 + 双目运算符 自左向右 减号 - 7 位左移运算符 << 双目运算符 自左向右 位右移运算符 >> 8 小于号 < 双目运算符 自左向右 小于等于号 <= 大于号 > 大于等于号 >= 9 等于号(判等运算符) == 双目运算符 自左向右 不等于号 != 10 按位与 & 双目运算符 自左向右 11 按位异或 ^ 双目运算符 自左向右 12 按位或 | 双目运算符 自左向右 13 逻辑与 && 双目运算符 自左向右 14 逻辑或 || 双目运算符 自左向右 15 条件运算符 ? : 否 三目运算符 自右向左 16 赋值运算符 = 双目运算符 自右向左 复合赋值运算符 += -= *= /= %= <<= >>= &= |= ^= 17 抛出异常运算符 throw 否 自左向右 18 逗号运算符 , 双目运算符 自左向右
运算符的优先级
运算符的结合性:左、右结合性
操作数个数:单、双、三目运算符
可重载性
运算符重载
运算符重载,就是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。
运算符函数
要重载运算符,需使用被称为运算符函数的特殊函数形式。运算符函数的格式如下: operator op (argument-list) operator是关键字,op是有效运算符,argument-list是参数列表。operator和运算符一起使用,表示一个运算符函数,理解时应将operator和运算符整体上视为一个函数名。 假设有一个Salesperson类,并为它定义了一个operator + ()成员函数,以重载+运算符,以便能够将两个Saleperson对象的销售额相加,则如果district2, sid和sara都是Salesperson类对象,便可以编写这样的等式: district2 = sid + sara; 编译器发现,操作数是Salesperson类对象,因此使用相应的运算符函数替换上述运算符: district2 = sid.operator+ (sara); 然后该函数将隐式地使用sid(因为它调用了方法),而显式地使用sara对象(因为它被作为参数传递),来计算总和,并返回这个值。当然最重要的是,可以使用简便的+运算符表示法,而不必使用笨拙的函数表示法。
关键字operator
调用方法
函数表示法
运算符表示法
作为类的成员函数还是非成员函数
对于很多运算符来说,可以选择使用成员函数或非成员函数来实现运算符重载。一般来说,非成员函数应是友元函数,这样它才能直接访问类的私有数据。例如, Time类的加法运算符在Time类声明中的原型如下: Time operator+(const Time & t) const; // 成员函数 这个类也可以使用下面的原型: // 非成员函数(友元函数) friend Time operator+ (const Time & tl, const Time & t2); 加法运算符需要两个操作数。对于成员函数版本来说,一个操作数通过this指针隐式地传递,另一个操作数作为函数参数显式地传递;对于友元版本来说,两个操作数都作为参数来传递。注意:非成员版本的重载运算符函数所需的形参数目与运算符使用的操作数数目相同;而成员版本所需的参数数目少一个,因为其中的一个操作数是被隐式地传递的调用对象。 这两个原型都与表达式T2 + T3匹配,其中T2和T3都是Time类型对象。也就是说,编译器将下面的语句: T1 = T2 +T3; 转换为下面两个的任何一个: T1 = T2.operator+(T3); // 成员函数 T1 = operator+ (T2, T3); // 非成员函数(友元函数) 记住,在定义运算符时,必须选择其中的一种格式,而不能同时选择这两种格式。因为这两种格式都与同一个表达式匹配,同时定义这两种格式将被视为二义性错误,导致编译错误。那么哪种格式最好呢?对于某些运算符来说(如“重载限制”),成员函数是唯一合法的选择。在其他情况下,这两种格式没有太大的区别。有时,根据类设计,使用非成员函数版本可能更好(尤其是为类定义类型转换时)。
重载限制
运算符重载的限制: 重载后的运算符必须至少有一个操作数是用户定义的类型,这将防止用户为标准类型重载运算符。因此,不能将减法运算符(-)重载为计算两个double值的和,而不是它们的差。虽然这种限制将对创造性有所影响,但可以确保程序正常运行。 使用运算符时不能违反运算符原来的运算符性质。例如,不能将求模运算符(%)重载成使用一个操作数;同样,不能修改运算符的优先级。 不能创建新运算符。例如,不能定义operator **()函数来表示求幂。 不能重载下面的运算符:sizeof运算符、成员运算符、成员指针运算符、作用域解析运算符、条件运算符、以及一些强制类型转换运算符。 大多数运算符都可以通过成员或非成员函数进行重载,但下面的运算符只能通过成员函数进行重载。:赋值运算符。():函数调用运算符。[]:下标运算符。->:通过指针访问类成员的运算符。
表达式分类
赋值表达式
C++将赋值表达式的值定义为左侧成员的值。 mids = (cooks = 4) + 3; z = y = x = 3;
赋值运算符=
组合赋值运算符
算数表达式
双目加减乘除取余运算符+-*/%
单目正运算符+、单目负运算符 -
逻辑表达式
逻辑运算符
逻辑与运算符 &&、逻辑或运算符 ||、逻辑非运算符 !
关系表达式
大于运算符>、小于运算符<、大或等于运算符>=、小或等于运算符<=
等于运算符==
不等于运算符!=
条件表达式
条件运算符? :
逗号表达式
逗号运算符
逗号的特性 逗号运算符保证按从左至右的顺序依次计算表达式,即逗号运算符是一个顺序点。 所以i = 20, j = 2 * i 这样的表达式是安全的。 逗号表达式的值是语句中最后一个表达式的值,所以上面式子的值是40。 在所有运算中,逗号运算符的优先级是最低的, 所以语句 cata = 17, 240; 被解释为 (cata = 17) , 240
逗号分隔符(区分)
可利用逗号作为列表分隔符一次性声明多个变量,比如: int i, j, k;
语句
表达式加上分号就变为成了语句,这是一个充分不必要条件。 age = 100 是表达式 age = 100; 是语句,更准确地说是一条表达式语句。
复合语句{ }
复合语句(语句块、代码块)由一对花括号和它们之间包含的语句组成,被视为一条语句。 如果在复合语句中定义的新变量,则仅当程序执行该语句块中的语句时,该变量才存在。执行完该复合语句后,变量将被释放。
流程控制
循环
关键字for、while、do while
递增运算符++
递减运算符--
注意
自增与自减运算符都有前缀和后缀两个版本,比如a++表示使用当前值计算表达式,然后将a的值加1;而++b表示先将b的值加1,然后再使用新的值来计算表达式。 int x = 5; int y = ++x; // y is 6 int z = 5; int y = z++; // y is 5 注意:不要在同一个语句对同一个值递增或递减多次,因为这将使“使用后修改”与“修改后使用”模糊不清。 比如: int x = 2; int y = 2 * (x++) * (10 - (++x); // y is 42 or 28 ? 在编译器VS2019中的结果是42 对于这样的语句,C++没有定义正确的行为。
副作用
副作用是指在计算表达式时对数据对象进行了修改。
顺序点
在C++中,语句中的分号就是一个顺序点,意味着程序处理下一条语句之前,赋值运算符、递增运算符、递减运算符执行的所有修改必须完成。 完整表达式末尾也是一个顺序点。完整表达式是指:不是另一个更大表达式的子表达式,比如while循环检测条件的表达式。 int guests = 0; while ( guests++ < 3) cout << guests ; //123 现在再来看下面语句: int x = 2; int y = 2 * (x++) * (10 - (++x); 表达式2 * (x++)不是一个完整表达式,因此C++不保证x的值在计算表达式2 * (x++)后立刻增加1。但整条赋值语句是个完整表达式,而且分号表示了顺序点,因此C++能保证程序在执行到下一条语句前,x的值将被递增两次。
执行速度
基于范围for循环
C++11新增一种基于范围的for循环,为了简化一种常见的循环任务:对数组(或容器类,如vector和array)的每个元素执行相同的操作,如下所示: #include <iostream> using namespace std; int main(int argc, char* argv[]) { double prices[5] = { 1.2, 32.1, 12.3, 64.6, 55.5 }; for (double x : prices) // x最初代表数组prices的第一个元素,随着执行循环,x依次表示数组其他元素 cout << x << ", "; cout << endl; for (double &x : prices) // 如果需要修改遍历对象的值需要使用&符号 x = x * 0.10; for (double x : prices) cout << x << ", "; cout << endl; return 0; }
条件
关键字if、if else、if else if else
关键字switch
整数表达式
标签
每个标签值都必须是整数(包括字符和枚举量),而且是常量。
整数、字符、枚举量
关键字default
break和continue语句
关键字continue与break
函数
基础概念
函数定义
“函数定义”是指对函数功能的确立,包括指定函数名,函数值类型、形参类型、函数体等,它是一个完整的、独立的函数单位。 函数的定义的格式: 返回类型 名字(形式参数表列){函数体语句 return 表达式;}
函数名
函数地址
函数声明(函数原型)
“函数声明”的作用则是把函数的名字、函数类型以及形参类型、个数和顺序通知编译系统,以便在调用该函数时系统按此进行对照检查(例如函数名是否正确,实参与形参的类型和个数是否一致)。 在书写形式上,函数声明可以把函数头部复制过来,在后面加一个分号;而且在参数表中可以只写各个参数的类型名,而不必写参数名。
默认参数
函数声明与定义的关系
函数的定义 “函数定义”是指对函数功能的确立,包括指定函数名,函数值类型、形参类型、函数体等,它是一个完整的、独立的函数单位: 包含函数类型、函数名、形参及形参类型、函数体等 在程序中,函数的定义只能有一次 函数首部与花括号间不加分号 函数的声明 函数的声明的作用则是把函数的名字、函数类型以及形参类型、个数和顺序通知编译系统,以便在调用该函数时系统按此进行对照检查(例如函数名是否正确,实参与形参的类型和个数是否一致): 不包含函数体(或形参) 调用几次该函数就应在各个主调函数中做相应声明 函数声明是一个说明语句,必须以分号结束
函数参数
实参argument与形参parameter
局部变量
参数的类型
一维数组名
数组表示与指针表示
当且仅当用于函数头或函数原型中时,int * arr和int arr [ ]的含义才是相同的,它们都表示arr是一个int指针。 但是数组表示法(int arr[ ])提示用户,arr不仅指向int,还指向int数组的第一个int元素。所以,当指针指向数组第一个元素时,建议使用数组表示法;而当指针指向一个独立的值时,使用指针表示法。 这也揭示了数组作为函数参数,本质就是以数组第一个元素的地址为值的指针作为参数。 进一步推论:如果在函数中对数组内容进行修改将影响到原始数组的内容,因为它本质就是指向原始数组的指针!
显式传递数组大小
在函数原型或定义中用数组名作为函数参数,本质上是以指针作为参数,所以必须显式地传递数组长度,而不能在函数中对该数组名使用sizeof 来获取数组的长度:指针本身并没有指出数组的长度,对指针使用sizeof获取的只是指针本身的大小。 为了将数组类型和数组元素数量告诉数组处理函数,请通过两个不同的参数来传递它们: void fillArray(int arr[ ], int size); // prototype 而不要试图使用方括号表示法来传递数组的长度: void fillArray(int arr[size]); // NO -- bad prototype
二维数组名
牢记:数组名被视为其地址,因此其作为函数参数时相应的形参是一个指针。 对于一个形如 typeName data[M][N] 的二维数组,它的数组名是Data,该数组有M个元素。第一个元素本身又是一个由N个元素组成的数组。因此该二维数组data作为函数参数时相应的形参是一个指向由N个typeName类型元素组成的数组的指针,其原型如下(指针表示法): typeName (*p)[N] 还有一种格式与上述原型的含义完全相同,但更具有可读性(数组表示法): typeName arr[ ][N] 上述两个原型都指出,该形参是指针而不是数组。还需要注意,指针类型指出,它指向由N个typeName类型元素组成的数组。因此,指针类型指定看列数,所以不必单独把列数作为参数传递。但是,由于指定了二维数组的列数,所以函数只能接受由N列组成的数组(不限定行数)。
C风格字符串
C语言中表示字符串的方式有三种: char数组; 用双引号括起的字符串常量(也称为字符串字面值); 被设置为字符串地址的指针; 要将上述字符串作为参数传递给函数时,可以用char *,实际上传递的是字符串的第一个字符的地址。 C风格字符串与常规char数组的区别是:C风格字符串有内置的空字符结束符。这也意味着可以不必将字符串长度作为参数传递给函数,而函数可以使用循环检查字符串中的每个字符直到遇到结尾的空字符为止。
C风格字符串作为返回值
函数无法直接返回一个字符串,但是可以返回字符串的地址,而且这样做的效率很高。 // 创建指定个数和指定字符的C风格字符串 #include <iostream> using namespace std; char* buildstr(char c, int n); int main(void) { int times; char ch; cout << "输入一个字符:"; cin >> ch; cout << "输入需要重复字符的次数:"; cin >> times; char* ps = buildstr(ch, times); cout << ps << endl; delete[] ps; // 释放空间 ps = buildstr('+', 20); // 指针指向新的空间 cout << ps << endl; delete[] ps; // 释放空间 return 0; } char* buildstr(char c, int n) { char* p = new char[n+1]; for (int i = 0; i < n; i++) { p[i] = c; } p[n] = '\0'; return p; }
结构
对象
string
array
函数
要将不同函数作为函数A的参数,需要完成以下三个工作: 获取函数的地址。 函数名就是函数地址,比如函数 int add(int a, int b)的地址就是add。 声明一个函数指针。与声明函数的原型的方法类似,只是将函数名改为指针形式,比如int (*pf) (int a, int b);。 使用函数指针来调用函数。将指针形式的 *pf 看做函数名来调用函数即可,比如 int c = (*pf) (1, 2);。
引用
重载引用参数
参数传递方式
分类
按值传递
按指针传递
按引用传递
指导原则
这些仅仅是指导原则,而不是标准,很可能有充分的理由做出其他选择。
使用传递的值而不做修改的函数
使用传递的值而不做修改的函数: 如果数据对象很小,如内置数据类型或小型结构,则值传递。 如果数据对象是数组,则使用指针,因为这是唯一的选择,并且将指针声明为指向const的指针。 如果数据对象是较大的结构,则使用const指针或const引用,以提高程序的效率(节省复制结构所需的时间和空间)。 如果数据对象是类对象,则使用const引用。传递类对象参数的标准方式就是按引用传递(类设计的语义常常要求使用引用)。
修改调用函数中数据的函数
修改调用函数中数据的函数: 如果数据对象是内置数据类型,则使用指针。如果看到诸如fixit(&x)这样的代码(其中的x是int),则明显地表示该函数将要修改x。 如果数据对象是数组,则只能使用指针。 如果数据对象是结构,则使用引用或指针。 如果数据对象是类对象,则使用引用。
经验
基本类型值传递、数组指针传递、类对象引用传递
函数分类
普通函数
递归函数
内联函数
关键字:inline
友元函数
关键字friend
深入
函数重载(函数多态)
方式:使用不同的参数列表完成相同的工作
参数列表(函数特征标)
参数列表又称函数特征标(function signature):参数数目、类型、顺序。
返回值
重载函数的返回值可以不同,但是特征标也必须不同。不存在特征标一样而返回值不同的重载函数。
函数模板
定义
template<typename T>
向下兼容
template<class T>
函数模板重载
模板的局限性
实例化
隐式实例化
隐式实例化比较简单,就是最正常模板函数调用,编译器根据参数来定义函数实例。
显式实例化
显式实例化直接使用了具体的函数参数类型,而不是让程序去自动判断。 显式实例化意味着可以直接命令编译器创建特定的实例,有两种显式声明的方式。 比如存在这么一个模板函数 template <typename T> void Swap(T &a, T &b) 第一种方式是声明所需的种类,用<>符号来指示类型,并在声明前加上关键词template,如下: template void Swap<int>(int &, int &); // 但是不用给出实现,因为已经有了通用的函数模板实现,这是只是明确指出要实例化函数的类型 第二种方式是直接在程序中使用函数创建,如下: Swap<int>(a,b); // 函数模板-显式实例化 #include<iostream> template<typename T> T Add(T a, T b) { return a + b; } int main() { int m = 6; double x = 10.2; //std::cout << Add(m, x) << std::endl; // 显式没有参数列表匹配的模板 std::cout << Add<double>(m, x) << std::endl; // 使用显式实例化,将函数模板强制为double类型实例化 // 并将参数m强制转换为double类型,以便与函数Add(double, double) // 的第二个参数匹配 }
显式具体化
如果某个特殊参数类型(对象类型)的函数名与已有的函数模板同名,但是该函数的处理内容又与已有函数模板不一致,就需要函数模板显式具体化。 函数模板显式具体化的优先级会大于普通的函数模板,从而保证了在处理特殊参数类型时使用函数模板显式具体化的函数。
template<>
对比
C++98标准 第三代具体化(ISO/ANSI C++标准) 对于给定的函数名,可以有非模板函数、模板函数和显式具体化函数以及它们的重载版本。 显式具体化的原型和定义应该以template<>打头,并通过名称来指出类型。 使用优先级:非模板函数 > 具体化函数 > 常规模板
例子
//函数模板-具体化 #include<iostream> struct Job { char name[40]; double salary; int floor; }; template<typename T> void swap(T&, T&); // 模板原型 template <> void swap<Job>(Job&, Job&); // 显式具体化 int main(void) { char c1 = 'a', c2 = 'b'; swap<char>(c1, c2); // 显式实例化 std::cout << c1 << "\t" << c2 << std::endl; int n1 = 1, n2 = 2; swap(n1, n2); // 隐式实例化 std::cout << n1 << "\t" << n2 << std::endl; Job job1 = { "ZhangLiang", 12.9, 1 }; Job job2 = { "WangChao", 24.7, 2 }; swap(job1, job2); // 显式具体化 std::cout << job1.name << "\t" << job1.salary << '\t' << job1.floor <<std::endl; std::cout << job2.name << "\t" << job2.salary << '\t' << job2.floor <<std::endl; return 0; } template<typename T> void swap(T& a, T& b) { T temp; temp = a; a = b; b = temp; } template<> void swap(Job& j1, Job& j2) { double t1; int t2; t1 = j1.salary; j1.salary = j2.salary; j2.salary = t1; t2 = j1.floor; j1.floor = j2.floor; j2.floor = t2; }
后置返回类型
//函数模板-后置返回类型 #include<iostream> template<typename T1, typename T2> auto gt(T1 x, T2 y) -> decltype(x + y) { return x + y; } int main() { int n = 100; double d = 150.20; std::cout << gt(n, d) << std::endl; return 0; }
关键字decltype
可变参数模板
参数包
模板参数包和函数参数包
C++11提供了一个用省略号表示的元运算符(meta-operator),让您能够声明表示模板参数包的标识符,模板参数包基本上是一个类型列表。同样,它还让您能够声明表示函数参数包的标识符,而函数参数包基,本上是一个值列表。其语法如下: template<typename... Args> // Args is a template parameter pack void show_list1(Args... args) // args is a function parameter pack 其中, Args是一个模板参数包,而args是一个函数参数包。与其他参数名一样,可将这些参数包的名称指定为任何符合C++标识符规则的名称。Args和T的差别在于, T与一种类型匹配,而Args与任意数量(包括零)的类型匹配。 更准确地说,这意味着函数参数包args包含的值列表与模板参数包Args包含的类型列表匹配,无论是类型还是数量。
元运算符...
展开参数包
在可变参数模板函数中使用递归
在可变参数模板函数中使用递归的核心理念是,将函数参数包展开,对列表中的第一项进行处理,再将余下的内容传递给递归调用,以此类推,直到列表为空。 与常规递归一样,确保递归将终止很重要。这里的技巧是将模板头改为如下所示; template<typename r, typename... Args> void show list3( T value, Args... args) 对于上述定义, show list3( )的第一个实参决定了T和value的值,而其他实参决定了Args和args的值。这让函数能够对value进行处理,如显示它。然后,可递归调用show_list3( ),并以args...的方式将其他实参传递给它。每次递归调用都将显示一个值,并传递缩短了的列表,直到列表为空为止。 示例代码 // variadic2.cpp #include <iostream> #include <string> // definition for 0 parameters void show_list() {} // definition for 1 parameter template<typename T> void show_list(const T& value) { std::cout << value << '\n'; } // definition for 2 or more parameters template<typename T, typename... Args> // Args表示模版参数包 void show_list(const T& value, const Args&... args) // args表示函数参数包 { std::cout << value << ", "; show_list(args...); // args... 表示展开参数包 } int main() { int n = 14; double x = 2.71828; std::string mr = "Mr. String objects!"; show_list(n, x); show_list(x*x, '!', 7, mr); return 0; }
重载解析
重载解析:在函数重载、函数模板、函数模板重载中,定义一个良好的决策来决定为函数调用使用哪一个函数定义。
重载解析过程
类型转换优先级
完全匹配和最佳匹配
函数调用的实现
栈
输入输出和文件
基础
控制台
输入
对象cin
cin是istream对象
istream类
必须包含头文件iostram
cin >> 变量
先输入数字再输入字符
可以连续读取多个以空格分隔的数字,当输入回车换行时将停止输入,但是要注意此时的换行符留在了输入队列中。如果要清除这个换行符,可以使用cin.get( ) // 读取数值数据后注意处理输入队列中留下的换行符 #include <iostream> using namespace std; const int NAME_LENGHTH = 100; int main(int argc, char* argv[]) { int age; char name[NAME_LENGHTH]; cout << "输入你年龄:" << endl; (cin >> age).get(); // 或者分开书写 cin>> age; cin.get(); cout << "输入你的姓名:" << endl; cin.getline(name, NAME_LENGHTH); cout << name << "你的年龄是:" << age << "岁"<< endl; }
输入字符
读取一个字符
cin>>ch
cin>>ch将跳过空格、换行符和制表符然后将内容输入ch中。
cin.get()
不接受任何参数的cin.get()成员函数返回输入中的下一个字符。可以如下使用: ch = cin.get(); 当字符输入时,该函数的将返回int类型的字符编码;当到达EOF时,该函数返回EOF。 例子: #include <iostream> using namespace std; int main(int argc, char* argv[]) { int ch; int count = 0; while ((ch = cin.get()) != EOF) { cout.put(char(ch)); ++count; } cout << endl << "一共输入了" << count << "个字符"; return 0; }
cin.get(char对象名)
cin.get(ch) 成员函数把读取的字符(如果成功读取)放入ch中,并且返回cin对象,该对象稍后会转化成bool值。 while(cin.get(ch)) { //do something } 首先调用cin.get(ch),如果成功,则将值放入ch中。然后获得函数调用的返回值,即cin。接下来,程序对cin进行bool转换,如果输入成功,则结果为true,否则为false。三条指导原则(确定结束条件、对条件进行初始化、更新条件)全部放在循环测试中。
读取一行字符串
cin.getline(字符数组名, 长度);
cin.getline(字符数组名, 长度)函数每次读取一行,它通过换行符来确定行尾,但是不保存换行符,并且,在存储字符串时,它用空字符来代替换行符。
cin.get(字符数组名, 长度)
cin.get(字符数组名, 长度)函数每次读取一行,它通过换行符来确定行尾,但是它将换行符留在输入队列中。 该函数的重载函数cin.get( )可以读取下一个字符(即使是换行符),可以用它来处理换行符。 由以上规律可知以下两种函数的效果等价: cin.get(字符数组名, 长度).get( ); cin.getline(字符数组名, 长度);
getline(cin, string对象名)
getline( )是istream的一个类方法
cin状态标志
cin.eof()
cin.fail()
cin.good()
输入错误
分类
类型不匹配
int n; cin >> n; 如果用户输入单词而不是数字,会出现类型不匹配,将会导致以下4个结果: n的值保持不变。 不匹配的输入将被留在输入队列中。 cin对象中的错误标记被设置。 对cin方法的调用将返回false(如果被转换为bool类型)
解决方案
重置cin以接受新的输入。 删除错误输入。 提示用户再输入。
条件判断
if(!cin) / while(!cin) 在std::basic_ios中定义了operator bool:explicit operator bool() const; //C++11 所以如果对标准IO流进行形如if (!cin)的bool判断时,便会调用operator bool,其返回的结果将会作为if的判断条件。 于是当cin输入流出现错误时,if (!cin)中的条件会被判断为真。 例如: // 填充数组,并返回实际写入数组的元素个数 int fill_array(double arr[], int limit) { double temp; int i; // 记录实际写入数组的元素个数 for (i = 0; i < limit; i++) { cout << "输入第" << (i + 1) << "个数的值:"; cin >> temp; if (!cin) { cin.clear(); // 重置输入 while (cin.get() != '\n') { continue; //清除错误输入 } cout << "输入错误,输入程序中断。\n"; break; } else if (temp < 0) // 如果是负数则退出输入 break; arr[i] = temp; } return i; } 说明: 数据到temp,temp是double类型,当从缓冲读取到其它非数字字符时,意味着读取失败。失败就给cin.flag产生错误标志,当错误标志存在时,cin就不能工作了,必须先清除掉。所以有了cin.clear() 1.当错误发生是,缓冲中已经被读取的字符已然没了,余下的都是从错误字符往后的字符了(产生错误的字符也已经被读走了,否则怎么产生错误)。Enter是在最后的,当然还存在着。 cin.clear(),只清除了错误标记。 2.既然发生了错误,就干脆把缓冲清空,重新进行输入。 所以就使用cin.get()从缓冲读取字符,直到连回车一起读走。每读一个字符,缓冲里就少一个字符。
cin.clear()、cin.sync()和cin.ignor()
输出
对象cout
ostream类
输出不同进制的整数
控制符 dec、 oct、 hex 分别用于指示cout以十进制、八进制和十六进制格式显示整数。默认格式为十进制,在修改格式之前,原来的格式将一直有效。 例如: cout << hex; cout << 32;
输出一个字符
cout.put(char)
控制输出的格式
cout.setf()
输出布尔值true、false
通常cout显示bool值之前将它们转换为int,但cout.setf(ios_base::boolalpha)函数设置了一个标记,该标记命令cout显示true和false,而不是1和0。 例如: #include <iostream> using namespace std; int main() { cout << (1 < 2) << endl; cout.setf(ios_base::boolalpha); cout << (1 < 2) << endl; return 0; }
输出char和wchar_t类型字符
控制符endl
文件
逻辑划分
文本文件
//写入与读取文本文件示例 #include <iostream> #include<fstream> using namespace std; const char FileName[] = "fish.txt"; int main(void) { // 向文本文件写入内容 ofstream fout; fout.open(FileName); double wt1 = 2020; double wt2 = 2030; double wt3 = 2040; fout << wt1 << "\t" << wt2 << "\t" << wt3 << endl; char line[100] = "Objects are closer than they appear."; fout << line ; fout.close(); //读取文本内容 ifstream inFile; inFile.open(FileName); if (!inFile.is_open()) { cout << "无法打开文件:" << FileName << endl; cout << "终止程序运行" << endl; exit(EXIT_FAILURE); } fout.close(); double value; char str[100]; while (inFile.good()) // while文件输入数据与变量类型匹配并且没有到文件末尾 { for (int i = 0; i < 3; i++) { inFile >> value; (i < 2) ? (cout << value << ", ") : (cout << value); } cout << endl; inFile.get(); inFile.getline(str, 100); cout << str; if (inFile.eof()) // if 到文件末尾 cout << "已经到了文件尾端" << endl; else if (inFile.fail()) // if 输入数据与变量类型匹配 cout << "输入数据与变量类型不匹配" << endl; else cout << "未知原因" << endl; } inFile.close(); //关闭文件 return 0; }
读取
ifstream类
写入
ofstream类
文本文件输出的主要步骤: 包含头文件fstream,比如 #include<fstream> 创建一个ofstream对象,比如 ofstream fout; 将该ofstream对象同一个文件关联起来,比如 fout.open("filename.txt"); 就像使用cout那样使用该ofstream对象,比如 fout << "hello world." << endl; 不在使用该ofstream对象时,关闭该对象,比如 fout.close( )
二进制文件
区别
文件结尾EOF
很多编程环境将Ctril + Z 视为模拟的EOF
回车与换行
\r与\n与\n\r
不同操作系统下换行符不同,如下: CR:Carriage Return,对应ASCII中转义字符\r,表示回车。MAC OS LF:Linefeed,对应ASCII中转义字符\n,表示换行。UNIX CRLF:Carriage Return & Linefeed,\r\n,表示回车并换行。window。通常C++在读取文件时将这两个字符转换为换行符\n,并在写入文件时执行相反的操作。 我们经常遇到的一个问题就是,Unix/Mac系统下的文件在Windows里打开的话,所有文字会变成一行;而Windows里的文件在Unix/Mac下打开的话,在每行的结尾可能会多出一个^M符号。
深入
输入和输出概述
流和缓冲区
流是进出程序的字节流。缓冲区是内存中的临时存储区域,是程序与文件或其他I/O设备之间的桥梁。信息在缓冲区和文件之间传输时,将使用设备(如磁盘驱动器)处理效率最高的尺寸以大块数据的方式进行传输。信息在缓冲区和程序之间传输时,是逐字节传输的,这种方式对于程序中的处理操作更为方便。
streambuf类
iostream文件
ios_base类
ios类
ostream类
istream类
iostream类
自动创建对象
用于窄字符流
cin、cout、cerr、clog
用于宽字符流
wcin、wcout、wcerr、wclog
重定向
标准输入和输出流通常是指键盘和屏幕
可使用重定向改变流连接对象
在默认情况下,标准输出和标准错误都将输出发送给标准输出设备(通常为显示器)。然而,如果要求操作系统将输出重定向到文件,则标准输出将与文件(而不是显示器)相连,但标准错误仍与显示器相连。
输入重定向<
输出重定向>
使用cout进行输出
输出基本类型和字符串
插入运算符<<
ostream类为所有基本类型以及以下指针类型重载了插入运算符: const signed char*; const unsigned char*; const char* void* 所以可以通过ostream类的对象cout用插入运算符<<输出它们。 插入运算符<<的返回值为调用该运算符的对象。
输出字符put( )
输出字符串write( )
刷新输出缓冲区
flush
endl
输出格式化
// 左侧设置格式化输出的函数效果与右侧的控制符效果一样 // cout成员函数(格式常量) 控制符 cout.setf(ios_base::boolalpha); cout << boolalpha; // 输入输出bool值 cout.unsetf(ios_base::boolalpha); cout << noboolalpha; cout.setf(ios_base::showbase); cout << showbase; // 对输出使用基数前缀 cout.unsetf(ios_base::showbase); cout << noshowbase; cout.setf(ios_base::showpoint); cout << showpoint; // 显式末尾小数点 cout.unsetf(ios_base::showpoint); cout << noshowpoint; cout.setf(ios_base::showpos); cout << showpos; // 整数前面加上+ cout.unsetf(ios_base::showpos); cout << noshowpos; cout.setf(ios_base::uppercase); cout << uppercase; // 对于16进制输出使用大写字母,E表示法 cout.unsetf(ios_base::uppercase); cout << nouppercase; cout.setf(ios_base::internal, ios_base::adjustfield); cout << internal; // 符号或基数前缀左对齐,值右对齐 cout.setf(ios_base::left, ios_base::adjustfield); cout << left; // 使用左对齐 cout.setf(ios_base::right, ios_base::adjustfield); cout << right; // 使用右对齐 cout.setf(ios_base::dec, ios_base::basefield); cout << dec; // 使用基数 10 cout.setf(ios_base::hex, ios_base::basefield); cout << hex; // 使用基数 16 cout.setf(ios_base::oct, ios_base::basefield); cout << oct; // 使用基数 8 cout.setf(ios_base::fixed, ios_base::floatfield); cout << fixed; // 使用定点计数法 cout.setf(ios_base::scientific, ios_base::floatfield); cout << scientific; // 使用科学计数法 setf( )函数原型如下: fmflags setf(fmflags); fmflags setf(fmflags, fmflags); 总的来说,使用控制符比使用成员函数更方便。
格式常量
标准控制符
浮点数的精度
浮点数精度的含义取决于输出模式。在默认模式下,它指的是显示的总位数。在定点模式和科学模式下,精度指的是小数点后面的位数。已经知道, C++的默认精度为6位(但末尾的0将不显示), precision()成员函数使得能够选择其他值。例如,下面语句将cout的精度设置为2: cout.precision (2); 精度设置将一直有效,直到被重新设置。
cout成员函数precison( )、width( )、fill( )
头文件iomanip
控制符setprecision( )、setw( )、setfill( )
使用cin进行输入
输入基本类型和字符串
格式化抽取方法
抽取运算符>>
cin>>检查输入
跳过空白(空格、换行、制表符)
流状态
流状态成员
到达文件尾
eofbit
eofbit 如果到达文件尾,则eofbit设置为1.
eof( )
流被破坏
badbit
badbit 如果流被破坏,则badbit设值为1。比如,文件读取错误。
bad( )
与预期不符
failbit
failbit 如果如数操作未能读取预期的字符或输出操作没有写入预期的字符,则failbit设置为1。
fail( )
正常状态
goodbit
goodbit goodbit表示正常状态,值是0。
good( )
如果流可以使用(所有位都被清除),则good( )返回true。
操作
返回流状态rdstate( )
设置状态
clear( )
clear(iostate s) 将流状态设置为s,s的默认值是0(goodbit);如果 (rdstate( )&exceptions( )) != 0 ,则引发异常basic_ios::failure
setstate( )
setstate(iostate s) 调用clear(rdstate( ) | s)。这将设置与s中设置的位对应的流状态位,其它流状态位保持不变。
流状态的影响
只有在流状态良好(所有位都被清除)的情况下,下面的测试才返回true: while(cin >> input) 如果测试失败,可以使用流状态成员函数来判断原因,比如: if(cin.eof())
I/O和异常
I/O和异常 假设某个输入函数设置了eofbit,这是否会导致异常被引发呢?在默认情况下,答案是否定的。但可以使用exceptions( )方法来控制异常如何被处理。 首先,介绍一些背景知识。exceptions( )方法返回一个位字段,它包含3位,分别对应于eofbit, failbit和badbit。修改流状态涉及clear( )或setstate( ),这都将使用clear( )。修改流状态后, clear( )方法将当前的流状态与exceptions( )返回的值进行比较。如果在返回值中某一位被设置,而当前状态中的对应位也被设置,则clear( )将引发ios base::failure异常。如果两个值都设置了badbit,将发生这种情况。如果exceptions( )返回goodbit,则不会引发任何异常。ios base::failure异常类是从std:exception类派生而来的,因此包含一个what(方法。 exceptions( )的默认设置为goodbit,也就是说,没有引发异常。但重载的exceptions (iostate)函数使得能够控制其行为: cin.exceptions (badbit); // setting badbit causes exception to be thrown 位运算符OR使得能够指定多位。例如,如果badbit或eofbit随后被设置,下面的语句将引发异常: cin.exceptions (badbit | eofbit);
异常
exceptions( )
exceptions( ) 返回一个位掩码,指出哪些标记导致异常被引发。
exceptions(isostate ex)
exceptions(isostate ex) 设置哪些状态将导致clear( )引发异常。例如,如果ex是eofbit,则当eofbit被设置时,clear( )将引发异常。
主动检测流状态
// cinexcp.cpp -- having cin throw an exception #include <iostream> int main() { using namespace std; cout << "Enter numbers: "; int sum = 0; int input; while (cin >> input) { sum += input; } cout << "Last value entered = " << input << endl; cout << "Sum = " << sum << endl; if (cin.fail() && !cin.eof()) // failed because of mismatched input { cin.clear(); // reset stream state while (!isspace(cin.get())) { continue; // get rid of bad input } /* while (cin.get() != '\n') { continue; // get rid of line } */ } else { cout << "I cannot go on!\n"; exit(1); } cout << "Now enter a new number: "; cin >> input; // will work now cout << input << endl; return 0; }
其它istream类方法
非格式化输入函数
单字符输入
cin.get(char&)
cin.get( )
对比
特征 cin.get(ch) ch = cin.get( ) 传输输入字符的方法 赋给参数ch 将函数返回值赋给ch 字符输入时函数的返回值 指向istream对象的引用 字符编码(int值) 达到文件尾时函数的返回值 转换为false EOF
字符串输入:getline( )、get( )、ignore( )
getine( )成员函数和get()的字符串读取版本都读取字符串,它们的函数特征标相同(这是从更为通用的模板声明简化而来的): istream & get (char *, int, char) ; istream & get (char *, int); istream getline(char *, int, char); istream & getline (char *, int); 第一个参数是用于放置输入字符串的内存单元的地址。第二个参数比要读取的最大字符数大1(额外的一个字符用于存储结尾的空字符,以便将输入存储为一个字符串),第3个参数指定用作分界符的字符,只有两个参数的版本将换行符用作分界符。上述函数都在读取最大数目的字符或遇到换行符后为止。 例如,下面的代码将字符输入读取到字符数组line中; char line [50]; cin.get (line, 50); cin.get( )函数将在到达第49个字符或遇到换行符(默认情况)后停止将输入读取到数组中。get()和getine( )之间的主要区别在于, get( )将换行符留在输入流中,这样接下来的输入操作首先看到是将是换行符,而gerline()抽取并丢弃输入流中的换行符。
意外字符输入:文件尾、流被破坏、无输入、输入到达或超过指定最大字符数
get(char *, int)和getline()的某些输入形式将影响流状态。与其他输入函数一样,这两个函数在遇到文件尾时将设置eofbit,遇到流被破坏(如设备故障)时将设置badbit.另外两种特殊情况是无输入以及输入到达或超过函数调用指定的最大字符数。 无输入 getline(char*, int) 如果没有读取任何字符(但换行符被视为读取了一个字符),则设置failbit get(char*, int) 如果没有读取任何字符,则设置failbit 超过函数调用指定的最大字符数 getline(char*, int)如果读取了最大数目的字符,且行中还有其它字符则设置failbit get(char*, int)它首先测试字符数,然后测试是否为文件尾以及下一个字符是否是换行符。如果它读取了最大数目的字符,则不设置failbit标记。然而,由此可以知道终止读取是否是由于输入字符过多引起的。可以用peek( ) 来查看下一个输入字符。如果它是换行符,则说明get( )已读取了整行;如果不是换行符,则说明get( )是在到达行尾前停止的。 现在来看一个例子,它使用peek( )来确定是否读取了整行。如果一行中只有部分内容被加入到输入数组中,程序将删除余下的内容。 // truncate.cpp -- using get() to truncate input line, if necessary #include <iostream> const int SLEN = 10; inline void eatline() { while (std::cin.get() != '\n') continue; } int main() { using std::cin; using std::cout; using std::endl; char name[SLEN]; char title[SLEN]; cout << "Enter your name: "; cin.get(name,SLEN); if (cin.peek( ) != '\n') cout << "Sorry, we only have enough room for " << name << endl; eatline(); cout << "Dear " << name << ", enter your title: \n"; cin.get(title,SLEN); if (cin.peek( ) != '\n') cout << "We were forced to truncate your title.\n"; eatline(); cout << " Name: " << name << "\nTitle: " << title << endl; // cin.get( ); return 0; } 注意,下面的代码确定第一条输入语句是否读取了整行: while (cin.get() != 'n') continue; 如果get( )读取了整行,它将保留换行符,而上述代码将读取并丢弃换行符。如果get()只读取一部分,则上述代码将读取并丢弃该行中余下的内容。如果不删除余下的内容,则下一条输入语句将从第一个输入行中余下部分的开始位置读取。对于这个例子,这将导致程序把字符串sniffer读取到title数组中。
read( )、peek( )、gcount( )、putback( )
文件输入和输出
fstream族与iostream族
iostream族支持程序与终端之间的I/O,而fstream族使用相同的接口提供程序和文件之间的I/O。
ifstream继承自istream
ofstream继承自ostream
fstream继承自iostream
简单的文件I/O
// https://github.com/lilinxiong/cppPrimerPlus-six-/blob/master/Chapter%2017/fileio.cpp // fileio.cpp -- saving to a file #include <iostream> // not needed for many systems #include <fstream> #include <string> int main() { using namespace std; string filename; cout << "Enter name for new file: "; cin >> filename; // create output stream object for new file and call it fout ofstream fout(filename.c_str()); fout << "For your eyes only!\n"; // write to file cout << "Enter your secret number: "; // write to screen float secret; cin >> secret; fout << "Your secret number is " << secret << endl; fout.close(); // close file // create input stream object for new file and call it fin ifstream fin(filename.c_str()); cout << "Here are the contents of " << filename << ":\n"; char ch; while (fin.get(ch)) // read character from file and cout << ch; // write it to screen cout << "Done\n"; fin.close(); // std::cin.get(); // std::cin.get(); return 0; }
流状态检查
is_open( )
打开多个文件
命令行处理技术
int main(int argc, char *argv[ ])
// count.cpp -- counting characters in a list of files #include <iostream> #include <fstream> #include <cstdlib> // or stdlib.h int main(int argc, char* argv[]) { using namespace std; if (argc == 1) // quit if no arguments { cerr << "Usage: " << argv[0] << " filename[s]\n"; exit(EXIT_FAILURE); } ifstream fin; // open stream long count; long total = 0; char ch; for (int file = 1; file < argc; file++) { fin.open(argv[file]); // connect stream to argv[file] if (!fin.is_open()) { cerr << "Could not open " << argv[file] << endl; fin.clear(); continue; } count = 0; while (fin.get(ch)) count++; cout << count << " characters in " << argv[file] << endl; total += count; fin.clear(); // needed for some implementations fin.close(); // disconnect file } cout << total << " characters in all files\n"; return 0; }
文件模式
文件模式描述的是文件将被如何使用:读、写、追加等。将流与文件关联时(无论是使用文件名初始化文件流对象,还是使用open()方法),都可以提供指定文件模式的第二个参数: ifstream fin("banjo", model); // constructor with mode argument ofstream fout( ) fout.open ("harp", mode2); // open( ) with mode arguments ios_base类定义了一个openmode类型,用于表示模式;与fmtflags和iostate类型一样,它也是一种bitmask类型(以前,其类型为int),可以选择ios_base类中定义的多个常量来指定模。
文件模式常量
文件模式常量 常量 含义 ios_base::in 打开文件,以便读取 ios_base::out 打开文件,以便写入 ios_base::ate 打开文件,并移到文件尾 ios_base::app 追加到文件尾 ios_base::trunc 如果文件存在,则截断文件 ios_base::binary 二进制文件 ifstream open( )方法和构造函数用ios_base::in作为模式参数的默认值; ofstream open( )方法和构造函数用ios_base::out | ios_base::trunc作为模式参数的默认值。
C语言模式字符串:"r"、"w"、"a"、"r+"、"w+"
对应关系
随机存取
指针移动
输入指针移动
seekg( )
seekg( )用于ofstream对象,它的原型如下 basic_istream<charT, traits>& seekg(off_type, ios_base::seekdir); basic_istream<charT, traits>& seekp(pos_type) 对于char具体化,上面两个原型等同于下面的代码: seekg(streamoff, ios_base::seekdir); seekp(streampos); streamoff定位到离ios_base::seekdir文件位置特定距离的位置(单位是字节) streampos定位到离文件开头特定距离的位置(单位是字节) seek_dir参数是ios_base类中定义的另一种整型,有3个可能的值。 常量ios_base::beg 指文件头位置 常量ios_base::cur 指当前位置 常量ios_base::end 指文件尾位置
输出指针移动
seekp( )
获取指针当前位置
对于输入流
tellg( )
对于输出流
tellp( )
注意
如果要检查文件指针的当前位置,则对于输入流,可以使用tellg( )方法,对于输出流,可以使用tellp( )方法。它们都返回一个表示当前位置的streampos值(以字节为单位,从文件开始处算起)。创建fstream对象时,输入指针和输出指针将一前一后地移动,因此tellg( )和tellp( ) 返回的值相同。然而,如果使用istream对象来管理输入流,而使用ostream对象来管理同一个文件的输出流,则输入指针和输出指针将彼此独立地移动,因此tellg( )和tellp()将返回不同的值。
使用临时文件
使用临时文件 开发应用程序时,经常需要使用临时文件,这种文件的存在是短暂的,必须受程序控制。您是否考虑过,在C++中如何使用临时文件呢?创建临时文件、复制另一个文件的内容并删除文件其实都很简单。首先,需要为临时文件制定一个命名方案,但如何确保每个文件都被指定了独一无二的文件名呢? cstdio 中声明的tmpnam()标准函数可以帮助您。 char* tmpnam( char* pszName); tmpnam( )函数创建一个临时文件名,将它放在pszName指向的C-风格字符串中。常量Ltmpnam和TMP_MAX (二者都是在cstdio中定义的)限制了文件名包含的字符数以及在确保当前目录中不生成重复文件名的情况下tmpnam()可被调用的最多次数。 生成10个临时文件名的代码 #include <iostream> #include<cstdio> int main(int argc, char* argv[]) { using namespace std; cout << TMP_MAX << "\t" << L_tmpnam << endl; char pszName[L_tmpnam] = { '\0' }; for (int i = 0; i < 10; i++) { tmpnam_s(pszName); cout << pszName << endl; } return 0; }
tmpnam( )
文件类型
文本文件
插入运算符<<写入、抽取运算符>>和get( )读取
二进制文件
write( )写入、read( )读取
例子
该程序将键盘输入读取到一个由string对象组成的vector中,将字符串内容(而不是string对象)存储到一个文件中,然后该文件的内容复制到另一个由string对象组成的vector中(来源:C++ Primer Plus 第6版 17章编程练习 第7题)。 技术:string对象的二进制文件存取。 #include <iostream> #include <string> #include <vector> #include <cstdlib> #include <algorithm> #include<fstream> // functor Store class Store { private: std::ostream& os; public: Store(std::ostream& o) : os(o) {} // overloaded funtions void operator()(const std::string& s); }; void Store::operator()(const std::string& s) { std::size_t len = s.length(); // store string length os.write((char*)&len, sizeof(std::size_t)); // store string data os.write(s.data(), len); } inline void ShowStr(const std::string& s); void GetStrs(std::ifstream& fin, std::vector<std::string>& vistr); int main() { using namespace std; vector<string> vostr; string temp; // acquire strings cout << "Enter strings (empty line to quit):\n"; while (getline(cin, temp) && temp[0] != '\0') vostr.push_back(temp); cout << "Here is your input" << endl; for_each(vostr.begin(), vostr.end(), ShowStr); // store in a file ofstream fout("strings.dat", ios_base::out | ios_base::binary); for_each(vostr.begin(), vostr.end(), Store(fout)); fout.close(); // recover file contents vector<string> vistr; ifstream fin("strings.dat", ios_base::in | ios_base::binary); if (!fin.is_open()) { cerr << "Could not open file for input." << endl; exit(EXIT_FAILURE); } GetStrs(fin, vistr); cout << "\nHere are the strings read from the file:\n"; for_each(vistr.begin(), vistr.end(), ShowStr); return 0; } inline void ShowStr(const std::string& s) { std::cout << s << std::endl; } void GetStrs(std::ifstream& fin, std::vector<std::string>& vistr) { std::size_t len; // string length while (fin.read((char*)&len, sizeof(std::size_t))) { std::string str; char ch; for (std::size_t i = 0; i < len; ++i) { fin.read(&ch, sizeof(char)); str.push_back(ch); } // put string to vector vistr.push_back(str); } }
对比
将数据存储在文件中时,可以将其存储为文本格式或二进制格式。文本格式指的是将所有内容(甚至数字)都存储为文本。例如,以文本格式存储值-2.324216e+07时,将存储该数字包含的13个字符。这需要将浮点数的计算机内部表示转换为字符格式,这正是<<插入运算符完成的工作。另一方面,二进制格式指的是存储值的计算机内部表示。也就是说,计算机不是存储字符,而是存储这个值的64位double表示。对于字符来说,二进制表示与文本表示是一样的,即字符的ASCII码的二进制表示。 每种格式都有自己的优点。文本格式便于读取,可以使用编辑器或字处理器来读取和编辑文本文件,可以很方便地将文本文件从一个计算机系统传输到另一个计算机系统。二进制格式对于数字来说比较精确,因为它存储的是值的内部表示,因此不会有转换误差或舍入误差。以二进制格式保存数据的速度更快,因为不需要转换,并可以大块地存储数据。二进制格式通常占用的空间较小,这取决于数据的特征。然而,如果另一个系统使用另一种内部表示,则可能无法将数据传输给该系统。同一系统上不同的编译器也可能使用不同的内部结构布局表示。在这种情况下,则必须编写一个将一种数据转换成另一种的程序。 二进制文件和文本文件 使用二进制文件模式时,程序将数据从内存传输给文件(反之亦然)时,将不会发生任何隐藏的转换,而默认的文本模式并非如此。例如,对于Windows文本文件,它们使用两个字符的组合(回车和换行)表示换行符; Macintosh文本文件使用回车来表示换行符;而UNIX和Linux文件使用换行(linefeed)来表示换行符。C++是从UNIX系统上发展而来的,因此也使用换行( linefeed )来表示换行符。为增加可移植性, Windows C++程序在写文本模式文件时, 自动将C++换行符转换为回车和换行; Macintosh C++程序在写文件时,将换行符转换为回车。在读取文本文件时,这些程序将本地换行符转换为C++格式。对于二进制数据,文本格式会引起问题,因此double值中间的宇节可能与换行符的ASCI1码有相同的位模式。另外,在文件尾的检测方式也有区别。因此以二进制格式保存数据时,应使用二进制文件模式(UNIX系统只有一种文件模式,因此对于它来说,二进制模式和文本模式是一样的)。
内核格式化
sstream族
iostream族支持程序与终端之间的I/O,而fstream族使用相同的接口提供程序和文件之间的I/O. C++库还提供了sstream族,它们使用相同的接口提供程序和string对象之间的I/O。也就是说,可以使用于cout的ostream方法将格式化信息写入到string对象中,并使用istream方法(如getline())来读取string对象中的信息。读取string对象中的格式化信息或将格式化信息写入string对象中被称为内核格式化(incore formatting)。
ostringstream继承自ostream
// https://github.com/lilinxiong/cppPrimerPlus-six-/blob/master/Chapter%2017/strout.cpp // strout.cpp -- incore formatting (output) #include <iostream> #include <sstream> #include <string> int main() { using namespace std; ostringstream outstr; // manages a string stream string hdisk; cout << "What's the name of your hard disk? "; getline(cin, hdisk); int cap; cout << "What's its capacity in GB? "; cin >> cap; // write formatted information to string stream outstr << "The hard disk " << hdisk << " has a capacity of " << cap << " gigabytes.\n"; string result = outstr.str(); // save result cout << result; // show contents // cin.get(); // cin.get(); return 0; }
str( )成员函数
istringstream继承自istream
// https://github.com/lilinxiong/cppPrimerPlus-six-/blob/master/Chapter%2017/strin.cpp // strin.cpp -- formatted reading from a char array #include <iostream> #include <sstream> #include <string> int main() { using namespace std; string lit = "It was a dark and stormy day, and " " the full moon glowed brilliantly. "; istringstream instr(lit); // use buf for input string word; while (instr >> word) // read a word a time cout << word << endl; // cin.get(); return 0; }
stringstream继承自iostream
sstream族给格式化的文本提供了缓冲区
内存模型
单独编译
文件与翻译单元
头文件管理
包含头文件命令#incude
"filename" 与<filename>
头文件内容
下面列出头文件中常包含的内容: 函数原型。 使用#define或const定义的符号常量。 结构声明。 类声明。 模板声明。 内联函数(不受单定义规则限制,但同一个函数的所有内联定义必须相同)。
预处理器编译命令
#ifndef
#define、#endif
多个库的连接
名称修饰
链接错误
重新编译源代码
介绍
分类:自动变量、寄存器变量(摒弃)、静态变量(包含3种)、动态变量
实质:自动变量、静态变量、动态变量(动态存储)
变量存储方式
自动变量和静态变量存储方式
自动变量
自动变量的初始化
自动变量和栈
静态变量
初始化
零初始化
常量表达式初始化
动态初始化
特性
静态持续变量 #include<iostream> #include"coordin.h" using namespace std; int global = 1000; // 静态持续性、外部链接性 static int one_file = 50; // 静态持续性、内部链接性 int main() { //... return 0; } void func1(int n) { static int count = 0; // 静态持续性、无链接性 }
静态持续性、外部链接性
全局变量(外部变量)
作用域解析运算符::
作用域解析运算符::放在变量名前时,该变量表示使用变量的全局版本。 #include<iostream> using namespace std; static int number = 10; int main() { int number = 100; cout << ::number << endl; // 10 return 0; }
静态持续性、内部链接性
局部变量(内部变量)
关键字static
关键字extern
静态存储持续性、无链接性
代码块和函数中
关键字static
存储三特性
持续性->变量在内存保留(持续)时间
自动存储持续性
静态存储持续性
线程存储持续性
动态存储持续性
作用域->变量在文件的多大范围内可见(可被程序使用)
代码块
文件
链接性->变量在哪些文件之间共享
无链接性(只能在当前函数或代码块中访问)
内部链接性(只在当前文件中访问)
外部链接性(可在其他文件中访问)
变量5种存储方式(引入命名空间前)特性总结
变量5种存储方式(引入命名空间前): 存储描述 持续性 作用域 链接性 如何声明 自动 自动 代码块 无 在代码块中 寄存器 自动 代码块 无 在代码块中,使用关键字register 静态,无连接性 静态 代码块 无 在代码块中,使用关键字static 静态,外部链接性 静态 文件 外部 不在任何函数内 静态,内部链接性 静态 文件 内部 不在任何函数内,使用关键字static
存储说明符
auto(C++11以后不再是说明符)
register
static
extern
thread_local
并行编程与关键字thread_local 多线程确实带来了很多问题。如果一个线程挂起或两个线程试图同时访问同一项数据,结果将如何呢?为解决并行性问题, C++定义了一个支持线程化执行的内存模型,添加了关键字thread_local,提供了相关的库支持。关键字thread_local将变量声明为静态存储,其持续性与特定线程相关;即定义这种变量的线程过期时,变量也将过期。
mutable
关键字mutable表示,即使结构或类变量为const,其某个成员也可以被修改。 #include<iostream> using namespace std; struct Data { char name[30]; mutable int accesees; }; int main() { const Data veep = { "Clay Clod", 0 }; // strcpy(veep.name, "Cheng Long"); // not allowed veep.accesees++; // allowed return 0; }
cv-限定符
关键字const
const全局变量链接性:内部
在C++中,全局const定义就像使用了static说明符一样,链接性是内部的。如果出于某种原因,希望某个常量的连接性为外部的,则可以使用关键字extern来覆盖默认的内部链接性: extern const int states = 50; // 外部链接性定义常量 在这种情况下,必须在所有使用该常量的头文件中使用extern关键字来声明它(这与常规外部变量不同,定义常规外部变量时,不必使用extern关键字,只在使用该变量的文件中必须使用extern),且只有一个文件该常量进行定义。
关键字volatile
转换
链接性拓展
变量和链接性
函数和链接性
和变量一样,函数也有链接性,虽然可选择的范围比变量小。和c语言一样, C++不允许在一个函数中定义另外一个函数,因此所有函数的存储持续性都自动为静态的,即在整个程序执行期间都一直存在。在默认情况下,函数的链接性为外部的,即可以在文件间共享。实际上,可以在函数原型中使用关键字extern来指出函数是在另一个文件中定义的,不过这是可选的(要让程序在另一个文件中查找函数,该文件必须作为程序的组成部分被编译,或者是由链接程序搜索的库文件)。还可以使用关键字static将函数的链接性设置为内部的,使之只能在一个文件中使用。必须同时在原型和函数定义中使用该关键字: static int private (double x); static int private (double x) { ... } 这意味着该函数只在这个文件中可见,还意味着可以在其他文件中定义同名的的函数。和变量一样,在定义静态函数的文件中,静态函数将覆盖外部定义,因此即使在外部定义了同名的函数,该文件仍将使用静态函数。 单定义规则也适用于非内联函数,因此对于每个非内联函数,程序只能包含一个定义。对于链接性为外部的函数来说,这意味着在多文件程序中,只能有一个文件(该文件可能是库文件,而不是您提供的)包含该函数的定义,但使用该函数的每个文件都应包含其函数原型。内联函数不受这项规则的约束,这允许程序员能够将内联函数的定义放在头文件中。这样,包含了头C++要求同一个函数的所有内联定义都必须相同。
持续性:静态
链接性:默认外部链接性
内部链接性:关键字static
非内联函数的单定义规则
内联函数特殊性
C++在哪里查找函数
C++在哪里查找函数 假设在程序的某个文件中调用一个函数, C++将到哪里去寻找该函数的定义呢?如果该文件中的函数原型指出该函数是静态的,则编译器将只在该文件中查找函数定义;否则,编译器(包括链接程序)将在所有的程序文件中查找。如果找到两个定义,编译器将发出错误消息,因为每个外部函数只能有一个定义。如果在程序文件中没有找到,编译器将在库中搜索。这意味着如果定义了一个与库函数同名的函数,编译器将使用程序员定义的版本,而不是库函数(然而, C++保留了标准库函数的名称,即程序员不应使用它n),有些编译器-链接程序要求显式地指出要搜索哪些库,
语言和链接性
C语言链接规范和C++语言链接规范
由于C语言链接和C++语言链接对函数名翻译方法不一样,可能会导致链接程序匹配函数时出现问题,因此可以显示地通过关键字extern来指明要使用哪种语言的链接规范。 extern "C" void spiff(int); //使用C语言链接规范 extern void spoff(int); //使用C++语言链接规范(默认) extern "C++" void spoff(int);//使用C++语言链接规范
存储方案和动态分配
动态分配内存方式
C++运算符new
new:运算符、函数和替换函数
运算符new和new[ ]分别调用以下分配函数(它们位于全局名称空间中): void * operator new(std::size_t); void * operator new [ ] (std::size_t) 同样,也有delete和delete [ ] 调用的释放函数: void operator delete(void *); void operator delete [ ](void *); 它们都使用了运算符重载语法。std::size_t是一个typedef,对应于合适的整型。 对于下面这样的基本语句会被自动转换: int * pi = new int; 转换为 int * pi = new(sizeof(int)); int * pa = new int[40]; 转换为 int * pa = new(40 * sizeof(int)); 类似,delete pi; 被转换为 delete(pi); 并且这些函数为可替换函数,可以根据需要对齐进行定制。在代码中仍然使用new运算符,但它调用自定义的new( )函数。
new失败时
引发异常std::bad_alloc
定位new运算符
程序员必须自己负责管理定位new运算符指向的内存单元。
内置类型与定位new运算符
对象与定位new运算符
显式调用析构函数
C函数malloc( )
malloc的全称是memory allocation,中文叫动态内存分配,用于申请一块连续的指定大小的内存块区域以void*类型返回分配的内存区域地址,当无法知道内存具体位置的时候,想要绑定真正的内存空间,就需要用到动态的分配内存,且分配的大小就是程序要求的大小。
变量5种存储方式不适用于动态分配的内存(动态存储)
存储方式仍然适用于用来跟踪动态内存的指针变量(自动或静态静态指针变量)
编译器使用三块独立的内存
一块用于静态变量(可再细分)
一块用于自动变量
一块用于动态存储
类和动态内存分配
命名空间
namespace 命名空间,也称为名称空间。
传统C++命名空间
声明区域
声明区域(declarationregion)是可以在其中进行声明的区域。例如,可以在函数外面声明全局变量,对于这种变量,其声明区域为其声明所在的文件。对于在函数值声明的变量,其声明区域为其所在的代码块。 C++关千全局变量和局部变量的规则定义了一种名称空间层次。每个声明区域都可以声明名称,这些名称独立于在其他声明区域中声明的名称。在一个函数中声明的局部变量不会与在另一个函数中声明的局部变量发生冲突。
潜在作用域
潜在作用域(potential scope):从变量声明点开始到其声明区域的结尾为变量的潜在作用域。 因此潜在作用域比声明区域小,这是由于变量必须先定义才能使用。
作用域
变量对程序而言可见的范围被称为作用域。 变量并非在其潜在作用域内的任何位置都是可见的。例如,它可能被另一个在嵌套声明区域中声明的同名变量隐藏。例如,在函数中声明的局部变量(对千这种变量,声明区域为整个函数)将隐藏在同一个文件中声明的全局变量(对千这种变量,声明区域为整个文件)。变量对程序而言可见的范围被称为作用域(scope)。
分类
全局(文件)作用域
局部(代码块)作用域
类作用域
访问作用域内成员方法
// 访问作用域内成员方法 #include<iostream> class IK { private: int fuss; // fuss 属于类作用域 public: IK(int f = 9) { fuss = f; } // fuss 在作用域中 void viewIK() const; // viewIK 属于类作用域 }; void IK::viewIK() const // 通过类名和域名解析符IK:: 让viewIK 进入IK类作用域 { std::cout << fuss << std::endl; // fuss 在类方法的作用域中 } int main() { IK* pik = new IK; IK ee = IK(8); // 构造器通过类名进入类作用域中 ee.viewIK(); // 通过类对象ee和直接成员运算符让viewIK 进入IK类作用域 pik->viewIK(); // 通过指向类对象的指针pik和间接成员运算符让viewIK 进入IK类作用域 }
作用域解析运算符::
直接成员运算符.
间接成员运算符->
新的命名空间特性
C++新增了这样一种功能,即通过定义一种新的声明区域来创建命名的名称空间,这样做的目的之一是提供一个声明名称的区域。一个名称空间中的名称不会与另外一个名称空间的相同名称发生冲突,同时允许程序的其它部分使用该名称空间中声明的东西。
分类
全局命名空间
全局变量
自定义命名空间
下面使用关键字namespace创建命名空间Jack。 namespace Jack{ double pail; void fetch; int pal; struct Well {...}; }
关键字namespace
特性
外部链接性(默认)
命名空间可以是全局的,也可以位于另一个命名空间中,但不能位于代码块中。因此在默认情况下,在命名空间中声明的名称的连接性是外部的(除非它引用了常量)。
解决命名冲突
任何命名空间中的名称都不会与其它命名空间中的名称发生冲突。
开放性
命名空间是开放的,可以把名称加入已有的命名空间中。 例如,下面这条语句将名称goose添加到Jill已有的命名空间中: namespace Jill{ char * goose (const char *); } 同样,在原来的Jack命名空间中提供了fetch( )函数的原型,现在可以在该文件后面(或者其它文件中)再次使用Jack命名空间来提供该函数的定义: namespace Jack{ void fetch( ) { ... } }
传递性
可以嵌套
可别名
无名命名空间
// 无名命名空间 #include<iostream> //static int counts; // 静态存储空间,内部链接性 namespace { int counts; // 静态存储空间,内部链接性。可以替代链接性为内部的静态变量。 } int main() { std::cout << counts << std::endl; return 0; }
访问命名空间中名称的方法
直接指定标识符
作用域解析运算符::
命名空间名称::想访问的名称
关键字using
using声明
using声明使特定的标识符可用。 在函数外使用using声明时,将把名称添加到全局名称空间中。 // 命名空间-using声明 #include<iostream> #include<string> namespace Jill { double bucket(double n) { return n; }; double fetch; struct Hill { int length = 1; }; } char fetch; // 全局命名空间 int main() { using Jill::fetch; // 将命名空间Jill中的fetch导入当局部命名空间 //double fetch; // 错误!因为已经有了一个局部变量也名为fetch std::cin >> fetch; // 获取一个值导入Jill::fetch std::cin >> ::fetch;// 获取一个值导入全局fetch return 0; } /*解析 由于using声明将名称添加到局部声明区域中,因此这个示例避免了将另一个 局部变量也命名为fetch。另外,和其他局部变量一样, fetch也将覆盖同名的全局变量。 如果在函数的外面使用using声明时,将把名称添加到全局名称空间中。 */
using编译指令
using编译指令使整个命名空间可用。 比如using namespace std; // 命名空间-using编译指令 #include<iostream> #include<string> namespace Jill { double bucket(double n) { return n; }; double fetch; struct Hill { int length = 1; }; } char fetch; // 全局命名空间 int main() { using namespace Jill; // 导入Jill命名空间的所有名称 Hill Thrill; // 创建一个Jill::Hill类型的结构 double water = bucket(2); // 使用Jill::bucket();函数 double fetch; // 局部变量将屏蔽Jill::bucket std::cin >> fetch; // 将值写入局部fetch变量 std::cin >> ::fetch; // 将值写入全局fetch变量 std::cin >> Jill::fetch; // 将值写入Jill::fetch变量 return 0; } int foom() { //Hill top; // 错误 Jill::Hill crest; // 有效的 } /*解析 在main()中,名称 Jill:: fetch被放在局部名称空间中,但其作用域不是局部的, 因此不会覆盖全局的fetch。然而,局部声明的fetch 将隐藏Jill::fetch 和全局fetch。 然而,如果使用作用域解析运算符,则后两个fetch变量都是可用的。可以与使用using 声明的示例进行比较。 虽然函数中的using编译指令将名称空间的名称视为在函数之外声明的, 但它不会使得 该文件中的其他函数能够使用这些名称。因此在foom()函数中不能使用未限定的标识符Hill。 */
对比
形式不同: using 声明:using 名称空间名 :: 名称 例如:using Jill:: fetch;//这是一个using 声明。 using 编译指令:using namespace 名称空间名 例如: using namespace Jill;//这是一个using 编译指令 using声明使名称空间中的特定标识符可用,例如 using Jill:: fetch;只有fetch这个名称可用; 而using 编译指令使得名称空间里得所有标识符可用。例如using namespace Jill 使得Jill空间里的所有名称可用。 但是使用using编译指令导入一个命名空间中所有的名称与使用多个using声明是不一样的,而更像是大量使用了作用域解析运算符。使用using 声明时,就好像声明了相应的变量一样,如果某个名称已经在函数中申明了,就不能使用using导入相同的名称。然而使用using 编译指令时,就像在包含using声明和命名空间本身的最小区域中声明了名称一样,如果与局部名称发生冲突,则局部名称称将覆盖名称空间版本(就像隐藏同名的全局变量一样),而编译器不会发出警告。不过仍然可以通过使用作用域解析符访问被隐藏的名称。 实例: using声明 在代码中使用using声明相当于定义此变量 namespace n1 { int x = 5; } int main() { using n1::x;//这里相当于int x = 5; int x = 5;//出错,双定义 } using编译指令 在代码块中使用using编译指令,则它其中的变量是全局的,但作用域是根据using编译指令的位置确定。 namespace n1 { int x = 5; } int main() { using namespace n1; int x = 5;//正确,局部变量覆盖全局变量 } 注意using编译指令的作用域 namespace n1 { int x = 5; } using namespace n1;//作用域是整个文件 int main() { int x = 5;//正确,局部变量覆盖全局变量 } namespace n1 { int x = 5; } int main() { using namespace n1;//作用域是main()函数 int x = 5;//正确,局部变量覆盖全局变量 }
使用命名空间的指导原则
随着程序员逐渐熟悉名称空间,将出现统一的编程理念。下面是当前的一些指导原则。 使用在己命名的名称空间中声明的变量,而不是使用外部全局变量。 使用在已命名的名称空间中声明的变量,而不是使用静态全局变量。 如果开发了一个函数库或类库,将其放在一个名称空间中。事实上, C++当前提倡将标准函数库放在名称空间std中,这种做法扩展到了来自C语言中的函数。例如,头文件math.h是与C语言兼容的,没有使用名称空间,但C++头文件cmath应将各种数学库函数放在名称空间std中。实际上,并非所有的编译器都完成了这种过渡。 仅将编译指令using作为一种将旧代码转换为使用名称空间的权宜之计。不要在头文件中使用using编译指令。首先,这样做掩盖了要让哪些名称可用;另外,包含头文件的顺序可能影响程序的行为。如果非要使用编译指令using,应将其放在所有预处理器编译指令#include之后。 导入名称时,首选使用作用域解析运算符或using声明的方法。 对于using声明,首选将其作用域设置为局部而不是全局。 别忘了,使用名称空间的主旨是简化大型编程项目的管理工作。对于只有一个文件的简单程序,使用using编译指令并非什么大逆不道的事。正如前面指出的,头文件名的变化反映了这些变化。老式头文件(如iostream.h)没有使用名称空间,但新头文件iostream使用了std名称空间。
面相对象
对象和类
过程性编程和面向对象编程
面相对象编程特性
面向对象编程(OOP)是一种特殊的、设计程序的概念性方法, C++通过一些特性改进了C语言,使得应用这种方法更容易。下面是最重要的OOP特性: 抽象 封装和数据隐藏 多态 继承 代码的可重用性 为了实现这些特性并将它们组合在一起,C++所做的最重要的改进是提供了类。
抽象和类
抽象是什么
抽象(Abstraction)是简化复杂的现实问题的途径。 抽象(Abstraction)是简化复杂的现实问题的途径,它可以为具体问题找到最恰当的类定义,并且可以在最恰当的继承级别解释问题。它可以忽略一个主题中与当前目标无关的那些方面,以便更充分地注意与当前目标有关的方面。抽象并不打算了解全部问题,而只是选择其中的一部分,暂时不用部分细节。抽象包括两个方面,一是过程抽象,二是数据抽象。它侧重于相关的细节和忽略不相关的细节。抽象作为识别基本行为和消除不相关的和繁琐的细节的过程,允许设计师专注于解决一个问题的考虑有关细节而不考虑不相关的较低级别的细节。
抽象(Abstraction)是简化复杂的现实问题的途径
包括
过程抽象
数据抽象
抽象和接口关系
类是什么
类是用户自定义类型的定义。类声明指定了数据将如何存储,同时指定了用来访问和操纵这些数据的方法(类成员函数)。
对象是什么
在面向对象(Object Oriented)的软件中,对象(Object)是某一个类(Class)的实例(Instance)。
类和对象的关系
类定义了一种类型,包括如何使用它。对象是一个变量或其他数据对象(如由new生成的),并根据类定义被创建和使用。类和对象之间的关系同标准类型与其变量之间的关系相同。
类如何实现抽象、数据隐藏和封装
类表示为用户可以使用类方法中的公共接口对类对象执行的操作,这是抽象。类的数据成员可以是私有的(默以值),这意味着只能通过成员函数来访问这些数据,这是数据隐藏。实现的具体细节(如数据表示和方法的代码)都是隐藏的,这是封装。
类与结构的区别、类与模板的区别
定义类
目标:使得使用类与使用基本的内置类型(如int)尽可能相同
关键字class
类规范
一般来说,类规范由两个部分组成。· 类声明:以数据成员的方式描述数据部分,以成员函数(被称为方法)的方式描述公有接口。 类方法定义:描述如何实现类成员函数。 简单地说,类声明提供了类的蓝图,而方法定义则提供了细节。 补充: 为开发一个类并编写一个使用它的程序,需要完成多个步骤。通常, C++程序员将接口(类声明)放在头文件中,并将类方法定义(类方法的代码)放在源代码文件中。
类声明(蓝图)
数据成员
成员函数
类方法定义(细节)
实现类成员函数
类设计步骤
1.提供类声明
访问控制
关键字private
默认private
数据成员常放在private部分
数据隐藏
将数据封装到私有部分中可以保护数据的完整性,这被称为数据隐藏。
关键字public
public和抽象
类成员可以是数据类型,也可以是函数类型。关键字public标识的类成员是类使用者看到的类的抽象部分——公共接口。
公共接口
函数成员常放在public部分
关键字protected
关键字protected与private相似,在类外只能用公有类成员来访问protected部分中的类成员。private和protected之间的区别只有在基类派生的类中才会表现出来。派生类的成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员。因此,对于外部世界来说,保护成员的行为与私有成员相似;但对于派生类来说,保护成员的行为与公有成员相似。
存储控制
关键字static
静态成员变量
静态成员函数
2.实现类成员函数
类成员函数特点
类作用域
作用域解析运算确定了方法定义对应的类的身份。可以说该方法具有类作用域(class scope)。
作用域解析运算符::
作用域解析运算符::标识函数所属类 类方法的完整名称中包括类名。类名::函数名是函数的限定名(qualified name);而简单的函数名全名的缩写(非限定名,unqualified name),它只能在类作用域中使用。
限定名
非限定名
类方法可以访问类的私有成员
内联函数
定义于类声明中的函数自动成为内联函数(隐式内联)。 定义于类声明之外的函数可以使用限定符inline变为内联函数(显式内联)。 注意: 内联函数的特殊规则要求在每个使用它们的文件中都对其进行定义。确保内联定义对多文件程序中的所有文件都可用的、最简便的方法是:将内联定义放在定义类的头文件中(有些开发系统包含智能链接程序,允许将内联定义放在一个独立的实现文件)。
内部链接性
共享一组成员函数
const成员函数
const成员函数可以保证函数不会修改该函数关联对象。 const成员函数声明即在函数的括号后面加上关键字const,比如 int fun( ) const;
使用简单的类
创建对象(类的实例)
将类名视为类型名
使用类函数(公有接口)
调用成员函数时,它将使用被用来调用它的对象的数据成员。 使用新类型:关键是了解类型成员函数的功能。
成员运算符句点.
客户-服务器模型
客户、服务器模型 OOP程序员常依照客户/服务器模型来讨论程序设计。在这个概念中,客户是使用类的程序。类声明(包括类方法)构成了服务器,它是程序可以使用的资源。客户只能通过以公有方式定义的接口使用服务器,这意味着客户(客户程序员)唯一的责任是了解该接口。服务器(服务器设计人员)的责任是确保服务器根据该接口可靠并准确地执行。服务器设计人员只能修改类设计的实现细节,而不能修改接口。这样程序员独立地对客户和服务器进行改进,对服务器的修改不会客户的行为造成意外的影响。
类的构造函数和析构函数
构造函数
作用:初始化对象
初始化与赋值
stock stock2 = Stock ("Boffo objects", 2, 2.0); stock1 = stock ("Nifty Foods", 10, 50.0); // temporary obiect 第一条语句是初始化,它创建有指定值的对象,可能会创建临时对象(也可能不会); 第二条语句是赋值。像这样在赋值语句中使用构造函数总会导致在赋值前创建一个临时对象。 提示:如果既可以通过初始化,也可以通过赋值来设置对象的值,则应采用初始化方式。通常这种方式的效率更高。
成员初始化列表
成员初始化列表的语法 如果Classy是一个类,而meml, mem2和mem3都是这个类的数据成员,则类构造函数可以使用如下的语法来初始化数据成员: Classy::Classy(int n, int m) :meml (n), mem2 (0) , mem3 (n*m+ 2) { //... } 上述代码将meml初始化为n,将mem2初始化为0,将mem3初始化为n*m+2,从概念上说,这些初始化工作是在对象创建时完成的,此时还未执行括号中的任何代码。请注意以下几点: 这种格式只能用于构造函数; 必须用这种格式来初始化非静态const数据成员(至少在C++11之前是这样的); 必须用这种格式来初始化引用数据成员。 数据成员被初始化的顺序与它们出现在类声明中的顺序相同,与初始化器中的排列顺序无关。 警告:不能将成员初始化列表语法用于构造函数之外的其他类方法。
类内初始化
C++11的类内初始化 C++11允许您以更直观的方式进行初始化: class Classy { int men1 = 10; //in-class initializatior const int mem2 = 20; // in-class initializatiorint } 这与在构造函数中使用成员初始化列表等价: Classy::Classy() : meml(10) , mem2 (20) {...} 成员meml和mem2将分别被初始化为10和20,除非调用了使用成员初始化列表的构造函数,在这种情况下,实际列表将覆盖这些默认初始值: Classy::Classy (int n) : meml(n) {..} 在这里,构造函数将使用n来初始化meml,但mem2仍被设置为20
等价
声明和定义构造函数
声明构造函数
默认参数
定义构造函数
默认构造函数
总结默认构造函数:默认构造函数是没有参数或所有参数都有默认值的构造函数。拥有默认构造函数后,可以声明对象,而不初始化它,即使已经定义了初始化构造函数。它还使得能够声明数组。 当且仅当没有定义任何构造函数时,编译器才会提供默认构造函数。为类定义了构造函数后,程序员就必须为它提供默认构造函数。如果提供了非默认构造函数(如Stock(const char * co, int n, doublepr)),但没有提供默认构造函数,则下面的声明将出错: Stock stockl; // not possible with current constructor 这样做的原因可能是想禁止创建未初始化的对象。然而,如果要创建对象,而不显式地初始化,则必须定义一个不接受任何参数的默认构造函数。 没有参数或所有参数都有默认值的构造函数,称为默认构造函数。由于只能有一个默认构造函数,因此不要同时采用这两种方式。
默认构造函数初始化
当程序创建未被显式初始化的类对象时,总是调用默认构造函数,默认构造函数初始化如下: Stock first; // 隐式调用默认构造函初始化 Stock first = Stock(); // 显式调用默认构造函初始化 Stock *p = new Stock // 隐式调用默认构造函初始化 区分: Stock first("Apple"); // 调用非默认构造函数,即接受参数的构造函数 Stock second(); // 调用返回Stock对象的second( )函数 注意:隐式调用默认构造函初始化时,不要使用圆括号。
调用方法:显式调用、隐式调用
析构函数
作用:完成清理工作
通常由编译器决定调用析构函数的时机
默认析构函数
由于在类对象过期时析构函数将自动被调用,因此必须有一个析构函数。如果程序员没有提供析构函数,编译器将隐式地声明一个默认析构函数,并在发现导致对象被删除的代码后,提供默认析构函数。
构造函数和析构函数
都没有返回类型(连void都没有)
this指针
每个成员函数(包括构造函数和析构函数)都有一个this指针。this指针指向调用对象。如果方法需要引用整个调用对象,则可以使用表达式*this。因为this是对象的地址,而是对象本身,即*this (将解除引用运算符*用于指针,将得到指针指向的值)。
类作用域
作用域为类的常量
在类中声明一个枚举(整数类型)
第一种方式是在类中声明一个枚举。 在类声明中声明的枚举的作用域为整个类,因此可以用枚举为整型常量提供作用域为整个类的符号名称。也就是说,可以这样开始Bakery声明: class Bakery { private: enumn {Months =12); double costs [Months]; } 注意,用这种方式声明枚举并不会创建类数据成员。也就是说,所有对象中都不包含枚举。另外, Months只是一个符号名称,在作用域为整个类的代码中遇到它时,编译器将用12来替换它。由于这里使用枚举只是为了创建符号常量,并不打算创建枚举类型的变量,因此不需要提供类名。顺便说一句,在很多实现中, ios_base类在其公有部分中完成了类似的工作,诸如ios_base:fixed等标识符就来自这里。其中, fixed是ios_base类中定义的典型的枚举量。
状态成员
使用关键字static
C++提供了另一种在类中定义常量的方式-使用关键字static: class Bakery{ private: static const int Months = 12; double costs [Months]; } 这将创建一个名为Months的常量,该常量将与其他静态变量存储在一起,而不是存储在对象中。因此,只有一个Months常量,被所有Bakery对象共享。 在C++98中,只能使用这种技术声明值为整数或枚举的静态常量,而不能存储double常量。C++11消除了这种限制。
作用域内枚举
传统的枚举存在一些问题,其中之一是两个枚举定义中的枚举量可能发生冲突。 假设有一个处理鸡蛋和T恤的项目,其中可能包含类似下面这样的代码: enum egg {Small, Medium, Large, Jumbo}; enum t_shirt {Small, Medium, Large, xlargel; 这将无法通过编译,因为egg Small和t_shirt Small位于相同的作用域内,它们将发生冲突。为避免这种问题, C++11提供了一种新枚举,其枚举量的作用域为类。这种枚举的声明类似于下面这样: enum class ega {Small, Medium, Large, Jumbol; enum class t_shirt {Small, Medium, Large, xlarge}; 也可使用关键字struct代替class。无论使用哪种方式,都需要使用枚举名来限定枚举量: egg choice =egg:: Large; // the Large enumerator of the egg enum t_shirt Floyd =t_shirt::Large; // Largehe Large enumerator of the t_shirt enum C++11还提高了作用域内枚举的类型安全。在有些情况下,常规枚举将自动转换为整型,如将其赋给int变量或用于比较表达式时,但作用域内枚举不能隐式地转换为整型。
对象数组
对象数组初始化与默认构造函数
当程序创建未被显式初始化的类对象时,总是调用默认构造函数。 初始化对象数组的方案是,首先使用默认构造函数创建数组元素,然后花括号中的构造函数将创建临时对象,然后将临时对象的内容复制到相应的元素中。因此,要创建(未被显式初始化)类对象数组,则这个类必须有默认构造函数。
抽象数据类型(ADT)
抽象数据类型(Abstract Data Type,ADT)是计算机科学中具有类似行为的特定类别的数据结构的数学模型;或者具有类似语义的一种或多种程序设计语言的数据类型。抽象数据类型是描述数据结构的一种理论工具,其目的是使人们能够独立于程序的实现细节来理解数据结构的特性。抽象数据类型的定义取决于它的一组逻辑特性,而与计算机内部如何表示无关。
类适合用于描述ADT
类很适合用于描述ADT.公有成员函数接口提供了ADT描述的服务,类的私有部分和类方法的代码提供了实现,这些实现对类的客户隐藏。
使用类
运算符重载
简单友元
友元函数
创建友元函数分为两步,第一步将其原型放在类声明中,并在原型声明前加上关键字friend: friend 类型名 友元函数名(形参表); 第二步是在类体外对友元函数进行定义。定义的格式和普通函数相同(因为它不是成员函数,所以不同使用类作用域限定符,另外不要使用关键字friend),但可以通过对象作为参数直接访问对象的私有成员。 说明如下: 1)必须在类的说明中说明友元函数,说明时以关键字friend开头,后跟友元函数的函数原型,友元函数的说明可以出现在类的任何地方,包括在private和public部分; 2)注意友元函数不是类的成员函数,所以友元函数的实现和普通函数一样,在实现时不用"::"指示属于哪个类,只有成员函数才使用"::"作用域符号; 3)友元函数不能直接访问类的成员,只能访问对象成员, 4)友元函数可以访问对象的私有成员,但普通函数不行; 5)调用友元函数时,在实际参数中需要指出要访问的对象, 6)类与类之间的友元关系不能继承。 7)一个类的成员函数也可以作为另一个类的友元,但必须先定义这个类。 提示:如果要为类重截运算符,并将非类的项作为其第一个操作数,则可以用友元函数来反转操作数的顺序。因为类重载二元运算符(带有两个参数的运算符)时,左侧的操作数是调用对象,将非类的项作为其第一个操作数(并与该类进行该运算符对应的运算时),不能调用该重载二元运算符对应的操作符重载函数,所以需要友元函数。
常用的有元
重载<<运算符
一般来说,要重载<<运算符来显示c_name的对象,可使用一个友元函数,其定义如下: ostream & operator << (ostream & os, const c_name & obj) { os<s...; // display object contents return os; } 警告:只有在类声明中的原型中才能使用friend关键字。除非函数定义也是原型,否则不能在函数定义中使用该关键字。
派生类通过强制转换为基类类型来使用基类的友元
类的自动转换和强制类型转换
类和动态内存分配
动态内存和类
类的静态成员
静态成员变量
静态成员变量:类的成员变量被声明为static(称为静态成员变量),意味着它为该类的所有实例所共享,也就是说当某个类的实例修改了该静态成员变量,其修改值为该类的其它所有实例所见。 简记:静态成员变量在类中声明。类外定义,声明时使用关键字static,定义时不加关键字static但是要加上类作用域(类名::)。 class A { public: static int a; //声明 }; int A::a = 3; //定义了静态成员变量,同时初始化 请注意: 不能在类声明中初始化静态成员变量,这是因为声明描述了如何分配内存,但并不分配内存。您可以使用这种格式来创建对象,从而分配和初始化内存。 对于静态类成员,可以在类声明之外使用单独的语句来进行初始化,这是因为静态类成员是单独存储的,而不是对象的组成部分。 初始化语句指出了类型,并使用了作用域运算符,但没有使用关键字static. 初始化是在方法文件中,而不是在类声明文件中进行的,这是因为类声明位于头文件中,程序可能将头文件包括在其他几个文件中。如果在头文件中进行初始化,将出现多个初始化语句副本,从而引发错误。 对于不能在类声明中初始化静态数据成员的一种例外情况是,静态数据成员为整型或枚举型const。 总结:静态数据成员在类声明中声明,在包含类方法的文件中初始化。初始化时使用作用城运算符来指出静态成员所属的类。但如果静态成员是整型或枚举型const,则可以在类声明中初始化。
关键字static
整型const、枚举型const静态成员可以在类声明中初始化
静态类成员函数
关键字static
特殊成员函数
C++ 自动提供这些特殊的成员函数: 默认构造函数,如果没有定义构造函数; 默认析构函数,如果没有定义; 复制构造函数,如果没有定义: 赋值运算符,如果没有定义; 地址运算符,如果没有定义。 更准确地说,编译器将生成上述最后三个函数的定义。如果程序使用对象的方式要求这样做。例如,如果您将一个对象赋给另一个对象,编译器将提供赋值运算符的定义。 C++11提供了另外两个特殊成员函数:移动构造函数(move constructor)和移动赋值运算符(moveassignment operator)。
分类
默认构造函数
默认的默认构造函数
默认的默认构造函数 在没有提供任何参数的情况下,将调用默认构造函数。如果您没有给类定义任何构造函数,编译器将提供一个默认构造函数。这种版本的默认构造函数被称为默认的默认构造函数。对于使用内置类型的成员,默认的默认构造函数不对其进行初始化;对于属于类对象的成员,则调用其默认构造函数。
复制构造函数
复制构造函数原型: Class_name(const Class_name &) 它接受一个指向常量类对象的引用作为参数。 何时调用复制构造函数 新建一个对象并将其初始化为同类现有对象时,复制构造函数都将被调用。这在很多情况下都可能发生,最常见的情况是将新对象显式地初始化为现有的对象。例如,假设motto是一个StringBad对象,则下面4种声明都将调用复制构造函数: stringBad ditto (motto); // calls stringBad (const stringBad &) stringBad metoo = motto; // calls stringBad (const stringBad &) stringBad also = StringBad (motto);// calls stringBad(const stringBad &) stringBad * pstringBad = new stringBad (motto) ;// calls stringBad (const stringBad &) 其中中间的2种声明可能会使用复制构造函数直接创建metoo和also,也可能使用复制构造函数生成个临时对象,然后将临时对象的内容赋给metoo和also,这取决于具体的实现。最后一种声明使用motto初始化一个匿名对象,并将新对象的地址赋给pstringBad指针。每当程序生成了对象副本时,编译器都将使用复制构造函数。具体地说,当函数按值传递对象或函数返回对象时,都将使用复制构造函数。记住,按值传递意味着创建原始变量的一个副本。编译器生成临时对象时,也将使用复制构造函数。例如,将3个Vector对象相加时,编译器可能生成临时的Vector对象来保存中间结果。何时生成临时对象随编译器而异,但无论是哪种编译器,当按值传递和返回对象时,都将调用复制构造函数。 总结起来调用复制构造函数的时机: 将新对象初始化为一个同类对象时 按值将对象传递给函数时 函数按值返回对象时 编译器生成临时对象时
默认的复制构造函数
默认的复制构造函数 默认的复制构造函数的功能默认的复制构造函数逐个复制非静态成员(成员复制也称为浅复制),复制的是成员的值。如果成员本身就是类对象,则将使用这个类的复制构造函数来复制成员对象。
赋值运算符
赋值运算符原型: Class_name & Class_name::operator=(const Class_name &); 它接受并返回一个指向类引用的对象。 赋值运算符的功能以及何时使用它 将已有的对象赋给另一个对象时,将使用重载的赋值运算符: stringBad headlinel("Celery stalks at Midnight"); ... stringBad knot; knot =headlinel; // assignment operator invoked 初始化对象时,并不一定会使用赋值运算符: StringBad metoo =knot; // use copy constructor, possibly assignment, too 这里, metoo是一个新创建的对象,被初始化为knot的值,因此使用复制构造函数。然而,正如前面指出的,实现时也可能分两步来处理这条语句:使用复制构造函数创建一个临时对象,然后通过赋值将临时对象的值复制到新对象中。这就是说,初始化总是会调用复制构造函数,而使用=运算符时也可能调用赋值运算符。与复制构造函数相似,赋值运算符的隐式实现也对成员进行逐个复制。如果成员本身就是类对象,则程序将使用为这个类定义的赋值运算符来复制该成员,但静态数据成员不受影响。
默认的赋值运算符
重载赋值运算符
注意
“浅复制”与“深复制”
警告:如果类中包含了使用new初始化的指针成员,应当定义一个复制构造函数,以复制指向的数据,而不是指针,这被称为深度复制。复制的另一种形式(成员复制或浅复制)只是复制指针值。浅复制仅浅浅地复制指针信息,而不会深入“挖掘”以复制指针引用的结构。
复制与赋值的异同
复制构造函数与赋值运算符的本质区别:复制构造函数创造新的对象,赋值运算符只是给已经存在的对象赋予值。 赋值运算符的特征: 由于目标对象可能引用了以前分配的数据,所以函数应使用delete[ ]来释放这些数据。 函数应当避免将对象赋给自身;否则,给对象重新赋值前,释放内存操作可能删除对象的内容。 函数返回指向调用对象的引用。
连续赋值问题
通过返回一个对象,函数可以像常规赋值操作那样,连续进行赋值,即如果S0、SI和S2都是StringBad对象,则可以编写这样的代码: S0= S1 = S2; 使用函数表示法时,上述代码为: S0.operator= (S1.operator=(S2)); 因此, S1.operator= (S2)的返回值是函数S0.operator=( )的参数。 因为返回值是一个指向StringBad对象的引用,因此参数类型是正确的。
析构函数
默认的析构函数
移动构造函数
默认的移动构造函数
移动赋值运算符
默认的移动赋值运算符
注意
使用时机
默认的移动构造函数和移动赋值运算符的工作方式与复制版本类似:执行逐成员初始化并复制内置类型。如果成员是类对象,将使用相应类的构造函数和赋值运算符。如果定义了移动构造函数和移动赋值运算符,这将调用它们;否则将调用复制构造函数和复制赋值运算符。
注意事项
编译器自动提供的函数
自动定义
存在隐患
显式自定义
解决隐患
启用和禁用成员函数
启用默认的方法
关键字default
禁用方法
关键字delete
伪私有方法
改进后的新String类
在构造函数中使用new时的注意事项
使用new时的推荐做法
new的三个“统一”
使用new初始化对象的指针成员时必须特别小心。具体地说,应当这样做。: 如果在构造函数中使用new来初始化指针成员,则应在析构函数中使用delete. new和deiete必须相互兼容。new对应于delete, new[]对应于delete[]. 如果有多个构造函数,则必须以相同的方式使用new,要么都带中括号,要么都不带。因为只有一个析构函数,所有的构造函数都必须与它兼容。 然而,可以在一个构造函数中使用new初始化指针,而在另一个构造函数中将指针初始化为空(0或C++11中的nullptr),这是因为delete (无论是带中括号还是不带中括号)可以用于空指针。
空指针:NULL、0、nullptr
NULL、 0还是nullptr:以前,空指针可以用0或NULL (在很多头文件中, NULL是一个被定义为0的符号常量)来表示。C程序员通常使用NULL而不是0,以指出这是一个指针,就像使用'\0'而不是0来表示空字符,以指出这是一个字符一样。然而, C++传统上更喜欢用简单的0,而不是等价的NULL。但正如前面指出的, C++11提供了关键字nullptr,这是一种更好的选择。
应当定义一个复制构造函数
应定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象。通常,这种构造函数与下面类似。 string::String (const string & st) { num strings++; // handle static member update if necessary len = st.len; //same length as copied string str = new char [len + 1]; // allot space std::strcpy(str, st.str); // copy string to new location 具体地说,复制构造函数应分配足够的空间来存储复制的数据,并复制数据,而不仅仅是数据的地址。另外,还应该更新所有受影响的静态类成员。
应当定义一个赋值运算符
应当定义一个赋值运算符,通过深度复制将一个对象复制给另一个对象。通常,该类方法与下面类似: string & string::operator=(const string & st) { if (this == &st) // object assigned to itself return *this; // all done delete [ ] str; // free old string len =st.len; strs new char [len+1]; // get space for new string std::strcpy (str, st.str); // copy the string return *this; // return reference to invoking object 具体地说,该方法应完成这些操作:检查自我赋值的情况,释放成员指针以前指向的内存,复制数据而不仅仅是数据的地址,并返回一个指向调用对象的引用。
包含类成员的类的逐成员复制
有关返回对象的说明
返回指向const对象的引用
返回指向非const对象的引用
返回对象
返回const对象
规律
返回对象根据是否const修饰,引用还是对象有四种类型,每种类型都有自己适用的地方。 总规律是: 如果方法或函数要返回局部对象,则应返回对象,而不是指向对象的引用。在这种情况下,将使用复制构造函数来生成返回的对象。如果方法或函数要返回一个没有公有复制构造函数的类(如ostream类)的对象,它必须返回一个指向这种对象的引用。最后,有些方法和函数(如重载的赋值运算符)可以返回对象,也可以返回指向对象的引用,在这种情况下,应首选引用,因为其效率更高。
使用指向对象的指针
析构函数调用时机
析构函数调用时机 如果对象是自动变量,则当执行完定义该对象的程序块时,将调用该对象的析构函数。 如果对象是静态变量,则在程序结束时调用对象的析构函数。 如果对象是动态变量,只能显式地删除对象时,才会调用析构函数。C++中需要使用new创建delate释放,c中是需要用malloc函数申请,free释放。
自动变量
静态变量
动态变量
指针和对象小结
指针和对象
// 指针和对象小结 String str; String* glamour; // 声明指向类对象的指针 String* first = &str; // 将指针初始化为已有的对象 String* gleep = new String; // 使用new和默认构造函数对指针进行初始化 String* glop = new String("My World"); // 使用new和String(const char*)类构造函数对指针进行初始化 String* favorite = new String(str); // 使用new和String(const String&)类构造函数对指针进行初始化 favorite->length(); // 使用->操作符通过指针方法类方法 *favorite; // 使用*解除引用操作符从指针获得对象
使用new创建对象具体步骤
//使用new创建对象步骤 String* pveg = new String("Cabbage Heads Home"); /* 1. 为对象分配空间,假设空间地址是2400。 2400: [str:__________ len:___________] 2. 调用类的构造函数。 a. 为"Cabbage Heads Home"分配空间,假设空间地址是2000 b. 将"Cabbage Heads Home"复制到分配的空间。 c. 将该空间的地址2000赋值给str,将字符串长度赋值给len。 2400: [str:___2000____ len:____19_____] d. 更新 num_strings 3. 创建pveg变量,假设空间地址是2800。 2800: [ ] 4. 将对象空间地址2400赋值给pveg变量。 2800: [2400] 设String的定义如下: class String { private: char* str; int len; static int num_string; public: String(const char* s); } String::String(const char* s) { len = strlen(s); str = new char[len + 1]; strcpy(str, s); num_string++; } */
模拟队列
嵌套结构和类
嵌套结构和类 在类声明中声明的结构、类或枚举被称为是被嵌套在类中,其作用域为整个类。这种声明不会创建数据对象,而只是指定了可以在类中使用的类型。如果声明是在类的私有部分进行的,则只能在这个类使用波声明的类型;如果声明是在公有部分进行的,则可以从类的外部通过作用域解析运算符使用被声明的类型。例如,如果Node是在Queue类的公有部分声明的,则可以在类的外面声明Queue::Node类型的变量。
类继承
一个简单的基类
派生一个类
继承的内容
派生类从基类那里继承了什么? 基类的公有成员成为派生类的公有成员(数据和方法)。基类的保护成员成为派生类的保护成员。基类的私有成员被继承,但不能直接访问。
不能继承的内容
派生类不能从基类那里继承了什么? 不能继承构造函数、析构函数、赋值运算符和友元。
需要增添的内容
构造函数
有关派生类构造函数的要点如下: 首先创建基类对象; 派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数; 派生类构造函数应初始化派生类新增的数据成员。 如果没有提供显式构造函数,因此将使用隐式构造函数。释放对象的顺序与创建对象的顺序相反即首先执行派生类的析构函数,然后自动调用基类的析构函数。 注意:创建派生类对象时,程序首先调用基类构造函数,然后再调用派生类构造函数。基类构造函数负责初始化继承的数据成员;派生类构造函数主要用于初始化新增的数据成员。派生类的构造函数总是调用一个基类构造函数。可以使用成员初始化列表语法指明要使用的基类构造函数,否则将使用默认的基类构造函数。派生类对象过期时,程序将首先调用派生类析构函数,然后再调用基类析构函数。
calss 派生类名: 访问控制符 基类名
calss 派生类名: 访问控制符 基类名 { ... }; 上述代码完成的工作: 派生类存储了基类的数据成员(派生类继承了基类的实现) 派生类可以使用基类的方法(派生类继承了基类的接口)
派生类构造函数
有关派生类构造函数的要点如下: 首先创建基类对象; 派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数; 派生类构造函数应初始化派生类新增的数据成员。
访问权限的考虑
派生类不能直接访问基类的私有成员,而必须通过基类方法进行访问。具体地说,派生类构造函数必须使用基类构造函数。创建派生类对象时,程序首先创建基类对象。从概念上说,这意味着基类对象应当在程序进入派生类勾造函数之前被创建。
派生类不能直接访问基类的私有成员
是否使用初始化列表
成员初始化列表 派生类构造函数可以使用初始化器列表机制将值传递给基类构造函数。请看下面的例子: derived: :derived (typel x, typez y) : base(x,y) // initializer list { ... } 其中derived是派生类, base是基类, x和y是基类构造函数使用的变量。例如,如果派生类构造函数接收到参数10和12,则这种机制将把10和12传递给被定义为接受这些类型的参数的基类构造函数。除虚基类外,类只能将值传递回相邻的基类,但后者可以使用相同的机制将信息传递给相邻的基类,依此类推。如果没有在成员初始化列表中提供基类构造函数,程序将使用默认的基类构造函数。成员初始化列表只能用于构造函数。
使用基类默认构造函数
显式调用基类构造函数
派生类和基类之间的特殊关系
派生类和基类之间的特殊关系 派生类->基类 派生类不能直接访问基类的私有成员,而必须通过基类方法进行访问。 派生类对象可以使用基类的方法,条件是方法不是私有的。 可以将派生类对象赋值给基类对象。此时将使用基类隐式重载赋值运算符,将派生类中基类的部分赋值给基类。 可以用派生类对象来初始化基类对象。此时将使用基类隐式复制构造函数,用将派生类中基类的部分来初始化基类。 基类->派生类 基类指针可以在不进行显式类型转换的情况下指向派生类对象。注意:基类指针只能用于调用基类方法。 基类引用可以在不进行显示类型转换的情况下引用派生类对象。注意:基类引用只能用于调用基类方法。 说明上述的第3和4点,其实是b的推论,因为基类重载赋值运算符和复制构造函数的参数都是基类的引用,根据b,显然可以将派生类作为参数。
继承:is-a关系
继承方式分类
公有继承
能建立
is-a
在C++中只使用公有继承建立is-a关系。
has-a
is-implement-as-a
uses-a
导致问题
不能建立
is-like-a
保护继承
私有继承
类之间关系分类
is-a
公有继承是最常用的方式,它建立一种is-a关系,即派生类对象也是一个基类对象,可以对基类对象执行的任何操作,也可以对派生类对象执行。例如,假设有一个Fruit类,可以保存水果的重量和热量。因为香蕉是一种特殊的水果,所以可以从Fruit类派生出Banana类。新类将继承原始类的所有数据成员,因此, Banana对象将包含表示香蕉重量和热量的成员。新的Banana类还添加了专门用于香蕉的成员,这些成员通常不用于水果,例如Banana Institute Peel Index (香蕉机构果皮索引),因为派生类可以添加特性,所以,将这种关系称为is-a-kind-of (是一种)关系可能更准确,但是通常使用术语is-a。
使用公有继承来处理
is-a关系的进一步抽象
has-a
公有继承不建立has-a关系。例如,午餐可能包括水果,但通常午餐并不是水果。所以,不能通过从Fruit类派生出Lunch类来在午餐中添加水果。在午餐中加入水果的正确方法是将其作为一种has-a关系:午餐有水果。
使用包含、私有继承和保护继承来处理
C++的一个主要目标是促进代码重用。公有继承是实现这种目标的机制之一,但并不是唯一的机制。其他方法之一是使用这样的类成员:本身是另一个类的对象。这种方法称为包含(containment)、组合(composition)或层次化(layering)。另一种方法是使用私有或保护继承。通常,包含、私有继承和保护继承用于实现has-a关系,即新的类将包含另一个类的对象。例如, HomeTheater类可能包含一个BluRayPlayer对象。多重继承使得能够使用两个或更多的基类派生出新的类,将基类的功能组合在一起。
is-like-a
公有继承不能建立is-like-a关系,也就是说,它不采用明喻。人们通常说律师就像鲨鱼,但律师并不是鲨鱼。例如,鲨鱼可以在水下生活。所以,不应从Shark类派生出Lawyer类。继承可以在基类的基础上添加属性,但不能删除基类的属性。在有些情况下,可以设计一个包含共有特征的类,然后以is-a或has-a类的基础上定义相关的类。
设计共有特征的类来处理
is-implement-as-a
公有继承不建立is-implemented-as-a (作为....来实现)关系。例如,可以使用数组来实现栈,但从Array类派生出Stack类是不合适的,因为栈不是数组。例如,数组索引不是栈的属性。另外,可以以其他方式实现栈,如链表。正确的方法是,通过让栈包含一个私有Array对象成员来隐藏数组实现。
使用隐藏数据成员来处理
uses-a
公有继承不建立uses-a关系。例如,计算机可以使用激光打印机,但从Computer类派生出Printer类(或反过来)是没有意义的。然而,可以使用友元函数或类来处理Printer对象和Computer对象之间的通信。
使用友元函数或类来处理
多态公有继承
实现多态公有继承机制
在派生类中重新定义基类的方法
使用虚方法
关键字virtual
多态中确定调用哪个类的方法
通过限定名
类名::方法名
类名决定
通过引用或指针
不使用关键字virtual
引用类型或指针类型决定
使用关键字virtual
引用或指针指向的对象类型决定
静态联编和动态联编
函数名联编
将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编。
分类
静态联编(早期联编)
动态联编(晚期联编)
指针和引用类型的兼容性
向上强制转换
将派生类引用或指针转换为基类引用或指针被称为向上强制转换(upcasting),这使公有继承不需要进行显式类型转换。
传递性
允许隐式向上类型转换
本质
is-a关系
向下强制转换
将基类指针或引用转换为派生类指针或引用称为向下强制转换。
只允许显式向下强制类型转换
运算符static_cast
虚成员函数和动态联编
为什么有两种类型联编以及默认为静态联编
效率
概念模型
虚函数
虚函数工作原理
虚函数表
虚函数的作用
有关虚函数注意事项
有关虚函数注意事项 在基类方法的声明中使用关键字virtual可使该方法在基类以及所有的派生类(包括从派生类派生出来的类)中是虚的。 如果使用指向对象的引用或指针来调用虚方法,程序将使用为对象类型定义的方法,而不使用为引用或指针类型定义的方法。这称为动态联编或晚期联编。这种行为非常重要,因为这样基类指针或引用可以指向派生类对象。 如果定义的类将被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚的。
构造函数
析构函数
友元
没有重新定义继承的方法
重新定义继承的方法
注意与函数重载区别
重新定义不会生成函数的两个重载版本,而是隐藏基类的版本。总之,重新定义继承的方法并不是重载。如果在派生类中重新定义函数,将不是使用相同的函数特征标覆盖基类声明,而是隐藏同名的基类方法,不管参数特征标如何。
经验规则
重新定义继承的方法的经验规则 如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针。这种特性被称为返回类型协变(covariance of return type),因为允许返回类型随类类型的变化而变化。 如果基类声明被重载了,则应在派生类中重新定义所有的基类版本。
访问控制:protected
对外部世界:保护成员的行为与私有成员相似
对派生类:保护成员的行为与公有成员相似
抽象基类
抽象基类(abstract base class, ABC)
抽象基类描述的是至少使用一个纯虚函数的接口,从抽象基类派生出的类将根据派生类的具体特征,使用常规虚函数来实现这种接口。
纯虚函数
=0
具体类
应用ABC概念
ABC理念
在处理继承的问题上, ABC方法具有系统性和规范性。设计ABC之前,首先应开发一个模型一指出编程问题所需的类以及它们之间相互关系。一种学院派思想认为,如果要设计类继承层次,则只能将那些不会被用作基类的类设计为具体的类。这种方法的设计更清晰,复杂程度更低。 可以将ABC看作是一种必须实施的接口。ABC要求具体派生类覆盖其纯虚函数-迫使派生类遵循ABC设置的接口规则。这种模型在基于组件的编程模式中很常见,在这种情况下,使用ABC使得组件设计人员能够制定“接口约定”,这样确保了从ABC派生的所有组件都至少支持ABC指定的功能。
继承和动态内存分配
分类
继承是怎样与动态内存分配(使用new和delete)进行互动的呢? 例如,如果基类使用动态内存分配,并重新定义赋值和复制构造函数,这将怎样影响派生类的实现呢?这个问题的答案取决于派生类的属性。
派生类不使用new
派生类使用new
必须自定义
当基类和派生类都采用动态内存分配时,派生类的析构函数、复制构造函数、赋值运算符都必须使用相应的基类方法来处理基类元素。这种要求是通过三种不同的方式来满足的。对于析构函数,这是自动完成的;对于构造函数,这是通过在初始化成员列表中调用基类的复制构造函数来完成的;如果不这样做,将自动调用基类的默认构造函数。对于赋值运算符,这是通过使用作用域解析运算符显式地调用基类的赋值运算符来完成的。
复制构造函数
赋值运算符
析构函数
使用动态内存分配和友元的继承示例
类设计回顾
编译器生成的公有成员函数
默认构造函数
默认构造函数 默认构造函数要么没有参数,要么所有的参数都有默认值。 如果没有定义任何构造函数,编译器将定义默认构造函数,让您能够创建对象。 自动生成的默认构造函数的另一项功能是,调用基类的默认构造函数以及调用本身是对象的成员所属类的默认构造函数。 另外,如果派生类构造函数的成员初始化列表中没有显式调用基类构造函数,则编译器将使用基类的默认构造函数来构造派生类对象的基类部分。在这种情况下,如果基类没有构造函数,将导致编译阶段错误。 如果定义了某种构造函数,编译器将不会定义默认构造函数。在这种情况下,如果需要默认构造函数,则必须自己提供。 提供构造函数的动机之一是确保对象总能被正确地初始化。另外,如果类包含指针成员,则必须初始化这些成员。因此,最好提供一个显式默认构造函数,将所有的类数据成员都初始化为合理的值。
复制构造函数
复制构造函数 复制构造函数接受其所属类的对象作为参数。例如, Star类的复制构造函数的原型如下:star (const star &); 在下述情况下,将使用复制构造函数: 将新对象初始化为一个同类对象; 按值将对象传递给函数; 函数按值返回对象; 编译器生成临时对象。 注意事项: 如果程序没有使用(显式或隐式)复制构造函数,编译器将提供原型,但不提供函数定义;否则,程序将定义一个执行成员初始化的复制构造函数。也就是说,新对象的每个成员都被初始化为原始对象相应成员的值。如果成员为类对象,则初始化该成员时,将使用相应类的复制构造函数。 在某些情况下,成员初始化是不合适的。例如,使用new初始化的成员指针通常要求执行深复制(参见“使用动态内存分配和友元的继承示例”),或者类可能包含需要修改的静态变量。在上述情况下,需要定义自己的复制构造函数。
赋值运算符
赋值运算符 默认的赋值运算符用于处理同类对象之间的赋值。不要将赋值与初始化混淆了。如果语句创建新的对象,则使用初始化;如果语句修改已有对象的值,则是赋值。 star sirius; star alpha = sirius;// initialization (one notation) star dogstar; dogstar = sirius// assignment 默认赋值为成员赋值。如果成员为类对象,则默认成员赋值将使用相应类的赋值运算符。如果需要显式定义复制构造函数,则基于相同的原因,也需要显式定义赋值运算符。Star类的赋值运算符的原型如下: tar & star::operator=(const star &); 赋值运算符函数返回一个Star对象引用。 编译器不会生成将一种类型赋给另一种类型的赋值运算符。如果希望能够将字符串赋给Star对象,则方法之一是显式定义下面的运算符 star & star::operator=(const char *) { ..} 另一种方法是使用转换函数将字符串转换成Star对象,然后使用将Star赋给Star的赋值函数。第一种方法的运行速度较快,但需要的代码较多,而使用转换函数可能导致编译器出现混乱。
其它类方法
构造函数
构造函数构 造函数不同于其他类方法,因为它创建新的对象,而其他类方法只是被现有的对象调用。这是构造函数不被继承的原因之一。继承意味着派生类对象可以使用基类的方法,然而,构造函数在完成其工作之前,对象并不存在。
析构函数
析构函数 一定要定义显式析构函数来释放类构造函数使用new分配的所有内存,并完成类对象所需的任何特殊的清理工作。对于基类,即使它不需要析构函数,也应提供一个虚析构函数。
类型转换
类型转换 使用一个参数就可以调用的构造函数定义了从参数类型到类类型的转换。例如,下述Star类的构造函数原型: Star (const char *); //converts char * to star Star (const Spectral &, int members =1);// converts Spectral to star 将可转换的类型传递给以类为参数的函数时,将调用转换构造函数。例如,在如下代码中: star north; north = "polaris"; 第二条语句将调用Star:operator = (const Star *)函数,使用Star:star (const char*)生成一个Star对象,该对象将被用作上述赋值运算符函数的参数。这里假设没有定义将char*赋给Star的赋值运算符。 在带一个参数的构造函数原型中使用explicit将禁止进行隐式转换,但仍允许显式转换: 要将类对象转换为其他类型,应定义转换函数,转换函数可以是没有参数的类成员函数,也可以是返回类型被声明为目标类型的类成员函数。即使没有声明返回类型,函数也应返回所需的转换值。下面是一些示例 star::Star double() {...}// converts star to double star::Star const char* (){...} // converts to const char 应理智地使用这样的函数,仅当它们有帮助时才使用。另外,对于某些类,包含转换函数将增加代码的二义性。例如,假设已经为Vector类型定义了double转换,并编写了下面的代码 Vector ius (6.0, 0.0); Vector lux =ius + 20.2// ambiguous 编译器可以将ius转换成double并使用double加法,或将20.2转换成veotor (使用构造函数之一)并使用vector加法。但除了指出二义性外,它什么也不做。 C++11支持将关键字explicit用于转换函数。与构造函数一样, explicit允许使用强制类型转换进行显式转换,但不允许隐式转换。
按值传递对象与传递引用
按值传递对象与传递引用 通常,编写使用对象作为参数的函数时,应按引用而不是按值来传递对象。这样做的原因之一是为了提高效率。按值传递对象涉及到生成临时拷贝,即调用复制构造函数,然后调用析构函数。调用这些函数需要时间,复制大型对象比传递引用花费的时间要多得多。如果函数不修改对象,应将参数声明为const引用。 按引用传递对象的另外一个原因是,在继承使用虚函数时,被定义为接受基类引用参数的函数可以接受派生类。
返回对象和返回引用
返回对象和返回引用 有些类方法返回对象。您可能注意到了,有些成员函数直接返回对象,而另一些则返回引用。有时方法必须返回对象,但如果可以不返回对象,则应返回引用。来具体看一下。首先,在编码方面,直接返回对象与返回引用之间唯一的区别在于函数原型和函数头: Star noval (const star &);// returns a star object star & nova2 (const star &);// returns a reference to a star 其次,应返回引用而不是返回对象的的原因在于,返回对象涉及生成返回对象的临时副本,这是调用函数的程序可以使用的副本。因此,返回对象的时间成本包括调用复制构造函数来生成副本所需的时间和调用析构函数删除副本所需的时间。返回引用可节省时间和内存。直接返回对象与按值传递对象相似:它们都生成临时副本。同样,返回引用与按引用传递对象相似:调用和被调用的函数对同一个对象进行操作。 然而,并不总是可以返回引用。函数不能返回在函数中创建的临时对象的引用,因为当函数结束时,临时对象将消失,因此这种引用将是非法的。在这种情况下,应返回对象,以生成一个调用程序可以使用的副本。 通用的规则是,如果函数返回在函数中创建的临时对象,则不要使用引用。例如,下面的方法使用构·造函数来创建一个新对象,然后返回该对象的副本:V ector Vector: :operator+ (const vector &b) const { return Vector (x + b.x, y +b.y); } 如果函数返回的是通过引用或指针传递给它的对象,则应按引用返回对象。例如,下面的代码按引用返回调用函数的对象或作为参数传递给函数的对象: const stock & stock: :topval (const stock & s) const { if (s.total val >total val) return s; // argument object else return *this // invoking object
使用const
使用const 使用const时应特别注意。可以用它来确保方法不修改参数: Star::Star (const chars)(.. // won't change the string to which s points 可以使用const来确保方法不修改调用它的对象:void star: :show() const {...} // won't change invoking object 这里const表示const Star * this,而this指向调用的对象。 通常,可以将返回引用的函数放在赋值语句的左侧,这实际上意味着可以将值赋给引用的对象。但可以使用const来确保引用或指针返回的值不能用于修改对象中的数据: const stock & stock::topval (const stock & s) const { if (s.total val > total val) return s; // argument object else return *this; // invoking object 该方法返回对this或s的引用。因为this和s都被声明为const,所以函数不能对它们进行修改,这意味着返回的引用也必须被声明为const. 注意,如果函数将参数声明为指向const的引用或指针,则不能将该参数传递给另一个函数,除非后者也确保了参数不会被修改。
公有继承的考虑因素
is-a关系
为什么不能被继承
赋值运算符
私有成员与保护成员
虚方法
析构函数
友元函数
有关使用基类方法的说明
类函数小结
函数 能否继承 成员还是友元 默认能否生成 能否为虚函数 是否可以有返回类型 构造函数 否 成员 能 否 否 析构函数 否 成员 能 能 否 = 否 成员 能 能 能 & 能 任意 能 能 能 转换函数 能 成员 否 能 否 () 能 成员 否 能 能 [] 能 成员 否 能 能 -> 能 成员 否 能 能 op= 能 任意 否 能 能 new 能 静态成员 否 否 void * delete 能 静态成员 否 否 void * 其他运算符 能 任意 否 能 能 其他成员 能 成员 否 能 能 友元 否 友元 否 否 能
成员函数属性
能否继承
成员还是友元
能否默认生成
能否为虚函数
是否可以有返回类型
C++中的代码重用
包含对象成员的类
包含(也称为:组合、层次化)
建立has-a关系
A对象中的B对象:A包含B
对比
包含=A类获得了其成员B对象的实现,但不继承接口
公有继承=获得实现(若有)+继承接口
接口与实现
接口和实现 使用公有继承时,类可以继承接口,可能还有实现(基类的纯虚函数提供接口,但不提供实现),获得接口是is-a关系的组成部分。 使用组合,类可以获得实现,但不能获得接口。不继承接口是has-a关系的组成部分。
接口:这里的接口是狭义上的接口,特指被public访问控制符包含的类成员,包括公有的数据成员和公有的函数成员。
初始化被包含的对象(成员对象)
构造函数使用其成员名
因为该构造函数初始化的是成员对象,而不是继承的对象,所以在初始化列表中使用的是成员名,而不是类名。初始化列表中的每一项都调用与之匹配的构造函数。 如果不使用初始化列表语法,情况将如何呢? C++要求在构建对象的其他部分之前,先构建继承对象的所有成员对象。因此,如果省略初始化列表, C++将使用成员对象所属类的默认构造函数。
初始化顺序
当初始化列表包含多个项目时,这些项目被初始化的顺序为它们被声明的顺序,而不是它们在初始化列表中的顺序。
使用被包含对象的接口
被包含对象的接口不是公有的,但可以在类方法中使用它。
对象名.数据成员 / 对象名.函数成员
私有继承
C++还有另一种实现has-a关系的途径--私有继承。使用私有继承,基类的公有成员和保护成员都将成为派生类的私有成员。这意味着基类方法将不会成为派生对象公有接口的一部分,但可以在派生类的成员函数中使用它们。 下面更深入地探讨接口问题。使用公有继承,基类的公有方法将成为派生类的公有方法。总之,派生类将继承基类的接口;这是is-a关系的一部分。使用私有继承,基类的公有方法将成为派生类的私有方法。总之,派生类不继承基类的接口。正如从被包含对象中看到的,这种不完全继承是has-a关系的一部分。使用私有继承,类将继承实现。例如,如果从String类派生出Student类,后者将有一个String类组件,可用于保存字符串。另外, Student方法可以使用String方法来访问String组件。包含将对象作为一个命名的成员对象添加到类中,而私有继承将对象作为一个未被命名的继承对象添加到类中。我们将使用术语子对象(subobject)来表示通过继承或包含添加的对象。
建立has-a关系
C++提供了几种重用代码的手段。公有继承能够建立is-a关系,这样派生类可以重用基类的代码。私有继承和保护继承也使得能够重用基类的代码,但建立的是has-a关系。使用私有继承时,基类的公有成员和保护成员将成为派生类的私有成员;使用保护继承时,基类的公有成员和保护成员将成为派生类的保护成员。无论使用哪种继承,基类的公有接口都将成为派生类的内部接口。这有时候被称为继承实现,但并不继承接口,因为派生类对象不能显式地使用基类的接口。因此,不能将派生对象看作是一种基类对象。由于这个原因,在不进行显式类型转换的情况下,基类指针或引用将不能指向派生类对象。
使用包含还是私有继承
通常,应使用包含来建立has-a关系;如果新类需要访问原有类的保护成员,或需要重新定义虚函数,则应使用私有继承。 解释: 使用包含还是私有继承由于既可以使用包含,也可以使用私有继承来建立has-a关系,那么应使用种方式呢?大多数C++程序员倾向于使用包含。首先,它易于理解。类声明中包含表示被包含类的显式命名对象,代码可以通过名称引用这些对象,而使用继承将使关系更抽象。其次,继承会引起很多问题,尤其从多个基类继承时,可能必须处理很多问题,如包含同名方法的独立的基类或共享祖先的独立基类。总之,使用包含不太可能遇到这样的麻烦。另外,包含能够包括多个同类的子对象。如果某个类需要3个string对象,可以使用包含声明3个独立的string成员。而继承则只能使用一个这样的对象(当对象都没有名称时,将难以区分)。 然而,私有继承所提供的特性确实比包含多。例如,假设类包含保护成员(可以是数据成员,也可以是成员函数),则这样的成员在派生类中是可用的,但在继承层次结构外是不可用的。如果使用组合将这样的类包含在另一个类中,则后者将不是派生类,而是位于继承层次结构之外,因此不能访问保护成员。但通过继承得到的将是派生类,因此它能够访问保护成员。 另一种需要使用私有继承的情况是需要重新定义虚函数。派生类可以重新定义虚函数,但包含类不能使用私有继承,重新定义的函数将只能在类中使用,而不是公有的。
初始化基类组件
构造函数使用基类的类名
访问基类的方法
基类名::方法名
访问基类的对象
使用强制类型转换
(const 基类名&) *this
访问基类的友元函数
使用强制类型转换
保护继承
各种继承方式对比
使用using重新定义访问权限
只适用于继承关系
多重继承
类模板
定义模板类
template<typename Type>
template<class Type>
泛型标识符 Type
类型参数
使用模板类
必须显式地指出模板类的具体类型
数组模板示例和非类型参数
指定数组大小的数组模板
方案1:在类中使用动态数组和构造函数来提供数目
方案2:使用模板参数来提供常规数据的大小
对比
构造函数方法使用的是通过new和delete管理的堆内存,而表达式参数方法使用的是为自动变量维护的内存栈。这样,执行速度将更快,尤其是在使用了很多小型数组时。 表达式参数方法的主要缺点是,每种数组大小都将生成自己的模板。也就是说,下面的声明将生成两个独立的类声明: ArrayTpcdouble, 12> eggweights; ArrayTp<double, 13> donuts; 但下面的声明只生成一个类声明,并将数组大小信息传递给类的构造函数: stack<int> eggs (12); stack<int> dunkers(13); 另一个区别是,构造函数方法更通用,这是因为数组大小是作为类成员(而不是硬编码)存储在定义中的。这样可以将一种尺寸的数组赋给另一种尺寸的数组,也可以创建允许数组大小可变的类。
非类型参数(表达式参数)
表达式参数的限制: 表达式参数只能是整型、枚举、引用和指针。 模板代码不能修改参数的值,也不能使用参数的地址。 实例化模板类时,用作表达式参数的值必须是常量表达式。
array模板类
模板多功能性
递归使用模板
Array< Array<int, 5>, 10> twodee;
使用多个类型参数
模板可以包含多个类型参数。例如,假设希望类可以保存两种值,则可以创建并使用Pair模板来保存两个不同的值(标准模板库提供了类似的模板,名为pair)。
预定义模板类pair<class T, class U>
默认类型模板参数
类模板的另一项新特性是,可以为类型参数提供默认值: template cclass T1, class T2 = int> class Topo {...;} 这样,如果省略T2的值,编译器将使用int: Topo<double, double> m1; // T1 is double, T2 is double Topo<double> m2; // TI is double, T2 is int 标准模板库经常使用该特性,将默认类型设置为类。虽然可以为类模板类型参数提供默认值,但不能为函数模板参数提供默认值(但函数模板可以具体化)。然而,可以为非类型参数提供默认值,这对于类模板和函数模板都是适用的。
模板具体化
隐式实例化
显式实例化
显式具体化
部分具体化
成员模板
将模板用作参数
模板和友元
模板别名
友元和异常
友元
分类
友元函数
友元类
例子:电视机类和遥控器类
友元类 什么时候希望一个类成为另一个类的友元呢?我们来看一个例子。假定需要编写一个模拟电视机和遥控器的简单程序。决定定义一个TV类和一个Remote类,来分别表示电视机和遥控器。很明显,这两个类之间应当存在某种关系,但是什么样的关系呢?遥控器并非电视机,反之亦然,所以公有继承的is-a关系并不适用。遥控器也非电视机的一部分,反之亦然,因此包含或私有继承和保护继承的has-a关系也不适用。事实上,遥控器可以改变电视机的状态,这表明应将Romote类作为Tv类的一个友元。 这个例子的主要目的在于表明,类友元是一种自然用语,用于表示一些关系。如果不使用某些形式的友元关系,则必须将TV类的私有部分设置为公有的,或者创建一个笨拙的、大型类来包含电视机和遥控器。这种解决方法无法反应这样的事实,即同一个遥控器可用于多台电视机。
友元成员函数
向前声明
其它友元关系
互为友元类
共同的友元
函数需要访问两个类的私有数据
嵌套类
嵌套类 在C++中,可以将类声明放在另一个类中。在另一个类中声明的类被称为嵌套类(nested class),它通过提供新的类型类作用域来避免名称混乱。包含类的成员函数可以创建和使用被嵌套类的对象;而仅当声明位于公有部分,才能在包含类的外面使用嵌套类,而且必须使用作用域解析运算符. 对类进行嵌套与包含并不同。包含意味着将类对象作为另一个类的成员,而对类进行嵌套不创建类成员,而是定义了一种类型,该类型仅在包含嵌套类声明的类中有效。对类进行嵌套通常是为了帮助实现另一个类,并避免名称冲突。
嵌套类和访问权限
嵌套类作用域
嵌套类、结构和枚举的作用域特征 声明位置 包含它的类是否可以使用它 从包含它的类而派生而来的类是否可以使用它 在外部是否可以使用 私有部分 是 否 否 保护部分 是 是 否 公有部分 是 是 是,通过类限定符来使用
访问控制
访问控制 类可见后,起决定作用的将是访问控制。对嵌套类访问权的控制规则与对常规类相同。在Queue类声明中声明Node类并没有赋予Queue类任何对Node类的访问特权,也没有赋予Node类任何对Queue类的访问特权。因此, Queue类对象只能显示地访问Node对象的公有成员。由于这个原因,在Queue示例中,Node类的所有成员都被声明为公有的。这样有悖于应将数据成员声明为私有的这一惯例,但Node类是Queue类内部实现的一项特性,对外部世界是不可见的。这是因为Node类是在Queue类的私有部分声明的。所以,虽然Queue的方法可直接访问Node的成员,但使用Queue类的客户不能这样做。 总之,类声明的位置决定了类的作用域或可见性。类可见后,访问控制规则(公有、保护、私有、友元)将决定程序对嵌套类成员的访问权限。
模板中的嵌套
异常
调用abort( )
返回错误码
异常机制
关键字try、关键字catch、关键字throw
将对象用作异常类型
通常,引发异常的函数将传递一个对象。这样做的重要优点之一是,可以使用不同的异常类型来区分不同的函数在不同情况下引发的异常。另外,对象可以携带信息,程序员可以根据这些信息来确定引发异常的原因。同时, catch块可以根据这些信息来决定采取什么样的措施。
异常规范
关键字noexcept、关键字throw
栈解退
假设try块没有直接调用引发异常的函数,而是调用了对引发异常的函数进行调用的函数,则程序流程将从引发异常的函数跳到包含try块和处理程序的函数。这涉及到栈解退(unwinding the stack),下面进行介绍。 首先来看一看C++通常是如何处理函数调用和返回的。C++通常通过将信息放在栈中来处理函数调用。具体地说,程序将调用函数的指令的地址(返回地址)放到栈中。当被调用的函数执行完毕后,程序将使用该地址来确定从哪里开始继续执行。另外,函数调用将函数参数放到栈中。在栈中,这些函数参数被视为自动变量。如果被调用的函数创建了新的自动变量,则这些变量也将被添加到栈中。如果被调用的函数调用了另一个函数,则后者的信息将被添加到栈中,依此类推。当函数结束时,程序流程将跳到该函数被调用时存储的地址处,同时栈顶的元素被释放。因此,函数通常都返回到调用它的函数,依此类推,同时每个函数都在结束时释放其自动变量。如果自动变量是类对象,则类的析构函数(如果有的话)将被调用。 现在假设函数由于出现异常(而不是由于返回)而终止,则程序也将释放栈中的内存,但不会在释放栈的第一个返回地址后停止,而是继续释放栈,直到找到一个位于try块中的返回地址。随后,控制权将转到块尾的异常处理程序,而不是函数调用后面的第一条语句。这个过程被称为栈解退。引发机制的一个非常重要的特性是,和函数返回一样,对于栈中的自动类对象,类的析构函数将被调用。然而,函数返回仅仅处理该函数放在栈中的对象,而throw语句则处理try块和throw之间整个函数调用序列放在栈中的对象。如果没有栈解退这种特性,则引发异常后,对于中间函数调用放在栈中的自动类对象,其析构函数将不会被调用。
普通函数的调用返回机制
栈解退特性
对比
throw和return之间的区别
throw和return之间的区别 假设函数n1()调用函数2(),2()中的返回语句导致程序执行在函数fl()中调用函数2()后面的一条语句。 throw语句导致程序沿函数调用的当前序列回溯,直到找到直接或间接包含对2()的调用的try语句块为止。它可能在f()中、调用f()的函数中或其他函数中。找到这样的try语句块后,将执行下一个匹配的catch语句块,而不是函数调用后的语句。
其它异常特性
throw-catch机制
虽然throw-catch机制类似于函数参数和函数返回机制,但还是有些不同之处。其中之一是函数fun( )中的返回语句将控制权返回到调用fun()的函数,但throw语句将控制权向上返回到第一个这样的函数:包含能够捕获相应异常的try-catch组合。
临时拷贝机制
另一个不同之处是,引发异常时编译器总是创建一个临时拷贝,即使异常规范和catch块中指定的是引用。例如,请看下面的代码: class problem {...}; ... void super() throw (problem) { ... if (oh_no)problem oops; // construct object throw oops; // throw it ... } ... try { super(); } catch (problem & p) { // statements } p将指向oops的副本而不是oops本身。这是件好事,因为函数super( )执行完毕后, oops将不复存在。顺便说一句,将引发异常和创建对象组合在一起将更简单: throw problem(); // construct and throw default problem object 您可能会问,既然throw语句将生成副本,为何代码中使用引用呢?毕竟,将引用作为返回值的通常原因是避免创建副本以提高效率。答案是,引用还有另一个重要特征:基类引用可以执行派生类对象。假设有,一组通过继承关联起来的异常类型,则在异常规范中只需列出一个基类引用,它将与任何派生类对象匹配。
exception类
what( )函数
头文件stdexcept
logic_error类
domain_error类
invalid_argument类
length_eroor类
out_of_bounds类
runtime_error类
range_error类
overflow_error类
underflow_error类
头文件new
bad_new类
std::bad_alloc 异常
空指针和new
std::nothrow
异常、类和继承
异常、类和继承以三种方式相互关联。首先,可以像标准C++库所做的那样, 从一个异常类派生出另一个, 可以在类定义中嵌套异常类声明; 这种嵌套声明本身可被继承,还可用作基类。
未捕获异常和意外异常
异常被引发后,在两种情况下,会导致问题。 首先,如果它是在带异常规范的函数中引发的,则必须与规范列表中的某种异常匹配(在继承层次结构中,类类型与这个类及其派生类的对象匹配),否则称为意外异常(unexpected exception),在默认情况下,这将导致程序异常终止(虽然C++11摒弃了异常规范,但仍支持它,且有些现有的代码使用了它)。 如果异常不是在函数中引发的(或者函数没有异常规范),则必须捕获它。如果没被捕获(在没有try块或没有匹配的catch块时,将出现这种情况),则异常被称为未捕获异常(uncaught exception),在默认情况下,这将导致程序异常终止。 可以修改程序对意外异常和未捕获异常的反应。
未捕获异常
terminate( )
默认调用abort( )
set_terminate( )
意外异常
unexpected( )
set_unexpected( )
注意事项
内存动态分配和异常
内存泄漏问题
智能指针模板
运行阶段类型识别(RTTI)
RTTI的用途
RTTI的工作原理
运算符dynamic_cast
注意:通常,如果指向的对象(*pt)的类型为Type或者是从Type直接或间接派生而来的类型,则下面的表达式将指针pt转换为Type类型的指针: dynamic cast<Type *>(pt) 否则,结果为0,即空指针。
bad_cast异常
运算符typeid
重载了==和!=运算符
bad_typeid异常
type_info类
name( )成员函数
类型转换运算符
string类和标准模板
string类和STL库全面总结
附录F 模板类string
附录G 标准模板库方法和函数STL
string类
构造字符串
9种构造函数
string类输入
getline(cin, str)
cin >> str
使用字符串
其他功能
返回C-风格字符串str.c_str( )
字符串种类
本质:模板类basic_string的具体化,然后typedef取的别名
string、wstring、u16string、u32string
解读
成员函数和运算符被多次重载
参数是string对象、C-风格字符串、char值
智能指针模板类
智能指针模板类用于帮助动态内存管理。
使用智能指针
智能指针模板类auto_ptr、unique_ptr、shared_ptr
所有的智能指针类都是explicit构造函数
智能指针只能指向动态分配的堆内存
注意事项
auto_ptr导致的多次删除同一对象
为何有三种智能指针呢?实际上有4种,但本书不讨论weak ptr。为何摒弃auto_ptr呢?先来看下面的赋值语句: auto_ptr<string> ps (new string("I reigned lonely as a cloud.")); auto_ptr<string> vocation;vocation = ps; 上述赋值语句将完成什么工作呢?如果ps和vocation是常规指针,则两个指针将指向同一个string对象。这是不能接受的,因为程序将试图删除同一个对象两次,一次是ps过期时,另一次是vocation过期时。要避免这种问题,方法有多种。 定义赋值运算符,使之执行深复制。这样两个指针将指向不同的对象,其中的一个对象是另一个对象的副本。 建立所有权(ownership)概念,对于特定的对象,只能有一个智能指针可拥有它,这样只有拥有对象的智能指针的构造函数会删除该对象。然后,让赋值操作转让所有权。这就是用于auto_ptr和unique_ptr的策略,但unique_ptr的策略更严格。 创建智能更高的指针,跟踪引用特定对象的智能指针数。这称为引用计数(reference counting).例如,赋值时,计数将加1,而指针过期时,计数将减1,仅当最后一个指针过期时,才调用delete.这是shared_ptr采用的策略。 当然,同样的策略也适用于复制构造函数。
选择智能指针
要使用多个指向同一对象的指针
选择shared_ptr
不需要使用多个指向同一对象的指针
选择unique_ptr
标准模板库
模板类vector
要创建vector模板对象,可使用通常的<type>表示法来指出要使用的类型。另外, vector模板使用动态内存分配,因此可以用初始化参数来指出需要多少矢量: #include vector using namespace std; vectorsint> ratings (5); // a vector of 5 intsint n; cin >> n; vector<double> scores (n); // a vector of n doubles 由于运算符[ ]被重载,因此创建vector对象后,可以使用通常的数组表示法来访问各个元素: ratings [0] =9; for (int i = 0; i s n; it+) cout << scores[i] << endl;
分配器
分配器 与string类相似,各种STL容器模板都接受一个可选的模板参数,该参数指定使用哪个分配器对象来管理内存。例如, vector模板的开头与下面类似: template <class T, class Allocator = allocator<T>> class vector {... 如果省略该模板参数的值,则容器模板将默认使用allocator<T>类。这个类使用new和delete.
可对vector执行的操作
size( )、begin( )、end( )、push_back( )、erase( )、insert( )
对vector可执行的其它操作
STL函数:for_each( )、random_shuffle( )、sort( )
sort( )与全排序、完整弱排序
基于范围的for循环
for(auto 元素 : 容器)
对比for_each( )
在这种for循环中,括号内的代码声明一个类型与容器存储的内容相同的变量,然后指出了容器的名称。接下来,循环体使用指定的变量依次访问容器的每个元素。例如: for_each(books.begin(), books.end(), ShowReview); 可将其替换为下述基于范围的for循环: for (auto x: books) ShowReview(x); 根据book的类型(vector<Review>),编译器将推断出x的类型为Review,而循环将依次将books中的每个Review对象传递给ShowReview( )。 不同于for_each(),基于范围的for循环可修改容器的内容,诀窍是指定一个引用参数。例如,假设有如下函数 void InflateReview (Review &r) {r.rating++}; 可使用如下循环对books的每个元素执行该函数: for (auto &x: books) InflateReview(x);
泛型编程
有了一些使用STL的经验后,来看一看底层理念。STL是一种泛型编程(generic programming)。面向对象编程关注的是编程的数据方面,而泛型编程关注的是算法。它们之间的共同点是抽象和创建可重用代码,但它们的理念绝然不同。 泛型编程旨在编写独立于数据类型的代码。在C++中,完成通用程序的工具是模板。当然,模板使得能够按泛型定义函数或类,而STL通过通用算法更进了一步。模板让这一切成为可能,但必须对元素进行仔细地设计。为解模板和设计是如何协同工作的,来看一看需要迭代器的原因。
标准模板库STL
STL术语
概念:描述一系列的要求
模板参数与概念
模板参数与迭代器概念
正如您多次看到的, STL函数使用迭代器和迭代器区间。从函数原型可知有关迭代器的假设。例如,copy()函数的原型如下: template<class Inputtterator, class Outputtterator> OutputIterator copy (InputIterator first, InputIterator last.OutputIterator result); 因为标识符Inpulterator和Outputterator都是模板参数,所以它们就像T和U一样。然而, STL文档使用模板参数名称来表示参数模型的概念。因此上述声明告诉我们,区间参数必须是输入迭代器或更高级别的迭代器,而指示结果存储位置的迭代器必须是输出迭代器或更高级别的迭代器。
模板参数与函数符概念
有些函数有这样的版本,即根据将函数应用于容器元素得到的结果来执行操作。这些版本的名称通常以if结尾。例如,如果将函数用于旧值时,返回的值为true,则replace_if( )将把旧值替换为新的值。下面是该函数的原型: template<class ForwardIterator, class Predicate class T> void replace_if (Forwarditerator first, Forwarditerator last, predicate pred, const T& new value); 谓词是返回bool值的一元函数。Predicate也是模板参数名称,可以为T或U,然而, STL选择用Predicate来提醒用户模拟Predicate概念(函数符-谓词的概念)
注意编译器不直接检查概念
STL使用诸如Generator和BinaryPredicate等术语来指示必须模拟函数符概念的参数。请记住,虽然文档可指出迭代器或函数符需求,但编译器不会对此进行检查。如果您使用了错误的迭代器,则编译器试图实例化模板时,将显示大量的错误消息。
改进:表示概念上的继承
模型:概念的具体实现
模型:概念的具体实现 因此,指向int的常规指针是一个随机访问迭代器模型,也是一个正向迭代器模型,因为它满足该概念的所有要求。 vector是一种序列容器类型,还是可反转容器概念的模型。
STL组成:容器(containers)、迭代器(iterators)、空间配置器(allocator)、适配器(adapters)、算法(algorithms)、函数符(functors)六个部分
迭代器
迭代器(iterator)有时又称游标(cursor)是程序设计的软件设计模式,可在容器(container,例如链表或阵列)上遍访的接口,设计人员无需关心容器的内容。
为何使用迭代器
迭代器是STL算法的接口
模板使得算法独立于存储的数据类型
迭代器使得算法独立于使用的容器类型
基于算法的要求设计迭代器特征和容器特征
来总结一下STL方法。首先是处理容器的算法,应尽可能用通用的术语来表达算法,使之独立于数据类型和容器类型。为使通用算法能够适用于具体情况,应定义能够满足算法需求的迭代器,并把要求加到容器设计上。即基于算法的要求,设计基本迭代器的特征和容器特征。
超尾标记
迭代器类型
分类
输入迭代器、输出迭代器、正向迭代器、双向迭代器、随机访问迭代器
共性:可以执行解除引用操作、可以进行比较、递增、同一个类级typedef名称:iterator
迭代器层次结构
5种迭代器功能具有层次递增的包含关系
迭代器功能 迭代器功能 输 入 输出 正向 双向 随机访问 解除引用读取 有 无 有 有 有 解除引用写入 无 有 有 有 有 固定和可重复排序 无 无 有 有 有 ++i i++ 有 有 有 有 有 --i i-- 无 无 无 有 有 i[n] 无 无 无 无 有 i + n 无 无 无 无 有 i - n 无 无 无 无 有 i += n 无 无 无 无 有 i -= n 无 无 无 无 有
将指针用作迭代器
适配器
适配器是一个类或函数,可以将其它接口转换为STL使用的接口。
迭代器适配器
ostream_iterator模板使ostream输出用作迭代器接口
istream_iterator模板使istream输入用作迭代器接口
其他有用的迭代器
reverse_iterator
back_insert_iterator、front_insert_iterator、insert_iterator
容器种类
容器
容器概念
容器概念:是具有诸如容器、序列容器和关联容器等名称的一般类别。
容器的要求
容器类型
容器类型:可用于创建具体容器对象的模板。
vector、set、map等15种容器类型
序列容器
序列的要求
序列的可选要求
分类
vector
deque
list
forward_list
适配器类
queue
priority_queue
stack
非STL容器
array
关联容器
有序关联容器
set
multiset
map
multimap
模板类pair<class T, class U>
无序关联容器
unordered set、unordered multiset,、unorderedmap、unordered multimap
函数对象(函数符)
很多STL算法都使用函数对象,也叫函数符(functor),函数符是可以以函数方式与( )结合使用的任意对象。这包括函数名、指向函数的指针和重载了( )运算符的类对象(即定义了函数operator( )的类)。 STL定义了多个基本函数符,它们执行诸如将两个值相加、比较两个值是否相等操作。提供这些函数对象是为了支持将函数作为参数的STL函数。
函数符概念
函数对象是重载了( )运算符的类
分类
生成器、一元函数、二元函数
谓词、二元谓词
预定义的函数符
头文件function
运算符和对应的函数符
对于所有内置的算术运算符、关系运算符和逻辑运算符, STL都提供了等价的函数符。它们可以用于处理C++内置类型或任何用户定义类型(如果重载了相应的运算符)。 运算符 对应的函数符 + plus - minus * multiplies / divides % modulus - negate == equal_to != not_equal_to > greater < less >= greater_equal <= less_equal && logical_and || logical_or ! logical_not 使用函数符例子: #include <functional> ... plus<double> add; // create a pluse<double> object double y= add(2.2, 3.4); // using plus<doublex::operator( ) ( )
包装器(适配器)
包装器(也叫适配器)。用于给其它编程接口提供更一致或更合适的接口。
自适应函数符和函数适配器
自适应函数符
这里列出的预定义函数符都是自适应的。实际上STL有5个相关的概念: 自适应生成器(adaptablegenerator)、自适应一元函数(adaptable unary function)、自适应二元函数(adaptable binary function)、自适应谓词(adaptable predicate)和自适应二元谓词(adaptable binary predicate)。 使函数符成为自适应的原因是,它携带了标识参数类型和返回类型的typedef成员。这些成员分别是result_type、first_argument_type和second_argument_type,它们的作用是不言自明的。例如, plus<int>对象的返回类型被标识为plus<int>::result_type,这是int的typedef。 函数符自适应性的意义在于:函数适配器对象可以使用函数符,并认为存在这些typedef成员。例如接受一个自适应函数符参数的函数(比如函数适配器函数)可以使用result_type成员来声明一个与函数的返回类型匹配的变量。
函数适配器
函数适配器是用来让一个函数对象表现出另外一种类型的函数对象的特征,以满足算法的接口要求。例如函数适配器可以使二元函数的第二个参数值为默认值,从而使二元函数转变为一元函数,一些使用一元函数的算法就可以使用该算法了。
函数适配器类
binder1st、binder2nd
函数适配器函数
bind1st( )、bind2nd( )
function类模板
可调用类型:函数名、函数指针、函数对象、有名称的Lambda表达式
函数符替代品:Lambda表达式
比较函数指针、函数符和Lambda函数
Lambda函数例子: [ ] (int x) {return x%3 == 0;} Lambda函数要点: 1. 使用[ ]代替函数名 2. 没有返回值类型。仅当lambad表达式完全由一条返回语句组成时, 自动类型推断才管用;否则,需要使用新增的返回 类型后置语法: [ ] (double x)->double {int y =x; returnx-y;} // return type is double // 比较函数指针、函数符和Lambda函数 // lambda0.cpp -- using lambda expressions #include <iostream> #include <vector> #include <algorithm> #include <cmath> #include <ctime> const long Size1 = 39L; const long Size2 = 100 * Size1; const long Size3 = 100 * Size2; bool f3(int x) { return x % 3 == 0; } bool f13(int x) { return x % 13 == 0; } int main() { using std::cout; std::vector<int> numbers(Size1); std::srand(std::time(0)); std::generate(numbers.begin(), numbers.end(), std::rand); // 使用函数指针 cout << "Sample size = " << Size1 << '\n'; int count3 = std::count_if(numbers.begin(), numbers.end(), f3); cout << "Count of numbers divisible by 3: " << count3 << '\n'; int count13 = std::count_if(numbers.begin(), numbers.end(), f13); cout << "Count of numbers divisible by 13: " << count13 << "\n\n"; // increase number of numbers numbers.resize(Size2); std::generate(numbers.begin(), numbers.end(), std::rand); cout << "Sample size = " << Size2 << '\n'; // 使用函数符 class f_mod { private: int dv; public: f_mod(int d = 1) : dv(d) {} bool operator()(int x) { return x % dv == 0; } }; count3 = std::count_if(numbers.begin(), numbers.end(), f_mod(3)); cout << "Count of numbers divisible by 3: " << count3 << '\n'; count13 = std::count_if(numbers.begin(), numbers.end(), f_mod(13)); cout << "Count of numbers divisible by 13: " << count13 << "\n\n"; // increase number of numbers again numbers.resize(Size3); std::generate(numbers.begin(), numbers.end(), std::rand); cout << "Sample size = " << Size3 << '\n'; // 使用lambdas count3 = std::count_if(numbers.begin(), numbers.end(), [](int x) {return x % 3 == 0; }); cout << "Count of numbers divisible by 3: " << count3 << '\n'; count13 = std::count_if(numbers.begin(), numbers.end(), [](int x) {return x % 13 == 0; }); cout << "Count of numbers divisible by 13: " << count13 << '\n'; // std::cin.get(); return 0; }
有名的Lambda表达式
auto mod3 = [ ](int x){return x % 3 == 0;} 这样mod3就是这个lambda表达式的名称,可以把mod3当作函数名使用。
额外功能:访问作用域内变量
lambda有一些额外的功能。具体地说, lambad可访问作用域内的任何动态变量;要捕获要使用的变量,可将其名称放在中括号内。如果只指定了变量名,如[z],将按值访问变量;如果在名称前加上&,如[&count],将按引用访问变量。[&]让您能够按引用访问所有动态变量,而[=]让您能够按值访问所有动态变量。还可混合使用这两种方式,例如, [ted, &edj让您能够按值访问ted以及按引用访问ed, [&, tedj让您能够按值访问ted以及按引用访问其他所有动态变量。
例子:使用Lambda时机
// lambda1.cpp -- use captured variables #include <iostream> #include <vector> #include <algorithm> #include <cmath> #include <ctime> const long Size = 390000L; int main() { using std::cout; std::vector<int> numbers(Size); std::srand(std::time(0)); std::generate(numbers.begin(), numbers.end(), std::rand); cout << "Sample size = " << Size << '\n'; // using lambdas int count3 = std::count_if(numbers.begin(), numbers.end(), [](int x){return x % 3 == 0;}); cout << "Count of numbers divisible by 3: " << count3 << '\n'; int count13 = 0; std::for_each(numbers.begin(), numbers.end(), [&count13](int x){count13 += x % 13 == 0;}); cout << "Count of numbers divisible by 13: " << count13 << '\n'; // using a single lambda count3 = count13 = 0; std::for_each(numbers.begin(), numbers.end(), [&](int x){count3 += x % 3 == 0; count13 += x % 13 == 0;}); cout << "Count of numbers divisible by 3: " << count3 << '\n'; cout << "Count of numbers divisible by 13: " << count13 << '\n'; // std::cin.get(); return 0; }
算法
STL包含很多处理容器的非成员函数,前面已经介绍过其中的一些: sort( ),copy( )、find( )、random_shuffle( )、 set_union( )、 set_intersection( )、 set_difference( )和transform( ),可能已经注意到,它们的总体设计是相同的,都使用迭代器来标识要处理的数据区间和结果的放置位置。有些函数还接受一个函数对象参数,并使用它来处理数据。 对于算法函数设计,有两个主要的通用部分。首先,它们都使用模板来提供泛型;其次,它们都使用迭代器来提供访问容器中数据的通用表示。因此, copy( )函数可用于将double值存储在数组中的容器、将string值存储在链表中的容器,也可用于将用户定义的对象存储在树结构中(如set所使用的)的容器。因为指针是一种特殊的迭代器,因此诸如copy( )等STL函数可用于常规数组。 统一的容器设计使得不同类型的容器之间具有明显关系。例如,可以使用copy( )将常规数组中的值复制到vector对象中,将vector对象中的值复制到list对象中,将list对象中的值复制到set对象中。可以用==来比较不同类型的容器,如deque和vector,之所以能够这样做,是因为容器重载的==运算符使用迭代器来比较内容,因此如果deque对象和vector对象的内容相同,并且排列顺序也相同,则它们是相等的。
算法组
非修改式序列操作
修改式序列操作
排序和相关操作
头文件algorithm
通用数字运算
头文件numerica
算法的通用特征
模板函数参数标识符的作用
标识符指出算法需要的模型对应的概念
按算法结果放置位置分类
就地算法
复制算法
以_copy结尾
根据将函数应用于容器元素得到的结果来执行操作的算法
以_if结尾
STL和string类
string类虽然不是STL的组成部分,但设计它时考虑到了STL,例如,它包含begin( )、 end( )、rbegin( )和rend( )等类成员,因此可以使用STL接口。
STL函数和容器方法
优先选择容器方法
有时可以选择使用STL方法或STL函数。通常方法是更好的选择。 首先,它更适合于特定的容器;其次,作为成员函数,它可以使用模板类的内存管理工具,从而在需要时调整容器的长度。 例如,假设有一个由数字组成的链表,并要删除链表中某个特定值(例如4)的所有实例。如果la是一个listint对象,则可以使用链表的remove( )方法: la.remove (4); // remove all 4s from the list 调用该方法后,链表中所有值为4的元素都将被删除,同时链表的长度将被自动调整。 还有一个名为remove( )的STL算法,它不是由对象调用,而是接受区间参数。因此,如果lb是一个lists<int>对象,则调用该函数的代码如下: remove (1b.begin(), 1b.end (), 4), 然而,由于该remove( )函数不是成员,因此不能调整链表的长度。它将没被删除的元素放在链表的开始位置,并返回一个指向新的超尾值的迭代器。这样,便可以用该迭代器来修改容器的长度。例如,可以使用链表的erase()方法来删除一个区间,该区间描述了链表中不再需要的部分。
对于容器:STL函数更通用,容器方法更合适
使用STL
//usealgo.cpp -- using several STL elements //https://github.com/lilinxiong/cppPrimerPlus-six-/blob/master/Chapter%2016/usealgo.cpp /* STL是一个库,其组成部分被设计成协同工作。STL组件是工具,但也是创建其他工具的基本部件。 我们用一个例子说明。假设要编写一个程序,让用户输入单词。希望最后得到一个按输入顺序排 列的单词列表、一个按字母顺序排列的单词列表(忽略大小写),并记录每个单词被输入的次数。 出于简化的目的,浸设输入中不包含数字和标点符号。 */ #include <iostream> #include <string> #include <vector> #include <set> #include <map> #include <iterator> #include <algorithm> #include <cctype> using namespace std; char toLower(char ch) { return tolower(ch); } string ToLower(string st); void display(const string& s); int main() { vector<string> words; cout << "Enter words (enter quit to quit):\n"; string input; while (cin >> input && input != "quit") words.push_back(input); cout << "You entered the following words:\n"; for_each(words.begin(), words.end(), display); cout << endl; // place words in set, converting to lowercase set<string> wordset; transform(words.begin(), words.end(), insert_iterator<set<string> >(wordset, wordset.begin()), ToLower); cout << "\nAlphabetic list of words:\n"; for_each(wordset.begin(), wordset.end(), display); cout << endl; // place word and frequency in map map<string, int> wordmap; set<string>::iterator si; for (si = wordset.begin(); si != wordset.end(); si++) wordmap[*si] = count(words.begin(), words.end(), *si); // display map contents cout << "\nWord frequency:\n"; for (si = wordset.begin(); si != wordset.end(); si++) cout << *si << ": " << wordmap[*si] << endl; // cin.get(); // cin.get(); return 0; } string ToLower(string st) { transform(st.begin(), st.end(), st.begin(), toLower); return st; } void display(const string& s) { cout << s << " "; }
容器的列表初始化
模板initializer_list
提供initializer_list类的初衷旨在让您能够将一系列值传递给构造函数或其他函数。 利用模板initializer_list可使用初始化列表语法将STL容器初始化为一系列值: std::vector<double> payments {45.99, 39.23, 19.95, 89.01}; //注意大括号{ }与小括号( )的区别 这将创建一个包含4个元素的容器,并使用列表中的4个值来初始化这些元素。这之所以可行,是因,为容器类现在包含将initializer_list<T>作为参数的构造函数。例如, vector<double>包含一个将initializer_list<double>作为参数的构造函数,因此上述声明与下面的代码等价: std::vector double> payments({45.99, 39.23, 19.95, 89.01}) ; 这里显式地将列表指定为构造函数参数。 如果类有接受initializer_list作为参数的构造函数,则使用语法{ }将调用该构造函数。 std::vector<int>vi{10}; // 1 element set to 10, vi{10} 转换为 vi({10})
使用initializer_list对象
要在代码中使用initializer_list对象,必须包含头文件initializer_list。这个模板类包含成员函数begin( )和end( ),您可使用这些函数来访问列表元素。它还包含成员函数size( ),该函数返回元素数。
与参数包的区别与联系
补充专题
关键字专题
未来持续更补充
关键字const
const用于函数
假设有一个股票类Stock,它的一个成员函数(用于比较两支股票的价格)的原型如下: const stock & topval (const stock & s) const; 该函数隐式地访问一个对象(调用该函数的对象),而显式地访问另一个对象(函数参数s),并返回其中一个对象的引用。括号中的const表明,该函数不会修改被显式地访问的对象s;而括号后的const表明,该函数不会修改被隐式地访问的对象(调用该函数的对象)。由于该函数返回了两个const对象之一的引用,因此返回类型也应为const引用。
统一建模语言(UML)
UML 教程
UML各种图总结-精华
接口
接口泛指实体把自己提供给外界的一种抽象化物(可以为另一实体),用以由内部操作分离出外部沟通方法,使其能被内部修改而不影响外界其他实体与其交互的方式。 人类与电脑等信息机器或人类与程序之间的接口称为用户界面。电脑等信息机器硬件组件间的接口叫硬件接口。电脑等信息机器软件组件间的接口叫软件接口。 在计算机中,接口是计算机系统中两个独立的部件进行信息交换的共享边界。这种交换可以发生在计算机软、硬件,外部设备或进行操作的人之间,也可以是它们的结合。
广义接口
狭义接口
软件接口
硬件接口
硬件接口(hardware interface)指的是两个硬件设备之间的连接方式。硬件接口既包括物理上的接口,还包括逻辑上的数据传送协议。
操作系统
内存管理
堆、栈、静态区、常量区、代码区
自动存储、静态存储、动态存储
计算机组成原理
存储系统
位、字节、字
运算方法与运算器
运算方法
数的机器码表示
整数的表示
原码、反码、补码
浮点数的表示
深入浅出浮点数
浮点数的表示和运算
在线演示
非数值数据的表示
Unicode
Unicode(统一码、万国码、单一码)是计算机科学领域里的一项业界标准,包括字符集、编码方案等。Unicode 是为了解决传统的字符编码方案的局限而产生的,它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。1990年开始研发,1994年正式公布。
国际化策略:字符编码问题
编码类型:ASCII、UTF-8、UTF-16、UTF-32
ASCII编码
ASCII ((American Standard Code for Information Interchange): 美国信息交换标准代码)是基于拉丁字母的一套电脑编码系统,主要用于显示现代英语和其他西欧语言。它是最通用的信息交换标准,并等同于国际标准ISO/IEC 646。ASCII第一次以规范标准的类型发表是在1967年,最后一次更新则是在1986年,到目前为止共定义了128个字符
运算器
定点数四则运算
编译原理
编译原理知识汇总
程序编译过程
例子:gcc编译C语言程序

附录G 标准模板库方法和函数
标准模板库(STL)旨在提供通用算法的高效实现,它通过通用函数(可用于满足特定算法要求的任何容器)和方法(可用于特定容器类实例)来表达这些算法。本附录假设您对STL有一定的了解,例如,本章假设您了解迭代器和构造函数。
STL容器
大部分容器都有的成员
为所有容器定义的类型
X::value_type
X::reference、X::const_reference
X::iterator、X::const_iterator
X::different_type
X::size_type
为所有容器定义的操作
X u、X( )、X(a)、X u(a)、X u = a
r = a
(&a)->X( )
begin( )、end( )、cbegin( )、cend( )
size( )、maxsize( )
empty( )
swap( )
==、!=
可反转容器定义的类型和操作
可反转容器:vector、list、deque、array、set、map
X::reverse_iterator( )、X::const_reverse_iterator( )
a.rbegin( )、a.rend( )、a.crbegin( )、a.crend( )
有序容器操作
除无序set和无序map容器外都需要支持的操作
<、>、<=、>=
序列容器的其它成员
为序列容器定义的其它操作
序列容器:vector、forward_list、list、deque、array
X(n, t)、X a(n, t)
X(i, j)、X a(i, j)
X(初始化列表对象)
a = 初始化列表对象
a.cmplace(p, args)
a.insert(p, t)、a.insert(p, n, t)
a.insert(p, i, j)
a.insert(p, 初始化列表对象)
a.resize(n)、a.resize(n, t)
a.assign(i, j)、a.ssign(n, t)、a.ssign(初始化列表)
a.erase(q)、a.erase(q1, q2)
a.clear( )
a.front( )
为某些序列容器定义的操作
a.back( )
部分序列容器:vector、list、deque
a.push_back(t)
部分序列容器:vector、list、deque
a.pop_back( )
部分序列容器:vector、list、deque
a.emplace_back(args)
部分序列容器:vector、list、deque
a.push_front(t)
部分序列容器:forward_list、list、deque
a.emplace_front()
部分序列容器:forward_list、list、deque
a.pop_front( )
部分序列容器:forward_list、list
a[n]、a.at(n)
部分序列容器:vector、deque、array
vector的其它操作
a.capacity( )
a.reserve(n)
list的其它操作
a.splice(p, b)、a.splice(p, b, i)、a.splice(p, b, i, j)
a.remove(const T& t)
a.unique( )、a.unique(BinaryPredicate bin_pred)
a.merge(b)、a.merge(b, Compare comp)
a.sort( )、a.sort(Compare comp)
a.revese( )
forward_list操作
insert_after( )、erase_after( )、splice_after( )
其它操作同list
有序关联容器
有序关联容器:set、multiset、map、multimap
为有序关联容器定义的类型
X::key_type
X::key_compare
X::value_compare
X::mapped_type
仅限于容器map、multimap
为有序关联容器定义的操作
X(i, j, c)、X a(i, j, c)、X(i, j)、X a(i, j)
X(初始化列表对象)
a = 初始化列表对象
a.key_comp( )
a.value_comp( )
a_uniq.insert(t)
a_eq.insert(t)
a.insert(p, t)
a.insert(初始列表对象)
a_uniq.emplace(args)、a_eq.emplace(args)
a.emplace_hint(args)
a.erase(迭代器)
e.erase(q1, q2)
a.clear( )
键值k相关操作
a.erase(k)
a.find(k)
a.count(k)
a.lower_bound(k)
a.upper_bound(k)
a.equal_range(k)
a.operator[ ](k)
仅限于map
无序关联容器
无序关联容器:unordered_set、unordered_multiset、unordered_map、unordered_multimap
为无序关联容器定义的类型
X::key_type
X::key_equal
X::hasher
X::local_iterator
X::const_local_iterator
X::mapped_type
为无序关联容器定义的操作
X(n, hf, eq)、X a(n, hf, eq)、X(i, j, n, hf, eq)、X a(i, j, n, hf, eq)
b.hash_function( )
b.key_eq( )
b.bucket_count( )
b.max_bucket_count( )
b.bucket(键值)
b.bucket_size(n)
b.begin(n)、b.end(n)、b.cbegin(n)、b.cend(n)
b.load_factor( )
b.max_load_factor( )、b.max_load_factor(z)
a.rehash(n)
a.reserve(n)
STL函数
非修改式序列操作
all_of( )、any_of( )、none_of( )
for_each( )
find( )、find_if( )、find_if_not( )
find_end( )
find_first_of( )
adjacent_find( )
count( )、count_if( )
mismatch( )
equal( )
is_permutation( )
search( )
search_n( )
修改式序列操作
copy( )、copy_n( )、copy_if( )、copy_backward( )
move( )、move_backward( )
swap( )、swap_ranges( )
iter_swap( )
transform( )
replace( )、repalce_if( )、replace_if( )、replace_copy( )、replace_copy_if( )
fill( )、fill(n)
generate( )、generate_n( )
remove( )、remove_if( )、remove_copy( )、remove_copy_if( )
unique( )、unique_copy( )
reverse( )、reverse_copy( )
rotate( )、rotate_copy( )
shuffle( )
random_shuffle( )
partition( )
stable_partition( )
partition_copy( )
partition_point( )
排序和相关操作
sort( )、stable_sort( )、partial_sort( )、partial_sort_copy( )
is_sorted( )
is_sorted_until( )
nth_element( )
lower_bound( )
upper_bound( )
equal_range( )
binary_search( )
merge( )
implace_merge( )
includes( )
set_union( )
set_intersection( )
set_defference( )
set_symmetric_difference( )
make_heap( )
push_heap( )
pop_heap( )
sort_heap( )
is_heap( )
is_heap_until( )
min( )
max( )
minmax( )
min_element( )
max_element( )
minmax_element( )
lexicographic_compare( )
next_permutation( )
previous_permutation( )
通用数字运算
accumulate( )
inner_product( )
partial_sum( )
adjacent_difference( )
iota( )
附录F 模板类string
string类是基于下述模板定义的: template<class charT, class traits = char_traits<charT>, class Allocator = allocator<charT >> Class basic_string (...); 其中, chatT是存储在字符串中的类型; trais参数是一个类,它定义了类型要被表示为字符串时,所必须具备的特征。例如,它必须有length( )方法,该方法返回被表示为charT数组的字符串的长度。这种数组结尾用charT(0)值(广义的空值字符)表示。这个类还包含用于对值进行比较等操作的方法。 Allocator参数是用于处理字符串内存分配的类。默认的allocator<charT>模板按标准方式使用new和delete. 有4种预定义的具体化: typedef basic string-char> string; typedef basic stringcchar16 t> u16string; typedef basic string<char32 t> u32string; typedef basic string wchar_t> wstring; 上述具体化又使用下面的具体化: char_traits<char> allocator<char> char_traits<char16_t> allocator<char_16> char_ traits<char_32> allocator<char_32> char_traits<wchar_t> allocator<wcha_t> 除char和wchar_t外,还可以通过定义traits类和使用basic_string模板来为其他一些类型创建一个string类。
模板basic_string定义了13种类型,供以后定义方法时使用
常量npos
string的数据方法
大部分术语来自STL
迭代器
begin( )、end( )
rbegin( )、rend( )
说明
还有它们对应的4个const版本:cbegin( )、cend( )、crbegin( )、crend( )
元数个数size( )、length( )
容量capacity( )
最大长度max_size( )
返回const charT*指针data( )、c_str( )
get_alloctor( )
11种字符串构造函数
默认构造函数
C-风格字符串的构造函数、部分C-风格字符串的构造函数
左值引用的构造函数、右值引用的构造函数
一个字符的n个副本的构造函数
区间构造函数
初始化列表构造函数
内存操作
risize( )、shrink_to_fit( )、clear( )、empty( )
字符串存取
[ ]、at( )
front( )、back( )
字符串搜索
find( )
rfind( )
find_first_of( )
find_last_of( )
find_first_not_of( )
find_last_not_of( )
字符串比较
compare( )
重载的关系运算符
字符串修改
追加和相加
append( )、push_back( )
重载的+、+=运算符
赋值
assign( )
重载的=运算符
插入
insert( )
清除
erase( )、pop_back( )
替换
replace( )
复制
copy( )
交换
swap( )
字符串输入和输出
输入
getline( )函数
重载的>>运算符
输出
重载的<<运算符
参数规律
操作对象:basic_string&、const charT*、charT
操作区间
使用计数
使用位置+使用计数
使用迭代器区间
C++开始的地方
C++百度百科
官网
参考手册
教程
本资源作者 袁宵
https://yuanxiaosc.github.io/
源代码
预编译
预处理过程的代码
编译
汇编代码
汇编
目标代码
连接
可执行代码
字符函数库
ctype库
字母或数字?
isalnum( )
用于字母
isalpha( )
islower( )
isupper( )
tolower( )
toupper( )
用于数字
isdigit( )
isxdigit( )
用于空白
isspace( )
用于标点
ispunct( )
用于控制符
iscntrl( )
用于打印符
isgraph( )
isprint( )