10 Pointers
1. Pointer Arithmetic
Pointer arithmetic is a crucial topic in C, particularly in embedded systems where direct memory manipulation is often required. Here, we'll delve into how to perform arithmetic on pointers and the implications of doing so.
1.1 Basic Arithmetic Operations
Addition
When you add an integer to a pointer, the resulting pointer points to a location that is n
elements ahead.
int arr[] = {1, 2, 3, 4, 5};
int *p = arr; // points to the first element, 1
p = p + 2; // Now points to the third element, 3
Subtraction
Subtracting an integer from a pointer points to a location that is n
elements behind.
p = p - 1; // Now points back to the second element, 2
Comparisons
You can compare pointers using relational operators (<
, >
, ==
, !=
, <=
, >=
).
if(p > arr) {
// Do something
}
1.2 Pointer Type and Memory Size
The actual amount the pointer advances in memory when you perform arithmetic is dependent on the type of data the pointer is pointing to.
int *pInt;
pInt + 1; // Advances 4 bytes (on most systems)
char *pChar;
pChar + 1; // Advances 1 byte
1.3 Pointer Difference
In C, you can calculate the difference between two pointers using the -
operator. This is useful for finding the length of an array.
int arr[] = {1, 2, 3};
int *p1 = &arr[0];
int *p2 = &arr[2];
int length = p2 - p1; // length will be 2
1.4 Bounds and Limitations
Always ensure that you do not go out of bounds while performing pointer arithmetic, as that leads to undefined behavior.
int arr[] = {1, 2, 3};
int *p = arr;
p = p + 4; // Out of bounds!
1.5 Caution in Embedded Systems
In embedded systems, pointer arithmetic can be even more risky due to direct hardware access and limited memory. Always make sure your pointers are within valid bounds before dereferencing them.
2. Far and Near Pointers
In some embedded systems, particularly those with segmented memory architectures, you'll come across the terms "far" and "near" pointers. These qualifiers specify the scope of the pointer in relation to memory access.
2.1 Near Pointers
A "near" pointer is limited to addressing memory within a specific segment. This often corresponds to the default data segment of a program. Near pointers are typically 16-bit in size and are faster and require less memory, making them suitable for smaller, less complex embedded systems.
2.2 Far Pointers
A "far" pointer, on the other hand, can address memory across different segments. Far pointers are usually 32-bit and can point to any location in the memory. However, they are slower and consume more memory, which might be a concern in resource-constrained systems.
2.3 Syntax
The specific syntax for declaring these pointers can vary depending on the compiler and architecture, but here's a general idea:
near int *nearPtr; // Declare a near pointer
far int *farPtr; // Declare a far pointer
2.4 Conversions
It is possible to convert a near pointer to a far pointer and vice versa, but caution is advised. Doing this incorrectly can lead to undefined behavior. Always consult the documentation specific to your system and compiler.
farPtr = (far int *) nearPtr; // Unsafe without proper checks
2.5 Considerations in Embedded Systems
- Memory Footprint: Far pointers consume more memory, which could be a limitation in constrained environments.
- Speed: Near pointers are generally faster, but their range is limited.
- Portability: Code using far and near pointers is less portable and might require adjustments when shifting to a different architecture.
3. Use Cases in Embedded Systems
3.1 Direct Memory Access
Pointers are indispensable in embedded systems when you need to control hardware directly through memory-mapped I/O. Here, you use pointers to read from and write to specific memory addresses mapped to hardware registers.
volatile uint32_t *register_address = (uint32_t *)0x40021018; // Example address
*register_address |= 1 << 5; // Set the 5th bit to enable a peripheral, for instance
3.2 Function Callbacks
In event-driven embedded systems or real-time operating systems (RTOS), function pointers are commonly used for callbacks. This allows you to design more modular and extensible software.
// Define a function pointer type for a callback
typedef void (*callback_t)(int);
// Function that takes a callback
void trigger_event(callback_t callback, int arg) {
callback(arg);
}
// A sample callback function
void my_callback(int value) {
// Do something with 'value'
}
3.3.Buffer Management
Embedded systems often have to manage data buffers, like in UART or SPI communication, for example. Pointers offer a way to dynamically manage these buffers.
uint8_t rx_buffer[256];
uint8_t *rx_read_ptr = rx_buffer; // Read pointer
uint8_t *rx_write_ptr = rx_buffer; // Write pointer
void buffer_write(uint8_t data) {
*rx_write_ptr = data;
++rx_write_ptr;
if(rx_write_ptr >= (rx_buffer + sizeof(rx_buffer))) {
rx_write_ptr = rx_buffer; // Loop back to the start
}
}
uint8_t buffer_read() {
uint8_t data = *rx_read_ptr;
++rx_read_ptr;
if(rx_read_ptr >= (rx_buffer + sizeof(rx_buffer))) {
rx_read_ptr = rx_buffer; // Loop back to the start
}
return data;
}
4. Q&A
1. Question: What is a pointer in C?
Answer: A pointer is a variable that stores the memory address of another variable. It allows for direct access to memory locations, facilitating dynamic memory allocation, array manipulations, and interaction with complex data structures.
2. Question:
How do you declare a pointer to an integer and initialize it to point to an integer variable x
?
Answer: You can declare and initialize a pointer to an integer as follows:
int x = 10;
int *ptr = &x;
Here, ptr
is a pointer pointing to the variable x
.
3. Question:
What does the operation ptr++
do, given that ptr
is a pointer to an integer?
Answer:
When ptr++
is executed, the pointer ptr
is incremented to point to the next integer in memory. This means it's incremented by sizeof(int)
bytes, typically 4 bytes on many platforms.
4. Question: What are near and far pointers?
Answer: In the context of 16-bit architectures (like the early x86), memory was segmented.
-
Near Pointer: Points to data in the same segment as the pointer itself, and only the offset is stored.
-
Far Pointer: Can point to data in any segment and stores both the segment and the offset.
5. Question:
What's the difference between *ptr++
and (*ptr)++
given that ptr
is a pointer to an integer?
Answer:
- *ptr++
: Increments the pointer ptr
to point to the next integer location, then dereferences the original address.
- (*ptr)++
: Dereferences the pointer first, increments the integer value it points to, and leaves the pointer itself unchanged.
6. Question:
Given the declaration int **ppi;
, what does ppi
represent?
Answer:
ppi
is a pointer to a pointer of an integer. It means it can store the address of another pointer that points to an integer.
7. Question: How can pointers be beneficial in embedded systems?
Answer: Pointers are particularly beneficial in embedded systems for: - Dynamic memory allocation, even though it's used judiciously in embedded systems. - Implementing data structures like linked lists and trees. - Directly accessing hardware registers or memory-mapped I/O. - Efficient array and string manipulations.
8. Question:
Consider the following declaration: char (*ptr)[5];
. What is ptr
?
Answer:
ptr
is a pointer to an array of 5 characters. It's not a pointer to a single char nor an array of pointers to char.
9. Question: In the context of embedded systems, what are the potential risks of using pointers?
Answer: Risks include: - Dangling pointers: Pointers pointing to memory that has been freed or gone out of scope. - Wild pointers: Pointers that have not been initialized and point to some arbitrary memory location. - Memory leaks: Dynamically allocated memory that isn't freed. - Overstepping array bounds, which can corrupt data or cause crashes.
10. Question:
Consider the declaration int (*fun_ptr)(int, int);
. What is fun_ptr
?
Answer:
fun_ptr
is a pointer to a function that takes two integers as arguments and returns an integer.
11. Question:
Consider the following code:
char *ptr1 = "Hello";
char ptr2[] = "Hello";
Can you modify ptr1[0]
? How about ptr2[0]
? Why?
Answer:
No, you cannot modify ptr1[0]
because ptr1
points to a string literal, which is stored in read-only memory. Attempting to do so results in undefined behavior.
Yes, you can modify ptr2[0]
because ptr2
is an array initialized with the string, and it's stored in read-write memory.
12. Question:
Given the following code:
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr + 2;
What is the value of *ptr
and what does ptr - arr
yield?
Answer:
*ptr
is 3
. The expression ptr - arr
yields 2
, representing the difference in array indices pointed by ptr
and arr
.
13. Question:
How do you pass a pointer to a function in such a way that changes to the pointer inside the function do not affect the original pointer?
Answer:
You would pass a pointer to the pointer. However, modifying the pointer-to-pointer will not change the original pointer, but the data pointed to can still be changed.
Example:
void func(int **pp) {
int y = 5;
*pp = &y; // Changes where pp points, but not the original pointer outside the function
}
int main() {
int x = 10;
int *px = &x;
func(&px);
// px still points to x here
}
14. Question:
What is wrong with the following code?
int* function() {
int x = 10;
return &x;
}
Answer:
The function returns a pointer to a local variable x
. Once the function completes, x
goes out of scope, and the pointer returned will be a dangling pointer, leading to undefined behavior if dereferenced.
15. Question:
For embedded systems where memory is a concern, when would you prefer using a pointer over an array?
Answer:
When the size of data isn't known at compile time or when you need to allocate memory dynamically based on certain conditions. Pointers provide more flexibility in such cases, whereas arrays have a fixed size defined at compile time.
16. Question:
Given the following snippet:
void foo(int arr[]) {
int size = sizeof(arr) / sizeof(arr[0]);
printf("%d", size);
}
Why might the function not print the expected size of the array?
Answer:
When an array is passed to a function, it decays to a pointer. Thus, sizeof(arr)
will give the size of the pointer and not the size of the entire array. The method used in the function will not yield the correct size of the array.
17. Question:
Given the pointer definition int *(*p)[3];
, what does p
point to?
Answer:
p
is a pointer to an array of 3 integer pointers.
18. Question:
How would you ensure that a pointer passed to your function isn't modified inside the function, without using a pointer-to-pointer?
Answer:
You can use the const
qualifier to indicate that the pointer itself can't be modified.
Example:
void func(const int *ptr) {
// ptr can't be modified, but the content it points to can be
}
19. Question:
Consider the following:
int x = 5;
int *ptr = &x;
What will the following expressions yield: *ptr + 2
and *(ptr + 2)
?
Answer:
*ptr + 2
will yield 7
because it dereferences the pointer and adds 2 to the value pointed by ptr
.
*(ptr + 2)
will yield the value of the integer located two memory locations after x
, which is undefined in this context.
20. Question:
What is the potential problem with the following code in embedded systems?
int *ptr = (int *)malloc(10 * sizeof(int));
Answer:
This code dynamically allocates memory. In many embedded systems, dynamic memory allocation is discouraged due to limited memory and the risk of memory fragmentation, which can be problematic for long-running systems.
21. Question:
Given the following pointer declaration, what does it represent?
int (*ptr)[5];
Answer:
ptr
is a pointer to an array of 5 integers.
22. Question:
Can you explain the difference between these two declarations?
int *ptr[10];
int (*ptr)[10];
Answer:
The first declaration, int *ptr[10];
, represents an array of 10 pointers to integers.
The second declaration, int (*ptr)[10];
, represents a pointer to an array of 10 integers.
23. Question:
Given the declaration:
void (*fun_ptr)(int);
What does fun_ptr
represent, and how would you use it?
Answer:
fun_ptr
is a pointer to a function that takes an integer as an argument and returns void
. To use it:
void someFunction(int a) {
// Do something with a
}
fun_ptr = &someFunction;
fun_ptr(5); // Calls someFunction with 5 as an argument
24. Question:
What does the following pointer declaration represent?
char *(*ptr[10])(char *);
Answer:
ptr
is an array of 10 pointers to functions. Each function takes a pointer to a char (or a string) as an argument and returns a pointer to char (or a string).
25. Question:
Given this tricky declaration:
int *(*ptr_func(int))[3];
Can you decipher it?
Answer:
ptr_func
is a function that takes an integer as a parameter and returns a pointer to an array of 3 integer pointers.
26. Question:
What will the following code print?
int arr[] = {10, 20, 30, 40, 50};
int *p = arr;
++*p;
printf("%d", *p);
Answer:
It will print 11
. The ++*p
operation increments the value pointed by p
, which is the first element of the array.
27. Question:
For the following code, what does ptr
point to, and what will be the output?
char str[] = "hello";
char *ptr = str + 4;
printf("%c", *--ptr);
Answer:
ptr
initially points to the 'o' in "hello". After the decrement operation, it points to the 'l' before the 'o'. Thus, the output will be l
.
28. Question:
Why might one use a pointer to a pointer in embedded C? Give an example use-case.
Answer:
One reason is to manipulate an array of strings. Since a string can be represented as a pointer to char, an array of strings can be seen as a pointer to a pointer to char. Another use-case is to modify a pointer passed to a function from within that function.
Example:
void modifyPointer(int **pp) {
int y = 5;
*pp = &y;
}
int main() {
int x = 10;
int *px = &x;
modifyPointer(&px); // After this call, px points to y.
return 0;
}
29. Question:
Consider the declaration int **ptr;
. If you were to allocate memory for ptr
to point to an array of pointers, which of the following is correct?
1. ptr = (int **)malloc(5 * sizeof(int));
2. ptr = (int **)malloc(5 * sizeof(int *));
Answer:
The correct answer is 2. Since ptr
is intended to point to an array of integer pointers, each element of the array should have the size of an int pointer (int *
).
30. Question:
In embedded systems, what is the significance of the volatile
keyword when used with pointers?
Answer:
In embedded systems, the volatile
keyword tells the compiler that the object pointed to by the pointer can change at any time without any action being taken by the code the compiler finds. This is essential for memory-mapped peripheral registers, as their values might change asynchronously to the program flow. It prevents the compiler from optimizing out repeated accesses to that address, ensuring the value is read from memory every time.
31. Question:
Take a look at the following code snippet:
int *p;
*p = 5;
What's wrong with this code?
Answer:
The pointer p
is uninitialized and then dereferenced. This leads to undefined behavior as you're writing a value to an unknown memory location. Before dereferencing, p
should point to a valid memory location.
32. Question:
Inspect this code:
int arr[5];
int *p = arr + 5;
Is there any issue?
Answer:
While there isn't a direct error in this code, it's risky. The pointer p
is pointing just past the end of the array. While it's not being dereferenced here, using p
for reading/writing in the future could lead to undefined behavior.
33. Question:
Consider:
int x = 10;
int *p = &x;
free(p);
What's the problem here?
Answer:
p
points to a variable (x
) that is on the stack, not dynamically allocated memory. Calling free()
on a non-dynamically allocated memory leads to undefined behavior.
34. Question:
Examine:
int *allocateArray(int size) {
int arr[size];
return arr;
}
What's wrong?
Answer:
The function is returning a pointer to a local variable (arr
). This local variable will be destroyed once the function returns, leaving the pointer dangling and any dereference of it will be undefined behavior.
35. Question:
Review this snippet:
int *p = (int *)malloc(5 * sizeof(int));
p[5] = 10;
Spot the mistake.
Answer:
Array indexing starts from 0. Hence, p[5]
is accessing the memory just beyond the allocated space, resulting in undefined behavior.
36. Question:
Here's a code fragment:
char *str1 = "hello";
str1[0] = 'H';
What's the issue?
Answer:
str1
points to a string literal, which is stored in read-only memory. Attempting to modify it leads to undefined behavior.
37. Question:
Consider this:
int *p;
for (int i = 0; i < 5; i++) {
p = (int *)malloc(sizeof(int));
}
free(p);
Can you identify the flaw?
Answer:
Memory is allocated in each iteration of the loop, but only the last allocated memory is freed. This leads to a memory leak for the first four allocated blocks.
38. Question:
Look at the code below:
int *ptr = (int *)malloc(5 * sizeof(int));
if(ptr) {
// Some operations on ptr
}
free(ptr);
ptr = NULL;
free(ptr);
What's not right?
Answer:
After freeing ptr
, it's set to NULL and then free()
is called again on the NULL pointer. While calling free()
on a NULL pointer is safe and does nothing, it's unnecessary and can be confusing.
39. Question:
Here's a code segment:
int val = 10;
int *p1 = &val;
int *p2 = &val;
free(p1);
free(p2);
What's problematic?
Answer:
Both p1
and p2
point to a variable on the stack, and you shouldn't call free()
on them. Additionally, calling free()
on the same memory location more than once (double freeing) is undefined behavior.
40. Question:
Given the code:
int *foo() {
int x = 10;
return &x;
}
What's the mistake?
Answer:
The function foo
is returning the address of a local variable. Once the function exits, the stack memory where x
was stored will be reclaimed, making the returned pointer a dangling pointer, leading to undefined behavior when dereferenced.
41. Question:
Consider the following:
int *p = (int *)malloc(10 * sizeof(int));
memset(p, 0, 20);
What's the mistake?
Answer:
The memory allocated is for 10 integers, but the memset
function is called to set the memory of 20 bytes to zero. This will not cover all the memory allocated for the integers.
42. Question:
Examine this snippet:
int nums[5] = {1, 2, 3, 4, 5};
int *p = nums + 3;
int *q = nums + 8;
What's wrong?
Answer:
The pointer q
is pointing far beyond the memory of the nums
array. Accessing or dereferencing q
would lead to undefined behavior.
43. Question:
Check out the following:
int x = 5;
int *p = &x;
int *q;
*q = *p;
Can you spot the error?
Answer:
The pointer q
is uninitialized when it's being dereferenced, which leads to undefined behavior.
44. Question:
Consider this code:
int *func() {
int arr[5];
// Some operations on arr
return arr;
}
What's the issue?
Answer:
The function returns a pointer to a local array arr
, which will go out of scope once the function returns. This results in a dangling pointer.
45. Question:
Here's a segment:
int *arr = (int *)malloc(5 * sizeof(int));
if (arr[4] = 10) {
printf("Success");
}
What's wrong here?
Answer:
The issue is with the conditional check if (arr[4] = 10)
. This is an assignment, not a comparison. The correct check would be if (arr[4] == 10)
.
46. Question:
Examine this snippet:
double val = 10.5;
double *p = &val;
int *q = (int *)p;
What's problematic?
Answer:
The pointer q
is an integer pointer, but it's being assigned the address of a double. This type of casting can lead to undefined behavior when dereferencing q
.
47. Question:
Look at this:
int arr[5];
int *p = &arr[2];
p += 5;
Any issues?
Answer:
The pointer p
originally pointed to the third element in the arr
array. By adding 5, it now points beyond the array, which could lead to undefined behavior if dereferenced.
48. Question:
Consider:
int *p = (int *)malloc(10 * sizeof(int));
p++;
free(p);
Spot the flaw.
Answer:
After allocating memory, the pointer p
is incremented. When trying to free the memory, p
no longer points to the start of the allocated block. This will result in a memory leak and undefined behavior.
49. Question:
Given:
int nums[] = {1, 2, 3, 4, 5};
int *p = &nums[2];
p = p - 3;
What's the mistake?
Answer:
After the operations, pointer p
is pointing before the start of the nums
array, which can lead to undefined behavior if p
is accessed or dereferenced.
50. Question:
Inspect this code:
char *str = "Hello";
*(str + 1) = 'a';
What's the issue?
Answer:
The pointer str
is pointing to a string literal. Modifying string literals results in undefined behavior, as they are stored in read-only memory.