$29
1 Introduction and purpose
In this project you will revise your elephant functions from the previous project to use dynamic memory allocation. In many situations we don’t know ahead of time how much data needs to be stored, which is one reason for using dynamic memory allocation. In this project you will now dynamically allocate elephant structures, and elephants will also now have names, which are character strings that will be dynamically allocated as well. This allows names of arbitrary size to be created without wasting memory, because only the memory actually needed to store each name needs to be allocated.
The main purpose of the project is to get some practice with basic dynamic memory allocation, before you have to use it for more complex data structures soon. But another purpose is to get some practice with a few of C’s string library functions from Chapter 9 in the Reek text, which you were responsible for reading on your own. You are expected to use the string library functions whenever there is one that can be used. Do not write code (using loops or recursion) that duplicates the effects of any string library functions, or you will lose credit. Just use the string library functions instead. In fact, it is not necessary to use any loops or recursion anywhere to write the project, so if you write any loops or use any recursion in the project at all you will lose considerable credit.
Like the previous two projects this is a small project. It has mostly public tests, and will not be graded for style. So you again have a shorter time to do it, so you will have more time for later projects that are more difficult.
You can submit this project three times. After that, every submission you make, for whatever reason, will result in losing 1 point from your score. You are expected to compile, run, and test your code yourself, before submitting. (Even if you change something as innocuous as a comment you may have made a typing mistake that would cause a syntax error– recompile and check your results again before submitting.)
Due to the size of the course it is not feasible for us to be able to provide project information or help via email/ELMS messages, so we will be unable to answer such questions. You are welcome to ask any questions verbally during the TAs’ office hours.
2 Functions to be written
The fields of the Elephant structure in elephant.h are the same as before except for the addition of a name field, which is a pointer to a char. Functions that have to store a name for an elephant must dynamically allocate memory for the name first. In all cases where functions have to store an elephant’s name they must create a string of exactly the
size needed to store the name– not longer (and certainly not any shorter). There is only one function now that creates an elephant, new_elephant(), and it must dynamically allocate an Elephant structure, store its parameters’ values in its fields, and return a pointer to the allocated structure. Some of the functions have been removed, and most of the remaining ones now take a pointer to a Elephant structure as a parameter (or pointers to two Elephant structures).
Since most of the functions now take an Elephant pointer as a parameter, for brevity below we may write things like “the function’s Elephant parameter” to mean “the Elephant structure variable that the function’s pointer parameter points to”.
Note that to reduce duplicative code the public tests all call a function check_elephant() that we have provided. And two of the public tests check whether some of your functions have memory leaks or cause problems in the heap. This is explained further in Section 2.8 below.
2.1 Elephant *new_elephant(const char name[], enum type which_type, unsigned int id, unsigned short weight, float trunk_length)
Project #5 discussed some approaches for writing structure functions in C. Another option not mentioned then is for a function to return a pointer to a structure that the function has itself dynamically allocated. Using this approach a function can return a pointer to an initialized structure without requiring the caller to declare a structure or to allocate memory for a structure. It is common for functions to dynamically allocate a structure and return a pointer to it after suitably initializing its fields. This function will use this approach.
The function should just return NULL without doing anything else if its array parameter is NULL. Otherwise it should return a pointer to a dynamically–allocated Elephant structure that has the parameters’ values stored in its fields. The name field of the newly created Elephant structure has no fixed or maximum length, so this function now must allocate
© 2023 L. Herman; all rights reserved 1
a string of exactly the right size to store the name parameter (however long it may be), copy the value of the name parameter to the allocated string, and make the name field of the allocated structure point to it.
To emphasize: the new elephant’s name field must point to new memory that this function allocates, not just point to the parameter name that was passed in. In other words, this function should not just be doing pointer aliasing.
Use string library functions for all manipulations of the elephant’s name, not loops (or recursion), or you will lose considerable credit.
2.2 unsigned short get_type(const Elephant *const el_ptr, enum type *const type_ptr)
This function should “return” to the caller the type of its parameter elephant, but it should also return 0 if either parameter is NULL. However, since the first constant in an enum has the numeric value 0, if the function only returned a value, the caller would not be able to distinguish between it returning 0 to indicate that either parameter was NULL, or it returning the enum value AFRICAN. What the function should do instead is to return 0 or 1 depending upon its parameters’ validity, and, if the parameters are valid, to store the elephant’s type into the variable that type_ptr points to.
So if either parameter is NULL the function should return 0 without changing anything else, while otherwise it should return 1 after storing its parameter elephant’s type into the variable that its second parameter points to.
2.3 const char *get_name(const Elephant *const el_ptr)
This function should return NULL if it parameter is NULL, or if its parameter is not NULL but its parameter’s name field is NULL. Otherwise it should return its parameter elephant’s name, but it should not just return a pointer to the name field of the Elephant structure itself. Instead, it should allocate enough memory to store the name, copy the parameter elephant’s name to the newly–allocated memory, and return a pointer to this new copy of the name. The caller will be responsible for freeing the returned memory later, if they want to avoid memory leaks. (This is their responsibility to do, which your function cannot enforce.)
Use string library functions for all manipulations of the elephant’s name, not loops (or recursion), or you will lose considerable credit.
2.4 void print_elephant(const Elephant *const el_ptr)
This function should have no effect if its parameter is NULL, or if its parameter is not NULL but its parameter’s name field is NULL. Otherwise it should print its parameter elephant’s fields, in the same manner as print_elephant() did in Project #5, except the elephant’s name is to be printed first. (One of the public tests illustrates the expected output format.)
2.5 short compare(const Elephant *const el_ptr1, const Elephant *const el_ptr2)
This function should return −1 if either of its parameters is NULL, or if either parameter is not NULL but the elephant’s name field is NULL. (Note that its return type is now short, as opposed to unsigned short in the Project #5 version of this function.) If its parameters are non–NULL it should return 1 if its two Elephant structure parameters have all the same values, and 0 otherwise.
Use string library functions for all manipulations of the elephant’s name, not loops (or recursion), or you will lose considerable credit.
2.6 unsigned short change_name(Elephant *const el_ptr, const char new_name[])
This function should return 0 without changing anything if either parameter is NULL. Otherwise it should change the name field of its elephant parameter to be new_name and return 1. (It is not a problem if the elephant’s name field is initially NULL, because the function is going to change the name anyway.) The function must change the name by allocating a new string of exactly the right size to store the value of new_name, copy the contents of new_name to the allocated string, and make the name field of the structure that el_ptr points to now point to the new allocated name.
Note that el_ptr’s name field should be set to point to new memory that the function allocates, of exactly the right size needed, and copy the contents of new_name to the new memory. The function should not just set el_ptr’s name field to point to new_name itself. In other words, this function should not just be doing pointer aliasing. But before reassigning the structure’s name parameter the function should free the memory of the elephant’s current name, to avoid memory leaks.
© 2023 L. Herman; all rights reserved 2
Use string library functions for all manipulations of the elephant’s name, not loops (or recursion), or you will lose considerable credit.
2.7 unsigned short copy(Elephant *const el_ptr1, const Elephant *const el_ptr2)
This function should return 0 without changing anything if either parameter is NULL, or if either parameter is not NULL but the elephant’s name field is NULL. Otherwise it should return 1 after copying all of the fields of the structure that el_ptr2 points to into the structure that el_ptr1 points to.
This function should store new values into the fields of the Elephant structure that its first parameter points to. How-ever, as in new_elephant() and change_name(), this function should cause its first parameter’s name field to point to new memory that the function allocates that contains a copy of el_ptr2’s name, which is just the right size to store the name. el_ptr1’s name should not just point to el_ptr2’s name field (the function should not just be doing pointer aliasing). But before making el_ptr1’s name field point to a new string, the function should free the memory used for el_ptr1’s current name, to avoid memory leaks.
Important: the caller is passing in the memory addresses of existing Elephant structures that were created before calling this function. This function has to allocate memory for el_ptr1’s new name, but not for the structure that el_ptr1 points to. The Elephant structures that the parameters point to were created before this function is called.
Use string library functions for all manipulations of the elephant’s name, not loops (or recursion), or you will lose considerable credit.
2.8 Checking elephants, memory functions, and memory checking
We are providing (in the project tarfile) two compiled object files named check-elephant.o and memory-functions.o, and their associated header files check-elephant.h and memory-functions.h. We are not providing the source files for check-elephant.c or memory-functions.c, just the object files. If you choose to write a makefile to compile your code, see Section A.2 below regarding considerations regarding these object files.
check-elephant.o defines a single function check_elephant(), whose parameters can be seen in the header file check-elephant.h. It simply returns 1 if all of the fields of its first Elephant parameter are equal to the remaining parameters, and 0 otherwise. Since checking an elephant’s data is performed a number of times in the tests, it just avoids repetitive code to use this helper function instead of duplicating the checks. (See the comment in the header file.)
memory-functions.o contains three compiled functions setup_memory_checking(), get_memory_in_use(), and check_heap(), which have no parameters. get_memory_in_use() returns an int, while the others have no return value. We use them in two public tests to detect memory leaks in some of your functions, and other possible errors in the heap. Their effects are:
• setup_memory_checking() must be called once in a program before any memory is dynamically allocated; it sets up things to check the consistency and correctness of the heap. If this function has been called initially, and the program has any memory errors later (such as overwriting beyond the end of an allocated memory region in the heap) the program will crash, consequently failing the test.
• get_memory_in_use() returns the amount of memory currently allocated (in use) in the heap.
• check_heap() performs certain consistency checks on all of the currently allocated memory in the heap.
Two tests initially call setup_memory_checking(), then call get_memory_in_use() before performing some opera-tions on elephants. Later in the test, when the same amount of memory should be used, they call get_memory_in_use() again and verify that the memory used is the same as it was earlier, causing the test to fail if not. At their end, these two tests call check_heap() to ensure that the heap is valid.
When you compile these tests note that you will have to include the provided object file memory-functions.o in linking rules to form their executables.
Lastly note that the public tests (even the two that call our memory checking functions) will unavoidably have memory leaks. The tests are calling your new_elephant() function, which returns a dynamically–allocated Elephant structure, but the tests are not freeing the structure anywhere. That is not your problem. That would be up to the user of your code to do, not your functions. In other words we aren’t freeing any memory in the tests, but you should free memory that is no longer needed in your functions in elephant.c. And these two tests just check that your functions don’t have memory leaks, even though the tests themselves do cause memory leaks.
© 2023 L. Herman; all rights reserved 3
• Development procedure review
A.1 Obtaining the project files, compiling, checking your results, and submitting
Log into the Grace machines and use commands similar to those from before:
cd ~/216
tar -zxvf ~/216public/project06/project06.tgz
This will create a directory project06 that contains the files for the project, including the header files elephant.h, check-elephant.h, and memory-functions.h, the two object files check-elephant.o and memory-functions.o, and the public tests. You must have your coursework in your special course disk space for this class. Create a file in the project06 directory named elephant.c (spelled exactly that way) that will #include the header file elephant.h, and in it write the functions whose prototypes are in elephant.h.
A command like gcc public01.c elephant.c check-elephant.o -o public01.x will compile your program for the first public test (or you can use Emacs to compile if desired); replace the 1s in the command with 2s for the second public test, etc. You will have to add memory-functions.o to compilation commands for the two tests that use our heap checking functions. You can also use separate compilation, compiling each source file to form object files, which are then linked together. You are welcome to write a makefile to compile the public tests if you want (see the next section), but if you do you are advised to compile your code at least once by hand before submitting, just in case any makefile errors would result in your code compiling on Grace but not on the submit server.
A.2 Makefile considerations, checking your results, and submitting
If you do write your own makefile, remember that you will have to include the provided object file check-elephant.o to all linking rules, and add memory-functions.o to linking rules to form the executables of the two tests that use our memory checking functions. But since you aren’t being given the source files check-elephant.c or memory-functions.c your makefile should not have rules to create check-elephant.o or memory-functions.o from them, because those rules would fail. Just use check-elephant.o in all linking rules, and use memory-functions.o in the linking rules for the tests that call the heap checking functions, without having rules creating these object files.
If you do write a makefile, also make sure that your clean target does not remove all object files, otherwise it would also remove check-elephant.o and memory-functions.o, meaning you would just have to extract them from the project tarfile again. (To prevent removing check-elephant.o and memory-functions.o in a clean target, just avoid using wildcards when specifying the object files the clean target should delete– instead just explicitly list the names of all of the object files that should be removed.)
As before, use diff to compare the tests’ output to the public test outputs that are in the project tarfile, for example public01.x | diff - public01.output will test your code’s results on the first public test.
Running submit from the project directory will submit your project, but before you submit you must make sure you have passed all the public tests, by compiling and running them yourself. Unless you have versions of all required functions that will at least compile, your program will fail to compile at all on the submit server. (Suggestion– create skeleton versions of all functions when starting to code, that just have an appropriate return statement if they are non–void functions.)
A.3 Grading criteria
Your grade for this project will be based on:
public tests
85 points
secret tests
15 points
• Project–specific requirements, suggestions, and other notes
◦ There are only a few secret tests. The observant student will notice that none of the public tests check that get_name() works properly, so should be able to draw some conclusions about what may be tested in secret tests.
◦ A robust C program should always check that all memory allocations succeed, and take appropriate action if not. (The appropriate action might just be gracefully exiting the program, but it at least should not be just crashing). However, for simplicity in this project you may assume that all memory allocations always succeed.
© 2023 L. Herman; all rights reserved 4
• Be careful not to have any pointer aliasing in writing your functions.
• Do not write code using loops (or recursion) that has the same effect as any string library functions. If you need to perform an operation on strings and there is a string library function already written that accomplishes that task, you are expected to use it, otherwise you will lose credit. You are expected to use the string library functions whenever there is one that can be used.
As mentioned above, the entire project can be written without any explicit user–written loops (or recursion), so if your code has any loops at all that are operating upon strings (names) you should expect to lose significant credit.
• Do not cast the return value of the memory allocation functions or you will lose credit. Besides being completely unnecessary, in some cases this can mask certain errors in code.
• Do not include the header files check-elephant.h or memory-functions.h in your elephant.c source file. A source file should only include header files if it is using definitions that they contain. Our tests are calling the functions whose prototypes are in check-elephant.h and memory-functions.h– your functions in elephant.c do not call the functions in check-elephant.o or memory-functions.o, so your source file does not need to include these header files.
• You cannot modify anything in the three provided header files, or add anything to them, because your submission will be compiled on the submit server using our versions of them.
Your code may not comprise any source (.c) files other than elephant.c, so all your code must be in that file. You cannot write any new header files of your own either.
Do not write a main() function in elephant.c, because your code won’t compile (our tests already have main() functions). Write any tests in your own separate source files, and compile them together with elephant.c.
• You can only use the C language features that have been covered in class up through Chapter 11 in the Reek text. (You can use the string library functions in Chapter 9 even though that chapter was not covered in class, because you were told to read it on your own.)
• Keep in mind from Section 1 that you can submit this project only three times before losing credit.
If your code compiles on Grace but not on the submit server, you may have changed the provided header files, which you were not supposed to do, or something in your account setup may be wrong. Run check-account-setup and come to the TAs’ office hours for help if you can’t fix any problems that it identifies on your own. (Other causes of this could be: you put your code in a subdirectory of the project06 directory, you added source or header files, or you wrote a makefile but it is not fully correct and you did not try compiling your code by hand before submitting, to check for problems.)
• If you have a problem with your code and have to come to the TAs’ office hours, you must come with tests you have written yourself that illustrate the problem (not just the public tests), what cases it occurs in, and what cases it doesn’t occur in. In particular you will need to show the smallest test you were able to write that illustrates the problem, so whatever the cause is can be narrowed down as much as possible before the TAs even start helping you.
To emphasize: the TAs will not look at your program’s results on any of the public tests in office hours. You must have written your own tests to receive any help with your program code.
You must also have used the gdb debugger, explained recently in discussion section, and be prepared to show the TAs how you attempted to debug your program using it and what results you got.
• Academic integrity
Please carefully read the academic honesty section of the syllabus. Any evidence of impermissible cooperation on projects, use of disallowed materials or resources, publicly providing others access to your project code online, or unautho-rized use of computer accounts, will be submitted to the Office of Student Conduct, which could result in an XF for the course, or suspension or expulsion from the University. Be sure you understand what you are and what you are not permit-ted to do in regards to academic integrity when it comes to projects. These policies apply to all students, and the Student Honor Council does not consider lack of knowledge of the policies to be a defense for violating them. More information is in the course syllabus– please review it now.
The academic integrity requirements also apply to any test data for projects, which must be your own original work.
Exchanging test data or working together to write test cases is also prohibited.
© 2023 L. Herman; all rights reserved 5