9.10. The this Pointer: class stack

Time: 00:03:01 | Download: Large, Large (CC), Small | Streaming, Streaming (CC) | Slides (PDF)
Review

Aside from making a grammatically awkward sentence, what is the "this" pointer, and why is it important? To help us answer these questions, we expand on our initial introduction to the this pointer and its example statement. We'll explore how C++ initializes the value saved in this and how it binds an object to a member function by converting a stack structure and its supporting functions into a class with corresponding member functions.

const int SIZE = 100;

struct stack
{
	char	st[SIZE];
	int	sp;
};

stack	make_stack();
void	init_stack(stack* s);
void	push(stack* s, char data);
char	pop(stack* s);
int	size(stack* s);
char	peek(stack* s);
 
void push(stack* s, char data)
{
    if (s->sp < SIZE)
	s->st[s->sp++] = data;
    else
	cerr << "ERROR: stack is full" << endl;
}

char pop(stack* s)
{
    if (s->sp > 0)
	return s->st[s->--sp];
    else
	cerr << "ERROR: stack is empty" << endl;
}
(a)(b)
A stack implemented as a structure. A non-object-oriented program can specify a structure to maintain the data representing a stack object. However, a program using the stack structure may need multiple stacks - multiple instances of the stack structure. So, programmers must "tell" the stack functions on which stack to operate during any given call. To this end, the stack functions include a parameter, s, that points to a specific stack object. (An unfortunate collision of terms warrants some caution: Array-based stack implementations often include a variable called a stack pointer, which we must not confuse with a variable that points to a specific stack.)
  1. All but one stack function requires an argument that is a pointer-to-a-stack. During a function call, that argument binds the function to the specific stack on which the function operates. Binding the operations to a specific stack while they run allows programs to define and use multiple stacks without confusion or conflict.
  2. Two stack functions illustrate how the functions generally use the pointer-to-a-stack parameter, s. The stack pointer, sp, points to an empty array element at the top of the stack - the location where the next push operation will store data. push necessarily uses the post-increment operator, sp++, and pop uses the pre-decrement operator, --sp. The operators used in pop can be a little confusing: The green characters form the arrow operator and the red ones the decrement operator: s->st[s->--sp].

I included make_stack to maintain some consistency between the original stack example and previous versions of the Time example, but it's not needed. Except for make_stack, each function requires a pointer-to-a-stack argument. Three functions, init_stack, push, and pop, modify the stack, so we must pass the stack argument using an INOUT technique - a pointer or reference. The example uses a pointer to set the stage for the this pointer.

The Role of the this Pointer

A natural question to ask ourselves at this time is, "If we always pass a stack pointer to every stack function, can we automate the process to make it easier?" And the answer is that we can! Let's begin by rewriting the stack structure as a class:

class stack
{
	private:
		static const int SIZE = 100;

		char	st[SIZE];
		int	sp;

	public:
			stack() : sp(0) {}
		void	push(char data);
		char	pop();
		int	size();
		char	peek();
};
A stack implemented as a class. The definition of SIZE is moved into and made a static member of the stack class. make_stack and init_stack are replaced with a single default constructor. All of the functions are made members of the class, and the explicit pointer to a stack is removed from each function's argument lists.

Converting the stack from a structure to a class changes the appearance of the functions but not their behavior. When called, the functions must still "know" on which stack they are operating, which is equivalent to saying that each function must be bound to an object when it runs. Furthermore, this binding occurs in the same way as in the structure version: the program passes the stack's address to the function as an argument. The programmer must explicitly calculate the stack's address in the structure version and pass the address as an explicit argument. The C++ compiler does both steps automatically for member functions.

Whenever the program calls a member function, the calling object's address is automatically passed as an "invisible" or implicit pointer argument. Invisible or not, the program saves every value it passes to a function in a local parameter variable. In the struct version of the stack, that argument and how the function uses it are visible in the function definitions, as illustrated in Figure 1. In the class version of the stack, the compiler automatically passes the address of the bound object and generates the "receiving" variable or parameter, naming it this. The following figure contrasts the code that a programmer writes with the code that the compiler automatically generates.

Programmer Writes Compiler Generates
s.push('a');
s.push(&s, 'a');
void stack::push(char data)
{
    if (sp < SIZE)
        st[sp++] = data;
    else
        cerr << "ERROR: stack is full" << endl;
}
void stack::push(stack* this, char data)
{
    if (this->sp < SIZE)
        this->st[this->sp++] = data;
    else
        cerr << "ERROR: stack is full" << endl;
}
The C++ compiler automatically generates the function/object binding.
this

The this pointer is an automatic local variable that the compiler generates for every non-static member function, saving the address of the object that calls the function. The object-to-function binding through the this pointer begins when any non-static member function is called and lasts until the function ends and returns. For example, given the function call o.f();, while function f is running, this == &o.

#include<iostream>
#include "stack.h"
using namespace std;

void stack::push(char data)
{
	if (sp < SIZE)
		st[sp++] = data;
	else
		cerr << "ERROR: stack is full" << endl;
}

char stack::pop()
{
	if (sp > 0)
		return st[--sp];
	else
	{
		cerr << "ERROR: stack is empty" << endl;
		return '\0';	// no good value to return
	}
}

int stack::size()
{
	return sp;
}

char stack::peek()
{
	return st[sp - 1];
}
The stack member functions. The final version of the stack member functions. The push and pop operations test for overflow (pushing data on a full stack) and underflow (popping data off an empty stack) conditions, respectively, but they can only print error messages. We'll create more secure responses to these conditions when we explore exception handling later in this chapter.

Visualizing The this Pointer

The this pointer is a predefined parameter whose memory the program automatically allocates whenever it calls a non-static member function and deallocates when the function ends, making the binding between an object and a member function temporary. The program establishes the binding when it calls the function and dissolves it when it ends. Any stack object can play the role of the implicit or "this" object for any of the member functions:

stack	r;
stack	s;
stack	t;
Three rectangles represent three stack objects named r, s, and t. Each object has two member variables named sp and st.
(a)
s.push('a');
A picture illustrating that when object s calls the print function, it is bound to the function by the 'this' pointer. The function call passes the address of s to the 'this' pointer, which points to object s until the function call returns.
(b)
t.push('a');
Similar to the previous picture, but now illustrating that when object r calls the print function, it is bound to the function by the 'this' pointer. The function call passes the address of r to the 'this' pointer, which points to object r until the function call returns.
(c)
this binds objects to member functions. this is an implicitly defined automatic parameter defined in all non-static member functions. Like all automatic local variables, the program allocates and deallocates its memory when calling and returning from the function.
  1. The statements instantiate three distinct stack objects
  2. this stores the address of s while the push operation is running
  3. this stores the address of t while the push operation is running

Downloadable Code