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代码变成可执行文件的详细过程
- 预处理 把源文件翻译成预处理文件
gcc -E code.c 显示预处理结果到终端 gcc -E code.c -o code.i 生成以.i结尾的预处理文件 - 编译 把预处理文件翻译成汇编文件
gcc -S code.i 生成以.s结尾的汇编文件 - 汇编 把汇编文件翻译成二进制的目标文件
gcc -c code.s 生成以.o结尾的目标文件 - 链接 把若干个目标文件合并成一个可执行文件
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
数据类型
? 为什么要对数据进行分类?
-
? 现实中的数据本身就自带类别属性 -
对数据进行分类可以节约存储空间,提升运行速度 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){
case val:
· · ·
break;
case val2:
default:
}
? case 1 ··· 3:可以表示[a,b]的范围,但是只有在GNU编译器才支持该语法,不建议使用。
练习:输入一个月份,判断它是什么季节
(春:123月份,夏:456月份,秋:789月份,冬:10,11,12月份)
练习:输入一个月份,判断该月有多少天。(不考虑闰年)
for循环语句
? 循环就是一种让代码反复执行的方法,到达你想要效果for循环是一种非常灵活,变样多样且危险的循环
for([1];[2];[3]){
[4]
}
? 大括号建议使用:
? 1.建议上下对齐
? 2.如果循环体中,只有一行代码,大括号可以省略
? 但是不利于扩展,一般的商业代码都要求大括号不能省略
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之间所有素数
跳转语句
-
goto 可以在函数内,任意跳转。 标签名:
···
goto 标签名
练习:计算N的的阶乘,不能使用循环语句实现
int s = 1;
lx:
s *= N--;
if( N )
{
goto lx;
}
printf("%d\n",s);
-
break 1. 在switch中关闭case执行开关
2. 跳出循环,只能跳一层循环
-
continue 结束本次循环,进入下一次循环
-
return 1. 结束函数的执行,返回到调用位置
2. 返回一个数据给函数的调用者
数组
? 什么是数组:变量的组合,是一种批量定义类型相同的变量的方式
int array[100];
练习:定义一个长度为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] = {
{'#','#','#','#','#','#','#','#','#','#'},
{'#',' ','#',' ','#',' ',' ',' ',' ','#'},
{'#','@','#',' ','#',' ','#','#',' ','#'},
{'#',' ',' ',' ','#',' ','#','#',' ','#'},
{'#',' ','#','#','#',' ','#','#',' ','#'},
{'#',' ','#','#','#',' ',' ','#',' ','#'},
{'#',' ','#','#','#',' ','#',' ',' ','#'},
{'#',' ','#','#','#',' ','#','#',' ','#'},
{'#',' ',' ',' ',' ',' ','#','#',' ',' '},
{'#','#','#','#','#','#','#','#','#','#'}
};
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语言中管理代码的最小单位
? 把代码封装成一个个函数,可以方便管理和调用代码
练习:实现一个函数,判断是否是素数,调用它显示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;
for(int i = 2 ; i <= 100 ; i ++ )
{
int carry = 0;
for(int j = 0 ; j < cnt ; j ++ )
{
int num = rets[j] * i + carry;
rets[j] = num % 10;
carry = num/10;
}
while( carry )
{
rets[cnt++] = carry % 10;
carry /= 10;
}
}
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
const
- "保护"变量的值不能被显示地修改,但是能可以通过访问内存来修改值
- 但是如果修饰的是初始化的全局变量,初始化的静态变量,则该变量会从data改为text,变成“常量”
volatile
- 如果变量的值没有被显示的修改,那么在使用该变量时,不会从内存中读取,而是继续使用上一次读取的结果,这个过程叫做取值优化,一般变量都会进行。
- 变量被volatile修饰后,编译器不对该变量进行取值优化,每次都是从内存中重新读取
- 一般硬件编程,多线程编程时会使用到
register
- 申请把变量的存储介质由内存改为寄存器,由于寄存器数量有限,不一定申请成功
- 存储介质:硬盘 - 内存 - 高级缓存 - 寄存器
- 寄村器变量不能取地址
typedef
- 类型重定义,在定义变量时,在类型前加typedef,那么变量名就变成了这个类型的新名字
- 注意:typedef不是替换关系
小项目:五子棋
? 需要的数据:
#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;
}
}
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;
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();
Louzi();
if( )
{
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 = 地址;// 必须是有意义且有权限的地址
-
解引用: *变量名_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 )
- 当函数的参数是指针,别人传给你的指针可能是空指针
- 从函数获取的返回值是指针类型时,可能会返回空指针
-
野指针:
- 指向不确定的内存空间的指针叫做野指针
- 对野指针解引用的后果:
- 野指针比空指针的危害更严重,因为它无法判断出来,而且可能是隐藏性的错误,短时间不暴露。
- 所有的野指针都是程序员自己制造出来的,如何避免产生野指针
- 定义指针变量时一定要初始化( 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];
注意:当数组作为函数的参数时锐变成了指针,所以长度丢失
-
指针是变量是拥有自己存储空间,它与所指向的内存,是指向关系 -
二级指针
-
函数指针
- 函数名就是一个地址,函数名代表了函数在代码段(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的过程。
使用堆内存需要注意的问题
? 内存泄漏:内存无法再使用,也无法被释放,而需要在次使用时只能重新申请。
? 上述过程重复使用,会将内存逐渐耗尽
注意:一旦进程结束属于该进程的所有资源都会被操作系统会回收
? 如何尽量地避免内存泄漏:谁申请内存,谁释放;谁知道该释放,谁释放;
? 如何判断,定位内存泄漏:
-
ps -aux Linux系统查看运行的进程 任务管理器 Windows系统
-
借助代码分析工具 mtrace ,检查malloc和free是否成对出现 -
封装malloc,free,记录申请,释放的信息到日志文件中
? 内存碎片:已经释放了,但依然无法继续使用的内存叫做内存碎片。由于申请和释放的时间不协调导致的,无法完全避免只能尽量减少
? 如何减少内存碎片:
1. 尽量使用栈内存
2. 尽量不要频繁的申请和释放内存
3. 尽量申请较大块的内存自己管理
内存清理函数
#include <strings.h>
void bzero(void *s, size_t n);
#include <string.h>
void *memset(void *s, int c, size_t n);
#include <stdio.h>
#include <stdlib.h>
void CreateHeap(void** p,size_t n){
*p = malloc(n);
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 (*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]);
}
}
? 缺点:对连续内存要求高,可能申请失败
? 优点:申请简单
练习:计算出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;
}
输入输出缓冲区
? 程序中要输出显示的数据并不会立即显示在屏幕上,而是先存储在输出缓冲区中,当满足一定条件时才会从缓冲区中显示在屏幕上
- 遇到’\n’换行字符后
- 遇到输入语句时
- 当输出缓冲区满的时候,也会全部显示出来
- 当程序结束时,结束之前会显示在屏幕上
- Linux中手动刷新 fflush(stdout);
输入缓冲区
程序并不是立即从键盘获取输入的内容,而是当按下回车后,终端输入的内容会先存储到输入缓冲区中,然后函数再从输入缓冲区中读取数据到内容中。
-
当你想要输入的数据是整性或浮点型数据,但是缓冲区中的数据是符号或字符时,读取会失败,数据会残留在缓冲区中,影响接下来所有数据的读取 ? 解决: ? 判断scanf的返回值是反全部正确,如果不是,先清理输入缓冲区残留数据,在提示重新输入。 -
fgets可以接受指定size-1个字符,如果有多余的字符会残留在缓冲区中,可能会影响接下来的输入 解决: 先判断’\n’在不在字符串内,如果不在,则说明在缓冲区内,需要缓冲清理 scanf(“%*[^\n]”); 从缓冲区中读任意数据并丢弃,如果不是’\n’,继续读取,直到运用’\n’,停止读取(正则表达式) scanf(“%*c”);从缓冲区中读入一个字符并丢弃
注意:可以考虑封装新的fgets函数解决输入过多问题
Linux:
stdin->_IO_read_ptr = stdin->_IO_read_end; 将当前缓冲区中的当前位置指针,移动到缓冲区末尾,相当于清理了输入缓冲区,但是只能在Linux、UNIX系统中使用这两个位置指针 -
当先输入整数,浮点型数据,再输入字符,字符字符串时,前一次的输入会残留’\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);
long atol(const char *nptr);
long long atoll(const char *nptr);
double atof(const char *nptr);
#include <string.h>
char *strstr(const char *haystack,const char *needle);
int sprintf(char *str,const char *format,··· );
int sscanf(const char *str,const char *format,··· );
void *memcpy(void *dest,const void *src,size_t n);
int memcmp(const void *s1,const void *s2,size_t n);
通讯录系统
? 要求:存储联系人信息:姓名,性别,电话。最多存50人。
? 功能要求:
增加
除(按名字)
修改(按名字)
查找联系人,按电话或姓名,支持模糊查找
显示所有联系人信息
退出系统
#include <stdio.h>
#include <stdlib.h>
#include <getch.h>
#include <unistd.h>
#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;
}
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
修改 ~/.bashrc 终端配置文件
#define 定义宏
注意:宏函数后面的替换不能直接换行,需要在每一行末尾通过续行符 \ 来换行
? 如果有多行代码也可以使用大括号保护
练习:实现一个交换两个变量的值的宏函数,数据类型通用,能写多少个,分析优劣
#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 的区别
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皆为指针 -
宏函数与普通函数的区别,优缺点
-
是什么? 宏函数:带参数的宏替换,只是代码替换,只能使用时想函数而已,不是真正的函数 函数:是一段具有某项功能的代码集合,会被翻译成二进制指令存储代码段,函数名就是它的首地址,有独立的栈内存,命名空间 -
有什么不同? 函数:有返回值,类型检查 内存的申请释放 不冗余 宏函数:运算结果 不检查内容类型 通用 替换 冗余 快 -
注意:调用频繁,内容简单的功能适合写成宏函数
条件编译预处理
#include <stdio.h>
#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 $@
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
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)
gedit + 要打开的文件
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;
}
{
#include <stdbool.h>
extern int (*view)[4];
extern int score;
void show_view(void);
void rand_two(void);
bool is_die(void);
}
#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 重定义结构类型名
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];
int num;
}
- 如何判断系统的大小端问题?
- 假设十六进制数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的基础上,增加读权限
返回值:文件指针,不需要关心里面有什么数据,只需要知道它是一个针对已打开的文件凭证,打开文件失败会返回NULL。
int 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,失败返回-1。
void rewind(FILE *stream);
功能:把文件位置指针设置到文件开头
long ftell(FILE *stream);
功能:获取当前文件位置指针的位置
返回值:在第几个字节
int fgetc(const *stream);
功能:从文件中读一个字节
返回值:失败,读取完毕返回EOF(-1)
char *fgets(char *s,int size,FILE *stream);
功能:读取一行字符串到s中,最多读size-1个
int fputc(int c,FILE *stream);
功能:读取一个字符到文件中
返回值:成功返回非负整数,失败返回-1;
int fputs(int ,FILE *stream)
功能:写一个字符串到文件中
返回值:成功返回非负整数,失败返回-1;
int remove(const char *pathname);
功能:删除文件
pathname:文件路径
返回值:成功返回0,失败返回-1;
int 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+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的结点 双亲结点和孩子结点:结点的子树被称为该结点的孩子结点,该结点就是被称为孩子的双亲结点 ? 兄弟结点:具有同一个双亲结点的结点,互为兄弟结点 ? 祖先结点:从根节点出发到该结点,路径上经过的所有结点都称为该结点的祖先 ? 子孙结点:一个结点的子树中任意一个结点都是它的子孙 ? 堂兄弟结点:双亲结点互为,兄弟结点。 -
树的存储
? 兄弟表示法:
? 链式
? 双亲只存储第一个子节点 数据 链式指向所有兄弟节点
? 优点:可以方便找所有兄弟节点
? 缺点:找双亲不便
注意:普通数不常用,一般会使用二叉树进行存储
二叉树
? 一种常用的数据结构,比普通树处理起来要简单,而且普通也比较方便地转换成二叉树。
? 定义:节点的度为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的右节点,否则没有右节点 。
二叉树的操作
? 构建,销毁,遍历,高度,密度,插入,删除,查询,求左,求右,求根。
二叉树的存储
顺序存储:必须要安照完全二叉树的格式,把每个节点按从上到下,从左到右的顺序依次存入连续的内存中,如果有空位置则使用特殊数据代替存入
数据项:存储节点的内存首地址,
链式存储
二叉树的遍历
- 前序遍历:根,左,右
- 中序遍历:左,根,右
- 后序遍历:左,右,根
注意:无论是前中后,由根节点决定,并且左右子树的遍历顺序不会改变
根据前序+中序,或者,后序+中序 就可以还原一颗树
注意:层序遍历,配合队列使用
有序二叉树
- 左子树的数据小于根,右子树的数据大于根,这种树被称为有序二叉树,二叉树搜索树,二叉排序树
注意:这种树的节点需要频繁地插入,删除,因此不适合顺序存储
有序二叉树的中序遍历刚好就是升序或降序,所以有序二叉树也是一种排序算法,查找又天然是二分查找。
练习:
- 把一棵二叉树转换为它的镜像树。
- 输入两棵二叉树A,B,判断B是不是A的子结构(我们约定空树不是任意一个树的子结构)。
- 将一棵有序二叉树转换成一个有序的双向链表。
- 计算出有序二叉树中倒数第K个大的数。
- 判断一个二叉树是否对称。
- 请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其他行以此类推。
线索二叉树
- 规律:在N个节点的链式二叉树中必定有N+1个空指针域(NULL指针)。
- 有序链式二叉树中有很多的空指针。如果要使之有意义,可以让它们的空指针指向下一个,或前一个,这样在遍历时,可以不用递归,而可以使用循环遍历,提高树的遍历速度。
中序线索二叉树节点数据项
? 数据
? 左子树指针
? 右子树指针
? 由子树指针标准位
实现过程:
- 创建有序二叉树
- 建立线索
- 通过线索遍历二叉树
选择树(胜者树,败者树)
? 是一种完全二叉树,待比较的数据都存储在最后一层,根节点是根据左右子树其中一个生成,因此更节点是最大或者最小的,选择树的功能是快捷地找出最大值或最小值。
堆:是一种完全二叉树
- 大顶堆(大根堆):根节点比左右子树大
- 小顶堆(小根堆):根节点比左右子树小
数据项:
? 存储数据的内存首地址
? 容量
? 数量
平衡二叉树(AVL树)
前提是有序的二叉树,它的左右子树的高度不超过1,而且它的所有子树也满足这个条件
如果一个有序二叉树呈现单支状(类似链表),它的查找效率接近链表,因此只有达到平衡时它的查找效率最高。
由于节点的值受限,因此只能通过调整达到有序,而不能进行值的修改
二叉树不平衡的基础原因: Y
? X - 以Y为轴向右旋转 Z X
? / \ T1
? Y T1
Z
红黑树(了解)
红黑树是一种自平衡树,它不是根据子树的高度差来调整平衡的,而是给节点设置一种颜色,来达到平衡。
红黑树的特性:
- 每个节点或者是黑色,或者是红色
- 根节点必须是黑色
- 每个叶子节点(NULL)是黑色
- 如果一个节点是红色,则它的子节点必须是黑色。(不能有两个红色的节点)
- 从一个节点出发,到该节点的子孙节点的所有路径上包含了相同数量的黑色节点
保证大致上红黑树是平衡的(最长路径不超过最短路径的两倍)。 - 插入的节点一定是红色
红黑树插入后的调整:
-
如果父节点是黑色,直接插入 -
如果父节点是红色,需要调整
? 叔叔不存在 or 叔叔为黑色
? 进行 左旋 or 右旋
? 祖父节点置红,父节点置黑
? 叔叔存在且为红色
? 祖父置红,父节点和叔叔置黑
? 把祖父节点当做当前节点
优点:插入,删除的效率比AVL树高
缺点:没有AVL树平均,查找效率没有AVL树高,但也并不差
哈夫曼树(了解)
-
基本概念:
- 路径长度:从一个节点到另一个节点之间的路径条目数。(根节点到第N层节点的路径长度为N-1)
数的路径长度:从根节点出发,到每个节点的路径长度和。 节点的权:若将书中节点赋予一个有某种意义的数值,该数值称为该节点的权 节点的带权路径长度:从根节点出发到该节点的路径长度与该节点的权的乘积。 树的带权路径长度:所有的叶子节点的带权路径长度的和,称为WPL (哈夫曼树是衡量一颗带权二叉树优劣的关键,目的是为了生成一颗WPL最小的带权二叉树) -
构建哈夫曼树:
- 把N个带权节点存入一个集合中F中,把每个节点左右子树置空
- 从F中选取权值最小的两个节点作为左右子树构建成一颗新的二叉树,且新的根节点的权为左右子树的权值之和
- 从F中删除刚刚选出来的两个节点,把新得到的根节点放入F中
- 重复2,3步骤,直到F中为空
-
哈夫曼编码:
数组与矩阵
-
数组:存储空间连续的表结构 -
矩阵:带二维信息的数据,一般用二维数组来存储矩阵 -
特殊矩阵:
-
稀殊矩阵:有效的信息数据不多,绝大多数都是无效信息数据不需要存储,没有特定的标准,全凭感觉
这些矩阵如果使用二维数组来存储的话,会非常浪费存储空间,为了节约空间,我们可以对这些矩阵进行压缩
-
上三角矩阵 [X][X][X][X]
[ ][X][X][X]
[ ][ ][X][X]
[ ][ ][ ][X]
-
下三角矩阵 [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][-]
图
- 图型结构:由有穷且非空的顶点和顶点之间边的集合组成
- 通常表示:G(V,E) G表示一个图,V是图中顶点集合(元素),E是图中所有边(元素之间的关系)的集合
- 无向图:
- 完全无向图:
- 在无向图中,任意两个顶点之间都有边,这种叫完全无向图。
- 含有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)
{
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");
}
图的遍历:深度优先遍历和广度优先遍历
#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(){
}
算法
分治:分而治之,将一个大且复杂的问题,分解成小且简单的问题
-
实现分治的方法:循环,递归 -
查找算法:
-
顺序查找
- 对待查找的数据没有要求,从头到尾逐一比较,在小规模的查找中比较常见,
#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算法中的应用
-
排序算法:
-
排序算法的稳定性:如过有值相同的数据,在排序的过程中的全程中都不会改变它们的先后顺序,则认为该排序算法是稳定的 -
冒泡排序:将相邻的左右数据进行比较,按一定规则(升序:左比右大,交换。降序:左比右小,交换),该算法对数据的有序性比较敏感。如果,待排序的数据基本有序,则效率高。
-
选择排序:假定最开始的位置是最小值,并记录计入下标min,然后与后面的数据比较,如果有比min为下标的数据还要小,则更新min,最后判断如果min的值发生了该变,则交换min位置的数据与最开始的数据。
虽然选择排序的时间复杂度较高,但是数据交换次数少,因此实际运行效率并不慢
是冒泡排序的变种,但是没有对数据有序敏感性,数据混乱情况下比冒泡块
注意:算法的时间复杂度不能代表算法的实际时间,有时候时间复杂度高的反而速度更快
-
插入排序:将数据看做两部分,一部分是有序的,将剩余数据逐个插入到有序的
-
希尔排序:是在插入排序的增强版,由于插入排序数 -
快速排序:找到一个标杆,备份标杆的值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 ){
while( l < pi && arr[l] <= pv ) l++;
if( l < pi ){
arr[pi] = arr[l];
pi = l;
}
while( r > pi && arr[r] >= pv ) r--;
if( r > pi ){
arr[pi] = arr[r];
pi = r;
}
}
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);
}
-
归并排序:先把一组待排序的数据分成单独的个体,存放到临时空间中,然后两两比较合并。
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++];
}
}
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,把下标+最小值 还原数据一次放回数组中,是一种典型的空间换时间的算法
- 该排序算法理论上速度非常快,它不是基于比较的算法,在一定范围内整数排序时快于任意的一种比较排序算法,但是有很大的局限性:适用排序整性数据,而且数据的范围差别不大
-
桶排序:根据数据的值存储到不同的桶中,然后再调用其它的排序算法,度桶中的数据进行排序,然后再从桶中依次拷贝回数组中,从而降低排序的规模以此提高排序的速度,是一种典型的以空间换时间的算法
- 缺点:如何分桶,桶范围多大,这些都需要对数据由一定的了解
- 桶排序的稳定性取决于桶内排序中使用的算法
void _bucket_sort(int* arr,size_t,int cnt,int range) {
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) {
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()
虚拟内存
- 系统会为每个进程分配4G的虚拟内存空间
- 用户只能使用虚拟机内存,无法直接使用物理内存
- 虚拟地址与物理内存进行映射后才能使用(否则就会产生段错误)
- 虚拟地址与物理内存之间的映射是由操作系统动态维护(malloc)
- 让用户使用虚拟地址,一方面是为了安全,另一方面操作系统可以让应用程序使用实际物理内存更大的地址空间
- 4G的虚拟地址空间分为两部分:一部分为用户空间[0G3G),一部分为内核空间[3G4G]
- 用户空间中的代码不能直接访问内核空间的代码和数据,可以通过系统调用(API)从用户态切换到内核态后,间接与内核交换数据
- 对虚拟内存越界访问(使用了没有映射的虚拟内存),导致段错误
映射虚拟内存和物理内存的函数
void *sbrk(intptr_t increment);
int brk(void *addr);
练习:计算出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);
int munmap(void* addr,size_t length);
内存管理总结
- sprk/prk底层维护了一个指针,该指针记录着映射内存的结尾,移动该指针就会映射,取消映射。映射的内存属于堆内存
- mmap/munmap底层不维护任何东西,如果mmap映射成功返回映射后的内存首地址,映射的内存属于堆内存
- malloc/free底层调用了sbrk/brk/mmap/munmap
- 每个进程都有4G的虚拟地址空间,虚拟地址只是虚假的地址编号,并不是真实物理内存编号,虚拟地址必须必须和物理内存进行映射后才能使用
- 平时所说的堆内存的分配和释放有两层含义:
- 重点是理解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>
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转换成字符串,然后再通过
文件描述符:
- 非负整数,代表打开的文件
- 通过系统调用open、create返回,可以被内核引用
- 它代表了一个内核对象,因为内核不能暴露它的内存地址,不能返回真正的内核对象地址,只能用文件描述符来对应
- 内核中有一张打开文件的表格,文件描述符是访问这张表的下标,因此也称**“句柄”**,相当于访问已打开文件的凭证
内核中有默认一直打开的文件描述符
? 0 标准输入文件 scanf底层read 对应的文件指针stdin
? 1 标准输出文件 printf底层write 对应的文件指针stdout
? 2 标准错误文件 对应的文件指针stderr
文件描述符的复制:
int dup(int oldfd);
功能:复制一个已打开的文件描述符
返回值:返回一个当前没有使用过的最小的文件描述符
int dup2(int oldfd,int newfd);
功能:复制一个指定的文件描述符
返回值:成功返回newfd,失败返回-1
文件同步问题
- 在写入数据时,内存到磁盘之间有一块缓冲区,这种磁盘降低了磁盘读写次数,提高了读写效率
- 但是这种机制带来的后果是磁盘的数据与实际写入的数据不匹配,系统提供了三个函数可以让缓冲区的数据立即写入到磁盘
#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);
功能:根据路径修改文件权限
权限屏蔽码
#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信息,文件名
- 区别:
- 删除源文件,只是删除了源文件的iNode信息,硬链接不受影响,而软链接无法访问
- 对于一个文件而言,硬链接数删除为0时,文件才被真正的删除。
- 当修改硬链接文件的内容,源文件也会被修改
- 硬链接不能链接目标,软链接可以
- 硬链接不能跨文件系统,软链接可以
#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;
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信号处理
-
基本概念
-
中断: 当操作系统接收到消息后中止当前正在执行的程序,转而执行其它任务,等待它任务执行完成后再返回,这种执行模式叫做中断 分为硬件中断和软件中断 -
信号: 是一种软件中断,由操作系统发出,程序接收后会执行相应的操作 -
常见信号
命令 kill -l 查看所有信号
SIGINT(2) Ctrl+C 终止
SIGQUIT(3) Ctrl+\ 终止+core
SIGFPE(8) 除零 终止+core
SIGKILL(9) 终止信号 终止(不能被捕获,忽略)
SIGSEGV(11) 非法访问内存 终止+core
-
不可靠信号和可靠信号 建立在早期的信号处理机制上(1~31)的信号是不可靠信号(不支持排队,可能会丢失,如果同一个信号连续产生多次,进程可能只相应了一次) 建立在新的信号处理机制上(34~64)的信号是可靠信号(支持排队,信号不会丢失) -
信号的来源 硬件异常:除零,非法访问内存,未定义的指令,总线错误 软件异常:通过一些命令,函数产生的信号 -
信号的处理方式
- 忽略
- 终止进程
- 终止进程并产生core文件(记录内存映像)
- 停止
-
继续 -
捕获并处理(在信号发生前,先向内核注册一个函数。当信号来临时系统会自动执行该函数) -
信号捕获 typedef void (*sighandler_t)(int);
功能:说明信号处理函数的格式
sighandler_t signal(int signum, isghandler_t handler);
功能:向内核注册一个信号处理函数
signum:信号编号
handler:函数指针
SIG_IGN 忽略
SIG_DFL 按默认方式处理
返回值:之前的信号处理方式
- 注意:SIGKILL(9),SIGSTOP(19)不能被捕获和忽略
- 注意:当信号处理完后可能会返回产生信号的代码继续运行,如果我们不捕获并处理段错误,算术异常等信号可能会产生死循环,正确的处理段错误,算数异常信号应该是备份数据并直接结束程序
- 注意:有些系统通过signal注册的信号处理只能执行一次,如果想要持续有效,可以在信号处理函数中再从新注册一次
- 注意:子进程会继承父进程的信号处理方式,但是通过exec系类函数创建子进程,会恢复默认
信号的方式方式
-
键盘 Ctrl + c
Ctrl + \
Ctrl + z 暂停、挂起 fg 继续
-
错误 除0
非法访问内存
总线错误
-
命令 kill -信号编号 进程号
功能:向指定进程发信号
killall -信号编号 进程名
功能:给同名的进程发送同一个信号
-
函数 #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;
int si_errno;
int si_code;
int si_trapno;
pid_t si_pid;
uid_t si_uid;
int si_status;
clock_t si_utime;
clock_t si_stime;
sigval_t si_value;
int si_int;
void *si_ptr;
int si_overrun;
int si_timerid;
void *si_addr;
long si_band;
int si_fd;
short si_addr_lsb;
void *si_call_addr;
int si_syscall;
unsigned int si_arch;
}
#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 启动进程的命令
-
父进程,子进程,孤儿进程,僵尸进程
-
进程表示符 每一个进程都有一个非负整数表示的唯一标识,即进程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, ... );
path:可执行文件的路径
arg:命令行参数,个数不定,由实际的可执行文件所需命令
命令行参数:一般第一个可执行文件的名字,至少有一个,一定要以NULL结尾
int execlp(const char *file, const char *arg, ...);
file:可执行文件名,去系统默认路径查找file
arg:命令行参数,个数不定,由实际的可执行文件所需命令
命令行参数:一般第一个可执行文件的名字,至少有一个,一定要以NULL结尾
int execle(const char *path, const char *arg, ...);
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 ...);
path:可执行文件的路径
arg:命令行参数,个数不定,由实际的可执行文件所需命令
命令行参数:一般第一个可执行文件的名字,至少有一个,一定要以NULL结尾
envp:环境变量表,父进程可以在加载子进程时把环境变量边传递给子进程
> exec函数正常情况下不会返回,当子进程加载失败才会返回-1.
> 虽然通过vfork,exec系列函数创建的子函数不会继承父进程的信号处理函数,但是能继承父进程的信号屏蔽集
- 通过fork创建的子进程会拷贝父进程(数据段,bss段,堆,栈,I/O缓冲区),子进程与父进程共享代码段,子进程会继承父进程的信号处理方式
- fork函数调用后父子进程各自独立运行,谁先返回不确定。但是可以通过睡眠确认让哪个进程先执行
- 通过fork创建的子进程可以共享父进程的文件描述符
- 可以根据返回值的不同让父子进程进入不同的分支,执行不同的代码
练习:为进程创建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查看)
进程结束
- 向父进程发送信号SIGCHLD
- 该函数也不会返回
- 进程的最后一个线程执行了return语句
- 进程的最后一个线程执行了pthread_exit函数
进程的异常终止
- 进程调用了about函数,产生SIGABRT(6)信号
- 进程接收到某种信号,可以是其它进程发送的,也可以自己的错误导致的
- 进程的最后一个线程接收到了“取消”请求操作,并响应
这三种方式结束进程,它的父进程都无法获取结束状态码因此加做异常终止
注意:无论进程是如何结束的,它们最后都会执行同一段代码,会帮你关闭所有打开的文件,并释放所有的内存
子进程的回收
-
对于子进程的结束而言,都希望父进程都能够知道并做出一定的反应,通过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(){
}
进程间通信
一,基本概念
? 什么是进程间通信:是指两个或多个进程之间交互数据的过程,因为进程之间是相互独立的,为了进程之间协同工作就必须实现进程间交互数据
二,进程间通信的分类
? 简单的进程间通信:信号,普通文件,环境变量表,命令行参数
? 传统的进程间通信:管道文件
? 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 以只读方式映射共享内存
返回值:与共享内存映射成功后的虚拟内存首地址,失败返回
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
创建消息队列 获取消息队列
发送消息 获取消息
删除消息队列
信号量
- 基本特点:由内核管理的一个“全局变量”,用于记录共享资源的数量,限制进程对共享资源的访问使用
- 信号量是一种数据操作锁,本身不具备数据交互功能,而是通过控制其它的信号资源从而配置实现进程间通信
- 如果信号量的值大于0,说明可以使用资源,使用时需要想好量-1,然后再使用
- 如果信号量的值等于0,说明没有资源可以使用,此时进程进入休眠,知道信号量的值大于0,进程会被唤醒,执行步骤1
- 当资源使用完毕,把信号量的值+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;
short sem_flg;
}
项目-基于进程间通信技术的银行系统
主要分为两个大模块(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>
#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;
char sun_path[UNIX_PATH_MAX];
};
struct sockaddr_in {
__kernel_sa_family_t sin_family;
__be16 sin_port;
struct in_addr sin_addr;
};
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;
__be16 sin_port;
struct in_addr sin_addr;
};
#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字节的网络字节序转本地字节序
#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
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
- gcc -fpic -c network.c
- gcc -shared -fpic network.o -o libnw.so
- sudo cp libnw.so /usr/lib
- sudo cp network.h /usr/include
使用:共享库
- 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 一直阻塞,直到某个文件描述符发生了变化
0秒0微秒 不阻塞
大于0秒 等待超时时间,超时返回0
struct timeval {
time_t tv_sec;
suseconds_t tv_usec;
};
返回值:监控到发生相关操作的文件描述符的个数;超时返回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设计不合理的地方
- 每次调用select都需要向它重新传递被监控的文件描述符集合
- 调用结束后如果想知道具体是哪个文件描述符发生了相关操作,必须对所有被监控点文件描述符进行一遍测试
- 优点:它是最早的多路复用函数,几乎所有的操作系统都支持,兼容性很好
- 缺点:
#include <signal.h>
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fds: struct pollfd 结构变量指针
struct pollfd {
int fd; 表示一个被监控的文件描述符
short events; 想要监控的事件
short revents; 实际监控到的事件
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_data_t data;
};
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”
客户端断点续传过程:
-
获取服务器,客户端同名文件大小 server_size -
获取服务器,客户端同名文件的时间戳 -
判断客户端同名文件与服务器同名文件的时间戳一致,并且大小不一样,则该文件需要续传 -
当3个条件都满足,开始断点续传 ? 客户端文件从server_size处开始读 ? 向服务器发送 REST server_size 处开始续传 ? STOR 开始上传文件
线程管理
- 基本概念:
- 线程是进程的执行线路,它是进程内部的控制序列,或者说线索是进程的一部分(进程是一个资源单位,线程是执行单位,线程是进程的一部分,负责真正的执行)。
- 线程是轻量级的,没有自己独立的代码段,数据段,堆,环境变量,命令行参数,文件描述符,信号处理函数,当前信号集等资源。
- 线程有自己独立的栈内存,线程ID,错误码,信号掩码。
- 一个进程中可以有多个线程(多个执行路线),但是至少有一个线程在活动,该线程称为主线程。
- 线程是进程的实体,可以当做系统独立的认为调度和分配的基本单位
- 线程有不同的状态和属性,系统提供了线程的控制接口,例如:创建,销毁,控制
- 进程中的所有线程同在一个虚拟地址空间中,进程中的所有资源对线程而言都是共享的。因此当多个线程协同工作时,需要解决资源竞争问题(加锁)
- 线程的系统开销小,任务切换快;多个线程之间不需要数据交换,因此不需要类似于XSI的通讯机制;因此使用线程简单而高效
- 线程之间有优先级的差异
ps -T -p <pid> : 查看pid进程中线程情况 或者htop命令也可以查看
-
POSIX线程
-
早期的UNIX和Linux系统没有线程概念,微软的Windows系统首先使用线程,之后UNIX和Linux系统也逐渐增加了线程 -
早期各个厂商有自己都私有的线程库,而且接口的实现差异较大,不利于移植。世界标准化组织于1995年,指定了统一的线程接口标准规范——遵循该标准的线程称为POSIX线程,简称 pthread -
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; 建议不要初始化
线程的执行轨迹
- 同步方式:默认(可结合状态)
- 创建子线程后,主线程通过pthread_join函数等待子线程终止,并释放线程资源
- 异步方式:(分离状态)
- 无需创建者等待(如果创建者调用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一旦进入等待,分离也不在会退出等待
线程的结束
- 线程执行到入口函数的最后一行代码(包括return语句)
- 线程执行了pthread_exit函数
void pthread_exit(void *retval);
功能:结束当前线程
retval:等同于return后面的val
注意:从表面上看当主线程结束后,子线程会跟着一起结束,就误以为主线程的结束影响了子线程的结束,但是实际上子线程之所有结束是因为主线程执行main函数中隐藏的return语句,导致整个进程结束,所有进程中的线程才会随之结束。
如果主线程调用pthread_exit自杀,这样就没有线程去执行main函数的return语句,进程就不会提前结束,子线程就不受影响
总结:主线程结束不会影响子线程的执行
- 如果所在的进程结束,所有的线程都随之结束
- 向指定线程发送取消请求
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),并通知所有消费者全部醒来消费
进程与线程相关的问题
- 进程与线程的区别?
- 进程在处理多任务时,需要解决什么问题?
- 线程在处理多任务时,需要解决什么问题?
- TCP服务端的编程模型有哪几种,以及它们的优缺点?
- 随着客户端的连接和退出越来越频繁,服务端都需要创建,销毁线程,该过程会比较耗时,如何决绝?
Windows编程准备工作:
- 解压编译器mingw到C盘
- 复制: C:\mingw\bin 路径
- 右点击此电脑→属性→高级系统设置→高级→环境变量→Path→编辑→新建→粘贴路径→确定
- Windows+R→输入cmd→输入gcc -v 测试编译器是否添加成功
- 下载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网络编程区别
- 额外加库 -lws2_32
- 头文件不同 <windsock2.h>
- 先初始化网络库 固定的
- 使用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;
}
- 文件的扩展名由“.c” 变为 .cpp .cc .C .cxx
- 编译器由gcc变成g++,但gcc也可以继续编译C++文件,需要增加参数 -xC++ -lstdc++
- C++的标准库头文件不带.h iostream 意味着 in && out
stream,在C++中输入,输出被封装流操作,C语言中头文件还可以继续使用,但是建议在C标准头文件名前加c并去掉.h,因为这样的头文件中删除了大量不使用的宏,并重新添加到名字空间中,防止在C++中命名冲突 - 输入输出:
cout 用于输出 cin 用于输入 不需要使用占位符,会自动识别数据占位符 cout/cin 是C++标准库中类对象 endl 负责换行 - 增加了名字空间机制,是C++中为了解决命名冲突而发明的一项机制
C++与C的不同点
C++的堆内存管理
- C++中有专门管理堆内存的语句(new 分配堆内存 | delete 释放堆内存),而C语言只能使用标准库中的函数malloc / free
- new在分配内存时,可以直接初始化
常用格式:类型* p = new 类型;(未初始化) 常用格式:类型* p = new 类型( 初始化的值 );(初始化) - delete p; 释放内存
- 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并不会调用构造,析构函数
}
-
数组分配内存和释放 int* arr = new int[/*数量*/]; // 自动调用构造函数
delete[/*数量*/] arr; // 自动调用析构函数
注意:new\delete malloc\free new[]\delete[] 必须不能混用
使用new【】为结构体,联合,类申请到内存前4个字节记录了申请的次数,这样的目的是为了让编译器在delete【】中知道应该调用多少次析构函数
-
重复释放 delete和free一样,都可以释放空指针,但都不能重复释放相同内存 -
内存分配失败 malloc分配失败则返回NULL new分配失败则会抛出一个异常 std: :bad_array_new_length -
new和malloc返回值不同 malloc成功返回void* new成功返回有类型的指针
重点:malloc、free 和 new、delete的区别
- 身份: 函数 运算符、关键字
- 返回值: void* 带类型的指针
- 参数: 字节个数(手动计算) 类型 ,自动计算
- 处理数量: 手动计算数组总字节数 new 类型【数量】
- 扩容: realloc重新申请 不好直接处理
- 失败: 返回NULL 抛异常并结束
- 构造、解析: 不调用 自动调用
- 初始化: 不能初始化 new 类型(value)
- 头文件: stdlib.h 直接使用
- 重载: 不允许重载 允许重载
- 分配内存: 堆内存 堆内存 、 自由存储区
注意:自由存储区只是一个抽象概念,由于new底层默认调用malloc,此时分配的是堆内存,但是new可以当做运算符被程序员重载的方式 或者 new(地址)类型 内存时,可能会被分配到其它任意的区域
笔试题:现有一块已经分配好的内存(栈,堆),如何让新申请结构变量、类对象使用这块指定的内存
函数重载
- 什么是函数重载
在同一个作用域下,函数名相同,参数列表不同的函数构成重载关系
- 参数列表:参数个数 参数的类型
- 函数重载与返回值类型,参数无关
- C++是如何实现函数重载?
通过g++ -S filename.cpp 生成汇编代码得知,编译器会把函数的参数类型缩写后追加到函数名后面,也就是说编译时会给函数进行换名 - extern “C”
因为C++编译器中再编译函数调用语句时,会找换名后的函数调用,这样就无法调用到已经使用C编译器成功的函数了 使用 extern “C” 会让C++编译器按照C编译器的格式来翻译函数名,这样函数的声明与定义就匹配,就可以正确地调用C标准库,系统库函数 - 重载和隐藏
在同一作用域下同名不同参的函数构造成重载关系 在不同作用域(父子类)下的同名函数遵循名字隐藏原则 - 参数类型转换
当调用函数时,编译请器会优先调用类型最精确的类型,如果没有则会做一定程度的类型提升或者标准装换,而不是全部直接报错。但具有优先级与编译器有关,因此优先选择最精确的参数即可,否则容易产生函数调用的二义性
默认形参
- 什么是默认形参
C++中可以给函数的参数设置默认值,当函数调用者提供了实参则使用实参,如果没有提供则使用默认值 - 默认形参要靠右
如果有函数多个默认形参,必须遵循从右往左依次设置,否则有二义性 - 只能在函数声明处世子默认形参
如果函数声明与定义分开实现,只能在话术声明时设置默认形参,否则语法错误 - 默认形参可能会影响函数重载的效果
如果为重载过的函数设置默认形参是一定要小心
内联函数
-
什么是内联函数
- 普通函数会翻译成二进制指令存储在代码段中,调用语句会生成一句跳转指令,并让程序跳转到该函数所在代码段出运行,运行结束在返回
- 内联函数也会被翻译成二进制指令,调用语句不会生成跳转指令,而是字节把函数的二进制指令替换成调用语句,这样既没有跳转也没有返回,而是直接往下执行被调用函数,这种函数成为内联函数
- 显示内联和隐式内联
显示内联:在函数的返回值前加 inline ,则该函数就以内联机制调用,但并不是所有编译器都支持内联机制,我们现在的 g++ gcc 都不支持 隐式内联:结构,联合,类中的成员函数会自动被当做内联函数处理 -
内联函数的使用条件 内联的优点:节约了函数的跳转和返回的时间。提高了代码的运行速度。 内联的缺点:多次调用内联函数时,它的二进制代码会被拷贝多份,产生冗余,导致最终的可执行文件增大 使用的条件:
- 适合一次调用多次执行的情况,不适合内容多且调用少的函数,因此这样节约的时间还弥补不了牺牲的内存 2. 带有递归属性的函数无法内联,编译器会自动屏蔽inline关键字
内联函数(inline)与宏函数(#define)的相同点和不同点
引用
强制类型转换
C语言中强制类型转换还能在C++中继续使用,因此C++中新的强制类型转换有点鸡肋
为什么C++要重新设计新的强制类型转换?
为什么C++的强制类型转换设计的很复杂,使用很麻烦?
- 因为C++之父认为只有在程序设计不合理情况下,才需要进行强制类型转换,之所以设计复杂就是不想让程序员使用,从而反思,重新设计自己的代码
static_cast<目标类型名>(原数据):静态类型强制转换
dynamic_cast<目标类型名>(原数据):动态类型强制转换
目标类型与原数据类型之间必须存在继承关系
const_cast<目标类型名>(原数据):去常类型转换
目标类型和原类型必须是指针或者引用,并且除了是否带const属性之外,其它类型都必须相同,否则出错
reinterpret_cast<目标类型名>(原数据):重解释类型转换
只能是把整数数据转换成指针,或者把指针转换成整数,否则出错
// 要求目标类型数据与原数据之间必须有一个方向可以自动类型转换
面向对象
类和对象
什么是类和对象?
-
类是由程序员自己设计的一种数据类型,它里面包含了成员变量和成员函数两部分;而对象是类的实例化,其实可以理解为使用类创建的变量 -
类是设计和实例化: class 类型
{
成员变量; // 类中成员默认属性是private私有
public:
返回值 函数名(参数列表);
}; // 分号不能少
/*
实例化:
方法一: 类名 类对象名;
方法二: 类名* p = new 类型
*/
返回值 类名::函数名(参数列表)
{
// 成员函数内可以直接使用成员变量 不用. →
}
注意:如果类的内容不多,可以考虑在头文件中全部实现出来
访问控制限定符
private:私有的,被它修饰的成员只能在类内访问,是类的默认属性
public:被它修饰的成员可以在任意位置访问,一般把类的成员函数设置为公开的
protected:保护的,被它修饰的成员可以在类内核子类中访问,但是不能再类外访问
构造函数
构造函数是类的同名函数,没有返回值,当实例化对象时,它会自动执行;一般负责对类进行初始化,分配资源
class 类名
{
int* p;
public:
类名(参数)
{
p = new int;
}
}
-
构造函数必须是public否则无法实例化对象 -
构造函数可以重载,可以有多个版本 -
带参数的构造函数的调用方法: 类名 对象名(实例); 类名 对象指针 = new 类名(实参); -
默认情况下,编译器会自动生成一个无参构造函数,该函数什么都不做;一旦显示地实现了构造函数,则编译器不生成构造函数
类名 对象名; // 调用无参构造,如果没有无参构造,则报错
-
也可以使用默认形参实现,构造函数的效果 -
构造函数没有返回值 -
不要使用malloc为类实例化对象分配内存,因为malloc不会调用构造函数
析构函数
析构函数负责对类对象进行收尾工作,例如:释放类内存中资源,保存数据等,当对象实例化;
class 类名{
public:
~类名(){
}
}
- 析构函数权限必须是public
- 无返回值,无参数,不能重载
- 当类对象生命周期完结时 或者 使用delete销毁对象时 会自动调用析构函数
- 如果没有显示的实现析构函数,编译器也会自动生成一个什么都不敢的析构函数
- 构造函数一定会执行,但是析构函数不一定会执行
- 不要使用free来销毁对象,因为它不会使用析构函数
初始化列表
初始哈列表是构造函数的一种特殊语法,只能在构造函数中使用
class 类名
{
成员1
成员2
...
成员n
public:
类名(参数):成员名1(初始化数据),成员名2(初始化数据). . .
{
// 构造函数
}
}
- 类的成员变量在旧版本的标准中不可以设置初始值,而且在构造函数执行之前成员变量都已经定义完毕,因此const属性的成员变量无法在构造函数内正常赋值初始化(新标准中可以直接设置初始值,但是也只能给常量,功能有限)
- 初始化列表 先于 构造函数执行,初始化列表执行时类对象还没有构造完成,因此它能给const属性成员变量赋值的方法
- 当参数名与成员名相同时,初始化列表可以自动分辨成员和参数名
- 当类中有类对象成员时,该成员的有参构造函数也可以在初始化列表中执行
作业:
- 使用C++语法写通讯录
- 使用C++语法实现五子棋
- 使用C++语法实现封装一个network
- struct,class关键字可以省略
- bool类型,可以不同加stdbool.h的头文件。因为,它在C++中是关键字
- 如果自行定义了一个构造函数,则不再自动生成无参构造
- 构造,联合,类:都有 构造函数,解析函数,拷贝函数,赋值函数
- class中的成员默认为private属性,struct中默认public属性
- 构造函数没有返回值,且可以重载
- 析构函数没有返回值,没有参数,不能重载
- 只能在构造函数中进行初始化列表。写在定义处
- 函数还可以设置默认形参,声明可以写,定义不可以写
- 函数可以重载
- 通过域限定符(::)可以指定使用重名的成员函数或则是全局函数
- 通过域限定符(::)可以访问被屏蔽的全局变量
- 类中的成员函数可以直接使用成员变量
- 任意类型的指针可以自动转成void* ,void* ,不能自动转换成其它类型指针,必须使用强制类型转换
对象的创建和销毁过程分析
- 对象创建
- 给对象划分内存空间(栈,堆)
- 执行初始化列表
- 根据继承表顺序来调用父类的无参构造或有参构造
通过:父类类名(val)调用父类有参构造 - 根据成员的定义顺序调用类类型成员的无参构造或者有参构造
通过:成员名(val)调用类类型成员有参构造 初始化普通成员 - 执行构造函数,申请资源
- 对象销毁
- 执行自己的析构函数,释放内存
- 根据成员定义的顺序,逆序调用类类型成员的析构函数
- 根据继承表顺序,逆序调用父类的析构函数
- 释放对象的内存
成员函数是如何区别调用它的对象
- 对象的内存只存储了它的成员变量,没有存储成员函数指针
- 当通过对象调用成员函数时,编译器会自动把对象的地址传递给成员函数,也就是说成员函数中有一个隐藏的参数,叫做this指针参数,来接收对象的地址
- this指针拿到了调用对象的地址,就可以访问该对象的成员,完成了区别对象的工作
- 虽然this指针是隐藏的,但是可以在成员函数内显示地使用它,但是正常情况下不写等于写了
常函数
- 被const修饰过的成员函数,称为常函数
返回值 成员函数名(参数列表)const{} - 已知当对象调用成员函数时,编译器会把对象的地址隐式地传递给成员函数的this指针
- 如果对象被const修饰过,就不能使用普通成员函数,编译会报错,因为此时传递的对象指针带有const属性,而普通的成员函数隐藏的this指针不带const属性,所以把带const属性的指针赋值不带const属性的指针变量,编译器不允许
- 如果成员函数被const修饰,本质上修饰的是隐藏的this指针,这样this就带const属性,也就可以被带const属性的对象调用
- 带const属性的对象只能调用常函数,常函数也只能调用常函数
- 常函数可以重载成不带const函数但其它形参完全相同的成员函数
- 在常函数中不能修饰成员变量的值,除非该成员变量定义时被 mutable 修饰
拷贝构造和赋值操作
拷贝构造是一种特殊的构造函数,格式为
类名(const 类名& that){} /* const不是必须加的 */
- 什么时候会调用拷贝构造:当使用旧对象给新对象进行初始化时,会自动调用拷贝构造。
- 拷贝构造的任务:
顾名思义拷贝构造负责把旧对象中的成员变量拷贝给新对象,且编译器会默认生成具备这样功能的一个隐藏拷贝函数 - 什么时候应该显示地实现拷贝构造:
普通情况下编译器自动生成的拷贝构造完全够用,但是当类中的成员有指针且为指针分配了堆内存,默认的拷贝只会对指针的值进行拷贝,这样就会导致两个对象的指针成员都指向同一块堆内存,在执行析构函数时,造成重复释放堆内存导致内存崩溃,此时必须要显示的实现拷贝构造
浅拷贝和深拷贝:
- 当类中有成员是指针类型且分配了堆内存,浅拷贝只会拷贝指针变量的值,深拷贝不拷贝指针变量的值,而是申请新的内存,拷贝内存中的数据内容
赋值操作函数:
所谓的赋值操作,就是一个对象给另一个对象赋值(这两个对象都已经创建完毕),在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];
}
赋值运算符函数的任务:
- 它与拷贝构造的任务基本一致,默认下编译器也会自动生成具备浅拷贝功能的赋值操作函数,但是当需要进行拷贝时不仅需要显示实现拷贝构造,同时也需要显示的实现赋值运算符函数
- 实现赋值运算符函数需要注意的问题:
- 赋值运算符函数与拷贝构造函数任务虽然接近,但是实现过程有所不同:
- 问题:两个对象的指针都已经分配好内存
- 先释放内赋值者的指针变量所指向的原内存
- 再给赋值者的指针变量重新申请内存
- 把赋值者指针变量所指向内存的内存拷贝给赋值者新申请的内存中
- 问题:有可能对象自己给自己赋值
- 需要立即判断this和赋值者的地址是否相同
- 如果相同立即结果结束,不相同时进行赋值操作
- 问题:允许 n1 = n2 = n3 连续赋值操作
- 因此赋值运算符函数的返回值返回类型 应为对象的引用
作业:实现一个string类,实现四个成员函数
class String {
char* str;
public:
String(const char* str = NULL);
~String(void);
String(const String& that);
String& operator=(const String& that);
}
静态成员
什么是单例模式:只能实例化出一个类对象 什么情况下会使用单例模式:
- 任务管理器、日志管理器
- 网站访问计数器
- 线程池,内存池,数据池
- 服务器的连接管理器
实现单例模式的原理:
-
禁止在类外创建类对象,把 构造函数 和 拷贝函数 私有化 -
确保类对象只有一份,在类中定义一个静态成员指针变量或类对象 -
提供一个获取静态类对象,指针的接口,设计静态成员函数用于获取静态类对象,指针
- 饿汉模式的单例:
- 程序运行时就实例化出类对象,不管后期是否用到都会创建出来
- 优点:不可能被多个线程同时运行时,创建多份(确保线程安全)
- 缺点:如果后期使用不到单例对象对象,浪费了资源
- 懒汉模式的单例:
- 直到真正使用时,才创建单例对象
- 优点:什么时候用什么时候创建,如果用不到就不会创建,节约资源
- 缺点:有可能多个线程同时创建多个单例对象(线程不安全)
实现:饿汉模式的单例,和线程安全的懒汉模式的单例
酒店管理系统项目:
旅客类:身份证号码,姓名,性别。构造函数,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)
友元
在实现类的全局运算符函数时,可能会使用类内的私有成员,此时全局运算符函数是没有访问权限,如果把私有成员变成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++中,前自变的结果是左值,后自变的结果是右值
特殊的运算符
- 下标运算符[]
当想让一个类对象当成数组使用时,可以考虑重载下标运算符,例如vector类中就重载了【】 - 函数运算符()
当想让一个类对象当成函数使用时,可以考虑重载该运算符。
注意下标运算符合函数运算符,不能重载成全局
-
解引用*运算符 和 访问运算符→ 重载这两个运算符时,可以让一个类对象像指针一样使用 C++的智能指针就是重载了它们来实现的 -
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获取类型函数的运算符
- 只能重载成全局函数的运算符
<< 输出运算符 >> 输入运算符 - 只能重载成成员函数的运算符
[] 下表运算符 () 函数运算符 = 赋值操作运算符,类内本身就一定有一个=成员函数 → 间接访问成员的运算符 - 运算符重载可以自定义运算的过程,但是无法改变运算符的优先级
- 运算符的运算对象数量和格式不能改变
- 不能发明新的运算符
建议:
- 重载运算符要遵循运算符含义一致性,不要改变运算符的运算规则
- 不要忘记重载运算符的初衷,不要炫技
继承
-
当遇到问题时,先查看现有的类能否解决部分问题,如果有,则继承该类,在此类的基础上进行扩展来解决问题,以此可以缩短解决问题的时间(代码复用) -
当遇到一个大而复杂的问题时拆分成若干个小问题,然后为每个小问题设计一个类进行解决,最后通过继承的方式把这些类汇总到一个类中从而解决最终的大问题的,以此降低问题的难度,也可以同时让多个程序员一起工作解决问题 ? 子类继承父类 派生类继承基类
虚函数和覆盖
虚函数:当成员函数前加virtual修饰后,这样的函数称为虚函数,该类也会想虚函数一样多路一个虚指针。
虚函数表:虚指针指向一张表格的首地址,该表格中记录的是所有虚函数的首地址
覆盖:当使用了virtual关键字修饰父类的成员函数,此时父类中多了一个虚指针(虚表指针),子类会把父类的虚指针一起继承过来,当子类中有与父类虚函数同名的成员函数时,编译器会比较这两个函数的格式,如果格式完全相同,则会把子类的同名函数地址覆盖掉虚函数表中父类的同名虚函数的原地址 此时,使用父类的指针或引用指向子类对象时,调用虚函数则会调用被覆盖后的虚函数表中所指向的子类的同名且格式相同的成员函数
((void(*)(void))(*(int*)*(int*)b))();//相当于调用了虚函数表中第一个函数
( ( void(*)(void) ) (**(int**)b) )();// 调用虚函数表中的第一个函数
构成覆盖的条件:
- 必须是发生咋父子类,且一定为public继承
- 要被覆盖的父类函数必须为虚函数(virtual修饰)
- 子类中必须有同名且格式(返回值,参数列表,常属性)完全一致的虚函数,才能构成覆盖
- 覆盖要求,返回值类型相同;或者,子类函数的返回值可以向父类虚函数的返回值类型做隐式转换且有继承关系时,可以构成覆盖
面试题:重载,覆盖,隐藏,重写的区别 隐藏:
- 如果同名但格式不同,无论是否加virtual都是隐藏,在子类中都会隐藏父类同名成员函数
- 子类中如果同名且格式相同,不加virtual,子类也会隐藏父类的同名成员函数
总结:同名成员函数,要么构成隐藏,要么构成覆盖
多态
什么是多态:是指同一个事物,指令可以有多种形态,当调用同一个指令时,根据参数,环境的不同会做出不同的响应操作,这种模式称为多态!
根据确定执行操作的时间,多态分为:编译时多态,运行时多态。
编译时多态:
- 当调用重载过的函数时,编译器会根据参数的不同,在编译时就能确定哪个版本的重载函数,这种叫做编译时多态,还有模板技术
运行时多态:
- 在父子类中,当子类覆盖了父类的同名的虚函数,然后用父类指针或引用访问虚函数,它即可能调用父类的虚函数,也可能调用子类的同名函数,具体调用哪个取决于该父类指针或引用指向目标是谁,而这个目标的确定需要运行时才能最终决定下来,这种情况叫做运行时多态
- 构成运行时多态的条件
- 父子类之间且有构造覆盖关系的同名函数
- 子类是以public继承父类(让父类指针,引用指向子类对象)
- 通过父类指针或引用 访问被覆盖的虚函
虚构造和虚析构
虚构造:构造函数不能设置为虚构造,假如构造函数可以是虚函数。那么,子类构造函数会自动覆盖父类的构造函数,当创建类对象时,子类执行自己的构造函数之前,会自动调用父类的构造函数,但是此时父类的构造函数已经被覆盖成了子类的构造函数,就会在此调用子类的构造函数,这样就形成了死循环,因此编译器不允许把构造声明为虚函数
虚析构:析构函数可以设置为虚函数,当使用类多态时,通过父类指针或引用释放子类对象时,默认情况下不加虚析构是不会调用子类的析构函数,如果子类析构函数中有要释放子类对象时,会先调用覆盖后子类析构函数,而且之后,还会调用父类的析构函数,这样就不会由内存泄漏了
总结:当使用多态且子类的构造函数中有申请内存,此时父类的析构函数一定要设置为虚析构
纯虚函数和纯抽象类
纯虚函数的格式:
virtual 返回值 成员函数名(参数列表) = 0;
- 纯虚函数可以只声明不实现(一般也没必要去实现它)
- 父类中如果没有纯虚函数,子类就必须覆盖父类的纯虚函数,否则无法创建对象
- 有纯虚函数的类是无法创建对象的,因为这样的话,纯虚函数没有被覆盖
- 纯虚函数的目的就是为了强制子类去覆盖父类的纯虚函数,强制子类实现某些功能
- 有纯虚函数的类称为抽象类
- 析构函数可以被定义为纯虚函数,但是需要在类外实现它
纯抽象类:所有成员都是纯虚函数的类,叫做纯抽象类。这种类一般用于设置功能的接口,也被称为接口类
工厂模式
C++文件操作
练习:设计一个学生类,并创建学生对象,然后把它的内容以文本形式写入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++的二进制读写
-
创建流对象,打开文件
- 使用有参构造创建并打开文件
- 使用无参构造创建对象,通过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中,实现的小工具:
- ls -al
- cp
- mv
- ping 命令
- rm -rf
- 类型信息识别(头文件:)
C++中使用typeid运算符可以获取数据的类型
- 返回值是一个类 类型对象
- 该类型中有一个name的成员函数,可以获取数据的类型并转换为字符串
- 该类重载了 == 运算符 可以判断两个数据是否是同一个类型
- 它可以区别父类指针或引用指向那个子类,但是前提是父子类之间构成多态
模板技术
模板:是一种自动生成代码的技术,这种技术能让程序员在编写代码时不需要考虑数据类型,这种技术也被称为泛型编程技术
为什么需要使用模板?
- 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() {
}
- 可以使用void*+回调函数的方式实现通用代码,但是实现难度高,使用比较麻烦
- 借助宏函数实现部分通用代码,类型检查不严格,没有返回值,容易有二义性
- C++借助函数重载也可以实现通用代码,增加代码段的内容,未知类型需要重载新版本
- 综合以上因素,C++之父在C++中实现了模板技术,目的是为了让C++的编程彻底摆脱数据类型的困扰
函数模板
-
函数模板的格式 template<typename T1,typename T2...>
T3 函数名(T1 arg1,T2 arg2) {
T3 temp = arg1 + arg2;
return temp;
}
// 注意:此两行要紧靠着。可以给未知类型取任何名字,一般约定俗称写T
// 一个template模板声明只能修饰一个紧跟着的函数
-
函数模板的原理
- 函数模板会经历两次编译
-
检查函数模板的语法是否有错误,但不会生成函数的二进制指令 -
根据调用者提供的参数类型来在次检查代码,如果没有错误才会生成二进制指令并存放到代码段,如果没有一次调用则不会生成二进制指令 这种方式称为函数模板的惰性实例化准则 -
函数模板的调用 // 可以使用默认类型参数
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···)
- 特化前,必须有一个基础的函数模板
- 一定要加template<>,否则就会变成了普通函数
- 编译器有优先调用特化版本,不会普通函数模板冲突
- 当同时存在类型相同的普通函数,特化模板,普通模板时,且会优先调用普通模板函数
- 普通函数无论是否被调用都会生成二进制指令,但是模板特化依然是遵循“惰性实例化”准则
- 特化函数的形参和返回值基础类型,要与模板函数基础类型一致,否则报错!
例如:函数模板中使用引用,特化也要加引用。也就是除了特化类型不同,其它要一致
作业:实现二分查找,冒泡,选择,插入,快排,归并,堆等算法的函数模板
类模板
类模板:是一种使用未知类型来设计类的技术
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 该异常 -
自定义异常类 -
使用异常需要注意的问题
- 不要返回局部变量,对象的地址,但可以返回临时对象
- 建议都用引用的方式进行捕获异常,否则会构造,拷贝两次
- 不要再构造函数,析构函数抛异常
- 如果存在父类类异常,先捕获子类类型,在捕获父类类型
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++中用处不大,但是在嵌入式开发有很大用处
|