07.md

Warm-up

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>.

On Linux, you will need to link with the math library to get the sqrt() function code, ie:

$ gcc -lm warm-up.c

An extension: fill the circle up.

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

Solution: 👀 circle.c

Pointer size

int *p;

sizeof (p) is an 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

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.

By definition, the value of a variable or an expression of type array is the address of an element zero of the array.

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

pa = &a[0];     // [] 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 }

heavy_exclamation_mark: As you can see, incrementing a pointer increments the value by the size of the object the pointer points to. 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 variable, then work with it. With arrays, the generated code directly works with the memory address the array starts at. See below.

Array variables are not modifiable

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 variable 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 adress of its first character.

👀 string-const-address.c

You 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`

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 array. The internal array created and initialized from the string literal is in 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 puts such arrays into read-write memory. So, working code complied with that compiler and modifying string literals will crash if compiled 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 or subtracting a pointer and an integer
  • subtracting or comparing two pointers to members of the same array
  • assigning or 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.

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.

void
myfn(int a[])
{
	/*
	 * 'a' is of a pointer to int type, 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 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.

void
myfn(int a[2][2])
{
        return;
}

int
main(void)
{
	/*
	 * You need () as "*p[2]" would be "p is an array of pointers to int" as
	 * [] has higher priority.  More on that later.
	 */
        int (*p)[2];
        int (*p2)[3];

        myfn(p);	// OK
        myfn(p2);	// will trigger a warning
}
$ 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.

So, the following will not do what we might naively assume it would:

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.

Implement a function

Write a function that copies a string to a character array. 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';

Take 👀 implement-function.c and implement the function, 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.

Solution: 👀 implement-function-solution.c