C语言总结:结构体,联合体,文件操作及课程管理系统项目中遇到的部分问题及解决方案

结构体

结构体定义:

结构体(struct)是一种复合数据类型,允许将多个不同类型的变量组合成一个单一的变量名。这个变量名称为结构体变量,可以包含多个成员(或称为字段),每个成员可以是不同的数据类型。

理解:

包含多个成员的集合(接口)

结构体创建

typedef struct 结构体名 {  
    数据类型 成员变量1;  
    数据类型 成员变量2;  
    // ...  
    数据类型 成员变量N;  
};

或者

typedef struct {  
    数据类型 成员变量1;  
    数据类型 成员变量2;  
    // ...  
    数据类型 成员变量N;  
} 结构体名;

示例:    char courseId[10];
    char courseName[20];
    char teacherName[20];
    char courseTime[20];
    char courseClassroom[20];
}Course;

指向结构体的指针与指向整数指针的区别

List* list; 和 int *list = &List; 

  1. List* list;
    这行代码声明了一个名为 list 的指针变量,该指针变量指向 List 类型的对象(假设 List 是一个已经定义好的结构体类型或其他类型)。在声明之后,list 指针并没有被初始化,它并不指向任何有效的内存位置。在使用它之前,你需要将它指向某个有效的 List 类型的对象或为其分配内存(例如使用 malloc 或 calloc)。

  2. int *list = &List;

    • 假设 List 是一个变量名,那么它必须是一个 int 类型的变量,因为 &List 会取 List 的地址,这个地址的类型是 int *(指向整数的指针)。但是,通常我们不会使用结构体类型的名称(如 List)作为变量名,因为这可能会导致混淆。
    • 如果 List 真的是一个 int 类型的变量,那么 int *list = &List; 是合法的,它将 list 指针初始化为指向 List 变量的地址。
    • 如果 List 是一个结构体类型(如前面假设的),那么 int *list = &List; 是不合法的,因为 &List 的类型将是 List * 而不是 int *
  • List* list; 声明了一个指向 List 类型的指针变量 list,但没有初始化它。
  • int *list = &List; 假设 List 是一个 int 类型的变量,则它声明了一个指向整数的指针变量 list,并将其初始化为指向 List 的地址。如果 List 不是 int 类型的变量,这行代码将不会编译通过。
  • 对于指向整数的指针,可以直接对其进行算术运算(如递增、递减)以访问数组中的连续整数。
  • 对于指向结构体的指针,虽然也可以进行算术运算,但结果通常不那么直观,因为结构体的大小可能不是字节的整数倍。然而,你可以通过指针访问结构体的成员。
  • 对于指向整数的指针,你不能直接访问其“成员”,因为整数没有成员。
  • 对于指向结构体的指针,你可以使用 -> 运算符来访问结构体的成员,如 ptr->memberName

联合体

联合体定义:

在C语言中,联合体(Union)是一种特殊的数据结构,它允许在相同的内存位置存储不同的数据类型。但是,在任何时候,只有联合体的一个成员是有效的,因为它们是共享同一块内存空间的。

理解:

共享内存的n哥同根成员

联合体对于不同数据类型成员共享内存的实现:
  1. 内存分配:当你声明一个联合体变量时,编译器会为它分配足够的内存以存储其最大的成员。这个内存块是联合体所有成员共享的。
  2. 写入值:当你给联合体的一个成员赋值时,你实际上是在写入这块共享内存。例如,如果你给一个int类型的成员赋值,那么这块内存的前四个字节(在大多数系统上)将被设置为该整数的二进制表示。
  3. 读取值:当你读取联合体的一个成员时,你实际上是在读取这块共享内存,并根据该成员的数据类型进行解释。如果你读取的是与之前写入的类型不同的成员,那么你将得到一个基于原始数据内存表示的不同类型解释。
  4. 不安全:由于联合体允许在相同的内存位置存储不同类型的数据,并且没有类型检查,因此使用联合体时需要格外小心。在读取联合体成员之前,你应该清楚地知道哪个成员是有效的,并且最近一次被写入的是哪个成员。

基础文件操作

只创建copied文件,文件中写入300 400并执行以下代码:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
int main() 
{
    int a, b;

char arr[20];

    FILE* fp = fopen("copied.txt", "r");

//注:若写的文件后缀存在txt则读取失败,原因在于系统自动给文件加上了txt后缀
    if (fp == NULL) 
    { printf("open failed\n"); exit(-1);

//exit括号中数字在出错时可直接输出,在不同程序中使用不同返回值有助于定位出错位置
    }
    fscanf(fp, "%d %d %s", &a, &b,arr);//数组出现默认指向首地址,不需要取地址符
    printf("a=%d,b=%d,arr=%s", a, b,arr);//非编译器文件格式可能arr会乱码
    fclose(fp);
    return 0;
}

注:若文件中出现字符串应将其另存为编译器默认格式文件,否则可能出现乱码

结合结构体:

typedef struct
{
    char id[10];
    char name[20];
    char gender[5];
} Student;
int main()
{
    Student stu[3];
    FILE*fp = fopen("stu inf.txt", "r");
    if (fp == NULL)
    {
        printf("open failed\n");
        exit(-2);
    }
    //feof(fp)判断是否到文件结尾,非结尾返回0,结尾返回1
    int i = 0;
    while(feof(fp) == 0)
    {
        fscanf(fp,"%[^,],%[^,],%[^\n],\n", stu[i].id, stu[i].name, stu[i].gender);

//常规%s识别\n会读一整行
        printf("%s--%s--%s\n", stu[i].id, stu[i].name, stu[i].gender);
        i++;
    }
    fclose(fp);
    return 0;
}

教务管理系统中部分问题思考:

添加课程函数实现:

void addcourse()
{
	printf("type in the course you wanna add\n");
	int i = find();//获取find函数返回值(-1为已经存在)
	if (i != -1)
	{
		printf("course exist\n");
		return ;
	}
	else
	{
		int j = list->cursize;//由于cursize-1才是真正的课程数,所以cursize实际为课程数+1
		int n = 0;
		int fun;
		do//do while反复更新所要加的课程内容
		{
		printf("type in function\n");
		printf("1:course id***2:course name***3:teacher name***4:course time***5:course classroom***6:done\n");
			scanf("%d", &fun);
			int c;
			while ((c = getchar()) != '\n' && c != EOF) {
				// 丢弃字符,直到遇到换行符或EOF  (使用getchar直接读走,这一步可以被广泛运运用)
			}
			switch (fun)
			{
			case 1:
				printf("type in new id\n");
				for (char mid = getchar(); mid != '\n';mid=getchar(), n++) //!!!for函数中(for(1;2;3)1只在最开始进行判定,且一与二都不能更新内容,所以最好在3处执行操作)
				{
				list->courses[j].courseid[n]=mid;
				}
				list->courses[j].courseid[n] = '\0';
				n = 0;
				break;//使用getchar()一位一位读取(别问为啥,练习(闲的了))
			case 2:
				printf("type in new name\n");
				for (char name = getchar(); name != '\n';name=getchar(),n++)
				{
					list->courses[j].coursename[n]=name;
				}
				list->courses[j].coursename[n] = '\0';
				n = 0;
				break;
			case 3:
				printf("type in teacher\n");
				for (char t_name = getchar(); t_name != '\n';t_name=getchar(),n++)
				{
				list->courses[j].teachername[n]=t_name;
				}
				list->courses[j].teachername[n] = '\0';
				n = 0;
				break;
			case 4:
				printf("type in time\n");
				for (char time = getchar(); time != '\n';time=getchar(),n++)
				{
				list->courses[j].coursetime[n]=time;
				}
				list->courses[j].coursetime[n] = '\0';
				n = 0;
				break;
			case 5:
				printf("type in classroom\n");
				for (char classroom = getchar(); classroom != '\n';classroom=getchar(), n++)
				{
				list->courses[j].courseclassroom[n]=classroom;
				}
				list->courses[j].courseclassroom[n] = '\0';
				n = 0;
				break;
			default:
				printf("type in error\n");
				break;
			}
		} while (fun != 6);
		list->cursize++;
		savedata(list);//将内容写入文件的函数
	}
}

!!!for函数中(for(1;2;3){};其中1只在最开始进行判定,且一与二都不能更新内容,所以最好在3处执行更新操作)

以下示例中错误代码不可实现原因分析:

case 3:
				printf("type in teacher\n");
				char t_name;
				t_name = getchar();
				for (char t_name = getchar(); t_name != '\n'; n++)
				{
					 list->courses[j].coursename[n]=t_name;
				}
				list->courses[j].teachername[n] = '\0';
				n = 0;
				break;

该代码可执行,但存在逻辑问题,致使无法正确写入

原因便在于for函数中char t_name只会在初次判定时执行

只会得到一个t_name的值,并且将其不断赋给list->courses[j].coursename[n]。

在课程更新函数中,更新时间问题

方案一:

case 2:
		printf("type in new time\n");
		int n=0;
		char mid;
		rewind(stdin);
		while (1)
		{
			mid = getchar();
			if (mid != '\n')
			{
				list->courses[i].coursetime[n] = mid;
                n++;
			}
			else if (mid == '\n')
			{
				break;
			}
		}
		list->courses[i].coursetime[n] = '\0';
		break;

部分网站不建议原因及优化代码

尝试通过 rewind(stdin); 来重置标准输入流 stdin 的行为。rewind 函数是用于重置文件流的,但 stdin 通常不支持 rewind,因为它是一个不可重定位(non-rewindable)的流。

此外,即使 stdin 支持 rewind(这通常不是标准C库的行为),在 scanf 读取了一个整数之后,输入缓冲区中还会留下换行符(\n),这将导致 getchar() 直接读取到换行符并退出循环,而不等待用户输入时间。

优化后代码:

case 2:  
    printf("type in new time (e.g., 14:00-16:00)\n");  
    // 清除可能残留在输入缓冲区中的换行符  
    int c;  
    while ((c = getchar()) != '\n' && c != EOF);  
  
    // 使用fgets读取一行,包括空格  
    char time_buffer[50]; // 假设时间字符串不会超过这个长度  
    if (fgets(time_buffer, sizeof(time_buffer), stdin) != NULL) {  
        // 去除fgets可能读取的换行符  
        size_t len = strlen(time_buffer);  
        if (len > 0 && time_buffer[len-1] == '\n') {  
            time_buffer[len-1] = '\0';  
        }  
        // 将读取的字符串复制到目标位置  
        strcpy(list->courses[i].coursetime, time_buffer);  
    } else {  
        // 处理fgets读取失败的情况  
        // 例如,可能是EOF或者读取错误  
    }  
    break;

详细解释:

rewind 通常用于将文件流(file stream)的读写位置重置到文件的开始处。然而,当这个函数应用于标准输入流 stdin 时,其行为是未定义的(undefined behavior)。

rewind 的基本用法

对于文件流(如通过 fopen 打开的文件),rewind 函数用于将文件流的读写位置指针重置到文件的开始。这是非常有用的,特别是当你想要重新读取文件的内容时。

 

c复制代码

FILE *file = fopen("example.txt", "r");
if (file != NULL) {
// 读取文件内容...
rewind(file); // 将文件指针重置到文件开始
// 可以再次读取文件内容...
fclose(file);
}

为什么 rewind(stdin) 是未定义的?

stdin 是一个特殊的文件流,它通常与终端(terminal)或控制台(console)相关联,而不是与磁盘上的文件相关联。因此,尝试将 stdin 的读写位置重置到开始处是没有明确语义的。在 Unix 和类 Unix 系统(如 Linux 和 macOS)中,stdin 通常是一个管道(pipe)或套接字(socket),而不是一个可以“重绕”的文件。

此外,即使 stdin 实际上与某个文件相关联(这可以通过重定向实现,例如 ./program < input.txt),rewind(stdin) 也可能不起作用,因为不同的 C 库实现可能有不同的行为。

开辟空间相关操作理解:

list = (List*)malloc(sizeof(List))

即使用malloc开拓了list大小的动态内存,将其初始地址赋给指针list

为什么必须要有“指针中介”,直接访问文件内部为什么不可行

在C语言中,当你想要修改文件的内容时,你通常不能直接“访问并修改”文件内部的数据,因为文件系统通常不允许这种直接的原位修改(in-place modification)。

  1. 数据完整性和一致性:直接的原位修改可能会破坏数据的完整性和一致性。文件系统在设计和实现时,考虑到了数据的稳定性和可靠性。通过不允许直接的原位修改,文件系统可以确保数据在读写过程中的一致性和完整性,防止因为意外的修改而导致数据损坏或丢失。

  2. 磁盘操作的特性:磁盘操作通常是以块(block)为单位进行的,而不是以字节为单位。当你想修改文件中的一个字节时,文件系统可能需要读取包含该字节的整个块到内存中,修改内存中的字节,然后再将整个块写回磁盘。这个过程涉及到磁盘I/O操作,相对较慢,并且如果直接进行原位修改,可能会导致块中的其他数据也被意外修改。

  3. 并发访问和共享在多用户或多任务系统中,文件可能被多个进程或用户同时访问。如果允许直接的原位修改,可能会导致并发访问的问题。例如,一个进程正在读取文件的一个块,而另一个进程同时修改了该块中的数据,这会导致读取的数据与原始数据不一致。为了避免这种情况,文件系统通常采用锁定机制或其他并发控制技术来确保数据的正确访问和修改。

  4. 系统设计和性能考虑:文件系统的设计考虑了性能和效率。通过不允许直接的原位修改,文件系统可以采用更高效的缓存和缓冲策略来优化读写性能。例如,当多个进程需要读取同一个文件时,文件系统可以将该文件的数据块加载到内存中,并缓存起来,以便后续快速访问。如果允许直接的原位修改,则可能需要频繁地更新缓存和缓冲区,从而降低系统性能。

  5. 可恢复性和容错性:文件系统通常具有可恢复性和容错性机制,用于在发生故障时恢复数据。这些机制依赖于文件系统的日志、检查点和其他元数据来跟踪文件的变化和状态。如果允许直接的原位修改,这些机制可能会失效,导致数据无法恢复或恢复到不一致的状态。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值