We will be encountering various aspects of the C++ language syntax throughout this course. We expect much of this material will be new to most students. However, rather than introduce all of this new material at once, we will attempt to highlight new aspects as they arise later in the course.
Chapters 1.1-1.4 of our text provides a brief but thorough overview of many such aspects of C++. In particular, this chapter is meant for an audience which is familiar in programming in a similar language, such as C or Java, yet possibly new to C++. Much of the presentation focusses specifically on areas in which C++ differs. We expect that some of the material in those sections will serve as review, but some new.
Some such differences discussed are important, but we leave as reading:
The use of cin and cout for formatted
input/output
vs.
scanf and printf
Additional primitive data type, bool
C++ style comments, e.g.
// this is a comment
/* as is this C-style comment */
C++ class std::string
vs.
C-style strings (char[ ])
Explicit cast operations are prefered in C++
vs.
C-style casts
Operator Overloading
enum MealType { NO_PREF, REGULAR, LOW_FAT, VEGETARIAN};
struct Passenger {
string name;
MealType mealPref;
bool isFreqFlyer;
string freqFlyerNo;
}
With such a definition, we can now declare a variable of this type,
such as:
Passenger p = { "John Smith", VEGETARIAN, true, "293145"};
Or we can declare a pointer to such a datatype, and then dynamically
create a new such data object,
Passenger *p; p = new Passenger; p->name = "John Smith";and when done, dynamically delete this same object,
delete p;
A class allows for the definition of a type which includes both composite data as well as functions. Let's look at one of the examples from the text (p. 36), after which we discuss many aspects of the syntax:
class Passenger {
private:
string name;
MealType mealPref;
bool isFreqFlyer;
string freqFlyerNo;
public:
bool isFrequentFlyer() const
{ return isFreqFlyer; }
void makeFrequentFlyer(const string& newFreqFlyerNo) {
isFreqFlyer = true;
freqFlyerNo = newFreqFlyerNo;
}
};
We will call the variables data members or member variables. We will call the functions member functions or methods.
We also point out that each member of the class has an access control specifier (e.g. public, private). These directly support the principle of encapsulation. In this example, it so happens that all of the data members are assigned private access control, whereas all of the member functions are assigned public access control. Though this is a common configuration, one is free to define both public and private variables as well as public and private functions.
The use of the keyword const has a variety of meaning in different contexts. For example,
const can be used in the declaration of a variable (not demonstrated in this example)
const can be used in the declaration of a method (e.g., isFrequentFlyer())
We will often distinguish between accessor functions and update functions of a class, depending on whether a function has the possible effect of changing any of the member variables.
const can be used in the declaration of a variable (e.g., newFreqFlyerNo parameter of makeFrequentFlyer)
This is most often used to allow a parameter to be passed by reference, yet with a read-only expectation.
Function Body Definitions
In the above example, the function bodies were declared within the actual class definition (e.g., makeFrequentFlyer). It is also possible to define the function body from outside of the class, so long as the class defintion contains a prototype of the function definition.
For example, an equivalent definition for class Passenger could be written as:
class Passenger {
private:
string name;
MealType mealPref;
bool isFreqFlyer;
string freqFlyerNo;
public:
bool isFrequentFlyer() const; // Note the function body is not given!
void makeFrequentFlyer(const string& newFreqFlyerNo);
};
Of course, before using the class, the function bodies must be defined elsewhere. The reason for the separation is often so that the class definition can be given in a header file (e.g. Passenger.h), with the actual implementation of the function methods contained in a separate file (e.g. Passenger.cpp).
However, when providing body definitions from outside the class definition, it is important to provide the context, in the form of a scope resolution operator, ::, as in the following:
bool Passenger::isFrequentFlyer() const
{ return isFreqFlyer; }
void Passenger::makeFrequentFlyer(const string& newFreqFlyerNo) {
isFreqFlyer = true;
freqFlyerNo = newFreqFlyerNo;
}
Creating and Using Objects from a defined Class
With such a class definition, code could be written using objects of type Passenger, such as:
Passenger pass; // pass is a Passenger
// ...
if (!pass.isFrequentflyer()) { // not already a frequent flyer?
pass.makeFrequentFlyer("392953"); // set pass's freq flyer number
}
pass.name = "Joe Blow"; // ERROR! name is a private member
Constructors
Again, we begin with an analogy to C-style structs. When an object of a defined struct is created, there is an issue as to how the data fields should be initialized. In general, there may be default values assigned or presumably the user will explicitly initialize the values as desired after the object is created.
To allow more general functionality, a class has a special function called a constructor which is automatically called each time an object of that class comes into existence. The name of the construct must be precisely the name of the class. By default, a constructor does not take any parameters. For example, here may be a typical constructor for class Passenger:
Passenger::Passenger() { // default constructor
name = "--NO NAME--";
mealPref = NO_PREF;
isFreqFlyer = false;
freqFlyerNo = "NONE";
}
Yet it is also possible to allow for alternate forms of the constructor with various parameters. For example, here is a constructor which allows for initial data values to be expressed using explicit parameters.
Passenger::Passenger(const string& nm, MealType mp, string ffn = "NONE") {
name = nm;
mealPref = mp;
isFreqFlyer = (ffn != "NONE"); // true only if ffn given
freqFlyerNo = ffn;
}
Looking carefully at the above, we note that only three parameters are
given, as the isFreqFlyer value can be determined based on
whether a valid freqFlyerNo is given. Furthermore, the
ffn parameter is declared with a default argument of
"NONE". This means that the constructor can be invoked with
three explicit parameters, for instance, ("John Smith",
VEGETARIAN, 293145), or only two parameters, e.g., ("Joe
Blow", NO_PREF), in which case the default parameter value is
used for ffn.
As another example, here is a "copy" constructor which creates a new passenger whose data members are initialized so as to match the values associated with some other existing passenger:
Passenger::Passenger(const Passenger& pass) {
name = pass.name;
mealPref = pass.mealPref;
isFreqFlyer = pass.isFreqFlyer;
freqFlyerNo = pass.freqFlyerNo;
}
Note that the above constructors are relatively straightforward, in that they are used simply to set the initial values of the data members. However, more significant effort is allowed. For example, each time a passenger is constructed it may be that we updated some other count of the overall number of passengers, or run a security check on the passenger name, etc.
Destructors
In similar fashion, a destructor is a member function that is automatically called when a class object ceases to exist (either because delete is called, or because an explicit variables goes out of scope).
A destructor function must be named with a tilde (~) followed by the exact class name. For example, the class Passenger would have a destructor ~Passenger(). Note that destructors do not take accept parameters.
In some cases, nothing special needs to be done when an object is destructed, and so the destructor can be omitted. But in other cases, more significant book keeping may be done at this time.