C is a procedural, compiled, general-purpose programming language developed in the early 1970s by Dennis Ritchie at Bell Labs. It's known for its efficiency, flexibility, and low-level memory access capabilities.
C is considered middle-level because it bridges the gap between low-level (like Assembly) and high-level (like Python or Java) languages. It provides high-level abstractions like functions and data types while also allowing direct memory manipulation through pointers, which is a low-level feature.
A compiler (which C uses) translates the entire source code into machine code (an executable file) in one go. You then run the resulting executable file. An interpreter reads the source code line by line, translating and executing it on the fly.
- Compiler: Scans the whole program first, then executes. Faster execution.
- Interpreter: Executes line-by-line. Slower execution, but easier for debugging.
The primary data types are:
int: For integer values (4 bytes).short int(2 bytes, smaller range)long int(4 or 8 bytes, larger range)unsigned int(only positive values, doubles the upper limit)
char: For single characters (1 byte).float: For single-precision floating-point numbers (4 bytes).double: For double-precision floating-point numbers (more precise thanfloat) (8 bytes).void: Represents the absence of a type, primarily used for functions that don't return a value or for generic pointers.
These can be modified with qualifiers like short, long, signed, and unsigned.
The main() function is the entry point of every C program. When you run a C program, the operating system starts execution from the first line of code inside the main() function.
#include <stdio.h>: Angle brackets tell the preprocessor to search for the header file in the system directories (standard library locations).#include "myheader.h": Quotation marks tell the preprocessor to search for the header file in the current directory first, then in system directories if not found.
Keywords are reserved words that have special meaning in C and cannot be used as variable names or identifiers. Examples include: int, char, if, else, while, for, return, struct, union, typedef, static, extern, const, volatile, sizeof, break, continue, switch, case, default.
=: Assignment operator - assigns a value to a variable (x = 5;).==: Equality operator - compares two values for equality and returns true (1) or false (0) (if (x == 5)).
The key difference is when the condition is checked.
- A
whileloop is an entry-controlled loop. It checks the condition before executing the loop body. If the condition is false initially, the loop will never execute. - A
do-whileloop is an exit-controlled loop. It executes the loop body at least once and then checks the condition at the end.
You should use a switch statement when you are comparing a single variable against a list of discrete constant integer or character values. It can be more readable and sometimes more efficient than a long chain of if-else if statements. An if-else if ladder is more flexible and can handle complex conditions, ranges, and non-constant expressions.
break: Immediately terminates the innermost loop (for,while,do-while) or aswitchstatement it is in. Execution continues at the statement immediately following the loop or switch.continue: Skips the remaining code inside the current iteration of a loop and proceeds to the next iteration.
Both increment the value of i by 1, but they differ in when the increment happens:
++i(pre-increment): Incrementsifirst, then returns the new value.i++(post-increment): Returns the current value ofifirst, then increments it.
In standalone statements, there's no difference. The difference matters when used in expressions: x = ++i; assigns the incremented value, while x = i++; assigns the original value.
The ternary operator (? :) is a conditional operator that provides a shorthand way to write simple if-else statements.
Syntax: condition ? expression1 : expression2
If the condition is true, expression1 is evaluated and returned; otherwise, expression2 is evaluated and returned.
Example: max = (a > b) ? a : b;
goto: An unconditional jump statement that transfers control to a labeled statement within the same function. It's generally discouraged as it can make code difficult to read and debug.- Function calls: Transfer control to a separate function, which can be in the same file or a different file. They provide better code organization, reusability, and maintainability.
- Declaration (or Prototype): Tells the compiler about a function's name, return type, and the types of its parameters. It's like a function's signature. Example:
int add(int a, int b);. - Definition: Provides the actual body of the function, i.e., the code that will be executed when the function is called.
C is strictly pass by value. This means that when you pass an argument to a function, a copy of the argument's value is made and sent to the function. Any changes the function makes to this copy do not affect the original variable in the calling function.
To simulate pass by reference, you pass the address of a variable (i.e., a pointer) to the function. This allows the function to modify the original variable's value by dereferencing the pointer.
Function overloading is the ability to define multiple functions with the same name but different parameter lists (different number or types of parameters). C does not support function overloading - each function must have a unique name. Languages like C++ support function overloading.
Recursion is a programming technique where a function calls itself to solve a problem by breaking it down into smaller, similar subproblems.
Advantages:
- Elegant and simple code for problems that have recursive structure (like factorial, Fibonacci)
- Natural fit for tree and graph traversals
Disadvantages:
- Can lead to stack overflow for deep recursions
- Generally slower than iterative solutions due to function call overhead
- Uses more memory due to function call stack
- Formal parameters: Variables declared in the function definition. They are placeholders that receive values when the function is called.
- Actual parameters (arguments): The real values or expressions passed to a function when it is called.
Example: In int add(int a, int b) - a and b are formal parameters. In result = add(5, 10); - 5 and 10 are actual parameters.
An array is a collection of a fixed number of elements of the same data type stored in contiguous memory locations. Elements are accessed using an index, starting from 0.
In C, a string is a one-dimensional array of characters that is terminated by a special character called the null character (\0). All standard library string functions (like strlen(), strcpy()) rely on this null terminator to know where the string ends.
strlen(): A library function from<string.h>that returns the number of characters in a string, not including the null terminator (\0).sizeof(): A compile-time operator that returns the total memory size allocated to the array in bytes, which includes the space for the null terminator and any other unused space in the array.
char str[] = "Hello";: Creates a character array in memory and copies the string "Hello" into it. The string is modifiable, andstris the name of the array (not a pointer variable).char *str = "Hello";: Creates a pointer that points to a string literal stored in the program's read-only memory section. Attempting to modify this string leads to undefined behavior.
C doesn't store array length information. For static arrays declared in the same scope, you can use: int length = sizeof(array) / sizeof(array[0]);. However, this doesn't work for arrays passed to functions (they decay to pointers). The common practice is to pass the array size as a separate parameter to functions.
sizeof(array)→ total bytes used by the array.sizeof(array)/sizeof(array[0])→ number of elements in the array.- Examples:
char c[20];
double d[20];
printf("%zu\n", sizeof(c)); // 20 (1 byte each)
printf("%zu\n", sizeof(d)); // 160 (8 bytes each)
// But number of elements:
printf("%zu\n", sizeof(c) / sizeof(c[0])); // 20
printf("%zu\n", sizeof(d) / sizeof(d[0])); // 20Multidimensional arrays are arrays of arrays. A 2D array like int arr[3][4] can be visualized as a table with 3 rows and 4 columns. In memory, multidimensional arrays are stored in row-major order - all elements of the first row, then all elements of the second row, and so on.
Array decay refers to the implicit conversion of an array to a pointer to its first element when the array is used in most expressions. When you pass an array to a function, what actually gets passed is a pointer to the first element, not the entire array. This is why sizeof() doesn't work as expected for arrays inside functions.
A pointer is a special variable that stores the memory address of another variable. It "points" to the location where data is stored rather than storing the data itself.
Pointers are one of C's most powerful features. They are used for:
- Dynamic memory allocation (allocating memory on the heap).
- Efficiently passing large data structures to functions (to avoid copying).
- Simulating pass by reference.
- Creating complex data structures like linked lists, trees, and graphs.
- Directly interacting with hardware memory addresses.
A NULL pointer is a pointer that does not point to any valid memory address. It's a special value (0) used to indicate that a pointer is intentionally empty. It's good practice to initialize pointers to NULL to prevent them from pointing to random memory locations.
A dangling pointer is a pointer that points to a memory location that has already been deallocated or freed. Accessing a dangling pointer leads to undefined behavior and can cause your program to crash. This often happens if you free() memory but don't set the pointer back to NULL.
Pointer arithmetic involves performing arithmetic operations (addition, subtraction, increment, decrement) on pointers. When you add 1 to a pointer, it doesn't add 1 byte - it adds the size of the data type it points to. For example, if int *p points to an integer, p + 1 points to the next integer (4 bytes ahead on most systems).
- For example:
#include <stdio.h>
int main() {
int arr[3] = {10, 20, 30};
int *p = arr; // p points to arr[0]
printf("p points to value: %d at address %p\n", *p, (void*)p);
p = p + 1; // move pointer by 1 (but jumps 4 bytes, since int = 4 bytes)
printf("After p+1, p points to value: %d at address %p\n", *p, (void*)p);
p = p + 1; // move pointer again
printf("After another p+1, p points to value: %d at address %p\n", *p, (void*)p);
return 0;
}
// OUTPUT:
// p points to value: 10 at address 0x7ffee8d630
// After p+1, p points to value: 20 at address 0x7ffee8d634
// After another p+1, p points to value: 30 at address 0x7ffee8d638| Data Type | Typical Size (bytes) | Pointer Arithmetic Jump (when you do p+1) |
|---|---|---|
char |
1 byte | Moves 1 byte forward |
short |
2 bytes | Moves 2 bytes forward |
int |
4 bytes | Moves 4 bytes forward |
float |
4 bytes | Moves 4 bytes forward |
double |
8 bytes | Moves 8 bytes forward |
long |
4 or 8 bytes (depends on system) | Moves that many bytes forward |
long long |
8 bytes | Moves 8 bytes forward |
void* |
8 bytes on 64-bit, 4 bytes on 32-bit | Just holds an address (not used for arithmetic without casting) |
*p++: Due to operator precedence, this is equivalent to*(p++). It dereferences the current pointer value, then increments the pointer (not the value).(*p)++: Dereferences the pointer first, then increments the value it points to.++*p: Equivalent to++(*p). Increments the value that the pointer points to.
A wild pointer is an uninitialized pointer that contains a random memory address. Using a wild pointer can lead to unpredictable behavior because it might point to any location in memory. Always initialize pointers before using them.
-
Stack: A region of memory where local variables and function call information are stored. Memory is managed automatically by the compiler (LIFO - Last In, First Out). It's fast but has a limited, fixed size.
-
Heap: A region of memory used for dynamic memory allocation. The programmer is responsible for manually allocating (
malloc,calloc) and deallocating (free) memory. It's larger and more flexible than the stack but slightly slower to access.
Both functions allocate memory on the heap.
-
void* malloc(size_t size): Allocates a single, contiguous block of memory of the specified size. The allocated memory is not initialized; it contains garbage values. -
void* calloc(size_t num, size_t size): Allocates memory for an array ofnumelements, each of sizesize. The allocated memory is initialized to zero.
The free() function deallocates memory that was previously allocated using malloc(), calloc(), or realloc(). It returns the memory block back to the heap so it can be reused. Failing to call free() leads to memory leaks.
void* realloc(void* ptr, size_t new_size) changes the size of a previously allocated memory block. It can:
- Expand the existing block if there's adjacent free space
- Allocate a new block, copy the data, and free the old block if expansion isn't possible
- Free the memory if
new_sizeis 0 - Act like
malloc()ifptrisNULL
Memory leaks occur when dynamically allocated memory is not freed, causing the program to gradually consume more memory over time. Prevention strategies:
- Always pair
malloc()/calloc()withfree() - Set pointers to
NULLafter freeing them - Use tools like Valgrind to detect memory leaks
- Follow consistent memory management patterns
A segmentation fault (segfault) occurs when a program tries to access memory that it's not allowed to access, such as:
- Dereferencing
NULLor uninitialized pointers - Accessing memory outside array bounds
- Accessing freed memory
- Writing to read-only memory This results in the operating system terminating the program.
struct(Structure): It is a user defined data type that can be used to group elements of different types into a single type. Memory is allocated for all its members, and the size of the struct is the sum (or more, due to padding) of the sizes of its members. For example:
struct {
char *engine;
} car1, car2;
int main() {
car1.engine = "DDis 190 Engine";
car2.engine = "1.2 L Kappa Dual VTVT";
printf("%s\n", car1.engine);
printf("%s", car2.engine);
return 0;union(Union): A user-defined data type where all members share the same memory location. Memory is allocated for the largest member only. You can only use one member of the union at a time.
Analogy: A struct is a toolbox with different compartments for each tool. A union is a single compartment where you can only place one tool at a time.
A structure tag is the name given to a structure type definition. It allows you to declare variables of that structure type later in your code without redefining the structure. The tag follows the struct keyword in the structure definition.
Example:
struct Point { // "Point" is the structure tag
int x;
int y;
};You can now declare variables of type struct Point:
struct Point p1, p2;Notes:
- The structure tag is optional, but without it, you cannot declare variables of that type elsewhere unless you use a typedef.
- The tag only defines the type, not a variable itself.
Anonymous struct (no tag):
struct {
int x;
int y;
} p1; // Only p1 can be declared this wayWith typedef:
typedef struct Point {
int x;
int y;
} Point;
Point p1; // Now you can use "Point" directlyWith type:
typedef is a keyword used to create aliases (alternative names) for existing data types. It makes code more readable and portable.
Example: typedef unsigned int uint; allows you to use uint instead of unsigned int.
Common use with structures: typedef struct { int x, y; } Point; allows you to declare variables as Point p; instead of struct Point p;.
Structure padding is the insertion of unused bytes between structure members to ensure proper memory alignment. Most processors access data more efficiently when it's aligned to specific byte boundaries (usually multiples of the data type's size). The compiler automatically adds padding to achieve this alignment, which is why sizeof(struct) might be larger than the sum of its members' sizes.
Yes, C supports nested structures. You can declare a structure as a member of another structure. This is useful for organizing related data hierarchically.
struct Address {
char street[50];
char city[30];
};
struct Person {
char name[30];
struct Address addr; // Nested structure
int age;
};
// driver code
int main() {
struct Person p1; // instantiation of struct Person
// Accessing nested fields
strcpy(p1.addr.street, "123 Main St");
strcpy(p1.addr.city, "New York");
printf("Street: %s\n", p1.addr.street);
printf("City: %s\n", p1.addr.city);
return 0;
}A bit field allows you to specify the exact number of bits a structure member should occupy. This is useful for memory optimization when you need to store small integer values.
struct Flags {
unsigned int flag1 : 1; // 1 bit
unsigned int flag2 : 2; // 2 bits
unsigned int flag3 : 5; // 5 bits
};The static keyword has two main uses depending on its context:
-
Inside a function: A
staticlocal variable retains its value between function calls. It is initialized only once. -
Outside a function (at the global level): A
staticglobal variable or function is only visible within the file it is declared in. This is called internal linkage.
The volatile keyword tells the compiler that a variable's value may be changed at any time by something outside of the program's control (e.g., the operating system, hardware, or another thread). This prevents the compiler from applying optimizations that might assume the variable's value is constant.
Preprocessor directives are commands that are processed by the C preprocessor before the actual compilation begins. They start with a # symbol. Common examples include:
#include: To include the contents of another file (like a header file).#define: To create macros (symbolic constants or function-like macros).#if, #else, #endif: For conditional compilation.
- Macro (
#define): A preprocessor directive that performs simple text substitution before compilation. It's faster as there is no function call overhead, but it has no type checking and can lead to unexpected side effects if not written carefully. - Function: A compiled block of code. It involves a function call overhead (pushing/popping from the stack), but it is type-safe and generally easier to debug.
Command line arguments allow you to pass information to your program when it's executed. The main() function can accept two parameters:
int argc: The number of command line arguments (including the program name)char *argv[]: An array of strings containing the arguments
Example: int main(int argc, char *argv[])
- Local variables: Declared inside a function or block. They have limited scope (visible only within that function/block) and automatic storage duration (destroyed when the function ends).
- Global variables: Declared outside all functions. They have file scope (visible throughout the file) and static storage duration (exist for the entire program execution).
Header guards prevent the same header file from being included multiple times in the same compilation unit, which would cause redefinition errors. They are implemented using preprocessor directives:
#ifndef MYHEADER_H
#define MYHEADER_H
// Header content here
#endifAlternatively, many compilers support #pragma once as a simpler solution.
When using fopen(), you can specify a mode to determine how the file will be accessed:
"r": Read - Opens an existing text file for reading. Fails if the file doesn't exist."w": Write - Creates a text file for writing. If the file exists, its contents are erased."a": Append - Opens a text file for writing at the end. Creates the file if it doesn't exist."r+": Opens a text file for both reading and writing."w+": Creates a text file for both reading and writing. Erases existing content."a+": Opens a text file for both reading and appending.
You can also add a b to any mode (e.g., "rb", "wb") to open the file in binary mode, which prevents any translation of special characters like newline sequences.
These functions are used for random file access.
-
int fseek(FILE *stream, long offset, int whence): Sets the file position indicator for the given stream.stream: The file pointer.offset: The number of bytes to move from thewhenceposition.whence: The starting position (SEEK_SETfor the beginning,SEEK_CURfor the current position,SEEK_ENDfor the end of the file).
-
long ftell(FILE *stream): Returns the current value of the file position indicator for the given stream. This tells you how many bytes you are from the beginning of the file.
int fgetc(FILE *stream): Reads a single character from the specified file stream.int getchar(): Reads a single character from the standard input (stdin). It's equivalent tofgetc(stdin).
Both return the character as an int (not char) to accommodate the EOF value.
- Text mode: The default mode where the system may perform character translations (like converting
\nto\r\non Windows). Used for human-readable text files. - Binary mode: No character translations are performed. The data is read/written exactly as it appears in memory. Essential for non-text files like images, executables, or when you need exact byte-for-byte copying.
fprintf() allows you to specify the output stream, making it more flexible:
- You can write to files using file pointers
- You can write to
stderrfor error messages printf()is essentiallyfprintf(stdout, ...)- Better for logging and debugging when you need output to go to different destinations
A pointer to a pointer is a variable that stores the memory address of another pointer. It is used in scenarios where you need to change the address stored in a pointer from within a function. A common use case is dynamically allocating a 2D array.
int **ptr; declares a pointer ptr that can hold the address of a pointer to an int.
A function pointer is a pointer that stores the memory address of a function. This allows you to treat functions like variables, you can pass them to other functions, store them in arrays, and call them indirectly through the pointer. They are essential for implementing callback mechanisms and plugins.
Declaration: return_type (*pointer_name)(parameter_types);
Example: void (*my_func_ptr)(int);
A void pointer is a generic pointer that can point to a variable of any data type. It is also known as a generic pointer. Because the compiler doesn't know what type of object it points to, you cannot directly dereference a void pointer. You must first cast it to a specific pointer type (e.g., int*, char*) before dereferencing it. Functions like malloc() return a void pointer.
While arrays and pointers are closely related in C, they are not identical:
Arrays:
- Represent a fixed-size collection of elements
- Array name is a constant pointer to the first element
sizeof(array)gives the total size of the array- Cannot be reassigned
Pointers:
- Variables that store memory addresses
- Can be reassigned to point to different locations
sizeof(pointer)gives the size of the pointer itself (usually 4 or 8 bytes)- Can perform pointer arithmetic
- Pointer to constant (
const int *ptr): The value being pointed to cannot be changed through this pointer, but the pointer itself can be changed to point elsewhere. - Constant pointer (
int * const ptr): The pointer cannot be changed to point elsewhere, but the value it points to can be modified. - Constant pointer to constant (
const int * const ptr): Neither the pointer nor the value it points to can be changed.
A self-referential structure is a structure that contains a member which is a pointer to another structure of the same type. This is the fundamental building block for creating dynamic data structures like linked lists, trees, and graphs.
struct Node {
int data;
struct Node* next; // Pointer to another struct of type Node
};Memory padding is the practice of adding empty bytes between members of a structure to ensure that each member is aligned on a memory address that is a multiple of its size. This is done by the compiler to improve the CPU's performance when accessing the members, as many CPUs can read data more efficiently from aligned addresses. Because of padding, sizeof(struct) may be greater than the sum of the sizes of its individual members.
This concept is crucial when dealing with structures containing pointers.
-
Shallow Copy: Copies the values of the members directly. If a member is a pointer, it copies the address, not the data being pointed to. Both the original and the copy will point to the same memory location, which can lead to errors (e.g., freeing the same memory twice).
-
Deep Copy: Copies the values of the members, but if a member is a pointer, it allocates new memory and copies the data from the original pointer's location to the new memory location. This ensures the original and the copy are completely independent.
Memory fragmentation occurs when free memory is broken into small, non-contiguous blocks, making it difficult to allocate large blocks even when total free memory is sufficient. There are two types:
- External fragmentation: Free memory exists but is scattered in small chunks
- Internal fragmentation: Allocated memory blocks contain unused space due to allocation policies
- Stack overflow: Occurs when the call stack exceeds its maximum size, typically due to infinite recursion or very deep function calls.
- Buffer overflow: Occurs when data is written beyond the boundaries of a buffer, potentially overwriting adjacent memory. This is a serious security vulnerability.
Bitwise operators perform operations on the individual bits of integer-type operands.
&(Bitwise AND): Sets a bit to 1 if both corresponding bits are 1.|(Bitwise OR): Sets a bit to 1 if at least one of the corresponding bits is 1.^(Bitwise XOR): Sets a bit to 1 if the corresponding bits are different.~(Bitwise NOT): Inverts all the bits.<<(Left Shift): Shifts bits to the left, filling with zeros (equivalent to multiplication by 2).>>(Right Shift): Shifts bits to the right (equivalent to division by 2).
A C program goes through four main stages to become an executable:
- Preprocessing: The preprocessor handles directives like
#include,#define, and removes comments. The output is a.ifile. - Compilation: The compiler translates the preprocessed code into assembly language specific to the target processor. The output is a
.sfile. - Assembly: The assembler converts the assembly code into machine code (object code). The output is a
.oor.objfile. - Linking: The linker combines the object code from your program with code from libraries (like the standard library) to resolve references and create the final executable file.
You can use the bitwise AND operator with 1:
if (n & 1)- the number is oddif (!(n & 1))- the number is even
This works because the least significant bit of any odd number is 1, and for even numbers it's 0.
Using bitwise XOR:
a = a ^ b;
b = a ^ b; // b = (a ^ b) ^ b = a
a = a ^ b; // a = (a ^ b) ^ a = bOr using arithmetic (be careful of overflow):
a = a + b;
b = a - b; // b = (a + b) - b = a
a = a - b; // a = (a + b) - a = bStorage classes define the scope (visibility) and lifetime (duration) of variables and functions. There are four storage classes:
auto: The default for local variables. They exist only within the block they are declared in.extern: Used to declare a global variable or function that is defined in another file. It extends the visibility of the variable/function.static: As explained before, it gives a local variable a permanent lifetime or limits the scope of a global variable/function to the current file.register: A hint to the compiler to store the variable in a CPU register instead of memory for faster access. The compiler can ignore this hint.
72. What is the difference between const int* p, int const* p, int* const p, and const int* const p?
This is a classic pointer declaration question. Read the declaration from right to left.
const int* porint const* p: This is a pointer to a constant integer. You cannot change the value of the integerppoints to (*p = 10;is illegal), but you can change the pointer itself (p = &another_var;is legal).int* const p: This is a constant pointer to an integer. You cannot change the pointer itself (p = &another_var;is illegal), but you can change the value it points to (*p = 10;is legal).const int* const p: This is a constant pointer to a constant integer. You can change neither the pointer nor the value it points to.
Typecasting is the explicit conversion of a variable from one data type to another. For example, you can cast a float to an int to truncate the decimal part.
Syntax: (target_type) expression;
Example: int x = (int)3.14; // x will be 3
This is different from implicit type promotion, where the compiler automatically converts a smaller type to a larger type in an expression to avoid data loss (e.g., int to float).
- Signed: Can represent both positive and negative numbers. The most significant bit is used as a sign bit (0 for positive, 1 for negative).
- Unsigned: Can only represent non-negative numbers (0 and positive). All bits are used for the magnitude, allowing for a larger positive range.
For example, signed char ranges from -128 to 127, while unsigned char ranges from 0 to 255.
Type promotion is the automatic conversion of smaller integer types to larger types during arithmetic operations. For example:
charandshortare promoted tointin expressions- If one operand is
float, the other is converted tofloat - If one operand is
double, the other is converted todouble
This ensures precision is maintained during calculations.
size_t is an unsigned integer type defined in <stddef.h> that represents the size of objects in bytes. It's the return type of the sizeof operator and is used for array indexing and loop counting. Its actual size depends on the platform (32-bit or 64-bit).
An enumeration is a user-defined data type that consists of a set of named integer constants. It makes code more readable by giving meaningful names to integer values.
enum Color {
RED, // 0
GREEN, // 1
BLUE // 2
};You can also assign specific values: enum Status { FAILURE = -1, SUCCESS = 1 };
#define: Creates simple text substitutions at preprocessing time. No type checking, no scope.enum: Creates actual integer constants with type checking and proper scope. Preferred for related constants.
Variadic functions can accept a variable number of arguments. They use the ellipsis (...) notation and require the <stdarg.h> header for implementation.
#include <stdarg.h>
int sum(int count, ...) {
va_list args;
va_start(args, count);
int total = 0;
for (int i = 0; i < count; i++) {
total += va_arg(args, int);
}
va_end(args);
return total;
}printf() is a famous example of a variadic function.
return: Normal termination frommain(). Returns control to the calling environment and passes the return value as the exit status.exit(): Immediately terminates the program from anywhere in the code. Calls cleanup functions registered withatexit()before termination.
Both set the program's exit status, but exit() can be called from any function.
Inline functions (using the inline keyword) suggest to the compiler to replace function calls with the actual function code at compile time. This eliminates function call overhead but may increase code size. The compiler may ignore the inline suggestion for complex functions.
inline int square(int x) {
return x * x;
}puts(): Outputs a string followed by a newline character. Simpler and faster for plain string output.printf(): Formatted output function that can handle various data types and format specifiers. More versatile but has more overhead.
// Method 1: Initialize with values
int arr1[5] = {1, 2, 3, 4, 5};
// Method 2: Partial initialization (rest filled with 0)
int arr2[5] = {1, 2};
// Method 3: Size determined by initializer
int arr3[] = {1, 2, 3, 4, 5};
// Method 4: Initialize all elements to 0
int arr4[5] = {0};
// Method 5: Designated initializers (C99)
int arr5[5] = {[0] = 1, [4] = 5};#include: Physically inserts the entire contents of another file, including all its definitions and declarations.- Forward declaration: Tells the compiler about the existence of a function or structure without providing its complete definition. Used to resolve circular dependencies and reduce compilation time.
// Forward declaration
struct Node;
void process(struct Node* n);
// vs including a header file
#include "node.h"C doesn't have built-in exception handling. Common error handling techniques include:
- Return codes: Functions return specific values to indicate success or failure
- Global
errnovariable: Set by system calls to indicate error types - Assertions: Using
assert()macro for debugging - Defensive programming: Checking parameters and return values
FILE *fp = fopen("file.txt", "r");
if (fp == NULL) {
perror("Error opening file");
return -1;
}errno is a global variable defined in <errno.h> that stores error codes set by system calls and library functions. You can use perror() or strerror() to get human-readable error messages.
#include <errno.h>
#include <string.h>
if (some_function() == -1) {
printf("Error: %s\n", strerror(errno));
}Assertions are debugging aids that check assumptions in your code. The assert() macro from <assert.h> terminates the program if the given expression is false. Assertions can be disabled by defining NDEBUG before including assert.h.
#include <assert.h>
int divide(int a, int b) {
assert(b != 0); // Program terminates if b is 0
return a / b;
}- Print statements: Adding
printf()statements to trace execution - Debuggers: Using tools like GDB to step through code
- Static analysis: Tools that analyze code without executing it
- Memory checkers: Tools like Valgrind to detect memory errors
- Assertions: Checking assumptions during development
- Logging: Systematic recording of program execution
strcpy(dest, src): Copies the entire source string to destination. Unsafe because it doesn't check buffer bounds.strncpy(dest, src, n): Copies at mostncharacters. Safer but doesn't guarantee null termination if the source is longer thann.
For safety, always use strncpy() and manually add null termination:
strncpy(dest, src, sizeof(dest) - 1);
dest[sizeof(dest) - 1] = '\0';strcmp(str1, str2): Compares two entire strings lexicographically. Returns 0 if equal, negative if str1 < str2, positive if str1 > str2.strncmp(str1, str2, n): Compares at most the firstncharacters of two strings.
memcpy(dest, src, n): Copiesnbytes from source to destination (no overlap allowed)memmove(dest, src, n): Likememcpy()but handles overlapping memory regionsmemset(ptr, value, n): Setsnbytes of memory to a specific valuememcmp(ptr1, ptr2, n): Comparesnbytes of two memory areas
getchar(): Standard C function from<stdio.h>. Reads from stdin, echoes the character, and waits for Enter key.getch(): Non-standard function (common in older compilers like Turbo C). Reads a single character without echoing and without waiting for Enter.
Note: getch() is not part of standard C and is not portable.
- Buffer overflows: Writing beyond array bounds
- Memory leaks: Not freeing dynamically allocated memory
- Dangling pointers: Using pointers after freeing memory
- Uninitialized variables: Using variables before setting their values
- Integer overflow: Arithmetic operations exceeding data type limits
- Off-by-one errors: Incorrect loop boundaries or array indexing
- Format string vulnerabilities: Using user input directly in printf format strings
- Always initialize variables before use
- Check return values of functions
- Use
constfor values that shouldn't change - Prefer
strncpy()overstrcpy()for safety - Free all dynamically allocated memory
- Use meaningful variable and function names
- Comment your code appropriately
- Avoid global variables when possible
- Use static functions for file-local functions
- Validate input parameters in functions
- Use safe string functions (
strncpy(),strncat(),snprintf()) - Always check array bounds
- Use dynamic memory allocation with proper size calculation
- Validate input lengths before processing
- Use tools like static analyzers and runtime checkers
- Consider using safer alternatives or libraries
Defensive programming is a practice of writing code that continues to function properly even when used incorrectly or in unexpected ways. Techniques include:
- Input validation
- Null pointer checks
- Error handling for all possible failure cases
- Assertions for debugging
- Clear error messages
- Graceful degradation when errors occur
int divide(int a, int b) {
if (b == 0) {
fprintf(stderr, "Error: Division by zero\n");
return -1; // Or some error code
}
return a / b;
}Endianness refers to the order in which bytes are stored in memory for multi-byte data types:
- Big-endian: Most significant byte first (network byte order)
- Little-endian: Least significant byte first (common in x86 systems)
This matters when:
- Reading binary files created on different systems
- Network programming
- Embedded systems programming
#pragma directives provide compiler-specific instructions. Common examples:
#pragma once: Prevents multiple inclusions of a header file#pragma pack(n): Controls structure padding alignment#pragma warning(disable:xxxx): Disables specific compiler warnings
#pragma pack(1) // No padding
struct PackedStruct {
char c;
int i;
};
#pragma pack() // Restore default padding- Library: A collection of functions and utilities that your code calls. You are in control of the program flow. Examples: C standard library, math library.
- Framework: A structure that calls your code. The framework controls the program flow, and you fill in the details. Less common in C compared to higher-level languages.
- Standard C Library (
libc): Basic functions (stdio, stdlib, string, math) - POSIX libraries: System calls and utilities for Unix-like systems
- OpenSSL: Cryptography and SSL/TLS
- SQLite: Embedded database
- cURL: HTTP client library
- GTK: GUI toolkit
- SDL: Game development and multimedia
- ncurses: Terminal-based user interfaces
- Compilation: Translates source code (.c files) into object code (.o files). Each source file is compiled independently.
- Linking: Combines object files and libraries into a final executable. Resolves external references and addresses.
- External linkage: Variables and functions can be accessed from other source files. This is the default for global variables and functions.
- Internal linkage: Variables and functions are only accessible within the current source file. Achieved using the
statickeyword.
In embedded systems, volatile is crucial for:
- Memory-mapped hardware registers that can change independently
- Variables modified by interrupt service routines
- Variables shared between threads
- Preventing compiler optimizations that assume values don't change
C doesn't have built-in generics, but you can simulate them using:
-
Void pointers with function pointers:
void sort_array(void* arr, size_t count, size_t size, int (*compare)(const void*, const void*));
-
Macros:
#define MAX(a,b) ((a) > (b) ? (a) : (b))
-
C11 _Generic keyword (limited support):
#define ABS(x) _Generic((x), \ int: abs, \ float: fabsf, \ double: fabs \ )(x)
-
C90/C89: Original ANSI C standard
-
C99: Added features like:
- Variable-length arrays
inlinefunctions//comments- New data types (
long long,_Bool) - Designated initializers
-
C11: Added:
- Multi-threading support
_Genericselections- Anonymous structures and unions
- Static assertions (
_Static_assert)
-
C18: Minor revision with bug fixes and clarifications, no new features

