C语言程序设计学习笔记
绪论
1. 什么是编程语言?
编程语言是人给计算机下达命令的工具,计算机会通过编程语言编写的代码一步一步地执行命令。
举个例子:你去星巴克,你告诉服务员要一杯咖啡,服务员就泡好一杯咖啡给你,因为服务员是一个活生生的人,所以你不需要详细地告诉他泡咖啡的每一步该如何操作。假如服务员是一个机器人,机器人内部会有一系列指令一步一步地命令机器人泡咖啡,机器人首先根据我们的要求选择面前的杯子型号,然后拿起杯子,然后转身,然后打开水龙头,然后接取咖啡,等等一系列的步骤。这些指挥机器人的一系列具体的指令都需要用编程语言去编写,无论看起来多么智能的机器人,它都不能真的理解人类的语言,机器人只是根据各种外界信号选择执行不同的一系列执行命令。
2. 什么是C语言?
C语言是编程语言的一种。
编程语言如同自然语言(汉语、英语、日语等)一样分为很多门类。在编程语言的几十年发展历程中,涌现了成百上千种编程语言,每种编程语言都有各自的特性。比如scratch是为了培养少年儿童的编程能力,Basic是为了编写类似英语的易读代码。C语言是专为程序员编写程序设计,是功能强大、使用广泛的的编程语言之一。
1972年,贝尔实验室的Dennis Ritch 和Ten Thompson在PDP 11计算机上为开发UNIX操作系统,在B语言的基础上设计了C语言。许多编程语言在主流编程语言中昙花一现,而C语言至今依然生机蓬勃,TIOBE是编程语言排行榜,C语言常年排在TIOBE前三(https://www.tiobe.com/tiobe-index/)
3. C语言的特性
- 高效:若采用相同的计算方法,C语言编写的程序与其他编程语言编写的程序相比,C语言所花费的时间通常更少。
- 可移植:C语言只需要做少量修改或不做修改就能在不同的操作系统或CPU上运行。
- 强大而灵活:别的编程语言能实现的功能C语言都能实现,而且C语言还能实现某些的编程语言实现不了的功能。
- 面向程序员:C语言专为程序员编写程序而生。
4. 开发环境
-
编写代码通常需要使用IDE(集成开发工具)。
-
初学者建议使用Dev-C++作为学习编写代码时使用的IDE(下载链接https://bloodshed-dev-c.en.softonic.com/)。
-
启动Dev-C++,点击“文件”->“新建”->“源代码”就能开始编写程序。
-
C语言是编译执行的语言,所以编写好的源代码要先保存为 (*.c) 文件、编译之后才能运行。
-
编译是指将编写的源代码翻译为计算机硬件能读懂执行的机器语言代码。
一、C程序基本结构
从Hello, World!启程
#include <stdio.h>//包含头文件
int main(){
printf("Hello, World!\n");//格式化输出
return 0;
}
1.1 头文件
stdio.h
- stdio.h 是一个头文件 (C语言标准输入输出头文件) ,头文件调用库函数,#include 用来引入头文件。
1.2 预处理指令
#include <stdio.h>
- #include <stdio.h>是一条预处理命令,它的作用是通知C语言编译系统在对C程序进行正式编译之前需做一些预处理工作。
1.3 主函数main()
int main(){
函数体;
}
- c语言程序由 函数 构成,函数就是实现代码逻辑的一个小的单元,函数主体由大括号括起来,也成为 块。
- 括号要成对写,如果需要删除的话也要成对删除;
- 函数体内的语句要有明显缩进,通常以按一下Tab键为一个缩进;
- 每个函数可以实现一个或多个功能,一个正规程序可以有多个函数,但 有且只有一个main函数,所有的 C 语言程序都需要包含 main() 函数,代码从 main() 函数开始执行。
- 函数只有在被调用的时候才执行,主函数main由系统调用执行。
1.4 printf()
printf("Hello, World!\n");
- printf() 用于格式化输出到屏幕,就是按照指定的格式在屏幕上显示指定的信息。
- printf() 函数在 “stdio.h” 头文件中声明,当编译器遇到 printf() 函数时,如果没有找到 stdio.h 头文件,会发生编译错误。
1.5 return
return;
- return返回一个函数运行结果的值,同时退出此函数,main前的int表示main() 函数的返回值为整型,根据函数类型的不同,返回的值也是不同的。
1.6 注释
//双斜杠后面的内容会被编译器忽略
/*
这中间的内容
会被编译器忽略
*/
程序中/* … */ 单行或多行注释,//用于单行注释,注释里的内容将会被编译器忽略,通常一个注释说明或一个可执行语句占一行
1.7 特别说明
- 当一句可执行语句结束的时候末尾需要有分号;
- 代码中所有符号均为 英文半角符号;
- 保存文件后缀为 .c,保存之后才能编译、运行;
- 因编译器的原因,生成的 .exe文件打开时会一闪而过,从而观察不到其运行的结果,这是因为 main() 函数结束时,DOS 窗口会自动关闭,为了避免这个问题可在 return 0; 前加入 system(“pause”); 语句。
二、变量、常量
2.1变量
2.1.1 整型变量
例
#include <stdio.h>
int main(){
int myAge = 18;
return 0;
}
分析
#include <stdio.h>
int main() {
int myAge = 18;
//myAge是一个变量名,int表示变量的数据类型是整型,=是赋值号
//int myAge = 18;是一个表达式,该表达式声明一个变量名为myAge的整型变量,同时赋值18
//变量也可以先声明再在另一行单独赋值
//空格分隔语句的各个部分,让编译器能识别语句中的某个元素(比如 int)在哪里结束,下一个元素在哪里开始。
return 0;
}
2.1.2 浮点型变量
#include <stdio.h>
int main() {
float width;//声明单精度浮点型变量width
double length;//声明双精度浮点型变量length
width = 2.3;//赋值
length = 22.3;
return 0;
}
2.1.3 字符型变量
#include <stdio.h>
int main() {
char a = 'a';//字符型变量赋值要用单引号
return 0;
}
2.1.4 标志符
- 编程时给变量、函数起的名字是标识符,就好比姓名就是每个人的标识符。
- C语言的标识符是不可以随便起名字的,必须遵守一定的规则。
- 一个标识符以字母 A-Z 或 a-z 或下划线 _ 开始,后跟零个或多个字母、下划线和数字(0-9)。
- C 标识符内不允许出现标点字符,比如 @、$ 和 %。
- 标识符的长度最好不要超过8位,因为在某些版本的C中规定标识符前8位有效,当两个标识符前8位相同时,则被认为是同一个标识符。
- 标识符是严格区分大小写的。例如Boy和boy是两个不同的标识符。
- 标识符最好选择有意义的英文单词组成做到"见名知意",不能使用中文。
- 标识符不能是C语言的关键字。
关键字 | 说明 |
---|---|
auto | 声明自动变量 |
break | 跳出当前循环 |
case | 开关语句分支 |
char | 声明字符型变量或函数返回值类型 |
const | 定义常量,如果一个变量被 const 修饰,那么它的值就不能再被改变 |
continue | 结束当前循环,开始下一轮循环 |
default | 开关语句中的"其它"分支 |
do | 循环语句的循环体 |
double | 声明双精度浮点型变量或函数返回值类型 |
else | 条件语句否定分支(与 if 连用) |
enum | 声明枚举类型 |
extern | 声明变量或函数是在其它文件或本文件的其他位置定义 |
float | 声明浮点型变量或函数返回值类型 |
for | 一种循环语句 |
goto | 无条件跳转语句 |
if | 条件语句 |
int | 声明整型变量或函数 |
long | 声明长整型变量或函数返回值类型 |
register | 声明寄存器变量 |
return | 子程序返回语句(可以带参数,也可不带参数) |
short | 声明短整型变量或函数 |
signed | 声明有符号类型变量或函数 |
sizeof | 计算数据类型或变量长度(即所占字节数) |
static | 声明静态变量 |
struct | 声明结构体类型 |
switch | 用于开关语句 |
typedef | 用以给数据类型取别名 |
unsigned | 声明无符号类型变量或函数 |
union | 声明共用体类型 |
void | 声明函数无返回值或无参数,声明无类型指针 |
volatile | 说明变量在程序执行中可被隐含地改变 |
while | 循环语句的循环条件 |
2.2 常量
- 常量是固定值,在程序执行期间不会改变。这些固定的值,又叫做 字面量 。
- 常量可以是任何的基本数据类型,比如整数常量、浮点常量、字符常量,或字符串字面值,也有枚举常量。
- 常量 就像是常规的变量,只不过常量的值在定义后不能进行修改。
2.2.1 整数常量
- 整数常量可以是十进制、八进制或十六进制的常量。
- 前缀指定基数:0x 或 0X 表示十六进制,0 表示八进制,不带前缀则默认表示十进制。
- 整数常量也可以带一个后缀,后缀是 U 和 L 的组合,U 表示无符号整数(unsigned),L 表示长整数(long)。后缀可以是大写,也可以是小写,U 和 L 的顺序任意。
86 | 十进制 |
---|---|
025 | 八进制 |
0x4b | 十六进制 |
30u | 无符号十进制 |
30l | 长整数 |
30ul | 无符号长整数 |
2.2.2 浮点常量
- 浮点常量由整数部分、小数点、小数部分和指数部分组成。
- 可以使用小数形式或者指数形式来表示浮点常量。
- 当使用小数形式表示时,必须包含整数部分、小数部分,或同时包含两者。如:3.14159
- 当使用指数形式表示时, 必须包含小数点、指数,或同时包含两者。带符号的指数是用 e 或 E 引入的。如:314159E-5
2.2.3 字符常量
- 字符常量是括在单引号中,例如,‘x’ 可以存储在 char 类型的简单变量中。
- 字符常量可以是一个普通的字符(例如 ‘x’)、一个转义序列(例如 ‘\t’),或一个通用的字符(例如 ‘\u02C0’)。
在 C 中,有一些特定的字符,当它们前面有反斜杠时,它们就具有特殊的含义,这些字符被称为转义字符。
转义字符 | 含义 |
---|---|
\ | \ 字符 |
’ | ’ 字符 |
" | " 字符 |
? | ? 字符 |
\a | 警报铃声 |
\b | 退格键 |
\f | 换页符 |
\n | 换行符 |
\r | 回车 |
\t | 水平制表符 |
\v | 垂直制表符 |
\ooo | 一到三位的八进制数 |
\xhh . . . | 一个或多个数字的十六进制数 |
例
#include <stdio.h>
int main() {
printf("Hello\tWorld\n\n");
return 0;
}
2.2.4 字符串常量
- 字符串字面值或常量是括在双引号 “” 中的。
- 一个字符串包含类似于字符常量的字符:普通的字符、转义序列和通用的字符。
- 可以使用空格做分隔符,把一个很长的字符串常量进行分行。
下面的实例显示了一些字符串常量。下面这三种形式所显示的字符串是相同的。
“dear” “hello, \ dear” "hello, " “d” “ear”
2.2.5 自定义常量
在 C 中,有两种简单的定义常量的方式:
- 使用 #define 预处理器。
- 使用 const 关键字。
2.2.5.1#define 预处理器(宏定义常量)
例
#include <stdio.h>
#define LENGTH 10
#define WIDTH 5
#define NEWLINE '\n'
int main(){
int area;
area = LENGTH * WIDTH;
printf("value of area : %d", area);
printf("%c", NEWLINE);
return 0;
}
2.2.5.2 const 关键字
使用 const 前缀声明指定类型的不可变变量)
#include <stdio.h>
int main(){
const int LENGTH = 10;
const int WIDTH = 5;
const char NEWLINE = '\n';
int area;
area = LENGTH * WIDTH;
printf("value of area : %d", area);
printf("%c", NEWLINE);
return 0;
}
2.2.5.3 #define 和 const 两者的区别
- #define 定义的是不带类型的常量,只进行简单的字符替换。在预编译的时候起作用,不存在类型检查。
- const 定义的是变量不是常量,只是这个变量的值不允许改变是常变量!带有类型。编译运行的时候起作用存在类型检查。
(1) 编译器处理方式不同
- #define 宏是在预处理阶段展开。
- const 常量是编译运行阶段使用。
(2) 类型和安全检查不同
- #define 宏没有类型,不做任何类型检查,仅仅是展开。
- const 常量有具体的类型,在编译阶段会执行类型检查。
(3) 存储方式不同
- #define宏仅仅是展开,有多少地方使用,就展开多少次,不会分配内存。(宏定义不分配内存,变量定义分配内存。)
- const常量会在内存中分配(可以是堆中也可以是栈中)。
(4) const 可以节省空间,避免不必要的内存分配。 例如:
#define NUM 3.14159 //常量宏
const doulbe Num = 3.14159; //此时并未将Pi放入ROM中 ......
double i = Num; //此时为Pi分配内存,以后不再分配!
double I= NUM; //编译期间进行宏替换,分配内存
double j = Num; //没有内存分配
double J = NUM; //再进行宏替换,又一次分配内存!
const 定义常量只是给出了对应的内存地址,而不是象 #define 一样给出的是立即数,所以,const 定义的常量在程序运行过程中只有一份拷贝(因为是全局的只读变量,存在静态区),而 #define 定义的常量在内存中有若干个拷贝。
(5) const提高了效率。 编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。
(6) define宏替换只作替换,不做计算,不做表达式求解;
宏预编译时就替换了,程序运行时,并不分配内存。
2.3 数据类型转换
2.3.1 自动转换
C 语言中如果一个表达式中含有不同类型的常量和变量,在计算时,会将它们自动转换为同一种类型。
int main(){
int a;
double b=5.5;
a=b;//浮点型b给整型a赋值,5.5自动省略小数取5赋给a
return 0;
}
自动转换规则:
- 浮点数赋给整型变量,该浮点数小数被舍去;
- 整数赋给浮点型变量,数值不变,但是被存储到相应的浮点型变量中;
2.3.2 强制转换
在 C 语言中也可以对数据类型进行强制转换;
强制类型转换形式: (类型说明符)(表达式)
例
int main(){
int a=5;
double b;
b=(int)a;//a的数据类型从整型强制转换为浮点型
return 0;
}
2.4 格式化输入、输出
C 语言中的 I/O (输入/输出) 通常使用 printf() 和 scanf() 两个函数。
2.4.1 格式化输出
格式化输出整数
#include <stdio.h>
int main(){
int a;
printf("请输入a的值:%d",&a);//格式化输出整数,整型占位符%d
return 0;
}
格式化输入浮点数
#include <stdio.h>
int main(){
float a;
double b;
printf("请输入a的值:%f",&a);//格式化输出单精度浮点数,单精度浮点数占位符%f
printf("请输入b的值:%lf",&b);//格式化输出双精度浮点数,双精度浮点数占位符%lf
return 0;
}
格式化输出字符
#include <stdio.h>
int main(){
char c;
printf("请输入c的值:%c",&c);//格式化输出字符,字符占位符%c
return 0;
}
格式化输出字符
#include <stdio.h>
int main() {
char c;
printf("这是字符串\n");//可以直接输出字符串,\n是换行
printf("%s", "这也是字符串");
/*
%s是字符串占位符,字符串要用双引号。
C语言不存在字符串变量
*/
return 0;
}
2.4.2 格式化输入
格式化输入整数
#include <stdio.h>
int main(){
int a;
scanf("请输入a的值:%d",&a);//格式化输入整数,整型占位符%d
return 0;
}
格式化输入浮点数
#include <stdio.h>
int main(){
float a;
double b;
scanf("请输入a的值:%f",&a);//格式化输入单精度浮点数,单精度浮点数占位符%f
scanf("请输入b的值:%lf",&b);//格式化输入双精度浮点数,双精度浮点数占位符%lf
return 0;
}
格式化输入字符
#include <stdio.h>
int main(){
char c;
scanf("请输入c的值:%c",&c);//格式化输入字符,字符占位符%c
return 0;
}
2.4.3 特别说明
#include <stdio.h>
int main () {
int a,b,i;
char c;
scanf("请输入a和b:%d%d", &a, &c);//连续输入两个非字符变量,用空格区分
printf("a=%d,b=%d",a,b);
scanf("%d%c", &i, &c);
//输入整数后直接输入想要输入的字符,不用空格区分,如果输入空格再输入想要输入的字符,c将被赋值空格
printf("%d%c", i, c);
return 0;
}
#include <stdio.h>
int main(){
int i;
char c;
scanf("请输入i的值:%d", &i);
getchar();//如果没有这一行,c将被赋值“回车”,而不能赋值想要赋给的值
scanf("请输入c的值:%c", &c);
return 0;
}
三、C 运算符
运算符是一种告诉编译器执行特定的数学或逻辑操作的符号。
3.1算术运算符
假设变量 A 的值为 10,变量 B 的值为 20
运算符 | 描述 | 实例 |
---|---|---|
+ | 把两个操作数相加 | A + B 将得到 30 |
- | 从第一个操作数中减去第二个操作数 | A - B 将得到 -10 |
* | 把两个操作数相乘 | A * B 将得到 200 |
/ | 分子除以分母 | B / A 将得到 2 |
% | 取模运算符,整除后的余数 | B % A 将得到 0 |
++ | 自增运算符,整数值增加 1 | A++ 将得到 11 |
– | 自减运算符,整数值减少 1 | A-- 将得到 9 |
例
#include <stdio.h>
int main(){
int a = 21;
int b = 10;
printf("a+b的值是 %d\n",a+b);
printf("a-b的值是 %d\n",a-b);
printf("a*b的值是 %d\n",a*b);
printf("a/b的值是 %d\n",a/b);
printf("a++的值是 %d\n",a++);
printf("a--的值是 %d\n",a--);
printf("++b的值是 %d\n",++b);
printf("--b的值是 %d\n",--b);
//a++,a--是先赋值在计算,++b,--b是先计算再赋值
}
3.2关系运算符
假设变量 A 的值为 10,变量 B 的值为 20
运算符 | 描述 | 实例 |
---|---|---|
== | 检查两个操作数的值是否相等,如果相等则条件为真。 | (A == B) 为假。 |
!= | 检查两个操作数的值是否相等,如果不相等则条件为真。 | (A != B) 为真。 |
> | 检查左操作数的值是否大于右操作数的值,如果是则条件为真。 | (A > B) 为假。 |
< | 检查左操作数的值是否小于右操作数的值,如果是则条件为真。 | (A < B) 为真。 |
>= | 检查左操作数的值是否大于或等于右操作数的值,如果是则条件为真。 | (A >= B) 为假。 |
<= | 检查左操作数的值是否小于或等于右操作数的值,如果是则条件为真。 | (A <= B) 为真。 |
例
#include <stdio.h>
int main(){
int x=5,y=5,z=10;
printf("x是否大于y:%d\n", x>y ); //x是否大于y
printf("y是否大于等于x:%d\n",y>=x); //y是否大于等于x
printf("y是否小于z:%d\n",y<z); //y是否小于z
printf("z是否小于等于x:%d\n", z<=x); //z是否小于等于x
printf("z是否等于x+y:%d\n",z==x+y); //z是否等于x+y
return 0;
}
关系表达式:指计算机程序中用关系运算符将两个表达式连接起来的式子。
3.3逻辑运算符
假设变量 A 的值为 1,变量 B 的值为 0
运算符 | 描述 | 实例 |
---|---|---|
&& | 称为逻辑与运算符。如果两个操作数都非零,则条件为真。 | (A && B) 为假。 |
|| | 称为逻辑或运算符。如果两个操作数中有任意一个非零,则条件为真。 | (A || B) 为真。 |
! | 称为逻辑非运算符。用来逆转操作数的逻辑状态。如果条件为真则逻辑非运算符将使其为假。 | !(A && B) 为真。 |
例
#include <stdio.h>
int main(){
int height = 175; //身高为175cm
int money = 1500000; //银行存款为150万
printf("是否符合条件:%d\n", height>=180&&money>=1000000); //填写算式
return 0;
}
逻辑表达式:用逻辑运算符将关系表达式或逻辑量连接起来的有意义的式子,逻辑表达式的值是一个逻辑值,即“true”或“false”。
3.4位运算符
位运算符作用于位,并逐位执行操作。
运算符 | 名称 | 计算结果描述 |
---|---|---|
& | 按位与运算符 | p & q按二进制位进行"与"运算,如果p、q某二进制位全都为真,那么该位计算结果为真 |
| | 按位或运算符 | p | q按二进制位进行"或"运算,如果p、q某二进制位其中一个为真,那么该位计算结果为真 |
^ | 按位异或运算符 | p ^ q按二进制位进行"异或"运算,如果p、q某位真假不同,那么该位计算结果为真 |
~ | 按位取反运算符 | ~q按二进制位进行"取反"运算。一个有符号二进制数的补码形式。 |
<< | 二进制左移运算符 | 按二进制位进行"取反"运算。将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补0)。 |
>> | 二进制右移运算符 | 将一个数的各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃。 |
&、 | 和 ^ 的真值表如下
p | q | p & q | p | q | p ^ q |
---|---|---|---|---|
0 | 0 | 0 | 0 | 0 |
0 | 1 | 0 | 1 | 1 |
1 | 1 | 1 | 1 | 0 |
1 | 0 | 0 | 1 | 1 |
假设如果 A = 60,且 B = 13,现在以二进制格式表示:A = 0011 1100,B = 0000 1101
位运算 | 二进制位运算结果 | 转换为十进制 |
---|---|---|
A&B | 0000 1100 | 12 |
A|B | 0011 1101 | 61 |
A^B | 0011 0001 | 49 |
~A | 1100 0011 | -61 |
A << 2 | 1111 0000 | 240 |
A >> 2 | 0000 1111 | 15 |
例
#include <stdio.h>
int main() {
int a = 60; /* 60 = 0011 1100 */
int b = 13; /* 13 = 0000 1101 */
printf("a & b的值是 %d\n", a & b );/* 12 = 0000 1100 */
printf("a | b的值是 %d\n", a | b );/* 61 = 0011 1101 */
printf("a ^ b的值是 %d\n", a ^ b );/* 49 = 0011 0001 */
printf("~a的值是 %d\n", ~a );/*-61 = 1100 0011 */
printf("a << 2的值是 %d\n", a << 2 );/* 240 = 1111 0000 */
printf("a >> 2的值是 %d\n", a >> 2 );/* 15 = 0000 1111 */
}
3.5赋值运算符
运算符 | 描述 | 实例 |
---|---|---|
= | 简单的赋值运算符,把右边操作数的值赋给左边操作数 | C = A + B 将把 A + B 的值赋给 C |
+= | 加且赋值运算符,把右边操作数加上左边操作数的结果赋值给左边操作数 | C += A 相当于 C = C + A |
-= | 减且赋值运算符,把左边操作数减去右边操作数的结果赋值给左边操作数 | C -= A 相当于 C = C - A |
*= | 乘且赋值运算符,把右边操作数乘以左边操作数的结果赋值给左边操作数 | C *= A 相当于 C = C * A |
/= | 除且赋值运算符,把左边操作数除以右边操作数的结果赋值给左边操作数 | C /= A 相当于 C = C / A |
%= | 求模且赋值运算符,求两个操作数的模赋值给左边操作数 | C %= A 相当于 C = C % A |
<<= | 左移且赋值运算符 | C <<= 2 等同于 C = C << 2 |
>>= | 右移且赋值运算符 | C >>= 2 等同于 C = C >> 2 |
&= | 按位与且赋值运算符 | C &= 2 等同于 C = C & 2 |
^= | 按位异或且赋值运算符 | C ^= 2 等同于 C = C ^ 2 |
|= | 按位或且赋值运算符 | C |= 2 等同于 C = C | 2 |
例
#include <stdio.h>
int main() {
int a = 21;
int c ;
printf("c = a,c 的值 = %d\n", c = a );
printf("c += a,c 的值 = %d\n", c += a );
printf("c -= a,c 的值 = %d\n", c -= a);
printf("c *= a,c 的值 = %d\n", c *= a );
printf("c /= a,c 的值 = %d\n", c /= a );
c = 200;
printf("c %%= a,c 的值 = %d\n", c %= a );
printf("c <<= 2,c 的值 = %d\n", c <<= 2 );
printf("c >>= 2,c 的值 = %d\n", c >>= 2 );
printf("c >>= 2,c 的值 = %d\n", c >>= 2 );
printf("c ^= 2,c 的值 = %d\n", c ^= 2 );
printf("c |= 2,c 的值 = %d\n", c |= 2 );
}
3.6杂项运算符
运算符 | 描述 | 实例 |
---|---|---|
sizeof() | 返回变量的大小。 | sizeof(a) 将返回 4,其中 a 是整数。 |
& | 返回变量的地址。 | &a将给出变量的实际地址。 |
* | 指向一个变量。 | *a将指向一个变量。 |
? : | 条件表达式,三目运算符 | 如果条件为真 ? 则值为 X : 否则值为 Y |
例(三目元算符)
#include <stdio.h>
int main(){
double money =23.33;//定义兜里的钱
double cost =66.66;//定义打车回家的费用
printf("能不能打车回家呢:");
printf("%c/n",cost<=money?'y':'n');
//输出y就打车回家了,输出n就不能打车回家
return 0;
}
3.7运算符优先级
-
较高优先级的运算符会优先被计算
优先级 运算符 1 () 2 ! + (正号) - (负号) ++ – 3 * / % 4 +(加) -(减) 5 < <= >= > 6 == != 7 && 8 || 9 ?: 10 = += -= *= /= %=
四、C程序结构语句
4.1分支
4.1.1if语句
if(条件表达式){
执行代码块;
}
例
#include <stdio.h>
int main() {
int money = 999, price = 648;
if (money >= price) {
//如果条件为真执行代码块里面的内容
printf("恭喜小明氪金大礼包\n");
printf("欢迎下次再来!");
}//如果if接单行语句可以不要大括号
getchar();
return 0;
}
4.1.2if-else语句
if (条件表达式) {
执行代码块1;
} else {
执行代码块2;
}
例
#include <stdio.h>
int main() {
int year = 2004; //今年是2014年
if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) {
printf("今年是润年");
} else {
printf("今年是平年" );
}
return 0;
}
4.1.3多重if-else语句
if(条件表达式1) {
执行代码块1;
}else if(条件表达式2){
执行代码块2;
}else if(条件表达式3){
执行代码块3;
}
...
else{
执行代码块n;
}
return 0;
}
#include <stdio.h>
int main() {
int score = 6666;
if(score>=10000){
printf("%s\n","钻石玩家");
}else if(score>=5000){
printf("%s\n","白金玩家");
}else if(score>=1000){
printf("%s\n","青铜玩家");
}else{
printf("%s\n","黑铁玩家");
}
return 0;
}
4.1.4嵌套if-else语句
if(条件表达式1){
if(条件表达式2){
执行代码块1;
}else{
执行代码块2;
}
}else{
执行代码块3;
}
#include <stdio.h>
int main() {
int score = 80;
if (score > 60) {
printf("真不错,");
if (score > 90) {
printf("但是不要骄傲");
} else {
printf("期望你再接再厉");
}
} else {
printf("很遗憾,期望你再接再厉");
}
return 0;
}
4.1.5switch-case语句
switch(表达式){
case 常量表达式:执行代码块1;break;
case 常量表达式:执行代码块2;break;
...
case 常量表达式:执行代码块n;break;
default:执行代码块n+1;break;
}
例
#include <stdio.h>
int main() {
int score = 98;
score/=10;
switch(score){
case 10:printf("等级S");break;
case 9:printf("等级A");break;
case 8:
case 7:printf("等级B");break;//score70-89等级都是B
case 6: printf("等级C");break;
default:printf("等级D");break;
}
return 0;
}
注意:
1、在case后的 各常量表达式的值不能相同 ,否则会出现错误。
2、在case子句后如果没有break;会一直往后执行一直到遇到break;才会跳出switch语句。
3、switch后面的表达式语句只能是 整型或者字符类型 。
4、在case后, 允许有多个语句 ,可以不用{}括起来。
5、各case和default子句的先后顺序可以变动,而不会影响程序执行结果。
6、default子句可以省略不用。
4.1.6 switch与if语句综合应用
计算2008年8月8日这一天,是该年中的第几天。
程序分析:以5月1日为例,应该先把前4个月的天数加起来,然后再加上1天即本年的第几天。
特殊情况:在计算闰年的时候,2月是29天。
#include <stdio.h>
int main() {
int year = 2008,month = 8,day = 8,i,j;
if(year%400==0||year%4==0&&year%100!=0)i=1;
else i=0;
switch(month){
case 1:j=day;break;
case 2:j=31+ day;break;
case 3:j=31+ 28+ day;break;
case 4:j=31+ 28+ 31+ day;break;
case 5:j=31+ 28+ 31+ 30+ day;break;
case 6:j=31+ 28+ 31+ 30+ 31+ day;break;
case 7:j=31+ 28+ 31+ 30+ 31+ 30+ day;break;
case 8:j=31+ 28+ 31+ 30+ 31+ 30+ 31+day;break;
case 9:j=31+ 28+ 31+ 30+ 31+ 30+ 31+ 31+ day;break;
case 10:j=31+ 28+ 31+ 30+ 31+ 30+ 31+ 31+ 30+day;break;
case 11:j=31+ 28+ 31+ 30+ 31+ 30+ 31+ 31+ 30+ 31+day;break;
case 12:j=31+ 28+ 31+ 30+ 31+ 30+ 31+ 31+ 30+ 31+ 30+ day;break;
}
if(i==1&&month>2)j+=1;
printf("%d年%d月%d日是该年的第%d天",year,month,day,j);
return 0;
}
4.1.7 goto语句
-
goto语句是一种无条件分支语句,goto 语句的使用格式为:
goto 语句标号;//语句标号是一个标识符,该标识符一般用英文大写并遵守标识符命名规则。 语句标号:执行代码; //这个标识符加上一个冒号“:”一起出现在函数内某处,执行goto语句后,程序将跳转到该标号处并执行其后的语句。
-
goto语句通常不用,主要因为它将使程序层次不清,且不易读,但在特定情况下,可以使用goto语句来提高程序的执行速度。
例 使用goto语句,实现当输出完3之后跳出循环体。
#include <stdio.h>
int main(){
int sum = 0;
int i;
for(i=1; i<=10; i++){
printf("%d\n", i);
if(i==3)goto LOOP;
}
LOOP:printf("结束for循环了....");
return 0;
}
4.2循环
4.2.1while循环
while语句的语义是:计算表达式的值,当值为真(非0)时, 执行循环体代码块。
while(条件表达式){
执行代码块;
}
例
#include <stdio.h>
int main(){
int i=1,sum=0;
while(i<=10){
sum=sum+i;
i++;//改变循环变量的值
}
printf("10以内所有整数之和为:%d\n", sum);
return 0;
}
4.2.2do while循环
do-while循环语句的语义是:它先执行循环中的执行代码块,然后再判断while中表达式是否为真,如果为真则继续循环;如果为假,则终止循环。因此, do-while循环至少要执行一次循环语句 。
do{
执行代码块;
}while(条件表达式);//注意这里有分号
例
#include <stdio.h>
int main() {
int salary = 2000;
int year = 2022;
do {
year++;
salary *= 1.1;
} while (salary < 8000);
printf("到%d年薪水突破8000元\n", year);
return 0;
}
4.2.3for循环
for(表达式1;表达式2;表达式3){
执行代码块;
}
第一步:执行表达式1,对循环变量做初始化;
第二步:判断表达式2,若其值为真(非0),则执行for循环体中执行代码块,然后向下执行;若其值为假(0),则结束循环;
第三步:执行表达式3;
第四步:执行for循环中执行代码块后执行第二步;
第五步:循环结束,程序继续向下执行。
在for循环中,表达式1 是一个或多个 赋值语句 ,它用来 控制变量的初始值 ; 表达式2 是一个 关系表达式 ,它决定什么时候退出循环; 表达式3 是循环变量的 步进值 ,定义控制循环变量每循环一次后按什么方式变化。这 三部分之间用分号(;)分开 。
使用for语句应该 注意 :
1、for循环中的“表达式1、2、3”均可缺省,但 分号(;)不能缺省 。
2、省略“表达式1(循环变量赋初值)”, 表示不对循环变量赋初始值 。如:
int i=1;
for(;i<=10;i++){
printf("加班第%d天",i);
}
3、省略“表达式2(循环条件)”,不做其它处理, 循环一直执行(死循环) 。
int i=1;
for(;;i++){
printf("加班第%d天",i);
}
4、省略“表达式3(循环变量增量)”,不做其他处理, 循环一直执行(死循环) 。
int i=1;
for(;i<=10;){
printf("加班第%d天",i);
}
注:死循环可以使用break解决
5、表达式1可以是设置循环变量的初值的赋值表达式,也可以是其他表达式。
int i=1,addsalary,salary=1200;
for(addsalary=200;i<=10;i++){
salary+=addsalary;
printf("加班第%d天,salary=%d",i,salary);
}
6、表达式1和表达式3可以是一个简单表达式 也可以是多个表达式以逗号分割 。
int i,j;
for(i=1,j=0;j<100;i++,j++){
printf("加班第%d天,猝死几率=%d%%",i,j);
}
7、表达式2不论表达式是什么形式,只要其 值非零 ,就执行循环体。
4.2.4多重循环
例 打印9*9乘法表
#include <stdio.h>
int main() {
int i, j, result;
for (i = 9; i >= 1; i--) {
for (j = 1; j <= i; j++) {
result = i * j;
printf("%d*%d=%d\t", i, j, result);
}
printf("\n");
}
return 0;
}
例 打印三角形
#include <stdio.h>
int main() {
int i, j, k;
for (i = 1; i < 5; i++) {
for (j = 1; j <= 5 - i; j++)
printf(" ");
for (k = 1; k <= 2 * i - 1; k++)
printf("*");
printf("\n");//每次循环换行
}
return 0;
}
4.3结束循环
4.3.1break语句
break跳出当前循环层
例 找出0-50之间的所有素数,所谓素数就是只能被1和它本身整除的数字,比如:7,13,23等。
#include <stdio.h>
int main() {
int m, n;
for (m = 2; m <= 50; m++) {
for (n = 2; n < m; n++)if (m % n == 0)break;//发现不是素数跳出循环
if (m == n)printf("%d ", m); //n循环结束后,如果m=n的话就是素数
}
return 0;
}
注:在多层循环中,一个break语句跳出当前循环层之后会继续外层循环。
4.3.1continue语句
continue跳过当前循环层的本轮次,继续执行当前循环的下一轮次
例 计算1到20之间不能被3整除的数字之和。
#include <stdio.h>
int main(){
int i, sum;
for(i=1, sum=0; i<=20; i++){
if(i%3==0)continue;
sum += i;
}
printf("sum=%d\n", sum);
return 0;
}
五、函数
5.1自定义函数
- C语言提供了大量的库函数,比如stdio.h提供输出函数。
- 但是还是满足不了我们开发中的一些应用,所以这个时候需要自己定义函数,自定义函数的一般形式:
[数据类型说明]函数名称([参数]){
执行代码块;
return(表达式);
}
注意:
- 函数名称遵循标识符命名规范;
- 如果无函数声明,则自定义函数放在main函数之前,如果自定义函数要放在main函数后面,需要在main函数之前先声明自定义函数,(函数声明 见6.3)。
- [数据类型说明]和[参数]可以省略,数据类型说明省略,默认是int类型函数;参数省略表示该函数是无参函数,参数不省略表示该函数是有参函数,(函数参数 见6.4);
例 自定义两个函数
#include <stdio.h>
int sayLove(){
printf("I Love you\n");
return 0;
}
int dividLine(){
printf("*************\n");
return 0;
}
5.2函数调用
我们需要用到自定义的函数的时候,就得调用它,那么在调用的时候就称之为 函数调用。
在C语言中,函数调用的一般形式为:
函数名(参数);
例
#include <stdio.h>
int sayLove() {
printf("I Love you\n");
return 0;
}
int dividLine() {
printf("*************\n");
return 0;
}
int main() {
dividLine();// 调用所写函数
sayLove();
dividLine();
return 0;
}
注意:
- 对无参数函数调用的时候可以将参数省略,但函数名后的()不能省略。
5.3函数声明
- C语言按照从上至下的顺序编译代码,可以通过 函数声明 告诉编译器函数名称及如何调用函数,将函数的实际主体单独定义。
- 函数声明格式: **[数据类型说明]函数名称([参数]); **
例
#include <stdio.h>
int sayLove();//函数声明
int dividLine() ;
int main() {
dividLine();// 调用所写函数
sayLove();
dividLine();
return 0;
}
int sayLove() {
printf(" I Love you\n");
return 0;
}
int dividLine() {
printf("*************\n");
return 0;
}
5.4函数参数
如果函数要使用参数,则必须在定义函数时声明 参数数据类型 和接受参数值的 变量名(通常也别成为“形式参数”)。
#include <stdio.h>
int test1(){
//函数不使用参数
printf("学C语言真快乐\n");
}
int test2(int n){
//函数使用int类型参数,接受参数值的变量是n,此变量仅在此函数内部有效
printf("已经学了%d天C语言",n);
}
int main(){
test1();
test2(66);
//被调用时test2的内部变量n赋值66,如果()里填入的不是常数,而是变量或表达式,也是将相应的值传递给自定义函数的参数,main中的变量本身是不能传进去的。
return 0;
}
5.5函数返回值
-
函数的值只能通过return语句返回主调函数。return语句的一般形式为: **return 表达式 ** 或者为: **return (表达式); **
-
函数值的类型和函数定义中函数的类型应保持一致。如果两者不一致,则 以函数返回类型为准 ,自动进行类型转换。
例
返回一个字符型数据
char option(){
return 'A';
}
返回一个整型数据
int number(){
return 100;
}
没有返回值的函数,返回类型为void。
void noresult{
}
注意:void函数中可以有执行代码块,但是不能有返回值,另void函数中如果有return语句,该语句只能起到结束函数运行的功能。其格式为:return;
5.6递归函数
递归就是一个函数在它的函数体内调用它自身。执行递归函数将反复调用其自身,每调用一次就进入新的一层。
例
#include <stdio.h>
int savemoney(int n) {
if (n == 1)return 1;
else return savemoney(n - 1) + n ;
}
int main() {
int num = savemoney(5);
printf("5天存了%d元。\n", num);
return 0;
}
此例中savemoney(1)返回值为1,否则不断调用自身。由此可以看出 递归函数必须有结束条件。
递归函数特点:
- 每一级函数调用时都有自己的变量;
- 每次调用都会有一次返回;
- 递归函数中必须有终止语句。
5.7作用域规则
C语言中的变量,按作用域范围可分为两种,即局部变量和全局变量。
-
在函数或块内部定义的变量为 局部变量,局部变量也称为内部变量、本地变量、自动变量,其 作用域仅限于定义位置所处的大括号(块)内。
-
在所有函数外部定义的变量为 全局变量,它不属于哪一个函数,它属于一个源程序文件。其 作用域是整个源程序。
-
在程序中,局部变量和全局变量的名称可以相同,但是在函数内,如果两个名字相同,会使用局部变量值,全局变量不会被使用。
#include <stdio.h>
int x = 77;//定义全局变量x
void overall() {
printf("全局变量, x=%d\n", x);
}
int main() {
int x = 10;//定义main内的局部变量x
if (x > 0) {
int x = 50;//定义if函数内的局部变量x
x /= 2;
printf("if语句内, x=%d\n", x);
}
printf("main方法内, x=%d\n", x);
overall();
return 0;
}
六、数据类型
数据类型指的是用于声明不同类型的变量或不同类型的函数(函数的类型指的是函数返回值的类型)的一个广泛的系统。
数据类型决定了数据存储占用的空间,以及如何解释存储的二进制位模式。
C 中的数据类型可分为以下几种:
- 基本类型:它们是算术类型,包括两种类型:整数类型和浮点类型。
- 枚举类型:它们也是算术类型,被用来定义在程序中只能赋予其一定的离散整数值的变量。
- void 类型:类型说明符 void 表明没有可用的值。
- 派生类型:指针类型、数组类型、结构类型、共用体类型和函数类型,数组类型和结构类型统称为聚合类型。
6.1 整数类型
整型数据是指不带小数的数字。
类型 | 存储大小 | 值范围 |
---|---|---|
char | 1 字节 | -128 到 127 或 0 到 255 |
unsigned char | 1 字节 | 0 到 255 |
signed char | 1 字节 | -128 到 127 |
int | 2 或 4 字节 | -32,768 到 32,767 或 -2,147,483,648 到 2,147,483,647 |
unsigned int | 2 或 4 字节 | 0 到 65,535 或 0 到 4,294,967,295 |
short | 2 字节 | -32,768 到 32,767 |
unsigned short | 2 字节 | 0 到 65,535 |
long | 4 字节 | -2,147,483,648 到 2,147,483,647 |
unsigned long | 4 字节 | 0 到 4,294,967,295 |
- char是字符类型,存储方式与整型相同,本质上是整型。
- int、short int、long int是根据编译环境的不同,所取范围不同。
- 其中short int和long int至少是表中所写范围,但是int在表中是以16位编译环境写的取值范围。
- c语言int的取值范围在于他占用的字节数 ,不同的编译器,规定是不一样。
- ANSI标准定义int是占2个字节。
- 在VC里,一个int是占4个字节的。
为了得到某个类型或某个变量在特定平台上的准确大小,您可以使用 sizeof 运算符。
例
#include <stdio.h>
int main() {
printf("int 存储大小 : %d \n", sizeof(int));
return 0;
}
分析
#include <stdio.h>
int main() {
printf("int 存储大小 : %d \n", sizeof(int));
//%d是占位符的其中一种,用来将逗号后面的计算结果按照十进制整型输出
//表达式sizeof(type)得到对象或类型的存储字节大小。
return 0;
}
整型输出
#include <stdio.h>
int main() {
printf("十进制输出:%d\n", 10);
//%d和%i占位符,格式化输出十进制整数
printf("八进制输出:%#o\n", 10);
//%o占位符,格式化输出八进制整数
printf("十六进制输出:%#x\n", 10);
//%x占位符,格式化输出十六进制整数
return 0;
}
6.2 浮点类型
浮点数据是指带小数的数字。
类型 | 存储大小 | 值范围 | 精度 |
---|---|---|---|
float | 4 字节 | 1.2E-38 到 3.4E+38 | 6 位有效位 |
double | 8 字节 | 2.3E-308 到 1.7E+308 | 15 位有效位 |
long double | 16 字节 | 3.4E-4932 到 1.1E+4932 | 19 位有效位 |
float:单精度浮点值。二进制计数,1位符号,8位指数,23位小数。
0 | 01010010 | 00011000110001100011011 |
---|---|---|
符号 | 指数 | 小数 |
double:双精度浮点值。二进制计数,1位符号,11位指数,52位小数。
0 | 01010010111 | 0001100011000110001101100011000110001100011011011011 |
---|---|---|
符号 | 指数 | 小数 |
例
#include <stdio.h>
#include <float.h>
//FLT_MIN、FLT_MAX、FLT_DIG的值都在float.h中定义
int main(){
printf("float 存储最大字节数 : %lu \n", sizeof(float));
printf("float 最小值: %E\n", FLT_MIN );
printf("float 最大值: %E\n", FLT_MAX );
printf("精度值: %d\n", FLT_DIG );
return 0;
}
程序运行结果
float 存储最大字节数 : 4
float 最小值: 1.175494E-38
float 最大值: 3.402823E+38
精度值: 6
6.3 void 类型
- 函数返回为空:C 中有各种函数都不返回值,或者您可以说它们返回空。不返回值的函数的返回类型为空。例如void exit (int status);
- 函数参数为空:C 中有各种函数不接受任何参数。不带参数的函数可以接受一个 void。例如 int rand(void);
- 指针指向 void:类型为 void * 的指针代表对象的地址,而不是类型。例如,内存分配函数 void *malloc( size_t size ); 返回指向 void 的指针,可以转换为任何数据类型。
七、数组
7.1 数组基本概念
- 数组:一块连续的,大小固定并且里面的元素数据类型一致的内存空间
- 数组下标从0开始
第1个元素 | 第2个元素 | 第3个元素 | 第4个元素 | ··· | 第n个元素 |
---|---|---|---|---|---|
arr[0] | arr[1] | arr[2] | arr[3] | ··· | arr[n-1] |
7.2申明数组
- 声明一个数组:数据类型 数组名称[长度],数据类型可以是任意有效的 C 数据类型,长度必须大于0;
int main(){
int arr[5];//数组里面元素的类型是int,最多存储5个元素
return 0;
}
- C语言的数组长度一经声明,长度就是固定,无法改变,并且C语言并不提供计算数组长度的方法。
- 在声明数组后没有进行初始化的时候,静态(static)和外部(extern)类型的数组元素初始化元素为0,自动(auto)类型的数组的元素初始化值不确定。(存储类别 见第十章)
7.3初始化数组
数组在初始化的时候,数组内元素的个数不能大于声明的数组长度;
C语言中的数组初始化是有三种形式的,分别是:
- 数据类型 数组名称[长度n] = {元素1,元素2…元素n};
#include <stdio.h>
int main(){
int arr[3] ={
1,2,3}//如果元素个数小于数组的长度时,多余的数组元素初始化为0;
printf("%d %d %d\n",arr[0],arr[1],arr[2]);//数组的下标均以0开始;
return 0;
}
- 数据类型 数组名称[] = {元素1,元素2…元素n};
#include <stdio.h>
int main(){
int arr[] ={
1,2,3}
printf("%d %d %d\n",arr[0],arr[1],arr[2]);
return 0;
}
- 数据类型 数组名称[长度n]; 数组名称[0] = 元素1; 数组名称[1] = 元素2; 数组名称[n-1] = 元素n;
#include <stdio.h>
int main(){
int arr[3];
arr[0]=1;
arr[1]=2;
arr[2]=3;
printf("%d %d %d\n",arr[0],arr[1],arr[2]);
return 0;
}
7.4访问数组元素
#include <stdio.h>
int main(){
int arr[] = {
0, 1, 2, 3, 4,};
int i;
for(i=0;i<5;i++)printf("%d\n",arr[i]);
/*
经常采用循环遍历的方式访问数组
注意数组最大下标是数组长度减1,遍历时不要越界
*/
return 0;
}
7.5 多维数组
多维数组的申明格式是:数据类型 数组名称 [常量表达式1] [常量表达式2]…[常量表达式n];
多维数组的申明与初始化
int main() {
//第一种方式初始化方式声明并初始化三维数组arr1
int arr1[2][2] = {
10, 20, 30, 40,50,60};//等价{
{10, 20}, {30, 40},{50,60}}
//第二种方式初始化方式声明并初始化二维数组arr2
int arr2[2][2];
arr2[0][0] = 10;
arr2[0][1] = 20;
arr2[1][0] = 30;
arr2[1][1] = 40;
return 0;
}
多维数组可以看做一个矩阵
第0列 | 第1列 | 第2列 | |
---|---|---|---|
第0行 | arr[0] [0] | arr[0] [1] | arr[0] [2] |
第1行 | arr[1] [0] | arr[1] [1] | arr[1] [2] |
第2行 | arr[2] [0] | arr[2] [1] | arr[2] [2] |
7.6访问多维数组
#include <stdio.h>
int main(){
int arr[3][3] = {
{
1,2,3},{
4,5,6},{
7,8,9}};
int i,j;
int sum=0;
for(i=0;i<3;i++){
for(j=0;j<3;j++){
if(i=j||i+j=2)
sum=sum+arr[i][j];
}
}
printf("对角线元素之和是:%d\n",sum);
return 0;
}
7.7数组作为函数参数
#include <stdio.h>
void printarr1(int arr[], int size) {
for (int i = 0; i < size; i++)printf("%d ", arr[i]);
printf("\n");
}
void printarr2(int arr[5]) {
for (int i = 0; i < 5; i++)printf("%d ", arr[i]);
printf("\n");
}
int main() {
int a[10] = {
1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
printarr1(a, 10);//调用第一个函数
printarr2(a);//调用第二个函数
//就函数而言,传入的数组的长度是无关紧要的,因为 C 不会对传入的数组参数执行边界检查。
return 0;
}
八、指针
8.1 指针基本概念
-
内存地址的单位是字节。
-
每一个变量都有一个内存位置,每一个内存位置都定义了可使用 & 运算符访问变量的地址,它表示了在内存中的一个地址。
-
指针也就是内存地址,指针变量是用来存放内存地址的变量。就像其他变量或常量一样,必须在使用指针存储其他变量地址之前,对其进行声明。
-
指针的一般形式为 *type var-name;
-
type 是指针指向的内存空间里存储的数据的数据类型,它必须是一个有效的 C 数据类型,var-name 是指针变量的名称。
例
#include <stdio.h>
int main () {
int *p;
// 定义指针变量,p是一个指针,可以存储一个内存地址,*是一个作用在p的运算符,指向地址p里的值,int表示*p的值是一个整型数据
return 0;
}
8.2 指针常用操作
#include <stdio.h>
int main (){
int i = 20;
int *p;//指针变量p的声明
p = &i;//在指针变量中存储 i 的地址
//前两行等价于 int *p=&i;
printf("i 变量的地址: %p\n",&i);
printf("p 变量存储的地址: %p\n",p);//在指针变量中存储的地址
printf("*p 变量的值: %d\n", *p );//使用指针访问值
return 0;
}
8.3 NULL 指针
- 在变量声明的时候,如果没有确切的地址可以赋值, 可以为指针赋NULL 值。
- 赋为 NULL 值的指针被称为空指针。
- NULL 指针是一个定义在标准库中的值为零的常量。
#include <stdio.h>
int main (){
int *p = NULL;
printf("p 的地址是 %p\n", p);
return 0;
}
- 在大多数的操作系统上,程序不允许访问地址为 0 的内存,因为该内存是操作系统保留的。
- 指针指向内存地址0表明指针不指向一个可访问的内存位置。
- 按照惯例,如果指针包含空值(零值),则假定它不指向任何东西。
8.4 指针的算术运算
- C 指针是一个用数值表示的地址。因此,可以对指针执行算术运算。
- p++运算会在不影响内存中实际值的情况下,移动指针到下一个变量的内存位置,即当前位置按位往后移一个对应变量所占的内存大小。
- 指针的每一次递增,它其实会指向下一个元素的存储单元。
- 指针的每一次递减,它都会指向前一个元素的存储单元。
- 指针在递增和递减时跳跃的字节数取决于指针所指向变量数据类型长度,比如 int 就是 4 个字节。
#include <stdio.h>
int main () {
int *p;
printf("p=%p\n", p);
p++;
printf("p=%p\n", p);
p--;
printf("p=%p\n", p);
return 0;
}
- 指针可以用关系运算符进行比较,如 ==、< 和 >。
- 如果 p1 和 p2 指向两个相关的变量,比如同一个数组中的不同元素,则可对 p1 和 p2 进行大小比较。
#include <stdio.h>
int MAX = 3;
int main () {
int a[] = {
10, 100, 200};
int i, *p;
p = a;//数组中第一个元素的地址赋值给指针
i = 0;
while ( p <= &a[MAX - 1] ) {
//如果指针小于等于数组最后一个元素的地址
printf("a[%d]存储地址:%p\n", i, p );
printf("a[%d]存储值 :%d\n", i, *p );
p++;//指向下一个位置
i++;
}
return 0;
}
8.5 指针数组
-
指针数组:元素都是指针的数组
-
数组指针:指向数组的指针,注意与“数组指针”区别
#include <stdio.h>
int main () {
int a[3] = {
10, 100, 200};
int i, *p[3];//p[3]是指针数组
for ( i = 0; i < 3; i++)
p[i] = &a[i]; //将a[i]的地址赋给p[i]
for ( i = 0; i < 3; i++)
printf("Value of a[%d] = %d\n", i, *p[i] );
return 0;
}
8.6 指向指针的指针
指向指针的指针是一种多级间接寻址的形式,或者说是一个指针链。通常,一个指针包含一个变量的地址。当我们定义一个指向指针的指针时,第一个指针包含了第二个指针的地址,第二个指针指向包含实际值的位置。
int **p;//p是一个地址,*p也是一个地址,**p是*p地址存储的int变量的值
#include <stdio.h>
int main () {
int i, *P1, **P2;
i = 100;
P1 = &i;//获取 i 的地址
P2 = &P1;//获取 P1 的地址
printf("i = %d\n", i );
printf("P1 = %p\n", P1 );
printf("*P1 = %d\n", *P1 );
printf("P2 = %p\n", P2 );
printf("**P2 = %d\n", **P2);
return 0;
}
8.7 传递指针给函数
#include <stdio.h>
void printarr(int *arr, int size) {
for (int i = 0; i < size; i++)printf("%d ", arr[i]);
printf("\n");
}
int main() {
int a[10] = {
1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
printarr(a, 10);//数组名就是数组第一个元素的地址
return 0;
}
8.8 从函数返回指针
注意:用指针作为函数返回值时需要注意的一点是,函数运行结束后会销毁在它内部定义的所有局部数据,即C 语言不支持在调用函数时返回局部变量的地址,返回函数内定义变量地址需要将变量定义为 static变量(见10.6)。
#include <stdio.h>
int *func() {
static int n = 100;
return &n;
}
int main() {
printf("value = %d\n", *func());
return 0;
}
九、字符、字符串、字符串函数
9.1 单字符输入输出
**scanf()和printf()**输入、输出字符使用c%作为占位符;
int getchar(void) 函数从屏幕读取下一个可用的字符,并把它返回为一个整数。这个函数在同一个时间内只会读取一个单一的字符。您可以在循环内使用这个方法,以便从屏幕上读取多个字符。
int putchar(int c) 函数把字符输出到屏幕上,并返回相同的字符。这个函数在同一个时间内只会输出一个单一的字符。您可以在循环内使用这个方法,以便在屏幕上输出多个字符。
#include <stdio.h>
int main() {
char a, b;
printf("请输入第一个字符:");
scanf("%c", &a);
printf("您输入的第一个字符是:%c\n", a);
getchar();//用来接收回车键
printf("请输入第二个字符:");
b = getchar();
printf("您输入的第二个字符是:");
putchar(b);
return 0;
}
9.2 定义字符串
C语言中字符串是以空字符(\0)结尾的char类型一维数组。
9.2.1 字符串字面量(字符串常量)
用双引号括起来的内容称为 字符串字面量(string literal),也叫做 字符串常量 (string constant)。双引号中的字符和编译器自动加入末尾的\0字符,都作为字符串存储在内存中。
如果字符串字面量之间没有间隔,或者用空白字符分隔,C语言会将其视作串联起来的字符串字面量。
char greeting[50]="HELLO" "WORLD";
char greeting[50]="HELLOWORLD";
//两个字符串等价
如果要在字符串内部使用双引号,必须在双引号前面加上一个反斜杠(\)
printf("She said:\"I love you.\"");
9.2.2 字符串数组和初始化
定义字符串数组时,必须让编译器知道需要多少空间。一种方法是用足够的空间的数组储存字符串,另一种方法是省略数组初始化声明中的大小,编译器会自动计算数组的大小。
char SOS[10] = {
'S', 'O', 'S', '\0'};
//注意最后的空字符,如果没有这个空字符,这就不是一个字符串,而是一个字符数组。
char SOS[10] = "SOS";
//在指定数组大小时,要确保数组的元素个数至少比字符串长度多1()为了容纳空字符。
char SOS[] = "SOS";
//让编译器确定数组的大小更方便
9.3 指针和字符串
#include <stdio.h>
#include <string.h>
char *strlong(char *str1, char *str2) {
return strlen(str1) >= strlen(str2)?str1:str2;//strlen()计算字符串长度
//返回长的字符串
}
int main() {
char str1[10] = {
'a', 'b', 'c', 'd'}, str2[10] = {
'a', 'b', 'c', 'd', 'e'};
char *str;//用指针定义一个字符串,指针是字符串第一个字符的地址
str = strlong(str1, str2);
printf("Longer string: %s\n", str);
return 0;
}
9.4 字符串数组
可以用指针数组,也可以用二维数组。
char *love[3]={
"III","love","you"};
char love[3][5]={
"III","love","you"};
9.5 字符串输入
如果想把一个字符串读入程序,首先必须预留储存该字符串的空间,然后用输入函数获取该字符串。
char name[21];
scanf("%s",name)
/*
scanf读取字符串时变量名前不需要&
scanf()和转换说明%s只能读取一个单词(即连续的字符,不能读取空格)
*/
char name[21];
gets(name);
//gets()可以读取一整行输入
9.6 字符串输出
#include <stdio.h>
int main() {
char name[21];
puts("请输入name:");//puts输出常量
scanf("%s", name);
printf("printf输出变量:%s\n", name);
puts("puts输出变量:");//puts输出结束会自动换行
puts(name);
return 0;
}
9.7 字符串函数
序号 | 函数 | 目的 |
---|---|---|
1 | strcpy(s1, s2); | 复制字符串 s2 到字符串 s1。 |
2 | strcat(s1, s2); | 连接字符串 s2 到字符串 s1 的末尾。 |
3 | strlen(s1); | 返回字符串 s1 的长度。 |
4 | strcmp(s1, s2); | 如果 s1 和 s2 是相同的,则返回 0;如果 s1<s2 则返回小于 0;如果 s1>s2 则返回大于 0。 |
5 | strchr(s1, ch); | 返回一个指针,指向字符串 s1 中字符 ch 的第一次出现的位置。 |
6 | strstr(s1, s2); | 返回一个指针,指向字符串 s1 中字符串 s2 的第一次出现的位置。 |
#include <stdio.h>
#include <string.h>
int main () {
char s1[11] = "III";
char s2[11] = "LIIIKE";
char s3[11];
int len ;
int cmp;
char *p;
strcpy(s3, s1);//1.复制字符串 s1 到字符串 s3
printf("strcpy( s3, s1) : %s\n", s3 );
strcat(s1, s2);//2.连接字符串 s2 到字符串 s1 的末尾
printf("strcat( s1, s2): %s\n", s1 );
len = strlen(s1);//3.连接后,s1 的总长度
printf("strlen(s1) : %d\n", len );
cmp = strcmp(s1, s2);//比较s1, s2长度
printf("strcmp(s1, s2) : %d\n", cmp);
p = strchr(s2, 'K'); //返回一个指针,指向字符串 s2 中字符 K 的第一次出现的位置
printf("strchr(s2,K) : %p\n", p);
p = strstr(s2, s1);//返回一个指针,指向字符串 s2 中字符串 s1 的第一次出现的位置
printf("strstr(s2, s1) : %p\n", p);
return 0;
}
十、存储类别
10.1 作用域
作用域是描述程序中可以访问标识符的区域。一个C变量的作用域可以是块作用域、函数作用域、函数原型作用域或文件作用域。
C 语言中有三个地方可以声明变量:
- 在函数或块内部的局部变量
- 在所有函数外部的全局变量,全局变量对所有的程序文件都是可见的。
- 在自定义函数原型的形式参数中
10.1.1 块作用域
- 块是用一对花括号括起来的区域,定义在块中的变量是具有块作用域的局部变量。
- 块作用域变量的可见范围是从变量定义处到包含该定义的块的末尾。
- 虽然函数的参数声明在函数的左花括号之前,但是它们也具有块作用域,属于函数体这个块。
- 在某个函数或块的内部声明的变量称为局部变量,它们只能被该函数或该代码块内部的语句使用。
- 局部变量在函数外部是不可知的,局部变量具有块作用域。
注意:在块内不同层级定义两个相同的变量,内层块执行时会忽略外层块的同名变量。
#include <stdio.h>
int main() {
int i = 1;
{
int i = 2;
printf("内层块i=%d\n", i);
}
printf("外层块i=%d\n", i);
return 0;
}
10.1.2 函数作用域
函数作用域仅用于goto语句的标签。这意味着一个标签首次出现在函数的内层块中,它的作用域延伸至整个函数。
10.1.3 函数原型作用域
- 函数原型作用域用于函数原型中的形式参数。
- 函数原型作用域的范围是从形式参数定义处到原型声明结束。
- 编译器在处理函数原型中的形参时只关心它的类型,而形参名无关紧要。
#include <stdio.h>
void print(int a);//这个a的作用域在这一行就结束了
int main() {
int i = 1;
print(i);
return 0;
}
void print(int b) {
printf("%d", b);
}
10.1.4 文件作用域
变量的定义在函数的外面具有文件作用域。具有文件作用域的变量,从他的定义处到该定义所在的文件的末尾均可见,也成为全局变量。
#include <stdio.h>
int i = 2;
int main() {
printf("%d", i);
return 0;
}
10.2 链接
- C变量有3中链接属性:外部链接、内部链接、无链接。
- 具有块作用域、函数作用域、函数原型作用域的变量都是无链接变量,这意味着这些变量属于定义他们的块、函数、函数原型。
- 具有文件作用域的变量可以是外部链接或内部链接。
- 外部链接变量可以在多文件程序中使用,内部链接变量只能在一个翻译单元(一个源代码文件和它所包含的头文件)中使用。
10.3 存储期
存储期描述了通过标识符访问的对象的生存期。C语言根据变量的生存周期来划分,C对象有4种存储期:静态存储期、线程存储期、自动存储期、动态分配存储期。
10.3.1 静态存储期
- 静态存储期变量在整个程序执行过程中都存在,如全局变量,程序运行期间为静态存储期变量分配固定的存储空间。
- 注意:对于文件作用域变量,关键字static表明了其链接属性,而非存储期。以static声明的文件作用域变量具有内部链接。但无聊是内部链接还是外部链接,所有的文件作用域变量都具有静态存储期。
10.3.2 线程存储期
- 线程存储期:线程存储期用于并发程序设计,程序执行可分为多个线程。
- 具有线程存储期的对象,从被声明时到线程结束一直存在。
- 以关键字_Thread_local声明一个对象时,每个线程都获得该变量的私有备份。
10.3.3 自动存储期
- 块作用域的变量通常都具有自动存储期。当程序进入定义这些变量的块时,为这些变量分配内存,当退出这个块时,释放刚才为变量分配的内存。
- 自动存储相当于把自动变量占用的内存视为一个可重复
10.3.4 动态分配存储期
- 动态分配存储期:是指在程序运行期间根据需要进行动态的分配存储空间的方式。动态存储区中存放的变量是根据程序运行的需要而建立和释放的,通常包括:函数形式参数、自动变量、函数调用时的现场保护和返回地址等。
C使用作用域、链接和存储期为变量定义了多种存储方案。
存储类别 | 存储期 | 作用域 | 链接 | 声明方式 |
---|---|---|---|---|
自动 | 自动 | 块 | 无 | 块内 |
寄存器 | 自动 | 块 | 无 | 块内,使用关键字register |
静态外部链接 | 静态 | 文件 | 外部 | 所有函数外 |
静态内部链接 | 静态 | 文件 | 内部 | 所有函数外,使用关键字static |
静态无链接 | 静态 | 块 | 无 | 块内,使用关键字static |
10.4 自动变量
- 属于自动存储类别的变量具有自动存储期、块作用域、无链接。
- 默认情况下,声明在块或函数头中的任何变量都属于自动存储类别。
- 为了更清楚地表达意图,可以显示使用关键字auto。
int main(){
int mount;
auto int month;
return 0;
}
//两个带有相同存储类的变量,auto 只能用在函数内,即 auto 只能修饰局部变量
- 关键字auto是存储类别说明符。
- auto关键字在C++中的用法完全不同,如果编写C/C++兼容程序,最好不要使用auto作为存储类别说明符。
- 块作用域和无链接意味着只有在变量定义所在的块中才能通过变量名访问该变量。另一个函数可以使用同名变量,但是该变量是储存在不同内存位置上的另一个变量。
- 自动存储期意味着程序在进入该变量声明所在的块时变量存在,程序在退出该块时变量消失。
- 嵌套块中声明的变量仅限于该块及其包含的块使用。
- C99特性:作为循环或if语句的一部分,即使不使用花括号也是一个块。整个循环是他所在块的子块,循环体是整个循环块的子块。if语句是一个块,与其关联的子语句是if语句的子块。
10.5 寄存器变量
register代表了寄存器变量,C语言允许将局部变量的值放在CPU中的寄存器中,不在内存(RAM)中使用,这意味着与普通变量相比,访问和处理这些变量的速度更快,变量的最大尺寸等于寄存器的大小(通常是一个字)。
- 不能对寄存器变量应用一元的 ‘&’ 运算符,因为寄存器变量储存在寄存器而非内存中,没有内存位置。
- 寄存器变量和自动变量都是块作用域、无链接、自动存储期。
- 一个计算机系统中的寄存器数目有限,不能定义任意多个寄存器变量;
int main(){
register int miles;
return 0;
}
10.6 块作用域的静态无链接变量
- 静态的意思是该变量在内存中原地不动,并不是说它的值不变。
- 具有文件作用域的变量自动具有(也必须是)静态存储期。
- 前面提到过,可以创建具有静态存储期、块作用域的局部变量。(见8.8)
- 块作用域的静态无链接变量和自动变量具有相同的作用域,但程序离开他们所在的函数后,自动变量会消失,而块作用域的静态无链接变量不会消失。
- 块作用域的静态无链接变量具有块作用域、无链接、静态存储期。
- 程序在多次函数调用的过程中会不断记录块作用域的静态无链接变量的值。
- 块作用域的静态无链接变量也成为局部静态变量。
#include <stdio.h>
void func1(void);
int main() {
int count = 10;
while (count--)
func1();
return 0;
}
void func1(void) {
static int grow = 1;//grow是func1的局部静态变量,只初始化一次,每次调用函数func1,grow值不会被重置。
int stay = 1;//stay是局部自动变量,每次函数调用会重置变量。
printf(" stay 为 %d , fade 为 %d\n", grow, stay);
grow++;
stay++;
}
10.7 外部链接的静态变量
- 外部链接的静态变量具有文件作用域、外部链接和静态存储期。
- 该类别有时称为外部存储类别,属于该类别的变量称为外部变量。
- 把变量的定义性声明放在所有函数的外面便创建了外部变量。
- 为了指出函数使用了外部变量,可以在函数中用关键字extern再次声明外部变量。
- 如果一个源代码文件使用的外部变量定义在另一个源代码文件中,则必须用extern在该文件中声明该变量。
注:main.c和support.c需要添加到同一个项目
第一个文件:main.c
#include <stdio.h>
int num1=100;
int main(){
extern int num1;//声明要调用的外部变量,可选的声明
extern int num2;//声明要调用的外部变量,必须的声明
printf("money=%d",money);
return 0;
}
第二个文件:num2.c
int num2=100;//定义并初始化一个外部变量
10.8 内部链接的静态变量
- 该存储类别的变量具有静态存储期、文件作用域、内部链接。
- 在所有函数外部(这点和外部变量相同)用存储类别说明符static定义的变量具有这种存储类别。
- 普通的外部变量可用于同一程序中的任意文件中的函数,但是内部链接的静态变量只能用于同一个文件中的函数。
#include <stdio.h>
static int money=100;//定义内部链接的静态变量,不能被其他文件引用
int main(){
printf("money=%d",money);
return 0;
}
10.9 存储类别说明符
- C语言有6个关键字作为储存类别说明符:auto、register、static、extern、_Thread_local、typedef。
- typedef关键字与任何内存存储无关,把它归于此类有一些语法上的原因。在绝大多数情况下,不能在声明中使用多个存储类别说明符,所以这意味着不能使用多个存储类别说明符作为typedef的一部分。唯一例外是_Thread_local,他可以和static或extern一起使用。
- auto说明符表面变量是自动存储期,只能用于块作用域的变量声明中。由于在块作用域中声明的变量本身就具有自动存储期,所以使用auto主要是为了明确表达要使用与外部变量同名的局部变量。
- register说明符也只用于块作用域的变量,它把变量归为寄存器存储类别,请求最快速度访问该变量。同时还保护了该变量的地址不被获取。
- static说明符创建的对象具有静态存储期,载入程序是创建对象,当程序结束时对象消失。如果static用于文件作用域声明,作用域受限于该文件。如果static用于块作用域声明,作用域则受限于该块。因此,只要程序在运行对象就存在并保留其值,但只有在执行块内的代码时,才能通过标识符访问,块作用域的静态变量变量无链接。文件作用域的静态变量具有内部链接。
- extern 说明符表面声明的变量定义在别处。如果包含extern的声明具有文件作用域,则引用的变量必须具有外部链接。如果包含extern的声明具有块作用域,则引用的变量可能具有外部链接或内部链接,这取决于该变量的定义式声明。
10.11 存储类别和函数
10.11.1内部函数
- 如果一个函数只能被本.c文件中其他函数所调用,它称为内部函数。在定义内部函数时,在函数名和函数类型的前面加 static,即
static [数据类型说明]函数名称([参数]){
执行代码块;
return(表达式);
}
- 内部函数又称静态函数。使用内部函数,可以使函数的作用域只局限于所在文件。即使在不同的文件中有同名的内部函数,也互不干扰。提高了程序的可靠性。
外部函数
10.11.2 外部函数
- 如果在定义函数时,在函数的首部的最左端加关键字 extern,则此函数是外部函数,可供其它文件调用。
extern [数据类型说明]函数名称([参数]){
执行代码块;
return(表达式);
}
- C 语言规定,如果在定义函数时省略 extern,则默认为外部函数。
例 main.c和test.c需要添加到同一个项目
文件 main.c
#include <stdio.h>
void printLine() {
printf("**************\n");
}
int main() {
extern void say();//声明引用的外部函数
say();
return 0;
}
文件 print.c
#include <stdio.h>
void say() {
extern void printLine();//声明引用的外部函数
printLine();
printf("I love you\n");
printf("you are my world\n");
printf("forever\n");
printLine();
}
10.12 动态内存分配
- 所有的程序都必须预留足够的内存来储存程序使用的数据。
- 内存可以自动分配,如为一个intl类型的值和一个字符串预留了足够的内存。
int x;
char arr[]="I LOVE YOU";
- 可以显示指定分配一定数量的内存。
int arr[100];
该声明预留100个内存位置,每个位置都用于储存int类型的值。声明还为内存提供了一个标识符。因此可以使用x或arr识别数据。静态数据在程序载入内存时分配,而自动数据在程序执行块时分配,并在程序离开块时销毁。
10.12.1 malloc()
- malloc()可以在程序运行时分配更多的内存。
- 该函数接受一个参数:所需的内存字节数。
- malloc()函数会找到合适的空闲内存块,这样的内存是匿名的,即malloc()分配内存,但是不会为其赋名。
- malloc()函数返回动态分配内存块的首字节地址,因此可以把该地址赋给一个指针变量,并使用指针访问这块内存。
- malloc()函数的返回类型被定义为指向void的指针,该类型相当于一个“通用类型”,可用于返回指向数组的指针、指向结构的指针等,所以通常该函数的返回值会被强制转换为匹配的类型,应该坚持使用强制类型转换提高代码的可读性。
- 如果malloc()分配内存失败,将返回空指针。
double *p;
p=(double*)malloc(10*sizeof(double));
- 以上代码为10个double类型的值请求内存空间,并设置p指向该位置。
- 指针p被声明为指向一个double类型,而不是指向内含10个double类型值的块。
- 数组名是该数组首元素的地址,因此如果让p指向这个块的首元素,便可像使用数组名一样使用它,即使用p[0]访问该块的首元素,p[1]访问第2个元素,以此类推。
现在,我们有3种创建数组的方法。
-
声明数组时,用常量表达式表示数组的维度,用数组名访问数组元素。可以用静态内存或自动内存创建这种数组。
-
声明变长数组(C99新增的特性)时,用变量表达式表示数组的维度,用数组名访问数组的元素。具有这种特性的数组只能在自动内存中创建。
# include<stdio.h>
int main{
int n;
scanf("%d",&n);
int a[n];
return 0;
}
- 声明一个指针,调用malloc(),将其返回值赋给指针,使用指针访问数组的元素。该指针可以是静态的或自动的。
# include<stdio.h>
# include<stdlib.h>//malloc()和free()的原型都在stdlib.h头文件中。
int main() {
int *p;
int n;
scanf("%d", &n);
p = (int *)malloc(n * sizeof(int));//p可以视为有n个int元素的数组名
free(p);
return 0;
}
- 使用第2种和第3种方法可以创建动态数组。这种数组和普通数组不同,可以在程序运行时选择数组的大小和内存分配。
- 使用malloc()程序可以在运行时才确定数组的大小。
如果内存分配失败,可以调用**exit()**函数结束程序。EXIT_FAILURE的值也被定义在stdlib.h中。标准提供了两个返回值以保证所有操作系统中都能正常工作;EXIT_SUCCESS(或者相当于0)表示普通的程序结束,EXIT_FAILURE表示程序异常终止。
# include<stdio.h>
# include<stdlib.h>//malloc()和free()的原型都在stdlib.h头文件中。
int main() {
int *p;
int n;
scanf("%d", &n);
p = (int *)malloc(n * sizeof(int));
if(p==NULL){
puts("Memory allocation failed.GOODBYE.");
exit(EXIT_FAILURE);
}
free(p);
return 0;
}
10.12.2 free()
- 通常,malloc()要与free()配套使用。free()函数的参数是之前malloc()返回的地址,该函数释放之前malloc()分配的内存。
- 动态分配内存的存储期从调用malloc()分配内存到调用free()释放内存为止。
- free()的参数是一个指针,指向malloc()分配的一块内存,不能用free()释放通过其他方式分配的内存。
- malloc()和free()的原型都在stdlib.h头文件中。
- 静态内存的数量在编译时是固定的,在程序运行期间也不会改变。自动变量使用的内存数量在程序执行期间自动增加或减少。但是动态分配的内存数量只会增加,除非使用free()进行释放。
free()的重要性
# include<stdio.h>
# include<stdlib.h>
void temp(int a[],int n){
int *p=(int *)malloc(n*sizeof(int));
free(p);//假设忘记使用free()
}
int main(){
int arr[2000];
int i;
for(int i=0;i<1000;i++)temp(arr,200);
return 0;
}
假如代码如注释所示,遗漏了free(),每次调用函数结束时作为自动变量的指针temp会消失,但它所指向的内存却依然被占用,被占用的内存不能被重复使用,当多次循环后,内存消耗殆尽,这类问题被称为内存泄漏。在函数末尾处调用free()可避免这类问题。
10.12.3 calloc()函数
分配内存还可以使用calloc()函数
int *p;
p=(int *)calloc(100,sizeof(int));
- 和malloc()类似,在ANSI之前,calloc()返回指向char的指针,在ANSI之后,返回指向void的指针。
- 如果要存储不同的类型,应使用强制类型转换运算符。
- calloc()函数接受两个无符号整数作为参数,第一个参数是所需的存储单元数量,第2个参数是存储单元的大小(以字节为单位)。
- calloc()函数还有一个特性:它把块中的所有位都设置为0。
- free()函数也用于释放calloc()分配的内存。
十一、结构
一份图书目录包含每本图书的各种信息:书名、作者、出版社、版权日期、页数、册数、价格。同时需要创建多份列表:按书名排序、按作者排序、按价格排序等等。因此需要一种既能包含字符串又能包含数字的数据形式,这种数据形式叫 结构。
11.1 建立结构声明
结构声明描述了一个结构的组织布局。
struct book{
char title[MAXTITL];
char author[MAXAUTL];
float value;
};
- 该声明描述了一个由两个字符数组和一个float类型变量组成的结构。
- 该声明并未创建实际的数据对象,只描述了该对象由声明组成。
- 有时我们把结构声明成为模板,因为它勾勒出结构是如何储存数据的。
- 关键字struct 表面数据形式是结构,后面是一个可选标记(该例中是book),以示例中的方式建立结构时(在一处定义结构布局,在另一处定义实际的结构变量),必须使用标记。
- 把library声明为一个使用book结构布局的结构变量。
struct book library;//book是结构布局,library是book结构布局的结构变量
- 花括号括起来的是结构变量的成员列表。
- 每个成员都用自己的声明来描述。例如title部分是一个内含MAXTITL个元素的char类型数组。
- 成员可以是任何一种C的数据类型,也可以是其他结构。
- 右花括号后面的分号是声明所必需的,表示结构布局定义结束。
- 结构声明置于一个函数内部,它的标记就只限于该函数内部使用,如果结构声明置于函数的外部,那么结构声明之后的所有函数都能使用它的标记。
11.2 定义结构变量
结构有两层含义,一层含义是“结构布局”,结构布局告诉编译器如何表示数据,但是它并未让编译器为数据分配空间;另一层含义是创建一个结构变量。
struct book library;
- 编译器执行这一行代码便创建了一个结构变量library。
- 编译器使用book模板为该变量分配空间:一个内含MAXTITL个元素的char数组、一个内含MAXAUTL个元素的char数组和一个float类型的变量。这些存储空间都与一个名称library结合在一起。
- 在结构变量的声明中,struct book所起的作用相当于一般声明中的int或float。例如可以定义两个struct book类型的变量,或者甚至是指向struct book类型结构的指针
struct book a,b,*p;
声明1
struct book library;
声明2
struct book{
char title[MAXTITL];
char author[MAXAUTL];
float value;
}library;
声明1是声明2的简化。
若声明结构同时定义结构变量,则结构布局标记可以省略
struct {
char title[MAXTITL];
char author[MAXAUTL];
float value;
}library;
11.3初始化结构
初始化一个结构变量与初始化数组的语法类似
struct book library={
"Communist Manifesto",
"karl marx",
66.66
};
- 使用在一对花括号中括起来的初始化列表进行初始化,各初始项用逗号分隔。
- 为了让初始化项与结构中各成员的关联更加明显,我们让每个成员的初始化项独占一行,这样做只是为了提高代码可读性,对编译器而言,只需要用逗号分隔各成员的初始化项即可。
- 如果初始化一个静态存储期结构,初始化列表中的值必须是常量表达式。如果是自动存储期,初始化列表里的值可以不是常量。
11.4访问结构成员
使用结构成员运算符一一点(.)访问结构中的成员。
#include<stdio.h>
struct book{
char title[30];
char author[30];
float value;
}library;
int main(){
struct book library={
"Communist Manifesto",
"karl marx",
66.66
};
printf("%s\n%s\n%.2f",library.title,library.author,library.value);
return 0;
}
11.5 结构的初始化器
结构的指定初始化器使用点运算符和成员名标识特定的元素。
例如只初始化book结构的value成员
struct book book_1={
.value=66.66};
可以按照任意顺序使用指定初始化器
struct book book_2={
.value=66.66,
.author="maomao",
.title="maomaoyulu"};
对成员最后一次赋值才是实际的值
struct book book_2={
.value=66.66,
.author="maomao",
88.88};
赋给value的值是88.88,因为它在结构声明中紧跟在author成员之后,新值88.88取代之前的66.66。
11.6 结构数组
声明结构数组
struct book library[MAXBKS];
标记结构数组成员
library[0].value//第1个数组元素的value项
library[2].title[4]//第3个数组元素的第5个字符
11.7嵌套结构
#include <stdio.h>
#define LEN 20
struct stu {
char name[LEN];
int age;
};
struct group {
struct stu boy;
struct stu girl;
};
int main() {
struct group group_1 = {
{
"sam", 16},
{
"marry", 15}
};
printf("boy:%s %d\n", group_1.boy.name, group_1.boy.age);
printf("girl:%s %d\n", group_1.girl.name, group_1.girl.age);
//用多重点运算访问嵌套结构项
return 0;
}
11.8 指向结构的指针
声明结构指针
struct stu *boy;//这是一个指向结构的指针
用指针访问成员
#include <stdio.h>
#define LEN 20
struct stu {
char name[LEN];
int age;
};
int main() {
struct stu BOY = {
"sam", 16};
struct stu *boy;
boy = &BOY; //boy是一个指向BOY的指针
printf("BOY:%s %d\n", BOY.name, BOY.age);
printf("boy:%s %d", boy->name, boy->age);//通过指针访问结构成员
return 0;
}
11.9 向函数传递结构的信息
11.9.1 传递结构的成员
#include <stdio.h>
#define LEN 20
struct stu {
char name[LEN];
int age;
};
void print(char *, int );
int main() {
struct stu boy = {
"sam", 16};
print(boy.name, boy.age);//结构成员传递给函数
return 0;
}
void print(char *a, int b) {
printf("name:%s\nage:%d", a, b);
}
11.9.2 传递结构
#include <stdio.h>
#define LEN 20
struct stu {
char name[LEN];
int age;
};
void print(struct stu) ;//参数是一个结构
int main() {
struct stu boy = {
"sam", 16};
print(boy);//结构传入函数
return 0;
}
void print(struct stu a) {
printf("name:%s\nage:%d", a.name, a.age);
}
11.9.3 结构其他特性
- 相同类型的结构可以直接互相赋值
- 结构可以作为函数参数,也可以作为函数返回值
11.9.4 结构和结构指针的选择
指针作为参数
- 优点:只需要传递一个地址,执行效率高。
- 缺点:无法保护数据,某些操作可能会意外影响原来结构中的数据。
- ANSI C新增的const限定符可以防止原始数据被意外篡改。
#include <stdio.h>
#define LEN 20
struct score {
int math;
int art;
};
void print(const struct score *) ;
int main() {
struct score boy = {
51, 52};
print(&boy);
return 0;
}
void print(const struct score *a) {
//const限定符让结构变为只读,成员不能被修改
printf("score:%d", a->art + a->math);
}
结构作为参数
- 优点:保护原始数据,代码风格更清楚。
- 缺点:传递结构浪费时间和内存空间。
13.9.5 结构、指针、malloc()
- 如果使用malloc()分配内存并使用指针储存该地址,可以使用malloc()为字符串分配合适的存储空间。
- 这样的字符串并未储存在结构中,而是储存在malloc()分配的内存块中,结构中储存着字符串的地址。
- 处理字符串的函数通常都要使用字符串的地址,这种方式不用访问结构体就能通过地址处理结构体中的字符串。
- 注意:malloc()和free()应该成对使用。
13.9.6 复合字面量和结构
#include <stdio.h>
#define LEN 20
struct stu {
char name[LEN];
int age;
};
struct group {
struct stu boy;
struct stu girl;
};
int main() {
struct group group_1 ;
group_1 = (struct group) {
//复合字面量强制转换为结构
{
"sam", 16},
{
"marry", 15}
};
printf("boy:%s %d\n", group_1.boy.name, group_1.boy.age);
printf("girl:%s %d\n", group_1.girl.name, group_1.girl.age);
return 0;
}
11.10 匿名结构
#include <stdio.h>
#define LEN 20
struct group {
struct {
char name1[LEN];int age1;};
struct {
char name2[LEN];int age2;};
//嵌套的这两个结构是匿名的,没有结构名
};
int main() {
struct group group = {
{
"sam", 16},
{
"marry", 15}
};
printf("stu1:%s %d\n", group.name1, group.age1);
printf("stu2:%s %d\n", group.name2, group.age2);
//把内层匿名结构的元素直接作为外层结构的元素访问
return 0;
}
十二、联合
- 联合(union,也翻译为共用体)是一种数据类型,它能在同一个内存空间中储存不同的数据类型(不是同时储存)。
- 其典型的用法是,设计一种表以储存既无规律、事先也不知道顺序的混合类型。
- 使用联合类型的数组,其中的联合大小都相等,每个联合可以储存各种数据类型。
12.1 建立联合声明
创建联合和创建结构的方式相同,需要一个联合模板和联合变量。
union Magic{
int Life_value;
char weapon;
double mana;
}
以上形式声明的联合存储三个变量其中的一个。
union Magic magic;//Magic类型的联合变量
union Magic magic[10];//联合变量数组
union Magic *magic;//指向联合的指针
12.2 定义联合变量并初始化
#include <stdio.h>
union Magic {
int Life_value;
char *weapon;
double mana;
};
int main() {
union Magic magicA;
//声明变量magicA
magicA.Life_value = 66;
//使用指定初始化器
union Magic magicB = magicA;
//把一个联合初始化为另一个同类型的联合
union Magic magicC = {
66};
//初始化联合的成员,自动赋值给类型匹配的成员
union Magic magicD = {
.mana = 66.66};
//使用指定初始化器
return 0;
}
12.3 使用联合
#include <stdio.h>
union Magic {
int Life_value;
char *weapon;
double mana;
};
int main() {
union Magic magic;
magic.Life_value = 66;//给magic.Life_value赋值
magic.mana = 66.66;//清除magic.Life_value的值,给magic.mana赋值
magic.weapon = "gun";//清除magic.mana的值,给magic.weapon赋值
return 0;
}
使用指针
#include <stdio.h>
union Magic {
int Life_value;
char *weapon;
double mana;
};
int main() {
union Magic *magic;
magic->Life_value = 66;
magic->mana = 66.66;
magic->weapon = "gun";
return 0;
}
12.4 匿名联合(C11)
#include <stdio.h>
#define LEN 20
struct group {
struct {
char name1[LEN];int age1;};
struct {
char name2[LEN];int age2;};
//嵌套的这两个结构是匿名的,没有结构名
};
int main() {
struct group group = {
{
"sam", 16},
{
"marry", 15}
};
printf("stu1:%s %d\n", group.name1, group.age1);
printf("stu2:%s %d\n", group.name2, group.age2);
//把内层匿名结构的元素直接作为外层结构的元素访问
return 0;
}
十三、枚举
枚举是 C 语言中的一种基本数据类型,它可以让数据更简洁,更易读。
#define MON 1
#define TUE 2
#define WED 3
#define THU 4
#define FRI 5
#define SAT 6
#define SUN 7
enum DAY{MON=1, TUE, WED, THU, FRI, SAT, SUN};
两段代码能够起到相同的作用,但第二段更简洁易读。
13.1 枚举类型定义
枚举类型定义格式
enum 枚举名 {
枚举元素1,枚举元素2,……};
//花括号内的标识符枚举了enum变量可能的值,这些符号常量被称为枚举符或枚举元素(enumerator),枚举符是int类型常量。
枚举类型定义实例
enum DAY{
MON=1, TUE, WED, THU, FRI, SAT, SUN};
//第一个枚举成员的默认值为整型的 0,后续枚举成员的值在前一个成员上加 1。
//我们在这个实例中把第一个枚举成员的值定义为 1,第二个就为 2,以此类推。
- 可以在定义枚举类型时改变枚举元素的值:
enum season {
spring, summer=3, autumn, winter};
//没有指定值的枚举元素,其值为前一元素加 1。也就说 spring 的值为 0,summer 的值为 3,autumn 的值为 4,winter 的值为 5
13.2枚举变量的定义
- 先定义枚举类型,再定义枚举变量
enum DAY{
MON=1, TUE, WED, THU, FRI, SAT, SUN};
enum DAY day;
- 定义枚举类型的同时定义枚举变量
enum DAY{
MON=1, TUE, WED, THU, FRI, SAT, SUN} day;
- 省略枚举名称,直接定义枚举变量
enum{
MON=1, TUE, WED, THU, FRI, SAT, SUN} day;
#include <stdio.h>
enum DAY{
MON=1, TUE, WED, THU, FRI, SAT, SUN};//定义枚举类型模板
int main(){
enum DAY day;//定义枚举变量day,枚举变量day可以是任意整数类型,占位符可以用d%、u%、o%、x%等
return 0;
}
注:C枚举的一些特性不适用于C++。例如C允许枚举变量使用++运算符,但是C++标准不允许。
13.3 enum的用法
#include <stdio.h>
#include <stdlib.h>
int main(){
enum color {
red=1, green, blue };
enum color favorite_color;
printf("请输入你喜欢的颜色: (1. red, 2. green, 3. blue): ");
scanf("%u", &favorite_color);
switch (favorite_color){
case red:
printf("你喜欢的颜色是红色");
break;
case green:
printf("你喜欢的颜色是绿色");
break;
case blue:
printf("你喜欢的颜色是蓝色");
break;
default:
printf("你没有选择你喜欢的颜色");
}
return 0;
}
十四、其它数据形式要点
14.1 共享命名空间
- C语言使用命名空间标识程序中的各部分,即通过名称来识别。
- 作用域是命名空间概念的一部分:两个不同作用域的同名变量不冲突;两个相同做同于的同名变量冲突。
- 命名空间是分类别的,在特定的作用域中结构标记、联合标记、枚举标记共享相同的命名空间,该命名空间与普通变量使用的空间不同,这意味着在相同作用域中变量和标记的名称可以相同,不会引起冲突。
14.2 typedef
利用typedef可以为某一类型自定义名称。
typedef unsigned char BYTE;//为单字节数字定义了一个术语 BYTE
在这个类型定义之后,标识符 BYTE 可作为类型 unsigned char 的缩写。
BYTE b1, b2;//等价unsigned char b1, b2;
使用 typedef 来为用户自定义的数据类型取一个新的名字。
#include <stdio.h>
typedef struct Books {
char title[30];
char author[30];
float value;
} Book;//BOOK是struct Books的别名,可以等价使用
int main() {
Book book = {
//等价struct Books book
"Communist Manifesto",
"karl marx",
66.66
};
printf("%s\n%s\n%.2f", book.title, book.author, book.value);
return 0;
}
typedef vs #define
#define 是 C 指令,用于为各种数据类型定义别名,与 typedef 类似,但是它们有以下几点不同:
- typedef 仅限于为类型定义符号名称,#define 不仅可以为类型定义别名,也能为数值定义别名,比如您可以定义 1 为 ONE。
- typedef 是由编译器执行解释的,#define 语句是由预编译器进行处理的。
14.3其它复杂的声明
看懂以下声明,关键要理解 * 、( )、[ ]的优先级,数组名后面的 [ ] 和函数名后面的 ( ) 具有相同的优先级,他们比 * 的优先级高。
int a[5][5];//声明一个内含int的数组的数组
int **p;//声明一个指向指针的指针
int *a[5];//声明一个内含5个元素的数组,每个元素都是一个指向int的指针
int (*a)[5];//声明一个指向数组的指针,数组内含5个int类型的值
int *a[3][5];//声明一个3*5的数组,没有元素都是指向int的指针
int (*a)[3][5];//声明一个指向3*5二维数组的指针,该数组中内含int类型值
int (*a[3])[5];//声明一个内含3个指针元素的数组,其中每个指针都指向一个内含4个int类型元素的数组
char *fn(int);//返回值为字符指针的函数
char (*fn)(int)//指向函数的指针,该函数的返回值是char
char (*fn[3])(int);//3个元素的数组,每个元素是指针,指向返回值为char的函数
十五、链表
15.1 链表
- 链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
- 链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。
- 每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。
- 使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。
例,创建一个链表,给链表成员赋值,打印链表成员值,释放已分配内存。
#include <stdio.h>
#include <stdlib.h>
typedef struct _node {
int value;
struct _node *next;
} node;
int main() {
node *head = NULL;//头结点
node *last = NULL;//尾结点
node *p;//新增结点,临时变量
int number;
/*链表赋值,-1表示结束*/
do {
scanf("%d", &number);
if (number != -1) {
//add to linked-list
p = (node *)malloc(sizeof(node));//获取新增结点内存,返回
p->value = number;
p->next = NULL;
if (head == NULL) {
//头结点和尾结点为空,给第一个结点赋值
head = p;
last = p;
} else {
//如果头结点不为空,把p加在链表后面
last->next = p;//当前最后一个结点的next指向p
last = p;//更新最后一个结点
}
} else
break;
} while (1);
p = head;
//读取下一个结点,如果下一个结点不为空,打印下一个结点的value
while (p != NULL) {
//从头结点开始打印
printf("%d\n", p->value, p);
p = p->next;
}
p = head;
while (head != NULL) {
head = p->next; //头结点变为当前结点的下一个结点
free(p);//释放当前结点的内存
}
return 0;
}
15.2 链表的函数
把前面这个例子拆分成多个函数的形式
#include <stdio.h>
#include <stdlib.h>
typedef struct _node {
int value;
struct _node *next;
} node;
void add(node **head, node **last, int number);
//增加结点,如果要通过函数改变结点的指针,需要传入指针的指针
void Free(node *head);//释放链表内存
void print(node *head);//打印结点value
int main() {
node *head = NULL;//头结点
node *last = NULL;//尾结点
int number;
//创建链表并赋值,-1表示结束
do {
scanf("%d", &number);
if (number != -1) {
add(&head, &last, number);
//head是链表结点指针,&head是指针的指针
} else
break;
} while (1);
print(head);//打印链表value
Free(head);//释放链表内存
return 0;
}
void add(node **head, node **last, int number) {
node *p;
p = (node *)malloc(sizeof(node)); //获取新增结点内存
p->value = number;
p->next = NULL;
//头结点和尾结点为空,给第一个结点赋值
if (*head == NULL) {
*head = p;
*last = p;
} else {
(*last)->next = p;//当前最后一个结点的next指向p
*last = p;//更新最后一个结点
}
}
void print(node *head) {
node *p = head;
//读取下一个结点,如果下一个结点不为空,打印下一个结点的value
while (p != NULL) {
//从头结点开始打印
printf("%d ", p->value);
p = p->next;
}
}
void Free(node *head) {
node *p = head;
while (head != NULL) {
head = p->next; //头结点变为当前结点的下一个结点
free(p);//释放当前结点的内存
p = head;
}
}
此例中函数add()有三个参数,可以进一步优化
void add(node **head, node **last, int number)
#include <stdio.h>
#include <stdlib.h>
typedef struct _node {
int value;
struct _node *next;
} node;
typedef struct _list {
node *head;
node *last;
int number;
} List;
//用List来代表整个链表
void add(List *list);
//增加结点,如果要通过函数改变结点的指针,需要传入指针的指针
void Free(List *list);//释放链表内存
void print(List *list);//打印结点value
int main() {
List list;
list.head = list.last = NULL;
//创建链表并赋值,-1表示结束
do {
scanf("%d", &list.number);
if (list.number != -1) {
add(&list);
//head是链表结点指针,&head是指针的指针
} else
break;
} while (1);
print(&list);//打印链表value
Free(&list);//释放链表内存
return 0;
}
void add(List *list) {
node *p;
p = (node *)malloc(sizeof(node)); //获取新增结点内存
p->value = list->number;
p->next = NULL;
//头结点和尾结点为空,给第一个结点赋值
if (list->head == NULL) {
list->head = p;
list->last = p;
} else {
list->last->next = p;//当前最后一个结点的next指向p
list->last = p;//更新最后一个结点
}
}
void print(List *list) {
node *p = list->head;
//读取下一个结点,如果下一个结点不为空,打印下一个结点的value
while (p != NULL) {
//从头结点开始打印
printf("%d ", p->value);
p = p->next;
}
}
void Free(List *list) {
node *p = list->head;
while (list->head != NULL) {
list->head = p->next; //头结点变为当前结点的下一个结点
free(p);//释放当前结点的内存
p = list->head;
}
}
15.3 删除链表结点
分别将读入的数据存储为单链表、将链表中所有存储了某给定值的结点删除。
函数readlist
从标准输入读入一系列正整数,按照读入顺序建立单链表。当读到−1时表示输入结束,函数应返回指向单链表头结点的指针。
函数deletem
将单链表L
中所有存储了m
的结点删除。返回指向结果链表头结点的指针。
#include <stdio.h>
#include <stdlib.h>
struct ListNode {
int data;
struct ListNode *next;
};
struct ListNode *readlist();
struct ListNode *deletem( struct ListNode *L, int m );
void printlist( struct ListNode *L );
int main(){
int m;
struct ListNode *L = readlist();
scanf("%d", &m);
L = deletem(L, m);
printlist(L);
return 0;
}
void printlist( struct ListNode *L ){
struct ListNode *p = L;
while (p) {
printf("%d ", p->data);
p = p->next;
}
printf("\n");
}
struct ListNode *readlist(){
struct ListNode *head=NULL;
struct ListNode *tail=NULL;
struct ListNode *p;
int number;
do{
scanf("%d",&number);
if(number!=-1){
p=(struct ListNode *)malloc(sizeof(struct ListNode));
p->data=number;
if(head==NULL)head=tail=p;
else {
tail->next=p;
tail=p;
}
}else break;
}while(1);
return head;
}
struct ListNode *deletem( struct ListNode *L, int m ){
struct ListNode *head=NULL;
struct ListNode *pre=NULL,*cur;
cur=L;
do{
if(cur->data!=m){
if(head==NULL)head=cur;
pre=cur;
cur=cur->next;
}else{
if(pre==NULL){
pre=cur;
cur=cur->next;
free(pre);
}else{
pre->next=cur->next;
free(cur);
cur=pre->next;
}
}
}while(cur!=NULL);
return head;
}
15.4 拆分链表
例.分别将读入的数据存储为单链表、将链表中奇数值的结点重新组成一个新的链表。
函数readlist
从标准输入读入一系列正整数,按照读入顺序建立单链表。当读到−1时表示输入结束,函数应返回指向单链表头结点的指针。
函数getodd
将单链表L
中奇数值的结点分离出来,重新组成一个新的链表。返回指向新链表头结点的指针,同时将L
中存储的地址改为删除了奇数值结点后的链表的头结点地址(所以要传入L
的指针)。
#include <stdio.h>
#include <stdlib.h>
typedef struct ListNode {
int data;
struct ListNode *next;
} node;
struct ListNode *readlist();
struct ListNode *getodd( struct ListNode **L );
void Free(node *head);
void printlist( struct ListNode *L );
int main() {
struct ListNode *L, *Odd;
L = readlist();
Odd = getodd(&L);
printlist(Odd);
printlist(L);
Free(L);
Free(Odd);
return 0;
}
void printlist( struct ListNode *L ) {
struct ListNode *p = L;
while (p) {
printf("%d ", p->data);
p = p->next;
}
printf("\n");
}
struct ListNode *readlist() {
struct ListNode *head = NULL;
struct ListNode *last = head;
struct ListNode *p;
int number;
do {
scanf("%d", &number);
if (number != -1) {
//add to linked-list
p = (struct ListNode *)malloc(sizeof(struct ListNode));
p->data = number;
p->next = NULL;
//find the last
if (last) {
last->next = p;
last = last->next;
} else {
head = p;
last = p;
}
}
} while (number != -1);
return head;
}
struct ListNode *getodd( struct ListNode **L ) {
struct ListNode *p, *head1, *last1, *head2, *last2;
//*p是原链表结点指针
//*head1是奇数值链表头结点指针,*last1是奇数值链表尾结点指针
//*head2是偶数值链表头结点指针,*last2是偶数值链表尾结点指针
p = *L; //L是原链表头指针的指针,*L是原链表的头指针
head1 = NULL;
last1 = NULL;
head2 = NULL;
last2 = NULL;
while (p != NULL) {
if (p->data % 2 == 0) {
//如果data是偶数,p接到偶数链表的尾部
if (last2) {
last2->next = p;
last2 = last2->next;
} else {
head2 = p;
last2 = p;
*L = p;
}
} else {
//如果data是奇数,p接到奇数链表的尾部
if (last1) {
last1->next = p;
last1 = last1->next;
} else {
head1 = p;
last1 = p;
}
}
p = p->next;
}
if (last1)
last1->next = NULL; //奇数链表尾结点next保证为空
if (last2)
last2->next = NULL; //偶数链表尾结点next保证为空
else *L = NULL; //若偶数链表不存在,则删除了奇数值结点后的链表的头结点地址为空
return head1;
}
void Free(node *head) {
node *p = head;
while (head != NULL) {
head = p->next; //头结点变为当前结点的下一个结点
free(p);//释放当前结点的内存
p = head;
}
}
十六、位操作
16.1二进制、位、字节
-
二进制:日常生活都是基于数字10来书写数字,例如日常说的2333是十进制的,可以写为2×10^3+ 3×10^2+ 3×10^1+ 3×10^0。这种书写数字的方法是基于10的幂,所以称以10为基底书写2333。计算机适用基底为2的数制系统,它用2的幂而不是10的幂,以2为基底表示的数字被称为二进制数(binary number)。二进制是计算技术中广泛采用的一种数制,二进制数据是用0和1两个数码来表示的数,进位规则是“逢二进一”。
-
例:二进制数1101可表示为:
1×23+ 1×22+ 0×21+ 1×20
以十进制数表示为:
1×8 + 1×4 + 0×2 + 1×1 = 13
-
位:二进制计数系统中,位简记为b,也称为比特,每个二进制数字0或1就是一个位(bit)。
-
字节:位是数据存储的最小单位,其中8 bit 就称为一个字节(Byte)。
位编号 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
二进制数 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
位值 | 128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 |
128 + 64 + 32 + 16 + 8 + 4 + 2 + 1 = 255
因此,1字节可储存0 ~255范围内的数字,总共256个值。或者,通过不同的方式解释位组合 (bit pattern),程序可以用1字节储存-128~+127范围内的整数,总共还是 256个值。例如,通常unsigned char用1字节表示的范围是0~255,而signed char用1字节表示的范围是-128~+127。
16.2 八进制
-
计算机也通常使用八进制和十六进制,因为8和16都是2的幂,八进制和十六进制比十进制更接近计算机的二进制系统。
-
八进制(octal)是指八进制记数系统。该系统基于8的幂,用0~7表示数字(正如十进制用0~9表示数字一样)。
-
例如,八进制数451(在C中写作0451,前面的0表示这是一个八进制数)表示为:4×8^2+ 5×8^1+ 1×8^0。
-
因为2的3次方等于8,所以每个八进制位对应3个二进制位。
八进制位 0 1 2 3 4 5 6 7 等价二进制位 000 001 010 011 100 101 110 111 -
例:八进制数0173的二进制数01111011。
八进制位 0173 1 7 3 等价二进制位 01111011 001 010 011
16.3 十六进制
-
十六进制(hexadecimal或hex)是指十六进制记数系统。该系统基于16 的幂,用0~15表示数字。由于没有单独的数表示10~15,所以用字母A~F来表示。
-
例如:十六进制数 A3F(在C中写作0xA3F,前面的0x表示这是一个十六进制数)表示为: 10×162+3×161+15×16^0。
-
因为2的4次方等于16,所以每个十六进制位对应4个二进制位。
十进制 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 十六进制 0 1 2 3 4 5 6 7 8 9 A B C D E F 等价二进制位 0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111 -
例:十六进制数0xAC2的等价二进制数101011000010。
十六进制位 0xAC2 A C 2 等价二进制位 101011000010 1010 1100 0010
16.4 C按位逻辑运算符
4个按位逻辑运算符都用于整型数据,之所以叫作按位 (bitwise)运算,是因为这些操作都是针对每一个位进行,不影响它左右两边的位。
16.4.1二进制反码或按位取反:~
一元运算符~把1变为0,把0变为1。如下例子所示:
表达式 :~(10011010)
结果值 :(01100101)
为了理解位运算,这里用二进制计数法写出值,但实际的C程序中不这样使用二进制数。
假设变量a的类型是int,已被赋值为2,在二进制中,00000010表示2。那么,~a的值是11111101,即253。
注意,该运算符不会改变a的值,就像3 * a不会改变a的值一样, a仍然是2。但是,该运算符确实创建了一个可以使用或赋值的新值。
#include <stdio.h>
int main() {
unsigned char a = 2;
unsigned char b;
b = ~a;
printf("%d", b);
return 0;
}
16.4.2 按位与:&
二元运算符&通过逐位比较两个运算对象,生成一个新值。对于每个 位,只有两个运算对象中相应的位都为1时,结果才为1。
从真/假方面看,只有当两个位都为真时,结果才为真。
如下例子所示:
表达式 :(10010011) & (00111101) ,二进制10010011是十进制147,二进制00111101是十进制61。
由于两个运算对象中编号为4和0的位都为1,得:
结果值 :(00010001) 二进制00010001是十进制17
#include <stdio.h>
int main() {
unsigned char a = 147;
unsigned char b = 61;
printf("%d", a & b);
return 0;
}
16.4.3 按位或:|
二元运算符|,通过逐位比较两个运算对象,生成一个新值。对于每个 位,如果两个运算对象中相应的位为1,结果就为1。
从真/假方面看,如果两个运算对象中相应的一个位为真或两个位都为真,那么结果为真。
如下例子所示:
表达式 :(10010011) || (00111101) ,二进制10010011是十进制147,二进制00111101是十进制61。
除了编号为6的位,这两个运算对象的其他位至少有一个位为1,得:
结果值:(10111111)二进制10111111是十进制191
#include <stdio.h>
int main() {
unsigned char a = 147;
unsigned char b = 61;
printf("%d", a | b);
return 0;
}
16.4.4 按位异或:^
二元运算符^逐位比较两个运算对象。对于每个位,如果两个运算对象中相应的位一个为1(但不是两个为1),结果为1。
从真/假方面看,如果两个运算对象中相应的一个位为真且不是两个为同为1,那么结果为真)。
如下例子所示:
表达式 :(10010011)^(00111101)
,二进制10010011是十进制147,二进制00111101是十进制61。
编号为0的位都是1,所以结果为0,得:
结果值 :(10101110) ,二进制10101110是十进制174
#include <stdio.h>
int main() {
unsigned char a = 147;
unsigned char b = 61;
printf("%d", a ^ b);
return 0;
}
C有一个按位异或和赋值结合的运算符:^=。下面两条语句产生的最终作用相同:
a^= 123;
a = a ^ 123;
16.5 C按位逻辑运算符用法
16.5.1 掩码
例. 假设定义符号常量MASK为2 (即,二进制形式为00000010),只有1号位是1,其他位都是0。下面的语句:
flags = flags & MASK;
把flags中除1号位以外的所有位都设置为0,因为使用按位与运算符 (&)任何位与0组合都得0。
1号位的值不变(如果1号位是1,那么 1&1得 1;如果 1号位是0,那么 0&1也得0)。
这个过程叫作“使用掩码”,因为掩 码中的0隐藏了flags中相应的位。
用&=运算符可以简化前面的代码,如下所示:
flags &= MASK;
下面这条语句是按位与的一种常见用法:
ch &= 0xff; /* 或者 ch &= 0377; */
oxff的二进制形式是11111111,八进制形式是0377。这个掩码保持ch中最后8位不变,其他位都设置为0。无论ch原来是8位、16位或是 其他更多位,最终的值都被修改为1个8位字节。
16.5.2 打开位
例如,为了打开内置扬声器,必须 打开 1 号位,同时保持其他位不变。这种情况可以使用按位或运算符 (|)
flags = flags | MASK; //假设定义符号常量MASK为2
把flags的1号位设置为1,且其他位不变,因为使用 | 运算符,任何位与0 组合,结果都为本身,任何位与1组合,结果都为1。
例如,假设flags是00001111,MASK是10110110。下面的表达式:
flags | MASK
即是: (00001111) | (10110110) // 表达式
其结果为: (10111111) // 结果值
MASK中为1的位,flags与其对应的位也为1。MASK中为0的位,flags与其对应的位不变。
用|=运算符可以简化上面的代码,如下所示:
flags |= MASK;
15.5.3 关闭位(清空位)
例如,要关闭变量flags中的1号位。
flags = flags & ~MASK;//假设定义符号常量MASK为2
因为MASK除1号位为1以外,其他位全为0,所以~MASK除1号位为0 以外,其他位全为1。
使用&,任何位与1组合都得本身,所以这条语句保持 1 号位不变,改变其他各位。
另外,使用&,任何位与0组合都的0。所以无论1号位的初始值是什么,都将其设置为0。
例如,假设flags是00001111,MASK是10110110。下面的表达式:
flags & ~MASK
即是: (00001111) & ~(10110110) // 表达式
其结果为: (00001001) // 结果值
MASK中为1的位在结果中都被设置(清空)为0。
flags中与MASK为0的位相应的位在结果中都未改变。
可以使用下面的简化形式:
flags &= ~MASK;
16.5.4切换位
切换位指的是打开已关闭的位,或关闭已打开的位。可以使用按位异或 运算符(^)切换位。
也就是说,假设b是一个位(1或0),如果b为1,则 1^b
为0;如果b为0,则``1^b`为1。
另外,无论b为1还是0,0^b均为b。
因此,如果使用^组合一个值和一个掩码,将切换该值与MASK为1的位相对应的 位,该值与MASK为0的位相对应的位不变。
要切换flags中的1号位,可以使用下面两种方法
flags = flags ^ MASK;
flags ^= MASK;
例如,假设flags是00001111,MASK是10110110。表达式:
flags ^ MASK
即是: (00001111) ^ (10110110) // 表达式
其结果为: (10111001) // 结果值
flags中与MASK为1的位相对应的位都被切换了,MASK为0的位相对应的位不变。
16.5.5 检查位的值
例如,flags中 1号位是否被设置为1?不能这样直接比较flags和MASK: if (flags == MASK)
这样做即使flags的1号位为1,其他位的值会导致比较结果为假。
因此, 必须覆盖flags中的其他位,只用1号位和MASK比较:
if ((flags & MASK) == MASK)
由于按位运算符的优先级比==低,所以必须在flags & MASK周围加上圆括号。
为了避免信息漏过边界,掩码至少要与其覆盖的值宽度相同。
16.6 位移运算符
16.6.1 左移:<<
左移运算符(<<)将其左侧运算对象每一位的值向左移动其右侧运算对象指定的位数。左侧运算对象移出左末端位的值丢失,用0填充空出的位置。
下面的例子中,每一位都向左移动两个位置:
(10001010) << 2 // 表达式
(00101000) // 结果值
该操作产生了一个新的位值,但是不改变其运算对象。
例如,假设 a为1,那么 a<<2为4,但是a本身不变,仍为1。
#include <stdio.h>
int main() {
unsigned char a = 1;
printf ("%d", a << 2);
return 0;
}
可以使用左移赋值运算符(<<=)来更改变量的值,该运算符将变量中的位向左移动其右侧运算对象给定值的位数。
#include <stdio.h>
int main() {
unsigned char a = 1;
a<<=2;
printf ("%d", a);
return 0;
}
16.6.2 右移
右移运算符(>>)将其左侧运算对象每一位的值向右移动其右侧运算 对象指定的位数。左侧运算对象移出右末端位的值丢。对于无符号类型,用 0 填充空出的位置;对于有符号类型,其结果取决于机器。空出的位置可用 0填充,或者用符号位(即,最左端的位)的副本填充:
(10001010) >> 2 // 表达式,有符号值
(00100010) // 在某些系统中的结果值
(10001010) >> 2 // 表达式,有符号值
(11100010) // 在另一些系统上的结果值
下面是无符号值的例子:
(10001010) >> 2 // 表达式,无符号值
(00100010) // 所有系统都得到该结果值
每个位向右移动两个位置,空出的位用0填充。
右移赋值运算符(>>=)将其左侧的变量向右移动指定数量的位数。如下所示:
#include <stdio.h>
int main() {
unsigned char a = 16;
a>>=3;
printf ("%d", a);
return 0;
}
16.6.3 移位运算符用法
例1
移位运算符针对2的幂提供快速有效的乘法和除法:
number << n number乘以2的n次幂
number >> n 如果number为非负,则用number除以2的n次幂
这些移位运算符类似于在十进制中移动小数点来乘以或除以10。
例2
移位运算符还可用于从较大单元中提取一些位。例如,假设用一个unsigned long类型的值表示颜色值,低阶位字节储存红色的强度,下一个字节储存绿色的强度,第 3 个字节储存蓝色的强度。随后你希望把每种颜色的强度分别储存在3个不同的unsigned char类型的变量中。
#define BYTE_MASK 0xff
unsigned long color = 0x002a162f;
unsigned char blue, green, red;
red = color & BYTE_MASK;
green = (color >> 8) & BYTE_MASK;
blue = (color >> 16) & BYTE_MASK;
以上代码中,使用右移运算符将 8 位颜色值移动至低阶字节,然后使用掩码技术把低阶字节赋给指定的变量。
十七、文件
17.1文件
17.1.1什么是文件?
文件通常是在磁盘或固态硬盘上的一段已命名的存储区。
例如,编写的C程序就保存在文件中,用来编译C程序的程序也保存在文件中。后者说明,某些程序需要访问指定的文件。
当编译储存在名为psy.c
文件中的程序时,编译器打开psy.c
文件并读取其中的内容。当编译器处理完后,会关闭该文件。其他程序,例如文字处理器,不仅要打开、读取和关闭文件,还要把数据写入文件。
C把文件看作是一系列连续的字节,每个字节都能被单独读取。这与UNIX
环境中(C的发源地)的文件结构相对应。C提供两种文件模式:文本模式和二进制模式。
17.1.2文本模式和二进制模式
首先,要区分文本内容和二进制内容、文本文件格式和二进制文件格式,以及文件的文本模式和二进制模式。
所有文件的内容都以二进制形式(0或1)储存。但是,如果文件最初使用二进制编码的字符(例如, ASCII或Unicode)表示文本(就像C字符串那样),该文件就是文本文件,其中包含文本内容。如果文件中的二进制值代表机器语言代码或数值数据或图片或音乐编码,该文件就是二进制文件,其中包含二进制内容。UNIX用同一种文件格式处理文本文件和二进制文件的内容。
为了规范文本文件的处理,C 提供两种访问文件的途径:二进制模式和文本模式。
在二进制模式中,程序可以访问文件的每个字节。而在文本模式中,程序所见的内容和文件的实际内容不同。程序以文本模式读取文件时,把本地环境表示的行末尾或文件结尾映射为C模式。例如,C程序在旧式Macintosh中以文本模式读取文件时,把文件中的\r
转换成\n
;以文本模式写入文件时,把\n
转换成\r
。或者,C文本模式程序在MS-DOS平台读取文件时,把\r\n
转换成\n
;写入文件时,把\n
转换成\r\n
。在其他环境中编写的文本模式程序也会做类似的转换。
除了以文本模式读写文本文件,还能以二进制模式读写文本文件。如果读写一个旧式MS-DOS文本文件,程序会看到文件中的\r
和\n
字符,不会发生映射。如果要编写旧式 Mac格式、MS-DOS格式或UNIX/Linux格式的文件模式程序,应该使用二进制模式,这样程序才能确定实际的文件内容并执行相应的动作。
虽然C提供了二进制模式和文本模式,但是这两种模式的实现可以相同。因为UNIX使用一种文件格式,这两种模式对于UNIX实现而言完全相同。Linux也是如此。
17.1.3I/O
的级别
大多数情况下,可以选择I/O
的两个级别(即处理文件访问的两个级别)。
从较低层面上,C可以使用主机操作系统的基本文件工具直接处理文件,这些直接调用操作系统的函数被称为底层I/O(low-levelI/O)
。底层I/O(low-level I/O)
使用操作系统提供的基本I/O
服务。标准高级I/O(standard high-level I/O)
使用C库的标准包和stdio.h
头文件定义。
C是一门强大、灵活的语言,有许多用于打开、读取、写入和关闭文件的库函数。因为无法保证所有的操作系统都使用相同的底层I/O
模型,C标准只支持标准I/O
包。从较高层面上,C还可以通过标准I/O
包(standardI/Opackage)
来处理文件。这涉及创建用于处理文件的标准模型和一套标准I/O
函数。在这一层面上,具体的C实现负责处理不同系统的差异,以便用户使用统一的界面。
上面讨论的差异指的是什么?例如,不同的系统储存文件的方式不同。有些系统把文件的内容储存在一处,而文件相关的信息储存在另一处;有些系统在文件中创建一份文件描述。在处理文件方面,有些系统使用单个换行符标记行末尾,而其他系统可能使用回车符和换行符的组合来表示行末尾。有些系统用最小字节来衡量文件的大小,有些系统则以字节块的大小来衡量。如果使用标准I/O
包,就不用考虑这些差异。因此,可以用if(ch=='\n')
检查换行符。即使系统实际用的是回车符和换行符的组合来标记行末尾,I/O
函数会在两种表示法之间相互转换。
17.1.4标准文件
C程序会自动打开3个文件,它们被称为标准输入(standard input)
、标准输出(standard output)
和标准错误输出(standard error output)
。
在默认情况下,标准输入是系统的普通输入设备,通常为键盘;标准输出和标准错误输出是系统的普通输出设备,通常为显示屏。
通常,标准输入为程序提供输入,它是 getchar()
和 scanf()
使用的文件。程序通常输出到标准输出,它是putchar()
、puts()
和printf()
使用的文件。标准错误输出提供了一个逻辑上不同的地方来发送错误消息。
17.2 标准I/O
与底层I/O
相比,标准I/O
包除了可移植以外还有两个好处。
第一,标准I/O
有许多专门的函数简化了处理不同I/O
的问题。例如,printf()
把不同形式的数据转换成与终端相适应的字符串输出。
第二,输入和输出都是缓冲的。也就是说,一次转移一大块信息而不是一字节信息(通常至少512字节)。
例如,当程序读取文件时,一块数据被拷贝到缓冲区(一块中介存储区域)。这种缓冲极大地提高了数据传输速率。程序可以检查缓冲区中的字节。缓冲在后台处理,所以让人有逐字符访问的错觉(如果使用底层I/O
,要自己完成大部分工作)。
/* 示例:使用标准 I/O */
#include <stdio.h>
#include <stdlib.h> // 提供 exit()的原型
int main(int argc, char *argv []){
int ch; // 读取文件时,储存每个字符的地方
FILE *fp; // 文件指针
unsigned long count = 0;
if (argc != 2){
printf("Usage: %s filename\n", argv[0]);
exit(EXIT_FAILURE);
}
if ((fp = fopen(argv[1], "r")) == NULL){
printf("Can't open %s\n", argv[1]);
exit(EXIT_FAILURE);
}
while ((ch = getc(fp)) != EOF){
putc(ch, stdout); // 与 putchar(ch); 相同
count++;
}
fclose(fp);
printf("File %s has %lu characters\n", argv[1], count);
return 0;
}
上述程序检查argc
的值,查看是否有命令行参数。如果没有,程序将打印一条消息并退出程序。
字符串 argv[0]
是该程序的名称。显式使用 argv[0]
而不是程序名,错误消息的描述会随可执行文件名的改变而自动改变。这一特性在像 UNIX 这种允许单个文件具有多个文件名的环境中也很方便。但是,一些操作系统可能不识别argv[0]
,所以这种用法并非完全可移植。
exit()
函数关闭所有打开的文件并结束程序。exit()
的参数被传递给一些操作系统,包括 UNIX、Linux、Windows和MS-DOS,以供其他程序使用。
通常的惯例是:正常结束的程序传递0
,异常结束的程序传递非零值。不同的退出值可用于区分程序失败的不同原因,这也是UNIX和DOS编程的通常做法。但是,并不是所有的操作系统都能识别相同范围内的返回值。因此,C 标准规定了一个最小的限制范围。尤其是,标准要求0
或宏EXIT_SUCCESS
用于表明成功结束程序,宏EXIT_FAILURE
用于表明结束程序失败。这些宏和exit()
原型都位于stdlib.h
头文件中。
根据ANSI C的规定,在最初调用的main()
中使用return
与调用exit()
的效果相同。因此,在main(),return 0;
和exit(0);
作用相同。
但是要注意,我们说的是“最初的调用”。如果main()
在一个递归程序中,exit()
仍然会终止程序,但是return
只会把控制权交给上一级递归,直至最初的一级。然后return
结束程序。return
和exit()
的另一个区别是,即使在其他函数中(除main()
以外)调用exit()
也能结束整个程序。
17.2.1fopen()
函数
上述程序使用fopen()
函数打开文件。该函数声明在stdio.h
中。它的第1个参数是待打开文件的名称,更确切地说是一个包含该文件名的字符串地址。第 2 个参数是一个字符串,指定待打开文件的模式。
模式字符串 | 说明 |
---|---|
r |
以只读方式打开文件,该文件必须存在。 |
w |
以只写方式打开文件,若文件存在则文件长度清为零,即该文件内容会消失;若文件不存在则创建该文件。 |
r+ |
以读/写方式打开文件,该文件必须存在。 |
rb 、wb 、ab 、ab+ 、a+b 、wb+ 、w+b 、ab+ 、a+b |
以读/写方式打开一个二进制文件,只允许读/写数据。 |
rt+ |
以读/写方式打开一个文本文件,允许读和写。 |
w+ |
以可读/写方式打开文件,若文件存在则文件长度清为零,即该文件内容会消失;若文件不存在则创建该文件。 |
a |
以附加的方式打开只写文件。若文件不存在,则会创建该文件;如果文件存在,则写入的数据会被加到文件尾后,即文件原先的内容会被保留(EOF 符保留)。 |
a+ |
以附加方式打开可读/写的文件。若文件不存在,则会创建该文件,如果文件存在,则写入的数据会被加到文件尾后,即文件原先的内容会被保留(EOF 符不保留)。 |
wx 、wbx 、w+x 、wb+x 、w+bx |
C11 新增的写模式 |
像UNIX和Linux这样只有一种文件类型的系统,带b字母的模式和不带b字母的模式相同。
新的C11
新增了带x字母的写模式,与以前的写模式相比具有更多特性。
第一,如果以传统的一种写模式打开一个现有文件,fopen()
会把该文件的长度截为 0,这样就丢失了该文件的内容。但是使用带 x字母的写模式,即使fopen()
操作失败,原文件的内容也不会被删除。
第二,如果环境允许,x模式的独占特性使得其他程序或线程无法访问正在被打开的文件。
如果使用任何一种"w"模式(不带x字母)打开一个现有文件,该文件的内容会被删除,以便程序在一个空白文件中开始操作。然而,如果使用带x字母的任何一种模式,将无法打开一个现有文件。
程序成功打开文件后,fopen()
将返回文件指针(file pointer)
,其他I/O
函数可以使用这个指针指向该文件。文件指针的类型是指向FILE的指针,FILE是一个定义在stdio.h
中的派生类型。文件指针并不指向实际的文件,它指向一个包含文件信息的数据对象,其中包含操作文件的I/O
函数所用的缓冲区信息。因为标准库中的I/O
函数使用缓冲区,所以它们不仅要知道缓冲区的位置,还要知道缓冲区被填充的程度以及操作哪一个文件。标准I/O
函数根据这些信息在必要时决定再次填充或清空缓冲区。
17.2.2getc()
和putc
函数
getc()
和putc()
函数与getchar()
和putchar()
函数类似。所不同的是,要告诉getc()
和putc()
函数使用哪一个文件。
ch = getchar();
这条语句的意思是“从标准输入中获取一个字符”:
ch = getc(fp);
这条语句的意思是“从fp
指定的文件中获取一个字符”:
putc(ch, fpout);
这条语句的意思是“把字符ch
放入FILE指针fpout
指定的文件中”:
在putc()
函数的参数列表中,第1个参数是待写入的字符,第2个参数是文件指针。
17.2.3 文件结尾
计算机操作系统要以某种方式判断文件的开始和结束。检测文件结尾的一种方法是,在文件末尾放一个特殊的字符标记文件结尾。CP/M、IBM、DOS和MS-DOS
的文本文件曾经用过这种方法。如今,这些操作系统可以使用内嵌的Ctrl+Z
字符来标记文件结尾。这曾经是操作系统使用的唯一标记,不过现在有一些其他的选择,例如记录文件的大小。所以现代的文本文件不一定有嵌入的Ctrl+Z
,但是如果有,该操作系统会将其视为一个文件结尾标记。
操作系统使用的另一种方法是储存文件大小的信息。如果文件有3000字节,程序在读到3000字节时便达到文件的末尾。MS-DOS
及其相关系统使用这种方法处理二进制文件,因为用这种方法可以在文件中储存所有的字符,包括Ctrl+Z
。新版的DOS
也使用这种方法处理文本文件。UNIX使用这种方法处理所有的文件。
无论操作系统实际使用何种方法检测文件结尾,在C语言中,用getchar()
读取文件检测到文件结尾时将返回一个特殊的值,即EOF(end of file的缩写)
。scanf()
函数检测到文件结尾时也返回EOF
。通常, EOF
定义在stdio.h
文件中:#define EOF (-1)
。为什么是-1?因为getchar()
函数的返回值通常都介于0~127
,这些值对应标准字符集。但是,如果系统能识别扩展字符集,该函数的返回值可能在0~255
之间。无论哪种情况,-1
都不对应任何字符,所以,该值可用于标记文件结尾。某些系统也许把EOF
定义为-1
以外的值,但是定义的值一定与输入字符所产生的返回值不同。如果包含stdio.h
文件,并使用EOF
符号,就不必担心EOF
值不同的问题。这里关键要理解EOF
是一个值,标志着检测到文件结尾,并不是在文件中找得到的符号。
那么,如何在程序中使用EOF
?把getchar()
的返回值和EOF
作比较。如果两值不同,就说明没有到达文件结尾。也就是说,可以使用下面这样的表达式:
while ((ch = getchar()) != EOF)
如果正在读取的是键盘输入不是文件会怎样?绝大部分系统(不是全部)都有办法通过键盘模拟文件结尾条件。
/*重复输入,直到文件结尾 */
#include <stdio.h>
int main(void){
int ch;
while ((ch = getchar()) != EOF)
putchar(ch);
return 0;
}
注意下面几点:
-
不用定义
EOF
,因为stdio.h
中已经定义过了。 -
不用担心
EOF
的实际值,因为EOF
在stdio.h
中用#define
预处理指令定义,可直接使用,不必再编写代码假定EOF
为某值。 -
变量
ch
的类型从char
变为int
,因为char
类型的变量只能表示0~255
的无符号整数,但是EOF
的值是-1
。getchar()
函数实际返回值的类型是int
,所以它可以读取EOF
字符。如果实现使用有符号的char
类型,也可以把ch
声明为char
类型,但最好还是用更通用的形式。由于getchar()
函数的返回类型是int
,如果把getchar()
的返回值赋给char
类型的变量,一些编译器会警告可能丢失数据。ch
是整数不会影响putchar()
,该函数仍然会打印等价的字符。 -
使用该程序进行键盘输入,要设法输入
EOF
字符。不能只输入字符EOF
,也不能只输入-1
(输入-1
会传送两个字符:一个连字符和一个数字1)。正确的方法是,必须找出当前系统的要求。例如,在大多数UNIX
和Linux
系统中,在一行开始处按下Ctrl+D
会传输文件结尾信号。许多微型计算机系统都把一行开始处的Ctrl+Z
识别为文件结尾信号,一些系统把任意位置的Ctrl+Z
解释成文件结尾信号。 -
每次按下
Enter
键,系统便会处理缓冲区中储存的字符,并在下一行打印该输入行的副本。这个过程一直持续到以UNIX
风格模拟文件结尾(按下Ctrl+D
)。在PC中,要按下Ctrl+Z
。
17.2.4fclose()
函数
fclose(fp)
函数关闭fp
指定的文件,必要时刷新缓冲区。对于较正式的程序,应该检查是否成功关闭文件。如果成功关闭,fclose()
函数返回0
,否则返回EOF
:
if (fclose(fp) != 0)
printf("Error in closing file %s\n", argv[1]);
如果磁盘已满、移动硬盘被移除或出现I/O
错误,都会导致调用fclose()
函数失败。
17.2.5指向标准文件的指针
stdio.h
头文件把3个文件指针与3个标准文件相关联,C程序会自动打开这3个标准文件。如表所示:
标准文件 | 文件指针 | 通常使用的设备 |
---|---|---|
标准输入 | stdin |
键盘 |
标准输出 | stdout |
显示器 |
标准错误 | stderr |
显示器 |
这些文件指针都是指向FILE的指针,所以它们可用作标准I/O
函数的参数,如fclose(fp)
中的fp
。
17.2.6fprintf()
和fscanf()
函数
/* addaword.c -- 使用 fprintf()、fscanf() 和 rewind() */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX 41
int main(void){
FILE *fp;
char words[MAX];
if ((fp = fopen("wordpsy", "a+")) == NULL){
fprintf(stdout, "Can't open \"wordy\" file.\n");
exit(EXIT_FAILURE);
}
puts("Enter words to add to the file; press the #");
puts("key at the beginning of a line to terminate.");
while ((fscanf(stdin, "%40s", words) == 1) && (words[0] != '#'))
fprintf(fp, "%s\n", words);
puts("File contents:");
rewind(fp); /* 返回到文件开始处 */
while (fscanf(fp, "%s", words) == 1)
puts(words);
puts("Done!");
if (fclose(fp) != 0)
fprintf(stderr, "Error closing file\n");
return 0;
}
该程序可以在文件中添加单词。使用"a+"
模式,程序可以对文件进行读写操作。首次使用该程序,它将创建wordpsy
文件,以便把单词存入其中。
随后再使用该程序,可以在wordpsy
文件后面添加单词。虽然"a+"
模式只允许在文件末尾添加内容,但是该模式下可以读整个文件。
rewind()
函数让程序回到文件开始处,方便while
循环打印整个文件的内容。注意,rewind()
接受一个文件指针作为参数。
17.2.7fgets()
和fputs()
函数
fgets()
一个调用fgets()
函数的例子:fgets(buf, STLEN, fp);
- 第1个参数是表示储存输入位置的地址(
char * 类型
); - 第2个参数是一个整数,表示待输入字符串的大小 ;
- 最后一个参数是文件指针,指定待读取的文件。
buf
是char
类型数组的名称,STLEN
是字符串的大小,fp
是指向FILE的指针。
fgets()
函数读取输入直到第 1 个换行符的后面,或读到文件结尾,或者读取STLEN-1
个字符(以上面的 fgets()
为例)。然后,fgets()
在末尾添加一个空字符使之成为一个字符串。字符串的大小是其字符数加上一个空字符。如果fgets()
在读到字符上限之前已读完一整行,它会把表示行结尾的换行符放在空字符前面。fgets()
函数在遇到EOF
时将返回NULL
值,可以利用这一机制检查是否到达文件结尾;如果未遇到EOF
则之前返回传给它的地址。
fputs()
一个调用fputs()
函数的例子:fputs(buf, fp);
fputs()
函数接受两个参数:第1个是字符串的地址;第2个是文件指针。
该函数根据传入地址找到的字符串写入指定的文件中。和 puts()
函数不同,fputs()
在打印字符串时不会在其末尾添加换行符。
由于fgets()
保留了换行符,fputs()
就不会再添加换行符。
ANSI标准库的标准I/O
系列有几十个函数,不一一列举。
十八、C预处理
- C预处理器在程序编译之前查看程序。
- 根据程序中的预处理指令,预处理器把符号缩写替换成其表示的内容。
- 预处理器可以包含程序所需的其他文件,可以选择让编译器查看哪些代码。
- 预处理器并不知道C,它的工作是把一些文本转换成另一些文本。
18.1 翻译程序的第一步
在预处理之前,编译器必须对程序进行一些翻译处理。
- 首先,编译器把源代码中出现的字符映射到源字符集。
- 第二,编译器定位每个反斜杠后面跟着换行符的示例,并删除它们。
printf("aaaa\
aaaa");
转换成一个逻辑行
printf("aaaaaaaa");
注意:在这次场合中,删除的“换行符”是通过Enter键在源代码中换行所生成的字符,而不是转义字符。
由于预处理表达式的长度必须是一个逻辑行,所以这一步为预处理器做好了准备工作,一个逻辑行可以是多个物理行。
- 第三,编译器把文本划分程预处理记号序列、空白序列和注释序列,记号是由空格、制表符或换行符分隔的项,编译器将用一个空格字符替换每一条注释。而且,实现可以用一个空格替换所有的空白字符序列(不包括换行符)。
- 最后,程序已经准备好进入处理阶段,预处理器查找一行中以 # 号开始的预处理指令。
18.2 明示常量 #define
每行#define(逻辑行)都由3部分组成。
- 第一部分是#define指令本身。
- 第二部分是选定的缩写,也成为宏。有些宏代表值,这些宏被称为类对象宏。此外还有类函数宏。宏的名字中间不能有空格,而且必须遵守C变量的命名规则:只能使用字符、数字、下划线,而且首字符不能是数字。
- 第三部分是指令的其余部分,称为替换列表或替换体,一旦预处理器在程序中找到宏的示实例后,就会用替换体代替该宏。从宏变为最终替换文本的过程称为宏展开。
18.3 在#define中使用参数
- 在#define中使用参数可以创建外形和作用与函数类似的类函数宏。
- 带有参数的宏看上去很像函数,因为这样的宏也使用圆括号。
- 类函数宏定义的圆括号中可以有一个或多个参数,随后这些参数出现在替换体中。
#define MEAN(X,Y) ((X+Y)/2)
在程序中使用类函数宏像函数调用,但是它的行为和函数调用完全不同。
#include <stdio.h>
#define SQUARE(X) X*X//SQUARE 是宏标识符,SQUARE(X)中的 X 是宏参数,X*X 是替换列表
#define PR(X) printf("The result is %d.\n", X)//这里用宏参数创建了输出字符串
int main(void) {
int x = 5;
int z;
printf("x = %d\n", x);
z = SQUARE(x);
printf("Evaluating SQUARE(x): ");
PR(z);
z = SQUARE(2);
printf("Evaluating SQUARE(2): ");
PR(z);
printf("Evaluating SQUARE(x+2): ");
PR(SQUARE(x + 2));
/*预处理器不做计算、不求值,只替换字符序列,预处理器把出现x的地方都替换成x+2。
因此,x*x变成了x+2*x+2,x为5,5+2*5+2 = 5 + 10 + 2 = 17。*/
printf("Evaluating 100/SQUARE(2): ");
PR(100 / SQUARE(2));
printf("x is %d.\n", x);
printf("Evaluating SQUARE(++x): ");
PR(SQUARE(++x));
printf("After incrementing, x is %x.\n", x);
return 0;
}
- 函数调用在程序运行时把参数的值传递给函数。
- 宏调用在编译之前把参数记号传递给程序。因此,必要时要使用足够多的圆括号来确保运算和结合的正确顺序。如:
#define SQUARE(x) ((x)*(x))
18.4 用宏参数创建字符串:#运算符
例,如果x是一个宏形参,那么#x就是转换为字符串"x"的形参名。这个过程称为字符串化 。
#include <stdio.h>
#define PSQR(x) printf("The square of " #x " is %d.\n",((x)*(x)))
int main(void) {
int y = 5;
PSQR(y);//"y"替换#x
PSQR(2 + 4);//"2 + 4"替换#x
return 0;
}
18.5预处理器黏合剂:##运算符
##运算符把两个记号组合成一个记号
#include <stdio.h>
#define XNAME(n) x ## n
#define PRINT_XN(n) printf("x" #n " = %d\n", x ## n);
int main(void) {
int XNAME(1) = 14; // 变成 int x1 = 14;
int XNAME(2) = 20; // 变成 int x2 = 20;
int x3 = 30;
PRINT_XN(1); // 变成 printf("x1 = %d\n", x1);
PRINT_XN(2); // 变成 printf("x2 = %d\n", x2);
PRINT_XN(3); // 变成 printf("x3 = %d\n", x3);
return 0;
}
18.6 变参宏:...
和_ _VA_ARGS_ _
- 一些函数(如 printf())接受数量可变的参数。
- 通过把宏参数列表中最后的参数写成省略号(即,3个点
...
)来实现这一功能。 _ _VA_ARGS_ _
可用在替换部分中,表明省略号代表什么。
#define PR(...) printf(_ _VA_ARGS_ _)
假设稍后调用该宏:
PR("Howdy");
PR("weight = %d, shipping = $%.2f\n", wt, sp);
展开后的代码是:
printf("Howdy");
printf("weight = %d, shipping = $%.2f\n", wt, sp);
例
#include <stdio.h>
#include <math.h>
#define PR(X, ...) printf("Message " #X ": " __VA_ARGS__)
int main(void) {
double x = 48;
double y;
y = sqrt(x);
PR(1, "x = %g\n", x);
//展开后成为:print("Message " "1" ": " "x = %g\n", x);
//简化为:print("Message 1: x = %g\n", x);
PR(2, "x = %.2f, y = %.4f\n", x, y);
return 0;
}
省略号...
只能代替最后的宏参数,不能代替中间的部分参数。
#define WRONG(X, ..., Y) #X #_ _VA_ARGS_ _ #y //不能这样做
18.7文件包含:#include
- 当预处理器发现#include 指令时,会查看后面的文件名并把文件的内容 包含到当前文件中,即替换源文件中的#include指令。这相当于把被包含文件的全部内容输入到源文件#include指令所在的位置。
- #include指令有两种形式:
#include <stdio.h> //文件名在尖括号中
#include "mystuff.h" //文件名在双引号中
在 UNIX 系统中,
-
尖括号告诉预处理器在标准系统目录中查找该文件。
-
双引号告诉预处理器首先在当前目录中(或文件名中指定的其他目录)查找该文件,如果未找到再查找标准系统目录
#include <stdio.h> //查找系统目录
#include "hot.h" //查找当前工作目录
#include "/usr/biff/p.h" //查找/usr/biff目录
- 集成开发环境(IDE)也有标准路径或系统头文件的路径。
- 许多集成开发环境提供菜单选项,指定用尖括号时的查找路径。
- 在 UNIX 中,使用双引号意味着先查找本地目录,但是具体查找哪个目录取决于编译器的设定。
- 有些编译器会搜索源代码文件所在的目录,有些编译器则搜索当前的工作目录,还有些搜索项目文件所在的目录。
为什么要包含文件?
因为编译器需要这些文件中的信息。例如,stdio.h 文件中通常包含EOF、NULL、getchar()和 putchar()的定义。getchar()和 putchar()被定义为宏函数。此外,该文件中还包含C的其他I/O函数。 C语言习惯用.h后缀表示头文件,这些文件包含需要放在程序顶部的信息。头文件经常包含一些预处理器指令。有些头文件(如stdio.h)由系统提供,也可以创建自己的头文件。 包含一个大型头文件不一定显著增加程序的大小。在大部分情况下,头文件的内容是编译器生成最终代码时所需的信息,而不是添加到最终代码中的材料。
18.8 使用头文件
头文件中最常用的形式如下:
- 明示常量——例如,stdio.h中定义的EOF、NULL和BUFSIZE(标准I/O 缓冲区大小)。
- 宏函数——例如,getc(stdin)通常用getchar()定义,而getc()经常用于定义较复杂的宏,头文件ctype.h通常包含ctype系列函数的宏定义。 、
- 函数声明——例如,string.h头文件(一些旧的系统中是strings.h)包含字符串函数系列的函数声明。在ANSI C和后面的标准中,函数声明都是函数原型形式。
- 结构模版定义——标准I/O函数使用FILE结构,该结构中包含了文件和与文件缓冲区相关的信息。FILE结构在头文件stdio.h中。
- 类型定义——标准 I/O 函数使用指向 FILE 的指针作为参数。通常,stdio.h 用#define 或typedef把FILE定义为指向结构的指针。类似地,size_t和 time_t类型也定义在头文件中。
- 使用头文件声明外部变量供其他文件共享。
- #include和#define指令是最常用的两个C预处理器指令,还有一些其他指令。