谭浩强C语言笔记

article/2025/8/20 7:37:13

文章目录

谭浩强C语言笔记

1.C语言基础知识

1.常量和变量

1.1入门程序

#include <stdio.h>int main() {// 定义一个整型变量 aint a;// 为变量 a 赋值为 100a = 100;return 0;
}

1.2 常量

在程序运行过程中,其值不能被改变的量叫做常量。常用的常量有以下几类:

1.2.1 整型常量

整型常量就是数学里面的定义的整数,例如:0,-1,999……

1.2.2 实型(浮点型)常量

实型常量就是数学里面定义的小数,例如:0.0,3.14,-1.8,999.0……

实型常量在 C 语言里面有两种表现形式:

  1. 正常的十进制小数形式:3.14……
  2. 指数形式:3.14e5(表示 3.14 * 10的5次方),-123.456E6(表示 -123.456 * 10的6次方),-346.87e-25(表示 -346.87 * 10-25)……

对于指数形式来说,e(E)前后必须要有数字,e(E)后面必须是整数,e(E)前面可以是整数也可以是浮点数。

1.2.3 字符常量
  1. 普通字符

使用单引号括起来的一个字符,如:'a','B','*','#','1'……不能写成:‘ab’,‘12’……

计算机在存储字符的时候,并不是存储字符本身,而是存储字符对应的 ASCII 码。

例如存储字符 ‘#’ 的时候,并不是存储其本身,而是存储 35(二进制形式)。需要的时候再通过 ASCII 映射出对应的字符。特殊的字符序号:‘A’ - 65, ‘a’ - 97

注意:单引号(‘’)不属于字符的一部分,这只是一个标志。标志着这是一个字符。

完整 ASCII 码表:https://baike.baidu.com/item/ASCII/309296

  1. 转义字符

除了普通字符外,C 语言中还有一些特殊的字符常量,这些字符常量以“\”开头。

这些常量都会有一些特殊的作用,例如之前遇到的 “\n” 表示换行,类似的字符还有很多。

转义字符字符值
一个单引号
"一个双引号
\一个反斜杠
\t水平制表符
\n换行符
?一个问号
\a警告
\b退格
\f换页
\r回车
\v垂直制表符
\o [o…] 其中 o 指代一个八进制数字该八进制数对应的 ASCII 字符。例如,\123 表示字符 ‘S’
\x h[h…] 其中 h 指代一个十六进制数字该十六进制数对应的 ASCII 字符。例如,\x53 表示字符 ‘S’
1.2.4 字符串常量

双引号括起来的若干字符称为字符串常量。例如:"Hello World!","123","A"……

千万不要错写成 'Hello World!',这种写法是错误的,单引号里面只能包含一个字符(转义字符除外)用来表示字符常量。

1.2.5 符号常量

在 C 语言中,我们会使用 #define 指令来定义一个符号常量,如下面:

#define PI 3.14`,`#define NAME "Jack"

注意了,定义符号常量的时候,结尾是没有分号的。编译时,会将所有的符号常量替换成对应的值。

1.3 变量

变量是一个有名字的、具有特定属性的存储单元。它是用来存放数据的,被存放的数据叫做变量的值。在程序运行期间,变量的值是可以改变的。

变量必须先定义,后使用。定义的时候必须指明变量的名字和类型,例如代码 int n = 10;

(等价于int n; n = 10;)就是定义了一个整型变量 n。其中 n 是变量的名字,int 表示这个变量是整型的,后面使用赋值符号(=)来给变量赋初试值 10。因此,我们可以使用“值为 10 的变量 n”来描述这个变量。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aZwspQiq-1665324647760)(E:\Blog\source\images\1656666146861-84c1cff3-eb86-4cf8-8897-2ca851017566.png)]

看内存图:

矩形框就表示一个存储单元,这个存储单元在内存中的地址是1002。

存储单元里面存着整数 10,n 指向这个存储单元(其实有映射表)。

每次使用 n 的时候,默认取 n 指向的单元里的值。

1.4 常变量

我们在定义变量的时候,在前面加上关键字 const就会使这个变量变为常变量。如代码

const int n = 10;就是定义了一个值为 10 的整型常变量。

可以看出,常变量和变量类似有名字、有类型还可以存值。但常变量归根到底还是常量,因此常变量一旦定义,其值就不可以改变了。

正是由于常变量一旦定义就不能改变其值的特性,导致我们在定义常变量的时候必须给予其初值。否则这个常变量就没有任何意义。

和符号常量相比,常变量使用起来更加方便,因此我们推荐使用常变量

简要阐述一下常变量符号常量之间的异同:

相同点:两者都给常量命名;二者都是用来定义常量的

不同点:

  1. 符号常量:#define 来定义;不关注常量的类型;该常量不分配内存
  2. 常变量:类似于变量定义;关注常量类型;分配内存

2.标识符和关键字

2.1 标识符

标识符主要用来标识变量和函数。一个合法的标识符由数字、英文字母(a-z,A-Z)、下划线_)构成,其中数字不能开头且标识符不能和关键字相同

C 语言的标识符是区分大小写的,例如 nameNameNAME都是不同的标识符。

2.2 关键字
autobreakcase
charconstcontinue
defaultdodouble
elseenumextern
floatforgoto
ifintlong
registerreturnshort
signedsizeofstatic
structswitchtypedef
unsignedunionvoid
volatilewhile
inlinerestrict_Bool
_Complex_Imaginary
2.3 小习题

判断下列标识符的合法性,将结果填入后面的框内(✓或×):

namen0_0n
_age0M.D.John
i51job
intv_v
int_inline
INTProgram$

3. 基本数据类型

内存单位:比特位(bit),字节(B),千字节(KB),兆字节(MB)

换算:1 B = 8 位;1 KB = 1024 B;1 MB = 1024 KB

可以使用 sizeof 运算符来查看自己编译器下某类型的所占据的字节数,如 sizeof(int)

3.1 整型
类型关键字字节数取值范围
整型int4-231 ~ (231 - 1)
无符号整型unsigned int40 ~ (232 - 1)
短整型short2-215 ~ (215 - 1)
无符号短整型unsigned short20 ~ (216 - 1)
长整型long4-231 ~ (231 - 1)
无符号长整型unsigned long40 ~ (232 - 1)
长长整型long long8-263 ~ (263 - 1)
无符号长长整型unsigned long long80 ~ (264 - 1)
3.2 字符型

我们之前提到过,字符型变量里面存储的是该字符的 ASCII 码,而 ASCII 码本质上是一个整数,所以字符型变量本质上还是一个整型。经过观察可以发现,ASCII 里面只有 127 个字符,我们使用 1 个字节(8 位)就可以表示出所有的字符。所以使用int来表示字符是很浪费内存的,因此特地新开一个类型**char**,这是 character 的简写。

// 下面两句话是等价的,都表示字符 A,存储的时候都存储 65(二进制)。
char c = 65;
char c = 'A';// 问:既然存储的时候把 A 存储成 65,那么输出的时候怎么变回 A?
// %d 是十进制整数的输出占位符;%c 是字符输出占位符。
printf("%d, %c", c1, c1);

参考 ASCII 表,思考:如何将一个大写字母变为对应的小写字母?

答:大写字母的ASCII码值比小写字母的ASCII值小32,所以只需要在char 类型下 +32 就可以了。

给定 0-9 之间的一个整数,将其转成对应的字符,即 0-‘0’, 9-‘9’

#include <stdio.h>
int main() {int a;char b;scanf("%d",&a);   b=a+'0';    printf("转换后的字符为:%c\n",b);return 0;
}
3.3 浮点型

浮点型也就是实型数据。3.14 * 100,0.314 * 101,31.4 * 10-1 都表示同一个数据。可以看出,我们可以通过操作指数来使得小数点在数字之间浮动,故名浮点数。

类型关键字字节数
单精度浮点型float4
双精度浮点型double8
3.4 常量的类型及转化

对于整数(例如 99)来讲默认类型是 int,对于小数(例如 3.14)来讲默认类型是 double。

所以代码int n = 99;没什么问题。

但是代码float f = 3.14;会报警告,你可以在数字结尾加一个f(或 F)表示这个数字是单精度浮点型,如:float f = 3.14f;

4.运算符

4.1 算术运算符

运算符含义举例
+正号运算符(默认可省略)+5, 5
-负号运算符,用来表示负数-5
+加法运算符,求两个数的和1 + 2
-减法运算符,求两个数的差3 - 2
*乘法运算符,求两个数的积2 * 3
/除法运算符,求两个数的商 若操作数都是整型,则表示整除操作数里有浮点型,则表示除法整除的结果会向 0 取整: 5 / 2 = 2, 6 / 2 = 3, -5 / 2 = -2 浮点数除法就是正常小数
%模运算(取余运算)符。a % b 表示获取 a 除以 b 的余数。只有整数之前可以取余,浮点数不行。 5 % 2 = 1,6 % 2 = 0

4.2 自增(++),自减(–)

自增(++)、自减(–)运算符的作用是使变量的值加1 或 减 1。例如:

++i,–i (在使用 i 之前,先将 i 的值加 1 或减 1

i++,i-- (在使用 i 之后,再将 i 的值加 1 或减 1

++ 和 – 可以作用在所有数字上,不一定非是整数,浮点数也可以。

表面上看i++和++i都表示将 i 的值加 1,但两者之间存在着加 1 顺序的区别。例如:

int i = 1, j;// ++ 在后就后加 1
// 等价于:j = i; i = i + 1;
j = i++;// ++ 在前就先加 1
// 等价于:i = i + 1; j = i;
j = ++i;int i = 1, j = 1;
// 下面等价于 (i++)+j
i+++j;

4.3 混合运算

4.3.1 优先级

在混合运算过程中,一个表达式里会有多个不同的运算符,那么在运算的时候依据它们的优先级来计算。例如:

1 + 2 * 5是一个包含了加法和乘法的表达式,很显然***运算符优先于+**运算符,因此计算结果是 11。

n = 1 + 2表达式包含了赋值运算符=和加法运算符+,显然加法运算符优先级高,因此 n 的值为 3 而不是 1。

4.3.2 结合方向

结合方向就是遇到同级运算符时,应该先算左边还是右边。例如:

左结合案例:1 + 3 - 2加法和减法是同级运算符,但是由于加减是左结合的,因此先加后减。

右结合案例:n = i = 1两个赋值运算是同级的,又由于赋值运算是右结合的,因此先执行 i = 1,再执行 n = i

记住这两个案例:1. 算术运算符都是左结合; 2. 赋值运算符是右结合。

4.3.3 自动类型转换

各数值类型大小关系:char < int < float < double

若某运算前后两个数据的类型不一致,则按照“小变大”的原则同步双方的类型。

但是注意:不要以上帝视角去转换类型,而是走一步看一步。例如:5 / 2 / 2.0

一眼看穿,自动类型转换都是将小类型往大类型转。

4.3.4 强制类型转换

有些时候,自动转换类型并不能满足我们的要求,需要手动转换变量的类型,这时就用到强制类型转换了。强转时只需要在要转的变量或表达式前加上(要转的类型)。例如:

1.(int) a表示将量 a 强转成 int 型。

2.(double) (a + b)表示将 a + b 的结果强转成 double 型。

3.(double) a + b表示将 a 强转成 double 型再将其与 b 想加。

强制类型转换,可以小转大,也可以大转小,取决于你的心意。

小转大,数据会完全包容,没什么问题。但大转小存在数据精度丢失的风险,例如n = (int) 3.14,我们将double 类型数据 3.14 强转成 int 型,那么小数部分会丢失,也就是会向 0 取整。

4.3.5 C 语言优先级表

https://www.yuque.com/docs/share/ea772be5-f17c-4091-bf6f-4b38a31fdcd8?#

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-udREvBFb-1665324647761)(E:\Blog\source\images\1656827642693-e0dd3997-a2ce-4bee-8e45-5958dc6e4867.png)]

4.3.6 小习题

1.口算下面的运行结果并与编程运行结果比较:

表达式结果类型结果值
(double) 2 * 5 / 4 + 1double3.5
5 / 2 + 1.2 * 2 - 1double3.4
(double)(7 / 2 + 1)double4.0
5 / 2 + 7 / 2 (5 + 7) / 2int2
70 % ‘A’ * 10 % 2 / 2int0

2.对于任意的四位整数 n,如何取到 n 的个位数、十位数、百位数、千位数?


4.4.位运算

运算符含义运算符含义
&按位与~按位取反
|按位或<<左移
^按位异或>>右移

注:参加位运算的对象只能是整型或字符型的数据,不能为实型数据。

例题 2的n次方计算

#include <stdio.h>
int main()
{int a,n;a=1;scanf("%d",&n);a=a<<n;printf("%d",a);return 0;
}

5. 语句

5.1 语句分类

5.1.1 控制语句

控制语句是用于完成一定的控制功能,C 语言中有 9 种控制语句,它们分别是:

  1. if ()…else… (条件语句)
  2. for ()… (循环语句)
  3. while()… (循环语句)
  4. do…while() (循环语句)
  5. continue (结束本次循环语句)
  6. break (终止 switch 或循环语句)
  7. switch (多分支选择语句)
  8. return (函数返回语句)
  9. goto (转向语句。极其不推荐使用!!)

5.1.2 函数调用语句

例如:printf("Hello World!\n");就是一个函数调用语句。

5.1.3 表达式语句

一个表达式后面加上;就变成了表达式语句。例如n = 10是一个表达式,而n = 10;就是一个表达式语句。相似的,i++是一个表达式,而i++;就是一个语句。

5.1.4 空语句

空语句就是一个单独的;。空语句什么都不会做,但它就是一个语句。

5.1.5 复合语句

将若干条普通语句使用{}括起来就变成了一条复合语句。例如下面就是一条复合语句:

{int n = 10;n++;printf("%d", n);
}

5.2 赋值

5.2.1 案例

给定三角形三边长 a、b、c,求该三角形的面积。

已知海伦公式求三角形面积为:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6TcisfBv-1665324647762)(E:\Blog\source\images\76f5b6258a519c9f741d61fd85e001a3.svg+xml)],其中 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-md846SGa-1665324647762)(E:\Blog\source\images\bf9b283fa6a817d4465843a9355c4bb9.svg+xml)]

#include <stdio.h>
#include <math.h>int main() {double a, b, c, p, S;a = 3.67;b = 5.43;c = 6.21;p = (a + b + c) / 2;S = sqrt(p * (p - a) * (p - b) * (p - c));printf("三角形面积为:%f\n", S);
}

5.2.2 赋值运算符

赋值运算符的作用是将一个数据赋给一个变量。例如n = 10的作用是将 10 这个常量赋给变量 n。

n = 1 + 2作用是将 1 + 2 这个表达式的值赋给变量 n。

5.2.3 赋值运算符的变种

表达式等价写法
a += ba = a + b
a -= ba = a - b
a *= ba = a * b
a /= ba = a / b
a %= ba = a % b

5.2.4 赋值表达式

前面的语句分类里面我们知道,表达式语句由“表达式+;”构成。所以赋值语句也是赋值表达式加“;”组成,那何为赋值表达式呢?答:由赋值运算符将变量与表达式连接起来的式子称之为赋值表达式。

我们知道所有的表达式都是有值的,赋值表达式作为表达式的一员也是有值的,赋值表达式的值就是被赋值变量的值。例如赋值表达式n = 10的值是 10。注意区分表达式的值与被赋值变量的值之间的不同,虽然两者值是相同的。

赋值表达式左侧应该是一个变量,右侧可以是变量、常量、表达式。当右侧还是一个赋值表达式的时候是什么情况呢?例如a = b = 5,这完全等价于 a = (b = 5)。逐步分析,这个表达式会先执行b = 5,然后将其值赋给 a。

分析以下赋值表达式的含义:

表达式a, b, c 的值表达式的值
a = b = c = 5a = , b = , c =返回 a 的值,
a = (b = 3) * (c = 4)a = , b = , c =返回 a 的值,
已知:a=3, b=4, c=5a += (b *= 2) / (c -= 3)a = , b = , c =返回 a 的值,

5.2.5 赋值时强转

对某变量赋值但值与变量的类型不匹配时,会发生赋值时强转。例如int n = 3.14;可以看出,n 是 int 型,但赋值时却使用 3.14(double 型数据)为其赋值。此时会进行强转,相当于int n = (int) 3.14;强转的规则上节讲过,会丢失精度。

5.2.6 定义变量时赋予初值

// 定义整型变量 a 时赋予初值 1
int a = 1;// 定义整型变量 a、b、c 并为 b 赋予初值 2
int a, b = 2, c;// 定义整型变量 a、b、c 并将初值都设为 1
int a = 1, b = 1, c = 1;// 这是错误的,不符合语法的
int a = b = c = 1;

6. 输入和输出

输入和输出主要使用printfscanf函数来操作,这两个函数都在stdio.h头文件里声明,因此使用这两个函数的时候,一定记住要在文件开头导入这个头文件#include <stdio.h>

6.1 输出函数 printf

printf 函数的一般格式:printf(格式字符串, 输出列表)

其中,输出列表不是必须的。当格式字符串里没有占位符的时候,就可以省略输出列表;否则有多少占位符,输出列表里就得有多少元素。什么是占位符?

6.1.1 占位符案例

// 占位符都以 % 开头,下面的 %d 就是一个整型占位符。
// 输出时,会使用后面的 999 替换掉这个 %d,因此被称为占位符。
printf("一刀 %d", 999);// 下面的例子有两个占位符,一个是整型 %d 占位符,一个是双精度浮点型 %lf 占位符
// 后面的数据按照顺序和前面的占位符对应起来,注意数据要和占位符类型对应起来。
printf("老铁%d,圆周率是:%lf", 666, 3.14159);

6.1.2 所有占位符

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RdxISVDw-1665324647763)(E:\Blog\source\images\1656810721834-e652c4d9-3685-42c5-b469-b9787fc7c3fb.png)]
在这里插入图片描述

6.2 输入函数 scanf

scanf 函数的一般形式:scanf(格式字符串, 地址列表)

格式字符串和 printf 里面的是一样的,但是地址列表和输出列表就有极大不同了。地址列表里面可以是变量的地址,也可以是数组的首地址。scanf 函数也是在格式化字符串里使用占位符的方式来确定你输入的是什么类型的数据,占位符格式和上述差不多。

6.2.1 常用占位符

占位符含义
%c字符占位符
%d十进制整数占位符
%f单精度浮点型占位符
%lf双精度浮点型占位符
%s字符串占位符

6.2.2 输入函数小案例

  1. 整型、浮点型数据输入

    int a, b;
    double d;// 要求输入两个整数,使用 a 和 b 来接收。
    // 注意,这边 a 和 b 前面都有一个 &,这是取地址运算符。
    // 输入的时候,格式要和格式字符串完全对应,所以我应当这样输入“a=10, b=99”
    scanf("a=%d, b=%d", &a, &b);// 输入样本:10, 3.14
    scanf("%d, %lf", &a, &d);// 这是一种特殊情形,两个占位符放在一起
    // 如果我们还是按照格式输入,那会有歧义,所以这种特殊情况,我们需要在两个数字间添加一个空格
    scanf("%d%d", &a, &b);
    scanf("%d%lf", &a, &d);
    

2.字符型数据输入

char ch, ch2;// 直接输入一个字符即可
scanf("%c", &ch);// 按照格式,输入:ch=A
scanf("ch=%c", &ch);// 按照格式输入:A B
scanf("%c %c", &ch, &ch2);// 这个和上面不太一样,因为空格本身就是一个字符,不能擅自在两个字符之间加空格
// 又由于字符之间连着输入是不会有歧义的,所以直接输入而不加空格:AB
scanf("%c%c", &ch, &ch2);

6.3 字符输入输出函数

6.3.1 字符输出函数 putchar

一般形式:putchar(ch),其中 ch 是一个字符。可以是普通字符,也可以是转义字符。

// 输出 A 字符
// 等价于:printf("%c", 'A');
putchar('A');// 输出一个换行符
putchar('\n');

6.3.2 字符输入函数 getchar

// 输入一个字符并存到 ch 里面
char ch = getchar();

6.4 小习题

6.4.2 输入 1 个小写字母,输出其大写字母形式。

#include <stdio.h>
int main() {char c1,c2;c1=getchar();  // 从键盘读入一个字母,赋值给字符变量c1c2=c1-32;    //求对应小写字母的ASCII码,放在字符变量c2中putchar(c2);  //输出c2的值putchar('\n'); //输出换行return 0;
}
#include <stdio.h>
int main() {char c1,c2;c1=getchar();  // 从键盘读入一个字母,赋值给字符变量c1c2=c1-32;    //求对应小写字母的ASCII码,放在字符变量c2中printf("%c的大写字母为:%c\n",c1,c2); //输出c1 c2的值return 0;
}

6.4.2 输入 1 个数字,输出对应字母形式。对应规则:0-A,1-B,……

#include <stdio.h>
int main() {int n;char c;scanf("%d",&n);c=n+65;printf("%d-%c\n",n,c); return 0;
}

6.4.3 键盘输入一个数 x,分别输出:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Wq81LGhU-1665324647764)(E:\Blog\source\images\ce28e65aec62bfb9ab16663f90e35407.svg+xml)]

关于 C 语言中各数学函数的使用参考:《附录》。对数函数的换底公式:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oQJM6wXb-1665324647764)(E:\Blog\source\images\06f247245f5a5440875ae7854699b6ee.svg+xml)]

#include <stdio.h>
#include<math.h>
int main() {double x,a,b,c,d,e;scanf("%lf",&x);a=exp(x);b=log(x);c= (log10(x)/log10(x));d= pow(x,x);e= sqrt(x+1);printf("a=%lf\n b=%lf\n c=%lf\n d=%lf\n e=%lf\n ",a,b,c,d,e);return 0;
}

6.4.4 使用 getchar 函数读入一个字符存到 ch 里面,思考:

1.ch 这个变量可以定义为 int 还是 char?

2.如果要用字符形态输出 ch,有什么方法?ASCII 码形态呢?

3.char ch;与int ch;有何不同?

2.选择结构

2.2 if 语句

2.2.1 if 语句的一般形式

if (条件 1) 语句1;
else if (条件 2) 语句2;
...
else if (条件 n-1) 语句 n-1;
else 语句 n;

其中所有 else if 分支和 else 分支都可以省略。

2.2.2 案例体验

  1. 输入一个数 x,输出 f(x)。其中 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ox1jwphN-1665324647764)(E:\Blog\source\images\a7e6560a0698dc8c053697de6a5687b5.svg+xml)]
#include <stdio.h>
int main() {
int x;scanf("%d",&x);if(x<=0)x=x*x;else if(x>0)x=x*x*x;printf("x=%d\n",x);return 0;
}

2.输入一个 1-100之间的整数,返回这个分数对应的等级。分数与等级的对应关系如下:

    1. 1-59:D
    2. 60-79:C
    3. 80-94:B
    4. 95-100:A
#include <stdio.h>
int main() {
int x;scanf("%d",&x);if(x>=1&&x<59)printf("D\n");else if(x>=60&&x<79)printf("C\n");else if(x>=80&&x<94)printf("B\n");else if(x>=95&&x<100)printf("A\n");return 0;
}

3.输入两个整数 a 和 b,若 a > b 则交换 a 与 b 的值,最后输出 a 和 b。

#include <stdio.h>
int main() {int a,b,c;scanf("%d%d",&a,&b);if(a>b){ c=a;    //大括号很重要 如果不加大括号,就没发判断 a<b的状态了a=b;b=c;}printf("a=%d b=%d\n",a,b);return 0;
}

2.2.3 if 结构的嵌套

ifelse ifelse语句里,可以继续嵌套 if 结构语句。

看一个例子:输入三个数a、b、c,输出其中的最大值。

/* 算法思想:通过两个数相比较,得出大的那个数然后再和第三个数进行比较 若大于第三个数则是最大值 若小于第三个数 则第三个数为最大值*/
#include <stdio.h>
int main() {int a,b,c,max;scanf("%d %d %d",&a,&b,&c);if(a>b){ if(a>c)max=a;elsemax=c;}else { if(b>c)max=b;elsemax=c;}printf("max=%d\n",max);return 0;
}

2.2.4 条件运算符

条件运算符的一般形式:表达式 1 ? 表达式 2 : 表达式 3 (三目运算)

运行流程:计算表达式 1 的值,若为真(非0),则返回表达式 2 的值,否则返回表达式 3 的值。

  1. 输入两个数 a、b,返回两者中较大的那一个数

    #include <stdio.h>
    int main() {int a,b,max;scanf("%d %d",&a,&b);max=(a>b)?a:b;  //注意括号一定要加printf("最大的数为:%d\n",max);return 0;
    }
    

2.返回 a, b, c 里面最大值

#include <stdio.h>
int main() {int a,b,c,max;scanf("%d %d %d",&a,&b,&c);max=(a>b)?a:b;max = max>c?b:c;printf("最大的数为:%d\n",max);return 0;
}

3.输入一个字符,若它是大写字母就将其转换成对应的小写字母,否则不转换

#include <stdio.h>
int main() {char ch,Mch;scanf("%c",&ch);
(ch>='A'&&ch<='Z')?ch+=32:ch;printf("转换后的字母为:%c\n",ch);return 0;
}

2.3 多分支 switch

if 一般用于两个分支的选择结构,若是有多分支操作,你需要使用if、else if、else的组合,这样的组合效率很低,原因后面说。此时 switch 应运而生,switch 一般形式:

switch(整型表达式) {case 常量 1: 语句 1;case 常量 2: 语句 2;……case 常量 n: 语句 n;default: 语句 n + 1;
}

switch 语句中的表达值必须是一个整数,case 后面必须跟着不重复的常量。执行流程:设表达式的值为 n,从所有常量里找到与 n 值相等的那个 case,执行后面的语句;若找不到相等的值,就执行 default 后面的语句,若 default 语句被省略了,就啥都不执行。

注意点:假设,表达式的值等于常量 2 的值,那么程序不仅仅只执行语句 2,后面的所有语句都会执行,若是不想全部执行,那就需要在恰当的位置使用 break

来个例子感受感受:

已知 A 对应 85-100分,B 对应 70-84 分,C 对应 60-69 分,D 对应 < 60分。要求输入分数的等级,输出对应分数范围。

#include <stdio.h>
int main() {
char grade;
scanf("%c",&grade);
switch(grade)
{
case 'A': printf("85~100\n");break;
case 'B': printf("70~84\n");break;
case 'C': printf("60~69\n");break;
case 'D': printf("小于60\n");break;
default : printf("输入数据错误!\n");
}	
return 0;
}

2.4.1 设 a = 3, b = 4, c = 5,求下列逻辑表达式的值

  1. a + b > c && b == c 0
  2. a || b + c && b - c 1
  3. !(a > b) && !c || 1 0
  4. !(x=a) && (y=b) && 0 0 (遇到逻辑运算符需要先在逻辑运算两边的表达式先加上括号)
  5. !(a + b) + c - 1 && b + c / 2 1

2.4.2 表达式类型的题

  1. 已知i = 1, j = 2, k = 3,则执行完代码int m = i-- && j++ || k--;之后,各变量的值分别为i = , j = , k = , m =
  2. 已知i = j = k = 1,则执行完代码int m = ++i || ++j && ++k;之后,各变量的值分别为 i = , j = , k = , m =
  3. 已知int m = 1, n = 2;则表达式++m == n的值为:
  4. 已知x = 43, y = 0, c = 'a'则表达式x >= y && c <= 'b' && !y的值为:

2.4.3 判断一个数是否是完全平方数


2.4.4 给一个百分制数,输出该分数对应的等级

条件:90 分及以上为 A,80~89 为 B,70~79 为 C,60~69为 D,其余为 E

/* 使用 if 分支写 */
#include <stdio.h>int main() {int score;char grade;scanf("%d", &score);if (score <= 59) grade = 'E';else if (score <= 69) grade = 'D';else if (score <= 79) grade = 'C';else if (score <= 89) grade = 'B';else grade = 'A';putchar(grade);return 0;
}
/*使用switch写*/

2.4.5 输入一个年份和月份 year、month,判断该年是不是闰年,该月有多少天



2.4.6 输入三条边长,判断这三条边能构成:等腰三角形、等边三角形、直角三角形、不是三角形


2.4.7 水仙花数

对于一个三位数 n,设 a, b, c 是其百位、十位、个位数,若 a3 + b3 + c3 = n 则称 n 为水仙花数。

键盘输入一个三位正整数,判断 n 是否是水仙花数。


3.循环结构

3.1while 循环

3.1.1 引言

循环结构是很常用的一个结构。有很多案例,例如:键盘输入 50 个数字代表一个班级学生的成绩。我们可以使用scanf语句来获取键盘输入,但现在要输入 50 个数字,难道要使用 50 次 scanf?那遇到 500 次、5000 次呢?

理智分析我们可以发现,对于录入分数这个操作,我们都是使用scanf操作。既然操作都是一样的,我们就没必要反复写一样的代码了。

while 循环的一般形式:

// 只有条件为“真”才会执行循环体内容
while (条件) {循环体语句;   
}

3.1.2 小习题

使用循环求解 1 + 2 + 3 + ……+ 1000

#include <stdio.h>
int main() {
int sum=0,i=1;
while(i<=1000){sum+=i;i++;
}
printf("sum=%d\n",sum);
return 0;
}

3.2 do…while 循环

3.2.1 引言

do…while 循环是先做再看条件的循环,是《第 2 节算法》里面的的直到型循环的一类。其一般形式为:

/*代码自上而下先执行循环体内容,然后再去判断条件是否满足。满足条件,继续执行循环体……直到某一次条件不满足,退出循环。
*/do {循环体;
} while(条件);

3.2.2 梅开二度

使用 do…while 循环实现 1000 内数字的求和

#include <stdio.h>
int main() {
int sum=0,i=1;
do{sum+=i;i++;
}while(i<=1000);
printf("sum=%d\n",sum);
return 0;
}

3.2.3 对比 while 和 do…while

相同点:两者都是循环语句

不同点:while 语句的循环体部分可能一次都不执行;do…while 循环体部分最少执行一次。

案例:《谭》P115

3.3 for 循环

3.3.1 引言

for 循环的一般形式是:

for (初始化表达式; 循环条件; 变量修改表达式) {循环体语句;   
}for (; ; ) {循环体语句;   
}

3.3.2 小案例

  1. 使用 for 循环求 1000 以内数字的和
#include <stdio.h>
int main() {
int sum=0,i=1;
for(i=1;i<100;i++){sum+=i;
}
printf("sum=%d\n",sum);
return 0;
}

2.从键盘读取字符并输出这个字符直到遇到换行符

//可以使用while和for来写
char ch;while((ch = getchar()) != '\n') putchar(ch);for(; (ch= getchar()) != '\n'); putchar(ch)

3.输入一个数 n,输出其满足角谷猜想的角谷路径。

所谓角谷猜想是指,对任意正整数执行若干次[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5QKgCq3h-1665324647765)(E:\Blog\source\images\d20f3c7aace6efc568d7144830d7e6b0.svg+xml)]总会得到结果 1。


4.键盘输入一行字符(以换行结尾),统计字符串中数字、字母、空格出现的次数


3.3 for 循环

3.3.1 引言

for 循环的一般形式是:

for (初始化表达式; 循环条件; 变量修改表达式) {循环体语句;   
}for (; ; ) {循环体语句;   
}

3.3.2 小案例

  1. 使用 for 循环求 1000 以内数字的和
#include <stdio.h>
int main() {
int sum=0,i=1;
for(i=1;i<100;i++){sum+=i;
}
printf("sum=%d\n",sum);
return 0;
}

2.从键盘读取字符并输出这个字符直到遇到换行符

//可以使用while和for来写
char ch;while((ch = getchar()) != '\n') putchar(ch);for(; (ch= getchar()) != '\n'); putchar(ch)

3.输入一个数 n,输出其满足角谷猜想的角谷路径。

所谓角谷猜想是指,对任意正整数执行若干次[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rJir2Zuo-1665324647766)(E:\Blog\source\images\d20f3c7aace6efc568d7144830d7e6b0.svg+xml)]总会得到结果 1。


4.键盘输入一行字符(以换行结尾),统计字符串中数字、字母、空格出现的次数


3.4 重循环(循环的嵌套)

重循环也叫循环嵌套,即在一个循环体语句里面包含另一个循环语句。

3种循环(while循环、do…while循环和for循环)可以互相嵌套。

案例:输出如下矩阵

1	2	3	4	5
2	4	6	8	10
3	6	9	12	15
4	8	12	16	20
#include<stdio.h>
int main(){int i,j,n=0;for(i=1;i<=4;i++)for(j=1;j<=5;j++,n++)   //n用来累计输出数据的个数{ if(n%5==0)        //控制在输出5个数据后换行printf("\n");printf("%d\t",i*j);}printf("\n");return 0;
}

3.5 循环控制

3.5.1 break

在循环或者 switch 语句中,使用 break 语句可以跳出循环或 switch 语句。这主要用于提前结束循环或者作为循环出口使用。

案例:在全校 1000 个学生中进行募捐,当 1000 个同学都捐完或捐款总额达到 10000元 的时候停止募捐。

#include <stdio.h>
#define SUM 100000  //指定符号常量SUM代表100000
int main() 
{float amount,total;int i;for(i=1,total=0;i<=1000;i++){printf("请您输入捐款的金额:");scanf("f%",&amount);total=total+amount;if(total>=SUM) break;}printf("本次捐款人数为:%d\n",i);return 0;
}

3.5.2 continue

continue 作用于循环中,跳过后面未被执行的循环体语句

案例:输出 100 ~ 200 之间所有不能被 3 整除的数

#include <stdio.h>
int main() 
{int i;for(i=100;i<=200;i++){if(i%3 == 0)continue;printf("num=%d\t",i);}return 0;
}

3.5.3 break 和 continue 区别

观察如下两种循环结构,分析执行流程

while (表达式1) {语句1;if (表达式2) break;语句2;
}
  1. 若“表达式1”值为真,进入循环体
  2. 执行“语句1”
  3. 若“表达式2”值为真,执行 break 退出循环
  4. 否则走正常流程
while (表达式1) {语句1;if (表达式2) continue;语句2;
}
  1. 若“表达式1”值为真,进入循环体
  2. 执行“语句1”
  3. 若“表达式2”值为真,执行 continue 结束本次循环,直接到达“表达式1”处
  4. 否则走正常流程

3.6习题

3.6.1 按照下面的格式输出乘法表

1 * 1 = 1
1 * 2 = 2	2 * 2 = 4
...
1 * 9 = 9	2 * 9 = 18	...	9 * 9 = 81

3.6.2 输入 10 个数,输出其中的最大值

#include<stdio.h>
int main(){int n,max=0;for(int i;i<5;i++){scanf("%d",&n);if(n>max)max=n;}printf("%d",max);return 0;
}

3.6.3 求 π 的近似值

已知:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LHWGTkYq-1665324647766)(E:\Blog\source\images\1113836e8d5eb0a6c3a79226d7332138.svg+xml)]当公式中某一项绝对值小于 10-6为止。


3.6.4 输入正整数 n,判断其是否是素数


3.6.5 输出斐波那契数列前20项

已知,满足条件[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-va5x89qM-1665324647767)(E:\Blog\source\images\2b6b32b05b12c1a6f07f8a1c7e9a07ce.svg+xml)]的数列叫做斐波那契数列。


3.6.6 输入一个正整数 n,输出其反序数

例如 123 的反序数是 321,100 的反序数是 1。


3.6.7 密文

输入一行字符(以换行结尾),对于串中的英文字母(包含大写和小写字母)做后移 4 位操作,得到一个新串并输出。

后移 4 位操作是指A -> E, b -> f, W -> A, z -> d


3.6.8 按照如下规律输出图案

image.pngimage.png


1."水仙花数"是指一个三位数,它个数位数字的立方和这个数字的数值相等,编程打印所有的水仙花数

#include<stdio.h>
int main(){int d0,d1,d2,n;printf("这些数字是:");for(n=100;n<=999;n++){d0=n%10;d1=n%100/10;d2=n/100;if(n==d0*d0*d0+d1*d1*d1+d2*d2*d2)printf("%5d",n);}printf("\n");return 0;
}

4.数组

4.1 引言

之前已经介绍完所有的基本数据类型,但很多时候只用基本类型是远远不够的,尤其是批量处理数据的时候。例如,一个班级 30 个学生,现在要统计每个人的成绩怎么办?如果使用基本数据类型,那就需要定义 30 个变量:s1, s2, …, s30。那统计一个学校 50000 个数据呢?就会很麻烦。

我们观察一下发现,这些数据类型都是一致的,只是各个值不同。对于这种一类的数据,人们会使用统一的名字来命名它们,使用下标来标志各个不同的值。就例如上面说的:s1, s2, …, sn。这边 s 就表示这些数据的名字,而右下角的下标用来区分各个数据。

在 C 语言中,使用数组来表示这样一组有序数据的集合。s 就是数组名,数字就是下标。同一个数组中的数据类型是一致的,不能把不同类型的数据(如分数和性别)放到同一个数组中。因为计算机键盘无法输入下标,所以我们使用这样的语法结构来代替:s[n],这就表示 s 数组中下标为 n 的元素。

4.2 维度

数组是存在维度的,最少是 1 维。例如s[1]就是一维的,可以表示第一名学生的成绩。s[2][1]就是 2 二维的,可以用来表示 2 班第一名的成绩。s[2][3][1]是 3 维的,可以用来表示 2 年纪 3 班第 1 名的成绩。以此类推,使用方式都是大同小异。

4.2一维数组

4.2.1 一维数组的定义

一维数组定义的一般形式:

类型修饰符 数组名[常量或常量表达式];

例如,int a[10];表示定义一个名字为 a、类型为 int、且大小为 10 的数组。对数组命名时要满足 C 标识符规范。这边的 10 表示这个数组最多能存 10 个数据,取数据时按照下标获取元素,但是注意下标从 0 开始。所以,争取的取元素有a[0], a[1], ..., a[9],错误的取元素是a[10]。因为一共定义 10 个空间,下标当然是 0~9,理所当然不存在 a[10]。

定义数组的时候表示数组大小必须使用常量或常量表达式,看下例子:

// 合法,直接使用常量定义
int a[10];// 合法,使用常量表达式定义
int a[10 + 5];// 合法,使用常量定义
const int n = 10;
int a[n];// 合法,使用常量定义
#define N 10
int a[N];// 非法,使用变量定义
int n = 10;
int a[n];

4.2.2 引用一维数组元素

数组里面的元素只能一个个引用,如arr[5] = 100表示给下标为 5 的数组元素赋值为 100,arr[2]表示直接引用下标为 2 元素。一定要搞清楚,对于数组int arr[10]的数组而言,最大下标是多少!!

案例:定义一个长度为 10 的数组,内部元素值分别赋值为 1、2… 10,要求按照逆序输出数组的元素。

#include <stdio.h>int main() {int i, a[10];for (i = 0; i < 10; i++) a[i] = i + 1;for (i = 9; i >= 0; i--) printf("%d ", a[i]);return 0;
}

4.2.3 一维数组初始化

所谓初始化就是在定义的时候就进行赋值的操作。数组在定义的时候也能够给该数组赋初值,具体语法看下面:

// 使用大括号为数组赋初值,大括号内元素使用逗号分隔
int a[5] = {1, 2, 3, 4, 5};// 若是大括号内元素个数小于定义的数组长度,则补0赋值
int a[5] = {1, 2, 3};// 有初值的情况下可以不指定数组的长度,此时数组长度为元素个数
int a[] = {1, 2, 3};

案例:将斐波那契数列的前20项存到一个数组中

#include <stdio.h>int main() {int i, a[20] = {1, 1};for (i = 2; i < 20; i++) a[i] = a[i - 1] + a[i - 2];for (i = 0; i < 20; i++) printf("%d ", a[i]);return 0;
}

4.2.4 内存结构

以数组int a[] = {1, 2, 3};为例

img

数组里的元素在内存里也是连续存放的,所以根据数组的首地址就可以很轻易推算出任意一个元素的地址。而数组名就是指向首元素地址的。

4.2.5 排序(重点)

给定一个整型数组,按照数组元素大小,升序排列。

如将数组[1, 4, 3, 5, 2]变为[1, 2, 3, 4, 5]

1.冒泡排序

#include <stdio.h>int main() {int i, j;int n = 5, a[] = {1, 4, 3, 5, 2};/* 冒泡排序:外层循环记录要排多少轮;内层循环做两两交换的任务 */for (i = 0; i < n - 1; i++) {for (j = 0; j < n - 1 - i; j++) {if (a[j] > a[j + 1]) {int t = a[j];a[j] = a[j + 1];a[j + 1] = t;}}}for (i = 0; i < n; i++) printf("%d ", a[i]);return 0;
}

2.选择排序

#include <stdio.h>int main() {int i, j;int n = 5, a[] = {1, 4, 3, 5, 2};/* 选择排序:* 外循环中的 i 指向乱序数组第一个元素的下标; * 内循环找乱序数组中最小元素的下标,若最小元素不在乱序数组中第一个位置,就交换两者;*/for (i = 0; i < n - 1; i++) {int k = i;for (j = k + 1; j < n; j++) {if (a[j] < a[k]) k = j;}if (k != i) {int t = a[k];a[k] = a[i];a[i] = t;}}for (i = 0; i < n; i++) printf("%d ", a[i]);return 0;
}

4.3二维数组

4.3.1 二维数组的定义

img img

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qTEATih7-1665324647767)(E:\Blog\source\images\image-20220824234404010.png)]

二维数组定义的一般形式是:

类型修饰符 数组名[行数][列数](其中行数和列数都是常量或常量表达式)

例如,int a[2][3]表示定义了一个 2 行 3 列、数据为整型的二维数组。

二维数组可以理解成特殊的一维数组,只不过一维数组里每个元素还是一个一维数组。例如对于上述的 a 数组而言,a 是二维数组,a[0] 是一维数组且代表着二维数组的下标为 0 的那一行。

4.3.2 内存结构

我们画二维数组的逻辑图的时候都是按照行列来画的二维图,但实际上二维数组在内存也是使用线性存储的。还是上面的二维数组,在内存里面是按照下图方式存放的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qL45aiqQ-1665324647768)(E:\Blog\source\images\1657265862758-1b4d39e3-3450-40e3-b4c9-a65a6da638ff.png)]

易见,二维数组存储时,是一行一行存储的,而不是按照行列存储。

4.3.3 引用二维数组元素

二维数组不论行下标还是列下标都是从 0 开始,所以尤其需要注意最大下标的问题。

例如有二维数组int a[3][4],其元素a[3][4]是不存在的,最右下角的元素是a[2][3]

4.3.4 二维数组初始化

// 内层大括号表示一行。按照行赋值,每个元素都有初值。
int arr[3][2] = {{11, 12}, {21, 22}, {31, 32}};// 按行赋值,但有些行数据不全,自动补 0
int arr[3][2] = {{11}, {21, 22}, {31}};// 使用空括号表示当前行不赋值,补 0
int arr[3][2] = {{11}, {}, {31}};// 直接给定所有元素初值,一行一行给。如果元素不全就补 0
int arr[3][2] = {11, 12, 21, 22, 31};// 有初值的时候,可以省略行数,但不能省略列数。
// 系统会根据元素个数和数组列数来计算出行数。
int arr[][2] = {11, 12, 21, 22};// 这样赋值也是可以的,三行两列数组,元素没有初值的就补 0
int arr[][2] = {{11, 12}, {}, {31}};

4.3.5 例题

1.如下图,将一个二维数组 a 转置并存到另一个二维数组 b 中

imgimg


2.给定一个二维数组,输出数组内最大元素的值、行下标和列下标


3.使用筛选法求 100 以内所有的素数

基本思路:把从1开始的、某一范围内的正整数从小到大顺序排列, 1不是素数,首先把它筛掉。剩下的数中选择最小的数是素数,然后去掉它的倍数。依次类推,直到“筛子”为空时结束。


4.插入一个数据到有序数组之中


5.给定一个升序数组,再给定一个数字 n,要求删除数组中所有的 n。


6.从有序数组中找某元素所在下标


4.4字符数组

字符数组说到底还是数组,又因为字符型本质上也是整型,故字符数组本质上就是整型数组。但是,因为字符串这个类型在 C 语言里是不存在的,C 里面就是使用字符数组来描述字符串,所以把字符数组拎出来讲。

4.4.1 定义、初始化、引用

// 定义一个长度为 10 的字符数组
char c[10];// 定义的时候初始化值
char c[10] = {'H', 'e', 'l', 'l', 'o', '!'};// 使用循环遍历每一个字符
int i;
for (i = 0; i < 6; i++)printf("%c", c[i]);

4.4.2 结束标记

我们在存储姓名的时候会用到字符串,而不同的人姓名长度不一样。例如“Jack” 长度是 4,而“Charles”的长度是 7。为了有足够空间描述所有人的姓名,我们会开一个最大长度是 20 的字符数组,即char name[20]。问题来了,假设我在 name 数组里面存了“Jack”,我取的时候怎么确定从数组中取多少字符?简而言之,对于字符数组这种特殊的数组而言,人们更关心有效字符的长度而不是数组总长度。

为了解决这个问题,C 语言规定:在有效字符后面添加字符'\0'作为结束标记,这只是一个标记,不作为有效串的一部分。

字符串常量的末尾有系统自己加的'\0'标记,即"Hello!"{'H', 'e', 'l', 'l', 'o', '!', '\0'}完全等价,注意最后的结束标记。

// 初始化字符串的时候以下三种写法完全等价
char ch[] = "Hello!";
char ch[] = {"Hello!"};
char ch[] = {'H', 'e', 'l', 'l', 'o', '!', '\0'};

4.4.3字符串大小、长度

字符串的长度指字符串中有效字符的个数。

字符串的大小是指系统为该字符串分配了多少存储空间。

// 下面的字符串的长度和大小分别是?
// 长度:6,大小:10
char ch[10] = {"Hello!"};// 下面的字符串的长度和大小分别是?
// 长度:6,大小:7
char ch[] = "Hello!";// 下面的字符串的长度和大小分别是?
// 长度:2,大小:2
char ch[] = {'a', 'b'};// 长度:2,大小:10
char ch[10] = {'a', 'b', '\0'};

4.4.4 字符串的输出

1.循环输出

// 挨个输出字符串中的字符,直到遇到'\0'
int i = 0;
char c, ch[] = "Hello!";
while ((c = ch[i++]) != '\0') {printf("%c", c);putchar(c);
}

2.%s 输出

// 直接使用 %s 输出,输出原理也是找'\0'
char ch[] = "Hello!";
printf("%s", ch);

3.puts 输出

// 直接使用 puts 函数输出一个字符串,以'\0'作为结束标记
// 和 printf 不一样,puts 输出时会在最后追加一个换行符
char ch[] = "Hello!";
puts(ch);

4.4.5 字符串输入

1.循环输入

// 循环输入一个字符串,遇到换行符结束
int i = 0;
char c, ch[20];
while ((c = getchar()) != '\n') {ch[i++] = c;
}
ch[i] = 0;

2.%s 输入

char ch[20];// 使用 scanf 输入一个字符串,输入时遇到空白字符停止。结束后默认在最后添加'\0'
scanf("%s", ch);

3.gets 输入

char ch[20];// 使用 scanf 输入一个字符串,输入时遇到空白字符停止。结束后默认在最后添加'\0'
scanf("%s", ch);

4.5 字符串函数

C 语言库string.h提供了很多操作字符串的库函数,以下库函数都需要牢记。

4.5.1 stract 字符串连接函数

一般形式:strcat(str1, str2)

作用:将字符串 str2 的有效内容连接到字符串 str1 后面,str2 最后的 ‘\0’ 也会一起过去。

注意点:str1 字符串的最大长度要足够存储两个两串的字符才行,不然会越界的。

char str1[10] = "ni";
char str2[] = " hao!";// 输出:ni hao!
puts(strcat(str1, str2));// 第二个参数可以是一个字符串常量
// 第一个参数不能使用常量,因为常量不可修改
puts(strcat(str1, "newStr"));

4.5.2 strcpy strncpy 字符串复制函数

一般形式:strcpy(str1, str2)

作用:将字符串 str2 的内容复制到 str1 里面,从头开始覆盖,str2 最后的 ‘\0’ 也会一起过去。

注意点:str1 的长度要能够完全容纳 str2 的内容,否则会出错的。

char str1[10] = "ni";
char str2[] = " hao!";// 输出:ni hao!
puts(strcpy(str1, str2));// 第二个参数可以是一个字符串常量
// 第一个参数不能使用常量,因为常量不可修改
puts(strcpy(str1, "newStr"));

一般形式:strncpy(str1, str2, n)

作用:将字符串 str2 前 n 个字符赋值到 str1 之中,不会在末尾加 ‘\0’。

注意点:复制时 n 的值不能超过 str1 和 str2 的总长度。

char str1[10];
char str2[20] = "Hello World!";// 将 str2 前 3 个字符复制到 str1 中
puts(strncpy(str1, str2, 3));// 第二个参数完全可以使用字符串常量代替
puts(strncpy(str1, "hahaha", 3));

4.5.3 strcmp 字符串比较函数

一般形式:strcmp(str1, str2)

作用:比较两个字符串的大小,返回一个整数。

比较规则:从前到后挨个字符按照 ASCII 码进行比较,直到遇到不同的字符或两个串同时结束为止。

返回值说明:返回 0 表示两个串相等。返回一个正数表示 str1 > str2。返回一个负数表示 str1 < str2。

char str1[] = "ab";
char str2[] = "aE";// 比较两个串
strcmp(str1, str2);
strcmp(str1, "EF");
strcmp("RO", str2);

4.5.4 strlen 求字符串长度函数

一般形式:strlen(str)

作用:返回字符串 str 的长度。

int len = strlen("nihao");

4.5.5 strlwr 大写转小写函数

一般形式:strlwr(str)

作用:将字符串 str 中大写字母转成小写字母,其余字符不变。

4.5.6 strupr,小写转大写函数

一般形式:strupr(str)

作用:将字符串 str 中小写字母转成大写字母,其余字符不变。

4.5.7 习题

1. 统计单词个数 

键盘输入一个只包含英文字母、空格、标点符号的句子,统计这个句子中有多少单词。

输入: 输出:
Are you ok? 3
I like C program. 4


2. 逆序数组 

键盘输入一些整数存进数组中,将数组元素逆转存放。

输入:

1 2 3 4 5

1 2 3 4 5 6

输出:

5 4 3 2 1

6 5 4 3 2 1

/* 解题思路:以中间元素为中心,将其两侧对称的元素的值互换即可。*/
#include<stdio.h>
#define N 5
int main()
{int a[N],i,temp;printf("请输入数组:\n");for(i=0;i<N;i++)scanf("%d",&a[i]);printf("数组a:\n");for(i=0;i<N;i++)printf("%4d",a[i]);for(i=0;i<N/2;i++){temp=a[i];a[i]=a[N-i-1];a[N-i-1]=temp;}printf("\n 现在数组a:\n");for(i=0;i<N;i++)printf("%4d",a[i]);printf("\n");return 0;
}
3. 鞍点 

键盘输入一个 m, n 表示创建一个 m 行 n 列的二维数组,然后输入整数填满这个数组,求出这个二维数组鞍点所在行、列、值。所谓鞍点是指在二维数组中某一元素是本行最大但同时是本列最小。


4. 输入一个字符串,判断其是否是回文串 

5. 输入两个大整数,输出两者的和 

6. 给定两个有序数组 a 和 b,将两个数组合并成一个新的有序数组 

7. 键盘输入一行字符,提取其中的数字字符,并输出这些数字组合成的数字 

输入:
abc12;9K0
输出:
1290


8. 自己实现一个 strcat 函数 

5.函数

5.1 函数的定义

5.1.1 无参函数定义

一般形式:

返回值类型 函数名 () {

函数体

}

返回值类型 函数名 (void) {

函数体

}

上述两种形式都是定义无参函数的形式。因为是无参函数,所以参数列表里面可以是空的,也可以是一个 void。

5.1.2 定义有参函数

一般形式:

返回值类型 函数名 (形参列表) {

函数体

}

5.2 函数调用

5.2.1 函数调用语句

将函数调用作为一条单独的语句使用。例如我们上面说的有返回值的99乘法表show函数,我们完全可以使用show();来将其当做一条语句执行,即使该函数调用有返回值,但是我们并不在乎,我们只关注该函数体内部的代码是否执行。

5.2.2 函数表达式

承接之前课上的表达式和语句的区别,我们当时说的最大的区别就是“表达式大部分有值,但语句一定没有值”。对于有返回值的函数来说,我们完全可以使用函数表达式来获取其返回值。例如上面说的两个数求最大值的max函数,我们完全可以使用一个变量接受函数表达式的值int z = max(a, b);

5.2.3 函数返回值作为参数

函数的返回值也可以作为函数的参数来使用。还是拿max函数举例子,int m = max(c, max(a, b));我们可以调用两次求最大值函数做到求 3 个数的最大值。再如,printf("%d", max(a, b));就是打印 a 和 b 中的最大值。

对于函数返回值作为参数,我们调用的时候是以函数表达式的形式来使用其返回值的,而不是函数调用语句。因此如下写法:printf("%d", max(a, b););是错误的,因为语句没有值。

5.3 参数传递

5.3.1 形参和实参

在函数调用时,主调函数和被调函数之间有数据传递关系。定义函数时,函数名后面的小括号里面的参数就是形参(形式参数、虚拟参数)。我们在调用函数时,传到小括号里面的参数就是实参(实际参数)。传递的时候,实参可以是变量、常量、表达式。

5.3.2 实参与形参间的数据传输

在调用函数时,系统会把实参的值传递(复制)给被调函数的形参。在函数执行过程中这个形参都是有效的,可以参与各种运算,但是一旦函数执行完毕返回了,那么形参会被销毁(内存释放)。函数调用过程中发生的实参与形参间的数据传递称为“虚实结合”。

案例:传入两个整数,返回两个数的平均值


程序分析:案例中,实参是“2 和 3”,形参是“a 和 b”。在调用函数的时候,会将 2 传入 a 中,3 传入 b 中。因此使用(a + b) / 2.0就是在求传入的两个数的平均值。

传参的类型转换。传参时若实参与形参类型不一致时会发生类型转换,例如形参是int a但是却传进去一个 3.14,那么此时会这样处理int a = (int) 3.14,也就是说两种类型不一致以形参类型为准。

5.3.3值传递

值传递是指传递时实参会把值存到形参里面,也就是说实参和形参只是值相等的两个不同变量。所有的基本数据类型的数据传递时都使用值传递。

img

函数定义为:void func(int a) {}

可见函数 func 有一个名为 a 类型为 int 的形参。

我现在使用int x = 10; func(x);来调用 func 函数。所以实参是 x 且值为 10,现在将实参 x 传给形参 a。本质上和int a = x;是一致的。

因此 a 和 x 是两个变量,二者不共享内存,只是值是相等的。

判断下面的函数能否交换变量 a 与 b 的值。交换不了。

#include <stdio.h>void swap(int a, int b) {int t = a;a = b;b = t;
}int main() {int a = 2, b = 3;swap(a, b);printf("%d, %d", a, b);return 0;
}

5.3.4 地址传递

地址传递是将实参的地址传递到形参里。也就是说形参接受实参的地址,那么形参操作该地址里的数据的时候,实参也会跟着变。注意,形参里面存着地址,那也就意味着形参并不是普通变量,而是指针变量。我们当前学过的数据类型中属于传地址的是数组。关于数组作为参数后面再详细叙述。

img

如图,实参指向某一块内存,并且实参还将内存地址传给了形参,因此形参也会指向这块内存。

既然是对同一块内存进行操作,那么两者的操作是互相影响的。

案例:使用函数传进去一个数组,交换数组前两个元素。

#include <stdio.h>void swap(int a[]) {int t = a[0];a[0] = a[1];a[1] = t;
}int main() {int a[] = {1, 2};swap(a);printf("%d, %d", a[0], a[1]);return 0;
}

5.4 函数调用过程

  1. 定义函数时指定的形参,在函数未被调用时,它们并不会被分配存储空间。只有函数被调用了,形参才会被分配存储空间,并且存储空间会在函数调用结束而释放掉。
  2. 函数调用时会将实参的值传给形参,也就是在这个时候,形参才有了存储空间,才是真是存在的一个变量。
  3. 函数里面使用 return 语句将函数的值返回。但是要注意,返回的值类型要与函数定义的类型一致,若是不一致,按照定义时的函数类型进行强转,强转不了就报错。
  4. 形参和实参进行数据传递的时候,只是值传递。也就是说两者值是一样的,但并不共享内存。因此函数调用结束,形参空间被释放,这不影响实参,实参的值保持不变。

5.5 函数的返回值

通常,我希望调用某个函数可以返回一个值,我们使用这个值再做其他的操作。这个值就是函数的返回值,例如c = max(a, b);我们这边就是使用变量 c 来接受 max 函数的返回值。对于返回值有以下两点需要注意:

  1. 函数的返回值通过 return 语句返回

如果一个函数需要返回某个值,那么必须使用 return 语句来将值返回出去。若某函数不需要返回值(定义时指定函数类型为 void),那当然可以不使用 return 语句。一个函数里面可以使用多次 return 语句,但是执行的时候,遇到第一个 return 就会退出这个函数。

案例:编写函数要求传入一个分数,返回对应的等级。A:>89,B:>79,C:>59,D:<60

#include <stdio.h>char getGrade(int score) {if (score > 89) return 'A';if (score > 79) return 'B';if (score > 59) return 'C';return 'D';
}int main() {printf("%c", getGrade(87));return 0;
}

​ 2.函数的类型

函数在定义的时候必须指定函数的返回值类型,若没有返回值则将函数指定为 void 类型。函数体里面使用 return 语句返回值时,值的类型要与函数类型一致,若不一致,值会被自动转为函数类型。例如,对于函数int func()来说,函数类型是 int,若是在函数体里面返回 3.14,实际返回时会对小数做一次类型转换,也就是实际返回 3。

5.6 函数声明

我们知道,函数必须满足先定义再调用的性质。对于定义在其他文件里的函数,我们需要引入对应的文件,例如之前讲的使用输入输出函数之前必须先使用#include <stdio.h>来导入对应的头文件。对于我们自己定义的函数,在调用这个函数之前,这个函数必须被定义出来。

举个例子,我们在主函数里面调用了max函数,那么在主函数之前就得定义max函数,而不可以在主函数后面定义。但是这样做之后,我们的主函数就会被挤到最后面,不太合理。我想函数“定义在后,调用在前”能不能实现?可以,这时就需要使用声明,做声明的主要目的就是通知 C 编译系统某函数的基本信息,便于检查代码的语法是否错误。

int main() {int a = 10;// 错误。调用一个未定义的函数func(a);return 0;
}int func(int a) {}
int func(int a) {}int main() {int a = 10;// 正确。func 函数已经定义func(a);return 0;
}
int main() {int a = 10;int func(int a);// 正确。上面声明了 func 函数func(a);return 0;
}int func(int a) {}

看出函数声明和定义函数时的函数首部基本一样,函数声明的一般形式:

函数类型 函数名(参数列表); 函数类型 函数名(参数类型列表);

可以看出来,函数声明有两种。结合上面的案例,int func(int a);int func(int);是等价的声明。为什么可以省略形参的名字?回忆一下,声明是用来干什么的。声明是用来通知编译系统,该函数的基本信息的,这个基本信息包括函数类型、函数名、参数个数、各个参数的类型,对于参数的名字并不关心,所以参数名字完全可以省略掉。

#include <stdio.h>// 全局声明 func3 函数。
void func3();int main() {func3();return 0;
}// 定义函数 func1
void func1() {func3();
}// 定义函数 func2
void func2() {func3();
}// 定义函数 func3
void func3() {}

5.7 函数嵌套调用

函数的定义是相互平行的、独立的。也就是说,不可以在一个函数的函数体里定义函数。但是调用没有要求,你可以在函数内任意多次调用其他函数。

案例:使用函数嵌套调用实现求 4 个数的最大值。

#include <stdio.h>int main() {int max4(int, int, int, int);int a = 1, b = 2, c = 3, d = 4;printf("%d", max4(a, b, c, d));return 0;
}int max2(int a, int b) {return a > b ? a : b;
}int max4(int a, int b, int c, int d) {return max2(max2(max2(a, b), c), d);
}

5.8 递归

一个函数或直接或间接的调用自己称为递归。自己调用自己,必须要有尽头,否则一直调用下去就和死循环一样会栈溢出。因此,对于递归函数来说一定要有退出条件即出口。

案例1:定义一个递归函数求 n!

#include <stdio.h>/* 返回 n 的阶乘 */
int prep(int n) {if (n == 1) return 1;return prep(n - 1) * n;
}int main() {printf("%d", prep(4));return 0;
}

案例2:汉诺塔问题

image.png

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B5Hmo6Bm-1665324647769)(E:\Blog\source\images\1658736970101-58a021a3-606d-4885-acfd-e494b4eeaebf.png)]

#include <stdio.h>/* 将 n 个盘子从 A 借助 B 到达 C */
void hanoi(int n, char A, char B, char C) {if (n == 1) {printf("%c -> %c\n", A, C);return;}hanoi(n - 1, A, C, B);printf("%c -> %c\n", A, C);hanoi(n - 1, B, A, C);
}int main() {hanoi(3, 'A', 'B', 'C');return 0;
}

5.9 数组作为函数参数

数组元素作为函数参数用法与之前讲的变量作为参数完全一致。看下面的对比案例:

#include <stdio.h>int add(int a, int b) {return a + b;   
}int main() {// 使用普通变量作为函数参数int a = 1, b = 2;printf("%d", add(a, b));// 使用数组元素作为函数参数int arr[] = {1, 2};printf("%d", add(arr[0], arr[1]));return 0;
}

5.9.2 一维数组名作为函数参数

经过上面的学习,我们知道一维数组名代表该数组的首地址即首元素地址。所以将数组名传到函数里面,也就是将数组的首地址传到函数中,因此在函数中操作数组对应的内存时,也会影响到原数组。将数组名作为函数参数的语法结构为:

void test(int arr[]) {}int main() {int arr[10];test(arr);
}

可以见到,函数的参数我们写成了int arr[10],这样 C 系统就知道参数 arr 是一个 int 数组类型。

有趣的是,C 系统只要知道参数类型就可以了,至于数组有多少元素并不关注,所以传参数的时候完全可以使用int arr[]。为什么数组长度不重要呢?因为我们需要分辨你是想传值还是传指针。

void test(int arr[]) {}

案例:编写一个函数,要求传入一个整型数组,返回这个数组里所有数字的平均值。

#include <stdio.h>double avg(int a[], int n) {int i = 0, sum = 0;while (i < n) sum += a[i++];return 1.0 * sum / n;
}int main() {int a[] = {1, 2, 3, 4}, n = 4;printf("%lf", avg(a, n));return 0;
}

任务:将冒泡排序和选择排序分别写成函数的形式



5.9.3 多维数组名作为函数参数

将多维数组作为参数传到函数中,可以使用下面两种参数定义方法:

void test(int arr[][10]) {}
void test(int arr[5][10]) {}

也就是说,可以省略掉第一维的长度,但是不能省略第二乃至更高的维度长度。为什么呢?因为二维数组是由若干个一维数组构成的。在内存中,数组是按行存放的,因此在定义二维数组的时候必须指定列数(即一行有多少元素)。因此int arr[5][]这种写法是错误的。既然编译器并不关心数组第一维的长度,所以你可以在参数中乱写数组第一维的长度。

案例:给定一个二维数组,编写函数输出其中的最大值及其对应的行列数。

#include <stdio.h>void find(int a[][3], int m, int n) {int i, j;int max_row = 0, max_col = 0;for (i = 0; i < m; i++)for (j = 0; j < n; j++)if (a[i][j] > a[max_row][max_col]) {max_row = i;max_col = j;}printf("(%d, %d) => %d", max_row, max_col, a[max_row][max_col]);
}int main() {int a[][3] = {1, 2, 3, 4, 5, 6};find(a, 2, 3);return 0;
}

5.10 局部变量和全局变量

C 语言中变量有其对应的作用域。所谓作用域就是变量起作用的范围,我们根据作用域的不同将变量分为局部变量和全局变量。在一个 C 源文件中,定义在函数里的变量就是局部变量,定义在函数外的变量就是全局变量。

5.10.1 局部变量

局部变量的作用范围是最小包围的{}之中。这分为两种:一种是直接在函数内定义,那么其作用域就是自定义处开始一直到整个函数结束的位置;另一种是在复合语句中定义,其作用域也是从定义处开始一直到该复合语句结束的位置。看下面的案例:

void test() {int i = 0;{int j = i + 1;printf("%d", j);}printf("%d", i);
}

案例解析:我们可以看到,在函数 test 中定义了两个变量i, j。其中 i 在函数中直接定义,所以 i 的作用域是从定义处开始,覆盖整个 test 函数,函数中的复合语句也属于函数的一部分,因此复合语句中也能访问这个变量 i。变量 j 是在函数中的复合语句中定义的,所以 j 的作用域从其定义处开始,覆盖整个复合语句,注意复合语句外是无法访问这个变量的。

所以,我们发现{}有锁住里面变量的能力,在某{}中定义的变量只能在里面起作用。但是注意哦,{}只会限制里面的变量不让其出去,但不会阻止外面的变量进来,简单记为“对内不对外”、“只进不出”。

有人问:如果内层和外层的变量重名冲突了怎么办?

答:强龙不压地头蛇,内层变量会覆盖掉外层变量。

5.10.2 全局变量

全局变量就是定义在函数外面的变量,因此没有{}将这个变量括起来。所以这个变量的作用域是从变量定义的位置开始,一直到整个文件结束。如果我们在函数外面定义了一个全局变量,我在函数内部是否可以访问这个变量呢?当然是可以的,我们说过,{}对内不对外,所以函数的{}并不会阻止外面的全局变量进入函数内部。那若是全局变量和局部变量重名冲突了呢?一样的,强龙不压地头蛇,局部变量优先。看案例:

#include <stdio.h>int Ans = 0;void test1() {Ans += 10;   
}void test2() {printf("%d", Ans);   
}int main() {test1();test2();return 0;
}

为了在使用时能够区分出某一变量是否是全局变量,因此我们约定俗成:在命名局部变量的时候变量名全小写;命名全局变量的时候变量名首字母大写。

全局变量的应用:我们知道调用函数的时候可以使用 return 语句返回一个值。若我们想返回多个值怎么办?第一种思路,你可以返回一个动态申请内存的数组。第二种思路,将要返回的值放到某一全局变量中返回。第三种思路:将外部变量的地址传进函数当中,然后我们将要返回的数据存到实参里面。

5.11 变量的存储方式和生命周期

5.11.1 动态存储和静态存储

变量在内存的存储有两种方式:动态存储和静态存储。其中,静态存储是指在程序运行过程中由系统分配固定的存储空间的方式,而动态存储则是在程序运行期间按照需求动态分配存储空间的方式。静态存储的变量在程序运行的整个过程中都一直存在,例如我们之前讲过的全局变量都是使用静态存储方式。动态存储的变量,只有其所在函数被调用的过程中才会被临时分配空间,一旦所在函数执行完毕,那么这部分的存储空间会被收回,这个变量也就不复存在了。

C 语言将内存分为:程序区、静态存储区(堆)、动态存储区(栈)。见名知意,我们的代码段会放在程序区,所有的静态存储的变量都在静态存储区,动态存储的变量都在动态存储区。

5.11.2 局部变量的存储类别

  1. 自动变量(auto 变量)

函数中的局部变量只要不专门声明为 static 类型,都是动态分配内存且数据存在栈中。函数的形参、函数中定义的局部变量(包括在复合语句中定义的局部变量)都属于这一类。在调用该函数的时候,系统会为这些变量申请内存,函数调用结束的时候释放内存。因此这类局部变量被称为自动变量。自动变量使用关键字 auto来声明。

void func(int a) {int b;auto int c;
}

在上面的案例中,有三个变量 a、b、c。其中,a 是形参、b 是普通变量、c 是指定的 auto 类型的变量。这些变量都是函数执行过程中分配内存,函数运行结束而销毁变量(内存)。实际上,关键字 auto 可以省略,因此 a 变量和 b 变量都是 auto 类型的,只不过省略了 auto 关键字而已。自动变量在定义的时候若是没有进行初始化,那么里面会有一个垃圾值。

2.静态局部变量(static 变量)

有时希望函数中的变量在函数调用之后不会被销毁而继续保留原值。这时应该指定该局部变量为“静态局部变量”,使用关键字static修饰。分析下面的案例输出什么?

#include <stdio.h>int func(int a) {int b = 0;static int c = 3;return a + b + c++;
}int main() {printf("%d\t", func(2));printf("%d\t", func(2));printf("%d\t", func(2));return 0;
}

对于静态变量来说,必然会在编译的时候进行一次初始化。如果你在定义静态变量的时候给了初值就使用你提供的初值进行初始化,否则使用 0 进行初始化。静态局部变量虽然不会随着函数调用结束而被销毁,但作为局部变量,它还是逃不出{}的制裁,也就是说静态局部变量的作用域还是在{}之内。

由于静态局部变量一旦定义,直到整个程序执行完毕才会释放空间,所以静态变量对内存不友好。并且对拥有静态变量的程序而言可读性也很差,因此我们不推荐使用静态变量。

​ 3.寄存器变量(register 变量)

顾名思义,寄存器变量就是存在寄存器中的变量。存在寄存器中的\变量有一个好处:存取速度极快。因此对于一些需要频繁使用的变量可以设置为寄存器变量。例如代码register int sum;就定义了一个寄存器变量。但是由于现代计算机的速度越来越快,编译系统会自己优化,将一些使用过程中发现的频次很高的变量自动转成寄存器变量,因此这个修饰符使用的就不多了。

5.12 全局变量作用域拓展

5.12.1本文件拓展

我们知道在一个文件中,全局变量的作用域是从定义处开始一直到本文件结束。那如果在定义之前我想使用的话,就需要使用 extern 来声明一下。看下面的案例:

#include <stdio.h>int main() {// 因为 A 和 B 定义在 main 后面,现在我想在 main 函数里面引用这两个变量// 使用 extern 关键字再次声明 A 和 B,声明时变量的类型可以省略掉。extern A;extern int B;B++;printf("A = %d, B = %d", A, B);return 0;
}int A, B;   //定义外部变量 A,B

5.12.2 跨文件拓展

直接上案例:

// demo.c
#include <stdio.h>// 使用 extern 关键字导入其他文件的全局变量
extern Num;void showNum() {printf("%d", Num);
}
// demo.c
#include <stdio.h>// 使用 extern 关键字导入其他文件的全局变量
extern Num;void showNum() {printf("%d", Num);
}

执行流程:当系统遇到 extern 关键字时会从本文件中寻找对应变量,找不到就到其他文件中寻找。全部找不到就报错。将一个变量的作用域扩展到其他文件有很大的隐患,所以非必要不要使用这样的写法。

5.12.3 限制变量的跨文件使用

正如上面所说,全局变量跨文件使用是有很大隐患的,所以有时我们需要保证某个文件中的全局变量不会被跨文件使用。此时我们需要在全局变量的前面加上 static 关键字,这样这个全局变量就无法被其他文件引用到。看例子:

// demo.c
#include <stdio.h>extern Num;void showNum() {printf("%d", Num);
}
// main.c
static int Num = 999;int main() {void showNum();showNum();return 0;
}

5.13 作用域和生命周期小结

还是相同的案例,但是变量 Num 被 static 关键字修饰了,因此在 demo.c 文件中使用extern Num是无法成功的,因为变量 Num 被锁定在 main.c 之中了。

  1. 从作用域角度,变量分为局部变量和全局变量。它们采取的存储类别:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gFVC0TxA-1665324647769)(E:\Blog\source\images\bd253f1070a3dcd7e5a315dabe340c5d.svg+xml)]

2.从变量的生存周期看,有静态存储和动态存储两种类型。静态存储的变量在整个程序运行期间都是存在的,而动态存储的变量只有在调用函数的时候才会被临时分配内存。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k2zJOk4F-1665324647770)(E:\Blog\source\images\b9f6072d465cb98a19f661332d932833.svg+xml)]

3.从变量值存放的位置区分,可以分为:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rfeZCoT9-1665324647771)(E:\Blog\source\images\95ae79437de21380171a30ac2ecf929c.svg+xml)]

4.可见性和存在性一览

可见性就是在某一范围内,该变量是否可以访问,即作用域是否包含这个范围。存在性就是在某一范围内,该变量是否被销毁了?如果没有被销毁那就说明这个变量是存在的。

变量存储类型函数内函数外
可见性存在性可见性存在性
自动变量和寄存器变量××
静态局部变量×
静态外部变量
外部变量

5.关于 static 关键字

static 关键字可以修饰局部变量和全局变量。当使用 static 关键字修饰局部变量时会将局部变量变为静态局部变量,这虽然不会扩大该变量的作用域但会延长变量的生命周期。当使用 static 关键字修饰全局变量的时候,会将这个全局变量限制在本文件之中,不让外部文件访问这个全局变量。尤其需要注意的是,全局变量即使不使用 static 关键字修饰,其也是存在静态存储区的。我们注意到,所有被 static 关键字修饰的变量都是受限制的,局部变量被限制在函数中,全局变量被限制在文件中。

5.14 内部函数和外部函数

函数分为内部函数和外部函数两种,所谓内外之分,就是文件外的地方能否访问到这个函数。默认定义的函数,在其他文件都是可以访问的,所以之前定义的绝大多数函数都是外部函数。内部函数就是限制函数仅在本文件起作用,外部文件无法访问到这个函数。

5.14.1 外部函数

使用关键字extern来修饰一个函数,表示这个函数是一个外部函数,即其他文件可访问这个函数。默认情况下,若是没有修饰符,可以看做是隐含的extern修饰。即函数默认都是外部函数。看以下案例:

/* demo.c */// show1 函数默认就是 extern 修饰的
void show1() {}
extern void show2() {}
/* main.c */int main() {// 在导入外部函数的时候一定要加 extern 关键字extern void show1();extern void show2();show1();show2();return 0;
}

5.14.2 内部函数

使用关键字static来修饰一个函数,表示这个函数是一个内部函数,即外部文件不可访问这个函数。和使用 static 关键字修饰全局变量有异曲同工之妙。看下面的案例:

/* demo.c */
static void show1() {}
/* main.c */int main() {// show1 函数被 static 修饰,所以这边无法导入void show1();show();return 0;
}

5.15 小习题

  1. 括号的有效性判断。输入一行字符串,保证字符串中只包含()[]{}字符,要求使用函数返回这个串是否合法。有效字符串需满足:(1)左括号必须闭合 (2)左括号必须使用正确的次序闭合

    
    

2.键盘输入一行句子,使用函数返回这个句子中最后一个单词的长度。


3.键盘输入一个正整数n,输出杨辉三角形的前n行。杨辉三角看下例:

image.png image.png


6.指针

6.1 什么是指针

按照我们之前所学,每一个变量都对应内存里面的一小块空间,那块内存里面存着对应的数据。指针就是那块内存的起始地址,指针变量是存储这这个地址的变量。我们看案例:

int i = 10;
printf("%d", i);

这个案例非常简单,定义变量 i 的时候先分配了一块内存供变量 i 使用(假设内存首地址是 1002),然后往内存里面存储整数 10。后面打印变量 i 的时候,先到地址为 1002 的内存里面取出数据 10,然后打印出数字 10。当我们需要使用地址为 1002 内存中值的时候,我们可以直接使用变量 i 来存取,这种访问内存的方式称为直接访问

除此之外,我们还可以使用间接访问的方式。我们可以先将 1002 存到一个变量 i_pointer 之中,根据上面我们知道,这个 1002 就是变量 i 对应的内存地址。如果我们直接操作 i_pointer 的值,这只是对地址这个数字进行操作,对 i 指代内存里的数据没啥影响。但如果我们先取出 i_pointer 里面的值(1002),然后拿着这个值找对应的内存,再对那个内存操作,那不就可以影响到 i 了?

image-20220803173008356
int i = 10;// 定义指针变量 i_pointer 接受 i 的地址。
int *i_pointer = &i;// 直接对地址进行自增处理。
// 自增之后,i_pointer 不再指向 i,后续也无法间接操作变量 i 了。
i_pointer++;// *i_pointer 表示转向指针变量 i_pointer 里面地址对应的内存空间即 i(1002)。
// (*i_pointer)++ 表示对内存地址为 1002 的空间里的数据自增。这其实就是在操作变量 i 了。
(*i_pointer)++;

6.2指针变量

6.2.1 指针变量的定义

定义指针变量的一般形式为:类型名 \*指针变量名;

例如:int *i_pointer;表示定义一个指向整型数据的指针变量。定义指针变量时,变量的类型是必须的,因为不同类型的变量所占据的字节数或数据存储方式是不一样的。因此定义指针变量的时候必须严格指出该指针变量是指向什么数据类型的。

我们给指针变量赋值的时候,是将某变量的地址传进去的,看下面的对比案例:

int a = 100;
int *pointer1;pointer1 = a;
pointer1 = &a;  // 逻辑对
pointer1 = 100;*pointer1 = a;
*pointer1 = &a; // 逻辑错
*pointer1 = 100;int *pointer2 = a;
int *pointer2 = &a;  // 逻辑对
int *pointer2 = 100;

6.2.2 引用指针变量

  1. 给指针变量赋值

给指针变量赋值的时候,需要把一个变量的地址赋给它。如int *p; p = &a;

​ 2.引用指针变量指向的变量

假设指针变量 p 指向整型变量 a(int a = 100; int *p = &a;),我们使用*p来访问 a 变量的值。我们也可以使用*p = 99;来修改变量 a 对应内存里的值,这相当于执行了a = 99;

​ 3.引用指针变量本身的值

对于指针变量int *p = &a;来说引用指针变量 p 本身的值实际上就是引用变量 a 的地址。

对于指针这一节,我们要熟练掌握两个运算符:

  • & 取地址运算符。&a 表示获取变量 a 的地址。
    • 指针运算符。*p 表示获取指针所指向的对象,即 a。

案例:输入两个整数 a 和 b,按照大小顺序输出两个数

#include <stdio.h>int main() {int a = 100, b = 99;int *p1 = &a, *p2 = &b;if (*p1 > *p2) {int *t = p1;p1 = p2;p2 = t;}printf("%d, %d\n", a, b);printf("%d, %d", *p1, *p2);return 0;
}

6.2.3 指针变量作为函数参数

指针变量也可以作为函数的参数,之前使用的数组形参本质上就是一个指针形参。

案例:使用函数交换变量 a 和 b 的值

#include <stdio.h>void swap(int *p1, int *p2) {int t = *p1;*p1 = *p2;*p2 = t;
}int main() {int a = 100, b = 99;swap(&a, &b);printf("%d, %d", a, b);return 0;
}

分析下面交换两个变量的函数,有没有什么错误的地方?

// 操作正确,含义:交换两个指针 p1 和 p2 的指向
void swap1(int *p1, int *p2) {int *t = p1;p1 = p2;p2 = t;
}// 下面操作错误
void swap2(int *a, int *b) {int *t = *a;*a = *b;*b = *t;
}// 下面的操作正确,含义:交换指向的两个变量的值
void swap3(int *a, int *b) {int tmp;int *t = &tmp;*t = *a;*a = *b;*b = *t;
}

注意点:int *p = &a;int *p; p = &a等价。而不是和int *p; *p = &a;等价。

6.3 通过指针引用数组

6.3.1数组元素的指针

一个变量有其独有的地址,那么对于一个数组元素,也有对应的内存地址。指针变量可以指向一个变量,自然也可以指向一个数组元素。

int a[3] = {1, 2, 3}; // 定义一个整型数组 a
int *p = &a[0]; // 定义指针 p 指向数组第 0 个元素的地址
int *p2 = a; // 定义指针 p2 指向数组首地址(第 0 个元素的地址),和上面写法等价int m = *a;  // 直接获取数组 a 中第 0 个元素
int n = *p;  // 也是获取数组 a 中第 0 个元素,和上面等价
int k = p[0]; // 和上面等价
int j = p2[0];  // 和上面等价

6.3.2 指针的算术运算

对于一个普通变量进行算术运算能够理解,那对一个地址(指针)来说算术运算有什么含义呢?首先,*/%这三种算术运算对指针来讲没有意义。但是在某些情况下,+-对指针来说有重要意义,这种重要情况就是当指针变量指向数组元素的时候。

  1. 加法运算

    1. 表示后移指针,例如对指针int *p = &a[0];来说 p + 1表示将指针移向后一个元素,也就是 a[1] 所在地址。因此,对地址加一并不是真加一,而是加这个类型的字节数。例如,假设 p 里面存的地址是 2000,那么p += 1p++之后,p 里面不是存储 2001,而是存储 2004。
    2. 如果 p 的初值是 &a[0] 或 a,那么 p + n 指向地址的计算公式为:p + n * d(其中 d 是当前类型占据的字节数)。因此,p + 9 指向的地址就是 &a[9],它是指向 a[9] 的。
    3. *(p + i) 与 *(a + i) 完全等价,它们是在取 a 数组中下标的为 i 的元素的值。即 *(p + i) 等价于 *(a + i) 等价于 a[i]。实际上,在编译的时候,对数组 a 来说,就是将 a 当做一个指针来看待的。运算符*[]在某种程度上是一致的。
    4. 注意点:虽然说在用法上指针变量 p 和数组名 a 完全等价。但是和指针变量不同的是,数组名不能改变指向,如a = &b[0];是错误的,试图修改数组名的指向是不允许的,这是和指针变量的最大不同之处。本质上数组名是一个指针常量,常量不可修改。
  2. 减法运算

    1. 表示前移指针,对于指针变量int *p = &a[3];来说,p - i 表示指向当前元素前 i 个元素。这和加法完全相反。因此,p--会将 p 里面存储的地址值减去 4。
    2. 两个指针变量之间做减法,当且仅当两个指针变量指向同一个数组中的元素时这样的减法才有意义。例如对于指针变量int *p = &a[0], *q = &a[3];来说q - p的值为 3。所以两个指针变量之间的减法实际意义为:返回两个指针之间差几个元素。

​ 3.对于定义int a[5]; int *p = a + 1;以下对数组元素的访问错误的是:C,D,E,F,G

​ A. a[2] B. *p C. *(p + 4) D. a[5] E. p + 2 F. p[4] G. *a++ H. ++*p

6.4 通过指针操作一维数组

6.4.1 引用数组元素

通过上面的学习,我们知道引用一个数组元素可以使用:

  1. 下标法。如使用 a[i]p[i] 的形式来访问下标为 i 的元素,其中 a 是数组名,p 是指向数组首元素的指针变量。

  2. 指针法。使用 *(a + i)*(p + i) 的形式访问下标为 i 的元素,a 与 p 的含义同上。

    案例:依次输出数组内的元素

    #include <stdio.h>int main() {int a[5] = {1, 2, 3, 4, 5};int n = 5, *p = a;for (int i = 0; i < n; i++) {// 数组名和指针变量都使用下标法访问数组元素printf("%d\t", a[i]);printf("%d\t", p[i]);}return 0;
    }int main() {int a[5] = {1, 2, 3, 4, 5};int n = 5, *p = a;for (int i = 0; i < n; i++) {// 数组名和指针变量都使用指针法访问数组元素printf("%d\t", *(a + i));printf("%d\t", *(p + i));}return 0;
    }int main() {int a[5] = {1, 2, 3, 4, 5}, n = 5, *p;// 利用对指针变量加一表示移动指向到下一个元素的特性// 思考下,这个地方直接使用 a 来进行 ++ 操作可不可以for (p = a; p < a + n; p++)printf("%d\t", *p);return 0;
    }
    

案例:键盘输入 3 个整数到数组中,然后输出这三个元素

#include <stdio.h>int main() {int a[3], *p;for (p = a; p < a + 3; p++)scanf("%d", p);for (int i = 0; i < 3; i++)printf("%d\t", *(p + i));return 0;
}

6.4.2 指针运算符与自增(自减)搭配使用

int a[N], *p = a;*p++;  // 返回 a[0]
*++p;  // 返回 a[1]
(*p)++;  // 返回 a[0]
++(*p);  // 返回 a[0] + 1
++*p;  // 返回 a[0] + 1

6.4.3 数组名作为函数参数

之前探讨过,数组名作为参数时会将数组的首地址传到参数中。将地址传给另一个变量,那我接受的那个变量当然定义为指针变量咯。所以在形参里出现的int a[],这边的 a 并不是数组名的意思,而是这个 a 是一个指针变量,只不过看着像数组的定义。正因为 a 是一个指针变量,因此 a 是可以改变指向的,这和普通数组名是存在极大不同的。因此函数void func(int a[], int n);void func(int *a, int n);完全等价。

不能因为上面所说,你就认为指针和数组名使用方面完全等价。它们有以下几点不同:

  1. 数组名是常量,指针变量是变量
  2. 数组名所表示的数组是有内存的,但直接定义一个指针变量是不会有数组空间的。

对于一个指针变量来说,必须先为其赋予指向的地址,否则它的指向是随机的、不确定的,会有很大的意外发生。看下面的代码对比:

// 数组 a 是有对应空间的,为其赋值合情合理
int a[10];
for (int i = 0; i < 10; i++) {scanf("%d", a + i);scanf("%d", &a[i]);
}// 直接定义的指针变量 p 没有具体的指向,也就是没有数组空间,为其赋值就有问题了。
int *p;
for (i = 0; i < 10; i++)scanf("%d", &p[i]);

案例:键盘输入 5 个数字存入数组中,为其排序并输出

#include <stdio.h>void selectSort(int *a, int n) {int i, j;for (i = 0; i < n - 1; i++) {int k = i;for (j = k + 1; j < n; j++)if (a[j] < a[k]) k = j;if (k != i) {int t = a[k];a[k] = a[i];a[i] = t;}}
}// 想想 main 函数里面三个 p = a 是否可以省略
int main() {int a[5], *p, n = 5;for (p = a; p < a + n; p++)scanf("%d", p);p = a;selectSort(p, n);for (p = a; p < a + n; p++)printf("%d\t", *p);return 0;
}

6.5 通过指针操作多维数组

6.5.1 多维数组元素的地址

以二维数组为例,int a[M][N];这边的 a 数组就是一个二维数组。a[0] 是什么?之前我们说过 a[0] 表示数组 a 的第 0 行,是一个一维数组。因此,a[0] 是一个指向数组第 0 行首元素的指针,也就是存的也是一个地址。从这个角度来说,a 是一个特殊的一维数组,只不过每一个元素也都是一个数组而已。那么 a 作为一个“一维数组”,当然指向首元素的地址。而首元素 a[0] 也是一个地址,所以 a 是一个指向指针的指针。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sVRhUyfG-1665324647771)(E:\Blog\source\images\1658993708045-e990d8da-5037-4539-8f20-ca83996f3925.png)]

按照这个思路,a 是指向 a[0] 的指针,那么 a + 1 呢?当然是指向 a[1] 的指针。所以你会发现,对于指针 a 来说每次加一都是直接移向下一行,我们称这样的指针为行指针。我们知道指针 a[0] 指向第 0 行首元素的地址,那 a[0] + 1 当然指向第 0 行第 1 列的元素了。所以,a[0] 这样的指针每次加一都是移动一列,我们称这样的指针为列指针。

以上面的图为例,假设初始 a 指向的地址为 2000,问:

a[0] 的地址:2000 a[0] + 1 的地址:2004 【一个整型数据占4个字节】

a + 1 的地址:2012(2000+4*3) a + 3 的地址:2036

a[3] 的地址:2036 a[3] + 2 地址:2044

二维数组的等价关系:

a + i  <==>  &a[i]`、`*(a + i)  <==>  a[i]
a[i] + j  <==>  &a[i][j]`、`*(a[i] + j)  <==>  a[i][j]
*(*(a + i) + j)  <==>  a[i][j]

整理表格为:

表示形式含义
a指向第 0 行的行指针。地址:2000
a[0], *a指向第 0 行第 0 列的列指针。地址:2000
a + 1, &a[1]指向第 1 行的行指针。地址:2012
a[1], *(a + 1)指向第 1 行第 0 列的列指针。地址:2012
a[1] + 2, *(a + 1) + 2, &a[1][2]指向第 1 行第 2 列的列指针。地址:2020
*(a[1] + 2), *(*(a + 1) + 2), a[1][2]第 1 行第 2 列的数组元素值。数对应位置元素值

6.5.2 经验总结

下面的内容是我总结的操作二维数组指针之间的关系,读者可自行按照下面的方法推广到更高维度的数组之中。但考试最多考到二维,因此更高维度我就不涉及了。

我将指针分维度:

类型案例维度
行指针a + i2
列指针a[i], *(a + i)1
元素值a[i][j], *(*(a + i) + j)0
运算符含义作用
&取地址运算符升维
*, []指针运算符、下标运算符降维

案例:分析下面各表达式的含义

输出二维数组的有关数据(地址和元素的值)

#include <stdio.h>int main() {int a[3][4] = {{0, 1, 2, 3},{10, 11, 12, 13},{20, 21, 22, 23},};printf("%d, %d\n", a, *a);					 //0行起始地址和0行0列元素地址printf("%d, %d\n", a[0], *(a + 0)); 		//0行0列元素地址printf("%d, %d\n", &a[0], &a[0][0]);		//0行起始地址 和 0行0列元素地址 printf("%d, %d\n", a[1], a + 1);			//1行0列元素地址和1行起始地址printf("%d, %d\n", &a[1][0], *(a + 1) + 0);		//1行0列元素地址printf("%d, %d\n", a[2], *(a + 2));				//2行0列元素地址printf("%d, %d\n", &a[2], a + 2);				//2行起始地址	printf("%d, %d\n", a[1][0], *(*(a + 1) + 0));	//1行0列元素的值printf("%d, %d\n", *a[2], *(*(a + 2) + 0));		//2行0列元素的值return 0;
}

6.5.3 行指针变量

我们知道,int a[10]; int *p = a;定义的指针变量 p 指向一个一维数组的首地址。也就是说,这个指针变量 p 是一个列指针。而一个二维数组的数组名是一个行指针,直接这样使用int a[3][4]; int *p = a;是错误的,因为 a 是一个行指针,却赋值给了一个列指针变量。那么我想定义一个行指针变量怎么办呢?

使用形如:int a[3][4]; int (*p)[4] = a;

案例:使用一个行指针变量输出二维数组

#include <stdio.h>int main() {int a[3][4] = {{0, 1, 2, 3},{10, 11, 12, 13},{20, 21, 22, 23},};int (*p)[4] = a;  //定义一个行指针变量int i, j, *q;for (i = 0; i < 3; i++) {q = *(p + i);    //列指针for (j = 0; j < 4; j++) {printf("%d\t", *q++);}printf("\n");}return 0;
}

根据之前讲过的,数组在内存的存储形式一直都是一维存储。所以我们完全可以使用一个列指针输出整个二维数组,看下面的代码:

#include <stdio.h>int main() {int a[3][4] = {{0, 1, 2, 3},{10, 11, 12, 13},{20, 21, 22, 23},};int i = 0, *p = *a;while (i++ < 12) {printf("%d\t", *p++);if (i % 4 == 0) printf("\n");}return 0;
}

6.5.4 将多维数组作为函数参数

将多维数组作为参数传递,之前讲过数组传递法。今天来比较比较,看案例:录入 3 名学生各 4 门课成绩,定义函数输出所有学生的总平均分、定义函数找出有科目不及格的学生并输出其全部成绩。

#include <stdio.h>void getAvg(int *p, int n) {int i, sum = 0;for (i = 0; i < n; i++)sum += *(p + i);printf("平均分:%lf\n", 1.0 * sum / n);
}void findLoser(int (*p)[4], int n) {int i, j;for (i = 0; i < n; i++) {for (j = 0; j < 4 && *(*(p + i) + j) >= 60; j++);if (j < 4) {for (j = 0; j < 4; j++)printf("%d\t", *(*(p + i) + j));printf("\n");}}
}int main() {int score[3][4] = {{59, 78, 91, 88},{98, 79, 83, 77},{100, 45, 98, 99}};// 调用求平均值函数。使用列指针的方式并传入分数的个数getAvg(*score, 12);// 调用找有成绩不及格的学生的函数,把成绩表和学生个数传进去findLoser(score, 3);return 0;
}

6.6 通过指针操作字符串

6.6.1 使用字符指针遍历字符串

我们之前学过数组的思路来操作字符串,现在我们看看指针类型的操作,看下面的案例:

#include <stdio.h>int main() {char str1[] = "Hello";puts(str1);char *str2 = "World!";str2 = "Ni Hao";//while (*str2 != 0) putchar(*str2++);puts(str2);return 0;
}

6.6.2 使用字符指针做参数

使用上和普通指针没差,看案例:

#include <stdio.h>void strcpy1(char *from, char *to) {while (*from != '\0') {*to = *from;to++, from++;}*to = '\0';
}void strcpy2(char from[], char to[]) {while ((*to++ = *from++) != '\0');
}int main() {char *from = "abcdef";char to[] = "sdigbvaldsbgvsl";strcpy1(from, to);puts(to);return 0;
}

6.6.3 对比数组名和字符指针变量

  1. 字符数组从定义时就有了明确的指向即数组空间。但字符指针变量定义时若是不给初值,那么它的指向是未知的。
  2. 赋值方式。可以对字符指针变量赋值,但数组名属于常量不可赋值。
char *a;
a = "abcd";  // 对字符指针变量赋值完全可以
a++;  // 指向下一个元素的地址,也是合法的char str[20];
str[0] = 'a';  // 对字符数组中某一个元素赋值是合理的
str = "heihei";  // 直接对数组名赋值是不可以的
str++;  // 数组名不可修改,这是错误的

​ 3.给初值的含义不同

// 这种写法是合法的。这会使得指针 a 直接指向常量 Hello 所在空间。
char *a = "Hello";// 这种写法也是合法的。但实际上是将常量的内容复制到数组 str 空间中。
char str[] = "Hello";

​ 4.存储单元的内容

编译时为字符数组分配若干存储单元供其使用。但对于指针变量来说,只会分配一个存储地址的空间,如果没有初值,直接使用指针变量有可能造成严重的bug。看下面的案例:

#include <stdio.h>int main() {char *a;scanf("%s", a);puts(a);return 0;
}

5.对元素的修改

对于数组里面的每一个元素都是可以修改的,但对于某些指针变量指向的字符串来说,元素可能不可修改。

#include <stdio.h>int main() {char a[] = "the";char *p = "the";a[0] = 'T';p[0] = 'T';puts(a);puts(p);return 0;
}

6.对数组元素的引用

对于数组名来说引用元素可以使用a[i], *(a + i)这样的方式oib,而且a[i]*(a + i)完全等价,必然都是获取下标为 i 的元素。对于指针变量来说也可以使用p[i], *(p + i)的方式来引用数组中的元素,其中p[i]*(p + i)也是完全等价的,但两者不一定是获取数组中下标为 i 的元素,例如char *p = a + 1

6.7 指向函数的指针

6.7.1 前言

程序中定义了一个函数,在编译时会把函数的源代码转变为可执行代码并存入内存中的程序区。这块内存也会有一个地址,称之为函数的入口地址。每次调用函数的时候都是从这个地址开始执行函数。函数名中就存储这块内存的地址。因此函数名本质上就是一个指向函数的指针。

6.7.2 函数指针的定义

定义一个函数指针的一般形式为:

返回值类型 (* 指针变量名) (函数参数列表);

例如,我们使用int (*p)(int a, int b);这样的写法就成功的定义出了一个函数指针,而且我们发现这个函数指针并不是能够指向所有的函数。它只能指向返回值类型为 int,并且有两个 int 类型参数的函数。还是那句话,其实对于形参的名字我们根本不关注,因此定义函数指针的写法可以简化为int (*p)(int, int);

案列:定义一个求最大值的函数并用一个函数指针指向它

#include <stdio.h>
int max(int a,int b){return a>b?a:b;
}
int main() {//定义一个返回值类型为int,且有两个int形参的函数指针int(*p)(int,int);//将同类型函数max赋值给函数指针pp=max;//使用函数指针来调用函数printf("%d\n",(*p)(1,2));return 0;
}

6.7.3 函数指针作为参数

函数指针作为参数的写法和定义函数指针是一样的,其一般形式类似于:

void test(int (*p)(int,int)){ }

案列:键盘输入一个简单的算术表达式,并求值

#include <stdio.h>
int add(int a,int b){return a+b;
}int sub(int a,int b){return a-b;
}int mul(int a,int b){return a*b;
}int mod(int a,int b){return a%b;
}int div(int a,int b){return a/b;
}int calculate(int a,int b,int (*p)(int,int)){return (*p)(a,b);
}int main()
{int a,b;char op;int (*p)(int,int);scanf("%d%c%d",&a,&op,&b);switch(op){default:case'+':p=add;break;case'-':p=sub;break;case'*':p=mul;break;case'/':p=div;break;case'%':p=mod;break;}printf("%d\n",calculate(a,b,p));return 0;
}

7.自定义数据类型

7.1 结构体

7.1.1 定义结构体

我们此前定义的数据都是离散的,没有内在联系的。但有时我们定义的数据是有联系的,例如学号、姓名、性别等数据都属于同一个人,那么它们之间是有联系的。为了描述这样的联系,我们可以定义一个结构体来存储它们。定义结构体的一般形式为:

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

案例:定义一个学生结构体包含生日,其中生日也是一个结构体

struct Date {int year;int month;int day;
};struct Student {int sno;char sname[20];// 引用结构体类型的时候前面的 struct 不能丢struct Date birthday;
};

7.1.2 定义结构体类型变量

1.正常定义变量

我们使用形如struct Student s1, s2;的形式来定义一个结构体变量,再次注意表示结构体类型的时候需要加上 struct 关键字。这和定义int a, b;没有本质区别。定义变量的时候,该变量分配到的内存大小为:4 + 20 + 4 * 3 = 36 字节。只要我们先定义了学生结构体,后面都可以使用这样的方式定义变量,非常灵活。

2.定义结构体的时候定义变量

这种写法的一般形式为:

struct 结构体名 {成员列表
} 变量名1, 变量名2, ..., 变量名n;

例如下面在定义学生结构体的同时定义变量:

struct Student {int sno;char sname[20];// 引用结构体类型的时候前面的 struct 不能丢struct Date birthday;
} s1, s2;struct Student s3;

这种方式将结构体的定义和变量的定义放在一起,书写简单但是不建议多用。

3.不指定类型名直接定义结构体变量

这种写法的一般形式为:

struct {成员列表
} 变量名1, 变量名2, ..., 变量名n;

和上面一样的例子,可以写成:

struct {int sno;char sname[20];// 引用结构体类型的时候前面的 struct 不能丢struct Date birthday;
} s1, s2;

看以看出使用这种方式定义结构体类型的时候并没有给这个结构体命名,即该结构体类型没有名字。显然这样的话,我们后面就无法使用这个结构体类型了,只能临时一用,因此也不建议使用这样的写法。

7.1.3 结构体变量的初始化

假设已经定义了学生结构体类型,定义和上述相同,我们使用如下的方式对结构体变量进行初始化:

// 初始化的时候使用大括号括起来,大括号里面的值按照顺序给到结构体变量里面的成员变量里
// 可以看到,对应结构体类型的成员变量我们单独使用大括号将其初值括起来
struct Student s = {1002, "Jack", {2022, 1, 1}};// 和上面相似,但是结构体类型成员变量初值外面的大括号可以省略,这同样能够依次赋值
struct Student s = {1002, "Jack", 2022, 1, 1};// 如果给的初值少于成员变量个数,那就从前到后依次给予,其他没分到初值的变量赋默认值 0
struct Student s = {1002, "Jack"};// 想要单独为某个成员变量赋值的时候可以采用下面的语法
// 使用 .sname="Jack" 表示将 Jack 赋予成员变量 sname
// 使用 .birthday.year = 2022 表示将 2022 赋予成员变量 birthday 的成员变量 year
struct Student s = {.sname = "Jack", .birthday.year = 2022};

7.1.4 结构体变量的引用

直接看案例:

#include <stdio.h>
#include <string.h>struct Date {int year;int month;int day;
};struct Student {int sno;char sname[20];struct Date birthday;
};int main() {struct Student s1, s2;// 对结构体变量的成员变量赋值使用“.”运算符s1.sno = 1002;strcpy(s1.sname, "Jack");// 如果成员变量里还有结构体变量,那么继续使用“.”往里面赋值s1.birthday.year = 2022;s1.birthday.month = 5;s1.birthday.day = 1;struct Date d = {2022, 1, 1};// 结构体变量之间可以互相赋值,就像普通变量一样s2.birthday = d;// 键盘输入学生2的姓名scanf("%s", s2.sname);// 输出学生1的学号printf("%d", s1.sno);return 0;
}

7.1.5 使用结构体数组

如果一个数组里面的元素类型都是结构体,那么这个数组就称为结构体数组。使用结构体数组和使用普通数组没啥不同

1.定义结构体数组的三种方式

//定义一个无名结构体的同时定义数组
struct{char name[20];int score;
}students[10];//在定义结构体类型的同时定义数组
struct Student {char name[20];int score;
}student[10];//使用结构体类型名定义一个数组
struct Student students[10];

2.结构体数组的初始化

struct Student {char name[20];int score;
};// 用一个大括号为数组赋值,每一个元素再用一层括号,缺失的元素补 0
struct Student students[10] = {{"Jack", 98}, {"Lucy", 100}};// 同样的,内层的大括号可以省略,缺失的元素补 0
struct Student students[10] = {"Jack", 98, "Lucy", 100};

案例:定义学生结构体类型(包含姓名和成绩),键盘输入每个学生的姓名和成绩,按照成绩降序排序

#include <stdio.h>
#include <string.h>struct Student {char name[20];int score;
};void sort(struct Student students[], int n) {int i, j;for (i = 0; i < n - 1; i++) {int k = i;for (j = k + 1; j < n; j++)if (students[j].score > students[k].score) k = j;if (i != k) {struct Student t = students[i];students[i] = students[k];students[k] = t;}}
}int main() {int i;struct Student students[3];for (i = 0; i < 3; i++) {scanf("%s%d", students[i].name, &students[i].score);}sort(students, 3);for (i = 0; i < 3; i++)printf("%s, %d\n", students[i].name, students[i].score);return 0;
}

7.1.6 结构体指针

指向结构体变量的指针就是结构体指针。结构体指针定义的一般形式为:

struct 结构体名 * 变量名;

案例:使用结构体指针指向一个结构体变量,并输出其成员变量

#include <stdio.h>struct Student {char name[20];int score;
};int main() {struct Student s = {"Jack", 100};struct Student *p = &s;printf("姓名:%s,成绩:%d\n", (*p).name, s.score);return 0;
}

从案例中也可以看出,结构体类型指针的使用和普通指针没什么区别。但是也可以发现,我们使用结构体指针访问对应的成员变量的时候还是比较麻烦的,因此 C 语法有了简化:(*p).name等价于p -> name。于是对于访问结构体变量中成员变量 score 有了以下三种等价写法:

s.score` <==> `(*p).score` <==> `p -> score //口诀:一般变量就用点,指针变量用箭头或者星号括号点。

7.1.7 指向结构体数组的指针

案例:使用结构体指针输出学生数组中每一个学生的信息

#include <stdio.h>struct Student {char name[20];int score;
};int main() {struct Student *p, students[2] = {"Jack", 100, "Lucy", 98};for (p = students; p < students + 2; p++)printf("姓名:%s,成绩:%d\n", p->name, p->score);p = students;while (p < students + 2) {printf("姓名:%s\n", p++->name);}return 0;
}

对比:p++->score(p++)->score++p->score(++p)->scorep->score++

7.1.8 结构体变量或指针作为函数参数

将一个结构体变量的值传递给一个函数有以下三种方式:

1.将结构体变量的成员做参数。

例如将s.score作为参数,若成员是普通类型那就是简单的复制值过去,双方互不影响;若成员是指针类型实参和形参指向同一块空间,形参操作这块空间时实参值也会变

2.直接将结构体变量做参数。

直接传递结构体变量就是值传递,会将结构体里面的数据复制一份到形参当中。若是结构体变量占据的内存很大,那么形参也需要同样大小的内存,有些浪费空间。而且因为是值传递,那么在函数内部操作结构体变量不会影响到实参就很苦恼。因此很少用这样的方式。

3.用指向结构体变量的指针做参数。

直接使用指针传递,和之前讲的效果完全一致,形参和实参双方都是操作同一块内存,那么函数中修改,函数外也是可见的。

案例:定义函数实现输入学生的姓名与四门课成绩、求出每个学生的总分、对学生按照总分排序、依次输出每个学生的信息

#include <stdio.h>struct Student {char name[20];int scores[4];int sum;
};void input(struct Student p[], int n) {int i, j;for (i = 0; i < n; i++) {int sum = 0;scanf("%s", p[i].name);for (j = 0; j < 4; j++) {scanf("%d", p[i].scores + j);sum += p[i].scores[j];}p[i].sum = sum;}
}void sort(struct Student *p, int n) {int i, j;for (i = 0; i < n - 1; i++) {int k = i;for (j = k + 1; j < n; j++)if (p[j].sum > p[k].sum) k = j;if (i != k) {struct Student t = p[i];p[i] = p[k];p[k] = t;}}
}void show(struct Student *p, int n) {int i, j;for (i = 0; i < n; i++) {printf("%s, ", p[i].name);for (j = 0; j < 4; j++) {printf("%d, ", p[i].scores[j]);}printf("%d\n", (p + i) -> sum);}
}int main() {struct Student students[3];input(students, 3);sort(students, 3);show(students, 3);return 0;
}

7.2 共用体

7.2.1 定义共用体

有时我们为节约空间想把多个变量存到同一块内存里,当然这样存的话,数据会互相覆盖,因此要求这几个变量不会同时访问。能实现这样功能的结构叫做共用体,其一般定义形式为:

union 共用体名 {成员列表;   
};

例如可以使用下面的用法:

// 在定义共用体的同时定义变量
union Data {int i;char ch;double d;
} d1, d2;// 使用共用体类型名定义变量
union Data d3, d4;// 定义一个无名共用体的同时定义变量
union {int i;char ch;double d;
} d5, d6;

可以看出,共用体和结构体的定义方式很类似,但两者差距很大。结构体变量所占据内存大小是其所有成员变量占据内存和,共用体占据内存大小由占据内存最大的成员决定。

7.2.2 引用共用体变量

1.初始化

// 初始化需要使用大括号将初值括起来
// 但是值得关注的是,共用体初始化时只能在大括号里写一个元素
union Data d = {100};// 同样的,可以使用下面的方式选择给哪一个变量赋初值
union Data d = {.ch = 'A'};

2.引用成员变量

union Data d;// 键盘给某一个成员变量赋值
scanf("%d", &d.i);// 虽然没有给 ch 这个成员赋值,但是我们给 i 变量赋值了
// 又因为 ch 与 i 公用内存,所以会将 int 型变量 i 的值转成 char 类型输出
putchar(d.ch);

7.2.3 共用体特性

因为共用体里各变量共用同一块内存,所以每个变量会互相覆盖。举个🌰:

union Data d;d.ch = 'A';
d.d = 3.14;
d.i = 100;

执行上面的案例后,内存里面会存整数 100,之前的字符和浮点数都被覆盖了。而且有趣的是,&a.i&a.d&a.ch三者的值是相同的。

7.3 枚举类型

7.3.1 枚举的定义

如果一个变量的取值只有有限的几种可能,那么我们可以把这个变量定义为枚举类型。所谓枚举就是将值一一列举出来,变量取值只能从列举出的值中选择。枚举类型的一般定义形式:

enum 枚举名 {枚举元素列表};

看案例:

// 定义一个枚举类型 Weekday,并且取值有下面 7 种取法
enum Weekday {sun, mon, tue, wed, thu, fri, sat};// 使用枚举名定义一个枚举变量
enum Weekday workday;// 定义枚举的同时定义枚举变量
enum Weekday {sun, mon, tue, wed, thu, fri, sat} workday;// 定义一个无名枚举的同时定义变量
enum {sun, mon, tue, wed, thu, fri, sat} workday;

7.3.2 枚举变量解析

1.引用枚举变量

enum Weekday workday;// 正确。将该枚举变量赋值为 7 种之一的 mon
workday = mon;// 错误。monday 是一个不存在的枚举常量
workday = monday;// 输出枚举变量的值
printf("%d", workday);

2.原理

C 编译系统会将枚举类型中的枚举元素当做常量看待,也就是说 sun、mon、…、sat 都是常量。因此类似于sun = 0的操作是错误的,不能对常量赋值。那么作为常量,sun 之类的枚举元素的值都是多少呢?有都是什么类型的数据呢?

答:枚举元素的类型是整型。如果在写枚举元素的时候没有给初值,那么默认从 0 开始编号,即sun=0, mon=1, ..., sat=6,我们也可以手动给枚举元素赋值。看案例:

// 这样给初值后,每一个枚举元素的值为:
// sun=3, mon=1, tue=2, wed=3, thu=2, fri=3, sat=4
enum Weekday {sun = 3, mon = 1, tue, wed, thu=2, fri, sat};

看以看出,你给初值,那么该元素就使用你给的值。后面没给到值的元素按照前面的值加一。所以我们发现,枚举元素的值有可能相同。

作为整型的一种,枚举元素也是可以比较的。if (workday == mon)...if (workday > tue)...

课后着重看看,枚举的书本例题:P324,例 9.12

7.4 使用 typedef 声明新类型

7.4.1 作用

使用 typedef 能够指定新的类型名来代替已有的类型名。例如:

typedef int Integer;int i;
Integer j;

使用上面的写法我们就可以使用 Integer 来定义一个整型变量。这个案例看不出什么,甚至定义完更复杂了,但对于某些本身就很复杂的类型,使用 typedef 会有简化程序的奇效。例如:

// 将下面的结构体定义为 Date 类型
typedef struct {int year;int month;int day;
} Date;// 直接使用定义出来的类型定义变量
Date d;
d.year = 2020;

7.4.2 用法

使用 typedef 的三步法:

  1. 按照定义变量的形式写出定义语句(int i;)
  2. 将变量名换成新类型名(int Integer;)
  3. 在最前面加上 typedef 关键字(typedef int Integer;)

不要小瞧三步法,对于某些复杂的类型使用 typedef 可没那么简单,例如下面使用 typedef 定义一个整型数组类型:

// 三步法为整型数组起别名
1. int a[10];
2. int Array[10];
3. typedef int Array[10];// 后面定义长度为 10 的整型数组可以使用以下两种方式
1. int a[10];
2. Array a;

习惯上我们会把 typedef 定义的变量首字母大写,以便于将其和系统默认的关键字区分开来。

7.5 结构体的应用——链表

7.5.1 前言

链表是一种重要的数据结构,它是一种能动态分配内存的结构。例如之前我们说的使用数组,那么数组必然是连续的空间,但链表不需要空间连续。而且链表可以实现要多少内存就申请多少内存,故链表的在某些情况使用频率很高。下图能够阐述最简单的链表结构:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qs3IBrM1-1665324647772)(E:\Blog\source\images\image-20221009214711300.png)]

链表有一个头指针,图中用 head 表示,他存放一个地址,该地址指向链表的一个元素。链表的每一个元素称为“结点”,每个节点都包含两个信息:

  1. 用户需要使用的实际数据
  2. 下一个节点的地址

可以看出,head 指向第一个元素,第一个元素指向第二个元素……以此类推,最后一个元素没有指向,用 NULL 代替。为什么需要一个指针专门指向下一个节点的地址?因为链表的节点之间在内存中是不连续的,因此没有办法通过前一个元素的地址推算出下一个元素的地址,故而需要使用额外的空间存储下一个元素的地址

通过观察,可以发现最适合链表元素的数据结构是结构体,因为结构体可以把多个数据封装成一个整体。例如我想定义一个学生链表,该结构体要如何设计呢?看下面:

/**
结构体中 name 和 score 是我们需要描述一个学生的基本信息,由我们的业务逻辑决定。
next 是一个学生类型的结构体指针,使用 next 变量可以指向下一个学生的地址。
*/
struct Student {char name[20];int score;struct Student *next;
}

7.5.2 静态建立链表

#include <stdio.h>struct Student {int num;int score;struct Student *next;
};int main() {struct Student s1, s2, s3;struct Student *p, *head;s1.num = 1001, s1.score = 98;s2.num = 1002, s2.score = 86;s3.num = 1003, s3.score = 100;head = &s1, s1.next = &s2, s2.next = &s3, s3.next = NULL;for (p = head; p != NULL; p = p -> next) {printf("%d, %d\n", p -> num, p -> score);}return 0;
}

代码很简单。先建立三个结构体变量,然后通过每个人的 next 成员将它们链接起来。最后让 head 指向 s1。访问的时候,使用指针从前到后依次访问这个链表的每一个值。

7.5.3 动态建立链表

#include <stdio.h>
#include <stdlib.h>typedef struct node {int n;struct node *next;
} Node;Node *newNode(int n) {Node *p = (Node *) malloc(sizeof(Node));p -> n = n;p -> next = NULL;return p;
}void show(Node *head) {Node *p;for (p = head; p != NULL; p = p -> next)printf("%d\t", p -> n);
}int main() {int i, n = 5;int data[] = {1, 2, 3, 4, 5};Node *t, *p, *head = NULL;for (i = 0; i < n; i++) {t = newNode(data[i]);if (head == NULL) head = t;else p -> next = t;p = t;}show(head);return 0;
}

7.5.4 销毁创建的链表

void destroy(Node *head) {Student *p = head, *q;while (p != NULL) {q = p -> next;free(p);p = q;}
}

7.5.6 关于链表的习题

  1. 写函数实现:元素的创建、链表的创建、链表的销毁、链表的输出、链表元素个数的统计

    #include <stdio.h>
    #include <stdlib.h>typedef struct Node {int data;struct Node *next;
    } Node;Node * newNode(int data) {Node *s = (Node *) malloc(sizeof(Node));s -> data = data;s -> next = NULL;return s;
    }void show(Node *head) {while (head != NULL) {printf("%d\n", head->data);head = head -> next;}
    }void destroy(Node *head) {Node *p = head, *q;while (p != NULL) {q = p -> next;free(p);p = q;}
    }int count(Node *head) {int n = 0;while (head != NULL) {n++;head = head -> next;}return n;
    }int main() {int i, n = 5;int nums[] = {1, 2, 3, 4, 5};struct Node *head = NULL, *p = NULL, *q;for (i = 0; i < n; i++) {q = newNode(nums[i]);if (head == NULL) head = q;else p -> next = q;p = q;}show(head);destroy(head);return 0;
    }
    
    1. 写函数判断链表是否是一个环

      int isCircle(Node *head) {if (head == NULL) return 0;Node *p = head -> next;while (p != NULL) {if (p == head) return 1;p = p -> next;}return 0;
      }
      

      3.写函数反转整个链表

      Node * reverse(Node *head) {if (head == NULL) return head;Node *p = head -> next, *q;head -> next = NULL;while (p != NULL) {q = p -> next;p -> next = head;head = p;p = q;}return head;
      }
      
      Node *reverse2(Node *head) {if (head == NULL || head -> next == NULL) return head;Node *p = reverse2(head -> next), *q = p;while (q -> next != NULL) q = q -> next;q -> next = head;head -> next = NULL;return p;
      }

      4.写函数判断两个链表是否有交集

      int isTransaction(Node *p, Node *q) {int i = 0;int c1 = count(p), c2 = count(q);if (c1 < c2) {Node *t = p;p = q;q = t;}while (i++ < abs(c1 - c2)) p = p -> next;while (p != q) {p = p -> next;q = q -> next;}return p != NULL;
      }
      

      5.写函数判断链表中是否存在环

      int hasCircle(Node *head) {Node *p = head, *q = head;while (p != NULL) {q = q -> next;p = p -> next;if (p == NULL) return 0;p = p -> next;if (p == q) return 1;}return 0;
      }
      

      6.往有序链表中插入一个元素

      Node *insert(Node *head, int n) {Node *p = head, *q = newNode(n);if (head == NULL) return q;if (head -> data >= n) {q -> next = head;return q;}while (p != NULL) {if (p->next == NULL || p->next->data >= n) {q -> next = p -> next;p -> next = q;return head;}p = p->next;}return NULL;
      }
      

      7.综合题:定义数据结构存储学生的姓名、四门课成绩。定义函数求:每个学生的总分、每个科目的平均分、对学生按照总分排序并输出排序后学生信息

#include <stdio.h>
#include <stdlib.h>#define N 50typedef struct {char name[20];int scores[4];int total;
} Student;void show(Student students[], int n) {int i, j;for (i = 0; i < n; i++) {printf("%s: ", students[i].name);for (j = 0; j < 4; j++)printf("%d, ", students[i].scores[j]);printf("%d\n", students[i].total);}
}void getTotal(Student students[], int n) {int i, j;for (i = 0; i < n; i++) {int sum = 0;for (j = 0; j < 4; j++)sum += students[i].scores[j];students[i].total = sum;printf("第 %d 个学生的总分为:%d\n", i, sum);}
}void getAvg(Student students[], int n) {int i, j;for (i = 0; i < 4; i++) {double sum = 0.0;for (j = 0; j < n; j++)sum += students[j].scores[i];printf("科目 %d 平均分为:%lf\n", i, sum / 4);}
}void selectSort(Student students[], int n) {int i, j;for (i = 0; i < n - 1; i++) {int k = i;for (j = k + 1; j < n; j++)if (students[j].total > students[k].total) k = j;if (i != k) {Student t = students[i];students[i] = students[k];students[k] = t;}}
}int main() {int i, j, n;Student students[N];scanf("%d", &n);for (i = 0; i < n; i++) {scanf("%s", students[i].name);for (j = 0; j < 4; j++)scanf("%d", &students[i].scores[j]);}getTotal(students, n);getAvg(students, n);selectSort(students, n);show(students, n);return 0;
}

8.文件

8.1 理解文件

8.1.1 文件名

文件名是一个文件的唯一标识,它包括三部分:(1)文件路径;(2)文件名主干;(3)文件后缀。

举个例子,D:\User\C\file.txt就是一个文件名,千万不要以为file.txt是一个文件名,文件名是包含文件路径的。

8.1.2 文件的分类

在 C 语言里,数据文件可分为ASCII 文件(文本文件)二进制文件(映像文件)。如果数据直接以二进制的形式存到文件中,那么这个文件就是一个二进制文件。如果将内容按照 ASCII 代码的形式存储在文件中,那么这个文件被称为文本文件。

举个例子,对于整数 1000,其对应的二进制为00000000 00000000 00000011 11101000,若是直接将这串二进制存到文件中,那么这个文件就是一个二进制文件。对于整数 1000 来说,我们还可以将其视为字符1、0、0、0的组合,而字符1的 ASCII 码为 49(00110001)、字符0的 ASCII 码为 48(00110000)。所以这种情况下,1000 的二进制表示形式为:00110001 00110000 00110000 00110000,若是将这样的二进制存进文件,那么这个文件就是文本文件。

可以看出,不管是二进制文件还是文本文件,它们存储的时候都是存的二进制。对于文本文件,其存取的时候要做二进制转换,所以文本文件的存取效率比较低。

8.1.3 文件缓冲区

文件缓冲区是系统自动的在内存中为每一个正在使用的文件开辟的缓冲区。从内存向磁盘输出数据的时候必须先将数据送到缓冲区中,待缓冲区满了之后再由系统将其写入磁盘。从磁盘读文件的时候,系统一次从文件读取一批内容装满缓冲区,然后程序读取的内容都是从缓冲区拿。有了这么一个缓冲区,系统操作文件的效率会大大增加,能节省很多时间。注意:读文件时的缓冲区和写文件时候的缓冲区是同一个缓冲区。

8.1.4 文件指针

之前讲指针变量的时候,这些普通的指针会指向内存中某一块空间。文件指针和这个大同小异,它就是一个指向文件内容的指针。我们知道指针可以移动,文件指针也可以移动用来改变对文件内容的指向。文件指针的定义一般形式为:FILE *fp;

其中 FILE 是系统定义的文件类型(是一种结构体),其定义在stdio.h中,毕竟文件的输入输出也属于输入输出不是?因此定义在标准输入输出头文件里合情合理。若是要同时访问多个文件,需要定义多个文件指针,因为一个文件指针只能同时操作一个文件。

注意:文件指针并不会真正指向外存的地址,它还是指向某一个内存地址。之前说过的,文件的读取都依赖于缓冲区,这个缓冲区是在内存里的。

8.2 打开关闭文件

文件使用之前应该先打开文件,文件使用结束之后应该关闭文件。所谓打开和关闭是形象的说法,打开文件会为该文件创建一个缓冲区并设置文件指针指向它。关闭文件就是断开这个文件指针的指向并销毁该文件对应的缓冲区。可见,文件关闭之后,文件指针就不会指向这个文件了,所以后续也不能直接使用这个文件指针了。

8.2.1 打开文件

C 语言中使用库函数 fopen 来打开文件,其一般调用形式为:

**FILE \*fp 
.= fopen(文件名, 文件使用方式);**

fopen 函数有两个参数,一个是文件名,注意这是包含路径的文件名,如果不包含路径会按照执行文件所在目录找文件。第二个参数是文件使用方式,例如你是想只读文件或只写文件或读写文件?两个参数都使用字符串表示。

文件使用方式详细见下表:

文件使用方式含义指定文件不存在时
r(只读)只能读文件,操作文本文件报错
w(只写)只能销毁型写文件,操作文本文件创建新文件
a(追加)只能追加型写文件,操作文本文件报错
rb(只读)只能读文件,操作二进制文件报错
wb(只写)只能销毁型写文件,操作二进制文件创建新文件
ab(追加)只能追加型写文件,操作二进制文件报错
r+(读写)读与覆盖型写文件,操作文本文件报错
w+(读写)读与销毁型写文件,操作文本文件创建新文件
a+(读写)读与追加型写文件,操作文本文件报错
rb+(读写)读与覆盖型写文件,操作二进制文件报错
wb+(读写)读与销毁型写文件,操作二进制文件创建新文件
ab+(读写)读与追加型写文件,操作二进制文件报错

对于这些文件操作模式中,模式名里带w的都比较特殊。使用这样的模式打开文件,若这个文件存在就删除该文件,然后重新创建一个新文件。因此,使用这样的方式打开文件会完全清空文件内容,谨慎使用。

打开文件时可能因为各种原因导致文件打开失败,此时 fopen 函数会返回 NULL。因此,我们打开文件时一般使用下面的写法:

FILE *fp;
if ((fp = fopen("fileName", "modeString")) == NULL) {printf("open file error\n");exit(0);  // 退出程序
}
...

调用 fopen 函数之后要判断返回的文件指针是否为 NULL,如果是 NULL 的话直接退出程序,否则做自己的操作。

8.2.2 关闭文件

C 语言中使用库函数 fclose 来关闭文件,其一般调用形式为:**fclose(fp);**

fclose 函数只有一个参数,那就是一个文件指针。从前面我们知道,关闭文件的时候会断开文件指针和该文件的联系并撤销对应的缓冲区。而在撤销缓冲区的时候会将缓冲区里面的内容刷新到磁盘里面,如果没有文件关闭的操作,有可能导致缓冲区的内容丢失,造成莫名其妙的错误。因此在使用完文件之后记得关闭文件。

fclose 函数有一个整型返回值,关闭成功返回 0,关闭失败返回 -1(EOF)。

8.3 顺序读写文件

顺序读写文件就是按照文件内容的顺序从前到后依次读写,不会出现跳跃到后面先读写后面内容的现象。

8.3.1 向文本文件读写一个字符

函数名调用形式功能返回值
fgetcfgetc(fp)从 fp 指向的文件读取一个字符读成功,返回读到的字符;失败则返回 EOF。
fputcfputc(ch, fp)将字符 ch 写到 fp 指向的文件中写成功,返回 ch;失败返回 EOF。

这两个函数名被使用 #define 定义了别名分别叫做:getc 和 putc。

案例1:键盘输入一行字符,将其写入到文件中

#include <stdio.h>
#include <stdlib.h>int main() {FILE *fp;if ((fp = fopen("test.txt", "w")) == NULL) {printf("File Open Error!");exit(0);}char ch;while ((ch = getchar()) != '\n') {fputc(ch, fp);}fclose(fp);return 0;
}

案例2:复制一个文件内容到另一个文件之中

#include <stdio.h>
#include <stdlib.h>int main() {FILE *fp, *fp2;if ((fp = fopen("test.txt", "r")) == NULL) {printf("File Open Error!");exit(0);}if ((fp2 = fopen("test2.txt", "w")) == NULL) {printf("File Open Error!");exit(0);}char ch;while ((ch = fgetc(fp)) != EOF) {fputc(ch, fp2);}fclose(fp);fclose(fp2);return 0;
}

8.3.2 从文本文件读写一个字符串

函数名调用形式功能返回值
fgetsfgets(str, n, fp)从 fp 指向的文件中读取 n - 1 个字符存到字符数组 str 中。读成功返回 str 的地址;失败返回 NULL。
fputsfputs(str, fp)把 str 指向的字符串写到 fp 指向的文件中。写成功返回 0;失败返回 EOF。

对 fgets 函数,是从文件中读取 n - 1 个字符存入字符数组 str 中,因为会在最后添加一个 \0,所以实际上 str 中会存储 n 个字符。如果在读取的过程中,遇到 \n 或文件结束,读到多少字符就往 str 里面存多少字符,其中 \n 也会被写入。

案例:从键盘读取几行字符串排序后写到文件中

#include <stdio.h>
#include <stdlib.h>
#include <string.h>#define N 50void selectSort(char a[][N], int n) {int i, j;char t[N];for (i = 0; i < n - 1; i++) {int k = i;for (j = k + 1; j < n; j++)if (strcmp(a[j], a[k]) < 0) k = j;if (i != k) {strcpy(t, a[i]);strcpy(a[i], a[k]);strcpy(a[k], t);}}
}int main() {FILE *fp;if ((fp = fopen("test.txt", "w")) == NULL) {printf("File Open Error!");exit(0);}int i, n;char strings[N][N];scanf("%d", &n);getchar();for (i = 0; i < n; i++)gets(strings[i]);selectSort(strings, n);for (i = 0; i < n; i++) {if (i > 0) fputc('\n', fp);fputs(strings[i], fp);}fclose(fp);return 0;
}

8.3.3 格式化方式读写文本文件

还记得控制台的格式输入输出函数 scanf 和 printf 吗?对于文件的格式输入输出函数也是差不多的。

函数名调用形式功能返回值
fscanffscanf(fp, 格式字符串, 地址列表)按照指定格式从文件读取成功读取变量个数
fprintffprintf(fp, 格式化字符串, 输出列表)按照指定格式向文件写入成功写入字符个数

fscanf 的返回值表示成功读取变量的个数。因为 fprintf 是按照字符串的格式写入的,所以其返回值表示成功写入字符的个数,\0 不参与写入。

案例:按照指定格式从文件读取变量 a、b、c 的值并输出其和

#include <stdio.h>
#include <stdlib.h>int main() {FILE *fp;if ((fp = fopen("test.txt", "r")) == NULL) {printf("File Open Error!");exit(0);}int a, b, c;while (fscanf(fp, "a=%d, b=%d, c=%d", &a, &b, &c) == 3) {printf("%d + %d + %d = %d\n", a, b, c, a + b + c);fgetc(fp);}fclose(fp);return 0;
}

8.3.4 二进制文件读写

函数名调用形式功能返回值
freadfread(buffer, size, count, fp)从文件中读取 size * count 个字节数据存到 buffer 中。buffer 是一个指针。成功就返回 count;失败返回其他数字。
fwritefwrite(buffer, size, count, fp)将 buffer 中前 size * count 字节的数据写进文件中。buffer 是一个指针。成功返回 count;失败返回其他数字。

案例:键盘输入一些学生的信息,将其保存到文件中并读取打印出来

#include <stdio.h>
#include <stdlib.h>#define N 50typedef struct {char name[20];int age;int score;
} Student;void write(Student students[], int n) {FILE *fp;if ((fp = fopen("test.dat", "wb")) == NULL) {printf("File Open Error!");exit(0);}fwrite(students, sizeof(Student), n, fp);fclose(fp);
}void read(int n) {FILE *fp;if ((fp = fopen("test.dat", "rb")) == NULL) {printf("File Open Error!");exit(0);}Student stu;for (int i = 0; i < n; i++) {fread(&stu, sizeof(Student), 1, fp);printf("%s, %d, %d\n", stu.name, stu.age, stu.score);}fclose(fp);
}int main() {int i, n;scanf("%d", &n);Student students[N];for (i = 0; i < n; i++) {scanf("%s%d%d", students[i].name, &students[i].age, &students[i].score);}write(students, n);read(n);return 0;
}

8.4 随机读写文件

8.4.1 前言

之前我们讲过顺序读写文件,这需要从前到后依次读取文件内容,原理很简单:刚打开文件时,文件指针指向内容开头,每次读写一块内容之后指针自动后移,那么下一次操作的就是下一部分内容。随机读取可以直接定位到某一位置进行文件的读写,这主要靠手动移动文件指针来实现。

8.4.2 定位文件指针

  1. 重置指针到开头

函数一般形式为:**rewind(文件指针)**

案例:使用一个文件指针往某文件写入“abcd”然后通过读文件将其读取出来并输出

#include <stdio.h>
#include <stdlib.h>int main() {FILE *fp;if ((fp = fopen("test.txt", "w+")) == NULL) {printf("File Open Error!");exit(0);}char string[20] = {0};fprintf(fp, "abcdefg");rewind(fp);fscanf(fp, "%s", string);puts(string);fclose(fp);return 0;
}
  1. 移动文件指针

函数一般形式为:**fseek(文件指针, 位移量, 起始点)**

位移量是指你想让指针移动多少字节,通过正负数来控制向前还是向后移动,这是一个 long 类型的整数。起始点是指移动指针时以谁为基准。

起始点名字对应值
文件开头位置SEEK_SET0
文件当前位置SEEK_CUR1
文件结尾位置SEEK_END2
// 以文件开头为基准,向后偏移 100 字节
fseek(fp, 100L, 0);// 以文件结尾为基准,向前偏移 100 字节
fseek(fp, -100L, SEEK_END);// 以文件指针当前位置为基准,向后偏移 100 字节
fseek(fp, 100L, SEEK_CUR);
  1. 获取文件指针当前位置

因为文件指针经常移动,为了能够知道当前指针已经偏移到什么位置,可以调用**ftell(文件指针)**函数来获取当前文件指针距离文件开头的偏移量。如果函数调用出现问题则返回 -1。

8.4.3 随机读写

使用 rewind 和 fseek 函数可以达到定位文件指针的目的,这样就可以实现随机读写了。

案例:准备 3 个学生的信息,将其存到文件中,并将奇数位学生的信息读取出来并打印。

void read(int n) {FILE *fp;if ((fp = fopen("test.dat", "rb")) == NULL) {printf("File Open Error!");exit(0);}int i;Student student;for (i = 0; i < n; i += 2) {fread(&student, sizeof(Student), 1, fp);printf("%s, %d, %d\n", student.name, student.age, student.score);fseek(fp, sizeof(Student), SEEK_CUR);}fclose(fp);
}

8.4.4 文件读写出错检测

  1. ferror 函数

在调用各种输入输出函数(putc、getc、fread、fwrite等)出错的时候,除了各自函数返回值可以体现出错信息,你还可以调用**ferror(文件指针)**函数来判断文件调用是否出错。如果 ferror 函数返回 0 表示一切正常,若 ferror 函数返回一个非 0 值表示出错了。

注意,ferror 的返回值是动态变化的,每次调用文件读写函数都会改变其值。因此,要使用其返回值就要及时。

  1. clearerr 函数

使用这个函数来清空出错标记。如果某次文件读写出了错误,ferror 函数会返回一个非 0 值,这时调用 clearerr 函数能够将其返回值清 0。这是手动清 0,你也可以调用其他文件读写函数来改变 ferror 的返回值。
fclose(fp);
return 0;
}


#### 8.3.3 格式化方式读写文本文件还记得控制台的格式输入输出函数 scanf 和 printf 吗?对于文件的格式输入输出函数也是差不多的。| **函数名** | **调用形式**                          | **功能**               | **返回值**       |
| ---------- | ------------------------------------- | ---------------------- | ---------------- |
| fscanf     | `fscanf(fp, 格式字符串, 地址列表)`    | 按照指定格式从文件读取 | 成功读取变量个数 |
| fprintf    | `fprintf(fp, 格式化字符串, 输出列表)` | 按照指定格式向文件写入 | 成功写入字符个数 |fscanf 的返回值表示成功读取变量的个数。因为 fprintf 是按照字符串的格式写入的,所以其返回值表示成功写入字符的个数,\0 不参与写入。案例:按照指定格式从文件读取变量 a、b、c 的值并输出其和```c
#include <stdio.h>
#include <stdlib.h>int main() {FILE *fp;if ((fp = fopen("test.txt", "r")) == NULL) {printf("File Open Error!");exit(0);}int a, b, c;while (fscanf(fp, "a=%d, b=%d, c=%d", &a, &b, &c) == 3) {printf("%d + %d + %d = %d\n", a, b, c, a + b + c);fgetc(fp);}fclose(fp);return 0;
}

8.3.4 二进制文件读写

函数名调用形式功能返回值
freadfread(buffer, size, count, fp)从文件中读取 size * count 个字节数据存到 buffer 中。buffer 是一个指针。成功就返回 count;失败返回其他数字。
fwritefwrite(buffer, size, count, fp)将 buffer 中前 size * count 字节的数据写进文件中。buffer 是一个指针。成功返回 count;失败返回其他数字。

案例:键盘输入一些学生的信息,将其保存到文件中并读取打印出来

#include <stdio.h>
#include <stdlib.h>#define N 50typedef struct {char name[20];int age;int score;
} Student;void write(Student students[], int n) {FILE *fp;if ((fp = fopen("test.dat", "wb")) == NULL) {printf("File Open Error!");exit(0);}fwrite(students, sizeof(Student), n, fp);fclose(fp);
}void read(int n) {FILE *fp;if ((fp = fopen("test.dat", "rb")) == NULL) {printf("File Open Error!");exit(0);}Student stu;for (int i = 0; i < n; i++) {fread(&stu, sizeof(Student), 1, fp);printf("%s, %d, %d\n", stu.name, stu.age, stu.score);}fclose(fp);
}int main() {int i, n;scanf("%d", &n);Student students[N];for (i = 0; i < n; i++) {scanf("%s%d%d", students[i].name, &students[i].age, &students[i].score);}write(students, n);read(n);return 0;
}

8.4 随机读写文件

8.4.1 前言

之前我们讲过顺序读写文件,这需要从前到后依次读取文件内容,原理很简单:刚打开文件时,文件指针指向内容开头,每次读写一块内容之后指针自动后移,那么下一次操作的就是下一部分内容。随机读取可以直接定位到某一位置进行文件的读写,这主要靠手动移动文件指针来实现。

8.4.2 定位文件指针

  1. 重置指针到开头

函数一般形式为:**rewind(文件指针)**

案例:使用一个文件指针往某文件写入“abcd”然后通过读文件将其读取出来并输出

#include <stdio.h>
#include <stdlib.h>int main() {FILE *fp;if ((fp = fopen("test.txt", "w+")) == NULL) {printf("File Open Error!");exit(0);}char string[20] = {0};fprintf(fp, "abcdefg");rewind(fp);fscanf(fp, "%s", string);puts(string);fclose(fp);return 0;
}
  1. 移动文件指针

函数一般形式为:**fseek(文件指针, 位移量, 起始点)**

位移量是指你想让指针移动多少字节,通过正负数来控制向前还是向后移动,这是一个 long 类型的整数。起始点是指移动指针时以谁为基准。

起始点名字对应值
文件开头位置SEEK_SET0
文件当前位置SEEK_CUR1
文件结尾位置SEEK_END2
// 以文件开头为基准,向后偏移 100 字节
fseek(fp, 100L, 0);// 以文件结尾为基准,向前偏移 100 字节
fseek(fp, -100L, SEEK_END);// 以文件指针当前位置为基准,向后偏移 100 字节
fseek(fp, 100L, SEEK_CUR);
  1. 获取文件指针当前位置

因为文件指针经常移动,为了能够知道当前指针已经偏移到什么位置,可以调用**ftell(文件指针)**函数来获取当前文件指针距离文件开头的偏移量。如果函数调用出现问题则返回 -1。

8.4.3 随机读写

使用 rewind 和 fseek 函数可以达到定位文件指针的目的,这样就可以实现随机读写了。

案例:准备 3 个学生的信息,将其存到文件中,并将奇数位学生的信息读取出来并打印。

void read(int n) {FILE *fp;if ((fp = fopen("test.dat", "rb")) == NULL) {printf("File Open Error!");exit(0);}int i;Student student;for (i = 0; i < n; i += 2) {fread(&student, sizeof(Student), 1, fp);printf("%s, %d, %d\n", student.name, student.age, student.score);fseek(fp, sizeof(Student), SEEK_CUR);}fclose(fp);
}

8.4.4 文件读写出错检测

  1. ferror 函数

在调用各种输入输出函数(putc、getc、fread、fwrite等)出错的时候,除了各自函数返回值可以体现出错信息,你还可以调用**ferror(文件指针)**函数来判断文件调用是否出错。如果 ferror 函数返回 0 表示一切正常,若 ferror 函数返回一个非 0 值表示出错了。

注意,ferror 的返回值是动态变化的,每次调用文件读写函数都会改变其值。因此,要使用其返回值就要及时。

  1. clearerr 函数

使用这个函数来清空出错标记。如果某次文件读写出了错误,ferror 函数会返回一个非 0 值,这时调用 clearerr 函数能够将其返回值清 0。这是手动清 0,你也可以调用其他文件读写函数来改变 ferror 的返回值。


http://chatgpt.dhexx.cn/article/gfqcrJw9.shtml

相关文章

天池比赛总结1

这次参加天池的一场比赛 先把数据读取了如下 接下来准备使用YOLO框出图片中的字符&#xff0c;然后进行识别

比赛总结+近期总结

比赛总结&#xff1a; 这次比赛没考好 20(没加高精度)0&#xff08;文件输出写错&#xff09;1000120 T1&#xff1a;这一题的方法是分解质因数高精度 T2&#xff1a;明显就是一道spfa的题嘛 T3&#xff1a;强大的四维DP&#xff08;我的神啊&#xff01;&#xff09; T4&#…

计算机课件比赛总结,课件制作比赛活动总结

【www.gz85.com - 投篮比赛活动工作总结】 课件制作比赛&#xff0c;是对计算机多媒体等辅助手段的一次检阅&#xff0c;也有力地促进了制作多媒体课件技艺的提高。下面是小编为您整理的“课件制作比赛活动总结”&#xff0c;仅供参考&#xff0c;希望您喜欢&#xff01;更多详…

2018年全国邀请赛(江苏) 比赛总结

先吐槽一下中矿大。。。周六在食堂吃的午饭&#xff0c;肉菜一个鱼一个辣土豆炒牛肉&#xff0c;对于对鱼过敏又感冒比较严重的我来说。。。&#xff08;然后再也没去食堂吃饭&#xff09; 南湖校区大是真大&#xff0c;风景也不错&#xff0c;就是门口离体育馆有点远。。。&a…

比赛总结

比赛总结 比赛总结-a5165.png 初赛终于结束了&#xff0c;头一次如此投入去打比赛&#xff0c;这一个多月以来真是痛并快乐着。最大的感悟是&#xff1a;构造线下验证集并没有什么用&#xff0c;做了一堆工作还不如一个leak。首先取得这个成绩算是给自己一个交代了&#xff0c;…

关于全国大学生软件测试大赛总结与反思

关于全国大学生软件测试大赛总结与反思 文章目录 一、软件测试大赛简介二、可能出现的错误三、个人总结与反思四、谈谈软件测试工程师1、测试的三个阶段2、就业优势3、就业要求4、参考薪资 一、软件测试大赛简介 由教育部软件工程专业教学指导委员会、全国高等院校计算机基础教…

【赛后总结】第十三届服务外包创新创业大赛总结——A14

目录 前言组队&#xff06;选题分工&项目推进提交材料&项目答辩区域赛初赛区域赛决赛全国总决赛 写在最后 前言 先摆两个参赛视频 初赛视频 决赛视频 比赛已经过去几个月了&#xff0c;也算是想起来这个比赛可以写一个总结了。在历时8个月左右的时间之后&#xff0c;我…

计算机大赛总结发言稿,学校技能比赛总结发言稿

学校技能比赛总结发言稿 总结就是把一个时间段取得的成绩、存在的问题及得到的经验和教训进行一次全面系统的总结的书面材料&#xff0c;写总结有利于我们学习和工作能力的提高&#xff0c;因此十分有必须要写一份总结哦。那么总结要注意有什么内容呢&#xff1f;以下是小编帮大…

计算机知识与技能比赛活动总结,中职技能大赛总结(精选6篇)

中职技能大赛总结(精选6篇) 总结是对取得的成绩、存在的问题及得到的经验和教训等方面情况进行评价与描述的一种书面材料&#xff0c;它能够使头脑更加清醒&#xff0c;目标更加明确&#xff0c;因此我们需要回头归纳&#xff0c;写一份总结了。那么你知道总结如何写吗&#xf…

稳定的iOS迅雷来了 不用再每次想用都要重装

迅雷iOS下载:www.xunlei-iosd.top 用过苹果产品的朋友都知道&#xff0c;在iOS系统里&#xff0c;迅雷这一APP是不存在的。当有朋友分享了某些资源给你&#xff0c;只能想办法使用复杂的操作安装一个寿命只有几天的迅雷APP。 今天&#xff0c;官方推出了“永久版”iOS迅雷&…

iOS - Threads 多线程

1、Threads 1.1 进程 进程是指在系统中正在运行的一个应用程序。每个进程之间是独立的&#xff0c;每个进程均运行在其专用且受保护的内存空间内。 比如同时打开 QQ、Xcode&#xff0c;系统就会分别启动两个进程。通过 “活动监视器” 可以查看 Mac 系统中所开启的进程。 一个程…

iOS面试知识点梳理

1.iOS开发者账号类型 “个人”开发者可以申请升级“公司”&#xff0c;可以通过拨打苹果公司客服电话&#xff08;400 6701 855&#xff09;来咨询和办理。公司账号允许多个开发者进行协作开发&#xff0c;比个人多一些帐号管理的设置&#xff0c;可设置多个Apple ID&#xff0…

2019年iOS面试真题大全(3-5年)

如果你想去大公司,如果你是3年左右的iOS开发者,如果你对面试的未知没有十足的信心,如果你期望的薪资在15K,那么请认真刷完这300道面试题,都是真实公司经历的…答案会在近期更新!你要先自思考,看个人那些方面还不足! 1、自我介绍 2、如何实现一个倒计时功能&#xff0c;类似于蘑…

IOS开发系列之阿堂教程:构建开发IOS应用的虚拟机开发环境实践

说到IOS的开发,不能不说 到一个问题,如何配置和构建一个IOS的开发环境!我下面要说的主要是针对没有MAC Apple机的网友,如何安装和配置一个属于自己的IOS开发环境。如果已经有MAC 苹果机的网友,请忽略此文。因为有MAC 苹果机,就只需要安装XCODE的IDE开发工具就行了。 …

web安全渗透测试基础知识

渗透测试入门 渗透测试前置知识靶场环境搭建windows基础网络基础web应用/架构搭建/站库分离/路由访问web四大件-系统web四大件-中间件web四大件-数据库web四大件-源码路由访问 web架构/前后端分离/建站分配Web架构/OSS存储/CDN加速/反向代理APP架构反弹SHELL/文件下载抓包技术算…

《iOS移动开发从入门到精通》图书连载2:如何成为一名iOS开发者

iOS开发人员&#xff0c;和其它传统开发者相比有哪些不同之处&#xff1f;需要具备怎样的硬件和软件条件&#xff1f;今天我们就来说说这一部分的内容。 一.硬件上的需求 开发iOS应用&#xff0c;首先您需要使用Apple的电脑。尽管您可以使用黑苹果或者虚拟机&#xff0c;在PC上…

Jetson Nano目标检测手把手实战教程(pytorch训练、tensorrt推理,含完整代码和数据)

目录 一、概述 1.1 深度学习和边缘计算 1.2 Jetson Nano简介 二、深度学习环境安装 2.1 Pytorch框架 2.2 在Jetson Nano上安装Pytorch 三、算法原理 四、具体实现步骤 4.1 工程代码和环境准备 4.2 模型训练和推理 4.2.1数据集准备 4.2.2训练 4.2.3模型转换 4.2.4…

各种HOOK方式和检测对抗方法

一、什么是HOOK? hook翻译过来是拦截的意思, 我们很多时候也叫钩子,其实是很形象的. hook有什么作用呢? 1.当代码执行到某行时,获取寄存器值和内存里的值&#xff0c;进行调试分析&#xff0c;例如hook明文包. 2.当代码执行到某行时,插入想执行的代码.例如迅雷拦截发包函…

2019 - iOS最新最全面试题梳理(内含框架和算法题)

内存中的区域划分 栈区(stack):由系统自动分配和释放&#xff0c;存放局部变量的值&#xff0c;容量小速度快&#xff0c;有序 堆&#xff1a;一般由程序员分配和释放&#xff0c;如果不释放&#xff0c;则出现内存泄露。程序会回收您的内存&#xff0c;特点&#xff1a;容量大…

②(常识篇)、《史上最全iOS八股文面试题》2022年,金三银四我为你准备了,iOS《1000条》笔试题以及面试题(包含答案)。带面试你过关斩将,(赶紧过来背iOS八股文)

iOS面试题 一共分为笔试题和面试题两部分 笔试题 一共分为10个 总共613题 面试题 一共400题 笔试题 一个10个系列 分别为 ①(语法篇) 共147题 已更新 ②(常识篇) 共72题 已更新 ③(界面篇) 共83题 已更新 ④(iOS篇) 共52题 已更新 ⑤(操作篇) 共68题 已更新 ⑥(数据结构篇) 共2…