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_ptrper 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_ptrs and/orweak_ptrs per object - target object lives as long as at least one
shared_ptrpoints 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 &creturns memory address ofc
*
char c = 65;
char* p = &c;
*p = 88;
char x = *p;
&creturns memory address ofc*paccesses 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
0in memory! (depends on platform)
Coding Convention: nullptr signifies value not available
- set pointer to
nullptror valid address on initialization - check if not
nullptrbefore 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
thisreturns the address of an object itselfthis->can be used to access members*thisaccesses 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
Declarations of Types
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_;
…
};
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!