Pointers Pointers Pointers
Why
?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
Pointer to T
- 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
)
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
std::unique_pointer<T>
- used to access dynamic storage, i.e., objects on the
heap
- only one
unique_ptr
per object - pointer and target object have same lifetime
std::shared_pointer<T>
std::weak_pointer<T>
- used to access dynamic storage, i.e., objects on the
heap
- many
shared_ptr
s and/orweak_ptr
s 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.
Operators
&
char c = 65;
char* p = &c;
- raw pointer variable of type
T*
can store an address of an object of typeT
&c
returns memory address ofc
*
char c = 65;
char* p = &c;
*p = 88;
char x = *p;
&c
returns memory address ofc
*p
accesses value at the address inp
->
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
Syntax
* | & | |
---|---|---|
Type Modifier |
Pointer Declaration
|
Reference Declaration
|
Unary Operator |
Dereferencing
|
Taking Address
|
Binary Operator |
Multiplication
|
Bitwise AND
|
Declaration Pitfall
int* p1, p2; // int*, int
int *p1, *p2; // int*, int*
Better & Unambiguous:
int* p1 = …;
int* p2 = …;
Redirection
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
}
nullptr
C++11
- 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!
const
Purposes
- read-only access to objects
- preventing pointer redirection
Syntax
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"
Examples
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 itselfthis->
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!
1 3
2 4
3 7
of Types
DeclarationsSometimes 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_;
…
};
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.
Avoid Pointers If Possible Try to Avoid Pointers Avoid Pointers!
Pointers are prone to dangling
- 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
Error-prone argument passing
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
Code is harder to read
*p = *p * *p + (2 * *p + 1); // SO MANY STARS!