◐ Shell
clean mode source ↗

Introduction To C++ Pointers

Observing Objects

  • indirection without copying: referencing / keeping track of objects
  • if we want to change the target of an indirection at runtime ⇒ can't use references

Accessing Dynamic Memory

  • access objects of dynamic storage duration i.e., objects whose lifetime is not tied to a variable / a scope (in later chapters)

Building Dynamic, Node-Based Data Structures

visualization of the indirection structure of some node based data structures

  • stores a memory address of an object of type T
  • can be used to inspect/observe/modify the target object
  • can be redirected to a different target (unlike references)
  • may also point to no object at all (be a Null Pointer)

a char and a pointer to it in memory


Raw Pointers: T*

  • essentially an (unsigned) integer variable storing a memory address
  • size: 64 bits on 64 bit platforms
  • many raw pointers can point to the same address / object
  • lifetimes of pointer and taget (pointed-to) object are independent

Smart Pointers C++11

  • used to access dynamic storage, i.e., objects on the heap
  • only one unique_ptr per object
  • pointer and target object have same lifetime

  • used to access dynamic storage, i.e., objects on the heap
  • many shared_ptrs and/or weak_ptrs per object
  • target object lives as long as at least one shared_ptr points to it

We will learn how to use these smart pointers in later chapters.

Address Operator &

char  c = 65;
char* p = &c;
  • raw pointer variable of type T* can store an address of an object of type T
  • &c returns memory address of c

a char and a pointer to it on the stack

Dereference Operator *

char  c = 65;
char* p = &c;
*p = 88;
char  x = *p;
  • &c returns memory address of c
  • *p accesses value at the address in p

a char, a pointer to it and an additional int on the stack

Member Access Operator ->

struct Coord {
  char x = 0; 
  char y = 0; 
};
Coord a {12,34};
Coord* p = &a;
char v = p->x;  // v = 12
char w = p->y;  // w = 34
// alternative:
char s = (*p).x;  // s = 12
char t = (*p).y;  // t = 34

an aggregate of 2 chars and a pointer to it on the stack

* &
Type Modifier

Pointer Declaration

Type* ptr = nullptr;

Reference Declaration

Type& ref = variable;
Unary Operator

Dereferencing

value = *pointer;

Taking Address

pointer = &variable;
Binary Operator

Multiplication

product = expr1 * expr2;

Bitwise AND

bitand = expr1 & expr2;
Declaration Pitfall
int*  p1, p2;    // int*, int
int  *p1, *p2;   // int*, int*
Better & Unambiguous:
int* p1 = …;
int* p2 = …;

unlike references, pointers can be redirected:

#include <iostream>

int main () {
int a = 0;
int b = 0; 
int* p = &a;
*p = 2;
p = &b;
*p = 9;
std::cout << a << '\n';  // 2
std::cout << b << '\n';  // 9
}
  • special pointer value
  • is implicitly convertible to false
  • not necessarily represented by 0 in memory! (depends on platform)
Coding Convention: nullptr signifies value not available
  • set pointer to nullptr or valid address on initialization
  • check if not nullptr before dereferencing
int* p = nullptr;   // init to nullptr
if () {                             
  int i = 5;
  p = &i;  // assign valid address
  
  // check before dereferencing!
  if (p) *p = 7;  
  
  // set to nullptr, signalling 'not available'
  p = nullptr;   
}                             
// i's memory is freed, // any pointer to i would be invalidated!

Purposes

  1. read-only access to objects
  2. preventing pointer redirection
pointer to type T pointed-to value modifiable pointer itself modifiable
T *
T const *
T * const
T const * const
Read it right to left: "(const) pointer to a (const) T"
int i = 5;
int j = 8;
int const* cp = &i;
*cp = 8;   //  COMPILER ERROR: pointed-to value is const
cp = &j;   //  OK
int *const pc = &i;
*pc = 8;   //  OK
pc = &j;   //  COMPILER ERROR: pointer itself is const
int const*const cpc = &i;
*cpc = 8;  //  COMPILER ERROR: pointed-to value is const
cpc = &j;  //  COMPILER ERROR: pointer itself is const

East const

one consistent rule:
what's left of const is constant

const West

(still) more widespread, but less consistent

int const c = ;
int const& cr = ;
int const* pc = ;
int *const cp = ;
int const*const cpc = ;
const int c = 1;
const int& cr = ;
const int* pc = ;
int *const cp = ;
const int *const cpc = ;
  • available inside member functions
  • this returns the address of an object itself
  • this-> can be used to access members
  • *this accesses the object itself
#include <iostream>

class IntRange {
  int l_ = 0;
  int r_ = 0;
public:
  explicit
  IntRange (int l, int r): l_{l}, r_{r} {
    if (l_ > r_) std::swap(l_, r_);
  }
  int left ()  const { return l_; }
  // can also use 'this' to access members:
  int right () const { return this->r_; }
  
  // returns reference to object itself
  IntRange& shift (int by) {
    l_ += by;
    r_ += by;
    return *this;
  }
  IntRange& widen (int by) {
    l_ -= by;
    r_ += by;
    return *this;
  }
};

int main () {
  IntRange r1 {1,3};

  r1.shift(1);
  std::cout << '[' << r1.left() <<','<< r1.right() << "]\n";

  r1.shift(2).widen(1);  
  std::cout << '[' << r1.left() <<','<< r1.right() << "]\n";
}
IntRange r1 {1,3};
r1.shift(1);
r1.shift(2).widen(1);  // chaining possible!

Sometimes necessary if one needs two types to refer to each other:

// forward declaration
class Hub;
class Device {
  Hub* hub_;
  
};
class Hub {
  std::vector<Device const*> devs_;
  
};

a number of hub and device objects pointing to each other

In order to define a type, the memory sizes of all its members must be known.

This in turn is only possible if the full definition of all members is known.

However, all pointer types have the same size
⇒ we can just declare the existence of Hub, because Device only needs a pointer to it.

  • dangling = pointer points to an invalid/inaccessible memory address
  • value stored in pointer can be any address
  • programmer has to make sure pointer target is valid / still exists
int* p;  // p not initialized!
*p = 7;  //  UNDEFINED BEHAVIOR
p = nullptr;  
*p = 7;  //  UNDEFINED BEHAVIOR access to nullptr
{
  int x = 8;  
  p = &x;   
}        // x's lifetime ends
*p = 7;  //  UNDEFINED BEHAVIOR access to freed memory
void swap_values (int* a, int* b) {
  int t = *a;
  *a = *b;
  *b = t;
}
int x = 3, y = 4;
swap_values(&x, &y)        // OK
swap_values(&x, 0);        //  UNDEFINED BEHAVIOR
swap_values(&x, nullptr);  //  UNDEFINED BEHAVIOR
*p = *p * *p + (2 * *p + 1);   // SO MANY STARS!

Prefer references if possible, especially for function parameters