Example of declarations are:
int a; /* declaring single identifier of type int */
The above declaration declares single identifier named a
which refers to some object with int
type.
int a1, b1; /* declaring 2 identifiers of type int */
The second declaration declares 2 identifiers named a1
and b1
which refers to some other objects though with the same int
type.
Basically, the way this works is like this - first you put some type, then you write a single or multiple expressions separated via comma (,
) (which will not be evaluated at this point - and which should otherwise be referred to as declarators in this context). In writing such expressions, you are allowed to apply only the indirection (*
), function call (( )
) or subscript (or array indexing - [ ]
) operators onto some identifier (you can also not use any operators at all). The identifier used is not required to be visible in the current scope. Some examples:
/* 1 */ int /* 2 */ (*z) /* 3 */ , /* 4 */ *x , /* 5 */ **c /* 6 */ ;
# | Description |
---|---|
1 | The name of integer type. |
2 | Un-evaluated expression applying indirection to some identifier z . |
3 | We have a comma indicating that one more expression will follow in the same declaration. |
4 | Un-evaluated expression applying indirection to some other identifier x . |
5 | Un-evaluated expression applying indirection to the value of the expression (*c) . |
6 | End of declaration. |
Note that none of the above identifiers were visible prior to this declaration and so the expressions used would not be valid before it.
After each such expression, the identifier used in it is introduced into the current scope. (If the identifier has assigned linkage to it, it may also be re-declared with the same type of linkage so that both identifiers refer to the same object or function)
Additionally, the equal operator sign (=
) may be used for initialization. If an unevaluated expression (declarator) is followed by =
inside the declaration - we say that the identifier being introduced is also being initialized. After the =
sign we can put once again some expression, but this time it'll be evaluated and its value will be used as initial for the object declared.
Examples:
int l = 90; /* the same as: */
int l; l = 90; /* if it the declaration of l was in block scope */
int c = 2, b[c]; /* ok, equivalent to: */
int c = 2; int b[c];
Later in your code, you are allowed to write the exact same expression from the declaration part of the newly introduced identifier, giving you an object of the type specified at the beginning of the declaration, assuming that you've assigned valid values to all accessed objects in the way. Examples:
void f()
{
int b2; /* you should be able to write later in your code b2
which will directly refer to the integer object
that b2 identifies */
b2 = 2; /* assign a value to b2 */
printf("%d", b2); /*ok - should print 2*/
int *b3; /* you should be able to write later in your code *b3 */
b3 = &b2; /* assign valid pointer value to b3 */
printf("%d", *b3); /* ok - should print 2 */
int **b4; /* you should be able to write later in your code **b4 */
b4 = &b3;
printf("%d", **b4); /* ok - should print 2 */
void (*p)(); /* you should be able to write later in your code (*p)() */
p = &f; /* assign a valid pointer value */
(*p)(); /* ok - calls function f by retrieving the
pointer value inside p - p
and dereferencing it - *p
resulting in a function
which is then called - (*p)() -
it is not *p() because else first the () operator is
applied to p and then the resulting void object is
dereferenced which is not what we want here */
}
The declaration of b3
specifies that you can potentially use b3
value as a mean to access some integer object.
Of course, in order to apply indirection (*
) to b3
, you should also have a proper value stored in it (see pointers for more info). You should also first store some value into an object before trying to retrieve it (you can see more about this problem here). We've done all of this in the above examples.
int a3(); /* you should be able to call a3 */
This one tells the compiler that you'll attempt to call a3
. In this case a3
refers to function instead of an object. One difference between object and function is that functions will always have some sort of linkage. Examples:
void f1()
{
{
int f2(); /* 1 refers to some function f2 */
}
{
int f2(); /* refers to the exact same function f2 as (1) */
}
}
In the above example, the 2 declarations refer to the same function f2
, whilst if they were declaring objects then in this context (having 2 different block scopes), they would have be 2 different distinct objects.
int (*a3)(); /* you should be able to apply indirection to `a3` and then call it */
Now it may seems to be getting complicated, but if you know operators precedence you'll have 0 problems reading the above declaration. The parentheses are needed because the *
operator has less precedence then the ( )
one.
In the case of using the subscript operator, the resulting expression wouldn't be actually valid after the declaration because the index used in it (the value inside [
and ]
) will always be 1 above the maximum allowed value for this object/function.
int a4[5]; /* here a4 shouldn't be accessed using the index 5 later on */
But it should be accessible by all other indexes lower then 5. Examples:
a4[0], a4[1]; a4[4];
a4[5]
will result into UB. More information about arrays can be found here.
int (*a5)[5](); /* here a4 could be applied indirection
indexed up to (but not including) 5
and called */
Unfortunately for us, although syntactically possible, the declaration of a5
is forbidden by the current standard.