C语言入门

小颜同学 Lv4

此篇为C语言基础入门,适合C语言初学者

目录

一、C语言简介

​ 1、程序语言基础

​ 1.1、程序设计语言概述

​ 1.1.1、什么是计算机程序?

​ 1.1.2、什么是计算机语言?

​ 1.1.3、程序设计语言的基本概念

​ 1.2、语言处理程序

​ 1.3、程序设计语言的基本成分

​ 1.3.1、程序设计语言的数据成分

​ 1.3.2、程序设计语言的运算成分

​ 1.3.3、程序设计语言的控制成分

​ 1.3.4、程序设计语言的传输成分

​ 2、什么是C语言

​ 3、C语言的发展历程

​ 4、C语言的特点

二、基本数据类型

​ 1、数据的表现形式

​ 1.1、常量

​ 1.2、变量

​ 1.2.1、变量的基本概念

​ 1.2.2、标识符的命名规则

​ 1.2.3、变量的定义

​ 2、基本数据类型

​ 2.1、数据类型的基本概念

​ 2.2、基本数据类型简介

​ 2.3、确定常量的类型

​ 3、格式化输入输出函数

​ 3.1、有关数据输入和输出的概念

​ 3.2、用printf函数输出数据(格式化输出函数)

​ 3.3、用scanf函数输入数据(格式化输入函数)

三、运算符和表达式

​ 1、C语言运算符

​ 1.1、C运算符的分类

​ 1.2、运算符的操作数以及目数

​ 1.3、运算符的优先级

​ 2、算术运算符

​ 2.1、基本的算术运算符

​ 2.2、自增、自减运算符

​ 2.3、算术表达式与运算符的优先级和结合性

​ 2.4、不同类型数据间的混合运算

​ 3、强制类型转换运算符

​ 4、关系运算符

​ 5、逻辑运算符

​ 6、条件运算符

​ 7、位运算符

​ 8、表达式和C语句

​ 8.1、表达式和语句的基本概念

​ 8.2、逗号表达式

​ 8.3、最基本的语句——赋值语句

​ 8.4、C语句的分类

四、选择结构和循环结构

​ 1、选择结构

​ 1.1、if语句

​ 1.2、switch语句

​ 2、循环结构

​ 2.1、for循环

​ 2.2、while循环

​ 2.3、do while循环

​ 2.4、循环结构的分类

​ 3、跳转语句

​ 3.1、break语句

​ 3.2、continue语句

​ 3.3、goto跳转语句

五、数组

​ 1、什么是数组?

​ 2、一维数组的定义和引用

​ 2.1、定义一维数组

​ 2.2、使用数组及引用数组元素

​ 2.3、一维数组的初始化

​ 2.4、一维数组的输入输出

​ 3、二维数组

​ 3.1、二维数组的定义

​ 3.2、二维数组的初始化

​ 3.3、二维数组元素的访问

​ 3.4、二维数组的输入输出

​ 4、字符数组

​ 4.1、字符数组的定义及初始化

​ 4.2、引用字符数组中的元素

​ 4.3、字符数组的输入和输出

​ 4.4、二维字符数组

​ 4.5、字符串处理函数

六、函数

​ 1、函数的基本概念

​ 2、函数的定义

​ 2.1、定义无参无返回值函数

​ 2.2、定义有参无返回值函数

​ 2.3、定义有参数有返回值函数

​ 2.4、定义无参数有返回值函数

​ 3、函数的调用

​ 3.1、函数调用语句

​ 3.2、函数参数

​ 3.3、实参和形参之间的数据传递

​ 3.4、函数的返回值

​ 4、函数的声明

​ 5、局部变量和全局变量

​ 5.1、局部变量

​ 5.2、全局变量

​ 5.3、静态变量与动态变量

​ 6、函数的嵌套调用

​ 7、函数的递归调用

​ 8、数组作为函数参数传递

七、预处理

​ 1、预定义符号

​ 2、宏定义

​ 2.1、无参宏定义

​ 2.2、带参宏定义

​ 2.3、常量的定义

​ 3、文件包含

​ 3.1、包含头文件

​ 3.2、头文件的重复包含

​ 4、条件编译

​ 4.1、#if……#else的使用

​ 4.2、#ifdef……#endif的使用

​ 4.3、#ifndef……#endif的使用

八、构造数据类型

​ 1、结构体

​ 1.1、什么是结构体?

​ 1.2、为什么要用结构体?

​ 1.3、结构体类型的声明和结构体变量的定义

​ 1.4、结构体变量的初始化和引用

​ 1.5、使用typedef关键字自定义类型名

​ 1.6、结构体的嵌套定义

​ 1.7、结构体数组

​ 2、共用体

​ 2.1、什么是共用体?

​ 2.2、共用体类型的声明和共用体变量的定义

​ 2.3、共用体类型所占内存

​ 3、枚举类型

​ 3.1、枚举类型的概念

​ 3.2、枚举类型的声明

​ 3.3、枚举变量的定义

​ 3.4、枚举类型应用举例

九、C语言文件操作

​ 1、什么是文件?

​ 1.1、文件的概念

​ 1.2、文件的分类

​ 1.3、文件存储方法的区别

​ 2、指向文件的指针

​ 2.1、文件指针的定义

​ 2.2、打开与关闭文件

​ 3、顺序读写文件

​ 3.1、字符输入和输出函数

​ 3.2、字符串输入和输出函数

​ 3.3、文件格式化输入和输出函数

​ 3.4、以二进制的形式读写数据

​ 4、随机读写文件

​ 4.1、强制使文件指针指向文件开头

​ 4.2、使文件指针指向文件中的任意位置

​ 5、文件的出错检测

​ 5.1、文件读写出错检测

​ 5.2、文件末尾判断

​ 5.3、文件错误标志

十、C语言的灵魂——指针

​ 1、什么是指针?

​ 2、指针常量与指针变量

​ 2.1、指针常量

​ 2.2、指针变量

​ 3、指针变量作为函数参数

​ 3.1、函数参数为指针类型的函数

​ 3.2、指针函数

​ 4、通过指针引用数组

​ 4.1、数组元素的地址

​ 4.2、指针指向数组元素

​ 4.3、指针指向的移动(指针的偏移)

​ 4.4、指针指向字符串

​ 5、指向函数的指针(函数指针)

​ 5.1、什么是函数指针?

​ 5.2、函数指针的定义

​ 5.3、函数指针的初始化及使用

​ 5.4、使用函数指针作为函数参数(回调函数)

​ 5.5、使用typedef给函数指针取别名

​ 5.6、指针函数和函数指针的区别

​ 6、指针数组和数组指针

​ 6.1、指针数组

​ 6.2、数组指针

​ 7、指针常量和常量指针

​ 8、动态内存分配

​ 8.1、什么是动态内存分配

​ 8.2、怎样建立内存的动态分配

​ 9、结构体指针

​ 9.1、指向结构体变量的指针

​ 9.2、结构体指针的定义

​ 9.3、通过结构体指针引用结构体成员

​ 10、多重指针(多级指针)

​ 10.1、什么是多重指针

​ 10.2、多重指针的定义

​ 10.3、多重指针的使用

​ 10.4、双重指针作为函数形参

​ 11、内存四区

十一、排序算法

​ 1、排序的基本概念

​ 1.1、什么是排序?

​ 1.2、排序的稳定性

​ 1.3、排序的分类

​ 1.4、排序的过程

​ 1.5、排序算法

​ 2、冒泡排序

​ 3、简单选择排序

​ 4、直接插入排序

十二、顺序表

​ 1、顺序表的基本概念

​ 2、顺序表的定义

​ 3、顺序表的功能实现

十三、链表

​ 1、链表的基本概念

​ 1.1、什么是链表

​ 1.2、链表的特点

​ 1.3、链表和数组的区别

​ 2、链表的结构

​ 2.1、链表的结构体示意图

​ 2.2、单链表节点的插入和删除结构示意图

​ 3、单链表

​ 3.1、单链表结构的声明

​ 3.2、单链表的创建与功能实现

十四、栈和队列

​ 1、栈和队列的基本概念

​ 2、数据结构中的栈和队列

​ 2.1、栈(stack)

​ 2.2、队列(queue)

​ 3、栈和队列的基本结构

​ 3.1、栈和队列的结构示意图

​ 3.2、栈和队列中数据的插入和删除

​ 4、栈和队列的实现

​ 4.1、栈功能的实现

​ 4.2、队列功能的实现

附录:C语言常用基础知识

​ 1、C语言中的关键字

​ 2、ASCII码表

​ 3、运算符的优先级

​ 4、C语言常用头文件和库函数

​ 4.1、数学函数

​ 4.2、字符函数

​ 4.3、字符串函数

​ 4.4、输入输出函数

​ 4.5、动态分配函数和随机函数

一、C语言简介

1、程序语言基础

1.1、程序设计语言概述

1.1.1、什么是计算机程序?

所谓程序,就是一组计算机能识别和执行的指令。每一条指令能使计算机执行特定的操作。

1.1.2、什么是计算机语言?

人与人之间交流需要通过语言,我们中国人之间交流用普通话,英国人用英语,俄国人用俄语等。

那人和计算机之间交流当然也要使用一种语言了,所以我们创造了一种计算机和人都能识别的语言,这就是计算机语言。

1.1.3、程序设计语言的基本概念

计算机语言是为了编写计算机程序而设计的符号语言,是用于对计算过程进行描述、组织和推导,方便人机交互的一种语言。我们需要简单了解一下从低级语言到高级语言。

(1)机器语言

由于计算机工作基于二进制,计算机硬件只能识别由0、1字符串组成的机器指令序列,也就是机器指令程序,我们把它称为机器语言,所以机器语言是最基本的计算机语言。用机器语言编制程序,编写的效率低、可读性差,也难以理解、修改和维护。

(2)汇编语言

由于使用机器语言对于人类来说太不友好了,所以人们设计了汇编语言,用一些容易记忆的符号代替0、1序列,来表示机器指令中的操作码和操作数。例如:用ADD表示加法、SUB表示减法等。相对于机器语言,使用汇编语言编写程序的效率和程序可读性有所提高,但汇编语言是面向机器的语言,其书写格式在很大程度上取决于特定计算机指令。
由于它比较“贴近”于计算机,或者说“偏向”于计算机,对计算机比较友好,所以机器语言和汇编语言被称为低级语言。

(3)高级语言

随着计算机的发展,人们开发了功能更强、可读性更高、更加“偏向”于人们逻辑思维的语言,为了更好的支持程序设计,因此产生了面向各类应用的程序设计语言,即高级语言。高级语言更接近于人们习惯使用的自然语言和数学语言,程序中用到的语句和指令是用英文单词来表示的,用到的运算符和表达式和我们日常用的数学算式差不多,比较容易理解。

我们常用的高级语言有C、C++、Java、PHP、Python等,这类语言与人们使用的自然语言比较接近,能够大大的提高程序设计的效率。

1.2、语言处理程序

编译程序和解释程序

尽管人们可以借助高级语言与计算机进行交互,但是计算机仍然只能理解和执行由0、1序列构成的机器语言,所以高级程序设计语言需要翻译成计算机能够识别的机器语言,担负这一任务的程序称为“语言处理程序”。由于应用程序设计语言不同,语言之间的翻译也是多种多样的,它们大致可以分为汇编程序、解释程序和编译程序。

用某种高级语言或汇编语言编写的程序称为源程序,源程序不能直接在计算机上执行。如果源程序是用汇编语言编写的,就需要汇编程序来把它翻译成目标程序才能执行;如果源程序是用某种高级语言编写的,就需要对应的解释程序或者编译程序进行翻译才能执行。

(1)编译程序:计算机是不能直接识别高级语言程序的,需要用一种称为编译程序的软件把用高级语言写的程序(源程序)转换成为机器指令的程序(目标程序),计算机才能够执行,最后才能得到结果。高级语言的一个语句往往对应多条机器指令。

C语言是编译型语言,从C语言源程序到可执行的目标程序需要经过预处理、编译和连接三个步骤。

(2)解释程序:也称为解释器,它可以直接解释执行源程序,或者将源程序翻译某种中间表示形式后再加以执行,不生成独立的目标程序。

1.3、程序设计语言的基本成分

程序设计语言的基本成分包括数据、运算、控制和传输等。

1.3.1、程序设计语言的数据成分

程序设计语言的数据成分是指程序设计语言所支持的数据类型。数据是程序操作的对象,具有类型、名称、作用域、存储类别和生存期等属性,在程序运行过程中要为他分配内存空间。

数据名称可以由用户通过标识符命名;数据类型说明数据占用内存的大小和存放形式;作用域则说明这个数据的使用范围;存储类别说明数据在内存中的位置;生存期说明数据占用内存的时间范围。

从不同的角度来看,可以将数据进行分类。

(1)根据程序运行时数据的值能否被改变可分为常量和变量。在程序中数据可以具有左值和右值,左值是指数据的存储空间(内存地址),右值是指数据的内容(值)。在程序运行中,数据的左值是不可被改变的,也就是常量,右值是可以被改变的,也就是变量。也有一些符号和数值类型等,只有右值,在程序运行过程中其右值不可被改变,所以也将他们称为字符常量和数值常量。

(2)按照数据类型不同可分为基本类型、特殊类型、用户自定义类型、构造类型及其它类型等。

(3)按照作用域的不同可分为全局量和局部量。全局量的作用域为整个程序,所以它在整个程序中都是可用的,在程序运行中其存储空间一般不可改变;而局部量的作用域为一条语句或者一个函数中,它在其他语句或者函数中是不可用的。

1.3.2、程序设计语言的运算成分

运算成分指明允许使用的运算符号及运算规则。大多数高级程序设计语言的基本运算可分为算术运算、关系运算和逻辑运算等类型,有些语言还提供了位运算(如C、C++),运算符和数据类型密切相关,为了得到明确的运算结果,运算符号要规定优先级和结合性,必要时需要使用圆括号来改变其运算顺序。

1.3.3、程序设计语言的控制成分

控制成分是指程序设计语言允许使用的控制结构,程序员可以使用控制成分来构造程序中的控制逻辑。控制结构可分为顺序、选择和循环这三种。

(1)顺序结构:用来表示一个计算操作序列的计算过程是按照所描述的第一个操作开始执行,按顺序依次执行后续操作,直到执行完最后一个操作。

(2)选择结构:也称为分支结构,它提供了在两个或多个分支中选择其中一个的逻辑。首先选择结构指定一个条件,然后根据条件是否成立来决定程序的走向,能从两个或多个分支中选择一个满足条件的来执行。

(3)循环结构:描述了重复计算的过程。通常由初始化、需要重复计算的部分和重复计算的条件组成。

1.3.4、程序设计语言的传输成分

传输成分是指程序设计语言的输入和输出等数据传输。

比如:格式化输出函数、格式化输入函数

2、什么是C语言

C语言是一门面向过程的计算机编程语言,与C++、Java等面向对象编程语言有所不同。C语言的设计目标是提供一种能以简易的方式编译、处理低级存储器、仅产生少量的机器码以及不需要任何运行环境支持便能运行的编程语言。C语言描述问题比汇编语言迅速、工作量小、可读性好、易于调试、修改和移植,而代码质量与汇编语言相当。C语言一般只比汇编语言代码生成目标程序的效率低个10%~20%左右。所以,C语言可以编写系统软件。

在编程领域中,C语言的运用非常之多,它兼顾了汇编语言和高级语言的优点,相对于其它编程语言而言,其具有较大的优势。C语言的普遍性较强,能够适用于许多计算机操作系统中,并且执行效率高。计算机系统的设计以及应用程序的编写是C语言应用的两大领域。

C语言经过了漫长的发展历史,其拥有一套完整的理论体系,在编程语言中具有举足轻重的地位。

3、C语言发展历程

怎样利用C语言作为工具进行程序设计?为什么要选择C语言呢?

首先有必要对C语言的发展和特点有一定的了解。
C语言是在国际上广泛流行的高级计算机语言,其是BCPL语言发展而来的。

1967年英国剑桥大学的Martin Richards推出了没有类型的BCPL( Basic Combined Programming Language)语言。

1970 年美国AT&.T贝尔实验室的Ken Thompson以BCPL语言为基础,设计出了很简单且很接近硬件的B语言(取BCPL的第一个字母)。但B语言过于简单,功能有限。

1972- 1973年间,美国贝尔实验室的D.M.Ritchie在B语言的基础上设计出了C语言。C语言既保持了BCPL和B语言的优点(精练,接近硬件),又克服了它们的缺点(过于简单、无数据类型等),C语言的新特点主要表现在具有多种数据类型(如字符、数值、数组、结构体和指针等)。开发C语言的目的在于尽可能降低用它所写的软件对硬件平台的依赖程度,使之具有可移植性。
最初的C语言只是为描述和实现UNIX操作系统提供一种工作语言 而设计的。

1973年,Ken Thompson和D. M. Ritchie合作把UNIX的90%以上用C语言改写,即UNIX第5版。随着UNIX的广泛使用,C语言也迅速得到推广。

1978年,在Brian W.Kernighan和D. M. Ritchie合著的《The C programming Language》一书中介绍了C语言,这可以说是C语言的第一个标准,其后来成为了广泛使用的C语言基础版本。

1983年,美国国家标准协会(ANSI)成立了一个委员会,根据C语言之前的各个版本对C语言的发展和扩充制定了第一个C语言标准草案,在之前C语言的版本之上有了很大的发展。

在之后的十几年间,国际标准化组织ISO对C语言做了一些修订,直到1999年,ISO又对C语言标准进行修订,针对于应用的需要,在保留以前版本的C语言特征的基础上,新增了一些功能,此次修正被称为C语言的C99版本。之后的几年又先后进行了两次技术的修正。

4、C语言的特点

C语言原本是专用于编写系统软件而设计的,许多大型软件基本都是用的C语言进行编写,其具有以下特点:

①C语言语法简洁、结构紧凑,使用较为灵活、方便,程序编写格式较为自由。
②数据类型丰富,包括整型、浮点型、字符型、数组类型指针类型和共用体类型等,C99又扩充了复数浮点型、超长整型和布尔类型等。特别是指针类型,丰富多样,使用非常灵活,可以用来实现多种复杂的数据结构。
③运算符类型极其丰富,包含的范围很广泛,表达式类型多样化,灵活使用运算符能够实现许多复杂的运算。
④C语言是结构化和模块化的编程语言。具有结构化控制语句,以函数为基本单位,易于实现模块化编程。
⑤语法限制不太严格,程序设计自由度大。如:对数组的下标越界不会进行检查,由程序员自己保证程序的正确。
⑥C语言能允许直接访问物理地址,能进行位(bit)操作,能实现汇编语言的大部分功能,可以直接对硬件进行操作。
⑦用C语言编写的程序可移植性好,C编译系统比较简洁,几乎在所有计算机系统中都可以使用C语言。
⑧生成目标代码质量高,程序执行效率高,是最接近于汇编语言执行效率的高级语言。

由于C语言具备以上特点,使得C语言得到了广泛的应用,除了编写系统软件以外,许多应用软件也是用的C语言进行编写。

二、基本数据类型

1、数据的表现形式

在计算机高级语言中,数据有两种表现形式:常量和变量。

1.1、常量

在程序运行过程中,其值不能被改变的量称为常量。例如:数字1、2、3、0.1、3.14和字母’a’、’b’等。数值常量就是数学中的常数。

常用的常量有以下几类:

(1)整型常量

如:

1
1 , 2 , 3 , 10000 , 0 , -100 , 056 , 0xAB;	//整型常量

(2)实型常量

有两种表示形式:

①十进制小数形式

由数字和小数点组成。

如:

1
123.456 , 0.789 , -12.34 , 0.0 , 10.0

②指数形式

如:

1
2
12.34e3;	//表示12.34*10^3,也就是12.34乘以10的3次方
-67.89e-6; //表示-67.89*10^-6,也就是-67.89乘以10的-6次方

由于计算机输入或输出时无法表示上标和下标,所以规定以字母e或E代表以10为底的指数,需要注意的是e和E之前必须要有数字,且e和E后面必须为整数,不能写成e4、12e2.5等这种形式。

(3)字符常量:有两种形式的字符常量

①普通字符常量:用单引号括起来的一个字符。

如:

1
’a’ , ’B’ , ’3’ , ’@’ , ’#’;

不能写成’ab’或’12’,一个单引号内只会包含一个有效字符。注意单引号只是个界限符,字符是指用单引号括起来的符号,不包括单引号。字符型在内存中是以ASCII码形式存储的,例如字符’a’的ASCII码的十进制为97,在存储单元中存放的是97的二进制补码形式。

②转义字符:C语言中还有一种特殊形式的字符常量,是以字符\开头的字符序列。例如:’\n’换行、’\t’水平制表符(tab)、’\’’单引号、’\”’双引号、’?’问号、’\’斜杠、’\a’警告声音提示、’\b’退格删除符、’\f’换页符、’\r’回车、’\v’垂直制表符、’\o’八进制形式、’\x’十六进制形式等。转义字符的意思是将’\’后面的字符转换成另外的意义。如’\n’中的n不代表字母n,而是作为换行符。

(4)字符串常量:如”ABC”、”123”等,用双引号把若干个字符括起来,字符串不包括双引号。注意不能写成’abc’、’123’,单引号内只能包含一个字符,表示字符常量;双引号内可以包含一串字符,表示字符串常量。

(5)符号常量:用#define指令指定用一个符号代表一个常量。

如:

1
#define PI 3.1415926	//注意末尾不需要分号

也就是用PI代替3.1415926,意思很简单,代表圆周率。

使用符号常量可以让常量在使用时含义更清楚,并且在需要改变程序中多处用到了同一个常量的时候,能够做到“一改全改”。

注意不要把符号常量误认为变量。

(6)地址常量:每一个常量、变量、数组和函数的地址在程序运行期间是不能够改变的,称为地址常量。

1.2、变量

1.2.1、变量的基本概念

变量就是在程序运行中,值可以改变的量。

变量代表一个有名字的、具有特定属性的一个存储单元,可以用来存储数据,也就是变量的值。

变量必须先定义后使用。变量名的命名规则应符合标识符命名规则。

1.2.2、标识符的命名规则

什么是标识符?

标识符用来给我们所定义的变量、符号常量、函数、数组和类型名等命名,或者说标识和区分它们,标识符命名规则:只能由字母、数字、下划线’_’组成,并且只能由字母和下划线开头。同一个作用域范围内定义的标识符不允许重名,不允许是关键字。

如下列的合法标识符可以作为变量名:

1
type,CLass,sum,student_name,day,point1_2_3,_10,n1,temp

1.2.3、变量的定义

定义变量的基本格式:数据类型名 变量名;
如:定义一个整型变量a

1
int a;	//int为数据类型名,a为变量名

2、基本数据类型

2.1、数据类型的基本概念

所谓的类型,就是对数据分配存储单元的安排,包括存储单元的长度(占多少个字节)以及数据的存储形式。不同类型分配不同的长度和存储形式,我们将int、float、char等称为类型名称,或者数据类型关键字。

为什么在用计算机运算时,要指定数据类型呢?

数学运算与计算机运算的区别。

在数学中,数值是不分类型的,数值的运算是绝对精准的,例如1/3的值是0.333333333……(循环小数)。数学是一门研究抽象的学科,数和数的运算都是抽象的。而在计算机中,数据是存放在存储单元中的,它是具体存在的。并且存储单元是由有限的字节构成的,每一个存储单元中存放数据的范围是有限的,不可能存放“无穷大”或者“无限”的数,也不能存放循环小数了。例如计算和输出1/3,以%f格式输出得到的结果是0.333333,只有6位小数,而不是无限循环小数。

1字节=8位(【0000 0000】)

C99还新增了布尔型(bool,0/1)和双长整型(long long int)两种基本整型数据类型。

2.2、基本数据类型简介

各种基本数据类型所占用的存储空间和取值范围如下所示:

(1)整型

整型用于存储整数。

数据类型 关键字 大小(字节) 取值范围
整型 int 4 -2^31 ~ 2^31-1
无符号整型 unsigned int 4 0 ~ 2^32-1
短整型 short 2 -2^15 ~ 2^15-1
无符号短整型 unsigned short 2 0 ~ 2^16-1
长整型 long 4 -2^31 ~ 2^31-1
无符号长整型 unsigned long 4 0 ~ 2^32-1
双长整型 long long 8 -2^63 ~ 2^63-1
无符号双长整型 unsigned long long 8 0 ~ 2^64-1

可以用求字节运算符sizeof求出数据类型所占字节数:sizeof(数据类型);

如:

1
2
sizeof(int);	//求int类型所占字节数
sizeof(unsigned long); //求unsigned long类型所占字节数

(2)字符型

数据类型 关键字 大小(字节) 取值范围
字符型 char 1 ASCII码表

ASCII码表:

字母:大写字母AZ和小写字母az;

数字:0~9;

专用字符:! ” # ’ & % * ( ) + - / _ ^ { } [ ] ** ~ < > = , . ? ; : | ~ ` 等;

空格符:空格’ ’、水平垂直制表符’\t’、换行符、换页符等。

不能显示的字符:空(NULL)字符’\0’、警告字符’\a’、退格符’\b’、回车符’\r’等。

特殊情况:当字符存储其他文字字符时,其占两个或多个字节,如:char ch=’中’;

中文字符实际上占2个字节,内存的补正(补齐)会把超过一个字节但不超过四个字节的,统一按照4个字节处理。

(3)浮点型

浮点型数据是用来表示具有小数点的实数。

为什么在C语言中把实数称为浮点数呢?

在C语言中,实数是以指数的形式存放在存储单元里的。一个实数表示为指数可以有不止一种形式,但它们表示同一个数值。
如:

1
3.14159可以表示为3.14159*10^00.314159*10^10.0314159*10^231.4159*10^-1314.159*10^-23141.59*10^-3

以此看来,小数点的位置是可以在314159几个数字之间和之前或之后加0浮动的,只要在小数点浮动的同时改变指数的值,就可以保证它的大小不会被改变。由于小数点的位置可以浮动,所以实数又称为浮点数。

在指数形式的多种表示方式中,把小数点前的数字为0和小数点后的数字不为0的表示形式称为规范的指数形式,在程序以指数形式输出一个实数时,必然以规范化的指数形式输出,如123.456的规范形式为1.23456e+2。

C语言中的浮点型

数据类型 关键字 大小(字节) 取值范围(绝对值)
单精度浮点型 float 4 0以及1.2*10^-38 ~ 3.4 *10^38
双精度浮点型 double 8 0以及1.2*10^-308 ~ 3.4 *10^308
长双精度浮点型 long double 8 0以及1.2*10^-308 ~ 3.4 *10^308

2.3、确定常量的类型

在C语言中,不仅变量有类型,常量也是有类型的。为什么要把常量分为不同的类型呢?在程序中出现的常量是要存放在计算机中的存储单元中的,这就必须确定分配给它多少字节,按什么方式存储。例如,程序中有整数16,在编译器中会分配给它4个字节,按补码形式存储。

怎样确定常量的类型呢?从常量的表示形式即可判定其类型。

(1)符号常量:对于符号常量来说很简单,只要看到由单引号括起来的单个字符或转义字符就是字符常量,以ACSII码值进行存储,占1个字节。

(2)整型常量:不带小数点的数值是整型常量,但要注意其有效范围。如果某系统中为整型数据分配2个字节,其表示范围为-3276832767(-2^162^16-1),如果在程序中出现数值常量12345,则系统把它作为int型处理,用4个字节存放。如果出现数值常量45678,由于其大于32767,2个字节放不下,所以系统就会把它作为长整型(long int)进行处理,分配4个字节。以此类推,如果出现的数值常量超过4个字节的表示范围的话,系统就会把它当作双长整型(long long int)。
在整数的末尾加上大写字母L或小写字母l,则表示它是长整型(long int)。例如666l、123L等。在VS编译环境中int和long int数据都分配4个字节,因此没有必要用long int。如果在整数的末尾加上大写字母LL或小写字母ll,则表示它是长长整型(long long int),其在内存中占8个字节。

(1)浮点型常量:凡是以小数形式或指数形式出现的实数,都是浮点型常量,在内存中以指数形式存储。如:0、10为整型常量,0.0、10.0是浮点型常量。注意:对于浮点型常量,编译器默认是按双精度进行处理的。例如:float a=3.14159;在编译时,给float变量分配4个字节,而对于浮点型常量3.14159来说,则是按double型处理,分配8个字节,编译器有时会发出“警告”。这种警告一般不会影响程序运行结果,但是会影响程序运行结果的精确度。
可以在常量的末尾加一个F或f,强制指定常量的类型为单精度。如果在实型常量的末尾加一个L或l,就是指定此常量为long double类型。

注意:区分类型与变量、变量与常量的概念

3、格式化输入输出函数

3.1、有关数据输入和输出的概念

从之前的每个程序看来,几乎每一个C程序都包含输入和输出。因为程序要进行运算,就必须给出数据,而运算的结果当然也需要输出了,便于人们应用。没有输出的程序是没有意义的,输入输出是程序中最基本的操作之一。
讨论程序的输入和输出时首先要注意:所谓的输入输出是以计算机主机为主而言的。
计算机向输出设备(如显示器、打印机等)输出数据称为输出,从输入设备(如键盘、磁盘、光盘、扫描仪等)向计算机输入数据称为输入。
C语言函数库中提供了一批“标准输入输出函数”,它是以标准的输入输出设备(一般为终端设备)为输入输出对象的。其中有:printf,scanf,putchar,getchar,puts,gets等函数。在使用这些库函数时,要在程序文件的开头用预处理指令#include把相关头文件加载进本程序中,如:

1
2
3
4
#include <stdio.h>
#include <math.h>
#include <string.h>
#include <windows.h>

3.2、用printf函数输出数据(格式化输出函数)

printf函数的一般格式为:

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

例如:

1
2
int a = 10,c = 'a';
printf(“%d,%c\n”,n,c);

括号内包括两个部分:

①格式控制

格式控制是用双引号括起来的一个字符串,称为“转换控制字符串”,简称“格式字符串”。

它包括两个信息:

●格式声明:由%和格式字符组成,用作将输出的数据转换为指定的格式然后输出。格式声明符总是由‘%’字符开始的。

常用的格式声明符有:

%d(十进制),%o(八进制),%x(十六进制),%u(无符号十进制),%c(单个字符),

%s(字符串),%f(单精度),%lf(双精度),%e(科学计数法),%i(自适应整数类型),%g(自适应浮点类型)。

●普通字符:即需要在输出时原样输出的字符。

如:\n、\t、空格、逗号等。

1
printf(“a=%d\n”,a);		//这里的a=和\n是原样输出的:a=1’\n’

②输出列表

输出列表就是程序需要输出的一些数据,可以是常量、变量或表达式。

格式化输出函数printf是个函数,所以“格式控制字符串”和“输出列表”实际上都是函数的参数。

printf函数的一般形式可以表示为:printf(“参数1”,参数2,参数3,…,参数n);

参数1是格式控制字符串,参数2~参数n是所需要输出的数据。

我们还可以自定义输出的分隔符和输出的格式。

如:

1
2
3
printf(“%d %c %f\n”,a,b,c);
printf(“%d,%c,%f\n”,a,b,c);
printf(“%d\t%c\t%f\n”,a,b,c);

%4d和%5.2f中的4和5.2是在格式声明中用来指定输出数据的域宽(所占列数)的,如%6d,是指定输出的整型数据占6列,%8.3f,是指输出的浮点型数据总共占8列,其中小数占3列,小数点占1列。还可以在域宽数字前加一个’-’号,用于使输出左对齐。

1
printf(“%-4d\t%5.2f\n”,a,b);

3.3、用scanf函数输入数据(格式化输入函数)

scanf函数的一般格式为:

scanf(格式控制,地址列表);

其中“格式控制”的含义与printf函数相同。“地址列表”是由若干个地址组成的列表,可以是变量的地址,或者是字符串的首地址。

使用格式化输入函数scanf时,需要注意以下几点:

①函数的地址列表是以变量的地址作为参数的时候,变量名前面必须加一个取地址符号’&’,用于取此变量的地址,否则会出错。

如:

1
scanf(“%d %f %c”,a,b,c);//错误

②如果在“格式化控制字符串”中除了格式声明符以外还有其他字符,则应在输入数时在对应位置上输入与这些字符对应的相同的字符。

如有

1
scanf(“a=%d,b=%f,c=%c”,&a,&b,&c);

要想输入数据1,2,3,则应输入:a=1,b=2,c=3(回车)//注意这里的a=、b=、c=和逗号’,’

当然,其中的格式可以自己定义,如可以用空格’ ’隔开,还有输入时间的时分秒可以用’:’隔开等。

这里特别要注意字符和字符串的混合输入问题。

如:

1
scanf("%c%c%c",&a,&b,&c);//输入x y z,则a=‘x’,b=‘ ’,c=‘y’;应连续输入xyz。

输入输出函数数据之间的格式化间隔符(空格、TAB、回车或非法数据(“%d”,12A)等)

非格式间隔符(‘,’、‘!’等任意间隔符);使用非格式间隔符时需要在输入的时候也用此间隔符分隔。

③在输入数值型数据时,如输入空格、回车、tab键或非法字符(除scanf格式声明中指定的)等不属于数值的字符,则认为此数据输入结束。

如:

1
scanf(“%d%c%f”,&a,&b,&c);//输入1234a567.8b9,a=1234,b=’a’,c=567.8 //&取址符

④scanf函数中double型数据需要使用%lf才能正常得到所输入的值,而输出可以用%f输出。

⑤在输入时也可以进行域宽控制

如:

1
scanf("%3d%4d",&a,&b); //输入12345678,则a=123,b=4567;

注意各种类型的数据混合输入存在的问题

(1)字符型数据的输入和输出

①putchar字符输出函数

如:

1
2
3
char a = 'c';
putchar(a);//输出字符型变量a的值
//请区别对待putchar('a');

②getchar字符输入函数

如:

1
2
3
char ch;
ch=getchar(); //从键盘输入一个字符,存入字符变量ch中
printf("%c",getchar()); //也可在格式化输出语句中直接输出所接收的字符

由于scanf、getchar等输入函数没有从键盘接收到数据就不会继续执行,所以在程序中可以起到与system(“pause”);函数类似的暂停效果。

三、运算符和表达式

1、C语言运算符

1.1、C运算符的分类

C提供了各种各样不同作用的运算符,共分为以下几类:

(1)算术运算符 (+、-、*、/、%、++、–)加、减、乘、除、模、自增、自减

(2)关系运算符 (>、<、==、>=、<=、!=)

(3)逻辑运算符 (&&与、||或、!非)

(4)位运算符 (左移<<、右移>>、按位非~、按位或|、按位异或^、按位与&)

(5)赋值运算符 (=及其扩展赋值运算符+=、-=、=、/=、%=等)等于

(6)条件运算符 (? :) (表达式1)?(表达式2):(表达式3) 三目运算符

(7)逗号运算符 (,)

(8)指针运算符 (*、&)

(9)求字节数运算符 (sizeof())

(10)强制类型转换符 ((类型名))

(11)成员引用符 (.、->)结构体或共用体的成员引用符

(12)下标运算符 ([])数组元素下标

(13)其他运算符 (如函数调用运算符(),复合语句符{},语句结束符;等)

这一章主要讲解算术运算符、关系运算符、逻辑运算符、赋值运算符和条件运算符等常用运算符的运算规则,其他运算符待之后几章讲解相关知识点时再仔细讲解。

1.2、运算符的操作数以及目数

①操作数:操作数是运算符操作的实体,是表达式的一个组成部分,它规定了运算指令中进行数值运算的量。

②目数:这些运算符根据其参与运算的操作数的个数不同,而分为了单目运算符、双目运算符和三目运算符。

有1个操作数的运算符为单目运算符,有2个操作数的运算符为双目运算符,有3个操作数的运算符为三目运算符。C语言中运算符的操作数最多为3个。

如:

1
2
3
int x,y;
x = 1; //其中x和1分别是赋值运算符=的左操作数和右操作数,操作数为两个,所以=是双目运算符
y = x++; //其中y和x++分别是赋值运算符=的左操作数和右操作数,而x又是自增运算符++的左操作数,所以++是单目运算符

1.3、运算符的优先级

C运算符的优先级及相关内容如下表所示:

通常意义上来说(广义)运算符的优先级大致为:单目>双目>三目。

优先级详细划分为:算术运算符>关系运算符>逻辑运算符>条件运算符>赋值运算符>逗号运算符

2、算术运算符

2.1、基本的算术运算符

运算符 含义 举例 结果
+ 正号运算符(单目运算符) +a a的值
- 负号运算符(单目运算符) -a a的算术负值
* 乘法运算符(双目运算符) a*b a和b的乘积
/ 除法运算符(双目运算符) a/b a除以b的商
% 取余运算符(双目运算符) a%b a除以b的余数
+ 加法运算符(双目运算符) a+b a与b的和
- 减法运算符(双目运算符) a-b a与b的差

说明:

①由于键盘没有×号,运算符×以*代替。

②由于键盘没有÷号,运算符÷以/代替(反斜杠)。(注意区分\斜杠)

③整数相除的结果仍为整数!如:-5/3结果为-1(向0取整,舍去小数)。(5.0/3=1.666666)

④%(求余运算符)要求参加运算的运算对象(即操作数)为整数,结果也为整数。如8%6结果为2。(思考一下8.0%6=?)

⑤除%以外运算符的操作数都可以是任何算术类型。

2.2、自增、自减运算符

++(自增运算符)、–(自减运算符)它们都属于单目运算符。

例如:

++i;–i;(在使用i之前,先使i的值加(减)1,先加减后使用)

i++;i–;(在使用i之后,使i的值加(减)1,先使用后加减)

粗略地看,++i和i++的作用都相当于i=i+1。但是++i和i++的不同之处在于:++i是先执行i=i+1,再使用i的值;而i++是先使用i的值,再执行i=i+1。

例如:

1
2
3
int i=1,j;
j=++i; //i的值先+1变成2,再赋值给j,j的值为2
j=i++; //先将i的值赋值给j,j的值为2,然后再将i的值+1变成3

又例如:

1
2
3
int i=3
printf(“%d”,++i); //输出4,i=4
printf(“%d”,i++); //输出4,i=5

注意:++和–运算符只能用于变量,而不能用于常量或表达式,如5++或–(i+j)等。

使用++和–的时候,会出现一种情况,如i+++j,是理解成(i++)+j呢?还是理解成i+(++j)呢?为了避免二义性,可以加一些“不必要的括号”,如(i++)+j。

2.3、算术表达式与运算符的优先级和结合性

什么叫算术表达式呢?

用算术运算符和括号将运算对象(操作数)连接起来的、符合C语法规则的式子,就称为C语言算术表达式。运算对象包括常量、变量、函数等。例如:

1
(a+b)*c/d-1.5+'a'

C语言规定了运算符的优先级和结合性。

与数学运算符的优先级和结合性类似,乘、除、模(、/、%)运算符的优先级要高于加减(+、-),结合方向都是“从左至右”,同一优先级的运算符,按结合性依次执行。

2.4、不同类型数据间的混合运算

在程序运行中,经常会遇到不同类型的数据进行运算,如6+8.8。如果一个运算符的两侧数据类型不同,则先自动进行类型转换,使二者具有同一种类型,然后进行运算。因此整型、实型、字符型数据间可以进行混合运算。

(1)+、-、*、/运算的两个操作数进行运算,如果其中有一个数位float或double型,结果是double型,因为系统将所有float型数据都先转换为double型,然后进行运算的。

(2)如果是int型与float或double型数据进行运算,先把int型和float型数据转换为double型,然后再进行运算,结果为double型。

(3)字符型(char)数据与整型数据进行运算,就是把字符的ASCII码与整型数据进行运算。如:10+’A’,由于字符A的ASCII码为65,就相当于10+65,等于75,字符型数据可以直接与整型数据进行运算。如果字符型数据与实型数据进行运算,则将字符对应的ASCII码转换为double型数据,然后进行运算。

(4)整型(int)与无符号整型(unsigned)之间进行运算,以无符号整型为准,先把int型数据转换为unsigned型,然后再做运算。

以上转换是隐式类型转换,是编译系统自动完成的,用户不必过问。

例如:

以下表达式的值为多少?

10+’a’-3*2.5+5/2-7%2=107-7.5+2-1=100.500000

例题:给定一个大写字母,要求输出其对应的小写字母。

1
2
char ch = 'A';
putchar(ch+32);

3、强制类型转换运算符

C语言中可以运用强制类型转换运算符将一个表达式转换成所需的类型。

强制类型转换的一般格式为:(类型名)(表达式)

例如:

1
2
3
4
(double)a	//将变量a强制类型转换为double类型
(int)(x+y) //将表达式(x+y)的结果强制类型转换为int类型
(float)(5%3)//将表达式(5%3)的结果强制类型转换为float类型
(int)6.8%3 //将小数6.8强制类型转换为int类型,然后再于整数3求余

注意:表达式应该用括号括起来。

如果写成:(int)x+y,则只是将x转换成整型,然后再与y相加。如果想要将表达式x+y的值转换为整型,应写成(int)(x+y)。

强制类型转换后的值只是临时值,不对数据本身操作。

4、关系运算符

关系运算符是一个双目运算符,用于比较两个操作数之间的大小关系,其中包含大于(>)、小于(<)、等于(==)、大于等于(>=)、小于等于(<=)、不等于(!=)这些运算符,运算结果是一个逻辑值(真/假)。

逻辑值:0为假,非0为真,如:1、2、100、-1、-10、’a’、12.3 等数据的逻辑值为真,逻辑值为真的表达式结果默认为1,也就是1代表真,只有0代表逻辑假。

1
2
3
4
5
6
7
8
9
//关系运算符(>、<、==、>=、<=、!=)
//运算结果为逻辑值:0表示假,非0表示真(1)
1<=2; //结果为真(1),因为1小于等于2,条件满足
1>3 //结果为假(0),因为1大于3,条件不满足
printf("%d\n", 5 != 5); //结果为假(0),因为5不等于5,条件为假

int a=10,b=20;
printf("%d\n",a<=b); //结果为真(1),a小于b条件满足
printf("%d\n",a==b); //结果为假(0),a等于b条件不满足

5、逻辑运算符

逻辑运算符用于连接多个条件语句,判断多个条件值联合的结果,其中包含与(&&)、或(||)、非(!)三种运算。

与、或、非运算的基本使用如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/*
&&(与运算):双目运算符,用于连接两个表达式,相当于并且的意思
运算规则:同真为真,否则为假
只有&&运算符两边操作数的逻辑值同时为真,整个逻辑表达式的值才为真,否则整个表达式的值为假
*/
printf("%d\n", 1<2 && 5<4); //1<2并且5<4,其中1<2为真,5<4为假,整个表达式的值为假
printf("%d\n", 1<2 && 5!=4); //1<2并且5!=4,其中1<2为真,5!=4为真,整个表达式的值为真

/*||(或运算):双目运算符,用于连接两个表达式,相当于或者的意思
运算规则:同假为假,否则为真
只有||运算符两边的操作数的逻辑值同时为假,整个逻辑表达式的值才为假,否则整个表达式的值为真
*/
printf("%d\n", 0 || -1); //其中-1的逻辑值为真,整个表达式的结果为真(1)
int x=0,y=0;
printf("%d\n", x || y);
//x和y的值都为0,也就是逻辑值都为假,所以整个逻辑表达式 x || y的值就为假,输出结果为0

/*
!(非运算):单目运算符,用于将之后的表达式的逻辑值取反
运算规则:真变假,假变真,(取反)
*/
printf("%d\n", !-2); //其中(-2)的逻辑值为真,所以(!-2)的逻辑值为假,输出结果为0
int n = 0;
printf("%d\n", !n); //其中n的逻辑值为假,所以(!n)的逻辑值为真,输出结果为1

//&&和||存在的短路运算:
int g = 1, h = 1;
++g || ++h; //||的短路运算:前一个条件为真,之后的条件不管为真还是为假都不执行
printf("%d\t%d\n", g, h); //结果为:1 1

g = 1, h = 1;
--g && --h; //&&的短路运算:前一个条件为假,之后的条件不管为真还是为假都不执行
printf("%d\t%d\n", g, h); //结果为:0 1

与、或、非运算的操作数可以是int、float、char等多种数据类型的表达式。

6、条件运算符

条件运算符(? :)用于条件判断。

基本格式:

1
(表达式1)?(表达式2):(表达式3);

运算规则:首先判断表达式1的逻辑值,如果表达式1的逻辑值为真,则执行冒号’:‘之前的表达式2,整个条件表达式的值为表达式2的值;如果表达式1的逻辑值为假,则执行冒号’:‘之后的表达式3,整个条件表达式的值为表达式3的值。

例如:

1
2
3
4
5
6
7
//输出两个数中的最大值
int x, y;
scanf("%d %d", &x, &y);
printf("%d\n", x > y ? x : (x == y ? x : y));
//判断表达式(x>y)的逻辑值,如果为真则整个表达式的值为冒号之前的x的值
//否则执行(x == y ? x : y),这又是一个含有条件运算符的条件表达式
//判断(x == y)的值,如果为真则整个表达式的值为x,否则为y

7、位运算符

位运算符是针对数据的二进制进行运算的,其中包含左移”<<”、右移”>>”、按位非”~”、按位或”|”、按位异或”^”、按位与”&”这六种位运算,其只能对整型数据操作,运算规则如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/*
左移运算符"<<",是一个双目运算符
基本格式:整数<<n
其用于将一个整数的二进制向左移动n个二进制位
*/
int a = 10;
printf("%d\n",a<<2); //输出结果为:40,相当于乘以2的2次方
//将变量a的值左移两个二进制位并输出,a本身的值不变
//a等于10,10的二进制为00001010,左移2位就是00101000,转换为十进制为40

/*
右移运算符">>",是一个双目运算符,与左移类似
基本格式:整数>>n
用于将一个整数的二进制向右移动n个二进制位
*/
a>>=3; //a=1,相当于a除2的3次方,
printf("%d\n",a); //输出结果为:1
//相当于a=a>>2,将变量a的值右移两个二进制位后赋值给本身,a本身的值变为40
//a等于10,10的二进制为00001010,右移3位就是00000001,转换为十进制为1

/*
按位非"~",是一个单目运算符
基本格式:~整数
用于将一个整数的二进制各个位数取反
*/
short b = 20;
printf("%d\n",~b); //输出结果为:-21
//b等于20,20的二进制为:00000000 00010100,各位取反为:11111111 11101011
//由于计算机内是用补码做运算的,所以负数的补码转换成原码后为:10000000 00010100,结果为-21

/*
按位或'|',双目运算符
基本格式:整数|整数
用于将两个整数的二进制相或,遵循有1为1,否则为0的规则
*/
int a = 10,b = 20;
printf("%d\n",a|b); //结果为30
// a: 00000000 00001010
// b: 00000000 00010100
// a|b: 00000000 00011110
// 相同二进制位相或,有1为1,否则为0

/*
按位与'&',双目运算符
基本格式:整数&整数
用于将两个整数的二进制相与,遵循有0为0,否则为1的规则
*/
int a = 10,b = 30;
printf("%d\n",a&b); //结果为10
// a: 00000000 00001010
// b: 00000000 00011110
// a&b: 00000000 00001010
// 相同二进制位相与,有0为0,否则为1

/*
按位异或'^'
基本格式:整数^整数
用于将两个整数的二进制相异或,遵循相同为0,相异为1的规则
*/
int a = 10,b = 30;
printf("%d\n",a^b); //结果为20
// a: 00000000 00001010
// b: 00000000 00011110
// a^b: 00000000 00010100
// 相同二进制位相异或,相同为0,相异为1

8、表达式和C语句

8.1、表达式和语句的基本概念

一个C语言程序由若干个源程序文件组成,一个源文件由若干个函数和预处理指令以及全局变量声明部分组成。如一个函数有数据声明部分和执行语句,其都是由语句组成的。语句的作用使向计算机系统发出操作指令,要求执行相应的操作。一个C语句经过编译后产生若干条机器指令。
C程序的基本组成单位是函数,函数又由一条或多条C语句构成,而C语句又是多个表达式的组合。

8.2、逗号表达式

逗号表达式是一类特殊的表达式,其是由逗号运算符隔开的多个表达式的组合,逗号起到分隔的作用。

如:

1
2
int a=10,b=20,c=30;
//逗号隔开的多个相同类型变量的定义个初始化赋值

逗号分隔的多个表达式是依次从左至右执行的,如:

1
2
3
int a,b,c;
a=1,b=2,c=a+b;
//这里是先执行a=1,再执行b=2,最后才执行c=a+b,c的值为3

整个逗号表达式的值以逗号分隔的最后一个表达式为准,如:

1
2
3
4
5
6
int a,b,c,d;
d=(a=1,b=2,c=a+b);
//这里d的值为逗号分隔的最后一个表达式的值,也就是c=a+b的值,相当于d=c,d的值为3

d=a=1,b=2,c=a+b;
//这里d的值为第一个表达式的值,d的值为1

8.3、最基本的语句——赋值语句

在C语言中最常用的语句就是赋值语句和输入输出语句了。其中最基本的就是赋值语句,程序中的计算功能大部分是由赋值语句实现的,几乎每个有实用价值的程序都包括赋值语句。

(1)赋值运算符’=’

与数学中的’=’不同的是,在C语言中,’=’为赋值运算符,它是用来将一个数据赋值给一个变量的。如a=1;的作用是执行一次赋值操作(赋值运算),把1赋值给变量a。也可以将一个表达式的值赋值给一个变量,如a=1+2;a=b+c+1;等。

(2)复合的赋值运算符

在赋值运算符=之前加上其他运算符,可以构成复合的运算符。

主要有:+=、-=、=、/=、%=这几种复合赋值运算符。

例如:

1
2
3
4
x+=y;	//相当于:x=x+y;
x=y; //相当于:x=x*y;
x+=y+1; //相当于x+=(y+1); x=x+(y+1);
x=y+1; //相当于x=(y+1); x=x(y+1);

(3)赋值表达式和赋值语句

右值和左值

左值应该为可修改的变量,右值可以为任意符合规范且赋值有效的表达式。

1
2
3
4
a=1
a=b=c=1; //a=1;b=1;c=1;
a=(b=1)+(c=2); //a=3;b=1;c=2;
a=(b=1)(c=2);

赋值运算符是按照“从右至左”的结合顺序运行的。

(4)变量赋初值

在变量定义时对变量赋值就称为变量赋初值,也称为变量的初始化赋值。

1
int a=1;

8.4、C语句的分类

C语句分为以下5类:

(1)表达式语句

表达式语句是由一个表达式加一个分号构成,最典型的是由赋值表达式构成的赋值语句,例如:a=3是赋值表达式,而a=3;是赋值语句。还有由逗号表达式构成语句,的由条件表达式构成的条件表语句,和逻辑表达式构成的逻辑语句等。

(2)控制语句

控制语句是由流程控制表达式组成的语句,用于完成一定的流程控制功能。

C语言提供9种控制结构语句,它们分别为:

①if()…else… (选择结构)

②for(;;)… (循环结构)

③while()… (循环结构)

④do…while(); (循环结构)

⑤break; (结束整个循环)

⑥continue; (结束本次循环,执行下次循环)

⑦switch (多分支选择结构)

⑧return (函数返回语句)

⑨goto (跳转语句,在结构化程序中最好不要用goto语句)

(3)函数调用语句

例如:

1
printf("hello world!");

这是一个简单的调用输出函数的语句。其中printf(“hello world!”)是一个函数调用,加一个分号’;’就是函数调用语句了。

(4)空语句

空语句就是:

1
;	//只有一个分号的语句,什么都不做

(5)复合语句

在C语言中可以用{}把一些语句括起来,形成一条复合语句(又称为语句块)。

如:

1
2
3
4
5
6
{	//这是一个复合语句(语句块)
int a,b,c;
a=1;
b=2;
c=3;
}

复合语句需要注意变量的作用域问题,其常用于选择分支结构和循环结构等控制语句中,其目的是让控制语句可以控制多条语句,达到我们想要的效果。

四、选择结构和循环结构

C语言中的基本控制结构分为顺序结构、选择结构和循环结构,它们控制着程序的执行。

1、选择结构

在很多情况下,需要根据某个条件是否满足来决定是否执行指定的操作任务,或者从给定的两个或多个操作选择其中一个执行,这就需要用到我们的选择结构了。

C语言提供了两种选择结构:if语句和switch语句。

1.1、if语句

If语句的一般形式:

if(表达式)语句1;

[else 语句2;]

根据if语句的一般形式,可以写成不同的形式,最常用的有以下三种形式:

(1)if(表达式)语句1; //单独的一个if语句,用于实现两个分支的判断选择

(2)if(表达式)语句1; //if和else组合,用于实现两个分支的判断选择

​ else 语句2;

(3)if(表达式)语句1; //if else嵌套,用于实现多分支结构

​ else if(表达式)语句2; //if和else的配对问题,就近原则

​ else if(表达式)语句3; //else和前面最近的没有配对的if配对

​ else语句4;……

例题1:从键盘输入一个年份,判断其是否为闰年。

提示:闰年是指能被4整除并且不能被100整除(普通闰年),或者能被400整除的年份(世纪闰年),否则就为平年。

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

int main()
{
int year;

printf("请输入一个年份:\n");
scanf("%d", &year);

if (year % 400 == 0)
{
printf("%d年是世纪闰年。\n", year);
}
else if (year % 4 == 0 && year % 100 != 0)
{
printf("%d年是普通闰年。\n", year);
}
else
{
printf("%d年是平年。\n", year);
}

getchar();
getchar();
return 0;
}

1.2、switch语句

switch语句的一般形式:

switch(判断条件)

{

case 常量1:语句1;

case 常量2:语句2;

case 常量3:语句3;

……

case 常量n:语句n;

default:语句n+1;

}

例题2:从键盘输入的学生成绩(0~100分),给学生的成绩评定等级。优秀(成绩>=90)、良好(90>成绩>=80)、及格(80>成绩>=60)和不及格(成绩<60)。

(1)用if语句实现

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

int main()
{
int score;
printf("请输入一个成绩(1~100分):\n");

scanf("%d", &score);

if (score >= 0 && score <= 100)
{
printf("成绩等级:");
if (score >= 90)
printf("优秀\n");
else if (score >= 80)
printf("良好\n");
else if (score >= 60)
printf("及格\n");
else
printf("不及格\n");
}
else
{
printf("成绩输入错误!\n输入的成绩应该在1~100之间!\n");
}

getchar();
getchar();
return 0;
}

(2)用switch语句实现

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

int main()
{
int score;
printf("请输入一个成绩(1~100分):\n");

scanf("%d", &score);

if (score >= 0 && score <= 100)
{
printf("成绩等级:");
switch (score / 10)
{
case 10:
case 9: printf("优秀\n"); break;
case 8: printf("良好\n"); break;
case 7:
case 6: printf("及格\n"); break;
default: printf("不及格\n");
}
}
else
{
printf("成绩输入错误!\n输入的成绩应该在1~100之间!\n");
}

getchar();
getchar();
return 0;
}

2、循环结构

在程序所处理的问题中,我们常常会需要重复处理同一类操作,这样我们就需要循环控制它们的执行了。

2.1、for循环

for循环的一般格式为:

1
2
3
4
5
6
7
for(表达式1;表达式2;表达式3) 		//注意:表达式之间的分隔符‘;’不能少
{
循环体;
}
//表达式1:条件的初始化语句
//表达式2:循环继续的条件
//表达式3:改变循环条件的语句

如:循环输出整数0~9

1
2
for(int a=0;a<10;a++)
printf(“a=%d\t”,a);

2.2、while循环

while循环的一般格式为:

while(表达式)

{

​ 循环体;

}

2.3、do while循环

do while循环的一般格式:

do

{

​ 循环体;

}while(表达式);

2.4、循环结构的分类

(1)当型循环

当循环条件满足时才执行循环体中的语句。

(2)直到型循环

直到条件不满足时才结束循环,至少会执行一次循环体。

例题:输出以下图案:

(1)

1
2
3
4
5
***
***
***
***
***

C语言代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
for(int i=0;i<5i++)	//for循环
{
printf("***\n");
}

int i = 5;
where(i--) //where循环
{
printf("***\n");
}

i = 5;
do //do where循环
{
printf("***\n");
}where(--i);

3、跳转语句

3.1、break语句

break语句用于switch分支结构和循环结构中,起到跳出switch分支结构或者跳出整个循环结构的作用。

基本格式:

1
break;

3.2、continue语句

continue语句用于循环语句中,起到跳出本次循环,继续执行下次循环的作用。

基本格式:

1
continue;

3.3、goto跳转语句

goto语句支持跳转到指定的位置。

基本格式:

1
2
3
标记:	//标记的命名需要符合标识符的命名规则
……
goto 标记; //执行goto语句后,会跳转到指定标记的位置

由于goto语句使程序执行的流程变复杂,不利于结构化程序的结构控制和意义的理解,所以一般不使用goto语句。

五、数组

在之前的程序中使用的变量都属于基本类型,如整型、字符型、浮点型数据,这些都是简单的数据类型。对于简单的问题,使用这些简单的数据类型就可以了。

由于程序有时候需要处理大批量的数据,如:一个班有60个学生,每个学生都有一个成绩,求这些学生的平均成绩,我们怎么实现呢?按照之前定义变量的方法,需要定义60个float类型的变量才能保存下所有学生的成绩。

只用简单的数据类型虽然也可以实现,但是会太过麻烦。所以人们就想出了一个办法:既然它们都是同一类性质的数据,就可以用同一个名字来代表它们,而在名字后加一个数字来表示是第几个数值。如:用S代表这些学生,那就可以用S1、S2、S3、…、S60代表学生1、学生2、学生3、…、学生60等60个学生的成绩,和数学中的数列类似,这样就产生了数组这样一个概念。

1、什么是数组?

数组的定义可以用以下3点来说明:

(1)数组是一组有序数据的集合。
数组中各数据的排列是有一定规律的,下标代表数据在数组中的序号。

(2)用一个数组名和下标来唯一地标识确定数组中的元素。
如:S10就代表第十个学生的成绩。

(3)数组中的每一个元素都是属于同一数据类型。
规定不能把不同数据类型的数据放在同一个数组中。

2、一维数组的定义和引用

2.1、定义一维数组

基本格式:数据类型 数组名[数组大小];

如:

1
2
float S[60]; 
//定义一个浮点型数组,取名为S,大小为60,用于保存60个学生的成绩。

其中,flost为数据类型(int、char、double),S为数组名,60为数组下标,也就是数组的大小。

2.2、使用数组及引用数组元素

基本格式:数组名[下标];

如:

1
2
3
S[0]=1; 
S[1]=S[0];
S[10]=S[0]+S[1];

注意:

(1)数组名的命名规则和变量名相同,要遵循标识符的命名规则。

(2)在定义数组时,需要指定数组中元素的个数,也就是数组的大小,在定义数组时[]方括号中必须是常量表达式,可以包括数值常量和符号常量。如:int a[6+8]; char b[‘a’];

(3)在C语言中数组下标是从0开始的,如:S[0]代表第一个数组元素。

思考一下数组的最后一个元素是多少?

(4)整个数组的内存大小为元素个数*单个元素类型所占字节数

1
sizeof(数组名);	//求数组内存大小

2.3、一维数组的初始化

为了使程序简洁,常在定义数组的同时,给个数组元素赋值,这称为数组的初始化。

可以用“初始化列表”的方法实现数组的初始化。

如:

1
int a[10]={0,1,2,3,4,5,6,7,8,9};

可以只给数组的部分元素赋初值。

如:

1
int a[10]={0,1,2,3,4};

那未赋值的元素初始值是多少呢? 没有赋初值的元素初始值默认为0

也可以不给数组的大小,利用数据初始化赋值决定数组大小。

如:

1
2
int a[]={0,1,2,3,4,5,6};
//此时数组大小根据初始化时元素的多少来决定,这里的数组大小为7

2.4、一维数组的输入输出

可以使用循环输入/输出每一个数组元素的值。

1
2
3
4
5
6
7
8
9
10
11
int a[10];

for(int i=0;i<10;i++)
{
scanf(“%d”,&a[i]);
}

for(int i=0;i<10;i++)
{
printf(“%d”,a[i]);
}

3、二维数组

二维数组就是多个相同类型相同大小的一维数组的组合。

如果把一维数组看成一行数据的话,二维数组就像一个Excel表格一样,可以写成行和列的排列形式。

3.1、二维数组的定义

基本格式:数据类型 数组名 数组行数 数组列数;

如:

1
int a[3][4];

此二维数组有3行4列,如下图所示:

二维数组的内存:

1
行数*列数*单个元素类型所占字节数

3.2、二维数组的初始化

(1)给全部元素赋值

1
2
3
4
int arr[3][4] ={1,2,3,4,5,6,7,8,9,10,11,12};
//定义一个3行4列的二维数组,并给它的元素初始化赋值
int arr[3][4]={};//全部赋值为0
int arr[3][4]={0};//全部赋值为0

(2)给每一行元素赋值

1
int arr[3][4] ={{1,2,3,4},{5,6,7,8},{9,10,11,12}};

(3)给部分元素赋值

可以不指定数组行大小,如:

1
2
3
4
5
6
7
8
int arr[][3]={1,2};
//给数组前两个元素赋值,数组行数为1,此行最后一个元素默认为0

int arr[][3]={1,2,3,4};
//给数组前四个元素赋值,数组行数为2,第二行最后两个元素默认为0

int arr[][3]={{1,2},{3,4}};
//给数组每行的前两个元素赋值,数组行数为2,每行最后一个元素默认为0

虽然可以不给定数组行的大小,但是必须给定数组列的大小,也必须初始化赋值,因为数组行的大小是由初始化列表中数据的个数决定。

3.3、二维数组元素的访问

二维数组与一维数组类似,通过数组名带下标的形式访问数组元素,由于是二维数组,所以需要带两个下标。

如有以下定义:

1
2
3
4
int a[][4]={1,2,3,4,5,6,7,8,9,10,11,12};
a[0][0]; //访问数组第1行第1个元素
a[1][0]; //访问数组第2行第1个元素
a[2][1]; //访问数组第3行2个元素

3.4、二维数组的输入输出

可以使用双重循环输入/输出每一个二维数组元素的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
int a[3][4];

for(int i=0;i<3;i++)
for(int j=0;j<4;j++)
{
scanf(“%d”,&a[i][j]);
}

for(int i=0;i<3;i++)
for(int j=0;j<4;j++)
{
printf(“%d”,a[i][j]);
}

4、字符数组

字符数组简单的来说就是char类型的数组。

为什么要把字符数组分开讲呢?
字符数组是一类特殊的数组,由于C语言中没有字符串类型,所以字符串是存放在字符型数组中的。

4.1、字符数组的定义及初始化

用来存放字符数据的数组就是字符数组。字符数组中的一个元素存放一个字符。

如:

1
2
char c[6]={‘a’,’b’,’c’,’d’,’e’,’f’}; 	//定义一个字符数组c,并以单个字符初始化赋值
char s[8]=”abcdefg”; //定义一个字符数组c,并以字符串初始化赋值

注意:以字符串的形式初始化赋值时,字符串的结尾有一个字符串结束标志’\0’,所以定义的数组大小至少要比字符串的长度大一个。

4.2、引用字符数组中的元素

通过数组名带下表的形式访问数组元素,如:

1
2
3
char s[10]; //定义一个字符数组
s[0]=’a’; //引用字符数组s的第一个元素s[0]并赋值
s[1]=’b’; //引用字符数组s的第二个元素s[1]并赋值

4.3、字符数组的输入和输出

如有定义以下字符数组:

1
char s[10];

方法一:以%c的格式循环输入/输出字符数组中的每个数组元素的值

1
2
3
4
5
6
7
8
9
for(int i=0;i<10;i++)
{
scanf(“%c”,&a[i]);
}

for(int i=0;i<10;i++)
{
printf(“%c”,a[i]);
}

方法二:以%s的格式输入/输出字符串

1
2
scanf(“%s”,a);
printf(“%s”,a);

4.4、二维字符数组

二维字符数组与普通数组的定义方式一致。如:

1
char s[3][10];

二维字符数组的每一行都可以存储一个字符串。

如:二维字符数组的输入输出:

1
2
3
4
5
6
7
8
9
for(int i=0;i<3;i++)
{
scanf(“%s”,s[i]);
}

for(int i=0;i<3;i++)
{
printf(“%s”,s[i]);
}

4.5、字符串处理函数

常用的字符串处理函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
puts(字符数组);
//字符串的输出函数

gets(字符数组);
//字符串的输入函数

strlen(字符串);
//测字符串长度函数,只计算有效字符,不包括’\0’

strcat(字符数组1,字符数组2);
//字符串连接函数,将字符串2连接到字符串1的末尾,并返回字符串1的起始地址

strcpy(字符数组1,字符数组2);
//字符串复制函数,将字符串2复制到字符串1中,并返回字符串1的起始地址

strcmp(字符串1,字符串2);
/*字符串比较函数
比较字符串1和字符串2的大小,从两个字符串的第一个字符开始,按照各字母的ASCII码比较各个字符的大小,
如果字符串1比字符串2大,则返回1,如果字符串1比字符串2小,则返回-1,如果相等则返回0
*/

strstr(字符串1,字符串2);
//求子串函数,求字符串1(父串)中字符串2(子串)首次出现的位置(地址)

使用字符串处理函数需要包含头文件:#include<string.h>

六、函数

1、函数的基本概念

什么是函数?
所谓“函数”是从英文function翻译过来的,其意思既是“函数”,也是“功能”。从本质意义上来说明函数就是用来完成一定功能的,是把实现功能的代码封装起来,给这些封装起来的代码取个名字就是函数名,每一个函数用来实现一个特定的功能,函数的名字对应其代表的功能。

为什么要使用函数呢?
函数就是为了提高代码的复用性,提高程序的可读性,在使用过程中比较灵活,更加方便了程序代码的编写。

2、函数的定义

C语言要求,在程序中用到的所有函数,必须“先定义,后使用”。

定义函数应包括以下几个内容:

(1)指定函数的名字,以便后续按名调用。
(2)指定函数的类型,即函数返回值的类型。
(3)指定函数参数的名字、数据类型和个数,以便在调用函数时向它们传递数据。(无参函数不需要这一项)
(4)指定函数所完成的功能,也就是规定函数要完成什么操作,说明函数是做什么的,这是最重要的一点,函数的功能都是写在函数体中的。

函数定义的一般格式如下:

2.1、定义无参无返回值函数

基本格式:

1
2
3
4
void 函数名()	//void为空类型
{
函数体;
}

在函数体里描述函数实现的功能。

如:

1
2
3
4
void fun()
{
printf(“--------------------------------------------------\n”);
}//这里定义了一个函数,函数名为fun,无返回值,完成打印分隔线的功能。

2.2、定义有参数无返回值函数

基本格式:

1
2
3
4
void 函数名(函数参数1,函数参数2,……)
{
函数体;
}

如:求两个数据的和并输出

1
2
3
4
5
6
7
void max(int x,int y)
{
int z;
z=x>y?x:y;
printf(“%d\n”,,z);
}
//输出两个数之间的最大值

2.3、定义有参数有返回值函数

基本格式:

1
2
3
4
5
类型名 函数名(函数参数1,函数参数2,……)
{
函数体
return 值;
}

如:

1
2
3
4
5
6
7
int max(int x,int y)
{
int z;
z=x>y?x:y;
return z;
}
//求最大值函数并返回

2.4、定义无参数有返回值函数

基本格式:

1
2
3
4
5
类型名 函数名()
{
函数体;
return 值;
}

如:

1
2
3
4
5
6
7
int SCANF()
{
int temp;
printf("请输入一个整型数据:\n");
scanf("%d",&temp);
return temp;
}

3、函数的调用

3.1、函数调用语句

函数调用的基本格式:函数名(实参列表);

如:

1
2
int a=10,b=20,c;
c=max(a,b); //函数调用语句,调用max函数求a和b两个数中的最大值赋值给c

3.2、函数参数

函数的参数分为实参和形参。

在调函数时,我们将调用其他函数的函数称为主调函数,将被调用的函数称为被调函数。在调用有参函数时,主调函数和被调函数之间有数据传递关系。主调函数中将值传递出去的参数称为“实际参数”(简称实参),被调函数中用于接收主调函数所传递过来值的参数称为“形式参数”或“虚拟参数”(简称形参)。

3.3、实参和形参之间的数据传递

在调用函数的过程中,系统会把实参的值传递给被调函数的形参,或者说形参从实参得到一个值。

函数调用过程中需要注意以下几点:

(1)实参可以是常量、变量或表达式。如:max(3,a+b);

(2)实参与形参的数据类型应相同或者赋值兼容,并且实参在实参列表中的位置与形参在形参列表中的位置必须对应。

(3)函数遇到return返回语句返回过后,不再继续执行return之后的语句了。

(4)形参在其所在函数的调用期间有效,可以参加此函数中的运算,但是不能用于其他函数中。

(5)函数的形参和实参是两个不同的变量,所以,一般情况下形参值的改变不影响实参的值,除非在函数参数的传递类型为引用传递(地址传递)。在未调用函数时,形参并不占用存储单元,开始函数调用时,才给形参开辟存储空间,函数调用结束后,形参的存储单元就会被释放。

3.4、函数的返回值

函数通过return语句带回返回值,应注意返回值类型应与函数类型一致,即函数类型决定返回值的类型。函数的返回值可以根据函数的功能拟定,并不固定。

4、函数的声明

问:把某自定义函数的定义放在最后,主函数里能够调用此函数吗?
答:不能,函数需要先定义后使用,除非在调用函数之前有此函数的声明。

函数声明语句的一般格式:
函数类型 函数名(参数类型1 参数名1,参数类型2 参数名2,……,参数类型n 参数名n);

函数声明其实就是将函数头部前置。

如:

1
2
3
4
5
6
7
8
9
10
11
12
//max函数的声明
int max(int x,int y); //函数头部

void main()
{
max(10,20); //函数调用在函数定义之前
}

int max(int x,int y); //求最大值函数,返回两个数之间的最大值
{
return x>y?x:y;
}

在一个函数中调用另一个函数(即被调函数)需要具备以下条件:

(1)首先被调用函数必须是已经定义好的函数(库函数或自定义的函数)。
(2)如果使用库函数,应该在本文件开头用#include指令将调用相关库函数时所需用到的信息“包含”到本文件中来。如:#include<stdio.h>。
(3)如果使用用户自定义的函数,在调用函数之前必须要有被调函数相关的声明语句,也就是函数需要先声明后使用。

5、局部变量和全局变量

按照变量作用域的不同,我们将变量分为局部变量和全局变量。

5.1、局部变量

局部变量的作用范围只在一定范围内有效

局部变量的定义可能有以下几种情况:

(1)在函数开头定义;

(2)在函数内部的复合语句中定义;

5.2、全局变量

全局变量的作用范围相对于局部变量来说更为广泛,其在函数外部定义,也称为外部变量

5.3、静态变量与动态变量

程序中所定义的变量默认是动态局部变量(auto)。

在定义变量前加一个static可定义一个静态变量。

静态变量在程序开始后定义,结束前才会被释放,所以其生命周期比较长,在函数调用中只会被定义一次,不会被定义多次,当再次执行到定义语句时,其值不会被重置(初始化),会保留上次改变的值。

6、函数的嵌套调用

函数的定义时相互平行、独立的,在定义函数时,一个函数内不能再定义另一个函数,也就是说,函数不能嵌套定义。但是函数可以嵌套调用,也就是再调用一个函数的过程中,又调用另一个函数。

思考:怎么实现求三个数中的最大值的函数呢?

如:

1
max(max(x,y),z);

7、函数的递归调用

在调用一个函数的过程中又出现直接或间接地调用此函数本身,称为函数的递归调用。

函数的递归调用演示示例:

1
2
3
4
5
6
//递归求1~n的和
int function(int n)
{
if(0>=n)return n; //结束递归的条件
return n+function(n-1); //这里在函数中调用此函数本身,实现递归
}

注意:递归的函数中应有结束递归的条件,否则会和死循环一样,陷入无限递归,或者说死递归。

8、数组作为函数参数传递

数组名为数组的首地址,所以整个数组作为函数参数传递实际上是引用传递,传递的是地址,形参的改变会影响到实参。

如:字符串的输出

1
2
3
4
5
6
7
8
9
10
void PUTS(char str[])
{
puts(str);
}

main
{
char S[20]="hello world!";
PUTS(S);
}

这里数组作为函数参数传递是数组的首地址,有涉及到指针的相关概念,之后讲到指针时再详细讲解。

注意:在函数中改变形参数组元素的值,那实参数组元素的值是否会被改变呢?

七、预处理

预处理是在编译前所做的工作,编译器自动调用预处理程序对源码中以’#’开头的预处理部分进行处理,处理完毕后,进入源码的编译阶段。

1、预定义符号:

常用的预定义符号:

1
2
3
4
5
6
7
8
9
_CRT_SECURE_NO_WARNINGS //安全检查

__FILE__ //当前编译的文件名.
__FUNCTION__//当前所在函数的函数名.
__DATE__ //当前编译日期.
__TIME__ //当前编译时间.
//注意标识符前后都有两个下划线'_',以上格式占位符都用%s ,如:printf("%s",__FILE__);

__LINE__ //当前行数,格式占位符用%d,如:printf("%d",LINE);

2、宏定义

宏定义,又称为宏替换,自定义一个宏(要符合标识符的命名规则),用于替换任意数据、标识符或者表达式。

2.1、无参宏定义

基本格式:#define 宏名 宏替换

比如:

1
2
3
4
#define A 35   		//用A代表数据35
#define INT int //用INT代替int

INT a=A; //使用宏定义别名定义int类型的变量a,并初始化赋值为35

不能给宏定义的常量赋值

如:

1
A = 66; // 错误,不能给宏定义常量赋值

2.2、带参宏定义

基本格式:#define 宏名(参数表) 宏替换

带参宏可以像函数一样调用,比如:

1
2
3
4
#define M(a,b) a+b-2

K = M(1,2) * 4;
//K = 1+2-2*4 = 1+2-8 = -5

注意:宏定义是替换,其在替换完成前并不会计算。

宏名尽量用大写,使其在程序中容易辨别区分

2.3、常量的定义:

除了宏定义以外,还可以通过const关键字定义常量:

定义常量的基本格式:

1
<cosnt> <数据类型> <常量名> = <常量值>;

如:

1
const int a = 30; //定义一个常量a,其值等于30

定义成常量后,值不可被改变。

如:

1
a = 40; //错误,不能给常量赋值

3、文件包含

3.1、包含头文件

我们想要用库函数就需要包含头文件,也就是文件包含,当然也可以编写自定义头文件,包含自己编写的头文件。

1
2
3
4
5
#include <stdio.h>
//包含系统头文件用<>,只会在系统头文件中找

#include "name.h"
//包含自定义头文件用"",在自定义头文件中找不到就会在系统头文件中找

文件包含允许嵌套,即在一个被包含文件中可以包含其它文件。

3.2、头文件的重复包含

头文件的嵌套包含可能会引起头文件的重复包含,从而出现函数和变量的重定义问题,所以需要避免头文件重复包含,某些宏定义语句可以防止头文件重复包含,如:

1
#pragma once 	//不让文件被包含两次,在头文件最前面添加

此预处理语句是vs独有的,有使用平台的限制,其他平台可能不存在。

4、条件编译

所谓的条件编译就是根据不同的条件编译不同的代码段。

4.1、#if……#else的使用

1
2
3
4
5
6
#if 表达式
//判断表达式的逻辑值(真或假),若逻辑值为真,则编译代码段1,否则编译代码段2
代码段1;
#else
代码段2;
#endif

4.2、#ifdef……#endif的使用

1
2
3
4
#ifdef 宏名
//如果定义了宏"宏名",则编译代码段
代码段;
#endif
1
2
3
4
5
6
#ifdef 宏名
//如果定义了宏"宏名",则编译代码段1,否则编译代码段2;
代码段1;
#else
代码段2;
#endif

4.3、#ifndef……#endif的使用

1
2
3
4
#ifndef 宏名
//如果没有定义宏"宏名",则编译代码段
代码段;
#endif
1
2
3
4
5
6
#ifndef 宏名
//如果没有定义宏"宏名",则编译代码段1,否则编译代码段2;
代码段1;
#else
代码段2;
#endif

八、构造数据类型

1、结构体

构造数据类型:用户自己建立的数据类型(自定义数据类型)。

C语言中的构造数据类型有:数组类型、结构体类型和共用体类型。

1.1、什么是结构体?

C语言允许用户根据需要自己建立的由不同类型数据组成的组合型的数据类型,我们把它称之为结构体(struct)。

1.2、为什么要用结构体?

在日常生活中有许多事物用单一的数据类型可能没办法完全表示出来,例如:学校要存储学生的学号、姓名、性别、年龄、成绩和家庭地址等信息,这些信息需要用不同的数据类型来存储,显然用我们一个普通的单一的数据类型是无法全部存储起来的,就比如说数组,我们常用它来存储一串连续的信息,但是它的数据类型单一,显然无法把学生的这些信息全部保存。

所以,结构体这种数据类型就诞生了,它能根据用户需要来更方便的存储各种各样的信息。

1.3、结构体类型的声明和结构体变量的定义

(1)声明一个结构体类型的一般形式为:

​ struct 结构体类型名{成员列表};

(2)定义一个结构体类型的变量:

可以在声明的时候直接定义结构体变量,也可以先声明后定义结构体变量。

例如:学生类结构体

1
2
3
4
5
6
7
8
struct student{
int id; //学生学号
char name[10]; //学生姓名
char sex[4]; //学生性别
int age; //学生年龄
int score; //学生成绩
char address[20]; //学生家庭住址
}S1,S2,S3; //声明时定义结构体变量S1,S2,S3

这里定义了一个结构体,其中struct为结构体关键字,srtuct student为结构体类型名,id、name、sex、age、score、address为结构体成员名,S1、S2、S3为结构体变量名。

注意:一定要区分清楚什么是结构体类型名、结构体成员名和结构体变量名。

1
2
3
4
5
int main()
{
struct student S4,S5;//声明之后使用结构体类型名定义的结构体变量S4,S5
return 0;
}

(3)不指定结构体类型名而直接定义结构体类型的变量

其一般形式为:

1
2
3
4
struct //这里缺省了结构体类型名
{
成员列表;
}变量名列表;

如:

1
2
3
4
5
6
7
8
9
srtuct	//这里没有给定结构体类型名
{
int id;
char name[10];
char sex[4];
int age;
int score;
char address[20];
}t1,t2,t3; //由于没有给定类型名,所以变量只能声明时定义

注意:以此方式定义结构体,由于没有结构体类型名,只能在声明时定义此结构体的变量,而不能再以此结构体类型名去定义其他变量了。(这种方式用得不多)

1.4、结构体变量的初始化和引用

(1)结构体变量的初始化:

1
2
3
4
5
6
7
8
9
srtuct student{
int id;
char name[10];
char sex[4];
int age;
int score;
}s1={666,"小李","男",30,100},t2,t3;
//这里给srtuct student类型的结构体变量s1赋初值(初始化赋值)
//在定义结构体变量的同时给变量赋初值,应按次序给每一个成员或部分成员赋值

(2)结构体变量和成员的引用

相同结构体类型的结构体能够相互赋值:
如有定义:

1
struct student s1,s2;

​ 就可以有:

1
s1=s2;

不同结构体类型的结构体不能相互赋值:
如有定义:

1
2
struct student s;
struct teacher t;

​ 则不能有:

1
s=t;

​ 也不能有:

1
student = teacher;

结构体成员引用符:’.’

结构体成员引用的一般格式:结构体变量名.成员名

如:

1
s1.id=1;	s2.name;	s3.score;

1.5、使用typedef关键字自定义类型名

typedef:简单来说就是用一个新的标识符名代替原有的类型名。

如:

1
typedef int INT;	//这里用INT代替int,之后定义整型变量就可以用INT了 

typedef的使用方法与#define INT int 类似,相当于给数据类型关键字取别名。

但是需要注意的是:typedef只能用于给数据类型关键字取别名,除此之外没有其他用途。

使用typedef取的别名也需要遵循标识符的命名规则

可以给一个数据类型取多个别名(没有意义)

如:

1
typedef int INT,I,inter;

或者:

1
2
typedef int INT;
typedef int I;

typedef一般使用在数据类型名比较长的情况下,

如:声明结构体

1
2
3
4
5
6
7
8
typedef struct student{
int id; //学生学号
char name[10]; //学生姓名
char sex[4]; //学生性别
int age; //学生年龄
int score; //学生成绩
char address[20]; //学生家庭住址
}S; //这里的S是代表struct student这个数据类型的别名,而不是结构体的变量名了

我们可以用struct student定义此结构体类型的变量,如:

1
struct student s1,s2;

也可以用struct student的别名S等于此结构体类型的变量,如:

1
S s3,s4;

思考一下:结构体变量所占的内存大小怎么计算?

​ 一般来说结构体类型所占内存大小是所有成员大小之和,但是存在内存补齐。

1.6、结构体的嵌套定义

用一个结构体类型作为另一个结构体类型的成员。

1
2
3
4
5
6
7
8
struct test2
{
int y;
struct test1
{ //结构体的嵌套定义:一个结构体类型中定义了另一个结构体类型
int x;
}z;
}n;

通过结构体变量n引用成员变量x:**n.z.x=10; //给成员变量x赋值为10

1.7、结构体数组

(1)结构体数组的定义

例如:

1
struct student S[3];

这是一个struct student类型的结构体数组,此数组中有三个struct student结构体类型的元素,分别为S[0],S[1],S[2]。

(2)结构体数组元素的使用

使用结构体数组元素成员的一般格式:结构体数组名[数组下标].成员变量名;

例如:

1
2
3
4
S[0].id=1;
//这里引用了struct student类型的结构体数组S的第1个元素S[0]的成员id,使其值等于1
printf(“%s\n”,S[2].name);
//这里是输出struct student类型的结构体数组S的第3个元素S[2]的成员name的值

如有以下定义:

1
struct student s1[3],s2[3];

那么可以有:

1
s1[0]=s2[1];	//相同类型的结构体数组元素赋值

不能有:

1
s1=s2;	//错误,结构体数组之间不能相互赋值

思考一下:结构体数组所占的内存大小怎么计算?

2、共用体

2.1、什么是共用体?

共用体关键字:union

有时候想用同一段内存单元存放不同类型的变量。如:把一个整型变量、浮点型变量和字符型变量放在同一个内存单元中,它们在内存中所占字节数不同,但是共用同一段内存地址,也就是共用体了。

2.2、共用体类型的声明和共用体变量的定义

共用体类型声明和定义的一般格式为:

1
2
3
4
union 共用体名
{
成员列表;
}变量列表;

如:

1
2
3
4
5
6
7
8
9
union DATA
{
int a;
double b;
char c[10];
}d1={1},d2,d3; //可以对共用体变量进行初始化,但是初始化列表中只能有一个常量。
d1.a=10;
d1.b=6.6;
strcpy(d1.c,”abcdefg”); //这里分别给共用体成员赋值

#注意:由于共用体中的每个成员共用一段内存空间,所以共用体在同一时刻只能保存一个成员的值,也就是保存最后一个赋值的成员的值。

2.3、共用体类型所占内存

由于共用体类型中所有成员是共用一段内存的,所以整个共用体类型所占内存是最大成员所占的内存空间(也存在内存补齐)。

如:

1
2
3
4
5
6
7
union DATA
{
int a;
double b;
char c[10];
}d1,d2,d3;
printf(“%d\n”,sizeof(d1)); //这里输出共用体的内存大小为16(两个double的大小)

3、枚举类型

3.1、枚举类型的概念

如果一个变量只有几种可能的取值,就可以定义为枚举类型,所谓的“枚举”就是指吧可能的值一一列举出来,枚举变量的取值范围只限于列举出来的值的范围内,也就是只能在值的集合内选择。

枚举类型本质上是作为int类型数据做运算的,也可以将枚举类型看成取值范围受限的整型。

3.2、枚举类型的声明

枚举类型关键字:enum

枚举类型定义的基本格式:enum 类型名{枚举元素列表};

例如:

1
2
enum Weekday{sun,mon,tue,wed,thu,fri,sat};
//以上声明了一个枚举类型的变量Weekday,用于表示星期,其变量的取值范围为星期天至星期六

3.3、枚举变量的定义

根据以上声明我们可以定义此枚举类型的变量。

例如:

1
enum Weekday workday,weekend;

其中workday和weekend是此枚举类型的变量,其每个变量的取值范围为集合{sun,mon,tue,wed,thu,fri,sat}中的一个值。

例如:

1
2
3
4
workday=mon;
weekend=sun;

workday=abc; //错误,不存在枚举值abc

可以在声明时同时定义变量。

如:

1
enum Weekday{sun,mon,tue,wed,thu,fri,sat}workday,weekend;

声明的枚举类型也可以没有类型名,但如果需要使用此枚举类型的话,就必须在声明时同时定义变量。

如:

1
enum {sun,mon,tue,wed,thu,fri,sat}workday,weekend;

说明:

(1)在C语言中枚举类型的枚举元素是按照常量进行处理的,所以又称为枚举常量。不要因为它们是标识符而把它们当作变量来使用,在使用时是不能对它们赋值的。

例如:

1
sun=0;	mon=1;	sun = 7; //错误,不能给枚举常量赋值

(2)每一个枚举元素都代表一个整数,C语言编译器按照定义时的顺序默认它们的值为0,1,2,3,4,5,6……。在上面的声明中,sun的值为0,mon的值为1,tue的值为2,依次类推。

如果有赋值语句:

1
workday=mon;

就相当于:

1
workday=1;

枚举常量的引用和输出:

如:

1
printf(“%d”,sun);//输出整数0

枚举变量的引用和输出:

如:

1
2
workday=mon;
printf(“%d”,workday);//输出整数1

(3)我们也可以人为的指定枚举元素的数值,在声明的枚举类型的时候显式指定。

例如:

1
enum Weekday{sun=7,mon=1,tue,wed,thu,fri,sat}workday,weekend;

指定枚举常量sun的值为7,mon的值为1,之后的值顺序加1,也就是tue的值为2,sat的值为6。

由于枚举类型的值为整数,因此C99把枚举类型也作为整型数据的一种,即用户自己定义的整数类型。

(4)枚举类型可以用来比较和判断。

例如:

1
2
if(workday==mon){……}
if(workday<=sat){……}

枚举类型的比较规则是按照其初始化时指定的整数大小来进行比较的。如果声明时没有人为指定,就按默认规则处理,即第一个枚举元素的值为0,第二个枚举元素的值为1,依次类推,所以mon>sun,sat>fri。

3.4、枚举类型应用举例

猜拳游戏:玩家输入1、2、3进行猜拳。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

enum InputType{ 石头, 剪刀, 布 }; //猜拳枚举类型

void FingerGuessing() //猜拳游戏
{
InputType computer, player; //电脑和玩家
srand((unsigned)time(NULL)); //随机种子

while (1)
{
computer = (InputType)(rand() % 3); //电脑随机猜拳
printf("\n猜拳游戏\n0、石头\n1、剪刀\n2、布\n请猜拳:");
scanf("%d", &player);
switch (player) //比较玩家猜的拳
{
case 石头:
printf("玩家:石头\n");
switch (computer) //比较电脑猜的拳
{
case 石头:
printf("电脑:石头\n");
printf("平局!\n");
break;
case 剪刀:
printf("电脑:剪刀\n");
printf("玩家赢了!\n");
break;
case 布:
printf("电脑:布\n");
printf("玩家输了!\n");
break;
}
break;
case 剪刀:
printf("玩家:剪刀\n");
switch (computer) //比较电脑猜的拳
{
case 石头:
printf("电脑:石头\n");
printf("玩家输了!\n");
break;
case 剪刀:
printf("电脑:剪刀\n");
printf("平局!\n");
break;
case 布:
printf("电脑:布\n");
printf("玩家赢了!\n");
break;
}
break;
case 布:
printf("玩家:布\n");
switch (computer) //比较电脑猜的拳
{
case 石头:
printf("电脑:石头\n");
printf("玩家赢了!\n");
break;
case 剪刀:
printf("电脑:剪刀\n");
printf("玩家输了!\n");
break;
case 布:
printf("电脑:布\n");
printf("平局!\n");
break;
}
break;
}
}
}

int main()
{
FingerGuessing();

getchar();
getchar();
return 0;
}

九、C语言文件操作

1、什么是文件?

文件有不同的类型,在程序设计中,主要用到两种文件:

(1)程序文件。包括源程序文件(后缀名为.c)、目标文件(后缀名为.obj)、可执行文件(后缀名为.exe)等。这一类型的文件主要用于存储程序代码。

(2)数据文件。此文件的内容不是程序,而是程序运行时读写的数据,比如程序运行过程中输出到磁盘或其他设备上的数据,或在程序运行过程中供程序读取的数据。

这里C语言的文件操作主要是对数据文件的操作。

在之前程序中所处理的数据的输入和输出都是以终端为对象的,都是从键盘输入数据,然后运行结果输出到终端显示器上。实际上,我们有时候需要将一些数据(程序运行的最终结果或者中间数据)保存起来,方便以后需要时再调用,而这就需要用到磁盘文件了。

1.1、文件的概念

每一个文件都需要一个唯一的文件标识,以便用户使用,就像我们的变量名一样,同一程序中不能有相同的变量名。

文件标识也称为文件名,它由3部分组成:

①文件路径:表示文件在外存设备中的存储位置;

②文件名主干:表示文件的名字,可由用户自定义,命名规则应遵循标识符的命名规则。

③文件后缀:表示文件的性质,也称为文件的格式,用于描述文件的类型(如txt、ppt等)。

文件路径能唯一标识文件在外存中的位置。

如:D:\C++\VSproject\TEXT\text.c

1.2、文件的分类

根据数据的组织形式,数据文件可分为ASCII文件和二进制文件。

数据在内存中是以二进制形式存储的,如果不加转换的输出到外存,就是二进制文件,可以认为它是存储在内存的数据的映像,所以称之为映像文件。如果要求在外存上以ASCII码形式存储,就需要在存储前进行转换。ASCII文件又称为文本文件,每一个字节存放一个字符的ACSII码。

1.3、文件存储方法的区别

一个数据在磁盘上存储,字符一律以ASCII形式存储,数值型既可以用ASCII形式存储也可以用二进制形式存储。如整数10000,用ASCII码形式存储在磁盘上占5个字节(每个字符占一个字节),而用二进制形式存储在磁盘上只占4个字节(00000000 00000000 00100111 00010000‬)。用ASCII形式存储时字符与字节一一对应,一个字节代表一个字符,便于逐个处理,但占的存储空间较多,而且处理的时候要花费转换时间(二进制与ASCII码之间的转换)。二进制形式存储就相当于直接把内存中的内容原封不动存储在磁盘上,由于不需要转换,所以二进制文件更加方便计算机处理。

2、指向文件的指针

C语言要想操作内存,需要用到各种数据类型的指针,而文件也是需要调用到内存中才能够使用,所以就需要用到“指向文件的指针”,就是“文件类型的指针”,简称“文件指针”。

每一个被使用的文件都在内存中开辟一个相应的文件信息区,用来存放文件的相关信息(如文件的名字、文件的状态和文件的位置等)。这些信息是保存在一个结构体变量中的,此结构体类型是由系统声明的,取名为FILE,其被包含在stdio.h头文件中。

2.1、文件指针的定义

由于文件类型已经在stdio.h头文件中有声明了,所以我们不需要另外声明,直接使用就行了。

文件指针的定义格式为:FILE *指针类型名;

如:

1
FILE *fp;

定义一个指针fp用于指向FILE类型的数据,可以使fp指向某一个文件在内存中的文件信息区(结构体变量),通过此文件信息区能够访问此文件。也就是说通过文件指针变量能够找到并可以操作其指向的文件。

2.2、打开与关闭文件

(1)用fopen函数打开文件

​ C语言规定用文件标准输入输出函数fopen来实现打开文件。

fopen函数的调用方式为:

​ fopen(文件名,使用文件的方式);

例如:

1
2
fopen("text1.txt","r"):
//以只读的方式打开名为“text1.txt”文件。

此时fopen函数返回的是“text1.txt”文件的起始地址,我们通常将fopen函数的返回值赋值给一个文件指针,用文件指针指向此文件的地址。

例如:

1
2
FILE *fp;//定义一个文件指针fp
fp=fopen("text1.txt","r");//使fp指向文件“text1.txt”的首地址

这样fp就与文件“text1.txt”有联系了。在打开一个文件时,给定编译系统以下3个信息:

①需要打开的文件名称;②文件的打开方式;③使用哪个文件指针指向被打开的文件。

(2)文件的打开方式

文件打开方式与含义如下表所示:

文件的打开方式 含义 如果指定的文件不存在
“r”(只读) 为了输入数据,打开一个已存在的文本文件 出错
“w”(只写) 为了输出数据,打开一个文本文件 新建文件
“a”(追加) 向文本文件尾部添加数据 出错
“rb”(只读) 为了输入数据,打开一个已存在的二进制文件 出错
“wb”(只写) 为了输出数据,打开一个二进制文件 新建文件
“ab”(追加) 向二进制文件尾部添加数据 出错
“r+”(读写) 为了读和写,打开一个文本文件 出错
“w+”(读写) 为了读和写,打开一个文本文件 新建文件
“a+”(读写) 为了读和写,打开一个文本文件 出错
“rb+”(读写) 为了读和写,打开一个二进制文件 出错
“wb+”(读写) 为了读和写,打开一个二进制文件 新建文件
“ab+”(读写) 为了读和写,打开一个二进制文件 出错

(3)用fclose函数关闭文件

在使用完一个文件之后,为了防止它被误用,应该关闭它。“关闭”就是撤销文件信息区和文件缓冲区,使指针不再指向此文件了,也无法操作此文件了,除非重新打开此文件,使指针指向此文件。

关闭文件用fclose函数。

fclose函数的基本格式为:

​ fclose(文件指针);

例如:

1
fclose(fp);

在每次程序终止之前都要养成习惯关闭所有的文件,当fclose函数成功关闭文件时,返回0;否则返回EOF(-1)。

3、顺序读写文件

文件打开完成之后就可以对它进行读写操作了。

常用的文件操作函数如下所示:

3.1、字符输入和输出函数

(1)字符读取函数
使用字符读取函数fgetc从文件读取一个字符

如:

1
ch=fgetc(fp);

从文件指针fp指向的位置读取一个字符存入字符变量ch中,

读取成功返回所读的字符,失败则返回为你文件结束标志EOF(-1)。

(2)字符写入函数
使用字符写入函数fputc向文件写入一个字符。

如:

1
fputc(ch,fp);

向文件指针fp指向的位置写入字符ch,写入成功返回输出的字符,失败则返回EOF(-1)

3.2、字符串输入和输出函数

(1)字符串读取函数
使用字符串读取函数fgets从文件读取一个字符串。

如:

1
fgets(str,n,fp);

从文件指针fp指向的位置读取一个长度位n-1的字符串(最后一位赋值‘\0’,用作字符串结束标志),存放在字符数组str中。

读取成功返回地址str,失败则返回NULL。

(2)字符串写入函数
使用字符串写入函数fputs向文件写入一个字符串。

如:

1
fputs(str,fp);

把str所指向的字符串写入文件指针fp指向的位置,写入成功返回0,否则返回非0值。

3.3、文件格式化输入和输出函数

(1)格式化输出函数
使用格式化输出函数fprintf向文件写入数据:

​ fprintf(文件指针,”格式化字符串”,输出列表);

例如:

1
fprintf(fp,"%d,%c",x,y);

将变量x以整型的形式写入文件指针fp指向的位置,把变量y以字符的形式写入文件指针fp指向的位置。

(2)格式化输入函数
使用格式化输入函数fscanf从文件读取数据:

​ fscanf(文件指针,”格式化字符串”,输入列表);

例如:

1
fscanf(fp,"%d,%f",&x,&y);

从文件指针fp指向的位置读入一个整型数据和一个单精度型数据,分别存入变量x和变量y中。

3.4、以二进制的形式读写数据

(1)二进制读取函数

使用fread函数以二进制的形式从文件读出数据:

1
fread(arr,size,count,fp);

把数组arr中size个count大小的数据放入文件指针fp所指向的文件中。

arr是一个地址(数组),用于存储从文件读取出来的数据,size为需要读取的字节数,count为需要读取数据项的个数(每个数据项的大小为size)。

(2)二进制写入函数

使用fwrite函数以二进制的形式向文件写入数据:

1
fwrite(arr,size,count,fp);

从文件指针fp所指向的文件中读取size个count大小的数据放入数组arr中。

4、随机读写文件

可以通过改变文件指针的位置标记及定位来实现文件的随机读写。

4.1、强制使文件指针指向文件开头

使用rewind函数强制使文件指针fp指向文件开头的位置。

如:

1
rewind(fp);

4.2、使文件指针指向文件中的任意位置

使用fseek函数使文件指针指向文件中任意位置。

基本格式:
fseek(fp,位移量,起始点);

起始点用0、1、2代替,0代表文件开始位置,1代表当前位置,2代表文件末尾位置。
位移量是指以起始点为基础,向前移动的字节数,其为long类型的参数。

例如:

1
2
3
fseek(fp,100,0);	//将文件指针fp向后移动到离文件开头100个字节处
fseek(fp,50,1); //将文件指针fp向后移动到离当前位置50个字节处
fseek(fp,-10,2); //将文件指针fp向前移动到离文件末尾10个字节处

fseek一般用于二进制文件。

用rewind和fseek函数实现随机读写。

5、文件的出错检测

5.1、文件读写出错检测

ferror函数用于检测文件读写出错,如果文件读写正常返回0,出错则返回非零值。

基本格式:

​ ferror(fp);

如:

1
2
3
4
if(ferror(fp))
{
printf(“文件读写失败!”);
}

5.2、文件末尾判断

feof函数用于检测文件指针是否读到了文件末尾,如果文件指针读到了文件末尾则返回非零值,否则返回0。

基本格式:

​ feof(fp);

如:

1
2
3
4
if(feof(fp))
{
printf(“文件读写完毕!”);
}

5.3、文件错误标志

clearerr函数的作用是使文件错误标志和文件结束标志置为0。如果在文件读写出错后,ferror函数值为一个非零值,应该立即调用clearerr(fp),使ferror(fp)的值变为0,以便进行下次检测。

基本格式:

​ clearerr(fp);

注意:只要出现文件读写错误标志,它就会一直保留,直到调用clearerr函数或rewind函数,或其他任何一个输入输出函数。

十、C语言的灵魂——指针

1、什么是指针?

在了解指针之前先要弄清楚地址的概念。

如果在程序中定义了一个变量,在对程序进行编译时,系统就会给这个变量分配内存单元。编译系统根据城西中定义的变量类型,分配一定长度的空间。例如:整型变量分配4个字节,字符型分配1个字节,单精度分配4个字节等。内存区的每一个字节有一个编号,这就是“地址编号”,它就相当于旅馆中的房间号,每一个房间都可以看作一块内存区域,都可以用来存放东西,我们给每个房间都编一个房间门牌号,用于更好的区分每一个房间,内存中也是一样的,整个内存由很多个字节组成,每个字节都有其对应的“房间号”,这就是“地址”了。通过这个“房间号”就可以找到其对应的“房间”,然后就可以从房间里取东西,或者把东西放进房间里了。

理解了地址的概念之后,那所谓的指针,就是内存地址,也就是地址的编号,可以把“指针指向地址”理解成“用小本本把房间号记下来”,那这个小本本就相当于一个用于记房间号的指针了,一个变量的地址称为此变量的“指针”。

2、指针常量与指针变量

2.1、指针常量

之前有了解过不同数据类型的变量所占内存字节数的这个概念,那么系统在编译时给一个变量分配的内存地址就称为此变量的“指针”,这个指针的指向是无法改变的,所以又称为指针常量,数组的地址也是指针常量(也称为地址常量)。

2.2、指针变量

(1)指针变量的概念

如果有一个变量专门用来存放另一个变量的地址,则称这个变量为“指针变量”,也就是说C语言中有一类变量是专门用来存储(指向)地址的,我们将它称为“指针变量”,指针变量的中存储的地址可以被改变,也就是可以改变指针变量的指向,就好比一张纸或一个小本本,写着一个房间的房间号,那把这个房间的房间号擦掉,写上另一个房间的房间号也是可以的,这就是指针变量和指针常量最大的区别所在了,可以改变指针变量的指向。

(2)指针变量的定义

定义指针变量的一般格式:

类型名 *指针变量名;

例如:

1
2
3
int *p,*q;
char *p1,*q1;
double *p2,*q2;

注意:左端的int、char等是在定义指针变量时必须指定的“基类型”。指针变量的基类型用来规定此指针变量可以指向的变量的类型。如:上面定义的p和q只能用于指向int整型变量的地址,p2和q2只能用于指向double双精度类型变量的地址。

(3)指针变量的引用

与指针和地址相关运算符:’*’(指针运算符)和’&’(取地址运算符)这里的取地址运算符要区别对待位运算符&

例如:

1
int a,*p;	p=&a;	*p=10;

在引用指针变量时,有以下几种情况:

①给指针变量赋值

如:

1
2
3
int a=10,b=20;
int *p=&a;
//定义一个整型指针变量p,初始化p的值为a的地址,也就是p指向a地址

②解引用
解引用就是通过指针使用其所指向地址中存储的数据。

1
2
3
4
5
6
7
*p=30;	//通过指针变量p引用a变量,改变a的值为30
//这里的’*’为解引用符号,p引用指针变量p所指向地址中对应的值
scanf(“%d”,p); //scanf通过指针变量p给变量a赋值
printf(“%d\n”,*p); //通过指针变量p解引用输出变量a的值
*p=b; //将b的值放入指针变量p所指向的内存地址中(a的地址单元中)
p=&b; //改变指针p的指向,指针p不再指向a的地址了,而是指向b的地址
printf(“%d\n”,*p); //输出变量b的值

③输出内存地址编号

1
2
3
4
printf(“%p\n”,p);	//以十六进制的格式输出指针变量p所指向地址的内存地址编号
printf(“%d\n”,&a); //以十进制的格式输出变量a所在的内存地址编号
printf(“%o\n”,&b); //以八进制的格式输出变量b所在的内存地址编号
printf(“%p\n”,&p); //以十六进制的格式输出指针变量p所在的内存地址编号

3、指针变量作为函数参数

函数的参数不仅可以是整型、浮点型、字符型的数据,还可以是指针类型。它的作用是将一个变量的地址传递到另一个函数中。

3.1、函数参数为指针类型的函数

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void fun1(int x,int y)
{//这里定义了一个普通函数fun1
printf(“x=%d\ty=%d\n”,x++,y++);
}
void fun2(int *x,int *y)
{//这里定义了一个形参为整型指针类型函数fun2,其形参为指针类型的变量
printf(“x=%d\ty=%d\n”,*x++,*y++);
//注意:和printf(“x=%d\ty=%d\n”,x++,y++);的区别,也就是没有和有的区别
}
int main()
{
int a=10,b=20,*p,*q;
fun(a,b); //调用普通函数fun1
printf(“a=%d\tb=%d\n”,a,b);
fun2(&a,&b); //这里调用函数fun时,所传递的实参必须是地址
printf(“a=%d\tb=%d\n”,a,b);
p=&a; //使用整型指针变量p指向整型变量a的地址
q=&b; //使用整型指针变量q指向整型变量b的地址
fun(p,q); //这里使用指针变量p和q作为实参传递
printf(“p=%d\tq=%d\n”,*p,*q);
return 0;
}

3.2、指针函数

函数返回值为指针类型的函数称为“指针函数”,通常用来返回一个地址。

如:

1
2
3
4
5
6
int *fun3(int *x,int y)//这是一个指针函数,返回值类型为整型int指针类型
{
*x+=y;
printf(“%d”,++*x);
return x; //返回指针变量x所指向的内存地址
}

4、通过指针引用数组

4.1、数组元素的地址

数组元素的地址表示:

如:

1
2
int a[10] = {0,1,2,3,4,5,6,7,8,9},*p;
&a[0]; //引用数组元素a[0]地址的表示方法

4.2、指针指向数组元素

1
p=&a[1];	//指针变量p指向数组元素a[1]的地址

4.3、指针指向的移动(指针的偏移)

指针的偏移:指针每次会以其基类型所占字节数为单位进行偏移。

1
2
3
4
5
6
p=&a[0];
++*p; //指针指向地址中的数值加1
printf(“%#p\n”,p); //打印指针变量p所指向的地址编号
p++; //指针移动到数组元素a[1]的位置
printf(“%#X\n”,p); //打印移动后指针变量p所指向的地址编号
//指针变量++(或--)移动一次是移动其基类型大小的内存区域

使用指针的偏移输入/输出数组

1
2
3
4
5
6
7
8
for(int i=0;i<10;i++)
{
printf(“%d”,*(p+i)); //通过指针移动引用数组元素,输出数组
}
for(int i=0;i<10;i++)
{
printf(“%d”,p[i])); //通过指针带下标的形式引用数组元素
}

4.4、指针指向字符串

指针可以用于直接引用字符串常量。
如:

1
2
3
4
5
char *p = “abcdefg”;
while (*p)
{
printf("%c\t", *p++);
}

注意:可能VS某些版本需要在指针定义之前加一个const定义指针为常量指针

5、指向函数的指针(函数指针)

之前有学到过函数参数是指针类型的函数以及指针函数,熟悉了一些基本的指针与函数的应用,了解了变量的地址与指针变量。

思考一下:那既然变量有地址,数组也有地址,那么函数会有对应的地址么?

5.1、什么是函数指针?

首先,函数是会占内存空间的。在程序中定义了一个函数,在编译时,编译系统会为函数代码分配一段存储空间,这段存储空间就是函数的地址,这段地址的起始地址(又称入口地址)就称为这个函数的指针(或函数的首地址)。

既然函数也有地址,那么我们能不能用一个指针指向函数的地址呢?

既然都是地址,那么就可以用指针指向它。

指向整型变量地址的指针是整型指针,指向字符型变量地址的指针是字符型指针,指向单精度变量地址的指针是float型指针,那指向函数的指针是什么指针呢?

这就是接下来要接触到的函数指针了。

5.2、函数指针的定义

简单来说,函数指针就是指向函数的指针。

定义函数指针的一般格式:*数据类型 (函数指针名)(函数参数列表);

如:

1
2
int (*funp)(int,char);
//定义了一个指针函数,用于指向返回值类型为int型、函数参数为(int,char)型的函数

5.3、函数指针的初始化及使用

函数指针可以用于指向与其类型匹配的函数。
如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int funsum(int x,int y)
{ //求和函数
return x+y;
}
int funmax(int x,int y)
{ //求最大值函数
return x>y?x:y;
}
int funmin(int x,int y)
{ //求最小值函数
return x<y?x:y;
}
int main()
{
int (*funp)(int,int)=funsum; //定义一个函数指针funp,初始化赋值指向函数funsum
int a=10,b=20,c; //函数指针有以下两个赋值方式和两种调用方式
c=(*funp)(a,b); //通过函数指针funp调用函数funsum,(*)区分funp是个函数指针
funp=&funmax; //改变函数指针funp的指向,&取函数地址,使其指向函数funmax
c=funp(a,b); //此时是通过函数指针funp调用函数funmax
funp=funmin; //改变函数指针funp的指向,可以省略&取址符,使其指向函数funmin
c=funp(a,b); //通过函数指针funp调用函数funmin
return 0;
}

5.4、使用函数指针作为函数参数(回调函数)

函数指针的一个重要的用途是把函数的地址作为参数传递到其他函数。

回调函数:通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就称这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。

如:

1
2
3
4
5
6
int function(int x,int y,int (fun)(int,int))
{//这里的函数形式参数fun为函数指针类型
return (*fun)(x,y); //可以通过函数fun调用其所指向的函数
}
function(1,2,sum); //调用时直接以函数名作为函数参数
function(10,20,max);

5.5、使用typedef给函数指针取别名

1
2
3
4
typedef (*funp)(int,int);		//给 (*)(int,int)类型的函数指针取一个别名为funp
funp p1; //用别名funp定义()(int,int)类型的函数指针p1*
p1 = sum; //函数指针p1指向函数sum
p1(1,2); //通过函数指针p1调用函数

5.6、指针函数和函数指针的区别

所谓的指针函数,其本质上是个函数,是返回值为指针类型的函数

所谓的函数指针,其本质上是个指针,是指向函数的指针

6、指针数组和数组指针

6.1、指针数组

(1)什么指针数组

所谓指针数组,其本质上是一个数组,数组中的每一个元素都是指针类型的,都可以指向对应数据类型的地址。

(2)指针数组的定义

定义的一般格式:数据类型 *指针数组名[数组元素个数];

如:

1
int *p[6];	//定义一个指针数组,有6个元素,分别可以指向六个地址

(3)指针数组的使用

使用指针数组指向二维字符数组

1
2
3
4
5
6
7
8
9
10
11
12
13
char arr[10][10],*p[10];
for(int i=0;i<10;i++)
{
p[i]=arr[i]; //指针数组p中的每个元素指向二维数组arr的每一行
}
for(int i=0;i<10;i++)
{
scanf(“%s”,p[i]); //使用指针数组p给二维数组arr赋值
}
for(int i=0;i<10;i++)
{
printf(“%s”,p[i]); //使用指针数组p输出二维数组arr
}

使用指针数组存储字符串的形式代替二维字符数组,达到省空间的目的

1
char p[6]={“abc”,”123456”,”abcdefg”,”hello world!”,”xiaowei”,”x”};

6.2、数组指针

(1)什么是数组指针

所谓的数组指针,其本质上是一个指针,是一个用于指向数组地址的指针。

(2)数组指针的定义

定义的一般格式:数据类型 (指针变量名)[所指向数组的大小];*

如:

1
2
3
int a[3][4];
int (*p)[4]; //定义一个整型数组指针p,用于指向大小为4的整型数组
p=a; //将整型数组指针p指向二维数组a的第一行

(3)数组指针的使用

使用数组指针完成二维数组的输入和输出

1
2
3
4
5
6
7
8
9
10
11
12
13
int a3;
int (*p)[6];
p = a;
for (int i = 0; i < 3; i++)
for (int j = 0; j < 6;j++)
{
scanf("%d",&p[i][j]);
}
for (int i = 0; i < 3; i++)
for (int j = 0; j < 6;j++)
{
printf("%d\t",p[i][j]);
}

(4)数组指针的移动

1
2
3
p++;	//移动一整行
*(*(p+i)+j) //可以把数组指针理解成为一个二级指针,通过两次解引用得到元素值
p[i][j]; //指针带数组下标的形式访问数组元素

7、指针常量与常量指针

7.1、指针常量

指针常量就是指针的指向不能够被改变的指针,也就是指针类型的常量,指针中存储的地址不可被改变。

如:

1
int * const p;

它的指向不能被改变,但是能够改变值。

示例:

1
2
3
4
5
int a = 10,b=20; 
int * const q = &a; //必须初始化指针指向
*q = 30; //可以改变值
//q = &b; //错误:不能改变指针的指向
printf("%d\n",*q);

7.2、常量指针

常量指针就是指向常量的指针,也就是指针指向的地址内的值不可被改变,其又被称为只读指针。

如:

1
const int *p;

它的能够改变指向,但是不能够改变地址内的值

示例:

1
2
3
4
5
6
7
int a = 10,b=20; 
const int *p;
p = &a;
printf("%d\n",*p);
//*p=30; //错误:不能通过指针改变变量的值
p = &b; //可以改变指针的指向
printf("%d\n",*p);

指针常量和常量指针一般用于函数参数的传递,为了使在函数使用中不改变值以及指针的指向。

如:指向常量的指针常量

1
2
3
4
void fun(const int * const p) //指向常量的指针常量 
{
//指针p是一个只读指针,既不能改变指针的指向,也不能改变其指向地址里的值,在函数中防止被篡改
}

8、动态内存分配

8.1、什么是动态内存分配

动态内存分配就是使用户可以根据自己的需要,向系统申请所需大小的内存空间;由于没有声明部分来定义它们是为变量的地址还是为数组的地址,所有只能通过指针来引用它们。

动态内存分配的内存空间可以像普通的变量或数组一样使用,也支持存入和取出数据。

8.2、怎样建立内存的动态分配

①使用malloc函数动态申请内存

基本格式:

1
malloc(int size);

malloc函数用于动态申请一个大小为size的内存区域,并返回这块区域的首地址。

例如:

1
2
char *s = (char *)malloc(100)
//动态开辟一块大小为100个字节的内存区域,并使指针s指向这块区域的首地址

size可以直接写大小,但是一般有sizeof计算

②使用calloc函数动态申请数组内存

基本格式:

1
calloc(unsigned n,int size);

calloc函数用于分配n个大小为size的连续内存区域,可以开辟一个一维数组大小的动态内存空间,n为数组元素个数,每个数组元素的大小为size。

例如:

1
2
int *p = (int *)calloc(10,sizeof(int));
//动态申请10个int类型大小的连续空间,并使指针p指向那块空间的首地址

③使用realloc函数扩大/缩小动态内存

基本格式:

1
realloc(void *p,unsigned int size);

realloc函数用于重新分配已通过malloc函数或calloc函数开辟的内存空间,可以改变其内存空间的大小。

例如:

1
2
realloc(p,sizeof(int)*5);
//将指针p所指向的动态内存区域调整为5个整型大小

④使用free函数释放动态内存空间

基本格式:

1
void free(void *p);

free函数用于释放指针所指向的动态内存空间。

例如:

1
free(p);	//释放掉p指针所指向的动态内存

用于释放指针变量p所指向的动态内存空间,使得这部分空间能被其他变量使用,否则这段内存空间需要等到程序结束后才会被释放。

每次使用完动态内存空间的时候记得释放内存空间。

开辟的内存空间大小size一般由sizeof(数据类型);来进行计算

(注意:以上函数的声明在stdlib.h头文件中,使用这些函数之前需要包含stdlib.h头文件)

9、结构体指针

9.1、指向结构体变量的指针

所谓的结构体指针就是指向结构体变量的指针,一个结构体变量的起始地址就是这个结构体变量的指针。如果把一个结构体变量的起始地址存放在一个指针变量中,那么这个指针变量就指向此结构体变量。

9.2、结构体指针的定义

如有以下结构体:

1
2
3
4
5
6
7
struct student
{
int id;
char name[20];
char sex[4];
float score;
}s1,s2,s3;

则可以定义指向struct student类型结构体的指针:

1
2
struct student *sp;
sp = &s1;//用struct student类型的结构体指针sp指向struct student类型的结构体变量s1

9.3、通过结构体指针引用结构体成员

指向结构体成员运算符:’->’

通过结构体指针引用结构体成员的基本格式:结构体指针名->结构体成员名

如:

1
2
sp->id=100;	//通过结构体指针引用结构体成员用指向结构体成员运算符’->’
printf(“%s”,sp->name);

10、多重指针(多级指针)

10.1、什么是多重指针

之前有了解过变量、函数等都有其对应的地址,都可以由其对应数据类型的指针变量指向这个地址。那么指针变量也有地址么?

指针变量也是有其对应地址的,那么既然有地址,就可以用另一个指针变量指向它的地址,也就是指向指针变量地址的指针,简称指向指针的指针(双重指针/二级指针)。而指向指针的指针也是有地址的,那又可以有指向其地址的指针,这就是多重指针了。

10.2、多重指针的定义

定义双重指针(二级指针)基本格式:数据类型 **指针变量名;

定义三重指针(三级指针)基本格式:数据类型 ***指针变量名;

依次类推:四级指针、五级指针……

10.3、多重指针的使用

如有以下定义:

1
int a = 10,*p,**q,***r;	//定义整型变量a、指针p、双重指针q、三重指针r

就可以有以下赋值语句:

1
2
3
4
5
6
p=&a;	//使一级指针p指向变量a的地址
q=&p; //使双重指针q指向一级指针p的地址
r=&q; //使三重指针r指向双重指针q的地址
*p=20; //使用一级指针p给变量a赋值
**q=30; //使用二级指针q给变量a赋值
***r=40; //使用三级指针r给变量a赋值

10.4、双重指针作为函数形参

一般来说函数的形参无法改变实参,除非形参是指针类型的。那么如果实参是一个指针,想要在一个函数中改变一个指针的指向应该怎么做?

例如:若定义了以下函数fun,如果指针p是该函数的形参,要求通过p把动态分配存储单元的首地址传回主调函数,则形参p应当怎样正确定义?

1
2
3
4
5
6
7
8
9
10
void fun(……)
{
*p=(int )malloc(10*sizeof(int));
}

void main()
{
int *p;
fun(p);
}

这里形参p的类型应该定义为整型双重指针类型:

1
int **p;

双重指针可用于在函数中改变一级指针的指向。

11、内存四区

在系统为程序开辟内存时,将内存区域划分为4部分,分别为:

栈区:存放函数的形参、局部变量等。由编译器自动分配和释放,当函数执行完毕时自动释放。

堆区:用于动态内存的申请与释放,一般由程序员手动分配和释放,若程序员不释放,则程序结束时由操作系统回收。

全局静态常量区(全局区):存放常量(一般是字符串常量和其他常量)、全局变量和静态变量,在程序结束后由操作系统释放。

代码区:存放可执行的代码,一般为CPU 执行的机器指令。

十一、排序算法

1、排序的基本概念

1.1、什么是排序?

排序是指把一组数据以某种关系(递增或递减)按顺序排列起来的一种算法。

例如:数列 8、3、5、6、2、9、1、0、4、7

递增排序后 0、1、2、3、4、5、6、7、8、9

递减排序后 9、8、7、6、5、4、3、2、1、0

1.2、排序的稳定性

如果在一组需要排序的数据序列中,数据ki和kj的值相同,即ki= =kj,且在排序前ki在序列中的位置领先于kj,那么当排序后,如果ki和kj的相对前后次序保持不变,即ki仍然领先于kj,则称此类排序算法是稳定的。如果ki和kj的相对前后次序变了,即kj领先于ki了,则称此类排序算法是不稳定的。

1.3、排序的分类

●内部排序:指待排序数据全部存放在内存中进行排序的过程。

●外部排序:指待排序数据的数量很大,内存无法全部容纳所有数据,在排序过程中需要对外存进行访问的排序过程。

1.4、排序的过程

排序的过程中需要进行如下两种基本操作:

(1)比较两个数据的大小;

(2)移动两个数据的位置。

1.5、排序算法

排序算法按照其实现的思想和方法的不同,可以分为许多种。

我们比较常用的排序算法有:冒泡排序、 插入排序、选择排序、希尔排序(缩小增量排序)、快速排序、堆排序、归并排序。

排序算法的分类:

交换类排序:冒泡排序、快速排序
插入类排序: 直接插入排序、希尔排序(缩小增量排序)
选择类排序:简单选择排序、堆排序
归并排序
基数排序

2、冒泡排序

冒泡排序的规则:n个数据进行冒泡排序,首先将第一个数据和第二个数据进行比较,如果为逆序就交换两个数据的值,然后比较第二个和第三个数据,依此类推,直到第最后一个和倒数第二个比较完了为止。上述过程为冒泡排序的第一趟冒泡排序,其结果是最大或者最小的数据被放置在末尾的位置。然后进行第二趟排序,把第二大或者第二小的数放置在倒数第二的位置,之后每一趟排序都会使一个数据有序,直到此序列的全部数据都有序为止。

冒泡排序的演示示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void BubbleSort(int x[],int n)//冒泡排序 
{
int i, j;
for (i = 0;i<n-1;i++)
for (j = 0; j <n-1-i; j++)
{
if (x[j] > x[j + 1])
{
x[j] += x[j + 1];
x[j + 1] = x[j] - x[j + 1];
x[j] = x[j] - x[j + 1];
}
}
for (i=0;i<n;i++)
printf("%d\t",x[i]);
}

3、简单选择排序

对一个序列进行选择排序,首先通过一轮循环比较,从n个数据中找出最大或者最小的那个数据的位置,然后按照递增或者递减的顺序,将此数据与第一个或最后一个数据进行交换。然后再找第二大或者第二小的数据进行交换,以此类推,直到序列全部有序为止。

选择排序与冒泡排序的区别在于,冒泡排序每比较一次后,满足条件的数据就交换,而选择排序是每次比较后,记录满足条件数据的位置,一轮循环过后再作交换。

选择排序的演示示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void SelectSort(int x[], int n)//选择排序 
{
int i, j, min,k;
for (i = 0; i < n; i++)
{
min=i;
for (j = i; j < n; j++)
{
if (x[min] > x[j])
min = j;
}
k = x[i];
x[i] = x[min];
x[min] = k;
}
for (i=0;i<n;i++)
printf("%d\t",x[i]);
}

4、直接插入排序

插入排序的规则是:第一轮开始时默认序列中第一个数据是有序的,之后各个数据以此为基准,判断是插入在此数据的前面还是后面,之后的数据依次向后移动,腾出位置,让数据插入,以此类推,直到整个序列有序为止。每比较一次,如果满足条件(升序:前面一个数比后面需要插入的数大),就直接交换。

特点:对基本有序的序列插入排序速度相对而言比较快,插入排序的优势越明显,数据量越多,劣势也越明显

插入排序的演示示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void InsertSort(int x[], int n)//插入排序 
{
int i, j, k;
for (i = 1; i < n; i++)
{
for (j = i; j >= 0; j--)
if (x[j - 1]>x[j])
{
k = x[j - 1];
x[j - 1] = x[j];
x[j] = k;
}
}
for (i = 0; i < n;i++)
printf("%d\t",x[i]);
}

优化后的插入排序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void insert_sort(int arr[],int len)//优化后的插入排序 
{
int tempVal;
int j;
for (int i = 1; i < len; ++i)//确定循环次数,改变i是为了把i直接当成下标
{
tempVal = arr[i];//把待插入的数据另行保存一份
j = i - 1;
while (j >= 0 && tempVal < arr[j])
{
arr[j + 1] = arr[j];
--j;
}
arr[j + 1] = tempVal;//注意 : j+1
}
}

十二、顺序表

1、顺序表的基本概念

顺序表是将表中的数据依次存放在计算机内存中一组地址连续的存储单元中的一种数据结构,可以将顺序表看成一个可以动态改变大小的数组。

​ 数据元素物理存储的相邻关系来反映数据元素之间逻辑上的相邻关系

是线性表的一种,也就是采用顺序存储结构的线性表简称为”顺序表”。

顺序表的存储特点是:只要确定了起始位置 ,数据可以通过指定位置得到:首地址+(位置*偏移大小)

2、顺序表的定义

顺序表结构的定义如下所示:

1
2
3
4
5
typedef int Type //类型别名的定义
struct Array {
Type *data; //数据域(存储数据的空间)
int length; //顺序表的长度
};

3、顺序表的功能实现

数据的4种基本操作:增、删、改、查,顺序表的基本操作:增、删、改、查。

使用函数实现以下顺序表的基本操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
/*顺序表功能函数的实现*/
//①构造一个空的顺序线性表
array * arr_init() //顺序表的初始化函数
{
array * temp = (array *)malloc(sizeof(array)); //顺序表结构体的初始化
if (NULL == temp)
{
printf("顺序表初始化失败:");
return NULL;
}
temp->data = (Type *)calloc(1,sizeof(Type)); //顺序表数据域的初始化
temp->lenth = 0; //顺序表长度的初始化
return temp; //将顺序表返回
}
//②销毁顺序表
void arr_free(array * arr)
{
if (arr != NULL)
{
if (arr->data != NULL)
free(arr->data); //释放顺序表的数据域
free(arr); //释放整个顺序表
}
else
printf("顺序表为空:");
}
//③重置为空表
void arr_clear(array * arr)
{
if (arr == NULL)
{
printf("顺序表为空:");
return;
}
// for (int i = 0; i < arr->lenth; i++)
// {
// arr->data[i] = 0; //顺序表元素置0
// }
// arr->data = (Type *)realloc(arr->data, sizeof(Type)); //顺序表数据域清空
if (arr->data == NULL)
{
printf("顺序表数据域为空:");
return;
}
free(arr->data);
arr->data = (Type *)calloc(1, sizeof(Type));
arr->lenth = 0; //顺序表长度置0
}
//④判断是否为空表
int arr_empty(array * arr)
{
if (arr->lenth == 0)
{
printf("顺序表数据为空:");
return 1;
}
return 0;
}
//⑤插入顺序表元素
void arr_push(array * arr, Type elem)
{
if (arr == NULL)
{
Error("顺序表为空:");
return;
}
if (arr->data == NULL)
{
Error("顺序表数据域为空:");
return;
}
arr->lenth++; //顺序表长度+1
arr->data = (Type *)realloc(arr->data, sizeof(Type)*arr->lenth); //顺序表数据域内存扩大
arr->data[arr->lenth-1] = elem; //元素放入顺序表最后
}
//⑥在顺序表指定位置插入新的数据元素
void arr_insert(array * arr, int index, Type elem)
{
if (arr == NULL)
{
printf("顺序表为空:");
return;
}
if (arr->data == NULL)
{
printf("顺序表数据域为空:");
return;
}
if (arr_empty(arr))
return; //判断顺序表是否为空
// if (index > arr->lenth) //如果插入的位置大于顺序表最大长度,则插入错误
// {
// printf("插入数据的位置错误:");
// return;
// }
int i = arr->lenth++;
arr->data = (Type *)realloc(arr->data, sizeof(Type)*arr->lenth); //顺序表数 据域内存扩大
for (; i > index - 1; i--)
{
arr->data[i] = arr->data[i - 1];
}
arr->data[i] = elem;
}
//⑦删除顺序表指定位置的数据元素, 并返回元素的值
Type arr_remove(array * arr, int index)
{
if (arr == NULL)
{
printf("顺序表为空:");
return 0;
}
if (arr->data == NULL)
{
printf("顺序表数据域为空:");
return 0;
}
if (arr_empty(arr))
{
return -1; //判断顺序表是否为空
}
Type val = arr->data[index - 1]; //记录被删除元素
int i = index - 1;
for (; i < arr->lenth; i++)
{
arr->data[i] = arr->data[i + 1];
}
arr->lenth--; //顺序表长度减1
arr->data = (Type *)realloc(arr->data, sizeof(Type)*arr->lenth); //顺序表数据域内存减少
return val;
}
//⑧输出顺序表
void arr_out(array * arr)
{
if (arr == NULL)
{
printf("顺序表为空:");
return;
}
if (arr->data == NULL)
{
printf("顺序表数据域为空:");
return;
}
arr_empty(arr); //判断顺序表是否为空
for (int i = 0; i < arr->lenth; i++)
{
printf(P, arr->data[i]);
}
putchar('\n');
}

十三、链表

1、链表的基本概念

1.1、什么是链表

链表是数据结构中线性表的一种,其中的每个元素实际上是一个单独的结构体对象,而所有对象都通过

每个元素中的指针链接在一起。它是以结构体为节点,将一个结构体看成数据域和指针域两个部分,数

据域用于存储数据,指针域用于连接下一个节点,链表中每个结构体对象叫做节点,其中第一个数据节

点叫做链表的首元节点;如果第一个节点不用于存储数据,只用于代表链表的起始点,则这个节点称为

链表的头节点。

1.2、链表的特点

链表有以下特点:

①链表没有固定的长度,可以自由增加节点
②链表能够实现快速的插入删除数据,也就是可以快速的插入和删除链表中的节点
③与数组类似,链表也是一种线性数据结构
④链表的尾结点的后继必定指向空

1.3、链表和数组的区别:

数组和顺序表是顺序存储的,也就是内存是连续的;而链表是通过指针将不连续的内存连接起来,实现链式存储的。

2、链表的结构

3、单链表

3.1、单链表结构的声明

单链表结构体的定义如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
typedef int Type; //数据类型 通过取别名的形式进行灵活使用

struct Node{ //单链表节点结构体的声明
Type data; //链表节点的数据域,用于存储数据
struct Node *next; //链表节点的指针域,用于指向和连接下一个节点
};

struct LinkList{ //单链表结构体的声明
struct Node *head; //链表头节点的指针域,用于指向链表的开头
struct Node *next; //链表尾节点的指针域,用于指向链表的末尾
int lenth; //链表的长度
};

3.2、单链表的创建与功能实现

(1)单链表的创建

1
2
3
4
5
6
7
8
9
10
11
12
13
//1、创建单链表 
LL *list_init()
{
LL *temp = (LL *)malloc(sizeof(LL));
if (temp == NULL)
{
return NULL;
}
temp->head = NULL; //链表头节点指针置空(初始化)
temp->end = NULL; //链表尾结点指针置空(初始化)
temp->lenth = 0; //链表长度置0(初始化)
return temp;
}

(2)单链表节点的链接

1
2
3
4
5
6
7
8
9
10
11
12
//2、链接单链表节点
Node *node_init(Type val)
{
Node *temp = (Node *)malloc(sizeof(Node));
if (temp == NULL)
{
return NULL;
}
temp->data = val; //链表节点的数据域赋值
temp->next = NULL; //(*temp).next = NULL
return temp;
}

(3)单链表的输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//3、单链表的输出
void list_print(LL *list)
{
if (list == NULL)
{
printf("链表空间不存在!\n");
return;
}
if (list->head == NULL)
{
printf("链表为空!\n");
return;
}
for (Node *temp = list->head; temp != NULL; temp = temp->next)
{
printf(T, temp->data);
}
printf("NULL\n");
}

(4)单链表节点的插入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//4、单链表节点的插入
void list_insert_end(LL *list, Type val) //尾插法
{
if (list == NULL)
{
printf("插入错误,链表空间不存在!\n");
}
//Node *temp = node_init(val);
if (list->head == NULL) //如果是空链表
{
list->head = list->end = node_init(val); //第一个链表节点,其既是头部也是尾部
list->lenth++; //链表长度+1
}
else
{
//Node *temp = node_init(val);
//list->end->next = temp
//list->end = temp;
list->end->next = node_init(val); //创建一个新节点,连接到链表当前的末尾
list->end = list->end->next; //新节点成为新的尾部
list->lenth++; //链表长度+1
}
}

(5)单链表节点的删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//5、单链表节点的删除
Type list_delete(LL *list, int index)
{
if (list == NULL)
{
printf("删除错误,链表空间不存在!\n");
return 0;
}
if (index > list->lenth || index <= 0)
{
printf("删除位置错误!\n");
return 0;
}
if (index == 1)
{
Node *temp = list->head; //记录当前头部
list->head = list->head->next; //第二个节点成为新的头部
Type val = temp->data; free(temp);
list->lenth--;
return val;
}
if (index == list->lenth)
{
Node *temp = list->head;
for (int i = 1; i < index - 1; i++)
{
temp = temp->next; //找到删除位置前一个节点
}
Type val = temp->next->data; free(temp->next);
temp->next = NULL; //链表末尾指向空
list->end = temp; //倒数第二个节点成为新的尾部
list->lenth--;
return val;
}
Node *temp1 = list->head; Node *temp2;
for (int i = 1; i < index - 1; i++)
{
temp1 = temp1->next;
//找到删除位置前一个节点
}
Type val = temp1->next->data; //记录被删除节点的数据
temp2 = temp1->next; //temp2指向被删除节点
temp1->next = temp2->next; //删除位置前的节点跳过被删除的节点,指向下下一个节点
free(temp2); //释放被删除节点
list->lenth--;
return val;
}

(6)向单链表指定位置插入数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//6、指定位置插入
void list_insert(LL *list, int index, Type val) //index指定位置
{
if (list == NULL)
{
printf("插入错误,链表空间不存在!\n");
return;
}
if (index > list->lenth+1 || index<=0)
{
printf("插入位置错误!\n");
return;
}
if (index == list->lenth + 1) //如果插入位置刚好比链表长度大一个
{
list_insert_end(list, val); //从尾部插入
return;
}
if (index == 1) //如果在当前头节点之前插入
{
Node * New = node_init(val); //创建新插入的节点
New->next = list->head; //新节点指向当前头节点
list->head = New; //新节点成为新的头部
list->lenth++;
return;
}
Node *temp = list->head;
for (int i = 1; i < index-1; i++)
{
temp = temp->next; //找到插入位置前一个节点
}
Node * New = node_init(val); //创建新插入的节点
New->next = temp->next; //新节点连接插入位置之后的节点
temp->next = New; //链表插入位置之前的节点连接上新节点
list->lenth++;
}

(7)获取单链表指定位置上的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//7、获取单链表指定位置上的数据
Type list_get(LL *list, int index)
{
if (list == NULL)
{
printf("错误,链表空间不存在!\n");
return 0;
}
if (index > list->lenth || index <= 0)
{
printf("位置错误!\n");
return 0;
}
Node *temp = list->head;
for (int i = 1; i < index; i++)
{
temp = temp->next;
}
return temp->data;
}

(8)单链表的销毁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//8、链表的销毁
void list_delete_all(LL **list)
{
if (*list == NULL)
{
printf("错误,链表空间不存在!\n");
return;
}
// if (list->head == NULL)
// {
// free(list);
// }
Node *temp1 = (*list)->head,*temp2;
for (; temp1 != NULL;) //循环释放链表中的各个数据节点
{
temp2 = temp1;
temp1 = temp1->next;
free(temp2);
}
free(*list); //释放整个链表
*list = NULL; //链表指针指向空
}

十四、栈和队列

1、栈和队列的基本概念

在数组中,我们可以通过索引(下标)访问随机元素。 但是,在某些情况下,我们可能需要限制处理

顺序,这就产生了栈和队列这两种功能受限的线性结构。

栈和队列是两种不同的处理顺序:先进后出和先进先出,以及两个相应的线性数据结构。

2、数据结构中的栈和队列

2.1、栈 (stack)

数据后进先出,先进后出:LIFO (last in first out)

栈只有一个开口,先进去的就到下面,后进来的就在上面(top),要是拿出去的话,肯定是从开口端拿出去,所以说先进后出,后进先出。

入栈:push

出栈:pop

获取栈顶元素:top

判断栈是否已经为空:is_empty

判断栈是否已经满了:is_full (如果是数组实现的)

2.2、队列(queue)

数据入队规则:先进先出,后进后出,FIFO(first in first out)

队列有队首(front)和队尾(back),数据从队尾入队,从队首出队。

队首(front)指向队列的第一个数据,队尾(back)指向队列中的最后一个数据。

入队:push

出队:pop

队首:front

队尾:back

3、栈和队列的基本结构

3.1、栈和队列的结构示意图

3.2、栈和队列中数据的插入和删除

(1)栈中数据的插入和删除

(2)队列中数据的插入和删除

4、栈和队列的实现

4.1、栈功能的实现

可以使用链表或顺序表的结构实现栈。

如:栈的数组实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include<stdlib.h>
#include<stdio.h>
#include<assert.h>
typedef int Type;
#define STACK_SIZE 10 //栈的最大大小
//栈的数组实现
typedef struct Stack //栈的结构体声明
{
Type data[STACK_SIZE]; //数据域
int top; //栈顶元素下标
}stack;
/*栈的功能实现*/
//栈的初始化函数
stack *stack_init()
{
stack *temp = (stack *)malloc(sizeof(stack));
assert(temp);
temp->top = -1; //初始化栈顶元素下标为-1,-1表示栈中没有元素
return temp;
}
//数据入栈函数
void stack_push(stack *st, Type val)
{
assert(st); //判断栈是否存在
assert(!stack_full(st)); //判断栈是否满了
st->data[++st->top] = val; //从栈顶插入元素
}
//数据出栈函数
Type stack_pop(stack *st)
{
assert(st); //判断栈是否存在
assert(!stack_empty(st)); //判断栈是否空了
Type val = st->data[st->top]; //记录当前栈顶元素
st->top--; //栈顶元素下标-1
return val;
}
//获取栈顶元素
Type stack_top(stack *st)
{
assert(st); //判断栈是否存在
assert(!stack_empty(st)); //判断栈是否空了
return st->data[st->top];
}
//判断栈是否为空
bool stack_empty(stack *st)
{
return st->top == -1; //栈空了
}
//判断栈是否满了
bool stack_full(stack *st)
{
return st->top >= STACK_SIZE; //栈满了
}

4.2、队列功能的实现

可以使用链表或顺序表的结构实现队列。

如:队列的链表实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#include<stdlib.h>
#include<stdio.h>
#include<assert.h>
typedef int Type;
/*队列的链表实现*/
//链表节点结构的声明
typedef struct node
{
Type data; //数据域
struct node *next; //指针域
}Node;
//队列链表结构的声明
typedef struct queue
{
Node *front; //队首指针
Node *back; //队尾指针
}Queue;
/*队列功能的实现*/
//队列单链表的初始化
Queue *queue_init()
{
Queue *temp = (Queue*)malloc(sizeof(Queue));
assert(temp); //如果队列初始化失败则报错
temp->front = temp->back = NULL; //队首和队尾指针初始化为NULL
return temp;
}
//队列单链表节点的创建函数
Node *node_create(Type val)
{
Node *temp = (Node *)malloc(sizeof(Node));
assert(temp);
temp->data = val; //节点数据域的初始化
temp->next = NULL; //节点指针域的初始化
return temp;
}
//数据入队函数
void queue_push(Queue *q, Type val) //链表的尾插法
{
assert(q);
if (q->front == NULL)
{
q->front = node_create(val); //创建队列的第一个节点
q->back = q->front; //队列只有一个节点,其既是头部也是尾部
}
else
{
q->back->next = node_create(val); //新创建的节点连接到队列尾部
q->back = q->back->next; //新节点是新的尾部
}
}
//数据出队函数
Type queue_pop(Queue *q) //链表的头删法
{
assert(q); //整个链表不存在
assert(q->front); //链表为空
Node *temp = q->front;
Type val = temp->data;
q->front = q->front->next; //当前队首的下一个节点成为成为新的队首
free(temp); //之前的队首节点释放
temp = NULL; //避免temp成为野指针
return val;
}
//获取队首元素
Type queue_front(Queue *q)
{
assert(q); //整个链表不存在
//assert(q->front); //链表为空
assert(!queue_empty(q)); //链表为空
return q->front->data;
}
//获取队尾元素
Type queue_back(Queue *q)
{
assert(q); //整个链表不存在
//assert(q->front); //链表为空
assert(!queue_empty(q)); //链表为空
return q->back->data;
}
//判断队列是否为空
bool queue_empty(Queue *q)
{
assert(q); //整个链表不存在
//if (q->front == NULL) //如果队列为空
// return true; //返回真1
//else
// return false; //否则返回假0
return q->front == NULL; //判断队列是否为空
}
  • 标题: C语言入门
  • 作者: 小颜同学
  • 创建于: 2022-09-02 12:36:14
  • 更新于: 2023-12-18 09:06:28
  • 链接: https://www.wy-studio.cn/2022/09/02/C语言入门/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
 评论
此页目录
C语言入门