Addressing Pointers
Concepts of, and Operations on, Pointers
C's pointer types and operations on them, are vital. Misunderstanding them often causes program failures. Mastering these aspects is invaluable for C/C++ programmers. This discussion covers all C99 rules for pointers, which also applies to C++, but is not necessarily a beginner's guide. C++ references are not covered.
PREREQUISITES — Readers should already…
- have some programming experience, preferably in C.
- understand the concept and purpose of types in a statically typed language.
- understand expressions, operators and precedence.
Background
The model of memory, as represented to programs in most CPU architectures, is linear. To programs, memory is a sequential arrangement of bytes. Each of these bytes can be accessed by address, which is another way to say: ‘every byte has an address’.
NOTE — Architecture and Environment
Examples and values here are for a typical 32-bit, little endian
Intel x86 architecture, with 4-byte integers: sizeof(int)
.
Addresses are random, and for reference only. These choices do not alter
the discussed concepts and rules.
Typographical Conventions
Only a small number of unusual typographical conventions are employed here, in particular:
- Guillemets (French quotes) are used to delimit descriptions in syntax, so for example: ‹ident› means ‘identifier’, and must be replaced with a legal identifier.
- A long left arrow (\longleftarrow) followed by text, describes the syntax or expression to the left of the arrow.
- This sequence: t is shorthand for ‘has the type’.
- This sequence: r is shorthand for ‘read as’, or ‘is pronounced9.
- The math symbol: \equiv is shorthand for ‘is equivalent to’.
The other conventions are self-explanatory, like employing a
monospaced
font for code.
Memory Layout
The following assembler code extract (Intel® x86 syntax) allocates 4
variables (C
, S
, I
and
L
), each having different sizes:
x86 — Allocating variables in assembly
DB 052h ; BYTE (82 in decimal)
C DW -1 ; SHORT (FFFF in hex, 2's complement)
S DD 123456 ; DWORD (0001E240 in hex)
I DQ 0102030405060708h ; QWORD L
An example memory layout model is represented in the following diagram. Note that the address of each variable, is the address of the first byte of the sequence that comprises its value. This is the byte with the lowest address, regardless of the endianness of the architecture.
The equivalent example in C is shown below. Unlike assembler, C affords little control over the order in which variables are allocated, or where they are allocated. The memory layout may not exactly match the model above.
For performance reasons, variables may be aligned on 2-byte, 4-byte or 8-byte boundaries, so gaps may exist between the variables (padding). This is possible in assembler as well, but must be directly controlled by the programmer. C compilers generally default to performance alignment, but can often be controlled with a compiler switch / option.
c — Example variable definitions & initialisation
// variable definitions --------------------- typical size in bytes
char C = 'R'; //← 1 (BYTE)
short S = -1; //← 2 (WORD)
int I = 123456; //← 4 (DWORD)
long long L = 0x0102030405060708LL; //← 8 (QWORD/OWORD)
// output statements ------------------------ outputs (values only)
("&C = %p, C = %c\n", &C, C); //← A
printf("&S = %p, S = %d\n", &S, S); //← -1
printf("&I = %p, I = %d\n", &I, I); //← 123456
printf("&L = %p, L = %lld\n", &L, L); //← 72623859790382856 printf
For brevity, the addresses passed to printf()
were not cast to void*
,
which is what "…%p…"
expects. This is seldom an issue,
apart from a potential compiler warning. The address-of operator
(&
) used in the example, is discussed later.
Memory Divisions
The space for the machine code, stack, static memory, and dynamic memory, are generally in separate sections or segments. That does not necessarily prohibit a program from accessing any part of memory; it is simply how memory is organised. Some architectures or operating systems, for security reasons, may prevent programs from accessing the machine code division or code segment, as data (read or write).
This does not affect operations on pointers, only what is allowed to be accessed, and it facilitates a better understanding of the C memory environment. The stack and dynamic memory often grow towards each other. Static memory is fixed at compile time, and initialised before accessed. The program memory is also fixed, but is of little concern, since it is not directly manipulated in programs.
Addresses
Machine instructions often take arguments as addresses. Some instructions use these addresses to fetch bytes from memory, starting at the first byte, and store it in a register, potentially swapping the bytes based on CPU endianness. Endianness doesn't influence how we handle addresses or understand C pointers.
An address is a number that represents the location of a single byte in memory. It provides no semantics regarding the number of bytes that comprise the value stored there. |
In assembler, an address is just a number. Programmers decide when a number should be treated as an address. Other times, a number might represent the count of items in a collection, or the age of a person, or the code of a character. Without context, a number is just a number, especially as far as machine code is concerned. How the number is applied, or which machine code instructions are utilised, determines its meaning from a problem-solving perspective.
Any non-trivial machine code program, regardless of how it was written, will be littered with address values, used immediately, or indirectly, to access other values in memory. It is therefore fundamental to the operation of programs in a computer.
TERMINOLOGY — Immediate Values
In assembler, values that form part of the machine instructions (op-codes), are called immediate values. This means they exist in the code segment, as opposed to values (variables, even read-only variables), that reside in one of the data segments.
Macro assemblers allow programmers to name memory locations, but even so, the names simply result in addresses. This does not absolve the programmer from using these named addresses correctly. The number of bytes comprising the values fetched (or stored), depends on the instructions chosen.
Indirection
Some machine instructions can be supplied with a register or memory
location, and use the value stored there as an address, which in turn
indicates the location where a BYTE
(1-byte),
WORD
(2-byte), DWORD
(4-byte),
QWORD
(8-byte), or longer value, can be fetched or stored.
This process is called dereferencing, or
indirection.
When a value in memory is accessed by first obtaining its address from another value, the process is called indirection, and will require prior knowledge regarding the number of bytes that comprise the value. |
Simplistically, the following assembler extract shows one variation
on an indirect memory fetching instruction. It loads the value of
V
into a CPU register called
EAX
, by getting the address of V
from another
register, called EBX
. Obviously, the correct address of
V
must be in EBX
. The same indirection
instruction can therefore fetch different variables, by simply changing
the address value in EBX
.
x86 — Indirection in x86 assembly
DD 123456 ; DWORD variable (0001E240h).
V
···lea EBX, V ; load address of V into EBX.
mov EAX, dword ptr [EBX] ; indirectly, via EBX, load the
; value of V into EAX.
The diagram below attempts to illustrate the operation. In the PDF versions of this material, it may be on the next page.
The address of V
could have been stored
in another memory location, for example named
P
, and another instruction to fetch the
value of V
, indirectly through
P
, could have been used. This has no
effect on the principle outlined here.
Memory Address Zero
Especially in architectures with operating systems, a user program is
not loaded at address 0
(the beginning of physical memory).
Practically, this means that, for all intents and purposes, address
0
is an illegal address. So much so, that it is often
represented with a keyword, or special value in programming languages,
like nil
, null
, nullptr
, or
NULL
. In C and C++, a null pointer does not
necessarily have the value 0
— it's an
abstraction.
More sophisticated architectures will allow an operating system to
let a program thinks it has access to all of memory, starting
from address 0
, but meanwhile it may be mapped to another
location in physical memory via hardware. Furthermore, a program may be
loaded at random memory locations on every run, for security
reasons.
Data Movement
In computer terminology, moving a value in memory, means making a copy. In assembler, programmers explicitly choose whether to move data to the memory location of a variable, or whether the address of the variable should be loaded. In a programming language like C, where variables are generally represented by a variable identifier, the compiler will emit one of two kinds of instructions, depending on context.
When the variable is an operand to an operator that modifies memory, the relevant machine code instructions are emitted. Such operators are often categorised as side-effect operators, and still return a result, like all operators. They require an lvalue operand.
When the variable is an operand to an operator that does not modify memory, the value is placed as a result in the expression where it was used. Results of operators are temporary values (rvalues), except for the indirection operators (and references in C++).
In either case, the name of the variable itself, does not automatically cause it to be fetched. It depends entirely on the operators in the same expression. If an operator with a side-effect (one that modifies memory) is used on the variable, that instruction is performed. On the other hand, if the variable is used in conjunction with other operators, the value is fetched and placed in the expression as a temporary.
Lvalues
C uses the term lvalue to refer to expressions, like
variables, that represent memory. Several operators will report that an
lvalue is required, when an incorrect operand is supplied. Not all
variables are lvalues. Array variables, for example, are not lvalues,
nor are the result of most operators. Variables with a
const
storage class, are lvalues, but are
read-only. Lvalues will always have an address. Addresses of
temporary variables cannot be taken.
Some operators can also represent memory, and their results qualify
as lvalues. These are the indirection, subscript, and indirect member
selection operators. As we shall see later, they all result in
indirection expressions. If the indirection expression
represents an array, or const
storage class memory, the
result is not an lvalue.
Rvalues
The term rvalue refers to expressions that are not necessarily required to represent memory. It means the operators applied to it only require a value, and need not modify memory. Variables can be rvalues, but so can literals, and the results of expressions.
Rvalue expressions need not have addresses. For example, literals and enumerated values often result in immediate values in the machine code, and their addresses cannot be obtained. Any temporary results of expressions, like return values of functions, are rvalues, often called temporaries, and may even be stored in CPU registers.
Assignment
The most obvious data movement is performed with the assignment operator. This includes all the compound assignment operators. The assignment operator is not to be confused with initialisation syntax, which also uses the equals sign. The same implicit type conversion rules apply however.
Assignment operators, apart from the results they place in expressions, as is the case with all operators, also modify memory; meaning the assignment operators are part of the set of side-effect operators mentioned above.
Argument Passing
When an argument is passed to a function, it serves as initialisation of a parameter, which is a special kind of local variable in the function being called (the callee). Parameters have the same scope and lifetime as local variables — for all practical purposes, they are local variables, except that the caller can initialise them with arguments.
Although we use phrases like ‘pass an argument’, that is
really an abstraction for ‘initialise a special parameter
variable’, so that given a function F
, returning
void
with one parameter P
, of type
int
, i.e.:
void F (int param)
…
then calling the function as: F(123);
is equivalent to
this C pseudocode:
F(int param=123)
…
Syntax resembling this can be seen in languages that allow the
arguments to be named when calling a
function. In C#, for example, this would be a legal call:
F(param=123)
.
Function Returns
The return
statement conceptually assigns the result of
an expression to a temporary anonymous ‘function result’ variable.
Abstraction: given for example, a function that returns a type
T
, then the statement:
return
‹expr›;
effectively results in this pseudocode:
T retvar
=
‹expr›;
assembler: reclaim local variable memory
(stack)
assembler: restore saved
registers
assembler: jump to address after
call.
The result of retvar
will be available in the calling
expression as the result of the function call operator, as a temporary
variable. Often retvar
may be a register for the sake of
efficiency, but the abstraction remains valid.
IMPORTANT — About Data Movement
The point about data movement is that, regardless of which kind of movement is used, a source type and a destination type are involved. In all these cases, if the source and destination types do not match, the compiler will, if possible, provide implicit type conversion on the source type to match the destination type.
Addresses in C/C++
The C/C++ languages, like many others, abstract machine code operations, by allowing the programmer to choose types for variables and values. The compiler will then emit the appropriate instructions to access 1, 2, 4 or 8-byte values, based on the type. It also uses the type of a value or variable, to allocate the correct amount of space. This is a compile-time operation; once compiled, there are no more types, just pure machine code, i.e.: statically typed.
Types & Pointers
Abstractions ease the cognitive process in programming environments, and a type is an abstraction — it makes a programmer's life easier. Different languages have different levels, and kinds of, abstractions. Some higher level languages tend to hide the numeric nature of addresses behind abstractions called references.
Pointers
In C, the abstraction of an address is elementary, and is called a pointer. Formally, we say that a pointer type is a derived type, simply because the word ‟pointer” by itself, is just a concept. Application of the concept involves other, existing, types. This means, in a program, we can have ‘pointer to some type’, not just ‘pointer’. So saying ‘pointer’ is like saying ‘carnivore’ — it only categorises broadly, not qualifies unambiguously.
The term pointer represents a classification for a group of derived types that abstract the concept: ‘the address of some type of value’. |
Pointer Types
The pattern: T*
, may be read as ‘T pointer’,
where T
can be any existing type — hence the assertion that
pointers are derived types. Consequently, we then read, or pronounce,
T**
as ‘T
pointer pointer’.
Some might prefer to pronounce it as ‘pointer to a pointer to a
T
’, which is slightly closer to its most verbose
explanation: ‘the type of a value, that is the address of a value, which
has the address of a T
value’.
Since ‘T
pointer pointer’ correlates with the reading
direction, maps one-to-one to the tokens, and is the most succinct, we
will persist with that pronunciation, except in one early example.
The type T* , pronounced ‘T pointer’,
or ‘pointer to T’, is a type that depicts the address of a
T -type value, or informally:
‘means the address of a T
value’. |
Syntactically, whitespace, or the absence thereof, between the
T
and the *
, or after the *
, is
not significant. However, we consciously join them together to help
emphasise that it is a singular type, albeit a derived
type.
Examples of pointer types include the following: (we use the longer
description in the comments, hoping that in the short term: ‘pointer to
an int
-type value’ might have more value than
‘int
pointer’).
Type | Description / Verbalisation |
---|---|
int* |
‘pointer to an int -type
value’, or: ‘int pointer’. |
double* |
‘pointer to a double -type
value’, or: ‘double pointer’. |
unsigned long long* |
‘pointer to an
unsigned long long -type value’, or:‘ unsigned long long pointer’ |
int** |
‘pointer to an int* -type
value’; same as:‘pointer to a pointer to an int -type value’, or:‘ int pointer pointer’. |
Since int
, long
, etc., are existing types,
we could derive some pointer types from them. Whether we use
existing types, or derive some types, variables can be created with the
type:
c — Derived pointer types
int V; //← `V` has type `int`.
int* P; //← `P` has `int*` (‘int pointer’)
int** Q; //← `Q` has type `int**` (‘int pointer pointer’)
Considering that int*
is a type, albeit a derived type,
we can in turn derive a pointer type from it: int**
. And
since that is also a type, we can derive another pointer type
from it as well: int***
, ad infinitum.
Variables
The C programming language provides syntax to represent a value in memory. The representation allows operators to access the value, either by reading it, or changing it. The most common method is to use a named variable to represent a piece of memory. The size of the memory is determined by the type of the variable. This premise holds for any other expressions that also represent memory.
Given: Then: |
T V = X; \longleftarrow variable called
V , of type
T , containing
X .In expressions, V represents the
memory containing X , with type
T . |
REMINDER: A variable in C is a contiguous ‘chunk’ of memory, whose size is determined by its type, and is either represented by name, or referenced via a correctly typed pointer.
Lvalues & Rvalues
An expression, like a variable name, which represents memory, does not by itself affect the memory; operators that utilise the expression, however, can read from, or write to, the memory so represented. In fact, operators that modify memory, require expressions that represent memory.
In C, we formally state that operators that write to memory, require lvalues, and those that read from memory, require rvalues. Lvalues can always be used as rvalues, but an rvalue, on the other hand, is not automatically an lvalue.
For example, a literal value 123
cannot be assigned to
(it is not an lvalue), but its value can be used in an expression (it is
an rvalue). Conversely, regarding V
from the definition
above, V
can be assigned to, because it is an lvalue.
V
can be used as an rvalue (value is fetched). All
variables, except array variables, are lvalues by default.
Constness
Normally, as we have seen, non-array variables are by default
lvalues. We can override this, by prefixing a variable's definition with
the const
modifier. This give the variable the const
storage class, which for practical purposes, makes it a
read-only variable.
This sounds like a contradiction, but is not: It is handled and stored in exactly the same way as other variables, except that the compiler does its best to see that you do not create instructions to modify it. In other words, it checks that you do not use it as an lvalue. And yes, you can circumvent the compiler if you really want to.
Void Pointers
The closest we can get to an address as just a number, in C,
is by using a void pointer. This has type void*
,
which fortuitously, we pronounce ‘void
pointer’. It is
simply a pointer type that very few operators can work with, but the few
operations that are legal, can be valuable. These include: assignment,
passing as argument, the result of function return, and casting to any
other pointer type.
No arithmetic or indirection can be performed on void pointers. Some
compilers may allow subtracting two void pointers, but that is undefined
behaviour. They are most commonly used for functions that can work with
any type of memory, like memcpy()
,
and memset()
.
Writing a function taking a void pointer as parameter, means it is
easy to call, since you can pass it any address without a cast. However,
inside the function, the void
pointer will have to be cast
to another pointer type, before anything practical can be done with
it.
Obtaining Pointers in C
C provides four ways in which a programmer can manifest values of a pointer type:
- Using the address-of operator:
&
variable - Implicitly, by string literal:
"
…"
- Implicitly, by array expression.
- Implicitly, by function name.
The last two are special cases; they have their own specialised rules, and even involve exceptions to established rules.
Pointer Type Categories
C provides three categories of pointer types, which affect syntax, and the operators which can be used:
T*
\longleftarrow pointer to aT
value.T(*)[N]
\longleftarrow pointer-to-array ofN
elements of typeT
.T(*)(P)
\longleftarrow pointer-to-function, takingP
parameters, returningT
.
The second occurs whenever we take the address of an array. The third
is simply the type that all function names result in. As always, any
T
or type, can be a combination of
other types, including more pointer types.
Pointer & Type Syntax Complexity
The following is entirely legal, but not considered recreational reading, and should probably be skipped at first. It is shown here simply to highlight the deficiencies in the syntax chosen for types, which are fine at a simple level, but not some much when they are combined.
c — Gibberish
double*(*(*X[3])(int(*)(void)))(long); //←define `X`.
double d = *X[i](f)(123L); //←use `X`, store result.
X
is an array of 3
elements, which are
pointers-to-functions, each taking a parameter of (pointer to function,
taking void parameters, returning int
), returning a pointer
to a function taking a long
as parameter, returning a
double*
.
The explanation from cdecl.org is as follows: “declare X as array 3 of pointer to function (pointer to function (void) returning int) returning pointer to function (long) returning pointer to double”, while ChatGPT-4 has this explanation:
X
is an array of 3 pointers to functions. Each function
takes one argument — a pointer to a function that takes no arguments and
returns int
. These functions return a pointer to another
function that takes a long
as argument and returns a
pointer to double
. Array
of Function Pointers.
ChatGTP-4 May 24 2023
Of course, some functions must still be defined, and the actual
elements of X
given values, but it is workable, and
conceptually, not so complex — the type syntax is the problem:
X[i]
Select one of the elements inX
, which is a pointer to a function.X[i](f)
Call the function, passing it a function pointer tof
. It returns another function pointer.X[i](f)(123L)
Call the function pointer returned from the previous step, passing it123L
. It returns adouble*
.*X[i](f)(123L)
Use indirection to represent thedouble
from thedouble*
returned in the previous step.double d = *X[i](f)(123L)
Define and initialised
with the result of the previous step.
Pure poetry of the highest order, but complex enough that the use of
such code should be discouraged (at least when not using
typedef
to simplify the syntax).
Address-Of Operator
The most obvious way to obtain a pointer value, is by using the
address-of operator: &
(ampersand). It is a
prefixed unary operator. Its operand can be any expression that
has an address. All lvalues have addresses; rvalues don't.
Even though an array expression is not an lvalue, it
nevertheless has an address. An array's elements are lvalues, unless
they are also arrays. When defined as const
, the elements
are read-only. Literals, on the other hand, or the results of most
operators, cannot be used as operands to the address-of operator.
Given: and: Then: and: Also: |
T V; \longleftarrow variable named
V of type
T .T A[N]; \longleftarrow array of
N values of type
T .&V t
T* r ‘address
of V has type
T pointer’&A t
T(*)[N] r
‘address of A t
pointer-to-array of N values of
T ’.&A[0] t
T* \longleftarrow address of first
element. |
Take note that &A
(address of array) is not
equivalent to &A[0]
(address of first element) — they
do not have the same type, even though they have the same
value. Also, one can take the address of a
const
variable. Given:
const T V = E
c;
, then the
result of &V
will be: const T*
(or
T const *
if you like), instead of T*
as
above.
Any arbitrarily complex expression, that produces a value of a
pointer type, is called a pointer expression. This includes
string literals and const pointers. |
Since int*
means ‘address of an
int
’, it seems reasonable that taking the address of an
int
variable with the address-of operator, should produce
an int*
; and it does, which is why the assignment to
P
below, is valid. The same goes for the assignment to
Q
, which is a variable that can store an int**
value, and because P
has type int*
, taking its
address, yields an int**
type value, it can be stored in
Q
.
c — Addresses of variables are pointers
int V = 123; //← variable `V`, storing `123`, of type `int`.
int* P = &V; //← variable `P`, storing `ADR_OF_V`, of type `int*`.
int** Q = &P; //← variable `Q`, storing `ADD_OF_P`, of type `int**`.
In the illustration below, the variable P
occupies 4
bytes, and by coincidence, so does the variable V
— they
are not necessarily the same size on all architectures. A little-endian
architecture stores the low byte of a value, in low memory, so the
hexadecimal value in P
is 0x0000014A
, and that
of V
, is 0x00003039
. In a big endian architecture, the bytes in
memory would have been reversed, but the values would remain the
same.
The actual addresses of the variables, i.e., the results of the address-of operators, depend on the compiler, architecture, compiler options, memory model, and numerous other factors, maybe even the time of day, or phase of the moon. In C, we rarely care about the actual numerical value of an address — just that it is a valid numerical value, and in this case, will be the correct address for the first byte of the variable.
Printing Pointers
The only portable way to print the value of a pointer with
printf
, requires the %p
formatting sequence.
It expects a value of type void*
though, so in order to
avoid warnings, cast the pointer expression passed as argument, to
void*
("&I = %p\n", (void*)&I); printf
Remember that ‘pointer expression’ means: an arbitrarily complex expression, resulting in a final value having a pointer type.
Pointer Variable Definitions
Since C and C++ allows multiple variables to be defined in the same statement, some confusion is possible when defining multiple pointer variables like this. The asterisk unfortunately binds to the identifiers of the variables, and not to the type.
c — Multiple variable definitions in same statement
int i, * p = &i; //←`i` -t→ `int`, `p` -t→ `int*`.
int* p, i; //←`i` -t→ `int`, `p` -t→ `int*`.
int i, *p = &i, *q = &i; //←`i` -t→ `int`, `p` & `q` -t→ `int*`.
int* p, i, * q = &i; //←`i` -t→ `int`, `p` & `q` -t→ `int*`.
Spaces around the asterisk are immaterial and of no semantic value.
Where i
is defined before p
or q
,
its address can be used to initialise the pointer variables.
This strange situation will not arise when creating a type alias for a pointer type. An alias is always a single name, and will apply to all variables when used in a multiple variable definition statement.
c — Multiple variable definitions with type alias
typedef int* IP; //←`IP` ≡ `int*`.
, p; //←`i` & `p` -t→ `int*`.
IP i, i; //←`i` & `p` -t→ `int*`.
IP p, p, q; //←`i` & `p` & `q` -t→ `int*`.
IP i, i, q; //←`i` & `p` & `q` -t→ `int*`. IP p
A type alias created with typedef
, is
not a new type. It is simply a synonym for an existing type. Do
not create type synonyms with macros, especially not for pointer
types.
Array Variables and Expressions
The C array derived type, has the form:
T[N]
, where T
can be any
non-abstract type (cannot be void
or a function type); and
N
is a constant expression (const-expr), representing the number of elements, or
count. When using an array type to define variables, the type
must surround the variable: T V [N];
. Spacing the
between V
and [N]
, or around N
,
is immaterial.
Array variables are somewhat special — any expression representing an array, by array variable name, or other means, will result in the address of the first element. As a consequence, an array never represents the complete collection of values that it contains. The result is that arrays cannot be moved around as a whole (cannot be assigned, returned from functions, or passed as arguments). It is not as limiting as it sounds — pointer operations are powerful.
Given: Then: Thus: Note: |
T A[N]; \longleftarrow array variable
A , containing
N values of type
T ; A \equiv
&A[0] \longleftarrow decays to the address
of the first element.A t
T[N ] becomes
T* .T[N] \longleftarrowused for memory allocation
only; becomes T* in expressions. |
The type of A
is still T[N]
, meaning
A
is an array, as evidenced by taking its size:
‘sizeof A
’, which will be equal to
sizeof(T[N])
. When represented in an expression, except as
operand to the sizeof
or cast operators, it
results in, or decays to, a pointer, in this case:
T*
.
It is important to understand the ‘T[N]
’ means
‘an array of N
elements of type T
’, which
imparts more information than, for example: ‘T*
’, which
means ‘address of a T
value’. The implicit
conversion from ‘T[N]
’ to ‘T*
’ represents a
loss of information, hence the term: decay.
The first element of an array A
, can be expressed in
code as A[0]
. It represents the first T
value.
The address of the first element, can thus be expressed as:
&A[0]
. In other words then, as an expression,
A
is equivalent to &(A[0])
, yielding a
T*
value, where T
is the type of the first
element of A
. Due to operator precedence, we can shorten
the rule to: A
\equiv
&A[0]
. Both will be legal expressions, but it would
never be necessary to write &A[0]
, and because of pointer arithmetic, and subscript operator rules, the following
equivalences are all valid:
A
≡ &A[0]
≡ &0[A]
≡ A + 0
≡
0 + A
If the subscript operator is overloaded in C++,
&0[A]
will not be equivalent, and if addition is
overloaded on the type of A
, the commutativity of addition
will not necessary hold.
c — Array variables decaying to pointers
int A[3] = { 11, 22, 33 }; //←initialised sequence of `int` values.
int* P = A; //←`P` = address of first element of `A`.
int* Q = &A[0]; //←redundant, since `A` ≡ `&A[0]`
if (A != &A[0])
("It's the end of days!\n");
printfif (sizeof A != sizeof(int[3]))
("Life as we know it, has ceased.\n"); printf
In short: The type of the variable A
above is
int[3]
; the type of the expression A
is int*
.
The same operations applicable to A
, above, are also
applicable, and work exactly the same, when applied to P
,
since both A
and P
have the same type and
value in an expression. They are not remotely the same kind of
variable: the one is an array variable, the other is a
pointer variable, by classification.
Operators never care where the value of their operands originated: whether it was a literal, a variable, or the result of a previous operator. They only care about the operand types.
Sizeof Operator and Arrays
The sizeof
operator does not treat the expression
A
, as a pointer. It is an exception, and only because the
definition of A
is within scope. It is fortuitous and for
our convenience. For example, the expression: ‘sizeof
A
/
sizeof
A[0]
’, or
‘sizeof
A
/
sizeof
*A
’, will result in the number of elements in the array. It
is often wrapped in a macro:
c — Array size macro
#define ARRAY_SIZE(arr) (sizeof(arr)/sizeof(*(arr)))
···int total = 0, data[] = { 11, 22, 33 };
for (int i = 0; i < ARRAY_SIZE(data); ++i)
+= data[i];
total ("Sum of data = %d\n", total); printf
Because we did not specify the size of the array called
data
, we can easily add more initialisers, or remove some,
without worrying about maintaining a macro with the size of the array,
which is what often happens.
Function Pointers
Although function types are not data types, the name of a function results in the address of the function in the code, which is a data type. This is called a function pointer, or verbosely, pointer to a function. Since we have defined the term pointer to be synonymous with an associated type, it follows that ‘function pointer’ is only a classification; it becomes concrete when it has an actual type.
Given: Then: Thus: |
T F (P) \longleftarrow function
F , taking P
parameters, returning T .F \equiv
&F \equiv
*F t
T(*)(P) t ‘pointer to function taking P
parameters, returning a T ’.Functions represented in an expression, result in function pointers. |
The only legitimate way, therefore, to obtain a function pointer in C, is to use the name of a function. Other languages, like C++11, allow for anonymous functions as expressions. These expressions are formally called lambdas, so you might hear that some language supports, or does not support, lambdas. Well, C does not support lambdas, which is a shame, but only a minor inconvenience.
The names of the parameters are not relevant in the function pointer type. Pointer arithmetic cannot be performed on function pointers.
fpdemo01.c
— Function Pointer Example 01
/*!@file fpdemo01.c
* @brief Function Pointer Demonstration 01
*/
#include <stdio.h>
// divide an `int` by `10` with rounding, returning a `long`.
//
long F (int p) {
return ((long)p * 10L + 50L) / 100L;
}
// divide an `int` by `10` without rounding, returning a `long`.
//
long G (int p) {
return (long)p / 10L;
}
int main (void) {
long (*P)(int);
= F; printf("%ld\n", P(95)); //← `P(95)` calls `F(95)`.
P = G; printf("%ld\n", P(95)); //← `P(95)` calls `G(95)`.
P
return EXIT_SUCCESS;
}
The simplistic example above shows that the variable P
can store any value, as long as the value has type
long(*)(int)
, i.e., ‘pointer to function taking one
int
parameter, returning a long
’. That is the
type of F
and G
in expressions, so we can
assign either to P
.
Code involving function pointers is significantly simplified by using
a typedef
. It creates type aliases. Here is the
same program, with the addition of two user-defined type
aliases, called FT
, which is an alias for
long(int)
, and FTP
, which is simply an alias
for long(*)(int)
:
fpdemo02.c
— Function Pointer Example 02
/*!@file fpdemo02.c
* @brief Function Pointer Demonstration 02
*/
#include <stdio.h>
typedef long FT(int); //←`FT*` ≡ `long(*)(int)`.
typedef long (*FTP)(int); //←`FTP` ≡ `long(*)(int)`.
// divide an `int` by `10` with rounding, returning a `long`.
//
long F (int p) {
return ((long)p * 10L + 50L) / 100L;
}
// divide an `int` by `10` without rounding, returning a `long`.
//
long G (int p) {
return (long)p / 10L;
}
int main (void) {
;
FTP P* Q;
FT
= P = F; printf("%ld,%ld\n", P(95), Q(95)); //←calls `F(95)`.
Q = P = G; printf("%ld,%ld\n", P(95), Q(95)); //←calls `G(95)`.
Q
return EXIT_SUCCESS;
}
Note that we could have create the FTP
alias in terms of
FT
: typedef FT* FTP;
Using the FTP
type, it becomes trivial to pass function
pointers, return function pointers, or store them in arrays:
c — Using pointer-to-function aliases
typedef long FT(int); //← `FT*` ≡ `lont(*)(int)` (1)
typedef long (*FTP)(int); //← `FTP` ≡ `long(*)(int)` (2)
···// function returning a function pointer.
* FRP (void); //← using typedef (1)
FT(void); //← using typedef (2).
FTP FRP long (*FRP(void))(void); //← not using typedef.
// function taking a function pointer as argument.
void FAP (FT* parm); //← using typedef (1).
void FAP (FTP parm); //← using typedef (2).
void FAP (long (*parm)(int)); //← not using typedef.
// array of function pointers.
* AFP[2] = { F, G }; //← using typedef (1).
FT[2] = { F, G }; //← using typedef (2).
FTP AFPlong (*AFP[2])(int) = { F, G }; //← not using typedef.
// function returning, and accepting a function pointer.
* FFF (FT* parm); //← using typedef (1).
FT(FTP parm); //← using typedef (2).
FTP FFF long (*FFF(long (*parm)(int))(int); //← ouch.
Like all operators, the function call operator has no idea from where
its function pointer operand originates. It simply does its job,
regardless. Some programmers, with convoluted rationalisations, will
write: P = &F;
instead of P = F;
, as if
they are somehow different. The compiler will allow this syntax, but it
is ignored. Similarly, they write: (*P)(95)
, instead of
P(95)
, as if it's special. Again: it is allowed,
but the compiler completely ignores it. In fact, you can even
write: (*F)(95)
, and it will also be ignored, and be
treated as F(95)
.
If you're not convinced, consider what set of rules, other than
mentioned, explain why this will compile, and call F()
,
every time:
c — Irrelevant indirection on pointer-to-functions
long F (int parm) { ··· }
long (*P)(int) = F; //← or `… = &F;` if you like.
···(*****F)(123); P(123); //← calls `F()`.
(***F)(123); (*P)(123); //← calls `F()`.
(*F)(123); (***P)(123); //← calls `F()`.
(123); (*****P)(123); //← calls `F()`. F
- The name
F
, represents a function, and results in type:long(*)(int)
. - The variable
P
, contains the address ofF
, and has type:long(*)(int)
.
- Expressions
(*F)
and(*P)
both represent functions with type:long(*)(int)
. - Etcetera.
The function call operator gets the same value, and the same type for
any of these expressions, and will perform the same task on each — call
F()
.
If you like to see even more pathologically insane variants of the
above, consider the following, which assumes that the definitions of
F
and P
above are still in scope:
c — Irrelevance to the extreme
(****(***(**(*F))))(123); //← still calls `F()`.
(****(***(**(*P))))(123); //← still calls `F()`.
(&*&*&*&*&*&*&*&F)(123); //← still calls `F()`.
(&*&*&*&*&*&*&*&P)(123); //← still calls `F()`.
It does not matter how you represent a function, by name or pointer variable, it will always just be a function pointer — which can only be called with the function call operator, stored in a variable, passed as argument, or returned from a function; whether you unnecessarily apply indirection to it or not.
Literal Strings
Literal strings (e.g. "ABC"
or L"ABC"
)
decays to pointers to their first characters. Since each
character has type char
, or wchar_t
, that
means the type is char*
or wchar_t*
respectively. In C++, they decay to have types const char*
and const wchar_t*
respectively. The fact that they are not
const
pointers in C, does not mean it is portable to write
to the string location.
Technically, a literal string results firstly in an array of
char
or wchar_t
. Then number of elements will
be the count of characters plus 1
for a terminal NUL character (value 0):
"ABCDE"
t char[6]
decays char*
\longleftarrow C
"ABCDE"
t const char[6]
decays
const char*
\longleftarrow C++
c — Literal string types and their decays
("%zu\n", sizeof "ABCDE")); //⇒6
printf("%zu\n", sizeof(char[6])); //⇒6 printf
Furthermore, duplicate & identical literal strings are allowed to
share the same space. Although it has no real impact on programs, a
literal string can be considered an array of char
s or
wchar_t
s, but since representing it will result in a
pointer, it is rather a moot point.
The important point here is that, conceptually, programmers tend not to think of a literal string as a pointer. They think of it as a ‘string’, which it is, and which it isn't, depending on your perspective. But regardless of perspective or assumption, it results in a pointer, and hence all operators that work with pointers, will work with literal strings.
c — Literal string shenanigans
( *"ABCDEF" ); //← indirection, ⇒ `A`
putchar( "ABCDEF"[2] ); //← subscript (indirection), ⇒ `C`
putchar( 2["ABCDEF"] ); //← subscript (indirection), ⇒ `C`
putchar( *("ABCDEF" + 2)); //← ptr arithmetic & indirection. ⇒ `C`
putchar(*(2 + "ABCDEF") ); //← ptr arithmetic & indirection: ⇒ `C`
putcharchar* P = "ABCDEF"; //← store pointer in `P` variable.
char S[] = "ABCDEF"; //← exception. not a literal string:
char T[] = {'A','B','C','D','E','F','\0'}; //←equivalent to this.
We have not yet discussed the intricacies of indirection, pointer arithmetic, and the subscript operator, so do not get too concerned about that. The code is only to prove that literal strings result in pointers — in other words, the code is syntactically correct, will compile, run, and produce the same results, on all conformant C compilers.
In C++, the type of a literal string is: const char[N]
,
decaying to: const char*
. You should also treat
literals strings in C as if they are const
pointers, as
matter of good programming convention. In other words, instead of:
char* ident = "ABC";
.
rather use:
const char* ident = "ABC";
Writing to the memory occupied by the characters of a string literal is undefined behaviour.
Operations on Pointers
Pointer values are often passed to functions as arguments, so that functions may have the option to modify the value at that address, without the function needing to know the name of that value. In some other languages, the compiler may do it automatically, and then it is called ‘pass by reference’. C has no syntax for this feature; we must manually pass addresses, should we need to.
Indirection Operator
In order to represent the value a pointer ‘points to’, we can use the indirection operator. Any expression, where the last operator to be performed is the indirection operator, is called an ‘indirection expression’.
Given: Then: Means: Type: |
E t
T* \longleftarrow any expression E
of type T* .*E r ‘indirect
E ’‘represent the T value at address
E ’.*E t
T |
Assuming the runtime value of the expression E
above is
ADR_OF_V
, which is an address, we often say ‘E
points to ADR_OF_V
’. Generally however, we know
ADR_OF_V
is the address of some variable, say
V
, in which case we will say: ‘E
points to
V
’. Hence the term ‘pointer’.
Since we have defined a variable name to represent a value in memory,
and we have also defined an indirection expression, like *E
above, to represent a value in memory, it means that the same
op-erations can be applied to either. Given the right values, it will be
possible for an indirection to be a practical alias, in almost
every respect, for some variable:
mini_indirect.c
— Minimal Indirection Example
#include <stdio.h>
int main (void) {
int V = 123; //←value 123. assume `&V` is ADR_V.
int* P = &V; //←value ADR_V. assume `&P` is ADR_P.
int** Q = &P; //←value ADR_P. assume `&Q` is ADR_Q.
#define _p(expr) printf("%*s = %p\n", -5, #expr, ((void*)(expr)))
( &V ); //⇒ &V = ADR_V
_p( &P ); //⇒ &P = ADR_P
_p( &Q ); //⇒ &Q = ADR_Q
_p( &*P ); //⇒ &*P = ADR_V
_p( &*Q ); //⇒ &*Q = ADR_P
_p( &**Q ); //⇒ &**Q = ADR_V
_p( P ); //⇒ P = ADR_V
_p#undef _p
#define _p(expr) printf("%*s = %d\n", -5, #expr, expr)
( V ); //⇒ V = 123
_p( *P ); //⇒ *P = 123
_p( **Q ); //⇒ **Q = 123
_p#undef _p
}
From the C precedence table, notice that the address-of and
indirection operators have the same level of precedence, but they
associate with their operands from right to left. Consequently,
in the expression &*P
, indirection is performed first,
which represents memory, and is exactly what the address-of
operator requires: it can only take the address of an expression that
represents memory.
Empirically, one can see by the output that *P
is
effectively (not entirely), an alias for V
, just like
*Q
is effectively an alias for P
, and
**Q
consequently also an alias for V
. That
supports the above definitions, so it should be no surprise. This will
remain true for as along as P
contains the address of
V
. If P
is modified to contain the address of
another variable, say W
of type int
, then from
that point on, *P
will be an alias for W
.
The example above is not practical code, since there is no point in taking the address of a variable, and store it in another variable in the same scope. This is not illegal though, so we used it to illustrate the fundamentals of indirection. In practical programs, the most common reason for taking an address, is to pass it to a function, so that the function may modify the variable via indirection.
c — Function with pointer type parameter
extern void tripple_it (int* parm); //←declaration.
···int var1 = 12, var2 = 20;
("var1 = %d\n", var1); //⇒ 12
printf(&var1);
tripple_it ("var1 = %d\n", var1); //⇒ 36
printf("var2 = %d\n", var2); //⇒ 20
printf(&var2);
tripple_it ("var2 = %d\n", var2); //⇒ 60
printf
···void tripple_it (int* parm) {
*parm *= 3; //← `*parm` is an ‘alias’ for value at the address in `parm`.
}
The same machine code in tripple_it()
can now modify any
variable whose address has been passed, by using the indirection
operator, without ever knowing the names of the variables, or employing
variables defined on the external level.
Another reason we pass pointers to functions, is when we have no choice. This is when, conceptually, we want to pass an array. We say conceptually, because we have now established that an array cannot be used as a complete unit, or ‘chunk’ of memory.
Returning Pointers
Functions can return pointers, though this should be used with care: for example, a function should never return a pointer to a local variable, or parameter. If a parameter is a pointer, it will be safe to return the value of the parameter. The lifetime and ownership of the value referenced by the pointer returned, should be well documented.
c — Function returning a pointer
long* frp (void) {
static long global_lifetime_var = 0L;
return &global_lifetime_var;
}
···long l = *frp();
("1) *frp() = l = %ld" "L %ld" "L\n", *frp(), l);
printf= *frp() = 123L;
l ("2) *frp() = l = %ld" "L %ld" "L\n", *frp(), l);
printf= frp()[0] = 234L;
l ("3) *frp() = l = %ld" "L %ld" "L\n", *frp(), l);
printf
long* p = &*frp();
*p = 345L;
("4) *p = *frp() = %ld" "L %ld" "L\n", *p, *frp()); printf
1) *frp() = l = 0L 0L
2) *frp() = l = 123L 123L
3) *frp() = l = 234L 234L
4) *p = *frp() = 345L 345L
It is safe for frp
to return the address of
static
local variable, since it has a global
lifetime.
Const Pointers
The problem now is that we may want to pass the array so that the
function can read from it; we don't want the function to write
to the array. Of course, we can hope the function will not modify it.
But better to be safe than sorry, which brings us to the concept of
‘read-only pointers’ or, more formally: ‘const
pointers’.
Given: Same: Then: |
const T* P; \longleftarrow pointer to a value of type
const T .T const* P; \longleftarrow pointer to a value of type
const T .*P t
const T \longleftarrow means it can only be
read. |
The following definition of P
has the same effect:
T const *P;
, but most programmers use the first version in
the definition above. Here, this pattern is used in an example:
c — Iteration using const pointer parameters
#define ARRSZ(a) (sizeof(a)/sizeof(*(a)))
extern int sum (const int* beg, const int* end);
···int data[] = { 11, 22, 33, 44 };
int total = sum(data, data + ARRSZ(data));
("Sum of data = %d\n", total);
printf
···int sum (const int* beg, const int* end) {
(beg && end && beg < end);
assertint result = *beg++;
while (beg != end)
+= *beg++;
result return result;
}
Of course, you could have used another algorithm and parameters for
sum()
, but you will still have at least one parameter of
type const int*
. We show such an example below, but also
explain one more rule:
Array Parameter Optional Syntax
To aid readability, C allows one to optionally define pointer parameters as if they are arrays.
When, and only when, a
‹param›eter of a function is declared or defined, then: ‘ T*
‹param›’ \equiv
‘T
‹param›[] ’, and‘ const T*
‹param›’ \equiv
‘const T
‹param›[] ’.‘ T (* ‹param›)[N] ’
\equiv
‘T
‹param›[][N] ’. Also‘ const T (*
‹param›)[N] ’ \equiv
‘const T
‹param›[][N] ’.Constant integer expressions between the square brackets, are allowed, but ignored. The N for the pointer-to-array type, is
required however (part of type). |
This rule exists solely so that programmers may convey more meaning:
‘abstractly, expecting an array’. It does not change any behaviour. If a
function is expecting an array, seeing that a parameter is of type
T []
, is more meaningful to a reader than seeing it is of
type T*
, which could just as well be the address of a
single value.
Any constant expression between the square brackets have absolutely no meaning — it is simply ignored by the compiler. It does not even have to match the existence, or not, of such a value in the definition of the function and its declaration.
It is considered a good coding convention to use this rule, where applicable:
c — Array parameter is a pointer
#define ARRSZ(a) (sizeof(a)/sizeof(*(a)))
extern int sum (const int arr[], size_t count);
···int data[] = { 11, 22, 33, 44 };
int total = sum(data, ARRSZ(data));
("Sum of data = %d\n", total);
printf
···int sum (const int arr[], size_t count) {
int result = 0;
for (size_t i = 0; i < count; ++i)
+= arr[i];
result return result;
}
If, for some reason, you wanted a pointer variable or parameter, to
also be const
, not just what it is pointing to,
you can use const
twice:
int const * const p = some_value;
Now both p
and *p
result in
const
types, and that is why p
has to be
initialised with some value — last chance you'll get. It could also have
been written as follows, with the same semantics:
const int* const p = some_value;
If only p
has to be const
, and not
what it points to, i.e. *p
:
int* const p = some_value;
In the last case, p
must be initialised, otherwise it
will result in a compilation error.
Passing Pointers for Speed
The only other reason we pass pointers to functions, is when passing
by value would be too expensive, as may be the case for large structure
type values. Passing an address could be much faster. Again, we should
make it a const
pointer, if we do not want the function to
modify the members of the struct
.
c — Struct pointer as parameter
typedef struct ST {
int member;
// ··· additional members ···
} ST;
void F (const ST* parm) {
("(*parm).member = %d\n", (*parm).member);
printf(" parm->member = %d\n", parm->member);
printf}
···= { 123 };
ST V (&V); //⇒ … = 123 … = 123 F
We have to parenthesise the (*parm)
expression, which
represents the actual value parm
points to, since the
indirection operator (*
) has lower precedence than the
member selection operator (.
). The member selection
operation will not work on pointers, hence the need to represent the
struct
first, then we can select a member from the
representation.
Indirect member selection, like subscript, is a shortcut operator.
The expression S->M
is preferred, but is synonymous with
(*S).M
. It is therefore also an indirection
expression, and if S
is an lvalue, so is
S->M
, assuming M
complies with lvalue rules
(not an array).
Pointer Arithmetic
It is possible to add or subtract (+/-
) an integer type
value to, or from, a pointer type value. This is a special case, and is
called pointer arithmetic. In this ‘arithmetic’,
1+1
will not necessarily be equal to 2
, nor
will 2-1
necessarily be equal to 1
.
Given: Then: Value: |
E t
T* , and I t any integer type.E + I \equiv I + E , E - I t T* \longleftarrow commutativity1 of
+ holds.E ± I * sizeof( T ) r ‘value of E
plus-or-minus I times
sizeof( T ) ’ |
From the definition, we can see that -
or +
will not change the type of the pointer expression operand, so that the
result will still be T*
.
Conceptually, this means that a T*
, can only point to
T
values, so when incremented, for example, it can only
point to the next T
value. If two T
values are adjacent in memory, and E
results in the address
of the first, then E+1
, will give us the next
T
, i.e., the address of the second T
value,
regardless of the size of a T
value. This can be extended
to any sequence, so that E+5
, for example, will result in
the address of the sixth T
. Of course, although not
necessary, E+0
is legal, and equal to E
.
Pointer arithmetic exposes one of the biggest dangers in C. By using
pointer arithmetic, any address can effectively be reached,
whether that address is valid, or whether that address actually contains
a value of type T
, or not. The compiler cannot help us to
stay within the bounds of our sequence of T
values — it is
just performing arithmetic. In the example below, we combine pointer
arithmetic and indirection, which allows us to access elements of the
array, using a pointer to the first element, stored in P
,
and an offset.
c — Pointer arithmetic (commutativity of addition)
int* P = (int[3]){ 11, 22, 33 }; //← ptr. to seq. of 3 `int`s.
("*(P + 0) = %d\n", *(P + 0)); //⇒ 11
printf("*(P + 1) = %d\n", *(P + 1)); //⇒ 22
printf("*(P + 2) = %d\n", *(P + 2)); //⇒ 33
printf("*(0 + P) = %d\n", *(0 + P)); //⇒ 11
printf("*(1 + P) = %d\n", *(1 + P)); //⇒ 22
printf("*(2 + P) = %d\n", *(2 + P)); //⇒ 33 printf
The first statement uses a C99 ‘compound literal’ to create a
static, unnamed array of 3
int
values, and
assign the address of the first element to P
. An array
compound literal acts just like an array represented by name — it
results in the address of the first element.
Arrays of Arrays
An element of an array could be another array, also known as ‘an array of arrays’, more commonly misrepresented as a ‘multi-dimensional array’, leading to all kinds of misconceptions. Talking of a multi-dimensional array, is useful only as an algorithmic abstraction — it does not represent C semantics, which only involves types, and pointer arithmetic.
In the example below, you can easily replace ROW
with
T
, and see how it compares with the definitions above.
Ultimately, M + 1
must result, as we have seen, in the
address of the next ROW
type (2nd
element). For that to work, the compiler uses the type to calculate that
offset. And the type of the element is ROW
, which is a
synonym for int[3]
(array of 3 int
s). Thus,
M + 1
gets calculated as:
M + 1 * sizeof(ROW)
, or, since
ROW
is a synonym for int[3]
, as:M + 1 * sizeof(int[3])
, which
provides the correct address.c — Array-of-arrays with type alias
/* typedef version
*/ {
typedef int ROW[3]; //←`ROW` is synonym for `int[3]`.
[4] = { //←`M` stores 4 `ROW` values.
ROW M{ 11, 12, 13 }, //←values for `M[0]` ‘first ROW’.
{ 21, 22, 23 }, //←values for `M[1]` ‘second ROW’.
{ 31, 32, 33 }, //←values for `M[2]` ‘third ROW’.
{ 41, 42, 43 }}; //←values for `M[3]` ‘fourth ROW’.
* P = M; //←`M` === `&M[0]`
ROW(*Q)[4] = &M; //←`&M` is ‘ptr-to-array of 4 `ROW`s’.
ROW
("M %p\n", (void*) M);
printf("&M %p\n", (void*) M);
printf("P %p\n", (void*) P);
printf("&P %p\n", (void*) &P);
printf
("%d %d\n", *(*(M + 1) + 2) , M[1][2] ); //⇒ 23 23
printf("%d %d\n", *(*(P + 1) + 2) , P[1][2] ); //⇒ 23 23
printf("%d %d\n", *(*(*(Q + 0) + 1) + 2), Q[0][1][2]); //⇒ 23 23
printf}
Without using the ROW
user-defined typedef
,
no operators need to change, only the syntax for the definition of the
M
variable. We add one additional twist: taking the address
of Q
, which will result in a int(**)[4][3]
type.
c — Array-of-arrays without type alias
/* fundamental type version
*/ {
int M[4][3] = {
{ 11, 12, 13 }, //←values for `M[0]` ‘first int[3]’.
{ 21, 22, 23 }, //←values for `M[1]` ‘second int[3]’.
{ 31, 32, 33 }, //←values for `M[2]` ‘third int[3]’.
{ 41, 42, 43 }}; //←values for `M[3]` ‘fourth int[3]’.
int (*P)[3] = M; //←`M` === `&M[0]`
int (*Q)[4][3] = &M; //←`&M` is ‘ptr-to-array of `int[4][3]`s’.
int (**R)[4][3] = &Q; //←`*R` ≡ `Q`.
(" M %p\n", (void*) M );
printf("&M %p\n", (void*) &M );
printf(" P %p\n", (void*) P );
printf("&P %p\n", (void*) &P );
printf(" Q %p\n", (void*) Q );
printf("&Q %p\n", (void*) &Q );
printf(" R %p\n", (void*) R );
printf(" R %p\n", (void*) *R );
printf("&R %p\n", (void*) &R );
printf
("%d %d\n", *(*(M + 1) + 2) , M[1][2] ); //⇒23 23
printf("%d %d\n", *(*(P + 1) + 2) , P[1][2] ); //⇒23 23
printf("%d %d\n", *(*(*(Q + 0) + 1) + 2), Q[0][1][2]); //⇒23 23
printf
("%d %d\n",*(*(*(*R + 0) + 1) + 2), R[0][0][1][2]); //⇒23 23
printf("%d %d\n",*(*(*(*R + 0) + 1) + 2),(*R)[0][1][2]); //⇒23 23
printf
("%d %d %d\n", **P, ***Q, ****R); //⇒11 11 11
printf}
Take care to understand that A[0][0]
≡ **A
,
and P[0][0]
≡ **P
, and Q[0][0][0]
≡ ***Q
. Take A[0][0]
as an example, which will
result in: *(*(A+0)+0)
and is calculated as follows:
*(*(A + 0 * sizeof(int[3])) + 0 * sizeof(int))
= *(*(A + 0) + 0)
= **A
.
As arrays of arrays can become large quite quickly, and because the stack can be very limited in some environments, it is often more convenient to allocate the memory at runtime (dynamically), using the standard library, or a custom library. Here is a program similar to the above examples, but for employing dynamic memory:
c — Dynamically allocated array-of-arrays
int (*M)[3] = (int(*)[3])malloc(4 * 3 * sizeof(int));
// or: `…malloc(4 * sizeof(int[3]));`.
if (!M) { // malloc returns null pointer on failure.
(stderr, "No memory.");
fprintf(EXIT_FAILURE);
exit}
[0][0] = 11; M[0][1] = 12; M[0][2] = 13;
M[1][0] = 21; M[1][1] = 22; M[1][2] = 23;
M[2][0] = 31; M[2][1] = 32; M[2][2] = 33;
M[3][0] = 41; M[3][1] = 42; M[3][2] = 43;
M
("M[1][2] = %d\n", M[1][2]); //←tidy expression, but is
printf(" = %d\n", *(*(M + 1) + 2)); // calculated like this.
printf
···(M); //← important. free
Since C does not store metadata anywhere for arrays, it follows that
M
, &M
, and &M[0]
, will
all produce the same address. The only difference is in the
type of the address &M
produces, as opposed to
the type of M
and &M[0]
, which in turn
affects any pointer arithmetic applied to it.
One of the biggest problems with dynamic memory allocation, is to
remember to free()
the memory once done with it. This is
easy to forget, or to miss on a return path, and is called a ‘memory
leak’. Similarly, one must check the return value of
malloc()
for a failure to allocate memory. To continue
without error checking, is looking for trouble, and indicates sloppy or
lazy programming.
An alternative for C99, which does not use the stack, or dynamic memory, but rather static memory (global lifetime), is to use a compound literal. The compiler simply creates an unnamed array, with a global lifetime, and returns a pointer to the first element:
c — C99 array-of-arrays compound literal
int (*M)[3] = (int[4][3]){
{ 11, 12, 13 },
{ 21, 22, 23 },
{ 31, 32, 33 },
{ 41, 42, 43 }};
// now we can use `M` algorithmically like a 2D array. the `1`
// is the ‘row’ offset, and the `2` is the ‘column’ offset:
("M{Row2,Col3} = %d\n", *(*(M + 1) + 2)); //⇒ `23` printf
All the arrays of arrays examples above use the same pointer
arithmetic. The exposition below refers to any one of the above arrays
of arrays examples, all referenced by M
; since the types
are the same, the same operators will produce the same results.
Instead of using the above C99/11 compound
literal syntax, we could use the type int[4][3]
with
malloc()
, then the code might looks as follows:
c — Dynamically allocated array-of-arrays
int (*M)[3] = (int(*)[3])malloc(sizeof(int[4][3]));
int i, j;
[i=0][j=0] = 11; M[i][++j] = 12; M[i][++j] = 13;
M[i=1][j=0] = 21; M[i][++j] = 22; M[i][++j] = 23;
M[i=2][j=0] = 31; M[i][++j] = 32; M[i][++j] = 33;
M[i=3][j=0] = 41; M[i][++j] = 42; M[i][++j] = 43;
M
···(M); //← release the dynamically allocated memory. free
To place initial values in the M
‘array’, is now more
cumbersome, explaining the addition of compound literals to the C
language.
Arrays of Arrays Pointer Arithmetic
Since M
, in an expression, is pointing to the first
element, which is an array, the type is a pointer-to-array, which we
write as: int(*)[3]
, or ROW*
, if using the
synonym.
So, *(M + 1)
represents the 2nd row, but
since the second row is an array, it must result in a pointer to the
first element: int*
.
Assuming the result of *(M + 1) == R
, then
(R + 2)
is the address of the 3rd element, and
thus *(R + 2)
, i.e., *(*(M + 1) + 2)
represents the 3rd element: 23
, of the second
element of M
.
The following example does not add much more, but does try to show
that an int[2][3]
array (like A
below), will
result in a pointer-to-array: int(*)[3]
. Given a variable
of that type, like Q
, the same operators will give the same
result on both A
and Q
; they are different
kinds of variables, but they have the same type, and in the example, the
same value in an expression.
c — More pointer-to-arrays and arrays-of-arrays
// ‘P is a ptr-to-array of 3 elements of type `int`’, and the compound
// initialiser, whose result is assigned to `P`, is:
// ‘an array of 2 elements, each being an array of 3 elements of `int`.’
//
int (*P)[3] = (int[2][3]){ { 11, 12, 13 }, { 21, 22, 23 } };
// in an expression, “`A` *results* in a ptr-to-array of 3 elements,
// of type `int`”. Or: a pointer to a `int[3]`, which we cannot write
// as `int[3]*`, we must write it as `int(*)[3]`.
//
int A[2][3] = { { 11, 12, 13 }, { 21, 22, 23 } };
int (*Q)[3] = A; //← `A` has type `int(*)[3]` here.
// all `printf`s below, output `23`.
//
("*(*(A + 1) + 2) = %d\n", *(*(A + 1) + 2) );
printf("*(*(Q + 1) + 2) = %d\n", *(*(Q + 1) + 2) );
printf
// `A[1]`, for example, represents an array (the second element), so it
// must result in a `int*`, because the first element of the second
// array, is an `int`.
int* L = *(A + 1); //← all good.
//int** M = A; //← illegal. will not compile. wrong types.
Pointer arithmetic is at the core of all array operations. Fortunately, as shown later, C provides the subscript operator, which allows for more concise expressions.
Pointer Difference
As a matter of interest, pointers can be subtracted from each other.
The result has the type ptrdiff_t
, (from
<stddef.h>
), which is not an intrinsic type. It is
‘implementation-defined’, which means a compiler implementer can decide
about the size, and therefore range, of the value. Generally, it is a
signed type. Not all possible differences may be legal, i.e., a result
may be bigger than PTRDIFF_MAX
(from
<stdint.h>
).
Caveat: pointer differences are only well-defined when pointers to different elements in the same array, or contiguous memory block dynamically allocated, are subtracted, and includes the pointer that is one past the end of the array. That also implies that the pointer operands must be of the same type.
The return value is in terms of units of type
T
, where T
is from the pointers' types:
T*
.
Subscript Operator
The subscript operator, or index operator, is actually simply shorthand, or ‘syntactic sugar’. In fact, it is very superficial shorthand, since a subscript operation is simply physically rearranged into a pointer arithmetic and indirection expression before types are checked, or machine code generated. This is crucial to accept and understand, otherwise the following will not make sense.
Given: Rewritten: Meaning: |
X[Y] \longleftarrow i.e., any pattern in
this form, is…*(X + Y) \longleftarrow before type checking and
compilation.X[Y] \equiv
*(X + Y) \equiv
*(Y + X) \equiv
Y[X] |
This is transformed literally, so that X[Y]
is equal to
Y[X]
, just like *(X + Y)
is equal to
*(Y + X)
, which is what the first two patterns are
translated to respectively, anyway2. This is only a problem
if you have preconceived ideas about the subscript operator. Since most
C programmers are not aware of this definition, the convention is to
persevere with the most ‘natural-looking’ version.
c — Non-overloaded subscripting as syntactic sugar
int A[3] = { 11, 22, 33 };
int* P = A; //← `&A[0]` stored in `P`.
("A[2] = %d\n", A[2] ); //← recommended pattern.
printf("2[A] = %d\n", 2[A] );
printf("*(A + 2) = %d\n", *(A + 2) );
printf("*(2 + A) = %d\n", *(2 + A) );
printf("P[2] = %d\n", P[2] ); //← recommended pattern.
printf("2[P] = %d\n", 2[P] );
printf("*(P + 2) = %d\n", *(P + 2) );
printf("*(2 + P) = %d\n", *(2 + P) ); printf
Now, when most junior C programmers are asked to find the address of
the third element, they most likely will write: &A[2]
(hopefully, they are not so junior, that they will write
&A[3]
). But it should be clear that it will give the
same result as: A + 2
.
The only reason you may want to use &A[2]
, as
opposed to A + 2
, is your conviction that it provides more
information to a reader or maintainer of your code. Also, prefer
A[2]
over *(A + 2)
, even if you know that is
what C compiles, regardless of your abstraction.
Subscript Operator and Arrays of Arrays
Since the subscript operator is shorthand for pointer arithmetic, we can avoid manually applying pointer arithmetic. Consider a previous arrays of arrays example, rewritten to use the subscript operator:
c — Subscripting arrays-of-arrays
int M[4][3] = {
{ 11, 12, 13 },
{ 21, 22, 23 },
{ 31, 32, 33 },
{ 41, 42, 43 }};
// now we can use `M` algorithmically like a 2D array. the `1`
// is the ‘row’ offset, and the `2` is the ‘column’ offset:
("M{Row2,Col3} = %d\n", M[1][2]); //⇒ `23` printf
Since M[1][2]
is firstly translated to:
*(M[1] + 2)
, and M[1]
subsequently rewritten
as well, it leaves us with: *(*(M + 1) + 2)
, which is the
expression that the previous example used to select the 3rd
element from the 2nd ‘row’. This is clearly the preferred
syntax to use, as long as there is no doubt, that this is not abstract,
but simply disguised pointer arithmetic.
Arrays of Arrays Alternative
Arrays of arrays are not convenient, mostly because the number of ‘columns’ must be constant, and part of the type. This makes it difficult to write generic functions with such types.
c — Array of pointers
int _mem[4][3] = {
{ 11, 12, 13 },
{ 21, 22, 23 },
{ 31, 32, 33 },
{ 41, 42, 43 }};
int* M[4] = { _mem[0], _mem[1], _mem[2], _mem[3] }; //← array of `int*`.
int** P = M;
("M{Row2,Col3} = %d\n", M[1][2]); //⇒ `23`
printf("P{Row2,Col3} = %d\n", P[1][2]); //⇒ `23` printf
The addresses of the ‘rows’ stored in M
, could have been
dynamically allocated. To keep the code small, the example uses
_mem
to ‘allocate’ and initialise the memory. We could have
used C99's designated initialisers instead, but the ‘rows’ would not be
guaranteed to be contiguous, which could be problematic for certain
algorithms:
c — Array of pointers using compound literals
int* M[5] = {
(int[3]){ 11, 12, 13 },
(int[3]){ 21, 22, 23 },
(int[3]){ 31, 32, 33 },
(int[3]){ 41, 42, 43 }};
The point is that M
is not an array of arrays — it is
simply an array which happens to contain a list of int
pointers. Consequently, selecting an element yields an
int*
, which we arranged to be the address of a sequence of
3
int
values. Now we can use an additional
subscript operator to represent an element in the ‘row’:
M[row][col]
.
Memory for the array of pointers, and the elements can be allocated as one block. In this case, the number of ‘rows’ and the number of ‘columns’ can be determined dynamically, depending on runtime requirements. We can arrange memory as follows:
There may be a gap between the array of pointers, and the memory for the actual elements, without affecting operations. Since both the number of ‘rows’ and the number of ‘columns’ can vary, both values must be transmitted to a function taking such a construct as parameter:
c — Function with parameter as array of pointers
int sum2d (int* arr[], size_t rows, size_t cols) {
int total = 0;
for (size_t r = 0; r < rows; ++r)
for (size_t c = 0; c < cols; ++c)
+= arr[r][c];
total return total;
}
// Dynamically allocate the ‘2D’ array. `R` and `C` can be variables
// determined at runtime from other sources. It takes a few liberties
// regarding the alignment of `int*` and `int`, and not check if the
// memory allocation succeeded. Also sets values for all elements.
size_t R = 4, C = 3;
int** M = (int**)malloc(R * sizeof(int*) + R * C * sizeof(int));
for (size_t r = 0; r < R; ++r) {
[r] = (int*)(M + R) + r * C;
Mfor (size_t c = 0; c < C; ++c)
[r][c] = (r + 1) * 10 + (c + 1);
M}
("M[1][2] = %d\n", M[1][2]);
printf("sum2d(M, R, C) = %d\n", sum2d(M, R, C)); printf
Remember that, in this context, int* arr[]
is equivalent
to: int** arr
, but is more descriptive for this particular
situation. For better portability, the space for the ‘row’ pointer could
have been separately allocated from the memory for the actual elements.
The only danger with that option, is remembering to free()
two blocks of memory. C allows us to decide on tradeoffs.
If we wanted to protect the array elements from accidental
modification in a function like sum2d()
, we could have
defined it as:
c — Const pointer to const in array of pointers parameter
int sum2d (const int * const arr[], size_t rows, size_t cols) {
···[1][2] = 99; //← will fail to compile
arr
···}
((const int * const *)M, R, C) … … sum2d
Unfortunately, because of some C inadequacies, we have to cast the
passed parameter to the correct type. When safety is paramount, however,
it is a small price to pay. We could also have moved the first
const
, without affecting semantics:
int sum2d (int const * const arr[], size_t rows, size_t cols)
…
Because of the memory layout achieved, we could also treat the actual
elements as a contiguous array of int
values, and pass it
to functions that can work with a ‘normal’ array:
c — Passing array of pointers as begin/end pointers
int sum (const int* beg, const int* end) {
int total = *beg++;
while (beg != end)
+= *beg++;
total return total;
}
···int t = sum(M[0], M[0] + R * C);
This version also does not suffer from having to cast the argument
passed to the sum()
function.
Pointer Type Conversions
Simplistically: any pointer type can be converted to any other
pointer type with an explicit cast. The only implicit pointer type cast,
is converting from any pointer type to a void*
. In C, the
reverse is also automatic, but should not be depended on, since it is
not true in C++.
The practical result of casting a const T*
to
T*
(a const
pointer, to a
non-const
pointer), is implementation defined, and
generally just bad practice.
Casting function pointers is possible, but very dubious, and the result is implementation defined.
Converting an integer type to a pointer type, and vice versa, is allowed. But the result is implementation defined, and thus not necessarily very portable.
Assuming data is properly aligned in memory, a pointer to the first
byte, e.g. char*
, can be cast to a pointer of any type,
e.g. long*
. Applying the indirection operation to the
result means that, effectively, we can treat any piece of memory as any
type of value. Again, we try to avoid this as much as possible.
c — Treat memory as different types via casting
unsigned char _mem[] = { 0x41, 0x42, 0x43, 0x44, 0x00, 0x11, 0x22, 0x00 };
("%c\n", *(char*)_mem); //⇒ A
printf("%s\n", (char*)_mem); //⇒ ABCD
printf("%d\n", *(short*)_mem); //⇒ 16961
printf("%d\n", *(int*)_mem); //⇒ 1145258561
printf("%ld\n", *(long*)_mem); //⇒ 9588842051093057
printf("%c\n", *(char*)(_mem+2)); //⇒ C printf
The values used are not significant. We only wanted to show that the
same sequence of memory can be treated as different types, and hence can
produce different values, since more bytes are involved in the value.
Also note that, if sizeof(long)
is not 8
bytes, it will not display the value as indicated in the relevant
comment.
Any pointer type value can be cast implicitly to void*
,
as mentioned before. The reverse is true in C, but not C++, so it should
be rather cast explicitly. The return of malloc()
, for
example, is a void*
, and this extract follows the
suggestion:
int* P = (int*)malloc(N * sizeof(int));
This would be considered good programming practice.
Supplementary Topics
A few topics, although not directly related to pointers, are lightly covered below, because they are often not well understood, and this may aid comprehension.
Implicit Data Movement
Some operations are implied, in other words, the operation takes place because of the context in which an expression is used, not because of a physical operator. In particular, when passing arguments to functions, no operator is required to facilitate the movement of the expression's value to the special local variable of the function, which we call a parameter.
Here is a summary of all occasions where data movement takes place, in other words, a source and a destination is involved. The source and destination types should either be the same, or the source type must be implicitly convertible to the destination type.
Variable initialisation — Option clause to variable definition syntax.
Lvalue assignment — Use of side-effect operators on expressions that represent modifiable memory.
Parameter initialisation — Passing arguments to a function effectively initialises parameters, which are, in all other respects, local variables to the function being called.
Function returns — Conceptually, all non-
void
functions have an anonymous variable that holds its return value temporarily, when thereturn ‹
expression
›;
statement is executed.
When we say a language has ‘pass by reference’, it means the language has a syntax whereby a programmer can specify that an argument to a parameter must be automatically passed as an address (transparently), and that access to the parameter will automatically and transparently, indirect through the address.
In C, we must do all that explicitly, by declaring the parameter as a pointer type, by explicitly obtaining the address of the argument, and by explicitly applying the indirection operators — no syntax, nothing automatic, no transparency. Even in languages that support it, ‘passing a reference’ is different from ‘pass by reference’.
Implicit data movement takes place when a value is returned from a function. It is returned as a temporary, anonymous, variable. Practically, for efficiency, smaller values may be returned in a register, but this does not affect the principle. The same rules that apply to assignment, not only apply to argument passing, but also to function returns.
Concept of a Singular Type / Value
A value in C does not have to map cleanly to assembler types (integers of varying sizes and floating point values). A single value in C can be compound; in other words, not a scalar. There is only one way, however, to get a compound, arbitrarily sized value, and that is with a structured type value.
Regardless of size, a structured type value can be moved around
(assigned, passed as argument, or returned from a function), as
effortlessly as an int
. We can therefore treat a structured
type value as singular, when we require — it is just another
T
. Technically, however, we cannot call it scalar.
For types that depict singular types, the syntax for definition is
simple: T V;
. The variable V
follows the type
T
. This is also true for pointer types (scalar values):
T* P;
, but this is not surprising, since we have already
ascertained that T*
is ‘just another’
T
ype.
For contrast: With arrays, the syntax requires that the type enfolds
the object of the type: T A[N];
. The type of A
is T[N]
, but we must write the type around the
A
. If, however, we create a synonym for this type, we can
use it as a singular type:
c — Type alias for array
typedef int T[3]; // `T` is synonym for `int[5]`.
···= { 11, 22, 33 }; //← `A` has type `T`, i.e. `int[3]`.
T A int B[3] = { 11, 22, 33 }; //← `B` has type `int[3]`
* P = &A; //← `T*` is synonym for `int(*)[3]`.
Tint(*Q)[3] = &A; //← `P` and `Q` have the same type.
("%d %d %d %d\n", A[1], B[1], P[0][1], Q[0][1]); printf
The output will be 22
for all expressions passed to
printf()
. This is not useful for function types, which also
enfold the subject of the type, but conversely, it is very
useful for function pointer types.
Type Syntax Variations
Here is a complete summary of the shapes (syntax) for various
categories of types in C. The ◎
symbol indicates the
subject of the type, i.e., the language element to which the type in the
declaration or definition, is applied:
T ◎
\longleftarrow singular type. Includes structured and union types, or type synonyms.T* ◎
\longleftarrow pointer type. A derived type3.T ◎ [N]
\longleftarrow array type. A derived type.T ◎ (P)
\longleftarrow function type. Not a data type. Define or declare functions only.T(* ◎ )[M]
\longleftarrow pointer-to-array type.T(* ◎ )(P)
\longleftarrow pointer-to-function type.void
\longleftarrow abstract type.
Optional keywords used together with types, as type modifiers, or type specifiers, that affect the storage class, linkage, and volatility:
extern static volatile register const
These have no effect on the operators discussed, except for variables
with register
qualified types, whose addresses cannot be
taken.
Performing indirection on a const*
pointer yields a
const
, which is not an lvalue. Ordinarily, indirection
yields an lvalue.
Remember that subscript and indirect member selection, are also indirection expressions (or involve indirection, in the case of the indirect member selection operator).
Summary
All aspects of pointers, and indirection, are supported by a handful of rules. An (admittedly intimate) understanding a handful of core rules, is all that is required. This means mastery is eminently achievable. It does not preclude the requirement of understanding the other rules of C, but considering these are arguably the most complex, the premise should hold.
It should be apparent that arrays are only superficially supported in C, and that pointers, and the operators that employ them, play an indispensable role.
Pointers and Operators Summary
Summarized in the points below, T
is any type,
P
has type T*
, and A
is an array
of N
elements of type T
. V
is a
variable of type T
, and F
is a function taking
parm
parameters (any), returning a T
value.
X
can be any integer type (int
used here). In
short, assume these definitions and declarations as ‘given’, and
properly initialised:
T V; T* P; T A[N]; T F(parm); int X;
- An address is a number depicting the address of a byte in memory.
- A pointer type, e.g.,
T*
, depicts the address of aT
value. &V
(address ofV
) results in a value of typeT*
.*P
(indirectP
) means ‘represent aT
at addressP
’ and is an lvalue by default.P +/- I
is commutative, and equalsP +/- I * sizeof(T)
.A[X]
orX[A]
is rewritten as*(A + X)
or*(X + A)
respectively, before compilation. This is legal, but givenA
is the pointer operand, usingX[A]
is discouraged.A
results in a pointer to the first element, and thus has typeT*
. This applies to any expression representing an array, not just array names.&A
has type:T(*)[N]
(pointer-to-array).F
has type:T(*)(P)
(pointer-to-function).F(arg)
has typeT
(result of function call operator).P->M
is syntactic sugar (shorthand) for(*P).M
.
The last rule requires that P
is a pointer to a
structured type, and that M
is a member of that structured
type.
Obtaining Pointers Summary
Although some of the following points are implied by the previous summary, the focus here is on obtaining pointer type values only. Programmers can obtain a pointer type value by:
- Using the address-of operator on lvalues.
- Representing an array in an expression.
- Using the name of a function.
- Using a string literal.
Last Words
There are not many rules. Nor complicated ones. The main problem is
the syntax chosen for declarations, especially since the types can be
combined in endless combinations. Generally, many examples and hours of
practice are required before most programmers feel completely
comfortable with all these rules. But it is entirely possible. A
suggested course of action is to become familiar with these rules,
before trying to combine several types, since that tends to obscure the
patterns. The liberal use of typedef
is another feasible
technique.
2023-06-03: Rephrasing, edits and several new links to resources.
[brx]
2021-11-26: Fixed int*
where char*
should have
been. [brx]
2020-05-29: Formatting (due to Ockert van Schalkwyk's advice).
[brx]
2019-02-18: Changed type alias FP
to FT
.
[brx]
2018-10-17: Additional typedef
for function pointer types.
[brx]
2018-08-10: Code corrections, typography changes, small additions &
editing. [brx]
2018-05-24: Modified some output examples for arrays-of-arrays.
[brx]
2018-04-12: Fixed reported typos. [brx]
2017-11-16: Update to new admonitions. [brx]
2017-09-22: Editing and formatting. [jjc]
2017-03-11: Created. [brx]