文章目录
- 第24章 错误处理
- 24.1 <assert.h>: 诊断
- 24.2 <errno.h>: 错误
- 24.2.1 perror函数和strerror函数
- 24.3 <signal.h>: 信号处理
- 24.3.1 信号宏
- 24.3.2 signal函数
- 24.3.3 预定义的信号处理函数
- 24.3.4 raise函数
- 24.4 <setjmp.h>: 非局部跳转
- 问与答
- 写在最后
第24章 错误处理
——编写无错程序的方法有两种,但只有第三种写程序的方法才行得通。
学习C
语言的学生所编写的程序在遇到异常输入时经常无法正常运行,但真正商业用途的程序却必须“非常稳健”,即能够从错误中恢复正常而不至于崩溃。为了使程序非常稳健,我们需要能够预见程序执行时可能遇到的错误,包括对每个错误进行检测,并提供错误发生时的合适行为。
本章讲述两种在程序中检测错误的方法:调用
assert
宏以及测试errno
变量。24.1节
介绍了<assert.h>
头,assert
宏就是在这里定义的。24.2节
讨论了<errno.h>
头,其中定义了errno
变量。这一节还包含perror
函数与strerror
函数,这两个函数分别来自<stdio.h>
和<string.h>
,它们与errno
变量紧密相关。
24.3节
讲解如何检测并处理称为信号的条件,一些信号用于表示错误。处理信号的函数在<signal.h>
头中声明。
最后,
24.4节
探讨setjmp/longjmp
机制,它们经常用于响应错误。setjmp
和longjmp
都属于<setjmp.h>
头。
错误的检测和处理并不是C
语言的强项。C
语言对运行时错误以多种形式表示,而没有提供一种统一的方式。而且,在C
程序中,需要由程序员编写检测错误的代码。因此,很容易忽略一些可能发生的错误。一旦发生某个被忽略的错误,程序经常可以继续运行,虽然这样也不是很好。C++
、Java
和C#
等较新的语言具有“异常处理”特性,可以更容易地检测和响应错误。
24.1 <assert.h>: 诊断
void assert(scalar expression);
assert
定义在<assert.h>
中。它使程序可以监控自己的行为,并尽早发现可能会发生的错误。
虽然assert
实际上是一个宏,但它是按照函数的使用方式设计的。assert
有一个参数,这个参数必须是一种“断言”——一个我们认为在正常情况下一定为真的表达式。每次执行assert
时,它都会检查其参数的值。如果参数的值不为0
,assert
什么也不做;如果参数的值为0
,assert
会向stderr(标准误差流,22.1节)
写一条消息,并调用abort函数(26.2节)
终止程序执行。
例如,假定文件
demo.c
声明了一个长度为10
的数组a
,我们关心的是demo.c
程序中的语句
a[i] = 0;
可能会由于i
不在0~9
之间而导致程序失败。可以在给a[i]
赋值前使用assert
宏检查这种情况:
assert(0 <= i && i < 10) ; /* checks subscript first */
a[i] = 0; /* now does the assignment */
如果i
的值小于0
或者大于等于10
,程序在显出类似下面的消息后会终止:
Assertion failed: 0 <= i && i < 10, file demo.c, line 109
C99
对assert
做了两处小修改。C89
标准指出,assert
的参数必须是int
类型的。C99
放宽了要求,允许参数为任意标量类型(因此在assert
的原型中出现了单词scalar
)。例如,现在参数可以为浮点数或指针。此外,C99
要求失败的assert
显示其所在的函数名。(C89
只要求assert
以文本格式显示参数、源文件及源文件中的行号。)C99
建议的消息格式为
Assertion failed: expression, function abc, file xyz, line nnn.
根据编译器的不同,
assert
生成的消息格式也不尽相同,但它们都应包含标准要求的信息。例如,GCC
在上述情况下给出如下的消息:
a.out: demo.c:109: main: Assertion '0 <= i && i < 10' failed.
assert
有一个缺点:因为它引入了额外的检查,所以会增加程序的运行时间。偶尔使用一次assert
可能对程序的运行速度没有很大影响,但在实时程序中,这么小的运行时间增加可能也是无法接受的。因此,许多程序员在测试过程中会使用assert
,但当程序最终完成时就会禁止assert
。要禁止assert
很容易,只需要在包含<assert.h>
之前定义宏NDEBUG
即可:
#define NDEBUG
#include <assert.h>
NDEBUG
宏的值不重要,只要定义了NDEBUG
宏即可。一旦之后程序又有错误发生,就可以去掉NDEBUG
宏的定义来重新启用assert
。
请注意!!不要在
assert
中使用有副作用的表达式(包括函数调用)。万一某天禁止了assert
,这些表达式将不会再被求值。考虑下面的例子:assert((p = malloc(n)) != NULL);
一旦定义了
NDEBUG
,assert
就会被忽略并且malloc
不会被调用。
函数assert
是在程序运行期间做诊断工作,从C11
开始引入的静态断言_Static_assert
可以把检查和诊断工作放在程序编译期间进行(18.7节
)。
24.2 <errno.h>: 错误
标准库中的一些函数通过向
<errno.h>
中声明的int
类型errno
变量存储一个错误码(正整数)来表示有错误发生。[errno
可能实际上是个宏。如果确实是宏,C
标准要求它表示左值(4.2节)
,以便像变量一样使用。]大部分使用errno
变量的函数集中在<math.h>
,但也有一些在标准库的其他部分中。
假设我们需要使用一个库函数,该库函数通过给errno
赋值来产生程序运行出错的信号。在调用这个函数之后,我们可以检查errno
的值是否为零。如果不为零,则表示在函数调用过程中有错误发生。举例来说,假如需要检查sqrt函数(23.3节)
的调用是否出错,可以使用类似下面的代码:
errno = 0;
y = sqrt(x);
if (errno != 0 ) { fprintf(stderr, "sqrt error; program terminated.\n"); exit(EXIT_FAILURE);
}
当使用errno
来检测库函数调用中的错误时,在函数调用前将errno
置零非常重要。虽然在程序刚开始运行时errno
的值为零,但有可能在随后的函数调用中已经被改动了。库函数不会将errno
清零,这是程序需要做的事情。
当错误发生时,向
errno
中存储的值通常是EDOM
或ERANGE
。(这两个宏都定义在<errno.h>
中。)这两个值代表调用数学函数时可能发生的两种错误:
定义域错误(EDOM)
:传递给函数的一个参数超出了函数的定义域。例如,用负数作为sqrt
的参数就会导致定义域错误。取值范围错误(ERANGE)
:函数的返回值太大,无法用返回类型表示。例如,用1000
作为exp函数(23.3节)
的参数就经常会导致取值范围错误,因为 e 1000 {e^{1000}} e1000太大导致无法在大多数计算机上用double
类型表示。
一些函数可能会同时导致这两种错误。可以用errno
分别与EDOM
和ERANGE
比较,然后确定究竟发生了哪种错误。
C99
在<errno.h>
中增加了EILSEQ
宏。特定头(尤其是<wchar.h>头,25.5节
)中的库函数在发生编码错误(22.3节)
时把EILSEQ
的值存储到errno
中。
24.2.1 perror函数和strerror函数
void perror(const char *s); //来自<stdio.h>
char *strerror(int errnum); //来自<string.h>
下面看两个与变量errno
有关的函数,不过这两个函数都不属于<errno.h>
。
当库函数向
errno
存储了一个非零值时,可能会希望显示一条描述这种错误的消息。一种实现方式是调用perror
函数(在<stdio.h>
中声明),它会按顺序显示以下信息:(1)
调用perror
的参数;(2)
一个冒号;(3)
一个空格;(4)
一条出错消息,消息的内容根据errno
的值决定;(5)
一个换行符。perror
函数会输出到stderr流(22.1节)
而不是标准输出。下面是一个使用perror
的例子:
errno = 0;
y = sqrt(x);
if (errno != 0) { perror("sqrt error"); exit(EXIT_FAILURE);
}
如果sqrt
调用因定义域错误而失败,perror
会产生如下输出:
sqrt error:Numerical argument out of domain
perror
函数在sqrt error
后所显示的出错消息是由实现定义的。在这个例子中,Numerical argument out of domain
是与EDOM
错误相对应的消息。ERANGE
错误通常会对应于不同的消息,例如Numerical result out of range
。
strerror
函数属于<string.h>
。当以错误码为参数调用strerror
时,函数会返回一个指针,它指向一个描述这个错误的字符串。例如,调用
puts(strerror(EDOM));
可能会显示
Numerical argument out of domain
strerror
函数的参数通常是errno
的值,但以任意整数作为参数时strerror
都能返回一个字符串。
strerror
与perror
函数密切相关。如果strerror
的参数为errno
,那么perror
所显示的出错消息与strerror
所返回的消息是相同的。
24.3 <signal.h>: 信号处理
<signal.h>
提供了处理异常情况(称为信号)的工具。信号有两种类型:运行时错误(例如除以0)
和发生在程序以外的事件
。例如,许多操作系统都允许用户中断或终止正在运行的程序,C
语言把这些事件视为信号。当有错误或外部事件发生时,我们称产生了一个信号。大多数信号是异步的:它们可以在程序执行过程中的任意时刻发生,而不仅是在程序员所知道的特定时刻发生。由于信号可能会在任何意想不到的时刻发生,因此必须用一种独特的方式来处理它们。
本节按C
标准中的描述来介绍信号。这里对信号谈得很有限,但实际上信号在UNIX
中的作用很大。这里不作详细讨论。
24.3.1 信号宏
<signal.h>
定义了一系列的宏,用于表示不同的信号。表24-1
中列出了这些宏以及它们的含义。每个宏的值都是一个正整型常量
。C
语言的实现可以提供更多的信号宏,只要宏的名字以SIG
和一个大写字母开头就行。(特别地,UNIX
实现提供许多额外的信号宏。)
表24-1 信号
宏名 | 含义 |
---|---|
SIGABRT | 异常终止(可能由于调用abort 导致) |
SIGFPE | 在算术运算中发生错误(可能是除以0 或溢出) |
SIGILL | 无效指令 |
SIGINT | 中断 |
SIGSEGV | 无效存储访问 |
SIGTERM | 终止请求 |
C
标准并不要求表24-1
中列出的信号都自动产生,因为对于某个特定的计算机或操作系统,不是所有的信号都有意义。大多数C
语言的实现都至少支持其中的一部分。
24.3.2 signal函数
void (*signal(int sig, void (*func)(int)))(int);
<signal.h>
提供了两个函数:raise
和signal
。这里先讨论signal
函数,它会安装一个信号处理函数,以便将来给定的信号发生时使用。signal
函数的使用比它的原型看起来要简单得多。它的第一个参数是特定信号的编码,第二个参数是一个指向会在信号发信生时处理这一号的函数的指针。例如,下面的signal
函数调用为SIGINT
信号安装了一个处理函数:
signal(SIGINT, handler);
handler
就是信号处理函数的函数名。一旦随后在程序执行过程中出现了SIGINT
信号,handler
函数就会自动被调用。
每个信号处理函数都必须有一个int
类型的参数,且返回类型为void
。当一个特定的信号产生并调用相应的处理函数时,信号的编码会作为参数传递给处理函数。知道是哪种信号导致了处理函数被调用是十分有用的,尤其是它允许我们对多个信号使用同一处理函数。
信号处理函数可以做许多事。这可能包含忽略该信号、执行一些错误恢复或终止程序。然而,除非信号是由调用
abort函数(26.2节)
或raise
函数引发的,否则信号处理函数不应该调用库函数或试图使用具有静态存储期(18.2节)
的变量。(但这些规则也有例外。)
一旦信号处理函数返回,程序就会从信号发生点恢复并继续执行,但有2
种例外情况:
- 如果信号是
SIGABRT
,当处理函数返回时程序会(异常地)终止; - 如果处理的信号是
SIGFPE
,那么处理函数返回的结果是未定义的。(也就是说,不要处理它。)
虽然
signal
函数有返回值,但经常被丢弃。返回值是指向指定信号的前一个处理函数的指针。如果需要,可以将它保存在变量中。特别是,如果打算恢复原来的处理函数,那么就需要保留signal
函数的返回值:
void (*orig_handler)(int); /* function pointer variable */
...
orig_handler = signal(SIGINT, handler);
这条语句将handler
函数安装为SIGINT
的处理函数,并将指向原来的处理函数的指针保存在变量orig_handler
中。如果要恢复原来的处理函数,我们需要使用下面的代码:
signal(SIGINT, orig_handler); /* restores original handler */
24.3.3 预定义的信号处理函数
除了编写自己的信号处理函数,还可以选择使用
<signal.h>
提供的预定义的处理函数。有两个这样的函数,每个都是用宏表示的。
-
SIG_DFL
。SIG_DFL
按“默认”方式处理信号。可以使用下面的调用安装SIG_DFL
:signal(SIGINT, SIG_DFL); /* use default handler */
调用
SIG_DFL
的结果是由实现定义的,但大多数情况下会导致程序终止。 -
SIG_IGN
。调用signal(SIGINT, SIG_IGN); /* ignore SIGINT signal */
指明随后当信号
SIGINT
产生时,忽略该信号。
除了SIG_DFL
和SIG_IGN
,<signal.h>
可能还会提供其他的信号处理函数,其函数名必须是以SIG_
和一个大写字母
开头。当程序刚开始执行时,根据不同的实现,每个信号的处理函数都会被初始化为SIG_DFL
或SIG_IGN
。
<signal.h>
还定义了另一个宏SIG_ERR
,它看起来像是个信号处理函数。实际上,SIG_ERR
是用来在安装处理函数时检测是否发生错误的。如果一个signal
调用失败(即不能对所指定的信号安装处理函数),就会返回SIG_ERR
并在errno
中存入一个正值。因此,为了测试signal
调用是否失败,可以使用如下代码:
if (signal(SIGINT, handler) == SIG_ERR) { perror("signal(SIGINT, handler) failed"); ...
}
在整个信号处理机制中,有一个棘手的问题:如果信号是由处理这个信号的函数引发的,那会怎样呢
?为了避免无限递归,C89
标准为程序员安装的信号处理函数引发信号的情况规定了一个两步的过程。首先,要么把该信号对应的处理函数重置为SIG_DFL(默认处理函数)
,要么在处理函数执行的时候阻塞该信号。(SIGILL
是一个特殊情况,当SIGILL
发生时这两种行为都不需要。)然后,再调用程序员提供的处理函数。
请注意!!信号处理完之后,处理函数是否需要重新安装是由实现定义的。
UNIX
实现通常会在使用处理函数之后保持其安装状态,但其他实现可能会把处理函数重置为SIG_DFL
。在后一种情况下,处理函数可以通过在其返回前调用signal
函数来实现自身的重新安装。
C99
对信号处理过程做了一些小的改动。当信号发生时,实现不仅可以禁用该信号,还可以禁用别的信号。对于处理SIGILL
或SIGSEGV
信号(以及SIGFPE
信号)的信号处理函数,函数返回的结果是未定义的。C99
还增加了一条限制:如果信号是因为调用abort
函数或raise
函数而产生的,信号处理函数本身一定不能调用raise
函数。(我的理解是,raise
不能连续发生)
24.3.4 raise函数
int raise(int sig);
通常信号是由于运行时错误或外部事件而产生的,但有时候如果程序可以触发信号会非常方便。raise
函数就可以实现这一目的。raise
函数的参数指定所需信号的编码:
raise(SIGABRT); /* raises the SIGABRT signal */
raise
函数的返回值可以用来测试调用是否成功:0
代表成功,非0
则代表失败。
下面的程序说明了如何使用信号。首先,给
SIGINT
信号安装了一个惯用的处理函数(并小心地保存了原先的处理函数),然后调用raise_sig
产生该信号;接下来,程序将SIG_IGN
设置为SIGINT
的处理函数并再次调用raise_sig
;最后,它重新安装信号SIGINT
原先的处理函数,并最后调用一次raise_sig
。
/*
tsignal.c
--Tests signals
*/
#include <signal.h>
#include <stdio.h>
void handler(int sig);
void raise_sig(void);
int main(void)
{ void (*orig_handler)(int); printf("Installing handler for signal %d\n", SIGINT); orig_handler = signal(SIGINT, handler); raise_sig(); printf("Changing handler to SIG_IGN\n"); signal(SIGINT, SIG_IGN); raise_sig(); printf("Restoring original handler\n"); signal(SIGINT, orig_handler); raise_sig(); printf("Program terminates normally\n"); return 0;
}
void handler(int sig)
{ printf("Handler called for signal %d\n", sig);
}
void raise_sig(void)
{ raise(SIGINT);
}
当然,调用raise
并不需要在单独的函数中。这里定义raise_sig
函数只是为了说明一点:无论信号是从哪里产生的(无论是在main
函数中还是在其他函数中),它都会被最近安装的该信号的处理函数捕获。
这段程序的输出可能会有多种。下面是一种可能的输出形式:
Installing handler for signal 2
Handler called for signal 2
Changing handler to SIG_IGN
Restoring original handler
这个输出结果表明,我们的实现把SIGINT
的值定义为2
,而且SIGINT
原先的处理函数一定是SIG_DFL
。(如果是SIG_IGN
,应该会看到信息Program terminates normally
。)最后,我们注意到SIG_DFL
会导致程序终止,但不会显示出错消息。
24.4 <setjmp.h>: 非局部跳转
int setjmp(jmp_buf env);
_Noreturn void longjmp(jmp_buf env, int val);
通常情况下,函数会返回到它被调用的位置。我们无法使用
goto语句(6.4节)
使它转到其他地方,因为goto
只能跳转到同一函数内的某个标号处。但是<setjmp.h>
可以使一个函数直接跳转到另一个函数,不需要返回。
在<setjmp.h>
中最重要的内容就是setjmp
宏和longjmp
函数。setjmp
宏“标记”程序中的一个位置,随后可以使用longjmp
跳转到该位置。虽然这一强大的机制可以有多种潜在的用途,但它主要被用于错误处理。
如果要为将来的跳转标记一个位置,可以调用
setjmp
宏,调用的参数是一个jmp_buf
类型(在<setjmp.h>
中声明)的变量。setjmp
宏会将当前“环境”(包括一个指向setjmp
宏自身位置的指针)保存到该变量中,以便将来可以在调用longjmp
函数时使用,然后返回0
。
要返回setjmp
宏所标记的位置可以调用longjmp
函数,调用的参数是调用setjmp
宏时使用的同一个jmp_buf
类型的变量。longjmp
函数会首先根据jmp_buf
变量的内容恢复当前环境,然后从setjmp
宏调用中返回——这是最难以理解的。这次setjmp
宏的返回值是val
,就是调用longjmp
函数时的第二个参数。(如果val
的值为0
,那么setjmp
宏会返回1
。)
请注意!!一定要确保作为
longjmp
函数的参数之前已经被setjmp
调用初始化了。还有一点很重要:包含setjmp
最初调用的函数一定不能在调用longjmp
之前返回。如果两个条件都不满足,调用longjmp
会导致未定义的行为。(程序很可能会崩溃。)
总而言之,setjmp
会在第一次调用时返回0
;随后,longjmp
将控制权重新转给最初的setjmp
宏调用,而setjmp
在这次调用时会返回一个非零值。明白了吗?我们可能需要一个例子。
下面的程序使用
setjmp
宏在main
函数中标记一个位置,然后函数f2
通过调用longjmp
函数返回到这个位置。
/*
tsetjmp.c
--Tests setjmp/longjmp
*/
#include <setjmp.h>
#include <stdio.h>
jmp_buf env;
void f1(void);
void f2(void);
int main(void)
{ if (setjmp(env) == 0) printf("setjmp returned 0\n"); else { printf("Program terminates: longjmp called\n"); return 0; } f1(); printf("Program terminates normally\n"); return 0;
} void f1(void)
{ printf("f1 begins\n"); f2(); printf("f1 returns\n");
}void f2(void)
{ printf("f2 begins\n"); longjmp(env, 1); printf("f2 returns\n");
}
这段程序的输出如下:
setjmp returned 0
f1 begins
f2 begins
Program terminates: longjmp called
setjmp
宏的最初调用返回0
,因此main
函数会调用f1
。接着,f1
调用f2
,f2
使用longjmp
函数将控制权重新转给main
函数,而不是返回到f1
。当longjmp
函数被执行时,控制权重新回到setjmp
宏调用。这一次setjmp
宏返回1
(就是在longjmp
函数调用时所指定的值)。
问与答
问1:书上说,在调用可能修改
errno
的库函数之前把errno
设置为0
是很重要的。但是,我见过一些UNIX
程序在没有把errno
设置为0
的情况下就对其进行测试。这是什么缘故呢?
答:UNIX
程序通常包含对操作系统函数的调用。这些系统调用需要用到errno
,但使用方法与本节提到的方法略有不同。当这样的调用失败时,除了在errno
中存储一个值之外,还会返回一个特殊的值(例如-1
或空指针
)。程序不需要在这些调用之前往errno
中存储0
,因为函数的返回值本身就可以表明发生了错误。C
标准库中的一些函数也是这样的:errno
更多地用于指明错误类型而不是用于发出出错信号。
问2:我使用的
<errno.h>
版本中除了EDOM
和ERANGE
以外,还定义了其他的宏。这是合法的吗?
答:是合法的。C
标准允许使用宏表示其他错误条件,只要宏的名字以字母E
开头并且其后有一个数字或大写字母。UNIX
实现中通常会定义许多这样的宏。
问3:一些表示信号的宏的名字含义比较模糊,比如
SIGFPE
和SIGSEGV
。这些名字是如何得来的呢?
答:信号的名字可以追溯到早期的C
编译器,这些编译器运行在DECPDP-11
计算机上。PDP-11
的硬件可以检测一些错误,诸如“Floating Point Exception”
和“Segmentation Violation
”。
问4:我很好奇。书上说除非信号是由
abort
函数或raise
函数引发的,否则信号处理函数不应该调用库函数。但你又说有例外情况,是什么例外呢?
答:信号处理函数可以调用singal
函数,只要第一个参数是当前正在处理的信号就可以。这一限定条件很重要,因为它允许信号处理函数自身进行重新安装。在C99
中,信号处理函数还可以调用abort
函数或_Exit函数(26.2节)
。
问5:接着上一个问题,信号处理函数通常不能访问具有静态存储期的变量。这个规则的例外是什么?
答:这个问题要难回答一些。答案涉及<signal.h>
头中声明的一个名为sig_atomic_t
的类型。根据C
标准,sig_atomic_t
是一个可以作为一个“原子实体”访问的整型。换句话说,CPU
可以用一条指令从内存中取出sig_atomic_t
的值或将其存放到内存中,而不需要用两条或更多条指令。通常把sig_atomic_t
定义为int
,因为大多数CPU
可以用一条指令存取int
类型的值。
下面谈谈信号处理函数不可以访问静态变量这一规则的例外情况。C
标准允许信号处理函数在sig_atomic_t
类型的变量中存储值(即使该变量具有静态存储期也可以),前提是该变量声明为volatile
。为了了解这一不可思议的规则产生的原因,考虑信号处理函数要修改一个类型比sig_atomic_t
宽一些的静态变量的情况。如果程序在信号发生之前从内存中取出了该变量的一部分,并在信号处理完毕后取完该变量,那么这个值就没有价值了。sig_atomic_t
类型的变量可以一步取出,所以不会出现这种问题。把变量声明为volatile
会警告编译器,变量的值随时可能改变。(信号可能突然产生,并调用信号处理函数来修改该变量。)
问6:程序
tsignal.c
在信号处理函数内调用了printf
函数。这不是非法的吗?
答:如果信号处理函数是由raise
或abort
调用的,那么就可以调用库函数。tsignal.c
使用raise
来调用信号处理函数。
问7:
setjmp
会如何修改传递给它的参数呢?C
语言不是始终以值的形式传递参数吗?
答:C
标准要求jmp_buf
必须是一个数组类型,因此传递给setjmp
的实际上是一个指针。
问8:我在使用
setjmp
时遇到一些问题。使用setjmp
有什么限制吗?
答:按照C
标准,只有2
种使用setjm
p的方式是合法的。
-
作为表达式语句中的表达式(可能会强制转换成
void
)。 -
作为
if
、swtich
、while
、do
或for
语句中控制表达式的一部分。整个控制表达式必须符合下面的形式之一,其中constexp
是一个整型常量表达式,而op
是关系或判等运算符。setjmp(...)
!setjmp(...)
constexpr op setjmp(...)
setjmp(...) op constexpr
其他的用法会导致未定义的行为。
问9:调用
longjmp
函数后,程序中变量的值是什么?
答:大部分变量的值保留了longjmp
函数被调用时的值。然而,包含setjmp
宏的函数中,自动变量的值是不确定的,除非该变量被声明为volatile
或者在执行setjmp
时没有被修改过。
问10:在信号处理函数里调用
longjmp
函数合法吗?
答:是合法的,只要该信号处理函数的调用不是由某个信号处理函数执行过程中触发的信号引发的。(C99
删除了这一限制。)
写在最后
本文是博主阅读《C语言程序设计:现代方法(第2版·修订版)》时所作笔记,日后会持续更新后续章节笔记。欢迎各位大佬阅读学习,如有疑问请及时联系指正,希望对各位有所帮助,Thank you very much!