第03章 C语言提高专题(下)

声明:仅为个人学习总结,还请批判性查看,如有不同观点,欢迎交流。

摘要

本文总结了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)中,字节不仅可以表示字符,还可以表示数值数据、机器语言代码等各种数据。

文本文件具有两种二进制文件没有的特性:

特性WindowsUNIXMac 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,那么 fprintfprintf 等价;
    • 如果写入 stderr(标准错误流),可以让出错消息出现在屏幕上,即使用户重定向 stdout 也没关系。
  • 关于格式串参数,其中的“普通字符”会原样输出,而“转换说明”则描述了如何把剩余的实参转换为字符格式显示出来,复习回顾:入门专题(上)=> 3.1.1 printf 函数

  • 关于函数原型结尾的“代表可变数量实参的 … 符号”,以及另外两个“可以向流写入格式化的输出,并且带有可变参数列表”的 vfprintfvprintf 函数,可以回顾:提高专题(上)=> 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 读入内容,那么 fscanfscanf 等价
  • …scanf 函数的调用类似于 …printf 函数的调用,但他们的工作原理完全不同

  • 可以把 scanffscanf 函数看作“模式匹配”函数,格式串表示的就是 …scanf 函数在读取输入时试图匹配的模式。如果输入和格式串不匹配,在发现不匹配时,函数就会返回。不匹配的输入字符将被“保留在缓冲区中”待以后读取。

  • 关于格式串参数,可能含有“转换说明、空白字符、非空白字符”三类信息,复习回顾:入门专题(上)=> 3.1.2 scanf 函数

  • 关于函数原型结尾的“代表可变数量实参的 … 符号”,以及另外两个“可以从流读入格式化的输入,并且带有可变参数列表”的 vfscanfvscanf 函数,可以回顾:提高专题(上)=> 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 程序时,最好将它看作一些独立的模块。模块是一组服务的集合,包括内部的实现和对外的接口。

  • “服务”就是函数;
  • “接口”就是头文件,包含可以被程序中其他文件调用的函数原型;
  • “实现”就是源文件,包含该模块中的函数定义。

好的模块接口应该具有“高内聚性、低耦合性”,据此,模块通常分为下面几类:

  1. 数据池,是一些相关的变量或常量的集合。
    • 通常只是一个头文件(通常不建议放变量);
    • 示例:C 库中的 <float.h> 头和 <limits.h> 头。
  2. 库,是一个相关函数的集合。
    • 示例:<string.h> 头就是字符串处理函数库的接口。
  3. 抽象对象,是指“对隐藏的数据结构进行操作的”函数的集合。
    • 缺点:无法拥有该对象的多个实例。
  4. 抽象数据类型(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,而且 turefalse 都是关键字;
  • 在 C 中,布尔类型是 _Bool;需要要包含 stdbool.h 头文件才可以使用 booltruefalse

3.8 可选拼写

  • 在 C++ 中,可以用 or 来代替 ||,还有一些其他的可选拼写,它们都是关键字;
  • 在 C99 和 C11 中,这些可选拼写都被定义为宏,要包含 iso646.h 才能使用它们。

3.9 宽字符支持

  • 在 C++ 中,

    • wchar_tchar16_t(C++11) 和 char32_t(C++11) 是关键字,它们是内置类型;
    • 通过 iostream 头文件提供宽字符 I/O 支持。
  • 在 C 中,

    • C99 和 C11 的 wchar_t 类型被定义在多个头文件中(stddef.hstdlib.hwchar.hwctype.h);
    • C11 的 char16_tchar32_t 定义在 uchar.h 头文件中;
    • C99 通过 wchar.h 头文件提供宽字符 I/O 支持包。

3.10 复数类型

  • C++ 在 complex 头文件中提供一个复数类来支持复数类型;
  • C 有内置的复数类型,并通过 complex.h 头文件来支持;
  • 两种方法区别很大,不兼容。

3.11 内联函数

C99 支持了 C++ 的内联函数特性(实现更加灵活)

  • C 允许混合使用内联定义和外部定义,而 C++ 不允许。
  • 在 C++ 中,内联函数默认是内部链接。如果一个内联函数多次出现在多个文件中,该函数的定义必须相同,而且要使用相同的语言记号。例如,intint32_t 是不同的语言记号,即便使用 typedefint32_t 定义为 int 也不能这样做。但是在 C 中可以这样做。

3.12 C++11 中没有的 C99/C11 特性

  • 指定初始化器;
  • 复合初始化器(Compound initializer);
  • 受限指针(Restricted pointer),即 restric 指针;
  • 变长数组;
  • 伸缩型数组成员;
  • 带可变数量参数的宏。

参考

  1. [美] K. N. 金(K. N. King)著,吕秀锋,黄倩译.C语言程序设计:现代方法(第2版·修订版).人民邮电出版社.2021:209.
  2. [美] 史蒂芬·普拉达著.C Primer Plus(第6版 中文版 最新修订版).人民邮电出版社.2019:115.

宁静以致远,感谢 Vico 老师。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值