C
C is an imperative memory-unsafe low-level general-purpose programming language created by Dennis Ritchie in 1972 at Bell Laboratories as a successor to the B programming language. It was initially proposed as a minimalist language for systems implementation, particularly for re-implementing the UNIX operating system kernel.
Nowadays C is sometimes called the "mother of programming languages" due to its influence in numerous modern languages, such as C++ and Java, although unlike most modern programming languages, C is statically typed and lacks support for features like automatic garbage collection, object-oriented programming, operator overloading, templates, and (safe) generics.
C's design emphasises efficiency, simplicity, and low-level access to memory (and other resources, such as CPU registers) and therefore is highly suitable for system-level programming, but less so for high-level programming using high level abstractions, common in newer languages like Python and JavaScript.
However, aside from the standard library, developers are able to make use of the vast and mature C ecosystem full of third-party libraries for more specific needs with tailored higher-level abstractions, such as graphics (e.g. Cairo), multimedia processing (e.g. FFmpeg), database management (e.g. SQLite), asynchronous I/O (e.g. libuv), and even web applications (e.g. Vessel).
Basic Syntax
C has a really simple syntax with only 32 keywords, however it includes certain components to keep in mind:
Functions
return_type function_name(parameter_list) {
// Function body
return value; // Optional, depending on the return type
}
Return type must either be a primitive type, a custom type (typedef
), or void
which states that a function does not return anything. The parameter list must be a list of variable definitions (type_name variable_name
such as const char *name
) or void
, indicating no arguments (often used in entry points, because by default all functions in C are variadic)
Header Files and Main Function
#include <stdio.h>
int main() {
// Program code here
return 0;
}
This is to:
- Include necessary libraries like
stdio.h
for input/output operations. - The
main()
function is the entry point of nearly every C program.
Keywords
Keyword | Description |
---|---|
auto
|
Declares an automatic (local) variable. |
break
|
Exits a loop or switch statement. |
case
|
Defines a branch in a switch statement. |
char
|
Declares a character variable. |
const
|
Declares a constant value. |
continue
|
Skips the current iteration of a loop. |
default
|
Defines the default branch in a switch statement. |
do
|
Starts a do-while loop. |
double
|
Declares a double-precision floating-point variable. |
else
|
Provides an alternative branch in an if statement. |
enum
|
Defines an enumeration type. |
extern
|
Declares an external variable or function. |
float
|
Declares a floating-point variable. |
for
|
Starts a for loop. |
goto
|
Transfers control to a labeled statement. |
if
|
Starts a conditional statement. |
int
|
Declares an integer variable. |
long
|
Declares a long integer variable. |
register
|
Declares a register variable for faster access. |
return
|
Exits from a function and optionally returns a value. |
short
|
Declares a short integer variable. |
signed
|
Specifies that a variable can hold positive and negative values (default for int) |
sizeof
|
Determines the size of a data type or variable in bytes. |
static
|
Declares a static variable that retains its value between function calls. |
struct
|
Defines a structure type. |
switch
|
Starts a switch statement for multi-way branching. |
typedef
|
Creates an alias for a data type. |
union
|
Defines a union that shares memory among its members. |
unsigned
|
Specifies that a variable can only hold positive values, increasing its range. |
void
|
Specifies no return value or no parameters for functions, or an unknown type. |
volatile
|
Prevents compiler optimizations on variables that can be modified externally. |
while
|
Starts a while loop. |
Operators
Operator Type | Description | Examples |
---|---|---|
Arithmetic Operators | Perform mathematical operations on operands. | + (Addition), - (Subtraction), * (Multiplication), / (Division), % (Modulus)
|
Relational Operators | Compare two values and return a boolean result. | < , <= , > , >= , == , !=
|
Logical Operators | Perform logical operations and return boolean values. | && (AND), || (OR)
|
Bitwise Operators | Perform bit-level operations on operands. | & (AND), | (OR), ^ (XOR), << (Left shift), >> (Right shift)
|
Assignment Operators | Assign values to variables. | = , += , -= , *= , /= , %=
|
Unary Operators | Operate on a single operand. | ++ (Increment), -- (Decrement), ! (Logical NOT), ~ (Bitwise NOT), & (Address of), * (Dereference)
|
Ternary Operator | Evaluate a condition and return one of two values based on the result. | (condition) ? (if true) : (else)
|
Miscellaneous Operators | Include special-purpose operators such as size determination and member access. | . , -> , (type) (Typecasting), sizeof()
|
For example:
int magic = (5 * 7 - 1) ^ 0x1234;
Statements and Semicolons
Each statement ends with a semicolon:
int x = 123; // Assignment statement
printf("Hello, World!\n"); // Output statement
Conditionals
The if
keyword allows us execute a block of code if it is true:
if (x > 7) {
printf("x is greater than 7\n");
} else {
printf("x is less than or equal to 7\n");
}
You may also chain conditions using &&
(logical AND), ||
(logical OR), and similar. And also use else if
to add more fallback conditions:
if (...) {
...
} else if (...) {
...
} else if (...) {
...
} ...
else {
...
}
switch
and case
To efficiently match many values you can use simply a switch-case:
switch (x) {
case 11:
puts("case 11");
break; // required for it to not "fall through" to the next case"
case 19:
puts("case 19");
break;
case 99:
// puts("case is 99");
case 15:
puts("99 or 15, 99 falls through to the case 15");
break;
...
default:
puts("Default case: Unknown number!");
break;
}
Loops
for
Loops
for
loops execute a block of code for a specified number of iterations (or until a specified condition is specified):
for (int i = 0; i < 5; ++i) {
printf("%d\n", i);
}
This syntax means "create integer variable i and set it to 0 (setup - ran once); while i is less than 5 (condition); increment i by 1 (iteration)"
while
Loops
Similarly, while
loops allow us to execute a block of code while a condition is true, and are nearly equivalent to for loops and vice-versa:
int i = 1; // setup
while (i <= 5) { // condition
printf("%d\n", i);
++i; // iteration
}
do
-while
Loops
do
-while
loops first execute the body of the do { } block and then run again while the condition in while is true:
int i = 1;
do {
printf("%d\n", i);
++i;
} while (i <= 5);
Labels
Labels can also be used as loops in conjunction with the goto
keyword, because that's all they are on the assembly level. This is not recommended but you technically can:
int i = 1;
my_loop:
printf("%d\n", i);
++i;
if (i < 5)
goto my_loop;
... your code ...
continue
and break
The continue
keyword continues the execution onto the next iteration, whereas break
stops iteration overall.
Comments
Comments improve code readability and are ignored by the compiler:
// Single-line comment (C99+)
/*
* Multi-line comment
* Can be single-line as well :)
*/
Low-Level Keywords
C provides several keywords that offer low-level control over program behaviour and memory management. These keywords are especially useful for optimising performance and interacting directly with hardware.
extern
The extern
keyword is used to declare a variable or function that is defined in another translation unit (such as an object file) or file (C file). It allows programs to share global variables, functions, and other related symbols across multiple files.
For example:
extern int some_external_symbol; // Declaration of a variable defined elsewhere
This keyword is primarily used in header files to declare global variables or functions that are defined in separate source files.
extern
is the only one out of four low-level keywords that is not considered a type qualifier.
register
The register
keyword suggests to the compiler that a variable should be stored in a CPU register instead of RAM for much faster access. However, modern compilers often ignore this suggestion and optimise variable storage automatically. It mainly remains part of the language for backward compatibility purposes.
For example:
register int counter; // Suggest storing counter in a CPU register
register int foo asm("eax"); // Store foo in the eax register (GCC only)
restrict
The restrict
keyword is a type qualifier used with pointers which indicates that the pointer is the only means of accessing the object it points to during its lifetime, allowing for better optimization.
For example:
void do_stuff(int *restrict data) {
// Compiler assumes data is accessed exclusively here
}
Using restrict
can significantly improve performance in programs that perform heavy computations involving pointers or alike.
volatile
The volatile
keyword tells the compiler that a variable can change unexpectedly outside the program's control, such as by a different thread, and it prevents the compiler from optimising access to such variables, ensuring they are always read from memory.
volatile int x; // Compiler will never optimize x
Entry Point
C programs follow a structured execution flow, beginning with functions main()
and for low-level control: _start()
.
_start()
Function
The _start()
function marks the true beginning of a C program. This function is part of the C runtime library and is automatically linked to your program during compilation.
Its main job is to set up the environment for your main entry point to run and does important tasks like initialising global variables, creating the stack, and setting up the command-line arguments (argv
, argc
) that the program may use. After finishing these tasks, _start()
calls the main()
function so that the program can start running the user's code logic.
The _start()
function is not something you (usually) write yourself - it's built into the system's runtime. However, if you require such low-level control you may write the _start()
entry point with the following syntax:
#include <stdio.h>
#include <stdlib.h>
int my_main(void);
void _start(void) {
int ret = my_main();
exit(ret);
}
int my_main(void) {
puts("Custom _start without main()");
return 0;
}
Which you can compile like this:
$ gcc a.c -nostartfiles
$ ./a.out
Custom _start without main()
Note that the main function prototype must come before the definition of the _start()
function.
main()
Function
The main()
function is the standard entry point for most C program and it is where user-defined logic begins after the runtime environment has been initialised. The main()
function can take three common forms:
main()
Function without Command-Line Arguments
int main(void) {
... your code ...
return 0; /* 0 = success, >0 - failure */
}
(void)
explicitly specifies that this main will not have any arguments because by default functions in C are variadic unlike in C++
main()
Function with Command-Line Arguments
int main(int argc, char *argv[]) {
... your code ...
return 0;
}
argc
is the element count in the argv
array. argv[0]
is the program name, and everything after the first argument is used-supplied arguments. You may also qualify them as const
if you so choose to.
main()
Function with Command-Line Arguments and Environment Variables (UNIX)
This version of main()
is UNIX-Specific, which means it will not work on Windows. However, if you are developing an application for Linux or alike, you may also add a third argument (envp) to get environment variables without calling to getenv()
:
#include <stdio.h>
int main(int argc, char *argv[], char *envp[]) {
while (*envp != NULL) {
printf("%s\n", *envp);
envp++;
}
return 0;
}
At the end of envp
there will always be NULL
to mark the end, because envp
does not come with a static pre-defined length.
Data Types
C provides a vast array of data types, type modifiers, and extensions to manage memory effectively (the main concept in C) and perform operations on various types of data.
Primitive Data Types
Primitive data types are the basic building blocks provided by the C language by default, however they have the downside of not being static and can fluctuate in size.
Type | Size (bytes) | Range |
---|---|---|
int
|
2 or 4 | -32,768 to 32,767 (short), or larger |
float
|
4 | ~3.4E-38 to ~3.4E+38 |
double
|
8 | ~1.7E-308 to ~1.7E+308 |
long double
|
10 or 16 | ~3.4E-4932 to ~1.1E+4932 |
char
|
1 | -128 to 127 (signed), 0 to 255 (unsigned) |
void
|
- | Represents no value |
For example:
int signed_int = 10;
float floating_point = 3.14;
double simple_double = 2.71; /* storage is larger than `float` */
long double extended_perc_double = 3.234786328468326482;
char c = 'h';
void *generic_pointer = NULL;
auto
Type
The auto
pseudotype automatically resolves the type of a given variable based on its value during compile time, however, it is not a generic and cannot change besides what's assigned at compile-time.
auto x = 1234; // same as: int x = 1234;
Custom Types
Custom types allow us to define more meaningful and reusable abstractions for data. They are typically created using typedef
, struct
, and/or enum
, however unions are also very useful for this purpose.
typedef
typedef
allows us to define our own custom type aliases.
typedef unsigned int my_uint;
my_uint age = 18;
Note that not using typedef
means you will directly have to address the type (instead of MyAlias
you will have to use the aliased type, struct MyAlias
, or enum MyAlias
)
struct
Structures allow us to pack multiple types into a single structured object.
typedef struct {
long x, y;
} Point;
Point p = {1, 2};
enum
Enumerations allow us to have multiple preset values to a type.
typedef enum { RED, GREEN, BLUE, ORANGE, YELLOW } Colour;
Colour clr = Colour_BLUE;
union
Unions allow multiple members to share the same memory location and be reinterpreted in multiple set ways.
typedef union {
int b;
char c[64];
} MyUnion;
MyUnion h = {0};
h.c[0] = 'h';
printf("%d\n", h.b); // 104
printf("%s\n", h.c); // h
This works because the ASCII value for 'h' is 104, and since the other bytes are NULL we get an output of 104. The total memory allocated is 64 bytes (because unions only allocate the memory for the largest element), with a struct this would be 68 because field b would get its own memory space instead of sharing it with c.
Type Modifiers
Modifiers alter the properties of primitive types, such as size and sign.
Modifier | Applicable Types | Effect |
---|---|---|
short
|
int , char
|
Reduces storage size |
long
|
int , double
|
Increases storage size |
long long
|
int
|
Increases storage size more (64-bits) |
signed
|
int , char
|
Allows negative values (default unless unsigned is used)
|
unsigned
|
int , char
|
Restricts values to non-negative |
Modifiers must be used after a type, however, a type is not always required - usually defaults to int
. For example:
int a; /* 32 bit signed integer */
short int b; /* 16 bit signed integer */
unsigned int c; /* 32 bit unsigned integer (only positive values) */
You may also combine the modifiers:
unsigned short int d; /* 16-bit integer, only positive values */
Type Qualifiers
Qualifiers provide additional constraints or behaviours for variables.
Qualifier | Purpose |
---|---|
const
|
Prevents modification of variable |
volatile
|
Indicates variable can change unexpectedly (disables compiler optimization for the variable) |
restrict
|
Optimizes pointer access |
static
|
Makes a variable exist the entire program runtime. |
Static Types (stdint.h
)
The <stdint.h>
header defines fixed-width integer types and limits for portability across platforms.
Type | Size (bytes) | Range |
---|---|---|
int8_t
|
1 | -128 to 127 |
uint8_t
|
1 | 0 to 255 |
int16_t
|
2 | -32,768 to 32,767 |
uint16_t
|
2 | 0 to 65,535 |
int32_t
|
4 | -2,147,483,648 to 2,147,483,647 |
uint32_t
|
4 | 0 to 4,294,967,295 |
int64_t
|
8 | Very large range |
Type Extensions
Extensions provide advanced or platform-specific data types to make your C experience nicer.
Feature/Type | C Version | Compiler Support | Type Name | Details |
---|---|---|---|---|
128-bit integers | GCC extension | GCC (x86-64) | __int128 , unsigned __int128
|
Provides extended precision arithmetic; available on platforms with wide integer modes. |
Atomic types | C11 (or GCC/Clang extension) | GCC, Clang, MSVC | Defined in <stdatomic.h> or by using __sync /__atomic built-ins.
|
Enables atomic operations for multithreading. Includes types like atomic_int , atomic_flag .
|
Boolean type | C99 | All major compilers | _Bool or <stdbool.h> . You may also define your own using typedef enum { False = 0, True = 1} Bool;
|
Represents true/false values; standardized in C99. |
Exact-width integers | C99 | All major compilers | Types in <stdint.h>
|
Includes int8_t , int16_t , int32_t , etc., for precise control over integer sizes.
|
Variable-length arrays | C99 | GCC, Clang | N/A | Arrays with runtime-determined length; deprecated in C11 but still supported by some compilers. |
Wide characters | C99 | All major compilers | <wchar.h>
|
Provides support for wide characters and strings. |
Complex numbers | C99 | GCC, Clang | <complex.h>
|
Enables manipulation of complex numbers (float _Complex , double _Complex ).
|
Imaginary numbers | C11 | GCC, Clang | <complex.h>
|
Adds imaginary types (float _Imaginary , double _Imaginary ).
|
Aligned objects | C11 | GCC, Clang | <stdalign.h>
|
Allows querying and specifying object alignment. |
Thread management | C11 | GCC, Clang | <threads.h> (for platform-specific POSIX threading you can use <pthreads.h> )
|
Provides thread creation, mutexes, and condition variables. |
Type Casting
Type casting in C allows us to convert a variable from one data type to another. For example:
float num = 5.75;
int int_part_of_num = (int)num; /* explicit float to int */
char ch = 'A';
int ascii_value = (int)ch; /* explicit char to int */
int number = 42;
float float_value = (float)number; /* explicit int to float */
double large_num = 123.456;
int trunc_value = (int)(float)large_num; /* explicit double to float, then to int */
Pointers
A pointer is a variable that stores the memory address of another variable; instead of holding a value directly, it holds the location where the value resides in memory (if you imagine your memory as a large array - an index)
Pointers are a fundamental feature in C, allowing direct manipulation of memory addresses. They provide powerful, but unsafe capabilities for dynamic memory management, efficient array handling, and implementing complex data structures like linked lists and tr(e|i)es.
Syntax
Pointers are declared using the *
symbol. For example:
int *ptr; /* declares a pointer to an integer */
The &
operator retrieves the address of a variable, while the *
operator dereferences a pointer to access the value stored at its address as follows:
#include <stdio.h>
int main(void) {
int num = 10;
int *ptr = # /* Pointer stores the address of 'num' */
printf("Value of num: %d\n", num); /* Outputs 10 */
printf("Address of num: %p\n", ptr); /* Outputs address of 'num' */
printf("Value through pointer: %d\n", *ptr); /* Outputs 10 (dereferenced) */
return 0;
}
Pointer Arithmetic
Pointers allow navigation through contiguous memory locations, such as arrays. Arithmetic operations like increment (ptr++
) or addition (ptr + n
) adjust the pointer to point to subsequent memory locations, for example:
int arr[] = {10, 20, 30};
int *ptr = arr; /* Points to the first element */
printf("%d\n", *ptr); /* Outputs 10 */
printf("%d\n", *(ptr + 1)); /* Outputs 20 */
In C, an array name acts as a constant pointer to its first element because arrays and pointers closely related:
int a[] = {10, 20, 30};
int *p = a;
/* Converting to `char *` to treat the array as a contiguous array of bytes */
printf("First element: %d\n", *(p));
printf("Second element: %d\n", *((char *)p + sizeof(int)));
printf("Third element: %d\n", *((char *)p + 2 * sizeof(int)));
Void Pointers
A void pointer (void *
) can hold the address of any data (poor man's generic) type but must be typecast before dereferencing.
void *v_ptr;
int num = 42;
v_ptr = #
printf("Value: %d\n", *(int *)v_ptr); /* Typecast to int* */
Dynamic Memory Allocation with Pointers
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *arr =
(int *)malloc(5 * sizeof(int)); /* Allocates memory for 5 integers */
for (int idx = 0; idx < 5; ++idx) {
arr[idx] = idx + 1;
}
for (int idx = 0; idx < 5; ++idx) {
printf("%d ", arr[idx]); /* Outputs: 1 2 3 4 5 */
}
free(arr); /* Frees allocated memory */
return 0;
}
Pointer Types
Pointer Type | Description | Features |
---|---|---|
char *
|
Pointer to a character or a string (array of characters). | Points to a NULL-terminated string (\0 indicates end) or a single character.
|
int *
|
Pointer to an integer variable. | Stores the address of an integer; can be dereferenced to access or modify the integer value. |
float *
|
Pointer to a floating-point variable. | Stores the address of a float; allows direct manipulation of floating-point values (IEEE 754) |
double *
|
Pointer to a double precision floating-point variable. | Similar to float * , but for double precision values.
|
void *
|
Generic pointer that can point to any data type. | Cannot be directly dereferenced; must be cast to another pointer type before use. |
NULL Pointer
|
A pointer initialized to NULL .
|
Indicates it points to nothing; useful for error handling and initialization. |
Wild Pointer
|
Uninitialized pointer that points to an arbitrary memory location. | Dangerous as it may lead to undefined behaviour. |
Dangling Pointer
|
A pointer that references memory that has been freed or deallocated. | Accessing it causes undefined behaviour; avoid by setting it to NULL after freeing memory.
|
Pointer to Pointer
|
A pointer that stores the address of another pointer. | Useful for dynamic memory allocation and multi-level indirection. |
Pointers are also affected by type modifiers and qualifiers, such as const
, volatile
, and restrict
, which define how pointers interact with memory and optimise compiler behaviour. Note that nested pointers (pointer to pointer) may need their own qualifiers as well.
Memory Management
Memory management in C involves manually allocating, using, and freeing memory during program execution - you manage your memory. You may allocate the memory statically on the stack at compile-time, or allocate it dynamically by using dynamic memory allocators.
Dynamic Memory Allocation Functions
Function | Description |
---|---|
malloc(size_t size)
|
Allocates a block of memory of the specified size and returns a pointer to it. |
calloc(int num, int size)
|
Allocates memory for an array of elements, initializes them to zero, and returns a pointer. |
realloc(void *ptr, size_t newsize)
|
Resizes previously allocated memory to a new size. |
free(void *ptr)
|
Frees previously allocated memory, making it available for reuse. |
Risks and Best Practices
Managing your memory can be dangerous, for instance you could run into one of the following common pitfalls of managing your own memory:
- Memory leaks (when memory is allocated but not freed)
- Segmentation faults (when accessing freed or invalid memory)
- Buffer overflows (when accessing memory outside of an allocated buffer)
To mitigate these risks follow best practices, such as:
- Always using
free()
- Always ensure correct lifetime management.
- Use tooling, maximum compiler checks, and linters to avoid security vulnerabilities.
Tooling
Category | Tools |
---|---|
Compilers | GCC, Clang, MSVC, TCC |
Strippers | GNU Strip, LLVM-Strip |
Linters | Clang-Tidy, Splint |
Formatters | Clang-Format, AStyle, GNU Indent |
Debugging | GDB, Valgrind (Callgrind, other tools in Valgrind's toolset), AddressSanitizer |
LSPs | Clangd, CCLS |
Build Tools | Make, CMake, Ninja, Meson |
Compiler and Stripping Flags
Here's some flags to help your compiler optimise your code:
Compiler Flags
Category | Flag | Description | Applicable Compilers |
---|---|---|---|
Security | -fstack-protector-strong
|
Protects against stack smashing attacks | GCC, Clang |
-D_FORTIFY_SOURCE=2
|
Enables buffer overflow checks | GCC, Clang | |
-mcet-switch -fcf-protection
|
Control flow integrity protection | GCC (x86) | |
-ffunction-sections
|
Enables fine-grained address-space layout randomization | GCC, Clang | |
-Werror=format-security
|
Reject unsafe format strings | GCC, Clang | |
-Wl,-z,relro
|
Read-only segments after relocation | GCC, Clang | |
-Wl,-z,now
|
Disables lazy binding | GCC, Clang | |
-fwrapv
|
Assumes signed integer overflow wraps around | GCC, Clang | |
Warnings | -Wall
|
Enables most common warnings | GCC, Clang |
-Wextra
|
Enables additional warnings | GCC, Clang | |
-Wpedantic
|
Enforces strict ISO compliance | GCC, Clang | |
-pedantic-errors
|
Converts pedantic warnings to errors | GCC, Clang | |
-Weverything
|
Enables all warnings (Clang only) | Clang | |
-Wimplicit-fallthrough
|
Warns about implicit fall-through in switch statements | GCC, Clang | |
Optimization | -O2
|
Standard optimization level | GCC, Clang |
-O3
|
Aggressive optimizations | GCC, Clang | |
-flto=full
|
Enables full link-time optimization | GCC, Clang | |
-march=native -mtune=native
|
Optimizes for the host machine architecture | GCC, Clang | |
-funroll-loops
|
Unrolls loops for performance | GCC, Clang |
Striping Flags
Flag | Description |
---|---|
--strip-debug
|
Removes debugging information |
--strip-sections
|
Removes unused sections |
--strip-unneeded
|
Removes unneeded symbols |
--remove-section=.note.gnu.gold-version
|
Removes gold linker version .note.gnu.gold-version section
|
--remove-section=.note.gnu.build-id
|
Removes build ID .note.gnu.build-id section
|
--discard-locals
|
Discards local symbols |
--strip-symbol=__gmon_start__
|
Strips the debugging section |
Extreme Optimization
For software if you want to extremely optimize the software you may want to use the following flags:
export STRIP=llvm-strip
export STRIPFLAGS='--strip-debug --strip-sections --strip-unneeded -T --remove-section=.note.gnu.gold-version --remove-section=.note --strip-all --discard-locals --remove-section=.gnu.version --remove-section=.eh_frame --remove-section=.note.gnu.build-id --remove-section=.note.ABI-tag --strip-symbol=__gmon_start__ --strip-all-gnu --remove-section=.comment --remove-section=.eh_frame_ptr --discard-all'
export CC=clang
export CFLAGS='-Wpedantic -flto=full -fno-trapping-math -fstrict-aliasing -fno-math-errno -fno-stack-check -fno-strict-overflow -funroll-loops -fno-stack-protector -fvisibility-inlines-hidden -mfancy-math-387 -fomit-frame-pointer -fstrict-overflow -Wshadow -fno-exceptions -D_FORTIFY_SOURCE=0 -Wall -Wextra -fno-signed-zeros -fno-strict-aliasing -pedantic -O3 -fvisibility=hidden -ffast-math -funsafe-math-optimizations -std=c99 -fno-asynchronous-unwind-tables -Werror -fdiscard-value-names -femit-all-decls -fmerge-all-constants -fno-use-cxa-atexit -fno-use-init-array -march=native -mtune=native -pedantic-errors'
For libraries you will need to adjust them, for instance, making the symbol visibility public. However note that this disables security features so please understand the flags before using them.
C Versions (Standards)
C Version | Release Year | Key Features | Differences and Quirks |
---|---|---|---|
C89/C90 | 1989/1990 | Function prototypes, improved type checking, standard library (e.g., stdio.h , stdlib.h ), and memory management.
|
First standardized version; lacks inline comments (// ), variable-length arrays, and complex numbers. Highly portable but limited modern features.
|
C99 | 1999 | inline functions, long long int , complex numbers, variable-length arrays, flexible array members, one-line comments (// ), and improved floating-point support.
|
Introduced many modern features but required compiler updates; some compilers (e.g., MSVC) were slow to adopt full C99 support. |
C11 | 2011 | Multi-threading (threads.h ), atomic operations, generic selection (_Generic ), improved Unicode support (char16_t , char32_t ), bounds-checked functions, and new keywords like _Static_assert .
|
Focused on concurrency and safety; introduced optional features like threads that may not be supported by all compilers. Backward-compatible with C99. |
C17 | 2018 | Technical corrections and clarifications to defects in C11; no new language features. | Considered a "maintenance release"; primarily aimed at compiler implementers rather than developers. |
C23 | 2024 | New keywords (constexpr , nullptr , auto ), binary literals (0b ), tagged enums with underlying types, UTF-8 character literals (u8"" ), decimal floating-point types, empty initializers ({} ), and attributes like [[deprecated]] . Library additions include floating-point formatting functions and memset_explicit .
|
Adds significant modern features for usability and compatibility with other languages like C++. Improves readability but increases complexity of the standard. |
Most C programmers code in C89 to C11 standards, however C17 and C23 are gaining popularity with time.
Differences Between C and C++
Aspect | C | C++ |
---|---|---|
Programming Paradigm | Procedural programming, focuses on functions | Object-oriented programming, focuses on objects and data |
Classes and Objects | Does not support classes and objects | Supports classes and objects |
Inheritance & Polymorphism | Not supported | Supported |
Encapsulation | Minimal encapsulation only | Supported |
Memory Management | Uses malloc() and free() for dynamic memory allocation
|
Uses new and delete operators
|
Standard I/O Operations | Uses printf() and scanf() or platform-dependent APIs
|
Uses cout and cin
|
Function Overloading | Not supported | Supported |
Default Arguments | Not supported | Supported |
Inline Functions | Supported (in newer versions) | Supported |
Namespaces | Requires manual prefixing | Supported |
Exception Handling | Requires manual handling | Supports try-catch blocks
|
Size | Small | Large |
Compilation TImes | Fast | Slow |
Complexity | Simple | Complex |
Cross-Compatibility | Most C++ code won't work in C. | Most C code will work in C++. C++ is a superset of C. |
Best Practices
- Adhere to your C standard of choice avoiding undefined behaviour.
- Use meaningful names for your variables, functions, structures, and enumerations.
- Avoid code duplication.
- Avoid using global variables unless needed. Prefer to use the
static
keyword if needed. - Avoid deep nesting and use short-circuit conditions where possible.
- Use a consistent coding style.
- If you can easily implement it yourself, avoid dependencies.
Code Style
For styling one may prefer to use automated tools, such as clang-format
with their own .clang-format
configuration, such as:
# .clang-format example file
---
BasedOnStyle: LLVM
IndentWidth: 4
SortIncludes: false
AlignConsecutiveAssignments: true
AlignConsecutiveBitFields: true
AlignConsecutiveMacros: true
AlignEscapedNewlines: true
AllowShortCaseLabelsOnASingleLine: true
AllowShortEnumsOnASingleLine: true
AllowShortFunctionsOnASingleLine: true
AllowShortLambdasOnASingleLine: true
BinPackParameters: false
IndentCaseBlocks: true
IndentCaseLabels: true
IndentExternBlock: true
IndentGotoLabels: true
---
Similarly, .editorconfig
:
# .editorconfig example file
root = true
[*]
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
charset = utf-8
trim_trailing_whitespace = true
tab_width = 2
However, it is also important to know common standards. For example:
- Macros should be in UPPER_SNAKE_CASE.
- Structure and type names should be in PascalCase.
- Variable and function names should be in snake_case.
- Enumeration elements should have the enumeration name prepended to them.
- Functions that work on some structure should have the structure name prepended to them.
- Use header guards and avoid implementation in headers.
- Have consistent naming, for instance, if you use *_destroy() everywhere do not name something *_destruct()
- ...
Albeit meant for Python, PEP8 is a great resource for understanding good code style.
Uses
C is used in:
- Operating system kernels like Linux because it provides low-level access to hardware and memory.
- Embedded systems, microcontrollers, and IoT devices because it allows direct control of hardware components.
- Databases because its speed supports complex data processing tasks efficiently.
- Game development for performance-intensive applications because of its ability to handle real-time computations and graphics rendering effectively.
- Graphical user interfaces because it enables efficient handling of graphical data and user interfaces.
- Device drivers such as network, mouse, and keyboard drivers because it allows precise control over hardware interactions.
- Web servers because its speed ensures high performance for handling large volumes of traffic.
- Used in compilers and interpreters for languages like Python and PHP because its portability ensures compatibility across platforms.
- ... Everywhere! It's a general-purpose language :) And, in fact, one of the most popular languages out there.
Examples
"Hello, World!" Program
#include <stdio.h>
int main(void) {
puts("Hello, World!");
/* Or: printf("Hello, World!\n"); */
return 0;
}
Prime Number Checker
#include <stdio.h>
#include <stdbool.h>
int main(void) {
unsigned num, i;
bool is_prime = true;
printf("Enter a positive integer: ");
scanf("%u", &num);
if (num <= 1) {
is_prime = false;
} else {
for (i = 2; i <= num / 2; i++) {
if (num % i == 0) {
is_prime = false;
break;
}
}
}
if (is_prime)
printf("%u is a prime number.\n", num);
else
printf("%u is not a prime number.\n", num);
return 0;
}
Fibonacci Series
#include <stdio.h>
int main(void) {
unsigned n, t1 = 0, t2 = 1, next_term;
printf("Enter the number of terms: ");
scanf("%u", &n);
printf("Fibonacci Series: %u, %u", t1, t2);
for (int i = 3; i <= n; ++i) {
next_term = t1 + t2;
printf(", %u", next_term);
t1 = t2;
t2 = next_term;
}
putchar('\n');
return 0;
}
FizzBuzz
#include <stdio.h>
static void fizz_buzz(int n) {
for (int i = 1; i <= n; ++i) {
// or just i % 15 == 0
if (i % 3 == 0 && i % 5 == 0) {
puts("FizzBuzz");
} else if (i % 3 == 0) {
puts("Fizz");
} else if (i % 5 == 0) {
puts("Buzz");
} else {
printf("%d\n", i);
}
}
}
int main(void) {
const int n = 128;
fizz_buzz(n);
return 0;
}
Notes
- Pointers are not automatically allocated in C; you must allocate memory for them using
malloc()
before accessing the variable or its members. - To access the value stored at a pointer's address, use the dereference operator
*
- it is essential for working with pointers of primitive types. - C does not support methods. Instead, pseudo-methods are commonly implemented using functions with pointers as arguments. Use the reference operator
&
to access the memory address of stack-allocated objects.