5.6. Other pointer characteristics¶
This section wraps up pointers with a discussion of some alternative constraints and special pointer types.
5.6.1. Constant pointers¶
Pointers can be declared const, just like any other type.
Where const appears controls what is held constant:
// odd whitespace to help see where const is used
int x = 5;
int* p1 = &x; // non-const pointer to non-const int
const int* p2 = &x; // non-const pointer to const int
int* const p3 = &x; // const pointer to non-const int
const int* const p4 = &x; // const pointer to const int
You may find it helpful to read pointer declarations from right to left.
In
p1, nothing is constant. Either the pointer or the value pointed to can change.In
p2, the pointer can change, but the value pointed to is constant. You can’t use this pointer to change the value of x.In
p3, the pointer is constant, but the value pointed to can change. You can use this pointer to change the value of x, but can’t point to a different variable.In
p4, both are held constant.
5.6.2. The nullptr type¶
In section Comparison with references, we mentioned that unlike a reference, a pointer might point to ‘nothing’.
What exactly is ‘nothing’?
Many languages refer to this ‘nothing’ as NULL.
Prior to C++11, there was no unambiguous definition. Typically the value 0 was used:
#define NULL 0LL
This definition carries over from standard C.
Using the value long long 0 as an indicator for a null pointer created
several problems over the years in C++ programs.
Null pointers are the same type as regular integral types.
While it is unlikely that the number 0 could ever be confused with a valid address,
it creates problems regular old C never had to handle.
Specifically, C++ introduces function overloads,
which exposes the weakness in using an integral type for both
numbers and the concept NULL.
For example:
#include <cstdio>
#define NULL 0LL
// Three overloads of f
void f(int) { puts("f(int)"); }
void f(bool) { puts("f(bool)"); }
void f(void*) { puts("f(void*)"); }
int main() {
f(0); // calls f(int) overload, not f(void*)
f(NULL); // might not compile, typically calls
// f(int) overload.
// Never calls f(void*)
}
The overload with f(NULL) is never called,
because NULL is not a pointer type.
C++ resolves this by creating a new type just to hold the null pointer.
The type is nullptr_t and the variable of that type is nullptr.
#include <cstdio>
// Three overloads of f
void f(int) { puts("f(int)"); }
void f(bool) { puts("f(bool)"); }
void f(void*) { puts("f(void*)"); }
int main() {
f(0); // calls f(int) overload as before
f(nullptr); // calls f(void*) overload
}
The variable nullptr is a distinct type.
It is not a pointer type, pointer to member, integral type, size type, reference type,
or a member of any type group.
The nullptr does implicitly convert to a pointer type.
In short, using nullptr improves code clarity and correctness.
Using nullptr improves code clarity, especially when auto variables are involved.
Consider the following code example, from Effective Modern C++:
// A function that returns a pointer
int* findRecord() {
return nullptr;
}
int main() {
// If you don’t happen to know (or can’t easily find out) what findRecord returns,
// it may not be clear whether result is a pointer type or an integral type.
//
// After all, 0 (what result is tested against) could go either way.
{
auto result = findRecord();
if (result == 0) {
}
}
// If you see the following, on the other hand ...
{
auto result = findRecord();
if (result == nullptr) {
}
// there’s no ambiguity: result must be a pointer type.
}
}
5.6.3. void pointers¶
A void pointer is a pointer to some memory, but the compiler doesn’t know the type.
It is about as close to a raw machine address as you can get in C++.
Legitimate uses are
calls between functions in different languages or
templates where the provided value could literally be anything,
such as the actual implementation of new in C++.
Important!
void* is not the same as void
There are no objects of type void:
int i; // declare an int
void x; // error! void is not a type
void print(); // function returns nothing
Any pointer can be assigned to void*:
1int* i = new int{5};
2double* x = new double[10];
3int* j = i; // OK: i and j are both int*
4void* p1 = i; // OK: assign int* to void*
5void* p2 = d; // OK: assign double* to void*
6
7int* i2 = p1; // error
8 // can't assign void* to int*
The last assignment is invalid, even though p1 was last assigned an int*.
A human reader knows the void pointer currently holds an int pointer,
but the compiler does not.
The compiler can’t know the size of the value pointed to.
void isn’t a type, so it has no size:
int* i = new int{5};
void* p = i; // OK
int* j = p; // error
To resolve this error,
we have to give the compiler size information.
We can use one of C++ casts to convert void*
to another pointer type that has a size:
int* i = new int{5};
void* p = i; // OK
//int* j = p; // error
int* j = static_cast<int*>(p); // OK
More to Explore
From the ISO C++ FAQ: Does “Const Fred* p” mean that *p can’t change?
Effective Modern C++ by Scott Meyers Item 8: Prefer nullptr to 0 and NULL