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

You have attempted of activities on this page