第三章 数据和C
整数
1.如何选择需要使用的整数类型
C语言只规定了short存储的空间不能多于int,long存储空间不能少于int。目前个人计算机最常见的设置是long long占64位,long占32位,short占16位,int占16位或者32位。
选取的原则如下:
- 如果是非负值,首先考虑unsigned类型,因为它可以表示更大的整数
- 如果超出了int类型的取值范围,但又在long类型的取值范围内,那么应该使用long。
- 当确实需要32位整数时,请使用long。同理,如果确实需要64位整数,那么应该使用long long类型。
2. 和 long long常量
要把一个较小的常量作为long类型对待时,可以在值的末尾加上L
后缀(小写的l不容易和数字1区分)。在支持long long的系统中,可以在值的末尾加上LL
区分long long类型。
3.注意整数溢出问题
当整数超过其类型所能表示的范围时,就会发生整数溢出的问题:
#include
int main(void)
{
int i;
i = 2147483647;
printf("%d %d %d\n", i, i+1, i+2);
unsigned int j;
j = 4294967295;
printf("%u %u %u\n", j, j+1, j+2);
}
输出结果为:
2147483647 -2147483648 -2147483647
4294967295 0 1
字符类型char
1.字符类型的表示
C语言中用单引号指明字符常量(注意双引号表示的是字符串)
char grade = 'A';
2.打印字符
printf()
函数可以用%c
打印字符。如下图所示,char
型本质上存储的是一个整数,通过不同的格式控制符我们可以选择输出字符型对应的字符或者是对应的整数。
布尔类型_Bool
只占用1位的存储空间,用于表示逻辑值“是”还是“否”
float、double和long double
1.float类型
float
至少6位小数,且取值至少$ 10^{-37} $
到$ 10^{37} $
。通常系统存储一个浮点数需要32位,前8位表示指数的值和符号,后24位用于表示非指数部分及符号。
2.double类型
一般情况下double
类型和float
类型的取值范围相同,但至少能表示10位有效数字。double
类型也叫做双精度类型,因为它占用64位,同时也至少能表示13位有效数字。
3.注意事项
默认情况下,编译器表示浮点型常量是double
类型的精度。举个例子:
float some;
some = 4.0 * 2.0;
这种情况下,首先将
4.0
和2.0
存储为64位的double
类型,然后使用双精度进行乘法运算,最后将乘积截断成float
类型输出。这样做会减缓运行速度,在浮点数后加上f
或者F
可以覆盖默认设置。
4.浮点型的打印
使用%f
可以打印十进制的float
和double
类型浮点型,用%e
打印指数计数法的浮点数。
5.浮点型的上溢和下溢
系统规定最大的float
类型为3.4E38
,编写如下代码实现浮点数的上溢:
#include
int main(void)
{
float toobig = 3.4e38 * 10.0f;
printf("%f %e\n", toobig, toobig);
}
输出结果:
inf inf
6.浮点型四舍五入的错误
追根究底是因为浮点型缺少足够的有效数字精度(
float
类型最少表示6位有效数字而double
最多表示13位有效数字)。
举个例子:
#include
int main(void)
{
float a, b;
b = 2.0e20 + 1.0;
a = b - 2.0e20;
printf("%f\n", a);
}
输出结果并不为1。
显示类型大小
在C语言中,我们可以通过sizeof()
函数获取某个类型占用字节的大小。
#include
int main(void)
{
printf("Type int has a size of %zd bytes.\n", sizeof(int));
printf("Type char has a size of %zd bytes.\n", sizeof(char));
printf("Type double has a size of %zd bytes.\n", sizeof(double));
printf("Type long has a size of %zd bytes.\n", sizeof(long));
printf("Type long long has a size of %zd bytes.\n", sizeof(long long));
printf("Type long double has a sizeof %zd bytes.\n", sizeof(long double));
}
输出结果为:
Type int has a size of 4 bytes.
Type char has a size of 1 bytes.
Type double has a size of 8 bytes.
Type long has a size of 8 bytes.
Type long long has a size of 8 bytes.
Type long double has a sizeof 16 bytes.
第四章 字符串与格式化输入输出
字符串简介
1.char类型数组与null字符
C语言没有用于专门存储字符串的变量类型,字符串都被存储在char类型的数组中。数组由连续的存储单元组成,字符串中的字符被存储在相邻的存储单元中,每个单元存储一个字符。
2.字符串与字符
字符串常量"x"
与字符常量'x'
不同,前者是派生类型(char
数组),后者是基本类型(char
)。字符串常量"x"
由两个字符'x'
和空字符\0
组成。
3.strlen()函数
对于一个字符串使用strlen()
函数,可以得到它存储的字符串长度(不需要加上末尾的空字符)。使用sizeof()
指的是给char
数组分配的存储空间。
常量和C预处理器
1.C语言声明常量
#define TAXRATE 0.015
2.定义字符和字符串常量
字符使用单引号,字符串使用双引号
#define BEEP '\a'
#define TEE 'T'
#define ESC '\033'
#define OOPS "Now you have done it!"
注意#define TOES = 20
是错误的,相当于预处理器会把所有TOES
都替换为= 20
而非20
。
3.const限定符
用于限定一个变量为只读,改变量的值在整个程序中不可更改
const
限定符用起来比#define
更灵活,后续讨论。
4.明示变量
在
limits.h
和float.h
中分别提供了与整数类型和浮点类型大小限制相关的详细信息,每个头文件中都定义了一系列供实现使用的明示变量。
举个例子,limits.h
中包含以下类似的代码,用于表示int
型可表示的最大值和最小值。
#define INT_MAX +32767
#define INT_MIN -32768
printf()和scanf()
这两个函数实现了程序和用户之间的交流,称为输入/输出函数。
1.printf()函数
如果需要打印%
的话,只需要使用%%
。
printf()中可以插入转换说明,比如%c
输出单个字符,%d
输出有符号十进制整数,%s
输出字符串等。
同时printf()函数在%
和转换字符之间可以插入转换说明修饰符。
修饰符 | 含义 |
---|---|
标记 | 包含- 、+ 、# 、空格和0 五种标记,可以不使用或使用多个 |
数字 | 最小字段宽度 |
.数字 | 精度 |
h | 和整型转换说明一起使用,表示short int 或unsigned short int |
hh | 表示signed char 或unsigned char |
j | 表示intmax_t 或uintmax_t |
l | 表示long int 或unsigned long int |
ll | 表示long long int 或unsigned long long int |
L | 表示long double |
t | 表示ptrdiff_t |
z | 表示size_t |
sizeof
运算符会返回以字节为单位的类型或值的大小,这应该是某种形式的整数。但是标准中只规定了该值是无符号整数,在不同的实现中,它可能是各种各样的整数。为了实现不同系统更好的移植性,C语言在stddef.h
头文件中已经把size_t
定义为系统使用sizeof
返回的类型。
float参数的转换
在printf()
函数中对于浮点类型有double
和long double
的转换说明,但是没有float
类型的。这是因为printf()
函数会将所有float
类型的参数自动转换为double
类型,实现对不同标准的兼容。
标记 | 含义 |
---|---|
- | 待打印项左对齐,配合宽度一起使用 |
+ | 有符号值若为正,则在值前面显示+ 号,如果为负,则显示- 号 |
空格 | 有符号值若为正,则在值前面显示前导空格(不显示任何符号);若为负,则在值前面显示- 号 |
# | 把结果转换为另一种形式 |
0 | 对于数值格式,用前导0代替空格填充字段宽度。对于整数格式,如果出现- 标记或者指定精度,则忽略该标记 |
2.使用修饰符和标记的例子
控制整数的输出格式:
#include
#define PAGES 959
int main(void)
{
printf("*%d*\n", PAGES);
//由于限制的宽度2小于PAGES本身长度,所以相当于不做限制
printf("*%2d*\n", PAGES);
printf("*%10d*\n", PAGES);
printf("%-10d*\n", PAGES);
return 0;
}
输出结果:
*959*
*959*
* 959*
959 *
控制浮点数的输出格式:
#include
int main(void)
{
const double RENT = 3852.99;
printf("*%f*\n", RENT);
printf("*%e*\n", RENT);
printf("*%4.2f*\n", RENT);
printf("*%3.1f*\n", RENT);
printf("*%10.3f*\n", RENT);
printf("*%10.3E*\n", RENT);
printf("*%+4.2f*\n", RENT);
printf("*%010.2f*\n", RENT);
return 0;
}
输出结果:
*3852.990000*
*3.852990e+03*
*3852.99*
*3853.0*
* 3852.990*
* 3.853E+03*
*+3852.99*
*0003852.99*
3.scanf()函数
scanf()
函数所做的工作和printf()
所做的工作正好相反,scanf()
把输入的字符串转换为整数、浮点数、字符或字符串等。但是scanf()
函数需要使用指向变量的指针。
- 如果用
scanf()
读取基本变量类型的值,在变量名前加上一个&
- 如果用
scanf()
把字符串读入字符数组中,不需要使用&
对于
scanf()
,除了%c
之外的所有转换说明都会自动跳过待输入值前面的所有空白
scanf()
函数返回成功读取的项数。
- 如果没有读取任何项,且需要读取一个数字而用户却输入一个非数值字符串,
scanf()
便返回0。 - 当
scanf()
检测到“文件结尾”时,会返回EOF
(一般会使用#define
指令将EOF
定义为-1)。
4.printf()的用法提示
- 如果想要将数据打印成列,那么指定足够大的固定字段宽度可以让输出整齐美观
- 如果两个转换说明中间插入一个空白符,可以确保即使一个数字溢出了自己的字段,下一个数字也不会紧跟着该数字一起输出
第五章 运算符、表达式和语句
基本运算符
1.赋值运算符: =
赋值表达式实现的功能是将值存储到对应的内存位置上
- 数据对象:存储值的数据存储区域
- 左值:标识特定数据对象的名称或者表达式
- 可修改的左值:表示可修改的对象(区分const限定符创建的左值)
- 右值:能赋值给可修改的左值,且本身不是左值
因此,数据指的是实际的数据存储地址,而左值是表示或者定位存储位置的标签
2.加减乘除
注意整数除法得到的是整数,浮点数除法得到的是浮点数。在C语言中将整数除法丢弃小数部分的过程称为截断(truncation)
3.运算符优先级
注意两点:
- 在
6 * 12 + 5 * 20
中,虽然乘法会优先于加法进行计算,但是两个乘法的优先级取决于硬件 -
=
运算符的结合律是从右往左,即将右边的表达式算完后赋值给左边
其他运算符
-
sizeof
运算符以字节为单位返回运算对象的大小,返回类型是size_t
(可使用printf()
函数配合转换说明符%zd,%u,%lu
) - 求模运算符
%
只能用于整数,会返回余数 -
++
运算符有前缀和后缀两种模式,工作中为了防止降低可读性,不要花里胡哨的
表达式
每一个表达式都应该有一个值,比如c = 3 + 8
这种带=
的表达式就返回赋值运算符左侧变量的值;5 > 3
这种判断表达式返回一个布尔值
类型转换
1.基本的类型转换规则
- 涉及两种类型的运算,两个值会被分别转换成两种类型的更高级别
- 在赋值表达式语句中,计算的最终结果会被转换成被赋值变量的类型,这个过程可能导致类型升级(promotion)或者降级(demotion)
- 当作为函数参数传递时,
char
和short
会被转换成int
,float
会被转换double
- 类型转换出现在表达式时,无论是
unsigned
还是signed
的char
和short
都会被自动转换成int
- 类型级别从高到低分别是
long double
、double
、float
、unsignedlong long
、long long
、unsigned long
、long
、unsigned int
和int
。之所以short
和char
类型没有列出,是因为它们已经被升级为int
或者unsigned int
2.待赋值的值与目标类型不匹配时
- 目标类型是无符号整形,且待赋的值是整数时,额外的位直接被忽略。例如目标类型是8位的
unsigned char
,待赋的值是原始值求模256 - 如果目标类型是一个有符号整形,且待赋的值时整数,结果因实现而异
- 目标类型是一个整形,且待赋的值是浮点数时,该行为是未定义的
3.强制类型转换运算符
使用方式:(type)
,使用前应谨慎
参数:实参与形参
- 实参:
argument
- 形参:
parameter
实参是函数调用提供的值,形参是变量
第六章 C控制语句:循环
while循环
1.基本结构
while (expression)
statement
2.循环体可以是空语句
跳过所有整数输入,直到输入一个非整数
while (scanf("%d", &num) == 1)
; /* 跳过整数输入 */
3.C中可以使用_Bool
类型表示布尔型
不确定循环与计数循环
根据预先直到需要执行多少次循环可以分为计数循环和不确定循环
1.计数循环
- 必须初始化计数器
- 计数器与有效的值作比较
- 每次循环时递增计数器
for循环
while计数循环常常需要在循环体外初始化计数器,这常常容易导致错误,因此更佳的方案是使用for循环
1.格式
for
循环将初始化、测试和更新三个步骤组合到一起,格式如下:
for (n = 0; n < 10; n++)
statement;
2.在for循环中使用逗号
逗号可以使得
for
循环更佳灵活
#include
int main(void)
{
const int FIRST_OZ = 46;
const int NEXT_OZ = 20;
int ounces, cost;
printf("ounces cost \n");
for(ounces = 1, cost = FIRST_OZ; ounces <= 16; ounces++, cost += NEXT_OZ)
{
printf("%5d $%4.2f\n", ounces, cost / 100.0);
}
return 0;
}
输出结果:
ounces cost
1 $0.46
2 $0.66
3 $0.86
4 $1.06
5 $1.26
6 $1.46
7 $1.66
8 $1.86
9 $2.06
10 $2.26
11 $2.46
12 $2.66
13 $2.86
14 $3.06
15 $3.26
16 $3.46
do while出口循环
while
和for
循环都是入口条件循环,而do while
循环是出口条件循环,即在循环的每次迭代之后检查测试条件,这会保证至少执行循环体中的内容一次。
do
statement
while (expression)
嵌套循环
1.用于按行和按列显示数据
#include
#define ROWS 6
#define CHARS 10
int main(void)
{
int row;
char ch;
for (row = 0; row < ROWS; row++) /* 输出ROWS行 */
{
for (ch = 'A'; ch < ('A' + CHARS); ch++)
{
printf("%c", ch); /* 每一行输出CHARS个字符 */
}
printf("\n");
}
}
输出:
ABCDEFGHIJ
ABCDEFGHIJ
ABCDEFGHIJ
ABCDEFGHIJ
ABCDEFGHIJ
ABCDEFGHIJ
2.利用外层循环遍历控制内层循环
#include
int main(void)
{
const int ROWS = 6;
const int CHARS = 6;
int row;
char ch;
for (row = 0; row < ROWS; row++) /* 输出ROWS行 */
{
for (ch = ('A' + row); ch < ('A' + CHARS); ch++)
{
printf("%c", ch); /* 每一行输出CHARS个字符 */
}
printf("\n");
}
}
输出:
ABCDEF
BCDEF
CDEF
DEF
EF
F
数组简介
数组是按顺序存储的一系列类型相同的值
1.数组声明
float debts[20];
上述声明表示debts
是包含20个float
类型元素的数组
2.数组赋值
debts[5] = 88.32
注意c语言并不会去检查数组的下表是否正确,如果越界的话会导致数据被放在已被其他数据占用的地方,会破坏程序结果甚至导致程序运行出错
3.字符数组与字符串
char类型的数组末尾如果包含一个包含字符串结尾的
\0
,则数组内容构成了一个字符串
第七章 分支和跳转
if与else
if (expression)
statement1
else
statement2
else if
相当于if else分支的变形
循环辅助:continue与break
- continue:跳过本次迭代的剩余部分,进入下一个循环
- break:终止循环,执行下一个阶段
多重选择:switch和break
注意:如果不加break,会从匹配标签一直执行到switch结尾。所以有时候不加break也能用于实现多选。
1.形式
switch (expression)
{
case label1: statement1 //使用break跳出switch
case label2: statement2
default: statement3
}
2.约束
expression
和label
值都必须是整数,不过也可以包括char
类型
goto语句
一般不主张使用goto
,会使代码的可读性降低很多。但是在C语言中,有一种情况可以例外,即多重循环中碰到问题需要跳出循环(因为一条break
只能跳出一层循环):
while (expression)
{
for (i=1; i<=100; i++)
{
for(j=1; j<=100; j++)
{
其他语句
if (问题)
goto help;
其他语句
}
其他语句
}
其他语句
}
其他语句
help: 语句
第八章 字符输入输出和输入验证
单字符I/O
使用的是
getchar()
和putchar()
函数,它们都定义在stdio.h
头文件中
设计一个程序从键盘获取输入字符并输出,直到遇到#
字符停止:
#include
#define STOP '#'
int main(void)
{
char ch;
while ((ch = getchar()) != STOP)
{
putchar(ch);
}
return 0;
}
交互式输入输出:
这是第一行输入
这是第一行输入
下面输入空格行
下面输入空格行
最后一行正常输入
最后一行正常输入
带井号的输入
带井号的输入
这是一行带#号的输入
这是一行带%
缓冲区
1.无缓冲输入
在老式系统中运行上述代码,可能会出现如下情况:
HHeelllloo,, tthheerree .. II ww[enter]
lliikkee aa #
像这种直接重复打印用户输入结果的属于“无缓冲”输入,即程序可立即使用输入的字符(有一个问题就是你甚至无法直接修改你的输入)。
2.缓冲输入
大部分系统在用户按下Enter
键之前不会重复打印正在输入的字符,这种输入形式属于缓冲输入。用户输入的字符被收集并存储在一个被称为缓冲区(buffer)的临时存储区,按下Enter
键后程序才可以使用用户输入的字符。
3.使用缓冲区的原因
- 把若干字符作为一个块进行传输比逐个发送字符节约时间
- 如果用户打错字符,可以通过键盘修正错误
- 即使缓冲输入有诸多好处,但是无缓冲输入也有应用,比如在游戏等交互式程序中,我们希望按下一个键就执行对应的指令
4.缓冲的类别
- 完全缓冲I/O:指缓冲区被填满才刷新缓冲区(内容被发送至目的地),==通常出现在文件输入中==。缓冲区的大小取决于系统,常见的是512字节和4096字节
- 行缓冲I/O:出现换行符时刷新缓冲区,键盘输入通常是行缓冲输入,按下
Enter
键后才刷新缓冲区
结束键盘输入
1.文件,流和键盘输入
- 文件:存储器中存储信息的区域,通常文件都保存在某种永久存储器中。对于文字处理器,不仅要打开、读取和关闭文件,还需要把数据写入文件
- 流:C处理的是流而并非文件,流是一种实际输入或输出映射的理想化数据流。打开文件的过程就是把流和文件相关联,并且读写都通过流来完成
2.文件结尾
无论操作系统以何种方法检测文件结尾,在C语言中,getchar()
和scanf()
方法读取文件检测到文件结尾时将返回一个特殊的值EOF(end of file)
。
通常
EOF
定义在stdio.h
头文件中,常常被定义为-1
,因为getchar()
的返回值通常介于0~255
,-1
不对应于任何字符。
在下面这个程序中,每次按下Enter
键系统就会处理缓冲区中存储的字符,并在下一行打印输入行的副本,直到遇到EOF
:
#include
int main(void)
{
char ch;
while ((ch = getchar()) != EOF)
{
putchar(ch);
}
return 0;
}
第九章 函数
函数定义
- 函数原型
function prototype
:表明函数的类型 - 函数调用
function call
:表明在此处执行函数 - 函数定义
function definition
:表明函数要做什么
一些细节
- 函数声明可以置于
main
函数前面,也可以放在main
函数的声明变量处 - 注意,如果函数结尾没有
;
表明这是一个函数定义,而不是调用函数或者声明函数原型 - 你可以把函数和
main()
放在同一个文件,也可以把它们放在两个文件中。放在一个文件的单文件形式容易编译,而使用多个文件方便在不同的程序中使用同一个函数。 - 函数中的变量时局部变量
local variable
,意思是该变量只属于这个函数,我们可以在程序中其他地方使用这个变量,不过它们是同名的不同变量,不会引起冲突
函数体结构
==如果把函数放在一个单独的文件,要把
#define
和#include
指令也放入该文件==,如下面的函数体结构
调用函数
被调用的函数不关心传入的数值是来自常量、变量还是一般表达式。实际参数actual argument
是具体的值,该值要赋给作为形式参数的变量。
因为被调用函数的值是从主调函数中拷贝而来,所以无论被调用函数对拷贝数据进行什么操作,都不影响主调函数中的原始数据。
递归
C允许函数调用它自己,这种调用过程被称为递归recursion
。如果递归代码中没有终止递归的条件测试部分,一个调用自己的函数会无限递归。
可以使用循环的地方通常都可以使用递归,有时候用循环解决问题比较好,有时候用递归更好。递归方案更简洁但是效率却没有循环高。
1.递归的注意点
- 每级函数调用都有自己的变量,也就是每一层使用的相同名称的变量不同,它们对应的地址值也不同
- 递归函数中位于递归调用之前的语句,按照被调函数的顺序执行
- 递归函数中位于递归调用之后的语句,按照被调函数相反的顺序执行
- ==虽然每级递归都有自己的变量,但是并没有拷贝函数的代码==,程序按顺序执行函数中的代码,而递归调用就相当于又从头开始执行函数的代码,==除了为每次递归调用创建变量外,递归调用非常类似于一个循环语句==。
2.尾递归
最简单的递归形式是将递归调用置于函数的尾部,即正好在return
之前,尾递归是最简单的递归形式,它本身相当一个循环。
3.递归和倒序计算
在处理倒序问题时,递归比循环简单。
举个例子,我们需要编写一个函数,打印一个整数的二进制数。
- 在二进制中,奇数的末尾一定是1,偶数的末尾一定是0,所以对于数字n,通过
n % 2
即可确定n的二进制最后一位是1还是0 - 获取下一个数字的方法:要获取下一位数字必须把原数除以2,这种方法相当于在十进制下把小数点左移一位,然后对该数除以2的余数判断下一位是0还是1
- 如何停止:当与2相除的结果小于2时停止计算
实现函数:
#include
void to_binary(unsigned long n);
int main(void)
{
int num = 0;
printf("please input a number: \n");
scanf("%d", &num);
to_binary(num);
return 0;
}
void to_binary(unsigned long n) /* 递归函数 */
{
int r;
r = n % 2;
if (n >= 2)
to_binary(n/2);
putchar(r == 0 ? '0' : '1');
}
4.递归的优缺点
递归优点是为某些编程问题提供了最简单的解决方案,缺点是一些递归算法会快速消耗计算器的内存资源。另外,递归也不方便进行阅读和维护。
举个例子,斐波那契数列(每一个数都是前两个数字之和):
#include
unsigned long Fibonacci(unsigned n);
int main(void)
{
unsigned num = 0;
printf("please input a number: \n");
scanf("%d", &num);
unsigned long result = Fibonacci(num);
printf("the result is: %ld\n", result);
return 0;
}
unsigned long Fibonacci(unsigned n)
{
// 双递归
if (n > 2)
return Fibonacci(n-1) + Fibonacci(n-2);
else
return 1;
}
在这个函数中,假设我们调用了FIbonacci(40)
,那么第一级调用创建了变量n
,它会调用两次函数,在二级递归中分别创建两个变量,第三级递归中又会创建四个变量。每级递归创建的变量都是上一级递归的两倍,所以变量的数量呈指数型增长,很快会消耗计算机的大量内存从而使得程序崩溃。
第十章 数组和指针
数组
1.声明
float candy[365];
char code[12];
int states[50];
2.初始化
我们可以通过用逗号分隔的值列表(花括号括起来)来初始化数组
int powers[8] = {1,2,4,6,8,16,32,64}
一般我们最好用常量来表示数组的大小:
#define MONTHS 12
int days[MONTHS] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
另外,如果我们可以把数组设置为只读,这样程序只能从数组中检索值,不能把新值写入数组。可以使用const
声明和初始化数组:
const int days[MONTHS] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
3.未经初始化的数组
在使用数组前必须先初始化,与普通变量类似,在使用数组元素之前必须给它们赋初值,编译器使用的值时内存相应位置上的现有值,因此可能得到意料之外的数组元素。
4.部分初始化的数组
以int
数组为例,如果部分初始化数组,那么未被初始化的数组元素就会被初始化为0。
5.获取数组元素个数
const int days[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
for (index = 0; index < sizeof days / sizeof days[0]; index++)
printf("Month %2d has %d days.\n", index + 1, days[index]);
return 0;
6.给数组元素赋值
days[1] = 31
数组下标越界
注意int days[MONTHS]
数组的最后一个元素是days[MONTHS-1]
,如果访问days[MONTHS]
会导致越界。
良好的遍历数组元素的习惯为:
#define SIZE 4
int main(void)
{
int arr[SIZE];
for (i = 0; i < SIZE; i++)
{
...
}
}
多维数组
float rain[5][12]
// 表示rain是一个内含5个元素的数组,每个元素是一个内含12个float元素的数组,即rain中每个元素的类型是float[12]
1.处理一个二维数组的实例
/* rain.c --计算每年的总降水量、年平均降水量和5年终每个月的平均降水量 */
#include
#define MONTHS 12
#define YEARS 5
int main(void)
{
// 用2010年~2014年的降水量初始化二维数组
const float rain[YEARS][MONTHS] =
{
{4.3, 4.3, 4.3, 3.0, 2.0, 1.2, 0.2, 0.2, 0.4, 2.4, 3.5, 6.6},
{9.5, 8.2, 1.2, 1.6, 2.4, 0.0, 5.2, 0.9, 0.3, 0.9, 1.4, 7.3},
{9.1, 8.5, 6.7, 4.3, 2.1, 0.8, 0.2, 0.2, 1.1, 2.3, 6.1, 8.4},
{7.2, 9.9, 8.4, 3.3, 1.2, 0.8, 0.4, 0.0, 0.6, 1.7, 4.3, 6.2},
{7.6, 5.6, 3.8, 2.8, 3.8, 0.2, 0.0, 0.0, 0.0, 1.3, 2.6, 5.2}
};
int year, month;
float subtot, total;
printf(" YEAR RAINFALL(inches)\n");
for (year = 0, total = 0 ; year < YEARS; year ++)
{
for (month = 0, subtot = 0; month < MONTHS; month++)
subtot += rain[year][month];
printf("%5d %15.1f\n", 200 + year, subtot);
total += subtot; // 5年的总降水量
}
printf("\nThe yearly average is %.1f inches.\n\n", total / YEARS);
printf("MONTHLY AVERAGE:\n\n");
printf(" Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec \n");
for (month = 0; month < MONTHS; month++)
{
for (year = 0, subtot = 0; year < YEARS; year++)
{
subtot += rain[year][month];
}
printf("%4.1f", subtot / YEARS);
}
printf("\n");
return 0;
}
输出结果:
YEAR RAINFALL(inches)
200 32.4
201 38.9
202 49.8
203 44.0
204 32.9
The yearly average is 39.6 inches.
MONTHLY AVERAGE:
Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
7.5 7.3 4.9 3.0 2.3 0.6 1.2 0.3 0.5 1.7 3.6 6.7
2.初始化二维数组
仍然以上面的例子为例,初始化二维数组:
const float rain[YEARS][MONTHS] =
{
{4.3, 4.3, 4.3, 3.0, 2.0, 1.2, 0.2, 0.2, 0.4, 2.4, 3.5, 6.6},
{9.5, 8.2, 1.2, 1.6, 2.4, 0.0, 5.2, 0.9, 0.3, 0.9, 1.4, 7.3},
{9.1, 8.5, 6.7, 4.3, 2.1, 0.8, 0.2, 0.2, 1.1, 2.3, 6.1, 8.4},
{7.2, 9.9, 8.4, 3.3, 1.2, 0.8, 0.4, 0.0, 0.6, 1.7, 4.3, 6.2},
{7.6, 5.6, 3.8, 2.8, 3.8, 0.2, 0.0, 0.0, 0.0, 1.3, 2.6, 5.2}
};
初始化时也可以省略内部的花括号,只保留最外面的一堆花括号,只要保证初始化的数值个数正确即可。但是如果初始化的数值不够,则按照先后顺序进行初始化直到用完所有的值,后面没有初始化的元素被统一初始化为0。
下面的图展示了这两种初始化方法的不同之处:
很多时候,储存在数组比如rain中的元素不能修改,因此我们需要加上const关键字声明该数组。
3.三维数组
可以将一维数组想成一行数据,将二维数组想象成数据表,将三维数组想象成一叠数据表。下面box
三维数组就相当于10个二维数组(每个数组都是20行30列)。
int box[10][20][30]
指针和数组
对于一个数组而言,数组名是数组首元素的地址,也就是说如果flizny是一个数组,下面的语句成立:
flizny == &flizny[0];
我们可以根据这个性质灵活地使用数组:
dates + 2 == &dates[2] //相同地址
*(dates + 2) == dates[2] //相同值
函数、数组和指针
如果一个函数需要处理数组,我们可以写成:
int sum(int * ar)
{
int i;
int total = 0;
for (i=0; i<10; i++)
total += ar[i];
return total;
}
我们也可以将需要处理的数组个数作为第二个参数传入,否则处理多少个元素就只能在代码中写死:
int sum(int * ar, int n)
{
int i;
int total = 0;
for (i = 0; i < n; i++)
total += ar[i];
return total;
}
只有在函数原型或者函数定义头中,我们才能用
int arr[]
替换int * ar
:
int sum(int ar[], int n);
.使用指针形参
函数处理数组必须知道何时开始与何时结束,一种有效的做法是用一个整数形参表明待处理数组的元素个数(指针形参也表明了数组中的数据类型);另一种做法是传递两个指针,第一个指针指明数组的开始处,第二个指针指明数组的结束处。
int sump(int * start, int * end)
{
int total = 0;
while (start < end)
{
total += *start;
start++;
}
return total;
}
注意这里的
end
指向数组最后一个元素的下一个元素,这种“越界”指针使得函数调用更加简洁:
answer = sump(marbles, marbles + SIZE);
指针操作
- 赋值:可以把地址赋给指针,包括数组名、带地址运算符
&
的bi按量名或者另一个指针赋值。 - 解引用:
*
运算符给出指针指向地址上所存储的值。 - 取址:指针变量本身也有自己的地址和值,也可以取值
- 指针和整数相加:使用
+
运算符把指针与整数相加,整数会和指针所指向类型的大小(以字节为单位)相乘,然后把结果与初始地址相加。 - 递增指针:递增指向数组元素的指针可以让指针移动至数组的下一个元素
- 指针减去一个整数:和指针与整数相加相似
- 指针求差:通常求差的两个指针分别指向同一个数组的不同元素,通过求差计算出两个元素之间的距离。
- 比较:使用关系运算符可以比较两个指针的值,前提是两个指针都指向相同类型的对象
保护数组中的数据
如果一个函数需要数组参数的话,通常都是传递指针,因为这样可以提高效率(否则如果一个函数需要按值传递数组,则必须分配足够的空间来存储原来数组的副本,然后把原数组所有的数据拷贝至新的数组中)。
这会导致一个问题:C通常都按值传递数据,这样做可以保证数据的完整性,因为如果函数只是使用原有数据的副本,就不会意外修改原始数据。但是使用指针就很难保证数组本身的完整性。
1.对形式参数使用const
如果函数不需要修改数组中的数据内容,我们可以在函数原型和函数定义中声明形式参数时使用const关键字。
int sum(const int ar[], int n); /* 函数原型 */
int sum(const int ar[], int n); /* 函数定义*/
{
int i;
int total = 0;
for (i = 0; i < n; i++)
total += ar[i];
return total;
}
以上代码中的const
关键字告诉编译器这个函数不能修改ar
指向数组中的内容。
2.const的其他内容
虽然使用#define
可以创建类似功能的符号常量,但是const
的用法更加灵活,可以创建const
数组、const
指针和指向const
的指针。
- 使用
const
关键字保护数组
后续修改该数组元素的值,编译器会报错
#define MONTH 12
const int days[MONTHS] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
- 指向
const
的指针不能用于改变值
下面这个例子中指针类型是const double*
,因此我们不能通过指针来修改这个double
数组
double rates[5] = {88.99, 100.12, 59.45, 183.11, 340.5};
const double * pd = rates;
*pd = 29.89; //不允许
pd[2] = 222.22; //不允许
rates[0] = 99.99; //允许,因为rates并非被cosnt限定
pd++; //允许,可以改变指针值
3.需要注意的点
- 将
const
数据和非const
数组的地址初始化为指向cosnt
的指针或为其赋值是合法的:
// 构造普通数组和const数组
double rates[5] = {88.99, 100.12, 59.45, 183.11, 340.5};
const double locked[4] = {0.08, 0.075, 0.0725, 0.07};
// 构造指向const的指针,即我们不能通过指针修改数组
const double *pc = rates; //有效
pc = locked; //有效
pc = &rates[3]; //有效
- 只能将非
const
数据的地址赋值给普通指针(防止通过指针修改const
数组)
// 构造普通数组和const数组
double rates[5] = {88.99, 100.12, 59.45, 183.11, 340.5};
const double * pd = rates;
double * pnc = rates; //有效,指向非const数组
pnc = lockedl //无效,普通指针不能指向const数组
pnc = &rates[3]; //有效
因此:如果一个函数接收
const
数组,那么该函数不仅可以保护数据数组,还可以让函数处理const
数组。
指针和多维数组
举个例子:int zippo[4][2]
,我们可以推导出如下结论:
- 数组名
zippo
是该二维数组首元素的地址,即内含两个int
值的数组的地址 - 因为
zippo
是数组首元素的地址,所以zippo
的值和&zippo[0]
的值相同。而zippo[0]
本身就是一个内含两个整数的数组,所以zippo[0]
的值和首元素(单个int
)的地址(即&zippo[0][0]
的值相同)。总之zippo[0]
是一个占用一个int
大小对象的地址,而zippo
是一个占用两个int
大小对象的地址。虽然zippo
和zippo[0]
的值相同,但是他们的类型不同。 - 给指针或者地址加1,其值会增加对应类型大小的数值。因为
zippo
指向的对象占用了两个int
大小,而zippo[0]
指向的对象只占用一个int
大小,因此zippo + 1
和zippo[0] + 1
的值不同。 - 解引用:
*(zippo[0])
表示存储在zipoo[0][0]
上的值,即一个int
类型;*zippo
表示数组首元素zippo[0]
的值,即&zippo[0][0]
。
结论:**zippo
与zippo[0][0]
等价。
- 与
zippo[2][1]
等价的指针表示法是*(*(zippo+2) + 1)
1.指向多维数组的指针
仍然以int zippo[4][2]
为例,声明一个pz
指向该二维数组:
int (* pz)[2];
注意
[]
的优先级高于*
,如果漏掉括号相当于:
int * pax[2] // pax是内含两个指针元素的数组,每个元素都是指向int的指针
2.函数和多维数组
/* rain.c --计算每年的总降水量、年平均降水量和5年终每个月的平均降水量 */
#include
#define ROWS 3
#define COLS 4
void sum_rows(int ar[][COLS], int rows);
void sum_cols(int [][COLS], int); // 省略参数名
int sum2d(int(*ar)[COLS], int rows); // 另一种表示二维数组的方法
int main(void)
{
int junk[ROWS][COLS] = {
{ 2, 4, 6, 8 },
{ 3, 5, 7, 9 },
{ 12, 10, 8, 6 }
};
sum_rows(junk, ROWS);
sum_cols(junk, ROWS);
printf("Sum of all elements = %d\n", sum2d(junk, ROWS));
return 0;
}
void sum_rows(int ar[][COLS], int rows)
{
int r;
int c;
int tot;
for (r = 0; r < rows; r++)
{
tot = 0;
for (c = 0; c < COLS; c++)
tot += ar[r][c];
printf("row %d: sum = %d\n", r, tot);
}
}
void sum_cols(int ar[][COLS], int rows)
{
int r;
int c;
int tot;
for (c = 0; c < COLS; c++)
{
tot = 0;
for (r = 0; r < rows; r++)
tot += ar[r][c];
printf("col %d: sum = %d\n", c, tot);
}
}
int sum2d(int ar[][COLS], int rows)
{
int r;
int c;
int tot = 0;
for (r = 0; r < rows; r++)
for (c = 0; c < COLS; c++)
tot += ar[r][c];
return tot;
}
输出结果:
row 0: sum = 20
row 1: sum = 24
row 2: sum = 36
col 0: sum = 17
col 1: sum = 19
col 2: sum = 21
col 3: sum = 23
Sum of all elements = 80
变长数组VLA
C规定数组的维数必须是常量,不能用变量来替代COLS。C99新增了变长数组(variable-length array, VLA
),允许使用变量表示数组的维度。
int quarters = 4;
int regions = 5;
doible sales[regions][quarters]; //一个变长数组(VLA)
变长数组中的“变”不是指可以修改已创建数组的大小,一旦创建了变长数组,它的大小保持不变。这里的变是指:在创建数组时,可以使用变量来指定数组的维度。
复合字面量
字面量指的是除符号常量之外的常量,例如5
是int
类型的字面量,81.3
是double
类型的字面量,复合字面量指的是代表数组和结构内容的字面量。
// 普通数组
int diva[2] = {10, 20};
// 复合字面量的匿名数组
(int [2]){10, 20};
第十一章 字符串和字符串函数
表示字符串和字符串I/O
字符串是以空字符(
\0
)结尾的char
类型数组
#include
#define MSG "I am a symbolic string constant."
#define MAXLENGTH 81
int main(void)
{
char words[MAXLENGTH] = "I am a string in an array.";
const char * pt1 = "Something is pointing at me.";
puts("Here are some strings:");
puts(MSG);
puts(words);
puts(pt1);
words[8] = 'p';
puts(words);
return 0;
}
输出:
Here are some strings:
I am a symbolic string constant.
I am a string in an array.
Something is pointing at me.
I am a spring in an array.
puts()
函数只显示字符串,而且自动在显示的字符串末尾加上换行符。
1.在程序中定义字符串
上述程序中用了字符串常量、char类型数组和指向char的指针三种方法定义字符串,程序中应该确保有足够的空间储存字符串。
- 字符串字面量(字符串常量)
用双引号括起来的内容被称为字符串字面量(string literal),也叫做字符串常量(string constant),双引号中的字符和编译器自动加入末尾的\0
字符都会作为字符串存储在内存中。 - 字符串常量属于静态存储类别(
static storage class
),这说明如果在函数中使用字符串常量,该字符串只会被存储一次,在整个程序的生命期内存在,即使该函数被调用多次。
2.字符串数组和初始化
定义字符串数组时,必须让编译器直到需要多少空间。一种方法是用足够空间的数组存储字符串:
const char m1[40] = "Limit yourself to one line's worth.";
// 下面是等价的但是麻烦的标准数组初始化
const char m1[40] = {
'L', 'i', 'm', 'i', 't', ' ', 'y', 'o', 'u', 'r', 's', 'e', 'l', 'f',
' ', 't', 'o', ' ', 'o', 'n', 'e', ' ', 'l', 'i', 'n', 'e', '\'', 's',
' ', 'w', 'o', 'r', 't', 'h', '.', '\0'
};
// 如果没有最后的空字符串,那么这就不是一个字符串而是一个字符数组。
// 在指定数组大小时,需要确保数组的元素至少比字符串长度多1(为了容纳最后的空字符),所有未被使用的元素都被自动初始化为\0
3.数组和指针
可以使用指针表示法创建字符串,例如:
const char * pt1 = "Something is pointing at me.";
该声明与下述声明几乎相同:
const char ar1[] = "Something is pointing at me.";
- 数组形式:
数组形式ar[]
在计算机的内存中分配为一个内含29个元素的数组(每个元素对应一个字符,加上末位的空字符\0
),每个元素被初始化为字符串字面量对应的字符。字符串存储在静态存储区(static memory)中。但是,程序在开始运行时才会为该数组分配内存。此时,才将字符串拷贝到数组中(到12章解释)。
注意,此时字符串有两个副本,一个是在静态内存中的字符串字面量,另一个是存储在ar1数组中的字符串。
此后,编译器便把数组名ar1识别为该数组首元素地址&ar1[0]
的别名。==这里关键要理解,在数组形式中,ar1
是地址常量。不能更改ar1
,如果改变了ar1
,则意味着改变了数组的存储位置(即地址)。==
可以进行类似
ar1 + 1
这样的操作,但是不允许进行++ar1
这样的操作。
- 指针形式:
指针形式*pt1
也使得编译器为字符串在静态存储去预留29个元素的空间。另外,一旦开始执行程序,它会为指针变量pt1
留出一个存储位置,并将字符串的地址存储在指针变量中。该变量最初指向该字符串的首字符,但是它的值可以改变。因此可以使用递增运算符,例如++pt1
将指向第二个字符(o
)。
字符串字面量被视为
const
数据,由于pt1
指向这个const
数据,所以应该把pt1
声明为指向const
数据的指针。==这意味着不能使用pt1
改变它所指向的数据,但仍然可以改变pt1
的值。==如果把一个字符串字面量拷贝给一个数组,就可以随意改变数据,除非把数组声明为const
。
- 总结:
初始化数组把静态存储区的字符串拷贝到数组中,而初始化指针只把字符串的地址拷贝给指针。
/* addresses.c --字符串地址 */
#include
#define MSG "I'm special"
int main(void)
{
char ar[] = MSG;
const char * pt = MSG;
printf("address of \"I'm special\": %p \n", "I'm special");
printf(" address ar: %p\n", ar);
printf(" address pt: %p\n", pt);
printf(" address of MSG: %p\n", MSG);
printf("address of \"I'm special\": %p \n", "I'm special");
return 0;
}
输出结果:
address of "I'm special": 0x10b505f30
address ar: 0x7fff546faa7c
address pt: 0x10b505f30
address of MSG: 0x10b505f30
address of "I'm special": 0x10b505f30
注意三个"I'm special"存储的地址是相同的,
pt
与MSG
存储的位置也是相同的。静态数据使用的内存与ar
使用的动态内存不同,不仅值不同,特定编译器甚至使用不同的位数表示两种内存。
4.数组和指针的区别
char heart[] = "I love Tillie!";
const char *head = "I love Millie";
- 主要区别:数组名heart是常量,指针名head是变量
- 数组表示法:
heart[i]
和head[i]
都是合法的 - 指针加法:
*(heart + i)
和*(head + i)
都是合法的 -
head = heart
是合法的,让head
指向数组heart
,但是heart = head
是非法的,因为heart
是常量 -
heart[7] = 'M'
是合法的,因为数组名是常量,但是数组元素是变量;head
指向指针,所以这种操作是未定义的
5.字符串数组
创建一个字符串数组会很方便,我们可以通过数组下标访问多个不同的字符串,有两种方法构造:
- 指向字符串的指针数组
const char *mytalents[5] = {
"Adding numbers swiftly",
"Multiplying accurately",
"Stashing data".
"Following instructions to the letter",
"Understanding the C language"
};
- char类型数组的数组
char yourtalents[5][40] = {
"Walking in a staight line",
"Sleeping",
"Watching television",
"Mailing letters",
"Reading email"
};
mytalents
数组是一个内含5个指针的数组,在我们的系统中共占用40字节,而yourtalents
是一个内含5个数组的数组,每个数组内含40个char
类型的值,共占用200字节。
mytalents
中的指针指向初始化时所用的字符串字面量的位置,这些字符串字面量被储存在静态内存中;而yourtalents
中的数组则存储着字符串字面量的副本,所以每个字符串都被存储了两次。
为字符串数组分配内存的使用率较低,yourtalents
中每个元素的大小必须相同,而且必须是能存储最长字符串的大小。
6.指针和字符串
字符串的绝大多数操作都是通过指针完成的
/* p_and_s.c --指针和字符串 */
#include
int main(void)
{
const char * mesg = "Don't be a fool!";
const char * copy;
copy = mesg;
printf("%s\n", copy);
printf("mesg = %s; &mesg = %p; value = %p\n", mesg, &mesg, mesg);
printf("copy = %s; © = %p; value = %p\n", copy, ©, copy);
}
输出结果:
Don't be a fool!
mesg = Don't be a fool!; &mesg = 0x7fff55dc6a88; value = 0x109e39f56
copy = Don't be a fool!; © = 0x7fff55dc6a80; value = 0x109e39f56
注意两个指针是不同的(指针本身的地址不同),但是它们都指向同一个地址(字符串首字符的地址)。这就意味着程序并没有拷贝字符串,而只是拷贝一个地址,防止字符串过长时拷贝效率较低。
如果确实需要拷贝整个数组,可以使用
strcpy()
或者strncpy()
函数。
字符串输入
如果想把一个字符串读入程序,必须预留存储该字符串的空间,然后用输入函数获取该字符。
1.分配空间
不要指望计算机在读取字符串时顺便计算它的长度,然后再分配空间(计算机并不会这么做)
最简单的做法是,在声明时显示指明数组的大小:
// 正确的做法
char name[81];
// 错误的做法
// 虽然可能通过编译,但是在读入name时可能会擦写掉程序中的数据或者代码
char *name;
scanf("%s", name);
为字符串分配内存后便可以读入字符串,C库提供了许多读取字符串的函数:scanf()
、gets()
和fgets()
函数。
2.不幸的gets()函数
scanf()
函数和转换说明%s
只能读取一个单词,但是在程序中经常要读取一整行输入。gets()
函数读取一整行输入,直到遇到换行符,然后丢弃换行符存储其余字符,并在这些字符的末尾添加一个空字符使其成一个C
字符串,
但是
gets()
函数并不能检查数组是否有足够大小可以装得下行,即gets()
函数只知道数组的开始处,但是并不知道数组中有多少元素。此时输入的字符串过长,会造成==缓冲区溢出(buffer overflow)==,即多于的字符只是占用了尚未使用的内存,就不会立即出现问题。
C11标准委员会采取了强硬的态度,直接从标准中废除了
gets()
函数。
3.fgets()
函数(和fputs()
)
fgets()
通过第二个参数限制读入的字符数来解决溢出的问题:
-
fgets()
函数的第二个参数指明了读入字符的最大数量,如果该参数的值是n
,那么fgets()
将读入n-1
个字符,或者读到遇到的第一个换行符 - 如果
fgets()
读到一个换行符,会把它储存在字符串中,这与gets()
丢弃换行符函数不同 -
fgets()
的第三个参数指明要读入的文件,如果读入从键盘输入的数据,需要用stdin
作为参数 -
fgets()
把换行符放在字符串的末尾,通常与fputs()
函数配对使用
4.gets_s()
函数
C11
新增的gets_s()
函数与fgets()
类似,用一个参数限制读入的字符数,区别在于:
-
gets_s()
只从标准输入行中读取数据,所以不需要第三个参数 -
gets_s()
如果读取到换行符,会丢弃掉而不是存储它 -
gets_s()
读到最大字符数都没有读到换行符,会执行以下几步:首先把目标数组中的首字符设置成空字符,读取并丢弃随后的输入直至读到换行符或文件结尾,然后返回空指针
在输入行未超过最大字符数时,
gets_s()
hegets()
几乎一样。输入行过长时,gets()
会擦写现有数据,存在安全隐患。
5.scanf()
函数
与其他函数相比,
scanf()
函数用于获取单次而非整行输入,它会从第一个非空白字符开始,到下一个空白字符(空格、空行、制表符或者换行符)结束作为字符串。
另外,scanf()
函数返回一个整数值,该值等于scanf()
成功读取的项数或EOF(读到文件结尾时返回EOF)
另外,scanf()
和gets()
一样都存在着输入行过长时数据溢出的问题,不过在%s
转换说明中使用字段宽度就可以防止溢出。
字符串输出
1.puts()
函数
将字符串的地址作为参数传递给该函数即可使用:
- 在显示字符串时会在末尾自动添加一个换行符
-
puts()
函数遇到空字符时就会停止输出
2.fputs()
函数
fputs()
相当于是puts()
函数针对文件定制的版本:
-
fputs()
第二个参数指明要写入数据的文件 -
fputs()
不会在输出的末尾加上换行符
3.printf()
函数
printf()
函数可以执行更多的功能,但是计算机执行的时间也更长
自定义输入/输出函数
可以通过getchar()
和putchar()
的基础上自定义需要的输入输出函数。
字符串函数
1.strlen()
统计字符串的长度
2.strcat()
用于拼接字符串,接收两个字符串作为参数,并将第二个字符串的备份
3.strncat()
strcat()
函数无法检查第1个数组是否能够容纳第2个字符串,如果分配给第1个数组的空间不够大,多出来的字符溢出到相邻存储单元时就会出问题。strncat()
函数第3个参数指定了最大添加字符数。
4.strcmp()
和 strncmp()
如果是要比较两个字符串的内容是否相同,可以使用该函数。strncmp()
在比较两个字符串时,可以比较到字符不同的地方,也可以比较第3个参数指定的字符数。
5.strcpy()
和strncpy
如果pts1
和pts2
都是指向字符串的指针,那么下面语句拷贝的是字符串的地址而不是字符串本身:
pts2 = pts1;
如果希望拷贝整个字符串需要使用strcpy()
函数,可以将整个字符串从临时数组拷贝到目标数组。
6.sprintf()
该函数和printf()
类似,但是它是把数据写入字符串,而不是打印到显示器上。因此,该函数可以把多个元素合成一个字符串。sprintf()
的第1个参数是目标字符串的地址。
字符串示例:字符串排序
/* sort_str.c --读入字符串,并排序字符串 */
#include
#include
#define SIZE 81 /*限制字符串长度,包括\0*/
#define LIM 20 /*可读入的最多行数*/
#define HALT "" /*空字符串停止输入*/
void stsrt(char *strings [], int num); /*字符串排序函数*/
char * s_gets(char * st, int n);
int main(void)
{
char input[LIM][SIZE]; /*储存输入的数组*/
char *ptstr[LIM]; /*内含字符指针的数组*/
int ct = 0; /*输入计数*/
int k; /*输出计数*/
printf("Input up to %d lines, and I will sort them.\n", LIM);
printf("To stop, press the Enter key at a line's start.\n");
while (ct < LIM && s_gets(input[ct], SIZE) != NULL
&& input[ct][0] != '\0')
{
ptstr[ct] = input[ct]; /*设置指针指向字符串*/
ct++;
}
stsrt(ptstr, ct); /*字符串排序函数*/
puts("\nHere's the sorted list:\n");
for(k = 0; k < ct; k++)
puts(ptstr[k]); /*排序之后的指针*/
return 0;
}
/* 字符串-指针-排序函数 */
void stsrt(char *strings [], int num)
{
char *temp;
int top, seek;
for (top = 0; top < num - 1; top++)
for (seek = top + 1; seek < num; seek++)
if (strcmp(strings[top], strings[seek]) > 0)
{
temp = strings[top];
strings[top] = strings[seek];
strings[seek] = temp;
}
}
char * s_gets(char * st, int n)
{
char * ret_val;
int i = 0;
ret_val = fgets(st, n, stdin);
if (ret_val)
{
while (st[i] != '\n' && st[i] != '\0')
i++;
if (st[i] == '\n')
st[i] = '\0';
else
while (getchar() != '\n')
continue;
}
return ret_val;
}
输出结果:
Input up to 20 lines, and I will sort them.
To stop, press the Enter key at a line's start.
O that I was where I would be,
Then would I be where I am not;
But where I am I must be,
And where I would be I can not.
Here's the sorted list:
And where I would be I can not.
But where I am I must be,
O that I was where I would be,
Then would I be where I am not;
1.指针排序而非字符串排序
上述程序的巧妙之处在于排序的是指向字符串的指针,而非字符串本身
最初时ptrst[0
被设置为input[0]
,ptrst[1]
被设置为input[1]
。这意味着ptrst[i]
指向input[i]
的首字符,该程序把ptrst
重新排列,但是并没有改变input
。
2.选择排序
selection sort algorithm
的具体做法是,利用for
循环依次把每个元素与首元素比较,如果待比较的元素在当前首元素的前面,则交换两者。外层for
循环重复这一过程,这次从input
第二个元素开始,当内层循环执行完毕时,ptrst
中第2个元素指向排在第2的字符串。
C库中有一个更高级的排序函数
qsort()
,该函数使用一个指向函数的指针进行排序比较。
ctype.h
字符函数和字符串
虽然
ctype.h
函数不能处理整个字符串,但是可以处理字符串中每一个字符。
-
toupper()
:将字符转为大写 -
ispunct()
:判断字符是否为标点 -
strchr()
:查找换行符
find = strchr(line, '\n');
if (find) // 如果地址不是NULL, 用空字符串替换
*find = '\0';
把字符串转换为数字
1.atoi
该函数能将字符串转化为数字,在字符串仅以整数开头时也能处理,即只把开头的整数转换为字符,例如atoi("42regular")
将返回42
。如果是非数字则返回0
。
2.atof
和atol
这两函数工作原理和atoi()
类似,但是前者返回double
类型,后者返回long
类型。
3.strtol()
ANSIC
还提供一套更加智能的函数: strtol()
把字符串转换为long
类型的值,strtoul()
把字符串转换为unsigned long
类型的值,strtod()
把字符串转换为double
类型的值。这些函数的智能之处在于识别和报告字符串中的首字符是否是数字。而且strtol()
和strtoul()
还可以指定数字的进制。
// nptr是指向待转换字符串的指针,endptr是一个指针的地址,被设置为标识输入数字结束字符的地址
// base表示以什么进制写入数字
# 第十二章 存储类别、链接和内存管理
## 存储类别`storage class`
* 对象:从硬件的角度,被存储的每一个值都占用一定的物理内存,C语言把这样的一块内存称之为对象`object`
* 标识符:标识符`identifier`是一个名称,指定特定对象的内容
* 左值:==指定对象的表达式被称之为左值==
```c
int entity = 3;
int * pt = &entity;
int ranks[10];
entity
既是标识符也是左值;*pt
虽然不是标识符,因为它不是一个名称,但是它既是表达式也是左值。ranks + 2 * entity
既不是标识符(不是名称)也不是左值(==不指定内存位置上的值==)。但是*(ranks + 2 * entity)
是一个左值,因为它的确指定了特定内存为止上的值,即ranks
数组上第7个元素。
如果可以用左值修改所指向对象的值,那么该左值就是一个可修改的左值(
modifiable lvalue
)
可以用存储期storage duration
描述对象,所谓存储期是指对象在内存中保留了多长时间。标识符用于访问对象,可以用作用域scope
和链接linkage
描述标识符,作用域和链接表明了程序中哪些部分可以使用它。
1.作用域
作用域描述了程序中可访问标识符的区域,一个C变量的作用域可以是块作用域、函数作用域、函数原型作用域或文件作用域
2.链接
C变量有三种链接:外部链接、内部链接或无链接。具有块作用域、函数作用域或函数原型作用域的变量都是无链接变量。这意味着这些变量属于定义它们的块、函数或原型私有。具有文件作用域的变量可以是外部链接或内部链接。外部链接可以在多文件程序中使用,内部链接变量只能在一个翻译单元中使用。
总而言之,“内部链接的文件作用域”即“文件作用域”,“外部链接的文件作用域”为“全局作用域”或者“程序作用域”。
区别文件作用域变量是内部链接还是外部链接可以看外部定义中是否使用了存储类别说明符static
:
int giants = 5; // 外部链接
static int dodgers = 3; // 内部链接
3.存储期
作用域和链接标识了标识符的可见性。存储期描述了通过这些标识符访问的对象的生存期。C对象有4种存储期:静态存储期、线程存储期、自动存储期和动态分配存储期。
静态存储期
如果对象具有静态存储期,那么它在程序的执行期间内一直存在,文件作用域变量具有静态存储期。(static
表明了其链接属性,而非存储期,以static
声明的文件作用域变量具有内部链接,但是无论是内部链接还是外部链接所有的文件作用域变量都具有静态存储期)线程存储期
用于并发程序设计,程序执行被分为多个线程。具有线程存储期的对象,从被声明到线程结束一直存在,以_Thread_local
声明一个对象时,每个线程都会获得该变量的私有备份。自动存储期
块作用域的变量通常都具有自动存储器,当程序进入定义这些变量的块时,为这些变量分配内尺寸;当退出这个块时,释放刚才为变量分配的内存。
void bore(int number)
{
int index;
for (index = 0; index < number; index++)
puts("They don't make them the way they used to.\n");
return 0;
}
变量number
和index
在每次调用bore()
函数时被创建,在离开函数时被销毁。
然而,块作用域也能具有静态存储期。为了创建这样的变量,要把变量声明在块中,且在声明前加上关键字
static
:
void more(int number)
{
int index;
static int ct = 0;
...
return 0;
}
这里,变量ct
存储在静态内存中,它从程序被载入到程序结束期间都存在。但是,它的作用域定义在more()
函数块中。只有在执行该函数时,程序才能调用ct
访问它锁指定的对象。(但是,该函数可以给其他函数提供该存储区的地址以便间接地访问该对象,例如通过指针形参或者返回值)
存储类别 | 存储期 | 作用域 | 链接 | 声明方式 |
---|---|---|---|---|
自动 | 自动 | 块 | 无 | 块内 |
寄存器 | 自动 | 块 | 无 | 块内,使用关键字register |
静态外部链接 | 静态 | 文件 | 外部 | 所有函数外 |
静态内部链接 | 静态 | 文件 | 内部 | 所有函数外,使用关键字static |
静态无链接 | 静态 | 块 | 无 | 块内,使用关键字static |
4.自动变量
属于自动存储类别的变量具有自动存储期、块作用域且无链接。==默认情况下,声明在块或函数头中的任何变量都属于自动存储类别==。为了更清楚地表明你的意图,你可以显式使用关键字auto
。
自动存储期意味着程序在进入该变量声明所在的块时变量存在,程序在退出该块时变量消失。原来该变量占用的内存位置现在可做他用。
注意:
- 没有花括号的块:作为循环或者
if
语句中的一部分,即使不用花括号,也是一个块 - 自动变量的初始化:自动变量不会初始化,除非显式地初始化它,比如:
// tents变量被初始化为5
// repid变量的值时之前占用分配给`repid`的空间中的任意值,别指望它是0
int main(void)
{
int repid;
int tents = 5;
}
5.寄存器变量
如果幸运的话,寄存器变量可以存储在CPU的寄存器(最快的可用内存)中,但是可声明为register
的数据类型有限,例如处理器中的寄存器可能没有足够大的空间来储存double
。
6.块作用域的静态变量
静态变量
static variable
指的是该变量在内存中原地不动,而非说它的值不变。
块作用域的静态变量在程序离开他们所在的函数后,这些变量并不会消失,计算机在多次调用之间也会记录它们的值。
另外,对于块作用域的变量而言,非静态变量每次它的函数被调用时都会初始化该变量,但是静态变量在编译它的函数时只初始化一次,==如果未显式初始化静态变量,它们会被初始化为0==。
7.外部链接的静态变量
外部链接的静态变量具有文件域、外部链接和静态存储期。该类别有时称为外部存储类别(external storage class
),属于该类别的变量称为外部变量。把变量的定义性声明放在所有函数外面便创建了外部变量。
当然,为了指出该函数使用了外部变量,可以在函数中用关键字
extern
再次声明。如果一个源代码文件使用的外部变量定义在另一个源代码文件中,则必须使用extern
在该文件中声明该变量。
int Errupt; /* 外部定义的变量 */
extern char Coal; /* 如果Coal被定义在另一个文件中, 则必须这么声明 */
- 初始化外部变量:和自动变量类似也可以被显式初始化,但是如果未初始化外部变量,它们会被自动初始化为0。
- 定义和声明:
int tern = 1; /* tern被定义 */
main()
{
extern int tern; /* 使用在别处定义的tern */
...
}
8.内部链接的静态变量
在所有函数外部用存储类别说明符static
定义的变量具有这种存储类别:
static int svil = 1; // 静态变量,内部链接
int main(void)
{
...
}
内部链接的静态变量只能用于同一个文件中的函数,可以使用存储类别说明符extern
,在函数中反复声明任何具有文件作用域的变量,这样的声明并不会改变其链接属性:
int traveler = 1; // 外部链接
static int stayhome = 1; // 内部链接
int main()
{
extern int traveler; // 使用定义在别处的traceler
extern int stayhome; // 使用定义在别处的 stayhome
}
对于该程序所在的翻译单元,traveler
和stayhome
都具有作用域,但是只有traveler
可用于其他翻译单元(因为它具有外部链接)。这两个声明都使用了extern
关键字,指明了main()
中使用的两个变量都定义在别处,但是这并未改变stayhome
的内部链接属性。
9.多文件
只有当程序由多个翻译单元组成时,才能体现内部链接和外部链接的重要性。
复杂的C程序通常由多个单独的源代码文件组成,有时这些文件可能要共享一个外部变量,C通过在一个文件中进行“定义式声明”,然后再其他文件中进行“引用式声明”来实现共享。
除了一个定义式声明外,其他声明都需要使用
extern
关键字,而且只有定义式声明才能初始化变量。
10.存储类别说明符
C语言有6个关键字作为存储类别说明符:auto
、register
、static
、extern
、_Thread_local
和typedef
。
-
auto
表示变量是自动生存期,只能用于块作用域的变量声明中,在块中声明的变量本身就具有自动存储期,使用auto
主要是为了明确表达要使用与外部变量同名的局部变量的意图 -
register
说明符也只用于块作用域的变量,把变量归为寄存器存储类别,请求最快速度访问该变量,同时保护了该变量的地址不被获取 -
static
说明符创建的对象具有静态存储期(载入程序时创建对象,程序结束时对象消失),如果static
用于文件作用域声明,表明该变量受限于该文件。如果static
用于块作用域声明,表明该变量作用域受限于该块。因此,只要程序在运行对象就存在并保留其值(静态的含义),但是只有在执行块内的代码时,才能通过标识符访问。块作用域的静态变量无链接,文件作用域的静态变量具有内部链接。 -
extern
说明符表明声明的变量定义在别处。如果包含extern
的声明具有文件作用域,则引用的变量必须具有外部链接。如果包含extern
的声明具有块作用域,则引用的变量可能具有外部链接或者内部链接。
11.存储类别和函数
函数也有存储类别:可以使外部函数、静态函数或内联函数。
double gamma(double); // 该函数默认为外部函数
static double beta(int, int);
extern double delta(double, int);
static
存储类别说明符表明创建的函数属于特定模块私有,其他文件中的函数不能调用beta()
。这样做可以避免名字冲突的问题,由于beta()
受限于它所在的文件,所以在其他文件中可以使用与之同名的函数。
通常的做法是:用extern
关键字声明定义在其他文件中的函数,这样做是为了表明当前文件中使用的函数被定义在别处,==除非使用static
关键字,否则一般函数声明都默认为extern
==。
12.存储类别的选择
初学者会认为外部存储类别不错,把所有变量都设置为外部变量就无须使用参数和指针在函数之间传递信息了。然而这可能隐藏一个陷阱:A()
函数可能私下修改B()
函数使用的变量,违背使用者的意图。
const
数据可以保证在初始化之后就不会被修改,所以不用担心它们被意外篡改。
随机数函数和静态变量
随机数函数开始于一个
seed
,然后该函数使用种子生成新的数,这个新数又称为新的种子用于生成更新的种子。
该方案成功的最重要因素在于必须记录它上一次被调用时所使用的种子,这里需要一个静态变量。
/* s_and_r.c --包含 rand1() 和 srand1() 的文件 */
static unsigned long int next = 1; /* 种子 */
int rand1(void)
{
/*生成随机数的魔术公式*/
next = next * 1103515245 + 12345;
return (unsigned int) (next / 65536) % 32768;
}
void srand1(unsigned int seed)
{
next = seed;
}
分配内存:malloc()
和free()
在确定使用哪种存储类别后,根据已制定好的内存管理规则,编译器会自动选择其作用域和存储期。但我们也可以通过库函数来分配和管理内存。
1.回顾
例如以下声明:
// 为float类型和字符串预留足够的内存
float x;
char place[] = "Dancing Oeen Creek";
// 显式指定分配一定的内存
// 该声明预留了100个内存位置,每个位置存储int类型
// 并且为内存提供了一个标识符,可以通过`x`或者`place`识别数据
int plates[100];
2.malloc
分配内存
double * ptd;
ptd = (double *) malloc(30 * sizeof(double));
-
malloc
返回指针,通常该返回值会被强制转化为匹配的类型,但是最好还是加上强制类型转换(double *)
提高代码可读性 -
malloc
分配内存失败时会返回空指针
现在我们有三种创建数组的方法:
- 声明数组时,用常量表达式表示数组的维度,用数组名访问数组的元素。可以使用静态内存或者动态内存自动创建这种数组。
- 声明变长数组,用变量表达式表示数组的维度,用数组名访问数组的元素。具有这种特性的数组==只能在自动内存中创建==。
- 声明一个指针,调用
malloc()
,将其返回值赋给指针,使用指针访问数组的元素。该指针可以是静态的或者自动的。
使用第二种或者第三种方法可以创建动态数组,这种数组和普通数组不同,可以在程序运行时选择数组的大小和分配内存。
3.free()
的重要性
静态内存的数量在编译时是固定的,在程序运行期间也不会改变。自动变量使用的内存数量在程序执行期间自动增加或者减少。但是动态分配的内存数量只会增加,除非使用free()
进行释放。
内存泄漏:调用
malloc
分配内存但是并没有及时使用free()
释放,如果分配的内存过多程序会耗尽所有的内存。
4.calloc()
函数
分配内存也可以使用calloc()
函数:
long * newmem;
newmem = (long *)calloc(100, sizeof(long));
5.动态内存分配和变长数组
变长数组(VLA
)和调用malloc()
在功能上有一些重合,例如两者都可用于创建运行时确定大小的数组:
int vlamal()
{
int n;
int * pi;
scanf("%d", &n);
pi = (int *) malloc (n * sizeof(int));
int ar[n]; //变长数组
pi[2] = ar[2] = -5;
}
不同点:
- 变长数组是自动存储类型,程序在离开变长数组定义的块时(
vlamal()
函数结束时),变长数组占用的内存空间会被自动释放,不必使用free()
- 用
malloc()
创建的数组不必局限在一个函数内访问,比如被调函数创建一个数组并返回指针供主调函数访问,然后主调函数在末尾调用free()
释放之前被调函数分配的内存。另外,free()
所用的指针变量可以与malloc()
的指针变量不同,但是两个指针必须储存相同的地址,==不同释放同一块内存两次==。
6.多维变长数组
int n = 5;
int m = 6;
int ar2[n][m]; // nxm的变长数组(VLA)
int (* p2)[6]; // C99之前的写法,表示指向一个内含6个int类型值的数组,因此p2[i]代表一个由6个整数构成的元素
int (* p3)[m]; // 要求支持变长数组
p2 = (int (*)[6]) malloc(n * 6 * sizeof(int)); // nx6数组
p3 = (int (*)[m]) malloc(n * m * sizeof(int)); // nxm数组(要求支持变长数组)
7.存储类别和动态内存分配
理想化的情况下,程序可以把它可用的内存分成三部分:一部分供具有外部链接、内部链接和无链接的静态变量使用;一部分供自动变量使用;一部分供动态内存分配。
- 静态变量:所占用的内存数量在编译时确定,只要程序还在运行,就可以访问存储在该部分的数据,该类别的变量在程序开始执行时被创建,在程序结束时被销毁。
- 自动存储:在程序进入变量定义所在块时存在,在程序离开块时消失。因此,随着程序调用函数和函数结束,自动变量所用的内存数量也相应地增加和减少,这部分的内存通常作为栈来处理,这意味着新创建的变量按照顺序加入内存,然后以相反的顺序销毁。
- 动态分配的内存在调用
malloc()
或相关的函数时存在,在调用free()
后释放,这部分的内存由程序员管理。内存块可以在一个函数中创建,在另一个函数中销毁。==这部分ed内存用于动态内存分配会支离破碎,未使用的内存块分散在已使用的内存块之间,而且使用动态内存通常比使用栈内存慢。==
ANSIC 类型限定符
1.const
类型限定符
const
关键字声明的对象可以初始化,但是不能修改它的值。
在指针和形参声明中使用const
:
const float * pf; // pf指向一个float类型的const值,不能修改float的值
float * const pt; // pt是一个const指针,不能修改pt
const float * const pt; // pt本身的值不能修改,它所指向的值也不能修改
对全局变量使用const
:
使用全局变量是一种冒险的方法,因为这样做暴露了数据,程序的任何部分都可以修改数据,但是使用
const
限定符声明全局数据就比较合理。可以创建const
变量、const
数组和const
结构。
在不同的文件间共享const
数据需要小心,可以采用两个策略:
- 遵循外部变量的常用规则,即在一个文件中使用定义式声明,在其他文件中使用引用式声明(用
extern
关键字):
/* file1.c --定义一些外部cosnt变量 */
const double PI = 3.14159
const char * MONTHS[12] = {"January", "February", "March", "April", "May",
"June", "July", "Augest", "September", "Octover", "November", "December"
};
/* file2.c 00使用定义在别处的const变量 */
# include "constant.h"
- 把
const
变量放在一个头文件中,然后在其他文件中包含该头文件:这种方法必须在头文件用关键字static
声明全局const
变量,如果去掉static
,那么在file1.c
和其他文件包含constant.h
会导致每个文件中都有一个相同标识符的定义式声明。这种做法相当于给每个文件提供了一个单独的数据副本,由于每个副本只对该文件可见,所以无法用这些数据和其他文件通信,不过由于他们都是const
数据完全相同,所以也没有大问题。
/* constant.h --定义了一些外部const变量 */
static const double PI = 3.14159;
static const char * MONTHS[12] = {"January", "February", "March", "April", "May",
"June", "July", "Augest", "September", "Octover", "November", "December"
};
/* file1.c --使用定义在别处的外部const变量*/
# include "constant.h"
2.volatile
类型限定符
volatile
限定符告知计算机,代理(而不是变量所在的程序)可以改变该变量的值,通常它被用于硬件地址以及在其他程序或同时运行的线程中共享数据。
假设有以下代码:
val1 = x;
/* 一些不使用x的代码 */
val2 = x;
智能的编译器会注意到以上代码使用了两次x
,但是并没有改变它的值,于是编译器把x
的值临时存储在寄存器中,然后在val2
需要使用x
时才从寄存器(而不是原始内存位置上)读取x
的值以节约时间。这个过程被称为高速缓存(caching
)。但是如果其他代码在以上两条语句之间改变了x
的值,就不能这样优化了,为安全起见编译器不会进行高速缓存。
3.restrict
类型限定符
restrict
关键字允许编译器优化某部分代码以更好地支持计算。它只能用于指针,表明该指针是访问数据对象的唯一且初始的方式。
int ar[10];
int * restrict restar = (int *) malloc(10 * sizeof(int))
int * par = ar
这里,指针restar
是访问由malloc()
所分配内存的唯一且初始的方式,因此可以用restrict
关键字限定它。而指针par
既不是访问ar
数据中数据的初始方式,也不是唯一方式。所以不用把它设置为restrict
。
举个编译器优化的例子:
for (n = 0; n < 10; n++)
{
par[n] += 5;
restar[n] += 5;
ar[n] *= 2;
par[n] += 3;
restar[n] += 3;
}
由于之前声明了restar
是访问它所指向数据块的唯一且初始化的方式,编译器可以把设计restar
的两条语句替换成下面这条语句,效果相同:
restar[n] += 8;
但是对于par
就不能做这种操作,因为未使用restrict
关键字时编译器就必须假设最坏的情况(即在两次使用指针之间,其他的标识符可能已经改变了数据)。如果使用了restrict
关键字,编译器就可以选择捷径优化计算。
restrict
限定符还可用于函数形参中的指针,这意味着编译器可以假定在函数体内其他标识符不会修改该指针指向的数据,而且编译器可以尝试对其优化,使其不做别的用途。
4._Atomic
类型限定符(C11)
_Atomic int hogs; // 原子类型的变量
atomic_store(&hogs, 12); // 在hogs中存储12是一个原子过程
long strtol(const char * restrict nptr, char ** restrict endptr, int base);