11.md

Warm-up

Declare a 3-d array of 2x2x3 ints and statically initialize it with numbers 1..12. Retype the array as 1-d and print all elements. You should get then:

$ ./a.out
1 2 3 4 5 6 7 8 9 10 11 12

The last initializer and a comma

In C89, having a comma after the last initializer was a syntax error. Since C99, it's optional.

struct s {
	int a;
	int b;
} s = { 1, 2, };	// ',' after 2 is optional

See why keeping the last comma always there might be a very good idea: 👀 missing-comma-in-initializer.c

Enumeration constant

We already know integer constants, character and string constants, a floating-point constant, constant expression (an expression that involves only constants).

There is one other kind of constant, the "enumeration constant". An enumeration is a list of constant integer values, as in:

enum boolean { NO, YES};

The first name has value 0, the next one 1, etc. You can use explicit values as well. Unspecified values continue to progress from the last value specified. For example:

enum months {
	JAN = 1,
	FEB,
	MAR,
	APR,
	MAY,
	JUN,
	JUL,
	AUG,
	SEP,
	OCT,
	NOV,
	DEC,
};

You could also do as follows:

enum weird {
	one = 3,
	two,		// 4
	three = 3,
	four = 0,
	five,		// 1
	six,		// 2
	seven = -1,
	...
}

The list consists of enumerators. I.e. both JAN = 1 and FEB are enumerators. When used in code, JAN, FEB, MAR etc. are enumeration constants. The values of enumeration constants must fit an int. However, the implementation may define any integer type to be compatible with the enumerated type under the condition that all enumeration constants fit the type. So, the implementation might choose a char to be compatible with the enum boolean above.

enum white_space { NL = '\n', SP = ' ', TAB = '\t' };

The sizeof operator applied on an enum itself tells you the integer type used (sizeof (enum weird)), and sizeof on any enumeration constant gives you the same number.

Names in different enumerations must be distinct. In other words, all enumerators share the same name space. Values in the same enumerations need not be distinct, e.g.:

enum not_really_useful { GGG = 1, HHH = 1, GHGH = 0 };

Example: 👀 enums.c, enum-values.c

Use enums!

This:

enum commands {
	XXX_LIST,
	XXX_EXTRACT,
	XXX_CREATE
}

is so much better than this:

#define	XXX_LIST	0
#define	XXX_EXTRACT	1
#define	XXX_CREATE	2

The reason is that the enum is known to the compiler and may be included into debug symbols. So, when debugging, you might, if the compiler/debugger support it, see XXX_LIST instead of just 0. In the latter case, as the compiler only see the numbers when compiling the code, you will never see any symbolic names. So, seeing the symbols instead of common numbers 0 or 1 is really helping.

The const keyword

const is a qualifier that may be applied to the declaration of any variable. It specifies its value cannot be changed. If you apply const to an array, the array elements cannot be modified.

You can initialize the variable declared with the const qualifier.

👀 const.c

Using const will not really make the storage constant, you just cannot use that variable for an assignment to the storage.

int i = 'c';
int const *p2 = &i;

// illegal
*p2 = 'x';
// ok
i = 'x';

👀 const-not-really-a-const.c

It also depends where you apply the const keyword.

char const *s1;		// s1 is a pointer to const char
const char *s1;		// same as above
char *const s2;		// s2 is a const pointer to a char

*s1 = 'c';		// illegal
s1 = NULL;		// legal

s2 = NULL;		// illegal
*s2 = 'a';		// legal

👀 const-and-pointers.c

Using const can get a bit confusing. It is mostly used for pointer arguments to constant memory to specify that an array will not be changed in the function. For example, string functions:

size_t strlen(const char *s);
char *strncpy(char *dest, const char *src, size_t n);
...

🔧 const

The following is a const pointer to a const character. So, you can neither do *p = ... nor p = .... Verify.

const char *const p;

The typedef keyword

  • note that a char, signed and unsigned integer types, and the floating types are collectively called basic types.
  • then you have derived types - an array type, a structure, a union, a function type, a pointer type.

With typedef, you create new data type names for existing types. Note that you never create new data types with typedef. In other words, with typedef, you create synonyms to existing types.

typedef is most commonly used for derived types but many C provided names by the standard are based on basic types. For example, size_t, see its definition in /usr/include/sys/types.h.

typedef is used as follows:

typedef int myint;
typedef *char mycharptr;

mycharptr p = "my string";

typedef is also great to create complex type names in small steps. As we will see next.

typedef char **array[10];
array a;

👀 derived-types.c

The convention is to add _t to a new type name. For example:

typedef struct mystruct_s {
	int a;
	char c;
} mystruct_t;

And you can use it like this:

struct mystruct_s x;
mystruct_t y;

x and y are of the same type.

👀 typedef.c

🔧 Task

Use typedef in the warm-up code for the item structure and use the new name instead of struct item ....

Variable name space and scope

Identifiers fall into several name spaces that do not interfere with one another.

These distinct classes are:

  • objects, functions, typedef constants
  • labels
  • tags of structures or unions, and enumerations
  • structure or union individually

The "individual" part means each structure or union has its own namespace. So, you can have two different structures, each using the same member names.

👀 identifier-name-space.c

Reading complex declarations

To read a complex declaration, you use operator priorities and watch for parentheses.

char **argv

argv is a pointer to a pointer to char

int (*a)[10]

a is a pointer to an array of 10 ints

int *a[10]

a is an array of 10 pointers to int

void *myfunc()

myfunc is a function returning a pointer to void

void (*myfunc)()

myfunc is a pointer to a function returning void

char (*(*x())[])()

x is a function returning pointer to an array of pointers to a function returning a char

char (*(*x[3])())[5]

x is an array of 3 pointers to a function returning a pointer to an array of 5 chars

How to read it

  1. find the identifier (non keyword or custom type) of a variable or function
  2. start decoding:
  • left to right
  • ) => reverse decoding direction
  • () => denotes function
  • [] => array
  • ; => reverse the direction
  • when reading from right to left, we can hit:
    • ( => reverse the direction
    • * => pointer
    • type identifier => starts the whole definition

Example:

char *(*(**foo[][8])())[];

You can mentally simplify as:

char *(*SOME_FUNCTION)[];

Where SOME_FUNCTION is (**foo[][8])()

Read as:

foo is an array of arrays of 8 pointers to a pointer to a function returning a pointer to an array of pointers to a char.

Alternatively, you can do it via two simpler steps:

typedef char *a_of_p[];
typedef a_of_p *(**foo[][8])();

👀 complex-declaration.c

🔧 Practice:

int *(*(*fp1)(int))[10];
char (*(*x())[])()
double *f()[]           // this is not legal in C as a function cannot
                        // return an array

Bitwise operations

🔧 Task

Print argv[1] in binary (assume it is a correct decimal number). Note that printf(3) does not have a conversion specifier for it (unlike 'x' for hexa and 'o' for octal). Limit the input to positive ints. Do not use bit operators even if you know how to (ie. do NOT use >> etc.)

To verify, use bc(1) with obase=2. E.g.:

$ bc
obase=2
10
1010
255
11111111
2^31-1
1111111111111111111111111111111

$ ./a.out 2147483647
1111111111111111111111111111111
$ ./a.out 255
11111111
$ ./a.out 348508345
10100110001011101000010111001

You can also use obase=8, obase=16, etc. Note that bc(1) takes only capital letters as digits, ie. use "E4" for 0xe4, not "e4". "e4" is treated as a variable:

$ bc
ibase=16
obase=2
e4
0
E4
11100100

👀 print-in-binary.c

Bitwise operators are as follows:

Operator Meaning
& bitwise AND
| bitwise OR
^ bitwise XOR
<< left shift
>> right shift
~ one's complement (unary)

Note, individual bits are being processed. I.e.:

316 & 978			-> convert to binary to see it
100111100 & 1111010010		-> do logical AND bit by bit now

0100111100
1111010010
----------
0100010000	== 272 (use ibase=2 with bc(1) to verify)

So,

((338 & 978) == 272).	// == is of higher prority than &

Common uses of bitwise operators:

n = n & 01777;			// zero out some highest bits

/*
 * Setting individual flags.  Note that each of the flags has
 * just one bit set.
 */
#define	LIGHT_OFF	0x0000
#define	LIGHT_GREEN	0x0001
#define	LIGHT_RED	0x0002
#define	LIGHT_BLUE	0x0004
#define	LIGHT_YELLOW	0x0008
#define	LIGHT_VIOLET	0x0010
#define	LIGHT_WHITE	0x0020
// ...

lights = lights | LIGHT_YELLOW;	// turn on yellow light
lights = lights & ~LIGHT_RED;	// turn off red light

Binary operators and integer promotion

When working with bitwise operators, operands are promoted the same way as we learned in Conversions and promotions

That means:

13U & -1				-> promote int (-1) to unsigned,
					   i.e.  promote -1 to unsigned.
					   You get:

13 & 4294967295				-> now convert to binary
1101 & 11111111111111111111111111111111	-> the result clearly is 1101

👀 conv-and-prom-with-bitwise-ops.c

🔧 Task: rewrite print-in-binary.c using bitwise operators

🔧 Task: count 1 bits in a number (argv[1]).

$ ./a.out 337
4
$ ./a.out 13375
9

🔧 Home assignment

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

Linked list, part II.

(Note: this assignment was given in one of the previous years as the assignment for the final in-person test with the 90 minute limit).

/*
 * Create a set of functions for manipulating a linked list.  The program
 * accepts input commands as command line arguments and executes them.  New
 * elements are always appended to the list and any item is always removed from
 * the head.  Keep pointers to the first list item (head) and the last one
 * (tail).
 *
 * Commands are: create a list, insert an item, remove an item, and print the
 * list.  It is mandatory to create a list before doing anything else.
 *
 *   create a list:		C
 *   insert an item:		I<nnn>
 *   remove an item:		R
 *   print a list:		P
 *
 * Do reasonable checking for dealing with invalid input.  See below.
 *
 * Example:
 *
 *   $ ./a.out C I1 I2 I3 I4 I5 P R P I6 P I7 P R P R P R P R R R P I88 P R P
 *   List: 1 2 3 4 5
 *   List: 2 3 4 5
 *   List: 2 3 4 5 6
 *   List: 2 3 4 5 6 7
 *   List: 3 4 5 6 7
 *   List: 4 5 6 7
 *   List: 5 6 7
 *   List: EMPTY
 *   List: 88
 *   List: EMPTY
 *
 *   $ ./a.out C R
 *   a.out: Cannot remove an item from an empty list.
 *
 *   $ ./a.out C P
 *   List: EMPTY
 *
 *   $ ./a.out C L
 *   a.out: Wrong command: L
 *
 *   $ ./a.out C C
 *   a.out: List already created, exiting.
 *
 *   $ ./a.out I88
 *   a.out: List not created yet.
 */

👀 linked-list.c

getbits()

Implement a function getbits(x,p,n) that returns the (right adjusted) n-bit field of x that begins at position p. We assume that bit position 0 is at the right end and that n and p are sensible positive values. For example, getbits(x,4,3) returns the three bits in positions 4, 3 and 2, right-adjusted.

The idea is to zero out anything that is to the left of p, then shift those n bits to the very right, and you get the result.

/* 1 0000 0000: should return 1 */
i = getbits(0x100, 8, 1);
(void) printf("0x%X\n", i);
assert(i == 1);

/* 1110 0100: should return 9 */
i = getbits(0xe4, 5, 4);
(void) printf("0x%X\n", i);
assert(i == 9);

/* 1010 1010: should return the same number, ie. 0xAA */
i = getbits(0xAA, 7, 8);
(void) printf("0x%X\n", i);
assert(i == 0xAA);