Programming in C Exam Review and Practices (I)

Here is a series of general study guides to college-level C programming courses. This is the first part covering compilation and linking, file operations, typedef, structures, string operations, basic pointer operations, etc.

Compilation and Linking

  • Write the command to compile a single C file named "hello.c" into an object file called "hello.o".

    gcc -c hello.c -o hello.o

  • Write the command to link two object files named "hello.o" and "goodbye.o" into the executable called "application".

    gcc hello.o goodbye.o -o application

  • Can you "run" an object file if it contains the "main()" function?

    No, an object file cannot be run directly. If you force it to run, it will exec format error.

  • Can you "run" an executable that contains a single function called "main()"?

    Yes, an executable with just main() can be run.

  • Can you "run" an executable that does not contain a function called "main()"?

    No, main() is required to run an executable.

  • What does the "-Wall" flag do?

    "-Wall" enables all compiler warnings

  • What does the "-g" flag do?

    "-g" adds debugging information.

  • What does the "-ansi" flag do?

    "-ansi" enables strict ANSI C mode. The "-ansi" flag is equivalent to the -"std=c89" flag.

  • What does the "-c" flag do?

    "-c" compiles to object file only, does not link.

  • What does the "-o" flag do?

    "-o" specifies output file name.

    • If "-c" is also used with a single [filename].c file, and no other .o in the command line, gcc will default generate an object file named [filename].o. If "-o" is used in such a case, it will create an object file with the specified name.
    • If no "-c" is used, gcc will by default create an executable file named "a.out".

File Operations

  • Given the following FILE pointer variable definition, write the code that will open a file named "hello.txt" for read-only access and print a message of your choice if there was an error in doing so.

    FILE *my_file = 0;

    1
    2
    3
    4
    my_file = fopen("hello.txt", "r");
    if (my_file = NULL) {
    fprintf(stdout, "Failed to open the file\n");
    }

  • Write code that will, without opening any file, check if a file named "hello.txt" can be opened for read access. Put the code inside the 'if' predicate:

    1
    2
    3
    if (access("hello.txt", R_OK) == 0) {
    /* Yes, we can open the file... */
    }

  • Write code that will, without opening any file, check if a file named "hello.txt" can be opened for write access. Put the code inside the 'if' predicate:

    1
    2
    3
    if (access("hello.txt", W_OK) == 0) {
    /* Yes, we can open the file... */
    }

  • Write a function called read_and_print() that will do the following:

    • Open a text file called "hello.txt" for read-only access.
    • Read a word that is terminated by a newline from the file into the character array called "my_string".
    • Read an integer terminated by a newline into the int variable called "my_int".
    • Print the string and the integer value.
    • Return the my_int value.
    • If the file cannot be opened for reading, return -1.
    • If an error occurs while reading from the file, return -1.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    int read_and_print() {
    char my_string[100];
    my_int;

    FILE *fp = fopen("hello.txt", "r");
    if(!fp) return -1;

    if (fscanf(fp, "%s", my_string) != 1) {
    fclose(fp);
    fp = NULL;
    return -1;
    }
    if (fscanf(fp, "%d", &my_int) != 1) {
    fclose(fp);
    fp = NULL;
    return -1;
    }
    printf("%s %d\n", my_string, my_int);
    fclose(fp);
    fp = NULL;
    return my_int;
    }

  • Write a function named print_reverse that will open a text file named "hello.txt" and print each character in the file in reverse. i.e. print the first character last and the last character first. The function should return the number of characters in the file. Upon any error, return -1. HINT: Use fseek() a lot to do this.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    int print_reverse(char* filename) {

    FILE* fp = fopen(filename, "r");
    if(!fp) return -1;

    fseek(fp, 0, SEEK_END);
    int size = ftell(fp);

    for (int i = size - 1; i >= 0; i--) {
    fseek(fp, i, SEEK_SET);
    char c = fgetc(fp);
    printf("%c", c);
    }

    fclose(fp);
    fp = NULL;
    return size;
    }

  • Write a function that defines a structure, initializes it, writes it to a file called "struct.out", closes the file, re-opens the file for read-only access, reads a single structure into a new struct variable, and then closes the file. Print the structure contents to the screen. On any error, return -1. Otherwise, return 0.

    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
    33
    34
    35
    36
    37
    38
    #include <stdio.h>

    struct Person {
    char name[50];
    int age;
    };

    int write_and_read_struct() {
    struct Person p = { "John Doe", 30 };

    // Write struct to file
    FILE* fp = fopen("struct.out", "w");
    if (!fp) return -1;

    if (fwrite(&p, sizeof(struct Person), 1, fp) != 1) {
    fclose(fp);
    fp = NULL:
    return -1;
    }
    fclose(fp);

    // Read struct from file
    fp = fopen("struct.out", "r");
    if (!fp) return -1;

    struct Person p2;
    if (fread(&p2, sizeof(struct Person), 1, fp) != 1) {
    fclose(fp);
    fp = NULL;
    return -1;
    }
    fclose(fp);
    fp = NULL;

    // Print struct
    printf("Name: %s, Age: %d\n", p2.name, p2,age);
    return 0;
    }

Typedef

  • Declare a type called "my_array_t" that is an array of 15 floats.

    1
    typedef float my_array_t[15];

  • Declare a type called "struct_arr_t" that is an array of 10 structs of the format

    1
    2
    3
    4
    struct str {
    int x;
    int y;
    };

    1
    typedef struct str struct_arr_t[10];

  • Define a variable called my_str_arr of type struct_arr_type.

    1
    struct_arr_t my_str_arr;

Structures

  • Can two elements within a structure have the same name?

    No, two elements cannot have the same name

  • Can you initialize a structure like this?

    1
    2
    3
    4
    struct my_str {
    int x;
    float y;
    } mine = { 0, 0.0 };
    Yes, you can initialize it like that.

  • Can you initialize a structure like this?

    1
    2
    3
    4
    5
    6
    7
    struct my_str {
    int x;
    float y;
    };
    void my_func(int n) {
    my_str mine = { n, 0.0 };
    }
    No, here my_str is not a type. To fix this, use struct str mine = { n, 0.0 }; instead.

  • Declare a structure that contains an integer element named i, a floating point element named f, and an array of 20 characters named str (in that order). Name it anything you want.

    1
    2
    3
    4
    5
    struct mystruct {
    int i;
    float f;
    char str[20];
    };

  • Define a variable called "my_new_struct" of the type in the previous question.

    1
    struct mystruct my_new_struct;

  • Define a variable called "my_array_of_structs" that is an array of 40 structures of the type in the prior two questions.

    1
    struct mystruct my_array_of_structs[40];

  • Define a function called bigger_rectangle() that will accept one argument of the structure type rectangle (declared below) and will multiply the width dimension by 1.5, the height dimension by 2.5 and the length dimension by 3. The function should return the new structure. Define a temporary local variable if you want to.

    1
    2
    3
    4
    5
    struct rectangle {
    float height;
    float width;
    float length;
    };

    1
    2
    3
    4
    5
    6
    7
    struct rectangle bigger_rectangle(struct rectangle r) {
    struct rectangle bigger;
    bigger.height = r.height * 2.5;
    bigger.width = r.width * 1.5;
    bigger.length = r.length * 3;
    return bigger;
    }

  • Write a function named sum_rectangles that will open a binary file named "rect.in" for reading and read the binary images of rectangle structures from it. For each rectangle structure, add its elements to those of the first structure read. e.g. sum the height fields of all the structures, sum the width fields of all the structures, etc... Return a structure from sum_rectangles where each element represents the sum of all structures read from the file. i.e. the height field should be the sum of all of the height fields of each of the structures. On any file error, return the structure { -1.0, -1.0, -1.0 }.

    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
    33
    34
    35
    36
    37
    38
    39
    #include <stdio.h>

    struct rectangle {
    float height;
    float width;
    float length;
    };

    struct rectangle sum_rectangles() {
    struct rectangle bad_struct = {-1.0, -1.0, -1.0};

    FILE *fp = fopen("rect.in", "rb");
    if(!fp) {
    return bad_struct;
    }

    struct rectangle sum = {0, 0, 0};
    struct rectangle r;

    if (fread(&r, sizeof(struct rectangle), 1, fp) != 1) {
    fclose(fp);
    fp = NULL;
    return bad_struct;
    }

    sum.height = r.height;
    sum.width = r.width;
    sum.length = r.length;

    while (fread(&r, sizeof(struct rectangle), 1, fp) == 1) {
    sum.height += r.height;
    sum.width += r.width;
    sum.length += r.length;
    }

    fclose(fp);
    fp = NULL;
    return sum;
    }

assert()

  • Under what circumstances would you place an assert() into your code?

    Used to check for logical errors and malformed data.

  • What will be the result of the following code:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    int my_func() {
    int count = 0;
    int sum = 0;

    for (count = 0; count < 100; count++) {
    assert(sum > 0);
    sum = sum + count;
    }
    return sum;
    }
    The program will abort/crash on the assert line.

  • What might you do to the previous code to make it do a "better" job?

    Move assert(sum > 0); down, after for loop. Or change to assert(sum >= 0);

String Operations

  • Write a function called do_compare() that will prompt the user for two strings of maximum length 100. It should compare them and print one of the following messages:

    • The strings are equal.
    • The first string comes before the second.
    • The second string comes before the first.

    The function should always return zero.

    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
    #include <stdio.h>
    #include <string.h>

    int do_compare() {
    char str1[101], str2[101];

    // Prompt the user to enter two strings
    printf("Enter the first string (up to 100 characters): ");
    fgets(str1, sizeof(str1), stdin);

    printf("Enter the second string (up to 100 characters): ");
    fgets(str2, sizeof(str2), stdin);

    // Compare the strings
    int cmp = strcmp(str1, str2);

    // Print the comparison result
    if (cmp == 0) {
    printf("The strings are equal.\n");
    } else if (cmp < 0) {
    printf("The first string comes before the second.\n");
    } else {
    printf("The second string comes before the first.\n");
    }

    return 0;
    }

Variables

  • What is the difference between initialization of a variable and assignment to a variable?

    Initialization is giving a variable its initial value, typically at the time of declaration, while assignment is giving a new value to an already declared variable at any point after initialization.

  • What is the difference between a declaration and a definition?

    Declaration is announcing the properties of var (no memory allocation), definition is allocating storage for a var and initializing it.

  • What is the difference between a global variable and a local variable?

    Global variables have a broader scope, longer lifetime, and higher visibility compared to local variables, which are limited to the scope of the function in which they are declared.

  • For the following questions, assume that the size of an 'int' is 4 bytes, the size of a 'char' is one byte, the size of a 'float' is 4 bytes, and the size of a 'double' is 8 bytes. Write the size of the following expressions:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    struct my_coord {
    int x;
    int y;
    double altitude;
    };

    struct my_line {
    struct my_coord first;
    struct my_coord second;
    char name[10];
    };

    struct my_coord var;
    struct my_coord array[3];
    struct my_line one_line;
    struct my_line two_lines[2];

    sizeof(struct my_coord) = __16___

    sizeof(var) = __16___

    sizeof(array[1]) = __16___

    sizeof(array[2]) = __16___

    sizeof(array) = __48___

    sizeof(struct my_line) = __48___

    sizeof(two_lines) = __96___

    sizeof(one_line) = __48___

    Explanation: When calculating the size of a struct, we need to consider alignment and padding, which can affect the overall size of the struct. In the case of struct my_line, the total size is influenced by the alignment requirements of its members. The largest member of struct my_coord is double altitude, which is 8 bytes. This means that the double altitude member will determine the alignment and padding for the entire struct my_coord within struct my_line.

    So here char name[10]; will occupy (10 bytes) + (6 bytes padding to align char[10] on an 8-byte boundary). This ends up with (16+16+10+6) for the size of struct my_line.

    Remember that the size of the structure should be a multiple of the biggest variable.

  • Draw the memory layout of the prior four variables; var, array, one_line, and two_lines on a line of boxes. Label the start of each variable and clearly show how many bytes each element within each structure variable consumes.

  • Re-define the two_lines variable above and _initialize_ it's contents with the following values:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    first my_line structure:
    first my_coord structure:
    x = 1
    y = 3
    altitude = 5.6
    second my_coord structure:
    x = 4
    y = 5
    altitude = 2.1
    name = "My Town"
    second my_line structure:
    first my_coord structure:
    x = 9
    y = 2
    altitude = 1.1
    second my_coord structure:
    x = 3
    y = 3
    altitude = 0.1
    name = "Your Town"

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    struct my_line two_lines[2] = {
    {
    {1, 3, 5.6},
    {4, 5, 2.1},
    "My Town"
    },
    {
    {9, 2, 1.1},
    {3, 3, 0.1},
    "Your Town"
    }
    };

  • How many bytes large is the following definition?

    1
    2
    3
    4
    5
    struct my_coord new_array[] = {
    { 0,0,3.5 },
    { 1,2,4.5},
    { 2,0,9.5}
    };

    (4 + 4 + 8) * 3 = 48

Basic Pointer Operations

  • What is printed by the following three pieces of code:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    int x = 0;                int x = 0;                int x = 0;
    int y = 0; int y = 0; int y = 0;
    int *p = NULL; int *p = NULL; int *p = NULL;
    int *q = NULL; int *q = NULL;
    p = &x;
    *p = 5; p = &x; p = &y;
    p = &y; q = p; q = &x;
    *p = 7; *q = 7; p = 2;
    q = 3;
    printf("%d %d\n", x, y); printf("%d %d\n", x, y); printf("%d %d\n", x, y);

    The 1st column code snippet printed 5 7. The 1st column code snippet printed 7 0. The 1st column code snippet printed 0 0.

  • Consider the following variable definitions:

    1
    2
    3
    int x = 2;
    int arr[10] = {4, 5, 6, 7, 1, 2, 3, 0, 8, 9};
    int *p;

    And assume that p is initialized to point to one of the integers in arr. Which of the following statements are legitimate? Why or why not?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    p = arr;      arr = p;      p = &arr[2];   p = arr[x];     p = &arr[x];

    arr[x] = p; arr[p] = x; &arr[x] = p; p = &arr; x = *arr;

    x = arr + x; p = arr + x; arr = p + x; x = &(arr+x); p++;

    x = --p; x = *p++; x = (*p)++; arr++; x = p - arr;

    x = (p>arr); arr[*p]=*p; *p++ = x; p = p + 1; arr = arr + 1;

    Let's go through each statement to determine if it is legitimate or not, and explain:

    • p = arr; - Legitimate. Assigns the address of the first element of arr to p.
    • arr = p; - Not legitimate. You cannot assign to an array name.
    • p = &arr[2]; - Legitimate. Assigns the address of arr[2] to p.
    • p = arr[x]; - Not legitimate. arr[x] is an integer value, not an address.
    • p = &arr[x]; - Legitimate. Assigns the address of arr[x] to p.
    • arr[x] = p; - Not legitimate. arr[x] is an integer value, not a pointer.
    • arr[p] = x; - Not legitimate. arr[p] is not a valid operation. p should be an index, not a pointer.
    • &arr[x] = p; - Not legitimate. You cannot assign a value to the address of an element.
    • p = &arr; - Not legitimate. &arr is the address of the whole array, not a pointer to an integer.
    • x = *arr; - Legitimate. Assigns the value of the first element of arr to x.
    • x = arr + x; - Legitimate. Calculates the address of arr[x] and assigns it to x.
    • p = arr + x; - Legitimate. Calculates the address of arr[x] and assigns it to p.
    • arr = p + x; - Not legitimate. You cannot assign to an array name.
    • x = &(arr+x); - Not legitimate. & expects an lvalue, but (arr+x) is not an lvalue.
    • p++; - Legitimate. Increments the pointer p to point to the next element.
    • x = --p; - Legitimate. Decrements p and assigns its value to x.
    • x = *p++; - Legitimate. Assigns the value pointed to by p to x, then increments p.
    • x = (*p)++; - Legitimate. Assigns the value pointed to by p to x, then increments the value pointed to by p.
    • arr++; - Not legitimate. You cannot increment the entire array arr.
    • x = p - arr; - Legitimate. Calculates the difference in addresses between p and arr and assigns it to x.
    • x = (p>arr); - Not legitimate. Comparison between a pointer and an array is not valid.
    • arr[*p]=*p; - Not legitimate. arr[*p] is not a valid assignment target.
    • *p++ = x; - Legitimate. Assigns x to the value pointed to by p, then increments p.
    • p = p + 1; - Legitimate. Increments the pointer p to point to the next memory location.
    • arr = arr + 1; - Not legitimate. You cannot increment the entire array arr.

    📝Notes: The difference between x = *p++; and x = (*p)++; lies in how the increment operator (++) is applied.

    • x = *p++; This statement first dereferences the pointer p to get the value it points to, assigns that value to x and then increments the pointer p to point to the next element (not the value pointed to by p). So, x gets the value pointed to by p before the increment.
    • x = (*p)++; This statement first dereferences the pointer p to get the value it points to, assigns that value to x, and then increments the value pointed to by p. So, x gets the value pointed to by p before the increment, and the value at the memory location pointed to by p is incremented.

    Here's a brief example to illustrate the difference:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #include <stdio.h>

    int main() {
    int array[] = {1, 2, 3};
    int *p = array;
    int x;

    // x gets the value pointed to by p, then p is incremented
    x = *p++; // x = 1, p now points to array[1]
    printf("x = %d, array[1] = %d, p points to %d\n", x, array[1], *p);

    // x gets the value pointed to by p, then the value pointed to
    // by p is incremented
    x = (*p)++; // x = 2, array[1] is now 3
    printf("x = %d, array[1] = %d, p points to %d\n", x, array[1], *p);
    return 0;
    }

    The output of the above program is

    1
    2
    x = 1, array[1] = 2, p points to 2
    x = 2, array[1] = 3, p points to 3

    To test your understanding, now check the following code snippet, what will the output be:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    int x = 2, y = 15, z = 0;
    int *p = 0;

    p = &y;
    x = *p++;
    printf("x = %d, y = %d, z = %d\n", x, y, z);

    p = &y;
    z = (*p)++;
    printf("x = %d, y = %d, z = %d\n", x, y, z);

    Answer

    1
    2
    x = 15, y = 15, z = 0
    x = 15, y = 16, z = 15

    So the variable y has its value incremented after z = (*p)++;.

  • Given the following definitions:

    1
    2
    int arr[] = { 0, 1, 2, 3 };
    int *p = arr;
    are the following two statements equivalent?

    1
    2
    p = p + 1;
    p++;
    What can you say about the result of adding a pointer to an integer?

    Yes, the two statements p = p + 1; and p++; are equivalent in this context. Both statements increment the pointer p to point to the next element in the array arr.

    In general, if ptr is a pointer to type T, then ptr + n will point to the memory location "ptr + n * sizeof(T)". This is useful for iterating over arrays or accessing elements in memory sequentially.

  • Write a function called 'swap' that will accept two pointers to integers and will exchange the contents of those integer locations.

    • Show a call to this subroutine to exchange two variables.

      Here is the sample code:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      #include <stdio.h>

      void swap(int *a, int *b) {
      int temp = *a;
      *a = *b;
      *b = temp;
      }

      int main() {
      int x = 5, y = 10;

      printf("Before: x = %d, y = %d\n", x, y);
      swap(&x, &y);
      printf("After: x = %d, y = %d\n", x, y);
      return 0;
      }

    • Why is it necessary to pass pointers to the integers instead of just passing the integers to the Swap subroutine?

      It is necessary to pass pointers to the integers instead of just passing the integers themselves to the swap subroutine because C passes arguments by value. When you pass an integer to a function, a copy of the integer's value is made and passed to the function. Any changes made to the parameter inside the function do not affect the original variable outside the function.

      By passing pointers to integers (int *a and int *b), you are passing the memory addresses of the integers. This allows the swap function to access and modify the actual integers in memory, rather than working with copies. As a result, the values of the integers are swapped correctly, and the changes are reflected outside the function.

      In summary, passing pointers to integers allows the swap function to modify the values of the integers themselves, rather than just copies of the values.

    • What would happen if you called swap like this:

      1
      2
      int x = 5;
      swap(&x, &x);

      If you called swap(&x, &x); with the same pointer &x for both arguments, it would effectively try to swap the contents of x with itself. The result would be that x would remain unchanged, as the swap operation would effectively cancel itself out. The swap operation had no net effect on x.

    • Can you do this: (why or why not?)

      1
      swap(&123, &456);
      No, you cannot do this because &123 and &456 are not valid addresses in memory. 123 and 456 are constants, not variables, so you cannot take their addresses for swapping the content.

  • What does the following code print:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    int func() {
    int array[] = { 4, 2, 9, 3, 8 };
    int *P = NULL;
    int i = 0;

    p = &array[2];
    p++;
    printf("%d\n", *(p++));
    *(--p) = 7;

    (*p)++;
    for (i = 0; i < (sizeof(array)/sizeof(int)); i++) {
    printf("%d ", array[i]);
    }
    }

    The output is

    1
    2
    3
    4 2 9 8 8

    Explanation:

    • Initially, p points to array[2] which is 9.
    • After p++, p points to array[3] which is 3. The value 3 is printed.
    • Then, *(--p) = 7; sets array[3] to 7.
    • Next, (*p)++; increments the value at array[3] (which is now 7) to 8.
    • Finally, the for loop prints the elements of the array, which are 4 2 9 8 8.
  • Write a subroutine called clear_it that accepts a pointer to integer and an integer that indicates the size of the space that the pointer points to. clear_it should set all of the elements that the pointer points to to zero.

    1
    2
    3
    4
    5
    void clear_it(int *ptr, int size) {
    for (int i = 0; i < size; i++) {
    *(ptr + i) = 0;
    }
    }

  • Write a subroutine called add_vectors that accepts three pointers to integer and a fourth parameter to indicate the size of the spaces that the pointers point to. add_vectors should add the elements of the first two 'vectors' together and store them in the third 'vector'. e.g. if two arrays of 10 integers, A and B, were to be added together and the result stored in an array C of the same size, the call would look like add_vectors(a, b, c, 10); and, as a result, c[5] would be the sum of a[5] and b[5]

    All four implementations below are equivalent solutions to this problem:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    void add_vectors(int *a, int *b, int *c, int size) {
    for (int i = 0; i < size; i++) {
    c[i] = a[i] + b[i];
    }
    }

    void add_vectors1(int *a, int *b, int *c, int size) {
    int *end = c + size;
    while (c < end) {
    *c++ = *a++ + *b++;
    }
    }

    void add_vectors2(int *a, int *b, int *c, int size) {
    for (int i=0; i<size; i++) {
    *c++ = *a++ + *b++;
    }
    }

    void add_vectors3(int *a, int *b, int *c, int size) {
    for (int i=0; i<size; i++) {
    *(c+i) = *(a+i) + *(b+i);
    }
    }