C 语言高级2-多维数组,结构体,递归操作

1. 多维数组

        

1.1 一维数组

  1. 元素类型角度:数组是相同类型的变量的有序集合
  2. 内存角度:连续的一大片内存空间

 在讨论多维数组之前,我们还需要学习很多关于一维数组的知识。首先让我们学习一个概念。

1.1.1 数组名

考虑下面这些声明:

int a;

int b[10];

我们把a称作标量,因为它是个单一的值,这个变量是的类型是一个整数。我们把b称作数组,因为它是一些值的集合。下标和数名一起使用,用于标识该集合中某个特定的值。例如,b[0]表示数组b的第1个值,b[4]表示第5个值。每个值都是一个特定的标量。那么问题是b的类型是什么?它所表示的又是什么?一个合乎逻辑的答案是它表示整个数组,但事实并非如此。在C中,在几乎所有数组名的表达式中,数组名的值是一个指针常量,也就是数组第一个元素的地址。它的类型取决于数组元素的类型:如果他们是int类型,那么数组名的类型就是“指向int的常量指针”;如果它们是其他类型,那么数组名的类型也就是“指向其他类型的常量指针”。

请问:指针和数组是等价的吗?

 答案是否定的。数组名在表达式中使用的时候,编译器才会产生一个指针常量。那么数组在什么情况下不能作为指针常量呢?在以下两种场景下:

  1. 当数组名作为sizeof操作符的操作数的时候,此时sizeof返回的是整个数组的长度,而不是指针数组指针的长度。
  2. 当数组名作为&操作符的操作数的时候,此时返回的是一个指向数组的指针,而不是指向某个数组元素的指针常量。
int arr[10];
//arr = NULL; //arr作为指针常量,不可修改
int *p = arr; //此时arr作为指针常量来使用
printf("sizeof(arr):%d\n", sizeof(arr)); //此时sizeof结果为整个数组的长度
printf("&arr type is %s\n", typeid(&arr).name()); //int(*)[10]而不是int*

 1.1.2 下标引用

int arr[] = { 1, 2, 3, 4, 5, 6 };

*(arr + 3) ,这个表达式是什么意思呢?

首先,我们说数组在表达式中是一个指向整型的指针,所以此表达式表示arr指针向后移动了3个元素的长度。然后通过间接访问操作符从这个新地址开始获取这个位置的值。这个和下标的引用的执行过程完全相同。所以如下表达式是等同的:

问题1:数组下标可否为负值?

问题2:请阅读如下代码,说出结果:

int arr[] = { 5, 3, 6, 8, 2, 9 };

int *p = arr + 2;

printf("*p = %d\n", *p);

printf("*p = %d\n", p[-1]);

 那么是用下标还是指针来操作数组呢?对于大部分人而言,下标的可读性会强一些。

1.1.3 数组和指针

        指针和数组并不是相等的。为了说明这个概念,请考虑下面两个声明:

int a[10];

int *b;

声明一个数组时编译器根据声明所指定的元素数量为数组分配内存空间然后再创建数组名指向这段空间的起始位置声明一个指针变量的时候编译器只为指针本身分配内存空间并不为任何整型值分配内存空间指针并未初始化指向任何现有的内存空间

因此,表达式*a是完全合法的,但是表达式*b却是非法的。*b将访问内存中一个不确定的位置将会导致程序终止。另一方面b++可以通过编译a++却不行因为a是一个常量值。

1.1.4 作为函数参数的数组名

当一个数组名作为一个参数传递给一个函数的时候发生什么情况呢?我们现在知道数组名其实就是一个指向数组第1个元素的指针,所以很明白此时传递给函数的是一份指针的拷贝。所以函数的形参实际上是一个指针。但是为了使程序员新手容易上手一些,编译器也接受数组形式的函数形参。因此下面两种函数原型是相等的:

int print_array(int *arr);

int print_array(int arr[]);

我们可以使用任何一种声明,但哪一个更准确一些呢?答案是指针。因为实参实际上是个指针,而不是数组。同样sizeof arr值是指针的长度,而不是数组的长度。现在我们清楚了,为什么一维数组中无须写明它的元素数目了,因为形参只是一个指针,并不需要为数组参数分配内存。另一方面,这种方式使得函数无法知道数组的长度。如果函数需要知道数组的长度,它必须显式传递一个长度参数给函数。

 1.2 多维数组

如果某个数组的维数不止1个,它就被称为多维数组。接下来的案例讲解以二维数组举例。

void test01(){

//二维数组初始化

int arr1[3][3] = {

{ 1, 2, 3 },

{ 4, 5, 6 },

{ 7, 8, 9 }

};

int arr2[3][3] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };

int arr3[][3] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };

//打印二维数组

for (int i = 0; i < 3; i++){

for (int j = 0; j < 3; j ++){

printf("%d ",arr1[i][j]);

}文章来源地址https://uudwc.com/A/PdgW4

printf("\n");

}

}

1.2.1 数组名 

一维数组名的值是一个指针常量,它的类型是“指向元素类型的指针”,它指向数组的第1个元素。多维数组也是同理,多维数组的数组名也是指向第一个元素,只不过第一个元素是一个数组。例如:

int arr[3][10]

可以理解为这是一个一维数组,包含了3个元素,只是每个元素恰好是包含了10个元素的数组。arr就表示指向它的第1个元素的指针,所以arr是一个指向了包含了10个整型元素的数组的指针。

1.2.2 指向数组的指针(数组指针)

  数组指针,它是指针,指向数组的指针。

数组的类型由元素类型数组大小共同决定:int array[5]  的类型为  int[5];C语言可通过typedef定义一个数组类型:

定义数组指针有一下三种方式:

//方式一
void test01(){

	//先定义数组类型,再用数组类型定义数组指针
	int arr[10] = {1,2,3,4,5,6,7,8,9,10};
	//有typedef是定义类型,没有则是定义变量,下面代码定义了一个数组类型ArrayType
	typedef int(ArrayType)[10];
	//int ArrayType[10]; //定义一个数组,数组名为ArrayType

	ArrayType myarr; //等价于 int myarr[10];
	ArrayType* pArr = &arr; //定义了一个数组指针pArr,并且指针指向数组arr
	for (int i = 0; i < 10;i++){
		printf("%d ",(*pArr)[i]);
	}
	printf("\n");
}

//方式二
void test02(){

	int arr[10];
	//定义数组指针类型
	typedef int(*ArrayType)[10];
	ArrayType pArr = &arr; //定义了一个数组指针pArr,并且指针指向数组arr
	for (int i = 0; i < 10; i++){
		(*pArr)[i] = i + 1;
	}
	for (int i = 0; i < 10; i++){
		printf("%d ", (*pArr)[i]);
	}
	printf("\n");

}

//方式三
void test03(){
	
	int arr[10];
	int(*pArr)[10] = &arr;

	for (int i = 0; i < 10; i++){
		(*pArr)[i] = i + 1;

	}
	for (int i = 0; i < 10; i++){
		printf("%d ", (*pArr)[i]);
	}
	printf("\n");
}

 1.2.3 指针数组(元素为指针)

        1.2.3.1 栈区指针数组

        

//数组做函数函数,退化为指针
void array_sort(char** arr,int len){

	for (int i = 0; i < len; i++){
		for (int j = len - 1; j > i; j --){
			//比较两个字符串
			if (strcmp(arr[j-1],arr[j]) > 0){
				char* temp = arr[j - 1];
				arr[j - 1] = arr[j];
				arr[j] = temp;
			}
		}
	}

}

//打印数组
void array_print(char** arr,int len){
	for (int i = 0; i < len;i++){
		printf("%s\n",arr[i]);
	}
	printf("----------------------\n");
}

void test(){
	
	//主调函数分配内存
	//指针数组
	char* p[] = { "bbb", "aaa", "ccc", "eee", "ddd"};
	//char** p = { "aaa", "bbb", "ccc", "ddd", "eee" }; //错误
	int len = sizeof(p) / sizeof(char*);
	//打印数组
	array_print(p, len);
	//对字符串进行排序
	array_sort(p, len);
	//打印数组
	array_print(p, len);
}

1.2.3.2 堆区指针数组

//分配内存
char** allocate_memory(int n){
	
	if (n < 0 ){
		return NULL;
	}

	char** temp = (char**)malloc(sizeof(char*) * n);
	if (temp == NULL){
		return NULL;
	}

	//分别给每一个指针malloc分配内存
	for (int i = 0; i < n; i ++){
		temp[i] = malloc(sizeof(char)* 30);
		sprintf(temp[i], "%2d_hello world!", i + 1);
	}

	return temp;
}

//打印数组
void array_print(char** arr,int len){
	for (int i = 0; i < len;i++){
		printf("%s\n",arr[i]);
	}
	printf("----------------------\n");
}

//释放内存
void free_memory(char** buf,int len){
	if (buf == NULL){
		return;
	}
	for (int i = 0; i < len; i ++){
		free(buf[i]);
		buf[i] = NULL;
	}

	free(buf);
}

void test(){
	
	int n = 10;
	char** p = allocate_memory(n);
	//打印数组
	array_print(p, n);
	//释放内存
	free_memory(p, n);
}

1.2.4二维数组三种参数形式

1.2.4.1 二维数组的线性存储特性

        

void PrintArray(int* arr, int len){
	for (int i = 0; i < len; i++){
		printf("%d ", arr[i]);
	}
	printf("\n");
}

//二维数组的线性存储
void test(){
	int arr[][3] = {
		{ 1, 2, 3 },
		{ 4, 5, 6 },
		{ 7, 8, 9 }
	};

	int arr2[][3] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
	int len = sizeof(arr2) / sizeof(int);

	//如何证明二维数组是线性的?
	//通过将数组首地址指针转成Int*类型,那么步长就变成了4,就可以遍历整个数组
	int* p = (int*)arr;
	for (int i = 0; i < len; i++){
		printf("%d ", p[i]);
	}
	printf("\n");

	PrintArray((int*)arr, len);
	PrintArray((int*)arr2, len);
}

1.2.4.2 二维数组的3种形式参数

        

//二维数组的第一种形式
void PrintArray01(int arr[3][3]){
	for (int i = 0; i < 3; i++){
		for (int j = 0; j < 3; j++){
			printf("arr[%d][%d]:%d\n", i, j, arr[i][j]);
		}
	}
}

//二维数组的第二种形式
void PrintArray02(int arr[][3]){
	for (int i = 0; i < 3; i++){
		for (int j = 0; j < 3; j++){
			printf("arr[%d][%d]:%d\n", i, j, arr[i][j]);
		}
	}
}

//二维数组的第二种形式
void PrintArray03(int(*arr)[3]){
	for (int i = 0; i < 3; i++){
		for (int j = 0; j < 3; j++){
			printf("arr[%d][%d]:%d\n", i, j, arr[i][j]);
		}
	}
}

void test(){
	
	int arr[][3] = { 
		{ 1, 2, 3 },
		{ 4, 5, 6 },
		{ 7, 8, 9 }
	};
	
	PrintArray01(arr);
	PrintArray02(arr);
	PrintArray03(arr);
}

1.3总结

1.3.1 编程提示

  1. 源代码的可读性几乎总是比程序的运行时效率更为重要
  2. 只要有可能,函数的指针形参都应该声明为const
  3. 在多维数组的初始值列表中使用完整的多层花括号提高可读性

1.3.2 内容总结

在绝大多数表达式中,数组名的值是指向数组第1个元素的指针。这个规则只有两个例外,sizeof和对数组名&。

指针和数组并不相等。当我们声明一个数组的时候,同时也分配了内存。但是声明指针的时候,只分配容纳指针本身的空间。

当数组名作为函数参数时,实际传递给函数的是一个指向数组第1个元素的指针。

我们不单可以创建指向普通变量的指针,也可创建指向数组的指针。

2.结构体

        

2.1 结构体基础知识

2.1.1 结构体类型的定义

struct Person{

char name[64];

int age;

};

typedef struct _PERSON{

char name[64];

int age;

}Person;

注意:定义结构体类型时不要直接给成员赋值,结构体只是一个类型,编译器还没有为其分配空间,只有根据其类型定义变量时,才分配空间,有空间后才能赋值。

2.1.2 结构体变量的定义

struct Person{

char name[64];

int age;

}p1; //定义类型同时定义变量

struct{

char name[64];

int age;

}p2; //定义类型同时定义变量

struct Person p3; //通过类型直接定义

2.1.3 结构体变量的初始化

struct Person{

char name[64];

int age;

}p1 = {"john",10}; //定义类型同时初始化变量

struct{

char name[64];

int age;

}p2 = {"Obama",30}; //定义类型同时初始化变量

struct Person p3 = {"Edward",33}; //通过类型直接定义

2.1.4 结构体成员的使用

struct Person{

char name[64];

int age;

};

void test(){

//在栈上分配空间

struct Person p1;

strcpy(p1.name, "John");

p1.age = 30;

//如果是普通变量,通过点运算符操作结构体成员

printf("Name:%s Age:%d\n", p1.name, p1.age);

//在堆上分配空间

struct Person* p2 = (struct Person*)malloc(sizeof(struct Person));

strcpy(p2->name, "Obama");

p2->age = 33;

//如果是指针变量,通过->操作结构体成员

printf("Name:%s Age:%d\n", p2->name, p2->age);

}

2.1.5结构体赋值

2.1.5.1 赋值基本概念

相同的两个结构体变量可以相互赋值,把一个结构体变量的值拷贝给另一个结构体,这两个变量还是两个独立的变量。

struct Person{

char name[64];

int age;

};

void test(){

//在栈上分配空间

struct Person p1 = { "John" , 30};

struct Person p2 = { "Obama", 33 };

printf("Name:%s Age:%d\n", p1.name, p1.age);

printf("Name:%s Age:%d\n", p2.name, p2.age);

//将p2的值赋值给p1

p1 = p2;

printf("Name:%s Age:%d\n", p1.name, p1.age);

printf("Name:%s Age:%d\n", p2.name, p2.age);

}

2.1.5.2 深拷贝和浅拷贝

//一个老师有N个学生

typedef struct _TEACHER{

char* name;

}Teacher;

void test(){

Teacher t1;

t1.name = malloc(64);

strcpy(t1.name , "John");

Teacher t2;

t2 = t1;

//对手动开辟的内存,需要手动拷贝

t2.name = malloc(64);

strcpy(t2.name, t1.name);

if (t1.name != NULL){

free(t1.name);

t1.name = NULL;

}

if (t2.name != NULL){

free(t2.name);

t1.name = NULL;

}

}

2.1.6 结构体数组

struct Person{

char name[64];

int age;

};

void test(){

//在栈上分配空间

struct Person p1[3] = {

{ "John", 30 },

{ "Obama", 33 },

{ "Edward", 25}

};

struct Person p2[3] = { "John", 30, "Obama", 33, "Edward", 25 };

for (int i = 0; i < 3;i ++){

printf("Name:%s Age:%d\n",p1[i].name,p1[i].age);

}

printf("-----------------\n");

for (int i = 0; i < 3; i++){

printf("Name:%s Age:%d\n", p2[i].name, p2[i].age);

}

printf("-----------------\n");

//在堆上分配结构体数组

struct Person* p3 = (struct Person*)malloc(sizeof(struct Person) * 3);

for (int i = 0; i < 3;i++){

sprintf(p3[i].name, "Name_%d", i + 1);

p3[i].age = 20 + i;

}

for (int i = 0; i < 3; i++){

printf("Name:%s Age:%d\n", p3[i].name, p3[i].age);

}

}

2.2 结构体嵌套指针

2.2.1 结构体嵌套一级指针

struct Person{

char* name;

int age;

};

void allocate_memory(struct Person** person){

if (person == NULL){

return;

}

struct Person* temp = (struct Person*)malloc(sizeof(struct Person));

if (temp == NULL){

return;

}

//给name指针分配内存

temp->name = (char*)malloc(sizeof(char)* 64);

strcpy(temp->name, "John");

temp->age = 100;

*person = temp;

}

void print_person(struct Person* person){

printf("Name:%s Age:%d\n",person->name,person->age);

}

void free_memory(struct Person** person){

if (person == NULL){

return;

}

struct Person* temp = *person;

if (temp->name != NULL){

free(temp->name);

temp->name = NULL;

}

free(temp);

}

void test(){

struct Person* p = NULL;

allocate_memory(&p);

print_person(p);

free_memory(&p);

}

2.2.2 结构体嵌套二级指针

//一个老师有N个学生

typedef struct _TEACHER{

char name[64];

char** students;

}Teacher;

void create_teacher(Teacher** teacher,int n,int m){

if (teacher == NULL){

return;

}

//创建老师数组

Teacher* teachers = (Teacher*)malloc(sizeof(Teacher)* n);

if (teachers == NULL){

return;

}

//给每一个老师分配学生

int num = 0;

for (int i = 0; i < n; i ++){

sprintf(teachers[i].name, "老师_%d", i + 1);

teachers[i].students = (char**)malloc(sizeof(char*) * m);

for (int j = 0; j < m;j++){

teachers[i].students[j] = malloc(64);

sprintf(teachers[i].students[j], "学生_%d", num + 1);

num++;

}

}

*teacher = teachers;

}

void print_teacher(Teacher* teacher,int n,int m){

for (int i = 0; i < n; i ++){

printf("%s:\n", teacher[i].name);

for (int j = 0; j < m;j++){

printf("  %s",teacher[i].students[j]);

}

printf("\n");

}

}

void free_memory(Teacher** teacher,int n,int m){

if (teacher == NULL){

return;

}

Teacher* temp = *teacher;

for (int i = 0; i < n; i ++){

for (int j = 0; j < m;j ++){

free(temp[i].students[j]);

temp[i].students[j] = NULL;

}

free(temp[i].students);

temp[i].students = NULL;

}

free(temp);

}

void test(){

Teacher* p = NULL;

create_teacher(&p,2,3);

print_teacher(p, 2, 3);

free_memory(&p,2,3);

}

2.3 结构体成员偏移量

//一旦结构体定义下来,则结构体中的成员内存布局就定下了

#include <stddef.h>

struct Teacher

{

char a;

int b;

};

void test01(){

struct Teacher  t1;

struct Teacher*p = &t1;

int offsize1 = (int)&(p->b) - (int)p;  //成员b 相对于结构体 Teacher的偏移量

int offsize2 = offsetof(struct Teacher, b);

printf("offsize1:%d \n", offsize1); //打印b属性对于首地址的偏移量

printf("offsize2:%d \n", offsize2);

}

2.4 结构体字节对齐

在用sizeof运算符求算某结构体所占空间时,并不是简单地将结构体中所有元素各自占的空间相加,这里涉及到内存字节对齐的问题。

从理论上讲,对于任何变量的访问都可以从任何地址开始访问,但是事实上不是如此,实际上访问特定类型的变量只能在特定的地址访问,这就需要各个变量在空间上按一定的规则排列, 而不是简单地顺序排列,这就是内存对齐

2.4.1 内存对齐

2.4.1.1 内存对齐原因

我们知道内存的最小单元是一个字节,当cpu从内存中读取数据的时候,是一个一个字节读取,所以内存对我们应该是入下图这样:

但是实际上cpu将内存当成多个块,每次从内存中读取一个块,这个块的大小可能是2、4、8、16等,

那么下面,我们来分析下非内存对齐和内存对齐的优缺点在哪?

内存对齐是操作系统为了提高访问内存的策略。操作系统在访问内存的时候,每次读取一定长度(这个长度是操作系统默认的对齐数,或者默认对齐数的整数倍)。如果没有对齐,为了访问一个变量可能产生二次访问。

至此大家应该能够简单明白,为什么要简单内存对齐?

  1. 提高存取数据的速度。比如有的平台每次都是从偶地址处读取数据,对于一个int型的变量,若从偶地址单元处存放,则只需一个读取周期即可读取该变量;但是若从奇地址单元处存放,则需要2个读取周期读取该变量。
  2. 某些平台只能在特定的地址处访问特定类型的数据,否则抛出硬件异常给操作系统。

2.4.1.2如何内存对齐

  1. 对于标准数据类型,它的地址只要是它的长度的整数倍。
  2. 对于非标准数据类型,比如结构体,要遵循一下对齐原则:

1. 数组成员对齐规则。第一个数组成员应该放在offset为0的地方,以后每个数组成员应该放在offset为min(当前成员的大小,#pargama pack(n))整数倍的地方开始(比如int在32位机器为4字节,#pargama pack(2),那么从2的倍数地方开始存储)。

2. 结构体总的大小,也就是sizeof的结果,必须是min(结构体内部最大成员,#pargama pack(n))的整数倍,不足要补齐。

3. 结构体做为成员的对齐规则。如果一个结构体B里嵌套另一个结构体A,还是以最大成员类型的大小对齐,但是结构体A的起点为A内部最大成员的整数倍的地方。(struct B里存有struct A,A里有char,int,double等成员,那A应该从8的整数倍开始存储。),结构体A中的成员的对齐规则仍满足原则1、原则2。

手动设置对齐模数:

    1. #pragma pack(show)

显示当前packing alignment的字节数,以warning message的形式被显示。

    1. #pragma pack(push)

将当前指定的packing alignment数组进行压栈操作,这里的栈是the internal compiler stack,同事设置当前的packing alignment为n;如果n没有指定,则将当前的packing alignment数组压栈。

    1. #pragma pack(pop)

从internal compiler stack中删除最顶端的reaord; 如果没有指定n,则当前栈顶record即为新的packing alignement数值;如果指定了n,则n成为新的packing alignment值

    1. #pragma pack(n)

指定packing的数值,以字节为单位,缺省数值是8,合法的数值分别是1,2,4,8,16。

2.4.2 内存对齐案例

#pragma pack(4)

typedef struct _STUDENT{

int a;

char b;

double c;

float d;

}Student;

typedef struct _STUDENT2{

char a;

Student b; 

double c;

}Student2;

void test01(){

//Student

//a从偏移量0位置开始存储

//b从4位置开始存储

//c从8位置开始存储

//d从12位置开存储

//所以Student内部对齐之后的大小为20 ,整体对齐,整体为最大类型的整数倍 也就是8的整数倍 为24

printf("sizeof Student:%d\n",sizeof(Student));

//Student2

//a从偏移量为0位置开始

//b从偏移量为Student内部最大成员整数倍开始,也就是8开始

//c从8的整数倍地方开始,也就是32开始

//所以结构体Sutdnet2内部对齐之后的大小为:40 , 由于结构体中最大成员为8,必须为8的整数倍 所以大小为40

printf("sizeof Student2:%d\n", sizeof(Student2));

}

3.文件操作

文件在今天的计算机系统中作用是很重要的。文件用来存放程序、文档、数据、表格、图片和其他很多种类的信息。作为一名程序员,您必须编程来创建、写入和读取文件。编写程序从文件读取信息或者将结果写入文件是一种经常性的需求。C提供了强大的和文件进行通信的方法。使用这种方法我们可以在程序中打开文件,然后使用专门的I/O函数读取文件或者写入文件。

3.1文件相关概念

3.1.1 文件的概念

一个文件通常就是磁盘上一段命名的存储区。但是对于操作系统来说,文件就会更复杂一些。例如,一个大文件可以存储在一些分散的区段中,或者还会包含一些操作系统可以确定其文件类型的附加数据,但是这些是操作系统,而不是我们程序员所要关心的事情。我们应该考虑如何在C程序中处理文件。

3.1.2 流的概念

流是一个动态的概念,可以将一个字节形象地比喻成一滴水,字节在设备、文件和程序之间的传输就是流,类似于水在管道中的传输,可以看出,流是对输入输出源的一种抽象,也是对传输信息的一种抽象。

C语言中,I/O操作可以简单地看作是从程序移进或移出字节,这种搬运的过程便称为流(stream)。程序只需要关心是否正确地输出了字节数据,以及是否正确地输入了要读取字节数据,特定I/O设备的细节对程序员是隐藏的。

3.1.2.1 文本流

文本流,也就是我们常说的以文本模式读取文件。文本流的有些特性在不同的系统中可能不同。其中之一就是文本行的最大长度。标准规定至少允许254个字符。另一个可能不同的特性是文本行的结束方式。例如在Windows系统中,文本文件约定以一个回车符和一个换行符结尾。但是在Linux下只使用一个换行符结尾。

标准C把文本定义为零个或者多个字符,后面跟一个表示结束的换行符(\n).对于那些文本行的外在表现形式与这个定义不同的系统上,库函数负责外部形式和内部形式之间的翻译。例如,在Windows系统中,在输出时,文本的换行符被写成一对回车/换行符。在输入时,文本中的回车符被丢弃。这种不必考虑文本的外部形势而操纵文本的能力简化了可移植程序的创建。

3.1.2.1 二进制流

二进制流中的字节将完全根据程序编写它们的形式写入到文件中,而且完全根据它们从文件或设备读取的形式读入到程序中。它们并未做任何改变。这种类型的流适用于非文本数据,但是如果你不希望I/O函数修改文本文件的行末字符,也可以把它们用于文本文件。

c语言在处理这两种文件的时候并不区分,都看成是字符流,按字节进行处理。

我们程序中,经常看到的文本方式打开文件和二进制方式打开文件仅仅体现在换行符的处理上。

比如说,在widows下,文件的换行符是\r\n,而在Linux下换行符则是\n.

当对文件使用文本方式打开的时候,读写的windows文件中的换行符\r\n会被替换成\n读到内存中,当在windows下写入文件的时候,\n被替换成\r\n再写入文件。如果使用二进制方式打开文件,则不进行\r\n和\n之间的转换。 那么由于Linux下的换行符就是\n,所以文本文件方式和二进制方式无区别。

3.2 文件的操作

3.2.1 文件流总览

标准库函数是的我们在C程序中执行与文件相关的I/O任务非常方便。下面是关于文件I/O的一般概况。

  1. 程序为同时处于活动状态的每个文件声明一个指针变量,其类型为FILE*。这个指针指向这个FILE结构,当它处于活动状态时由流使用。
  2. 流通过fopen函数打开。为了打开一个流,我们必须指定需要访问的文件或设备以及他们的访问方式(读、写、或者读写)。Fopen和操作系统验证文件或者设备是否存在并初始化FILE。
  3. 根据需要对文件进行读写操作。
  4. 最后调用fclose函数关闭流。关闭一个流可以防止与它相关的文件被再次访问,保证任何存储于缓冲区中的数据被正确写入到文件中,并且释放FILE结构。

标准I/O更为简单,因为它们并不需要打开或者关闭。

I/O函数以三种基本的形式处理数据:单个字符文本行二进制数据。对于每种形式都有一组特定的函数对它们进行处理。

输入/输出函数家族

家族名

目的

可用于所有流

只用于stdin和stdout

getchar

字符输入

fgetc、getc

getchar

putchar

字符输出

fputc、putc

putchar

gets

文本行输入

fgets

gets

puts

文本行输出

fputs

puts

scanf

格式化输入

fscanf

scanf

printf

格式化输出

fprintf

printf

3.2.2 文件指针

我们知道,文件是由操作系统管理的单元。当我们想操作一个文件的时候,让操作系统帮我们打开文件,操作系统把我们指定要打开文件的信息保存起来,并且返回给我们一个指针指向文件的信息。文件指针也可以理解为代指打开的文件。这个指针的类型为FILE类型。该类型定义在stdio.h头文件中。通过文件指针,我们就可以对文件进行各种操作。

对于每一个ANSI C程序,运行时系统必须提供至少三个流-标准输入(stdin)、标准输出(stdout)、标准错误(stderr),它们都是一个指向FILE结构的指针。标准输入是缺省情况下的输入来源,标准输出时缺省情况下的输出设置。具体缺省值因编译器而异,通常标准输入为键盘设备、标准输出为终端或者屏幕。

ANSI C并未规定FILE的成员,不同编译器可能有不同的定义。VS下FILE信息如下:

struct _iobuf { 

        char  *_ptr;         //文件输入的下一个位置

        int   _cnt;          //剩余多少字符未被读取

        char  *_base;        //指基础位置(应该是文件的其始位置)

        int   _flag;         //文件标志

        int   _file;         //文件的有效性验证

        int   _charbuf;      //检查缓冲区状况,如果无缓冲区则不读取

        int   _bufsiz;       //文件的大小

        char  *_tmpfname;    //临时文件名

}; 

typedef struct _iobuf FILE;

3.2.3 文件缓冲区

  1. 文件缓冲区

ANSI C标准采用“缓冲文件系统”处理数据文件 所谓缓冲文件系统是指系统自动地在内存区为程序中每一个正在使用的文件开辟一个文件缓冲区从内存向磁盘输出数据必须先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘去 如果从磁盘向计算机读入数据,则一次从磁盘文件将一批数据输入到内存缓冲区(充满缓冲 区),然后再从缓冲区逐个地将数据送到程序数据区(给程序变量) 。

那么文件缓冲区有什么作用呢?

如我们从磁盘里取信息,我们先把读出的数据放在缓冲区,计算机再直接从缓冲区中取数据,等缓冲区的数据取完后再去磁盘中读取,这样就可以减少磁盘的读写次数,再加上计算机对缓冲区的操作大大快于对磁盘的操作,故应用缓冲区可大大提高计算机的运行速度。

3.2.4 文件打开关闭

3.2.4.1 文件打开(fopen)

文件的打开操作表示将给用户指定的文件在内存分配一个FILE结构区,并将该结构的指针返回给用户程序,以后用户程序就可用此FILE指针来实现对指定文件的存取操作了。当使用打开函数时,必须给出文件名、文件操作方式(读、写或读写)。

FILE * fopen(const char * filename, const char * mode);

功能:打开文件

参数:

filename:需要打开的文件名,根据需要加上路径

mode:打开文件的权限设置

返回值:

成功:文件指针

失败:NULL

方式

含义

“r”

打开,只读,文件必须已经存在。

“w”

只写,如果文件不存在则创建,如果文件已存在则把文件长度截断(Truncate)0字节。再重新写,也就是替换掉原来的文件内容文件指针指到头。

“a”

只能在文件末尾追加数据,如果文件不存在则创建

“rb”

打开一个二进制文件,只读

“wb”

打开一个二进制文件,只写

“ab"

打开一个二进制文件,追加

“r+”

允许读和写,文件必须已存在

“w+”

允许读和写,如果文件不存在则创建,如果文件已存在则把文件长度截断为0字节再重新写 。

“a+”

允许读和追加数据,如果文件不存在则创建

“rb+”

以读/写方式打开一个二进制文件

“wb+”

以读/写方式建立一个新的二进制文件

“ab+”

以读/写方式打开一个二进制文件进行追加

示例代码:

void test(){

FILE *fp = NULL;

// "\\"这样的路径形式,只能在windows使用

// "/"这样的路径形式,windows和linux平台下都可用,建议使用这种

// 路径可以是相对路径,也可是绝对路径

fp = fopen("../test", "w");

//fp = fopen("..\\test", "w");

if (fp == NULL) //返回空,说明打开失败

{

//perror()是标准出错打印函数,能打印调用库函数出错原因

perror("open");

return -1;

}

}

应该检查fopen的返回值!如何函数失败,它会返回一个NULL值。如果程序不检查错误,这个NULL指针就会传给后续的I/O函数。它们将对这个指针执行间接访问,并将失败.

3.2.4.2 文件关闭(fclose)

文件操作完成后,如果程序没有结束,必须要用fclose()函数进行关闭,这是因为对打开的文件进行写入时,若文件缓冲区的空间未被写入的内容填满,这些内容不会写到打开的文件中。只有对打开的文件进行关闭操作时,停留在文件缓冲区的内容才能写到该文件中去,从而使文件完整。再者一旦关闭了文件,该文件对应的FILE结构将被释放,从而使关闭的文件得到保护,因为这时对该文件的存取操作将不会进行。文件的关闭也意味着释放了该文件的缓冲区。

int fclose(FILE * stream);

功能:关闭先前fopen()打开的文件。此动作让缓冲区的数据写入文件中,并释放系统所提供的文件资源。

参数:

stream:文件指针

返回值:

成功:0

失败:-1

它表示该函数将关闭FILE指针对应的文件,并返回一个整数值。若成功地关闭了文件,则返回一个0值,否则返回一个非0值.

3.2.4 文件读写函数回顾

  1. 按照字符读写文件:fgetc(), fputc()
  2. 按照行读写文件:fputs(), fgets()
  3. 按照块读写文件:fread(), fwirte()
  4. 按照格式化读写文件:fprintf(), fscanf()
  5. 按照随机位置读写文件:fseek(), ftell(), rewind()

3.2.4.1 字符读写函数回顾

int fputc(int ch, FILE * stream);

功能:将ch转换为unsigned char后写入stream指定的文件中

参数:

ch:需要写入文件的字符

stream:文件指针

返回值:

成功:成功写入文件的字符

失败:返回-1

int fgetc(FILE * stream);

功能:从stream指定的文件中读取一个字符

参数:

stream:文件指针

返回值:

成功:返回读取到的字符

失败:-1

int feof(FILE * stream);

功能:检测是否读取到了文件结尾

参数:

stream:文件指针

返回值:

非0值:已经到文件结尾

0:没有到文件结尾

void test(){

//写文件

FILE* fp_write= NULL;

//写方式打开文件

fp_write = fopen("./mydata.txt", "w+");

if (fp_write == NULL){

return;

}

char buf[] = "this is a test for pfutc!";

for (int i = 0; i < strlen(buf);i++){

fputc(buf[i], fp_write);

}

fclose(fp_write);

//读文件

FILE* fp_read = NULL;

fp_read = fopen("./mydata.txt", "r");

if (fp_read == NULL){

return;

}

#if 0

//判断文件结尾 注意:多输出一个空格

while (!feof(fp_read)){

printf("%c",fgetc(fp_read));

}

#else

char ch;

while ((ch = fgetc(fp_read)) != EOF){

printf("%c", ch);

}

#endif

}

将把流指针fp指向的文件中的一个字符读出,并赋给ch,当执行fgetc()函数时,若当时文件指针指到文件尾,即遇到文件结束标志EOF(其对应值为-1),该函数返回一个 -1 给ch,在程序中常用检查该函数返回值是否为 -1 来判断是否已读到文件尾,从而决定是否继续。

3.2.4.2 行读写函数回顾

int fputs(const char * str, FILE * stream);

功能:将str所指定的字符串写入到stream指定的文件中, 字符串结束符 '\0'  不写入文件。

参数:

str:字符串

stream:文件指针

返回值:

成功:0

失败:-1

char * fgets(char * str, int size, FILE * stream);

功能:从stream指定的文件内读入字符,保存到str所指定的内存空间,直到出现换行字符、读到文件结尾或是已读了size - 1个字符为止,最后会自动加上字符 '\0' 作为字符串结束。

参数:

str:字符串

size:指定最大读取字符串的长度(size - 1)

stream:文件指针

返回值:

成功:成功读取的字符串

读到文件尾或出错: NULL

void test(){

//写文件

FILE* fp_write= NULL;

//写方式打开文件

fp_write = fopen("./mydata.txt", "w+");

if (fp_write == NULL){

perror("fopen:");

return;

}

char* buf[] = {

"01 this is a test for pfutc!\n",

"02 this is a test for pfutc!\n",

"03 this is a test for pfutc!\n",

"04 this is a test for pfutc!\n",

};

for (int i = 0; i < 4; i ++){

fputs(buf[i], fp_write);

}

fclose(fp_write);

//读文件

FILE* fp_read = NULL;

fp_read = fopen("./mydata.txt", "r");

if (fp_read == NULL){

perror("fopen:");

return;

}

//判断文件结尾

while (!feof(fp_read)){

char temp[1024] = { 0 };

fgets(temp, 1024, fp_read);

printf("%s",temp);

}

fclose(fp_read);

}

3.2.4.3 块读写函数回顾

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

功能:以数据块的方式给文件写入内容

参数:

ptr:准备写入文件数据的地址

size: size_t 为 unsigned int类型,此参数指定写入文件内容的块数据大小

nmemb:写入文件的块数,写入文件数据总大小为:size * nmemb

stream:已经打开的文件指针

返回值:

成功:实际成功写入文件数据的块数,此值和nmemb相等

失败:0

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

功能:以数据块的方式从文件中读取内容

参数:

ptr:存放读取出来数据的内存空间

size: size_t 为 unsigned int类型,此参数指定读取文件内容的块数据大小

nmemb:读取文件的块数,读取文件数据总大小为:size * nmemb

stream:已经打开的文件指针

返回值:

成功:实际成功读取到内容的块数,如果此值比nmemb小,但大于0,说明读到文件的结尾。

失败:0

typedef struct _TEACHER{

char name[64];

int age;

}Teacher;

void test(){

//写文件

FILE* fp_write= NULL;

//写方式打开文件

fp_write = fopen("./mydata.txt", "wb");

if (fp_write == NULL){

perror("fopen:");

return;

}

Teacher teachers[4] = {

{ "Obama", 33 },

{ "John", 28 },

{ "Edward", 45},

{ "Smith", 35}

};

for (int i = 0; i < 4; i ++){

fwrite(&teachers[i],sizeof(Teacher),1, fp_write);

}

//关闭文件

fclose(fp_write);

//读文件

FILE* fp_read = NULL;

fp_read = fopen("./mydata.txt", "rb");

if (fp_read == NULL){

perror("fopen:");

return;

}

Teacher temps[4];

fread(&temps, sizeof(Teacher), 4, fp_read);

for (int i = 0; i < 4;i++){

printf("Name:%s Age:%d\n",temps[i].name,temps[i].age);

}

fclose(fp_read);

}

3.2.4.4 格式化读写函数回顾

int fprintf(FILE * stream, const char * format, ...);

功能:根据参数format字符串来转换并格式化数据,然后将结果输出到stream指定的文件中,指定出现字符串结束符 '\0'  为止。

参数:

stream:已经打开的文件

format:字符串格式,用法和printf()一样

返回值:

成功:实际写入文件的字符个数

失败:-1

int fscanf(FILE * stream, const char * format, ...);

功能:从stream指定的文件读取字符串,并根据参数format字符串来转换并格式化数据。

参数:

stream:已经打开的文件

format:字符串格式,用法和scanf()一样

返回值:

成功:实际从文件中读取的字符个数

失败: - 1

注意fscanf遇到空格和换行时结束。

void test(){

//写文件

FILE* fp_write= NULL;

//写方式打开文件

fp_write = fopen("./mydata.txt", "w");

if (fp_write == NULL){

perror("fopen:");

return;

}

fprintf(fp_write,"hello world:%d!",10);

//关闭文件

fclose(fp_write);

//读文件

FILE* fp_read = NULL;

fp_read = fopen("./mydata.txt", "rb");

if (fp_read == NULL){

perror("fopen:");

return;

}

char temps[1024] = { 0 };

while (!feof(fp_read)){

fscanf(fp_read, "%s", temps);

printf("%s", temps);

}

fclose(fp_read);

}

3.2.5.5 随机读写函数回顾

int fseek(FILE *stream, long offset, int whence);

功能:移动文件流(文件光标)的读写位置。

参数:

stream:已经打开的文件指针

offset:根据whence来移动的位移数(偏移量),可以是正数,也可以负数,如果正数,则相对于whence往右移动,如果是负数,则相对于whence往左移动。如果向前移动的字节数超过了文件开头则出错返回,如果向后移动的字节数超过了 文件末尾,再次写入时将增大文件尺寸。

whence:其取值如下:

SEEK_SET:从文件开头移动offset个字节

SEEK_CUR:从当前位置移动offset个字节

SEEK_END:从文件末尾移动offset个字节

返回值:

成功:0

失败:-1

long ftell(FILE *stream);

功能:获取文件流(文件光标)的读写位置。

参数:

stream:已经打开的文件指针

返回值:

成功:当前文件流(文件光标)的读写位置

失败:-1

void rewind(FILE *stream);

功能:把文件流(文件光标)的读写位置移动到文件开头。

参数:

stream:已经打开的文件指针

返回值:

无返回值

typedef struct _TEACHER{

char name[64];

int age;

}Teacher;

void test(){

//写文件

FILE* fp_write = NULL;

//写方式打开文件

fp_write = fopen("./mydata.txt", "wb");

if (fp_write == NULL){

perror("fopen:");

return;

}

Teacher teachers[4] = {

{ "Obama", 33 },

{ "John", 28 },

{ "Edward", 45 },

{ "Smith", 35 }

};

for (int i = 0; i < 4; i++){

fwrite(&teachers[i], sizeof(Teacher), 1, fp_write);

}

//关闭文件

fclose(fp_write);

//读文件

FILE* fp_read = NULL;

fp_read = fopen("./mydata.txt", "rb");

if (fp_read == NULL){

perror("fopen:");

return;

}

Teacher temp;

//读取第三个数组

fseek(fp_read , sizeof(Teacher) * 2 , SEEK_SET);

fread(&temp, sizeof(Teacher), 1, fp_read);

printf("Name:%s Age:%d\n",temp.name,temp.age);

memset(&temp,0,sizeof(Teacher));

fseek(fp_read, -(int)sizeof(Teacher), SEEK_END);

fread(&temp, sizeof(Teacher), 1, fp_read);

printf("Name:%s Age:%d\n", temp.name, temp.age);

rewind(fp_read);

fread(&temp, sizeof(Teacher), 1, fp_read);

printf("Name:%s Age:%d\n", temp.name, temp.age);

fclose(fp_read);

}

原文地址:https://blog.csdn.net/cat_fish_rain/article/details/132087716

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请联系站长进行投诉反馈,一经查实,立即删除!

h
上一篇 2023年08月05日 12:17
pycharm运行pytest无法实时输出信息
下一篇 2023年08月05日 12:17