First things first! We are not talking about Definitions, but Declarations.
The terms declarations and definitions have often been used interchangeably in the context of programming languages. So, before we proceed with the topic, let’s first clarify the context in which each one is used in programming languages.
When a symbol is “declared”, the compiler is informed that a symbol of that particular name is going to be used in the program. The attributes are as in the following declaration:
eg: extern int inx;
With this declaration, the compiler is informed that the symbol “inx” is of type int and is going to be used in the program. Up to this point, there is no consideration of actual memory allocation. Please note that the symbol could also be the name of a function.
But when we have a definition like int inx;, the compiler not only notes the symbol and its attributes but also decides about the allocation of the memory required for the symbol (such as the size of memory, the section of memory, etc).
With the explanation of “declaration versus definition” out of the way, please realize that in this blog, we will be discussing declaration in both contexts. We hope that you, the reader, being a discerning soul, will figure out whether it is a declaration or a definition.
While writing or reading programs in C, any reasonable program will have declarations of variables or functions. Most of the time these variable/function declarations will be pretty straightforward. Such as:
int inx; // inx is a 4 byte integer variable
char ch; // ch is a 1 byte character variable
A slightly more complicated one is a declaration like:
int *iptr; // iptr is pointer variable that contains the address of a
memory location that contains an integer
Then here is a declaration of similar complexity:
int *iparr[10]; // iparr is an array of 10 pointers to integers.
What if there are a couple of parentheses in the mix?
int (*iparr)[10];
Now how many pointers do we have?
Do we still have ten-pointers (if so, what is the big idea with the parentheses)? Or is it something else? And what is the need for such a declaration/definition?
There are just three rules to remember while writing/reading declarations in C.
Taking the above-mentioned examples, the first two declarations can do fine without actually applying the rules. Even the third one can be put in this category. But with the fourth one, we need to apply the rules to be sure about what the variable is.
The three rules that define the precedence of symbols/operators are:
Now, based on these rules, if we look at the third and fourth declarations:
int *iparr[10];
Since there is a symbol in the declaration, let us start with and apply the rules. In the declaration, there is a * and square brackets.
No parentheses are grouping any symbols/operators, so the first rule can be skipped.
But there is a pair of square brackets, so iparr is an array.
And there is a * and, so iparr is an array of pointers.
Finally, there is the type – int. So iparr is an array of ten pointers to integers.
int (*iparr)[10];
Again, there is a symbol present in the declaration, so we start with that. This is similar to the previous declaration we considered but with one difference.
There is a pair of parentheses grouping a symbol (iparr) and an operator (*). So, the first rule has to be applied. That means, iparr is a pointer. Note that it is just “a pointer” or, we have just one pointer unlike the previous case (where we had ten pointers to integers).
Next, we have square brackets, which means iparr is a pointer to an array of size ten.
And finally, the type in the declaration tells us that iparr is a pointer to an array of 10 integers.
We haven’t considered the case where we have parentheses associated with functions that have the same precedence as square brackets. We will take a look at that a little later after we understand what function pointers are.
The first two declarations are pretty straightforward. So let’s proceed with the third one.
Consider the scenario when ten strings have to be read in from the keyboard (or a file) and stored in the program. One way to do this would be to create a two-dimensional array of chars. However, this could result in a waste of space or having insufficient space to store one or more of the strings. An example of wastage of space in declaring a two-dimensional array of chars is:
char str[10][60];
If all but a couple of strings are only about 20 bytes long, there will be a large amount of memory wasted. On the other hand, if we reduce the number of columns to around 20 and declare the array as:
char str[10][20];
There will be a problem of overflow as the strings can’t be stored in the limited memory space.
So an ideal scenario for using the array of pointers is:
char *str[10];
Now we can store ten strings, each one having an arbitrary length, by allocating memory for each of the ten pointers. A piece of code that would do this is:
#include <stdio.h>
#include <string.h>
#define NUM_STR 10
#define MAX_SIZE 100
char tmp[MAX_SIZE];
char *str[NUM_STR];
int inx;
char *new_ln;
for (inx = 0; inx < NUM_STR; inx++) {
printf (“Enter string num. %d: “, inx);
fgets (tmp, sizeof (tmp), stdin);
if ((new_ln = strchr (tmp, ‘\n’)) != NULL)
*new_ln = 0;
str[inx] = strdup (tmp);
}
Now comes the question of the fourth declaration. What could be a use case for this?
int (*ptr)[10];
When a single pointer has to be declared to point-to-point to a two-dimensional array is the best example usage for this declaration. Consider the following case:
int arr[10][10];
When we need to declare to pointer to contain the starting address of this array (given by the name arr), then we should declare that pointer as:
int (*ptr)[10];
Now we can assign arr to ptr:
ptr = arr;
Another example would be when there is a function that receives a two-dimensional array as argument. Consider a function that receives the maximum number of rows and columns of a two-dimensional array and a two-dimensional array as arguments and reads in the array elements from the user. The most common way to declare the prototype for such a function would be:
#define MAX 10
int get_arr_val (int r, int c, int arr[ ][MAX]);
But, if we want a pointer version in the declaration, then we would have to use the example we are currently considering.
int get_arr_val (int r, int c, int (*arr)[MAX]);
Now the question could be why use this?
The reason is quite straightforward – when we declare the argument using the pointer variation, it is made pretty explicit that this argument is just one pointer. The same point is made by the gurus (Kernighan & Ritchie) on page number 83 in their classic book – The C Programming Language.
In the next installment, we will take the case of “mysterious” function pointers!
Ready to take your skills to the next level? Explore job opportunities at Vayavya, where innovation meets talent. Apply now and join our dynamic team!
Venu Kolathur is Chief Architect and Co-Founder at Vayavya Labs and has over 38 years of industry & academic experience. He is responsible for product technology road-map, and design strategies.