C 语言笔记

前言

此笔记是本人根据谭浩强的教材《C程序设计(第五版)》及其结合自己在网上找的再自己写的。认真看完它并加上充分的练习应对期末、专升本应该是足够了,当然你也可以作为巩固复习资料使用,对初学者也是有一定的作用。当然,本人也是初学水平所做的复习笔记,如有不足之处还请谅解指正内容多,请结合大纲使用

第1章 程序设计和C语言

C语言的发展及其特点

  • C语言发展:

C语言是由贝尔实验室的Dennis Ritchie在20世纪70年代初开发的一种通用计算机编程语言。C语言的设计目标是提供一种高效、可移植的编程语言,用于系统级编程和应用程序开发。其发展可以分为以下几个阶段:

  1. 诞生和发展初期(1970年代):C语言最初是为了开发UNIX操作系统而设计的。在这个阶段,C语言的设计和实现主要由Dennis Ritchie和Ken Thompson完成。他们通过C语言的简洁和灵活性,成功地将UNIX系统移植到不同的计算机平台上。
  2. 标准化(1980年代):随着C语言的流行和广泛应用,人们开始意识到需要一个标准化的C语言规范,以确保不同的编译器能够产生相同的结果。于是,美国国家标准学会(ANSI)和国际标准化组织(ISO)联合制定了C语言的标准规范,于1989年发布了C89标准。
  3. C语言的变体和扩展(1990年代):在C语言的基础上,出现了一些变体和扩展,如C++、Objective-C等。C++是由Bjarne Stroustrup在1980年代初开发的,它在C语言的基础上增加了面向对象编程的特性。Objective-C是由Brad Cox在1980年代初开发的,它在C语言的基础上增加了面向对象编程和动态运行时特性。
  4. 标准更新(2000年代至今):C语言的标准规范在2000年和2011年分别进行了更新,分别发布了C99和C11标准。这些更新主要是为了提供更多的特性和功能,以适应计算机技术的发展和应用需求的变化。
  • C语言特点:
  1. 语言简洁、紧凑,使用灵活方便、灵活
  2. 运算符丰富
  3. 数据类型丰富
  4. 具有结构化的控制语句。eg:if…else语句、while语句、do…while语句等
  5. 允许直接访问物理地址,能进行位(bit)操作,能实现编汇语言的大部分功能,可以直接对硬件进行操作

C语言的程序的基本程序结构

  1. 一个程序由一个或多个源程序文件组成
  • 预处理指令

预处理指令是在编译之前对源代码进行处理的指令,由以”#”开头的特殊语句组成。预处理指令不是真正的C语句,而是用来指导编译器在编译之前进行一些处理操作的。eg:#include:用于引入头文件,将头文件的内容插入到当前位置。#define:用于定义宏,将标识符替换为指定的值或代码片段

  • 全局声明

全局声明是指在函数外部声明的变量或函数,其作用域在整个程序中都可见。全局声明可以在任何函数中使用,而不需要在每个函数中重新声明。eg:把“ int a,sum; ” 放到了main函数之前

  • 函数定义

函数定义是指在程序中实现函数的具体功能。函数定义包括函数的返回类型、函数名、参数列表和函数体。

1
2
3
4
5
返回类型 函数名(参数列表) {
// 函数体
// 执行具体的功能
// 可能包含一条或多条语句
}

其中,返回类型指定函数的返回值类型,函数名是函数的标识符,参数列表是函数接受的输入参数,函数体是函数的具体实现

1
2
3
4
int add(int a, int b) {
int sum = a + b;
return sum;
}

在上述函数定义中,返回类型是int,函数名是add,参数列表是(int a, int b),函数体中计算了a和b的和,并将结果存储在sum变量中,最后通过return语句返回了sum的值。函数定义可以在程序中的任何位置,通常放在main函数之前。函数定义的目的是将函数的实现与函数的声明分离,使代码更加模块化和可维护。需要注意的是,函数定义必须与函数声明一致,即函数的返回类型、函数名和参数列表必须与函数声明中的一致。否则会导致编译错误。

  1. 函数是C程序的主要组成部分函数是C程序的主要组成部分,它们是用来执行特定任务的代码块。通过将代码分解为多个函数,可以使程序更加模块化和可维护。每个函数都有特定的功能,可以独立地编写、测试和调试。程序的执行从main函数开始,可以在main函数中调用其他函数来完成不同的任务。函数可以接受输入参数并返回一个值,用于传递数据和获取函数的结果。通过使用函数,可以提高程序的可读性、可重用性和可扩展性。
  2. 一个函数包括两部分
  • 函数首部

函数首部是指函数的声明或定义中的第一行,用于指定函数的返回类型、函数名和参数列表。函数首部的一般形式如下:
返回类型 函数名(参数列表);
其中:

  • 返回类型指定函数返回的数据类型,可以是基本类型(如int、float等)或者自定义的结构体类型。
  • 函数名是函数的标识符,用于在程序中调用该函数。
  • 参数列表指定函数接受的参数类型和参数名,多个参数之间用逗号分隔。

举例:
int max(int num1, int num2);
这个函数首部声明了一个名为max的函数,返回类型为int,接受两个int类型的参数num1和num2。
float average(float arr[], int size);
这个函数首部声明了一个名为average的函数,返回类型为float,接受一个float类型的数组arr和一个int类型的size参数。
void printHello();
这个函数首部声明了一个名为printHello的函数,返回类型为void(即无返回值),不接受任何参数。

  • 函数体

函数体是函数定义或声明中的代码块,用于实现函数的具体功能。函数体的一般形式如下:

1
2
3
返回类型 函数名(参数列表) {
// 函数体代码
}

其中:

  • 返回类型与函数首部中的返回类型相对应,指定函数返回的数据类型。
  • 函数名与函数首部中的函数名相对应,用于在程序中调用该函数。
  • 参数列表与函数首部中的参数列表相对应,指定函数接受的参数类型和参数名。

举例:

1
2
3
4
int square(int num) {
int result = num * num;
return result;
}

这个函数体实现了一个名为square的函数,接受一个int类型的参数num,计算num的平方并返回结果。在函数体中,先定义一个局部变量result,将num乘以num的结果赋值给result,然后使用return语句将result作为函数的返回值返回。

  1. 程序总是从main函数开始执行
  2. 程序中要求计算机的操作是由函数中的C语句完成的
  3. 在每个数据声明和语句的最后必须要有一个分号

下面是一个满足上述要求的完整C语言代码示例:

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
#include <stdio.h>

int max(int num1, int num2);
void printHello();

int main() {
int a = 10;
int b = 20;
int result = max(a, b);

printf("最大值是:%d\n", result);

printHello();
return 0;
}

int max(int num1, int num2) {
if (num1 > num2) {
return num1;
} else {
return num2;
}
}

void printHello() {
printf("你好,世界!\n");
}

在这个例子中,程序由一个源文件组成。首先,使用预处理指令#include <stdio.h>引入标准库头文件stdio.h,这样我们就可以使用printf和scanf等标准库函数。然后,在全局声明部分,我们声明了两个函数max和printHello,它们的函数原型分别出现在main函数之前,用于告诉编译器这两个函数的返回类型、函数名和参数列表。接下来,我们定义了主函数main,它是程序的入口点。在main函数中,我们声明了两个整型变量a和b,然后调用了函数max来求出a和b的最大值,并将结果存储在result变量中。最后,我们使用printf函数打印出最大值和一个简单的问候语。在max函数和printHello函数的定义中,我们分别实现了这两个函数的功能。max函数接受两个整型参数,并通过比较来确定最大值,并使用return语句返回该值。printHello函数不接受参数,直接打印出一个问候语。最后,我们在main函数最后返回了0,表示程序正常结束。请注意,每个语句和声明末尾都有一个分号,这是C语言中的语法规则。这些分号用于表示语句的结束。


结构化程序设计方法的思想

  1. 模块化:将程序分解为多个模块或函数,每个模块或函数负责处理特定的功能或任务。模块化的设计使得程序结构清晰,易于理解和维护。
  2. 自顶向下设计:从整体到局部的设计思路,先设计整体的程序结构和主要功能,然后逐步细化为更小的模块或函数。这种设计方法使得程序的层次结构清晰,易于理解和开发。
  3. 顺序、选择和循环结构:使用顺序、选择和循环结构来组织程序的逻辑流程。顺序结构按照代码的顺序执行,选择结构根据条件选择不同的执行路径,循环结构重复执行一段代码。
  4. 数据抽象和封装:将数据和对数据的操作封装在模块或函数中,只暴露必要的接口给外部使用。这种封装和抽象的方式提高了程序的可读性、可维护性和可重用性。
  5. 模块间的通信:模块之间通过参数传递和返回值来进行数据的交换和通信。模块之间的通信应该尽量简洁明了,避免过多的全局变量和副作用。

编辑、编译、运行一个C程序的步骤

  1. 上机输入和编辑源程序C语言文件后缀(.c)
  2. 对源程序进行编译预处理阶段是在编译之前进行的,主要处理以”#”开头的预处理指令,如包含头文件、宏定义等。预处理器会根据这些指令对源代码进行处理,生成一个经过宏展开、条件编译等处理的新的源代码文件。编译阶段将预处理后的源代码转换为汇编代码。编译器会对源代码进行词法分析、语法分析和语义分析,生成对应的中间代码(通常是汇编代码)。汇编阶段将汇编代码转换为机器代码。汇编器会将汇编代码翻译成机器指令,生成目标文件(通常是以”.o”或”.obj”为扩展名)。
  3. 进行链接处理链接阶段将目标文件与其他的目标文件或库文件进行合并,生成最终的可执行文件。链接器会解析目标文件中的符号引用,将其与其他目标文件或库文件中的符号定义进行匹配,生成最终的可执行文件(.exe)
  4. 运行可执行文件,得到运行结果

第2章 算法——程序的灵魂

算法的概念

一个程序包括两个方面:算法 + 数据结构 = 程序

  1. 对数据的描述在程序中要指定用到哪些数据,以及这些数据的类型和数据的组成形式,这就是数据结构
  2. 对操作的描述要求计算机操作的步骤,也就是算法。

算法的特点

  1. 有穷性一个算法应该包括有限的操作步骤。
  2. 确定性算法中的每一个步骤都应该是确定的。
  3. 有零个或多个输入输入是指在执行算法时需要从外界取得必要的信息
  4. 有一个或多个输出算法的目的是为了求解,解就是输出
  5. 有效性算法中的每个步骤都应当能有效地进行,并得到确定的结果

怎样表示一个算法

  1. 用自然语言(就是讲人话解释清楚)
  2. 用流程图算法表示
  • 传统流程图传统流程图是一种图形化的工具,用于描述算法或程序的执行流程。它由各种不同形状的框和箭头组成,表示程序中的不同步骤和控制流程。

  • N-S流程图N-S流程图是一种简单的图表,用来描述一个过程或流程中的各个步骤和决策。它使用箭头表示流程的方向,起点为N(Start),终点为S(Stop)。在流程图中,不同的步骤和决策用不同的图形和符号表示。通过这种图表,人们可以清晰地了解和描述一个过程中的执行顺序和条件判断。

举例:输入三角形三边长,判断三遍构成的是等边,等腰,还是一般三角形

  1. 用伪代码表示算法伪代码是一种类似于编程语言的描述性语言,用于描述算法或程序的逻辑结构和执行流程。它不是一种具体的编程语言,而是一种方便程序员理解和描述算法的工具。伪代码通常使用类似于编程语言的语法和关键字,但不需要遵循具体语言的语法规则。示例:计算两个数的和

    1
    2
    3
    4
    1. 输入两个数
    2. 计算和 = 第一个数 + 第二个数
    3. 显示和
    4. 结束
  2. 用计算机语言比如:用C语言、Java表示…

能够利用传统流程图和N-S流程图描述算法

上面以及写过了,不用我在解释了吧 关键在多练,多做题你就会了


第3章 最简单的C程序设计—顺序程序设计

常量和变量

一、常量:在程序运行的过程中,其值不能被改变的量称为常量

  1. 整型常量eg:1000、0、123.25、-652…
  2. 实型常量
  • 十进制小数形式。有数字和小数组成。eg:12.5….
  • 指数形式。
  1. 字符常量
  • 普通字符。用单引号括起来的单个字符。 eg:’5’、’e’、’?’….

在C语言中,普通字符(char)在计算机中的存储通常使用ASCII编码。ASCII编码使用一个字节(8位)来表示一个字符,范围从0到127。其中,0到31是控制字符,32到126是可打印字符,而127是删除字符。例如,字符 ‘A’ 的ASCII码是65,以二进制表示为01000001。在计算机中,该字符会被存储为一个字节,即8位,从高位到低位依次为 0 1 0 0 0 0 0 1。
ASCII表

  • 转义字符。以 \ 开头的字符。

在C语言中使用字符串时,有时候需要插入一些特殊字符或者控制字符,这时就可以使用转义字符来实现。转义字符以反斜杠(\)开头,后面跟着一个或多个字符,用来表示特定的含义。例如,\n代表换行符,当我们在字符串中插入\n时,编译器会将其解释为换行符,输出时会在该位置换行。同样地,\t代表制表符,\r代表回车符,\b代表退格符,\f代表换页符等等。

  1. 字符串常量。双引号括起来的若干个字符。eg:”hello”、”123”
  2. 符号常量。用 #define 指令,指定用一个符号名称代表一个常量。

#define PI 3.1415 //末尾没有分号,程序用到 PI 就代表是3.1415


二、变量:在程序运行期间,其值可以改变的量叫做变量。变量在使用前需要先声明,声明变量时需要指定变量的类型和名称。在C语言中,变量的声明和使用有一些不同的语法。下面是一个使用变量的C语言例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
int main() {
// 声明一个整数类型的变量
int age = 25;

// 声明一个字符类型的变量
char grade = 'A';

// 声明一个浮点数类型的变量
float height = 1.75;

// 修改变量的值
age = 26;

// 输出变量的值
printf("Age: %d\n", age);
printf("Grade: %c\n", grade);
printf("Height: %.2f\n", height);
return 0;
}

在上面的例子中,我们首先包含了头文件stdio.h,该头文件中包含了输入输出函数的声明。然后,在main函数中声明了三个变量:agegradeheight,并给它们赋予了初始值。接着,我们修改了age变量的值,并使用printf函数输出了变量的值。在C语言中,变量的类型需要在声明时指定,并且变量名需要遵循一定的命名规则。不同的变量类型可以存储不同类型的数据。通过修改变量的值,我们可以改变变量存储的数据。需要注意的是,在C语言中,变量的声明通常放在函数的开头,而变量的使用可以放在任意位置。


三、常变量在定义变量前加一个 const )表示该变量是常量,其值不能被修改。常变量一旦被初始化,其值将在整个程序执行过程中保持不变。举例:
const int MAX_VALUE = 100;
在上述例子中,MAX_VALUE被声明为一个常变量,其值为100,该值在程序执行期间不能被修改。


关键字和标识符

一、标识符用来表示变量、函数、类、结构体等程序实体的名称叫做标识符。标识符的命名规则如下:

  • 可以使用大小写字母(A-Z、a-z)、数字(0-9)和下划线(_)。
  • 标识符开头必须是字符或者是下划线,不能以数字开头。
  • 标识符区分大小写,例如”num”和”Num”是不同的标识符。
  • 标识符不能是C语言的关键字,如if、for、int等。

二、关键字关键字是在编程语言中具有特殊含义和用途的保留字。它们被编程语言用于表示语法结构、控制流程、定义数据类型等特定的功能和用途。关键字具有固定的语法和语义,不能作为标识符来使用。在C语言中,关键字用于表示条件判断、循环、函数返回值、数据类型等。例如,if关键字用于表示条件判断,for关键字用于表示循环,int关键字用于定义整型数据类型。举例:if、else、switch、while…..


数据类型

在C语言中,数据类型用于定义变量的类型,以及变量可以存储的数据范围和操作。
一、整型数据

  1. 基本整型(int 型)

占用4个字节,取值范围为-2,147,483,648到2,147,483,647或0到4,294,967,295

  1. 短整型 (short int型)

占用2个字节,取值范围为-32,768到32,767或0到65,535

  1. 长整型 (long int 型)

占用4个字节(32位系统)或8个字节(64位系统),取值范围为-2,147,483,648到2,147,483,647

  1. 双长整型 (long long int 型)

占用8个字节,取值范围为-9,223,372,036,854,775,808到9,223,372,036,854,775,807

  • 在定义基本整型外,是可以像以下便捷操作的
    1
    2
    3
    short num01 = 3;
    long num02 = 4;
    long long num03 = 5; //int关键字可以省略
    实际上等价于以下代码:
    1
    2
    3
    short int num01 = 3;
    long int num02 = 4;
    long long int num03 = 5;

二、整型变量的符号属性C语言中的整型变量可以具有符号属性(有符号)或无符号属性(无符号),这取决于变量的声明方式。有符号整型变量可以表示正数、负数和零,而无符号整型变量只能表示非负数(包括零)。有符号整型变量的取值范围是从负最大值到正最大值之间,例如int类型的取值范围是-2,147,483,648到2,147,483,647。有符号整型变量的计算方式使用补码表示,即正数的二进制表示与无符号整型相同,而负数使用补码表示。无符号整型变量的取值范围是从0到正最大值之间,例如unsigned int类型的取值范围是0到4,294,967,295。无符号整型变量的计算方式直接使用二进制表示。在C语言中,整型变量的符号属性可以通过声明时的类型修饰符来指定。
例如:

1
2
int signedVar;           // 有符号整型变量
unsigned int unsignedVar; // 无符号整型变量

如果不明确指定符号属性,默认情况下,int和unsigned int被视为有符号和无符号整型变量。unsigned short类型:

  • 占用字节数:2字节,取值范围:0 到 65,535

unsigned int类型:

  • 占用字节数:4字节,取值范围:0 到 4,294,967,295

unsigned long类型:

  • 占用字节数:4字节(32位系统)或 8字节(64位系统),取值范围:0 到 4,294,967,295

unsigned long long类型:

  • 占用字节数:8字节,取值范围:0 到 18,446,744,073,709,551,615

三、字符型数据由于字符是按其代码(整数)形式储存的,因此在C语言中字符型数据作为整数类型的一种,但它又有自己的特点,需单独拿出来讲

  1. 字符与字符代码

字符与字符代码并不是任写一个字符,程序都能识别。例如代表圆周率的 π 就是不能识别的,只能使用系统的字符集中的字符,目前大多数系统大多采用ASCII字符集。

  1. 字符变量

字符变量就是用类型符** char **定义字符变量。char是英文character(字符)的缩写,见名知意。

  • 有符号字符型(signed char,一般写作 char )字节数:

通常为1个字节(8位),取值范围:-128 到 127。

  • 无符号字符型(unsigned char):

字节数:通常为1个字节(8位),取值范围:0 到 255。


四、浮点型数据用来表示具有小数点的实数

  • 单精度浮点型(float 型)
    • 通常占用4个字节(32位),其有效数字位数为约6-9位。该类型可以表示大约1.2e-38到3.4e+38之间的数值。
  • 双精度浮点型(double 型)
    • 通常占用8个字节(64位),其有效数字位数为约15-18位。该类型可以表示大约2.3e-308到1.7e+308之间的数值。
  • 长双精度型 (long double 型)
    • 占用的字节数较为可变,通常是8个字节或更多。其有效数字位数也会相应增加。这种类型通常提供比double更高的精度。

需要注意的是,数据类型的大小,取值范围和有效数字位数可能会因编译器和平台的不同而有所变化。


运算符与表达式

C语言提供了很多运算符,当前章节就讲讲算术运算符与赋值运算符一、基本算术运算符

  • 加法运算符(+):将两个操作数相加。

    1
    2
    3
    int a = 5;
    int b = 3;
    int c = a + b; // c = 8
  • 减法运算符(-):从第一个操作数中减去第二个操作数。

    1
    2
    3
    int a = 5;
    int b = 3;
    int c = a - b; // c = 2
  • 乘法运算符(*):将两个操作数相乘。

    1
    2
    3
    int a = 5;
    int b = 3;
    int c = a * b; // c = 15
  • 除法运算符(/):将第一个操作数除以第二个操作数,得到商。

    1
    2
    3
    int a = 10;
    int b = 3;
    int c = a / b; // c = 3,因为是整型,所以结果没有小数点
  • 取模运算符(%):将第一个操作数除以第二个操作数,得到余数。

    1
    2
    3
    int a = 10;
    int b = 3;
    int c = a % b; // c = 1
  • 正号运算符(+):表达数据为正

    1
    +a;
  • 负号运算符(-):表达数据位负

    1
    -a;

二、自增自减运算符

  1. 自增运算符有两种形式:前缀形式和后缀形式。

前缀形式的自增运算符(++x)会先将操作数的值增加1,然后返回增加后的值。

1
2
int x = 5;
int y = ++x; // x = 6, y = 6

后缀形式的自增运算符(x++)会先返回操作数的值,然后再将其增加1。

1
2
int x = 5;
int y = x++; // x = 6, y = 5
  1. 自减运算符的使用方法与自增运算符类似,只是将值减少1。

前缀形式的自减运算符(–x)会先将操作数的值减少1,然后返回减少后的值。

1
2
int x = 5;
int y = --x; // x = 4, y = 4

后缀形式的自减运算符(x–)会先返回操作数的值,然后再将其减少1。

1
2
int x = 5;
int y = x--; // x = 4, y = 5

三、算术表达式C语言的算术表达式是由操作数、运算符和括号组成的表达式,用于表示数值之间的运算关系。C语言支持一系列算术运算符,包括加法(+)、减法(-)、乘法(*)、除法(/)和取模(%)等。算术表达式可以包含整数、浮点数和字符类型的操作数。这些操作数可以直接使用字面值,也可以是变量或表达式的结果。例如,下面是一些C语言的算术表达式的示例:

  1. ** **整数运算:int a = 5;int b = 3;int c = a + b; // 加法运算,c的值为8int d = a - b; // 减法运算,d的值为2int e = a * b; // 乘法运算,e的值为15int f = a / b; // 除法运算,f的值为1(整数除法)int g = a % b; // 取模运算,g的值为2
  2. 浮点数运算:float x = 3.14;float y = 2.0;float z = x + y; // 加法运算,z的值为5.14float w = x * y; // 乘法运算,w的值为6.28float q = x / y; // 除法运算,q的值为1.57
  3. 混合类型运算:int m = 5;float n = 2.5;float p = m + n; // 加法运算,p的值为7.5float r = m * n; // 乘法运算,r的值为12.5float s = m / n; // 除法运算,s的值为2.0

*例如,表达式 a + b __ c 可以通过加上括号来明确指定运算的顺序:(a + b) * c。同时也遵循先乘除后加减的优先级原则此外,C语言还支持一些其他的算术运算符和特殊的运算规则,如自增(++)、自减(–)、赋值运算符(=)等。


四、不同数据类型之间的混合运算在C语言中,不同类型之间的混合运算指的是在表达式中使用不同类型的操作数进行运算。当表达式中存在不同类型的操作数时,C语言会根据一组规则来确定运算的结果类型。

  1. **整数和浮点数的混合运算: **

    • 如果一个操作数是浮点数,那么另一个操作数将被自动转换为浮点数,结果将是浮点数。
    • 如果一个操作数是整数,而另一个操作数是浮点数,那么整数将被自动转换为浮点数,结果将是浮点数。
      1
      2
      3
      4
      5
      6
      int a = 5;
      float b = 2.5;
      double c;

      c = a + b; // 整数a被自动转换为浮点数,结果c为浮点数
      c = a * b; // 整数a被自动转换为浮点数,结果c为浮点数
  2. **字符和整数的混合运算: **

    • 字符可以被视为整数类型,因此可以与整数进行运算。
    • 字符与整数之间的运算结果将是整数
      1
      2
      3
      4
      5
      char ch = 'A';
      int num = 2;
      int result;

      result = ch + num; // 字符ch被视为整数类型,结果result为整数
      ch是一个字符类型变量,其值为’A’,对应的ASCII码值为65。num是一个整数类型变量,其值为2。因此,最终结果是67
  3. **不同整数类型的混合运算: **

    • 如果两个操作数的类型不同,那么C语言会将它们转换为相同的类型,然后进行运算。
    • 转换的规则是,将较小的整数类型转换为较大的整数类型。
    • long long > long > int > short > char
      1
      2
      3
      4
      5
      short a = 10;
      int b = 20;
      int result;

      result = a + b; // short类型的a被转换为int类型,结果result为int类型
      需要注意的是,混合运算可能会导致数据丢失或精度损失。因此,在进行混合运算时,应该确保转换是安全和合理的,以避免出现错误。总之,在C语言中,不同类型之间的混合运算是允许的,并且根据一组规则来确定运算的结果类型。理解这些规则对于正确编写和理解表达式非常重要。

五、强制类型转换运算符C语言中的强制类型转换运算符是一种用于显式地指定数据类型转换的运算符。它的语法形式是在要转换的值或表达式前面加上括号,并在括号内指定要转换的目标类型。例如:
(type) expression
其中,type表示目标类型,expression表示要转换的值或表达式。
强制类型转换运算符可以用于以下几种情况:

  1. 将一个表达式的结果强制转换为特定的类型。

    1
    2
    int a = 10;
    double b = (double) a; // 将整数a强制转换为浮点数类型double
  2. 将一个变量的类型强制转换为另一种类型。

    1
    2
    int a = 65;
    char ch = (char) a; // 将整数a强制转换为字符类型char
  3. 在混合运算中,显式地指定操作数的类型。

    1
    2
    3
    int a = 10;
    double b = 3.14;
    int c = (int) (a + b); // 将表达式a + b的结果强制转换为整数类型int

    注意:

  • 强制类型转换只是转换当前的临时值,但该表达式结束后,数据仍为原类型。如第二个例子,在char ch = (char) a; 过后,a 还是int整型。
  • 需要注意的是,强制类型转换可能会导致数据的截断或溢出。因此,在进行强制类型转换时,应该确保转换是安全的,并且不会导致数据丢失或溢出。
  • 此外,强制类型转换运算符的优先级较高,因此在表达式中使用时需要注意运算顺序。可以使用括号来明确指定运算顺序。

六、赋值语句

  • 赋值运算符(=):将一个数据赋值给一个变量

a = 3; //把常量赋值给变量a

  • 复合赋值运算符

C语言中的复合赋值运算符是一种简化表达式的方式,它将运算符和赋值操作结合在一起。下面是C语言中的所有复合赋值运算符及其解释:

  • +=:加法赋值运算符

将右操作数的值加到左操作数上,并将结果赋给左操作数。例如,a += b等价于a = a + b

  • -=:减法赋值运算符

将右操作数的值从左操作数中减去,并将结果赋给左操作数。例如,a -= b等价于a = a - b

  • =:乘法赋值运算符

将右操作数的值乘以左操作数,并将结果赋给左操作数。例如,a *= b等价于a = a * b

  • /=:除法赋值运算符

将左操作数的值除以右操作数,并将结果赋给左操作数。例如,a /= b等价于a = a / b

  • %=:取模赋值运算符

将左操作数的值除以右操作数得到的余数,并将结果赋给左操作数。例如,a %= b等价于a = a % b


数据的输入输出

一、用 printf函数输出数据printf函数(格式输出函数)用来向终端输出若干个任意类型的数据

  1. printf函数的一般格式
    1
    2
    3
    4
    5
    printf(格式控制, 输出列表);
    //举例
    int i = 10;
    char c = '0';
    printf("%d,%c\n",i,c);
  • “格式控制”:是用英文双引号括起来的一个字符串,称为格式控制字符串,简称格式字符串

包含以下两个信息:

  • 格式声明:

“%”和 **格式字符 **组成,如%d、%f。将输出的数据转换为指定的格式后输出,总是有 **”%” **开始的

  • 普通字符:

即需要输出时原样输出的字符。如双引号中的逗号、空格、换行符….

  • 输出列表:是程序需要输出的一些数据,可以是常量、变量、或者表达式。如上例中的 “ i 和 c“
    2格式字符

在输出时,不同的数据类型需要指定不同的格式声明,格式声明中最重要的内容是格式字符。

  • d 、i 格式字符:用于输出有符号十进制整数

    1
    2
    3
    int num = 10;
    printf("%d\n", num); // 输出:10
    printf("%i\n", num); // 输出:10
  • u 格式符:用于输出无符号十进制整数

    1
    2
    unsigned int Num = 20;
    printf("%u\n", Num); // 输出:20
  • f 格式符:用于输出浮点数

    • 基本型,用%f
      1
      2
      float fNum = 3.14;
      printf("%f\n", fNum); // 输出:3.140000
  • 指定数据宽度和小数位数,用 %m.nf

m代表输出的数据占几位,n代表输出几位小数

1
2
float x = 3.14159;
printf("%8.2f\n", x);

输出:
3.14
%8.2f 中的8代表输出的数据占8位,其中包括小数点和小数位数。2代表输出2位小数。
因此,输出结果为3.14,小数点前面有4个空格,总共占了8位。

  • 输出的数据向左对齐,用 %-m.nf

m代表输出的数据占几位,n代表输出几位小数,- 代表向左对齐

  • c 格式符:用于输出字符

    1
    2
    char ch = 'A';
    printf("%c\n", ch); // 输出:A
  • s 格式符:用于输出字符串

    1
    2
    char str[] = "Hello, World!";
    printf("%s\n", str); // 输出:Hello, World!
  • p 格式符:用于输出指针地址

    1
    2
    int* ptr = &num;
    printf("%p\n", ptr); // 输出:0x7ffeed56a9c4 (根据具体内存地址而定)
  • %x:用于输出十六进制整数(小写字母)

  • %X:用于输出十六进制整数(大写字母)

  • %o:用于输出八进制整数

  • %e:用于输出科学计数法表示的浮点数(小写字母)

  • %E:用于输出科学计数法表示的浮点数(大写字母)

  • %g:根据数值的大小自动选择使用%f或%e格式输出浮点数(小写字母)

  • %G:根据数值的大小自动选择使用%f或%E格式输出浮点数(大写字母)

  • %%:用于输出百分号

注意:在格式声明中,在 % 和上述格式字符键可以插入以下附加字符(又称修饰符)

  • l 长整型整数:

长整型整数是一种数据类型,用于存储比普通整数更大的整数值,可以加在格式符 d、o、x、u 前面,如 %ld、%lld

1
2
3
4
5
6
7
8
long int num1 = 1234567890;
long long int num2 = 1234567890123456789;

// 使用%ld打印long int类型的整数
printf("Long int: %ld\n", num1); // 输出:1234567890

// 使用%lld打印long long int类型的整数
printf("Long long int: %lld\n", num2); // 输出:1234567890123456789
  • m.n 字符:

m代表数据最小宽度;n实数表示输出n位小数,对字符串表示截取的字符个数(m、n都代表一个正整数)

1
2
3
4
5
6
7
8
9
10
11
12
int num1 = 123;
float num2 = 3.14159;
char ch = 'A';

// 使用%5d打印整数,最小宽度为5
printf("Integer: %5d\n", num1); // 输出: 123

// 使用%8.2f打印浮点数,最小宽度为8,小数位数为2
printf("Float: %8.2f\n", num2); // 输出: 3.14

// 使用%c打印字符
printf("Character: %c\n", ch); // 输出:A

定义了一个整数 num1,一个浮点数 num2 和一个字符 ch使用不同的格式符来打印这些数据,同时指定了最小宽度和小数位数。使用%md来打印整数,其中m表示最小宽度。使用%m.nf来打印浮点数,其中m表示最小宽度,n表示小数位数。使用%c来打印字符。

输出的数字或字符在域内向左靠当我们使用-标志时,输出的数字或字符将在域内向左靠齐。
下面是一个使用-标志的C语言代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
int num1 = 123;
float num2 = 3.14159;
char ch = 'A';

// 使用%-5d打印整数,最小宽度为5,左对齐
printf("Integer: %-5d\n", num1); // 输出:123

// 使用%-8.2f打印浮点数,最小宽度为8,小数位数为2,左对齐
printf("Float: %-8.2f\n", num2); // 输出:3.14

// 使用%-c打印字符,左对齐
printf("Character: %-c\n", ch); // 输出:A

在上面的代码中,我们定义了一个整数num1,一个浮点数num2和一个字符ch。使用-标志与其他格式符一起使用,来使输出的数字或字符在域内向左靠齐。使用%-md来打印整数,其中m表示最小宽度。使用%-m.nf来打印浮点数,其中m表示最小宽度,n表示小数位数。使用%-c来打印字符。这些格式符与-标志一起使用,可以使输出的数字或字符在域内向左靠齐。


二、scanf函数输入数据

  1. 一般形式

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

  • 格式控制:

含义同 printf 函数的格式控制

  • 地址表列:

由若干个地址组成的表列,可以是变量的地址,或字符串的首地址。**& 为取地址符,字如其意在 C 语言中,scanf 函数用于从用户输入中读取数据。它的工作方式是通过引用传递来修改变量的值。为了让 scanf 函数能够修改变量的值,我们需要向其提供变量的地址。这就是在 scanf 中使用 & 的原因。****&**** 运算符用于获取变量的地址。当我们在 scanf 中使用 & 运算符时,它表示我们要将变量的地址传递给 scanf 函数,以便它可以直接修改变量的值。**示例:

1
2
3
4
int num;
scanf("%d", &num); // 使用 & 获取 num 的地址,并将用户输入的值存储在 num 中

printf("用户输入的值:%d\n", num);

在上面的示例中,**scanf 函数使用 %d 格式说明符来读取用户输入的整数。为了让 scanf 函数能够修改 num 的值,我们使用 & 运算符获取 num 的地址,并将其传递给 scanf 函数。需要注意的是,&**** 运算符只能用于具有内存地址的变量。它不能用于常量、表达式或函数调用的结果。**

  1. 格式声明

与printf函数中的格式声明相似,以 % 开始,以一个格式字符结束,中间也可以插入附加的字符。

1
2
int a;
scanf("a = %d",&a); //&为取地址符,字如其意
  • d 、i 格式符:输入有符号的十进制整数

    1
    2
    int num;
    scanf("%d", &num);
  • u 格式符:输入无符号的十进制

  • o 格式符:输入无符号的八进制

  • x 、X 格式符:输入无符号的十六进制整数(大小写作用相同)

  • e 格式符:输入单个字符

  • s 格式符:

输入字符串,将字符串送到一个字符数组中,在输入时以非空白字符开始,以第一个空白字符结束。字符串以标志 ‘\0‘ 作为其最后一个字符

  • f 格式符:输入实数,可以用小数形式或指数形式输入
  • e 、E 、g 、G 格式符:与 f 作用相同,e 与 f 、g 可以互相替换(大小写作用相同)
  • scanf 函数中用到的格式附加字符
    • l 字符:输入长整型数据如%ld、%lo、%lx、%lu 以及 double型数据类型%lf、%le
    • h 字符:输入短整型数据,如%hd、%ho、%hx
    • *** 字符:**本输入项在输入后不赋给相应的变量
    • m 字符:

m代表输入数据所占宽度(m代表一个正整数)


三、字符输入输出函数

  1. putchar 函数输出一个字符

只要是字符都可以。字符类型也属于整数类型,对照ASCII表,putchar(65); 与 putchar(A); 都输出字符 B

  • 一般形式

putchar(c); // c为一个字符

  1. getchar 函数输入一个字符
  • 一般形式
    1
    2
    char a;
    a = getchar(); // 从键盘输入一个字符,传给字符变量a

第4章 选择结构程序设计

关系、逻辑运算符

  1. 关系运算符

在C语言中,关系运算符用于比较两个值之间的关系,结果为真(非零)或假(零)。逻辑运算符用于组合多个关系表达式,结果为真或假。C语言中常用的关系运算符:

  • 相等:==
  • 不等:!=
  • 大于:>
  • 小于:<
  • 大于等于:>=
  • 小于等于:<=
  1. 逻辑运算符

逻辑运算符用于组合多个关系表达式,常用的逻辑运算符有:

  • 逻辑与:&&
  • 逻辑或:||
  • 逻辑非:!

表达式是由操作数和运算符组成的。操作数可以是变量、常量或表达式的结果,运算符用于操作操作数。通过组合运算符和操作数,可以构建复杂的表达式来进行计算和判断。例如,下面的表达式计算两个数的平均值:

1
2
3
4
5
int a = 5;
int b = 3;
int average;

average = (a + b) / 2; // 计算a和b的和,然后除以2,得到平均值

在这个表达式中,(a + b)计算了 a 和 b 的和,然后除以2得到平均值,并将结果存储在变量 average 中。


条件运算符

在C语言中,条件运算符(也称为三元运算符)是一种简洁的条件表达式,用于根据条件的真假选择不同的值。条件运算符的语法如下:
expression1 ? expression2 : expression3
其中,expression1 是一个条件表达式,其结果可以是真(非零值)或假(零值)。如果条件表达式为真,则整个表达式的值为 expression2;如果条件表达式为假,则整个表达式的值为 expression3。
条件运算符的示例:

1
2
3
4
int a = 10;
int b = 5;

int max = (a > b) ? a : b;

在上面的示例中,如果 a 大于 b,则 max 被赋值为 a 的值;否则 max 被赋值为 b 的值。


if 语句

if语句是一种条件语句,用于在满足特定条件时执行一段代码。它的基本操作包括以下几个步骤:

  1. 检查条件:首先,if语句会检查指定的条件是否为真(非零)。条件可以是一个表达式,也可以是一个变量的值。如果条件为真,即满足条件,那么将执行if语句后面的代码块;如果条件为假(即为0),那么将跳过if语句的代码块,继续执行后面的代码。
  2. 执行代码块:如果条件为真,那么将执行if语句后面的代码块。代码块是由一对花括号{}包围的一组语句组成,可以是单个语句或多个语句的集合。这些语句可以是任何合法的C语言语句,例如变量赋值、函数调用、条件语句、循环语句等。
  3. 跳过代码块:如果条件为假,那么将跳过if语句后面的代码块,继续执行后面的代码。这意味着if语句的代码块只会在条件为真时执行,否则将被忽略。

使用if语句的示例,用于判断一个数是否为正数:

1
2
3
4
int num = 5;
if (num > 0) {
printf("这个数是正的\n");
}

在这个示例中,if语句检查num > 0的条件是否为真。因为num的值为5,满足条件,所以将执行if语句后面的代码块,输出 “这个数是正的”。如果num的值为负数或零,那么条件为假,将跳过if语句的代码块,不会输出任何内容。


if 语句的三种形式

  1. if语句:

if 语句用于在条件为真时执行一段代码。语法如下:

1
2
3
if (condition) {
// 当condition为真时执行的代码
}

例如,下面的代码根据变量num的值判断是否为正数,并输出相应的信息:

1
2
3
4
5
int num = 5;

if (num > 0) {
printf("这个数是正的\n");
}

如果num的值大于0,则条件为真,将会执行printf语句输出 “The number is positive.”。

  1. if-else语句:

if-else语句用于在条件为真时执行一个代码块,否则执行另一个代码块。语法如下:

1
2
3
4
5
if (condition) {
// 当condition为真时执行的代码
} else {
// 当condition为假时执行的代码
}

例如,下面的代码根据变量num的值判断是否为正数,并输出相应的信息:

1
2
3
4
5
6
7
int num = -3;

if (num > 0) {
printf("这个数是正的\n");
} else {
printf("这个数不是正的\n");
}

如果num的值大于0,则条件为真,将会执行第一个printf语句输出 “这个数是正的”。否则,将会执行第二个printf语句输出 “这个数不是正的”。

  1. if-else if语句:

if-else if语句用于根据多个条件选择不同的代码块执行。语法如下:

1
2
3
4
5
6
7
if (condition1) {
// 当condition1为真时执行的代码
} else if (condition2) {
// 当condition2为真时执行的代码
} else {
// 当所有条件都为假时执行的代码
}

例如,下面的代码根据变量score的值判断学生的等级,并输出相应的信息:

1
2
3
4
5
6
7
8
9
10
11
int score = 85;

if (score >= 90) {
printf("Grade A\n");
} else if (score >= 80) {
printf("Grade B\n");
} else if (score >= 70) {
printf("Grade C\n");
} else {
printf("Grade D\n");
}

根据score的值,将会执行相应的printf语句输出对应的等级。在这个例子中,score的值为85,满足第二个条件score >= 80,因此将会执行第二个printf语句输出 “Grade B”。


Switch Break 语句

Switch语句是一种多分支选择结构,用于根据不同的条件执行不同的代码块。它的基本语法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
switch (表达式) {
case1:
// 如果表达式匹配值1,则执行这里的代码
break;
case2:
// 如果表达式匹配值2,则执行这里的代码
break;
case3:
// 如果表达式匹配值3,则执行这里的代码
break;
...
default:
// 如果表达式不匹配任何值,则执行这里的代码
}

switch语句的执行过程如下:

  1. 表达式求值:首先,计算表达式的值。表达式可以是一个变量或一个常量值。
  2. 匹配值:接下来,将计算得到的表达式的值与每个case语句后面的值进行比较,以确定是否匹配。
  3. 执行代码块:如果匹配成功,即表达式的值与某个case语句后面的值相等,那么将执行该case语句后面的代码块。代码块是由一对花括号{}包围的一组语句组成。
  4. 跳出switch语句:在执行完匹配的代码块后,程序会继续执行后面的代码。为了避免执行其他case语句后面的代码块,可以在每个case语句的末尾使用break语句来跳出switch语句。

如果表达式的值与任何case语句后面的值都不匹配,那么将执行default语句后面的代码块(如果有)。default语句是可选的,用于处理表达式不匹配的情况。
使用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
int month;
printf("请输入一个月份:");
scanf("%d", &month);

switch (month) {
case 1:
case 2:
case 12:
printf("冬季\n");
break;
case 3:
case 4:
case 5:
printf("春季\n");
break;
case 6:
case 7:
case 8:
printf("夏季\n");
break;
case 9:
case 10:
case 11:
printf("秋季\n");
break;
default:
printf("输入的月份无效\n");
}

第5章 循环结构程序设计

while 循环

while 循环是一种常用的循环结构。它的作用是在满足指定条件的情况下,重复执行一段代码块。以下是while循环的基本语法:

1
2
3
while (条件) {
// 要执行的代码
}

在循环开始之前,首先会对条件进行评估。如果条件为真(非零值),循环体中的代码将会被执行。执行完循环体中的代码后,再次对条件进行评估。如果条件仍然为真,循环将会继续执行,直到条件为假(零值),此时循环结束,程序继续执行循环之后的代码。以下是一个使用while循环的示例程序,用于打印从1到10的数字:

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

int main() {
int i = 1;

while (i <= 10) {
printf("%d ", i);
i++;
}

return 0;
}

在这个程序中,while (i <= 10)作为循环的条件。只要i的值小于等于10,循环体中的代码都会被执行。循环体中首先会打印当前的i的值,然后通过i++将i的值增加1,以准备下一次循环。程序会重复执行这个过程,直到i的值大于10,循环就会结束。以上程序的输出结果为:1 2 3 4 5 6 7 8 9 10。


do while 循环

C语言中的do-while语句是一种循环结构,用于重复执行一段代码块,直到满足指定的条件为止。它的基本语法如下:

1
2
3
do {
// 循环体代码
} while (条件);

do-while循环的执行过程是先执行一次循环体代码,然后再检查条件是否满足。如果条件为真,则继续执行循环体代码;如果条件为假,则退出循环。下面是一个使用do-while循环的示例程序,计算1到10的累加和:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
int main() {
int num = 1;
int sum = 0;

do {
sum += num;
num++;
} while (num <= 10);

printf("1到10的累加和是:%d\n", sum);

return 0;
}

在这个程序中,使用do-while循环计算了1到10的累加和。首先将num初始化为1,然后在循环体内将num添加到sum中,并递增num的值。当num的值小于等于10时,继续执行循环体代码。当num的值达到11时,不满足条件,退出循环。最后输出累加和的结果。


for 循环

for语句是一种常用的循环结构,用于重复执行一个代码块,可以指定起始条件、循环条件和循环迭代。它的基本语法如下:

1
2
3
for (初始化语句; 循环条件; 迭代语句) {
// 循环体代码
}

for循环的执行过程是先执行初始化语句,然后检查循环条件是否满足。如果条件为真,则执行循环体代码,并执行迭代语句;如果条件为假,则退出循环。
下面是一个使用for循环的示例程序,计算1到10的累加和:

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

int main() {
int sum = 0;

for (int num = 1; num <= 10; num++) {
sum += num;
}

printf("1到10的累加和是:%d\n", sum);

return 0;
}

在这个程序中,使用for循环计算了1到10的累加和。循环的初始化语句将num初始化为1,循环的条件是num小于等于10,循环的迭代语句是对num进行递增操作。在每次循环中,将num添加到sum中。当循环条件不满足时,即num大于10时,退出循环。最后输出累加和的结果。


break、continue作用

break语句可用于switch语句的跳出循环中断的跳出

  • break 语句

break语句用于中断当前所在的循环结构,即在满足某个条件时提前跳出循环,结束循环的执行。当break语句执行时,程序将跳出当前的循环,并执行循环后面的代码。下面是一个使用break语句的示例程序,其中使用了一个无限循环(while循环),并在满足某个条件时使用break语句跳出循环:

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

int main() {
int num;

while (1) {
printf("请输入一个整数(输入0结束循环):");
scanf("%d", &num);

if (num == 0) {
break; // 如果输入的整数是0,则跳出循环
}

printf("你输入的整数是:%d\n", num);
}

printf("循环结束!\n");

return 0;

在这个程序中,使用了一个无限循环(while循环),并在循环内部输入一个整数。如果输入的整数是0,则使用break语句跳出循环。否则,输出输入的整数。当输入的整数为0时,循环将被提前结束,跳出循环,并执行后续的代码。

  • continue 语句

continue语句用于终止当前循环迭代中的剩余语句,并进入下一次循环迭代。也就是说,当程序执行到continue语句时,会跳过当前循环迭代剩余的代码,然后继续下一次迭代。通过使用一个for循环,找出一个整数数组中的偶数,并输出这些偶数:

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

int main() {
int numbers[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

for (int i = 0; i < 10; i++) {
// 如果是奇数,则跳过当前循环迭代,继续下一次迭代
if (numbers[i] % 2 != 0) {
continue;
}

printf("%d ", numbers[i]);
}

return 0;
}

在这个示例中,使用一个for循环迭代整数数组numbers。当遇到奇数时,使用continue语句跳过当前迭代,继续下一次迭代。只有当遇到偶数时,才会执行输出语句。因此,程序输出结果为:2 4 6 8 10


第6章 利用数组处理批量数据

数组

  1. 数组的定义

C语言中的数组,可用于存储一系列相同类型的元素。数组可以在内存中连续存储多个元素,并*通过索引(或者叫下标)来访问和操作这些元素。数组的定义形式:***数据类型 数组名[数组长度];**数据类型指定了数组中元素的类型,数组名是数组的标识符,数组长度指定了数组可以存储的元素个数。定义一个包含5个整数的数组:
int numbers[5];


  1. 数组的初始化

数组的初始化可以在定义时进行,也可以在后续的代码中进行。

  • 静态初始化: 在定义时就定义了所有数据

int numbers[5] = {1, 2, 3, 4, 5};

  • 动态初始化: 先定义数组,再后续定义数据
    1
    2
    3
    4
    5
    6
    int numbers[5];
    numbers[0] = 1;
    numbers[1] = 2;
    numbers[2] = 3;
    numbers[3] = 4;
    numbers[4] = 5;
    数组的长度是固定的,一旦定义后就不能改变。 如果需要存储更多的元素,可以重新定义一个更大长度的数组,并将原数组中的元素复制到新数组中。

  1. 数组的访问

在C语言中,可以使用索引(或者叫下标)来访问数组中的元素。数组的索引(或者叫下标)从0开始递增,表示数组中元素的位置。*访问数组元素的方式为:***数组名[索引]**
例如,对于一个包含5个整数的数组 numbers,可以使用索引0、1、2、3、4来表示数组中的第1个、第2个、第3个、第4个和第5个元素。

1
2
3
4
5
6
int numbers[5] = {1, 2, 3, 4, 5};

int x = numbers[0]; // 访问数组中的第1个元素,x的值为1
int y = numbers[2]; // 访问数组中的第3个元素,y的值为3

numbers[1] = 10; // 修改数组中的第2个元素,将其值改为10

注意:数组的索引必须在合法的范围内,即从0到数组长度减1如果使用超出范围的索引来访问数组元素,将会导致未定义的行为,可能会访问到无效的内存地址


一维数组

一维数组是由一组具有相同数据类型的元素组成的,这些元素按照一维的方式排列在连续的内存空间中。一维数组可以看作是一个线性的数据结构,可以通过索引来访问和操作数组中的元素。刚刚对于数组的解释全都是属于一维数组的范畴


二维数组

二维数组是一种特殊的数组,它包含一组具有相同数据类型的元素,这些元素按照二维的方式排列在连续的内存空间中。二维数组可以看作是一个表格或矩阵,它由行和列组成。在C语言中,二维数组可以使用两个索引(或者叫下标)来访问和操作数组中的元素,一个索引(或者叫下标)表示行,另一个索引(或者叫下标)表示列。

  1. 二维数组的定义

*二维数组的定义形式:***数据类型 数组名[行数][列数];**数据类型表示数组中元素的数据类型,数组名是一个标识符用于表示数组的名称,行数表示二维数组的行数,列数表示二维数组的列数。定义一个包含3行4列的二维数组的语句如下:
int matrix[3][4];


  1. 二维数组的初始化
  • 静态初始化:
    在定义时就定义了所有数据

    1
    int str[2][3] = {{1,2,3},{4,5,6}};
  • 将所有数据写在同一花括号内:

系统按照下标自动自动分配行列

1
2
int str[2][3] = {1,2,3,4,5,6};	
int str[2][3] = {{1,2,3},{4,5,6}}; //两者均表示一个意思
  • 只对部分元素赋值:

未赋值的元素自动赋值为 0

1
2
3
4
int str[2][3] = {{1},{4}};
//这样表示的
1 0 0
4 0 0
  • 定义时,可以对第一维不指定,但第二维必须指定:
    1
    2
    3
    int str[2][3] = {1,2,3,4,5,6};
    //等价于
    int str[][3] = {1,2,3,4,5,6}; //第二维为三,总共有6个数据,所以第一维自然为二

  1. 二维数组的访问

在C语言中,可以使用索引(或者叫下标)来访问数组中的元素。数组的索引(或者叫下标)从0开始递增,表示数组中元素的位置。*访问数组元素的方式为:***数组名[索引][索引]**初始化一个包含3行4列的二维数组的语句如下:

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

二维数组中的元素可以通过两个索引来访问和操作。第一个索引表示行,第二个索引表示列。索引从0开始递增,表示数组中元素的位置。例如,访问二维数组中的第2行第3列的元素的语句如下:
int x = matrix[1][2]; //表示二维数组 matrix 中的第2行第3列的元素。
需要注意的是,二维数组的行数和列数是固定的,一旦定义后就不能改变。如果需要存储更多的元素,可以重新定义一个更大行数和列数的二维数组,并将原数组中的元素复制到新数组中。
二维数组以上的多维数组同理,大差不错


1
2
3
4
5
6
7
str[1] = 'e';
str[2] = 'l';
str[3] = 'l';
str[4] = 'o';
str[5] = '\0'; //添加结束字符'\0',表示字符串结束,马上就讲到

printf("%s",str);

字符数组(字符串)变量 str,它有容量为5个字符。接下来,通过 str[0] 到 str[4] 分别赋值为字符’H’、’e’、’l’、’l’、’o’,这样就形成了一个字符串”Hello”。C语言数组的索引是从0开始的,所以str[0]表示数组中的第一个元素,即字符串的第一个字符。每个字符都用单引号括起来。


  1. 字符数组的初始化
  • 静态初始化:

直接定义好,再一一配队

1
2
3
char str[5] = {'H','e','l','l','o'};
//两者同一个意思
char str[] = {'H','e','l','l','o'};

大括号内的值按照顺序依次赋给数组中的元素。如果数组的长度小于大括号内的元素个数,会产生编译错误。如果数组的长度大于大括号内的元素个数,则剩余的元素会被自动初始化为0。


  1. 字符数组的访问

访问字符数组时,可以使用索引来访问数组中的每个字符。索引从0开始,递增到数组长度减1。示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
char str[] = "Hello";

// 访问单个字符
char firstChar = str[0]; // 获取第一个字符 'H'
printf("第一个字符:%c\n", firstChar);

// 修改单个字符
str[0] = 'h'; // 将第一个字符修改为 'h'
printf("修改后的字符串:%s\n", str); // 输出 "hello"

// 遍历字符数组
int length = sizeof(str) / sizeof(char); // 计算字符数组的长度
printf("字符数组长度:%d\n", length);

for (int i = 0; i < length - 1; i++) {
printf("%c ", str[i]);
}
printf("\n"); // 输出 "h e l l o"

在上面的示例中,我们首先通过索引来访问字符数组 str 中的单个字符。通过使用 str[0],我们可以获取第一个字符 ‘H’。然后,我们将 str[0] 的值修改为 ‘h’,从而将第一个字符改为小写接下来,我们使用一个循环来遍历整个字符数组。我们通过计算字符数组的长度(使用 sizeof 运算符)来确定循环的终止条件。需要注意的是,我们减去1是为了避免访问到字符串的结束标志 ‘\0’。在循环中,我们使用索引 i 来访问字符数组中的每个字符,并使用 printf 函数打印出来。最终,我们得到了字符数组中的每个字符。


  1. 字符串和字符串结束标志
  • 字符串结束标志

C语言规定,以字符 ‘\0’ 作为结束标志。如果字符数组中存在若干个字符,前面9个字符都不是空字符(’\0’),而第10个字符是 ‘\0’ ,则认为数组中有一个字符串,其有效字符为9个。意为,在遇到字符 ‘\0’ 时,表示字符串结束,把它前面的字符组成一个字符串
了解C语言字符串处理后,字符数组初始化方法可以多一种——字符串常量字符数组初始化

1
2
3
char str[] = {"Hello world"};
//同理
char str[] = "Hello world";

  1. 字符数组的输入输出
  • 逐个字符输入输出,用格式符 ‘%c’:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    char str[20];
    int i;

    printf("请输入一个字符串:");
    for(i = 0; i < 20; i++) {
    scanf("%c", &str[i]);
    if(str[i] == '\n') {
    str[i] = '\0'; // 将换行符替换为字符串结束符
    break;
    }
    }
    printf("输入的字符串为:%s\n", str);

    printf("逐个字符输出字符串:");
    for(i = 0; str[i] != '\0'; i++) {
    printf("%c ", str[i]);
    }

    // 输出: 请输入一个字符串:Hello World 输入的字符串为:Hello World 逐个字符输出字符串:H e l l o W o r l d
  • 将整个字符串一次性输入输出,用格式符 ‘%s’:

    1
    2
    3
    4
    5
    char str[20];
    printf("请输入一个字符串:");
    scanf("%s", str);
    printf("输入的字符串为:%s\n", str);
    // 输出: 请输入一个字符串:Hello World 输入的字符串为:Hello

  1. 字符串处理函数
  • puts 函数——输出字符串函数

将一个字符串(以 ‘\0’ 结束的字符序列)输出到终端

1
2
char str[10] = "China";
puts(str);

输出:
China
同时,puts函数输出的字符串可以包含转义字

1
2
char str[] = "Hello\nChina";
puts(str);

输出:

1
2
Hello
China

在用puts输出时将字符串结束标志’\0’转换成’\n’,即输出完字符串后换行


  • gets 函数——输入字符串函数

从终端输入一个字符串到字符数组,并且得到一个返回值。返回值:如果成功,返回输入的字符串;如果发生错误或到达文件结束,返回 NULL。

1
2
char str[100];
gets(str); \\假定输入China

输出:
China
对于输入 “China”,由于最后的换行符也会被存储在数组中,所以实际上是存储了 6 个字符:’C’、’h’、’i’、’n’、’a’ 和换行符。


  • strcat函数——字符串连接函数

一般形式:
strcat(字符数组1,字符数组2); //把字符串2连接到字符串1的后面
示例:

1
2
3
4
5
char destination[20] = "Hello";
char source[] = " World!";

strcat(destination, source);
printf("%s\n", destination);

输出:
Hello world!


  • strcpy、strncpy函数——字符串复制函数
  • strcpy函数

一般形式:
strcpy(字符数组1,字符数组2); //将字符串2复制到字符串1中去
示例:

1
2
3
char str1[10], str2[] = "China";
strcpy(str1,str2);
printf("%s",str1);

输出:
China


  • strncpy函数

strncpy函数用于将一个字符串的指定长度复制到另一个字符串中。一般形式:
strncpy(字符串1, 字符串2, 要复制的字符数);
strncpy函数会将源字符串的指定长度的字符复制到目标字符串中,如果源字符串的长度小于指定的字符数,则目标字符串将以结束符\0填充到指定字符数。如果源字符串的长度大于指定的字符数,则目标字符串将不会以结束符\0结尾。需要注意的是,strncpy函数不会自动在目标字符串的末尾添加结束符\0,因此在使用strncpy函数后,需要手动将结束符\0添加到目标字符串的末尾。示例:

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

int main() {
char str1[10];
char str2[] = "Hello, World!";

strncpy(str1, str2, sizeof(str1)-1);
str1[sizeof(str1)-1] = '\0'; // 手动添加结束符\0

printf("strncpy: %s\n", str1);

return 0;
}

输出结果为:
strncpy: Hello, Wo
在这个示例中,我们定义了一个长度为10的字符数组str1,并初始化为空字符串。我们还定义了一个字符数组str2,并初始化为”Hello, World!”。使用strncpy函数将str2字符串的前9个字符复制到str1字符串中。由于指定了目标字符串的最大长度为10,strncpy函数复制了str2字符串的前9个字符到str1字符串中,但没有添加结束符\0。因此,我们需要手动将结束符\0添加到str1字符串的最后。最后,通过printf函数打印str1字符串,输出结果为”Hello, Wo”。


  • strcmp函数——字符串比较函数

一般形式:
strcmp(字符串1, 字符串2);
比较字符串1和字符串2,例如:

1
2
3
4
5
char str1 = "Hello";
char str2 = "world";
strcmp(str1,str2);
strcmp("China","Korea");
strcmp("China",str1);

比较规则:将两字符串自左至右逐个字符相比(按ASCII码值大小比较),直到出现不同的字符或遇到 ‘\0’为止

  • 如全部字符相同,则认为两字符串相等
  • 若出现不相同的字符,则以第1对不相同的字符的比较结果为准。

eg:”DOG”>”cat”
比较的结果由函数值带回:

  • 如字符串1与字符串2相同,则函数值为0
  • 如字符串1>字符串2,则函数值为一个正整数
  • 如字符串1<字符串2,则函数值为一个负整数

  • strlen 函数——测字符串长度的函数

一般形式:
strlen(字符数组);函数的值为字符串中的实际长度(不包括 ‘\0’ 在内)

1
2
char str[10] = "China";
printf("%d",strlen(str));

输出的结果不是10,也不是6,而是5


  • strlwr 函数——转换为大写的函数

一般形式:
strlwr(字符串);其函数的作用是将字符串中大写字母换成小写字母


  • strupr 函数——转换为大写的函数

一般形式:
strupr(字符串);其函数的作用是将字符串中小写字母换成大写字母


第7章 用函数实现模块化程序设计

为什么要用函数

模块化程序设计的思路模块化程序设计的思路是将一个复杂的程序拆分成独立的、可重用的模块,每个模块负责实现一个特定的功能或完成一个具体的任务。例如用sin函数实现求一个数的正弦值,需要用时,直接在程序中写上sin(a)来调用系统函数库中的函数代码,执行这些代码,得到结果。好处:C语言中模块化程序设计可以使代码结构更清晰、减少代码的复杂性、提高代码的复用性、方便调试和测试,并且提高代码的可维护性。


定义函数

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

  1. 定义函数的规则:
  • 指定函数的名字,以便以后按名调用
  • 指定函数的定义,即函数返回值的类型
  • 指定函数的参数的名字和类型,以便在调用函数时向它们传递数据,对无参函数不需要这个
  • 指定函数应当完成什么操作,即函数的功能

  1. 定义函数的方法:
  • 定义无参函数

函数名后面的括号中是空的,没有任何参数一般形式:

1
2
3
4
5
6
7
8
9
类型名 函数名()
{
函数体
}
// 或者
类型名 函数名(void)
{
函数体
}

示例:

1
2
3
4
void hello() {
printf("Hello, World!\n");
}
// 调用hellp函数只会输出:Hello, World!

  • 定义有参函数

函数名后面的括号中就有了形参(形式参数表列)一般形式:

1
2
3
4
类型名 函数名(形参)
{
函数体
}

  • 定义空函数

程序设计中,有时会用到一般形式:

1
2
类型名 函数名()
{ }

空函数在初学者学习编程的过程中可能没有太多直接的用途,因为它们没有实际完成任何具体的操作或功能然而,空函数在编程中仍然有一些可以帮助初学者的用途:

  • 框架搭建:在程序的早期阶段,可以使用空函数作为某个功能的占位符。这样,你可以在函数的基础上构建程序的其他部分,并在后续逐步填充具体的功能。
  • 接口一致性:在学习面向对象编程时,接口是一种规定了类中公共方法的方式。如果你正在实现一个接口,并且某个方法对于你来说暂时没有具体的实现逻辑,你可以将其定义为空函数。这样,你可以确保你的代码与接口的规范保持一致,然后逐步实现其他相关功能。
  • 调试与测试:在调试或测试过程中,你可能需要模拟某个函数的行为,而不需要实际执行任何操作。你可以将该函数定义为空函数,以便在调试和测试过程中临时禁用该函数的功能。

调用函数

一般形式:
函数名 (实参列表);
如果是调用无参列表,则“实参列表”可以没有,但括号不能省略如果“实参列表”包含多个实参,则各参数间用逗号隔开

  1. 函数调用的3种形式:
  • 函数调用语句:

把函数调用单独作为一个语句。
printf_ns();

  • 函数表达式:

是表达式的一部分,来参与表达式的运算
c = 2 * max(a,b); //求出a,b之间的最大值,再与2进行计算

  • 函数参数:

函数调用作为另一个函数调用时的参数
m = max(a,max(b,c));


  1. 函数调用时的数据传递
  • 形式参数和实际参数

形式参数:再定义函数时函数名后面括号中的变量名称 为“形式参数”(简称“形参”)“虚拟参数”实际参数:再主调函数中调用一个函数时,函数名后面括号中的参数称为“实际参数”(简称“实参”)使用形式参数和实际参数的代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
// 定义一个函数,使用形式参数
void printNumber(int num) {
printf("The number is: %d\n", num);
}
int main() {
int number = 10;

// 调用printNumber函数,传递实际参数
printNumber(number);

return 0;
}

定义了一个函数printNumber,它接受一个整数形式参数num。在main函数中,我们声明了一个整数变量number并赋值为10。然后,调用printNumber函数,并将number作为实际参数传递给该函数。在函数调用过程中,实际参数number的值被复制到形式参数num中。在printNumber函数内部,我们可以使用形式参数num来访问并打印传递的实际参数的值。输出结果为:
The number is: 10
在这个例子中,形式参数num是函数printNumber的虚拟参数,它在函数定义时被声明。而实际参数number是在调用函数时传递给函数的真实数据。

  • 实参和形参间的数据传递

再调用函数过程中,系统会把实参的值传递给被调用函数的形参。或者说,形参从实参得到一个值。该值再函数调用期间有效,可以参加该函数中的运算。使用实参传递给形参的C语言代码示例:

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

// 定义一个函数,使用形式参数进行运算
int addNumbers(int num1, int num2) {
int sum = num1 + num2;
return sum;
}

int main() {
int number1 = 10;
int number2 = 20;

// 调用addNumbers函数,将number1和number2作为实际参数传递
int result = addNumbers(number1, number2);

printf("The sum is: %d\n", result);

return 0;
}

定义了一个函数addNumbers,它接受两个整数形式参数num1和num2。在main函数中,我们声明了两个整数变量number1和number2,并分别赋值为10和20。然后,我们调用addNumbers函数,并将number1和number2作为实际参数传递给该函数。在函数调用过程中,实际参数number1和number2的值被传递给形式参数num1和num2。在addNumbers函数内部,我们使用形式参数num1和num2进行加法运算,并将结果保存在sum变量中。最后,我们将sum作为返回值返回给调用者。在main函数中,我们将addNumbers函数的返回值保存在result变量中,并打印出来。输出结果为:
The sum is: 30

在例子中,实参number1和number2的值被传递给形参num1和num2,形参在函数调用期间有效,并参与了函数内部的加法运算。最后,函数的返回值被传递给了result变量。


  1. 函数调用的过程
  • 在定义函数中指定的形参,在未出现函数调用时,它们并不占用内存中的储存单元。在发生函数调用时,函数的形参才被临时分配内存单元
  • 将实参的值传递给对应形参,让形参得到实参的值。在执行函数期间,由于形参已经有值,就可以利用形参进行有关的运算
  • 通过 return 语句(后面会讲)将函数值带回到主调函数。执行 return 语句就把这个函数返回值带回主调函数 main。

注意,返回值的类型应与函数类型一致。如果函数不需要返回值,则不需要 return 语句。这时函数应该是 void 类型。

  • 调用结束,形参单元被释放。但实参单元仍保留并维持原值,没变。如果在执行一个被调用函数时,形参的值发生改变,不会改变主调函数的实参值。

示例:

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

// 函数定义,计算两个整数的和
int add(int a, int b) {
int sum = a + b;
return sum;
}

int main() {
int num1 = 10;
int num2 = 20;

// 函数调用,将num1和num2作为实参传递给add函数
int result = add(num1, num2);

// 打印结果
printf("The sum is: %d\n", result);

return 0;
}

在上面的代码中,首先定义了一个函数add,该函数接受两个整数作为参数,并返回它们的和。然后在main函数中声明了两个整数变量num1和num2,并将它们作为实参传递给add函数进行调用。调用结束后,将返回的结果赋值给变量result,并通过printf函数打印出来。执行上述代码,输出结果为The sum is: 30,说明函数调用和返回值传递的过程是正确的。


  1. 函数的返回值

希望通过函数调用时主调函数能得到一个确定的值,这就是函数值(函数的返回值)

  • 函数的返回值是通过函数中的 return 语句获得的
  • 函数值的类型
  • **在定义函数时指定的函数类型一般应该和 return语句中的表达式类型一致 **

代码示例,演示了函数的返回值的获取和类型的一致性:

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
#include <stdio.h>

// 函数定义,计算两个整数的和
int add(int a, int b) {
int sum = a + b;
return sum;
}

// 函数定义,计算两个浮点数的平均值
float average(float a, float b) {
float avg = (a + b) / 2;
return avg;
}

int main() {
int num1 = 10;
int num2 = 20;

// 函数调用,将num1和num2作为实参传递给add函数,并获取返回值
int result = add(num1, num2);

// 打印结果
printf("The sum is: %d\n", result);

float num3 = 2.5;
float num4 = 3.5;

// 函数调用,将num3和num4作为实参传递给average函数,并获取返回值
float avgResult = average(num3, num4);

// 打印结果
printf("The average is: %.2f\n", avgResult);

return 0;
}

在上面的代码中,定义了两个函数add和average,分别用于计算整数的和和浮点数的平均值。在main函数中,分别调用了这两个函数,并通过return语句获取了它们的返回值。注意,add函数返回的是int类型的值,而average函数返回的是float类型的值。执行上述代码,输出结果为:

1
2
The sum is: 30
The average is: 3.00

说明函数的返回值获取和类型的一致性是正确的。


函数递归

该知识点难理解,强烈建议结合例子习题理解C语言允许在调用一个函数的过程中有出现直接或间接地调用该函数本身,这种行为称为函数的递归调用在C语言中,函数递归的实现需要满足以下几个要点:

  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
#include <stdio.h>

// 递归函数,计算阶乘
int factorial(int n) {
// 基本情况:当n为0或1时,直接返回1
if (n == 0 || n == 1) {
return 1;
}

// 递归调用:将问题规模缩小,计算n的阶乘需要先计算(n-1)的阶乘
return n * factorial(n - 1);
}

int main() {
int num = 5;

// 调用递归函数计算阶乘
int result = factorial(num);

// 设置中文环境
setlocale(LC_ALL, "");

// 打印结果
wprintf(L"%d的阶乘是:%d\n", num, result);

return 0;
}

在上面的代码中,定义了一个递归函数factorial,用于计算一个整数的阶乘。在函数中,首先判断输入的整数是否为0或1,如果是,则直接返回1,作为递归终止条件。否则,通过递归调用自身,将问题规模缩小为计算(n-1)的阶乘,并将结果乘以n,最终得到n的阶乘。在main函数中,我们使用setlocale函数将程序的本地化环境设置为中文环境,以支持中文输出。然后使用wprintf函数来输出中文字符串,其中的L前缀表示宽字符字符串。执行上述代码,输出结果为:
5的阶乘是:120
说明递归函数成功地计算了5的阶乘。需要注意的是,在实际应用中,递归函数可能会因为递归的深度过大而导致栈溢出,因此在设计递归函数时需要注意递归的终止条件和问题规模的缩小,以避免出现问题。

数组作为函数参数

数组元素也可以用作函数实参,用法与变量一致,向形参传递数组元素的值。此外,数组名也可以作实参和形参,传递的事数组第一个元素的地址。

  • 数组元素作函数参数

数组元素可以用作函数实参,但是不能用作形参。因为形参是在函数被调用时临时分配储存单元的,不可能作为一个数组元素单独分配储存单元(数组是一个整体,在内存中占连续的一段储存单元)。在用数组元素作函数实参时,把实参的值传给形参,是“值传递”方式。数据传递的方向是从实参传到形参,单向传递。示例:

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

void changeValue(int num) {
num = 10;
}

int main() {
int array[5] = {1, 2, 3, 4, 5};

printf("调用函数前: %d\n", array[0]);
changeValue(array[0]);
printf("调用函数后: %d\n", array[0]);

return 0;
}

在上面的例子中,我们定义了一个数组 array,并将其第一个元素传递给函数 changeValue。函数 changeValue 将形参 num 的值改为 10。然而,在函数调用结束后,数组 array 的第一个元素的值并没有改变。这是因为在函数调用时,只是将数组元素的值传递给了函数的形参,而不是直接传递数组元素本身。因此,对形参的修改不会影响到原始数组。输出结果:

1
2
1
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
#include <stdio.h>

void printArray(int arr[], int size) {
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}

void changeArray(int arr[], int size) {
for (int i = 0; i < size; i++) {
arr[i] = arr[i] * 2;
}
}

int main() {
int array[5] = {1, 2, 3, 4, 5};

printf("调用函数前:");
printArray(array, 5);

changeArray(array, 5);

printf("调用函数后:");
printArray(array, 5);

return 0;
}

在上面的例子中,我们定义了两个函数:printArray 和 changeArray。printArray 函数用于打印数组的元素,接受一个整数数组和数组大小作为参数。changeArray 函数用于将数组的每个元素乘以2,同样接受一个整数数组和数组大小作为参数。
在 main 函数中,我们声明了一个大小为5的整数数组 array,并将其初始化为 {1, 2, 3, 4, 5}。首先,我们调用 printArray 函数打印数组的元素,然后调用 changeArray 函数修改数组的元素。最后,我们再次调用 printArray 函数打印修改后的数组元素。
在函数调用时,我们将数组名 array 作为实参传递给了函数的形参,即 printArray(array, 5) 和 changeArray(array, 5)。由于数组名传递的是数组首元素的地址,函数在接收到数组名后,可以通过指针的方式访问数组的元素,并对其进行操作。输出结果:调用函数前:1 2 3 4 5调用函数后:2 4 6 8 10


局部变量和全局变量

定义变量可能有3种情况:

  1. 在函数的开头定义
  2. 在函数内的复合语句内定义
  3. 在函数的外部定义
  4. 作用域

在一个函数中定义的变量,在其他函数中能否被引用?在不同位置定义的变量,在什么范围内有有效?这就是变量的作用域的问题,每个变量都有一个作用域问题,即它们在什么范围内有效。变量的作用域从空间的角度来观察,可分为全局变量和局部变量


  1. 局部变量

局部变量是在函数或者语句块内部定义的变量,它的作用域仅限于定义它的函数或者语句块内部。局部变量在函数或者语句块执行结束后会被销毁,其占用的内存空间也会被释放。
例如,下面是一个定义局部变量的示例:

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

void myFunction() {
int x; // 定义一个局部变量x
x = 10; // 赋值给x
printf("x = %d\n", x); // 输出x的值
}

int main() {
myFunction();
return 0;
}

在上面的示例中,x是一个局部变量,它只在myFunction函数内部有效。当myFunction函数执行完毕后,变量x会被销毁。局部变量的作用域仅限于定义它的函数或者语句块内部。这意味着在其他函数或者语句块中无法访问到局部变量。例如,在上面的示例中,如果在main函数中尝试访问变量x,会导致编译错误。局部变量的生命周期与其作用域相对应。当程序执行到定义局部变量的语句时,会为变量分配内存空间;当变量的作用域结束时,会释放该内存空间。


  1. 全局变量

全局变量是在函数外部定义的变量,它的作用域从定义点开始,直到文件结束。全局变量可以在整个程序中被访问和使用。在C语言中,全局变量的定义通常在所有函数的外部,即在函数之外。全局变量的定义可以在任何函数之前,但是只能定义一次。
例如,下面是一个定义全局变量的示例:

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

int globalVariable; // 定义一个全局变量

void myFunction() {
globalVariable = 10; // 在函数内部访问和修改全局变量
printf("globalVariable = %d\n", globalVariable); // 输出全局变量的值
}

int main() {
myFunction();
return 0;
}

在上面的示例中,globalVariable是一个全局变量,它可以在myFunction函数内部访问和修改。全局变量的值在整个程序中都是可见的。全局变量的作用域从定义点开始,直到文件结束。这意味着在整个程序中的任何函数中都可以访问和使用全局变量。例如,在上面的示例中,如果在main函数中尝试访问全局变量globalVariable,是完全合法的。全局变量的生命周期与整个程序的运行时间相对应。当程序开始执行时,会为全局变量分配内存空间;当程序结束时,会释放该内存空间。


变量的存储方式和生存期

变量的作用域从存在的时间(即生存期)来观察,可分为静态存储方式、动态存储方式

  • 静态、动态存储方式:

静态存储方式是指在程序运行期间由系统分配固定的存储空间的方式动态存储方式是在程序运行期间根据需要进行动态的分配存储空间的方式
在内存中的供用户使用的存储空间可分为三部分:(1)程序区;(2)静态存储区;(3)动态存储区。
数据分别放在静态存储区和动态存储区中:

  • 全局变量全部存放在”静态存储区“中,在程序开始执行时给全局变量分配存储区,程序执行完毕就释放。

在程序执行过程中它们占据固定的存储单元,而不是动态地进行分配和释放

  • 在”动态存储区“中存放以下数据:

(1)函数形式参数。在调用函数时给形参分配存储单元(2)函数中定义的没有用关键字static声明的变量,即自动变量(后面马上讲)(3)函数调用时的现场保护和返回地址等


局部变量的存储类别

在定义和声明变量和函数时,一般应同时指定其数据类型和存储类别,也可以采用默认方式指定(就是用户不指定,系统会隐含地指定为某一种存储类别)根据变量的存储类别,可以知道变量的作用域和生存区。C语言的存储变量分为自动变量(auto)、静态变量(static)、寄存器变量(register)、外部变量(extern)** 局部变量分为以下三种:**

  • 自动变量(auto变量)

在函数中的局部变量,如果不专门声明为 static(静态)存储类别,都是动态的分配存储空间的,数据存储在动态存储区中。函数中的形参和在函数中定义的局部变量(包括在复合语句中定义的局部变量),都属于此类。调用该函数时,系统会给这些变量分配储存空间。在函数调用结束时就自动释放这些存储空间。因此这类局部变量称为自动变量实际上,关键字 auto可以省略,不写 auto 则隐含指定为“自动存储类别”,它属于动态存储方式。

1
2
3
4
void f() {
auto int x = 10;
// ...
}

  • 静态局部变量(static局部变量)

有时希望函数中的局部变量的值在函数调用结束后不消失,而继续保留原值,其占用的储存单元不释放,在下一次在调用该函数时,该变量已有值(就是上一次调用结束时的值)。此类局部变量叫静态局部变量。

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

void increment() {
static int count = 0; // 静态局部变量
count++;
printf("Count: %d\n", count);
}

int main() {
increment(); // 输出 Count: 1
increment(); // 输出 Count: 2
increment(); // 输出 Count: 3
return 0;
}

在上面的代码中,increment函数内部定义了一个静态局部变量count,并初始化为0。每次调用increment函数时,count的值都会自增,并通过printf函数打印出来。由于count是静态局部变量,它的值在函数调用之间保持不变,即使increment函数被多次调用,count的值也会保持累加。因此,上述代码的输出结果将是:

1
2
3
Count: 1
Count: 2
Count: 3

  • 寄存器变量(register 变量)

一般地,变量(包括静态存储方式和动态存储方式)的值是存放在内存中的。如果有一些变量使用频繁,为提高执行效率,允许将局部变量的值存放在CPU中的寄存器中。需要用时直接从寄存器取出参加运算,不必再到内存中去存取。以提高执行效率。这种变量叫做寄存器变量。实际上使用不多,知道有这种类别即可

全局变量存储类别

全局变量都是存放在静态存储区中的。因此它们的生存期是固定的,存在于程序的整个运行过程。

  • 在一个文件内拓展外部变量的作用域

如果外部变量不在文件的开头定义,其有效地作用范围值限于定义处到文件结束。在定义点之前的函数不能引用该外部变量。如果由于某种考虑,在定义之前的函数需要引用该外部变量,则应该在应用之前用关键字 extern 对该变量作“外部变量声明”,表示把该外部变量的作用域扩展到此位置。有了此声明,就可以从“声明”处起,合法地使用外部变量下面是一个简单的C语言代码例子,展示了如何使用extern关键字声明变量和函数:在文件a.c中定义全局变量x:

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

int x = 10; // 定义全局变量x

void foo(); // 声明函数foo

int main() {
foo(); // 调用函数foo
printf("x = %d\n", x); // 打印全局变量x的值
return 0;
}

在文件b.c中使用extern关键字声明变量x,并定义函数foo:

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

extern int x; // 使用extern关键字声明全局变量x

void foo() {
printf("Inside foo: x = %d\n", x); // 打印全局变量x的值
x = 20; // 修改全局变量x的值
}

在上述代码中,文件a.c中定义了一个全局变量x,并声明了函数foo。文件b.c中使用extern关键字声明了变量x,并定义了函数foo。在文件a.c的main函数中,首先调用了函数foo,然后打印了全局变量x的值。在函数foo中,打印了全局变量x的值,并修改了其值为20。在编译和链接这两个文件时,编译器会在链接阶段把文件b.c中的变量x和函数foo与文件a.c中的引用关联起来,从而实现了在不同文件中共享变量和函数的功能。

  • 将外部变量的作用域拓展找其他文件

在文件a.c中定义外部变量x:

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

int x = 10; // 定义外部变量x

int main() {
printf("x = %d\n", x); // 打印外部变量x的值
return 0;
}

在文件b.c中使用extern关键字引用外部变量x:

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

extern int x; // 使用extern关键字引用外部变量x

void foo() {
printf("Inside foo: x = %d\n", x); // 打印外部变量x的值
x = 20; // 修改外部变量x的值
}

在上述代码中,文件a.c中定义了一个外部变量x,并在main函数中打印了外部变量x的值。文件b.c中使用extern关键字引用了外部变量x,并在函数foo中打印了外部变量x的值,并修改了其值为20。在编译和链接这两个文件时,编译器会在链接阶段将文件b.c中的引用与文件a.c中的定义关联起来,从而实现了在不同文件中共享外部变量的功能。

外部函数与内部函数

  1. 内部函数

如果一个函数只能被本文件其他函数所调用,它称为内部函数。在定义内部函数时,在函数名和函数类型的前面加 static 即可
static 类型名 函数名(形参表);
例如,函数的首行:

1
2
static int fun(int a,int b);
//表示fun是一个内部函数,不能被其他文件调用

内部函数又称静态函数,因为它是用 static声明的。使用内部函数,可以使函数的作用域只局限于所在文件。这样,在不同的文件中即使有同名的内部函数,也互不干扰,不必担心所用函数是否会与其他文件模块中的函数同名。

  1. 外部函数

如果在定义函数时,在函数首部的最左段加关键字extern,则此函数时外部函数,可供其他文件调用。
extern int fun(int a,int b);
C语言规定,如果在定义函数时省略 extern,则默认为外部函数。


第8章 善于利用指针

本章节是C语言的重难点,多看,多练

指针的含义

  1. 数据在内存中的存储与读取

在程序中定义了一个变量,在对程序进行编译时,系统给这个变量分配内存单元。不同变量分配不同长度空间,在VS中,整型变量(int)分配4个字节,双精度浮点型变量(double)也分配8个字节…内存区的每一个字节有一个编号,这就是“地址”,相当与酒店里的房间号。在地址所标志的内存单元中存放的数据则是相当于酒店房间中居住的旅客。由于通过地址能找到所需的变量单元,即为地址指向该变量单元把地址形象化地称为“指针”,能找到以它为地址的内存单元。

  1. 直接访问与间接访问

直接访问:直接访问是指直接使用变量名来访问变量的值。例如,int num = 10; 定义了一个整型变量num,我们可以直接使用num来访问和修改它的值。间接访问:间接访问是通过指针来访问变量的值。指针存储了一个变量的地址,通过指针我们可以间接地访问和修改该地址处的数据。例如,int _ptr = __#__(_、&符号后面马上讲到)将整型变量num的地址赋值给指针变量ptr,然后我们可以使用*ptr来访问和修改num的值。


举例:为将数值3送到变量中,有两种表达方式:(1)将3直接送到变量 i 所标识的单元中,如“ i = 3; ”。(2)将3送到变量 i_pointer 所指向的单元(即变量 i 的存储单元),如“ *i_pointer = 3; ”, 其中 *i_pointer 表示 i_pointer指向的对象指向就是通过地址来体现的,由于通过地址能找到所需的变量单元。因此说,地址指向该变量单元(如房间号指向或代表着一个房间一样,将地址形象化地称为指针)


指针变量

如果有一个变量专门来存放另一变量的地址(即指针),则它称为“指针变量”。指针变量就是地址变量,用来存放地址,指针变量的值就是地址

  1. 定义指针变量
    1
    2
    3
    类型名 *指针变量名;
    int *pointer;
    //"*"的位置无所谓,靠左int* pointer靠右int *pointer在中间int * pointer都行
  • 上例,左端的 int是在定义指针变量时必须指定的”基类型“,用来指定次指针变量可以指向的变量的类型

如上例 int *pointer只能用于指向整型。


  1. 引用指针变量
  • 给指针变量赋值

    1
    2
    int a = 5;
    int *p = &a;

    定义了一个整型变量a,并赋值为5。然后定义了一个整型指针p,并将a的地址赋给p。这样,p就指向了a的内存地址。**&符号在C语言中表示取地址操作符。它用于获取变量的内存地址。在这段代码中,&a表示获取变量a的内存地址,并将该地址赋给指针变量p**

  • 引用指针变量指向的变量

    1
    2
    3
    4
    5
    6
    int a = 5;
    int *p = &a;
    printf("%d",*p);

    printf("%d",a);
    //最终的输出结果为: 5 5

    首先定义了一个整型变量a,并赋值为5。然后定义了一个整型指针p,并将a的地址赋给p。接下来使用printf函数来输出变量的值。第一行printf语句中,使用了解引用操作符_,即_p,表示获取指针p指向的地址的值。所以输出的结果为5。第二行printf语句中,直接输出变量a的值,也是5。如果有:
    *p = 3;
    表示将整数 3 赋值给 p 当前所指向的变量,如果 p 指向 a,则相当于把 1 赋值给a。

  • 引用指针变量的值

printf(“%o”,p);
作用是以八进制数形式输出指针变量 p 的值,如果 p 指向了 a,就是输出了 a 的地址,即 &a


  1. 指针变量作为函数参数

函数的参数不仅可以是整型,浮点型,字符型的数据,还可以是指针类型,它的作用是将一个变量的地址传送到另一个函数中。下面是一个使用指针变量作为函数参数的C语言代码例子:

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

// 函数原型,参数为指针变量
void changeValue(int *ptr);

int main() {
int a = 5;
printf("函数调用前的值:%d\n", a);

// 调用函数,传入指针变量a的地址
changeValue(&a);

printf("函数调用后的值:%d\n", a);

return 0;
}

// 函数定义,通过指针修改变量的值
void changeValue(int *ptr) {
// 解引用指针,修改变量的值
*ptr = 10;
}-----

在这个例子中,我们定义了一个函数changeValue,它接受一个指针变量作为参数。在main函数中,我们定义了一个整型变量a并赋值为5。然后,我们调用changeValue函数,并传入a的地址&a作为参数。在changeValue函数中,我们通过解引用指针*ptr来修改变量的值。最后,在main函数中打印修改后的a的值。输出结果为:

1
2
函数调用前的值:5
函数调用后的值:10

可以看到,通过指针变量作为函数参数,我们可以在函数内部修改变量的值,并且这个修改也会影响到函数外部的变量。


指针引用数组

  • 数组元素的指针

一个变量有地址,一个数组包含若干个元素,每个数组元素都在内存中占有存储单元,他们都有相应的地址。指针变量既然可以指向变量,当然也可以指向数组元素。可以用一个指针变量指向一个数组元素:

1
2
3
int a[] = {1,2,3};
int *p;
p = &a[0];

首先定义了一个整型数组a,并初始化为 {1, 2, 3}。然后,声明了一个整型指针变量p。接下来,将数组a的第一个元素的地址


在C语言中数组名(不算形参数组名)代表数组中首元素(如a[0])的地址因此,下面两者是等价的:

1
2
p = &a[0];	//设定a是数组名,p是指针变量
p = a;

  • 在引用数组元素时的指针运算

指针以指向一个元素时,可以对指针进行以下运算:在引用数组元素时,可以对指针进行一些运算操作。下面是一些常见的指针运算:

  1. 加一个整数:可以使用指针加上一个整数来访问数组中的下一个元素。例如,p + 1会将指针p向后移动一个位置,指向数组中的下一个元素。
  2. 减一个整数:类似地,可以使用指针减去一个整数来访问数组中的前一个元素。例如,p - 1会将指针p向前移动一个位置,指向数组中的前一个元素。
  3. 自增运算:可以使用自增运算符++来将指针向后移动一个位置。例如,p++或++p会将指针p指向数组中的下一个元素。
  4. 自减运算:类似地,可以使用自减运算符–来将指针向前移动一个位置。例如,p–或–p会将指针p指向数组中的前一个元素。
  5. 两个指针相减:当两个指针指向同一数组中的元素时,可以使用减法运算符-来计算它们之间的距离(以元素个数为单位)。例如,p2 - p1会得到两个指针之间的元素个数。

需要注意的是,只有在指针指向同一数组中的元素时,才能进行指针的加减运算和两个指针的相减运算。否则,结果将是未定义的。


  • 通过指针引用数组元素

引用数组元素可以用下标法(p = &a[0]),也可以用指针法及通过指向数组元素的指针找到所需元素。下面是使用C语言代码通过指针引用数组元素的两种方法:下标法和指针法。嗯下标法:

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

int main() {
int a[] = {1, 2, 3};
int i;

for (i = 0; i < 3; i++) {
printf("a[%d] = %d\n", i, a[i]);
}

return 0;
}

在这个例子中,使用了一个for循环来遍历数组a。通过下标i来访问数组元素,即a[i]。通过循环迭代,可以依次引用数组中的每个元素。
指针法:

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

int main() {
int a[] = {1, 2, 3};
int *p;

for (p = &a[0]; p < &a[3]; p++) {
printf("*p = %d\n", *p);
}

return 0;
}

在这个例子中,定义了一个整型指针变量p,并将其初始化为指向数组a的第一个元素的地址,即&a[0]。然后,使用for循环来遍历数组a。通过指针p来引用数组元素,即*p。在每次循环迭代中,指针p向后移动一个位置,指向下一个数组元素。


指针引用字符串

  1. 字符串的引用方式

在C语言中,字符串可以通过两种方式进行引用:作为字符数组和作为字符指针。字符数组方式引用字符串:

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

int main() {
char str1[] = "Hello, World!"; // 字符数组方式引用字符串

printf("str1: %s\n", str1);

return 0;
}

在这个例子中,使用字符数组str1来引用字符串”Hello, World!”。字符数组会在内存中分配足够的空间来存储字符串的每个字符,字符串的末尾会自动添加一个空字符\0作为结束符。字符指针方式引用字符串:

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

int main() {
char *str2 = "Hello, World!"; // 字符指针方式引用字符串

printf("str2: %s\n", str2);

return 0;
}

在这个例子中,使用字符指针str2来引用字符串”Hello, World!”。字符指针指向字符串的第一个字符,字符串的末尾仍然有一个空字符\0作为结束符。需要注意的是,字符数组方式引用字符串可以修改字符串的内容,而字符指针方式引用字符串不能修改字符串的内容。例如,下面的代码会导致编译错误:

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

int main() {
char *str = "Hello, World!";

str[0] = 'h'; // 编译错误,字符指针方式引用的字符串是只读的

return 0;
}

  1. 字符指针作函数参数

在C语言中,可以使用字符指针作为函数的参数来传递字符串。通过传递字符指针,函数可以访问并操作字符串的内容。下面是一个例子,演示了如何使用字符指针作为函数参数来打印字符串:

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

void printString(char *str) {
printf("%s\n", str);
}

int main() {
char *str = "Hello, World!";
printString(str);

return 0;
}

在这个例子中,定义了一个函数printString,它接受一个字符指针参数str,并使用printf函数打印字符串。在main函数中,定义了一个字符指针str,并将其作为参数传递给printString函数。需要注意的是,字符指针作为函数参数传递时,函数内部不能修改指针的值,也不能修改指针所指向的字符串的内容。如果需要在函数内部修改字符串的内容,可以使用字符数组作为函数参数。


指向函数的指针

  1. 函数的指针

在程序中定义了一个函数,在编译时会把函数的源代码转换为可执行代码并分配一段存储空间。这段存储空间有一个起始地址,也称为函数的入口地址。每次调用函数时都从该地址入口开始执行此段函数代码。函数名代表函数的起始地址函数名就是函数的指针,它代表函数的起止地址
int (*p)(int,int);
定义P是一个指向函数的指针变量可以指向函数类型为整型且有两个整型参数的函数


  1. 用函数指针变量调用函数

要使用函数指针变量调用函数,可以直接使用函数指针变量的名称后跟括号,类似于调用函数的语法。下面是一个示例代码,演示了如何使用函数指针变量调用函数:

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

int add(int a, int b) {
return a + b;
}

int subtract(int a, int b) {
return a - b;
}

int main() {
int (*funcPtr)(int, int); // 声明一个指向函数的指针变量

funcPtr = add; // 将函数的地址赋值给函数指针变量
int result = funcPtr(5, 3); // 通过函数指针调用函数
printf("Result of add: %d\n", result);

funcPtr = subtract; // 可以将函数指针指向不同的函数
result = funcPtr(5, 3); // 再次通过函数指针调用函数
printf("Result of subtract: %d\n", result);

return 0;
}

在这个例子中,我们定义了两个函数add和subtract,它们分别接受两个整数参数并返回整数。然后,我们声明了一个指向函数的指针变量funcPtr,它接受两个整数参数并返回整数。我们将add函数的地址赋值给funcPtr,然后通过funcPtr(5, 3)调用add函数,并将结果存储在result变量中。接着,我们将subtract函数的地址赋值给funcPtr,再次通过funcPtr(5, 3)调用subtract函数,并将结果存储在result变量中。


  1. ** 定义和使用指向函数的指针变量**

定义指向函数的指针变量:

1
2
返回类型 (*指针变量名)(参数类型1, 参数类型2, ...);
int (*p)(int,int);

下面是一个示例代码,演示了如何定义和使用指向函数的指针变量:

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 add(int a, int b) {
return a + b;
}

int subtract(int a, int b) {
return a - b;
}

int main() {
// 声明一个指向函数的指针变量
int (*funcPtr)(int, int);

// 将函数的地址赋值给函数指针变量
funcPtr = add;

// 使用函数指针调用函数
int result = funcPtr(5, 3);
printf("add函数的结果: %d\n", result);

// 将函数的地址赋值给函数指针变量
funcPtr = subtract;

// 使用函数指针调用函数
result = funcPtr(5, 3);
printf("subtract函数的结果: %d\n", result);

return 0;
}

在这个例子中,我们定义了两个函数add和subtract,它们分别接受两个整数参数并返回整数。然后,我们声明了一个指向函数的指针变量funcPtr,它接受两个整数参数并返回整数。我们将add函数的地址赋值给funcPtr,然后通过funcPtr调用add函数,并将结果存储在result变量中。接着,我们将subtract函数的地址赋值给funcPtr,再次通过funcPtr调用subtract函数,并将结果存储在result变量中。


动态内存分配

在C语言中,动态内存分配是通过malloc、calloc、realloc和free函数来实现的。这些函数可以帮助我们在程序运行时动态地分配和释放内存。

  1. malloc 函数用于分配指定字节数的内存,并返回一个指向分配内存的指针。它的基本用法如下:

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

    int main() {
    int* ptr = (int*)malloc(5 * sizeof(int));

    if (ptr == NULL) {
    printf("内存分配失败\n");
    return 1;
    }

    // 使用分配的内存

    free(ptr);

    return 0;
    }
  2. calloc 函数与malloc函数类似,但它会将分配的内存块的每个字节都初始化为零。它的基本用法如下:

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

    int main() {
    int* ptr = (int*)calloc(5, sizeof(int));

    if (ptr == NULL) {
    printf("内存分配失败\n");
    return 1;
    }

    // 使用分配的内存

    free(ptr);

    return 0;
    }
  3. realloc 函数用于重新分配已经分配的内存块的大小。如果原内存块的大小不够,realloc函数会重新分配更大的内存块,并将原内存块的内容复制到新内存块中。如果原内存块的大小过大,realloc函数会缩小内存块的大小。它的基本用法如下:

    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
    #include <stdio.h>
    #include <stdlib.h>

    int main() {
    int* ptr = (int*)malloc(5 * sizeof(int));

    if (ptr == NULL) {
    printf("内存分配失败\n");
    return 1;
    }

    // 使用分配的内存

    // 重新分配内存块的大小为10个整数
    ptr = (int*)realloc(ptr, 10 * sizeof(int));

    if (ptr == NULL) {
    printf("内存重新分配失败\n");
    return 1;
    }

    // 使用重新分配的内存

    free(ptr);

    return 0;
    }
  4. free 函数用于释放之前通过malloc、calloc或realloc函数分配的内存。它的基本用法如下:

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

    int main() {
    int* ptr = (int*)malloc(5 * sizeof(int));

    if (ptr == NULL) {
    printf("内存分配失败\n");
    return 1;
    }

    // 使用分配的内存

    free(ptr);

    return 0;
    }

    需要注意的是,动态分配的内存需要手动释放,否则会导致内存泄漏。在释放内存后,不能再使用已释放的内存,否则会导致未定义的行为。


第9章 用户自己建立数据类型

结构体变量

  1. 结构体定义

结构体由多个成员变量组成,每个成员变量可以是不同的数据类型。通过结构体,我们可以将相关的数据组织在一起,方便管理和操作。结构体的定义使用struct****关键字,后面跟着结构体的名称和成员变量的列表。结构体的定义:

1
2
3
4
5
6
7
8
9
10
11
struct 结构体名
{
成员列表;
};

struct Student
{
char name[50];
int age;
float gpa;
};

在上面的例子中,我们定义了一个名为Student的结构体,它有三个成员变量:name、age和gpa。name是一个字符数组,用于存储学生的姓名;age是一个整数,用于存储学生的年龄;gpa是一个浮点数,用于存储学生的平均成绩。结构体成员变量的运用:

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>

// 定义一个结构体类型
struct Student {
int id;
char name[20];
int age;
};

int main() {
// 声明一个结构体变量并初始化
struct Student stu1 = {1, "Alice", 18};

// 访问结构体成员变量并输出
printf("学生ID:%d\n", stu1.id);
printf("学生姓名:%s\n", stu1.name);
printf("学生年龄:%d\n", stu1.age);

// 修改结构体成员变量的值
stu1.age = 19;

// 输出修改后的结果
printf("学生年龄(修改后):%d\n", stu1.age);

return 0;
}

我们可以通过结构体的名称和成员变量的名称来访问结构体的成员。例如,下面是一个使用结构体的例子:

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

struct Student {
char name[50];
int age;
float gpa;
};

int main() {
struct Student student1;

// 设置学生的信息
strcpy(student1.name, "Tom");
student1.age = 20;
student1.gpa = 3.5;

// 打印学生的信息
printf("姓名:%s\n", student1.name);
printf("年龄:%d\n", student1.age);
printf("平均成绩:%f\n", student1.gpa);

return 0;
}

在上面的例子中,我们首先定义了一个名为student1的结构体变量。然后,我们使用.运算符来访问结构体的成员变量,并设置其值。最后,我们打印了学生的姓名、年龄和平均成绩。


typedef关键字typedef关键字是C语言中用来给已经存在的数据类型起一个新的名字的关键字。通过使用typedef关键字,可以为已有的数据类型定义一个新的名称,使得使用该数据类型时更加方便和直观。typedef关键字通常与结构体一起使用,用于给结构体类型定义一个新的名称。typedef关键字的语法格式如下:
typedef 原类型名 新类型名;
例如,可以使用typedef关键字为结构体类型定义一个新的名称,如下所示:

1
2
3
4
5
typedef struct {
int year;
int month;
int day;
} Date;

在上述示例中,使用typedef关键字为结构体类型定义了一个新的名称Date。之后可以直接使用Date作为结构体类型的名称,而不需要每次都使用struct关键字。


  1. 定义结构体类型变量

当我们定义结构体类型变量时,可以使用3种方法:a. 先声明结构体类型,在定义该类型的变量:

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

struct Student {
char name[50];
int age;
float gpa;
}; // 先声明结构体类型

int main() {
struct Student student1; // 再定义结构体变量

strcpy(student1.name, "Tom");
student1.age = 20;
student1.gpa = 3.5;

printf("姓名:%s\n", student1.name);
printf("年龄:%d\n", student1.age);
printf("平均成绩:%f\n", student1.gpa);

return 0;
}

在上面的例子中,我们首先使用struct关键字声明了一个名为Student的结构体类型。然后,在main函数中,我们定义了一个名为student1的结构体变量,该变量的类型是之前声明的Student结构体类型。b.在声明类型的同时定义变量就是将结构体的声明和初始化合并在一起。示例:

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

// 声明结构体类型的同时定义结构体变量
struct Point {
int x;
int y;
} p1 = {10, 20}; // 定义并初始化结构体变量

int main() {
// 使用已定义的结构体变量
printf("坐标点:(%d, %d)\n", p1.x, p1.y);

return 0;
}

在示例中,我们定义了一个结构体类型struct Point,其中包含两个整型成员变量x和y。而在结构体类型的声明之后,我们紧接着定义了一个结构体变量p1,并使用初始化列表对其成员变量进行了赋值。c.不指定类型名而直接定义结构体类型变量可以通过使用匿名结构体类型直接定义结构体类型变量,而不需要指定类型名。这样的结构体类型被称为匿名结构体。示例:

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

int main() {
// 直接定义匿名结构体类型的变量
struct {
int x;
int y;
} p1 = {10, 20}; // 定义并初始化匿名结构体变量

// 使用已定义的匿名结构体变量
printf("坐标点:(%d, %d)\n", p1.x, p1.y);

return 0;
}

在示例中,我们没有显式地声明一个具名的结构体类型,而是直接定义了一个匿名的结构体类型,并创建了一个匿名结构体变量p1。同样,我们也可以在定义时使用初始化列表对其成员变量进行赋值。值得注意的是,匿名结构体类型仅在定义它的作用域内有效。这意味着,我们无法在其他位置复用或引用这个结构体类型,也不能通过typedef为其定义一个类型名。


1
2
3
4
5
6
7
8
9

结构体类型 数组名[数组长度];

typedef struct {
int x;
int y;
} Point;

Point points[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
#include <stdio.h>

typedef struct {
char name[100];
int age;
float gpa;
} Student;

int main() {
Student students[3];

// 输入学生信息
for (int i = 0; i < 3; i++) {
printf("请输入学生 %d 的信息:\n", i+1);
printf("姓名: ");
scanf("%s", students[i].name);
printf("年龄: ");
scanf("%d", &students[i].age);
printf("GPA: ");
scanf("%f", &students[i].gpa);
printf("\n");
}

// 输出学生信息
for (int i = 0; i < 3; i++) {
printf("学生 %d 的信息:\n", i+1);
printf("姓名: %s\n", students[i].name);
printf("年龄: %d\n", students[i].age);
printf("GPA: %.2f\n", students[i].gpa);
printf("\n");
}

return 0;
}

这个示例中,我们定义了一个名为 Student 的结构体类型,其中包含了学生的姓名、年龄和 GPA(平均绩点)信息。在 main 函数中,我们声明了一个名为 students 的结构体数组,数组长度为 3。然后,我们使用一个循环来输入每个学生的详细信息,并将其存储到对应的结构体数组元素中。最后,我们再次使用循环遍历结构体数组,并输出每个学生的详细信息。运行该程序,你将被要求依次输入每个学生的姓名、年龄和 GPA,然后程序将输出输入的学生信息。

结构体指针

如果把一个结构体变量的起始地址存放在一个指针变量中,这个指针变量就指向该结构体变量两种不同的访问结构体成员的方式:** 点操作符 ****.**使用点操作符 . 可以直接访问结构体变量的成员。这种方式适用于直接访问结构体变量。例如:

1
2
3
4
5
6
7
8
typedef struct {
int x;
int y;
} Point;

Point p1;
p1.x = 3;
p1.y = 5;

**箭头操作符 ****->**当使用指针指向结构体变量时,需要使用箭头操作符 -> 来访问结构体变量的成员。这种方式适用于访问结构体指针指向的结构体变量。例如:

1
2
3
4
5
6
7
8
9
typedef struct {
int x;
int y;
} Point;

Point p1;
Point *ptr = &p1;
ptr->x = 3;
ptr->y = 5;

注意,只有当我们使用指针指向结构体变量时,才需要使用箭头操作符 -> 。如果我们直接使用结构体变量,则应使用点操作符 .

  1. 指向结构体变量、数组的指针

指向结构体对象指着变量即可指向结构体变量,也可指向结构体数组中的元素指针变量的基类型必须与结构体变量的类型相同。

1
2
struct Student *pt;	
//pt 可以指向 struct Student类型的变量或数组元素

指向结构体变量的指针

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

typedef struct {
int x;
int y;
} Point;

int main() {
Point p1 = {5, 10};
Point *ptr = &p1;

printf("Point p1: x = %d, y = %d\n", ptr->x, ptr->y);

return 0;
}

在这个例子中,我们定义了一个结构体类型 Point,并创建了一个结构体变量 p1。然后,我们定义了一个指向 Point 类型的指针 ptr,并将其指向结构体变量 p1。通过指针 ptr,我们可以访问结构体变量 p1 的成员。指向结构体数组中元素的指针

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

typedef struct {
int x;
int y;
} Point;

int main() {
Point arr[3] = {{1, 2}, {3, 4}, {5, 6}};
Point *ptr = arr;

printf("Point 1: x = %d, y = %d\n", ptr->x, ptr->y);
printf("Point 2: x = %d, y = %d\n", (ptr+1)->x, (ptr+1)->y);
printf("Point 3: x = %d, y = %d\n", (ptr+2)->x, (ptr+2)->y);

return 0;
}

在这个例子中,我们定义了一个包含三个元素的结构体数组 arr。然后,我们定义了一个指向 Point 类型的指针 ptr,并将其指向结构体数组的第一个元素 arr[0]。通过指针 ptr 可以访问结构体数组中的元素,并通过 ptr+1 和 ptr+2 可以访问数组的下一个元素。


  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
#include <stdio.h>

typedef struct {
int x;
int y;
} Point;

void modifyPoint(Point *ptr) {
ptr->x = 7;
ptr->y = 10;
}

int main() {
Point p1 = {5, 10};

printf("Before modification: x = %d, y = %d\n", p1.x, p1.y);
// 输出 "Before modification: x = 5, y = 10"

modifyPoint(&p1);

printf("After modification: x = %d, y = %d\n", p1.x, p1.y);

// 输出 "After modification: x = 7, y = 10"

return 0;
}

在上面的示例中,我们定义了一个结构体类型 Point,并在 modifyPoint 函数中接受一个 Point 类型的指针作为参数。在 modifyPoint 函数中,通过箭头操作符 -> 修改了指针指向的结构体变量 ptr 的成员值。在 main 函数中,我们创建了一个 Point 类型的结构体变量 p1,然后通过 modifyPoint(&p1) 将 p1 的地址作为参数传递给 modifyPoint 函数。在函数调用后,p1 的成员值被修改为 7 和 10。所以,通过将结构体变量的指针作为函数参数,我们可以在函数中直接修改结构体变量的值。


指针处理链表

:::info 该章节所涉及的链表均为单链表 :::

  1. 链表定义

链表(单向链表)是一种常见的数据结构,用于存储和组织数据。链表开头有一个头指针变量(Head),存放一个地址(1249),该地址指向一个元素(A)。链表末尾有一个尾指针变量,存放“NULL”,表示空地址,链表到此结束除了头尾指针外,链表中每一个元素称为“结点”,每个结点包括两部分:(1)数据域:用户需要用的实际数据(上图中的A、B、C、D)。(2)指针域:下一个结点的地址(1356、1475、1021)。从上例得知,Head指向第1个元素,第1个元素又指向第2个元素…直到最后一个元素,该元素不在指向其他元素,只存放一个空地址,链表结束。

  • 链表中的各元素在内存中的地址可以是不连续的
  • 要找某一元素,必须先找到上一个元素,根据他提供的下一个地址才能找到下一个元素
  • 如果不提供“头指针”(head),则整个链表都无法访问

  1. C语言表示链表

链表这种数据结构,显然是指针加结构体最合适

  • 定义单链表的节点结构体
    1
    2
    3
    4
    struct Node {
    int data; // 数据
    struct Node* next; // 指向下一个节点的指针
    };
    上述代码定义了一个单链表的节点结构体 struct Node,它包含两个成员变量:int data:用于存储节点的数据。struct Node* next:用于指向下一个节点的指针。它是一个指针类型的成员变量,指向 struct Node 类型的节点。通过这个节点结构体,我们可以创建单链表的节点,并使用 data 成员变量存储节点的数据,使用 next 成员变量指向下一个节点。

  • 建立简单的静态列表

通过一个例子来说明如何建立输出一个简单链表:

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
#include <stdio.h>
struct Student //声明结构体类型Student
{
int num;
float score;
struct Student *next;
};
int main()
{
struct Student a, b, c, *head, *p; //定义3个结构体变量a, b, c 和结构体指针变量*head, *p
a.num = 101; a.score = 90;
b.num = 102; b.score = 88;
c.num = 103; c.score = 85; //分别对结构体变量a,b,c赋值

head = &a; //结点a的起始地址赋值给头指针head
a.next = &b; //结点b的起始地址赋值给a结点的next成员
b.next = &c; //结点c的起始地址赋值给b结点的next成员
c.next = NULL; //结点c的next被赋值NULL,表示不存放任何结点地址
p = head; //使p指向head头指针

do
{
printf("%d %0.1f\n",p->num,p->score);
p = p->next;
}
while(p != NULL);

return 0;
}

上面的代码定义了一个结构体类型 struct Student,它包含三个成员变量:

  • int num:用于存储学生的学号。
  • float score:用于存储学生的分数。
  • struct Student *next:用于指向下一个学生节点的指针。这里使用了自引用的方式,使得可以构建出一个链表结构。

在 main() 函数中,首先建了三个结构体变量 a、b 和 c,并给它们分别赋值,表示三个学生的学号和分数。然后,通过将 a.next、b.next 和 c.next 分别赋值为 &b、&c 和 NULL,将这三个节点串联成一个链表。头指针 head 被赋值为 &a,即指向链表的第一个节点。然后定义了一个指针变量 p,将它初始化为 head。接下来,使用 do-while 循环遍历链表并打印每个节点的学号和分数。这里使用 printf() 函数打印输出。最后,返回 0,表示程序执行成功结束。运行上述代码,输出结果为:

1
2
3
101  90.0
102 88.0
103 85.0

  • 建立动态链表

所谓动态链表是指在程序执行过程中,从无到有地建立起一个链表,即一个一个地开辟结点和输入各结点数据,并建立起前后相连的关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
#include "stdio.h"
#include "malloc.h"
typedef int ElemType; //给int取别名

//创建一个单链表模板
typedef struct node
{
ElemType data; //数据域
struct node* next; //指针域,指向下一个“相邻”的单链表
}SlinkNode;

//创建一个空的单链表(由L指向它),即头结点,带头结点,data数据域没有值,next指针域为空
void InitList(SlinkNode*& L) //(括号内为指针变量的引用,不看*就是普通变量,加上就是指针变量)
{
L = (SlinkNode*)malloc(sizeof(SlinkNode));
L->next = NULL;
}

//销毁线性表
void DestroyList(SlinkNode*& L)
{
SlinkNode* pre = L; //单链表的头结点“L”传给pre
SlinkNode* p = pre->next; //p指向着pre的下一个单链表
while (p != NULL) //只要p不指向尾结点,就一直重复
{
free(pre); //每动一步,就释放pre指向的一个数据结点的空间,即销毁了一个数据结点
pre = p; //再让pre指向下一个数据结点,也就是p所指向的
p = p->next; //紧接着p移动,指向下下一个数据结点
}
free(pre); //p已经指向了尾结点,释放pre指向的最后一个的数据结点,做到销毁整个单链表
}

//求线性表的长度
int GetLength(SlinkNode* L) //该单链表的头结点“L”
{
int i = 0;
SlinkNode* p = L->next; //让p指向L指向的该单链表的首结点,因为只计算除头尾结点之外的数据结点的长度
while (p != NULL) //只要p不指向尾结点,就一直执行
{
i++; //i++记录长度
p = p->next; //让p指向下一个数据结点
}
return i; //记录结束,返回i的值,即该单链表的长度
}

//求线性表中第i个元素
int GetElem(SlinkNode* L, int i, ElemType& e) // 该单链表的头结点“L”,要找的第i个元素位置, 要找的元素位置上的值e
{
int j = 0; //j为计数,从0加到第i个,因为首结点不算,所以是0,加一个数据结点就+1(从头结点开始,j = 0 / 从数据结点开始,j = 1)
SlinkNode* p = L; //让p指向L指向的该单链表的首结点,因为头尾结点数据域没有值
if (i <= 0) //判断数字i是否合理
return 0;
while (p != NULL && j < i) //p不指向尾结点,且计数j不大于等于i,才能继续下去
{ //情况1,p指向尾结点了,都没值可用了,退出循环
j++; //情况2,j都等于i了,到第i个位置了,退出循环
p = p->next;
}
if (p == NULL) //循环结束,只要p都指向尾结点了,就是说没e值可用,说明没找到
return 0;
else //否则就是找到了
{
e = p->data; //把p指向的数据结点的数据域的值传给e,实时更新主函数的e值,因为是 ElemType& e,这个“&”
return 1;
}
}

//按e值查找是第几个位置
int Locate(SlinkNode* L, ElemType e) //该单链表的头结点“L”,要的找在单链表中什么位置的值e(看看e在什么位置)
{
SlinkNode* p = L->next; //让p指向L指向的该单链表的首结点
int j = 1; //设为1,直接先找第1个位置(从头结点开始,j = 0 / 从数据结点开始,j = 1)
while (p != NULL && p->data != e)
{
p = p->next;
j++;
}
if (p == NULL) //p指向尾结点了,证明没有找到
return (0);
else //否则就是找到了,返回第j个值
return (j);
}

//插入元素
int InsElem(SlinkNode*& L, ElemType x, int i) // 该单链表的头结点“L”,插入结点的数据域的值x,要插入的第i个位置
{
int j = 0; //(从头结点开始,j = 0 / 从数据结点开始,j = 1)
SlinkNode* p = L; //把该单链表的头结点“L”传给p
SlinkNode* s;
if (i <= 0) //判断第i个位置是否合理
return 0;
while (p != NULL && j < i - 1)
{
j++;
p = p->next;
}
if (p == NULL)
return 0;
else
{
s = (SlinkNode*)malloc(sizeof(SlinkNode));
s->data = x;
s->next = p->next;
p->next = s;
return 1;
}
}

//删除元素
int DelElem(SlinkNode*& L, int i)
{
int j = 0;
SlinkNode* p = L;
SlinkNode* q;
if (i <= 0)
return 0;
while (p != NULL && j < i - 1)
{
j++;
p = p->next;
}
if (p == NULL)
return 0;
else
{
q = p->next;
if (q == NULL)
return 0;
else
{
p->next = q->next;
free(q);
return 1;
}
}
}

//输出线性表
void DispList(SlinkNode* L)
{
SlinkNode* p = L->next;
while (p != NULL)
{
printf("%d", p->data);
p = p->next;
}
printf("\n");
}

int main()
{
int i;
ElemType e;
SlinkNode* L;
InitList(L);
InsElem(L, 1, 1);
InsElem(L, 3, 2);
InsElem(L, 1, 3);
InsElem(L, 5, 4);
InsElem(L, 4, 5);
InsElem(L, 2, 6);
printf("线性表:"); DispList(L);
printf("长度:%d\n", GetLength(L));
i = 3; GetElem(L, i, e);
printf("第%d个元素:%d\n", i, e);
e = 1;
printf("元素%d是第%d个元素\n", e, Locate(L, e));
i = 4; printf("删除第%d个元素\n", i);
DelElem(L, i);
printf("线性表:"); DispList(L);
DestroyList(L);
}

输出结果:

1
2
3
4
5
6
线性表:131542
长度:6
第3个元素:1
元素1是第1个元素
删除第4个元素
线性表:13142

共用体类型

共用体(Union)是一种特殊的数据类型,它允许在相同的内存位置存储不同的数据类型。共用体的内存大小取决于其中最大的成员的大小。共用体的成员可以是不同的数据类型,但是在同一时间只能存储其中的一个成员值。当一个成员值被赋值给共用体时,其他成员值都会被覆盖。共用体的使用可以节省内存空间,因为共用体只会占用足够存储最大成员的内存空间。然而,由于共用体的成员共享同一块内存空间,因此对一个成员的修改会影响其他成员的值。

  • 定义共用体:

    1
    2
    3
    4
    union 共用体名
    {
    成员列表
    }变量列表;
  • 声明类型同时定义变量:

    1
    2
    3
    4
    5
    6
    union Data
    {
    int i;
    char ch;
    float f;
    }a,b,c;
  • 类型声明与定义变量分开:

    1
    2
    3
    4
    5
    6
    7
    8
    union Data
    {
    int i;
    char ch;
    float f;
    };
    union Data a, b, c;
    //先声明一个union Data类型,再将a,b,c定义为union Data类型的变量

  • 共同体类型的引用

对于共用体的成员引用,应使用共用体变量名和成员名之间的** . 运算符** 进行引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
union data {
int i;
float f;
char str[20];
};

int main() {
union data d;

d.i = 10;
printf("d.i = %d\n", d.i);

d.f = 3.14;
printf("d.f = %f\n", d.f);

strcpy(d.str, "Hello");
printf("d.str = %s\n", d.str);

return 0;
}

在上面的示例中,我们定义了一个共用体data,它有三个成员:整型i、浮点型f和字符数组str。在main函数中,我们声明了一个共用体变量d,然后通过d.i、d.f和d.str来分别访问共用体的不同成员。对于共用体的成员访问,不需要使用.运算符,而是直接使用共用体变量名和成员名之间的.运算符。这是因为共用体的不同成员共享同一块内存空间。因此,对一个成员的修改会影响其他成员的值。在使用共用体时,需要确保对共用体的成员的访问是有意义的,并且不会引起意外的结果。


  • 共用体类型数据的特点

    • 同一个内存段可以用来存放几种不同类型的成员,但在每一瞬间只能存放其中一个成员,而不是同时存放几个
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      union Data
      {
      int i;
      char ch;
      float f;
      }a;
      a.i = 97;

      printf("%d",a.i); //输出整数97
      printf("%c",a.ch); //输出字符'a'
      printf("%f",a.f); //输出实数0.000000
      在代码中,a.i = 97;将整数值97赋给了共用体的整型成员i。因此,printf(“%d”, a.i);会输出整数97。接下来,printf(“%c”, a.ch);尝试输出共用体的字符成员ch。由于共用体的不同成员共享同一块内存空间,整型成员i的值97被解释(ASCII表)为字符’a’,因此输出字符’a’。最后,printf(“%f”, a.f);尝试输出共用体的浮点型成员f。由于共用体的不同成员共享同一块内存空间,整型成员i的值97被解释为浮点数0.000000,因此输出实数0.000000。这个例子展示了共用体的一个重要特性:不同类型的成员共享同一块内存空间。这意味着对一个成员的修改可能会影响其他成员的值。在使用共用体时,需要小心确保对共用体的成员的访问是有意义的,并且不会引起意外的结果。
  • 可以对共用体变量初始化,但初始化表中只能有一个常量

    1
    2
    3
    4
    5
    6
    7
    8
    union Data
    {
    int i;
    char ch;
    float f;
    }a = {1, 'a', 1.5}; //不能初始化3个成员,它们是占用同一个存储单元的
    union Data a = {16}; //正确,对第1个成员初始化
    union Data a = {.ch = 'j'}; //C99允许对指定的一个成员初始化
  • 共用体变量中起作用的成员是最后一次被赋值的成员,在对共用体变量中的一个成员赋值后,原有变量存储单元中的值就取代

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    union Data {
    int i;
    char ch;
    float f;
    };

    int main() {
    union Data d;

    d.i = 97;
    printf("d.i = %d\n", d.i); // 输出整数97

    d.ch = 'a';
    printf("d.ch = %c\n", d.ch); // 输出字符'a'

    d.f = 3.14;
    printf("d.f = %f\n", d.f); // 输出实数3.140000

枚举类型

如果一个变量只用几种可能的值,则可以定义为枚举类型(enum),所谓枚举就是指把可能的值一一列举出来,变量的值只限于列举出来的值的范围内

  • 枚举类型定义:
    1
    2
    3
    4
    enum 枚举名 
    {
    枚举元素列表
    };
    1
    2
    3
    4
    5
    6
    7
    8
    9
    enum Weekday {
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
    Sunday
    };
    在上面的代码中,我们定义了一个名为Weekday的枚举类型,它包含了一周的所有工作日和周末。枚举常量的默认值从0开始自增,因此Monday的值为0,Tuesday的值为1,以此类推。枚举类型定义后,我们可以声明枚举类型的变量,并将其赋值为枚举常量。例如:
    enum Weekday today = Tuesday;
    在上面的代码中,我们声明了一个名为today的枚举类型变量,并将其赋值为Tuesday枚举常量。我们还可以通过枚举常量的名称来访问其对应的值。例如:
    printf(“Today is %d\n”, today);
    上述代码将输出Today is 1,因为Tuesday的值为1。枚举类型还可以与switch语句一起使用,以便根据枚举变量的值执行相应的操作。例如:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    switch (today) {
    case Monday:
    case Tuesday:
    case Wednesday:
    case Thursday:
    case Friday:
    printf("It's a weekday\n");
    break;
    case Saturday:
    case Sunday:
    printf("It's a weekend\n");
    break;
    default:
    printf("Invalid day\n");
    break;
    }
    综合案例:示例:
    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
    #include <stdio.h>

    enum Month {
    January = 1,
    February,
    March,
    April,
    May,
    June,
    July,
    August,
    September,
    October,
    November,
    December
    };

    struct Date {
    int day;
    enum Month month;
    int year;
    };

    void printDate(struct Date date) {
    printf("%d/%d/%d\n", date.day, date.month, date.year);
    }

    int main() {
    struct Date today;

    printf("Enter day: ");
    scanf("%d", &today.day);

    printf("Enter month (1-12): ");
    int month;
    scanf("%d", &month);
    today.month = (enum Month)month;

    printf("Enter year: ");
    scanf("%d", &today.year);

    printf("Today's date is: ");
    printDate(today);

    return 0;
    }
    在上面的代码中,我们定义了一个枚举类型Month,用于表示一年中的月份。每个月份都被赋予一个整数值,从1开始自增。然后,我们定义了一个结构体Date,它包含了一个整型的day、一个枚举类型的month和一个整型的year。在main函数中,我们声明了一个名为today的结构体变量,并通过用户输入来初始化其中的成员。最后,我们调用printDate函数来打印今天的日期。该函数接受一个Date类型的参数,并按照day/month/year的格式打印日期。

第10章 对文件的输入输出

C文件基本知识

  • 什么是文件

在程序设计中,主要用到两种文件:
程序文件: 例如源程序文件(.c)、目标文件(.obj)、可执行文件(.exe)
数据文件: 文件的内容不是程序,而是供程序运营时读写的数据,如在程序运行过程中输出到磁盘(或其他外部设备)的数据,或在程序运行过程中供读入的数据。如一批学生的成绩数据…本章主要讨论的数据文件


在以前的章节里,所处理的数据的输入输出,都是以终端为对象的,即从终端的键盘输入数据,运行结果输出到终端显示器上。实际上,常常需要将一些数据输出到磁盘上保存起来,以后需要时再从磁盘中输入到计算机内存。这就是磁盘文件统一文件处理:从操作系统的角度看,每一个与主机相连的输入输出设备都看作一个文件。操作系统把各种设备都统一作为文件来处理。例如,终端键盘是输入文件,显示屏和打印机是输出文件文件:在C语言中,文件是一种用于存储和读取数据的抽象概念。文件可以是磁盘上的实际文件,也可以是输入/输出设备(如键盘、显示器)的抽象表示。


文件名: 一个文件要有一个唯一的文件标识,以便用户识别和引用。

  • 文件标识包括3部分:

1、文件路径 2、文件名主干 3、文件后缀文件路径表示文件在外部存储设备中的位置。

1
2
3
4
D:\CC\temp\file.dat
文件路径:D:\CC\temp
文件名主干:file
文件后缀:.dat

表示文件 file.dat 存放在D盘中的CC目录下的temp子目录下面方便起见,文件标识常被称为文件名


文件缓冲区:指的就是系统自动地在内存区为程序中每一个正在使用的文件开辟一个文件缓冲区。从内存向磁盘输出数据必须先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘去。如果从磁盘向计算机读入数据,则一次从磁盘文件将一批数据输入到内存缓冲区中(充满缓冲区)然后再从缓冲区逐个地将数据送到程序数据区(给程序变量)


文件指针类型:文件类型指针是C语言中用于操作文件的一种指针类型。它可以指向一个已经打开的文件,通过该指针可以对文件进行读取、写入、移动指针位置等操作。文件类型指针的声明方式如下:
FILE fp;
其中,FILE是C语言中定义的文件类型,fp是一个指向FILE类型的指针。在声明文件类型指针时,需要包含stdio.h头文件,该头文件中包含了对文件类型的定义。文件类型指针的声明方式与其他指针类型的声明方式相同,都是通过在变量名前面加上类型修饰符
来声明一个指针变量。例如,FILE *fp;声明了一个名为fp的文件类型指针变量。在声明文件类型指针时,可以不进行初始化,即不指向任何文件。在需要打开文件之前,可以将文件类型指针赋值为NULL,表示该指针不指向任何有效的文件。例如,FILE *fp = NULL;声明了一个名为fp的文件类型指针变量,并将其初始化为NULL。需要注意的是,文件类型指针只能指向已经打开的文件,不能直接指向文件名或文件路径。要打开一个文件,并将文件类型指针指向该文件,需要使用fopen函数。


打开与关闭文件

  1. fopen 函数打开数据文件
  • fopen 函数调用方式

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

1
2
FILE *fp;
fp = fopen("al", "r");

声明了一个名为fp的文件类型指针,并使用fopen函数打开了一个名为al的文件,打开模式为只读模式。

  • 使用文件方式:

上例中的“r”就是使用文件方式的一种

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

  1. fclose 函数关闭文件

fclose函数用于关闭一个已打开的文件。它的作用和意义如下:

  1. 释放资源:当我们打开一个文件时,操作系统会为该文件分配一些资源,如文件描述符等。当我们不再需要该文件时,通过调用fclose函数来关闭文件,可以释放这些资源,以便其他程序或操作系统可以使用它们。
  2. 保存文件:在关闭文件之前,fclose函数会将文件缓冲区中的数据写入到磁盘上的文件中。这样可以确保在关闭文件之后,文件中的数据已经被正确保存,而不会丢失或损坏。
  3. 刷新缓冲区:在关闭文件之前,fclose函数会刷新文件缓冲区,即将缓冲区中的数据写入到文件中。这样可以确保文件中的数据与缓冲区中的数据同步,避免数据丢失或不一致的情况。

需要注意的是,如果在关闭文件之后,我们仍然尝试对已关闭的文件进行读写操作,将会导致未定义的行为。因此,在关闭文件之后,我们应该确保不再对该文件进行任何操作。
fopen和fclose函数以不同的方式打开和关闭文件:

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
#include <stdio.h>

int main() {
FILE *fp;

// 使用 "r" 模式打开文件,以只读方式打开
fp = fopen("example.txt", "r");
if (fp == NULL) {
printf("无法打开文件 example.txt\n");
return 1;
}

// 文件操作...

// 关闭文件
fclose(fp);

// 使用 "w" 模式打开文件,以写入方式打开
fp = fopen("output.txt", "w");
if (fp == NULL) {
printf("无法打开文件 output.txt\n");
return 1;
}

// 文件操作...

// 关闭文件
fclose(fp);

// 使用 "a" 模式打开文件,以追加方式打开
fp = fopen("log.txt", "a");
if (fp == NULL) {
printf("无法打开文件 log.txt\n");
return 1;
}

// 文件操作...

// 关闭文件
fclose(fp);

return 0;
}

首先使用fopen函数以只读方式打开文件example.txt,然后进行一些文件操作,最后使用fclose函数关闭文件。接下来,我们使用fopen函数以写入方式打开文件output.txt,进行一些文件操作,然后使用fclose函数关闭文件。最后,我们使用fopen函数以追加方式打开文件log.txt,进行一些文件操作,然后使用fclose函数关闭文件。

文件字符读写

  1. fgetc 函数

调用方式:
fgetc(fp);
从fp指向的文件读入一个字符返回值:读成功,带回所读的字符,失败则返回文件结束标志EOF(即-1)


  1. fputc 函数

调用方式:
fputc(ch, fp);
把字符 ch 写到文件指针变量 fp 所指向的文件中返回值:输出成功,返回值就是输出的字符输出失败,则返回EOF(即-1)
使用fgetc和fputc函数进行示例:

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() {
FILE *fp1, *fp2;
int ch;

// 打开输入文件
fp1 = fopen("input.txt", "r");
if (fp1 == NULL) {
printf("无法打开输入文件 input.txt\n");
return 1;
}

// 打开输出文件
fp2 = fopen("output.txt", "w");
if (fp2 == NULL) {
printf("无法打开输出文件 output.txt\n");
return 1;
}

// 从输入文件读取字符,然后写入输出文件
while ((ch = fgetc(fp1)) != EOF) {
fputc(ch, fp2);
}

// 关闭文件
fclose(fp1);
fclose(fp2);

return 0;
}

首先使用fopen函数以只读方式打开输入文件input.txt,然后使用fopen函数以写入方式打开输出文件output.txt。接下来,我们使用fgetc函数从输入文件中读取一个字符,并将其作为int类型的返回值存储在变量ch中。然后,我们使用fputc函数将字符ch写入输出文件。通过循环重复以上步骤,直到到达输入文件的末尾。最后,我们使用fclose函数关闭输入文件和输出文件。


文件字符串读写

  1. fgets 函数
  • 调用形式:

fgets(str, n, fp);

  • 功能:

从 fp 指向的文件读入一个长度为(n -1)的字符串,存放带字符数组中 str 中

  • 返回值:

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

  1. fputs 函数
  • 调用形式:

fputs(str, fp);

  • 功能:

把 str 所指向的字符串写到文件指针变量 fp 所指向的文件中

  • 返回值:

输出成功,返回 0;否则返回非0值


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
#include <stdio.h>
#include <stdlib.h>

// 函数:写入文件
void writeFile(const char* filename, const char* content) {
FILE* file = fopen(filename, "w");
if (file == NULL) {
printf("无法打开文件\n");
return;
}

fputs(content, file);
fclose(file);
}

// 函数:读取文件
void readFile(const char* filename) {
FILE* file = fopen(filename, "r");
if (file == NULL) {
printf("无法打开文件\n");
return;
}

char buffer[100];
while (fgets(buffer, sizeof(buffer), file) != NULL) {
printf("%s", buffer);
}

fclose(file);
}

int main() {
const char* filename = "example.txt";
const char* content = "你好,世界!";

writeFile(filename, content);
printf("成功写入文件\n");

printf("读取到的文件内容:\n");
readFile(filename);

return 0;
}

定义了两个函数:writeFile用于将字符串写入文件,readFile用于从文件中读取内容并打印出来。在writeFile函数中,我们使用fputs函数将内容写入文件。在readFile函数中,我们使用fgets函数逐行读取文件内容,并将每行内容打印出来。在main函数中,我们首先调用writeFile将字符串”你好,世界!”写入名为”example.txt”的文件中,然后调用readFile从文件中读取内容,并打印出来。

格式化方式读写文本文件

在C语言中,是可以对文件进行格式化输入输出的,即用到 fprintf函数和 fscanf函数作用和prinf、scanf类似,但读写的对象是文件而不是终端

  • 调用方式:
    1
    2
    fprintf(文件指针, 格式符字符串, 输出表列);
    fscanf(文件指针, 格式字符串, 输入表列);
    举例:
    fprintf(fp,”%d,%6.2f”,i,f);
    将 int 型变量 i 和 float 型变量 f 的值按%d和%6.2f的格式输出到 fp 指向的文件中
    fscanf(fp,”%d,%f”,&i,&f);
    从fp指向的文件中读取数据,并将其存储在变量i和f中。

二进制文件数据块读写

在程序中不仅需要一次输入输出一个数据,而且常常需要一次输入输出一组数据(如数组或结构体变量的值)即用到 fread 函数和 fwrite 函数,在读写时是以二进制形式进行的。在向磁盘写数据时,直接将内存中一组数据原封不动、不加转换地复制到磁盘文件上,在读入时也是将磁盘文件中若干字节的内容一批读入内存

  • 调用形式:
    1
    2
    fread(buffer, size, count, fp);
    fwrite(buffer, size, count, fp);
    buffer:是一个地址。对fread来说,它是用来存放从文件读入的数据的存储区的地址。对fwrite来说,它是要把此地址开始的存储区中的数据向文件输出。size:要读写的字节数。count:要读写多少个数据项(每个数据项长度为size)fp:FILE类型指针

示例:

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
#include <stdio.h>

int main() {
FILE *fp;
char buffer[100];
int count;

// 打开文件以进行读取
fp = fopen("data.txt", "rb");
if (fp == NULL) {
printf("无法打开文件\n");
return 1;
}

// 从文件中读取数据到缓冲区
count = fread(buffer, sizeof(char), 100, fp);
if (count > 0) {
printf("成功读取 %d 个字符\n", count);
printf("读取的内容是: %s\n", buffer);
} else {
printf("读取失败\n");
}

// 关闭文件
fclose(fp);

// 打开文件以进行写入
fp = fopen("output.txt", "wb");
if (fp == NULL) {
printf("无法打开文件\n");
return 1;
}

// 将缓冲区中的数据写入文件
count = fwrite(buffer, sizeof(char), count, fp);
if (count > 0) {
printf("成功写入 %d 个字符\n", count);
} else {
printf("写入失败\n");
}

// 关闭文件
fclose(fp);

return 0;
}

首先使用fread函数从一个名为”data.txt”的文件中读取最多100个字符到缓冲区中然后,使用fwrite函数将缓冲区中的数据写入一个名为”output.txt”的文件中最后,打印出成功读取和写入的字符数。


随机读写数据文件

不同于文件进行顺序读写,随机访问读写不是按数据在文件中的物理位置次序进行读写而是可以对任何位置上的数据进行访问

  1. 文件位置标记及其定位
  • 文件位置标记

件位置标记是指在文件中标记当前读写位置的指针。它用于记录文件中当前操作的位置在文件中的偏移量,以便随时定位到文件的特定位置。文件位置标记通常由文件指针表示,文件指针是一个指向FILE结构体的指针。在打开文件时,文件指针会被初始化为文件的起始位置,随着读写操作的进行,文件指针会自动更新。通过文件位置标记,我们可以在文件中进行随机访问读写,不需要按照数据在文件中的物理位置次序进行操作。可以根据需要,将文件指针定位到任何位置,然后进行读写操作。

  • 文件位置标记的定位
    • 用 rewind 函数使文件位置标记指向文件开头

rewind 函数的作用是使文件位置标记重新返回文件的开头,次函数没有返回值。

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() {
FILE *file = fopen("example.txt", "r");
if (file == NULL) {
printf("无法打开文件。\n");
return 1;
}

// 读取文件内容
char buffer[100];
fgets(buffer, sizeof(buffer), file);
printf("重置前:%s", buffer);

// 重置文件位置标记到文件起始位置
rewind(file);

// 重新读取文件内容
fgets(buffer, sizeof(buffer), file);
printf("重置后:%s", buffer);

// 关闭文件
fclose(file);

return 0;
}

这个示例打开一个名为”example.txt”的文件,并从文件中读取一行内容。然后,使用rewind函数将文件位置标记重置为文件的起始位置,再次读取文件内容并打印出来。这样就可以看到在重新读取文件内容之前,文件位置标记已经回到了文件的起始位置。

  • 用 fseek 函数改变文件位置标记

fseek函数用于将文件指针定位到特定位置。调用形式:
int fseek(FILE *stream, long offset, int origin);

  • stream:指向要操作的文件的指针。
  • offset:偏移量,可以是正数、负数或零。正数表示向文件末尾方向移动,负数表示向文件起始位置方向移动,零表示不移动。
  • origin:起始位置,可以是以下值之一:
    • SEEK_SET:从文件起始位置开始计算偏移量。
    • SEEK_CUR:从当前位置开始计算偏移量。
    • SEEK_END:从文件末尾位置开始计算偏移量。

fseek函数的返回值为0表示成功,非零值表示失败。fseek函数的作用是根据指定的偏移量和起始位置将文件指针定位到特定位置。偏移量可以是正数、负数或零,用于指定相对于起始位置的移动距离。起始位置可以是SEEK_SET(从文件起始位置开始计算偏移量)、SEEK_CUR(从当前位置开始计算偏移量)或SEEK_END(从文件末尾位置开始计算偏移量)。使用fseek函数可以实现以下功能:

  • 将文件指针移动到文件的起始位置、当前位置或末尾位置。
  • 在文件中定位到特定位置进行读写操作。
  • 跳过指定数量的字节或记录。

需要注意的是,fseek函数在定位文件指针时可能会受到一些限制。例如,如果文件是以文本模式(而不是二进制模式)打开的,则fseek函数可能无法准确地定位到指定的位置。此外,对于某些类型的文件(如终端设备),fseek函数可能无法正常工作。在使用fseek函数之前,需要确保文件已经成功打开,并且需要检查函数的返回值以确定是否定位成功。如果fseek函数返回非零值,则表示定位失败,可能是由于文件指针超出了文件的范围或其他原因导致的。


  1. 随机读写

有了 rewind 和 fseek 函数就可以实现随机读写的操作了

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
#include <stdio.h>

int main() {
FILE *file = fopen("example.txt", "r");
if (file == NULL) {
printf("无法打开文件。\n");
return 1;
}

// 使用fseek将文件指针定位到文件末尾
fseek(file, 0, SEEK_END);

// 获取文件大小
long size = ftell(file);
printf("文件大小:%ld 字节\n", size);

// 使用fseek将文件指针定位到文件起始位置
fseek(file, 0, SEEK_SET);

// 读取文件内容
char buffer[100];
fgets(buffer, sizeof(buffer), file);
printf("重置前:%s", buffer);

// 使用rewind将文件指针重置到文件起始位置
rewind(file);

// 重新读取文件内容
fgets(buffer, sizeof(buffer), file);
printf("重置后:%s", buffer);

// 关闭文件
fclose(file);

return 0;
}

首先使用fseek函数将文件指针定位到文件末尾,然后使用ftell函数获取文件的大小,并将其打印出来。接下来,我们使用fseek函数将文件指针定位到文件起始位置,然后使用fgets函数读取文件的第一行内容并打印出来。最后,我们使用rewind函数将文件指针重置到文件起始位置,再次使用fgets函数读取文件的第一行内容并打印出来。