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
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…