Pointers
In earlier
chapters, variables have been explained as locations in the computer's memory
which can be accessed by their identifier (their name). This way, the program
does not need to care about the physical address of the data in memory; it simply
uses the identifier whenever it needs to refer to the variable.
For a C++ program,
the memory of a computer is like a succession of memory cells, each one byte in
size, and each with a unique address. These single-byte memory cells are
ordered in a way that allows data representations larger than one byte to
occupy memory cells that have consecutive addresses.
This way, each cell
can be easily located in the memory by means of its unique address. For
example, the memory cell with the address 1776 always follows
immediately after the cell with address 1775 and precedes the one
with 1777, and is exactly one thousand cells after 776 and
exactly one thousand cells before 2776.
When a variable is
declared, the memory needed to store its value is assigned a specific location
in memory (its memory address). Generally, C++ programs do not actively decide
the exact memory addresses where its variables are stored. Fortunately, that
task is left to the environment where the program is run - generally, an operating
system that decides the particular memory locations on runtime. However, it may
be useful for a program to be able to obtain the address of a variable during
runtime in order to access data cells that are at a certain position relative
to it.
Address-of
Operator (&)
The address of a
variable can be obtained by preceding the name of a variable with an ampersand
sign (&), known as address-of operator. For example:
|
foo = &myvar; |
This would assign the address of variable myvar to foo; by
preceding the name of the variable myvar with the address-of
operator (&), we are no longer assigning the content of the
variable itself to foo, but its address.
The actual address
of a variable in memory cannot be known before runtime, but let's assume, in
order to help clarify some concepts, that myvar is placed during
runtime in the memory address 1776.
In this case,
consider the following code fragment:
1 |
myvar = 25; foo = &myvar; bar = myvar; |
The values contained in each variable after the execution of this are shown in
the following diagram:
First, we have assigned the value 25 to myvar (a variable
whose address in memory we assumed to be 1776).
The second statement assigns foo the address of myvar, which we
have assumed to be 1776.
Finally, the third statement, assigns the value contained
in myvar to bar. This is a standard assignment operation, as
already done many times in earlier chapters.
The main difference between the second and third statements is the appearance
of the address-of operator (&).
The variable that stores the address of another variable (like foo in
the previous example) is what in C++ is called a pointer. Pointers
are a very powerful feature of the language that has many uses in lower level
programming. A bit later, we will see how to declare and use pointers.
Dereference Operator (*)
As just seen, a
variable which stores the address of another variable is called a pointer.
Pointers are said to "point to" the variable whose address they
store.
An interesting property of pointers is that they can be used to access the
variable they point to directly. This is done by preceding the pointer name
with the dereference operator (*). The operator itself can be
read as "value pointed to by".
Therefore, following with the values of the previous example, the following
statement:
|
baz = *foo; |
This could be read as: "baz equal to value pointed to
by foo", and the statement would actually assign the
value 25 to baz, since foo is 1776, and the value
pointed to by 1776 (following the example above) would be 25.
It is important to clearly differentiate that foo refers to the
value 1776, while *foo (with an asterisk * preceding
the identifier) refers to the value stored at address 1776, which in this
case is 25. Notice the difference of including or not including the dereference
operator (I have added an explanatory comment of how each of these two
expressions could be read):
1 |
baz = foo; // baz equal to foo (1776) baz = *foo; // baz equal to value pointed to by foo
(25) |
The reference and dereference operators are thus complementary:
Thus, they have sort of opposite meanings: An address obtained
with & can be dereferenced with *.
Earlier, we performed the following two assignment operations:
1 |
myvar = 25; foo = &myvar; |
Right after these two statements, all of the following expressions would give
true as result:
1 |
myvar == 25 &myvar ==
1776 foo == 1776 *foo == 25 |
The first expression is quite clear, considering that the assignment operation
performed on myvar was myvar=25. The second one uses the
address-of operator (&), which returns the address of myvar, which we
assumed it to have a value of 1776. The third one is somewhat obvious,
since the second expression was true and the assignment operation performed
on foo was foo=&myvar. The fourth expression uses the dereference
operator (*) that can be read as "value pointed to by", and
the value pointed to by foo is indeed 25.
So, after all that, you may also infer that for as long as the address pointed
to by foo remains unchanged, the following expression will also be true:
|
*foo == myvar |
Declaring Pointers
Due to the ability
of a pointer to directly refer to the value that it points to, a pointer has
different properties when it points to a char than when it points to
an int or a float. Once dereferenced, the type needs to be
known. And for that, the declaration of a pointer needs to include the data
type the pointer is going to point to.
The declaration of pointers follows this syntax:
type * name;
where type is the data type pointed to by the pointer. This type is
not the type of the pointer itself, but the type of the data the pointer points
to. For example:
1 |
int * number; char * character; double *
decimals; |
These are three declarations of pointers. Each one is intended to point to a different
data type, but, in fact, all of them are pointers and all of them are likely
going to occupy the same amount of space in memory (the size in memory of a
pointer depends on the platform where the program runs). Nevertheless, the data
to which they point to do not occupy the same amount of space nor are of the
same type: the first one points to an int, the second one to a char,
and the last one to a double. Therefore, although these three example
variables are all of them pointers, they actually have different
types: int*, char*, and double* respectively, depending on
the type they point to.
Note that the asterisk (*) used when declaring a pointer only means that it is
a pointer (it is part of its type compound specifier), and should not be
confused with the dereference operator seen a bit earlier, but
which is also written with an asterisk (*). They are simply two different
things represented with the same sign.
Let's see an example on pointers:
1 |
// my first
pointer #include
<iostream> using namespace
std; int main () { int firstvalue, secondvalue; int * mypointer; mypointer = &firstvalue; *mypointer = 10; mypointer = &secondvalue; *mypointer = 20; cout << "firstvalue is "
<< firstvalue << '\n'; cout << "secondvalue is "
<< secondvalue << '\n'; return 0; } |
firstvalue is 10 secondvalue is 20 |
Notice that even though
neither firstvalue nor secondvalue are directly set any
value in the program, both end up with a value set indirectly through the use
of mypointer. This is how it happens:
First, mypointer is assigned the address of firstvalue using the
address-of operator (&). Then, the value pointed to
by mypointer is assigned a value of 10. Because, at this
moment, mypointer is pointing to the memory location
of firstvalue, this in fact modifies the value of firstvalue.
In order to demonstrate that a pointer may point to different variables during
its lifetime in a program, the example repeats the process
with secondvalue and that same pointer, mypointer.
Here is an example a little bit more elaborated:
1 |
// more pointers #include
<iostream> using namespace
std; int main () { int firstvalue = 5, secondvalue = 15; int * p1, * p2; p1 = &firstvalue; // p1 = address of firstvalue p2 = &secondvalue; // p2 = address of
secondvalue *p1 = 10; // value pointed to by p1 = 10 *p2 = *p1; // value pointed to by p2 = value
pointed to by p1 p1 = p2; // p1 = p2 (value of pointer is
copied) *p1 = 20; // value pointed to by p1 = 20 cout << "firstvalue is "
<< firstvalue << '\n'; cout << "secondvalue is "
<< secondvalue << '\n'; return 0; } |
firstvalue is 10 secondvalue is 20 |
Each assignment operation includes a comment on how each line could be read:
i.e., replacing ampersands (&) by "address of", and asterisks (*)
by "value pointed to by".
Notice that there are expressions with pointers p1 and p2, both
with and without the dereference operator (*). The meaning of
an expression using the dereference operator (*) is very
different from one that does not. When this operator precedes the pointer name,
the expression refers to the value being pointed, while when a pointer name
appears without this operator, it refers to the value of the pointer itself
(i.e., the address of what the pointer is pointing to).
Another thing that may call your attention is the line:
|
int * p1, * p2; |
This declares the two pointers used in the previous example. But notice that
there is an asterisk (*) for each pointer, in order for both to have
type int* (pointer to int). This is required due to the
precedence rules. Note that if, instead, the code was:
|
int * p1, p2; |
p1 would indeed be of type int*, but p2 would be of
type int. Spaces do not matter at all for this purpose. But anyway, simply
remembering to put one asterisk per pointer is enough for most pointer users
interested in declaring multiple pointers per statement. Or even better: use a
different statement for each variable.
Pointers and Arrays
The concept of
arrays is related to that of pointers. In fact, arrays work very much like
pointers to their first elements, and, actually, an array can always be
implicitly converted to the pointer of the proper type. For example, consider
these two declarations:
1 |
int myarray [20]; int * mypointer; |
The following assignment operation would be valid:
|
mypointer =
myarray; |
After that, mypointer and myarray would be equivalent and
would have very similar properties. The main difference being
that mypointer can be assigned a different address,
whereas myarray can never be assigned anything, and will always
represent the same block of 20 elements of type int. Therefore, the
following assignment would not be valid:
|
myarray =
mypointer; |
Let's see an example that mixes arrays and pointers:
1 |
// more pointers #include
<iostream> using namespace
std; int main () { int numbers[5]; int * p; p = numbers; *p = 10; p++;
*p = 20; p = &numbers[2]; *p = 30; p = numbers + 3; *p = 40; p = numbers; *(p+4) = 50; for (int n=0; n<5; n++) cout << numbers[n] << ",
"; return 0; } |
10, 20, 30, 40,
50, |
Pointers and arrays support the same set of operations, with the same meaning
for both. The main difference being that pointers can be assigned new
addresses, while arrays cannot.
In the chapter about arrays, brackets ([]) were explained as specifying the
index of an element of the array. Well, in fact these brackets are a
dereferencing operator known as offset operator. They dereference
the variable they follow just as * does, but they also add the number
between brackets to the address being dereferenced. For example:
1 |
a[5] = 0; // a [offset of 5] = 0 *(a+5) = 0; // pointed to by (a+5) = 0 |
These two expressions are equivalent and valid, not only if a is a
pointer, but also if a is an array. Remember that if an array, its
name can be used just like a pointer to its first element.
Pointer initialization
Pointers can be
initialized to point to specific locations at the very moment they are defined:
1 |
int myvar; int * myptr =
&myvar; |
The resulting state of variables after this code is the same as after:
1 |
int myvar; int * myptr; myptr =
&myvar; |
When pointers are initialized, what is initialized is the address they point to
(i.e., myptr), never the value being pointed (i.e., *myptr).
Therefore, the code above shall not be confused with:
1 |
int myvar; int * myptr; *myptr =
&myvar; |
Which anyway would not make much sense (and is not valid code).
The asterisk (*) in the pointer declaration (line 2) only indicates that it is
a pointer, it is not the dereference operator (as in line 3). Both things just
happen to use the same sign: *. As always, spaces are not relevant, and
never change the meaning of an expression.
Pointers can be initialized either to the address of a variable (such as in the
case above), or to the value of another pointer (or array):
1 |
int myvar; int *foo =
&myvar; int *bar = foo; |
Pointer arithmetics
To conduct arithmetical
operations on pointers is a little different than to conduct them on regular
integer types. To begin with, only addition and subtraction operations are
allowed; the others make no sense in the world of pointers. But both addition
and subtraction have a slightly different behavior with pointers, according to
the size of the data type to which they point.
When fundamental data types were introduced, we saw that types have different
sizes. For example: char always has a size of 1
byte, short is generally larger than that,
and int and long are even larger; the exact size of these
being dependent on the system. For example, let's imagine that in a given
system, char takes 1 byte, short takes 2 bytes, and long takes
4.
Suppose now that we define three pointers in this compiler:
1 |
char *mychar; short *myshort; long *mylong; |
and that we know that they point to the memory locations 1000, 2000,
and 3000, respectively.
Therefore, if we write:
1 |
++mychar; ++myshort; ++mylong; |
mychar, as one would expect, would contain the value 1001. But not so
obviously, myshort would contain the value 2002,
and mylong would contain 3004, even though they have each been
incremented only once. The reason is that, when adding one to a pointer, the
pointer is made to point to the following element of the same type, and,
therefore, the size in bytes of the type it points to is added to the pointer.
This is applicable both when adding and subtracting any number to a pointer. It
would happen exactly the same if we wrote:
1 |
mychar = mychar +
1; myshort = myshort
+ 1; mylong = mylong +
1; |
Regarding the increment (++) and decrement (--) operators, they both can be
used as either prefix or suffix of an expression, with a slight difference in
behavior: as a prefix, the increment happens before the expression is
evaluated, and as a suffix, the increment happens after the expression is
evaluated. This also applies to expressions incrementing and decrementing
pointers, which can become part of more complicated expressions that also
include dereference operators (*). Remembering operator precedence rules, we
can recall that postfix operators, such as increment and decrement, have higher
precedence than prefix operators, such as the dereference operator (*).
Therefore, the following expression:
|
*p++ |
is equivalent to *(p++). And what it does is to increase the value
of p (so it now points to the next element), but
because ++ is used as postfix, the whole expression is evaluated as
the value pointed originally by the pointer (the address it pointed to before
being incremented).
Essentially, these are the four possible combinations of the dereference
operator with both the prefix and suffix versions of the increment operator
(the same being applicable also to the decrement operator):
1 |
*p++ // same as *(p++): increment pointer, and
dereference unincremented address *++p // same as *(++p): increment pointer, and
dereference incremented address ++*p // same as ++(*p): dereference pointer,
and increment the value it points to (*p)++ //
dereference pointer, and post-increment the value it points to |
A typical -but not so simple- statement involving these operators is:
|
*p++ = *q++; |
Because ++ has a higher precedence than *,
both p and q are incremented, but because both increment
operators (++) are used as postfix and not prefix, the value assigned
to *p is *q before both p and q are
incremented. And then both are incremented. It would be roughly equivalent to:
1 |
*p = *q; ++p; ++q; |
Like always, parentheses reduce confusion by adding legibility to expressions.
Pointers and Const
Pointers can be used
to access a variable by its address, and this access may include modifying the
value pointed. But it is also possible to declare pointers that can access the
pointed value to read it, but not to modify it. For this, it is enough with
qualifying the type pointed to by the pointer as const. For example:
1 |
int x; int y = 10; const int * p =
&y; x = *p; // ok: reading p *p = x; // error: modifying p, which is
const-qualified |
Here p points to a variable, but points to it in a const-qualified
manner, meaning that it can read the value pointed, but it cannot modify it.
Note also, that the expression &y is of type int*, but this
is assigned to a pointer of type const int*. This is allowed: a pointer to
non-const can be implicitly converted to a pointer to const. But not the other
way around! As a safety feature, pointers to const are not implicitly
convertible to pointers to non-const.
One of the use cases of pointers to const elements is as function
parameters: a function that takes a pointer to non-const as parameter can
modify the value passed as argument, while a function that takes a pointer
to const as parameter cannot.
1 |
// pointers as
arguments: #include
<iostream> using namespace
std; void
increment_all (int* start, int* stop) { int * current = start; while (current != stop) { ++(*current); // increment value pointed ++current; // increment pointer } } void print_all
(const int* start, const int* stop) { const int * current = start; while (current != stop) { cout << *current << '\n'; ++current; // increment pointer } } int main () { int numbers[] = {10,20,30}; increment_all (numbers,numbers+3); print_all (numbers,numbers+3); return 0; } |
11 21 31 |
Note that print_all uses pointers that point to constant elements.
These pointers point to constant content they cannot modify, but they are not
constant themselves: i.e., the pointers can still be incremented or assigned
different addresses, although they cannot modify the content they point to.
And this is where a second dimension to constness is added to pointers:
Pointers can also be themselves const. And this is specified by appending const
to the pointed type (after the asterisk):
1 |
int x; int * p1 = &x; // non-const pointer to non-const int const int * p2 = &x; // non-const pointer to const int int * const p3 = &x; // const pointer to non-const int const int * const
p4 = &x; // const pointer to const
int |
The syntax with const and pointers is definitely tricky, and
recognizing the cases that best suit each use tends to require some experience.
In any case, it is important to get constness with pointers (and references)
right sooner rather than later, but you should not worry too much about
grasping everything if this is the first time you are exposed to the mix
of const and pointers. More use cases will show up in coming
chapters.
To add a little bit more confusion to the syntax of const with
pointers, the const qualifier can either precede or follow the
pointed type, with the exact same meaning:
1 |
const int * p2a =
&x; // non-const pointer to const int int const * p2b =
&x; // also non-const pointer to
const int |
As with the spaces surrounding the asterisk, the order of const in this case is
simply a matter of style. This chapter uses a prefix const, as for
historical reasons this seems to be more extended, but both are exactly
equivalent. The merits of each style are still intensely debated on the
internet.
Pointers and String Literals
As pointed
earlier, string literals are arrays containing null-terminated
character sequences. In earlier sections, string literals have been used to be directly
inserted into cout, to initialize strings and to initialize arrays of
characters.
But they can also be accessed directly. String literals are arrays of the
proper array type to contain all its characters plus the terminating
null-character, with each of the elements being of type const
char (as literals, they can never be modified).
For example:
|
const char * foo
= "hello"; |
This declares an array with the literal representation
for "hello", and then a pointer to its first element is assigned
to foo. If we imagine that "hello" is stored at the
memory locations that start at address 1702, we can represent the previous
declaration as:
Note that here foo is a pointer and contains the value 1702, and
not 'h', nor "hello", although 1702 indeed is the address
of both of these.
The pointer foo points to a sequence of characters. And because
pointers and arrays behave essentially in the same way in
expressions, foo can be used to access the characters in the same way
arrays of null-terminated character sequences are.
For example:
1 |
*(foo+4) foo[4] |
Both expressions have a value of 'o' (the fifth element of the
array).
Pointers to pointers
C++ allows the use
of pointers that point to pointers, that these, in its turn, point to data (or even
to other pointers). The syntax simply requires an asterisk (*) for each level
of indirection in the declaration of the pointer:
1 |
char a; char * b; char ** c; a = 'z'; b = &a; c = &b; |
This, assuming the randomly chosen memory locations for each variable
of 7230, 8092, and 10502, could be represented as:
With the value of
each variable represented inside its corresponding cell, and their respective
addresses in memory represented by the value under them.
The new thing in this example is variable c, which is a pointer to a
pointer, and can be used in three different levels of indirection, each one of
them would correspond to a different value:
Invalid pointers and null pointers
In principle,
pointers are meant to point to valid addresses, such as the address of a
variable or the address of an element in an array. But pointers can actually
point to any address, including addresses that do not refer to any valid
element. Typical examples of this are uninitialized pointers and
pointers to nonexistent elements of an array:
1 |
int * p; // uninitialized pointer
(local variable) int myarray[10]; int * q =
myarray+20; // element out of bounds |
Neither p nor q point to addresses known to contain a
value, but none of the above statements causes an error. In C++, pointers are allowed
to take any address value, no matter whether there actually is something at
that address or not. What can cause an error is to dereference such a pointer
(i.e., actually accessing the value they point to). Accessing such a pointer
causes undefined behavior, ranging from an error during runtime to accessing
some random value.
But, sometimes, a pointer really needs to explicitly point to nowhere, and not
just an invalid address. For such cases, there exists a special value that any
pointer type can take: the null pointer value. This value can be
expressed in C++ in two ways: either with an integer value of zero, or with
the nullptr keyword:
1 |
int * p = 0; int * q =
nullptr; |
Here, both p and q are null pointers, meaning
that they explicitly point to nowhere, and they both actually compare equal:
all null pointers compare equal to other null pointers.
It is also quite usual to see the defined constant NULL be used in
older code to refer to the null pointer value:
|
int * r = NULL; |
NULL is defined in several headers of the standard library, and is defined
as an alias of some null pointer constant value (such
as 0 or nullptr).
Do not confuse null pointers with void pointers!
A null pointer is a value that any pointer can take to
represent that it is pointing to "nowhere", while
a void pointer is a type of pointer that can point to somewhere
without a specific type. One refers to the value stored in the pointer, and the
other to the type of data it points to.
Pointers to functions
C++ allows
operations with pointers to functions. The typical use of this is for passing a
function as an argument to another function. Pointers to functions are declared
with the same syntax as a regular function declaration, except that the name of
the function is enclosed between parentheses () and an asterisk (*) is inserted
before the name:
1 |
// pointer to
functions #include
<iostream> using namespace
std; int addition (int
a, int b) { return (a+b); } int subtraction
(int a, int b) { return (a-b); } int operation
(int x, int y, int (*functocall)(int,int)) { int g; g = (*functocall)(x,y); return (g); } int main () { int m,n; int (*minus)(int,int) = subtraction; m = operation (7, 5, addition); n = operation (20, m, minus); cout <<n; return 0; } |
8 |
In the example above, minus is a pointer to a function that has two
parameters of type int. It is directly initialized to point to the
function subtraction:
|
int (* minus)(int,int)
= subtraction; |
C++ added the
so-called reference variables (or references in short). A reference is an alias, or an alternate name to
an existing variable. For example, suppose you make peter
a reference (alias) to paul
, you can refer to the person as either peter
or paul
.
The main use of
references is acting as function formal parameters to support
pass-by-reference. In an reference variable is passed into a function, the
function works on the original copy (instead of a clone copy in pass-by-value).
Changes inside the function are reflected outside the function.
A reference is
similar to a pointer. In many cases, a reference can be used as an alternative
to pointer, in particular, for the function parameter.
Recall that C/C++
use &
to denote the address-of operator in an expression. C++ assigns
an additional meaning to &
in
declaration to declare a reference variable.
The meaning of
symbol &
is different
in an expression and in a declaration. When it is used in an expression, &
denotes the address-of operator, which
returns the address of a variable, e.g., if number
is
an int
variable, &number
returns the address of the
variable number
(this has
been described in the above section).
Howeve,
when &
is used in a declaration (including function formal parameters), it is part of the type
identifier and is used to declare a reference
variable (or reference or alias or alternate name).
It is used to provide another name,
or another reference,
or alias to an existing
variable.
The syntax is as
follow:
type &newName = existingName;
// or
type& newName = existingName;
// or
type & newName = existingName; // I shall adopt this convention
It
shall be read as "newName is a
reference to exisitngName
",
or "newNew is an alias of existingName
". You can now refer to the
variable as newName or existingName.
For
example,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
/* Test reference declaration and initialization (TestReferenceDeclaration.cpp) */ #include <iostream> using namespace std; int main() { int number = 88; // Declare an int variable called number int & refNumber = number; // Declare a reference (alias) to the variable number // Both refNumber and number refer to the same value cout << number << endl; // Print value of variable number (88) cout << refNumber << endl; // Print value of reference (88) refNumber = 99; // Re-assign a new value to refNumber cout << refNumber << endl; cout << number << endl; // Value of number also changes (99) number = 55; // Re-assign a new value to number cout << number << endl; cout << refNumber << endl; // Value of refNumber also changes (55) } |
How References Work?
A
reference works as a pointer. A reference is declared as an alias of a
variable. It stores the address of the variable, as illustrated:
References vs. Pointers
Pointers
and references are equivalent, except:
1.
A reference is a name constant for
an address. You need to initialize the reference during
declaration.
int & iRef; // Error: 'iRef' declared as reference but not initialized
Once
a reference is established to a variable, you cannot change the reference to
reference another variable.
2.
To get the value pointed to by a pointer, you need to use the
dereferencing operator *
(e.g., if pNumber
is
a int
pointer, *pNumber
returns
the value pointed to by pNumber
. It is
called dereferencing or indirection). To assign an address of a variable into
a pointer, you need to use the address-of operator &
(e.g., pNumber
= &number
).
3.
On the other hand, referencing and dereferencing are done on the
references implicitly. For example, if refNumber
is a
reference (alias) to another int
variable, refNumber
returns
the value of the variable. No explicit dereferencing operator *
should
be used. Furthermore, to assign an address of a variable to a reference variable,
no address-of operator &
is needed.
For
example,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
/* References vs. Pointers (TestReferenceVsPointer.cpp) */ #include <iostream> using namespace std; int main() { int number1 = 88, number2 = 22; // Create a pointer pointing to number1 int * pNumber1 = &number1; // Explicit referencing *pNumber1 = 99; // Explicit dereferencing cout << *pNumber1 << endl; // 99 cout << &number1 << endl; // 0x22ff18 cout << pNumber1 << endl; // 0x22ff18 (content of the pointer variable - same as above) cout << &pNumber1 << endl; // 0x22ff10 (address of the pointer variable) pNumber1 = &number2; // Pointer can be reassigned to store another address // Create a reference (alias) to number1 int & refNumber1 = number1; // Implicit referencing (NOT &number1) refNumber1 = 11; // Implicit dereferencing (NOT *refNumber1) cout << refNumber1 << endl; // 11 cout << &number1 << endl; // 0x22ff18 cout << &refNumber1 << endl; // 0x22ff18 //refNumber1 = &number2; // Error! Reference cannot be re-assigned // error: invalid conversion from 'int*' to 'int' refNumber1 = number2; // refNumber1 is still an alias to number1. // Assign value of number2 (22) to refNumber1 (and number1). number2++; cout << refNumber1 << endl; // 22 cout << number1 << endl; // 22 cout << number2 << endl; // 23 } |
A
reference variable provides a new name to an existing variable. It is dereferenced implicitly and does not need the
dereferencing operator *
to
retrieve the value referenced. On the other hand, a pointer variable stores an
address. You can change the address value stored in a pointer. To retrieve the
value pointed to by a pointer, you need to use the indirection operator *
, which is known as explicit dereferencing. Reference can be treated as
a const
pointer. It has to be
initialized during declaration, and its content cannot be changed.
Reference
is closely related to pointer. In many cases, it can be used as an alternative
to pointer. A reference allows you to manipulate an object using pointer, but without
the pointer syntax of referencing and dereferencing.
The
above example illustrates how reference works, but does not show its typical
usage, which is used as the function formal parameter for pass-by-reference.
Pass-By-Reference into Functions with Reference
Arguments vs. Pointer Arguments
In
C/C++, by default, arguments are passed into functions by value (except arrays which is treated as
pointers). That is, a clone copy of the argument is made and passed into the function.
Changes to the clone copy inside the function has no effect to the original
argument in the caller. In other words, the called function has no access to
the variables in the caller. For example,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
/* Pass-by-value into function (TestPassByValue.cpp) */ #include <iostream> using namespace std; int square(int); int main() { int number = 8; cout << "In main(): " << &number << endl; // 0x22ff1c cout << number << endl; // 8 cout << square(number) << endl; // 64 cout << number << endl; // 8 - no change } int square(int n) { // non-const cout << "In square(): " << &n << endl; // 0x22ff00 n *= n; // clone modified inside the function return n; } |
The
output clearly shows that there are two different addresses.
In
many situations, we may wish to modify the original copy directly (especially in
passing huge object or array) to avoid the overhead of cloning. This can be
done by passing a pointer of the object into the function, known as pass-by-reference. For example,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
/* Pass-by-reference using pointer (TestPassByPointer.cpp) */ #include <iostream> using namespace std; void square(int *); int main() { int number = 8; cout << "In main(): " << &number << endl; // 0x22ff1c cout << number << endl; // 8 square(&number); // Explicit referencing to pass an address cout << number << endl; // 64 } void square(int * pNumber) { // Function takes an int pointer (non-const) cout << "In square(): " << pNumber << endl; // 0x22ff1c *pNumber *= *pNumber; // Explicit de-referencing to get the value pointed-to } |
The
called function operates on the same address, and can thus modify the variable
in the caller.
Instead
of passing pointers into function, you could also pass references into
function, to avoid the clumsy syntax of referencing and dereferencing. For
example,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
/* Pass-by-reference using reference (TestPassByReference.cpp) */ #include <iostream> using namespace std; void square(int &); int main() { int number = 8; cout << "In main(): " << &number << endl; // 0x22ff1c cout << number << endl; // 8 square(number); // Implicit referencing (without '&') cout << number << endl; // 64 } void square(int & rNumber) { // Function takes an int reference (non-const) cout << "In square(): " << &rNumber << endl; // 0x22ff1c rNumber *= rNumber; // Implicit de-referencing (without '*') } |
Again,
the output shows that the called function operates on the same address, and can
thus modify the caller's variable.
Take
note referencing (in the caller) and dereferencing (in the function) are done
implicitly. The only coding difference with pass-by-value is in the function's
parameter declaration.
Recall
that references are to be initialized during declaration. In the case of function
formal parameter, the references are initialized when the function is invoked,
to the caller's arguments.
References
are primarily used in passing reference in/out of functions to allow the called
function accesses variables in the caller directly.
A const
function formal parameter cannot be
modified inside the function. Use const
whenever
possible as it protects you from inadvertently modifying the parameter and
protects you against many programming errors.
A const
function parameter can receive both const
and non-const
argument.
On the other hand, a non-const
function
reference/pointer parameter can only receive non-const
argument.
For example,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
/* Test Function const and non-const parameter (FuncationConstParameter.cpp) */ #include <iostream> using namespace std; int squareConst(const int); int squareNonConst(int); int squareConstRef(const int &); int squareNonConstRef(int &); int main() { int number = 8; const int constNumber = 9; cout << squareConst(number) << endl; cout << squareConst(constNumber) << endl; cout << squareNonConst(number) << endl; cout << squareNonConst(constNumber) << endl; cout << squareConstRef(number) << endl; cout << squareConstRef(constNumber) << endl; cout << squareNonConstRef(number) << endl; // cout << squareNonConstRef(constNumber) << endl; // error: invalid initialization of reference of // type 'int&' from expression of type 'const int' } int squareConst(const int number) { // number *= number; // error: assignment of read-only parameter return number * number; } int squareNonConst(int number) { // non-const parameter number *= number; return number; } int squareConstRef(const int & number) { // const reference return number * number; } int squareNonConstRef(int & number) { // non-const reference return number * number; } |
You
can also pass the return-value as reference or pointer. For example,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
/* Passing back return value using reference (TestPassByReferenceReturn.cpp) */ #include <iostream> using namespace std; int & squareRef(int &); int * squarePtr(int *); int main() { int number1 = 8; cout << "In main() &number1: " << &number1 << endl; // 0x22ff14 int & result = squareRef(number1); cout << "In main() &result: " << &result << endl; // 0x22ff14 cout << result << endl; // 64 cout << number1 << endl; // 64 int number2 = 9; cout << "In main() &number2: " << &number2 << endl; // 0x22ff10 int * pResult = squarePtr(&number2); cout << "In main() pResult: " << pResult << endl; // 0x22ff10 cout << *pResult << endl; // 81 cout << number2 << endl; // 81 } int & squareRef(int & rNumber) { cout << "In squareRef(): " << &rNumber << endl; // 0x22ff14 rNumber *= rNumber; return rNumber; } int * squarePtr(int * pNumber) { cout << "In squarePtr(): " << pNumber << endl; // 0x22ff10 *pNumber *= *pNumber; return pNumber; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
/* Test passing the result (TestPassResultLocal.cpp) */ #include <iostream> using namespace std; int * squarePtr(int); int & squareRef(int); int main() { int number = 8; cout << number << endl; // 8 cout << *squarePtr(number) << endl; // ?? cout << squareRef(number) << endl; // ?? } int * squarePtr(int number) { int localResult = number * number; return &localResult; // warning: address of local variable 'localResult' returned } int & squareRef(int number) { int localResult = number * number; return localResult; // warning: reference of local variable 'localResult' returned } |
This
program has a serious logical error, as local variable of function is passed
back as return value by reference. Local variable has local scope within the
function, and its value is destroyed after the function exits. The GCC compiler
is kind enough to issue a warning (but not error).
It
is safe to return a reference that is passed into the function as an argument.
See earlier examples.
Instead,
you need to dynamically allocate a variable for the return value, and return it
by reference.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
/* Test passing the result (TestPassResultNew.cpp) */ #include <iostream> using namespace std; int * squarePtr(int); int & squareRef(int); int main() { int number = 8; cout << number << endl; // 8 cout << *squarePtr(number) << endl; // 64 cout << squareRef(number) << endl; // 64 } int * squarePtr(int number) { int * dynamicAllocatedResult = new int(number * number); return dynamicAllocatedResult; } int & squareRef(int number) { int * dynamicAllocatedResult = new int(number * number); return *dynamicAllocatedResult; } |
Instead
of define an int
variable (int number
), and assign the address of the variable
to the int
pointer (int *pNumber = &number
), the storage can be
dynamically allocated at runtime, via a new
operator.
In C++, whenever you allocate a piece of memory dynamically via new
, you need to use delete
to remove the storage (i.e., to return
the storage to the heap).
The new
operation returns a pointer to the memory
allocated. The delete
operator
takes a pointer (pointing to the memory allocated via new
) as its sole argument.
For
example,
// Static allocation
int number = 88;
int * p1 = &number; // Assign a "valid" address into pointer
// Dynamic Allocation
int * p2; // Not initialize, points to somewhere which is invalid
cout << p2 << endl; // Print address before allocation
p2 = new int; // Dynamically allocate an int and assign its address to pointer
// The pointer gets a valid address with memory allocated
*p2 = 99;
cout << p2 << endl; // Print address after allocation
cout << *p2 << endl; // Print value point-to
delete p2; // Remove the dynamically allocated storage
Observe
that new
and delete
operators work on pointer.
To
initialize the allocated memory, you can use an initializer for fundamental
types, or invoke a constructor for an object. For example,
// use an initializer to initialize a fundamental type (such as int, double)
int * p1 = new int(88);
double * p2 = new double(1.23);
// C++11 brace initialization syntax
int * p1 = new int {88};
double * p2 = new double {1.23};
// invoke a constructor to initialize an object (such as Date, Time)
Date * date1 = new Date(1999, 1, 1);
Time * time1 = new Time(12, 34, 56);
You
can dynamically allocate storage for global pointers
inside a function. Dynamically allocated storage inside the function remains
even after the function exits. For example,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// Dynamically allocate global pointers (TestDynamicAllocation.cpp) #include <iostream> using namespace std; int * p1, * p2; // Global int pointers // This function allocates storage for the int* // which is available outside the function void allocate() { p1 = new int; // Allocate memory, initial content unknown *p1 = 88; // Assign value into location pointed to by pointer p2 = new int(99); // Allocate and initialize } int main() { allocate(); cout << *p1 << endl; // 88 cout << *p2 << endl; // 99 delete p1; // Deallocate delete p2; return 0; } |
The
main differences between static allocation and dynamic allocations are:
In
static allocation, the compiler allocates and deallocates the storage automatically,
and handle memory management. Whereas in dynamic allocation, you, as the
programmer, handle the memory allocation and deallocation yourself (via new
and delete
operators).
You have full control on the pointer addresses and their contents, as well as
memory management.
Static
allocated entities are manipulated through named variables. Dynamic allocated
entities are handled through pointers.
Dynamic
array is allocated at runtime rather than compile-time, via the new[]
operator. To remove the storage, you need
to use the delete[]
operator
(instead of simply delete
).
For example,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
/* Test dynamic allocation of array (TestDynamicArray.cpp) */ #include <iostream> #include <cstdlib> using namespace std; int main() { const int SIZE = 5; int * pArray; pArray = new int[SIZE]; // Allocate array via new[] operator // Assign random numbers between 0 and 99 for (int i = 0; i < SIZE; ++i) { *(pArray + i) = rand() % 100; } // Print array for (int i = 0; i < SIZE; ++i) { cout << *(pArray + i) << " "; } cout << endl; delete[] pArray; // Deallocate array via delete[] operator return 0; } |
C++03
does not allow your to initialize the dynamically-allocated array. C++11 does
with the brace initialization, as follows:
// C++11
int * p = new int[5] {1, 2, 3, 4, 5};
Size of Array
The
operation sizeof(
arrayName)
returns
the total bytes of the array.
You can derive the length (size) of the array by dividing it with the size of
an element (e.g. element 0). For example,
int numbers[100];
cout << sizeof(numbers) << endl; // Size of entire array in bytes (400)
cout << sizeof(numbers[0]) << endl; // Size of first element of the array in bytes (4)
cout << "Array size is " << sizeof(numbers) / sizeof(numbers[0]) << endl; // (100)
Passing Array
In/Out of a Function
An
array is passed into a function as a pointer to the
first element of the array. You can use array notation
(e.g., int[]
) or pointer
notation (e.g., int*
) in the function
declaration. The compiler always treats it as pointer (e.g., int*
). For example, the following declarations are
equivalent:
int max(int numbers[], int size);
int max(int *numbers, int size);
int max(int number[50], int size);
They
will be treated as int*
by
the compiler, as follow. The size of the array given in []
is ignored.
int max(int*, int);
Array
is passed by reference into the function, because a pointer is passed instead
of a clone copy. If the array is modified inside the function, the
modifications are applied to the caller's copy. You could declare the array
parameter as const
to prevent
the array from being modified inside the function.
The
size of the array is not part of the array parameter, and needs to be passed in
another int
parameter.
Compiler is not able to deduce the array size from the array pointer, and does
not perform array bound check.
Example: Using the
usual array notation.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
/* Passing array in/out function (TestArrayPassing.cpp) */ #include <iostream> using namespace std; // Function prototypes int max(const int arr[], int size); void replaceByMax(int arr[], int size); void print(const int arr[], int size); int main() { const int SIZE = 4; int numbers[SIZE] = {11, 22, 33, 22}; print(numbers, SIZE); cout << max(numbers, SIZE) << endl; replaceByMax(numbers, SIZE); print(numbers, SIZE); } // Return the maximum value of the given array. // The array is declared const, and cannot be modified inside the function. int max(const int arr[], int size) { int max = arr[0]; for (int i = 1; i < size; ++i) { if (max < arr[i]) max = arr[i]; } return max; } // Replace all elements of the given array by its maximum value // Array is passed by reference. Modify the caller's copy. void replaceByMax(int arr[], int size) { int maxValue = max(arr, size); for (int i = 0; i < size; ++i) { arr[i] = maxValue; } } // Print the array's content void print(const int arr[], int size) { cout << "{"; for (int i = 0; i < size; ++i) { cout << arr[i]; if (i < size - 1) cout << ","; } cout << "}" << endl; } |
Take
note that you can modify the contents of the caller's array inside the
function, as array is passed by reference. To prevent accidental modification,
you could apply const
qualifier to
the function's parameter. Recall that const
inform
the compiler that the value should not be changed. For example, suppose that
the function print()
prints the
contents of the given array and does not modify the array, you could
apply const
to both the
array name and its size, as they are not expected to be changed inside the
function.
void print(const int arr[], int size);
Compiler
flags out an error "assignment of read-only location" if it detected
a const
value would be
changed.
Example: Using pointer
notation.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
/* Passing array in/out function using pointer (TestArrayPassingPointer.cpp) */ #include <iostream> using namespace std; // Function prototype int max(const int *arr, int size); int main() { const int SIZE = 5; int numbers[SIZE] = {10, 20, 90, 76, 22}; cout << max(numbers, SIZE) << endl; } // Return the maximum value of the given array int max(const int *arr, int size) { int max = *arr; for (int i = 1; i < size; ++i) { if (max < *(arr+i)) max = *(arr+i); } return max; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
/* Test sizeof array (TestSizeofArray.cpp) */ #include <iostream> using namespace std; // Function prototypes void fun(const int *arr, int size); // Test Driver int main() { const int SIZE = 5; int a[SIZE] = {8, 4, 5, 3, 2}; cout << "sizeof in main() is " << sizeof(a) << endl; cout << "address in main() is " << a << endl; fun(a, SIZE); } // Function definitions void fun(const int *arr, int size) { cout << "sizeof in function is " << sizeof(arr) << endl; cout << "address in function is " << arr << endl; } |
sizeof in main() is 20
address in main() is 0x22fefc
sizeof in function is 4
address in function is 0x22fefc
The
address of arrays in main()
and
the function are the same, as expected, as array is passed by reference.
In main()
, the sizeof
array
is 20 (4 bytes per int
,
length of 5). Inside the function, the sizeof
is
4, which is the sizeof
int
pointer (4-byte address). This is why you
need to pass the size into the function.
Operating
on a Range of an Array
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
/* Function to compute the sum of a range of an array (SumArrayRange.cpp) */ #include <iostream> using namespace std; // Function prototype int sum(const int *begin, const int *end); // Test Driver int main() { int a[] = {8, 4, 5, 3, 2, 1, 4, 8}; cout << sum(a, a+8) << endl; // a[0] to a[7] cout << sum(a+2, a+5) << endl; // a[2] to a[4] cout << sum(&a[2], &a[5]) << endl; // a[2] to a[4] } // Function definition // Return the sum of the given array of the range from // begin to end, exclude end. int sum(const int *begin, const int *end) { int sum = 0; for (const int *p = begin; p != end; ++p) { sum += *p; } return sum; } |
Program
Notes:
·
To write a function that operates on a range of the given array,
you can pass the begin pointer and the end pointer into the function. By
convention, the operation shall start at the begin pointer, up to the end
pointer, but excluding the end pointer.
·
In "const int *p
", *p
(content
pointed-to) is constant, but p
is not constant.
More On Pointers
In
C/C++, functions, like all data items, have an address. The name of a function
is the starting address where the function resides in the memory, and
therefore, can be treated as a pointer. We can pass a function pointer into
function as well. The syntax for declaring a function pointer is:
// Function-pointer declaration
return-type (* function-ptr-name) (parameter-list)
// Examples
double (*fp)(int, int) // fp points to a function that takes two ints and returns a double (function-pointer)
double *dp; // dp points to a double (double-pointer)
double *fun(int, int) // fun is a function that takes two ints and returns a double-pointer
double f(int, int); // f is a function that takes two ints and returns a double
fp = f; // Assign function f to fp function-pointer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
/* Test Function Pointers (TestFunctionPointer.cpp) */ #include <iostream> using namespace std; int arithmetic(int, int, int (*)(int, int)); // Take 3 arguments, 2 int's and a function pointer // int (*)(int, int), which takes two int's and return an int int add(int, int); int sub(int, int); int add(int n1, int n2) { return n1 + n2; } int sub(int n1, int n2) { return n1 - n2; } int arithmetic(int n1, int n2, int (*operation) (int, int)) { return (*operation)(n1, n2); } int main() { int number1 = 5, number2 = 6; // add cout << arithmetic(number1, number2, add) << endl; // subtract cout << arithmetic(number1, number2, sub) << endl; } |
A void
pointer can hold address of any data type
(except function pointer). We cannot operate on the object pointed to by void
pointer, as the type is unknown. We can
use a void
pointer to
compare with another address.
[TODO]
Example
Non-constant
pointer to constant data: Data pointed to CANNOT be changed; but pointer CAN be
changed to point to another data. For example,
int i1 = 8, i2 = 9;
const int * iptr = &i1; // non-constant pointer pointing to constant data
// *iptr = 9; // error: assignment of read-only location
iptr = &i2; // okay
Constant
pointer to non-constant data: Data pointed to CAN be changed; but pointer
CANNOT be changed to point to another data. For example,
int i1 = 8, i2 = 9;
int * const iptr = &i1; // constant pointer pointing to non-constant data
// constant pointer must be initialized during declaration
*iptr = 9; // okay
// iptr = &i2; // error: assignment of read-only variable
Constant
pointer to constant data: Data pointed to CANNOT be changed; and pointer CANNOT
be changed to point to another data. For example,
int i1 = 8, i2 = 9;
const int * const iptr = &i1; // constant pointer pointing to constant data
// *iptr = 9; // error: assignment of read-only variable
// iptr = &i2; // error: assignment of read-only variable
Non-constant
pointer to non-constant data: Data pointed to CAN be changed; and pointer CAN
be changed to point to another data. For example,
int i1 = 8, i2 = 9;
int * iptr = &i1; // non-constant pointer pointing to non-constant data
*iptr = 9; // okay
iptr = &i2; // okay