Microsoft Visual C++ Windows Applications by Example
上QQ阅读APP看书,第一时间看更新

Classes

A class is defined and implemented. The definition of the class sets the fields and the prototypes of the methods, and the implementation defines the individual methods.

The methods can be divided into four categories: constructors, destructors, modifiers, and inspectors. One of the constructors is called when the object is created and the destructor is called when it is destroyed. A constructor without parameters is called a default constructor. A class is not required to have a constructor; in fact, it does not have to have any members at all. However, I strongly recommend that you include at least one constructor to your classes. If there is at least one constructor, one of them has to be called when the object is created, which means that unless the default constructor is called, parameters have to be passed to the constructor when the object is created. Methods may be overloaded the same way as freestanding functions and a class may have several constructors as long as they have different parameter lists. However, the class can only have one destructor because it cannot have parameters and can therefore not be overloaded. As the names imply, modifiers modify the fields of the class and inspectors inspect them.

The First Example

Let us start with a simple example. How about a car? What can we do with a car? Well, we can increase and decrease the speed, we can turn left and right, and we can read the speed and the direction of the car (let us assume we have a compass as well as a speedometer in the car).

Let us define two constructors, one with and one without parameters (default constructer). Every constructor has the name of the class without a return type (not even void). The destructor does also have the name of the class preceded by a tilde (~). In this class, we really do not need a destructor, but let us throw one in anyway.

A field in a class is often preceded by m_, identifying it as a field ('m' stands for member) in order to distinguish them from local and global variables.

We have two fields for speed and direction: m_iSpeed and m_iDirection. We want to increase and decrease the speed as well as turn left and right. This gives us four modifiers: IncreaseSpeed, DecreaseSpeed, TurnLeft, and TurnRight. We also want to read the speed and direction, which gives us two inspectors: GetSpeed and GetDirection.

The code is often divided into several files: one header file, often named after the class with extension .h, containing the class definition, and one implementation file, with extension .cpp, containing the method definitions. The main function is often placed in a third file. In this example, the files are named Car.h, Car.cpp, and Main.cpp.

Car.h

class Car
{
  public:
    Car();
    Car(int iSpeed, int iDirection);
    ~Car();

    void IncreaseSpeed(int iSpeed);
    void DecreaseSpeed(int iSpeed);

    void TurnLeft(int iAngle);
    void TurnRight(int iAngle);

    int GetSpeed();
    int GetDirection();

  private:
    int m_iSpeed, m_iDirection;
};

In every class implementation file, we have to include the header file. Then we define the methods, every method must be marked with its class name followed by two colons (Car:: in this case) in order to distinguish between class methods and freestanding functions.

In the first constructor, we initialize the fields value to zero with colon notation. The colon notation can only be used in constructors and is used to initialize the values of the fields. We have to do that in the constructor. Otherwise, the fields would be uninitialized, meaning they would be given arbitrary values. The second constructor initializes the fields with the given parameters. The destructor is usually used to free dynamically allocated memory or to close opened files. We have nothing of that kind in this program, so we just leave the destructor empty.

The modifiers take each parameter and update the values of the fields. The inspectors return the value of one field each.

Car.cpp

#include "Car.h"
Car::Car()
 :m_iSpeed(0),
  m_iDirection(0)
{
    // Empty.
}

Car::Car(int iSpeed, int iDirection)
 :m_iSpeed(iSpeed),
  m_iDirection(iDirection)
{
  // Empty.
}

Car::~Car()
{
  // Empty.
}
void Car::IncreaseSpeed(int iSpeed)
{
  m_iSpeed += iSpeed;
}

void Car::DecreaseSpeed(int iSpeed)
{
  m_iSpeed -= iSpeed;
}

void Car::TurnLeft(int iAngle)
{
  m_iDirection -= iAngle;
}

void Car::TurnRight(int iAngle)
{
  m_iDirection += iAngle;
}

int Car::GetSpeed()
{
  return m_iSpeed;
}

int Car::GetDirection()
{
  return m_iDirection;
}

The main function file must also include the header file. We create an object by writing the class name followed by the object name. If there is at least one constructor in the class, we have to call it by sending a list of matching actual parameters (or no parameters at all in the case of a default constructor). In this case, we can choose between two integers (speed and direction) or no parameters at all.

One important issue to notice is that if we do not send parameters, we will also omit the parentheses. If we do add parentheses, it would be interpreted as a function declaration (prototype). The following line would interpret car3 as a freestanding function that returns an object of the class Car and take no parameters. As the function is not defined, it would result in a linking error.

Car car3();

Notice that the fields of the class are private. That means that attempting to the field m_iSpeed at the last line would result in a compile-time error. We do not have to set fields to private; however, it is a good practice to do so.

Main.cpp

#include <iostream>
using namespace std;

#include "Car.h"

void main()
{
  Car car1(100, 90);

  cout << "Car1: " << car1.GetSpeed()        // 100
       << " degrees, " << car1.GetDirection() // 90
       << " miles per hour" << endl;
  Car car2(150, 0);
  car2.TurnRight(180);

  cout << "Car2: " << car2.GetSpeed()        // 150
       << " degrees, " << car2.GetDirection() // 180
       << " miles per hour" << endl;

  Car car3;
  car3.IncreaseSpeed(200);
  car3.TurnRight(270);

  cout << "Car3: " << car3.GetSpeed()      // 200
     << " degrees, " << car3.GetDirection() // 270
     << " miles per hour" << endl;

// Causes a compiler error as m_iSpeed is a private member.
// cout << "Speed: " << car3.m_iSpeed << endl;
}

The Second Example

Let us go on to the next example, this time we model a bank account. There are a few new items. As mentioned in the first chapter, we can define a constant inside a function. We can also define a constant field in a class. It can only be initialized by the constructors; thereafter, it cannot be modified. In this example, the account number is a constant. It is written in capital letters, which is an established style.

We can also define methods to be constant. Only inspectors can be constant, as a constant method cannot modify the value of a field. In this example, the inspectors GetNumber and GetSaldo are constant. We cannot mark any other methods as constant, and neither constructors nor the destructor can be constant.

In addition, objects can be constant. A constant object can only call constant methods; that is inspectors marked as constant. In this way, we assure that the values of the fields are not altered since the object was created.

We also add a copy constructor to the class, which takes a (possible constant) reference to another object of the same class. As the name implies, it is used to create a copy of an object. The new object can be initialized with the parentheses or the assignment operator. The second and third of the following lines are completely interchangeable.

BankAccount accountOriginal(123);
BankAccount accountCopy1(accountOriginal);
BankAccount accountCopy2 = accountOriginal;

In the same way as a freestanding function, a method may have default parameters. However, just as is the case of freestanding functions, we can indicate the default values only in the method declarations. I recommend that you state the value in the method's declaration in the header file, and surround the values by block comments in the definition. In this example, the first constructor has a default parameter.

A method (including the constructors and the destructor) can also be inline. That means that it is defined (not just declared) in the class definition of the header file. There is a good rule of thumb to limit the inline methods to short ones, preferably methods whose whole code goes into one row. In this example, the modifiers Deposit and Withdraw as well as the inspectors GetNumber and GetSaldo are inline.

BankAccount.h

class BankAccount
{
  public:
    BankAccount(int iNumber, double dSaldo = 0);
    BankAccount(const BankAccount& bankAccount);

    void Deposit(double dAmount)  {m_dSaldo += dAmount;}
    void Withdraw(double dAmount) {m_dSaldo -= dAmount;}

    int GetNumber()   const {return m_iNUMBER;}
    double GetSaldo() const {return m_dSaldo; }

  private:
    const int m_iNUMBER;
    double m_dSaldo;
};

The implementation file is short; it only contains the definitions of the constructors. Note the comments around the default value in the constructor's parameter list.It would result in a compile-time error if we indicated the value in the implementation file.

BankAccount.cpp

#include "BankAccount.h"

BankAccount::BankAccount(int iNumber, double dSaldo /* = 0 */)
 :m_iNUMBER(iNumber),
  m_dSaldo(dSaldo)
{
  // Empty.
}

BankAccount::BankAccount(const BankAccount& bankAccount)
 :m_iNUMBER(bankAccount.m_iNUMBER),
  m_dSaldo(bankAccount.m_dSaldo)
{
  // Empty.
}

The main function creates two objects of the BankAccount class. The second one is constant, which means that the call of the non-constant method Withdraw at the last line would result in an error.

Main.cpp

#include <iostream>
using namespace std;

#include "BankAccount.h"

void main()
{
  BankAccount account1(123);
  account1.Deposit(100);
  cout << "Account1: number " << account1.GetNumber() // 123
        << ", $" << account1.GetSaldo() << endl;      // 100

  account1.Withdraw(50);
  cout << "Account1: number " << account1.GetNumber() // 123
       << ", $" << account1.GetSaldo() << endl;       // 50

  BankAccount copyAccount(account1);
  cout << "Copy Account: number " << copyAccount.GetNumber()
       << ", $" << copyAccount.GetSaldo() << endl;  // 50, 123

  const BankAccount account2(124, 200);
  cout << "Account2: number " << account2.GetNumber() // 124
       << ", $" << account2.GetSaldo() << endl;       // 200
// Would cause a compiler error.
// account2.Withdraw(50);
}