c语言学习笔记
课前鸡汤
计算机是一门"做中学"的学科,不是会了再做,而是做了才会 --大佬说的
1.c语言有什么用
1.c语言的优点以及c语言的用途
-
简洁、紧凑、灵活、方便:C语言只有32个关键字和9个控制语句。程序可以自由编写,主要用小写字母表示。它结合了高级语言的基本结构和语句与低级语言的实用性。C语言可以像汇编语言一样对位、字节和地址进行操作,而这三者是计算机最基本的工作单元。
-
运算符丰富:C的算子涵盖范围很广,一共有34个算子。 C 语言将括号、赋值和强制类型转换视为运算符。因此,C的运算类型极其丰富,表达式类型多样化,灵活运用各种运算符可以实现其他高级语言难以实现的运算。
-
丰富的数据结构:C的数据类型包括:整数、实数、字符、数组、指针、结构体、联合体等,可用于实现对各种复杂数据类型的操作,并引入指针概念使程序更高效。此外,C语言具有强大的图形功能,支持多种显示器和驱动程序,具有强大的计算功能和逻辑判断功能。
-
C是一种结构化语言:结构化语言的显着特点是代码和数据的分离,即程序的各个部分除了必要的信息交换外是相互独立的。这种结构化的方式可以使程序层次清晰,易于使用、维护和调试。C语言以函数的形式提供给用户,易于调用,并具有多种循环和条件语句来控制程序的流程,使程序完全结构化。
-
C语法限制较少,编程自由度高:虽然C也是一种强类型语言,但它的语法更加灵活,让程序员有更大的自由度。
-
允许直接访问物理地址,可以直接在硬件上操作:因此,C既具有高级语言的功能,又具有低级语言的许多功能。它可以像汇编语言一样对位、字节和地址进行操作,这三者是计算机最基本的工作单元,可以用来编写系统软件。
- C语言程序生成高质量代码,程序执行效率高:一般只比汇编器生成的目标代码效率低 10-20%。
- C语言应用范围广,可移植性好C语言的突出优点之一是它适用于多种操作系统,如DOS、UNIX,也适用于多种模型。
2.是学习路线的开始:是未来学习java,c++,pyton3以及计算机组成原理,数据结构与算法,操作系统,计算机网络的基础
2.我能不能学好编程(如何自学,学习能力的培养)
-
要做好心理准备,打持久战
-
要有自己的学习计划和目标:[01星球计算机学习计划链接](https://www.bilibili.com/video/BV15e4y1h7dW/?spm_id_from=333.337.search-card.all.click&vd_source=ddbedbfe95d1f10340d558c7e3efaa26title = "点击观看视频")
-
少说多做,只有经过时间积累的练习,编程水平才能得到提升
3.学习的意义
1.找到事情干,许多大学生清醒的堕落,就是没有目标找不到事情做,天天躺尸
2.获得荣誉,提升自己
3.找到工作(有了安身立命的本事)或者考研成功(提升学历)
正课
0.使用的工具
1.记笔记
- 学会使用键盘,b站自己看,练习上小键人,多打字,多打代码[小键人](https://dazi.91xjr.com/title = "这是练习打字网站的链接")
- Typora,需要学习一门简单的笔记语言markdown [这是markdown教程](https://www.cnblogs.com/c0625/p/16401438.htmltitle = "这是一个链接,点击试试")
2.查资料,看网课
- acwing点击进入acwing
- b站点击进入b站
- csdn点击进入csdn
- goole和百度以及bing百度一下你就知道
- 知乎(有很多大佬分享,逼格很高,就是装逼娃太多)点击进入知乎
- 看书(深入学习,当然前提要耐得住)点击进入微信读书
- 学习编程推荐边学边查,就像我们学习说话一样,你不会把语言学完才说话吧,包括到了工作依然还是要多查
3.刷题
- acwing点击进入acwing
- leetcode点击进入leetcode
- 洛谷点击加入计协洛谷团队
4.编译器选择(新手随便你选一个,在线编译器都行)点击进入联想软件商店下载
-
dev-cpp(新手推荐,简单大方)配置教程
-
vscode(需要配置环境,新人不推荐,我都没用过)
-
vs studio(太大了,不方便)
-
vim(linux必备)
-
codeblock(没用过,似乎也不错)
6.c语言编译的过程 预处理,编译,汇编,链接
1.c语言变量, 表达式, 语句, 运算符与输入输出
第一个c代码
#include<stdio.h>//包含头文件,头文件里定义了这个库里的函数等
int main()//main函数是主函数,是程序的入口
{
printf("Hello, World!\n");//printf是一个函数,调用这个函数输出字符串到标准输出(显示器)
return 0;//main函数的返回值,0为正常退出,非0为异常退出,正常我们都写0,是规定
}
代码风格:很重要,编程语言要想写的漂亮规范,一定要注意空格和代码缩进点击查看代码风格详解
1.变量
- 变量的存储:内存四区 栈区,堆区,静态区(分为代码区和数据区)内存四区详解
- 变量的四个属性变量的属性详解
1.变量的类型: c语言内置的基本类型如 int,char,float,double,还有函数类型,结构体类型(面向对象语言还有自定义的类类型)
2.变量的作用域:一个变量在什么范围内有用,程序是按照一定顺序执行的,变量的作业域开始于定义变量开始,结束决定于它的链接属性,和在哪个区域定义
3.变量的生命周期
4.变量的链接属性 - 使用变量:
先声明再定义
int a;//单个声明
a = 10;//定义a,也是把10赋值给a
int c, d;//连续声明但不定义
c = 2;
d = 3;
声明并且定义
int a = 10;//声明并定义
int c = 10, d = 5;//连续声明并且定义
float x = 1.0, y = 11;
声明和定义一定要区分,声明是申请了一个int变量的内存空间,但是还没有进行定义,这个变量的值现在是随机的,可能是其他程序用完的值,我们把它称为垃圾值,所以变量一定要定义再使用
小tips,交换两个变量的三种方式
//方式一临时变量法
int a = 1, b = 2;
{
int t = a;
a = b;
b = t;
}
//方式二累加运算法
int a = 1, b = 2;
a = a + b;
b = a - b;
a = a - b;
//方式三异或性质运用
int a = 1, b = 2;
a = a ^ b;
b = a ^ b;
a = a ^ b;
2.表达式表达式详解
- 类似我们数学中写的一个运算式子比如1+1就是一个表达式,表达式的结果是它的运算结果
- 逗号表达式是多个表达式写在一起,按从左到右的顺序执行,结果是最后一个表达式的值
- 表达式你可以把它当成只能用一次的变量一样,if后面的括号,函数的参数都可以使用表达式,只要表达式的值符合
代码展示,第一个不将表达式直接写在参数里
#include<stdio>
int main()//计算二乘三
{
int a = 2, b = 3;
int res = a * b;
printf("%d\n", res);
printf("%d\n", 2 * 3);//直接将表达式写在参数里
return 0;
}
3.语句:
- c语言中一个分号表示一个语句结束
int a;//分号表示这个语句结束了,语句可以是一个表达式,也可以是逗号表达式
printf("Hello, World!\n"), a = 10;//逗号表达式
//完整程序
#include<stdio.h>
int main()
{
int b;
printf("Hello, World!\n"), b = 10;
return 0;
}
- 代码块:就是用{}围起来的区域,可以看作是一个语句集合,但是在执行的时候代码块和语句都是基本单位,所以for,if后默认控制一个代码块或者语句,同时块内定义的auto变量只具有代码块作用域
所谓语句块(block),就是用花括号包围的零条或多条语句的序列。语句块也是语句的一种,在任何要求使用语句的地方都可以使用语句块。 --来自<<c++ primer>>
#include<stdio.h>
int main()
{
{
int a = 10;
}
printf("%d\n", a);//代码将会报错,因为a已经'死'了
}
4.运算符号c语言运算符详解
-
算术运算符号:+, -, *, /, 自增运算符(++, --), 复合算术运算(+=, -=, *=, /=)
-
逻辑运算符号:与(&&), 或(||), 非(!)
-
位运算:左移(<<), 右移(>>), 位与(&), 位或(|)
-
关系运算符:大于(>), 小于(<), 等于(==), 不等于(!=)
-
其它运算符:解引用运算符,也可以用来声明指针(*), 取地址符号(&), 计算占用内存大小(sizeof)
-
运算符优先级:算术 > 关系 > 逻辑 > 赋值 单目运算符 > 双目运算符,但是都可以通过括号来改变
理解小于号可以经过逻辑演化出所有关系运算符号
代码
#include<stdio.h>
int main()
{
int a = 2, b = 1, c = 1;
//可以使用<号组合出其它关系运算符号
//a > b 等价于 b < a
//a == b 等价于 a >= b && a <= b 也就是 (!(a < b) && !(b < a)) 我不比你小并且我不比你大,咱俩就是相等的
//a != b 等价于 (a < b || a > b)
//a >= b 等价于 !(a < b)
//a <= b 等价于 !(b < a)
//可以自己动手实验,这里只测试 ==, !=
if(a < b || b < a)
{
cout<<"a和b不相等"<<endl;
}
else
{
cout<<"a和b相等"<<endl;
}
if(!(b < c) && !(c < b))
{
cout<<"b和c相等"<<endl;
}
else
{
cout<<"b和c不相等"<<endl;
}
return 0;
}
5.输入与输出c语言输入和输出详解
-
标准输入和标准输出(stdin和stdout):一般标准输入指的是键盘,标准输出是显示器
-
scanf():需要了解一下格式控制符号 %d是int %f是浮点数 %c是字符,%s是字符串,同时理解一下分隔符号的作用(相当于能够知道你的一句话是哪里结束,程序和人一样,它也得知道你输入的东西到哪结束)
-
printf():属于stdio库,用来格式化输出
-
getchar()和putchar():用来读取和输出字符
-
gets()和puts():用来读取字符串和输出字符串,gets()默认的分隔符号是换行符,可以用来读取一行数据
小提醒:输入与输出还是很重要的,我们可以把计算机当成一个黑盒子,给它一个输入它会给我们一个输出
#include<stdio.h>
int main()
{
int a, b;
scanf("%d%d", &a, &b);//以空白字符为分隔符读入a和b,&是取地址运算符号
printf("%d\n", a + b);
char c;
c = getchar();//读入一个字符赋值给c
putchar(c);//输出c
return 0;
}
2.流程控制流程控制详解
程序如果只能顺序执行那么工作量就会太大了,流程控制能够提高代码的复用性,而且不具有完备性,图灵证明了只要有顺序,循环和判断就可以表示现实生活中任何事情的流程
1.顺序执行
程序默认是顺序执行的,从main()函数开始到main函数返回
语句一般是顺序执行的:语句块的第一条语句首先执行,然后是第二条语句,依此类推。当然,少数程序,包括我们解决书店问题的程序,都可以写成只有顺序执行的形式。但程序设计语言提供了多种不同的控制流语句,允许我们写出更为复杂的执行路径。 --来自<<c++ primer>>
2.判断语句
- if 和 if.. else if ..else:已经可以实现所有的判断逻辑
#include<stdio.h>
int main()
{
int a = 1;
//if(a == 0) printf("灯关闭\n");
//else if(a == 1) printf("一档\n");
//else printf("二档\n");
return 0;
}
- switch..case..case..default:设计的目的是在条件分支过多时候代替if..else if ..else的,但是似乎并不受到程序员的喜欢,有的程序员可能一辈子都可以不使用它
#include<stdio.h>
int main()
{
int a = 1;
switch(a)
{
case 0:
printf("灯关闭\n");
break;
case 1:
printf("灯一档\n");
break;
default:
printf("灯二档\n");
}
return 0;
}
- 嵌套判断:类似数学中的复合函数,大家经常说的套娃
#include<stdio.h>
int main()
{
char a = '3';
if(a >= '0' && a <= '9')
{
printf("a是数字\n")
if(a == '3')
{
printf("a是数字3\n");
}
}
else
printf("a不是数字\n");
return 0;
}
- 三目运算符 表达式1 : 表达式2 ? 表达式3 表达式1为真执行表达式2否则执行表达式3
#include<stdio.h>
int main()
{
char a = '3';
(a >= '0' && a <= '9') ? printf("a是数字\n") : printf("a不是数字\n");
return 0;
}
5.用逻辑语句来实现判断语句
利用&&的性质,语句1 && 语句2 只有语句1为真时候才会执行语句2,所以if(语句1)语句2是等价于语句1 && 语句2的
#include<stdio.h>
int main()
{
int a;
scanf("%d", &a);
(a > 5 && a < 15) && (printf("a在范围内\n"), a = 11);
(a > 5 && a < 15) || (printf("a不在范围内\n"), a = 22);
printf("%d\n", a);
return 0;
}
3.循环语句
1.for循环
for循环是用来简化while循环的,我们使用循环时候总是需要一个初始值,判断语句,每次循环后对初始值的操作,for(语句1:语句2:语句3)中的语句1就是定义初始值语句,语句2是判断语句,为真执行为假退出循环,语句3就是对初始值的操作,比如每次都加1
#include<stdio.h>
int main()
{
for(int i = 1; i <= 9; ++i)//使用for循环打印99乘法表
{
for(int j = 1; j <= i; ++j)
{
printf("%-2d * %-2d = %-2d ", j, i, i * j);
}
printf("\n");
}
return 0;
}
2.while和do..while循环
do .. while用于人机交互
#include<stdio.h>
int main()
{
int i = 1;
while(i <= 9)
{
int j = 1;
while(j <= i)
{
printf("%-2d * %-2d = %-2d ", j, i, i * j);
++j;
}
printf("\n");
++i;
}
return 0;
}
3.break和continue
braek用来跳出循环,continue用来跳过这次循环continue下面的所有语句,都是跳出距离它们最近的一层,换句话说就是看它们是哪个循环内部的语句
#include<stdio.h>
int main()
{
int i = 1;
while(i)//从3开始打印99乘法表到8
{
if(i < 3)
{
++i;
continue;
}
int j = 1;
while(j <= i)
{
printf("%-2d * %-2d = %-2d ", j, i, i * j);
++j;
}
printf("\n");
++i;
if(i == 9) break;
}
return 0;
}
4.go to语句
可以随意改变程序执行顺序,多用来跳出多重循环,一般不建议使用
#include<stdio.h>
int main()
{
goto s;
printf("没有跳过\n");
s:
return 0;
}
数组C语言数组详解
最基础的数据结构
1.数组的概念
数组是若干个相同类型的变量在内存中有序存储的集合。
概念理解:
1. 数组用于存储一组数据
2. 数组里面存储的数据类型必须是相同的
3. 数组在内存中会开辟一块连续的空间
int a[10];//定义了一个整型的数组a,a是数组的名字,数组中有10个元素,每个元素的类型都是int类
型,而且在内存中连续存储,这十个元素分别是a[0] a[1]...a[9], a[0]~a[9]在内存中连续的顺序存储
2.数组的分类
1. 按存储元素类型分类
#include<stdio.h>
int main()
{
int a[10];//整型一维数组
char b[10];//字符型一维数组
double c[10];//浮点型一维数组
int *d[10];//整型指针的一维数组
return 0;
}
2. 按维度分类
1.一维数组
- 定义与声明
声明用 元素类型 + 数组名 + [数组大小] 如int a[10];
定义可以用逐个赋值比如 a[0] = 1,或者列表初始化 int a[10] = {1, 2, 3, 4}; - 赋值的方式
可以直接让一个数组等于另一个数组,但是他们其实是一个数组,也可以逐个赋值
如:a[1] = 1, a[2] = 2; - 使用的方式
直接使用数组名+[下标]就可以像使用变量一样使用数组元素 如a[1] - 内存模型
arr:
+----+----+----+----+----+
| 0 | 1 | 2 | 3 | 4 |
+----+----+----+----+----+
//例题 数组替换
#include<stdio.h>
int main()
{
int a[10];//定义一个长度为10的整型数组
for(int i = 0; i < 10; ++i)//循环输入数组元素,i用来当数组下标,注意,数组下标从0开始
{
scanf("%d", &a[i]);
if(a[i] <= 0) a[i] = 1;
printf("X[%d] = %d", i, a[i]);
}
return 0;
}
2.二维数组
- 定义与声明
声明类似一维数组就是多加了个[], 比如 int a[2][2]
定义可以使用列表初始化,比如int a[2][2] = {{1, 2}, {1, 2}}
也可以一个一个赋值 - 赋值的方式
赋值就是直接a[i][j] = x - 使用的方式
要指出行下标和列下标就可以像使用变量一样使用了比如a[i][j] - 内存模型
int **a = (int *)malloc(sizeof(int) * 6);
array:
+---+ +---+---+---+
| *-|--->| 1 | 2 | 3 |
+---+ +---+---+---+
| *-|--->| 4 | 5 | 6 |
+---+ +---+---+---+
int *a[];
array:
+-------+ +---+---+---+
| array[0] | ---> | 1 | 2 | 3 |
+-------+ +---+---+---+
| array[1] | ---> | 4 | 5 | 6 |
+-------+ +---+---+---+
int a[][];
array:
+---+---+---+---+---+---+
| 1 | 2 | 3 | 4 | 5 | 6 |
+---+---+---+---+---+---+
//二维数组例题 数组的左方元素
#include<stdio.h>
int main()
{
char op[2];//使用字符数组读入字符,因为字符用scanf读入时候是会读入空格,而字符数组会忽略空
格
scanf("%s", &op);//%s是字符串输入的格式化符号
double a[12][12];//定义一个存储类型是double的12行12列的数组,名称是a
double sum = 0, cnt = 0;//sum用来求和左方元素,cnt用来统计左方元素的数量
for(int i = 0; i < 12; ++i)
{
for(int j = 0; j < 12; ++j)
{
scanf("%lf", &a[i][j]);//double用%lf来当格式化输入符号
if(i > j && i + j < 11)//正方形数组主对角线性质 i == j, 副对角线性质 i + j= n(n是数组列长 - 1), 根据这个性质左上(i > j)和左下(i + j < 11)的交集(交集用&&)就是左方区域
{
sum = sum + a[i][j];
cnt++;//cnt += 1 cnt = cnt + 1;
}
}
}
if(op[0] == 'S')
printf("%.1lf\n", sum);//输出和
else printf("%.1lf\n", sum / cnt);//输出平均数
return 0;
}
3.多维数组
定义声明与赋值和以上差不多,基本用不到多维数组
#include<stdio.h>
int main()
{
int a[2][2][2];//定义了一个多维数组
return 0;
}
4.数组与指针
- 数组名和指针
- 用指针的方式定义和声明数组
#include<stdio.h>
int main()
{
int a[10] = {1, 2, 3, 4, 5, 6};
int * b = a;
printf("%d\n", b[1]);
return 0;
}
5.数组常用函数
include<stdio.h>
int main()
{
int a[10];
memset(a, -1, szieof a);//将a的所有元素赋值为-1, sizeof是计算变量或者数组或者结构体占用内存大小的关键字
int b[10];
memcpy(b, a, sizeof a);//将a数组复制给b数组
//利用循环遍历数组
for(int i = 0; i < 10; ++i)
{
printf("%d ", a[i]);
}
printf("\n");
int c[10][10];//声明了一个二维数组
memset(c, 0x3f, sizeof c);
//利用嵌套循环按行遍历数组
for(int i = 0; i < 10; ++i)//枚举行
{
for(int j = 0; j < 10; ++j)//枚举列
{
printf("%d ", a[i][j]);
}
printf("\n");
}
return 0;
}
5.字符串c语言字符串详解
字符串是人机交互的关键
1. 字符串和字符数组的区别
C语言中没有字符串这种数据类型,可以通过char型数组来替代
注意:C语言中,字符串一定是一个char型数组,但char型数组未必是字符串
在C语言的char型数组中,数字0(和字符‘\0’等价)结尾的char数组就是一个字符串,但如果char型数
组没有以数字0结尾,那么就不是一个字符串,只是普通字符数组,所以字符串是一种特殊的char的数组
#include<stdio.h>
#include<string.h>
int main()
{
char a[] = "Hello, World!";
printf("%s\n", a);
int n = sizeof a / sizeof a[0];
printf("%d\n", n);//n的长度还包括了结束符号\0
printf("%d\n", strlen(a));//strlen(a)返回a的长度是a不包括\0的长度
char b[20];
memcpy(b, a, sizeof a);//将a字符串从头开始的sizeof a个字符拷贝(复制的意思)给b(从头开始)
printf("%s\n", b);
return 0;
}
2.字符串的初始化
#include<stdio.h>
int main()
{
//不指定长度,没有0结束符,有多少个元素就有多长
char buf[] = { 'a','b','c' };
printf("buf= %s\n", buf);
//指定长度,后面没有赋值的元素,自动补0
char buf2[100] = { 'a','b','c' };
char buf[1000] = { "hello" };
printf("buf2=%s\n", buf2);
//所有元素赋值为0
char buf3[100] = { 0 };
//char buf4[2] = { '1','2','3' };//数组越界
char buf5[50] = { '1','a','b','0','7' };
printf("buf5=%s\n", buf5);
char buf6[50] = { '1','a','b',0,'7' };
printf("buf6=%s\n", buf6);
char buf7[50] = { '1','a','b','\0','7' };
printf("buf7=%s\n", buf7);
//使用字符串初始化,编译器自动在后面补0,常用
char buf8[] = "dgasiugduaishg";
//'\0'后面最好不要连着数字,有可能几个数字连起来正好是一个转义字符
//'\ddd'八进制字义字符,'\xdd'十六进制转义字符
//\012相当于\n
char str[] = "\012abc";
printf("str==%s\n", str);
return 0;
}
3.字符串的输入与输出
由于字符串采用了'\0'标志,字符串的输入输出将变得简单方便
#include<stdio.h>
#include<string.h>
int main()
{
char s[20];
scanf("%s", &s);//scanf输入,分隔符是空白字符,遇到空格,回车,制表符都会结束输入
gets(s);//gets输入,按行输入,遇到换行符号才停止
printf("%s\n", s);//printf输出,不带换行符
puts(s);//puts输出,会自动为字符串加上换行符号
}
4.字符串处理相关函数
#include<stdio.h>
#include<string.h>
int main()
{
char s[] = "hello, world!";
printf("%d\n", strlen(s));//strlen()计算字符串长度,不包含\0
char p[] = "";
strcpy(p, s);//将字符串s复制给字符串p
puts(p);//输出p
strcat(s, p);//将字符串p连接到字符串s后边
puts(s);//输出字符串s
if(strcmp("abc", "bcd") > 0)//字符串比较函数
{
printf("1字符串大于2字符串\n");
}
else if(strcmp("abc", "bcd") == 0)
{
printf("1字符串等于2字符串\n");
}
else
{
printf("1字符串小于2字符串\n");
}
printf("%d\n", strstr(s, "he"))//strstr(s, p)查找字符串p在字符串s中第一次出现的位置,没有出现返回NULL,成功查找到返回p在s第一次出现的下标
}
5..利用sizeof计算数组或者字符串的元素个数
#include<stdio.h>
int main()
{
char s[] = "hell0!";
int n = sizeof s / sizeof s[0];//n是包含\0的长度
printf("%d\n", n);
return 0;
}
6.函数c语言函数详解
c语言基本单位是函数,这也是c语言面向过程的特点
函数提高了代码复用性,避免了重复性操作,是泛型思想的体现 --李学长
1.函数的概念
类似数学上的函数,拥有函数名,返回值(函数的值范围类似数学函数的值域),参数(可以有0个1个或者多个,范围类似数学上的定义域),函数体(函数功能实现部分)
可以把函数看成一个黑盒子,你给它输入或者不给它输入它给你一个输出或者实现一个功能
以类型的角度来看,函数是一种函数类型,就像之前的int类型,float类型,char类型,数组类型,没有什么不同,所以函数依然拥有声明与定义的概念,只不过函数是一种可以调用的类型
形式:
数学上 定义函数f(x): y = x + 1 给函数一个参数x函数体是x + 1返回的值y是x + 1
c语言函数
返回值 函数名 (参数1, 参数2, 参数3....)
{
函数体;
return 返回值;
}
2.函数的定义与声明以及使用
c语言函数声明必须要写在main函数之上,定义可以在声明时定义,也可以和声明分开定义
通过一个程序认识函数的使用
#include<stdio.h>
int my_max(int a, int b);//函数声明
int my_min(int a, int b)//函数声明加定义
{
return a > b ? b : a;
}
int main()
{
int a, b;//怎么区分声明和定义?声明只是告诉编译器我有这个变量但是编译器还没有给它分配内存来存储,定义就是编译器给分配内存了,除了extern修饰的,类型在声明时就会申请内存空间完成定义
scanf("%d%d", &a, &b);//用scanf函数输入两个变量
printf("%d", my_max(a, b));//调用my_max函数求a, b最大值
printf("%d", my_min(a, b));//调用my_min函数求a, b最小值
}
int my_max(int a, int b)//my_max函数定义或者叫函数实现
{
return a < b ? b : a;
}
3.函数与递归
递归是函数本身调用自己的行为,比较难理解,我们通过思想+画图模拟函数递归过程+编程实战来理解
思想上就像是每个人只负责自己的事情和传唤下一个人继续完成,烽火台就类似,每个看守烽火台的只负责在看到上一个烽火台点亮后,他就点亮自己的烽火台,他的烽火台点亮后就会被下一个看守看见,就会重复他的动作
但是递归肯定是不能无穷无尽进行下去的,所以递归的终止条件就很重要
理论上所有的循环都可以使用递归写出来,但是有些递归无法用循环实现,或者说很难用循环实现,这也是为什么会有函数式编程的原因,事物与事物之间的关系只要能用函数来表示,那么函数就可以完成一切事物的操作,函数式编程也是一种编程大思想,和面向过程面向对象一样,也有它的语言,比如Haskell,它们的数学基础是拉姆达演算式
我认为递归有三要素
1.明确函数作用
2.确定递归结束条件
3.确定函数等价的递推式子

递归编程实战,求阶乘
#include<stdio.h>
int f(int n)//函数功能:求n的阶乘 三要素之一
{
if(n == 0) return 1;//递归结束条件 三要素之一
return n * f(n - 1);//n的阶乘等于n乘以(n - 1)的阶乘 整个递归等价的式子是 n * (n -1) * (n - 2) * ... * 1 三要素之一
}
int main()
{
int n;
scanf("%d", &n);
printf("%d\n", f(n));//调用函数f求n的阶乘
return 0;
}
4.利用函数实现一些库
//实现字符串的一些操作函数,比如strlen, strcat, strcpy
#include<stdio.h>
int my_strlen(char *s);
char *my_strcat(char *a, char * b);
void my_strcpy(char *a, char * b);
int main()
{
char a[] = "Hello,", b[] = "World!";
return 0;
}
课下任务
1.利用循环加函数实现nn乘法表
2.实现上述的递归版本
7.指针
指针是c语言的灵魂,是c语言最强大的地方,但如果不能熟练使用指针也会给程序编写带来很多困难
1.内存和地址
计算机组成通常包括中央处理器, 内存, 高速缓存(cache), 外存, 显卡, 外部设备,而我们程序员就主要是操作内存的
我们要学习指针必须先了解一下内存,内存像是无数节火车连接在一块,每个块都有自己的编号来唯一确定它自己,这个编号也就是我们常说的内存地址,而指针其实也是一种变量但是它存储的是内存地址
2.值和类型
类型分为编程语言提供的类型(int, double)和用户自定义的数据结构(struct),需要理解指针也是一种类型,指针类型是由指向的数据类型决定的
值分为两种:一种是数据,一种是地址
3.指针变量的内容
指针变量的内容是内存地址,指针变量的类型是指针类型,指针类型是由指向的数据类型决定的
#include<stdio.h>
int main()
{
int a = 10;
int *p = &a;
printf("%p\n", p);//%p是打印地址的格式化符号
printf("%d\n", *p);
return 0;
}
4.间接访问操作符
间接访问操作符是*,用来访问指针指向的内存地址的值
#include<stdio.h>
int main()
{
int a = 10;
int *p = &a;
printf("%d\n", *p);
return 0;
}
5.野指针和内存泄漏
野指针是指向未知内存地址的指钋,使用野指针会导致程序崩溃
内存泄漏是指程序在运行过程中分配的内存没有被释放,导致内存泄漏
小tips:c++中的智能指针,可以避免内存泄漏
#include<stdio.h>
int main_01()
{
int *p;
*p = 10; //野指针
printf("%d\n", *p);
return 0;
}
int main_02()
{
//内存泄漏
int *p = (int *)malloc(sizeof(int));
*p = 10;
return 0;
}
6.NULL指针
NULL指针是指向空地址的指针,可以用来初始化指针变量
#include<stdio.h>
int main()
{
int *p = NULL;
printf("%p\n", p);
return 0;
}
7.指针间接访问左值
指针间接访问左值是指可以通过指针修改指针指向的内存地址的值
#include<stdio.h>
int main()
{
int a = 10;
int *p = &a;
*p = 20;
printf("%d\n", a);
return 0;
}
8.多级指针
多级指针是指指向指针的指针,可以用来修改指针指向的内存地址
常见的用途是传递二维数组给函数修改数组的值
#include<stdio.h>
void modify(int **p, int n, int m) //数组会退化为指针,需要同时传递数组的行和列
{
for(int i = 0; i < n; ++i)
{
for(int j = 0; j < m; ++j)
{
p[i][j] = i * m + j;
}
}
}
int main(int argc, char *argv[])
{
int n = 3, m = 4;
int **p = (int **)malloc(sizeof(int *) * n);
for(int i = 0; i < n; ++i)
{
p[i] = (int *)malloc(sizeof(int) * m);
}
modify(p, n, m);
for(int i = 0; i < n; ++i)
{
for(int j = 0; j < m; ++j)
{
printf("%d ", p[i][j]);
}
printf("\n");
}
return 0;
}
9.指针表达式
指针表达式是指指针变量和常量之间的运算
指针的++和--运算是根据指针类型的大小来决定的,如果是一维数组,指针的++和--运算是根据数组元素的大小来决定的
如果是二维数组,指针的++和--运算是根据数组行的大小来决定的
#include<stdio.h>
int main()
{
int a = 10;
int *p = &a;
printf("%p\n", p + 1);
printf("%p\n", p - 1);
printf("%d\n", p[0]);
printf("%d\n", p[1]);
return 0;
}
10.指针运算
10.1算术运算
指针的-操作是指针之间的减法,结果是两个指针之间内存地址的差距,但是两个指针必须是同一类型的指针并且在同一个进程的内存空间中
指针之间的+,*,/运算是没有意义的
#include<stdio.h>
int main()
{
int a = 10;
int *p = &a;
int *q = p + 1;
printf("%p\n", p);
printf("%p\n", q);
printf("%d\n", q - p);
return 0;
}
10.2关系运算
指针的关系运算是指针之间的大小比较,指针之间的大小比较是根据指针的内存地址来决定的,指针的大小比较是有意义的,但是两个指针必须是同一类型的指针并且在同一个进程的内存空间中
#include<stdio.h>
int main()
{
int a = 10;
int *p = &a;
int *q = p + 1;
printf("%d\n", p < q);
printf("%d\n", p > q);
return 0;
}
11.我对指针的理解
11.1指针也是一种类型,不同指针类型的差别在于"步长不同"
指针也是一种类型,不同指针类型的差别在于"步长不同",比如int *p和double *q, p和q的步长是不同的,所以p + 1和q + 1的结果是不同的
#include<stdio.h>
int main()
{
int a = 10;
double b = 10.0;
int *p = &a;
double *q = &b;
printf("%p\n", p);
printf("%p\n", q);
printf("%p\n", p + 1);
printf("%p\n", q + 1);
return 0;
}
11.2指针与按址传递
指针的最大意义就是可以通过指针修改函数外部的变量,这也是按址传递的原理
#include<stdio.h>
void modify(int *p)
{
*p = 20;
}
int main()
{
int a = 10;
modify(&a);
printf("%d\n", a);
return 0;
}
11.3指针与数组
数组的名字是数组的首地址,数组的名字是一个常量,不能被修改
#include<stdio.h>
int main()
{
int a[10];
int *p = a;
printf("%p\n", a);
printf("%p\n", p);
printf("%p\n", a + 1);
printf("%p\n", p + 1);
return 0;
}
11.4指针与函数
指针与函数的关系是指针可以指向函数,函数的名字就是函数的地址,也称为函数指针
#include<stdio.h>
int add(int a, int b)
{
return a + b;
}
int main()
{
int (*p)(int, int) = add;
printf("%d\n", p(1, 2));
return 0;
}
11.5指针与结构体
指针与结构体的关系是指针可以指向结构体,结构体的名字就是结构体的地址
#include<stdio.h>
struct Student
{
char name[20];
int age;
};
int main()
{
struct Student s = {"张三", 20};
struct Student *p = &s;
printf("%s\n", p->name);
printf("%d\n", p->age);
return 0;
}
8.结构体
1.结构体的概念
结构体是一种自定义的数据类型,可以包含多个不同类型的成员变量,结构体的成员变量可以是基本数据类型,数组,指针,结构体等
#include<stdio.h>
struct Student
{
char name[20];
int age;
};
int main()
{
struct Student s = {"张三", 20};
printf("%s\n", s.name);
printf("%d\n", s.age);
return 0;
}
2.结构体的实现
结构体原理是在内存中开辟一块连续的空间,结构体的成员变量按照定义的顺序存储在内存中,结构体关键要理解->运算符,实际上是一种偏移量的计算,结构体的成员变量的地址是相对于结构体的首地址的偏移量
#include <stdio.h>
#include <stdint.h>
#include <string.h>
struct Student {
char name[20];
int age;
};
int main() {
struct Student s = {"张三", 20};
struct Student *p = &s;
printf("%s\n", p->name);
printf("%d\n", p->age);
// 手动计算结构体成员相对于首地址的偏移量,修改 name
uintptr_t base_address = (uintptr_t)p;
uintptr_t name_offset = 0; // name 在结构体中的偏移量为 0
uintptr_t age_offset = name_offset + sizeof(s.name); // age 在结构体中的偏移量
// 修改 name 成员的值
strcpy((char *)(base_address + name_offset), "李四");
printf("%s\n", p->name);
printf("%d\n", *(int *)(base_address + age_offset)); // 直接访问 age 成员
return 0;
}
3.结构体的内存对齐
结构体的内存对齐是为了提高内存访问的效率,结构体的内存对齐是根据结构体成员变量的大小来决定的,结构体成员变量的大小是根据数据类型来决定的
对齐数=max(max(成员大小),操作系统对齐数大小)
结构体大小=对齐数的整数倍
#include<stdio.h>
struct Student
{
char name[7];
int age;
double score;
};
int main()
{
//对其数是8,所以结构体大小是24(因为成员大小和是19, 19小于8的三倍)
printf("%d\n", sizeof(struct Student));//大小并不是成员变量的和,而是对齐数的整数倍
return 0;
}
4.使用结构体实现链表
#include<stdio.h>
#include<stdlib.h>
struct Node
{
int data;
struct Node *next;
};
int main()
{
struct Node *head = (struct Node *)malloc(sizeof(struct Node));
head->data = 1;
head->next = NULL;
struct Node *p = head;
for(int i = 2; i <= 10; ++i)
{
struct Node *q = (struct Node *)malloc(sizeof(struct Node));
q->data = i;
q->next = NULL;
p->next = q;
p = q;
}
p = head;
while(p != NULL)
{
printf("%d ", p->data);
p = p->next;
}
printf("\n");
return 0;
}
9.宏定义
1.宏的概念与应用
宏定义是一种预处理指令,用来定义一个标识符,这个标识符可以代表一个常量,一个表达式,一个函数等,宏定义的作用是提高代码的可读性,减少代码的重复性
宏的应用有log日志打印,调试信息打印,条件编译,头文件防止重复包含等
#include<stdio.h>
#define PI 3.1415926
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define PRINT(a) printf("%d\n", a)
int main()
{
printf("%f\n", PI);
int a = 10, b = 20;
printf("%d\n", MAX(a, b));
PRINT(10);
return 0;
}
2.宏的工作原理
宏定义是一种预处理指令,宏定义的工作原理是在编译之前,预处理器会将宏定义的标识符替换为宏定义的内容,然后再编译
3.宏常量
宏常量相对于const常量的优势是宏常量是在预处理阶段替换的,const常量是在编译阶段替换的,宏常量的优势是可以定义数组的长度,可以定义结构体的大小
#include<stdio.h>
#define N 10
int main()
{
int a[N];
return 0;
}
4.宏函数
宏函数是一种宏定义,宏函数的优势是可以定义一个表达式,一个函数,一个语句等,宏函数的缺点是没有类型检查,没有作用域,没有返回值
#include<stdio.h>
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int main()
{
int a = 10, b = 20;
printf("%d\n", MAX(a, b));
return 0;
}
5.条件编译
条件编译是一种宏定义,条件编译的作用是根据条件编译的结果来决定编译哪一部分代码,条件编译的优势是可以根据不同的条件编译不同的代码
#include<stdio.h>
#define DEBUG
int main()
{
#ifdef DEBUG
printf("debug\n");
#endif
return 0;
}
10.常用库函数
c语言常用库函数
1.数学函数
#include<stdio.h>
#include<math.h>
int main()
{
printf("%f\n", sqrt(2.0));//开方函数
printf("%f\n", pow(2.0, 3.0));//幂函数
printf("%f\n", fabs(-2.0));//绝对值函数
printf("%f\n", ceil(2.3));//向上取整函数
printf("%f\n", floor(2.3));//向下取整函数
printf("%f\n", log(2.0));//对数函数
printf("%f\n", exp(2.0));//指数函数
printf("%f\n", sin(2.0));//正弦函数
printf("%f\n", cos(2.0));//余弦函数
printf("%f\n", tan(2.0));//正切函数
return 0;
}
2.字符串函数
字符串函数是c语言中常用的库函数,字符串函数的作用是对字符串进行操作,字符串函数的优势是可以提高代码的可读性,减少代码的重复性
#include<stdio.h>
#include<string.h>
int main()
{
char a[] = "hello, world!";
printf("%d\n", strlen(a));//字符串长度
printf("%s\n", strcat(a, "hello"));//字符串连接
printf("%s\n", strcpy(a, "hello"));//字符串复制
printf("%d\n", strcmp("abc", "bcd"));//字符串比较
printf("%s\n", strstr(a, "he"));//字符串查找
return 0;
}
3.内存函数
内存函数是c语言中常用的库函数,内存函数的作用是对内存进行操作,内存函数的优势是可以提高代码的可读性,减少代码的重复性
#include<stdio.h>
#include<string.h>
int main()
{
char a[10];
memset(a, -1, sizeof a);//将a的所有元素赋值为-1
char b[10];
memcpy(b, a, sizeof a);//将a数组复制给b数组
return 0;
}
4.时间函数
#include<stdio.h>
#include<time.h>
int main()
{
time_t t = time(NULL);
printf("%ld\n", t);//时间戳
struct tm *p = localtime(&t);
printf("%d-%d-%d %d:%d:%d\n", p->tm_year + 1900, p->tm_mon + 1, p->tm_mday, p->tm_hour, p->tm_min, p->tm_sec);
return 0;
}
5.文件函数
文件函数是c语言中常用的库函数,文件函数的作用是对文件进行操作,文件函数的优势是可以提高代码的可读性,减少代码的重复性
#include<stdio.h>
int main()
{
FILE *fp = fopen("test.txt", "w");
fprintf(fp, "hello, world!");
fclose(fp);
return 0;
}
6.输入输出函数
输入输出函数是c语言中常用的库函数,输入输出函数的作用是对输入输出进行操作,输入输出函数的优势是可以提高代码的可读性,减少代码的重复性
#include<stdio.h>
int main()
{
int a;
scanf("%d", &a);
printf("%d\n", a);
return 0;
}
11.文件
linux中有万物皆文件的思想
1.文件的概念
文件是一种数据存储的方式,文件是一种数据集合,文件是一种数据流,文件是一种数据结构,文件是一种数据类型
2.文件的操作
文件的操作是对文件进行读写,文件的操作是对文件进行打开关闭,文件的操作是对文件进行创建删除,文件的操作是对文件进行复制移动
3.文件的类型
文件的类型有普通文件,目录文件,设备文件,链接文件,管道文件,套接字文件
4.文件的打开
文件的打开是对文件进行打开,文件的打开是对文件进行读写,文件的打开是对文件进行创建删除,文件的打开是对文件进行复制移动
5.文件的读写
文件的读写是对文件进行读写,文件的读写是对文件进行打开关闭,文件的读写是对文件进行创建删除,文件的读写是对文件进行复制移动
6.文件的关闭
文件的关闭是对文件进行关闭,文件的关闭是对文件进行读写,文件的关闭是对文件进行创建删除,文件的关闭是对文件进行复制移动

浙公网安备 33010602011771号