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
Table of Contents:
A) Understanding Hexadecimal and Its Use in Programming🎦
Video: https://youtu.be/u_atXp-NF6w
A.1) Introduction to Number Systems
Decimal (Base 10)
- Digits: 0 through 9.
- Place values:
, , , , etc. - Example:
.
Binary (Base 2)
- Digits: 0 and 1.
- Place values:
, , , , , etc. - Example:
Hexadecimal (Base 16)
- Digits: 0 through 9, followed by A through F (representing
to in decimal). - Place values:
, , , , etc. - Example:
0xADC
A.2) Why Hexadecimal?
Concise Representation of Binary
- 1 Hexadecimal digit = 4 Binary digits (bits).
- Example:
Human Readability
- Binary sequences like
are easier to read as 0xF7B
.
Prefix and Ambiguity:
- To distinguish hexadecimal from decimal, prefix hex numbers with
0x
:- Decimal:
- Hexadecimal:
0x397
- Decimal:
Common in Programming
- Memory addresses, color codes in graphics (e.g.,
0xFF5733
), compact binary representations.
A.3) Place Values in Hexadecimal
Hexadecimal values are analogous to decimal and binary:
Number System | Base | Place Values |
---|---|---|
Decimal | ||
Binary | ||
Hexadecimal |
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 (<anonymous>) at fulfilled (plugin:digitalgarden:65:24)</pre>0x46A2B93D
Hexadecimal to Binary
- Convert each hexadecimal digit into 4 binary digits.
- Combine the binary groups.
Example: 0x1F4
A.4) Example: Hexadecimal Conversion (Decimal)
Decimal to Hexadecimal
- Divide the decimal number by
. - Record the remainder (this becomes a hex digit).
- Repeat with the quotient until it is
. - Write the hex digits in reverse order.
Example:
0x18D
Hexadecimal to Decimal
- Expand using powers of
. - Add up the results.
Example:
0x18D
B) Understanding Pointers in C 🎦
Video: https://youtu.be/XISnO2YhnsY
B.1) What Are Pointers?
- A pointer is a data item whose value is a memory address.
- The type of a pointer determines what kind of data is stored at that address.
- Example:
int *pk;
declarespk
as a pointer to an integer. If dereferenced (*pk
), it points to an integer in memory.
Note: I think pointers
would be easier to understand if they were called adress_reading_variables
B.2) Why Are Pointers Useful?
- Pass by reference: Enables functions to modify variables directly, avoiding the need to return values or work with copies.
- Memory efficiency: Allows direct access to variables, avoiding unnecessary data duplication.
Analogy: The Notebook
- Without pointers: You make a copy of your notebook, pass it to someone for updates, and reconcile changes manually.
- With pointers: You hand over the original notebook for direct updates, saving time and effort.
B.3) Data Types and Memory Usage
Memory Types:
- RAM: Used for volatile data (data lost when the computer is turned off).
- Hard disk: Stores permanent data but cannot be manipulated directly.
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
- Think of memory as a large array of byte-sized cells.
- Each byte has an address (e.g.,
0x0
,0x1
,0x2
, etc.).
- A string in C is a sequence of characters (
char
) ending with a null terminator (\0
). - Example: The string
Lloyd
requires:- 5 bytes for characters (
L
,l
,o
,y
,d
) - 1 byte for
\0
- 5 bytes for characters (
- In C, a string is a
char *
(pointer to a character). - The size of a
char *
depends on the system:- 32-bit system: 4 bytes
- 64-bit system: 8 bytes
B.3.2) Arrays and Pointers
- Array names are pointers:
- The name of an array points to its first element.
- Example:
int arr[5] = {1, 2, 3, 4, 5}; int *p = arr; // p points to the first element of arr
B.4) Declaring and Using Pointers
Declaration
int *p;
declaresp
as a pointer to an integer.
Assignment
- Use the
&
(address-of) operator to store the address of a variable in a pointer.- Example:
int x = 5; int *p = &x; // p now holds the address of x
- Example:
Dereferencing
- Use the
*
(dereference) operator to access or modify the value at the address stored in a pointer.- Example:
*p = 10; // Changes the value of x to 10
- Example:
What are Null Pointers?
- A null pointer points to nothing.
- Always initialize pointers to a valid address or
NULL
to avoid accidental access to random memory.
B.5) Visualizing Pointers
Example 1: Pointer Assignment
int k = 5;
int *pk = &k;
- Memory Diagram:
k
: Stores5
at an address (e.g.,0x80C
).pk
: Points to the address ofk
(0x80C
).
**Example 2: Changing a Pointer's Target
int m = 4;
pk = &m;
- Result:
pk
now points tom
's address instead ofk
's.
B.6) Practical Tips (using Pointers)
- Always initialize pointers to
NULL
or a valid address. - Be cautious when dereferencing pointers; ensure they point to valid memory.
- Understand the dual role of
*
:- As part of a type (e.g.,
int *p
). - As the dereference operator (
*p
).
- As part of a type (e.g.,
Common Pitfalls:
- Uninitialized Pointers:
- May contain garbage values, leading to undefined behavior.
- Always initialize with
NULL
or a valid address.
- Dereferencing Null Pointers:
- Causes a segmentation fault.
- Misunderstanding Syntax:
- Example:
int *p1, p2;
p1
is a pointer to an integer.p2
is an integer.
- Example:
B.7) Summary: Pointers in C
- Pointers are addresses of memory locations.
- The
&
operator retrieves the address of a variable. - The
*
operator dereferences a pointer to access the value at its address. - Pointers are powerful tools for:
- Passing data between functions.
- Manipulating memory directly.
- Working with arrays and strings.
C) Defining Custom Types🎦
Video: https://youtu.be/96M4q0OnMfY
C.1) Introduction to typedef
- The
typedef
keyword in C allows you to create aliases for existing data types or user-defined types. - It simplifies cumbersome type names, making code shorter and easier to read.
- Syntax:
typedef old_name new_name;
- **`old_name`**: The existing type you want to rename.
- **`new_name`**: The alias or shorthand name.
**Example: Basic Usage**
```c
typedef unsigned char byte;
-
Before
typedef
:unsigned char a, b;
-
After
typedef
:byte a, b;
Why is this useful?
-
Reduces verbosity and makes the code cleaner.
-
Example: CS50 uses
typedef
to definestring
:typedef char* string;
- Instead of writing
char *
every time, you can usestring
.
- Instead of writing
C.3) typedef
with Structures
Structures often have verbose type names like struct car
. Using typedef
, you can simplify this.
Example: Two-Step typedef
-
Define the structure:
struct car { char* make; char* model; int year; };
-
Create a shorthand alias:
typedef struct car car_t;
Benefits:
-
Before
typedef
:struct car my_car;
-
After
typedef
:car_t my_car;
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
-
Shorter Code:
- Instead of writing
struct car
repeatedly, you can usecar_t
.
- Instead of writing
-
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));
-
-
Convenience:
- As your programs grow, you’ll define and use more complex data types.
typedef
keeps things manageable.
- As your programs grow, you’ll define and use more complex data types.
6. Summary: using typedef
and struct
-
typedef
creates aliases for data types. -
It works for:
- Basic types (e.g.,
unsigned char
tobyte
). - Complex user-defined types like structures.
- Basic types (e.g.,
-
Syntax:
typedef old_name new_name;
-
Combine with structures for cleaner and more readable code:
typedef struct { char* make; char* model; int year; } car_t;
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
- Dynamic memory allocation allows programs to request memory from the heap while the program is running.
- Contrast:
- Static memory: Allocated on the stack (e.g.,
int x = 5
). - Dynamic memory: Allocated on the heap using functions like
malloc
.
- Static memory: Allocated on the stack (e.g.,
- Why dynamic memory?
- Useful when you don’t know how much memory you’ll need at compile time (e.g., user inputs or linked lists).
D.1.2) Memory Layout: Stack vs. Heap
- Stack:
- Grows upwards in memory.
- Stores statically allocated variables and function call data.
- Heap:
- Grows downwards in memory.
- Used for dynamically allocated memory.
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);
- Allocates
size
bytes of memory on the heap and returns a pointer to the first byte. - Important: Always check if
malloc
returnsNULL
. - Example:
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
)
- Use
sizeof
to determine the number of bytes for a type. - Example:
float* arr = malloc(10 * sizeof(float)); // Allocates memory for an array of 10 floats
D.2.2) Freeing Memory with free
- Dynamically allocated memory must be released using the
free
function.
-
Syntax:
free(pointer);
-
Example:
free(px); // Releases memory allocated to px
-
Rules:
- Every memory block allocated with
malloc
must be freed. - Only free memory allocated with
malloc
. - Never free the same memory block twice.
- Every memory block allocated with
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
-
What is a memory leak?
- Occurs when dynamically allocated memory is not freed.
- Consequences:
- Reduces available memory for other programs.
- Slows down the system.
-
Example of a Memory Leak:
int* px = malloc(sizeof(int)); *px = 10; // Forgetting to call free(px) causes a memory leak
D.5) Common Pitfalls
-
Dereferencing NULL pointers:
int* px = malloc(sizeof(int)); if (px == NULL) { *px = 10; // Error: Dereferencing a NULL pointer }
-
Double freeing:
int* px = malloc(sizeof(int)); free(px); free(px); // Error: Double free
-
Using freed memory:
int* px = malloc(sizeof(int)); free(px); *px = 10; // Error: Accessing freed memory
D.6) Example: Dynamic Memory Allocation Workflow
-
Allocate memory:
pointer = malloc(size);
-
Check for NULL:
if (pointer == NULL) { // Handle error }
-
Use the memory:
*pointer = value; // Access or modify the memory
-
Free the memory:
free(pointer);
D.7) Summary: dynamic memory allocation
- Use
malloc
to dynamically allocate memory on the heap. - Always check if
malloc
returnsNULL
to handle allocation failures. - Free dynamically allocated memory using
free
to prevent memory leaks. - Follow the three golden rules:
- Everything
malloc
ed must befree
d. - Only
free
what youmalloc
. - Never
free
the same memory twice.
- Everything
E) Understanding the Call Stack in C 🎦
Video: https://youtu.be/aCPkszeKRa4
1. What is the Call Stack?
- The call stack is a mechanism that manages function calls in memory.
- Whenever a function is called, the system creates a stack frame (or function frame) for that function.
- A stack frame contains:
- Space for local variables.
- Space for calculations or operations specific to that function.
- A return address to know where to go back when the function completes.
2. Function Frames in the Call Stack
- When a function calls another function:
- The current function’s frame is pushed onto the stack.
- The new function gets its own frame on top of the stack and becomes the active frame.
- When a function completes:
- Its frame is popped off the stack.
- The frame just below it becomes the new active frame.
3. How Does Recursion Work with the Call Stack?
- Recursion relies on the call stack to manage multiple function calls.
- Each recursive call creates a new frame with its own parameters and local variables.
- The function executes until it either:
- Returns a value (base case).
- Calls itself again (recursive step).
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:
-
main
callsprintf
, which callsfact(5)
.- A new frame for
fact(5)
is created and becomes the active frame.
- A new frame for
-
Inside
fact(5)
, the value ofn
is checked:n != 1
, so it computes5 * fact(4)
and callsfact(4)
.fact(5)
’s frame is now paused, andfact(4)
gets a new frame.
-
This process repeats:
fact(4)
callsfact(3)
, pausing itself.fact(3)
callsfact(2)
.fact(2)
callsfact(1)
.
-
Base case:
fact(1)
returns 1:fact(1)
’s frame is popped off the stack.fact(2)
resumes and computes2 * 1 = 2
. It returns2
.
-
Unwinding the Stack:
fact(3)
resumes, computes3 * 2 = 6
, and returns6
.fact(4)
resumes, computes4 * 6 = 24
, and returns24
.fact(5)
resumes, computes5 * 24 = 120
, and returns120
.
-
Completion:
printf
prints120
and completes.main
finishes execution, and the program terminates.
Visualizing the Call Stack
At each step, the call stack looks like this:
-
Initial call to
fact(5)
:| fact(5) | <-- Active frame | main |
-
After calling
fact(4)
:| fact(4) | <-- Active frame | fact(5) | | main |
-
After reaching
fact(1)
:| fact(1) | <-- Active frame | fact(2) | | fact(3) | | fact(4) | | fact(5) | | main |
-
During unwinding (after
fact(1)
returns):| fact(2) | <-- Active frame | fact(3) | | fact(4) | | fact(5) | | main |
-
Final stack after all functions return:
| main | <-- Active frame
Key Takeaways
-
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.
-
Recursion works because inactive frames "pause" until their called functions complete.
- Each frame resumes exactly where it was paused.
-
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 🎦
Video: https://youtu.be/bOF-SpEAYgk
Introduction to File I/O
- Persistent Data: Data that exists beyond the lifetime of the program (e.g., saving user moves in a game).
- File Pointers: Used to work with files in C. Represented as
FILE*
.- Declared in the header file
<stdio.h>
.
- Declared in the header file
- File Operations:
- Opening (
fopen
): Establishes a connection to a file. - Closing (
fclose
): Closes the connection to the file. - Reading/Writing:
- Character by character:
fgetc
andfputc
. - Block of data:
fread
andfwrite
.
- Character by character:
- Additional utilities for file management (e.g.,
fprintf
,fseek
).
- Opening (
Basic File Operations
- 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; }
- Syntax:
- Closing a File (
fclose
)- Syntax:
fclose(ptr);
- Ensures resources are released and data is written properly.
- Syntax:
Reading and Writing Data
1. Character-by-Character
-
Reading a Character (
fgetc
)- Reads the next character from a file:
char ch = fgetc(ptr);
- Example: Stops when
EOF
(End of File) is reached.
- Reads the next character from a file:
-
Writing a Character (
fputc
)- Writes a single character to a file:
fputc('A', ptr);
- Writes a single character to a file:
2. Reading and Writing Strings
-
Reading a String (
fgets
):- Reads a line or a set number of characters into a buffer:
char buffer[100]; fgets(buffer, 100, ptr);
- Reads a line or a set number of characters into a buffer:
-
Writing a String (
fputs
):- Writes a string to a file:
fputs("Hello, World!", ptr);
- Writes a string to a file:
3. Block Data with fread
and fwrite
-
Use for efficient reading/writing of larger chunks of data.
-
Syntax:
-
fread
: Reads data from a file into memory.fread(buffer, size, count, ptr);
-
fwrite
: Writes data from memory to a file.fwrite(buffer, size, count, ptr);
-
-
Example:
int arr[10]; fread(arr, sizeof(int), 10, ptr); // Reads 10 integers fwrite(arr, sizeof(int), 10, ptr); // Writes 10 integers
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
-
fprintf
:- Works like
printf
, but writes to a file. - Example:
fprintf(ptr, "Age: %d\n", age);
- Works like
-
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.
-
ftell
:-
Returns the current position of the file pointer.
-
Example:
long position = ftell(ptr);
-
-
feof
:- Checks if the end of the file is reached.
if (feof(ptr)) { printf("End of file reached.\n"); }
- Checks if the end of the file is reached.
-
ferror
-
Checks if an error occurred during file operations.
if (ferror(ptr)) { printf("File error occurred.\n"); }
-
Best Practices
- Always check the result of
fopen
:- Handle cases where the file does not exist or cannot be opened.
- Close files when done:
- Use
fclose
to release system resources.
- Use
- Avoid mixing reading and writing on the same file pointer:
- Open separate pointers for reading and writing if necessary.
- Use
ferror
andfeof
:- Handle errors and detect end of file gracefully.
Z) 🗃️ Glossary
File | Definition |
---|
Uncreated files | Origin Note |
---|