[Phy 405/905 Home Page] [ Lecturer ]

3. Pointers - Part 1

We begin an in-depth look at pointers by learning how they're assigned, dereferenced, and used in functions.


What are pointers?

Assigning Pointers

Recall that an object is a region of storage; a variable such as
double my_var=3.4;
is an object - its name, an lvalue, refers to "the region of storage" occupied by the variable my_var. So fine - my_var is somewhere in storage, i.e., somewhere in the "memory" of the computer. I put "memory" in quotes, because there are different types of memory - the stack (which is where local variables live), the heap (from which objects are dynamically allocated), virtual memory, .... Usually we don't care where my_var is, precisely, as long as we can access and modify its value:
mY_var = 4.0;
cout <<"my_var="<<my_var<<'\n';
But sometimes we would really like to know where my_var lives, i.e., its address. A pointer is a type that holds this information:
pointer to type T
a derived type, an object which holds the address of another object or function (of type T). type T can be any type (fundamental, function, pointer of any type)

A "pointer to type T" is declared as follows:

// declarations of T*:
T* x;
T *y; // my preference
T * z;
These all declare T*, i.e., "pointers to type T". The second one is my own preferred form because I'm less apt to get confused when writing multiple variable declarations:
T *x, *y, z; // x and y are T*, but z is simply of type T
Of course, someday someone will come up to you, armed with a 2-inch thick stack of Coding Guidelines, and demand that you not use multiple variable declaration statements. Follow your conscience. Personally I think it's quite useful to have multi-var. declarations, even including a mix of pointers and non-pointers, providing that they're all related:
int old_ss, *new_ss;
How does a pointer get a value? It can be initialized in its definition statement (recall that definitions can include initialization) or simply assigned using an assignment operator (= for the moment; +=,-= are discussed below, all other assignment operators are not allowed for pointers)
int x=4, *y = &x;
int *z;
z = &x;
The type T that a pointer points to is part of its type (that's how pointers are derived types). Different types of pointers, which can be converted using the casting operation:
int a=1, *b;
char c='1', *d;
b = (int *)(&a); // better know what you're doing here!
d = (char *)(&b);// better know what you're doing here!
Explicit pointer conversions such as the above are not inherently evil, are sometimes essential, but they are potentially dangerous - merely unportable in some situations, will crash the program in others - and we will consider explicit cases in which we'll have a good reason for pointer casts.

Another pair of casts is possible, because, after all, a pointer holds an address, which is bound to be some kind of (presumably positive) integer:

long unsigned int my_addr = 0xff3c0d013; // culled from a 
// computer manual somewhere? double *x = (double *)(my_addr); // hopefully this is a valid address!
double y;
long unsigned int y_addr = (long unsigned int)(&y); // hopefully // y_addr big enough!
Typically you'd have code like this if you were doing very low-level access of the operating system or hardware internals. It is thus expected that the specific type of integer big enough to hold a (double *) is system-dependent. Also note that explicit casts are necessary; (al)most (all) integers are not automatically converted to pointer type.

The single exception is the value 0, which, when resulting from any constant expression, can be converted into any type of null pointer.

double *x = 0; // pointer initialized to null pointer
int *q = 3.0-1.0+2.0; // not a good idea (very unclear)
The first line is fine; the second line is unfortunate behavior inherited from ANSI C and not to be recommended. A common practice in ANSI C, of using (void *)0 is simply not going to work in most situations in C++:
double *x = (void *) 0; // legal C, illegal C++
The reason is that this was a type-checking loophole for C (a (void *) could be assigned to any type of pointer) that has been closed in C++. So this is something to watch for in porting C code to C++.

Using (dereferencing) pointers

Assuming that a pointer currently holds the address ("points to") a well-defined object of the right type, the pointer can be dereferenced (using the * operator), and can stand in for the original variable in most circumstances - the * operator converts to pointer to an lvalue identified with the original variable.
int x = 20;
int *y = &x;
// access value of x
cout <<"x is "<<x<<" is "<<*y<<'\n';

// assign a value to x, through y
*y = -10;
++*y; // verify precedence

// check that x is properly updated:
cout <<"x is "<<x<<" is "<<*y<<'\n';
The operator * is a prefix operator that groups RL (right-to-left) and has lower precedence than the ++ and -- operators; in the expression ++*y, the dereference wins out, however, because of the RL grouping.

One can have double, triple pointers, accessed the same way. But note! The operand of & must be an lvalue (object or function), and operator* and operator& are not precisely inverses of each other

double x;
double *a = &x, **b = &a, ***c = &b, ****d = &c;
This is legal since each operand of & is an object (a is a variable, is an object, is an lvalue, so &a is okay). The following is illegal, however:
double **bad_ptr = &&x; // illegal!!
The reason is that the rightmost &, which is evaluated first, yields the address of x, which is a value - not an lvalue - so that if &x were literally equal to "103", the expression would be akin to &103, which is not allowed.

What good are pointers?

Consider:
int i, j, *iptr;
i = 4;
j = 0;
*iptr += 3;
iptr = &j;
*ipter += 5;
Certainly the use of pointers in this example is not the clearest way of increasing i,j by 3,5 respectively. What's the point of pointers, then?

function calls by reference

One common use is make function call by reference. The following function does not perform as apparently hoped for:
// **** bad code *****
void order(int x, int y)
// orders the two arguments in increasing order
{
if (x > y)
return; // nothing to do, already ordered
else
{
int temp = x;
x = y;
y = temp;
}
}
The problem is that the function calls we've used up to this point are only call-by-value - the formal variables x and y are not identified with the actual parameters provided in each call of the function order, they're just copies of the values. Using pointers gets around this problem:
// ****** better code *****
void order(int *x, int *y)
// orders the two arguments in increasing order
{
if (*x > *y)
return; // nothing to do, already ordered
else
{
int temp = *x;
*x = *y;
*y = temp;
}
}
Of course, the function calls are still by value (but by value of a pointer, which is enough for our purposes). One can similarly manipulate (double ****) and so on.

Multiple function return values

Another use for pointers is for cases where we need to return more than one "value", such as when possible error conditions need to be flagged:
double my_div(double num, double denom, int *flag)
// returns num/denom, but flag=1 if divide by 0, flag=0 no error
{
if (!denom)
{
*flag = 1; // error! division by zero
return 0; // return result is irrelevant for error
}
else
{
*flag = 0;
return num/denom;
}
}

Efficiency in argument passing

Pointers are also good for cases where we only need call-by-value, but the argument being passed is so large that just copying the actual to formal argument is significant computationally. We'll see this when dealing with structures in the next lecture.

[ Phy 405/905 Home Page] [Lecturer ]