前言
1.初识程序
有穷性
在有限的操作步骤内完成。有穷性是算法的重要特性,任何一个问题的解决不论其采取什么样的算法,其终归是要把问题解决好。如果一种算法的执行时间是无限的,或在期望的时间内没有完成,那么这种算法就是无用和徒劳的,我们不能称其为算
法。
确定性
每个步骤确定,步骤的结果确定。算法中的每一个步骤其目的应该是明确的,对问题的解决是有贡献的。如果采取了一系列步骤而问题没有得到彻底的解决,也就达不到目的,则该步骤是无意义的。
可行性
每个步骤有效执行,得到确定的结果。每一个具体步骤在通过计算机实现时应能够使计算机完成,如果这一步骤在计算机上无法实现,也就达不到预期的目的,那么这一步骤是不完善的和不正确的,是不可行的。
零个或多个输入
从外界获得信息。算法的过程可以无数据输入,也可以有多种类型的多个数据输入,需根据具体的问题加以分析。
一个或多个输出
算法得到的结果就是算法的输出(不一定就是打印输出)。算法1 的目的是为解决一个具体问题,一旦问题得以解决,就说明采取的算法是正确的,而结果的输出正是验证这一目的的最好方式。
2.编程语言的演变
2.1 计算机语言
计算机语言(ComputerLanguage) 指用于人与计算机之间通讯的语言。计算机语言是人与计算机之间传递信息的媒介。计算机系统最大特征是指令通过一种语言传达给机器。为了使电子计算机进行各种工作,就需要有一套用以编写计算机程序的数字、字符和语法规划,由这些字符和语法规则组成计算机各种指令(或各种语句)。这些就是计算机能接受的语言。
2.2 分类
#include<stdio.h> // 头文件// 入口函数
int main() {printf("Hello World!\n"); //打印输出
}
// 注释
2.3 解释型vs编译型
解释型语言:解释性语言编写的程序不进行预先编译,以文本方式存储程序代码。执行时才翻译执行。程序每执行一次就要翻译一遍。
优缺点:跨平台能力强,易于调,执行速度慢。
编译型语言: 编译型语言在执行之前要先经过编译过程,编译成为一个可执行的机器语言的文件,比如exe。
因为翻译只做一遍,以后都不需要翻译,所以执行效率高。编译型语言的优缺点:执行效率高,缺点是跨平台能力弱,不便调试。
3. 初识C语言
3.1 C语言的简介
C语言是一门面向过程的计算机编程语言,与C++、C#、Java等面向对象编程语言有所不同。C语言的设计目标是提供一种能以简易的方式编译、处理低级存储器、仅产生少量的机器码以及不需要任何运行环境支持便能运行的编程语言。C语言描述问题比汇编语言迅速、工作量小、可读性好、易于调试、修改和移植,而代码质量与汇编语言相当。C语言一般只比汇编语言代码生成的目标程序效率低10%-20%。因此,C语言可以编写系统软件。
3.2 C语言的发展历史
C语言诞生于美国的贝尔实验室,由丹尼斯·里奇(Dennis MacAlistair Ritchie)以肯尼斯·蓝·汤普森(Kenneth Lane Thompson)设计的B语言为基础发展而来,在它的主体设计完成后,汤普森和里奇用它完全重写了UNIX,且随着UNIX的发展,c语言也得到了不断的完善。为了利于C语言的全面推广,许多专家学者和硬件厂商联合组成了C语言标准委员会,并在之后的1989年,诞生了第一个完备的C标准,简称“C89”,也就是“[ANSI C](https://baike.baidu.com/item/ANSI C/7657277?fromModule=lemma_inlink)”,截至2020年,最新的C语言标准为2018年6月发布的“C18”。 [5]
C语言之所以命名为C,是因为C语言源自Ken Thompson发明的B语言,而B语言则源自BCPL语言。
1967年,剑桥大学的Martin Richards对CPL语言进行了简化,于是产生了BCPL(Basic Combined Programming Language)语言。
20世纪60年代,美国AT&T公司贝尔实验室(AT&T Bell Laboratories)的研究员肯·汤普森(Kenneth Lane Thompson)闲来无事,手痒难耐,想玩一个他自己编的,模拟在太阳系航行的电子游戏——Space Travel。他背着老板,找到了台空闲的小型计算机——PDP-7。但这台电脑没有操作系统,而游戏必须使用操作系统的一些功能,于是他着手为PDP-7开发操作系统。后来,这个操作系统被命名为——UNICS(Uniplexed Information and Computing Service)。
1969年,美国贝尔实验室的Ken Thompson,以BCPL语言为基础,设计出很简单且很接近硬件的B语言(取BCPL的首字母),并且用B语言写了初版UNIX操作系统(叫UNICS)。
1971年,同样酷爱Space Travel的丹尼斯·里奇为了能早点儿玩上游戏,加入了汤普森的开发项目,合作开发UNIX。他的主要工作是改造B语言,使其更成熟。 [6]
1972年,美国贝尔实验室的丹尼斯·里奇在B语言的基础上最终设计出了一种新的语言,他取了BCPL的第二个字母作为这种语言的名字,这就是C语言。
1973年初,C语言的主体完成。汤普森和里奇迫不及待地开始用它完全重写了UNIX。此时,编程的乐趣使他们已经完全忘记了那个“Space Travel”,一门心思地投入到了UNIX和C语言的开发中。随着UNIX的发展,C语言自身也在不断地完善。直到2020年,各种版本的UNIX内核和周边工具仍然使用C语言作为最主要的开发语言,其中还有不少继承汤普逊和里奇之手的代码。 [6]
在开发中,他们还考虑把UNIX移植到其他类型的计算机上使用。C语言强大的移植性(Portability)在此显现。机器语言和汇编语言都不具有移植性,为x86开发的程序,不可能在Alpha、SPARC和ARM等机器上运行。而C语言程序则可以使用在任意架构的处理器上,只要那种架构的处理器具有对应的C语言编译器和库,然后将C源代码编译、连接成目标二进制文件之后即可在哪种架构的处理器运行。 [6]
1977年,丹尼斯·里奇发表了不依赖于具体机器系统的C语言编译文本《可移植的C语言编译程序》。
C语言继续发展,在1982年,很多有识之士和美国国家标准协会(ANSI)为了使C语言健康地发展下去,决定成立C标准委员会,建立C语言的标准。委员会由硬件厂商、编译器及其他软件工具生产商、软件设计师、顾问、学术界人士、C语言作者和应用程序员组成。1989年,ANSI发布了第一个完整的C语言标准——ANSI X3.159-1989,简称“C89”,不过人们也习惯称其为“ANSI C”。C89在1990年被国际标准化组织(International Standard Organization,ISO)一字不改地采纳,ISO官方给予的名称为:ISO/IEC 9899,所以ISO/IEC9899:1990也通常被简称为“C90”。1999年,在做了一些必要的修正和完善后,ISO发布了新的C语言标准,命名为ISO/IEC 9899:1999,简称“C99”。 [6]在2011年12月8日,ISO又正式发布了新的标准,称为ISO/IEC9899:2011,简称为“C11”。
3.3 C语言的应用
广泛应用于底层开发。C语言能以简易的方式编译、处理低级存储器。C语言是仅产生少量的机器语言以及不需要任何运行环境支持便能运行的高效率程序设计语言。尽管C语言提供了许多低级处理的功能,但仍然保持着跨平台的特性,以一个标准规格写出的C语言程序可在包括类似嵌入式处理器以及超级计算机等作业平台的许多计算机平台上进行编译。
3.4 C语言的特点和缺点
主要特点
- 简明的语言
- 具有结构化的结构语句
- 丰富的数据类型
- 丰富的运算符
- 可对物理地址进行直接操作
- 代码具有较好的可移植性
- 可生产高质量、目标代码执行效率高的程序
特有特点
- 广泛性
- 简洁性
- 结构完善
缺点
-
C语言的缺点主要表现为数据的封装性弱,这一点使得C在数据的安全性上有很大缺陷,这也是C和C++的一大区别。
-
C语言的语法限制不太严格,对变量的类型约束不严格,影响程序的安全性,对数组下标越界不作检查等。从应用的角度,C语言比其他高级语言较难掌握。也就是说,对用C语言的人,要求对程序设计更熟练一些。
一、Visual Studio 安装
**预先攻其事必先利其器。**写代码也是如此,需要寻找一款“趁手”的 IDEA 来进行开发。
Visual Studio 2022 系统要求:
https://learn.microsoft.com/zh-cn/visualstudio/releases/2022/system-requirements
受支持的操作系统
在以下 64 位操作系统上支持 Visual Studio 2022:
- Windows 11 最低支持的 OS 版本或更高版本:家庭版、专业版、专业教育版、专业工作站版、企业版和教育版
- 可在此处找到支持的 Windows 11 OS:Windows 11 企业版和教育版支持
- Windows 10 最低支持的 OS 版本或更高版本:家庭版、专业版、教育版和企业版。
- 可在此处找到支持的 Windows 10 OS:Windows 10 企业版和教育版支持
- Windows Server Core 2022
- Windows Server Core 2019
- Windows Server 核心 2016
- Windows Server 2022:Standard 和 Datacenter。
- Windows Server 2019:Standard 和 Datacenter。
- Windows Server 2016:Standard 和 Datacenter。
不支持以下各项:
32 位和 ARM32 操作系统。
Windows 11 家庭版 S 模式、Windows IoT 企业版、Windows 10 IoT 核心板、Windows 10 企业版 LTSC 版本、Windows 10 S 和 Windows 10 团队版。 可以使用 Visual Studio 2022 生成在这些 Windows 版本上运行的应用。
Windows Server 的服务器 IoT 和最小服务器接口选项。
Windows 容器(排除 Visual Studio 生成工具)。
在虚拟机环境中运行,而无完整的 Windows 操作系统。
应用程序虚拟化解决方案,例如 Microsoft App-V for Windows 或 MSIX for Windows 或者第三方应用虚拟化技术。
多个用户在同一计算机上同时使用该软件,包括共享虚拟桌面基础结构计算机或共用 Windows 虚拟桌面主机池。
以下就是 Visual Studio 的安装示例:
Visual Studio 官网地址:
https://visualstudio.microsoft.com/zh-hans/
点击此处跳转
跳转到官网后在导航菜单点击下载
下载社区版
下载完启动器后,双击启动
点击继续,出现以下页面,等待其下载完毕
安装成功
向下方滑动找到使用C++的桌面开发选项,并勾选该选项
勾选完,点击安装,等待下载完毕,即可使用。
安装完成后,启动应用,点击启动 Visual Studio 即可进入。
以上就是完整的安装示例。
二、C语言之 hello 项目的创建
首先,点击创建新项目选项,进入项目模块页面,选择控制台应用选项。
接着,给把项目名称改为 hello,修改文件存放位置,点击创建即可。
进入以下页面,VS 会自动生成 C++ 的代码
点击运行按钮,VS开始编译运行代码
效果如下:
这时候就有小伙伴疑惑了,我要如何编写C语言的代码呢?
不急请看以下操作:
- 在右侧边栏找到 hello.cpp 的文件,将文件后缀改为 .c
-
再将自动生成的代码全选删除,写入以下代码
#include <stdio.h> // 注释 头文件// 入口函数 int main() {printf("hello world!"); }
-
最后点击运行按钮或按 Ctrl + F5,运行代码即可。
三、C语言基础语法
1.常量与变量
#include <stdio.h>int main() {// %d 占位符// 10 常量+ 20 常量+ 30 常量 在程序运行过程中,其值不会发生改变// 变量:其值可以改变的量被称为变量// 如何定义一个变量 数据类型 变量名 = 值;int a = 10;int b = 20;a = 30; // = 起到赋值的作用,跟数学的 "=" 用法不同printf("总价格为:%d", a + b);/** 变量规则:* 1. 不能以数字开头* 2. 不能以特殊符号开头,下划线 _ 可以* 3. 不能以关键字(标识符)开头,如:int*/}
2.关键字
cuto | brcak | casc | char |
---|---|---|---|
const | continue | default | do |
double | else | enw | extern |
flcat | for | goto | if |
int | long | register | returm |
short | signed | sizeof | static |
struct | switch | typedef | uni on |
unsigned | void | volatile | while |
3.数据类型
3.1. 整型数据
#include <stdio.h> // 引入头文件// 入口
int main() {short bank_sum = 32768;unsigned int band_sum = 4294967297;int price = 1000;long sum = 1000L; // l L 1000L是一个长整型的常量int length = sizeof(sum); // sizeof() 计算数据类型占的字节数printf("总价格 %u", band_sum);int a1 = 123; //十进制int a2 = 0123; //八进制int a3 = 0x123; //十六进制 1-9 a b c d e fint a4 = 0b101; //二进制printf("%d", a4);printf("%o", a2);printf("%x", a3);// printf("%b", a4); 二进制的显示结果要自己写}
int | 4字节 | -2,147,438,648 到 2,147,438,647 |
---|---|---|
unsigned int | 4字节 | 0 到 4,294,967,295 |
short | 2字节 | -32,768 到 32,768 |
unsigned short | 2字节 | 0 到 65,535 |
long | 4字节 | -2,147,438,648 到 2,147,438,647 |
unsigned long | 4字节 | 0 到 4,294,967,295 |
C语言中的占位符
%d 输出十进制的(有符号)整型
%u 输出无符号十进制的整数
%o 输出八进制(有符号)的整数
%x 输出十六进制(有符号)的整数
%c 输出字符
%s 输出字符串
%p 输出指针地址
拓展:
long 和 int 的区别
在16位系统中:long是4字节,int是2字节
在32位系统中:long是4字节,int是4字节
在64位系统中:long是4字节,int是4字节
指针的长度默认是 unsigned long
3.2 浮点型数据
#include <stdio.h> // 引入头文件// 入口
int main() {// 浮点型(实型)int price = 12;//float totalPrice = price * 0.9;//double totalPrice = price * 0.9;double num1 = 10 / 3; // 结果为3.00000double num2 = 10.0 / 3; // 结果为3.33333// 10 和 10.0 是两个不同数据类型的数,10是整型、10.0是浮点型,导致进行数学运算的结果不同。float a = 0.1f;// %f 输出单精度浮点数// %lf 输出双精度浮点数printf("价格 %f", num2);
}
关键字 | 字节 | 数值范围 |
---|---|---|
float | 4字节 | 3.4E-38 到 3.4E+38 |
double | 8字节 | 1.7E-308 到 1.7E+308 |
3.3 字符串类型
#include <stdio.h> // 引入头文件// 入口
int main() {// 字符串类型printf("欢迎谢小然回来\n"); // 字符串常量/** 如何让一行字符进行换行?* 使用转义字符 \n*/char a = 'a'; // 单引号里面只能放一个字符// 双引号 其实是两个字符,占两个字节printf("字符是 %c", a);// %c 输出字符// 怎么存汉字? 汉字占两个字节// 怎么存多个字符?char str[] = "谢小然"; // 字符串要用双引号printf("字符串是 %s", str); // %s 输出字符串常量
}
常用的转义字符
\n 换行 \t 空格 \0 空字符(NULL)\r 回车 \' 一个单引号 \" 一个双引号
unsigned char | 1字节 | 0 到 255 |
---|---|---|
unsigned char | 1字节 | -128 到 127 |
4.运算符
用算数运算符将运算对象(操作数)连接起来的,符合C语法规则的式子,称为C算术表达式,对象包括常量、变量和函数等。
运算符的分类:
1、双目运算符:即参加运算的操作数有两个
a * b / c + 6
2、单目运算符:参加运算的操作数只有一个 ++ 自增运算符,给变量值 +1
a++
3、三目运算符:即参加运算的操作符有三个 ()?()😦)
int a = b > c ? 10 : 20
4.1 算术运算符
+,-,*,/,%,+=.-=,/=,%=
#include <stdio.h>int main() {int a = 10;int b = 3;int c = a % b;int time = 1000;int hours = time / 60;int mins = time % 60;printf("%d:%d", hours, mins);}
复合运算符:
a += 1 相当于 a = a + 1;
4.2 关系运算符
>、<、==、>=、<=、!=
#include <stdio.h>
#include <stdbool.h> // 引入该头文件才能使用布尔值类型int main() {int a = 28;//printf("结果是%d", a > 30); // 结果是0/** 比较结果* 1 为真,成立* 0 为假,不成立*/int price = 28;if (price == 28) {printf("符合条件");}bool isMatch = true;printf("%d", isMatch);/** 布尔值类型 boolean true false* C89 没有布尔值类型* C99 增加了布尔值类型*/
}
4.3 逻辑运算符
-
&& 逻辑与
两个条件为真,结果为真
-
|| 逻辑或
一个条件为真另一个条件为假,则结果为真;两个条件都为假,则结果为假
-
! 逻辑非
#include <stdio.h>int main() {//printf("%d\n", 1 < 2 && 3 > 4);// && 同真为真,同假为假//printf("%d", 1 > 2 || 3 > 4);// || 一真为真,同假为假int price = 300; // 总价格int type = 1; // 类型 0 代表食品 1 家电 2 衣服int day = 2; // 6 星期六 7 星期日if (price > 200 && type == 1 && !(day == 6 || day == 7)) {price *= 0.7;printf("%d", price);}// && 短路int gender = 0; // 0 女 1 男gender == 0 && printf("女");// 左真才会执行
}
4.4 三目运算符
- 语法:
条件 ? 条件为true时执行 : 条件为false时执行
#include <stdio.h>int main() {//int age = 20;/*if (age >= 18) {printf("您已成年");}*/int age = 20 >= 18 ? printf("您已经成年") : printf("未成年");
}
4.5 逗号运算符
int a = (1,2)
逗号运算符的结果是后边表达式的结果
#include <stdio.h>int main() {// 定义一个整数,先加一再平方int num = 10;num = (num += 1, num * num);printf("结果是%d", num); // 121
}
4.6 自增自减运算符
++
- 进行自增运算
- 分成两种,前置 ++ 和后置 ++
- 前置 ++,会先把值自动加一,再返回
- 后置 ++,会先把值返回,再自动加一
--
- 进行自减
- 分两种,前置 - - 和后置 - -
- 前置 - -,会先把值自动减一,再返回
- 后置 - -,会先把值返回,再自动减一
#include <stdio.h>int main() {int a = 1;//a++;//++a;int result = 0;result = 10 + a++; // 先计算再自增//result = 10 + ++a; // 先自增再计算printf("%d %d", result, a);
}
4.7 运算符的优先级
运算符优先级
- 在表达式中按照优先级先后进行运算,优先级高的先于优先级低的运算
- 优先级一样的按结构性来运算
运算符结构性
左结合性:从左向右运算
sum = x + y + z;
右结合性:从右向左运算
#include <stdio.h>int main() {int a, b, c, d;a = b = c = d = 100;a += b += c += d += 100;printf("%d %d %d %d", a, b, c, d); // 500 400 300 200
}
5.分支语句
5.1 if 分支结构
if 语句
通过一个 if 语句来决定代码是否执行
语法: if (判断条件) {要执行的代码}
if else 语句
通过 if 条件来决定执行哪一个 {} 里面的代码
语法: if (判断条件) {条件为true时要执行的代码} else {条件为false时要执行的代码}
if else if 语句
通过 if 和 else if 来设置多个条件进行判断
语法: if(条件1) {条件1为true时要执行的代码} else if (条件2) {条件2为true时要执行的代码}
#include <stdio.h>int main() {//int age = 18;//if (age >= 18) {// // 条件达成// printf("您已成年");//}//else {// // 条件未达成// printf("您还未成年");//}int price = 100; // 满200减20 满100减10if (price >= 200) {price -= 20;printf("%d", price);}else if (price >= 100) {price -= 10;printf("%d", price);}else {printf("%d", price);}
}
案例
#include <stdio.h>int main() {/*案例1:判断奇偶数被 2 整除的是偶数;不能被 2 整除的是奇数*/int num = 0;printf("请输入数字\n");scanf_s("%d", &num);/*if (num % 2 == 0) {printf("%d是偶数", num);}else {printf("%d是奇数", num);}*/if (!(num % 2)) {printf("%d是偶数", num);}else {printf("%d是奇数", num);}/* 案例2:根据0~100的数字输出成绩[90, 100] 输出A[80, 90) 输出A[70, 80) 输出A[60, 70) 输出A[0, 60) 输出A*/int score = 0;printf("请输入成绩\n");scanf_s("%d", &score);if (score >= 90) {printf("A");} else if (score >= 80) {printf("B");}else if (score >= 70) {printf("C");}else if (score >= 60) {printf("D");}else {printf("E");}/*案例3:判断闰年什么是闰年?世纪闰年:公历年份是整百数的,必须是400的倍数才是闰年普通闰年:公历年份是4的倍数,且不是100的倍数世纪闰年: year % 400 == 0普通闰年: year % 4 == 0 && year % 100 != 0*/int year = 0;printf("请输入年份\n");scanf_s("%d", &year);if (year % 400 == 0 || year % 4 == 0 && year % 100 != 0) {printf("%d是闰年", year);}else {printf("%d是平年", year);}
}
5.2 switch 条件分支结构
-
对于某一个变量的判断
-
语法:
switch (要判断的变量) {case 情况1:情况1要执行的代码break;case 情况2:情况2要执行的代码break;default:上述情况不满足时执行的代码 }
案例
#include <stdio.h>int main() {/*案例:根据成绩匹配相应的等级*/int score = 0;printf("请输入成绩0——100: \n");scanf_s("%d", &score);int flag = score / 10;switch (flag) {case 10:case 9:printf("A");break;case 8:printf("B");break;case 7:printf("C");break;case 6:printf("D");break;default:printf("E");}}
6.循环结构
- 循环结构,就是根据某些给出的条件,重复执行同一段代码
- 循环必须要有某些固定的内容组成
- 初始化
- 条件判断
- 要执行的代码
- 自身改变
6.1 while 循环
语法: while (条件) {满足条件就执行}
因为满足条件就执行,所以我们写的时候一定要注意,设定一个边界值,否则程序会一直执行下去
#include <stdio.h>int main() {// 初始条件int n = 0;while (n < 10) {printf("%d\n", n);n++; // 调整步长}}#include <stdio.h>int main() {// 案例1 利用 while 循环计算1-100之间的和/*int sum = 0;int num = 1;while (num <= 100) { sum += num;num++;}printf("1-100之间的和为:%d", sum);*/// 案例2 利用 while 循环算数的阶乘printf("请输入数字:\n");int num = 0; // 3! = 3*2*1scanf_s("%d", &num);int temp = num;int f = 1;while (num >= 1) {f *= num;num--;}printf("%d的阶乘是:%d", temp, f);
}
6.2 do while 循环
do while 循环,先执行一次代码,然后再进行条件判断
语法: do {要执行的代码} while (条件)
#include <stdio.h>int main() {int n = 1;int sum = 0;do {sum += n;n++;} while (n <= 100);printf("%d", sum);
}
6.3 for 循环
语法: for (int i = 0; i < 10; i++) {要执行的代码}
#include <stdio.h>int main() {// 案例1:利用 for循环计算1-100的和/*int sum = 0;for (int i = 1; i <= 100; i++) {sum += i;}*///printf("%d", sum);//案例2:求用户输入数字的阶乘/*printf("请输入数字:\n");int num = 0;scanf_s("%d", &num);int sum = 1;for (int i = num; i >= 1; i--) {sum *= i;}printf("%d", sum);*/// 案例3:计算1000-2000年之间所有的闰年// 世纪闰年 是400的整倍数// 普通闰年 是4的倍数且不能被100整除/*for (int i = 2000; i <= 2023; i++) {if (i % 400 == 0 || i % 4 == 0 && i % 100 != 0) {printf("%d是闰年\n", i);}}*/// 案例4:计算用户输入某年到某年之间所有的闰年printf("请输入开始年份与结束年份:\n");int beginYear = 0;int endYear = 0;scanf_s("%d%d", &beginYear, &endYear);int count = 0;for (int i = beginYear; i <= endYear; i+=4) {if (i % 400 == 0 || i % 100 != 0) {printf("%d是闰年 ", i);count++;if (count % 4 == 0) {printf("\n");}}}//案例5:求质数 质数除了一和本身没有其他的约数printf("请输入数字:\n");int num = 0;scanf_s("%d", &num);int flag = true;for (int i = 2; i < num; i++) {if (num % i == 0) {flag = false;}}printf("%d", flag);if (flag) {printf("%d是质数", num);}else {printf("%d不是质数", num);}int i;for (i = 2; i <= num / 2; i++) {if (num % i == 0) {break;}}if (i <= num / 2) {printf("%d不是质数", num);}else {printf("%d是质数", num);}// 案例6:打印九九乘法表/* * 1*1=1 * 1*2=2 2*2=4 * 1*3=3 2*3=6 3*3=9 * 1*4=4 2*4=8 3*4=12 4*4=16 * 1*5=5 2*5=10 3*5=15 4*5=20 5*5=25 * 1*6=6 2*6=12 3*6=18 4*6=24 5*6=30 6*6=36 * 1*7=7 2*7=14 3*7=21 4*7=28 5*7=35 6*7=42 7*7=49 * 1*7=7 2*7=14 3*7=21 4*7=28 5*7=35 6*7=42 7*7=49 * 1*8=8 2*8=16 3*8=24 4*8=32 5*8=40 6*8=48 7*8=56 8*8=64 * 1*9=9 2*9=18 3*9=27 4*9=36 5*9=45 6*9=54 7*9=63 8*9=72 9*9=81 */for (int i = 1; i <= 9; i++) {for (int j = 1; j <= i; j++) {printf("%d*%d=%d\t",j,i,i*j);}printf("\n");}
}
循环控制语句
- break:跳出本次循环
- continue:跳过本次循环,接着执行下面的代码
7.初始函数
7.1 函数的概念
函数就是把任意一段代码包装起来,在需要执行的时候,就调用这个函数
#include <stdio.h>// 定义一个函数
sayHello() {printf("hello");
}int main() {sayHello(); // 调用函数
}
7.2 函数的参数
在定义函数是出现的()
是存放参数的位置,参数分为实参和形参。
void function(形参写这里) {// 一段代码
}function(实参写这里)
形参与实参的作用
- 形参
- 在函数内部可以使用的变量,外部不能调用
- 形参可以同时定义多个,用
,
隔开
- 实参
- 在调用函数时给形参赋值
- 函数内部的形参的值,由函数调用的时候传递的实参决定
- 多个形参时,是按顺序一一对应
#include <stdio.h>// 单个形参
isEven(int num) {if (num % 2 == 0) {printf("%d是偶数\n", num);}else {printf("%d是奇数\n", num);}
}// 两个或多个形参
getSum(int x, int y, int z) {int sum = 0;sum = x + y + z;printf("%d", sum);
}int main() {isEven(3);isEven(4);isEven(89);getSum(10, 90, 454);
}
7.3 函数的返回值
return
返回的意思,其实就是给函数一个返回值 和 中断函数
返回值
-
函数调用本身也是一个表达式,表达式就应该有一个值出现
-
return
关键字就是可以给函数执行完毕一个结果#include <stdio.h>// 两个或多个形参 int getSum(int x, int y, int z) {int sum = 0;sum = x + y + z;//printf("%d", sum);return sum; }int main() {int mySum = getSum(10, 20, 30);printf("%d", mySum); // 结果为 60 }
中断函数
- 当函数开始执行时,函数内部的代码会自上而下依次运行
- 必须等到函数内代码执行完毕
return
关键字可以将函数中断,在return
之后的代码将不再执行
7.4 函数的声明
为什么要声明函数?
编译器在编译C语言时,程序是从上到下执行的,如果没有对函数进行声明,则编译器不认识该函数。
-
直接声明法
#include <stdio.h>// 直接声明法 void sayHello();int main() {sayHello(); }void sayHello() {printf("hello"); }
-
间接声明法
将函数的声明放在头文件中,.c文件中包含头文件即可
main.c #include "k.h"k.h extern void func(void);
使用函数的好处?
- 定义一次,可以多次调用,减少代码的冗余度。
- 使代码模块化更好,方便调试程序,方便阅读。
7.5 函数的内存分区
1. 内存:物理内存、虚拟内存
- 物理内存:实实在在存在的存储设备
- 虚拟内存:操作系统虚拟出来的内存。
- 操作系统会在物理内存和虚拟内存之间做映射。
- 在写应用程序的,咱们看到的都是虚拟地址。
2. 在运行程序的时候,操作系统会将 虚拟内存进行分区。
1). 堆
在动态申请内存的时候,在堆里开辟内存。
2). 栈
主要存放局部变量。
3). 静态全局区
1:未初始化的静态全局区
静态变量(定义变量的时候,前面加 static修饰),或全局变量,没有初始化的,存在此区
2:初始化的静态全局区
全局变量、静态变量、赋过初值的,存放此区
4). 代码区
存放程序代码
5). 文字常量区
存放常量
7.6 普通全局变量
在函数外部定义的变量成为全局变量
int num = 100; // 这就是一个全局变量int main() {return 0;
}
作用范围:
- 普通全局变量的作用范围,是程序的所有地方。
- 只不过用之前需要声明。声明方法 extern int number;
- 注意声明的时候,不要赋值。
生命周期:
- 程序运行的整个过程,一直存在,直到程序结束。
注意:
- 定义普通的全局变量的时候,如果不赋初值,它的值默认为0
7.7 静态全局变量 static
static int num = 100; // 这就是一个静态全局变量int main() {return 0;
}
作用范围: 只能在它定义的.c(源文件)中有效
生命周期:在程序的整个运行过程中,一直存在
注意:定义静态全局变量的时候,如果不赋值,它的默认值为0
7.8 普通局部变量
在函数中定义或在复合语句中定义的变量成为局部变量
int main() {int num; //普通局部变量if (1) {int a; // 普通局部变量}
}
作用范围:
- 在函数中定义的变量,在它的函数中有效。
- 在复合语句中定义的,在它的复合语句中有效。
7.9 静态的局部变量
定义局部变量的时候,前面加 static 修饰作用范围:在它定义的函数或复合语句中有效。
生命周期:第一次调用函数的时候,开辟空间赋值,函数结束后,不释放,以后再调用函数的时候,就不再为其开辟空间,也不赋初值,用的是以前的那个变量。
7.10 静态函数
在定义函数的时候,返回值类型前面加 static 修饰,这样的函数被称为静态函数。
static 限定了函数的作用范围,在定义的.c 文件中有效。
8.数组
8.1 数组的概念
数组是若干个行同类型的变量在内存中有序存储的集合。
8.2 数组的分类
-
字符数组
char s[10];
-
短整型数组
shor a[10];
-
整形数组
int a[10];
-
长整型数组
long a[5];
-
浮点型数组
float a[6]; a[4] = 3.14f;
``double a[8]; a[7] = 3.115926;`
-
指针数组
char *a[10]
-
结构体数组
struct student a[10]
8.3 二维数组
数组名 [行下标] [列下标]
int a[3][3]
#include <stdio.h>int main() {int grades[2][2] = {{10,20},{30,40}};printf("%d\n", grades[0][1]);char str[][100] = { "张三", "李四", "Mike" };for (int i = 0; i < sizeof(str) / (sizeof(char) * 100); i++) {printf("%s\n", str[i]);}printf("%d\n", str[0][2]);
}
8.4 冒泡排序
- 先遍历数组,让挨着的两个进行比较,如果前一个比后一个大,那么就把两个换个位置。
- 数组遍历一遍以后,那么最后一个数字就是最大的那个了。
- 然后进行第二遍的遍历,还是按照之前的规则,第二大的数字就会跑到倒数第二的位置。
- 以此类推,最后就会按照顺序把数组排好了。
#include <stdio.h>int main() {int arr[10] = { 4,1,6,78,64,88,67,32,70,23 };for (int i = 0; i < sizeof(arr) / sizeof(int) - 1; i++) {for (int j = 0; j < sizeof(arr) / sizeof(int) - i - 1; j++) {if (arr[j + 1] > arr[j]) {int temp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = temp;}} }for (int i = 0; i < sizeof(arr) / sizeof(int); i++) {if (i == 0) {printf("{%d,", arr[i]);}else if (i == sizeof(arr) / sizeof(int) - 1) {printf("%d}", arr[i]);}else {printf("%d,", arr[i]);}}}
8.5 选择排序
- 先假定数组中的第0个是最小数字的索引
- 遍历数组,只要找到比第0个数字小的数,替换之前记录的索引
- 直到数组遍历完后,找到最小数字的索引,就将其与第0个数的位置交换
- 再进行第二次遍历,假定第1个数是最小的数
- 找到比第1个数小的数,再次替换之前记录的索引,将其与第1个数交换位置
- 依此类推即可
#include <stdio.h>int main() {int arr[] = { 21,3,5,76,32,65,11,45,1,54 };int index = 0; // 假定第0个位置对应的数字是最小的//for (int i = 1; i < sizeof(arr) / sizeof(int); i++) {// if (arr[i] < arr[index]) {// index = i;// //printf("%d", index);// }//}//int temp = arr[0];//arr[0] = arr[index];//arr[index] = temp;//int index2 = 1; // 假定第1个位置对应的数字是最小的//for (int i = 2; i < sizeof(arr) / sizeof(int); i++) {// if (arr[i] < arr[index2]) {// index2 = i;// //printf("%d", index);// }//}//int temp2 = arr[1];//arr[1] = arr[index2];//arr[index2] = temp2;for (int i = 0; i < sizeof(arr) / sizeof(int); i++) {int index = i;for (int j = i + 1; j < sizeof(arr) / sizeof(int); j++) {if (arr[index] > arr[j]) {index = j;}}int temp = arr[i];arr[i] = arr[index];arr[index] = temp;}for (int i = 0; i < sizeof(arr) / sizeof(int); i++) {if (i == 0) {printf("{%d,", arr[i]);}else if (i == sizeof(arr) / sizeof(int) - 1) {printf("%d}", arr[i]);}else {printf("%d," , arr[i]);}}
}
9.指针
9.1 指针的概念
系统给虚拟内存的每个存储单元分配了一个编号,从0x00 00 00 00 ~ 0xff ff ff ff
这个编号咱们称之为地址,指针就是地址
指正变量:用来存放地址编号的变量
在32位平台下,地址总线是32位的,所以地址是32位编号,指针变量是32位的即4个字节
字符变量 char ch = 'b'; ch 占 1 个字节,它有一个地址编号,这个地址编号就是 ch 的地址整型变量 int a = 0x12 34 56 78; a 占 4 个字符,它占有 4 个字节的存储单位,有4个地址编号。
9.2 指针变量的定义
-
简单的指针变量
数据类型 *指针变量名
int *p; // 定义了一个指针变量 p
在定义指针变量的时候 * 是用来修饰变量的,说明变量 p 是个指针变量
-
关于指针的运算符
& 取地址、* 取值
int a = 0x1234abcd; int *p; // 在定义指针变量时,*代表修饰的意思,修饰 p 是个指针变量 p = &a; // 把 a 的地址给 p 赋值,&是取地址符。
9.3 指针的用处
- 动态内存分配:指针可以用来在运行时动态分配内存。
- 传递数组:指针可以用来传递数组,从而使函数能够操作数组中的元素。
- 实现链表、树等数据结构:指针可以用来实现链表、树等数据结构。
- 指向函数:指针可以指向函数,从而实现回调函数等功能。
#include <stdio.h>void swap(int *num1, int *num2) {int temp = *num1;*num1 = *num2;*num2 = temp;
}void soft(int *myArr, int length) { for (int i = 0; i < length - 1; i++) {for (int j = 0; j < length - i - 1; j++) {if (myArr[j] > myArr[j + 1]) {// 调用交换数值函数swap(&myArr[j], &myArr[j + 1]);}}}
}void printArray(int *myArr, int length) {for (int i = 0; i < length; i++) {if (i == 0) {printf("{%d,", myArr[i]);}else if (i == length - 1) {printf("%d}", myArr[i]);}else {printf("%d,", myArr[i]);}}
}int main() {int arr[] = { 2,34,5,123,675,23,132,1,4,87 }; // arr 拿到的是数组最开始数字的地址值// 调用排序函数soft(arr, sizeof(arr) / sizeof(int));// 调用打印数组函数printArray(arr, sizeof(arr) / sizeof(int));printf("\n%p\n", arr); // arr &arr &arr[0] 存的是数组第0个数字的指针地址printf("%p\n", &arr);printf("%p\n", &arr[0]);printf("%p\n", &arr[1]); // &arr[1] 存的是数组第1个数字的指针地址}
9.4 指针和数组
int a[5];
int *p;
p = &a[0];
指针变量 p 保存了数组 a 中第 0 个元素的地址,即 a[0]的地址
通过指针变量运算加取值的方法来引用数组的元素
#include <stdio.h>void swap(int* num1) {int temp = *num1;*num1 = *(num1 + 1);*(num1 + 1) = temp;
}void soft(int* myArr, int length) {for (int i = 0; i < length - 1; i++) {for (int j = 0; j < length - i - 1; j++) {if (myArr[j] > myArr[j + 1]) {// 调用交换数值函数swap(&myArr[j]);}}}
}void printArray(int* myArr, int length) {for (int i = 0; i < length; i++) {if (i == 0) {printf("{%d,", myArr[i]);}else if (i == length - 1) {printf("%d}", myArr[i]);}else {printf("%d,", myArr[i]);}}
}int main() {//int arr[] = { 11,22,33 };//int* p = arr;//printf("%p\n", arr);//printf("%p\n", &arr);//printf("%p\n", &arr + 1); // 跳过整个数组的长度//printf("%p\n", arr + 1);//printf("%p\n", p + 1);//printf("%d\n", *(p + 1));int arr[] = { 2,34,5,123,675,23,132,1,4,87 }; // arr 拿到的是数组最开始的地址值// 调用排序函数soft(arr, sizeof(arr) / sizeof(int));// 调用打印数组函数printArray(arr, sizeof(arr) / sizeof(int));}
9.5 指针的分类
按指针指向的数据的类型来分
- 字符指针
字符型数据的地址
char *p;//定义了一个字符指针变量,只能存放字符型数据的地址编号
- 短整型指针
short *p;//定义了一个短整型的指针变量 p,只能存放短整型变量的地址
- 整型指针
int *p;//定义了一个整型的指针变量 P,只能存放整型变量的地址
- 长整型指针
1ong *p;//定义了一个长整型的指针变量 P,只能存放长整型变量的地址
- float型的指针
float *p;//定义了一个 float 型的指针变量 p,只能存放 float 型变量的地址
- double 型的指针
double *p;//定义了一个 double 型的指针变量 p,只能存放 double 型变量的地址
-
函数指针
-
结构体指针
-
指针的指针
-
数组指针 T
-
通用指针 void *p;
无论什么类型的指针变量,在 32位系统下,都是4个字节。
指针只能存放对应类型的变量的地址编号。
9.6 指针与字符串
#include <stdio.h>int main() {/*int arr[] = { 11,22,33 };int* p = { 11,22,33 };*//*char ch[] = { 'x','x','r' };char *p = { 'x','x','r' };*/char ch[] = "xxr";char* p = ch;/**(p + 2) = 'x';printf("%s", ch);*/char* p1 = "xxr"; // 可读printf("%c", *(p1 + 1));char* p1 = "xxr"; // 不可写printf("%c", *(p1 + 1));
}
文字常量区里的内容是不可被修改的
9.7 指针数组
定义一个数组,数组有若干个相同类型指针变量,这个数组被称为指针数组int *p[5]
指针数组本身就是个数组,是若干个相同类型的指针变量构成的集合
#include <stdio.h>int main() {char* str1 = "xxr";char* str2 = "Aclicse";char* str3 = "gy";//char* str[3] = { str1,str2,str3 };char* str[3] = { "xxr","Aclicse","gy" };/*for (int i = 0; i < sizeof(str) / sizeof(char*); i++) {printf("%p\n", str[i]);}*/for (int i = 0; i < sizeof(str) / sizeof(char*); i++) {printf("%s\n", str[i]);}int arr1[] = { 10,20,30 };int arr2[] = { 40,50,60 };int arr3[] = { 1,2,3 };int* arr[] = { arr1,arr2,arr3 }; // 数组arr本身就是一个指针地址,存放的是数组第0个数字的地址for (int i = 0; i < sizeof(arr) / sizeof(int*); i++) {//printf("%d\n", arr[i][0]);printf("%d\n", *(arr[i] + 1));}
}
9.8 指针的指针
指针的指针,即指针的地址 指针占8个字节
int a = 0x12345678;
假如:a的地址是 0x00000002
int *p = &a;
则 p 中存放的是 a 的地址编号 即0x00000002
假如:p的地址是 0x00000003
int **q = &p; // q 保存了 p 的地址
q 中存放的是 p 的地址编号 即0x00000003
9.9 数组指针
本身是个指针,指向一个数组,加1跳下个数组即指向下个数组,占8个字节
int (*p)[5] 指向数组的类型 (*指针变量名) [指向的数组的元素个数]
假设数组a中的第0个元素的地址为1000,那么每一个一维数组的首地址如下图所示:
#include <stdio.h>int main() {// 数组指针,至少指向二维数组int arr[][3] = {{10,20,30}, // 一班{40,50,60}, // 二班{70,80,90} // 三班};/* arr 存放的是第一行的地址*arr 存放的是第一行第一列的地址**arr 第一行第一列元素arr[0][0]的值*///printf("%d\n", *(*(arr)+1));/*int aa 存放第0个元素的地址 *a 取到第0个元素的值a+1 第一行的地址*(a+1) 第一行第一列的地址*(a+1)+1 第一行第二列的地址*(*(a+1)+2) 第一行第二列元素a[1][2]的值 */int(*p)[3] = arr; // 指向二维数组的指针for (int i = 0; i < 3; i++) {for (int j = 0; j < 3; j++) {//printf("%d\n", arr[i][j]);printf("%d\n", *(*(p+i)+j));}}
}
9.10 数组名字取地址
变成 数组指针
#include <stdio.h>int main() {int arr[] = { 1,2,3,4,5 };printf("%p\n", arr);printf("%p\n", &arr);printf("%p\n", arr+1);printf("%p\n", &arr+1);int(*p)[5] = &arr;printf("%d", (*p)[0]);//for (int i = 0; i < 5; i++) {// //printf("%d", arr[i]);// printf("%d\n", (*p)[i]);//}
}
arr 和 &arr 所代表的地址编号是一样的,即它们指向同一个存储单元,但是 arr 和 &arr 的指针类型不同
arr 是个 int* 类型的指针,是arr[0]的地址,&arr 变成了数组指针,加1跳一个 10 元素的整型一维数组
9.11 数组名字和指针变量的异同
int a[5];
int *p = a;
相同点:
a 是数组的名字,也是a[0]的地址,把a赋值给p,即p保存了a[0]的地址,即 a 和 p 都指向 a[0],所以在引用数组元素时,a 和 p 等价引用数组元素。
不同点:
- a 是常量,p 是变量
- 对 a 取地址与对 p 取地址结果不同
因为 a 是数组名字,所以对a取地址结果是数组的指针
p是指针变量,对p取地址结果是指针的指针
9.12 给函数传指针参数
要想改变主调函数中变量的值,必须传变量的地址,而且还得通过*+地址去赋值。无论这个变量是什么类型的。
#include <stdio.h>void func(int *a) {*a = 200;
}void func2(char **a) {*a = "gy";
}void func3(int *arr) {*(arr + 1) = 55;
}void func4(int (*arr)[2]) {arr[1][1] = 66;
}void func5(int** arr) {arr[0][1] = 77;
}int main() {int x = 100;int* p = &x;func(p);printf("%d\n", x);char* str = "xxr";func2(&str);printf("%s\n", str);int arr[3] = { 11,22,33 };func3(arr);printf("%d\n", arr[1]);int arr2[2][2] = {{11,22},{33,44}};func4(arr2);printf("%d\n", arr2[1][1]);int arr3[3] = { 11,22,33 };int arr4[3] = { 44,55,66 };int *arr5[] = { arr3,arr4 };func5(arr5);printf("%d\n", arr3[1]);
}
9.13 函数返回值是指针
#include <stdio.h>typedef int(*xxrerwei)[2]; // 给基础的数据变量定义新名字int* func1() {static int a = 100;int* p = &a;return p;
}int* func2() {static int arr[] = { 11,22,33 };return arr;
}//int (*func3())[2] { // 定义数组指针返回值类型名字太复杂了,用 typedef 方法给它定义一个新名字
// static int arr[][2] = {
// {11,22},
// {33,44}
// };
//
// return arr;
//}xxrerwei func3() {static int arr[][2] = {{11,22},{33,44}};return arr;
}int** func4() {static int arr1[] = { 11,22 };static int arr2[] = { 33,44 };static int* arr3[] = { arr1,arr2 };return arr3;
}int main() {// 返回值是个指针int* p = func1();printf("%d\n", *p);int* p2 = func2();printf("%d\n", *(p2 + 1));int(*p3)[2] = func3();printf("%d\n", *(*(p3 + 1)+1));int** p4 = func4();printf("%d\n", p4[1][1]);
}
9.14 函数返回指针的作用
#include <stdio.h>char* func() {char* p = "xxr";return p;
}int* swap(int num1, int num2) {int temp = num1; // 定义一个临时变量num1 = num2;num2 = temp;static int arr[2];arr[0] = num1;arr[1] = num2;return arr;
}void soft(int* myArr, int length) {for (int i = 0; i < length - 1; i++) {for (int j = 0; j < length - i - 1; j++) {if (myArr[j] > myArr[j + 1]) {// 调用交换数值函数int *p = swap(myArr[j], myArr[j + 1]);myArr[j] = p[0];myArr[j + 1] = p[1];}}}
}void printArray(int* myArr, int length) {for (int i = 0; i < length; i++) {if (i == 0) {printf("{%d,", myArr[i]);}else if (i == length - 1) {printf("%d}", myArr[i]);}else {printf("%d,", myArr[i]);}}
}int main() {char* p = func();printf("%s\n", p);int arr[] = { 2,34,5,123,675,23,132,1,4,87 }; // arr 拿到的是数组最开始的地址值// 调用排序函数soft(arr, sizeof(arr) / sizeof(int));// 调用打印数组函数printArray(arr, sizeof(arr) / sizeof(int));
}
9.15 初始函数指针
我们定义的函数,在运行程序的时候,会将函数的指令加载到内存的代码段,所以函数也有起始地址。
C语言规定:函数的名字就是函数首地址,即函数的入口地址,就可以定义一个指针变量,来存放函数的地址,这个指针变量就是函数指针变量。
9.16 函数指针的定义和调用
定义:
int max(int x,int y) {...
}int (*p)(int,int); // 定义了一个函数指针变量 p,p 指向的函数p = max;
调用:
(*p)(30,50);
p(30,50);
9.17 函数指针数组
#include <stdio.h>int max(int a, int b) {if (a > b) {return a;}else {return b;}
}int min(int a, int b) {if (a < b) {return a;}else {return b;}
}int add(int a, int b) {return a + b;
}int sub(int a, int b) {return a - b;
}int main() {int (*p[])(int, int) = { add,sub,max,min };int res1 = (*p)(1, 2);printf("%d\n", res1);int res2 = (p[1])(1, 2);printf("%d\n", res2);int res3 = (*(p+2))(11, 22);printf("%d\n", res3);int res4 = (*(p + 3))(11, 22);printf("%d\n", res4);
}
9.18 函数指针的用处
#include <stdio.h>void myShow(int* video);// 库函数
void gstreamerParVideo(int *originVideoData, void (*p)(int *videoData)) {//处理数据for (int i = 0; i < 3; i++) {originVideoData[i] += 100;}// 回调函数传入解析后的数据p(originVideoData);
}//自己封装的函数
void parseVideo(int *video) {//引入gstreamerPar库gstreamerParVideo(video, myShow);
}//自己封装一个回调函数
void myShow(int * videoData) {for (int i = 0; i < 3; i++) {printf("%d\n", videoData[i]);}
}int main() {// 调用 gstreamerParVideo 库int arr[] = { 11,22,33 };parseVideo(arr);
}
9.19 特殊指针
-
空类型指针 (void*)
void* 通用指针,任何类型的指针地址都可以给 void*类型的指针变量赋值
-
空指针 NULL
char *p = NULL;
p 哪里都不指向,也可以认为 p 指向内存为 0 的存储单位。
#include <stdio.h>// 宏定义
#define XXR_INT 1
#define XXR_FLOAT 2void swap(void* a, void* b,int type) {if (type == XXR_INT) {int temp = *(int *)a;*(int*)a = *(int*)b;*(int*)b = temp;}else if (type == XXR_FLOAT) {float temp = *(float*)a;*(float*)a = *(float*)b;*(float*)b = temp;}
}int main() {// 空指针char* p = NULL;printf("%p\n", p);// 空类型指针int num = 100;void* vp = # // 只能保存指针的值,不能记录指针指向的类型printf("%d\n", *(int *)vp);int a = 200;int b = 300;float c = 1.1f;float d = 2.2f;swap(&a, &b, XXR_INT);swap(&c, &d, XXR_FLOAT);printf("%d\n", a);printf("%d\n", b);printf("%f\n", c);printf("%f\n", d);
}
四、动态内存申请
1. 初始动态内存
C语言提供了一些内存管理函数,这些内存管理函数可以按需要动态的分配内存空间,也可把不再使用的空间回收再次利用。
静态分配
- 在程序编译或运行过程中,按事先规定大小分配内存空间的分配方式。 int a[10]
- 必须事先知道所需空间的大小。
- 分配在栈区或全局变量区,一般数组的形式。
- 按计划分配
动态分配
- 在程序运行过程中,根据需要大小自由分配所需空间
- 按需分配
- 分配在堆区,一般使用特定的函数进行分配。
2. malloc 函数
void * malloc(int size)
在内存的动态存储区(堆区)中分配一块长度为 size 字节的连续区域,用来存放类型说明字符指定的类型。函数原型返回 void*指针,使用时必须做相应的强制类型转换。
返回值:
- 分配空间的起始地址(分配成功)
- NULL(分配失败)
注意:
- 在调用 malloc 之后,一定要判断一下,是否申请内存成功。
- 如果多次 malloc 申请的内存,第1次和第2次申请的内存不一定是连续的
3. free 函数(释放内存函数)
free 函数释放 p 指向的内存
char *p = (char *)malloc(100);
free(p);
4. calloc 函数
在内存中,申请 n 块,每块的大小为 size 个字节的连续区域
函数的返回值:
- 返回申请的内存的首地址(申请成功)
- 返回 NULL(申请失败)
注意:malloc 和 calloc 函数都是用来申请内存的。
区别:
- 函数名字不一样。
- 参数个数不一样。
- malloc 申请的内存,内存中存放的内容是随机的,不确定的,而 calloc 函数申请的内存中的内容为0
5. realloc 函数
在原先s指向的内存基础上重新申请内存,新的内存的大小为 new_size 个字节,如果原先内存后面有足够大的空间,就追加,如果后边的内存不够用,则relloc 函数会在堆区找一个 newsize 个字节大小的内存申请,将原先内存中的内容拷贝过来,然后释放原先的内存,最后返回 新内存的地址。
6. 内存泄露
申请的内存,首地址丢了,找不了,再也没法使用了,也没法释放了,这块内存就被泄露。
int main() {char *p;p = (char *)malloc(100);// 接下来,可以用 p 指向的内存了p = "xxr"; // p 指向别的地方了// 从此以后,再也找不到你申请的 100 个字节,则动态申请的 100 个字节就被泄露了
}
void func() {char *p;p = (char *)malloc(100);
}int main() {func();func();
}
// 每一次调用 fun 函数 泄露 100 个字节
五、字符串处理函数
1. 字符串拷贝函数 strcpy_s
拷贝 scr 指向的字符串到 dest 指针指向的内存中,'\0'也会拷贝
#include <stdio.h>
#include <stdlib.h>
#include <string.h>int main() {char* p = (char*)malloc(100);char* str = "xxr";if (!p) return;/*for (int i = 0; i < 4; i++) {p[i] = str[i];}*//*printf("%s", p);*/// strcpy_s(p, 7, str);strncpy_s(p, 7, str, 2);printf("%s", p);free(p);
}
2. 测字符串长度函数 strlen
测字符指针 s 指向的字符串中字符的个数,不包括'\0'
#include <stdio.h>
#include <string.h>
#include <stdlib.h>int main() {char p1[] = "xxr";char* p2 = "xxr";printf("%lld\n", sizeof(p1) / sizeof(char));printf("%lld\n", sizeof(p2));printf("%zd\n", strlen(p1));printf("%zd\n", strlen(p2));char* p = (char*)malloc(100);if (!p) return;strcpy_s(p, strlen(p2) + 1, p2);printf("%s", p);free(p);
}
3. 字符串追加函数 strcat_s
strcat 函数追加 src 字符串到 dest 指向的字符串的后面,追加的时候会追加'\0'
#include <stdio.h>
#include <stdlib.h>
#include <string.h>int main() {/*char* p = (char*)malloc(120);if (!p) return;scanf_s("%s", p, 100);char* str = "先生/女士";strcat_s(p, 120, str);printf("%s\n", p);*/char p[100] = { 'x','x','r' };//char* p = "xxr"; // 字符串常量不能被修改char* str = "先生/女士";//strcat_s(p, 100, str);strncat_s(p, 100, str, 4);printf("%s", p);}
4. 字符串比较函数 strcmp
比较 s1 和 s2 指向的字符串的大小,比较方法:逐个字符去比较 ASCII 码,一旦比较出大小返回。如果所有的字符都一样,则返回0。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>int main() {char* a = "abc";char* b = "acd";char* c = "aaa";char* d = "abd";//int res = strcmp(a, d);int res = strncmp(a, d, 3);printf("%d\n", res);char* p1 = (char*)malloc(100);char* p2 = (char*)malloc(100);if (!p1) return;if (!p2) return;printf("请输入密码\n");scanf_s("%s", p1, 100); printf("请再次输入密码\n");scanf_s("%s", p2, 100);if (strcmp(p1, p2) == 0) {printf("密码正确\n");}else {printf("两次输入的密码不匹配\n");}
}
5.字符串查找函数 strchr
在字符指针 s 指向的字符串中,找 ASCII 码为 c 的字符 注意,是首次匹配,如果 s 指向的字符串中有多个 ASCII 为 c 的字符,则找的是第1个字符
#include <stdio.h>
#include <string.h>int main() {//char* str = "xxr";char* res = strchr(str, 'k');//char* res = strrchr(str, 'x'); // 末次匹配//if (res == NULL) {// printf("不在");// return;//}//printf("%p\n", str);//printf("%p\n", res);//printf("%d\n", (int)(res-str));// 模糊查询char* str[] = { "xxr", "xp", "gy"};printf("请输入你要查询的字母\n");char search = 0;scanf_s("%c", &search, 1);for (int i = 0; i < sizeof(str) / sizeof(char*); i++) {if (strchr(str[i], search) != NULL) {printf("%s\n", str[i]);}}
}
6. 字符串匹配函数 strstr
char *strstr(const char *a, const char *b);
在 a 指向的字符串中查找 b 指向的字符串,也是首次匹配
#include <stdio.h>
#include <string.h>int main() {// 模糊查询char* str[] = { "xxr", "xp", "gy", "xxx"};printf("请输入你要查询的字母串\n");char search[100];scanf_s("%s", &search, 100);// search 拼接 \0strcat_s(search, 100, "");for (int i = 0; i < sizeof(str) / sizeof(char*); i++) {if (strstr(str[i], search) != NULL) {printf("%s\n", str[i]);}}
}
7. 字符串转换数值
atoi/atol/atof 字符转换功能
函数声明:int atoi(const char *nptr);
int num;
num = atoi("12岁");
则 num 的值为 12
8.字符串切割函数 strtok
函数声明:char *strtok(char *str, const char *delim);
字符串切割,按照 delim 指向的字符串中的字符,切割 str 指向的字符串。其实就是在 str 指向的字符串中发现了delim 字符串中的字符,就将其变成10, 调用一次 strtok 只切割一次,切割一次之后,再去切割的时候 strtok的第一个参数传 NULL,意思是接着上次切割的位置继续切
#include <stdio.h>
#include <stdlib.h>
#include <string.h>void split(char* str, char** splitStr, char* p) {char* buf = NULL;splitStr[0] = strtok_s(str, p, &buf);//printf("%s\n", splitStr[0]);int i = 0;while (splitStr[i]) {i++;splitStr[i] = strtok_s(NULL, p, &buf);}//printf("%s\n", splitStr[3]);
}int main() {// 录入地址char str[100] = "广东|广州|天河区";char* splitStr[100];//printf("%s\n", str);split(str, splitStr, "|");int m = 0;while (splitStr[m]) {printf("%s\n", splitStr[m]);m++;}
}
9. 空间设定函数 menset
函数声明:void* memeset(void *ptr,int value,size_t num);
memset 函数是将 ptr 指向的内存空间的 num 个字节全部赋值为 value
#include <stdio.h>
#include <stdlib.h>
#include <string.h>int main() {//char* p = (char*)calloc(25,4);char* p = (char*)malloc(100);char p1[] = "xxr";if (!p) return;memset(p, 100, 100);/*for (int i = 0; i < 100; i++) {printf("%d\n", p[i]);}*/memset(p1, 0, 4);/*for (int i = 0; i < 4; i++) {printf("%d\n", p1[i]);}*/int* ip = (int*)malloc(100);if (!ip) return;memset(ip, -1, 100); // int数组 只能改成 0 和 -1for (int i = 0; i < 100/sizeof(int); i++) {printf("%d\n", ip[i]);}
}
六、结构体
1. 初识结构体
在程序开发时,有些时候需要将不同类型的数据组合成一个有机的整体
struck {char name[100];int score;int age;
}
结构体是一种构造类型的数据结构,是一种或多种基本类型或构造类型的数据整合
2. 结构体初始化与访问
结构体变量,是若干个相同或不同数据结构构成的集合:
- 在定义结构体变量之前首先得有结构体类型,然后再定义变量
- 在定义结构体变量时,可以顺便给结构体变量赋值,被称为结构体初始化
- 结构体变量初始化时,各个成员顺序初始化
#include <stdio.h>
#include <string.h>typedef struct ADDRESS {char* province;char* city;char* district;
} ADDR;typedef struct student {//char name[100];char* name;int age;//char* address[3];ADDR address;
} STU;int main() {STU student0 = {"谢小然",18,{"广东","广州","天河"}};STU student1 = {"xiaoming"};//student1.name = "zhangsan"; // name[100] 是字符数组,存储的是常量无法被修改//strcpy_s(student1.name, 100, "zhangsan");student1.name = "zhangsan";student1.age = 100;//student1.address[1] = "深圳";student1.address.city = "深圳";//printf("%s,%d", student0.name, student0.age);printf("%s,%d", student1.name, student1.age);printf("%s", student1.address.city);
}
3. 结构体数组
结构体数组是个数组,邮若干个相同类型的结构体变量构成的集合
struct 结构体类型名 数组名 [元素个数];
struct student stu[3];
4. 结构体指针
结构体指针:即结构体的地址,结构体变量存放内存中,也有起始地址。
在64位系统环境下,结构体指针占8个字节。
语法:struct 结构体类型名 *结构体指针变量名
#include <stdio.h>struct student {char* name;int age;
};int main() {/*struct student stu = {"谢小然", 18};struct student *sp = &stu;printf("%s\n", (*sp).name);printf("%s\n", sp->name);printf("%d\n", sp->age);*/struct student stu[3] = {{"谢小然", 18},{"顾优", 20},{"陈轩宇", 18}};struct student* sp = stu;printf("%s\n", sp[0].name);printf("%d\n", sp[0].age);printf("%s\n", (sp + 1)->name);printf("%s\n", (sp + 2)->name);
}
5. 结构体与函数
给函数传结构体变量的地址
#include <stdio.h>
#include <stdlib.h>
#include <string.h>typedef struct student {char name[100];int score;
} STU;void input(STU *p) {printf("录入名字和成绩\n");scanf_s("%s%d", p->name, 100, &p->score);
}float sumfunc(STU *p, int num) {float sum = 0;for (int i = 0; i < num; i++) {sum += (p + i)->score;}return sum;
}int main() {int num = 0;printf("请输入要录入的人数\n");scanf_s("%d", &num);STU* stu = (STU*)malloc(num * sizeof(STU));if (!stu) return;for (int i = 0; i < num; i++) {input(stu+i);}float sum = 0;sum = sumfunc(stu, num);printf("平均成绩%f\n", sum / num);free(stu);
}
6. 结构体内存分配
结构体变量大小是所有成员大小之和。
规则1
以多少个字节位单位开辟内存给结构体变量分配内存时,会去结构体变量中找基本类型的成员。
哪个基本类型的成员占字节数多,就以它的大小开辟内存。
- 成员中只有 char 型数据,以1个字节开为单位辟内存;
- 出现 short int 类型数据,没有比其更大的数据时,以2个字节为单位开辟内存;
- 出现 int float 类型数据,没有比其更大的数据时,以4个字节为单位开辟内存;
- 出现 double 类型数据,以 8 个字节为单位开辟内存。
规则2
- char 1字节对齐,即存放 char 型的变量,内存单元的编号是 1 的倍数即可;
- short int 2字节对齐,起始内存单元的编号是 2 的倍数即可;
- int 4字节对齐,起始内存单元的编号是 4 的倍数即可;
- long int 在32位系统下,4字节对齐,起始内存单元的编号是 4 的倍数即可;
- float 4字节对齐,起始内存单元的编号是 4 的倍数即可;
- double 8字节对齐,起始内存单元的编号是 8 的倍数即可。
字节对齐的好处
用空间来换时间,提高 cpu 读取数据的效率
struct stu {char a;int b;
};
7. 链表
概念:链表是一种五路存储结构上非连续,非顺序的存储结构,数据元素的逻辑顺序是通过链表中指针连接次序实现的。
#include <stdio.h>
#include <stdlib.h>struct student {char name[100];int iNumber;struct student* pNext;
};struct student* Create() {struct student* pHead = NULL;struct student* pEnd, * pNew;pEnd = pNew = (struct student*)malloc(sizeof(struct student));if (!pNew) return NULL;printf("请录入姓名和学号,输入数字0则会退出操作\n");scanf_s("%s", pNew->name, 100);scanf_s("%d", &pNew->iNumber);pNew->pNext = NULL;if (pNew->iNumber != 0) {pHead = pNew;}while (pNew->iNumber != 0) {pNew = (struct student*)malloc(sizeof(struct student));if (!pNew) return NULL;scanf_s("%s", pNew->name, 100);scanf_s("%d", &pNew->iNumber);if (pNew->iNumber != 0) {pNew->pNext = NULL;pEnd->pNext = pNew;pEnd = pNew;}}return pHead;
}void Print(struct student* pHead) {struct student* pTemp = pHead;while (pTemp != NULL) {printf("%s,%d\n", pTemp->name, pTemp->iNumber);pTemp = pTemp->pNext;}
}int main() {struct student* pHead = Create();Print(pHead);
}
七、共用体
共用体和结构体类似,也是一种构造类型的数据结构。
几个不同的变量共同占用一段内存的结构,在C语言中,被称为“共用体”类型结构
共用体所有成员占有一段地址空间,共用体的大小是其占用内存长度最大的 成员的大小
typedef struct {char name[100];int score;
}stu;typedef struct {char name[100];int salary;
}tea;typedef union {stu student;tea teacher;
}any;
共用体的特点:
- 同一内存段可以用来存放几种不同类型的成员,但每一瞬间只有一种起作用
- 共用体变量中起作用的成员是最后一次存放的成员,在存入一个新的成员后原有的成员的值会被覆盖
- 共用体变量的地址和它的各成员的地址都是同一地址
八、 枚举
将变量的值一一列举出来,变量的值只限于列举出来的值的范围内
语法:enum 枚举类型名 {枚举值列表};
在枚举值表中应列出所有可用值,也称为枚举元素;
枚举元素是常量,编号默认是从 0 开始的
enum TYPE {STU = 1, TEA};enum TYPE type;if (type == STU) {// ...
}
注意:
- 宏定义是一个值/表达式,不是一种类型
- 枚举是一种类型,可以定义枚举类型的一个变量
九、位运算
1. 原码 反码 补码
正数在内存中以原码形式存放,负数在内存中以补码形式存放
正数的原码 = 反码 = 补码
原码:将一个整数,转换成二进制,就是其原码。(如字节的 5 的原码为:0000 0101;则 -5 的原码为 1000 0101。)
反码:正数的反码就是其原码;负数的反码是将原码中,除符号位以外,每一位取反。(如单字节的 5 的反码为:0000 0101;-5 的反码为 1111 1010。)
补码:正数的补码就是其原码;负数的反码 +1 就是补码。(如单字节的 5 的补码为:0000 0101; -5 的补码为 1111 1011。)
2. 位运算
无论是正数还是负数,编译系统都是按照内存中存储的内容进行位运算。
-
& 按位 与
任何值与 0 得 0,与 1 保持不变
-
| 按位 或
任何值或 1 得 1,或 0 保持不变
-
~ 按位取反
1 变 0,0 变 1
-
^ 按位异或
相异得 1,相同得 0
-
位移
>> 右移 << 左移
十、 预处理
预编译
将.c 中的头文件展开,宏展开生成的文件是 .i 文件
编译
将预处理之后的 .i 文件生成 .s 汇编文件
汇编
将 .s 汇编文件生成 .o 目标文件
链接
将 .o 文件链接成目标文件
执行
1. 宏定义 define
定义宏用 define 去定义,宏是在预编译时进行替换。
-
不带参数
#define PI 3.1415
在预编译时如果代码出现了 PI 就用 3.1415 去替换
-
带参数
#define Max(a,b) (a>b?a:b)
将来在预处理时,替换成实参代替字符串的形参,其他字符保留
带参宏和带参函数的区别
- 带参宏被调用多少次就会展开多少次,执行代码的时候没有函数调用的过程,不需要压栈弹栈。所以带参宏
是浪费了空间,因为被展开多次,节省时间。 - 带参函数,代码只有一份,存在代码段,调用的时候去代码段取指令,调用的时候要,压栈弹栈。有个调用的
过程。所以说,带参函数是浪费了时间,节省了空间。 - 带参函数的形参是有类型的,带参宏的形参没有类型名。
2. 选择性编译
-
#ifdef XXR代码段一 #else代码段二 #endif
-
#ifndef XXR代码段一 #else代码段二 #endif
-
#if XXR == 1代码段一 #elif XXR == 2代码段二 #else代码段三 #endif
注意和 if else 语句的区别
- if else 语句都会被编译,通过条件选择性执行代码
- 选择性编译,只有一块代码被编译
十一、 文件
1. 初始文件
文件用来存放程序、文档、音频、视频数据、图片数据。
文件就是存放在磁盘上的一些数据集合。
磁盘文件:指一组相关数据的有序集合,通常存储在外介质(如磁盘上),使用时才调入内存。
设别文件:在操作系统中把每一个与主机相连的输入、输出设备看作是一个文件,把它们的输入、输出等同于对磁盘文件的读和写。
键盘:标准输入文件
屏幕:标准输出文件
其他设备:打印机、触摸屏、摄像头、音箱等
2. 标准 io 库函数对磁盘文件的读取
文件缓冲区是库函数申请的一段内存,由库函数对其进行操作,程序员没有必要知道存放在哪里,只需要知道对文件操作时的一些缓冲特点即可。
3. 磁盘文件的分类
一个文件通常是磁盘上一段命名的存储区,计算机的存储在物理上是二进制的,所以物理上所以的磁盘文件本质上都是一样的,以字节为单位进行顺序存储。
从用户或者操作系统使用的角度
把文件分为:
- 文本文件:基于字符编码的文件
- 二进制文件:基于值编码的文件
文本文件、二进制文件对比
译码:
文本文件编码基于字符定长,译码容易些;
二进制文件编码是变长的,译码难一些(不同的二进制文件格式,有不同的译码方式,一般需要特定软件进行译码)。
空间利用率
二进制文件用一个比特来代表一个意思(位操作);
而文本文件任何一个意思至少是一个字符。
所以二进制文件,空间利用率高。
可读性:
文本文件用通用的记事本工具就几乎可以浏览所有文本文件
二进制文件需要一个具体的文件解码器
4. 文件指针
文件指针在程序中用来标识(代表)一个文件的,在打开文件的时候得到文件指针,文件指针就用来代表咱们打开的文件。
FILE*指针变量标识符;
typedef struct _iobuf {int cnt; // 剩余的字符,如果输入缓冲区,那么就表示缓冲区还有多少个字符未被读取char *ptr; // 下一个要被读取的字符的地址char *base; // 缓冲区基地址int flag; // 读写状态标志位int fd; // 文件描述符
}FILE;
在缓冲文件系统中,每个被使用的文件都要在内存中开辟一块 FILE 类型的区域,存放与操作文件相关的信息
对文件操作的步骤:
- 对文件进行读写等操作之前 要打开文件得到文件指针
- 可以通过文件指针对文件进行读写等操作
- 读写等操作完毕后,要关闭文件,关闭文件后,就不能在通过此文件指针操作文件了
5. fopen
FILE *fopen(const char *path, const char *mode);
函数的参数:
- 参数1:打开的文件路径
- 参数2:文件打开的方式,即以什么方式打开 r w a +
模式 | 功能 |
---|---|
r 或 rb | 以只读方式打开一个文本文件(不创建文件) |
w 或 wb | 以写方式打开文件(使文件长度截断为0字节创建一个文件) |
a 或 ab | 以追加方式打开文件,即在末尾添加内容,当文件不存在时,创建文件用于写 |
r+ 或 rb+ | 以可读、可写的方式打开文件(不创建新文件) |
w 或 wb+ | 以可读、可写的方式打开文件(使文件长度为0字节,创建一个文件) |
a 或 ab+ | 以追加方式打开文件,打开文件并在末尾更改文件(如果文件不存在,则创建文件) |
返回值:
- 成功:打开文件对应的文件指针
- 失败:返回 NULL
6. fclose
int fclose(FILE *fp);
关闭 fp 所代表的文件
返回值:
- 成功返回 0
- 失败返回非0
7. fgetc 和 fputc
int fgetc(FILE *stream);
fgetc 从 stream 所标识的文件中读取一个字节,将字节值返回
**返回值:**读到文件结尾返回 EOF
EOF 是在 stdio.h 文件中定义的符号常量,值为-1
int fputc(int C, FILE *stream)
fputc将c的值写到 stream 所代表的文件中。
返回值:
如果输出成功,则返回输出的字节值;
如果输出失败,则返回一个 EOF。
#include <stdio.h>int main() {FILE* fp1, * fp2;char ch;int error1 = fopen_s(&fp1, "xxr.txt", "r");int error2 = fopen_s(&fp2, "copy.txt", "a");if (error1 != 0) {printf("打开错误");return;}if (error2 != 0) {printf("打开错误");return;}while ((ch = fgetc(fp1)) != EOF) {//printf("%c", ch);fputc(ch, stdout);fputc(ch, fp2);}fclose(fp1);fclose(fp2);
}
8.fgets 与 fputs
char *fgets(char *s, int size, FILE *stream);
从 stream 所代表的文件中读取字符,在读取的时候碰到换行符或者是碰到文件的末尾停止读取,或者是读取了
size-1个字节停止读取,在读取的内容后面会加一个10, 作为字符串的结尾
int fputs(const char *s, FILE *stream);
将s指向的字符串,写到 stream 所代表的文件中
#include <stdio.h>int main() {FILE* fp1, * fp2;int error1 = fopen_s(&fp1, "xxr.txt", "r");int error2 = fopen_s(&fp2, "copy.txt", "a");if (error1 != 0) {printf("打开错误");return;}if (error2 != 0) {printf("打开错误");return;}char str[100];fgets(str, 100, fp1);printf("*%s*", str);fputs(str, fp2);fclose(fp1);fclose(fp2);
}
9.fread
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
fread 函数从 stream 所标识的文件中读取数据,每块是 size个字节,共nmemb块,存放到ptr指向的内存里返回值:实际读到的块数。
10.fwrite
size_t fwrite(void *ptr, size_t size, size_t nmemb, FILE *stream);
fwrite 函数将 ptr 指向的内存里的数据,向stream 所标识的文件中写入数据,每块是size 个字节,共nmemb 块。
11.rewind
rewind 复位读写位置
void rewind(文件指针);
把文件内部的位置指针移到文件首
12.fseek
int fseek(FILE *stream, long offset, int whence);
移动文件流的读写位置.
whence 起始位置
- 文件开头 SEEK SET O
- 文件当前位置 SEEK_CUR 1
- 文件未尾 SEEK_END 2
位移量:以起始点为基点,向前、后移动的字节数,正数往文件末尾方向偏移,负数往文件开头方向偏移。
#include <stdio.h>
#include <string.h>int main() {printf("注册密码:");char password[7] = "";scanf_s("%s", password, 7);FILE* fp;int error = fopen_s(&fp, "xxr.txt", "w+");if (error != 0) {return;}fputs(password, fp);printf("注册成功\n");printf("登录密码:");scanf_s("%s", password, 7);//rewind(fp);//fseek(fp, 0, SEEK_SET);//fseek(fp, -6, SEEK_CUR);fseek(fp, -6, SEEK_END);char password_db[7] = "";fgets(password_db, 7, fp);int result = strcmp(password, password_db);if (result == 0) {printf("登录成功");}else {printf("登录失败%d", result);}
}
13. ftell
测文件读写位置距文件开始有多少个字节
long ftell(文件指针);
返回值:返回当前读写位置(距离文件起始的字节数),出错时返回-1.
1ong int length;
length = fte11(fp)