2022-11-21malloc()和free()

存储类别有一个共同之处: 在确定用哪种存储类别后,根据已制定好的内存管理规则, 将自动选择其作用域和存储期。 然而, 还有更灵活地选择, 即用库函数分配和管理内存。

所有程序都必须预留足够的内存来储存程序使用的数据。

float x;

char place[] = "Dancing Oxen Creek";

为一个float类型的值和一个字符串预留了足够的内存, 或者可以显式指定分配一定数量的内存.

int plates[100];

该声明预留了100个内存位置, 每个位置都用于储存int类型的值。 声明还为内存提供了一个标识符。 因此, 可以使用x或place识别数据。静态数据在程序载入内存时分配, 而自动数据在程序执行块时分配, 并在程序离开该块时销毁。

C 能做的不止这些。 可以在程序运行时分配更多的内存。 主要的工具是malloc()函数, 该函数接受一个参数: 所需的内存字节数。 malloc()函数会找到合适的空闲内存块, 这样的内存是匿名的。 也就是说, malloc()分配内存, 但是不会为其赋名。 然而, 它确实返回动态分配内存块的首字节地址。因此, 可以把该地址赋给一个指针变量, 并使用指针访问这块内存。 因为char表示1字节, malloc()的返回类型通常被定义为指向char的指针。 然而,从ANSI C标准开始, C使用一个新的类型: 指向void的指针。 该类型相当于一个“通用指针”。 malloc()函数可用于返回指向数组的指针、 指向结构的指针等, 所以通常该函数的返回值会被强制转换为匹配的类型。

在ANSI C中, 应该坚持使用强制类型转换, 提高代码的可读性。 然而, 把指向 void的指针赋给任意类型的指针完全不用考虑类型匹配的问题。 如果 malloc()分配内存失败, 将返回空指针。

有3种创建数组的方法

声明数组时, 用常量表达式表示数组的维度, 用数组名访问数组的元素。 可以用静态内存或自动内存创建这种数组。

声明变长数组(C99新增的特性) 时, 用变量表达式表示数组的维度,用数组名访问数组的元素。 具有这种特性的数组只能在自动内存中创建。

声明一个指针, 调用malloc(), 将其返回值赋给指针, 使用指针访问数组的元素。 该指针可以是静态的或自动的。

使用第2种和第3种方法可以创建动态数组(dynamic array) 。 这种数组和普通数组不同, 可以在程序运行时选择数组的大小和分配内存。

使用malloc(), 程序可以在运行时才确定数组大小。

在C中, 不一定要使用强制类型转换(double *), 但是在C++中必须使用。 所以, 使用强制类型转换更容易把C程序转换为C++程序。

free()函数位于程序的末尾, 它释放了malloc()函数分配的内存。free()函数只释放其参数指向的内存块。 一些操作系统在程序结束时会自动释放动态分配的内存, 但是有些系统不会。 为保险起见, 请使用free()

使用动态数组给程序带来了更多灵活性。

free()的重要性

静态内存的数量在编译时是固定的, 在程序运行期间也不会改变。 自动变量使用的内存数量在程序执行期间自动增加或减少。 但是动态分配的内存数量只会增加, 除非用 free()进行释放。

在函数末尾处调用free()函数可避免内存泄漏

callloc()函数

long * newmem;

newmem = (long *)calloc(100, sizeof (long));

和malloc()类似, 在ANSI之前, calloc()也返回指向char的指针; 在ANSI之后, 返回指向void的指针。 如果要储存不同的类型, 应使用强制类型转换运算符。 calloc()函数接受两个无符号整数作为参数(ANSI规定是size_t类型) 。 第1个参数是所需的存储单元数量, 第2个参数是存储单元的大小(以字节为单位) 。

用sizeof(long)而不是4, 提高了代码的可移植性。 这样, 在其他long不是4字节的系统中也能正常工作。

calloc()函数还有一个特性: 它把块中的所有位都设置为0(注意, 在某些硬件系统中, 不是把所有位都设置为0来表示浮点值0) 。

free()函数也可用于释放calloc()分配的内存。

动态内存分配和变长数组

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()的指针变量不同, 但是两个指针必须储存相同的地址。 但是, 不能释放同一块内存两次。

存储类别 和动态内存分配

一个理想化模型。 可以认为程序把它可用的内存分为 3部分: 一部分供具有外部链接、 内部链接和无链接的静态变量使用; 一部分供自动变量使用; 一部分供动态内存分配。

静态存储类别所用的内存数量在编译时确定, 只要程序还在运行, 就可访问储存在该部分的数据。 该类别的变量在程序开始执行时被创建, 在程序结束时被销毁。

然而, 自动存储类别的变量在程序进入变量定义所在块时存在, 在程序离开块时消失。 因此, 随着程序调用函数和函数结束, 自动变量所用的内存数量也相应地增加和减少。 这部分的内存通常作为栈来处理, 这意味着新创建的变量按顺序加入内存, 然后以相反的顺序销毁。

动态分配的内存在调用 malloc()或相关函数时存在, 在调用 free()后释放。 这部分的内存由程序员管理, 而不是一套规则。 所以内存块可以在一个函数中创建, 在另一个函数中销毁。 正是因为这样, 这部分的内存用于动态内存分配会支离破碎。 也就是说, 未使用的内存块分散在已使用的内存块之间。 另外, 使用动态内存通常比使用栈内存慢。

静态数据(包括字符串字面量) 占用一个区域, 自动数据占用另一个区域, 动态分配的数据占用第3个区域(通常被称为内存堆或自由内存) 。

ANSIC类型限定符

我们通常用类型和存储类别来描述一个变量。 C90 还新增了两个属性:恒常性(constancy) 和易变性(volatility) 。 这两个属性可以分别用关键字const 和 volatile 来声明, 以这两个关键字创建的类型是限定类型(qualified type) 。 C99标准新增了第3个限定符: restrict, 用于提高编译器优化。 C11标准新增了第4个限定符: _Atomic。 C11提供一个可选库, 由stdatomic.h管理, 以支持并发程序设计, 而且_Atomic是可选支持项。

C99 为类型限定符增加了一个新属性: 它们现在是幂等的(idempotent) ! 这个属性听起来很强大, 其实意思是可以在一条声明中多次使用同一个限定符, 多余的限定符将被忽略。

const类型限定符

const关键字声明的对象, 其值不能通过赋值或递增、 递减来修改。

const int nochange; /* 限定nochange的值不能被修改 */

nochange = 12; /* 不允许 */

可以初始化const变量。

const int nochange = 12; /* 没问题 */

该声明让nochange成为只读变量。 初始化后, 就不能再改变它的值。

在指针和形参声明中使用const

声明普通变量和数组时使用 const 关键字很简单。 指针则复杂一些, 因为要区分是限定指针本身为const还是限定指针指向的值为const。

把const放在类型名之后、 *之前, 说明该指针不能用于改变它所指向的值。 简而言之, const放在*左侧任意位置, 限定了指针指向的数据不能改变; const放在*的右侧, 限定了指针本身不能改变。

const 关键字的常见用法是声明为函数形参的指针。 例如, 假设有一个函数要调用display()显示一个数组的内容。 要把数组名作为实际参数传递给该函数, 但是数组名是一个地址。

void display(const int array[], int limit);

在函数原型和函数头, 形参声明const int array[]与const int * array相同,所以该声明表明不能更改array指向的数据。

ANSI C库遵循这种做法。 如果一个指针仅用于给函数访问值, 应将其声明为一个指向const限定类型的指针。 如果要用指针更改主调函数中的数据, 就不使用const关键字。

对全局数据使用const

前面讲过, 使用全局变量是一种冒险的方法, 因为这样做暴露了数据,程序的任何部分都能更改数据。 如果把数据设置为 const, 就可避免这样的危险, 因此用 const 限定符声明全局数据很合理。 可以创建const变量、 const数组和const结构。

另一种方案是, 把const变量放在一个头文件中, 然后在其他文件中包含该头文件。

头文件方案的好处是, 方便你偷懒, 不用惦记着在一个文件中使用定义式声明, 在其他文件中使用引用式声明。 所有的文件都只需包含同一个头文件即可。 但它的缺点是, 数据是重复的。

volatile类型限定符

volatile 限定符告知计算机, 代理(而不是变量所在的程序) 可以改变该变量的值。 通常, 它被用于硬件地址以及在其他程序或同时运行的线程中共享数据。

智能的(进行优化的) 编译器会注意到以上代码使用了两次 x, 但并未改变它的值。 于是编译器把 x的值临时储存在寄存器中, 然后在val2需要使用x时, 才从寄存器中(而不是从原始内存位置上) 读取x的值, 以节约时间。 这个过程被称为高速缓存(caching) 。 通常, 高速缓存是个不错的优化方案, 但是如果一些其他代理在以上两条语句之间改变了x的值, 就不能这样优化了。 如果没有volatile关键字, 编译器就不知道这种事情是否会发生。为安全起见, 编译器不会进行高速缓存。 这是在 ANSI 之前的情况。 现在, 如果声明中没有volatile关键字, 编译器会假定变量的值在使用过程中不变, 然后再尝试优化代码。

restrict类型限定符

restrict 关键字允许编译器优化某部分代码以更好地支持计算。 它只能用于指针, 表明该指针是访问数据对象的唯一且初始的方式。

int ar[10];

int * restrict restar = (int *) malloc(10 * sizeof(int));

int * par = ar;

这里, 指针restar是访问由malloc()所分配内存的唯一且初始的方式。 因此, 可以用restrict关键字限定它。 而指针par既不是访问ar数组中数据的初始方式, 也不是唯一方式。 所以不用把它设置为restrict。

restrict 限定符还可用于函数形参中的指针。 这意味着编译器可以假定在函数体内其他标识符不会修改该指针指向的数据, 而且编译器可以尝试对其优化, 使其不做别的用途。 例如, C 库有两个函数用于把一个位置上的字节拷贝到另一个位置。 在C99中, 这两个函数的原型是

void * memcpy(void * restrict s1, const void * restrict s2, size_t n);

void * memmove(void * s1, const void * s2, size_t n);

这两个函数都从位置s2把n字节拷贝到位置s1。 memcpy()函数要求两个位置不重叠, 但是memove()没有这样的要求。 声明s1和s2为restrict说明这两个指针都是访问相应数据的唯一方式, 所以它们不能访问相同块的数据。 这满足了memcpy()无重叠的要求。 memmove()函数允许重叠, 它在拷贝数据时不得不更小心, 以防在使用数据之前就先覆盖了数据。

restrict 关键字有两个读者。 一个是编译器, 该关键字告知编译器可以自由假定一些优化方案。 另一个读者是用户, 该关键字告知用户要使用满足restrict要求的参数。

Atomic类型限定字符(C11)

C11通过包含可选的头文件stdatomic.h和threads.h, 提供了一些可选的(不是必须实现的)管理方法。 值得注意的是, 要通过各种宏函数来访问原子类型。 当一个线程对一个原子类型的对象执行原子操作时, 其他线程不能访问该对象。

int hogs;// 普通声明

hogs = 12; // 普通赋值

可以替换成:

_Atomic int hogs; // hogs 是一个原子类型的变量

atomic_store(&hogs, 12); // stdatomic.h中的宏

这里, 在hogs中储存12是一个原子过程, 其他线程不能访问hogs。编写这种代码的前提是, 编译器要支持这一新特性。

旧关键字的新位置

C99允许把类型限定符和存储类别说明符static放在函数原型和函数头的形式参数的初始方括号中。 对于类型限定符而言, 这样做为现有功能提供了一个替代的语法。

void ofmouth(int * const a1, int * restrict a2, int n); // 以前的风格

该声明表明a1是一个指向int的const指针, 这意味着不能更改指针本身,可以更改指针指向的数据。 除此之外, 还表明a2是一个restrict指针, 如上一节所述。

void ofmouth(int a1[const], int a2[restrict], int n); // C99允许

根据新标准, 在声明函数形参时, 指针表示法和数组表示法都可以使用这两个限定符。

static的情况不同, 因为新标准为static引入了一种与以前用法不相关的新用法。 现在, static除了表明静态存储类别变量的作用域或链接外, 新的用法告知编译器如何使用形式参数。

double stick(double ar[static 20]);

static 的这种用法表明, 函数调用中的实际参数应该是一个指向数组首元素的指针, 且该数组至少有20个元素。 这种用法的目的是让编译器使用这些信息优化函数的编码。

restrict 关键字有两个读者。 一个是编译器, 该关键字告知编译器可以自由假定一些优化方案。 另一个读者是用户, 该关键字告知用户要使用满足restrict要求的参数。

关键字概念

静态内存、 自动内存和动态分配内存的属性。 尤其要注意: 静态内存的数量在编译时确定; 静态数据在载入程序时被载入内存。 在程序运行时, 自动变量被分配或释放, 所以自动变量占用的内存数量随着程序的运行会不断变化。 可以把自动内存看作是可重复利用的工作区。 动态分配的内存也会增加和减少, 但是这个过程由函数调用控制, 不是自动进行的。

本章小结

内存用于存储程序中的数据, 由存储期、 作用域和链接表征。 存储期可以是静态的、 自动的或动态分配的。 如果是静态存储期, 在程序开始执行时分配内存, 并在程序运行时都存在。 如果是自动存储期, 在程序进入变量定义所在块时分配变量的内存, 在程序离开块时释放内存。 如果是动态分配存储期, 在调用malloc()(或相关函数) 时分配内存, 在调用free()函数时释放内存。

作用域决定程序的哪些部分可以访问某数据。 定义在所有函数之外的变量具有文件作用域, 对位于该变量声明之后的所有函数可见。 定义在块或作为函数形参内的变量具有块作用域, 只对该块以及它包含的嵌套块可见。

链接描述定义在程序某翻译单元中的变量可被链接的程度。 具有块作用域的变量是局部变量, 无链接。 具有文件作用域的变量可以是内部链接或外部链接。 内部链接意味着只有其定义所在的文件才能使用该变量。 外部链接意味着其他文件使用也可以使用该变量。

C的5种存储类别(不包括线程的概念)

自动——在块中不带存储类别说明符或带 auto 存储类别说明符声明的变量(或作为函数头中的形参) 属于自动存储类别, 具有自动存储期、 块作用域、 无链接。 如果未初始化自动变量, 它的值是未定义的。

寄存器——在块中带 register 存储类别说明符声明的变量(或作为函数头中的形参) 属于寄存器存储类别, 具有自动存储期、 块作用域、 无链接,且无法获取其地址。 把一个变量声明为寄存器变量即请求编译器将其储存到访问速度最快的区域。 如果未初始化寄存器变量, 它的值是未定义的。

静态、 无链接——在块中带static存储类别说明符声明的变量属于“静态、 无链接”存储类别, 具有静态存储期、 块作用域、 无链接。 只在编译时被初始化一次。 如果未显式初始化, 它的字节都被设置为0。

静态、 外部链接——在所有函数外部且没有使用 static 存储类别说明符声明的变量属于“静态、 外部链接”存储类别, 具有静态存储期、 文件作用域、 外部链接。 只能在编译器被初始化一次。 如果未显式初始化, 它的字节都被设置为0。

静态、 内部链接——在所有函数外部且使用了 static 存储类别说明符声明的变量属于“静态、 内部链接”存储类别, 具有静态存储期、 文件作用域、内部链接。 只能在编译器被初始化一次。 如果未显式初始化, 它的字节都被设置为0。

动态分配的内存由 malloc()(或相关) 函数分配, 该函数返回一个指向指定字节数内存块的指针。 这块内存被free()函数释放后便可重复使用,free()函数以该内存块的地址作为参数。

类型限定符const、 volatile、 restrict和_Atomic。 const限定符限定数据在程序运行时不能改变。 对指针使用const时, 可限定指针本身不能改变或指针指向的数据不能改变, 这取决于const在指针声明中的位置。 volatile 限定符表明, 限定的数据除了被当前程序修改外还可以被其他进程修改。 该限定符的目的是警告编译器不要进行假定的优化。 restrict限定符也是为了方便编译器设置优化方案。 restrict限定的指针是访问它所指向数据的唯一途径。




文件输入/输出

文件是当今计算机系统不可或缺的部分。 文件用于储存程序、 文档、 数据、 书信、 表格、 图形、 照片、 视频和许多其他种类的信息。 作为程序员,必须会编写创建文件和从文件读写数据的程序。

与文件进行通信

有时, 需要程序从文件中读取信息或把信息写入文件。 这种程序与文件交互的形式就是文件重定向。

books > bklist

用户的输入被重定向到 bklist 中。 这样做不仅会把不符合要求的文本写入 bklist, 而且用户也看不到要回答什么问题。

C提供了更强大的文件通信方法, 可以在程序中打开文件, 然后使用特殊的I/O函数读取文件中的信息或把信息写入文件。

文件是什么

文件(file) 通常是在磁盘或固态硬盘上的一段已命名的存储区。 对我们而言, stdio.h就是一个文件的名称, 该文件中包含一些有用的信息。 然而, 对操作系统而言, 文件更复杂一些。 例如, 大型文件会被分开储存, 或者包含一些额外的数据, 方便操作系统确定文件的种类。

C把文件看作是一系列连续的字节, 每个字节都能被单独读取。 这与UNIX环境中(C的发源地) 的文件结构相对应。 由于其他环境中可能无法完全对应这个模型, C提供两种文件模式: 文本模式和二进制模式。

文本模式和二进制模式

要区分文本内容和二进制内容、 文本文件格式和二进制文件格式, 以及文件的文本模式和二进制模式。

所有文件的内容都以二进制形式(0或1) 储存。 但是, 如果文件最初使用二进制编码的字符(例如, ASCII或Unicode) 表示文本(就像C字符串那样) , 该文件就是文本文件, 其中包含文本内容。 如果文件中的二进制值代表机器语言代码或数值数据(使用相同的内部表示, 假设, 用于long或double类型的值) 或图片或音乐编码, 该文件就是二进制文件, 其中包含二进制内容。

UNIX用同一种文件格式处理文本文件和二进制文件的内容。

鉴于C是作为开发UNIX的工具而创建的, C和UNIX在文本中都使用\n(换行符) 表示换行。 UNIX目录中有一个统计文件大小的计数, 程序可使用该计数确定是否读到文件结尾。

C 提供两种访问文件的途径: 二进制模式和文本模式。 在二进制模式中, 程序可以访问文件的每个字节。 而在文本模式中, 程序所见的内容和文件的实际内容不同。 程序以文本模式读取文件时,把本地环境表示的行末尾或文件结尾映射为C模式。

除了以文本模式读写文本文件, 还能以二进制模式读写文本文件。 如果读写一个旧式MS-DOS文本文件, 程序会看到文件中的\r 和\n 字符, 不会发生映射 。 如果要编写旧式 Mac格式、 MS-DOS格式或UNIX/Linux格式的文件模式程序, 应该使用二进制模式, 这样程序才能确定实际的文件内容并执行相应的动作。


虽然C提供了二进制模式和文本模式, 但是这两种模式的实现可以相同。 前面提到过, 因为UNIX使用一种文件格式, 这两种模式对于UNIX实现而言完全相同。 Linux也是如此。

I/O的级别

除了选择文件的模式, 大多数情况下, 还可以选择I/O的两个级别(即处理文件访问的两个级别) 。 底层I/O(low-level I/O) 使用操作系统提供的基本I/O服务。 标准高级I/O(standard high-level I/O) 使用C库的标准包和stdio.h头文件定义。

因为无法保证所有的操作系统都使用相同的底层I/O模型, C标准只支持标准I/O包。 有些实现会提供底层库, 但是C标准建立了可移植的I/O模型, 我们主要讨论这些I/O。

标准文件

C程序会自动打开3个文件, 它们被称为标准输入(standard input) 、 标准输出(standard output) 和标准错误输出(standard error output) 。 在默认情况下, 标准输入是系统的普通输入设备, 通常为键盘; 标准输出和标准错误输出是系统的普通输出设备, 通常为显示屏。

标准输入为程序提供输入, 它是 getchar()和 scanf()使用的文件。程序通常输出到标准输出, 它是putchar()、 puts()和printf()使用的文件。

重定向把其他文件视为标准输入或标准输出。 标准错误输出提供了一个逻辑上不同的地方来发送错误消息。 例如, 如果使用重定向把输出发送给文件而不是屏幕, 那么发送至标准错误输出的内容仍然会被发送到屏幕上。 这样很好, 因为如果把错误消息发送至文件, 就只能打开文件才能看到。

标准I/O

与底层I/O相比, 标准I/O包除了可移植以外还有两个好处。 第一, 标准I/O有许多专门的函数简化了处理不同I/O的问题。第二, 输入和输出都是缓冲的。也就是说, 一次转移一大块信息而不是一字节信息(通常至少512字节)

检查命令行参数

检查argc的值, 查看是否有命令行参数。如果没有, 程序将打印一条消息并退出程序。 字符串 argv[0]是该程序的名称。 显式使用 argv[0]而不是程序名, 错误消息的描述会随可执行文件名的改变而自动改变。 这一特性在像 UNIX 这种允许单个文件具有多个文件名的环境中也很方便。

exit()函数关闭所有打开的文件并结束程序。 正常结束的程序传递0, 异常结束的程序传递非零值。 不同的退出值可用于区分程序失败的不同原因。

根据ANSI C的规定, 在最初调用的main()中使用return与调用exit()的效果相同。在main()中

return 0;

和下面这条语句的作用相同:

exit(0);

如果main()在一个递归程序中, exit()仍然会终止程序, 但是return只会把控制权交给上一级递归, 直至最初的一级。 然后return结束程序。 return和exit()的另一个区别是, 即使在其他函数中(除main()以外) 调用exit()也能结束整个程序。

fopen()函数

程序使用fopen()函数打开文件。 该函数声明在stdio.h中。 它的第1个参数是待打开文件的名称, 更确切地说是一个包含改文件名的字符串地址。 第 2 个参数是一个字符串, 指定待打开文件的模式。

像UNIX和Linux这样只有一种文件类型的系统, 带b字母的模式和不带b字母的模式相同。

新的C11新增了带x字母的写模式, 与以前的写模式相比具有更多特性。 第一, 如果以传统的一种写模式打开一个现有文件, fopen()会把该文件的长度截为 0, 这样就丢失了该文件的内容。 但是使用带 x字母的写模式,即使fopen()操作失败, 原文件的内容也不会被删除。 第二, 如果环境允许,x模式的独占特性使得其他程序或线程无法访问正在被打开的文件。

程序成功打开文件后, fopen()将返回文件指针(file pointer) , 其他I/O函数可以使用这个指针指定该文件。 文件指针(该例中是fp) 的类型是指向FILE的指针, FILE是一个定义在stdio.h中的派生类型。 文件指针fp并不指向实际的文件, 它指向一个包含文件信息的数据对象, 其中包含操作文件的I/O函数所用的缓冲区信息。因为标准库中的I/O函数使用缓冲区, 所以它们不仅要知道缓冲区的位置, 还要知道缓冲区被填充的程度以及操作哪一个文件。 标准I/O函数根据这些信息在必要时决定再次填充或清空缓冲区。 fp指向的数据对象包含了这些信息

getc()和putc()函数

getc()和putc()函数与getchar()和putchar()函数类似。 所不同的是, 要告诉getc()和putc()函数使用哪一个文件。

“从标准输入中获取一个字符”:ch = getchar();

“从fp指定的文件中获取一个字符”:ch = getc(fp);

“把字符ch放入FILE指针fpout指定的文件中”:putc(ch, fpout);

在putc()函数的参数列表中, 第1个参数是待写入的字符, 第2个参数是文件指针。

程序把stdout作为putc()的第2个参数。 stdout作为与标准输出相关联的文件指针, 定义在stdio.h中, 所以putc(ch, stdout)与putchar(ch)的作用相同。 实际上, putchar()函数一般通过putc()来定义。 与此类似, getchar()也通过使用标准输入的getc()来定义。

文件结尾

从文件中读取数据的程序在读到文件结尾时要停止。

为了避免读到空文件, 应该使用入口条件循环(不是do while循环) 进行文件输入。

// 设计范例 #1

int ch; // 用int类型的变量储存EOF

FILE * fp;

fp = fopen("wacky.txt", "r");

ch = getc(fp); // 获取初始输入

while (ch != EOF)

putchar(ch); // 处理输入

ch = getc(fp); // 获取下一个输入

}

// 设计范例 #2

int ch;

FILE * fp;

fp = fopen("wacky.txt", "r");

while (( ch = getc(fp)) != EOF)

putchar(ch); //处理输入

}

读到文件结尾时也会返回一个错误信号(EOF 或 NULL指针) 。

你可能感兴趣的:(2022-11-21malloc()和free())