C

From Computernewb Wiki
Jump to navigation Jump to search

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:

  1. Include necessary libraries like stdio.h for input/output operations.
  2. 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 = &num; /* 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 = &num;

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:

  1. Operating system kernels like Linux because it provides low-level access to hardware and memory.
  2. Embedded systems, microcontrollers, and IoT devices because it allows direct control of hardware components.
  3. Databases because its speed supports complex data processing tasks efficiently.
  4. Game development for performance-intensive applications because of its ability to handle real-time computations and graphics rendering effectively.
  5. Graphical user interfaces because it enables efficient handling of graphical data and user interfaces.
  6. Device drivers such as network, mouse, and keyboard drivers because it allows precise control over hardware interactions.
  7. Web servers because its speed ensures high performance for handling large volumes of traffic.
  8. Used in compilers and interpreters for languages like Python and PHP because its portability ensures compatibility across platforms.
  9. ... 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.