[C语言]一、C语言基础(01)

G:\Cpp\C语言精讲

1. C语言入门

1.1 初识计算机语言

  • 计算机编程语言,就是人与计算机交流的方式。人们可以使用编程语言对计算机下达命令,让计算机完成人们需要的功能。

    • 计算机语言有很多种。如:C 、C++、Java、Go、JavaScript、Python,Scala等。

    • 语言=语法+逻辑

1.1.1 计算机语言简史

        第一代:机器语言
        第二代:汇编语言
        第三代:高级语言

1.2 初识C语言

1.2.1 C语言的由来

        1972年,丹尼斯·里奇(Dennis Ritchie)在 B 语言的基础上重新设计了一种新语言,这种新语言取代了 B 语言,称为C 语言。
        1988年,美国国家标准协会(ANSI:American National Standards Institute)正式将C语言标准化,标志着C语言开始稳定和规范化。

1.2.2 为什么要学习C语言

1、C语言具有可移植性好、跨平台的特点,用C编写的代码可以在不同的操作系统和硬件平台上编译和运行。
2、C语言在许多领域应用广泛。
        - 操作系统
        - 嵌入式系统
        - 系统软件
        - 网络:C语言广泛用于开发网络应用程序,如Web服务器,网络协议和网络驱动程序。
        - 数据库系统:Oracle、mySQL、PostgreSQL
        - 游戏开发
        - 人工智能:神经网络和深度学习
        - 科学应用:仿真软件和数值分析工具
        - 金融应用:股票市场分析和交易系统等金融应用
3、C语言能够直接对硬件进行操作、管理内存、跟操作系统对话,使得它是一种非常接近底层的语言,非常适合写跟硬件交互、有极高性能要求的程序。
4、有助于快速学习其他语言

1.2.3 计算机语言排行榜

        TIOBE (TIOBE Index - TIOBE)是一个流行编程语言排行,每月更新。排名权重基于世界范围内 工程师数量,Google、Bing、Yahoo! 、Wikipedia、Amazon、Youtube和百度这些主流的搜索引擎,也将作为排名权重的参考指标。

1.2.4 C语言的版本

版本1:K&R C

K&R C 指的是 C 语言的原始版本。1978年,C 语言的发明者布莱恩·柯林(Brian Kernighan)和丹尼斯·里奇(Dennis Ritchie)合写了一本著名的教材《C 编程语言》(The C programming language)。

由于 C 语言还没有成文的语法标准,这本书就成了公认标准,以两位作者的姓氏首字母作为版本简称“K&R C”。

版本2:ANSI C(又称 C89 或 C90)

C 语言的原始版本非常简单,对很多情况的描述非常模糊,加上 C 语法依然在快速发展,要求将 C 语言标准化的呼声越来越高。

1989年,美国国家标准协会(ANSI)制定了一套 C 语言标准,并于次年被国际标准化组织(ISO)通过。它被称为“ANSI C”,也可以按照发布年份,称为“C89 或 C90”。

版本3:C99

C 语言标准的第一次大型修订,发生在1999年,增加了许多语言特性,比如双斜杠( // )的注释语法,可变长度数组、灵活的数组成员、复数、内联函数和指定的初始值设定项。这个版本称为 C99,是目前最流行的 C 版本

版本4:C11

2011年,标准化组织再一次对C 语言进行修订,增加了_Generic、static_assert 和原子类型限定符。这个版本称为C11。

需要强调的是,修订标准的原因不是因为原标准不能用,而是需要跟进新的技术。

版本5:C17

C11 标准在2017年进行了修补,但发布是在2018年。新版本只是解决了 C11 的一些缺陷,没有引入任何新功能。这个版本称为 C17。

版本6:C23

2023年预计发布,计划进一步增强安全性,消除实现定义的行为,引入模块化语言概念等新特性,使C语言在安全和可靠性方面有重大提高。

1.3 第一个C程序:编写->编译->运行

1.3.1 步骤1:HelloWorld的编写

C 语言的源代码文件,以后缀名 .c 结尾。下面是一个简单的 C 程序 Hello.c

#include <stdio.h>              

int main()                  	
{                               
    printf("hello,world!!\n"); 
  	return 0;                   
}

1.3.2 步骤2:编译器的安装与配置

C 语言是一种编译型语言,源码都是文本文件,本身无法执行。必须通过编译器,生成二进制的可执行文件,才能执行。

目前,最常见的 C 语言编译器是自由软件基金会推出的 GCC 编译器,可以免费使用。Linux 和 Mac 系统可以直接安装 GCC,Windows 系统可以安装 MinGW

MinGW和GCC的区别:

GCC是一个跨平台的编译器集合,可用于多种操作系统和处理器架构,包括Windows;
而MinGW是GCC在Windows平台上的移植版本,主要用于在Windows上本地编译C和C++代码。
MinGW 编译器的 安装和配置
1 MinGW 介绍
        MinGW( Minimalist GNU on Windows )实际上是 GCC Windows平台上的移植版本,因此可以将源代码编译为可在 Windows 中运行的可执行程序。 MinGW 是,主要用于在 Windows 上本地编译 C C++代码。
目前适用于 Windows 平台、受欢迎的 GCC 移植版主要有 2 种,
分别为 MinGW 和 Cygwin。 其中:
MinGW 侧重于服务 Windows 用户可以使用 GCC 编译环境,是真
正的GCC移植,相比后者体积更小,使用更方便; Cygwin 只是提
供一个类unix的环境内部是原生的GCC,借助它不仅可以在
Windows 平台上使用 GCC 编译器,理论上可以运行 Linux 平台上
所有的程序;
Cygwin 只是提供一个类unix的环境内部是原生的GCC。
如果读者仅需要在 Windows 平台上使用 GCC,可以使用 MinGW
或者 Cygwin;除此之外,如果还有更高的需求(例如运行 POSIX
应用程序),就只能选择安装 Cygwin。
2 、下载与安装
2.1 下载
下载地址:  https://sourceforge.net/projects/mingw/files/
点击 “Download Latest Version” 即可
2.2 安装
下载完成后,会得到一个名为 mingw-get-setup.exe 的安装包,双击打开它,可以看到如下的对话框。
直接点击 “Install” ,进入下面的对话框:
读者可根据自己操作系统的实际情况,自定义 MinGW 的安装位置( 建议安装到非 C盘的指定目录下:D:\soft_dev\MinGW   ),然后点击 “continue”,进入下面的对话框:
进入安装 MinGW 配置器的界面,耐心等待安装完成(显示 100%) 即可。
安装完成之后,继续点击 “continue”,进入下面的对话框,这是一个名为 MinGW Installer Manager ” 的软件,借助它,我们可以随时根据需要修改 GCC 编译器的配置。

常见的安装包介绍如下。
其中 minw32-gcc-g++ 支持 C++ 编译和 minw32-gcc 支持 C 编译。
为使 GCC 同时支持编译 C 语言和 C++ ,需勾选上图中标注的 2 项。选中其中一项,鼠标右键点击,选择 “Mark for Installation”,如图所示。
标记完以后如图所示。
GCC 还支持其它编程语言,读者可借助此配置器,随时根据需要 安装自己需要的编译环境。
勾选完成后,在菜单栏中选择 Installation -> Apply Changes
弹出如下对话框:
选择 “Apply” 。然后耐心等待,直至安装成功,即可关闭此界面。
3 、配置: path 环境变量
在安装完成的基础上,我们需要手动配置 PATH 环境变量。 1 )依次 右击计算机(我的电脑) - > 属性 - > 高级系统设置 -> 环境变量 
2 )打开命令行窗口(通过在搜索栏中执行 cmd 指令即可),输入 gcc - v 指令,如果输出 GCC 编译器的具体信息,则表示安装成功,例如:
通过上面的安装,我们就可以在当前 Windows 平台上编译、运行 C 或者 C++ 程序了。
因为 MinGW-w64 本来就是将 GCC 移植到 Windows 上的产物,所以操作方式和 GCC 一样,只是在 Linux 下命令是被键入到 终端 ”中,而 Windows 下则是被键入到 命令提示符 里。
yum install -y gcc

1.3.3 步骤3:编译和运行

编译器将代码从文本翻译成二进制指令的过程,就称为编译阶段,又称为“编译时”(compile time),跟运行阶段(又称为“运行时”)相区分。

假设你已经安装好了 GCC 编译器,可以通过win+r打开cmd命令行,在Hello.c文件所在目录下执行下面的命令。

上面命令使用 gcc 编译器,将源文件 Hello.c 编译成二进制代码。

运行这个命令以后,默认会在当前目录下生成一个编译产物文件 a.exe。执行该文件,就会在屏幕上输出 Hello World 。

**GCC 的 -o 参数(output 的缩写)可以指定编译产物的文件名。**

 gcc -o Hello Hello.c

上面命令的 `-o Hello` 指定,编译得到的可执行文件名为 Hello.exe ,取代默认的 a.exe。执行该文件,也会得到同样的结果。

**GCC 的 `-std= 参数`(standard 的缩写)还可以指定按照哪个 C 语言的标准进行编译。**

```
> gcc -std=c99 Hello.c
```

上面命令指定按照 C99 标准进行编译。

1.4 IDE的使用

IDE(Integrated Development Environment,集成开发环境):相较于文本开发工具,IDE可以把代码编写,编译,执行,调试等多种功能综合到一起的开发工具。

1.4.1 开发工具介绍

方式1:本地安装的IDE工具

1. Code::Block

Code::Block是一个免费的跨平台IDE,它支持C、C++和Fortan程序的开发。Code::Block的最大特点是它支持通过插件的方式对IDE自身功能进行扩展,这使得Code::Block具有很强的灵活性,方便用户使用。

官网地址:https://www.codeblocks.org

2. Microsoft Visual C++ 2010

Visual C++ 2010,简称VC2010,是由微软开发的独立的、免费的 C/C++ 编译工具,与Visual Basic等并列,最后微软将它们整合在一起组成了Visual Studio。

Visual C++从发布起到现在已经有10个大版本了,这里介绍的Visual C++ 2010就是Visual C++ 10,简称VC10。上朔10多年发布的Visual C++ 6.0,被称为史上最经典的VC,现在有很多企业还在用它,大量的教材基于这个版本的VC来写的。但VC6比较弱,被淘汰是迟早的。

3. Microsoft Visual Studio

Visual Studio(简称 VS)是由微软公司发布的集成开发环境。它包括了整个软件生命周期中所需要的大部分工具,如UML工具、代码管控工具、集成开发环境(IDE)等。

Visual Studio 支持 C/C++、C#、F#、VB 等多种程序语言的开发和测试,可以用于生成Web应用程序,也可以生成桌面应用程序,功能十分强大。但下载和安装很可能耗时数小时,还可能会塞满磁盘。

Visual Studio 2019有三种版本:社区版(免费,不支持企业使用),专业版(第一年1199美元/ 799美元续订)和企业版(第一年5999美元/2569美元续订)。企业版拥有面向架构师的功能、高级调试和测试,这些功能是另两种SKU所没有的。

Visual Studio旨在成为世界上最好的IDE(集成开发环境),目前最新版本为 Visual Studio 2023。

这就好像Office 2007是由Word 2007、Excel 2007、Access 2007等等组成的一个道理。其中Visual C++就是Visual Studio的一个重要的组成部分。

官网地址:https://visualstudio.microsoft.com

4. CLion

CLion是一款由JetBrains推出的跨平台C/C++集成开发环境(IDE),它具有智能编辑器、CMake构建支持、调试器、单元测试、代码分析等功能,可以极大提高C/C++开发效率。

官网地址:CLion: A Cross-Platform IDE for C and C++ by JetBrains

方式2:可在线使用的工具

CodingGround: Online C Compiler

OnlineGDB: https://onlinegdb.com/online_c_compiler

Lightly:https://cde2f3ce.lightly.teamcode.com/

1.4.2 CLion的下载与安装

1.4.3 CLion中HelloWorld的执行

1)选择"New Project":

2)指定创建C可执行文件、工程目录,图中的“untitled1”需要修改为自己的工程名称。如下所示:

3)选择C可执行文件,修改工程名称为demo1

4)点击“Create”进行下一步,如图所示

5)此处选择编译器,默认MinGW即可,点击“OK”按钮,如图所示,默认创建了main.c文件。

6)点击执行按钮,如下所示

1.4.4 C/C++ Single File Execution插件的安装

1、为何安装C/C++ Single File Execution插件?

前面已经创建了一个demo1工程,项目文件夹内存在一个代码文件,名为`main.c`。如果再创建一个C源文件,内部如果也包含main()函数,则会报错!因为默认C工程下只能有一个main()函数。如何解决此问题呢?

2、安装并测试

1)在 File - Settings - Plugins 中搜索 `C/C++ Single File Execution` 插件并安装

2)在需要运行的代码中右键,点击 Add executable for single c/cpp file

3)此时可以在 Cmakelists.text 文件中看到多出的这一行代码,这就是插件帮我们完成的事情

4)右键项目文件夹,点击 Reload CMake Project 进行刷新

5)此时右上角标签处已经增加了我们的文件选项,选择需要的标签

6)点击小三角,或右键代码处点击 Run 选项,即可运行代码。

7)在该工程下创建main2.c文件,文件中的代码如下所示,执行上面相同的步骤。

可以发现一个工程中允许存在多个main方法了,而且可以独立允许。

1.4.5 C 程序运行机制

过程1:编辑

编写C语言源程序代码,并以文件的形式存储到磁盘中。源程序文件以“`.c`”作为扩展名。

过程2:编译

将C语言源程序转换为`目标程序(或目标文件)`。如果程序没有错误,没有任何提示,就会生成一个扩展名为“`.obj`”的二进制文件。C语言中的每条可执行语句经过编译后最终都将被转换成二进制的机器指令。

过程3:链接/连接

编译形成的目标文件“.obj”和库函数及其他目录文件连接/链接,形成统一的`可执行的`二进制文件“`.exe`”。

为什么需要链接库文件呢?
因为我们的C程序中会使用 C程序库的内容,比如<stdio.h> 、<stdlib.h> 
中的函数printf()、system()等,这些函数不是程序员自己写的,而是C程序库中提供的,
因此需要链接。链接后,生成的.exe 文件,比obj 文件大了很多。

过程4:运行

有了可执行的exe文件,我们可以在控制台下直接运行此exe文件。

练习:

1. 计算机高级语言程序的运行方法有编译执行和解释执行两种,以下叙述中正确的是(  )。
A.C语言程序仅可以编译执行
B.C语言程序仅可以解释执行
C.C语言程序既可以编译执行,又可以解释执行
D.以上说法都不对

【答案】A

【解析】编译执行是指程序执行前需要一个专门的编译过程把程序编译成机器语言的文件,再次运行时不需要重新翻译,执行效率高;解释执行是指每个语句都是执行的时候才翻译,执行效率低。用C语言编写的程序必须经过编译器编译后,转换为二进制的机器指令来运行。

2. 以下叙述中错误的是(  )。

A.C语言的可执行程序是由一系列机器指令构成的

B.用C语言编写的源程序不能直接在计算机上运行

C.通过编译得到的二进制目标程序需要链接才可以运行

D.在没有安装C语言集成开发环境的机器上不能运行C源程序生成的exe文件

【答案】D

【解析】A项正确,C语言的可执行程序是由一系列机器指令组成的;BC项正确,用C语言编写的源程序必须经过编译,生成二进制目标代码,再经过连接才能运行;D项错误,C语言经过编译链接后的二进制目标代码可以脱离C语言集成开发环境独立运行。答案选择D选项。

3.以下叙述中错误的是(  )。

A.C语言中的每条可执行语句和非执行语句最终都将被转换成二进制的机器指令

B.C程序经过编译、链接步骤之后才能形成一个真正可执行的二进制机器指令文件

C.用C语言编写的程序称为源程序,它以ASCII代码形式存放在一个文本文件中

D.C语言源程序经编译后生成后缀为.obj的目标程序

【答案】A

【解析】A项错误,注释语句不会被翻译成二进制的机器指令。C源程序经过C编译程序编译之后生成后缀为.obj的二进制文件(称为目标文件),然后由“链接程序”(Link)的软件把.obj文件与各种库函数连接起来生成一个后缀为.exe的可执行文件。答案选择A选项。

标准库、头文件

printf() 是在标准库的头文件 `stdio.h` 中定义的。要想在程序中使用这个函数,必须在源文件头部引入这个头文件。即:#include <stdio.h>

何为标准库?

        程序需要用到的功能,不一定需要自己编写,C 语言可能已经自带了。程序员只要去调用这些自带的功能就可以了。C 语言自带的所有这些功能,统称为`“标准库”(standard library)`,包含C 内置函数、常量和头文件。

因为它们是写入标准的,到底包括哪些功能,应该怎么使用,都是规定好的,我们直接调用即可。

何为头文件?

        不同的功能定义在不同的文件里,这些文件统称为`“头文件”(header file)`。如果系统自带某一个功能,就一定会自带描述这个功能的头文件,比如 printf() 的头文件就是系统自带的 `stdio.h` 。头文件的后缀通常是 `.h` 。

预处理命令:#include命令

        如果要使用某个功能,就必须先加载其对应的头文件,加载使用的是 `#include` 命令,声明在各文件模块的开头。C语言中以 # 号开头的命令称为`预处理命令`。顾名思义,在编译器对当前C程序进行编译前执行预处理操作。

格式:

```
#include <头文件名>
```

举例:

```
#include <stdio.h>   
```

> 注意,加载头文件的 #include 语句不需要分号结尾

对比写法:

#include <stdio.h> //编译系统在系统头文件所在目录搜索

#include "stdio.h" //编译系统首先在当前的源文件目录中查找 stdio.h,找不到的话,再转向系统头文件所在目录搜索。

结论:

-  引用系统头文件,两种形式都会可以,`#include <> `效率高。

-  引用用户头文件,只能使用 `#include " "`。

常用的C头文件

- `stdio.h`——定义核心输入和输出函数
         printf()、scanf()、getchar()、putchar()
- `stdlib.h`——定义数值转换函数、伪随机网络生成器和内存分配
- `string.h`——定义字符串处理函数
- `stdint.h`——定义精确宽度的整数类型
- `math.h`——定义常用的数学函数
        sin()、sqrt()
- `stddef.h`——定义了几个有用的类型和宏

练习:

1. 以下叙述中正确的是( )。
A.C程序中的注释只能出现在程序的开始位置和语句的后面
B.C程序书写格式严格,要求一行内只能写一个语句
C.C程序书写格式自由,一个语句可以写在多行上
D.用C语言编写的程序只能放在一个程序文件中

【答案】C

【解析】C程序的注释可以出现在C程序的任何位置,注释符号:“//”或“/*…*/”,选项A错误。C程序中,一行内可写多个语句,每条语句用分号“;”结束,选项B错误,选项C正确。用C语言编写的程序可以放在多个程序文件中,用#include命令行实现文件包含功能,选项D错误。答案选择C选项。

2.【中央财经大学2018研】以下叙述错误的是( )。
A.在程序中凡是以“#”开始的语句行都是预处理命令行
B.预处理命令行的最后不能以分号表示结束
C.#include MAX是合法的宏定义命令行
D.C程序对预处理命令行的处理是在程序执行的过程中进行的

【答案】D

【解析】在C语言中,凡是以“#”开头的行都称为编译预处理命令行,为了区别C语句,后面是不加分号的。编译预处理是在编译程序对C源程序进行编译前执行的,而不是在程序执行过程中进行的。

1.5 关于输出

1.5.1 printf()标准格式

printf(格式控制字符串,输出列表);

其中,

- `"格式控制字符串"`是用双引号括起来的一个字符串。包括:
  - 普通字符:普通字符即需要在输出时原样输出的字符。
  - 占位符:由“%”和格式字符组成。这个位置可以用其它值代入。

- `"输出列表"`是程序需要输出的一些数据,可以是常量、变量或表达式。用于替换占位符的位置。

注意:printf() 参数与占位符是一一对应关系。如果参数个数少于对应的占位符, printf() 可能会输出内存中的任意值。

1.5.2 占位符

占位符的第一个字符是 `%` ,第二个字符表示占位符的类型。

printf() 的占位符有许多种类,与 C 语言的数据类型相对应。

下面按照字母顺序,列出占位符如下,方便查阅(红色为常用的):

%a :浮点数(仅C99有效)
%A :浮点数(仅C99有效)
%c :char型数据
%d :十进制整数(int)

%e :使用科学计数法的浮点数,指数部分的 e 为小写
%E :使用科学计数法的浮点数,指数部分的 E 为大写
%i :整数,基本等同于 %d 
%f :浮点数(float)
%g :6个有效数字的浮点数。整数部分一旦超过6位,就会自动转为科学计数法,指数部分的 e 为小写
%G :等同于 %g ,唯一的区别是指数部分的 E 为大写
%hd :十进制 short int 类型
%ho :八进制 short int 类型
%hx :十六进制 short int 类型
%hu :unsigned short int 类型
%ld :十进制整数(long)
%lo :八进制 long int 类型
%lx :十六进制 long int 类型
%lu :unsigned long int 类型
%lld :十进制 long long int 类型
%llo :八进制 long long int 类型
%llx :十六进制 long long int 类型
%llu :unsigned long long int 类型
%le :科学计数法表示的 long double 类型浮点数
%lf :十进制浮点数(double)
%n :已输出的字符串数量。该占位符本身不输出,只将值存储在指定变量之中
%o :八进制整数
%p :指针
%s :字符串
%u :十进制无符号整数(unsigned int)

%x :十六进制整数
%zd : size_t 类型
%% :输出一个百分号

1.5.3 举例

举例1:%d

int num = 10;
printf("count is %d\n",num);  //输出:count is 10  

举例2:%lf 和 %f

float f = 3.1415926535f;    // 单精度浮点数
double d = 3.1415926535;  // 双精度浮点数

// 使用 %f 输出单精度浮点数
printf("Float: %f\n", f);  //Float: 3.141593
// 使用 %lf 输出双精度浮点数
printf("Double: %lf\n", d); //Double: 3.141593

// 使用 %f 输出单精度浮点数
printf("Float: %.8f\n", f);  //Float: 3.14159274
// 使用 %lf 输出双精度浮点数
printf("Double: %.8lf\n", d); //Double: 3.14159265

举例3:%c

char level = 'A';
printf("this score level is:%c\n",level); //输出:this score level is:A

举例4:%s

char level = 'A';
printf("this score level is:%c\n",level); //输出:this score level is:A

举例5:多个占位符

输出文本里面可以使用多个占位符。

printf("%s有%d部手机\n", "老板", 2); //输出:老板有2部手机

1.5.4 输出格式

printf() 可以定制占位符的输出格式。

格式1:限定宽度

printf() 允许限定占位符的最小宽度。

printf("%5d\n", 123); // 输出为 "  123" 

说明:%5d 表示这个占位符的宽度至少为5位。如果不满5位,对应的值的前面会添加空格。

输出的值默认是右对齐,即输出内容前面会有空格;如果希望改成左对齐,在输出内容后面添加空格,可以在占位符的 % 的后面插入一个 - 号。

printf("%-5d\n", 123); // 输出为 "123  "

对于小数,这个限定符会限制所有数字的最小显示宽度

printf("%12f\n", 123.45); // 输出 "  123.450000"

%12f 表示输出的浮点数最少要占据12位。由于小数的默认显示精度是小数点后6位,所以123.45 输出结果的头部会添加2个空格。

格式2:总是显示正负号

默认情况下, printf() 不对正数显示 + 号,只对负数显示 - 号。如果想让正数也输出 + 号,可以在占位符的 % 后面加一个 + 。

printf("%+d\n", 11); // 输出 +11
printf("%+d\n", -11); // 输出 -11

格式3:限定小数位数

输出小数时,有时希望限定小数的位数。举例来说,希望小数点后面只保留两位,占位符可以写成 %.2f 。

printf("Number is %.2f\n", 0.8); // 输出 Number is 0.80

这种写法可以与限定宽度占位符,结合使用。

printf("%6.2f\n", 0.8); // 输出为 "  0.80"

说明:%6.2f 表示输出字符串最小宽度为6,小数位数为2。整体长度不足 6 位时,右对齐显示。

最小宽度和小数位数这两个限定值,都可以用 * 代替,通过 printf() 的参数传入。

printf("%*.*f\n", 6, 2, 0.8);
//等同于
printf("%6.2f\n", 0.8);

练习

【华南理工大学2018研】十六进制形式输出整数的格式说明符是( )。

A.%u
B.%ld
C.%x
D.%o

【答案】C

【解析】A表示输出的是无符号整型;B表示输出的是有符号长整型;D表示输出的是八进制。

练习1:开发一个 ILoveC.c 程序,可以输出 "某某 is studying c!"

#include<stdio.h>

int main(){
    //printf("谷小妹 is studying C!\n");
    //printf("%s is studying C!\n","谷小妹");
    return 0;
}

练习2:控制台打印:5 + 3 = 8

#include <stdio.h>

int main() {
    printf("%d + %d = %d\n", 5, 3, (5 + 3));
    return 0;
}								//函数结束

2. 变量与进制

2.1 关键字(keyword)

定义:被C语言赋予了特殊含义,用做专门用途的字符串(或单词)。

特点:全部关键字都是`小写字母`。

传统的C语言(ANSI C)有32个关键字。如下:

类型具体关键字
控制语句关键字(12 个)break, case, continue, default, do, else, for, goto, if, return, switch, while
数据类型关键字(12 个)char, enum, double, long, float, int, short, signed, struct, unsigned, union, void
存储类型关键字(4 个)auto, extern, register, static
其他关键字(4 个)const, sizeof, typedef, volatile

后续,1999年,C99标准增加了5个关键字:inlinerestrict_Bool_Complex_Imaginary

2011年,C11标准又增加了7个关键字:`_Alignas`、`_Alignof`、`_Atomic`、`_Static_assert`、`_Noreturn`、`_Thread_local`和`_Generic`。

说明:

1、ANSI C、C99和C11,它们之间差别并不大,在大多数情况下,它们都是和谐共处的。

2、不需要死记硬背,学到哪里记到哪里即可。

2.2 标识符(identifier)

C语言中变量、函数、数组名、结构体等要素命名时使用的字符序列,称为标识符。

技巧:凡是自己可以起名字的地方都叫标识符。

标识符的命名规则(必须遵守的硬性规定

  • 只能由26个英文字母大小写,0-9 或 _ 组成

  • 数字不可以开头

  • 不可以是关键字,但可以包含关键字

  • C99和C11允许使用更长的标识符名,但是编译器只识别前63个字符。(会忽略超出的字符)

  • 不允许有空格。

  • 严格区分大小写字母。比如:Hello、hello是不同的标识符。

标识符的命名建议(建议遵守的软性要求

  • 在起名字时,为了提高阅读性,要尽量有意义,“见名知意”。如:sum,name,max,year,total 等。

  • 不要出现仅靠大小写区分不同的标识符。如:name、Name 容易混淆

  • 尽量避免名字中出现数字编号,如value1、value2等,除非逻辑上需要编号。

  • 习惯上,所有宏定义、枚举常数、常量(只读变量)全用大写字母命名,用下划线分隔单词。

    比如: const double TAX_RATE = 0.08; //TAX_RATE 只读变量

  • 系统内部使用了一些下划线开头的标识符(比如两个下划线开头的变量名、一个下划线 + 大写英文字母开头的变量名)。比如,C99 标准添加的类型 _Bool。为防止冲突,建议用户尽量避免使用下划线开头的标识符。

  • 下划线通常用于连接一个比较长的变量名。如:max_classes_per_student。

  • 变量名、函数名:多单词组成时,第一个单词首字母小写,第二个单词开始每个单词首字母大写:xxxYyyZzz (驼峰法,小驼峰)。比如:short stuAge = 20;tankShotGame

举例:合法的标识符:

a、BOOK1、_sun、MAX_SIZE、Mouse、student23、Football、FOOTBALL、max、_add、num_1、sum_of_numbers

举例:非法的标识符:

$zj、3sum、ab#cd、23student、Foot-baii、s.com、b&c、j**p、book-1、tax rate、don't

练习:

【武汉科技大学2019研】

以下均是合法变量名的是( )。
A.#name   total
B.node  value_max
C._var  long
D.stu-code   a+b

【答案】B

【解析】C语言中变量名只能包含数字,字母和下划线,且只能以字母和下划线开始。A项含非法字符#,错误;C中long为关键字,变量不能以关键字命名;D中含非法字符-和+。

【四川大学2017研】以下不合法的用户标识符是(  )。
A.J2_KEY
B.Double
C.4d
D.\_8_

【答案】C

【解析】标识符只能包含数字,字母,下划线,且不能以数字开头,选项C错误。

下列定义变量的语句中错误的是(  )。

A.double int_; B.float US$; C.char For; D.int _int;

【答案】B【解析】标识符由字母、数字、下划线组成。$是非法字符,不能出现在标识符中。答案选择B选项。

2.3 变量(variable)

2.3.1 为什么需要变量

变量是程序中不可或缺的组成单位,最基本的存储单元

2.3.2 初识变量

  • 变量的概念:

    • 内存中的一个存储区域,该区域的数据可以在同一类型范围内不断变化。

    • 通过变量名,可以访问这块内存区域,获取里面存储的值。

    • 变量的构成包含三个要素:数据类型变量名存储的值

    • C语言中变量声明的格式:数据类型 变量名 = 变量值

  • 变量的作用:用于在内存中保存数据。

  • 使用变量注意:

    • C语言中每个变量必须先声明,后使用。

    • 不同的数据类型,占用的空间大小不一样。

    • 一旦声明,变量的类型就不能在运行时修改。

2.3.3 变量的声明与赋值

步骤1:变量的声明

格式:数据类型  变量名;  //声明变量的语句必须以分号结尾

举例1:int width;

举例2:

int width,height;

// 等同于
int width;
int height;

步骤2:变量的赋值

变量声明时,就为它分配内存空间,但是不会清除内存里面原来的值。这导致声明变量以后,变量会是一个随机的值。所以,变量一定要赋值以后才能使用。

int age; //变量的声明
age = 18;  //变量的赋值

变量的声明和赋值,也可以写在一行。

int age = 18;

多个相同类型变量的赋值,可以写在同一行。

int a = 1, b = 2;

int a, b;
a = 1;
b = (a = 2 * a);

int a, b, c, x, y;
a = b = c = x = y = 10;  //连续赋值

注意:声明变量以后,不用忘记初始化赋值!定义变量时编译器并不一定清空了这块内存,它的值可能是无效的数据,运行程序,会异常退出。

2.3.4 变量的作用域(scope)

  • 变量的作用域:其定义所在的一对{ }内。

  • 变量只有在其作用域内才有效。出了作用域,变量不可以再被调用。

  • 同一个作用域内,不能定义重名的变量。

  • C 语言的变量作用域主要有两种:文件作用域(file scope)和块作用域(block scope)

        文件作用域(file scope)指的是,在源码文件顶层声明的变量,从声明的位置到文件结束都有效。

int x = 1;
int main() {
    printf("%d\n", x);
    return 0;
}

块作用域(block scope)指的是由大括号( {} )组成的代码块,它形成一个单独的作用域。凡是在块作用域里面声明的变量,只在当前代码块有效,代码块外部不可见。

int main() {
    int m = 10;
    if (m == 10) {
        int n = 20;
        printf("%d %d\n", m, n);  // 10 20
    }
    printf("%d\n", m);  // 10
    printf("%d\n", n);  // 超出作用域,报错
    
    return 0;
}

最常见的块作用域就是函数,函数内部声明的变量,对于函数外部是不可见的。 for 循环也是一个块作用域,循环变量只对循环体内部可见,外部是不可见的。

for (int i = 0; i < 10; i++){
    printf("%d\n", i);
}
printf("%d\n", i); // 超出作用域,报错

2.3.5 变量按类型的分类

变量可以按`数据类型`来分,也可以按`声明的位置`来分(全局变量、局部变量)。本节主讲变量的不同类型。

C 语言中的变量按照数据类型分为:

注意1:这里列举的是C语言的常用类型,后续C语言版本还有新增的类型。

注意2:空类型:void 表示空类型(无类型)。通常应用于函数的返回值类型、函数的参数、指针类型。

注意3:在C语言中,没有`字符串类型`,使用字符数组表示字符串。

2.4 基本数据类型

2.4.1 整数类型

1. 类型说明

- C语言规定了如下的几类整型:短整型(short)、整型(int)、长整型(long)、更长的整型(long long)
- 每种类型都可以被 signed 和unsigned 修饰。其中,
          - 使用 `signed 修饰`,表示该类型的变量是带符号位的,有正负号,可以表示负值。`默认是signed`。
          - 使用 `unsigned 修饰`,表示该类型的变量是不带符号位的,没有有正负号,只能表示零和正整数。
- bit(位):计算机中的最小存储单位。
- byte(字节):计算机中基本存储单元。  1byte = 8bit

long long int是C99新增的:

类型修饰符占用空间取值范围
long long [int]signed8个字节(=64位)-9223372036854775808~ 9223372036854775807(-$2^{63}$ ~ $2^{63}$-1)
long long [int]unsigned8个字节(=64位)0 ~ 18446744073709551615(0 ~ $2^{64}$-1)

说明1:不同计算机的 int 类型的大小是不一样的。比较常见的是使用4个字节(32位)存储一个 int 类型的值,具体情况如下:

类型16位编译器32位编译器64位编译器
short int2字节2字节2字节
int2字节4字节4字节
unsigned int2字节4字节4字节
long4字节4字节8字节
unsigned long4字节4字节8字节
long long8字节8字节8字节

说明2:C标准虽然没有具体规定各种类型数据所占用存储单元的长度,但几条铁定的原则(ANSI/ISO制订的):
① sizeof(short int) ≤ sizeof(int) ≤ sizeof(long int) ≤ sizeof(long long),具体由各编译系统自行决定的。其中,sizeof是测量类型或变量长度的运算符。

② short int至少应为2字节,long int至少应为4字节。

这样约定的好处就是使得C语言可以长久使用。`现在的主流CPU是64位`,可以预测不久的将来会推出128位甚至256位的CPU,但是在C语言刚刚出现的时候,CPU还是以8位和16位为主。如果那时候就将整型定死为8位或16位,那么现在我们肯定不会再学习C语言了。

说明3:

最常用的整型类型为:int类型。

整数型常量,默认为int类型

2. 举例

举例1:对于 int 类型,默认是带有正负号的。即 int 等同于 signed int 。一般情况下,关键字signed省略不写。

signed int m;  //声明了一个带符号的整数变量 m 
// 等同于
int m;   //声明了一个带符号的整数变量 m

举例2:int 类型也可以不带正负号,只表示非负整数。这时就必须使用关键字 unsigned 声明变量。表数范围为:0~4294967295

unsigned int a;   //声明了一个不带符号的整数变量a,表数范围为:0~4294967295

unsigned int 里面的 int 可以省略,所以上面的变量声明也可以写成这样:unsigned a;

举例3:

int 类型使用4个字节表示一个整数,对于小整数,这样做很浪费空间。另一方面,某些场合需要更大的整数,8个字节还不够。此时,可以使用short int (简写为 short )、long int (简写为 long )、long long int (简写为 long long )

signed short int a; 
signed long int b;
signed long long int c;

默认情况下, short 、 long 、 long long 都是带符号的(signed),即 signed 关键字可以省略。代码简写为:

short a; 
long b;
long long c;

它们也可以声明为不带符号(unsigned),使得能够表示的最大值扩大一倍。

unsigned short a;  //无符号短整型,表数范围:0~65535
unsigned long b;   //无符号长整型,表数范围:0~4294967295
unsigned long long c;  //无符号长整型,表数范围:0~18446744073709551615

3 关于后缀

编译器将一个整数字面量指定为 int 类型,但是如果希望将其指定为 long 类型,需要在该字面量末尾加上后缀 `l` 或 `L` ,编译器会把这个字面量的类型指定为 long 。

long x = 123L; //或者写成 123l

如果希望字面量指定为long long类型,则后缀以`ll`或`LL`结尾。

long long y = 123LL;

如果希望指定为无符号整数 unsigned int ,可以使用后缀 `u` 或 `U` 。

unsigned int x = 123U;

L 和 U 可以结合使用,表示 unsigned long 类型。 L 和 U 的大小写和组合顺序无所谓。

u 还可以与其他整数后缀结合,放在前面或后面都可以,比如 10UL 、 10ULL 和 10LLU 都是合法的。

unsigned long int      x = 1234UL;
unsigned long long int x = 1234ULL;

4 精确宽度类型

C 语言的整数类型(short、int、long)在不同计算机上,占用的字节宽度可能是不一样的,无法提前知道它们到底占用多少个字节。程序员有时控制准确的字节宽度,这样的话,代码可以有更好的可移植性,头文件 stdint.h 创造了一些新的类型别名。

精确宽度类型(exact-width integer type):保证某个整数类型的宽度是确定的。

- int8_t :8位有符号整数
- int16_t :16位有符号整数
- int32_t :32位有符号整数
- int64_t :64位有符号整数
- uint8_t :8位无符号整数
- uint16_t :16位无符号整数
- uint32_t :32位无符号整数
- uint64_t :64位无符号整数
上面这些都是类型别名,编译器会指定它们指向的底层类型。比如,某个系统中,如果 int 类型为32位, int32_t 就会指向 int ;如果 long 类型为32位, int32_t 则会指向 long 。

#include <stdio.h>
#include <stdint.h>

int main() {
	int32_t x32 = 45933945;  //变量 x32 声明为 int32_t 类型,可以保证是32位的宽度。
	printf("x32 = %d\n", x32);
	return 0;
}

5 整型的极限值

有时候需要查看,当前系统不同整数类型的最大值和最小值,C 语言的头文件 `limits.h` 提供了相应的常量。比如:INT_MIN 代表 signed int 类型的最小值 -2147483648, INT_MAX 代表 signed int 类型的最大值 2147483647。

#include <limits.h>

int main() {
    printf("%d\n", INT_MIN  );  // -2147483648
    printf("%d\n", INT_MAX  );  // 2147483647
    return 0;
}

为了代码的可移植性,需要知道某种整数类型的极限值时,应该尽量使用这些常量。

  • SCHAR_MIN , SCHAR_MAX :signed char 的最小值和最大值。

  • SHRT_MIN , SHRT_MAX :short 的最小值和最大值。

  • INT_MIN , INT_MAX :int 的最小值和最大值。

  • LONG_MIN , LONG_MAX :long 的最小值和最大值。

  • LLONG_MIN , LLONG_MAX :long long 的最小值和最大值。

  • UCHAR_MAX :unsigned char 的最大值。

  • USHRT_MAX :unsigned short 的最大值。

  • UINT_MAX :unsigned int 的最大值。

  • ULONG_MAX :unsigned long 的最大值。

  • ULLONG_MAX :unsigned long long 的最大值。

2.4.2 浮点类型

1 类型说明

        浮点型变量,也称为实型变量,用来存储小数数值的。因为32位浮点数提供的精度或者数值范围还不够,C 语言又提供了另外两种更大的浮点数类型。
        在C语言中,浮点型变量分为三种:单精度浮点型(float)、双精度浮点型(double)、长双精度浮点型(long double)。

类型占用空间取值范围
float4个字节 (=32位)$-1.410^{-45}$ ~ $-3.410^{+38}$,$1.410^{-45}$ ~ $3.410^{+38}$
double8个字节 (=64位)$-4.910^{-324}$ ~ $-1.710^{+308}$,$4.910^{-324}$ ~ $1.710^{+308}$
long double12个字节(=96位)太大了...

其中,

类型16位编译器32位编译器64位编译器
float4字节4字节4字节
double8字节8字节8字节

C语言的第3种浮点类型是long double,以满足比double类型更高的精度要求。不过,C只保证long double类型至少与double类型的精度相同。
浮点型变量不能使用signed或unsigned修饰符。
最常用的浮点类型为:double 类型,因为精度比float高。
浮点型常量,默认为 double 类型。

关于后缀:

对于浮点数,编译器默认指定为 double 类型,如果希望指定为float类型,需要在小数后面添加后缀 `f`或`F`;如果希望指定为long double类型,需要在小数后面添加后缀 `l`或`L`。

float x       = 3.14f;
double x      = 3.14;
long double x = 3.14L;

2 举例

举例1:

float f = 123.4f;  //后面必须加上字母f
double d1 = 101.1; //后面可以省略字母d
double d2 = 299.4;  //后面可以加上字母d

举例2:

C 语言允许使用科学计数法表示浮点数,使用字母 e 来分隔小数部分和指数部分。注意,e 的前后,不能存在空格。

double x = 123.456e+3; // 123.456 x 10^3
// 等同于
double x = 123.456e3;

另外,科学计数法的小数部分如果是 0.x 或 x.0 的形式,那么 0 可以省略。

0.3E6
// 等同于
.3E6
    
3.0E6
// 等同于
3.E6

举例3:可以在常量的末尾加专用字符,强制指定常量的类型

float a = 3.14159f; //把此3.14159按单精度浮点常量处理
long double a = 1.23L; //把此1.23作为long double型处理

举例4:

有人用温度计测量出用华氏法表示的温度(如64°F),今要求把它转换为以摄氏法表示的温度(如17.8℃)。转换的公式为:$c = \frac{5}{9}(f - 32)$。

其中,f 代表华氏温度,c 代表摄氏温度。

#include <stdio.h>

int main() {
    float f, c; //定义f和c分别表示华氏温度、摄氏温度
    f = 64.0; //指定f的值
    c = (5.0 / 9) * (f - 32); //利用公式计算c的值
    printf("f=%f\nc=%f\n", f, c); //输出c的值
    return 0;
}

3 存储规则

        任何有小数点的数值,都会被编译器解释为浮点数。所谓“浮点数”就是使用 m * b^e 的形式,存储一个数值, m 是小数部分, b 是基数, e 是指数部分。

从十进制的角度:

从二进制的角度:

根据国际标准IEEE(电气和电子工程协会) 754,任意一个二进制浮点数V可以表示成下面的形式:$(-1)^S * M * 2^E$

其中:

- $(-1)^s$表示符号位,当s=0,V为正数;当s=1,V为负数。

- M表示有效数字,大于等于1,小于2。

- $2^E$表示指数位。

举例来说:

十进制的5.0,写成二进制是 101.0 ,相当于 1.01×2^2 。即,按照上面V的格式,可以得出s=0,M=1.01,E=2。

十进制的-5.0,写成二进制是 -101.0 ,相当于 -1.01×2^2 。即,s=1,M=1.01,E=2。

IEEE 754规定:

对于32位的浮点数,最高的1位是符号位s,接着的8位是指数E,剩下的23位为有效数字M。

对于64位的浮点数,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M。

浮点数的存储方式,决定了浮点数精度控制在一定范围内。有效数字部分可能丢失,造成精度损失。

2.4.3 字符类型

C语言中,使用 char 关键字来表示字符型,用于存储一个`单一字符`。
字符型变量赋值时,需要用一对英文半角格式的单引号(`''`)把字符括起来。
每个字符变量,在16位、32位或64位编译器中都是`占用 1 个字节(=8位)`。

表示方式1:最常见

char c = 'A'; //为一个char类型的变量赋值字符'A'

每个字符对应一个整数(由 ASCII 码确定),比如 A 对应整数 65 。
只要在字符类型的范围之内,整数与字符是可以互换的,都可以赋值给字符类型的变量。、

表示方式2:ASCII 码值

char c = 66;
// 等同于
char c = 'B';

两个字符类型的变量可以进行数学运算。

char a = 'B'; // 等同于 char a = 66;
char b = 'C'; // 等同于 char b = 67;
printf("%d\n", a + b); // 输出133

常见的ASCII值与对应的字符如下:(ASCII数值范围为0-127)

ASCII码:上个世纪60年代,美国制定了一套字符编码,对英语字符二进制位之间的关系,做了统一规定。这被称为ASCII码。ASCII码一共规定了127个字符的编码,比如空格“SPACE”是32(二进制00100000),大写的字母A是65(二进制01000001)。这128个符号(包括32个不能打印出来的控制符号),只占用了一个字节的后面7位,最前面的1位统一规定为0,也就是说,ASCII虽然用8位二进制编码表示字符,但是其有效位为7位。

举例1:字符′1′和整数1是不同的概念。(参看ASCII码表)

char c1 = 1;
char c2 = '1';
printf("c1 = %d\n",c1); // c1 = 1
printf("c2 = %d\n",c2); // c2 = 49

举例2:

char c='?'; //定义c为字符型变量并使初值为字符'?'。'?'的ASCII代码是63,系统把整数63赋给变量c。
printf("%d %c\n",c,c); //用"%d"格式输出十进制整数63,用"%c"格式输出字符'?'

signed 和 unsigned 修饰:

根据C90标准,C语言允许在关键字char前面使用signed或unsigned。

signed char c; // 范围为 -128 到 127
unsigned char c; // 范围为 0 到 255

注意,C 语言规定 char 类型默认是否带有正负号,由当前系统决定,这一点与 int 不同, int 等同于 signed int 。这就是说, char 不等同于signed char ,它有可能是 signed char(范围-128 到 127) ,也有可能是 unsigned char (范围0 到255)。不管是哪种,范围都正好都能覆盖 0 到 127 的 ASCII 字符范围。

表示方式3:使用转义字符

单引号本身也是一个字符,如果要表示这个字符常量,必须使用反斜杠转义。

char t = '\'';

char还可以用来表示转义字符。比如:

字符形式含义
\n换行符(光标移动到下行行首)
\t水平制表符,光标移到下一个Tab位置
\'单引号字符 '
\"双引号字符 "
\\反斜杠字符 ’\’
\r回车符,光标移到本行开头
\0null 字符,代表没有内容。注意,这个值不等于数字0。
\b退格键,光标回退一个字符,但不删除字符

2.4.4 布尔类型

C语言标准(C89)没有为布尔值单独设置一个类型,所以在判断真假时,使用整数 0 表示假,所有非0表示真。比如:

int main(){
    int handsome = 1;
    
    if (handsome) {
        printf("我好帅!\n");
    }
    
	return 0;
}

上述做法不直观,可以借助于C语言的宏定义处理。比如:

// 定义布尔类型的宏
#define BOOL int   //可以使用 typedef int BOOL; 替换
#define TRUE 1
#define FALSE 0


int main(){
    BOOL handsome = TRUE;
    
    if(handsome){
        printf("好帅~");
    }

    return 0;
}

此外,C99 标准添加了类型 `_Bool`,表示布尔值,即逻辑值true和false。但是,这个类型的值其实只是整数类型的别名,还是使用 0 表示false, 1 表示true,其它非0的值都会被存储为1。所以_Bool类型实际上也是一种整数类型。

#include <stdio.h>

int main() {
    _Bool isFlag = 1;
    if (isFlag)
        printf("你好毒~~\n");
    return 0;
}

与此同时,C99还提供了一个头文件 stdbool.h,文件中定义了`bool`代表`_Bool`,并且定义了 true 代表 1 、 false 代表 0 。只要加载这个头文件,就可以使用 bool 定义布尔值类型,以及 false 和 true 表示真假。

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

int main() {
    bool isFlag = true;
    if (isFlag)
        printf("你好毒~~\n");
    return 0;
}

【武汉科技大学2019研】

以下选项中不属于C语言类型的是(  )。
A.short int
B.unsigned long int
C.char
D.bool

【答案】D

【解析】C语言中没有bool型,只有C++才有boolean型,也称bool。C语言中一般用“0”表示“假”,用“1”表示“真”。

【四川大学2017研】有4个圆塔,圆心分别为(2,2)、(-2,2)、(-2,-2)、(2,-2),圆半径为1。这4个塔的高度为10m,塔以外无建筑物。今输入任一点的坐标,求该点的建筑高度(塔外的高度为零)。

【答案】 N-S图如图1所示。

        图1 计算某点建筑高度的N-S流程图 程序如下:

#include<stdio.h>

int main() {

int h = 10;
float x1 = 2, y1 = 2, x2 = -2, y2 = -2, x3 = -2, y3 = -2, x4 = 2, y4 = -2;
float x, y; //表示随意选中的一个点的坐标
float d1, d2, d3, d4; //(x,y)这个点的坐标到各个圆心的距离
printf("请输入一个点(x,y):");
scanf("%f,%f", &x, &y);
d1 = (x - x1) * (x - x1) + (y - y1) * (y - y1); //求该点到各中心点距离
d2 = (x - x2) * (x - x2) + (y - y2) * (y - y2);
d3 = (x - x3) * (x - x3) + (y - y3) * (y - y3);
d4 = (x - x4) * (x - x4) + (y - y4) * (y - y4); 
if (d1 > 1 && d2 > 1 && d3 > 1 && d4 > 1) //判断该点是否在塔外  
  h = 0;
printf("该点高度为%d\n", h);
return 0;
}

2.5 变量间的运算规则

在C语言编程中,经常需要对不同类型的数据进行运算,运算前需要先转换为同一类型,再运算。为了解决数据类型不一致的问题,需要对数据的类型进行转换。

2.5.1 隐式类型转换

情况1:窄类型自动转为宽类型

即,系统自动将`字节宽度较小`的类型转换为`字节宽度较大`的数据类型,它是由系统自动转换完成的。基本数据类型的转换规则如图所示:

注意:最好避免无符号整数与有符号整数的混合运算。因为这时 C 语言会自动将 signed int 转为unsigned int ,可能不会得到预期的结果。

举例1:

- 不同的整数类型混合运算时,宽度较小的类型会提升为宽度较大的类型。比如 short 转为 int ,int 转为 long 等。
- 不同的浮点数类型混合运算时,宽度较小的类型转为宽度较大的类型,比如 float 转为double , double 转为 long double 。

float y = 12 * 2; //整数赋值给浮点数变量时,会自动转为浮点数。结果24.0

//char类型 与 int类型运算,会自动提升为 int 。
char c = 10;
int i = 10;
int j = c + i;  //ok

short s1 = 10;
int num1 = s1;    //ok
double num2 = s1; //ok

int i = 10;
double d1 = 12.3;
double d2 = i + d1; //系统自动将i的类型由int转换为double类型,故i+d1结果为double类型

double d;
d = 2 + 'A' + 1.5F;

举例2:

两个相同类型的整数运算时,或者单个整数的运算,一般来说,运算结果也属于同一类型。但是有例外,宽度小于 int 的类型,运算结果会自动提升为 int 。

char c1 = 10;
short s1 = 10;
int i1 = c1 + s1;  //char类型和short类型的变量运算的结果默认为int类型

unsigned char a = 1;
unsigned char b = 255;
unsigned char c = 255;

if ((a - 5) < 0) 
    do_something();

if ((b + c) > 300) 
    do_something();

说明:表达式 a - 5 和 b + c 都会自动转为 int 类型,所以函数 do_something() 会执行两次。

情况2:宽类型赋值给窄类型

字节宽度较大`的类型,赋值给`字节宽度较小`的变量时,会发生类型降级,自动转为后者的类型。这时可能会发生截值(truncation),系统会自动截去多余的数据位,导致精度损失。

这反映了C语言在检查类型匹配方面不太严格。最好不要养成这样的习惯。

举例1:

double pi = 3.14159;
int i = pi; // i 的值为 3

C编译器把浮点数转换成整数时,会直接丢弃(截断)小数部分,而不进行四舍五入。

举例2:

int x = 3.14; //浮点数赋予整数变量时,C 语言直接丢弃小数部分。结果 3

int cost = 12.99;              // double类型的值转为int类型,结果为:12
float pi = 3.1415926536;     // double类型的值转为float类型,结果为:3.141593

举例3:

int i = 322;
char ch = i; // ch 的值是 66

图示:

举例4:

float f1 = 1.1f; //ok
double d2 = 4.58667435;
f1 = d2; // 出现精度损失 (double -> float )

printf("f1=%.8f", f1); // 期望: 4.58667435

由于存在精度限制,浮点数只是一个近似值,它的计算是不精确的。

举例5:

float a = 3.14159; //3.14159为双精度浮点常量,分配8个字节;a为float变量,分配4个字节

编译时系统会发出警告(warning: truncation from ′const double′ to′float′),提醒用户注意这种转换可能损失精度。

2.5.2 强制类型转换

        隐式类型转换中的宽类型赋值给窄类型,编译器是会产生警告的,提示程序存在潜在的隐患。如果非常明确地希望转换数据类型,就需要用到`强制(或显式)类型转换`。

形式: **(类型名称)(变量、常量或表达式)**
功能:将“变量、常量或表达式”的运算结果强制转换为“类型名称”所表示的数据类型。
注意:强制类型转换会导致精度损失。

举例:

double x = 12.3;
int y = 10;
int z = (int)x + y; //将变量x的值转换成int后,再与y相加

将浮点数转换为整数时,将舍弃浮点数的小数部分,只保留整数部分。

float f1,f2;
f1 = (int)1.2 + 3.4;
f2 = (int)(1.2 + 3.4);
printf("f1=%f,f2=%f",f1,f2);

输出结果:f1=4.4,f2=4.0。

举例2:

int i = 40000;
short s = (short)i;
printf("%d\n",s); //-25536

举例3:

long y = (long) 10 + 12; // (long) 将 10 显式转为 long 类型。这里的显示转换其实是不必要的,因为可以自动转换

2.5.3 运算的溢出问题

每一种数据类型都有数值范围,如果存放的数值超出了这个范围(小于最小值或大于最大值),需要更多的二进制位存储,就会发生溢出。大于最大值,叫做`向上溢出(overflow)`;小于最小值,叫做`向下溢出(underflow)`。

一般来说,编译器不会对溢出报错,会正常执行代码,但是会忽略多出来的二进制位,只保留剩下的位,这样往往会得到意想不到的结果。所以,应该避免溢出。

举例1:

unsigned char x = 255;
x = x + 1;
printf("%d\n", x); // 0

 x 是 unsign char 类型,最大值是255 (二进制 11111111 ),加 1 后就发生了溢出, 256 (二进制 100000000 )的最高位 1 被丢弃,剩下的值就是 0 。

举例2:

unsigned int ui = UINT_MAX;  // 4,294,967,295
ui++;
printf("ui = %u\n", ui); // 0
ui--;
printf("ui = %u\n", ui); // 4,294,967,295

常量 UINT_MAX 是 unsigned int 类型的最大值。如果加 1 ,对于该类型就会溢出,从而得到 0 ;而 0 是该类型的最小值,再减 1 ,又会得到 UINT_MAX 。

溢出很容易被忽视,编译器又不会报错,所以必须非常小心。

2.6 常量

2.6.1 常量分类

程序运行时,其值不能改变的量,即为`常量`。

C语言中的常量分为以下以下几种:
- 字面常量
- \#define 定义的标识符常量
- const 修饰的常变量
- 枚举常量

举例:字面常量

1、2、12是整型常量,2.1、12.5、3.14是实型常量,'a'、 'b'、'c'是字符型常量。

#include <stdio.h>

int main(){
    //字面常量
    3.14;//字面常量
    1000;//字面常量
}

2.6.2 多种方式定义常量

1 使用#define

这种方式是在文件开头用 #define 来定义常量,也叫作“宏定义”。所谓宏定义,**就是用一个标识符来表示一个常量值**,如果在后面的代码中出现了该标识符,那么编译时就全部替换成指定的常量值。即用宏体替换所有宏名,简称`宏替换`。

定义格式:`#define 符号常量名 常量值`

- 符号常量名,称为`宏体`,属于标识符,一般定义时用大写字母表示。
- 常量值,称为`宏名`,可以是数值常量,也可以是字符常量。

习惯上,**宏名用大写字母表示**,以便于与变量区别。但也允许用小写字母。

举例1:

#include <stdio.h>

#define ZERO 0   //#define的标识符常量

int main() {
    printf("zero = %d\n", ZERO);
    return 0;
}

跟#include一样,“#”开头的语句都是“预处理语句”,在编译之前,预处理器会查找程序中所有的“ZERO”,并把它替换成0,这个过程称为预编译处理。

然后将预处理的结果和源程序一起再进行通常的编译处理,以得到目标代码 (OBJ文件)。

举例2:

#include <stdio.h>

#define PI = 3.14  // 定义常量 PI,常量值 3.14。因为宏定义不是 C 语句,后面不能有分号

int main() {
   //PI = 3.1415  可以吗? => 不可以
  double area;
  double r = 1.2;
  area = PI * r * r;
  printf("面积 : %.2f", area);
  getchar();
  return 0;
}

举例3:

//函数结果状态代码
#define OK 1
#define ERROR 0
#define OVERFLOW -2

#define 对于考研数据结构来说没有什么贡献,我们只要认得它就行。

例如1,`#define MAX_Size 50`这句,即定义了常量`MAX_Size`(此时x = 50;等价于x = MAX_Size;)。

例如2,你要定义一个数组,如`int A[MAX_Size];`,加上一句注释“`/*MAX_Size为已经定义的常量,其值为50*/`”即可。

2 使用const限定符

C99中新的声明方式,这种方式跟定义一个变量是一样的,只需要在变量的数据类型前再加上一个const关键字,这被称为“限定符”。

格式:const 数据类型 常量名 = 常量值;

举例:

#include <stdio.h>

int main(){
	//const 修饰的常变量
	const float PI = 3.14f;
	//PI = 5.14;//是不能直接修改的!

	return 0;
}

const修饰的对象一旦创建就不能改变,所以必须初始化。

跟使用 #define定义宏常量相比,const定义的常量有详细的数据类型,而且会在编译阶段进行安全检查,在运行时才完成替换,所以会更加安全和方便。

3 定义枚举常量

举例:

#include <stdio.h>

//使用enum定义枚举类
enum Sex{
    //括号中的MALE,FEMALE,SECRET是枚举常量
	MALE,
	FEMALE,
	SECRET
};

int main(){
	//枚举常量
	printf("%d\n", MALE);
	printf("%d\n", FEMALE);
	printf("%d\n", SECRET);
	//注:枚举常量默认是从0开始,依次向下递增1的

    enum Sex gender = FEMALE;
    printf("性别:%d", gender);
	return 0;
}

在C/C++中,枚举类型的元素默认从0开始递增。当你定义枚举类型Sex时,它会隐式地为每个元素分配一个整数值,依次从0开始。在上述代码中,MALE没有指定具体的整数值,因此它的值默认为0;对应的,FEMALE之后出现,其值为1。如果你希望显式地为枚举类型指定其他整数值,可以像下面这样进行修改:

```c
enum Sex{
    MALE = 5,
    FEMALE = 10,
    SECRET = 15
};
```

这样定义后,MALE=5,FEMALE=10,SECRET=15,你可以根据自己的需求来指定枚举元素对应的整数值。

【北京航空航天大学2018研】若已知有如下宏定义

#define  CANBERRA(x,y)  ((x-y)/(x+y))

则以下表达式中,返回结果值最大的是(  )。
A.CANBERRA(3.0,2.0);
B.CANBERRA(4.0,1.0);
C.CANBERRA(1.0+2.0,0.0+2.0);
D.CANBERRA(1.0+2.0,1.0+1.0);

【答案】C

【解析】A项中为1.0/5.0,结果为0.2;B项中为3.0/5.0,结果为0.6;C项中的宏替换后为(1.0+2.0-0.0+2.0)/(1.0+2.0+0+2.0)=1.0;D项中宏替换后为(1.0+2.0-1.0+1.0)/(1.0+2.0+1.0+1.0)=0.6,因此最后答案为C。

【中央财经大学2018研】若有如下宏定义:

#define  N  2
#define  y(n)  ((N+1)*n)

则执行下列语句:z=4*(N+y(5));后的结果是(  )。
A.语句有错误
B.z值为68
C.z值为60
D.z值为180

【答案】B

【解析】y(5)=15,z=4\*(N+y(5))=4*17=68,答案选B。

2.7 输入/输出函数

所谓输入输出是以计算机主机为主体而言的。

- 输出:从计算机向外部输出设备(显示器、打印机)输出数据。
- 输入:从输入设备(键盘、鼠标、扫描仪)向计算机输入数据。

c语言本身没有提供专门的输入输出语句,所有的输入输出都是由调用标准库函数中的输入输出函数来实现的。

输入函数:scanf() 、 getchar()、gets():

  • scanf(),是格式输入函数,可接收任意类型的数据。

  • getchar(),是字符输入函数, 只能接收单个字符

  • gets(),是字符串输入函数。

输出函数:printf() 、 putchar()、puts():

  • printf(),是格式输出函数,可按指定的格式显示任意类型的数据。

  • putchar(),字符显示函数,只能显示单个字符

puts(),是字符串输出函数。

2.7.1 scanf()的使用

scanf()函数的作用:把从键盘上输入的数据根据找到的地址存入内存中,即给变量赋值。

格式: `scanf("格式控制字符串",参数地址列表); `

- “格式控制字符串”:约定输入数据的类型和格式,参数的个数必须与变量地址的个数一致。
- “参数地址列表”:以逗号 “, ”分隔的、`输入数据变量地址`序列。

举例:

scanf("%d%d%d",&a,&b,&c)

其中,&a,&b,&c中的`&`是寻址操作符,&a表示对象a在内存中的地址。

注意,

- 如果scanf中%d是连着写的,如“`%d%d%d`”,在输入数据时,数据之间不可以用逗号分隔,只能用空白字符(空格或tab键或者回车键)分隔。即“`2(空格)3(tab)4`” 或 “`2(tab)3(回车)4`”等。
- 如果是“`%d,%d,%d`”,则在输入数据时需要加“,”,如“`2,3,4`”。

举例1:计算圆的面积,其半径由用户指定

#include <stdio.h>

int main() {
    float radius, area;
    printf("请输入半径值:  ");
    scanf("%f", &radius);      //输入半径
    area = 3.14 * radius * radius;
    printf("area=%f\n", area); //输出圆的面积
    
    return 0;
}

注意:变量名之前要加上&运算符,表示取变量的地址,如“&a,&b”。否则将会出现错误。

举例2:输入一个整数,求其绝对值。

#include <stdio.h>

int main() {

    int num;
    printf("输入一个整数:");

    scanf("%d", &num);
    int absNum = num;
    if(absNum < 0)
        absNum = -absNum;
    printf("\n 整数:%d--->绝对值为:%d\n", num, absNum);

    return 0;
}

举例3:输入多个变量的值,求乘积

#include <stdio.h>

int main() {
    int a,b,c;
    printf("请输入整数a,b:");
    scanf("%d%d",&a,&b);
    c=a*b;
    printf("%d*%d=%d\n",a,b,c);
    
    return 0;
}

【武汉科技大学2019研】若有声明语句:int x; char y[20]; double z;则正确的输入语句是( )。

A.scanf("%d%c%le\n",&x,&y,&z);

B.scanf("%2d%s%lf",&x,&y,&z);

C.scanf("%d%s%lf",&x,y,&z);

D.scanf("%x%s%3.2f",&x,y,&z);

【答案】C

【解析】y为一维数组名,指向数组首元素的地址,因此不需要再使用取地址运算符&,AB错误;D中%3.2f表示长度为3,小数为2位,但是小数点也占一位,因此D错误,答案选C。

在C语言的`scanf`函数中,格式指示符`%f`或`%lf`用于读取浮点数,而不支持指定小数点的位置和长度。当使用`%3.2f`作为格式控制字符串时,`scanf`会尝试匹配精确的格式,即3位整数部分和2位小数部分,但前提是小数点前有空间。然而,在这种情况下,它会因为小数点的字符位置而无法正确解析输入

因此,可以通过分开整数部分和小数部分来解决这个问题,如下所示:

```c
int x;
char y[20];
double z;

scanf("%x %s %lf", &x, y, &z);
```

这样,使用`%lf`来读取double型变量,而不指定小数点位置和长度,`scanf`会正确的读取浮点数值,并将其赋值给变量`z`,避免了原始问题中小数点导致的错误。

2.7.2 getchar()与putchar()的使用

- getchar():输入字符数据
  - 格式:getchar()
  - 功能:从键盘缓冲区读入一个字符

- putchar():输出字符
  - 格式: putchar(ch),其中ch是一个字符变量
  - 功能:从标准输出设备输出一个字符

举例:

#include <stdio.h>

int main() {
    char c = 0;
    putchar('A'); //输出单个字符A
    putchar(c);   //输出变量c的ASCII对应字符
    putchar('\n'); //执行换行效果,屏幕不显示
}

#include <stdio.h>

int main() {
    char ch;
    ch = getchar();
    putchar(ch);
    return 0;
}

2.7.3 gets()与puts()的使用(超纲)

puts():

在C语言中,puts() 是一个用于输出字符串的标准库函数,其原型定义在 `<stdio.h>` 头文件中。`puts()` 函数的作用是将一个以 null 字符(`\0`)结尾的字符串打印到标准输出(通常是控制台)上,并自动添加一个换行符。

int main() {

    char str1[]={"China\nBeijing"};
    char str2[] = "helloworld";

    puts(str1);

    puts(str2);

    return 0;
}

注意,puts()函数只能用于输出字符串,而不能输出其他类型的数据。如果需要输出其他类型的数据,应使用 `printf()` 函数。

gets():

读取标准输入设备输入的字符串,直到遇到【Enter】键才结束。

char str[20];   //定义一个数组
gets(str);      //获取输入的字符串,存放到字符数组中

举例:字符串的读写

int main() {

    char str[15];
    printf("enter your name:");
    gets(str);        //输入字符串至数组变量str
    printf("your name is ");
    puts(str);        //输出字符串

    return 0;
}

2.8 变量按声明位置的分类

变量按照声明的位置,可以分为:局部变量 和 全局变量。

  • 局部变量

    • 函数体内定义的变量,也称内部变量。局部变量只能在定义它的函数中使用。

  • 全局变量

    • 函数之外定义的变量称为外部变量,外部变量是全局变量(也称全程变量)。

    • 一个程序中,凡是在全局变量之后定义的函数,都可以使用在其之前定义的全局变量。

举例:

#include <stdio.h>

int global = 2023;//全局变量
int main(){
    int local = 2022;//局部变量
    
    //下面定义的global会不会报错?
    int global = 2024;//局部变量
    printf("global = %d\n", global);
    return 0;
}

当局部变量和全局变量同名的时候,局部变量优先使用。

2.9 常见的进制

2.9.1 二进制概述

计算机底层如何存储数据呢?

计算机世界中只有二进制,所以计算机中存储和运算的`所有数据`都要转为`二进制`。包括数字、字符、图片、声音、视频等。

二进制的由来

二进制,是计算技术中广泛采用的一种数制,由德国数理哲学大师`莱布尼茨`于1679年发明。
二进制数据是用0和1两个数码来表示的数。它的基数为2,进位规则是“`逢二进一`”。

二进制的应用

二进制广泛应用于我们生活的方方面面。比如,广泛使用的摩尔斯电码(Morse Code),它由两种基本信号组成:短促的点信号“`·`”,读“`滴`”;保持一定时间的长信号“`—`”,读“`嗒`”。然后,组成了26个字母,从而拼写出相应的单词。

我们偶尔会看到的:SOS,即为:

2.9.2 进制的分类

十进制(decimal)

- 数字组成:0-9
- 进位规则:满十进一
- C 语言的整数默认都是十进制数

二进制(binary)

- 数字组成:0-1
- 进位规则:满二进一,以`0b`或`0B`开头

int x = 0b101010;

八进制(octal):很少使用

- 数字组成:0-7
- 进位规则:满八进一,以数字`0`开头表示

int a = 012; // 八进制,相当于十进制的10
int b = 017; // 八进制,相当于十进制的15

十六进制

- 数字组成:0-9,a-f
- 进位规则:满十六进一,以`0x`或`0X`开头表示。此处的 a-f 不区分大小写

int a = 0x1A2B; // 十六进制,相当于十进制的6699
int b = 0X10;   // 十六进制,相当于十进制的16

2.9.3 进制的换算举例

十进制二进制八进制十六进制
0000
1111
21022
31133
410044
510155
611066
711177
81000108
91001119
10101012a或A
11101113b或B
12110014c或C
13110115d或D
14111016e或E
15111117f或F
16100002010

2.9.4 输出格式

不同的进制只是整数的书写方法不同,不会对整数的实际存储方式产生影响。不同进制可以混合使用,比如 10 + 015 + 0x20 是一个合法的表达式。

printf() 的进制相关占位符如下:

- %d :十进制整数。
- %o :八进制整数。
- %x :十六进制整数。
- %#o :显示前缀 0 的八进制整数。
- %#x :显示前缀 0x 的十六进制整数。
- %#X :显示前缀 0X 的十六进制整数。

int x = 100;
printf("dec = %d\n", x); // 100
printf("octal = %o\n", x); // 144
printf("hex = %x\n", x); // 64
printf("octal = %#o\n", x); // 0144
printf("hex = %#x\n", x); // 0x64
printf("hex = %#X\n", x); // 0X64

2.9.5 进制间的转换

1 二进制如何表示整数?
  • 计算机数据的存储使用二进制补码形式存储,并且最高位是符号位

    • 正数:最高位是0

    • 负数:最高位是1

  • 规定1:正数的补码与反码、原码一样,称为三码合一

  • 规定2:负数的补码与反码、原码不一样:

    • 负数的原码:把十进制转为二进制,然后最高位设置为1

    • 负数的反码:在原码的基础上,最高位不变,其余位取反(0变1,1变0)

    • 负数的补码:反码+1

2 二进制与十进制间的转换

二进制转十进制:权相加法

针对于一个字节的数据举例来说:

例如:1个字节(8位)

25 ==> 原码  0001 1001 ==> 反码  0001 1001 -->补码  0001 1001

-25 ==>原码  1001 1001 ==> 反码1110 0110 ==>补码 1110 0111

整数:
正数:25   00000000 00000000 000000000 00011001(原码)
正数:25   00000000 00000000 000000000 00011001(反码)
正数:25   00000000 00000000 000000000 00011001(补码)

负数:-25  10000000 00000000 000000000 00011001(原码)
负数:-25  11111111 11111111 111111111 11100110(反码)
负数:-25  11111111 11111111 111111111 11100111(补码)

一个字节可以存储的整数范围是多少?

//1个字节:8位

0000 0001  ~  0111 111 ==> 1~127

1000 0001 ~ 1111 1111 ==> -127 ~ -1

0000 0000 ==>0

1000 0000 ==> -128(特殊规定)=-127-1

十进制转二进制

十进制转二进制:`除2取余的逆`

3 二进制与八进制、十六进制间的转换

二进制转八进制

**二进制转十六进制**

八进制、十六进制转二进制

练习:以下叙述中错误的是(  )。
A.C程序在运行过程中所有计算都以十进制方式进行
B.C程序在运行过程中所有计算都以二进制方式进行
C.所有C程序都需要编译链接无误后才能运行
D.C程序中字符变量存放的是字符的ASCII值

【答案】A

【解析】C程序在运行过程中所有计算都以二进制方式进行。答案选择A选项。

【华南理工大学2018研】与十进制1100等值的十六进制数是(  )。 A.44A B.44C C.54A D.54C

【答案】B

【解析】1100转换成二进制为0100 0100 1100,因此转换为十六进制为44C。

3. 运算符与流程控制

3.1 运算符(Operator)

运算符是一种特殊的符号,用以表示数据的运算、赋值和比较等。

运算符的分类:

- 按照`功能`分为:算术运算符、赋值运算符、比较(或关系)运算符、逻辑运算符、位运算符、条件运算符、sizeof运算符

分类运算符
算术运算符+、-、+、-、*、/、%、++、--
赋值运算符=、+=、-=、*=、/=、%=等
比较(或关系)运算符>、>=、<、<=、==、!=
逻辑运算符&&、||、!
位运算符&、|、^、~、<<、>>
条件运算符(条件表达式)?结果1:结果2
sizeof运算符sizeof()

- 按照`操作数个数`分为:一元运算符(单目运算符)、二元运算符(双目运算符)、三元运算符 (三目运算符)

分类运算符
一元运算符(单目运算符)正号(+)、负号(-)、++、--、!、~
二元运算符(双目运算符)除了一元和三元运算符剩下的都是二元运算符
三元运算符 (三目运算符)(条件表达式)?结果1:结果2

3.1.1 算术运算符

算术运算符专门用于算术运算,主要有下面几种。

int x = -12;
int y = -x;
int z = +y; //+可以省略

int a = 4 + 2;

int num = 5;
printf("%d\n", num * num); // 输出 25

float x = 6 / 4;
printf("%f\n", x); // 输出 1.000000

float x = 6.0 / 4; // 或者写成 6 / 4.0
printf("%f\n", x); // 输出 1.500000

%,运算结果的符号与被模数相同

int x1 = 6 % 4; // 2
int x2 = -6 % 4; // -2
int x3 = 6 % -4; // 2
int x4 = -6 % -4; // -2

自加自减运算

理解:++ 运算,表示自增1。同理,-- 运算,表示自减1,用法与++ 一致。

1、单独使用

  • 变量在单独运算的时候,变量前++和变量后++,是没有区别的。

  • 变量前++ :例如 ++a

  • 变量后++ :例如 a++

int main() {
    int a = 10;

    //++a;
    a++;
    //无论是变量前++还是变量后++,结果都是11
    printf("%d\n",a);

    return 0;
}

2、复合使用

  • 和`其他变量放在一起使用`或者和`输出语句放在一起使用`,`前++`和`后++`就产生了不同。
  • 变量`前++` :变量先自增1,然后再运算。
  • 变量`后++` :变量先运算,然后再自增1。

int main() {
    // 其他变量放在一起使用
    int x = 3;
    //int y = ++x; // y的值是4,x的值是4,
    int y = x++; // y的值是3,x的值是4

    printf("%d\n",x);
    printf("%d\n",y);
    printf("==========\n");

    // 和输出语句一起
    int z = 5;
    //printf("%d\n",++z);// 输出结果是6,z的值也是6
    printf("%d\n",z++);// 输出结果是5,z的值是6
    printf("%d\n",z);

    return 0;
}

与此对应的:

- 变量`前--` :变量先自减1,然后再运算。
- 变量`后--` :变量先运算,然后再自减1。

案例1:

随意给出一个整数,打印显示它的个位数,十位数,百位数的值。
格式如下:
数字xxx的情况如下:
个位数:
十位数:
百位数:

例如:
数字153的情况如下:
个位数:3
十位数:5
百位数:1

#include <stdio.h> 
    
int main() {
    int num = 153;

    int bai = num / 100;
    int shi = num % 100 / 10;//int shi = num / 10 % 10;
    int ge = num % 10;

    printf("百位为:%d\n", bai);
    printf("十位为:%d\n", shi);
    printf("个位为:%d\n", ge);

    return 0;
}

**案例2:**为抵抗洪水,战士连续作战89小时,编程计算共多少天零多少小时?

#include <stdio.h>

int main() {
    int hours = 89;
    int day = hours / 24;
    int hour = hours % 24;
    printf("为抵抗洪水,战士连续作战%d小时:是%d天%d小时\n",hours,day,hour);

    return 0;
}

3.1.2 赋值运算符

  • 符号 =

    • 当“=”两侧数据类型不一致时,可以使用自动类型转换或使用强制类型转换原则进行处理。

    • 支持连续赋值

  • 扩展赋值运算符: +=、 -=、*=、 /=、%=

运算符名称实例展开形式
+=复合加赋值a+=ba=a+b
-=复合减赋值a-=ba=a-b
*=复合乘赋值a*=ba=a*b
/=复合除赋值a/=ba=a/b
%=复合模赋值a%=ba=a%b

i += 3;  // 等同于 i = i + 3
i -= 8;  // 等同于 i = i - 8
i *= 9;  // 等同于 i = i * 9
i /= 2;  // 等同于 i = i / 2
i %= 5;  // 等同于 i = i % 5

//连续赋值的测试
//以前的写法
int a1 = 10;
int b1 = 10;

//连续赋值的写法
int a2,b2;
a2 = b2 = 10;

int a3 = 10,b3 = 20;

int a = 10;
a += 1.7;  // a=a+1.7 => 11.7 => 11
printf("a=%d", a);  // a=11

int a, b, c;
a = (b = 5);    //表达式的值为5,将5赋值给b,接着将b的值赋值给a
a = 5 + (c = 6);        //表达式值为11,a值为11,c值为6
a = (b = 4) + (c = 6);    //表达式值为10,a值为10,b等于4,c等于6
a = (b = 10) / (c = 2);    //表达式值为5,a等于5,b等于10,c等于2
a = (b = 3 * 4);        //表达式值为12,a,b值均为12

【武汉科技大学2019研】

若a、b和t都为int变量,则下面不能交换变量a和b值的是(  )。
A.t=a; a=b; b=t;
B.a=t; t=b; b=a;
C.t=b; b=a; a=t;
D.a=a+b; b=a-b; a=a-b;

【答案】B

【解析】B中首先把t的值赋值给了a,则a的值已经被取代了,后面执行b=a,则ab的值都等于t的值。

【中央财经大学2018研】以下程序运行后的输出结果是( )。

int main(){  
double d;  
float f;  
long l;  
int i;  
i = f = l = d = 20/3;  
printf("%d %ld %.1f %.1f\n",i,l,f,d);  
return 0;
} 

A.6 6 6.0 6.0
B.6 6 6.7 6.7
C.6 6 6.0 6.7
D.6 6 6.7 6.0

【答案】A

【解析】赋值运算符是自右向左结合的,所以首先执行d=20/3=6,同时i、l、f也全为6,在进行输出时,f和d要保留一位小数,所以答案选A。

以下选项中正确的定义语句是(  )。
A.double a;b;
B.double a=b=7;
C.double a=7,b=7;
D.double,a,b;

【答案】C

【解析】同一类型变量的定义时,不同变量之间需要用“,”分隔,选项A错误;定义变量时初始化赋值不能用等号连接,选项B错误;变量类型说明后面不能用逗号,而是用空格分离,选项D错误。答案选择C选项。

3.1.3 比较运算符(或关系运算符)

常用的比较运算符:

关系运算符含义举例
>大于num > 10
>=大于等于num >= 10
<小于num < 10
<=小于等于num <= 10
==等于num == 10
!=不等于num != 10

- 比较运算的结果只有两个取值,要么是真(非0 表示,默认使用1),要么是假(0 表示)。
  - 比如, 20 > 12 返回 1 , 12 > 20 返回 0 。
- 比较运算符“`==`”不能误写成“`=`” 

举例1:

int i1 = 10;
int i2 = 20;

printf("%d\n",i1 == i2);//0
printf("%d\n",i1 != i2);//1
printf("%d\n",i1 >= i2);//0


int m = 10;
int n = 20;
printf("%d\n",m == n);//0
printf("%d\n",m = n);//20

举例2:多个关系运算符不宜连用。

i < j < k   //期望判断j是否大于i,且小于k

这是合法表达式,不会报错,但是通常达不到想要的结果,即不是保证变量 j 的值在 i 和 k 之间。比如:

//i < j < k
int j = 10;
if(15 < j < 20){
    printf("j大于15,且小于20"); //输出此语句
}else{
    printf("j不在15到20之间");
}

因为关系运算符是从左到右计算,所以实际执行的是:

(i < j) < k; //i < j 返回 0 或 1 ,所以最终是 0 或 1 与变量 k 进行比较

期望的效果应该写为:

//i < j < k
int j = 10;
if(15 < j && j < 20){
    printf("j大于15,且小于20"); 
}else{
    printf("j不在15到20之间"); //输出此语句
}

【北京航空航天大学2018研】若变量a,b,c的取值分别是1,2,3,则表达式“!((b+c)>(a+4))”的值是( )。

A.0 B.1 C.2 D.3

【答案】B

【解析】首先b+c等于5,a+4也等于5,因此 (b+c)>(a+4)为“假”,即0,对0取非结果为1,因此答案为B。

3.1.4 逻辑运算符

主要有下面三个运算符:

逻辑运算符描述功能举例
&&与运算符两个条件都要满足num1 >= 10 && num2 >= 20
||或运算符两个条件只需满足其一num1 >= 10 || num2 >= 20
!非运算符否定条件!(num1 >= 10)(等价于 num1 < 10)

逻辑运算符提供逻辑判断功能,用于构建更复杂的表达式。

举例:

aba && ba || b!a
1(真)1(真)1(真)1(真)0(假)
1(真)0(假)0(假)1(真)0(假)
0(假)1(真)0(假)1(真)1(真)
0(假)0(假)0(假)0(假)1(真)

对于逻辑运算符来说,任何非零值都表示真,零值表示伪。比如, 5 || 0 会返回 1 , 5 && 0 会返回0 。

int x = 5;
int y = 11;
if (x < 10 && y > 20)
    printf("今天天气真晴朗\n");

举例2:**短路现象**

- &&:a && b
  - 当 a 为假(或0)时,因为a && b 结果必定为 0,所以不再执行表达式 b
  - 当 a 为真(非0)时,因为a && b 结果不确定,所以会继续求解表达式b

int i = 0;
int j = 10;
if(i && j++ > 0){
    printf("床前明月光");
}else{
    printf("我叫郭德纲");
}

printf("%d\n",j); //10
  • || :a || b

    • 当 a 为真(非0)时,因为a || b 结果必定为 1,所以不再执行表达式 b

    • 当 a 为假(或0)时,因为a || b 结果不确定,所以会继续求解表达式b

int i = 1;
int j = 10;
if(i || j++ > 0){
    printf("床前明月光");
}else{
    printf("我叫郭德纲");
}

printf("%d\n",j); //10

练习:请写出如下程序运行后的结果

int main() {
    int x = 1;
    int y = 0;
    short z = 42;

    if ((z++ == 42) && (y = 1)) {
        z++; // z = 44
    }
    if ((x = 0) || (++z == 45)) {
        z++;  // z = 46
    }

    printf("z=%d", z);

    return 0;
}

答案:z=46

【华南理工大学2018研】设int a=3;,下列哪一个表达式的值等于0(  )。
A.a&&(a>0)
B.!a||a
C.a%=a
D.a>=a

【答案】C

【解析】A中a!=0且a>0所以表达式的值为1;B中||表示或,所以值也为1;D中表达式值也为1;答案选C。

【四川大学2017研】语句:printf("%d",(a=2) && (b=-2));的输出结果是(  )。
A.无输出
B.结果不确定
C.-1
D.1

【答案】D

【解析】a=2为真,b=-2也为真,所以输出1,答案选D。

3.1.5 位运算符

- C 语言提供一些位运算符,用来操作二进制位(bit)。

- 位运算符的运算过程都是基于二进制的补码运算。

运算符描述运算规则
<<二进制左移将一个数的各二进制位全部左移指定的位数,左边的二进制位丢弃,右边补0。
>>二进制右移将一个数的各二进制位全部右移指定的位数,正数左补0,负数左补1,右边丢弃。
&按位与两个二进制位都为 1,结果为1,否则为0。
|按位或两个二进制位只要有一个为1(包含两个都为 1 的情况),结果为1,否则为0。
^按位异或两个二进制位一个为0,一个为1,结果为1,否则为0。
~按位取反将每一个二进制位变成相反值,即 0 变成 1 , 1 变成 0 。

- 结合赋值运算符的经验,这里有:<<= 、 >>= 、 &= 、 ^= 等

举例1:

(1)左移:<<

运算规则:在一定范围内,数据每向左移动一位,相当于原数据*2。(正数、负数都适用)

【注意】当左移的位数n超过该数据类型的总位数时,相当于左移(n-总位数)位

3<<4  类似于  3*2的4次幂 => 3*16 => 48

-3<<4  类似于  -3*2的4次幂 => -3*16 => -48

(2)右移:>>

运算规则:在一定范围内,数据每向右移动一位,相当于原数据/2。(正数、负数都适用)

【注意】

1、如果不能整除,`向下取整`。

2、右移运算符最好只用于无符号整数,不要用于负数。因为不同系统对于右移后如何处理负数的符号位,有不同的做法,可能会得到不一样的结果。

69>>4  类似于  69/2的4次幂 = 69/16 =4

-69>>4  类似于  -69/2的4次幂 = -69/16 = -5

练习:高效的方式计算2 * 8的值(经典面试题)

答案:2 << 3 、  8  << 1

举例2:

(1)按位与:&

运算规则:对应位都是1才为1,否则为0。

- 1 & 1 结果为1

- 1 & 0 结果为0

- 0 & 1 结果为0

- 0 & 0 结果为0

9 & 7 = 1

-9 & 7 = 7

(2)按位或:|

运算规则:对应位只要有1即为1,否则为0。

- 1 | 1 结果为1

- 1 | 0 结果为1

- 0 | 1 结果为1

- 0 & 0 结果为0

9 | 7  //结果: 15

-9 | 7 //结果: -9

(3)按位异或:^

运算规则:对应位一个为1一个为0,才为1,否则为0。

- 1 ^ 1 结果为0
- 1 ^ 0 结果为1
- 0 ^ 1 结果为1
- 0 ^ 0 结果为0

9 ^ 7  //结果为14

-9 ^ 7 //结果为-16

(4)按位取反:~

运算规则:对应位为1,则结果为0;对应位为0,则结果为1。

- ~0就是1  
- ~1就是0

~9  //结果:-10

~-9  //结果:8

练习1:

int i = 12,j = 5;

printf("%d\n",i & j); //4
printf("%d\n",i | j); //13
printf("%d\n",i ^ j); //9

练习2:特定位清零

技巧:待清零的位与0,其它位与1

示例:设字符型 x 的当前值为 53,将其最低两位清 0,其余位保持不变

分析:与二进制的0b11111100数值求&运算即可。

int main() {
    char x = 53; // 0b00110101
    x = x & 252; // 0b11111100
    printf("%d", x); //0b00110100
    return 0;
}

举例:判断特定位是否为零
技巧:待判定位与 1,其它位与 0;判与运算结果是否为 0
示例:设字符型 x 的当前值为 53,判定其最高位是否为 0
分析:与二进制的0b10000000求&运算,若结果为 0,则最高位为 0,否则为 1

int main() {
    char x = 53; // 0b00110101
    x = x & 128; // 0b10000000
    if(x == 0)
        printf("最高位为0");
    else
        printf("最高位不为0");
    
    return 0;

}

举例:保留特定位
技巧:待保留位全部与 1,其余位与 0
示例:设字符型 x 的当前值为 53,保留其最低 4 位,其余位清零
分析:与二进制的0b00001111求&运算即可。

int main() {
    char x = 53; // 0b00110101
    x = x & 15;  // 0b00001111

    printf("%d\n",x); //0b00000101

    return 0;

}

举例:待置 1 或 1,保持位或 0
示例:设字符型 x 的当前值为 53,将其最低两位置 1,其余位保持不变
分析:与二进制0b00000011求或运算即可

int main() {
    char x = 53; // 0b00110101
    x = x | 3;   // 0b00000011

    printf("%d\n",x); //0b00110111

    return 0;

}

举例:特定位取反
待取反位异或 1,保持位异或 0
示例:设字符型 x 的当前值为 53,将其最低两位取反,其余位保持不变
分析:与二进制0b00000011求异或运算即可

int main() {
    char x = 53; // 0b00110101
    x = x ^ 3;   // 0b00000011

    printf("%d\n",x); //0b00110110

    return 0;

}

3.1.6 条件运算符

条件运算符格式:(条件表达式)? 表达式1:表达式2
说明:条件表达式是如果为 true (非0值),就执行表达式1,否则执行表达式2。

如果运算后的结果赋给新的变量,要求表达式1和表达式2为同种或兼容的类型

**举例1:**获取两个数中的较大值

int main() {
    //获取两个数的较大值
    int m1 = 10;
    int m2 = 20;

    int max1 = (m1 > m2)? m1 : m2;
    printf("m1和m2中的较大值为%d\n",max1);

    return 0;

}

**举例2:**获取三个数中的最大值

int main() {
    int n1 = 23;
    int n2 = 13;
    int n3 = 33;
    //写法1:
    int tempMax = (n1 > n2)? n1:n2;
    int finalMax = (tempMax > n3)? tempMax : n3;
    printf("三个数中最大值为%d\n",finalMax);

    //写法2:不推荐,可读性差
    int finalMax1 = (((n1 > n2)? n1:n2) > n3)? ((n1 > n2)? n1:n2) : n3;
    printf("三个数中最大值为%d\n",finalMax1);

    return 0;

}

【华南理工大学2018研】输入一个字符,判别它是否为大写字母,如果是,将它变为小写字母;如果不是,不转换。然后输出最后得到的字符。请在下面空白处填上适当语句。

#include<stdio.h>
int main(){ 
char ch;  
scanf("%c", ① );  
ch=(ch>='A'&& ② )?(ch+32): ③ ;  
printf( ④ ,ch);  
return ⑤ ;
} 

【答案】①&ch②ch<='Z'③ch④"%c"⑤0

【解析】程序进行输入时要加上地址符&;main函数中的第三行为一个三目运算符,当ch在A~Z之间时ch为真,此时ch+32变为小写字母,否则不变;程序进行输出时应用%加上数据类型进行输出,最后用return 0来结束程序。

3.1.7 sizeof 运算符

sizeof 运算符:sizeof(参数)

  • 参数可以是数据类型的关键字,也可以是变量名或某个具体的值

  • 返回某种数据类型或某个值占用的字节数量。

举例1:参数为数据类型

int x = sizeof(int);  //通常是 4 或 8

举例2:参数为变量

int i;
sizeof(i); //通常是 4 或 8

// 参数为数值
sizeof(3.14); //浮点数的字面量一律存储为double类型,故返回 8

**sizeof返回值的类型说明**

sizeof 运算符的返回值,C 语言只规定是无符号整数,并没有规定具体的类型,留给系统自己去决定sizeof 到底返回什么类型。不同的系统中,返回值的类型有可能是 unsigned int ,也有可能是unsigned long ,甚至是 unsigned long long ,对应的 printf() 占位符分别是 %u 、 %lu和 %llu 。这样不利于程序的可移植性。

C 语言提供了一个解决方法,创造了一个类型别名 `size_t` ,用来统一表示 sizeof 的返回值类型。该别名定义在 `stddef.h` 头文件里面,对应当前系统的 sizeof 的返回值类型,可能是 unsigned int ,也可能是 unsigned long 。

printf() 有专门的占位符 `%zd` 或 `%zu` ,用来处理 size_t 类型的值。

printf("%zd\n", sizeof(int));

上面代码中,不管 sizeof 返回值的类型是什么, %zd 占位符(或 %zu )都可以正确输出。
如果当前系统不支持 %zd 或 %zu ,可使用 %u (unsigned int)或 %lu (unsigned long int)代替。

3.1.8 运算符的优先级

运算符有不同的优先级,所谓优先级就是在表达式运算中的运算符顺序。
上一行中的运算符总是优先于下一行的。

开发建议:

1. 不要过多的依赖运算的优先级来控制表达式的执行顺序,这样可读性太差,尽量`使用()来控制`表达式的执行顺序。
2. 不要把一个表达式写得过于复杂,如果一个表达式过于复杂,则把它`分成几步`来完成。例如:
   ​ (num1 + num2) * 2 > num3 && num2 > num3 ? num3 : num1 + num2;

3.2 流程控制结构

  • 流程控制结构是用来控制程序中各语句执行顺序的语句,可以把语句组合成能完成一定功能的小逻辑模块。

  • 程序设计中规定的三种流程结构,即:

    • 顺序结构

      • 程序从上到下逐行地执行,中间没有任何判断和跳转。

    • 分支结构

      • 根据条件,选择性地执行某段代码。

      • if…elseswitch-case两种分支语句。

    • 循环结构

      • 根据循环条件,重复性的执行某段代码。

      • forwhiledo-while三种循环语句。

  • 生活中、工业生产中流程控制举例

3.2.1 顺序结构

程序`从上到下逐行`地执行。表达式语句都是顺序执行的。并且上一行对某个变量的修改对下一行会产生影响。

        

int main() {
    int x = 1;
    int y = 2;
    printf("x = %d\n", x);
    printf("y = %d\n", y);
    //对x、y的值进行修改
    x++;
    y = 2 * x + y;
    x = x * 10;
    printf("x = %d\n", x);
    printf("y = %d\n", y);

    return 0;

}

c语言中定义变量时采用合法的`前向引用`。如:

int main() {
    int num1 = 12;
    int num2 = num1 + 2;
    
    return 0;
}

错误形式:

int main() {
    int num2 = num1 + 2; //use of undeclared identifier 'num1'
    int num1 = 12;
    
    return 0;
}

3.2.2 分支结构1:if-else

结构1:单分支

if(条件表达式){
      语句块;

执行流程:条件表达式为真(值不为 0 )时,就执行语句块。

        

成年人心率的正常范围是每分钟60-100次。体检时,如果心率不在此范围内,则提示需要做进一步的检查。

int main(){
    int heartBeats = 89;

    if(heartBeats < 60 || heartBeats > 100){
        printf("你需要做进一步的检查");
    }

    printf("体检结束");
    return 0;
}

结构2:双分支

if(条件表达式) { 
      语句块1;
}else {
      语句块2;
}

执行流程:

  1. 首先判断条件表达式看其结果是为真(值不为 0 )还是假(值为0)

  2. 如果是真,就执行语句块1

  3. 如果是假,就执行语句块2

        

定义一个整数,判定是偶数还是奇数

int main() {
    int a = 10;

    if (a % 2 == 0) {
        printf("%d是偶数\n", a);
    } else {
        printf("%d是奇数\n", a);
    }
    
    return 0;
}

结构3:多重分支

if (条件表达式1) {
      语句块1;
} else if (条件表达式2) {
      语句块2;
}
...
}else if (条件表达式n) {
     语句块n;
} else {
      语句块n+1;
}

执行流程:

1. 首先判断关系表达式1看其结果是真(值不为0)还是假(值为0)
2. 如果是真,就执行语句块1,然后结束当前多分支
3. 如果是假,就继续判断条件表达式2,看其结果是真还是假
4. 如果是真,就执行语句块2,然后结束当前多分支
5. 如果是假,就继续判断条件表达式…看其结果是真还是假
​    …

   n.  如果没有任何关系表达式为真,就执行语句块n+1,然后结束当前多分支。

岳小鹏参加C语言考试,他和父亲岳不群达成承诺:
如果:
成绩为100分时,奖励一辆跑车;
成绩为(80,99]时,奖励一辆山地自行车;
当成绩为[60,80]时,奖励环球影城一日游;
其它时,胖揍一顿。

说明:默认成绩是在[0,100]范围内

int main() {
    int score = 67;//岳小鹏的期末成绩
    //写法一:默认成绩范围为[0,100]
    if (score == 100) {
        printf("奖励一辆跑车");
    } else if (score > 80 && score <= 99) {    //错误的写法:}else if(80 < score <= 99){
        printf("奖励一辆山地自行车");
    } else if (score >= 60 && score <= 80) {
        printf("奖励环球影城玩一日游");
    }
    //else{
    //	printf("胖揍一顿");
    //}


    //写法二:
    // 默认成绩范围为[0,100]
    if (score == 100) {
        printf("奖励一辆跑车");
    } else if (score > 80) {
        printf("奖励一辆山地自行车");
    } else if (score >= 60) {
        printf("奖励环球影城玩一日游");
    } else {
        printf("胖揍一顿");
    }

    return 0;
}

当条件表达式之间是“`互斥`”关系时(即彼此没有交集),条件判断语句及执行语句间顺序无所谓。
>
> 当条件表达式之间是“`包含`”关系时,“`小上大下 / 子上父下`”,否则范围小的条件表达式将不可能被执行。

> 当if-else结构是“多选一”时,最后的`else是可选的`,根据需要可以省略

> 语句块只有一条执行语句时,一对`{}可以省略`,但建议保留

练习:判断输出结果

int main() {
    int number = 20;
    if (number > 6)
        if (number < 12)
            printf("The number >= 6,且 <= 12.\n");
    else
        printf("判断不成功.\n");

    return 0;
}

说明:

如果有多个 if 和 else ,没有{}的情况下, else 总是跟最接近的 if 匹配。

为了提供代码的可读性,建议使用大括号,明确 else 匹配哪一个 if 。

如果希望else与外层的if匹配,改写为如下:

int main() {
    int number = 20;
    if (number > 6) {
        if (number < 12) {
            printf("The number >= 6,且 <= 12.\n");
        }
    } else {
        printf("判断不成功.\n");
    }

    return 0;
}

举例1:

#include <stdio.h>

int main() {

    int age = 0;
    scanf("%d", &age);
    if (age < 18) {
        printf("未成年\n");
    } else {
        printf("成年\n");
    }

    return 0;
}

举例2:

编写一个程序,判定某个年份是否为闰年。年份满足如下条件之一,即是闰年:

⑴ year 是 400 的整倍数: year%400==0
(2) 能被4整除,但不能被100整除:year % 4 == 0 && year % 100 != 0

#include<stdio.h>

int main() {
    int year;
    printf("输入年份: ");
    scanf("%d", &year);
    if (year % 400 == 0 || (year % 4 == 0 && year % 100 != 0))
        printf("%d 是闰年\n", year);
    else
        printf("%d 不是闰年\n", year);
    
    return 0;
}

举例3:解方程

设计求解一元二次方程 $ax^2+bx+c=0$ (a≠0)的程序

#include<stdio.h>
#include <math.h>

int main() {
    float a, b, c;
    float x1, x2, d;
    printf("输入方程中的系数与常量 a,b,c: ");
    scanf("%f%f%f", &a, &b, &c);
    d = b * b - 4 * a * c;

    if (d >= 0) {
        x1 = (-b + sqrt(d)) / (2 * a);
        x2 = (-b - sqrt(d)) / (2 * a);
        printf("x1=%f,x2=%f\n", x1, x2);
    } else
        printf("方程没有根\n");

    return 0;
}

举例4:判断水的温度

如果大于95℃,则打印“开水”;

如果大于70℃且小于等于95℃,则打印“热水”;

如果大于40℃且小于等于70℃,则打印“温水”;

如果小于等于40℃,则打印“凉水”。

int main() {

    int waterTemperature = 85;

    if (waterTemperature > 95) {
        printf("开水");
    } else if (waterTemperature > 70 && waterTemperature <= 95) {
        printf("热水");
    } else if (waterTemperature > 40 && waterTemperature <= 70) {
        printf("温水");
    } else {
        printf("凉水");
    }


    return 0;
}

举例5:**if-else的嵌套**

出票系统:根据淡旺季的月份和年龄,打印票价。要求,月份和年龄从键盘获取输入。

4_10 旺季:
    成人(18-60):60
    儿童(< 18)   :  半价
    老人(> 60)   :  1/3
淡季:
    成人   :  40
    其他   :  20

int main() {
    int month, age;
    double price = 60.0;

    printf("请输入月份(1-12):");
    scanf("%d", &month);

    printf("请输入年龄:");
    scanf("%d", &age);

    if (month >= 4 && month <= 10) {  // 旺季
        if (age < 18) {
            price = price / 2;  // 半价
        } else if (age > 60) {
            price = price / 3;  // 1/3价格
        }
    } else {  // 淡季
        if (age >= 18) {
            price = 40.0;
        } else {
            price = 20.0;
        }
    }

    printf("您的票价是:¥%.2f\n", price);

    return 0;

}

【武汉科技大学2019研】对下述程序段的描述正确的是(  )。

```c
scanf("%d,%d",&a,&b);
if(a>b)  
a=b;b=a;
else  
a++;b++;
printf("a=%d,b=%d",a,b); 
```

A.若输入4,5则输出a=5,b=6
B.若输入5,4则输出a=4,b=5
C.若输入5,4则输出a=5,b=5
D.有语法错误,不能通过编译

【答案】D

【解析】if(表达式)后面如果没有用花括号括起来,那么if的子语句只包括第一条语句,即在程序中只有a=b是属于if语句块的,if和else中间隔了一条语句b=a,编译无法通过。

【华南理工大学2018研】两次运行下面的程序,如果从键盘上分别输入6和4,则输出的结果是(  )

```c
void main(void){
int x;
scanf("%d",&x);
if(x++>5)
    printf("%d\n",x);
else
    printf("%d\n",x--);
return 0;

```

A.7和5
B.6和3
C.7和4
D.6和4

【答案】A

【解析】当输入6时,判断 x++>5为真,进入if语句块,此时x=7,输出7;当输入4时,进入else语句块,此时x=5,然后因为--是先运算后自减,所以先输出5,后x的值为4,答案选A。

3.2.3 分支结构2:switch-case

1 基本语法

switch 语句用于判断条件有多个常量结果的情况。它把多重的 else if 改成更易用、可读性更好的形式。

switch(表达式){
    case 常量值1: 
        语句块1;
        //break;
    case 常量值2: 
        语句块2;
        //break;
    ┇ ┇
    case 常量值n: 
        语句块n; 
        //break;
    [default: 
        语句块n+1;
    ]
}

执行流程图:

**执行过程:**

第1步:根据switch中表达式的值,依次匹配各个case。如果表达式的值等于某个case中的常量值,则执行对应case中的执行语句。

第2步:执行完此case的执行语句以后,
​              情况1:如果遇到break,则执行break并跳出当前的switch-case结构
​                情况2:如果没有遇到break,则会继续执行当前case之后的其它case中的执行语句。--->`case穿透`
​                 ...
​                直到遇到break关键字或执行完所有的case及default的执行语句,跳出当前的switch-case结构

**使用注意点:**

- case子句中的值必须是常量,不能是变量名或不确定的表达式值或范围。
- 同一个switch语句,所有case子句中的常量值互不相同。
- 如果没有break,程序会顺序执行到switch结尾;从使用频率说,一般switch-case结构中,都需要编写break。
- default子句是可选的。同时,位置也是灵活的。当没有匹配的case时,执行default语句。

2 举例

举例1:

int main() {
    int grade = 1;
    switch (grade) {
        case 0:
            printf("zero\n");
            break;
        case 1:
            printf("one\n");
            break;
        case 2:
            printf("two\n");
            break;
        case 3:
            printf("three\n");
            break;
        default:
            printf("other\n");
    }
    return 0;
}

与如下代码对比:

int main() {
    int grade = 1;
    switch (grade) {
        case 0:
            printf("zero\n");
            //break;
        case 1:
            printf("one\n");
            //break;
        case 2:
            printf("two\n");
            //break;
        case 3:
            printf("three\n");
            //break;
        default:
            printf("other\n");
    }
    return 0;
}

举例2:使用switch-case实现:对学生成绩大于60分的,输出“合格”。低于60分的,输出“不合格”。

int main() {
    int score = 83;

    //方式1:
    switch(score / 10){
        case 0:
            printf("不及格");
            break;
        case 1:
            printf("不及格");
            break;
        case 2:
            printf("不及格");
            break;
        case 3:
            printf("不及格");
            break;
        case 4:
            printf("不及格");
            break;
        case 5:
            printf("不及格");
            break;
        case 6:
            printf("及格");
            break;
        case 7:
            printf("及格");
            break;
        case 8:
            printf("及格");
            break;
        case 9:
            printf("及格");
            break;
        case 10:
            printf("及格");
            break;
        default:
            printf("成绩输入有误");
            break;
    }
    return 0;
}

如果多个 case 分支对应同样的语句体,可以改写如下:

int main() {
    int score = 83;

    //方式2:体会case穿透
    switch(score / 10){
        case 0:
        case 1:
        case 2:
        case 3:
        case 4:
        case 5:
            printf("不及格");
            break;
        case 6:
        case 7:
        case 8:
        case 9:
        case 10:
            printf("及格");
            break;
        default:
            printf("成绩输入有误");
            break;
    }

    return 0;
}

从算法层面,还可以优化下代码,改为如下:

int main() {
    int score = 83;

    //方式3:算法层面优化
    switch(score / 60){
        case 0:
            printf("不及格");
            break;
        case 1:
            printf("及格");
            break;
        default:
            printf("成绩输入有误");
            break;
    }

    return 0;
}

错误举例:

int main() {

    int key = 10;
    switch (key) {
        case key > 0 :  //提示:Expression is not an integer constant expression
            printf("正数");
            break;
        case key < 0:
            printf("负数");
            break;
        default:
            printf("零");
            break;
    }

    return 0;
}

【武汉科技大学2019研】若有定义:int a=1,b=2; float x=3,w; 则合法的switch语句是( )。

A.

```c
switch (a) {
case 1:
w = a / b;
break;
case 2:
w = a % b;
break;
}
```


B.

```c
switch (b) {
case 1:
z = a % b;
case 2:
z = a / b;
break;
}
```

C.

```c
switch (x) {
case 2:
w = a % b;
break;
case 1:
w = a / b;
break;
}
```

D.

```c
switch (a + b);{
case 3:
case 2:
w = a % b;
break;
}
```

【答案】A

【解析】B中,变量z未定义;C中x为浮点型,switch后面的表达式不能是浮点型,只能是整型和字符型;D中swith表达式后面不能加分号,答案选A。

> 【北京航空航天大学2018研】对于下列代码:
>
> ```c
> switch(option){
> case 'H':
> printf("Hello ");
> case 'W':
> printf("Welcome ");
> case 'B':
> printf("Bye");
> break;
> }
> ```
>
> 若option的取值为'W',则该代码段的输出结果是(  )。
> A.Welcome
> B.Welcome Bye
> C.Hello Welcome Bye
> D.以上结果都不对
>
> 【答案】B
>
> 【解析】由于option为'W',所以首先应该输出Welcome,但是由于该语句后面没有break语句来终止选择语句switch,因此会继续执行下面的语句,直到遇上break,所以最后输出Welcome Bye,答案为B。

3 if-else与switch-case比较

- 结论:凡是使用switch-case的结构都可以转换为if-else结构。反之,不成立。
- 开发经验:如果既可以使用switch-case,又可以使用if-else,建议使用switch-case。因为效率稍高。

- 细节对比:
  - if-else语句优势
    - if语句的条件可以用于范围的判断,也可以用于等值的判断,`使用范围更广`。
    - switch语句的条件是一个常量值,只能判断某个变量或表达式的结果是否等于某个常量值,`使用场景较狭窄`。
  - switch语句优势
    - 当条件是判断某个变量或表达式是否等于某个固定的常量值时,使用if和switch都可以,习惯上使用switch更多。因为`效率稍高`。当条件是区间范围的判断时,只能使用if语句。
    - 使用switch可以利用`穿透性`,同时执行多个分支,而if...else没有穿透性。

3.2.4 循环结构之1:for循环

  • 循环结构的理解:循环语句具有在某些条件满足的情况下,反复执行特定代码的功能。

  • 循环结构分类:

    • for 循环

    • while 循环

    • do-while 循环

  • 循环结构四要素

    • 初始化部分

    • 循环条件部分

    • 循环体部分

    • 迭代部分

1 基本语法

语法格式:

for (①初始化部分; ②循环条件部分; ④迭代部分){
             ③循环体部分;

执行过程:①-②-③-④-②-③-④-②-③-④-.....-②

图示:

        

说明:

- for(;;)中的两个;不能多也不能少
- ①初始化部分,用于初始化循环变量,只执行一次。可以声明多个变量,但必须是同一个类型,用逗号分隔
- ②循环条件部分,只要为 true ,就会不断执行循环体;当值为false时,退出循环
- ④迭代部分,每轮循环结束后执行,使得循环变量发生变化。可以有多个变量更新,用逗号分隔

2.举例

**案例1:使用for循环重复执行某些语句**

题目:输出5行HelloWorld

int main() {

    //写法1:
//    printf("Hello World!\n");
//    printf("Hello World!\n");
//    printf("Hello World!\n");
//    printf("Hello World!\n");
//    printf("Hello World!\n");

    //写法2:
    for (int i = 1; i <= 5; i++) {
        printf("Hello World!\n");
    }

    return 0;
}

案例2:格式的多样性

题目:写出输出的结果

int main() {

    int num = 1;
    for(printf("a");num < 3;printf("c"),num++){
        printf("b");

    }
    return 0;
}

int main() {

    int i, j;
    for (i = 0, j = 999; i < 10; i++, j--) {
        printf("%d, %d\n", i, j);
    }

    return 0;
}

案例3:累加的思想

题目:遍历1-100以内的偶数,并获取偶数的个数,获取所有的偶数的和

int main() {

    int count = 0;//记录偶数的个数
    int sum = 0;//记录偶数的和

    for (int i = 1; i <= 100; i++) {

        if (i % 2 == 0) {
            printf("%d\n", i);
            count++;
            sum += i;
        }

        //printf("偶数的个数为:" + count);
    }

    printf("偶数的个数为:%d\n", count);
    printf("偶数的总和为:%d\n", sum);
    
    return 0;
}

案例4:结合分支结构使用

题目:输出所有的水仙花数,所谓水仙花数是指一个3位数,其各个位上数字立方和等于其本身。例如: 153 = 1*1*1 + 3*3*3 + 5*5*5

int main() {

    //定义统计变量,初始化值是0
    int count = 0;

    //获取三位数,用for循环实现
    for (int x = 100; x < 1000; x++) {
        //获取三位数的个位,十位,百位
        int ge = x % 10;
        int shi = x / 10 % 10;
        int bai = x / 100;

        //判断这个三位数是否是水仙花数,如果是,统计变量++
        if ((ge * ge * ge + shi * shi * shi + bai * bai * bai) == x) {
            printf("水仙花数:%d\n", x);
            count++;
        }
    }

    //输出统计结果就可以了
    printf("水仙花数共有%d个", count);

    return 0;
}

拓展:打印出四位数字中“个位+百位”等于“十位+千位”并且个位数为偶数,千位数为奇数的数字,并打印符合条件的数字的个数。

案例5:结合break的使用

说明:输入两个正整数m和n,求其最大公约数和最小公倍数。

比如:12和20的最大公约数是4,最小公倍数是60。

int main() {

    //需求1:最大公约数
    int m = 12, n = 20;
    //取出两个数中的较小值
    int min = (m < n) ? m : n;

    for (int i = min; i >= 1; i--) {//for(int i = 1;i <= min;i++){

        if (m % i == 0 && n % i == 0) {
            printf("最大公约数是:%d\n", i); //公约数

            break; //跳出当前循环结构
        }
    }


    //需求2:最小公倍数
    //取出两个数中的较大值
    int max = (m > n) ? m : n;

    for (int i = max; i <= m * n; i++) {

        if (i % m == 0 && i % n == 0) {

            printf("最小公倍数是:%d\n", i);//公倍数

            break;
        }
    }

    return 0;
}

说明:

1、我们可以在循环中使用break。一旦执行break,就跳出当前循环结构。

2、小结:如何结束一个循环结构?

​      结束情况1:循环结构中的循环条件部分返回false

​      结束情况2:循环结构中执行了break。

3、如果一个循环结构不能结束,那就是一个死循环!我们开发中要避免出现死循环。

【华南理工大学2018研】有一个分数序列$2/1,3/2,5/3,8/5,13/8,21/13,...$,求这个数列的前20项之和。请在下面空白处填上适当语句。

```c
int main(){  
int i,n=20;  
double a=2,b=1,s=0,t;  
for(i=1; ① ;i++){    
 s= ② ;        
 ③ ;   
 a=a+b;        
 ④ ;  
}  
printf("sum=%16.10f\n", ⑤ );  
return 0;

```

【答案】①i<=n②s+a/b③t=a④b=t⑤s

【解析】此程序循环是从=1开始,所以要计算数列前20项,则循环条件应为i<=n;s用来累加求和,所以每次进行累加操作,即s=s+a/b;通过分析数列可知,数列中分子是上一项的分子分母之和,而分母是上一项的分子,依照此关系可以通过中间变量t进行换算,最后输出所求结果s。

【华南理工大学2018研】从键盘输入10个整数,编程求其中大于3且小于100的数的平均值并输出结果。

【答案】
此题可以先利用循环从屏幕读取10个数,然后依次判断数值是否大于3且小于100的数,最后取平均值进行输出,程序如下:

#include<stdio.h>
int main() {
int n = 10;
int num; //输入的整数
int sum; //记录总和
int count;//记录个数
for(int i = 1;i <= n;i++){
  scanf("%d",&num);

  if(num > 3 && num < 100){
      sum += num;
      count++;
  }
}
//    printf("%d\n",sum);
//    printf("%d\n",count);
printf("%d\n",sum / count);
return 0;
}

3.2.5 循环结构之2:while循环

1 基本语法

语法格式:

①初始化部分
while(②循环条件部分){
    ③循环体部分;
    ④迭代部分;
}

执行过程:①-②-③-④-②-③-④-②-③-④-...-②

        

说明:

- while(循环条件部分)中循环条件为非零值,表示true、真;为零值,表示false、伪。
- 注意不要忘记声明④迭代部分。否则,循环将不能结束,变成死循环。
- for循环和while循环`可以相互转换`。二者没有性能上的差别。实际开发中,根据具体结构的情况,选择哪个格式更合适、美观。
- for循环与while循环的区别:`初始化条件部分的作用域不同`。

2 举例

**案例1:**输出5行HelloWorld!

int main() {
    int i = 1;
    while (i <= 5) {
        printf("Hello World!\n");
        i++;
    }
    return 0;
}

**案例2:**遍历1-100的偶数,并计算所有偶数的和、偶数的个数(累加的思想)

int main() {
    //遍历1-100的偶数,并计算所有偶数的和、偶数的个数(累加的思想)
    int num = 1;

    int sum = 0;//记录1-100所有的偶数的和
    int count = 0;//记录1-100之间偶数的个数

    while (num <= 100) {

        if (num % 2 == 0) {
            printf("%d\n", num);
            sum += num;
            count++;
        }

        //迭代条件
        num++;
    }

    printf("偶数的总和为:%d\n", sum);
    printf("偶数的个数为:%d\n", count);
    return 0;
}

**案例3:折纸珠穆朗玛峰**

世界最高山峰是珠穆朗玛峰,它的高度是8848.86米,假如我有一张足够大的纸,它的厚度是0.1毫米。
请问,我折叠多少次,可以折成珠穆朗玛峰的高度?

int main() {
    //定义一个计数器,初始值为0
    int count = 0;

    //定义珠穆朗玛峰的高度
    int zf = 8848860;//单位:毫米

    double paper = 0.1;//单位:毫米

    while (paper < zf) {
        //在循环中执行累加,对应折叠了多少次
        count++;
        paper *= 2;//循环的执行过程中每次纸张折叠,纸张的厚度要加倍
    }

    //打印计数器的值
    printf("需要折叠:%d次\n", count);
    printf("折纸的高度为%f米,超过了珠峰的高度", paper / 1000);

    return 0;
}

>  【武汉科技大学2019研】如果有定义:int x=0,s=0; 则下面程序段的执行结果是(  )。
>
>  ```c
>  while(!x!= 0)  
>      s+=x++;
>  printf("%d",s);
>  ```
>
>  A.1
>  B.0
>  C.无限循环
>  D.控制表达式非法,无法编译
>
>  【答案】B
>
>  【解析】while后面的表达式中,首先执行!运算符,然后再执行!=运算符,第一次判断中,x=0则!x!=0满足条件,进入循环中,执行s += x++,x++是先运算,再自加,执行完后s=0,x=1,再回到while的判断条件,判断为false,跳出循环,输出s的值为0,答案选B。

3.2.6 循环结构之3:do-while循环

do-while 结构是 while 的变体,它会先执行一次循环体,然后再判断是否满足条件。如果满足的话,就继续执行循环体,否则跳出循环。

1 基本语法

**语法格式:**

①初始化部分;
do{
    ③循环体部分
    ④迭代部分
}while(②循环条件部分); 
 

**执行过程:**①-③-④-②-③-④-②-③-④-...-②

**说明:**

- do{}while();最后有一个分号
- do-while结构的循环体语句是至少会执行一次,这个和for和while是不一样的
- 循环的三个结构for、while、do-while三者是可以相互转换的。

2 举例

**案例1:**遍历1-100的偶数,并计算所有偶数的和、偶数的个数(累加的思想)

int main() {
    //遍历1-100的偶数,并计算所有偶数的和、偶数的个数(累加的思想)
    //初始化部分
    int num = 1;

    int sum = 0;//记录1-100所有的偶数的和
    int count = 0;//记录1-100之间偶数的个数

    do {
        //循环体部分
        if (num % 2 == 0) {
            printf("%d\n", num);
            sum += num;
            count++;
        }

        num++;//迭代部分


    } while (num <= 100); //循环条件部分


    printf("偶数的总和为:%d\n", sum);
    printf("偶数的个数为:%d\n", count);

    return 0;
}

**案例2:**体会do-while至少会执行一次循环体

int main() {
    //while循环:
    int num1 = 10;
    while (num1 > 10) {
        printf("hello:while\n");
        num1--;
    }

    //do-while循环:
    int num2 = 10;
    do {
        printf("hello:do-while\n");
        num2--;
    } while (num2 > 10);

    return 0;
}

**案例3:ATM取款**

声明变量balance并初始化为0,用以表示银行账户的余额,下面通过ATM机程序实现存款,取款等功能。

=========ATM========
   1、存款
   2、取款
   3、显示余额
   4、退出
请选择(1-4):

int main() {
    //初始化条件
    double balance = 0.0;//表示银行账户的余额
    int selection; //记录客户的选择

    double addMoney, minusMoney; //分别记录存钱、取钱的额度

    int isFlag = 1;//用于控制循环的结束

    do {
        printf("=========ATM========\n");
        printf("\t1、存款\n");
        printf("\t2、取款\n");
        printf("\t3、显示余额\n");
        printf("\t4、退出\n");
        printf("请选择(1-4):");

        scanf("%d", &selection);

        switch (selection) {
            case 1:
                printf("要存款的额度为:");
                scanf("%lf", &addMoney);
                if (addMoney > 0) {
                    balance += addMoney;
                }
                break;
            case 2:
                printf("要取款的额度为:");
                scanf("%lf", &minusMoney);
                if (minusMoney > 0 && balance >= minusMoney) {
                    balance -= minusMoney;
                } else {
                    printf("您输入的数据非法或余额不足\n");
                }
                break;
            case 3:
                printf("当前的余额为:%lf\n", balance);
                break;
            case 4:
                printf("欢迎下次进入此系统。^_^\n");
                isFlag = 0;
                break;
            default:
                printf("请重新选择!\n");
                break;
        }

    } while (isFlag);

    return 0;
}

3 小结:三种循环结构

  • 三种循环结构都具有四个要素:

    • 循环变量的初始化条件

    • 循环条件

    • 循环体语句块

    • 循环变量的修改的迭代表达式

  • 从循环次数角度分析

    • do-while循环至少执行一次循环体语句。

    • for和while循环先判断循环条件语句是否成立,然后决定是否执行循环体。

  • 如何选择

    • 遍历有明显的循环次数(范围)的需求,选择for循环

    • 遍历没有明显的循环次数(范围)的需求,选择while循环

    • 如果循环体语句块至少执行一次,可以考虑使用do-while循环

    • 本质上:三种循环之间完全可以互相转换,都能实现循环的功能

3.2.7 "无限"循环

1 基本语法

语法格式:

  • 最简单"无限"循环格式:while(1) , for(;;)

适用场景:

  • 开发中,有时并不确定需要循环多少次,需要根据循环体内部某些条件,来控制循环的结束(使用break)。

  • 如果此循环结构不能终止,则构成了死循环!开发中要避免出现死循环。

2 举例

案例1:实现爱你到永远..

int main() {
    for (;;) {
        printf("我爱你!\n");
    }
    //printf("end\n");//永远无法到达的语句

    return 0;
}

int main() {
    for (int i = 1; i <= 10; ){ //循环变量没有修改,条件永远成立,死循环
        printf("我爱你!\n");
    }

    return 0;
}

思考:如下代码执行效果

int main() {
    for (int i = 1; i >= 10;) { //一次都不执行
        printf("我爱你!");
    }
    return 0;
}

.**案例2:**从键盘读入个数不确定的整数,并判断读入的正数和负数的个数,输入为0时结束程序。

int main() {

    int positiveNumber = 0;//统计正数的个数
    int negativeNumber = 0;//统计负数的个数
    int num;  //记录输入的整数

    while(1){   //for (;;){
        printf("请输入一个整数(输入为0时结束程序):");
        scanf("%d", &num);
        if (num > 0) {
            positiveNumber++;
        } else if (num < 0) {
            negativeNumber++;
        } else {
            printf("程序结束\n");
            break;
        }
    }
    printf("正数的个数为:%d\n", positiveNumber);
    printf("负数的个数为:%d\n", negativeNumber);


    return 0;
}

3.2.8 嵌套循环(或多重循环)

1 使用说明

- **所谓嵌套循环**,是指一个循环结构A的循环体是另一个循环结构B。比如,for循环里面还有一个for循环,就是嵌套循环。其中,for ,while ,do-while均可以作为外层循环或内层循环。
  - 外层循环:循环结构A
  - 内层循环:循环结构B

  • 实质上,嵌套循环就是把内层循环当成外层循环的循环体。只有当内层循环的循环条件为false(值为0)时,才会完全跳出内层循环,才可结束外层的当次循环,开始下一次的外层循环。

  • 设外层循环次数为m次,内层为n次,则内层循环体实际上需要执行m*n次。

  • 技巧:从二维图形的角度看,外层循环控制行数,内层循环控制列数

  • 开发经验:实际开发中,我们最多见到的嵌套循环是两层。一般不会出现超过三层的嵌套循环。如果将要出现,一定要停下来重新梳理业务逻辑,重新思考算法的实现,控制在三层以内。否则,可读性会很差。

例如:两个for嵌套循环格式

for(初始化语句①; 循环条件语句②; 迭代语句⑦) {
    for(初始化语句③; 循环条件语句④; 迭代语句⑥) {
          循环体语句⑤;
    }
}

//执行过程:① - ② - ③ - ④ - ⑤ - ⑥ - ④ - ⑤ - ⑥ - ... - ④ - ⑦ - ② - ③ - ④ - ⑤ - ⑥ - ④..

**执行特点:**外层循环执行一次,内层循环执行一轮。

2 举例

**案例1:**打印5行6个*

int main() {

    /*

    ******
    ******
    ******
    ******
    ******

    */

    for (int j = 1; j <= 5; j++) {

        for (int i = 1; i <= 6; i++) {
            printf("*");
        }

        printf("\n");
    }

    return 0;
}

**案例2:**打印5行直角三角形

*
**
***
****
*****

int main() {

    for (int i = 1; i <= 5; i++) {
        for (int j = 1; j <= i; j++) {
            printf("*");
        }
        printf("\n");
    }

    return 0;
}    

**案例3:**打印5行倒直角三角形

        i    j(*的上限)   i + j = 6  --> j = 6 - i
*****   1    5
****    2    4
***        3    3
**        4    2
*        5    1

int main() {

    for (int i = 1; i <= 5; i++) {
        for (int j = 1; j <= 6 - i; j++) {
            printf("*");

        }
        printf("\n");
    }

    return 0;
}

**案例4:九九乘法表**

int main() {

    for (int i = 1; i <= 9; i++) {
        for (int j = 1; j <= i; j++) {
            printf("%d*%d=%d\t", i, j, i * j);
        }
        printf("\n");
    }
    return 0;
}

【北京航空航天大学2018研】对于下列for循环语句,请将其改写为功能完全相同的while循环语句。

int i,j,count=0;
for(i=0;i<100;i++){  
for(j=100;j>=i;j-=2)  {    
count+=j-i;  
}

【答案】
此题可以在定义时先令i=0,j=100,在第一层while循环时只需判断i<100即可,在第二层while循环时只需判断j>=i即可,j-=2放在内层循环体中即可,具体程序如下:

int i=0, j=100, count=0;
while(i<100){  
while(j>=i)  {    
count+=j-i;    
j-=2;  
}  
i++;

3.2.9 break和continue关键字

1 使用说明

关键字适用范围循环结构中的作用相同点
breakswitch-case--
break循环结构一旦执行,就结束(或跳出)当前循环结构此关键字的后面,不能声明语句
continue循环结构一旦执行,就结束(或跳出)当次循环结构此关键字的后面,不能声明语句

代码验证:

int main() {

    for (int i = 1; i <= 10; i++) {
        if (i % 4 == 0) {
            break;//123
            //continue;//123567910
            //如下的语句不可能被执行!
            //printf("今晚迪丽热巴要约我吃饭");
        }

        printf("%d", i);
    }

    printf("\n####\n");

    //嵌套循环中的使用
    for (int i = 1; i <= 4; i++) {

        for (int j = 1; j <= 10; j++) {
            if (j % 4 == 0) {
                break; //结束的是包裹break关键字的最近的一层循环!
                //continue;//结束的是包裹break关键字的最近的一层循环的当次!
            }
            printf("%d", j);
        }
        printf("\n");
    }
    return 0;
}

2 举例

举例1:在全系1000名学生中举行慈善募捐,当总数达到10万元时就结束,统计此时捐款的人数以及平均每人捐款的数目。

#include <stdio.h>

#define SUM 100000 //指定符号常量SUM代表10万

//在全系1000名学生中举行慈善募捐,当总数达到10万元时就结束,统计此时捐款的人数以及平均每人捐款的数目。
int main() {
    double amount, total = 0;//分别代表着每人捐款的数额,总捐款额
    int count = 0; //捐款人数
    for (int i = 1; i <= 1000; i++) {
        printf("请输入你的捐款额:");
        scanf("%lf", &amount);
        total = total + amount;
        count++;
        if (total >= SUM)
            break;
    }
    double aver = total / count; //人均捐款额度
    printf("捐款总人数是:%d\n人均捐款额为:%10.2f\n", count, aver);
    return 0;
}

举例2:要求输出100~200之间的不能被3整除的数。

int main() {
    int n;
    for (n = 100; n <= 200; n++) {
        if (n % 3 == 0)
            continue;
        printf("%d ", n);
    }
    printf("\n");
    return 0;
}

【华南理工大学2018研】编程求100~200间的全部素数。

【答案】
素数意思是只能被1和本身整除,因此将1到本身之间的数做除数,进行求余,如果余数为0,则不是素数,否则是素数。根据经验,假设所要判断的数为n,则一般只需要判断1到根号n之间的数即可,具体程序如下:

#include <stdio.h>
#include<math.h>

int main() {
int count = 0; //记录是否有约数
for (int i = 100; i <= 200; i++) {
  for (int j = 2; j <= sqrt(i); j++) {
      if (i % j == 0){
          count++;
          break;
      }
  }
  if (count == 0)
      printf("%d\n", i);

  count = 0;
}
}

【武汉科技大学2019研】以下正确的描述是(  )。 A.从多层循环嵌套中退出时,只能使用break语句 B.在循环体内使用continue和break语句,作用相同 C.只能在循环体内和switch体内使用break语句 D.continue语句的作用是结束整个循环的执行

【答案】C

【解析】从多层嵌套中退出不是只能使用break语句,也可以使用return或者程序自己执行完,A错误;在循环体内continue代表不执行该次循环中的剩余未执行语句,break代表直接跳出本层循环,BD错误,答案选C。

【北京航空航天大学2018研】以下关于循环语句的叙述中,正确的是(  )。
A.for循环语句的三个部分必须都要有表达式
B.while循环语句的循环体内至少要有一条语句
C.do…while循环语句的循环体至少会被执行一次
D.continue语句可以退出包含它的整个循环体

【答案】C

【解析】for循环的三个表达式都可以省略,但是之间的分号不能省略,同时要有退出循环的机制,因此A项错误;while循环语句的循环体内可以为空,并不违反相应语法,只不过循环什么也不执行,因此B项错误;continue语句只是不执行本次循环的剩余语句,而并非退出整个循环,因此D项错误,答案选C。

3.2.10 goto关键字

使用goto,可以实现无条件的语句的转移。

一般格式:goto 标号;
 其中,标号,属于标识符,以“:”为标记,位于某语句前面。

执行 goto 语句后,程序将跳转到指定标号处执行。这样可以随意将控制转移到程序中的任意一条语句上,然后执行它。

int main() {
    
    loop_label:printf("Hello, world!\n");
    goto loop_label;

    return 0;
}

loop_label是一个标签名,可以放在正常语句的前面。程序执行到 goto 语句,就会跳转到它指定的标签名位置继续执行。因此,上面的代码会产生无限循环。

实际使用中,goto语句通常与条件语句配合。可用来实现条件转移,跳出循环体等功能。

**举例2:**录入学生成绩,并计算学生的平均分。当输入-1时程序结束。

int main() {
    int score, i = 0, sum = 0;
    next:printf("请输入第 %d 个学生成绩(输入-1结束):", i+1);
    scanf("%d", &score);
    if (score != -1) {
        sum += score;
        i++;
        goto next;
    }
    if (i != 0)
        printf("%d个学生的平均分是 %d\n",i, sum / i);

    return 0;
}

注意:goto 只能在同一个函数之中跳转,并不能跳转到其他函数。

**举例3:**goto 的一个主要用法是跳出多层循环

for(...) {
  for (...) {
    while (...) {
      do {
        if (some_error_condition)
          goto bail;    
     } while(...);
   }
 }
}
    
bail:
// ... ...

上面代码有很复杂的嵌套循环,不使用 goto 的话,想要完全跳出所有循环,写起来很麻烦。

**举例4:**goto 的另一个用途是提早结束多重判断

if (do_something() == ERR)
  goto error;
if (do_something2() == ERR)
  goto error;
if (do_something3() == ERR)
  goto error;
if (do_something4() == ERR)
  goto error;

上面示例有四个判断,只要有一个发现错误,就使用 goto 跳过后面的判断。

小结:

从理论上 goto语句是没有必要的,实践中没有goto语句也可以很容易的写出代码。使用goto反而容易造成程序流程的混乱,致使程序容易出错。故建议不要轻易使用。

这里只是为了语法的完整,介绍一下它的用法。

4. 数组

4.1 数组的概述

4.1.1 为什么需要数组

**需求分析1:**

需要统计某公司50个员工的工资情况,例如计算平均工资、找到最高工资等。用之前知识,首先需要声明`50个变量`来分别记录每位员工的工资,这样会很麻烦。因此我们可以将所有的数据全部存储到一个容器中统一管理,并使用容器进行计算。

需求分析2:

容器的概念:

- **生活中的容器:**水杯(装水等液体),衣柜(装衣服等物品),集装箱(装货物等)。
- **程序中的容器:**将多个数据存储到一起,每个数据称为该容器的元素。

4.1.2 数组的概念

  • 数组(Array),是多个相同类型数据按一定顺序排列的集合,并使用一个名字命名,并通过编号的方式对这些数据进行统一管理。

  • 数组中的概念

    • 数组名

    • 下标(或索引、index)

    • 元素

    • 数组的长度

数组的特点:

  • 数组中的元素在内存中是依次紧密排列的,有序的。

  • 创建数组对象会在内存中开辟一整块连续的空间。占据的空间的大小,取决于数组的长度和数组中元素的类型。

  • 我们可以直接通过下标(或索引)的方式调用指定位置的元素,速度很快。

  • 数组,一旦初始化完成,其长度就是确定的。数组的长度一旦确定,就不能修改

  • 数组名中引用的是这块连续空间的首地址。

4.1.3 数组的分类

按照数组维度分:

  • 一维数组:存储一组数据

  • 二维数组:存储多组数据,相当于二维表,一行代表一组数据。每一行长度可以不同。

  • 三维数组、四维数组、....

按照元素的数据类型分:

  • int类型数组

  • char类型数组

  • double类型数组

4.2 一维数组的定义

4.2.1 数组的定义方式1

数组通过变量名后加方括号表示,方括号里面是数组可以容纳的成员数量(即长度)。

int arr[10];  //数组 arr ,里面包含10个成员,每个成员都是 int 类型

#define NUM 10
int arr1[NUM];

注意,声明数组时,必须给出数组的大小。

4.2.2 数组元素的调用

- 格式:`数组名[下标]`

- 数组的`下标从0开始`,用“int arr[10];”定义数组,则`最大下标值为9`,不存在数组元素arr[10]。

arr[0] = 13;       //对该位置数组元素进行赋值
int score = arr[0]; //调用此位置的元素值

**数组角标越界:**

假设数组有n个元素,如果使用的数组的下标小于0,或者大于n-1,就是数组越界访问了,超出了数组合法空间的访问。

C语言不做数组下标越界的检查,编译器也不一定报错,但是编译器不报错,并不意味着程序就是正确!

int scores[20];
scores[20] = 51;

说明:数组 scores 只有20个成员,因此 scores[20] 这个位置是不存在的。但是,引用这个位置并不会报错。赋值操作会导致紧跟在 scores 后面的那块内存区域被赋值(这实际是其它变量的区域),因此不知不觉就更改了其它变量的值。这很容易引发错误,而且难以发现。

4.2.3 关于长度

**数组的字节长度**

sizeof 运算符会返回整个数组的字节长度

int arr[10];
printf("数组的字节长度为:%zd\n",sizeof(arr)); //40

**数组的长度**

在定义数组时,需要指定数组中元素的个数,方括号中的常量表达式用来表示元素的个数,即数组长度。

由于数组成员都是同一个类型,每个成员的字节长度都是一样的,所以数组整体的字节长度除以某个数组元素的字节长度,就可以得到数组的成员数量。

//数组中元素的个数:
int arrLen = sizeof(arr) / sizeof(arr[0]);

int a[10];
printf("数组的字节长度为:%zu\n", sizeof(a));   // 40
printf("数组每个元素的字节长度为:%zu\n", sizeof(int)); // 4
printf("数组的长度为:%zu\n", sizeof(a) / sizeof(int)); // 10

复习: sizeof 返回值的数据类型是 `size_t` ,所以 sizeof(a) / sizeof(a[0]) 的数据类型也是size_t 。在 printf() 里面的占位符,要用 %zd 或 %zu 。

注意:数组一旦声明/定义了,其**长度就固定了,不能动态变化**。

4.2.4 数组的遍历

将数组中的每个元素分别获取出来,就是`遍历`。for循环与数组的遍历是绝配。

举例1:声明长度为10的int类型数组,给数组元素依次赋值为0,1,2,3,4,5,6,7,8,9,并遍历数组所有元素.

int main() {

    int arr[10];
    
    //给数组中的每个元素赋值
    for (int i = 0; i < sizeof(arr)/sizeof(int); i++) { //对数组元素arr[0]~arr[9]赋值
        arr[i] = i;
    }
    //遍历数组中的元素
    printf("遍历数组中的元素:\n");
    for (int i = 0; i < sizeof(arr)/sizeof(int); i++) { //输出arr[0]~arr[9]共10个数组元素
        printf("%d ", arr[i]);
    }
    printf("\n");
    return 0;
}

4.2.5 数组的其它定义方式

**定义方式2:**(定义方式1在2.1节讲的)

数组可以在声明时,使用大括号,同时对每一个成员赋值。

int arr[5] = {22, 37, 90, 48, 95};

变形形式1:C 语言允许省略方括号里面的数组成员数量,这时根据大括号里面的值的数量,自动确定数组的长度。

int arr[3] = {10,20,30};
// 等同于
int arr[] = {10,20,30};  //数组 arr 的长度,将根据大括号里面的值的数量,确定为 3

变形形式2:对数组部分元素赋初值:如果大括号里面的值,少于数组的成员数量,那么未赋值的成员自动初始化为 0 。

int arr[5] = {10, 20, 30};
// 等同于
int arr[5] = {10,20,30, 0, 0};

变形方式3:将整个数组的每一个成员都设置为零,最简单的方式如下

int a[100] = {0};

错误方式:使用大括号赋值时,大括号里面的值不能多于数组的长度,否则编译时会报错。

int arr[3] = {1,2,3,4};  // 报错

定义方式3:数组初始化时,可以指定为哪些位置的成员赋值。

int arr[15] = {[2] = 10, [5] = 20, [14] = 30};  //非角标2、5、14的位置自动赋值为0

//等同于
int arr[15] = {[5] = 20, [14] = 30, [2] = 10}; //指定位置的赋值可以不按角标从小到大的顺序

变形形式1:指定位置的赋值与顺序赋值,可以结合使用。

int arr[15] = {1, [5] = 10, 11, [10] = 20, 21}; //角标0、5、6、10、11的位置被赋值

变形形式2:省略成员数量时,如果同时采用指定位置的赋值,那么数组长度将是最大的指定位置再加1。

int arr[] = {[2] = 6, [9] = 12};  //此时数组的长度是10

4.3 一维数组内存分析

4.3.1 数组内存图

针对于如下代码:

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

对应的内存结构:

      

说明:

1)数组名,记录该数组的首地址 ,即 a[0]的地址。

2)数组的各个元素是连续分布的, 假如 a[0] 地址是0x1122,则a[1]地址= a[0]的地址+int字节数(4) = 0x1122 + 4 = 0x1126,后面 a[2] 地址 = a[1]地址 + int 字节数(4) = 0x1126 + 4 = 0x112A,依次类推...

4.3.2 注意事项

C 语言规定,数组变量一旦声明,数组名指向的地址就不可更改。因为声明数组时,编译器会自动为数组分配内存地址,这个地址与数组名是绑定的,不可更改。

因此,当数组定义后,再用大括号重新赋值,是不允许的。下面的代码会报错。

错误举例1:

int nums[5];
nums = {22, 37, 3490, 18, 95}; // 使用大括号赋值时,必须在数组声明时赋值,否则编译时会报错。

错误举例2:

int nums[5] = {1, 2, 3, 4, 5};
nums = {6, 7, 8, 9, 10}; // 报错

错误举例3:

int ints[100];
ints = NULL; //报错

这也导致不能将一个数组名赋值给另外一个数组名。

int a[5] = {1, 2, 3, 4, 5};
// 写法一
int b[5] = a; // 报错
// 写法二
int b[5];
b = a; // 报错

上面两种写法都会更改数组 b 的地址,导致报错。

4.3.3 变长数组

数组声明的时候,数组长度除了使用常量,也可以使用变量或表达式来指定数组的大小。这叫做`变长数组`(variable-length array,简称 VLA)。

方式1:

int n = 10;
int arr[n];

变长数组的根本特征是数组长度只有`运行时才能确定`。它的好处是程序员不必在开发时,随意为数组指定一个估计的长度,程序可以在运行时为数组分配精确的长度。

任何长度需要运行时才能确定的数组,都是变长数组。比如,

int i = 10;
int a1[i];
int a2[i + 5];
int a3[i + k];

注意:变长数组在C99标准中被引入,在C11标准中被标记为可选特性。某些编译器可能不支持变长数组,或者可能有特定的限制和行为。

方式2:

如果你的编译器版本不支持变长数组,还可以考虑使用动态内存分配(使用`malloc()函数` )来创建动态大小的数组。

分配:

int length = 5;
int *arr = (int *)malloc(length * sizeof(int));

释放:free(arr);

4.4 一维数组的应用

4.4.1 数值型数组特征值统计

这里的特征值涉及到:平均值、最大值、最小值、总和等

举例1:定义一个int型的一维数组,包含10个元素,然后求出数组中的最大值,最小值,总和,平均值,并输出出来。

int main() {
    int arr[10] = {34, 54, 2, 32, 54, 57, 3, 32, 87, 43};

    int max = arr[0];//用于记录数组的最大值
    int arrLen = sizeof(arr) / sizeof(int);//获取数组中元素的个数
    for (int i = 1; i < arrLen; i++) {
        if (max < arr[i]) {
            max = arr[i];
        }
    }
    printf("最大值为:%d\n", max);

    //获取数组的最小值
    int min = arr[0];
    for (int i = 1; i < arrLen; i++) {
        if (min > arr[i]) {
            min = arr[i];
        }
    }
    printf("最小值为:%d\n", min);

    //获取数组的总和
    int sum = 0;
    for (int i = 0; i < arrLen; i++) {
        sum += arr[i];
    }
    printf("总和为:%d\n", sum);

    //获取数组的平均值
    int avg = sum / arrLen;
    printf("平均值为:%d\n", avg);


    return 0;
}

举例2:评委打分

分析以下需求,并用代码实现:

(1)在编程竞赛中,有10位评委为参赛的选手打分,分数分别为:5,4,6,8,9,0,1,2,7,3

(2)求选手的最后得分(去掉一个最高分和一个最低分后其余8位评委打分的平均值)

int main() {
    int scores[10] = {5,4,6,8,9,0,1,2,7,3};

    int max = scores[0]; //记录最高分
    int min = scores[0]; //记录最低分
    int sum = 0; //记录总分
    int arrLen = sizeof(scores) / sizeof(int); //记录数组长度
    for(int i = 0;i < arrLen;i++){
        if(max < scores[i]){
            max = scores[i];
        }

        if(min > scores[i]){
            min = scores[i];
        }

        sum += scores[i];
    }
    //计算平均分
    double avg = (double)(sum - max - min) / (arrLen - 2);

    printf("选手去掉最高分和最低分之后的平均分为:%.2lf\n" , avg);

    return 0;
}

4.4.2 数组的复制

由于数组名是指针,所以复制数组不能简单地复制数组名。

int a[3] = {10,20,30};
int* b;
b = a;

上面的写法,结果不是将数组 a 复制给数组 b ,而是让 a 和 b 指向同一个数组。

正确方式1:使用循环

这是复制数组最简单的方法,将数组元素逐个进行复制。比如,将数组 a 的成员逐个复制给数组 b。

#include <stdio.h>

#define LENGTH 3

int main() {
    int a[LENGTH] = {10, 20, 30};
    int b[LENGTH];

    // 复制数组 a 到数组 b
    for (int i = 0; i < LENGTH; i++) {
        b[i] = a[i];
    }

    // 打印数组 b 的内容
    printf("复制后的数组 b:");
    for (int i = 0; i < LENGTH; i++) {
        printf("%d ", b[i]);
    }
    printf("\n");

    return 0;
}

正确方式2:使用 memcpy() 函数

 memcpy() 函数定义在头文件 string.h 中,直接把数组所在的那一段内存,再复制一份。3个参数依次为:`目标数组`、`源数组`以及`要复制的字节数`。

#include <stdio.h>
#include <string.h>

#define LENGTH 3

int main() {
    int a[LENGTH] = {10, 20, 30};
    int b[LENGTH];

    // 使用 memcpy 函数复制数组 a 到数组 b
    memcpy(b, a, LENGTH * sizeof(int));

    // 打印数组 b 的内容
    printf("复制后的数组 b:");
    for (int i = 0; i < LENGTH; i++) {
        printf("%d ", b[i]);
    }
    printf("\n");

    return 0;
}

两种方式对比:

下面是对两种方式进行比较的一些要点:

1. 循环复制:
   - 优点:`简单直观`,容易理解和实现。不需要引入额外的头文件。
   - 缺点:需要编写循环代码来遍历数组并逐个赋值,相对而言可能`稍显繁琐`。不适用于复制大型数组或复杂数据结构。
2. memcpy函数复制:
   - 优点:使用标准库提供的函数,可以实现`快速且高效`的内存复制。适用于`大型数组或复杂数据`结构的复制。可以直接复制字节数,不需要遍历数组。
   - 缺点:需要包含 `<string.h>` 头文件。对于简单的数组复制,可能有些`过于繁重`。

4.4.3 数组元素的反转

实现思想:数组对称位置的元素互换。

方式1:

代码实现:

int main() {
    int arr[] = {1,2,3,4,5,6,7,8,9};
    int size = sizeof(arr) / sizeof(arr[0]); //数组的长度

    printf("原始数组:");
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    for(int i = 0;i < size / 2;i++){
        int temp = arr[i];
        arr[i] = arr[size - 1 - i];
        arr[size - 1 - i] = temp;
    }

    printf("反转后的数组:");
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    return 0;
}

方式2:

int main() {
    int arr[] = {1, 2, 3, 4, 5,6,7,8,9};
    int size = sizeof(arr) / sizeof(arr[0]); //数组的长度

    printf("原始数组:");
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    int left = 0; // 起始指针
    int right = size - 1; // 结尾指针

    while (left < right) {
        // 交换起始指针和结尾指针指向的元素
        int temp = arr[left];
        arr[left] = arr[right];
        arr[right] = temp;

        // 更新指针位置
        left++;
        right--;
    }

    printf("反转后的数组:");
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    return 0;
}

4.4.4 char型数组与字符串

1 char型数组

字符型数组,顾名思义,数组元素的数据类型为字符型的数组。

一方面,可以看做普通的数组,初始化、常用操作如前所述。比如:

char arr[] = {'a','b','c','d'};

另一方面,字符型数组可以用于存储字符串。

2 字符串的使用

"helloworld"
"abc"
"a"
"123"

这种由双引号引起来的一串字符称为字符串字面值(String Literal),简称字符串(String)。

通常把`""`称为`空串`,即一个不包含任意字符的字符串;而`" "`则称为`空格串`,是包含一个空格字符的字符串。二者不能等同。

C语言没有专门用于存储字符串的变量类型,字符串都被存储在char类型的数组中。在字符串结尾,C 语言会自动添加一个`'\0' `的转义字符作为字符串结束的标志,所以字符数组也必须以 '\0'字符结束。

**声明方式1:标准写法**

//显式以'\0'为最后一个字符元素结束
char str[] = {'h','e','l','l','o',' ','w','o','r','l','d','\0'};

如果一个字符数组声明如下,由于必须留一个位置给 \0 ,所以最多只能容纳9个字符的字符串。

char str1[10];

声明方式2:简化写法

字符串写成数组的形式,是非常麻烦的,C 语言提供了一种简化写法。双引号之中的字符,会被自动视为字符数组。

//自动在末尾添加'\0'字符
char str1[12] = {"hello world"};  //注意使用双引号,非单引号
//或者
char str2[12] = "hello world";   //可以省略一对{}来初始化数组元素

由于字符数组的长度可以让编译器自动计算,所以声明时可以省略字符数组的长度:

char str1[] = {"hello world"};
//或者
char str2[] = "hello world"; 

双引号里面的字符串,不用自己添加结尾字符 \0 ,C 语言会自动添加。所以,代码中数组 str1或str2的元素依次为 'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd', '\0'。

**字符串对应数组的长度**

对应的存储为:

其中,数组由连续的存储单元组成,字符串中的字符被存储在相邻的存储单元中,每个单元存储一个字符。所以,上述两个数组的长度不是11,而是12。

**字符串的长度**

char nation[10]={"China"};

数组nation的前5个元素为: ′C′,′h′,′i′,′n′,′a′,第6个元素为′\0′,后4个元素也自动设定为空字符。

#include <stdio.h>
#include <string.h>    //需要加载此头文件

int main() {
    char nation[10] = "China";
    printf("%d\n", strlen(nation));     //5
}

注意:在计算字符串长度的时候,'\0' 是结束标志,不算作字符串内容。

**区分:'\0'、0、'0'**

字符 '\0' 不同于字符 '0' ,前者的ASCII 码是0(二进制形式 00000000 ),后者的 ASCII 码是48(二进制形式 00110000 )。

即 '\0' 就表示ASCII的0,所以 '\0'==0 结果为true(1)

**练习1:字符数组、字符串的长度**

char s1[50] = "hello";  //声明1

char s2[] = "hello";    //声明2

char s3[5] = "hello";   //声明3

对于声明1:赋给的元素的个数小于该数组的长度,则会自动在后面加 '\0', 表示字符串结束。所以,字符数组 s1 的长度是 50 ,但是字符串“hello”的实际长度只有5(不包含结尾符号 '\0' ),所以后面空出来的45个位置,都会被初始化为 '\0'。

对于声明2:字符数组 s2 的长度是 6(包含结尾符号 '\0' ),但是字符串“hello”的实际长度只有5。

对于声明3:赋给的元素的个数等于该数组的长度,则不会自动添加 '\0'。但字符串要求以'\0'结束,所以这种写法是错误的,要避免。

**练习2:比较"x"和'x'的不同**

- 书写形式不同:字符串常量用双引号,字符常量用单引号。

- 存储空间不同:在内存中,字符常量只占用一个字节的存储空间,而字符串存储时自动加一个结束标记'\0',所以'x'占用1个字节,而"x"占用2个字节。

                

- 二者的操作也不相同。例如,可对字符常量进行加减运算,字符串常量则不能。

练习3:输出字符数组

#include <stdio.h>

int main() {

    char str1[]={"China\nBeijing"};
    char str2[] = "helloworld";

    puts(str1);

    puts(str2);

    return 0;
}

【中央财经大学2018研】若有定义和语句:char s[10]; s="abcd"; printf("%s\n",s);,则结果是( )。 A.输出abcd@#$ B.输出a C.输出abcd D.编译不通过

【答案】D

【解析】在定义一维字符数组时,s为数组名,指向数组首元素的地址,为地址常量,不可更改,因此语句s="abcd"是非法的,编译不会通过。

4.5 多维数组

4.5.1 理解

二维数组、三维数组、...都称为多维数组。本节主要讲解二维数组,三维及以上的数组,以此类推即可。

举例:公司有3个攻坚小分队,每队有6名同事,要把这些同事的工资用数组保存起来以备查看。

此时建立数组salary用于存储工资,它应当是二维的。第一维用来表示第几分队,第二维用来表示第几个同事。例如用`salary2,3`表示角标2对应分队的角标3对应队员的工资。

对于二维数组的理解,可以看作是由一维数组嵌套而成的。即一维数组array1又作为另一个一维数组array2的元素而存在。

4.5.2 二维数组的定义方式1

**定义方式1:**

int a[3][4]; //二维数组

二维数组a可看成由三个一维数组构成,它们的数组名分别为 a[0]、a[1]、a[2]。这三个一维数组各有 4 个元素,如,一维数组 a[0] 的元素为 `a[0][0]`、`a[0][1]`、`a[0][2]`、`a[0][3]`。二维数组a共有12个成员(3 x 4 = 12)。

也可以简化理解为:

二维数组,常称为`矩阵(matrix)`。把二维数组写成`行(row)`和`列(column)`的排列形式,可以形象化地理解二维数组的逻辑结构。

三维数组如下:

int arr1[3][4][5]; //三维数组

技巧:C 语言允许声明多维数组,有多少个维度,就用多少个方括号,比如二维数组就使用两个方括号。

**错误方式:**

float a[3,4];  //在一对方括号内不能写两个下标

4.5.3 二维数组的内存分析

用`矩阵形式`(如3行4列形式)表示二维数组,是`逻辑`上的概念,能形象地表示出行列关系。而在`内存`中,各元素是连续存放的,不是二维的,是`线性`的。

C语言中,二维数组中元素排列的顺序是`按行存放`的。即:先顺序存放第一行的元素,再存放第二行的元素。(最右边的下标变化最快,第一维的下标变化最慢)。

举例:关于长度

int b[3][3];
printf("%d\n",sizeof(b)); //36
printf("%d\n",sizeof(b)/sizeof(int)); //9

4.5.4 成员的调用

格式:**数组名[下标] [下标]**

跟一维数组一样,多维数组每个维度的第一个成员也是从 0 开始编号。

举例1:给指定索引位置的元素赋值

int arr1[3][5];
//给指定索引位置的元素赋值
arr1[0][0] = 12;
arr1[3][4] = 5;

举例2:查看数组元素的地址

int main() {

    int arr2[3][4];

    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 4; j++) {
            printf("&arr2[%d][%d] = %p\n", i, j, &arr2[i][j]);
        }
    }

    return 0;
}

输出结果如下:

4.5.5 二维数组其它定义方式

定义方式2:声明与初始化同时进行

多维数组也可以使用大括号,在声明的同时,一次性对所有成员赋值。

int a[3][4] = {{1,2,3,4},
               {5,6,7,8},
               {9,10,11,12}};

上例中, a 是一个二维数组,这种赋值写法相当于将第一维的每个成员写成一个数组。

说明:这里的地址以十进制数值进行的说明。

int main() {

    int a[3][4] = {{1,2,3,4},
                   {5,6,7,8},
                   {9,10,11,12}};

    printf("%p\n",a[0]);     //0000006ac71ffd30
    printf("%p\n",a[0] + 1); //0000006ac71ffd34
    printf("%p\n",a[0] + 2); //0000006ac71ffd38
    printf("%p\n",a[0] + 3); //0000006ac71ffd3c
    printf("%p\n",a[0] + 4); //0000006ac71ffd40
    printf("%p\n",a + 1);    //0000006ac71ffd40

    printf("%p\n",a[1]);     //0000006ac71ffd40
    printf("%p\n",a[1] + 1); //0000006ac71ffd44

    return 0;
}

**定义方式3:部分元素赋值**

多维数组也可以仅为指定的位置进行初始化赋值,未赋值的成员会自动设置为“零”值 。

//指定了 [0][0] 和 [1][1] 位置的值,其他位置就自动设为 0 。
int a[2][2] = {[0][0] = 1, [1][1] = 2};  

定义方式4:使用单层大括号赋值

多维数组也可以使用单层大括号赋值。不管数组有多少维度,在内存里面都是线性存储。对于`a[2][2]`来说, `a[0][0]` 的后面是 `a[0][1]` ,再后面是`a[1][0]` ,以此类推。

int a[2][2] = {1, 0, 0, 2};  //会自动匹配到各行各列

定义方式5:方式4的简化

在方式4的基础上,如果对全部元素赋值,那么第一维的长度可以不给出。

//int a[2][3] = {1, 2, 3, 4, 5, 6}; 
//可以写为:
int a[][3] = {1, 2, 3, 4, 5, 6}; 
//也可以写为:
int a[][3] = {{1, 2, 3},{4, 5, 6}}; //行数自然判定为2

练习:下面哪些赋值操作是正确的?(都对)

int arr1[3][2]={{1,2},{3,4},{5,6}};  //对应定义方式2

int arr2[3][2]={1,2,3,4,5,6};  //对应定义方式4

int arr3[][2]={1,2,3,4,5,6};  //对应定义方式5

int arr4[][2]={{1,2},{3,4},{5,6}}; //对应定义方式5

int arr5[][2]={1,2,3,4,5};  //对应定义方式5。未显式赋值的位置默认赋值为0

**错误方式:**在定义二维数组时,必须指定列数(即一行中包含几个元素)

int array[][];  //错误,必须指定列数
int array[3][]; //错误,必须指定列数

【武汉科技大学2019研】以下能对数组value进行正确初始化的语句是(  )。
A.int value\[2][]={{1,1},{2,2}};
B.int value[][3]={{1,,3},{4,5,6}};
C.int value\[2][3]={1,2,3,4,5,6};
D.int value[][3]={{1},{4,6,}};

【答案】C

【解析】二维数组的定义必须指定列数,可以不用指定行数,A错误;数组value为int型数组,不能给数组里面的元素赋空值,BD错误,答案选C。

4.5.6 举例

举例1:获取arr数组中所有元素的和

提示:使用for的嵌套循环即可。

#include <stdio.h>


#define ROWS 3
#define COLS 4

int main() {

    int arr[ROWS][COLS] = {{3, 5, 8},
                           {12, 9},
                           {7, 0, 6, 4}};

    int sum = 0;//记录总和

    for (int i = 0; i < ROWS; i++) {
        for (int j = 0; j < COLS; j++) {
            sum += arr[i][j];
        }
    }

    printf("总和为%d\n", sum);
    return 0;
}

举例2:求二维数组最大值以及对应的行列角标

#include <stdio.h>

#define ROWS 3
#define COLS 4

int main() {
    int a[ROWS][COLS] = {{1,   2,  3,  4},
                         {9,   8,  7,  6},
                         {-10, 10, -5, 2}};

    int maxValue = a[0][0];
    int maxRow = 0;
    int maxCol = 0;

    for (int i = 0; i < ROWS; i++) {
        for (int j = 0; j < COLS; j++) {
            if (maxValue < a[i][j]) {
                maxValue = a[i][j];
                maxRow = i;
                maxCol = j;
            }
        }
    }

    printf("最大值: %d\n", maxValue);
    printf("对应的行索引: %d\n", maxRow);
    printf("对应的列索引: %d\n", maxCol);

    return 0;
}

举例3:将一个二维数组行和列的元素互换,存到另一个二维数组中。

`a[i][j] ---> b[j][i]`

#include <stdio.h>

#define ROWS 2
#define COLS 3

int main() {
    int a[ROWS][COLS] = {{1, 2, 3},
                         {4, 5, 6}};
    int b[COLS][ROWS];
    printf("数组 a:\n");
    for (int i = 0; i < ROWS; i++) { //处理a数组中的一行中各元素
        for (int j = 0; j < COLS; j++) { //处理a数组中某一列中各元素
            printf("%5d", a[i][j]); //输出a数组的一个元素
        }
        printf("\n");
    }

    for (int i = 0; i < ROWS; i++) { //处理a数组中的一行中各元素
        for (int j = 0; j < COLS; j++) { //处理a数组中某一列中各元素
            b[j][i] = a[i][j]; //将a数组元素的值赋给b数组相应元素
        }
    }

    printf("数组 b:\n"); //输出b数组各元素
    for (int i = 0; i < COLS; i++) { //处理b数组中一行中各元素
        for (int j = 0; j < ROWS; j++) //处理b数组中一列中各元素
            printf("%5d", b[i][j]); //输出b数组的一个元素
        printf("\n");
    }
    return 0;
}

运行结果:

举例4:二维char型数组

将"Apple"、"Orange"、"Grape"、"Pear"、"Peach"存储在数组中。

char fruit[][7]={"Apple","Orange","Grape","Pear","Peach"};

对应图示:

举例5:使用二维数组打印一个 10 行杨辉三角。

提示:

1. 第一行有 1 个元素, 第 n 行有 n 个元素

2. 每一行的第一个元素和最后一个元素都是 1

3. 从第三行开始, 对于非第一个元素和最后一个元素的元素。即:

   ```
   yanghui[i][j] = yanghui[i-1][j-1] + yanghui[i-1][j];
   ```

#include <stdio.h>

#define ROWS 10

int main() {
    int yangHui[ROWS][ROWS];


    for (int i = 0; i < ROWS; i++) {
        //初始化第一列和对角线上的元素为1
        yangHui[i][0] = 1;
        yangHui[i][i] = 1;
        //给其他位置元素赋值
        for (int j = 1; j < i; j++) {
            yangHui[i][j] = yangHui[i - 1][j - 1] + yangHui[i - 1][j];
        }
    }

    // 打印杨辉三角
    for (int i = 0; i < ROWS; i++) {
        // 打印每行的元素
        for (int j = 0; j <= i; j++) {
            printf("%5d ", yangHui[i][j]);
        }
        printf("\n");
    }

    return 0;
}

【华南理工大学2018研】以下数组定义中不正确的是(  )。
A.int a\[2][3];
B.int b\[][3]={0};
C.int c\[100][100]={0};
D.int d\[3][]={{1}, {1, 2, 3},{1}};

【答案】D

【解析】定义二维数组时一定要指定数组的列数,可以不指定数组的行数,D错误。

5. 指针

5.1 指针的理解与定义

5.2 指针的运算

5.3 野指针

5.4 二级指针(多重指针)

5.5 专题:指针与数组

6. 函数

6.1 函数的基本使用

6.2 进一步认识函数

6.3 参数传递机制

6.4 函数的高级应用

6.5 再谈变量

7. 结构体与共用体

7.1 结构体(struct)类型基本使用

7.2 进一步认识结构体

7.3 结构体数组

7.4 结构体指针

7.5 结构体在数据结构中的应用

7.6 共用体(union)

7.7 typedef的使用

8. C语言常用函数

8.1 字符串相关函数

8.2 日期和时间相关函数

8.3 数学运算相关的函数

8.4 内存管理相关的函数

9. 文件操作

9.1 文件和流的介绍

9.2 C输入和输出

9.3 C文件的读写

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值