Key concepts in C/C++

Static Functions

A static function is a function whose scope is limited to the file or class in which it is declared. It has special properties depending on whether it's used in C or C++ (or other programming languages), but its key feature is its limited accessibility.

1. Static Functions in C

Definition:

A static function in C is a function that is only visible within the file in which it is declared. It is declared using the static keyword.

Purpose:

  • Restrict the scope of the function to the file.

  • Prevent name conflicts with functions of the same name in other files.

#include <stdio.h>

static void printMessage() {
    printf("This is a static function.\n");
}

void callStaticFunction() {
    printMessage();
}

Explanation:

  • The function printMessage can only be called within the file where it is declared.

  • If another file defines a function with the same name, there will be no conflict.

Static Functions in C++

Definition:

In C++, a static function is a member function of a class that:

  • Does not operate on an instance of the class.

  • Belongs to the class itself rather than any specific object of the class.

Key Properties:

  1. Can be called using the class name.

  2. Cannot access non-static members or this pointer directly.

  3. Useful for utility functions related to a class.

Example:

#include <iostream>
class MyClass {
public:
    static int count;

    static void printCount() {
        std::cout << "Count: " << count << std::endl;
    }
};

int MyClass::count = 10;

int main() {
    MyClass::printCount(); // Call without creating an object
    return 0;
}

Explanation:

  • printCount is a static function and can be called without an object of MyClass.

  • It accesses only static data members, such as count.

Advantages of Static Functions

  1. Encapsulation:

    • Limits the scope of the function in C.

    • Encapsulates utility functions in a class in C++.

  2. Avoid Name Conflicts:

    • Prevents global namespace pollution in C.

    • Functions with the same name in different classes are distinct in C++.

  3. Efficient Utility Functions:

    • Provides a way to write helper functions related to a class in C++ without needing an instance.

Real-World Use Cases

In C:

  • Modular Programming: Keep helper functions private to a file.

In C++:

  • Singleton Pattern: Use static functions for managing single-instance classes.

  • Global Counters: Use static functions to maintain and provide access to global counters within a class.

volatile Keyword in C

The volatile keyword in C is used to tell the compiler that a variable's value can change at any time, outside the program's control. This prevents the compiler from optimizing the code in ways that assume the variable's value does not change unexpectedly.

Why Use volatile?

In certain cases, a variable's value may change due to external factors, such as:

  1. Hardware: Reading from hardware registers or memory-mapped I/O.

  2. Interrupts: Variables modified by an interrupt service routine (ISR).

  3. Multithreading: Shared variables accessed by multiple threads.

Without volatile, the compiler might optimize the code in ways that cause incorrect behavior, such as:

  • Storing the variable's value in a register and not checking the actual memory.

  • Reordering reads and writes to the variable.

Example Without volatile

Code:

#include <stdio.h>
int flag = 0;
// fix : volatile int flag = 0;

void someFunction() {
    while (flag == 0) {
        // Do nothing, waiting for flag to change
    }
    printf("Flag changed!\n");
}

Issue:

  • The compiler might optimize the while (flag == 0) loop by assuming flag will never change since it isn't modified within the code. This can result in an infinite loop, even if flag is updated by an external event, such as an interrupt.

Fix:

  • By declaring flag as volatile, the compiler is forced to:

    1. Read flag directly from memory each time.

    2. Avoid optimizations that assume flag does not change.

Common Use Cases

a. Memory-Mapped I/O

#define STATUS_REGISTER (*(volatile unsigned int *)0x40004000)

void checkStatus() {
    while (!(STATUS_REGISTER & 0x01)) {
        // Wait until bit 0 of the status register is set
    }
}

b. Interrupt Service Routines (ISRs)

volatile int dataReady = 0;

void ISR() {
    dataReady = 1; // Set by interrupt
}

void mainFunction() {
    while (!dataReady) {
        // Wait for interrupt
    }
    printf("Data is ready!\n");
}

volatile ensures the main function doesn't cache the value of dataReady and always checks its actual value.

Endianness

Endianness refers to the order in which bytes are arranged in memory for multi-byte data types like integers, floats, etc. It determines whether the most significant byte (MSB) or the least significant byte (LSB) is stored at the lowest memory address.

Types of Endianness

1. Little-Endian

  • The least significant byte (LSB) is stored first (at the lowest memory address).

  • Example: For the hexadecimal value 0x12345678 (32-bit integer),

    • Memory representation: 78 56 34 12

2. Big-Endian

  • The most significant byte (MSB) is stored first (at the lowest memory address).

  • Example: For the same value 0x12345678,

    • Memory representation: 12 34 56 78

Illustration

Assume we have a 32-bit integer 0x12345678 stored in memory at address 0x1000. Here's how it would look in both systems:

AddressLittle-EndianBig-Endian
0x10007812
0x10015634
0x10023456
0x10031278

Example

Little-Endian

  • Processor reads 0x78 (from the lowest address 0x1000) as the least significant byte.

  • The final value is reconstructed as 0x12345678.

Big-Endian

  • Processor reads 0x12 (from the lowest address 0x1000) as the most significant byte.

  • The final value is reconstructed as 0x12345678.

Code Example

To detect endianness, as discussed earlier:

#include <stdio.h>

void checkEndianness() {
    unsigned int num = 1;      // Step 1: Declare an unsigned integer with value 1.
    char *ptr = (char *)&num;  // Step 2: Point to the memory address of 'num' using a char pointer.

    if (*ptr == 1) {           // Step 3: Check the value stored at the first byte of 'num'.
        printf("System is Little Endian\n");
    } else {
        printf("System is Big Endian\n");
    }
}

int main() {
    checkEndianness();         // Call the function to check endianness.
    return 0;
}

How the Code Works

Step 1: Declare an Integer

unsigned int num = 1;

  • num is a 4-byte (32-bit) unsigned integer with the value 1.

  • In memory, num will be stored in binary as: 00000000 00000000 00000000 00000001

Step 2: Use a Character Pointer

char *ptr = (char *)&num;
  • The &num operator retrieves the address of the variable num.

  • The pointer ptr is declared as a char pointer, meaning it will interpret the memory of num one byte at a time (since char is 1 byte in size).

Step 3: Check the Value of the First Byte

if (*ptr == 1)

  • *ptr dereferences the pointer to access the first byte of num.

Depending on the system's endianness:

  1. Little-Endian:

    • The least significant byte (LSB) is stored at the lowest memory address.

    • Memory representation of num (value = 1):

    •     Address | Byte
          --------|-----
          0x1000  | 01
          0x1001  | 00
          0x1002  | 00
          0x1003  | 00
      

      In this case, *ptr will read 01, so the program prints: System is Little Endian

  2. Big-Endian:

    • The most significant byte (MSB) is stored at the lowest memory address.

    • Memory representation of num (value = 1):

    •     Address | Byte
          --------|-----
          0x1000  | 00
          0x1001  | 00
          0x1002  | 00
          0x1003  | 01
      

Structures

A structure in C is a user-defined data type that allows grouping variables of different types under one name.

Example:

#include <stdio.h>

struct Employee {
    int id;           // Integer field
    char name[50];    // String (array of characters)
    float salary;     // Floating-point field
};

int main() {
    struct Employee emp1;  // Declare a structure variable

    // Assign values
    emp1.id = 101;
    emp1.salary = 50000.5;
    snprintf(emp1.name, 50, "Alice");  // Safely copy string

    // Print values
    printf("ID: %d, Name: %s, Salary: %.2f\n", emp1.id, emp1.name, emp1.salary);

    return 0;
}

Structures may include padding to align members in memory, improving performance.

Useful for managing memory by storing flags or small integers in a structure.

struct Flags {
    unsigned int isRunning : 1;  // 1-bit field
    unsigned int hasError  : 1;  // 1-bit field
    unsigned int priority  : 3;  // 3-bit field (0-7)
};

Unions

A union is similar to a structure, but all its members share the same memory location. This means a union can only store one member value at a time.

#include <stdio.h>

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

int main() {
    union Data data;

    data.i = 42;
    printf("Integer: %d\n", data.i);

    data.f = 3.14;
    printf("Float: %.2f\n", data.f);

    snprintf(data.str, 20, "Hello");
    printf("String: %s\n", data.str);

    return 0;
}

Only the largest member size determines the memory allocated.

Efficient use of memory when only one field is needed at a time (e.g., interpreting data types).

Unions can interpret data as multiple types.

Enumerations (Enums)

An enum is a user-defined data type that consists of integral constants.

#include <stdio.h>

enum Weekday { MON, TUE, WED, THU, FRI, SAT, SUN };

int main() {
    enum Weekday today = WED;

    if (today == WED) {
        printf("It's Wednesday!\n");
    }

    return 0;
}

Constants start from 0 by default (MON = 0, TUE = 1, etc.).