声明:仅为个人学习总结,还请批判性查看,如有不同观点,欢迎交流。
摘要
本文总结了3个主题相关的知识点,包括文件相关操作、部分重要的程序设计思想、C 和 C++ 之间重叠且稍有不同的规则。
1、输入/输出(文件)
1.1 流
文件(file)是存储器中存储信息的已命名的区域。C 语言把所有输入和输出设备都视为文件,并把键盘和显示设备视为每个 C 程序自动打开的文件。
从概念上看,C 程序并不直接处理文件,而是处理“流”(stream)。对不同属性和不同种类的输入/输出文件进行映射,由属性更统一的流来表示。打开文件的过程就是把流与文件相关联,读写都通过流来完成。
流,表示任意输入的源或任意输出的目的地
- 通过输入流获得数据,例如键盘、扫描仪、存储在介质上的文件、网络端口
- 通过输出流输出数据,例如屏幕、打印机、存储在介质上的文件、网络端口
本章讨论的文件主要是普通文件,但在 <stdio.h> 头(输入/输出函数的主要存储位置)中,许多函数可以处理各种形式的流,并不限于表示普通文件的流。
1.1.1 文件指针
C 程序通过文件指针(file pointer)对流进行访问,指针的类型为 FILE *
,指针指向一个 FILE
结构对象,结构中包括文件名称、缓冲区的“位置和被填充程度”等信息。
一些“标准流”的文件指针具有特定的名字,如 stdin、stdout;可以用 FILE *
声明除了标准流之外的流。
FILE *fp1, *fp2;
1.1.2 标准流和重定向
<stdio.h> 提供了3个标准流,它们可以直接使用,不需要进行声明,也不用打开或关闭。
文件指针 | 流 | 默认的含义 | 相关库函数 |
---|---|---|---|
stdin | 标准输入 | 键盘 | scanf、getchar、gets |
stdout | 标准输出 | 屏幕 | printf、putchar、puts |
stderr | 标准错误 | 屏幕 |
默认情况下,stdin 表示键盘,stdout 和 stderr 表示屏幕。许多操作系统允许通过重定向(redirection)机制来改变这些默认的含义。
- 输入重定向(input redirection):通过在命令行中加入
<输入文件的名字
,使stdin
流表示文件而非键盘。
// stdin 流表示 in.dat 文件,通过 in.dat (而不是从键盘)获得输入数据
demo <in.dat
- 输出重定向(output redirection):通过在命令行中加入
>输出文件的名字
,使stdout
流表示文件而非屏幕。
// stdout 流表示 out.dat 文件,所有写入 stdout 的数据都会写入 out.dat 中(而不是出现在屏幕上)
demo >out.dat
// 可以结合使用输入重定向和输出重定向
demo < in.dat > out.dat
// 可以通过重定向丢弃 stdout(1) 和 stderr(2) 的输出
demo > nul 2>&1 // Windows 系统
demo > /dev/null 2>&1 // Linux 系统
1.1.3 文本文件和二进制文件
在文本文件(text file)中,字节用来表示各种编码(ASCII、Unicode等)的字符,可以直接查看或编辑文件;
在二进制文件(binary file)中,字节不仅可以表示字符,还可以表示数值数据、机器语言代码等各种数据。
文本文件具有两种二进制文件没有的特性:
特性 | Windows | UNIX | Mac OS 新版 | Mac OS 旧版 |
---|---|---|---|---|
分为若干行,每行以特殊字符结尾 | 回车符(‘\x0d’) + 换行符(‘\x0a’) | 换行符(‘\x0a’) | 换行符(‘\x0a’) | 回车符(‘\x0d’) |
可能包含“文件末尾”标记 | 末尾标记(‘\x1a’, Ctrl+Z),非必须 | 无 | / | / |
二进制文件不分行,也没有行末标记和文件末尾标记。
1.2 文件操作
1.2.1 打开文件
/**
* @brief 打开文件
* 宏 FOPEN_MAX 指定了可以同时打开的文件的最大数量
* @param[in] filename 要打开的文件名,可能包含关于文件位置的信息,如驱动器符或路径
* 对于 Windows 的路径分隔符,需要用 '\\' 或 '/' 代替 '\'
* 宏 FILENAME_MAX 指定了文件名的最大长度
* @param[in] mode 模式字符串(见下表),用来指定打算对文件执行的操作
* @return 文件指针,在对文件进行操作时使用
* 如果无法打开文件(路径错误、没有权限等原因),返回空指针
*/
FILE *fopen(const char* restrict filename, const char * restrict mode);
传递给 fopen
函数的模式字符串列表:
文本文件模式 | 二进制文件模式 | 含义 |
---|---|---|
“r” | “rb” | 打开文件用于读 |
“w” | “wb” | 打开文件用于写(文件不需要存在) |
“wx” | “wbx” | 创建文件用于写(文件不能已经存在)① |
“a” | “ab” | 打开文件用于追加(文件不需要存在) |
“r+” | “r+b” 或 “rb+” | 打开文件用于读和写,从文件头开始 |
“w+” | “w+b” 或 “wb+” | 打开文件用于读和写(如果文件存在就清空) |
“w+x” | “w+bx” 或 “wb+x” | 创建文件用于读和写(文件不能已经存在)① |
“a+” | “a+b” 或 “ab+” | 打开文件用于读和写(如果文件存在就追加) |
① 从 C11 开始引入的模式(独占的创建-打开模式)
打开文件用于读和写(模式字符串包含字符 +
)时:
- 如果没有先调用一个文件定位函数,那么就不能从读模式转换成写模式,除非读操作遇到了文件的末尾;
- 如果既没有调用
fflush
函数,也没有调用文件定位函数,那么就不能从写模式转换成读模式。
在二进制模式中,程序可以访问文件的每个字节。而在文本模式中,会对本地环境表示的“行末尾或文件结尾”与 C 程序中的模式进行映射,程序所见的内容和文件的实际内容可能不同。
- 对于 Windows 系统文本文件,不同打开模式的区别:
打开模式 | 文件结尾标记字符 | 换行符 |
---|---|---|
文本模式 | 可以识别结尾标记字符 Ctrl+Z | 对 \r\n 和 \n 进行映射 |
二进制模式 | 结尾标记只是普通字符,后面还可能包含其它用于填充的字符,实际的文件结尾在最后 | 保持 \r\n 不变 |
- 对于 UNIX 文本文件,通常,既没有
Ctrl+Z
结尾符,也没有\r
换行符,不需要区分打开模式。
1.2.2 关闭文件
/**
* @brief 关闭不再使用的文件
* @param[in] stream 文件指针,来自 fopen 函数或 freopen 函数的调用
* @return 如果成功关闭文件,返回零;
* 否则,返回错误代码 EOF(在<stdio.h>中定义的宏)
*/
int fclose(FILE *stream);
1.2.3 为打开的流附加文件
/**
* @brief 为已经打开的流 stream 附加一个不同的文件 filename,同时关闭流中的旧文件
* @param[in] filename 同 fopen
* C99:如果是空指针,freopen 会试图把流的模式修改为 mode 参数指定的模式。
* 具体的实现可以不支持这种特性;如果支持,则可以限定能进行哪些模式改变。
* @param[in] mode 同 fopen
* @param[in] stream 文件指针,标识要被附加文件(重新打开)的流
* @return 返回第三个参数(文件指针)
* 如果无法打开新文件,返回空指针。(如果无法关闭旧的文件,freopen 函数会忽略错误)
*/
FILE *freopen(const char * restrict filename,
const char * restrict mode,
FILE * restrict stream);
// 常见用法:把文件和一个标准流(stdin、stdout 或 stderr)相关联
// 首先,关闭先前(通过命令行重定向或者 freopen 函数调用)与标准流相关联的所有文件
// 然后,打开文件 foo,并将其与 stdout 相关联,
// 后续,写入到 stdout 中的内容都会被重定向写入 foo 文件
if (freopen("foo", "w", stdout) == NULL) {
// error; foo can't be opened
}
1.2.4 临时文件
/**
* @brief 创建一个临时文件(用"wb+"模式打开),该临时文件将一直存在,除非关闭它或程序终止
* 但是无法知道创建的文件名是什么
* @return 文件指针;如果创建文件失败,返回空指针
*/
FILE *tmpfile(void);
/**
* @brief 为临时文件产生名字
* 不能过于频繁地调用 tmpnam 函数。
* 宏 TMP_MAX(在<stdio.h>中定义)指明了程序执行期间由 tmpnam 函数产生的临时文件名的最大数量。
* @param[out] s 用于保存临时文件名字的字符数组,函数返回指向数组第一个字符的指针
* 如果实参是空指针,函数会把文件名存储到一个静态变量中,并且返回指向此变量的指针
* @return 临时文件名字指针;如果生成文件名失败,返回空指针
*/
char *tmpnam(char *s);
// L_tmpnam 是<stdio.h>中的一个宏,指明了保存临时文件名的字符数组的长度
char filename[L_tmpnam];
tmpnam(filename);
1.2.5 文件缓冲
文件缓冲(buffering):
- 对于输出流,把写入流的数据存储在内存的缓冲区域内;当缓冲区满了(或者关闭流)时,对缓冲区进行“清洗”(flush,写入实际的输出设备)。
- 对于输入流,缓冲区包含来自输入设备的数据;从缓冲区读数据而不是从设备本身读数据。
/**
* @brief 主动清洗文件的输出缓冲区
* @param[in] stream 目标文件指针,为“和 stream 相关联的文件”清洗缓冲区
* 如果实参是空指针,则清洗全部输出流
* @return 如果成功,返回零值。如果发生错误,则返回 EOF,且设置错误标识符(即 feof)
*/
int fflush(FILE *stream)
/**
* @brief 改变缓冲流的方法,控制缓冲区的大小和位置
* setvbuf 函数的调用必须在打开 stream 之后,在对其执行任何其他操作之前
* @param[in] stream 标识一个打开的流
* @param[in] buf 期望缓冲区的地址。缓冲区可以有静态存储期、自动存储期,还可以是动态分配的。
* 如果缓冲区是局部于函数的,并且具有自动存储期,一定要确保在函数返回之前关闭流。
* 动态分配缓冲区可以在不需要时释放缓冲区,一定要确保在释放缓冲区之前已经关闭了流。
* 如果设置为 NULL,函数自动分配一个指定大小的缓冲区。
* @param[in] mode 期望的缓冲类型,为以下三个宏之一
* _IOFBF(满缓冲),当缓冲区为空时,从流读入数据;当缓冲区满时,向流写入数据。(默认设置,如果流没有与交互式设备相连)
* _IOLBF(行缓冲),每次从流读入一行数据或者向流写入一行数据。
* _IONBF(无缓冲),直接从流读入数据或者直接向流写入数据,不使用缓冲区。
* @param[in] size 缓冲区的大小,以字节为单位
* @return 如果成功,返回零值,否则返回非零值。
*/
int setvbuf(FILE * restrict stream, char * restrict buf, int mode, size_t size);
1.2.6 文件删除和重命名
/**
* @brief 删除文件
* 如果打开了要删除的文件,需要在调用 remove 函数之前关闭此文件。
* @param[in] filename 要被删除的文件名称
* @return 如果成功,返回零值。否则,返回非零值。
*/
int remove(const char *filename);
/**
* @brief 改变文件的名字
* 如果打开了要改名的文件,需要在调用 rename 函数之前关闭此文件。
* @param[in] old 要被重命名/移动的文件名称
* @param[in] new 文件的新名称
* 如果具有新名字的文件已经存在了,改名的效果会由实现定义。
* @return 如果成功,返回零值。否则,返回非零值。
*/
int rename(const char *old, const char *new);
1.3 输入/输出
1.3.1 格式化的输入输出
格式化的输入输出,使用格式串来控制读/写:
- 在输入时把“字符格式”的数据转换成“数值格式”的数据
- 在输出时把“数值格式”的数据转换成“字符格式”的数据
1.3.1.1 …printf 函数
/**
* @brief 向输出流中写入可变数量的数据项,并且利用格式串来控制输出的形式。
* @param[in] format 包含“普通字符、转换说明”的格式串
* @param[in] ... 可变数量的实际参数
* @return 写入的字符数,若出错则返回一个负值
*/
int fprintf(FILE * restrict stream, const char * restrict format, ...);
int printf(const char * restrict format, ...);
-
printf
函数始终向stdout
(标准输出流)写入内容; -
fprintf
函数向第一个参数stream
指定的流中写入内容,不仅可以写入磁盘文件,还可以写入任何输出流- 如果写入
stdout
,那么fprintf
与printf
等价; - 如果写入
stderr
(标准错误流),可以让出错消息出现在屏幕上,即使用户重定向stdout
也没关系。
- 如果写入
-
关于格式串参数,其中的“普通字符”会原样输出,而“转换说明”则描述了如何把剩余的实参转换为字符格式显示出来,复习回顾:入门专题(上)=> 3.1.1 printf 函数
-
关于函数原型结尾的“代表可变数量实参的 … 符号”,以及另外两个“可以向流写入格式化的输出,并且带有可变参数列表”的
vfprintf
和vprintf
函数,可以回顾:提高专题(上)=> 3.4.3 可变参数输入/输出
1.3.1.2 …scanf 函数
/**
* @brief 从输入流读入数据,并且使用格式串来指明输入的格式。
* @param[in] format 包含“转换说明、空白字符、非空白字符”的格式串
* @param[out] ... 可变数量的对象指针
* 格式串的后边可以有任意数量的指针(每个指针指向一个对象)作为额外的实际参数;
* 输入的数据项根据格式串中的转换说明进行转换,并且存储在指针指向的对象中。
* @retval 读入并且赋值给对象的数据项的数量
* 如果没有输入字符可以读、编码错误(C99,按多字节字符的方式读取非多字节字符)、输入字符和格式串不匹配,函数会提前返回
* @retval EOF 在读取任何数据项之前发生输入失败,或检测到“文件结尾”,返回EOF(通常定义为-1)
*/
int fscanf(FILE * restrict stream, const char * restrict format, ...);
int scanf(const char * restrict format, ...);
-
scanf
函数始终从标准输入流stdin
中读入内容 -
fscanf
函数从第一个参数stream
所指定的流中读入内容- 如果从
stdin
读入内容,那么fscanf
与scanf
等价
- 如果从
-
…scanf
函数的调用类似于…printf
函数的调用,但他们的工作原理完全不同 -
可以把
scanf
、fscanf
函数看作“模式匹配”函数,格式串表示的就是…scanf
函数在读取输入时试图匹配的模式。如果输入和格式串不匹配,在发现不匹配时,函数就会返回。不匹配的输入字符将被“保留在缓冲区中”待以后读取。 -
关于格式串参数,可能含有“转换说明、空白字符、非空白字符”三类信息,复习回顾:入门专题(上)=> 3.1.2 scanf 函数
-
关于函数原型结尾的“代表可变数量实参的 … 符号”,以及另外两个“可以从流读入格式化的输入,并且带有可变参数列表”的
vfscanf
和vscanf
函数,可以回顾:提高专题(上)=> 3.4.3 可变参数输入/输出
1.3.2 检测文件末尾和错误
/**
* @brief 在针对流的操作失败后,通过测试流的指示器,查找失败的原因
* @param[in] stream 需要测试的流
* @return 如果流设置了“文件末尾指示器”,那么 feof 返回非零值,否则返回零
* 如果流设置了“错误指示器”,那么 ferror 返回非零值,否则返回零
*/
int feof(FILE *stream);
int ferror(FILE *stream);
/**
* @brief 同时清除文件末尾指示器和错误指示器
* 一旦设置了错误指示器或者文件末尾指示器,流就会保持这种状态直到被显式地清除
* 某些其他库函数也会顺便清除某种或两种指示器:
* 1)调用 rewind 函数可以清除这两种指示器,就好像打开或重新打开流一样;
* 2)调用 ungetc 函数、fseek 函数或者 fsetpos 函数仅可以清除文件末尾指示器。
*/
void clearerr(FILE *stream);
每个流都有与之相关的两个指示器:错误指示器(error indicator)和文件末尾指示器(end-of-file indicator):
- 打开流时,会清除这些指示器;
- 遇到文件末尾,就设置文件末尾指示器;
- 输入流上发生读错误,输出流上发生写错误,就设置错误指示器。
使用示例:
如果要求…scanf
函数读入并存储n
个数据项,那么期望的返回值就是n
。如果返回值小于n
,那么有三种可能的出错情况:
- 文件末尾:函数在完全匹配格式串之前遇到了文件末尾;
- 读取错误:函数不能从流中读取字符;
- 匹配失败:数据项的格式是错误的(例如,在搜索整数的第一个数字时遇到了一个字母)。
可以通过流的指示器来判断失败原因:
- 如果
feof
函数返回了非零的值,说明已经到达了输入文件的末尾;- 如果
ferror
函数返回了非零的值,说明在输入过程中产生了读错误;- 如果两个函数都没有返回非零值,那么一定是发生了匹配失败。
1.3.3 字符的输入/输出
本节中的函数用于读写单个字符,可以处理文本流和二进制流。把字符作为 int
类型而非 char
类型的值来处理。因为输入函数通过返回 EOF
(一个负的整型常量) 来说明文件末尾(或错误)情况。
1.3.3.1 输出函数
// 向指定的流 stream 中写一个字符,只作为函数实现
// 如果出现写错误,为流设置错误指示器并且返回 EOF;否则,返回写入的字符。
int fputc(int c, FILE *stream);
// 向指定的流 stream 中写一个字符
// 通常作为宏来实现(也有函数实现),putc 宏可以对 stream 参数多次求值
int putc(int c, FILE *stream);
// 向标准输出流 stdout 写一个字符
int putchar(int c);
1.3.3.2 输入函数
// 从指定的流 stream 中读入一个字符,只作为函数实现
// 如果遇到了文件末尾,会设置流的文件末尾指示器,并且返 回EOF
// 如果产生了读错误,会设置流的错误指示器,并且返回 EOF
// 把字符看作 unsigned char 类型的值(返回之前转换成 int 类型),所以不会返回除 EOF 之外的负值
int fgetc(FILE *stream);
// 从指定的流 stream 中读入一个字符
// 通常作为宏来实现(也有函数实现),getc 宏可以对参数多次求值(可能会有问题)
int getc(FILE *stream);
// 从标准输入流 stdin 中读入一个字符
int getchar(void);
// 把从流中读入的字符“放回”,并清除流的文件末尾指示器。
// 如果持续调用 ungetc 函数,只有第一次的函数调用保证会成功
// 调用文件定位函数(即 fseek、fsetpos 或 rewind)会导致需要放回的字符丢失。
// 返回要求放回的字符,如果试图放回 EOF 或者放回超过最大允许数量的字符数,会返回 EOF。
int ungetc(int c, FILE *stream);
// 如果在输入过程中需要往前多看一个字符,可以使用 ungetc 来实现
// 示例:读入一系列数字,并且在遇到首个非数字时停止操作
while (isdigit(ch = getc(fp))) {
...
}
ungetc(ch, fp); // pushes back last character read
1.3.4 行的输入/输出
本节中的函数用于逐行读写,主要处理文本流。
1.3.4.1 输出函数
// 把字符串写入到指定的流 stream 中,不会在字符串末尾添加换行符
// 当出现写错误时,返回 EOF;否则,返回一个非负的数。
int fputs(const char * restrict s, FILE * restrict stream);
// 把字符串写入到标准输出流 stdout 中,会在字符串末尾添加一个换行符
int puts(const char *s);
1.3.4.2 输入函数
/**
* @brief 从指定的流 stream 中读取一行
* 逐个读入字符,直到遇到首个换行符,或者已经读入了 n-1 个字符,或者到达文件末尾;会在字符串的末尾追加空字符
* 如果读入了换行符,会把换行符和其他字符一起存储(gets 函数从来不存储换行符)
* @param[out] s 用于保存输入字符的数组的指针
* @param[in] n 要读取的最大字符数(包括最后的空字符),通常使用以 s 传递的数组的长度
* @param[in] stream 要从中读取字符的流,如果是 stdin,则从标准输入流中读取
* @return 如果成功,返回第一个实参;
* 如果遇到错误,或者是在读取任何字符之前达到了输入流的末尾(可以通过指示器判断),返回空指针。
*/
char *fgets(char * restrict s, int n, FILE * restrict stream);
1.3.5 块的输入/输出
本节中的函数用于逐块读写,主要处理二进制流。
1.3.5.1 输出函数
/**
* @brief 把 ptr 所指向的数组中的数据写入到给定流 stream 中
* @param[in] ptr 数组的地址(也可以是任意类型变量的地址,相当于只有一个元素的数组)
* @param[in] size 每个数组元素的大小(以字节为单位)
* @param[in] nmemb 要写的元素数量
* @param[in] stream 输出文件指针
* @return 返回实际写入的元素(不是字节)的数量;如果出现写入错误,那么返回值就会小于第三个实参。
*/
size_t fwrite(const void * restrict ptr, size_t size, size_t nmemb, FILE * restrict stream);
1.3.5.2 输入函数
/**
* @brief 从给定流 stream 读取数据到 ptr 所指向的数组中
* @param[out] ptr 数组的地址,最小尺寸为 size*nmemb 字节
* @param[in] size 每个数组元素的大小(以字节为单位)
* @param[in] nmemb 要读的元素数量
* @param[in] stream 输入文件指针
* @return 返回实际读取的元素(不是字节)的数量。应该等于第三个参数,除非达到了输入文件末尾或者出现了错误。
*/
size_t fread(void * restrict ptr, size_t size, size_t nmemb, FILE * restrict stream);
1.4 文件定位
每个流都有相关联的文件位置(file position)
- 打开文件时,会将文件位置设置在文件的起始处(如果以“追加”模式打开,初始位置可以在起始处,也可以在末尾,依赖于具体的实现);
- 在执行读或写操作时,文件位置会自动推进,这样可以按照顺序贯穿整个文件;
- 通过特定库函数,程序可以确定当前的文件位置,或者改变文件的位置。
/**
* @brief 为流 stream 设置新的文件位置,更适用于二进制流
* 对于文本流,只可以移动到起始处或末尾处,或者返回前面访问过的通过 ftell 获得的位置
* 对于二进制流,不强制要求支持 whence 是 SEEK_END 的调用
* @param[in] stream 目标文件指针
* @param[in] offset 相对 whence 的偏移量,以字节为单位,可以为正(前移)、负(后移)或 0(无偏移)
* @param[in] whence 偏移量的起始点
* SEEK_SET:文件的起始处;
* SEEK_CUR:文件的当前位置;
* SEEK_END:文件的末尾处。
* @return 通常情况下,返回 0;如果产生错误(例如,要求的位置不存在),返回非零值。
*/
int fseek(FILE *stream, long int offset, int whence);
// 返回当前文件位置;如果发生错误,返回-1L,并且把错误码存储到 errno(<errno.h> 中声明的 int 类型变量)
// 对于二进制流,以字节计数来返回当前文件位置,其中 0 表示文件的起始处
// 对于文本流,存在类似“把 \r\n 当作一个字节计数”的情况,所以返回值可能不是实际字节计数
long int ftell(FILE *stream);
// 功能与 ftell、fseek(文件位置只能存储在长整数中) 类似,但是可以通过 fpos_t 处理更大的文件
// 成功时,返回 0;否则,返回非零值,同时把错误码存储到 errno 中
int fgetpos(FILE * restrict stream, fpos_t * restrict pos);
int fsetpos(FILE *stream, const fpos_t *pos);
// 示例:使用 fgetpos 函数保存文件位置,稍后使用 fsetpos 函数返回之前的位置
fpos_t file_pos;
...
fgetpos(fp, &file_pos); // saves current position
...
// file_pos 值必须通过前面的 fgetpos 调用获得
fsetpos(fp, &file_pos); // returns to old position
// 把文件位置设置在起始处
// 调用 rewind(fp) 几乎等价于 fseek(fp, 0L, SEEK_SET),两者的差异是:
// 1)rewind 函数没有返回值,
// 2)rewind 会为 fp 清除错误指示器。
void rewind(FILE *stream);
2、程序设计
简要介绍几个在程序设计中比较重要的思想。
2.1 模块
设计 C 程序时,最好将它看作一些独立的模块。模块是一组服务的集合,包括内部的实现和对外的接口。
- “服务”就是函数;
- “接口”就是头文件,包含可以被程序中其他文件调用的函数原型;
- “实现”就是源文件,包含该模块中的函数定义。
好的模块接口应该具有“高内聚性、低耦合性”,据此,模块通常分为下面几类:
- 数据池,是一些相关的变量或常量的集合。
- 通常只是一个头文件(通常不建议放变量);
- 示例:C 库中的 <float.h> 头和 <limits.h> 头。
- 库,是一个相关函数的集合。
- 示例:<string.h> 头就是字符串处理函数库的接口。
- 抽象对象,是指“对隐藏的数据结构进行操作的”函数的集合。
- 缺点:无法拥有该对象的多个实例。
- 抽象数据类型(ADT),将具体实现方式隐藏起来的数据类型。
- 可以使用该类型来声明变量,但不知道变量的具体数据结构;
- 通过调用抽象数据类型模块所提供的函数,对变量进行操作;
- 从技术上说,C 库中没有抽象数据类型,但有一些很接近,比如 FILE 类型;
- 相对“抽象对象”,ADT 可以有任意个实例。
2.2 信息隐藏
设计良好的模块经常会对它的客户隐藏一些信息,即信息隐藏。其优点包括:
- 必须通过模块自身提供的接口来进行操作,提高了模块的安全性;
- 修改模块的内部实现时,不需要改变模块的接口,提高了模块的灵活性。
在 C 语言中,强制信息隐藏的主要工具是 static
存储类型,通过声明为 static
:
- 使得具有文件作用域的变量不能被其他文件访问;
- 使得函数只能被同一文件中的其他函数直接调用。
2.3 不完整类型(封装)
C 语言提供的唯一封装工具为:不完整类型(incomplete type),描述了对象但缺少定义对象大小所需的信息。
// 告诉编译器 t 是一个结构标记,但并没有描述结构的成员(编译器并没有足够的信息来确定该结构的大小)
// 不完整类型会在程序的其他地方将信息补充完整
struct t;
// 错误,编译器不知道不完整类型的大小,不可以用它来声明变量
struct t s;
// 类型 T 的变量是指向标记为 t 的结构的指针,可以声明类型 T 的变量
typedef struct t *T;
不完整类型情况例举:
- 声明结构标记而不指明结构的成员;
- 声明联合标记而不指明联合的成员;
- 声明数组但不指定大小
extern int a[]
; - 弹性数组成员;
void
永远不能变成完整类型,无法声明其变量。
3、C 和 C++ 的区别
在很大程度上,C++ 是 C 的超集。C 和 C++ 的主要区别是,C++ 支持许多附加特性。C++ 中也有许多规则与 C 稍有不同,这些不同使得 C 程序作为 C++ 程序编译时可能以不同的方式运行或根本不能运行。本节着重讨论“C99、C11 与 C++11 之间的”区别。
3.1 函数原型
- 在 C++ 中,函数原型参数必不可少;在 C 中,是可选的。
// 在 C 中,空圆括号说明这是前置原型
// 在 C++ 中,空圆括号说明该函数没有参数
int slice();
// 在 C++ 中,允许声明多个同名函数,只要它们的参数列表不同即可
int slice(int, int);
int main(){
...
slice(20, 50);
...
}
int slice(int a, int b){
...
}
3.2 char 常量
- 在 C++ 中,把
char
常量视为char
类型;在 C 中,将其视为int
类型。
// 在 C++ 中,常量 'A' 和变量 ch 都占用1字节。
// 在 C 中,
// 常量 'A' 被存储在 int 大小的内存块中(字符编码被存储为一个 int 类型的值);
// 变量 ch 只占内存的(最后)1字节。
char ch = 'A';
// 有些 C 程序利用 char 常量被视为 int 类型这一特性,用字符来表示整数值(依赖特定的字符编码)
// 每个字符都对应一个字节,可以单独设置 int 类型中的每个字节( C 的早期版本没有提供十六进制记法)
// 如果一个系统中的 int 是4字节,可以这样编写 C 代码:
int x = 'ABCD';
3.3 const 限定符
- 在 C++ 中,全局的
const
限定符,默认具有内部链接;在 C 中,默认具有外部链接。
// 在 C++ 中
// 内部链接声明,如果在头文件中使用 const,那么每个包含该头文件的文件都会获得一份 const 变量的备份
const double PI = 3.14159;
// 外部链接声明
extern const double PI = 3.14159;
// 在 C 中
// 外部链接声明,必须在一个文件中进行定义式声明,然后在其他文件中使用关键字 extern 进行引用式声明
const double PI = 3.14159;
// 内部链接声明
static const double PI = 3.14159;
- 在 C++ 中,可以用
const
来声明普通数组的大小;在 C99 中,使用相同的声明,可以创建一个变长数组。
const int ARSIZE = 100;
double loons[ARSIZE];
- 在 C++ 中,可以使用
const
值来初始化其他const
变量;在 C 中,不能这样做。
const double RATE = 0.06; // C++ 和 C 都可以
const double STEP = 24.5; // C++ 和 C 都可以
const double LEVEL = RATE * STEP; // C++ 可以,C 不可以?
3.4 结构和联合
- 声明一个有标记的结构或联合后,在 C++ 中,可以使用这个标记作为类型名;在 C 中,不可以。
struct duo
{ int a;
int b;
};
struct duo m; // C++ 和 C 都可以
duo n; // C++ 可以,C 不可以
// 在 C++ 中,结构名会与变量名冲突?
float duo = 100.3;
- 对于在一个结构内部声明的另一个结构,C 可以直接访问,C++ 不可以。
struct box {
struct point {int x; int y;} upperleft;
struct point lowerright;
};
struct box ad; // C 和 C++ 都可以
struct point dot; // C 可以,C++ 不可以
box::point dot; // C 不可以,C++ 可以
3.5 枚举
- C++ 使用枚举比 C 严格:
- 只能把
enum
常量赋给enum
变量,然后把变量与其他值作比较; - 不经过显式强制类型转换,不能把
int
类型值赋给enum
变量,而且也不能递增一个enum
变量。
- 只能把
enum sample {sage, thyme, salt, pepper};
enum sample season;
season = sage; // C 和 C++ 都可以
season = 2; // 在 C 中会发出警告,在 C++ 中是一个错误
season = (enum sample) 3; // C 和 C++ 都可以
season++; // C 可以,在 C++ 中是一个错误
- 与结构和联合的情况类似,在 C++ 中,
- 不使用关键字
enum
也可以声明枚举变量; - 如果一个变量和
enum
类型的同名会导致名称冲突。
- 不使用关键字
enum sample {sage, thyme, salt, pepper};
sample season; // C++ 可以,在 C 中不可以
3.6 指向 void 的指针
- 任意类型的指针可以赋给指向
void
的指针;(延伸:C++ 可以把派生类对象的地址赋给基类指针) - 在 C++ 中,只有使用“显式强制类型转换”才能把指向
void
的指针赋给其他类型的指针;在 C 中,不需要“显式强制类型转换”。
int ar[5] = {4, 5, 6,7, 8};
int *pi;
void *pv;
pv = ar; // C 和 C++ 都可以
pi = pv; // C 可以,C++ 不可以
pi = (int*) pv; // C 和 C++ 都可以
3.7 布尔类型
- 在 C++ 中,布尔类型是
bool
,而且ture
和false
都是关键字; - 在 C 中,布尔类型是
_Bool
;需要要包含stdbool.h
头文件才可以使用bool
、true
和false
。
3.8 可选拼写
- 在 C++ 中,可以用
or
来代替||
,还有一些其他的可选拼写,它们都是关键字; - 在 C99 和 C11 中,这些可选拼写都被定义为宏,要包含
iso646.h
才能使用它们。
3.9 宽字符支持
-
在 C++ 中,
wchar_t
、char16_t
(C++11) 和char32_t
(C++11) 是关键字,它们是内置类型;- 通过
iostream
头文件提供宽字符 I/O 支持。
-
在 C 中,
- C99 和 C11 的
wchar_t
类型被定义在多个头文件中(stddef.h
、stdlib.h
、wchar.h
、wctype.h
); - C11 的
char16_t
和char32_t
定义在uchar.h
头文件中; - C99 通过
wchar.h
头文件提供宽字符 I/O 支持包。
- C99 和 C11 的
3.10 复数类型
- C++ 在
complex
头文件中提供一个复数类来支持复数类型; - C 有内置的复数类型,并通过
complex.h
头文件来支持; - 两种方法区别很大,不兼容。
3.11 内联函数
C99 支持了 C++ 的内联函数特性(实现更加灵活)
- C 允许混合使用内联定义和外部定义,而 C++ 不允许。
- 在 C++ 中,内联函数默认是内部链接。如果一个内联函数多次出现在多个文件中,该函数的定义必须相同,而且要使用相同的语言记号。例如,
int
和int32_t
是不同的语言记号,即便使用typedef
把int32_t
定义为int
也不能这样做。但是在 C 中可以这样做。
3.12 C++11 中没有的 C99/C11 特性
- 指定初始化器;
- 复合初始化器(Compound initializer);
- 受限指针(Restricted pointer),即
restric
指针; - 变长数组;
- 伸缩型数组成员;
- 带可变数量参数的宏。
参考
- [美] K. N. 金(K. N. King)著,吕秀锋,黄倩译.C语言程序设计:现代方法(第2版·修订版).人民邮电出版社.2021:209.
- [美] 史蒂芬·普拉达著.C Primer Plus(第6版 中文版 最新修订版).人民邮电出版社.2019:115.
宁静以致远,感谢 Vico 老师。