Skip to content
Home » All Posts » Pointer in C – All Mysteries Revealed Now

Pointer in C – All Mysteries Revealed Now

c pointer

Introduction

Pointer in C is probably the most interesting but yet terrifying feature about this programming language. It is a special type of variable that stores only the memory address of another variable. A memory address is normally expressed in hexadecimal form. If you are not familiar with it, you can refer to this wiki page here. It essentially “points” to the memory location of a data variable, thus the name. Pointer in C offers a way to indirectly manipulate and access data by referencing or dereferencing a memory address rather than working with the actual data. This is an advanced programming technique and has several advantages in terms of performance. In this blog, I will explain how C pointer works and the best practices with pointers.

Facts About Pointers

  • A pointer references a location in memory
  • Obtaining the value pointed by a pointer in memory is called dereferencing the pointer.
  • You must use a pointer to perform dynamic memory allocation. For example:
int * a = NULL;
a = (a *) malloc (sizeof(int));
  • You need to master the art of pointer to become an advanced C programmer.
  • A variable can be passed to a function by value. For example:
int a = 10;
somefunction(a);    /* pass by value - the value of 10 is copied inside somefunction() */
  • A variable can also be passed to a function by reference using the ampersand operator (&). For example:
int a = 10;
somefunction(&a);    /* pass by reference - the address of variable a is copied inside somefunction() */

Reference a Pointer

Reference a C pointer means to declare a variable as a pointer type and points to some reference. This reference can be another variable’s address, NULL (point to nothing), or a function (also known as function pointer). We can use the = operator (same operator to give a variable value) to set a reference to point to.

Syntax to Reference a Pointer

This syntax declares a basic pointer:

Take the Memory Address of a Variable

We can use the ampersand (&) operator to get the memory address of a variable and assign it to a pointer like below:

Dereference a Pointer

Dereference a pointer basically means “value pointed by”, which returns the value it points to rather than its address. Based on the data type of the pointer being dereferenced, the compiler will evaluate the value based on the storage size of the data type. The example above illustrates a pointer of type int, which means that the compiler will “evaluate 4 bytes” from where this pointer points at and treat them like an integer value. Dereferencing can be done using the asteroid (*) operator. Consider this example:

The example above first prints the pointer myptr as a pointer address and then prints the “value pointed by” myptr using the dereferencing operator. It will produce this output below:

myptr references to the memory address of 0x4d03fd0c
myptr dereferences to the value of 90

Pointer Arithmetic

Arithmetic operations with a pointer? What?

Yes, it is 100% doable. A pointer is just a number after all, a very big one. So, it is possible to do arithmetic operations on it. To add, substract, multiple, bit-wise operations… you name it. This basically changes the reference that the pointer points to.

myptr = myptr + 10;
myptr = myptr - 20;
myptr++;
myptr--;
/* ... and many more arithmetic operations */

Be Careful

Arithmetic operations on pointers are handy, but be very, very careful with this. It is possible to make a pointer reference to any memory address. If you try to dereference a pointer that points to a memory segment that you are not allowed to operate on, the operating system will terminate your application due to memory violation (For example, a Segmentation Fault on Linux based platform).

This is also known as an “application crash”.

Function Taking C Pointer as Argument

One of my favorite use cases with pointers is to pass them in functions as arguments, which act also like return values. Yes you heard this right, arguments can hold return values using C pointers, and you can have more than on. This is often set up with a integer in the original return value to indicate if the function is executed successfully or not. Pointer argument is done by passing the arguments by reference and have the function modify the values pointed by the pointers using dereferencing operator (*). Consider this example:

/* 
 * this function takes the input "in_month" and returns how many days
 * in that month in "out_NumDays" and the name of the month in "out_monthName"
 * returns 0 if input is valid -1 otherwise.
 */
int pointerArgumentExample (int in_month, int * out_numDays, char * out_monthName)
{
	if (in_month < 0 || in_month > 12)
		return -1;

	if (!out_numDays || !out_monthName)
		return -1;

	switch(in_month)
	{
		case 1:
		{
			*out_numDays = 31;
			strcpy(out_monthName, "January");
			break;
		}
		case 2:
		{
			*out_numDays = 28;
			strcpy(out_monthName, "February");
			break;
		}
		case 3:
		{
			*out_numDays = 31;
			strcpy(out_monthName, "March");
			break;
		}
		case 4:
		{
			*out_numDays = 30;
			strcpy(out_monthName, "April");
			break;
		}
		/* you may add more case here... */
		default:
		{
			*out_numDays = -1;
			strcpy(out_monthName, "Invalid");
			return -1;
			break;
		}
	}
	return 0;
}
int main (void)
{
	int ret = -1;
	int monthDays = 0;
	char monthName[64] = {0};

	ret = pointerArgumentExample(2, &monthDays, &monthName[0]);
	if (ret)
		printf("invalid input\n");
	else
		printf("monthDays = %d, monthName = %s\n", monthDays, monthName);
}

The pointerArgumentExample() above takes 3 arguments, 2 of which are pointers (out_numDays and out_monthName). These 2 arguments are references to the variables monthDays and monthName declared in the main function and the pointerArgumentExample function is responsible for filling their values. The argument in_month is passed by value to indicate the month that the caller is interested in. Changing this value within the function does not affect the original value at the caller.

The function first does some error checking to make sure in_month is within valid range. The function returns -1 on invalid ranges:

	if (in_month < 0 || in_month > 12)
		return -1;

Then the function checks the validity of the pointers, which must point to a valid address that is not NULL. The function returns -1 on NULL pointers.

	if (!out_numDays || !out_monthName)
		return -1;

The return value of -1 indicates to the main function that pointerArgumentExample() has encountered an error and the values of monthDays and monthName are most likely invalid. The return value of 0 indicates to the main function that pointerArgumentExample() has filled monthDays and monthName with valid values.

pointerArgumentExample() uses a switch statement, dereferencing operator * and standard strcpy() function to modify the values pointed by the pointers out_numDays and out_monthName.

	switch(in_month)
	{
		case 1:
		{
			*out_numDays = 31;
			strcpy(out_monthName, "January");
			break;
		}
		... ... ...
		... ... ...
	}

Passing Reference from the Caller

The main function (the caller) declares 3 variables: ret, monthDays and monthName:

  • ret is to hold the return value (error code) from the function. In the above example, 0 means successful, otherwise failure.
  • monthDays is a local variable that is to hold number of days of a month and it is passed to pointerArgumentExample() as an address using the ampersand (&) operator.
  • monthName is also a local variable that is to hold the name of the month as a char array. The address of the first element of this array is passed to pointerArgumentExample() using the ampersand (&) operator as well.

At the main function, the local variables, monthDay and monthName will only have values when pointerArgumentExample() returns zero (success), and therefore it prints their values out on success.

Advantage of Such Setup

Functions can result in error in some cases, and normally we use a return value of int to indicate different error codes. Returning an error code is a very good practice when writing a function, so the caller can learn about reasons of failure and handle it accordingly. If the function also needs to return additional values back to the caller, they can choose to use C pointers in the function arguments to hold these values at the caller.

Function Returning a C Pointer

A function returning a pointer is a common but dangerous use case that requires a programmer to pay extra attention. A function cannot return any pointer reference as it likes, it is responsible making sure the pointer address is valid and safe to use. This normally involves the scope of a variable and dynamic memory allocation.

Consider this example:

int * pointerReturnExample(void)
{
	int a = 1000;
	return &a;
}
int main (void)
{
	int * myintptr = NULL;
	myintptr = pointerReturnExample();
	printf("pointerReturnExample returns %d\n", *myintptr);
	return 0;
}

The above example will NOT work because the variable a inside pointerReturnExample is a local variable that lives only within that function. It resides in local stack memory only accessible within the function. When this function returns, the memory of this local variable is destroyed. This local memory address has no use at the main function, therefore the code will not work.

Consider another example:

int * pointerReturnExample(void)
{
	int * b = NULL;
	b = (int *)malloc(sizeof(int));
	if(b)
	{
		*b = 1500;
	}
	return b;
}
int main (void)
{
	int * myintptr = NULL;
	myintptr = pointerReturnExample();
	printf("pointerReturnExample returns %d\n", *myintptr);
	return 0;
}

The above example will work fine because the variable b inside pointerReturnExample points to a piece of heap memory reserved using the malloc() function call. This memory is valid throughout the life time of the program even after this function returns. (Unless this piece of memory is explicitly free()-ed. Because the heap memory is valid, the main function (the caller) is able to operate or read the values referenced by this pointer.

The Void Pointer

The void keyword normally means nothing when used in a function’s declaration. A function taking void in argument means it takes no argument; a function returning void means it returns nothing. However, when void is used with a pointer (void *), it means this reference can be of any data type. You are responsible for casting the void pointer to a suitable data type before you can work on it.

Consider this example below:

typedef struct user
{
	int userid;
	char username[64];
	unsigned int age;
	char occupation[64];
} UserInfo;

void voidPointerExample(void * input1, void * input2)
{
	char * charptr = (char*) input1; /* input1 shall be cast to a string (char*) before we can work with it */
	UserInfo * userptr = (UserInfo*) input2; /* input2 shall be cast to UserInfo struct before we can work with it */

	printf("charptr = %s\n", charptr);
	printf("userptr.userid = %d\n", userptr->userid);
}

int main (void)
{
	UserInfo myuserinfo;
	voidPointerExample("kasjhdkajshdjk", &myuserinfo);
}

The voidPointerExample() function above takes 2 arguments, each having data type of void *, which means they can be any data type pointers. It is the programmer’s responsibility to know what data type pointer the void * can potentially represent. In the example, input1 is cast to a string (char*) while input2 is cast to UserInfo struct because this is just an example. In a real function working with void pointer, you may have additional function arguments that tells the function what data type to cast the void pointer to.

Void pointer is most useful when a function works with multiple data types, but you do not wish to declare an argument for each data type as it is inefficient. Instead, you can use a void pointer to represent any data type and have a selector argument to select a data type. For example:

enum mytype
{
	TYPE_INT,
	TYPE_CHAR_PTR,
	TYPE_SHORT
};

void voidPointerExample(void * mydata, int type)
{
	switch(type)
	{
		case TYPE_INT:
		{
			int * myintptr = (int *) mydata;
			printf("input is integer: %d\n", *myintptr);
			break;
		}
		case TYPE_CHAR_PTR:
		{
			char * mycharptr = (int *) mydata;
			printf("input is string: %s\n", mycharptr);
			break;
		}
		case TYPE_SHORT:
		{
			short * myshortptr = (short *) mydata;
			printf("input is short: %d\n", *myshortptr);
			break;
		}
		default:
		{
			printf("unsupported data type\n");
			break;
		}
	}
}

int main (void)
{
	int myintdata = 200;
	voidPointerExample(&myintdata, TYPE_INT);
	
	voidPointerExample("some random read only string", TYPE_CHAR_PTR);
	
	short myshortdata = 100;
	voidPointerExample(&myshortdata, TYPE_SHORT);
}

The Function Pointer

A pointer can also reference to a existing function as long as you know its return value and the list of arguments. This is particularly handy if a function needs to call multiple other functions at different conditions and you do not want to write a huge if statement containing each case. A function pointer can reduce amount of code writing and make the code more organized.

We can declare a function pointer following this general syntax:

Syntax:
[return type](*[function pointer name) ([list of argument's data types]);

Example:
void (*handleError_ptr) (int, char*);

The above example “declares” a function pointer that shall point to another function that takes 2 arguments having data types of int and char* and returns void. Please note that we are just declaring it, we have not pointed handleError_ptr to any function yet. Consider the example below where we point it to a real function using the ampersand (&) operator.

void handleError(int errorCode, char * errorMsg)
{
	printf("handling error code = %d and msg = %s\n", errorCode, errorMsg);
	/* add your own logic here to handle different errors */
}

void functionPointerExample(void)
{
	/* declare a function pointer */
	void (*handleError_ptr) (int, char*);

	/* point it to handleError function */
	handleError_ptr = &handleError;

	/* invoke the function via the function pointer */
	handleError_ptr(-2, "file is corrupted");
}
int main(void)
{
	functionPointerExample();
	return 0;
}

Ok, so in the above example, we basically declare a function pointer and call the function via this pointer. It is not very useful, isn’t it? We could have just called the function directly right? True, but the versatility of a function pointer is that it can be passed to another function as an argument and have the function call it at certain condition. For example:

void doSomething( void (*errorHandler)(int, char*) )
{
	/* 
	 * if an error is encountered, it will call "errorHandler"
	 * function pointer supplied by the caller to handle the error
	 */

	int errorfound = 1;
	if(errorfound)
	{
		errorHandler(-3, "another error");
	}
}
void handleError(int errorCode, char * errorMsg)
{
	printf("handling error code = %d and msg = %s\n", errorCode, errorMsg);
	/* add your own logic here to handle different errors */
}

void functionPointerExample(void)
{
	/* declare a function pointer */
	void (*handleError_ptr) (int, char*);

	/* point it to handleError function */
	handleError_ptr = &handleError;

	/* call doSomething function and give it the function pointer */
	doSomething(handleError_ptr);
}
int main(void)
{
	functionPointerExample();
	return 0;
}

In the example above, we have another function called doSomething() which takes an argument of a function pointer. It shares a very similar syntax as declaring a function pointer. errorHandler() function pointer contains a reference to the actual function called handleError() and doSomething() can invoke this function indirectly by calling the function pointer instead. This is handy in the case where this function changes at different conditions. So, we can simply change the reference of where the function pointer points to instead of manually writing out all possible function calls.

Related Posts

1 Comment on this post

  1. Pingback: A Deeper Look at Computer Memory - techbuddies.io

Join the conversation

Your email address will not be published. Required fields are marked *