C语言程序设计

计算机语言

计算机语言(Computer Language)指用于人与计算机之间通讯的语言。计算机语言是人与计算机之间传递信息的媒介。计算机系统最大特征是指令通过一种语言传达给机器。为了使电子计算机进行各种工作,就需要有一套用以编写计算机程序的数字、字符和语法规划,由这些字符语法规则组成计算机各种指令(或各种语句)。这些就是计算机能接受的语言。

解释型vs编译型

解释型语言:解释性语言编写的程序不进行预先编译,以文本方式存储程序代码。执行时才翻译执行。程序每执行一次就要翻译一遍。
优缺点:跨平台能力强,易于调,执行速度慢。

编译型语言:编译型语言在执行之前要先经过编译过程,编译成为一个可执行的机器语言的文件,比如exe。因为翻译只做一遍,以后都不需要翻译,所以执行效率高。
编译型语言的优缺点:执行效率高,缺点是跨平台能力弱,不便调试。

C语言之Hello

1
2
3
4
5
6
7
8
9
#include<stdio.h> //头文件

//入口函数
int main()
{
printf("Hello World!\n"); //打印输出
}
//注释

C语言基础

关键字

auto break case char
const continue default do
double else enum extern
float for goto if
int long register retrun
short signed sizeof static
struct switch typedef union
unsigned void volatile while

数据类型

  • 基本类型
    • 数值类型
      • 整型
        • 短整型 short
        • 整型 int
        • 长整型 long
      • 浮点型
        • 单精度型 float
        • 双精度型 double
    • 字符类型 char
  • 构造类型
    • 数组
    • 结构体 struct
    • 共用体 union
    • 枚举类型 enum
  • 指针类型
  • 空类型void

整型数据

1
2
3
4
5
6
7
8
9
10
11
#include<stdio.h> // 注释,引入头文件

//入口
int main() {
//常量:在程序运行过程中,其值不可以改变的量
//整型常量 10 -10
//变量: 其值可以改变的量被称为变量
int age = 100;
printf("年龄是%d", age);
}

数据类型 关键字 空间大小 数据范围
短整型 short 2字节 [2^-15,2^15-1]
无符号短整型 unsigned short 2字节 [0,2^15-1]
整型 int 4字节 [2^-31,2^31-1]
无符号整型 unsigned int 4字节 [0,2^31-1]
长整型 long windows:4字节 [2^-31,2^31-1]
无符号长整型 unsigned long windows:4字节 [0,2^31-1]

浮点型数据

1
2
3
4
5
6
7
8
9
10
11
12
#include<stdio.h> // 注释,引入头文件

//入口
int main() {
//常量:在程序运行过程中,其值不可以改变的量
//实型(浮点型)常量 10.1 -10.1

//变量: 其值可以改变的量被称为变量
float price = 100.0f;
printf("价格是%f", price);
}

数据类型 关键字 字节 数值范围
单精度 float 4字节 3.4E-38 ~ 3.4E+38
双精度 double 8字节 1.7E-308 ~ 1.7E+308

字符型数据

1
2
3
4
5
6
7
8
9
10
11
#include<stdio.h> // 注释,引入头文件

//入口
int main() {
//常量:在程序运行过程中,其值不可以改变的量
//字符型常量 'k'
//字符串 "kerwin"
//变量: 其值可以改变的量被称为变量
char alias = 'K';
printf("年龄是%c", alias);
}
数据类型 关键字 空间大小
字符型 signed char 1字节 [-128,127]
无符号字符型 unsigned char 1字节 [0,255]

运算符

算术运算符

对数字类型(整型,浮点型,字符型)的数据进行运算

运算符 含义 示例
+ 对两个数字进行相加的计算 10 + 3 = 13
- 对两个数字进行相减的计算 10 - 3 = 7
* 对两个数字进行相乘的计算 10 * 3 = 30
/ 对两个数字进行相除的计算 10 / 3 = 3
% 对两个数字进行求模的计算(求余数) 10 % 3 = 1
++x 前自增:x先进行+1,再进行运算 y = ++x;x = x+1,y = x + 1
x++ 后自增:再进行运算,x先进行+1 y = x++;y = x,x = x + 1
–x 前自减:x先进行-1,再进行运算 y = –x;x = x-1,y = x - 1
x– 后自增:再进行运算,x先进行-1 y = x–;y = x,x = x - 1

注意事项:

  1. 整型与整型计算的结果,还是一个整型,所以如果10/3,得到的结果是浮点型3.33333,此时系统会将这个数字强制类型转换成整型的结果,舍去小数点后面的所有数字,只保留整数部分3
  2. 在进行计算的时候,结果会进行类型提升,将结果提升为取值氛围大的数据类型
    • int 与 int 的计算结果是 int
    • int 与 long 的计算结果是 long
    • float 与 long 的计算结果是 float
    • float 与 double 的计算结果是 double
1
2
3
4
5
6
7
8
9
int main(){
int y = 0,x = 0;
y = x++;
std::cout << "y = " << y << ";x = " << x << std::endl; // y = 0;x = 1

int y = 0,x = 0;
y = ++x;
std::cout << "y = " << y << ";x = " << x << std::endl; // y = 1;x = 1
}

赋值运算符

将等号=右边的值赋给左边的变量
下面表格的前提:int num = 10;

运算符 示例 运算结果
= num = 10 (num = 10) => 10
+= num += 10 num = (int)(num +10)
-= num -= 10 num = (int)(num -10)
*= num *= 10 num = (int)(num *10)
/= num /= 10 num = (int)(num /10)
%= num %= 10 num = (int)(num %10)

关系运算符

对两个变量进行大小关系的比较,最后比较的结果一定是布尔类型的

运算符 示例 运算结果
< 10 < 20 true
> 10 > 20 false
<= 10 <= 20 true
>= 10 >= 20 false
== 10 == 20 false
!= 10 != 20 true

逻辑运算符

对两个布尔类型的变量进行的逻辑操作

运算符 描述 示例
& 与运算,两边为真即为真,任意一个为假,结果即为假 true & true => true
| 或运算,两边为假即为假,任意一个为真,结果即为真 true | false => true
! 非运算,非真即假,非假及真 !true => false
^ 异或运算,相同为假,不同为真 true ^ true => false
&& 短路与,左边的结果为假,右边的表达式不参与运算 false && true => true
|| 短路或,左边的结果为真,右边的表达式不参与运算 true || false => true

位运算符

作用于两个整数数字的运算,将参与运算的每一个数字计算出补码,对补码中的每一位进行类似于逻辑运算的操作,1相当于True,0相当于False

  • 原码:十进制数据的二进制表现形式,最左边为符号位,0为正,1为负
  • 反码:正数的反码是本身,负数的反码在原码的基础上,符号位不变。数值取反,0变1,1变0
  • 补码:正数的补码是本身,负数的补码在反码的基础上+1
运算符 描述 示例
& 位与运算 10 & 20
| 位或运算 10 & 20
^ 位异或运算 10 & 20
~ 按位取反运算 ~10
<< 位左移运算 10 << 1 => 10 * 2
>> 位右移运算 10 >> 1 => 10 / 2

三目运算符

语法:condition(条件) value1 : value2
condition:是一个bool类型的变量或者bool类型运算结果的表达式
运算逻辑:如果condition的值是true,三目运算符的结果取value1,否则取value2

1
2
3
4
int main(){
int age = 22;
std::cout << (age >= 18 ? "已成年" : "未成年") << std::endl; // 已成年
}

运算符的优先级

运算符优先级

  • 在表达式中按照优先级先后进行运算,优先级高的先于优先级低的先运算。
  • 优先级一样的按结合性来运算

运算符结合性

左结合性:从左向右运算

1
sum = x + y + z;

右结合性:从右向左运算

1
2
3
int a,b,c;
int d = 100;
a = b = c = d += 100;
优先级别 运算符 运算形式 结合方向 名称或含义
1 () [] . -> (e) a[e] x.y p->x 自左至右 圆括号 数组下标 成员运算符 用指针访问成员的指向运算符
2 -+ ++ – ! ~ (t) * & sizeof -e ++x或x++ !e ~e (t)e ¥p &.x sizeof(t) 自右至左 负号和正号 自增运算和自减运算 逻辑非 按位取反 类型转换 指针运算,由地址求内容 求变量的地址 求某类型变量的长度
3 * / % e1 * e2 自左至右 乘、除和求余
4 + - e1 + e2 自左至右 加和减
5 << >> e1 << e2 自左至右 左移和右移
6 < <= > >= e1 < e2 自左至右 关系运算(比较)
7 == != e1 == e2 自左至右 等于和不等于比较
8 & e1 & e2 自左至右 按位与
9 ^ e1 ^ e2 自左至右 按位异或
10 | e1 | e2 自左至右 按位或
11 && e1 && e2 自左至右 逻辑与(并且)
12 || e1 || e2 自左至右 逻辑或(或者)
13 ? : e1 ? e2 : e3 自右至左 条件运算 赋值运算
14 = += -= *= /= %= >>= <<== &= ^= |= x=e x+=e 自右至左
15 , e1,e2 自左至右 顺序求值运算

流程控制

顺序结构

代码从上往下,依次执行

1
2
3
4
5
6
7
int main(){
printf("%d",10);
printf("%d",20);
printf("%d",30);
printf("%d",40);
// 输出: 10 20 30 40
}

分支结构

程序在某一个节点遇到了多种执行的可能性,根据条件,选择一个分支继续执行

if-else

if else 语句:可用于变量的区间范围进行判断,根据结果选择分支继续执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if(条件判断1 true or false){
// 条件判断1结果为true,执行该括号内的代码
} else if(条件判断1 true or false) {
// 条件判断1结果为false,条件判断2结果为true,执行该括号内的代码
} else {
// 条件判断2结果为false,执行该括号内的代码
}....

// 如果if 或 else if 或 else 后面的括号中有且只有一条语句,此时括号可以省略
if(条件判断1 true or false)
// 条件判断1结果为true,执行该条语句
else if(条件判断1 true or false)
// 条件判断1结果为false,条件判断2结果为true,执行该括号内的代码
else
// 条件判断2结果为false,执行该括号内的代码
....
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int main(){
int age = 22;
if( age >= 16 && age <= 18 ){
printf("青少年");
} else if( age >18 ){
printf("成年人");
} else {
printf("年龄错误!!");
}
}
----------------------------------------------
int main(){
int age = 22;
if( age >= 16 && age <= 18 )
printf("青少年");
if( age >18 )
printf("成年人");
else
printf("年龄错误!!");
}

switch-case

switch case语句:用于多重分支且条件判断是等值(固定特定值)判断的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
int main(){
switch(variable){
case const1:
// variable == const1,执行该代码段
break;
case const1:
// variable == const2,执行该代码段
break;
default:
// variable 没有匹配的值,则执行该代码段
break;
}
}
  • variable:确定的字符整数
  • case:值只能是字符整数的字面量,不能是变量,值不允许重复
  • break:表示跳出/结束,结束switch语句
  • default:所有情况都不匹配,执行该处的内容,可以写在任意位置,也可以省略不写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>

int main() {
int month = 123;
switch (month) {
case 123:
printf("春天");
break;
case 456:
printf("夏天");
break;
case 789:
printf("秋天");
break;
case 101112:
printf("冬天");
break;
default:
printf("没有该季节");
break;
}
return 0; // 输出:春天
}

switch case 语句中的的穿透性

  • 当switch的变量和某一个case的值匹配上之后,将会跳过后续的case或者default的匹配,直接向后穿透
  • 为了避免switch的穿透性,每一个case和default可以使用break,来跳出switch语句
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

int main() {
int month = 456;
switch (month) {
case 123:
printf("春天");
case 456:
printf("夏天");
case 789:
printf("秋天");
case 101112:
printf("冬天");
default:
printf("没有该季节");
}
return 0;
// 输出:夏天 秋天 冬天 没有该季节!!!
}

当然也可以利用switch的穿透性实现特定的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>

int main() {
int month = 6;
switch (month) {
case 1:
case 2:
case 3:
printf("春天");
break;
case 4:
case 5:
case 6:
printf("夏天");
break;
case 7:
case 8:
case 9:
printf("球天");
break;
case 10:
case 11:
case 12:
printf("冬天");
break;
default:
printf("没有该季节");
break;
}
return 0;
// 输出:夏天
}

循环结构

某段代码需要被重复执行多次并且遵循一定规律,则使用循环结构

while循环

1
2
3
while(条件表达式){
// 循环体
}
  • 条件表达式:循环终止的判断条件语句,结果为bool类型的表达式
  • 循环体:n行循环要执行的语句

流程说明:

  1. 执行条件表达式,也就是执行循环是否终止的判断条件,表达式的值如果是false,则循环结束,如果是true,循环继续执行
  2. 执行循环语句,大括号中的代码,需要循环的代码
  3. 回到第一步再次执行,直到表达式的结果为false,while循环才会结束

注意事项

  1. while循环本身没有循环变量的声明和初始化的部分,应在while循环前声明循环变量并赋值
  2. while循环本身也没有控制循环终止的判断条件语句部分,所以需要再循环体中增加相应的控制语句,否则容易死循环
1
2
3
4
5
6
7
8
9
int main(){
int count = 1; // 循环变量声明和初始化
while(i <= 5){ // 循环条件
// 循环体
printf("%d",i);
i++; // 更新循环变量
}
// 输出:1 2 3 4 5
}

示例:需要在控制台上输入一个整型数字,如果用户在控制台上输入的不正确,让用户重复输入,直到输入正确为止

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

int main(){
int num = 0;
printf("请输入一个数字:");
scanf("%d",num);
while(true){
printf("输入错误,请重新输入");
scanf("%d",num);
}
printf("num = %d",i);
}

do-while循环

1
2
3
do{
// 循环体
} while(条件表达式);

流程说明

  1. 先执行循环体中的语句
  2. 执行条件表达式(循环终止的条件判断语句),结果如果为true,继续执行,如果是false,则循环结束
  3. 回到第一步,再次执行,直到条件表达式的结果为false

注意事项

  • do-while循环为先执行后判断,先执行一次循环体中的代码,然后再执行条件表达式,所以do-while循环至少执行一次
  • 其他特点跟while循环一样
1
2
3
4
5
6
7
8
9
10
int main(){
// 初始化部分
int count = 0;
do {
// 循环体
printf("Hello World");
// 循环变量更新
count++;
} while(count < 3); // 循环条件
}

for循环

1
2
3
for(循环起点;循环条件;循环步长){
// 循环体
}
  • 循环起点:循环变量的初始化,如 int i = 0
  • 循环条件:循环终止的条件,为布尔表达式, 如 i < 10
  • 循环步长:循环改变的控制条件语句,如 i++
  • 循环体:循环要执行的语句
  • 表达式之间要用分号;分隔

流程说明

  1. 第一步:执行循环变量初始化语句(循环起点)
  2. 第二步:执行循环终止的判断条件表达式,结果为ture,继续执行第三步,结果为false,结束循环
  3. 第三步:执行循环语句
  4. 第四步:执行循环步长,也就是循环改变的控制条件语句,使循环变量的值发生改变
  5. 第五步:回到第二步,再次执行执行第二步到第五步,直到第二步的循环条件的表达式结果为false,循环结束
1
2
3
4
5
6
int main(){
for(int i = 0;i < 5;i++) {
printf("当前循环变量的值:%d",i);
}
// 输出: 0 1 2 3 4
}

for循环的小括号中每一个部分都可以省略不写,但是分号;不能省略

1
2
3
4
5
6
7
int main(){
int i = 0;
for( ; ; ){
printf("%d",i);
i++;
}
}

流程控制的关键字

break

  • 用于终止某个语句块的执行
  • 如果是在循环中,则是跳出所在的循环,如果是在switch语句中,则为跳出所在的switch语句
1
2
3
4
5
6
7
8
9
int main(){
for(int i = 1;i < 10;i++) {
if(i == 6){
break;
}
printf("%d",i);
}
// 输出:1 2 3 4 5
}

continue

  • 跳过本次循环,执行下一次循环,(如果有多次循环,默认继续执行离自己最近的循环)提前终止本次循环
  • 只能在循环语句中使用
1
2
3
4
5
6
7
8
9
int main(){
for(int i = 1;i < 10;i++) {
if(i == 6){
continue;
}
printf("%d",i);
}
// 输出:1 2 3 4 5 7 8 9
}

goto

  • 可以在任意的位置设置标签,使用关键字goto可以直接跳转到指定的标签的位置继续执行
1
2
3
4
5
6
7
8
9
10
11
12
int main(){
label1:
printf("1")
goto label3;

label2:
printf("2");

label3:
printf("3");
// 输出:1 3
}

函数

函数的概念

  • 函数就是把任意一段代码放在一个 盒子 里面

  • 在我想要让这段代码执行的时候,直接执行这个 盒子 里面的代码就行

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 这个是我们以前写的一段代码
    for (int i = 0; i < 10; i++) {
    printf("%d", i);
    }

    // 函数,这个 {} 就是那个 “盒子”
    void fn() {
    // 这个函数我们以前写的代码
    for (int i = 0; i < 10; i++) {
    printf("%d", i);
    }
    }

函数的参数

  • 我们在定义函数和调用函数的时候都出现过 ()

  • 现在我们就来说一下这个 () 的作用

  • 就是用来放参数的位置

  • 参数分为两种 形参实参

    1
    2
    3
    4
    5
    6
    void fn(行参写在这里) {
    // 一段代码
    }

    fn(实参写在这里)

形参和实参的作用

  1. 形参

    • 就是在函数内部可以使用的变量,在函数外部不能使用

    • 每写一个单词,就相当于在函数内部定义了一个可以使用的变量(遵循变量名的命名规则和命名规范)

    • 多个单词之间以 , 分隔

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      // 书写一个参数
      void fn(num) {
      // 在函数内部就可以使用 num 这个变量
      }

      // 书写两个参数
      void fun(num1, num2) {
      // 在函数内部就可以使用 num1 和 num2 这两个变量
      }

    • 行参的值是在函数调用的时候由实参决定的

  2. 实参

    • 在函数调用的时候给行参赋值的

    • 也就是说,在调用的时候是给一个实际的内容的

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      void fn(num) {
      // 函数内部可以使用 num
      }

      // 这个函数的本次调用,书写的实参是 100
      // 那么本次调用的时候函数内部的 num 就是 100
      fn(100)

      // 这个函数的本次调用,书写的实参是 200
      // 那么本次调用的时候函数内部的 num 就是 200
      fn(200)
    • 函数内部的行参的值,由函数调用的时候传递的实参决定

    • 多个参数的时候,是按照顺序一一对应的

      1
      2
      3
      4
      5
      6
      7
      void fn(num1, num2) {
      // 函数内部可以使用 num1 和 num2
      }

      // 函数本次调用的时候,书写的参数是 100 和 200
      // 那么本次调用的时候,函数内部的 num1 就是 100,num2 就是 200
      fn(100, 200)

函数的return

  • return 返回的意思,其实就是给函数一个 返回值终断函数

返回值

  • 函数调用本身也是一个表达式,表达式就应该有一个值出现

  • return 关键字就是可以给函数执行完毕一个结果

    1
    2
    3
    4
    5
    6
    int fn() {
    // 执行代码
    return 100
    }

    // 此时,fn() 这个表达式执行完毕之后就有结果出现了
    • 我们可以在函数内部使用 return 关键把任何内容当作这个函数运行后的结果

终断函数

  • 当我开始执行函数以后,函数内部的代码就会从上到下的依次执行

  • 必须要等到函数内的代码执行完毕

  • return 关键字就是可以在函数中间的位置停掉,让后面的代码不在继续执行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    void fn() {
    printf("11");
    printf("22");
    return; //后面不会执行了
    printf("33");
    printf("44");
    }


    // 函数调用
    fn()

函数的声明

为什么要声明?

有些情况下,如果不对函数进行声明,编译器在编译的时候,可能不认识这个函数,因为编译器在编译 c 程序的时候,从上往下编译的。

(1) 直接声明法

1
2
3
4
5
6
7
8
9
void func(void);
int main()
{
func();
}
void func(void)
{
printf("hello kerwin\n");
}

(2) 间接声明法

将函数的声明放在头文件中,.c 程序包含头文件即可

1
2
3
4
5
6
main.c
#include”k.h”


k.h
extern void func(void);

使用函数的好处?

1、定义一次,可以多次调用,减少代码的冗余度。

2、使咱们代码,模块化更好,方便调试程序,而且阅读方便。

内存的分区

1、内存:物理内存、虚拟内存

  • 物理内存:实实在在存在的存储设备

  • 虚拟内存:操作系统虚拟出来的内存。

  • 操作系统会在物理内存和虚拟内存之间做映射。

  • 在写应用程序的,咱们看到的都是虚拟地址。

2、在运行程序的时候,操作系统会将 虚拟内存进行分区。

1).堆

​ 在动态申请内存的时候,在堆里开辟内存。

2).栈

​ 主要存放局部变量。

3).静态全局区

​ 1:未初始化的静态全局区

​ 静态变量(定义变量的时候,前面加 static 修饰),或全局变量,没有初始化的,存在此区

​ 2:初始化的静态全局区

​ 全局变量、静态变量,赋过初值的,存放在此区

4).代码区

​ 存放咱们的程序代码

5).文字常量区

​ 存放常量的。

普通的全局变量

在函数外部定义的变量.

1
2
3
4
5
int number=100;//number 就是一个全局变量
int main()
{
return 0;
}

作用范围:

  • 普通全局变量的作用范围,是程序的所有地方。
  • 只不过用之前需要声明。声明方法 extern int number;
  • 注意声明的时候,不要赋值。

生命周期:

  • 程序运行的整个过程,一直存在,直到程序结束。

注意:

  • 定义普通的全局变量的时候,如果不赋初值,它的值默认为 0

静态全局变量 static

1
2
3
4
5
6
定义全局变量的时候,前面用 static 修饰。
static int number=100;//number 就是一个静态全局变量
int main()
{
return 0;
}

作用范围:

  • 只能在它定义的.c(源文件)中有效

生命周期:

  • 在程序的整个运行过程中,一直存在。

注意:

  • 定义静态全局变量的时候,如果不赋初值,它的值默认为 0。

普通的局部变量

在函数内部定义的,或者复合语句中定义的变量

1
2
3
4
5
6
7
int main()
{
int num;//普通局部变量
if(1){
int a;//普通局部变量
}
}

作用范围:

  • 在函数中定义的变量,在它的函数中有效
  • 在复合语句中定义的,在它的复合语句中有效。

静态的局部变量

定义局部变量的时候,前面加 static 修饰

作用范围:

  • 在它定义的函数或复合语句中有效。

生命周期:

  • 第一次调用函数的时候,开辟空间赋值,函数结束后,不释放,以后再调用函数的时候,就不再为其开辟空间,也不赋初值,用的是以前的那个变量。

静态函数

在定义函数的时候,返回值类型前面加 static 修饰。这样的函数 被称为静态函数。

static 限定了函数的作用范围,在定义的.c 中有效。

数组

数组的概念

数组是若干个相同类型的变量在内存中有序存储的集合。

int a[5];//定义了一个整型的数组 a,a 是数组的名字,数组中有 5 个元素,每个元素的类型都是 int 类型,而且在内存中连续存储。

这十个元素分别是 a[0] a[1] a[2] a[3] a[4]

image-20220730112642632

数组的分类

1)字符数组

char s[10]; s[0],s[1]....s[9];

2)短整型的数组

short a[10];

3)整型的数组

int a[10];

  1. 长整型的数组

lont a[5];

5)浮点型的数组(单、双)

float a[6]; a[4]=3.14f;

double a[8]; a[7]=3.115926;

6)指针数组

char *a[10]

7)结构体数组

struct student a[10];

二维数组

数组名【行下标】【列下标】

int a [3] [3]

arr[0,0] arr[0,1] arr[0,2]
arr[1,0] arr[1,1] arr[1,2]
arr[2,0] arr[2,1] arr[2,2]

数组的排序

  • 排序,就是把一个乱序的数组,通过我们的处理,让他变成一个有序的数组

冒泡排序

  • 先遍历数组,让挨着的两个进行比较,如果前一个比后一个大,那么就把两个换个位置

  • 数组遍历一遍以后,那么最后一个数字就是最大的那个了

  • 然后进行第二遍的遍历,还是按照之前的规则,第二大的数字就会跑到倒数第二的位置

  • 以此类推,最后就会按照顺序把数组排好了

选择排序

  • 先假定数组中的第 0 个就是最小的数字的索引
  • 然后遍历数组,只要有一个数字比我小,那么就替换之前记录的索引
  • 知道数组遍历结束后,就能找到最小的那个索引,然后让最小的索引换到第 0 个的位置
  • 再来第二趟遍历,假定第 1 个是最小的数字的索引
  • 在遍历一次数组,找到比我小的那个数字的索引
  • 遍历结束后换个位置
  • 依次类推,也可以把数组排序好

指针

指针的概念

image-20220802101023536

字符变量 char ch=‘b’; ch 占 1 个字节,它有一个地址编号,这个地址编号就是ch 的地址整型变量 int a=0x12 34 56 78; a 占 4 个字节,它占有 4 个字节的存储单元,有4 个地址编号。

image-20220802101124817

指针变量的定义

1.简单的指针变量

数据类型 * 指针变量名;

1
int * p;//定义了一个指针变量 p 

在 定义指针变量的时候 * 是用来修饰变量的,说明变量 p 是个指针变量。

2.关于指针的运算符

& 取地址 、 *取值

1
2
3
int a=0x1234abcd;
int *p;//在定义指针变量的时候*代表修饰的意思,修饰 p 是个指针变量。
p=&a;//把 a 的地址给 p 赋值 ,&是取地址符,
image-20220802101453052

指针的用处

直接影响原数据

1
2
3
4
5
void swap(int *a,int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
1
2
3
4
5
6
7
8
9
void sort(int *arr,int length) {
for (int i = 0; i < length - 1; i++) {
for (int j = 0; j < length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
swap(&arr[j], &arr[j + 1]);
}
}
}
}

指针和数组

1
2
3
4
int a[5];
int *p;
p=&a[0];
指针变量 p 保存了数组 a 中第 0 个元素的地址,即 a[0]的地址
image-20220803101840802

通过指针变量运算加取值的方法来引用数组的元素

1
2
3
4
int a[5];
int *p;
p=a; *(p+2)=100;//也是可以的,相当于 a[2]=100
解释:p 是第 0 个元素的地址,p+2 是 a[2]这个元素的地址。对第二个元素的地址取值,即 a[2]

指针的分类

按指针指向的数据的类型来分

1:字符指针

字符型数据的地址

1
char *p;//定义了一个字符指针变量,只能存放字符型数据的地址编号

2:短整型指针

1
short *p;//定义了一个短整型的指针变量 p,只能存放短整型变量的地址

3:整型指针

1
int *p;//定义了一个整型的指针变量 p,只能存放整型变量的地址

4:长整型指针

1
long *p;//定义了一个长整型的指针变量 p,只能存放长整型变量的地址

5:float 型的指针

1
float *p;//定义了一个 float 型的指针变量 p,只能存放 float 型变量的地址

6:double 型的指针

1
double *p;//定义了一个 double 型的指针变量 p,只能存放 double 型变量的地址

7:函数指针

8、结构体指针

9、指针的指针

10、数组指针

11、通用指针 void *p;

无论什么类型的指针变量,在 32 位系统下,都是 4 个字节。

指针只能存放对应类型的变量的地址编号。

字符串和指针

字符串就是以’\0’结尾的若干的字符的集合:比如“hello world”。

字符串的地址,是第一个字符的地址。如:字符串“hello world”的地址,其实是字符串中字符’h’的地址。

我们可以定义一个字符指针变量保存字符串的地址,比如:char *s =”hello world”;

字符串的可修改性

字符串内容是否可以修改,取决于字符串存放在哪里

  1. 存放在数组中的字符串的内容是可修改的

    1
    2
    char str[100]=”kerwin”;
    str[0]=‘y’;//正确可以修改的
  2. 文字常量区里的内容是不可修改的

    1
    2
    char *str=”kerwin”; 
    *str =’y’;//错误,存放在文字常量区,不可修改

指针数组

定义一个数组,数组中有若干个相同类型指针变量,这个数组被称为指针数组int *p[5]

指针数组本身是个数组,是个指针数组,是若干个相同类型的指针变量构成的集合

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main()
{
char *name[3] = {"kerwin","tiechui","xiaoming"};
int i;
for(i=0;i<3;i++)
{
printf("%s\n",name[i]);
}
}

指针的指针

指针的指针,即指针的地址,

1
2
3
4
5
6
7
8
9
10
int a=0x12345678
假如:a 的地址是 0x00002000
int *p;
p =&a;
则 p 中存放的是 a 的地址编号即 0x00002000

假如:指针变量 p 的地址编号是 0x00003000,这个地址编号就是指针的地址
int **q;
q=&p;//q 保存了 p 的地址,也可以说 q 指向了 p
则 q 里存放的就是 0x00003000
image-20220804145952789

数组指针

本身是个指针,指向一个数组,加 1 跳一个数组,即指向下个数组。

指向的数组的类型(*指针变量名)[指向的数组的元素个数]

int (*p)[5];

//定义了一个数组指针变量 p,p 指向的是整型的有 5 个元素的数组p+1 往下指 5 个整型,跳过一个有 5 个整型元素的数组。

image-20220803203736931 image-20220803203746786 image-20220803200143055

数组名字取地址

变成 数组指针

1
2
3
4
5
6
7
8
9
#include<stdio.h>
int main()
{
int a[10];
printf("a=%p\n",a);
printf("a+1=%p\n",a+1);
printf("&a=%p\n",&a);
printf("&a+1=%p\n",&a+1);
}

a 和&a 所代表的地址编号是一样的,即他们指向同一个存储单元,但是a和&a 的指针类型不同。

a 是个 int *类型的指针,是 a[0]的地址。 &a 变成了数组指针,加 1 跳一个 10 个元素的整型一维数组

数组名字和指针变量的异同

1
2
3
int a[5]= {1,2,3,4,5}
int *p;
p=a;

相同点:

a 是数组的名字,是 a[0]的地址,p=a 即 p 保存了 a[0]的地址,即 a 和 p 都指向a[0],所以在引用数组元素的时候,a 和 p 等价 引用数组元素回顾: a[1]、**(a+1)、p[1]、* *(p+1) 都是对数组 a 中 a[1]元素的引用。

不同点:

  1. a 是常量、p 是变量

  2. 对 a 取地址,和对 p 取地址结果不同

因为 a 是数组的名字,所以对 a 取地址结果为数组指针。

p 是个指针变量,所以对 p 取地址(&p)结果为指针的指针。

给函数传指针参数

要想改变主调函数中变量的值,必须传变量的地址,而且还得通过*+地址去赋值。无论这个变量是什么类型的。

1
2
3
4
5
6
7
8
9
10
11
12
void fun(char **barr) {
printf("%d\n", barr[0][0]);
barr[0][0] = 2;
}
int main() {

int arr1[] = { 1,2,3 };
int arr2[] = { 4,5,6 };
int* arr[] = { arr1,arr2 };
fun(arr);
printf("%d\n", arr[0][0]);
}

函数返回值是指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int* swap( int a,int b) {
int temp = a; //临时变量先存其中一个值
a = b;
b = temp;
static int arr[2]; //静态
arr[0] = a;
arr[1] = b;
return arr;
}

......
int *x = swap(myarr[j], myarr[j + 1]);
myarr[j] = x[0];
myarr[j + 1] = x[1];
......

初识函数指针

咱们定义的函数,在运行程序的时候,会将函数的指令加载到内存 的代码段。所以函数也有起始地址。

c 语言规定:函数的名字就是函数的首地址,即函数的入口地址 咱们就可以定义一个指针变量,来存放函数的地址。 这个指针变量就是函数指针变量。

image-20220806134839960

函数指针的定义和调用

定义:

1
2
3
4
5
6
7
8
int max(int x,int y)
{
...
}

int(*p)(int,int);//定义了一个函数指针变量 p,p 指向的函数

p=max;

调用:

1
2
(*p)(30,50);
p(30,50);

函数指针的用处

1
2
3
4
5
6
7
8
9
10
11
12
13
14

void call_back(int current , int total){
printf("压缩进度是:%d------%d" , current , total); // 22------100
}


void compress(char* file_name ,void(call_back)(int , int)){
call_back(22 , 100);
}

void main(){

compress("kerwin.jpg" , call_back);
}

水煮易混淆指针

1、 int *a[5];

这是个指针数组,数组 a 中有 5 个整型的指针变量

a[0]~a[4] ,每个元素都是 int *类型的指针变量

2、int (*a)[5];

数组指针变量,它是个指针变量。它占 8 个字节,存地址编号。

它指向一个数组,它加 1 的话,指向下一行。

3、 int **p;

这个是个指针的指针,保存指针变量的地址。

它经常用在保存指针的地址:

4、int *f(void);

注意:*f 没有用括号括起来

它是个函数的声明,声明的这个函数返回值为 int *类型的。

5、int (*f)(void);

注意f 用括号括起来了,修饰 f 说明,f 是个指针变量。

f 是个函数指针变量,存放函数的地址,它指向的函数,

必须有一个 int 型的返回值,没有参数。

特殊指针

  1. 空类型的指针(void *)

void* 通用指针,任何类型的地址都可以给 void*类型的指针变量赋值。

1
2
void *memcpy(void *dest, const void *src, size_t n);

因为对于这种通用型接口,你不知道用户的数据类型是什么,但是你必须能够处理用户的各种类型数据,因而会使用void。void能包容地接受各种类型的指针。也就是说,如果你期望接口能够接受任何类型的参数,你可以使用void类型。但是在具体使用的时候,你必须转换为具体的指针类型。例如,你传入接口的是int,那么你在使用的时候就应该按照int*使用。

  1. 空指针 NULL
1
char *p=NULL;

p 哪里都不指向,也可以认为 p 指向内存编号为 0 的存储单位。

动态内存申请

初识动态内存

C语言提供了一些内存管理函数,这些内存管理函数可以按需要动态的分配内存空间,也可把不再使用的空间回收再次利用。

静态分配

1、 在程序编译或运行过程中,按事先规定大小分配内存空间的分配方式。int a [10]

2、 必须事先知道所需空间的大小。

3、 分配在栈区或全局变量区,一般以数组的形式。

4、 按计划分配。

动态分配

1、在程序运行过程中,根据需要大小自由分配所需空间。

2、按需分配。

3、分配在堆区,一般使用特定的函数进行分配。

malloc 函数

void * malloc(int size )

在内存的动态存储区(堆区)中分配一块长度为 size 字节的连续区域,用来存放类型说明符指定的类型。函数原型返回 void*指针,使用时必须做相应的强制类型转换 .

返回值:

  • 分配空间的起始地址 ( 分配成功 )

  • NULL ( 分配失败 )

注意:

  • 在调用 malloc 之后,一定要判断一下,是否申请内存成功。
  • 如果多次 malloc 申请的内存,第 1 次和第 2 次申请的内存不一定是连续的

free 函数(释放内存函数)

free 函数释放 p 指向的内存。

1
2
char *p=(char *)malloc(100);
free(p);

calloc 函数

在内存的堆中,申请 n 块,每块的大小为 size 个字节的连续区域

函数的返回值:

  • 返回 申请的内存的首地址(申请成功)
  • 返回 NULL(申请失败)

注意:malloc 和 calloc 函数都是用来申请内存的。

区别:

​ 1) 函数的名字不一样

​ 2) 参数的个数不一样

​ 3) malloc 申请的内存,内存中存放的内容是随机的,不确定的,而calloc 函数申请的内存中的内容为 0

realloc 函数

在原先 s 指向的内存基础上重新申请内存,新的内存的大小为 new_size 个字节,如果原先内存后面有足够大的空间,就追加,如果后边的内存不够用,则relloc 函数会在堆区找一个 newsize 个字节大小的内存申请,将原先内存中的内容拷贝过来,然后释放原先的内存,最后返回 新内存的地址。

image-20220807141500827

内存泄露

申请的内存,首地址丢了,找不了,再也没法使用了,也没法释放了,这块内存就被泄露了。

1
2
3
4
5
6
7
8
int main()
{
char *p;
p=(char *)malloc(100);
//接下来,可以用 p 指向的内存了
p="kerwin";//p 指向别的地方了
//从此以后,再也找不到你申请的 100 个字节了。则动态申请的 100 个字节就被泄露了
}
1
2
3
4
5
6
7
8
9
10
11
void func()
{
char *p;
p=(char *)malloc(100);
}
int main()
{
func();
func();
}
//每调用一次 fun 泄露 100 个字节

字符串处理函数

字符串拷贝函数strcpy_s

拷贝 src 指向的字符串到 dest 指针指向的内存中,’\0’也会拷贝

1
2
3
4
5
6
7
#include<string.h>

char* p = (char*)malloc(100);
if (!p) return;
strcpy_s(p, 7,"kerwin");

printf("%s", p);

测字符串长度函数strlen

测字符指针 s 指向的字符串中字符的个数,不包括’\0’

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <string.h>
int main()
{
char str1[20]="hello";
char *str2 ="hello";
printf("%d\n",sizeof(str1)); //20
printf("%d\n",sizeof(str2));//8
printf("%d\n",strlen(str1));//5
printf("%d\n",strlen(str2));//5
return 0;
}

字符串追加函数strcat_s

strcat 函数追加 src 字符串到 dest 指向的字符串的后面。追加的时候会追加’\0’

1
2
3
4
5
6
char* str = (char*)malloc(100);
if (str == NULL) return;
scanf_s("%s", str, 100);
char *str2 = "先生/女士";
strcat_s(str, 100,str2);
printf("%s\n", str);

字符串比较函数strcmp

比较 s1 和 s2 指向的字符串的大小, 比较的方法:逐个字符去比较 ascII 码,一旦比较出大小返回。 如果所有字符都一样,则返回 0

1
2
3
4
5
6
7
8
9
10
11
12
char* a = (char *)malloc(100);
char* b = (char *)malloc(100);

scanf_s("%s", a,100);
scanf_s("%s", b,100);
if (a == NULL || b == NULL) return;
if (strcmp(a, b) == 0) {
printf("输入正确");
}
else {
printf("输入错误");
}

字符查找函数strchr

在字符指针 s 指向的字符串中,找 ascii 码为 c 的字符 注意,是首次匹配,如果过说 s 指向的字符串中有多个 ASCII 为 c 的字符,则找的是第1 个字符

1
2
3
4
5
6
7
8
9
10
char* str[] = { "teichui","xiaoming","kerwin" };

char x;

scanf_s("%c", &x,1);
for (int i= 0; i < sizeof(str) / sizeof(char*); i++) {
if (strchr(str[i], x) != NULL) {
printf("%s\n", str[i]);
}
}

字符串匹配函数strstr

char *strstr(const char *haystack, const char *needle);

在 haystack 指向的字符串中查找 needle 指向的字符串,也是首次匹配

1
2
3
4
5
6
7
8
char* str[] = { "teichui","xiaoming","kerwin"};
char x[100];
scanf_s("%s", x,100);
for (int i= 0; i < sizeof(str) / sizeof(char*); i++) {
if (strstr(str[i], x) != NULL) {
printf("%s\n", str[i]);
}
}

字符串转换数值atoi

atoi/atol/atof 字符串转换功能

函数的声明:int atoi(const char *nptr);

1
2
3
int num;
num=atoi(“12岁”);
则 num 的值为 12

字符串切割函数strtok

函数声明:char *strtok(char *str, const char *delim);

字符串切割,按照 delim 指向的字符串中的字符,切割 str 指向的字符串。其实就是在 str 指向的字符串中发现了 delim 字符串中的字符,就将其变成’\0’, 调用一次 strtok 只切割一次,切割一次之后,再去切割的时候 strtok 的第一个参数传 NULL,意思是接着上次切割的位置继续切

1
2
3
4
5
6
7
8
9
10
void split(char *p,char **myp) {
char* buf = NULL;
myp[0] = strtok_s(p,"|", &buf);
int i = 0;

while (myp[i]) {
i++;
myp[i] = strtok_s(NULL, "|", &buf);
}
}

空间设定函数memset

函数声明:void* memset(void *ptr,int value,size_t num);

memset 函数是将 ptr 指向的内存空间的 num 个字节全部赋值为 value

1
2
3
4
5
6
7
8
int* str = (int*)malloc(100);
if (!str)return;
//memset(str, -1, 100);
memset(str, 0, 100);
for (int i = 0; i < 25; i++)
{
printf("%d\n",str[i]);
}

结构体

初识结构体

在程序开发的时候,有些时候我们需要将不同类型的数据组合成一个有机的整体

1
2
3
4
5
struct {
char name[100];
int score;
int age;
}

结构体是一种构造类型的数据结构, 是一种或多种基本类型或构造类型的数据的集合。

结构体初始化与访问

结构体变量,是个变量,这个变量是若干个相同或不同数据构成的集合注:

  1. 在定义结构体变量之前首先得有结构体类型,然后再定义变量
  2. 在定义结构体变量的时候,可以顺便给结构体变量赋初值,被称为结构体的初始化
  3. 结构体变量初始化的时候,各个成员顺序初始化
1
2
3
4
5
6
7
8
9
10
11
struct stu {
char name[100];
int score;
int age;
};

struct stu student1= {"tiechui",100,18};
struct stu student0;
strcpy_s(student0.name, 100, "kerwin");
student0.score = 100;
student0.age = 100;

结构体数组

结构体数组是个数组,由若干个相同类型的结构体变量构成的集合

struct 结构体类型名 数组名[元素个数];

1
struct student stu[3];

结构体指针

即结构体的地址,结构体变量存放内存中,也有起始地址 咱们定义一个变量来存放这个地址,那这个变量就是结构体指针变量。 结构体指针变量也是个指针,既然是指针在 64 位环境下,指针变量的占 8 个字节,存放一个地址编号。

struct 结构体类型名 * 结构体指针变量名;

1
2
3
4
5
6
7
struct student *p;
struct student student1;

p = &student1;
(*p).name
p->name

结构体与函数

给函数传结构体变量的地址

1
2
3
4
5
void input(struct stu *student) {
printf("请录入姓名,年龄, 成绩\n");
scanf_s("%s%d%d", student->name, 100, &student->age, &student->score);
}

结构体内存分配

结构体变量大小是,它所有成员的大小之和。

规则 1

以多少个字节为单位开辟内存 给结构体变量分配内存的时候,会去结构体变量中找基本类型的成员哪个基本类型的成员占字节数多,就以它大大小为单位开辟内存。

(1):成员中只有 char 型数据 ,以 1 字节为单位开辟内存。

(2):成员中出现了 short int 类型数据,没有更大字节数的基本类型数据。以 2 字节为单位开辟内存

(3):出现了 int float 没有更大字节的基本类型数据的时候以 4 字节为单位开辟内存。

(4):出现了 double 类型的数据, 以 8 字节为单位开辟内存。

规则 2

(1):char 1 字节对齐 ,即存放 char 型的变量,内存单元的编号是1 的倍数即可。

(2):short int 2 字节对齐 ,即存放 short int 型的变量,起始内存单元的编号是2 的倍数即可。

(3):int 4 字节对齐 ,即存放 int 型的变量,起始内存单元的编号是4 的倍数即可

(4):long int 在 32 位平台下,4 字节对齐 ,即存放 long int 型的变量,起始内存单元的编号是4的倍数即可

(5):float 4 字节对齐 ,即存放 float 型的变量,起始内存单元的编号是4 的倍数即可

(6):double 8 字节对齐,即存放 double 型变量的起始地址,必须是 8 的倍数,double 变量占8字节

字节对齐的好处

用空间来换时间,提高 cpu 读取数据的效率

image-20220817114718562

链表

概念:链表是一种 物理存储结构上非连续 、非顺序的存储结构,数据元素的 逻辑顺序 是通过链表中的 指针链 接 次序实现的 。

image-20220818105810715

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

#include<stdio.h>
#include<stdlib.h>
struct student {
char name[100];
int iNumber;
struct student* pNext;
};
int count = 0;
struct student* create() {
struct student* pHead = NULL;
struct student* pEnd, * pNew;


pEnd = pNew = (struct student*)malloc(sizeof(struct student));
if (!pNew) return;
printf("请输入名字,和学号\n");
scanf_s("%s", pNew->name, 100);
scanf_s("%d", &pNew->iNumber);

while (pNew->iNumber != 0) {
count++;
if (count == 1) {
pNew->pNext = NULL;
pEnd = pNew;
pHead = pNew;
}
else {
pNew->pNext = NULL;
pEnd->pNext = pNew;
pEnd = pNew;
}

pNew = (struct student*)malloc(sizeof(struct student));
if (!pNew) return;
scanf_s("%s", pNew->name, 100);
scanf_s("%d", &pNew->iNumber);
}

return pHead;
}
void print(struct student* pHead) {
int index = 1;
struct student *pTemp = pHead;
while (pTemp != NULL) {
printf("第%d个学生信息\n", index);
printf("%s,%d\n", pTemp->name, pTemp->iNumber);
pTemp = pTemp->pNext;
index++;
}
}

int main() {
struct student* pHead = create();

print(pHead);
}

共用体

共用体和结构体类似,也是一种构造类型的数据结构。把 struct 改成 union 就可以了。

几个不同的变量共同占用一段内存的结构,在C语言中,被称作“共用体”类型结构共用体所有成员占有同一段地址空间 共用体的大小是其占内存长度最大的成员的大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct {
char name[100];
int score;
}stu;

typedef struct {
char name[100];
int salary;
}tea;

typedef union
{
stu student;
tea teacher;
} any;

共用体的特点:

​ 1、同一内存段可以用来存放几种不同类型的成员,但每一瞬时只有一种起作用

​ 2、共用体变量中起作用的成员是最后一次存放的成员,在存入一个新的成员后原有的成员的值会被覆盖

​ 3、共用体变量的地址和它的各成员的地址都是同一地址

枚举

将变量的值一一列举出来,变量的值只限于列举出来的值的范围内

enum 枚举类型名 { 枚举值列表; };

在枚举值表中应列出所有可用值,也称为枚举元素

枚举元素是常量,默认是从 0 开始编号的。

1
2
3
4
5
6
7
enum TYPE { STU = 1, TEA };

enum TYPE type;

if (type == STU) {
//......
}

注意:

  • 宏定义是一个值/表达式,不是一种类型

  • 枚举是一种类型,可以定义枚举类型的一个变量

位运算

原码反码补码

正数在内存中以原码形式存放,负数在内存中以补码形式存放

正数的 原码=反码=补码

原码:将一个整数,转换成二进制,就是其原码。 如单字节的 5 的原码为:0000 0101;-5 的原码为 1000 0101。

反码:正数的反码就是其原码;负数的反码是将原码中,除符号位以外,每一位取反。如单字节的 5 的反码为:0000 0101;-5 的反码为 1111 1010。

补码:正数的补码就是其原码;负数的反码+1 就是补码。 如单字节的 5 的补码为:0000 0101;-5 的补码为 1111 1011。

位运算

无论是正数还是负数,编译系统都是按照内存中存储的内容进行位运算。

  • &按位 与

    任何值与 0 得 0,与 1 保持不变

  • |按位 或

    任何值或 1 得 1,或 0 保持不变

  • ~ 按位取反

    1 变 0,0 变 1

  • ^ 按位异或

    相异得 1,相同得 0

  • 位移

    >>右移 << 左移

预处理

  1. 预编译

​ 将.c 中的头文件展开、宏展开 生成的文件是.i 文件

  1. 编译

​ 将预处理之后的.i 文件生成 .s 汇编文件

  1. 汇编

​ 将.s 汇编文件生成.o 目标文件

  1. 链接

​ 将.o 文件链接成目标文件

  1. 执行

宏定义define

定义宏用 define 去定义, 宏是在预编译的时候进行替换。

(1) 不带参宏

#define PI 3.1415

在预编译的时候如果代码中出现了 PI 就用 3.1415 去替换。

(2) 带参宏

#define MAX(a,b) (a>b?a:b)

将来在预处理的时候替换成 实参替代字符串的形参,其他字符保留

带参宏和带参函数的区别

  • 带参宏被调用多少次就会展开多少次,执行代码的时候没有函数调用的过程,不需要压栈弹栈。所以带参宏,是浪费了空间,因为被展开多次,节省时间。

  • 带参函数,代码只有一份,存在代码段,调用的时候去代码段取指令,调用的时候要,压栈弹栈。有个调用的过程。 所以说,带参函数是浪费了时间,节省了空间。

  • 带参函数的形参是有类型的,带参宏的形参没有类型名。

选择性编译

(1)

1
2
3
4
5
#ifdef KERWIN
代码段一
#else
代码段二
#endif

(2)

1
2
3
4
5
#ifndef KERWIN
代码段一
#else
代码段二
#endif

(3)

1
2
3
4
5
6
7
#if KERWIN==1
代码段一
#elif KERWIN==2
代码段二
#else
代码段三
#endif

注意和 if else 语句的区别

  • if else 语句都会被编译,通过条件选择性执行代码

  • 选择性编译,只有一块代码被编译

文件

初识文件

文件用来存放程序、文档、音频、视频数据、图片等数据的。

文件就是存放在磁盘上的,一些数据的集合。

磁盘文件: 指一组相关数据的有序集合,通常存储在外部介质(如磁盘)上,使用时才调入内存。

设备文件:在操作系统中把每一个与主机相连的输入、输出设备看作是一个文件,把它们的输入、输出等同于对磁盘文件的读和写。

键盘:标准输入文件

屏幕:标准输出文件

其它设备:打印机、触摸屏、摄像头、音箱等

标准 io 库函数对磁盘文件的读取

image-20220824104049479

文件缓冲区是库函数申请的一段内存,由库函数对其进行操作,程序员没有必要知道存放在哪里,只需要知道对文件操作的时候的一些缓冲特点即可。

磁盘文件的分类

一个文件通常是磁盘上一段命名的存储区 ,计算机的存储在物理上是二进制的,所以物理上所有的磁盘文件本质上都是一样的:以字节为单位进行顺序存储.

从用户或者操作系统使用的角度

把文件分为:

文本文件:基于字符编码的文件 

二进制文件:基于值编码的文件

文本文件、二进制文件对比

译码

文本文件编码基于字符定长,译码容易些; 

二进制文件编码是变长的,译码难一些(不同的二进制文件格式,有不同的译码方式,一般需要特定软件进行译码)。

空间利用率

二进制文件用一个比特来代表一个意思(位操作);

而文本文件任何一个意思至少是一个字符。

所以二进制文件,空间利用率高。

可读性:

文本文件用通用的记事本工具就几乎可以浏览所有文本文件

二进制文件需要一个具体的文件解码器

文件指针

文件指针在程序中用来标识(代表)一个文件的,在打开文件的时候得到文件指针,文件指针就用来代表咱们打开的文件。

FILE * 指针变量标识符;

1
2
3
4
5
6
7
8
typedef struct _iobuf {
int cnt; // 剩余的字符,如果是输入缓冲区,那么就表示缓冲区中还有多少个字符未被读取
char *ptr; // 下一个要被读取的字符的地址
char *base; // 缓冲区基地址
int flag; // 读写状态标志位
int fd; // 文件描述符
// 其他成员
} FILE;

在缓冲文件系统中,每个被使用的文件都要在内存中开辟一块 FILE 类型的区域,存放与操作文件相关的信息

image-20220824104646521

对文件操作的步骤:

 1、对文件进行读写等操作之前要打开文件得到文件指针 

 2、可以通过文件指针对文件进行读写等操作 

 3、读写等操作完毕后,要关闭文件,关闭文件后,就不能再通过此文件指针操作文件了

fopen

FILE *fopen(const char *path, const char *mode);

函数的参数:

  • 参数 1:打开的文件的路径
  • 参数 2:文件打开的方式,即以什么样的方式 r w a +

image-20220824105245000

返回值:

  • 成功:打开的文件对应的文件指针
  • 失败:返回 NULL

fclose

int fclose(FILE *fp);

关闭 fp 所代表的文件

返回值:

  • 成功返回 0
  • 失败返回非 0

fgetc 与 fputc

int fgetc(FILE *stream);

fgetc 从 stream 所标识的文件中读取一个字节,将字节值返回

返回值: 读到文件结尾返回 EOF

EOF 是在 stdio.h 文件中定义的符号常量,值为-1

int fputc(int c, FILE *stream)

fputc 将 c 的值写到 stream 所代表的文件中。

返回值

如果输出成功,则返回输出的字节值; 

如果输出失败,则返回一个 EOF。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include<stdio.h>
#pragma warning (disable:4996)
int main(void)
{
FILE* fp1,*fp2;
char ch;
fp1 = fopen(".\\a.txt", "r+");

if (fp1 == NULL)
{
printf("Cannot open the file\n");
return 0;
}
fp2 = fopen(".\\b.txt", "w+");
if (fp2 == NULL)
{
printf("Cannot open the file\n");
return 0;
}
while ((ch = fgetc(fp1)) != EOF)
{
fputc(ch, fp2);
}


fclose(fp1);
fclose(fp2);
}

fgets 与 fputs

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

从 stream 所代表的文件中读取字符,在读取的时候碰到换行符或者是碰到文件的末尾停止读取,或者是读取了 size-1 个字节停止读取,在读取的内容后面会加一个\0,作为字符串的结尾

int fputs(const char *s, FILE *stream);

将 s 指向的字符串,写到 stream 所代表的文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include<stdio.h>

int main() {
FILE* fp1, * fp2;

char str[100];

int error1 = fopen_s(&fp1, "kerwin.txt", "r");
if (error1 != 0) {
printf("打开失败");
return;
}
int error2 = fopen_s(&fp2, "copy.txt", "a");
if (error2 != 0) {
printf("打开失败");
return;
}
fgets(str, 100, fp1);
fputs(str, fp2);
printf("|%s|\n", str);

fclose(fp1);
fclose(fp2);
}

fread

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

fread 函数从 stream 所标识的文件中读取数据,每块是 size 个字节,共nmemb 块,存放到ptr 指向的内存里

返回值: 实际读到的块数。

fwrite

size_t fwrite(void *ptr, size_t size, size_t nmemb, FILE *stream);

fwrite 函数将 ptr 指向的内存里的数据,向 stream 所标识的文件中写入数据,每块是size 个字节,共nmemb 块。

rewind

rewind 复位读写位置

void rewind(文件指针);

把文件内部的位置指针移到文件首

fseek

int fseek(FILE *stream, long offset, int whence);

移动文件流的读写位置.

whence 起始位置

  • 文件开头 SEEK_SET 0

  • 文件当前位置 SEEK_CUR 1

  • 文件末尾 SEEK_END 2

位移量: 以起始点为基点,向前、后移动的字节数,正数往文件末尾方向偏移,负数往文件开头方向偏移。

1
2
3
4
5
6
7
8
9
10
11
12
FILE* fp;

int error = fopen_s(&fp, "kerwin.txt", "r+");
if (error != 0) {
return;
}
fseek(fp, 4, 0);

char bir[100];
fgets(bir,100,fp);

printf("%s", bir);

ftell

测文件读写位置距文件开始有多少个字节

long ftell(文件指针);

返回值: 返回当前读写位置(距离文件起始的字节数),出错时返回-1.

1
2
long int length;
length = ftell(fp)

千锋图书管理借阅系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
struct bookInfo
{
char name[100];
float price;
int num;
};

struct Node {

struct bookInfo data;
struct Node* next;
};

struct Node* list = NULL;


struct Node* createHead() {
struct Node* headNode = (struct Node *)malloc(sizeof(struct Node));

if (headNode == NULL) return;
headNode->next = NULL;

return headNode;
}
struct Node* createNode(struct bookInfo data) {
struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));

if (newNode == NULL) return;

newNode->data = data;
newNode->next = NULL;
return newNode;
}
void insertBook(struct Node *headNode,struct bookInfo data) {

//根据 传来的data,创建一个新的Node 结构体,newNode
struct Node* newNode = createNode(data);
newNode->next = headNode->next;
headNode->next = newNode;
}

void readInfo(char* filename, struct Node* headNode) {
//fscanf 返回EOF;
FILE* fp = fopen(filename, "r");
if (fp == NULL) {
fp = fopen(filename, "w");
fclose(fp);
return;
}
struct bookInfo temp;
while (fscanf(fp, "%s %f %d\n",temp.name,&temp.price,&temp.num) != EOF) {
insertBook(list, temp);
}


fclose(fp);
}

void saveInfo(char *filename,struct Node *headNode) {
//打开文件
FILE* fp = fopen(filename, "w");
//写入文件 fwrite , fprintf
if (fp == NULL) {
return;
}
struct Node* move = headNode->next;
while (move != NULL) {
fprintf(fp, "%s %.1f %d\n", move->data.name, move->data.price, move->data.num);

move = move->next;
}

//关闭文件
fclose(fp);
}

void showlist(struct Node* headNode) {

struct Node* move = headNode->next;
while (move != NULL) {
printf("%s %.1f %d\n", move->data.name, move->data.price, move->data.num);

move = move->next;
}

}

struct Node* searchByName(struct Node *headNode,char *name) {
struct Node* move = headNode->next;

while (move != NULL && strcmp(move->data.name,name)) {

move = move->next;
}
return move;
}

void deleteByName(struct Node* headNode, char* name) {

struct Node* move = headNode->next;
struct Node* prev = headNode;
while (move != NULL && strcmp(move->data.name, name)) {
prev = move;
move = move->next;
}

if (move == NULL) {
return;
}
else {

printf("删除成功");
prev->next = move->next;
free(move);
move = NULL;
}
}
void keyEvent() {
int key = 0;
scanf("%d", &key);
struct bookInfo temp;
struct Node* result;
switch (key)
{
case 0:
printf(" [录入] \n");
printf(" 录入的书籍信息,名字,价格,数量\n");
scanf("%s%f%d",temp.name,&temp.price,&temp.num);
//printf("%s,%.1f,%d", temp.name, temp.price, temp.num);

// 同步到链表
insertBook(list,temp);
// 同步文件
saveInfo("kerwin.txt", list);
break;
case 1:
printf(" [速览] \n");
showlist(list);
break;
case 2:
printf(" [借阅] \n");
printf(" 查询的书名是:\n");
scanf("%s", temp.name);

result = searchByName(list, temp.name);
if (result == NULL) {
printf("未找到相关书籍,无法借阅\n");

}
else {
//
if (result->data.num > 0) {
result->data.num--;
printf("借阅成功\n");
saveInfo("kerwin.txt", list);
}
else {

printf("没有库存,无法借阅");
}
}
break;
case 3:
printf(" [归还] \n");
printf(" 查询的书名是:\n");
scanf("%s", temp.name);

result = searchByName(list, temp.name);
if (result == NULL) {
printf("未找到相关书籍,无法借阅\n");

}
else {
result->data.num++;
printf("归还成功\n");
saveInfo("kerwin.txt", list);
}
break;
case 4:
printf(" [查找] \n");
printf(" 查询的书名是:\n");
scanf("%s", temp.name);

result = searchByName(list, temp.name);
if (result == NULL) {
printf("未找到相关书籍\n");

}
else {
printf("书名 价格 数量\n");
printf("%s %.1f %d\n", result->data.name, result->data.price, result->data.num);

}
break;
case 5:
printf(" [删除] \n");
printf(" 删除的书名是:\n");
scanf("%s", temp.name);
deleteByName(list,temp.name);
saveInfo("kerwin.txt",list);
break;
case 6:
printf(" [退出] \n");
exit(0);//ctrl+c;
break;
default:
break;
}
}
void menu() {

printf("----------------------------------\n");
printf("千锋图书管理借阅系统\n");
printf("0.录入书籍\n");
printf("1.速览书籍\n");
printf("2.借阅书籍\n");
printf("3.归还书籍\n");
printf("4.查找书籍\n");
printf("5.删除书籍\n");
printf("6.退出系统\n");
printf("----------------------------------\n");
printf("请输入(0~6):");

}
int main() {
//初始化头部指针。
list = createHead();
// 读取原来文件的数据.
readInfo("kerwin.txt", list);
while (1) {
menu();
keyEvent();

system("pause");
system("cls");
}
}