๐ŸŒ˜C/C++

C Fundamentals

Welcome to the C Fundamentals course. In this course, we will cover the basics of the C programming language, as well as some more advanced topics. We will start by discussing how to create variables, use conditions and loops, and import libraries. By the end of this course, you should have a solid understanding of the basics of C and be ready to start writing your own programs.

Variables

In C, a variable is a way to store a value or a reference to an object. To create a variable, you need to declare it with a specific data type and give it a name, for example:

int x = 5; // Integer variable
float pi = 3.14; // Floating-point variable
char grade = 'A'; // Character variable
double temperature = 25.5; // Double-precision floating-point variable
unsigned int count = 10; // Unsigned integer variable
short int smallNumber = 100; // Short integer variable
long int largeNumber = 1000000; // Long integer variable

// Print the values of the variables
printf("Integer x: %d\n", x);
printf("Floating-point pi: %.2f\n", pi);
printf("Character grade: %c\n", grade);
printf("Double temperature: %.2f\n", temperature);
printf("Unsigned int count: %u\n", count);
printf("Short int smallNumber: %d\n", smallNumber);
printf("Long int largeNumber: %ld\n", largeNumber);

This creates a variable named x with the data type int and assigns it the value of 5.

  • float is used for floating-point numbers.

  • char is used for single characters.

  • double is used for double-precision floating-point numbers.

  • unsigned int is used for non-negative integers.

  • short int is used for short integers.

  • long int is used for long integers.

Vector (C++)

In C++, vectors are dynamic arrays that provide flexibility in managing collections of elements.

  • push_back: Appends a new element to the end of the vector.

  • pop_back: Removes the last element from the end of the vector.

  • insert: Inserts elements at a specified position in the vector.

  • erase: Removes elements from the vector at a specified position or within a range.

  • clear: Removes all elements from the vector, leaving it with a size of 0.

  • size: Returns the number of elements in the vector.

  • empty: Checks if the vector is empty.

  • resize: Changes the size of the vector, optionally filling new elements with a specified value.

  • front: Returns a reference to the first element in the vector.

  • back: Returns a reference to the last element in the vector.

Let's look at an example:

#include <iostream>
#include <vector>

int main() {
    // Creating a vector of integers
    std::vector<int> myVector;

    // Adding elements to the vector using push_back method
    myVector.push_back(10);
    myVector.push_back(20);
    myVector.push_back(30);

    // Accessing elements using index
    std::cout << "Elements in the vector: ";
    for (int i = 0; i < myVector.size(); ++i) {
        std::cout << myVector[i] << " ";
    }
    return 0;
}
  1. Vector Declaration: std::vector<int> myVector; declares a vector named myVector that can store integers.

  2. Adding Elements: myVector.push_back(10); adds the value 10 to the end of the vector. Similarly, we add 20 and 30.

  3. Accessing Elements: The for loop iterates through the vector using its size and prints each element.

This example demonstrates the basic usage of vectors, including declaration, addition of elements using push_back, and accessing elements using an index. Vectors in C++ provide dynamic sizing and convenient methods for managing collections.

Conditions

Conditions are used to make decisions in a program. The most common type of condition is the if-else statement. For example:

int x = 5;
if (x > 0) {
    printf("x is positive");
} else {
    printf("x is not positive");
}

This code checks if the value of x is greater than 0. If it is, the program will print "x is positive", otherwise it will print "x is not positive".

Loops

Loops are used to repeat a block of code multiple times. The two most common types of loops in C are the for loop and the while loop.

The for loop is used to execute a block of code a fixed number of times. For example:

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

This code will print the numbers from 0 to 4, because the condition (i < 5) is true for each iteration of the loop.

The while loop is used to execute a block of code as long as a certain condition is true. For example:

int x = 5;
while (x > 0) {
    printf("%d\n", x);
    x--;
}

This code will print the numbers from 5 down to 1, because the condition (x > 0) is true for each iteration of the loop.

Libraries

C has a large number of libraries that can be imported to add additional functionality to your programs. To import a library, you use the #include preprocessor directive followed by the name of the library. For example:

#include <stdio.h>

This imports the standard input/output library, which provides functions such as printf() and scanf().

Once a library is imported, you can use its functions by referencing them with the library name as a prefix. For example:

#include <stdio.h>
int main() {
    printf("Hello, World!");
    return 0;
}

This code imports the standard input/output library and then uses the printf() function to print the string "Hello, World!" on the screen.

Pointers

Pointers are one of the most powerful features of C, they allow you to directly manipulate memory and access variables by their memory address. Pointers are declared using the * operator, for example:

int x = 5;
int *ptr = &x;

This declares a pointer ptr that points to the memory address of the variable x. You can then use the pointer to manipulate the value of the variable it points to, or to allocate and deallocate memory dynamically during runtime.

int x = 5;
int *ptr = &x;
*ptr = 10; // x now has the value 10
int *ptr = (int *) malloc(sizeof(int));
*ptr = 5;
free(ptr);

The first example uses the pointer ptr to change the value of the variable x and the second one uses the malloc() function to dynamically allocate memory for an integer and the free() function to deallocate the memory once it's no longer needed.

This code demonstrates the use of pointers in C. It defines three functions, each of which takes a pointer as its argument. The first function, edit_int, takes a pointer to an integer and changes its value to 128. The second function, edit_char, takes a pointer to a character and changes its value to Z. The third function, edit_string, takes a pointer to a pointer to a character and changes the value of the pointer to a new string Foxtrot. The main function declares an integer, a character, and a string, and passes pointers to each of them to the corresponding functions. It then prints out the original and modified values of each variable.

#include <stdio.h>

void edit_int(int *);
void edit_char(char *);
void edit_string (char **);

int main(void) {
    int nb = 1;
    char letter = 'A';
    char *string = "Alpha";

    printf("First value: %d\n", nb);
    edit_int(&nb);
    printf("Second value: %d\n", nb);
    putchar('\n');

    printf("First value: %c\n", letter);
    edit_char(&letter);
    printf("Second value: %c\n", letter);
    putchar('\n');

    printf("First value: %s\n", string);
    edit_string(&string);
    printf("Second value: %s\n", string);
    putchar('\n');

    return 0;
}

void edit_int(int *nb) { *nb = 128; }

void edit_char(char *character) { *character = 'Z'; }

void edit_string(char **string) { *string = "Foxtrot"; }

Structures

Structures are a way of grouping together different variables of different data types, similar to objects in object-oriented languages. They allow you to create custom data types with several fields, for example:

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

This defines a new data type named Person with three fields: a character array named name, an integer named age, and a float named salary.

int main() {
    struct Person person;

    printf("Enter name: ");
    scanf("%s", person.name);

    printf("Enter age: ");
    scanf("%d", &person.age);

    printf("Enter salary: ");
    scanf("%f", &person.salary);

    printf("\n%s is %d years old and earns $%.2f.\n", person.name, person.age, person.salary);

    return 0;
}

In this example, the program creates a new Person structure named person. The program prompts the user to enter the person's name, age, and salary, and then stores the input in the appropriate fields of the person structure. Finally, the program prints out the person's information using the printf() function.

Functions

Functions in C work similarly to how they do in other programming languages, they allow you to define a block of code that can be reused multiple times in your program. Functions in C must be declared before they are used, this is done by writing a function prototype that specifies the return type, name and parameters of the function.

In C, a function is a self-contained block of code that performs a specific task. Functions are used to divide a large program into smaller, manageable parts. Each function has a name, parameters, and a return type, and it can be called (invoked) from other parts of the program.

Function Definition

A function in C is defined with the following syntax:

return_type function_name(parameter_type parameter_name, ... ) {
    // Function body
    // Code to perform a specific task
    return return_value;
}
  • return_type: Specifies the type of data that the function will return. If a function doesn't return anything, you can use void as the return type.

  • function_name: The name of the function, which should be a unique identifier within the program.

  • parameter_type: The type of each parameter the function accepts.

  • parameter_name: The name of each parameter used to reference values passed to the function.

  • Function body: Contains the code that defines what the function does.

  • return_value: The value to be returned by the function. If the return type is void, this part is omitted.

Example: add Function

Let's look at an example of a simple add function that takes two integers and returns their sum:

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

In this example:

  • int is the return type, indicating that the function will return an integer.

  • add is the function name.

  • (int a, int b) are the parameters. The function takes two integer parameters, a and b.

  • The function body calculates the sum of a and b and returns the result (sum variable) using the return statement.

Function Prototypes

Before you use a function in C, it must be declared or defined. To declare a function, you can provide a function prototype. A function prototype specifies the function's name, return type, and parameters without defining the full function. This allows you to use the function before its actual implementation.

Here's a function prototype for the add function:

int add(int a, int b);

This prototype informs the compiler that there is a function called add that takes two integers as parameters and returns an integer. You can place function prototypes at the beginning of your source code or include them in header files for use in multiple source files.

Using the add Function

To use the add function in your program, you can call it from the main function:

#include <stdio.h>

int main() {
    int result = add(5, 3);
    printf("Sum: %d\n", result);
    return 0;
}

In this code, we include the <stdio.h> header for the printf function and call the add function to calculate the sum of 5 and 3.

We've covered the basics of functions in C, including their definition, parameters, return values, and function prototypes. Functions are an essential part of C programming, enabling code organization, reusability, and modularity. They allow you to break down complex tasks into manageable components, making your code more readable and maintainable.

Class (C++)

Classes are an essential part of object-oriented programming, and they allow you to create custom data types and encapsulate both data and behavior into a single unit. We'll use the example code you provided as a starting point to explain various concepts related to C++ classes.

What is a Class?

In C++, a class is a blueprint for creating objects. It defines the structure and behavior of objects that will be created based on it. A class can contain data members (also called attributes or properties) and member functions (methods) that operate on these data members.

Example: Auth Class

Let's start by examining the code you provided. The Auth class represents a simple authentication system with a predefined username and password. Here's the code with explanations:

#include <iostream>
#include <string>

using namespace std;

class Auth {
  private:
    string username = "fastiraz"; // Private data member for the username
    string password = "Sup3r_S3cr3t_P4ssw0rd"; // Private data member for the password

  public:
    bool login(string usr, string pwd) {
      if (usr == this->username && pwd == this->password) {
        return true; // Successful login
      } else {
        return false; // Failed login
      }
    }
};

In this code, we define a class called Auth, which contains:

  • Private Data Members: The username and password are private data members of the class, which means they can only be accessed within the class itself. This encapsulates sensitive information, making it inaccessible from outside the class.

  • Public Member Function: The login function is a public member function that allows external code to attempt a login. It takes two arguments, usr (the entered username) and pwd (the entered password). It compares these values with the stored username and password, and if they match, it returns true, indicating a successful login; otherwise, it returns false.

Using the Auth Class

The main function demonstrates how to create an instance of the Auth class and use it for authentication:

int main(void) {
  string usr, pwd;

  cout << "Username: ";
  cin >> usr;
  cout << "Password: ";
  cin >> pwd;

  Auth auth; // Create an instance of the Auth class

  if (auth.login(usr, pwd)) {
    cout << "[Successful!]" << endl;
  } else {
    cout << "[Failed!]" << endl;
  }

  return 0;
}

In this main function, we perform the following steps:

  1. Prompt the user for a username and password.

  2. Create an instance of the Auth class named auth.

  3. Call the login method of the auth instance to attempt authentication.

  4. Based on the result of the login method, display a message indicating whether the login was successful or not.

Header files

C also allows you to create your own libraries, also called header files, which can be included in your program to reuse code and organize your program in a more modular way. To create a library, you can create a header file with the extension .h that contains the function prototypes and any global variables or constants. Then, in a separate file with the extension .c, you can provide the implementation of the functions.

By the end of this course, you should have a good understanding of the C programming language and be able to write programs that make use of variables, conditions, loops, functions, pointers, structures, dynamic memory allocation, and more.

Bitwise operations

C provides a set of bitwise operators such as &, |, ^, ~, <<, and >> that allow you to manipulate individual bits of a variable. These operators can be used to optimize certain types of algorithms, perform low-level operations on hardware, or encode and decode data.

unsigned char x = 10; // binary: 00001010
unsigned char y = 5;  // binary: 00000101
unsigned char z = x & y;  // binary: 00000010

In this example, the & operator is used to perform a bitwise AND operation on the variables x and y, resulting in a new variable z with the value 2 (binary: 00000010).

typedef

In C, the typedef keyword is a powerful tool that allows you to create your own custom data types, giving you the ability to enhance code readability and maintainability. This course will explain what typedef is, how it works, and provide practical examples to help you understand its usage.

What is typedef?

In C, typedef is a keyword used to define custom data types. It allows you to create aliases or synonyms for existing data types, making your code more readable and expressive.

Defining Custom Data Types

The syntax for creating a custom data type using typedef is as follows:

typedef existing_data_type new_data_type;
  • existing_data_type: The data type for which you want to create an alias.

  • new_data_type: The alias or synonym you want to create for the existing data type.

Using typedef with Basic Data Types

Example 1: Creating an Alias for int

typedef int Integer;

In this example, we create an alias "Integer" for the int data type. Now, you can use Integer instead of int in your code.

Example 2: Creating an Alias for char

typedef char Character;

Here, we create an alias "Character" for the char data type.

Creating Complex Custom Types

You can also use typedef to create complex custom data types. For instance:

Example 3: Creating a Structure Alias

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

typedef struct Date MyDate;

In this example, we define a custom structure Date and then create an alias MyDate for it using typedef.

Benefits of typedef

  • Improved Code Readability: typedef can make your code more descriptive and easier to understand by using meaningful names for data types.

  • Easy Maintenance: If you need to change the underlying data type, you only have to update it in one place where the typedef is defined.

  • Enhanced Portability: Using typedef can help make your code more portable since it abstracts the underlying data types, making it easier to adapt to different platforms.

Enum

In C, an enum is a user-defined data type used to create a set of named integer constants. It provides a way to represent a set of values with meaningful names, making code more readable and maintainable. This course will explain what enum is, how it works, and provide practical examples to help you understand its usage.

Enums, short for "enumerations," are a data type in C that allow you to create a set of named integral constants. They make your code more readable and maintainable by providing descriptive names for values rather than using raw numbers.

Declaring Enums

In C, you declare an enum using the enum keyword followed by a list of constant names enclosed in curly braces.

enum Days {
    SUNDAY,    // 0
    MONDAY,    // 1
    TUESDAY,   // 2
    WEDNESDAY, // 3
    THURSDAY,  // 4
    FRIDAY,    // 5
    SATURDAY   // 6
};

In this example, Days is the enum type, and the constants SUNDAY, MONDAY, etc., are assigned consecutive integer values starting from 0.

Accessing Enum Constants

You can access enum constants by using the enum type name followed by the constant name.

enum Days today = TUESDAY;

Enum Constants and Values

While the example above assigns integer values to enum constants by default, you can also explicitly set the values.

enum Colors {
    RED = 1,
    GREEN = 2,
    BLUE = 4
};

In this case, RED will have a value of 1, GREEN will have 2, and BLUE will have 4.

Using Enums in Switch Statements

Enums are often used in switch statements for improved code readability.

enum Days day = WEDNESDAY;

switch (day) {
    case MONDAY:
        printf("It's Monday!");
        break;
    case WEDNESDAY:
        printf("It's Wednesday!");
        break;
    default:
        printf("It's not a special day.");
        break;
}

Benefits of Enums

  • Readability: Enums make code more readable as constant names are self-explanatory.

  • Type Safety: You can't assign arbitrary values to an enum; it only accepts values defined within the enum.

  • Maintenance: When constants change, you only need to update the enum definition.

  • Compile-Time Constants: Enums are evaluated at compile-time.

Preprocessor Macros

C also provides a preprocessor that allows you to define macros using the #define directive. Macros are a way to create simple and efficient substitutes for repetitive or complex expressions, or to define constants that can be easily changed later.

#define PI 3.14
#define MIN(a,b) ((a) < (b) ? (a) : (b))

In this example, the first macro defines the constant PI as 3.14, and the second macro defines a MIN function that returns the minimum of two values.

Here's another helpful illustration:

#define output(x) std::cout << x << std::endl;
#define input(x) std::cin >> x;

In this instance, we are defining custom print and read functions (input and output) to simplify content display and input.

You can utilize them as follows:

int main (void) {
  string usr, pwd;

  output("Username: ");
  input(usr);
  output("Password: ");
  input(pwd);

  Auth auth;
  if (auth.login(usr, pwd)) {
    output("[Successful!]");
  } else {
    output("[Failed!]");
  }
  
  return 0;
}

Conditional Compilation

In C programming, you can instruct the preprocessor whether to include a block of code or not. To do so, conditional directives can be used.

It's similar to a if statement with one major difference.

The if statement is tested during the execution time to check whether a block of code should be executed or not whereas, the conditionals are used to include (or skip) a block of code in your program before execution.


Uses of Conditional

  • use different code depending on the machine, operating system

  • compile the same source file in two different programs

  • to exclude certain code from the program but to keep it as a reference for future purposes

How to use conditional?

To use conditional, #ifdef, #if, #defined, #else and #elif directives are used.

ifdef Directive

#ifdef MACRO     
   // conditional codes
#endif

Here, the conditional codes are included in the program only if MACRO is defined.

#if, #elif and #else Directive

#if expression
   // conditional codes
#endif

Here, expression is an expression of integer type (can be integers, characters, arithmetic expression, macros, and so on).

The conditional codes are included in the program only if the expression is evaluated to a non-zero value.

The optional #else directive can be used with #if directive.

#if expression
   conditional codes if expression is non-zero
#else
   conditional if expression is 0
#endif

You can also add nested conditional to your #if...#else using #elif

#if expression
    // conditional codes if expression is non-zero
#elif expression1
    // conditional codes if expression is non-zero
#elif expression2
    // conditional codes if expression is non-zero
#else
    // conditional if all expressions are 0
#endif

#define

The special operator #defined is used to test whether a certain macro is defined or not. It's often used with #if directive.

#if defined BUFFER_SIZE && BUFFER_SIZE >= 2048
  // codes

Predefined Macros

Here are some predefined macros in C programming.

MacroValue

__DATE__

A string containing the current date.

__FILE__

A string containing the file name.

__LINE__

An integer representing the current line number.

__STDC__

If follows ANSI standard C, then the value is a nonzero integer.

__TIME__

A string containing the current time.

The following program outputs the current time using __TIME__ macro.

#include <stdio.h>
int main() {
   printf("Current time: %s",__TIME__);   
}

Unions

Unions are a special type of data structure in C that allows you to store different data types in the same memory location. This can be useful for creating efficient and flexible data structures, or for performing type punning.

union Number {
    int i;
    float f;
    double d;
};

In this example, a union named Number is defined that can store an integer, a float, or a double in the same memory location.

File handling

C also provides a standard library for file handling, which allows you to read and write data from files using file streams. File streams are similar to the standard input and output streams, but they operate on files instead of the console.

#include <stdio.h>

int main() {
    FILE *file;
    file = fopen("example.txt", "w");
    fprintf(file, "Writing to a file in C");
    fclose(file);
    return 0;
}

In this example, the fopen() function is used to open a file named "example.txt" in write mode, the fprintf() function is used to write a string to the file, and the fclose() function is used to close the file.

Pipes

Understanding Pipes

In C, a pipe is a form of interprocess communication (IPC) that allows two processes to communicate with each other. One process writes data to the pipe, while the other process reads the data from the pipe. A pipe can be thought of as a unidirectional data channel.

A pipe is created using the pipe() function. This function takes an array of two integers as an argument, which represent the read and write ends of the pipe. The pipe() function returns 0 on success and -1 on failure.

#include <unistd.h>

int pipe(int pipefd[2]);

The pipefd parameter is a two-element array that will be populated with the file descriptors for the read and write ends of the pipe. For example:

int pipefd[2];
if (pipe(pipefd) == -1) {
    perror("pipe");
    exit(EXIT_FAILURE);
}

This code creates a pipe and stores the read and write file descriptors in pipefd.

Using Pipes

Once a pipe has been created, the two processes can use it to communicate with each other. The process that wants to write data to the pipe writes to the write end of the pipe, while the process that wants to read data from the pipe reads from the read end of the pipe.

Here is an example of how to write data to a pipe:

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

int main(void)
{
    int pipefd[2];
    pid_t pid;
    char buffer[20];

    if (pipe(pipefd) == -1) {
        perror("pipe");
        exit(EXIT_FAILURE);
    }

    pid = fork();

    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }

    if (pid == 0) {
        /* Child process */
        close(pipefd[0]); /* Close unused read end */
        write(pipefd[1], "Hello, world!", 13);
        exit(EXIT_SUCCESS);
    } else {
        /* Parent process */
        close(pipefd[1]); /* Close unused write end */
        read(pipefd[0], buffer, 13);
        printf("%s\n", buffer);
        exit(EXIT_SUCCESS);
    }

    return 0;
}

This code creates a pipe, forks a child process, and then writes the string "Hello, world!" to the pipe from the child process. The parent process reads the string from the pipe and prints it to the console.

Pipes are an important form of interprocess communication in C. They provide a simple way for two processes to communicate with each other. In this course, we have covered the basics of how pipes work, how to create and use them, and provided some code examples to illustrate their usage. With this knowledge, you can start using pipes in your own C programs to facilitate communication between different processes.

Signals

Understanding Signals

Signals are an important concept in C programming, as they allow a process to be interrupted by the operating system when a certain event occurs. Some common events that can trigger signals include a user pressing Ctrl-C, the process running out of memory, or the process receiving a specific signal from another process.

When a signal is sent to a process, the process is interrupted and the signal handler function is called. The signal handler function is responsible for handling the signal and responding appropriately. This can include terminating the process, ignoring the signal, or performing some other action.

Signal Handling in C

Signal handling in C involves defining a signal handler function that will be called when a signal is received by the process. The signal() function is used to set up the signal handler function, and takes two arguments: the signal number to handle, and the name of the signal handler function.

#include <signal.h>

void sigint_handler(int signum) {
    printf("Caught signal %d\n", signum);
}

int main(void) {
    signal(SIGINT, sigint_handler);

    while (1) {
        printf("Waiting for signal...\n");
        sleep(1);
    }

    return 0;
}

In the example above, we define a signal handler function sigint_handler() that will be called when the SIGINT signal is received. We then set up the signal handler function using the signal() function. Finally, we enter an infinite loop that waits for signals to be received.

Sending Signals in C

Signals can also be sent from one process to another using the kill() function. The kill() function takes two arguments: the process ID of the receiving process, and the signal number to send.

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

int main(int argc, char *argv[]) {
    pid_t pid;
    int signum;

    if (argc != 3) {
        printf("Usage: %s <pid> <signum>\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    pid = atoi(argv[1]);
    signum = atoi(argv[2]);

    if (kill(pid, signum) == -1) {
        perror("kill");
        exit(EXIT_FAILURE);
    }

    return 0;
}

In the example above, we use the kill() function to send a signal to a process with a specified process ID. We take the process ID and signal number as command line arguments.

Handling Multiple Signals

It is possible to handle multiple signals in a single signal handler function using the sigaction() function. The sigaction() function allows us to specify a structure that contains information about how to handle the signal, including a pointer to the signal handler function.

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

void sig_handler(int signum) {
    printf("Caught signal %d\n", signum);
}

int main() {
    struct sigaction sa;

    sa.sa_handler = sig_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;

    sigaction(SIGINT, &sa, NULL);
    sigaction(SIGTERM, &sa, NULL);

    while {
        printf("Waiting for signals...\n");
        sleep(1);
    }

    return 0;
}

In the example above, we define a signal handler function sig_handler() that will be called when either the SIGINT or SIGTERM signal is received. We then use the sigaction() function to set up the signal handler function for both signals.

Signals are an important mechanism for interprocess communication and handling events in C. By understanding how signals work, how to send and receive signals, and how to handle signals, you can write robust and reliable C programs that respond appropriately to events and signals sent by the operating system.

In this course, we have covered the basics of signals in C, including how to handle single and multiple signals, and how to send signals to other processes. We encourage you to explore further and experiment with signals in your own C programs.

Semaphore

Introduction

Semaphore is a synchronization primitive in operating systems that allows multiple processes/threads to access shared resources in a mutually exclusive manner. Semaphore can be used to implement locks, mutual exclusion, and coordination between multiple processes/threads.

In this course, we will learn about semaphores in C programming language, including their definition, use, and implementation using code examples.

Definition

A semaphore is an integer value that is used to control access to a shared resource in a concurrent system. It consists of two operations:

  • Wait (also known as P or down): The wait operation decrements the value of the semaphore by 1. If the resulting value is negative, the process/thread is blocked until the semaphore value becomes positive again.

  • Signal (also known as V or up): The signal operation increments the value of the semaphore by 1. If there are any processes/threads waiting for the semaphore, one of them is unblocked.

Semaphore implementation in C

In C programming language, semaphores can be implemented using the sem_t data type, which is defined in the semaphore.h header file.

Creating a semaphore

To create a semaphore, we use the sem_init() function, which takes three arguments:

int sem_init(sem_t *sem, int pshared, unsigned int value);
  • sem: A pointer to the semaphore variable that is to be initialized.

  • pshared: A flag that indicates whether the semaphore should be shared between multiple processes or not. A value of 0 means that the semaphore is shared between threads of the same process, while a value of 1 means that the semaphore is shared between multiple processes.

  • value: The initial value of the semaphore.

Here's an example:

#include <semaphore.h>

sem_t my_sem;

int main() {
    sem_init(&my_sem, 0, 1);
    // ...
}

In this example, we create a semaphore my_sem with an initial value of 1, which means that the first process/thread that tries to access the shared resource will be able to do so.

Waiting and signaling

To perform the wait and signal operations on a semaphore, we use the sem_wait() and sem_post() functions, respectively. Both functions take a pointer to the semaphore variable as their argument.

int sem_wait(sem_t *sem);
int sem_post(sem_t *sem);

Here's an example that demonstrates the use of sem_wait() and sem_post():

#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>

sem_t my_sem;

void *worker(void *arg) {
    sem_wait(&my_sem);
    printf("Worker thread acquired the semaphore.\n");
    // ...
    sem_post(&my_sem);
    printf("Worker thread released the semaphore.\n");
    return NULL;
}

int main(void) {
    pthread_t tid;
    sem_init(&my_sem, 0, 1);
    pthread_create(&tid, NULL, worker, NULL);
    // ...
    sem_wait(&my_sem);
    printf("Main thread acquired the semaphore.\n");
    // ...
    sem_post(&my_sem);
    printf("Main thread released the semaphore.\n");
    pthread_join(tid, NULL);
    return 0;
}

In this example, we create a semaphore my_sem with an initial value of 1. We then create a worker thread that waits for the semaphore using sem_wait(), performs some work, and then signals the semaphore using sem_post(). The main thread also waits for the semaphore using sem_wait(), performs some work, and then signals the semaphore using sem_post(). Both the main thread and the worker thread will be able to access the shared resource in a mutually exclusive manner.

Error handling

When working with semaphores, it is important to handle errors that may occur during initialization or operation. The sem_init(), sem_wait(), and sem_post() functions all return 0 on success and -1 on failure, so we can use this to check for errors.

Here's an example:

#include <stdio.h>
#include <semaphore.h>
#include <errno.h>

sem_t my_sem;

int main(void) {
    if (sem_init(&my_sem, 0, 1) == -1) {
        perror("sem_init");
        return 1;
    }
    // ...
    if (sem_wait(&my_sem) == -1) {
        perror("sem_wait");
        return 1;
    }
    // ...
    if (sem_post(&my_sem) == -1) {
        perror("sem_post");
        return 1;
    }
    // ...
    return 0;
}

In this example, we use the perror() function to print an error message if sem_init(), sem_wait(), or sem_post() fails. The errno variable contains the error code that caused the failure.

In this course, we learned about semaphores in C programming language, including their definition, use, and implementation using code examples. Semaphores are a powerful synchronization primitive that can be used to implement locks, mutual exclusion, and coordination between multiple processes/threads. By understanding semaphores, you will be better equipped to design and implement concurrent systems that are correct, efficient, and reliable.

goto

In this chapter, we'll explore the goto statement in C/C++. The goto statement allows altering the flow of control in a program by transferring the execution to a labeled statement within the same function. However, it's essential to use goto judiciously as its misuse can lead to less readable and more complex code.

Syntax

The syntax for goto statement is straightforward:

goto label_name;

Where label_name is an identifier followed by a colon : that marks the label in the code.

Example

Consider the following example:

#include <stdio.h>

int main() {
    int count = 0;
    start: // Label declaration
    printf("Count: %d\n", count);
    count++;
    
    if (count < 5) {
        goto start; // Jump to the 'start' label
    }
    
    printf("End of the loop\n");
    return 0;
}

Explanation

  • count is initialized to 0.

  • The start label is declared just before the printf statement.

  • It prints the value of count, increments count by 1, and checks if count is less than 5.

  • If count is less than 5, the program uses goto start; to jump back to the start label and repeats the process.

  • Once count is not less than 5, the loop ends, and "End of the loop" is printed.

Output

Count: 0
Count: 1
Count: 2
Count: 3
Count: 4
End of the loop

Best Practices and Caution

  • Use sparingly: While goto can be useful in certain cases, overuse can lead to code that is hard to follow and maintain.

  • Prefer other control structures: In most scenarios, loops (for, while, do-while) and conditional statements (if-else) are more readable and maintainable than goto.

The goto statement is a powerful control flow tool in C/C++ programming. However, it should be used cautiously and sparingly to ensure code readability and maintainability.

Conclusion

C is a powerful and versatile language that is widely used in systems programming, embedded systems, and high-performance computing. It's a great choice for low-level programming, and its popularity and wide availability make it an excellent choice for learning the fundamentals of programming. With this course, you should have a solid understanding of the basics of C and be ready to start writing your own programs and experimenting with advanced features of the language.

Last updated