C Cheatsheet: A Beginner’s Notebook
Contents
- Introduction
- Memory Management in C Programs
- Preventing Memory Corruption
- Returning Data from Functions
- Iterating Over Arrays and Strings
- Declaration of Local Variables
- Read-only Function Parameters Using
const
- Functions Without Parameters
- Convention for Writing Pointers
- Error Handling and Resource Cleanup Pattern
- C Programming with the Code::Blocks IDE
- Terminology used in C Programming
- Common Abbreviations Used in C Programming
- Useful Links
Introduction
This document provides an introduction to programming in the C programming language for those familiar with programming in general and constructs such as arrays, pointers, strings, and procedures in particular. It aims to fill the gap between sole explanation of theoretical concepts and development of programs using C without knowledge of relevant pitfalls. Practical examples are given to demonstrate the presented concepts. The provided recommendations enable well-founded decisions for the application of a certain approach to solve a problem.
Memory Management in C Programs
C only provides basic automatic memory management. Therefore, code written in C is prone to memory leaks (memory is not made available for reuse when it is not needed any more) and memory corruption (e. g., by overwriting data using a pointer still pointing to already reused memory). Memory on the stack occupied by a procedure’s local variables will be freed automatically when the procedure is left.
Freeing dynamically allocated memory
Programs often acquire memory at runtime using the functions malloc
, calloc
, and realloc
. On many architectures the memory is allocated on the heap. This memory has to be returned for reuse when it is no longer needed by calling the free
function on the pointer (memory address) obtained when allocating the memory. The example program below creates a storage whose size depends on the user’s input. Then the storage is filled with numbers entered by the user. Afterwards, the storage is freed:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
printf("Enter the size of the storage: ");
int size;
scanf("%d", &size);
if (size <= 0)
{
printf("Size must be greater than 0.\n");
return EXIT_FAILURE;
}
int *numbers = malloc(sizeof(int) * size);
if (numbers == NULL)
{
printf("Memory allocation failed.\n");
return EXIT_FAILURE;
}
for (int i = 0; i < size; i++)
{
printf("Enter a number: ");
int input;
scanf("%d", &input);
numbers[i] = input;
}
for (int i = 0; i < size; i++)
printf("numbers[%d] = %d\n", i, numbers[i]);
free(numbers);
numbers = NULL;
return EXIT_SUCCESS;
}
It is generally good practice to set pointers to NULL
after calling free
to prevent writing to the memory pointed to by the variable inadvertently. In addition, declaring the pointer variable as read-only using const t *const v
both prevents losing the pointer by assigning another pointer to the variable and changing the data it points to.
In more complex data structures, a clear policy should be established regarding the responsibility of freeing memory when objects are deleted. Take a linked list consisting of nodes as an example:
// Forward declaration required because nodes store pointers to nodes.
typedef struct node node;
typedef struct node
{
node *next;
void *value;
};
Each instance of node
stores a pointer to the next node in the list in its member next
and a pointer to data assigned to the node in its member value
. Implementation of this rather simple data structure raises several questions regarding memory management, for example:
Can nodes only be created on the heap via designated functions of the list data structure or can they be created independent from a list for later insertion into a list?
Should the list data structure free memory of nodes when they get removed from the list? This would leave invalid pointers to individual nodes stored outside the list, but the list cannot set these pointers to
NULL
.Should external data pointed to by the nodes’ member
value
be freed too upon deletion of a node? What if it is the only pointer to dynamically allocated memory available in the program?
A possible solution to the problems regarding the linked list looks like this:
The list is solely responsible for memory mangement of itself and its nodes (allocation, freeing). It will will free memory of nodes when they are removed from the list and provide a desginated function to free all memory occupied by the list and its nodes.
Nodes stored in the list must solely be created and deleted by the list.
The
next
pointers of individual nodes must solely be altered by the list. (The list has to keep track of all nodes for freeing upon destruction of the whole list. Breaking the list into two would make the nodes after the removed one inaccessible for the list.)The list does not free memory occupied by data pointed to by the nodes’
data
member. Pointers to data have to be stored stored outside the nodes for future cleanup prior to removing according nodes or deletion of the whole list.
Unfortunately, C does not support suitable mechanisms to enforce such policies, such as encapsulation, special reference/pointer types, garbage collection, etc. The only solution is proper documentation and careful implementation.
Recommendations
The rules for memory management of objects can be generalized:
The creator (memory allocation) of an object has the sole responsibility for its deletion (freeing of memory).
Objects must only be manipulated using designated manipulator functions.
Pointers to foreign objects should only be stored sparingly and as long as absolutely necessary (obtain pointer, perform operation using the pointer, setting the pointer to
NULL
) to prevent dangling pointers after object destruction.
Preventing Memory Corruption
C does not provide protection against manipulating memory outside the memory location reserved for a certain variable. The program below demonstrates how data stored in a variable can be corrupted when using the strcpy
function to overwrite a string’s null terminator and writing into the memory of another variable:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
char s1[] = "Daniel";
const char s2[] = "Francis";
printf(
"s1 = \"%s\" (strlen: %ld, sizeof: %ld)\n"
"s2 = \"%s\" (strlen: %ld, sizeof: %ld)\n",
s1, strlen(s1), sizeof s1,
s2, strlen(s2), sizeof s2
);
const char *const src = "Christopher";
strcpy((char *)s1, src);
printf(
"s1 = \"%s\" (strlen: %ld, sizeof: %ld)\n"
"s2 = \"%s\" (strlen: %ld, sizeof: %ld)\n",
s1, strlen(s1), sizeof s1,
s2, strlen(s2), sizeof s2
);
return EXIT_SUCCESS;
}
The two local string variables s1
and s2
are stored on the stack. When using GCC, the variables are stored in the memory right after each other (this may, however, vary between compilers and systems). The memory looks like that (»0« signifies the string’s null terminator):
Daniel0Francis0
^s1 ^s2
By copying a longer name to s1
using strcpy
, the null terminator of s1
and the content of s2
get overwritten with the characters »opher« and a new null terminator. The array variables s1
and s2
, however, still point to their original memory locations. The memory looks like that after calling strcpy
:
Christopher0is0
^s1 ^s2
The strlen
function returns the number of characters in a string without its null terminator. The sizeof
operator determines the size of an array in bytes (one char
is one byte). Writing over an array’s boundary does not increase its size, but changes the string’s length, as can be seen in the program’s output:
s1 = "Daniel" (strlen: 6, sizeof: 7)
s2 = "Francis" (strlen: 7, sizeof: 8)
s1 = "Christopher" (strlen: 11, sizeof: 7)
s2 = "pher" (strlen: 4, sizeof: 8)
Situations like this can be prevented by checking the destination’s length prior to writing to it:
char s1[] = "Daniel";
const char s2[] = "Francis";
const char *src = "Joseph"; // Source fits into destination, copy.
// `const char *src = "Richard";` // Source is too long, do not copy.
if (strlen(src) < sizeof s1)
strcpy((char *)s1, src);
else
printf("String variable `s1` is too short.\n");
printf(
"s1 = \"%s\" (strlen: %ld, sizeof: %ld)\n",
s1, strlen(s1), sizeof s1
);
Recommendations
The size of the destination variable’s memory can be checked manually using the
sizeof <variable>
operator.The
_s
versions of the string manipulation functions, such asstrcpy_s
, can be used, which provide size parameters and error checking. However, they are poorly supported and only available since C17 (in theory). In order to use them,#define __STDC_WANT_LIB_EXT1__ 1
has to be defined before inclusion ofstring.h
.
Returning Data from Functions
Passing data from functions back to the calling code is essential for procedural programming. C provides multiple methods to return data from functions, which might be more or less suitable in a certian scenario.
Values in function return values
Simple values, such as numbers, single characters, enumerations, and structures can be returned directly by value:
#include <stdio.h>
#include <stdlib.h>
int add(const int a, const int b)
{
int sum = a + b; // Local variable stored on the stack.
return sum; // Return copy of the variable's value.
}
int main(void)
{
int sum = add(10, 20);
printf("sum = %d\n", sum);
return EXIT_SUCCESS;
}
Pointers in function return values
Local variables are stored on the stack (except those marked as static
) and data stored in them gets lost after the procedure is left. Therefore, local variables such as arrays or pointers to local variables must not be returned by a function, as the returned pointer will be invalid:
#include <stdio.h>
#include <stdlib.h>
// Do not use!
char *get_string(void)
{
char result[] = "Hello World!"; // Local variable stored on the stack.
// GCC warning: function returns address of local variable
// [-Wreturn-local-addr]
return result;
}
int main(void)
{
char *result = get_string();
printf("result = \"%s\".\n", result);
return EXIT_SUCCESS;
}
Manipulate data passed in pointer parameters
To return a string from a function, usually a buffer is passed to the function and the function writes the value to the buffer or manipulates its content:
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
bool get_string(char *const buf, const size_t len)
{
const char src[] = "Hello World!"; // 13 chars including null terminator.
if (len <= strlen(src))
return false;
strcpy(buf, src);
return true;
}
int main(void)
{
#define BUF_LEN 40
char buf[BUF_LEN];
if (get_string(buf, BUF_LEN))
printf("buf = \"%s\" (strlen+1: %ld)\n", buf, strlen(buf) + 1);
else
{
printf("Buffer is too small.");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
#undef BUF_LEN
}
Using pointers for output parameters
Output parameters can be used to pass data back to the caller of a function. These parameters hold a pointer to a variable that should receive the result. The parameters are passed by reference:
#include <stdio.h>
#include <stdlib.h>
void add(const int a, const int b, int *const sum)
{
*sum = a + b; // Write result to address in memory pointed to by `sum`.
}
int main(void)
{
int sum;
add(3, 5, &sum); // Pass memory address of variable `sum` to the function.
printf("3 + 5 = %d.\n", sum);
return EXIT_SUCCESS;
}
Allocating memory and returning pointers
As an alternative to passing a buffer to the function, the function itself can create a string on the heap instead of the stack. The function then returns the pointer to the newly created string in an output parameter result
. The caller passes a variable to the function, the function allocates the string containing the result and makes the passed pointer variable point to the result string:
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Equivalent to `bool get_string(const char *result[const])`.
bool get_string(const char **const result)
{
const char src[] = "Hello World!"; // 13 chars including null terminator.
// Allocate string on the heap, the one additonal character is used for the
// null terminator.
char *const dst = malloc(sizeof(char) * strlen(src) + 1);
if (dst == NULL) // Allocation failed.
return false;
strcpy(dst, src);
*result = dst; // Make result parameter point to the string on the heap.
return true;
}
int main(void)
{
const char *result = NULL;
if (!get_string(&result)) // `result` points to the string on the heap.
{
printf("String allocation failed.\n");
return EXIT_FAILURE;
}
printf("result = \"%s\"\n", result);
// Free the string on the heap to prevent a memory leak.
free((void *)result);
result = NULL;
return EXIT_SUCCESS;
}
In some cases it might be suitable to return the pointer to the result in the function’s return value, or NULL
to indicate failure:
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char *get_string(void)
{
const char src[] = "Hello World!"; // 13 chars including null terminator.
char *const dst = malloc(sizeof(char) * strlen(src) + 1);
if (dst == NULL) // Allocation failed.
return NULL;
return strcpy(dst, src);
}
int main(void)
{
const char *result = get_string();
if (result == NULL) // `NULL` signifies failure.
{
printf("String allocation failed.\n");
return EXIT_FAILURE;
}
printf("result = \"%s\"\n", result);
free((void *)result);
result = NULL;
return EXIT_SUCCESS;
}
Returning data using a structure
If a function should return multiple values, they can be combined in a struct
. Structures are passed by value, therefore they can be returned in the function’s return value or passed back in an output parameter using a pointer. In case of larger structures (typically larger than 4 integers) using an output parameter or allocating memory for the structure on the heap should be considered:
#include <math.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
typedef struct
{
double circumference;
double area;
}
circle_data;
circle_data calc_circle(const double r)
{
circle_data retval;
retval.circumference = 2 * r * M_PI;
retval.area = pow(r, 2) * M_PI;
return retval;
}
bool calc_circle2(const double r, circle_data *const result)
{
if (r < 0)
return false;
result->circumference = 2 * r * M_PI;
result->area = pow(r, 2) * M_PI;
return true;
}
int main(void)
{
const double r = 10;
// Returning result in the function's return value.
circle_data result = calc_circle(r);
printf(
"Circle with radius %f cm:\n"
"Circumference: %f cm\n"
"Area: %f cm²\n",
r, result.circumference, result.area
);
// Returning result in an output parameter.
circle_data result2;
if (calc_circle2(r, &result2))
printf(
"Circle with radius %f cm:\n"
"Circumference: %f cm\n"
"Area: %f cm²\n",
r, result.circumference, result.area
);
else
{
printf("Radius must be >= 0.\n");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
const
in function return types
C does not support the concept of const correctness, as it is available in the C++ programming language. In C, const
can be used as a qualifier of a function’s return type. Declaring the return type as const
serves multiple purposes. Marking values or pointers as const
enables additional compile-time checks and documents how the return values should be treated by the caller. Additionally, it allows for certain optimizations by the compiler.
Returning const
to preserve the qualifier
If a value passed to a function as const
is returned by the function, it can be marked as const
to preserve the qualifier:
#include <stdio.h>
#include <stdlib.h>
// Return `s` preserving the `const` qualifier.
const char *returns(const char *s)
{
return s;
}
int main(void)
{
const char *s = "Hello World!";
s = returns(s);
return EXIT_SUCCESS;
}
Returning const
to indicate a value is read-only
The function get_string
below returns a string literal that must not be modified as const char *
. The pointer itself is not qualified as const
to indicate that it can be discarded and must not be freed by the caller:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
const char *get_string(void)
{
return "Hello World!";
}
int main(void)
{
const char *s = get_string();
printf("%s\n", s);
// Fails because `s` is `const char *`.
//strcpy(s, "Goodbye World!");
return EXIT_SUCCESS;
}
Returning const
pointers to indicate the pointer has to be freed
The get_string
function below allocates a string and returns the pointer as *const
. This prevents modification of the pointer and indicates that the returned pointer has to be retained as it has to be freed by the caller of get_string
:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char *const get_string(void)
{
char *const s = malloc(sizeof(char) * 20);
strcpy(s, "Hello World!");
return s;
}
int main(void)
{
char *const s = get_string();
printf("%s\n", s);
// Fails because `s` is a read-only variable.
//s = "Goodbye World!";
free((void *)s);
return EXIT_SUCCESS;
}
As with function parameter types, the qualifier const
can also be applied to both the type and the pointer, e. g., const char *const get_string(void)
. In this case calling strcpy(s, "Goodbye World!")
inside main
would lead to a warning because this call would discard const
from the target type.
Recommendations
Simple values, such as integers, can be returned in the function’s return value. Multiple values can be returned in a
struct
containing multiple members or in output parameters.Returning strings is usually done by passing a buffer to the function that takes the string that should be returned. The characters of strings passed to a function via parameters, such as
char *const s
, can be manipulated inside the function, e. g., using functions ofstring.h
.Allocating a string inside the function and passing back the pointer in an output parameter or as the function’s return value is rather uncommon, as it leaves freeing of the string to the caller, which might not be obvious and requires exact documentation (strings stored in a special data section of the memory must not be freed using
free
).The rules demonstrated for strings also apply to arrays of and pointers to other data types.
Iterating Over Arrays and Strings
C supports multiple ways to iterate over arrays and strings, based on language features like loops (for
, while
, do…while
), arrays, and pointers. In most cases the decision for a certain iteration method should be based on safety and understandability of the developed source code.
Iterating over arrays
Iterating using a for
loop
In order to loop over an array using a for
loop, the length of the array has to be known at runtime. The naïve implementation below is suboptimal, as the array length has to be kept in sync manually in multiple places (the array length is optional in the array declaration, which can be alternatively written as const int numbers[] = { … }
):
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
const int numbers[5] = { 1, 3, 8, 2, 7 };
int sum = 0;
for (int i = 0; i < 5; i++)
sum += numbers[i];
printf("%d\n", sum);
return EXIT_SUCCESS;
}
A preprocessor #define
macro can be utilized to make the array length available where it is needed:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
#define NUMBERS_LEN 5
const int numbers[NUMBERS_LEN] = { 1, 3, 8, 2, 7 };
int sum = 0;
for (int i = 0; i < NUMBERS_LEN; i++)
sum += numbers[i];
printf("%d\n", sum);
return EXIT_SUCCESS;
#undef NUMBERS_LEN
}
Using a constant variable like const int numberslen = 4
containing the number of elements in the array declaration const int numbers[numberslen] = { … }
does not work.
The number of elements in the array can also be determined dynamically with the sizeof
operator and a simple division of the total size (in bytes) of the array by the size (in bytes) of a single element of the array:
const int numbers[] = { 1, 3, 8, 2, 7 };
const size_t numberslen = sizeof numbers / sizeof numbers[0];
printf("Length of array: %ld\n", numberslen);
int sum = 0;
for (int i = 0; i < numberslen; i++)
sum += numbers[i];
printf("%d\n", sum);
Elements of an array can too be accessed via a pointer instead of the index, as in numbers[…]
:
const int numbers[] = { 1, 3, 8, 2, 7 };
const size_t numberslen = sizeof numbers / sizeof numbers[0];
const int *p = numberslen;
int sum = 0;
for (int i = 0; i < numberslen; i++)
sum += *p++;
printf("%d\n", sum);
Iterating over a string (array of char
)
Strings in C are arrays of char
that are terminated by a null character '\0'
(not to be confused with the NULL
pointer or EOF
code used to indicate the end of a stream or file). Since char
is a numeric integer type, the null terminator is simply the integer value 0.
const char s[] = "Hello World!";
int i = 0;
while (s[i])
putc(s[i++], stdout);
putc('\n', stdout);
Iterating using a for
loop
In order to loop over the characters of a string, the string’s length has to be either known or determined using the strlen
function. The sample below prints a string char-by-char and ends the output by a newline:
const char s[] = "Hello World!";
const int slen = strlen(s);
for (int i = 0; i < slen; i++)
putc(s[i], stdout);
putc('\n', stdout);
Iterating using a while
loop
As strings are null-terminated in C, the last array element contains a null character (written as char
literal '\0'
or simply 0
). This information can be utilized to iterate over a string of unknown length using a while
loop. No code changes are necessary when the length of the string gets changed. The head of the while
loop below can be shortened to while (c = *s++)
or written as while ((c = *s++) != 0)
:
const char *s = "Hello World!";
char c;
while ((c = *s++) != '\0')
putc(c, stdout);
putc('\n', stdout);
In order to obtain the pointer to the current character inside the loop, incrementation of the pointer has to be done at the bottom of the loop:
const char *s = "Hello World!";
while (*s)
{
putc(*s, stdout);
s++;
}
putc('\n', stdout);
However, by incrementing s
we lose the pointer to the initial string. This can be fixed easily as follows:
const char *const s = "Hello World!";
const char *cp = s;
while (*cp)
putc(*cp++, stdout);
putc('\n', stdout);
printf("%s\n", s);
Having a pointer to the current element available inside the loop is useful to manipulate the character it points to. The string must not be declared and initialized as char *s = "Hello World!"
in the example below because it has to be mutable:
const char s[] = "Hello World!";
char *cp = (char *)s;
while (*cp)
{
char c = *cp;
if (c >= 'a' && c <= 'z')
*cp = c - 32; // Make uppercase.
sp++;
}
printf("%s\n", s);
In the example above the variable cp
is also visible outside the loop, which is unfortunate. This can be avoided by placing the code in an unnamed scope. Alternatively, a for
loop and array brackets can be used to access and manipulate individual characters of the string:
char s[] = "Hello World!";
int slen = strlen(s);
for (int i = 0; i < slen; i++)
if (s[i] >= 'a' && s[i] <= 'z')
s[i] -= 32; // Make uppercase.
printf("%s\n", s);
A for
loop can even be utilized to iterate using a pointer:
char s[] = "Hello World!";
for (char *cp = s; *cp; cp++)
if (*cp >= 'a' && *cp <= 'z')
*cp -= 32; // Make uppercase.
printf("%s\n", s);
The solution below works with the C compiler GCC, but not with Clang. GCC performs the pointer incrementation cp++
after executing the loop’s body, whereas Clang increments cp
before its execution (when incrementation occurs is undefined behavior):
const char s[] = "Hello World!";
// Undefined behavior, do not use!
for (char cp = (char *)s, c; (c = *cp) != '\0'; cp++)
if (c >= 'a' && c <= 'z')
*cp = c - 32; // Make uppercase.
printf("%s\n", s);
Declaration of Local Variables
In C local variables can be declared almost everywhere inside a function body nowadays. C99 lifted the variable declaration at top of block constraint, which enforced variable declarations directly after the opening {
. The code below illustrates visibility and scope of local variables declared in various places:
void f(void)
{
int v1; // Compiles.
g();
int v2; // Compiles. (Did not compile prior to C99.)
while (1)
{
int v3; // Compiles.
int v1; // Compiles. Note the same name as the variable `i` above,
// this is called _variable shadowing_.
g();
int v4; // Compiles. (Did not compile prior to C99.)
}
g(v4); // Does not compile. `v4` only visible in the `while` loop.
v1 = v2; // `v1` refers to variable declared on top of the function.
for (int v1 = 0; i < 10; i++) // Compiles. Variable shadowing of `v1`.
v2 = v1; // Variables declared in the `for` loop's
// head are also visible in its body.
v5 = 10; // Does not compile. Variables can only be used below their
int v5; // declaration.
{ // Unnamed scope.
int v1; // Compiles. Variable shadowing of `v1`.
int v6; // Compiles. Variable only visible in the containing block.
}
v6 = 10; // Does not compile. Variable `v6` not declared.
}
According to C99 variables cannot be declared directly after a label (case
, default
, or named label), as a statement is expected to follow the label. This limitation can be circumvented by placing an empty statement followed by the variable declaration or an unnamed scope containing the variable declaration directly after the label.
Recommendations
Visibility of variables should be as small as possible. Most variables should be declared in the section of code where they are used, this especially applies to helper variables and variables of minor importance. If possible, variables should be declared inside the scope where they are needed:
void f(const char s[const], const size_t len)
{
⋮
// Variables `i` and `c` are only available in the `for` loop's scope.
for (int i = 0; i < len; i++)
{
char c = s[i];
⋮
}
⋮
}
Read-only Function Parameters Using const
Function parameters can be marked with the qualifier const
to make them read-only. This allows for better optimization by the compiler as well as compile-time error detection. By marking a variable as read-only, its value cannot be changed directly via the variable’s name. It can only be changed intentionally after casting the variable to a non-const
type. This helps in preventing erroneous manipulation of variables.
The rules for const
function parameters also apply to const
variables. Errors and warnings in the examples below are taken from GCC version 11.4.0.
Uses of const
for function parameters
Marking value types parameters with the qualifier const
makes them read-only inside the function:
void f1(const char c) // Semantically equivalent to `void f1(char const c)`.
{
// GCC error: assignment of read-only parameter ‘c’
c = 'a';
}
Function parameters of pointer types can also be made const
so they cannot be manipulated inside the function:
void f2(const char *s) // Semantically equivalent to `void f2(char const *s)`.
{
// Compiles.
s = "Test";
// Compiles.
s++;
// GCC error: assignment of read-only location ‘*s’
*s = 'a';
}
void f3(char *const s)
{
// GCC error: assignment of read-only parameter ‘s’
s = "Test";
// GCC error: increment of read-only parameter ‘s’
s++;
// Compiles.
*s = 'a';
}
void f4(const char *const s)
{
// GCC error: assignment of read-only parameter ‘s’
s = "Test";
// GCC error: increment of read-only parameter ‘s’
s++;
// GCC error: assignment of read-only location ‘*(const char *)s’
*s = 'a';
}
Array function parameters can also be marked as const
:
void f5(char s[const])
{
// GCC error: assignment of read-only parameter ‘s’
s = "Test";
// GCC error: increment of read-only parameter ‘s’
s++;
// Compiles.
s[2] = 'a';
}
void f6(const char s[const])
{
// GCC error: assignment of read-only parameter ‘s’
s = "Test";
// GCC error: increment of read-only parameter ‘s’
s++;
// GCC error: assignment of read-only location ‘*(s + 2)’
s[2] = 'a';
}
In C++ there is a convention to write the function declaration as void f(const char* const s)
(pointer near the type). It can be read from right-to-left as s
is a const
pointer to a char
that is const
. More information can be found in the article Const Correctness, C++ FAQ.
Practical examples
Passing a read-only string (array of char
) to a function:
The variable cannot be changed to point to another string than the one passed to the function by assignment.
Pointer arithmetic on the variable is forbidden to guarantee that the variable always points to the same string in the function body.
The characters in the string cannot be altered using the variable.
char *read_file(const char *const filename)
{
⋮
}
Guaranteeing that values such as IDs, positions, etc. cannot be altered in the function body:
void show_record(const int id)
{
// Erroneous assignment (`=`) instead of comparison (`==`).
// GCC error: assignment of read-only parameter ‘s’
if (id = 100)
;
}
Passing a read-only array to a function:
void show_users(const char user_ids[const])
{
⋮
}
Passing a read-only string to a function and looping over the characters:
The type cast to non-
const
is optional (warning), but recommended.Pointer arithmetic using the parameter
s
such aswhile ((c = *s++) != '\0')
would not work becauses
is declared asconst char *const
.The variable
cp
is qualified byconst
to indicate that it cannot be used to manipulate the characters of the string.
void prints(const char *const s)
{
const char *cp = (const char *)s; // <-------------------------------- (1)
char c;
while ((c = *cp++) != '\0')
putc(c, stdout);
}
Note that even strings passed to a function in a const char *const
parameter can be altered, just not via the parameter directly:
Both
const
qualifiers do not make sense in the context of atoupper
function.The function contains a naïve implementation for demonstration purposes only.
#include <stdlib.h>
void toupper_bad(const char *const s) // <-------------------------------- (1)
{
char *cp = (char *)s;
char c;
while ((c = *cp) != '\0')
{
if (c >= 'a' && c <= 'z')
*cp = c - 32;
cp++;
}
}
int main(void)
{
char s[] = "Hello World!";
toupper_bad(s);
printf("%s\n", s); // Output: "HELLO WORLD!"
return EXIT_SUCCESS;
}
Functions Without Parameters
Functions without parameters should be defined and declared as t f(void)
. In C23 they can be defined and declared as t f()
.
Convention for Writing Pointers
The *
used for pointer declarations should be written near the function or variable name to prevent visual confusion with multiplications (v1 * v2
) and clarity regarding the type of individual variables in combined variable declarations (t *v1, v2
will create variable v1
of type t *
and v2
of type t
):
char *reverse(char *text);
int main(int argc, char **argv);
int *prev, *next;
int value = *prev;
int *prev = (int *)first;
int *next = &value;
The parameter declaration char **argv
denotes a pointer to a pointer to char
and is synonymous to char *argv[]
, which is an array of pointers to char
. The parameter declarations char* argv[]
and char* *argv
would arguably make these semantics more obvious, but they would break symmetry to the convention used in variable declarations (t *v1[], v2[]
and t **v1, v2
).
In C qualifiers such as const
are often written in front of the type in variable and parameter declarations, e. g., const char *const c
. In the declaration const char *c1, *c2
both variables are declared as a pointer to a char
that is const
. const
is written in front of the type because it—contrary to the pointer *
—belongs to the type, not the variable (const char *c1, const *c2
does not compile). In case of constant pointers const
has to be specified for every variable in a list of variable declarations, char *const c1, *const c2
declares two variables that are a const
pointer to a char
.
Error Handling and Resource Cleanup Pattern
The C standard library function free
provided by stdlib.h
has to be called to free memory allocated using functions like malloc
or calloc
. A sample error handling pattern is shown below.
Implementation notes
The first constant in an
enum
will automatically have the value 0. It is generally recommendable to return 0 in case of success and another value in case of an error to allow for simple error checking usingif (error) …
.The variable
retval
defaults toSUCCESS
. In case of an error an appropriate error code is assigned to the variable and returned in the code below theout
label.Two labels are defined,
cleanup_out
andout
. In case of an error the function can be left by jumping to the according label using thegoto
statement. This use ofgoto
is acceptable as it is used in a recognizable pattern and jumps are only performed downwards.The function
free
will do nothing ifNULL
is passed to it. Therefore, it is superfluous to distinguish different places wherefree
failed in the cleanup code.
Example program
#include <stdio.h>
#include <stdlib.h>
typedef enum {
SUCCESS, // <--------------------------------------------------------- (1)
ERR_INVALID_SIZE,
ERR_MALLOC_FAILED
} result;
result f(const size_t size)
{
int retval = SUCCESS; // <-------------------------------------------- (2)
if (size > 1000)
{
retval = ERR_INVALID_SIZE;
goto out; // <---------------------------------------------------- (3)
}
int *m1 = malloc(sizeof(int) * size);
if (m1 == NULL)
{
retval = ERR_MALLOC_FAILED;
goto cleanup_out;
}
char *m2 = malloc(sizeof(char) * size);
if (m2 == NULL)
{
retval = ERR_MALLOC_FAILED;
goto cleanup_out;
}
cleanup_out:
free(m1); // <-------------------------------------------------------- (4)
free(m2);
out:
return retval;
}
void printerr(const result err)
{
switch (err)
{
case ERR_INVALID_SIZE:
fprintf(stderr, "Invalid size supplied.\n");
break;
case ERR_MALLOC_FAILED:
fprintf(stderr, "Error allocating memory.\n");
break;
}
}
int main(void)
{
result err = f(2000);
if (err) // An error occurred.
{
printerr(err);
return EXIT_FAILURE;
}
printf("Program completed successfully.\n");
return EXIT_SUCCESS;
}
The pattern can be generalized as follows:
result f(…)
{
result retval = SUCCESS;
result err;
resource *r1 = open_resource1();
if (err = step1())
{
retval = ERR_STEP1_FAILED;
goto cleanup_step1_out;
}
resource *r2 = open_resource2();
if (err = step2())
{
retval = ERR_STEP2_FAILED;
goto cleanup_step2_step1_out;
}
resource *r3 = open_resource3();
if (err = step3())
{
retval = ERR_STEP3_FAILED;
goto cleanup_step3_step2_step1_out;
}
cleanup_step3_step2_step1_out:
free_resource(r3);
cleanup_step2_step1_out:
free_resource(r2);
cleanup_step1_out:
free_resource(r1);
out:
return retval;
}
C Programming with the Code::Blocks IDE
- Starting a program with arguments
- Menu Project → Set program’s arguments….
- Changing the terminal used to launch console programs
- Menu Settings → Environment…, section General settings, option Terminal to launch console programs. Various presets are available. The pre-configured Xterm is suboptimal as it does not allow for easy copying of text from the terminal.
Terminology used in C Programming
- length
- Length of an array (number of entries), length of a string (in characters).
- size
- Size of data in bytes, typically determined using the
sizeof
operator and stored in thesize_t
type. Size values can have a minimum (MIN
) and a maximum (MAX
).
Common Abbreviations Used in C Programming
Cryptic abbreviations should be generally avoided so source code can be easily read by other people. However, completely avoiding the use of abbreviations in names often leads to verbose code that distracts from its purpose.
The list below contains common abbreviations that can be safely used in C source code. (The abbreviations can be used in other programming languages too if no deviating naming conventions exist.)
In the naming convention proposed by the author, multiple words forming a name are concatenated using the underscore character (_
), e. g., get_address
. The underscore might be omitted if the name is short and composed of abbreviations (err_msg
vs. errmsg
) or a word and an abbreviation.
arg
,args
- argument. Used to refer to arguments, such as the command line arguments passed to a C program’s
int main(int argc, char *argv[])
function. buf
- buffer. Example: Variable used as a buffer, e. g.,
char buf[20]
. c
- count. Used in variable names as a suffix to indicate that a variable contains the number of certain items, such as in the
argc
(argument count) parameter of a C program’smain
function. cat
- category. Example: Variable
term_cat
, functionfind_cat
. cmd
,CMD
- command. Example: Definition
#define CMD_OPEN 1
. dst
,dest
- destination. Example: Variable used as a destination of an operation, e. g.,
char dst[40]
. err
,ERR
- error. Used as a variable containing an error code, such as
err
or as prefix for definitions of error codes, such asERR_INVALID_VALUE
. In the C standard library the error stream is namedstderr
. ex
,ext
- extended, extension. Used to mark a function with extended functionality, e. g.,
replace_ex
. i
- index. Example: Variable
iend
. id
- identifier. Unique identifier used to reference a certain record. Example: Variable
user_id
. len
- length. Example: Variable
file_len
, definitionLEN_MAX
. max
,MAX
- maximum. Used in variable names, constants, or definitions, e. g.,
INT_MAX
. mem
- memory. Example: Variable
mem_size
. min
,MIN
- minimum. See
max
,MAX
. msg
- message. Example: Variable
err_msg
. num
,NUM
- number, number of items. Used for numbered items, such as the variable
room_num
, or in macro definitions, such as#define NUM_ROOMS 24
. opt
,opts
- option. Example: Variable
compile_opts
. p
,ptr
- pointer. Used as a suffix for a variable or parameter name, such as
memptr
ormemp
, to indicate that the variable is intended for pointer arithmetic as opposed to a simple reference to an entity. pos
- position. Example: Variable
start_pos
. prev
- previous. Used in contexts such as
prev
andnext
to indicate the previous element. retval
- return value. Used as a variable name to store a function’s return value.
s
- Plural. Appended to indicate a plural, e. g.,
args
,opts
. src
- source. Example: Variable used as the source of a copy operation, e. g.,
char *src = "Hello"
. std
- standard. Used in the C standard library, e. g., in header filenames such as
stdlib.h
or in stream names such asstdin
. v
,val
- value. Used to indicate a value, e. g., in (
key
,val
) or (k
,v
) pairs, or as a suffix indicating that a variable contains values, such as thechar *argv[]
(argument values) parameter of a C program’smain
function. var
- variable.
Useful Links
- OnlineGDB – Online C Compiler
- Develop, run, and debug C programs online.
- Code::Blocks
- Free and open-source IDE for C and C++ that runs on various operating systems.
- C reference – cppreference.com
- Compact but extensive reference for the C programming language and the C standard library.