CS 134

C for C++ Programmers

Like Linux and classic Unix, OS/161 is written in C, not C++ (and mostly “old-school” classic C). If you're used to C++, you'll find that C is a bit more basic. On the positive site, in C there's less magic and fewer ways to shoot yourself in the foot. On the negative side, you'll find that you're missing a lot of the features you're used to. This document will help you get up to speed on the differences between C and C++.

The good news is that C syntax is almost identical to that of C++. However, there are many things you're used to that aren't available in C:

  • Classes and all their associated tricks: templates, inheritance, etc.
  • new and delete
  • The stream operators << and >>
  • The // comment character
  • The bool keyword
  • All those weird casting operators (dynamic_cast, static_cast)
  • The standard libraries you're used to (e.g. std::iostream, std::string, std::vector, std::map, etc.)
  • Templates
  • Exceptions
  • Lots of other stuff…

Comments

In classic C, the only valid way to specify a comment was like so:

/* this is a comment */

/* This is a multiline
   comment */

You cannot nest comments.

/* This is a  /*nested */ comment. And it is illegal. */

Since C99, you can also use // for comments, and you'll find that style used sometimes in the OS/161 codebase, but most comments use the /* */ style.

void* foo;  // this is a comment about what foo is for

I/O

C doesn't have stream operators. Instead you'll want to use the functions provided in the stdio library; in particular, printf, fprintf, fgets, fputs.

Output: printf, fprintf, fputs

The most common output function in C is printf(), which prints characters to the screen (or wherever standard out is directed to go). Here's a quick hello world program that illustrates its use:

#include <stdio.h>

int main() {
    printf("Hello World");
}

printf() has a variable number of arguments, the first of which is a format string. The format string can contain both ordinary characters (what you want to appear on the screen such as the Hello World shown above) and conversion character sequences. You can think of conversion characters as placeholders for a specific type formatted in a particular way. Conversion character sequences begin with % and end with a conversion character. An example will perhaps make this clearer. Here we're printing out the integers from 0 to 9:

int i;

for (i = 0; i < 10; i++) {
    printf("the variable 'i' is: %d", i);
}

The conversion sequence here is %d, which you can think of as a placeholder for an integer. Since this is the first conversion character sequence, the value of the first argument after the format string--in this case the value of i—will be placed in the held spot. Conversion characters can specify type, precision, and all sorts of other formatting information. See K&R or the man pages for all the gory details, but here are some essential ones cribbed from K&R:

Sequence Type Produces
%d or %i int signed decimal notation
%s char * prints characters until the null character is reached
%x int hexidecimal notation
%p void * prints as a pointer
`printf()` always prints to standard out. If you want to print to somewhere else, such as standard error, use `fprintf()`, which takes the stream you're printing on as its first argument:

```c
fprintf(stderr, "Fatal Error #2212. We're hosed.");
/* or with values */
fprintf(stderr, "Fatal Error in foo(): value of bar is %p\n", bar);

Note that we're calling standard in, out, and error slightly different names than we do in C++:

C++ C
cin stdin
cout stdout
cerr stderr

Finally, fputs() will also allow us to write to a stream:

int fputs(const char *str, FILE *stream);

fputs() takes a null-terminated string str and writes it to stream, but drops the null character. It returns a non-negative integer if okay and EOF on error. An example:

if ( (fputs("Hello world", stdout)) == EOF) {
    fprint(stderr, "Whoops, something went wrong");
}

fputs() functions similarly to printf() when it writes to stdout, but it doesn't do any conversion (which probably means it's quite a bit faster).

Input

Input is a bit trickier. For reading an entire line you'll probably want to use fgets(). Here's the prototype:

char *fgets(char *buffer, int size, FILE *stream);

fgets() reads up to size - 1 characters from stream and stores them in buffer. fgets() adds the null character ('\0') after the last character read into the buffer and returns buffer if everything works fine, or NULL on error or end of file. Here's an example:

char *cptr;
char buffer[256];

printf("Enter some stuff:\n");
cptr = fgets(buffer, 256, stdin);
if(cptr != NULL) {
    printf("You typed : %s\n", cptr);
}

Here's a more complicated example. Readline() uses fgets() to read up to MAX_LINE - 1 characters into the buffer in. It strips preceding whitespace and returns a pointer to the first non-whitespace character.

char *Readline(char *in) {
    char *cptr;

    if (cptr = fgets(in, MAX_LINE, stdin)) {
        /* kill preceding whitespace but leave \n
           so we're guaranteed to have something*/
        while(*cptr == ' ' || *cptr == '\t') {
            cptr++;
        }
        return cptr;
    } else {
        return 0;
    }
}

Memory Allocation

There are no memory management operators such as new and delete in C. All memory allocation is done with the function malloc(). Here's its prototype:

void * malloc(int nbytes)

malloc() takes an int indicating the number of bytes you want allocated and it returns a void pointer to the start of the allocated memory.

/* We're going to allocate enough space for an integer
    and assign 5 to that memory location */
int *foo;
/* malloc call: note the cast of the return value
    from a void * to the appropriate pointer type */
foo = (int *) malloc(sizeof(int));
*foo = 5;

Even more complicated structures, such as arrays, are also allocated through malloc():

/* Here we're allocating space for 5 integers */
int *foo;
foo = (int *) malloc(sizeof(int) * 5);
foo[0] = 17;
foo[4] = 22;

Freeing memory is done with the function free(). The prototype:

void free(void *);

Here's an example of it being used:

free(foo);

Pretty simple. However, note that it's disastrous to free a piece of memory that has already been freed.

More information is available in the man pages.

Variable Declaration

In C++ you can declare variables pretty much anywhere in your program. But classic C required you to declare variables at the beginning of a function and before any other code. That rule includes loop counter variables, which meant that you couldn't write code like

/* ... some code ... */
for (int i = 0; i < 200; i++) {

and instead had to write it like

int i;
/* ... some code ... */
for (i = 0; i < 200; i++) {

This restriction was also removed in C99, so you can declare variables anywhere in your program. However, you'll still see the traditional style in a lot of C code, including the OS/161 codebase.

Constants

The standard way to declare a constant in C is to use #define:

#define MAX_LEN 1024
#define NUM_CALLS 20

This code is also valid C++ code, though the use of #define is usually discouraged in C++. Like all preprocessor directives, #defines usually appear at the top of the source file.

Types

structs

C has no classes, but you can create composite types with struct:

struct point {
    int x;
    int y;
};

You would then use them as

  struct point p;
  p.x = 0;
  p.y = 0;

or

  struct point p = {0, 0};

While this usage is acceptable, the preferred approach is to use pointers when using structures:

struct point *p;
p = (struct point *) malloc(sizeof(struct point));
p->x = 0;
p->y = 0;

The struct qualifier is required any time you're referring to a struct. You can see that in the code above, in the cast of the return value from malloc() and in the sizeof () expression. In other words, having declared the struct point, we can't simply write

/* this code won't work */
point p;
p.x = 0;

as we would with a C++ class. We can get rid of this repetition of the struct qualifier by using typedefs. The typedef keyword essentially creates an alias. Thus we can typedef struct point to Point (or to lowercase point, but we're using title case to remind ourselves that Point is a struct).

typedef struct point Point;
Point *p;
p = (Point *) malloc(sizeof(Point);
p->x = 0;

Booleans

There was no bool keyword in classic C. Instead, booleans in C are the same as integers with a value of 0 for false or 1 for true. Thus the following is an infinite loop:

while (1) {
    ; /* do nothing */
}

In C99 and later, you can use <stdbool.h> to get the bool type and true/false constants. Many C programmers stick to the old ways, and the OS/161 codebase is no exception, but you could (in principle) use <stdbool.h> in your OS/161 code.

Function Pointers

Function pointers are frequently used in OS development for callback mechanisms and implementing polymorphism. They allow you to pass functions as arguments to other functions, which is particularly useful for event handlers, sorting algorithms, and device drivers.

Declaring and Using Function Pointers

The syntax for declaring function pointers can be tricky. Here's the general form:

return_type (*pointer_name)(parameter_types);

An example:

int (*compare_func)(const void*, const void*);

// Using the function pointer
int compare_ints(const void* a, const void* b) {
    return (*(int*)a - *(int*)b);
}

compare_func = compare_ints;

// Using with qsort
qsort(array, array_size, sizeof(int), compare_func);

Function Pointers in Structs

Function pointers are often used in structs to create vtable-like structures:

struct device_ops {
    void (*init)(struct device*);
    int (*read)(struct device*, void*, size_t);
    int (*write)(struct device*, const void*, size_t);
};

struct device {
    // ... device-specific data ...
    struct device_ops* ops;
};

// Usage
device->ops->init(device);

The C Preprocessor

The preprocessor is a powerful tool in C, especially useful in OS development for conditional compilation, macro definitions, and code generation.

Macro Definitions

Macros can be used for constants, inline functions, and code generation:

#define MAX_PROCESSES 256
#define MIN(a, b) ((a) < (b) ? (a) : (b))

// Multiline macros
#define DEBUG_LOG(msg, ...) do { \
    if (debug_enabled) { \
        printf("DEBUG: " msg "\n", ##__VA_ARGS__); \
    } \
} while(0)

Conditional Compilation

Use #ifdef, #ifndef, #if, #else, #elif, and #endif for conditional compilation:

#ifdef DEBUG
    // Debug-only code
#endif

#if defined(X86_64)
    // 64-bit specific code
#elif defined(X86_32)
    // 32-bit specific code
#else
    #error "Unsupported architecture"
#endif

Include Guards

Prevent multiple inclusions of header files:

#ifndef OS161_PROCESS_H
#define OS161_PROCESS_H

// Header content goes here

#endif /* OS161_PROCESS_H */

Preprocessor Stringification and Concatenation

Use # for stringification and ## for concatenation:

#define STRINGIFY(x) #x
#define CONCAT(a, b) a ## b

printf("%s\n", STRINGIFY(Hello));  // Prints: "Hello"
int CONCAT(var, 1) = 42;  // Creates a variable named var1

Common Preprocessor Directives

  • #include: Include header files
  • #define and #undef: Define and undefine macros
  • #error: Stop compilation with an error message
  • #pragma: Compiler-specific directives

Remember, while powerful, excessive use of the preprocessor can make code harder to debug and maintain. Use judiciously!

Libraries

Library functions are included by specifying the name of the appropriate header file in an #include statement:

#include <stdlib.h>
#include <string.h>

This usage is similar to C++'s, but with C, the .h is required.

Here are some of the most useful C libraries:

  • <stdio.h> : I/O functions (e.g., printf, fprintf, sprintf, fgets, fputs)
  • <stdlib.h> : memory management and utility functions (e.g., malloc, free, atoi, atol)
  • <string.h> : string manipulation functions (e.g., strcpy, strcmp, strncpy, strtok, strlen)
  • <assert.h> : Assertions (the assert macro)
  • <stdbool.h> : Boolean type and constants (C99 and later)
  • <stdint.h> : Fixed-width integer types (C99 and later)
  • <limits.h> : Limits of integer types
Library File Contents
<stdio.h> I/O functions (e.g., printf, fprintf, sprintf, fgets, fputs)
<stdlib.h> Memory management and utility functions (e.g., malloc, free, atoi, atol)
<string.h> String manipulation functions (e.g., strcpy, strcmp, strncpy, strtok, strlen)
<assert.h> Assertions (the assert macro)
<stdbool.h> Boolean type and constants (C99 and later)
<stdint.h> Fixed-width integer types (C99 and later)
<limits.h> Limits of integer types

(When logged in, completion status appears here.)