Programming in C Exam Review and Practices (II)
Here is a series of general study guides to college-level C programming courses. This is the second part covering dynamic memory allocation, advanced pointer operations, recursion, linked list and tree common functions, etc.
Dynamic Memory Allocation
Given the following definitions:
1
2
3
4int *pi = NULL;
float *pf = NULL;
char *pc = NULL;
char my_string[] = "Hello, World!";write statements to do the following memory operations:
reserve space for 100 integers and assign a pointer to that space to pi
1
2pi = (int *)malloc(sizeof(int) * 100);
assert(pi != NULL);reserve space for 5 floats and assign a pointer to that space to pf
1
2pf = (float *)malloc(sizeof(float) * 5);
assert(pf != NULL);unreserve the space that pi points to
1
2free(pi);
pi = NULL;reserve space for enough characters to hold the string in my_string and assign a pointer to that space to pc. Copy my_string into that space.
1
2
3pc = (char *)malloc(strlen(my_string) + 1));
assert(pc != NULL);
strcpy(pc, mystring);free everything that hasn't been unreserved yet.
1
2
3
4free(pc);
free(pf);
pc = NULL;
pf = NULL;
What happens if you reserve memory and assign it to a pointer named p and then reserve more memory and assign the new pointer to p? How can you refer to the first memory reservation?
If you reserve then assign then reserve more memory you will have a memory leak. If you want to refer to the first pointer, you can set a new pointer to point to the new one before reserving more memory.Does it make sense to free() something twice? What's a good way to prevent this from happening?
No, it doesn’t make sense to free something twice, a good way to prevent this is setting the thing you freed to NULL after freeing it.
Advanced Pointer Operations
Suppose p is a pointer to a structure and f is one of its fields. What is a simpler way of saying:
x = (*p).f;
.1
x = p->f;
Given the following declarations and definitions:
what will the following code print?1
2
3
4struct s {
int x;
struct s *next;
};1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21struct s *p1 = NULL;
struct s *p2 = NULL;
struct s *p3 = NULL;
struct s *p4 = NULL;
struct s *p5 = NULL;
p5 = malloc(sizeof(struct s));
p5->x = 5;
p5->next = NULL;
p4 = malloc(sizeof(struct s));
p4->x = 4;
p4->next = p5;
p3 = malloc(sizeof(struct s));
p3->x = 3;
p3->next = p4;
p2 = malloc(sizeof(struct s));
p2->x = 2;
p2->next = p3;
p1 = malloc(sizeof(struct s));
p1->x = 1;
p1->next = p2;
printf("%d %d\n", p1->next->next->next->x, p2->next->x);It will print "4 3".
Write a subroutine called
do_allocate
that is passed a pointer to the head pointer to a list of block structures:do_allocate(struct block **)
. If the head pointer is NULL,do_allocate
should allocate a new struct block and make the head pointer point to it. If the head is not NULL, the new struct block should be prepended to the list, and the head pointer set to point to it.This is a linked list insertion function. New data items should always be inserted into the front of the list. Note the input argument has to be a pointer to pointer to make a change to the original head pointer. A sample solution is shown below
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct block {
int data;
struct block *next;
};
void do_allocate(struct block **head) {
struct block *new_block = malloc(sizeof(struct block));
if (new_block == NULL) {
// Handle memory allocation failure
return;
}
// Initialize the new block
new_block->data = 0;
new_block->next = *head;
// Update the head pointer
*head = new_block;
}Write a subroutine called my_free that will accept a pointer to a pointer of some arbitrary type and:
- free the space pointed to by the pointer
- set the pointer to NULL
1
2
3
4
5
6
7
8
void my_free(void **ptr) {
if (ptr != NULL && *ptr != NULL) {
free(*ptr);
*ptr = NULL;
}
}Given the following declaration:
write a subroutine called create_employee that accepts two string parameters for the new name and title and one integer parameter for the ID. It should return a newly allocated Employee structure with all of the fields filled in.1
2
3
4
5struct employee {
char *name;
char *title;
int id;
};1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
struct employee *create_employee(const char *name, const char *title, int id)
{
struct employee *new_employee = malloc(sizeof(struct employee));
if (new_employee == NULL) {
return NULL;
}
// Allocate memory for the name and copy the string
new_employee->name = malloc(strlen(name) + 1);
if (new_employee->name == NULL) {
free(new_employee);
return NULL;
}
strcpy(new_employee->name, name);
// Allocate memory for the title and copy the string
new_employee->title = malloc(strlen(title) + 1);
if (new_employee->title == NULL) {
free(new_employee->name);
free(new_employee);
return NULL;
}
strcpy(new_employee->title, title);
// Set the ID
new_employee->id = id;
return new_employee;
}Write a subroutine called fire_employee that accepts a pointer to pointer to struct employee, frees its storage and sets the pointer that points to the storage to NULL.
1
2
3
4
5
6
7
8void fire_employee(struct employee **emp_ptr) {
if (emp_ptr != NULL && *emp_ptr != NULL) {
free((*emp_ptr)->name);
free((*emp_ptr)->title);
free(*emp_ptr);
*emp_ptr = NULL;
}
}
Recursion
Create a recursive function to compute the factorial function.
1
2
3
4unsigned long long factorial(unsigned int n) {
if (n == 0) return 1;
return n * factorial(n - 1);
}Create a recursive function to compute the Nth element of the Fibonacci sequence: 0 1 1 2 3 5 8 13 21 34 55 ...
1
2
3
4
5unsigned int fibonacci(unsigned int n) {
if (n == 0) return 0;
if (n == 1) return 1;
return fibonacci(n - 1) + fibonacci(n - 2);
}Implement a recursive list search. e.g. each function call should either return the list node that it's looking at because it matches the search item or it should return the value from calling itself on the next item in the list.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15struct Node {
int data;
struct Node* next;
};
struct Node* search(struct Node* node, int value) {
if (node == NULL) return NULL;
if (node->data == value) {
return node;
} else {
// Recursive call on the next node
return search(node->next, value);
}
}
Linked List Functions
1 |
|
Tree Common Functions
1 |
|
Local, Static and Global Variables
Try the following two programs to appreciate the differences between static and non-static local variables.
1
2
3
4
5
6
7
8
9
10
11
12
13
14void try() { void try() {
int x = 0; static int x = 0;
if (x == 0) { if (x == 0) {
x = 5; x = 5;
} }
x++; x++;
printf("X = %d\n", x); printf("X = %d\n", x);
} }
int main() { int main() {
int i=0; int i=0;
for (i=0; i<10; i++) for (i=0; i<10; i++)
try(); try();
} }
// Output "X = 6" always // Output "X = 6/7/8/..."What happens if you define a global variable with a static storage class in one module and attempt to refer to that variable in a different module?
The variable will not be accessible in the other module. This is because static variables have internal linkage by default, meaning they are only accessible within the same module.Can a function be declared with a static storage class? If so, how? If not, why not?
Yes, you can declare a function with the static storage class, you can use the static keyword. It means that the function has internal linkage, which restricts its scope to the current translation unit (i.e., the source file in which it is defined). This means that the function can only be called from within the same source file, and its name is not visible outside of that file.Create a global variable in one module and, in another module use an "extern" declaration to refer to it.
module1.c 1
int globalVariable = 42;
module2.c 1
2
3
4
5
6extern int globalVariable; // Declare the global variable from module1
int main() {
printf("The value of globalVariable is: %d\n", globalVariable);
return 0;
}
Types
Under what conditions can you qualify a type as "const"?
The const keyword is used to indicate that the value of the object with that type cannot be modified.What is the difference between the following types?
1
2
3const char * cp1;
char * const cp2;
const char * const cp3;const char * cp1;
: This declares cp1 as a pointer to a constant char. It means that the data cp1 points to cannot be modified through cp1, but cp1 itself can be changed to point to a different memory location.char * const cp2;
: This declares cp2 as a constant pointer to a char. It means that cp2 always points to the same memory location, and this memory location cannot be changed. However, the data at this memory location can be modified through cp2.const char * const cp3;
: This declares cp3 as a constant pointer to a constant char. It means that both cp3 and the data it points to are constant. cp3 cannot be changed to point to a different memory location, and the data it points to cannot be modified through cp3.In summary:
- const to the left of * makes the data constant.
- const to the right of * makes the pointer constant.
- const on both sides makes both the pointer and the data constant.
Name all of the first-class types in "C".
Scalar types (e.g., int, float, double, char, void, short, long, etc.)Give an example of a derived type in "C".
Pointer types (e.g.,int *
,char *
, etc.).
Pointer to function types (e.g.,int (*)(int, int)
, a pointer to a function that takes twoint
arguments and returns anint)
An example is declaring a struct type, e.g.:
1
2
3
4
5struct person {
char name[20];
int age;
float height;
};Can you assign a float variable to an int variable?
Yes, but the value will be truncated.Can you assign an int variable to a float variable?
Yes, but the type will be promoted.Can you assign any first-class type variable to any other first-class type variable?
Yes, you just have to typecast them to the matching data type.Can you assign a first-class type variable to any kind of derived type variable?
No, e.g. you cannot assign an int to a structure
C Preprocessor and Libraries
- Review how to use the following preprocessor directives:
1 |
|
#define
is a preprocessor directive in C that unconditionally defines a macro.
#ifdef
is a preprocessor directive in the C programming language that tests whether a macro has been defined or not. It allows conditional compilation of code based on whether a particular macro has been defined or not.
#else
is run if the macro is not defined in a #ifdef
#endif
Ends a #ifdef macro
1 |
#if
is a preprocessor directive in the C programming language that allows conditional compilation of code based on the value of an expression.
Does the following program cause a compile-time error?
No, no compile time error. The macro A is defined as B and B is defined as C. So when the preprocessor replaces A in the1
2
3
4
5
6
7
8
9
10
11
int function() {
int x = 0;
return;
return 0;
}#if
directive, it replaces it with B and then replaces B with C. Therefore, the#if
statement is effectively replaced by#if (C == 1)
.Since C is defined as 1, the condition in the
#if
statement evaluates to true, and the code in the first branch of the if statement is executed, which is a return statement without a value.In this specific case, the program still works because the function return type is
int
, and the return statement in the first branch of the if statement might just return some undetermined number.In general, however, it is good practice to always explicitly return a value from a function that has a return type, as it makes the code more clear and less error-prone.
What are the reasons for using libraries?
To import useful code, promote modular programming, and provide cross-platform compatibility.What are the differences between static and dynamic (shared) libraries?
Aspects Static library Dynamic library Linking Linked at compile time Linked at run time Size Increase the size of the executable (the library code is included in the executable. Reduce the size of the executable (the library code is stored separately and referenced at run time) Memory Usage Increase memory usage (the entire library code is loaded into memory) Reduce memory usage (the code is shared among multiple processes, and only one copy of the library code is loaded into memory) Ease of Updates Require recompilation of the entire program Allow for easier updates (can replace the library file without recompiling the program) Portability More portable (does not require the presence of the library file at run time) Less portable (requires the library file to be present and correctly configured at run time) Runtime Dependencies No (directly included in the executable) Yes (must be present in the correct location for the program to run) What are the trade-offs between the above two?
The trade-offs between static and dynamic libraries involve executable size, memory usage, ease of updates, runtime dependencies, portability, and performance considerations.How do you create a library?
Compile c files into an object file and link them with1
gcc (name).o –shared –o library.so