Home
Articles
FAQ
Pointers and Arrays - Part II
by

Vijay Kumar R. Zanvar



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         


4. Address Arithmetic

4.1 Address Constants

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:

The macro NULL is generally defined as:

#define  NULL 0

or

#define  NULL ((void*) 0)

int a;

void func ( void )
{
    static int b;
    int *ptr = &a;
    ...
    ptr = &b;
}

/* reg_x: a fictitious memory-mapped register */
const volatile unsigned long *reg_x = (unsigned long*) 0x1000;


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:  

  1. has data types of different size; i.e., objects of different types will have different memory requirements  

  2. supports uniformity of arithmetic operations across different (pointer) types 

  3. 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:

Once the translation of the C code completes, the compiler leaves out putting the data type information in the final object code.   


4.3 Operations on Pointers

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:

  1. can be taken for computation purposes 

  2. 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.

See also: Sections 3.2, 5.1.


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. 


6.1 Pointers and Integers

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:

short s = 3;
long *lp;
lp = s;			/* implementation-defined behaviour */
lp = (long *) s;	/* OK. But may hide a potential bug */
#include <stdint.h>
intptr_t
uinptr_t
A 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:

int i, *ip = &i;
short s, *sp = &s;
sp = (short *) ip;	/* OK */
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.
struct some_struct1 *sp1;
struct some_struct2 *sp2;
sp1 = (struct some_struct1 *) sp2;
short int si;
char *cp;
cp = (char *) &si;

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

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 */ 

Chapter 7 - Miscellany  

Topics not covered in the earlier sections are mentioned here.


7.1 Pointer Initialization

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:

K&R-II, Page 102: "C guarantees that zero is never a valid address for data ..."  

char *cp;             /* not recommended */
char *ncp = NULL;     /*
recommended */

{
    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 */
}

int ia[5] = { 0, 1, [4] = 4, };

In the above, ia[2] and ia[3]are zeroes.  

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.

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


7.2 Flexible Array Members

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:


7.3 Dangling Pointers

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.

{
    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.

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.

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.

#include <stdlib.h>

{
    char *cp = malloc ( A_CONST );

    /* ... */

    free ( cp );      /* cp now becomes a dangling pointer */
    cp = NULL;       
/* cp is no longer dangling */

      /* ... */
}


7.4 Complicated Declarations

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 *

  1. Array subscript operators ([]) bind tightly than the indirection operator (*)

  2. fnp, therefore, is an array

  3. 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

  4. After having its meaning understood, substitute (*fnp[4]) with X, so the declaration trims down to  void *(X())();

  5. 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

  6. 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

  7. 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 *

  8. Substituting the value of X in the step 7 leads to the fact that ((*fnp[4])()) returns void *()

  9. 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: 

  1. ia[n] decays to a pointer that points at ia[n][0], where 0 <= n <= 2. (Section 5.2)

  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.


This page has been read/viewed Counter times.