Thursday, November 3, 2011

The Reference-to-Pointer Syntax in C++

The syntax of pointer declarations is sometimes difficult to decipher, just consider the following three, elementary cases:
  • int *p[2]
  • int (*p)[2]
  • int (*p)()

 

Example 1

int *p[2];
This is actually an array of two int pointers. We can also think of this as a pointer to two consecutive int pointers.

int *p[2];

int *a = new int;
*a = 21;
int *b = new int;
*b = 22;

p[0] = a;
p[1] = b;

std::cout << *p[0] << std::endl;          // 21
std::cout << *p[1] << std::endl;          // 22

int **q = p;

std::cout << **q << std::endl;
std::cout << **(q + 1) << std::endl;

delete a;
delete b;
http://codepad.org/6c07RgKf

 

Example 2

int (*p)[2];
This is a pointer to an array of two ints. Just as in the first example, we can think of this a pointer to a pointer, since an array is basically a const pointer.


int n[2];
n[0] = 4;
n[1] = 7;

int (*p)[2] = &n;

std::cout << (*p)[0] << std::endl;       // 4
std::cout << (*p)[1] << std::endl;       // 7

// or ...

std::cout << **p << std::endl;           // 4
std::cout << *(*p + 1) << std::endl;     // 7
http://codepad.org/UDcH5cIQ

 

Example 3

int (*p)();
This is a pointer to a function returning an int.
int takeThisInt()
{
    return 123;
}

// ...

int (*p)() = &takeThisInt;
int x = p();                      // note the parentheses
std::cout << x << std::endl;      // 123
http://codepad.org/iL701mg5

Pointer-to-member-function syntax

This pointer-to-member-function syntax in C++ is not exactly making things more intuitive. Here is an example:
void (SomeClass::*x)(int) const = &SomeClass::func2;
Hmm… Let's take a look at the context in which this statement appears.
class SomeClass
{
public:
    void func1(int x) const { std::cout << "func 1: " << x; }
    void func2(int x) const { std::cout << "func 2: " << x; }
    void func3(int x) const { std::cout << "func 3: " << x; }
};

void callSomeFunction(void (SomeClass::*m)(int) const)
{
    SomeClass obj;
    (obj.*m)(1);
}

int main()
{
    void (SomeClass::*x)(int) const;
    x = &SomeClass::func2;
    callSomeFunction(x);         // func 2: 1
    return 0;
}
http://codepad.org/eUtSGfbV

In main(), void (SomeClass::*x)(int) const introduces a pointer (named x) to a const member function of SomeClass that returns void and takes an int as parameter. Then, &SomeClass::func2 takes the address of func2, which happens to fit this description. Finally, in callSomeFunction(), the supplied function pointee is called with the argument 1 on the stack allocated object.

A typedef can help to make this a bit more readable:
typedef void (SomeClass::*SomeClassMember)(int) const;
The program now reads:
class SomeClass
{
public:
    void func1(int x) const { std::cout << "func 1: " << x; }    
    void func2(int x) const { std::cout << "func 2: " << x; }    
    void func3(int x) const { std::cout << "func 3: " << x; }
};

typedef void (SomeClass::*SomeClassMember)(int) const;

void callSomeFunction(SomeClassMember m)
{
    SomeClass obj;
    (obj.*m)(1);
}

int main()
{
    SomeClassMember x = &SomeClass::func2;
    callSomeFunction(x);
    return 0;
}
Ok, this post was supposed to be about the reference-to-pointer syntax.

Reference vs. Pointer

Pointers are memory addresses with type information. The type specifies the kind of data stored in the pointed-to address. References are typically implemented as pointers by the compiler, but on the syntactic level there are several differences:
  • A reference can't, once created, be changed to reference another object.
int n = 1;
int *p = &n;    // p points to n
int k = 2;
p = &k;         // p now points to k instead

int &r = n;     // r will always reference n
  • References cannot be null.
int *p = new int;
*p = 123;
int &x = *p;     // x is now a local reference to p
x = 5;

std::cout << *p << std::endl;          // 5

delete p;
p = 0;           // pointer is now NULL

if (p) {         // we can't do this with a reference
   *p = 1;
}

x = 3;           // undefined behavior
  • References must be initialized when created, and cannot be uninitialized.
int &ref;  // error: ‘ref’ declared as reference but not initialized 

Why reference-to-pointer?

Let's look at some different function declarations to see how the reference-to-pointer syntax can be useful.
  • void takeInt(int x);
  • void takeIntRef(int &r);
  • void takePtr(int *p);
  • void takePtrToPtr(int **p);
  • void takeRefToPtr(int *&rp);

    void takeInt(int x);
    void takeIntRef(int &r);
    void takePtr(int *p);
    The first three cases are straighforward:

    1. The int is passed by value, which means that a local copy is made.
    2. The int is passed by reference, the local name acts as an alias for the original variable.
    3. A pointer is passed, similar to passing a reference but less safe.

    Note that a pointer is actually passed by value, i.e., only a copy of the pointer is passed to the function. The address of the int's memory location is copied to the local pointer p.
     void takePtrToPtr(int **p);
    This is a bit more interesting. A pointer to a pointer is passed. Suppose that a points to b, and b points to an int.

    a → b → int

    Passing (a) to the function, we can now change not only the original int pointee, but also the pointer (b) itself. Here is an example:
    void takePtrToPtr(int **p)
    {
        int *x = new int;
        *x = 123;
        *p = x;
    }
    
    int main()
    {
        int n = 1;
        int *p = &n;
        assert(p == &n);
        takePtrToPtrcout << *p;      // 123
        delete p;             // no leaks ^_^
    
        return 0;
    }
    Finally,
    void takeRefToPtr(int *&rp);
    is similar to the pointer-to-pointer approach, but with a slightly more friendly syntax. We are now passing a reference to the pointer to takeRefToPtr().
    void takeRefToPtr(int *&rp)
    {
        int *x = new int;
        *x = 123;
        rp = x;
    }
    
    int main()
    {
        int n = 55;
        int *p = &n;
        takeRefToPtr(p);
        assert(p != &n);
        std::cout << *p << std::endl;        // 123;
        delete p;
    
        return 0;
    }
    Note how the function changes the the original pointer p.

    We could think of the relationship
    • int *x ↔ int *&x

    as similar to that between
    • int x ↔ int &x.


      Finally, here is a program that demonstrates the use of the reference-to-pointer syntax:
      #include <iostream>
      #include <assert.h>
      
      static int five = 5;
      static int nine = 9;
      
      void takeInt(int x)
      {
          x = 0;
      }
      
      void takeIntRef(int &r)
      {
          r = 0;
      }
      
      void takePtr(int *p)
      {
          *p = 1;
          p = &five;
      }
      
      void takePtrToPtr(int **p)
      {
          **p = 789;
          *p = &five;
      }
      
      void takeRefToPtr(int *&rp)
      {
          *rp = 1;
          rp = &nine;   
      }
      
      int main()
      {
          int n = 123;
          takeInt(n);                      
              // local copy is modified
          std::cout << n << std::endl;     
              // 123 (value unchanged)
      
          takeIntRef(n);                   
              // value passed by reference, original is modified
          std::cout << n << std::endl;     
              // 0 (assigned in function)
      
          int *pt = &n;                    
              // pt points to n
          takePtr(pt);                     
              // pointer is passed by value
      
          assert(pt == &n);                
              // pointer has not changed (p still points to n) ...
          std::cout << n << std::endl;     
              // 1 ... but pointee has
      
          takePtrToPtr(&pt);               
              // pass a pointer to pt (= pointer to a pointer to n)
          assert(pt != &n);                
              // this time the pointer has changed,
              // pt now no longer points to n
      
          std::cout << *pt << std::endl;   
              // 5
          assert(pt == &five);             
              // indeed, pt points to the static "five"
      
          std::cout << n << std::endl;     
              // 789 (changed in first line of takePtrToPtr) 
      
          takeRefToPtr(pt);                
              // pass a pointer by reference
          std::cout << *pt << std::endl;   
              // 9 (pt is now modified in function)
      
          return 0;
      }
      http://codepad.org/3OD0sVvw

      1 comments:

      何念泰 said...

      Great Explain!

      Post a Comment