07.md

Warm-up

Print a circle

Print a circle of a chosen radius. Use a define for the radius so that you can test other sizes. This is for 12, for example:

$ ./a.out
            *
        **** ****
      **         **
     *             *
    *               *
   *                 *
  *                   *

 *                     *



*                       *



 *                     *

  *                   *
   *                 *
    *               *
     *             *
      **         **
        **** ****
            *

Hint: an equation for a circle is x^2 + y^2 = MYRAD^2, ie.

y = sqrt(MYRAD * MYRAD - x * x);

Obviously, there are more ways how to do it.

  • The easiest way is to use a two-dimensional array and go through the x axis and fill the corresponding y value before printing out the array line by line.
    • To print the array, instead of printing it a character by character, we can also use array lines as strings if we terminate each line first with a \0.
  • You could print the output line by line, that is, figure out what to print on each line. You do not need a two-dimensional array then, and speaking of which, not even an array at all.

You can find the sqrt() function declaration in <math.h>. You can also use memset() from string.h to set a piece of memory to a specific byte (= character).

On Linux and possibly other systems, you will need to link with the math library to get the sqrt() function code, i.e:

$ gcc -lm warm-up.c

An extension: fill the circle up.

$ ./a.out
            *
        *********
      *************
     ***************
    *****************
   *******************
  *********************
  *********************
 ***********************
 ***********************
 ***********************
 ***********************
*************************
 ***********************
 ***********************
 ***********************
 ***********************
  *********************
  *********************
   *******************
    *****************
     ***************
      *************
        *********
            *

🔑 circle.c

A simplified solution:

🔑 circle-simplified.c

File API

Part of the standard since C90.

Opening/closing

  • The fopen("path", "mode") opens a file and returns a pointer to an opaque FILE type. That pointer serves as a handle.
    • Getting NULL means an error.
    • The mode argument controls the behavior: read (r), write (w), append (a).
      • The + adds the other mode (write for read and vice versa, read for append).
      • Write mode w creates the file if it does not exist, and truncates it if it does exist.
    • The fclose closes the handle - Important to avoid resource leak (fopen can allocate both memory and file descriptor).
FILE *fp;

/* Choose any other file you have on your system. */
if ((fp = fopen("/etc/passwd", "r")) == NULL)
	err(1, "fopen");

/* process the file... */

if (fclose(fp) != 0)
	err(1, "fclose");
  • The b binary mode usually does not have any effect.
  • The freopen can be used to associate the standard streams (stderr, stdin, or stdout) with a file
    • That means e.g. reading the standard input would automatically read from a specific file, if you wanted that.
    • printf(...) is equivalent to fprintf(stdout, ...). However, you can print directly to stderr with fprintf.
fprintf(stderr, "Error happened: %s\n", "some error");

🔧 Write a code that opens the same file in an cycle (until fopen() fails) without calling fclose() on the handle. After how many iterations does it fail on your system?

🔑 fopen-leak.c

I/O

  • fprintf works like printf but prints to a specific stream
  • fscanf
    • Basically parses text input from a stream according to format string
    • After the format string, all argument must be pointers.
  • fputs/fgets - send/read string to/from a stream
fputs("hello, world\n", stdout);
  • fputc/fgetc - send/read char to/from a stream
fputc('x', stderr);
  • fwrite/fread - for writing/reading binary data (such as structures or raw numeric types)

  • feof/ferror - determine if the last I/O operation hit end of file or error.

Read a file

The fread() reads a selected number of items of a given size to memory. We can use either an array or we can directly read to a variable through an operator address-of. In our case, we will be reading a file byte by byte, so we can give fread() just an address of a character variable.

You can ignore the restrict keyword, it is intended as a hit for a compiler optimization, and also just accept now that we can assign any pointer to a void * pointer.

size_t fread(void *restrict ptr, size_t size, size_t nmemb,
	     FILE *restrict stream);

Now we will read the file in chunks of one byte only. Therefore we can use an address-of operator on a char object.

char c;
FILE *fp;

/* Choose any other file you have on your system. */
if ((fp = fopen("/etc/passwd", "r")) == NULL)
	err(1, "fopen");

/*
 * fread() returns a number of *items* read.  In our case, it's the same as
 * number of bytes as we read it one byte at a time.
 */
while (fread(&c, sizeof (c), 1, fp) == 1) {
	putchar(c);
}

fclose(fp);

👀 read-file.c

🔧 Note that you could read more characters at a time. However, keep in mind the 2nd argument is the size of the element read, and the 3rd argument is how many elements we read in one call. Remember that using an array name a is the same as &a[0]. For example:

char a[16];
...
while ((n = fread(a, sizeof (a[0]), sizeof (a) / sizeof (a[0]), fp)) > 0) {
	/* Process the bytes here. */

	/* If we read less elements than requested, we hit end of file or error. */
	if (n < (sizeof (a) / sizeof (a[0])))
		break;
}

What could happen if reading bytes into character array and the 2nd and 3rd arguments are swapped ?

👀 read-file2.c

Check manual page for fread() and ignore for now that the 1st argument is of type void *, we will get there later. As mentioned above, you can safely put there an array or an address of a variable.

🔧 Check the man page for fwrite() and modify the code so that what is read from the file you write to some other file. The file should be created if it does not exist. If it exists, its contents should be truncated. Do not forget to open the output file for writing. All the details are in the man page.

🔑 file-copy.c

Seeking

When reading/writing to a file using the above function, the current position changes accordingly. However, the position can be manipulated without performing any I/O.

  • fseek moves the position.
    • The whence parameter has 3 possible values and makes the offset parameter relative to:
      • SEEK_SET - the beginning of the file
      • SEEK_END - the end of the file
      • SEEK_CUR - the current location of the cursor in the file
  • ftell returns the current position in the file.

🔧 By seeking, write code that creates a file that has every 4-th character a upper case letter of alphabet (A to Z) and then prints the contents of the file byte by byte. Print the non-printable bytes in hexadecimal (with 0x prefix).

🔑 file-AZ.c

Pointer size

int *p;

sizeof (p) is a size of a p variable which holds an address, which corresponds to the address size. In our case either 32 or 64 bits depending on how your code was compiled.

#include <stdio.h>

int
main(void)
{
	int *p;

	printf("%zu\n", sizeof (p));
}
$ cc main.c
$ file a.out
a.out: Mach-O 64-bit executable x86_64
$ ./a.out
8

Here the sizeof (*p) is equivalent to sizeof (int) as that is what the type of *p is.

Arrays and pointers

In C, there is a strong relationship between arrays and pointers. Most of the time, you can use array notation with pointers, and pointer notation with arrays, but not always.

By definition, the value of an array identifier or an expression of an array type is the address of the first array element (an element zero).

int *pa;
int a[4] = { 1, 2, 3, 4 };

pa = &a[0];     // means &(a[0]) as [] is of higher precedence than &
pa = a;         // this gets the same result as above

p[0] = 3;	// the array is now { 3, 2, 3, 4 }
*(a + 1) = 5;	// the array is now { 3, 5, 3, 4 }

❗ As you can see, incrementing a pointer increments the value by the size of the object the pointer points to. That was already mentioned in the pointer intro. Another example:

int a[] = { 0, 1, 2 };
int *p = a;

printf("%d\n", *(p + 2));       // will print 2

👀 ptr-inc.c

❗ While you can work with arrays and pointers together, arrays are not pointers and pointers are not arrays. To work with a pointer, the generated code must first get the value of the pointer identifier, then work with it. With arrays, the generated code directly uses the memory address the array starts at. See below.

Array identifiers (i.e. variables) are not modifiable

As just said, the generated code directly uses the memory address of an array. So, you cannot do the following:

int a[1];
int aa[1];
++a;            // error
a = aa;         // error

When assigning to an array element, the compiler generates code that assigns to a piece of memory representing the element using an offset from the beginning of the array.

int a[1];

a[0] = 15;

The generated assembler code (e.g. objdump -d from LLVM) for the above assigns 15 directly to the piece of memory on the stack (started by the base pointer rbp) that represents the first array element. That also hints that one cannot assign to an array identifier in C as it would need a special semantics that would not fit the way C works with arrays.

100000fa6:      c7 45 fc 0f 00 00 00    movl    $15, -4(%rbp)

👀 array-var-not-modifiable.c

Strings, arrays, and pointers

We already went through basics on strings and string literals.

Given that a string constant is internally used to initialize an array of chars, and an array name represents its first element memory address, the value of a string is an address of its first character.

👀 string-literal-address.c

We already know that one can use an array notation with pointers. So, if you really wanted, you could do something like this:

printf("%c\n", "hello, world"[1]);	// will print `e`

or even this:

printf("%c\n", 1["hello, world"]);	// will also print `e`

Strings and pointers

Given that a string constant is a pointer to its first character, we can use it like this:

char *p = "hello";

printf("%c\n", p[1]);   // will print 'e'

👀 array-notation-with-ptr.c

❗ Pointer initialized with a string literal may not be changed in the same way as an array. The internal array created and initialized from the string literal is read-only by the specification. Writing to it is an undefined behavior. Writing to it with gcc and clang will crash the program. However, for example, Oracle Developer Studio used to put such arrays into read-write memory by default in older versions. So, working code compiled with an older version of that compiler and modifying string literals would crash if compiled with those other two.

👀 string-literal-write.c

And as you can use a pointer notation with arrays (we already mentioned that the array name is a synonym for the address of its first element), you can do this:

char a[] = "hello, world";

printf("%c\n", *(a + 1));       // will print 'e'

👀 ptr-with-array-notation.c

Valid pointer operations

Valid pointer operations are:

  • Assignment of pointers of the same type.
  • Adding and subtracting a pointer and an integer.
  • Subtracting and comparing two pointers to members of the same array.
  • Assigning and comparing a pointer to zero.

All other pointer arithmetic are invalid operations and may or may not trigger a warning.

More specifically, assigning an integer to a pointer is not a valid operation, usually you get a warning.

int
main(void)
{
	int *p = 0x1234;        // is not a valid behavior
}
$ cc int-to-ptr.c
int-to-ptr.c:4:7: warning: incompatible integer to pointer conversion initializing 'int *' with an
      expression of type 'int' [-Wint-conversion]
        int *p = 0x1234;        // is not a valid behavior
             ^   ~~~~~~
1 warning generated.

👀 int-to-ptr.c

Arrays as function arguments

❗ When an array name is passed to a function, what is actually passed is the address of the first element and the local variable representing the argument within the function is of a type of pointer to the array element. So, you can just declare the argument as an pointer to an element type and pass in an array name.

The reason for this is that as C can only pass arguments by value, copying array data to function arguments would be quite inefficient.

Even if you declare a function argument as an array, the argument is always treated as a pointer. The optional array size is accepted but ignored. Do not use it as it is only confusing. See for example 6.9.1 Function definitions, paragraph (10) in C99.

void
myfn(int a[])
{
	/*
	 * 'a' here is a pointer to int, not an array of ints.  So, you can
	 * assign to it, for example (which you cannot do with an array).
	 */
	a = NULL;
}

👀 array-as-argument.c

Some compilers even notice that the sizeof operator is applied on an array passed into a function and produce a warning about it (clang with -Wsizeof-array-argument).

$ clang -Wsizeof-array-argument array-as-argument.c
array-as-argument.c:15:25: warning: sizeof on array function parameter will
    return size of 'char *' instead of 'char [20]' [-Wsizeof-array-argument]
        printf("%zu\n", sizeof (a));
                               ^
array-as-argument.c:12:11: note: declared here
myfn(char a[20])
          ^
1 warning generated.

However, it only applies to the first array dimension. That is, a multi-dimensional array is just a 1-dimensional array of elements that are of an array type. Note that we are getting ahead of ourselves a little bit here, more on that will be later, especially on reading more complex declarations.

👀 passing-arrays.c

$ cc src/passing-arrays.c
src/passing-arrays.c:14:14: warning: incompatible pointer types passing 'int (*)[3]' to parameter of
      type 'int (*)[2]' [-Wincompatible-pointer-types]
        myfn(p2);       // will trigger a warning
             ^~
src/passing-arrays.c:2:10: note: passing argument to parameter 'a' here
myfn(int a[2][2])
         ^
1 warning generated.

Calling by value vs calling by reference

In C, function arguments are always passed by value. To change a value of an argument, thus emulating a calling by reference, you need to pass pointers. That means, you pass a pointer by value so that you know what piece of memory to modify.

The following will not do what one might naively assume it would. What you actually swap are values on a stack or in registers while the original objects were left untouched.

int
swap(int a, int b)
{
	int n = a;

	a = b;
	b = n;
}

👀 swap-by-value.c

🔧 modify the code above so that swap() that actually swaps the values of the two ints. Hint: aside from the dererence operator *, you will also need the address-of operator & - see pointer basics

🔑 swap-by-reference.c

🔧 Home assignment

Note that home assignments are entirely voluntary but writing code is the only way to learn a programming language.

File of integers

🔧 Get a file size using the standard IO API (that is, lseek(2) is prohibited even if you know it).

🔧 Create an array of int values (of arbitrary positive length with values ranging from INT_MAX to 0), write the array to a file, read the values into another array and print them to the standard error output. Between the writing and reading, the file handle has to remain open. Use the same file handle for reading and writing. Use xxd(1) (or od(1), hexdump(1)) to verify the content of the file. Thus it is handy to start with INT_MAX and e.g. divide by 2 for each successive value.

🔑 fopen-binary.c

🔧 Use the file created by the previous program. Read the values from the end of the file to the beginning of the file one by one without knowing the file size and print the numbers to the standard error output.

File read

🔧 create a text file where each line begins with integer followed by space and a string, e.g.:

42 towel
13 dwarfs and Snow White

Read the file using fscanf() and print the values (i.e. integer and a string) from each line to standard output.

Implement a function to copy string to array

Write a function that copies a string to a character array, including the terminating null character. It returns the number of characters copied.

Note that a string constant is read-only by definition, and writing to it is an undefined behavior. That is:

char *s = "hello"
// Do not do this.  Different compilers may act differently.
s[0] = 'H';

To write the function, take :eyes: tasks/string-to-array-copy.c and implement it there. See the comment inside.

Verify the code prints exactly what it says in the comments. gcc -Wall -Wextra implement-function.c must be clear of any warnings.

🔑 tasks/string-to-array-copy-solution.c