Classes and Objects


A class is a collection of C++ statements that specify the state (data members) and implementations of behavior (member functions) that a particular type of object may have. Think of a class as a "blueprint" that objects may be created from. So a class is not an object, but a description of an object. When we define a class in C++, we are defining a new data type.

An object is simply a variable of a class type.

To define a new class in C++, we write a class definition. For example, here is a class definition for a new class named Student, which represents information about a student.

class Student
{
private:

    // Data members
    char name[31];
    double gpa;

public:

    // Constructors
    Student();
    Student(const char*, double);

    // Accessor and mutator member functions
    void set_name(const char*);
    const char* get_name() const;
    void set_gpa(double);
    double get_gpa() const;

    // Other member functions
    void print() const;
};

A class definition starts with the keyword class, followed by the name of the new class data type (Student in this case). The body of the class is enclosed in braces.

The class definitions contains variable declarations for the data members of the class and prototypes (declarations) for the member functions of the class.

Definitions for the member functions of the class are normally placed outside of the class definition.

C++ also has the struct keyword for defining new composite data types, mostly to maintain backward compatibility with the C programming language. In practice, there is very little difference between a class and a struct in C++, beyond the fact that everything in a class is private by default while everything in a struct is public by default.

Access Modifiers

Access modifiers are keywords that modify the accessibility of members of a class. C++ uses three access modifiers:

Modifier Access
private Accessible in member functions of the class, member functions of friend classes, or by friend functions. Default access for class members.
protected Accessible in member functions of the class, member functions of friend classes, or by friend functions. Also accessible in member functions of derived classes and friends of those derived classes.
public Accessible in any function that has access to an object of the class. Default access for struct members.

In general, data members that are not constants should usually have private access. Constants and member function prototypes should usually have public access. A constant or member function that is only used within the class (say a sort member function that is only called by another member function of the class) may be made private instead.

The protected keyword is used with inheritance and will be covered in more detail later in the semester.

Constructors

A constructor is a special type of member function that is called to create a new object of the class. The usual job of a constructor is to initialize the data members of a new object to valid values.

The name of a constructor is always the same as the name of the class. A constructor has no return type and cannot be constant. The constructor may have 0 to many parameters passed to it.

Through function overloading, a C++ class may have multiple constructors as long as their parameters are different. For example, if one constructor takes zero parameters and another constructor takes two, the compiler will be able to tell them apart.

If you do not code any constructors for your class, the compiler will generate a "default" constructor without any parameters. The compiler-supplied constructor does nothing (effectively it has an empty function body). If you write any constructors for your class, the compiler will not supply this default constructor.

The code in a constructor definition typically sets the data members of the new object to some valid default value or to the values of the parameters passed into the constructor. This may be done using assignment statements, a function like strcpy() (if the data member is a C string), or by calling the mutator (set) member function for the data member. The latter is especially common when the mutator member function incorporates some error checking code that you don't want to duplicate.

Here are a couple of examples:

Student::Student()
{
    strcpy(name, "None");
    gpa = 0.0;
}

Student::Student(const char* new_name, double new_gpa)
{
    set_name(new_name);
    set_gpa(new_gpa);
}

If your constructor accepts parameters, do not give them the same names as data members of the class unless you are prepared to use the this pointer to let the compiler tell them apart.

Accessor and Mutator Member Functions

If the data members of a class are private, how can code outside the class access or modify them?

Since private members are accessible to member functions of the class, we can write a member function to get the value of a data member (an accessor member function) and/or to set a new value for the data member (a mutator member function). The mutator member function may incorporate some code to ensure that the data member can only be set to a valid value. For example:

void Student::set_name(const char* new_name)
{
    strcpy(name, new_name);
}
        
const char* Student::get_name() const
{
    return name;
}

void Student::set_gpa(double new_gpa)
{
    if (new_gpa < 0.0)
        gpa = 0.0;
    else if (new_gpa > 4.0)
        gpa = 4.0;
    else
        gpa = new_gpa;
}
    
double Student::get_gpa() const
{
    return gpa;
}

Since accessor member functions do not modify any data members of the object that called the member function, they should be coded as constant member functions.

If an accessor member function returns a reference or pointer to a data member, the return data type should usually be coded as a reference or pointer to a constant variable. Failure to do so could allow the code that called the accessor method to use the reference or pointer that was returned to modify the data member directly, even though it is private.

Parameters passed to mutator member function using a reference or pointer should usually be coded as a reference or pointer to a constant variable. The mutator member function code has no need to change the original variable in the calling routine using the reference or pointer and should not be allowed to do so.

As the following code example illustrates, we cannot directly access private data members of a class, but we can get the value of a data member or modify the value of a data member by calling a public accessor or mutator member function.

Student s("Anna Gonzalez", 3.65);
    
s.gpa = 3.72;                    // Syntax error - gpa is private.
cout << s.name << endl;          // Syntax error - name is private.

s.set_gpa(3.72);                 // This is correct and will change the gpa data member to 3.72.
cout << s.get_name() << endl;    // This is correct and will print the name.

Other Member Functions

We can write other member functions for the class as needed. For example, we might write a function to print the data members of an object in a neatly formatted fashion.

If a member function does not modify any data members of the object that called it, the function should be coded as a constant member function.

C++11 Initialization Option for Data Members

C++11 added the ability to initialize the non-static data members of a class at the time you declare them using a "brace-or-equal" syntax. This is very convenient, and can eliminate most or all of the code from your default constructor. Here are a few examples of the kind of initializations you can do in a class definition:

class InitTest
{
private:

    int a = 0;
    int b{10};

    double c = 9.9;
    double d{23.76};

    bool e = false;
    bool f{true};

    char g = 'a';
    char h{'x'};

    char str1[21]{};         // Initialized to an empty string
    char str2[11]{"Joe"};

    string str3 = "hello";
    string str4{"goodbye"};

    int ar1[4]{2, 4, 6, 8};
    
    ...
};

Even if you initalize all of your data members in your class definition, you may still need to write a default constructor (although the body of the constructor may be entirely empty). If you write at least one constructor, the compiler will not generate a default constructor definition for your class. If you then try to create an object in a way that would call the default constructor, you will get a syntax error.

There are two solutions to this issue:

  1. Write an empty default constructor.
  2. Code = default on a default constructor prototype to instruct the compiler to generate a default constructor definition even though you've written other constructors.

The second solution is used in the code example below.

Full Code Example

Putting this all together, here's the full code for the Student class, as well as a main() function that tests the class.

#include <iostream>
#include <iomanip>
#include <cstring>

using std::cout;
using std::endl;
using std::fixed;
using std::setprecision;

class Student
{
private:

    // Data members
    char name[31]{"None"};
    double gpa{0.0};

public:

    // Constructors
    Student() = default;
    Student(const char*, double);

    // Accessor and mutator member functions
    void set_name(const char*);
    const char* get_name() const;
    void set_gpa(double);
    double get_gpa() const;

    // Other member functions
    void print() const;
};

int main()
{
    Student s1;
    Student s2("Anna Gonzalez", 3.65);
    const Student s3("Todd Bell", 3.24);
    
    // Test object s1.
    s1.print();
    cout << endl << s1.get_name() << endl;
    cout << s1.get_gpa() << endl << endl;
    
    s1.set_name("Shannon Willis");
    s1.set_gpa(3.54);

    s1.print();
    cout << endl << s1.get_name() << endl;
    cout << s1.get_gpa() << endl << endl;

    // Test object s2.
    s2.print();
    cout << endl << s2.get_name() << endl;
    cout << s2.get_gpa() << endl << endl;
    
    s2.set_name("Anna Castro");
    s2.set_gpa(3.72);
    
    s2.print();
    cout << endl << s2.get_name() << endl;
    cout << s2.get_gpa() << endl << endl;

    // Test object s3. Object is constant, so calling a non-constant
    // mutator member function will cause a syntax error.
    s3.print();
    cout << endl << s3.get_name() << endl;
    cout << s3.get_gpa() << endl;
    
    return 0;
}

// Student constructor

Student::Student(const char* new_name, double new_gpa)
{
    set_name(new_name);
    set_gpa(new_gpa);
}

// Student accessor and mutator member functions

void Student::set_name(const char* new_name)
{
    strcpy(name, new_name);
}

const char* Student::get_name() const
{
    return name;
}

void Student::set_gpa(double new_gpa)
{
    if (new_gpa < 0.0)
        gpa = 0.0;
    else if (new_gpa > 4.0)
        gpa = 4.0;
    else
        gpa = new_gpa;
}

double Student::get_gpa() const
{
    return gpa;
}

// Other member functions

void Student::print() const
{
    cout << "Name: " << name << endl
         << "GPA: " << fixed << setprecision(2) << gpa << endl;
}

Download the full program text and makefile