#include <stdio.h> /* <- make sure the I/O functions are visible to the compiler */
/* main is the functions the OS calls when a program is loaded. It gets the
* number of command-line arguments and an array with the arguments. The first
* entry is the name of the executable.
*
* Main returns an integer that is the exit status of the program (0 on success).
*/
int main(int argc, char **argv) {
("Hello, World!\n"); /* We've used printf before */
printf
return 0;
}
Save as hello.c
and compile this program using
$ gcc hello.c -o hello
Note that we don’t need to use -no-pie
because we
are not combining our C code with any assembly
Blocks of scope are delimited by {
and
}
Functions are declared pretty much like Java methods:
(type1 arg1, type2 arg2, ...) return_type function_name
void
Control flow
We have the usual conditionals:
if (something) {
// do stuff
}
if (something) {
// do stuff
}
else {
// do other stuff
}
While loops and do-while loops
while (condition) {
// do this while condition holds
}
do {
// do this at leas once and then keep doing it again while condition holds
} while (condition);
For loops
for (initializer; condition; updater) {
// 1. run the initializer expression
// 2. if condition holds go to 3, else go to 6
// 3. do stuff in body
// 4. run the updater expression
// 5. Go to 2
// 6. End
}
Comparison operators: <
, >
,
<=
, >=
, ==
Logical operators: !
, &&
,
||
You can skip the rest of the current iteration of the innermost
loop with continue
You can break out of the innermost loop with break
Types
signed
and
unsigned
integral types: char
(1 byte =
8-bit), short int
(16-bit), int
(32-bit), long int
(64-bit)unsigned short int
float
(32-bit) and
double
(64-bit) (most implementations: IEE 754 single- and double-precision
floats)struct
s (see below)union
s (later)12
, -10.5
,
…'
'
:
'a'
"Hello, World"
(see
below)No booleans: just 0
and non-0
_Bool
now…)0
is
“false”!(!b) == b
does not hold as a resultNo string type, just a collection of characters
There is a difference between defining and declaring something (variable, function)
For us, this will usually relate to functions
Declaration states that something exists and states its type/signature but doesn’t give it’s value or body
Definition gives a type/signature + value/body
Function declaration example:
double pow(double base, int exp);
Function definition example
double pow(double base, int exp) {
double result = 1;
for (int i = 0; i <= exp; i++) {
*= base;
result }
return result;
}
Using the *
after the type name
E.g.,
int *pi; // pi is a pointer to an integer (uninitialized)
Note: If a pointer isn’t set to point to anything right away,
it’s good practice to initialize it to NULL
:
int *pi = NULL;
Getting the value that a pointer points to is called
dereferencing and is denoted by putting *
before
the variable name:
("%d\n", *pi); printf
The easiest way we can get a pointer to point to something meaningful is to give it an address
The address-of operator &
serves this
purpose:
int i = 42;
int *pi = &i; // pi now points to where the variable i is stored
("%d %d\n", i, *pi); // 42 42
printf
= 43;
i ("%d %d\n", i, *pi); // 43 43
printf
*pi = 44; // note the *
("%d %d\n", i, *pi); // 44 44
printf
int j = 123;
= &j;
pi ("%d %d\n", i, *pi); // 44 123 printf
Pointers can point to other pointers:
int i = 42;
int *pi = &i;
int **ppi = &p;
("%d %d %d\n", i, *pi, **pi); printf
You can have more levels of indirection
Remember char **argv
?
Spoiler alert: arrays are just pointers with some fancy syntax
We will work with static (size known at compile-time) and dynamic arrays
We’ll talk about static ones first
E.g.,
float nums[4]; // create an array of 4 floats
This makes room for 4 floats
These will be stored contiguously in memory
nums
points to
the first element
We can access them individually using indices, starting from 0
float nums[4]; // create an array of 4 floats
[0] = 0.1;
nums[1] = 3.14;
nums[2] = 1.5;
nums[3] = 3214;
nums
("2nd element: %f\n", nums[1]); printf
Arrays can also be initialized:
float nums[4] = { 0.1, 3.14, 1.5, 3214 };
("2nd element: %f\n", nums[1]); printf
In C (like in Assembly for us), strings are just arrays of
characters, terminated by a 0 byte (also written
'\0'
)
A string literal "Hello, world!"
is just the corresponding array of characters with an extra char for
0
I.e.
char msg1[6] = "Hello";
char msg2[6] = { 'H', 'e', 'l', 'l', 'o', '\0' };
// msg1 and msg2 define exactly the same sequence of bytes in memory
C comes with a string library (#include <string.h>
)
that you’ll want to use for things like comparing strings, or creating
copies of strings, etc.
Memory can be allocated using the library function malloc
It’s defined in stdlib.h
It takes the number of bytes we want and returns a pointer to the block of memory (if successful)
Allocated memory needs to be freed using free
int *one_int = malloc(4);
*one_int = 42;
(one_int); free
We will mostly use malloc
to allocate arrays and
struct
s (below)
int *fifty_ints = malloc(50 * sizeof(int));
for (int i = 0; i < 50; ++i) {
[i] = i * i;
fifty_ints}
(fifty_ints); free
Structs are the most useful user-defined data types in C
Think of them as Java classes, but everything is public (thought make the whole struct opaque) and there are no methods
They’re actually more similar to the define-struct
in Fundies I, but there are types
A struct allows us to store multiple values of different types together
It is defined using the struct
keyword:
struct address {
unsigned int house_no;
char street[32];
char city[24];
char state[3];
unsigned int zip;
}
Note that structs occupy a separate namespace - to create a struct variable of the above type, we need to use the struct keyword again:
struct address home, work; // inside a function this will allocate two
// structs on the stack
To access a field, we use the .
:
.house_no = 360;
work(work.street, "Huntington Ave"); // see man 3 strcpy
strcpy(work.city, "Boston");
strcpy(work.state, "MA");
strcpy.zip = 02115; work
Structs can, of course, be nested:
struct person {
char first[32];
char last[32];
struct address home;
}
They can be passed to and returned from a function:
struct address get_address(struct person p) { ... }
typedef
Writing out struct every time can be tiring
C allows us to introduce type synonyms using typedef
:
typedef person_t struct person; // now we can use person_t to mean struct person
typedef
can be used with any type to make code more
readable:
typedef age_t unsigned char;
Of course, we can have pointers to structs:
struct person *p; // OR
*p; person_t
We can use the address-of operator &
to get the
address of a struct:
struct address *current = &work;
We can also allocate memory for structs dynamically, using
malloc
and sizeof
:
struct person *ferd = malloc(sizeof(struct person)); // or equivalently:
*ferd = malloc(sizeof(person_t)); person_t
We can also create arrays of structs:
[80];
person_t class*friends = malloc(5 * sizeof(person_t));
person_t
// ...
for (int i = 0; i < 5; ++i) {
if (strcmp(friends[i].home.street, "Huntington Ave") == 0) {
("%s lives close!\n", friends[i].first);
printf}
}
Often, pointers are used to pass a struct to a function, to avoid copying the contents into the function’s stack frame
When accessing fields via a pointer, we can use ->
to make things
more readable:
int lives_in_boston(person_t *p) {
return strcmp(p->home.city, "Boston") != 0; // equivalent to
return strcmp((*p).home.city, "Boston") != 0;
}
#define
This directive is used to define a textual macro
The macro can be a constant macro or a parametrized macro
E.g.,
#define COUNT 100
#define COURSE "Computer Systems"
will define the macros COUNT
and COURSE
;
everywhere else where COUNT
is mentioned, it will be
replaced with 100
and COURSE
will be replaced
with "Computer Systems"
Note, that the expression is simply substituted for the macro and does not get evaluated at the definition site
Hence there is a subtlety that one has to keep in mind: since the
preprocessor will replace any occurrence of the macro name with the
snippet that it’s defined to, using an expression 10 + 2
has some consequences
Consider
#define X 10 + 2
int a = X; // expands to 10 + 2
int b = 3 * X; // expands to 2 * 10 + 2 - this might not be what we expect
The solution is to always put an expression in parentheses when using define:
#define X (10 + 2)
int b = 3 * X; // expands to 2 * (10 + 2)
We can also define macros with arguments using #define
These look like function calls, but they get expanded at compile-time
Example:
#define max(a, b) (a > b ? a : b)
("%d\n", max(3, 4)); printf
The argument to a macro does not get evaluated before being used in the macro, so we have a similar problem as above:
#define dbl(x) (2 * x)
("%d\n", dbl(10 + 1)); // expands to 2 * 10 + 1, so prints 21, not 22! printf
So any argument use in a macro body should be enclosed in
()
:
#define max(a, b) ((a) > (b) ? (a) : (b))
#define dbl(x) (2 * (x))
Another caveat: consider the following:
#define foomacro(x) ((x) + (x))
int foofun(int x) {
return x + x;
}
Although both seem to be computing the same result, they will behave differently if the expression passed in has side-effects:
int x = 10;
("%d\n", foomacro(++x)); // will likely print 23
printf
= 10;
x ("%d\n", foofun(++x)); // prints 22 printf
Why?
Note: a good modern C compiler will usually warn you about the
above use of foomacro
#include
#include
directive
performs a simple textual inclusion of the given file.h
files#if
/#ifdef
/#ifndef
/#elif
/#else
This set of directives allows conditional compilation
Basically, these are compile-time conditionals that hide or expose parts of the source file from or to the compiler
#ifdef
checks if the given
Example:
#ifdef UNIX
"/"
PATH_SEPARATOR #elif defined WINDOWS
"\\"
PATH_SEPARATOR #endif
Other example:
for (int i = 0; i < length; i++) {
+= array[i];
sum #if DEBUG_LEVEL >= 1
("array[%d] = %d, sum = %d\n", i, array[i], sum);
printf#endif
}
…
…
…