导图社区 C语言CPP语言系统总结学习笔记
C语言与CPP相关的笔记。C语言部分包括控制台、变量、存储类型、数组、字符串、表达式、程序基本结构、函数、指针、结构体、枚举、动态分配内存、链表、标准C函数库、编译过程等内容。CPP 包括面向对象、类、类型声明、构造函数、析构函数、const成员、mutable成员、静态成员、友元、单继承、多继承、虚拟继承、虚函数、纯虚函数、抽象类、操作符重载、内部类、命名空间、模板、异常、标准模板库等内容。此外还有Windows dll 链接库等内容。导图中的注释还有很多相关的详细说明与示例代码,希望能帮到大家!
编辑于2022-10-20 13:08:47介绍归纳 C Sharp 语音的基础重点知识。包括语言基础、字面常量、程序集、不安全代码、基础类、枚举、数组、泛型、字符串、正则表达式、委托与事件、文件、异常、多线程、异步、反射、网络、绘图、WinForm、Windows、跨平台调用等内容。思维导图示例中,有示例代码,方便学习与练习。
这份思维导图归纳了一些HTML基本的元素标签、布局、表单,以及 HTML5 API 如 WebSockets、Fetch API 等内容。CSS 主要是归纳了选择器。JavaScript 主要是包含了函数与箭头函数、this 关键字、Promise 异步对象。此外还有AJAX、jQuery 与 jQuery AJAX、JSONP 等内容。导图中的注释有很多相关的详细说明与示例代码,其中后端的测试代码是用的 PHP。希望能帮到大家!
WPF开发相关的笔记。WPF基本概念、XAML基本语法、控件与布局、Binding、依赖属性与附加属性、路由事件与附加事件、命令、资源、模板与样式、2D绘图与动画、3D绘图等内容。导图中的注释还有很多相关的详细说明与示例代码,希望能帮到大家!
社区模板帮助中心,点此进入>>
介绍归纳 C Sharp 语音的基础重点知识。包括语言基础、字面常量、程序集、不安全代码、基础类、枚举、数组、泛型、字符串、正则表达式、委托与事件、文件、异常、多线程、异步、反射、网络、绘图、WinForm、Windows、跨平台调用等内容。思维导图示例中,有示例代码,方便学习与练习。
这份思维导图归纳了一些HTML基本的元素标签、布局、表单,以及 HTML5 API 如 WebSockets、Fetch API 等内容。CSS 主要是归纳了选择器。JavaScript 主要是包含了函数与箭头函数、this 关键字、Promise 异步对象。此外还有AJAX、jQuery 与 jQuery AJAX、JSONP 等内容。导图中的注释有很多相关的详细说明与示例代码,其中后端的测试代码是用的 PHP。希望能帮到大家!
WPF开发相关的笔记。WPF基本概念、XAML基本语法、控件与布局、Binding、依赖属性与附加属性、路由事件与附加事件、命令、资源、模板与样式、2D绘图与动画、3D绘图等内容。导图中的注释还有很多相关的详细说明与示例代码,希望能帮到大家!
C、C++学习笔记
作者:ERIZEIUM
C部分
1. 控制台
输出
示例: int count = printf("输出 %f, %d, %d", 0.3, 66, 99);
说明
"输出 %f, %d, %d"
格式化字符串
“输出”
正常字符
“%f”
格式控制符
%[标志][宽度][.精度][类型长度]类型
以 % 开端,[] 为可选
[标志]
可组合使用: 如:printf("%0#19x\n", 123);
‘-’
有
左对齐
无
默认对齐(一般为右对齐)
'+'
有
整数、小数输出符号(+、-)
无
整数、小数只有负数会输出符号
' '
整数、小数,正数冠以空格,负数冠以负号
'#'
小数
强制带上小数点
八进制数
加前缀 0
十六进制数
加前缀 0x/0X
'0'
有
用0补齐(右对齐时有效)
无
用空格补齐
[宽度]
宽度常数可由 '*' 代替,由参数指定,例: printf("%*s", 10, s); //字符串最小输出宽度为10
最小输出宽度
也就是至少占用几个字符的位置。(包括小数点)
实际宽度<限制,则补齐
实际宽度>限制,则不限制
[.精度]
precision常数可由 '*' 代替,由参数指定,例: printf("%.*s", 20, s); //字符串最大输出长度为20
小数
小数点后的最大输出位数
(默认为6)
实际位数<限制,则用 0 补齐
实际位数>限制,则四舍五入丢弃多余
整数
最小输出位数
实际位数<限制,则用 0 补齐(左、右对齐都有效)
实际位数>限制,则不限制
字符串
最大输出长度
实际长度<限制,则不限制
实际长度>限制,则截掉多余字符(从左→右)
[类型长度]
类型
说明符
十进制整数
d
decimal;
有符号
u
unsigned;
无符号
小数
f、F
float; f:小写;F:大写(INF、NAN、IND等)。
浮点数
e、E
exponent; e:小写;E:大写。
指数形式的浮点数
g、G
general; g:小写;G:大写。
以小数/指数中较短的形式输出
根据大小自动选 f格式 或 e格式,且不输出无意义的零。
a、A
; a:小写;A:大写。
十六进制p计数法形式的浮点数
字符
c
character;
字符
s、S
string; s:字符串(字符数据类型为 char); S:宽字符串(字符数据类型为 wchar_t)。
字符串
例: printf("%s\n", "测试test"); setlocale(LC_ALL, "zh_CN"); wchar_t wtest[] = L"测试Test"; printf("%S\n", wtest);
其他进制
p
pointer; (显示为大写)
指针的值(地址的值)
x
hexadecimal; x:小写;X:大写。
无符号十六进制数
o
octal;
无符号八进制数
其他
%
输出百分号
0.3, 66, 99
参数表
从右至左依次push ←
int count
成功:返回打印字符数
失败:返回负数, 且设置与流关联的 error指示器; 若打印宽字符出错,则会设置全局变量 errno。
输入
示例: int item = scanf("%d, %d, %d", &a, &b, &c);
VS提示安全因素无法编译时,在源文件添加宏定义: #define _CRT_SECURE_NO_WARNINGS
说明
"%d, %d, %d"
格式化字符串
从左至右依次对应:a, b, c
输入需与格式化字符串形式一致
分隔符
隐式
空格 ' '、水平制表符 '\t'、换行符 '\n' 等
例: scanf("%d%f%f", &a, &b, &c); 则输入为: "12 3.4 5.6"
显式
若两个格式控制符间,有一个或多个普通字符, 则在输入数据时,在两个数据间也必须以这一个或多个字符分隔
例: scanf("a=%d,b=%f,c=%f", &a, &b, &c); 则输入应为: "a=12,b=3.4,c=5.6"
格式控制符
%[*][宽度][类型长度]匹配类型
以 % 开端,[] 为可选
[*]
丢弃读取到的匹配数据
例:scanf("%*[a-z]"); *:丢弃;
有 * 时,scanf(~)参数表可不带指针参数
[宽度]
读取匹配数据的最大宽度
例:scanf("%*3d"); 3:最多读取3个输入的字符;
[类型长度]
匹配类型
说明符
若第1个字符匹配,则会读到第1次不匹配为止; 否则立即返回,不会往后寻找。
参考printf格式控制符“类型”
高级用法
“[]”
需读取的字符集合
例:%[abcd],只读取为a、b、c、d的字符。
“-”
集合连接符
使用例:[ASCII-ASCII]
前面的ASCII数值 例: scanf("%[a-z]", str); //读取a~z的小写字符; scanf("%[0-9A-Za-z]", str); //读取所有十进制数字,字母;
“^”
取反集合
使用:加“^”于不匹配的字符(集合)前
例: scanf("%[^\n]", str); //匹配 除 换行符“\n”外的所有字符。可替代“gets(~)”; scanf("%[^0-9]", str); //匹配 除 十进制数以外的所有字符;
妙用
清空输入缓冲区
标准C方法
1、scanf("%*[^\n]"); //清空换行符前的所有字符 2、scanf("%*c"); //清空剩下的换行符
原理:缓冲区最后一个字符是"\n"(行缓冲模式)
API方法
头文件:windows.h: FlushConsoleInputBuffer(GetStdHandle(STD_INPUT_HANDLE));
&a, &b, &c
待存储的指针
int item
成功:返回读入项数
失败:返回 EOF(-1), 且设置与流关联的 error指示器 或 与流关联的 end-of-File指示器; 若解释宽字符出错,则会设置全局变量 errno。
EOF输入方法: Windows:Ctrl+Z; Linux/Unix/Mac:Ctrl+D;
2. 变量
概念
在程序中定义的变量,编译系统会为其分配相应的存储单元
程序语言中的变量是对存储单元的抽象
一等公民概念
First-Class Citizen(Object / Value / Entity)
一等公民
可以作为参数传递
可以从子程序中返回
可以赋值给变量
二等公民
例: 数组是二等公民,数组作为参数传递会退化为指针。
可以作为参数传递
不可以从子程序中返回
不可以赋值给变量
三等公民
不可以作为参数传递
不可以从子程序中返回
不可以赋值给变量
声明、定义、初始化
术语说明
声明
声明一个变量只是将变量名标识符的有关信息告诉编译器,使编译器“认识”该标识符,但声明不一定引起存储空间分配。
在C语言中,大多数情况下变量声明也就是变量定义,声明变量的同时也就完成了变量的定义。当然,还有例外,比如extern变量就只进行声明。
定义
定义变量意味着给变量分配存储空间,用于存放对应类型的数据,变量名就是对相应存储单元的命名。
初始化
定义变量后,其存储空间的值是随机的,所以需要对这个存储空间进行初始化,指定一个数值。
赋值
销毁一个变量原来的值,并赋予一个新值。
定义仅可出现一次,声明可出现多次
形式
存储类型 类型修饰符 数据类型 variable_list;
定义性声明,会分配存储空间
例:
声明并定义
int a, b, c;
声明并定义、初始化
使用“初始值”初始化
int a = 1;
若按照C++的概念,该形式的初始化属于“复制初始化”。
说明
int
变量类型
a
变量名
组成
由 字母、下划线、数字 组合
仅字母、下划线可做开头
命名长度符合“min-length && max-information”原则
命名方法
驼峰命名法
第1个单词首字母小写,后续单词首字母大写
例:int elemNum;
帕斯卡(Pascal)命名法
单词首字母大写。也叫大驼峰命名法
例:int ElemNum;
匈牙利命名法
说明
在变量名前面加上相应的小写字母的符号标识作为前缀,标识出变量的作用域,类型等
前缀之后的单词采用帕斯卡(Pascal)命名法
基本组成:属性+类型+对象描述
例:int iElemNum;
下划线命名法
全小写,下划线分隔单词
例:int elem_num;
=
赋值
1
初始值(常量表达式)
使用“初始化器列表”初始化
int a = {1};
若按照C++的概念,该形式的初始化属于“复制初始化”。
说明
int
变量类型
a
变量名
=
赋值
{1}
初始化器列表
其中的初始值“1”会赋值给变量“a”
引用性声明,不分配存储空间,所以声明时不能初始化
例:
extern int a;
extern若包含初始化就由声明变成定义了,这样就抵消了extern关键字的作用
当试图初始化extern修饰的局部变量时,编译器会报错
数据类型分类
基本类型
整数(算术类型)
signed/unsigned
char
bool
C++中
short
wchar_t
typedef unsigned short wchar_t;
int
long
long long
浮点数(算术类型)
float
double
void类型
无类型
也称为“空类型”。字长0字节,无值、空值。
派生类型
构造类型(聚合类型)
枚举类型(算术类型)
数组类型
结构体类型
联合体类型
指针类型
type* ptr;
引用类型
type& refer; C++中。
函数(返回值)类型
获取变量内存地址
使用 '&' 操作符 取地址
获取变量大小
使用 sizeof 操作符
类型转换
表示范围
double
>int>short>char
>float
高>低
转换
方向
高→低
收缩转换
数据损失/截断
低→高
扩大转换
无数据损失
方式
显式
例:(double)100
隐式
例:1+2.0,编译器先将表达式中表示范围低的操作数“1”的类型,隐式地提升至表示范围高的类型,然后再运算
3. 常量
C/C++是强类型语言,变量、常量均有类型
字面常量
整数常量
默认 signed int 类型
例:int a = 123;
123 是 int 型字面常量
浮点数常量
默认 double 类型
总是写成10进制的形式,必须有一个小数点或一个指数
例:
double a = 1.0; double b = 5.; double c = .5; double d = 1e3;
字符常量
例:char c = 'A';
单引号括起的 'A' 是 字符常量,字符常量等价于一个整数:'A' <=> 65
字符串常量
例:const char* str = "hello";
双引号括起的 "hello" 是 字符串常量
C语言中其类型为 char[](C++中类型更严谨为 const char[])
本身存储在一块只读内存区域(常量数据区)
前后缀
前
0b/0B
二进制
例:0b101、-0b101
部分编译器支持
0
八进制
例:07、-07
=> 十进制不能由数字 "0" 起始,否则会被当作八进制
0x/0X
十六进制
例:0x1a、-0x1a
L
该“L”来源于“Long”的首字母。
宽字符
“L”告诉编译器按宽字符处理
对比: char x[] = "中国"; //d6 d0 b9 fa 00,使用扩展ASCII编码(GB2312)。类型:多字节字符,Multibyte字符可能既包含SBCS(Single-Byte Character Set)单字节字符集编码模式的字符,又包含DBCS(Double-Byte Character Set)双字节字符集编码模式的字符。另外,UTF-8编码的字符也以多字节字符集编码模式表示。 wchar_t xl[] = L"中国"; //2d 4e fd 56 00 00,一律使用Unicode字符集的编码。类型:宽字符,Windows系统中,WideChar每个字符占两个字节,能容纳一个UTF-16编码的字符。
Windows系统默认为UTF-16LE
Linux系统默认为UTF-32
例:
wchar_t c = L'A';
const wchar_t* s = L"hello";
u8
C++中,C++11支持。
UTF-8字符
例:
char c = u8'A';
char8_t c = u8'A';
( char8_t 类型:c++20开始使用)
const char* s = u8"hello";
u
C++中,C++11支持。
UTF-16字符
例:
char16_t c = u'A';
const char16_t* s = u"hello";
U
C++中,C++11支持。
UTF-32字符
例:
char32_t c = U'A';
const char32_t* s = U"hello";
前缀(可选)R"分隔符(原始字符串)分隔符"
C++中,C++11支持。
原始字符串
概念
原始字符串中的字符不会进行转义操作
说明
前缀
L、u8、u、U 之一
分隔符
除了 括号、反斜杠、空格 以外的任何源字符所构成的字符序列(可为空,长度至多 16 个字符)
原始字符串
任何字符序列,但必须不含闭序列:)分隔符"
例:
const char* r = R"delimiter()")delimiter";
const char* r = R"(Hello " \n \ world)";
const wchar_t* r = LR"(Hello " \n \ world)";
const char* r = u8R"(Hello " \n \ world)";
后
u/U
unsigned
l/L
常用L
long int
long double
ll/LL
常用LL
long long
f/F
float
[u]ix
MSVC编译器扩展支持
u
unsigned
ix
i8
char
i16
short
i32
int
i64
__int64
定义常量
命名风格
全大写,推荐用下划线分隔单词
类别
宏定义
#define IDENTIFIER value
可产生常量效果,但宏定义只是进行“原文替换”,并非严格意义上的常量
const
通过const修饰而定义的常量 也只是 只读变量
其值在编译时确定,只能通过初始化指定一个值,后续不能再赋值
const type VARIABLE = value;
例
const int A = 1;
关于常量(的)指针定义分类
const int* P =&A;
与 int const* P =&A; //两者等价
P:指向常量的(变量)指针
解释
指针P指向 const int 类型常量(底层const),但指针P本身是变量
底层const:指针指向的对象是常量。
效果
解引用指针不能赋值
*P = 2; //报错
指针本身可赋值
P = &b;
=> 定义时可不初始化
int* const P = &a;
P:指向变量的(常量)指针
解释
指针P指向 int 类型变量,但指针P本身是const常量(顶层const)
顶层const:指针本身是常量。
效果
解引用指针可赋值
*P = 2;
指针本身不能赋值
P = &b; //报错
=> 定义时需初始化
const int* const P = &A;
P:指向常量的(常量)指针
解释
指针P指向 const int 类型常量(底层const),指针P本身也是const常量(顶层const)
效果
解引用指针不能赋值
指针本身不能赋值
=> 定义时需初始化
4. 存储类型
概念
在C语言中,每个变量、函数都有2个属性
存储类型(storage class)
定义了作用域、生命期、存储位置
作用域
(代码角度)
分类
全局
Global
变量
在函数 外 定义的变量
如果用局部变量能完成功能,那就不要用全局变量。
未初始化的变量会自动初始化为默认值
局部
Local
变量
在函数 内 定义的变量(包括形参)
C89规定,局部变量声明必须在任何执行语句之前,即在块的开头声明。 C99以及C++中则没有这个限制,即可在块的任何位置声明局部变量。
未初始化的变量不会自动初始化
在某层大括号内,从定义之处起生效
至该层大括号结束后失效
生命期
(运行角度)
概念
程序运行期间,对象存在的时间
分类
全局(永恒)
在内存中位于全局数据区,由应用程序管理。
变量
程序运行期间,变量始终存在且可访问
局部(临时)
在内存中位于栈区,由应用程序管理。
变量
生命期与作用域基本一致
动态(动态)
在内存中位于堆区,由内存管理器管理。
变量
在malloc时生效,free时失效
类型(type)
存储类型
说明
auto
注: 在C++中,从C++11开始,auto关键字表示让编译器从变量的初始化表达式推断出变量的类型。
系统自动为变量分配、释放内存
extern
引用性声明,告诉编译器 该变量/函数 可能在本文件或外部文件定义
例:
//===other.c=== #include <stdio.h> int a; //定义 void Print(int value) //定义 { printf("value: %d\n", value); } //====== //===main.c=== extern int a; //extern 声明 extern void Print(int value); //extern 声明 int main() { int b = a + 1; Print(b); return 0; } //======
static
静态变量
效果
只初始化1次,未初始化的变量会自动初始化为默认值
变量的生命期为整个程序运行期
分类
静态全局变量
作用域
仅在定义该变量的源代码文件内可见
生命期
为整个程序运行期
静态局部变量
作用域
仅在定义该变量的一对{}内可见
生命期
为整个程序运行期,变量可以在函数各次调用之间保持其值
静态函数
作用域
仅在定义该函数的源文件内可见
生命期
为整个程序运行期
register
提示编译器把局部变量或函数的形参尽可能放入CPU的寄存器中,以便快速访问。因此变量的字节长度不应该超过寄存器的长度。且无法用取地址符(&)获得此变量的内存地址,因为它不在内存中。
比较
类型修饰符
type specifier
const
表示(运行时)常量语义
编译器对于试图直接修改const对象的表达式会产生编译错误。
volatile
指明变量随时可能发生变化,强制编译器减少优化,且每次访问必须从内存取值
volatile表示“易变的”,即在运行期对象可能在当前程序上下文的控制流以外被修改(例如多线程中被其它线程修改;对象所在的存储器可能被多个硬件设备随机修改等情况)。
类型定义
typedef
作用
用来给已存在的数据类型定义别名,本身是一种存储类关键字
形式
typedef 数据类型 类型别名_list;
即在定义变量的形式“type variable_list;”之前加上 typedef
理解
前面有typedef,就是某类型的 别名定义
前面没有typedef,就是某类型的 变量定义
例:
基本类型
typedef int size; void measure(size* psz);
数组
typedef char Line[81]; Line text;
指针
typedef char* pstr; int mystrcmp(const pstr p1, const pstr p3);
结构体
typedef struct { int id; char name[16]; char* phone; } Ctt, *PCtt; Ctt c; PCtt p;
函数指针
typedef void (*pretfunc)(int); pretfunc pfunc;
5. 数组
一维
声明并定义
存储类型 类型修饰符 type arr[length];
type
元素类型
arr
数组名称
也是 数组在内存中的首地址
length
元素个数(数组长度)
应为常量表达式
声明并定义、初始化
指定每个元素的值
char arr[3] = {1, 2, 3};
若按照C++的概念,该形式的初始化属于“聚合初始化”。
指定部分元素的值
若初始化器列表的初始值项目个数 比 数组声明的元素个数 少, 则没有被初始化器列表中初始值覆盖的数组元素将被初始化为默认值0(这在C++中,是通过值初始化进行的)
例
char arr[3] = {1, 2}; //指定前2个,最后一个元素为0
char arr[3] = {0}; //指定前1个,效果是每个元素都为0
不指定长度
char arr[] = {1, 2, 3}; //数组的长度为初始化器列表中初始值项目的个数
指定某个元素的值
关于初始化器列表的“指定初始化项”
C99增加的特性。
语法
使用 带“[]”的数组元素下标 指定某个元素
特点
(1)若在某个指定初始化项后 还连续跟有初始值,则这些初始值将依次初始化 该指定初始化项指定元素 之后的元素
(2)若多次对同一个元素进行初始化,则最后的1次初始化有效
例:
char arr[12] = {21, 28, [4] = 31, 30, 27, [1] = 29, [9] = 12};
初始化后,各元素的值: [0] 21, [1] 29, [2] 0, [3] 0, [4] 31, [5] 30, [6] 27, [7] 0, [8] 0, [9] 12, [10] 0, [11] 0
访问
索引/下标/key/唯一标识符 方式
即 元素操作符 [] 中的值。
范围
0 ~ 数组长度-1
基索引为 0
不能越界访问
例
读
a = arr[1];
写
arr[1] = a;
取元素地址
&arr[1]
指针 方式
范围
arr ~ arr+(数组长度-1)
例
读
a = *(arr + 1);
写
*(arr + 1) = a;
取元素地址
arr + 1
多维
二维
声明并定义
存储类型 类型修饰符 type arr[size1][size2];
size1
第一维大小
size2
第二维大小
声明并定义、初始化
int arr[3][4] = { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12} };
外层“{}”:一维
内层“{}”:二维
int arr[3][4] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
嵌套“{}”是可选的,该例与上例等效。 更好地体现了二维数组元素在内存中的连续排列。
6. 字符,字符串
字符编码
字符编码
发展脉络
ASCII
中文
GB2312
GBK
GB18030
BIG5
西欧
ISO8859-1
统一码
UNICODE
计算机支持分类
非UNICODE字符集
Windows系统ANSI代码页(本地化编码)
ASCII
GB2312
UNICODE字符集
说明
ASCII
字符用[0~127]之间的数字表示
码元
单字节
编码字节数
单字节
GB2312
码元
单字节
编码字节数
单字节(ASCII字符)、双字节(非ASCII字符)
常用表示法
EUC-CN
Unicode字符集
编码方式
UTF-8
UTF-8不推荐使用BOM。
码元
单字节
编码字节数
单字节(ASCII字符)、多字节(非ASCII字符)
字节顺序
采用单字节码元,无字节顺序问题
BOM
BOM (Byte Order Mark) (字节顺序标记):用于标明码元的字节存储顺序。
EF BB BF
UTF-16
对于已在IANA注册的字符编码(这里的字符编码实际为字符编码方式CEF)UTF-16来说,则必须使用BOM。 对于已在IANA注册的字符编码(这里的字符编码实际为字符编码模式CES)UTF-16BE、UTF-16LE来说,不推荐使用BOM。因为其名称本身已决定了其字节顺序。
码元
双字节
编码字节数
双字节(BMP字符)、四字节(非BMP字符)
字节顺序
采用多字节码元,有字节顺序问题
BOM
小端
FF FE
大端
FE FF
UTF-32
对于已在IANA注册的字符编码(这里的字符编码实际为字符编码方式CEF)UTF-32来说,则必须使用BOM。 对于已在IANA注册的字符编码(这里的字符编码实际为字符编码模式CES)UTF-32BE、UTF-32LE来说,不推荐使用BOM。因为其名称本身已决定了其字节顺序。
码元
四字节
编码字节数
四字节
字节顺序
采用多字节码元,有字节顺序问题
BOM
小端
FF FE 00 00
大端
00 00 FE FF
一些术语
代码点(Code Point)
字符编码二维表中行与列相交的点
点中的值称为“代码点值”
码元(Code Unit)
字符编码方式CEF(Character Encoding Form)对码点值进行编码处理时作为一个整体来看待的最小基本单元(基本单位)
分类
单字节码元
多字节码元
双字节码元
四字节码元
数据顺序
存储方面
字节序
分类
大端序BE(Big-Endian),也称高尾端序
数据的 高 字节存储在内存的 低 地址中
数据的 低 字节存储在内存的 高 地址中
小端序LE(Little-Endian),也称为低尾端序
数据的 高 字节存储在内存的 高 地址中
数据的 低 字节存储在内存的 低 地址中
中间序ME(Middle-Endian),也称为混合序
说明
无论字节序是哪种,内存的读写永远从低地址开始
例:unsigned int value = 0x12345678;
位序
关于有效位
MSB
Most Significant Bit
最高有效位,是二进制数中对数值影响最大的位
LSB
Least Significant Bit
最低有效位,是二进制数中对数值影响最小的位
例:0b1(MSB)0000111(LSB)
分类
大端序
数据的 高 有效位存储在内存的 低 地址位中
数据的 低 有效位存储在内存的 高 地址位中
小端序
数据的 高 有效位存储在内存的 高 地址位中
数据的 低 有效位存储在内存的 低 地址位中
传输方面
字节序
分类
MSB first
指先发送高字节
LSB first
指先发送低字节
位序
分类
MSB first
指先发送高有效位
LSB first
指先发送低有效位
编码模式
多字节字符集(Multi-Byte Character Set, MBCS)
码元
单字节
编码字节数
单字节、多字节
例:
Unicode字符集的UTF-8编码方式
子级分类
单字节字符集(Single-Byte Character Set, SBCS)
码元
单字节
编码字节数
单字节
例:
ACSII
双字节字符集(Double-Byte Character Set, DBCS)
码元
单字节
编码字节数
双字节
例:
GB2312
宽字符(Wide Character)
Windows平台默认
码元
双字节
编码字节数
双字节、四字节
例:
Unicode字符集的UTF-16编码方式
Linux平台默认
码元
四字节
编码字节数
四字节
例:
Unicode字符集的UTF-32编码方式
字符
字符常量
例
char c = 'A'; // 'A' 是 字符常量,'A' <=> 65
字符数组
声明并定义、初始化
char carr[6] = {'h', 'e', 'l', 'l', 'o'};
//特殊的初始化方法 char carr[6] = {"hello"}; char carr[6] = "hello"; char carr[] = "hello";
转义字符
常见转义字符
例:printf("\a"); //即可让系统发出警告声
其他转义字符
字符串
C风格字符串特征(C-Style String)
在内存中紧密排列,以结束符 '\0' 结尾
利用结束符,可以截断字符串。
例:"hello"
以内存 首地址 代表该字符串
长度
从第1个字符开始,到末尾结束符之间的字符数。(不包含结束符 '\0')
形式
把一块存储了字符串的内存,称为一个字符串对象。
字符串常量
例
const char* str = "hello" "world"; <=> const char* str = "hello\ world"; <=> const char* str = "helloworld";
字符串常量赋给字符串指针时: C中还可以使用: char* str = "hello"; C++中只能使用 const char* str = "hello";
多行表示说明
方法1:使用 “” 连接多段文本(两个字符串之间可以被空白字符分开)
方法2:行末“\”表示接续到下一行(下行首的空白字符也属于字符串,所以若不需要多余空白字符,新行就需要顶格书写),“\”后要紧跟回车
扩展知识:在Unix系统中,“\”表示跳脱字符,使特殊字符变成一般字符(如enter,$,空格等)。
字符数组
动态字符串(字符串指针)
字符串存储在动态分配的内存上,也就是字符串对象在堆上。
例
char* str = (char*)malloc(3); str[0] = 'o'; str[1] = 'k'; str[2] = '\0';
7. 操作符,表达式
操作符
目数
单目
操作一个操作数
双目
操作两个操作数
三目
操作三个操作数
分类
算术
*、/、+、-
%(取模/取余)
取模运算:Modulus Operation 取余运算:Remainder Operation
计算 例:a % b
1、计算整数商:c = a / b;
取模
Python
商向负无穷方向舍入(floor())
取余
C、C++、C#、Java
商向原点(0)方向舍入(fix())
2、计算模/余数:r = a - c * b;
应用 例:奇偶判断
若 "numb % 2" == 0,则 numb 为偶数
赋值
=
左值
字面
L(eft)value
= 号左边的
深层
L(ocation)value
可寻址可写的值
右值
字面
R(ight)value
= 号右边的
深层
R(ead)value
不可寻址仅可读的值
例:a = 1;
赋值表达式“a = 1”的值,等于左值最终的值
复合赋值
算术操作符
a *= b 等价于 a = a * b a /= b 等价于 a = a / b a %= b 等价于 a = a % b a += b 等价于 a = a + b a -= b 等价于 a = a - b
位操作符
a <<= b 等价于 a = a << b a >>= b 等价于 a = a >> b a &= b 等价于 a = a & b a |= b 等价于 a = a | b a ^= b 等价于 a = a ^ b
串联
a = b = c = 1 等价于 (a = (b = (c = 1)))
关系
<、<=、>、>=、==、!=
表达式的值
真(1)
假(0)
逻辑
!(非)、&&(与)、||(或)
表达式
值
真(1)
假(0)
提前结算
ex1 && ex2:ex1为假时,ex2不会运算
ex1 || ex2:ex1为真时,ex2不会运算
条件
? :
表达式
test ? true : false
test为
真(非0)
表达式的值 = true部分的值
假(0)
表达式的值 = false部分的值
逗号
,
表达式
(ex1, ex2, ..., exN)
前后加 ()
从左→右依次计算
最后一个子表达式 exN 的值,为整个表达式的值
自增/自减
++、--
用于整型,每次 加/减 1
位置
后置
先参与运算,再自增/自减
例:i++、i--
前置
先自增/自减,再参与运算
例:++i、--i
位操作
更适合用于无符号整型。
分类
移位
<<
例:M << 3 M:0b00000111 ← 0b00111000
M 所有位 左移 3位
右侧空出的3位填充0
>>
例:M >> 3 M:0b10010100 → 0b00010010
M 所有位 右移 3位
左侧空出的3位填充0
位运算
~
按位取反
"1"反得"0","0"反得"1"
例:~M M:0b10100111 ↓ 0b01011000
&
按位与
都为"1"则为"1"
例:M & N M:0b10100111 N:0b11100011 ↓ 0b10100011
|
按位或
有1个为"1"就为"1"
例:M | N M:0b10100111 N:0b11100011 ↓ 0b11100111
^
“异或”也叫半加运算,与“模二加法”等同,其运算法则相当于不带进位的二进制加法:二进制下用1表示真,0表示假, 则异或的运算法则为:0⊕0=0,1⊕0=1,0⊕1=1,1⊕1=0(同为0,异为1),这些法则与加法是相同的,只是不带进位,所以异或常被认作不进位加法。 “异或”运算: 数学符号:⊕ 计算机符号:xor C语言符号:^
按位异或
两个不同则为"1"
例:M ^ N M:0b10100111 N:0b11100011 ↓ 0b01000100
应用例
M bit5清0
M & (~(1u << 5));
M bit5置1
M | (1u << 5);
判断 M bit5是否为1
(M & (1u << 5)) ? 1 : 0;
int、char 互转
int a = 0xa8b3bec9; char b[4] = { 0 }; //int => char b[0] = a; b[1] = a >> 8; b[2] = a >> 16; b[3] = a >> 24; //char => int a = 0; a = b[0] & 0xff; a += (b[1] & 0xff) << 8; a += (b[2] & 0xff) << 16; a += (b[3] & 0xff) << 24;
char 类型的数字要 &0xff 再赋值给 int 类型,其本质原因就是想保持二进制数据的一致性: 当 char(例-127)要转化为 int 的时候,高的24位必然会补1,这样,其二进制补码数据其实已经不一致了,&0xff 可以将高的24位置为0,低8位保持原样。
类型转换
(type)
显式转换
例:(double)100
类型大小
sizeof
C/C++中,sizeof是编译时确定大小的。 动态分配是运行过程中得到大小的,也就是说C/C++中malloc/new出来的内存,sizeof是无法统计的。
用法
sizeof(type) //类型大小
sizeof(var_name) //变量大小
sizeof(数组名):返回数组所有元素占用的内存空间字节数。 sizeof(指针):返回计算机系统的地址字节数,如果是32位系统返回4,16位系统返回2。
返回字节数
指针
取地址
&
上下文意义区分
表达式位置
ptr = &var;
取地址
声明的位置
type& refer = var;
声明引用类型变量
例:&var
间接寻址 (解引用)
*
上下文意义区分
表达式位置
*ptr = var;
解引用
声明的位置
type* ptr = &var;
声明指针类型变量
例:*ptr
成员
点 .
通过实例访问成员
箭头 ->
通过实例的指针访问成员
域
C++中。
::
表示作用域、从属关系
分类
全局作用域
::Name
命名空间作用域
NamespaceName::Name
类作用域
ClassName::Name
优先级
常用
[]、()、.、-> 高于 *
*、/、% 高于 +、-
! 高于 && 高于 ||
其他
同一优先级的运算符,运算次序由结合方向所决定
合理使用括号调整优先级 ()
表达式
定义
由 某操作符 连接成的表达式叫 某表达式
组成
操作数
操作符(运算符)
8. 语句,程序基本结构
语句
定义
以 ';' 结束的一行
分类
语句
空语句
只有1个 ';'
复合语句
也称“语句块”。
用"{}"括起来的语句
与1条单语句的语法地位相同
程序的3种基本结构
顺序结构
选择结构
单分支
if (表达式) 语句 注: 编写 相等判断 时,常量最好写在左边。 如“if(0 == x)”,因为常量不能做左值,所以当少写1个“=”时,可以让编译器发现错误。
双分支
if (表达式) 语句1 else 语句2
多分支
(1): if (表达式1) 语句1 else if (表达式2) 语句2 else if ... ... else //最后的else可省略。 语句N (2): switch (整型) { case 整型常量1: 语句1 break; //注:case分支语句后注意加break,否则该分支执行完后,程序将会继续执行下个case分支。 case 整型常量2: 语句2 break; case ...: ...... default: 语句 break; }
循环结构
分类
当型
(1): for ( 单次表达式; 条件表达式; 末尾循环体 ) 中间循环体 //执行顺序: 单次表达式-->: 条件表达式(真)-->中间循环体-->末尾循环体-->条件表达式; 条件表达式(假)-->退出循环; 注:循环控制变量如“i”优先从 0 开始。 条件表达式优先采用“半开半闭区间”写法,如优先写为“i < 9”而不是“i <= 9 - 1”。 (2): while(条件表达式) 循环体 //执行顺序: 条件表达式(真)-->执行循环; 条件表达式(假)-->退出循环;
直到型
do 循环体 while(条件表达式); //执行顺序: 循环体-->: 条件表达式(真)-->继续循环; 条件表达式(假)-->退出循环;
特殊控制
break
终止循环,并跳出本层循环
continue
结束本轮循环(语句),直接进入下一轮
continue后的语句,本轮将被忽略
goto
不建议使用。
概念
无条件转移语句
将控制流无条件转移到同一函数内的被标记的语句处(局部跳转)
用法
例:
void jmptest() { int i = 0; jmplabel: //语句标号,标号后加冒号。(标号命名风格与变量名的相同) i++; if (i < 5) { goto jmplabel; //转移控制流到指定标号处的语句 } }
9. 函数
概念
一个可完成指定功能的模块,能执行任务并返回结果
声明、定义
形式
(基本完整形式)声明(函数原型)
存储类型 返回值类型 调用约定 函数名称(参数列表);
使用函数原型的目的在于告诉编译器函数的返回值类型、调用约定、函数名称、参数列表(主要是其类型、数量)等。从而使编译器能检查源程序中对函数的调用形式是否正确。 函数声明可以出现多次。 例: int max(int num1, int num2); int max(int, int); //在函数声明中,参数的类型是必需的,参数名称可省略
((基本完整形式)声明并)定义
存储类型 返回值类型 调用约定 函数名称(参数列表) //函数首部 { 函数体 }
函数首部说明了函数的返回值类型、调用约定、函数名称、参数列表等。 函数定义提供了函数的实际主体(函数的实现)。函数定义只能出现1次。
引用性声明
例:
extern int __stdcall add(int a, int b);
参数列表
在函数实现中,那些在函数内部不能确定的量,就定为参数(也叫参变量,是一个变量),放在参数列表里。
组成
参数个数
参数的类型、名称
参数间用逗号 "," 分隔
形参、实参
形参:定义时的参数
形式参数(parameter): 它不是实际存在变量,所以又称虚拟变量。是在定义函数时声明使用的参数,目的是用来接收调用该函数时传入的参数。在调用函数时,实参将赋值给形参。 注:如果函数无参数,建议声明其形参为 void。如: int getnum(void) { return 9; }
实参:调用时的参数
实际参数(argument): 是在调用时传递给函数的参数。实参可以是常量、变量、表达式、函数等。无论实参是何种类型的量,在进行函数调用时,它们都必须具有确定的值, 以便把这些值传递给形参。 当参数类型不匹配时,编译器会尝试隐式转换。
输入、输出
输入参数
输入信息
参数的实际值赋值给形参
传值调用(Call by Value)
输出参数
可保存输出的信息
参数的地址赋值给形参
指针调用
(本质上是传值,传的地址值)
参数的引用赋值给形参
C++中。
引用调用(Call by Reference)
(形参是实参的别名,对形参的访问就是对实参的访问。编译后的汇编代码中仍是传地址)
参数默认值
C++中。
概念
在函数参数列表中给参数指定默认值
例:void Show(int x, int y, int z = 1);
形式
有默认值的参数须放在参数列表中的后面
函数声明与定义分开时,应在声明处指定默认值
使用
调用时指定了这个参数,则使用实际传入值
调用时未指定这个参数,则使用默认值
函数名称
命名
有意义
规范化
帕斯卡(Pascal)命名法:单词首字母大写
例:SaveData
下划线命名法:全小写,下划线分隔单词
例:save_data
调用约定
说明
函数的声明和定义处调用约定要相同
例:
int __stdcall add(int a, int b); int __stdcall add(int a, int b) { return a + b; }
一般使用时未显式指出函数的调用约定,默认使用 __cdecl 调用约定
分类
返回结果
用"return"返回,函数会立即退出
分类
return; //终止函数,但无返回值
return (val); //终止函数,并返回一个值。括号可以省略,在返回一个表达式的值时,不建议省略
调用
概念
函数的使用称为“调用”
调用前需知道函数原型
C语言代码由上到下依次执行,原则上函数定义要出现在函数调用之前,否则就会报错。但在实际开发中,经常会在函数定义之前使用它们,这个时候就需要提前声明(前向声明)。
返回值
可以忽略
例:Test(1);
可以直接使用
例:printf("%d\n", Test(1));
递归调用
一个函数 直接/间接 地调用自己
递归算法
高阶问题 降为 低阶相同问题
需设置终止条件(最低阶),以控制递归深度
可替换为 非递归算法,用循环实现
变量
变量名的重名问题
不同函数内的变量,允许重名
同一函数内,变量可以和它上一层级的变量重名
重名时优先访问本层级的变量(就近原则)
函数名重载(overload)
C++中。
概念
C++中,允许存在名字相同,参数列表不同的函数
当“函数名、参数列表”都相同时,才被认为是重复的函数。
C中,以“函数名”来唯一区分一个全局函数; C++改进,以“函数名、参数列表”进行唯一区分。
原因补充
符号管理
C、C++编译器对函数的编译处理不完全相同,C编译器编译后函数符号名主要以函数名命名。 C++编译器编译后函数符号名主要以函数名、参数类型等命名(名称重整(Name Mangling)机制),以支持函数重载。 例如函数void fun(int, int),C编译后可能是_fun,C++编译后可能是_fun_int_int。
链接指示
extern "语言"
C++为了与C兼容,在符号的管理上,有一个用来声明或定义一个C的符号的关键字: extern "C"。
C++编译器会将 extern "C" 修饰的 语句/复合语句 的代码当做C语言代码处理,也就是代码不会使用C++的名称重整机制。
例:
//=== Object.h === struct Object { int value; int which; }; //结合条件编译,实现C编译时无重整,C++编译时有重整 #ifdef __cplusplus //C++编译器会在以C++语言编译时默认定义这个宏 extern "C" { #endif void PrintObj(struct Object* obj); #ifdef __cplusplus } #endif //=== Object.c === #include <stdio.h> #include "Object.h" void PrintObj(struct Object* obj) { printf("obj value: %d\n", obj->value); printf("obj which: %d\n", obj->which); } //=== main.cpp === #include "Object.h" int main() { struct Object obj = { 123, 456 }; PrintObj(&obj); return 0; }
调用
调用重载函数(函数名相同)时
编译器根据“参数列表”匹配不同的函数
“返回值类型”不参与匹配比较
匹配
精确匹配
参数个数相同,类型相同
模糊匹配
参数个数相同,类型不同,但支持隐式转换
内联函数
C++中。
关键字
inline
例: inline int Max(int a, int b) { return a > b ? a : b; }
必须放在函数定义前添加,在函数声明前添加无用
inline是“用于实现的关键字”: 因为内联函数要在调用点展开,所以编译器必须在调用处可见内联函数的定义。这要求每个调用了内联函数的文件都出现了该内联函数的定义。以及,为了保证各调用处的函数定义一致,将内联函数的定义放在头文件里实现更合适一些。
使用原因
调用函数有基本的入栈、出栈开销,当基本开销指令>函数体指令时,显得运行效率低
作用
只是建议编译器将函数体指令直接“内联”到调用点
使用限制
只适合函数体代码简单的函数
不能包含复杂控制语句,如选择、循环等
不支持直接递归函数
以空间换时间,如果内联函数不能增强性能,就避免使用它!
类中的成员函数
“定义”在类中的成员函数若符合条件则默认设置为内联函数
如果在类中未给出成员函数定义,而又想内联该函数的话,那在类外定义时就要加上 inline,否则就认为不是内联的。 例: (1): class A { public: void Foo(int x, int y) {} // 若符合条件则默认设置为内联函数 }; (2): class A { public: void Foo(int x, int y); }; inline void A::Foo(int x, int y) {}
main函数
原型
int main(int argc, char* argv[], char* envp[]);
参数
argc:传入参数的个数
argv:每个参数字符串的字符串数组
envp:每个环境变量字符串的字符串数组
返回值
返回给该程序的调用者
int main(int argc, char* argv[]);
int main();
程序的入口
程序中第一个被执行的函数
指针函数
定义
返回值为指针的函数
例: int ret; int* add(int a, int b) { ret = a + b; return &ret; }
回调函数
详见(九、指针.函数指针)
内联汇编
MSVC 编译器
关键字
__asm
定义、分隔内联汇编语句
语法
__asm assembly-instruction[;]
#include <stdio.h> int main() { int value = 1; __asm mov eax, value __asm inc eax __asm mov value, eax __asm mov eax, value __asm inc eax __asm mov value, eax printf("%d\n", value); return 0; }
__asm { assembly-instruction-list }[;]
例: #include <stdio.h> int main() { int value = 1; __asm { mov eax, value inc eax mov value, eax } printf("%d\n", value); return 0; }
寄存器使用
有些情况下,进入内联汇编需要手动保存某些寄存器的值,退出内联汇编时再恢复
例: __asm { push eax //保存 eax //其他操作 pop eax //恢复 eax }
10. 指针
概念
地址表示
理论上可以用 unsigned int 表示,C/C++中为了突出强调它是一个内存地址, 提出了一个新的整数类型,即“指针类型”。 指针(Pointer)意思是指向一个地址(Point to an address)
声明与定义
type* ptr;
说明
type*
全称为“type型指针类型”
指向 type 型变量
即不同类型的指针,不能直接互相赋值
即不能通过隐式转换赋值。
* 的位置是自由的
int* a; //该形式可理解为 变量a 的类型为 int指针 int * a; int *a; //该形式可理解为 变量a解引用后 的类型为 int
同类型变量的混合声明、定义(不推荐)
int a, *p = &a, *p2, b;
ptr
称为“指针类型的变量”、“指针变量”、“指针”
访问指针指向地址的值
通过“*”解引用
b = *p; //读 *p = b; //写
只有指针类型才支持解引用
例:
int a =1; int* p = &a; //“&a”表示取变量a的地址
定义了一个 int型指针类型的变量 p,其值为变量a的地址。 即 指针p指向了变量a(的地址)。
空指针
定义:值为0的指针
不能执行解引用操作
空指针值
NULL
#ifndef NULL #ifdef __cplusplus #define NULL 0 //C++中使用整数类型常量 0 作为NULL的值 #else #define NULL ((void*)0) //C语言中使用无类型指针常量 (void*)0 作为NULL的值。 //C语言中任何类型的指针都可以 隐式地 转换为void*型,反过来也行, //而C++中void*型 不能反过来隐式地 转换为其他类型的指针 #endif #endif
nullptr
C++11中引入的关键字,表示空指针常量,解决C++中NULL实际为整数类型的问题
void*型指针
“无类型指针类型”
仅表示一个内存地址,不支持指针运算
与其他指针类型的转换
其他指针类型转 void*,可隐式转换
void* 转其他指针类型,需显式转换
多重指针
二重指针(两个*)
指向指针的指针(多级间接寻址)
例: int a = 1; int* p = &a; int** pp = &p; **pp = 2; //访问时也要解引用2次
int**
表示int*型变量的地址
指针(的)数组
例
const char* parts[16] = { NULL }; //与下例等价
访问:
parts[0] = "abc";
parts的类型:const char* [16]
16个元素的数组,元素都是指针
语法:([]优先级高于*)
(1)数组: parts与[16] 先结合,表示元素个数;
(2)指针: const char* 后结合,表示元素类型;
const char** parts = (const char**)malloc(sizeof(char*) * 16);
访问:
*(parts + 0) = "abc";
parts类型:const char**
const char* parts :(指针) ↓ const char** parts :(指针)的指针
数组(的)指针
例
一维
int a[3] = { 1,2,3 }; int* p = a;
p类型:int*
指向 int 类型(数组) 的指针
二维
int a[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}}; int (*p)[4] = (int(*)[4])a;
访问:
*(*(p + 0)+0) = 15; //指针 (*(p + 0))[0] = 15; //指针 p[0][0] = 15; //数组
p的类型:int[4]*
指向 int [4] 类型(数组) 的指针
语法:
(1)指针: 括号中 *与p 先结合,表示指针;
(2)类型: int与[4] 后结合,表示指针的类型;
数组做函数参数的方法
传入“数组的首地址”、“长度”
例
一维
int avg(int* p, int len) //p:数组首地址,len:长度 <==> 等价于 int avg(int p[], int len) 或 int avg(int p[3], int len) //p[3]中的数字3会被忽略
二维
int avg(int (*p)[4], int rows) //p:数组首地址,还需指定第二维长度4,以辅助说明指针p的类型 <==> 等价于 int avg(int p[][4], int rows) //[4]:指定第二维长度,rows:第一维长度
指针运算
++、--、+、-
以 元素 为单位进行运算
注:函数指针不支持运算。
例:
int i; for (i = 0; i < 10; i++) { printf(", %d" + !i + !i, i); //i为0时,!i的值为1,传入的格式化字符串为"%d";i为非0时,!i的值为0,传入的格式化字符串为", %d" } 程序输出: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
函数指针
概念
地址表示
在编译时,每一个函数都有一个首地址(入口地址)
说明
程序中的一个函数总是占用一段连续的存储区,而函数名称就代表这段存储区的首地址。
和变量地址一样,可用一个指针表示
说明
因为函数名称相当于一个指向函数入口地址的常量指针,所以函数调用其实应该通过解引用指针的形式调用,如“result = (*add)(5, 7);” 然而,这样使用显然不方便,所以“result = add(5, 7);”形式的调用大概算是编译器的一个语法糖吧(所以函数指针也支持这个语法糖)。
声明与定义
返回值类型 (调用约定 *函数指针变量名)(参数列表);
注意:声明函数指针时,调用约定要写在括号里。
返回值类型 (调用约定 *)(参数列表)
全称为““返回值类型 调用约定 (参数列表) ”型(函数)指针类型”
指向“返回值类型 调用约定 (参数列表)”型变量
语法
(1)指针: 括号中 *与函数指针变量名 先结合,表示(函数)指针;
两者需用 () 括起,以调整优先级
若不加 (),则变成声明一个指针函数。 例: int* func(int x, int y); //声明指针函数 int (*func)(int x, int y); //声明函数指针
(2)类型: 返回值类型 调用约定 (参数列表) 后结合,表示(函数)指针指向的函数的返回值类型、调用约定及参数列表
该形式的目的在于告诉编译器指针指向的函数的返回值类型、调用约定、参数列表(主要是其类型、数量)。从而使编译器能检查源程序中对函数的调用形式是否正确。 辅助理解: 把函数声明 “返回值类型 调用约定 函数名称(参数列表);” 中的函数名称看作变量。 对比函数指针声明: “返回值类型 (调用约定 *函数指针变量名)(参数列表);”。
函数指针变量名
称为“(函数)指针类型的变量”、“(函数)指针变量”、“(函数)指针”
例:
int __stdcall add(int a, int b) { return a + b; } int (__stdcall *p)(int a, int b) = NULL; //声明函数指针 p = add; //赋值方式1 p = &add; //赋值方式2 result = p(5, 7); //调用方式1 result = (*p)(5, 7); //调用方式2
定义了一个 int __stdcall (int, int) 型(函数)指针类型的变量 p(即 int (__stdcall *)(int, int) 型的变量 p),其值为函数add的首地址。 即 (函数)指针p指向了函数add的首地址。
赋给函数指针的函数的返回值类型、调用约定及参数列表 要和 函数指针表示的函数的返回值类型、调用约定及参数列表 一致
主要用途
调用函数
做函数的参数
回调机制
回调
callback
在一个函数中调用另外一个函数
回调函数
Callback Function
概念
是一个通过函数指针调用的函数
其他应用
钩子函数
说明
“钩子函数”(Hook Function) 是回调函数的一种,挂钩子的过程称为注册 。 使用时,钩子函数由系统调用,回调函数由用户调用。 钩子(Hook),是Windows消息处理机制的一个平台,应用程序可以在上面设置子程以监视指定窗口的某种消息,而且所监视的窗口可以是其他进程所创建的。当消息到达后,在目标窗口处理函数之前处理它。钩子机制允许应用程序截获处理消息或特定事件(事件本质上是对消息的封装,是IDE编程环境为了简化编程而提供的有用的工具。这个封装是在窗体过程中实现的。)。 钩子实际上是一个处理消息的程序段,通过注册,把它挂入系统。每当特定的消息发出,在没有到达目的窗口前,钩子就先捕获该消息,亦即钩子函数先得到控制权。这时钩子函数即可以加工处理(改变)该消息,也可以不作处理而继续传递该消息,还可以强制结束消息的传递。
回调分类
阻塞式回调(同步回调)
延迟式回调(异步回调)
机制实现步骤
(1)定义一个“回调函数”;
(2)将回调函数的函数指针注册给“中间函数”(调用者);
(3)“起始函数”调用中间函数,中间函数使用函数指针调用回调函数。
例
int add(int a, int b) { return a + b; } //(1) int calc(int a, int b, int (*func)(int, int)) //第3个参数为函数指针 //(2) { return func(a, b); //回调(通过函数指针,往回调用回调函数) } int main() { int result = calc(8, 9, add); //(3) return 0; }
函数指针(的)数组
例:
int (__stdcall *p[2])(int, int) = {NULL}; //声明并定义 p[0] = add; //赋值 p[1] = ⊂ //赋值 result = p[0](5, 7); //调用 result = (*p[1])(5, 7); //调用 result = (*(p + 1))(5, 7); //调用
函数指针(的)数组(的)指针
例:
int (__stdcall *(*ap)[2])(int, int) = &p; result = (*ap)[0](5, 7); //调用 result = (*(*ap)[1])(5, 7); //调用 result = (*((*ap) + 1))(5, 7); //调用
复制
“浅拷贝”
Bitwise Copy、Shallow Copy
操作
只复制了指针本身的值
结果
两个指针指向同一个对象
“深拷贝”
Memberwise Copy、Deep Copy
操作
将源对象复制一份,并将目的指针指向复制出的对象副本
结果
两个指针分别指向两个对象
安全使用规范
思路
指针指向哪儿?
指针指向的内存是否可用?
规范
杜绝野指针
野指针(wild pointer):未初始化的指针。其值为一个随机的值。 悬空指针(dangling pointer):指向已释放内存的指针。 无论是野指针还是悬空指针,都是指向无效(不安全不可控)内存区域的指针。 访问"不安全可控"(invalid)的内存区域将导致"Undefined Behavior"。
严防数组越界
目标内存是否有效
例:将指针作为函数的返回值时,若返回局部变量,在函数返回后,局部变量生命期就已经结束了
11. 结构体
类型形式
类型定义
struct 类型名{成员变量列表};
说明
定义时各成员末尾加“;”
大括号末尾要加“;”
将基本类型组合起来,形成一个新的自定义类型
类型名 称为“tag”,即“标签”,实际上是一个临时名字。 struct关键字 和 类型名 一起,构成了完整的结构体类型名。
在C++中,C++对C语言的结构体、联合体、枚举等自定义数据类型做了扩展。 C++中的结构体类型名、联合体类型名、枚举类型名本身就构成了完整的类型名,使用时可直接用于变量的声明、定义,不需要在类型名前再加上 struct、union、enum 等关键字。
命名风格
成员变量名可加前缀“m_”,表示member
例:
struct Contact { int m_id; char* m_name; char m_phone[16]; };
扩展: 借助函数指针成员,给结构体增加操作成员变量的“成员函数”,简单实现面向对象中类的概念。 例: struct Contact { int m_id; char* m_name; char phone[16]; int (*show)(struct Contact* that); //函数指针成员 };
变量定义
struct 类型名 变量 = {成员变量初始值列表};
访问成员用“.”
成员初始值中,各成员用“,”分隔
例:
声明并定义
struct Contact c;
声明并定义、初始化
指定每个成员变量的值 (初始值顺序要与成员变量顺序一致)
struct Contact c = { 1, "Abc", "12345" };
指定部分成员变量的值
与数组的初始化器列表类似,若初始化器列表的初始值项目个数 比 结构体声明的成员变量个数 少, 则没有被初始化器列表中初始值覆盖的成员变量将被初始化为默认值0
例
struct Contact c = { 1, "Abc" };
struct Contact c = { 0 };
指定某个成员变量的值
与数组一样,C99下结构体初始化也支持“指定初始化项”
语法
使用 成员操作符“.”和成员名 指定某个成员
例:
struct Contact c = { .m_name = "Abc", "12345" };
struct 类型名* 指针 = &变量;
访问成员:通过指向结构体变量的指针访问结构体变量中的成员要用“->”
特殊定义方法
直接定义变量
struct {成员变量列表} variable_list;
紧凑定义变量
struct 类型名{成员变量列表} variable_list;
定义别名
typedef struct (类型名){成员变量列表} 类型别名_list;
类型别名是完整的类型名,使用类型别名定义变量无需再加struct
例: typedef struct { int id; char* name; char* phone; } Ctt, *PCtt; //定义了一个结构体类型别名、一个结构体类型指针类型别名 Ctt c; PCtt p;
“类型名”可省略
在函数内部定义
结构体数组
例:
struct Contact c[4] = { { 1,"A","111" }, { 2,"B","222" }, { 3,"C","333" }, { 4,"D","444" } };
变量间赋值
相同类型的结构体变量间可以直接赋值
底层采用内存复制的方式实现
对于数组成员变量,可以支持赋值
对于指针成员变量,复制的内容仅为指针本身的值,指针指向的内存不会自动申请内存进行复制,即“浅拷贝”
例:
struct Contact c = { 1, "Abc", "12345" }; struct Contact d = c;
对齐与填充
CPU指令一般只能在对齐的地址上存取。 编译器会为了“对齐”考虑,在结构体的成员之间填充一些字节
例: #include <stdio.h> #include <stddef.h> struct Object { char a; int b; }; int main() { int s = sizeof(struct Object); //s == 8 s = offsetof(struct Object, a); //s == 0 s = offsetof(struct Object, b); //s == 4。成员a内存后面会填充3个字节 return 0; }
N字节对齐,即成员的内存首地址能被N整除
位域(位字段)(bit-field)
定义
成员变量后加“:”,再标明该成员变量占多少个位
先定义的占低位,后定义的占高位
位域不能跨越字节边界
例
struct Flags { unsigned char a : 3; //占低位,byte0的bit0~bit2 unsigned char b : 4; //↓,byte0的bit3~bit6 unsigned char c : 4; //↓,byte1的bit0~bit3 unsigned char d : 5; //占高位,byte2的bit0~bit4 };
12. 联合体
类型形式
类型定义
union 类型名{成员变量列表};
说明
定义时各成员末尾加“;”
大括号末尾要加“;”
对成员的访问都是相对于该联合体实例基地址的偏移量为 0 处开始
相当于“共享空间”,即同一份数据的不同视图
使用时注意数据存储顺序对成员数值的影响
union大小
就是所有成员变量里体积最大的那个成员变量大小
可被成员变量类型的大小整除
应用
分离字节
例: union SendData { unsigned int d; unsigned char c[4]; }; union SendData sd = { 0x68676665 };
分离位
例: struct Bits { unsigned char bit0 : 1; unsigned char bit1 : 1; unsigned char bit2 : 1; unsigned char bit3 : 1; unsigned char bit4 : 1; unsigned char bit5 : 1; unsigned char bit6 : 1; unsigned char bit7 : 1; }; union CharBits { unsigned char data_char; struct Bits data_bits; }; union CharBits flag = { 'A' }; flag.data_bits.bit1 = 1;
13. 枚举
类型形式
类型定义
enum 枚举类型名{枚举成员列表};
说明
定义时各成员间加“,”
大括号末尾要加“;”
枚举成员本质上是整数字面常量
若不指定,首项的值默认为0
后面各项的值依次 +1
命名风格
枚举成员一般使用帕斯卡(Pascal)命名法,或常量的命名风格
枚举成员的作用域与枚举类型本身的作用域相同
例: enum Number { Zero, One, Two = 2 };
变量定义
enum 枚举类型名 变量 = 枚举成员;
例:
声明并定义
enum Number num;
声明并定义、初始化
enum Number num = One; //赋枚举成员
enum Number num = (enum Number)1; //强制转换
特殊定义方法
直接定义变量
enum {枚举成员列表} variable_list;
紧凑定义变量
enum 枚举类型名{枚举成员列表} variable_list;
定义别名
typedef enum (枚举类型名){枚举成员列表} 类型别名_list;
“枚举类型名”可省略
使用别名定义变量无需再加enum
例: typedef enum { Red, Green, Blue } Color; Color c;
枚举类型或运算
要使用枚举类型进行按位或运算,应该用2的幂(1、2、4、8等) 来定义枚举常量, 以确保组按位运算结果与枚举中的各个标志都不重叠
例
enum Number { One = 0b0001, Two = 0b0010, Four = 0b0100, Eight = 0b1000 }; enum Number num = One | Two; //num的值为 One | Two (3)
限定作用域的枚举类型
C++11支持。
说明
枚举成员的作用域位于枚举类型本身的作用域之内
类型定义
enum class 枚举类型名{枚举成员列表};
enum struct 枚举类型名{枚举成员列表};
例:
enum class Number { Zero, One, Two }; enum struct Color { Red, Green, Blue }; int main() { //Number num = One; //报错 Number num = Number::One; //通过添加枚举类型作用域限定访问枚举成员 Color col = Color::Red; return 0; }
14. 动态分配内存
函数
头文件
stdlib.h
分类
分配
void* malloc(size_t size);
Allocate memory block。 malloc 函数至少分配 size 字节存储区域。 而块会大于 size 字节,这是由于对齐和维护信息需要空间。
返回分配的内存块的指针,用作其他类型,需要强制转换
返回 NULL 表示分配失败
void* calloc(size_t count, size_t size);
Allocate and zero-initialize(cleared) array。 在内存中动态地分配 count 个长度为 size 的连续空间,并将每一个字节都初始化为 0。
void* realloc(void* memblock, size_t newsize);
Reallocate memory block。 重新分配内存,把原分配的内存块扩展到 newsize。 如果size较小,原来申请的动态内存后面还有空余内存,系统将直接在原内存空间后面扩容,并返回原动态空间基地址; 如果size较大,原来申请的空间后面没有足够大的空间扩容,系统将重新申请一块 newsize 的内存。并把原来空间的内容复制过去,原来的空间free; 如果size非常大,系统内存申请失败,返回NULL,原来的内存不会释放; 如果扩容后的内存空间较原空间小,将会出现数据丢失; 如果直接realloc(p, 0),相当于free(p)。
释放
void free(void* memblock);
只能释放用 malloc、calloc、realloc 分配的内存
每个分配的内存块,只能释放1次
使用原则
尽可能少地申请分配内存
防止导致“内存溢出”
无法申请到足够的内存。 内存溢出(Out Of Memory,简称OOM)是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的最大内存。此时程序就运行不了,系统会提示内存溢出,有时候会自动关闭软件,重启电脑或者软件后释放掉一部分内存又可以正常运行该软件。而由系统配置、数据流、用户代码等原因而导致的内存溢出错误,即使用户重新执行任务依然无法避免。
尽可能快地释放
防止产生“内存泄漏”
无法释放已申请的内存。 内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
例
int* p = (int*)malloc(sizeof(int) * 100); free(p); p = NULL; //释放后指针及时置为空指针,以防止产生“悬空指针”
用户内存区域

15. 链表
概念
把若干个对象用“指针”串联起来,形成一个链状的数据结构
节点
组成
数据域
指针域
1个指针域存放1个指针
分类
头节点
prev指针必须设置为 NULL
分类
无头链表
所有节点都包含有效数据
链表长度为0时难以表示
有头链表
有一个固定的头结点指代整个链表
头结点不包含有效数据
头部节点称为“头节点”
后面的节点称为“数据节点”
末节点
next指针必须设置为 NULL
分类
单向链表
每个节点包含1个指针,指向当前节点的直接后继
双向链表
每个节点包含2个指针,指向当前节点的直接前驱和直接后继
循环链表
令末节点的next指针指向头节点
静态链表
借助一维数组描述链表
例: typedef struct { int data; //数据域 int cur; //游标(指针域) }component;
例
typedef struct node { dataType data; //数据域 struct node *prev, *next; //指针域 }Node, *LinkList;
为什么可以包含node*成员?因为node*是指针类型,本质是整型。 若包含node类型成员,则编译错误。
16. 文件操作
概念
作用
持久化存储数据
最终都是以二进制的数字存储
属性
文件名
路径
按表示对象分类
文件路径
目录路径
按路径位置分类
绝对路径
相对路径
符号表示
例: //以当前目录“目录3”为例: #include "D:\目录1\目录2\目录3\test.h" //绝对路径 #include "\目录1\目录2\目录3\test.h" //相对路径 #include "..\..\..\目录1\目录2\目录3\test.h" //相对路径 #include "..\..\目录2\目录3\test.h" //相对路径,以当前目录的上一级目录的上一级目录为起点 #include "..\目录3\test.h" //相对路径,以当前目录的上一级目录为起点 #include ".\test.h" //相对路径 #include "test.h" //相对路径,前面没有任何路径符号,实测 "test.h" 与 ".\test.h" 等效,相当于隐含了“.\” 
\
表示以当前盘符根目录为起点
..\
表示以当前目录的上一级目录为起点
.\
表示以当前目录为起点
长度
文件内存储了多少字节数据
内容
存储了什么
权限
只读/只写/读写
文件读写
stdio.h
库数据类型
FILE
用于标识流并包含对其进行控制所需的信息
流
标准流
分类
本质是指向 FILE结构 的指针 #define stdin (__acrt_iob_func(0)) #define stdout (__acrt_iob_func(1)) #define stderr (__acrt_iob_func(2)) _ACRTIMP_ALT FILE* __cdecl __acrt_iob_func(unsigned _Ix);
stdin
默认对应设备:键盘
标准输入流
stdout
默认对应设备:屏幕
标准输出流
stderr
默认对应设备:屏幕
标准错误流
操作的函数一般不带前缀"f",无需传入流参数
例: int puts(const char* str);
文件流
指向 FILE结构 的指针唯一地标识一个流
操作的函数一般带前缀"f"(即file),需传入流参数
例: int fputs(const char* str, FILE* stream); fwrite("12345", 1, 5, stdout); //流指定为stdout,会直接输出在Console窗口里
size_t
typedef unsigned int size_t;
以字节为单位表示大小
fpos_t
typedef long long fpos_t;
表示文件中的位置
库宏
EOF
#define EOF (-1)
BUFSIZ
#define BUFSIZ 512
FILENAME_MAX
表示文件路径的最大长度
#define FILENAME_MAX 260
FOPEN_MAX
表示可以同时打开的文件数量
#define FOPEN_MAX 20
库函数
文件访问
FILE* fopen(const char* filepath, const char* mode);
功能
打开文件
参数
filepath
文件路径
mode
文件访问模式
访问模式
翻译模式修饰符
使用方法
加在访问模式字母后面
例:fopen("aaa.txt", "wb+");
Unicode支持(Windows平台)
使用方法
打开:mode字符串(翻译模式需为"t")中,后部增加ccs键值对
例:"rt+, ccs=字符编码"
不同ccs标志(字符编码)打开不同字符编码的文件时,实际使用的字符编码(有BOM的优先使用BOM指定的字符编码):
读写:字符需以 wchar_t 类型存储
例: wchar_t bufw[1024] = { 0 }; fgetws(bufw, 1024, fp); //读取 fputws(L"写入", fp); //写入
返回值
成功:返回被打开文件的 FILE结构 的指针
失败:返回 NULL,且设置全局变量 errno
FILE* freopen(const char* filepath, const char* mode, FILE* stream);
freopen 通常用于将预先打开的流 stdin、stdout 和 stderr 重定向到用户指定的文件。 例: freopen("myfile.txt", "w", stdout); printf("This sentence is redirected to a file."); fclose(stdout);
功能
重新打开文件
同时清除与流关联的 error指示器、与流关联的 end-of-File指示器。
参数
filepath
文件路径
mode
文件访问模式
stream
目标流
是输入及输出参数,调用完成后,stream参数的值也与返回值同步。
返回值
成功:返回被重新打开文件的 FILE结构 的指针
失败:返回 NULL,目标流也会被关闭,且设置全局变量 errno
int fclose(FILE* stream);
功能
关闭流
参数
stream
被关闭的流的 FILE结构 的指针
返回值
成功:0
失败:EOF
int fflush(FILE * stream);
特例:fflush(NULL); //可以对所有打开文件的I/O缓冲区做Flush操作
功能
刷新流
立即将流的缓冲区中的内容输出到流
参数
stream
被刷新的流的 FILE结构 的指针
返回值
成功:0
指定流没有缓冲区或以只读的打开状态的情况下也返回 0
失败:EOF,且设置与流关联的 error指示器
int setvbuf(FILE* stream, char* buffer, int mode, size_t size);
这个函数应该在打开流后,在任何对该流的输入输出操作前,立即调用。
功能
设定流的缓冲区
参数
stream
目标流
buffer
用户分配的缓冲区
如果设置为 NULL,该函数会自动分配一个指定大小的缓冲区。
mode
缓冲模式
操作磁盘文件一般为 全缓冲; 涉及交互的标准输入输出一般为 行缓冲; 标准错误输出一般为 无缓冲。
值
_IOFBF
全缓冲(Full)
对于输出,数据在缓冲填满时被一次性写入
对于输入,缓冲会在请求输入且缓冲为空时被填充
_IOLBF
行缓冲(Line)
对于输出,数据在遇到换行符("\n")或者在缓冲填满时被写入
对于输入,缓冲会在请求输入且缓冲为空时被填充,直到遇到下一个换行符
_IONBF
无缓冲(No)
不使用缓冲。每个 I/O 操作都被即时写入。buffer 和 size 参数被忽略。
size
缓冲区大小(字节)
允许的范围: 2 ≤ size ≤ INT_MAX。 在内部,为 size 提供的数值向下舍入到最接近2的倍数。
返回值
成功:0
失败:非0
void setbuf(FILE* stream, char* buffer);
这个函数应该在打开流后,在任何对该流的输入输出操作前,立即调用。 此函数已弃用,请使用 setvbuf。
功能
设定流的缓冲区
参数
stream
目标流
buffer
用户分配的缓冲区
该函数假定缓冲区的大小至少有 BUFSIZ 字节,所以定义缓冲区时,其大小应 ≥ #define BUFSIZ 512 若传入 NULL ,则不使用缓冲。
直接输入输出
例:存储double double val = 123.345; fwrite(&val, 1, sizeof(val), fp); //写("wb") fread(&val, 1, sizeof(val), fp); //读("rb")
size_t fread(void* buffer, size_t size, size_t count, FILE* stream);
功能
从流中读取数据
参数
buffer
数据读取后的存储位置
size
块大小,即每次读取多少字节
推荐传入 1
count
块数量,即共读取多少次
stream
输入流的 FILE结构 的指针
返回值
返回实际读取了多少块
实测返回值存在向下取整的现象。 如块大小为3,块数量为5,实际读取3*4+1字节,只会返回4。 如果返回值与count参数不同,则在读取时可能发生读取错误或到达文件末尾。 在这两种情况下,会对应设置与流关联的 error指示器 或 与流关联的 end-of-File指示器,可以分别通过 ferror 和 feof 检查。
size_t fwrite(const void* buffer, size_t size, size_t count, FILE* stream);
功能
将数据写入流
参数
buffer
要写入的数据的指针
size
块大小,即每次写入多少字节
推荐传入 1
count
块数量,即共写入多少次
stream
输出流的 FILE结构 的指针
返回值
返回实际写入了多少块
字符输入输出
int fgetc(FILE* stream);
功能
从流获取1个字符
参数
stream
目标流
返回值
成功:返回 获取的字符的编码值
失败:返回 EOF, 且根据错误设置与流关联的 error指示器、 或与流关联的 end-of-File指示器。
int fputc(int character, FILE* stream);
功能
将字符写入流
参数
character
写入的字符的编码值
stream
目标流
返回值
成功:返回 写入字符的编码值
失败:返回 EOF,且设置与流关联的 error指示器
char* fgets(char* strbuf, int num, FILE* stream);
功能
从流获取字符串。当读取了num个字符,或读取到换行符,或到达文件尾时停止(会复制换行符,会自动附加结束符 '\0')
参数
strbuf
读取的字符串的存储位置
num
要复制到strbuf中的最大字符数(包含自动附加的结束符 '\0')
stream
目标流
返回值
成功:返回 读取的字符串的存储位置
失败:返回 NULL, 且根据错误设置全局变量 errno、或与流关联的 error指示器、 或与流关联的 end-of-File指示器。
int fputs(const char* str, FILE* stream);
功能
将字符串写入流(字符串结束符 '\0' 不会输出)
参数
str
输出的字符串
stream
目标流
返回值
成功:返回非负数
失败:返回 EOF,且设置与流关联的 error指示器
int ungetc(int character, FILE* stream);
功能
将字符推入流,使它是下一个被读取到的字符。 同时清除与流关联的 end-of-File指示器
该操作不会输出到文件
参数
character
推入的字符的编码值
stream
目标流
返回值
成功:返回 推入字符的编码值
失败:返回 EOF
格式化输入输出
int fscanf(FILE* stream, const char* format[, variable-argument]);
功能
从流中读取格式化数据
参数
stream
目标流
format
格式化字符串
variable-argument
(指针类型)可变参数
返回值
成功:返回读入项数
失败:返回 EOF(-1), 且设置与流关联的 error指示器 或 与流关联的 end-of-File指示器; 若解释宽字符出错,则会设置全局变量 errno。
int fprintf(FILE* stream, const char* format[, variable-argument]);
"[]"表示可省略;
功能
将格式化的数据打印到流
参数
stream
目标流
format
格式化字符串
variable-argument
可变参数
返回值
成功:返回打印字符数
失败:返回负数, 且设置与流关联的 error指示器; 若打印宽字符出错,则会设置全局变量 errno。
文件定位
文件位置指示器
是FILE结构体中的成员
fopen打开文件时,其值为0; fread/fwrite 等读取/写入时,其值会增加相应的 字节 数
long ftell(FILE* stream);
由于long型的数值范围,对大于2.1G的文件进行操作时会出错。
功能
获取当前文件(相对于文件首的)偏移位置
参数
stream
目标流
返回值
成功:当前文件偏移位置
失败:-1L,且设置全局变量 errno
int fseek(FILE *stream, long offset, int origin);
功能
移动当前文件(相对于文件首的)偏移到指定位置
同时清除与流关联的 end-of-File指示器。丢弃 ungetc 函数对流的操作。
参数
stream
目标流
offset
偏移
origin
起始位置
值
SEEK_SET
文件首
SEEK_CUR
当前位置
SEEK_END
文件尾
返回值
成功:0
失败:非0,且设置与流关联的 error指示器
int fgetpos(FILE* stream, fpos_t* pos);
是对 ftell 函数的升级;
功能
获取当前文件(相对于文件首的)偏移位置
参数
stream
目标流
pos
存储当前文件偏移位置(输出参数)
返回值
成功:0
失败:非0,且设置全局变量 errno
int fsetpos(FILE* stream, const fpos_t* pos);
是对 fseek 函数的升级;
功能
移动当前文件(相对于文件首的)偏移到指定位置
同时清除与流关联的 end-of-File指示器。丢弃 ungetc 函数对流的操作。
参数
stream
目标流
pos
指定的文件偏移位置(输入参数)
返回值
成功:0
失败:非0,且设置全局变量 errno
void rewind(FILE* stream);
功能
移动当前文件(相对于文件首的)偏移到文件首位置(倒带)
同时清除与流关联的 error指示器、与流关联的 end-of-File指示器。丢弃 ungetc 函数对流的操作。
参数
stream
目标流
错误处理
全局变量 errno
void perror(const char* str);
print error message
功能
打印全局变量 errno 对应的错误消息(后面会跟换行符'\n')
参数
str
前置字符串
传入非空字符串
将打印(会添加冒号和空格)“前置字符串: 错误信息”
传入空字符串("" 或 NULL)
将只打印“错误信息”
与流关联的 error指示器
该指示器通常由先前对流的操作设置,可通过调用 clearerr、rewind、freopen 清除。
int ferror(FILE* stream);
file error indicator
功能
检查与流关联的 error指示器
参数
stream
目标流
返回值
无错误:0
有错误:非0
void clearerr(FILE* stream);
功能
清除与流关联的 error指示器、与流关联的 end-of-File指示器
参数
stream
目标流
与流关联的 end-of-File指示器
文件结尾指示器,该指示器通常由先前对流的操作设置,可通过调用 clearerr、rewind、fseek、fsetpos、freopen 清除。
int feof(FILE* stream);
功能
检查与流关联的 end-of-File指示器,即检查“读取时”是否到达文件结尾
调用前,需先对流进行一次读操作,才能返回实际是否到达文件结尾。
参数
stream
目标流
返回值
未到达文件结尾:0
到达文件结尾:非0
文件操作
int rename(const char* oldpath, const char* newpath);
功能
重命名文件、目录,移动文件
参数
oldpath
老路径
newpath
新路径
返回值
成功:0
失败:非0,且设置全局变量 errno
int remove(const char* filepath);
功能
删除文件
参数
filepath
文件路径
返回值
成功:0
失败:非0,且设置全局变量 errno
FILE* tmpfile(void);
功能
创建临时文件("wb+"模式),关闭后会自动删除
返回值
成功:返回被临时文件的 FILE结构 的指针
失败:返回 NULL
io.h
库函数
int _access(const char* path, int mode);
功能
检查文件访问属性
参数
path
文件或目录路径
mode
访问属性
值
返回值
包含该属性:返回 0
不包含该属性:返回 -1
目录操作
说明
关于“当前工作目录”
工作目录可以在程序运行时改变,所以与程序启动目录(即exe启动时所在目录)可能并不相同。 在程序中,相对路径“..\”、“.\”所相对的目录就是当前的工作目录。 要准确获取程序启动目录,有2种可行的方法: (1)通过main函数的参数间接获取: 当使用的main函数原型为“int main(int argc, char* argv[])”或“int main(int argc, char* argv[], char* envp[])”时,参数argv的第1个元素就是程序的启动路径; 例: #include <string.h> char exedir[260] = {0}; strcpy(exedir, argv[0]); strrchr(exedir, '\\')[0] = '\0'; //通过最后一个“\”的位置截断字符串,删除尾随文件名和反斜杠获取目录路径 (2)使用Windows API函数间接获取: DWORD WINAPI GetModuleFileName(HMODULE hModule, LPTSTR lpFilename, DWORD nSize); 参数hModule为正在请求其路径的已加载模块的句柄。如果此参数为NULL,则检索当前进程的可执行文件的路径。 例: #include <windows.h> #include <Shlwapi.h> #pragma comment(lib, "ShLwApi.Lib") TCHAR filename[MAX_PATH] = {0}; GetModuleFileName(NULL, filename, MAX_PATH); PathRemoveFileSpec(filename); //从路径中删除尾随文件名和反斜杠。头文件:Shlwapi.h
direct.h
库函数
char* _getcwd(char* dirbuf, int maxlen);
功能
获取当前的工作目录
参数
dirbuf
获取的目录路径字符串的存储位置
maxlen
允许获取的 目录路径字符串的 最大长度
返回值
成功:返回 获取的目录路径字符串的存储位置
失败:返回 NULL, 且设置全局变量 errno
int _chdir(const char* dirname);
功能
改变当前的工作目录
参数
dirname
新工作目录的路径
返回值
成功:0
失败:-1, 且设置全局变量 errno
int _mkdir(const char* dirname);
功能
创建一个(即调用1次只能创建1个)新目录
参数
dirname
新目录的路径
返回值
成功:0
失败:-1, 且设置全局变量 errno
int _rmdir(const char* dirname);
功能
删除目录
目录必须为空,并且不能为当前工作目录或根目录。
参数
dirname
新目录的路径
返回值
成功:0
失败:-1, 且设置全局变量 errno
17. 标准C函数库
stdio.h
标准输入输出操作头文件。
库数据类型
FILE
用于标识流并包含对其进行控制所需的信息
size_t
以字节为单位表示大小
typedef unsigned int size_t;
fpos_t
表示文件中的位置
typedef long long fpos_t;
库宏
NULL
EOF
#define EOF (-1)
BUFSIZ
#define BUFSIZ 512
FILENAME_MAX
#define FILENAME_MAX 260
FOPEN_MAX
#define FOPEN_MAX 20
L_tmpnam
#define L_tmpnam 260
TMP_MAX
#define TMP_MAX 2147483647
_IOFBF、_IOLBF、_IONBF
SEEK_SET、SEEK_CUR、SEEK_END
库函数
int getc(FILE* stream); //从流获取1个字符 int putc(int character, FILE* stream); //将字符写入流
int getchar(void); //从标准输入流获取1个字符 int putchar(int character); //将字符写入标准输出流
char* gets(char* strbuf); //从标准输入流获取字符串。当读取到换行符,或到达文件尾时停止(不会复制换行符,会自动附加结束符 '\0') int puts(const char* str); //将字符串写入标准输出流(会自动附加换行符)
int scanf(const char* format[, variable-argument]); //从标准输入流中读取格式化数据 int printf(const char* format[, variable-argument]); //将格式化的数据打印到标准输出流 int sscanf(const char* str, const char* format[, variable-argument]); //从字符串中读取格式化数据 int sprintf(char* strbuf, const char* format[, variable-argument]); //将格式化的数据打印到字符串存储区(会自动附加结束符 '\0')
stdlib.h
标准通用工具头文件。
库数据类型
div_t
int类型商和余数结构体
typedef struct { int quot; //商 int rem; //余数 } div_t;
ldiv_t
long类型商和余数结构体
typedef struct { long quot; long rem; } ldiv_t;
库宏
RAND_MAX
rand函数返回的最大值
#define RAND_MAX 0x7fff
EXIT_SUCCESS
成功终止码
#define EXIT_SUCCESS 0
EXIT_FAILURE
故障终止码
#define EXIT_FAILURE 1
MB_CUR_MAX
多字节字符的最大字符长度
其值是当前语言环境(类别LC_CTYPE)的多字节字符中的最大字节数,可通过setlocale函数(<locale.h>)设置。 其值永远不会大于MB_LEN_MAX(<limits.h>)。
库函数
动态内存管理
分配
void* malloc(size_t size); void* calloc(size_t count, size_t size); void* realloc(void* memblock, size_t newsize);
释放
void free(void* memblock);
环境
int system(const char* command); //调用CMD执行系统命令
例:
"title 标题":设置控制台标题
"color 3f":设置控制台颜色(高位:背景,低位:前景)
0=黑色 1=蓝色 2=绿色 3=湖蓝色 4=红色 5=紫色 6=黄色 7=白色 8=灰色 9=淡蓝色 a=淡绿色 b=淡浅绿色 c=淡红色 d=淡紫色 e=淡黄色 f=亮白色
"mode con cols=100 lines=30":设置控制台大小
"cls":清屏
"pause":暂停
"date /t":显示日期
"time /t":显示时间
char* getenv(const char* varname); //获取环境变量名对应的值。未获取到则返回 NULL
void abort(void); //中止当前进程,导致程序异常终止
该函数会引发SIGABRT信号。
int atexit(void (*func)(void)); //压入 进程正常终止时 执行的函数,至少可压入32个
void exit(int termination_code); //正常终止进程。参数为库宏定义的终止码
伪随机序列生成
void srand(unsigned int seed); //设置伪随机数字生成器的起始种子值
例: srand(time(NULL));
int rand(void); //生成一个伪随机数(0 到 RAND_MAX)
排序、查找
void qsort(void* arr, size_t length, size_t size, int compar(const void* p1, const void* p2));
功能
对数组执行快速排序(默认升序,即compar返回值>0时,交换元素)
参数
arr
待排序数组指针
length
数组长度
size
元素大小
compar
元素比较函数
例:
int compareMyType(const void * p1, const void * p2) { if (*(MyType*)p1 < *(MyType*)p2) return -1; //返回值 <0,p1所指元素将排在p2所指元素 前面 if (*(MyType*)p1 == *(MyType*)p2) return 0; //返回值 =0,p1所指元素 p2所指元素 顺序不确定 if (*(MyType*)p1 > *(MyType*)p2) return 1; //返回值 >0,p1所指元素将排在p2所指元素 后面 }
int compare(const void* a, const void* b) { return (*(int*)a - *(int*)b); }
void* bsearch(const void* key, const void* arr, size_t length, size_t size, int compar(const void* pkey, const void* pelem));
功能
对升序数组执行二分查找
参数
key
待查找元素关键字
arr
数组指针
length
数组长度
size
元素大小
compar
元素比较函数
例:(同上)
返回值
成功:返回数组中匹配的元素指针。若有多个匹配则返回任意一个
失败:返回 NULL
整数计算
int abs(int n); //返回 int类型n 的绝对值
long labs(long n); //返回 long类型n 的绝对值
div_t div(int numer, int denom); //int类型除法,numer / denom(分子÷分母)
ldiv_t ldiv(long numer, long denom); //long类型除法
字符串转整数、浮点数
int atoi(const char * str); //转换字符串为int
long atol(const char * str); //转换字符串为long
double atof(const char* str); //转换字符串为double
函数会扫描参数字符串,跳过前面的空格字符,直到遇上数字或正负符号才开始做转换,而再遇到非数字或字符串结束时才结束转换,并将结果返回。
long strtol(const char* str, char** endptr, int base);
功能
转换字符串为long,并存储转换字符串结束位置的下一个字符位置
参数
str
待转换字符串
endptr
char*类型变量的指针,传入值非 NULL 时,其值由函数设置为 str 中本次转换数值字符串后的下一个字符位置
base
进制
当base值为0时默认采用10进制做转换, 但遇到如'0x'前置字符则会使用16进制做转换、 遇到'0'前置字符会使用8进制做转换。
值
0
2~36
返回值
成功:转换后的数值
失败:0L
unsigned long strtoul(const char* str, char** endptr, int base);
功能
转换字符串为unsigned long,并存储转换字符串结束位置的下一个字符位置
参数
str
待转换字符串
endptr
char*类型变量的指针,传入值非 NULL 时,其值由函数设置为 str 中本次转换数值字符串后的下一个字符位置
base
进制
当base值为0时默认采用10进制做转换, 但遇到如'0x'前置字符则会使用16进制做转换、 遇到'0'前置字符会使用8进制做转换。
值
0
2~36
返回值
成功:转换后的数值
失败:0uL
double strtod(const char* str, char** endptr);
函数会扫描参数字符串,跳过前面的空格字符,直到遇上数字或正负符号才开始做转换,而再遇到非数字或字符串结束时才结束转换,并将结果返回,同时存储转换字符串结束位置的下一个字符位置。 例: 代码: #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <string.h> int main(void) { const char* p = "111.11 -2.22 Nan nan(2) inF 0X1.BC70A3D70A3D7P+6 1.18973e+4932 ABCDEFG 1 2 3"; char* end = NULL; double f = 0; printf("Parsing '%s':\n", p); printf("p addr = %p\n", p); printf("end addr = %p\n", end); puts(""); for (f = strtod(p, &end); p != end; f = strtod(p, &end)) { printf("p addr = %p\n", p); printf("end addr = %p\n", end); printf("end - p = %d\n", (int)(end - p)); printf("'%.*s' -> ", (int)(end - p), p); p = end; if (errno != 0) { printf("%s: ", strerror(errno)); errno = 0; } printf("%lf\n\n", f); } } 输出: Parsing '111.11 -2.22 Nan nan(2) inF 0X1.BC70A3D70A3D7P+6 1.18973e+4932 ABCDEFG 1 2 3': p addr = 00DD8638 end addr = 00000000 p addr = 00DD8638 end addr = 00DD863E end - p = 6 '111.11' -> 111.110000 p addr = 00DD863E end addr = 00DD8644 end - p = 6 ' -2.22' -> -2.220000 p addr = 00DD8644 end addr = 00DD8648 end - p = 4 ' Nan' -> nan p addr = 00DD8648 end addr = 00DD864F end - p = 7 ' nan(2)' -> nan p addr = 00DD864F end addr = 00DD8653 end - p = 4 ' inF' -> inf p addr = 00DD8653 end addr = 00DD8668 end - p = 21 ' 0X1.BC70A3D70A3D7P+6' -> 111.110000 p addr = 00DD8668 end addr = 00DD8677 end - p = 15 ' 1.18973e+4932' -> Result too large: inf
功能
转换字符串为double,并存储转换字符串结束位置的下一个字符位置
参数
str
待转换字符串
endptr
char*类型变量的指针,传入值非 NULL 时,其值由函数设置为 str 中本次转换数值字符串后的下一个字符位置
返回值
成功:转换后的数值
失败:0.0
多字节字符
int mblen(const char* mbc, size_t checksize);
功能
根据locale的设置确定多字节字符的长度(Get length of multibyte character)
例: int clen = 0; setlocale(LC_CTYPE, "Chinese_China.936"); clen = mblen("语", MB_CUR_MAX); //clen == 2
参数
mbc
值
指向多字节字符的第一个字节的指针
NULL
传入空指针时,函数会将其内部移位状态重置为初始值
建议使用该函数前先调用 mblen(NULL, 0); 进行复位。
checksize
要检查的字符长度的最大字节数
可传入 MB_CUR_MAX 。
返回值
mbc非空指针时
成功:多字节字符的字节大小
当 mbc 指向的字符构成有效的多字节字符(其中,指向结束符时返回0)
失败:-1
当 mbc 指向的字符不构成有效的多字节字符时
mbc为空指针时
非0
多字节字符编码与内部移位状态有关
0
多字节字符编码与内部移位状态无关
int mbtowc(wchar_t* dstwc, const char* srcmbc, size_t checksize);
功能
根据locale的设置将多字节(字符)序列转换为宽字符(Convert multibyte sequence to wide character)
例: wchar_t dest = 0x0; int clen = 0; setlocale(LC_CTYPE, "Chinese_China.936"); clen = mbtowc(&dest, "言", MB_CUR_MAX); //clen == 2
参数
dstwc
转换后的宽字符存储位置
srcmbc
值
指向多字节字符的第一个字节的指针
NULL
传入空指针时,函数会将其内部移位状态重置为初始值
建议使用该函数前先调用 mbtowc(NULL, NULL, 0); 进行复位。
checksize
要检查的字符长度的最大字节数
可传入 MB_CUR_MAX 。
返回值
srcmbc非空指针时
成功:多字节字符的字节大小
当 srcmbc 指向的字符构成有效的多字节字符(其中,指向结束符时返回0)
失败:-1
当 srcmbc 指向的字符不构成有效的多字节字符时
srcmbc为空指针时
非0
多字节字符编码与内部移位状态有关
0
多字节字符编码与内部移位状态无关
int wctomb(char* dstmbc, wchar_t srcwc);
功能
根据locale的设置将宽字符转换为多字节(字符)序列
例: char* dest = NULL; int clen = 0; setlocale(LC_CTYPE, "Chinese_China.936"); dest = (char*)calloc(MB_CUR_MAX, sizeof(char)); clen = wctomb(dest, L'编'); //clen == 2
参数
dstmbc
值
转换后的多字节(字符)序列存储位置
NULL
传入空指针时,函数会将其内部移位状态重置为初始值
srcwc
宽字符
返回值
dstmbc非空指针时
成功:写入 dstmbc 的多字节字符的字节大小
失败:-1
当 srcmbc 指向的字符不构成有效的多字节字符时
dstmbc为空指针时
非0
多字节字符编码与内部移位状态有关
0
多字节字符编码与内部移位状态无关
多字节字符串
size_t mbstowcs(wchar_t* dstwcs, const char* srcmbs, size_t convmax);
功能
根据locale的设置将多字节字符串转换为宽字符串(结束符也会转换)
例: const char* src = "C语言"; wchar_t dst[3 + 1] = { 0x0 }; size_t count = 0; setlocale(LC_CTYPE, "Chinese_China.936"); count = mbstowcs(dst, src, 3 + 1); //count == 3
参数
dstwcs
转换后的宽字符串存储位置
srcmbs
多字节字符串
convmax
转换宽字符的最大个数(即转换后存储位置的空间)
返回值
成功:转换出的宽字符个数(不包括结束符)
失败:0xffffffff (即:-1)
size_t wcstombs(char* dstmbs, const wchar_t* srcwcs, size_t convmax);
功能
根据locale的设置将宽字符串转换为多字节字符串(结束符也可会转换)
例: const wchar_t* src = L"C语言"; char* dst = NULL; size_t count = 0; setlocale(LC_CTYPE, "Chinese_China.936"); dst = (char*)calloc((MB_CUR_MAX * (3 + 1)), sizeof(char)); count = wcstombs(dst, src, MB_CUR_MAX * (3 + 1)); //count == 5
参数
dstmbs
转换后的多字节字符串存储位置
srcwcs
宽字符串
convmax
转换多字节字符的最大个数(即转换后存储位置的空间)
返回值
成功:转换出的多字节字符个数(不包括结束符)
失败:0xffffffff (即:-1)
locale.h
本地化头文件。
库数据类型
struct lconv
包含了与数字和货币有关的区域设置信息的结构体
库函数
struct lconv* localeconv(void); //获取与数字和货币有关的区域设置信息
例: struct lconv* lc = NULL; setlocale(LC_MONETARY, ""); lc = localeconv();
char* setlocale(int category, const char* locale);
C语言是世界性的编程语言,它支持全球的语言系统,可以处理英文、中文、日文、韩文、德语、法语、拉丁文、希腊文等。可以通过 setlocale(~) 函数进行地域设置,改变程序的语言环境。 地域设置是与某个地区(或者说某个国家)的语言和文化相关的一系列内容,包含字符集(字符编码)、日期格式、数字格式、货币格式(例如货币符号、国际货币代码)、字符处理(例如字符分类)、字符比较(字符排序)等。
功能
区域设置(本地设置、地域设置)
在程序执行时设置。
参数
category
受语言环境影响的类别
值
#define LC_ALL 0
影响所有内容
#define LC_COLLATE 1
影响字符比较(字符排序)
#define LC_CTYPE 2
影响字符分类和字符转换
影响某些多字节字符和宽字符处理函数
#define LC_MONETARY 3
影响货币信息,包括货币符号、国际货币代码等
#define LC_NUMERIC 4
影响数字格式,包括小数点(用哪个字符来表示小数点)、数字分组等
#define LC_TIME 5
影响日期时间的格式
locale
区域设置说明
值
基础格式
"C"
默认的地域设置
C语言程序启动时就使用"C"地域设置,也就是相当于调用 setlocale(LC_ALL,"C") 。
""
使用当前操作系统默认的地域设置
如果操作系统是英文版的,那就使用英文环境,如果操作系统是中文版的,那就使用中文环境,这样做提高了C程序的兼容性,可以根据操作系统的版本自动地选择语言。
NULL
不指定任何名称
setlocale 不会对地域设置进行任何修改,仅返回当前地域设置的名称。
扩展格式
不同平台及编译器版本对扩展格式的支持程度存在一些区别。
Windows系统
"语言缩写-区域缩写"
简易格式的 IETF 标准化字符串。
例:
语言限制匹配规范
"zh-Hans":简体中文
"zh-Hant":繁体中文
区域限制匹配规范
"zh-CN":简体中文(中国大陆)
"en-US":英语(美国)
可用清单
语言-区域
"语言[_区域[.代码页]]"
例:"Chinese (Simplified)_China.936"
可用清单
代码页
".代码页"
例:
".936" //Chinese Simplified (GB2312)
".65001" //Unicode (UTF-8)
Linux系统
"语言缩写_区域缩写[.字符集]"
例:"zh_CN.UTF-8"
Linux系统可使用命令“locale -a”查看所有支持的locale。
返回值
成功:当前地域设置的名称
失败:NULL
errno.h
错误码头文件。
库宏
errno
#define errno (*_errno())
最后一次错误代码(该宏是int类型的可修改左值)
初始值为0,表示无错误
使用
通过 void perror(const char* str); 打印错误消息(stdio.h)
通过 char* strerror(int errnum); 获取错误码的错误消息(string.h)
assert.h
诊断功能头文件。 常用于防御式编程。 断言(Assertions):编写代码时,我们总是会做出一些假设,断言就是用于在代码中捕捉这些假设,可以将断言看作是异常处理的一种高级形式。断言表示为一些布尔表达式,程序员相信在程序中的某个特定点该表达式值为真。 一个断言通常是一个例程(routines)或者一个宏(marcos)。 每个断言通常含有两个参数:一个布尔表示式(a boolean expression)和一个消息(a message)。
库函数
void assert(int expression);
这是一个宏定义函数,在调试结束后,可以通过在包含 #include 的语句之前定义 #define NDEBUG 来禁用 assert 调用,示例代码如下: #define NDEBUG #include <assert.h> 该宏旨在捕获编程时的错误,而不是用户或运行时的错误,因此通常在程序退出调试阶段后将其禁用。 程序一般分为Debug 版本和Release 版本,Debug 版本用于内部调试,Release 版本发行给用户使用。assert 函数只在 Debug 版本中才有效,如果编译为 Release 版本则被忽略。
功能
评估断言,当其参数表达式的值等于0时(即表达式的值为false),会将一条消息写入 stderr 并调用 abort 函数,从而终止程序执行
消息格式:
Assertion failed: expression, file filename, line line number
例:
void print_number(int* myInt) { assert(myInt != NULL); //assert和后面的语句间最好空一行 printf("%d\n", *myInt); }
参数
expression
表达式
stdarg.h
可变参数处理头文件。 stdarg名称是由standard(标准) arguments(参数)简化而来,主要目的为让函数能够接收可变参数。 可变参数函数(Variadic functions)是stdarg.h内容典型的应用。
预备知识
可变参数函数(又称 参数个数可变函数)
声明
type var_arg_func(type fixedarg1, type fixedarg2, ...);
例: int printf(const char* format, ...);
参数列表 分为两部分
固定参数
C语言中至少要有1个
可变参数
数目、类型不定,声明时用"..."占位表示
库数据类型
va_list
可变参数列表
typedef char* va_list;
不同平台定义可能不同。
库宏
宏函数
va_start
调用顺序:需最先调用。
初始化可变参数列表,设置ap指向该函数的第1个可变参数
void va_start(va_list ap, paramN)
参数
ap
传入 需初始化的 va_list变量
paramN
传入 可变参数的前一个参数,即最后一个固定参数
以固定参数的地址为起点,依次确定各可变参数的内存起始地址。 该参数不能为 声明为register存储类、或函数、或数组、或引用(C++) 及其他不兼容的 类型。
va_arg
检索下一个可变参数,检索后会设置ap指向再下一个可变参数
可变参数的类型和个数需由程序代码控制,它并不能智能地识别不同参数的个数和类型。
type va_arg(va_list ap, type)
参数
ap
传入 需检索的 va_list变量
type
传入 需检索的可变参数的类型
返回值
返回当前检索的可变参数的值
va_end
调用顺序:最后必须调用,以允许使用了 va_start 宏的带有可变参数的函数返回。 例: int vaf(int a, ...) //假设约定传入2个可变参数,分别为 int 和 char* 类型 { va_list ap; va_start(ap, a); printf("第1个可变参数:%d\n", va_arg(ap, int)); printf("第2个可变参数:%s\n", va_arg(ap, char*)); va_end(ap); return a; } int main() { vaf(1, 2, "字符串"); return 0; }
结束使用可变参数列表,一般会设置ap指向 (va_list)0
void va_end(va_list ap)
参数
ap
传入 需结束使用的 va_list变量
signal.h
信号处理头文件。 信号用于进程间通信。
库数据类型
sig_atomic_t
可以作为原子实体访问的整数类型
为保证其读写是原子操作,其在不同平台的C语言库中取不同的类型
例: typedef int sig_atomic_t;
库宏
不同平台间存在不同的定义。
信号值
整型
SIGABRT
(异常终止信号)异常终止,例如由abort函数启动的异常终止。
SIGFPE
(浮点异常信号)错误的算术运算,例如除零或导致溢出的运算(不一定是浮点运算)。
SIGILL
(非法指令信号)无效的函数映像,例如非法指令。这通常是由于代码损坏或尝试执行数据所致。
SIGINT
(中断信号)交互式注意力信号。通常由用户生成。
SIGSEGV
(段错误信号)对内存的无效访问:当程序尝试在已分配的内存之外进行读写时。
SIGTERM
(终止信号)发送至当前程序的终止请求。
NSIG
Linux:_NSIG
其值为:最大已定义信号值 + 1
用于辅助判断信号是否有效(或已定义)。
(signal函数的)信号处理者(参数)
函数指针类型
SIG_DFL
默认处理:该信号由该特定信号的默认操作处理。
SIG_IGN
忽略信号:信号被忽略。
(signal函数的)特殊的指示失败的返回值
函数指针类型
SIG_ERR
signal函数发生错误时的返回值
例: if(signal(SIGTERM, signal_handler) == SIG_ERR) { }
库函数
void (*signal(int sig, void (*handler)(int)))(int);
该函数返回值是一个函数指针类型,可以这样理解: typedef void (*pretfunc)(int); pretfunc signal(int sig, void (*handler)(int)); 使用示例: #include <stdio.h> #include <signal.h> sig_atomic_t signaled = 0; void my_handler(int param) { signaled = param; } int main() { void (*prev_handler)(int) = signal(SIGINT, my_handler); //首次指定处理者 printf("prev_handler is %p.\n", prev_handler); if (prev_handler != SIG_ERR) { raise(SIGINT); } printf("signaled is %d.\n", signaled); prev_handler = signal(SIGINT, my_handler); //再次次指定处理者 printf("prev_handler is %p.\n", prev_handler); if (prev_handler != SIG_ERR) { raise(SIGINT); } printf("signaled is %d.\n", signaled); prev_handler = signal(SIGINT, my_handler); //连续指定处理者,不调用 raise 函数 printf("prev_handler is %p.\n", prev_handler); prev_handler = signal(SIGINT, my_handler); printf("prev_handler is %p.\n", prev_handler); return 0; }
功能
指定一个信号的处理者
参数
sig
信号
handler
处理者
返回值
成功:返回“之前”处理该信号的 handler
失败:返回 SIG_ERR
int raise(int sig);
功能
产生信号,将信号发送到当前程序
实测: Windows下成功调用该函数之后,该信号的处理者会被恢复为 SIG_DFL 。 而Linux下不会恢复。
参数
sig
信号值
返回值
成功:0
失败:非0
setjmp.h
非局部跳转头文件。 提供了一种避免通常的函数调用和返回顺序的途径,特别的,它允许立即从一个多层嵌套的函数调用中返回。 注:C++中不宜使用该头文件声明的函数,setjmp和longjmp并不能很好地支持C++中面向对象的语义。 非局部跳转的实现机制: C语言的运行控制模型,是一个基于栈结构的指令执行序列,表现出来就是call/return: call调用一个函数,然后return从一个函数返回。在这种运行控制模型中,每个函数调用都会对应着一个栈帧,其中保存了这个函数的参数、返回值地址、局部变量以及控制信息等内容。当调用一个函数时,系统会创建一个对应的栈帧压入栈中,而从一个函数返回时,则系统会将该函数对应的栈帧从栈顶退出。正常的函数跳转就是这样从栈顶一个一个栈帧逐级地返回。 另外,系统内部有一些寄存器记录着当前系统的状态信息,其中包括当前栈顶位置、位于栈顶的栈帧位置以及其他一些系统信息(例如代码段,数据段等等)。这些寄存器指示了当前程序运行点的系统状态,可以称为程序点。在宏函数setjmp中就是将这些系统寄存器的内容保存到jmp_buf类型变量env中,然后在函数longjmp中将函数setjmp保存在变量env中的系统状态信息恢复,此时系统寄存器中指示的栈顶的栈帧就是调用宏函数setjmp时的栈顶的栈帧。于是,相当控制流跳过了中间的若干个函数调用对应的栈帧,到达setjmp所在那个函数的栈帧。这就是非局部跳转的实现机制,其不同于上面所说的call/return跳转机制。 正是因为这种实现机制,“包含setjmp宏调用的函数一定不能终止”。如果该函数终止的话,该函数对应的栈帧也已经从系统栈中退出,于是setjmp宏调用保存在env中的内容在longjmp函数恢复时,就不再是setjmp宏调用所在程序点。此时,调用函数longjmp就会出现不可预测的错误。
库数据类型
jmp_buf
保存环境状态信息
包括
程序位置、栈和框架指针
其它重要的寄存器和内存数据
库宏
宏函数
setjmp
int setjmp(jmp_buf env);
功能
保存当前环境状态到 env变量(设置“jump点”),供 longjmp 函数恢复环境时使用
参数
env
用于保存“jump点”环境状态信息
返回值
0
直接调用 setjmp 时
非0
由 longjmp 调用 setjmp 时。(若longjmp的val参数为非0,则返回val的值)
库函数
void longjmp(jmp_buf env, int val);
例: jmp_buf jump; double divide(double n, double d) { if (0 == d) { longjmp(jump, 1); //控制流转移到“jump点” } return n / d; } int main() { if (setjmp(jump) == 0) //设置“jump点” { divide(1, 0); puts("操作完成"); //正常 } else { puts("除数为0"); //异常 } return 0; }
功能
恢复当前环境到 env变量 所指示的状态,并将控制流转移到“jump点”
参数
env
用于恢复“jump点”环境状态信息
val
传给 setjmp 的返回值
若为0,则 setjmp 会返回1
若为非0,则 setjmp 会返回val的值
string.h
字符串头文件。 定义了一些数据类型、宏和各种操作字符数组的函数。
库函数
字符串
复制
char* strcpy(char* dest, const char* src); //复制字符串
char* strncpy(char* dest, const char* src, size_t num); //复制字符串,参数 num 指定要复制的字符数
连接
char* strcat(char* dest, const char* src); //连接字符串
将源字符串的副本附加到目标字符串末尾。 目标字符串中的终止符将被源字符串的第1个字符覆盖,并且在目标中的新字符串的末尾添加一个结束符。
char* strncat(char* dest, const char* src, size_t num); //连接字符串,参数 num 指定将源的前num个字符附加到目标
目标字符串中的终止符将被源字符串的第1个字符覆盖,并且在目标中的新字符串的末尾添加一个结束符。
比较
int strcmp(const char* str1, const char* str2);
功能
比较字符串
逐个字符按二进制值比较。
参数
str1
字符串1
str2
字符串2
返回值
<0
两个字符串中不相等的第1个字符的值:str1的 < str2的
0
两个内存块的内容相等
>0
两个内存块中不相等的第1个字符的值:str1的 > str2的
int strncmp(const char* str1, const char* str2, size_t num);
功能
比较字符串
逐个字符按二进制值比较。
参数
str1
字符串1
str2
字符串2
num
比较前num个字符
返回值
<0
两个字符串中不相等的第1个字符的值:str1的 < str2的
0
两个内存块的内容相等
>0
两个内存块中不相等的第1个字符的值:str1的 > str2的
int strcoll(const char* str1, const char* str2);
功能
根据locale(LC_COLLATE类别)的设置,比较字符串
逐个字符比较。
参数
str1
字符串1
str2
字符串2
返回值
<0
两个字符串中不相等的第1个字符的值:str1的 < str2的
0
两个内存块的内容相等
>0
两个内存块中不相等的第1个字符的值:str1的 > str2的
size_t strxfrm(char* dest, const char* src, size_t num);
功能
根据locale(LC_COLLATE类别)的设置,转换字符串
参数
dest
目标字符串
src
源字符串
num
转换前num个字符
返回值
转换后字符串的长度(不包括结束符)
查找
char* strchr(const char* str, int c);
功能
在字符串中查找字符
内部使用 char 类型的值进行查找。
参数
str
字符串
c
查找的字符
返回值
成功:字符串中第1个匹配的位置
失败:NULL
char* strrchr(const char* str, int c);
功能
在字符串中查找字符最后出现的位置
内部使用 char 类型的值进行查找。
参数
str
字符串
c
查找的字符
返回值
成功:字符串中最后出现的位置
失败:NULL
char* strstr(const char* str, const char* sub_str);
功能
在字符串中查找子字符串
参数
str
字符串
sub_str
子字符串
返回值
成功:字符串中第1个匹配的位置
失败:NULL
char* strpbrk(const char* str, const char* charset_str);
功能
在字符串中查找字符集任意字符最先出现的位置
参数
str
字符串
charset_str
字符集
返回值
成功:在字符串中,字符集任意字符最先出现的位置
失败:NULL
size_t strspn(const char * str, const char * charset_str);
功能
从字符串第1个字符开始,查找其连续包含字符集中任意字符的个数范围
参数
str
字符串
charset_str
字符集
返回值
从字符串起始连续包含字符集中任意字符的个数
size_t strcspn(const char* str, const char* charset_str);
功能
从字符串第1个字符开始,查找其连续有几个不属于字符集中任意字符的个数范围
参数
str
字符串
charset_str
字符集
返回值
从字符串起始连续有几个不属于字符集中任意字符的个数
char* strtok(char* str, const char* delimiters);
功能
分割字符串为一组字符串
待分割字符串中属于分隔符集合中的字符,会被设置为 '\0'。 strtok是一个线程不安全的函数,因为它使用了静态分配的空间来存储被分割的字符串位置。 例: char str[] = "- This, a sample string."; char* deli = " ,.-"; char* pch = strtok(str, deli); do { puts(pch); } while (pch = strtok(NULL, deli));
参数
str
首次调用时,str指向待分割的字符串
再次调用时,需给str传入 NULL
delimiters
分隔符集合
返回值
从str开头开始的一个个被分割的串(1次调用返回1个)
连续的分隔符会被当做一个分隔符处理。
当str中的字符查找到末尾时,返回 NULL
如果查找不到分隔符集合中的字符,则返回待分割字符串
其他
size_t strlen(const char* str); //获取C字符串的长度(不包括结束符)(字符串会被解释为单字节字符串,因此即使字符串包含多字节字符,返回值也始终等于字节数)
char* strerror(int errnum); //获取错误码的错误消息
内存
复制
void* memcpy(void* dest, const void* src, size_t count);
目的内存块与源内存块不能重叠。 执行速度比 memmove 函数快。
功能
复制内存块
参数
dest
目的内存块
src
源内存块
count
复制的字节数
返回值
目的内存块 dest
void* memmove(void* dest, const void* src, size_t count);
目的内存块与源内存块可以重叠。 当目的内存块与源内存块不重叠时,效果与 memcpy 函数一样。
功能
移动内存块
参数
dest
目的内存块
src
源内存块
count
移动的字节数
返回值
目的内存块 dest
比较
int memcmp(const void* ptr1, const void* ptr2, size_t count);
功能
比较内存块
逐个字节按 unsigned char 类型的值比较。
参数
ptr1
内存块1
ptr2
内存块2
count
比较的字节数
返回值
<0
两个内存块中不相等的第1个字节的值:ptr1的 < ptr2的
0
两个内存块的内容相等
>0
两个内存块中不相等的第1个字节的值:ptr1的 > ptr2的
查找
void* memchr(const void* ptr, int c, size_t count);
功能
在内存块中查找字符
内部使用 unsigned char 类型的值进行查找。
参数
ptr
内存块
c
查找的字符
count
要检查的字符数
返回值
成功:内存块中第1个匹配的位置
失败:NULL
填充
void* memset(void* ptr, int value, size_t count);
功能
填充内存块
内部使用 unsigned char 类型的值进行填充。
参数
ptr
内存块
value
填充的值
count
要填充的字节数
返回值
内存块 ptr
time.h
时间头文件。 定义了一些数据类型、宏和各种操作日期和时间的函数。
时间概念
UT - 世界时
Universal Time
说明
世界时是最早的时间标准。 在1884年,国际上将1s确定为全年内每日平均长度的1/8.64×104。以此标准形成的时间系统,称为世界时,即 UT1。 1972年国际上开始使用国际原子时标,从那以后,经过格林威治老天文台本初子午线的时间便被称为世界时,即UT2,或称格林威治时间(GMT),是对地球转速周期性差异进行校正后的世界时。
TAI - 国际原子时
International Atomic Time
说明
1967年第13届国际度量衡会议上通过一项决议,定义 1s 为铯-133原子基态两个超精细能级间跃迁辐射9,192,631,770周所持续的时间,这是利用铯原子振荡周期极为规律的特性。 现在用的时间就是1971年10月定义的国际原子时,是通过世界上大约200多台原子钟进行对比后,再由国际度量衡局时间所进行数据处理,得出的统一的原子时。
GMT - 格林尼治标准时间,格林威治平均时间
Greenwich Mean Time
说明
由于地球轨道并非圆形,其运行速度又随着地球与太阳的距离改变而出现变化,因此视太阳时欠缺均匀性。视太阳日的长度同时亦受到地球自转轴相对轨道面的倾斜度所影响。为着要纠正上述的不均匀性,天文学家计算地球非圆形轨迹与极轴倾斜对视太阳时的效应。 平太阳时就是指经修订后的视太阳时。在格林尼治子午线上的平太阳时称为世界时(UT0),又叫格林尼治平时(GMT)。
UTC - 协调世界时
Universal Time Coordinated
说明
UTC是国际无线电咨询委员会制定和推荐的,UTC相当于本初子午线(即经度0度)上的平均太阳时。它是经过平均太阳时(以格林威治标准时间GMT)、地轴运动修正后的新时标以及以「秒」为单位的国际原子时所综合精算而成的时间,计算过程相当严谨精密,因此若以「世界标准时间」的角度来说,UTC比GMT来得更加精准。其误差值必须保持在0.9秒以内,若大于0.9秒则由位于巴黎的国际地球自转事务中央局发布闰秒,使UTC与地球自转周期一致。所以基本上UTC的本质强调的是比GMT更为精确的世界时间标准.它其实是个更精确的GMT。
LT - 本地时间
Local Time
说明
所在时区的时间。UTC或GMT与LT如下的换算关系:LT=UTC+时区差 ; 东区时区差为正,西区时区差为负。如北京是东八区,则北京时间=UTC+8.
计算
LT=UTC+时区差
库数据类型
struct tm
时间结构体
struct tm { int tm_sec; //分钟后的秒数,包括闰秒-[0,60] int tm_min; //小时后的分钟-[0,59] int tm_hour; //自午夜以来的小时数-[0,23] int tm_mday; //每月的某天-[1,31] int tm_mon; //自一月以来的月份-[0,11] int tm_year; //年份,偏移1900 int tm_wday; //自星期日以来的天数-[0,6] int tm_yday; //自1月1日以来的天数-[0,365] int tm_isdst; //夏令时标志,实行:正数,不实行:0,未知:负数 };
time_t
时间戳
自 1970 年 1 月 1 日 00:00:00 (UTC) 时起的秒数
typedef long time_t; //32位,可以表示到 2038 年 1 月 19 日 03:14:07 (UTC) 秒 typedef long long time_t; //64位
clock_t
时钟滴答计数
typedef long clock_t;
库宏
CLOCKS_PER_SEC
每秒时钟滴答数
#define CLOCKS_PER_SEC ((clock_t)1000)
库函数
时间操作
clock_t clock(void); //获取程序消耗的处理器时间
time_t time(time_t* timeptr);
功能
获取当前时间的时间戳
参数
timeptr
获取的时间戳的存储位置
值
可以传入 存储位置
可以传入 NULL
返回值
成功:时间戳
失败:-1
double difftime(time_t end, time_t beginning); //返回时间差(秒)
时间转换
struct tm* localtime(const time_t* timeptr); //将时间戳转换为时间结构体描述的本地时间
struct tm* gmtime(const time_t* timeptr); //将时间戳转换为时间结构体描述的UTC时间
time_t mktime(struct tm* tmptr);
功能
将时间结构体描述的本地时间转换为时间戳
参数
tmptr
时间结构体的存储位置
返回值
成功:时间戳
失败:-1
char* asctime(const struct tm* tmptr); //将时间结构体转换为字符串(会自动附加换行符),返回的值指向一个静态局部数组
as a calendar time。 返回字符串格式:Www Mmm dd hh:mm:ss yyyy 该函数实现等同于: char* asctime(const struct tm* tmptr) { static const char wday_name[][4] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }; static const char mon_name[][4] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; static char result[26]; sprintf(result, "%.3s %.3s%3d %.2d:%.2d:%.2d %d\n", wday_name[tmptr->tm_wday], mon_name[tmptr->tm_mon], tmptr->tm_mday, tmptr->tm_hour, tmptr->tm_min, tmptr->tm_sec, 1900 + tmptr->tm_year); return result; }
char* ctime(const time_t* timeptr); //将时间戳转换为本地时间的字符串(会自动附加换行符),返回的值指向一个静态局部数组
calendar time。 返回字符串格式:Www Mmm dd hh:mm:ss yyyy 该函数等同于: asctime(localtime(timeptr));
size_t strftime(char* strbuf, size_t bufmaxsize, const char* format, const struct tm* tmptr);
功能
将时间结构体根据格式化字符串转换为字符串
参数
strbuf
转换后字符串的存储位置
bufmaxsize
可容纳字符的总数,包括字符串结束符
format
格式化字符串
%a 输出:星期缩写名 * 例:Thu %A 输出:星期全名 * 例:Thursday %b 输出:月份的缩写名 * 例:Aug %B 输出:月份全名 * 例:August %c 输出:日期和时间表示 * 例:Thu Aug 23 14:55:02 2001 %C 输出:年除以100,并截断为整数 (00-99) 例:20 %d 输出:月的某天,零填充 (01-31) 例:3 %D 输出:MM/DD/YY 短日期,相当于 %m/%d/%y 例:08/23/01 %e 输出:月的某天,以空格填充 ( 1-31) 例: 3 %F 输出:YYYY-MM-DD短日期,相当于 %Y-%m-%d 例:2001/8/23 %g 输出:基于周的年份,后两位数字 (00-99) 例:1 %G 输出:基于周的年份 例:2001 %h 输出:月份的缩写名 * (与 %b 相同) 例:Aug %H 输出:小时(24小时格式) (00-23) 例:14 %I 输出:小时(12小时格式) (01-12) 例:2 %j 输出:年的某天 (001-366) 例:235 %m 输出:以十进制数字表示的月份 (01-12) 例:8 %M 输出:分钟 (00-59) 例:55 %n 输出:换行符 ('\n') 例: %p 输出:AM或PM 例:PM %r 输出:12小时制时间 * 例:2:55:02 PM %R 输出:24-小时 HH:MM 时间,相当于 %H:%M 例:14:55 %S 输出:秒 (00-61) 例:2 %t 输出:水平制表符 ('\t') 例: %T 输出:ISO 8601时间格式(HH:MM:SS),相当于%H:%M:%S 例:14:55:02 %u 输出:ISO 8601以十进制数字表示的星期,星期一为1 (1-7) 例:4 %U 输出:星期数(以星期日为1周的第1天) (00-53) 例:33 %V 输出:ISO 8601 星期数 (01-53) 例:34 %w 输出:以十进制数字表示的星期,星期日为0 (0-6) 例:4 %W 输出:星期数(以星期一为1周的第1天) (00-53) 例:34 %x 输出:日期 * 例:08/23/01 %X 输出:时间 * 例:14:55:02 %y 输出:年,最后两位数字 (00-99) 例:1 %Y 输出:年 例:2001 %z 输出:与时区UTC的偏移量(1分钟= 1,1小时= 100),如果无法确定时区,则没有字符 例:100 %Z 输出:时区名称或缩写 *,如果无法确定时区,则没有字符 例:CDT %% 输出:% 符号 例:% 标有星号(*)的说明符与语言环境有关。
tmptr
时间结构体的存储位置
返回值
成功: 如果生成的C字符串的长度(包括结束符)不超过bufmaxsize,则该函数返回复制到strbuf的字符总数(不包括结束符)
失败:0
math.h
数学运算头文件。 声明了常用的一些数学运算。
库函数
double abs(double x); //返回 x 的绝对值
double sin(double x); //返回弧度角 x 的正弦 double cos(double x); //返回弧度角 x 的余弦 double tan(double x); //返回弧度角 x 的正切
度和弧度(角度和弧度) 度和弧度都是衡量角的大小的单位。度用°来表示,弧度用rad表示。 度和弧度之间可以相互转换,转换规则为: 1° = (π/180) rad ≈ 0.01745 rad 1 rad = (180/π)° ≈ 57.3°
double ceil(double x); //向上取整 double floor(double x); //向下取整
double exp(double x); //返回 e 的 x 次幂的值 double log(double x); //返回 x 的自然对数(基数为 e 的对数) double log10(double x); //返回 x 的常用对数(基数为 10 的对数) double pow(double x, double y); //返回 x 的 y 次幂 double sqrt(double x); //返回 x 的平方根
stddef.h
标准定义头文件。 定义了一些类型、标准宏。
iso646.h
ISO 646 替代运算符拼写头文件。 该头文件声明了一些宏来帮助非QWERTY键盘的国家或地区使用C语言的逻辑运算符。
limits.h
整数类型大小头文件。 用于表示整数类型的取值范围限制。
float.h
浮点类型特征头文件。 提供了浮点型的范围和精度的宏,一般用于数值分析。
ctype.h
字符分类处理头文件。 声明了一些用于检测和转换单个字符的函数。
wctype.h
宽字符分类处理头文件。 声明了一些用于检测和转换单个宽字符的函数。
wchar.h
宽字符头文件。 用于处理宽字符。
stdint.h
C++11。 整数类型头文件。 提供了各种位宽的整数类型,还定义了一些宏表示其取值范围及对其进行初始化。
inttypes.h
C++11。 整数类型头文件。 提供了各种位宽的整数类型输入输出时的转换标志宏。
stdbool.h
C++11。 布尔类型头文件。 此头文件在C中的目的是添加 bool 类型以及 true 和 false 值作为宏定义。 在直接支持这些类型的C++中,头文件仅定义一个宏,可用于检查是否定义了该类型。
fenv.h
C++11。 浮点数环境头文件。 声明了一些数据类型、宏和函数,以访问浮点环境。 浮点环境维护一系列状态标志和特定的控制模式。 关于浮点环境的具体内容取决于实现方式,但是状态标志通常包括浮点异常及其相关信息,并且控制模式至少包括舍入方向。
uchar.h
C++11。 Unicode字符头文件。 此头文件提供对16位和32位字符的支持,适合使用UTF-16和UTF-32进行编码。
tgmath.h
C++11。 通用数学运算头文件。 它包含了头文件math.h、complex.h,根据调用时函数的参数自动对应到这2个头文件中类型适合的函数。
18. 多文件项目及编译过程
多文件项目的生成
编译时的字符集
源码字符集(the source character set)
源代码文件中的用于保存字符的字符编码
执行字符集(the execution character set)
可执行文件中的用于保存字符的字符编码
编译器
MSVC 编译器
生成过程
(1)预处理
过程
处理所有文件的预处理指令,生成一个独立的编译单元
(2)生成目标文件(编译、汇编)
过程
将每个源文件(的编译单元)各自生成目标文件“*.obj”
符号检查
在该阶段,全局符号的声明仅用于协助编译器进行类型检查
分类
增量编译
只编译有改动的文件
全量编译
编译所有的文件
(3)链接
过程
将每个目标文件综合链接在一起,生成可执行文件“*.exe”、动态链接库“*.dll”、静态链接库“*.lib”等
符号检查
在该阶段,编译器会检查每个全局符号的定义
对字符的处理
源码字符集
判断
MSVC编译器根据源代码文件BOM、系统当前代码页、编译选项确定其字符编码(默认使用系统当前代码页的字符编码)
使用
编译器通过源码字符集,将源代码文件中的字符数据 转换为 编译器内部处理数据
执行字符集
判断
MSVC编译器根据系统当前代码页、编译选项确定其字符编码(默认使用系统当前代码页的字符编码)
使用
编译器通过执行字符集,将之前的编译器内部处理数据 转换为 可执行文件中的字符数据
在程序执行时,若区域设置(setlocale)的字符编码与执行字符集的一致(简体中文Windows系统默认使用GB2312字符编码),则字符可以正确显示
GCC 编译器
生成过程
(1)预处理
(2)编译
(3)汇编
(4)链接
对字符的处理
源码字符集
判断
(默认使用UTF-8字符编码)
执行字符集
判断
(默认使用UTF-8字符编码)
使用
在程序执行时,若区域设置(setlocale)的字符编码与执行字符集的一致(Linux系统默认使用UTF-8字符编码),则字符可以正确显示
预处理、编译相关
代码文件
头文件(*.h)
作用
主要用于保存程序的声明
解决符号处处使用时,需处处声明的问题
原理
在预处理阶段,预处理器将所有源文件里的 #include 行,替换展开为 引用头文件的内容
因为这个原理,所以理论上头文件中写任何C语言支持的内容都可以,包括变量定义、函数定义、main函数等等。
若头文件里还有 #include,则继续替换,直到没有 #include
使用
一般放入的内容
类型声明
变量、函数声明
嵌入包含其他头文件
防止头文件重复包含
例: //=== main.c === #include "Object.h" //1次, #include "Object.h" //2次,重复包含
#ifndef 方式
特点:
由语言支持所以移植性好。 #ifndef的方式受C/C++语言标准支持。它不仅可以保证同一个文件不会被包含多次,也能保证内容完全相同的两个文件(或者代码片段)不会被不小心同时包含。 当然,缺点就是如果不同头文件中的宏名不小心“撞车”,可能就会导致你看到头文件明明存在,但编译器却硬说找不到声明的状况——这种情况有时非常让人郁闷。 由于编译器每次都需要打开头文件才能判定是否有重复定义,因此在编译大型项目时,ifndef会使得编译时间相对较长,因此一些编译器逐渐开始支持#pragma once的方式。
例:
#ifndef OBJECT_H #define OBJECT_H struct Object { int value; int which; }; void PrintObj(struct Object* obj); #endif
#pragma once 方式
特点:
可以避免宏名冲突。 #pragma once 一般由编译器提供保证:同一个文件不会被包含多次。注意这里所说的“同一个文件”是指物理上的一个文件,而不是指内容相同的两个文件。 其无法对一个头文件中的一段代码作#pragma once声明,而只能针对文件。 其好处是,你不必再担心宏名冲突了,当然也就不会出现宏名冲突引发的奇怪问题。大型项目的编译速度也因此提高了一些。 对应的缺点就是如果某个头文件有多份拷贝,本方法不能保证它们不被重复包含。当然,相比宏名冲突引发的“找不到声明”的问题,这种重复包含很容易被发现并修正。 另外,这种方式不支持跨平台!#pragma once方式不受一些较老版本的编译器支持,一些支持了的编译器又打算去掉它,所以它的兼容性可能不够好。
例:
#pragma once struct Object { int value; int which; }; void PrintObj(struct Object* obj);
源文件(*.c、*.cpp)
主要用于保存程序的实现
例:
//=== Object.h === struct Object { int value; int which; }; void PrintObj(struct Object* obj); //=== Object.c === #include <stdio.h> #include "Object.h" void PrintObj(struct Object* obj) { printf("obj value: %d\n", obj->value); printf("obj which: %d\n", obj->which); } //=== main.c === #include "Object.h" int main() { struct Object obj = { 123, 456 }; PrintObj(&obj); return 0; }
预处理(器)指令
预处理指令不是语句,行尾不加分号。
引用
#include
作用
引用一个头文件
用法
#include <头文件>
引用系统(目录)头文件
#include "头文件"
(优先)引用用户(目录)头文件
相对路径以当前工程目录为起点
路径字符串由预处理器处理,无需使用转义字符
例:
#include "Object.h" #include "..\Project1\Test\Test.h"
宏
#define
作用
宏定义,用于定义一个宏
被定义为“宏”的标识符称为“宏名”
在预处理阶段,预处理器将代码中所有“宏名”代换为宏定义代表的“语言符号”,这称为“宏代换”或“宏展开”
语言符号
要注意括号的使用
可以是常数、表达式、格式串等
支持多行,即使用跳脱字符“\”
例: #define HWSTR "hello\ world" const char* hw = HWSTR; //HWSTR展开为"helloworld"
用法
无参宏定义
#define 宏名 语言符号
例:
#define PI 3.14 #define SUMAB (a + b) double a = PI; double b = 2; double c = SUMAB; //SUMAB展开为(a + b)
带参宏定义
注:宏名和左括弧之间不能有空格。
说明
在宏定义中的参数称为形式参数,在宏调用中的参数称为实际参数。
对带参数的宏,在调用中,不仅要宏展开,而且要用实参去代换形参。
分类
普通
#define 宏名(形参表) 语言符号
形参表
固定参数
例:
#define MAX(a, b) ((a)>(b) ? (a) : (b)) int v1 = 6; int v2 = 61; int v3 = MAX(v1, v2); //MAX(v1, v2)展开为((v1)>(v2) ? (v1) : (v2))
可变参数
数目、类型不定,声明时用“...”占位表示,语言符号中使用“__VA_ARGS__”代换
C99新增支持。
例:
#define LOG(...) printf(__VA_ARGS__) LOG("123+%s=%d\n", "3", 123 + 3); //展开为printf("123+%s=%d\n", "3", 123 + 3)
特殊
“##”符号
用法1:连接前后两个参数符号
#define 宏名(参数1, 参数2) 参数1##参数2
例:
#define CONTACT2(a, b) a##b int varOne = 123; int ret1 = CONTACT2(var, One); //CONTACT2(var, One)展开为varOne
#define 宏名(参数) 参数##子语言符号
例:
#define CONTACT1F(a) a##rs int argrs = 456; int ret2 = CONTACT1F(arg); //CONTACT1F(arg)展开为argrs
#define 宏名(参数) 子语言符号##参数
例:
#define CONTACT1R(b) fs##b int fspara = 789; int ret3 = CONTACT1R(para); //CONTACT1R(para)展开为fspara
用法2:连接前后两个token。当可变参数列表实参为空时,“##”则不进行连接,并去除“__VA_ARGS__”前多余的“,”
实测为GCC下需要的用法。
例:
#include <stdio.h> //#define LOG2(...) printf("123+s=d\n", __VA_ARGS__) //该宏定义GCC下报错,MSVC下未报错 #define LOG2(...) printf("123+s=d\n", ##__VA_ARGS__) //该宏定义GCC、MSVC下均未报错 int main() { LOG2(); //测试 return 0; }
“#”符号 将参数符号转换成 字符串类型,并加 "" 号
#define 宏名(参数) #参数
例:
#define GETSTR(a) #a const char* str = GETSTR(Hello); //GETSTR(Hello)展开为"Hello"
“#@”符号 将参数符号转换成 字符类型,并加 '' 号
#define 宏名(参数) #@参数
例:
#define GETCHAR(a) #@a char ch = GETCHAR(1); //GETCHAR(1)展开为'1'
#undef
作用
取消宏定义,用于撤销已定义的宏
用法
#undef 宏名
预定义宏名
__LINE__
当前代码行号
例: printf("行号:%d\n", __LINE__);
__FILE__
当前源文件名
例: printf("源文件:%s\n", __FILE__);
__DATE__
当前编译日期
例: printf("日期:%s\n", __DATE__);
__TIME__
当前编译时间
例: printf("时间:%s\n", __TIME__);
__FUNCTION__
当前函数名
例: printf("函数名:%s\n", __FUNCTION__);
__func__
当前函数名
例: printf("函数名:%s\n", __func__);
__VA_ARGS__
可变参数列表
__STDC__
以标准C编译时,编译器会定义这个宏
__cplusplus
以C++语言编译时,编译器会定义这个宏
条件编译
defined 操作符
作用
检测一个宏名是否已经由 #define 定义过。若已定义则返回真,若未定义则返回假
用法
defined(宏名)
例:
#define PI 3.14 #if defined(PI) //此处宏名也可以不用括号括起,如:#if defined PI puts("已定义"); //被编译 #endif
#if
作用
若表达式的值为真就对本分支代码进行编译
用法
#if 表达式
例:
#define PI 3.14 #if defined(PI) puts("已定义"); //被编译 #endif
支持关系操作
<、<=、>、>=、==、!=
例:
#if _MSC_VER >= 1600
支持逻辑操作
!(非)
例:
#define PI 3.14 #if !defined(PI) puts("TRUE"); #else puts("FALSE"); //被编译 #endif
&&(与)
例:
#define PI 3.14 #define R 10 #if defined(PI) && R > 10 puts("TRUE"); #else puts("FALSE"); //被编译 #endif
||(或)
例:
#define PI 3.14 #if defined(PI) || defined(R) puts("TRUE"); //被编译 #else puts("FALSE"); #endif
#else
作用
作为其他条件编译预处理指令的剩余选项进行编译
用法
#else
例:
#define PI 3.14 #if defined(PI) && defined(R) puts("TRUE"); #else puts("FALSE"); //被编译 #endif
#elif
作用
相当于“#else #if”组合的作用,可构成多分支选择结构
用法
#elif 表达式
例:
#define R 10 #if defined(PI) puts("PI已定义"); #elif defined(R) puts("R已定义"); //被编译 #endif
#ifdef
作用
相当于“#if defined”的简写,一次只可判断1个宏名
用法
#ifdef 宏名
例:
#define PI 3.14 #ifdef PI puts("TRUE"); //被编译 #endif
#ifndef
作用
相当于“#if !defined”的简写,一次只可判断1个宏名
用法
#ifndef 宏名
例:
#ifndef PI puts("未定义"); //被编译 #endif
#endif
作用
结束一个条件编译块
用法
#endif
例:
#define PI 3.14 #if defined(PI) puts("已定义"); //被编译 #endif
错误
#error
作用
当预处理器预处理到#error命令时,将停止编译并输出用户自定义的错误消息。
用法
#error [用户自定义的错误消息]
“[]”代表用户自定义的错误消息可以省略不写。
例:
例: #error 编译出现错误
行号
#line
作用
指定下一行的行号(即改写 __LINE__),指定源文件名(即改写 __FILE__)
用法
#line 下一行行号 [新源文件名]
“[]”代表可省略不写。省略后不会指定源文件名。
例:
例: printf("行号1=%d\n", __LINE__); printf("源文件1=%s\n", __FILE__); #line 200 "F:\\Test\\test.c" printf("行号2=%d\n", __LINE__); printf("源文件2=%s\n", __FILE__); #line 300 printf("行号3=%d\n", __LINE__); printf("源文件3=%s\n", __FILE__); 输出: 行号1=13 源文件1=D:\Project\Test\Project1\Project1\main.c 行号2=200 源文件2=F:\Test\test.c 行号3=300 源文件3=F:\Test\test.c
编译(器)指令
编译指令不是语句,行尾不加分号。
#pragma
作用
为编译器提供特定的编译指示
#pragma在不同的编译器间是不可移植的。
用法
#pragma 参数
常用“参数”分类
once
作用
指示该头文件(物理层面)只被包含一次
示例
#pragma once
message
作用
指示在编译过程中显示一条信息
示例
#pragma message("字符串")
message(~) 的参数只能是C风格字符串类型,可以为单个字符串或多段字符串组成,如: #pragma message( "编译 " __FILE__)
comment
作用
指示将一个注释记录放置到一个目标文件或可执行文件中
格式
#pragma comment(comment-type[, "commentstring"])
comment-type 分类
lib
作用
将一个库搜索记录放置到目标文件中
commentstring参数必须有包含你要连接程序搜索的库名(和可能的路径)。 因为在目标文件中该库名先于默认的库搜索记录,所以链接程序将如同你在命令行输入这些库一样来搜索它们。你可以在一个源文件中放置多个库搜索记录,每个记录将按照它们出现在源文件中的顺序出现在目标文件中。
相对路径以当前工程目录为起点
示例
#pragma comment(lib, "WSock32.lib") #pragma comment(lib, "..\\Project1\\Lib\\WSock32.lib")
"commentstring"
字符串由编译器处理,为C风格字符串
pack
作用
指定结构体,联合体和类成员的包装对齐方式。指定以“n”字节对齐
格式
#pragma pack([show]|[push|pop][, identifier], n)
例: #pragma pack() // n defaults to 8; equivalent to /Zp8 #pragma pack(show) // warning C4810 #pragma pack(4) // n = 4 #pragma pack(show) // warning C4810 #pragma pack(push, r1, 16) // n = 16, pushed to stack #pragma pack(show) // warning C4810 #pragma pack(pop, r1, 2) // n = 2 , stack popped #pragma pack(show) // warning C4810
show
显示当前用于包装对齐的字节数。 该数据通过警告消息显示
push
将当前包装对齐字节数压入内部编译器堆栈,并将当前包装对齐字节数设置为n
如果未指定n,则压入当前的包装对齐字节数。
pop
弹出内部编译器堆栈的顶部记录。 并将当前包装对齐字节数设置为n
例: #pragma pack(pop, 16)
如果未指定n,则使用弹出记录的包装对齐字节数。
弹出内部编译器堆栈的指定ID的记录。 并将当前包装对齐字节数设置为弹出记录的包装对齐字节数
例: #pragma pack(pop, r1)
identifier
当与push一起使用时,则为内部编译器堆栈上的记录分配一个名称
当与pop一起使用时,从内部堆栈中弹出记录,直到指定ID为止; 如果在内部堆栈上找不到指定ID,则不会弹出任何内容
n
包装对齐字节数,默认值为8,有效值为1、2、4、8、16
execution_character_set
作用
设置“执行字符集”
格式
#pragma execution_character_set("target")
"target"
要设置成的字符集
目前仅支持 "utf-8"
例:
#if defined(_MSC_VER) && (_MSC_VER >= 1600) //判断编译器cl.exe的版本是否等于或高于16.00。VisualStudio 2010使用的cl.exe版本为16.00。 #pragma execution_character_set("utf-8") #endif
其他说明
该指令从VS2010开始支持,但VS2010需要打SP1补丁。而VS2012又不支持,VS2013又可以支持。
从VS2015 Update 2开始,该指令已过时。 推荐使用编译器选项“/execution-charset:utf-8”设置执行字符集为UTF-8,同时还建议在源代码字符(串)字面常量前加上“u8”前缀。
MSVC编译器选项(项目的属性→配置属性→C/C++→命令行→其他选项): (1)设置源码字符集为UTF-8:/source-charset:utf-8 (2)设置执行字符集为UTF-8:/execution-charset:utf-8 (3)同时设置源码、执行字符集为UTF-8:/utf-8;相当于 /source-charset:utf-8 /execution-charset:utf-8 选项格式: /source-charset:[IANA_name|.CPID] /execution-charset:[IANA_name|.CPID] IANA_name:IANA 定义的字符集名称。 CPID:代码页标识符。
编译工具
Visual Studio
MSVC 编译器
源文件后缀为“.c”时,按C语言编译
源文件后缀为“.cpp”时,按C++语言编译
运行时库
运行时库(Runtime Library)、C运行时库(C Run-time Library)(CRT)。 选项位置:项目属性→配置属性→C/C++→代码生成→运行库。 提示:链接的各模块间的运行时库要一致。
例:Release配置
多线程DLL(/MD)(推荐)
动态链接系统的运行时库
导入库文件:msvcrt.lib,动态链接库文件:msvcrxxx.dll。 提示: 打开程序出现“应用程序配置不正确”错误时,可能是系统的 CRT dll 文件存在问题,可以尝试更换合适版本的dll、或补全缺失的dll解决;还可以尝试将程序运行时库设置为静态链接系统的再重新编译解决。
多线程(/MT)
静态链接系统的运行时库
对象库文件:libcmt.lib。
堆内存释放问题
静态链接CRT时,exe、dll等各模块将拥有各自的堆, 所以在exe/dll中malloc的内存,只能在exe/dll中free
C++部分
1. 面向对象
概念
面向对象(Object-Oriented,OO)
客观世界由许多具体的事物、事件、概念和规则组成,这些均可被看成对象。面向对象方法是一种非常实用的系统化软件开发方法,它以客观世界中的对象为中心,其分析和设计思想符合人们的思维方式,分析和设计的结果与客观世界的实际比较接近,容易被人们所接受。
识别等式
可以说,采用这 4 个概念开发的软件系统是面向对象的。
面向对象 = 对象(Object)+分类(Classification)+继承(Inheritance)+通过消息的通信(Communication with Messages)
三大特性
封装
封装是一种信息隐蔽技术(信息隐蔽是目的,封装是达到这个目的的技术),它的目的是使对象的使用者和生产者分离,使对象的定义和实现分开。
继承
多态
软件设计七大原则
目的
降低对象之间的耦合,增加程序的可复用性、可扩展性和可维护性。
分类
开闭原则
概念
对扩展开放,对修改关闭
目的
降低维护带来的新风险
依赖倒置原则
概念
高层不应该依赖低层,要面向接口编程
目的
更利于代码结构的升级扩展
单一职责原则
概念
一个类只干一件事,类的职责要单一
目的
便于理解,提高代码的可读性
接口隔离原则
概念
一个接口只干一件事,使类对接口的依赖精简单一
目的
功能解耦,高聚合、低耦合
迪米特原则
概念
不该知道的不要知道,一个类应该保持对其它对象最少的了解,降低耦合度
目的
只和朋友交流,不和陌生人说话,减少代码臃肿
里氏替换原则
概念
不要破坏继承体系,子类重写方法功能发生改变,不应该影响父类方法的含义
目的
防止继承泛滥
合成复用原则
概念
尽量使用组合或者聚合关系实现代码复用,少使用继承
目的
降低代码耦合
基本概念
对象
定义
是面向对象系统中基本的运行时的实体
是类的具体化,是类的实例(Instance)
组成
一个对象把属性和行为封装为一个整体。
对象名
数据:(成员变量)、(字段)、(属性)
作用于数据的操作:(成员函数)、(方法)、(行为)
消息
定义
对象之间进行通信的一种构造
消息传递
对象是类的实例。尽管对象的表示在形式上与一般数据类型十分相似,但是它们之间存在一种本质区别:对象之间通过消息传递方式进行通信。 消息传递原是一种与通信有关的概念,OOP使得对象具有交互能力的主要模型就是消息传递模型。 对象被看成用传递消息的方式互相联系的通信实体,它们既可以接收也可以拒绝外界发来的消息。一般情况下,对象接收它能够识别的消息,拒绝它不能识别的消息。对于一个对象而言,任何外部的代码都不能以任何不可预知或事先不允许的方式与这个对象进行交互。 发送一条消息至少应给出一个对象的名字和要发给这个对象的那条消息的名字。通常,消息的名字就是这个对象中外界可知的某个方法的名字。在消息中,经常还有一组参数(也就是那个方法所要求的参数),将外界的有关信息传给这个对象。接收对象解释接收的消息及其信息,并予以响应。 对于一个类来说,它关于方法界面的定义规定了实例的消息传递协议,而它本身则决定了消息传递的合法范围。由于类是先于对象构造而成的,所以一个类为它的实例提供了可以预知的交互方式。 例:C++中一个对象调用另一个对象的方法。假设m1是类Manager的一个实例(或对象),当外界要求把这个对象所代表的那位经理的级别改变为2时,就应以下面的方式向这个对象发出一条消息: m1.ChangeLevel(2); //“m1”:对象名,“ChangeLevel”:消息名,“2”:信息
类
定义
是在对象之上的抽象,把一组对象的共同特征加以抽象并存储
一个类所包含的数据和方法,描述一组对象的共同属性和行为
类的封装性
一是能把数据和方法组合在一起
二是能隐蔽信息
公有成员可以供外部访问
私有成员只允许内部访问
类的实例化功能
实例生成
实例消除
分类
实体类
表示真实实体
接口类(边界类)
人的接口
系统接口
控制类
控制活动流
关系
is-a 关系
即特殊和一般的关系。一些类是某个类的特殊情况,某个类是一些类的一般情况。
一般类 是 特殊类 的 父类(基类)
特殊类 是 一般类 的 子类(派生类)
特殊类 是一种 一般类
has-a 关系
即整体和部分的关系。一个类拥有另外一些类。
继承
定义
是父类和子类之间共享数据和方法的机制
这是类之间的一种关系,在定义和实现一个类的时候,可以在一个已经存在的类的基础上进行,把这个已经存在的类所定义的内容作为自己的内容(继承部分),并加入若干新的内容(增加部分)。
父类、子类
父类描述了子类的公共数据和方法
子类可以继承它的父类(或祖先类)中的数据和方法
当执行一个子类的实例生成方法时,首先在类层次结构中从该子类沿继承路径上溯至它的一个基类,然后自顶向下执行该子类所有父类的实例生成方法;最后执行该子类实例生成方法的函数体。 当执行一个子类的实例消除方法时,顺序恰好与执行该子类实例生成方法相反;先执行该子类实例消除方法的函数体,再沿继承路径自底向上执行该子类所有父类的实例消除方法。
分类
在一个面向对象系统中,子类与父类之间的继承关系构成了这个系统的类层次结构。
单重继承
类层次结构用树描述。
一个子类只从一个父类继承
多重继承
类层次结构用图描述。
一个子类从两个或更多个父类继承
多态
定义
不同对象 收到 同一消息 可产生 不同结果,这一现象称为多态(Polymorphism)
分类
通用
对工作的类型无限制,允许对不同类型的值执行相同的代码。
参数(Parametric)
是静态多态。
例:
类属
类属是程序设计语言中的一种参数多态机制。一个类属类是关于一组类的一个特性抽象,它强调的是这些类的成员特征中与具体类型无关的那些部分,而与具体类型相关的那些部分则用变元来表示。
C++中的模板
包含(Inclusion)
是动态多态。
同样的操作可用于一个类型及其子类型
子类型:遵循里氏替换原则的子类。
例:
C++中的虚函数
特定
只对有限的类型有效,对不同类型的值可能要执行不同的代码。
强制(Coercion)
是静态多态。
例:
强制类型转换
过载(Overloading)
是静态多态。
同一个名字(操作符﹑函数名称)在不同的上下文中有不同的含义
动态绑定
定义
绑定
把过程调用和响应调用所需要执行的代码加以结合的过程
动态绑定是和类的继承以及多态相联系的。在继承关系中,子类是父类的一个特例,所以父类对象可以出现的地方,子类对象也可以出现。 因此在运行过程中,当一个对象发送消息请求服务时,要根据接收对象的具体情况将请求的操作与实现的方法进行连接,即动态绑定。
分类
静态绑定
在编译时绑定。
动态绑定
在运行时绑定。
面向对象程序设计(Object-Oriented Programming,OOP)
定义
实质是选用一种面向对象程序设计语言(Object-Oriented Programming Language,OOPL),采用对象、类及其相关概念所进行的程序设计。
面向对象的程序设计语言一切从类和对象出发,语言中类库的丰富程度表征了该面向对象程序设计语言的成熟度。
它的关键在于加入了类和继承性,从而进一步提高了抽象程度。特定的OOP概念一般是通过OOPL中特定的语言机制来体现的。
概念
类
继承和类层次结构
对象、消息传递和方法
对象自身引用
对象自身引用(self-Reference)是OOPL中的一种特有结构。这种结构在不同的OOPL中有不同的名称,在C++和Java中称为 this。 对象自身引用的值和类型分别扮演了两种意义的角色:对象自身引用的值使得方法体中引用的成员名与特定的对象相关,对象自身引用的类型则决定了方法体被实际共享的范围。 对象自身引用机制使得在进行方法的设计和实现时并不需要考虑与对象联系的细节,而是从更高一级的抽象层次,也就是类的角度来设计同类型对象的行为特征,从而使得方法在一个类及其子类的范围内具有共性。在程序运行过程中,消息传递机制和对象自身引用将方法与特定的对象动态地联系在一起,使得不同的对象在执行同样的方法体时,可以因对象的状态不同而产生不同的行为,从而使得方法对具体的对象具有个性。
重置或覆盖
重置或覆盖是在子类中重新定义父类中已经定义的方法,其基本思想是通过一种动态绑定机制的支持,使得子类在继承父类接口定义的前提下用适合自己要求的实现去置换父类中的相应实现。 在OOPL中,重置机制有相应的语法供开发人员选择使用。在C++语言中,通过虚函数(Virtual Function)的定义来进行重置的声明,通过虚函数跳转表(Virtual Functions Jump Tables,VTBL)结构来实现重置方法体的动态绑定。
类属类
无实例的类
例如C++中的抽象类,通过在类中定义纯虚函数创建抽象类。
步骤
面向对象分析(Object-Oriented Analysis,OOA)
活动
认定对象
组织对象
描述对象间的相互作用
确定对象的操作
定义对象的内部信息
面向对象设计(Object-Oriented Design,OOD)
活动
识别类及对象
定义属性
定义服务
识别关系
识别包
面向对象测试
层次
算法层
基本上相当于传统软件测试中的单元测试。
类层
基本上相当于传统软件测试中的模块测试。
模板层
基本上相当于传统软件测试中的集成测试。
系统层
2. 引用
概念
引用相当于一个被绑定对象的别名
声明与定义
左值引用
type& refer = 左值;
定义时必须初始化
初始化后,该引用变量 便与某个对象绑定, 中途无法解绑(可以视为功能受限的指针)。
引用只能初始化时绑定,后续不能再重新绑定
底层实现类似于顶层const,例:type* const refer = &左值;
例:
int a =1; int& r = a;
右值引用
C++11支持。
type&& refer = 右值;
定义时必须初始化
初始化后,该引用变量 便与某个对象绑定, 中途无法解绑。
作用
支持“移动语义”
利用移动语义,程序可以将资源(如动态分配的内存)从一个对象转移到另一个对象。 使资源能够从无法在程序中的其他位置引用的临时对象转移出去,从而减少对象复制等的消耗
std::move函数
标准库 utility 中的 std::move 函数转移了资源的控制权,本质上是将左值强制转化为右值,以用于移动、复制或赋值,避免对含有资源的对象进行无谓的复制。std::move对于拥有如内存、文件句柄等资源的成员的对象有效,如果是一些基本类型,如int和char[10]数组等,如果使用std::move,仍会发生复制(因为没有对应的移动构造函数),所以说std::move对含有资源的对象更有意义。 例: #include <utility> int GetY(int x) { return x * 2; } int main() { int a = 1; int&& r1 = std::move(a); r1 = 2; int&& r2 = GetY(1); int b = r2; r2 = 3; return 0; } 参考:https://www.jianshu.com/p/d19fc8447eaa
支持“完美转发”
减少对泛型函数重载的需求
std::forward函数
标准库 utility 中的 std::forward 函数可以保留模板参数的引用类型(左值、右值)。 例: #include <utility> #include <iostream> void ProcessValue(int& a) { std::cout << "左值引用" << std::endl; } void ProcessValue(int&& a) { std::cout << "右值引用" << std::endl; } //当发生自动类型推断时(如函数模板的类型自动推导,或auto关键字),&&是一个Universal References,可以接受左值或右值。这个特性让它适合作为一个参数的路由,然后再通过std::forward按照参数实际的引用类型去匹配对应的重载函数,最终实现完美转发。 template<typename T> void ForwardValue(T&& val) { ProcessValue(std::forward<T>(val)); //按照参数实际的引用类型进行转发 } int main() { int x = 0; ForwardValue(x); //传入左值;实际调用 void ProcessValue(int& a) ForwardValue(0); //传入右值;实际调用 void ProcessValue(int&& a) return 0; } 参考:https://www.cnblogs.com/qicosmos/p/4283455.html
C++11右值
分类
纯右值
概念
相当于传统C++中的右值
将亡值
概念
生命期将要结束的值
被绑定到右值引用后,其生命期将与右值引用变量的相同
例:
int&& r = 1;
安全使用规范
目标内存是否有效
例:将引用作为函数的返回值时,若返回局部变量,在函数返回后,局部变量生命期就已经结束了
函数中的使用
参数
对引用的访问,实质上都是对被引用对象的访问
返回值
返回引用时,称该函数的返回值是左值
例: int a; int& Test() { return a; } Test() = 666;
const引用
可参考 本思维导图::C部分::常量::定义常量。
例:
const int& r = a;
const int& r = 1; //const引用可以绑定右值
3. 对象初始化
概念
问:对象为什么要初始化? 答:对象经过初始化后,对象中的随机的数值变为指定值,使得该对象成为一个有意义的对象。
初始化方式
直接初始化
未使用赋值操作符“=”初始化的方式
形式
数据类型 var;
默认初始化
若数据类型为基本类型
且变量为全局变量
会初始化为默认值0
且变量为局部变量
对于非static局部变量
不会进行初始化
对于static局部变量
会进行值初始化
值初始化
处理方式
如果数据类型为基本类型,会初始化为默认值0
如果数据类型为类,该类有用户自定义的默认构造函数,会调用用户自定义的默认构造函数
如果数据类型为类,该类只有编译器生成的默认构造函数,会先将成员变量初始化为默认值0,再调用编译器生成的默认构造函数
发生场景
使用了初始化器(即使用了小括号或大括号),但却没有为对应变量提供初始值时
例:
int a = int();
Object obj{};
若数据类型为类,会调用默认构造函数进行初始化
数据类型 var();
因为C++分析器更偏向于声明,所以该形式会优先被视为函数声明,无法用于对象初始化
若数据类型为类,会调用默认构造函数进行初始化。应使用上一形式“数据类型 var;”
数据类型 var(初始值列表);
若数据类型为基本类型,会初始化为初始值
例:
int a(1);
若数据类型为类,会调用匹配的构造函数进行初始化
例:
Object obj(1);
Object obj(1, 2);
数据类型 var{initializer list};
C++11支持“{initializer list}”。 相比“(初始值列表)”,list中的元素不会自动进行隐式收缩转换。 另外,当list的元素个数 比 变量声明的元素个数 少,则没有被list中元素覆盖的变量元素将会进行值初始化。
若数据类型为基本类型,会初始化为初始值
例:
int a{};
int a{1};
int arr[3]{1, 2, 3};
若数据类型为聚合类型,会进行聚合初始化
具体到聚合类型的每个元素,进行的是直接初始化
若数据类型为类,会调用匹配的构造函数进行初始化
例:
Object obj{};
Object obj{1};
Object obj{1, 2};
Object objArr[3]{{7, 8}, {9, 10}, {11, 12}};
复制初始化
使用赋值操作符“=”初始化的方式
理论过程
实际允许的情况下,编译器一般会优化成直接初始化。这个从编译后的程序汇编代码也可以看出来。
(1)“=”右侧的表达式,先通过直接初始化方式初始化一个临时变量
(2)“=”操作符再将临时变量的值,复制到左侧要初始化的变量
若数据类型为类,会调用复制构造函数进行复制
形式
数据类型 var = 初始值;
对于初始化过程中“=”右侧表达式生成的临时变量
若数据类型为基本类型,会初始化为初始值
若数据类型为类
且初始值类型与该类类型相同,会将初始值复制到左侧要初始化的变量
例:
Object obj2 = obj;
且初始值类型与该类类型不同,会先将右侧表达式隐式转换为“数据类型(初始值)”形式,然后调用匹配的构造函数进行初始化
例:
Object obj = 1; ==> Object obj = Object(1);
在赋值表达式中,也存在这样的隐式转换: obj = 1; ==> obj = Object(1);
数据类型 var = 数据类型();
对于初始化过程中“=”右侧表达式生成的临时变量
若数据类型为基本类型,会初始化为默认值0
例:
int a = int();
若数据类型为类,会调用默认构造函数进行初始化
例:
Object obj = Object();
数据类型 var = 数据类型(初始值列表);
数据类型 var = {initializer list};
C++11支持“initializer list”。
数据类型 var = 数据类型{initializer list};
C++11支持“initializer list”。
4. 动态创建对象
概念
对于对象的动态创建,使用C语言中的 malloc 函数为对象分配的内存中的数值是随机的,所以对象中的数值都是无意义的,使得该对象也是无意义的。
C++引入类及 构造/析构 函数后,malloc/free 只能 分配/释放 内存,不会自动调用 构造/析构 函数进行相关处理,无法胜任对象 创建/销毁 的工作,所以C++引入了 new/delete 操作符以完成该工作。
操作符
new
创建对象
会先分配内存(底层调用malloc),再进行相关处理
相比 malloc 函数,new 操作符分配前无需指定分配存储区域大小,分配后的存储区域指针在使用时也无需进行强制转换。这些事情,new 操作符会自动处理。
delete
销毁对象
会先进行相关处理,再释放内存(底层调用free)
使用形式
操作单个对象
数据类型* p = new 数据类型; //创建(若数据类型为类,会调用默认构造函数进行初始化) delete p; //销毁
例:
int* p = new int; //创建 delete p; //销毁 p = NULL;
数据类型* p = new 数据类型(); //创建并初始化为默认值0(若数据类型为类,会调用默认构造函数进行初始化) delete p; //销毁
例:
int* p = new int(); //创建 delete p; //销毁 p = NULL;
数据类型* p = new 数据类型(初始值列表); //创建并初始化为初始值(若数据类型为类,会调用匹配的构造函数进行初始化) delete p; //销毁
例:
int* p = new int(1); //创建 delete p; //销毁 p = NULL;
数据类型* p = new 数据类型{initializer list}; //创建并初始化为初始值(若数据类型为类,会调用匹配的构造函数进行初始化) delete p; //销毁
C++11支持“initializer list”。
例:
int* p = new int{1}; //创建 delete p; //销毁 p = NULL;
操作多个对象
数据类型* p = new 数据类型[个数]; //创建(若数据类型为类,会调用默认构造函数对每个对象分别进行初始化) delete[] p; //销毁
例:
int* p = new int[3]; //创建 delete[] p; //销毁 p = NULL;
数据类型* p = new 数据类型[个数](); //创建并将每个对象分别初始化为默认值0(若数据类型为类,会调用默认构造函数对每个对象分别进行初始化) delete[] p; //销毁
例:
int* p = new int[3](); //创建 delete[] p; //销毁 p = NULL;
数据类型* p = new 数据类型[个数]{initializer list}; //创建并将每个对象分别初始化为初始值(若数据类型为类,会调用匹配的构造函数对每个对象分别进行初始化) delete[] p; //销毁
C++11支持“initializer list”。
例:
int* p = new int[3]{ 1,2,3 }; //创建。*(p+0)的值初始化为1,*(p+1)的值初始化为2,*(p+2)的值初始化为3 delete[] p; //销毁 p = NULL;
5. 类
概念
引入
第1阶段:最初的C++称为“带类的C”,它扩展了C语言结构体的功能
历史: 在1979年,一名刚获得博士学位的研究员,为了开发一个软件项目发明了一门新编程语言,该研究员名为Bjarne Stroustrup,该门语言则命名为“C with Classes”,四年后改称为C++。C++是一门通用编程语言,支持多种编程范式,包括过程式、面向对象(object-oriented programming, OOP)、泛型(generic programming, GP),后来为泛型而设计的模版,被发现及证明是图灵完备的,因此使C++亦可支持模版元编程范式(template metaprogramming, TMP)。 
C++中的结构体成员可以包含
数据,(数据成员)(成员变量)
操作这些数据的函数,(成员函数)
例: struct SObject { //数据 int a; int b; double c; double d; //操作这些数据的函数 double GetSum(SObject* that) { return (double)that->a + (double)that->b + that->c + that->d; } };
信息隐蔽
为了达到信息隐蔽的目的,限制对结构体中成员(成员变量、成员函数)的非法访问,C++增设了3个“访问修饰符”,用于设置成员的访问权限
分类
public
修饰的成员
称为“公有成员”、“公共成员”
在该结构体的内部和外部均可访问;
protected
修饰的成员
称为“受保护成员”
在该结构体的内部可访问,外部不可访问(但在继承该结构体的结构体内部可访问);
private
修饰的成员
称为“私有成员”
在该结构体的内部可访问,外部不可访问;
访问权限大小排序
public > protected > private
访问权限设置的有效范围
结构体中可以有多个 public、protected、private 标记区域(使用时访问修饰符后要加冒号)。 每个标记区域在下一个标记区域开始之前, 或者在遇到结构体主体结束的右大括号之前 都是有效的。
例1: struct Base { public: //公有成员 protected: //受保护成员 private: //私有成员 }; 例2: struct SObject { protected: private: public: int a; //最终受 public 修饰 int b; //最终受 public 修饰 private: public: protected: double c; //最终受 protected 修饰 public: protected: private: double d; //最终受 private 修饰 private: protected: public: double GetSum(SObject* that) //最终受 public 修饰 { return (double)that->a + (double)that->b + that->c + that->d; } };
访问权限被限制的成员变量
可添加 getter 、setter 供外部访问
this 指针
历史
C++程序到C程序的翻译
C++是在C语言的基础上发展而来的,早期的C++编译器cfront实际上是先将C++代码翻译成C代码,然后再用C语言编译器进行编译。 C语言没有类的概念,只有结构体,函数是全局函数,没有成员函数。所以翻译时,class翻译为struct,对象翻译为结构体变量,类的成员函数翻译为全局函数。但是,如何让从(非静态)成员函数翻译出的全局函数还能作用在翻译后的结构体变量上呢?答案就是引入“this 指针”。 例: //========翻译前的C++代码============ class PC { public: int m_performance; void SetPerformance(int perf) { m_performance = perf; } }; int main() { PC pc; pc.SetPerformance(666666); return 0; } //=================================== //========翻译后的C代码============== struct PC { int m_performance; }; void SetPerformance(struct PC* const this, int perf) // this 指针参数,使翻译出的全局函数能作用在翻译后的结构体变量上 { this->m_performance = perf; } int main() { struct PC pc; SetPerformance(&pc, 666666); return 0; } //===================================
作用
指向非静态成员函数所作用的对象
关于静态成员函数: 静态成员函数属于整个类,与类的实例无关,只与类有关,不会传入 this 指针。本质上相当于全局函数。
在C++中,this 指针是编译器隐含加上的,编码时可直接使用
例: //===C++代码=== struct PC { int m_performance; void SetPerformance(int perf) { this->m_performance = perf; //直接使用 this 指针 m_performance = perf; //为了简化代码,访问内部成员无冲突时(如:无变量名重名情况), this 指针 可以省略不写 } }; int main() { PC pc; pc.SetPerformance(666666); return 0; }
第2阶段:
因为C++中的结构体扩展自C,以及为了兼容C代码,所以C++中结构体成员的访问权限都默认为public,使外部可以访问其成员。这在编码时,可能会因疏忽了成员访问权限的设置导致意外地破坏了封装。此外,使用时C++中扩展的结构体还容易与C中的混淆。 基于以上这些原因,C++引入了功能与struct相同但使用更安全的一个新数据类型 —— 类:class 。
struct 、 class 的主要区别
成员访问属性
struct
默认 public
class
默认 private
继承
struct
默认 public
class
默认 private
类
类型形式
类型声明
class 类型名;
说明
使用类类型声明的目的在于告诉编译器存在这样一个类类型
这时声明的类类型,是一个不完整的类型,因为它还没有定义
该形式有时也称为“前向声明”(forward declaration)
例:
一般形式的类类型声明
class Object;
属于一个命名空间的类类型声明
namespace ID { class Object; }
类型定义
class 类型名{成员列表};
说明
C++中,“类型名”本身就已构成完整的类类型名,可在变量声明、定义时直接使用,不需要在类型名前再加上 class 关键字。
例:
class Object { private: int a; int b; int c = 1; //这里为成员变量提供了一个“类内初始值”,其初始化过程在初始化列表之后,及构造函数体之前执行。C++11支持该形式的初始化 public: void Test(int a, int b) { this->a = a + 3; //与C语言中出现变量名重名时类似,根据“就近原则”,参数 a 是距离此处最近的变量,会优先访问。若要访问成员变量 a,则要加上 this 指针以显式指定 this->b = b + 4; } };
变量定义
一个具体的变量(即对象)是该类的一个实例(instance)
定义
类型名 变量;
例:
Object obj;
Object obj(1);
Object obj = Object();
类型名* 指针;
例:
Object* ptr;
Object* ptr(&obj);
Object* ptr = new Object();
类的分离式写法
步骤
(1)成员声明写在类体中
(2)成员定义写在类体外,并添加类作用域限定
注意: 非静态成员变量属于类的实例,与类的实例有关,其定义不能在类体外编写。
例:
例: (1): //===Object.h=== #ifndef OBJECT_H #define OBJECT_H class Object { private: int a; public: int GetA(); void SetA(int value); }; #endif //====== (2): //===Object.cpp=== #include "Object.h" int Object::GetA() //函数首部中函数名称前的“Object::”限定了该函数的类作用域 { return this->a; } void Object::SetA(int value) { this->a = value; } //======
特殊成员函数
构造函数
例: class Circle { private: int x; int y; int radius; public: Circle() //默认构造函数 { x = y = 0; radius = 1; } //Circle(int xVal = 0, int yVal = 0) //默认构造函数 //{ // x = xVal; // y = yVal; // radius = 1; //} Circle(const Circle& other) //复制构造函数 { x = other.x; y = other.y; radius = other.radius; } Circle(int xVal, int yVal, int rVal) //普通构造函数 { x = xVal; y = yVal; radius = rVal; } Circle(int r) //转换构造函数 { x = y = 0; radius = r; } ~Circle() //析构函数 { x = y = 0; radius = 0; } };
作用
完成类的实例生成功能。在对象创建时被调用,对新创建的对象进行初始化,使对象创建后就是有意义的
一般形式
(声明并)定义
类型名(参数列表) { 函数体 }
构造函数允许重载
默认构造函数
特点
没有参数 或 参数均有默认值
当没有编写任何的构造函数时,编译器会自动生成一个默认构造函数
复制构造函数
特点
有且只有1个参数,且参数类型为“const 类型名&”
即参数类型为本类类型的常量(的)引用类型。
在对象复制时被调用
当没有编写复制构造函数时,编译器会自动生成一个复制构造函数
自动生成的复制构造函数只会执行浅拷贝。
移动构造函数
C++11支持。
特点
有且只有1个参数,且参数类型为“类型名&&”
用于对(将亡值)对象的资源进行浅拷贝,避免不必要的深拷贝,从而提高执行效率
转换构造函数
特点
有且只有1个参数,且参数类型与该类类型不同
关于 本思维导图::C++部分::对象初始化::复制初始化中“数据类型 var = 初始值;”形式说明提到的表达式隐式转换,若需要限制该隐式转换,可以在转换构造函数声明前添加 explicit 关键字修饰,使其只能显式调用。
例:
//... explicit Circle(int r) //转换构造函数 { x = y = 0; radius = r; } //... Circle o1 = 1; //报错,因为隐式转换被限制 Circle o2 = Circle(2); //未报错,因为进行了显式调用 o1 = 1;//报错,因为隐式转换被限制 o2 = Circle(2);//未报错,因为进行了显式调用
普通构造函数
访问权限一般设置为public
使用
一般不在代码中显式调用,编译器在对象创建时分配内存后会自动调用
通过变量显式调用的方式: (1)类内部显式调用: Circle(); (2)类外部显式调用,需要添加类作用域限定: Circle o1; o1.Circle::Circle();
构造函数不能为虚函数
初始化列表
作用
(1)调用父类的构造函数
若没有在初始化列表中显式调用父类的构造函数,则编译器会(根据继承的先后顺序)自动调用父类的默认构造函数
在继承中,每个派生类只负责它的直接基类的构造函数调用
在虚拟继承中,每一层派生类都要负责虚基类的构造函数调用; 但在创建对象时,仅会执行被创建对象的派生类构造函数中的调用
(2)使用直接初始化的方式初始化成员变量
所以,在构造函数体中对成员变量的赋值并不是初始化,而只是赋值。 一个好的原则是,能使用初始化列表的时候尽量使用初始化列表。
执行时,初始化列表中各成员变量初始化的执行顺序,与成员变量在类中的声明顺序一致
没有在初始化列表中显式初始化的成员变量,会执行默认初始化
说明
初始化列表会在构造函数体之前执行
使用
语法
位于构造函数定义处的小括号之后,大括号之前
以“:”引导,各初始化表达式之间用逗号隔开
例:
//===========main.cpp=========== class Position { private: int x; int y; int z; public: Position() : x(0), y(0), z(0) { } Position(int x, int y, int z) : x(x), y(y), z(z) //从初始化列表中元素的形式上可以看出,元素使用直接初始化的方式初始化指定变量,所以在初始化列表中的元素如“x(x)”表达式中,小括号外的x是被初始化的变量,小括号内的x是初始值。 // “x(x)”表达式中两个标识符的名称虽然都是x,但却是两个层面的概念,所以两者在这种情况下并不属于变量名重名。 { } }; class Rotate { private: int xr; int yr; int zr; }; class Color { private: int red; int green; int blue; public: Color() : red(0), green(0), blue(0) { } Color(int r, int g, int b) : red(r), green(g), blue(b) { } }; class Shape { protected: int length; int width; public: Shape() : length(0), width(0) { } Shape(int len, int w) : length(len), width(w) { } }; class Box : public Shape { private: int flag; int hight; int weight; Position pos; Rotate angle; Color color; int data[5]; public: Box(); }; Box::Box() : Shape(), hight(), weight(0), pos(1, 2, 3), angle(), color{104, 105, 106}, data{11, 12, 13, 14, 15} //初始化列表中: // 元素“Shape()”调用了父类的默认构造函数; // 成员变量“flag”没有在初始化列表中显式初始化,会执行直接初始化之中的默认初始化; // 元素“hight()”使用了初始化器却没有提供初始值,会执行直接初始化之中的值初始化; // 元素“weight(0)”会执行直接初始化。又因为其数据类型为基本类型,所以会初始化为指定的初始值0; // 元素“pos(1, 2, 3)”会执行直接初始化。又因为其数据类型为类,所以会调用匹配的构造函数进行初始化; // 元素“angle()”使用了初始化器却没有提供初始值,且数据类型为类,该类只有编译器生成的默认构造函数,会执行直接初始化之中的值初始化,先将成员变量初始化为默认值0,再调用编译器生成的默认构造函数; // 元素“color{104, 105, 106}”使用了C++11支持的initializer list,且数据类型为类,会调用匹配的构造函数进行初始化; // 元素“data{11, 12, 13, 14, 15}”使用了C++11支持的initializer list,且数据类型为聚合类型,会进行聚合初始化。具体到数组的每个元素,进行的是直接初始化; { length = 200; //赋值 width = 100; //赋值 } int main() { Box box; return 0; } //======================
析构函数
作用
完成类的实例消除功能。在对象销毁时被调用,进行一些清理工作
一般形式
(声明并)定义
~类型名() { 函数体 }
析构函数不允许重载
当没有编写析构函数时,编译器会自动生成一个析构函数
访问权限一般设置为public
使用
一般不在代码中显式调用,编译器在对象销毁时内存释放前会自动调用
通过变量显式调用的方式: (1)类内部显式调用,需要通过this指针访问: this->~Circle(); (2)类外部显式调用: Circle o1; o1.~Circle();
用作基类的类的析构函数通常声明为虚函数
使得当用一个基类的指针删除一个派生类的对象时,派生类的析构函数会被调用。
例: Parent* p = new Child; delete p; //若父类的析构函数不是虚函数,这里将直接调用父类的析构函数,不会调用子类的析构函数,导致实例不能正确析构
特殊成员
const成员
const成员变量
也可以说是“成员常量”。
初始化
因为const变量只能初始化,后续不能再赋值,所以其初始化只能通过构造函数的初始化列表进行
例:
class Object { private: const int A; const int* P; //指针 P 本身是变量 int* const PC; //指针 PC 本身是常量 public: Object() : A(1), PC((int* const)&A) //通过初始化列表初始化 const成员变量 { P = &A; } };
const成员函数
原理
例: class Object { private: const int A; const int* P; int* const PC; public: Object() : A(1), PC((int* const)&A) { P = &A; } void Test() const //const成员函数。相当于:void Test(const Object* this) { this->P = &A; //此处报错 } void Test() //非const成员函数。相当于:void Test(Object* this) { this->P = &A; //此处无报错 ((const Object*)this)->Test(); //匹配 void Test() const } void Test(Object* that) const //const成员函数 { this->P = &A; //此处报错 this->Test(); //匹配 void Test() const that->P = &A; //此处无报错 that->Test(); //匹配 void Test() } };
const修饰的是非静态成员函数隐含的this指针参数,使this指针成为指向常量的指针
因为this指针参数是隐含的,所以const关键字只好写在函数的“)”后面
因为this指针参数是被const修饰的,所以该非静态成员函数内再(通过this指针)调用非静态成员函数时,只会匹配const成员函数
mutable成员
mutable关键字
对于被声明为mutable的成员变量,即使在const成员函数中也可以修改其值
只能用于非静态和非常量成员变量
例:
class Test { private: bool m_flag; mutable int m_accessCount; public: bool GetFlag() const { m_accessCount++; //在const成员函数中修改 return m_flag; } };
静态成员
概念
即在声明前添加了 static 关键字修饰的成员
静态成员属于整个类
从语义上说: 静态成员对类有意义; 非静态成员(实例成员)对实例有意义。
与类的实例无关,只与类有关
所以,静态成员函数不会传入 this 指针
在内存中只有1份存储,为所有对象共享
本质上相当于作用域在类范围内的全局变量/全局函数
分类
静态成员变量
静态成员函数
使用
语法
在成员声明前添加 static 关键字修饰
因为类体内编写的成员变量只是声明,所以静态成员变量还必须在类体外定义,为其分配存储空间
外部访问方法
类型名::静态成员;
对象名.静态成员;
例:
//===========main.cpp=========== class Object { private: int a; static int b; //静态成员变量声明 public: static int c; //静态成员变量声明 public: void SetAB(int value) { a = value; b = a; } static void SetB(int value) //静态成员函数声明并定义 { b = value; //a = b; //报错 //SetAB(b); //报错。因为静态成员函数不会传入this指针,所以只能访问静态成员,不能访问非静态成员 } }; int Object::b; //静态成员变量定义、初始化 int Object::c = 0; //静态成员变量定义、初始化 int main() { Object::SetB(1); Object::c = 2; Object obj; obj.SetAB(3); obj.SetB(4); obj.c = 5; return 0; } //======================
友元
概念
友元机制 允许类 给其他类或函数 授予 直接访问类的包括非公有成员在内的所有成员的 权限
“友元”就是告诉编译器某个外部成分是该类的“朋友”,该类允许朋友直接访问包括非公有成员在内的所有成员。 不过,朋友关系是“单向”的。若有类A、类B,B被声明为A的友元,则B可以访问A的所有成员。但反之不成立,因为A不是B的友元。
语法
在类体内,使用关键字 friend 声明友元
声明在类体中的位置不分先后,一般放在类体的前面部分
友元 不是 声明该友元的 类的 成员,它 是 一个获得访问权限的 该类之外的 成分
分类
友元类
声明形式
friend class 类型名;
注意:关键字class也要一起写
例:
class A { friend class B; //声明 友元类 private: int x; int y; }; class B { private: int z; public: int GetSum(A a) { return a.x + a.y + z; //直接访问类A的非公有成员 } };
友元函数
声明形式
friend 函数原型;
注意:
若要声明一个友元成员函数,则函数原型的函数名称前需要添加类作用域限定
例:
class B; //前向声明类B,供类A的 成员函数GetSum 声明时使用 class A { private: int x; int y; public: int GetSum(B b); }; class B { friend int A::GetSum(B b); //声明 友元成员函数 private: int z; }; int A::GetSum(B b) //类B定义后,再定义类A的 成员函数GetSum { return x + y + b.z; //直接访问类B的非公有成员 }
例:
class A { friend int CalcSum(A a); //声明 友元函数 private: int x; int y; }; int CalcSum(A a) { return a.x + a.y; //直接访问类A的非公有成员 }
继承
概念
来源
因为类是在对象之上的抽象,所以一些类之间存在着“is-a 关系”。 即特殊和一般的关系。一些类是某个类的特殊情况(子类(派生类(derived class))),某个类是一些类的一般情况(父类(基类(base class)))。 在C++中,使用继承的语法来表达这种关系。
作用
是父类和子类之间共享数据和方法的机制
例: 通过C语言结构体简单实现面向对象中单重继承的概念: struct Shape //===一般情况==== { int length; int width; }; struct Box //===特殊情况=== { struct Shape shape; //(1)“继承部分” int hight; //(2)“增加部分” };
子类首先会把父类所定义的内容作为自己的内容(继承部分)(基类子对象)
但不能继承
基类的构造函数和析构函数
基类的重载操作符
基类的友元函数
静态成员
子类还可以加入若干新的内容(增加部分)(派生子对象)
可以新增
增加新的成员变量和成员函数
改变基类成员在派生类中的访问属性
重定义(隐藏)(redefining)基类的成员函数,(针对作用域方面)
即派生类的成员函数屏蔽了与其有相同函数名称的基类成员函数。
重写(覆盖)(override)基类的成员函数,(针对虚函数表方面)
要求: 基类成员函数必须有virtual修饰,且派生类重写的成员函数的返回值类型、函数名称、参数列表要与基类的相同。当返回值类型为指针或引用时,可允许不同,但要支持协变。
继承语法
类派生列表
语法
派生类通过类派生列表指出它是由哪些基类继承而来
以“:”引导,后跟基类列表
形式
普通的继承
class 派生类名 : [继承方式] 基类名1, [继承方式] 基类名2, ... { 派生类增加的内容... };
虚拟继承
class 派生类名 : virtual [继承方式] 基类名1, virtual [继承方式] 基类名2, ... { 派生类增加的内容... };
例:
class Base1 { }; class Base2 { }; class Base3 { }; class Derived : public Base1, protected Base2, private Base3 { };
继承方式
不同继承方式对成员代码上的访问权限做了相应限制。不过,从内存视图看,派生类是包含基类的所有成员的。
public
派生类继承基类的public/protected成员,不会更改继承来的成员的访问权限
protected
派生类继承基类的public/protected成员,并将继承来的成员的访问权限降低为protected
private
派生类继承基类的public/protected成员,并将继承来的成员的访问权限降低为private
final 声明
C++11支持。
作用
声明为“final”的类类型将不能被继承
用法
在类类型定义的类型名后面添加关键字 final
例:
class NoDerived final { }; class Derived : NoDerived //报错。无法从“NoDerived”继承,因为它已被声明为“final” { };
单继承
派生类只有一个基类
示例:
#include <iostream> class Parent { protected: void Test1() { std::cout << "父类Test1" << std::endl; } void Test2() { std::cout << "父类Test2" << std::endl; } public: void Test3() { std::cout << "父类Test3" << std::endl; } }; class Child : public Parent //单继承 { public: void Test2() //重定义父类的成员函数,且可以更改其访问权限 { std::cout << "子类Test2" << std::endl; } void Test3() //重定义父类的成员函数 { std::cout << "子类Test3" << std::endl; } void Test4() { Test1(); //调用继承来的父类的成员函数 Test2(); //调用子类重定义的成员函数 Parent::Test2(); //通过类作用域限定,显式调用继承来的父类的成员函数 std::cout << "子类Test4" << std::endl; } }; int main() { Child ch; //ch.Test1(); //外部不可访问 ch.Test2(); //输出:子类Test2 ch.Test3(); //输出:子类Test3 ch.Test4(); //输出:父类Test1、子类Test2、父类Test2、子类Test4" ch.Parent::Test3(); //通过类作用域限定,显式调用继承来的父类的成员函数;输出:父类Test3 return 0; }
多继承
派生类有多个基类
派生类能够继承多个基类的成员
当不同基类拥有同名成员时,容易产生二义性的命名冲突
示例:
#include <iostream> class A { public: void F1() { std::cout << "From A" << std::endl; } }; class B { public: void F1() { std::cout << "From B" << std::endl; } }; class MI : public A, public B //多继承 { public: void F2() { } }; int main() { MI mi; //mi.F1(); //报错;存在二义性。编译器无法确定应调用哪个基类的成员函数 mi.A::F1(); //通过类作用域限定,显式调用继承来的某基类的成员函数 return 0; }
虚拟继承
引入原因
多继承的问题
派生类成员函数调用查找顺序
1.在派生类中查找
2.在基类中查找
在单继承中,该方法能正确找到成员函数所属的类
在多继承中,当一个类从多个基类派生,这些基类又从同一个基类派生时,该方法会产生成员名称的二义性
示例:
class Person //Person是StuEmployee的间接基类,也是Student、Employee的直接基类。Student、Employee均从Person派生,所以Person也称为它们的公共基类 { private: int age; public: void SetAge(int value) { age = value; } int GetAge() { return age; } }; class Student : public Person { }; class Employee : public Person { }; class StuEmployee : public Student, public Employee { }; int main() { StuEmployee se; int value = 0; size_t z = sizeof(se); //z = 8 //se.SetAge(0); //报错;存在二义性。编译器无法确定应调用哪个基类的成员函数 se.Student::SetAge(1); //通过类作用域限定,显式调用继承来的某基类的成员函数 se.Employee::SetAge(2); //通过类作用域限定,显式调用继承来的某基类的成员函数 value = se.Student::GetAge(); //value = 1 value = se.Employee::GetAge(); //value = 2。对象se的age成员有两个不同的值,出现了数据不一致的现象 StuEmployee* pderive2 = &se; //pderive2 = 0x0039fe90 Person* pbase; //pbase = &se; //报错;存在二义性。编译器无法确定应使用派生类对象中哪个间接基类对象的地址 pbase = (Student*)&se; //pbase = 0x0039fe90。通过类作用域限定,告诉编译器使用派生类对象中某基类中的间接基类对象的地址 pbase = (Employee*)&se; //pbase = 0x0039fe94。通过类作用域限定,告诉编译器使用派生类对象中某基类中的间接基类对象的地址 return 0; } 
类继承示意图
或 该类型的继承图像一个菱形,所以也称为“菱形继承”。
对象结构示意图
  Visual Studio 的 Developer 命令行工具的命令: cl "源代码文件路径" /d1reportSingleClassLayout类名 可以查看类内存布局。
从图中可以看出,同一个对象se中存在Person的两份不同的对象
不仅浪费存储空间
而且还容易产生数据的不一致性
为了解决多继承的命名冲突和冗余数据问题,C++ 提出了虚拟继承,使派生类对象中只保留一份(共享的)间接基类对象
使用
用关键字 virtual 修饰继承,将(公共)基类指定为虚基类,实现虚拟继承
示例:
class Person { private: int age; public: void SetAge(int value) { age = value; } int GetAge() { return age; } }; class Student : virtual public Person //声明Person为虚基类 { }; class Employee : virtual public Person //声明Person为虚基类 { }; class StuEmployee : public Student, public Employee { }; int main() { StuEmployee se; int value = 0; size_t z = sizeof(se); //z = 12 se.Student::SetAge(1); //通过类作用域限定,显式调用继承来的某基类的成员函数 se.Employee::SetAge(2); //通过类作用域限定,显式调用继承来的某基类的成员函数 value = se.Student::GetAge(); //value = 2 value = se.Employee::GetAge(); //value = 2。从对象se的两个基类作用域获取的age成员值均相同 se.SetAge(3); //直接调用继承来的间接基类的成员函数 value = se.GetAge(); //value = 3 StuEmployee* pderive2 = &se; //pderive2 = 0x00dff7cc Person* pbase; pbase = (Student*)&se; //pbase = 0x00dff7d4。通过类作用域限定,获取派生类对象里间接基类对象的地址 pbase = (Employee*)&se; //pbase = 0x00dff7d4。通过类作用域限定,获取派生类对象里间接基类对象的地址 pbase = &se; //pbase = 0x00dff7d4。直接获取派生类对象里间接基类对象的地址 return 0; }  在这里,vbptr(virtual base table pointer)虚基类表指针指向了一个虚基类表(virtual base table),虚表中第一项表示该基类的vbptr与基类对象之间的偏移量,第二项表示该基类的vbptr与虚基类对象之间的偏移量。
类继承示意图

对象结构示意图
 
赋值兼容规则
原因
派生类对象中含有基类子对象
概念
在需要基类对象的地方都可以用public继承方式的派生类对象替代
情况
派生类对象 赋值给 基类对象
C++采用对象切割(Object Slicing)的方法从派生类对象中取出基类子对象,并将之赋值给基类对象。
派生类对象的地址 赋值给 基类指针
派生类对象 作为 基类对象的引用
虚函数
引入原因
针对赋值兼容规则中的情况
不论哪种方式,通过基类类型的指针或引用都只能访问到派生类对象从基类中继承到的成员
即使可以将基类类型的指针或引用强制转换为派生类类型来访问派生类对象的成员,但这样使用不够灵活
为了通过基类类型的指针或引用能访问到派生类对象的成员函数,C++给出了虚函数的解决方案
定义
用关键字 virtual 修饰(非静态)成员函数
virtual用于指示编译器,对该函数采取迟后联编(动态绑定)的方法,在程序运行时确定实际调用的函数。 没有使用virtual修饰的函数,编译器采取早期联编(静态绑定)的方法,在编译时就确定调用的函数。 一旦将某个类的成员函数声明为虚函数后,该成员函数在派生类的继承体系中就永远为虚函数了。
使用
(1)派生类重写基类的虚函数
(2)基类类型的指针或引用,指向或绑定到到派生类对象
(3)通过基类类型的指针或引用,调用基类的虚函数
(4)程序在运行时,会调用指针或引用所实际指向或绑定的对象类型的成员函数
示例:
class Parent { public: int a; virtual ~Parent() { } virtual int GetValue1() { return 0x01; } virtual int GetValue2() { return 0x02; } int GetValue3() { return 0x03; } }; class Child : public Parent { public: virtual ~Child() { } int GetValue1() { return 0x11; } }; int main() { size_t sz = 0; int res = 0; Parent* p = new Child; sz = sizeof(p); //sz = 4 sz = sizeof(*p); //sz = 8 delete p; //先调用~Child(),再调用~Parent() Child c2; Parent* p2 = &c2; Parent& r2 = c2; sz = sizeof(c2); //sz = 8 sz = sizeof(p2); //sz = 4 sz = sizeof(*p2); //sz = 8 p2->a = 1; res = p2->GetValue1(); //res = 0x11。调用指针所实际指向的对象类型的成员函数 res = r2.GetValue1(); //res = 0x11。调用引用所实际绑定的对象类型的成员函数 res = p2->GetValue2(); //res = 0x02 res = p2->GetValue3(); //res = 0x03 return 0; }
对象结构示意图
 在这里,vfptr(virtual function table pointer)虚函数表指针指向了一个虚函数表(virtual function table)。 Parent类的vfptr指向的vftable中,第一项表示该类的虚析构函数地址,第二项表示该类的虚函数GetValue1地址,第三项表示该类的虚函数GetValue2地址。 Child类的vfptr指向的vftable中,第一项里派生类的虚析构函数地址覆盖了基类的虚析构函数地址,第二项里派生类的虚函数GetValue1地址替换了基类的虚函数GetValue1地址,第三项里仍是基类的虚函数GetValue2地址。
final 和 override 声明
C++11支持。
final
作用
基类中声明为“final”的成员函数无法被派生类重写
用法
写在需要使用的成员函数声明最后
override
作用
派生类中声明为“override”的成员函数必须重写基类的成员函数
这一作用可以辅助我们发现编码中的错误。
用法
写在需要使用的成员函数声明最后
例:
class Parent { public: virtual int GetValue1() { return 0x01; } virtual int GetValue2() final { return 0x02; } }; class Child : public Parent { public: int GetValue1(int a) override //报错。包含重写说明符“override”的成员函数没有重写任何基类成员函数 { return 0x11; } int GetValue2() //报错。基类中声明为“final”的成员函数无法被派生类重写 { return 0x12; } };
纯虚函数与抽象类
纯虚函数
定义
在虚函数声明后面加“= 0”
“= 0”可以理解为将指向函数体的指针初始化为NULL。
说明
一般的纯虚函数不能有定义
例:
class CmdHandler { public: virtual void OnCommand(char* cmdLine) = 0; };
纯虚析构函数需要有定义
使得从派生类到基类的逐层析构能顺利执行
例:
class IPaper { public: virtual ~IPaper() = 0 {}; //给纯虚析构函数加上函数体 {} }; 或 分离式写法: class IPaper { public: virtual ~IPaper() = 0; }; IPaper::~IPaper() { }
其他概念
包含纯虚函数的类称为纯虚类
纯虚函数也称为抽象函数
纯虚类也称为抽象类
抽象类不能被实例化,只能被继承
如果派生类没有定义其所有的抽象函数,则派生类仍是抽象类
在C++中,抽象函数用于实现设计模式里“接口”的概念
组合
概念
来源
因为类是在对象之上的抽象,所以一些类之间存在着“has-a 关系”。 即整体和部分的关系。一个类拥有另外一些类。
例:
class Color //===部分=== { private: int red; int green; int blue; }; class Box //===整体=== { private: Color color; //Box类拥有一个Color类型的成员变量 };
应用
在软件工程中实现复用一般优先采用对象的组合,而不是类的继承。
6. 操作符重载
基本概念
操作符
操作符是一种特殊的函数,称为操作符函数
操作符重载
操作符重载是一种特殊的函数重载,它的函数名称由关键字 operator 和其后要重载的操作符共同组成
操作符重载函数
参数个数与操作符操作的操作数个数相同
单目操作符
参数有1个
双目操作符
参数有2个
对于全局操作符重载函数
左侧操作数传递给第1个参数
右侧操作数传递给第2个参数
对于类成员操作符重载函数(显式声明的参数个数只有1个)
左侧操作数绑定到隐含的this指针指向的对象上
右侧操作数传递给显式声明的第1个参数
参数类型至少要有一个是类类型或枚举类型
注意
合理地重载操作符
操作符重载为类成员还是非类成员函数?
下标“[]”、调用“()”、访问“->”、赋值“=”必须是类成员函数
复合赋值操作符、改变对象状态的操作符、与给定类型密切相关的操作符一般是类成员函数
具有对称性的操作符一般是非类成员函数
示例:
单目自增操作符重载
class Fraction { friend Fraction& operator--(Fraction& origin); //操作该类的全局操作符重载函数最好声明为友元函数 friend Fraction operator--(Fraction& origin, int); //操作该类的全局操作符重载函数最好声明为友元函数 public: int num; int den; Fraction() : num(1), den(1) {} Fraction(int n, int d) : num(n), den(d) {} //类成员操作符重载函数 Fraction& operator++() //前置自增 { this->num = this->num + this->den; return *this; } Fraction operator++(int) //后置自增。1个int类型的形式参数告诉编译器这是后置自增操作符重载函数 { Fraction res = *this; ++(*this); return res; } }; //全局操作符重载函数 Fraction& operator--(Fraction& origin) //前置自减 { origin.num = origin.num - origin.den; return origin; } Fraction operator--(Fraction& origin, int) //后置自减 { Fraction res = origin; --origin; return res; } int main() { Fraction a(2, 3); ++a; //调用 Fraction::operator++() --a; //调用 operator--(Fraction& origin) a++; //调用 Fraction::operator++(int) a--; //调用 operator--(Fraction& origin, int) a.operator++(); //显式调用 Fraction::operator++() a.operator++(0); //显式调用 Fraction::operator++(int),要给int形参传一个值,使编译器匹配后置版本操作符重载函数 operator--(a); //显式调用 operator--(Fraction& origin) operator--(a, 0); //显式调用 operator--(Fraction& origin, int) return 0; }
双目算术操作符重载
class Fraction { friend Fraction operator+(const Fraction& left, const Fraction& right); //操作该类的全局操作符重载函数最好声明为友元函数 public: int num; int den; Fraction() : num(1), den(1) {} Fraction(int n, int d) : num(n), den(d) {} Fraction operator+(const Fraction& other) //类成员操作符重载函数 { Fraction res; res.den = this->den * other.den; res.num = this->num * other.den + this->den * other.num; return res; } }; Fraction operator+(const Fraction& left, const Fraction& right) //全局操作符重载函数 { Fraction res; res.den = left.den * right.den; res.num = left.num * right.den + left.den * right.num; return res; } int main() { Fraction a(2, 3); Fraction b(4, 5); Fraction c; c = a + b; c = a.operator+(b); //显式调用类成员操作符重载函数 c = operator+(a, b); //显式调用全局操作符重载函数 return 0; }
类型转换操作符重载
形式
operator type() const { return type类型的数据; }
一般声明为const函数。
注意
类型转换函数原型无形式参数
类型转换函数原型无返回值类型
类型转换函数需要返回将要转换成的类型
例:
class Fraction { public: int num; int den; Fraction() : num(1), den(1) {} Fraction(int n, int d) : num(n), den(d) {} explicit operator double() const //类型转换类成员操作符重载函数。添加 explicit 修饰可以要求显式转换,以防止意外的隐式转换 { return (double)this->num / this->den; } }; int main() { Fraction a(2, 3); double d = 0; //d = a; //未显式转换报错 d = (double)a; d = a.operator double(); //显式调用类型转换类成员操作符重载函数 return 0; }
函数对象
概念
如果类重载了调用操作符,则我们可以像函数一样使用该类的对象
该类的对象称为函数对象
因为是类,所以还能存储状态
例:
#include <string> class SizeComp { private: size_t sz; public: SizeComp(size_t n) : sz(n) {} bool operator()(const std::string& s) const //调用操作符重载 { return s.size() >= sz; } }; int main() { SizeComp sc(10); std::string str1 = "123"; std::string str2 = "1234567890abcdef"; bool res1 = sc(str1); //res1为false bool res2 = sc(str2); //res2为true res1 = sc.operator()(str1); //显式调用 res2 = sc.operator()(str2); //显式调用 return 0; }
应用
Lambda表达式
C++11中的Lambda表达式底层就是用函数对象实现的。
形式
[捕获列表](函数参数列表) mutable 异常类型 属性 -> 返回值类型 {函数体}
说明: (1)“捕获列表”中捕获的变量被传给Lambda函数对象的构造函数,从而可以在Lambda“函数体”中使用。 (2)mutable声明:当捕获列表中的变量采用值捕获方式时,使得Lambda函数体可以修改传递进来的复制值。因为Lambda内部实现的()操作符重载函数默认是const成员函数。 (3)若不需要,“(函数参数列表)”、mutable、异常类型、属性、尾置返回类型“-> 返回值类型”可以省略。如果函数体只有一条return语句,Lambda可以自动推断出返回值类型。
捕获列表
[]
空捕获列表
捕获列表为空的Lambda表达式可以被转换为一个可以指向相同函数原型函数的函数指针。
[要捕获的变量列表]
默认采用值捕获方式捕获列表中的变量;若某个变量前加了&,则对这个变量采用引用捕获方式
[=]
隐式捕获列表,采用值捕获方式自动捕获Lambda函数体中所使用的来自外部的变量
[&]
隐式捕获列表,采用引用捕获方式自动捕获Lambda函数体中所使用的来自外部的变量
[=, 显式采用引用方式捕获的变量列表(变量前要加&)]
默认采用值捕获方式隐式捕获变量;显式声明的变量采用引用捕获方式
[&, 显式采用值方式捕获的变量列表]
默认采用引用捕获方式隐式捕获变量;显式声明的变量采用值捕获方式
例:
#include <string> #include <functional> int c = 9; int main() { auto f = [] {return 1; }; int a = f(); //a = 1 int(*pf)() = f; //将Lambda表达式传递给函数指针 a = pf(); size_t sz = 10; auto sizeComp = [sz](const std::string& s) -> bool { return s.size() >= sz; }; //捕获列表中可以捕获局部变量 std::string str1 = "123"; std::string str2 = "1234567890abcdef"; bool res1 = sizeComp(str1); //res1为false bool res2 = sizeComp(str2); //res2为true std::function<int(int, int)> f1 = [a](int v1, int v2) mutable -> int { v1 += v2; a += v1; //修改值捕获方式捕获的变量(不影响外部被捕获的变量) return a; }; int b = f1(2, 3); //b = 6 auto f2 = [=] { int v1 = a + c; }; f2(); auto f3 = [&] { a = 2; }; f3(); auto f4 = [=, &b] {int v1 = a; b = a; }; f4(); auto f5 = [&, b] {int v1 = b; a = b + 1; }; f5(); return 0; }
std::function
C++11支持。 头文件:<functional>
一个通用的多态函数包装器
std::bind
C++11支持。 头文件:<functional>
相当于一个函数适配器,接受一个可调用对象,而生成一个新的可调用对象来适配原来的参数列表
7. 内部类、命名空间
内部类
概念
一个声明或定义在另一个类内部的类,称为内部类或嵌套类
作用
主要用于避免类名冲突
使用
内部类的访问权限为public时,可以在外部使用内部类,不过需要添加其外部类的类作用域限定
在访问外部类的成员权限方面,内部类类似于外部类的友元类
命名空间
作用
用于避免名称(函数、类、变量等)冲突
定义
格式
namespace 命名空间名称 { }
注意
只能在全局作用域层级定义
命名空间可以嵌套
可以从多处向同一个命名空间添加成员
例: namespace test { int a; } namespace test { int b; } int main() { test::a = 1; test::b = 2; return 0; }
使用
使用命名空间中的成员,需要在成员名称前添加命名空间作用域限定
using
使用 using 指示或声明告诉编译器,后续的代码将使用指定的命名空间中的名称
头文件代码中,一般不应(在全局作用域层级)包含 using 指示或声明,防止头文件被包含时引起命名冲突。
方式
(using指示)使用整个命名空间中的名称
格式
using namespace 命名空间名称;
例:
using namespace std;
(using声明)使用命名空间中的某个名称
格式
using 命名空间名称::成员名称;
例:
using std::cout;
命名空间别名
作用
为命名空间名称设置同义词
格式
namespace 别名 = 原命名空间名称;
例:
namespace test { int a; } namespace t1 = test; //创建test命名空间的别名t1 namespace t2 = test; //创建test命名空间的别名t2 int main() { t1::a = 1; t2::a = 2; return 0; }
8. 模板
概念
模板(template)
是C++实现代码重用机制的重要工具
是泛型技术(即与数据类型无关的通用程序设计技术)的基础
定义
关键字 template 后跟模板参数列表
模板参数列表
是一个“<>”号括起的逗号分隔的一个或多个模板参数的列表
模板参数
由 typename 或 class 修饰的模板参数称为类型参数
在模板实例化时被具体类型替换
由具体类型名称修饰的模板参数称为非类型参数
在模板实例化时只能被常量值替换
例:
template <typename T, int N> //T为类型参数,N为非类型参数 int Compare(T v1) { if (v1 < N) return -1; if (N < v1) return 1; return 0; } int main() { int a = Compare<double, 100>(99.9); //a = -1 a = Compare<double, 100>(100.1); //a = 1 a = Compare<double, 100>(100.0); //a = 0 return 0; }
形式
template <typename T1, typename T2, ...> ...
分类
函数模板
template <typename T1, typename T2, ...> 返回值类型 函数名称(参数列表) { ... }
类模板
template <typename T1, typename T2, ...> class 类名称 { ... };
实例化
概念
编译器将模板实参对应绑定到模板参数,生成一个具体的函数或类的过程
在代码分离式写法中,模板的声明和实现一般都写在头文件里。
关系图

指定模板参数
隐式
在函数模板调用语句中,编译器可以根据函数实参类型推断出模板实参
例:
template <typename T> T Max(T a, T b) { return (a > b) ? a : b; } int main() { double a = 0; a = Max(1, 2); //隐式函数模板实参调用。编译器根据函数实参类型自动推断出模板实参为int return 0; }
显式
显式模板实参列表
是一个“<>”号括起的逗号分隔的一个或多个模板实参的列表
例:
template <typename T> T Max(T a, T b) { return (a > b) ? a : b; } template <typename T> class Test { public: size_t Size() { return sizeof(T); } }; int main() { double a = 0; a = Max<double>(1, 2); //显式函数模板实参调用。<double>是显式模板实参列表,显式指定模板实参类型为double Test<double> td; //<double>是显式模板实参列表 a = td.Size(); return 0; }
分类
隐式实例化
概念
在使用函数模板和类模板,且不存在指定类型的函数模板和类模板的实体时,由编译器根据指定模板实参隐式生成函数模板或者类模板的实体的过程,称之为模板的隐式实例化。
例:
template <typename T> T Max(T a, T b) { return (a > b) ? a : b; } template <typename T> class Test { public: size_t Size() { return sizeof(T); } }; int main() { double a = 0; a = Max<double>(1, 2); //隐式实例化 Test<double> td; //隐式实例化 a = td.Size(); return 0; }
显式实例化
概念
在未发生函数调用时将函数模板实例化,或者在不使用类模板的时候将类模板实例化,称之为模板显式实例化。
方法
通过显式实例化声明将模板实例化
例:
template <typename T> T Max(T a, T b) { return (a > b) ? a : b; } template <typename T> class Test { public: size_t Size() { return sizeof(T); } }; template double Max<double>(double, double); //显式实例化 template class Test<double>; //显式实例化 int main() { double a = 0; a = Max(1.1, 2.1); //将会调用函数模板的显式实例化的实例 Test<double> td; //将会使用类模板的显式实例化的实例 a = td.Size(); return 0; }
函数模板
template 返回值类型 函数名称<模板实参列表>(参数列表);
注:模板参数已被替换为模板实参。
类模板
template class 类名称<模板实参列表>;
注:模板参数已被替换为模板实参。
extern 声明
C++11支持。
引入原因
当同一模板在多个源文件中使用且提供相同模板实参时,则每个源文件中都会产生该模板的相同实例。在大系统中,这会带来额外开销。因此,引入 extern 声明来解决该问题。
例:
//===template.h=== #ifndef TEMPLATE_H #define TEMPLATE_H template <typename T> T Max(T a, T b) { return (a > b) ? a : b; } template <typename T> class Test { public: size_t Size() { return sizeof(T); } }; #endif //====== //===other.cpp=== #include "template.h" template double Max<double>(double, double); //显式实例化 template class Test<double>; //显式实例化 //====== //===main.cpp=== #include "template.h" extern template double Max<double>(double, double); //显式实例化 extern 声明 extern template class Test<double>; //显式实例化 extern 声明 int main() { double a = 0; a = Max(1.1, 2.1); Test<double> td; a = td.Size(); return 0; } //======
显式具体化
概念
对于某些特殊类型,可能不适合现有模板实现,需要重新定义实现,此时可以使用显式具体化(Explicit Specialization)。
分类
全特化
说明
即指定了模板实参列表中的所有参数
函数模板
template <> 返回值类型 函数名称<模板实参列表>(参数列表) { ... }
注:模板参数已被替换为模板实参。
类模板
template <> class 类名称<模板实参列表> { ... };
注:模板参数已被替换为模板实参。
偏特化
说明
即只指定了模板实参列表中的部分参数
只有类模板支持偏特化
例:
template <typename T> T Max(T a, T b) { return (a > b) ? a : b; } template <typename T> class Test { public: size_t Size() { return sizeof(T); } }; template <> double Max<double>(double a, double b) //显式具体化(全特化) { return (a > b) ? a : b; } template <> class Test<double> //显式具体化(全特化) { public: size_t Size() { return sizeof(double); } }; //==================== template <typename T1, typename T2> class Test2 { public: size_t Size() { return sizeof(T1) + sizeof(T2); } }; template <typename T2> class Test2<double, T2> //显式具体化(偏特化),已指定模板参数T1为double { public: size_t Size() { return sizeof(double) + sizeof(T2); } }; int main() { double a = 0; a = Max(1.1, 2.1); //将会调用函数模板的显式具体化的实例 Test<double> td; //将会使用类模板的显式具体化的实例 a = td.Size(); Test2<double, int> t2; //将会使用类模板的显式具体化的实例 a = t2.Size(); return 0; }
9. 异常
概念
C++的异常处理机制能将异常检测与异常处理分离开来,当异常发生时,它能自动调用异常处理程序进行错误处理。
异常处理组成
throw 表达式
作用
程序异常检测部分使用 throw 表达式引发一个异常
抛出的异常必须被捕获,若本层没有匹配的 catch 子句,异常将会被系统按调用链往上逐层尝试匹配。
使用
throw 异常对象;
异常对象可以是任何类型的对象。
try 语句块
组成
try 块
作用
检测可能发生的异常
同一 try 块可以抛出多种不同类型的异常,所以一般应有多个 catch 子句与之对应
catch 子句
作用
捕获异常声明中所声明类型的异常并处理
一个 catch 子句只能捕获一种类型的异常
异常声明
分类
单个异常类型变量声明
单个异常类型声明
...
表示捕获任何类型的异常
再次抛出异常
使用不带异常对象的 throw 语句
形式
try { } catch (异常声明) { } catch (异常声明) { } ......
一套异常类
标准异常
exception
最通用的异常类
头文件:<exception> 命名空间:std
例:
#include <exception> void Test(int sel) { try { switch (sel) { case 0: throw 1; break; case 1: throw 1.0; break; case 2: throw std::exception(); break; default: throw "未知异常"; break; } } catch (int v) //单个异常类型变量声明 { } catch (double) //单个异常类型声明 { } catch (const std::exception& ex) { } catch (...) //捕获任何类型的异常 { throw; //再次抛出异常 } } int main() { for (int i = 0; i < 4; i++) { Test(i); } return 0; }
10. 标准模板库
概念
标准模板库(Standard Template Library,STL)是基于模板技术的一个库,它提供了许多模板化的通用数据结构、类和算法等。
主要内容
容器(container)
分类
顺序容器
vector
list
deque
关联容器
set
multiset
map
multimap
容器适配器
stack
queue
迭代器(iterator)
说明
迭代器用于遍历容器
iterator 一般作为容器的内部类定义
算法(algorithm)
内容
find
count
search
merge
sort
扩展
1. Windows
链接库
概念
分类
动态链接
动态链接所调用的函数代码并没有被复制到应用程序的可执行文件中去,而是仅仅在其中加入了所调用函数的描述信息(往往是一些重定位信息)。仅当应用程序被装入内存开始运行时,在Windows的管理下,才在应用程序与相应的DLL之间建立链接关系。当要执行所调用DLL中的函数时,根据链接产生的重定位信息,Windows才转去执行DLL中相应的函数代码。 注:允许可执行模块(.dll文件或.exe文件)仅包含在运行时定位DLL中函数的可执行代码所需的信息。
在程序运行时,将程序中的函数调用等与dll中的函数体等链接起来
相关文件
导入库
import library
包含
指令、数据等的索引信息
作用
程序开发时,给链接器提供信息,以建立.exe文件中用于动态链接的重定位表格
扩展名
一般为 .lib
动态链接库
dynamic-link library
包含
实际的指令、数据等
作用
程序运行时,动态链接后执行相关指令等使用
扩展名
一般为 .dll
扩展名为.dll时才可以被Windows系统自动加载。
共享特性
指令部分
当.dll被加载时,一般映射到进程虚拟空间的同一地址上,即只被加载一次,是共享的
数据部分
普通数据
当dll被加载时,被各个程序各自复制一份,是私有的
共享数据
若通过特殊的编译设置指定了共享数据,则可被所有链接该.dll的程序共享
文件部署位置
与.exe相同的目录
或系统的其他dll文件搜索目录
静态链接
静态链接是由链接器在链接时将库的内容加入到可执行文件中的做法。链接器是一个独立程序,将一个或多个库或目标文件(先前由编译器或汇编器生成)链接到一起生成可执行文件。静态链接把要调用的函数等链接到可执行文件中,成为可执行文件的一部分。
在程序开发时,将程序中的函数调用等与lib中的函数体等链接在一起
相关文件
对象库
object library
包含
指令、数据等的索引信息
实际的指令、数据等
作用
程序开发时,里面的内容被链接器添加到.exe文件中
扩展名
一般为 .lib
共享特性
指令部分
被链接到.exe里了,是私有的
数据部分
普通数据
被链接到.exe里了,是私有的
共享数据
若通过特殊的编译设置指定了共享数据,则在链接该.lib的程序中,可以被同一个.exe的各个进程共享
分类
动态链接库
Dynamic-Link Library
建立项目
VS2010步骤
(1)文件→新建→项目
(2)Visual C++→Win32控制台程序→输入名称→确定
(3)在向导界面中设置应用程序类型为“DLL”→完成
关于“预编译头”
所谓头文件预编译,就是把一个工程(Project)中使用的一些标准头文件(如windows.h)预先编译。以后该工程编译时,不再编译这部分头文件,仅仅使用预编译的结果。这样可以加快编译速度,节省时间。 预编译头输出文件通过编译stdafx.cpp生成,一般以工程名命名,如:ProjectName.pch。 编译器通过一个头文件stdafx.h来使用预编译头输出文件。stdafx.h这个头文件名可以在工程属性的预编译头设置里指定。编译器认为,所有在指令 #include "stdafx.h" 前的代码都是预编译的,它将跳过 #include "stdafx. h" 指令,使用预编译头输出文件编译这条指令之后的所有代码。因此,若使用了预编译头,则工程中所有的cpp文件第一条语句都要是:#include "stdafx.h"。 参考资料: https://www.cnblogs.com/lidabo/archive/2012/11/21/2781470.html
导出与导入
分类
导出符号
__declspec 方式
对于导出,__declspec(dllexport) 会将导出指令添加到目标文件中,因此可以不再使用 .def 文件。
关于 __declspec 关键字
是用于指定给定类型实例的Microsoft专用存储类型特性的扩展特性语法
格式
__declspec(扩展修饰符序列)
扩展修饰符序列的元素之间使用空格分隔。
一般应将 __declspec 关键字放在声明的开头
例:
在类型声明的开头指定 __declspec,扩展特性将应用于该类型的实例。 例如: __declspec(dllimport) int a; //特性应用于变量a 在 class 或 struct 关键字后与类名之前指定 __declspec,扩展特性将应用于该类型。 例如: class __declspec(dllimport) X{}; //特性应用于类型X
导出变量
例:
__declspec(dllexport) int a; //导出变量定义。__declspec(dllexport)默认表示定义 extern __declspec(dllexport) int a; //导出变量声明。需使用 extern 关键字强制进行声明
导出函数
例:
__declspec(dllexport) int Add(int a, int b); //导出函数声明
导出类
例:
class __declspec(dllexport) Test //导出类声明 { public: int a; int Calc(int v); }; //通过以上方式导出类会导出类中的所有公共数据成员和成员函数,导致导入方对类的实现依赖较多,对导入方的编译环境也可能有更高的要求,降低了dll的通用性。 //更好的方式是dll只对外部公开一个抽象类(可以合理利用其vfptr,从而不用导出),内部创建派生类实例,通过导出函数传递实例指针给外部,外部只需要根据抽象类提供的接口操作该实例。 参考资料: https://www.cnblogs.com/lidabo/p/7121745.html
DEF 文件方式
.def 文件声明导出符号后,代码中就可以不再使用__declspec(dllexport)关键字声明导出符号。不过,在 .def 文件的基础上另外使用 __declspec(dllexport) 不会导致生成错误。 许多导出指令(如序号、NONAME 和 PRIVATE)只能在 .def 文件中创建,并且必须使用 .def 文件来指定这些特性。 在库的使用方,仍需要使用__declspec(dllimport)关键字声明导入符号,否则将可能不会生成更好的代码,甚至出错(如使用未声明导入的导出变量)。
关于 DEF 文件
模块定义 (.def) 文件为链接器提供有关被链接程序的导出、特性及其他方面的信息
语法规则
语句、特性关键字和用户指定的标识符区分大小写; 如果使用 NAME 或 LIBRARY 语句,则这些语句必须位于所有其他语句之前; 包含空格或分号(;)的长文件名必须用双引号(")引起; 如果字符串参数与保留字匹配,则必须用双引号(")将字符串参数引起; 以十进制或十六进制指定数值参数; 指定参数的冒号(:)或等号(=)两旁要有零个或多个空格、制表符或换行符; 使用一个或多个空格、制表符或换行符,将语句关键字同其参数分开和将各语句分开; 在 .def 文件中,SECTIONS 和 EXPORTS 语句可以出现多次。 每个语句都可以采用多个规范,各规范间必须用一个或多个空格、制表符或换行符分开。语句关键字必须在第一个规范的前面出现一次,并且可以在每个附加规范的前面重复; .def 文件中的注释由每个注释行开始处的分号(;)指定。 注释不能与语句共享一行,但可以在多行语句的规范间出现。( SECTIONS 和 EXPORTS 为多行语句。) 许多语句都具有等效的 LINK 命令行选项。
关于dll的主要语句
LIBRARY
作用
告知 LINK 创建 DLL、导入库
格式
LIBRARY [library][BASE=address]
library 参数指定 DLL 的名称。也可以使用 /OUT 链接器选项指定 DLL 输出名。 BASE= address 参数设置操作系统用来加载 DLL 的基址。该参数重写 0x10000000 的默认 DLL 位置。有关基址的详细信息,请参见 /BASE 选项说明。
EXPORTS
作用
引入一个由一个或多个导出定义组成的节,这些定义可指定函数或数据的导出名或序号
每个定义必须在单独一行上
格式
EXPORTS definition
definition格式
entryname[=internalname] [@ordinal [NONAME]] [[PRIVATE] | [DATA]]
entryname 是要导出的函数名或变量名,这是必选项。如果导出的名称与 DLL 中的名称不同,则使用 internalname 指定 DLL 中导出的名称。 由于 Visual C++ 编译器针对 C++ 函数使用名称重整,因此你必须将重整名用作 entryname 或 internalname。或在源代码中使用 extern "C" 定义导出函数。编译器还将重整使用 __stdcall 调用约定的 C 函数,其重整名称带有下划线(_)前缀和由at符号(@)后跟参数列表中的字节数采用十进制所组成的后缀。 若要查找由编译器产生的重整名,请使用 DUMPBIN 工具或链接器 /MAP 选项。重整名特定于编译器。如果要将重整名导出到 .DEF 文件中,则链接到 DLL 的可执行文件也必须通过使用同一版本的编译器生成。这可确保调用方中的重整名与 .DEF 文件中的导出名相匹配。 可以使用 @ordinal 指定序号而不是函数名将进入 DLL 的导出表。许多 Windows DLL 将导出序号以支持旧版代码。通常使用采用 16 位 Windows 编码的序号,因为这有助于最大程度地减小 DLL 的大小。除非 DLL 的客户端需要按序号导出函数以支持旧版,否则我们不建议你执行此操作。由于 .LIB 文件将包含序号与函数之间的映射,因此你可以像通常在使用 DLL 的项目中那样使用函数名。 通过使用可选 NONAME 关键字,你可以只按序号导出,并减小结果 DLL 中导出表的大小。但是,如果你想要在 DLL 上使用 GetProcAddress,你必须要知道序号,因为函数名将无效。 可选 PRIVATE 关键字禁止将 entryname 包含在由 LINK 生成的导入库中。它不会影响同样是由 LINK 生成的映像中的导出。 可选 DATA 关键字指定导出的是数据,而不是代码。
例:
LIBRARY dllcalc EXPORTS count3 scount6 Mul @1 Div=Divide @2
导入符号
__declspec 方式
符号不论是用 .def 文件导出还是用 __declspec(dllexport) 关键字导出的, __declspec(dllimport) 关键字均有效。
导入变量
例:
__declspec(dllimport) int a; //导入变量声明。__declspec(dllimport)默认表示声明 extern __declspec(dllimport) int a; //导入变量声明。可以使用 extern 关键字显式进行引用性声明
导入函数
例:
__declspec(dllimport) int Add(int a, int b); //导入函数声明
导入类
例:
class __declspec(dllimport) Test //导入类声明 { public: int a; int Calc(int v); };
DllMain函数
作用
dll的入口
在dll被加载或卸载等的时候,系统会以不同的参数调用该函数
dll可以在通过这个过程完成初始化或逆初始化
返回值
若函数中的操作成功完成,应该返回非0值
设置共享数据
(1)设定一个独立的数据段
提示:进程不同,地址空间也不同,所以在共享数据段共享指针是不安全的。
使用 #pragma data_seg 预编译指令
格式
#pragma data_seg([[{push | pop},] [identifier,] [[segment-name] [,"segment-class"]])
说明:没有参数的data_seg会将段重置为.data
(2)指定数据段特性为共享
使用 #pragma comment(linker, "/SECTION:数据段名,RWS") 预编译指令
库的使用
隐式链接
所需信息
开发时
导入库 .lib文件
运行时
动态链接库 .dll文件
使用步骤
(1)导入符号的声明
(2)指示编译器将库搜索记录放置到目标文件中
(3)在代码中使用导入符号
显式链接
所需信息
开发时
无
运行时
动态链接库 .dll文件
使用步骤
(1)使用WIndows API LoadLibrary(~) 加载dll
(2)
对于变量或函数
使用WIndows API GetProcAddress(~) 获取到dll中导出符号的地址后使用
对于资源
使用资源类型对应的WIndows API获取到相关信息后使用
(3)使用WIndows API FreeLibrary(~) 卸载dll
示例
DLL
头文件
dllcalc.h
#ifndef DLLCALC_H #define DLLCALC_H //#define STATICLIBRARY #ifdef STATICLIBRARY //是否用于静态链接库 #define DLLCALC_API #else //结合条件编译,实现本DLL项目中编译时,DLLCALC_API宏定义为导出,非本DLL项目中编译时,DLLCALC_API宏定义为导入 #ifdef DLLCALC_EXPORTS //VS建立的DLL项目会默认定义一个 工程名_EXPORTS 的宏 #define DLLCALC_API __declspec(dllexport) #else #define DLLCALC_API __declspec(dllimport) #endif #endif #ifdef __cplusplus #define EXTERNC extern "C" #else #define EXTERNC #endif //导出变量 EXTERNC extern DLLCALC_API int count1; extern DLLCALC_API int count2; extern DLLCALC_API int count3; //既使用__declspec(dllexport)关键字,又通过DEF文件导出 EXTERNC extern DLLCALC_API int scount4; extern DLLCALC_API int scount5; extern int scount6; //不使用__declspec(dllexport)关键字,通过DEF文件导出。隐式链接该库的程序仍需使用__declspec(dllimport)声明导入,否则可能出错 //导出函数 EXTERNC DLLCALC_API int __stdcall Abs(int a); //普遍情况下,DLL导出函数会添加链接指示 extern "C" 以及调用约定 __stdcall 修饰符。必要时还可以使用DEF文件设置导出函数的名称,以增强dll通用性 EXTERNC DLLCALC_API int Inc(int a); DLLCALC_API int Add(int a, int b); DLLCALC_API int Sub(int a, int b); int Mul(int a, int b); //不使用__declspec(dllexport)关键字,通过DEF文件导出。隐式链接该库的程序最好使用__declspec(dllimport)声明导入,以生成更好的代码 int Divide(int a, int b); //不使用__declspec(dllexport)关键字,通过DEF文件导出。隐式链接该库的程序最好使用__declspec(dllimport)声明导入,以生成更好的代码 #ifdef __cplusplus //导出类 class DLLCALC_API Book { public: int page; Book(); ~Book(); int Read(int to); }; //“导出类”更好的方法: //公开一个抽象类 class IPaper { public: virtual ~IPaper() = 0; virtual int Read(int to) = 0; }; //导出派生类的实例生成和消除函数 DLLCALC_API IPaper* CreateIPaperDObj(); DLLCALC_API void DestroyIPaperDObj(IPaper* pdobj); #endif #endif
DeriveIPaper.h
#include "dllcalc.h" class Magazine : public IPaper { public: int page; Magazine(); ~Magazine(); int Read(int to); };
targetver.h
#pragma once #include <SDKDDKVer.h>
stdafx.h
#pragma once #include "targetver.h" #define WIN32_LEAN_AND_MEAN #include <windows.h>
源文件
dllcalc.cpp
#include "stdafx.h" #include "dllcalc.h" int count1; int count2 = 2; int count3 = 3; #pragma data_seg("shared") //设定一个数据段,其名称为:shared int scount4 = 4; //用于共享的变量要初始化,使其被放在shared段 int scount5 = 5; int scount6 = 6; #pragma data_seg() //重置为.data段 #pragma comment(linker, "/SECTION:shared,RWS") //通过链接选项设置shared数据段特性为 READ WRITE SHARED int __stdcall Abs(int a) { return a < 0 ? -a : a; } int Inc(int a) { count1 += a; count2 += a; count3 += a; scount4 += a; scount5 += a; scount6 += a; return ++a; } int Add(int a, int b) { return a + b; } int Sub(int a, int b) { return a - b; } int Mul(int a, int b) { return a * b; } int Divide(int a, int b) { return a / b; } Book::Book() : page(0) { } Book::~Book() { page = 0; } int Book::Read(int to) { page += (to * 2); return page; }
DeriveIPaper.cpp
#include "stdafx.h" #include "DeriveIPaper.h" IPaper::~IPaper() { } Magazine::Magazine() : page(0) { } Magazine::~Magazine() { page = 0; } int Magazine::Read(int to) { page += (to * 3); return page; } IPaper* CreateIPaperDObj() { return new Magazine; } void DestroyIPaperDObj(IPaper* pdobj) { delete pdobj; }
dllmain.cpp
#include "stdafx.h" BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) //DLL的入口点 { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; }
stdafx.cpp
#include "stdafx.h"
模块定义文件
dllcalc.def
LIBRARY dllcalc EXPORTS count3 scount6 Mul @1 Div=Divide @2
APP
CPP
隐式链接
#include "dllcalc.h" #pragma comment(lib, "dllcalc.lib") //将库搜索记录放置到目标文件中 int main() { int v = 0; v = count1; v = count2; v = count3; v = scount4; v = scount5; //v = scount6; //实际读出为一个随机值,说明无法使用 v = Abs(-1); v = Inc(1); v = count1; v = count2; v = count3; v = scount4; v = scount5; //v = scount6; v = Add(1, 2); v = Sub(3, 4); v = Mul(5, 6); //v = Divide(19, 5); //报错,无法解析的外部符号 Book bk; v = bk.Read(5); v = bk.page; IPaper* paper = CreateIPaperDObj(); v = paper->Read(6); DestroyIPaperDObj(paper); return 0; }
显式链接
#include <windows.h> int main() { int v = 0; HMODULE hdllcalc = LoadLibrary(TEXT("dllcalc.dll")); if (hdllcalc) { int* pc1 = (int*)GetProcAddress(hdllcalc, "count1"); if (pc1) { v = *pc1; } int* pc2 = (int*)GetProcAddress(hdllcalc, "count2"); //返回 NULL if (pc2) { v = *pc2; } int* pc3 = (int*)GetProcAddress(hdllcalc, "count3"); if (pc3) { v = *pc3; } int* psc4 = (int*)GetProcAddress(hdllcalc, "scount4"); if (psc4) { v = *psc4; } int* psc5 = (int*)GetProcAddress(hdllcalc, "scount5"); //返回 NULL if (psc5) { v = *psc5; } int* psc6 = (int*)GetProcAddress(hdllcalc, "scount6"); if (psc6) { v = *psc6; } typedef int(__stdcall* PInt_sInt)(int); PInt_sInt pAbs = (PInt_sInt)GetProcAddress(hdllcalc, "Abs"); //返回 NULL if (pAbs) { v = pAbs(-1); } typedef int (*PInt_cInt)(int); PInt_cInt pInc = (PInt_cInt)GetProcAddress(hdllcalc, "Inc"); if (pInc) { v = pInc(1); } if (pc1) { v = *pc1; } if (pc2) { v = *pc2; } if (pc3) { v = *pc3; } if (psc4) { v = *psc4; } if (psc5) { v = *psc5; } if (psc6) { v = *psc6; } typedef int (*PInt_cIntInt)(int, int); PInt_cIntInt pAdd = (PInt_cIntInt)GetProcAddress(hdllcalc, "Add"); //返回 NULL if (pAdd) { v = pAdd(1, 2); } PInt_cIntInt pSub = (PInt_cIntInt)GetProcAddress(hdllcalc, "Sub"); //返回 NULL if (pSub) { v = pSub(3, 4); } PInt_cIntInt pMul = (PInt_cIntInt)GetProcAddress(hdllcalc, "Mul"); if (pMul) { v = pMul(5, 6); } pMul = (PInt_cIntInt)GetProcAddress(hdllcalc, (LPCSTR)1); //通过导出函数的序号获取地址 if (pMul) { v = pMul(5, 6); } PInt_cIntInt pDiv = (PInt_cIntInt)GetProcAddress(hdllcalc, "Div"); if (pDiv) { v = pDiv(19, 5); } pDiv = (PInt_cIntInt)GetProcAddress(hdllcalc, (LPCSTR)2); //通过导出函数的序号获取地址 if (pDiv) { v = pDiv(19, 5); } } FreeLibrary(hdllcalc); return 0; }
C语言
隐式链接
#include "dllcalc.h" #pragma comment(lib, "dllcalc.lib") int main() { int v = 0; v = count1; //v = count2; //报错,无法解析的外部符号 //v = count3; //报错,无法解析的外部符号 v = scount4; //v = scount5; //报错,无法解析的外部符号 //v = scount6; //报错,无法解析的外部符号 v = Abs(-1); v = Inc(1); v = count1; //v = count2; //报错,无法解析的外部符号 //v = count3; //报错,无法解析的外部符号 v = scount4; //v = scount5; //报错,无法解析的外部符号 //v = scount6; //报错,无法解析的外部符号 //v = Add(1, 2); //报错,无法解析的外部符号 //v = Sub(3, 4); //报错,无法解析的外部符号 //v = Mul(5, 6); //报错,无法解析的外部符号 //v = Divide(19, 5); //报错,无法解析的外部符号 return 0; }
显式链接
#include <windows.h> typedef int(__stdcall* PInt_sInt)(int); typedef int (*PInt_cInt)(int); typedef int (*PInt_cIntInt)(int, int); int main() { int v = 0; { HMODULE hdllcalc = LoadLibrary(TEXT("dllcalc.dll")); if (hdllcalc) { int* pc1 = NULL; int* pc2 = NULL; int* pc3 = NULL; int* psc4 = NULL; int* psc5 = NULL; int* psc6 = NULL; pc1 = (int*)GetProcAddress(hdllcalc, "count1"); if (pc1) { v = *pc1; } pc2 = (int*)GetProcAddress(hdllcalc, "count2"); //返回 NULL。lpProcName参数传入重整后的名称"?count2@@3HA"方可正常获取 if (pc2) { v = *pc2; } pc3 = (int*)GetProcAddress(hdllcalc, "count3"); if (pc3) { v = *pc3; } psc4 = (int*)GetProcAddress(hdllcalc, "scount4"); if (psc4) { v = *psc4; } psc5 = (int*)GetProcAddress(hdllcalc, "scount5"); //返回 NULL。lpProcName参数传入重整后的名称"?scount5@@3HA"方可正常获取 if (psc5) { v = *psc5; } psc6 = (int*)GetProcAddress(hdllcalc, "scount6"); if (psc6) { v = *psc6; } { PInt_sInt pAbs = (PInt_sInt)GetProcAddress(hdllcalc, "Abs"); //返回 NULL。lpProcName参数传入重整后的名称"_Abs@4"方可正常获取 if (pAbs) { v = pAbs(-1); } } { PInt_cInt pInc = (PInt_cInt)GetProcAddress(hdllcalc, "Inc"); if (pInc) { v = pInc(1); } if (pc1) { v = *pc1; } if (pc2) { v = *pc2; } if (pc3) { v = *pc3; } if (psc4) { v = *psc4; } if (psc5) { v = *psc5; } if (psc6) { v = *psc6; } } { PInt_cIntInt pAdd = (PInt_cIntInt)GetProcAddress(hdllcalc, "Add"); //返回 NULL。lpProcName参数传入重整后的名称"?Add@@YAHHH@Z"方可正常获取 if (pAdd) { v = pAdd(1, 2); } } { PInt_cIntInt pSub = (PInt_cIntInt)GetProcAddress(hdllcalc, "Sub"); //返回 NULL。lpProcName参数传入重整后的名称"?Sub@@YAHHH@Z"方可正常获取 if (pSub) { v = pSub(3, 4); } } { PInt_cIntInt pMul = (PInt_cIntInt)GetProcAddress(hdllcalc, "Mul"); if (pMul) { v = pMul(5, 6); } pMul = (PInt_cIntInt)GetProcAddress(hdllcalc, (LPCSTR)1); //通过导出函数的序号获取地址 if (pMul) { v = pMul(5, 6); } } { PInt_cIntInt pDiv = (PInt_cIntInt)GetProcAddress(hdllcalc, "Div"); if (pDiv) { v = pDiv(19, 5); } pDiv = (PInt_cIntInt)GetProcAddress(hdllcalc, (LPCSTR)2); //通过导出函数的序号获取地址 if (pDiv) { v = pDiv(19, 5); } } } FreeLibrary(hdllcalc); } return 0; }
C#
using System.Runtime.InteropServices; namespace ConsoleApp { class Program { #region 导入非托管dll中的函数 [DllImport("dllcalc.dll", CallingConvention = CallingConvention.StdCall)] static extern int Abs(int a); [DllImport("dllcalc.dll", CallingConvention = CallingConvention.Cdecl)] static extern int Inc(int a); [DllImport("dllcalc.dll", EntryPoint = "?Add@@YAHHH@Z", CallingConvention = CallingConvention.Cdecl)] static extern int Add(int a, int b); [DllImport("dllcalc.dll", EntryPoint = "?Sub@@YAHHH@Z", CallingConvention = CallingConvention.Cdecl)] static extern int Sub(int a, int b); [DllImport("dllcalc.dll", CallingConvention = CallingConvention.Cdecl)] static extern int Mul(int a, int b); [DllImport("dllcalc.dll", CallingConvention = CallingConvention.Cdecl)] static extern int Divide(int a, int b); [DllImport("dllcalc.dll", CallingConvention = CallingConvention.Cdecl)] static extern int Div(int a, int b); #endregion static void Main(string[] args) { int v = 0; v = Abs(-1); v = Inc(1); v = Add(1, 2); //导入时若不额外设置EntryPoint属性,则报错,无法找到入口点 v = Sub(3, 4); //导入时若不额外设置EntryPoint属性,则报错,无法找到入口点 v = Mul(5, 6); //v = Divide(19, 5); //报错,无法找到入口点 v = Div(19, 5); } } }
静态链接库
Static Library
库项目
新建
VS2010步骤
(1)文件→新建→项目
(2)Visual C++→Win32控制台程序→输入名称→确定
(3)在向导界面中设置应用程序类型为“静态库”→完成
基于动态链接库工程更改
VS2010步骤
(1)将动态链接库工程的属性→配置属性→常规→配置类型改为“静态库(.lib)”→确定
(2)删除代码中的导出与导入关键字
库的使用
链接
所需信息
开发时
对象库 .lib文件
运行时
无
使用步骤
(1)导入符号的声明
(2)指示编译器将库搜索记录放置到目标文件中
(3)在代码中使用导入符号
示例
DLL
头文件
dllcalc.h
#ifndef DLLCALC_H #define DLLCALC_H #define STATICLIBRARY #ifdef STATICLIBRARY //是否用于静态链接库 #define DLLCALC_API #else //结合条件编译,实现本DLL项目中编译时,DLLCALC_API宏定义为导出,非本DLL项目中编译时,DLLCALC_API宏定义为导入 #ifdef DLLCALC_EXPORTS //VS建立的DLL项目会默认定义一个 工程名_EXPORTS 的宏 #define DLLCALC_API __declspec(dllexport) #else #define DLLCALC_API __declspec(dllimport) #endif #endif #ifdef __cplusplus #define EXTERNC extern "C" #else #define EXTERNC #endif //导出变量 EXTERNC extern DLLCALC_API int count1; extern DLLCALC_API int count2; extern DLLCALC_API int count3; //既使用__declspec(dllexport)关键字,又通过DEF文件导出 EXTERNC extern DLLCALC_API int scount4; extern DLLCALC_API int scount5; extern int scount6; //不使用__declspec(dllexport)关键字,通过DEF文件导出。隐式链接该库的程序仍需使用__declspec(dllimport)声明导入,否则可能出错 //导出函数 EXTERNC DLLCALC_API int __stdcall Abs(int a); //普遍情况下,DLL导出函数会添加链接指示 extern "C" 以及调用约定 __stdcall 修饰符。必要时还可以使用DEF文件设置导出函数的名称,以增强dll通用性 EXTERNC DLLCALC_API int Inc(int a); DLLCALC_API int Add(int a, int b); DLLCALC_API int Sub(int a, int b); int Mul(int a, int b); //不使用__declspec(dllexport)关键字,通过DEF文件导出。隐式链接该库的程序最好使用__declspec(dllimport)声明导入,以生成更好的代码 int Divide(int a, int b); //不使用__declspec(dllexport)关键字,通过DEF文件导出。隐式链接该库的程序最好使用__declspec(dllimport)声明导入,以生成更好的代码 #ifdef __cplusplus //导出类 class DLLCALC_API Book { public: int page; Book(); ~Book(); int Read(int to); }; //“导出类”更好的方法: //公开一个抽象类 class IPaper { public: virtual ~IPaper() = 0; virtual int Read(int to) = 0; }; //导出派生类的实例生成和消除函数 DLLCALC_API IPaper* CreateIPaperDObj(); DLLCALC_API void DestroyIPaperDObj(IPaper* pdobj); #endif #endif
DeriveIPaper.h
#include "dllcalc.h" class Magazine : public IPaper { public: int page; Magazine(); ~Magazine(); int Read(int to); };
targetver.h
#pragma once #include <SDKDDKVer.h>
stdafx.h
#pragma once #include "targetver.h" #define WIN32_LEAN_AND_MEAN
源文件
dllcalc.cpp
#include "stdafx.h" #include "dllcalc.h" int count1; int count2 = 2; int count3 = 3; #pragma data_seg("shared") //设定一个数据段,其名称为:shared int scount4 = 4; //用于共享的变量要初始化,使其被放在shared段 int scount5 = 5; int scount6 = 6; #pragma data_seg() //重置为.data段 #pragma comment(linker, "/SECTION:shared,RWS") //通过链接选项设置shared数据段特性为 READ WRITE SHARED int __stdcall Abs(int a) { return a < 0 ? -a : a; } int Inc(int a) { count1 += a; count2 += a; count3 += a; scount4 += a; scount5 += a; scount6 += a; return ++a; } int Add(int a, int b) { return a + b; } int Sub(int a, int b) { return a - b; } int Mul(int a, int b) { return a * b; } int Divide(int a, int b) { return a / b; } Book::Book() : page(0) { } Book::~Book() { page = 0; } int Book::Read(int to) { page += (to * 2); return page; }
DeriveIPaper.cpp
#include "stdafx.h" #include "DeriveIPaper.h" IPaper::~IPaper() { } Magazine::Magazine() : page(0) { } Magazine::~Magazine() { page = 0; } int Magazine::Read(int to) { page += (to * 3); return page; } IPaper* CreateIPaperDObj() { return new Magazine; } void DestroyIPaperDObj(IPaper* pdobj) { delete pdobj; }
stdafx.cpp
#include "stdafx.h"
APP
CPP
链接
#include "dllcalc.h" #pragma comment(lib, "dllcalc.lib") //将库搜索记录放置到目标文件中 int main() { int v = 0; v = count1; v = count2; v = count3; v = scount4; v = scount5; v = scount6; v = Abs(-1); v = Inc(1); v = count1; v = count2; v = count3; v = scount4; v = scount5; v = scount6; v = Add(1, 2); v = Sub(3, 4); v = Mul(5, 6); //v = Divide(19, 5); //报错,无法解析的外部符号 Book bk; v = bk.Read(5); v = bk.page; IPaper* paper = CreateIPaperDObj(); v = paper->Read(6); DestroyIPaperDObj(paper); return 0; }
C语言
链接
#include "dllcalc.h" #pragma comment(lib, "dllcalc.lib") int main() { int v = 0; v = count1; //v = count2; //报错,无法解析的外部符号 //v = count3; //报错,无法解析的外部符号 v = scount4; //v = scount5; //报错,无法解析的外部符号 //v = scount6; //报错,无法解析的外部符号 v = Abs(-1); v = Inc(1); v = count1; //v = count2; //报错,无法解析的外部符号 //v = count3; //报错,无法解析的外部符号 v = scount4; //v = scount5; //报错,无法解析的外部符号 //v = scount6; //报错,无法解析的外部符号 //v = Add(1, 2); //报错,无法解析的外部符号 //v = Sub(3, 4); //报错,无法解析的外部符号 //v = Mul(5, 6); //报错,无法解析的外部符号 //v = Divide(19, 5); //报错,无法解析的外部符号 return 0; }