The C Handbook1
The C Handbook1
com
The C Handbook
Written by Flavio Copes. Updated 2020
1. Introduction to C
2. Variables and types
2.1. Integer numbers
2.2. Unsigned integers
2.3. The problem with overflow
2.4. Warnings when declaring the wrong type
2.5. Floating point numbers
3. Constants
4. Operators
4.1. Arithmetic operators
5. Conditionals
5.1. if
5.2. switch
6. Loops
6.1. For loops
7. Arrays
8. Strings
9. Pointers
10. Functions
11.2. scanf()
17. Structures
20.3. Macros
20.4. If defined
20.5. Predefined symbolic constants you can use
1. Introduction to C
C is probably the most widely known programming language. It is used as the
reference language for computer science courses all over the world, and it’s
probably the language that people learn the most in school among with Python and
Java.
C is not just what students use to learn programming. It’s not an academic language.
And I would say it’s not the easiest language, because C is a rather low level
programming language.
Today, C is widely used in embedded devices, and it powers most of the Internet
servers, which are built using Linux. The Linux kernel is built using C, and this also
means that C powers the core of all Android devices. We can say that C code runs a
good portion of the entire world. Right now. Pretty remarkable.
When it was created, C was considered a high level language, because it was
portable across machines. Today we kind of give for granted that we can run a
program written on a Mac on Windows or Linux, perhaps using Node.js or Python.
Once upon a time, this was not the case at all. What C brought to the table was a
language simple to implement, having a compiler that could be easily ported to
different machines.
I said compiler: C is a compiled programming language, like Go, Java, Swift or Rust.
Other popular programming language like Python, Ruby or JavaScript are
interpreted. The difference is consistent: a compiled language generates a binary file
that can be directly executed and distributed.
C is not garbage collected. This means we have to manage memory ourselves. It’s a
complex task and one that requires a lot of attention to prevent bugs, but it is also
what makes C ideal to write programs for embedded devices like Arduino.
C does not hide the complexity and the capabilities of the machine underneath. You
have a lot of power, once you know what you can do.
I want to introduce the first C program now, which we’ll call “Hello, World!”
hello.c
#include <stdio.h>
int main(void) {
printf("Hello, World!");
}
Let’s describe the program source code: we first import the stdio library (the name
stands for standard input-output library).
C is a very small language at its core, and anything that’s not part of the core is
provided by libraries. Some of those libraries are built by normal programmers, and
made available for others to use. Some other libraries are built into the compiler.
Like stdio and others.
This function is wrapped into a main() function. The main() function is the entry
point of any C program.
A function is a routine that takes one or more arguments, and returns a single value.
In the case of main() , the function gets no arguments, and returns an integer. We
identify that using the void keyword for the argument, and the int keyword for the
return value.
The function has a body, which is wrapped in curly braces, and inside the body we
have all the code that the function needs to perform its operations.
The printf() function is written differently, as you can see. It has no return value
defined, and we pass a string, wrapped in double quotes. We didn’t specify the type
of the argument.
That’s because this is a function invocation. Somewhere, inside the stdio library,
printf is defined as
You don’t need to understand what this means now, but in short, this is the
definition and when we call printf("Hello, World!"); , that’s where the function is
ran.
#include <stdio.h>
int main(void) {
printf("Hello, World!");
}
In any case, when you open the terminal window you can type gcc , and this
command should return you an error saying that you didn’t specify any file:
That’s good. It means the C compiler is there, and we can start using it.
Now type the program above into a hello.c file. You can use any editor, but for the
sake of simplicity I’m going to use the nano editor in the command line:
Type the program:
./hello
to run it:
I prepend ./ to the program name, to tell the terminal that the command is in the
current folder
Awesome!
Now if you call ls -al hello , you can see that the program is only 12KB in size:
This is one of the pros of C: it’s highly optimized, and this is also one of the reasons
it’s this good for embedded devices that have a very limited amount of resources.
This means that any variable has an associated type, and this type is known at
compilation time.
This is very different than how you work with variables in Python, JavaScript, PHP
and other interpreted languages.
When you create a variable in C, you have to specify the type of a variable at the
declaration.
int age;
A variable name can contain any uppercase or lowercase letter, can contain digits
and the underscore character, but it can’t start with a digit. AGE and Age10 are valid
variable names, 1age is not.
You can also initialize a variable at declaration, specifying the initial value:
Once you declare a variable, you are then able to use it in your program code, and
you can change its value at any time, using the = operator for example, like in age =
In this case:
#include <stdio.h>
int main(void) {
int age = 0;
age = 37.2;
printf("%u", age);
}
the compiler will raise a warning at compile time, and will convert the decimal
number to an integer value.
The C built-in data types are int , char , short , long , float , double , long double . Let’s
find out more about those.
int
short
long
Most of the times, you’ll likely use an int to store an integer. But in some cases, you
might want to choose one of the other 3 options.
The char type is commonly used to store letters of the ASCII chart, but it can be
used to hold small integers from -128 to 127 . It takes at least 1 byte.
int takes at least 2 bytes. short takes at least 2 bytes. long takes at least 4 bytes.
As you can see, we are not guaranteed the same values for different environments.
We only have an indication. The problem is that the exact numbers that can be
stored in each data type depends on the implementation and the architecture.
We’re guaranteed that short is not longer than int . And we’re guaranteed long is
not shorter than int .
The ANSI C spec standard determines the minimum values of each type, and thanks
to it we can at least know what’s the minimum value we can expect to have at our
disposal.
If you are programming C on an Arduino, different board will have different limits.
On an Arduino Uno board, int stores a 2 byte value, ranging from -32,768 to 32,767 .
On a Arduino MKR 1010, int stores a 4 bytes value, ranging from -2,147,483,648 to
2,147,483,647 . Quite a big difference.
On all Arduino boards, short stores a 2 bytes value, ranging from -32,768 to 32,767 .
For all the above data types, we can prepend unsigned to start the range at 0, instead
of a negative number. This might make sense in many cases.
unsigned char will range from 0 to at least 255
Given all those limits, a question might come up: how can we make sure our
numbers do not exceed the limit? And what happens it we do exceed the limit?
If you have an unsigned int number at 255, and you increment it, you’ll get 256 in
return. As expected. If you have a unsigned char number at 255, and you increment
it, you’ll get 0 in return. It resets starting from the initial possible value.
If you have a unsigned char number at 255 and you add 10 to it, you’ll get the
number 9 :
#include <stdio.h>
int main(void) {
unsigned char j = 255;
j = j + 10;
printf("%u", j); /* 9 */
}
If you have a signed value, the behavior is undefined. It will basically give you a huge
number which can vary, like in this case:
#include <stdio.h>
int main(void) {
char j = 127;
j = j + 10;
printf("%u", j); /* 4294967177 */
}
In other words, C does not protect you from going over the limits of a type. You
need to take care of this yourself.
2.4. Warnings when declaring the wrong type
When you declare the variable and initialize it with the wrong value, the gcc
#include <stdio.h>
int main(void) {
char j = 1000;
}
#include <stdio.h>
int main(void) {
char j;
j = 1000;
}
#include <stdio.h>
int main(void) {
char j = 0;
j += 1000;
}
Floating point types can represent a much larger set of values than integers can, and
can also represent fractions, something that integers can’t do.
Using floating point numbers, we represent numbers as decimal numbers times
powers of 10.
1.29e-3
-2.3e+5
float
double
long double
are used to represent numbers with decimal points (floating point types). All can
represent both positive and negative numbers.
The minimum requirements for any C implementation is that float can represent a
range between 10^-37 and 10^+37, and is typically implemented using 32 bits.
double can represent a bigger set of numbers. long double can hold even more
numbers.
On your specific computer, how can you determine the specific size of the types?
You can write a program to do that:
#include <stdio.h>
int main(void) {
printf("char size: %lu bytes\n", sizeof(char));
printf("int size: %lu bytes\n", sizeof(int));
printf("short size: %lu bytes\n", sizeof(short));
printf("long size: %lu bytes\n", sizeof(long));
printf("float size: %lu bytes\n", sizeof(float));
printf("double size: %lu bytes\n",
sizeof(double));
printf("long double size: %lu bytes\n",
sizeof(long double));
}
3. Constants
Let’s now talk about constants.
Like this:
It’s just a convention, but one that can greatly help you while reading or writing a C
program as it improves readability. Uppercase name means constant, lowercase
name means variable.
A constant name follows the same rules for variable names: can contain any
uppercase or lowercase letter, can contain digits and the underscore character, but it
can’t start with a digit. AGE and Age10 are valid variable names, 1AGE is not.
#define AGE 37
In this case, you don’t need to add a type, and you don’t also need the = equal sign,
and you omit the semicolon at the end.
The C compiler will infer the type from the value specified, at compile time.
4. Operators
C offers us a wide variety of operators that we can use to operate on data.
arithmetic operators
comparison operators
logical operators
miscellaneous operators
In this section I’m going to detail all of them, using 2 imaginary variables a and b as
examples.
In this macro group I am going to separate binary operators and unary operators.
Binary operators work using two operands:
Operator Name Example
= Assignment a = b
+ Addition a + b
- Subtraction a - b
* Multiplication a * b
/ Division a / b
% Modulo a % b
The difference between a++ and ++a is that a++ increments the a variable after using
it. ++a increments the a variable before using it.
For example:
int a = 2;
int b;
b = a++ /* b is 2, a is 3 */
b = ++a /* b is 4, a is 4 */
|| OR (example: a || b )
Those operators are useful to perform an assignment and at the same time perform
an arithmetic operation:
Operator Name Example
+= Addition assignment a += b
-= Subtraction assignment a -= b
*= Multiplication assignment a *= b
/= Division assignment a /= b
%= Modulo assignment a %= b
The ternary operator is the only operator in C that works with 3 operands, and it’s a
short way to express conditionals.
Example:
a ? b : c
The sizeof operator returns the size of the operand you pass. You can pass a
variable, or even a type.
Example usage:
#include <stdio.h>
int main(void) {
int age = 37;
printf("%ld\n", sizeof(age));
printf("%ld", sizeof(int));
}
With all those operators (and more, which I haven’t covered in this post, including
bitwise, structure operators and pointer operators), we must pay attention when
using them together in a single expression.
int a = 2;
int b = 4;
int c = b + a * a / b - a;
What’s the value of c ? Do we get the addition being executed before the
multiplication and the division?
Operators also have an associativity rule, which is always left to right except for the
unary operators and the assignment.
In:
int c = b + a * a / b - a;
Then we can perform the sum and the subtraction: 4 + 1 - 2. The value of c is 3 .
In all cases, however, I want to make sure you realize you can use parentheses to
make any similar expression easier to read and comprehend.
int c = b + ((a * a) / b) - a;
5. Conditionals
Any programming language provides the programmers the ability to perform
choices.
We want to check data, and do choices based on the state of that data.
The first is the if statement, with its else helper, and the second is the switch
statement.
5.1. if
In an if statement, you can check for a condition to be true, and then execute the
block provided in the curly brackets:
int a = 1;
if (a == 1) {
/* do something */
}
You can append an else block to execute a different block if the original condition
turns out to be false;
int a = 1;
if (a == 2) {
/* do something */
} else {
/* do something else */
}
Beware one common source of bugs - always use the comparison operator == in
comparisons, and not the assignment operator = , otherwise the if conditional
check will always be true, unless the argument is 0 , for example if you do:
int a = 0;
if (a = 0) {
/* never invoked */
}
Why does this happen? Because the conditional check will look for a boolean result
(the result of a comparison), and the 0 number always equates to a false value.
Everything else is true, including negative numbers.
You can have multiple else blocks by stacking together multiple if statements:
int a = 1;
if (a == 2) {
/* do something */
} else if (a == 1) {
/* do something else */
} else {
/* do something else again */
}
5.2. switch
If you need to do too many if / else / if blocks to perform a check, perhaps because
you need to check the exact value of a variable, then switch can be very useful to
you.
You can provide a variable as condition, and a series of case entry points for each
value you expect:
int a = 1;
switch (a) {
case 0:
/* do something */
break;
case 1:
/* do something else */
break;
case 2:
/* do something else */
break;
}
We need a break keyword at the end of each case, to avoid the next case to be
executed when the one before ends. This “cascade” effect can be useful in some
creative ways.
int a = 1;
switch (a) {
case 0:
/* do something */
break;
case 1:
/* do something else */
break;
case 2:
/* do something else */
break;
default:
/* handle all the other cases */
break;
}
6. Loops
C offers us three ways to perform a loop: for loops, while loops and do while
loops. They all allow you to iterate over arrays, but with a few differences. Let’s see
them in details.
The first, and probably most common, way to perform a loop is for loops.
Using the for keyword we can define the rules of the loop up front, and then
provide the block that is going to be executed repeatedly.
Like this:
The (int i = 0; i <= 10; i++) block contains 3 parts of the looping details:
We first define a loop variable, in this case named i . i is a common variable name
to be used for loops, along with j for nested loops (a loop inside another loop). It’s
just a convention.
The variable is initialized at the 0 value, and the first iteration is done. Then it is
incremented as the increment part says ( i++ in this case, incrementing by 1), and all
the cycle repeats until you get to the number 10.
Inside the loop main block we can access the variable i to know at which iteration
we are. This program should print 0 1 2 3 4 5 5 6 7 8 9 10 :
for (int i = 0; i <= 10; i++) {
/* instructions to be repeated */
printf("%u ", i);
}
Loops can also start from a high number, and go a lower number, like this:
While loops is simpler to write than a for loop, because it requires a bit more work
on your part.
Instead of defining all the loop data up front when you start the loop, like you do in
the for loop, using while you just check for a condition:
And this loop will be an infinite loop unless you increment the i variable at some
point inside the loop. An infinite loop is bad because it will block the program,
nothing else can happen.
int i = 0;
There’s one exception to this, and we’ll see it in one minute. Before, let me introduce
do while .
While loops are great, but there might be times when you need to do one particular
thing: you want to always execute a block, and then maybe repeat it.
This is done using the do while keyword, in a way that’s very similar to a while loop,
but slightly different:
int i = 0;
do {
/* do something */
i++;
} while (i < 10);
The block that contains the /* do something */ comment is always executed at least
once, regardless of the condition check at the bottom.
In all the C loops we have a way to break out of a loop at any point in time,
immediately, regardless of the conditions set fo the loop.
This is useful in many cases. You might want to check for the value of a variable, for
example:
Having this option to break out of a loop is particularly interesting for while loops
(and do while too), because we can create seemingly infinite loops that end when a
condition occurs, and you define this inside the loop block:
int i = 0;
while (1) {
/* do something */
i++;
if (i == 10) break;
}
7. Arrays
An array is a variable that stores multiple values.
Every value in the array, in C, must have the same type. This means you will have
arrays of int values, arrays of double values, and more.
int prices[5];
You must always specify the size of the array. C does not provide dynamic arrays out
of the box (you have to use a data structure like a linked list for that).
int prices[5] = { 1, 2, 3, 4, 5 };
But you can also assign a value after the definition, in this way:
int prices[5];
prices[0] = 1;
prices[1] = 2;
prices[2] = 3;
prices[3] = 4;
prices[4] = 5;
int prices[5];
And you can reference an item in the array by using square brackets after the array
variable name, adding an integer to determine the index value. Like this:
Array indexes start from 0, so an array with 5 items, like the prices array above, will
have items ranging from prices[0] to prices[4] .
The interesting thing about C arrays is that all elements of an array are stored
sequentially, one right after another. Not something that normally happens with
higher-level programming languages.
Another interesting thing is this: the variable name of the array, prices in the above
example, is a pointer to the first element of the array, and as such can be used like a
normal pointer.
8. Strings
In C, strings are one special kind of array: a string is an array of char values:
char name[7];
I introduced the char type when I introduced types, but in short it is commonly used
to store letters of the ASCII chart.
Or more conveniently with a string literal (also called string constant), a sequence of
characters enclosed in double quotes:
printf("%s", name);
Do you notice how “Flavio” is 6 chars long, but I defined an array of length 7? Why?
This is because the last character in a string must be a 0 value, the string terminator,
and we must make space for it.
This library is essential because it abstracts many of the low level details of working
with strings, and provides us a set of useful functions.
#include <string.h>
9. Pointers
Pointers are one of the most confusing/challenging parts of C, in my opinion.
Especially if you are new to programming, but also if you come from a higher level
programming language like Python or JavaScript.
In this section I want to introduce them in the simplest yet not-dumbed-down way
possible.
We can use the & operator to get the value of the address in memory of a variable:
Using int *address in the declaration, we are not declaring an integer variable, but
rather a pointer to an integer.
We can use the pointer operator * to get the value of the variable an address is
pointing to:
This time we are using the pointer operator again, but since it’s not a declaration
this time it means “the value of the variable this pointer points to”.
In this example we declare an age variable, and we use a pointer to initialize the
value:
int age;
int *address = &age;
*address = 37;
printf("%u", *address);
When working with C, you’ll find that a lot of things are built on top of this simple
concept, so make sure you familiarize with it a bit, by running the above examples
on your own.
Pointers are a great opportunity because they force us to think about memory
addresses and how data is organized.
int prices[3] = { 5, 4, 3 };
The prices variable is actually a pointer to the first item of the array. You can get the
value of the first item using this printf() function in this case:
printf("%u", *prices); /* 5 */
The cool thing is that we can get the second item by adding 1 to the prices pointer:
We can also do many nice string manipulation operations, since strings are arrays
under the hood.
We also have many more applications, including passing the reference of an object
or a function around, to avoid consuming more resources to copy it.
10. Functions
Functions are the way we can structure our code into subroutines that we can:
1. give a name to
2. call when we need them
Starting from your very first program, an “Hello, World!”, you immediately make use
of C functions:
#include <stdio.h>
int main(void) {
printf("Hello, World!");
}
The main() function is a very important function, as it’s the entry point for a C
program.
The function body is the set of instructions that are executed any time we invoke a
function.
If the function has no return value, you can use the keyword void before the
function name. Otherwise you specify the function return value type ( int for an
integer, float for a floating point value, const char * for a string, etc).
A function can have arguments. They are optional. If it does not have them, inside
the parentheses we insert void , like this:
void doSomething(void) {
/* ... */
}
In this case, when we invoke the function we’ll call it with nothing in the parentheses:
doSomething();
If we have one parameter, we specify the type and the name of the parameter, like
this:
When we invoke the function, we’ll pass that parameter in the parentheses, like this:
doSomething(3);
We can have multiple parameters, and if so we separate them using a comma, both
in the declaration and in the invocation:
doSomething(3, 4);
Parameters are passed by copy. This means that if you modify value1 , its value is
modified locally, and the value outside of the function, where it was passed in the
invocation, does not change.
If you pass a pointer as a parameter, you can modify that variable value because
you can now access it directly using its memory address.
You can’t define a default value for a parameter. C++ can do that (and so Arduino
Language programs can), but C can’t.
Make sure you define the function before calling it, or the compiler will raise a
warning and an error:
➜ ~ gcc hello.c -o hello; ./hello
hello.c:13:3: warning: implicit declaration of
function 'doSomething' is invalid in C99
[-Wimplicit-function-declaration]
doSomething(3, 4);
^
hello.c:17:6: error: conflicting types for
'doSomething'
void doSomething(int value1, char value2) {
^
hello.c:13:3: note: previous implicit declaration
is here
doSomething(3, 4);
^
1 warning and 1 error generated.
The warning you get regards the ordering, which I already mentioned.
The error is about another thing, related. Since C does not “see” the function
declaration before the invocation, it must make assumptions. And it assumes the
function to return int . The function however returns void , hence the error.
In any case, make sure you declare the function before using it. Either move the
function up, or add the function prototype in a header file.
Inside a function, you can declare variables.
Inside a function, you can call the function itself. This is called recursion and it’s
something that offers peculiar opportunities.
This is not something unique to C, of course. It’s common for the language core to
be agnostic of I/O.
#include <stdio.h>
printf()
scanf()
sscanf()
fgets()
fprintf()
Before describing what those functions do, I want to take a minute to talk about I/O
streams.
We have 3 kinds of I/O streams in C:
With I/O functions we always work with streams. A stream is a high level interface
that can represent a device or a file. From the C standpoint, we don’t have any
difference in reading from a file or reading from the command line: it’s an I/O
stream in any case.
Some functions are designed to work with a specific stream, like printf() , which we
use to print characters to stdout . Using its more general counterpart fprintf() , we
can specify the stream to write to.
11.1. printf()
printf() is one of the first functions you’ll use when learning C programming.
printf("hey!");
and the program will print the content of the string to the screen.
You can print the value of a variable, and it’s a bit tricky because you need to add a
special character, a placeholder, which changes depending on the type of the
variable. For example we use %d for a signed decimal integer digit:
%c for a char
%s for a string
%p for pointers
We can use escape characters in printf() , like \n which we can use to make the
output create a new line.
11.2. scanf()
This function is used to get a value from the user running the program, from the
command line.
We must first define a variable that will hold the value we get from the input:
int age;
Then we call scanf() with 2 arguments: the format (type) of the variable, and the
address of the variable:
scanf("%d", &age);
If we want to get a string as input, remember that a string name is a pointer to the
first character, so you don’t need the & character before it:
char name[20];
scanf("%s", name);
#include <stdio.h>
int main(void) {
char name[20];
printf("Enter your name: ");
scanf("%s", name);
printf("you entered %s", name);
}
This means that it will be available in some places, but not in others.
global variables
local variables
This is the difference: a variable declared inside a function is a local variable, like this:
int main(void) {
int age = 37;
}
Local variables are only accessible from within the function, and when the function
ends they stop their existence. They are cleared from the memory (with some
exceptions).
int main(void) {
/* ... */
}
Global variables are accessible from any function of the program, and they are
available for the whole execution of the program, until it ends.
I mentioned that local variables are not available any more after the function ends.
The reason is that local variables are declared on the stack, by default, unless you
explicitly allocate them on the heap using pointers, but then you have to manage
the memory yourself.
I said “inside a function”, because global variables are static by default, so there’s
no need to add the keyword.
int incrementAge() {
int age = 0;
age++;
return age;
}
If we call incrementAge() once, we’ll get 1 as the return value. If we call it more than
once, we’ll always get 1 back, because age is a local variable and it’s re-initialized to
0 on every single function call.
int incrementAge() {
static int age = 0;
age++;
return age;
}
Now every time we call this function, we’ll get an incremented value:
printf("%d\n", incrementAge());
printf("%d\n", incrementAge());
printf("%d\n", incrementAge());
will give us
1
2
3
We can also omit initializing age to 0 in static int age = 0; , and just write static int
We can also have static arrays. In this case, each single item in the array is initialized
to 0:
int incrementAge() {
static int ages[3];
ages[0]++;
return ages[0];
}
A local variable is defined inside a function, and it’s only available inside that
function.
Like this:
#include <stdio.h>
int main(void) {
char j = 0;
j += 10;
printf("%u", j); //10
}
j is not available anywhere outside the main function.
#include <stdio.h>
char i = 0;
int main(void) {
i += 10;
printf("%u", i); //10
}
A global variable can be accessed by any function in the program. Access is not
limited to reading the value: the variable can be updated by any function.
Due to this, global variables are one way we have of sharing the same data between
functions.
The main difference with local variables is that the memory allocated for variables is
freed once the function ends.
Starting from the built-in C types, we can create our own types, using this syntax:
and once you do so, you can define new NUMBER variables:
NUMBER one = 1;
Now you might ask: why? Why not just use the built-in type int instead?
Well, typedef gets really useful when paired with two things: enumerated types and
structures.
typedef enum {
//...values
} TYPENAME;
typedef enum {
true,
false
} BOOLEAN;
C comes with a bool type, so this example is not really practical, but you get the
idea.
typedef enum {
monday,
tuesday,
wednesday,
thursday,
friday,
saturday,
sunday
} WEEKDAY;
#include <stdio.h>
typedef enum {
monday,
tuesday,
wednesday,
thursday,
friday,
saturday,
sunday
} WEEKDAY;
int main(void) {
WEEKDAY day = monday;
if (day == monday) {
printf("It's monday!");
} else {
printf("It's not monday");
}
}
This means the conditional could have been if (day == 0) instead of if (day ==
monday) , but it’s way simpler for us humans to reason with names rather than
numbers, so it’s a very convenient syntax.
17. Structures
Using the struct keyword we can create complex data structures using basic C
types.
struct <structname> {
//...variables
};
Example:
struct person {
int age;
char *name;
};
You can declare variables that have as type that structure by adding them after the
closing curly bracket, before the semicolon, like this:
struct person {
int age;
char *name;
} flavio;
struct person {
int age;
char *name;
} flavio, people[20];
In this case I declare a single person variable named flavio , and an array of 20
person named people .
struct person {
int age;
char *name;
};
and once we have a structure defined, we can access the values in it using a dot:
struct person {
int age;
char *name;
};
struct person {
int age;
char *name;
};
flavio.age = 38;
Structures are very useful because we can pass them around as function parameters,
or return values, embedding various variables within them, and each variable has a
label.
It’s important to note that structures are passed by copy, unless of course you pass
a pointer to a struct, in which case it’s passed by reference.
Using typedef we can simplify the code when working with structures.
typedef struct {
int age;
char *name;
} PERSON;
PERSON flavio;
For simple needs, all you need to do so is change the main() function signature from
int main(void)
to
argc is an integer number that contains the number of parameters that were
provided in the command line.
When the program starts, we are provided the arguments in those 2 parameters.
Note that there’s always at least one item in the argv array: the name of the
program
Let’s take the example of the C compiler we use to run our programs, like this:
If this was our program, we’d have argc being 4 and argv being an array containing
gcc
hello.c
-o
hello
#include <stdio.h>
If the name of our program is hello and we run it like this: ./hello , we’d get this as
output:
./hello
If we pass some random parameters, like this: ./hello a b c we’d get this output to
the terminal:
./hello
a
b
c
This system works great for simple needs. For more complex needs, there are
commonly used packages like getopt.
You can move parts of a program to a separate file, then you create a header file.
A header file looks like a normal C file, except it ends with .h instead of .c , and
instead of the implementations of your functions and the other parts of a program,
it holds the declarations.
You already used header files when you first used the printf() function, or other I/O
function, and you had to type:
#include <stdio.h>
to use it.
The preprocessor goes and looks up the stdio.h file in the standard library, because
you used brackets around it. To include your own header files, you’ll use quotes, like
this:
#include "myfile.h"
#include "myfolder/myfile.h"
Let’s make an example. This program calculates the years since a given year:
#include <stdio.h>
int main(void) {
printf("%u", calculateAge(1983));
}
And a calculate_age.h file where I put the function prototype, which is same as the
function in the .c file, except the body:
Now in the main .c file we can go and remove the calculateAge() function
definition, and we can import calculate_age.h , which will make the calculateAge()
function available:
#include <stdio.h>
#include "calculate_age.h"
int main(void) {
printf("%u", calculateAge(1983));
}
Don’t forget that to compile a program composed by multiple files, you need to list
them all in the command line, like this:
And with more complex setups, a Makefile is necessary to tell the compiler how to
compile the program.
It parses our program and makes sure that the compiler gets all the things it needs
before going on with the process.
For example, it looks up all the header files you include with the #include directive.
It also looks at every constant you defined using #define and substitutes it with its
actual value.
That’s just the start, and I mentioned those 2 operations because they are the most
common ones. The preprocessor can do a lot more.
Did you notice #include and #define have a # at the beginning? That’s common to
all the preprocessor directives. If a line starts with # , that’s taken care by the
preprocessor.
20.1. Conditionals
One of the things we can do is to use conditionals to change how our program will
be compiled, depending on the value of an expression.
#include <stdio.h>
int main(void) {
#if DEBUG == 0
printf("I am NOT debugging\n");
#else
printf("I am debugging\n");
#endif
}
#define VALUE 1
#define PI 3.14
#define NAME "Flavio"
When we use NAME or PI or VALUE in our program, the preprocessor replaces its
name with the value, before executing the program.
Symbolic constants are very useful because we can give names to values without
creating variables at compilation time.
20.3. Macros
With #define we can also define a macro. The difference between a macro and a
symbolic constant is that a macro can accept an argument and typically contains
code, while a symbolic constant is a value:
Notice the parentheses around the arguments, a good practice to avoid issues
when the macro is replaced in the precompilation process.
The big difference with functions is that macros do not specify the type of their
arguments or return values, which might be handy in some cases.
20.4. If defined
#include <stdio.h>
#define VALUE 1
int main(void) {
#ifdef VALUE
printf("Value is defined\n");
#else
printf("Value is not defined\n");
#endif
}
We also have #ifndef to check for the opposite (macro not defined).
We can also use #if defined and #if !defined to do the same task.
It’s common to wrap some block of code into a block like this:
#if 0
#endif
#define DEBUG 0
#if DEBUG
//code only sent to the compiler
//if DEBUG is not 0
#endif
The preprocessor also defines a number of symbolic constants you can use,
identified by the 2 underscores before and after the name, including:
SQL
SUBSCRIBE TO MY NEWSLETTER
Enter your email
SUBSCRIBE NOW