- C语言笔记
- 第二章 常量、变量及数据类型
- 第三章 运算符与表达式
- 第四章 输入输出操作管理
- 第五章 判断与分支
- 第六章 判断与循环
- 第 7 章 数组
- 第8章 字符数组和字符串
- 第 9 章 用户自定义函数
- 第 10 章 结构体和共用体
- 第 11 章 指针
- 第 12 章 文件管理
- 第 13 章 动态内存分配与链表
C语言笔记
第二章 常量、变量及数据类型
2.2 字符集
C语言的字符集包括:字母、数字、特殊字符和空格
2.3 C标记符
在C语言中最小的单元称为C标记符
C语言标记符包括
- 关键字: float、while
- 标识符(给函数和变量起名): main、amount
- 常量: -15.9、100
- 字符串: “ABC”、“year”
- 运算符:
+
、-
2.4 关键字与标识符
所有关键字都有固定的含义,且其含义不可改变
标识符是变量名、函数和数组名。是自定义的名称
标识符的规则
- 第一个字符必须是字母或者下划线
- 只能由字母、数组或下划线组成
- 只有头31个字符是有效的(标识符最长31个字符)
- 不能使用关键字
- 不能包含空格
2.5 常量
C语言的常量是指固定值,在程序的运行中不能修改
常量的类型
- 数值常量
- 整型常量
- 实数常量
- 字符常量
- 单字符常量
- 字符串常量
2.5.1 整型常量
整型常量有三种类型
- 十进制
- 由0-9的数字组成,前面可加
-
和+
- 例如: 123、0、-312
- 由0-9的数字组成,前面可加
- 十六进制
- 数字前加0x或0X的数字,由0-9及A-F或a-f组成
- 例如: 0x2、0x9f
- 八进制
- 由0-7的数字组成,且由0开头
- 例如: 02、027
补充: 二进制: 0b1100
可以给U、L和UL修饰符,U代表无符号整数,L代表长整数
2.5.2 实数常量
由十进制标识,且后面跟有一个小数点和小数部分,例如: 0.002、-0.79、215.、.96
也可用指数表示法,例如:0.65e4、12e2等
浮点常量通常为双精度数,但是后缀f或F用于强制转换为单精度数
2.5.3 单字符常量
单字符常量是用一对单引号括起来的单个字符,例如: ‘X’、’:'等
字符常量具有ASCII整数值,可以对字符常量进行算术操作
2.5.4 字符串常量
字符串常量是用双引号括起来的一系列字符,例如: “hello”、"2014"等
2.5.5 反斜杠字符常量
符号’\n’为换行符
2.6 变量
变量是用来保存数据值的数据名
变量命名规则
- 必须以字母开头,也可以下划线开头
- ANSI标准只识别前31个字符
- 区分大小写
- 不能是关键字
- 不允许使用空格
2.7 数据类型
C语言支持三种数据类型
- 基本数据类型
- 派生数据类型
- 自定义数据类型
五种基本数据类型
-
整形(
int
)- 十六位计算机的整形为-32768-+32767,32位为-2147483648-+2147483647
short int
、int
、long int
- 还可将其声明为
unsigned int
-
浮点型(
float
)与双精度浮点型(double
)float
、double
、long double
-
void类型
- void类型没有数值
- 当函数不返回值时定义为void
- 它还可以起一般作用,用于表示其他各种标准类型
-
字符类型(
char
)- 单字符定义为字符类型,字符通常用8位来保存
signed
和unsigned
可用于字符
2.8 变量的声明
在设计了适当的变量名之后,需要进行变量声明。声明完成两件事
- 告诉编译器变量名是什么
- 指定变量的数据类型
2.8.1 基本类型的声明
声明变量的语法为: data-type v1,v2,v3;
目前的规则允许在函数或代码块的任何位置声明变量,但必须在使用前声明。之前的规则中,只允许在函数或代码块的开头位置声明变量
2.8.2 自定义类型的声明
Typedef type indentifier
用于将一个已有的数据类型,赋予一个新的名称。type
为已有的数据类型,identifier
是赋给该数据类型的新名字
typedef
的主要优点是可创建具有意义的数据类型名,从而提高程序的可读性
枚举类型
1 | enum day{Monday, Tuesday, ..., Sunday}; |
编译器自动将整数赋予给所有枚举常量,如上程序,0赋给Monday
,1赋给Tuesday
2.9 存储类型的声明
全局变量与局部变量
- 全局变量: 可以在程序的所有函数中使用,并不需要在其他函数中再进行声明
- 局部变量:只有再定义它的函数中可见且有意义
C语言提供了一些存储类型标识符,可以显式地声明变量的作用域和生存期
- auto: 局部变量,直在声明它的函数中有效
- static: 静态变量,不多解释,C中比较复杂(https://blog.csdn.net/keyeagle/article/details/6708077)
- extern: 全局变量,对文件中的所有函数都有效
- register: 局部变量,存储在寄存器中
2.10 变量的赋值
使用=
运算符可以将数值赋给变量: variable_name = constant
可以直接在变量声明时进行赋值: data-type variable_name = constant;
scanf()
函数用来从键盘中读取输入的数据: scanf("%d", &number)
2.11 符号常量的定义
1 |
|
#define
语句是一条预处理编译器指令,它可以位于程序的任何位置,但是必须位于使用它的语句之前
2.12 其他
const
用来将变量声明为常量,在定义之后,这种变量无法再被修改
volatile
用于告诉编译器,变量的值可能随时被其他外部因素修改。每次碰到该变量时,编译器都会检测该变量的值是否被外部因素修改过
第三章 运算符与表达式
3.1 概述
运算符是一种符号,它告诉计算机执行某些数学或逻辑操作
运算符分为
- 算术运算符
- 关系运算符
- 逻辑运算符
- 赋值运算符
- 递增和递减运算符
- 条件运算符
- 逐位运算符
- 特殊运算符
表达式是操作数和运算符的组成系列,最终产生一个单独的值
3.2 算术运算符
+
、-
、*
、\
、%
3.3 关系运算符
<
、>
、!=
、==
等
关系表达式为真,则值为1;为假,则值为0
3.4 逻辑运算符
&&
、||
、!
3.5 赋值运算符
=
赋值语句的返回值,等于其右边表达式的值
1 |
|
上程序将返回2
3.6 递增和递减运算符
++
和 --
对于++m
和m++
,当单独使用时,意义相同
再赋值语句中,意义不同
y = ++m
,先++
再赋值y = m++
,先赋值再++
++
和--
的优先级和关联性与一元的+
和-
相同
3.7 条件运算符
exp1 ? exp2: exp3
1 | a = 10; |
3.8 逐位运算符
用于对数据的位进行操作,可将位左移或者右移
逐位运算符不能用于float
或double
&
逐位与、|
逐位或、^
逐位异或、>>
左移位、<<
右移位
3.9 特殊运算符
3.9.1 逗号运算符
逗号运算符用于将相关的表达式链接在一起。由于逗号链接的表达式是从左到右计算的,因此最右边的表达式的值即为该组合表达式的值
1 | int main(){ |
逗号运算符具有最低的优先级
3.9.2 sizeof运算符
sizeof是编译时运算符,当用于操作数的时候,返回操作数所占的字节数
1 | m = sizeof(sum); |
3.10 算术表达式
算数表达式就是变量、常量和运算符按C语言的语法组成的组合
1 | a * b - c |
3.11 表达式的计算
语法: variable = expression;
在计算之前,表达式中的所有变量都必须已经赋值
3.12 算术表达式的优先级
- 首先,加括号的子表达式从左到右进行计算
- 如果括号有嵌套,应从最里面的子表达式开始计算
- 在计算子表达式时,优先规则用于确定运算符的使用顺序
- 当两个或多个同等优先级的运算符出现在子表达式中时,应用关联规则
- 算术表达式使用优先级规则从左到右计算
- 当使用括号时,括号中的表达式为最高优先级
3.14 表达式中的类型转换
3.14.1 隐式类型转换
C语言允许在表达式中混合使用不同类型的常量和变量。C语言主动将所有中间值转换为正确的类型,这样就可以确保计算正确而不丢失任何内容。这种自动转换称为隐式类型转换
如果操作数是不同的类型,那么在运算进行之前,“较低”类型自动转换为“较高”类型
3.14.2 显示类型转换
有时我们可能想按与自动转换不同的方式来进行强制转换
1 | ratio = (float)female_number/male_number; |
运算符(float)无法影响变量female_number的值,而且在程序的其他地方,female_number的类型仍为int
3.15 运算符的优先级及其关联性
优先级
- C语言的每个运算符都有与之关联的优先级。
- 优先级用于确定含有多个运算符的表达式是如何进行计算的。较高优先级的运算符先运算
关联性
- 用来确定多个同级运算符应用的顺序
第四章 输入输出操作管理
4.1 概述
输入输出
- C语言的语法中没有任何内置的输入\输出语句。所有IO都是通过诸如
printf
和scanf
完成的
#include
指令
- 一种预编译指令
- 每个使用了标准输入输出函数的程序都必须在程序的开始处包含如下语句
#include <stdio.h>
- 对于
printf
和scanf
函数没有必要包含上述语句,因为它们已被定义为C的一部分 - 文件
stdio.h
是标准输入输出头文件的缩写。指令#include<stdio.h>
告诉编译器去搜索名为stdio.h
的文件,并将其内容放入到程序中。编译后,头文件的内容就变成了源代码的一部分
4.2 读取一个字符
读取某个字符可以通过函数getchar()
来完成,语法为:variable_name = getchar();
在ctype.h
头文件中,包含许多字符测试函数,例如isdigit
用于判断字符是否为数字
4.3 写一个字符
写字符可以通过函数putchar()
来完成,语法为:putchar(variable_name);
1 | char answer = 'Y'; |
4.4 格式化输入
格式化输入是指输入数据已按特定格式排列好了
scanf函数的一般形式为
1 | scanf("控制字符串", arg1, arg2, ..., argn); |
- 控制字符串用于指定数据输入的格式
- 而
arg1
,arg2
指数据的保存地址
控制字符串包含格式说明符,用于指定输入数据的格式转换说明,可以包括
- 格式说明符,包含
- 转换字符
%
- 字符宽度说明符
- 数据类型字符
- 例如,
%1d
,说明输入格式为宽度为1的整数
- 转换字符
- 空白符、制表符或换行符
4.4.1 整数输入
1 | "% w sd" |
w
为一个整数指定要读取的数字的字段宽度s
可以为字母l
或者字母h
,是可省略的,代表长整数和短整数d
为数据类型字符
另外,通过在字段的宽度说明符位置,指定为*
,就可跳过输入字段
1 | scanf("%d %*d %d",&a, &b) |
- 若输入数据为
123 456 789
,则123赋给a,456被忽略, 789赋给b
4.4.2 实数输入
与整数不同,实数的字段宽度不用指定,因而scanf
函数只需要用简单的字段说明符%f
来读取实数,可用十进制小数或指数形式来表示实数
如果要读取的数字为double
类型,那么字段说明符应为%lf
%*f
可用于忽略字段
4.4.3 字符串输入
字段说明符可以为%ws
或者%wc
- 当使用
%ws
时,w
为指定长度,若指定长度,则系统将一直等待,直到第w个字符被键入 - 当使用
%wc
时,任何字符都会被键入,直到输入字符串达到w
长度为止 - 使用
%[^\n]
时,可以直接实现直接键入new york
的效果
4.4.4 混合数据类型的读取
可以使用一条scanf
语句来读取含有多种数据类型的一行数据,此时应确保输入的数据项与控制说明符的顺序和类型相匹配。如果试图读取不匹配的项,scanf
函数将不再进一步读取任何数据,并立刻返回已读取的值
1 | scanf("%d %c %f %s", &count, &code, &ratio, name); |
4.4.5 错误输入的检测
scanf
函数的返回值为int
类型,将返回已成功读取的项数,可用于检测在读取输入时是否有错误发生
当试图为int
变量读取实数时,将把实数的整数部分赋给变量,截取后的小数部分则赋给下一个变量
4.4.6 使用scanf函数时的几个要点
I/O历程并不是c语言的组成部分,而是c库函数的单独模块或者操作系统的一部分
scanf
的几个注意要点
- 函数的所有参数,除控制字符串外,都必须是指向变量的指针
- 控制字符串中包含的格式说明符应依次与相应的参数匹配
- 输入数据项必须用空格分隔开,并且必须按相同的顺序与接受输入的变量匹配
- 当
scanf
遇到不匹配的数据或者不合法的字符,读取会停止 - 一行中任何未读的数据项都被认为是下一个scanf函数的数据输入行
4.5 格式化输出
printf
语句提供某些特性,能有效地用来控制在终端显示的对齐方式和间距
printf
的一般形式为
1 | printf("控制字符串", arg1, arg2, ..., argn); |
控制字符串的组成
- 显示在屏幕上的字符的外观
- 用于定义每项显示的输入格式的格式说明符
- 转义序列字符,如
\n
、\t
和\b
等
注意,printf
不能自动换行,因此多条printf
语句产生的输出将显示在同一行中。利用\n
可以实现换行
4.5.1 整数的输出
用于显示整数的格式说明符为%wd
,其中w
指定输出的最小字段宽度
通过在%
字符后面放置一个减号,就可以强制使显示输出左对齐
在字段宽度说明符之前加一个0
,可以使得输出结果的前面用零来填充
1 | printf("%06d", 9876); |
4.5.2 实数的输出
用于显示实数的格式说明符为%w.pf
,该值将被圆整为p个小数位,并在列宽为w的区域内以右对齐的方式显示
也可以用指数的形式来显示实数%w.pe
一些系统还支持使用特殊的字段说明符,让用户在运行程序时定义字段的大小,形式如下
1 | printf("%*.*f", width, precision, number); |
4.5.3 单个字符的显示
用于显示单个字符的格式字符串为%wc
,字符将以右对齐的方式显示在列宽为w
的区域内。在整数w
之前加负号,则以左对齐的方式显示。w
默认为1
4.5.4 字符串的显示
用于显示字符串的格式说明符为%w.ps
,w
指定显示的区域宽度,p
表示只显示字符串的前p
个字符,并且右对齐
第五章 判断与分支
5.1 概述
在很多情况中必须基于某些条件来改变语句的执行顺序,或反复执行一组语句,直到满足某些特定的条件。
如下语句具有判断的能力,也称为控制语句
if
语句switch
语句- 条件运算符语句
goto
语句
5.2 if语句
if
语句是含有两条分支的判断语句,附带一个表达式,形式如下
1 | if(判断表达式) |
首先计算判断表达式,然后根据表达式的值是真还是假,将控制权转换到特定的语句
5.3 简单if语句
形式如下
1 | if(判断表达式){ |
statement-block
可以是一条或者一组语句- 如果判断表达式为真,将执行
statement-block
语句,否则将跳过statement-block
语句 - 无论如何,都会执行
statement-x
语句
5.4 if…else语句
形式如下
1 | if(判断表达式){ |
- 如果判断表达式为真,就执行紧跟
if
语句的true-block
,否则执行false-block
- 在这两种情况下,接下来控制权都将被转移到
statemnt-x
语句
5.5 嵌套if…else语句
在C中,一条else语句总是与最近的未终止的if匹配
5.6 阶梯式else if语句
形式如下
1 | if(condition-1) |
- 条件是从上往下计算的
- 只要发现有一个为真的条件,就执行与之相关的语句,然后将控制权转移到
statement-x
语句 - 如果所有条件都为假,就执行最后那个含有
default-statement
的else
语句
5.7 switch语句
c内置名为switch
的多路判断语句。switch
语句把给定变量(或表达式)的值与一个case
值列表进行比较,如果发现有case
与之匹配,就执行与该case
相关的语句块
形式如下
1 | switch(expression){ |
expression
可以是整数表达式或者字符value
为常量或常量表达式- 当运行
switch
语句时,expression
的值与value-1
、value-2
的值进行比较,如果发现某个case
的值与expression
的值匹配,就执行该case
后边的语句块 - 每个语句块的末尾是
break
语句。它标志着该case
的结尾,并使控制权从该switch
语句中退出来,转移到switch
后面的statement-x
语句
5.8 ?:运算符
条件运算符使用的格式为
1 | 条件表达式? 表达式1: 表达式2 |
5.9 goto语句
c
也支持goto
语句,用于无条件地从程序的一处跳转到另一处
goto
语句要求有个标签,来标识要跳转的未知。一个标签就是一个合法的变量名,后面必须跟一个冒号
1 | goto label; |
第六章 判断与循环
6.1 概述
程序循环由两部分组成,一部分称为循环体,另一部分称为控制语句。控制语句测试某些条件,然后指示包含在循环体中的语句反复执行
入口控制循环和出口控制循环
- 入口控制循环中,控制条件在循环执行开始之前测试。如果条件不满足,将不执行循环体
- 出口控制循环中,测试是在循环体的末尾进行的,因此循环体的第一次执行是无条件的
无限循环: 由于某些原因不能把控制权转移出循环,因此形成无限循环
循环处理过程
- 设置并初始化条件变量
- 执行循环体中的语句
- 用指定的值来测试条件变量,从而决定是否再次执行循环体
- 递增并更新条件变量的值
C的三种循环结构
- while语句
- do语句
- for语句
计数器控制循环和始终标记控制循环
-
如果知道循环将执行的确切次数,就使用计数器控制循环,例如
1
2
3
4while( n <= 10){
sum = sum + n;
n++;
}这是计数器控制循环,变量
n
称为计数器 -
在始终标记控制循环中,使用一个称为始终数值的特殊值来改变循环控制表达式的值,在循环结束之前,并不知道
循环重复的次数1
2
3
4while(c != 'Y'){
c = getchar();
}
statement-x;这是典型的始终标记控制循环,字符常量’y’称为始终标记值
6.2 while语句
while的基本格式为
1 | while(测试条件){ |
while是入口控制语句。循环体将不断执行,直到测试条件最终为假。此时,控制权将转移出该循环。控制权移除后,程序继续执行紧跟在循环体后面的语句
6.3 do语句
do的基本格式
1 | do{ |
运行到do
语句时,程序接着执行循环体。在循环体的末尾,计算while语句中的测试条件。如果条件为真,程序再次执行循环体
这是一种出口控制循环,循环体至少执行一次
6.4 for语句
6.4.1 简单for循环
for
循环是另一种入口控制循环,一般形式为
1 | for(initialization; test-condition; increment){ |
for
语句执行的过程如下
- 首先进行控制变量的初始化
- 使用测试条件表达式来测试控制变量的值,若测试条件为真,则执行循环体;否则,循环结束
- 当循环体执行时,在计算完循环的最后一条语句后,将控制权转交给
for
语句。此时控制变量使用诸如i=i+1
之类的赋值语句进行递增计算,并把控制变量的新值应用到测试条件表达式
for
循环的主要一点是,所有三个动作,即初始化、测试和递增,都放在for
语句本身之中,因而程序员和用户在一个地方就可以看见它们
6.4.2 for循环的其他特性
例如
1 | for(n=1, m=50; n<=m && n<10; n++, m--){ |
-
特性一: 初始化段可由多个组成部分,用逗号隔开
-
特性二: 递增段也可以不止一部分
-
特性三: 测试条件可以含有任何组合关系
-
特性四: 可以在初始化段和递增段的赋值语句中使用表达式
1
for(x=(m+n)/2; x>0; x=x/2){}
-
特性五: 如果有必要可以省去其中的一个或多个段
1
for(;m != 100;;){}
6.4.3 for循环的嵌套
可以根据需要嵌套多条for语句。循环应正确地缩排,以便读者能容易地确定每条for语句中包含了哪些语句
6.5 循环中的跳转
6.5.1 跳出循环
利用break
语句或者goto
语句可以实现从循环中退出来
当循环为嵌套时,break
语句只从包含它地循环中退出。也就是只能退出一层循环
结构化程序设计
- 只使用三种控制结构,从而使程序地逻辑容易理解:顺序结构、选择结构、重复结构
- 结构化程序设计有助于实现良好设计的程序,更容易编写、阅读和维护
- 结构化程序设计不鼓励使用诸如goto、break、continue之类的跳转语句来实现无条件分支
6.5.2 跳出循环的一部分
c支持continue语句,在循环中,continue语句使得控制权直接跳转到测试条件
6.5.3 避免使用goto语句
良好的编程习惯避免使用goto语句
- 当使用goto语句时,程序的结构太复杂,不可读
- 而且编译器产生的代码效率会变得底下
6.5.4 跳出程序
当我们希望终止程序,回到操作系统,可以使用exit
函数
1 | if(test-condition)exit(0); |
6.6 简洁的测试表达式
由于每个整数表达式都有真值和假值,所以没必要显式的和0比较,例如
1 | if(expression == 0) |
等价于
1 | if(!expression) |
第 7 章 数组
7.1 概述
C提供一种派生数据类型,称为数组,是一种功能强大的数据类型,可以灵活高效地存储、访问和操作数据项
所谓数组,就是一个大小固定、含有相同数据类型元素的顺序集合。使用单个数组名来表示元素的集合,通过指定序号来引用某项元素,使得程序的开发更加简单高效
7.2 一维数组
数组下标可以是整形常量、整形变量或者可以生成整数的表达式
C语言不进行边界检查,因此应确保数组索引位于合理范围内
7.3 一维数组的声明
数组必须在使用之前进行声明,数组声明的形式为
1 | type variable-name[size]; |
- type指定包含在数组中的元素的类型
- size则指定数组所能存储元素的最大数目
7.4 一维数组的初始化
7.4.1 编译时初始化
可以像普通变量一样在声明数组时便初始化数组的元素。初始化数组的一般形式为
1 | type array-name[size] = {list of values}; |
- 若列表中的数值少于元素的个数,只有部分元素被初始化,剩余的元素自动设置为默认值,int类型将设置为0,字符类型会被初始化为空
- 数组的大小可以省略。此时,编译器将为所有已初始化的元素分配足够的存储空间
- 如果用于初始化的数值多于所声明的数组大小,编译器会报错
1
int num[3] = {10, 20, 30, 40};
7.4.2 运行时初始化
可以在运行时,显式初始化数组
1 | for(i = 0; i < 100; i ++){ |
7.5 二维数组
二维数组的声明如下
1 | type array-name[row_size][column_size]; |
与一维数组一样,数组的每个维都是从0到最大值-1索引的,第1个索引选择行,第2个索引选择该行的列
7.6 二维数组的初始化
通过在二维数组的声明语句的后面加上大括号括起来的初始值列表,就可以进行二维数组的初始化
例如
1 | int table[2][3] = {0, 0, 0, 1, 1, 1}; |
- 可以不指定第一维的大小
- 初始化时,遗漏的元素值将初始化为0
- 二维数组的存储也是连续存储在内存中
7.7 多维数组
c支持多维数组,具体限制由编译器决定,大多数编译器只允许7到10维
多维数组的一般形式为
1 | type array-name[s1][s2][s3]...[sm]; |
7.8 动态数组
静态数组
- 在编译时分配存储空间的过程称为静态内存分配
- 接受静态内存存储空间的数组称为静态数组
动态数组
- 在c中,可以在运行时为数组分配存储空间,称为动态内存分配,在运行时创建的数组称为动态数组。可以有效地将数组的定义推迟到运行时
- 动态数组使用指针变量和内存管理函数
malloc
、calloc
和realloc
来创建
7.9 与数组相关的内容
- 使用指针访问数组
- 将数组作为函数的参数
- 将数组作为结构体的成员
- 把结构类型的数据作为数组元素
- 把数组作为动态数据类型
- 操作字符数组和字符串
第8章 字符数组和字符串
8. 1 概述
字符串就是字符的序列,可以看成是单个数据项
定义在双引号之间的字符组就是一个字符串常量
字符串经常用来创建有意义且可读性好的程序,作用于字符串的常见操作有
- 读写字符串
- 字符串的合并
- 字符串的赋值
- 比较两个字符串是否相等
- 从字符串中提取子字符串
8.2 字符串变量的声明和初始化
c语言并不支持字符串数据类型,但允许使用字符数组来表示字符串。在c中,字符串变量就是一个字符数组。
一般形式为
1 | char string_name[size]; |
-
当编译器把字符串赋给字符数组时,会自动在字符串的末尾添加空字符
\0
,因此size必须比字符串长度大一个,不然会乱码 -
在初始化字符数组时,也可以不指定数组的大小。在这种情况下,数组的大小将根据初始化元素的数量自动确定
1
char string[] = "GOOD";
-
也可以把数组的大小声明得比初始化字符串更大,这种情况下多余的位置将放置空字符
\0
注意
- 不能把初始化从声明中分隔开
- 数组名不能作为赋值运算符的左操作数
8.3 从终端读取字符串
8.3.1 使用scanf函数
可以使用scanf
函数加上%s
格式说明符来读取字符串,例如
1 | char address[10]; |
scanf函数的问题是,一旦遇到空白符,就会终止输入
在字符数组中,变量名的前面不需要加&
符号,因为数组名变量的值即是数组的首地址
但是,未占用的空间中存储的仍然是垃圾
也可以在scanf语句中使用%ws
格式说明符来指定字段的宽度,用于从输入字符串中读取指定数量的字符
1 | scanf("%ws", name); |
- 宽度
w
等于或大于所键入的字符数时,整个字符串都保存在字符串变量中 - 宽度
w
小于键入的字符数,多余的字符将被截除,不被读取
8.3.2 读取文本行
可以使用编辑集转换码%[^\n]
来读取包含空格的文本行
1 | char line[80]; |
8.3.3 使用getchar和gets函数
可以使用getchar函数从终端中读取单个字符
1 | char ch; |
要读取含有空格的字符串文本,更加方便的方法是使用库函数gets
,它位于头文件<stdio.h>
中,形式如下
1 | gets(str); |
str
是一个已经正确声明过的字符串变量gets
函数从键盘中读取字符到str
中,直到遇到一个换行符,然后将一个空字符附加到该字符串中
8.4 在屏幕上显示字符串
8.4.1 使用printf
函数
格式说明符%s
可以用来显示以空字符结尾的字符数组,例如
1 | printf("%s", name); |
说明符%10.4s
表示前4个字符显示在宽度为10列的字段中,字符串以右对齐的方式显示
8.4.2 使用putchar和puts函数
putchar(ch)
可以将ch字符输出到屏幕上
显示字符串值更加方便地方式是使用puts函数puts(str)
,此函数会自动加入\n
8.5 字符的算术计算
C允许像数字一样对字符进行操作,当某个字符常量或者字符变量在表达式中出现时,系统自动将它转换为对应的整数值
也可以对字符常量和字符变量执行算术运算
1 | char x = 'z' - 1; |
还可以在关系表达式中使用字符常量,例如
1 | ch >= 'A' && ch <= 'Z' |
c有一个库函数atoi
可以将数组字符串转换为相应的整数值
1 | num = "1998"; |
8.6 字符串处理函数
8.6.1 strcat函数
strcat
用于将两个字符串拼接在一起
1 | strcat(str1, str2); |
- 当执行strcat函数时,str2附加到str1上
- 它将str1后的空字符删除,然后把str2放置在其后。字符串str2保持不变
- 必须确保str1足够大,以便容纳最终的字符串
8.6.2 strcmp函数
strcmp
对两个字符串进行比较
1 | strcmp(str1, str2); |
- 如果相等,返回值为0
- 如果不相等,就返回字符串中第一个不匹配的字符的数值差
8.6.3 strcpy函数
strcpy函数用于字符串拷贝
1 | strcpy(str1, str2); |
- 它将str2的内容赋给str1
8.6.4 strlen函数
strlen
函数计算并返回字符串中的字符数
第 9 章 用户自定义函数
9.1 概述
C函数分为两类,库函数和自定义函数
库函数不需要自己编写,自定义函数需要用户自己开发
9.2 为什么需要自定义函数
main是C的一个特别的函数。每个程序都必须有一个main函数,以表明程序运行的起始点
但是,只用一个main函数可能会导致程序变得太大、太复杂,使得调试、测试和维护工作变得困难
自定义函数这种分而治之的方法有很多优点
- 便于自顶向下的模块化编程
- 通过在适当的地方使用函数,可以减短源代码的长度
- 更容易定义和隔离有错误的函数,以便进一步检查
- 函数可以被其他多个程序共用
9.3 多函数程序
函数就是执行某个特定任务的自包含的代码块。函数一旦设计和封装好后,就可以看成一个黑盒子
除了起点之外,程序中的函数之间不存在其他预定义的关系、优先规则和层次结构。函数可按任意顺序排列。被调用函数可放在调用函数之前,也可放在调用函数之后
9.4 自定义函数的元素
在c中,函数被归类于派生数据类型
函数和变量之间的关系
- 可将函数名和变量名看成标记符
- 与变量一样,函数具有与之相关的类型(返回值类型)
- 与变量一样,在使用之前,函数名及其类型必须已经定义和声明
为了使用自定义函数,我们需要创建和函数有关的3个元素
- 函数定义
- 函数调用
- 函数声明
9.5 函数定义
函数定义应包括
- 函数名
- 函数类型
- 参数列表
- 局部变量声明
- 函数语句
- 返回语句
一般形式为
1 | function_type function_name(parameter list){ |
9.5.1 函数头
函数头有三部分:函数类型、函数名和形参列表
- 函数类型: 函数类型指定希望函数返回给调用程序的值的类型。若没有显式地指定类型,将返回整型。如果函数不返回任何值,就需要将返回类型指定为
void
- 函数名是任何合法的c标识符,因此要遵守c的变量名命名规则
- 形参列表:形参列表声明的变量用来接受从调用程序发送来的数据。但没有参数时,可以在括号中填写
void
也可以不填写任何值
9.5.2 函数体
函数体中包含了函数声明以及完成任务所需的语句。函数体用括号括起来,包括三部分
- 局部变量: 即本函数所需的变量
- 完成函数任务的函数语句
- return语句,返回由函数所得的值
9.6 返回值及其类型
尽管可以给调用函数传递任意数量的值,但被调用函数在每次调用时最多只能返回一个值
当返回值时,将自动将表达式转化为声明的返回值类型。假设在函数中使用double类型,而返回值为int类型,那么返回值将被截取为int类型
9.7 函数调用
函数调用是后缀表达式,优先级很高。因此函数调用作为表达式的一部分时,除非使用括号来改变优先顺序,否则首先计算函数
如果实参比形参多,那么多余的实参将被丢弃
如果实参比形参少,那么没有实参与之匹配的形参将被初始化为垃圾数据
数据类型的任何不匹配都阿静导致产生垃圾数据;
9.8 函数声明
C程序中所有的函数在使用之前都必须声明
函数声明由4部分组成
- 函数类型
- 函数名
- 参数列表
- 终止分号
形如
1 | function-type function-name(paramater list); |
- 参数列表必须用逗号分隔开
- 函数原型声明与函数定义中的参数名无需相同
- 参数的类型、数量和顺序必须与函数定义中的匹配
- 声明中的参数名可以不写
9.9 函数的类型
9.10 无参数无返回值的函数
这种情况下在调用函数和被调用函数之间没有任何数据传递,只进行控制权转换
9.11 有参数无返回值的函数
函数调用时,只是将实参的值的副本传递给了被调用函数。被调用函数中所发生的一切都不会影响实参中的变量
可变的参数数量
- 有些函数的参数的数目和类型在编译时并不知道,这时可以使用称为省略号的新符号来处理这种函数
- 例如
double area(float d, ...)
- 函数声明和定义中都必须使用省略号来表明参数的数量和类型是任意的
9.12 有参数有返回值的函数
可以在main函数内部声明其他函数,但是函数定义要在外部完成
1 | main(){ |
- 但是按照目前的规则,不声明并不会报错,只会产生警告
9.13 有参数但有一个返回值的函数
9.14 返回多个值的函数
我们可以使用参数,来往调用函数返回信息,用来返回信息的参数称为输出参数
使用地址运算符&
和间接运算符*
可以实现通过参数返回信息
1 | void mathoperation(int x, int y, int *s, int *d); |
在函数调用中,我们是把x和y的实际值传递给该函数,并把保存s值和d值的内存地址传递给该函数
函数头的sum和diff声明中的间接运算符*
表示,这些变量用于保存地址,而不是实际的变量值
运算符*
称为间接运算符,是因为它通过变量的地址来间接引用变量
变量*sum
和*diff
称为指针,sum
和diff
称为指针变量。它们都被声明为int类型,因此它们可以指向int类型的数据的存储位置
9.15 函数的嵌套
9.16 函数的递归
递归指的是函数调用自身
9.17 将数组传递给函数
形如
1 | float largest(float array[], int size) |
在c中,数组名表示的是数组第一个元素的地址。传递数组名时,实际上是把数组的地址传递给被调用函数。这样被调用函数中的数组就指向内存中相同的数组了。这样被调用函数的数组的任何改变都将反映到原始数组上
把参数的地址传递给函数称为地址传递或指针传递
9.18 将字符串传递给函数
在c中因为字符串被看作字符数组,所以传递字符串给函数的规律非常类似于传递数组给函数
-
必须将字符串变量声明为函数的形参
1
void display(char item_name[]){}
-
函数调用必须带一个无下标的字符串数组名作为实参,例如
display(name)
按值传递和按指针传递
- 在按值传递中,实参的值被复制给被调用函数参数列表中的变量。被调用函数使用的是实参的副本,而不是原始值
- 在按指针传递中,发送给被调用函数的是变量的地址,而不是值的副本
9.19 变量的作用域、可见性和生存期
在C语言中,变量不仅属于某种数据类型,而且还具有某种存储类型(storage class
)
变量的储存类型有
- 自动变量
- 外部变量
- 静态变量
- 寄存器变量
作用域: 变量的作用域确定在程序的哪些区域可以使用该变量
可见性: 变量的可访问性
生存期: 程序运行时变量保持某个值的时间段
9.19.1 自动变量
自动变量是在某个函数中声明的变量,这些变量只能在该函数中使用。在调用函数时创建变量,在函数退出时自动销毁,因此命名为自动变量。自动变量又称为局部变量或内部变量
默认情况下,如果声明在某个函数中的变量没有指定存储类型,就为自动变量
也可以用关键字auto来显示声明自动变量
1 |
|
9.19.2 外部变量
在整个程序中都存在并活动的变量称为外部变量,又称为全局变量。与局部变量不同,全局变量可以被程序中的所有函数访问。外部变量在函数的外面进行声明
1 | int number; |
如果局部变量与全局变量同名,在声明局部变量的函数中,局部变量具有比全局变量更高的优先级
全局变量的另一个特性是,只有从全局变量的声明之处开始到程序的末尾可用
9.19.3 外部声明
例如
1 | main(){ |
尽管变量y声明在两个函数之后,但函数中y的外部声明语句告诉编译器,y是个整数,是在程序的其他地方声明的,这样就可以在函数中使用变量y了
9.19.4 静态变量
静态变量的值可以一直保持到程序结束,使用关键字static可以将变量声明为静态
1 | static int x; |
内部静态变量: 作用范围仅限于定义它的函数中,但是此变量会一直保持,只在程序编译时初始化一次,以后再也不进行初始化
静态外部变量与简单外部变量的区别是:它只在定义它的文件中可用,而简单外部变量可以被其他文件访问
static也可用来控制函数的作用范围。若需要一个函数,只对定义它的文件可见,而对其他文件中的任何函数都不可见,就可以通过用存储类型static定义函数来实现
9.19.5 寄存器变量
寄存器变量存储在寄存器中,而不是内存中,访问速度更快
1 | register int count; |
9.20 多文件函数
extern说明符告诉编译器,后面的变量类型和名称已经在其他地方进行了声明,不用再为它们创建存储空间了
第 10 章 结构体和共用体
10.1 概述
C语言支持一种结构化的数据类型,称为结构体,来用单个名称来表示不同类型的数据集合
结构体有助于以一种有意义的方法来组织复杂数据
10.2 结构体的定义
必须首先定义结构体的格式,然后才能声明和使用结构体的变量
例如
1 | struct book_bank{ |
- 关键字struct声明一个结构体,它有4个数据字段。且属于不同的数据类型
- book_bank时结构体名,称为结构体标记符
- 结构体模板以分号结尾
10.3 声明结构体变量
定义之后,才可以声明这种类型的变量,例如
1 | struct book_bank book1, book2, book3; |
当编译器遇到声明语句时,将为结构体变量保留存储空间
也可以使用typedef定义结构体,例如
1 | typedef struct{ |
10.4 访问结构体成员
成员与变量之间的链接可以使用成员运算符.
来建立,下面是访问结构体成员的例子
1 | strcpy(book1.title, "BASIC"); |
10.5 结构体的初始化
结构体变量也可以在声明时进行初始化,例如
1 | struct book_bank book1 = {"BKTL", "BKATR", 100, 10.11}; |
- 不能对结构体模板中的单个成员进行初始化
- 包含在花括号中的数值必须与结构体定义中的成员顺序一致
- 允许只初始化前面的成员
- 未初始化的成员将被赋值为0或者空字符
10.6 结构体变量的复制和比较
结构体变量可以直接进行赋值操作,例如
1 | book2 = book1; |
- 此时,这两个变量共享一块内存
但是不允许进行比较操作
1 | book2 == book1 //这是非法的 |
如果需要比较请依次对成员进行比较
10.7 单个成员的运算
结构体变量加句点运算符再加成员,就可以像其他变量名一样来处理,例如
1 | if(book1.num == 100){ |
访问成员的三种方式
- 使用句点表示法: v.x
- 使用间接表示法: (*ptr).x
- 使用选择表示法: ptr->x
10.8 结构体数组
例如
1 | struct marks{ |
10.9 结构体中的数组
略
10.10 结构体中的结构体
结构体的嵌套
1 | struct salary{ |
也等价于
1 | struct pay{ |
10.11 结构体与函数
C支持将结构体的值作为参数传递给函数,有三种方式
- 把结构体的每个成员作为函数调用的实参进行传递,然后像普通变量一样处理这些实参
- 将整个结构体的副本传递给被调用函数,由于函数使用的副本,所以在函数中对结构体成员的任何修改都不能反应到调用函数的初始结构体中
- 使用指针以参数形式来传递结构体。此时,结构体的地址被传递给被调用函数。类似于将数组传递给函数
第二种方式的例子
1 |
|
- 在这种方式中,函数update接收到的只是book1的副本,就如同传递一个简单变量一样
- 所以要完成更新操作,必须再次赋值
10.12 共用体
共用体中的所有成员使用相同的存储空间,只给共用体变量分配了一片存储空间。尽管共用体可以包含不同数据类型的多种成员,但一次只能处理一个成员
10.13 结构体的大小
可以使用sizeof
来获取结构体的大小
1 | sizeof(struct book_bank); |
10.14 位域
第 11 章 指针
11.1 概述
在C语言中,指针是一种派生数据类型。指针以内存地址作为值。由于内存地址表示在计算机内存中保存程序指令和数据的位置,因此可用指针来直接访问和操作存储在内存中的数据
指针的优点
- 便于处理数据和数据表
- 通过作为函数参数,指针可用来从函数中返回多个值
- 指针允许引用函数,因而可以把函数作为参数传递给其他函数
- 使用指向字符串的指针数组,可以节省内存的数据存储空间
- 指针使得C语言支持动态内存管理
- 指针为操作动态数据结构提供了帮助
11.2 理解指针
计算机的内存是一系列"存储单元"的集合。每个单元有一个称为地址的数字与之关联
当我们声明一个变量时,系统会在内存中分配合适的存储空间,以保存该变量的值
1 | int quantity = 179; |
- 程序运行时,系统总是把变量名
quantity
与地址5000相关联(类似房间和房间号) - 要访问数值179,可以使用变量名quantity或地址5000
- 由于内存地址只是编号,因而又可以把它们赋给变量。这种保存内存地址的变量就称为指针变量
- 指针变量只是保存地址的变量,而地址则是另一个变量在内存中的位置
我们并不关心指针变量的实际值,因为每次运行程序时,指针的值都是会发生变化的。我们关心的是变量p
与变量quantity
之间的关系
11.3 访问变量的地址
变量在内存中的实际地址与具体的系统有关,因此我们并不能立即知道某个变量的地址
我们利用地址运算符&
可以确定变量的地址
1 | p = &quantity; |
11.4 指针变量的声明
由于指针变量包含的是存储某种数据类型的地址,因此在使用之前必须把它们声明为指针
1 | data_type *pt_name |
*
说明变量pt_name
是指针变量pt_name
需要内存空间pt_name
指向data_type
类型的变量
声明语句使得编译器为指针变量分配存储空间。由于存储空间没有赋给任何值,因而这些变量中包含的是一些未知的值,这样它们指向的也是未知的地址。
11.5 指针变量的初始化
把变量的地址赋给指针变量的过程称为指针变量的初始化
所有未初始化的指针的值都是未知的,这些值同样会被解释为内存地址。它们可能不是有效地址,也可能指向错误的值。但是编译器不会检测这些错误,因而含有未初始化指针的程序会产生错误的结果
1 | int quantity; |
1 | int *p = &quantity; |
除了NULL和0之外,其他变量不能赋给指针变量
11.6 通过指针访问变量
通过间接运算符可以使用指针来访问变量的值
1 | int quantity, *p; |
在C语言中,指针和地址的赋值都是通过符号名来动态实现的。不能使用*5378
来访问存储在地址5378中的值
11.7 指针链
可以将一个指针指向另一个指针,来形成指针链
1 | int x, *p1, **p2; |
11.8 指针表达式
可以在指针变量的前面或后面添加递增或递减运算符
指针变量可以与整数值进行加减运算
当两个指针指向同一个数组时,可以用一个指针变量减去另一个指针变量
当两个指针指向相同数据类型的对象时,可以使用关系运算符对它们进行比较操作
不能对指针变量与常量做乘法运算
两个指针变量不能做加法操作
11.9 指针的递增和比例因子
当指针进行递增时,所增加的值为该指针指向的数据类型的长度,这种长度就称为比例因子(scale factor)
11.10 指针与数组
当声明数组时,编译器在连续的内存空间分配基本地址和足够的存储空间,以容纳数组的所有元素。基本地址是数组第一个元素的存储位置。编译器还将数组名定义为指向第一个元素的常量指针
1 | int x[5] = {1, 2, 3, 4, 5} |
11.11 指针与字符串
C语言支持另一种创建字符串的方式,即使用char
类型的指针变量
1 | char *str = "good"; |
- 上述语句创建了一个文本字符串,并将其地址保存在指针变量str中
- 这样指针str就指向了字符串
good
的第一个字符
11.12 指针数组
1 | char *name[3] = { |
11.13 将指针作为函数的参数
1 | void exchange(int *, int *); |
当把地址传递给函数时,接收地址的参数必须是指针。使用指针传递变量地址的函数调用过程称为引用调用
上面的程序有如下几点
- 函数的参数声明为指针
- 在函数体中使用了间接引用指针
- 当调用函数时,地址作为实参被传递
11.14 函数返回指针
1 | int *largest(int *, int *); |
11.15 指向指针的函数
与变量一样,函数也属于某种数据类型,在内存中也需要有存储空间。因此可以声明一个指向函数的指针
1 | double mul(int, int); |
(*p1)(x, y)
等价于mul(x, y)
兼容性与类型转换
- 总是有一种数据类型与指针关联,我们不能把一种类型的指针赋给另一种类型的指针,尽管两者都是以内存地址作为值。这称为指针的兼容性
- 对不同类型的指针不能使用赋值运算符,但可以利用类型转换,在不兼容的指针类型之间显式地进行赋值操作
空指针
- 空指针
void*
是通用指针,可以表示任何指针类型 - 所有指针类型都可以赋给空指针
- 而空指针无须类型转换就可以赋值给任意指针
11.16 指针与结构体
运算符->
、.
、()
和[]
的优先级最高,它们与它们的操作数高度捆绑在一起
记住下列语句的意义
*ptr->p
: 返回p
指向的内容*ptr->p++
: 在访问p
指向的内容后递增p
(*ptr->p)++
: 是p
指向的内容递增*ptr++->p
: 在访问p
的内容后,使ptr
递增
11.17 指针存在的问题
常见错误如下
- 向未初始化的指针赋值
1 | int *p, m = 100; |
- 将数值赋值给指针变量
1 | int *p, m = 100; |
- 需要时没有间接引用指针
1 | int *p, x = 100; |
- 将未初始化变量的地址赋值给指针
1 | int *p, m; |
- 比较指向不同对象的指针
1 | char name1[20], name2[20]; |
第 12 章 文件管理
12.1 概述
基于控制台的I/O操作有两个主要问题
- 通过终端来处理大量数据是笨拙且费时的
- 当程序终止或关机时,所有数据都将丢失
文件是在磁盘上存储一组相关数据的地方
基本的文件操作
- 文件命名
- 打开文件
- 从文件中读取数据
- 往文件中写入数据
- 关闭文件
在C语言中有两种不同的方法来执行文件操作
- 低级I/O操作,使用UNIX系统调用
- 高级I/O操作,使用C语言的标准I/O库函数
12.2 定文并打开文件
要把数据存储在辅存的文件中,就必须向操作系统指定文件的某些信息
- 文件名
- 数据结构
- 打开方式
文件名是个字符串,操作系统的合法文件名包括两部分:基本名称和可选的扩展名
文件的数据结构定义为FILE
,因此,所有文件在使用之前都必须声明为FILE
类型。FILE
是一种已定义的数据类型
声明并打开文件的一般格式为
1 | FILE *fp; |
- 第一条语句把变量
fp
声明为“指向FILE数据类型的指针” - 第二条语句打开名为
filename
的文件,并把标识符赋给FILE
类型的指针fp
fp
指针包含了文件的所有信息,随后可用作系统与程序之间的通信链接
mode
可以是以下情况之一
r
: 以只读方式打开文件w
:以只写方式打开文件a
: 以附加(或添加)数据的方式打开文件r+
: 打开已有文件,用于读和写数据w+
: 除了可用于读和写数据之外,其他的与w
相同a+
: 除了可用于读和写数据之外,其他的与a
相同
当试图打开文件时,将发生以下事情之一
- 当
mode
为w
时,如果文件不存在,就创建指定名称的文件;如果存在,就删除其内容 - 当
mode
为a
时,打开文件并且保留当前内容:如果文件不存在,就创建指定名称的文件 - 当
mode
为r
时,如果文件存在,就打开文件并保留当前内容:如果文件不存在,就会发生错误
12.3 关闭文件
只要所有操作已经完成,就应立即关闭文件
这样就可以确保与该文件有关的未完成的信息从缓冲区中冲刷掉,所有与该文件的链接也会被断开
形式如下
1 | fclose(file_pointer); |
12.4 文件的输入输出操作
12.4.1 getc
与putc
函数
最简单的文件I/O
函数是getc
和putc
。它们类似于getchar
和putchar
函数,一次处理一个字符
putc(c, fp1)
把字符变量c
包含的字符写入fp1
指向的文件中
c=getc(fp2)
getc函数用于从已读取方式打开的文件中读取一个字符
每次进行getc
和putc
操作后,文件指针就移动一个字符的位置。当到达文件末尾时,getc
函数将返回文件末尾标记符EOF
。因此,当遇到EOF
标记符时,读取工作将停止
1 |
|
12.4.2 getw
与putw
函数
getw
和putw
时基于整数的函数,用来读取和写入整数值
1 | putw(integer, fp); |
12.4.3 fprintf
与fscanf
函数
1 | fprintf(fp1, "%s %d %f", name, age, 7.5); |
12.5 I/O操作的错误处理
常见的错误包括
- 读取试图超过文件结尾标识符
- 设备溢出
- 试图使用还没有打开的文件
- 当文件打开用于某种操作时,试图执行另一种操作
- 打开不合法的文件名
- 试图往写保护的文件写入数据
feof
函数用来检测是否到达文件末尾,如果指定文件的所有数据都已读取,返回非零常数;否则返回零
1 | foef(fp) == 0 |
ferror
函数用于报告指定函数的状态。该函数也是以FILE
指针作为参数,如果检测出错误,就返回一个非零整数;否则返回零
1 | feeror(fp) == 0 |
当使用fopen
函数打开文件时,将返回一个文件指针。如果因为某些原因不能打开文件,那么函数返回NULL
指针
1 | if(fp == NULL) |
12.6 随机访问文件
ftell
函数以一个文件指针为参数,返回一个long
类型的数字,它对应于当前的位置。形式为
1 | n = ftell(fp); |
rewind
函数以一个文件指针作为参数,把指针位置重置到文件的开头
1 | rewind(fp); |
fseek
函数用于把文件指针移到指定的文件位置,形式如下
1 | fseek(file_ptr, offset, position); |
file_ptr
: 文件指针offset
: 指定从position
开始计算的要移动的位置- position可以是以下3个值
- 0: 文件的开头
- 1: 当前位置
- 2: 文件的末尾
12.7 命令行参数
第 13 章 动态内存分配与链表
13.1 概述
动态数据结构可以在运行时灵活地添加、删除或重排数据项,而动态内存管理则运行在运行时分配更多的内存空间或释放不再需要的空间,因而可以优化存储空间的使用
13.2 动态内存分配
在运行时分配内存空间的过程称为动态内存分配,尽管C语言本身不具备这种能力,但它有4个名为“内存管理函数”的库例程,可以用来在程序运行时分配和释放内存
内存分配过程
- 程序指令、全局变量和静态变量存储在永久存储区内,而局部变量存储在栈中
- 位于这两个区之间的内存空间可以用于程序运行时的动态分配。这些内存区称为堆
- 当程序运行时,堆的大小是不断变化的,因为会发生函数或代码块的局部变量的创建和销毁
- 因此有可能遇到内存的溢出,在这种情况下,内存分配函数将返回空指针
13.3 用malloc函数分配一块内存
利用malloc
函数可以分配一块内存。malloc
函数将保留指定大小的内存块,并返回void
类型的指针。这意味着可以通过类型转化将它赋给任意类型的指针
1 | int *x = (int *)malloc(100 * sizeof(int)); |
注意,动态分配的存储空间没有名称,因此只能通过指针来访问其内容
也可以用malloc
函数来给诸如结构体之类的复杂数据类型分配存储空间
1 | st_var = (struct store *)malloc(sizeof(struct store)); |
注意,malloc
函数分配的是连续的字节块。如果堆的空间不能满足要求,分配失败。如果失败,将返回NULL。因此,在使用内存指针之前,应检查内存分配是否成功
13.4 用calloc函数分配多个内存块
calloc是另一种内存分配函数,通常用于在运行时为了存储派生数据类型而分配所需的内存空间
malloc函数分配的是单个内存块,而calloc函数分配的是多个内存块,且每个内存块的大小相等,并把所有字节都设为0
1 | ptr = (cast-type *)calloc(n ,elem-size); |
13.5 用free函数释放已用的空间
变量的编译时存储空间是由系统根据其存储类型来分配和释放的。而对于运行时的内存分配,当不再需要时,由程序员来负责释放。当存储空间有限时,内存的释放就变得很重要了
当不再需要保存在内存块中的数据,且不打算用这块内存来存储任何其他信息时,可用free
函数来释放掉该内存块,以供将来使用
1 | free(ptr); |
13.6 用realloc函数改变内存块的大小
可以用realloc
函数来改变已分配内存的大小
1 | ptr = realloc(ptr, newsize); |
- 该函数把大小为newsize的新内存空间分配给指针变量
ptr
,并返回一个指向新内存块的第一个字节的指针 newsize
可以比size
更大或更小- 新内存块的开始位置可以与旧的相同
- 如果在相同区域中找不到其他的内存空间,就将在全新的区域中创建,旧内存块中的内容将移到新块中
- 如果函数没有成功分配更多的空间,将返回空指针,旧块被丢弃
13.7 链表的概念
列表是指按序组成的项值。
数组就是一种列表。在数组中,元素的顺序是由索引隐式地给定的。我们就是使用索引来访问和操作数组元素的
链表
- 用结构体表示一个列表成员,它含有指向下一个结构体成员的链接
- 它由两个字段组成:一个包含数据项,另一个包含指向链表中下一个数据项的地址
- 链表是结构体的集合,顺序不是由它们在内存中的物理位置确定的,而是由逻辑位置确定的
- 这种逻辑位置链接是指向同类型的另一个结构体的指针
1 | struct node{ |
node1的next
指针可以利用下面的语句来使其指向node2
1 | node1.next = &node2; |
C语言提供了空指针,可以把它存储在链表的最后一个节点的next
字段中
1 | node2.next = 0; |
13.8 链表的优点
优点
- 其大小可以在程序运行时增大或缩小。链表的长度可以按需决定
- 不会浪费空间,任何时候,链表使用的内存就是它所需要的
- 链表提供的灵活性允许高效地重排数据项,通过重排链接,可以很容易地插入和删除数据项
局限:访问任何数据项时有些笨拙或费时
13.9 链表的种类
- 线性链表
- 环形链表
- 双向链表
- 循环双向链表
13.10再论指针
在使用之前必须把指针初始化为内存地址,有两种初始化方式
- 赋给已有变量的地址(静态赋值)
1 | ptr = &count |
- 使用内存分配函数(动态赋值)
1 | ptr = (int*)malloc(sizeof(int)); |
13.11 创建链表
可以使用指针和诸如malloc
之类的动态内存分配函数来创建链表节点
1 | struct linked_list{ |
13.12 插入一个数据项
链表的优点之一是,相比较而言,它更容易插入一个新节点,只要求重置两个指针即可
13.13 删除一个数据项
略
13.14 链表的应用
略