Advanced C Language Pointer Techniques

  1. assert Macro for Debugging

The <assert.h> header file defines the assert() macro, which verifies that a specified condition holds true during program execution. If the condition is false, the program terminates with an error message. This macro is particularly useful for debugging purposes.

Here's how to use it:

assert(data_pointer != NULL);


When the program execution reaches this line, it checks if the variable data_pointer is not NULL. If the condition is satisfied, the program continues execution. Otherwise, the program halts and displays an error message indicating the failed assertion, along with the filename and line number.

The assert() macro accepts an expression as its parameter. If the expression evaluates to true (non-zero), assert() does nothing and the program proceeds. If the expression evaluates to false (zero), assert() reports an errer, writing a message to the standard error stream (stderr) that includes the expression that failed and the location of the assertion.

Using assert() offers several advantages for developers: it automatically identifies the file and line number where the error occurred, and it provides a mechanism to enable or disable assertions without modifying the code. Once you're confident that your program is functioning correctly and no longer need assertions, you can define the NDEBUG macro before including <assert.h>.

#define NDEBUG
#include<stdio.h>


When you recompile the program with this definition, the compiler will disable all assert() statements in the file. If you encounter issues later, simply remove the #define NDEBUG directive (or comment it out) and recompile to re-enable the assertions.

The primary drawback of assert() is that it adds runtime checks, which can impact program performance.

Typically, we use assert() in debug builds and disable it in release builds. In integrated development environments like Visual Studio, assert() statements are automatically optimized out in release builds. This approach allows developers to leverage assertions for problem identification during debugging while maintaining optimal performance in production releases.

  1. Pointer Usage and Address-Based Function Calls

2.1 Implementing a String Length Function

The library function strlen (found in <string.h>) calculates the length of a string by counting characters before the terminating null character ('\0').
The function prototype is:

size_t strlen ( const char * str );


Let's attempt to implement our own version:
Method 1 - Counter approach: The parameter str receives the starting address of a string. We then iterate through the string, counting characters until we encounter the null character.

Method 2 - Pointer subtraction: After receiving the starting address in str, we store it in str2, then traverse the string to find the '\0' position, returning the difference between the two pointers.

Code Example:

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

//Method 1: Counter approach
size_t calculate_string_length(const char* input)
{
	int character_count = 0;
	while (*input)//Loop stops when *input=='\0'
	{
		character_count++;
		input++;
	}
	return character_count;
}
//Method 2: Pointer subtraction
size_t calculate_string_length(const char* input)
{
	const char* start_position = input;
	while (*start_position)
		start_position++;
	return start_position - input;
}

int main()
{
	char text[] = "programming";
	printf("%zu ", strlen(text));
	printf("%zu ", calculate_string_length(text));
	return 0;
}


2.2 Call-by-Value vs Call-by-Reference

The purpose of learning pointers is to solve problems that cannot be addressed without them. What kind of problems require pointers?

Consider this challenge:
Create a function that swaps the values of two variables.
Without careful consideration, you might write:

void swap_values(int first, int second)
{
	int temp = first;
	first = second;
	second = temp;
}


However, upon reflecsion, we understand that function parameters are copies of the arguments passed to the function. This realization shows that the above function is ineffective.
This approach passes the actual values of the variables to the function, which is known as call-by-value.

So how can we implement a proper swap function?

We need to ensure that when the swap function is called, it operates directly on the variables in the main function, swapping their values.
We can achieve this by using pointers. In the main function, we pass the addresses of the variables to the swap function, which then indirectly accesses and modifies the variables in main.

Let's improve our swap function and test it:

#include<stdio.h>

void swap_values(int* first_ptr, int* second_ptr)
{
	int temp = *first_ptr;
	*first_ptr = *second_ptr;
	*second_ptr = temp;
}

int main()
{
	int value_a = 9;
	int value_b = 5;
	printf("value_a=%d value_b=%d\n", value_a, value_b);
	swap_values(&value_a, &value_b);
	printf("value_a=%d value_b=%d\n", value_a, value_b);
	return 0;
}


As we can see, our improved swap function successfully completes the task. When calling the swap function, we pass the addresses of the variables to the function. This method of function invocation is called call-by-reference.
Call-by-reference establishes a direct connection between the function and the calling function, allowing the called function to modify variables in the calling function. Therefore, if a function only needs the values of variables from the calling function to perform calculations, call-by-value is appropriate. If the function needs to modify variables in the calling function, call-by-reference is necessary.

  1. Understanding Array Names

You might have encountered or written code like this:

#include<stdio.h>
int main()
{
	int numbers[10] = { 0,1,2,3,4,5,6,7,8,9 };
	int* pointer = &numbers;
	return 0;
}


Here, we use &numbers[0] to get the address of the first element.
However, the array name itself is actually the address of the first element of the array. Let's verify this.

#include<stdio.h>
int main()
{
	int numbers[10] = { 0,1,2,3,4,5,6,7,8,9 };
	int* pointer1 = &numbers;
	int* pointer2 = numbers;
	printf("%p\n%p", pointer1, pointer2);
	return 0;
}


Pointer1 stores &numbers, while pointer2 stores numbers. When we print them using the %p format specifier:

As clearly shown, these two pointers point to exactly the same address!
This demonstrates that the array name is the address of its first element.

At this point, you might have some questions. If the array name is the address of the first element, how do we explain the following code?

#include<stdio.h>
int main()
{
	int values[] = { 0,1,2,3,4,5,6,7,8,9 };
	size_t size = sizeof(values);
	printf("%zu", size);//Output is 40
	return 0;
}


The output is 40. If values were the address of the first element, we would expect an output of 4 or 8 (depending on whether we're in a 32-bit or 64-bit environment).
The statement that the array name is the address of the first element is correct, but there are two exceptions:

sizeof(array_name), When sizeof is used with just the array name (note: just the array name!), 
the array name represents the entire array, and sizeof calculates the total size of the array in bytes.

&array_name, Here the array name represents the entire array, and & retrieves the address of the entire array
(The address of the entire array is different from the address of its first element!)


In all other contexts, the array name represents the address of the first element.

Let's test this with the following code:

#include <stdio.h>
int main()
{
	int data[10] = { 1,2,3,4,5,6,7,8,9,10 };
	printf("&data[0] = %p\n", &data[0]);
	printf("data     = %p\n", data);
	printf("&data    = %p\n", &data);
	return 0;
}


We'll notice that all three printouts are identical. So what's the difference between data and &data?

#include <stdio.h>
int main()
{
	int sequence[10] = { 1,2,3,4,5,6,7,8,9,10 };
	printf("&sequence[0]   = %p\n", &sequence[0]);
	printf("&sequence[0]+1 = %p\n", &sequence[0] + 1);
	printf("sequence       = %p\n", sequence);
	printf("sequence+1     = %p\n", sequence + 1);
	printf("&sequence      = %p\n", &sequence);
	printf("&sequence+1    = %p\n", &sequence + 1);
	return 0;
}


Here we observe that &sequence[0] and &sequence[0]+1 differ by 4 bytes, as do sequence and sequence+1. This is because &sequence[0] and sequence are addresses of the first element, so adding 1 moves to the next element.
However, &sequence and &sequence+1 differ by 40 bytes because &sequence is the address of the entire array, and adding 1 skips the entire array. (This concept will be explained in more detail in a subsequent article)

Summary: The array name is the address of its first element, with two exceptions.

  1. Accessing Arrays with Pointers

With the knowledge we've gained, we can conveniently use pointers to access array elements.

#include<stdio.h>
int main()
{
	int values[5] = { 0 };
	int* pointer = values;
	int size = sizeof(values)/sizeof(values[0]);
	for (int i = 0; i < size; i++)
	{
		//Input
		scanf("%d", pointer + i);
		//Alternatively, we could use values+i or &values[i]
	}
	for (int i = 0; i < size; i++)
	{
		//Output
		printf("%d ", *(pointer + i));
		//Other expressions could be used here as well
	}
	return 0;
}


Once you understand this code, we can analyze further. Since the array name values is the address of the first element and can be assigned to pointer, values and pointer are essentially equivalent in this context. If we can access array elements using values[i], can we also access the array using pointer in the same way?

#include<stdio.h>
int main()
{
	int elements[5] = { 0 };
	int* pointer = elements;
	int size = sizeof(elements)/sizeof(elements[0]);
	for (int i = 0; i < size; i++)
	{
		//Input
		scanf("%d", pointer + i);
		//We could also use elements+i or &elements[i]
	}
	for (int i = 0; i < size; i++)
	{
		//Output
		//printf("%d ", *(pointer + i));
		printf("%d ", pointer[i]);
		//Other expressions could be used here as well
	}
	return 0;
}


The result of this code is identical to the previous one, which means that pointer[i] is equivalent to *(pointer+i).
Similarly, elements[i] should be equivalent to *(elements+i). When the compiler processes array element access, it essentially converts it to accessing the element at the address of the first element plus an offset.

  1. The Essence of One-Dimensional Array Parameters

We've previously discussed arrays and how they can be passed to functions. In this section, we'll explore the fundamental nature of array parameters.
Let's start with a question: We've always calculated the number of aray elements outside functions. Can we determine the number of elements within a function after passing an array to it?

Let's test this:

#include <stdio.h>
void analyze_array(int data[])
{
	int size2 = sizeof(data) / sizeof(data[0]);
	printf("size2 = %d\n", size2);
}
int main()
{
	int values[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int size1 = sizeof(values) / sizeof(values[0]);
	printf("size1 = %d\n", size1);
	analyze_array(values);
	return 0;
}


We find that the function doesn't correctly determine the number of array elements.
This reveals the essence of array parameter passing: As discussed in the previous section, the array name is the address of the first element. When passing an array to a function, we're passing the array name, which means we're fundamentally passing the address of the first element.
Therefore, the function parameter should theoretically use a pointer variable to receive the address of the first element. When we write sizeof(data) inside the function, we're calculating the size of an address (in bytes), not the size of the array (in bytes). It's precisely because the function parameter is essentially a pointer that we cannot determine the number of array elements within the function.

Additional information:

void process_array(int data[])//Written as an array, but本质上 still a pointer
{
	printf("%zu\n", sizeof(data));
}
void process_array(int* data)//Written as a pointer
{
	printf("%zu\n", sizeof(data));//Calculates the size of a pointer variable
}
int main()
{
	int collection[10] = { 1,2,3,4,5,6,7,8,9,10 };
	process_array(collection);
	return 0;
}


This demonstrates: When passing a one-dimensional array, the parameter can be written in array form or pointer form.

  1. Bubble Sort Algorithm

Bubble sort is a sorting algorithm that can arrange an unsorted array in descending or ascending order.
The core idea of bubble sort is: Compare adjacent elements pairwise. After comparison, if these elements don't meet the desired order (descending or ascending), they are swapped.

How many comparisons are needed?
In the worst case, each full pass through the array places one element in its correct position. For example, in descending order, the first pass places the smallest element at the end. In subsequent passes, the last element no longer needs to participate in comparisons. After sz-1 passes (where sz is the number of elements), the remaining unsorted element is already in its correct position.

Code Example:

#include<stdio.h>
void bubble_sort(int data[], int size)//Parameter receives the number of array elements
{
	for (int i = 0; i < size - 1; i++)//Outer loop controls the number of passes
	{								//Loop size-1 times, as the last element doesn't need sorting
		for (int j = 0; j < size - i - 1; j++)//Inner loop controls comparisons in each pass
		{									//Loop size-i-1 times, as each outer pass places one element
											//in its correct position, which no longer needs sorting
			if (data[j] > data[j + 1])//This bubble sort sorts in ascending order
			{
				int temporary = data[j];
				data[j] = data[j + 1];
				data[j + 1] = temporary;//Swap variables
			}
		}
	}
}

int main()
{
	int array[] = { 3,1,7,5,8,9,0,2,4,6 };
	int size = sizeof(array) / sizeof(array[0]);
	bubble_sort(array, size);
	for (int i = 0; i < size; i++)
	{
		printf("%d ", array[i]);
	}
	return 0;
}


Is there room for optimization in this algorithm?
If the array becomes sorted after a few passes or even before any sorting begins, do we still need to continue sorting?
Let's look at the optimized version:

void optimized_bubble_sort(int data[], int size)//Parameter receives the number of array elements
{
	for (int i = 0; i < size - 1; i++)
	{
		int is_sorted = 1;//Assume this pass is already sorted
		for (int j = 0; j < size - i - 1; j++)
		{
			if (data[j] > data[j + 1])
			{
				is_sorted = 0;//A swap indicates the array is not sorted
				int temporary = data[j];
				data[j] = data[j + 1];
				data[j + 1] = temporary;
			}
		}
		if (is_sorted == 1)//No swaps in this pass means the array is sorted
			break;
	}
}
int main()
{
	int array[] = { 3,1,7,5,8,9,0,2,4,6 };
	int size = sizeof(array) / sizeof(array[0]);
	optimized_bubble_sort(array, size);
	for (int i = 0; i < size; i++)
	{
		printf("%d ", array[i]);
	}
	return 0;
}


Bubble sort time complexity: O(n²), space complexity: O(1)

  1. Pointers to Pointers

Pointer variables are variables, and like all variables, they have addresses. So where is the address of a pointer variable stored?
In pointers to pointers (also known as double pointers).

For example, in this code:

#include<stdio.h>
int main()
{
	int value = 10;
	int* single_pointer = &value;
	int** double_pointer = &single_pointer;//Double pointer stores the address of the pointer variable
	return 0;
}


Operations with pointers to pointers:

  1. double_pointer dereferences the address stored in double_pointer, accessing single_pointer. double_pointer essentially accesses single_pointer.
int new_value = 20;
*double_pointer = &new_value;//Equivalent to single_pointer = &new_value;


  1. double_pointer first dereferences double_pointer to find single_pointer, then dereferences single_pointer to access value. double_pointer is equivalent to *single_pointer, which accesses value.
**double_pointer = 30;
//Equivalent to *single_pointer = 30;
//Equivalent to value = 30;


  1. Pointer Arrays

Are pointer arrays arrays of pointers or pointers to arrays?
By analogy, an integer array is an array that stores integers, and a character array is an array that stores characters.
So what about pointer arrays? They are arrays that store pointers.

Each element of a pointer array is an address that can point to a memory region.

Let's analyze why a pointer array is written as int * arr[5]. We know that an integer array is written as int arr[5]. What does the int at the beginning represent? It's the type of variable stored in the array. The type of a pointer (for an integer pointer) is int*. Therefore, a pointer array is written this way.

  1. Simulating Two-Dimensional Arrays with Pointer Arrays

After understanding pointer arrays, let's conduct a small test.
We know that a two-dimensional array is declared as:

	int matrix[3][5];


This creates a 3-row by 5-column matrix. We can simulate this using a pointer array with 3 elements, where each element points to an array with 5 elements.
Example:

#include <stdio.h>
int main()
{
	int row1[] = { 1,2,3,4,5 };
	int row2[] = { 2,3,4,5,6 };
	int row3[] = { 3,4,5,6,7 };
	//Array names are addresses of their first elements, with type int*, 
	//so they can be stored in the pointer array
	int* matrix_ptr[3] = { row1, row2, row3 };
	int i = 0;
	int j = 0;
	for (i = 0; i < 3; i++)
	{
		for (j = 0; j < 5; j++)
		{
			printf("%d ", matrix_ptr[i][j]);
		}
		printf("\n");
	}
	return 0;
}


matrix_ptr[i] accesses an element of the matrix_ptr array. This element points to a one-dimensional integer array, and matrix_ptr[i][j] accesses an element within that one-dimensional array.
The above code simulates the effect of a two-dimensional array, but it's not truly a two-dimensional array because each row is not stored contiguously in memory.

Tags: C pointers Arrays memory-management assert

Posted on Tue, 23 Jun 2026 17:19:55 +0000 by indian98476