From repetition to reuse

This article is the second edition of an article I wrote earlier, "Have You Ever Tried Writing a C Program This Way", with the article titled more aptly "From Repetition to Reuse".

The development of development technology, from the first proposal of "function/subroutine" to achieve code-level reuse; to object-oriented "classes", to reuse data structures and algorithms; to "dynamic link libraries", "controls" and other reused modules ; To today's popular cloud computing, microservices can reuse the entire system. Although technological development is changing with each passing day, the essence is reuse, but the granularity is different. Therefore, the motivation for writing code should be to turn the repetitive work into a reusable solution, where the repetitive work includes repetitive scenarios in business, repetitive code in technology, etc. A good system simplifies the repetitive work of the present; a good system also foresees the repetitive work of the future.

This article does not talk about frameworks or architectures, but about those things about writing code! The following article always focuses on the solution of a problem, constantly discovers the "duplicated" code, and extracts "reusable" abstractions, and continues to "refactor". Through this process, I hope to share with you some ways to discover duplicate code and refine reusable abstractions.

question

As the main thread running through the whole text, there is a task to develop a program to complete: there is a file "work.txt" that stores employee information (name, age, salary), the content is as follows:

William 35 25000
Kishore 41 35000
Wallace 37 30000
Bruce 39 29999
It is required to read employee compensation from a file (work.txt) and output it to the screen.
3,000 yuan for all employees whose salary is less than 30,000 yuan.
Output the adjusted salary result on the screen.
Save the adjusted result to the original file.
That is, the result of the operation is that there will be eight lines of output on the screen, and the content of "work.txt" will become:

William 35 28000
Kishore 41 35000
Wallace 37 30000
Bruce 39 32999
test

After clarifying the requirements, the first step to do is to write test code, not functional code. The definition of refactoring in the book "Refactoring" is: "On the premise of not changing the external behavior of the code, the code is modified to improve the internal structure of the program." It clearly states that the "external behavior of the code" is does not change! When iteratively refactoring, "ensure that the behavior of each refactoring is unchanged" is also a repetitive task, so test-first can not only verify the correctness of the understanding of the requirements as early as possible, but also avoid repeated testing. This article completes the following work through a shell script:

Initialize the work.txt file.
Check that the content of standard output matches the expected result.
Check whether the content of the modified work.txt file is as expected.
Clean up the site.
#!/bin/sh
if [ $# -eq 0 ]; then
echo "usage: $0 " >&2
exit -1
fi
input=$(cat <William 35 25000
Kishore 41 35000
Wallace 37 30000
Bruce 39 29999
EOF
)
output=$(cat <William 35 28000
Kishore 41 35000
Wallace 37 30000
Bruce 39 32999
EOF
)
echo "$input" > work.txt
echo "$input" > .expect.stdout.txt
echo "$output" >> .expect.stdout.txt
echo "$output" > .expect.work.txt
(gcc "$1" -o main && ./main | diff .expect.stdout.txt - && diff .expect.work.txt work.txt) && echo PASS || echo FAIL
rm -f main work.txt .expect.work.txt .expect.stdout.txt
Save the above code as check.sh with the name of the source file to be tested as a parameter. If the program passes, "PASS" is displayed, otherwise a different line is output along with "FAIL".

Part 1: Maintainable Code
First edition: It works

Every skilled programmer can quickly come up with their own implementation. The sample code in this article is written in ANSI C99. It can be compiled and run normally with gcc under Mac, and other environments have not been tested. C is chosen because mainstream programming languages ​​borrow its syntax more or less, and its syntax features are sufficient for demonstration purposes.

The problem is very simple, so simple that it doesn't feel long to stuff all the code into the main function:

#include
int main(void) {
struct {
char name[8];
int age;
int salary;
} e[4];
FILE *istream, *ostream;
int i;
istream = fopen("work.txt", "r");
for (i = 0; i < 4; i++) {
fscanf(istream, "%s%d%d", e[i].name, &e[i].age, &e[i].salary);
printf("%s %d %d ", e[i].name, e[i].age, e[i].salary);
if (e[i].salary < 30000) {
e[i].salary += 3000;
}
}
fclose(istream);
ostream = fopen("work.txt", "w");
for (i = 0; i < 4; i++) {
printf("%s %d %d ", e[i].name, e[i].age, e[i].salary);
fprintf(ostream, "%s %d %d ", e[i].name, e[i].age, e[i].salary);
}
fclose(ostream);
return 0;
}
The first loop reads 4 lines of data from work.txt, and outputs the information to the screen (requirement #1); at the same time, adds 3,000 yuan for employees with a salary of less than 30,000 (requirement #2); the second loop Go through all the data, output the adjusted results to the screen (requirement #3), and save the results to work.txt (requirement #4).

Try to save the above code as 1.c and execute ./check.sh 1.c, the screen will output "PASS", that is, pass the test.

Second Edition: Clean Code, The Basics of Refactoring

The first version of the code solved the problem, turning the repetitive salary adjustment work into a simple, reusable program. If it were an answer to a C class assignment, it would look fine - at least consistent indentation and no mixing of spaces and tabs; but from a software engineering point of view, it sucks because there is no articulation intention:

The magic constant 4 appears repeatedly, and subsequent maintenance programmers can't tell if they happen to be equal or if there is some other reason they must be equal.
The file name work.txt appears repeatedly.
Repeated and unclear file pointer type definitions, it is easy to ignore the * in front of ostream.
The e and i variables are named as the names suggest.
Variables are defined too far from their usage.
Without exception handling, the file may not be readable.
To borrow the words of Mr. Qiao: "What you can't see should be done with your heart" - although users can't see and don't care about the problems of these codes, they must do it with their hearts - there have been repetitions in several conspicuous places. However, don't rush into refactoring until the code is clear, because clear code makes it easier to find duplicates! In response to the above problems with unclear intentions, we are going to make the following adjustments to the code:

Confirm that the meaning of the number 4 in the three places is the number of employee records, so define the shared constant #define RECORD_COUNT 4.
The constant "work.txt" is different from 4. Although the content is the same, the meaning is different: one for input and one for output. If only one constant FILE_NAME is simply defined to be shared, the workload will not be reduced when the two are changed independently. So when removing duplicate code, don’t just look at the same on the surface, and the same meaning behind is the real same, otherwise it is as meaningless as defining ONE aliases for all constants 1. So need to define three constants FILE_NAME, INPUT_FILE_NAME and OUTPUT_FILE_NAME.
Replace FILE* with a custom file type typedef FILE* File; to avoid missing pointers.
Variable e is all employee information, change the variable name to employees.
The variable i is the subscript of the iterative process, and the variable name is changed to index.
Put the index variable definition in the for statement.
Move the File variable definitions from the top to their respective positions before use.
Exception checking for file pointers, outputting an error message and terminating the program prematurely when the file cannot be opened.
The more semantic EXIT_FAILURE in is used when the program exits, and EXIT_SUCCESS is used when the program exits normally.
You might ask, "The numbers 30000 and 3000 are also magic numbers, why not adjust them?" The reason is that at this point they are neither repeating nor ambiguous. The complete code after finishing is as follows:

#include
#include
#define RECORD_COUNT 4
#define FILE_NAME "work.txt"
#define INPUT_FILE_NAME FILE_NAME
#define OUTPUT_FILE_NAME FILE_NAME
typedef FILE* File;
int main(void) {
struct {
char name[8];
int age;
int salary;
} employees[RECORD_COUNT];
File istream = fopen(INPUT_FILE_NAME, "r");
if (istream == NULL) {
fprintf(stderr, "Cannot open %s with r mode. ", INPUT_FILE_NAME);
exit(EXIT_FAILURE);
}
for (int index = 0; index < RECORD_COUNT; index++) {
fscanf(istream, "%s%d%d", employees[index].name, &employees[index].age, &employees[index].salary);
printf("%s %d %d ", employees[index].name, employees[index].age, employees[index].salary);
if (employees[index].salary < 30000) {
employees[index].salary += 3000;
}
}
fclose(istream);
File ostream = fopen(OUTPUT_FILE_NAME, "w");
if (ostream == NULL) {
fprintf(stderr, "Cannot open %s with w mode. ", OUTPUT_FILE_NAME);
exit(EXIT_FAILURE);
}
for (int index = 0; index < RECORD_COUNT; index++) {
printf("%s %d %d ", employees[index].name, employees[index].age, employees[index].salary);
fprintf(ostream, "%s %d %d ", employees[index].name, employees[index].age, employees[index].salary);
}
fclose(ostream);
return EXIT_SUCCESS;
}
Save the above code as 2.c and execute ./check.sh 2.c to get the expected output PASS, which proves that this refactoring has not changed the behavior of the program.

Third Edition: Code Mapping Requirements

After the optimization of the second edition, the intention of the single line of code is relatively clear, but there are still some premature optimizations that make the meaning of the code block unclear.

For example, the two functions of "output to screen" and "adjustment of salary" are coupled in the first loop. The advantage is that one loop can be reduced, and the performance may be improved; but these two functions are independent of each other in requirements, and subsequent independent changes more likely. Suppose that the new demand is the first step after outputting to the screen, requiring the user to input a command, and then decide whether to carry out salary adjustment. At this time, for the demander, there is only one new step and only one change; but when it comes to the code level, it is not a new step that corresponds to a new piece of code, but also involves handling Talking about unrelated blocks of code; the programmer responsible for maintenance, without knowing the context, is not sure if there is a historical reason why these two pieces of code are put together, and is afraid to take them apart easily. When the scale of the system is larger, this kind of code that does not correspond to the requirements one-to-one makes the maintainers at a loss!

Recalling the daily development, the changes to the requirements are small but the code affects the whole body. The root cause is often premature optimization. "Optimization" and "generalization" are often opposites. The more thorough the optimization, the more closely integrated with the business scenario, and the worse the generality. For example, a system will sort the received messages in the buffer queue. After going online, it is found that due to external reasons such as product design, the messages may be naturally close to the sorted order, so the insertion sort is used instead of a more general sorting algorithm such as quick sort. This is a non-universal optimization: it makes the system perform better, but the system is narrower. Premature optimization is to prematurely set a ceiling on system capabilities.

The ideal situation is that the code block corresponds to the requirement function point one by one. For example, the current requirement has 4 function points, and there must be 4 independent code blocks corresponding to it. The advantage of this is that when the requirements change, the modification of the code is relatively centralized. Therefore, the following adjustments are prepared based on the second version of the code:

Split up coupled looping blocks of code, each of which does one thing.
Use comments to clearly mark the requirements for each code block.
The complete code after finishing is as follows:

#include
#include
#define RECORD_COUNT 4
#define FILE_NAME "work.txt"
#define INPUT_FILE_NAME FILE_NAME
#define OUTPUT_FILE_NAME FILE_NAME
typedef FILE* File;
int main(void) {
struct {
char name[8];
int age;
int salary;
} employees[RECORD_COUNT];
/* read from file */
File istream = fopen(INPUT_FILE_NAME, "r");
if (istream == NULL) {
fprintf(stderr, "Cannot open %s with r mode. ", INPUT_FILE_NAME);
exit(EXIT_FAILURE);
}
for (int index = 0; index < RECORD_COUNT; index++) {
fscanf(istream, "%s%d%d", employees[index].name, &employees[index].age, &employees[index].salary);
}
fclose(istream);
/* 1. Output to screen */
for (int index = 0; index < RECORD_COUNT; index++) {
printf("%s %d %d ", employees[index].name, employees[index].age, employees[index].salary);
}
/* 2. Adjust salary */
for (int index = 0; index < RECORD_COUNT; index++) {
if (employees[index].salary < 30000) {
employees[index].salary += 3000;
}
}
/* 3. Output the adjusted result */
for (int index = 0; index < RECORD_COUNT; index++) {
printf("%s %d %d ", employees[index].name, employees[index].age, employees[index].salary);
}
/* 4. Save to file */
File ostream = fopen(OUTPUT_FILE_NAME, "w");
if (ostream == NULL) {
fprintf(stderr, "Cannot open %s with w mode. ", OUTPUT_FILE_NAME);
exit(EXIT_FAILURE);
}
for (int index = 0; index < RECORD_COUNT; index++) {
fprintf(ostream, "%s %d %d ", employees[index].name, employees[index].age, employees[index].salary);
}
fclose(ostream);
return EXIT_SUCCESS;
}
Save the above code as 3.c and execute ./check.sh 3.c to ensure that the behavior of the program has not changed.

Part 2: Object-Oriented Style
Fourth Edition: Employee Object Abstraction

After two rounds of transformation, the code structure is clear enough; now you can start refactoring to sort out the code hierarchy.

The most conspicuous is the formatted output of employee information: except for the different output streams, the format and content are exactly the same, and three of the four requirements appear. Generally, when encountering the same/similar code, a function can be abstracted: the same part is written in the function body, and different parts are passed in as parameters. Here, a function with structure data and file stream as input parameters can be abstracted, but at present this structure is still anonymous and cannot be used as a parameter of the function, so the first step is to select a suitable one for the anonymous staff structure The type name of:

typedef struct _Employee {
char name[8];
int age;
int salary;
} *Employee;
Then the abstract public function is used to format the output Employee to File, which also couples two functions:

Employee is serialized into a string.
The serialized result is output to the specified file stream.
Because there is no scene for using a function independently, there is no need to further split it at present:

void employee_print(Employee employee, File ostream) {
fprintf(ostream, "%s %d %d ", employee->name, employee->age, employee->salary);
}
The Employee structure + employee_print function is easily reminiscent of object-oriented "classes". The essence of object-oriented is that a system is composed of a group of objects with independent functions. The objects cooperate to complete tasks by sending messages. It is not necessary to have the class keyword, inheritance, encapsulation, polymorphism and other syntactic sugar.

The "functional independence" of objects, that is, high cohesion, requires data and related methods of manipulating data to be put together. Most programming languages ​​that support object-oriented programming provide the class keyword, which enforces bundling at the language level. C language does not do this. syntax, but coding conventions can be made to bring data structures and functions closer together physically.
"Sending a message to an object" has different expressions in different programming languages. For example, in Java, foo.baz() is to send a baz message to the foo object. The equivalent syntax in C++ is foo->baz(), and in Smalltalk is foo baz, and in C it is baz(foo).
To sum up, although C is generally considered to be not an object-oriented language, in fact it supports the object-oriented style. Along the above lines, four methods of employee objects can be abstracted:

employee_read: Constructor, allocate space, input and deserialize, similar to Java's new.
employee_free: Destructor, release space, that is, pure manual GC.
employee_print: Serialize and output.
employee_adjust_salary: Adjust employee salary, the only business logic.
With the staff object, the program no longer has only one main function. Suppose the main function is regarded as the application layer, and other functions are regarded as class libraries, frameworks or middleware, so that the program has layers, and the layers communicate only through open interfaces, that is, the encapsulation of objects.

In Java, there are four visibility modifiers: public, protected, default, and private. Functions in C language are public by default. After adding the static keyword, they are only visible in the current file. In order to prevent the application layer from sending messages to the object at will, it is agreed that only the functions used in the application layer are exposed, so two modifiers, public and private, are additionally defined. Currently, the four methods of the employee object are public.

The complete code after refactoring is as follows:

#include
#include
#define private static
#define public
#define RECORD_COUNT 4
#define FILE_NAME "work.txt"
#define INPUT_FILE_NAME FILE_NAME
#define OUTPUT_FILE_NAME FILE_NAME
typedef FILE *File;
/* staff object */
typedef struct _Employee {
char name[8];
int age;
int salary;
} *Employee;
public void employee_free(Employee employee) {
free(employee);
}
public Employee employee_read(File istream) {
Employee employee = (Employee) calloc(1, sizeof(struct _Employee));
if (employee == NULL) {
fprintf(stderr, "employee_read: out of memory ");
exit(EXIT_FAILURE);
}
if (fscanf(istream, "%s%d%d", employee->name, &employee->age, &employee->salary) != 3) {
employee_free(employee);
return NULL;
}
return employee;
}
public void employee_print(Employee employee, File ostream) {
fprintf(ostream, "%s %d %d ", employee->name, employee->age, employee->salary);
}
public void employee_adjust_salary(Employee employee) {
if (employee->salary < 30000) {
employee->salary += 3000;
}
}
/* application layer */
int main(void) {
Employee employees[RECORD_COUNT];
/* read from file */
File istream = fopen(INPUT_FILE_NAME, "r");
if (istream == NULL) {
fprintf(stderr, "Cannot open %s with r mode. ", INPUT_FILE_NAME);
exit(EXIT_FAILURE);
}
for (int index = 0; index < RECORD_COUNT; index++) {
employees[index] = employee_read(istream);
}
fclose(istream);
/* 1. Output to screen */
for (int index = 0; index < RECORD_COUNT; index++) {
employee_print(employees[index], stdout);
}
/* 2. Adjust salary */
for (int index = 0; index < RECORD_COUNT; index++) {
employee_adjust_salary(employees[index]);
}
/* 3. Output the adjusted result */
for (int index = 0; index < RECORD_COUNT; index++) {
employee_print(employees[index], stdout);
}
/* 4. Save to file */
File ostream = fopen(OUTPUT_FILE_NAME, "w");
if (ostream == NULL) {
fprintf(stderr, "Cannot open %s with w mode. ", OUTPUT_FILE_NAME);
exit(EXIT_FAILURE);
}
for (int index = 0; index < RECORD_COUNT; index++) {
employee_print(employees[index], ostream);
}
fclose(ostream);
/* release resources */
for (int index = 0; index < RECORD_COUNT; index++) {
employee_free(employees[index]);
}
return EXIT_SUCCESS;
}
Save the code as 4.c, and execute ./check.sh 4.c as usual to detect whether the program behavior has been changed.

Fifth Edition: Container Object Abstraction

The previous refactoring removed lexical and syntactic repetitions, just like words and sentences in an article, and then you can see if the paragraphs are repeated, that is, code blocks.

Similar to employees_print, the three-stage loop output employee information code is also obviously repeated, which can abstract employees_print, and also abstract another object - employee list - Employees. Referring to the employee object, four corresponding functions can be abstracted:

employees_read: Constructor, allocate list space, and create employee objects in turn.
employees_free: Destructor, release list space, and space for employee objects.
employees_print: Serialize and output the information of each employee in the list.
employees_adjust_salary: Adjust the salary of all employees who meet the requirements.
At this point, the main function only needs to call the method of the employee list object, and no longer directly calls the method of the employee object, so the visibility of the latter is reduced from public to private.

The complete code after refactoring is as follows:

#include
#include
#define private static
#define public
#define RECORD_COUNT 4
#define FILE_NAME "work.txt"
#define INPUT_FILE_NAME FILE_NAME
#define OUTPUT_FILE_NAME FILE_NAME
typedef FILE *File;
/* staff object */
typedef struct _Employee {
char name[8];
int age;
int salary;
} *Employee;
private void employee_free(Employee employee) {
free(employee);
}
private Employee employee_read(File istream) {
Employee employee = (Employee) calloc(1, sizeof(struct _Employee));
if (employee == NULL) {
fprintf(stderr, "employee_read: out of memory ");
exit(EXIT_FAILURE);
}
if (fscanf(istream, "%s%d%d", employee->name, &employee->age, &employee->salary) != 3) {
employee_free(employee);
return NULL;
}
return employee;
}
private void employee_print(Employee employee, File ostream) {
fprintf(ostream, "%s %d %d ", employee->name, employee->age, employee->salary);
}
private void employee_adjust_salary(Employee employee) {
if (employee->salary < 30000) {
employee->salary += 3000;
}
}
/* Staff list object */
typedef Employee* Employees;
public Employees employees_read(File istream) {
Employees employees = (Employees) calloc(RECORD_COUNT, sizeof(Employee));
if (employees == NULL) {
fprintf(stderr, "employees_read: out of memory ");
exit(EXIT_FAILURE);
}
for (int index = 0; index < RECORD_COUNT; index++) {
employees[index] = employee_read(istream);
}
return employees;
}
public void employees_print(Employees employees, File ostream) {
for (int index = 0; index < RECORD_COUNT; index++) {
employee_print(employees[index], ostream);
}
}
public void employees_adjust_salary(Employees employees) {
for (int index = 0; index < RECORD_COUNT; index++) {
employee_adjust_salary(employees[index]);
}
}
public void employees_free(Employees employees) {
for (int index = 0; index < RECORD_COUNT; index++) {
employee_free(employees[index]);
}
free(employees);
}
/* application layer */
int main(void) {
/* read from file */
File istream = fopen(INPUT_FILE_NAME, "r");
if (istream == NULL) {
fprintf(stderr, "Cannot open %s with r mode. ", INPUT_FILE_NAME);
exit(EXIT_FAILURE);
}
Employees employees = employees_read(istream);
fclose(istream);
/* 1. Output to screen */
employees_print(employees, stdout);
/* 2. Adjust salary */
employees_adjust_salary(employees);
/* 3. Output the adjusted result */
employees_print(employees, stdout);
/* 4. Save to file */
File ostream = fopen(OUTPUT_FILE_NAME, "w");
if (ostream == NULL) {
fprintf(stderr, "Cannot open %s with w mode. ", OUTPUT_FILE_NAME);
exit(EXIT_FAILURE);
}
employees_print(employees, ostream);
fclose(ostream);
/* release resources */
employees_free(employees);
return EXIT_SUCCESS;
}
Don't forget to run ./check.sh for regression testing.

Sixth Edition: Input-Output Abstraction

At this point, the main function is relatively clean, and there is one obvious repetition: open the file and check whether the file is opened normally. This is a file-related operation, and a file_open can be abstracted instead of fopen:

private File file_open(char* filename, char* mode) {
File stream = fopen(filename, mode);
if (stream == NULL) {
fprintf(stderr, "Cannot open %s with %s mode. ", filename, mode);
exit(EXIT_FAILURE);
}
return stream;
}
Then you can continue to abstract the input and output methods of the employee list object:

employees_input: Get data from file and create employee list object.
employees_output: Output the contents of the employee list object to a file.
After the refactoring, employees_read is no longer accessed by main, so it is changed to private. The complete code after refactoring is as follows:

#include
#include
#define private static
#define public
#define RECORD_COUNT 4
#define FILE_NAME "work.txt"
#define INPUT_FILE_NAME FILE_NAME
#define OUTPUT_FILE_NAME FILE_NAME
typedef FILE *File;
typedef char* String;
/* staff object */
typedef struct _Employee {
char name[8];
int age;
int salary;
} *Employee;
private void employee_free(Employee employee) {
free(employee);
}
private Employee employee_read(File istream) {
Employee employee = (Employee) calloc(1, sizeof(struct _Employee));
if (employee == NULL) {
fprintf(stderr, "employee_read: out of memory ");
exit(EXIT_FAILURE);
}
if (fscanf(istream, "%s%d%d", employee->name, &employee->age, &employee->salary) != 3) {
employee_free(employee);
return NULL;
}
return employee;
}
private void employee_print(Employee employee, File ostream) {
fprintf(ostream, "%s %d %d ", employee->name, employee->age, employee->salary);
}
private void employee_adjust_salary(Employee employee) {
if(employee->salary < 30000) {
employee->salary += 3000;
}
}
/* 职员列表对象 */
typedef Employee* Employees;
private Employees employees_read(File istream) {
Employees employees = (Employees) calloc(RECORD_COUNT, sizeof(Employee));
if (employees == NULL) {
fprintf(stderr, "employees_read: out of memory ");
exit(EXIT_FAILURE);
}
for (int index = 0; index < RECORD_COUNT; index++) {
employees[index] = employee_read(istream);
}
return employees;
}
public void employees_print(Employees employees, File ostream) {
for (int index = 0; index < RECORD_COUNT; index++) {
employee_print(employees[index], ostream);
}
}
public void employees_adjust_salary(Employees employees) {
for (int index = 0; index < RECORD_COUNT; index++) {
employee_adjust_salary(employees[index]);
}
}
public void employees_free(Employees employees) {
for (int index = 0; index < RECORD_COUNT; index++) {
employee_free(employees[index]);
}
free(employees);
}
/* I/O层 */
private File file_open(String filename, String mode) {
File stream = fopen(filename, mode);
if (stream == NULL) {
fprintf(stderr, "Cannot open %s with %s mode. ", filename, mode);
exit(EXIT_FAILURE);
}
return stream;
}
public Employees employees_input(String filename) {
File istream = file_open(filename, "r");
Employees employees = employees_read(istream);
fclose(istream);
return employees;
}
public void employees_output(Employees employees, String filename) {
File ostream = file_open(filename, "w");
employees_print(employees, ostream);
fclose(ostream);
}
/* 应用层 */
int main(void) {
Employees employees = employees_input(INPUT_FILE_NAME); /* 从文件读入 */
employees_print(employees, stdout); /* 1. 输出到屏幕 */
employees_adjust_salary(employees); /* 2. 调整薪资 */
employees_print(employees, stdout);/* 3. 输出调整后的结果 */
employees_output(employees, OUTPUT_FILE_NAME);/* 4. 保存到文件 */
employees_free(employees); /* 释放资源 */
return EXIT_SUCCESS;
}
别忘记执行./check.sh。

Related Articles

Explore More Special Offers

  1. Short Message Service(SMS) & Mail Service

    50,000 email package starts as low as USD 1.99, 120 short messages start at only USD 1.00

phone Contact Us