What Are Classes?

What Are Classes?

Here are the basics of C++ Classes. In particular:

  • Declarations and Definitions
  • Member Functions
  • Constructors and Destructors

Class Declarations

Here’s a simple class declaration for a complex number:

// complex.h
class Complex 
{ 
public:    
  // sets values    
  void set(const double r, 
           const double i);

  // prints values
  void print();

private:    
    double real, imag; 
};		// end of class declaration (semicolon required)

It consists of:

  • A class name, Complex
  • Several class members:
  • data members real and imag, and 
  • member functions set and print
  • Access specifiers private and public

NOTE: A class declaration is typically placed in a header file.

And here’s its corresponding class definition:

// complex.cpp
#include <iostream>
#include "complex.h"	// Note!

void Complex::set(const double r, 
                  const double i)    
{ 
  real = r; 
  imag = i; 
}    

void Complex::print()
{
  std::cout << '(' << real
            << ',' << imag << ')'
            << std::endl;
}
  • It must #include the class header file.
  • It contains the implementation of the class member functions.

NOTE:  A class definition is typically placed in one or more .cpp files.

Here’s how the class might be used:

// testComplex.cpp
#include <iostream>
#include "complex.h"	// Note!

int main() 
{ 
  Complex a;

  a.set(27.5, 13.4); 
  a.print();

  return 0; 
}
  • NOTE:
    • It must #include the class header file.
    • In this case, the Complex a; produces a local, auto instance of class Complex.
    • In this case, it calls both of the member functions of class Complex, set and print, applied to a.

It outputs:

(27.5,13.4)

class versus struct

A class is similar to a C struct:

  • It has a similar syntax.
  • It can contain data members.
  • It is an extension of the same concept.

A class differs from a C struct:

  • It uses the keyword class.
  • It can contain member functions as well as data members.
  • It can contain access specifiers (public and private).
  • Its usage does not require the keyword struct (or class).

In C++, a class and a struct are both extensions of a C struct;  they differ only in one respect (see later).

inline Member Functions

When you create many member functions which contain very little code, and call them often (a common occurrence in object-oriented programming), run-time efficiency often becomes an issue.

In C, the only solution was to use a macro, which has some potentially serious side-effects.

In C++, you have a much better alternative:

  • You can use inline member functions.

There are two ways to specify an inline member function:

  • Place the function body inside the class declaration,
    • This combines (part of) the class definition with the class declaration, which means you are exposing implementation details of the class to your clients. (It violates encapsulation)

or:

  • Define the function explicitly inline in the class header file.
    • This still has some encapsulation issues.

Placing the function body inside the class declaration:

// complex.h
class Complex 
{ 
private:    
  double real, imag; 
public:    
  // sets values
  void set(const double r, 
           const double i)
  { 
    real = r; 
    imag = i; 
	}
  void print();	// prints values
}; // end of class declaration (semicolon required)

This implicitly makes the member function inline.

Defining the function explicitly inline in the class header file:

// complex.h
class Complex 
{ 
private:    
  double real, imag; 
public:    
  void set(const double r, 
           const double i); // sets values
  void print();	// prints values
}; // end of class declaration (semicolon required)

inline void Complex::set(const double r, 
                         const double i)
{ 
	real = r; 
	imag = i; 
}

Question: Why does it have to be in the header file?

To inline or Not To inline?

Beware! Making a function inline exposes that part of the class implementation to its clients, which can lead to problems.   

When should one make a member function inline?

when:

  • The member function is truly trivial, and is likely to remain so.

and when:

  • Making the member function inline is not detrimental in other ways.

For example, we could have done the following:

// complex.h
#include <iostream>	// Large!!

class Complex 
{ 
private:    
  double real, imag; 
public:    
  void set(const double r, 
           const double i)
  { real = r; imag = i; }. // sets values
  void print()		         // prints values
  {
    std::cout << ‘(‘ << real 
              << ‘.’ << imag << ‘)’
              << std::endl;
  }
}; // end of class declaration (semicolon required)

But then, all clients would be forced to include <iostream>, which slows down compilation considerably!  Besides, in this case, the savings won’t be great.

Class Scope

  • C has the following scopes:
    • local (or block) scope
    • function scope (labels only)
    • global (or file) scope
  • To these, C++ adds:
    • class scope
      • The name of a class member is local to its class
      • Access to class members is controlled
    • namespace scope

Class Scope and Access Specifiers

  • The member functions of a class have unrestricted access to all the class members of that class.
  • Functions that are not member functions of that class have access only to the public members of that class
  • private disallows access to a class member from outside the class.
  • Any number of access specifiers is allowed, and no particular order is required.
    • However, note that this may affect the order in which the members are allocated in memory. 
  • The default access
    • in a class is private   
    • in a struct is public

     This is the only difference between a class and a struct in C++.

Class versus Struct

A C-style struct is, in C++, a class with all public members.

For example, you could do:

#include <iostream.h>
struct Complex 
{    
	double real, imag;    
	void set(const double r, 
           const double i); 
	void print();
};

but don’t!   

  • Use a class when there are member functions, and 
  • Use a struct when there are only data members.  

Class Constructors

A class can be told how to create instances of itself.

// complex.h
class Complex
{
public:
  Complex(const double r,
          const double i)
  { real = r; imag = i; }

  void print();

private:
  double real, imag;
};
  • A constructor is a special member function that is called whenever an instance of its class is created:
// complex.h
class Complex
{
private:
  double real, imag;
public:
  Complex()
  { real = 0.0; imag = 0.0; }

  Complex(const double r)
  { real = r; imag = 0.0; }

  Complex(const double r, 
          const double i)
  { real = r; imag = i; }

  void print();
};
  • A constructor always has the same name as its class.
  • It may not have a return type, nor may it return a value.
  • Constructors can be (and usually are) overloaded (see example code)

Constructor Usage

A constructor is guaranteed to be called for the creation of an instance of its class (even a static instance!)

Note that we have replaced the set method with a constructor.

  • More appropriate in C++, because of the above guarantee.
  • Of course, we could have both, if we wish.

Explicit Constructor Usage

Here are some examples of explicit constructor usage:

// testComplex.cpp
#include "complex.h"

static Complex g(54.6);

int main()
{
  Complex x;
  Complex y(34.5);
  Complex z(27.5, 13.4);

  g.print();
  x.print();
  y.print();
  z.print();

  return 0;
}

Constructors and Default Arguments

You can reduce the proliferation of constructors by using default arguments:

// complex.h
class Complex
{
public:
  Complex(const double r = 0.0, 
         const double i = 0.0)
  { real = r; imag = i; }

  void print();

private:
  double real, imag;
};

Default Constructors

A default constructor is one that can be called without an argument.

  • Note: A default constructor may or may not have zero arguments.  It depends on whether its declaration contains default arguments.
  • For this reason, using the term no-arg constructor is incorrect.

Here’s how a default constructor can be used:

// testComplex.cpp
#include "complex.h"

static Complex g; 	// Calls default constructor

int main()
{
  Complex x; 		// Calls default constructor
  Complex a[10];	// Calls default constructor 10 times

// ...

return 0;
}

Generated Default Constructors

If you declare no constructors for a class:

  • The C++ compiler will automatically generate a default constructor for that class.

If you declare any constructors for a class:

  • The C++ compiler will not generate a default constructor for that class.
  • In that case, if you want a default constructor for that class, you must declare (and write) it explicitly.
  • You will encounter classes which will require you to write default constructors explicitly.

For example:

// complex.h
class Complex
{
public:
  Complex(const double r, 
          const double i)
  { real = r; imag = i; }

  void print();

private:
  double real, imag;
};

// testComplex.cpp
#include "complex.h"

int main()
{
  Complex x;  // error C2512: 
              // 'Complex' : no appropriate default constructor available
  // ...
  return 0;
}

Constructor Usage

You can explicitly initialize elements of an array of class instances using constructors:

// testComplex.cpp
#include "complex.h"

int main()
{
  Complex x[5] =
  {
    Complex(), 
    Complex(2.5),
    Complex(2.3, 4.5)
  };

  // ...

  return 0;
}

If the initializer list does not contain an entry for every element, then the default constructor is used for those that are missing.

If there is no default constructor, then every element must be explicitly initialized.

Type Conversions

Consider:

Complex x(5.4);

This amounts to type conversion from double to Complex.

In general, it would be very convenient if we could accomplish implicit type conversions among instances of different classes.

One common way to accomplish type conversion is to use a constructor with a single argument (or one where all arguments but the first are defaulted):

// testComplex.cpp
#include "complex.h"

extern void bungle(Complex c);

int main()
{
  Complex x(1.0);			// explicit conversion

  bungle( Complex(37.5) );	// explicit conversion
  return 0;
}

Implicit Type Conversions

Implicit type conversions occur in situations where you may or may not want it!

// testComplex.cpp
#include "complex.h"

extern void blather(Complex c); // accepts a Complex

Complex doit() 	// returns a Complex
{
  blather(2.3);	// implicit conversion 
  // Equivalent to blather(Complex(2.3));

  // ...

  return 3.5;	// implicit conversion
  // Equivalent to return Complex(3.5);
}
  • Be extremely wary of using conversion constructors when you don’t want this to happen!  
  • This is easy to forget, so don’t say you haven’t been warned!

Copy Constructors

Consider:

Complex x = 1.0; // initialization, not assignment!

Because of implicit conversions, this can be equivalent to:

Complex x = Complex(1.0);

but then it is often (but not always!) optimized to:

Complex x(1.0);

A copy constructor is one that constructs an instance of a class from another instance of that class:

Complex(const Complex &src); (preferred) 

or:

Complex(Complex &src);

Note the reference (&), which is required 

The following:

Complex(const Complex src);

is not a copy constructor!

Generated Copy Constructors

If you do not declare a copy constructor for a class:

  • The C++ compiler will generate a copy constructor for that class.
    • This is sometimes called a default copy constructor, although the term can be confused with the simple default constructor.
  • For a simple class, the generated copy constructor will do memberwise initialization — i.e., a ‘shallow copy’.
    • This may, or may not, be what you want for the class.
  • Remember, if you declare no constructors for the class, the compiler will also generate a default constructor for the class.

If you do declare a copy constructor for a class:

  • The C++ compiler will not generate one for that class.
  • Neither will it generate a default constructor for the class.
    (Because you declared at least one constructor for the class.)

Explicit Copy Constructors

For example, here’s an explicit copy constructor:

// complex.h
class Complex
{
public:
  Complex(const double r = 0.0,
          const double i = 0.0)
  { real = r; imag = i; }

  Complex(const Complex &src)
  { real = src.real; imag = src.imag; }

  void print();

private:
  double real, imag;
};

// testComplex.cpp
#include "complex.h"
int main()
{
  Complex x(1.0, 4.5);
  Complex y(x);	// Copy constructor
  // ...	
  return 0;
}

Explicit or Generated Copy Constructors?

In some cases, the generated copy constructor is sufficient.

In other cases it is insufficient:

  • When a class contains a pointer to a non-shared object.
  • Other situations where it does not make sense to replicate only the contents of the class and not any external context.

In some other cases, copying a class is meaningless.

  • For example, does it make sense to copy a linked list?  What does it mean?
  • In this case, you need to ensure that no copy constructor exists.  (How?  Later…)

Class Destructors

A class can be told how to destroy an instance of itself.

A destructor is a special member function that is called whenever an instance of its class is destroyed:

// complex.h
class Complex
{
public:
  Complex(const double r = 0.0,
          const double i = 0.0)
  { real = r; imag = i; }

  ~Complex() {} // Destructor

  void print();

private:
  double real, imag;
};

A destructor always has the same name as its class, preceded by a tilde (~) character.

It may not have a return type, nor may it return a value.

A destructor always takes no arguments, so:

  • It cannot be overloaded.
  • A class may only have a single destructor.

Destructors are invoked implicitly;  it is rarely necessary to make an explicit call to a destructor (bad form!). 

A destructor is guaranteed to be called for the destruction of an instance of its class (even a static instance!), except for free store instances (more later…).

Implicit Destructor Usage

Example:

// testComplex.cpp
#include "complex.h"

static Complex g(54.6);

static void doit()
{
  Complex x;  // call constructor for x...
  // ...
}.            // call destructor for x...

int main()		// call constructor for g...
{
  doit();
  // ...
  return 0;
}             // call destructor for g...

Generated Destructors

If you declare no destructor for a class:

  • The C++ compiler will automatically generate a destructor for that class.
  • For a simple class, the generated destructor will do essentially nothing.
  • For derived classes, and classes with class member objects, the generated destructor does more! (Later…)

You can, of course, write your own — and you must for some classes.

What Does a Destructor Do?

In the case of class Complex, there is nothing to do.

In other situations, a destructor can:

  • Deallocate space allocated by the instance.
  • Close a file associated with the instance.
  • Remove a window from the screen that the instance caused to be created.
  • Defrangle a booblefratz, that had been previously frangled by the instance.

A destructor is the complement of a constructor;  it typically cleans up after the instance.

When a Destructor is NOT Called

Note that no destructor is called in the following case:

// testComplex.cpp
#include "complex.h"

static void doit()
{
  Complex *ptr = new Complex(1.0);	
   // constructor called for what ptr will point to.
   
  // ...
}								// no destructor called!

If you don’t clean it up with an explicit delete, it doesn’t get cleaned up!

Destructor Usage

Here’s an example of where you need an explicit destructor:

// complex.h
#include <string.h>

class MyString
{
public:
  MyString(const char source[])
  {
    p = new char[strlen(source) + 1];
    strcpy(p, source);
  }

  ~MyString() 
  { 
    delete [] p; 
  }
  // ... 

private:
  char *p; 
};

The constructor allocated an array which must be deallocated (else you will have a memory leak!).

Here’s a more complex example — a FileStats class:

// FileStats.h
#include <stdio.h>		// For FILE

class FileStats
{
public:
  FileStats(char *fileName);
  ~FileStats();
  void collectStats();

private:
  // Methods
  int readChar();

  // Data
  char *name;			// File name
  FILE *ifp;			// File pointer

    // Counts of characters, words, lines
  typedef unsigned int count;
  count 	chars;
  count 	words;
  count 	lines;
};
// FileStats.cpp
#include <iostream>
#include "FileStats.h"

// Constructor
FileStats::FileStats(char *fileName)
{
  ifp = fopen(fileName, "r"); // Open for reading
  if (ifp != NULL)
  {
    chars = words = lines = 0;
    name = new char[strlen(fileName) + 1];
    strcpy(name, fileName);
  }
  else
  {
    exit(1);	// Exit abruptly
  }
}

// Private utility member function
int FileStats::readChar()
{
  int ch;
  ch = fgetc(ifp);
  if (ch != EOF)
    chars++; // count chars
  if (ch == '\n')
    lines++; // count lines
  return ch;
}

// Collect File Statistics member function
void FileStats::collectStats()
{
  bool in_word = false;
  int ch;
  while (EOF != (ch = readChar()))
  {
    if (!in_word)
    {   	 // not already in word
      if (isalnum(ch))
      {  	// start of new word
        words++; // count words
        in_word = true;
      }
  }
  else
  {   	// already in a word
    if (!isalnum(ch))
    in_word = false; 
        // end of word
    }
  }
}

// Destructor
// Cleans up and prints statistics
//
FileStats::~FileStats()
{
  // We opened the file, so be sure to close it
  if (ifp != NULL)
    fclose(ifp);

  // Output file statistics
  std::cout << "File '" << name
            << "' statistics:" << std::endl
            << " chars : " << chars << std::endl
            << " words : " << words << std::endl
            << " lines : " << lines << std::endl;

  // Free up allocated space
  delete [] name;
}

Note that the actual reporting is done in the destructor!
This is not necessarily good practice — it depends on the situation.
We’re doing it here to show that a destructor can potentially do a fair amount of work — destructors are not necessarily trivial.

And now to use this magical class…

// testFileStats.cpp
#include <iostream>
#include "FileStats.h"

int main(int argc, const char *argv[])
{
  int retVal = 1;
  if (argc > 1)
  {
    FileStats fs(argv[1]);
    fs.collectStats();
    retVal = 0;
  }
  else
  {
    std::cerr << "No filename specified" 
              << std::endl;
 }
  return retVal;
}					// FileStats destructor gets called here...

Try using this FileStats class (cut and paste it), and have it print out the statistics from some of your files!

It will give you some practice with creating the various files that you typically use to implement a class and make it available to its clients.

Summary

We’ve talked about quite a few things relating to classes:

  • Class declarations and definitions
  • Class member functions
  • Class scope
  • Class constructors:
    • Default constructors
    • Copy constructors
    • Conversion constructors
  • Destructors

But there’s lots more to learn about classes…

Index