W4 - Shorts 📚🔍- Understanding Hexadecimal Memory Addresses, Pointers, and Strings in C

Read more here
CS50 IDE: https://cs50.dev/ (Online VSCode)
CS50 Library Documentation: https://manual.cs50.io/#cs50.h

#CS50x #C #Computer_Science #VisualStudioCode

🔙 Previous Part | Next Part 🔜

↩️ Go Back

Table of Contents:

🔙 Previous Part | Next Part 🔜

↩️ Go Back


A) Understanding Hexadecimal and Its Use in Programming🎦

A.1) Introduction to Number Systems

Decimal (Base 10)

Binary (Base 2)

Hexadecimal (Base 16)


A.2) Why Hexadecimal?

Concise Representation of Binary

Binary: 1111=Hexadecimal: F

Human Readability

Prefix and Ambiguity:

Common in Programming


A.3) Place Values in Hexadecimal

Hexadecimal values are analogous to decimal and binary:

Number System Base Place Values
Decimal 10 100, 101, 102, ...
Binary 2 20, 21, 22, ...
Hexadecimal 16 160, 161, 162, ...

Example: 0xADC<pre class="dataview dataview-error">Evaluation Error: SyntaxError: Invalid or unexpected token at DataviewInlineApi.eval (plugin:dataview:18404:21) at evalInContext (plugin:dataview:18405:7) at asyncEvalInContext (plugin:dataview:18415:32) at DataviewJSRenderer.render (plugin:dataview:18436:19) at DataviewJSRenderer.onload (plugin:dataview:18020:14) at e.load (app://obsidian.md/app.js:1:1230365) at DataviewApi.executeJs (plugin:dataview:18954:18) at Publisher.eval (plugin:digitalgarden:14434:23) at Generator.next (&lt;anonymous&gt;) at fulfilled (plugin:digitalgarden:65:24)</pre>0x46A2B93D

Hexadecimal to Binary

  1. Convert each hexadecimal digit into 4 binary digits.
  2. Combine the binary groups.

Example: Hexadecimal:  0x1F4

Binary: 1=0001F=11114=0100

Result: 000111110100


A.4) Example: Hexadecimal Conversion (Decimal)

Decimal to Hexadecimal

  1. Divide the decimal number by 16.
  2. Record the remainder (this becomes a hex digit).
  3. Repeat with the quotient until it is 0.
  4. Write the hex digits in reverse order.

Example:
Decimal: 397

397÷16=24,remainder: 13(D)24÷16=1,remainder: 8(8)1÷16=0,remainder: 1(1)

Hexadecimal:  0x18D

Hexadecimal to Decimal

  1. Expand using powers of 16.
  2. Add up the results.

Example:
Hexadecimal:  0x18D

1×256+8×16+13×1=397

Decimal: 397



B) Understanding Pointers in C 🎦

B.1) What Are Pointers?

Note: I think pointers would be easier to understand if they were called adress_reading_variables

B.2) Why Are Pointers Useful?

Analogy: The Notebook


B.3) Data Types and Memory Usage

Memory Types:

Data Type Memory Usage
char 1 byte
int 4 bytes
float 4 bytes
double 8 bytes
long long 8 bytes

B.3.1) Strings and Pointers in C (Memory as an Array)

Memory as an Array


B.3.2) Arrays and Pointers


B.4) Declaring and Using Pointers

Declaration

Assignment

Dereferencing


What are Null Pointers?


B.5) Visualizing Pointers

Example 1: Pointer Assignment

int k = 5;
int *pk = &k;

**Example 2: Changing a Pointer's Target

int m = 4;
pk = &m;

B.6) Practical Tips (using Pointers)

  1. Always initialize pointers to NULL or a valid address.
  2. Be cautious when dereferencing pointers; ensure they point to valid memory.
  3. Understand the dual role of *:
    • As part of a type (e.g., int *p).
    • As the dereference operator (*p).

Common Pitfalls:

  1. Uninitialized Pointers:
    • May contain garbage values, leading to undefined behavior.
    • Always initialize with NULL or a valid address.
  2. Dereferencing Null Pointers:
    • Causes a segmentation fault.
  3. Misunderstanding Syntax:
    • Example: int *p1, p2;
      • p1 is a pointer to an integer.
      • p2 is an integer.

B.7) Summary: Pointers in C


C) Defining Custom Types🎦

C.1) Introduction to typedef


- **`old_name`**: The existing type you want to rename.
- **`new_name`**: The alias or shorthand name.

**Example: Basic Usage**
```c
typedef unsigned char byte;

Why is this useful?


C.3) typedef with Structures

Structures often have verbose type names like struct car. Using typedef, you can simplify this.

Example: Two-Step typedef

  1. Define the structure:

    struct car {
        char* make;
        char* model;
        int year;
    };
    
  2. Create a shorthand alias:

    typedef struct car car_t;
    

Benefits:


C.4) Combining typedef and Structure Definition

Instead of defining the structure and aliasing it separately, you can do it in one step.

Syntax:

typedef struct {
    // Fields of the structure
    char* make;
    char* model;
    int year;
} car_t;

Example in Action:

car_t my_car; // Declare a variable of type car_t
my_car.make = "Toyota";
my_car.model = "Corolla";
my_car.year = 2023;

C.5) Advantages of typedef with Structures

  1. Shorter Code:

    • Instead of writing struct car repeatedly, you can use car_t.
  2. Improved Readability:

    • Makes the code more concise, especially when combined with malloc:

      car_t* car_ptr = malloc(sizeof(car_t));
      

      This is much cleaner than:

      struct car* car_ptr = malloc(sizeof(struct car));
      
  3. Convenience:

    • As your programs grow, you’ll define and use more complex data types. typedef keeps things manageable.

6. Summary: using typedef and struct

Use typedef to simplify your code and make working with complex types more intuitive.


D) Dynamic Memory Allocation in C (using Pointers like a Pro)🎦

D.1) Introduction: Static vs Dynamic memory allocation


D.1.2) Memory Layout: Stack vs. Heap

Visual Representation:

|---------------------------|  <-- Higher Memory
|       Heap (grows down)   |  <-- Dynamically Allocated Memory
|---------------------------|
|     Function Call Data    |  <-- Stack (grows up)
|---------------------------|
         Lower Memory


D.2) Using malloc to Allocate Memory

Syntax:

void* malloc(size_t size);
int* px = malloc(sizeof(int)); // Allocates 4 bytes for an int
if (px == NULL) {
    printf("Memory allocation failed!\n");
    return 1; // Exit program
}

D.2.1) Calculating Memory Size (sizeof)


D.2.2) Freeing Memory with free


D.3) Practical Examples

Allocating and Using an Integer:

int* px = malloc(sizeof(int)); // Allocate memory for an integer
if (px == NULL) {
    return 1; // Exit if memory allocation fails
}
*px = 42; // Assign 42 to the dynamically allocated memory
printf("%d\n", *px); // Output: 42
free(px); // Release memory

Allocating an Array:

int n = 5; // Number of elements
float* arr = malloc(n * sizeof(float)); // Allocate memory for an array of 5 floats
if (arr == NULL) {
    return 1; // Exit if memory allocation fails
}
for (int i = 0; i < n; i++) {
    arr[i] = i * 1.1; // Initialize array
}
for (int i = 0; i < n; i++) {
    printf("%.2f\n", arr[i]); // Print array elements
}
free(arr); // Release memory

D.4) Memory Leaks


D.5) Common Pitfalls

  1. Dereferencing NULL pointers:

    int* px = malloc(sizeof(int));
    if (px == NULL) {
        *px = 10; // Error: Dereferencing a NULL pointer
    }
    
  2. Double freeing:

    int* px = malloc(sizeof(int));
    free(px);
    free(px); // Error: Double free
    
  3. Using freed memory:

    int* px = malloc(sizeof(int));
    free(px);
    *px = 10; // Error: Accessing freed memory
    

D.6) Example: Dynamic Memory Allocation Workflow

  1. Allocate memory:

    pointer = malloc(size);
    
  2. Check for NULL:

    if (pointer == NULL) {
        // Handle error
    }
    
  3. Use the memory:

    *pointer = value; // Access or modify the memory
    
  4. Free the memory:

    free(pointer);
    

D.7) Summary: dynamic memory allocation


E) Understanding the Call Stack in C 🎦

1. What is the Call Stack?

2. Function Frames in the Call Stack

3. How Does Recursion Work with the Call Stack?

4. Illustration with Factorial Function

Let’s walk through the process of calculating the factorial of 5 using recursion.

Code Example:

int fact(int n) {
    if (n == 1) return 1;        // Base case
    return n * fact(n - 1);      // Recursive step
}

int main() {
    printf("%d\n", fact(5));     // Calls fact(5)
    return 0;
}

Step-by-Step Execution:

  1. main calls printf, which calls fact(5).

    • A new frame for fact(5) is created and becomes the active frame.
  2. Inside fact(5), the value of n is checked:

    • n != 1, so it computes 5 * fact(4) and calls fact(4).
    • fact(5)’s frame is now paused, and fact(4) gets a new frame.
  3. This process repeats:

    • fact(4) calls fact(3), pausing itself.
    • fact(3) calls fact(2).
    • fact(2) calls fact(1).
  4. Base case: fact(1) returns 1:

    • fact(1)’s frame is popped off the stack.
    • fact(2) resumes and computes 2 * 1 = 2. It returns 2.
  5. Unwinding the Stack:

    • fact(3) resumes, computes 3 * 2 = 6, and returns 6.
    • fact(4) resumes, computes 4 * 6 = 24, and returns 24.
    • fact(5) resumes, computes 5 * 24 = 120, and returns 120.
  6. Completion:

    • printf prints 120 and completes.
    • main finishes execution, and the program terminates.

Visualizing the Call Stack

At each step, the call stack looks like this:

  1. Initial call to fact(5):

    | fact(5) |  <-- Active frame
    | main    |
    
  2. After calling fact(4):

    | fact(4) |  <-- Active frame
    | fact(5) |
    | main    |
    
  3. After reaching fact(1):

    | fact(1) |  <-- Active frame
    | fact(2) |
    | fact(3) |
    | fact(4) |
    | fact(5) |
    | main    |
    
  4. During unwinding (after fact(1) returns):

    | fact(2) |  <-- Active frame
    | fact(3) |
    | fact(4) |
    | fact(5) |
    | main    |
    
  5. Final stack after all functions return:

    | main    |  <-- Active frame
    

Key Takeaways

  1. The call stack manages recursive calls by keeping track of function states.

    • Each call adds a frame to the stack.
    • Each return removes a frame from the stack.
  2. Recursion works because inactive frames "pause" until their called functions complete.

    • Each frame resumes exactly where it was paused.
  3. Stack Overflow Risk:

    • Too many recursive calls can exceed the memory allocated to the call stack, causing a stack overflow.

This structured explanation should make the concept of the call stack and recursion clearer! Let me know if you'd like further clarification.


F) File Pointers 🎦

Introduction to File I/O


Basic File Operations

  1. Opening a File (fopen)
    • Syntax:
      FILE *ptr = fopen("filename", "mode");
      
    • Modes:
      • "r": Open for reading. File must exist.
      • "w": Open for writing. Overwrites if file exists; creates a new file otherwise.
      • "a": Open for appending. Data is written to the end of the file.
    • Always check if fopen was successful:
      if (ptr == NULL) {
          printf("Error opening file.\n");
          return 1;
      }
      

  1. Closing a File (fclose)
    • Syntax:
      fclose(ptr);
      
    • Ensures resources are released and data is written properly.

Reading and Writing Data

1. Character-by-Character

2. Reading and Writing Strings

3. Block Data with fread and fwrite

4. Example: Copying a File (Simulating cp)

FILE *src = fopen("source.txt", "r");
FILE *dest = fopen("destination.txt", "w");

if (src == NULL || dest == NULL) {
    printf("Error opening file.\n");
    return 1;
}

char ch;
while ((ch = fgetc(src)) != EOF) {
    fputc(ch, dest);
}

fclose(src);
fclose(dest);

Advanced File Functions

  1. fprintf:

    • Works like printf, but writes to a file.
    • Example:
      fprintf(ptr, "Age: %d\n", age);
      
  2. fseek:

    • Moves the file pointer to a specific location.
    • Syntax:
      fseek(ptr, offset, origin);
      
    • Origins:
      • SEEK_SET: Beginning of the file.
      • SEEK_CUR: Current position.
      • SEEK_END: End of the file.
  3. ftell:

    • Returns the current position of the file pointer.

    • Example:

      long position = ftell(ptr);
      
  4. feof:

    • Checks if the end of the file is reached.
      if (feof(ptr)) {
          printf("End of file reached.\n");
      }
      
  5. ferror

    • Checks if an error occurred during file operations.

      if (ferror(ptr)) {
          printf("File error occurred.\n");
      }
      

Best Practices

  1. Always check the result of fopen:
    • Handle cases where the file does not exist or cannot be opened.
  2. Close files when done:
    • Use fclose to release system resources.
  3. Avoid mixing reading and writing on the same file pointer:
    • Open separate pointers for reading and writing if necessary.
  4. Use ferror and feof:
    • Handle errors and detect end of file gracefully.

🔙 Previous Part | Next Part 🔜

↩️ Go Back


Z) 🗃️ Glossary

File Definition
Uncreated files Origin Note