在原始代码中,这个片段是可疑的:
if(current->head!=NULL){
hx = current->head->grade;
cy = current->current->grade;}
else{
}
if(current->head==NULL){
缩进表明它自己的行上的右大括号是一个闯入者;您不需要一个空else
块,并且缩进表明if (current->head == NULL)
代码应该是主体的一部分,else
而不是一组新的条件。如果删除标识为闯入者的行,则可能需要在函数末尾添加一个额外的右大括号。
如果你干净地缩进你的代码,那么很容易看到块是如何排列的。当你把缩进弄得一团糟时,很难发现这样的错误。缩进用于使代码易于阅读和理解,误导性缩进意味着代码难以阅读,难以理解和正确。
在修改后的代码中,去掉了原始注释,重新格式化和缩进以便于阅读,并为讨论点添加了新的注释,我们有:
void add_course(STUDENT *current)
{
int hx,cy,tmpz; // P1
COURSE *tmp = current->current; // P2
if (current->head != NULL) // P3
{
hx = current->head->grade;
cy = current->current->grade;
}
if (current->head == NULL)
{
current->head = current->current; // P4
current->tail = current->current;
current->tail->next = NULL; // P5
}
else
{
if (hx > cy) // P6
{
current->current->next = current->head;
current->head = current->current;
current->current = current->head;
while (current->current->next != NULL)
current->current = current->current->next;
current->tail->next = current->current;
current->tail = current->current;
current->tail->next = NULL;
}
else
{ // P7
current->current = current->head;
hx = current->current->grade;
tmpz = tmp->grade;
while (current->current->next != NULL && tmpz > hx)
{
current->current = current->current->next;
hx = current->current->next->grade;
}
current->current->next = tmp->next;
current->current->next = tmp;
current->current = tmp;
}
}
}
用于块的开放式大括号的位置是有争议的。有些人,包括 K&R,喜欢与if
, else
, while
, for
,switch
等在同一行的左大括号。我非常喜欢Allman缩进风格,但在必要时使用本地约定。当我大量重新格式化某些东西时,它会进入 Allman 风格——但你不应该将其解释为我的反常性以外的任何东西。更改不是对您的代码的任何批评的一部分。
您没有向我们展示没有课程记录的学生记录是如何表示的。最合理的表示是head
,tail
和current
都初始化为 NULL。
您的函数需要学生记录,但(令人惊讶的是)不是要添加的课程,尽管名称add_course()
. 鉴于使用current->current
at P4,我们必须假设您current->current
在调用该函数之前通过设置为新课程部分添加了课程,并且您需要该函数来确定新课程在列表中的head
位置结束于tail
. 这在功能上不是很有凝聚力——这是一个糟糕的设计。理想情况下,新的课程记录应该作为单独的参数传递给函数,并且该next
字段应该为 NULL(并且应该初始化课程名称,并且必须初始化成绩):
void add_course(STUDENT *student, COURSE *new_course);
我们可以观察到 P1 和 P2 处的变量直到 P6 或 P7 才使用,因此 P1、P2 和 P3 处的代码应该移到 P6 之前或更晚。我将坚持使用 C89 代码,而不是使用 C99(以及 C11 和 C++)支持的“在任何地方定义”规则。
P4 到 P5 的段落应该处理一个空列表。它可以写成使用tmp
(在这种情况下,P2 的定义应该保留在原处)。写起来可能更清楚:
COURSE *new_course = current->current;
if (current->head == NULL)
{
current->head = new_course;
current->tail = new_course;
new_course->next = NULL;
}
如果在调用函数之前正确初始化课程,则最终分配应该是不必要的。
假设已经有一门课程与学生相关联,并且通过调用代码将新课程的值更改current->current
为新课程并调用此函数来添加新课程。有几种可能的情况需要考虑:
- 新课程是(该)现有课程的副本(应该被拒绝 - 但该函数不返回值,因此无法指示该课程被拒绝)。
- 新课程成绩低于现有课程成绩。
- 新课程成绩高于现有课程成绩。
- 新课程成绩与现有课程成绩相同。
问题没有具体说明第一种和最后一种情况应该怎么做。当出现第二种情况时,应在现有课程之前插入新课程;当第三个出现时,应在现有课程之后插入新课程。为明确起见,如果新成绩相同,则应按字母顺序列出课程。处理重复项涉及在整个列表中搜索匹配项;它可能应该被封装成一个函数:
COURSE *find_course(STUDENT *student, COURSE *course);
该函数获取一个学生,从head
到搜索列表tail
,将课程名称与列表中的课程进行比较。如果有,则返回列表中匹配的元素;此处的代码只要求函数返回 NULL 表示未找到该名称。
COURSE *find_course(STUDENT *student, COURSE *course)
{
COURSE *next_student = student->head;
while (next_student != NULL && strcmp(next_student->cname, course->cname) != 0)
next_student = next_student->next;
return next_student;
}
将此函数接口和实现更改为:
COURSE *find_course(COURSE *course, const char *cname)
{
while (course != NULL)
{
if (strcmp(course->cname, cname) == 0)
return(course);
course = course->next;
}
return(course);
}
这可用于搜索任何正确构建的课程列表,例如有效课程列表(因此您可以拒绝无效课程)。
我们还应该回顾当有多个现有课程时应该发生什么,这样我们就可以避免重复代码。重复的课程检查是相同的,仍然应该排在第一位。由于我们可以通过归纳安全地假设空列表是有序的,而单元素列表是有序的,我们可以决定add_course()
始终确保课程列表是有序的。
但是,我将把它留给你处理。
我们将需要一个课程比较功能。我们可以使用与 相同的约定strcmp()
,如果第一个参数应该在第二个之前返回一个负数,如果第二个应该在第一个之前返回一个正数;如果两个课程相同,则(名义上)为零:
int cmp_course(const COURSE *c1, const COURSE *c2)
{
if (c1->grade < c2->grade)
return -1;
else if (c1->grade > c2->grade)
return +1;
else
return(strcmp(c1->cname, c2->cname));
}
继续
[...长时间的停顿...24小时或更长时间...]
这是您的代码,经过注释,包装到工作,编译,运行(C99)程序中。除了轻微的重新格式化和添加断言之外,我根本没有更改代码add_course()
。
#include <assert.h>
#include <inttypes.h>
#include <stdio.h>
typedef struct student
{
char sname[32];
char sid[9];
struct student *snext;
struct course *current;
struct course *head;
struct course *tail;
} STUDENT;
typedef struct course
{
char cname[15];
char grade;
struct course *next;
} COURSE;
extern void add_course(STUDENT *current);
void add_course(STUDENT *current)
{
int hx,cy,tmpz;
COURSE *tmp = current->current;
assert(tmp != 0);
if (current->head != NULL)
{
hx = current->head->grade;
cy = current->current->grade;
}
if (current->head == NULL)
{
current->head = current->current;
current->tail = current->current;
current->tail->next = NULL;
}
else
{
if (hx > cy)
{
current->current->next = current->head;
current->head = current->current;
current->current = current->head;
while (current->current->next != NULL)
current->current = current->current->next;
current->tail->next = current->current;
current->tail = current->current;
current->tail->next = NULL;
}
else
{
current->current = current->head;
hx = current->current->grade;
tmpz = tmp->grade;
while (current->current->next != NULL && tmpz>hx)
{
current->current = current->current->next;
hx = current->current->next->grade;
}
current->current->next = tmp->next;
current->current->next = tmp;
current->current = tmp;
}
}
}
static void dump_student(FILE *fp, const char *tag, const STUDENT *student)
{
fprintf(fp, "Student: %s\n", tag);
fprintf(fp, "Name: %s; ID: %s\n", student->sname, student->sid);
fprintf(fp, "Next: 0x%" PRIXPTR "\n", (uintptr_t)student->snext);
fprintf(fp, "Current: 0x%" PRIXPTR "; ", (uintptr_t)student->current);
fprintf(fp, "Head: 0x%" PRIXPTR "; ", (uintptr_t)student->head);
fprintf(fp, "Tail: 0x%" PRIXPTR "\n", (uintptr_t)student->tail);
COURSE *cp = student->head;
while (cp != 0)
{
fprintf(fp, "Course: %-14s (%c) (0x%.16" PRIXPTR ")\n",
cp->cname, cp->grade, (uintptr_t)cp->next);
cp = cp->next;
}
}
int main(void)
{
STUDENT s1 = { "Yours Truly", "me", 0, 0, 0, 0 };
COURSE c[] =
{
{ "Math", 'B', 0 },
{ "English", 'A', 0 },
{ "Science", 'D', 0 },
{ "History", 'C', 0 },
{ "French", 'C', 0 },
};
dump_student(stdout, "Before", &s1);
for (int i = 0; i < 5; i++)
{
char buffer[8];
sprintf(buffer, "After%d", i+1);
s1.current = &c[i];
add_course(&s1);
dump_student(stdout, buffer, &s1);
}
return(0);
}
注意dump_student()
功能;我发现具有这种接口的函数非常有用,并且经常将它们保留在代码中以供以后调试。该FILE *
参数意味着可以将输出发送到标准错误(或日志文件)和标记以识别正在运行的调用的发生情况。如果需要,您可以在界面中添加文件、行、函数名;我通常不会那样做。
只有几个地方的代码是 C99;中的for
循环main()
和课程指针定义dump_student()
;如果您的 C 编译器不支持 C99 语法,您可以移动变量定义。
这是 Mac OS X 10.7.4 上 64 位编译的示例输出。
Student: Before
Name: Yours Truly; ID: me
Next: 0x0
Current: 0x0; Head: 0x0; Tail: 0x0
Student: After1
Name: Yours Truly; ID: me
Next: 0x0
Current: 0x7FFF643D84E0; Head: 0x7FFF643D84E0; Tail: 0x7FFF643D84E0
Course: Math (B) (0x0000000000000000)
Student: After2
Name: Yours Truly; ID: me
Next: 0x0
Current: 0x7FFF643D84E0; Head: 0x7FFF643D84F8; Tail: 0x7FFF643D84E0
Course: English (A) (0x00007FFF643D84E0)
Course: Math (B) (0x0000000000000000)
Student: After3
Name: Yours Truly; ID: me
Next: 0x0
Current: 0x7FFF643D8510; Head: 0x7FFF643D84F8; Tail: 0x7FFF643D84E0
Course: English (A) (0x00007FFF643D84E0)
Course: Math (B) (0x00007FFF643D8510)
Course: Science (D) (0x0000000000000000)
Student: After4
Name: Yours Truly; ID: me
Next: 0x0
Current: 0x7FFF643D8528; Head: 0x7FFF643D84F8; Tail: 0x7FFF643D84E0
Course: English (A) (0x00007FFF643D84E0)
Course: Math (B) (0x00007FFF643D8528)
Course: History (C) (0x0000000000000000)
Student: After5
Name: Yours Truly; ID: me
Next: 0x0
Current: 0x7FFF643D8540; Head: 0x7FFF643D84F8; Tail: 0x7FFF643D84E0
Course: English (A) (0x00007FFF643D84E0)
Course: Math (B) (0x00007FFF643D8540)
Course: French (C) (0x0000000000000000)
请注意,前几次插入很好,但之后出现问题。我跑了下来valgrind
,这给了代码一个干净的健康状况(尽管系统库之外没有动态内存分配)。
我建议您追查为什么在第三次插入后列表没有正确扩展。