Skip to main content

C

Welcome to C Programming

C is a powerful, efficient, and widely-used programming language that forms the foundation of many modern programming languages and operating systems. Developed by Dennis Ritchie at Bell Labs in the early 1970s, C provides low-level access to memory and hardware while maintaining portability across different platforms.

Known for its performance and simplicity, C is the language of choice for system programming, embedded systems, operating systems, and performance-critical applications. Many popular software like the Linux kernel, Windows operating system components, and database systems are written in C.

C strikes a perfect balance between high-level abstraction and low-level control, making it an excellent language for understanding how computers work at a fundamental level.

Introduction to C Programming

How to set up a C development environment and write your first "Hello, World!" program.

C is one of the most influential programming languages, known for its efficiency and close-to-hardware capabilities. Setting up a C development environment is straightforward and works on all major operating systems.

Step 1: Install a C Compiler

  1. Windows
    Install MinGW-w64 or Microsoft Visual Studio with C support. For MinGW, download from mingw-w64.org and add the bin directory to your PATH.

  2. macOS
    Install Xcode from the App Store, which includes the Clang compiler. Alternatively, install Command Line Tools by running xcode-select --install in Terminal.

  3. Linux
    Use your distribution's package manager. For Ubuntu/Debian:

    sudo apt install gcc

Step 2: Verify Installation

Open a terminal or command prompt and type:

gcc --version

This should display the installed compiler version.

Step 3: Write and Compile Your First C Program

  1. Create a file named hello.c with the following content:

    #include <stdio.h>
    
    int main() {
        printf("Hello, World!\n");
        return 0;
    }
  2. Compile the program:

    gcc hello.c -o hello
  3. Run the executable:

    ./hello   # On Linux/macOS
    hello.exe   # On Windows

    You should see the output: Hello, World!

Congratulations! You have successfully set up a C environment and run your first program. 🎉

C Syntax Basics

C has a simple and consistent syntax that has influenced many other programming languages. Understanding the basic syntax is crucial for writing correct C programs.

1. Basic Structure of a C Program

Every C program has a specific structure:

#include <stdio.h>  // Preprocessor directive for standard I/O

int main() {          // Main function - program entry point
    // Program statements go here
    printf("Hello, C!\n");
    return 0;         // Return 0 to indicate successful execution
}

2. Semicolons and Braces

C uses semicolons to terminate statements and braces {} to define code blocks:

int x = 5;        // Statement ends with semicolon

if (x > 3) {      // Braces define the if block
    printf("x is greater than 3\n");
}                 // Closing brace

3. Comments

C supports single-line and multi-line comments:

// This is a single-line comment (C99 and later)

/* 
 This is a multi-line comment
 It can span multiple lines
*/

4. Case Sensitivity

C is case-sensitive, meaning it distinguishes between uppercase and lowercase letters:

int myVar = 5;    // Different from myvar
int MyVar = 10;   // Different from myVar

printf("%d\n", myVar);  // Outputs: 5
printf("%d\n", MyVar);  // Outputs: 10

5. Variables and Static Typing

C is statically typed, meaning you must declare the type of a variable explicitly:

int x = 10;           // Integer
float y = 3.14;       // Floating-point number
char z = 'A';         // Character
char name[] = "C";    // String (character array)

printf("Type of x: integer\n");

Conclusion

Understanding C syntax is essential for writing correct and efficient programs. Key takeaways include:

  • C programs start execution from the main() function
  • Statements end with semicolons
  • Code blocks are defined with braces {}
  • C is case-sensitive
  • C uses static typing - variable types must be declared

Output with printf

The printf function in C is used to display formatted output on the screen. It is part of the stdio.h library and is one of the most commonly used functions for basic output and debugging.

1. Basic printf Usage

The simplest way to use printf is with a format string:

#include <stdio.h>

int main() {
    printf("Hello, World!\n");  // Outputs: Hello, World!
    printf("%d\n", 42);         // Outputs: 42
    return 0;
}

2. Format Specifiers

printf uses format specifiers to output different data types:

int age = 25;
float height = 5.9;
char grade = 'A';
char name[] = "John";

printf("Name: %s\n", name);      // %s for strings
printf("Age: %d\n", age);        // %d for integers
printf("Height: %.1f\n", height); // %f for floats, .1 for 1 decimal
printf("Grade: %c\n", grade);    // %c for characters

3. Common Format Specifiers

Specifier Description
%d Signed decimal integer
%f Floating-point number
%c Character
%s String
%p Pointer address
%x Hexadecimal integer

4. Formatting Options

double pi = 3.14159265359;

printf("Default: %f\n", pi);        // 3.141593
printf("2 decimals: %.2f\n", pi);   // 3.14
printf("10 width: %10.2f\n", pi);   // "      3.14"
printf("Left align: %-10.2f\n", pi); // "3.14      "

5. Printing Multiple Values

char product[] = "Computer";
int quantity = 5;
float price = 999.99;

printf("Product: %s, Quantity: %d, Price: $%.2f\n", 
       product, quantity, price);

Conclusion

The printf function is a fundamental tool for output in C. Key points:

  • Use format specifiers to output different data types
  • Control decimal places with .precision
  • Align output with width specifiers
  • Always include \n for new lines

Arithmetic Operators in C

C provides standard arithmetic operators for mathematical calculations. These operators work with numeric data types like integers and floating-point numbers.

Basic Arithmetic Operators

#include <stdio.h>

int main() {
    int a = 15, b = 4;
    
    printf("a + b = %d\n", a + b);  // Addition: 19
    printf("a - b = %d\n", a - b);  // Subtraction: 11
    printf("a * b = %d\n", a * b);  // Multiplication: 60
    printf("a / b = %d\n", a / b);  // Division: 3 (integer division)
    printf("a %% b = %d\n", a % b);  // Modulus: 3
    
    float x = 15.0, y = 4.0;
    printf("x / y = %.2f\n", x / y);  // Division: 3.75 (float division)
    
    return 0;
}

Increment and Decrement Operators

int count = 5;
printf("count = %d\n", count);     // 5
printf("++count = %d\n", ++count); // 6 (pre-increment)
printf("count++ = %d\n", count++); // 6 (post-increment)
printf("count = %d\n", count);     // 7
printf("--count = %d\n", --count); // 6 (pre-decrement)

Common Pitfalls

  • Integer division truncates the fractional part: 5 / 2 equals 2, not 2.5
  • Modulus operator % only works with integer types
  • Division by zero causes undefined behavior
  • Operator precedence matters: a + b * c is a + (b * c)

Comparison Operators in C

Comparison operators compare two values and return an integer result (1 for true, 0 for false). These are essential for conditional statements and loops.

Comparison Operators

#include <stdio.h>

int main() {
    int x = 7, y = 10;
    
    printf("x == y: %d\n", x == y);  // Equal to: 0 (false)
    printf("x != y: %d\n", x != y);  // Not equal to: 1 (true)
    printf("x > y: %d\n", x > y);    // Greater than: 0
    printf("x < y: %d\n", x < y);    // Less than: 1
    printf("x >= 7: %d\n", x >= 7);  // Greater than or equal: 1
    printf("y <=  7: %d\n", y <=  7);  // Less than or equal: 0
    
    return 0;
}

Using Comparisons in Conditions

int age = 18;
if (age >= 18) {
    printf("You are an adult\n");
} else {
    printf("You are a minor\n");
}

Common Pitfalls

  • Confusing assignment = with equality ==
  • Comparing floating-point numbers for exact equality can be problematic due to precision issues
  • In C, any non-zero value is considered true in boolean contexts

Logical Operators in C

Logical operators combine boolean expressions and return 1 (true) or 0 (false). C provides three logical operators: && (AND), || (OR), and ! (NOT).

Logical Operators

#include <stdio.h>

int main() {
    int isSunny = 1;  // true
    int isWarm = 0;   // false
    
    printf("isSunny && isWarm: %d\n", isSunny && isWarm);  // AND: 0
    printf("isSunny || isWarm: %d\n", isSunny || isWarm);  // OR: 1
    printf("!isWarm: %d\n", !isWarm);                      // NOT: 1
    
    // Complex conditions
    int age = 25;
    int hasLicense = 1;
    
    if (age >= 18 && hasLicense) {
        printf("You can drive\n");
    }
    
    return 0;
}

Short-Circuit Evaluation

// In AND (&&), if first condition is false, second isn't evaluated
// In OR (||), if first condition is true, second isn't evaluated

int x = 5;
if (x != 0 && 10/x > 1) {  // Safe because x != 0 is checked first
    printf("Condition satisfied\n");
}

Common Pitfalls

  • Using bitwise operators (&, |) instead of logical operators (&&, ||)
  • Forgetting that logical operators have lower precedence than comparison operators
  • Using = instead of == in logical expressions

Bitwise Operators in C

Bitwise operators perform operations on individual bits of integer types. They are used in low-level programming, embedded systems, and performance optimization.

Bitwise Operators

#include <stdio.h>

int main() {
    unsigned int a = 6;   // binary: 0110
    unsigned int b = 3;   // binary: 0011
    
    printf("a & b = %d\n", a & b);  // AND: 0010 (2)
    printf("a | b = %d\n", a | b);  // OR: 0111 (7)
    printf("a ^ b = %d\n", a ^ b);  // XOR: 0101 (5)
    printf("~a = %u\n", ~a);        // NOT: 11111111111111111111111111111001
    printf("a << 1 = %d\n", a << 1); // Left shift: 1100 (12)
    printf("b >> 1 = %d\n", b >> 1); // Right shift: 0001 (1)
    
    return 0;
}

Practical Applications

// Setting flags
#define FLAG_A (1 << 0)  // 0001
#define FLAG_B (1 << 1)  // 0010  
#define FLAG_C (1 << 2)  // 0100

unsigned int settings = 0;
settings |= FLAG_A;  // Set flag A
settings |= FLAG_C;  // Set flag C

// Check if flag B is set
if (settings & FLAG_B) {
    printf("Flag B is set\n");
} else {
    printf("Flag B is not set\n");
}

// Toggle a flag
settings ^= FLAG_A;  // Toggle flag A

Common Pitfalls

  • Confusing bitwise AND & with logical AND &&
  • Right-shifting negative numbers has implementation-defined behavior
  • Shifting beyond the bit width causes undefined behavior
  • Forgetting that bitwise NOT ~ flips all bits, not just the significant ones

Assignment Operators in C

Assignment operators assign values to variables. C provides compound assignment operators that combine assignment with arithmetic or bitwise operations.

Assignment Operators

#include <stdio.h>

int main() {
    int x = 10;        // Simple assignment
    printf("x = %d\n", x);
    
    x += 5;            // Equivalent to x = x + 5
    printf("x += 5: %d\n", x);  // 15
    
    x -= 3;            // Equivalent to x = x - 3
    printf("x -= 3: %d\n", x);  // 12
    
    x *= 2;            // Equivalent to x = x * 2
    printf("x *= 2: %d\n", x);  // 24
    
    x /= 4;            // Equivalent to x = x / 4
    printf("x /= 4: %d\n", x);  // 6
    
    x %= 4;            // Equivalent to x = x % 4
    printf("x %%= 4: %d\n", x);  // 2
    
    // Bitwise assignment operators
    unsigned int y = 5;
    y &= 3;            // AND assignment: y = y & 3
    y |= 8;            // OR assignment: y = y | 8
    y ^= 4;            // XOR assignment: y = y ^ 4
    
    return 0;
}

Multiple Assignment

int a, b, c;
a = b = c = 10;  // All variables get value 10

// Compound assignment in loops
int sum = 0;
for (int i = 1; i <=  5; i++) {
    sum += i;  // Add i to sum
}
printf("Sum: %d\n", sum);  // 15

Common Pitfalls

  • Confusing = (assignment) with == (equality comparison)
  • Assignment has right-to-left associativity: a = b = c means a = (b = c)
  • Forgetting that assignment returns the assigned value

Integer Data Types in C

C provides several integer types with different sizes and ranges. Choosing the right integer type depends on the required range and memory constraints.

Basic Integer Types

#include <stdio.h>
#include <limits.h>

int main() {
    // Signed integers (can represent negative numbers)
    short s = 100;           // Typically 2 bytes
    int i = 100000;          // Typically 4 bytes
    long l = 100000L;        // Typically 4 or 8 bytes
    long long ll = 100000LL; // Typically 8 bytes
    
    // Unsigned integers (only non-negative numbers)
    unsigned short us = 100;
    unsigned int ui = 100000;
    unsigned long ul = 100000UL;
    unsigned long long ull = 100000ULL;
    
    printf("short range: %d to %d\n", SHRT_MIN, SHRT_MAX);
    printf("int range: %d to %d\n", INT_MIN, INT_MAX);
    printf("unsigned int range: 0 to %u\n", UINT_MAX);
    
    return 0;
}

Integer Operations

int a = 10, b = 3;
printf("a + b = %d\n", a + b);  // 13
printf("a - b = %d\n", a - b);  // 7
printf("a * b = %d\n", a * b);  // 30
printf("a / b = %d\n", a / b);  // 3 (integer division)
printf("a %% b = %d\n", a % b);  // 1 (modulus)

Type Conversion

// Implicit conversion
int num = 3.14;  // num becomes 3 (truncation)

// Explicit conversion (C-style)
int x = (int)3.14;

// String to integer
#include <stdlib.h>
char str[] = "123";
int z = atoi(str);

Common Pitfalls

  • Integer overflow when result exceeds type's maximum value
  • Integer division truncates fractional parts
  • Mixing signed and unsigned types can cause unexpected behavior
  • Size of integer types can vary between platforms

Floating-Point Data Types in C

Floating-point types represent real numbers with fractional parts. C provides three floating-point types with different precision levels.

Floating-Point Types

#include <stdio.h>
#include <math.h>

int main() {
    float f = 3.14159f;           // Single precision (typically 4 bytes)
    double d = 3.14159265358979;  // Double precision (typically 8 bytes)
    long double ld = 3.141592653589793238L; // Extended precision
    
    printf("float: %.10f\n", f);
    printf("double: %.10f\n", d);
    printf("long double: %.10Lf\n", ld);
    
    // Special values
    printf("sqrt(-1): %f\n", sqrt(-1));  // NaN (Not a Number)
    printf("1.0/0.0: %f\n", 1.0/0.0);    // inf (Infinity)
    printf("-1.0/0.0: %f\n", -1.0/0.0);  // -inf
    
    return 0;
}

Floating-Point Operations

double a = 2.5, b = 1.5;
printf("a + b = %.2f\n", a + b);  // 4.0
printf("a - b = %.2f\n", a - b);  // 1.0
printf("a * b = %.2f\n", a * b);  // 3.75
printf("a / b = %.2f\n", a / b);  // 1.67

// Mathematical functions
printf("sqrt(a) = %.2f\n", sqrt(a));
printf("pow(a, b) = %.2f\n", pow(a, b));
printf("sin(pi/2) = %.2f\n", sin(M_PI/2));

Type Conversion

// String to floating-point
#include <stdlib.h>
char str[] = "3.14159";
double pi = atof(str);

// Floating-point to integer (truncation)
double d = 3.99;
int i = (int)d;  // i becomes 3

// Integer to floating-point
int x = 5;
double y = (double)x;  // y becomes 5.0

Common Pitfalls

  • Floating-point numbers have limited precision and rounding errors
  • Comparing floating-point numbers for exact equality is often problematic
  • Operations with very large or very small numbers can cause overflow/underflow
  • Floating-point arithmetic is generally slower than integer arithmetic

Strings in C

In C, strings are arrays of characters terminated by a null character (\0). C provides many functions in the string.h library for working with strings.

Creating and Using Strings

#include <stdio.h>
#include <string.h>

int main() {
    // Different ways to create strings
    char s1[] = "Hello";
    char s2[6] = {'W', 'o', 'r', 'l', 'd', '\0'};
    char s3[10];
    
    strcpy(s3, "C Programming");  // Copy string
    
    printf("s1: %s\n", s1);
    printf("s2: %s\n", s2);
    printf("s3: %s\n", s3);
    
    // Accessing characters
    printf("First character: %c\n", s1[0]);  // 'H'
    printf("Second character: %c\n", s1[1]);  // 'e'
    
    // String length
    printf("Length of s1: %zu\n", strlen(s1));
    
    return 0;
}

String Comparison and Searching

char str1[] = "apple", str2[] = "banana";
    
// Comparison
if (strcmp(str1, str2) == 0) {
    printf("Strings are equal\n");
} else if (strcmp(str1, str2) < 0) {
    printf("%s comes before %s\n", str1, str2);
} else {
    printf("%s comes after %s\n", str1, str2);
}

// Searching
char text[] = "Hello World";
char *pos = strstr(text, "World");
if (pos != NULL) {
    printf("Found 'World' at position %ld\n", pos - text);
}

String Modification

char str[20] = "Hello";
    
// Concatenation
strcat(str, " World");
printf("After concatenation: %s\n", str);
    
// String copying with length limit
char dest[10];
strncpy(dest, "This is a long string", sizeof(dest) - 1);
dest[sizeof(dest) - 1] = '\0';  // Ensure null termination
printf("Safe copy: %s\n", dest);

Common Pitfalls

  • Forgetting the null terminator \0
  • Buffer overflow when copying strings without bounds checking
  • Not allocating enough space for string operations
  • Modifying string literals (which are read-only)

String Functions in C

The C standard library provides many useful functions for string manipulation in the string.h header, including copying, concatenation, comparison, and searching.

Common String Functions

#include <stdio.h>
#include <string.h>

int main() {
    char str1[20] = "Hello";
    char str2[20] = "World";
    char result[50];
    
    // String length
    printf("Length of str1: %zu\n", strlen(str1));
    
    // String copy
    strcpy(result, str1);
    printf("After strcpy: %s\n", result);
    
    // String concatenation
    strcat(result, " ");
    strcat(result, str2);
    printf("After strcat: %s\n", result);
    
    // String comparison
    int cmp = strcmp(str1, str2);
    if (cmp == 0) {
        printf("Strings are equal\n");
    } else if (cmp < 0) {
        printf("str1 comes before str2\n");
    } else {
        printf("str1 comes after str2\n");
    }
    
    // String search
    char text[] = "The quick brown fox";
    char *found = strstr(text, "brown");
    if (found) {
        printf("Found 'brown' at position %ld\n", found - text);
    }
    
    return 0;
}

Safe String Functions

// Use strncpy, strncat for bounds checking
char dest[10];
char src[] = "This is too long";
    
// Safe copy with length limit
strncpy(dest, src, sizeof(dest) - 1);
dest[sizeof(dest) - 1] = '\0';  // Ensure null termination
printf("Safe copy: %s\n", dest);
    
// Safe concatenation
char buffer[20] = "Hello";
strncat(buffer, " World!!!", sizeof(buffer) - strlen(buffer) - 1);
printf("Safe concatenation: %s\n", buffer);

Character Classification

#include <ctype.h>

char str[] = "Hello123";
int i = 0;
    
while (str[i]) {
    if (isalpha(str[i])) {
        printf("%c is a letter\n", str[i]);
    } else if (isdigit(str[i])) {
        printf("%c is a digit\n", str[i]);
    } else if (isspace(str[i])) {
        printf("%c is whitespace\n", str[i]);
    }
    i++;
}

// Character conversion
char lower[] = "HELLO";
i = 0;
while (lower[i]) {
    lower[i] = tolower(lower[i]);
    i++;
}
printf("Lowercase: %s\n", lower);

Common Pitfalls

  • Forgetting to check return values of string functions
  • Not ensuring null termination after using strncpy
  • Buffer overflow from unsafe string operations
  • Assuming string functions work with uninitialized memory

String Formatting in C

C provides powerful string formatting capabilities through functions like printf, sprintf, and snprintf. These functions use format specifiers to control how data is formatted.

Using sprintf and snprintf

#include <stdio.h>

int main() {
    char name[] = "Alice";
    int age = 25;
    double score = 95.5;
    char buffer[100];
    
    // Using sprintf (be careful of buffer overflow)
    sprintf(buffer, "Name: %s, Age: %d, Score: %.1f", name, age, score);
    printf("%s\n", buffer);
    
    // Using snprintf (safer - specifies buffer size)
    snprintf(buffer, sizeof(buffer), 
             "Name: %s, Age: %d, Score: %.1f", name, age, score);
    printf("%s\n", buffer);
    
    return 0;
}

Common Format Specifiers

Specifier Description Example
%d Signed decimal integer printf("%d", 42) → "42"
%f Floating-point number printf("%.2f", 3.14159) → "3.14"
%c Character printf("%c", 'A') → "A"
%s String printf("%s", "hello") → "hello"
%x Hexadecimal integer printf("%x", 255) → "ff"
%p Pointer address printf("%p", &var)

Formatting Options

int number = 123;
double value = 3.14159265359;
    
printf("Default integer: %d\n", number);
printf("8 width: %8d\n", number);        // "     123"
printf("8 width left: %-8d\n", number);  // "123     "
printf("With zeros: %08d\n", number);    // "00000123"
    
printf("Default float: %f\n", value);
printf("2 decimals: %.2f\n", value);     // "3.14"
printf("10 width, 3 decimals: %10.3f\n", value); // "     3.142"

Reading Formatted Input

#include <stdio.h>

int main() {
    char name[50];
    int age;
    double salary;
    
    printf("Enter name, age, and salary: ");
    scanf("%s %d %lf", name, &age, &salary);
    
    printf("Name: %s, Age: %d, Salary: $%.2f\n", name, age, salary);
    
    // Reading line with spaces
    char line[100];
    printf("Enter a line: ");
    getchar();  // Consume newline from previous input
    fgets(line, sizeof(line), stdin);
    printf("You entered: %s", line);
    
    return 0;
}

Common Pitfalls

  • Buffer overflow with sprintf - always use snprintf
  • Format specifiers not matching variable types
  • Forgetting & with variables in scanf
  • Not checking return value of scanf

Arrays in C

Arrays are collections of elements of the same type stored in contiguous memory. C supports both fixed-size arrays and dynamic arrays using pointers.

Fixed-Size Arrays

#include <stdio.h>

int main() {
    // Declaration and initialization
    int numbers[5] = {1, 2, 3, 4, 5};
    double prices[] = {1.99, 2.99, 3.49};  // Size inferred
    
    // Accessing elements
    printf("First element: %d\n", numbers[0]);
    printf("Last element: %d\n", numbers[4]);
    
    // Modifying elements
    numbers[2] = 100;
    
    // Array size
    int size = sizeof(numbers) / sizeof(numbers[0]);
    printf("Array size: %d\n", size);
    
    // Iterating through array
    for (int i = 0; i < size; i++) {
        printf("numbers[%d] = %d\n", i, numbers[i]);
    }
    
    return 0;
}

Multi-dimensional Arrays

// 2D array (matrix)
int matrix[3][3] = {
    {1, 2, 3},
    {4, 5, 6},
    {7, 8, 9}
};

// Accessing elements
printf("Element at [1][2]: %d\n", matrix[1][2]);  // 6

// Nested loops for 2D array
for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 3; j++) {
        printf("%d ", matrix[i][j]);
    }
    printf("\n");
}

Array Limitations

// Arrays have fixed size
// Cannot return arrays from functions directly
// Arrays decay to pointers when passed to functions

int arr[5] = {1, 2, 3, 4, 5};
// arr[10] = 100;  // Undefined behavior - no bounds checking!

Common Pitfalls

  • Array indices start at 0, not 1
  • No bounds checking - accessing out-of-bounds causes undefined behavior
  • Arrays decay to pointers when passed to functions
  • Cannot return arrays from functions directly

Array Indexing in C

Array indexing allows access to individual elements in an array using their position. C uses zero-based indexing, meaning the first element is at index 0.

Basic Array Indexing

#include <stdio.h>

int main() {
    int numbers[5] = {10, 20, 30, 40, 50};
    
    // Accessing elements by index
    printf("numbers[0] = %d\n", numbers[0]);  // First element: 10
    printf("numbers[2] = %d\n", numbers[2]);  // Third element: 30
    printf("numbers[4] = %d\n", numbers[4]);  // Last element: 50
    
    // Modifying elements
    numbers[1] = 25;  // Change second element from 20 to 25
    numbers[3] = 45;  // Change fourth element from 40 to 45
    
    // Display modified array
    for (int i = 0; i < 5; i++) {
        printf("numbers[%d] = %d\n", i, numbers[i]);
    }
    
    return 0;
}

Multi-dimensional Array Indexing

// 2D array indexing
int matrix[2][3] = {
    {1, 2, 3},
    {4, 5, 6}
};

printf("matrix[0][1] = %d\n", matrix[0][1]);  // 2
printf("matrix[1][2] = %d\n", matrix[1][2]);  // 6

// 3D array indexing
int cube[2][2][2] = {
    {{1, 2}, {3, 4}},
    {{5, 6}, {7, 8}}
};
printf("cube[1][0][1] = %d\n", cube[1][0][1]);  // 6

Character Array Indexing

char word[] = "Hello";
printf("word[0] = %c\n", word[0]);  // 'H'
printf("word[4] = %c\n", word[4]);  // 'o'

// Strings are null-terminated
printf("word[5] = %d\n", word[5]);  // 0 (null character)

Common Pitfalls

  • Accessing out-of-bounds indices causes undefined behavior
  • Array indices start at 0, not 1
  • Negative indices are not allowed in standard arrays
  • No bounds checking by default

Array Functions in C

C provides several ways to work with arrays, including standard library functions and custom functions that operate on arrays.

Passing Arrays to Functions

#include <stdio.h>

// Function that takes an array and its size
void printArray(int arr[], int size) {
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

// Function that modifies array elements
void doubleArray(int arr[], int size) {
    for (int i = 0; i < size; i++) {
        arr[i] *= 2;
    }
}

int main() {
    int numbers[] = {1, 2, 3, 4, 5};
    int size = sizeof(numbers) / sizeof(numbers[0]);
    
    printf("Original array: ");
    printArray(numbers, size);
    
    doubleArray(numbers, size);
    
    printf("Doubled array: ");
    printArray(numbers, size);
    
    return 0;
}

Array Manipulation Functions

#include <stdio.h>
#include <string.h>

// Working with character arrays
void stringOperations() {
    char str1[20] = "Hello";
    char str2[20] = "World";
    
    // Copy strings
    strcpy(str1, "Hello");  // Copy "Hello" to str1
    strcat(str1, " ");      // Append space
    strcat(str1, str2);     // Append "World"
    printf("Concatenated: %s\n", str1);
    
    // Compare strings
    if (strcmp(str1, str2) == 0) {
        printf("Strings are equal\n");
    } else {
        printf("Strings are different\n");
    }
    
    // Get string length
    printf("Length: %zu\n", strlen(str1));
}

// Array sorting (bubble sort)
void bubbleSort(int arr[], int n) {
    for (int i = 0; i < n-1; i++) {
        for (int j = 0; j < n-i-1; j++) {
            if (arr[j] > arr[j+1]) {
                // Swap elements
                int temp = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = temp;
            }
        }
    }
}

Array Algorithms

// Find maximum element
int findMax(int arr[], int n) {
    int max = arr[0];
    for (int i = 1; i < n; i++) {
        if (arr[i] > max) {
            max = arr[i];
        }
    }
    return max;
}

// Calculate sum of elements
int arraySum(int arr[], int n) {
    int sum = 0;
    for (int i = 0; i < n; i++) {
        sum += arr[i];
    }
    return sum;
}

// Search for element
int linearSearch(int arr[], int n, int target) {
    for (int i = 0; i < n; i++) {
        if (arr[i] == target) {
            return i;  // Return index if found
        }
    }
    return -1;  // Return -1 if not found
}

Common Pitfalls

  • C-style arrays don't know their own size - you must track it separately
  • Arrays decay to pointers when passed to functions
  • C-string functions don't check buffer sizes, leading to potential overflows
  • Always ensure arrays are large enough for operations

Pointers in C

Pointers are variables that store memory addresses. They provide direct access to memory and are essential for dynamic memory allocation and efficient data structures.

Basic Pointer Operations

#include <stdio.h>

int main() {
    int number = 42;
    int* ptr = &number;  // ptr stores address of number
    
    printf("Value of number: %d\n", number);
    printf("Address of number: %p\n", &number);
    printf("Value of ptr: %p\n", ptr);
    printf("Value at ptr: %d\n", *ptr);  // Dereferencing
    
    // Modifying through pointer
    *ptr = 100;
    printf("New value of number: %d\n", number);
    
    // Pointer to different types
    double pi = 3.14159;
    double* dptr = π
    printf("Value at dptr: %.2f\n", *dptr);
    
    return 0;
}

Pointers and Arrays

int arr[] = {10, 20, 30, 40, 50};
int* ptr = arr;  // Array name decays to pointer to first element

printf("First element: %d\n", *ptr);        // 10
printf("Second element: %d\n", *(ptr + 1)); // 20
printf("Third element: %d\n", ptr[2]);      // 30 (array syntax works)

// Pointer arithmetic
ptr++;  // Move to next element
printf("After increment: %d\n", *ptr);  // 20

ptr += 2;  // Move two elements forward
printf("After +2: %d\n", *ptr);        // 40

Null Pointers and Safety

// Different ways to create null pointers
int* ptr1 = NULL;  // Preferred way
int* ptr2 = 0;

// Always check pointers before use
if (ptr1 != NULL) {
    printf("%d\n", *ptr1);
} else {
    printf("Pointer is null\n");
}

// Dangerous: dereferencing null pointer
// *ptr1 = 5;  // CRASH! Undefined behavior

Common Pitfalls

  • Dereferencing null or uninitialized pointers causes crashes
  • Pointer arithmetic can easily go out of bounds
  • Memory leaks if dynamically allocated memory isn't freed
  • Dangling pointers after memory is freed

Pointer Arithmetic in C

Pointer arithmetic allows you to navigate through memory by adding or subtracting from pointers. This is particularly useful with arrays and dynamic memory.

Basic Pointer Arithmetic

#include <stdio.h>

int main() {
    int arr[] = {10, 20, 30, 40, 50};
    int* ptr = arr;
    
    printf("Pointer at: %p\n", ptr);
    printf("Value: %d\n", *ptr);  // 10
    
    ptr++;  // Move to next int (4 bytes forward typically)
    printf("After ++: %p\n", ptr);
    printf("Value: %d\n", *ptr);  // 20
    
    ptr += 2;  // Move two ints forward
    printf("After += 2: %p\n", ptr);
    printf("Value: %d\n", *ptr);  // 40
    
    ptr--;  // Move one int backward
    printf("After --: %p\n", ptr);
    printf("Value: %d\n", *ptr);  // 30
    
    return 0;
}

Pointer Differences and Comparisons

int numbers[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int* start = numbers;
int* end = numbers + 10;  // Pointer one past the end

// Calculate number of elements between pointers
int distance = end - start;
printf("Distance: %d elements\n", distance);

// Pointer comparisons
int* middle = numbers + 5;
if (start < middle) {
    printf("start comes before middle\n");
}

if (middle < end) {
    printf("middle comes before end\n");
}

// Iterate using pointers
for (int* p = start; p < end; p++) {
    printf("%d ", *p);
}
printf("\n");

Pointer Arithmetic with Different Types

// Different types have different sizes
char chars[] = {'a', 'b', 'c', 'd', 'e'};
double doubles[] = {1.1, 2.2, 3.3, 4.4, 5.5};

char* cptr = chars;
double* dptr = doubles;

printf("char pointer: %p\n", cptr);
cptr++;
printf("after ++: %p\n", cptr);  // Moves 1 byte

printf("double pointer: %p\n", dptr);
dptr++;
printf("after ++: %p\n", dptr);  // Moves 8 bytes (typically)

Practical Applications

// Finding array length
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))

int data[] = {1, 2, 3, 4, 5};
printf("Array size: %zu\n", ARRAY_SIZE(data));

// String length using pointers
const char* str = "Hello";
const char* p = str;
while (*p != '\0') {
    p++;
}
printf("String length: %ld\n", (p - str));

Common Pitfalls

  • Pointer arithmetic only valid within same array or one past the end
  • Adding two pointers is not allowed
  • Pointer arithmetic on void pointers is not allowed (unknown size)
  • Going beyond array bounds causes undefined behavior

Structures in C

Structures (structs) allow you to group related variables of different types together. They are used to create custom data types that can represent real-world entities.

Basic Structure Definition

#include <stdio.h>
#include <string.h>

// Define a structure
struct Person {
    char name[50];
    int age;
    float height;
};

int main() {
    // Create structure variables
    struct Person person1;
    
    // Initialize structure members
    strcpy(person1.name, "Alice");
    person1.age = 25;
    person1.height = 5.6;
    
    // Access structure members
    printf("Name: %s\n", person1.name);
    printf("Age: %d\n", person1.age);
    printf("Height: %.1f\n", person1.height);
    
    // Initialize during declaration
    struct Person person2 = {"Bob", 30, 5.9};
    
    return 0;
}

Structures with Pointers

struct Point {
    int x;
    int y;
};

void printPoint(struct Point* p) {
    printf("Point: (%d, %d)\n", p->x, p->y);
}

int main() {
    struct Point p1 = {10, 20};
    struct Point* ptr = &p1;
    
    // Access members through pointer
    printf("x: %d\n", ptr->x);  // Arrow operator
    printf("y: %d\n", ptr->y);
    
    printPoint(&p1);
    
    return 0;
}

Nested Structures

struct Date {
    int day;
    int month;
    int year;
};

struct Employee {
    char name[50];
    int id;
    struct Date hireDate;
    float salary;
};

int main() {
    struct Employee emp = {
        "John Doe", 
        1001, 
        {15, 3, 2020}, 
        75000.50
    };
    
    printf("Employee: %s\n", emp.name);
    printf("Hire date: %d/%d/%d\n", 
           emp.hireDate.day, emp.hireDate.month, emp.hireDate.year);
    
    return 0;
}

Typedef with Structures

// Use typedef to create an alias
typedef struct {
    char title[100];
    char author[50];
    int year;
    float price;
} Book;

int main() {
    // Now we can use Book instead of struct Book
    Book book1 = {"The C Programming Language", "K&R", 1978, 49.99};
    
    printf("Title: %s\n", book1.title);
    printf("Author: %s\n", book1.author);
    printf("Year: %d\n", book1.year);
    
    return 0;
}

Common Pitfalls

  • Forgetting the struct keyword when declaring variables
  • Not using typedef for cleaner syntax
  • Comparing structures directly (must compare member by member)
  • Copying structures with pointers can lead to shallow copies

Unions in C

Unions are similar to structures but all members share the same memory location. Only one member can contain a value at any given time.

Basic Union Usage

#include <stdio.h>

union Data {
    int i;
    float f;
    char str[20];
};

int main() {
    union Data data;
    
    data.i = 10;
    printf("data.i: %d\n", data.i);
    
    data.f = 3.14;
    printf("data.f: %.2f\n", data.f);
    
    // Now data.i is overwritten!
    printf("data.i after setting f: %d\n", data.i);  // Undefined behavior
    
    return 0;
}

Practical Use Cases

// Union for representing different data types
typedef union {
    int intValue;
    float floatValue;
    char stringValue[20];
} Variant;

typedef struct {
    int type;  // 0 = int, 1 = float, 2 = string
    Variant value;
} TaggedUnion;

void printVariant(TaggedUnion* var) {
    switch (var->type) {
        case 0:
            printf("Integer: %d\n", var->value.intValue);
            break;
        case 1:
            printf("Float: %.2f\n", var->value.floatValue);
            break;
        case 2:
            printf("String: %s\n", var->value.stringValue);
            break;
    }
}

int main() {
    TaggedUnion var1 = {0, .value.intValue = 42};
    TaggedUnion var2 = {1, .value.floatValue = 3.14};
    TaggedUnion var3 = {2, .value.stringValue = "Hello"};
    
    printVariant(&var1);
    printVariant(&var2);
    printVariant(&var3);
    
    return 0;
}

Union vs Structure

struct S {
    int a;
    float b;
    char c;
};  // Size = sizeof(int) + sizeof(float) + sizeof(char) + padding

union U {
    int a;
    float b;
    char c;
};  // Size = size of largest member

printf("Size of struct: %zu\n", sizeof(struct S));
printf("Size of union: %zu\n", sizeof(union U));

Common Pitfalls

  • Accessing the wrong member of a union leads to undefined behavior
  • Forgetting which member was last set
  • Unions don't track which member contains valid data
  • Portability issues with different byte orders (endianness)

Variables in C

Variables are named storage locations in memory that hold data. C is a statically-typed language, meaning variable types must be declared explicitly.

Variable Declaration and Initialization

#include <stdio.h>

int main() {
    // Declaration and initialization
    int age = 25;
    double price = 19.99;
    char grade = 'A';
    char name[] = "Alice";
    
    // Multiple variables
    int x = 5, y = 10, z = 15;
    
    // Different initialization styles
    int a = 10;     // Copy initialization
    int b = 20;     // Another initialization
    
    printf("a: %d, b: %d\n", a, b);
    
    return 0;
}

Variable Scope

int globalVar = 100;  // Global variable

void demoFunction() {
    int localVar = 50;  // Local variable
    printf("Local: %d, Global: %d\n", localVar, globalVar);
    
    {
        int blockVar = 25;  // Block scope
        printf("Block: %d\n", blockVar);
    }
    // printf("%d\n", blockVar);  // Error: blockVar not accessible here
}

int main() {
    demoFunction();
    // printf("%d\n", localVar);  // Error: localVar not accessible here
    return 0;
}

Constants and Type Modifiers

// Constants
const double PI = 3.14159;
const int MAX_SIZE = 100;
// PI = 3.14;  // Error: const variable cannot be modified

// Type modifiers
unsigned int positiveOnly = 4000000000;
short smallNumber = 32767;
long bigNumber = 2147483647;
long long veryBigNumber = 9223372036854775807LL;

// Volatile (tells compiler variable may change unexpectedly)
volatile int hardwareRegister = 0;

// Register hint (suggests using CPU register)
register int counter = 0;

Variable Lifetime and Storage Classes

// Automatic (default) - created when block entered, destroyed when exited
void automaticDemo() {
    int x = 10;  // Automatic storage
}

// Static - persists for program lifetime
void staticDemo() {
    static int count = 0;  // Initialized only once
    count++;
    printf("Count: %d\n", count);
}

// External - accessible across files
extern int externalVar;  // Defined in another file

int main() {
    automaticDemo();
    staticDemo();  // Count: 1
    staticDemo();  // Count: 2
    staticDemo();  // Count: 3
    return 0;
}

Common Pitfalls

  • Uninitialized variables contain garbage values
  • Using variables outside their scope
  • Name shadowing (local variables hiding global ones)
  • Forgetting that char can be signed or unsigned depending on implementation

Conditional Statements: if, else if, else

Conditional statements allow your program to make decisions and execute different code blocks based on conditions.

Basic if Statement

#include <stdio.h>

int main() {
    int number;
    printf("Enter a number: ");
    scanf("%d", &number);
    
    // Simple if statement
    if (number > 0) {
        printf("The number is positive\n");
    }
    
    // if-else statement
    if (number % 2 == 0) {
        printf("The number is even\n");
    } else {
        printf("The number is odd\n");
    }
    
    return 0;
}

if-else if-else Chain

int score;
printf("Enter your score (0-100): ");
scanf("%d", &score);

if (score >= 90) {
    printf("Grade: A\n");
} else if (score >= 80) {
    printf("Grade: B\n");
} else if (score >= 70) {
    printf("Grade: C\n");
} else if (score >= 60) {
    printf("Grade: D\n");
} else {
    printf("Grade: F\n");
}

Nested if Statements

int age;
int hasLicense;
    
printf("Enter your age: ");
scanf("%d", &age);
printf("Do you have a license? (1 for yes, 0 for no): ");
scanf("%d", &hasLicense);
    
if (age >= 18) {
    if (hasLicense) {
        printf("You can drive legally\n");
    } else {
        printf("You need to get a license first\n");
    }
} else {
    printf("You are too young to drive\n");
}

Conditional Operator (Ternary)

int a = 10, b = 20;
    
// Traditional if-else
int max;
if (a > b) {
    max = a;
} else {
    max = b;
}
    
// Using ternary operator
int max2 = (a > b) ? a : b;
    
printf("Maximum: %d\n", max2);
    
// Nested ternary (use sparingly!)
int x = 5, y = 10, z = 15;
int largest = (x > y) ? ((x > z) ? x : z) : ((y > z) ? y : z);
printf("Largest: %d\n", largest);

Common Pitfalls

  • Using assignment = instead of equality == in conditions
  • Forgetting braces {} for multi-statement blocks
  • Dangling else problem (else matches with nearest if)
  • Overusing nested ternary operators reduces readability

for Loop in C

The for loop is used when you know exactly how many times you want to iterate. It has three parts: initialization, condition, and increment.

Basic for Loop

#include <stdio.h>

int main() {
    // Count from 1 to 5
    for (int i = 1; i <=  5; i++) {
        printf("Count: %d\n", i);
    }
    
    // Countdown from 10 to 1
    for (int i = 10; i >= 1; i--) {
        printf("%d ", i);
    }
    printf("\n");
    
    // Iterate through array
    int numbers[] = {2, 4, 6, 8, 10};
    for (int i = 0; i < 5; i++) {
        printf("%d ", numbers[i]);
    }
    printf("\n");
    
    return 0;
}

Advanced for Loop Variations

// Multiple variables in initialization
for (int i = 0, j = 10; i < j; i++, j--) {
    printf("i=%d, j=%d\n", i, j);
}

// Omitting parts (infinite loop with break)
int count = 0;
for (;;) {
    printf("%d ", count);
    count++;
    if (count >= 5) break;
}
printf("\n");

// Complex condition
for (int i = 0; i < 100; i += 5) {
    printf("%d ", i);
}
printf("\n");

Nested for Loops

// Multiplication table
for (int i = 1; i <=  5; i++) {
    for (int j = 1; j <=  5; j++) {
        printf("%d\t", i * j);
    }
    printf("\n");
}

// Pattern printing
for (int i = 1; i <=  5; i++) {
    for (int j = 1; j <=  i; j++) {
        printf("*");
    }
    printf("\n");
}

Common Pitfalls

  • Off-by-one errors (using <= instead of <)
  • Modifying loop counter inside the loop body
  • Infinite loops when condition never becomes false
  • Variable scope issues with loop counter declaration

while Loop in C

The while loop repeats a block of code as long as a condition is true. It's useful when you don't know in advance how many iterations are needed.

Basic while Loop

#include <stdio.h>

int main() {
    // Count from 1 to 5
    int i = 1;
    while (i <=  5) {
        printf("Count: %d\n", i);
        i++;
    }
    
    // User input validation
    int number;
    printf("Enter a positive number: ");
    scanf("%d", &number);
    
    while (number <=  0) {
        printf("Invalid input. Enter a positive number: ");
        scanf("%d", &number);
    }
    printf("Thank you! You entered: %d\n", number);
    
    return 0;
}

do-while Loop

// do-while executes at least once
char choice;
do {
    printf("Menu:\n");
    printf("1. Option One\n");
    printf("2. Option Two\n");
    printf("q. Quit\n");
    printf("Enter your choice: ");
    scanf(" %c", &choice);
    
    switch (choice) {
        case '1': printf("Option One selected\n"); break;
        case '2': printf("Option Two selected\n"); break;
        case 'q': printf("Goodbye!\n"); break;
        default: printf("Invalid choice\n");
    }
} while (choice != 'q');

Practical while Loop Examples

// Reading until sentinel value
int value, sum = 0;
printf("Enter numbers (0 to stop): ");
scanf("%d", &value);

while (value != 0) {
    sum += value;
    scanf("%d", &value);
}
printf("Sum: %d\n", sum);

// File reading
#include <stdio.h>
FILE* file = fopen("data.txt", "r");
char line[100];

while (fgets(line, sizeof(line), file) != NULL) {
    printf("%s", line);
}
fclose(file);

// Infinite loop with break
while (1) {
    int num;
    printf("Enter a number (negative to quit): ");
    scanf("%d", &num);
    
    if (num < 0) break;
    printf("Square: %d\n", num * num);
}

Common Pitfalls

  • Forgetting to update the loop control variable
  • Infinite loops when condition never becomes false
  • Using assignment = instead of comparison == in condition
  • Off-by-one errors in loop conditions

Loop Control Statements

C provides three loop control statements: break, continue, and goto (use sparingly). These statements alter the normal flow of loops.

break Statement

#include <stdio.h>

int main() {
    // break exits the loop immediately
    for (int i = 1; i <=  10; i++) {
        if (i == 5) {
            break;  // Exit loop when i reaches 5
        }
        printf("%d ", i);
    }
    printf("\n");  // Output: 1 2 3 4
    
    // Search example
    int numbers[] = {2, 4, 6, 8, 10, 12, 14};
    int target = 8;
    
    for (int i = 0; i < 7; i++) {
        if (numbers[i] == target) {
            printf("Found %d at position %d\n", target, i);
            break;  // Stop searching once found
        }
    }
    
    return 0;
}

continue Statement

// continue skips the rest of current iteration
for (int i = 1; i <=  10; i++) {
    if (i % 2 == 0) {
        continue;  // Skip even numbers
    }
    printf("%d ", i);  // Only odd numbers printed
}
printf("\n");  // Output: 1 3 5 7 9

// Input validation with continue
int sum = 0;
for (int i = 0; i < 5; i++) {
    int num;
    printf("Enter positive number %d: ", i + 1);
    scanf("%d", &num);
    
    if (num <=  0) {
        printf("Invalid number, skipping...\n");
        i--;  // Don't count this iteration
        continue;
    }
    sum += num;
}
printf("Sum of positive numbers: %d\n", sum);

Nested Loops with Control Statements

// break only affects innermost loop
for (int i = 1; i <=  3; i++) {
    for (int j = 1; j <=  3; j++) {
        if (i * j > 4) {
            printf("Breaking inner loop\n");
            break;  // Only breaks inner loop
        }
        printf("%d * %d = %d\n", i, j, i * j);
    }
}

// Using labels with break (rarely needed)
outer: for (int i = 1; i <=  3; i++) {
    for (int j = 1; j <=  3; j++) {
        if (i * j > 4) {
            printf("Breaking both loops\n");
            break outer;  // Break both loops
        }
        printf("%d * %d = %d\n", i, j, i * j);
    }
}

goto Statement (Use With Caution)

// goto can make code hard to read - use sparingly!
int attempts = 0;

retry:
if (attempts >= 3) {
    printf("Too many attempts!\n");
    return 1;
}

int number;
printf("Enter a positive number: ");
scanf("%d", &number);

if (number <=  0) {
    printf("Invalid input. ");
    attempts++;
    goto retry;
}

printf("Valid number: %d\n", number);

Common Pitfalls

  • Using break when continue is more appropriate
  • Overusing goto creates "spaghetti code"
  • break only exits the innermost loop
  • Placing code after continue that will never execute

Nested Loops in C

Nested loops are loops within loops. They are essential for working with multi-dimensional data, matrices, and complex patterns.

Basic Nested Loops

#include <stdio.h>

int main() {
    // Simple nested loop
    for (int i = 1; i <=  3; i++) {
        for (int j = 1; j <=  3; j++) {
            printf("(%d,%d) ", i, j);
        }
        printf("\n");
    }
    /* Output:
    (1,1) (1,2) (1,3) 
    (2,1) (2,2) (2,3) 
    (3,1) (3,2) (3,3) */
    
    // Multiplication table
    printf("\nMultiplication Table:\n");
    for (int i = 1; i <=  5; i++) {
        for (int j = 1; j <=  5; j++) {
            printf("%d\t", i * j);
        }
        printf("\n");
    }
    
    return 0;
}

Pattern Printing with Nested Loops

// Right triangle
int rows = 5;
for (int i = 1; i <=  rows; i++) {
    for (int j = 1; j <=  i; j++) {
        printf("*");
    }
    printf("\n");
}

// Inverted triangle
for (int i = rows; i >= 1; i--) {
    for (int j = 1; j <=  i; j++) {
        printf("*");
    }
    printf("\n");
}

// Pyramid
for (int i = 1; i <=  rows; i++) {
    // Print spaces
    for (int j = 1; j <=  rows - i; j++) {
        printf(" ");
    }
    // Print stars
    for (int j = 1; j <=  2 * i - 1; j++) {
        printf("*");
    }
    printf("\n");
}

Working with 2D Arrays

// Matrix operations
#define ROWS 3
#define COLS 3

int matrix[ROWS][COLS] = {
    {1, 2, 3},
    {4, 5, 6},
    {7, 8, 9}
};

// Print matrix
printf("Matrix:\n");
for (int i = 0; i < ROWS; i++) {
    for (int j = 0; j < COLS; j++) {
        printf("%d ", matrix[i][j]);
    }
    printf("\n");
}

// Sum of all elements
int sum = 0;
for (int i = 0; i < ROWS; i++) {
    for (int j = 0; j < COLS; j++) {
        sum += matrix[i][j];
    }
}
printf("Sum of all elements: %d\n", sum);

// Transpose matrix
printf("Transpose:\n");
for (int i = 0; i < COLS; i++) {
    for (int j = 0; j < ROWS; j++) {
        printf("%d ", matrix[j][i]);
    }
    printf("\n");
}

Mixed Loop Types

// while inside for
for (int i = 1; i <=  3; i++) {
    int j = 1;
    while (j <=  3) {
        printf("i=%d, j=%d\n", i, j);
        j++;
    }
}

// do-while inside for
for (int i = 1; i <=  2; i++) {
    int j = 1;
    do {
        printf("i=%d, j=%d\n", i, j);
        j++;
    } while (j <=  2);
}

Common Pitfalls

  • O(n²) time complexity can be inefficient for large data
  • Using wrong loop variables in inner vs outer loops
  • Forgetting to reset inner loop variables
  • Excessive nesting reduces readability (try to keep to 2-3 levels)

Functions in C

Functions are reusable blocks of code that perform specific tasks. They help in organizing code, reducing duplication, and making programs more modular.

Function Definition and Declaration

#include <stdio.h>
#include <math.h>

// Function declaration (prototype)
double calculateArea(double radius);

// Function definition
double calculateArea(double radius) {
    return M_PI * radius * radius;
}

// Function with multiple parameters
int addNumbers(int a, int b) {
    return a + b;
}

// Function without parameters
void greet() {
    printf("Hello, welcome to the program!\n");
}

// Function without return value (void)
void printMessage(char message[]) {
    printf("Message: %s\n", message);
}

int main() {
    greet();
    
    double radius = 5.0;
    double area = calculateArea(radius);
    printf("Area of circle with radius %.1f is %.2f\n", radius, area);
    
    int sum = addNumbers(10, 20);
    printf("Sum: %d\n", sum);
    
    printMessage("Functions make code organized!");
    
    return 0;
}

Function Parameters

// Pass by value (default)
void incrementByValue(int x) {
    x++;  // Only modifies local copy
    printf("Inside function: %d\n", x);
}

// Pass by pointer
void incrementByPointer(int *x) {
    (*x)++;  // Modifies original variable
    printf("Inside function: %d\n", *x);
}

int main() {
    int num = 5;
    
    incrementByValue(num);
    printf("After pass by value: %d\n", num);  // Still 5
    
    incrementByPointer(&num);
    printf("After pass by pointer: %d\n", num);  // Now 6
    
    return 0;
}

Recursive Functions

// Factorial using recursion
unsigned long long factorial(int n) {
    if (n <=  1) return 1;           // Base case
    return n * factorial(n - 1);    // Recursive case
}

// Fibonacci sequence
int fibonacci(int n) {
    if (n <=  1) return n;           // Base cases
    return fibonacci(n - 1) + fibonacci(n - 2);
}

int main() {
    printf("Factorial of 5: %llu\n", factorial(5));
    
    printf("Fibonacci sequence: ");
    for (int i = 0; i < 10; i++) {
        printf("%d ", fibonacci(i));
    }
    printf("\n");
    
    return 0;
}

Function Pointers

// Function that takes a function pointer as parameter
void applyOperation(int a, int b, int (*operation)(int, int)) {
    int result = operation(a, b);
    printf("Result: %d\n", result);
}

// Operation functions
int add(int a, int b) { return a + b; }
int multiply(int a, int b) { return a * b; }

int main() {
    int x = 10, y = 5;
    
    // Using function pointers
    applyOperation(x, y, add);
    applyOperation(x, y, multiply);
    
    // Array of function pointers
    int (*operations[])(int, int) = {add, multiply};
    printf("Using array: %d\n", operations[0](x, y));
    
    return 0;
}

Common Pitfalls

  • Forgetting return statement in non-void functions
  • Function declaration mismatch with definition
  • Infinite recursion without proper base case
  • Not checking if function pointers are NULL before calling

Include Directives and Headers in C

Header files contain declarations that can be shared across multiple source files. Include directives tell the preprocessor to include the contents of header files.

Standard Library Headers

// Input/output operations
#include <stdio.h>

// String manipulation
#include <string.h>

// Mathematical functions
#include <math.h>

// Character classification
#include <ctype.h>

// Standard library functions
#include <stdlib.h>

// Time and date
#include <time.h>

// Error handling
#include <errno.h>

// Limits of basic types
#include <limits.h>

int main() {
    // Examples using different headers
    char text[] = "Hello World";
    printf("String length: %zu\n", strlen(text));
    
    printf("Square root of 16: %.2f\n", sqrt(16));
    
    printf("Is 'A' uppercase? %d\n", isupper('A'));
    
    return 0;
}

Creating and Using Custom Headers

// math_utils.h (header file)
#ifndef MATH_UTILS_H
#define MATH_UTILS_H

// Function declarations
int add(int a, int b);
double calculateCircleArea(double radius);
int isPrime(int number);

#endif

// math_utils.c (implementation)
#include "math_utils.h"
#include <math.h>

int add(int a, int b) {
    return a + b;
}

double calculateCircleArea(double radius) {
    return M_PI * radius * radius;
}

int isPrime(int number) {
    if (number <=  1) return 0;
    for (int i = 2; i * i <=  number; i++) {
        if (number % i == 0) return 0;
    }
    return 1;
}

// main.c
#include <stdio.h>
#include "math_utils.h"

int main() {
    printf("5 + 3 = %d\n", add(5, 3));
    printf("Area of circle with radius 2: %.2f\n", calculateCircleArea(2));
    printf("Is 17 prime? %s\n", isPrime(17) ? "Yes" : "No");
    return 0;
}

Include Guards and Pragma Once

// Traditional include guards
#ifndef MY_HEADER_H
#define MY_HEADER_H
// Header content here
#endif

// Modern alternative (compiler-specific but widely supported)
#pragma once
// Header content here

/* Why use include guards?
   Prevents multiple inclusion of the same header
   Avoids redefinition errors
   Improves compilation speed */

Common Header Organization

// Typical header structure:

// 1. Include guards
#ifndef MY_HEADER_H
#define MY_HEADER_H

// 2. System includes (angle brackets)
#include <stdio.h>
#include <stdlib.h>

// 3. Constant definitions
#define MAX_SIZE 100
#define PI 3.14159

// 4. Type definitions
typedef struct {
    int x, y;
} Point;

// 5. Function declarations
void initialize(void);
int processData(int data);
void cleanup(void);

// 6. End include guard
#endif

Common Pitfalls

  • Missing include guards causing redefinition errors
  • Circular includes (A includes B, B includes A)
  • Including implementation files (.c) instead of headers
  • Forgetting to include necessary headers for used functionality

File Input/Output in C

C provides file stream functions for reading from and writing to files. The main functions use the FILE structure and are declared in stdio.h.

Basic File Operations

#include <stdio.h>

int main() {
    // Writing to a file
    FILE *outFile = fopen("example.txt", "w");
    if (outFile != NULL) {
        fprintf(outFile, "Hello, File!\n");
        fprintf(outFile, "This is line 2\n");
        fprintf(outFile, "Number: %d\n", 42);
        fclose(outFile);
        printf("File written successfully\n");
    } else {
        printf("Error opening file for writing\n");
    }
    
    // Reading from a file
    FILE *inFile = fopen("example.txt", "r");
    if (inFile != NULL) {
        char line[100];
        while (fgets(line, sizeof(line), inFile) != NULL) {
            printf("%s", line);
        }
        fclose(inFile);
    } else {
        printf("Error opening file for reading\n");
    }
    
    return 0;
}

Different File Opening Modes

// Various file opening modes
FILE *file1 = fopen("output.txt", "w");   // Write (truncate)
FILE *file2 = fopen("output.txt", "a");   // Append
FILE *file3 = fopen("output.txt", "r");   // Read
FILE *file4 = fopen("data.bin", "wb");    // Write binary
FILE *file5 = fopen("data.txt", "r+");    // Read and write

// Example: Append to file
FILE *logFile = fopen("log.txt", "a");
if (logFile != NULL) {
    fprintf(logFile, "New log entry\n");
    fclose(logFile);
}

Reading Different Data Types

// Writing mixed data types
FILE *dataFile = fopen("data.txt", "w");
if (dataFile != NULL) {
    fprintf(dataFile, "John Doe 25 85.5\n");
    fprintf(dataFile, "Jane Smith 30 92.0\n");
    fclose(dataFile);
}

// Reading mixed data types
FILE *inputFile = fopen("data.txt", "r");
if (inputFile != NULL) {
    char firstName[20], lastName[20];
    int age;
    double score;
    
    while (fscanf(inputFile, "%s %s %d %lf", 
                  firstName, lastName, &age, &score) == 4) {
        printf("Name: %s %s, Age: %d, Score: %.1f\n", 
               firstName, lastName, age, score);
    }
    fclose(inputFile);
}

// Reading character by character
FILE *charFile = fopen("example.txt", "r");
if (charFile != NULL) {
    int ch;
    while ((ch = fgetc(charFile)) != EOF) {
        putchar(ch);
    }
    fclose(charFile);
}

Binary File Operations

#include <stdio.h>

struct Person {
    char name[50];
    int age;
    double salary;
};

int main() {
    // Writing binary data
    struct Person p1 = {"Alice", 30, 50000.0};
    struct Person p2 = {"Bob", 25, 45000.0};
    
    FILE *binOut = fopen("people.dat", "wb");
    if (binOut != NULL) {
        fwrite(&p1, sizeof(struct Person), 1, binOut);
        fwrite(&p2, sizeof(struct Person), 1, binOut);
        fclose(binOut);
    }
    
    // Reading binary data
    struct Person p;
    FILE *binIn = fopen("people.dat", "rb");
    if (binIn != NULL) {
        while (fread(&p, sizeof(struct Person), 1, binIn) == 1) {
            printf("Name: %s, Age: %d, Salary: %.2f\n", 
                   p.name, p.age, p.salary);
        }
        fclose(binIn);
    }
    
    return 0;
}

File Position Operations

// Working with file positions
FILE *file = fopen("data.txt", "r+");
if (file != NULL) {
    // Get current position
    long pos = ftell(file);
    printf("Current position: %ld\n", pos);
    
    // Seek to beginning
    fseek(file, 0, SEEK_SET);
    
    // Seek to end
    fseek(file, 0, SEEK_END);
    long size = ftell(file);
    printf("File size: %ld bytes\n", size);
    
    // Seek to specific position
    fseek(file, 10, SEEK_SET);  // 10 bytes from beginning
    
    fclose(file);
}

Common Pitfalls

  • Forgetting to check if file opened successfully
  • Not closing files (can cause resource leaks)
  • Mixing text and binary modes
  • Assuming file operations always succeed - always check for errors

Memory Management in C

C provides manual memory management through functions like malloc, calloc, realloc, and free. Proper memory management is crucial for avoiding leaks and crashes.

Dynamic Memory Allocation

#include <stdio.h>
#include <stdlib.h>

int main() {
    // Allocate memory for an integer
    int *ptr = (int*)malloc(sizeof(int));
    if (ptr == NULL) {
        printf("Memory allocation failed\n");
        return 1;
    }
    
    *ptr = 42;
    printf("Value: %d\n", *ptr);
    
    // Free the memory
    free(ptr);
    ptr = NULL;  // Good practice to avoid dangling pointers
    
    return 0;
}

Array Allocation

#include <stdlib.h>

// Allocate array of integers
int size = 5;
int *arr = (int*)malloc(size * sizeof(int));
if (arr == NULL) {
    printf("Memory allocation failed\n");
    return 1;
}

// Initialize array
for (int i = 0; i < size; i++) {
    arr[i] = i * 10;
}

// Use array
for (int i = 0; i < size; i++) {
    printf("arr[%d] = %d\n", i, arr[i]);
}

// Free memory
free(arr);
arr = NULL;

calloc and realloc

// calloc - allocates and initializes to zero
int *zeros = (int*)calloc(5, sizeof(int));
if (zeros != NULL) {
    for (int i = 0; i < 5; i++) {
        printf("zeros[%d] = %d\n", i, zeros[i]);  // All zeros
    }
    free(zeros);
}

// realloc - resize allocated memory
int *dynamicArr = (int*)malloc(3 * sizeof(int));
if (dynamicArr != NULL) {
    dynamicArr[0] = 1;
    dynamicArr[1] = 2;
    dynamicArr[2] = 3;
    
    // Resize to hold 5 elements
    int *temp = (int*)realloc(dynamicArr, 5 * sizeof(int));
    if (temp != NULL) {
        dynamicArr = temp;
        dynamicArr[3] = 4;
        dynamicArr[4] = 5;
        
        for (int i = 0; i < 5; i++) {
            printf("%d ", dynamicArr[i]);
        }
        printf("\n");
    }
    free(dynamicArr);
}

Common Patterns

// Dynamic string
char *createString(const char *text) {
    char *str = (char*)malloc(strlen(text) + 1);  // +1 for null terminator
    if (str != NULL) {
        strcpy(str, text);
    }
    return str;
}

// Dynamic array of strings
char **createStringArray(int count, int maxLength) {
    char **arr = (char**)malloc(count * sizeof(char*));
    if (arr != NULL) {
        for (int i = 0; i < count; i++) {
            arr[i] = (char*)malloc(maxLength * sizeof(char));
            if (arr[i] == NULL) {
                // Cleanup on failure
                for (int j = 0; j < i; j++) {
                    free(arr[j]);
                }
                free(arr);
                return NULL;
            }
        }
    }
    return arr;
}

void freeStringArray(char **arr, int count) {
    for (int i = 0; i < count; i++) {
        free(arr[i]);
    }
    free(arr);
}

Common Pitfalls

  • Memory leaks (forgetting to free allocated memory)
  • Dangling pointers (using memory after it's freed)
  • Buffer overflow (writing beyond allocated memory)
  • Double free (freeing the same memory twice)
  • Not checking if allocation succeeded

C Preprocessor

The C preprocessor processes source code before compilation. It handles directives like #include, #define, and conditional compilation.

Macro Definitions

#include <stdio.h>

// Simple macro
#define PI 3.14159
#define MAX(a, b) ((a) > (b) ? (a) : (b))

// Multi-line macro
#define SWAP(a, b) do { \
    typeof(a) temp = a; \
    a = b; \
    b = temp; \
} while(0)

int main() {
    double radius = 5.0;
    double area = PI * radius * radius;
    printf("Area: %.2f\n", area);
    
    int x = 10, y = 20;
    printf("Max: %d\n", MAX(x, y));
    
    printf("Before swap: x=%d, y=%d\n", x, y);
    SWAP(x, y);
    printf("After swap: x=%d, y=%d\n", x, y);
    
    return 0;
}

Conditional Compilation

#include <stdio.h>

#define DEBUG 1
#define VERSION 2

int main() {
    // Conditional compilation based on macro value
    #if DEBUG
        printf("Debug mode enabled\n");
    #endif
    
    // Multiple conditions
    #if VERSION == 1
        printf("Running version 1\n");
    #elif VERSION == 2
        printf("Running version 2\n");
    #else
        printf("Unknown version\n");
    #endif
    
    // Check if macro is defined
    #ifdef DEBUG
        printf("DEBUG is defined\n");
    #endif
    
    #ifndef RELEASE
        printf("RELEASE is not defined\n");
    #endif
    
    return 0;
}

Predefined Macros

#include <stdio.h>

int main() {
    printf("File: %s\n", __FILE__);
    printf("Line: %d\n", __LINE__);
    printf("Date: %s\n", __DATE__);
    printf("Time: %s\n", __TIME__);
    
    #if defined(__STDC__)
        printf("ANSI C compliant\n");
    #endif
    
    #if defined(__cplusplus)
        printf("C++ compiler\n");
    #else
        printf("C compiler\n");
    #endif
    
    return 0;
}

Stringification and Token Pasting

#include <stdio.h>

// Stringification - convert parameter to string
#define STRINGIFY(x) #x

// Token pasting - combine tokens
#define PASTE(a, b) a##b

int main() {
    int xy = 100;
    
    printf("Stringified: %s\n", STRINGIFY(Hello World));
    printf("Pasted variable: %d\n", PASTE(x, y));  // xy
    
    // Practical example - creating function names
    #define MAKE_FUNCTION(name) void name##_function() { \
        printf("Called " #name "\n"); \
    }
    
    MAKE_FUNCTION(test)
    test_function();
    
    return 0;
}

Common Pitfalls

  • Missing parentheses in macro definitions causing operator precedence issues
  • Multiple evaluation of macro parameters
  • Overusing macros when functions would be better
  • Not protecting multi-statement macros with do { ... } while(0)

Advanced C Concepts

Advanced C features provide powerful tools for writing efficient, system-level code. These include function pointers, variable arguments, and low-level operations.

Variable Arguments

#include <stdio.h>
#include <stdarg.h>

// Function with variable arguments
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;
}

// Variable arguments with different types
void printValues(const char *types, ...) {
    va_list args;
    va_start(args, types);
    
    while (*types) {
        switch (*types) {
            case 'i':
                printf("int: %d\n", va_arg(args, int));
                break;
            case 'd':
                printf("double: %.2f\n", va_arg(args, double));
                break;
            case 's':
                printf("string: %s\n", va_arg(args, char*));
                break;
        }
        types++;
    }
    
    va_end(args);
}

int main() {
    printf("Sum: %d\n", sum(3, 10, 20, 30));
    printf("Sum: %d\n", sum(5, 1, 2, 3, 4, 5));
    
    printValues("ids", 42, 3.14, "hello");
    
    return 0;
}

Function Pointers and Callbacks

#include <stdio.h>
#include <stdlib.h>

// Typedef for function pointer
typedef int (*CompareFunc)(int, int);

// Comparison functions
int ascending(int a, int b) { return a - b; }
int descending(int a, int b) { return b -; }

// Function that takes a callback
void sortArray(int arr[], int size, CompareFunc compare) {
    for (int i = 0; i < size-1; i++) {
        for (int j = 0; j < size-i-1; j++) {
            if (compare(arr[j], arr[j+1]) > 0) {
                int temp = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = temp;
            }
        }
    }
}

int main() {
    int numbers[] = {5, 2, 8, 1, 9};
    int size = 5;
    
    printf("Original: ");
    for (int i = 0; i < size; i++) printf("%d ", numbers[i]);
    printf("\n");
    
    sortArray(numbers, size, ascending);
    printf("Ascending: ");
    for (int i = 0; i < size; i++) printf("%d ", numbers[i]);
    printf("\n");
    
    sortArray(numbers, size, descending);
    printf("Descending: ");
    for (int i = 0; i < size; i++) printf("%d ", numbers[i]);
    printf("\n");
    
    return 0;
}

Command Line Arguments

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
    printf("Number of arguments: %d\n", argc);
    
    for (int i = 0; i < argc; i++) {
        printf("argv[%d]: %s\n", i, argv[i]);
    }
    
    // Example: simple calculator
    if (argc == 4) {
        double a = atof(argv[1]);
        double b = atof(argv[3]);
        char op = argv[2][0];
        
        double result;
        switch (op) {
            case '+': result = a + b; break;
            case '-': result = a - b; break;
            case '*': result = a * b; break;
            case '/': result = a / b; break;
            default:
                printf("Unknown operator: %c\n", op);
                return 1;
        }
        
        printf("Result: %.2f\n", result);
    }
    
    return 0;
}

Bit Fields and Low-Level Operations

#include <stdio.h>

// Structure with bit fields
struct Settings {
    unsigned int enabled : 1;
    unsigned int mode : 2;
    unsigned int reserved : 5;
};

// Low-level bit manipulation
void printBinary(unsigned int num) {
    for (int i = 31; i >= 0; i--) {
        printf("%d", (num >> i) & 1);
        if (i % 8 == 0) printf(" ");
    }
    printf("\n");
}

int main() {
    // Bit fields
    struct Settings settings = {1, 2, 0};
    printf("Size of settings: %zu bytes\n", sizeof(settings));
    printf("Enabled: %u, Mode: %u\n", settings.enabled, settings.mode);
    
    // Bit manipulation
    unsigned int flags = 0;
    flags |= (1 << 3);  // Set bit 3
    flags |= (1 << 5);  // Set bit 5
    
    printf("Flags: ");
    printBinary(flags);
    
    // Check if bit is set
    if (flags & (1 << 3)) {
        printf("Bit 3 is set\n");
    }
    
    // Clear bit
    flags &= ~(1 << 3);
    printf("After clearing bit 3: ");
    printBinary(flags);
    
    return 0;
}

Error Handling

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

void demonstrateErrorHandling() {
    FILE *file = fopen("nonexistent.txt", "r");
    if (file == NULL) {
        printf("Error opening file: %s\n", strerror(errno));
        perror("fopen failed");
    }
    
    // Using errno
    errno = 0;
    double result = sqrt(-1);
    if (errno != 0) {
        printf("Math error: %s\n", strerror(errno));
    }
    
    // Custom error handling
    #define CHECK(condition, message) \
        do { \
            if (!(condition)) { \
                fprintf(stderr, "Error: %s (%s:%d)\n", \
                        message, __FILE__, __LINE__); \
                exit(EXIT_FAILURE); \
            } \
        } while(0)
    
    int *ptr = malloc(100 * sizeof(int));
    CHECK(ptr != NULL, "Memory allocation failed");
    
    free(ptr);
}

int main() {
    demonstrateErrorHandling();
    return 0;
}

Common Pitfalls

  • Not checking return values of functions
  • Buffer overflows in string operations
  • Memory leaks from not freeing allocated memory
  • Undefined behavior from accessing invalid memory
  • Integer overflow and underflow