Last Updated:

Special Class Member Functions in C++ | Examples

Some class member functions are special in the sense that they affect the creation, copying, and destruction of class objects or specify how values are cast to other types of values. Often, such special functions are called implicitly.

1. Constructors

A member function of a class with the same name as a class is called a constructor. It is used to build objects of this class. The constructor should not return any value, not even void.

class Complex { private: double r, m; public: Complex(double r, double m) : r(r), m(m) {} ... };

If a class has a constructor, all objects of that class will be initialized. If the designer requires parameters, they must be provided.

When a constructor is declared for a class, you cannot use the initialization list as an initializer.

Complex c1(5, -2); Complex c2 = {5, -2};Correct // Error – The Complex class has a constructor

1.1. Default Constructor

The Class X Default Constructor is a class X constructor that is called without parameters. The default constructor is usually of the form X::X(),but a constructor that can be called without parameters because it has parameters with silence, for example, X::X(int = 0), is also considered a default constructor. In the absence of other declared constructors, the default constructor is generated by the compiler.

class Complex
  {private:
     double r, m;
    public:
     Complex() : r(0), m(0) {}
    ...
   };
  
complexx;
   Call the default constructor
class Complex
  {private:
     double r, m;
    public:
     Complex(double nr = 0, double nm = 0) : r(nr), m(nm) {}
    ...
   };
  
Complex y1(-6, 3); Call the constructor with parameters
Complex y2; the constructor is called as a default constructor

1.2. Copy Builder

A copy constructor for class X is a constructor that can be called to copy a class X object, that is, a constructor that can be called with a single parameter, a reference to a class X object. For example, X::X(const X&) and X::X(X&, int = 0) are copy constructors.

If there are no declared copy constructors, the compiler generates a public copy constructor. The generated copy constructor performs bitwise copying of the object. This method is valid only if there are no pointers in the object that store dynamically allocated memory addresses. The generated constructor will copy the address rather than the contents of memory, so two different objects will refer to the same memory and change synchronously, which is not expected behavior. In this case, the programmer must write the copy constructor himself, which will, in particular, copy the contents of dynamically distributed memory.

However, in cases where the default copy constructor makes the right sense, it is best to rely on this default. It's shorter, and code readers need to understand the omissions. In addition, the compiler is aware of this omission and the possibilities of its optimization. And writing a piecemeal copy of classes with a large number of data members manually is a tedious task, and at the same time you can make a lot of mistakes.

The copy constructor—both user-defined and compiler-generated—is used:

  • when initializing variables;
  • When passing arguments.
  • When returning a value from a function.
  • when handling exceptions.

The semantics of these operations are by definition the same as the semantics of initialization.

Complex x = 2; Complex y = Complex(2, 0);Creates Complex(2), then copies it to // Creates Complex(2, 0), then copies it to xу

Calls to the copy constructor are easy to get rid of. You might as well write down the following:

Complex x(2); Complex y(2, 0);Initialize with value 2 // Initialize with value (2, 0) xу

For an example of a copy constructor, see the example at the end of the tutorial.

2. Destructors

A member function of class X named ~X is called a destructor. It is used to destroy the value of class X immediately before the destruction of the object containing it. The destructor has no parameters and no return type, you can not even set void.

Destructors are automatically invoked when

  • An automatic or temporary object leaves the scope.
  • the program ends (for statically constructed objects);
  • uses the delete operation for objects hosted by the new operation.

The destructor can also be invoked explicitly.

class X
   {private:
      intx;
     public:
      X(int n);
     ...
    };

X::X(int n) { x = n; }

 
X a = 1; Equivalent to X a = X(1)

3. Conversions

Transformations (type changes) of class objects are performed by constructors and transforming functions.

Such transformations, called custom transformations, are often implicitly applied in addition to standard transformations. For example, a function that expects a parameter of type X can be called not only with a parameter of type X, but also with a parameter of type T if there is a conversion from T to X. In addition, custom transformations are used to cast initializers, function parameters, values returned by functions, operands in expressions, control expressions, loop and selection operators, and to cast types explicitly.

Custom transformations are applied only where they are unambiguous.

3.1. Transforming by Constructor

A constructor with a single parameter specifies the conversion of its parameter type to the type of its class.

A constructor with a single parameter is not necessarily called explicitly.

class X
  {private:
     intx;
    public:
     X(int n);
    ...
   };

X::X(int n) { x = n; }

 
X a = 1; Equivalent to X a = X(1)

However, implicit conversion may not be desirable in some cases.

class Str
  {private:
     char*str;
    public:
     Str(int n) { str = new char[n]; *str = 0; }
     Str(const char *p) { str = new char [strlen(p) + 1]; strcpy(str,p); }
     ~Str() { if (str) delete [] str; }
   };
  
Str s = 'a'; Creating a string from Xint('a') elements

An implicit transformation can be suppressed by declaring a constructor with an explicit modifier. Such a constructor will be invoked only explicitly.

class Str
  {private:
     char*str;
    public:
     explicit Str(int n) { str = new char[n]; *str = 0; }
     Str(const char *p) { str = new char [strlen(p) + 1]; strcpy(str,p); }
     ~Str() { if (str) delete [] str; }
   };
  
Str s1 = 'a';
Str s2(10);

3.2. Transformative Functions

A member function of class X, whose name is operator <type>, defines the conversion from X to the type specified by the type name. Such functions are called transforming functions or casting functions.

Neither parameters nor a return type can be specified for such a function.

class X
  {private:
     intx;
    public:
     X(int n);
     operatorint();
    ...
   };
X::X(int n) { x = n; }

X::operator int() { return x; }

int a;
xb(0);
a = (int)b; Explicitly call a transforming function
a = b; Implicit call to a transforming function

3.3. Disambiguation

Assigning a value of type V to an object of class X is permissible if there is an assignment operator X::operator= (Z) such that V is Z or there is a single transformation of V to Z. Initialization is treated similarly.

In some cases, the value of the desired type can be created by reusing constructors or conversion operators. This should be done with explicit transformations—only one level of implicit user-defined transformations is allowed. In some cases, the value of the desired type can be created in more than one way – this is unacceptable.

class X { ... X(int); X(char*); ...};
class Y { ... Y(int); ...};
class Z { ... Z(X); ...};
 
Xf(X);
Yf(Y);
Zg(Z);
 

void main()
  {f(1);
    f(X(1));
    f(Y(1));
    g("mask");
    g(X("Mask"));
    g(Z("Mask"));
   }
   Ambiguity - f(X(1)) or f(Y(1))? Correct // Correct // Error - requires the use of two user-defined transformations // Correct – g(Z(X("Mask")))) // Correct – g(Z(X("Mask")))
class XX { XX(int); };
void h(double);
void h(XX);

void main()
  {h(1); } // h(double(1)) or h(XX(1))? Calling h(1) means h(double(1)),// because in this case only standard transformations are used.

The transformation rules are neither the easiest to implement, nor the easiest to document, nor as general as one might imagine. However, they are quite safe, and their use does not lead to unpleasant surprises. It's much easier to manually resolve ambiguities than it is to find an error caused by a transformation that you didn't know you had.

4. Examples

4.1. Developing a Stack Class

First option

The development of the class should begin with the interface, i.e. with open (public) functions that will be used by the rest of the program to interact with the developed class. Two operations are defined for a stack: taking an item from the stack and adding an item to the stack. We will also define a constructor (defaults) and a destructor (usually declared for all classes), as well as helper functions that check whether the stack is empty and whether there was an error when working with the stack. Thus, the interface of the class is as follows:

class stack
  {public:
     stack();
     ~Stack();
     int Push(int n);
     int pop();
     int IsEmpty() const;
     int IsError() const;
     const char* LastError() const;
   };

You can now design the data structure for the class. We will use an array to store the stack elements. We'll also need a variable that points to the current stack item and a variable that stores the error signature.

In addition, it is necessary, of course, to define the functions that are included in the class. You can then use the developed class.

#include <stdio.h>

class Stack
 {private:
    enum { SIZE = 100 };
    enum { NO_ERROR, STACK_EMPTY, STACK_FULL };
    int stack[SIZE];
    int*cur;
    int error;
   public:
    stack();
    ~Stack();
    int Push(int n);
    int pop();
    int IsEmpty() const;
    int IsError() const;
    const char* LastError() const;
  }; The scope of the constant defined by define is the file. To localize the scope of a constant in a class, use the enumerable type // Array to store stack elements // Pointer to the current stack element // Error symptom // Class member constant functions can be applied // to both constant and non -constant objects

Stack::Stack()
 { cur = stack; error = NO_ERROR; }

Stack::~Stack()
 {}

int Stack::Push(int n)
 { if (cur - stack < SIZE)
    {*cur++ = n; error = NO_ERROR; return 1; }
   else
    { error = STACK_FULL; return 0; }
  }

int Stack::Pop()
 { if (cur != stack)
    { error = NO_ERROR; return *--cur; }
   else
    { error = STACK_EMPTY; return 0; }
  }

inline int Stack::IsEmpty() const
 { return cur == stack; }

inline int Stack::IsError() const
 { return error != NO_ERROR; }

const char* Stack::LastError() const
 { if (error == NO_ERROR)
    return "There is no error";
   else if (error == STACK_EMPTY)
    return "Stack is empty";
   else
    return "Stack is full";
  }


int main()
 { Stack s;

   s.Push(1);
   s.Push(2);
   s.Push(3);
   while (!s.IsEmpty())
    printf("%d\n", s.Pop());
   printf("%d\n", s.Pop());
   printf("%s\n", s.LastError());
   for (int i = 0; i < 110; i++)
    s.Push(i);
   if (s.IsError())
    printf("%s\n", s.LastError());
  }

Second option

After some thought, the developer came to the conclusion that using a static array to store stack elements limited the developed class, and decided to use a dynamically distributed array, the size of which could be increased if necessary. Thus, instead of an array, we declare a pointer and another variable that will store the size of the dynamically distributed array.

As a result of these changes, the developer also had to change the constructor, which added operators for dynamic memory allocation, a destructor to which memory-free operators were added, and the add an element to the stack function, which redistributes the allocated memory if necessary. A copy constructor has also been added. The rest of the functions remained unchanged.

#include <stdio.h> 
#include <malloc.h>
class Stack
{ private: enum { SIZE = 100 }; enum { NO_ERROR, STACK_EMPTY, NOT_ENOUGH_MEMORY }; intsize; int *stack; int*cur; int error; public: Stack(); Stack(const Stack& s); ~Stack() copy constructor; int Push(int n); int pop(); int IsEmpty() const; int IsError() const; const char* LastError() const; }; Stack::Stack() { size = SIZE; stack = NULL; if (stack = (int *)malloc(size * sizeof(int))) { cur = stack; error = NO_ERROR; } else { error = NOT_ENOUGH_MEMORY; size=0; } } Stack::Stack(const Stack& s) { size = s.size; stack = NULL; error = NO_ERROR; if (size) if ((stack = (int *)malloc(size * sizeof(int))) == NULL) { error = NOT_ENOUGH_MEMORY; size=0; } else for (int i = 0; i < size; i++) *(stack + i) = *(s.stack + i); cur = s.cur; } Stack::~Stack() { if (stack) free(stack); } int Stack::Push(int n) { if (!stack) return 0; if (cur - stack < size) { *cur++ = n; error = NO_ERROR; return 1; } else if (stack = (int *)realloc(stack, (size + SIZE) * sizeof(int))) { cur = stack + size; size += SIZE; *cur++ = n; error = NO_ERROR; return 1; } else { error = NOT_ENOUGH_MEMORY; size=0; return 0; } } int Stack::Pop() { if (cur != stack) { error = NO_ERROR; return *--cur; } else { error = STACK_EMPTY; return 0; } } inline int Stack::IsEmpty() const { return cur == stack; } inline int Stack::IsError() const { return error != NO_ERROR; } const char* Stack::LastError() const { if (error == NO_ERROR) return "There is no error"; else if (error == STACK_EMPTY) return "Stack is empty"; else return "There is not enough memory"; }

int main()
 { Stack s;

   s.Push(1);
   s.Push(2);
   s.Push(3);
   while (!s.IsEmpty())
    printf("%d\n", s.Pop());
   printf("%d\n", s.Pop());
   printf("%s\n", s.LastError());
   for (int i = 0; i < 110; i++)
    s.Push(i);
   if (s.IsError())
    printf("%s\n", s.LastError());
  }

Please note – and this is the most important thing – that the main program has remained unchanged. Since the developer did the right thing and started by developing the interface, the changes in the class itself did not affect the rest of the program. Of course, for the stack everything is simple, because the set of necessary functions is small and well known. But the development of a class should always begin with the development of the interface. The better the interface of the classes used in the program is thought out, the fewer problems there will be in the development of the program.

4.2. Using the Move Designer

 

class Vector
 {private:
     intsize;
     int *vector;
   public:
     Vector(int s);
     Vector(const Vector&v); // Copy constructor
     Vector(Vector &&v) noexcept; // Move constructor
     ~vector();
 };

Vector::Vector(int s)
 { size = s;
   vector = new int[size];
  }

Vector::Vector(const Vector& v) : Vector()
 {size = v.size;
   vector = new int[size];
   for (int i = 0; i < size; i++)
     vector[i] = v.vector[i];
  }

Vector::Vector(Vector&& v) noexcept
 {size = v.size;
   vector = v.vector;
   v.size = 0;
   v.vector = nullptr;
  }

Vector::~Vector()
{ if (vector) delete[] vector; }

// Function f returns a local variable
Vector f() { Vector v = Vector(7); returnv; }

// The move constructor is used because after the function is executed, local variables are deleted
int main()
{ Vector v = f(); }

4.3. Using the Delegating Designer

Why use designer delegation? First, this avoids duplicating code. If one constructor does some actions, and another designer does the same actions and something else, you can not overwrite the same actions, but instead call one constructor from another.

The second reason is more interesting. The object is considered created when the first (called) constructor finishes. And if some kind of failure occurs in the calling (delegating) constructor, the destructor will still be called, because the object is considered created.

Consider an example of creating a class for a dynamic matrix. If memory is partially allocated and then an error occurs, the allocated memory will not be released. 

class Matrix
  {private:
      int rows, cols;
      int **matrix;
    public:
      Matrix(int r, int c);
      ~Matrix();
   };

Matrix::Matrix(int r, int c)
  { rows = r;
    cols=c;
    matrix = new int* [rows];
    for (int i = 0; i < rows; i++)
      matrix[i] = new int[cols];
   }

Matrix::~Matrix()
  { for (int i = 0; i < rows; i++)
      delete[] matrix[i];
    delete[] matrix;
   }

If we add a default constructor and call it from the constructor that creates the matrix, the matrix will be considered created after the default constructor is called, and the destructor will be called even if the constructor terminates prematurely (for example, when memory allocation errors occur).

  class Matrix
  {private:
      int rows, cols;
      int **matrix;
    public:
      matrix();
      Matrix(int r, int c);
      ~Matrix();
   };

Matrix::Matrix()
  { rows = cols = 0;
    matrix = nullptr;
   }

Matrix::Matrix(int r, int c) : Matrix()
  { rows = r;
    cols=c;
    matrix = new int* [rows];
    for (int i = 0; i < rows; i++)
      matrix[i] = nullptr;
    for (int i = 0; i < rows; i++)
      matrix[i] = new int[cols];
   }

Matrix::~Matrix()
  { for (int i = 0; i < rows; i++)
      delete[] matrix[i];
    delete[] matrix;
   }