C (programming language)
C is a general-purpose programming language developed by Dennis Ritchie at Bell Labs in the early 1970s. It is based on the concise syntax and expression-orient...
| Classification | General-purpose programming language, System programming language |
|---|---|
| Paradigms | Imperative programming, Procedural programming, Structured programming |
| Designer | Dennis Ritchie |
| Developer | Bell Labs |
| First released | 1972 |
| Preceded by | BCPL, B (programming language) |
| Type system | Static typing, weak type checking, explicit type conversion |
| Memory management | Manual memory management |
| Major standard | ISO/IEC 9899:2024 (C23) |
| File extensions | .c, .h |
C is a general-purpose programming language developed by Dennis Ritchie at Bell Labs in the early 1970s. It is based on the concise syntax and expression-oriented design inherited from BCPL and B (programming language), and was designed as a high-level language that could manipulate memory and data representations close to machine code without being directly tied to the instruction set of any particular hardware platform. C provides static typing, functions, structures, pointers, arrays, and explicit control flow, and combines a small language core with a standard library to support the implementation of software ranging from system software to general-purpose applications. According to Dennis Ritchie’s account, the evolution from B to C took place between 1971 and 1973, while the Computer History Museum records 1972 as the year in which C was released.
C emerged during the development and porting of UNIX. Early versions of UNIX were written in assembly language, but substantial portions were later reimplemented in C, reducing the need to rewrite all hardware-specific assembly code whenever the operating system was moved to a new computer. As C and UNIX spread together to universities and research institutions, C became established as a leading system programming language for writing operating systems, compilers, device drivers, and network software.
C programs are generally translated into native machine code for a target processor through a compiler, but the C language itself does not require any particular compilation method, operating system, or executable file format. The international standard defines the representation, syntax, constraints, semantic rules, input and output data, and standard library behavior of C programs, providing a common basis for porting programs across different data-processing systems. The current international standard is ISO/IEC 9899:2024, commonly known as C23, and C standardization is managed by WG14 of ISO/IEC JTC 1/SC 22.
C provides pointers that represent hardware addresses, bitwise operations, user-defined memory layouts, and calling interfaces that can be connected easily to operating systems and external libraries. Because the language does not provide automatic garbage collection or object lifetime management by default, resources such as dynamically allocated memory, files, and devices must be managed directly by the program or through separate libraries and design conventions. This structure enables fine-grained control over execution cost and memory usage, but it can also produce errors that make program behavior unpredictable, such as out-of-bounds array access, use of freed memory, and invalid pointer arithmetic.
C abstractions are generally designed to correspond efficiently to the data representations of real computers. The exact sizes of integer and floating-point types, alignment requirements, and some operational characteristics may vary depending on the target implementation, and the language standard does not assume that all hardware follows a single fixed machine architecture. This allows the same C source code to be ported across a wide range of processors and operating systems, but programs that depend on implementation-defined or undefined behavior may execute differently depending on the environment.
C is used in fields where performance and memory control are important, including operating system kernels and system libraries, compilers, database engines, embedded firmware, networking equipment, game engines, and scientific computing libraries. It has also influenced the syntax, type systems, execution models, and foreign function interfaces of many languages, including C++, Objective-C, Java, C#, JavaScript, Go (programming language), and Rust. The C code, ABIs, operating system APIs, and hardware toolchains accumulated over several decades continue to provide the foundation for C’s role as a common language connecting new systems with existing software.
History
The history of C is not the story of designing a single complete language from the beginning, but rather an evolutionary history formed by successively modifying BCPL, B (programming language), and NB within the operating-system research and constrained hardware environment of Bell Labs in the late 1960s. Dennis Ritchie, who designed C, recorded that C was created together with early UNIX between 1969 and 1973, with the most creative changes concentrated in 1972.[1]
The types, pointers, arrays, structures, declaration syntax, and preprocessor now recognized as the core of C were not completed all at once according to a single design document. C developed incrementally by solving problems discovered while implementing actual operating systems, compilers, and file-processing tools. Its type system and portability were later strengthened while UNIX was ported to other computers, and through the publication of 《The C Programming Language》 and ANSI and ISO standardization, it evolved from the system implementation language of a particular research laboratory into an internationally standardized programming language.
BCPL and the Linguistic Lineage
The direct linguistic lineage of C begins with BCPL. BCPL was designed by Martin Richards for writing compilers and system software, and its initial reference manual was written in 1967. Work related to BCPL implementations and research was also conducted at Bell Labs around 1969.[2]
BCPL was an untyped language that effectively used a single machine word as its basic unit of data. Memory was regarded as a continuous space of equally sized cells, and a single value could be interpreted as an integer, an address, or another kind of value depending on the operation. An array was also represented not as an independent high-level object, but as a combination of the location of its first memory cell and index calculation.
In BCPL and later B, array access conceptually had the following relationship.
V[i]
*(V + i)
This relationship continued into C, but in C, pointer arithmetic evolved so that the unit of movement is determined by the size of the data type to which the pointer points.
BCPL supported functions, global declarations, conditional and loop statements, indirect references corresponding to pointers, and separate compilation. C’s function-centered structure, many of its operators, and its control statements were inherited from BCPL and B, but C as a whole was not a direct copy of BCPL syntax. C’s data types, structures, declaration system, and preprocessor developed separately afterward.
Withdrawal from Multics and the Beginning of UNIX
In the late 1960s, Bell Labs participated in the Multics project, which was being developed jointly by MIT and General Electric. Multics was a large-scale operating system intended to provide time-sharing, multi-user operation, a hierarchical file system, dynamic linking, and a strong protection system.
However, as the project became increasingly complex and delayed, Bell Labs withdrew from Multics around 1969. Ken Thompson, Dennis Ritchie, and others began creating a smaller and more directly usable computing environment based on their experience with Multics.[3]
Early UNIX was developed on the DEC PDP-7. At the time, no suitable high-level language or development environment was available for that computer, so the core of the operating system was written in assembly language. The researchers needed a system programming language that could be implemented even on a small computer and was easier to use than assembly language.
The major concepts of early UNIX included the following.
- Processes
- A hierarchical file system
- A command interpreter running in user space
- Simple text files
- A common interface for files and devices
- Small, composable command-line tools
C developed from the need to implement programs and the operating system itself in this UNIX environment.
Development of B
Ken Thompson developed B (programming language), a simplified form of BCPL, between 1969 and 1970. B retained BCPL’s general computational model and function-centered structure while being reduced enough for a compiler and programs to run in the very small PDP-7 environment.
B was also an untyped language centered on a single machine word. It did not strictly distinguish integers from addresses at the language level, and an array variable stored a value representing the beginning of a contiguous region of memory.
A B function was written in a form similar to the following.
max(a, b) {
if (a > b)
return a;
return b;
}
This code is a simplified example intended to demonstrate the syntactic form of B. B already had several syntactic elements that later continued into C, including brace-delimited blocks, function calls, conditional statements, and return.
B introduced ++ and --, which later became characteristic C operators. A commonly repeated explanation states that these operators were created because of the PDP-11’s autoincrement addressing mode, but the PDP-11 was not yet in use when B was developed. Ritchie explained that the direct motivation was to express increment and decrement with compact syntax and to generate efficient code.[4]
The order of compound assignment operators in early B and C was the reverse of their current form.
value =+ 1;
value =- 1;
value =* 2;
In modern C, these are written as follows.
value += 1;
value -= 1;
value *= 2;
Instead of directly generating native machine code, the PDP-7 B compiler generated threaded code consisting of lists of addresses of small code fragments and executed it through an interpreter. This reduced the size of the compiler, but execution was slow, and the approach was insufficient for rewriting all of UNIX in B within the limited memory of the PDP-7.
The PDP-11 and the Limitations of B
As the UNIX project demonstrated its potential, the Bell Labs researchers obtained a DEC PDP-11 in 1970. Early UNIX for the PDP-11 was also implemented in assembly language, but the new hardware clearly exposed the limitations of B’s single-word data type.
The PDP-11 used 16-bit words and byte-addressed memory. The word-centered structure of BCPL and B treated integers, addresses, and character data as a single word, but the PDP-11 required the following elements to be distinguished.
- 8-bit characters
- 16-bit integers
- Byte-addressed memory addresses
- Pointers and arrays
- Floating-point values
In B, manipulating characters required extracting and recombining particular bytes within a word. Pointers also required additional calculations to convert word indexes into actual byte addresses. Representing every value with a single word could not use the PDP-11 hardware naturally and efficiently.
Beginning in 1971, Dennis Ritchie added data types such as char and int to B and began writing a new compiler that generated PDP-11 machine code directly instead of threaded code.
From NB to C
The transitional language between B and C was called NB, or New B. No NB source code has been found, and its characteristics are inferred from Dennis Ritchie’s recollections and surviving compiler materials.[5]
NB provided char and int, along with arrays and pointers for each type. Pointer arithmetic was adjusted according to the size of the pointed-to type.
char *characters;
int *numbers;
characters += 1;
numbers += 1;
Both operations add 1 to a pointer, but the actual address displacement differs according to the sizes of char and int.
NB arrays were still implemented in a manner similar to B. An array variable stored the address of separately allocated storage. This worked for simple arrays, but created problems when attempting to include an array within a structure.
For a structure to represent the actual layout of a file, device, or data in memory, the array elements had to be stored contiguously inside the structure itself. If each array contained a separate hidden pointer, the structure’s actual memory layout did not match the intended data.
While solving this problem, Ritchie created the key rule that established the relationship between arrays and pointers in C. An array became an actual object containing contiguous elements, while an array name was converted in most expressions into a pointer value referring to its first element.
int values[4];
int *first = values;
int *same = &values[0];
This rule made it possible to include arrays directly within structures while preserving much of the array-access behavior of existing B code.
Formation of Early C
Once the type system and new compiler had taken a stable form, Ritchie abandoned the name NB and began calling the language C. Ritchie did not settle on a single explanation of whether the name C was chosen simply because it followed B in the alphabet or because it represented the next stage after BCPL.
The years 1972 and 1973 were the critical period in the transition from untyped B to typed C. The last1120c and prestruct-c materials preserved by Dennis Ritchie are early C compiler sources written during this period. Their exact dates are difficult to determine because of timestamp information on the original tapes and subsequent copying, but it is clear that both compilers date from between 1972 and 1973.[6]
The last1120c compiler, which retains a 1972 copyright notice, contains function definitions of the following form.
main(argc, argv)
int argv[];
{
/* ... */
}
This code is a short excerpt preserving the actual form used at the time. Unlike modern C, main has no return type, the parameter names are listed first, and their types are declared separately on the following line. argv is also declared as int argv[] rather than the modern char **argv.[7]
The same compiler actually used the compound assignment operators of the period.
i =+ *sp++;
i =% hshsiz;
i =* pssiz;
Early compilers did not support the general compound declaration syntax common today. Pointers were declared using an array-like notation.
int pointer[];
A declaration system capable of combining pointer, array, and function types was developed later.
int *pointer;
int **pointer_to_pointer;
int (*callback)(int);
last1120c had no structures, and the string struct does not appear in its source. The later prestruct-c began to support structures and used the . and -> operators. However, early structure declarations used parentheses rather than braces, unlike modern C.[8]
There was no C preprocessor at the time. Token categories, operator properties, and similar values were written directly throughout the compiler source as numeric constants rather than named macros.
The early compiler used two passes distributed across several files. The first pass parsed the source and created a syntax tree, while the second pass read the tree and generated PDP-11 assembly code. Compared with modern compilers, it was extremely small and primitive, but it demonstrated an early form of a self-hosting system in which a compiler written in C translated C programs.
Development of Operators and the Declaration System
In early B and C, & and | were used both for bitwise operations and for logical operations in conditional expressions. && and || were added to distinguish logical operations from bitwise operations.
if (pointer != 0 && *pointer != 0) {
process(*pointer);
}
&& provides short-circuit evaluation by not evaluating the right-hand condition when the left-hand condition is false, while || does not evaluate the right-hand condition when the left-hand condition is true.
C declaration syntax developed to reflect the way a declared name appears when used in an expression.
int value;
int *pointer;
int values[10];
int function(int value);
int (*callback)(int value);
The declarations can be read so that *pointer denotes an int value, values[index] denotes an int value, and the result of function(value) is an int.
The declaration system for combining arrays, pointers, and functions across multiple levels did not exist in the earliest compilers, but had developed into a form close to the modern system by around 1975.
Emergence of the C Preprocessor
The C preprocessor began to be introduced between 1972 and 1973. The early preprocessor provided file inclusion and simple parameterless macro substitution.
#include "header.h"
#define BUFFER_SIZE 512
Function-like macros and conditional compilation were added later.
#define MAX(left, right) \
((left) > (right) ? (left) : (right))
#ifdef PDP11
/* PDP-11-specific code */
#endif
The preprocessor was implemented as a separate translation stage that transformed tokens before ordinary C parsing. Because of this history, preprocessing directives have syntax and processing rules distinct from ordinary C statements.
The Early hello, world
Brian Kernighan’s early C tutorial, 《Programming in C — A Tutorial》, written at Bell Labs, included the following program.
main() {
printf("hello, world");
}
This is an early form of the now widely used Hello, world! example. It has no return type for main, no declaration for printf, no header file, and no explicit return statement.[9]
Written in modern C using K&R brace style, the same program takes the following form.
#include <stdio.h>
int main(void) {
printf("hello, world\n");
return 0;
}
Early C could implicitly assume that an undeclared function returned an integer. Therefore, code that called printf without first including a function declaration was permitted. Such implicit function declarations caused type mismatch and portability problems and were later removed in C99.
Rewriting UNIX in C
By early 1973, most of the core elements of modern C were in place. The size and performance of the code generated by the compiler had also reached a level at which system programs written in assembly language could be replaced.
Ken Thompson attempted to port the UNIX kernel to early C in 1972, before structures were complete, but abandoned the work because the language was not yet mature enough. After structures and the type system developed further, the UNIX kernel was rewritten in C during the summer of 1973.[10]
Kernel source from around UNIX Fourth Edition contains forms such as the following.
proc[0].p_stat = SRUN;
proc[0].p_flag =| SLOAD|SSYS;
u.u_procp = &proc[0];
Structure member access, arrays, and address operations are similar to modern C, but compound assignment was still written as =| rather than |=.
Kernel functions of the period used old-style function definitions such as the following.
main()
{
register i1, *p;
/* ... */
}
Because this is historical source code, its original form is preserved. The return type and the type of i1 are omitted, and omitted types could be treated as int.
Device drivers of the period represented device memory addresses and control bits as integer constants.
#define LPADDR 0177514
#define IENABLE 0100
Through arrays, pointers, structures, and bitwise operations, C could represent operating-system process tables, file systems, and device registers. At the same time, because it was less dependent on specific hardware than assembly language, it provided a foundation for moving UNIX to other computers.
C and UNIX were not the first example of implementing an operating system in a high-level language, but UNIX written in C demonstrated that a small language could provide native performance, hardware control, and portability together.
Early C Reference Manuals
Around 1974 and 1975, reference manuals and tutorials describing the C implementation were organized. Dennis Ritchie’s 《C Reference Manual》 described the lexical elements, types, expressions, declarations, and program structure of C for the PDP-11.
At the time, C had the following basic types.
char character;
int integer;
float single_precision;
double double_precision;
Arrays, pointers, functions, and structures could be constructed from the basic types.
int values[10];
int *pointer;
int function();
struct point {
int x;
int y;
};
Function definitions used a form in which parameter names and types were separated.
int add(a, b)
int a, b;
{
return a + b;
}
This form later became known as an old-style function definition or K&R function definition syntax. It is a separate concept from the K&R placement of braces as a code style.
Using modern function declaration syntax and K&R code style, the same function is written as follows.
int add(int a, int b) {
return a + b;
}
The relationship between arrays and pointers also became established as an important language rule in manuals of the period.
array[index]
*(array + index)
Function arguments were passed by value, and a pointer had to be passed to modify an object belonging to the caller.
void swap(int *left, int *right) {
int temporary = *left;
*left = *right;
*right = temporary;
}
Portability and the Portable C Compiler
The portability of C and UNIX was not a fully developed primary objective from the moment the language was created. Early work focused on building a small system that the researchers could actually use, and the advantage of portability became increasingly important through later practical porting efforts.
Stephen C. Johnson developed the Portable C Compiler so that it could be adapted relatively easily to new processors. Johnson, Thompson, and Ritchie ported UNIX to the Interdata 8/32, and it was subsequently expanded to architectures including the VAX.[11]
The Interdata port demonstrated that C code and UNIX were not completely tied to the PDP-11. However, the process also exposed problems caused by remnants of the earlier untyped languages that remained in early C.
Early programs could use loose conversions between integers and pointers.
int address;
address = pointer;
pointer = address;
Such code did not operate correctly on systems where integers and pointers had different sizes. C subsequently developed toward a clearer distinction between pointer types and the explicit expression of required conversions.
Modern C can use appropriate integer types provided by the platform.
#include <stdint.h>
uintptr_t address = (uintptr_t)pointer;
pointer = (void *)address;
The unsigned type and explicit type casts were added, while type rules for structure pointers and function calls were also strengthened.
Development of lint
Early C compilers could not sufficiently check function calls and type mismatches across multiple translation units. Stephen C. Johnson developed lint to supplement these limitations.
Early function declarations did not include parameter types.
int calculate();
In this state, it was difficult to determine solely from the call site whether the following call matched the actual function definition.
double result = calculate(1.5);
lint analyzed multiple source files and diagnosed function argument and return-type mismatches, suspicious integer-to-pointer conversions, unused values, and similar issues. These problems became an important background for the introduction of function prototypes in ANSI C.
The Standard Input and Output Library
A portable input and output package written by Mike Lesk around 1973 became the foundation of the later C standard input and output library. Early UNIX programs directly used low-level operating-system file descriptors and system calls, but the portable input and output package provided a higher-level interface for handling characters and files in a similar way across different systems.
Modern C standard input and output is used in forms such as the following.
#include <stdio.h>
int copy_stream(FILE *input, FILE *output) {
int character;
while ((character = fgetc(input)) != EOF) {
if (fputc(character, output) == EOF) {
return -1;
}
}
return ferror(input) ? -1 : 0;
}
The separation of the language itself from its libraries provided the basis for maintaining a small language core while offering common functionality such as input and output, strings, and memory allocation in a portable form.
K&R C
In 1978, Brian Kernighan and Dennis Ritchie published 《The C Programming Language》. The book became known as K&R, after the surnames of its authors, and was also called the white book because of its white cover.
K&R was not the first C document, but it presented the core of the language and library in a concise and consistent form to a wide audience. Until an official standard was produced, it served for approximately ten years as the de facto definition of the C language.[12]
A typical program from the period of the first edition of K&R had the following form.
#include <stdio.h>
main()
{
printf("hello, world\n");
}
Because this code demonstrates historical syntax, its original form is preserved. The return type of main is omitted, and an empty parameter list did not explicitly declare that the function accepted no arguments, but instead meant that no parameter information had been specified.
A function with parameters used the following old-style definition syntax.
int maximum(a, b)
int a, b;
{
return a > b ? a : b;
}
A function declaration could be written as follows, but did not include parameter types.
int maximum();
C continued to change after the first edition of K&R. Between the late 1970s and early 1980s, the following features spread through the language and its implementations.
unsignedlongunionenumvoid- Structure assignment
- Passing and returning structures
- Stricter pointer type checking
- Compound assignment operators in their current form
Modern compound assignment operators have the following form.
value += offset;
flags |= ENABLED;
count *= 2;
The older =+, =|, and =* forms disappeared.
Expansion into Personal Computing and Industry
During the 1980s, C compilers became available for major computer architectures and operating systems. Many early compilers used the design and code of the Portable C Compiler, but independent commercial compilers and development environments gradually appeared.
C spread beyond UNIX workstations into the following environments.
- Personal computers
- MS-DOS applications
- Commercial operating systems
- Embedded controllers
- Networking equipment
- Databases
- Graphics software
- Games and system utilities
Implementing a C compiler for a new computer made it possible to port operating systems, development tools, libraries, and applications together. C was also used as a portable low-level target language for representing the compilation output of other languages.
During this period, C++ and Objective-C appeared as direct extensions of C. Java, C#, JavaScript, and later languages were also strongly influenced by C’s brace-delimited blocks, expressions, operators, and control-statement syntax.
The Need for Standardization
As C spread across companies, hardware platforms, and operating systems, incompatible dialects also increased. The first edition of K&R was widely used, but it was not an official standard that completely specified every detail of the language.
Compiler developers could handle the following elements differently.
- Sizes and conversions of integer types
- Passing and returning structures
- Function declaration and call checking
- Preprocessor behavior
- The standard library
- Pointer conversions
- Extension keywords
- Platform-specific calling conventions
Problems increased in which the same C source code was interpreted differently by different compilers or a program written in one environment could not be compiled in another. Commercial contracts and government procurement also required a formal specification for judging the conformance of C implementations.
ANSI formed the X3J11 technical committee in 1983 to begin developing a United States national standard for C. The committee’s objective was not to create an entirely new language, but to define the common practices of already widely used C clearly and consistently.[13]
The following principles were important during the standardization process.
- Preserve the value of existing C programs.
- Improve program portability.
- Do not adopt one particular compiler as the standard.
- Retain the ability to write hardware-specific system code.
- Do not obstruct efficient implementation.
- Prioritize features with practical implementation experience.
ANSI C and C89
X3J11 published ANSI X3.159-1989 in 1989. This edition is called C89 or ANSI C. ISO adopted it in 1990 as ISO/IEC 9899:1990, and it is also called C90 according to the publication year of the international standard.[14]
One of the important changes in C89 was the function prototype. A K&R-style function declaration did not indicate the number or types of parameters.
int calculate();
ANSI C made it possible to include parameter types in a declaration.
int calculate(int left, int right);
Function definitions could also use modern syntax with K&R brace style.
int calculate(int left, int right) {
return left + right;
}
The compiler could now check the number and types of arguments passed in a call.
int result = calculate(10, 20);
C89 standardized void, void *, const, and volatile.
#include <stddef.h>
static int sum(const int *values, size_t count);
int main(void) {
int values[] = { 1, 2, 3 };
return sum(values, 3) == 6 ? 0 : 1;
}
static int sum(const int *values, size_t count) {
int result = 0;
while (count != 0) {
result += *values++;
--count;
}
return result;
}
main(void) clearly indicates that the function accepts no parameters. const int * expresses a type constraint stating that the elements will not be modified through the pointer.
The general-purpose object pointer void * was also standardized.
void *memory;
int *numbers;
memory = numbers;
numbers = memory;
In C, conversions between void * and object pointers can be performed without an explicit cast. The same rule does not apply to function pointers.
C89 also specified preprocessor behavior in detail and standardized the stringification operator # and token-pasting operator ##.
#define STRINGIFY(value) #value
#define CONCAT(left, right) left ## right
int CONCAT(user_, id);
The standard library organized common interfaces for input and output, strings, memory allocation, mathematics, time, signal handling, and other functions.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
For compatibility with existing code, old-style function definition syntax continued to be permitted. This syntax remained for a long period before being removed in C23.
The Second Edition of K&R
The second edition of 《The C Programming Language》, published in 1988, reflected the new form of C that emerged during ANSI standardization. It explained modern declaration forms including function prototypes, void, and const, and became widely used in C education and documentation.
A function following the syntax of the second edition takes the following form.
int maximum(int left, int right) {
return left > right ? left : right;
}
This form, with the opening brace on the same line as the declaration, is K&R style.
C95
The 1990 international standard was amended in 1995 by ISO/IEC 9899:1990/Amd 1:1995. This edition is informally called C95. Rather than being an entirely new edition of the standard, it was an amendment applied to C90.[15]
C95 added digraphs so that C punctuation tokens could be represented even with limited character sets.
int values<:3:> = <% 10, 20, 30 %>;
The code above has the following meaning.
int values[3] = { 10, 20, 30 };
<iso646.h> provided macros that represented certain operators as words.
#include <iso646.h>
int check(int enabled, int failed) {
if (enabled and not failed) {
return 1;
}
return 0;
}
Multibyte and wide-character processing was also extended, and <wchar.h> and <wctype.h> were added.
#include <wchar.h>
#include <wctype.h>
int main(void) {
wchar_t character = L'A';
return iswalpha(character) ? 0 : 1;
}
The __STDC_VERSION__ macro made it possible to determine the standard edition supported by an implementation.
#if defined(__STDC_VERSION__) && \
__STDC_VERSION__ >= 199409L
/* C95 or later implementation */
#endif
C99
The second complete international standard edition, ISO/IEC 9899:1999, was published in December 1999 and is called C99. C99 incorporated a large set of compiler extensions, numerical-computing features, internationalization support, and modern programming practices that had developed since C89.[16]
C99 allowed variables to be declared in the middle of a block and in the initialization clause of a for statement.
int sum_values(const int *values, int count) {
int total = 0;
for (int index = 0; index < count; ++index) {
total += values[index];
}
return total;
}
In C89, declarations generally had to be grouped before executable statements in a block.
int sum_values(const int *values, int count) {
int total;
int index;
total = 0;
for (index = 0; index < count; ++index) {
total += values[index];
}
return total;
}
Single-line // comments were also incorporated into the standard.
int count = 10; // Number of elements to process
Designated initializers made it possible to name structure members and array indexes explicitly.
struct point {
int x;
int y;
};
struct point position = {
.y = 20,
.x = 10
};
int values[8] = {
[0] = 10,
[7] = 80
};
Compound literals made it possible to express unnamed objects.
struct point *create_origin(void) {
static struct point origin = {
.x = 0,
.y = 0
};
return &origin;
}
A compound literal could also be used directly as a function argument.
draw_point((struct point) {
.x = 10,
.y = 20
});
C99 introduced long long int and the integer types in <stdint.h>.
#include <stdint.h>
uint32_t flags = UINT32_C(0x80000000);
int64_t distance = INT64_C(9000000000);
Variable-length arrays, whose length is determined at runtime, were also added.
void clear_values(int count) {
int values[count];
for (int index = 0; index < count; ++index) {
values[index] = 0;
}
}
The restrict pointer qualifier provided the implementation with information about aliasing relationships among pointers used by a function.
#include <stddef.h>
void add_arrays(
size_t count,
float *restrict result,
const float *restrict left,
const float *restrict right
) {
for (size_t index = 0; index < count; ++index) {
result[index] = left[index] + right[index];
}
}
The inline function specifier was also added.
inline int square(int value) {
return value * value;
}
inline does not force actual inlining, and the implementation decides whether to perform the optimization.
Variadic macros were added as well.
#define LOG(format, ...) \
fprintf(stderr, format, __VA_ARGS__)
C99 introduced _Bool and <stdbool.h>.
#include <stdbool.h>
static bool running = true;
void stop(void) {
running = false;
}
Complex-number types and <complex.h> were also standardized.
#include <complex.h>
double complex multiply(
double complex left,
double complex right
) {
return left * right;
}
A flexible array member, which allows variably sized data to be placed at the end of a structure, was also added.
#include <stddef.h>
struct packet {
size_t size;
unsigned char data[];
};
C99 removed implicit int and implicit function declarations. Code such as the following is no longer a valid modern C program.
main() {
undeclared_function();
}
The return type and function declaration must be specified.
int declared_function(void);
int main(void) {
return declared_function();
}
C99 also included the following features.
__func__- Hexadecimal floating-point constants
<inttypes.h><tgmath.h><fenv.h>- Universal character names
- Clarification of integer division rules
- Extended structure and
unioninitialization
Three Technical Corrigenda for C99 were published in 2001, 2004, and 2007. WG14 N1256 is a publicly available working document incorporating these corrections.[17]
C11
The third international standard edition, ISO/IEC 9899:2011, was published in 2011 and is called C11. C11 incorporated requirements relating to multicore execution, concurrency, memory alignment, Unicode, and static verification into the language and standard library.[18]
The largest change was the standardization of multithreaded execution and the memory model. _Atomic and <stdatomic.h> made it possible to express atomic objects.
#include <stdatomic.h>
static _Atomic unsigned int job_count = 0;
void add_job(void) {
atomic_fetch_add(&job_count, 1);
}
The standard specified the ordering of atomic operations, memory visibility between threads, and the meaning of data races.
<threads.h> included interfaces for threads, mutexes, condition variables, and thread-specific storage.
#include <threads.h>
int worker(void *context) {
return context != 0;
}
int main(void) {
thrd_t thread;
int result;
if (thrd_create(&thread, worker, 0) != thrd_success) {
return 1;
}
thrd_join(thread, &result);
return result;
}
_Static_assert made it possible to verify a condition at translation time.
#include <stdint.h>
_Static_assert(
sizeof(uint32_t) == 4,
"uint32_t must be four bytes"
);
_Generic made it possible to select different results according to the type of an expression.
#define TYPE_NAME(value) \
_Generic((value), \
int: "int", \
double: "double", \
default: "other")
const char *get_type_name(void) {
return TYPE_NAME(10);
}
_Alignas and _Alignof were introduced to express memory alignment.
_Alignas(64) static unsigned char cache_line[64];
_Static_assert(
_Alignof(cache_line) >= 64,
"insufficient alignment"
);
Anonymous structures and anonymous union objects were also supported.
struct color {
union {
struct {
unsigned char red;
unsigned char green;
unsigned char blue;
unsigned char alpha;
};
unsigned int value;
};
};
_Noreturn was added to represent functions that do not return.
#include <stdlib.h>
_Noreturn void fatal_error(void) {
quick_exit(EXIT_FAILURE);
}
C11 also added the following features.
<uchar.h>- UTF-16 and UTF-32 character types
aligned_allocquick_exit- Exclusive file-creation mode
- An optional bounds-checking interface
- Thread storage duration
Some features could be provided optionally by implementations. Support for variable-length arrays, threads, atomic operations, and parts of complex-number functionality could differ according to the implementation environment.
C17
After C11, work focused on correcting defects and ambiguities in the standard rather than adding large-scale language features. This resulted in the publication of ISO/IEC 9899:2018.
This edition is sometimes called C17 according to its working year and C18 according to its publication year. WG14 generally uses the name C17.[19]
C17 did not add major new language syntax. It corrected and clarified the rules for atomic operations, alignment, threads, and library facilities introduced in C11.
Therefore, C11 code is used in the same form under C17.
#include <stdatomic.h>
static _Atomic int ready = 0;
int main(void) {
atomic_store(&ready, 1);
return atomic_load(&ready) == 1 ? 0 : 1;
}
A program can use __STDC_VERSION__ to check whether the implementation supports C17 or later.
#if !defined(__STDC_VERSION__) || \
__STDC_VERSION__ < 201710L
#error "C17 or later is required"
#endif
Rather than introducing new syntax, C17 organized and stabilized C11 and established a basis for the next large-scale revision.
C23
WG14 developed the next major revision after C11 under the working name C2x. The final standard was completed as C23 and published by ISO and IEC in 2024 as ISO/IEC 9899:2024. The difference between the common name C23 and the official publication year 2024 results from the difference between the committee’s development cycle and the ISO publication process.[20]
C23 included broader language changes than C11 and C17. Ordinary keyword forms were added for several names and macros that had previously begun with underscores.
bool enabled = true;
static_assert(sizeof(int) >= 2);
thread_local unsigned int worker_id;
Earlier standards used forms such as the following.
_Bool enabled = 1;
_Static_assert(
sizeof(int) >= 2,
"int is too small"
);
_Thread_local unsigned int worker_id;
C23 introduced typeof and typeof_unqual.
int value = 10;
typeof(value) another_value = 20;
typeof_unqual(value) copy = value;
constexpr objects were also introduced.
constexpr unsigned int buffer_size = 4096;
static_assert(buffer_size > 0);
Binary integer literals and apostrophe digit separators became available.
unsigned int mask = 0b1111'0000;
long population = 50'000'000;
Objects could be initialized to zero-equivalent values using an empty initializer list.
struct point {
int x;
int y;
};
struct point position = {};
int values[8] = {};
Standard attribute syntax was also added.
[[nodiscard]]
int parse_document(const char *source) {
return source != nullptr;
}
[[deprecated]]
void legacy_function(void) {
}
C23 features can be combined in code such as the following.
[[nodiscard]]
int calculate_mask(void) {
constexpr unsigned int mask = 0b1111'0000;
bool enabled = true;
typeof_unqual(mask) copy = mask;
static_assert(mask == 240);
return enabled ? (int)copy : 0;
}
The preprocessor gained #elifdef, #elifndef, #warning, and #embed.
#if defined(_WIN32)
#define PLATFORM_NAME "Windows"
#elifdef __linux__
#define PLATFORM_NAME "Linux"
#else
#warning "Unknown platform"
#endif
#embed allows external binary data to be included during translation.
static const unsigned char icon_data[] = {
#embed "icon.bin"
};
C23 also introduced nullptr and nullptr_t.
#include <stddef.h>
int *pointer = nullptr;
nullptr_t null_value = nullptr;
Old-style identifier-list function definitions were removed.
/* Old-style function definition removed in C23 */
int add(a, b)
int a, b;
{
return a + b;
}
Modern declarations and K&R brace style must be used.
int add(int a, int b) {
return a + b;
}
The meaning of a function declaration with empty parentheses also changed.
int function();
In earlier standards, this was a declaration with unspecified parameter information, but in C23 it is interpreted as a declaration of a function with no parameters.
C23 also added bit-precise integer types based on _BitInt.
_BitInt(24) signed_value = 0;
unsigned _BitInt(7) small_value = 100;
Object type inference using auto was also introduced.
auto integer = 42;
auto floating = 3.14;
C23 also included the following changes.
- Two’s-complement representation for signed integers
- Enumerations with fixed underlying types
- Improved UTF-8, UTF-16, and UTF-32 processing
- Rules for
u8characters and strings strdupandstrndupmemccpygmtime_randlocaltime_r- Integration of floating-point extensions
- Declarations before labels and compound statements at the end of labels
- Removal of several obsolete syntactic forms
The publicly available WG14 working document N3220 organizes the syntax, semantics, library, and major changes from previous editions of ISO/IEC 9899:2024.[21]
After C23
After the C23 revision cycle ended, WG14 began work on the next standard edition under the working name C2y. C2y is not the final name of the standard, but a working name for the next edition of C. N3220 records that work on the next edition began in January 2024 after the end of the C23 cycle.[22]
The following areas are under discussion for the next revision.
- Pointer provenance
- The memory-object model
- Reduction and diagnosis of undefined behavior
- Type safety
- Expression of array sizes
- Function calls and variadic arguments
- Expansion of
constexpr - String and memory APIs
- Compatibility with earlier standard editions
- Interoperability with C++ and other languages
Because C2y remains under development, not every proposed feature can be assumed to appear in the final standard. The final contents will be determined through WG14 discussion and voting, followed by review by national standards bodies.
Changes in Code Form
The historical development of C can also be observed in how the same computation is expressed in code.
In early C during the 1970s, old-style function definitions and compound assignment operators were used.
main(argc, argv)
int argv[];
{
value =+ 1;
}
A function from the K&R C period had the following form.
int add(a, b)
int a, b;
{
return a + b;
}
ANSI C made it possible to place parameter types inside the function declaration.
int add(int a, int b) {
return a + b;
}
C99 made it possible to use declarations inside loop statements and single-line comments.
int sum(int count) {
int result = 0;
for (int index = 0; index < count; ++index) {
result += index;
}
return result;
}
C11 made it possible to express atomic operations and static verification.
#include <stdatomic.h>
static _Atomic int ready = 0;
int initialize(void) {
_Static_assert(
sizeof(int) >= 2,
"invalid int"
);
atomic_store(&ready, 1);
return 0;
}
C23 allows the use of new keywords, literals, and attributes.
[[nodiscard]]
int create_mask(void) {
constexpr unsigned int mask = 0b1111'0000;
bool ready = true;
static_assert(mask == 240);
return ready ? (int)mask : 0;
}
These changes do not represent a sequence of entirely different languages replacing one another. They are the result of gradually adding stronger type checking, portability, numerical-computing facilities, concurrency, internationalization, and modern syntax while preserving the functions, expressions, pointers, arrays, structures, and direct memory model of early C.
Historical Significance
C began as a small systems language for implementing early UNIX, but through standardization it became an international standard language belonging to no particular operating system or processor.
The pointer and array model inherited from early BCPL and B, the type system created under the constraints of the PDP-11, the stronger type checking and Portable C Compiler developed through UNIX porting, the dissemination of the language through K&R, and the conservative standardization principles of ANSI and ISO remain directly present in modern C.
C’s historical continuity is also one reason why older syntax and certain irregularities remain in the language. At the same time, it provides the foundation that allows code written decades ago to connect with modern compilers, operating systems, and hardware.
Dennis Ritchie characterized C as a language with defects and peculiarities, but one efficient enough to replace assembly language and sufficiently abstract to express algorithms and system interactions across diverse environments.[23]
The history of C can be understood as a continuing process of balancing automated safety with direct control, abstraction with machine dependence, and new functionality with compatibility with existing code.
Language Characteristics
C is both a general-purpose high-level programming language and a system programming language capable of performing tasks close to hardware and operating systems. It provides control structures, functions, and compound data types for expressing general computation, data processing, and application programs, while also providing facilities for directly handling memory addresses, bit-level data, hardware registers, and external functions. WG14 describes C as a general-purpose high-level language suitable for low-level programming and identifies portability, interoperability, efficiency, and stability as prominent characteristics of the language.[24]
The characteristics of C cannot be fully explained merely by describing it as a language “close to machine code.” C programs are written for an abstract machine defined by the standard, while actual instructions, register allocation, calling conventions, and executable file formats are determined by the implementation and platform. However, C integers, arrays, structures, pointers, and explicit memory-management model were designed to correspond relatively directly to the data representations of actual computers. For this reason, the same language can be used not only for operating system kernels, device drivers, and embedded firmware, but also for databases, graphics libraries, and general-purpose applications.
General-Purpose and System Programming Language
C does not target only one specific kind of program. It is classified as a general-purpose programming language because it can be used to write ordinary applications through functions, conditional statements, loops, structures, arrays, arithmetic operations, and the standard library.
At the same time, C can express the following system-level operations.
- Manipulating memory addresses through pointers
- Controlling the layout and alignment of memory regions
- Examining and modifying individual bits of integers
- Accessing memory corresponding to hardware registers
- Calling operating system system calls and native libraries
- Running on constrained devices without a separate runtime environment
- Using extensions designed for particular processors and operating systems
The classification of C as a system programming language does not mean that a C program must necessarily be an operating system or device driver. C is a general-purpose language, but it is suitable for system implementation because programmers can control data representation and execution costs beneath abstractions relatively directly.
The low-level facilities provided by C are not completely identical to actual machine instructions either. For example, a pointer is a language-level value that refers to an object or function in an implementation-defined manner, and it is not guaranteed to be equivalent to a simple integer physical address on every platform. The C standard defines the syntax and semantics of programs, but does not specify the concrete manner in which programs are translated and called on actual systems.[25]
Imperative and Procedural Language
C is an imperative programming language in which the commands and state changes that a program must perform are described sequentially. It changes variable values, selects execution paths according to conditions, repeats commands through loops, and separates tasks through function calls.
int total = 0;
for (int i = 0; i < count; ++i) {
total += values[i];
}
This code reads each element of an array in sequence and changes the state of total. Instead of declaring the same computation as a mathematical relationship, it explicitly expresses how values are read and modified.
C organizes programs around functions in accordance with procedural programming. A function receives input through parameters and may return a value or modify external state. Related functions and data types can be divided across multiple source files and headers to modularize a program.
Arguments to function calls are passed by value by default. When a called function must directly modify an object belonging to the caller, a pointer referring to that object is passed.
void increment(int *value)
{
++*value;
}
C has no separate pass-by-reference syntax comparable to reference parameters in C++. Array parameters are adjusted to pointer types in function declarations, so although an array may appear to be passed, what is actually passed is a pointer referring to its elements.
Structured Programming
C is a structured programming language that can organize control flow hierarchically through statements, blocks, functions, conditional statements, and loops.
Its major control structures include the following.
ifandelseswitchwhiledofor- Function calls and returns
- Compound statements enclosed in braces
These structures allow programs to be expressed as hierarchies of conditions, repetition, and functions rather than as simple sequences of instruction addresses and unconditional branches.
C also provides goto. Therefore, the language does not enforce structured programming exclusively. goto can make ordinary control flow unnecessarily complicated, but it is sometimes used to reduce duplication in limited situations such as multi-stage resource cleanup and error handling.
FILE *file = fopen(path, "rb");
if (file == NULL) {
return ERROR_OPEN;
}
void *buffer = malloc(size);
if (buffer == NULL) {
goto cleanup_file;
}
/* Perform operation */
free(buffer);
fclose(file);
return SUCCESS;
cleanup_file:
fclose(file);
return ERROR_MEMORY;
C provides tools for expressing program structure but does not impose a particular design methodology or object model. By combining functions, structures, pointers, and separate translation units, programmers can directly design procedural interfaces, object-oriented-like structures, data-oriented structures, and other models.
Small Language Core
C consists of a relatively small language core and a separate standard library. File input and output, string processing, dynamic memory allocation, mathematical functions, and time processing are provided mostly through standard library functions and macros rather than as language syntax.
C language
├── Types and declarations
├── Expressions and operators
├── Statements and control flow
├── Functions
├── Objects and storage duration
├── Pointers and arrays
├── Structures, unions, and enumerations
└── Preprocessing directives
C standard library
├── Input and output
├── Strings and memory
├── Dynamic memory allocation
├── Mathematics
├── Time
├── Character classification and conversion
├── Atomic operations
└── Threads and synchronization
Describing the language as small does not mean that the entire C standard is simple. Precise and complex rules exist for array-to-pointer conversion, integer promotion, object lifetimes, effective types and aliasing, evaluation order, and preprocessing. Rather, it means that the number of keywords and fundamental abstractions used by the syntax is relatively small and that the language does not embed a large number of high-level facilities directly into the language itself.
C does not include the following facilities as fundamental language elements.
- Classes and inheritance
- Exception handling
- Automatic garbage collection
- Built-in arrays with bounds checking
- A dedicated built-in string type
- General-purpose containers
- A package and module distribution system
- Reflection
- Coroutine and asynchronous syntax
- Language-level networking and graphics APIs
Required functionality is implemented through libraries, code-generation tools, platform APIs, or designs within the program itself. This structure can keep the language and execution environment small, but it can also lead different projects to use different implementations and conventions.
Static Type System
C is a statically typed programming language in which the types of variables, functions, and expressions are primarily determined at translation time. Declarations specify the data types of objects and the parameter and return types of functions, and the compiler can diagnose expressions that do not follow the type rules.
int count;
double average;
char *text;
int calculate(int lhs, int rhs);
C types can broadly be divided into the following categories.
- Integer types
- Floating-point types
- Enumerations
- Pointer types
- Array types
- Function types
- Structures
union- Atomic types
void
Types determine value representation, permitted operations, object size and alignment, the unit of pointer arithmetic, and function-call rules. Structures, union, arrays, and pointers can be combined to represent data structures corresponding to hardware, file formats, and communication protocols.
However, C type checking is relatively flexible compared with modern strongly typed static languages. Automatic conversions and promotions occur between integer types, and explicit casts can express conversions among various pointer and integer types.
double value = 3.75;
int integer = (int)value;
Such conversions are necessary when dealing with hardware and external interfaces, but they may cause information loss and incorrect interpretation. A cast is not a guarantee that a type error has been resolved; it is syntax that explicitly requests a particular conversion from the compiler.
Value Representation and Implementation Dependence
C does not assume that all computers use the same integer sizes, byte order, or floating-point representation. It specifies relative minimum ranges and ordering for types such as char, short, int, and long, but some exact bit widths and representations are determined by the implementation.
The following properties may differ depending on the environment.
- Sizes of fundamental integer types
- Whether plain
charis signed by default - Pointer size and representation
- Alignment and padding of structure members
- Byte order
- Results of certain integer conversions
- Floating-point range and precision
- Function calling conventions
- Object-file and executable-file formats
This allows C to be implemented across systems ranging from 8-bit microcontrollers to 64-bit servers and special-purpose processors. On the other hand, code that implicitly assumes the sizes and representations of a particular environment may fail when ported to another platform.
Programs requiring exact widths can use types such as int32_t and uint64_t from <stdint.h>. However, some exact-width types may not be defined by implementations that cannot provide the corresponding width exactly.
The objectives of the C standard include promoting the portability, reliability, maintainability, and efficient execution of programs across different data-processing systems.[26] Portability does not mean that machine-level behavior is identical in every implementation, but rather that programs can distinguish between behavior guaranteed by the standard and behavior that implementations must document.
Pointers and Direct Memory Access
A pointer represents a value referring to the location of an object or function. C programs use pointers to access memory indirectly, traverse arrays, manage dynamically allocated objects and linked data structures, and connect functions with external system interfaces.
int number = 10;
int *pointer = &number;
*pointer = 20;
&number creates a pointer referring to the object number, while *pointer accesses the object to which the pointer refers.
Pointer arithmetic operates according to the size of the pointed-to type.
int values[4] = { 10, 20, 30, 40 };
int *pointer = values;
pointer += 2;
pointer += 2 does not simply add two bytes to a numeric address. It represents a position two int elements beyond the original location.
Arrays and pointers are closely related, but they are not the same type or object. An array is an object containing a fixed number of elements, while an array name is converted in most expressions into a pointer to its first element. This conversion does not occur in certain contexts, including sizeof and the address operator.
Pointers are a central element of C’s expressiveness and efficiency, but they can also cause the following errors.
- Dereferencing a null pointer
- Accessing beyond array bounds
- Using a pointer after the lifetime of its object has ended
- Using an uninitialized pointer
- Accessing an object through an incompatible type
- Incorrect alignment
- Freeing the same memory more than once
- Incorrect conversions between integers and pointers
C does not require automatic validation of object validity and array bounds for every ordinary pointer access. Programs requiring such checks must use explicit length information, validation, safe abstractions, and analysis tools.
Explicit Resource Management
C does not require a garbage collector that automatically reclaims dynamically allocated memory. A program allocates storage using malloc, calloc, and realloc, and returns it with free when it is no longer needed.
int *values = malloc(count * sizeof(*values));
if (values == NULL) {
return ERROR_MEMORY;
}
/* Use values */
free(values);
Files, sockets, mutexes, operating system handles, and graphics resources are also generally acquired and released explicitly.
Acquire resource
↓
Check for errors
↓
Use resource
↓
Release it on every execution path
Explicit resource management allows precise control over when resources are created, how long they live, and how much it costs to release them. In real-time systems, kernels, and embedded devices, this can be advantageous because it avoids unpredictable automatic reclamation work.
However, failure to manage resource lifetimes correctly can produce the following problems.
- Memory leaks
- Double frees
- Use after free
- Resource-handle leaks
- Missing cleanup on error paths
- Unclear ownership
- Incorrect destruction of partially initialized objects
C has no integrated language facility comparable to C++ destructors or Rust’s ownership system that automatically releases general resources when a scope ends. Programs manage lifetimes through functions, conventions, single cleanup paths, and separate libraries.
Direct Control over Execution Costs
C’s fundamental operations and data structures can be translated relatively directly into actual processor instructions and memory representations. Integer operations, pointer accesses, arrays, and structures do not require a complex object runtime imposed by the language, and programs are not required to automatically pay the costs of unused high-level facilities.
These characteristics allow programmers to control the following elements relatively clearly.
- Memory size of objects
- Structure composition
- Timing of memory allocation
- Function calls and the possibility of inlining
- Memory copying
- Contiguous data layout
- Bit-level representation
- Timing of system calls and library calls
However, one C statement does not always correspond to one particular machine instruction. Within the limits of preserving the observable behavior guaranteed by the language, a compiler may reorder or remove code, combine multiple operations, and use registers and vector instructions.
inline is also not a command requiring the elimination of a function call. The implementation determines how much the inline function specifier actually reduces calling costs under the standard.[27]
Performance is not automatically guaranteed merely by the name of the language. Results depend on algorithms, data structures, memory-access patterns, compilers, optimization options, and target hardware. C provides means for controlling performance, but also places responsibility for using those means correctly on the programmer.
Abstract Machine
Instead of directly defining a particular CPU, the C standard specifies the behavior of a conceptual abstract machine on which C programs execute. An implementation must translate a program so that the observable results required by the standard appear on actual hardware.
The abstract machine provides the basis for defining the following elements.
- Meaning of expressions
- Values and lifetimes of objects
- Storage duration
- Function calls and returns
- Side effects and evaluation
- Observable input and output behavior
- Threads and atomic operations
- Program startup and termination
A compiler does not have to execute every intermediate calculation in the source code literally. If externally observable behavior remains the same, it may remove calculations, replace them with constants, inline function calls, or transform loops.
int square(int value)
{
return value * value;
}
int result = square(4);
At runtime, the compiler may store 16 directly in result without performing an actual function call or multiplication. This is possible because the observable meaning of the program is preserved.
This model makes it possible to implement C on different hardware while also providing the theoretical foundation for optimization. Conversely, when a program depends on behavior whose meaning is not guaranteed by the standard, the compiler may produce results different from what the programmer expects.
Undefined Behavior
Undefined behavior is behavior for which the C standard imposes no requirements when an incorrect or nonportable program construct or data value is used. Dereferencing a null pointer is a representative example.[28]
Examples of undefined behavior include the following.
- Accessing an object outside array bounds
- Dereferencing a null pointer
- Integer division by zero
- Accessing an object after its lifetime has ended
- Signed integer overflow
- Modifying a string literal that cannot be modified
- Calling through an incompatible function-pointer type
- An unsynchronized data race
When undefined behavior occurs, the implementation is under no obligation to print an error message or terminate the program. The program may coincidentally produce the expected result, corrupt other data, or have relevant code removed through optimization.
This does not mean that the compiler deliberately destroys the program. The compiler may optimize under the assumption that a defined program follows the language rules. For example, it may simplify comparisons and loops under the assumption that signed integer overflow does not occur.
Undefined behavior allows varied implementations and optimizations without forcing the language to handle every hardware-specific condition whose checking cost may be high. However, it creates substantial risks for memory safety and security, so C programs must use input validation, static analysis, dynamic checking tools, and safe APIs.
Implementation-Defined and Unspecified Behavior
In addition to undefined behavior, C uses several categories to describe differences among implementations.
| Category | Meaning |
|---|---|
| Defined behavior | The standard specifies the result |
| Implementation-defined behavior | The implementation chooses a result and must document that choice |
| Unspecified behavior | One of several permitted results is chosen, but the choice does not have to be documented |
| Undefined behavior | The standard imposes no requirements |
Implementation-defined properties, such as whether plain char is signed, can be checked in compiler and platform documentation. When multiple orders are permitted, such as the order of function-argument evaluation, programs must not assume one particular order.
These distinctions are part of the way C supports diverse machines. Instead of fixing every detail to one virtual hardware architecture, the language allows implementations to choose efficient approaches while requiring documentation where necessary.
Balance between Portability and Machine Dependence
One of the primary goals of C standardization is to improve the portability of user programs across multiple execution environments. However, C does not eliminate every machine-dependent characteristic. WG14 describes the effort to improve portability while retaining some machine-dependent properties, avoiding the invalidation of existing programs, and preserving the basic structure and simplicity of the language as a balance among conflicting requirements.[29]
The portability of C programs can be divided into several levels.
Strictly portable code
↓
Uses only features guaranteed by the standard
Implementation-defined code
↓
Assumes implementation documentation
Platform-dependent code
↓
Uses operating system APIs and ABIs
Hardware-dependent code
↓
Uses registers, instructions, and memory maps
General-purpose libraries can use only types and behavior guaranteed by the standard whenever possible. In contrast, operating system kernels and device drivers inherently depend on particular CPUs, devices, and compiler extensions. C permits standard regions and implementation-extension regions together so that both kinds of code can be written in the same language.
The C89 and later standards committees considered it important to clarify already widespread practices rather than designing an entirely new language. Standardization rationale documents explain that established practices were retained when clear precedent existed and that improvements proven across multiple dialects were selected for standardization.[30]
Hosted and Freestanding Implementations
The C standard divides conforming implementations into hosted implementations and freestanding implementations.
A hosted implementation generally provides a complete program environment running on top of an operating system. It provides the standard-defined program startup method and a broad standard library. Ordinary C compilation environments on desktops and servers fall into this category.
A freestanding implementation targets environments where an operating system may not exist or where the ordinary program-startup method and complete standard library cannot be provided. Bootloaders, operating system kernels, and microcontroller firmware may fall into this category.
Hosted implementation
├── Operating system environment
├── Ordinary startup through main
└── Full standard library available
Freestanding implementation
├── May have no operating system
├── Entry point differs by implementation
└── Required standard-library scope is limited
The C23 standard distinguishes the two forms of conforming implementation as hosted and freestanding, and requires only a limited set of standard headers and facilities in freestanding implementations.[31]
This distinction provides the basis for using C not only for operating-system applications, but also for operating systems themselves and software that executes before an operating system. The language does not necessarily require one fixed runtime or execution environment.
Separate Compilation
A C program can be created by independently translating multiple translation units and then linking them into a single program. In general, one source file and the headers included during preprocessing form a preprocessing translation unit.
main.c ──────→ main.o ───┐
│
network.c ───→ network.o ─┼─→ Executable program
│
storage.c ───→ storage.o ─┘
Because each source file can be compiled independently, modifying one part of a program does not require retranslating all of its code. Previously translated code can be stored as static or dynamic libraries.
Translation units interact through functions and objects with external linkage, data files, and similar mechanisms. The C standard specifies that translation units may be translated independently and later linked to form an executable program.[32]
Header files provide declarations, types, and macros shared among multiple translation units. However, because header contents are textually included in each source file during preprocessing, the program and build system must maintain consistency between declarations and definitions.
Preprocessor
C provides the C preprocessor, which processes source text before full language translation. The preprocessor performs file inclusion, macro replacement, conditional compilation, and other operations.
#include <stdint.h>
#define BUFFER_SIZE 4096
#if defined(_WIN32)
#include <windows.h>
#else
#include <unistd.h>
#endif
The preprocessor provides the following facilities.
- Header-file inclusion
- Object-like macros
- Function-like macros
- Conditional compilation
- Token stringification
- Token pasting
- Implementation-specific pragmas
- Binary data inclusion in C23
The preprocessor is not a general function system that understands the meanings of types and variables, but a separate translation stage that transforms tokens. As a result, macro arguments may be evaluated multiple times, or operator precedence may differ from what is expected.
#define SQUARE(x) ((x) * (x))
When this macro is used with an expression containing side effects, such as SQUARE(value++), value may be incremented twice. Even when a preprocessing macro resembles a function, it does not automatically provide the same evaluation rules and type checking as a function.
The preprocessor is useful for managing platform differences and compilation settings within a single source tree, but excessive use can make the code actually being compiled difficult to understand.
Combination with Libraries
C delegates many facilities to libraries rather than including them in the language itself. The standard library defines a common API that every conforming hosted implementation must provide, while operating systems and platforms provide additional APIs above it.
C program
├── C language facilities
├── C standard library
├── Operating system APIs
├── External C libraries
└── Native interfaces of other languages
Strings are also represented not as independent high-level objects built into the language, but by convention as arrays of char elements terminated by a null character. String length, memory size, and buffer capacity are managed by the program and library caller.
This organization allows only the required facilities to be selected even in small execution environments. On the other hand, networking, graphics, and some threading facilities not provided by the standard library must depend on operating-system-specific APIs and separate libraries.
Interoperability and ABI
C is widely used as a language for expressing public interfaces of operating systems and libraries. On many platforms, C function-calling conventions and the layout of fundamental data types form the basis of the application binary interface, and other programming languages provide interfaces that can call C functions or be called from C.
The following languages and runtimes can use C interfaces.
This interoperability is not completely defined by the C standard alone. Structure alignment, function calling conventions, name mangling, and dynamic-library formats are determined by platform ABIs and compilers.
When using C interfaces between different languages, the following matters must be agreed upon.
- Sizes and representations of data types
- Function calling convention
- Structure layout and alignment
- Memory ownership
- String encoding and termination
- Error propagation method
- Lifetime of callback functions
- Thread safety
- Boundaries for exceptions and panics
C interfaces are widely used because the language provides simple, long-lived data-type and function models, and because C compilers and ABIs exist on most native platforms. WG14 also explains that C is used as a common language connecting systems and languages, as a compiler target language, an implementation language for interpreters, and a language for operating-system and embedded programming.[33]
Conservative Standardization and Compatibility
C has source code, libraries, and operating system APIs that have been used for decades. Therefore, when new features are added, not only the consistency of the language but also their effects on existing programs and implementations are considered important.
WG14 standardization principles include balancing the following requirements.
- Improve portability.
- Retain some machine-dependent characteristics.
- Avoid damaging the basic structure of the language through new features.
- Avoid unnecessarily invalidating existing programs.
- Improve clarity and consistency.
- Preserve the simplicity of C.
- Consider implementation experience and actual practice.
This policy improves compatibility between old C code and new compilers, but can also make it difficult to remove historically inherited irregularities and dangerous features quickly. C23 removed some old function-declaration forms and obsolete facilities, but the overall language model remains continuous with earlier C.
Distinction between the Language and Its Implementations
The C language must be distinguished from the features of particular compilers. Implementations such as GCC, Clang, and MSVC may provide the following extensions in addition to standard C.
- Vector types for particular CPUs
- Inline assembly
- Function and variable attributes
- Operating-system-specific calling conventions
- Nonstandard built-in functions
- Extended integer types
- Diagnostic and static-analysis facilities
- Statement expressions not defined by the standard
- Extensions for kernel development
Extensions allow the use of platform facilities that are difficult to express in standard C, but may not operate under other implementations. Separating portable code from platform-dependent code makes it possible to reuse common logic across environments while replacing only the required implementation-specific parts.
Common standard C code
+
Operating-system-specific code
+
Processor-specific code
+
Compiler-specific extensions
The fact that a compiler accepts particular syntax does not mean that the feature is part of the C standard. Build settings and documentation must clearly record which C standard edition and implementation extensions a program depends on.
Programmer Responsibility
C allows programmers to directly control data layout, resource lifetimes, pointers, buffer sizes, and error handling. At the same time, many matters are not automatically checked by the language or implementation.
C programmers may need to manage the following items directly.
- Lengths of arrays and buffers
- Pointer validity
- Object lifetimes
- Ownership of memory and resources
- Ranges of integer operations
- Function return values and error codes
- Synchronization of concurrent access
- String termination and encoding
- Implementation-defined behavior
- Platform-specific ABI differences
This design is often described by saying that C trusts the programmer. However, this does not mean that incorrect programs are considered correct. It means that various checking and resource-management policies are assigned to implementations, tools, and program design rather than always being enforced by the language runtime.
Knowing the language syntax alone is not enough to write safe and maintainable C programs. Compiler warnings, static analysis, address sanitizers, undefined-behavior sanitizers, fuzzing, code review, clear ownership rules, and restricted APIs must be used together.
Summary of Characteristics
C is a high-level language providing ordinary control flow, functions, and static typing, while also allowing the data and execution costs of actual systems to be handled directly through pointers, bitwise operations, and explicit resource management. Its small language core, library-centered structure, and distinction between hosted and freestanding environments allow it to be implemented across a broad range of systems, from machines with operating systems to small embedded devices.
C provides portability not by hiding all hardware differences, but by distinguishing behavior guaranteed by the standard from behavior that implementations may determine. Separate compilation and stable function-centered interfaces support large-scale system and library development as well as interoperability with other programming languages.
These characteristics provide high efficiency and a broad range of applications, while also creating the limitation that programmers must rigorously manage memory safety, resource lifetimes, and undefined behavior. The design of C is the result of continuously balancing automated safety with direct control, abstraction with machine dependence, and new functionality with compatibility with existing code.
Syntax and Components
A C program has a structure in which source files written as sequences of characters are preprocessed and translated into an executable program or library. Source code is divided into tokens such as keywords, identifiers, constants, string literals, operators, and punctuators, and these tokens form declarations, expressions, statements, function definitions, and preprocessing directives.
C syntax is designed by combining a relatively small number of fundamental elements. Variables, functions, and types are declared, values are calculated through expressions, and execution flow is constructed through statements. Complex data types can be expressed by combining type constructors such as arrays, pointers, functions, and structures.
#include <stdio.h>
struct point {
int x;
int y;
};
static int add_coordinates(const struct point *point) {
return point->x + point->y;
}
int main(void) {
struct point position = {
.x = 10,
.y = 20
};
printf("%d\n", add_coordinates(&position));
return 0;
}
This program contains the following major components of C.
- The
#includepreprocessing directive - A structure declaration using
struct - The fundamental
inttype - Object and function declarations
- A pointer and the
constqualifier - A function call
- Member access operators
- Designated initialization
- A
returnstatement - Compound statements
The C standard defines the syntax and meaning of these elements, but the arrangement of source files, build commands, object file formats, and executable file formats are determined by the implementation and operating system.[34]
Translation Units
A C program consists of one or more translation units. In general, one .c source file together with the contents of the headers included by that file during preprocessing forms a preprocessing translation unit.
main.c
+ included headers
↓
preprocessing translation unit
↓
compilation
↓
object file
Multiple translation units can each be translated independently and then linked into one program.
main.c → main.o ───┐
│
math.c → math.o ───┼→ executable program
│
storage.c → storage.o ┘
Functions and objects with external linkage can be shared between translation units. Header files provide types, function declarations, and macros that can be used in common by multiple translation units.
/* calculator.h */
#ifndef CALCULATOR_H
#define CALCULATOR_H
int add(int left, int right);
int subtract(int left, int right);
#endif
/* calculator.c */
#include "calculator.h"
int add(int left, int right) {
return left + right;
}
int subtract(int left, int right) {
return left - right;
}
/* main.c */
#include <stdio.h>
#include "calculator.h"
int main(void) {
printf("%d\n", add(10, 20));
return 0;
}
Each source file is compiled independently, but the types of function declarations and definitions must be compatible with one another. Declaring int add(int, int) in one translation unit and defining it with a different type in another does not form a correct program.
Translation Phases
The C standard conceptually divides the processing of source files into translation phases. An actual compiler does not have to execute every phase as a separate program, but the final result must have the same meaning as the order specified by the standard.
The major process can be summarized as follows.
source file characters
↓
character and line-break processing
↓
comment removal
↓
preprocessing token recognition
↓
preprocessing directives and macro expansion
↓
string literal concatenation
↓
translation unit analysis
↓
external reference linking
Comments are replaced with whitespace during preprocessing.
int value = 10; /* This comment is replaced with whitespace. */
Adjacent string literals are combined into one string.
const char *message =
"TechPedia "
"Wiki Engine";
This declaration represents the following string.
const char *message = "TechPedia Wiki Engine";
String concatenation can be used to divide a long sentence across multiple source lines or combine macros with strings.
Character Sets
C source is written using letters, digits, punctuation, and whitespace characters. The standard defines a basic source character set and a basic execution character set, and an implementation maps the actual character encoding of a source file to the C character set during translation.
Characters fundamentally used in C source include the following.
- Latin uppercase letters from
AthroughZ - Latin lowercase letters from
athroughz - Digits from
0through9 - Spaces, horizontal tabs, and line breaks
- Special characters used for operators and punctuators
Identifiers may use extended characters permitted by the implementation and standard in addition to basic Latin letters. However, considering compatibility with source code, tools, operating systems, and other languages, public API identifiers often use a restricted character set.
Comments
C supports block comments and line comments.
A block comment begins with /* and ends with */.
/*
* An explanation can be written
* across multiple lines.
*/
int value = 10;
A line comment extends from // to the end of the current line.
int count = 10; // Number of elements to process
Block comments cannot be nested.
/*
Outer comment
/* Inner comment */
The outer comment may already be treated as ended at this point.
*/
Nesting block comments to temporarily remove code can produce unexpected syntax errors. When conditional compilation is needed, #if 0 can be used.
#if 0
int disabled_function(void) {
return 10;
}
#endif
Tokens
During preprocessing and translation, source code is divided into tokens. The major token categories are as follows.
| Category | Examples |
|---|---|
| Keywords | int, return, struct |
| Identifiers | value, calculate, point |
| Constants | 10, 3.14, 'A' |
| String literals | "hello" |
| Operators | +, ==, && |
| Punctuators | ;, ,, {, } |
| Header names | <stdio.h>, "project.h" |
Whitespace separates tokens, but in most contexts the exact amount of whitespace does not affect meaning.
int value = 10;
The following code has the same token structure.
int
value
=
10
;
However, arbitrary whitespace cannot be inserted inside string and character literals, preprocessing directives, or multi-character operators.
value += 1;
The following contains separate + and = tokens rather than the += operator, so it forms different syntax.
value + = 1;
Keywords
Keywords have predefined meanings in C syntax and cannot be used as ordinary identifiers.
C keywords serve as data types, type qualifiers, storage classes, control statements, operators, and other language elements.
Representative keywords include the following.
| Category | Example keywords |
|---|---|
| Fundamental types | char, int, float, double, void |
| Signedness and size | signed, unsigned, short, long |
| Compound types | struct, union, enum |
| Type qualifiers | const, restrict, volatile, _Atomic |
| Storage classes | auto, extern, register, static, typedef |
| Control statements | if, else, switch, case, for, while, do |
| Jump statements | goto, continue, break, return |
| Operations and inspection | sizeof, typeof, typeof_unqual |
| Function specifiers | inline, _Noreturn |
| Alignment | _Alignas, _Alignof |
| Generic selection | _Generic |
| Static verification | static_assert, _Static_assert |
| C23 keywords | bool, true, false, nullptr, constexpr, thread_local |
The keyword set differs by C standard edition. For example, inline and restrict were included in the standard in C99, _Atomic and _Generic in C11, and typeof, constexpr, and nullptr in C23.
constexpr unsigned int buffer_size = 4096;
bool initialized = false;
int *pointer = nullptr;
This code can be used in C23, but in earlier standard editions the same identifiers are not standard keywords or the facilities themselves are unavailable.
Identifiers
Identifiers are names assigned to variables, functions, type aliases, structure tags, enumeration constants, labels, macros, and other entities.
int item_count;
double calculate_average(void);
struct network_packet;
typedef unsigned long object_id;
An identifier begins with a letter or permitted extended character and may contain digits afterward.
int value;
int value2;
int network_packet_count;
The following name cannot be an identifier because it begins with a digit.
int 2nd_value;
C is case-sensitive.
int value;
int Value;
int VALUE;
These are three different identifiers.
Reserved Identifiers
Some identifiers are reserved for the implementation and standard library. If a program arbitrarily defines reserved names, it may conflict with the standard library or compiler implementation.
Names that should generally be avoided include the following.
- Names beginning with two underscores
- Names beginning with an underscore followed by an uppercase letter
- Names beginning with an underscore at file scope
- Names reserved by the standard library in particular headers
- Names of standard functions and macros
int __internal_value;
int _SystemValue;
These names are reserved for the implementation.
Identifiers within a program are safer in forms such as the following.
int internal_value;
int system_value;
An implementation must support a sufficiently large maximum significant length for identifiers, but names with external linkage may also be constrained by the linker and object file format.
Name Spaces
C does not manage all identifiers within a single name space. Several kinds of names are managed separately according to their syntactic position.
The major name spaces are as follows.
- Label names
- Structure,
union, and enumeration tags - Members of structures and unions
- Other ordinary identifiers
- Preprocessing macro names
Therefore, the following declarations do not conflict.
struct item {
int item;
};
int item;
The item in struct item belongs to the tag name space, while the global variable item belongs to the ordinary identifier name space. The structure member item belongs to the member name space of that structure.
Labels also use a separate name space.
int process(void) {
int cleanup = 0;
cleanup:
return cleanup;
}
The variable cleanup and label cleanup belong to different name spaces and can therefore coexist syntactically. However, reusing the same name is generally avoided for readability.
Constants
Constants represent fixed values written directly in source code. C has integer constants, floating-point constants, enumeration constants, character constants, and other constant forms.
Integer Constants
Integer constants can use different prefixes according to their radix.
int decimal = 42;
int octal = 052;
int hexadecimal = 0x2A;
int binary = 0b101010;
- No prefix indicates decimal
- A leading
0indicates octal 0xor0Xindicates hexadecimal0bor0Bindicates binary in C23
C23 permits apostrophes to be inserted as separators between digits.
unsigned int mask = 0b1111'0000;
long population = 50'000'000;
Digit separators do not affect the value.
The type of an integer constant is determined by its value, radix, and suffix.
10
10U
10L
10UL
10LL
10ULL
Uoruindicates an unsigned typeLorlindicateslongLLorllindicateslong long
Because lowercase l can be confused with the digit 1, uppercase L is generally easier to read.
long distance = 1000L;
unsigned long flags = 0x8000UL;
Floating-Point Constants
Floating-point constants use a decimal point or exponent notation.
double first = 3.14;
double second = 1.0e-6;
float third = 2.5F;
long double fourth = 1.0L;
Without a suffix, the type is double; with F, it is float; and with L, it is long double.
Hexadecimal floating-point constants can also be used.
double value = 0x1.8p+1;
The exponent following p represents a power of two. The value above is hexadecimal 1.8 multiplied by (2^1), so it represents 3.0.
Character Constants
Character constants are written with single quotation marks.
int letter = 'A';
int newline = '\n';
int tab = '\t';
An ordinary character constant has type int. An object storing a character value usually uses char.
char letter = 'A';
A character constant containing multiple characters may be syntactically permitted, but its value is implementation-defined and it has low portability.
int value = 'AB';
Such multicharacter constants are generally avoided in ordinary programs.
Escape Sequences
Characters that are difficult to write directly in character and string literals are represented by escape sequences beginning with a backslash.
| Notation | Meaning |
|---|---|
\n | Line break |
\t | Horizontal tab |
\r | Carriage return |
\b | Backspace |
\f | Form feed |
\v | Vertical tab |
\\ | Backslash |
\' | Single quotation mark |
\" | Double quotation mark |
\0 | Null character |
const char *path = "C:\\Program Files\\Example";
const char *message = "first line\nsecond line\n";
Characters can also be represented by octal and hexadecimal values.
char letter_a = '\101';
char another_a = '\x41';
Universal character names can be used to represent Unicode code points in source code.
const char *copyright = "\u00A9";
The actual encoding of an execution string depends on the string prefix and the implementation’s character encoding rules.
String Literals
A string literal represents an array of characters enclosed in double quotation marks.
"hello"
An ordinary string literal creates an array of char with a null character automatically appended at the end.
'h' 'e' 'l' 'l' 'o' '\0'
Therefore, the size of the following array is 6.
char message[] = "hello";
A pointer can also refer to a string literal.
const char *message = "hello";
In C, the element type of an ordinary string literal is historically char, but attempting to modify its contents has undefined behavior.
char *message = "hello";
message[0] = 'H';
Because string literals must not be modified, using const char * is safer when referring to one through a pointer.
const char *message = "hello";
When a modifiable string is needed, it can be copied into an array.
char message[] = "hello";
message[0] = 'H';
String literals may have prefixes indicating character encoding.
"ordinary string"
u8"UTF-8 string"
u"UTF-16 string"
U"UTF-32 string"
L"wide string"
The element type and encoding rules of each string differ according to the prefix.
Punctuators
Punctuators delimit the structure of declarations, expressions, statements, and preprocessing syntax.
Representative punctuators include the following.
| Symbol | Major uses |
|---|---|
; | Terminates declarations and expression statements |
, | Separates declarators, arguments, and initializer elements |
() | Function calls, parameters, and expression grouping |
[] | Array declarations and subscripting |
{} | Compound statements and initializer lists |
. | Accesses members of structures or unions |
-> | Accesses members through a pointer |
: | Labels, case, and bit-fields |
... | Variadic parameters |
# | Preprocessing directives and stringification |
## | Preprocessing token pasting |
A semicolon can also form an empty statement.
while (is_device_busy()) {
}
The semicolon in the following code is the empty statement forming the body of while.
while (is_device_busy())
;
An unintended semicolon can make a control statement contain an empty body.
if (ready); {
process();
}
In this code, the block executes unconditionally regardless of the if.
Declarations
A declaration specifies the meaning and type of an identifier, its storage properties, and other attributes.
int count;
This declaration declares the name count as an object of type int.
Functions can also be declared.
int calculate(int left, int right);
A declaration generally consists of the following elements.
declaration specifiers + declarator + initializer
static const unsigned long count = 10UL;
The elements in this declaration have the following roles.
static: storage-class specifierconst: type qualifierunsigned long: type specifiercount: declarator= 10UL: initializer
Multiple identifiers can be declared in one declaration.
int left, right, result;
However, when pointers are involved, it is important to note that * belongs to each individual declarator.
int *left, right;
Only left is an int *; right is an int.
To declare two pointers, each declarator must contain *.
int *left;
int *right;
They can also be written as follows.
int *left, *right;
For readability and ease of modification, some projects use a rule of placing only one object in each declaration.
Declarators
A declarator combines an identifier with type structures such as pointers, arrays, and functions.
int value;
int *pointer;
int values[10];
int function(int value);
int (*callback)(int value);
A declaration can be read similarly to the form in which its identifier is used in an expression.
int *pointer;
This means that *pointer has type int.
int values[10];
This means that values[index] has type int.
int function(int value);
This means that function(value) returns an int.
int (*callback)(int value);
Because (*callback)(value) returns an int, callback is a function pointer.
Parentheses change the binding order of a declarator.
int *functions[4];
functions is an array whose elements are int *.
int (*function)(void);
function is a pointer to a function returning int.
int *function(void);
function is a function returning int *.
Complex declarations can be divided into stages with typedef.
typedef int (*comparison_function)(
const void *left,
const void *right
);
comparison_function comparator;
Definitions
A declaration introduces a name and type, while a definition creates storage for an object or provides the body of a function.
The following is an object definition.
int global_count;
The following declares an object but does not define it in this translation unit.
extern int global_count;
A function prototype is a function declaration.
int calculate(int left, int right);
Including a function body makes it a definition.
int calculate(int left, int right) {
return left + right;
}
Duplicating definitions of objects or functions with external linkage within one program can produce linker errors or violate definition rules.
Types
C types determine the kinds of values represented by objects and functions and the operations that can be applied to them.
The major type categories are as follows.
types
├── object types
│ ├── fundamental types
│ │ ├── integer types
│ │ ├── floating-point types
│ │ └── enumerations
│ ├── pointer types
│ ├── array types
│ ├── structures
│ ├── unions
│ └── atomic types
├── function types
└── void type
A type affects an object’s size and alignment, the range of its values, its representation, and the operations permitted on it.
Integer Types
The standard integer types include the following.
_Bool
char
signed char
unsigned char
short
unsigned short
int
unsigned int
long
unsigned long
long long
unsigned long long
C23 allows bool to be used alongside _Bool.
bool enabled = true;
The exact bit widths of integer types are not the same in every implementation. The standard specifies minimum representable ranges and ordering relationships among their sizes.
sizeof(char) ≤ sizeof(short) ≤ sizeof(int)
≤ sizeof(long) ≤ sizeof(long long)
sizeof(char) is always 1, but this does not mean that one C byte must contain exactly eight bits. The number of bits in one byte can be checked through CHAR_BIT.
#include <limits.h>
static_assert(CHAR_BIT >= 8);
char, signed char, and unsigned char are distinct types. Whether plain char is signed or unsigned is determined by the implementation.
When an exact width is required, the types in <stdint.h> can be used.
#include <stdint.h>
uint8_t byte;
int32_t coordinate;
uint64_t identifier;
An exact-width type is defined only when the implementation can provide precisely that number of bits.
Floating-Point Types
The real floating-point types are as follows.
float
double
long double
The range and precision of each type can differ according to the implementation and floating-point format.
float single_precision = 1.0F;
double double_precision = 1.0;
long double extended_precision = 1.0L;
C can also support complex types.
#include <complex.h>
double complex value = 1.0 + 2.0 * I;
Floating-point computation is not completely identical to ordinary real-number mathematics. It can be affected by rounding, infinities, NaN values, and the floating-point environment.
void
void indicates the absence of a value or the absence of a specified complete object type.
A function that returns no value uses void as its return type.
void print_message(void) {
puts("hello");
}
void * is used as a general-purpose pointer to an object.
void *memory = malloc(1024);
void itself is not an object type, so an object such as the following cannot be created.
void value;
To access the object referred to by a void *, it must be converted to a specific object pointer type.
int value = 10;
void *memory = &value;
int *integer = memory;
printf("%d\n", *integer);
Enumerations
An enumeration defines a set of named integer constants.
enum connection_state {
CONNECTION_DISCONNECTED,
CONNECTION_CONNECTING,
CONNECTION_CONNECTED
};
An enumeration object can be used to represent one of the defined states.
enum connection_state state = CONNECTION_CONNECTING;
Enumeration constants can be used as integer constant expressions.
int counters[CONNECTION_CONNECTED + 1];
C23 allows an enumeration to specify a fixed underlying type.
enum packet_type : unsigned char {
PACKET_CONNECT = 1,
PACKET_DATA = 2,
PACKET_DISCONNECT = 3
};
This syntax is unavailable in standard editions before C23.
Structures
A structure places members of different types sequentially within one object.
struct point {
int x;
int y;
};
A structure object is defined as follows.
struct point position;
It can also be initialized.
struct point position = {
.x = 10,
.y = 20
};
Members of a structure object are accessed with ..
position.x = 30;
When using a pointer to a structure, -> is used.
struct point *pointer = &position;
pointer->y = 40;
The following two expressions have the same meaning.
pointer->x
(*pointer).x
Padding for alignment can be inserted between structure members. Therefore, the size of a structure must not be assumed to equal the simple sum of the sizes of its members.
struct example {
char tag;
int value;
};
Padding may be inserted between char and int.
union
A union allows several members to share the same storage.
union number {
int integer;
float floating;
};
The size of a union is generally sufficient to satisfy the size and alignment requirements of its largest member.
union number value;
value.integer = 10;
Reading one member after storing a value in another involves detailed standard rules and implementation characteristics. It must not be assumed to be an unconditionally safe means of arbitrary type conversion.
A separate tag can be stored to distinguish the currently active member.
enum value_type {
VALUE_INTEGER,
VALUE_FLOATING
};
struct value {
enum value_type type;
union {
int integer;
float floating;
} data;
};
Arrays
An array stores elements of the same type contiguously.
int values[4];
Elements are accessed with indexes beginning at zero.
values[0] = 10;
values[1] = 20;
The number of elements in an array can be calculated as follows.
size_t count = sizeof(values) / sizeof(values[0]);
This method is valid only in a scope where values is an actual array. An array declaration used as a function parameter is adjusted to a pointer type.
void clear_values(int values[10]) {
/* The type of values is adjusted to int * inside the function. */
}
Therefore, inside the function, sizeof(values) yields the size of a pointer rather than the size of the original complete array.
The array size must be passed separately.
void clear_values(int *values, size_t count) {
for (size_t index = 0; index < count; ++index) {
values[index] = 0;
}
}
C99 variable-length arrays can use a size determined at runtime.
void process(size_t count) {
int values[count];
/* ... */
}
Support for variable-length arrays and the contexts in which they can be used can differ according to the standard edition and implementation.
Pointers
A pointer represents a value referring to an object or function.
int value = 10;
int *pointer = &value;
& obtains the address of an object, and * accesses the object referred to by a pointer.
*pointer = 20;
A null pointer does not refer to a valid object or function.
int *pointer = NULL;
C23 allows nullptr to be used.
int *pointer = nullptr;
Dereferencing a null pointer causes undefined behavior.
*pointer = 10;
Pointer arithmetic operates in units of elements of the pointed-to type.
int values[] = { 10, 20, 30 };
int *pointer = values;
pointer += 2;
pointer refers to the third int element.
Pointer arithmetic is defined only within the bounds of the same array object and the position immediately beyond its end. Arbitrarily adding or subtracting pointers to unrelated objects is not permitted.
Function Types
A function type includes a return type and parameter types.
int add(int left, int right);
add is a function that accepts two int values and returns an int.
A pointer to a function can be created.
int (*operation)(int left, int right) = add;
A function pointer can be called.
int result = operation(10, 20);
The following form represents the same call.
int result = (*operation)(10, 20);
Function pointers are used for callbacks, strategy selection, event processing, and dynamic library interfaces.
Type Qualifiers
Type qualifiers express additional constraints concerning how an object is accessed, optimization, and concurrency.
const
const indicates that an object cannot be modified through the corresponding identifier.
const int value = 10;
The following assignment is not permitted.
value = 20;
In a pointer declaration, the meaning differs according to the position of const.
const int *pointer;
The int referred to by pointer cannot be modified through this pointer.
int *const pointer = &value;
The pointer itself cannot be changed to refer to another address.
const int *const pointer = &value;
Neither the pointer itself nor the pointed-to value can be changed through the corresponding identifier.
const does not always mean that an object can never physically change. The possibility of modification through another unqualified pointer, hardware, or another thread depends on the object and program structure.
volatile
volatile indicates that access to an object may have side effects that the implementation must observe.
volatile unsigned int *status_register;
It can be used for memory-mapped hardware registers and certain signal-processing objects.
while ((*status_register & READY_BIT) == 0) {
}
The compiler treats each access as one that must actually be performed.
volatile is not a thread synchronization mechanism. When multiple threads access the same object, appropriate synchronization such as atomic types or mutexes is required.
restrict
restrict expresses a promise that a particular pointer is a primary means of accessing an object.
void copy_values(
int *restrict destination,
const int *restrict source,
size_t count
) {
for (size_t index = 0; index < count; ++index) {
destination[index] = source[index];
}
}
If the caller violates the aliasing rules associated with restrict, undefined behavior can occur. restrict is not merely an optimization request, but a semantic rule that the program must obey.
_Atomic
Atomic types provide indivisible atomic accesses between multiple threads.
#include <stdatomic.h>
_Atomic int counter = 0;
An atomic type specifier can also be used.
atomic_int counter = 0;
Standard atomic operations can be applied to atomic objects.
atomic_fetch_add(&counter, 1);
Atomicity alone does not automatically solve synchronization for an entire algorithm. Memory ordering and the consistency of compound state must also be designed.
Storage Classes
Storage class specifiers affect the storage duration and linkage of objects and functions and the nature of a declaration.
auto
An ordinary local variable at block scope has automatic storage duration by default.
void function(void) {
auto int value = 10;
}
auto traditionally represented the storage class of a local object, but it has almost always been omitted.
void function(void) {
int value = 10;
}
In C23, auto can also infer an object type from its initializer.
auto value = 42;
auto ratio = 3.14;
register
register was a storage class suggesting that an implementation provide fast access to an object.
register int index;
Modern compilers determine register allocation through their own analysis, so this specifier does not guarantee performance. Traditionally, there was also a restriction preventing the address operator from being applied to a register object.
static
At file scope, static gives an identifier internal linkage.
static int internal_count;
This object can be accessed by name only within the same translation unit.
It can also give a function internal linkage.
static int calculate_internal_value(void) {
return 10;
}
When used with a local object at block scope, it gives the object static storage duration.
int next_identifier(void) {
static int identifier = 0;
return ++identifier;
}
identifier retains its lifetime after a function call ends.
extern
extern indicates that an object or function may be defined elsewhere.
extern int global_count;
Another translation unit can contain the following definition.
int global_count = 0;
A function declaration has external linkage by default.
extern int calculate(void);
Normally, extern is omitted.
int calculate(void);
typedef
typedef assigns a new name to an existing type.
typedef unsigned long object_id;
object_id identifier = 10;
It can simplify the representation of a structure type.
typedef struct point {
int x;
int y;
} point;
point position = {
.x = 10,
.y = 20
};
typedef does not create a new incompatible type; it creates an alias for an existing type.
typedef int meter;
typedef int second;
meter and second are both the same type as int and are not automatically distinguished from each other.
Scope
Scope indicates the region of source code in which an identifier introduced by a declaration can be used.
The major scopes are as follows.
- File scope
- Block scope
- Function prototype scope
- Function scope
File Scope
An identifier declared outside a function has file scope.
static int global_count;
int calculate(void) {
return global_count;
}
The name can be used from its declaration point to the end of the translation unit.
Block Scope
An identifier declared inside a block can be used in that block and its nested blocks.
int calculate(void) {
int result = 10;
if (result > 0) {
int doubled = result * 2;
return doubled;
}
return result;
}
doubled cannot be used outside the if block.
An inner declaration can hide an outer name.
int value = 10;
void function(void) {
int value = 20;
printf("%d\n", value);
}
The value inside the function hides the global value.
Function Scope
A label name has function scope extending across the entire function.
int process(int failed) {
if (failed) {
goto cleanup;
}
return 0;
cleanup:
return -1;
}
A label can be referenced within the same function even when it appears after the goto.
Linkage
Linkage determines whether identical names in different scopes or translation units refer to the same object or function.
Linkage is divided as follows.
- External linkage
- Internal linkage
- No linkage
A function with external linkage can denote the same function in multiple translation units.
int calculate(void);
Declaring it with static at file scope gives it internal linkage.
static int calculate_internal(void);
A local variable has no linkage.
void function(void) {
int local_value;
}
Linkage and scope are different concepts. Scope is the source region in which a name can be used, while linkage determines whether multiple declarations are connected to the same entity.
Storage Duration
Storage duration indicates the period during which storage for an object exists.
The major storage durations are as follows.
- Static storage duration
- Thread storage duration
- Automatic storage duration
- Allocated storage duration
Static Storage Duration
File-scope objects and static local objects exist throughout the entire execution of a program.
static int global_count;
int next_value(void) {
static int local_count;
return ++local_count;
}
Thread Storage Duration
A thread_local or _Thread_local object has a separate instance for each thread.
thread_local int current_worker_id;
The object’s lifetime is associated with the execution period of the corresponding thread.
Automatic Storage Duration
Ordinary local variables and function parameters have automatic storage duration associated with execution of the block.
int calculate(int input) {
int result = input * 2;
return result;
}
When the function returns, the lifetimes of input and result end. Returning a pointer to such an object produces an invalid pointer.
int *invalid_pointer(void) {
int value = 10;
return &value;
}
Allocated Storage Duration
Storage obtained from a dynamic memory allocation function remains until it is explicitly released.
int *value = malloc(sizeof(*value));
if (value != NULL) {
*value = 10;
free(value);
}
Failing to release allocated memory causes a memory leak. Accessing storage after it has been released causes undefined behavior.
Initialization
Initialization establishes the initial value of an object when it is created.
int value = 10;
An array can use an initializer list.
int values[4] = { 10, 20, 30, 40 };
If only some elements are provided, the remaining elements are initialized to values equivalent to zero.
int values[4] = { 10 };
The array contains the following values.
10, 0, 0, 0
A structure can also be initialized in order.
struct point {
int x;
int y;
};
struct point position = { 10, 20 };
Designated initialization can explicitly name members.
struct point position = {
.y = 20,
.x = 10
};
Array indexes can also be designated.
int values[10] = {
[0] = 10,
[9] = 100
};
C23 allows an empty initializer list.
struct point position = {};
int values[10] = {};
Objects are initialized to values equivalent to zero for their respective types.
An object with static storage duration is zero-initialized even without an explicit initializer.
static int global_count;
static int *global_pointer;
An object with automatic storage duration can contain an indeterminate value when it is not explicitly initialized.
void function(void) {
int value;
printf("%d\n", value);
}
Reading the value of an uninitialized automatic object can produce undefined behavior or an indeterminate result depending on the object, type, and context, so the object must be assigned a value before use.
Type Conversions
C converts values to other types in many situations. A conversion can occur implicitly or be explicitly requested through a cast.
Integer Promotions
char, short, _Bool, and some enumeration types are promoted to int or unsigned int in arithmetic operations.
unsigned char left = 200;
unsigned char right = 100;
int result = left + right;
The addition is not performed directly in the original unsigned char type, but after integer promotion.
Usual Arithmetic Conversions
When values of different arithmetic types are used together in an operation, conversions determine a common type.
int integer = 10;
double floating = 2.5;
double result = integer + floating;
integer is converted to double before the addition.
Mixing signed and unsigned integers can produce unexpected results.
int signed_value = -1;
unsigned int unsigned_value = 1;
if (signed_value < unsigned_value) {
/* The environment and conversion rules must be understood. */
}
Before the comparison, signed_value can be converted to an unsigned type, causing -1 to be interpreted as a very large positive value.
Explicit Casts
A cast writes the destination type in parentheses.
double value = 3.75;
int integer = (int)value;
The fractional part is discarded and integer becomes 3.
Pointer types can also be converted.
void *memory = malloc(sizeof(int));
int *integer = (int *)memory;
In C, a conversion from void * to an object pointer type does not require a cast.
int *integer = memory;
An unnecessary cast can hide type errors. In particular, casting the return value of malloc can conceal a missing declaration caused by failing to include <stdlib.h>, so the cast is generally omitted in C.
A cast does not resolve invalid data, alignment, or object lifetime problems. It merely specifies a type conversion permitted by the standard.
Expressions
An expression is a syntactic element that computes a value, reads or modifies an object, or calls a function.
left + right
An expression has a value and type, and some expressions produce side effects.
count += 1;
This expression computes a value while also modifying count.
Expressions can be combined as parts of larger expressions.
result = left + right * scale;
Operator precedence causes multiplication to bind first.
result = left + (right * scale);
Evaluation order is a different concept from precedence. Precedence determines which operators bind to which operands syntactically, while separate rules determine the order in which the operands are actually evaluated.
Value Categories
C expressions can have different properties, such as designating an object or merely computing a value.
An expression designating an object can sometimes appear on the left side of an assignment.
int value;
value = 10;
An expression that does not designate a modifiable object cannot be assigned to.
10 = value;
Array and function expressions are converted in most contexts to pointers.
int values[4];
int *pointer = values;
A function name can also be converted to a function pointer.
int calculate(int value);
int (*callback)(int) = calculate;
Arithmetic Operators
Arithmetic operators are used for numerical computation.
| Operator | Meaning |
|---|---|
+ | Addition |
- | Subtraction |
* | Multiplication |
/ | Division |
% | Remainder |
+value | Unary plus |
-value | Unary minus |
int sum = left + right;
int difference = left - right;
int product = left * right;
int quotient = left / right;
int remainder = left % right;
Integer division truncates toward zero.
int value = 7 / 2;
The result is 3.
int value = -7 / 2;
The result is -3.
Integer division by zero causes undefined behavior.
int value = 10 / 0;
An operation whose result exceeds the representable range of a signed integer also causes undefined behavior.
int value = INT_MAX;
value += 1;
Unsigned integers perform modular arithmetic according to the range of their type.
unsigned int value = UINT_MAX;
value += 1;
The result is 0.
Increment and Decrement
++ and -- increase or decrease an object’s value by one.
++count;
--count;
A prefix operation yields the modified value as its result.
int result = ++count;
A postfix operation yields the original value and then modifies the object.
int result = count++;
Combining expressions with side effects in a complicated way within one statement can cause evaluation-order problems.
values[index++] = index;
An expression that reads and modifies the same object without the required sequencing can have undefined behavior. Separating the modifications into different statements is clearer.
values[index] = index;
++index;
Comparison Operators
Comparison operators compare two values and produce an int value of 1 when true and 0 when false.
| Operator | Meaning |
|---|---|
== | Equal |
!= | Not equal |
< | Less than |
> | Greater than |
<= | Less than or equal |
>= | Greater than or equal |
if (left == right) {
/* ... */
}
Confusing the assignment operator = with the equality operator == can cause errors.
if (value = 10) {
/* Assigns 10 to value and then evaluates as true. */
}
Use == for comparison.
if (value == 10) {
/* ... */
}
Enabling compiler warnings can diagnose suspicious assignments inside conditions.
Logical Operators
Logical operators combine conditions.
| Operator | Meaning | | | | -------- | ----------- | - | ---------- | | ! | Logical NOT | | | | && | Logical AND | | | | | | | Logical OR |
if (pointer != NULL && *pointer > 0) {
process(*pointer);
}
&& and || perform short-circuit evaluation.
&&does not evaluate the right operand when the left operand is false.||does not evaluate the right operand when the left operand is true.
Therefore, in the code above, *pointer is not evaluated when pointer is a null pointer.
A logical operation produces either 0 or 1.
int result = left < right && ready;
Bitwise Operators
Bitwise operators process individual bits of integer values.
| Operator | Meaning | |
|---|---|---|
& | Bitwise AND | |
| | Bitwise OR | |
^ | Bitwise XOR | |
~ | Bitwise complement | |
<< | Left shift | |
>> | Right shift |
unsigned int flags = 0;
flags |= FLAG_VISIBLE;
flags &= ~FLAG_DISABLED;
flags ^= FLAG_SELECTED;
A particular bit can be tested as follows.
if ((flags & FLAG_VISIBLE) != 0) {
/* ... */
}
Shift operations move bit positions.
unsigned int mask = 1U << 5;
If the shift count is negative or greater than or equal to the width of the type, undefined behavior occurs. Care is also required with shifts of negative signed values and left shifts beyond the representable range. Using unsigned integer types for bit manipulation usually makes the rules easier to understand.
Assignment Operators
Simple assignment stores the right-hand value in the left-hand object.
value = 10;
Compound assignment operators combine the existing value with an operation and store the result again.
value += 10;
value -= 10;
value *= 2;
value /= 2;
value %= 3;
flags &= mask;
flags |= mask;
flags ^= mask;
value <<= 1;
value >>= 1;
Compound assignment differs from simple textual replacement in that the left operand is evaluated only once.
values[index++] += 10;
index++ is evaluated once.
An assignment expression itself also has a value.
left = right = 0;
First, 0 is assigned to right, and then the resulting value is assigned to left.
The result of an assignment can be used in a condition, but parentheses and an explicit comparison should be used when the intent could otherwise be unclear.
while ((character = getchar()) != EOF) {
putchar(character);
}
Conditional Operator
The conditional operator ?: evaluates one of two expressions according to a condition.
int maximum = left > right ? left : right;
This is similar to the following if statement.
int maximum;
if (left > right) {
maximum = left;
} else {
maximum = right;
}
The conditional operator is suitable for short expressions that select a value. Readability can decrease when it contains multiple side effects or nested conditions.
Comma Operator
The comma operator evaluates the left expression, then evaluates the right expression, and uses the right result as the value of the complete expression.
int result = (prepare(), calculate());
Commas used in function argument lists and declarator lists have different syntactic roles from the comma operator.
function(left, right);
The evaluation order of the two function arguments is not guaranteed to be left-to-right merely because they are separated by a comma.
The comma operator is often used in a for statement to update multiple expressions together.
for (
size_t left = 0, right = count - 1;
left < right;
++left, --right
) {
/* ... */
}
sizeof
sizeof yields the size of a type or object representation in bytes.
size_t integer_size = sizeof(int);
size_t value_size = sizeof value;
Parentheses are required around a type name.
sizeof(int)
Parentheses can be omitted for an expression.
sizeof value
The size of an entire array can be obtained.
int values[10];
size_t size = sizeof(values);
size_t count = sizeof(values) / sizeof(values[0]);
Most expressions inside sizeof are not actually evaluated.
size_t size = sizeof(value++);
Except in some situations involving variable-length arrays, value is not incremented.
When allocating dynamic memory, calculating the size from the destination object makes the code resilient to type changes.
int *values = malloc(count * sizeof(*values));
Alignment Operators
_Alignof, or alignof in C23, yields the alignment requirement of a type.
size_t alignment = alignof(double);
_Alignas, or alignas, specifies an alignment requirement for an object.
alignas(64) unsigned char cache_line[64];
Alignment can be important for structure layout, SIMD, atomic access, and hardware interfaces.
Member Access
Members of structure and union objects are accessed with ..
position.x
Member access through a pointer uses ->.
pointer->x
-> combines dereferencing with ..
pointer->x
(*pointer).x
Parentheses are required because of the precedence of the dereference operator.
*pointer.x
This expression is interpreted as *(pointer.x), so it has a different meaning from structure pointer access.
Array Subscripting
Array subscripting combines pointer arithmetic with dereferencing.
values[index]
It is equivalent to the following expression.
*(values + index)
Syntactically, the following expression can produce the same result.
index[values]
However, it is not used because it violates convention and reduces readability.
C does not automatically check bounds for ordinary array subscripting.
int values[4];
values[4] = 10;
The valid indexes are 0 through 3, so this out-of-bounds access causes undefined behavior.
Function Calls
A function is called by writing parentheses and an argument list after a function name or function pointer.
int result = add(10, 20);
Argument expressions are converted according to the function’s parameter types.
double square(double value);
double result = square(10);
The integer 10 is converted to double.
Function arguments are generally not guaranteed to be evaluated from left to right.
function(first(), second());
It must not be assumed whether first or second is called first. When execution order matters, the calls should be separated into statements.
int first_result = first();
int second_result = second();
function(first_result, second_result);
Compound Literals
A C99 compound literal creates an unnamed object from a type name and initializer list.
struct point {
int x;
int y;
};
draw_point((struct point) {
.x = 10,
.y = 20
});
An array compound literal can also be created.
process_values(
(int[]) { 10, 20, 30 },
3
);
The storage duration of a compound literal is determined by its location of use and the rules of the corresponding standard edition.
Generic Selection
C11 _Generic selects one expression according to the type of a controlling expression.
#define TYPE_NAME(value) \
_Generic((value), \
int: "int", \
float: "float", \
double: "double", \
default: "other")
printf("%s\n", TYPE_NAME(10));
The result is "int".
A macro can also select a type-specific function.
#define ABSOLUTE(value) \
_Generic((value), \
int: abs, \
long: labs, \
float: fabsf, \
double: fabs \
)(value)
_Generic performs type selection at translation time rather than runtime branching.
Constant Expressions
A constant expression is a restricted expression that can be evaluated at translation time. It is used for array sizes, enumeration constants, case labels, static verification, and other contexts.
enum {
BUFFER_SIZE = 1024
};
int buffer[BUFFER_SIZE];
switch (value) {
case BUFFER_SIZE:
break;
}
A C23 constexpr object can define a named constant.
constexpr size_t buffer_size = 4096;
static_assert(buffer_size > 0);
A const object is not always accepted as an integer constant expression.
const int size = 10;
Depending on the standard edition and context, it may not be usable as a constant expression for an array size or similar construct. An enum, macro, or C23 constexpr can be used instead.
Statements
Statements construct the execution flow of a program.
The major statement categories are as follows.
- Labeled statements
- Compound statements
- Expression statements
- Selection statements
- Iteration statements
- Jump statements
Expression Statements
An expression followed by a semicolon forms an expression statement.
value = 10;
function();
++count;
A semicolon without an expression forms an empty statement.
;
Compound Statements
A group of declarations and statements enclosed in braces is called a compound statement or block.
{
int value = 10;
process(value);
}
Compound statements are used as function bodies and control statement bodies.
if Statement
if executes a statement according to a condition.
if (value > 0) {
process_positive(value);
}
else specifies a statement to execute when the condition is false.
if (value > 0) {
process_positive(value);
} else {
process_non_positive(value);
}
Multiple conditions can be connected.
if (value > 0) {
result = 1;
} else if (value < 0) {
result = -1;
} else {
result = 0;
}
In C, zero is treated as false and a nonzero value as true.
if (pointer) {
/* pointer is not a null pointer */
}
An explicit comparison can also be used.
if (pointer != NULL) {
/* ... */
}
When braces are omitted, only the next single statement belongs to the condition.
if (ready)
start();
Because additional statements may be added during maintenance, explanatory code in TechPedia uses braces by default even when the body contains only one statement.
if (ready) {
start();
}
switch Statement
switch selects one of several execution paths according to the value of an integer or enumeration expression.
switch (command) {
case COMMAND_START:
start();
break;
case COMMAND_STOP:
stop();
break;
default:
report_unknown_command();
break;
}
A case label requires an integer constant expression.
Without break, execution continues into the following case.
switch (level) {
case LEVEL_ERROR:
log_error();
case LEVEL_WARNING:
increase_warning_count();
break;
}
When such fallthrough is intentional, a C23 attribute or explicit comment can be used.
switch (level) {
case LEVEL_ERROR:
log_error();
[[fallthrough]];
case LEVEL_WARNING:
increase_warning_count();
break;
}
case values cannot be duplicated within the same switch.
while Statement
while repeats its body while the condition is true.
while (count > 0) {
process();
--count;
}
The condition is evaluated before each iteration, so the body is not executed at all when the condition is false initially.
An infinite loop can be written as follows.
while (true) {
process_events();
}
Before C23, true from <stdbool.h> or the integer 1 can be used.
while (1) {
process_events();
}
do Statement
A do statement executes its body first and checks the condition afterward.
do {
read_input();
} while (!input_is_valid());
The body executes at least once. A semicolon is required after the final while.
for Statement
for places initialization, a condition, and a post-iteration expression together.
for (size_t index = 0; index < count; ++index) {
process(values[index]);
}
Its structure is as follows.
for (initialization; condition; post-iteration expression)
Initialization executes once before the loop begins. The condition is evaluated before each iteration, and the post-iteration expression executes after the body.
Each part can be omitted.
for (;;) {
process_events();
}
This forms an infinite loop.
Multiple values can also be updated.
for (
size_t left = 0, right = count - 1;
left < right;
++left, --right
) {
swap(&values[left], &values[right]);
}
break
break terminates the nearest loop or switch statement.
while (true) {
if (should_stop()) {
break;
}
process();
}
Within nested loops, only the innermost loop is terminated.
continue
continue skips the remainder of the current iteration and proceeds to the next one.
for (size_t index = 0; index < count; ++index) {
if (!is_valid(values[index])) {
continue;
}
process(values[index]);
}
In a for statement, control moves to the post-iteration expression. In while and do, it moves to condition evaluation.
return
return terminates the current function and returns control to the calling location.
A value-returning function uses a return expression.
int add(int left, int right) {
return left + right;
}
A void function can return without a value.
void process(int value) {
if (value < 0) {
return;
}
/* ... */
}
The return expression is converted to the function’s return type.
double calculate(void) {
return 10;
}
The integer 10 is converted to double.
A problem occurs when a function that must return a value reaches the end of a normal execution path without one. As an exception, reaching the end of main has the same meaning as return 0;.
goto and Labels
goto transfers execution to a label within the same function.
int process(const char *path) {
FILE *file = fopen(path, "rb");
if (file == NULL) {
return -1;
}
void *buffer = malloc(4096);
if (buffer == NULL) {
goto cleanup_file;
}
free(buffer);
fclose(file);
return 0;
cleanup_file:
fclose(file);
return -1;
}
Because C has no integrated facility that automatically releases general resources when a scope ends, goto is sometimes used in a limited manner for error handling that releases several resources in reverse order.
int process(const char *path) {
int result = -1;
FILE *input = NULL;
FILE *output = NULL;
void *buffer = NULL;
input = fopen(path, "rb");
if (input == NULL) {
goto cleanup;
}
output = fopen("output.bin", "wb");
if (output == NULL) {
goto cleanup;
}
buffer = malloc(4096);
if (buffer == NULL) {
goto cleanup;
}
result = 0;
cleanup:
free(buffer);
if (output != NULL) {
fclose(output);
}
if (input != NULL) {
fclose(input);
}
return result;
}
goto cannot transfer control outside the function or to a label in another function.
Functions
A function is a program unit that accepts input through parameters, performs work, and optionally returns a value.
A function declaration specifies a return type, name, and parameter types.
int add(int left, int right);
A function definition includes a body.
int add(int left, int right) {
return left + right;
}
An appropriate declaration must be visible before a function is called.
int calculate(int value);
int main(void) {
return calculate(10);
}
int calculate(int value) {
return value * 2;
}
Since C99, an undeclared function is no longer implicitly treated as a function returning int.
Parameters
Function parameters are local objects initialized from argument values when the function is called.
int multiply(int left, int right) {
return left * right;
}
C passes arguments by value by default.
void set_value(int value) {
value = 10;
}
The caller’s object is not modified.
int value = 0;
set_value(value);
To modify the caller’s object, a pointer is passed.
void set_value(int *value) {
*value = 10;
}
int value = 0;
set_value(&value);
A structure can also be passed by value.
struct point move_point(struct point point, int x, int y) {
point.x += x;
point.y += y;
return point;
}
The original structure is not modified; the copied parameter is changed.
A large structure can be passed through a pointer when considering copying costs and the intention to modify it.
void move_point(struct point *point, int x, int y) {
point->x += x;
point->y += y;
}
Array Parameters
Array syntax in a function parameter is adjusted to a pointer type.
void process(int values[10]);
In the function type, this is compatible with the following declaration.
void process(int *values);
The array length is not passed automatically.
void process(const int *values, size_t count) {
for (size_t index = 0; index < count; ++index) {
printf("%d\n", values[index]);
}
}
C99 array parameter syntax can express qualifiers or a minimum element count.
void process(
size_t count,
int values[static count]
) {
/* The caller must provide at least count elements. */
}
This syntax supplies contract information to the caller and implementation, but it does not mean that runtime bounds checking is automatically performed.
Variadic Functions
Using ... as the last parameter declares a function accepting a variable number of arguments.
int printf(const char *restrict format, ...);
A user-defined variadic function uses <stdarg.h>.
#include <stdarg.h>
int sum_values(size_t count, ...) {
va_list arguments;
int result = 0;
va_start(arguments, count);
for (size_t index = 0; index < count; ++index) {
result += va_arg(arguments, int);
}
va_end(arguments);
return result;
}
The caller must provide exactly the number and types of arguments expected by the function.
int result = sum_values(3, 10, 20, 30);
Individual type checking through a function prototype does not apply to the variadic part. Reading an argument with the wrong type can cause undefined behavior.
Recursion
A C function can call itself directly or indirectly.
unsigned long long factorial(unsigned int value) {
if (value <= 1) {
return 1;
}
return value * factorial(value - 1);
}
Each recursive call may create new automatic objects and call information. Excessively deep recursion can exhaust the call stack of the execution environment.
inline
inline indicates that a function call can be implemented in inline form and also affects C linkage and definition rules.
static inline int square(int value) {
return value * value;
}
The compiler determines whether actual inlining occurs. inline alone does not guarantee elimination of call overhead.
When a short function is defined in a header, the static inline form with internal linkage is commonly used.
#ifndef VECTOR_H
#define VECTOR_H
struct vector2 {
float x;
float y;
};
static inline struct vector2 vector2_add(
struct vector2 left,
struct vector2 right
) {
return (struct vector2) {
.x = left.x + right.x,
.y = left.y + right.y
};
}
#endif
Function Pointers and Callbacks
A function pointer allows a function to be selected and passed like a value.
typedef int (*operation_function)(int left, int right);
int add(int left, int right) {
return left + right;
}
int subtract(int left, int right) {
return left - right;
}
int calculate(
operation_function operation,
int left,
int right
) {
return operation(left, right);
}
int result = calculate(add, 10, 20);
An array of function pointers can select a function according to a command.
static operation_function operations[] = {
add,
subtract
};
If a function pointer type is incompatible with the actual function, calling through it can cause undefined behavior.
Preprocessor
The C preprocessor processes preprocessing tokens in source code before ordinary C parsing.
Its major facilities are as follows.
- Header and file inclusion
- Macro definition and expansion
- Conditional compilation
- Translation diagnostics
- Implementation directives
- Source line information control
- Binary data inclusion in C23
A preprocessing directive generally begins with # as the first preprocessing token of a line.
#include <stdio.h>
#define BUFFER_SIZE 4096
#include
#include includes the contents of a specified header or file in the current preprocessing translation unit.
#include <stdio.h>
The angle bracket form generally uses the implementation’s and system’s header search paths.
#include "project.h"
The quoted form generally searches the current source and project paths first, and if the file is not found, may continue with the system header search method. The implementation determines the exact search rules.
Because header text is included directly, an include guard is used to prevent the same declarations from being processed multiple times.
#ifndef PROJECT_H
#define PROJECT_H
int project_initialize(void);
#endif
When supported by an implementation, #pragma once can also be used, but it is not an ISO C standard preprocessing directive.
Object-Like Macros
An object-like macro replaces a name with other preprocessing tokens.
#define BUFFER_SIZE 4096
#define APPLICATION_NAME "TechPedia"
The tokens are expanded where the macro is used.
char buffer[BUFFER_SIZE];
A macro is not a typed object. Enumeration constants or C23 constexpr objects can also be used for integer constants.
enum {
BUFFER_SIZE = 4096
};
constexpr size_t buffer_size = 4096;
Function-Like Macros
A function-like macro accepts arguments and substitutes tokens.
#define MAXIMUM(left, right) \
((left) > (right) ? (left) : (right))
Failing to parenthesize a macro body and its arguments can cause operator precedence problems.
#define SQUARE(value) value * value
int result = SQUARE(1 + 2);
The expansion is as follows.
int result = 1 + 2 * 1 + 2;
A safer form is as follows.
#define SQUARE(value) ((value) * (value))
However, the problem of evaluating an argument multiple times remains.
int result = SQUARE(index++);
Because index++ is expanded twice, undefined behavior or an unexpected result can occur. When types and evaluation rules are required, an inline function is safer.
static inline int square(int value) {
return value * value;
}
Stringification
Placing # before a parameter of a function-like macro converts the supplied tokens to a string.
#define STRINGIFY(value) #value
const char *text = STRINGIFY(hello);
The result is "hello".
When a macro must be expanded before stringification, a helper macro is required.
#define STRINGIFY_DIRECT(value) #value
#define STRINGIFY(value) STRINGIFY_DIRECT(value)
#define VERSION 23
const char *version = STRINGIFY(VERSION);
Token Pasting
## combines two preprocessing tokens into one.
#define DECLARE_VARIABLE(name) int variable_##name
DECLARE_VARIABLE(count);
The expansion is as follows.
int variable_count;
Token pasting can be used for repetitive declarations and code generation, but excessive use can make generated identifiers difficult to trace.
Variadic Macros
A variadic macro uses ... and __VA_ARGS__.
#define LOG(format, ...) \
fprintf(stderr, format, __VA_ARGS__)
LOG("value: %d\n", value);
C23 provides __VA_OPT__ to handle an empty variadic argument list.
#define LOG(format, ...) \
fprintf( \
stderr, \
format __VA_OPT__(,) __VA_ARGS__ \
)
LOG("started\n");
LOG("value: %d\n", value);
Conditional Compilation
Conditional compilation includes or excludes portions of source according to preprocessing conditions.
#if defined(_WIN32)
#define PLATFORM_NAME "Windows"
#elif defined(__linux__)
#define PLATFORM_NAME "Linux"
#elif defined(__APPLE__)
#define PLATFORM_NAME "Apple"
#else
#define PLATFORM_NAME "Unknown"
#endif
#ifdef and #ifndef test whether a macro is defined.
#ifdef DEBUG
log_debug_information();
#endif
#ifndef BUFFER_SIZE
#define BUFFER_SIZE 4096
#endif
C23 allows #elifdef and #elifndef.
#if defined(_WIN32)
#define PLATFORM_WINDOWS
#elifdef __linux__
#define PLATFORM_LINUX
#endif
A preprocessing conditional expression does not provide every facility of an ordinary C expression. It uses integer constant expressions and the defined operator available during preprocessing.
#error and #warning
#error produces a diagnostic that causes translation to fail.
#ifndef REQUIRED_FEATURE
#error "REQUIRED_FEATURE must be defined"
#endif
C23 #warning requests a warning diagnostic.
#ifndef OPTIMIZED_BUILD
#warning "Building without optimization"
#endif
#line
#line changes the line number and file name information for subsequent source.
#line 100 "generated.c"
Code generators and preprocessing tools can use it to map diagnostic locations back to original source.
#pragma
#pragma requests implementation-specific behavior.
#pragma STDC FENV_ACCESS ON
The standard defines some STDC pragmas, but many pragmas are compiler-specific extensions.
#pragma once
The handling of an unsupported pragma follows implementation rules. Portable code must consider pragma support and fallback paths.
_Pragma
_Pragma is a unary-operator-like form that represents a pragma using a string literal.
_Pragma("STDC FENV_ACCESS ON")
It has the advantage that a pragma can be generated within a macro.
#define ENABLE_FLOATING_ENVIRONMENT \
_Pragma("STDC FENV_ACCESS ON")
#embed
C23 #embed includes bytes from an external resource during preprocessing.
static const unsigned char icon_data[] = {
#embed "icon.bin"
};
The file’s data is inserted as array initializer elements. This can be used to include binary resources in an executable, such as embedded firmware data, game and graphics resources, and test data.
How #embed locates a file and which resource paths it supports are also affected by the implementation environment and options.
Predefined Macros
An implementation predefines several macros specified by the standard.
Representative macros include the following.
| Macro | Meaning |
|---|---|
__FILE__ | Current source file name |
__LINE__ | Current line number |
__DATE__ | Translation date |
__TIME__ | Translation time |
__STDC__ | Whether the implementation conforms to standard C |
__STDC_VERSION__ | Supported C standard edition |
__STDC_HOSTED__ | Whether the implementation is hosted |
printf(
"%s:%d\n",
__FILE__,
__LINE__
);
The standard edition can be checked.
#if defined(__STDC_VERSION__) && \
__STDC_VERSION__ >= 202311L
#define HAS_C23 1
#else
#define HAS_C23 0
#endif
Because __DATE__ and __TIME__ can produce different results for every build, reproducible builds may avoid them or control them through the build system.
Attributes
C23 attributes are standard syntax for attaching additional information to declarations, statements, and other constructs.
[[nodiscard]]
int create_resource(void);
An implementation can issue a diagnostic when the return value is not used.
[[deprecated]]
void legacy_function(void);
This indicates that use of the facility is discouraged.
switch (value) {
case 1:
process_first();
[[fallthrough]];
case 2:
process_second();
break;
}
Major attributes defined in C23 include the following.
deprecatedfallthroughmaybe_unusednodiscardnoreturnreproducibleunsequenced
An implementation can also provide extension attributes in separate attribute name spaces. Programs can preserve portability by considering the standard rules for handling unknown attributes.
Static Verification
static_assert checks a condition at translation time.
static_assert(sizeof(unsigned int) >= 4);
If the condition is false, a required diagnostic is produced during translation.
It can verify assumptions about structures and binary interfaces.
struct packet_header {
uint32_t type;
uint32_t size;
};
static_assert(
sizeof(struct packet_header) == 8,
"unexpected packet header size"
);
However, a simple size check alone cannot guarantee every property of structure padding and byte order.
The earlier C11 syntax is _Static_assert.
_Static_assert(
sizeof(int) >= 2,
"int is too small"
);
C23 allows static_assert to be used directly.
Complete Example
The following code combines several C syntactic elements and components in one small program.
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#define INITIAL_CAPACITY 8
struct integer_list {
int *values;
size_t count;
size_t capacity;
};
static bool integer_list_reserve(
struct integer_list *list,
size_t capacity
) {
if (capacity <= list->capacity) {
return true;
}
int *new_values = realloc(
list->values,
capacity * sizeof(*new_values)
);
if (new_values == NULL) {
return false;
}
list->values = new_values;
list->capacity = capacity;
return true;
}
static bool integer_list_push(
struct integer_list *list,
int value
) {
if (list->count == list->capacity) {
size_t new_capacity = list->capacity == 0
? INITIAL_CAPACITY
: list->capacity * 2;
if (!integer_list_reserve(list, new_capacity)) {
return false;
}
}
list->values[list->count++] = value;
return true;
}
static void integer_list_destroy(
struct integer_list *list
) {
free(list->values);
*list = (struct integer_list) {
.values = NULL,
.count = 0,
.capacity = 0
};
}
int main(void) {
struct integer_list list = { 0 };
for (int value = 1; value <= 10; ++value) {
if (!integer_list_push(&list, value)) {
fprintf(stderr, "memory allocation failed\n");
integer_list_destroy(&list);
return EXIT_FAILURE;
}
}
for (size_t index = 0; index < list.count; ++index) {
printf("%d\n", list.values[index]);
}
integer_list_destroy(&list);
return EXIT_SUCCESS;
}
This program uses the following elements.
- Standard headers and a macro
- A structure
- Pointers
size_t- Functions with internal linkage
- Function prototypes and parameters
- Dynamic memory allocation
sizeof- Conditional statements and loops
- The conditional operator
- Designated structure initialization
- A compound literal
- Error return values
- Explicit resource release
Each individual syntactic element of C is relatively simple, but the meaning of a program can become complex when types, object lifetimes, conversions, evaluation order, and pointer rules are combined. Writing a correct C program requires understanding not only its syntactic form, but also the type and value of each expression, when each object exists, and which accesses are permitted.[35]
Memory Model
The C memory model specifies when program objects are created and destroyed, how values are stored as byte representations, what pointers may refer to, when memory access through different types is permitted, and in what order reads and writes from multiple threads are observed.
Here, the memory model is not a concept that directly describes the virtual memory structure of a particular operating system or the physical memory layout of a processor. The C standard defines an abstract machine in which programs execute and requires implementations to reproduce the observable behavior of that abstract machine on actual hardware.
The memory of a C program is also commonly described as follows.
code area
read-only data
global and static data
dynamically allocated area
call stack
This structure is actually used in many operating systems and executable file formats, but the ISO C standard does not require these specific regions or directions. The central concepts in the standard are objects, types, storage duration, lifetime, alignment, representation, access rules, and execution order.
Objects
In C, an object is a region of storage whose contents are interpreted as representing a value. Not only values declared as variables, but also array elements, structure members, arrays created by string literals, and values created in dynamically allocated storage can be objects.
int count = 10;
count is an object of type int. Storage capable of representing an int value exists for this object.
An array is one array object that also contains its individual element objects.
int values[4] = {
10,
20,
30,
40
};
This declaration contains the array object values and four int element objects.
A structure is also one structure object containing multiple member objects.
struct point {
int x;
int y;
};
struct point position = {
.x = 10,
.y = 20
};
position is a structure object, while position.x and position.y are member objects contained within it.
An object does not necessarily have to possess an identifier. Objects created in dynamically allocated storage or through compound literals may have no ordinary variable name.
struct point *origin = &(struct point) {
.x = 0,
.y = 0
};
The compound literal creates an unnamed struct point object, and origin points to that object.
Objects and Values
An object and the value stored in that object are not the same concept.
int value = 10;
valueis an identifier denoting an object.- The object’s type is
int. - The value currently stored in the object is 10.
- The object has a particular byte representation for storing values.
The following assignment does not change the object itself into another object, but changes the value stored in the object.
value = 20;
A single object can store multiple values in sequence during its lifetime.
Subobjects
Array elements and members of structures or unions are subobjects contained within a larger object.
struct rectangle {
struct point minimum;
struct point maximum;
};
struct rectangle area;
The following object relationships exist within area.
area
├── minimum
│ ├── x
│ └── y
└── maximum
├── x
└── y
A subobject has its own type and value, but its storage and lifetime are closely tied to the enclosing object. When the lifetime of the enclosing structure object ends, the lifetimes of the member objects within it also end.
Storage Duration and Lifetime
An object’s storage duration classifies how storage is obtained and maintained. Its lifetime is the period during actual program execution in which the object’s storage is guaranteed to exist and the object can be validly referenced.
C defines the following four storage durations.
- Static storage duration
- Thread storage duration
- Automatic storage duration
- Allocated storage duration
During an object’s lifetime, its storage is maintained, its address generally remains constant, and the most recently stored value is retained. Referring to an object after its lifetime ends or improperly using a pointer that formerly pointed to that object causes undefined behavior.[36]
Storage duration differs from an identifier’s scope.
- Scope is the source-code region in which a name can be used.
- Storage duration is the manner in which an object’s storage exists.
- Lifetime is the period during execution in which the object actually exists.
- Linkage determines whether multiple declarations denote the same object or function.
Static Storage Duration
Objects defined at file scope and local objects declared with static have static storage duration.
static int global_count;
int next_identifier(void) {
static int identifier = 0;
return ++identifier;
}
global_count and identifier exist throughout the entire execution of the program. Even after next_identifier returns, the lifetime of the local static object identifier does not end.
Objects with static storage duration are initialized once before program startup. If no explicit initializer is provided, they are initialized to a zero value appropriate for their type.
static int count;
static double ratio;
static int *pointer;
Conceptually, they have the following initial values.
count → 0
ratio → 0.0
pointer → null pointer
Static storage duration does not necessarily imply external linkage.
int public_value;
static int private_value;
Both objects have static storage duration, but public_value may have external linkage while private_value has internal linkage.
Thread Storage Duration
An object declared with thread_local or _Thread_local has thread storage duration.
thread_local unsigned int worker_identifier;
Each thread has an independent instance of worker_identifier.
thread A → worker_identifier A
thread B → worker_identifier B
thread C → worker_identifier C
A value stored by one thread in its own instance does not directly modify another thread’s instance.
thread_local int error_code;
void set_error(int code) {
error_code = code;
}
The object’s lifetime is associated with the execution period of the corresponding thread. Indirectly referring to another thread’s thread-storage object can involve implementation rules and synchronization issues.
Automatic Storage Duration
Ordinary local variables and function parameters have automatic storage duration.
int calculate(int input) {
int result = input * 2;
return result;
}
The input and result objects exist when the function is called, and their lifetimes end when function execution finishes.
An object within a block is associated with execution of that block.
void process(bool enabled) {
if (enabled) {
int temporary = 10;
use_value(temporary);
}
}
The lifetime of temporary begins when execution enters the block and reaches the declaration, and ends when the block is left.
The address of an automatic object must not be used after the object’s lifetime has ended.
int *create_invalid_pointer(void) {
int value = 10;
return &value;
}
The lifetime of value ends when the function returns. The returned pointer no longer points to a live int object.
int *pointer = create_invalid_pointer();
printf("%d\n", *pointer);
This dereference has undefined behavior.
Automatic storage duration is not guaranteed to be implemented using the actual CPU call stack. A compiler can place an object in a register or eliminate it completely through optimization. Actual stack storage is more likely to be used during recursive calls or when an address escapes, but automatic storage duration is a standard concept and the stack is only a representative implementation technique.
Allocated Storage Duration
Storage provided by malloc, calloc, realloc, and related allocation functions has allocated storage duration.
int *value = malloc(sizeof(*value));
if (value == NULL) {
return -1;
}
*value = 10;
free(value);
The lifetime of storage obtained from a successful allocation extends from the time of allocation until the time it is released. A pointer returned by an allocation function is suitably aligned so that objects requiring fundamental alignment can be created within the requested size.[37]
malloc provides uninitialized storage of the specified size.
int *values = malloc(count * sizeof(*values));
The allocated bytes are not automatically set to zero.
calloc receives an element count and element size, allocates storage, and initializes every bit to zero.
int *values = calloc(count, sizeof(*values));
It must not be generalized that an all-bits-zero representation always means mathematical zero or a null pointer for every type. calloc is a function that sets bytes to zero, and in common modern implementations it is frequently used for integer-zero and null-pointer initialization. When a type-specific initial value is required, explicitly initializing objects individually makes the meaning clearer.
malloc and Object Types
Storage returned by malloc has no declared object type specified by a declaration.
void *memory = malloc(sizeof(int));
When the storage is used through an int * and a value is stored, the meaning of an int object required for that access is established.
int *integer = memory;
*integer = 10;
The C23 standard describes the type of an object without a declared type through effective type rules when a value is stored or copied using memcpy.[38]
In C, the return value of malloc generally does not need an explicit cast.
int *values = malloc(count * sizeof(*values));
It can also be written as follows, but the cast is unnecessary in C.
int *values = (int *)malloc(count * sizeof(*values));
In older compilation environments, using a cast could hide an error in which the function declaration was missing because <stdlib.h> had not been included. In C code, allocation without a cast and with sizeof based on the destination pointer type is generally safer.
Calculating Allocation Sizes
When allocating a dynamic array, the number of elements is multiplied by the size of each element.
int *values = malloc(count * sizeof(*values));
However, if the multiplication exceeds the range of size_t, storage smaller than the actual requirement may be allocated.
if (count > SIZE_MAX / sizeof(*values)) {
return NULL;
}
int *values = malloc(count * sizeof(*values));
Even before C23, some implementations and libraries provided separate allocation functions that checked for overflow, but portable fundamental C code may need to perform this check directly.
The result of a zero-byte allocation can be a special pointer value or a null pointer according to the implementation and function rules. Even when the returned pointer is not null, the storage must not be dereferenced as though it contained an object.
realloc
realloc changes the size of an existing allocation.
int *new_values = realloc(
values,
new_count * sizeof(*new_values)
);
if (new_values == NULL) {
/* The original values allocation remains valid. */
return false;
}
values = new_values;
A successful realloc may extend the allocation at its existing location or allocate storage at a new location and copy the contents.
Therefore, the original pointer must not be overwritten before success is checked.
values = realloc(values, new_size);
If the call fails, a null pointer is stored in values, potentially losing the only means of accessing the original allocation.
When realloc succeeds, the lifetime of the previous allocation ends. Even if the returned numerical address appears unchanged, separate pointers that referred to the previous object must no longer be used.
int *alias = values;
int *new_values = realloc(values, new_size);
if (new_values != NULL) {
values = new_values;
/* alias is no longer used. */
}
Deallocation and Use After Free
Dynamically allocated storage is returned using free.
free(values);
When free is called, the lifetime of the allocated object ends.
values[0] = 10;
Dereferencing a pointer after deallocation has undefined behavior.
Assigning a null pointer to the pointer variable can reduce accidental reuse of that same variable.
free(values);
values = NULL;
However, other aliasing pointers do not automatically become null pointers.
int *first = values;
int *second = values;
free(first);
first = NULL;
/* second still contains the address of released storage. */
For this reason, dynamic memory management must clearly establish which code owns an allocation and who is responsible for releasing it.
Ownership
The C standard has no language feature that automatically tracks ownership. However, actual programs must design ownership rules to manage resource lifetimes.
Representative rules include the following.
owning pointer
→ responsible for releasing the object
non-owning pointer
→ refers to the object but does not release it
move
→ transfers ownership to another object or function
copy
→ creates a separate object or increments a reference count
For example, the following function allocates a new string and returns ownership to the caller.
char *duplicate_text(const char *source) {
size_t length = strlen(source) + 1;
char *copy = malloc(length);
if (copy == NULL) {
return NULL;
}
memcpy(copy, source, length);
return copy;
}
The caller must release the returned pointer after use.
char *text = duplicate_text("TechPedia");
if (text == NULL) {
return -1;
}
puts(text);
free(text);
In contrast, the pointer returned by the following function points to an object with static storage duration and must not be released by the caller.
const char *application_name(void) {
return "TechPedia";
}
An API must document the ownership and lifetime rules of its pointers.
Difference Between Scope and Lifetime
The end of an identifier’s scope does not necessarily mean that the object’s lifetime ends.
int *pointer;
{
static int value = 10;
pointer = &value;
}
printf("%d\n", *pointer);
The block scope of the name value has ended, but the object has static storage duration and therefore still exists.
Conversely, even when a pointer name remains grammatically visible, the lifetime of the object it referred to may have ended.
int *pointer;
{
int value = 10;
pointer = &value;
}
/* The lifetime of value has ended. */
The pointer variable itself is still alive, but the pointed-to object no longer exists.
Object Addresses
The address of an object can be obtained with the & operator.
int value = 10;
int *pointer = &value;
During an object’s lifetime, its address is abstractly treated as remaining constant. However, an implementation does not have to represent an address in the same way as a physical address or simple integer.
If a compiler determines that an object’s address is not observable by the program, it can store the object in a register or eliminate it.
int square(int value) {
int result = value * value;
return result;
}
Optimized code may contain no memory address at all for result. This is permitted because the observable result of the program is the same in the abstract machine.
sizeof and Bytes
sizeof returns the size of an object or type in C bytes.
size_t size = sizeof(int);
In C, sizeof(char) is always 1.
static_assert(sizeof(char) == 1);
However, one C byte is not necessarily eight bits. The number of bits contained in one byte is obtained from CHAR_BIT in <limits.h>.
#include <limits.h>
static_assert(CHAR_BIT >= 8);
The total number of bits in an object can conceptually be calculated as follows.
size_t bits = sizeof(object) * CHAR_BIT;
When an exact-width integer type is required, a type from <stdint.h> can be used.
uint32_t value;
uint32_t is defined only by implementations capable of providing an unsigned integer type with exactly 32 bits.
Object Representation and Value Representation
An object is represented by one or more bytes. The complete set of storage bytes of an object is called its object representation.
uint32_t value = UINT32_C(0x12345678);
In a common environment with eight-bit bytes, value may have a four-byte object representation.
12 34 56 78
Or it may be stored as follows.
78 56 34 12
The order used depends on the implementation and the byte order of the target system.
Not every bit in an object representation is necessarily used for the value itself. Some types may include the following.
- Value bits
- Sign bits
- Padding bits
- Multiple representations of the same value
- Representations that do not denote a value
The same value may also have more than one object representation. Therefore, it must not be assumed that bytewise comparison accurately represents value equality for every type.
Accessing Object Representations Through Character Types
C permits an object’s representation to be observed as though it were an array of character type.
uint32_t value = UINT32_C(0x12345678);
const unsigned char *bytes =
(const unsigned char *)&value;
for (size_t index = 0; index < sizeof(value); ++index) {
printf("%02X\n", bytes[index]);
}
An unsigned char pointer can be used to inspect or copy an object’s byte representation.
This facility is used in the following areas.
- Serialization tools
- Debuggers
- Hash calculation
- Memory copying
- Binary format analysis
- Network conversion
- Low-level runtimes
However, the ability to read an object representation does not mean that the byte array can be freely reinterpreted as any other type.
Byte Order
The order in which a multibyte integer is arranged in memory is called endianness.
For the value 0x12345678, representative forms are as follows.
big-endian
low address → 12 34 56 78 → high address
little-endian
low address → 78 56 34 12 → high address
The C standard does not require one particular byte order. Therefore, directly writing the memory bytes of an integer object to a file or network can produce different formats on different platforms.
fwrite(&value, sizeof(value), 1, file);
This code can be used to store temporary data within the same implementation environment, but it does not guarantee a portable file format.
A fixed binary format explicitly encodes the bytes.
void encode_u32_be(
unsigned char output[4],
uint32_t value
) {
output[0] = (unsigned char)(value >> 24);
output[1] = (unsigned char)(value >> 16);
output[2] = (unsigned char)(value >> 8);
output[3] = (unsigned char)value;
}
The value is also reconstructed explicitly when read.
uint32_t decode_u32_be(
const unsigned char input[4]
) {
return
((uint32_t)input[0] << 24) |
((uint32_t)input[1] << 16) |
((uint32_t)input[2] << 8) |
(uint32_t)input[3];
}
Structure Layout and Padding
Structure members are placed in declaration order, but padding bytes may be inserted between them to satisfy each member’s alignment requirement.
struct example {
char tag;
int value;
};
A common implementation may use a layout similar to the following.
offset 0 : tag
offset 1 : padding
offset 2 : padding
offset 3 : padding
offset 4 : value
offset 8 : end of structure
Therefore, the following relationship must not be assumed always to hold.
sizeof(struct example)
==
sizeof(char) + sizeof(int)
The actual offset of a member can be obtained with offsetof from <stddef.h>.
#include <stddef.h>
size_t value_offset =
offsetof(struct example, value);
Tail padding may also be inserted at the end of a structure to preserve the alignment of array elements.
struct example values[2];
For the second element to be properly aligned, sizeof(struct example) itself must be a multiple of the required alignment.
When values are stored in a structure or union, padding bytes can hold unspecified values. It must not be assumed that structure assignment always copies padding bytes identically.[39]
Bytewise Comparison of Structures
Even when all logical members of two structures are equal, memcmp is not guaranteed to return zero.
struct point {
char tag;
int x;
};
struct point first = {
.tag = 'A',
.x = 10
};
struct point second = {
.tag = 'A',
.x = 10
};
The following comparison can produce an incorrect result because of padding bytes.
bool equal =
memcmp(&first, &second, sizeof(first)) == 0;
The members must be compared individually.
bool point_equal(
const struct point *left,
const struct point *right
) {
return
left->tag == right->tag &&
left->x == right->x;
}
Conversely, when the object representation itself is part of the specification, as with a fixed byte array, memcmp may be appropriate.
struct identifier {
unsigned char bytes[16];
};
If this structure contains only a byte array and the format is defined that way, the arrays can be compared directly.
Alignment
Each object type has a memory alignment requirement that its address must satisfy.
alignof(int)
alignof(double)
alignof(struct point)
A type with an alignment requirement of 4 generally must be placed at an address that is a multiple of 4.
alignment 4
example permitted addresses: 0, 4, 8, 12, 16
A stronger alignment can be specified for an object.
alignas(64)
unsigned char cache_line[64];
When aligned dynamic memory is needed, aligned_alloc can be used.
size_t alignment = 64;
size_t size = 256;
void *memory = aligned_alloc(alignment, size);
The size passed to aligned_alloc must be an integer multiple of the alignment.
if (size % alignment != 0) {
return NULL;
}
Converting an unaligned address to a pointer of a particular type and dereferencing it can cause undefined behavior.
unsigned char buffer[sizeof(int) + 1];
int *pointer = (int *)(buffer + 1);
*pointer = 10;
There is no guarantee that buffer + 1 satisfies the alignment required by int.
A value can be read from external bytes using memcpy.
int value;
memcpy(&value, buffer + 1, sizeof(value));
However, it must also be separately guaranteed that the bytes form a valid object representation of the type.
Storage of a union
All members of a union share the same storage.
union number {
uint32_t integer;
float floating;
};
A union must be large enough to store every member and satisfy the alignment of the strictest member.
union number value;
value.integer = UINT32_C(0x3F800000);
Reading the other member, floating, afterward involves type-reinterpretation practices and standard rules in C, but the portability and representation of the result are affected by the implementation.
float result = value.floating;
Code that assumes IEEE 754 and a particular byte order does not guarantee the same result in another environment.
Copying the bit pattern through bytes can express the intention more clearly.
float bits_to_float(uint32_t bits) {
float result;
static_assert(
sizeof(result) == sizeof(bits)
);
memcpy(&result, &bits, sizeof(result));
return result;
}
However, whether the copied bit pattern is a valid representation for the destination floating-point type and what value it denotes still depend on that implementation’s floating-point format.
Effective Type
An object created by a declaration has the declared type.
int value;
The effective type of value is int.
For an object without a declared type, such as dynamically allocated storage, the effective type can be determined by the manner in which values are stored or copied.
void *memory = malloc(sizeof(int));
int *integer = memory;
*integer = 10;
Because a value was stored through an lvalue of type int, the stored value can later be accessed as int.
Arbitrarily reading the same bytes through a different type may not be permitted.
float *floating = memory;
float result = *floating;
Accessing an object previously stored as int through an incompatible float lvalue can violate the effective type and aliasing rules.
Strict Aliasing Rule
A C compiler can generally infer that pointers of incompatible types do not point to the same object. This is commonly called the strict aliasing rule.
int update(int *integer, float *floating) {
*integer = 10;
*floating = 1.0F;
return *integer;
}
The compiler can assume that integer and floating do not point to the same object. Forcing one storage region to be addressed through both pointer types can violate the type access rules.
In general, a stored value of an object can be accessed through an lvalue of the following types.
- A type compatible with the object’s effective type
- The corresponding signed or unsigned type
- A qualified version of a compatible type
- Certain structures or unions containing one of the above types as a member
- A character type
Character-type access is a special exception for reading object representations.
const unsigned char *bytes =
(const unsigned char *)&value;
This exception does not mean that an unsigned char array can immediately be dereferenced as an arbitrary object of another type.
memcpy and Type Preservation
memcpy copies an object representation byte by byte.
struct point source = {
.x = 10,
.y = 20
};
struct point destination;
memcpy(
&destination,
&source,
sizeof(destination)
);
When copying between objects of the same structure type, destination can represent the same values as source.
When an object representation is copied into dynamically allocated storage without a declared type, the effective type of the source object can affect subsequent accesses.
struct point source = {
.x = 10,
.y = 20
};
void *memory = malloc(sizeof(source));
if (memory == NULL) {
return NULL;
}
memcpy(memory, &source, sizeof(source));
struct point *copy = memory;
The C standard specifies that when a value is copied into an object without a declared type using memcpy or memmove, the effective type of the source object is used for subsequent access.[40]
memcpy and Overlapping Regions
If the source and destination regions passed to memcpy overlap, undefined behavior occurs.
char text[] = "abcdef";
memcpy(text + 1, text, 5);
When overlap is possible, memmove is used.
memmove(text + 1, text, 5);
memmove behaves conceptually as though the source were first copied into a temporary array and then moved to the destination.
Pointers
A pointer represents a value referring to an object or function.
int value = 10;
int *pointer = &value;
A pointer value is not guaranteed to be identical to a simple integer address. An implementation may include the following additional information in a pointer.
- Address-space information
- Permissions
- Bounds
- Segments
- Object origin
- Hardware tags
- Distinction between function and object pointers
Therefore, it cannot be assumed that storing a pointer in a sufficiently large integer and converting it back always preserves every aspect of the original pointer. uintptr_t and intptr_t from <stdint.h> are defined only when the implementation can provide suitable integer types.
uintptr_t representation =
(uintptr_t)pointer;
int *restored =
(int *)representation;
The detailed meaning of such conversions must be checked against implementation and standard rules.
Null Pointers
A null pointer is a special pointer value that points to no valid object or function.
int *pointer = NULL;
C23 allows nullptr to be used.
int *pointer = nullptr;
The standard does not generally require a null pointer to have all bits zero.
int *pointer;
memset(&pointer, 0, sizeof(pointer));
It must not be generalized that this code produces a correct null pointer in every C implementation. A language-level null pointer constant must be used.
int *pointer = NULL;
A pointer object with static storage duration is initialized to a null pointer according to the language rules.
static int *pointer;
Dereferencing a null pointer has undefined behavior.
*pointer = 10;
Pointer Arithmetic
Pointer arithmetic is defined relative to the same array object.
int values[4] = {
10,
20,
30,
40
};
int *pointer = values;
The following operation makes the pointer refer to the second element.
pointer += 1;
Adding an integer n to a pointer moves it by n elements, not by n bytes.
int * + 1
→ moves by sizeof(int)
double * + 1
→ moves by sizeof(double)
A pointer to the position immediately after the end of an array can be used for calculation and comparison.
int *end = values + 4;
However, it cannot be dereferenced.
int value = *end;
This is an out-of-bounds array access.
A pointer calculation that produces a position before the beginning or beyond one past the end is not permitted.
int *invalid = values + 5;
Subtracting or relationally comparing pointers to different array objects is also generally undefined.
int first[4];
int second[4];
ptrdiff_t distance = &first[0] - &second[0];
The difference between pointers within the same array is measured in elements.
ptrdiff_t distance =
&values[3] - &values[0];
The result is 3.
Array Objects and Single Objects
For pointer arithmetic rules, a single non-array object is in some contexts treated as though it were an array of length 1.
int value = 10;
int *pointer = &value;
int *end = pointer + 1;
end can be used as a calculated pointer representing the position immediately after value, but it cannot be dereferenced.
Pointer Comparison
Pointers referring to the same object can be compared for equality using ==.
if (left == right) {
/* Represents the same address. */
}
They can also be compared with a null pointer.
if (pointer == NULL) {
return -1;
}
Relational pointer comparisons using <, >, <=, and >= have defined meaning between elements of the same array object.
if (current < end) {
/* Order comparison within the same array. */
}
The order of pointers to unrelated objects cannot be used as a portable address-sorting criterion.
Pointer Provenance
Modern compilers reason about pointers not only as numerical addresses, but also in terms of the objects from which they originated. This concept is called pointer provenance.
int first;
int second;
int *pointer = &first;
pointer can be interpreted not merely as a value with a particular numerical address, but as a pointer created through an access path to the object first.
Even if integer arithmetic happens to produce the same numerical address as second, it cannot be assumed that the value automatically becomes a valid pointer capable of accessing second.
Pointer provenance, integer conversions, reuse of the same address after deallocation, and device-memory access have long been areas of interpretation and improvement within WG14. Because the C object model exposes actual byte representations while compiler optimization uses information about object origin and lifetime, a simple “numerical address” model is insufficient to explain every program behavior.[41]
Portable code should follow the following principles.
- Use pointers obtained from valid objects or allocations.
- Do not manipulate pointers using arbitrary integer arithmetic.
- Do not use a pointer after the lifetime of its object has ended.
- Do not perform arithmetic outside the bounds of the same array.
- Follow implementation-documented interfaces for device memory.
- Check the platform ABI when pointer-to-integer conversion is required.
Function Pointers and Object Pointers
Function pointers and object pointers are different kinds of pointers.
int *object_pointer;
int (*function_pointer)(void);
void * can be used as a general-purpose object pointer, but ISO C does not generally guarantee conversions between function pointers and void *.
void *memory;
int (*function)(void);
POSIX and particular operating system APIs may provide separate rules for dynamic symbol lookup, but those are part of platform conventions rather than guarantees of ISO C alone.
Pointer Lifetime and Indeterminate Representations
When an object’s lifetime ends, not only can a pointer that referred to that object no longer be dereferenced, but under C23 rules the evaluation of the pointer value itself can become problematic.
int *pointer;
{
int value = 10;
pointer = &value;
}
/* The lifetime of value has ended. */
The following code is also not guaranteed to be safe pointer use afterward.
if (pointer != NULL) {
/* ... */
}
It may appear to compare the pointer only with null, but the pointer representation can become indeterminate after the lifetime of the pointed-to object ends. Program structure should ensure that the pointer is no longer used before the object’s lifetime ends.
The same applies to dynamic allocation.
free(pointer);
pointer = NULL;
The practice of immediately storing a null pointer into the same pointer object after free is commonly used. However, program logic must not be based on reading or comparing aliases that still hold values pointing to the released object.
Indeterminate Values
An automatic object that is not explicitly initialized can have an indeterminate value.
void function(void) {
int value;
printf("%d\n", value);
}
Reading an uninitialized value can cause undefined behavior depending on the type and circumstances.
Pointers must be initialized before use.
int *pointer = NULL;
Initializing an entire structure reduces omissions among its members.
struct state {
int count;
bool ready;
void *context;
};
struct state state = { 0 };
C23 also permits an empty initializer list.
struct state state = {};
Non-Value Representations
For some types, not every possible bit combination represents a valid value. An object representation that does not denote a value is called a non-value representation.
Reading such a representation through an lvalue of the type, other than through a character type, can cause undefined behavior.
In common modern two’s-complement integers, every bit pattern often represents an integer value, but non-value representations may exist for other types and special hardware.
Therefore, copying arbitrary bytes into an object of a particular type and immediately reading it may not be portable.
unsigned char bytes[sizeof(double)] = {
/* External data */
};
double value;
memcpy(&value, bytes, sizeof(value));
value can be safely used only when bytes form a valid double object representation for the implementation.
Memory Initialization and memset
memset stores the same byte value repeatedly.
memset(buffer, 0, size);
It is suitable for filling character arrays and raw buffers with zero bytes.
unsigned char buffer[1024];
memset(buffer, 0, sizeof(buffer));
It is also common to fill an entire structure with zero bytes.
struct state state;
memset(&state, 0, sizeof(state));
However, it must not be generalized that all-bits-zero is the same representation as a language-level zero value or null pointer for every type. Using initialization syntax is clearer for ordinary objects.
struct state state = { 0 };
A dynamically allocated structure can also receive a compound initial value.
struct state *state = malloc(sizeof(*state));
if (state == NULL) {
return NULL;
}
*state = (struct state) {
.count = 0,
.ready = false,
.context = NULL
};
Flexible Array Members
The last member of a structure can be an array whose size is omitted.
struct packet {
size_t size;
unsigned char data[];
};
This array is called a flexible array member.
The structure and its data region can be allocated together.
struct packet *packet_create(size_t size) {
if (
size >
SIZE_MAX - sizeof(struct packet)
) {
return NULL;
}
struct packet *packet = malloc(
sizeof(*packet) + size
);
if (packet == NULL) {
return NULL;
}
packet->size = size;
return packet;
}
The data uses the allocated region immediately following the structure.
packet->data[0] = 0x01;
Accessing an index larger than the additionally allocated region is out of bounds.
A flexible array member must be the last member of the structure, and the structure must contain another named member.
Variable-Length Arrays
A C99 variable-length array is an automatic array whose size is determined at runtime.
void process(size_t count) {
int values[count];
/* ... */
}
The storage of the variable-length array is associated with execution of the corresponding block. Using a very large size can exhaust the automatic storage available in the implementation environment.
void unsafe(size_t count) {
unsigned char buffer[count];
}
Using unvalidated external input as the size of a variable-length array can lead to stack exhaustion or denial-of-service problems.
Dynamic allocation can be more appropriate for large arrays or storage for which failure must be handled.
unsigned char *buffer = malloc(count);
if (buffer == NULL) {
return -1;
}
Variable-length arrays may be optionally supported depending on the standard edition and implementation, so projects requiring portability must check their availability.
Stack and Heap
The C standard does not directly define memory regions called the stack and heap. However, common implementations frequently use the following correspondences.
automatic storage duration objects
→ call stack or registers
static storage duration objects
→ executable data or BSS sections
allocated storage duration objects
→ heap or custom allocator region
string literals
→ possibly read-only data area
function code
→ executable code area
Automatic objects are not required to reside on a stack, and dynamically allocated objects are not required to reside in one global heap.
An embedded implementation may use a static memory pool, a game engine may use a frame allocator or arena, and an operating system kernel may use its own page or slab allocator.
struct arena {
unsigned char *memory;
size_t capacity;
size_t offset;
};
void *arena_allocate(
struct arena *arena,
size_t size,
size_t alignment
) {
uintptr_t current =
(uintptr_t)(arena->memory + arena->offset);
uintptr_t aligned =
(current + alignment - 1) &
~(uintptr_t)(alignment - 1);
size_t next =
(size_t)(aligned - (uintptr_t)arena->memory);
if (
next > arena->capacity ||
size > arena->capacity - next
) {
return NULL;
}
arena->offset = next + size;
return arena->memory + next;
}
Such a custom allocator must correctly handle the platform’s pointer representation, alignment requirements, and overflow. The example above assumes that alignment is a power of two and that pointer-to-integer conversions are supported, so it is not a completely general ISO C implementation.
Memory-Mapped Input and Output
In embedded systems and device drivers, a particular address may correspond to a hardware register.
#define STATUS_REGISTER_ADDRESS \
UINTPTR_C(0x40000000)
volatile uint32_t *status_register =
(volatile uint32_t *)
STATUS_REGISTER_ADDRESS;
uint32_t status = *status_register;
The meaning of this code is not complete under ISO C alone. The platform must guarantee the following conditions.
- Conversion from an integer address to a pointer
- Validity of the address
- Width and alignment of the register
- Number and order of accesses
- Byte order
- Cache policy
- Side effects of the bus and device
volatile is used to limit elimination or arbitrary merging of accesses, but it does not automatically provide CPU memory barriers or inter-thread atomicity.
volatile and the Memory Model
A volatile object indicates that its value may be changed in a manner unknown to the implementation or that the access itself may produce external side effects.
volatile sig_atomic_t signal_received;
void signal_handler(int signal) {
signal_received = signal;
}
It can also denote a memory-mapped register.
volatile uint32_t *device_status;
However, merely declaring a shared counter as volatile does not make it thread-safe.
volatile int counter = 0;
If two threads perform the following operation concurrently, a data race can occur.
++counter;
An increment can conceptually consist of multiple steps of reading, calculating, and writing and is not guaranteed to be atomic.
Atomic types or mutexes must be used for thread synchronization.
C’s Concurrent Execution Model
Since C11, the standard has defined memory access and atomic operations involving multiple execution threads.
Important concepts include the following.
- Evaluation
- Value computation
- Side effects
- sequenced before
- synchronizes with
- happens before
- Atomic operations
- modification order
- Data races
A sequenced before relationship can exist between evaluations within one thread. Synchronization operations between threads can establish a synchronizes with relationship, from which a happens before relationship is constructed.
write in thread A
↓ sequenced before
release store
↓ synchronizes with
acquire load
↓ sequenced before
read in thread B
When this relationship is established, thread B can correctly observe writes performed by thread A before the synchronization operation.
Data Races
A data race occurs when two threads perform conflicting accesses to the same memory location, at least one of those accesses is a write, no appropriate happens-before relationship exists, and the accesses involved are not atomic.
static int counter = 0;
int worker(void *context) {
++counter;
return 0;
}
If multiple threads execute worker concurrently, accesses to counter can have a data race.
In C, a data race causes undefined behavior.[42]
It must not be assumed that the result is limited merely to losing some increments. The compiler can optimize under the assumption that a correct program contains no data races.
Atomic Types
An atomic object provides indivisible reads and writes.
#include <stdatomic.h>
static atomic_int counter = 0;
Multiple threads can increment the value atomically.
void increment(void) {
atomic_fetch_add(&counter, 1);
}
The _Atomic specifier can also be used directly.
static _Atomic int counter = 0;
Ordinary assignment and reading of an atomic object have sequentially consistent meaning by default.
counter = 10;
int value = counter;
However, explicit atomic functions express the intention and memory order more clearly.
atomic_store(&counter, 10);
int value = atomic_load(&counter);
The size, representation, and alignment of an atomic type can differ from those of the corresponding non-atomic type.
static_assert(
sizeof(_Atomic int) == sizeof(int)
);
This relationship is not guaranteed in every implementation and must not be relied upon.
Atomic Operations
Representative atomic operations include the following.
atomic_load(&object);
atomic_store(&object, value);
atomic_exchange(&object, value);
atomic_fetch_add(&object, value);
atomic_fetch_sub(&object, value);
atomic_fetch_or(&object, value);
atomic_fetch_and(&object, value);
atomic_compare_exchange_strong(
&object,
&expected,
desired
);
An atomic compare-and-exchange stores a new value when the current value equals the expected value.
bool set_if_zero(atomic_int *value) {
int expected = 0;
return atomic_compare_exchange_strong(
value,
&expected,
1
);
}
If the comparison fails, the actual value may be written into expected.
Merely using atomic operations does not automatically make an entire complex data structure safe. Invariants between multiple atomic objects, object lifetimes, and memory reclamation must also be handled.
Memory Order
Atomic operations can specify the following memory orders.
memory_order_relaxedmemory_order_consumememory_order_acquirememory_order_releasememory_order_acq_relmemory_order_seq_cst
The simplest default is sequential consistency.
atomic_store(&ready, true);
Written explicitly, it is as follows.
atomic_store_explicit(
&ready,
true,
memory_order_seq_cst
);
memory_order_relaxed guarantees atomicity of the particular object but does not establish a synchronization order for other memory accesses.
atomic_fetch_add_explicit(
&counter,
1,
memory_order_relaxed
);
It can be used for independent statistical counters where only atomicity of the value is required.
Release and acquire operations are used to transfer writes and reads involving other objects.
struct message {
int value;
};
static struct message shared_message;
static atomic_bool ready = false;
Writer thread:
void publish_message(void) {
shared_message.value = 42;
atomic_store_explicit(
&ready,
true,
memory_order_release
);
}
Reader thread:
bool consume_message(int *result) {
if (!atomic_load_explicit(
&ready,
memory_order_acquire
)) {
return false;
}
*result = shared_message.value;
return true;
}
When the acquire load observes the value of the release store, a happens-before relationship is established so that the earlier write to shared_message.value becomes correctly visible to the reader thread.
Choosing an incorrect memory order can cause code to work on one processor but fail on another architecture or optimization level. Unless the required reason can be proven, using default sequential consistency or a mutex is safer.
Whether Atomic Operations Are Lock-Free
Not every atomic type is implemented lock-free using a single hardware instruction.
atomic_int value;
bool lock_free = atomic_is_lock_free(&value);
An implementation may use an internal lock or runtime function to provide atomicity.
Some fundamental atomic types have macros indicating whether they are lock-free.
ATOMIC_INT_LOCK_FREE
ATOMIC_POINTER_LOCK_FREE
Their values indicate implementation states such as always lock-free, sometimes lock-free, or never lock-free.
In signal handlers or highly restricted real-time contexts, safety must not be assumed merely because a type is atomic. Lock-free guarantees and the signal safety of functions must be checked separately.
Mutexes and Condition Variables
A mutex from <threads.h> can protect compound state.
#include <threads.h>
struct counter {
mtx_t mutex;
int value;
};
bool counter_increment(struct counter *counter) {
if (mtx_lock(&counter->mutex) != thrd_success) {
return false;
}
++counter->value;
mtx_unlock(&counter->mutex);
return true;
}
Locking and unlocking the same mutex establishes synchronization relationships between threads. A protected non-atomic object must be accessed only while the mutex is held.
A condition variable is used to wait until a particular state is reached.
struct queue {
mtx_t mutex;
cnd_t changed;
size_t count;
};
A condition-variable wait is used in a loop that rechecks the condition after waking.
mtx_lock(&queue->mutex);
while (queue->count == 0) {
cnd_wait(
&queue->changed,
&queue->mutex
);
}
/* Use a queue element. */
mtx_unlock(&queue->mutex);
The condition is tested with while rather than if to handle spurious wakeups and situations in which another thread changes the state first.
Evaluation Order and Memory
Even within one thread, incorrectly assuming expression evaluation order can cause problems.
int result = function(
first(),
second()
);
It is generally not guaranteed whether first or second executes first.
Modifying the same object multiple times without a sequencing relationship can cause undefined behavior.
int value = 0;
value = value++;
The following code is also problematic.
values[index++] = index;
The intended order must be expressed in separate statements.
values[index] = index;
++index;
It must not be assumed that a compiler translates statements unconditionally from top to bottom into machine instructions. Instructions can be reordered as long as the observable results and ordering relationships guaranteed by the standard are preserved.
Compiler Optimization and Memory Access
Consider the following loop.
while (!ready) {
}
If ready is an ordinary non-atomic object and another thread is expected to modify it, the program contains a data race.
The compiler may determine that there is no code in the current thread that changes ready and read the value only once or optimize the loop into an infinite loop.
Adding volatile alone does not provide thread synchronization.
volatile bool ready;
An atomic type must be used for an inter-thread flag.
atomic_bool ready = false;
while (!atomic_load(&ready)) {
}
In an actual program, a condition variable or platform wait facility can be more resource-efficient than busy waiting.
Undefined Behavior and Memory
Memory-related undefined behavior in C includes the following.
- Dereferencing a null pointer
- Accessing outside array bounds
- Accessing an object after its lifetime has ended
- Use after free
- Double free
- Dereferencing an incorrectly aligned pointer
- Accessing through an incompatible type
- Reading an uninitialized value
- Using
memcpywith overlapping regions - Invalid pointer arithmetic
- Signed integer overflow during size calculation
- Data races
- Violating a
restrictcontract - Using the wrong variadic argument type
- Modifying a read-only string literal
When undefined behavior occurs, the C standard places no restriction on the result.
int values[4];
values[4] = 10;
The program may terminate immediately, overwrite another object, or appear to behave normally. In an optimized build, related conditions and code may be removed.
Buffer Overflow
C arrays do not have built-in bounds checking.
void copy_text(
char *destination,
const char *source
) {
while ((*destination++ = *source++) != '\0') {
}
}
If the destination buffer is not large enough, the function writes beyond its bounds.
The size must be passed explicitly.
bool copy_text(
char *destination,
size_t capacity,
const char *source
) {
size_t length = strlen(source);
if (length >= capacity) {
return false;
}
memcpy(
destination,
source,
length + 1
);
return true;
}
Both the length and terminating null character must be considered.
required size
= number of string characters + one null terminator
The unit of a size parameter must also be documented.
whether it is a byte count
whether it is an element count
whether it includes the null character
whether it is the maximum writable length
Integer Overflow and Memory Safety
If integer overflow occurs while calculating a memory size, a small buffer may be allocated and then filled with a larger amount of data.
size_t size = count * element_size;
void *memory = malloc(size);
The multiplication must be checked first.
if (
element_size != 0 &&
count > SIZE_MAX / element_size
) {
return NULL;
}
size_t size = count * element_size;
Addition must also be checked.
if (
payload_size >
SIZE_MAX - sizeof(struct packet)
) {
return NULL;
}
Lengths received from external files or network packets must not be trusted without validation.
Double Free
Releasing the same allocation twice causes undefined behavior.
free(pointer);
free(pointer);
Ownership should be assigned to one code path so that release responsibility does not overlap.
void destroy_resource(
struct resource **resource_pointer
) {
if (
resource_pointer == NULL ||
*resource_pointer == NULL
) {
return;
}
struct resource *resource =
*resource_pointer;
free(resource->data);
free(resource);
*resource_pointer = NULL;
}
This approach still does not handle other aliases pointing to the same object. The fundamental solution is to manage ownership and lifetime structurally.
Memory Leaks
A memory leak occurs when access to allocated storage is lost without releasing it.
void leak(void) {
void *memory = malloc(1024);
if (memory == NULL) {
return;
}
/* Function ends without free. */
}
If the function is called repeatedly, the process’s memory usage grows.
Every resource must also be cleaned up on error paths.
int load_resource(const char *path) {
int result = -1;
FILE *file = NULL;
void *buffer = NULL;
file = fopen(path, "rb");
if (file == NULL) {
goto cleanup;
}
buffer = malloc(4096);
if (buffer == NULL) {
goto cleanup;
}
result = 0;
cleanup:
free(buffer);
if (file != NULL) {
fclose(file);
}
return result;
}
Memory Fragmentation
Repeated dynamic allocation and release can make it difficult to obtain a large contiguous block even when the total available space is sufficient. This is called memory fragmentation.
- External fragmentation occurs when free space is divided into many small regions.
- Internal fragmentation occurs when alignment and allocation units consume more space than requested.
ISO C does not specify an allocator’s internal strategy. Long-running servers, game engines, and embedded systems can use the following techniques according to their usage patterns.
- Memory pools
- Arena allocators
- Slab allocators
- Fixed-size blocks
- Frame-based bulk release
- Generational storage
- Per-object dedicated allocators
Arena Allocation
An arena allocates multiple objects sequentially from one large memory block and releases them all at once.
struct arena {
unsigned char *data;
size_t capacity;
size_t used;
};
A simple byte allocation can be implemented as follows.
void *arena_allocate_bytes(
struct arena *arena,
size_t size
) {
if (size > arena->capacity - arena->used) {
return NULL;
}
void *result =
arena->data + arena->used;
arena->used += size;
return result;
}
An allocator for actual objects must also handle alignment.
Advantages of an arena include the following.
- Allocation cost is small.
- Multiple objects can be released together.
- Fragmentation can be reduced.
- It is suitable for data with clear lifetime relationships.
It also has limitations.
- Releasing only one individual object is difficult.
- A pointer must not outlive the arena.
- Alignment and size overflow must be handled directly.
- The total maximum capacity must be planned.
Memory Pools
When fixed-size objects are repeatedly created and destroyed, a memory pool can be used.
struct node {
struct node *next;
int value;
};
struct node_pool {
struct node *free_list;
};
Released nodes can be placed in a free list and reused.
void node_release(
struct node_pool *pool,
struct node *node
) {
node->next = pool->free_list;
pool->free_list = node;
}
This approach can reduce the number of direct allocator calls and limit fragmentation for fixed-size objects.
However, if an external pointer continues referring to a released node, a severe lifetime error can occur after the same storage is reused for a new object.
Object Lifetime and Reuse
Even if a new object is created at the same address, it is not necessarily the same object as the previous one.
int *first = malloc(sizeof(*first));
if (first == NULL) {
return;
}
*first = 10;
free(first);
int *second = malloc(sizeof(*second));
Even if second happens to receive the same numerical address, the lifetime of the previous first object has already ended. The new object must not be accessed through first.
if (first == second) {
/* Even if the numerical representations appear equal,
first is not used. */
}
Address reuse and object identity are different concepts.
Strings and Memory
A C string is an array of characters terminated by a null character.
char text[] = "hello";
It is stored in memory as follows.
'h' 'e' 'l' 'l' 'o' '\0'
String functions read memory until they find a null character.
size_t length = strlen(text);
Passing an array without a null character to a string function can cause it to read beyond the array bounds.
char bytes[4] = {
't',
'e',
's',
't'
};
strlen(bytes);
bytes is not a C string.
Buffer capacity and string length must be distinguished.
char text[16] = "hello";
size_t capacity = sizeof(text);
size_t length = strlen(text);
capacity → 16 bytes
length → 5 characters
Lifetime of String Literals
A string literal creates an array object with static storage duration.
const char *message = "hello";
Even if message is a local pointer variable, the array created by the string literal exists throughout the execution of the program.
const char *get_message(void) {
return "hello";
}
This returned pointer differs from returning the address of an automatic local array.
Attempting to modify the contents of a string literal causes undefined behavior.
char *message = "hello";
message[0] = 'H';
When a modifiable array is required, a separate array is initialized.
char message[] = "hello";
message[0] = 'H';
Memory Safety Tools
Because the C language itself does not automatically check every memory error, development tools are used alongside it.
Representative methods include the following.
- Compiler warnings
- Static analysis
- AddressSanitizer
- UndefinedBehaviorSanitizer
- MemorySanitizer
- ThreadSanitizer
- Valgrind-family tools
- Fuzzing
- Unit testing
- Boundary-value testing
- Code review
For example, Clang- and GCC-family compilers can enable sanitizers in development builds.
-fsanitize=address,undefined
-fno-omit-frame-pointer
Thread data-race checking can be performed in a separate build.
-fsanitize=thread
AddressSanitizer and ThreadSanitizer generally cannot be used together in the same executable, so separate checking configurations are used.
Tools do not prove that every error is absent, but they are highly useful for detecting use after free, out-of-bounds access, some undefined behavior, and data races.
Principles of Safe Memory Management
Fundamental principles for improving memory safety in C programs include the following.
- Initialize every object before use.
- Pass length information together with arrays and pointers.
- Check addition and multiplication for size overflow.
- Do not create pointers that outlive the objects they refer to.
- Clearly establish allocation ownership and release responsibility.
- Do not use any aliases after an object is released.
- Do not depend on structure padding or byte order.
- Explicitly serialize portable data formats.
- Avoid arbitrary reinterpretation through pointer types.
- Observe the difference between
memcpyandmemmove. - Use atomic operations or locks for shared state in multithreaded programs.
- Do not use
volatileas a synchronization mechanism. - Apply memory-order optimizations only after correctness has been proven.
- Use warnings and static and dynamic analysis tools.
Summary of the Memory Model
The C memory model does not treat hardware memory merely as an array of addressed bytes. A program creates objects with types and lifetimes and accesses those objects and their subobjects through pointers. An object has alignment, storage duration, an object representation, and a value representation, while the type used for access and the origin of a pointer also affect program meaning.
storage
↓
object creation and lifetime
↓
type and alignment
↓
value and object representation
↓
pointers and access paths
↓
evaluation order and synchronization
C’s ability to expose object representations at character granularity and permit pointer arithmetic and direct dynamic allocation provides the strong control required for system programming. At the same time, programmers must accurately manage array bounds, object lifetimes, effective types, alignment, and memory release.
The concurrent memory model introduced in C11 defines the meaning of memory accesses by multiple threads through atomic operations and happens-before relationships. A data race without proper synchronization is not merely a condition in which values become inaccurate, but causes undefined behavior for the entire program.
Understanding the C memory model requires more than knowing the implementation-level distinction between stacks and heaps. It is necessary to consider when an object exists, through which types it may be accessed, from which array or object a pointer originated, whether stored bytes form a valid value representation, and which ordering relationships exist between different executions.
Execution Process
The execution process of a C program may appear to be a simple task in which an executable file is created with a single command after writing source code, but internally it consists of several connected stages. C source files are preprocessed, their syntax and semantics are analyzed, and they are transformed into machine instructions or an intermediate representation for the target system. Object files are then created and combined with the required libraries to construct a program image. At execution time, an operating system or freestanding environment places the program in memory and calls its designated startup function.
In a typical hosted environment, the overall flow can be represented as follows.
C source files
↓
preprocessing
↓
syntax and semantic analysis
↓
intermediate representation generation and optimization
↓
target machine-code generation
↓
assembly
↓
object files
↓
linking
↓
executable file or shared library
↓
operating system loader
↓
dynamic linker and runtime initialization
↓
main call
↓
program execution
↓
termination processing
Not every stage in this flow is specified by the ISO C standard using the same names and file formats. ISO C specifies the conceptual translation phases through which source characters are transformed into a program image, along with the meaning of program startup, execution, and termination. Preprocessor executables, assembly files, ELF, PE, and Mach-O object files, static and dynamic linkers, and operating system loaders are systems provided by concrete implementations and platforms.
GCC describes actual compilation as four stages: preprocessing, compilation proper, assembly, and linking. Clang likewise coordinates preprocessing, parsing, optimization, code generation, assembly, and linking through a single driver.[43][44]
Translation and Execution Environments
The C standard broadly divides an implementation environment into a translation environment and an execution environment.
translation environment
→ transforms C source files into a program image
execution environment
→ starts and runs the translated program
The translation environment can be responsible for the following tasks.
- Processing source characters
- Executing preprocessing directives
- Expanding macros
- Analyzing syntax and semantics
- Producing diagnostics
- Translating translation units
- Resolving external references
- Combining libraries
- Generating a program image required for execution
The execution environment can be responsible for the following tasks.
- Loading the program image
- Initializing objects with static storage duration
- Calling the execution startup function
- Providing the program’s input and output environment
- Handling program termination status
- Returning control to the execution environment
The translation and execution environments do not have to exist on the same computer. A program can be translated on a development computer for another processor or operating system and then executed on the target device.
development host
x86-64 Linux
↓ cross-compilation
ARM embedded executable
↓ transfer
target device
ARM microcontroller
This method is called cross-compilation.
Programs and Translation Units
An entire C program does not have to be translated at once. A program can be divided into multiple source files, and each source file together with the headers it includes becomes an independent translation unit.
main.c
renderer.c
network.c
storage.c
Each file can be translated separately.
main.c → main.o
renderer.c → renderer.o
network.c → network.o
storage.c → storage.o
The linker then combines the object files into one program.
main.o
renderer.o
network.o
storage.o
libc
system libraries
↓
executable program
ISO C distinguishes the source file after #include processing as a preprocessing translation unit, and the result after preprocessing as a translation unit. Multiple translation units can communicate through functions and objects with external linkage, files, and library interfaces.[45]
ISO C Translation Phases
ISO C divides translation into eight phases. An actual compiler is not required internally to execute eight independent programs. Multiple phases can be combined into a single process, but the final result must have the same meaning as though the standard-defined phases occurred in order.[46]
1. source character conversion
2. line splicing
3. preprocessing token decomposition and comment removal
4. preprocessing directive and macro processing
5. conversion of characters and strings to the execution character set
6. adjacent string-literal concatenation
7. syntax and semantic analysis and translation
8. external-reference resolution and program-image generation
Phase 1: Physical Source Character Processing
In the first phase, characters in the physical source file are mapped to the C source character set used by the implementation. This is necessary because the character encoding used in the file and the compiler’s internal character representation may differ.
UTF-8 source file
↓
compiler source-character interpretation
↓
C source character set
Line-ending representations in the source file are also processed as logical newline characters.
Depending on the operating system, text-file line endings can differ as follows.
UNIX-like systems → LF
Windows → CRLF
classic Mac → CR
The compiler interprets them as C source-line boundaries.
The encoding of a source file is not fixed solely by the C standard. Compiler options, the build environment, and source-control policies may determine the actual encoding.
Phase 2: Backslash Line Splicing
When a backslash is immediately followed by a newline, two physical lines are joined into one logical line.
#define LOG_MESSAGE(message) \
fprintf(stderr, "%s\n", message)
Conceptually, this produces the same preprocessing-token stream as the following single line.
#define LOG_MESSAGE(message) fprintf(stderr, "%s\n", message)
Line splicing is processed before string-literal tokens are recognized, including inside string literals.
const char *message = "hello \
world";
The newline itself is not included in the string; rather, the physical lines are joined.
When an actual string is to be divided across multiple lines, adjacent string literals are clearer.
const char *message =
"hello "
"world";
If whitespace appears after the backslash, line splicing may not occur, so care is required when writing macros.
#define VALUE 10 \
+ 20
The whitespace between the backslash and newline can prevent the intended macro from being formed.
Phase 3: Preprocessing Token Decomposition and Comment Removal
The source file is divided into preprocessing tokens and whitespace characters. Comments are replaced with a single space character.
int/* comment */value = 10;
After comment removal, the result is conceptually as follows.
int value = 10;
Comments are replaced with whitespace rather than simply removed without leaving any character, so separate tokens do not accidentally merge.
int/**/value;
This is processed as int value, not intvalue.
Comment markers inside string literals are not interpreted as comments.
const char *text = "/* not a comment */";
At this stage, the source is divided into preprocessing tokens such as the following.
- Header names
- Identifiers
- Preprocessing numbers
- Character constants
- String literals
- Punctuators
- Other non-whitespace characters
Phase 4: Execution of Preprocessing Directives
The preprocessor processes #include, #define, conditional compilation, and other directives.
#include <stdio.h>
#define BUFFER_SIZE 4096
#if defined(DEBUG)
#define LOG_ENABLED 1
#else
#define LOG_ENABLED 0
#endif
The following tasks are performed during this phase.
- Searching for included files
- Inserting header contents
- Expanding object-like macros
- Expanding function-like macros
- Selecting conditionally compiled code
- Processing
#embed - Processing
_Pragma - Producing preprocessing diagnostics
- Removing processed preprocessing directives
Consider the following source.
#define SQUARE(value) ((value) * (value))
int result = SQUARE(4);
After macro expansion, the central code is as follows.
int result = ((4) * (4));
#include causes the specified header to be processed again from the initial translation phases.
#include "project.h"
Character processing, line splicing, token decomposition, and preprocessing directives within the header file are handled in the same way.
With GCC and Clang, the -E option can be used to inspect the result after preprocessing only.
clang -E main.c
gcc -E main.c
GCC’s official documentation explains that -E performs preprocessing only and does not continue to later compilation stages.[47]
The preprocessing result can be much longer than the original source because it can contain the complete contents of included standard headers and the results of macro expansion.
Limitations of the Preprocessor
The preprocessor is not a general-purpose function system that understands C types and object semantics. It is a token-substitution stage.
#define SQUARE(value) \
((value) * (value))
The following invocation evaluates its argument twice.
int result = SQUARE(index++);
After expansion, it becomes similar to the following.
int result =
((index++) * (index++));
This can modify the same object multiple times without sequencing and cause undefined behavior.
When type checking and single evaluation are required, an inline function can be used.
static inline int square(int value) {
return value * value;
}
Phase 5: Execution Character-Set Conversion
Source characters and escape sequences within character constants and string literals are converted to corresponding characters in the character set used by the execution environment.
const char *message = "hello";
char newline = '\n';
Even if the source file is UTF-8, ISO C alone does not guarantee that every ordinary string literal becomes a UTF-8 byte array in the execution environment.
The kind of execution character encoding can differ according to the string prefix.
"ordinary"
u8"UTF-8"
u"UTF-16"
U"UTF-32"
L"wide"
The actual encoding and representation of the character type are affected by the standard edition and implementation settings.
A compiler can handle source encoding and execution encoding through separate options.
Phase 6: Adjacent String-Literal Concatenation
Adjacent string-literal tokens are combined into one string.
const char *message =
"TechPedia "
"Wiki Engine";
This is equivalent to the following.
const char *message =
"TechPedia Wiki Engine";
String literals can also become adjacent as a result of macro expansion.
#define PRODUCT_NAME "TechPedia"
const char *message =
PRODUCT_NAME " Wiki Engine";
After preprocessing, they are adjacent as follows.
const char *message =
"TechPedia" " Wiki Engine";
They ultimately become one string literal.
Phase 7: Syntax and Semantic Analysis
Preprocessing tokens are converted to actual C tokens, and the compiler processes the translation unit by analyzing syntax and semantics.
The following tasks occur during this phase.
- Recognizing keywords and identifiers
- Analyzing declarations
- Constructing types
- Analyzing expressions
- Binding operators
- Checking scope and linkage
- Checking function calls
- Diagnosing constraint violations
- Checking initialization
- Analyzing control flow
- Producing the translation unit
Consider the following function.
int add(int left, int right) {
return left + right;
}
The compiler constructs information such as the following.
function name → add
linkage → external linkage
return type → int
parameters → int, int
body → add two parameters and return the result
Passing an argument of an invalid type can be diagnosed.
int add(int left, int right);
int main(void) {
return add("text", 10);
}
Attempting to pass a string to an int parameter violates a constraint.
For a translation unit containing a constraint violation, a conforming ISO C implementation must produce at least one diagnostic message. However, after producing the diagnostic, the implementation may continue translation as an extension, and the concrete distinction between a warning and an error is determined by the implementation.[48]
Parsing
The compiler front end organizes tokens into syntactic structures according to C grammar.
result = left + right * scale;
After applying operator precedence, the structure is as follows.
assignment
├── left: result
└── right: addition
├── left
└── multiplication
├── right
└── scale
Multiplication binds more tightly than addition.
Many compilers represent this as an abstract syntax tree.
BinaryOperator '='
├── DeclRefExpr result
└── BinaryOperator '+'
├── DeclRefExpr left
└── BinaryOperator '*'
├── DeclRefExpr right
└── DeclRefExpr scale
In Clang, the abstract syntax tree can be inspected with a command such as the following.
clang -Xclang -ast-dump \
-fsyntax-only main.c
This command is a Clang implementation feature rather than an ISO C standard facility.
Semantic Analysis
Even when syntax is valid, type and declaration rules can still be violated.
int *pointer;
double value;
pointer = value;
The tokens and statement structure can be analyzed, but assigning a double directly to an object pointer is semantically invalid.
Semantic analysis checks matters such as the following.
- Whether a name has been declared
- Whether a type is complete
- Whether an operator can be applied to the type
- Whether function arguments are compatible with the prototype
- Whether a return expression is compatible with the return type
- Whether an assignment target is a modifiable object
- Whether labels and
gotostatements are valid - Whether
casevalues are duplicated - Whether a structure member exists
- Whether a constant expression appears where required
struct point {
int x;
int y;
};
int main(void) {
struct point position;
position.z = 10;
return 0;
}
Because no member named z exists, the compiler produces a diagnostic.
Intermediate Representation
Modern compilers frequently convert C source into one or more intermediate representations rather than directly into machine code.
C source
↓
abstract syntax tree
↓
high-level intermediate representation
↓
optimization intermediate representation
↓
target-machine intermediate representation
↓
machine instructions
Clang can transform a program analyzed by the C front end into LLVM IR.
Consider the following code.
int square(int value) {
return value * value;
}
LLVM IR can be emitted with the following command.
clang -S -emit-llvm square.c
Conceptually, an intermediate representation similar to the following is created.
define i32 @square(i32 %value) {
entry:
%result = mul nsw i32 %value, %value
ret i32 %result
}
An intermediate representation is not C itself, but an internal or publicly exposed tool interface of a particular compiler architecture.
Optimization
A compiler can transform a program as long as it preserves the observable behavior guaranteed by the standard.
Representative optimizations include the following.
- Constant folding
- Dead-code elimination
- Common-subexpression elimination
- Function inlining
- Loop transformation
- Vectorization
- Register allocation
- Branch simplification
- Removal of unnecessary memory accesses
- Interprocedural optimization
- Link-time optimization
Consider the following code.
int calculate(void) {
int result = 10 * 20;
return result;
}
The compiler can eliminate the runtime multiplication and return the constant 200.
return 200
Unused computation can also be removed.
int calculate(void) {
int unused = expensive_calculation();
return 10;
}
If expensive_calculation has no externally observable side effects, the call can be removed. However, if the function performs observable behavior such as input and output or volatile access, it cannot be removed arbitrarily.
Abstract Machine and Optimization
Each C source statement does not necessarily generate the same machine instructions in the same order.
int first = 10;
int second = 20;
int result = first + second;
In an optimized program, the three objects may not exist in actual memory, and only the result 30 may be used directly.
The compiler can assume that a correct C program follows rules such as the following.
- It does not perform undefined behavior.
- It does not dereference invalid pointers.
- Signed integer overflow does not occur.
- The strict aliasing rules are followed.
- There are no data races.
- Object lifetimes are respected.
When a program violates these rules, the optimized result can differ greatly from a simple intuition based on source order.
Optimization Levels
GCC- and Clang-family compilers provide optimization levels as options.
-O0
-O1
-O2
-O3
-Os
-Oz
Their general meanings are as follows.
| Option | General purpose |
|---|---|
-O0 | Minimize optimization and simplify debugging |
-O1 | Basic optimization |
-O2 | General balance between performance and code size |
-O3 | More aggressive optimization |
-Os | Reduce code size |
-Oz | Minimize code size more aggressively |
Exactly which optimizations are enabled can differ according to the compiler and version.
A higher optimization level does not always make a program faster. Actual performance depends on code size, caches, vectorization, branches, and the target CPU.
Code Generation
The compiler backend converts an intermediate representation into machine instructions for the target processor.
intermediate representation
↓
instruction selection
↓
register allocation
↓
instruction scheduling
↓
target assembly or machine code
The same C code produces different results depending on the target architecture.
int add(int left, int right) {
return left + right;
}
On x86-64, an instruction conceptually similar to the following may be generated.
lea eax, [rdi + rsi]
ret
On AArch64, it may be similar to the following.
add w0, w0, w1
ret
These are examples, and the actual result depends on the ABI, compiler, and optimization options.
The -S option can be used to inspect assembly output.
clang -S -O2 main.c
gcc -S -O2 main.c
An assembly file with the .s extension is generally generated.
Target System
Code generation requires information about the target system.
- Processor architecture
- Instruction set
- Operating system
- ABI
- Data model
- Calling convention
- Byte order
- Object file format
- Available CPU extensions
- Floating-point rules
Clang can use a target triple.
x86_64-unknown-linux-gnu
aarch64-apple-darwin
x86_64-pc-windows-msvc
arm-none-eabi
Each component generally represents the following information.
architecture-vendor-operating system-environment
The precise composition can differ according to the toolchain.
ABI
An application binary interface specifies rules that allow separately translated code to operate together at the binary level.
An ABI can specify the following.
- Registers used to pass function arguments
- Location of return values
- Registers preserved by the caller
- Registers preserved by the callee
- Stack alignment
- Structure layout
- Sizes of fundamental types
- Representation of names and symbols
- System call conventions
- Object file format
- Exception and unwinding information
The C prototype of the following function can be the same across platforms.
int add(int left, int right);
However, the actual argument-passing method can differ.
x86-64 System V
left → EDI
right → ESI
result → EAX
Windows x64
left → ECX
right → EDX
result → EAX
ISO C does not specify these registers. The platform ABI does.
Assembly
The assembler converts assembly language into an object file containing relocatable machine code and metadata.
main.s
↓ assembler
main.o
An object file generally contains the following information.
- Machine instructions
- Initialized data
- Size of uninitialized data
- Symbols
- Relocation information
- Debugging information
- Exception-handling information
- Section and alignment information
The -c option performs compilation and assembly only.
clang -c main.c -o main.o
gcc -c main.c -o main.o
GCC documentation describes -c as an option that does not run the linker and leaves the object file produced by the assembler as the output.[49]
Object Files
An object file is generally not yet a complete executable program. It can refer to symbols defined in other translation units or libraries.
/* main.c */
int calculate(int value);
int main(void) {
return calculate(10);
}
main.o contains the machine code for main, but the actual address of calculate may not yet be determined.
Conceptually, it contains symbol information such as the following.
defined symbols
main
undefined symbols
calculate
Another object file can define the function.
/* calculate.c */
int calculate(int value) {
return value * 2;
}
calculate.o
defined symbols
calculate
When the linker combines the two object files, it connects the calculate reference in main to the actual definition.
Object File Formats
Object file formats differ according to the platform.
| Platform family | Representative format |
|---|---|
| Linux and many UNIX-like systems | ELF |
| Windows | PE/COFF |
| macOS and Apple platforms | Mach-O |
| Some embedded environments | ELF or a proprietary format |
| WebAssembly | WebAssembly module format |
ELF can represent relocatable object files, executable files, shared objects, and core files. An ELF file can contain a header, program header table, and section header table.[50]
Tools that inspect object file internals include the following.
readelf
objdump
nm
otool
dumpbin
llvm-readobj
llvm-objdump
For example, ELF symbols can be inspected as follows.
readelf --symbols main.o
Sections
An object file can divide its contents into multiple sections according to purpose.
Representative ELF section names include the following.
.text
.rodata
.data
.bss
.symtab
.strtab
.rela.text
.debug_info
Their general roles are as follows.
| Section | General contents |
|---|---|
.text | Machine instructions |
.rodata | Read-only constant data |
.data | Global and static objects with explicit initial values |
.bss | Global and static objects initialized to zero |
.symtab | Symbol table |
.strtab | Symbol strings |
| Relocation sections | Locations that must be modified during linking |
| Debug sections | Information mapping source code to machine code |
ISO C does not require these section names or layouts.
The C standard alone cannot guarantee that the following declarations are placed in particular sections.
static const int first = 10;
static int second = 20;
static int third;
A typical implementation may place them in read-only data, initialized data, and BSS respectively, but the compiler and linker can merge or eliminate them.
Symbols
Symbols represent names of objects and functions in a form that can be linked.
int global_count;
int calculate(int value) {
return value * 2;
}
The object file can contain symbols such as the following.
global_count
calculate
A file-scope static object or function has internal linkage.
static int internal_count;
static int calculate_internal(void) {
return internal_count;
}
The same name in another translation unit does not denote the same entity.
If a function is inlined or an object is eliminated during optimization, the corresponding symbol may not remain in the final object file.
Relocation
When an object file is created, the final memory addresses of functions and objects are often still unknown.
extern int global_value;
int read_value(void) {
return global_value;
}
The compiler records the reference location and relocation information without knowing the address of global_value.
read_value machine code
+
global_value reference relocation entry
The linker determines the final address and modifies the corresponding location.
before relocation
load [unknown address]
after relocation
load [global_value address]
In position-independent code, the dynamic linker may process some relocations at execution time.
Linking
The linker combines multiple object files and libraries to create an executable file or shared library.
main.o
calculator.o
libc
startup code
↓ linker
application
Its major tasks include the following.
- Resolving external symbol references
- Combining sections
- Determining memory layout
- Applying relocations
- Selecting library code
- Setting the entry point
- Generating executable file headers
- Generating dynamic-linking information
- Removing unused sections
- Applying linker scripts
The final ISO C translation phase is described as resolving all external object and function references and combining library components to create a program image containing the information required by the execution environment.[51]
Symbol Resolution
The linker connects references containing declarations only to actual definitions.
/* main.c */
int add(int left, int right);
int main(void) {
return add(10, 20);
}
/* add.c */
int add(int left, int right) {
return left + right;
}
The linker connects the undefined symbol add in main.o to the definition in add.o.
If the definition cannot be found, a link error occurs.
undefined reference to `add`
If the same external symbol is defined multiple times in a disallowed manner, a multiple-definition error can occur.
multiple definition of `global_count`
Mismatched Declarations and Definitions
A linker usually does not know the complete C type information, or knows only a limited amount of it.
/* first.c */
int process(int value);
/* second.c */
double process(double value) {
return value;
}
Using incompatible declarations and definitions in different translation units violates C program rules. If only the symbol names match, some linkers may still complete linking, but different calling conventions and return-value interpretations can cause incorrect behavior at runtime.
A common header must be used to keep declarations and definitions consistent.
/* process.h */
#ifndef PROCESS_H
#define PROCESS_H
int process(int value);
#endif
/* process.c */
#include "process.h"
int process(int value) {
return value;
}
Static Libraries
A static library is an archive that groups multiple object files together.
math.o
vector.o
matrix.o
↓
libmath.a
The linker can select object files or sections required by the program from the static library and include them in the executable file.
clang main.o -L. -lmath \
-o application
General characteristics of static linking include the following.
- Required library code is included in the executable.
- The corresponding shared library may not be needed at execution time.
- The executable file can become larger.
- Multiple programs can contain separate copies of the same code.
- The program may need to be relinked when the library is updated.
- Whether fully static linking is possible depends on the platform.
A static library generally does not create a new translation unit; it is a collection of object files that have already been translated.
Shared Libraries
A shared library is a library format that can be mapped and used by multiple processes at program execution time.
Representative extensions include the following.
Linux and UNIX → .so
Windows → .dll
macOS → .dylib
An executable file can contain information about required shared libraries and symbols rather than the entire library code.
application
├── own code
├── requires libc.so
└── requires libgraphics.so
At execution time, the dynamic linker locates and maps the shared objects and processes their symbols and relocations.
Advantages of shared libraries include the following.
- Multiple programs can share code pages.
- Executable file size can be reduced.
- Some libraries can be updated independently.
- Plugins and runtime loading can be implemented.
The following problems can also occur.
- A library cannot be found.
- ABI incompatibility
- Symbol version conflicts
- Loading-path problems
- Differences between deployment environments
- Runtime relocation costs
- Behavioral changes caused by library replacement
Link Order
With some static linkers, the order in which libraries are specified can affect symbol resolution.
clang main.o -lmath
When the linker processes inputs from left to right and selects static archive members according to symbols currently required, dependent objects or libraries may need to appear before the libraries that provide those symbols.
application.o
↓ requires libgraphics
libgraphics
↓ requires libmath
libmath
clang application.o \
-lgraphics \
-lmath
The exact behavior depends on the linker and options used.
Link-Time Optimization
Link-time optimization preserves the intermediate representations of multiple translation units until the link stage and optimizes the complete program.
main.c → intermediate-representation object
math.c → intermediate-representation object
util.c → intermediate-representation object
↓
LTO link
↓
whole-program optimization
This can enable optimizations such as the following.
- Function inlining across translation units
- Removal of unused functions
- Global constant propagation
- Call-graph optimization
- Type-based optimization
With GCC- and Clang-family compilers, it is usually enabled as follows.
-flto
Compatible LTO settings and toolchains are required during both compilation and linking.
Executable Files
The result of linking can be an executable program image.
An executable file generally contains the following information.
- File-format identifier
- Target architecture
- Entry point
- Regions to load into memory
- Machine code
- Initial data
- List of dynamic libraries
- Relocation information
- Symbols and debugging information
- Permission and security metadata
The existence of an executable file does not necessarily mean that it can run on the current computer.
The following conditions may need to match.
- CPU architecture
- Operating system ABI
- Executable file format
- Required dynamic loader
- Shared libraries
- System call interface
- File permissions
- Security policy
Loading
When a user starts a program, the operating system’s program loader reads the executable file and constructs a new process image.
A representative UNIX-like flow is as follows.
shell
↓ execve
kernel
↓ verify executable file
map program segments
↓
construct stack and initial execution state
↓
dynamic linker or program entry point
When a dynamically linked ELF executable is started with execve on Linux, the interpreter specified in the ELF PT_INTERP segment is used to load the required shared objects.[52]
The program loader can perform the following tasks.
- Replace the existing process image
- Validate executable headers
- Map code and data regions
- Construct the initial stack
- Pass arguments and environment information
- Set page permissions
- Specify the dynamic linker
- Set the initial instruction pointer
These are behaviors of a particular operating system implementation. ISO C does not directly specify system calls or virtual-memory layouts.
Sections and Segments
Object-file sections and runtime segments serve different purposes.
sections
→ centered on linking and file organization
segments
→ centered on runtime memory loading
Multiple sections can be combined into one memory segment.
.text
part of .rodata
↓
read and execute segment
.data
.bss
↓
read and write segment
An ELF program header describes which file regions are mapped into memory with which permissions at execution time.
Virtual Memory Mapping
A typical operating system can map an executable file into virtual memory rather than copying the entire file into physical memory at once.
process virtual address space
├── executable code
├── read-only data
├── writable global data
├── dynamic libraries
├── heap
├── stack
└── operating-system-specific areas
Required pages can be connected to physical memory when first accessed.
This concrete memory layout is not guaranteed by ISO C. In an embedded environment, code may reside in flash memory while data resides in SRAM, and an operating system kernel or bootloader may use a separate linker script and memory map.
Dynamic Linker
In a dynamically linked program, the dynamic linker processes shared libraries before or during execution.
In a Linux ELF environment, an ld-linux.so-family program can perform dynamic linking. The dynamic linker locates and loads required shared objects and resolves symbols and relocations.[53]
A general flow is as follows.
load executable
↓
load dynamic linker
↓
search for required shared libraries
↓
map shared objects into memory
↓
resolve symbols
↓
apply relocations
↓
run initialization functions
↓
transfer to program startup code
Shared-library search paths differ according to the platform and configuration.
On Linux-family systems, the following elements can be involved.
- The executable file’s dynamic section
RPATHRUNPATH- Environment variables
- Dynamic-linker cache
- Default library directories
- Paths relative to the executable file
The exact search priority must be checked in the dynamic linker documentation.
Immediate and Lazy Binding
Function symbols in shared libraries can be resolved entirely at program startup or when the function is first called.
immediate binding
→ resolve symbols at startup
lazy binding
→ resolve symbols at first call
Lazy binding can reduce startup time, but the first call can incur additional cost.
Some environments can be configured to resolve all symbols at startup for security or predictable failure timing.
Position-Independent Code
Shared libraries can be mapped at different addresses and therefore frequently use position-independent code.
instead of fixed absolute addresses
access relative to the current position or a table
With GCC- and Clang-family compilers, the following option is commonly used.
-fPIC
An example of creating a shared library is as follows.
clang -fPIC \
-c library.c \
-o library.o
clang -shared \
library.o \
-o libexample.so
Options and extensions differ according to the platform.
Execution Entry Point
It is common to simplify the explanation by saying that the operating system jumps directly to C’s main function after loading an executable, but in an ordinary hosted implementation there can be runtime startup code between the actual entry point and main.
operating system entry point
↓
runtime startup code
↓
process initial-information processing
↓
C library initialization
↓
global runtime-state preparation
↓
main call
In an ELF environment, the executable header’s entry point can be runtime startup code such as _start.
_start
↓
C runtime initialization
↓
main(argc, argv)
↓
exit return processing
The name _start and the concrete call structure are not part of ISO C.
C Runtime
The C runtime is a general term for the code and data used for program startup and termination, standard-library integration, and adaptation to the execution environment.
It can include the following elements.
- Startup code
- Stack and argument interpretation
- Standard input and output preparation
- Static data initialization support
- Thread-local storage preparation
- Dynamic-linker interface
- Calling
main - Processing termination functions
- Process-termination system calls
- Compiler support functions
Operations such as integer division or wide-integer arithmetic may be implemented through compiler runtime functions when the target CPU does not support them directly.
64-bit division
↓
no direct instruction on target CPU
↓
compiler support-function call
Therefore, a program that does not use the standard library can still depend on the compiler runtime.
Initialization of Static Objects
The C standard requires all objects with static storage duration to have their initial values before program startup.[54]
static int count;
static int value = 10;
static int *pointer;
Conceptually, they have the following states at program startup.
count → 0
value → 10
pointer → null pointer
C static initializers are restricted to forms that can be processed at translation time. Unlike C++, the language does not automatically execute global object constructors using general user-function calls.
static int value = calculate();
In ordinary C, calculate() is not an integer constant expression and therefore cannot be used as the initializer of a static-storage object.
When runtime initialization is required, it is performed in main or a separate initialization function.
static int value;
static void initialize(void) {
value = calculate();
}
int main(void) {
initialize();
return 0;
}
Compiler extensions and platform-specific initialization sections can exist separately.
Hosted Environments
A hosted implementation generally provides a complete program environment in which an operating system and standard library are available.
In a hosted environment, the function called at program startup is named main.
A representative form is as follows.
int main(void) {
return 0;
}
It can receive command-line arguments.
int main(
int argc,
char *argv[]
) {
return 0;
}
Equivalent pointer notation can also be used.
int main(
int argc,
char **argv
) {
return 0;
}
The C standard also permits implementations to provide other implementation-defined forms of main.[55]
argc and argv
In the two-parameter form of main, argc represents the number of strings passed to the program, and argv points to an array of string pointers.
argc = 3
argv[0] → "application"
argv[1] → "--output"
argv[2] → "file.txt"
argv[3] → NULL
The C standard specifies the following.
argcis not negative.argv[argc]is a null pointer.- If
argcis greater than zero,argv[0]represents the program name or an empty string. - The following strings are program parameters provided by the execution environment.
argc,argv, and each argument string can be modified by the program.- Their stored values are retained from program startup until termination.
A simple program that prints arguments is as follows.
#include <stdio.h>
int main(
int argc,
char *argv[]
) {
for (int index = 0; index < argc; ++index) {
printf(
"argv[%d] = %s\n",
index,
argv[index]
);
}
return 0;
}
The method used to divide a command line into an array of strings can differ according to the operating system and execution environment. A shell may first process quotation marks, wildcards, environment variables, and other syntax.
Environment Variables
The ISO C standard library allows string values from the execution environment to be queried through getenv.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
const char *home = getenv("HOME");
if (home != NULL) {
printf("%s\n", home);
}
return 0;
}
The platform determines the names, meanings, and case-sensitivity rules of environment variables.
Some implementations provide an environment pointer as a third parameter to main, but this is not a standard ISO C form of main.
int main(
int argc,
char **argv,
char **environment
)
Portable C documentation must distinguish this as an implementation extension.
Standard Streams
In a hosted environment, three text streams are open when a program starts.
- Standard input,
stdin - Standard output,
stdout - Standard error,
stderr
#include <stdio.h>
int main(void) {
fprintf(stdout, "normal output\n");
fprintf(stderr, "error output\n");
return 0;
}
The execution environment can determine whether a stream is connected to a terminal, file, pipe, or another device.
application
↓ stdout
terminal
application
↓ stdout redirection
output.txt
application
↓ pipe
another application
Freestanding Environments
A freestanding implementation is an execution environment in which an operating system may not exist or a complete hosted environment may not be provided.
Representative examples include the following.
- Bootloaders
- Operating system kernels
- Microcontroller firmware
- Early runtimes
- Special-purpose processors
- Some real-time systems
In a freestanding environment, the name and type of the function called at startup are implementation-defined.
void reset_handler(void) {
/* Hardware initialization */
}
Another system can use a startup point such as the following.
void kernel_main(void) {
/* Kernel initialization */
}
ISO C does not require main in a freestanding environment. The available standard-library facilities may be more limited than in a hosted environment, and the effect of termination is implementation-defined.[56]
Freestanding Startup Process
A representative startup process for a microcontroller can be as follows.
power-on or reset
↓
reset vector
↓
set initial stack pointer
↓
copy .data initial values from flash to RAM
↓
initialize .bss region to zero
↓
initialize clock and hardware
↓
call user startup function
This process is not directly implemented by the C standard. Startup assembly, a linker script, the compiler runtime, and the hardware specification construct it together.
startup.s
linker.ld
main.c
An operating system kernel likewise has an entry process and memory environment different from an ordinary hosted program.
Linker Scripts
A linker script can instruct the linker to place code and data at particular locations in target memory.
An embedded device can contain memory regions such as the following.
FLASH
start address: 0x08000000
size: 512 KiB
RAM
start address: 0x20000000
size: 128 KiB
A conceptual layout is as follows.
.text → FLASH
.rodata → FLASH
.data → initial value in FLASH, execution copy in RAM
.bss → RAM
stack → RAM
heap → RAM
ISO C does not specify linker-script syntax or memory addresses. Documentation for the linker and platform must be followed.
Calling main
In a hosted environment, the program runtime calls main after completing the required initialization.
execution environment
↓
main(argc, argv)
main is syntactically a C function, so it can call other functions, create objects, and use the standard library.
#include <stdio.h>
#include <stdlib.h>
static int application_run(void) {
puts("running");
return EXIT_SUCCESS;
}
int main(void) {
return application_run();
}
An ordinary program can keep main as a thin entry point that coordinates program setup and termination status while dividing actual logic into separate functions and modules.
Program Execution
After a program starts, expressions and statements are evaluated according to the rules of the C abstract machine.
int main(void) {
int left = 10;
int right = 20;
int result = left + right;
return result == 30 ? 0 : 1;
}
The actual processor executes compiled machine instructions, but the meaning of a C program is judged according to the abstract semantics of the standard rather than a particular instruction order.
Execution can include the following.
- Function calls
- Creation and destruction of automatic objects
- Access to static objects
- Dynamic memory allocation
- Input and output
- System-library calls
- Thread creation
- Atomic operations
- Signal handling
- Operating system API calls
When facilities outside ISO C are called, part of the program’s meaning depends on the corresponding platform API.
Function Call Process
At the machine level, a typical function call can include the following process.
evaluate arguments
↓
place them in registers or on the stack according to the ABI
↓
store return location
↓
transfer to called function
↓
preserve required registers
↓
prepare space for local objects
↓
execute function body
↓
place return value
↓
return to caller
Consider the following function.
int add(int left, int right) {
return left + right;
}
It is called as follows.
int result = add(10, 20);
When optimization is disabled, an actual call instruction can be generated. When optimization is enabled, the function can be inlined and the call itself can disappear.
result = 30
A function call stack is a common implementation, but ISO C does not specify a particular stack-frame structure.
System Calls and Library Calls
A C standard-library function does not necessarily correspond directly to one operating system system call.
printf("hello\n");
A typical implementation can pass through layers such as the following.
printf
↓
format-string analysis
↓
standard I/O buffer
↓
C library internal function
↓
operating system write API
↓
kernel
↓
file, terminal, or pipe
Because of buffering, data may not be written to the actual device immediately after printf is called.
printf("loading...");
Output may occur when a newline is written, fflush is called, or the stream is closed.
printf("loading...");
fflush(stdout);
Normal Termination
In a hosted environment, returning a value from the initial invocation of main has the same effect as calling exit with that value.
int main(void) {
return 0;
}
It has the same meaning as follows.
int main(void) {
exit(0);
}
Reaching the end of main is treated as returning 0.
int main(void) {
}
It is equivalent to the following.
int main(void) {
return 0;
}
This special rule does not apply to all ordinary functions returning int.
int calculate(void) {
}
If an ordinary function that must return a value reaches the end without a valid return and the caller uses the resulting value, undefined behavior can occur.
Termination Status
A program can return a termination status to the execution environment.
#include <stdlib.h>
int main(void) {
return EXIT_SUCCESS;
}
A failure status can be returned as follows.
#include <stdlib.h>
int main(void) {
return EXIT_FAILURE;
}
Zero or EXIT_SUCCESS corresponds to successful termination, while EXIT_FAILURE corresponds to failed termination. The numerical values actually used by the operating system and the manner in which they are externally displayed are implementation-defined.
The external meaning of an arbitrary other integer return value can also differ according to the execution environment.
exit
exit performs normal program termination.
#include <stdlib.h>
void fatal_error(void) {
exit(EXIT_FAILURE);
}
Under the C23 standard, the normal exit process conceptually follows this order.
call atexit-registered functions in reverse order
↓
flush streams with pending output
↓
close open streams
↓
remove tmpfile temporary files
↓
return control to the execution environment
exit does not return to its caller.[57]
atexit
atexit registers a function to be called during normal termination.
#include <stdio.h>
#include <stdlib.h>
static void cleanup(void) {
puts("cleanup");
}
int main(void) {
if (atexit(cleanup) != 0) {
return EXIT_FAILURE;
}
puts("running");
return EXIT_SUCCESS;
}
The output flow can be as follows.
running
cleanup
When multiple functions are registered, they are generally called in reverse registration order.
atexit(cleanup_first);
atexit(cleanup_second);
The invocation order is as follows.
cleanup_second
cleanup_first
atexit is not a facility that automatically releases resources whenever the current function scope is left. It is called only during normal program termination.
quick_exit
quick_exit performs a more limited termination procedure than ordinary exit.
#include <stdlib.h>
quick_exit(EXIT_SUCCESS);
Functions registered with at_quick_exit are called, while functions registered with atexit are not.
#include <stdlib.h>
static void quick_cleanup(void) {
/* Limited cleanup */
}
int main(void) {
at_quick_exit(quick_cleanup);
quick_exit(EXIT_SUCCESS);
}
Stream handling and other termination effects differ from exit, so it must not be treated as equivalent to ordinary normal termination.
_Exit
_Exit returns control to the execution environment without calling functions registered with atexit or at_quick_exit.
#include <stdlib.h>
_Exit(EXIT_FAILURE);
Standard I/O buffers must not be expected to be processed in the same way as during ordinary normal termination.
It can be used in emergency termination or low-level runtime handling, but return or exit is generally appropriate for ordinary application termination.
abort
abort causes abnormal termination.
#include <stdlib.h>
void unreachable_state(void) {
abort();
}
It differs from ordinary normal termination, and open stream buffers are not guaranteed to be written or files to be closed.
abort
↓
processing related to SIGABRT
↓
abnormal termination
The execution environment can generate a core dump or error report, but this is platform policy.
Process Termination and Resources
When a process terminates, an operating system generally reclaims its address space and some kernel resources.
However, ISO C exit semantics and operating-system process-resource reclamation must be distinguished.
The following resources may still need to be explicitly cleaned up by the program.
- Application data not yet written to files
- Database transactions
- State shared with other processes
- Temporary files
- Device state
- Network protocol termination
- Lock files
- External service sessions
- Internal structures in shared memory
The fact that the operating system reclaims memory does not mean that a program can omit logical cleanup.
Signals and Abnormal Control Flow
In a hosted environment, a signal can interrupt a program’s ordinary control flow.
#include <signal.h>
static volatile sig_atomic_t interrupted;
static void handle_signal(int signal) {
interrupted = signal;
}
int main(void) {
signal(SIGINT, handle_signal);
while (!interrupted) {
/* Work */
}
return 0;
}
Functions callable from a signal handler and objects that can be accessed are subject to strict restrictions. An ordinary function must not be assumed to be reentrant or signal-safe.
POSIX provides more specific rules for signals and asynchronous-signal-safe functions than ISO C, but it is a separate standard.
Debug Information
A compiler can place debugging information in object and executable files that maps source code to machine instructions.
source file name
line number
function name
local variables
type information
call-frame information
GCC- and Clang-family compilers use -g.
clang -g main.c -o application
Representative debugging-information formats include DWARF and CodeView.
In optimized code, the following phenomena can occur.
- Variables are eliminated.
- Statement order differs from instruction order.
- Functions are inlined.
- One source line corresponds to multiple instruction ranges.
- Multiple source statements are combined into one instruction.
Therefore, source-line debugging can be less intuitive at high optimization levels.
Debug and Release Builds
A typical development configuration can be divided as follows.
debug build
-O0 or low optimization
-g
additional checks
assert enabled
release build
-O2 or -O3
required security options
unnecessary symbols removed
deployment settings
However, debug and release are project and build-system configurations rather than C standard concepts.
Errors that appear only under optimization can indicate undefined behavior, data races, uninitialized values, or aliasing violations. They must not be addressed merely by disabling optimization.
Separate Compilation
Separate compilation of multiple source files avoids retranslating unchanged files.
modify main.c
↓
regenerate main.o only
↓
reuse existing renderer.o
reuse existing network.o
↓
link again
This method reduces build time in large projects.
However, when a header changes, every translation unit that includes it may need to be recompiled.
modify common.h
↓
recompile main.c
recompile renderer.c
recompile network.c
The build system must track header dependencies.
Incremental Builds
An incremental build regenerates only changed sources and affected targets.
source modification times
header dependencies
compiler options
tool versions
generated files
This information is used to determine which targets must be rebuilt.
Representative build systems include the following.
- Make
- CMake
- Ninja
- Meson
- Bazel
- Custom build systems
A build system is not itself a compiler. It constructs and executes the required commands and dependency relationships.
Compiler Drivers
When a user runs clang or gcc, it appears that one program directly performs every transformation.
clang main.c -o application
In practice, the driver coordinates tools or internal stages such as the following.
compiler driver
├── preprocessor
├── C front end
├── optimizer
├── code generator
├── assembler
└── linker
Clang can use an integrated assembler internally or invoke an external system linker. GCC likewise invokes multiple internal programs and external tools depending on the target and configuration.
Clang’s -### option prints the commands that would be executed without executing them.
clang -### main.c -o application
Stopping at Each Stage
GCC- and Clang-compatible drivers generally provide the following options.
| Command | Result |
|---|---|
-E | Preprocessing result |
-S | Assembly output |
-c | Object file |
| No stage option | Perform linking |
clang -E main.c -o main.i
clang -S main.c -o main.s
clang -c main.c -o main.o
clang main.o -o application
Separating the stages makes it easier to determine where an error occurs.
Error Stages
Errors can occur at different points in the execution process.
preprocessing error
→ header not found
→ macro error
→ conditional-compilation error
compilation error
→ syntax error
→ type mismatch
→ declaration violation
assembly error
→ invalid inline assembly
→ target-instruction problem
link error
→ symbol definition missing
→ duplicate definition
→ missing library
load error
→ shared library missing
→ executable-format mismatch
→ permission problem
runtime error
→ invalid pointer
→ undefined behavior
→ logic error
→ external resource error
For example, the following error occurs during linking rather than compilation.
int calculate(int value);
int main(void) {
return calculate(10);
}
Because calculate is declared, compilation is possible, but if its definition is not linked, an error such as the following occurs.
undefined reference to `calculate`
Header Not Found Errors
The following error occurs during preprocessing.
#include "missing.h"
fatal error: 'missing.h' file not found
A header search path can be added.
clang -Iinclude \
main.c \
-o application
The concrete behavior and priority of -I follow the compiler documentation.
Library Not Found Errors
If the linker cannot locate a library, linking fails.
clang main.o \
-lexample \
-o application
cannot find -lexample
A library search path can be specified.
clang main.o \
-L/path/to/library \
-lexample \
-o application
Executable creation can succeed while execution later fails because a shared library cannot be found.
error while loading shared libraries
Link-time search paths and runtime dynamic-library search paths can differ.
Cross-Compilation
Cross-compilation creates code and executable files for a target other than the computer currently running the compiler.
host
x86-64 Linux
target
AArch64 Linux
With Clang, a form such as the following can be used.
clang \
--target=aarch64-linux-gnu \
--sysroot=/path/to/sysroot \
main.c \
-o application
A complete cross-toolchain may require the following.
- Target headers
- Target standard library
- Target compiler runtime
- Target linker
- Target startup code
- Target sysroot
- Target ABI settings
Clang’s official documentation likewise describes the target triple, sysroot, CPU, and ABI settings as major elements of cross-compilation.[58]
Separation of Compiler and Linker
A C compiler and linker can be separate tools.
clang
→ transforms C source into object files
ld or lld
→ combines object files into an executable
However, directly invoking ld can omit the following elements that a compiler driver normally adds automatically.
- Startup object files
- C library
- Compiler runtime
- Dynamic linker path
- Target-specific library paths
- Default linking options
An ordinary C program is more safely linked through the compiler driver.
clang main.o -o application
Direct use of the linker can be necessary when controlling the complete configuration, as with kernels, bootloaders, and specialized runtimes.
Reproducible Builds
A reproducible build is the property of repeatedly generating identical output from the same source and tool settings.
The following elements can cause the output to differ.
- Build date and time
- Absolute source paths
- File ordering
- Environment variables
- Temporary file names
- Parallel linking order
- Compiler version
- Library version
- Random build identifiers
- Locale and character encoding
The following macros can have different values in each build.
const char *build_date = __DATE__;
const char *build_time = __TIME__;
When reproducible builds are required, these values should be avoided or fixed metadata should be supplied by the build system.
Security of the Execution Process
Translation, linking, and loading also affect security.
Representative defensive techniques include the following.
- Stack protection
- Position-independent executables
- Address-space layout randomization
- Read-only relocations
- Non-executable data regions
- Control-flow protection
- Restricted symbol visibility
- Hardening option portfolios
- Undefined-behavior checking
- Source-integrity verification
- Trusted library paths
Examples of compiler and linker options include the following.
-fstack-protector-strong
-fPIE
-pie
-Wl,-z,relro
-Wl,-z,now
Support and meaning differ according to the operating system and toolchain.
Security options do not automatically make memory-unsafe code safe. They are defensive layers that can reduce the exploitability of errors.
Summary of the Execution Process
Executing a C program is not a process of simply substituting source files with machine code. Source characters pass through several conceptual translation phases to become tokens and translation units, and the compiler analyzes their syntax and semantics before transforming them into code and data for the target system.
source characters
↓
preprocessing tokens
↓
C tokens and translation units
↓
intermediate representation
↓
target machine code
↓
object files
↓
program image
Object files are intermediate results containing symbols and relocation information. The linker resolves external references among multiple translation units and libraries to create an executable file or shared library.
In a hosted environment, the operating system loader, dynamic linker, and C runtime prepare the executable and libraries before calling main. In a freestanding environment, the startup function, memory initialization, and termination effects can differ according to the implementation and hardware.
hosted environment
operating system → loader → runtime → main
freestanding environment
reset or boot → startup code → implementation-defined startup function
Returning from main or calling exit performs normal termination and returns a status to the execution environment. In contrast, _Exit, quick_exit, and abort have different cleanup and termination semantics.
To understand the execution process of C accurately, the translation and execution semantics specified by ISO C must be distinguished from the concrete tool stages implemented by compilers, linkers, and operating systems. The former defines the meaning of portable programs, while the latter realizes that meaning on a particular processor and operating system.
The C Standard and Standard Library
C is not a language defined by one particular compiler or operating system implementation. Its syntax and semantics, translation and execution environments, the minimum capabilities that implementations must provide, and the standard library are specified by the international standard ISO/IEC 9899.
The C standard is not merely a grammar manual describing the source form of programs. It also specifies the following matters.
- Characters and tokens that constitute C programs
- The syntax of declarations, types, expressions, and statements
- Semantic rules for interpreting programs
- Conceptual processes of translation and execution
- Objects and memory, evaluation order, and concurrency
- Capabilities that conforming implementations must provide
- Minimum implementation limits that implementations must support
- Input and output representations of programs
- Standard headers and library interfaces
- Defined behavior and implementation-defined, unspecified, and undefined behavior
- Requirements for hosted and freestanding implementations
ISO describes the purpose of the C standard as increasing the portability of C programs across diverse data-processing systems. The standard is intended for both implementers and programmers and specifies program representation, syntactic restrictions, semantic rules, input and output data representations, and the limitations of conforming implementations.[59]
ISO/IEC 9899
├── General terms and conformance
├── Translation and execution environments
├── C language
├── Standard library
├── Language syntax summary
├── Library summary
├── Portability and implementation limits
└── Annexes
The language and library are specified as separate parts of the C standard, but together form one standard. An environment that implements only the language syntax and provides no standard library at all cannot be a complete hosted implementation. Conversely, standard library functions operate according to the C language’s rules for types, objects, pointers, and execution.
ISO/IEC 9899
The identifier of the international C standard is ISO/IEC 9899. SC 22 under ISO/IEC JTC 1, the joint technical committee of ISO and IEC, is responsible for programming-language standards, while the practical development and maintenance of the C standard are performed by WG14.
ISO / IEC
↓
ISO/IEC JTC 1
↓
SC 22
↓
WG14
↓
ISO/IEC 9899
WG14 reviews defect reports for existing standards, discusses proposals for inclusion in future editions, and manages committee drafts and public working documents. WG14 describes itself as the C language committee responsible for maintaining ISO/IEC 9899 and related technical specifications.[60]
The standard is published in new editions through periodic revision cycles. When a new edition is published, it supersedes the previous edition as the international standard, but compilers and projects may continue to provide older standard modes for compatibility.
Standard Editions
The major editions of the C standard are as follows.
| Common name | Official document | Major characteristics |
|---|---|---|
| C89 | ANSI X3.159-1989 | First official ANSI C standard |
| C90 | ISO/IEC 9899:1990 | Adoption of C89 as an international standard |
| C95 | ISO/IEC 9899:1990/Amd 1:1995 | Amendment to C90 |
| C99 | ISO/IEC 9899:1999 | Large-scale language and library expansion |
| C11 | ISO/IEC 9899:2011 | Memory model, atomic operations, and threads |
| C17 | ISO/IEC 9899:2018 | Defect corrections and stabilization of C11 |
| C23 | ISO/IEC 9899:2024 | Modern syntax and library expansion |
C89 and C90 essentially refer to the same language edition. C89 is named after the year in which the American ANSI standard was published, while C90 is named after the year in which ISO adopted it as an international standard.
C95 is an informal name for Amendment 1 and the technical corrections applied to C90 rather than an independent full revision.
C17 was the name used during committee work, but because the official document was published in 2018, it is also called C18. WG14 project records primarily use the name C17.[61]
The latest published standard is ISO/IEC 9899:2024, and the common name used during development and for the language edition is C23.[62]
Standards and Language Modes
A compiler may provide options for selecting among multiple editions of the C standard.
GCC- and Clang-family compilers generally use the -std option.
-std=c89
-std=c90
-std=c99
-std=c11
-std=c17
-std=c23
GNU dialects that permit compiler-specific extensions also exist.
-std=gnu89
-std=gnu99
-std=gnu11
-std=gnu17
-std=gnu23
c17 and gnu17 are not the same.
c17
→ centered on ISO C17
→ restricts or diagnoses nonstandard extensions
gnu17
→ based on C17
→ also permits GNU extensions
Selecting a standard mode does not automatically guarantee that the compiler and system library completely provide every optional feature of that standard. The features and defects of the implementation, library version, and target environment must be checked.
Checking the Standard Edition
Implementations since C95 can indicate the supported standard edition through the __STDC_VERSION__ macro.
#include <stdio.h>
int main(void) {
#ifdef __STDC_VERSION__
printf("%ld\n", __STDC_VERSION__);
#else
puts("pre-C95 or nonconforming implementation");
#endif
return 0;
}
Representative values are as follows.
| Standard edition | __STDC_VERSION__ |
|---|---|
| C95 | 199409L |
| C99 | 199901L |
| C11 | 201112L |
| C17 | 201710L |
| C23 | 202311L |
A program can require a particular standard edition or later.
#if !defined(__STDC_VERSION__) || \
__STDC_VERSION__ < 201710L
#error "C17 or later is required"
#endif
C23 features can be used conditionally.
#if defined(__STDC_VERSION__) && \
__STDC_VERSION__ >= 202311L
constexpr unsigned int buffer_size = 4096;
#else
enum {
buffer_size = 4096
};
#endif
__STDC_VERSION__ is an important indicator of the compilation mode, but it does not describe every optional feature or compiler extension.
Conformance
The C standard distinguishes between conformance of programs and conformance of implementations.
The major concepts are as follows.
- Strictly conforming program
- Conforming program
- Conforming hosted implementation
- Conforming freestanding implementation
A strictly conforming program uses only language and library features specified by the standard, does not depend on unspecified or undefined behavior or on implementation-defined results for its output, and does not exceed the minimum implementation limits required by the standard.
strictly conforming program
├── uses only ISO C features
├── does not depend on nonstandard extensions
├── does not depend on undefined behavior
├── does not make output depend on unspecified results
└── does not exceed minimum implementation limits
A conforming program is a program accepted by a conforming implementation. It may use implementation-specific extensions and platform APIs and therefore is not necessarily portable.
#ifdef __linux__
#include <unistd.h>
#endif
This program can be validly used on a Linux implementation, but unistd.h is not an ISO C standard header.
The C23 standard defines two forms of conforming implementation: hosted implementations and freestanding implementations.[63]
Hosted Implementations
A hosted implementation generally provides a complete C program environment running on top of an operating system.
A hosted implementation provides the following elements.
- Program startup through
main - The complete standard library
- Standard input, output, and error streams
- Files and an execution environment
- Normal termination processing
- Program capabilities required by the standard
#include <stdio.h>
#include <stdlib.h>
int main(void) {
puts("hosted C program");
return EXIT_SUCCESS;
}
A conforming hosted implementation must accept every strictly conforming program.
However, the standard library is not required to be provided as one dynamic library file or one particular libc implementation. Compiler built-ins, static libraries, operating system APIs, and runtime code may work together to implement the standard interfaces.
Freestanding Implementations
A freestanding implementation is an environment that may lack a complete operating system and an ordinary program execution environment.
Representative examples include the following.
- Operating system kernels
- Bootloaders
- Microcontroller firmware
- Real-time control programs
- Constrained embedded systems
- Special-purpose processor runtimes
void reset_handler(void) {
initialize_memory();
initialize_hardware();
application_run();
for (;;) {
}
}
A freestanding implementation is not required to provide the entire standard library. C23 separately specifies the range of headers and features that a freestanding implementation must support.
The headers fundamentally required in a C23 freestanding implementation include the following.
<float.h>
<iso646.h>
<limits.h>
<stdalign.h>
<stdarg.h>
<stdbit.h>
<stdbool.h>
<stddef.h>
<stdint.h>
<stdnoreturn.h>
C23 also includes most of <string.h> and selected memory-alignment facilities from <stdlib.h> within the conformance requirements for freestanding implementations. Implementations that provide particular floating-point feature macros may also provide limited parts of <fenv.h>, <math.h>, and conversion functions.[64]
This does not mean that a freestanding implementation may provide only those features. An implementation may additionally provide the complete standard library or its own extensions.
Implementation-Defined Behavior and Documentation
C does not fix every detail of all hardware and operating systems to one model. Some characteristics are selected by the implementation, but the implementation must document its choices.
Representative implementation-defined matters include the following.
- Signedness of plain
char - Sizes of fundamental integer types
- Some results of right shifts
- Source and execution character sets
- Details of files and streams
- Results of some integer conversions
- Structure alignment and padding
- Additional forms of
main - Effects of signals and the execution environment
- Some environment-dependent behavior of the standard library
A conforming implementation must provide documentation describing its implementation-defined and locale-specific characteristics and the extensions it provides.[65]
ISO C standard
→ defines the range of choices available to implementations
compiler and platform documentation
→ describes the actual choices made
To write portable programs, the target compiler, ABI, and standard library documentation must be consulted alongside the standard.
Diagnostics
A conforming implementation must produce at least one diagnostic message when a translation unit violates a C syntax rule or constraint.
int main(void) {
int *pointer = 10;
return pointer;
}
Because this violates type constraints, the implementation must diagnose it.
However, the requirement to issue a diagnostic does not necessarily require translation to stop. A compiler may produce a warning and still generate an executable as an extension.
diagnostic required
≠
compilation must stop
An #error directive prevents successful translation when it is not excluded by conditional compilation.
#ifndef REQUIRED_PLATFORM
#error "REQUIRED_PLATFORM is not defined"
#endif
Undefined behavior is generally not something a compiler is required to diagnose.
int values[4];
values[4] = 10;
A compiler may issue a warning, but the standard does not require it to detect every out-of-bounds error across all execution paths.
Minimum Implementation Limits
The C standard specifies the minimum program sizes, nesting depths, ranges, and precision that a conforming implementation must be able to process.
These limits include categories such as the following.
- Nested block depth
- Conditional preprocessing nesting
- Number of function parameters
- Number of macro parameters
- Number of external identifiers
- Number of declarators in one declaration
- String literal length
- Number of characters in a source line
- Required integer ranges
- Minimum floating-point range and precision
The standard minimum does not prohibit an implementation from supporting larger values.
standard minimum limit
→ every conforming implementation supports at least this much
actual implementation limit
→ may exceed the standard minimum
A strictly conforming program must not depend on program structures that exceed the minimum limits required by the standard.
Standard Library
The C standard library is a collection of types, macros, functions, and headers defined so that C programs can use common facilities across conforming implementation environments.
The standard library provides the following areas.
standard library
├── diagnostics
├── characters and strings
├── memory processing
├── files and input/output
├── numerical computation
├── dynamic memory management
├── time and dates
├── localization
├── error handling
├── integer types and limits
├── variadic arguments
├── atomic operations
├── threads and synchronization
└── Unicode character processing
This does not mean that the standard library is special syntax built into the language. Most facilities are declared in headers and provided by an implementation library or compiler built-ins.
#include <stdio.h>
int main(void) {
printf("hello\n");
return 0;
}
printf is a standard library function declared in <stdio.h>, not a C syntax element.
By contrast, sizeof, return, and struct are language syntax.
size_t size = sizeof(int);
Headers
Standard library declarations are made available to programs through standard headers.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
The standard does not require headers to be implemented as actual text files in a file system. An implementation may provide header contents through an internal compiler database or another mechanism.
A header can declare or define the following elements.
- Functions
- Objects
- Macros
- Types
- Enumeration constants
- Structures and
uniontypes - Implementation limits
#include <stddef.h>
size_t size;
ptrdiff_t difference;
nullptr_t null_value;
Headers should generally be included at positions where external declarations or definitions are permitted.
Classification of Standard Headers
The C23 standard library headers can be grouped by function as follows.
Common Types and Language Support
| Header | Major facilities |
|---|---|
<stddef.h> | size_t, ptrdiff_t, nullptr_t, offsetof |
<stdint.h> | Exact-width and minimum-width integer types |
<inttypes.h> | Integer format specifiers and conversion functions |
<limits.h> | Ranges of integer types |
<float.h> | Characteristics of floating-point types |
<stdbool.h> | Boolean compatibility names |
<stdalign.h> | Alignment compatibility names |
<stdnoreturn.h> | Compatibility name for non-returning functions |
<stdarg.h> | Variadic argument processing |
<iso646.h> | Alternative operator spellings |
Diagnostics and Errors
| Header | Major facilities |
|---|---|
<assert.h> | Runtime diagnostic macro |
<errno.h> | Library error state |
<setjmp.h> | Nonlocal execution transfer |
<signal.h> | Signal handling |
Characters and Strings
| Header | Major facilities |
|---|---|
<ctype.h> | Narrow-character classification and conversion |
<string.h> | String and memory-block processing |
<wchar.h> | Wide strings and multibyte characters |
<wctype.h> | Wide-character classification and conversion |
<uchar.h> | UTF-family character conversion |
Input/Output and Execution Environment
| Header | Major facilities |
|---|---|
<stdio.h> | File and stream input/output |
<stdlib.h> | Memory allocation, conversion, sorting, termination, and searching |
<time.h> | Date and time |
<locale.h> | Localization |
<threads.h> | Threads and synchronization |
Numerical Computation
| Header | Major facilities |
|---|---|
<math.h> | Real-number mathematical functions |
<complex.h> | Complex numbers |
<tgmath.h> | Type-generic mathematics |
<fenv.h> | Floating-point environment |
<stdbit.h> | Integer bit operations |
Concurrency
| Header | Major facilities |
|---|---|
<stdatomic.h> | Atomic types and operations |
<threads.h> | Threads, mutexes, and condition variables |
The headers and facilities available differ according to the standard edition. For example, <stdint.h> was added in C99, <stdatomic.h> and <threads.h> in C11, and <stdbit.h> in C23.[66]
Repeated Header Inclusion
A standard header can generally be included multiple times within one translation unit.
#include <stddef.h>
#include <stddef.h>
The implementation must provide it in a manner that prevents duplicate declarations from conflicting.
Project headers generally use include guards.
#ifndef TECHPEDIA_BUFFER_H
#define TECHPEDIA_BUFFER_H
#include <stddef.h>
struct buffer {
void *data;
size_t size;
};
#endif
#pragma once is also widely supported, but it is not an ISO C standard feature.
Reserved Identifiers
The standard library reserves some identifiers for implementations and future extensions.
Representative reservation rules include the following.
- Identifiers beginning with two underscores
- Identifiers beginning with an underscore followed by an uppercase letter
- Identifiers beginning with an underscore at file scope
- External function names from the standard library
- Macros and names reserved by particular headers
- Names reserved for future library extensions
int __internal_value;
int _SystemValue;
A program must not define such names.
int internal_value;
int system_value;
A program must also not define a standard function name as another external function.
int malloc(int size) {
return size;
}
malloc is reserved as an external identifier of the standard library.
Even if the corresponding header has not been included, names of standard library external functions are reserved as identifiers with external linkage. The C23 standard also defines additional macro and file-scope identifier reservation rules that apply when particular headers are included.[67]
Library Functions and Macros
A standard library name may also be defined as a macro even when it appears to be a function.
#include <ctype.h>
int result = isalpha(character);
An implementation can provide isalpha as an efficient macro.
In special cases where access to the actual function is required, some names permit macro expansion to be suppressed by enclosing the name in parentheses.
int result = (isalpha)(character);
However, the same behavior must not be assumed for every standard interface. The specification of each header and function must be checked.
Arbitrarily applying #undef to or redefining a function-like standard library macro can change program meaning and portability.
#undef isalpha
Manipulation outside the range permitted by the standard should be avoided.
<assert.h>
<assert.h> provides the assert macro for checking program preconditions during execution.
#include <assert.h>
int divide(int left, int right) {
assert(right != 0);
return left / right;
}
When the condition is false, diagnostic information is printed and abnormal termination equivalent to abort occurs.
When NDEBUG is defined, assert checks are disabled.
#define NDEBUG
#include <assert.h>
Side effects that must occur even in builds where assertions are disabled must not be placed inside assert.
assert(initialize_system());
When NDEBUG is defined, the initialization function call itself may disappear.
bool initialized = initialize_system();
assert(initialized);
assert is not a general mechanism for handling errors in external input. It is suitable for internal program invariants and defect detection during development.
<ctype.h>
<ctype.h> provides narrow-character classification and case-conversion facilities.
#include <ctype.h>
int count_digits(const char *text) {
int count = 0;
while (*text != '\0') {
if (isdigit((unsigned char)*text)) {
++count;
}
++text;
}
return count;
}
Representative functions include the following.
isalnum
isalpha
isblank
iscntrl
isdigit
isgraph
islower
isprint
ispunct
isspace
isupper
isxdigit
tolower
toupper
The argument to a character-classification function must be EOF or a value representable as unsigned char. Passing a negative char value directly on an implementation where plain char is signed can cause undefined behavior.
isalpha((unsigned char)character);
Character-classification results can be affected by the current locale setting.
<errno.h>
<errno.h> provides errno and error macros used by some library functions to report failure causes.
#include <errno.h>
#include <stdlib.h>
bool parse_number(
const char *text,
long *result
) {
char *end;
errno = 0;
long value = strtol(text, &end, 10);
if (end == text) {
return false;
}
if (errno == ERANGE) {
return false;
}
*result = value;
return true;
}
errno is not guaranteed to be set to zero automatically when a function succeeds. For functions whose errors must be checked through errno, it is set to zero before the call and checked together with the return value.
check return value
+
check errno
The value of errno is meaningful only when an error has actually been reported.
<float.h>
<float.h> provides macros describing the range, precision, and rounding characteristics of floating-point types.
#include <float.h>
#include <stdio.h>
int main(void) {
printf("%d\n", FLT_DIG);
printf("%d\n", DBL_DIG);
printf("%d\n", LDBL_DIG);
return 0;
}
Representative macros include the following.
FLT_MIN
FLT_MAX
FLT_EPSILON
FLT_DIG
DBL_MIN
DBL_MAX
DBL_EPSILON
DBL_DIG
LDBL_MIN
LDBL_MAX
LDBL_EPSILON
LDBL_DIG
It must not be assumed that every C implementation uses the same IEEE 754 formats. C23 can indicate IEC 60559 binding features through conditional macros.
<limits.h>
<limits.h> describes the ranges of fundamental integer types.
#include <limits.h>
static_assert(CHAR_BIT >= 8);
Representative macros include the following.
CHAR_BIT
CHAR_MIN
CHAR_MAX
SCHAR_MIN
SCHAR_MAX
UCHAR_MAX
SHRT_MIN
SHRT_MAX
USHRT_MAX
INT_MIN
INT_MAX
UINT_MAX
LONG_MIN
LONG_MAX
ULONG_MAX
LLONG_MIN
LLONG_MAX
ULLONG_MAX
These macros and <stdint.h> should be used instead of assuming fixed integer sizes.
<stddef.h>
<stddef.h> provides types and macros commonly used by multiple library and language facilities.
Representative elements include the following.
size_t
ptrdiff_t
max_align_t
nullptr_t
NULL
offsetof
size_t is used for object sizes and array element counts.
#include <stddef.h>
size_t count = 10;
The difference between pointers within the same array is represented by ptrdiff_t.
ptrdiff_t distance = end - begin;
The offset of a structure member is obtained using offsetof.
#include <stddef.h>
struct packet {
unsigned int type;
unsigned int size;
};
static_assert(
offsetof(struct packet, size) >=
sizeof(unsigned int)
);
C23 also provides nullptr_t through <stddef.h>.
nullptr_t null_value = nullptr;
<stdint.h>
<stdint.h> provides integer types with specified width and range characteristics.
#include <stdint.h>
uint8_t byte;
int32_t coordinate;
uint64_t identifier;
The types are divided into the following categories.
exact width
int8_t
uint8_t
int16_t
uint16_t
int32_t
uint32_t
int64_t
uint64_t
minimum width
int_least32_t
uint_least32_t
fastest type
int_fast32_t
uint_fast32_t
pointer-convertible types
intptr_t
uintptr_t
maximum width
intmax_t
uintmax_t
Exact-width types are defined only when the implementation can provide the corresponding width without padding bits.
Integer constant macros are also provided.
uint64_t value = UINT64_C(10000000000);
intmax_t minimum = INTMAX_C(-1000);
<inttypes.h>
<inttypes.h> includes the types from <stdint.h> and provides integer format macros and conversion functions.
#include <inttypes.h>
#include <stdio.h>
int main(void) {
uint64_t identifier =
UINT64_C(123456789);
printf(
"%" PRIu64 "\n",
identifier
);
return 0;
}
Directly using %llu under the assumption that uint64_t is always the same as unsigned long long may not be portable.
Representative format macros include the following.
PRId32
PRIu32
PRIx32
PRId64
PRIu64
SCNd32
SCNu64
It also provides absolute-value, division, and string-conversion functions for maximum-width integers.
<stdarg.h>
<stdarg.h> processes the argument list of a variadic function.
#include <stdarg.h>
int sum_values(size_t count, ...) {
va_list arguments;
int sum = 0;
va_start(arguments, count);
for (size_t index = 0;
index < count;
++index) {
sum += va_arg(arguments, int);
}
va_end(arguments);
return sum;
}
The individual parameter types in the variadic portion cannot be checked through the function prototype. The caller and function must agree precisely on the number and types of arguments.
int result = sum_values(
3,
10,
20,
30
);
Default argument promotions also apply.
float
→ double
char, short
→ int or unsigned int
Using va_arg with the wrong type can cause undefined behavior.
<stdio.h>
<stdio.h> provides file- and stream-based input and output.
Representative types and objects include the following.
FILE
fpos_t
stdin
stdout
stderr
EOF
Major function categories include the following.
opening and closing files
fopen
freopen
fclose
character input and output
fgetc
fputc
getchar
putchar
string input and output
fgets
fputs
formatted input and output
printf
fprintf
snprintf
scanf
fscanf
sscanf
binary input and output
fread
fwrite
file positioning
fseek
ftell
rewind
fgetpos
fsetpos
status and buffering
fflush
feof
ferror
clearerr
setbuf
setvbuf
file management
remove
rename
tmpfile
tmpnam
A simple file copy can be written as follows.
#include <stdio.h>
int copy_file(
const char *source_path,
const char *destination_path
) {
int result = -1;
FILE *source = NULL;
FILE *destination = NULL;
unsigned char buffer[4096];
source = fopen(source_path, "rb");
if (source == NULL) {
goto cleanup;
}
destination = fopen(
destination_path,
"wb"
);
if (destination == NULL) {
goto cleanup;
}
while (!feof(source)) {
size_t count = fread(
buffer,
1,
sizeof(buffer),
source
);
if (
count > 0 &&
fwrite(
buffer,
1,
count,
destination
) != count
) {
goto cleanup;
}
if (ferror(source)) {
goto cleanup;
}
}
result = 0;
cleanup:
if (destination != NULL) {
fclose(destination);
}
if (source != NULL) {
fclose(source);
}
return result;
}
feof does not predict whether the next read will reach the end. It is checked after a read function encounters the end of the file and fails.
Text and Binary Streams
C distinguishes between text streams and binary streams.
FILE *text_file =
fopen("document.txt", "r");
FILE *binary_file =
fopen("image.bin", "rb");
On some UNIX-like systems, the two modes behave almost identically, but the C standard permits text mode to translate line endings and particular characters into the external representation of the execution environment.
On Windows-family implementations, newline conversion can occur in text mode.
Portable binary file processing uses the b mode.
FILE *file = fopen(path, "wb");
However, directly writing a structure with fwrite does not create a platform-independent file format.
fwrite(
&header,
sizeof(header),
1,
file
);
Structure padding, endianness, and type sizes affect the result, so fixed formats must be explicitly serialized.
Formatted Output
The printf family converts values to characters according to a format string.
#include <stdio.h>
int main(void) {
int count = 10;
double ratio = 0.75;
printf(
"count=%d ratio=%.2f\n",
count,
ratio
);
return 0;
}
The format specifier must match the actual argument type.
size_t count = 10;
printf("%zu\n", count);
The following can be incorrect because size_t is not guaranteed to be the same as unsigned int.
printf("%u\n", count);
Pointers use %p and void *.
printf("%p\n", (void *)pointer);
If the format string comes from external input, a format-string vulnerability can occur.
printf(user_input);
It must be output as data.
printf("%s", user_input);
snprintf
snprintf limits the size of the output buffer.
char buffer[128];
int length = snprintf(
buffer,
sizeof(buffer),
"value=%d",
value
);
The return value is the number of characters that would have been written if sufficient space had been available, excluding the null character.
if (length < 0) {
return false;
}
if ((size_t)length >= sizeof(buffer)) {
return false;
}
If the buffer is too small, the output can be truncated, but when possible it remains null-terminated.
<string.h>
<string.h> processes null-terminated strings and general memory blocks.
String functions include the following.
strlen
strcmp
strncmp
strcpy
strncpy
strcat
strncat
strchr
strrchr
strstr
strspn
strcspn
strpbrk
strtok
strcoll
strxfrm
strdup
strndup
Memory functions include the following.
memcpy
memmove
memset
memcmp
memchr
memccpy
String length is obtained with strlen.
size_t length = strlen(text);
It cannot be used on a byte array without a null terminator.
char bytes[4] = {
't',
'e',
's',
't'
};
strlen(bytes);
The function can search beyond the array bounds for a null character and cause an invalid access.
memcpy and memmove
memcpy copies between non-overlapping memory regions.
memcpy(
destination,
source,
size
);
If the regions overlap, behavior is undefined.
memcpy(
buffer + 1,
buffer,
count
);
When overlap is possible, memmove is used.
memmove(
buffer + 1,
buffer,
count
);
The size passed to memcpy and memmove is a number of bytes rather than a number of elements.
memcpy(
destination,
source,
count * sizeof(*destination)
);
Multiplication overflow must also be checked.
<stdlib.h>
<stdlib.h> provides a variety of general-purpose facilities.
Major categories include the following.
numeric conversion
atoi
strtol
strtoul
strtod
dynamic memory
malloc
calloc
realloc
free
aligned_alloc
program termination
abort
exit
quick_exit
_Exit
atexit
at_quick_exit
environment
getenv
system
searching and sorting
bsearch
qsort
integer operations
abs
labs
llabs
div
ldiv
lldiv
multibyte characters
mblen
mbtowc
wctomb
Numeric Conversion
The atoi family cannot report errors in detail, so the strtol family is more suitable for input that requires validation.
#include <errno.h>
#include <limits.h>
#include <stdlib.h>
bool parse_int(
const char *text,
int *result
) {
char *end;
errno = 0;
long value = strtol(
text,
&end,
10
);
if (end == text) {
return false;
}
if (*end != '\0') {
return false;
}
if (errno == ERANGE) {
return false;
}
if (
value < INT_MIN ||
value > INT_MAX
) {
return false;
}
*result = (int)value;
return true;
}
Numeric input must distinguish the following cases.
- Whether no digits were found
- Whether unnecessary trailing characters remain
- Whether the representable range was exceeded
- Whether a sign is permitted
- Which radix is used
Dynamic Memory Management
A dynamic array can be allocated as follows.
#include <stdlib.h>
int *values = malloc(
count * sizeof(*values)
);
if (values == NULL) {
return false;
}
The size multiplication can overflow.
if (
count >
SIZE_MAX / sizeof(*values)
) {
return false;
}
The memory is released after use.
free(values);
values = NULL;
calloc accepts an element count and element size and provides storage whose bytes are all zero.
int *values = calloc(
count,
sizeof(*values)
);
The result of realloc is checked through a temporary pointer.
int *new_values = realloc(
values,
new_count *
sizeof(*new_values)
);
if (new_values == NULL) {
return false;
}
values = new_values;
Aligned Allocation
aligned_alloc allocates memory satisfying a specified alignment.
#include <stdlib.h>
size_t alignment = 64;
size_t size = 256;
void *memory = aligned_alloc(
alignment,
size
);
The size must be an integer multiple of the alignment.
if (size % alignment != 0) {
return NULL;
}
C23 expanded memory-alignment interfaces and requires selected alignment facilities even in freestanding implementations.
qsort
qsort is a general-purpose array sorting function.
#include <stdlib.h>
static int compare_int(
const void *left_pointer,
const void *right_pointer
) {
const int left =
*(const int *)left_pointer;
const int right =
*(const int *)right_pointer;
return (left > right) -
(left < right);
}
void sort_values(
int *values,
size_t count
) {
qsort(
values,
count,
sizeof(*values),
compare_int
);
}
A simple subtraction such as the following can overflow.
return left - right;
The comparison function only needs to return a negative value, zero, or a positive value according to the ordering relation.
qsort does not guarantee a stable sort. When the relative order of elements with equal keys must be preserved, another sorting algorithm is required.
bsearch
bsearch performs a binary search on a sorted array.
int key = 20;
int *found = bsearch(
&key,
values,
count,
sizeof(*values),
compare_int
);
if (found != NULL) {
printf("%d\n", *found);
}
The array must already be sorted according to the same comparison function.
Program Termination
Normal termination can use return or exit.
#include <stdlib.h>
int main(void) {
return EXIT_SUCCESS;
}
void fatal_error(void) {
exit(EXIT_FAILURE);
}
atexit registers a function to run during normal termination.
#include <stdlib.h>
static void cleanup(void) {
release_global_resources();
}
int main(void) {
if (atexit(cleanup) != 0) {
return EXIT_FAILURE;
}
return application_run();
}
abort, quick_exit, and _Exit have different termination procedures. They must not be assumed to perform the same resource cleanup as ordinary normal termination.
<math.h>
<math.h> provides real-number mathematical functions.
Representative functions include the following.
trigonometric functions
sin
cos
tan
asin
acos
atan
atan2
exponential and logarithmic functions
exp
exp2
exp10
log
log2
log10
powers and roots
pow
sqrt
cbrt
hypot
rounding
ceil
floor
trunc
round
nearbyint
absolute value and remainder
fabs
fmod
remainder
classification
isfinite
isinf
isnan
isnormal
signbit
Each fundamental function may have float, double, and long double variants.
float first = sqrtf(4.0F);
double second = sqrt(4.0);
long double third = sqrtl(4.0L);
Mathematical functions can report domain and range errors. Return values, errno, and floating-point exceptions must be checked according to the specification of each function.
#include <errno.h>
#include <math.h>
errno = 0;
double value = sqrt(-1.0);
if (errno == EDOM) {
/* Domain error */
}
In environments supporting IEC 60559, additional rules for NaN and floating-point exceptions may apply.
<complex.h>
<complex.h> provides complex-number operations.
#include <complex.h>
#include <stdio.h>
int main(void) {
double complex value =
1.0 + 2.0 * I;
printf(
"%f %f\n",
creal(value),
cimag(value)
);
return 0;
}
Representative functions include the following.
cabs
carg
creal
cimag
conj
cproj
cexp
clog
cpow
csqrt
csin
ccos
ctan
Complex-number support can be treated as an optional facility depending on the implementation and standard edition.
<tgmath.h>
<tgmath.h> provides type-generic macros that select mathematical functions appropriate for the argument type.
#include <tgmath.h>
float first = sqrt(4.0F);
double second = sqrt(4.0);
long double third = sqrt(4.0L);
The same name is used, but according to the argument type it can correspond to sqrtf, sqrt, sqrtl, and related functions.
Because macro expansion and complex types are also involved, explicit functions may be selected in public APIs and debugging contexts.
<fenv.h>
<fenv.h> controls floating-point exceptions and rounding modes.
#include <fenv.h>
int calculate_upward(void) {
int previous = fegetround();
if (fesetround(FE_UPWARD) != 0) {
return -1;
}
double value = perform_calculation();
fesetround(previous);
consume(value);
return 0;
}
A pragma may be required to prevent the compiler from ignoring access to the floating-point environment.
#pragma STDC FENV_ACCESS ON
Complete support and meaning of the floating-point environment can depend on whether the implementation provides IEC 60559 binding features.
<time.h>
<time.h> provides calendar time, processor time, and time-conversion facilities.
Representative types and functions include the following.
time_t
clock_t
struct tm
timespec
time
clock
difftime
mktime
strftime
gmtime
localtime
timespec_get
The current calendar time can be obtained as follows.
#include <stdio.h>
#include <time.h>
int main(void) {
time_t now = time(NULL);
if (now == (time_t)-1) {
return 1;
}
struct tm *local =
localtime(&now);
if (local == NULL) {
return 1;
}
char buffer[64];
if (
strftime(
buffer,
sizeof(buffer),
"%Y-%m-%d %H:%M:%S",
local
) == 0
) {
return 1;
}
puts(buffer);
return 0;
}
localtime and gmtime may reuse the same static object, so care is required with consecutive calls and multithreaded use. C23 standardized reentrant variants.
struct tm result;
if (localtime_r(&now, &result) == NULL) {
return false;
}
The standard does not fix the unit and representation of time_t to a simple integer number of seconds.
<locale.h>
<locale.h> controls localization settings for character classification, numbers, dates, collation, and related facilities.
#include <locale.h>
int main(void) {
if (setlocale(LC_ALL, "") == NULL) {
return 1;
}
return 0;
}
Major categories include the following.
LC_ALL
LC_COLLATE
LC_CTYPE
LC_MONETARY
LC_NUMERIC
LC_TIME
The default locale at program startup is the "C" locale.
setlocale(LC_ALL, "C");
An empty string requests the locale settings of the execution environment.
setlocale(LC_ALL, "");
Locale settings can be global state, so care is required in programs where multiple libraries and threads use them together.
<wchar.h> and <wctype.h>
<wchar.h> provides wide characters, multibyte string conversion, wide input/output, and wide-string facilities.
#include <wchar.h>
wchar_t text[] = L"TechPedia";
size_t length = wcslen(text);
Representative functions include the following.
wcslen
wcscpy
wcscmp
wmemcpy
wprintf
fwprintf
fgetwc
fputwc
mbrtowc
wcrtomb
mbsrtowcs
wcsrtombs
The size and encoding of wchar_t can differ across platforms.
some Windows environments
wchar_t → 16-bit family
many UNIX-like systems
wchar_t → 32-bit family
Therefore, it must not be generalized that wchar_t always represents UTF-16 or UTF-32.
<wctype.h> provides wide-character classification and conversion.
#include <wctype.h>
if (iswalpha(character)) {
character = towlower(character);
}
<uchar.h>
<uchar.h> provides conversion facilities between UTF-family character types and multibyte characters.
#include <uchar.h>
char16_t character16;
char32_t character32;
Representative conversion functions include the following.
mbrtoc8
c8rtomb
mbrtoc16
c16rtomb
mbrtoc32
c32rtomb
String encodings, code units, and Unicode code points must not be treated as the same concept.
UTF-8
→ one character can use multiple 8-bit code units
UTF-16
→ some characters use surrogate pairs
UTF-32
→ one code unit represents one code point
<stdbit.h>
C23 <stdbit.h> provides bit-related operations for unsigned integers.
Major facilities include the following.
number of leading zero bits
number of trailing zero bits
number of one bits
first leading one bit
first trailing one bit
bit width
whether a value contains a single bit
next and previous powers of two
#include <stdbit.h>
unsigned int value = 0b0011'0000;
unsigned int ones =
stdc_count_ones(value);
unsigned int width =
stdc_bit_width(value);
It provides standardized interfaces for functionality that was previously implemented with explicit loops and bit operations. An implementation can use bit instructions from the target CPU.
<stdatomic.h>
<stdatomic.h> provides atomic types, operations, and memory orders.
#include <stdatomic.h>
static atomic_uint counter = 0;
void increment(void) {
atomic_fetch_add(
&counter,
1
);
}
Representative facilities include the following.
atomic_init
atomic_load
atomic_store
atomic_exchange
atomic_compare_exchange_strong
atomic_compare_exchange_weak
atomic_fetch_add
atomic_fetch_sub
atomic_fetch_or
atomic_fetch_xor
atomic_fetch_and
atomic_flag
atomic_thread_fence
atomic_signal_fence
An explicit memory order can be specified.
atomic_store_explicit(
&ready,
true,
memory_order_release
);
bool value = atomic_load_explicit(
&ready,
memory_order_acquire
);
Using atomic operations alone does not automatically make a complete data structure consisting of multiple objects safe. Object lifetime, memory reclamation, and invariants must also be designed.
<threads.h>
<threads.h> provides standard C threads and synchronization facilities.
Major elements include the following.
thrd_t
thrd_create
thrd_join
thrd_detach
thrd_current
thrd_sleep
thrd_yield
mtx_t
mtx_init
mtx_lock
mtx_trylock
mtx_unlock
mtx_destroy
cnd_t
cnd_init
cnd_wait
cnd_signal
cnd_broadcast
cnd_destroy
once_flag
call_once
tss_t
tss_create
tss_get
tss_set
tss_delete
A simple thread can be created as follows.
#include <stdio.h>
#include <threads.h>
static int worker(void *context) {
int value = *(int *)context;
printf("%d\n", value);
return value;
}
int main(void) {
int value = 10;
int result;
thrd_t thread;
if (
thrd_create(
&thread,
worker,
&value
) != thrd_success
) {
return 1;
}
if (
thrd_join(
thread,
&result
) != thrd_success
) {
return 1;
}
return result == 10 ? 0 : 1;
}
Not every operating system provides <threads.h> directly through its native thread API. An implementation may use another runtime or wrapper, and support may be limited in some environments.
Mutexes
Shared state can be protected with a mutex.
#include <threads.h>
struct counter {
mtx_t mutex;
int value;
};
bool counter_increment(
struct counter *counter
) {
if (
mtx_lock(
&counter->mutex
) != thrd_success
) {
return false;
}
++counter->value;
mtx_unlock(&counter->mutex);
return true;
}
The mutex must be unlocked on every execution path after it has been locked.
Condition Variables
A condition variable suspends a thread until state changes.
mtx_lock(&queue->mutex);
while (queue->count == 0) {
cnd_wait(
&queue->changed,
&queue->mutex
);
}
consume_queue_item(queue);
mtx_unlock(&queue->mutex);
The fact that a condition-variable wait has returned does not guarantee that the condition is true. Another thread may have changed the state first, or a spurious wakeup may have occurred, so the condition is checked again in a loop.
<setjmp.h>
<setjmp.h> provides nonlocal jumps that transfer execution to an earlier point without following ordinary function returns.
#include <setjmp.h>
static jmp_buf environment;
void fail(void) {
longjmp(environment, 1);
}
int main(void) {
if (setjmp(environment) == 0) {
fail();
return 0;
}
return 1;
}
setjmp returns 0 during its initial call and behaves as though it returned a nonzero value after control returns through longjmp.
Nonlocal jumps complicate the values of automatic objects, resource cleanup, and control flow. They must not cross C++ destructor boundaries or other language runtime boundaries, and explicit return values are often more appropriate for ordinary error handling.
<signal.h>
<signal.h> provides a minimal interface for signals and asynchronous event handling.
#include <signal.h>
static volatile sig_atomic_t
interrupted;
static void handle_signal(int signal) {
interrupted = signal;
}
int main(void) {
signal(SIGINT, handle_signal);
while (!interrupted) {
perform_work();
}
return 0;
}
Representative signal macros include the following.
SIGABRT
SIGFPE
SIGILL
SIGINT
SIGSEGV
SIGTERM
The ISO C signal model does not specify the complete operating-system-specific signal system. POSIX separately provides more signals, functions, and asynchronous-signal-safety rules.
Functions callable and objects accessible within a signal handler are strictly restricted.
<iso646.h>
<iso646.h> provides macros expressing some operators through alternative words.
#include <iso646.h>
if (ready and not failed) {
flags = flags or ENABLED;
}
They correspond to the following operators.
| Alternative spelling | Operator | | | | -------------------- | -------- | -- | - | | and | && | | | | and_eq | &= | | | | bitand | & | | | | bitor | | | | | compl | ~ | | | | not | ! | | | | not_eq | != | | | | or | | | | | or_eq | | = | | | xor | ^ | | | | xor_eq | ^= | | |
This facility exists for compatibility with environments that used restricted character sets, while modern code often uses symbolic operators.
Contracts of Library Functions
Standard library functions impose preconditions on the arguments and objects they receive.
memcpy(
destination,
source,
size
);
The caller must guarantee the following.
- The destination pointer is valid.
- The source pointer is valid.
- The specified number of bytes is accessible.
- Source and destination do not overlap in a prohibited manner.
- Object lifetimes are valid.
Violating a precondition can cause undefined behavior rather than causing the function to return an error code.
memcpy(
NULL,
source,
10
);
The C standard library generally does not perform runtime validation of every pointer and array bound.
restrict and the Library
Several standard function prototypes use restrict.
void *memcpy(
void *restrict destination,
const void *restrict source,
size_t count
);
This expresses a contract requiring the caller to follow aliasing rules during execution of the function.
For overlapping regions, memmove is used.
memmove(
destination,
source,
count
);
Thread Safety
Since C11, the standard library has included rules concerning use from multiple threads and data races.
In general, library functions must not create unnecessary data races when internally accessing distinct objects. However, when the caller writes to the same user object concurrently without synchronization, the program can contain a data race.
static char buffer[1024];
int first_worker(void *context) {
strcpy(buffer, "first");
return 0;
}
int second_worker(void *context) {
strcpy(buffer, "second");
return 0;
}
If the two threads execute concurrently, a data race can occur on buffer.
The existence of a library function does not automatically synchronize the caller’s shared data.
Localization and Global State
The following standard library facilities can involve global or shared state.
setlocalelocaleconverrno- Random-number state
- Standard input and output streams
- Internal objects used by time-conversion functions
- Signal handling
- Environment variables
Modern implementations may provide errno as thread-local state, but a program must use the errno macro provided by the header.
errno = 0;
It must not be declared directly as an external global variable.
extern int errno;
An implementation may provide it through a thread-local accessor function and macro.
Random Numbers
rand and srand from <stdlib.h> provide a pseudorandom-number interface.
#include <stdlib.h>
srand(1234);
int value = rand();
rand is not suitable for the following purposes.
- Cryptographic keys
- Session tokens
- Security nonces
- Password-reset codes
- Values that must not be predictable by an attacker
The C standard does not provide a cryptographically secure random-number generator. Security-sensitive programs must use operating system facilities or a verified cryptographic library.
The algorithm and quality of rand, as well as RAND_MAX, can differ among implementations.
Facilities Not Included in the Standard
The C standard library is not a library that abstracts an entire operating system.
The following facilities are not included in the ISO C standard library.
- Socket networking
- Directory traversal
- Dynamic library loading
- Process creation and control
- Virtual memory mapping
- Graphics
- Audio
- Input devices
- GUI
- File permissions
- Asynchronous input/output
- Operating system event loops
- Cryptography
- Compression
- Databases
- HTTP
These facilities are provided by operating system APIs, other international standards, and external libraries.
ISO C
→ language and fundamental library
POSIX
→ UNIX-like operating system interfaces
Win32
→ Windows platform interfaces
external libraries
→ graphics, networking, cryptography, and others
Difference from POSIX
POSIX specifies UNIX-like operating system interfaces based on the C language, but it is a separate standard from ISO C.
Representative POSIX facilities include the following.
unistd.h
pthread.h
dirent.h
sys/socket.h
sys/mman.h
dlfcn.h
The following is a POSIX program.
#include <unistd.h>
int main(void) {
const char message[] = "hello\n";
write(
STDOUT_FILENO,
message,
sizeof(message) - 1
);
return 0;
}
write and <unistd.h> are not ISO C standard library facilities.
Projects requiring portability can separate common ISO C code from platform-specific code.
common layer
→ ISO C
platform layer
├── POSIX
├── Win32
└── console and embedded APIs
Standard Library Implementations
The standard library does not refer to one particular codebase. Multiple implementations provide the same standard interfaces.
Representative implementations include the following.
glibc
musl
Microsoft Universal CRT
Apple libc
FreeBSD libc
Newlib
picolibc
WASI libc
compiler- and platform-specific runtimes
Each implementation provides the same ISO C interfaces while differing in the following areas.
- Supported C standard editions
- Optional features
- Extension functions
- Performance
- Thread implementation
- Files and localization
- ABI
- Dynamic linking
- Licensing
- Freestanding support
- Suitability for embedded environments
A compiler and standard library can be independent components.
Clang
+
glibc
Clang
+
musl
Clang
+
Microsoft CRT
GCC
+
Newlib
The same compiler can use different headers and libraries depending on the target platform and sysroot.
Compiler Built-in Functions
A standard library function is not always implemented as an actual external function call.
memcpy(
destination,
source,
16
);
The compiler can transform this directly into machine instructions, vector instructions, or an inline copy.
memcpy call
↓ optimization
direct load/store instructions
The implementation method is unrestricted as long as the observable behavior required by the standard remains the same.
The compiler can also recognize sqrt, strlen, memset, and other functions as built-ins. When a function address is used or particular compilation options are applied, an actual library call can remain.
Optional Features
Some C features may be provided conditionally by implementations.
Representative conditional features include the following.
- IEC 60559 floating point
- Complex numbers
- Whether atomic operations are lock-free
- Standard threads
- Particular character encodings
- Exact-width integer types
- Variable-length arrays
- Bounds-checking interfaces
An implementation indicates support for some features through predefined macros.
#ifdef __STDC_NO_THREADS__
/* Standard threads are not provided. */
#endif
#ifdef __STDC_NO_ATOMICS__
/* Standard atomic operations are not provided. */
#endif
#ifdef __STDC_NO_VLA__
/* Variable-length array support is restricted. */
#endif
Selecting a standard edition alone must not be taken to mean that every optional feature is available.
Annex K
C11-family standards have included a conditional annex commonly called Annex K, defining bounds-checking interfaces.
Representative function names include the following.
memcpy_s
strcpy_s
strcat_s
sprintf_s
The feature can be made conditionally available through the __STDC_LIB_EXT1__ macro and a request macro.
#define __STDC_WANT_LIB_EXT1__ 1
#include <string.h>
However, Annex K has not been implemented consistently by all major C libraries, and functions with the same names in Microsoft environments can differ from the ISO specification.
Portable code must not assume that the feature exists, and support and actual contracts must be checked in implementation documentation.
Technical Specifications and Extensions
In addition to ISO/IEC 9899, WG14 and ISO have managed extensions for particular fields through separate technical reports and technical specifications.
Examples include the following.
- Embedded processor support
- IEC 60559 floating-point extensions
- Secure coding rules
- Bounds-checking interfaces
- New character types
- Mathematical and parallel-computing extensions
Not all of these documents have the same status as mandatory features of the base C standard. Some are later integrated into or superseded by a standard edition, while others remain separate or are withdrawn.[68]
Defect Reports
When ambiguity or an error is discovered in the wording of a published standard, a defect report can be submitted to WG14.
standard published
↓
problem discovered during implementation or use
↓
defect report
↓
WG14 review
↓
response, interpretation, or correction in a future edition
A defect report can be reflected in the next standard edition or a technical corrigendum. Separate issue lists are maintained for C90, C99, C11 and C17, and C23.[69]
Committee drafts and defect reports are important resources for understanding the interpretation of the standard, but they must not be simplified as having exactly the same legal or normative status as the published ISO standard itself.
Normative Documents and Public Drafts
The official ISO standard is distributed by ISO. WG14 also publishes public working drafts produced during development.
N3220, the final public working document for C23, is very close in content to ISO/IEC 9899:2024 and is widely used to inspect language and library rules. However, its cover explicitly identifies it as a working draft, so it must be distinguished from the official standard document.[70]
official normative document
→ ISO/IEC 9899:2024
public working document
→ WG14 N3220
Formal conformance judgments and contracts should be based on the officially published document, while public drafts, rationale documents, and defect reports can be used for study and implementation comparison.
Standards and Implementation Extensions
A conforming implementation can provide extensions as long as they do not change the behavior of strictly conforming programs.
Representative extensions include the following.
- Inline assembly
- Vector types
- Function and variable attributes
- Statement expressions
- 128-bit integers
- Platform-specific calling conventions
- Section-placement directives
- Compiler built-in functions
- Extended pragmas
- Variable-sized structures
- Operating system headers
#if defined(__GNUC__)
__attribute__((always_inline))
#endif
static inline int square(int value) {
return value * value;
}
Extensions can be useful under the corresponding compiler, but may be unavailable in other implementations.
Standard and extension code can be separated conditionally.
#if defined(_MSC_VER)
#define FORCE_INLINE __forceinline
#elif defined(__GNUC__) || \
defined(__clang__)
#define FORCE_INLINE \
__attribute__((always_inline)) inline
#else
#define FORCE_INLINE inline
#endif
Conditional compilation alone does not automatically resolve semantic differences or ABI compatibility among extensions.
Standard Library and ABI
The C standard specifies function names, types, and semantics, but not the actual binary calling convention or library file names.
void *malloc(size_t size);
In an actual implementation, the following matters are determined by the ABI.
- Argument-passing registers
- Return-value location
- Symbol names
- Dynamic library format
- Actual underlying type of
size_t - Error-state storage method
- Symbol versions
- Calling convention
Libraries built for different ABIs may not be directly linkable even when they use the same C prototype.
ISO C function declaration
+
platform ABI
+
library implementation
=
actual callable interface
Version Compatibility
A new C standard does not guarantee that every program written for an older standard will be accepted completely unchanged.
For example, the following facilities have been removed or changed in meaning over time.
- Implicit
int - Implicit function declarations
- Old-style identifier-list function definitions
- Meaning of an empty function parameter list
gets- Some deprecated facilities
- Old preprocessing and declaration practices
/* Old C */
main() {
undeclared_function();
}
Modern C requires a return type and function declaration.
int declared_function(void);
int main(void) {
return declared_function();
}
Compiler warnings and migration documentation should be reviewed when adopting a newer standard.
Stability of the Standard Library
The C standard library provides a relatively small function-centered interface that has been maintained for a long time. For this reason, other languages, operating systems, and runtimes often use the C library and ABI as a common boundary.
However, older interfaces also contain aspects that are insufficient for modern safety requirements.
Representative problems include the following.
- Functions that do not receive array lengths separately
- Dependence on null-terminated strings
- Inconsistent error-reporting methods
- Global state
- Limited character-encoding model
- Dynamic allocation ownership not expressed in types
- Format strings and variadic arguments
- Lack of runtime bounds checking
strcpy(destination, source);
strcpy has no information about the size of the destination buffer.
The length must be checked in advance or a separate interface containing capacity information must be designed.
bool copy_text(
char *destination,
size_t capacity,
const char *source
) {
size_t length = strlen(source);
if (length >= capacity) {
return false;
}
memcpy(
destination,
source,
length + 1
);
return true;
}
Principles for Library Use
The following principles are necessary to use the standard library safely and portably.
- Include the required standard headers precisely.
- Do not rewrite function declarations manually.
- Check return values and error states.
- Manage array and memory sizes explicitly.
- Match format strings with argument types.
- Document object lifetimes and ownership.
- Guarantee null termination of strings.
- Check for overflow in integer size calculations.
- Distinguish the contracts of
memcpyandmemmove. - Pass valid arguments to character-classification functions.
- Consider the effects of localization and global state.
- Apply synchronization to objects shared among threads.
- Check optional features and implementation extensions conditionally.
- Distinguish ISO C from platform APIs.
Summary of the Standard and Standard Library
The international C standard does not treat the language syntax and standard library as separate products. Within ISO/IEC 9899, the language and library, translation and execution environments, implementation conformance, and program portability are specified together.
ISO/IEC 9899
├── C language
│ ├── types
│ ├── expressions
│ ├── declarations
│ ├── statements
│ ├── functions
│ └── preprocessing
│
├── execution model
│ ├── translation
│ ├── objects and memory
│ ├── program startup
│ └── program termination
│
├── conformance
│ ├── hosted implementations
│ ├── freestanding implementations
│ ├── strictly conforming programs
│ └── implementation-defined characteristics
│
└── standard library
├── common types and limits
├── input and output
├── strings and memory
├── numerical computation
├── time and localization
├── dynamic memory
├── atomic operations
└── threads and synchronization
The standard does not fix every computer as though it used identical hardware and operating systems. Instead, it distinguishes behavior that implementations must guarantee from behavior that implementations may select and document. This allows C to be implemented across systems ranging from small microcontrollers to desktops and servers, operating system kernels, and high-performance computing environments.
The standard library likewise does not abstract an entire operating system. It provides a portable common foundation for files, strings, memory, mathematics, time, and related facilities, while leaving networking, graphics, processes, and device control to platform APIs and external libraries.
The portability of a C program is not obtained merely by including standard headers. A program must obey the preconditions of each function, object lifetimes, type ranges, error-handling rules, optional features, and implementation-defined characteristics. The boundaries among ISO C, platform extensions, compiler dialects, and ABIs must be distinguished clearly.
The C standard and standard library have evolved by reflecting new hardware, execution environments, numerical computation, concurrency, character encodings, and safety requirements while preserving programs accumulated over decades. This conservative continuity and broad implementability are major reasons why C remains a common foundation for system interfaces and native software.
Implementations and Compilers
The C standard specifies program syntax and semantics, translation and execution environments, and standard library interfaces, but it does not prescribe a single concrete method for making them operate on actual machines. A C implementation must accept programs specified by the standard and provide the behavior required by the target environment. A compiler, preprocessor, assembler, linker, standard library, and runtime can together constitute one implementation.
In everyday usage, this entire system is often called a C compiler, but strictly speaking, a single compiler executable and a complete C implementation are not the same concept.
C implementation
├── compiler driver
├── C preprocessor
├── C front end
├── intermediate representation and optimizer
├── target code generator
├── assembler
├── linker
├── startup code
├── compiler runtime
└── C standard library
For example, even when Clang is used to build a Linux program, the following components can be combined.
Clang
→ C syntax and type analysis
LLVM
→ intermediate representation, optimization, and code generation
GNU as or the LLVM integrated assembler
→ object file generation
GNU ld, gold, or LLD
→ linking
glibc or musl
→ C standard library and operating system interfaces
compiler-rt or libgcc
→ compiler support runtime
Therefore, saying that a program was “built with Clang” does not mean that Clang independently provides the standard library and linker as well. Different components can be combined according to the target platform and toolchain configuration.
Implementations and Compilers
In the C standard, an implementation means the complete environment required to translate and execute C programs. An implementation can be either hosted or freestanding.
A hosted implementation generally provides the following elements.
- Program startup through
main - The complete C standard library
- Files and streams
- Normal program termination
- Integration with the execution environment and operating system
A freestanding implementation is used in environments such as the following.
- Operating system kernels
- Bootloaders
- Microcontrollers
- Real-time controllers
- Constrained embedded devices
- Programs with their own runtime
A freestanding environment may provide a compiler and only part of the standard library, or the user may directly implement startup code, memory allocation, and input and output.
Compiler Structure
A modern C compiler generally consists of the following stages.
C source
↓
front end
↓
intermediate representation
↓
optimizer
↓
back end
↓
assembly or machine code
Front End
The front end is the part that directly understands the C language.
Its major roles include the following.
- Processing preprocessing tokens
- Parsing syntax
- Type checking
- Analyzing declarations and scope
- Evaluating constant expressions
- Diagnosing constraint violations
- Constructing an abstract syntax tree
- Translating language semantics into an intermediate representation
int add(int left, int right) {
return left + right;
}
The front end analyzes this code and constructs information such as the following.
function name: add
return type: int
parameters: int, int
linkage: external linkage
body: return the result of adding two integers
Intermediate Representation
An intermediate representation is an internal form between the source language and the instructions of a particular CPU.
C
↓
GENERIC
↓
GIMPLE
↓
RTL
↓
machine code
GCC internally uses representations such as GENERIC, GIMPLE, and RTL. GCC’s internal documentation describes GIMPLE as one of its major intermediate representations.[71]
LLVM-based compilers use LLVM IR as a common intermediate representation.
define i32 @add(i32 %left, i32 %right) {
entry:
%result = add i32 %left, %right
ret i32 %result
}
An intermediate representation makes it possible to separate language analysis from target CPU code generation.
C front end ───────┐
C++ front end ─────┼→ common intermediate representation → multiple CPU back ends
Fortran front end ─┘
Optimizer
The optimizer transforms code while preserving the program’s observable meaning.
Representative optimizations include the following.
- Constant folding
- Dead-code elimination
- Function inlining
- Common-subexpression elimination
- Loop unrolling
- Loop vectorization
- Automatic vectorization
- Branch simplification
- Global constant propagation
- Alias analysis
- Interprocedural optimization
- Link-time optimization
int calculate(void) {
int value = 10 * 20;
return value;
}
The compiler can eliminate the runtime multiplication and produce a result such as the following.
int calculate(void) {
return 200;
}
Back End
The back end converts an intermediate representation into instructions for the target processor.
LLVM IR or GIMPLE
↓
instruction selection
↓
register allocation
↓
instruction scheduling
↓
machine code for x86-64, AArch64, RISC-V, or another target
A single C front end can support multiple CPUs because the back end is separated from it.
Compiler Drivers
Commands such as gcc, clang, and cl are generally compiler drivers. A driver coordinates preprocessing, compilation, assembly, and linker execution.
clang main.c -o application
Internally, the driver can perform tasks such as the following.
clang
├── run the C front end
├── perform LLVM optimization and code generation
├── run the assembler
├── add startup object files
├── add default libraries
├── run the linker
└── generate the executable file
GCC documentation describes the overall build process as preprocessing, compilation, assembly, and linking.[72]
Native and Cross Compilers
Generating a target for the same environment in which the compiler is currently running is called native compilation.
x86-64 Linux
↓ compilation
x86-64 Linux executable
Targeting another environment is called cross-compilation.
x86-64 Linux
↓ compilation
ARM Cortex-M firmware
A cross compiler requires the following information about the target system.
- Instruction set
- ABI
- Headers
- Standard library
- Startup code
- Linker script
- Object file format
- Sysroot
arm-none-eabi-gcc
aarch64-linux-gnu-gcc
x86_64-w64-mingw32-gcc
Self-Hosting
A C compiler written in C that can compile itself is called a self-hosting compiler.
existing compiler
↓
translate the source of a new C compiler
↓
new compiler executable
↓
translate its own source again
Moving a compiler to a new platform can require a bootstrap process.
stage 1 compiler
→ minimum functionality
stage 2 compiler
→ build the complete compiler using the stage 1 result
stage 3 compiler
→ compare and verify the results
In its normal build process, GCC can use several bootstrap stages to rebuild itself and compare the results.
The First C Compiler
The first C compiler was written by Dennis Ritchie for the PDP-11. C evolved from B by adding types, pointer arithmetic, structures, and a more general declaration system, and its compiler was modified alongside these early language changes.
The preserved last1120c and prestruct-c sources from Dennis Ritchie show the form of early C compilers from 1972 to 1973.
last1120c
→ early C before the introduction of structures
prestruct-c
→ C during the initial introduction of structures
The early compiler consisted of two major passes.
first pass
→ source analysis and tree construction
second pass
→ convert the tree to PDP-11 assembly
The early C compiler was not a modern multi-architecture compiler, but a tool for the PDP-11 and early UNIX development. However, the fact that the compiler itself was written in C demonstrated that C had matured enough to implement a compiler.[73]
Portable C Compiler
The Portable C Compiler is a portable C compiler developed by Stephen C. Johnson at Bell Labs. It is generally called PCC or pcc.
The earliest C compiler was closely tied to the PDP-11. PCC was designed to be easier to port to new architectures by separating machine-independent analysis from target-specific code generation.
C front end
↓
machine-independent intermediate structure
↓
target-specific code generator
PCC played an important role in spreading UNIX to multiple computers. Dennis Ritchie recorded that Stephen Johnson’s portable compiler became widely used.[74]
PCC was included in Version 7 UNIX and several other UNIX systems and was an important C compiler in early commercial UNIX and BSD environments. It was later replaced by GCC on many systems, but its separation of machine-independent and target-dependent compiler components had a major influence on compiler design.
The modern Portable C Compiler project is based on the original PCC but has extensively modified the code and supports C99, some features from later standards, and multiple targets.[75]
Amsterdam Compiler Kit
The Amsterdam Compiler Kit is a compiler toolset developed to support multiple languages and processors. It is generally called ACK.
ACK’s central idea was to translate source languages into a common intermediate language and then convert that representation into code for multiple machines.
C, Pascal, and Modula-2
↓
common intermediate code
↓
multiple processors
ACK was used in education and research as well as in the early MINIX toolchain. MINIX later gained support for GCC- and Clang-family toolchains, but ACK remains a historical example of a small and portable multilingual compiler architecture.
UNIX Vendor Compilers
As UNIX spread across the hardware products of multiple companies, each vendor provided its own C compiler.
Representative families included the following.
- AT&T C Compiler
- Sun C and SunPro C
- DEC C
- HP C
- IBM XL C
- SGI MIPSpro C
- Cray C
- SCO C
- Compilers dedicated to IRIX and AIX
These compilers not only translated ISO C, but also provided optimization for each company’s processors and operating systems, vector instructions and parallel processing, debuggers, and performance-analysis tools.
vendor C compiler
├── optimization for the vendor’s CPU
├── operating system ABI
├── system libraries
├── debugger
├── profiler
└── parallel and vector extensions
Before GCC became widely available, these vendor compilers occupied an important position in the workstation and server markets.
GCC
GCC is the compiler suite of the GNU Compiler Collection. It originally began as a C compiler named the GNU C Compiler, and was later expanded under the name GNU Compiler Collection as it gained front ends for C++, Objective-C, Fortran, Ada, and other languages.
The purpose of the GCC project was to provide a free optimizing compiler for free software operating systems. GCC became a central toolchain component of the GNU system and Linux ecosystem and is also widely used on several UNIX-like and embedded platforms.
GCC has a structure such as the following.
language front ends
├── C
├── C++
├── Objective-C
├── Fortran
├── Ada
└── other languages
↓
GENERIC
↓
GIMPLE
↓
optimization
↓
RTL
↓
target back end
GCC’s internal documentation describes language front ends, GENERIC, GIMPLE, RTL, and target-description structures as separate layers.[76]
GCC Characteristics
GCC’s major characteristics include the following.
- Support for a very wide range of CPU architectures
- Support for multiple operating systems and ABIs
- High-level optimization
- GNU C extensions
- OpenMP support
- Link-time optimization
- Automatic vectorization
- Warnings and static analysis
- Profile-guided optimization
- Extensive embedded toolchains
- Free software licensing
Representative targets include the following.
x86
x86-64
AArch64
Arm
RISC-V
PowerPC
IBM Z
MIPS
AVR
MSP430
LoongArch
multiple embedded processors
Supported targets differ according to the GCC version and distribution configuration.
GCC C Dialects
GCC provides ISO C standard modes and GNU extension modes.
-std=c90
-std=c99
-std=c11
-std=c17
-std=c23
Modes that also enable GNU extensions include the following.
-std=gnu90
-std=gnu99
-std=gnu11
-std=gnu17
-std=gnu23
GNU C extensions can include the following facilities.
- Statement expressions
typeof- Nested functions
- Computed
goto - Function and variable attributes
- Inline assembly
- Zero-length arrays
- Extended vector types
__int128- Built-in functions
- Section and visibility specifications
Some GNU extensions have later been standardized in similar forms in ISO C. For example, typeof had long been provided by GNU C and was introduced as a standard feature in C23. However, the detailed syntax and semantics of a GNU extension and a standard facility must not be assumed to be completely identical.
GCC Optimization
GCC performs machine-independent optimization based on GIMPLE and target-specific optimization based on RTL.
Representative features include the following.
-O1
-O2
-O3
-Os
-Ofast
-flto
-fprofile-generate
-fprofile-use
GCC supports automatic vectorization, loop optimization, function inlining, global optimization, and link-time optimization.
Current GCC Development
GCC regularly releases major versions and maintains multiple stabilization branches. According to GCC’s official announcement, GCC 16.1 was released on April 30, 2026.[77]
GCC version numbers do not correspond directly to C standard editions. To determine which C23 features a particular GCC version supports, its change log and C implementation status must be checked.
LLVM and Clang
LLVM is a collection of reusable compiler and toolchain technologies, while Clang is the LLVM project’s front end for the C, C++, and Objective-C language families.
Clang
→ C-family front end
LLVM IR
→ common intermediate representation
LLVM optimizer
→ machine-independent optimization
LLVM back end
→ target code generation
LLVM’s official introduction describes Clang as an LLVM-native C, C++, and Objective-C compiler intended to provide fast compilation, useful diagnostics, and support for source-level tool development.[78]
Background of Clang
GCC was a powerful compiler, but historically there were periods when its internal structure was difficult to reuse as independent libraries or integrate into IDEs and analysis tools. Clang was developed around the following goals.
- A fast front end
- Clear error diagnostics
- A reusable library-based architecture
- Support for IDEs and source-analysis tools
- High compatibility with GCC command-line options and extensions
- Use of LLVM optimization and code generation
Clang’s abstract syntax tree is used by tools such as the following.
- Code completion
- Refactoring
- Static analysis
- Source indexing
- Documentation generation
- Style and format checking
- Language servers
- Automated code transformation
Clang C Standard Support
Clang incrementally supports C89 and C90, C99, C11, C17, and C23 features. Clang’s official C status document states that it implements all of C99 and also tracks implementation status by feature for subsequent standards.[79]
clang -std=c99
clang -std=c11
clang -std=c17
clang -std=c23
GNU extensions can also be included.
clang -std=gnu17
clang -std=gnu23
Clang supports many GCC extensions and command-line options, but it is not a complete clone of GCC. Extension semantics, diagnostics, target support, and optimization results can differ.
LLVM IR
Clang translates C source into LLVM IR.
clang -S -emit-llvm main.c
LLVM IR is an intermediate representation shared by several LLVM-based languages and tools.
Clang
Rust front end
Swift
other languages and tools
↓
LLVM IR
↓
LLVM back ends
The fact that multiple languages use LLVM does not make their language semantics or ABIs identical.
Clang-Family Tools
The Clang ecosystem includes the following tools.
clang
clang-cl
clangd
clang-format
clang-tidy
Clang Static Analyzer
scan-build
clang-scan-deps
clang-cl provides a command-line interface similar to Microsoft cl.exe and can use the MSVC ABI and libraries on Windows.
clang
→ GCC-style command line
clang-cl
→ MSVC-style command line
Apple Clang
Apple provides Apple Clang, based on LLVM and Clang, in its development tools for macOS, iOS, iPadOS, watchOS, and other platforms.
Apple Clang is based on upstream Clang but combines the following elements.
- Apple platform SDKs
- Mach-O object format
- Apple ABI
- Xcode toolchain
- Apple linker
- Objective-C and Objective-C++
- Apple platform-specific extensions
- Deployment-target version handling
Apple Clang
+
macOS SDK
+
Apple libc
+
ld64 or another Apple linker
=
macOS native toolchain
Apple Clang version numbers may not directly match upstream LLVM and Clang version numbers. Supported features must be checked in the documentation for the corresponding Xcode and Command Line Tools versions.
MSVC
The Microsoft Visual C++ compiler is generally called MSVC or cl.exe. Although its name contains C++, it processes both C and C++ source and is Microsoft’s primary native compiler for Windows development.
cl.exe
→ C and C++ front end and driver
link.exe
→ PE/COFF linker
Microsoft CRT
→ C runtime and standard library
Windows SDK
→ Win32 APIs and system libraries
MSVC is closely integrated with the following environments.
- Visual Studio
- MSBuild
- Windows SDK
- Universal C Runtime
- Windows debugger
- PDB debugging information
- PE/COFF
- Windows x64 and Arm64 ABIs
MSVC C Mode
MSVC translates source as C or C++ according to the source file extension and options.
file.c
→ C source
file.cpp
→ C++ source
The language can also be specified explicitly.
/TC
→ process as C
/TP
→ process as C++
MSVC Standard Support
For a long time, MSVC primarily provided a C90-based language with Microsoft extensions, and later added C11 and C17 features. Microsoft maintains separate documentation describing conformance for individual C standard features.[80]
C standard modes can be selected using options such as the following.
/std:c11
/std:c17
According to Microsoft documentation, the /std option selects a supported C or C++ standard mode.[81]
MSVC support for C11 and C17 does not mean that every optional feature and library element is provided completely. The following areas require version-specific verification.
- Variable-length arrays
- Complex numbers
- C standard threads
- Atomic operations
- Preprocessor conformance
- C99 mathematics and library facilities
- C23 features
MSVC provides a more conforming token-based preprocessor through the /Zc:preprocessor option.[82]
Microsoft Extensions
MSVC provides various extensions for Windows and processor features.
__declspec
__stdcall
__cdecl
__vectorcall
__forceinline
__int64
SEH
compiler built-ins
Microsoft pragmas
For example, a dynamic library symbol can be exported as follows.
__declspec(dllexport)
int add(int left, int right) {
return left + right;
}
This syntax is a Microsoft extension rather than ISO C.
Relationship Among GCC, Clang, and MSVC
These three compilers are the general-purpose compilers most commonly encountered in modern C development, but they serve different purposes and environments.
| Item | GCC | Clang | MSVC |
|---|---|---|---|
| Primary ecosystem | GNU, Linux, UNIX, and embedded systems | LLVM, multiple operating systems, and tooling | Windows and Visual Studio |
| Command | gcc | clang | cl |
| Major intermediate representations | GIMPLE and RTL | LLVM IR | Microsoft internal representations |
| Major object formats | ELF and many others | ELF, Mach-O, COFF, and others | PE/COFF |
| Extensions | GNU C | GNU and Clang extensions | Microsoft extensions |
| License family | GPL | Apache 2.0 with LLVM exceptions | Proprietary software |
| Tool integration | GNU toolchain | Library- and IDE-oriented tooling | Visual Studio and Windows SDK |
| Target range | Very broad | Very broad | Primarily Windows |
No single compiler is absolutely superior in every situation.
Linux distributions and the GNU ecosystem
→ GCC or Clang
macOS and Apple platforms
→ Apple Clang
Windows MSVC ABI
→ MSVC or clang-cl
broad cross-compilation
→ GCC or Clang
IDE, static-analysis, and tooling development
→ the Clang ecosystem is frequently used
optimization for a particular vendor CPU
→ consider a vendor compiler
Intel C Compiler
Intel provided the Intel C Compiler and Intel C++ Compiler for many years. Their traditional commands were icc and icl.
icc
→ Intel C Compiler for Linux and UNIX-like systems
icl
→ Intel C/C++ Compiler for Windows
Intel compilers focused on the following areas.
- Optimization for Intel processors
- Automatic vectorization
- SIMD instructions
- Profile-guided optimization
- Numerical computation
- OpenMP
- High-performance computing
- Integration with Intel performance libraries
Intel later shifted its primary focus to the Intel oneAPI DPC++/C++ Compiler, based on LLVM technology. Its commands are generally icx and icpx.
icx
→ LLVM-based driver for C and C++
icpx
→ primarily for C++ and SYCL
The Intel oneAPI toolkits provide compilers, libraries, and analysis tools targeting multiple architectures, including CPUs and GPUs.[83]
Even when based on Clang and LLVM, Intel compilers can provide additional Intel-specific optimization, vectorization, mathematical libraries, and analysis features.
IBM XL C and Open XL C/C++
IBM has provided IBM XL C and XL C/C++ compilers for AIX, Linux on Power, and z/OS.
Traditional XL compilers provided optimization and vectorization for IBM Power and IBM Z architectures, along with enterprise ABI support for AIX and z/OS environments.
XL C
├── POWER optimization
├── IBM Z optimization
├── AIX ABI
├── z/OS environment
├── vectorization
└── high-performance numerical computation
IBM’s next-generation Open XL C/C++ products adopted Clang and LLVM technology. IBM describes Open XL C/C++ for z/OS as based on the Clang and LLVM technology framework and supporting C17 and C18 as well as C++ standards.[84]
Open XL for AIX and Linux on Power also uses the LLVM and Clang infrastructure while providing optimization for IBM Power hardware.[85]
Arm Compiler
Arm provides commercial embedded C and C++ compilers for Arm processors. Traditionally, Arm Compiler 5 used armcc, while Arm Compiler 6 uses armclang, based on LLVM and Clang technology.
Arm Compiler 5
→ based on armcc
Arm Compiler 6
→ Clang- and LLVM-based armclang
Arm Compiler is used in the following fields.
- Cortex-M firmware
- Cortex-R real-time systems
- Cortex-A embedded systems
- High-reliability devices
- Automotive and industrial control
- Code-size optimization
- Arm ABI
- Arm-specific instructions and built-ins
Arm provides Arm Compiler for Embedded as a mature embedded toolchain for Arm processors.[86]
Arm’s implementation-definition documentation describes type sizes, conversions, translation limits, and library characteristics that ISO C leaves to implementations.[87]
The Arm Compiler for Embedded FuSa product also exists for functional-safety fields that require certification and assessment.[88]
GNU Arm Toolchain
Arm also distributes the GCC-based GNU Arm Toolchain.
arm-none-eabi-gcc
→ Arm embedded targets without an operating system
aarch64-none-linux-gnu-gcc
→ AArch64 Linux targets
The GNU Arm Embedded Toolchain combines GCC, Binutils, a C library, and debugging tools. Arm has provided it as an open-source C and C++ tool collection for the Arm Cortex family.[89]
Arm Compiler and the GNU Arm Toolchain are different products.
Arm Compiler
→ Arm’s commercial and specialized optimization toolchain
GNU Arm Toolchain
→ GCC-based free software toolchain
IAR C/C++
The C and C++ compiler in IAR Embedded Workbench is a commercial toolchain used in microcontroller and embedded development.
Its major characteristics include the following.
- Support for multiple microcontrollers
- Code-size optimization
- Debugger and IDE integration
- Integrated linker and libraries
- Products with functional-safety certification
- Device-specific memory models
- Embedded-specific extensions
IAR compilers are used with target families such as the following.
Arm
RISC-V
AVR
MSP430
8051
Renesas families
STM8
other MCUs
The exact supported targets and C standard editions differ according to the product and version.
Keil C Compilers
Keil is well known for C compilers targeting multiple microcontroller families. The Keil product family is now part of the Arm ecosystem.
Representative families include the following.
C51
→ 8051 family
C251
→ 251 family
C166
→ Infineon C16x family
MDK and Arm Compiler
→ Arm Cortex family
Compilers such as Keil C51 provide memory-model extensions that differ from ordinary desktop C.
unsigned char xdata *pointer;
Keywords such as xdata, code, and idata are implementation extensions representing the separate address spaces of the 8051.
TI C/C++ Compiler
Texas Instruments provides C and C++ compilers for its DSPs and microcontrollers.
Representative targets include the following.
- C2000
- MSP430
- Arm-based TI MCUs
- C6000 DSP
- Other TI processors
TI compilers can provide the following elements.
- DSP instruction optimization
- Loop software pipelining
- Built-in functions
- Device register support
- Linker command files
- Real-time runtime
- Code Composer Studio integration
Some newer TI toolchains are transitioning to LLVM, and proprietary legacy compilers and LLVM-based compilers can coexist depending on the product family.
Green Hills Compiler
Green Hills Software’s C and C++ compilers are commercial compilers used in embedded, automotive, aerospace, and functional-safety fields.
Their major characteristics include the following.
- Safety-critical systems
- Support for multiple embedded CPUs
- Optimization
- Static analysis
- Certification materials
- Integration with the MULTI development environment
- Integration with the INTEGRITY operating system
They focus more on certification, long-term support, embedded hardware, and real-time systems than on ordinary general-purpose desktop development.
CompCert
CompCert is a formally verified optimizing C compiler. Unlike ordinary compilers, which primarily establish reliability through extensive testing and validation, CompCert uses mathematical proof to establish that its compilation transformations preserve program meaning.
meaning of C source
↓ verified transformation
intermediate representation
↓ verified transformation
meaning of machine code
CompCert is particularly important in safety-critical systems and compiler-verification research.
Compared with general-purpose GCC or Clang, it has the following differences.
- The supported range of C and extensions can be limited.
- Supported target architectures can be limited.
- It does not aim to provide every recent aggressive optimization.
- Semantic-preservation proof is the primary objective.
- It is available for commercial and research use.
CompCert does not prove that every C program is safe. The programmer remains responsible for writing a source program without undefined behavior.
Watcom C/C++
Watcom C/C++ was an important commercial compiler for PC software and game development from the late 1980s through the 1990s.
Watcom supported the following environments.
- DOS
- 16-bit Windows
- 32-bit Windows
- OS/2
- Novell NLM
- DOS extender environments
Watcom C/C++ was widely known for the quality of its x86 code generation and the DOS/4GW extender environment. It was used in the development of multiple games and commercial software products during the 1990s.
Open Watcom documentation records that Watcom introduced a C compiler product in 1987.[90]
After the commercial product was discontinued, its source was released and continued as the Open Watcom project.
Turbo C and Borland C
Borland’s Turbo C was a widely used C development environment on personal computers during the 1980s.
Turbo C provided the following elements in one product.
- C compiler
- Integrated editor
- Build facilities
- Debugging
- Libraries
- DOS development environment
Unlike many compilers of the period, which centered on separate command-line tools, Turbo C made C development more accessible to personal computer users through fast compilation and an integrated development environment.
The product later developed into the Borland C and Borland C++ families.
Turbo C
↓
Turbo C 2.x
↓
Borland C++
It is rarely used as a modern ISO C development tool, but occupies an important place in the history of DOS software, educational materials, and classic PC programs.
Microsoft C and QuickC
Microsoft has provided the Microsoft C Compiler since the MS-DOS and early Windows eras.
Its product lineage includes the following.
Microsoft C
QuickC
Microsoft Visual C++
MSVC
QuickC provided a relatively accessible integrated environment for PC developers, while Visual C++ later became a central tool for native Windows development.
Early Microsoft C and modern MSVC belong to the same product lineage, but their internal structures, standard support, target ABIs, and development environments differ greatly.
LCC
LCC is a small, retargetable C compiler developed by Chris Fraser and David Hanson.
LCC maintained a comparatively small architecture suitable for education, explanation of compiler structures, and research into code generation for new targets.
C front end
↓
machine-independent interface
↓
target code generator
The design and implementation of LCC are described in detail in *A Retargetable C Compiler: Design and Implementation*, which became widely used as compiler-education material.
Rather than being a comprehensive toolchain with the many optimizations and language front ends of GCC, LCC emphasized an architecture that made the structure of a C compiler easy to understand and port to new targets.
Open64
Open64 is an open-source compiler used as an optimizing compiler infrastructure for high-performance computing and research. It developed from a lineage related to SGI’s MIPSpro compiler technology.
Its major areas of interest included the following.
- Advanced loop optimization
- Interprocedural optimization
- Parallelization
- Scientific computation
- Compiler research infrastructure
Its usage in modern general-purpose C development is smaller than that of GCC and LLVM, but it influenced compiler-optimization research and multiple derivative compilers.
Oracle Developer Studio C
Sun C and SunPro C from Sun Microsystems were important vendor compilers for SPARC and Solaris environments. They later continued as Oracle Developer Studio C and C++ products.
Their major characteristics included the following.
- SPARC optimization
- Solaris ABI
- Automatic parallelization
- Profiling
- Numerical computation
- OpenMP
- Performance analyzers
Within the Solaris and SPARC ecosystems, they were major compiler choices alongside GCC.
Compilers and Standard Libraries
The same compiler can use different standard libraries.
GCC
├── glibc
├── musl
├── Newlib
├── picolibc
└── vendor libc
Clang
├── glibc
├── musl
├── Apple libc
├── Microsoft UCRT
├── FreeBSD libc
└── WASI libc
The distinction between a compiler and a C++ standard library is more visible in C++, but in C as well, the compiler and libc can be separate components.
For example, the following combinations are possible.
Clang + glibc
Clang + musl
GCC + glibc
GCC + Newlib
Clang + UCRT
Even when a compiler supports C23 syntax, the C library in use may not yet provide C23’s new functions and headers.
language feature support
≠
library feature support
Compiler Runtimes
A compiler can require internal support functions in addition to the standard library.
Representative runtimes include the following.
libgcc
compiler-rt
MSVC compiler runtime
Arm runtime libraries
vendor-specific support libraries
Operations not directly provided by the CPU can be translated into runtime function calls.
long long divide(
long long left,
long long right
) {
return left / right;
}
On a 32-bit CPU, this can call a runtime function that performs 64-bit division.
Instrumentation facilities can also require runtimes.
AddressSanitizer
UndefinedBehaviorSanitizer
ThreadSanitizer
profile instrumentation
code coverage
Linkers
The compiler and linker can also be separate components.
Representative linkers include the following.
- GNU ld
- GNU gold
- LLVM LLD
- Microsoft LINK
- Apple ld
- mold
- Vendor-specific linkers
Clang + LLD
Clang + GNU ld
GCC + GNU ld
MSVC + LINK
Apple Clang + Apple ld
Even with the same compiler, using a different linker can change link speed, supported object formats, linker-script capabilities, and optimization features.
Compiler Extensions
Actual C implementations provide extensions beyond ISO C.
The purposes of extensions include the following.
- Access to hardware instructions
- Operating system ABI support
- Function attributes
- Vector types
- Address spaces
- Memory section placement
- Interrupt functions
- Inline assembly
- Compile-time checking
- Performance optimization
#if defined(__GNUC__)
__attribute__((aligned(64)))
#endif
static unsigned char buffer[64];
#if defined(_MSC_VER)
__declspec(align(64))
#endif
static unsigned char buffer[64];
When C23 attributes are supported, some purposes can be expressed with standard syntax.
alignas(64)
static unsigned char buffer[64];
However, not every implementation extension is replaced by a standard feature.
Implementation-Defined Documentation
The C standard permits implementations to select the behavior of multiple characteristics. A conforming implementation must document its selections.
Documented matters include the following.
- Signedness of plain
char - Sizes of integer types
- Pointer representations
- Character sets
- Structure alignment
- Shift behavior
- Files and streams
- Signals
- Environment-dependent effects of library functions
- Translation limits
Therefore, when evaluating a C implementation, it is necessary to inspect its implementation-defined documentation, ABI, and library documentation rather than looking only at a label such as “C17 support.”
Testing Compiler Conformance
The conformance of a compiler to the C standard cannot be proven by the successful compilation of one example.
The following methods can be used for verification.
- Tests for each standard feature
- Compiler regression tests
- Cross-comparison among multiple compilers
- Random program generation using tools such as Csmith
- ABI testing
- Standard library testing
- Reproduction of defect reports
- Comparison of execution results
- Self-hosting
- Comparison of bootstrap results
the same C program
├── GCC
├── Clang
├── MSVC
├── ICC or ICX
└── other compilers
Different results do not necessarily indicate a compiler bug. The program may depend on the following.
- Undefined behavior
- Implementation-defined behavior
- Unspecified ordering
- Type sizes
- ABI
- Extension syntax
- Library differences
Compiler Bugs
When an implementation incorrectly translates a program that conforms to the C standard, it is a compiler bug.
Representative categories include the following.
- Incorrect optimization
- Type-analysis errors
- Code-generation errors
- ABI violations
- Preprocessor errors
- Debug-information errors
- Missing diagnostics
- Standard library implementation errors
When an incorrect result appears only at a particular optimization level, both of the following possibilities must be investigated.
undefined behavior in the program
or
a compiler optimization bug
Create a minimal reproducible example and
Fields of Application
C is used in many fields that form the foundation of computer software, ranging from operating systems and device control to databases, network servers, language runtimes, multimedia processing, and scientific computing. It is especially widespread in fields that require direct control over memory layout, execution cost, operating system interfaces, and hardware interfaces.
C is not used to the same extent in every kind of software. In some fields, such as operating system kernels and firmware, C serves as the primary language of the program. In others, such as web services and desktop applications, higher layers are written in other languages while only modules requiring performance or compatibility are implemented in C.
The forms of C usage can be broadly divided as follows.
Fields where C is used as a primary language
├── operating system kernels
├── device drivers
├── embedded systems and firmware
├── real-time operating systems
├── language runtimes and virtual machines
├── system libraries
├── database engines
├── network-based software
├── multimedia codecs and processing libraries
└── scientific and high-performance data libraries
Fields where C is used partially
├── games and game engines
├── graphics and rendering
├── desktop applications
├── mobile applications
├── web services
├── artificial intelligence and machine learning
├── security software
└── extension modules for other languages
C is widely used not only because of the features of the language itself, but also because of its long-established compilers, ABIs, operating system APIs, and library ecosystem. Many programming languages and operating systems use C function interfaces as a common boundary and provide foreign function interfaces capable of directly calling existing C libraries.
Operating System Kernels
Operating system kernels are one of the most representative fields in which C is used. A kernel must manage processors and memory, devices and processes, file systems, and networks, so it requires hardware-near access and predictable execution costs.
C is used in kernels for tasks such as the following.
- Process and thread management
- Virtual memory and page management
- File systems
- Network stacks
- System calls
- Interrupt and exception handling
- Device drivers
- Security and permission checks
- CPU architecture-specific code
- Synchronization and scheduling
The Linux kernel explicitly states in its official documentation that it is written in C and is generally compiled using the GNU C11 dialect. Linux supports builds using Clang as well as GCC.[91]
Kernel code does not use only standard C. It also combines compiler extensions, inline assembly, architecture-specific built-ins, and specialized linking methods.
struct task {
unsigned long state;
void *stack;
int priority;
};
static void schedule_task(struct task *task) {
if (task == NULL) {
return;
}
task->state = TASK_RUNNING;
}
This code is a simplified example of the general form of a kernel data structure. Actual kernels may also use atomic operations, memory barriers, intrusive lists, per-CPU data, and compiler extensions.
C is used not only in UNIX-like operating systems, but also in many commercial operating systems, real-time operating systems, and research operating systems. Some components of newer operating systems are also written in C++ and Rust, but large portions of existing kernels, ABIs, and device interfaces are still centered on C.
Device Drivers
Device drivers implement the interface between an operating system and hardware devices.
Representative targets include the following.
- Graphics processing units
- Storage devices
- Network cards
- USB devices
- Audio devices
- Cameras
- Sensors
- Input devices
- Bus controllers
- Power management devices
Drivers handle memory-mapped registers, DMA buffers, interrupts, device-specific commands, and operating system kernel APIs.
struct device_registers {
volatile uint32_t control;
volatile uint32_t status;
volatile uint32_t data;
};
static bool device_ready(
const struct device_registers *device
) {
return
(device->status & STATUS_READY) != 0;
}
volatile can be used to prevent device-register accesses from being removed unnecessarily. However, device access ordering and CPU memory barriers may require separate platform-specific APIs.
A driver is not an ordinary ISO C application. It depends heavily on internal kernel APIs, the target processor, the compiler, and hardware specifications.
Embedded Systems
C is widely used in embedded systems that use microcontrollers, limited memory, and low power consumption.
Representative devices include the following.
- Home appliances
- Automotive electronic control units
- Industrial controllers
- Medical devices
- Network equipment
- IoT sensors
- Wearable devices
- Robots
- Drones
- Keyboards and mice
- Storage controllers
- Smart cards
Embedded programs may run directly without an operating system or on top of a small real-time operating system.
#include <stdint.h>
#define GPIO_OUTPUT_ADDRESS \
UINTPTR_C(0x40020014)
static volatile uint32_t *const gpio_output =
(volatile uint32_t *)GPIO_OUTPUT_ADDRESS;
void led_set(bool enabled) {
if (enabled) {
*gpio_output |= 1U;
} else {
*gpio_output &= ~1U;
}
}
This code is a simplified example of memory-mapped input and output. The actual address, register width, and access order are specified by the microcontroller’s datasheet and SDK.
C is used in embedded systems for the following reasons.
- It can produce small executable files.
- Runtime dependencies can be minimized.
- Fixed memory layouts can be designed.
- Interrupts and registers can be handled directly.
- Cross compilers exist for many processors.
- Existing manufacturer SDKs and drivers provide C APIs.
- Real-time execution costs are comparatively easy to analyze.
- Many certified toolchains and established codebases exist.
C does not automatically provide real-time behavior or safety. Restrictions on dynamic memory, execution-time analysis, static analysis, coding rules, and hardware watchdogs are used together with it.
Real-Time Operating Systems
On small embedded devices, both the kernel and applications of a real-time operating system are often written in C.
FreeRTOS provides a kernel and libraries for microcontrollers and small microprocessors, and its official distribution includes C sources for multiple processor ports and for the kernel itself.[92]
Zephyr is a real-time operating system targeting resource-constrained devices and supporting multiple hardware architectures. Its kernel, drivers, network stack, and device-support code are developed in a source structure centered on C.[93]
C applications running on real-time operating systems use tasks, queues, mutexes, and timers.
static void sensor_task(void *context) {
struct sensor *sensor = context;
for (;;) {
struct sample sample;
if (sensor_read(sensor, &sample)) {
queue_send(&sample);
}
task_delay(SENSOR_INTERVAL);
}
}
The actual function and type names differ among real-time operating systems.
Bootloaders and Firmware
C is also used in bootloaders and system firmware that run before an operating system starts.
Major tasks include the following.
- CPU initialization
- Memory controller configuration
- Device discovery
- Reading storage devices
- Secure boot verification
- Loading the operating system image
- Passing hardware information
- Firmware updates
It is common to write the earliest startup code in assembly language and implement most logic in C after a basic execution environment has been prepared.
reset vector
↓
initial assembly code
↓
stack and memory initialization
↓
C startup function
↓
device initialization
↓
operating system loading
Firmware may not provide an ordinary hosted standard library, so string, memory, allocation, and input/output facilities may be implemented directly or supplied through a freestanding library.
System Libraries
C is also used in system libraries that provide fundamental interfaces between operating systems and applications.
Representative components include the following.
- C standard libraries
- POSIX libraries
- System call wrappers
- Dynamic linkers
- Thread libraries
- Cryptographic libraries
- Compression libraries
- File-format libraries
- Device-access libraries
System libraries often expose a C ABI so that they can be used by multiple languages.
struct image;
struct image *image_load(
const char *path
);
void image_destroy(
struct image *image
);
An opaque-pointer API like this can be called from C, C++, Rust, Python, C#, and other languages without exposing its internal structure.
Programming Language Implementations
C is widely used to implement interpreters, compiler runtimes, and virtual machines for other programming languages.
Representative components include the following.
- Lexers
- Parsers
- Bytecode compilers
- Interpreters
- Object and type systems
- Garbage collectors
- Module loaders
- Native function interfaces
- Standard libraries
- Parts of JIT runtimes
CPython is the representative implementation of Python, and its internal runtime and object system are developed primarily in C source. Python officially provides the Python/C API to support C extension modules and embedding.[94][95]
#include <Python.h>
static PyObject *add_values(
PyObject *self,
PyObject *arguments
) {
int left;
int right;
if (!PyArg_ParseTuple(
arguments,
"ii",
&left,
&right
)) {
return NULL;
}
return PyLong_FromLong(left + right);
}
Lua is a language designed to be easy to embed in applications, and its official site describes Lua as a language that is easy to integrate into applications. Lua implementations and the C API are used in games, tools, servers, and embedded programs.[96]
C has also been used as a core or partial implementation language in the following language implementations.
- Python implementations
- Lua implementations
- Ruby implementations
- PHP implementations
- Perl implementations
- R implementations
- Tcl implementations
- Parts of JavaScript engines
- Lisp and Scheme implementations
- Shells and command interpreters
- Domain-specific language runtimes
Not every implementation is still written purely in C. C++ and Rust, assembly language, and code-generation languages are often used together with it.
Extension Modules and Language Bindings
Parts of a higher-level language that require performance or operating system access can be written as C extension modules.
Representative purposes include the following.
- Calling existing C libraries
- Accelerating compute-intensive loops
- Accessing hardware devices
- Using operating system APIs
- Processing native file formats
- Image and audio processing
- Cryptography
- Compression
- Network protocols
Python, Lua, Ruby, Java, and C#
↓ foreign function interface
C API
↓
operating system or native library
Python’s C API allows C and C++ programs to access interpreter objects and runtime facilities and describes extension modules and embedding as major usage models.[97]
Extension modules must correctly manage cross-language conversion costs, object lifetimes, error handling, and threading rules. One faulty C extension can terminate the entire higher-level language runtime or corrupt its memory.
Compilers and Development Tools
C has been used to implement compilers, assemblers, linkers, and static analyzers.
Representative components include the following.
- Language front ends
- Optimizers
- Code generators
- Assemblers
- Linkers
- Debuggers
- Profilers
- Static analyzers
- Binary-analysis tools
- Build tools
- Version-control systems
The earliest C compiler and the Portable C Compiler were written in C, demonstrating the language’s self-hosting capability and portability. Modern large compilers use C++ extensively, but C continues to be used in system tools, small compilers, runtime libraries, and target-support code.
Git is a distributed version-control system designed to handle large projects, and its core source is centered on C. Git’s official user manual contains a separate chapter introducing the structure of Git’s source code.[98]
Database Engines
Database engines are another field in which C is used extensively.
Databases must precisely control the following elements.
- Disk-page layout
- Buffer caches
- Transaction logs
- Indexes
- Query executors
- Concurrency control
- Locks
- Network protocols
- File formats
- Memory allocation
- Extension modules
SQLite is an SQL database engine designed for small size and embedded execution, and its official site explains that SQLite has been implemented in ordinary C since 2000.[99]
SQLite is provided as a C library and can be embedded in many programs, including mobile devices, desktop applications, operating systems, and browsers.[100]
#include <sqlite3.h>
int open_database(
const char *path,
sqlite3 **database
) {
int result = sqlite3_open(
path,
database
);
if (result != SQLITE_OK) {
sqlite3_close(*database);
*database = NULL;
}
return result;
}
The core server and tools of PostgreSQL are also built around a C development environment, and the official documentation explains that C development tools are required for PostgreSQL core development. Server extension functions can also be written in C and loaded as dynamic libraries.[101][102]
C is used in other databases and storage engines as well, but modern distributed databases and high-level management layers often combine C++, Java, Go, Rust, and other languages.
File Systems and Storage Devices
File systems and storage engines must directly manage disk blocks, metadata, caches, and journals, so C is widely used in these fields.
Major targets include the following.
- Operating system file systems
- User-space file systems
- Compressed file formats
- Archive tools
- Key-value storage engines
- Flash translation layers
- Storage-device firmware
- Data-recovery tools
struct block_header {
uint32_t type;
uint32_t size;
uint64_t sequence;
};
bool block_header_read(
FILE *file,
struct block_header *header
) {
unsigned char bytes[16];
if (
fread(
bytes,
1,
sizeof(bytes),
file
) != sizeof(bytes)
) {
return false;
}
header->type = decode_u32_le(bytes);
header->size = decode_u32_le(bytes + 4);
header->sequence =
decode_u64_le(bytes + 8);
return true;
}
Portable storage formats explicitly encode byte order and field sizes rather than writing structures directly.
Network Stacks
C is widely used in the network stacks of operating system kernels and embedded devices.
Handled layers include the following.
- Ethernet
- Wi-Fi drivers
- ARP
- IPv4 and IPv6
- ICMP
- TCP and UDP
- DHCP
- DNS
- Parts of TLS implementations
- Routing and firewalls
- Packet filtering
struct ipv4_header {
uint8_t version_and_length;
uint8_t service_type;
uint16_t total_length;
uint16_t identifier;
uint16_t fragment;
uint8_t time_to_live;
uint8_t protocol;
uint16_t checksum;
uint32_t source;
uint32_t destination;
};
Code that directly maps actual network packet layouts onto C structures must account for padding, alignment, and endianness. Byte-level parsing and explicit conversion are generally used.
Network Servers and Proxies
C is also used in high-performance network servers, proxies, and load balancers.
nginx provides HTTP web-server, reverse-proxy, content-cache, load-balancing, and TCP and UDP proxy functions.[103]
C is suitable for implementing the following elements in such servers.
- Event loops
- Asynchronous sockets
- Connection pools
- Buffer management
- Protocol parsing
- Timers
- Memory pools
- Operating system-specific event APIs
- Module interfaces
struct connection {
int socket;
unsigned char *input;
size_t input_size;
size_t input_capacity;
};
static bool connection_reserve(
struct connection *connection,
size_t required
) {
if (
required <=
connection->input_capacity
) {
return true;
}
unsigned char *new_input =
realloc(
connection->input,
required
);
if (new_input == NULL) {
return false;
}
connection->input = new_input;
connection->input_capacity =
required;
return true;
}
Writing an entire modern web service in C is comparatively uncommon. Higher layers such as HTTP routing, business logic, and data processing often use Java, C#, JavaScript, Python, Go, Rust, and other languages. However, server runtimes, network libraries, cryptographic modules, and compression modules may be implemented in C.
Network Client Libraries
C is also used in network transport libraries supporting many protocols.
libcurl is a client transfer library supporting HTTP, HTTPS, FTP, SMTP, IMAP, MQTT, WebSocket, and many other protocols, and it provides a C API.[104]
#include <curl/curl.h>
int download_url(const char *url) {
CURL *handle = curl_easy_init();
if (handle == NULL) {
return -1;
}
curl_easy_setopt(
handle,
CURLOPT_URL,
url
);
CURLcode result =
curl_easy_perform(handle);
curl_easy_cleanup(handle);
return result == CURLE_OK
? 0
: -1;
}
The official libcurl documentation provides an API for C programs and bindings for multiple languages, while its fundamental interface is centered on C.[105]
Cryptographic and Security Libraries
Cryptographic and security libraries are often implemented in C.
Major functions include the following.
- Symmetric-key cryptography
- Public-key cryptography
- Hash functions
- Digital signatures
- Certificate processing
- TLS
- Random-number generation
- Key derivation
- Security protocols
- Hardware acceleration
C provides an ABI that is easy to call from multiple operating systems and languages and can be combined with CPU-specific instructions and assembly optimizations.
However, cryptographic C code must pay particular attention to the following problems.
- Lifetime of secret data
- Constant-time execution
- Integer overflow
- Buffer bounds
- Memory clearing that a compiler might remove
- CPU-specific optimization
- Side-channel attacks
- Random-number quality
- Error handling
void secure_buffer_clear(
volatile unsigned char *buffer,
size_t size
) {
while (size != 0) {
*buffer++ = 0;
--size;
}
}
Simple clearing based on volatile alone cannot guarantee complete security across every implementation and optimization environment. It is more appropriate to use a secure memory-erasure function provided by the platform or a verified library.
Compression and Archiving
C is widely used in compression libraries and archive processors.
Representative tasks include the following.
- Lossless compression
- Stream compression
- Image compression
- File archiving
- Checksums
- Hashing
- Data-block transformation
These libraries are included in operating systems, browsers, game engines, databases, and network protocols.
A C API allows the same compression implementation to be shared among multiple languages.
struct compressor;
struct compressor *compressor_create(
int level
);
bool compressor_update(
struct compressor *compressor,
const void *input,
size_t input_size,
void *output,
size_t output_capacity,
size_t *output_size
);
void compressor_destroy(
struct compressor *compressor
);
Multimedia Processing
Audio and video codecs, containers, and conversion tools process large quantities of data at a low level and require CPU-specific SIMD optimization, making multimedia a field in which C is used extensively.
FFmpeg is a collection of multimedia tools and libraries that reads media from input devices, files, and streams and filters or converts it into multiple output formats.[106]
FFmpeg includes libraries such as libavcodec, which provides codecs, and libavformat, which processes container formats.[107][108]
C is used in multimedia for the following tasks.
- Video decoding and encoding
- Audio codecs
- Color-space conversion
- Sample-format conversion
- Container parsing
- Streaming
- Filter processing
- Hardware-decoder interfaces
- CPU-specific vector optimization
void mix_audio(
float *restrict output,
const float *restrict left,
const float *restrict right,
size_t count
) {
for (
size_t index = 0;
index < count;
++index
) {
output[index] =
left[index] +
right[index];
}
}
Actual media implementations also consider saturation operations, sample formats, alignment, SIMD, and multithreading.
Image Processing
C is also used in image decoders, encoders, color conversion, and resizing libraries.
Representative targets include the following.
- PNG
- JPEG
- TIFF
- GIF
- WebP-related libraries
- Color management
- Image resizing
- Filters
- Pixel-format conversion
- Camera RAW processing
struct image {
unsigned char *pixels;
size_t width;
size_t height;
size_t stride;
unsigned int channels;
};
Pixel memory is generally managed as a contiguous array of bytes or integers, with row padding and pixel formats handled explicitly.
Graphics APIs and Rendering Foundations
In graphics, C is often used not for complete applications, but for graphics APIs, loaders, driver interfaces, and low-level libraries.
Representative areas include the following.
- OpenGL API
- Vulkan API
- Graphics drivers
- Shader compiler runtimes
- Image libraries
- Window and input libraries
- Rendering utilities
- Debug layers
A C API is easy to use as a language-neutral native interface.
struct graphics_buffer;
struct graphics_buffer *
graphics_buffer_create(
size_t size,
unsigned int usage
);
void graphics_buffer_destroy(
struct graphics_buffer *buffer
);
Higher-level renderers and game-engine structures are often written in C++ or Rust, while C is used at graphics API boundaries and in some system modules.
Game Development
Historically, C was widely used as the primary language for entire games. Today, it is mainly used in the following areas.
- Console and embedded games
- Small 2D games
- Game runtimes
- Platform layers
- Audio and video libraries
- Input handling
- Network libraries
- Scripting-language runtimes
- Plugin ABIs
- Emulators
- Retro-game tools
C++ is more widely used in large commercial games and engines, but C-based libraries and operating system APIs are used alongside it.
struct game_state {
bool running;
double time;
struct player player;
};
void game_update(
struct game_state *game,
double delta_time
) {
player_update(
&game->player,
delta_time
);
game->time += delta_time;
}
Because C has no object-oriented language features, game objects and systems are organized directly through functions, structures, data-oriented design, ECS, and similar approaches.
Emulators
C is used in emulators for game consoles, computers, CPUs, and other hardware.
Major components include the following.
- CPU instruction interpretation
- Memory maps
- Device models
- Timers
- Graphics processing
- Audio generation
- Input devices
- Save states
- Parts of dynamic recompilers
struct cpu {
uint16_t program_counter;
uint8_t accumulator;
uint8_t status;
uint8_t memory[65536];
};
void cpu_step(struct cpu *cpu) {
uint8_t opcode =
cpu->memory[
cpu->program_counter++
];
execute_opcode(cpu, opcode);
}
Emulators use extensive bit operations, explicit integer widths, and memory arrays, which fit well with C’s data representation model.
Scientific Computing
C is used to implement computational kernels and common libraries for numerical computation and scientific simulation.
Representative fields include the following.
- Linear algebra
- Numerical integration
- Signal processing
- Physical simulation
- Statistical computation
- Astronomy
- Weather and fluid simulation
- Bioinformatics
- Geospatial processing
- Data transformation
Higher-level research code often uses Python, R, MATLAB, Julia, and Fortran, while repeated computations and memory processing are implemented in C.
Python, R, and Julia
↓
C extension or C library
↓
numerical computation kernel
↓
SIMD, threads, and GPU interfaces
High-Performance Computing
In high-performance computing, C, C++, and Fortran are used in compute nodes and parallel library implementations.
C is used in the following areas.
- MPI-based distributed computing
- OpenMP-based shared-memory parallelism
- Numerical libraries
- Data input and output
- Cluster runtimes
- GPU API calls
- Network fabrics
- File formats
- Performance-analysis tools
The HDF5 library, used for storing large scientific datasets, is implemented in portable C for portability and provides interfaces for storing and processing high-performance data.[109]
#include <hdf5.h>
hid_t file = H5Fcreate(
"data.h5",
H5F_ACC_TRUNC,
H5P_DEFAULT,
H5P_DEFAULT
);
if (file >= 0) {
H5Fclose(file);
}
HDF5 provides interfaces for C++, Fortran, Java, and other languages as well as C, but its core library is based on portable C.
Numerical Libraries
C is used as a common interface for numerical libraries called from other languages.
void matrix_multiply(
size_t rows,
size_t columns,
size_t inner,
const double *left,
const double *right,
double *result
);
The C ABI can be connected comparatively easily from Fortran, C++, Python, Julia, Rust, and other languages.
Actual high-performance implementations may use the following technologies together.
- CPU-specific assembly
- SIMD intrinsics
- OpenMP
- GPU kernels
- Automatic vectorization
- Cache blocking
- Multithreading
- Distributed processing
Artificial Intelligence and Machine Learning
In artificial intelligence and machine learning, model definition and research code are often centered on Python, but C is used in execution engines, numerical kernels, device runtimes, and embedded inference.
Representative uses include the following.
- Tensor operation kernels
- Model inference runtimes
- CPU operators
- Inference for microcontrollers
- Image and signal preprocessing
- GPU and accelerator driver interfaces
- Python extension modules
- Model-file parsers
void relu(
float *values,
size_t count
) {
for (
size_t index = 0;
index < count;
++index
) {
if (values[index] < 0.0F) {
values[index] = 0.0F;
}
}
}
Large machine-learning frameworks often combine C++, Python, CUDA, and other languages. C appears more frequently in external APIs, low-level kernels, and embedded environments.
Robotics and Control Systems
In robotics and industrial control systems, C is used for sensors, motors, real-time control, and embedded communication.
Major tasks include the following.
- Sensor sampling
- Motor control
- PID control
- Real-time tasks
- Fieldbus communication
- Safe-state management
- Firmware updates
- Hardware diagnostics
struct pid_controller {
float proportional;
float integral;
float derivative;
float previous_error;
float accumulated_error;
};
float pid_update(
struct pid_controller *controller,
float target,
float measured,
float delta_time
) {
float error = target - measured;
controller->accumulated_error +=
error * delta_time;
float derivative =
(error -
controller->previous_error) /
delta_time;
controller->previous_error =
error;
return
controller->proportional *
error +
controller->integral *
controller->
accumulated_error +
controller->derivative *
derivative;
}
Actual control systems must also handle floating-point error, sampling periods, saturation, sensor failures, and safety limits.
Automotive Software
C has also long been widely used in automotive electronic systems.
Representative areas include the following.
- Engine control
- Transmission control
- Braking systems
- Airbags
- Battery management
- Firmware for infotainment devices
- In-vehicle networks
- Sensor processing
- Power management
Automotive software requires functional safety, real-time behavior, and operation under constrained resources. Coding rules, static analysis, traceable requirements, and verification procedures are used together.
Because C does not automatically guarantee memory safety, fields such as automotive, aerospace, and medicine often apply restricted language subsets and coding standards.
Aerospace and Defense Systems
C is also used in embedded control, communication, and signal processing for aerospace and defense systems.
Fields of use include the following.
- Flight control
- Navigation
- Onboard satellite software
- Communication devices
- Radar and signal processing
- Sensor fusion
- Real-time operating systems
- Lower-level modules of ground-control equipment
These systems place importance on long-term maintenance, verifiable toolchains, restricted dynamic memory, and deterministic execution times.
Medical Devices
C is used in firmware and real-time control for medical devices.
Representative targets include the following.
- Patient monitors
- Imaging equipment
- Infusion pumps
- Diagnostic equipment
- Portable measuring devices
- Sensors and data-acquisition devices
- Medical robots
In medical devices, risk analysis, verification, change management, and regulatory documentation are as important as code correctness. C benefits from extensive existing tooling and certification experience, but requires strict development procedures to prevent memory-safety errors.
Telecommunications Equipment
C is used in firmware and data paths for routers, switches, modems, base stations, and network-security equipment.
Major tasks include the following.
- Packet parsing
- Routing tables
- Cryptography
- Traffic classification
- QoS
- Network management
- Device drivers
- High-speed buffer processing
bool packet_process(
const unsigned char *data,
size_t size
) {
if (size < MINIMUM_HEADER_SIZE) {
return false;
}
uint16_t packet_type =
decode_u16_be(data);
return dispatch_packet(
packet_type,
data + 2,
size - 2
);
}
External network input cannot be trusted, so every length and offset must be validated before access.
Command-Line Tools
C is also used in small command-line utilities and system administration tools.
Representative categories include the following.
- File conversion
- Text processing
- Process management
- Network diagnostics
- System information
- Compilation tools
- Binary analysis
- Recovery tools
#include <stdio.h>
#include <stdlib.h>
int main(
int argc,
char *argv[]
) {
if (argc != 2) {
fprintf(
stderr,
"usage: %s FILE\n",
argv[0]
);
return EXIT_FAILURE;
}
return process_file(argv[1])
? EXIT_SUCCESS
: EXIT_FAILURE;
}
Command-line tools written in C can keep executable size and runtime dependencies small. On the other hand, Python, Rust, Go, and other languages may be chosen for tools where complex string and data processing or rapid development is important.
Desktop Applications
Historically, entire desktop applications were often written in C, and it is still used in some GUI toolkits and system applications.
Areas where C is used include the following.
- GUI toolkits
- Image editors
- File managers
- System settings tools
- Terminal emulators
- Media players
- Plugin systems
- Cross-platform runtimes
However, complex large-scale applications often use languages that provide higher-level abstractions, such as C++, C#, Java, Swift, Objective-C, and Rust. C often remains in native libraries, performance modules, and platform layers.
Mobile Applications
It is uncommon to write ordinary mobile UIs and application logic entirely in C. Kotlin and Java are primarily used on Android, while Swift and Objective-C are primarily used on iOS.
C is occasionally used for the following purposes.
- Game engines
- Audio processing
- Video codecs
- Image libraries
- Cryptography
- Databases
- Network libraries
- Cross-platform runtimes
- Native performance modules
- Porting existing code
mobile UI and application logic
→ Kotlin, Java, and Swift
native core
→ C and C++
operating system
→ C, C++, Objective-C, and others
C libraries such as SQLite can be embedded and used within mobile applications.
Web Services
It is possible to write complete web services in C, but this is uncommon in ordinary business applications.
C is mainly used in the following web-related layers.
- Web servers
- Reverse proxies
- TLS libraries
- Compression
- HTTP parsers
- Network runtimes
- Database engines
- Language interpreters
- Cache servers
- Performance-critical extension modules
Other languages are widely used in higher-level web applications because of requirements such as the following.
- Rapid feature development
- String and JSON processing
- Automatic memory management
- Frameworks
- Database ORMs
- Asynchronous programming
- Deployment automation
- Team-based maintenance
Therefore, C can be regarded as a language that is widely used in the foundational layers of web services but only occasionally used for business logic.
Browsers and Web Runtimes
Web browsers and JavaScript runtimes are extremely large software systems that combine C, C++, Rust, and multiple other languages.
Areas where C may be used include the following.
- Network libraries
- Image decoders
- Audio and video codecs
- Compression
- Cryptography
- Operating system interfaces
- SQLite
- Font and text libraries
- Extension modules
Implementing the complete DOM and rendering engine of a modern browser purely in C is not the ordinary model today. C is included through many foundational libraries and external dependencies.
Cloud and Containers
Cloud control systems and orchestration tools are often written in Go, Java, Python, Rust, and other languages, but C is used in lower-level systems.
Representative areas include the following.
- Linux kernel
- Network stack
- Container isolation facilities
- C libraries
- Cryptography
- Compression
- Storage engines
- Hypervisor components
- Parts of eBPF runtimes and tools
- High-performance data paths
Containers are not a C language facility. They use operating system process-isolation and resource-control features. C is widely used in the kernels and system libraries underlying those facilities.
Virtualization
C is used in hypervisors, virtual machine monitors, and device emulation.
Major tasks include the following.
- Virtual CPU management
- Memory mapping
- Virtual devices
- Interrupts
- Disk images
- Network virtualization
- Hardware acceleration interfaces
- Guest firmware
An entire virtualization system can consist of C, C++, Rust, and other languages. C plays an important role in operating system and hardware APIs, emulated devices, and low-level runtimes.
Security Tools
In security, C is relevant to both offensive and defensive work.
Examples of legitimate security software include the following.
- Network packet analyzers
- Binary-analysis tools
- Debuggers
- Sandboxes
- Antivirus engines
- Cryptographic libraries
- Fuzzers
- Vulnerability scanners
- System-auditing tools
- Intrusion-detection systems
Understanding C’s memory model, ABI, assembly output, and operating system interfaces is important when analyzing and defending native software vulnerabilities.
At the same time, C is prone to memory-safety errors such as buffer overflows, use after free, and integer overflow, so memory-safe languages such as Rust are increasingly adopted alongside it for new security-sensitive components.
Electronic Design and Hardware Tools
C is used in some tools for hardware verification and electronic design automation, as well as in simulators and firmware models.
Major uses include the following.
- Instruction-set simulators
- Hardware test programs
- FPGA control software
- Firmware verification
- Circuit simulators
- Device models
- JTAG tools
- Manufacturing tests
Interfaces may also be provided to connect simulation models generated from hardware description languages with C programs.
Education
C is used to teach computer architecture, operating systems, memory, and compilation processes.
Major learning topics include the following.
- Variables and functions
- Pointers
- Arrays
- Structures
- Dynamic memory
- File input and output
- Data structures
- Compilation and linking
- Operating system APIs
- Processes and threads
- Networking
- Embedded systems
Because C provides relatively little abstraction, it makes computer memory and execution structures easy to observe. However, beginners may develop incorrect habits if they do not adequately understand object lifetimes, pointers, and undefined behavior, so tools and safe coding rules should be taught alongside the language.
Competitive Programming and Algorithms
C can be used for competitive programming and algorithm study, but C++ is currently often chosen more frequently because of its standard containers and algorithm library.
When using C, dynamic arrays, hash tables, string processing, and similar facilities must be implemented directly or obtained from separate libraries.
int binary_search(
const int *values,
size_t count,
int target
) {
size_t left = 0;
size_t right = count;
while (left < right) {
size_t middle =
left + (right - left) / 2;
if (values[middle] < target) {
left = middle + 1;
} else {
right = middle;
}
}
if (
left < count &&
values[left] == target
) {
return (int)left;
}
return -1;
}
C is useful for directly observing algorithmic memory layout and time complexity, but can require more implementation work.
General-Purpose Applications
C can be used to create almost every kind of application, including document editors, media players, web browsers, games, servers, and graphics programs.
However, the fact that something is possible does not mean that it is the most suitable choice in actual development.
Other languages may be more appropriate in projects where the following requirements are important.
- Complex object models
- Automatic memory management
- High-level asynchronous processing
- Safe concurrency
- Rapid UI development
- Large package ecosystems
- Runtime reflection
- Strong module systems
- Memory safety
- Rapid prototyping
Rather than being chosen as the language for an entire application, C may be selected for foundational modules that require performance, portability, and native interfaces.
Fields Where C Is Widely Used
The fields in which C is still widely used as a primary language can be summarized as follows.
| Field | Why C is used |
|---|---|
| Operating system kernels | Direct control over hardware and memory |
| Device drivers | Access to registers and interrupts |
| Embedded systems | Small runtimes and predictable execution |
| Real-time operating systems | Low overhead and support for many processors |
| System libraries | Stable ABI and cross-language compatibility |
| Language runtimes | Implementation of objects, memory, and native extensions |
| Database engines | File layout, caching, and concurrency control |
| Network servers | Control over events, buffers, and protocols |
| Codecs and media | SIMD and low-level data processing |
| Scientific data libraries | Portability and integration with other languages |
| Security and cryptographic libraries | CPU-specific optimization and common APIs |
| Bootloaders and firmware | Freestanding environments before operating system startup |
Fields Where C Is Used Occasionally
Fields in which C is not the primary language of the entire program but is used in some layers include the following.
| Field | Typical role of C |
|---|---|
| Web applications | Web servers, databases, and native modules |
| Mobile applications | Codecs, game engines, and shared native cores |
| Desktop UIs | System libraries and performance modules |
| Games | Platform, audio, video, and scripting runtimes |
| Machine learning | Numerical kernels and embedded inference |
| Browsers | Codecs, compression, databases, and system layers |
| Cloud systems | Kernels, storage, and network foundations |
| Virtualization | Device emulation and hardware interfaces |
| Scientific applications | Computational libraries beneath Python, R, and Fortran |
| Projects in other languages | C APIs and foreign function interfaces |
In these fields, users often do not directly see the C code. A user may write a program in Python, JavaScript, Kotlin, or C#, while internally using databases, image decoders, cryptographic libraries, compression libraries, and operating system libraries written in C.
Choosing C for a New Project
Choosing C for a new project cannot be decided solely by execution speed.
Conditions under which C is likely to be appropriate include the following.
- Direct access to the operating system or hardware is required.
- A small runtime and executable are needed.
- A freestanding environment must be supported.
- Explicit memory layout is required.
- Tight integration with existing C APIs is required.
- A common ABI callable from multiple languages is needed.
- Older platforms and compilers must be supported.
- Execution costs must be controlled precisely.
- A certified C toolchain must be used.
Conditions under which another language may deserve priority include the following.
- Memory safety is the highest priority.
- Complex web business logic must be developed quickly.
- A large GUI application must be built.
- Automatic memory management is needed.
- Safe concurrency abstractions are important.
- String and dynamic data processing are central.
- Development speed is more important than execution speed.
- A mature ecosystem already exists in another language.
C can also be used together with other languages.
high-level application
Python, C#, Java, and Lua
↓
C API boundary
↓
performance and system module
C
↓
operating system and hardware
In this structure, the C module’s interface should remain small and stable, and memory ownership, error propagation, and threading rules must be defined clearly.
Summary of Fields of Application
C appears in nearly every field that uses computers, but its strongest position is in layers that directly implement the foundations of software.
applications and services
↓
language runtimes and frameworks
↓
databases, codecs, cryptography, and networking
↓
system libraries
↓
operating system kernels and drivers
↓
hardware
As one moves downward through this hierarchy, memory layout, execution costs, ABIs, and hardware control become more important, and the proportion of C usage tends to increase. As one moves upward, development productivity, safety, frameworks, and dynamic features become more important, so other languages are often used as the primary language.
C is frequently used as the central language in operating systems, embedded systems, language runtimes, databases, networking, and multimedia libraries. In games, graphics, mobile and desktop applications, web software, and machine learning, it is used more occasionally in performance-critical cores, native libraries, and foundational layers called by other languages rather than throughout the entire program.
C’s broad range of applications does not result from the language being the most convenient choice in every field. It results from its ability to be implemented across many kinds of hardware, provide a small runtime and stable ABI, and directly access memory and operating system interfaces. These advantages coexist with the responsibility placed on programmers to manage object lifetimes, array bounds, errors, and concurrency directly.
Relationship with Other Programming Languages
C developed directly from BCPL and B (programming language), and later influenced the syntax, execution models, and system interfaces of many programming languages. Some languages directly extended C, while others adopted only C’s brace-and-operator syntax and designed entirely different memory models and execution environments.
The relationship between C and other languages cannot be explained through a single lineage alone.
BCPL
↓
B
↓
C
├── Direct extensions
│ ├── C++
│ └── Objective-C
│
├── Syntactic influence
│ ├── Java
│ ├── C#
│ ├── JavaScript
│ ├── Go
│ ├── Rust
│ ├── Zig
│ └── Wave
│
├── System interfaces
│ ├── Operating system APIs
│ ├── C ABI
│ ├── Standard libraries
│ └── Foreign function interfaces
│
└── Implementation foundation
├── Language runtimes
├── Interpreters
├── Virtual machines
└── Extension modules
A language influenced by C is not necessarily derived from C. Even when the syntax is similar, a language should be regarded as separate if its type system, memory management, object model, and execution method are different.
BCPL and B
The direct predecessors of C are BCPL and B (programming language).
BCPL was a concise language designed for implementing compilers and system software and represented values primarily in terms of a single machine word. B was a language created by Ken Thompson by simplifying BCPL so that it could be used on smaller computers.
C inherited the following elements from B.
- Function-centered program structure
- Brace-delimited blocks
- A close relationship between pointers and arrays
- Many arithmetic and bitwise operators
if,while, andreturn- Concise expression syntax
- The goal of directly implementing system programs
B was an almost typeless language, but C introduced char, int, pointer types, and structures to handle the PDP-11’s byte-addressable memory and data of different sizes.
int values[4];
int *pointer = values;
C’s rule that an array name is converted in most expressions into a pointer to its first element developed from the memory models of BCPL and B.
C-Family Languages
Languages that share C’s braces, semicolons, expressions, and control-statement syntax are sometimes broadly described as C-family languages.
Representative common syntax includes the following.
if (condition) {
process();
}
for (int index = 0; index < count; ++index) {
process(values[index]);
}
The following elements repeatedly appear across many languages.
- Blocks using
{} - Statements ending with semicolons
if,else, andswitchfor,while, anddobreak,continue, andreturn+,-,*, and/==,!=,<, and>&&,||, and!++and--- Array subscripting with
[] - Function calls with
()
However, identical surface syntax does not mean that the programs have the same semantics.
C
→ Direct memory access
→ Manual memory management
→ Undefined behavior exists
→ Strongly affected by implementations and ABIs
Java
→ Virtual machine
→ Garbage collection
→ Array bounds checking
→ No pointer arithmetic
Rust
→ Ownership and borrow checking
→ Separation of safe and unsafe code
→ Memory-safety checks by default
Go
→ Garbage collection
→ Goroutines and channels
→ Restricted pointer operations
The term C family is useful for explaining syntactic relationships, but it should not be used as a criterion for judging language safety, execution cost, or memory models.
C++
C++ is one of the languages most closely related to C. Bjarne Stroustrup began work in 1979 on adding classes and object-oriented features to C, and the early language was called C with Classes. The name C++ came into use in 1983, and the first commercial implementation and language book were released in 1985.[110]
Early C++ directly used the following elements of C as its foundation.
- Fundamental data types
- Pointers and arrays
- Structures
- Functions
- Preprocessor
- Separate compilation
- Operating system and hardware access
- Compatibility with C libraries
- Native code generation
The following features were added.
- Classes
- Access control
- Constructors and destructors
- Inheritance
- Virtual functions
- Function and operator overloading
- References
- Templates
- Exceptions
- Namespaces
- Generic programming
- Standard Template Library
- Lambdas
- Automatic resource management
Object-like structures can be created in C using structures and functions.
struct counter {
int value;
};
void counter_increment(struct counter *counter) {
++counter->value;
}
In C++, data and behavior can be combined within a class.
class Counter {
public:
void increment() {
++value_;
}
private:
int value_ = 0;
};
C++ has historically maintained a high degree of compatibility with C, but modern C and C++ are separate languages managed by different international standards and committees.
C
→ WG14
→ ISO/IEC 9899
C++
→ WG21
→ ISO/IEC 14882
Not every valid C program can be compiled unchanged as a C++ program.
For example, C permits an implicit conversion from void * to another object-pointer type.
void *memory = malloc(sizeof(int));
int *value = memory;
C++ requires an explicit conversion or a different allocation method.
void *memory = std::malloc(sizeof(int));
int *value = static_cast<int *>(memory);
C and C++ differ in many areas, including keyword sets, type rules, initialization, string literals, enumerations, and function declarations.
Mixing C and C++
C++ programs often call libraries written in C. The extern "C" declaration can be used in a C++ compiler to specify C linkage.
extern "C" {
int library_initialize(void);
void library_shutdown(void);
}
A header intended to be included from both C and C++ can be written as follows.
#ifndef LIBRARY_H
#define LIBRARY_H
#ifdef __cplusplus
extern "C" {
#endif
struct library_context;
struct library_context *
library_create(void);
void library_destroy(
struct library_context *context
);
#ifdef __cplusplus
}
#endif
#endif
extern "C" is a feature for adjusting C++ name mangling and linkage conventions. It does not make the complete C and C++ object models identical.
Directly exposing C++ classes, exceptions, and template types across a C ABI boundary has low portability. Shared interfaces generally use fundamental types, pointers, explicit structures, and opaque handles.
Objective-C
Objective-C is a language that adds object-oriented message passing and a dynamic runtime to C. Apple’s official documentation describes Objective-C as a superset of C and explains that C syntax, fundamental types, structures, functions, pointers, and control statements can be used directly.[111]
An ordinary C function can be used unchanged in Objective-C source.
int add(int left, int right) {
return left + right;
}
Objective-C adds syntax for classes, methods, and sending messages to objects.
@interface Counter : NSObject
@property int value;
- (void)increment;
@end
@implementation Counter
- (void)increment {
self.value += 1;
}
@end
A method call is expressed as message passing using square brackets.
[counter increment];
Objective-C preserves the following elements of C.
- Pointers
- Structures
- Functions
- Preprocessor
- C standard library
- High compatibility with the C ABI
- Mixing with C source
It adds the following elements.
- Classes and objects
- Message passing
- Protocols
- Categories
- Dynamic method lookup
- Runtime reflection
- Automatic reference counting
Objective-C++ also allows C, C++, and Objective-C to be used together in one source file.
Java
Java was strongly influenced by the syntax of C and C++, but it is not a direct superset of C. The Java Virtual Machine specification explains that Java syntax resembles C and C++, while removing many features regarded as complex, confusing, or unsafe.[112]
Java inherited surface syntax such as the following from C.
if (value > 0) {
process(value);
}
for (int index = 0; index < count; ++index) {
process(values[index]);
}
Its execution model is substantially different.
| Item | C | Java |
|---|---|---|
| Typical execution | Native code | JVM bytecode and JIT |
| Memory management | Manual management | Garbage collection |
| Pointer arithmetic | Supported | Not supported |
| Array bounds | Generally unchecked | Checked at runtime |
| Objects | Structures and manual implementation | Class-based |
| Inheritance | No language feature | Class inheritance |
| Exceptions | No language feature | Exception handling |
| Unit of portability | Source and ABI | Bytecode and JVM |
Java references are not syntactically or semantically equivalent to C pointers. Their numerical addresses cannot be obtained, and arbitrary pointer arithmetic cannot be performed.
In exchange for reducing C’s direct control, Java makes the execution environment manage object lifetimes and array bounds.
C#
C# is a language that follows the syntactic tradition of C, C++, and Java. Its name itself indicates continuity with C-family languages, but it is not a superset of C.
public static int Add(
int left,
int right
) {
return left + right;
}
C# generally runs on the .NET runtime and provides the following features.
- Garbage collection
- Classes and structures
- Properties
- Delegates
- Events
- Generics
- Exceptions
- Reflection
- Asynchronous functions
- Managed arrays and strings
- Runtime type information
Most C# code does not directly use C pointers. Pointers can be used in an unsafe context when necessary.
unsafe static int Read(int *pointer) {
return *pointer;
}
C# can call C ABI libraries through P/Invoke and native interoperability facilities.
[DllImport("example")]
private static extern int add(
int left,
int right
);
The structure layout, string encoding, calling convention, and ownership rules of the C library must exactly match the C# declarations.
JavaScript
JavaScript was influenced by Java in its name and early syntax design, while its control statements and operator structure broadly belong to C-family syntax.
if (value > 0) {
process(value);
}
for (let index = 0; index < count; ++index) {
process(values[index]);
}
However, JavaScript’s type system and execution method differ substantially from C.
C
→ Static typing
→ Direct object representation
→ Pointers
→ Manual memory management
→ Native execution
JavaScript
→ Dynamic typing
→ Object-based execution model
→ Garbage collection
→ No direct pointers
→ Execution within a JavaScript engine
JavaScript engines and browser runtimes can use libraries and system components written in C and C++. C code can also be executed in browsers and other sandboxed environments through WebAssembly.
Go
Go (programming language) shares some of C’s concise syntax and native compilation model, but its memory management and concurrency were designed differently.
func add(left int, right int) int {
return left + right
}
Go has the following characteristics.
- Garbage collection
- Goroutines
- Channels
- Interfaces
- Some structural typing
- Slices
- Maps
- Automatically growing stacks
- Restricted pointers
- No pointer arithmetic
Go pointers can indirectly access objects, but ordinary C-style pointer arithmetic is not permitted.
value := 10
pointer := &value
*pointer = 20
Go can call C code through cgo.
/*
int add(int left, int right) {
return left + right;
}
*/
import "C"
func main() {
result := C.add(10, 20)
_ = result
}
The official Go FAQ explains that C and Go can operate within the same address space, but interface code is required and some of the memory-safety and stack-management characteristics of pure Go are lost.[113]
The following issues must be managed at a C boundary.
- How long C may retain a Go pointer
- Objects managed by the garbage collector
- Threads and goroutines
- Callbacks
- String and array conversion
- Releasing C memory
- Call overhead
Rust
Rust targets the system programming domain traditionally served by C and C++, while using ownership and borrow checking to reduce memory-safety problems.
fn add(left: i32, right: i32) -> i32 {
left + right
}
Rust includes the following features in its language and type system.
- Ownership
- Borrowing
- Lifetimes
- Pattern matching
- Enumerations and algebraic data types
ResultandOption- Traits
- Generics
- Prevention of null references by default
- Prevention of data races
- Separation of safe and
unsafecode
In C, ownership of a dynamically allocated object is not represented in its type.
struct resource *resource_create(void);
void resource_destroy(
struct resource *resource
);
The caller must read the documentation and release the object exactly once.
In Rust, ownership can be expressed through a type whose resources are cleaned up when the object leaves scope.
struct Resource {
data: Vec<u8>,
}
Rust does not make undefined behavior impossible in every program. Raw pointer dereferencing, foreign function calls, and some low-level operations are performed in an unsafe context.
unsafe fn read(pointer: *const i32) -> i32 {
*pointer
}
The official Rust Reference explains that undefined behavior must not occur even in unsafe code and that unsafe means responsibility for following those rules moves to the programmer.[114]
Rust can declare and call C ABI functions.
unsafe extern "C" {
fn add(left: i32, right: i32) -> i32;
}
A Rust library can also expose an interface callable from C.
#[unsafe(no_mangle)]
pub extern "C" fn add(
left: i32,
right: i32,
) -> i32 {
left + right
}
Across the language boundary, structure layout, panics, strings, memory ownership, and callback lifetimes must be restricted and defined clearly.
Zig
Zig targets a system programming domain similar to C and provides explicit memory allocation, compile-time execution, error sets, optional types, and related features.
Zig treats interoperability with C as an important feature and can import C headers or create C ABI libraries.
const c = @cImport({
@cInclude("example.h");
});
Zig preserves or uses the following characteristics of C.
- Native code
- Direct memory control
- C ABI
- Operating system and hardware access
- An execution model that can minimize a separate runtime
It handles the following elements more explicitly.
- Error handling
- Optional values
- Arrays and slices
- Compile-time computation
- Allocator passing
- Integer overflow operations
- Pointer categories
- Safety checks intended to reduce undefined behavior
Zig is not only a programming language, but can also be used as a build and cross-compilation tool for C and C++ projects.
Wave
Wave (programming language) is a programming language developed for the system programming and low-level control domains that C has served for decades. Development began in 2024, and an initial version was released in 2025.
Wave aims to retain the direct memory control offered by C pointers while reducing problems caused by pointer misuse, object-lifetime violations, and undefined behavior.
In C, programmers must directly manage the lifetime, aliasing, nullability, and valid range of the object referenced by a pointer.
int *create_value(void) {
int value = 10;
return &value;
}
When the function ends, the lifetime of value ends, so the returned pointer no longer refers to a valid object. Dereferencing it causes undefined behavior.
int *pointer = create_value();
int result = *pointer;
C permits this code to be expressed syntactically, and compiler diagnostics are not always guaranteed.
Wave was developed in a direction that more explicitly incorporates pointer types, mutability, and compiler checks into the language design to address these pointer and undefined-behavior problems in C.
Wave pointers specify their type in the form ptr<T>.
fun main() {
var value: i32 = 10;
var pointer: ptr<i32> = &value;
println("{}", deref pointer);
}
The early pre-beta version of Wave released in 2025 introduced the ptr<T> pointer type, address operator, and explicit dereference syntax.[115]
It also provides syntax that distinguishes mutability in variable declarations.
let immutable_value: i32 = 10;
let mut controlled_value: i32 = 20;
var mutable_value: i32 = 30;
This design attempts to express mutability and intent more explicitly than C’s variable model, in which ordinary objects are generally freely modifiable.[116]
Wave is a system programming language aimed at low-level control and high performance and pursues a structure in which functionality is provided through the standard library rather than placing many built-ins directly in the compiler.[117]
The early version released in 2025 was still at the pre-beta stage and therefore cannot be regarded as a completed standard language ready to replace C immediately. However, it is one example of a modern system language that attempts to preserve C’s direct hardware access and native execution while reducing recurring C errors through pointer safety, explicit mutability, and stronger compiler checks.
C
→ Direct pointers and memory control
→ Few runtime dependencies
→ Broad domain of undefined behavior
→ Programmer-managed object lifetimes and aliasing
Wave
→ Oriented toward system programming and low-level control
→ Explicit pointer types
→ Explicit dereferencing
→ Mutability distinctions
→ Intended to reduce errors through compiler checks
Wave’s relationship with C is closer to preserving the system programming domain pioneered by C while addressing pointer and undefined-behavior problems through a new language design than to being a simple syntactic extension of C.
D
D (programming language) is a language that adds modern language features while retaining syntax and system programming capabilities similar to those of C and C++.
D provides the following features.
- Classes and structures
- Garbage collection
- Templates
- Ranges
- Contract programming
- Modules
- Compile-time function execution
- Optional manual memory management
- C ABI interoperability
extern(C)
int add(int left, int right) {
return left + right;
}
D is not a direct superset of C. Rather than directly including C headers as valid syntax, it generally interoperates by declaring C ABI interfaces separately.
Swift
Swift was designed to replace or coexist with Objective-C and C-based frameworks on Apple platforms.
Swift provides the following features.
- Automatic reference counting
- Optional types
- Structures and classes
- Protocols
- Generics
- Pattern matching
- Error handling
- Value types
- Bounds checking
Swift projects can import and use C and Objective-C headers as modules.
int library_add(int left, int right);
In Swift, the imported function can be called like an ordinary function.
let result = library_add(10, 20)
When a C interface lacks information about nullability, array lengths, or ownership, Swift cannot automatically infer complete safety. C APIs in the Apple ecosystem can add nullability annotations, attributes, and module metadata so that other languages can use them more accurately.
Python
Python differs greatly from C syntactically, but has a deep relationship with C through its implementation and extension ecosystem.
The representative implementation, CPython, is implemented in C, and the Python/C API can be used to write extension modules or embed the Python interpreter in a C program.
#include <Python.h>
static PyObject *add_values(
PyObject *self,
PyObject *arguments
) {
int left;
int right;
if (!PyArg_ParseTuple(
arguments,
"ii",
&left,
&right
)) {
return NULL;
}
return PyLong_FromLong(
left + right
);
}
Many libraries visible from Python internally use native modules written in C, C++, and Fortran.
Python code
↓
Python C API
↓
C extension module
↓
Numerical, image, and database libraries
C extension modules combine Python’s high-level interface with C’s execution performance and existing libraries.
However, a memory error in a C extension is not safely contained as a Python exception and can terminate or corrupt the entire interpreter.
Lua
Lua is a scripting language designed to be easy to embed in C programs. Its implementation and public API are centered on C.
A C program can create a Lua state and execute a script.
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
int run_script(const char *path) {
lua_State *state =
luaL_newstate();
if (state == NULL) {
return -1;
}
luaL_openlibs(state);
int result =
luaL_dofile(state, path);
lua_close(state);
return result;
}
Lua is used as a scripting layer above C or C++ cores in game engines, applications, servers, and tools.
C or C++ engine
↓
Lua C API
↓
Lua game or tool scripts
C functions can also be registered with Lua.
static int add_values(
lua_State *state
) {
lua_Integer left =
luaL_checkinteger(state, 1);
lua_Integer right =
luaL_checkinteger(state, 2);
lua_pushinteger(
state,
left + right
);
return 1;
}
C as an Implementation Language for Other Languages
C has been used as the implementation language for many programming languages.
Representative examples include the following.
- CPython
- Lua
- CRuby
- PHP implementations
- Perl
- R
- Tcl
- Multiple Scheme and Lisp implementations
- Shells and command interpreters
- Virtual machines
- Bytecode runtimes
C is selected as an implementation language for the following reasons.
- Compilers exist for many operating systems and processors.
- Operating system APIs can be accessed directly.
- It provides a small and stable ABI.
- It is easy to call from other languages.
- Runtimes and memory managers can be designed directly.
- A long-established library ecosystem exists.
A language being implemented in C does not mean that user code in that language has C’s memory model.
Python user code
→ Garbage collection and Python object rules
CPython internals
→ C objects, pointers, and reference-count management
The implementation language and the language semantics exposed to users must be distinguished.
C ABI
One of the most important reasons C maintains a deep relationship with other languages is the C ABI.
A C ABI is a set of rules promised by the operating system and compiler at the binary level for elements such as the following.
- Passing function arguments
- Passing return values
- Structure layout
- Sizes of fundamental types
- Pointer representation
- Stack alignment
- Register preservation
- Symbol names
- Dynamic library formats
- Calling conventions
The C standard itself does not specify one universal ABI. The actual ABI is determined by the target platform.
C language
+
compiler
+
operating system
+
processor ABI
=
actual C binary interface
C APIs are generally designed in simple forms such as the following.
struct image;
struct image *image_create(
size_t width,
size_t height
);
bool image_load(
struct image *image,
const char *path
);
void image_destroy(
struct image *image
);
This interface is comparatively easy to bind from the following languages.
- C++
- Rust
- Zig
- Wave
- Python
- C#
- Java
- Swift
- Go
- Lua
- Ruby
Foreign Function Interfaces
A facility that allows another language to call C functions is called a foreign function interface, or FFI.
The general structure is as follows.
Higher-level language
↓
FFI declaration
↓
C ABI
↓
Shared library
↓
C implementation
At an FFI boundary, data representation and lifetime are more important than syntax.
Consider the following C structure.
struct point {
int32_t x;
int32_t y;
};
Another language must use exactly the same layout and alignment.
Rust uses repr(C).
#[repr(C)]
struct Point {
x: i32,
y: i32,
}
C# can use a structure-layout attribute.
[StructLayout(LayoutKind.Sequential)]
struct Point {
public int X;
public int Y;
}
If the layouts do not match, values can be interpreted incorrectly or memory corruption can occur.
Opaque Pointers
C APIs shared with other languages frequently use opaque pointers to hide internal structures.
Only the structure name is declared in the header.
struct database;
Creation and destruction functions are provided.
struct database *database_open(
const char *path
);
void database_close(
struct database *database
);
The structure definition is placed only in the C implementation file.
struct database {
FILE *file;
void *cache;
size_t page_count;
};
This approach has the following advantages.
- Reduces ABI exposure
- Allows internal implementation changes
- Prevents other languages from directly accessing structure members
- Can make ownership boundaries clearer
- Reduces differences among compilers and languages
Callbacks
C function pointers form the basis for passing callbacks from another language to a C library.
typedef void (*event_callback)(
int event,
void *context
);
void event_set_callback(
event_callback callback,
void *context
);
void *context passes user state together with the callback.
struct application {
int event_count;
};
static void handle_event(
int event,
void *context
) {
struct application *application =
context;
++application->event_count;
}
The following issues can arise when passing callbacks from another language.
- Lifetime of the callback object
- Garbage collector movement
- Calling thread
- Exception or panic propagation
- Calling convention
- Callbacks invoked after shutdown
- Ownership of user data
C++ exceptions, Rust panics, and runtime exceptions from other languages must not be allowed to propagate directly across a C function boundary.
String Interoperability
A C string is a byte array terminated by a null character.
const char *name =
"TechPedia";
Strings in other languages can differ as follows.
C
→ Null-terminated byte string
→ May have no separate length information
Rust
→ UTF-8 bytes and length
→ Allows interior null characters
Java
→ JVM string object
→ UTF-16-based representation
C#
→ .NET string object
→ UTF-16-based representation
Swift
→ Unicode character collection
→ Abstract internal representation
Encoding, length, null characters, and ownership must be defined clearly at a language boundary.
A C API that passes a length together with the string can be more general.
bool document_parse(
const char *text,
size_t length
);
An API requiring a null-terminated string should document that requirement.
bool document_open(
const char *null_terminated_path
);
Memory Ownership
A C ABI does not automatically express memory ownership through types.
It can be difficult to determine from the declaration alone who must release the pointer returned by the following function.
char *document_get_title(
struct document *document
);
Possible meanings include the following.
1. The caller releases it with free.
2. It must be released with a dedicated function.
3. It is owned by document.
4. It remains valid until the next call.
5. It remains valid until program termination.
6. It is an immutable internal string.
An API should make ownership as clear as possible through function names, documentation, and types.
char *document_title_copy(
const struct document *document
);
void document_string_free(
char *string
);
Alternatively, it can return a non-owning pointer.
const char *document_title_view(
const struct document *document
);
Bindings for another language must convert these rules into that language’s resource-management model.
Error Propagation
C functions communicate errors in several ways.
- Integer error codes
- Boolean results
- Null pointers
errno- Output parameters
- Separate error objects
- Callbacks
enum result_code {
RESULT_SUCCESS,
RESULT_INVALID_ARGUMENT,
RESULT_OUT_OF_MEMORY,
RESULT_IO_ERROR
};
enum result_code document_open(
const char *path,
struct document **result
);
This form is comparatively easy to convert into exceptions, Result, or error objects in another language.
C error code
↓
Rust Result
C# Exception
Python Exception
Go error
Functions crossing a C boundary should convert language-specific exceptions into results representable in C rather than allowing those exceptions to propagate directly.
Pointers and Undefined Behavior
One of the most important differences between C and modern system languages is how they handle pointers and undefined behavior.
The following actions can cause undefined behavior in C.
- Dereferencing a null pointer
- Use after free
- Out-of-bounds array access
- Accessing an object after its lifetime ends
- Dereferencing an unaligned pointer
- Access through an incompatible type
- Data races
- Signed integer overflow
- Invalid pointer arithmetic
- Violating a
restrictcontract
int *pointer = NULL;
*pointer = 10;
The C standard leaves these behaviors undefined to avoid imposing identical checking costs on all hardware and to allow implementations to perform strong optimization.
However, undefined behavior does not merely mean that a program returns an unexpected value. A compiler can assume that such behavior does not occur in a correct program.
WG14 documents concerning pointer provenance discuss the need to interpret pointers not merely as integer addresses, but as values that also carry information about the objects and allocations from which they originated.[118]
This issue is one of the reasons modern system languages such as Rust, Zig, and Wave have designed the following features.
- Restrictions on pointer arithmetic
- Distinction between arrays and slices
- Type-level representation of nullability
- Ownership
- Lifetime checking
- Explicit unsafe regions
- Integer-overflow checking
- Mutability distinctions
- Stronger compiler diagnostics
Languages That Replace or Complement C
New system languages are often introduced as complete replacements for C, but their actual relationship is more complex.
Replacement
→ Write new modules in another language
→ Gradually remove existing C code
Complement
→ Write a safe higher-level layer in the new language
→ Call existing C libraries through FFI
Coexistence
→ Use C at platform and ABI boundaries
→ Use Rust, Zig, or Wave for core modules
→ Retain existing libraries in C
C has an enormous existing codebase, operating system APIs, hardware SDKs, and libraries. Therefore, new system languages generally provide C ABI interoperability as well.
The fact that even languages seeking to replace C cannot easily abandon C interoperability shows that C serves not only as a source language, but also as a common interface for native software.
Using C as an Intermediate Language
Some language implementations translate source code into C and then use an existing C compiler to generate native code.
New language source
↓
C code generation
↓
C compiler
↓
Object file
↓
Executable program
This approach has the following advantages.
- Reuse of C compilers on many platforms
- Use of existing optimization back ends
- Reuse of ABIs and linker tools
- Reduced burden of platform-specific code generation
- Use of C debuggers and analysis tools
It also has disadvantages.
- Semantic differences between the original language and C
- Mapping of debugging information
- Possible exposure to C undefined behavior
- Limits on optimization control
- Increased compilation time
- Increased C source size
Using C as an intermediate representation was adopted by several language implementations even before separate intermediate representations such as LLVM IR and WebAssembly became widespread.
WebAssembly
C can be compiled to WebAssembly.
C source
↓
Clang, LLVM, or Emscripten
↓
WebAssembly module
↓
Browser or WASI runtime
In a WebAssembly environment, a C pointer is generally implemented as an offset into linear memory.
C pointer
→ WebAssembly linear memory address
A separate binding layer is required between browser JavaScript and a C module.
const result = module.exports.add(
10,
20
);
WebAssembly does not automatically eliminate undefined behavior from C. Invalid pointer arithmetic and lifetime violations at the C source level can still affect compiler optimization.
Operating System APIs
Many operating system APIs are exposed in the form of C functions and structures.
int open(
const char *path,
int flags,
...
);
void *mmap(
void *address,
size_t length,
int protection,
int flags,
int file,
off_t offset
);
Large parts of the Windows API are also based on C calling interfaces.
HANDLE CreateFileW(
LPCWSTR file_name,
DWORD access,
DWORD sharing,
LPSECURITY_ATTRIBUTES security,
DWORD creation,
DWORD attributes,
HANDLE template_file
);
Operating system libraries in other languages often wrap these C APIs.
Rust standard library
Go runtime
Python os module
Java native runtime
.NET runtime
↓
Operating system C ABI or system calls
This does not mean that every operating system facility is actually implemented in C. It means that public ABIs and headers are often provided in C form.
Common Library Boundary
C libraries form a common foundation shared by many languages.
Representative examples include the following.
- SQLite
- zlib
- libpng
- libjpeg
- OpenSSL
- libcurl
- FFmpeg
- Lua
- Git libraries
- Operating system standard libraries
- Graphics APIs
- Device SDKs
Bindings for multiple languages can be created around one C library.
Python binding
│
Rust binding ─ C library ─ C# binding
│
Go binding
This structure has the advantage of allowing one implementation to be maintained and used from multiple languages.
However, a C ABI boundary cannot automatically communicate the following information.
- Ownership
- Array lengths
- Nullability
- String encoding
- Thread safety
- Error semantics
- Callback lifetimes
- Exception handling
- Asynchronous execution rules
Therefore, creating a binding is not merely a matter of connecting function names.
Language-Independent API Design
A C API intended for use from multiple languages should generally be designed more restrictively than an internal C-only API.
Suitable elements include the following.
- Fixed-width integers
- Explicit sizes
- Opaque handles
- Simple structures
- Error codes
- Explicit creation and destruction
- Callbacks and user data
- Version fields
- Function tables
struct library_api {
uint32_t structure_size;
uint32_t version;
struct library_context *
(*create)(void);
void (*destroy)(
struct library_context *context
);
};
The following elements should be avoided.
- Compiler-specific bit fields
- Careless exposure of variable-length structures
- Types such as
longwhose width differs by platform - C++ classes
- Exception propagation
- Direct exposure of internal pointers
- Return values with unclear ownership
- Temporary pointers valid only during a call
- Undocumented thread requirements
Syntactic Influence and Semantic Differences
C syntax created a common system of expression that lowered the learning barrier for later languages.
if (...)
for (...)
while (...)
return ...
function(...)
array[index]
A developer familiar with one C-family language can easily read the basic control flow of another.
However, similar syntax can also produce incorrect assumptions.
The following code looks similar in C and Java.
values[index] = 10;
In C, bounds checking may be absent.
index out of range
→ Undefined behavior
Java checks array bounds.
index out of range
→ Exception
The following code also looks similar in C and C#.
object = NULL;
In C, this generally changes only the value of one pointer variable. A dynamically allocated object is not automatically released.
In a garbage-collected language, an object that is no longer referenced can later be reclaimed by the runtime.
Even when syntax is the same, object lifetime and error-handling rules must be understood according to the language in question.
Influence and Limitations
C widely spread the following programming practices.
- Brace-based blocks
- Expression-oriented syntax
- Semicolon-terminated statements
- Pointers and arrays
- Structures
- Functions and separate compilation
- Headers and libraries
- Preprocessing
- Native ABIs
- Treating zero as false in conditions
- Operator-precedence systems
At the same time, C’s limitations also influenced later language design.
C manual memory management
→ Garbage collection
→ Ownership and RAII
Null pointers
→ Optional types
→ Null safety
Absence of array bounds checking
→ Slices
→ Runtime bounds checking
Error codes
→ Exceptions
→ Result types
Preprocessor macros
→ Generics
→ Templates
→ Hygienic macros
Undefined behavior
→ Safe language subsets
→ Explicit unsafe boundaries
Header-based interfaces
→ Module systems
→ Package systems
Languages developed after C have generally attempted not merely to copy it, but to preserve its performance and portability while providing greater safety and abstraction.
Overall Relationship
The relationship between C and other programming languages can be summarized in four major categories.
Direct lineage
BCPL → B → C
Direct extensions of C
C → C++
C → Objective-C
Syntactic and design influence
C → Java
C → C#
C → JavaScript
C → Go
C → Rust
C → Zig
C → Wave
Implementation and interoperability
C ABI
C libraries
C-based runtimes
FFI and extension modules
C++ and Objective-C emerged by directly extending C, but today each has an independent language, standard, and object model. Java, C#, JavaScript, and Go inherited parts of C’s syntax, but their execution environments and memory-management methods differ substantially.
Modern system languages such as Rust, Zig, and Wave inherit the low-level programming domain pioneered by C while attempting to reduce problems involving pointers, object lifetimes, error handling, and undefined behavior.
C did not disappear as new languages emerged. Instead, it became a language with which most new system languages must interoperate. This is because a large proportion of operating system APIs, hardware SDKs, databases, multimedia systems, cryptographic libraries, and networking libraries provide C ABIs.
Therefore, C’s influence remains not only in source syntax. It also remains in how native code calls other native code, in public library interfaces, and at the boundaries between operating systems and language runtimes. In this sense, C is both a programming language and a common binary interface for the modern software ecosystem.
Influence
C began as a language created to implement a single operating system, but it later exerted broad influence on programming language syntax, the structure of system software, operating system interfaces, library and compiler design, and software education.
C’s influence lies not only in the fact that many programs were written in C. C demonstrated that an operating system could be separated from the assembly language of a particular computer and that common source code could be used across different machines. It also widely spread the model of combining a small language core centered on functions, structures, and pointers with separate libraries.
Influence of C
├── Portability of UNIX and operating systems
├── Benchmark for system programming languages
├── Spread of C-family syntax
├── Common foundation for native ABIs
├── Library-centered software structure
├── Development of compilers and toolchains
├── Embedded and hardware software
├── Performance- and data-layout-oriented thinking
├── Computer science education
├── Open-source development culture
└── Discussions of memory safety and undefined behavior
C came to be regarded as a language that provides high-level abstraction while still allowing programmers to predict generated machine code and memory layout relatively directly. This position caused many later system languages to use C as a point of comparison when describing themselves.
Portability of UNIX
One of C’s most important early influences was separating UNIX from the assembly language of a specific computer.
Early UNIX was written in the assembly languages of the PDP-7 and PDP-11. Porting the operating system to another processor required rewriting most of the code. After C had matured sufficiently, a substantial part of the UNIX kernel was rewritten in C, making it possible to separate common operating system logic from hardware-specific code.
Assembly-based operating system
→ Strongly tied to a particular processor
C-based operating system
→ Common kernel logic written in C
→ Assembly used only for some startup code and hardware access
→ Portable to new computers
Dennis Ritchie explained that C formed the foundation that made UNIX portable. As C and UNIX evolved together, the language acquired the facilities needed to implement a real operating system, while UNIX could be moved to new hardware for which a C compiler existed.[119][120]
The process of porting UNIX demonstrated to industry and academia that an entire operating system could be written in a high-level language. Later operating systems could adopt structures such as the following.
Operating system
├── Common C code
│ ├── File system
│ ├── Process management
│ ├── Networking
│ └── Memory management
│
└── Architecture-specific code
├── Booting
├── Interrupt entry
├── Context switching
└── Special instructions
Modern operating systems are not composed solely of C and may use C++, Rust, assembly language, and other languages together. However, separating common kernel code from hardware-specific layers remains an important structure widely spread by C and UNIX.
Operating System Development Methods
C also influenced the fundamental methods used to express operating system kernels.
Major operating system concepts are often represented using C structures and pointers.
struct process {
unsigned long identifier;
unsigned int state;
void *address_space;
struct process *parent;
};
It became common to represent operating system objects such as processes, files, devices, and network packets as structures and to have functions receive pointers to those structures and modify their state.
void process_set_state(
struct process *process,
unsigned int state
) {
process->state = state;
}
The Linux kernel is still developed primarily in C, and its official documentation explains that it is generally compiled using the GNU C11 dialect.[121]
Linux does not use only standard C. It also uses GNU extensions, architecture-specific assembly language, compiler built-ins, and Rust code. Even so, the kernel’s core data structures, internal APIs, and driver interfaces are centered on C’s type and function model.
Benchmark for System Programming Languages
C became a benchmark for evaluating later system programming languages.
New system languages are often described by comparing them with C in terms such as the following.
- Whether they provide execution performance comparable to C
- Whether they can control memory at the same level as C
- Whether they require a separate garbage collector
- Whether they can implement operating systems or firmware
- Whether they can interoperate with the C ABI
- Whether they are more memory-safe than C
- Whether they contain less undefined behavior than C
- Whether executable and runtime sizes remain small
New system language
↓ Points of comparison
C performance
C portability
C memory control
C ABI
C runtime dependencies
This does not mean that C is always the best choice in every systems field. It means that C has long represented the minimum set of capabilities expected of system software.
C++, Rust, Zig, D, Wave (programming language), and other languages attempt in different ways to extend or complement the system programming domain historically served by C.
Machine-Independent Low-Level Language
C is often described as a high-level assembly language, but it is not an assembly language that directly expresses the instructions of a particular processor.
One of C’s important influences was that it provided a machine-independent low-level representation.
struct register_block {
volatile uint32_t control;
volatile uint32_t status;
};
void device_enable(
struct register_block *device
) {
device->control |= 1U;
}
This code uses structures, pointers, and bit operations to represent device state, but the actual registers and instruction selection are determined by the target compiler and platform.
C offered a compromise between the following two requirements.
High-level language
→ Portability
→ Functions and data structures
→ Abstract representation
Low-level language
→ Addresses and memory
→ Bit operations
→ Hardware control
→ Predictable costs
This characteristic became one of the reasons C remained in long-term use in operating systems, embedded systems, compiler runtimes, and database engines.
C-Family Syntax
C also exerted enormous influence on the surface syntax of programming languages.
Forms such as the following repeatedly appeared in later languages.
if (condition) {
process();
}
for (int index = 0; index < count; ++index) {
process(values[index]);
}
return result;
Representative syntactic elements spread by C include the following.
- Brace-delimited blocks
- Semicolon-terminated statements
- Conditions enclosed in parentheses
ifandelseswitchandcasefor,while, anddobreak,continue, andreturn()for function calls[]for array subscripting.for structure-member access->for pointer-member access++and--&&,||, and!==,!=,<=, and>=- Compound assignment operators
C++ and Objective-C directly extended C and inherited this syntax. Java, C#, JavaScript, Go, Rust, and many other languages also adopted parts of it.
C syntax
├── C++
├── Objective-C
├── Java
├── C#
├── JavaScript
├── Go
├── Rust
├── D
├── Zig
└── Many scripting and system languages
This created common code forms across otherwise different languages. A developer who has learned one C-family language can usually read the basic control flow of another relatively easily.
However, syntactic influence does not imply identical execution semantics. C array access does not guarantee bounds checking, while Java and C# arrays perform runtime bounds checks. C pointers and Rust references also appear to serve similar purposes, but they follow different lifetime and safety rules.
Operator-Centered Expressions
C spread a style of expressing complex calculations and state changes by combining a small set of operators.
flags |= FLAG_VISIBLE;
value <<= 1;
pointer += offset;
result = condition ? first : second;
C’s operator system places arithmetic, pointers, bits, conditions, and assignment within one expression syntax.
This approach can produce concise code, but it also created expressions with many side effects and difficult precedence rules.
values[index++] =
source[offset++] +
calculate();
These problems helped motivate later languages to make choices such as the following.
- Excluding assignment from expressions
- Removing increment and decrement operators
- Reducing implicit conversions
- Making evaluation order explicit
- Restricting operator overloading
- Discouraging expressions with side effects
C’s expression syntax became both a standard for power and concision and a starting point for debates about readability and safety.
Pointer and Array Model
C’s relationship between arrays and pointers strongly influenced system programming and data-processing methods.
int values[4] = {
10,
20,
30,
40
};
int *pointer = values;
In most expressions, an array name is converted into a pointer to its first element.
values[index]
*(values + index)
This model offered the following advantages.
- Processing contiguous memory
- Passing subranges of arrays
- Implementing dynamic arrays
- String processing
- Accessing buffers and file data
- Accessing hardware memory
- Simple function interfaces
However, it also created the problem that an array’s length is not included in the pointer type.
void process(
int *values,
size_t count
);
The caller must pass the pointer and length together correctly.
This problem helped motivate later languages to introduce slices and array references carrying bounds.
C pointer
→ Passes only an address
→ Length is a separate value
Modern slice
→ Represents address and length together
→ Can support bounds checking
Rust slices, Go slices, and span and view structures in various libraries can be seen as ways of preserving the advantages of C array interfaces while addressing their limitations.
Structure-Based Data Model
C’s struct widely spread the method of combining values of different types into one contiguous data object.
struct image {
unsigned char *pixels;
size_t width;
size_t height;
size_t stride;
};
Structures became a fundamental form of data representation in fields such as the following.
- Operating system kernel objects
- File formats
- Network packets
- Device registers
- Database records
- Graphics resources
- Game objects
- Library contexts
- ABI interfaces
C has no classes or built-in encapsulation, but object-like APIs can be designed by combining structures and functions.
struct image;
struct image *image_create(
size_t width,
size_t height
);
void image_destroy(
struct image *image
);
The pattern of opaque structures with creation and destruction functions became a common design for native libraries shared with other languages.
Function-Centered Module Design
C organizes programs around functions and data structures rather than classes.
Header
→ Public types
→ Function declarations
→ Constants and macros
Source
→ Internal structures
→ Function definitions
→ Static objects and functions
A representative C module can be organized as follows.
/* buffer.h */
struct buffer;
struct buffer *buffer_create(
size_t capacity
);
bool buffer_append(
struct buffer *buffer,
const void *data,
size_t size
);
void buffer_destroy(
struct buffer *buffer
);
This pattern left the following influences.
- Explicit public APIs
- Hiding implementation details
- Header-based contracts
- Library-level distribution
- Stable ABIs
- Extensions through function pointers
- Callback-based event handling
- User-data pointers
Modern language modules, interfaces, and package systems provide higher-level support than C headers, but C-style function interfaces are still widely used at native library boundaries.
Separate Compilation
C widely established the model of separately translating multiple source files and then combining them with a linker.
main.c → main.o
renderer.c → renderer.o
network.c → network.o
↓
linker
↓
application
Separate compilation provided the following advantages.
- Recompile only modified files
- Allow multiple developers to work on separate modules
- Distribute static and shared libraries
- Separate compiler and linker roles
- Combine object files written in different languages
- Replace platform-specific implementations
This model continued into the build processes of C++, Objective-C, Rust, Swift, and many other native languages.
At the same time, it also created problems involving mismatches between headers and implementations, repeated preprocessing, long build times, and complex linker errors. Later module systems and package managers attempted to reduce these problems.
Compiler Structure
C influenced how compilers are ported to new platforms.
A traditional C compiler can have a structure such as the following.
C front end
↓
Intermediate representation
↓
Common optimization
↓
Target code generator
When adding support for a new processor, it became possible to add a target code generator and ABI support rather than reimplementing the entire language.
The Portable C Compiler was an early representative of this approach. Experience porting C compilers and UNIX contributed to the spread of compiler designs that separate language front ends from target back ends.
Modern GCC and LLVM use architectures that share multiple language front ends and processor back ends.
C
C++
Objective-C
Rust-family front ends
Other languages
↓
Common intermediate representation and optimization
↓
x86-64
AArch64
RISC-V
WebAssembly
Other targets
Not every modern compiler architecture can be said to derive directly from C, but C’s requirements for broad architecture support and portability provided an important practical motivation for developing reusable code generators and intermediate representations.
Self-Hosting
When early C compilers were written in C itself, they became representative examples of self-hosting compilers.
Early compiler
↓
Can translate C programs
↓
Compiler itself written in C
↓
Build the new compiler with the existing compiler
The process of porting C to a new computer could proceed as follows.
Existing system
→ Generate a C compiler for the target
Target system
→ Run the compiler
On the target system
→ Build C programs and the operating system
Self-hosting became a symbol that a language was expressive enough to implement large-scale software and its own development tools.
Later languages such as C++, Rust, Go, and Swift also evolved toward implementing their compilers or major tools in the language itself.
Using C as a Portability Layer
C is also used as a portability layer that hides differences among operating system APIs.
#if defined(_WIN32)
static bool platform_file_open(
struct file *file,
const char *path
) {
/* Win32 implementation */
}
#elif defined(__unix__)
static bool platform_file_open(
struct file *file,
const char *path
) {
/* POSIX implementation */
}
#endif
Higher-level code uses only the common interface.
struct file file;
if (!platform_file_open(
&file,
"document.dat"
)) {
return false;
}
This approach is widely used in cross-platform engines and libraries, language runtimes, and databases.
The C standard aims to increase the portability of C programs among diverse data-processing systems.[122]
Standard C alone cannot access every platform facility, but common logic can be written in ISO C while only a limited platform layer uses operating system-specific APIs.
International Standardization
C also had a major influence on programming language standardization.
As C spread across many compilers and industrial environments, different dialects emerged. ANSI and ISO standardization were pursued to unify them.
K&R C
↓
Multiple compilers and dialects
↓
ANSI X3J11
↓
ANSI C89
↓
ISO C90
↓
C99, C11, C17, and C23
The C standard carefully distinguishes the following concepts.
- Syntax
- Constraints
- Semantic rules
- Defined behavior
- Implementation-defined behavior
- Unspecified behavior
- Undefined behavior
- Conforming implementation
- Strictly conforming program
- Minimum implementation limits
These terms and normative structures became important reference models in other language standards and compiler documentation.
C standardization provided a foundation that prevented the language from being tied to one company or implementation and allowed multiple vendors to create compatible compilers and libraries.
Standard Library Model
C widely spread the structure of combining a small language core with a separate standard library.
C language
→ Types
→ Declarations
→ Expressions
→ Statements
→ Functions
→ Preprocessor
C standard library
→ Input and output
→ Strings
→ Memory
→ Mathematics
→ Time
→ Dynamic allocation
Input and output, strings, and memory allocation are not built directly into the language syntax but are provided through library functions.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
char *memory = malloc(128);
if (memory == NULL) {
return 1;
}
puts("hello");
free(memory);
return 0;
}
This structure has the following advantages.
- Keeps the language core small
- Allows libraries to be replaced according to the implementation environment
- Allows some facilities to be omitted in freestanding environments
- Allows functionality to be extended through external libraries
- Allows C libraries to be called from other languages
Modern languages provide larger standard libraries and package ecosystems, but the distinction between language core and library continues to be used.
Common Foundation for Native ABIs
One of C’s most persistent influences is its role as a common ABI for native software.
Operating systems and libraries often expose interfaces in the form of C functions.
struct database;
struct database *database_open(
const char *path
);
void database_close(
struct database *database
);
Such APIs can be called from many languages.
C++
Rust
Python
C#
Java
Go
Swift
Lua
Wave
↓
C ABI
↓
Native library
The C standard itself does not define one universal ABI. Each operating system and processor ABI determines argument passing, structure layout, symbols, and calling conventions.
However, describing ABIs using C functions, fundamental types, structures, and pointers became widespread.
For this reason, new programming languages generally provide C interoperability features.
- Importing C headers
extern "C"- C calling conventions
repr(C)- P/Invoke
- JNI
- cgo
- Foreign function interfaces
C became more than a source language: it became a common boundary connecting different languages and runtimes.
Library Ecosystem
C created an ecosystem in which foundational libraries across many fields are shared.
Representative C libraries exist in areas such as the following.
- Databases
- Cryptography
- Compression
- Images
- Audio
- Video
- Networking
- File formats
- Graphics
- Numerical computation
- Text and fonts
- Operating system interfaces
SQLite is an embedded database implemented as a C library, and its official site explains that SQLite is included in mobile phones, computers, and countless applications.[123]
The SQLite developers cite the following characteristics as reasons for using C.
- Performance
- Compatibility
- Low-level control
- Stable language and tools
- Small size
- An interface usable from multiple languages
SQLite officially explains that it has been implemented in ordinary C since 2000 and has no plans to be rewritten in another language.[124]
These libraries are also used by higher-level language programs.
Python application
→ SQLite C library
Mobile application
→ Image and compression C libraries
Web server
→ TLS, compression, and regular-expression C libraries
Game engine
→ Audio, codec, and graphics C APIs
Users can rely on foundational software implemented in C even without writing C directly.
Language-Neutral Operating System APIs
Function-centered C APIs made operating system facilities callable from multiple programming languages.
int file_open(
const char *path,
unsigned int flags
);
C APIs that do not require complex language objects or runtime metadata are comparatively easy to bind.
Operating system implementation
↓
C ABI function
↓
Language-specific wrapper
↓
High-level API
File and networking facilities in Java, Python, .NET, Go, and Rust also connect internally to native operating system interfaces.
This does not mean that every operating system API is implemented in C or belongs to ISO C. It means that public binary interfaces and headers are often expressed using C’s type system.
Embedded Industry
C became a common language of the microcontroller and embedded systems industry.
Semiconductor manufacturers generally provide the following resources in C-based form.
- Device headers
- Register definitions
- Peripheral drivers
- Startup code
- Real-time operating system ports
- Example firmware
- Debugger support
- Protocol stacks
#define TIMER_CONTROL \
(*(volatile uint32_t *) \
UINTPTR_C(0x40001000))
void timer_enable(void) {
TIMER_CONTROL |= 1U;
}
This structure can be commonly supported by manufacturer SDKs, compilers, debuggers, and static-analysis tools.
C’s broad embedded support established the practice of providing a C compiler, headers, and runtime early when a new processor is introduced.
Data-Layout-Oriented Design
C strengthened a culture of analyzing program performance through memory layout and access patterns as well as algorithms.
struct particle {
float x;
float y;
float z;
float velocity_x;
float velocity_y;
float velocity_z;
};
Member order, alignment, and array layout can affect cache efficiency and execution speed.
Algorithm
+
Data structure
+
Memory layout
+
Cache access
+
Branching and vectorization
=
Actual performance
This way of thinking is important in game engines, scientific computing, databases, and network processing.
Choosing between an array of structures and a structure of arrays is a representative example of data-oriented design in C.
An array of structures stores all values of each object together.
struct particle particles[1024];
A structure of arrays stores values of the same kind contiguously.
struct particles {
float x[1024];
float y[1024];
float z[1024];
};
Which layout is more advantageous depends on access patterns and operations.
Because C directly exposes memory representation, such optimizations are easy to attempt, but alignment, aliasing, and object-lifetime rules must also be followed accurately.
Zero-Cost Abstraction Discussions
C influenced a system programming culture in which using higher-level structures should not impose unnecessary runtime costs.
C functions and structures can often be translated into relatively simple machine code.
static inline int maximum(
int left,
int right
) {
return left > right
? left
: right;
}
The compiler can inline the call and leave only the conditional expression.
This expectation continued into C++’s zero-overhead principle, Rust’s zero-cost abstractions, and compile-time abstraction designs in other system languages.
High-level representation
→ No cost for unused features
→ Abstraction can be removed
→ Result similar to manually written low-level code
C itself offers limited high-level abstractions such as generics and classes, but its execution-cost model became a benchmark against which later system languages evaluated abstraction costs.
Software Performance Culture
C helped create a culture connecting performance analysis at the source level with analysis at the machine level.
C developers can examine the following together.
- C source
- Compiler intermediate representation
- Generated assembly
- Function calling conventions
- Cache misses
- Branch prediction
- Memory allocation
- System calls
- Profiling results
C source
↓
Optimization
↓
Machine instructions
↓
Processor execution
↓
Profiling
↓
Source and data-structure improvements
Using Compiler Explorer, profilers, debuggers, and performance counters to inspect how source changes affect generated code became widespread in C and C++ development.
This culture also carried into native performance analysis for Rust, Zig, and Swift.
Memory-Safety Discussions
C’s influence is not entirely positive. Its manual memory management and pointer model became sources of many categories of errors and security vulnerabilities.
Representative problems include the following.
- Buffer overflow
- Use after free
- Double free
- Null pointer dereference
- Uninitialized values
- Integer overflow
- Out-of-bounds array access
- Invalid type reinterpretation
- Data races
- Accessing objects after their lifetime ends
void copy_text(
char *destination,
const char *source
) {
while (
(*destination++ =
*source++) != '\0'
) {
}
}
This function does not know the size of the destination buffer. If the source string is larger than the buffer, an out-of-bounds write occurs.
These problems led the software industry to develop the following responses.
- Static analyzers
- Runtime checkers
- Compiler warnings
- Stack protection
- Address space layout randomization
- Non-executable memory
- Fuzzing
- Safe coding rules
- Restricted C subsets
- Memory-safe languages
- Sandboxing
- Hardware memory tagging
C’s safety problems did not remain merely historical defects of C. They became major motivations for research and design concerning how new programming languages should handle memory and errors.
Undefined Behavior
C’s undefined behavior had a major influence on compiler optimization and program safety.
The following code can exceed the range of a signed integer.
int increment(int value) {
return value + 1;
}
If value is INT_MAX, signed integer overflow occurs and the behavior is undefined.
A compiler may assume that such a situation does not occur in a correct C program.
bool greater_after_increment(
int value
) {
return value + 1 > value;
}
The compiler may optimize this to always return true for all defined executions.
Undefined behavior serves purposes such as the following.
- Avoiding mandatory identical checking costs on every hardware platform
- Allowing unusual processor representations
- Expanding compiler optimization possibilities
- Leaving the results of invalid programs unspecified by the standard
However, it creates the difficulty that a program error is not confined to producing an unpredictable value and can instead affect the result of optimization across the program.
This problem influenced later languages to introduce the following features.
- Checked integer operations
- Explicit wrapping operations
- Array bounds checking
- Nullability types
- Separation between safe and unsafe regions
- Lifetime checking
- Ownership
- Initialization tracking
- Data-race prevention
Safe System Languages
Languages emerged that attempted to retain C’s performance and control while reducing memory-safety problems.
Advantages of C
├── Native performance
├── Small runtime
├── Direct memory control
├── Operating system access
└── Stable ABI
Areas to improve
├── Pointer safety
├── Object lifetime
├── Null references
├── Array bounds
├── Data races
├── Error handling
└── Undefined behavior
Rust attempts to prevent many lifetime and data-race problems in safe code through ownership and borrow checking. Zig provides distinct pointer kinds, optional types, checked arithmetic, and explicit allocators. Wave is being developed in a direction that strengthens explicit pointers, mutability, and compiler checking to reduce problems involving pointers and undefined behavior in C.
These languages were not designed independently of C’s existence. Most make preserving C’s strengths while addressing recurring C problems a major objective.
C Coding Standards
C’s expressive freedom and implementation-defined areas led organizations to develop coding rules and restricted subsets.
Representative rules may restrict the following.
- Dynamic memory allocation
- Recursion
- Function pointers
goto- Implicit conversions
- Complex macros
- Pointer arithmetic
union-based type reinterpretation- Global state
- Expressions with many side effects
if (
count >
SIZE_MAX / sizeof(*values)
) {
return false;
}
Safety rules do more than prohibit specific syntax. They can require explicit validation of integer calculations, array lengths, and object lifetimes.
Automotive, aerospace, medical, and industrial control fields apply MISRA C, CERT C, and organization-specific safety rules in addition to the C language itself.
C demonstrated that language standards alone are insufficient to complete a safe development process and influenced methodologies that combine language, tools, rules, and verification.
Compiler Warnings and Static Analysis
C compilers evolved from syntax and type checkers into analysis tools capable of detecting potential errors.
Representative diagnostics include the following.
- Uninitialized variables
- Invalid format strings
- Signedness conversions
- Implicit narrowing conversions
- Unused values
- Possible out-of-bounds access
- Null pointers
- Lifetime errors
- Invalid function prototypes
- Unreachable code
-Wall
-Wextra
-Wconversion
-Wshadow
-Wformat=2
Clang Static Analyzer, GCC analysis features, and commercial static-analysis tools track control flow and object lifetimes.
C’s low-level access and undefined behavior made analysis difficult, but also became a major motivation for static-analysis research and industry.
Dynamic Analysis
Tools also developed to detect memory errors in C programs during execution.
Representative tools and facilities include the following.
AddressSanitizer
→ Out-of-bounds access
→ Use after free
UndefinedBehaviorSanitizer
→ Various forms of undefined behavior
MemorySanitizer
→ Use of uninitialized memory
ThreadSanitizer
→ Data races
Valgrind-family tools
→ Memory errors and leaks
Fuzzers
→ Search for abnormal inputs
-fsanitize=address,undefined
These tools connect C’s memory model with actual execution to find errors. They do not prove the absence of all defects, but they became an important validation layer in C and C++ development.
Security Vulnerability Classification
C memory errors also influenced modern security vulnerability classification and defense systems.
The following vulnerability categories have been repeatedly studied.
- Stack buffer overflow
- Heap buffer overflow
- Use after free
- Double free
- Integer underflow and overflow
- Format-string vulnerabilities
- Null pointer access
- Race conditions
- Invalid size calculations
- Type confusion
Operating systems and compilers provide the following defenses to mitigate these problems.
Stack canaries
Address space layout randomization
Non-executable data
Position-independent executables
Control-flow protection
Read-only relocations
Memory tagging
Hardened allocators
C’s influence therefore has both the negative aspect of contributing to security problems and the positive aspect of motivating advances in memory-safety and system-security research.
Open-Source Software
C was one of the foundational languages of the early open-source software ecosystem.
Representative categories of C-centered projects include the following.
- Operating system kernels
- System libraries
- Shells and command-line tools
- Compilers and linkers
- Version-control systems
- Databases
- Web servers
- Network libraries
- Multimedia tools
- Embedded operating systems
Through a relatively small standard language and a variety of freely available compilers, C allowed the same source code to be shared across multiple operating systems.
Public C source
↓
Compile on another operating system
↓
Patch and port
↓
Publish again
This model suited Internet-based collaboration, source-code distribution, porting, and patch culture.
The Linux kernel, GNU tools, BSD-family systems, and many database and networking projects demonstrated that large-scale distributed development could be organized around C.
API Stability and Long-Term Maintenance
C APIs can be maintained over long periods because they use relatively simple functions, structures, integers, and pointers.
struct library_context;
int library_create(
struct library_context **result
);
void library_destroy(
struct library_context *context
);
Even if the internal implementation changes, preserving opaque pointers and function prototypes can maintain source and binary compatibility for external users.
This characteristic is important in the following fields.
- Operating system system libraries
- Plugin APIs
- Graphics drivers
- Database extensions
- Language runtimes
- Device SDKs
- Commercial middleware
C APIs cannot provide rich high-level type information, but their simplicity and stability make them suitable as long-term compatibility boundaries.
Plugin Architectures
C function pointers and opaque handles also influenced the basic structure of plugin systems.
struct plugin_api {
uint32_t version;
bool (*initialize)(void);
void (*update)(double delta_time);
void (*shutdown)(void);
};
A host program can locate API functions in a shared library and receive a function table.
Host program
↓ Dynamic loading
Plugin library
↓
C function table
This structure is used in game engines, graphics tools, audio plugins, database extensions, and language runtimes.
C’s simple ABI allows plugins to be implemented in different languages, but versioning, structure sizes, lifetimes, and threading rules must be designed separately.
Function Pointers and Callback Culture
C function pointers became a representative method of expressing events, strategies, asynchronous work, and extension facilities.
typedef void (*event_callback)(
int event,
void *context
);
void event_register(
event_callback callback,
void *context
);
The combination of a callback and a user-data pointer is repeatedly used in fields such as the following.
- GUI events
- Network completion handling
- Sorting comparison functions
- Thread entry functions
- Plugin APIs
- File-system traversal
- Parser events
- Game engines
- Operating system asynchronous APIs
Modern language lambdas, closures, delegates, and function objects provide more state and type safety than C function pointers. However, function pointers and void * contexts remain common representations at C ABI boundaries.
Error-Code Culture
Because C has no general exception-handling syntax, error propagation through function return values, error codes, and output parameters became widespread.
enum result {
RESULT_SUCCESS,
RESULT_INVALID_ARGUMENT,
RESULT_OUT_OF_MEMORY,
RESULT_IO_ERROR
};
enum result document_open(
const char *path,
struct document **result
);
This model strongly influenced operating system and native library APIs.
Function call
↓
Success or error code
↓
Caller handles it explicitly
Its advantages include the following.
- Easy to represent through an ABI
- No exception runtime required
- Error paths are explicit
- Easy to cross language boundaries
It also has disadvantages.
- Return-value checks can be omitted
- Returning both an error code and an actual result is cumbersome
- Cleanup code becomes longer
- Error propagation becomes repetitive
- Cause information is easily lost
Rust’s Result, Go’s multiple returns and error, and exception and result types in many languages provide different solutions based on experience with C-style error handling.
Resource-Management Patterns
C’s manual resource management spread explicit creation and destruction patterns.
struct resource *resource_create(void);
void resource_destroy(
struct resource *resource
);
When multiple resources exist, they are cleaned up in reverse order.
int process(void) {
int result = -1;
struct first *first = NULL;
struct second *second = NULL;
first = first_create();
if (first == NULL) {
goto cleanup;
}
second = second_create();
if (second == NULL) {
goto cleanup;
}
result = 0;
cleanup:
second_destroy(second);
first_destroy(first);
return result;
}
This pattern helped demonstrate why automatic resource-management features such as C++ RAII, Rust ownership and Drop, defer in other languages, using, and context managers are useful.
C
→ Explicit creation and release
C++
→ Constructors and destructors
Rust
→ Ownership and Drop
C#
→ using
Python
→ with
Go
→ defer
Later languages attempted to preserve C’s explicit notion of resource lifetime while reducing cleanup omissions through language and type-system support.
Influence of the Preprocessor
The C preprocessor made file inclusion, macros, and conditional compilation into one common tool.
#if defined(_WIN32)
#include "platform_windows.h"
#elif defined(__linux__)
#include "platform_linux.h"
#endif
The preprocessor had a major influence in the following areas.
- Platform-specific code selection
- Header inclusion
- Debug features
- Feature flags
- Repetitive code generation
- Compile-time configuration
- API export control
At the same time, because the preprocessor is a token-substitution tool that does not understand types or scopes, it created many problems.
#define MAXIMUM(left, right) \
((left) > (right) \
? (left) \
: (right))
Arguments can be evaluated multiple times.
int result = MAXIMUM(
index++,
limit
);
These limitations influenced the following facilities in later languages.
- Type-safe generics
- Templates
- Hygienic macros
- Compile-time functions
- Modules
- Attributes
- Conditional package features
The C preprocessor was a highly successful tool, but also clearly demonstrated the risks of textual substitution outside the language proper.
Build Systems
C’s separate compilation and header dependencies were major motivations for the development of modern build systems.
Source files
Header files
Compiler options
Libraries
Generated code
Target platform
↓
Dependency calculation
↓
Incremental build
Make widely spread the method of executing only required commands according to file modification times and dependency relationships.
Later systems such as CMake, Ninja, Meson, and Bazel attempted to address the following problems.
- Compiler options across multiple platforms
- Header dependencies
- Static and shared libraries
- Generated code
- Installation and packaging
- Cross-compilation
- Testing
- Incremental builds
The complex build requirements of C projects strongly influenced the development of independent build tools and package-management systems.
Debuggers and Binary Tools
C often required direct analysis of the relationship between source code and machine code, so it also influenced the development of debuggers and binary-analysis tools.
Representative facilities include the following.
- Breakpoints
- Step-by-step execution
- Call stacks
- Register inspection
- Memory dumps
- Structure and pointer inspection
- Symbol tables
- Core dumps
- Mixed source and assembly views
C source
↓
Debug information
↓
Machine instructions and addresses
↓
Debugger
Because C programs require direct analysis of object lifetimes, pointers, and memory corruption, memory checking and core-dump analysis became part of development culture.
GDB, LLDB, objdump, readelf, nm, and platform-specific debuggers became central tools in C and C++ development.
Education
C has played an important role in computer science and software engineering education.
It is frequently used to explain the following concepts.
- Variables and data types
- Functions
- Arrays and strings
- Pointers
- Structures
- Dynamic memory
- Data structures
- File input and output
- Compilation and linking
- Processes and threads
- Operating systems
- Networking
- Computer architecture
C allows students to observe relatively directly how source code is translated into memory and machine instructions.
C variable
→ Object and storage
C pointer
→ Address and indirection
C function
→ Calling convention and stack frame
C array
→ Contiguous memory
C compilation
→ Object files and linker
Because C requires abstract data structures to be implemented directly, it is used to teach the internal structure of linked lists, trees, hash tables, and dynamic arrays.
However, it is also criticized because beginners may spend so much effort on pointers and undefined behavior that learning algorithms and problem solving is delayed. Accordingly, some curricula begin with Python or Java and teach C later in operating systems and systems courses.
Programming Mindset
C encouraged programmers to decompose programs into the following elements.
Data
+
Functions that process the data
+
Memory lifetime
+
Error codes
+
External system interfaces
Because the runtime automatically handles relatively little in C, programmers must directly consider questions such as the following.
- Where is an object stored?
- Who owns the memory?
- When is it released?
- What is the array length?
- What is the cost of a function call?
- Is data copied?
- Is the pointer valid?
- How is an error propagated?
- Are multiple threads accessing it concurrently?
- Does the code work on another platform?
This way of thinking is useful even in non-C languages when designing performance-sensitive code and system interfaces.
Minimalist Language Design
C demonstrated that a relatively small language core could implement a wide variety of programs.
C fundamentally lacks high-level facilities such as the following.
- Classes
- Garbage collection
- Exceptions
- Generic collections
- Package manager
- Reflection
- Asynchronous functions
- String objects
- Automatic array bounds checking
Even so, operating systems, compilers, databases, and network servers can be implemented by combining functions, structures, pointers, and libraries.
This success influenced minimalist language-design philosophies such as the following.
Small language core
+
Strong composability
+
Libraries
+
Tools
=
Broad range of applications
Go, Lua, Zig, and other languages are not the same as C, but they are often compared with it in their attempts to avoid making syntax and core facilities unnecessarily large.
Long-Term Source Compatibility
C created an ecosystem in which old source code can often be maintained relatively easily with newer compilers.
Because many compilers support old standards and implementation extensions, programs written decades ago can sometimes be built with only limited modification.
1970s C
↓ Modification
1980s ANSI C
↓
Modern compiler
However, complete compatibility is not automatic.
The following elements changed over time.
- Implicit
int - Implicit function declarations
- Old-style function definitions
- Removed library functions
- Integer and pointer sizes
- Operating system APIs
- Character encodings
- ABIs
- Compiler optimization
C standardization placed great importance on preserving existing code, but this conservatism also allowed old syntax and unsafe practices to remain for long periods.
Software Preservation
C’s long-term source availability and simple build model also influenced software preservation.
Old C programs can potentially be restored using only the following materials.
- Source code
- Headers
- Build commands
- Description of the target platform
- External libraries
They can be easier to preserve independently than programs strongly dependent on dynamic runtimes, central package repositories, and network services.
However, actual restoration requires understanding old compiler behavior, integer sizes, endianness, file formats, and operating system APIs.
C continues to be used to port historical operating systems, games, compilers, and research software to modern environments.
Influence on Hardware Design
C became an indirect benchmark for determining which facilities hardware should provide to software developers.
Processors and ABIs are often designed to support the following C operations efficiently.
- Integer arithmetic
- Pointer address calculation
- Array indexing
- Function calls
- Structure access
- Bit operations
- Conditional branches
- Stack frames
- Atomic operations
value = array[index];
This expression becomes an address calculation and memory load.
Base address
+
Index × element size
→ Load
Modern processors are not designed only for C, but efficiently executing large amounts of software written in C and C++ remains an important requirement for general-purpose processors and compiler optimization.
Coevolution of CPUs and Compilers
C compilers evolved to use new processor features.
Representative facilities include the following.
- SIMD
- Atomic instructions
- Conditional moves
- Bit-manipulation instructions
- Hardware floating point
- Memory barriers
- Branch prediction information
- Vector extensions
- Hardware acceleration
for (
size_t index = 0;
index < count;
++index
) {
output[index] =
left[index] +
right[index];
}
When conditions permit, the compiler can transform this loop into vector instructions.
C code can use hardware capabilities without direct inline assembly, while the compiler selects instructions appropriate for the target processor.
This relationship strengthened a culture of joint optimization between hardware designers and compiler developers.
Tension Between Portability and Optimization
C’s influence is also visible in the persistent tension between portability and low-level control.
The following code uses a fixed-width integer type.
uint32_t value;
However, the byte order in memory can differ among platforms.
Same C type
→ Different byte order
→ Different ABI
→ Different alignment
Implementation-dependent elements include the following.
- Sizes of integer types
- Signedness of
char - Structure padding
- Byte order
- Function calling conventions
- Atomic-operation support
- Text representation of files
C does not eliminate these differences completely. Instead, it manages them through ranges permitted by the standard and implementation documentation.
This is both an advantage that allows efficient implementation across diverse hardware and a factor that makes complete portability difficult.
Performance Portability
The fact that C source compiles on multiple platforms does not guarantee identical performance on all of them.
for (
size_t index = 0;
index < count;
++index
) {
result += values[index];
}
Performance depends on factors such as the following.
- CPU caches
- SIMD width
- Memory bandwidth
- Compiler optimization
- Alignment
- Branch prediction
- Data size
- Number of threads
- ABI
C can provide source portability, but performance portability requires separate profiling and optimization.
This problem influenced the development of abstract performance models, automatic vectorization, performance-portability libraries, and multi-backend frameworks.
Benchmark for Language Boundaries
C’s simple function and data model became a common denominator for language interoperability.
It is difficult to expose the complex facilities of another language directly across a C boundary.
C++ templates
Rust generics
Java objects
C# delegates
Swift protocols
↓ Difficult to pass directly
Convert to C functions and structures
Therefore, cross-language APIs are simplified into forms such as the following.
struct library;
enum library_result
library_open(
const char *path,
struct library **result
);
void library_close(
struct library *library
);
This structure sacrifices advanced language facilities in exchange for broad compatibility.
C established the role of a boundary language where different runtimes meet in the programming language ecosystem.
Cultural Influence
C also left its mark on programmer culture and expression.
Representative examples include the following.
- The
Hello, world!example - Zero-based array indexing
- Debates over brace styles
- K&R coding style
- Pointers and memory as a rite of passage
- Treating compiler warnings as errors
- Comparing source with assembly
- Small command-line tools
- Separation of header and source files
- Makefile-based builds
- Association with the UNIX philosophy
#include <stdio.h>
int main(void) {
puts("hello, world");
return 0;
}
hello, world became widely established as the first example in programming languages through Brian Kernighan’s early C tutorial and *The C Programming Language*.
Countless language documents and educational materials later adopted the same sentence as their first program.
Technical Documentation
The manual culture of C and UNIX influenced concise technical documentation and function-oriented reference formats.
Standard library function documentation generally explains the following items.
Function prototype
Parameters
Return value
Errors
Preconditions
Side effects
Thread safety
Standard edition
void *memcpy(
void *restrict destination,
const void *restrict source,
size_t count
);
The function prototype itself serves as the central summary of the API contract.
This format continued into POSIX manuals, operating system APIs, native library documentation, and API references in other languages.
Criticism of C’s Influence
C’s broad influence has also been criticized.
Major criticisms include the following.
- Spread of an unsafe pointer model
- Long-term use of null-terminated strings
- Array lengths not included in types
- Dependence on preprocessor macros
- Broad scope of undefined behavior
- Complexity of headers and separate compilation
- Possibility of omitted error checks
- Manual memory management
- Platform-specific ABI differences
- Unsafe default library functions
strcpy(destination, source);
This function does not know the size of the destination buffer.
An API that explicitly receives a size can be safer.
bool text_copy(
char *destination,
size_t capacity,
const char *source
);
C’s early design was reasonable for the constrained hardware and program sizes of its time, but risks increased in an era of the Internet, large networked software, aggressive optimization, and multithreading.
Continuing Influence
Although C was created decades ago, its influence remains across the entire modern software stack.
Applications
↓
Language runtimes
↓
Databases, codecs, and cryptography
↓
C ABI and system libraries
↓
Operating system kernel
↓
Device drivers and firmware
↓
Hardware
Even when high-level applications are written in Python, Java, C#, or JavaScript, they may internally use runtimes, libraries, and operating system facilities implemented in C.
Developers who do not write C directly are still affected by it through the following elements.
- Zero-based arrays
- Brace-based syntax
- Operating system C APIs
- Native libraries
- C ABI
- Null pointer concepts
- Integer and bit operations
- File and stream interfaces
- Compiler and linker structures
- Memory-safety policies
Historical Evaluation
C’s success was not achieved solely through the features of one language. C developed alongside UNIX and spread through the Portable C Compiler, K&R, ANSI and ISO standardization, numerous compilers, operating systems, and hardware vendors.
C language
+
UNIX
+
Compilers
+
Standard library
+
K&R
+
International standards
+
Hardware portability
=
Broad influence
Dennis Ritchie evaluated C as a language that, despite its flaws and peculiarities, was efficient enough to replace assembly language and abstract enough to express algorithms and system interactions across multiple environments.[125]
C’s most important achievement was creating a practical middle ground between low-level programs for specific hardware and portable high-level languages.
Summary of Its Influence
C’s influence can be broadly summarized as follows.
Operating systems
→ UNIX portability and kernel structure
Programming languages
→ C-family syntax and benchmark for system languages
Compilers
→ Self-hosting and multi-target code generation
Software structure
→ Functions, structures, libraries, and separate compilation
Industry
→ Embedded systems, databases, networking, and multimedia
Language interoperability
→ C ABI and FFI
Education
→ Learning memory, compilation, and operating systems
Performance
→ Data layout and generated-code-oriented thinking
Safety
→ Development of static analysis, runtime checking, and memory-safe languages
C separated operating systems from specific machines and provided a foundation for porting them across hardware. Program structures centered on functions, structures, pointers, and libraries became a common form of system software.
C syntax continued into countless languages, while the C ABI became a common boundary for native software connecting different languages and operating systems. Code written in C continues to be used in databases, codecs, cryptography, networking, language runtimes, and embedded software.
At the same time, C’s pointers, arrays, manual memory management, and undefined behavior exposed major problems in modern software security. These limitations motivated safe coding rules, static and dynamic analysis tools, and newer system languages such as Rust, Zig, and Wave.
C’s influence therefore does not lie merely in the fact that it is an old and widely used language. C shaped how modern software meets hardware, how programs are compiled and linked, how multiple languages share libraries, and how safety and performance are discussed in system programming.
Advantages and Limitations
C provides a relatively small language core, direct memory access, an abstract machine that can be implemented across many kinds of hardware, and stable native interfaces. Because of these characteristics, it has long been used in fields that form the foundation of execution environments, including operating systems, embedded systems, databases, and language runtimes.
At the same time, C requires programmers to manage array bounds, object lifetimes, dynamic memory, and error handling directly. The freedom provided by the language and the optimization opportunities available to implementations enable high performance and portability, but they also create the risk that incorrect programs will lead to memory corruption or undefined behavior.
The advantages and limitations of C are often not separate properties, but consequences of the same design choices.
Direct memory access
├── Advantage: Precise data layout and hardware control
└── Limitation: Possibility of bounds, lifetime, and aliasing errors
Small language and runtime
├── Advantage: Can be implemented in constrained environments
└── Limitation: High-level facilities must be built manually
Implementation freedom
├── Advantage: Efficient implementation across diverse hardware
└── Limitation: Implementation-defined and platform-specific differences
Undefined behavior
├── Advantage: Optimization without mandating checking costs
└── Limitation: Error results are difficult to predict or contain
C ABI
├── Advantage: Connects multiple languages and operating systems
└── Limitation: Difficult to express ownership, lengths, and nullability
Portability
C was standardized so that programs could be ported across diverse data-processing systems. ISO/IEC 9899 specifies the representation, syntax, semantic rules, and implementation limits of C programs, with the goal of increasing the portability of C programs among different systems.[126]
The same C source can be translated by multiple compilers for different operating systems and processors.
Common C source
├── x86-64 Linux
├── AArch64 Linux
├── Windows
├── macOS
├── RISC-V
├── Microcontrollers
└── Freestanding environments
By separating platform-specific functionality, most of a program can remain common code.
bool file_open(
struct file *file,
const char *path
) {
#if defined(_WIN32)
return windows_file_open(
file,
path
);
#elif defined(__unix__)
return posix_file_open(
file,
path
);
#else
return false;
#endif
}
However, portability in C does not mean that every implementation guarantees completely identical results. The following elements can differ among implementations and platforms.
- Sizes of integer types
- Signedness of plain
char - Byte order
- Structure padding and alignment
- Pointer representation
- Function calling conventions
- Source and execution character sets
- Representation of files and text streams
- Optional standard library features
- Compiler extensions
long value;
It cannot be assumed that long has the same bit width on every platform.
Interfaces requiring fixed widths can use types from <stdint.h>.
#include <stdint.h>
uint32_t identifier;
int64_t timestamp;
C can provide source-level portability, but writing portable programs requires deliberate management of implementation-defined characteristics and external API dependencies.
Diverse Execution Environments
C can be implemented not only in hosted environments with complete operating systems, but also in freestanding environments with limited standard libraries and no operating system.
Hosted environments
→ Desktops
→ Servers
→ Ordinary operating system applications
Freestanding environments
→ Firmware
→ Bootloaders
→ Operating system kernels
→ Microcontrollers
In a hosted environment, a program can use main, standard input and output, files, dynamic memory, and the complete standard library.
#include <stdio.h>
int main(void) {
puts("hosted program");
return 0;
}
In a freestanding environment, it can use an implementation-defined entry function and a limited library.
void reset_handler(void) {
initialize_memory();
initialize_hardware();
application_run();
for (;;) {
}
}
This flexibility allows C to be used on systems ranging from small microcontrollers to large servers.
On the other hand, programs must directly handle differences among execution environments. In systems without an operating system or standard library, memory management, input and output, startup, and termination code may need to be provided manually.
Execution Performance
C is translated into native machine code and does not require mandatory garbage collection or an object runtime at the language level.
void add_arrays(
float *restrict output,
const float *restrict left,
const float *restrict right,
size_t count
) {
for (
size_t index = 0;
index < count;
++index
) {
output[index] =
left[index] +
right[index];
}
}
A compiler can optimize this loop through techniques such as the following.
- Function inlining
- Removal of unnecessary memory accesses
- Loop unrolling
- SIMD vectorization
- Register allocation
- Instruction scheduling
- Constant propagation
Because C directly expresses data structures and memory accesses, performance-critical data layouts can be adjusted easily.
struct particles {
float x[1024];
float y[1024];
float z[1024];
};
Placing values of the same kind contiguously can improve cache and SIMD efficiency depending on the computation.
However, a program is not automatically fast merely because it is written in C.
Performance is affected by the following factors.
- Algorithms
- Data structures
- Cache access patterns
- Memory allocation
- Input and output
- Branching
- Synchronization
- Compiler and optimization options
- Target processor
- Code quality
A poorly designed C program can be slower than a well-optimized implementation in a higher-level language.
Predictable Costs
Basic C operations generally have comparatively clear execution costs.
value = values[index];
In an ordinary implementation, this becomes an address calculation followed by a memory read.
Array base address
+
index × element size
→ Memory read
The following features do not automatically intervene in ordinary C execution.
- General object garbage collection
- Dynamic method lookup
- Mandatory runtime reflection
- Automatic array growth
- Implicit string object creation
- General exception unwinding
This makes execution time and memory use easier to analyze in real-time and embedded systems.
However, this does not mean that compiler optimization, processor caches, operating system scheduling, and page faults are completely predictable. Dynamic allocation, file input and output, and system calls can vary greatly in cost depending on the environment.
Direct Memory Control
C allows programmers to directly manage object layout, addresses, alignment, and lifetimes.
struct packet {
uint32_t type;
uint32_t size;
unsigned char data[];
};
A header and its data can be placed in one contiguous allocation.
struct packet *packet_create(
size_t data_size
) {
if (
data_size >
SIZE_MAX - sizeof(struct packet)
) {
return NULL;
}
struct packet *packet = malloc(
sizeof(*packet) + data_size
);
if (packet == NULL) {
return NULL;
}
packet->size =
(uint32_t)data_size;
return packet;
}
This ability is important in the following fields.
- Operating systems
- Networking
- File formats
- Databases
- Graphics
- Game engines
- Codecs
- Embedded systems
- Hardware devices
- Numerical computation
However, storage size, alignment, object lifetime, and release must all be managed correctly by the programmer.
free(packet);
packet->size = 0;
Accessing an object after it has been freed causes undefined behavior.
Hardware Access
C can express device registers and memory-mapped input and output using pointers and bit operations.
#define DEVICE_STATUS_ADDRESS \
UINTPTR_C(0x40000000)
static volatile uint32_t *const
device_status =
(volatile uint32_t *)
DEVICE_STATUS_ADDRESS;
bool device_ready(void) {
return
(*device_status &
STATUS_READY) != 0;
}
For such code to operate correctly, the platform must define the following.
- Valid device addresses
- Alignment
- Access width
- Memory ordering
- Register side effects
- Cache policy
- Required memory barriers
ISO C alone does not define the meaning of hardware devices. C provides the means to express the platform specification.
Small Runtime
Depending on the functionality and target environment, a C program can be built with a very small runtime.
Startup code
+
Required C functions
+
Program code
=
Small firmware image
A freestanding implementation can provide only the functions it needs rather than the complete standard library.
void memory_copy(
unsigned char *destination,
const unsigned char *source,
size_t count
) {
while (count != 0) {
*destination++ =
*source++;
--count;
}
}
Small executables and runtimes are advantageous in the following environments.
- Microcontrollers
- Bootloaders
- Firmware
- Operating system kernels
- Recovery environments
- Memory-constrained devices
However, development burden can increase because standard library and operating system functionality may need to be provided directly.
Stable ABI
ABIs based on C functions and structures are used as common boundaries connecting languages and libraries.
struct decoder;
struct decoder *decoder_create(void);
bool decoder_process(
struct decoder *decoder,
const void *input,
size_t input_size
);
void decoder_destroy(
struct decoder *decoder
);
Such interfaces can be called from C++, Rust, C#, Python, Go, Swift, and other languages.
Higher-level language
↓
Foreign function interface
↓
C ABI
↓
Native library
Advantages of C ABIs include the following.
- Simple function-call structure
- Support from many compilers and languages
- Ability to hide implementation details through opaque pointers
- Suitability for dynamic libraries and plugins
- Long-term API stability
They also have limitations.
- Array lengths are not part of pointer types.
- String encodings cannot be expressed automatically.
- Ownership is not represented in types.
- Nullability is difficult to express.
- Exceptions and high-level objects are difficult to pass directly.
- Structure layout and calling conventions depend on the platform.
C ABIs are simple and widely supported, but API contracts must be supplemented through documentation and careful function design.
Mature Ecosystem
C has accumulated tools and libraries across many platforms and industries over a long period.
Major components include the following.
- GCC
- Clang
- MSVC
- Multiple embedded compilers
- GDB and LLDB
- Static analyzers
- Sanitizers
- Valgrind-family tools
- C standard library implementations
- Operating system SDKs
- Hardware manufacturer SDKs
- Numerical, cryptographic, and networking libraries
The Linux kernel is still written primarily in C, and its official documentation explains that it generally uses the GNU C11 dialect with GCC while also supporting Clang.[127]
A mature ecosystem is advantageous for supporting both old platforms and new architectures.
On the other hand, because the ecosystem is old, build systems, package management, compiler extensions, and platform-specific practices are not unified.
Compatibility with Existing Code
Decades of operating systems, libraries, firmware, and industrial software have been written in C.
New programs can reuse existing C libraries.
#include <sqlite3.h>
sqlite3 *database;
if (
sqlite3_open(
"application.db",
&database
) != SQLITE_OK
) {
return false;
}
SQLite officially explains that it has been implemented in ordinary C since 2000 and cites portability, performance, stability, and broad language bindings as reasons for using C.[128]
Compatibility with existing C code also motivates new languages and platforms to provide C interoperability.
However, old code may retain problems such as the following.
- Obsolete function declarations
- Unsafe string handling
- Assumptions about 32-bit environments
- Old compiler extensions
- Dependence on undefined behavior
- Conflicts with modern threading models
- Unmaintained external libraries
A large body of existing code can be both an advantage and a source of technical debt.
Simple Language Core
The core syntax of C is comparatively small.
Major elements include the following.
- Fundamental types
- Pointers
- Arrays
- Structures and
union - Functions
- Expressions
- Control statements
- Preprocessor
struct point {
int x;
int y;
};
int point_sum(
const struct point *point
) {
return point->x + point->y;
}
A small language core is advantageous for implementing compilers and analysis tools and for porting the language to new platforms.
However, a small language does not necessarily mean an easy or safe language. Although C’s syntax is relatively small, the following rules are complex.
- Declarators
- Integer promotions and conversions
- Object lifetimes
- Effective types
- Aliasing
- Pointer arithmetic
- Evaluation order
- Atomic memory ordering
- Undefined behavior
- Preprocessor extensions
Understanding the precise meaning of a program is more difficult than learning its surface syntax.
Composable Basic Elements
C can combine small basic elements to construct complex data structures and systems.
struct node {
struct node *next;
void *value;
};
struct list {
struct node *first;
struct node *last;
size_t count;
};
Generic libraries can be implemented with structures, function pointers, and void * even without classes or generic containers.
typedef int (*comparison_function)(
const void *left,
const void *right
);
However, type safety and automatic resource management become weaker.
void *value;
The actual type of the object referenced by value depends on the API contract and program logic.
Explicit Resource Management
In C, resources such as memory, files, locks, and devices are created and released explicitly.
FILE *file = fopen(
path,
"rb"
);
if (file == NULL) {
return false;
}
/* Use the file */
fclose(file);
Explicit resource management has the advantage that execution flow and costs are easy to inspect.
However, resources must be released along every error path.
int load_document(
const char *path
) {
int result = -1;
FILE *file = NULL;
void *buffer = NULL;
file = fopen(path, "rb");
if (file == NULL) {
goto cleanup;
}
buffer = malloc(4096);
if (buffer == NULL) {
goto cleanup;
}
result = 0;
cleanup:
free(buffer);
if (file != NULL) {
fclose(file);
}
return result;
}
C has no general language feature that automatically releases arbitrary resources when a scope is exited.
This can lead to the following errors.
- Memory leaks
- File handle leaks
- Failure to unlock
- Double free
- Use after free
- Missing cleanup along some error paths
Manual Memory Management
One of C’s greatest limitations is that the language does not automatically track ownership and lifetime of dynamically allocated memory.
char *text = malloc(128);
if (text == NULL) {
return false;
}
The program must determine the following directly.
- Who owns the memory
- Who releases it
- How long the pointer remains valid
- Whether aliasing pointers exist
- Whether the original pointer remains valid after reallocation
- Whether multiple threads share it
char *alias = text;
free(text);
puts(alias);
alias points to released storage, so using it causes undefined behavior.
Memory-safe languages attempt to reduce these problems through garbage collection, ownership and lifetime checking, reference counting, and automatic resource management.
CISA has noted that software written in languages such as C and C++, which do not guarantee memory safety by default, can be exposed to memory-safety vulnerabilities and recommends using memory-safe languages where practical.[129]
Lack of Array Bounds Checking
Ordinary C array access does not perform runtime bounds checking.
int values[4];
values[4] = 10;
The valid indices are 0 through 3. Access outside this range causes undefined behavior.
When a pointer is passed to a function, array length information is not passed automatically.
void clear_values(
int *values,
size_t count
) {
for (
size_t index = 0;
index < count;
++index
) {
values[index] = 0;
}
}
If the caller passes a count larger than the actual array, the function accesses memory outside the array.
int values[4];
clear_values(values, 100);
Pointers and lengths must always be managed together, and sizes received from external input must be validated before access.
Null-Terminated Strings
A C string is an array of characters terminated by a null character.
char text[] = "hello";
It is stored in memory as follows.
'h' 'e' 'l' 'l' 'o' '\0'
This representation is simple and easy to combine with existing byte-oriented APIs.
However, the string length is not part of the type, and string functions can read past an array if the null terminator is absent.
char text[4] = {
't',
'e',
's',
't'
};
size_t length = strlen(text);
text is not a valid C string because it has no null terminator.
The following problems can also occur.
- Insufficient buffer capacity
- Missing null termination
- Cost of repeatedly calculating string lengths
- Difficulty handling embedded null characters
- Confusion between character counts and byte counts
- Confusion between UTF-8 code points and bytes
An API that passes strings together with their lengths can be clearer.
bool text_process(
const char *text,
size_t length
);
Unsafe Library Interfaces
Some traditional C standard library functions do not receive the size of the destination buffer.
strcpy(
destination,
source
);
The caller must guarantee that destination is large enough to contain the entire string and its null terminator.
bool text_copy(
char *destination,
size_t capacity,
const char *source
) {
size_t length =
strlen(source);
if (length >= capacity) {
return false;
}
memcpy(
destination,
source,
length + 1
);
return true;
}
Functions that accept sizes can also be unintuitive.
strncpy(
destination,
source,
capacity
);
If the source length reaches or exceeds the limit, strncpy does not guarantee null termination and also fills the remainder of the destination with zero bytes. It should not be treated simply as a safe strcpy.
Each C library function must be used according to its exact contract.
Undefined Behavior
C contains undefined behavior for which the standard imposes no requirements on the result.
Representative examples include the following.
- Null pointer dereference
- Out-of-bounds array access
- Use after free
- Accessing an object after its lifetime ends
- Signed integer overflow
- Invalid pointer arithmetic
- Unaligned access
- Violations of effective type and aliasing rules
- Data races
- Calling through an incompatible function pointer
- Modifying a nonmodifiable string literal
int increment(int value) {
return value + 1;
}
If value is INT_MAX, signed integer overflow occurs.
Undefined behavior can produce results such as the following.
Appears to work normally
Program termination
Incorrect values
Memory corruption
Security vulnerabilities
Removal of related code
Changes to overall control flow
Undefined behavior does not refer to one particular error result. The standard imposes no requirements on the implementation.
Work on C2Y in WG14 has continued to analyze undefined behavior remaining in the C23 core language and to discuss converting some cases into constraint violations or defined behavior.[130]
Undefined behavior enables support for diverse hardware and aggressive compiler optimization, but makes program errors difficult to analyze.
Limits of Diagnostics
C compilers are not required to reject every invalid program.
Diagnostics may be required for syntax errors and constraint violations, but undefined behavior generally does not have to be diagnosed.
int values[4];
void write_value(
size_t index
) {
values[index] = 10;
}
At compile time, the actual value of index may be unknown, so the compiler may be unable to determine whether the access is out of bounds.
The following code can produce a compiler warning.
int values[4];
values[10] = 20;
However, the absence of a warning does not mean that the program is safe.
The following tools can be used together.
- High-level compiler warnings
- Static analysis
- AddressSanitizer
- UndefinedBehaviorSanitizer
- MemorySanitizer
- ThreadSanitizer
- Valgrind-family tools
- Fuzzing
- Code review
- Unit and integration testing
Complexity of Integer Conversions
C provides multiple integer types and implicit conversion rules.
int signed_value = -1;
unsigned int unsigned_value = 1;
if (
signed_value <
unsigned_value
) {
/* ... */
}
If signed_value is converted to an unsigned type during the comparison, the result can differ from expectations.
Small integer types are also subject to integer promotion before arithmetic.
unsigned char left = 200;
unsigned char right = 100;
int result = left + right;
These rules were created to support efficient arithmetic on multiple processors, but they can cause the following errors.
- Incorrect signed comparisons
- Narrowing conversions
- Overflow in size calculations
- Misunderstanding of bitwise results
- Format string mismatches
- Incorrect array sizes
Explicit types and range checks should be used.
if (
count >
SIZE_MAX / element_size
) {
return false;
}
Complex Declaration Syntax
C declarators reflect how an identifier is used in an expression.
int *pointer;
int values[10];
int function(int value);
int (*callback)(int value);
Simple declarations are easy to read, but nested arrays, pointers, and functions can become complex.
int (*handlers[8])(
const void *data,
size_t size
);
This is an array of eight pointers to functions that take two parameters and return int.
It can be separated using typedef.
typedef int (*handler_function)(
const void *data,
size_t size
);
handler_function handlers[8];
The complexity of declaration syntax can reduce the readability of APIs that use many function pointers and arrays.
Preprocessor
The C preprocessor provides header inclusion, conditional compilation, and macros.
#if defined(_WIN32)
#include "windows_platform.h"
#else
#include "unix_platform.h"
#endif
It is useful for organizing platform-specific code and reducing repetitive code.
However, it is a token-substitution system that does not understand types, scopes, or function semantics.
#define SQUARE(value) \
((value) * (value))
The following call evaluates its argument twice.
int result =
SQUARE(index++);
Macros can cause the following problems.
- Repeated argument evaluation
- Difficult debugging
- Confusing error locations
- Weak type checking
- Name collisions
- Large preprocessed output
- Source structures that differ by conditions
- Increased build dependencies
When possible, preprocessor use can be reduced by replacing macros with functions, inline, enumerations, constexpr, _Generic, and related facilities.
Lack of Modules and Package Management
ISO C does not provide a general language-level module system or package manager.
Programs primarily divide interfaces between headers and source files.
include/
library.h
src/
library.c
Searching for, downloading, versioning, and building external libraries are handled by separate tools.
Representative problems include the following.
- Operating system-specific library installation methods
- Compiler-specific options
- Header search paths
- Differences between static and shared libraries
- ABI compatibility
- Transitive dependencies
- Debug and release configurations
- Cross-compilation
- Package version conflicts
CMake, Meson, pkg-config, operating system package managers, and other tools supplement these missing facilities, but no single standardized method is universally used.
Header-Based Interfaces
Headers are the traditional method of sharing declarations across translation units.
#ifndef CALCULATOR_H
#define CALCULATOR_H
int add(
int left,
int right
);
#endif
Headers are simple and supported by nearly all C compilers.
However, they have the following problems.
- The same contents are preprocessed repeatedly.
- Include guards are required.
- Declarations and definitions are separated.
- Header order can matter.
- Macros can pollute names.
- Internal implementation details can be exposed.
- Compilation time can increase in large projects.
Opaque structures and small public headers can reduce the amount of exposed information.
Error Handling
C has no general exception-handling syntax.
Functions report errors using return values, output parameters, errno, and similar mechanisms.
enum result {
RESULT_SUCCESS,
RESULT_INVALID_ARGUMENT,
RESULT_OUT_OF_MEMORY,
RESULT_IO_ERROR
};
enum result document_open(
const char *path,
struct document **document
);
Explicit error codes have the following advantages.
- No runtime exception system is required.
- They are easy to pass through an ABI.
- Error paths are visible in source.
- They can be used in freestanding environments.
However, callers can ignore return values.
document_open(
path,
&document
);
/* No error check */
Error-propagation code can become repetitive, and when combined with resource cleanup, functions can become long.
if (!first_operation()) {
return false;
}
if (!second_operation()) {
cleanup_first();
return false;
}
Projects must design consistent error codes, logging, and cleanup rules.
Lack of Automatic Resource Management
C has no general automatic resource-management facility comparable to C++ destructors, Rust’s Drop, or C#’s using.
struct resource *resource =
resource_create();
if (resource == NULL) {
return false;
}
/* ... */
resource_destroy(resource);
If a function returns early, cleanup can be omitted.
if (!process(resource)) {
return false;
}
A cleanup label can collect resource release in one place.
bool run(void) {
bool result = false;
struct resource *resource = NULL;
resource =
resource_create();
if (resource == NULL) {
goto cleanup;
}
if (!process(resource)) {
goto cleanup;
}
result = true;
cleanup:
resource_destroy(resource);
return result;
}
This approach is explicit, but the language does not guarantee that cleanup occurs.
Difficulty of Concurrency
C11 introduced atomic types, standard threads, and memory ordering, but writing correct parallel programs remains difficult.
#include <stdatomic.h>
static atomic_bool ready = false;
static int shared_value;
A writer thread can use a release store.
shared_value = 42;
atomic_store_explicit(
&ready,
true,
memory_order_release
);
A reader thread can use an acquire load.
if (
atomic_load_explicit(
&ready,
memory_order_acquire
)
) {
use_value(shared_value);
}
Choosing an incorrect memory order can cause errors that appear only on particular architectures.
Accessing an ordinary object concurrently without synchronization creates a data race, which is undefined behavior in C.
static int counter;
void increment(void) {
++counter;
}
volatile is not a thread-synchronization mechanism. The official Linux kernel documentation also explains that using volatile as though it were an atomic variable is incorrect and that locks or proper synchronization mechanisms should be used for shared data.[131]
Security Risks
Memory errors in C can become security vulnerabilities rather than merely causing program crashes.
Representative vulnerabilities include the following.
- Stack buffer overflows
- Heap buffer overflows
- Use after free
- Double free
- Format string vulnerabilities
- Integer overflow
- Type confusion
- Data races
- Exposure of uninitialized memory
void print_message(
const char *message
) {
printf(message);
}
If external input is used as the format string, memory-read and memory-write vulnerabilities can occur.
printf("%s", message);
The following defenses are used together to improve the security of C programs.
- Memory-safe design
- Bounds checking
- Integer-overflow checking
- Static analysis
- Sanitizers
- Fuzzing
- Stack protection
- ASLR
- Non-executable memory
- Control-flow protection
- Hardened allocators
- Code review
- Restricted coding rules
Defensive options can reduce exploitability, but they do not automatically make incorrect C code safe.
Maintenance Cost
C provides a relatively small runtime and direct control, but maintenance burden can become high in large programs.
Major causes include the following.
- Ownership is not represented in types.
- Public headers and internal implementations are separated.
- Error handling is manual.
- Macros and conditional compilation are used.
- Platform-specific code is common.
- ABI compatibility must be managed.
- Global state is common.
- String handling is manual.
- Memory safety requires separate verification.
- Concurrency is low-level.
struct object *object_get_child(
struct object *object
);
The declaration alone does not reveal the following.
- Whether the returned pointer can be null
- Whether the caller must release it
- Whether it outlives the parent
- Whether it can be used from another thread
- Whether it is mutable
- Whether it remains valid after the next call
Documentation, naming conventions, static-analysis annotations, and clear API design must supplement the type system.
Productivity
C is efficient for tasks requiring direct control, but offers limited support for rapidly developing ordinary business logic and complex applications.
The standard language and library do not include the following facilities.
- Dynamic array containers
- Hash maps
- General string objects
- Regular expressions
- JSON processing
- Network sockets
- GUI
- Package management
- Reflection
- General asynchronous functions
- Automatic serialization
These facilities must be implemented directly or obtained from external libraries.
struct dynamic_array {
void *data;
size_t count;
size_t capacity;
size_t element_size;
};
Functionality supplied by standard libraries or package ecosystems in other languages may need to be designed manually in C.
Limited Abstraction Facilities
C can build abstractions using structures, function pointers, the preprocessor, and _Generic.
struct stream {
void *context;
size_t (*read)(
void *context,
void *buffer,
size_t size
);
void (*close)(
void *context
);
};
This approach allows direct control over runtime cost and data layout.
However, the following language-level features are limited or absent.
- Namespaces
- Methods
- Interfaces
- Type-safe generics
- Function overloading
- Operator overloading
- Classes
- Pattern matching
- Algebraic data types
- General modules
Large libraries often use name prefixes.
image_create();
image_destroy();
image_load();
image_save();
C23 modernized several areas, but C retains its fundamental function-and-structure-centered model.
Compiler and Dialect Differences
The C standard provides a common foundation, but real projects often use compiler extensions.
The Linux kernel uses the GNU C11 dialect rather than ISO C11 alone and relies on multiple GNU extensions.[132]
#if defined(__GNUC__)
__attribute__((noreturn))
#endif
void panic(
const char *message
);
Compiler extensions can provide the following facilities.
- Attributes
- Vector types
- Inline assembly
- 128-bit integers
- Section placement
- Calling conventions
- Statement expressions
- Built-in functions
Extensions can produce more efficient code on particular platforms, but reduce portability to other compilers.
ABI Dependence
C can be portable at the source level, but compiled object files and libraries depend on an ABI.
Same C source
↓
Linux System V ABI
Windows x64 ABI
Apple ABI
Embedded ABI
An ABI determines the following.
- Sizes of fundamental types
- Structure layout
- Argument passing
- Return values
- Register preservation
- Stack alignment
- Symbol names
- Shared library formats
Libraries compiled for different ABIs may not be linkable directly.
Exposing structures in a public API can make adding members an ABI-breaking change.
struct public_context {
uint32_t version;
uint32_t flags;
};
Long-lived ABIs can use opaque pointers or structure size and version fields.
Limited Standard Library
The C standard library provides a small common foundation but does not include every facility needed by modern applications.
Representative missing facilities include the following.
- Network sockets
- Process management
- Directory traversal
- Dynamic library loading
- File-system monitoring
- Cryptography
- Compression
- Graphics
- Audio
- GUI
- HTTP
- Databases
- XML and JSON
These facilities are supplied through POSIX, Win32, platform SDKs, and external libraries.
ISO C
→ Common language and fundamental library
Platform APIs
→ Operating system functionality
External libraries
→ Application-domain functionality
Projects must directly manage the selection, building, and distribution of external dependencies.
Unicode Processing
C’s traditional character model developed around char, null-terminated strings, the current locale, and multibyte conversion.
Modern Unicode processing presents the following problems.
chardoes not necessarily represent one character.- One UTF-8 character can occupy multiple bytes.
strlenreturns a byte count rather than a character count.- The size and encoding of
wchar_tdiffer among platforms. - Case conversion is not always one-to-one.
- Normalization and grapheme-cluster processing are absent from the standard library.
const char *text = "한글";
size_t bytes = strlen(text);
In a UTF-8 environment, bytes is the number of bytes rather than the number of Korean syllables.
Complete Unicode handling can require an external library.
Conservative Standard Evolution
The C standard places great importance on compatibility with existing code and implementations.
This conservatism has the following advantages.
- Preservation of old source code
- Protection of existing compiler and tool ecosystems
- Long-term support for industrial systems
- Gradual adoption of standards
- ABI and library stability
However, it makes old designs and unsafe practices difficult to remove quickly.
Compatibility preservation
↔
Language modernization
C23 added nullptr, constexpr, typeof, bit operations, and new preprocessing facilities, but did not replace C’s pointer, array, and manual memory-management model.
Learning Difficulty
The basic syntax of C can appear relatively small and simple.
int add(
int left,
int right
) {
return left + right;
}
However, writing correct C programs requires understanding the following.
- Types and integer conversions
- Pointers
- Array adjustment
- Object lifetimes
- Storage durations
- Scope and linkage
- Alignment
- Structure padding
- Effective types
- Strict aliasing
- Undefined behavior
- Compilation and linking
- ABIs
- The concurrency memory model
A program that compiles and appears to work in simple tests can still violate the standard.
Learning the basic syntax of C can be quick, but understanding the language precisely and writing safe system programs requires substantial knowledge.
When C Is Appropriate
C can be a strong choice under conditions such as the following.
- Direct access to operating systems and hardware is required.
- Microcontrollers and freestanding environments must be supported.
- A small runtime is required.
- Data layout and memory use must be controlled directly.
- Tight integration with existing C libraries is required.
- A stable ABI callable from multiple languages is needed.
- Old platforms and compilers must be supported.
- Certified C toolchains and existing industrial code are available.
- Execution costs must be analyzed precisely.
- Operating system kernels or drivers are being developed.
Hardware control
+
Small runtime
+
Existing C ecosystem
+
Stable ABI
→ C is likely to be appropriate
When C May Be Inappropriate
Another language may deserve priority when the following requirements are important.
- Memory safety is the highest priority.
- Complex web business logic must be developed quickly.
- A large GUI must be implemented.
- Automatic memory management is required.
- String and dynamic-data processing are central.
- Safe concurrency abstractions are required.
- Strong module and package ecosystems are needed.
- Development speed is more important than small execution costs.
- New security-sensitive code must be maintained for a long time.
- The team cannot reliably manage low-level memory rules.
C does not have to be excluded completely and can instead be limited to performance modules and platform layers.
Higher-level application
↓
Small C ABI
↓
Performance and hardware modules
In this structure, keeping the C boundary small and thoroughly validating it is important.
Combining C with Other Languages
C can be used as a foundational module even when it is not the primary language of the entire program.
Python, C#, Java, and Lua
↓
FFI
↓
C library
↓
Operating system and hardware
A mixed architecture has the following advantages.
- Development productivity of higher-level languages
- Reuse of existing C libraries
- Optimization only where performance matters
- Connection to operating system APIs
- A shared native core across multiple platforms
However, the following matters must be defined clearly at the language boundary.
- Memory ownership
- String encoding
- Array lengths
- Nullability
- Structure layout
- Calling conventions
- Error propagation
- Threads
- Callback lifetimes
- Exception and panic handling
Overall Advantages and Limitations
The major advantages of C are as follows.
Advantages
├── Support for diverse hardware and execution environments
├── Native execution performance
├── Predictable basic costs
├── Direct memory and hardware control
├── Small runtime
├── Stable C ABI
├── Mature compilers and libraries
├── High compatibility with existing code
├── Separate compilation and library distribution
└── Interoperability with multiple languages
Its major limitations are as follows.
Limitations
├── Manual memory management
├── Lack of array bounds checking
├── No tracking of object lifetimes and ownership
├── Limitations of null-terminated strings
├── Undefined behavior
├── Complex integer conversions and declarations
├── Risks of the preprocessor
├── Lack of modules and package management
├── Limited standard library
├── Complexity of concurrency
├── Risk of security vulnerabilities
└── Maintenance burden in large programs
C’s strengths and weaknesses often arise from the same design choices. Because array bounds checking is not mandatory, memory can be handled without additional checking costs, but an incorrect bound calculation can corrupt memory. Because object lifetimes are not managed automatically, runtimes can remain small and custom allocators can be designed, but use-after-free and memory leaks can occur.
Because implementations are given broad freedom to exploit diverse hardware characteristics, C can operate efficiently across many systems. However, if implementation-defined characteristics and undefined behavior are not understood, program results can differ by platform and optimization level.
Therefore, choosing C should not be based simply on the fact that it is fast or old. The target environment, existing ecosystem, performance and memory requirements, security and maintenance costs, and the development team’s experience must all be considered.
C remains powerful at the boundaries between operating systems and hardware, in small embedded devices, and in fields requiring stable native ABIs. In ordinary applications and new security-sensitive components, other languages that provide memory safety and higher-level abstractions may be more appropriate.
Related Documents
- Programming language
- History of programming languages
- Programming paradigm
- Imperative programming
- Procedural programming
- Structured programming
- System programming
- Low-level programming language
- Compiled language
- Static typing
- Type system
- C standard
- ISO/IEC 9899
- WG14
- C standard library
- C preprocessor
- C compiler
- Compiler
- Assembler
- Linker
- Loader
- Application binary interface
- C ABI
- Foreign function interface
- Object file
- Executable file
- Abstract machine
- Translation unit
- Separate compilation
- Cross-compilation
- Memory model
- Object
- Object lifetime
- Storage duration
- Memory alignment
- Dynamic memory allocation
- Pointer
- Pointer arithmetic
- Pointer provenance
- Array
- String
- Null pointer
- Structure
- Union
- Flexible array member
- Effective type
- Strict aliasing rule
- Undefined behavior
- Implementation-defined behavior
- Unspecified behavior
- Data race
- Atomic operation
- Memory ordering
- Thread
- Mutex
- Embedded system
- Real-time operating system
- Operating system
- UNIX
- Linux
- Device driver
- Firmware
- Bootloader
- Database
- Computer network
- Computer graphics
- Game engine
- Multimedia
- High-performance computing
- Memory safety
- Buffer overflow
- Use after free
- Static analysis
- Dynamic analysis
- Fuzzing
- AddressSanitizer
- UndefinedBehaviorSanitizer
- K&R C
- ANSI C
- C89
- C90
- C95
- C99
- C11
- C17
- C23
- BCPL
- B (programming language)
- C++
- Objective-C
- Java
- C#
- JavaScript
- Go (programming language)
- Rust
- Zig
- D (programming language)
- Swift
- Python
- Lua
- Wave (programming language)
- GCC
- Clang
- Microsoft Visual C++
- Portable C Compiler
- TinyCC
- SQLite
- PostgreSQL
- FreeRTOS
- Zephyr
- FFmpeg
- libcurl
- Dennis M. Ritchie, The Development of the C Language ↩
- Martin Richards, BCPL Reference Manual ↩
- Dennis M. Ritchie, The Evolution of the UNIX Time-sharing System ↩
- Dennis M. Ritchie, The Development of the C Language — B ↩
- Dennis M. Ritchie, Very Early C Compilers and Language ↩
- Dennis M. Ritchie, Very Early C Compilers and Language ↩
- Bell Labs, last1120c/c00.c ↩
- Dennis M. Ritchie, Very Early C Compilers and Language — Structure Syntax ↩
- Brian W. Kernighan, Programming in C — A Tutorial ↩
- Dennis M. Ritchie, The Development of the C Language — Embryonic C ↩
- S. C. Johnson and D. M. Ritchie, Portability of C Programs and the UNIX System ↩
- Dennis M. Ritchie, The Development of the C Language — K&R C ↩
- WG14, Rationale for International Standard — Programming Languages — C ↩
- WG14, ISO/IEC C Project Status and Milestones ↩
- WG14, Approved C Standards ↩
- WG14, C99 Rationale ↩
- WG14 N1256, C99 with Technical Corrigenda ↩
- WG14 N1570, C11 Committee Draft ↩
- WG14, ISO/IEC C Project Status and Milestones ↩
- ISO/IEC 9899:2024 ↩
- WG14 N3220, ISO/IEC 9899:2024 Working Draft ↩
- WG14 N3220, Introduction ↩
- Dennis M. Ritchie, The Development of the C Language — Conclusion ↩
- WG14 N3255, The C Standard Charter ↩
- ISO/IEC 9899:2024, Scope ↩
- ISO/IEC 9899:2024, Abstract and Scope ↩
- ISO/IEC 9899:2024, Function specifiers ↩
- ISO/IEC 9899:2024, 3.5.3 Undefined behavior ↩
- WG14 N3255, The C Standard Charter ↩
- WG14, C99 Rationale ↩
- ISO/IEC 9899:2024, Conformance and Freestanding Implementations ↩
- ISO/IEC 9899:2024, Program Structure and Translation Units ↩
- WG14 N3255, The C Standard Charter ↩
- WG14 N3220, ISO/IEC 9899:2024, Language ↩
- WG14 N3220, ISO/IEC 9899:2024, Lexical Elements, Types, Expressions, Declarations, Statements and Functions ↩
- ISO/IEC 9899:2024, 6.2.4 Storage durations of objects ↩
- ISO/IEC 9899:2024, 7.24.3 Memory management functions ↩
- ISO/IEC 9899:2024, 6.5 Expressions — effective type ↩
- ISO/IEC 9899:2024, 6.2.6 Representations of types ↩
- ISO/IEC 9899:2024, 6.5 effective type rules ↩
- WG14 N2223, Clarifying the C Memory Object Model ↩
- ISO/IEC 9899:2024, 5.1.2.5 Multi-threaded executions and data races ↩
- GCC, Options Controlling the Kind of Output ↩
- LLVM, Clang Command Guide ↩
- ISO/IEC 9899:2024, 5.1.1.1 Program structure ↩
- ISO/IEC 9899:2024, 5.1.1.2 Translation phases ↩
- GCC, Preprocessor Options ↩
- ISO/IEC 9899:2024, 5.1.1.3 Diagnostics ↩
- GCC, Overall Options ↩
- Linux manual, elf(5) ↩
- ISO/IEC 9899:2024, 5.1.1.2 Translation phases ↩
- Linux manual, execve(2) ↩
- Linux manual, ld.so(8) ↩
- ISO/IEC 9899:2024, 5.1.2.1 Execution environments ↩
- ISO/IEC 9899:2024, 5.1.2.3.2 Program startup ↩
- ISO/IEC 9899:2024, 5.1.2.2 Freestanding environment ↩
- ISO/IEC 9899:2024, 7.24.4.4 The exit function ↩
- LLVM, Cross-compilation using Clang ↩
- ISO, ISO/IEC 9899:2024 — Programming languages — C ↩
- ISO/IEC JTC1/SC22/WG14 — C ↩
- WG14, ISO/IEC 9899 Project Status and Milestones ↩
- ISO, ISO/IEC 9899:2024 ↩
- WG14 N3220, 4 Conformance ↩
- WG14 N3220, 4 Conformance — hosted and freestanding implementations ↩
- WG14 N3220, 4 Conformance ↩
- WG14 N3220, Clause 7 Library ↩
- WG14 N3220, 7.1.3 Reserved identifiers ↩
- WG14, C Projects and Technical Specifications ↩
- WG14, C Standard Issues Lists ↩
- WG14 N3220, ISO/IEC 9899:2024 Working Draft ↩
- GCC Internals, GENERIC and GIMPLE ↩
- GCC, Options Controlling the Kind of Output ↩
- Dennis M. Ritchie, Very Early C Compilers and Language ↩
- Dennis M. Ritchie, The Development of the C Language ↩
- Portable C Compiler Project ↩
- GCC Internals ↩
- GCC 16.1 Released ↩
- The LLVM Compiler Infrastructure Project ↩
- Clang C Language Status ↩
- Microsoft C/C++ Language Conformance ↩
- Microsoft, /std Specify Language Standard Version ↩
- Microsoft, /Zc:preprocessor ↩
- Intel oneAPI Toolkit ↩
- IBM Open XL C/C++ for z/OS ↩
- IBM Open XL C/C++ for AIX ↩
- Arm Compiler for Embedded ↩
- Arm, Standard C Implementation Definition ↩
- Arm Compiler for Embedded FuSa ↩
- GNU Arm Embedded Toolchain ↩
- Open Watcom C/C++ User’s Guide ↩
- Linux Kernel Documentation, Programming Language ↩
- FreeRTOS, Source Code Organization ↩
- Zephyr Project, Primary Source Repository ↩
- Python Developer’s Guide, CPython Internals ↩
- Python Documentation, Extending Python with C or C++ ↩
- Lua, Getting Started ↩
- Python/C API Reference, Introduction ↩
- Git User Manual, A bird’s-eye view of Git’s source code ↩
- SQLite, Why Is SQLite Coded In C ↩
- SQLite Home Page ↩
- PostgreSQL Wiki, So, you want to be a developer? ↩
- PostgreSQL Documentation, C-Language Functions ↩
- nginx Official Website ↩
- libcurl, Network Transfer Library ↩
- libcurl, C API Overview ↩
- FFmpeg Documentation ↩
- FFmpeg Codecs Documentation ↩
- FFmpeg Formats Documentation ↩
- The HDF Group, HDF5 Library and Programming Model ↩
- Standard C++ Foundation, How did C++ get its name and history ↩
- Apple, About Objective-C ↩
- Oracle, Java Virtual Machine Specification — Introduction ↩
- Go Documentation, Frequently Asked Questions — C and Go ↩
- The Rust Reference, Behavior considered undefined ↩
- Wave Programming Language Blog, Pointer Support Arrives ↩
- Wave Programming Language Blog, Explicit Mutability and Safer Pointers ↩
- Wave Programming Language, Official Repository ↩
- WG14 N2363, C provenance semantics: examples ↩
- Dennis M. Ritchie, The Development of the C Language ↩
- Dennis M. Ritchie, Biography ↩
- Linux Kernel Documentation, Programming Language ↩
- ISO, ISO/IEC 9899:2024 ↩
- SQLite, Home Page ↩
- SQLite, Why Is SQLite Coded In C ↩
- Dennis M. Ritchie, The Development of the C Language — Conclusion ↩
- ISO, ISO/IEC 9899:2024 — Programming languages — C ↩
- Linux Kernel Documentation, Programming Language ↩
- SQLite, Why Is SQLite Coded In C ↩
- CISA, The Urgent Need for Memory Safety in Software Products ↩
- WG14 N3529, Ghosts and Demons: Undefined Behavior in the C2Y Core Language ↩
- Linux Kernel Documentation, Why the volatile type class should not be used ↩
- Linux Kernel Documentation, Programming Language ↩