Home |
||
Pointers and Arrays - Part II |
Contents
Chapter
1 - Objects and Storage Allocation
1.1
Getting Started
1.2
What is a Pointer?
Chapter 2
- Pointers and Functions
2.1
Parameter Passing Techniques
2.2
Pointers as Function Parameters
2.2.1
Simple Usage
2.2.2
Qualified Usage
2.2.2.1
Using const
Qualifier
2.2.2.2
Using volatile
Qualifier
2.2.2.3
Using restrict
Qualifier
2.2.3
static
Storage Class Usage
2.2.4
Generic Usage
Chapter 3
- Pointers and Arrays
3.1
One-dimensional Array
3.2
How One-dimensional Array is Stored in Memory?
3.3
Operations on Array
3.3.1
Finding the Size of an Array
3.3.2
Finding the Address of an Array Object
3.3.3
An Array Variable is Not a Modifiable Lvalue
3.3.4
Obtaining a Pointer to the First Element of an Array
3.3.5
Accessing Array Elements
3.4
Variable Length Array
Chapter 4 - Address Arithmetic
4.1 Address
Constants
4.2 Why Pointers Should Have Data Types?
4.3
Operations on Pointers
4.3.1 Multiplicative Operations on Pointers
4.3.2 Additive Operations on Pointers
Chapter
5 - Multi-dimensional Arrays
5.1
How Multi-dimensional Arrays are Stored in Memory?
5.2 Decaying
of Multi-dimensional Arrays into Pointers
Chapter
6 - Type Conversion Between Pointers and Other Types
6.1 Pointers
and Integers
6.2
Pointers of Different Types
6.3 Function
Pointers of Different Types
Chapter
7 - Miscellany
7.1
Pointer Initialization
7.2
Flexible Array Members
7.3
Dangling Pointers
7.4
Complicated Declarations
7.4.1 How do I decipher the declarations: const
char *p; and
char
* const q;?
7.4.2 How do I decipher the declaration:
int(*f(char
*c))(int, long*);?
7.4.3 How do I decipher the declaration: void
*((*fnp[4])()) (); ?
7.5 An
Example Requiring Interpretation
An expression is said to be a constant expression, if can be evaluated during translation (compilation) rather than runtime. An address constant, therefore, is the address of an object that is known during translation.
Following are the address constants in the C programming language:
A null pointer. The macro, NULL, is the null pointer. It is #defined in <stddef.h>, <stdio.h>, <locale.h>, <stdlib.h>, <time.h> and <wchar.h>.
The macro NULL is generally defined as:
#define NULL 0
or
#define NULL ((void*) 0)
A pointer to an lvalue of an object of static storage duration. For example, in the following artificial sequence, the addresses of a and b can be determined during translation.
int a;
void func ( void )
{
static int b;
int *ptr = &a;
...
ptr = &b;
}
A
function designator, such as func
in the example above, has a constant address during the program's
life.
An integer constant cast to proper type. In the example below, the expression (unsigned long*) 0x1000 is an address constant.
/* reg_x: a fictitious memory-mapped register */
const volatile unsigned long *reg_x = (unsigned long*) 0x1000;
The unary & (address) operator also creates an address constant. See the function func above.
4.2 Why Pointers Should Have Data Types?
Let
us assume that an address in a hypothetical machine is 32 bits long. The
addressing of a byte or word will, therefore, require a 32-bit address.
This suggests that a pointer (as pointers store addresses) should be capable
enough to store, at least, a 32-bit value; no matter if it points to an integer
or a character. This brings in a question: Why pointers should have data types
when their size is always 4 bytes (in a 32-bit machine), irrespective of the target they
are pointing to?
Before we see why pointers should have data types, it would be beneficial to understand the following points.
The
C programming language:
has data types of different size; i.e., objects of different types will
have different memory requirements
supports uniformity of arithmetic operations across different (pointer) types
does not maintain data type information, unlike C++, in the object or
executable image
When objects of a given data type are stored consecutively in the memory (that is, an array), each object is placed at a certain offset from the previous object, if any, depending on its size. A compiler that generates code for a pointer, which accesses these objects using the pointer arithmetic, requires information on generating offset. The data type of the pointer provides this information. This explanation gives a good reason for the point 1 above.
The point 2, above, is also a reason why pointers should have data types. Sizes of various data types are basically decided by the machine architecture and/or the implementation. And, if arithmetic operations were not uniform, then the responsibility of generating proper offset for accessing array elements would completely rest on the programmer, which, in turn, has the following drawbacks:
A programmer is prone to commit mistakes: typo mistakes, providing wrong offsets, etc.
Porting the code to other implementations would have required changes, if data type sizes differ. This would have lead to the portability issues.
Once the translation of the C code completes, the compiler leaves out putting the data type information in the final object code.
4.3.1 Multiplicative Operations on Pointers
Multiplicative operations (*, % and /) on pointers or arrays are not allowed. Give your careful attention to the following description (given by Barry Schwarz), which is a dependable answer to a question on pointer multiplication.
On Tue, 6 Jan 2004 12:37:08 +0530, "Vijay Kumar R Zanvar" <vijoeyz@hotpop.com> wrote: >Hi, > > Why multiplication of pointers is not allowed? >Till now I only know this, but not the reason why! > >PS: As a rule, I searched the FAQ, but could not >find an answer. Adding an int to a pointer results in pointing to something a specified distance further to the "right" in memory. Subtracting an int from a pointer results in pointing to something a specified distance further to the "left" in memory. Subtracting one pointer from another results in how far apart the two memory locations are. If your program and data were to be magically relocated as a unit in memory, each of the above expressions would still produce the same result. Until you can define a concept of either adding two pointers or multiplying two pointers that meets the constraint in the previous paragraph, the two operations make no sense. (Hint: others have thought this through and decided such a definition is either not possible or of no programming value.) <<Remove the del for email>>
It, however, is possible to cast a pointer type to an integral type before a multiplicative operation; but it is a not an advisable thing to do. See the Section 6.1.
4.3.2 Additive Operations on Pointers
When an operand of the + (plus) operator has a pointer type, the other operand must be of integer type; the type of result is that of the pointer operand. The pointer operand could be a pointer to a non-array object, or an array element; the former case, however, does not make a good logic.
In the following example, ip is analogous to a pointer to an element of an array of length one.
int i;
int *ip = &i;*ip++; /* undefined behaviour */
Consider the expression ptr points the last element of int ia[4]. Following code fragments illustrate few facts:
ptr = &ia[4]; /* See point 1 */
if ( *(ptr+1) ) > 1 ) /* See point 2 */
{ /* ... */ }
Address of the one past the last element:
can be taken for computation purposes
can not be used to access the location for modification; doing so is an undefined behaviour
An integer can be subtracted from a pointer type; the result has the type of the pointer operand. A pointer, however, can be meaningfully deducted from another pointer, if and only if they both point to the members of an array. For a well defined behavior, the result of the subtraction of pointers should point to an element or one past the last element of the array.
Chapter 5 Multi-dimensional Arrays
C provides multi-dimensional arrays in concurrence with one-dimensional arrays. A Multi-dimensional array can be visualized as a matrix with row and columns. The following statement, for instance, is a declaration of a two-dimensional array of int, which has three rows and four columns.
int ia[3][4];
A pictorial visualization of the array above is shown below:
ia: Column
0 1 2 3
_____ _____ _____ _____
0 | | | | |
----- ----- ----- -----
Row 1 | | | | |
----- ----- ----- -----
2 | | | | |
----- ----- ----- -----
Multi-dimensional arrays are actually arrays of arrays. The general interpretation of ia as a two-dimensional array of ints with three rows and four columns is not proper, if not wrong. The next section, in addition to interpretation of ia, also describes various aspects of multi-dimensional arrays in more detail.
5.1 How Multi-dimensional Arrays are Stored in Memory?
Technically speaking, the only category of arrays the C language has is one-dimensional arrays.
The
array ia
above (Section 5)
is actually a one-dimensional array (of arrays). Its actual internal
interpretation is like this: ia
is a one-dimensional array of three elements, and each of the three
elements is an array four of ints.
(That is, ia is a one-dimensional array
three of array four of ints.)
A
matrix representation of a multi-dimensional array offers an easy picture to the
mind; however, the array is not organized as a matrix in the memory, but as a
one-dimensional array. From the Section 3.2,
we know how a
one-dimensional array is organized in the memory. Hence,
following is how ia is stored in the memory:
[0][0] [0][3] ia[1] [2][0] [2][3] |_____ _____ _____ _____|_____ _____ _____ _____|_____ _____ _____ _____| | | | | | | | | | | | | | |----- ----- ----- -----|----- ----- ----- -----|----- ----- ----- -----| ia[0] [1][0] [1][3] ia[2]
C employs the row-major method, that is, the rightmost subscript varies first.
5.2 Decaying of Multi-dimensional Arrays into Pointers
Not always does an array decay into a pointer; see the Section 3.3.4 for more information on this. However, when an array decays to a pointer, its interpretation is always pointer to the first element. The same concept applies for a multi-dimensional array (array of [array of [...] ] array, actually). Following table illustrates the decaying of arrays with few examples:
Declaration |
Usage |
Interpretation |
int a[5] | a | int * |
int a[4][5] | a | int (*)[5] |
a[1] | int * | |
a[1][2] | int | |
int a[3][4][5] | a | int (*)[3][4] |
a[1] | int (*)[5] | |
a[1][2] | int * | |
a[1][2][3] | int | |
void fn ( int * ); | int a[5]; fn ( a ); |
Decays to a pointer to the first element |
void fn ( int (*)[] ); | int a[5]; fn ( &a ); |
Pointer to the array itself; does not decay to a pointer. |
Chapter 6 Type Conversion Between Pointers and Other Types
Situations often arise in programming practice when a pointer is an operand of an expression with an incompatible operand type. Pointer behaviour, described in the following subsections, under such situations should be familiar to the programmer.
In C89, pointer and integers were considered equivalent and, hence, interchangeable. This was because pointers were uniform with the size of some integer types. As C has now been implemented on much architectures, it is not portable to assume pointers and integers equivalent. On some architecture - for example, embedded micro-controllerrs - pointers can be wider than integer types.
K&R-II (page 102): "Pointers and integers are not interchangeable ..."
Note the following points:
An integer, converted to a pointer type, causes implementation-defined behaviour. In addition, it may not properly be aligned and may not point to the intended memory location.
short s = 3; long *lp; lp = s; /* implementation-defined behaviour */ lp = (long *) s; /* OK. But may hide a potential bug */
A
pointer, converted to an integer type, also causes an implementation-defined
behaviour. In addition, few other aspects to note are:
the result may not be representable by the target integer type
the result need not be in the range of the target integer type
the
result may not properly be aligned
The only integer value that can be safely converted to a pointer of any type is constant 0.
int *ip = 0; /* OK. */
C99 understand this dilemma of integer and pointer conversions and, hence, provides a standard portable way of inter-mixing them. A conforming implementation may provide the following optional integer types:
#include <stdint.h>intptr_t uinptr_tA valid pointer to void can be converted to this type and back to the pointer to void. For example,struct some_struct *sp = ... ; intptr_t ip = (void *) sp;sp = (some_struct *) (void *) ip; /* sp still points to original object */
6.2 Pointers of Different Types
A pointer to void is the generic object pointer; a pointer to void, therefore, can be converted to pointer to object of any type (except function pointers) and back to that type, and vice versa. Following are some important piece of information on pointer conversions:
If a pointer to one type is required to be converted into a pointer to another type, an explicit cast is required.
int i, *ip = &i; short s, *sp = &s;sp = (short *) ip; /* OK */
A null pointer can be converted into pointer of any type
char *cp = NULL; /* OK */ int *ip = NULL;
In the above, after the initialization the type of null pointer is pointer to int and pointer to char, respectively.
When converting a pointer type to another pointer type, the behaviour is undefined if the resulting pointer is not correctly aligned.
struct some_struct1 *sp1; struct some_struct2 *sp2;sp1 = (struct some_struct1 *) sp2;
When
a pointer to an object is converted to pointer to char,
the result points to the lowest address of the object. In the
following example, if the sizeof (si) ==
2, and addresses of each byte are
0x1000 and
0x1001, then
cp points to
object in the location 0x1000.
short int si; char *cp;
cp = (char *) &si;
Consider the following example:
char c = 0x10;
int *ip = (int*) &c;*ip = 0x2030;
Let sizeof (int) == 4. The pointer, ip, points to an object of char, which is of one byte. But later in the statement, it is accessed as a 4-byte value. This may result into overwriting the adjacent memory locations, resulting into an undefined behaviour. On a Linux machine, this situation generally results into segmentation fault.
6.3 Function Pointers of Different Types
A conversion between pointers to functions that have different parameter-type information should be done with an explicit cast. If such a converted pointer is used to invoke the pointed to function, the behavior is undefined.
int func1 ( void ); int (*fptr1) ( void ) = func1; /* OK */
int func2 ( short int ); /* OK */ int (*fptr2) (short int) = func2;
fptr1 = ( int (*) (void) ) = func2; /* OK */ (*fptr1) (); /* undefined behaviour: func2 takes short int argument */
A pointer to a function should not be converted into a pointer to an object or pointer to void, and vice versa; doing so is invalid.
Topics not covered in the earlier sections are mentioned here.
Initialization provides an object a starting value before a program begins execution, whereas an assignment changes the value of the object during the execution. The following list itemizes various aspects of pointer initializations, including new C99 features:
A pointer declaration with automatic storage and without an initializer assumes an unspecified value, hence it is recommended that pointer variables of such type should be initialized to null. It must be made to point to a valid location before using it, even if it was initialized to null.
K&R-II, Page 102: "C guarantees that zero is never a valid address for data ..."
char *cp; /* not recommended */
char *ncp = NULL; /* recommended */
A
pointer declaration with static storage has following properties:
if not initialized, it is initialized to a null pointer by the implementation
if initialized, the initializer must be a compile-time constant expression
{
static char cas[] = "Avada
Kedavara!";
static char *cps = { "Petrifucus
Totalus!" };
char c;
static char *cp =
&c; /* error: &c
is not constant */
}
The size of an array of unknown size is determined by the largest index of the initializer list. If there are fewer elements in the initializer list than the size of an array, then the remainder of the array is filled with zeroes.
{
int ia[] = { 1, 2, 3, 4, }; /* array size is 4 */
int ib[3] = { 1, 2, }; /* ib[2] == 0 */
int ic[2] = { 1, 2, 3, }; /* error: initializer list size
exceeds array size */
}
C99 introduces selective initialization of an array. The following example illustrates this:
int ia[5] = { 0, 1, [4] = 4, };
In the above, ia[2] and ia[3]are zeroes.
In C99 implementations, initialization of arrays can be done from both ends.
int ia[NUM] = { 0, 1, 2, [NUM-3] = 7, 8, 9, };
In the above, if NUM is greater than 6, then the elements indexed between 3 and NUM-2 are initialized to zeroes; if it is less than 6, some of the first initializers will get overridden.
C99 introduces a new concept called compound literals. Compound literals are used for representing constants of aggregate or union types, hence they are not modifiable lvalues.
struct type_t { int a, b; };
struct type_t s1 = ( type_t ) { 10, 20 };
struct type_t s2 = ( struct type_t { int a, b } ) ( 30, 40 );int *p = (int []) { 10, 20 }; /* p points to the first element
of an array of two ints */
See also: 1.1
It is a common practice among advanced programmers to use a technique called structure hacking. In this technique, the last member of a structure is a pointer to the given type. When allocating a storage for an object of the given structure type, an extra amount of storage is set aside to be accessed as a member. The following example illustrates this concept:
#define SIZE 5
struct s {
int i;
/* ... */
int *ip; /* equivalently, int ip[1]; can also be used */
};{
/* read the explanation below */
struct s *sp = malloc ( sizeof *sp + sizeof (int) * (SIZE-1) );
/* code to check and initialize sp */
for ( int i = 0; i < SIZE; i++ ) /* declaring i, here, is allowed in C99 */
sp -> ip[i] = i;
/* ... */
}
In the above, while allocating storage for the struct object, sp, sizeof (int) * SIZE many extra bytes are allocated. The memory layout of the object, then, looks like this:
________ ______ ... _______|______ ______ ______ ______ | int i | |int *ip|int * |int * |int * |int * | -------- ------ ... -------|------ ------ ------ ------ <------ struct object -------> <--- appended storage ----->
This method, in effect, creates a structure with variable-size array. Semantically speaking, sp has no elements beyond ip. Accessing a structure object beyond its last object is an undefined behaviour. Since the concept of structure hacking is not mentioned by the Standard, an implementation is free to support this concept in any way it prefers, raising portability issues.
The C99 committee, keeping useful facilities the structure hacking technique provides in mind, has introduced a new feature called flexible array members.
A flexible array member is the last member, which is an array of incomplete type, of a structure with at least one named member. Following structure, which is portable, is equivalent to struct s, above:
struct ss {
int i;
/* ... */
int ip[];
};
However, there are few constraints that apply on flexible array members. The following list summarize them:
There must be at least one element in the structure and the flexible array member must be the last element.
A structure having a flexible array member cannot occur in other structures or arrays.
The sizeof operator ignores flexible array member while calculating the size of the structure.
A dangling pointer (also known as a wild pointer) is a pointer, which does not point to a valid memory location. By validity of a location, we mean that a running process has certain restrictions on accessing memory locations that do not fall under its address space.
A pointer not handled properly can produce serious bugs or a badly behaving program. Dangling pointers get, or can be, created in several ways. The following list gives you an idea about dangling pointers: their sources of creation, methods of prevention and effects.
A straightforward example can be the following one:
{
char *cp = NULL;/* ... */
{
char c;cp = &c;
} /* The memory location, which c was occupying, is released here */
/* cp here is now a dangling pointer */
}In the above, a better solution to avoid the dangling pointer is to make cp a null pointer after the inner block is exited.
The design philosophy of C make a compiler to believe that the programmer knows what he is doing. Though a code analysis tool, like lint, can help in finding potential programming mistakes, it is up to the programmer to ensure a good behaving program. As stated earlier, misapplied pointers can create a badly behaving program. Following paragraph points up an example.
A dangling pointer in a program, by definition, points to a memory location outside the process space. The location pointed to by the dangling pointer may or may not contain a valid object. If modified, the valid object's value will change unexpectedly, distorting the performance of the process owning the object. This condition is called memory corruption. This could lead the system's state into a vicious circle, crashing it ultimately.
A clear-cut technique to avoid dangling pointers is to initialize them to NULL, whenever they are declared and no more required.
A common programming misstep to create a dangling pointer is returning the address of a local variable.
char * func ( void )
{
char ca[] = "Pointers and Arrays - II";/* ... */
return ca;
}In the above, if it is required to return the address of ca, declare it with the static storage specifier.
A frequent source of creating dangling pointers is a jumbled combination of malloc() and free() library calls. A pointer becomes dangling, when the block of memory pointed it is freed.
#include <stdlib.h>
{
char *cp = malloc ( A_CONST );/* ... */
free ( cp ); /* cp now becomes a dangling pointer */
cp = NULL; /* cp is no longer dangling *//* ... */
}
C's declarations have often been criticized, because those involving pointers types can become quite complicated. However, once understood, complicated declarations can easily be interpreted.
C follows a simple philosophy that the declaration of a variable should look similar to its usage. A two-dimensional array declared as int a[4][5];, for example, would be used as a[4][5]; that is, the declaration and usage look similar.
Similarly, C has one simple philosophy on pointer declarations: in the declaration statement, if a variable is surrounded between (* and ), its becomes of type pointer to. Following examples illustrates this piece of information:
int i; /* i is an integer */
int (*ip); /* ip is a pointer to integer */
int *ip; /* parentheses are redundant because * is
* the only operator
*/
Notice how the interpretation of the following two declarations defer from each other. The usage of parentheses with respect to the identifier affect the meaning.
int (*ipa)[4]; /* ipa is pointer to an array four of int,
* because ipa is surrounded between (* and ).
*/int *iap[4]; /* Beware: iap is not a pointer to an array, but
* it is an array four of pointers to int. This
* declaration is equivalent to: int (*iap[4]);
*/
Similarly, the difference between pointers to functions and functions returning pointers is exemplified by the declarations shown below:
void (*fnp) ( void ); /* fnp is surrounded between (* and ), hence
* it is a pointer to a function returning
* void.
*/void *fn ( void ); /* fn is a function, which returns pointer to
* void. This declaration is equivalent to:
* void (*fn (void));
*/
Following sub-sections list out some more examples involving some complicated and/or confusing pointer declarations.
7.4.1 How do I decipher the declarations: const char *p; and char * const q; ?
----- Original Message ----- From: "shashi kiran" <kiran_vee@yahoo.com> To: "Vijay Kumar R Zanvar" <vijoeyz@hotpop.com> Sent: Monday, February 09, 2004 8:40 PM Subject: doubt > What is the difference between the declarations: > const char *p; > > and, > > char const * p;, > > Is it allowed in C? Yes. > Please clear my doubt with some examples > Sure. Read the following explanation - char c; /* c is a character */ char *pc; /* pc is a pointer to a char */ const char *pcc; /* pcc is pointer to a const char */ char const *pcc2; /* pcc2, too, is a pointer to a const char */ char * const cpc; /* cpc is a const pointer to (non-const) char */ const char * const cpcc; /* cpcc is a constant pointer to a constant char */ The first two declarations are quite simple, and need no explanation. To describe the rest of declarations, let us first consider the general form of a declaration: [qualifier] [storage-class] type [*[*]..] [qualifier] ident ; or [storage-class] [qualifier] type [*[*]..] [qualifier] ident ; where, qualifier: one of volatile const restrict (C99) storage-class: one of auto extern static register type: void char short int long float double signed unsigned _Bool _Complex _Imaginary (C99) enum-specifier typedef-name struct-or-union-specifier Both the forms are equivalent. Keywords in the brackets are optional. By comparing the third and fourth declarations with the general form above, we can say that they are equivalent. How to interpret, then? The simplest tip here is to notice the relative position of the `const' keyword with respect to the asterisk (*). Note the following points: + If the `const' keyword is to the left of the asterisk, and is the only such keyword in the declaration, then object pointed by the pointer is constant; however, the pointer itself is variable. For example: const char * pcc; char const * pcc; + If the `const' keyword is to the right of the asterisk, and is the only such keyword in the declaration, then the object pointed by the pointer is variable, but the pointer is constant; i.e., the pointer, once initialized, will always point to the same object through out it's scope. For example: char * const cpc; + If the `const' keyword is on both sides of the asterisk, then both, the pointer and the pointed to object, are constant. For example: const char * const cpcc; char const * const cpcc2; You can also follow the "nearness" principle; i.e., + If the `const' keyword is nearest to the `type', then the object is constant. For example: char const * pcc; + If the `const' keyword is nearest to the identifier, then the pointer is constant. For example: char * const cpc; + If the `const' keyword is nearest to both the identifier and the type, then both the pointer and the object are constant. For example: const char * const cpcc; char const * const cpcc2; However, the first method is more reliable.
7.4.2 How do I decipher the declaration: int(*f(char *c))(int, long*); ?
"Chris Saunders" <chris.saunders@sympatico.ca> wrote > I am attempting to write and interface from another language to > some C code. I am having some difficulty interpreting a declaration. > > int (*foo(char *))(int,long *); > > Any help would be appreciated. > > Regards > Chris Saunders > chris.saunders@sympatico.ca > Read the statement as follows: 1. Find out the name of the identifier. It is: foo 2. See on its right side. It has (char *ctx) on its right. An identifier followed by a left parenthesis, some declarations - zero or more - and a right parenthesis is a declaration of a function. So, " ... foo is a function taking as argument a pointer to char ..." 3. Now, look at it's left side. It has an asterisk on it's left; a probable sign of either pointer to a function or a function returning a pointer to _something_. But, before check that the asterisk, stuffs of steps 1 and 2 are enclosed within a parenthesis? Yes, they are. So, ( *foo ( char *ctx ) ) " ... foo is a function which takes pointer to an object of type char ... " 4. Now, see on the right side of ( *foo ( char *ctx ) ) We have again an argument list of a function! Repeat the steps 2 and 3 to find that: "foo is a function which takes a pointer to an object of type char as an argument; and, it returns a pointer to function which takes an int, and a pointer to an object of type long and returns an int."
7.4.3 How do I decipher the declaration: void *((*fnp[4])())(); ?
We can apply the philosophy of declarations, discussed in the Section 7.4, to decipher this declaration. Following are the steps used to deduce that fnp is an array four of pointers to functions, which take unspecified parameters and returns pointer to a function, which takes unspecified parameters and returns pointer to void *!
Array
subscript operators ([])
bind tightly than the indirection operator (*)
fnp,
therefore, is an array
Since
fnp[4]
is surrounded between (*
and ),
it has type pointer to. Thus, in
the expression (*fnp[4]),
fnp
has the type array four of pointer to
After
having its meaning understood, substitute (*fnp[4])
with X,
so the declaration trims down to void
*(X())();
Compare
the new declaration with a simple declaration like
int *p();
(function p
returns a pointer to an int).
Similarly, X
is a function, which returns a pointer to something. That
something is (),
a function
To
decide the return type of X,
we follow a simple philosophy of C that - in a declaration like int
*p;,
the data type of p
can be established by deleting p
from the declaration. So int
*
(pointer to int)
is the type of p
Hence,
after deleting (X())
from
void
*(X())();,
we find that the return type of (X())
is void
*(),
that is, a function returning a pointer to void.
Since a function name is interpreted as a pointer to that function, the return
type of (X())
is actually a pointer to function returning void
*
Substituting
the value of X
in the step 7 leads to the fact that ((*fnp[4])())
returns void
*()
Hence, fnp is an array four of pointers to a function, which takes unspecified parameters and returns pointer to a function, which also takes unspecified parameters and returns pointer to void *.
7.5 An Example Requiring Interpretation
Let us study the effect of following statements:
int ia[3][4];a[1][6] = 45; ((int*)a[1])[6] = 46; ((int*)a)[10] = 47;
Use the diagram of Section 5.1 for cross-reference. Note the following points:
ia[n] decays to a pointer that points at ia[n][0], where 0 <= n <= 2. (Section 5.2)
A pointer pointing to an element one past the last element of array can be used for calculating offsets, but
not for dereferencing that element.
Case 1: a[1][6] = 45;
a[1] decays to a pointer that points at a[1][0], which is an element of the array a[1]. The array a[1] has only 4 elements. (*(a+1)+6) still points within the bounds of the array a, but beyond the last member of the array a[1]. Since a[1][6] accesses the member past the last member of the array for modification (point 2), it causes an undefined behaviour.
Case 2: ((int*)a[1])[6] = 46;
This is similar to the case 1. a[1] is similar to (int*)a[1], because the type of a[1] is int *. The cast, therefore, is redundant. This statement results into an undefined behaviour.
Case 3: ((int*)a)[10] = 47;
(int*)a points at a[0][0], which is an element of the array a[0], which has only 4 elements. This statement also results into an undefined behaviour.