C Interview Questions 01: Pointers & Endianness

Function pointer, const int *ptr, endianness, & undefined behavior

Yu-Cheng (Morton) Kuo
Nerd For Tech

--

Outline

(0) Warm-Up
(1)
Function Pointer VS. Function Which Retruns a Pointer
(2)
Endianness
(3)
uint8_t & uint32_t Conversion with Pointer / Endianness
(4)
const keyword application
(5)
*(a + 1) & (*p - 1)
(6) Pointer to Pointer (Double Pointer)
(7)
Function Pointer Example
(8)
const int* p / int* const q
(9) char const *p / char* const p
(10) Explain Const
(11) References

(0) Warm-Up

0–1 Pointers

* // dereference operator
& // address-of operator

// 1. Declare and initialize a pointer to an integer
int *ptr; // Declare the pointer 'ptr' which will point to an integer
ptr = &var; // Initialize 'ptr' with the address of the integer variable 'var'

// 2. Combine the declaration and initialization above into one line
int *ptr = &var; // 'ptr' is a pointer to an integer, initialized with the address of the integer variable 'var'

// 3. Declare and initialize function pointers
void (*fptr)(type_a, type_b) = &func; // 'fptr' is a pointer to a function that takes 'type_a' and 'type_b' as arguments and returns void. It is initialized with the address of the function 'func'.
int (*a)(int); // Also a function pointer

Now, let’s look into pointers in more detail.

int a; // An integer.
int *a; // A pointer to an integer.
int **a; // A pointer to a pointer, which points to an integer.

int a[10]; // An array of 10 integers.
int *a[10]; // An array of 10 pointers, each pointing to an integer.
int (*a)[10]; // A pointer to an array of 10 integers.

int (*a)(int); // A pointer to a function that has one integer parameter and returns an integer.
int (*a[10])(int); // An array of 10 pointers, each pointing to a function that has one integer parameter and returns an integer.

0–2 Pointer to pointer

Figure: Illustrations of pointer to pointer. Retrieved from GeeksforGeeks

0–3 Undefined behavior

Figure: Undefined behavior. Retrieved from Impact of undefined behavior on performance.

Let’s see a few instances:

#include <stdio.h>
#include <stdint.h>
#include <limits.h>
#include <stdbool.h>

int main() {

int x = INT_MAX; // Defined in <limits.h>
printf("%d\n", x);
printf("%d\n", x + 1); // UB: Accessing beyond limit of signed int

bool y; // Defined in <stdbool.h>
printf("\n%d\n\n", y); // UB: Accessing an uninitialized variable

int a[10] = {0};
printf("%d\n", a[9]);
printf("%d\n", a[10]); // UB: Accessing an uninitialized variable

return 0;
}

The output:

2147483647
-2147483648

0

0
16

(1) Function Pointer VS. Function Which Retruns a Pointer

Q: Compare and explain these 2 lines of code.

int (*fun)(char); 
int *fun(char);

Ans:

// The 1st line
int (*fun)(char);
// It's a pointer to a function, or a function pointer
// A function that takes a single char argument and returns an int
// The 2nd line
int *fun(char);
// A function which retruns a pointer to an int

Usage Example 01: Function pointer

#include <stdio.h>

// Assume type_a is int and type_b is float
typedef int type_a;
typedef float type_b;

// Function matching the signature
void func(type_a a, type_b b) {
printf("Function called with %d and %f\n", a, b);
}

int main() {
// Function pointer declaration and initialization
void (*fptr)(type_a, type_b) = func;
// void (*fptr)(type_a, type_b) = &func;
// Remark: These two ways are equivalent

// Use the function pointer
type_a a = 5;
type_b b = 3.14;
fptr(a, b); // Call the function

return 0;
}

Another function pointer instance:

int exampleFunction(char c) {
return (int)c; // Cast char to int
}

int main() {
int (*fun)(char) = exampleFunction; // Assign the function to the pointer
int result = fun('A'); // Use the function pointer
return 0;
}

Usage Example 02: Function returning a pointer

#include <stdio.h>

int* func(char c) {
static int value; // Declare static int
value = (int)c; // Assign the value at runtime
return &value; // Return pointer to int
}

int main() {
int *result = func('A'); // Calling the function with 'A'

// You can use *result to access the value returned by func
printf("Value pointed by result: %d\n", *result);

// Calling func again with a different character
int *result2 = func('B');
printf("Value pointed by result after second call: %d\n", *result);
printf("Value pointed by result2: %d\n", *result2);

return 0;
}

In summary, this distinction is crucial in C and has different uses in programming, especially when dealing with dynamic function calls or arrays of function pointers for callback mechanisms.

(2) Endianness

Figures: Illustrations of little-endianness & big endianness [8][9]
  • 2–1 Little endianness

[1] Instances: x86 architecture (including x86_64 or AMD64) architecture, macOS, ARM (predominant), Linux (typically, e.g., on x86/x86_64 systems) etc.

  • 2–2 Big endianness

[1] Instances: TCP/IP protocol, ARM (rare), Linux (rare), Modbus protocol, etc.

[2] Also known as network byte order. In this context, big-endian means that when multi-byte values are sent over the network, the most significant byte (the “big end”) is sent first.

[3] For example, if you have a 16-bit number (like a port number in TCP or IP), the higher-order byte is transmitted first, followed by the lower-order byte. This standardization ensures that data is interpreted consistently across different systems regardless of their native byte order (little-endian or big-endian).

[4] When programming for network communications, it’s common to use functions like htons() (host to network short), htonl() (host to network long), ntohs() (network to host short), and ntohl() (network to host long) to convert between the host's native byte order and the network byte order. These functions are essential for ensuring that data is correctly understood by different systems communicating over a network.

(3) uint8_t & uint32_t Conversion with Pointer / Endianness

Q: What are ptr_1, ptr_2, *ptr_1, & *ptr_2?

uint8_t arr[4] = {0x12, 0x34, 0x56, 0x78};
uint32_t *ptr_1 = (uint32_t*) &arr[0];
uint32_t *ptr_2 = (uint32_t*) arr[0];

Ans:

#include <stdio.h>
#include <stdint.h>

int main() {
uint8_t arr[4] = {0x12, 0x34, 0x56, 0x78};
uint8_t arr_2[4] = {0x11, 0x22, 0x33, 0x44};

// Print the addresses of arr[0], arr[1], arr[2], arr[3]
printf("Address of arr : %p\n", (void*)&arr);
printf("Address of arr[0]: %p\n", (void*)&arr[0]);
printf("Address of arr[1]: %p\n", (void*)&arr[1]);
printf("Address of arr[2]: %p\n", (void*)&arr[2]);
printf("Address of arr[3]: %p\n\n", (void*)&arr[3]);

printf("Address of arr_2 : %p\n", (void*)&arr_2);
printf("Address of arr_2[0]: %p\n", (void*)&arr_2[0]);
printf("Address of arr_2[1]: %p\n", (void*)&arr_2[1]);
printf("Address of arr_2[2]: %p\n", (void*)&arr_2[2]);
printf("Address of arr_2[3]: %p\n\n", (void*)&arr_2[3]);

// Casting (= explicit conversion) [cf. implicit conversion]
uint32_t *ptr_1 = (uint32_t*) &arr[0];
uint32_t *ptr_2 = (uint32_t*) arr[0];

// Print the addresses stored in ptr_1 and ptr_2
printf("Address in ptr_1: %p\n", ptr_1);
printf("Address in ptr_2: %p\n", ptr_2);
// printf("Address in ptr_1: %p\n", (void*)ptr_1);

// Print the value pointed to by ptr_1
// Note: Dereferencing ptr_1 is safe because it points to a valid memory location
printf("Value pointed to by ptr_1: 0x%08X\n", *ptr_1);

// Note: Dereferencing ptr_2 is unsafe and can lead to undefined behavior
// printf("Value pointed to by ptr_2: 0x%08X\n", *ptr_2);

/*
For cross-platform code, it's best to use fixed-width integer types
(like uint32_t, int64_t, etc.) from <cstdint> (C++11 and later) or
<stdint.h> (in C99) if you need specific sizes.
*/
return 0;
}

Above is the corresponding complete code; below is the output. From the addresses of arr[0], arr[1], arr[2], arr[3], we see that my Windows 10 laptop is little-endian.

Note that arr locates just above arr_2 in terms of address, which comply with the fact that the arrays are stored on the stack of the memory layout of C.

Address of arr   : 000000000061FE0C
Address of arr[0]: 000000000061FE0C
Address of arr[1]: 000000000061FE0D
Address of arr[2]: 000000000061FE0E
Address of arr[3]: 000000000061FE0F

Address of arr_2 : 000000000061FE08
Address of arr_2[0]: 000000000061FE08
Address of arr_2[1]: 000000000061FE09
Address of arr_2[2]: 000000000061FE0A
Address of arr_2[3]: 000000000061FE0B

Address in ptr_1: 000000000061FE0C
Address in ptr_2: 0000000000000012
Value pointed to by ptr_1: 0x78563412

So,

  • [A] ptr_1 = 000000000061FE0C

ptr_1 corresponds to &arr[0], which is the address of arr[0]. And arr[0] equals 0x12.

  • [B] ptr_2 = 0000000000000012

ptr_2 corresponds to arr[0], which is 0x12.

  • [C] *ptr_1 = 0x78563412
uint32_t *ptr_1 = (uint32_t*) &arr[0];

Here comes the tricky one. *ptr_1 is a unsigned 32-bit int start at the address 000000000061FE0C, which is the address of arr[0]. So, *ptr_1 should intuitively be 0x12345678. However, since we have a 32-bit int consists of a array of four 1-byte uint8_t, endianness comes into play.

As my system is little-endain, the output is 0x78563412. On the contrary, for a big-endian system, it will be 0x12345678.

  • [D] *ptr_2

Undefined behavior!

  • This line casts the value of arr[0] (which is 0x12) to a uint32_t pointer.
  • Therefore, ptr_2 is being assigned to point to the memory address 0x00000012.
  • This is likely unintended and could lead to undefined behavior, as it points to a low memory address that the program probably doesn’t have valid access to. This line of code is almost certainly a bug and not what was intended.

(4) const keyword application

Q: Identify what the const keyword applies to in each declaration.

int *ptr;
const int *ptr;
int const *ptr;
int *const ptr;
const int *const ptr;

Ans:

int *pointerToInt;
const int *pointerToConstantInt;
int const *pointerToConstantInt;
int *const constantPointerToInt;
const int *const constantPointerToConstantInt;

(5) *(a + 1) & (*p - 1)

Q: The value of *(a + 1), (*p - 1)?

int a[5] = {1, 2, 3, 4, 5};
int *p = (int *)(&a + 1);

Ans:

#include <stdio.h>
#include <stdint.h>

int main() {

int a[5] = {1, 2, 3, 4, 5};
int *z = (int *)(&a);
int *p = (int *)(&a + 1);

printf("*(a + 1): %d\n", *(a + 1));
printf("(*z): %d\n", *(z));
printf("(*p): %d\n", *(p));
printf("(*p - 1): %d\n", (*p - 1));

// Print the addresses and values
printf("\nAddress of a: %p\n", (void*)a);
printf("Address of a[4]: %p\n", (void*)&a[4]);
printf("Address of a[1]: %p\n", (void*)&a[1]);
printf("Address of (a + 1): %p\n", (void*)(a + 1));

printf("\nAddress that z points to: %p\n", (void*)z);
printf("Address that p points to: %p\n", (void*)p);

return 0;
}

The output goes:

*(a + 1): 2
(*z): 1
(*p): 0
(*p - 1): 5

Address of a: 000000000061FDF0
Address of a[4]: 000000000061FE00
Address of a[1]: 000000000061FDF4
Address of (a + 1): 000000000061FDF4

Address that z points to: 000000000061FDF0
Address that p points to: 000000000061FE04
  • *(a + 1): 2

The 1 in *(a + 1) is a unit of the data type if a, which is a int.

  • (*p - 1)

Undefined behavior!

Since “int *p = (int *)(&a + 1);” make p = 000000000061FE04, which goes beyond the array bounds, *p is a undefined behavior. So, the 1 in “int *p = (int *)(&a + 1);” denotes jumping an array away.

(6) Pointer to Pointer (Double Pointer)

Q: Rewrite a line of code.

// Rewrite this line code into 2 lines of code below
void(*(*papf)[3])(char *);

// The alternative code
typedef__________;
pf (*papf)[3];

Ans:

// Original code: A pointer to an array of 3 pointers to functions taking a char* and returning void
void (*(*papf)[3])(char *);

// The alternative code using typedef
typedef void (*pf)(char *); // Define 'pf' as a type for a pointer to a function taking a char* and returning void
pf (*papf)[3]; // Declare 'papf' as a pointer to an array of 3 'pf' function pointers

typedef void (*pf)(char *); defines pf as a type that is a pointer to a function that takes a char * argument and returns nothing (void).

The usual simpler uses of typedef might involve directly renaming a basic type, like typedef int Integer; where Integer becomes an alias for int. In this case, the typedef is used to create an alias for a more complex type — a pointer to a function — to make further declarations easier and more readable.

(7) Function Pointer Example

Q: Please write a piece of code to declare a function pointer. Declaration syntax: return_type (*func_pointer)(parameter list);

Ans:

int add(int n1, int n2){ 
return n1 + n2;
}

int main() {
int (*fptr)(int, int);
fptr = add;

int num1 = 10, num2 = 30;
printf("num1 + num2 is : %d\n", fptr(num1, num2));

return 0;
}

(8) const int* p / int* const q

Q: What is the difference between const int* p and int* const q?

Ans:

  • For the former: The data type pointed to is const.
  • For the latter: The pointer itself is const.

(9) char const *p / char* const p

Q: Exaplain these lines of code.

char const *p;
char* const p;
void (*fp)();

Ans:

char const *p; // a pointer to a constant character.
char* const p; // a constant pointer to a character.
void (*fp)(); // a pointer to a function with no specified parameters and no return value.

(10) Explain Const

In the C language, `const` is used to define constants. Variables defined with `const` must be initialized when they are defined; otherwise, they will hold a random value, and their value cannot be changed after definition. `const` can indicate different things in the use of pointers: whether the data pointed to by the pointer is `const`, whether the pointer itself is `const`, or both:

- `int *a;` /* non-const pointer, non-const data */
- `const int a;` /* const data, `a` is a constant integer */
- `int const a;` /* const data, `a` is a constant integer */
- `const int *a;` /* non-const pointer, const data, `a` is a pointer to a constant integer (i.e., the integer is immutable, but the pointer can be modified) */
- `int const *a;` // Same as the last line above
- `int *const a;` /* const pointer, non-const data, `a` is a constant pointer to an integer (i.e., the integer pointed to by the pointer can be modified, but the pointer itself cannot be modified) */
- `int const *const a;` /* const pointer, const data, `a` is a constant pointer to a constant integer (i.e., both the integer pointed to and the pointer itself are immutable) */
- `const int *const a;` /* const pointer, const data, `a` is a constant pointer to a constant integer (i.e., both the integer pointed to and the pointer itself are immutable) */

(11) References

  1. 蔡文龍、何嘉益、張志成、張力元、歐志信、陳士傑(2021)。C & C++程式設計經典(第五版)。台北:碁峯資訊。
  2. 劉邦鋒(2019)。由片語學習C程式設計(第二版)。台北:國立臺灣大學出版中心。
  3. My interview experience [2023/11/30]
  4. C/C++ — 常見 C 語言觀念題目總整理(適合考試和面試) [2017]
  5. Classic C Interview Questions [2016]
  6. [面試] 聯發科技 MTK (內含考題) [2011]
  7. Why does C/C++ allow Undefined Behavior? [2021]
  8. Big Endian and Little Endian in Memory
  9. Mips memory layout
  10. ChatGPT-4

--

--

Yu-Cheng (Morton) Kuo
Nerd For Tech

CS/DS blog with C/C++/Embedded Systems/Python. Embedded Software Engineer. Email: yc.kuo.28@gmail.com