IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> C/C++ -> 正文阅读

[C++知识库]C/C++

C语言简介

? C语言诞生于1970年~1973年,在 肯·汤普逊 和 丹尼斯·里奇 的编写下完成,归属于美国贝尔实验室。

? C语言专门用于编写操作系统而发明的编程语言,所有天生适合对硬件编程,也以运行速度快而著称,也非常适合实现数据结构和算法。

? 由于出现时间过早,有很多缺陷,已经存在着很多的陷阱,但是我们的前辈给我们总结了一些避免陷阱的经验教训 《c陷阱与缺陷》

? C语言的语法很自由,但是也意味着危险。

? 自由源于自律!

? C89语法标准,默认是gcc语法编译器的语法标准

? C99语法标准,对C语言的扩展和增强 Ubuntu 16.04 默认C99 (-std=gnu99 指定为C99语法标准)

? C11语法标准,全新的升级

第一个C语言程序

程序员所写的代码不是标准C代码,需要一段程序把它翻译成标准C代码,负责翻译的程序叫做预处理器,翻译的过程叫预处理,需要被翻译的代码佳作预处理指令,以#开头的代码叫做预处理指令

gcc E hello.c 只执行hello.c 的预处理

#include 功能是导入头文件

? #include <xxx.h>

? <> 从系统指定路径查找头文件并导入

? #include “xxx.h”

? “” 从当前的工作路径找头文件,如果找不到再从系统指定路径找并导入

#include<stdio.h>
int main(){
    printf("Hello World!\n");
    return 0;
}

? 1. vim xxx.c 创建.c源文件

? 2. 编译代码,并保存退出

? 3. gcc xxx.c 编译.c源文件,成功会得到a.out文件

		4.	./a.out	运行可执行文件

注意: gcc xxx.c && ./a.out 可以编译并执行

stdio.h 标准输入输出文件

? 头文件:以.h结尾,里面存储的是辅助性代码,绝大多数都是函数的说明

main函数:

? C语言中以函数为单位管理代码,一个函数就是具有某项功能的代码段

? main函数是程序的执行入口,必须有且只能有一个

? int 是一个数据类型,表示main函数的执行结果是个整数。

? return 功能有两个:1.返回一个结果给函数的调用者

? 2.结束函数进程

? main函数的调用者是操作系统,它的返回值给了操作系统。* vim echo 程序 查询操作系统接收的return返回值

? 正数 出现异常

? 0 一切正常

? 负数 出现错误

? printf/scanf 是标准库中的函数,负责输出、输入数据

? printf(“想要输出的内容”);

? 转义字符:键盘上一些无法直接打印显示的符号,用一些特殊的字符组合来表示,这种特殊的字符组合称为转义字符, \n 换行

? \r 回到行首

? \t 制表符,相当于Tab键

? \b 退格键

? \a 响铃

? \\ 表示一个 \

? %% 表示一个%

? C语言中以分号作为一行代码的结束,使用大括号划分区域

C语言编译器gcc

? 负责把人能看懂地记录着代码的文件,翻译成计算机能看得懂的二进制文件,由预处理器,编译器,链接器组成。

? gcc是由GNU社区为了编译Linux内核代码而开发的一款免费的编译器

? gcc编译器常用编译参数:

? -E 只显示预处理的结果到终端

? -std=gnu99 设置C99语法标准

? -c 只编译不链接

? -o 指定编译结果名 -oname / -o name

? -S 生成汇编代码

? -I(大写 i) 指定头文件的加载路径 -I 加载路径

? -Wall 尽可能多地产生警告

? -Werror 将警告当错误处理

? -l(小写L) 指定要加载(使用)的代码库 -lm(加载math.h库)

C代码变成可执行文件的详细过程

  1. 预处理 把源文件翻译成预处理文件
    gcc -E code.c 显示预处理结果到终端
    gcc -E code.c -o code.i 生成以.i结尾的预处理文件
  2. 编译 把预处理文件翻译成汇编文件
    gcc -S code.i 生成以.s结尾的汇编文件
  3. 汇编 把汇编文件翻译成二进制的目标文件
    gcc -c code.s 生成以.o结尾的目标文件
  4. 链接 把若干个目标文件合并成一个可执行文件
    gcc a.o b.o c.o ···· 默认生成一个a.out可执行文件

C语言的文件类型

? .c 源文件

? .h 头文件

? .h.gch 头文件的编译结果文件,它会被优先使用

? .i 预处理文件

? .s 汇编文件

? .o 目标文件

? .a 静态库文件

? .so 共享库文件

存储空间的单位

? Bit(位) 比特 一个二进制位,只能存储0或1,计算机中存储数据的最小单位

? byte 字节 八个二进制位,计算机存储器描述存储容量的基本单位

? KB 1024字节

? MB 1024KB

? GB 1024MB

? TB 1024GB

数据类型

? 为什么要对数据进行分类?

  1. ? 现实中的数据本身就自带类别属性

  2. 对数据进行分类可以节约存储空间,提升运行速度

    C语言中数据分类为两大类:自建类和内建类

    自建类:程序员自己设计的类型

    内建类:C语言自带的类型

    注意:运算符 sizeof 可以计算类型,变量的字节数

       整型:
    
       	signed		有符号
    
       		signed char					1Btye				-128~127
    
       		signed short					2 Btye				-32768~32767
    
       		signed int					4 Btye				+-20亿
    
       		signed long					4/8 Btye				+- 9开头的19位整数
    
       		signed long long				8 Btye
    
       	unsigned	无符号
    
       		unsigned char					1Btye				0~255
    
       		unsigned short					2 Btye				0~65535
    
       		unsigned int					4 Btye				0~40亿
    
       		unsigned long					4/8 Btye				0~1开头的20位整数
    
       		unsigned long long				8 Btye				
    

    signed不加就代表了加!由于定义无符号整型时比较麻烦,C标准库中把一些类型重定义成一些新的简单的类型名:(需要导入同文件:<stdint.h>)

    uint8_t uint16_t uint32_t uint64_t

    int8_t int16_t int32_t int64_t

       浮点型:	有小数部分的类型
    
       		float			单精度		4Btye
    
       		double		双精度		8Btye
    
       		long double				12Btye/16Btye
    

    注意:小数点后六位有效!编程时,尽量使用整性。

    注意:采用一定的算法对真实的浮点数型到二级制数据进行转化,

    这个过程比存储,读取整数要慢得多。

       模拟型:
    
       		字符型:	char
    
       				字符就是符号或者图案,在内存中存储的依然是整数,需要显示出字符时,
    
       				会根据ASCII表中对应的关系显示出对应的字符或者图案
    
       		‘0’	~	48		‘A’	~	65		‘a'	~	97		'\0'	~	空字符(NULL或空格)
    
       		布尔型:	bool
    
       				先有了C语言后有的bool类型,所以C语言中不可能有真正的布尔类型,
    
       				在头文件stdbool.h 中对布尔类型进行了模拟
    
       				bool			TRUE			FALSE
    

?

 # 变量和常亮

 		什么是变量:程序运行期间的数值可以发生变化的佳作变量,相当于一个存储数据的盒子

 		定义:	类型名	变量名;

 						int		number;

 						变量名取名规则:

 						1.由字母,数字,下划线组合

 						2.不能以数字开头

 						3.不能与C语言的32个关键字重名

 						4.见名知意(功能,类型,作用范围)

 		使用:

 				赋值:	变量名	=	常量;

 				参与运算:		变量名嵌入表达式

  C语言中变量的初始值是随机的,为了安全起见,一般在定义时初始化为0。

 关键字:

?

 char	short	int	long	void	float	double

 struct	union	enum	sizeof

 auto	const	static	volatile	register	

 typedef	extern	signed

 unsigned

 if	else	switch	case	default

 for	do	while

 break	continue	goto	

?

 变量的输入输出:

 		int	printf(const	char	*format,	· · ·  );

 		功能:输出数据

 		format:”双引号包含的提示信息	+	占位符“

 		· · · :变量名列表

 		printf返回值:输出字符的个数

 	类型占位符:C语言中通过类型占位符传递变量的类型

	signed:%hhd	%hd	%d	%ld	%lld

 unsigned:%hhu	%hu	%u	%lu	%llu

		float:%f

	double:%lf

 long double:%LF

 字符型  char:%c	

		int	scanf(const	char	*format, · · · );

		功能:输入数据

		format:“双引号包含占位符”

		· · · :	变量地址列表

		scanf返回值:成功输出的变量的个数

	scanf需要提供变量的地址(	&变量名	==	地址符	)

练习1:定义各种类型的变量并初始化,使用printf显示它们各自的值

练习2:定义各种类型的变量并初始化为0,使用scanf输入,使用printf显示

?

 #include<stdio.h>
 #include<stdint.h>
 int main(){
     uint16_t num = 0;
     printf("请输入num的值:");
     scanf("%hu",&num);
     printf("num = %hu\n",num);
     return 0;
 }
 ```

?

? 什么是常量:程序运行期间数值不能改变的叫做常量

? 100 默认int类型

? 100l long

? 100ll long long

? 100u unsigned int

? 100lu unsigned long

? 100llu unsigned long long

? 3.14 默认double

? 3.14f float

? 3.14l long double

? 格式化输入输出

? %nd 显示n个字符宽度,不够则补充空格,右对齐

? %-nd 显示n个字符宽度,不够则补充空格,左对齐

? %0nd 显示n个字符宽度,不够则补充0,右对齐

? %n.mf 显示n个字符宽度(小数点也占一位),不够则补充则空格,m表示小数点后的位数(四舍五入),右对齐

? %g 不显示小数点后,多于的0

? 运算符

? 自变运算符 ++、-- 使变量的值自动加一和减一

? 前自变:++num/–num 立即生效

? 后自变:num++/num-- 下一行语句才有效

? 注意:不要再一行内,多次使用自变运算

#include<stdio.h>
int mainf(){
    int num = 10;
    printf("num = %d\n",++num);
    printf("num = %d\n",num++);
    return 0;
}

? 算术运算符:+ - * / %

? 整数/整数 结果还是整数,无小数点,只保留整数部分

? 整数%整数 取余

? / % 除数不能为0,否则就会浮点数例外,(核心已转存),这是个运行报错,一旦产生程序立即停止,后面不在执行

? 关系运算符:> < >= <= == !=

? 比较后得到结果为0(不成立)或1(成立),比较结果可以继续参与后续的计算

? int n = -100;

? if(10 < n <100) 恒成立

? 注意: == 建议常量放左边

? 逻辑运算符:&& || !

? 先把运算的对象转化成逻辑值,0转化为假,非0转化为真

? A && B 一假即假

? A || B 一真即真

? !A 求反

? && 和 || 运算符的短路特性:

	int num = 10;
	if( 100 < num && num++ ){	printf("Yes\n");}
	printf("%d\n",num);
//	当左边的值以及确定了结果,则右边不执行
    int n = 0;
	if( (100 > num) || (num++) && (n=10) ){	parint("ES");}
	printf("%d\n",n);

? 三目运算符:判断A的值是否为真,为真则执行B,否则就执行C A ? B : C ;

? 赋值运算符

? a = 10; a += 10; a -= 10; a *= 10; a /= 10; a %= 10;

? 位运算符: & | ~ ^ << >>

分支语言

 1. if(表达式){	表达式为真,则执行代码,否则不执行	}

 2. if(表达式){
    	表达式为真执行
    }else{
    	表达式为假执行
    }

 3. if(表达式1){
       表达式1真执行
    }else if(表达式2){
       表达式2真执行
       ······
    }else{
       如果以上都为假,则执行
    }

练习:输入三个整数,从小到大显示

练习:输入一个年份,判断是否是瑞年

练习:输入一个年份和月份,判断该月有多少天

练习:输入一个成绩判断等级

90~100 A

80~89 B

70~79 C

60~69 D

0~59 E

other 成绩有误

类型转换

? 只有相同类型的数据才能运算,如果类型不相同的数据需要先转换相同类型后再进行计算。

? 自动类型转换:

? 转换规则:以不丢失数据为基础,可以适当牺牲一些空间

? 1.字节少的,向字节多的转

? 2.有符号的,向无符号的转

? 3.整数,向浮点型转化

? 注意: char与short如果与不同类型的数据运算时,会优先提升为int类型后参与运算

? sizeof( 不计算内容,以大的字节数为基准! )

? 强制类型转换:

? (新类型名)数据;

? 这种方式有可能会丢失数据,谨慎使用

switch分支语句

switch(n){	//	n可以是常量,变量,表达式(表达式的结果必须是整数常量,不能是浮点数类型)。
    case val:	//	val必须数常量,如果val==n则打开开关
        · · ·
        break;	//	关闭开关。
    case val2:
    default:	//	无开关打开,则执行!并且,放在任意位置都可以最后打开。
}

? case 1 ··· 3:可以表示[a,b]的范围,但是只有在GNU编译器才支持该语法,不建议使用。

练习:输入一个月份,判断它是什么季节

(春:123月份,夏:456月份,秋:789月份,冬:10,11,12月份)

练习:输入一个月份,判断该月有多少天。(不考虑闰年)

for循环语句

? 循环就是一种让代码反复执行的方法,到达你想要效果for循环是一种非常灵活,变样多样且危险的循环

//	for循环,一般使用一个变量来引导的执行,该变量叫做循环变量
for([1];[2];[3]){
	[4]
}
/*	[1]给循环变量初始值,C99以上标准才可以在此处定义变量	std:gun99
	[2]判断循环变量是否到达边界,进入循环变量
	[4]被反复执行的代码,称之为循环体
	[3]改变循环变量,防止出现死循环,一般对循环进行自加,自减
*/

? 大括号建议使用:

? 1.建议上下对齐

? 2.如果循环体中,只有一行代码,大括号可以省略

? 但是不利于扩展,一般的商业代码都要求大括号不能省略

//	for循环的各种写法
for(;;)
{
    //	死循环
}

int i = 0;
for(;i<10;i++){}

for(int i = 0 ;; i ++ ){
    if( i > 10 ) break;
}

for(int i = 0 ; i < 10 ;){
    · · ·
    i++;
}

练习:计算出所有的水仙花数(abc=a3+b3+c^3)

练习:输入一个正整数,判断是否是素数

while循环语句

while(表达式)
{
    //	循环体
}
//	当表单式为真执行循环体,直到表达式为假,结束循环
do{
    //	循环体
}while(表达式);
//	先执行循环体,再判断循环条件,该循环至少执行一次

? 当明确直到循环次数时,用for循环

? while循环专门负责不知道循环次数

循环嵌套

? 循环里嵌套循环,外成循环执行一次,内层循环执行n次

练习:输入一个数,判断是否是回文数

练习:模拟输入六位密码,输入的密码正确显示“登录成功”,输入错误提示还有几次机会,并输入密码,最多错三次,否则显示“账号已锁定,请联系柜台”,并结束程序

练习:打印九九乘法表

练习:白钱白鸡问题

练习:计算出100~1000之间所有素数

跳转语句

  1. goto
    可以在函数内,任意跳转。

    标签名:
        ···
        goto 标签名
    /*		可能会破坏已经设计好的分支或者循环结构,因此绝大多数公司禁止使用goto
        	但是在驱动编程时特别时候处理异常
    */
        练习:计算N的的阶乘,不能使用循环语句实现
        	int s = 1;
    lx:		
    		s *= N--;
    		if( N )
            {
                goto lx;
            }
    		printf("%d\n",s);
    
  2. break

    1.switch中关闭case执行开关
    2.	跳出循环,只能跳一层循环
    
  3. continue

    结束本次循环,进入下一次循环
    
  4. return

    1.	结束函数的执行,返回到调用位置
    2.	返回一个数据给函数的调用者
    

数组

? 什么是数组:变量的组合,是一种批量定义类型相同的变量的方式

//	定义:	变量名	数组名[数量];
	int array[100];
//	使用方式:数组名[下标号];(下标号:从零开始。范围:[0,n-1])
/*	数组遍历:与for循环配合,使用循环变量当做数组的下标
	数组的初始化:		int array[100] = {0,···};
	1.因为数组的值默认是随机的,所有一般都会进行初始化。
	2.初始化的数据过多,编译器会产生警告并把多出的数据丢弃
	3.初始化的数据不够,编译器会默认在末尾补0零
	4.   
	5.这种初始化语法只能在定义中使用,并且只能逐个赋值,不能整体赋值
	6.初始化时,数组的数量可以省略,编译器会自动统计初始化中数据的个数,并且告诉数据确认数组的数量,一旦数组数量确定,后期无法更改
	sizeof(array)/sizeof(array[0]) == 数组的成员个数
	sizeof(array) == 数组的字节长度
	sizeof(array[0]) == 数组的成员的字节数
*/

练习:定义一个长度为10的数组并进行初始化,计算出最大值,最小值和平均值

练习:定义一个长度为10的数组并初始化,进行升序排列

数组越界

为了程序的编译,运行效率,编译器不去检查数组的下表越界

? 数组越界的后果:

? 1.段错误

? 2.一些正常

? 3.脏数据

? 在使用数组的过程中,要注意不要越界

练习4:定义一个长度为10的数据并初始化,找出数组中第二大的数,不允许排序

#include <stdio.h>
int main()
{
    int array[10] = {-1,23,9,-32,-93,345,76,43,26,10};
    int max_1 = array[0] > array[1] ? array[0] : array[1];
    int max_2 = array[0] < array[1] ? array[0] : array[1];
    for(int i = 1 ; i < 10 ; i ++ )
    {
        if( max_1 < array[i] )
        {
            max_2 = max_1;
            max_1 = array[i];
        }
        else if( max_2 < array[i] )
        {
            max_2 = array[i];
        }
    }
}

二维数组

? 一维数组相当于把变量排成一排,通过编号访问

? 二维数组相当于把变量排成一个矩阵,通过行号和列号访问定义

? 定义: 类型名 数组名[行数][列数]

? 使用: 数组名[行下标][列下标]

? 遍历: 需要于双重循环配合使用,一般外层循环负责遍历行,内层循环负责遍历列

二维数组初始化:

? 类型名 数组名[行数][列数] = {{第一行},{第二行}, · · · · · ,{} };

练习:定义一个5*5的二维数组,找出数组中最大的值的坐标

变长数组

? 定义数组时使用变量作为数组的长度,在代码编译期间数据的长度是不确定的,当运行到数组的定义语句时数据到长度才最终确定下来,这种数组称为变长数组

? 优点:可以根据实际情况确定数组大小,以此节约内存空间

? 缺点:不能进行初始化,因为初始化发生在程序编译时

练习:输入两个整数n,m(1<=n,m<=6),然后输入数组array[m][n],各元素的值,然后统计每个元素之和,统计非零元素的个数,计算出所有元素的平均值,大于平均值的元素个数

#include <stdio.h>
int main(){
    int m = 0 , n = 0;
    printf("请输入m,n的值")scanf("%d%d",&m,&n);
    int arr[m][n];
    double sum = 0 , avg = 0;
    int nozero_count = 0 , more_count = 0;
    
    printf("请输入各个元素的数据:");
    for(int i = 0 ; i < m ; i ++ )
    {
        for(int j = 0 ; j < n ; j ++ )
        {
            scanf("%d",&arr[i][j]);
            sum += arr[i][j];
            if( arr[i][j] )
            {
                nozero_count ++;
            }
        }
        avg = sum / (n*m);
        for(int i = 0 ; i < m ; i ++ )
        {
            for(int j = 0 ; j < n ; j ++ )
            {
                if( arr[i][j] > avg )
                {
                    more_count ++ ;
                }
            }
        }
    }
    printf("sum=%lf\n",sum);
    printf("nozero_count=%d\n",nozero_count);
    printf("avg=%lf\n",avg);
    printf("more_count"more_count);
    return 0;
}

练习:定义一个5*5的二维数组并初始化,找出最小值的坐标,并计算出最小值一圈数据之和

#include <stdio.h>
int main(){
    int arr[5][5] = {
        {1,2,3,4,5},
        {6,7,8,9,10}.
        {5,6,7,8,9},
        {4,1,4,7,4},
        {7,1,5,3,1}
    };
    int min = arr[0][0] , min_x = 0 , min_y = 0 ;
    for(int i = 0 ; i < 5 ; i ++ )
    {
        for(int j = 0 ; j < 5 ; j ++ )
        {
            if( min < arr[i][j] )
            {
                min = arr[i][j];
                min_x = i;
                min_y = y;
            }
        }
    }
    
    int sum = 0;
    for(int x = min_x-1 ; x <= min_x+1 ; x ++ )
    {
        for(int y = min_x-1 ; y <= min_y+1 ; y ++ )
        {
            if( 0 <= x && x <= 4 && 0 <= y && y <= 4 )
            {
                sum += arr[x][y];
            }
        }
    }
    printf("sum = %d\n",sum - min);
    return 0;
}

练习:输入N,显示N层杨辉三角

1
1	1
1	2	1
1	3	3	1
1	4	6	4	1

练习:输入一个日期(yyyy-mm-dd),计算该日期距离1年1月1日过了多少天

走迷宫代码

? 数据分析:

? 1.定义二维字符数组作为迷宫地图

? 2.定义变量记录角色的位置 x y

? 逻辑分析:

? 一.进入死循环:

? 1.显示地图,遍历二维数组

? 2.等待获取方向键并处理

? 判读前方是不是路(空格字符)

? 如果是:

? 1.把旧位置变成空格字符

? 2.把新位置变成"@"

? 3.更新角色位置坐标 x y

? 3.判断是否到达出口

? 如果是:程序结束

#include <stdio.h>
#include <stdlib.h>	//	系统命令头文件
#include <getch.h>	//	自定义头文件
#include <time.h>	//	

int main(){
    char maze[10][10] = {
        {'#','#','#','#','#','#','#','#','#','#'},
        {'#',' ','#',' ','#',' ',' ',' ',' ','#'},
        {'#','@','#',' ','#',' ','#','#',' ','#'},
        {'#',' ',' ',' ','#',' ','#','#',' ','#'},
        {'#',' ','#','#','#',' ','#','#',' ','#'},
        {'#',' ','#','#','#',' ',' ','#',' ','#'},
        {'#',' ','#','#','#',' ','#',' ',' ','#'},
        {'#',' ','#','#','#',' ','#','#',' ','#'},
        {'#',' ',' ',' ',' ',' ','#','#',' ',' '},
        {'#','#','#','#','#','#','#','#','#','#'}
    };
    //	记录角色初始位置坐标:man_x,man_y;
    char man_x = 2, man_y = 1;
    time_t start_time = time(NULL);
    for(;;)
    {
        //	判断是否到达出口
        if( man_y == 9 )
        {
            printf("到达出口,游戏结束!")
            return 0;
        }
        //	清理屏幕
        system("clear");
        //	显示地图
        for(int i = 0 ; i < 10 ; i ++ )
        {
            for(int j = 0 ; j < 10 ; j ++ )
            {
                printf("%c ",maze[i][j]);
            }
            printf("\n");
        }
        
        //	获取反向键处理
        switch( getch() )
        {
            case 183:	case 'w':	//	上
                if( ' ' == maze[man_x-1][man_y] )
                {
                    maze[man_x][man_y] = ' ';
                    maze[--man_x][man_y] = '@';
                }
                break;
            case 184:	case 's':	//	下
                if( ' ' == maze[man_x+1][man_y] )
                {
                    maze[man_x][man_y] = ' ';
                    maze[++man_x][man_y] = '@';
                }
                break;
            case 186:	case 'a':	//	左
                if( ' ' == maze[man_x][man_y-1] )
                {
                    maze[man_x][man_y] = ' ';
                    maze[man_x][--man_y] = '@';
                }
                break;
            case 185:	case 'd':	//	右
                if( ' ' == maze[man_x][man_y+1] )
                {
                    maze[man_x][man_y] = ' ';
                    maze[man_x][++man_y] = '@';
                }
                break;
        }
            if( 8 == man_x && 9 == man_y )
    		{
        		printf("游戏胜利,过了%lu秒!\n",time(NULL)-start_time);
        		return 0;
   		 	}
    }

    return 0;
}

推箱子

? 数据分析:

? 1.确定数值与字符的对应关系

? 0 ‘ ’

? 1 ‘@’

? 2 ‘#’

? 3 ‘$’

? 4 ‘o’

? 5 ‘@’

? 7 ‘$’

? 2.定义8*8的整数地图并初始化

? 3.定义记录角色位置的变量 x y

? 4.定义记录步数的变量

? 逻辑分析:

? 一.进入死循环

? 1.清理屏幕,显示地图

? 2.判断是否游戏胜利

? 3.获取方向键并处理

? 1.前方是路、目标点

? 2.前方是箱子,箱子的前方是路或目标点

进制转换

? 为什么要使用二进制,八进制,十进制,十六进制?

? 1.因为现在的CPU只能识别高低电平,只能对二进制的数据进行计算

? 2.虽然二进制可以直接被CPU识别计算,但是不方便书写,记录,把二进制的数据转换成八进制是为了方便记录到文档

? 3.由于CPU的位数的不断发展不断增加,由于8位逐渐发展到现在的64位,因此八进制就不能满足需求了,所以发展出了十六进制,但是由于历史原因八进制不能完全淘汰

十进制转N进制:

? 1.对十进制数进行求余数,然后继续对商求余,直到商为零,倒取余数,得到结果

? 2.求权法:用数值去减 最高位的权值*数值 (不能出现负数),直到把数据减完

练习:输入一个正整数m,输入一个n(n>=2),显示m的n进制数,超过10的用字母表示 10->A

二进制转十进制:每位加权求和

二进制位转八进制位:每三位二进制位转换为一位八进制位

二进制转十六进制:每四位对应一位十六进制数

在C语言中,以0开头的数是八进制数,以0x开头的数是十六进制数;

? %o 以八进制显示

? %x 以十六进制显示

? %#o %#x 可以把对应的前缀显示出来

原码,反码,补码

? 原码:数据的二进制

? 反码:正数反码就是它的原码

? 负数的反码就是它原码的,除符号位外,其它按位求反

? 补码:正数补码就是它们的原码

? 负数的补码是它的反码+1

内存中所有数据的存储都是以他的补码的形式存储

1.负数转换为二进制

2.符号位不变,其余按位求反,得到反码

3.反码+1得到补码

补码转数据:

? 1.先看是否有符号位

			- 如果是无符号的,直接转成原码
			- 如果是有符号的,且最高位是零,也就直接转换成十进制,最高位不动
  • 有符号且最高位是1:
    • 补码-1得到反码
    • 符号位不变,其它位,按位求反得到原码
      - 原码转化成十进制

位运算符

? A & B 按位与

? A | B 按位或

? ~A 按位求反 // 在位运算中优先级最高

? A ^ B 按位异或 相同为0,相异为1

? A << n 把A的补码向左移动n位 左边丢弃右面补0

? A >> N 把A的补码右移N位 右边丢弃,左边补符号位

(左移动,相当于乘2;右移动,相当于除2;

练习:输入一个整数,把它的4~7位设置为1010,其它位不能变;

优先级:单目 算数 位 关系 逻辑 三目 赋值

表达式中出现了位运算符,转换成二进制计算

函数

? 一段具有某项功能的代码,是C语言中管理代码的最小单位

? 把代码封装成一个个函数,可以方便管理和调用代码

  • 函数的分类:

    • 标准库函数

      • 由C语言标准委员会为C语言以函数的形式提供的一些基础功能,被封装在libc.so库中,使用时需要包含对应的头文件,通过 对应的函数名(实参)方式即可调用标准库中的函数

      • 随机数:

        #include <stdlib.h>
        #include <time.h>
        srand( time(NULL) );
        int num = rand();
        
        
        > ?	练习:	获取10个[100,1000]之间的随机数,循环不超过10次
        >
        > ?					rand() % 901+100
        >
        > ?	练习:	红球6组,每组从1-33中抽取一个,六个相互不重复。
        >
        > ?					然后篮球是从1-16中抽取一个数字
        >
      > ?					随机产生一组	双色球号码
      
    • 系统函数

    • 是操作系统以函数的接口形式提供的一系列功能,但是它不是真正意义上的函数

  • 内存管理,文件管理,文件IO,信号处理,进程管理,进程通信,线程管理,线程同步,网络通信

    • 第三方库函数

      • 由第三方提供的 开源或者收费的代码库
      • MD5 加密算法
    • Json 序列化,反序列化

    • Xml 配置文件解析算法

    • 自定义函数

      • 为了个更好地管理代码,减少代码冗余把代码封装成函数形式

        • 函数申明:

          • 函数申明,为了告诉其他代码该函数的调用格式

          • 返回值类型 函数名(类型1 形式参数1,类型2 形式参数,· · · );

            1.C语言中函数名一般全小写,下划线分隔

            2.如果返回值不需要,则写void

            3.如果不需要形式参数,建议也需要写void

        4.就算形式参数类型名相同,也要每个都加类型名

        • 函数定义:函数的实现代码

          • 返回值类型 函数名(类型1 形式参数1,类型2 形式参数,· · · ){

            ? // 函数体

            ? return 返回值类型;

            }

        • 函数调用

          • 函数名(实参)

练习:实现一个函数,判断是否是素数,调用它显示100~1000以内所有的素数

练习:输入两个日期(yyyy-mm-dd),计算两个日期间相隔多少天

练习:实现一个函数,判断一个整数是否是回文数,调用它显示1亿~10亿的所有有回文数

练习:输入一个数,显示它的补码

#include <stdio.h>
int main(){
    int n;
    scanf("%d",&n);
    char bits[32] = {};
    for(int i = 0 ; i < 32 ; i ++ )
    {
        bits[i] = n >> i & 1;
    }
    for(int i = 31 ; i >= 0 ; i -- )
    {
        printf("%hhd",bits[i]);
    }
    return 0;
}

练习:计算出100的阶乘

#include <stdio.h>
int main(){
    char rets[256] = {1};
    int cnt = 1;
    /*将一个数放到rets数组中,rets[0]为这个数的个位*/
    for(int i = 2 ; i <= 100 ; i ++ )	
    {
        int carry = 0;	//	记录进位
        for(int j = 0 ; j < cnt ; j ++ )
        {
            int num = rets[j] * i + carry;	//	将rets数组记录的数,的每一位从个位开始,乘以将要
            rets[j] = num % 10;				//	被阶乘的数i,放入num中。更新rets数组中,正在执行
            carry = num/10;		//	的rets[j]。carry表示记录num中未被rets数组记录的*进位*
        }
        while( carry )		//		将进位carry赋值给rets数组中,当前的最高位cnt
        {	//	rets数组的每一位记录,阶乘的一位
            rets[cnt++] = carry % 10;	//	rets数组的最高位cnt,取carry进位的末位
            carry /= 10;	//	更新carry进位的值,除去以存储的末位
        }
    }
    //	打印数组
    for(int i = cnt-1 ; i >= 0 ; i -- )
    {
        printf("%hhd",rets[i]);
    }
    return 0;
}

函数传参

? 1.形式参数,函数内定义的变量都只属于它所在的函数,出了该函数就不能再用

? 2.普通 实参与形参之间是通过赋值的方式传递数据的(单向值传递)

? 3.return 其实是将数据存放在一个公共区域(函数都可以访问),如果不写return语句,那么就会读该区域原来的数据,就调用一个 垃圾 数据

? 4.当数组作为函数的参数时,中括号中的长度就会丢失,需要额外增加一个变量传递数组的长度

? 5.数组作为函数参数传递是,传递是数组的首地址,叫做“址传递”。函数和函数的调用者可以共享一个数组!

练习:使用函数,实现找出数组中的最大值

练习:实现一个函数,对数组进行升序排序

练习:实现一个函数,查找数组是否存在某个值,如果存在则返回数组的下标。

设计函数的建议:

? 1.一个函数最好就解决一个问题,减低错误率,提高可读性

? 2.尽量减少函数之间的依赖层数(降低耦合度)

? 3.数据由调用者提供,结果返回给调用者(提高函数的通用性)

? 4.考虑函数的非法参数,可以通过返回值的方式告诉调用者参数有误,也可以通过注释方式写明情况

进程映像

程序:存储在磁盘上的可执行文件(二进制文件,脚本文件)

进程:在系统中运行中的程序

进程映像:指的是进程内存的分布情况

? text 代码段 存储二进制指令,常量数据,权限是只读。强制地修改就会产生段错误

? data 数据段 初始化的全局变量/初始化的静态局部变量

? bss 静态数据段 未初始化的全局变量/未初始化的局部变量。在程序进程运行前,该段内存会自动清理为0

? stack 栈 局部变量 操作系统自动管理,会自动申请和释放内存(小)

? heap 堆 由程序员手动管理 内存(大)

变量分类:局部变量 和 全局变量

  • 全局变量:定义在函数外的变量

    • 存储位置:data(初始化) 或者 bss(未初始化)
    • 生命周期:从程序开始到程序结束
    • 作用范围:在程序的任意位置都可以使用
  • 局部变量:

    • 存储位置:stack 栈内存
    • 生命周期:从函数调用开始,到函数结束
    • 作用范围:只能在函数内使用

    块变量: 定义在语句块中的变量

    • 存储位置:stack 栈内存
    • 生命周期:从函数调用开始,到函数结束
    • 作用范围:只能在函数内使用

注意:局部变量可以和全局变量同名,在函数内使用局部变量会屏蔽同名的全局变量,块变量在语句块内

会屏蔽同名的全局变量,块变量在语句块内会屏蔽同名的全局变量,局部变量,因此建议全局变量首字母大写

类型限定符

auto 用于定义自动分配内存,释放内存的变量(局部变量),不加就代表了加

注意:全局变量不能用auto修饰的

C11中用于自动识别类型

extern 声明变量 extern 类型名 变量名

? 告诉编译器此变量已经在别处定义过了,请放心使用

? 它只能临时让编译通过,但是在 链接时,如果找不到该变量,依旧会报错

? 在多文件编程中使用:假设a.c中定义了全局变量N,想要在b.c中使用N,需要在使用前声明变量

static

  • 改变存储位置:

    • 改变局部变量的存储位置,从stack改为data或者bss(取决于是否初始化)
      • 被static修饰的局部变量,叫做静态局部变量
  • 延长生命周期:

    • 延长了局部变量的生命周期,直到进程结束
  • 限制作用范围:

    • 限制 全局变量 · 函数 的使用范围,只能在本文件内使用

    可以防止全局变量,函数命名冲突,也可以防止被别的文件使用

const

	- "保护"变量的值不能被显示地修改,但是能可以通过访问内存来修改值
	- 但是如果修饰的是初始化的全局变量,初始化的静态变量,则该变量会从data改为text,变成“常量”

volatile

	- 如果变量的值没有被显示的修改,那么在使用该变量时,不会从内存中读取,而是继续使用上一次读取的结果,这个过程叫做取值优化,一般变量都会进行。
	- 变量被volatile修饰后,编译器不对该变量进行取值优化,每次都是从内存中重新读取
	- 一般硬件编程,多线程编程时会使用到

register

  • 申请把变量的存储介质由内存改为寄存器,由于寄存器数量有限,不一定申请成功
  • 存储介质:硬盘 - 内存 - 高级缓存 - 寄存器
  • 寄村器变量不能取地址

typedef

	- 类型重定义,在定义变量时,在类型前加typedef,那么变量名就变成了这个类型的新名字
	- 注意:typedef不是替换关系
小项目:五子棋

? 需要的数据:

  • 定义一个字符类型的数组15*15

  • 定义变量,记录落子位置

  • 定义变量,记录该落子为哪位棋手(黑棋“@” ,白棋 “$" )

    逻辑:

    • 定义需要的数据
    • 是否需要对数据进行初始化
    • 清理屏幕,显示棋盘
    • 落子。判断坐标是否合法,该位置不能有其它棋子。
    • 判断:检查是否五子连珠
    • 交换棋手
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

// 棋盘数组
char board[15][15];
//	棋子的位置
char key_x , key_y;
//	角色字符	黑棋 @	白棋 $
char role = '@';
//	初始化棋盘
void init_board(void){
    for(int i = 0 ; i < 15 ; i ++ )
    {
        for(int j = 0 ; j < 15 ; j ++ )
        {
            board[i][j] = '*';
        }
    }
}
//	显示刷新棋盘
void show_board (void){
    system("clear");
    for(int i = 0 ; i < 15 ; i ++ )
    {
        for(int j = 0 ; j < 15 ; j ++ )
        {
            printf("%c ",board[i][j]);
        }
        printf("\n");
    }
}

void get_key(void){
    while(1){
        printf("请%c输入棋子坐标:",role);
        scanf("%hhd%hhd",&key_x,&key_y);
        //	检查坐标是否合法
        if( key_x < 0 || key_y < 0 || key_x > 14 || key_y > 14 ){
            printf("坐标不合法,请重新输入!");
            continue;
        }
        //	检查是否有棋子
        if( '*' != board[key_x][key_y] ){
            printf("该位置已有棋子!请重新输入!");
            continue;
        }
        board[key_x][key_y] = role;
        return;
    }
}
//	重写get_key函数,并起名Luozi
void Luozi(void){
    printf("请%c落子",role);
    while(1){
        printf("\33[%hhd;%hhdH",key_x+1,(key_y+1)*2);
        switch( getch() ){
            case 183:	key_x > 0	&&	key_x--;	break;
            case 184:	key_x < 14	&&	key_x++;	break;
            case 186:	key_y > 0	&&	key_y--;	break;
            case 185:	key_y < 14	&&	key_y++;	break;
            case 10:	if( '*' == board[x][y] ){
                board[key_x][key_y] = role;
                return;
            }
        }
    }
}

//	检查是否五子连珠
bool is_win(void){
    //	左上 右下
    int cnt = 0;
    //	左上
/*    for(int x = key_x-1, y = key_y-1 ; x >= 0 && y >= 0 ; x-- , y-- )
    {
        if( role == board[x][y] ){
            cnt ++ ;
        }else{
            break;
        }
    }
    if( cnt >= 4 ) return true;
*/  
    if( count_key(-1,0)  + count_key(1,0)  >= 4 ) return true;
    if( count_key(0,-1)  + count_key(0,1)  >= 4 ) return true;
    if( count_key(-1,-1) + count_key(1,1)  >= 4 ) return true;
    if( count_key(-1,1)  + count_key(1,-1) >= 4 ) return true;
    return false;
}
//	检查是否五子连珠重写
int count_check(int ox,int oy){
    int count = 0;
    for(int x=key_x+ox, y=key_y+oy; x>0&&x<15&&y>0&&y<15; x+=ox,y+=oy ){
        if( role == board[x][y] ){
            count ++ ;
        }else{
            break;
        }
    }
    return count;
}

int main(){
    init();
    for(;;)
    {
    	show_board();
//      get_key();
        Louzi();
        if( /*is_win()*/ )
        {
            show_board();
            printf("%c胜利\n",role);
            return 0;
        }
        role = '@'==role? '$' : '@';
    }
    return 0;
}


函数递归

? 函数自己调用自己的这种行为叫做函数递归,可能会产生死循环。

? 递归是可以实现分治的这种思想,把一个大问题,分解成多个小问题,知道所有问题全部解决

  • 如何写好递归
    • 写好一个出口
      • 解决一个小问题
        • 调用自己

练习:计算前N项斐波那契数列

1 1 2 3 5 8 13 21 ······

  • 递归函数每一次调用都会在栈内产生一份自己的拷贝,直到执行到达出口,才会释放一层递归函数,因为此与循环相比递归非常消耗内存,速度很慢,因此如果能用循环解决的问题不要使用递归
  • 递归优缺点:
    • 好理解,思路清晰
    • 很好地解决非线性问题
    • 耗费内存,速度很慢

练习:使用递归模拟N层 汉诺塔

#include <stdio.h>

void hanio(int n,char star,char m,char end){
    if( 1 == n )
    {
        printf("%d:form %c to %c\n",star,end);
    }
    else
    {
        hanio(n-1,star,end,m);
        printf("%d:from %c to %c\n",star,end);
        hanio(n-1,m,star,end);
    }
}

思考:显示出0~9的全排列

指针

  • 什么是指针?

    • 指针是一种特殊的数据类型,使用它可以定义指针变量,
      指针变量存储的是整数数据,代表了内存的编号。
    • 通过编号可以直接访问对应的内存
  • 为什么要使用用指针?

    • 函数之间是相互独立的,但是有使用需要共享变量。
      传参是单向值传递
      全局变量容易命名冲突
      数组使用不便,还需要额外传递数组长度
      虽然函数命名空间是独立的,但是地址空间是同一个,因此指针可以解决共享变量的问题
    • 由于函数之间的传参是值传递(内存拷贝),对于字节数较多的变量,值传递的效率较低,如果传递变量的地址只需要传递 4 或 8 个字节。
    • 堆内存无法取名字,它不像data,bss,stack让变量名与内存建立联系,只能使用指针记录堆内存的地址来访问对应的内存
  • 如何使用指针:

    • 定义: 类型名* 指针变量名_p;
      类型名 *指针变量名_p;

    • 指针变量与普通变量的用法有很多区别,建议在取名时,以p结尾以式区分

    • 指针的类型表示,指针指向的是什么类型变量的地址。它决定了通过这个指针变量可以访问的字节数

    • 一个*号只能定义一个指针变量

      • int *p1,p2,p3; // p1是指针,p2,p3是int

        int *p1,*p2,*p3; // p1,p2,p3都是指针

      • 指针变量与普通变量一样默认值是随机的,一般初始化为NULL(空指针)

    • 赋值:变量名_p = 地址;// 必须是有意义且有权限的地址

      • 指向栈内存:
        • int num;
        • int* p = &num;
      • 指向堆内存:
        • int* p = malloc(4);
    • 解引用: *变量名_p

      • 通过指针变量中记录的内存的编号去访问对应的内存,该过程可能会产生段错误,原因是里面存储的内存编号是非法的。
      • 注意:访问的字节数由指针定义时类型决定,后面都不会改变

练习:实现一个交换变量的函数,用着函数进行排序

练习:实现一个函数计算两个整数的最大公约数,最小公倍数,最大公约数用return返回,最小公倍数使用指针输出型参数

#include <stdio.h>
int max_min_num(int x,int y,int* p){
    int max = 1;
    for(int i = 2 ; i <= x ; i ++ ){
        if( x % i == 0 && y % i == 0 ) max = i;
    }
    for(int i = x*y ; i >= x && i >= y ; i-- ){
        if( 0 == i%x && 0 == i%y ) *p = i;
    }
    return max;
}
int main(){
    
}

使用指针时需要注意的问题:

  • 空指针:

    • 值为NULL的指针变量叫做空指针
      • 如果对空指针解引用,一定产生段错误
        • NULL一般作为一种错误标志,当一个函数的返回值是指针类型时,可以使用NULL指针作为返回出错的结果
  • 如何避免空指针带来错误:

    • 使用来历不明的指针前先进性判断
    if( NULL == p )		if( !p )
    //	注意:NULL在绝大部分操作系统上是0,也有个别老的操作系统中是1
    
    • 当函数的参数是指针,别人传给你的指针可能是空指针
    • 从函数获取的返回值是指针类型时,可能会返回空指针
  • 野指针:

    • 指向不确定的内存空间的指针叫做野指针
    • 对野指针解引用的后果:
      • 一切正常
      • 段错误
      • 脏数据
    • 野指针比空指针的危害更严重,因为它无法判断出来,而且可能是隐藏性的错误,短时间不暴露。
    • 所有的野指针都是程序员自己制造出来的,如何避免产生野指针
      • 定义指针变量时一定要初始化( int *p = NULL )
      • 函数不要返回栈内存(函数内的局部变量)的地址
      • 指针指向的内存被释放后,指针变量要及时置空NULL
  • 指针的运算

    • 指针变量中存储的是整数,理论上整数可以使用的运算符它都可以使用,
      但是绝大部分运算符无意义的
    • 指针 + n:指针+指针类型的宽度*n <==> 相当于指针前进n个元素
    • 指针 - n:指针-指针类型的宽度*n <==> 相当于指针后退n个元素
    • 指针- 指针:(指针 - 指针)/指针类型宽度 <==> 相当于计算两个指针之间,间隔多少个指针元素
  • 指针与const

    • 当我们为了提高传参效率而使用指针为函数传参时,传参效率提高了,但是变量被共享在被修改的风险,可以使用const保护指针所指向内存
    • const int* p; // 保护指针所指向的内存不被修改
    • int const *p; // 保护指针所指向的内存不被修改
    • int* const p; // 保护指针变量不被修改
    • const int* const p; // 指针变量和指所指向内存都不能改
    • int const * const p; // 指针变量和指所指向内存都不能改
  • 指针数组和数组指针

    • 指针数组:由指针变量组成的数组,指针数组。

      ? 它的成员变量都是 类型相同的指针变量

      ? 类型* arr[长度];

      ? int *arr[10] = {};

    • 数组指针:专门指向数组的指值

      ? 类型 (*int)arr[长度];

  • 数组名和指针:

    • 数组名就是一种特殊的指针

    • 数组名是常量,不能修改它的值,数组名没有自己的存储空间,它与数组首地址之间是映射关系。
      数组名 == &数组名

      当指针变量指向数组首地址时,指针可以当做数组名使用,数组名也可以当做指针使用

      数组名[i] == *(数组名+i)

      *(p+i) == p[i];

      注意:当数组作为函数的参数时锐变成了指针,所以长度丢失

    • 指针是变量是拥有自己存储空间,它与所指向的内存,是指向关系

  • 二级指针

    • 二级指针就是指向指针的指针,里面存储的是指针变量的地址

    • 定义:类型名** 变量名_pp

    • 赋值:变量名_pp = &指针变量

    • 解引用:*变量名_pp <> 指针变量
      **变量名_pp <
      > 变量的地址

      注意:但需要函数之间共享指针变量,传递指针的地址(二级指针)

  • 函数指针

    • 函数名就是一个地址,函数名代表了函数在代码段(data)中所处的入口位置
    • 函数指针就是指向函数的指针,它里面存储的是函数在代码段处的入口位置
    • 返回值类型 (*p)(类型1,类型2,······ );
    • 可以通过函数指针,把函数当做参数传递给另一个函数,这种方式称之为函数的回调模式;

堆内存(heap)

一.什么是堆内存

? 是进程的一个内存段(text/data/bss/heeap/stack),由程序员手动管理

? 优点足够大,缺点使用麻烦

二.为什么要使用堆内存

	1. 随程序的复杂,数据量变多

 		2. 其它内存段的申请释放不受控制,堆内存的申请释放受控制,可以适时地节约内存

三.如何使用堆内存

? 注意:C语言中没有控制堆内存的语句,只能使用C语言标准库中的函数

? #include <stdlib.h>

? void *malloc (size_t size);

功能:从堆内存中申请size个字节的内存,申请的内存数据的值不确定

返回值:成功申请返回值的连续内存的首地址,失败返回NULL

? void free(void *prt);

功能:释放一块堆内存

prt:要释放堆指针的首地址

注意:free释放只是使用权限,数据不会全部清理

注意:不能连续释放同一内存,但可以连续释放空指针

? void *calloc(size_t nmemb, size_t size);

功能:从堆内存申请nmemb块,每块size字节大小的内存

calloc(10,4) == malloc(40)

注意:calloc申请的内存会被初始化为0;calloc比malloc慢!

? void *realloc(void ptr, size_t size);

功能:可以改变已有的堆内存的大小,size表示改变后的大小,在原有的基础上调大调小。

注意:返回值是调整后内存块新的首地址,一定要从新接受返回值,可能不是在原位置进行调整

? 如果无法在原位置进行调整:

? 1.申请一块新的符合大小的内存

? 2.拷贝原内存中的数据

? 3.释放原内存,返回新内存的首地址

malloc的内存管理系统

? 1.当我们首次向malloc申请内存,malloc会向操作系统申请内存,操作系统会直接分配33页(1页=4096字节)内存交给malloc管理,但是并不意味着可以越界访问,因为malloc可能把其它的内存分配给“其他人”,这样就会产生脏数据

#include <stdio.h>
int main(){
    int* p = malloc(4);
    for(int i = 0 ; i <= 4096*33-2 ; i ++ ){
        
    }
    return 0;
}

? 2.每个申请内存块之间会有空隙(4~12字节),一部分空隙是为了内存对齐,其中一定有4个字节记录了malloc的维护信息,这些信息决定了下次malloc分配内存的位置,如果破坏了维护信息,会影响下一次malloc或free的过程。

使用堆内存需要注意的问题

? 内存泄漏:内存无法再使用,也无法被释放,而需要在次使用时只能重新申请。

? 上述过程重复使用,会将内存逐渐耗尽

注意:一旦进程结束属于该进程的所有资源都会被操作系统会回收

? 如何尽量地避免内存泄漏:谁申请内存,谁释放;谁知道该释放,谁释放;

? 如何判断,定位内存泄漏:

  1. ps -aux Linux系统查看运行的进程

    任务管理器 Windows系统

    1. 借助代码分析工具 mtrace ,检查malloc和free是否成对出现

    2. 封装malloc,free,记录申请,释放的信息到日志文件中

? 内存碎片:已经释放了,但依然无法继续使用的内存叫做内存碎片。由于申请和释放的时间不协调导致的,无法完全避免只能尽量减少

? 如何减少内存碎片:

			1. 尽量使用栈内存

  				2. 尽量不要频繁的申请和释放内存
                   				3. 尽量申请较大块的内存自己管理

内存清理函数

#include <strings.h>
void bzero(void *s, size_t n);
/*	
	功能:把一块内存清理为0;
	s:内存块的首地址
	n:内存块的字节数
*/

#include <string.h>
void *memset(void *s, int c, size_t n);
/*
	功能:把内存块按字节设置为c
	s:内存块的首地址
	c:想要设置的ASCII值
	n:内存块的字节数
	返回值:成功设置后的首地址
*/

#include <stdio.h>
#include <stdlib.h>
void CreateHeap(void** p,size_t n){
    *p = malloc(n);
//	printf("func:%p\n",p);    
    if( NULL == *p ){
        printf("Create error\n");
        return;
    }
}
int main(){
    int* p = NULL;
    CreateHeap();
    printf("%p",p);
    return 0;
}

堆内存定义二维数组

? 指针数组

? 定义n行,m列二维数组

		int*	arr[n];
		for(int i = 0 ; i < n ; i ++ ) {
            arr[i] = malloc(m*sizeof(arr[i]));
        }

注意:每一行的m值可以不同,这种方式可以定义不规则的二维数组

优点:可以不规则,对内存要求较低

缺点:申请麻烦,容易产生内存碎片

?

? 数组指针

? 定义n行,m列二维数组

//		int* p = malloc(4*10);
		int (*arrp)[10] = malloc(sizeof(int)*10*m);
		for(int i = 0 ; i < 10 ; i ++ ){
            for(int j = 0 ; j < m ; j ++ ){
                printf("%d",arrp[i][j]);
            //  printf("%d",*(*(arrp+i)+j));
            }
        }

? 缺点:对连续内存要求高,可能申请失败

? 优点:申请简单

练习:计算出100~10000之间所有的素数,结果存储在堆内存中,尽量不要浪费内存

  • 先计算有多少个,在一个全申请出来
  • 一边算,一边申请

字符串

由字符组成的串型结构,结束标志是’\0’。

字符

在计算机中字符都是以整数形式存储,当需要显示字符时,就会根据ASCII码表中对应关系显示出相应的符号或段

‘\0’ 0 空字符

‘0’ 48

‘A’ 65

‘a’ 97

串:是一种数据结构,由一组连续的若干个类型相同的数据组成,末尾一定有一个结束标志。

? 对于这种数据结构的处理都是批量性,从开始的地方一直处理到结束标志停止

字符串的存在形式

? 字符数组: char str[5] = {‘a’,‘b’,‘c’,‘d’,‘\0’};

由char类型组成的数组,要为’\0’预留位置

使用的是stack内存,因此数据是可以中途修改的

? 字符串字面值: “由英文的双引号包含的若干个字符”

末尾会自动补’\0’

字符串字面值以地址的形式存在,字符数据存在text(代码)段,如果修改一定会产生段错误

常用方式

字符数组[] = “字符串字面值”;

会自动给’\0’预留位置,可以修改内容,初始化简单

当赋值完成后,字符串存在两份,一份在代码段,一份在栈内存

完全相同的字符串字面值,在代码段中,只存在一份。

const char* p = “12435adsf”;

sizeof§; 4/8 计算指针字节数

sizeof(“asdf”); 5 计算字符串字面值在代码段的字节数

字符串的输入输出

? 输入:

? char str[200] = {};

? scanf(“%s”,str);

缺点:scanf不能输入带空格的字符串

? char *gets (char *s );

功能:可以输入带空格的字符串

返回值:返回s的首地址,为了可以链式调用,一个函数的返回值作为另一个函数的参数

缺点:不限制输入长度,会有安全隐患,编译器会有警告

?

? char *fgets(char *s, int size, FILE *stream);

功能:从stream中最多输入size-1个字符到s中

s:字符数组

size:最多接收的字符个数+1

stream:stdin 相当于键盘文件 固定写

返回值:链式调用

如果输入超过size-1个字符,多出来的不接收

如果不住size-1个字符,'\n’也会接收

? 输出:

? printf(“%s”,字符数组,字符串字面值)

int puts(const char *s);

功能:输出字符串

返回值:成功输入字符的个数

注意:会自动打印换行符

练习:实现一个判断字符串是否是回文串的函数

#include <stdio.h>
#include <stdbool.h>

bool is_palin_str(const char* str){
    int len = 0;
    while( str[len] ){
        len++;
    }
    for(int i = 0 ; i < len/2 ; i ++ ){
        if( str[i] != str[len-1-i] ){
            return false;
        }
    }
    return true;
}

int main(){
    char str[256] = {};
    scanf("%s",str);
    printf("%s\n",is_palin_str(str)? "yes":"no");
}

练习:实现一个函数,把一个有数组字符组成的字符串转换成对应的整数

#include <stdio.h>
int str_to_int(const char* str){
    int sum = 0;
    while( *str ){
        if( *str >= '0' && *str <= '9' ){
            sum = 10*sun + (*str-'0');
        }
        str++;
    }
    return sum;
}
int main(){
    char str[256] = {};
    scanf("%s",s);
    printf("%d\n",str_to_int(str))
    return 0;
}

练习:实现一个函数,将字符串逆序输出

#include <stdio.h>
char* reverse_str(char* str){
    int len = 0;
    while( str[len] ) len++;
    for(int i = 0 ; i ; i < len/2 ; i ++ ){
        char temp = str[i];
        str[i] = str[len-1-i];
        str[len-1-i] = temp
    }
    return str;
}
int main(){
    char str[256] = {};
    scanf("%s",str);
    puts( reverse_str(str) );
    return 0;
}

输入输出缓冲区

? 程序中要输出显示的数据并不会立即显示在屏幕上,而是先存储在输出缓冲区中,当满足一定条件时才会从缓冲区中显示在屏幕上

  1. 遇到’\n’换行字符后
  2. 遇到输入语句时
  3. 当输出缓冲区满的时候,也会全部显示出来
  4. 当程序结束时,结束之前会显示在屏幕上
  5. Linux中手动刷新 fflush(stdout);

输入缓冲区

程序并不是立即从键盘获取输入的内容,而是当按下回车后,终端输入的内容会先存储到输入缓冲区中,然后函数再从输入缓冲区中读取数据到内容中。

  1. 当你想要输入的数据是整性或浮点型数据,但是缓冲区中的数据是符号或字符时,读取会失败,数据会残留在缓冲区中,影响接下来所有数据的读取

    ? 解决:

    ? 判断scanf的返回值是反全部正确,如果不是,先清理输入缓冲区残留数据,在提示重新输入。

  2. fgets可以接受指定size-1个字符,如果有多余的字符会残留在缓冲区中,可能会影响接下来的输入
    解决:

    先判断’\n’在不在字符串内,如果不在,则说明在缓冲区内,需要缓冲清理
    scanf(“%*[^\n]”); 从缓冲区中读任意数据并丢弃,如果不是’\n’,继续读取,直到运用’\n’,停止读取(正则表达式)
    scanf(“%*c”);从缓冲区中读入一个字符并丢弃

    注意:可以考虑封装新的fgets函数解决输入过多问题

    Linux:

    stdin->_IO_read_ptr = stdin->_IO_read_end;

    将当前缓冲区中的当前位置指针,移动到缓冲区末尾,相当于清理了输入缓冲区,但是只能在Linux、UNIX系统中使用这两个位置指针

  3. 当先输入整数,浮点型数据,再输入字符,字符字符串时,前一次的输入会残留’\n’,影响了后面的字符字符串的输出

解决:scanf(“(空格)%c”,&ch);

字符串相关操作函数

size_t strlen(const char *s);

功能:计算字符串字符长度,不包括’\0’

char *strcpy(char *dest, const char *src);

功能:把src拷贝给dest,相当于给dest赋值 =

返回值:链式调用

char *strcat(char *dest, const char *src);

功能:把src追加到dest末尾,相当于合并两个字符串, +=

返回值:链式调用

int strcmp(const char *s1,const char *s2);

功能:比较两个字符大小

? 从头开始,每一个字符一对一进行比较,按照字典序。

? 一旦比较出结果,立即返回结果,后面的不再比较

返回值:

0 s1 == s2

正数 s1 > s2

负数 s1 < s2

练习:自己实现strlen/strcpy/strcat/strcmp四个字符串处理函数【常考】

#include <assert.h>
size_t str_len(const char* str){
    assert( NULL != str );
    char* p = str;
    while( *p )	p++;
    return p-str;
}
char* str_cpy(char* dest,const char* src){
    assert( NULL != dest && NULL != src );
    char* p = dest;
    while( *p++ = *src++ );
    return dest;
}
char* str_cat(char* dest,const char* src){
    assert( NULL != dest && NULL != src );
    char* p = dest;
    while( *p ) p++ ;
    while( *p++ = *src++ );
    return dest;
}
size_t str_cmp(const char* s1,const char* s2){
    while( *s1 == *s2 && *s1 ) s1++,s2++;
    return *s1-*s2;
}

字符串相关操作函数

#include <stdilb.h>
int atoi(const char *nptr);
//	功能:字符串转int类型
long atol(const char *nptr);
//	功能:字符串转long类型
long long atoll(const char *nptr);
//	功能:字符串转long long类型
double atof(const char *nptr);
//	功能:字符串转double类型

#include <string.h>
char *strstr(const char *haystack,const char *needle);
//	功能:在字符串haystack中查找子串needle
//	needle在haystack中首次出现的位置,如果找不到则返回NULL

int sprintf(char *str,const char *format,··· );
//	功能:把各种类型的数据输出给字符串str
//	返回值:转换后字符个数
int sscanf(const char *str,const char *format,··· );
//	功能:从str中解析读取数据到变量
//	返回值:成功解析出来的变量个数

void *memcpy(void *dest,const void *src,size_t n);
//	功能:从src位置拷贝n个字节到dest位置
//	返回值:链式调用
int memcmp(const void *s1,const void *s2,size_t n);
//	功能:比较两块内存的值,按照字节比较,一旦比较出结果后面不再比较
/*	返回值:
			s1 == s2 0
			s1 < s2  负数
			s1 > s2  正数
*/
通讯录系统

? 要求:存储联系人信息:姓名,性别,电话。最多存50人。

? 功能要求:

增加

除(按名字)

修改(按名字)

查找联系人,按电话或姓名,支持模糊查找

显示所有联系人信息

退出系统

#include <stdio.h>
#include <stdlib.h>
#include <getch.h>
#include <unistd.h>		//	sleep函数库
#include <string.h>

static char name[50][20];	//	姓名
static char sex[50];		//	性别
static char tel[50][12];	//	电话
static int count = 0;		//	当前有效联系人的数量

int main(){
    while(1){
        //	显示主界面
        switch( menu() ){
            case '1':	add();break;
            case '2':	del();break;	
            case '3':	mod();break;
            case '4':	find();break;
            case '5':	show();break;
            case '6':	return 0;
        }
    }
}

int menu(void){
    system("clear");
    puts("****欢迎使用阿斯利康通讯录****\n");
    puts("1.添加练习人\n");
    puts("2.删除练习人\n");
    puts("3.修改练习人\n");
    puts("4.查询练习人\n");
    puts("5.查询所有人\n");
    puts("6.退出通讯录\n");
    puts("**************************\n");
    char cmd = getch();
    printf("%c\n",cmd);
    return cmd;
}
//	printf("%s\n",__func__);	两个下划线func两个下滑线:
void add(void){
    printf("%s\n",__func__);	//	测试函数调用
    if( 50 <= count ) {
        puts("系统正在升级中,请等待··· ");
        return;
    }
    int i = 0;
    while( sex[i] ){ i++; }
    printf("请输入姓名,性别,电话:");
    scanf("%s %c %s",name[i],&sex[i],tel[i]);
    count++;
    msg_show("成功添加联系人!\n",1.5);
}
void del(void){
    printf("%s\n",__func__);	//	测试函数调用
    char key[20] = {};
    printf("请输入要删除的联系人姓名:");
    scanf("%s",key);
    
    for(int i = 0 ; i < 50 ; i ++ ) {
        if( sex[i] && 0 == scrcmp(key,name[i]) ) {
            sex[i] = 0;
            count--;
            msg_show("删除成功!\n",1.5);
            return;
        }
    }
    msg_show("查无此人,删除失败!",1.5);
}
void mod(void){
    printf("%s\n",__func__);	//	测试函数调用
    char key[20] = {};
    printf("请输入要修改的联系人姓名:");
    scanf("%s",key);
    
    for(int i = 0 ; i < 50 ; i ++ ) {
        if( sex[i] && 0 == scrcmp(key,name[i]) ) {
            printf("请重新输入联系人的姓名,性别,电话:");
            scanf("%s %c %s",name[i],&sex,tel[i]);
            msg_show("修改联系人成功!\n",1.5);
            return;
        }
    }
    msg_show("查无此人,修改失败!",1.5);
}
void find(void){
    printf("%s\n",__func__);	//	测试函数调用
    char key[20] = {};
    printf("请输入要查询的关键字:");
    scanf("%s",key);
    for(int i = 0 ; i < 50 ; i ++ ) {
        if( sex[i] && ( strstr(name[i],key) || strstr(tle[i],key) ) ) {
            printf("姓名:%s,性别:%s,电话号码:%s\n",name[i],'w'==sex[i]?"女":"男",tel[i]);
        }
    }
}
void show(void){
    printf("%s\n",__func__);	//	测试函数调用
    for(int i = 0 ; i < 50 ; i ++ ) {
        if( sex[i] ) { 
            printf("姓名:%s,性别:%s,电话号码:%s\n",name[i],'w'==sex[i]?"女":"男",tel[i]);
        }
    }
    anykey_continue();
}
//	打印提示信息
void msg_show(const char* msg,float sec){
    printf("%s",msg);
    fflush(stdout);		//	手动刷新
    usleep(sec*1000000);
}
//	按任意键继续
void anykey_continue(void) {
    stdin->_IO_read_ptr = stdin->_IO_read_end;
    puts("请按任意键继续:");
    getch();
}

预处理指令的分类

#include	//	导入头文件 或 头文件包含
	#include <>		//	在系统指定路径查找头文件
	#include ""		//	先从当前工作路径查找,找不到再从系统指定路径查找,并导入头文件
gcc code.c -I path	//	编译时指定头文件的加载路径为phth,最先从该路径查找
修改 ~/.bashrc 终端配置文件 //	可以通过设置环境变量来修改,真加系统指定头文件加载路径

#define 定义宏

  • 宏常量:#define 宏名 常量值

    • 本质上就是,在代码中,将宏名替换成对应的常量值!
    • 优点:降低维护成本,提高可读性,提高安全性,提高可扩展性(可以与case连用)
    • 注意:宏常量名一般全大写,末尾不加分号
      • 局部变量,函数名全小写;局部变量首字母大写
      • 数组arr 指针 _p , _pp ,字符串 str
  • 宏函数:

    • 预定义宏函数:
    	__func__	获取函数名
    	__FILE__	获取文件名
    	__DATE__	获取当前函数
        __TIME__	获取当前日期
        __LIME__	获取当前行数 %d
    
    • 宏函数:
    • 定义:#define 宏名(参数)替换代码
    • 宏函数不是真正的函数,不检查参数类型,没有传参,只有表达式的计算结果没有返回值

    替换过程:

    1. 先把代码中使用到宏函数的笛梵替换为宏函数后面的代码

      1. 再把宏函数代码中使用到的函数替换为调用者提供的数据
  • 宏的二义性

    • 由于宏代码所处的位置,参数位置优先级的问题,导致同一个宏函数有不同的可解释结果
  • 如何避免二义性:

    • 给每个参数加小括号,给每个式子加小括号
    • 在使用宏函数时,不要提供带自变运算符的参数

注意:宏函数后面的替换不能直接换行,需要在每一行末尾通过续行符 \ 来换行

? 如果有多行代码也可以使用大括号保护

练习:实现一个交换两个变量的值的宏函数,数据类型通用,能写多少个,分析优劣

#define SWAP(a,b) a=a+b,b=a-b,a=a-b;	//	数据可能会溢出,不能够交换结构体数据,不能交换指针数据
#define SWAP1(a,b) a=a*b,b=a/b,a=a/b;
#define SWAP2(a,b) a=a^b,b=a^b,a=a^b;	//	不会溢出,只能交换整性,不能交换同一变量
#define SWAP3(A,B) long double t=a;a=b;b=t;	//	不能交换结构体
#define SWAP4(a,b,type) type t=a;a=b;b=t;	//	可以传结构体,节约内存
#define SWAP5(a,b) typeof(a) (t)=(a);(a)=(b);(b)=(t);

? typeof :返回变量名类型,只能在GNU编译器中使用

常考笔试面试题:

  • #define 与 typedef 的区别

    • #define INT int
      • typedef int INT

    INT num;

    如果是普通类型,他们功能能上没有区别

    #define INTP(1) int*

    typedef int* INTP(2);

    INTP(1) p1,p2,p3; p1为指针,p2,p3为普通的变量

    INTP(2) P1,P2,P3; p1,p2,p3皆为指针

  • 宏函数与普通函数的区别,优缺点

    • 是什么?

      宏函数:带参数的宏替换,只是代码替换,只能使用时想函数而已,不是真正的函数

      函数:是一段具有某项功能的代码集合,会被翻译成二进制指令存储代码段,函数名就是它的首地址,有独立的栈内存,命名空间

    • 有什么不同?

      函数:有返回值,类型检查 内存的申请释放 不冗余

      宏函数:运算结果 不检查内容类型 通用 替换 冗余 快

    • 注意:调用频繁,内容简单的功能适合写成宏函数

条件编译预处理

  • 根据条件决定哪些代码是否参与最终的编译

  • 版本控制:

    #if	条件
    #elif 条件
    #else
    #endif
    
  • 头文件卫士:防止头文件被 重复包含

    • 格式:

      #ifndef		宏名
      #define		宏名
      #endif
      //	宏名为文件名全部大写,小数点用下划线代替
      //	注意头文件中必加头文件卫士
      
  • 判断调试:

    #ifdef		宏名
    //	如果宏名定义了,此处代码参与编译
    #else		
    //	否则,此处编译
    #endif
    //	gcc code.c -D宏名		可以在编译时定义宏
    
#include <stdio.h>
//	定义打印调试信息的宏函数
//	gcc 文件名	-D	宏名		进行编译
#ifdef DEBUG
	#define debug(...) printf(__VA_ARGS__)
#else
	#define debug(...)
#endif

//	打印错误信息的宏函数
#define error(...) printf("%s:%s %d %s :%m %s %s",__FILE__,__func__,__LINE__,__VA_ARGS__,__DATE__,__TIME__)

int main(){
    int num = 88;
    printf("----------\n");
    debug("提示信息:%d\n",num);
    printf("----------\n");
    return 0;
}

头文件

  • 头文件中应该写什么?

    • 头文件可能会被任意的源文件包含,意味着头文件中的内容会在多个目标文件同时存在,因此要满足合并内容不能有冲突

    • 重点:头文件中只能编写申明语句,绝对不能有定义语句

      函数申明,全局变量,宏常量,宏函数,typedef,结构,联合,枚举的类型声明

  • 头文件的编写原则:

    • 为每个.c文件编写一份.h文件,.h文件对.c文件的说明
    • 如果需要用到某个.c文件中的全局变量,函数,宏文件等内容时,只需要把它的头文件导入即可
    • .c文件也要导入自己的.h文件,目的是为了声明与定义一致
  • 头文件的相互包含:

    • 假设:a.h包含b.h,b.h又包含了a.h,此时编译会出错(错误提示:未知类型名“XXX”)
    • 解决方案:将a.h需要导入b.h的内容和b.h需要导入a.h的内容提出来另一个c.h

Makefile

  • Makefile是由一系列编译指令组成的可执行文本文件,也叫编译脚本
  • 在终端执行make命令,就会自动执行Makefile脚本文件中的编译指令,它可根据文件的最后修改时间来判断哪些需要重新编译,哪些不需要重新编译,从而提高编译效率
  • 一个最简单的Makefile格式:
    • 执行目标:依赖
    • Tab键 执行命令
    • 被依赖目标1:依赖文件
    • Tab键 执行命令
    • ···
    • 被依赖目标2:依赖文件
    • Tab键 执行命令
CC   =  gcc
BIN  =  main
OBJ  =  main.o func.o tool.o
STD  =  -std=gnu99
FLAG =  -Wall -Werror
 LD  =  -lm
INCLUDE = -I
    
all : $(OBJ)
    $(CC) $(OBJ) -O $(BIN)
    
%.O:%.C
    %(CC) $(STD) -C %< -O $@
/*
main.o : maic.c func.h tool.h
	$(CC) -c main.c
func.o : func.c tool.h
	$(CC) -c func.c
tool.o : tool.c
	$(CC) -c tool.c
*/
clean:
	rm -rf $(BIN) $(OBJ)
练习项目 2048游戏
  • 文件规则:
    • main.c 程序入口
    • game2048.c game2048.h 游戏的业务逻辑
    • direction.c direction.h 四个方向处理
    • tools.c tool.h 工具函数
    • Makefile 编译脚本
//	系统命令,创建所有可能要写的空文件
    vim create_project.sh // 创建脚本文件

        touch main.c
        
    	touch game2048.c game2048.h
        
        touch direction.c direction.h
        
        tools.c tool.h
#编译脚本
        ls > Makefile

        ls > game2048.conf
        
        
//chmod +x create_project.sh
//	写Makefile文件
CC = gcc
BIN = game2048.bin
OBJ = direction.o game2048.o main.o tools.o
FLAG = -Wall -Werror

all : $(OBJ)
    $(CC) -O $(BIN) $(OBJ)
%.o : %.c
    $(CC) $(STD) $(FLAG) -C $< -O $@
clear:
	rm -rf $(BIN) $(OBJ)
        
        
//	vim game2048.conf
        gedit + 要打开的文件
 
//	 2048游戏代码

//game2048.c
    
int (*view)[4] = NULL;
#include <stdlib.h>
#include <time.h>
#include "game2048.h"
#inlcude "tool.h"

void init_game(void){
    debug("%s\n"n__func__);
    //	申请堆内存
    view = calloc(sizeof(view[0][0])*16);
    //	初始化数据
    srand(time(NULL));
    //	读取文件,加载数据;
}

void start_game(void){
    
}

void end_game(void){
    debug("%s\n",__func__);
    //	保存数据
    //	在释放内存
    free(view);
	view = NULL;
}

// tool.h文件
{
    #include <stdbool.h>
    extern int (*view)[4];
    extern int score;
    
    void show_view(void);
	void rand_two(void);
	bool is_die(void);
}

//	tool.c文件
#include <stdbool.h>
#include "tool.h"
void show_view(void){
    system("clear");
    printf("---------------------\n");
    for(int i = 0 ; i < 4 ; i ++ ){     
        for(int j = 0 ; j < 4 ; j ++ ){
            if( view([i][j] ){
                printf("|%4d");
            }else{
                printf("    ");
            }
        }
        printf("|\n");
        printf("---------------------\n");
    }
}
static bool is_full(void){
    int *num = (int*)view;
	for(int i = 0 ; i < 16 ; i ++ ){
        if( 0 == num[i] ) return false;
    }
    return true;
}
void rand_two(void){
    
}
void is_die(void){}

结构,联合,枚举

结构struct:

  • 结构是一种由程序员自己设计的数据类型,用于描述一个事务的各项特征数据,由若干个不同的基础类型组成

  • 设计:定义变量:

  • 注意:struct在C语言中,定义结构变量时不能省略

  • 变量初始化

    • struct 结构类型名 结构变量名 = {v1,v2,···};
    • struct 结构类型名 结构变量名 = {.成员名3=常量,.成员名2=常量 };
    • 注意:同类型的结构变量可以给另一个结构变量初始化
  • 由于结构体的字节数一般都比较大,普通的值传递效率非常地低,因此传递结构变量的地址,也即是传递结构指针变量,此时想要通过结构指针变量访问成员是借助 向右的箭头→,如果不需要修改,用const保护。

  • typedef 重定义结构类型名

    • typedef struct 结构类型名 结构类型名;

    • 结构指针变量名->成员变量
      
      typedef struct 结构类型名{
          
      }结构类型名;
      
struct 结构类型名{
    类型 成员名1;
    类型 成员名2;
    ···
};
//	定义结构变量
struct 结构类型名 结构变量名;
//	注意:结构类型名

//	列如:设计学生结构类型
#include <stdio.h>
#include <string.h>
struct Student{
    char name[20];
    char sex;
    char id[10];
    double score;
}
//	初始化
struct Student to = {"小明",'m',"10086",309.0};
struct Student stu;		//	定义一个学生结构变量

strcpy(stu.name,"小三");
stu.sex = 'w';			//	访问结构体成员:结构体变量.成员名
strcpy(stu.id,"10000");
stu.score = 230.5;
printf("%s %c %s %lf\n",stu.name,stu.sex,stu.id,stu.score);


练习:设计一个教师结构体类型,类型成员有:姓名,性别,工号,工龄。定义一个教师结构变量,使用scanf输入每个成员的值并显示。

  • 如何计算一个结构体的总字节数:
    • 结构成员顺序会影响它的总字节数,如果能够在设计结构体时合理地安排成员顺序,可以大大地节约内存
    • 内存对齐:假设第一个成员从零地址开始,存储每个成员的地址编号必须能被该成员的类型的字节数整除,如果不能整除,就用空白字节填充。
    • 内存补齐:结构体的总字节数,必须是它最大成员的整数倍,如果不够,则在末尾补空白字节。
    • 注意:在Linux系统下计算对齐补齐时,如果成员数超过4字节,按4对齐补齐。Windows系统按实际字节数对齐补齐。

联合union:联合与结构使用方法基本一致,与联合的所有成员共用一块内存,一个成员的值发生改变,其它成员也发生改变

联合的效果就是使用少量的内存对应多个标识符,以此达到节约内存的目的,但是现在基本不使用

union Data{
    char ch[5];		//	0~4
    int num;		//	0~3
}
//	注意:计算联合字节数时,不需要考虑内存对齐,但是需要考虑内存补齐
  • 如何判断系统的大小端问题?
    • 假设十六进制数0x01020304存储在0x0A~0x0D范围内的四字节内存中
    • 大端系统:高位数据存储在低位地址
    • (0x0A:0x01,0x0B:0x02,0x0C:0x03,0x0D:0x04)
    • 小端系统:低位数据存储在低位地址
    • (0x0A:0x04,0x0B:0x03,0x0C:0x02,0x0D:0x01)
  • 个人计算机一般都是小端系统,而Unix服务器和网络设置都是大端。网络字节序也就是大端模式的数据,本地字节序就是小端模式的数据 序列化和反序列化

枚举enum:枚举就是一种数据类型,将可能出现的所有值罗列出来,并起一个有意义的名字表示这些值,除此之外给该类型的变量赋其它值,是非法的 (愿望) 。

  • 枚举可以看做一种值受限的int类型,但在C编译器为了效率不检查,所以在C语言中枚举就相当于int类型的数据
  • 如果不给成员值,那么枚举中的值第一个默认从0开始,逐渐+1,如果设立某个值,后面的成员在它的基础上逐渐+1
  • 为什么要用枚举:
    • 为无意义的值取一个有意义的名字,提高代码的可读性,提高安全性 (比变量安全)。

练习:修改前次的通讯录,改为结构体存储数据,使用堆内存。

文件

  • 文本文件:存储的ASCII码值的二进制文件
  • 二进制文件:存储数据的补码

文件IO:

#include <stdio.h>
FILE *fopen(const char *path,const char *mode);
//	功能:打开或创建文件
path:	文件路径
mode:	文件打开模式
    r		以只读权限方式打开文件,如果不存在则失败
    r+		在r的基础上,增加写权限
    w		以只写权限打开文件,如果存在则清空打开,如果不存在则新建文件再打开
    w+		在w的基础上,增加读权限
    a		以只写权限打开文件,如果存在则在末尾追加内容,不存在则新建
    a+		在a的基础上,增加读权限
返回值:文件指针,不需要关心里面有什么数据,只需要知道它是一个针对已打开的文件凭证,打开文件失败会返回NULLint fclose(FILE *stream);
功能:关闭一个打开的文件
返回值:成功返回0,失败返回-1> 注意:如果需要立即修改文件的内容,最好先关闭文件

二进制方式的读写:

#include <stdio.h>
size_t fwrite(const void *ptr,size_t size,size_t nmemb,FILE *stream);
//	功能:把内存中的数据写入到文件中
pty:待写入的内存的首地址;
size:一次写入的字节数
nmemb:写入的次数
stream:文件指针,fopen的返回值,表示:将要写入数据的文件
返回值:成功写入的次数
    
size_t fread(void *ptr,size_t size,size_t nmemb,FILE *stream);
//	功能:从文件中读取数据到内存中
pty:从文件中读取数据到内存
size:一次读取的字节数
nmemb:读入的次数
stream:文件指针,表示:从哪个文件中读取数据
返回值:成功读取的次数

练习:设计并定义一个教师结构变量并初始化,以二进制方式写入到文件中

练习:从文件中读取一个教师结构体数据到变量中并显示

以文本方式读写:

#include <stdio.h>
int fprintf(FILE *stream,const char *format,···);
功能:以文本形式写入数据到文件中
stream:要写入的文件
format:占位符和提示信息
· · · :变量列表
返回值:成功写入的字符个数
    
int fscanf(FILE *stream,const char *format,···);
功能:从文件中读取数据到变量中
stream:要读取的数据
format:占位符 (数据格式)
· · · :变量列表
返回值:成功写入的字符个数

练习:设计并定义一个教师结构变量并初始化,以文本方式写入到文件中

练习:从文件中读取一个教师结构体数据到变量中并以文本方式显示

文件的位置指针:

  • 每个通过fopen打开的文件都有一个文件位置指针来记录着接下来要读写的位置,以r,r+,w,w+打开文件,位置指针在文件的开头,以a,a+打开文件,位置指针自动在末尾

  • 如果想要随意读写文件的任意位置,那么可以通过手动设置文件位置指针的位置

    #include <stdio.h>
    fseek	ftell	rewind
    int fseek(FILE *stream,long offset,int whence);
    stream:文件指针
    offset:偏移值
    whence:基础位置
        SEEK_SET	文件开头
        SEEK_CUR	当前位置
        SEEK_END	文件末尾
    返回值:调整成功(调整后位置指针还在文件中)返回0,失败返回-1void rewind(FILE *stream);
    功能:把文件位置指针设置到文件开头
        
    long ftell(FILE *stream);
    功能:获取当前文件位置指针的位置
    返回值:在第几个字节
        
    int fgetc(const *stream);
    功能:从文件中读一个字节
    返回值:失败,读取完毕返回EOF-1char *fgets(char *s,int size,FILE *stream);
    功能:读取一行字符串到s中,最多读size-1int fputc(int c,FILE *stream);
    功能:读取一个字符到文件中
    返回值:成功返回非负整数,失败返回-1int fputs(int ,FILE *stream)
    功能:写一个字符串到文件中
    返回值:成功返回非负整数,失败返回-1int remove(const char *pathname);
    功能:删除文件
    pathname:文件路径
    返回值:成功返回0,失败返回-1int rename(const char *oldpath,const char *newpath);
    功能:文件重命名
    返回值:成功返回0,失败返回-1

练习:实现cp命令 (cp dest src)( fread fwrite )

先读,存起来char buf[256],然后写进去,循环直到读完为止

#include <stdio.h>
int main(int argc,const char* argv[]){
    if( 3 == argc ){
        printf("User: ./cp src dest");
    }
    FILE* src = fopen(argv[1],"r");
    if( NULL == src ){
        printf("源文件不存在,请检查命令!\n");
        return -1;
    }
    FILE* dest = fopen(argv[2],"r");
    if( dest ){
        printf("目标文件已存在,是否覆盖!(y\\n)");
        char cmd = getcher();
        if( cmd != 'Y' && cmd != 'y' ){
            printf("停止拷贝\n");
            fclose(src);
            fclose(dest);
            src = NULL;
            dest = NULL;
        }
    }
    fclose(dest);
    dest = fopen(argv[2],"w");
    if( NULL == dest ){
        printf("文件路径权限有误,请检查\n");
    }
    char buf[250] = {};
    int ret = 0;
    while( ret = fread(buf,1,sizeof(buf),src) ){
        fwrite(buf,1,ret,dest);
    }
    fclose(src);
    fclose(dest);
    src = NULL;
    dest = NULL;
}

命令行参数:main函数的参数

  • 是为了获取 a.out命令行中的参数
  • argc 代表了命令行参数的个数
  • argv 每个参数字符串的首地址

注意:a.out是argv[0];

C语言大项目:学生信息管理系统

数据结构

  • 什么是数据结构?

    • 数据结构的起源

      1968年,美国高德纳教授,《计算机程序设计艺术》第一卷《基本算法》,开创了数据结构和算法的先河

      数据结构是研究数据之间关系和操作的学科,而非计算方法

      数据结构 + 算法 = 程序 美国沃斯提出 这句话揭露了程序的本质

    • 数据结构相关基础概念

      • 数据:所有能够输入到计算机中,能够被程序处理的描述客观事务的符号
      • 数据项:有独立含义的数据的最小单位,也称为数据域
      • 数据元素:组成数据的有一定含义的基本单位,也称为节点,结点,记录
      • 数据结构:相互之间存在,有一种或多种特殊关系的数据元素的集合
      • 算法:数据结构中所具备的功能,解决某种特定问题的方法
  • 数据结构的三大方面

    • 数据之间的逻辑关系
    • 数据的存储关系
    • 数据结构的运算
  • 数据结构的逻辑关系和存储关系

    • 逻辑关系:
      集合:数据元素同属于一个集合,但元素之间没有任何关系

      ? 线性结构:数据元素之间存在一对一关系

      ? 树形结构:数据元素之间存在一对多关系

      ? 图型结构:数据元素之间存在多对多关系

    • 存储(物理)关系:

      ? 顺序结构:数据元素存储在连续的内存中,用数据元素的相对位置来表示关系

      ? 优点:支持随机访问,访问速度查找效率高,适合用于查找数据频繁的结构

      ? 缺点:对插入,删除时,效率低不方便,内存空间利用率低,要求高

      ? 链式结构:数据元素存储在彼此相互独立的内存中,每个独立的元素也叫做结点,每个结点中增加一项数据项用于存储其它相关结点的地址,以此表示结点之间的关系

      ? 优点:空间利用率极高,对内存要求不高。插入,删除,更快更方便

      ? 缺点:不支持随机访问,只能从头到尾逐个访问

      注意:逻辑结构,存储结构采用哪种根据实现难度,空间,时间要求,操作习惯等方面综合考虑选择适合的结构

  • 逻辑结构与存储结构的关系:

    ? 线性表 顺序 链式

    ? 树 链式 顺序

    ? 图 顺序 + 链式

  • 数据结构的运算

    • 创建数据结构 create/creat
    • 销毁数据结构 destory
    • 清空数据结构 clean
    • 数据结构排序 sort
    • 插入元素 insert
    • 删除元素 delete
    • 访问元素 access
    • 查询元素 query
    • 修改元素 modify
    • 遍历数据结构 ergodic show print

线性表

顺序表和链式表实现

顺序表:

  • 数据项:
    表的容量
    存储元素的连续内存首地址
    当前元素的数量
    • 运算:
      创建,销毁,清空,插入,删除,访问,查询,修改,排序,遍历

注意:1.保证数据元素的连续性。2.不要越界

链式表:list(《大话数据结构》)

  • 元素(结点)的数据项:

    ? 数据域:可以是任何类型的若干个数据项
    ? 指针域:指向下一个结点

    由若干个结点通过指针域连接到一起形成了链表

    不带头结点的单链表:

    ? 第一个结点的数据域存储有效数据

    ? 缺点:添加,删除结点时,可能会修改第一个结点的指针,参数就需要使用二级指针,才能更改指针的指向,比较麻烦

    带头结点的单链表:

    ? 第一个结点的数据域不存有效元素,仅仅只是使用它的指针域永远指向链表的第一个数据有效的结点

    ? 优点:添加,删除时会比不带头结点的链表更方便

    ? 注意:其它操作要从头结点的下一个结点开始

功能受限的表结构

对表结构的部分功能加以限制,形成特殊的表结构

栈:只有一个进出口的表结构,先进后出,又名 FILO

  • 栈的应用:
    函数的调用,栈内存的特点
    生产者和消费者模型(仓库 - 》栈 )
    表达式解析

  • 顺序栈:
    数据域:存储元素的内存首地址,栈的容量,栈顶位置
    运算:创建,销毁,入栈,出栈,栈满,栈空

  • 链式栈:
    栈结构数据项:栈顶结点,结点数量

    ? 运算:创建,销毁,入栈,出栈,

  • 栈的常考笔试题面试题
    某个序列是一个栈的入栈顺序,判断,哪个是正确的出栈顺序(有可能边进边出)

    1	2	3	4	5	入栈
    1	2	3	4	5	YES
    3	2	1	4	5	YES
    3	1	2	5	4	NO
    
  • 两个容量相同的顺序栈,如何能够让空间利用率最高

练习:实现一个函数,序列a为入栈顺序,判断序列B是否是A的出栈顺序

队列:有两个端口,一个端口只能入队,另一个只能出队先进先出,又名FIFO

? 顺序队列:

? 数据项:存储内存首地址,容量

? 队头,队尾

? 运算:创建,销毁,入队,出队,队空,队满,查队头,查队尾,队中数量

  • 顺序队列的注意点:由队头下标front加对尾下标tail组成,人队tail++,出队front++,为了让队列能够反复使用,我能把队列相像成一个环,因此当front和tail加1后,都要用队列容量求余再重新赋值

    ? front = (front+1)%cal; tail = tail%cal

如何判断队空队满:二外多申请一个元素的内存

额外多申请一个元素的内存

? 队空: front == tail

? 队满: (tail+1)%cal == front

? 代价是空了一个位置不能使用,计算数量时较麻烦(最常考)

? 计算数量:(tail-front+cal)%cal

另一种方式是队列结构中增加一项数据项用于记录队列中元素个数,判断或空或满直接判断该数据即可,更方便

链式表

? 由链表结点组成的队列结构

? 数据项:

? 队头指针

? 队尾指针

? 结点数量

? 运算:创建,销毁,队空,入队,出队,队头,队尾

队列应用:

  • 消息队列
  • 树的层序遍历(使用队列)
  • 图的广度优先遍历(使用队列)
  • 封装线程池,数据池

练习:使用两个相同顺序的栈,来模拟一个队列

提示:栈2必须为空,栈1才能入栈2。栈1到栈2必须一个不留

Queue{
    stack* s1;
    stack* s2;
}

封装链表:

? 尾添加的效率低,非法下标的判断效率也非常低

  • 单链表
    结点:
    数据域
    指针域
    单链表数据项:头结点,尾结点,结点数量

  • 静态链表
    结点:
    数据域
    指针域
    静态链表的结点存储在连续的内存中,通过游标来访问下一个结点
    这种链表在插入删除时时需要修改游标的值,而不用中请,释放结点内存就可以达到类似链式结构的效果

    注意:静态链表牺牲了随机访问的功能,也没有达到链表动态申请内存的效果,只是给没有指针的编译语言实现链表的一种方式,适用范围不大

  • 循环链表
    链表的最后一个结点的next不再指向NULL,而是指向头结点,这种链表称为单向循环链表,简称循环链表。优点是可以通过任意结点来遍历整个链表

  • 双向链表

    ? 结点:

    ? 前驱指针: prev

    ? 数据域:

    ? 后继指针: next

    ? 数据项:

    ? 头结点

    ? 结点数量

    ? 特点:在任意结点都可以遍历整个列表。相比单链表,删除,插入更直接。

    如果已知结点位置,可以选择从前往后,或者从后往前,访问比单链表更有效率。

树型结构

? 树的基本概念:一种表示层次关系(一对多)的数据结构,有且只有一个特点的结点,该结点没有前驱结点,被称为根结点。剩余的n个互不相交的子集也都是一棵树,都被称为根节点的子树

注意:树型结构具有递归型(树中有树)

  • 树的表示方式:倒悬树,嵌套法,凹凸法

  • 树的专业术语:

    ? 结点:组成树的基本元素,同时它也是一棵树

    ? 结点的度:该结点子树的数量

    ? 树的度:一棵树中,最大的节点的度称为树的度;

    树的深度(高度):树的最大乘次数

    ? 结点的乘次:根节点的层次为1,它的孩子层次为2,孩子的孩子层次为3

    ? 叶子结点:结点的度为0的结点

    双亲结点和孩子结点:结点的子树被称为该结点的孩子结点,该结点就是被称为孩子的双亲结点

    ? 兄弟结点:具有同一个双亲结点的结点,互为兄弟结点

    ? 祖先结点:从根节点出发到该结点,路径上经过的所有结点都称为该结点的祖先

    ? 子孙结点:一个结点的子树中任意一个结点都是它的子孙

    ? 堂兄弟结点:双亲结点互为,兄弟结点。

  • 树的存储

    • 树可以顺序存储,链式存储,还可以混合存储

    • 可以根据存储的信息不同,树有以下存储方式:

      双亲表示法:顺序存储

      ? 位置 data 双亲下标

      ? 0 A -1(EOF)

      ? 1 B 0

      ? 2 C 0

      ? 3 D 1

      ? 4 E 1

      ? 5 F 1

      ? 6 G 2

      • 优点:方法寻找双亲
      • 缺点:查找孩子结点时麻烦

      孩子表示法:

      ? 顺序存储:耗费内存。array_son。

      ? 链式存储:节约了内存空间。ListHead。

      ? 位置 data 子节点下标(son_arr存储子节点下标)

      ? 0 A [1,2]

      ? 1 B [3,4,5]

      ? 2 C [6]

      ? 3 D []

      ? 4 E [7]

      ? 5 F

      ? 6 G

      ? 7 H

      • 优点:查找孩子下标方便
      • 缺点:查找双亲下标不便

? 兄弟表示法:

? 链式

? 双亲只存储第一个子节点 数据 链式指向所有兄弟节点

? 优点:可以方便找所有兄弟节点

? 缺点:找双亲不便

注意:普通数不常用,一般会使用二叉树进行存储

二叉树

? 一种常用的数据结构,比普通树处理起来要简单,而且普通也比较方便地转换成二叉树。

? 定义:节点的度为2。

二叉树是n个有限元素的集合,该集合或者为空、或者由一个称为根(root)的元素及两个不相交的、被分别称为左子树和右子树的二叉树组成,是有序树。当集合为空时,称该二叉树为空二叉树。在二叉树中,一个元素也称作一个节点 [1] 。

? 满二叉树:每层的节点数都是2^(i-1),则这种树就是满二叉树

1、满二叉树:如果一棵二叉树只有度为0的节点和度为2的节点,并且度为0的节点在同一层上,则这棵二叉树为满二叉树 [4] 。

2、完全二叉树:深度为k,有n个节点的二叉树当且仅当其每一个节点都与深度为k的满二叉树中编号从1到n的节点一一对应时,称为完全二叉树 [4] 。

二叉树性质(重点)

**性质1:**二叉树的第i层上至多有2^(i-1)(i≥1)个节点 [6] 。

**性质2:**深度为h的二叉树中至多含有(2^h)-1个节点 [6] 。

**性质3:**若在任意一棵二叉树中,有n0个叶子节点,有n2个度为2的节点,则必有n0=n2+1 。

**性质4:**具有n个节点的满二叉树深为log2n+1。

**性质5:**若对一棵有n个节点的完全二叉树进行顺序编号(1≤i≤n),那么,对于编号为i(i≥1)的节点: [6]

当i=1时,该节点为根,它无双亲节点 。

当i>1时,该节点的双亲节点的编号为i/2 。

若2i≤n,则有编号为2i的左节点,否则没有左节点 。

若2i+1≤n,则有编号为2i+1的右节点,否则没有右节点 。

二叉树的操作

? 构建,销毁,遍历,高度,密度,插入,删除,查询,求左,求右,求根。

二叉树的存储

顺序存储:必须要安照完全二叉树的格式,把每个节点按从上到下,从左到右的顺序依次存入连续的内存中,如果有空位置则使用特殊数据代替存入

数据项:存储节点的内存首地址,

链式存储

二叉树的遍历

  • 前序遍历:根,左,右
  • 中序遍历:左,根,右
  • 后序遍历:左,右,根

注意:无论是前中后,由根节点决定,并且左右子树的遍历顺序不会改变

根据前序+中序,或者,后序+中序 就可以还原一颗树

  • 层序遍历:从上到下,从左到右,依次遍历,一颗树。

注意:层序遍历,配合队列使用

有序二叉树

  • 左子树的数据小于根,右子树的数据大于根,这种树被称为有序二叉树,二叉树搜索树,二叉排序树

注意:这种树的节点需要频繁地插入,删除,因此不适合顺序存储

有序二叉树的中序遍历刚好就是升序或降序,所以有序二叉树也是一种排序算法,查找又天然是二分查找。

练习:

  1. 把一棵二叉树转换为它的镜像树。
  2. 输入两棵二叉树A,B,判断B是不是A的子结构(我们约定空树不是任意一个树的子结构)。
  3. 将一棵有序二叉树转换成一个有序的双向链表。
  4. 计算出有序二叉树中倒数第K个大的数。
  5. 判断一个二叉树是否对称。
  6. 请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其他行以此类推。

线索二叉树

  • 规律:在N个节点的链式二叉树中必定有N+1个空指针域(NULL指针)。
  • 有序链式二叉树中有很多的空指针。如果要使之有意义,可以让它们的空指针指向下一个,或前一个,这样在遍历时,可以不用递归,而可以使用循环遍历,提高树的遍历速度。

中序线索二叉树节点数据项

? 数据

? 左子树指针

? 右子树指针

? 由子树指针标准位

实现过程:

  1. 创建有序二叉树
  2. 建立线索
  3. 通过线索遍历二叉树

选择树(胜者树,败者树)

? 是一种完全二叉树,待比较的数据都存储在最后一层,根节点是根据左右子树其中一个生成,因此更节点是最大或者最小的,选择树的功能是快捷地找出最大值或最小值。

堆:是一种完全二叉树

  • 大顶堆(大根堆):根节点比左右子树大
  • 小顶堆(小根堆):根节点比左右子树小

数据项:

? 存储数据的内存首地址

? 容量

? 数量

平衡二叉树(AVL树)

前提是有序的二叉树,它的左右子树的高度不超过1,而且它的所有子树也满足这个条件

如果一个有序二叉树呈现单支状(类似链表),它的查找效率接近链表,因此只有达到平衡时它的查找效率最高。

由于节点的值受限,因此只能通过调整达到有序,而不能进行值的修改

二叉树不平衡的基础原因: Y

? X - 以Y为轴向右旋转 Z X

? / \ T1

? Y T1

Z

红黑树(了解)

红黑树是一种自平衡树,它不是根据子树的高度差来调整平衡的,而是给节点设置一种颜色,来达到平衡。

红黑树的特性:

  1. 每个节点或者是黑色,或者是红色
  2. 根节点必须是黑色
  3. 每个叶子节点(NULL)是黑色
  4. 如果一个节点是红色,则它的子节点必须是黑色。(不能有两个红色的节点)
  5. 从一个节点出发,到该节点的子孙节点的所有路径上包含了相同数量的黑色节点
    保证大致上红黑树是平衡的(最长路径不超过最短路径的两倍)。
  6. 插入的节点一定是红色

红黑树插入后的调整:

  • 插入的节点一定是红色
  1. 如果父节点是黑色,直接插入

  2. 如果父节点是红色,需要调整

    ? 叔叔不存在 or 叔叔为黑色

    ? 进行 左旋 or 右旋

    ? 祖父节点置红,父节点置黑

    ? 叔叔存在且为红色

    ? 祖父置红,父节点和叔叔置黑

    ? 把祖父节点当做当前节点

优点:插入,删除的效率比AVL树高

缺点:没有AVL树平均,查找效率没有AVL树高,但也并不差

哈夫曼树(了解)

  • 基本概念:

    • 路径长度:从一个节点到另一个节点之间的路径条目数。(根节点到第N层节点的路径长度为N-1)
      数的路径长度:从根节点出发,到每个节点的路径长度和。
      节点的权:若将书中节点赋予一个有某种意义的数值,该数值称为该节点的权
      节点的带权路径长度:从根节点出发到该节点的路径长度与该节点的权的乘积。
      树的带权路径长度:所有的叶子节点的带权路径长度的和,称为WPL
      (哈夫曼树是衡量一颗带权二叉树优劣的关键,目的是为了生成一颗WPL最小的带权二叉树)
  • 构建哈夫曼树:

    • 把N个带权节点存入一个集合中F中,把每个节点左右子树置空
    • 从F中选取权值最小的两个节点作为左右子树构建成一颗新的二叉树,且新的根节点的权为左右子树的权值之和
    • 从F中删除刚刚选出来的两个节点,把新得到的根节点放入F中
    • 重复2,3步骤,直到F中为空
  • 哈夫曼编码:

    • 目的:解决当年远距离通信(电报)的数据传输的最优解

      待发送的文字:BADCA DFEED

      方法1:转成二进制发送 A 000 B 001 共30个字符

      方法2:

      • 根据文字出现频率,构建哈夫曼树
        • 假设频率:A27 B8 C15 D15 E30 F5
          • A(01) B (1001) C (101) D (00) E (11) F (1000)
            • BADCA DFEED(1001 01 00 101 01 00 1000 11 11 00)
      • 根据哈夫曼树的左右分支为0,右分支为1,则从根节点到叶子节点经过的路径分支所组成的0,1编码序列为该对应字符的哈夫曼编码
    • 作用:数据压缩,文件压缩的其中一种方式

数组与矩阵

  • 数组:存储空间连续的表结构

  • 矩阵:带二维信息的数据,一般用二维数组来存储矩阵

  • 特殊矩阵:

    • 稀殊矩阵:有效的信息数据不多,绝大多数都是无效信息数据不需要存储,没有特定的标准,全凭感觉

      这些矩阵如果使用二维数组来存储的话,会非常浪费存储空间,为了节约空间,我们可以对这些矩阵进行压缩

      • 普通的稀蔬矩阵压缩方式:
        • 三元组:有三个数据项:行,列,值。
    • 上三角矩阵

      [X][X][X][X]
      [ ][X][X][X]
      [ ][ ][X][X]
      [ ][ ][ ][X]
      /*
      压缩方法:使用一维数组进行存储
      上三角矩阵N行N列:	第i行	第J列
      一维数组长度:n*(n+1)/2
      对应关系:(j+1)*j/2+i
      数据位置:i >= j
      */
      
    • 下三角矩阵

      [X][ ][ ][ ]
      [X][X][ ][ ]
      [X][X][X][ ]
      [X][X][X][X]
      /*
      压缩方法:使用一维数组进行存储
      上三角矩阵N行N列:	第i行	第J列
      一维数组长度:n*(n+1)/2
      对应关系:(i+1)*i/2+j
      数据位置:i <= j
      */
      
    • 对称矩阵

      [-][1][2][3]
      [1][-][ ][ ]
      [2][ ][-][ ]
      [3][ ][ ][-]
      
    • 对角矩阵、对状矩阵

      [-][1][ ][ ]
      [2][-][3][ ]
      [ ][4][-][5]
      [ ][ ][6][-]
      /*
      数组长度:3*N-2
      对应关系:i*2+j
      i和j要满足:abs(i-j) <= 1  的位置有值
      */
      

  • 图型结构:由有穷且非空的顶点和顶点之间边的集合组成
  • 通常表示:G(V,E) G表示一个图,V是图中顶点集合(元素),E是图中所有边(元素之间的关系)的集合
  • 无向图:
    • 边用(A,B)方式表示,A到B之间互通
  • 完全无向图:
    • 在无向图中,任意两个顶点之间都有边,这种叫完全无向图。
    • 含有N个顶点的完全无向图中,共有N*(N-1)/2
  • 有向图:
    • 边<A,B>方式表示,仅仅表示A点到B点的边,有向图中边也叫做弧,A是弧头,B是弧尾
  • 有向完全图:
    • 在有向图中任意两个顶点之间都有方向相反的两条弧,这种图叫做有向完全图
    • 含有N个顶点的有向完全图中,共有N*(N-1)条边

注意:不讨论顶点到自身的边,且不讨论重复的边,这种图统称为简单图,数据结构中只研究简单图

稀疏图:顶点多边少的图称为稀疏图,反之称为稠密图。

边的权重:图中的边附带有意义的数据,这些数据叫做边的权重,带权重度图,也称为网

度:依附于顶点的边的数量称为该顶点的度,有向图中,度分为出度(从该顶点出发的弧的数量),入度(指向该顶点的弧的数量)

路径:从一个顶点出发,到一个顶点之间,经过的边加路径

路径长度:路劲的边的条目树

环:图中从某个顶点出发,最终回到该点

回路:专指有向图,从某个点出发,最终会到该点

简单路径:边经过顶点序列中不重复的路径称为简单路径

简单回路:除了第一个顶点和最后一个顶点外,其余顶点不重复的回路称为简单回路

连通:如果顶点V到顶点V1之间路径,则成V和V1是连通的

连通图:任意顶点之间都是连通的,称之为连通图,如果一个图有N个低昂单则至少需要N-1条百年才能达到连通图

生成树顶点数为N,仅仅需要N-1条边的连通图,称之为生成树,如果个边配上权重,权重和最小的生成树称之为最小生成树

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include "list_queue.h"

//	邻接矩阵
typedef struct Graph
{
    char* v;	//	顶点一维数组
    char* e;	//	边的二维数组
    int cnt;	//	顶点数量
}Graph;
Graph* create_graph(const char* str)
{
    //	申请邻接矩阵内存
    Graph*graph = malloc(sizeof(Graph));
    //	计算顶点数量
    graph->cnt = strlen(str);
    //	申请顶点所需堆内存
    graph->v = malloc(graph->cnt);
    //	存储数据
    strcpy(str->v,str);
    //	申请存储边所需内存
    graph->v = calloc(graph->cnt,graph->cnt);
    return graph;
}
bool add_edge(Graph* graph,char v1,char v2)
{
    int x = -1, y = -1;
    for(int i = 0 ; i < graph->cnt ; i ++ ){
        if( graph->v[i] == v1 ) x = i;
        if( graph->v[i] == v2 ) y = i;
    }
    if( x == -1 || y == -1 ) return false;
    graph->e[x*graph->cnt+y] = 1;
    return true;
}
void show_graph(Graph* graph)
{
    printf(" ");
    for(int i = 0 ; i < graph->cnt ; i ++ ){
        printf(" %c",graph->v[i]);
    }
    pirntf("\n");
    for(int i = 0 ; i < graph->cnt ; i ++ )
    {
        printf("%c ",graph->v[i]);
        for(int j = 0 ; j < graph->cnt; j ++){
            printf("%hhd ",graph->e[i*graph->cnt+j]);
        }
        printf("\n");
    }
}
int od_graph(Graph* graph,char v)
{
    for(int i = 0 ; i < graph->cnt ; i ++ )
    {
        if( v == graph->v[x] )
        {
            int od = 0;
            for(int j = 0 ; j < graph->cnt ; j ++ )
            {
                od += graph->e[x*graph->cnt+y];
            }
            return od;
        }
    }
    return -1;
}
int id_graph(Graph* graph)
{
    for(int y = 0 ; y < graph->cnt ; y ++ )
    {
        if( v == graph->v[y])
        {
            int id = 0;
            for(int x = 0 ; x < graph->cnt ; x ++ )
            {
                id += graph->e[x*graph->cnt+y];
            }
            return id;
        }
    }
    return -1;
}
void _DFS(Graph* graph,int i,char* vflag)//	从第I个顶点进行深度优先遍历
{
    if( vflag[i] ) return;
    printf("%c ",graph->v[i]);
    vflag[i] = 1;
    for(int j = 0 ; j < graph->cnt ; j ++ ){
        if( graph->e[i*graph->cnt+j] ) _DFS(graph,j,vflag);
    }
}
void DFS_show(Graph* graph)
{
    //	给顶点加标志位
    char vflag[graph->cnt];
    memset(vflag,0,graph->cnt);
    for(int i = 0 ; i < graph->cnt ; i ++ )
    {
        _DFS(graph,0,vflag);
    }
}
void BFS_show(Graph* graph)
{
    char vflag[graph->cnt];
    memset(vflag,0,graph->cnt);
}
int main(int argc,const char* argv[])
{
    Graph* graph = create_graph("ABCDEFG");
}

图的遍历:深度优先遍历和广度优先遍历

  • 深度优先遍历(DFS):
  • 广度优先遍历(BFS):
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include "list_queue.h"
//	边
typedef struct Edge
{
    int index;			//	边指向顶点的下标
    struct Edge* next;	//	指向下一条边
}Edge;
//	顶点
typedef strcut Vertex
{
    char vertex;	//	顶点数据
    Edge* first;	//	指向该顶点的第一条边
}Vertex;
//	图
typedef struct Graph
{
    Vertex* v;	//	顶点数组
    int cnt;	//	顶点数量
}
int od_graph(Graph* graph,char v)
{
}
int main(){
     
}

算法

  • 数据结构中的算法,指的是数据结构所具备的功能(决绝待定概念的方法)

  • 评价一个算法:

    • 时间复杂度:算法的执行次数来代表算法的时间复杂度
      一般使用O(公式) 一般忽略常数
    • 空间复杂度:执行一个程序所需要的内存空间大小,是对一个算法在运行工程中临时占用存储空间大小的衡量。(一般只要算法不涉及东岱分配的内存以及递归,通常空间复杂度为O(1))

    注意:对于一个算法而言,其时间复杂度与空间复杂度往往是相互影响的,没有一个标准,需要结合实际

分治:分而治之,将一个大且复杂的问题,分解成小且简单的问题

  • 实现分治的方法:循环,递归

  • 查找算法:

    • 顺序查找

      • 对待查找的数据没有要求,从头到尾逐一比较,在小规模的查找中比较常见,
      #include <stdio.h>
      #include <stdlib.h>
      
      #define swap(a,b) {typeof(a) t = (a); (a) = (b) ; (b) = t;}
      #define show_arr(arr,len) { for(int i = 0 ; i < len ; \
      printf("%d ",arr[i++]) ); }
      
      //	顺序查找
      int order_search(int* arr,int len,int key){
          for(int i = 0 ; i < len ; i ++ ) {
              if( key == arr[i] ) return i;
          }
          return -1;
      }
      //	计数排序
      void count_sort(int* arr,size_t len) {
          int max = arr[0], min = arr[0];
          for(int i = 1 ; i < len ; i ++ ) {
              if( arr[i] > max ) max = arr[i];
              if( arr[i] < min ) min = arr[i];
          }
          //	哈希表
          int* tmp = calloc(4,max-min+1);
          //	标记哈希表
          for(int i = 0 ; i < len ; i ++ ){
              tmp[ arr[i] - min ] ++ ;
          }
          for(int i = 0, j = 0; i <= max-min ; i ++ ) {
              while( tmp[i]-- ) {
                  arr[ j ++ ] = i + min;
              }
          }
      }
      //	哈希查找
      bool hash_search(int* arr,int len,int key){
          //	直接定址法
          int hash[1000000] = {};
          for(int i = 0 ; i < len ; i ++ ){
              hash[ arr[i] ] ++ ;
          }
          return hash[ key ];
        //	数据分析法
          int max = arr[0], min = arr[0];
        for(int i = 0 ; i < len ; i ++ ){
              if( arr[i] > max ) max = arr[i];
            if( arr[i] < min ) min = arr[i];
          }
          int hash[ max-min+1 ];
        memset(hash,0,sizeof(hash));
          for(int i = 0 ; i < len ; i ++ ){
            hash[ arr[i-min] ] ++;
          }
        return hash[ key-min ];
      }
      void (){
          bool flag = true;
          for(int i = len-1 ; i > 0 && flag ; i -- ){
              flag = false;
              for(int j = 0 ; j < i ; j ++ ){
                  if( arr[j] > arr[j+1] ){
                      swap(arr[j],arr[j+1]);
                      flag = true;
                  }
              }
          }
      }
      void select_sort(TYPE* arr,size_t len){
          for(int i = 0 ; i < len-1 ; i ++ ){
              int min = i;
              for(int j = i + 1 ; j < len ; j ++ ){
                  if( min > arr[j] ) min = j;
              }
              if( i != min ) swap(arr[i],arr[min]);
          }
      }
      //	插入排序
      void insert_sort(TYPE* arr,size_t len){
          for(int i = 1 , j = 0 ; i < len ; i ++ ){
              int temp = arr[i];
              for( j = i-1 ; j >= 0 && arr[j] > temp; j -- ){
                  arr[j+1] = arr[j];
              }
              if( j+1 != i ) arr[j+1] = temp; 
          }
      }
      //	希尔排序
      void shell_sort(TYPE* arr,size_t len){
          for(int k = len/2 ; k > 0 ; k /= 2 ){
              for(int i = k ; i < len ; i ++ ){
                  int temp = arr[i];
                  for(int j = i-k ; j >= 0 && arr[j] > temp; j -= k ){
                      arr[j+k] = arr[j];
                  }
                  if( j+k != i ) arr[j+1] = temp; 
          	}
          }
          
      }
      int main(){
          int arr[10];
          for(int i = 0 ; i < 10 ; i ++ ) {
              arr[i] = rand()%100;
              printf("%d ",arr[i]);
          }
      }
      
    • 二分查找

      • 待查找的数据必须有序,从数据中间位置开始比较查找。
      • 时间复杂度:O(logN)
    • 块查找

      • 是一种数据处理的理想,不是一种特定的算法,当数据量非常多时,可以先把数据进行分块处理,然后再根据分块的条件进行查找
    • 哈希查找 (Hash)

      • 数据 经过 哈希函数 计算出数据在哈希表中的位置,然后标记位置,方便之后的查找,它的时间复杂度可以达到O(1)
      • 哈希函数的设计方法:
        • 直接定址法:直接把数据当做哈希表的下标,把哈希表中该下标的位置+1
        • 数据分析法:
        • 平方取中法,折叠法,随机数法,但都无法保证哈希数据的唯一性,出现所谓的哈希冲突,一般使用链表解决
      • Hash函数的应用:MD5,SHA-1都属于Hash算法中的应用
  • 排序算法:

    • 排序算法的稳定性:如过有值相同的数据,在排序的过程中的全程中都不会改变它们的先后顺序,则认为该排序算法是稳定的

    • 冒泡排序:将相邻的左右数据进行比较,按一定规则(升序:左比右大,交换。降序:左比右小,交换),该算法对数据的有序性比较敏感。如果,待排序的数据基本有序,则效率高。

      • 时间复杂度:最优O(n),平均:O(n^2)
      • 稳定
    • 选择排序:假定最开始的位置是最小值,并记录计入下标min,然后与后面的数据比较,如果有比min为下标的数据还要小,则更新min,最后判断如果min的值发生了该变,则交换min位置的数据与最开始的数据。

      • 时间复杂度:O(n^2)

      虽然选择排序的时间复杂度较高,但是数据交换次数少,因此实际运行效率并不慢

      是冒泡排序的变种,但是没有对数据有序敏感性,数据混乱情况下比冒泡块

      注意:算法的时间复杂度不能代表算法的实际时间,有时候时间复杂度高的反而速度更快

  • 插入排序:将数据看做两部分,一部分是有序的,将剩余数据逐个插入到有序的

    • 时间复杂度O(n^2)
    • 稳定
  • 希尔排序:是在插入排序的增强版,由于插入排序数

  • 快速排序:找到一个标杆,备份标杆的值val,一面从左找比val大的数,找到后,交换给P赋值,更新标杆P的位置到左标杆;在右边找比val小的数,找到后也赋值给P,更新P标杆到右标杆的位置。最终形成左边比右边小,右边比左边大。之后,用同样方法进行快排。

    • 时间复杂度:O(NlogN) 不稳定 快速排序的综合性能最高。
    #define TYPE int
    void _quick_sort(TYPE* arr,size_t left,int right){
        if( left >= right ) return;
        //	计算标杆下标
        int pi = (left+right)/2;
        //	备份标杆的值
        TYPE pv = arr[pi];
        //	备份左右标杆下标
        int l = left, r = right;
        while( l < r ){
            //	在标杆pi左边找比标杆大的数
            while( l < pi && arr[l] <= pv ) l++;
            if( l < pi ){
                //	找到了比pv大的数
                arr[pi] = arr[l];
                //	更新pi
                pi = l;
            }
            //	从pi的右边找比PV小的数
            while( r > pi && arr[r] >= pv ) r--;
            if( r > pi ){
                //	找到比pv小的数
                arr[pi] = arr[r];
                //	更新pi
                pi = r;
            }
        }
        //	pi左边小于PV,右边大于PV,还原PV
        arr[pi] = pv;
        if( pi - left > 1 ) _quick_sort(arr,left,pi-1);
        if( right - pi > 1 ) _quick_sort(arr,pi+1,right);
    }
    void quick_sort(TYPE* arr,size_t len){
        _quick_sort(arr,0,len-1);
    }
    
  • 归并排序:先把一组待排序的数据分成单独的个体,存放到临时空间中,然后两两比较合并。

    • 时间复杂度:O(NlogN) 稳定
//	拆分合并
void _merge_sort(int* arr,int* temp,int l,int r){
    if( l >= r ) return;
    int p = (l+r)/2;
    _merge_sort(arr,temp,l,p);
    _merge_sort(arr,temp,p+1,r);
    //	左右部分各自有序
    if( arr[p] <= arr[p+1] ) return ;
    int i = l, j = p + 1, k = l;
    while( i <= p && j <= r ){
        if( arr[i] < arr[j] ){
            temp[k++] = arr[i++];
        }else{
            temp[k++] = arr[j++];
        }
    }
    //	比完后还有没比较的放入temp中末尾
    while( i <= p ) temp[k++] = arr[i++];
    while( j <= r ) temp[k++] = arr[j++];
    memcpy(arr+l,temp+l,(r-l+1)*sizeof(int));
}
void merge_sort(int* arr,size_t len){
    int* temp = malloc(sizeof(int)*len);
    _merge_sort(arr,temp,0,len-1);
    free(temp);    
}
  • 堆排序:

  • 计数排序:找出数据中的最大值和最小值,并创建哈希表,把 数据-最小值 作为数据的下标访问哈希表并标记数量,标记完后,遍历哈希表,当表中的值大于0,把下标+最小值 还原数据一次放回数组中,是一种典型的空间换时间的算法

    • 该排序算法理论上速度非常快,它不是基于比较的算法,在一定范围内整数排序时快于任意的一种比较排序算法,但是有很大的局限性:适用排序整性数据,而且数据的范围差别不大
  • 桶排序:根据数据的值存储到不同的桶中,然后再调用其它的排序算法,度桶中的数据进行排序,然后再从桶中依次拷贝回数组中,从而降低排序的规模以此提高排序的速度,是一种典型的以空间换时间的算法

    • 缺点:如何分桶,桶范围多大,这些都需要对数据由一定的了解
    • 桶排序的稳定性取决于桶内排序中使用的算法
    //	cnt桶的数量	ragne桶的数据范围
    void _bucket_sort(int* arr,size_t,int cnt,int range) {
        //	申请桶内存,bucket指向每个桶的开头,bucket_end指向每个桶的末位
        int* bucket[cnt], *bucket_end[cnt];
        for(int i = 0 ; i < cnt ; i ++ ) {
            //	数据可能在一个桶中
            bucket[i] = malloc(len*sizeof(int*));
            //	末尾指针指向开头
            bucket_end[i] = bucket[i];
        }
        for(int i = 0 ; i < len ; i ++ ) {
            for(int j = 0 ; j < cnt ; j ++ ) {
                if( range*j <= arr[i] && arr[i] < range*(j+1) ) {
                    *(bucket_end[j]) = arr[i];
                    bucket_end[j] ++ ;
                }
            }
        }
        for(int i = 0 ; i < cnt ; i ++ ) {
            //	计算每个桶中元素数量
            int size = bucket_end[i] - bucket[i];
            if( size > 1 ) {
                调用排序算法
            }
            memcpy(arr,bucket[i],size*sizeof(int));
            arr += size;
        }
    }
    //	桶排序
    void bucket_sort(int* arr,size_t len) {
        _bucket_sort(arr,len,4,25);
    }
    
  • 基数排序:

    • 桶排序的基本实现,首先创建10个队列(链式队列),然后逆序计算出数据的个位十位,百位,然后入队到对应的队列中,结束后依次从队列中出队会数组中,数据下一位继续入队,依次循环,最大值就是位数就是循环
    • 时间复杂度:O(n+k)
    void radix_sort(int* arr,size_t len) {
        //	创建10个队列
        ListQueue* queue[10] = {};
        for(int i = 0 ; i < 10 ; i ++ ){
            queue[i] = create_list_queue();
        }
        int max = arr[0];
        for(int i = 1 ; i < len ; i ++ ){
            if( arr[i] > max ) max = arr[i];
        }
        //	计算最大值位数
        int cnt_max = 0;
        while( max ){
            cnt_max ++ ;
            max /= 10;
        }
        for(int i = 1 ; i <= cnt_max ; i ++ ) {
            int mod = pow(10,i);
            int div = mod/10;
            //	把所有数据入队
            for(int j = 0 ; j < len ; j ++ ){
                //	逆序获取每个树的每一位数
                int index = arr[j]%mod/div;
                //	入到对应下标的队列中
                push_list_queue(queue[index],arr[j]);
            }
            int k = 0;
            for(int j = 0 ; j < 10 ; j ++ ){
                while( !empty(queue[j]) ){
                    arr[k++] = front(queue);
                    
                }
            }
        }
    }
    

系统内存管理

内存管理

用户层

  • STL 自动分配、释放内存 调用C++
  • C++ new/delite 调用C
  • C malloc/free 调用POSIX或Linux
  • POSIX brk/sbrk 调用内核
  • Linux mmap/munmap 调用内核kernal

系统层

  • kernal kmalloc/vmalloc 调用驱动
  • 驱动 get_free_page

进程映像

  • 程序是存储在磁盘上的可执行文件,当执行程序时,系统会把可执行程序加载到内存中。在内存中运行中的程序就是进程。一个程序可以加载多个进程!
  • 进程的内存分别情况就是所谓的进程映像,从低地址到高地址以此分布为:
    • text 代码段 二进制指令,常量(字符串字面值,被const修饰过的"原data段数据")
    • data 数据段 初始化的全局变量和静态局部变量
    • bss 静态数据段 未初始化的全局变量和静态局部变量(该段内存在程序运行前,会自动清零)
    • heap 堆 由程序员手动管理的体量较大的数据
    • stack 栈 局部变量和快变量,大小有限,不会有内存碎片
    • environ 环境变量表 环境变量,每个进程都有一份,修改不会影响其它进程
    • argv 命令行参数 通过程序运行前命令附加的参数

练习:定义各个内存段的数据,然后分别打印它们的内存地址编号,与该进程的maps内存记录文件中对应

? /proc/进程id/maps

? 查询进程ID 命令:ps -aux

? 函数:getpid()

虚拟内存

  1. 系统会为每个进程分配4G的虚拟内存空间
  2. 用户只能使用虚拟机内存,无法直接使用物理内存
  3. 虚拟地址与物理内存进行映射后才能使用(否则就会产生段错误)
  4. 虚拟地址物理内存之间的映射是由操作系统动态维护(malloc)
  5. 让用户使用虚拟地址,一方面是为了安全,另一方面操作系统可以让应用程序使用实际物理内存更大的地址空间
  6. 4G的虚拟地址空间分为两部分:一部分为用户空间[0G3G),一部分为内核空间[3G4G]
  7. 用户空间中的代码不能直接访问内核空间的代码和数据,可以通过系统调用(API)从用户态切换到内核态后,间接与内核交换数据
  8. 对虚拟内存越界访问(使用了没有映射的虚拟内存),导致段错误

映射虚拟内存和物理内存的函数

  • sbrk/brk/mmap/munmap

  • 关于malloc获取映射好的虚拟内存地址的底层逻辑:

    1. 如果分配的内存小于128kb 调用sbrk/brk
    2. 如果大于128KB,调用mmap,munmap
  • 操作系统内部维护着一个指针指向内存映射的最后一个字节的下一个字节

    注意:系统映射内存是以页(1页=4096字节)为单位

    注意:sbrk/brk属于POSIX标准中的内存映射函数,都可以单独映射,取消映射,但是一般配合使用(sbrk映射,brk取消映射)

void *sbrk(intptr_t increment);
/*
功能:根据增量参数调整该指针的位置,既能映射也能取消映射
intcrement:增量
	0		获取指针的位置
	>0		映射内存
	<0		取消映射
返回值:返回该指针在移动前的位置
*/
int brk(void *addr);
/*
功能:直接使用addr的值,修改该指针的位置,既能映射也能取消映射
addr:
	> 位置指针	映射内存
	< 指针指针	取消映射
返回值:成功返回0,失败返回-1;
*/

练习:计算出1000个素数,存储到堆内存中,尽量不要浪费内存

#include <stdio.h>
#include <unistd.h>
#include <stdbool.h>
#include <math.h>
bool is_prime(int num){
    for(int i = 2 ; i <= sqrt(num) ; i ++ ){
        if( num % i == 0 ) return false;
    }
    return true;
}
int main(){
    int *arr = sbrk(0),cnt = 0;
    for(int i = 2 ; cnt < 1000 ; i ++ ){
        if( is_prime ){
            sbrk(4);
            arr[cnt++] = i;
            printf("%d ",arr[cnt-1]);
        }
    }
    brk(arr);
}
#include <sys/mman.h>
void *mmap(void *addr,size_t length,int prot,int flags,int fd,off_t offset);
/*
功能:映射虚拟内存与物理内存
addr:要映射的区域的首地址,可以自己制定,如果是NULL则系统帮你制定
length:映射的字节长度
prot:映射权限
	PROT_EXEC		执行权限
	PROT_READ		读权限
	PROT_WRITE		写权限
	PROT_NONE		没有权限
	例如:读写权限:PROT_READ | PROT_WRITE	
flags:映射标志
	MAP_ANONYMOUS	将虚拟内存映射到物理内存,而不是映射文件,因此忽略fd,offset。必须与
	MAP_SMAPED或MAP_PRIVATE中任意一个配合使用
	MAP_SMAPED		将虚拟内存映射到文件,并共享映射
	MAP_PRIVATE		将虚拟机内存映射到文件,不共享映射
fd:文件描述符
	0	不映射文件时,给0
offset:文件的偏移量
	0	不映射文件给0
返回值:成功放回映射后的首地址,失败返回MAP_FAILED((void*)-1等同于0xFFFFFFFF)
*/
int munmap(void* addr,size_t length);
/*
功能:取消映射
addr:要映射的首地址
length:要取消映射的字节长度
返回值:成功返回0,失败返回-1
*/

内存管理总结

  1. sprk/prk底层维护了一个指针,该指针记录着映射内存的结尾,移动该指针就会映射,取消映射。映射的内存属于堆内存
  2. mmap/munmap底层不维护任何东西,如果mmap映射成功返回映射后的内存首地址,映射的内存属于堆内存
  3. malloc/free底层调用了sbrk/brk/mmap/munmap
  4. 每个进程都有4G的虚拟地址空间,虚拟地址只是虚假的地址编号,并不是真实物理内存编号,虚拟地址必须必须和物理内存进行映射后才能使用
  5. 平时所说的堆内存的分配和释放有两层含义:
    • 权限的分配和回收
    • 映射关系的建立
  6. 重点是理解Linux系统对于内存的管理机制,而不是sbrk/brk/mmap/munmap的用法

文件操作

系统调用(系统API)

  • 系统调用就是操作系统提供的一些功能给程序员调用,这些系统调用被封装成C函数的形式提供给程序员,但是它们不是函数且不是标准C中的一部分
  • 一般应用程序运行在用户态[03G)上,当使用系统调用时运行在内核态[34G)
  • 常用的标准C库大部分时间工作在用户态,底层偶尔会调用系统调用进入内核态,结束调用后会转回用户态

ldd ./a.out 追踪系统调用

time ./a.out 计算程序运行时间

? real 总执行时间

? user 用户态总时间

? sys 内核态总用时

? real = user + sys + 切换时间

strace ./a.out 可以追踪系统的底层命令

  • 系统调用的代码是内核的一部分,其外部接口以函数形式定义在共享库中(linux-gate.so,ld-linux.so),这些接口的实现利用了软中断进入内核态执行真正的系统调用

一切皆文件

UNIX/Linux系统把所有的服务,设备等一切内容都抽象成了文件,并提供了一套简单而统一的接口,这部分接口就是系统文件读写调用,简称系统IO

标准C库提供的文件读写函数称为标准IO

也就是说UNIX、Linux系统中任何对象都可以被当做文件看待,可以以文件形式访问

  • 文件的分类
    • 普通文件 “-” 包含二进制,文本,压缩,库文件
    • 目录文件 “d” 有执行文件才能访问
    • 块设备文件 “b” 保存大块数据的设备,例如硬盘
    • 字符设备文件 “c” 存储与字符相关设备文件,例如:键盘,鼠标
    • 管道文件 “p” 与进程键通信相关文件
    • Socket文件 “s” 与网络通信相关文件,通常用于网络数据连接
    • 链接文件 “l” 类似Windows快捷方式

文件相关的系统调用

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
功能:打开文件
pathname:打开文件的路径
flags:打开文件的方式
    O_RDONLY	只读
    O_WRONLY	只写
    O_RDWR		读写
    O_CREAT		文件不存在则创建
    O_APPEND	追加在末尾
    O_EXCL		如果文件存在则创建失败
    O_TRUNC		如果文件存在则清空打开
返回值:文件描述符 0以上的整数 也是表示一个打开的文件的凭证
int open(const char *pathname, int flags, mode_t mode)
功能:打开打开文件
pathname:打开文件路径
flags:打开文件的方式,有O_CREAT
mode:文件权限
    S_IRWXU  00700	拥有者		读写执行权限
    S_IRUSR  00400 				读写权限

              S_IWUSR  00200 

              S_IXUSR  00100 user has execute permission

              S_IRWXG  00070 group has read, write, and execute permission

              S_IRGRP  00040 group has read permission

              S_IWGRP  00020 group has write permission

              S_IXGRP  00010 group has execute permission

              S_IRWXO  00007 others have read, write, and execute permission

              S_IROTH  00004 others have read permission
放回值:文件描述符

int creat(const char *pathname, mode_t mode);
功能:创建文件
mode:等同于open的mode
返回值:文件描述符
    
#include <unistd.h>
ssize_t write(int fd,const void *buf,size_t count);
功能:把内存中数据写入到文件中
fd:文件描述符 open的返回值
buf:待写入数据的内存首地址
count:要写入的字节数
返回值:成功写入的字节数
ssize_t read(int fd,void *buf,size_t count);
功能:从文件中读取数据到内存中
fd:文件描述符
buf:要读取的字节数据
返回值:实际读取到的字节数
int close(int fd);
功能:关闭一个文件
返回值:成功返回0,失败返回-1

练习:分别使用标准IO(fwrite)和系统IO(write)写入一百万个随机整数到文件中

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

void std_io(void){
    FILE* fwp = fopen("text.txt","w");
    if( fwp == NULL ){
        perror("fopen");
        return;
    }
    for(int i = 0 ; i < 1000000 ; i ++ ){
        int num = rand();
        fwrite(&num,sizeof(num),1,fwp);
    }
    fclose(fwp);
}

void sys_io(void){
    int fd = open("text,txt",O_WRONLY|O_CREAT|O_TRUNC,0644);
    if( fd < 0 ){
        perror("open");
        return;
    }
    for(int i = 0 ; i < 1000000 ; i ++ ) {
        int num = rand();
        write(fd,&num,sizeof(num));
    }
    close(fd);
}
int main(){
    std_io();
}

结论:使用标准IO比直接使用系统IO更快,因为标准IO有缓冲区机制,写入数据时,并不是直接调用系统IO进行写入,而是把缓冲区写满后,再进行系统调用写入到文件中,而直接使用系统IO会反复地切换用户态和内核态,更加耗时。

标准IO > 系统IO

系统IO + 缓冲区 > 标准IO

练习:使用系统IO实现一个带铺盖检查的CP命令

#include <stdio.h>
#include <string.h>
#include <getch.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc,const char* argv[]){
    if( argc != 3 ){
        puts("User:./Cp src dest");
        return 0;
    }
    int src = open(argv[1],O_RDONLY);
    if( src < 0 ) {
        puts("源文件不存在");
    }
    int dest = open(argv[2],O_WRONLY|O_CREAT|O_EXCL);
    if( dest < 0 ) {
        puts("目标文件已存在,是否覆盖(y/n)");
        char cmd = getch();
        puts("%c",cmd);
        if( cmd != 'Y' && cmd != 'y' ) {
            puts("停止拷贝!");
            close(src);
            return 0;
        }else{
            puts("进行覆盖!");
    		dest = open(argv[2],O_WRONLY|O_TRUNC);
        }
    }
    char buf[4096] = {};
    int ret = 0;
    while( ret = read(src,buf,sieof(buf)) ){
        write(dest,buf,ret);
    }
    close(src);
    close(dest);
}

随机读写:

  • 每个打开的文件都有一个记录读写位置的指针,叫做文件位置指针,所有对文件的读写操作都是从该指针的位置进行的,该指针会随着文件的读写自动往后移动当需要调整读写位置时,通过fseek/lseek进行调整
off_t lseek(int fd,off_t offset,int whence);
fd:
offset:
whence:基础位置
    SEEK_SET	开头
    SEEK_CUR	当前
    SEEK_END	结尾
返回值:成功返回调整后位置指针的位置,失败返回-1
注意:在越过文件末尾的位置写入数据,则原末尾与数据之间形成"空间"
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
//	使用系统IO,计算文件大小
int file_size(const char* path)
{
    int fd = open(path,O_RDONLY);
    if( fd < 0 ){
        perror("open");
        return -1;
    }
    int size = lseek(fd,0,SEEK_END);
    close(fd);
    return size;
}
int main(){
    
}

系统IO的文件文本读写:

  • 系统IO没有类似fprintf\fscanf函数,因此不能直接以文本形式读写文件
  • 写文本文件:
    • 先把各种类型数据内容通过sprintf转换成字符串,然后再通过write写入文件
  • 读取文本文件:
    • 先将各种类型数据内容通过sscanf转换成字符串,然后再通过

文件描述符:

  1. 非负整数,代表打开的文件
  2. 通过系统调用open、create返回,可以被内核引用
  3. 它代表了一个内核对象,因为内核不能暴露它的内存地址,不能返回真正的内核对象地址,只能用文件描述符来对应
  4. 内核中有一张打开文件的表格,文件描述符是访问这张表的下标,因此也称**“句柄”**,相当于访问已打开文件的凭证

内核中有默认一直打开的文件描述符

? 0 标准输入文件 scanf底层read 对应的文件指针stdin

? 1 标准输出文件 printf底层write 对应的文件指针stdout

? 2 标准错误文件 对应的文件指针stderr

文件描述符的复制:

int dup(int oldfd);
功能:复制一个已打开的文件描述符
返回值:返回一个当前没有使用过的最小的文件描述符
int dup2(int oldfd,int newfd);
功能:复制一个指定的文件描述符
返回值:成功返回newfd,失败返回-1
//注意:复制成功后,相当于两个不同值的文件描述符对应同一个东西。
//注意:其中一个关闭,不影响另一个使用。但,它们共享同一个文件位置指针

文件同步问题

  1. 在写入数据时,内存到磁盘之间有一块缓冲区,这种磁盘降低了磁盘读写次数,提高了读写效率
  2. 但是这种机制带来的后果是磁盘的数据与实际写入的数据不匹配,系统提供了三个函数可以让缓冲区的数据立即写入到磁盘
#include <unistd.h>
void sync(void);
/*
功能:立即把缓冲区中的数据同步到磁盘
注意:并不会等待数据同步结束才返回,而是提交要同步的数据到写入对列中,就返回
*/
int fsync(int fd);
/*
功能:把指定文件的内容同步到磁盘
注意:会等待完全写入磁盘后才返回
*/
int fdatasync(int fd);
//功能:把指定文件的内容同步到磁盘,只同步文件内容不同步文件属性

/*文件属性*/
int stat(const char *pathname,struct stat *buf);
功能:根据文件路径获取文件属性
buf:存储文件属性的结构体	是一个输出型参数
int fstat(int fd,struct stat *buf);
功能:根据文件描述符,获取文件属性
int lstat(const char *pathname,struct stat *buf);
功能:根据文件路径获取软连接文件属性
    
     S_IFMT     0170000   获取文件类型的掩码

           S_IFSOCK   0140000   socket
           S_IFLNK    0120000   symbolic link		软链接文件
           S_IFREG    0100000   regular file		普通文件
           S_IFBLK    0060000   block device		块设备文件
           S_IFDIR    0040000   directory			目录文件
           S_IFCHR    0020000   character device	字符设备文件
           S_IFIFO    0010000   FIFO				管道文件
    
    上述类型判断咋POSIX中定义了以下函数进行类型判断
           S_ISREG(m)  is it a regular file?	是普通文件?
           S_ISDIR(m)  directory?				是目录文件?
           S_ISCHR(m)  character device?		是字符设备文件?
           S_ISBLK(m)  block device?			是块设备文件
           S_ISFIFO(m) FIFO (named pipe)?		是管道文件?
           S_ISLNK(m)  (Not in POSIX.1-1996.)	是软连接文件
           S_ISSOCK(m) socket?  (Not in POSIX.1-1996.)

    st_mode包含权限信息
           S_ISUID     04000   set-user-ID bit
           S_ISGID     02000   set-group-ID bit (see below)
           S_ISVTX     01000   sticky bit (see below)

           S_IRWXU     00700   owner has read, write, and execute permission
           S_IRUSR     00400   owner has read permission
           S_IWUSR     00200   owner has write permission
           S_IXUSR     00100   owner has execute permission

           S_IRWXG     00070   group has read, write, and execute permission
           S_IRGRP     00040   group has read permission
           S_IWGRP     00020   group has write permission
           S_IXGRP     00010   group has execute permission

           S_IRWXO     00007   others (not in group) have read,  write,  and
                               execute permission
           S_IROTH     00004   others have read permission
           S_IWOTH     00002   others have write permission
           S_IXOTH     00001   others have execute permission


#include <sys/types.h>
#include <pwd.h>
struct passwd *getpwuid(uid_t uid);	//	用户
#include <grp.h>
struct group *getgrgid(gid_t gid);	//	组

文件权限

#include <unistd.h>
int access(const char *pathname,int mode);
功能:测试当前用户对文件的权限
pathname:文件路径
mode:想要测试的权限
    F_OK	文件是否存在
    R_OK	测试文件读权限
    W_OK	测试文件写权限
    X_OK	测试文件执行权限
返回值:存在返回0,否则返回-1

#include <sys/stat.h>
int chmod(const char *pathname, mode_t mode);
功能:根据路径修改文件权限
mode:由三位八进制数组成的权限码
		0644	普通文件
		0755	目录文件、可执行文件
int fchmod(int fd, mode_t mode);
功能:根据路径修改文件权限

权限屏蔽码

  • 如果我们不想让新创建的问加你拥有某项权限,则可以设置权限过滤,记录在权限屏蔽码中

  • 权限屏蔽码对chmod命令和函数是无效的

  • 通过命令 umask 查看当前终端的权限屏蔽码

    1. 通过 umask 0xxx 设置当前终端的权限屏蔽码

    注意:通过命令,函数修改权限屏蔽码,只会在当前终端生效,如果关闭会恢复默认值

#include <sys/types.h>
#include <sys/stat.h>
mode_t umask(mode_t mask);
mask:想要设置的权限屏蔽码
返回值:旧的权限屏蔽码

修改文件的大小

#include <sys/types.h>
#include <unistd.h>
int truncate(const char *path, off_t length);
功能:根据文件路径修改文件长度
int ftruncate(int fd, off_t length);
功能:根据文件描述符修改文件长度

练习:实现一个函数,可以删除文件的[N,M)个字节

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int cnt_file(const char *path, size_t n, size_t m){
    int fd = open(path,O_RDWR);
    if( fp < 0 ){
        preeor("fopen");
        return -1;
    }
    int len = lseek(fd,0,SEEK_END);
    if( len < m )  m = len;
    if( n >= m ) return -1;
    len -= m-n;
    char buf[4096] = {};
    lseek(fd,m,SEEK_SET);
    int ret = 0;
    while( ret = read(fd,buf,sizeof(buf) ) ){
        lseek(fd,n,SEEK_SET);
        write(fd,buf,ret);
        n += ret;
        m += ret;
        lseek(fd,m,SEEK_SET);
    }
    ftruncate(fd,len);
    close(fd);
}

文件的删除和重名

#include <stdio.h>
int remove(const char *pathname);
功能:删除文件或空目录
返回值:成功返回0,失败返回-1
int rename(const char *pathname);
功能:重命名,移动文件
返回值:成功返回0,失败返回-1
#include <unistd.h>
int unlink(const char *pathname);	remove底层调用该函数
功能:删除文件或空目录
返回值:成功返回0,失败返回-1

链接文件

  • Linux文件系统会有两个主要的文件分区:

    • iNode信息块:默认128Byte,记录文件权限,大小,所有者,修改时间等
    • block数据块:默认4k,记录了文件名和数据信息
    • 每个文件必须拥有唯一个index以及若干个block块
    • 读写文件需要借助目录的block中记录的文件名和iNode好找到该文件的iNode,再通过iNode读取block
  • 什么是软硬链接文件?

    • 硬链接:硬链接文件没有自己的iNode和block,只是在不同目录下复制了一份源文件的iNode信息,通过iNode信息访问源文件的block
    • 软链接:软链接会建立自己的新的iNode和block,软链接的block存储的是源文件的iNode信息,文件名
    • 区别:
      1. 删除源文件,只是删除了源文件的iNode信息,硬链接不受影响,而软链接无法访问
      2. 对于一个文件而言,硬链接数删除为0时,文件才被真正的删除。
      3. 当修改硬链接文件的内容,源文件也会被修改
      4. 硬链接不能链接目标,软链接可以
      5. 硬链接不能跨文件系统,软链接可以
    #include <unistd.h>
    int link(const char *oldpath, const char *newpath);
    功能:创建硬链接文件
    int symlink(const char *target, const char *linkpath);
    功能:创建软链接文件
    #include <fcntl.h>
    ssize_t readlink(const char *pathname,char *buf, size_t bufsiz);
    功能:读取软链接,链接路径
    

目录操作

int mkdir(const char *pathname,mode_t mode);
功能:创建目录
mode:权限,必须要执行权限
int rmdir(const char *pathname);
功能:删除空目录
char *getcwd(char *buf,size_t size);
功能:获取当前工作目录,相当于pwd
buf:存储结果内容
size:buf的大小
返回值:buf的地址,方便链式调用

int chdir(const char *path);
功能:根据路径字符串修改工作路径
int fchdir(int fd)
功能:根据文件描述符修改工作路径

#include <sys/types.h>
#include <dirent.h>
DIR *opendir(const char *name);
功能:打开目录文件,返回目录流结构体指针
DIR *fdopendir(int fd);
功能:打开目录文件,返回目录流结构体指针
struct dirent *readdir(DIR *dirp);
功能:从目录流中读取一条信息,该条信息记录了目录中一个文件的信息
struct dirent {
	ino_t	d_ino;	//	iNode号
	off_t	d_off;	//	下一条信息的偏移量
	unsigned short d_reclen;	//	当前信息的长度
	unsigned char  d_type;		//	文件类型
	char		   d_name[256];	//	文件名
}

作业:完善 ls -l 的完全功能

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>

作业:实现 rm -rf 的功能

Linux信号处理

  • 基本概念

    1. 中断:
      当操作系统接收到消息后中止当前正在执行的程序,转而执行其它任务,等待它任务执行完成后再返回,这种执行模式叫做中断
      分为硬件中断和软件中断

    2. 信号:
      是一种软件中断,由操作系统发出,程序接收后会执行相应的操作

    3. 常见信号

      命令 kill -l 查看所有信号

    SIGINT(2)	Ctrl+C		终止
    SIGQUIT(3)	Ctrl+\		终止+core
    SIGFPE(8)	除零         终止+core
    SIGKILL(9)	终止信号  	  终止(不能被捕获,忽略)
    SIGSEGV(11) 非法访问内存   终止+core
    
    
    1. 不可靠信号和可靠信号

      建立在早期的信号处理机制上(1~31)的信号是不可靠信号(不支持排队,可能会丢失,如果同一个信号连续产生多次,进程可能只相应了一次)
      建立在新的信号处理机制上(34~64)的信号是可靠信号(支持排队,信号不会丢失)

    2. 信号的来源
      硬件异常:除零,非法访问内存,未定义的指令,总线错误
      软件异常:通过一些命令,函数产生的信号

    3. 信号的处理方式

      1. 忽略
      2. 终止进程
      3. 终止进程并产生core文件(记录内存映像)
      4. 停止
    4. 继续

    5. 捕获并处理(在信号发生前,先向内核注册一个函数。当信号来临时系统会自动执行该函数)

  • 信号捕获

    typedef void (*sighandler_t)(int);
    功能:说明信号处理函数的格式
    sighandler_t signal(int signum, isghandler_t handler);
    功能:向内核注册一个信号处理函数
    signum:信号编号
    handler:函数指针
        SIG_IGN		忽略
        SIG_DFL		按默认方式处理
      
    返回值:之前的信号处理方式
    
    1. 注意:SIGKILL(9),SIGSTOP(19)不能被捕获和忽略
    2. 注意:当信号处理完后可能会返回产生信号的代码继续运行,如果我们不捕获并处理段错误,算术异常等信号可能会产生死循环,正确的处理段错误,算数异常信号应该是备份数据并直接结束程序
    3. 注意:有些系统通过signal注册的信号处理只能执行一次,如果想要持续有效,可以在信号处理函数中再从新注册一次
    4. 注意:子进程会继承父进程的信号处理方式,但是通过exec系类函数创建子进程,会恢复默认

信号的方式方式

  1. 键盘

    Ctrl + c	
    Ctrl + \	
    Ctrl + z	暂停、挂起	fg	继续
    
  2. 错误

    0
    非法访问内存
    总线错误
    
  3. 命令

    kill	-信号编号	进程号
    功能:向指定进程发信号
    killall		-信号编号	进程名
    功能:给同名的进程发送同一个信号
    
  4. 函数

    #include <sys/types.h>
    #include <signed.h>
    int kill(pid_t pid,int sig);
    功能:向指定进程发送指定信号
    int raise(int sig);
    功能:向自己发送信号sig
    void abort(void);
    功能:向进程自己发送信号SIGABRT
    unsigned int alarm(unsigned int seconds);
    功能:让内核在seconds秒后向进程发送SIGALRM信号
    返回值:上一次alarm设置的剩余秒数
    

进程休眠信号

#include <unistd.h>
int pause(void);
功能:让调用者进入休眠状态,直到进程遇到信号才会唤醒
返回值:要么不返回在休眠,要么返回唤醒后-1
unsigned int sleep(unsigned int seconds);
功能:让调用者进入休眠指定的秒数,当遇到信号时会提前唤醒返回
返回值:剩余的休眠秒数

信号集合信号阻塞

  • 信号集:是一种数据类型,定义变量。可以存储多个信号

    sigset_t	128位的二进制数,每一位都固定代表了一种信号
    
    • 信号集的相关函数:

      #include <signal.h>
      int sigemptyset(sigset_t *set);
      功能:清空信号集
      int sigfillset(sigset_t *set);
      功能:填满信号集
      int sigaddset(sigset_t *set, int signum);
      功能:向信号集set中添加信号signum
      int sigdelset(sigset_t *set, int signum);
      功能:从信号集set中删除signum
      int sigismember(const sigset_t *set, int signum);
      功能:测试信号集中是否存在signum信号
      返回值:-1非法,0不存在,1存在
      
  • 信号阻塞:

    • 当程序执行到一些特殊操作时,不适合处理信号,此时可以让内核先屏蔽信号,等操作执行完成后再解除屏蔽重新发送信号
    • 当信号产生时,内核会在其维护的信号表中为对应的进程设置与该信号对应的标记,这个过程就做递送
    • 从信号到完成递送有个时间间隔,处于这个间隔的信号状态称为未决
    • 信号阻塞(屏蔽)就是让被屏蔽的信号先处于未决状态,暂停递送,当屏蔽解除时,继续递送
    #include <signal.h>
    int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
    how:信号屏蔽的方式
        SIG_BLOCK		把set中的信号添加到屏蔽集中
        SIG_UNBLOCK		从信号屏蔽集中删除set集中的信号
        SIG_SETMASK		把set替换之前的信号屏蔽集
    set:准备好的信号集
    oldset:获取旧的屏蔽集,内容
    

附带信息的信号处理

#include <signal.h>
int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact);
功能:向内核注册一个信息处理函数
signum:要捕获的信号编号
act:设置要处理的动作
oldact:湖区原来要处理动作

 struct sigaction {
 	void     (*sa_handler)(int);//不附带其他信息的信号处理函数
 	void     (*sa_sigaction)(int, siginfo_t *, void *);//附带其他信息的信号处理函数
 	sigset_t   sa_mask;//信号屏蔽集
 	int        sa_flags;//信号处理动作标志
		SA_NODEFER	在信号处理过程中不要屏蔽当前信号
        SA_SIGINFO	使用           函数指针
 	void     (*sa_restorer)(void);//用于保留函数
};
siginfo_t {
               int      si_signo;     /* Signal number */
               int      si_errno;     /* An errno value */
               int      si_code;      /* Signal code */
               int      si_trapno;    /* Trap number that caused
                                         hardware-generated signal
                                         (unused on most architectures) */
               pid_t    si_pid;       /* Sending process ID */
               uid_t    si_uid;       /* Real user ID of sending process */
               int      si_status;    /* Exit value or signal */
               clock_t  si_utime;     /* User time consumed */
               clock_t  si_stime;     /* System time consumed */
               sigval_t si_value;     /* Signal value */
               int      si_int;       /* POSIX.1b signal */
               void    *si_ptr;       /* POSIX.1b signal */
               int      si_overrun;   /* Timer overrun count;
                                         POSIX.1b timers */
               int      si_timerid;   /* Timer ID; POSIX.1b timers */
               void    *si_addr;      /* Memory location which caused fault */
               long     si_band;      /* Band event (was int in
                                         glibc 2.3.2 and earlier) */
               int      si_fd;        /* File descriptor */
               short    si_addr_lsb;  /* Least significant bit of address
                                         (since Linux 2.6.32) */
             void    *si_call_addr; /* Address of system call instruction
                                         (since Linux 3.5) */
               int      si_syscall;   /* Number of attempted system call
                                         (since Linux 3.5) */
               unsigned int si_arch;  /* Architecture of attempted system call
                                         (since Linux 3.5) */
           }

#include <sigqueue>
int sigqueue(pid_t pid,int sig,const union sigval value);
功能:向指定进程发送指定信号,并附加信息一起发送
union sigval {
    int		sival_int;		//整数
    void 	*sival_ptr;		//指针
};

定时器

#include <sys/time.h>
int gettitimer(int which,struct itimerval *curr_value);
功能:获取定钱的定时方案
which:选择使用的计时器
    ITIMER_REAL		真实计时器	程序总的计算时间
    ITIMER_VIRTUAL	虚拟计时器	用户态的计算时间
    ITIMER_PROF		实际计时器	用户态+内核态计时器
    真实计时器	=	实际计时器	+	休眠时间	+	切换时间
int setitiner(int which,const struct itimerval *new_value,struct  itimerval *old_value);


struct itimeval {
    struct timeval it_interval;//每次时间信号产生的间隔时间
    struct timeval it_value;//第一次产生时钟信号的时间
}
struct timeval {
    time_t		tv_sec;		//	设置秒
    suseconds_t	tv_usec;	//	设置微秒
}

Linux进程管理

  • 进程的基本概念

    • 进程与程序:程序是存储在磁盘上的二进制可执行文件;程序加载到内存中开始运行时叫做进程;一个程序可以被多次加载生成多个进程,进程就是出于活动状态的计算机程序

    • 进程的分类
      进程一般分为三个种类,交互进程,批处理进程,守护进程

    • 查看进程
      简单模式:ps (当前用户有终端控制进程的简单信息)
      列表模式:ps -auxw(显示所有进程的详细信息)
      a 所有用户的有终端控制的进程
      x 无终端控制的进程
      u 显示进程的详细信息
      w 以更大的列宽显示

      USER	进程的所属用户,用户名
      PID		进程号
      %CPU	CPU的使用率
      %MEM	内存的使用率
      VSZ		虚拟内存使用的字节数
      RSS		物理内存使用的字节数
      TTY		终端设备号	?表示无终端控制
      STAT	进程状态
      		O	就绪态	等待被调用
      		R	运行态,Linux系统下没有O,就绪也用R表示
      		S	可被唤醒的睡眠态,如系统中断,获取资源,收到信号等都可以唤醒进入运行态
      		D	不可被唤醒的睡眠态,只能被系统唤醒
      		T	暂停态	收到SIGTSTP(8)信号进入暂停态,收到SIGCONT(18)信号时,转回运行态
      		X	死亡态
      		Z	僵尸态、僵死态
      		N	低优先级
      		<	高优先级
      		l	多线程进程
      		s	进程的领导者
      		+	位于后台进程组
      		L	内存页被锁定
      START	进程的启动时间
      TIME	进程运行时间
      COMMANO	启动进程的命令
      
  • 父进程,子进程,孤儿进程,僵尸进程

    • 父子进程:

      1. 一个进程可以被另一个进程创建,创建者叫做父进程,被创建者叫子进程。
      2. 子进程被父进程创建后会在操作系统的调度下同时运行
      3. 当子进程先于父进程结束,死前子进程会向父进程发送信号SIGCHLD,此时父进程应该去回收子进程的相关资源
    • 孤儿进程:父进程先于子进程就变成了孤儿进程,孤儿进程会被孤儿院(init守护进程)领养,init就是孤儿进程的父进程

    • 僵尸进程:该进程以死亡,但是它的父进程没有立即回收它的相关资源,该进程就进入了僵尸态

  • 进程表示符
    每一个进程都有一个非负整数表示的唯一标识,即进程ID\PID
    进程ID在任意时刻都是唯一的,但是可以回收,重新使用。进程一旦结束它的进程ID就会被系统回收,过一段时间后再重新分配给新创建的进程

    #include <sys/types.h>
    #include <unistd.h>
    pid_t getpid(void);
    功能:返回调用者的进程ID
    pid_t getppid(void);
    功能:返回父进程的ID
    
  • 创建进程

    #include <unistd.h>
    pid_t fork(void);
    功能:创建子进程
    返回值:一次调用两次返回,子进程返回0,父进程返回子进程的ID;
    		当进程的数量达到系统限制时,会创建失败,返回-1
    #include <sys/types.h>
    pid_t vfork(void);
    功能:以加载可执行文件的方式来创建子进程
    返回值:子进程返回0,父进程返回子进程的ID
    

vfork创建的子进程一定先返回,此时子进程并没有创建成功,需要加载一个可执行文件从而替换当前子进程当前的所有资源,当替换完成后子进程才算创建成功,此时父进程才返回

// 使用exec系列函数让子进程加载

#include <unistd.h>
extern char **environ;
int execl(const char *path, const char *arg, ... /* (char  *) NULL */);
path:可执行文件的路径
arg:命令行参数,个数不定,由实际的可执行文件所需命令
命令行参数:一般第一个可执行文件的名字,至少有一个,一定要以NULL结尾
int execlp(const char *file, const char *arg, .../* (char  *) NULL */);
file:可执行文件名,去系统默认路径查找file
arg:命令行参数,个数不定,由实际的可执行文件所需命令
命令行参数:一般第一个可执行文件的名字,至少有一个,一定要以NULL结尾
int execle(const char *path, const char *arg, .../*, (char *) NULL, char * constenvp[] */);
path:可执行文件的路径
arg:命令行参数,个数不定,由实际的可执行文件所需命令
命令行参数:一般第一个可执行文件的名字,至少有一个,一定要以NULL结尾
envp:环境变量表,父进程可以在加载子进程时把环境变量边传递给子进程
int execv(const char *path, char *const argv[]);
path:可执行文件的路径
argv:命令行参数的字符串数组,末尾最后以NULL结尾
int execvp(const char *file, char *const argv[]);
file:可执行文件名,去系统默认路径查找file
argv:命令行参数的字符串数组,末尾最后以NULL结尾
> 根据:PATH路径加载,file
int execvpe(const char *file, char *const argv .../*, (char *) NULL, char * constenvp[] */);
path:可执行文件的路径
arg:命令行参数,个数不定,由实际的可执行文件所需命令
命令行参数:一般第一个可执行文件的名字,至少有一个,一定要以NULL结尾
envp:环境变量表,父进程可以在加载子进程时把环境变量边传递给子进程
> exec函数正常情况下不会返回,当子进程加载失败才会返回-1.
> 虽然通过vfork,exec系列函数创建的子函数不会继承父进程的信号处理函数,但是能继承父进程的信号屏蔽集
  1. 通过fork创建的子进程会拷贝父进程(数据段,bss段,堆,栈,I/O缓冲区),子进程与父进程共享代码段,子进程会继承父进程的信号处理方式
  2. fork函数调用后父子进程各自独立运行,谁先返回不确定。但是可以通过睡眠确认让哪个进程先执行
  3. 通过fork创建的子进程可以共享父进程的文件描述符
  4. 可以根据返回值的不同让父子进程进入不同的分支,执行不同的代码

练习:为进程创建4个子进程,在为这4个子进程分别创建,2个子进程

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
    printf("我是进程:%u\n",getpid());
    for(int i = 0 ; i < 4 ; i ++ ){
        if( fork() == 0 ){
            printf("我是进程:%u, 我的父进程为%u\n",getpid(),getppid());
            for(int j = 0 ; j < 2 ; j ++ ){
                if( fork() == 0 ){
                    pause();
                }
            }
            pause();
        }
    }
    pause();
}

解体思路:子进程创建后执行万任务后就休眠,这彦不会被其他fork影响

练习:实现出孤儿进程和僵尸进程(ps查看)

进程结束

  • 进程正常退出

    • 在main函数中,执行return语句,进程正常退出

      • 该返回值可以被父进程获取,几乎与exit等价
    • 进程调用了exit(0)函数(exit函数是C标准库函数)

      #include <stdlib.h>
      void exit(int status);
      功能:在任何时间,地点调用了该函数都可以立即结束进程
      status:结束状态码,与main函数中return的返回值效果一致
          EXIT_SUCCESS
          EXIT_FAILURE
      返回值:该函数不会返回,状态码传给操作系统
      int atexit(void (*function)(void));
      功能:向内核注册一个进程结束前必须调用的函数
      int on_exit(void (*function)(int,void*), void *arg);
      功能:向内核注册一个进程结束前必须调用的函数
      arg:调用function时传给它
      

      进程退出前要完成:

      1. 先调用事先通过atexit\on_exit函数注册的函数
      2. 如果两个都注册了,执行顺序与注册顺序相反
      3. 冲刷并关闭所有打开状态的标准IO流
      4. 底层继续调用 _Exit / _exit函数
  • 调用 _Exit或 _exit

    #include <unistd.h>
    void _exit(int status);
    功能:结束进程,由系统提供
    #include <stdlib.h>
    void _Exit(int status);
    功能:结束进程,由标准库提供
    1. 它们的参数会被父进程获取到
    2. 进程结束前会关闭所有处于打开状态的文件描述符
    
  1. 向父进程发送信号SIGCHLD
  2. 该函数也不会返回
  • 进程的最后一个线程执行了return语句
  • 进程的最后一个线程执行了pthread_exit函数

进程的异常终止

  1. 进程调用了about函数,产生SIGABRT(6)信号
  2. 进程接收到某种信号,可以是其它进程发送的,也可以自己的错误导致的
  3. 进程的最后一个线程接收到了“取消”请求操作,并响应

这三种方式结束进程,它的父进程都无法获取结束状态码因此加做异常终止

注意:无论进程是如何结束的,它们最后都会执行同一段代码,会帮你关闭所有打开的文件,并释放所有的内存

子进程的回收

  • 对于子进程的结束而言,都希望父进程都能够知道并做出一定的反应,通过wait和waitpid函数可以知道子进程是如何结束的以及它的结束状态码

    #include <sys/types.h>
    #include <wait.h>
    pid_t wait(int *status);
    功能:等待子进程结束,并获取结束状态码
    status:输出型参数,接收结束状态码
    返回值:结束的子进程的ID
        1.如果所有子进程都还在运行,则阻塞
        2.如果有一个子进程结束,立即返回该进程的结束状态码和ID
        3.如果没有子进程返回-1
        	WIFEIXTEC(status)
        	判断进程是否是正常结束,如果是返回真
        	WEXITSTATUS(status)
        	如果进程是正常结束的,可以获取到正确的结束转态码
        	WIFSIGNALED(status)
        	判断进程是否异常结束,如果是返回真
        	WTERMSIG(status)
        	如果进程是异常结束的,可以获取到杀死进程的信号
    
    pid_t waitpid(pid_t pid,int *status,int options);
    功能:等待回收指定的某个或某些进程
    pid:进程ID
        > 0		等待该进程结束
      	0		等待同组的任意进程结束
        -1		等待任意进程结束
        <-1		等待abs(pid)	进程组中的任意进程结束
    status:输出进程参数,,接收结束状态
    options:
        WNOHANG:非阻塞模式,如果当前没有子进程结束,则立即返回0
        WUNTRACED:如果有子进程处于暂停态,返回该进程的状态码
        WCGNTINUED:如果有子进程从暂停态转为继续运行,返回该子进程的转态
        WIFSTOPPED(status)
            判断子进程是否转为暂停态,是返回真
    	WSTOPSIG(status)
            获取导致子进程进入暂停态的信号
    	WIFCONTINUED(status)
            判断子进程是否由暂停转为继续
    
    int system(const char *command);
    功能:通过创建子进程去执行一个可执行文件
    返回值:子进程结束后才返回
    注意:该函数底层用来fork,vfork,exec,wait函数
    

练习:综合进程管理的知识点,实现一个system

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int _system(const char* command) {
    pid_t pid = vfork();
    if( pid == 0 )
    {
        return execlp(command,command,NULL);
    }
    else
    {
        int status = -1;
        waitpid(pid,&status,0);
        return status;
    }
}
int main(){
    //	调用_system();
}

进程间通信

一,基本概念

? 什么是进程间通信:是指两个或多个进程之间交互数据的过程,因为进程之间是相互独立的,为了进程之间协同工作就必须实现进程间交互数据

二,进程间通信的分类

? 简单的进程间通信:信号,普通文件,环境变量表,命令行参数

? 传统的进程间通信:管道文件

? XSI的进程间通信:共享内存,消息队列,信号量

? 网络进程间通信:Socket套接字

传统的进程间通信技术-管道文件

? 管道是Unix系统中最古老的进程通信技术,古老意味着所有系统都支持。早期的管道是半双工通信,现有的系统管道是全双工通信。

? 管道就是一种特殊的文件,数据在文件中是流动的,读取之后就自动消失,如果文件中没有数据则会阻塞

  • 有名管道:基于有文件名的管道文件的通信
    编程模型
    进程A 进程B

    ? 创建管道
    ? 打开管道文件 打开管道文件
    ? write(写数据) read(读数据)
    ? 关闭管道文件 关闭管道文件
    ? 删除管道

    //	创建有名管道文件
    1.命令mkfifo
    2.函数
    #inlcude <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    int mkfifo(const char *pathname,mode_t mode);
    功能:创建有名管道
    
  • 匿名管道:

    只适合通过fork创建父子进程之间使用

    int pipe(int pipefd[2]);
    功能:创建一个匿名管道文件
    通过pipefd返回该匿名管道文件的读写权限fd
    pipefd[0]	用于读匿名管道
    pipefd[1]	用于写匿名管道
    编程模型:
        父进程				子进程
        获取一对fd			
        创建子进程		拷贝\共享一对fd
        关闭读				关闭写
        写数据				读数据
        关闭写				关闭读
    

XSI进程间通信

? X/open公司指定的用于进程间通信的系统(S)接口(I)

? XSI进程间通信都需要借助系统内核完成,需要创建内核对象,内核对象会以整数的形式返回个用户态,此时类似于文件描述符,也叫做IPC标识符

? 文件的创建需要借助文件名,但是IPC对象没有文件名。IPC内核对象借助IPC键值(整数)。必须要确保IPC键值是独一无二的

#include <sys/ipc.h>
#include <sys/types.h>
key_t ftok(const char *pathname,int proj_id);
功能:计算出一个独一无二的IPC键值
pathname:项目路径
proj_id:项目编号
返回值:计算出IPC对象编号
注意:项目路径必须真实存在,否则计算出来的KEY永远相同

共享内存

  • 基础特点:

    • 两个或多个进程之间共享一块由内核负责统一管理内存,该内存可以与多个进程的虚拟内存进行映射

      #include <shm.h>
      #include <ipc.h>
      int shmget(key_t key,size_t size,int shmflg);
      功能:创建、获取一块共享内存
      key:IPC键值
      size:共享内存的字节数大小,获取共享内存时此参数无意义,一般给0
      shmflg:
          IPC_CREAT	创建共享内存,如果已存在直接获取
          IPC_EXCL	共享内存已存在,返回失败
          获取时直接给0
      > 注意:创建共享内存时,还需要额外提供共享内存的权限(IPC_CREAT|0644);
      返回值:IPC标识符,失败返回-1
      void *shmat(int shmid, const void *shmaddr, int shmflg);
      功能:让虚拟内存与共享内存进行映射
      shmid:IPC标识符,shmget的返回值
      shmaddr:想要映射的虚拟内存的首地址。当它为NULL系统自动分配
      shmflg:
          SHM_RND		只要shmaddr参数不为NULL时,才有效,表示从shmaddr参数开始向下以整数页方式映射
          SHM_RDNOLY	以只读方式映射共享内存
      返回值:与共享内存映射成功后的虚拟内存首地址,失败返回 // (void*)-1  == 0xFFFFFFFF
      int shmdt(const void *shmaddr);
      功能:取消映射
      shmaddr:映射成功后的虚拟内存首地址
          
      int shmctl(int shmid, int cmd, struct shmid_ds *buf);
      功能:删除、控制共享内存
      shmid:IPC标识符
      cmd:选择功能
          IPC_STAT	获取共享内存的属性	buf为输出型参数
          IPC_SET		设置共享内存的属性	buf为输入型参数
          IPC_RMID	删除共享内存			NULL
      buf:
      

      编程模型:

      进程A 进程B

      创建共享内存 获取共享内存

      映射共享内存 映射共享内存

      写数据并通知其它进程 收到通知并读数据

      收到通知并读数据 写数据并通知其它进程

      取消映射 取消映射

      删除共享内存

共享内存的优缺点

  • 优点:不需要复制信息,直接读写内存,是最快的一种IPC机制
  • 缺点:需要考虑同步访问的问题,一般使用信号

消息队列

  • 基本特征:是由内核负责维护管理的链式数据队列,不是根据先后顺序出队,而是根据消息类型进行收发数据
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
功能:创建、获取消息队列
key:IPC键值
msgflg:
    IPC_CREAT	消息队列已存在则获取,否则创建
    IPC_EXCL	消息队列已存在则返回失败
返回值:IPC标识符,失败-1;
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
功能:向消息队列发送消息包
msqid:IPC标识符
msgp:要发送的消息包首地址
    struct msgbuf {
        long mtype;		//	消息类型
        char mtext[n];	//	数据
    }
msgsz:数据的字节数,不包含消息类型的
msgflg:
    阻塞发送一般给0
    IPC_NOWAIT		当消息队列满,不等待立即返回
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
功能:从消息队列中接收对应消息包的数据
msqid:IPC标识符
msgp:存储数据的内存首地址
msgsz:存储数据的内存字节数
msgtyp:消息类型(按照类型获取,不按照顺序)
    >0		读取消息类型=msgtyp的消息
    =0		读取消息队列中第一条消息
	<0		读取消息类型小于abs(msgtyp)消息,如果有多个则读值最小的
msgflg:
    IPC_NOWAIT		如果消息队列都不符合时,不阻塞,立即返回
    MSG_EXCEPT		如果msgtyp>0,则读取第一条不等于msgflg的消息
    MSG_NOERROR		如果不包含此标志,如果实际发送的数据字节数>接收的字节数,则返回失败
    				如果包含此标志,那么就将实际发送的数据中,截取到接收的字节数的数据
返回值:成功读取到的字节数
int msgctl(int msqid,int cmd,struct msqid_ds *buf);
功能:获取、修改消息队列的属性,删除队列
msqid:IPC标识符
cmd:选择功能
    IPC_STAT	获取消息队列的属性	buf为输出型参数
    IPC_SET		设置消息队列的属性	buf为输入型参数
    IPC_RMID	删除消息队列			NULL
buf:

编程模型

进程A 进程B

创建消息队列 获取消息队列

发送消息 获取消息

删除消息队列

信号量

  • 基本特点:由内核管理的一个“全局变量”,用于记录共享资源的数量,限制进程对共享资源的访问使用
  • 信号量是一种数据操作锁,本身不具备数据交互功能,而是通过控制其它的信号资源从而配置实现进程间通信
  1. 如果信号量的值大于0,说明可以使用资源,使用时需要想好量-1,然后再使用
  2. 如果信号量的值等于0,说明没有资源可以使用,此时进程进入休眠,知道信号量的值大于0,进程会被唤醒,执行步骤1
  3. 当资源使用完毕,把信号量的值+1,正在休眠的进程就会被唤醒
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key,int nsems,int semflg);
功能:创建、获取信号量
key:IPC键值
nsems:信号量的数量,一般写1
semflg:
    IPC_CREAT	信号量已存在则获取,否则创建
    IPC_EXCL	信号量已存在则返回失败
返回值:IPC标识符,失败-1

int semctl(int semid,int semnum,int cmd, ...);
功能:删除,控制信号量
semid:IPC标识符
semnum:要操作的第几个信号量,从0开始,下标
cmd
    IPC_STAT	获取信号量的属性      buf为输出型参数
    IPC_SET		设置信号量属性       buf为输入型参数
    IPC_RMID	删除信号量			NULL
    SETVAL		设置某个信号量的值
    SETALL		设置所有信号量的值
    GETVAL		获取某个信号量的值
    GETALL		获取所有信号量的值
    GETNCNT		获取等待拿资源的进程数量

int semop(int semid, struct sembuf *sops, size_t nsops);
功能:对信号量进行加减操作
semid:IPC标识符
struct sembuf {
    unsigned short sem_num;	//	信号量的下表
    short		   sem_op;	//	1,信号量加1;-1,信号量-1;如果不能减,则默认阻塞
    short		   sem_flg;	
    /*
    IPC_NOWAIT		不阻塞
    SEM_UNDO		如果进程终止没有手动还资源,系统会自动还  
    */
}
项目-基于进程间通信技术的银行系统

主要分为两个大模块(C/S)

  • 客户端(Client)
    • 进入一级菜单的功能:开户,销户,登录,解锁
      • 开户:身份证号,设置密码,每个用户在服务器上创建一个账号信息文件。(使用账号做文件名)
      • 销户:输入账号,密码。由服务器确认并询问是否销户,如果确认则服务器删除账号文件
      • 登入:输入账号,密码,三次错误锁定,确认进入二级菜单
      • 解锁:输入账号,身份证号
    • 进入二级菜单的功能:存钱,取钱,转账,查询,改密码
      • 存钱:输入金额
      • 取钱:输入金额
      • 转账:对方账号,转账金额
      • 查询:无输入
      • 改密码:原密码,新密码
  • 服务器(Server)
    • 开启服务器执行各项功能的子进程
    • 识别功能类型,根据消息包的消息类型,来接收每个客户端的请求并响应请求

使用技术:消息队列,进程管理vfork,exec系列函数

账号结构体{

? 账号 自动生成

? 身份证号

? 密码

? 金额 double

}

客户端 to 服务器{

? 消息类型 // long, 9大类型

? 账号结构体

? 客户端PID

}

服务器 to 客户端{

? 消息类型 // 客户端PID

? 提示信息

}

进程间通信-Socket套接字

  • 基本特点:socket是一种接口技术,抽象成一个文件操作,可以让同一台计算机的进程之间通信,也可以让不同计算机的进程通信(网络通信)
  • socket在同一计算机中的进程间通信
    • 底层需要借助socket文件,进行同一计算机下的进程间通信
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>	// struct sockaddr结构体
#include <netinet/in.h>
int socket(int domain,int type,int protocol);
功能:创建socket对象
domain:
	AF_UNIX/AF_LOCAL	本地通信,进程间通信
    AF_INET/AF_INET6	基于IPV4/IPV6地址的通信
type:
	SOCK_STREAM			基于数据流协议的通信,本地一般选。
    SOCK_DGRAM			基于数据包协议的通信
protocol:
	特殊通信协议,一般不用写0即可
返回值:成功返回socket描述符,失败返回-1
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:绑定socket和通讯协议
sockfd:socket描述符
addr:地址结构体指针。sockaddr_un或者sockaddr_in结构体指针,需要把它们统一转换为sockaddr*类型,但是C语言没有自动类型识别转换,需要进行强转
    struct sockaddr_un {
        __kernel_sa_family_t sun_family; /* AF_UNIX */	//地址簇 domain
        char sun_path[UNIX_PATH_MAX];   /* pathname */	//scoket文件地址
    };	//	本地通讯地址结构体类型

    struct sockaddr_in {
        __kernel_sa_family_t  sin_family; /*Address family */	//地址簇 domain
        __be16        sin_port;   /* Port number */				//	端口号
        struct in_addr    sin_addr;   /* Internet address */	//	IP地址
    /* Pad to size of `struct sockaddr'. */
    };
 struct in_addr {
     __be32  s_addr;
 };

addrlen:地址结构体的字节数,用于区分sockaddr_un还是sockaddr_in
返回值:成功0,失败-1

int listen(int sockfd, int backlog);
功能:监听socket,数据流通信时使用
sockfd:socket描述符
backlog:等待链接socket的排队数量,默认最大值为128;
放回值:成功返回0,失败返回-1;

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
功能:等待链接,数据流通信时使用。无连接时阻塞
sockfd:socket描述符
addr:获取连接者的地址
addrlen:即是输入,也是输出。
    即告诉accept函数当前计算机地址结构体的字节数,同时也能获取发送者的地址字节数
返回值:链接成功返回一个链接后的socket描述符

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:链接socket
sockfd:本机socket描述符
addr:链接目标的结构体指针
addrlen:同上
返回值:成功0,失败-1

本地通讯编程模型(数据流)

? 进程A 进程B

创建socket文件对象 创建socket文件对象

准备通讯地址(本地socket地址) 准备对方通讯地址(本地socket地址)

绑定socket和地址 。。。

开启监听 。。。

使用accept等待链接 使用connect链接

接收、发送数据 发送,链接数据

关闭链接好的socket文件 关闭链接socket文件

删除socket

网络编程

  • 底层遵循TCP/IP协议,在系统层上以socket接口方式呈现

  • 基于TCP协议的网络通信模型:

    ? 服务器server 客户端client

    ? 创建socket对象 创建socket对象

    ? 准备通信地址(端口号+IP地址) 准备通信地址

    ? 绑定通信地址

    ? 监听通信网道 / 设置监听和排队数量

    ? 等待客户端client链接 connect链接服务器

    分配新的socket对象+开辟新的进程线程

    ? 接收请求 发送请求

    ? 响应请求 接收响应

    ? 关闭socket 关闭socket

    int socket(int domain,int type,int protocol);
    功能:创建socket对象
    domain:
        AF_INET/AF_INET6	基于IPV4/IPV6地址的通信
    type:
    	SOCK_STREAM			基于数据流协议的通信,本地一般选。
        SOCK_DGRAM			基于数据包协议的通信
    protocol:
    	特殊通信协议,一般不用写0即可
    返回值:成功返回socket描述符,失败返回-1
    //	网络通信,结构体内存大端数据
        struct sockaddr_in {
            __kernel_sa_family_t  sin_family; /*Address family */	//地址簇 domain
            __be16        sin_port;   /* Port number */				//	端口号
            struct in_addr    sin_addr;   /* Internet address */	//	IP地址
        /* Pad to size of `struct sockaddr'. */
        };
    //	大小端数据转换函数
           #include <arpa/inet.h>
    
    	   uint32_t htonl(uint32_t hostlong);
    功能:将4字节的本地字节序转换成网络字节序
           uint16_t htons(uint16_t hostshort);
    功能:将2字节的本地字节序转换成网络字节序
           uint32_t ntohl(uint32_t netlong);
    功能:4字节的网络字节序转本地字节序
           uint16_t ntohs(uint16_t netshort);
    功能:2字节的网络字节序转本地字节序
    
    //	点分十进制,转换函数。IP地址转换函数
           #include <sys/socket.h>
           #include <netinet/in.h>
           #include <arpa/inet.h>
    
           in_addr_t inet_addr(const char *cp);
    功能:将字符串格式的点分十进制,转换成 整数形式的IP地址(大端)
           char *inet_ntoa(struct in_addr in);
    功能:将整数形式的IP地址转换成字符串格式的点分十进制表示的IP地址
    //	监听
    int listen(int sockfd, int backlog);
    功能:监听socket,数据流通信时使用
    sockfd:socket描述符
    backlog:等待链接socket的排队数量,默认最大值为128;
    放回值:成功返回0,失败返回-1;
    //	等客户端连接
    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    功能:等待客户端链接,无连接时阻塞
    sockfd:socket描述符
    addr:获取连接者的地址
    addrlen:即是输入,也是输出。
        即告诉accept函数当前计算机地址结构体的字节数,同时也能获取客户端的地址字节数
    返回值:链接成功返回一个新的连接后的socket描述符,失败返回-1
    
    //	连接服务器
    int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    功能:链接socket
    sockfd:本机socket描述符
    addr:连接服务器的公网IP地址结构体指针
    addrlen:同上
    返回值:成功0,失败-1
    
    //	发送
    ssize_t send(int sockfd,const void *buf, size_t len, int flags);
    功能:TCP协议通信时,专用的数据发送函数
    sockfd:连接成功的socket描述符
    buf:待发送数据的首地址
    len:要发送是字节数
    flags:
    	0		阻塞发送
        1		不阻塞发送
    返回值:
    	-1		出现错误
    	0		连接断开
    
    //	接收
    ssize_t recv(int socket,void *buf, size_t len );
    功能:TCP协议通信时专用的接收数据函数
    sockfd:连接成功的socket描述符
    buf:存储数据缓冲区的首地址
    len:缓冲区的大小
    flags:
    	0		阻塞接收
        1		不阻塞接收
    返回值:
    	-1		出现错误
    	0		连接断开
    

基于UDP通信协议的网络编程模型

? 接收端 发送端

创建socket 创建socket

准备通信地址 准备通信地址

绑定地址

接收请求 发送请求

响应请求 接收响应

关闭socket 关闭socket

//	UDP使用函数
int socket(int domain,int type,int protocol);
功能:创建socket对象
domain:
    AF_INET/AF_INET6	基于IPV4/IPV6地址的通信
type:
    SOCK_DGRAM			基于数据包协议的通信,UDP使用参数
protocol:
	特殊通信协议,一般不用写0即可
返回值:成功返回socket描述符,失败返回-1

//	发送数据
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, 
               const struct sockaddr *dest_addr, socklen_t addrlen);
功能:发送数据
sockfd:socket描述符
buf:待发送缓冲区的数据首地址
len:待发送缓冲区的数据的字节数
flags:是否阻塞,0阻塞
dest_addr:通信目标的地址
addrlen:地址结构体的字节数
返回值:成功发送的字节数
    0		通信关闭
    -1		出现错误

//	接收数据
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);
功能:接收数据
sockfd:socket描述符
buf:接收缓冲区的数据首地址
len:接收缓冲区的数据的字节数
flags:是否阻塞,0阻塞
src_addr:用于存储发送者的首地址
addrlen:地址结构体的字节数
    1. 即告诉函数当前src_addr结构体的字节数
    2. 同时也能实际接收到发送者的地址结构体字节数
返回值:成功发送的字节数
    0		通信关闭
    -1		出现错误

封装TCP\UDP

  1. gcc -fpic -c network.c
  2. gcc -shared -fpic network.o -o libnw.so
  3. sudo cp libnw.so /usr/lib
  4. sudo cp network.h /usr/include

使用:共享库

  1. gcc code.c -lnw
实现网络对战版的五子棋

多路复用

  • 只使用一个进程(且只有主线程)同时监控若干个文件描述符的读写情况,这种读写模式称为多路复用
  • 多用于TCP服务端,用于监控 客户端的连接和发送情况
  • 优点:不需要频繁地创建和销毁进程,从而节约了内存资源,时间资源,也避免了进程之间的竞争,等待
  • 缺点:要求单个客户端的任务不能太过于耗时,否则其它客户端感知到卡顿
  • 适合并发量高,但是任务量短小的情景。例如:web服务器
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout);
功能:同时监控多个文件描述符,写,异常操作
nfds:被监控的文件描述符中的最大值+1
readfds:文件描述符的集合。以下函数可以使用。读操作文件描述符
       void FD_CLR(int fd, fd_set *set);
功能:从集合set中删掉fd文件描述符
       int  FD_ISSET(int fd, fd_set *set);
功能:判断集合set中是否存在fd文件描述符
       void FD_SET(int fd, fd_set *set);
功能:向集合set中添加fd文件描述符
       void FD_ZERO(fd_set *set);
功能:清空set集合
writefds:监控写操作的文件描述符
exceptfds:监控异常操作的文件描述符
timeout:设置超时时间
    NULL	一直阻塞,直到某个文件描述符发生了变化
    00微秒	不阻塞
    大于0秒	等待超时时间,超时返回0
    struct timeval {
               time_t         tv_sec;     /* seconds */
               suseconds_t    tv_usec;    /* microseconds */
    };
返回值:监控到发生相关操作的文件描述符的个数;超时返回0;错误返回-1;
注意:readfds,writefds,exceptfds,这三个集合参数即是输入也是输出,调用select时这三个集合需要存储被监控的文件描述符发生了相应的操作而导致函数返回时,这三个集合中存储了这些文件描述符并返回给调用者

    
int pselect(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, const struct timespec *timeout, const sigset_t *sigmask);
功能:大致与select一致
区别:
	1.超时时间的结构类型不同
	struct timespec {
        long	tv_sec;		//微秒
		long	tv_nsec;	//纳秒
    };
	2.pselect监控时,可以通过sigmask参数设置要屏蔽的信号,可以保障pselect监控时不受这些信号干扰
共同点:本质上区别不大,select的缺点pselect也是一样的,只是个别功能有所区别

select设计不合理的地方

select设计不合理的地方

  1. 每次调用select都需要向它重新传递被监控的文件描述符集合
  2. 调用结束后如果想知道具体是哪个文件描述符发生了相关操作,必须对所有被监控点文件描述符进行一遍测试
  • 优点:它是最早的多路复用函数,几乎所有的操作系统都支持,兼容性很好
  • 缺点:
#include <signal.h>
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fds: struct pollfd 结构变量指针
    struct pollfd {
               int   fd;         /* file descriptor */	表示一个被监控的文件描述符
               short events;     /* requested events */	想要监控的事件
               short revents;    /* returned events */	实际监控到的事件
        POLLIN		普通优先级的读事件
        POLLPRI		高优先级的读事件
		POLLOUT		普通优先级的写事件
        POLLRDHUP	对方socket关闭
        POLLERR		错误事件
        POLLHUP		对方挂起事件
        POLLNVAL	非法描述符
    };
nfds:数组的长度
timeout:超时时间,毫秒赋值
返回值:监控到发生相关操作符的个数,超时返回0,错误返回-1

#include <sys/epoll.h>
int epoll_create(int size);
功能:创建一个epoll的内核对象,该对象可以管理,保存被监控 文件描述符
size:epoll对象管理描述符的数量
返回值:epoll对象描述符

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能:控制epoll对象,添加,删除描述符
epfd:epoll对象描述符
op:功能
    EPOLL_CTL_ADD	添加监控的描述符
    EPOLL_CTL_DEL	删除监控的描述符
    EPOLL_CTL_MOD	修改要监控的描述符事件
fd:要操作的文件描述符
event:
	struct epoll_event {
        uint32_t     events;      /* Epoll events */	要监控的事件
        epoll_data_t data;        /* User data variable */	
    };
		typedef union epoll_data {
               void        *ptr;
               int          fd;		产生事件的描述符
               uint32_t     u32;
               uint64_t     u64;
        } epoll_data_t;
返回值:成功为0,失败为-1

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
功能:监控文件描述符,并返回发生事件的描述符
epfd:epoll对象描述符
events:用于获取发生事件的描述符
maxevents:可以返回的最大事件数目
timeout:超时时间
返回值:监控到发生相关操作的文件描述符的个数;超时返回0;错误返回-1;

epoll优点

  • 只需要拷贝一次待监控的文件描述符
  • 会将发生事件的事件描述符返回,不需要遍历所有的描述符,大大地节约时间
  • 编程结构更简洁

epoll的条件触发和边缘触发

条件触发:当文件缓冲区中有需要读取的数据时,就会触发事件。类似于键盘!

边缘触发:当数据发送一次,就触发一次 ( 把监控时间增加设置为EPOLLET ) 类似于鼠标。

? 优点:大大降低事件触发次数,在某些只需要处理一次事件即可的情况下能够提高效率

零拷贝

[]: https://blog.csdn.net/qq_33036061/article/details/124975485 “零拷贝CSDN”

客户端断点续传过程:

  1. 获取服务器,客户端同名文件大小 server_size

  2. 获取服务器,客户端同名文件的时间戳

  3. 判断客户端同名文件与服务器同名文件的时间戳一致,并且大小不一样,则该文件需要续传

  4. 当3个条件都满足,开始断点续传

    ? 客户端文件从server_size处开始读

    ? 向服务器发送 REST server_size 处开始续传

    ? STOR 开始上传文件

线程管理

  • 基本概念:
    1. 线程是进程的执行线路,它是进程内部的控制序列,或者说线索是进程的一部分(进程是一个资源单位,线程是执行单位,线程是进程的一部分,负责真正的执行)。
    2. 线程是轻量级的,没有自己独立的代码段,数据段,堆,环境变量,命令行参数,文件描述符,信号处理函数,当前信号集等资源。
    3. 线程有自己独立的栈内存,线程ID,错误码,信号掩码。
    4. 一个进程中可以有多个线程(多个执行路线),但是至少有一个线程在活动,该线程称为主线程。
    5. 线程是进程的实体,可以当做系统独立的认为调度和分配的基本单位
    6. 线程有不同的状态和属性,系统提供了线程的控制接口,例如:创建,销毁,控制
    7. 进程中的所有线程同在一个虚拟地址空间中,进程中的所有资源对线程而言都是共享的。因此当多个线程协同工作时,需要解决资源竞争问题(加锁)
    8. 线程的系统开销小,任务切换快;多个线程之间不需要数据交换,因此不需要类似于XSI的通讯机制;因此使用线程简单而高效
    9. 线程之间有优先级的差异
ps -T -p <pid>	:	查看pid进程中线程情况  或者htop命令也可以查看
  • POSIX线程

    1. 早期的UNIX和Linux系统没有线程概念,微软的Windows系统首先使用线程,之后UNIX和Linux系统也逐渐增加了线程

    2. 早期各个厂商有自己都私有的线程库,而且接口的实现差异较大,不利于移植。世界标准化组织于1995年,指定了统一的线程接口标准规范——遵循该标准的线程称为POSIX线程,简称 pthread

    3. pshread线程包含一个头文件 <pthread.h> 和一个共享库 libpthread.so

      -l pthread

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine) (void *), void *arg);
功能:创建新线程
thread:获取线程ID
attr:用于设置线程属性,一般设置写NULL即可
start_routine:线程的入口函数。类似于主线程的main函数。
arg: 为start_routine线程入口函数的参数
返回值:成功返回0,失败返回错误码。
> 入口函数的参数,返回值都要确保它的可持续性。因此,不太适合使用栈内存,适合使用堆内存和全局变量。
int pthread_join(pthread_t thread, void **retval);
功能:等待线程结束,并获取该线程结束时的入口函数和返回值,并释放线程资源
thread:要等待的线程ID
retval:用于存储线程结束时,返回值的地址
返回值:成功返回0,失败返回错误编码

pthread_t pthread_self(void);
功能:获取进程的ID号

int pthread_equal(pthread_t t1,pthread_t t2);
功能:比较两个线程ID是否一致
返回值:一致返回非零值,不一致返回0
> 在个别操作系统下,pthread_t是以结构体实现的,大部分是以 unsigned long 呈现,为了可移植性,不能直接使用 
> " == " 进行比较;		ptherad_t tid; 建议不要初始化

线程的执行轨迹

  • 同步方式:默认(可结合状态)
    1. 创建子线程后,主线程通过pthread_join函数等待子线程终止,并释放线程资源
  • 异步方式:(分离状态)
    1. 无需创建者等待(如果创建者调用pthread_join函数会立即返回),线程结束时由系统释放资源

原因:避免资源泄漏,每个可结合状态的线程必须显示地调用pthread_join来回收资源;或者,将其变成分离状态

int pthread_detach(pthread_t thread);
功能:将已创建的thread标识线程与创建者线程分离
ptherad_datach的两种用法:
    1.主线程中调用pthread_detach(tid);
	2.新线程中调用pthread_detach(pthread_self() )
> 注意:无论哪一种分离写法,都必须保证pthread_join之前,否则join一旦进入等待,分离也不在会退出等待

线程的结束

  1. 线程执行到入口函数的最后一行代码(包括return语句)
  2. 线程执行了pthread_exit函数
void pthread_exit(void *retval);
功能:结束当前线程
retval:等同于return后面的val

注意:从表面上看当主线程结束后,子线程会跟着一起结束,就误以为主线程的结束影响了子线程的结束,但是实际上子线程之所有结束是因为主线程执行main函数中隐藏的return语句,导致整个进程结束,所有进程中的线程才会随之结束。

如果主线程调用pthread_exit自杀,这样就没有线程去执行main函数的return语句,进程就不会提前结束,子线程就不受影响

总结:主线程结束不会影响子线程的执行

  1. 如果所在的进程结束,所有的线程都随之结束
  2. 向指定线程发送取消请求
int pthread_cancel(pthread_t thread);
功能:向指定线程发送取消请求,默认情况下会响应请求
int  int pthread_setcancelstate(int state, int *oldstate);
功能:设置本线程是否响应取消,并获取之前的状态
state:
    PTHREAD_CANCEL_ENABLE
    PTHREAD_CANCEL_DISABLE
oldstate:之前的状态

int pthread_setcanceltype(int type, int *oldtype);
功能:设置延时响应
type:
	PTHREAD_CANCEL_DEFERRED			延时响应
	PTHREAD_CANCEL_ASYNCHRONOUS		立即响应

线程进程

  • 基本概念

    • 原子操作:中途不会被打断的操作称为原子操作(不会被其它线程竞争影响的操作)
    • 竞争与同步:同一个进程中的线程共享中绝大多数资源,当它们随意竞争时可能导致资源被破坏,脏数据,不完整,不一致的情况
      通过一些方法让线程在竞争资源使相互协调,避免出现以上情况,这种线程间协同工作成为线程同步
    • 临界区和临界资源:被多个线程同时访问的代码称为临界区,被同时访问的资源称为临界资源
  • 互斥量(互斥锁)

    查手册:	man pthread_mutex
    #include <pthread.h>
    pthread_mutex_init
    pthread_mutex_lock
    pthread_mutex_trylock
    pthread_mutex_unlock
    pthread_mutex_destroy
    
        pthread_mutex_t	是一种数据类型 可以定义互斥量
        
    宏: pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;	//初始化宏,系统默认初始化
        
    int pthread_mutex_init(pthread_mutex_t *restrict mutex,
                  const pthread_mutexattr_t *restrict attr);
    功能:初始化一个互斥量
    mutex:要初始化的互斥量变量
    attr:对互斥量的属性进行设置,一般给NULL即可
    注意:一般默认为解锁的状态
    返回值:成功返回0,失败返回-1
    int pthread_mutex_lock(pthread_mutex_t *mutex);
    功能:对互斥量进行加锁,成功则继续执行后续代码,失败则阻塞,直到互斥量被解锁并加锁成功,才返回
    int pthread_mutex_trylock(pthread_mutex_t *mutex);
    功能:对互斥量进行尝试加锁,成功或失败都立即返回
    返回值:成功返回0,失败返回EBUSY
    int pthread_mutex_unlock(pthread_mutex_t *mutex);
    功能:对互斥量解锁
    int pthread_mutex_destroy(pthread_mutex_t *mutex);
    功能:销毁互斥量
    

信号量

  • 与XSI中的原理相同,相当于线程之间使用同一个计数器,用于统计,控制,访问有限的共享资源的线程数量
man sem_
#include <semaphore.h>
int sem_init(sem_t sem,int pshared,unsigned int value);
功能:初始化信号量
sem:被初始化的信号量
pshared:
		0		只能在本进程内使用
        非0		表示该信号量可以以共享内存的形式,让多个进程共享使用(Linux4.0以前不支持)
value:信号量的初始值

int sem_wait(sem_t *sem);
功能:对信号量-1,如果信号量为0,不够减,则阻塞,减成功则继续执行
int sem_trywait(sem_t *sem);
功能:对信号量尝试-1,成功0,失败EAGAIM都立即返回
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
功能:对信号量-1,如果不够减则等待abs_timeout时间,如果超时返回ETIMEDOUT错误编码

int sem_post(sem_t *sem);
功能:对信号量+1
int sem_destory(sem_t *sem);
功能:销毁信号量

死锁

  • 多个进程或线程之间相互等待对方手中的资源,在得到新的资源之间不会主动释放自己手中的资源,这样如果形成了等待环路,称之为死锁
  • 产生死锁的四大必要条件
    • 资源互斥:资源只有两种状态,只有可用和不可用状态,不能同时使用,同一时刻内只能被进程或线程使用
    • 占用且请求:对已经得到资源的进程或线程,对旧资源保持占有,并继续请求新的资源。
    • 资源不可剥夺:资源已经分配给进程或线程后,不能被其它进程或线程强制性获取,除非资源的占有者主动释放
    • 环路等待:当死锁发生时,系统中必定有两个或两个以上的进程或线程执行路线形成环路等待

注意:以上四个条件同时成立,就会形成死锁,死锁一旦产生,基本无解,以现在的操作系统是无法解决死锁,因此只能防止死锁的产生。

  • 如何防止死锁的产生
    • 破坏资源互斥:想办法让资源能够共享使用
      缺点:实现环境和资金的影响无法让资源共享
    • 破坏占有且请求:采用预分配的方式,让进程或线程在运行前一次性申请所有资源,如果在资源没有满足时不投入运行
      缺点:系统资源的占用会严重浪费,因为有些资源可能开始使用,但是有些资源可能会最后才使用
    • 破坏资源不可剥夺:当一个进程或线程已经占有一个不可被剥夺的资源时,且在请求新资源无法被满足时,则释放已经占用的资源,等待一段时间重新申请请求
      缺点:该策略实现较麻烦,而且释放已经申请的资源可能会导致前一阶段的工作无效,反复地申请释放资源也会增加系统开销,占用CPU和寄存器,内存等资源
    • 破坏环路等待:给每个资源起编号,进程或线程按照编号依次请求资源,并且只有拿到前一个资源,才能继续请求下一个资源并且
  • 如何判断死锁
    • 画出资源分配图
    • 简化资源分配图
    • 使用死锁的判定原理进行判定,如果没有环路一定不会出现死锁

了解:银行家算法

  • 条件变量
    • 当某些条件满足时,可以让线程进入随眠,也可以当某些条件满足时唤醒正在随眠的线程
手册:man pthread_cond_  + Tab键
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;	//	初始化宏

int pthread_cond_destroy(pthread_cond_t *cond);
功能:销毁
int pthread_cond_init(pthread_cond_t *restrict cond,
           const pthread_condattr_t *restrict attr);
功能:初始化条件变量
cond:要初始化的条件变量
attr:默认给NULL即可

int pthread_cond_wait(pthread_cond_t *restrict cond,
           pthread_mutex_t *restrict mutex);
功能:让当前线程睡cond,并解锁mutex
返回值:直到线程被唤醒才返回

int pthread_cond_signal(pthread_cond_t *cond);
功能:唤醒cond中正在随眠的一个线程,在唤醒前要确保锁处于打开状态,当线程醒来是该线程都会自动把锁重新锁上
int pthread_cond_broadcast(pthread_cond_t *cond);
功能:唤醒cond中所有线程,线程是否醒来取决于能否在次加锁

int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
功能:让当前线程睡入cond,并解锁mutex,只睡abstime时间超时会被操作系统唤醒

注意:使用条件变量可以实现生产者和消费者模型

生产者与消费者模型

生产者:产生数据的线程

消费者:使用数据的线程

仓库:临时存放数据的空间

(数据池和线程池-----利用生产者和消费者模型)

  • 可能产生的问题

    • 消费快于生产:仓库空虚,饿死
    • 生产快于消费:仓库饱满,撑死
  • 一般会使用条件变量来解决以上的问题:

    • 当仓库空的时候,让消费者线程睡眠入条件变量(empty),并通知所有生产者线程全部醒来
    • 当仓库满的时候,让生产者线程睡眠入条件变量(full),并通知所有消费者全部醒来消费

进程与线程相关的问题

  1. 进程与线程的区别?
  2. 进程在处理多任务时,需要解决什么问题?
  3. 线程在处理多任务时,需要解决什么问题?
  4. TCP服务端的编程模型有哪几种,以及它们的优缺点?
  5. 随着客户端的连接和退出越来越频繁,服务端都需要创建,销毁线程,该过程会比较耗时,如何决绝?

Windows编程准备工作:

  1. 解压编译器mingw到C盘
  2. 复制: C:\mingw\bin 路径
  3. 右点击此电脑→属性→高级系统设置→高级→环境变量→Path→编辑→新建→粘贴路径→确定
  4. Windows+R→输入cmd→输入gcc -v 测试编译器是否添加成功
  5. 下载VSCode安装三个插件

VSCode配置:
1、设置->扩展->Run Code…->Run in Termial 等三项打钩
2、Exectuor Map->编辑->修改 ->保存
“c”: “cd $dir && gcc $fileName -std=gnu99 -lws2_32 -lpthread -o $fileNameWithoutExt && d i r dir dirfileNameWithoutExt”,
“cpp”: “cd $dir && g++ $fileName -o $fileNameWithoutExt && d i r dir dirfileNameWithoutExt”,

Windows与Linux的socket网络编程区别

  1. 额外加库 -lws2_32
  2. 头文件不同 <windsock2.h>
  3. 先初始化网络库 固定的
  4. 使用closesocket关闭socket描述符

C++简介

本贾尼·斯特劳特芦普,于1979年4月贝尔实验室负责分析UNIX系统的内核的流量情况,希望有一款更加模块化的工具,于1979年10月开始着手开发一种新的编程语言,在C语言的基础上增加了面向对象机制,这就是C++的来历,在1983年完成C++的第一个版本 C++ java

  • C/C++的区别
    • C++是完全兼容C的所有内容
    • C++支持面向对象的编程思想和机制
    • C++支持运算符重载,函数重载等编译时多态机制
    • C++支持泛型编程,模板机制
    • C++支持异常处理
    • C++的类型检查更严格

注意:学习C++的重点是学习面向对象这种编程思想。

#include <iostream>
using namespace std;
int main(int argc,const char* argv[]){
    cout << "Hello Word!" << endl;
    return 0;
}
  1. 文件的扩展名由“.c” 变为 .cpp .cc .C .cxx
  2. 编译器由gcc变成g++,但gcc也可以继续编译C++文件,需要增加参数 -xC++ -lstdc++
  3. C++的标准库头文件不带.h iostream 意味着 in && out
    stream,在C++中输入,输出被封装流操作,C语言中头文件还可以继续使用,但是建议在C标准头文件名前加c并去掉.h,因为这样的头文件中删除了大量不使用的宏,并重新添加到名字空间中,防止在C++中命名冲突
  4. 输入输出:
    cout 用于输出
    cin 用于输入
    不需要使用占位符,会自动识别数据占位符
    cout/cin 是C++标准库中类对象
    endl 负责换行
  5. 增加了名字空间机制,是C++中为了解决命名冲突而发明的一项机制

C++与C的不同点

  • 结构的不同

    1. C++中不在需要typedef关键字进行使用struct,在设计好结构后,定义结构变量时不再需要union关键字
    2. C++中允许结构体中的成员是函数
    3. C++中的成员函数 可以直接访问 其它成员变量和成员函数
    4. C++中的结构中,有一些隐藏的成员函数(构造,析构,拷贝构造,赋值函数)
    5. 可以给结构成员设置访问属性:public 公开(默认) protected 保护的 private 私有的
    6. 结构是可以继承其它结构,也可以被其它结构继承
  • 联合的不同

    1. C++中不在需要typedef关键字进行使用union,在设计好结构后,定义结构变量时不再需要union关键字
    2. C++中允许union中的成员是函数
    3. C++中的成员函数 可以直接访问 其它成员变量和成员函数
    4. C++中的结构中,有一些隐藏的成员函数(构造,析构,拷贝构造,赋值函数)
    5. 可以给结构成员设置访问属性:public 公开(默认) protected 保护的 private 私有的
  • 枚举的不同

    1. 不在需要typedef关键字进行省略enum,在设计好枚举后,定义枚举变量时不在需要enum关键字
    2. 是一种独立的数据类型,
  • bool类型的不同

    1. C++中有真正的bool类型,bool是C++中的关键字,不需要包含stdbool.h头文件
    2. true,false 也是C++中的关键字,但在C语言中都不是
    3. true,fasle 在C++中是1字节,而在C中都是4字节
  • 字符串的不同

    1. C++中的字符串封装成了string类,但还可以使用C中的字符串,而且能与string相互转化

    2. string类型被封装在string头文件,但已经被包含到iostream字符串成员函数,还可以使用运算符的方式操作string类

      = 			strcpy
      +=			strcat
      ==			strcmp
      .size()		strlen
      .c_str()	由string装换为char*
      
    3. C++中没有规定string必须以’\0’为结束表示,编译器可能在末尾加,也可能不加,由编译器决定,因为它的长度信息封装到了类的私有成员中

  • void*的不同,王能指针的不同

    • 在C语言中,void*可以与任何类型的指针进行相互转换
    • 在C++中,void*不可以自动转换成其他类型的指针,如果需要赋值给其他类型的指针时,只能强制类型装换后才能赋值。为了提高指针类型访问内存更安全。但是其它类型的指针依然可以自动转换成 void*类型的指针,C++之所以保留该功能,是因为C语言标准库,操作系统接口函数采用了大量的void*作为函数参数,如果不保留该功能,C++在调用这些函数时会非常麻烦(mmap,bzero,memcpy,memcmp···)
  • 名字空间

    1. 为什么需要名字空间
      C++完全兼容C语言,C++标准库中自带了大量的类名,函数名,变量名,所以全局的标识符大量增加,容易出现命名冲突增加
    2. 什么是名字空间
      C++中增加的一种对命名空间进行逻辑划分单元的技术,称为名字空间,是一种解决命名冲突的机制
    namespace 名字空间的称呼 {
        变量名;
        函数名;
        结构,联合,枚举;;
    }
    定义出一个封闭的名字空间的作用域
    
    1. 如何使用名字空间

      • 直接全部导入
        using namespace 名字空间的称呼
      • 使用域限定符
        名字空间的称呼 : : 标识符名
    2. 名字空间合并
      名字空间可以多次定义,不同位置的同名名字空间在编译时会被编译器自动合并

    3. 名字空间的申明和定义可以分开

      namespace Scope{
          extern int num;	//	声明
      }
      int Scope::num = 100; //定义
      
    4. 匿名名字空间
      所有的全局表示符都属于同一个名字空间,它没有名字,称为匿名名字空间,如果没有指定名字空间,其实默认使用匿名名字空间
      匿名名字空间中的成员可以使用 ::标识符 来访问
      所以:同名局部变量屏蔽全局变量时,可以调用 ::变量名来指定房网匿名空间中全区变量

    5. 名字空间中可以嵌套使用

    6. 给名字空间取别名

      namespace n123 = n1::n2::n3;
      

C++的堆内存管理

  1. C++中有专门管理堆内存的语句(new 分配堆内存 | delete 释放堆内存),而C语言只能使用标准库中的函数malloc / free
  2. new在分配内存时,可以直接初始化
    常用格式:类型* p = new 类型;(未初始化)
    常用格式:类型* p = new 类型( 初始化的值 );(初始化)
  3. delete p; 释放内存
  4. new \ delete 不能与 malloc \ free 混合使用
#include <iostream>
#include <cstdlib>
using namespace std;
int main() {
    int* p = new int(1234);
    cout << *p << endl;
    free( p ); //	语法支持,但不建议使用
//	但使用new分配内存时会自动调用结构,联合,类等类型的构造函数,而使用delete会自动调用这些析构函数,
//  但是malloc、free并不会调用构造,析构函数
}
  1. 数组分配内存和释放

    int* arr = new int[/*数量*/];	//	自动调用构造函数
    delete[/*数量*/] arr;			//	自动调用析构函数
    

    注意:new\delete malloc\free new[]\delete[] 必须不能混用

    使用new【】为结构体,联合,类申请到内存前4个字节记录了申请的次数,这样的目的是为了让编译器在delete【】中知道应该调用多少次析构函数

  2. 重复释放
    delete和free一样,都可以释放空指针,但都不能重复释放相同内存

  3. 内存分配失败
    malloc分配失败则返回NULL
    new分配失败则会抛出一个异常
    std: :bad_array_new_length

  4. new和malloc返回值不同
    malloc成功返回void*
    new成功返回有类型的指针

重点:malloc、free 和 new、delete的区别

  • 身份: 函数 运算符、关键字
  • 返回值: void* 带类型的指针
  • 参数: 字节个数(手动计算) 类型 ,自动计算
  • 处理数量: 手动计算数组总字节数 new 类型【数量】
  • 扩容: realloc重新申请 不好直接处理
  • 失败: 返回NULL 抛异常并结束
  • 构造、解析: 不调用 自动调用
  • 初始化: 不能初始化 new 类型(value)
  • 头文件: stdlib.h 直接使用
  • 重载: 不允许重载 允许重载
  • 分配内存: 堆内存 堆内存 、 自由存储区

注意:自由存储区只是一个抽象概念,由于new底层默认调用malloc,此时分配的是堆内存,但是new可以当做运算符被程序员重载的方式 或者 new(地址)类型 内存时,可能会被分配到其它任意的区域

笔试题:现有一块已经分配好的内存(栈,堆),如何让新申请结构变量、类对象使用这块指定的内存

函数重载

  1. 什么是函数重载
    在同一个作用域下,函数名相同,参数列表不同的函数构成重载关系
    • 参数列表:参数个数 参数的类型
    • 函数重载与返回值类型,参数无关
  2. C++是如何实现函数重载?
    通过g++ -S filename.cpp 生成汇编代码得知,编译器会把函数的参数类型缩写后追加到函数名后面,也就是说编译时会给函数进行换名
  3. extern “C”
    因为C++编译器中再编译函数调用语句时,会找换名后的函数调用,这样就无法调用到已经使用C编译器成功的函数了
    使用 extern “C” 会让C++编译器按照C编译器的格式来翻译函数名,这样函数的声明与定义就匹配,就可以正确地调用C标准库,系统库函数
  4. 重载和隐藏
    在同一作用域下同名不同参的函数构造成重载关系
    在不同作用域(父子类)下的同名函数遵循名字隐藏原则
  5. 参数类型转换
    当调用函数时,编译请器会优先调用类型最精确的类型,如果没有则会做一定程度的类型提升或者标准装换,而不是全部直接报错。但具有优先级与编译器有关,因此优先选择最精确的参数即可,否则容易产生函数调用的二义性

默认形参

  1. 什么是默认形参
    C++中可以给函数的参数设置默认值,当函数调用者提供了实参则使用实参,如果没有提供则使用默认值
  2. 默认形参要靠右
    如果有函数多个默认形参,必须遵循从右往左依次设置,否则有二义性
  3. 只能在函数声明处世子默认形参
    如果函数声明与定义分开实现,只能在话术声明时设置默认形参,否则语法错误
  4. 默认形参可能会影响函数重载的效果
    如果为重载过的函数设置默认形参是一定要小心

内联函数

  1. 什么是内联函数

    • 普通函数会翻译成二进制指令存储在代码段中,调用语句会生成一句跳转指令,并让程序跳转到该函数所在代码段出运行,运行结束在返回
    • 内联函数也会被翻译成二进制指令,调用语句不会生成跳转指令,而是字节把函数的二进制指令替换成调用语句,这样既没有跳转也没有返回,而是直接往下执行被调用函数,这种函数成为内联函数
    • 显示内联和隐式内联
      显示内联:在函数的返回值前加 inline ,则该函数就以内联机制调用,但并不是所有编译器都支持内联机制,我们现在的 g++ gcc 都不支持
      隐式内联:结构,联合,类中的成员函数会自动被当做内联函数处理
  2. 内联函数的使用条件
    内联的优点:节约了函数的跳转和返回的时间。提高了代码的运行速度。
    内联的缺点:多次调用内联函数时,它的二进制代码会被拷贝多份,产生冗余,导致最终的可执行文件增大
    使用的条件:

    1. 适合一次调用多次执行的情况,不适合内容多且调用少的函数,因此这样节约的时间还弥补不了牺牲的内存 2. 带有递归属性的函数无法内联,编译器会自动屏蔽inline关键字

    内联函数(inline)与宏函数(#define)的相同点和不同点

引用

  • 什么是引用:引用是一种取别名的机制
    为什么要使用指针:

    1. 跨函数共享变量(输出型参数)时,可以使用 引用 替代
    2. 提高函数传参效率,引用可以替代,且效率更高。因为引用不占字节
    3. 配合字符串时,使用指针。string可以替代
    4. 配合堆内存使用,继续使用指针
  • 什么时候时候使用引用:

    1. 跨函数共享变量(输出型参数)时,引用比指针安全(不存在空引用,极少出现野引用);引用比指针更方便(不同取地址,解引用)
    2. 提高传参效率,应用的传参效率比指针还高,因为指针还需要4、8个字节用于存储内存地址,而引用一个字节都不需要,但是引用与指针一样有被修改的风险,因此为了安全需要加const保护一下

    指针与引用的相同点和不同点

    相同点:都可以跨函数共享变量,都可以提供函数传参效率,也都需要const保护

    不同点:

    引用是一种取别名的的机制,而指针是一种数据类型。

    引用不需要额外的存储空间,而指针需要4、8字节存储内存地址编号。

    引用不同更换指向目标,而指针可以指向更改。引用必须初始化,而指针可以不初始化。

    有空指针,但没有空引用。

    指针可以配合堆内存使用,引用不行。

    可以定义指针数组,但是不同定义引用数组(可以定义数组指针,可以定义数组引用;可以定义函数指针,也可以定义函数引用)

  • 使用引用时需要注意的问题:

    • 引用必须初始化,不存在空的引用
    • 可以引用右值,但必须使用const修饰
    • 引用的过程中不能修改目标
    • 返回引用类型的数据时,不要返回局部变量的引用

强制类型转换

C语言中强制类型转换还能在C++中继续使用,因此C++中新的强制类型转换有点鸡肋

为什么C++要重新设计新的强制类型转换?

  • C语言中的强制类型转换太危险

为什么C++的强制类型转换设计的很复杂,使用很麻烦?

  • 因为C++之父认为只有在程序设计不合理情况下,才需要进行强制类型转换,之所以设计复杂就是不想让程序员使用,从而反思,重新设计自己的代码
static_cast<目标类型名>(原数据):静态类型强制转换
dynamic_cast<目标类型名>(原数据):动态类型强制转换
目标类型与原数据类型之间必须存在继承关系
const_cast<目标类型名>(原数据):去常类型转换
目标类型和原类型必须是指针或者引用,并且除了是否带const属性之外,其它类型都必须相同,否则出错
reinterpret_cast<目标类型名>(原数据):重解释类型转换
只能是把整数数据转换成指针,或者把指针转换成整数,否则出错
//	要求目标类型数据与原数据之间必须有一个方向可以自动类型转换

面向对象

  • 面向过程:

    • 关注的是如何解决问题,以及解决问题的步骤
  • 面向对象:

    • 抽象:先找出(想象出)能把你解决问题的"对象",分析该对象解决问题所需要的属性(成员变量)和行为(成员函数)

    • 封装:把抽象的结果封装成类(结构),并给类的成员变量,成员控制函数设置相应的访问权限

    • 继承:

      1. 在封装类前先考虑现有的类是否有能解决一部分问题,如果有则把现有的类继承过来,在此基础上进行扩展,以此基础上进行扩展,以此来节约解决问题的时间
      2. 把一个复杂的问题分析拆解成若干个小问题,每个问题设计一个类去解决,最后把这些类通过继承合并到一个能解决最终问题的类中
    • 多态:

      • 发出一个指令,系统会根据实际情况执行不同相应的操作,这种特征称之为多态(统一命令多种形态)例如:重载过的函数,当调用函数时,编译器会根据参数的类型调用对应的重载版本,这就是一种多态;而且具体调用哪个版本如果在编译时就能确定下来,这种重载称为编译时多态

      注意:面向对象的行为细节,本质上依然是面向过程,因此面向对象只是从更高的维度去思考问题,而不是寻求解决问题的捷径

类和对象

什么是类和对象?

  • 类是由程序员自己设计的一种数据类型,它里面包含了成员变量和成员函数两部分;而对象是类的实例化,其实可以理解为使用类创建的变量

  • 类是设计和实例化:

    class	类型
    {
        成员变量;	//	类中成员默认属性是private私有
        public:
        返回值 函数名(参数列表);
    };	//	分号不能少
    /*
    	实例化:
    	方法一:	类名	类对象名;
    	方法二:	类名*	p = new 类型
    */
    返回值 类名::函数名(参数列表)
    {
        //	成员函数内可以直接使用成员变量	不用. →
    }
    

    注意:如果类的内容不多,可以考虑在头文件中全部实现出来

访问控制限定符

private:私有的,被它修饰的成员只能在类内访问,是类的默认属性

public:被它修饰的成员可以在任意位置访问,一般把类的成员函数设置为公开的

protected:保护的,被它修饰的成员可以在类内核子类中访问,但是不能再类外访问

构造函数

构造函数是类的同名函数,没有返回值,当实例化对象时,它会自动执行;一般负责对类进行初始化,分配资源

class 类名
{
    int* p;
public:
    类名(参数)
    {
        p = new int;
    }
}
  1. 构造函数必须是public否则无法实例化对象

  2. 构造函数可以重载,可以有多个版本

  3. 带参数的构造函数的调用方法:
    类名 对象名(实例);
    类名 对象指针 = new 类名(实参);

  4. 默认情况下,编译器会自动生成一个无参构造函数,该函数什么都不做;一旦显示地实现了构造函数,则编译器不生成构造函数

    类名 对象名; // 调用无参构造,如果没有无参构造,则报错

  5. 也可以使用默认形参实现,构造函数的效果

  6. 构造函数没有返回值

  7. 不要使用malloc为类实例化对象分配内存,因为malloc不会调用构造函数

析构函数

析构函数负责对类对象进行收尾工作,例如:释放类内存中资源,保存数据等,当对象实例化;

class 类名{
    public:
    ~类名(){
        
    }
}
  1. 析构函数权限必须是public
  2. 无返回值,无参数,不能重载
  3. 当类对象生命周期完结时 或者 使用delete销毁对象时 会自动调用析构函数
  4. 如果没有显示的实现析构函数,编译器也会自动生成一个什么都不敢的析构函数
  5. 构造函数一定会执行,但是析构函数不一定会执行
  6. 不要使用free来销毁对象,因为它不会使用析构函数

初始化列表

初始哈列表是构造函数的一种特殊语法,只能在构造函数中使用

class  类名
{
    成员1
	成员2
	...
    成员n
public:
	类名(参数):成员名1(初始化数据),成员名2(初始化数据). . . 
	{
			// 构造函数
	}
}
  • 类的成员变量在旧版本的标准中不可以设置初始值,而且在构造函数执行之前成员变量都已经定义完毕,因此const属性的成员变量无法在构造函数内正常赋值初始化(新标准中可以直接设置初始值,但是也只能给常量,功能有限)
  • 初始化列表 先于 构造函数执行,初始化列表执行时类对象还没有构造完成,因此它能给const属性成员变量赋值的方法
  • 当参数名与成员名相同时,初始化列表可以自动分辨成员和参数名
  • 当类中有类对象成员时,该成员的有参构造函数也可以在初始化列表中执行

作业:

  1. 使用C++语法写通讯录
  2. 使用C++语法实现五子棋
  3. 使用C++语法实现封装一个network
  1. struct,class关键字可以省略
  2. bool类型,可以不同加stdbool.h的头文件。因为,它在C++中是关键字
  3. 如果自行定义了一个构造函数,则不再自动生成无参构造
  4. 构造,联合,类:都有 构造函数,解析函数,拷贝函数,赋值函数
  5. class中的成员默认为private属性,struct中默认public属性
  6. 构造函数没有返回值,且可以重载
  7. 析构函数没有返回值,没有参数,不能重载
  8. 只能在构造函数中进行初始化列表。写在定义处
  9. 函数还可以设置默认形参,声明可以写,定义不可以写
  10. 函数可以重载
  11. 通过域限定符(::)可以指定使用重名的成员函数或则是全局函数
  12. 通过域限定符(::)可以访问被屏蔽的全局变量
  13. 类中的成员函数可以直接使用成员变量
  14. 任意类型的指针可以自动转成void* ,void* ,不能自动转换成其它类型指针,必须使用强制类型转换

对象的创建和销毁过程分析

  1. 对象创建
    • 给对象划分内存空间(栈,堆)
    • 执行初始化列表
      • 根据继承表顺序来调用父类的无参构造或有参构造
        通过:父类类名(val)调用父类有参构造
      • 根据成员的定义顺序调用类类型成员的无参构造或者有参构造
        通过:成员名(val)调用类类型成员有参构造
        初始化普通成员
      • 执行构造函数,申请资源
  2. 对象销毁
    • 执行自己的析构函数,释放内存
    • 根据成员定义的顺序,逆序调用类类型成员的析构函数
    • 根据继承表顺序,逆序调用父类的析构函数
    • 释放对象的内存

成员函数是如何区别调用它的对象

  1. 对象的内存只存储了它的成员变量,没有存储成员函数指针
  2. 当通过对象调用成员函数时,编译器会自动把对象的地址传递给成员函数,也就是说成员函数中有一个隐藏的参数,叫做this指针参数,来接收对象的地址
  3. this指针拿到了调用对象的地址,就可以访问该对象的成员,完成了区别对象的工作
  4. 虽然this指针是隐藏的,但是可以在成员函数内显示地使用它,但是正常情况下不写等于写了

常函数

  1. 被const修饰过的成员函数,称为常函数
    返回值 成员函数名(参数列表)const{}
  2. 已知当对象调用成员函数时,编译器会把对象的地址隐式地传递给成员函数的this指针
  3. 如果对象被const修饰过,就不能使用普通成员函数,编译会报错,因为此时传递的对象指针带有const属性,而普通的成员函数隐藏的this指针不带const属性,所以把带const属性的指针赋值不带const属性的指针变量,编译器不允许
  4. 如果成员函数被const修饰,本质上修饰的是隐藏的this指针,这样this就带const属性,也就可以被带const属性的对象调用
  5. 带const属性的对象只能调用常函数,常函数也只能调用常函数
  6. 常函数可以重载成不带const函数但其它形参完全相同的成员函数
  7. 在常函数中不能修饰成员变量的值,除非该成员变量定义时被 mutable 修饰
  • C语言中的const和C++中的const有什么不同?

    • C和C++的const都是用来保护数据的
    1. C++中的const会优化变量的取值过程,哪怕该变量的内存被内存强行修改,也不会改变const变量的值,这样的机制为了安全性考虑
    2. C++中的const还可以修饰成员函数(隐藏的this指针),定义常函数
  • 一个空的结构体对象在C语言和C++中各自占多少字节?为什么?

    1. 在C++中结构可以定义成员函数,且默认隐藏的四个成员函数(构造,析构,拷贝,赋值),当创建结构对象时会调用成员函数,会把对象的地址自动传递给成员函数,这种机制就要求结构体对象无论如何都要在内存中拥有一席之地,因此当结构中没哟成员时,编译器也会让结构最少拥有不使用的1字节

拷贝构造和赋值操作

拷贝构造是一种特殊的构造函数,格式为

类名(const 类名& that){} /* const不是必须加的 */

  1. 什么时候会调用拷贝构造:当使用旧对象给新对象进行初始化时,会自动调用拷贝构造。
  2. 拷贝构造的任务:
    顾名思义拷贝构造负责把旧对象中的成员变量拷贝给新对象,且编译器会默认生成具备这样功能的一个隐藏拷贝函数
  3. 什么时候应该显示地实现拷贝构造:
    普通情况下编译器自动生成的拷贝构造完全够用,但是当类中的成员有指针且为指针分配了堆内存,默认的拷贝只会对指针的值进行拷贝,这样就会导致两个对象的指针成员都指向同一块堆内存,在执行析构函数时,造成重复释放堆内存导致内存崩溃,此时必须要显示的实现拷贝构造

浅拷贝和深拷贝:

  • 当类中有成员是指针类型且分配了堆内存,浅拷贝只会拷贝指针变量的值,深拷贝不拷贝指针变量的值,而是申请新的内存,拷贝内存中的数据内容

赋值操作函数:

所谓的赋值操作,就是一个对象给另一个对象赋值(这两个对象都已经创建完毕),在C++中会把运算符当做函数处理,使用运算符相当于调用运算符函数

Test t1,t2;
t1 = t2;	//	调用赋值操作
/*赋值操作函数,深赋值*/ 
void operator=(const 类名& that)	// operation后的=左右不能有空格!
{
    cout << "我是赋值操作函数" << endl;
    delete[] name;
    int len = strlen(that.name);
    name = new char[len+1];
}

赋值运算符函数的任务:

  • 它与拷贝构造的任务基本一致,默认下编译器也会自动生成具备浅拷贝功能的赋值操作函数,但是当需要进行拷贝时不仅需要显示实现拷贝构造,同时也需要显示的实现赋值运算符函数
  • 实现赋值运算符函数需要注意的问题:
    • 赋值运算符函数与拷贝构造函数任务虽然接近,但是实现过程有所不同:
      1. 问题:两个对象的指针都已经分配好内存
        1. 先释放内赋值者的指针变量所指向的原内存
        2. 再给赋值者的指针变量重新申请内存
        3. 把赋值者指针变量所指向内存的内存拷贝给赋值者新申请的内存中
      2. 问题:有可能对象自己给自己赋值
        1. 需要立即判断this和赋值者的地址是否相同
        2. 如果相同立即结果结束,不相同时进行赋值操作
      3. 问题:允许 n1 = n2 = n3 连续赋值操作
        1. 因此赋值运算符函数的返回值返回类型 应为对象的引用

作业:实现一个string类,实现四个成员函数

class String {
    char* str;
public:
    String(const char* str = NULL);
    ~String(void);
    String(const String& that);
    String& operator=(const String& that);
}

静态成员

  • 什么是静态成员:
    被static修饰过的成员变量和成员函数都被称为静态成员

    • 普通成员:每个类对象都有各自的一份成员变量,相互之间没有关联
    • 普通成员函数:成员函数都隐藏了一个this指针
    • 静态成员的特点:
      • 静态成员变量:
        1. 存储在bss或data内存段中,一个类的静态成员变量只有一份,能被多有类对象共享
        2. 静态成员变量的生命周期不再依赖于任何对象,为程序的整个生命周期都存在
        3. 既然静态成员变量不依赖任何对象,可以通过类名::静态变量名 的方式不经过对象字节访问郭凯的静态成员变量
        4. 必须在类内使用static声明,类外单独定义,初始化,在定义时不需要加static
          类型名 类名 : : 静态变量名 = val ;
      • 静态成员函数:
        1. 没有隐藏的this指针了,所以静态成员函数无法直接访问普通的成员变量和普通成员函数,但是可以直接访问静态成员函数和静态成员变量
        2. 虽然不能直接访问普通成员变量和普通成员函数,但是静态成员函数的作用域依然算作类内,因此只有让静态成员函数获取得到类内对象的指针或引用,则依然有权限访问private,protected的普通成员变量,成员函数(相当于自己传了一个this指针函数)
        3. 调用方式与静态成员变量类似,也不需要依赖对象也可以访问
          对象名.静态函数名();
          对象名→静态成员函数名();
          类名 : : 静态成员变量名 = val;

    静态成员的作用:

    1. 静态成员相当于多了一层类作用域的全局变量,全局函数
    2. 静态成员变量适合存储所有类对象公共属性(例如:利率,税率),这样可以节约内存资源
    3. 静态成员函数,可以当做访问静态成员变量的接口,这样一方面不会破坏静态成员变量的封装性,另一方面可以不需要创建对象即可调用静态成员函数,让类本身具有管理自己成员的能力
  • 单例模式

什么是单例模式:只能实例化出一个类对象
什么情况下会使用单例模式:

  1. 任务管理器、日志管理器
  2. 网站访问计数器
  3. 线程池,内存池,数据池
  4. 服务器的连接管理器

实现单例模式的原理:

  1. 禁止在类外创建类对象,把 构造函数拷贝函数 私有化

  2. 确保类对象只有一份,在类中定义一个静态成员指针变量或类对象

  3. 提供一个获取静态类对象,指针的接口,设计静态成员函数用于获取静态类对象,指针

    • 饿汉模式的单例:
      • 程序运行时就实例化出类对象,不管后期是否用到都会创建出来
      • 优点:不可能被多个线程同时运行时,创建多份(确保线程安全)
      • 缺点:如果后期使用不到单例对象对象,浪费了资源
    • 懒汉模式的单例:
      • 直到真正使用时,才创建单例对象
      • 优点:什么时候用什么时候创建,如果用不到就不会创建,节约资源
      • 缺点:有可能多个线程同时创建多个单例对象(线程不安全)

实现:饿汉模式的单例,和线程安全的懒汉模式的单例

酒店管理系统项目:

旅客类:身份证号码,姓名,性别。构造函数,show函数。setName,setSex,setId(可选)

房间类:房间号,房型(入住人数),房价,已入住数,旅客类的二级指针。
构造函数,入住,退房,查询show。

酒店类:楼高层数,每层房间数,房间对象(三级指针)。 (单例模式)

构造函数,入住,退房,查看空房,查询房间,升级。

管理员类(选做)

酒店构造函数:读酒店信息文件进行配置构造

层数

第一层房间数

运算符函数

在C++中会把运算符当做函数处理,一个表达式,其实是调用了很多的运算符函数完成计算,这种特性对于内建类型是没有意义的,但是对于自建类型的数据,可以进行个性化设计(例如:string类),可以大大的提高代码的可读性,易用性。

运算符函数的格式:

  • 单目运算符:【#是运算符 o是类对象】
    成员函数:

    []O::operator#(void) { }
    //	全局运算符函数不属于任何类,因此需要把调用者作为参数传递过来
    //	运算符成员函数,全局函数,只能同时实现一个,因为编译器会产生歧义
    
  • 双目运算符:

    a # b
    //成员函数:
    []类型A::operator#(类型& b) {}
    //	注意:运算符左边一定是发起者,类A一定是发起者
    //	全局函数
    []operator#(A& a , B& b) {}
    

    思考:如何实现一个坐标类
    Point P(1,1)

    • 运算类的双目运算符 T

      //	成员函数:a + b
      const  T  operator+(const  T&b)const
      {
          return T(x + b.x);
      }
      //	全局函数
      const  T  operator+(const  T&b)const
      {
          return T(a.x+b.x);	//	a是私有的,无法直接类外访问
      }
      

友元

在实现类的全局运算符函数时,可能会使用类内的私有成员,此时全局运算符函数是没有访问权限,如果把私有成员变成public会打破封装性,或者实现get函数会很麻烦,C++提供了友元这种方式来为全局的运算符函数提供独家授权,该函数称为友元函数。
方法:在类内对全局运算符函数进行声明,并且在声明,并且在声明前加friend关键字即可。

输入输出运算符

在C++中 << >> 运算符不光是按位左移,按位右移,同时它们还是cout ,cin 的输入输出,运算符

iostream库中有很多个cout << 运算符重载。

ostream& operator<<(ostream& os,const Test& t) { return os << t.x; }

注意:使用了私有成员,需要声明全局变量输出为友元
istream& opreator>>(istream& is,const Test& t) { return is >> t.x; }

  • 由于输入输出运算符,是可以连续调用的,因此返回值,应该还是cin或cout,所以返回值为istream或ostream
  • 由于输入,输出运算符的调用是左边的cin,cout,我们无法实现它们的成员运算符函数,只能实现全局的输入输出运算符函数
  • 如何在全局运算符函数中使用了私有成员,需要声明全局运算符函数为友元函数

运算类的单目运算符

单目运算符:++、–、!、~、*、&、sizeof、-

!	~	-
const 类名 operator-(void)const
{
    return Test(-x,-y);
}
//	注意:这些单目类运算符对象都可能带const属性,因此重载的单目运算符函数必须是常函数,并且运算过程中都不会改变自身的值,而是产生一个临时的运算结果,并且是右值,只能返回带const属性的对象
//	全局变量
const 类名 operator-(const 类名& t)
{
    return Test(-t.x,-t.y);
}

自变运算符函数

前自变运算符:++a,--a
const 类型名 operator++()
//	例如
type& operator++(void) {
    x++;y++;
    return *this;
}
//	全局
type& operator++(type& t) {
    t.x++;t.y++;
    return t;
}
后自变运算符:a++,a--
哑元:在参数类别中增加一个不会使用的哑元类型的参数,唯一目的就是为了区分前自变,还是后自变(哑元唯一用处)
//	成员
const type operator++(int) {
    return type(x++,y++);//返回值必须是临时对象,不能引用
}
//	全局成员
const type operator++(type& t,int) {
    return type(t.x++,t.y++);
}
  • 注意点:
    • 在C语言中,无论是前,后自变得到的结果都是左值,但是在C++中,前自变的结果是左值,后自变的结果是右值

特殊的运算符

  1. 下标运算符[]
    当想让一个类对象当成数组使用时,可以考虑重载下标运算符,例如vector类中就重载了【】
  2. 函数运算符()
    当想让一个类对象当成函数使用时,可以考虑重载该运算符。

注意下标运算符合函数运算符,不能重载成全局

  1. 解引用*运算符 和 访问运算符→
    重载这两个运算符时,可以让一个类对象像指针一样使用
    C++的智能指针就是重载了它们来实现的

  2. new和delete
    为什么要重载new和delete运算符?

    • 可以在重载该运算符函数时记录每次分配,释放内存的地址,时间,行数,等信息到日志中,可以检查哪里出现了内存泄漏,什么时候出现了

    • 成员函数与全局函数的格式一样

      void* operator new(size_t size)	// size是要申请内存的字节数,编译器会自动计算并传递过来
      {
          //	一般加入写入日志操作
          void* ptr = malloc(size);
          return ptr;
      }
      void operator delete(void* ptr)
      {
          //	一般加入写入日志操作
          free(ptr);
      }
      
    • 注意:如果只是针对某一个类重载它的new\delete,那么只需要实现成员函数即可,如果想要所有类型进行new\delete时,那就重载成全局函数

重载运算符规则

  • 有些运算符是不能重载:域限定符(:😃,(.)直接访问成员的运算符,(?:)三目运算符,sizeof计算字节数的运算符,typeid获取类型函数的运算符
  • 只能重载成全局函数的运算符
    << 输出运算符
    >> 输入运算符
  • 只能重载成成员函数的运算符
    [] 下表运算符
    () 函数运算符
    = 赋值操作运算符,类内本身就一定有一个=成员函数
    → 间接访问成员的运算符
  • 运算符重载可以自定义运算的过程,但是无法改变运算符的优先级
  • 运算符的运算对象数量和格式不能改变
  • 不能发明新的运算符

建议:

  1. 重载运算符要遵循运算符含义一致性,不要改变运算符的运算规则
  2. 不要忘记重载运算符的初衷,不要炫技

继承

  1. 当遇到问题时,先查看现有的类能否解决部分问题,如果有,则继承该类,在此类的基础上进行扩展来解决问题,以此可以缩短解决问题的时间(代码复用)

  2. 当遇到一个大而复杂的问题时拆分成若干个小问题,然后为每个小问题设计一个类进行解决,最后通过继承的方式把这些类汇总到一个类中从而解决最终的大问题的,以此降低问题的难度,也可以同时让多个程序员一起工作解决问题

    ? 子类继承父类 派生类继承基类

  • 继承语法

    class Son : 继承表[继承方式 父类名1,继承方式 父类名2,···]
    {
        //	成员变量;
    public:
        //	成员函数;
    };
    
  • 继承方式

    public

    protected

    private

  • C++的继承特点:

    1. C++中的继承可以有多个父类

    2. 子类会继承父类的所有内容

    3. 子类对象可以向它的父类类型转换(使用父类类型的指针或引用指向子类对象),但是父类对象不能向它的子类类型转换

      Base* b = new Son;			true
      Base& b = son;				true
      Son* son = new Base;		false
      Son& son = base;			false
      
    4. 子类会隐藏父类的同名成员,在子类中直接使用同名成员时,默认使用的是自己的同名成员。但是可以通过 父类名::同名成员 的方式指定访问父类的同名成员

    5. 子类与父类的同名函数是无法构成重载,因为:不在同一个作用域下,并且子类会隐藏同名的父类成员函数,因此只能通过域限定符的方式访问父类的同名成员函数

    6. 在执行子类的构造函数时,会按照继承表中的顺序执行父类的构造函数,默认情况下,执行父类的无参构造。可以在子类的构造函数的初始化类别中显示地调用父类的有参构造。最后才执行子类的构造函数

      Son(int num):Base(参数){}	//	调用Base的有参构造
      
    7. 在子类的析构函数执行结束过,才会调用父类的析构函数。如果有多个父类,会按照继承表的逆序,来析构父类

    8. 当子类执行拷贝构造时,默认也会调用父类的无参构造,但这是有问题的,因此需要在子类的拷贝构造函数的初始化列表中显示的调用父类的拷贝构造函数

    9. 当子类执行赋值操作时,默认不会调用父类的赋值操作函数。如果需要调用父类的赋值操作时,可以通过域限定符和赋值操作函数名的方式调用

      void Son::operator=(Son& that)
      {
          父类::operator=(that);	//	调用父类的赋值函数
      }
      
  • 继承方式和访问控制属性

    • 访问控制属性成员的访问范围:

      public:		可以在任意位置访问
      protected:	只能在类内和子类中访问
      private:	只能在类内使用
      
    • 继承方式的影响:

      1. 父类的成员是否能在子类中访问:是在设计父类时的访问控制属性决定的,不受继承方式的影响
      2. 但是继承方式能够决定父类成员被子类继承后,在子类中变成什么样的访问控制权限属性
      3. 只有使用public继承父类,父类的指针或引用才能指向子类对象(多态的基类)
  • 多重继承和钻石继承

    1. 多重继承
      当一个类继承了多个父类时,称为 多重继承,会按照继承表的顺序在子类中排列父类的内容。当把子类指针转换成父类指针对象时,编译器会自动计算出该父类内容所在的位置,并让指针偏移到该位置,因此,可能会出现转换后的父类指针与转换前子类指针不相同的情况。

    2. 钻石继承

      假如有类A,类B和类C都继承了类A,类D又同时继承了类B和类C,当子类的父类中有共同的祖先时,这种称为钻石继承

      1. 类B和类C中都各自有类A的内容
      2. 类D会继承类B和类C的所有内容,就会导致类D 中有两份类A的内容
      3. 当类D对象去访问类A中的成员时,编译器会产生歧义,不确定要使用的哪份类A中的成员,因此编译不通过
  • 虚继承

    • 当使用virtual关键字修饰继承表时,此时变成虚继承,此时子类中就会多一个虚指针用于指向父类的内容,当这个子类被继承时,孙子类也会继承该虚指针,并且通过虚指针比较是否含有多份相同的祖先类,如果有只保留一份
    • 因此通过虚继承可以在钻石继承中解决子类有多份祖先类成员的问题

虚函数和覆盖

虚函数:当成员函数前加virtual修饰后,这样的函数称为虚函数,该类也会想虚函数一样多路一个虚指针。

虚函数表:虚指针指向一张表格的首地址,该表格中记录的是所有虚函数的首地址

覆盖:当使用了virtual关键字修饰父类的成员函数,此时父类中多了一个虚指针(虚表指针),子类会把父类的虚指针一起继承过来,当子类中有与父类虚函数同名的成员函数时,编译器会比较这两个函数的格式,如果格式完全相同,则会把子类的同名函数地址覆盖掉虚函数表中父类的同名虚函数的原地址
此时,使用父类的指针或引用指向子类对象时,调用虚函数则会调用被覆盖后的虚函数表中所指向的子类的同名且格式相同的成员函数

((void(*)(void))(*(int*)*(int*)b))();//相当于调用了虚函数表中第一个函数
( ( void(*)(void) ) (**(int**)b) )();//	调用虚函数表中的第一个函数

构成覆盖的条件:

  • 必须是发生咋父子类,且一定为public继承
  • 要被覆盖的父类函数必须为虚函数(virtual修饰)
  • 子类中必须有同名且格式(返回值,参数列表,常属性)完全一致的虚函数,才能构成覆盖
  • 覆盖要求,返回值类型相同;或者,子类函数的返回值可以向父类虚函数的返回值类型做隐式转换且有继承关系时,可以构成覆盖

面试题:重载,覆盖,隐藏,重写的区别
隐藏:

  1. 如果同名但格式不同,无论是否加virtual都是隐藏,在子类中都会隐藏父类同名成员函数
  2. 子类中如果同名且格式相同,不加virtual,子类也会隐藏父类的同名成员函数

总结:同名成员函数,要么构成隐藏,要么构成覆盖

多态

什么是多态:是指同一个事物,指令可以有多种形态,当调用同一个指令时,根据参数,环境的不同会做出不同的响应操作,这种模式称为多态!

根据确定执行操作的时间,多态分为:编译时多态,运行时多态。

编译时多态:

  • 当调用重载过的函数时,编译器会根据参数的不同,在编译时就能确定哪个版本的重载函数,这种叫做编译时多态,还有模板技术

运行时多态:

  • 在父子类中,当子类覆盖了父类的同名的虚函数,然后用父类指针或引用访问虚函数,它即可能调用父类的虚函数,也可能调用子类的同名函数,具体调用哪个取决于该父类指针或引用指向目标是谁,而这个目标的确定需要运行时才能最终决定下来,这种情况叫做运行时多态
  • 构成运行时多态的条件
    1. 父子类之间且有构造覆盖关系的同名函数
    2. 子类是以public继承父类(让父类指针,引用指向子类对象)
    3. 通过父类指针或引用 访问被覆盖的虚函

虚构造和虚析构

虚构造:构造函数不能设置为虚构造,假如构造函数可以是虚函数。那么,子类构造函数会自动覆盖父类的构造函数,当创建类对象时,子类执行自己的构造函数之前,会自动调用父类的构造函数,但是此时父类的构造函数已经被覆盖成了子类的构造函数,就会在此调用子类的构造函数,这样就形成了死循环,因此编译器不允许把构造声明为虚函数

虚析构:析构函数可以设置为虚函数,当使用类多态时,通过父类指针或引用释放子类对象时,默认情况下不加虚析构是不会调用子类的析构函数,如果子类析构函数中有要释放子类对象时,会先调用覆盖后子类析构函数,而且之后,还会调用父类的析构函数,这样就不会由内存泄漏了

总结:当使用多态且子类的构造函数中有申请内存,此时父类的析构函数一定要设置为虚析构

纯虚函数和纯抽象类

纯虚函数的格式:

virtual 返回值 成员函数名(参数列表) = 0;
  • 纯虚函数可以只声明不实现(一般也没必要去实现它)
  • 父类中如果没有纯虚函数,子类就必须覆盖父类的纯虚函数,否则无法创建对象
  • 有纯虚函数的类是无法创建对象的,因为这样的话,纯虚函数没有被覆盖
  • 纯虚函数的目的就是为了强制子类去覆盖父类的纯虚函数,强制子类实现某些功能
  • 有纯虚函数的类称为抽象类
  • 析构函数可以被定义为纯虚函数,但是需要在类外实现它

纯抽象类:所有成员都是纯虚函数的类,叫做纯抽象类。这种类一般用于设置功能的接口,也被称为接口类

工厂模式

C++文件操作

  • I/O流
    C++中把对文件的读写操作都封装在以下类中:头文件
    ofstream:对文件的写操作,继承ostream类的功能
    ifstream:对文件的读操作,继承istream类的功能
    fstream:对文件的读写操作,继承了ofstream类和ifstream类的功能

  • C++中对文本文件的读写操作

    • 创建流对象,通过流对象进行打开文件

      //	创建流对象并用有参构造打开文件
      ofstream fout(const char *filename,<openmode>);
      //	无参构造创建流对象,再通过成员函数打开文件
      void open( const char *filename);
      void open( const char *filename, openmode mode);
      mode://文件的打开方式
      	ios::app		//	添加输出
          ios::ate		//	当已打开是寻找到EOF
          ios::binary		//	以二进制模式打开文件
          ios::in			//	为读取打开文件
          ios::out		//	为写入打开文件
          ios::trunc		//	覆盖已存在的文件
      

      ifstream类:默认只读方式打开文件,文件不存在则失败。(相当于"r")

      ofstream类:默认只写方式打开文件,文件不存在则创建,存在则清空打开。(相当于”w“)

      fstream类:默认读写方式打开文件,文件不存在则失败。(相当于“r+”)

    • 判断文件是否打开成功

      /*
      使用 !类对象名 的方式进行判断
      使用good,fail成员函数进行判断上一次文件操作是否成功或失败,需要注意返回值
      */
      
    • 读写文件

      流对象 << 文本数据		写操作
      流对象 >> 文本数据		读操作
      
    • 关闭文件

      close();//关闭流对象的当前文件,但是流对象依然可以打开新的文件
      

练习:设计一个学生类,并创建学生对象,然后把它的内容以文本形式写入stu.txt中。

从stu.txt中读取出来,并用该内容实例化一个新的学生对象并显示

注意:在读写内建类型数据时,原有的<< >>运算符就可以直接进行文本读写,但是在类对象进行读写操作时,绝大多数的类成员变量是private,因此无法直接在类外进行读写

? 由于ostream/istream是ofstream/ifstream的父类,因此如果在它们里面重载了<< >>运算符,那么可以直接通过cout/cin输出输入类对象外,还可以用于直接文本读写类对象

  • C++的随机读写

    /*
    C++为文件IO流提供了两套设置位置指针的成员函数,目的是为了兼容有两个位置指针的操作系统,但是UNIX、Linux和Windows系统底层只有一个位置指针,所以使用哪套都可以
    */
    istream &seekg(off_type offset,ios::seekdir origin);
    功能:通过偏移值+基础位置。设置 输入流位置指针的位置
    offset:偏移值
    origin:基础位置
        iOS::beg	文件开头
        ios::cur	当前位置
        iOS::end	文件末尾
    istream &seekg(pos_type position);
    功能:通过 绝对位置 设置输入流位置指针的位置
    ostream &seekp(off_type offset,ios::seekdir origin);
    ostream &seekg(pos_type position);
    
    pos_type tellg();
    功能:获取输入流位置指针的位置
    pos_type tellp();
    功能:获取输入流位置指针的位置
    bool eof();
    功能:判断文件是否读到文件末尾
    
  • C++的二进制读写

    • 创建流对象,打开文件

      1. 使用有参构造创建并打开文件
      2. 使用无参构造创建对象,通过open成员函数打开文件
        mode参数增加 ios::binary(Windows下一定要加)
    • 二进制读写操作

      ostream &write(const char *buffer,streamsize num);
      功能:以二进制方式写入数据
      buffer:待写入的字节数
      num:要写入的字节
      注意:C++中文件I/O流的wirte只有两种结果,要么num个字节全部写入成功,要么失败一字节也不写入,只需要使用good、fail判断是否写入成功
      ostream &read(const *buffer,streamsize num);
      功能:以二进制方式读文件
      buffer:存储内存数据的首地址
      num:要读取的字节数
          
      streamsize gcount();
      功能:获取上一次读文件操作,读到的字节数
      bool eof();
      功能:判断上一次操作,是否是文件末尾
      //	注意:在以二进制方式读写对象时,对象的成员不应该有指针(以及string)类型,因为在读写时,只会写入指针成员变量(地址编号),而下一次读取该指针变量时的内存了,因此读到的该编号没有意义了
      
      while(true)
      {
          //	读操作 read
          if( file.eof() ) break;
      }
      
    • 关闭文件

练习:使用C++实现mv命令(实现带覆盖提示)

mv dest src

移动文件:

  • 源文件是普通文件
    • 目标不是当前路径 创建打开目标文件
      • 目标只是个路径 新建同名文件,移动删除
      • 目标是路径+文件名 存在则判断是否覆盖,不存在则新建并移动
      • 目标是当前路径 重命名 rename

移动目录:

  • 源文件是目录文件 通过Linux C 打开目录流

Linux中,实现的小工具:

  1. ls -al
  2. cp
  3. mv
  4. ping 命令
  5. rm -rf
  • 类型信息识别(头文件:)
    C++中使用typeid运算符可以获取数据的类型
    1. 返回值是一个类 类型对象
    2. 该类型中有一个name的成员函数,可以获取数据的类型并转换为字符串
    3. 该类重载了 == 运算符 可以判断两个数据是否是同一个类型
    4. 它可以区别父类指针或引用指向那个子类,但是前提是父子类之间构成多态

模板技术

模板:是一种自动生成代码的技术,这种技术能让程序员在编写代码时不需要考虑数据类型,这种技术也被称为泛型编程技术

为什么需要使用模板?

  1. C/C++都是一种静态编程语言(编辑→预处理→编译→汇编→链接→可执行文件)。静态编程语言的优点就是运行速度比较快;缺点:实现代码通用比较麻烦;

任务:实现一个通用的选择排序、快速排序。

#include <stdio.h>
#include <string.h>
int cmp(const void* p1,const void* p2) {
    int n1 = *(int*)p1,n2 = *(int*)p2;
    return n1 - n2;
}
int str_cmp(const void* p1,const void* p2) {
    char* s1 = *(char**)p1, *s2 = *(char**)p2;
    return strcmp(s1,s2);
}
void select_sort(void* buf,size_t num,size_t size,int (*cmp)(const void*,const void*))
{
    for(int i = 0 ;  i < num - 1 ; i ++ ) {
        void* min = buf+i*size;
        for(int j = i + 1 ; j < num ; j ++ ) {
            if( cmp(min,buf+j*size) > 0 ) {
                min = buf+j*size;
            }
        }
        if( min != buf+i*size ) {
            char temp[size];
            memcpy(temp,min,size);
            memcpy(min,buf+(i*size),size);
            memcpy(buf+(i*size),temp,size);
        }
    }
}

void _q_sort(void* buf,int left,int right,size_t size,int (*cmp)(const void*,const void*))
{
    if( left >= right ) return;
    int l = left,r = right,p = left;
    char pv[size];
    memcpy(pv,buf+(p*size),size);
    while( l < r ) {
        while( r > p && cmp(pv,buf+r*size) <= 0 ) r--;
        if( r > p ) {
            memcpy(buf+p*size,buf+r*size,size);
            p = r;
        }
        while( l < p && cmp(pv,buf+l*size) >= 0 ) l++;
        if( l < p ) {
            memcpy(buf+p*size,buf+l*size,size);
            p = l;
        }
    }
    memcpy(buf+p*size,pv,size);
    _q_sort(buf,left,p-1,size,cmp);
    _q_sort(buf,p+1,right,size,cmp);
}
void qsort(void* buf,size_t num,size_t size,int (*cmp)(const void*,const void*))
{
    _q_sort(buf,0,num-1,size,cmp);
}
int main() {
    
}
  1. 可以使用void*+回调函数的方式实现通用代码,但是实现难度高,使用比较麻烦
  2. 借助宏函数实现部分通用代码,类型检查不严格,没有返回值,容易有二义性
  3. C++借助函数重载也可以实现通用代码,增加代码段的内容,未知类型需要重载新版本
  4. 综合以上因素,C++之父在C++中实现了模板技术,目的是为了让C++的编程彻底摆脱数据类型的困扰

函数模板

  • 函数模板的格式

    template<typename T1,typename T2...>
    T3 函数名(T1 arg1,T2 arg2) {
        T3 temp = arg1 + arg2;
        return temp;
    }
    //	注意:此两行要紧靠着。可以给未知类型取任何名字,一般约定俗称写T
    //	一个template模板声明只能修饰一个紧跟着的函数
    
  • 函数模板的原理

    • 函数模板会经历两次编译
      1. 检查函数模板的语法是否有错误,但不会生成函数的二进制指令

      2. 根据调用者提供的参数类型来在次检查代码,如果没有错误才会生成二进制指令并存放到代码段,如果没有一次调用则不会生成二进制指令
        这种方式称为函数模板的惰性实例化准则

  • 函数模板的调用

    //	可以使用默认类型参数
    template<typename T1,typename T2,typename T3 = long>
    T3 func(T1 arg1,T2 arg2) {
        T3 ret = arg1 + arg2;
        return ret;
    }
    int main() {
        cout << func<int,double,long>(10,3.14) << endl;
    }
    /*
    自动:编译器会根据实参的类型,自动获取类型替换模板类型
    手动:函数名<type1,type2,type3...>(实参)
    */
    

    C++编译器不会把函数模板当做一个函数实体,相当于一个可以生成不同版本函数的模具,当给函数提供了实参发生调用时,才会生成函数实体

    调用函数模板生成具体函数实体的过程叫做实例化,遵循“惰性实例化”准则

    函数默认的类型参数也可以像成员函数的参数一样设置默认类型,规则与成员函数的默认形参一致,但是只有C++11语法才支持,编译需要加 -std=gnu++0x 或 -stu=c++0x

  • 函数模板的特化
    模板虽好但不是万能的,不能解决所有问题,有些特殊的类型与普通类型的运算规则不同,因此需要给特殊类型实现一个特殊版本,称为函数模板的特化版本,例如 char*

    //	函数模板特化格式:
    template<>
    特化类型返回值 函数名(特化类型 参数1···)
    
    1. 特化前,必须有一个基础的函数模板
    2. 一定要加template<>,否则就会变成了普通函数
    3. 编译器有优先调用特化版本,不会普通函数模板冲突
    4. 当同时存在类型相同的普通函数,特化模板,普通模板时,且会优先调用普通模板函数
    5. 普通函数无论是否被调用都会生成二进制指令,但是模板特化依然是遵循“惰性实例化”准则
    6. 特化函数的形参和返回值基础类型,要与模板函数基础类型一致,否则报错!
      例如:函数模板中使用引用,特化也要加引用。也就是除了特化类型不同,其它要一致

作业:实现二分查找,冒泡,选择,插入,快排,归并,堆等算法的函数模板

类模板

类模板:是一种使用未知类型来设计类的技术

template<typename M,typename T,typename R...>
class Test
{
    	M num;
  	public:
    	Test(T t) {}
    	R func(void)
        {
            R ret;
            return ret;
        }
};
  • 类模板的使用:
    必须先实例化对象才能使用对应的类模板,与函数模板不同的是它不支持自动实例化,必须显示地手动实例化
    类名<类型名1,··· > 对象名;

练习:实现链式类模板

template<typename T>
struct Node {
    T data;
    Node* next;
    Node(T& data):data(data),next(NULL) {}
}
template<typename T>
class Queue {
    Node<T>* head;
    Node<T>* tail;
    public:
    	Queue(void):head(NULL),tail(NULL) {}
    	bool empty(void) {
            return head == NULL;
        }
    	void push(T& data) {
            Node<T>* node = new Node<T>(data);
            if( empty() ) {
                head = node;
                tail = node;
            } else {
                tail→next = node;
                tail = node;
            }
        }
    	void pop(void) {
            Node<T>* temp = head;
            head = head→next;
            delete temp;
        }
    	T& front(void) {
            return head->data;
        }
    	T& back(void) {
            return tail->data;
        }
    	size_t size(void) {
            size_t ans = 0;
            Node<T>* node = head;
            while( node ) {
                node = node→next;
                ans ++;
            }
            return ans;
        }
}
int main() {
    Queue<const char*> queue;
    const char* str[] = {
        "abcdef0",     "abcdef1",      "abcdef2",
        "abcdef3",     "abcdef4",      "abcdef5",
    };
    for(int i = 0 ; i < 6 ; i ++ ) {
        queue.push( str[i] );
        cout << queue.back() << endl;
    }
    cout << "----------------" << endl;
    for(int i = 0; i < 6 ; i ++ ) {
        cout << queue.front() << endl;
        queue.pop();
    }
}
  • 类模板的静态成员
    类模板的金他爱成员与普通类的静态成员一样都需要在类外定义,并且在定义静态变量时需要以以下个格式定义:

    template<typename T>
    class Test {
        static 类型名 静态成员名;
    }
    template<> 类型名 Test<实例化类型>::静态成员名 = 初始化数据;
    
  • 递归实例化
    什么类型都可以作为类模板的类型,包括类模板

    template<typename T>
    class A {
        T t;
    };
    
    template<typename T>
    class B {
        T t;
    };
    
    int main() {
        A<B<...> > a;	//	一定要加空格! 模板<套娃>
    }
    
  • 类模板的默认形参
    与函数模板一样设置,方法规则一致

  • 类模板的局部特化
    当类模板中的成员函数不能支持所有类型时,可以针对不同的类型提供了模板成员函数的局部特化版本

    • 方法1:通过类外实现一个局部特化函数

      template<> 返回值 类型<特化类型>::函数名(特化类型参数)
      {
          //	除了特化类型外,其它内容与普通版本一样
          //	必须类外实现
      }
      
    • 方法2:在原成员函数中通过typeid,比较模板类型是不是特殊类型,如果是,可以通过分支语句进行特殊处理,就不需要实现特化

      //	typeid 头文件 <typeinfo>
      if( typeid(T) == typeid(const char*) ) {
          //	特殊处理
      } else {
          //	正常处理
      }
      
  • 类的全局特化
    为特殊类型的类模板实现一个全新的特化版本,称为类的全局特化

    template<>
    class Test<特殊类型> {	//	使用特殊类型重新实现类		}
    
  • 在定义类模板和函数模板是,class 可以与 typename 替换

智能指针

常规指针的缺点:

? 当一个指针离开了作用域时,只有该指针变量占用的内存空间(4字节、8字节)被释放,而他所指向的内存空间不会被释放,当free、delete、delete[] 语句无法执行、或者忘记执行时,就会导致内存泄漏。

智能指针的优点:

? 智能指针是一个封装了常规指针的类类型对象,封装了 * 和 → 运算符
? 当智能指针离开了作用域时,它的析构函数就会自动执行,析构函数中执行了常规指针的释放操作,从而做到了自动释放内存的效果,避免了内存泄漏

智能指针与常规指针的相同点:

? 智能指针虽然是类对象,但是它重载了* 与 → ,因此在使用时可以像常规指针一个使用

C++STL中提供了四个智能指针:auto_ptr unique_ptr shared_ptr weak_ptr

其中C++98只有 auto_ptr ,C++11中只支持后三个,第一个被弃用了,使用会产生警告

头文件

  • auto_ptr
    采用独占式拥有模式,不能同时有多个auto_ptr指向一个资源,但是又不能完全限制指向操作,因此有隐患!

    auto_ptr<类型> p1(new 类型(初始值));
    cout << *p1 << endl;	//	访问内存
    auto_ptr<类型> p2;
    p2 = p1;	//	不报错,p1转移所有权p2
    cout << *p1 << endl;	//	空指针解引用
    //	auto_ptr 不能放入容器内
    
  
- unique_ptr    独享指针
  相当于auto_ptr的升级版本,完全实现了独占式拥有模式,保证同一时间内只有一个
  unique_ptr指向某一资源,通过把拷贝构造函数,赋值操作函数声明为delete来实现独占的效果

  ```c++
  unique_ptr<类型> p1(new 类型(初始值));
  cout << *p1 << endl;	//	访问内存
  unique_ptr<类型> p2;
  p2 = p1;	//	不报错,p1转移所有权p2
  p2 = unique_ptr<string>(new string("xixi"));//不报错,允许临时对象的赋值
  unique_ptr<类型> p3;
  p3 = move(p1);	//	p1 = NULL,P3指向原p1内存
  注意:如果想要转移使用权,可以使用C++标准库函数move进行
  • shared_ptr 共享指针
    采用共享式拥有模式,让多个shared_ptr同时指向相同资源
    当shared_ptr指向某个资源时,在内部有该资源会有一个引用计数,并且+1
    当有别的shared_ptr指向该资源时,原来的引用计数+1
    当一个shared_ptr离开了作用域时,所指向的资源的引用计算-1,并放弃指向
    或者当调用了reset成员函数时,放弃指向资源,引用计数-1
    当该资源的引用计数为0时,最后一个shared_ptr在结束前,自定释放资源内存

    //	相关函数
    use_count();	//	返回引用计数的数量
    unique();		//	返回是否独占资源
    reset();		//	放弃对资源的指向
    swap();			//	标准库函数,交换两个指针指向
    get();			//	返回内部对象(指针)
    
    • shared_ptr的循环引用问题:
      当有两个类,都有可以指向对象类型的共享指针成员变量,并且在类外分别通过另外两个共享指针指向new出来的这两个类对象,并且让它们内部的共享指针成员变量分别指向对方类对象,因此就构成了shared_ptr的循环引用死锁
  • weak_ptr 弱引用指针
    weak_ptr是为了配合shared_ptr而引入的一种智能指针,它指向一个由shared_ptr管理的资源,但是不影响该资源的生命周期,也即是weak_ptr指向一个由shared_ptr管理的资源时,shared_ptr的引用计数不会改变
    无论是否有weak_ptr指向,一旦指向资源的shared_ptr计数为0,资源就会释放。因此weak_ptr更像是shared_ptr的助手
    因此当shared_ptr因为循环引用产生死锁问题时,把其中一个类的shared_ptr成员变量 变为 weak_ptr即可

C++异常处理

  • 什么是异常?

    • 当代码出错时,停止执行,返回一个数据。
    • C语言中调用者只能通过使用,接收返回值的方式处理错误、异常
    • C++中可以接收自己返回或者系统返回的返回值,根据返回数据的类型不同,从而执行不同的错误,异常处理语句
  • 如何抛异常

    throw 数据;	//	数据可以是任意类型
    //	注意:尽量不要抛局部变量
    
  • 如何捕获异常

    try{	//	可能会产生异常的代码段(例如函数调用等)	
    }catch(类型名& 变量名){	//	处理错误
    }
    
  • 抛异常的说明

    /*返回值 函数名(参数列表) throw(类型1,类型2,···){ //	代码块	} */
    //	异常说明相当于该函数的限制或承诺,只能抛出说明过的异常类型,如果函数抛出了异常说明外的异常类型,运行时无法捕获并会产生运行错误
    
  • 标准异常

    C++中已经定义好的异常类,当对应的异常发生后,会自动抛出定义好的异常类对象

    exception 该异常是所有标准C++异常的父类,能捕获所有的标准异常

    bad_alloc 该异常可以通过 new 抛出(内存不够),如果是C++11,会抛出它子类 bad_array_new_length

    bad_cast 该异常

  • 自定义异常类

  • 使用异常需要注意的问题

    1. 不要返回局部变量,对象的地址,但可以返回临时对象
  1. 建议都用引用的方式进行捕获异常,否则会构造,拷贝两次
  2. 不要再构造函数,析构函数抛异常
  3. 如果存在父类类异常,先捕获子类类型,在捕获父类类型

STL标准模板库

STL是Standard Template Library缩写,中文名叫做标准模板库。(B站搜索侯捷STL,学习模板进阶知识)

库,由惠普实验室提供的(使用了C++的模板语言封装的常见的数据结构和算法),里面共有三大类内容:

算法:以函数模板形式实现的常用算法,例如:swap、max、min、find、sort

容器:以类模板形式实现了常用的数据结构,例如:栈,队列,链式表,顺序表,红黑树

迭代器:它是容器的成员,用于帮助访问容器中的元素,使用时方法类似指针

常用的算法函数:

#include <algorithm>
iterator find(iterator start,iterator end,const TYPE& val);
功能:顺序查找
start:指向第一个元素的迭代器
end:指向最后一个元素的下一个位置的迭代器
val:带查找的关键数据
返回值:如果[start,end)范围内找到val,返回该val的迭代器;
       如果没找到,返回end;
void sort(iterator start,iterator end);
void sort(iterator start,iterator end,StrictWeakOrdering cmp);
功能:快速排序
注意:排序的元素要支持 < 运算符,否则要在后面提供比较函数

Vector向量容器

头文件:#include

采用顺序表结构存储数据,可以通过下标随机访问元素,因此也称为数组容器

#include <vector>
vector(size_type num,const TYPE& val = TYPE());
num:数组的长度
val:用于初始化vector中所有的元素,不给默认为0
vector(input_iterator start,input_iterator end);
功能:通过使用一组数据进行初始化向量容器

/*支持的运算符*/
== != <= >= < > 会对容器进行整体比较,根据字符串的比较规则进行比较(一个个元素按照字典序比较,一旦比较出结果就结束)
[]	让向量可以当数组使用,一样不检查下标是否合法,当下标 >= size(),可能会出现段错误,向量不会随着访问扩容

/*常见成员函数*/
void assign(size_type num,const TYPE& val);
功能:使用一组数据为val的数据给向量赋值
void assign(input_iterator start,input_iterator end);
功能:使用一组范围(start,end)的数据给向量赋值
> 注意:赋值函数让向量原数据被覆盖,且数量会自动增加或减少
TYPE& at( size_type loc );
const TYPE& at( size_type loc ) const;
功能:访问下标为loc的元素,相当于[];当loc越界是at函数一定会抛出异常;但是[]越界可能会出现段错误
TYPE& back();
const TYPE& back() const;
功能:返回向量容器中的最后一个元素
TYPE& front();
const TYPE& front() const;
功能:返回向量容器中的第一个元素
iterator begin();
功能:返回指向第一个元素的迭代器
iterator end();
功能:返回指向最后一个元素的迭代器
/*	begin()与end()的使用
for(vector<int>::iterator it = v.begin() ; it != v.end() ; it ++ ) {
	cout << *it << endl;
}
*/
size_type capacity() const;
功能:获取向量的容量,容量是可以随容器的使用,动态变化
size_type size() const;
功能:获取向量的现有的元素数量;
void clear();
功能:清空向量容器中的所有元素。容量不变,数量清零!
bool empty() const;
功能:判断向量元素是否为空,空位真。
iterator erase( iterator loc );
功能:删除一个元素
iterator erase( iterator start, iterator end );
功能:删除[start,end)范围的元素
> 注意:参数必须为迭代器。删除清掉元素,容量不变。
void insert(iterator loc,const TYPE& val);
功能:在某个元素之前插入值为val的元素,位置必须以迭代器形式提供
void insert(iterator loc,size_type num,const TYPE& val);
功能:在某个元素之前插入num个值为val的元素,位置必须以迭代器形式提供
void insert(iterator loc,input_iterator start,input_iterator end);
功能:在某个元素之前一组[start,end)数据,位置必须以迭代器形式提供
size_type max_size() const;
功能:计算出向量中最多能存储的元素个数;<该结果与向量存储的元素类型及计算机的内存大小有关>
void pop_back();
功能:删除最后一个元素;
void push_back( const TYPE& val );
功能:在末尾添加一个元素,可能会导致容量不足,不足会自动扩容(在原基础的容量上,翻倍扩容)。

reverse_iterator rbegin();
const_reverse_iterator rbegin() const;
功能:返回一个逆向迭代器,它会指向最后一个元素
reverse_iterator rend();
const_reverse_iterator rend() const;
功能:返回一个逆向迭代器,它会指向第一个元素的前一个
void reserve( size_type size );
功能:修改向量容器的容量
> 注意:只能修改后比原容量大
void resize( size_type num, const TYPE& val );
功能:修改向量容量的容量
> 注意:往大的改,就会在末尾添加num个值的val的元素,且容量边为num
	往小的改,就会从末尾清空到容器内只剩num个元素,容量不变!

LIst链表

头文件:include

是一个功能齐全的双向循环链表

list( size_type num,const TYPE& val = TYPE() );
功能:创建num个元素,初始值为val的链表
list( input_iterator start, input_inerator end );
功能:创建一组数据初始化链表  
//	所支持的运算符 = == != < > <= >= ,也就是链表整体以字符串比较规则进行比较
//	元素必须支持 < 才能拿使用以上运算
/*链表常用成员函数*/
void assign( size_type num, const TYPE& val );
功能:向链表赋值num个值为val的数据
void assign(input_iterator start,input_iterator end);
功能:向链表赋值一组(start,end)的数据
iterator begin();
功能:返回指向第一个元素的正向迭代器
const_iterator begin() const;
功能:返回指向第一个元素的(常)正向迭代器	// 返回值不可修改
iterator end();
功能:返回指向最后一个元素的正向迭代器
const_iterator end() const;
功能:返回指向最后一个元素的正向迭代器		// 返回值不可修改
TYPE& back();
const TYPE& back() const;
功能:返回链表容器中的最后一个元素
TYPE& front();
const TYPE& front() const;
功能:返回链表容器中的第一个元素
void clear();
功能:清空链表的所有元素
iterator erase( iterator loc );
功能:删除链表中指定位置的一个元素,以迭代器形式提供位置
iterator erase( iterator start, iterator end );
功能:删除链表中一段[start,end)范围的元素,以迭代器形式提供位置
> 注意:链表的迭代器中不允许it+n这种语法,因为不支持随机访问
inerator insert( iterator loc, const TYPE& val );
功能:向链表中插入一个值为val的元素,位置以迭代器形式提供
void insert(iterator loc,size_type num,const TYPE& val);
功能:向链表中插入num个值为val的元素,位置以迭代器形式提供
template<TYPE> void insert(inerator loc,input_iterator start,input_iterator end);
功能:向链表中指定位置,插入一组元素,位置以迭代器的形式提供

void merge( list &lst );
功能:按照顺序合并两个链表
void merge( list &lst , BinPerd compfunction );
功能:合并两个链表,如果不支持 < 运算符,需要此函数提供比较函数
> 注意:想要合并后有序,合并前也必须各种有序,需要向各自排序后才能有序合并,合并后lst元素会删除
void sort();
void sort( BinPerd compfunction );
功能:向链表中的元素进行快速排序,需要支持 < 运算符才行,否则需要提供比较函数
reverse_iterator rbegin();
const_reverse_iterator rbegin() const;
功能:返回一个逆向迭代器,它会指向最后一个元素
reverse_iterator rend();
const_reverse_iterator rend() const;
功能:返回一个逆向迭代器,它会指向第一个元素的前一个

void remove( const TYPE &val );
功能: 移除链表中所有等于val的元素
void remove_if( UnPerd pr );
功能: 删除符合条件的元素
/*
bool pr(const T& num) {
	return num < 50;	//	写想要删除条件
}
*/
void resize(size_type num,const TYPE& val = TYPE() );
功能:修改链表中的元素数量为num
     往大了修改,相当于尾添加值为val的元素
	 往小了修改,相当于尾删除,使链表中的元素只剩num个
void reverse();
功能:反转链表
void splice( iterator pos ,list& lst );
功能:把链表lst合并到当前链表的指向位置中,位置以迭代器形式提供,lst会删除所有元素
void splice( iterator pos,list& lst, iterator start,iterator end );
功能:把链表lst指定del位置的元素合并到当前链表pos位置,位置以迭代器形式提供,lst会删除del开始元素
void spllice(iterator pos,list& lst,iterator start,iterator end)
功能:把链表lst指定del位置的元素合并到当前链表pos位置,位置以迭代器形式提供,lst会删除[start,end)开始元素
void unique();
功能:删除重复元素,只保留一个
void unique( BinPred pr );
功能:删掉重复元素,但元素类型必须支持 == 运算符,否则需要提供比较函数 pr

Queue队列容器

头文件:#include

链式队列!不支持运算符,没有迭代器,只有无参构造和拷贝构造

#include <queue>  头文件
back();		对尾元素
front();	队头
empty();	是否为空
push();		入队
pop();		出队
size();		元素个数

/* priority_queue 优先队列容器 */
> 优先队列,该队列会对入队的数据进行排序,默认元素值越大,优先级越高,越先出队
> 存储的元素必须支持 < 运算符
top();		队头
empty();	是否为空
push();		入队
pop();		出队
size();		元素个数
> 注意:如果类型不支持,需要手动实现 < 运算符函数,可以根据需要在该函数修改优先级

Stack栈容器

#include <stack>	头文件
//	链式栈!不支持运算符,没有迭代器,只有无参构造和拷贝构造
push();		入栈
pop();		出栈
top();		栈顶
size();		栈内元素个数
empty();	栈是否为空

deque双向队列容器

#include <deque>
双向队列容器,用法与vector一模一样,只是底层采用双向链表存储数据。
vector是顺序表,deque链表
优点:比vector节约内存
缺点:速度要比vector慢
> 支持运算符:[] 可以向向量"随机访问"元素(底层是for循环遍历)
 = ==  !<  < > <= >=   比较规则与vector一致
//	注意:虽然可以向随机访问一样去访问元素,但底层不支持随机访问的,本质上[]运算符还是遍历访问,因此速度比vector慢,其实就是在vector的基础上增加了双端操作的功能而已

set集合容器

#include <set>
//	集合容器,特点是元素不重复,会对元素自动排序,因此它存储的元素必须支持 < 运算符,只能迭代器遍历访问
//	运算符:与list一致。
//	底层采用红黑树实现的
size_type count( const key_type& key );
功能:获取集合中key元素的数量,只能是1或0
pair<iterator,iterator> equal_range(const key_type& key);
功能:查找容器中key的元素位置范围,返回一个数据对[start迭代器,end迭代器);
iterator find( const key_type& key );
功能:查找set中为key的元素位置,如果找到返回该元素的迭代器,否则返回end()的值
iterator insert( iterator i, const TYPE& val );
功能:查找set中为key的元素位置,如果找到返回该元素的迭代器,否则返回end()的值
pair<iterator,bool> insert(const TYPE& val);
功能:向容器中插入一个数据
返回一个键值对,成功(first迭代器,ture),否则(end迭代器,false);
key_compare key_comp() const;
功能:返回容器中元素的比较方法,使用该方法可以该类型的数据进行比较
/*
set<T>::key_compare 自定义函数名 = s.key_comp(); 
cout << cmp(T t1,T t2) << endl;
*/
iterator lower_bound( const key_type& key );
功能:返回一个最小的大于等于key的迭代器 ( >= )
iterator upper_bound( const key_tyoe& key );
功能:返回一个最小的大于key的迭代器 ( > )

multiset多重集合

多重集合容器,特点是元素可以重复,依然会对元素自动排序,因此它存储的元素也必须支持 < 运算符,只能用迭代器遍历

size_type count( const key_type& key );
功能:获取multiset集合中key元素的数量,只能是1或0
pair<iterator,iterator> equal_range(const key_type& key);
功能:查找multiset容器中key的元素位置范围,返回一个数据对[start迭代器,end迭代器);
//	与set大致相同,此时上面两个函数在multiset中有意义

map容器

映射容器,是存储了由(key/value)组成的元素(字典,键值对),要求键值对中的key不能重复,而且会根据key进行排序。要求key必须支持 < 运算符
key和value是一 一对应的,可以根据key类访问value;
一般会在里面存储频繁需要查找的数据,因为map的查找速度特别快(Redis内存数据库,内部使用map类似结构)
底层使用红黑树,查找效率高

map( iterator start , iterator end );
功能:使用一组数据pair<key/value>数据构造映射容器
map( iterator start , iterator end , const key_compare cmp );
功能:使用一组数据pair<key/value>数据构造映射容器
/*支持[]运算符:通过[]运算符,将key作为下标,也可以给该键值对赋值(插入) m[key] = value; */
iterator insert( iterator i , const TYPE& pair );
功能:在指定位置插入一个键值对(pair类型)
void insert(input_iterator start,input_iterator end );
功能:向容器中插入一组数据(pair类型)
pair<iteraotr,bool> insert( const TYPE& pair );
功能:向映射容器插入一个键值对,返回插入位置以及结果
key_compare key_comp() const;
功能:返回map容器中key的比较函数,该函数可以针对相同类型的键值对key进行比较
value_compare value_comp() const;
功能:返回map容器中value的比较函数,该函数可以针对相同类型的键值对进行value的比较

multimap多重映射容器

使用方法与map一致,但不同的是它的key可以对应多个value,所以无法支持[]运算符

通过迭代器删除元素时需要注意的问题:

对于关联容器(set \ multiset \ map \ multimap ),删除当前iterator时,仅仅会让当前的迭代器失效,可以在erase时,递增当前的iterator可以连续删除 set.erase(it++) ,因为它们底层都是红黑树实现,因此,插入,删除一个节点不会导致其它节点无法访问
对于顺序容器(vector),当删除当前iterator会时后面的元素的iterator失效,因为它底层是连续分配的内存。因此不能vector.erase(it++);
但是可以通过返回值拿下一个元素的迭代器,从前赋值给it的方式删除。 it = vector.erase(it);

bitset位集合容器

位集合容器,是一种封装了各种位运算操作的数据结构

支持运算符:

!= == &= ^= |= ~ <<= >>= []
成员函数

bool any();
功能:任意有一位二进制位1,返回真
size_type count();
功能:返回二进制位 为1的个数
bitset<N>& flip();
功能:指定二进制位求反
bitset<N>& flip( size_t pos );
功能:指定二进制位求反
bitset<N>& reset();
功能:把所有二进制 清零
bitset<N>& reset( size_t pos );
功能:把所有二进制 清零
bitset<N>& set();
功能:把所有二进制位置1
bitset<N>& set( size_t pos , int  val=1 );
功能:把指定二进制指针1
size_t size();
功能:计算二进制的位数
bool test( size_t pos );
功能:测试某个二进制位是0还是1
string to_string();
功能:把二进制转换成字符串
unsigned long to_ulong();
功能:把二进制转化成unsigned long

注意:bitset在C++中用处不大,但是在嵌入式开发有很大用处

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-11-05 00:06:35  更:2022-11-05 00:10:11 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/19 6:21:10-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码