Class

Class

Access control (Encapsulation)

Access control restricts the access to certain parts of the class.

public and private are two major access levels.

  • Anyone can access public members.
  • Only the member functions of the class can access private members.

In struct, all the members are public if you don’t specify the access level. To make members private, you have to explicitly specify it.

struct Student {
    private:
        char name[20];
        int born;
        bool male;
        char address[100];
        float gpa;

    public:
        void set_name(const char* s) {
            if (strlen(s) < sizeof(name)) {
                strncpy(name, s, sizeof(name) - 1);
                name[sizeof(name) - 1] = '\0';
            }
            else {
                cout << "Name is too long!" << endl;
            }
        }
        // other getters and setters are omitted
};
  • Rather than allowing users of Student to modify the name directly, we only expose the set_name method.
  • This ensures that the name is not modified incorrectly.

class keyword

In C++, people usually use class instead of struct to define a class.

The only difference between struct and class is the default access level of the members.

  • struct members are public by default.
  • class members are private by default.
class Student {
    // private members
    // it's still recommended to use `private` explicitly
    char name[20];
    int born;
    bool male;
    char address[100];
    float gpa;

    public:
        void set_name(const char* s) {
            if (strlen(s) < sizeof(name)) {
                strncpy(name, s, sizeof(name) - 1);
                name[sizeof(name) - 1] = '\0';
            }
            else {
                cout << "Name is too long!" << endl;
            }
        }
};

You can only access private members through the member functions.

A question: then how can we initialize the member variables? See constructors below.

Recall

Aggregate initialization

Recall that in struct, we can use aggregate initialization to initialize member variables.

struct Student {
    char name[20];
    int born;
    bool male;
};

Student s1 = {"John", 2000, true};

Default member initializer

C++11 introduces default member initializer.

struct Student {
    char name[20] = "John";
    int born = 2000;
    bool male = true;
};

To create a Student object without using the default values, you can use the following ways2 3:

// Student s1 = {"David", 2001, true}; // error

Student s2 {"David", 2001, true}; // c++11, list initialization
Student s3 = Student {"Jane", 2002, false}; // copy-list-initialization
Student s4 = Student {.name="Eric"}; // c++20, designated initialization

Constructors

Constructor

Constructor is a special method with special declaration.

  • Name of the constructor is the same as the name of the class.
  • No return type is needed.
  • A constructor will be invoked automatically when an object is created.

In our previous examples, we haven’t written a constructor, so the compiler will automatically generate a default constructor with empty body.

Example

class Student {
    private:
        string name;
        int born;
        bool male;
        string address;
        float gpa;

    public:
        /* Constructor:
        1. No return type
        2. Name is the same as the class name */
        Student(string name_, int born_, bool male_, string address_, float gpa_) {
            name = name_;
            born = born_;
            male = male_;
            address = address_;
            gpa = gpa_;
        };
};

Constructor initialization list

Initialization lists are an alternative technique for initializing an object’s data members in a constructor.

Student(string name_, int born_, bool male_, string address_, float gpa_) : 
name(name_), born(born_), male(male_), address(address_), gpa(gpa_) {}

Prefer member initializer lists over assigning values in the body of the constructor.

  • A member initializer list directly initializes the members.
  • Assigning values in the constructor body creates a temporary object and then copies it to the member variable, which is less efficient.

Destructors

Destructor

An object’s destructor is its clean-up method.

  • A destructor will be invoked automatically before an object is destroyed.
  • Be formed from the class name with a ~ prefix.
  • No return type, no parameters.
class Student {
    private:
        string name;
        int born;
        bool male;

    // ... other members and methods ...
    public:
        ~Student() {
            cout << "Destructor called for " << name << endl;
        }
};

In practice, you don’t need to write a destructor for an object that has no dynamic memory allocation. Variables on stack are automatically destroyed when they are out of scope.

The next example shows a case where you need to write a destructor.

// avoid namespace pollution
namespace myvec {
    class vector {
        private:
            int* data;
            int size;
        public:
            vector(int size) : size(size) {
                data = new int[size];
            }
            ~vector() {
                delete[] data;
            }
            // ... other methods ...
    };
}

this pointer

Introduction

In C++, every non-static member function has an implicit parameter this pointer.

void Student::setName(const char* s) {
    strncpy(name, s, sizeof(name) - 1);
}

is equivalent to

void setName(const char* s) {
    strncpy(this->name, s, sizeof(this->name) - 1);
}

this pointer is a constant pointer to the object for which the member function is called.

Why this pointer?

The use of this allows all instances of a class to share the same function code, rather than each object having its own copy of member functions.

  • All the member variables are allocated contiguously in memory.
  • Should methods be stored in the object or separately?
    • To reduce the memory usage, methods are usually stored separately from the object.
  • Then how does the method know which object it is supposed to operate on?
Student s1 = Student {"Amy", 2000, false};
Student s2 = Student {"Bob", 2001, true};

s2.setName("Bob");
  • With this pointer, the compiler knows which object is calling the method, so it can correctly modify the corresponding object.
    • this points to the object that called the method.

this pointer can also avoid ambiguity in codes with the same name.

void setBorn (int b){
    born = b;
}

Equivalent to:

void setBorn (int born){
    this->born = born;
}

const and static

const variables

In previous lectures, we’ve seen that const can be used to declare a constant variable or used in function parameter.

const int a = 10;
const int *p_int = &a;
int * const p_int2 = &a;

void func(const int *p);
void func2(const int &ref);

const member variables

In class, const can also be used to declare a constant member variable.

const member variables is similar to const variables:

  • It must be initialized with a value at the time of object creation.
  • It can’t be modified through the member functions.

To initialize a const member variable, you can use the initialization list.

class MyClass {
    private:
        const int constVar;
    public:
        MyClass(int v) : constVar(v) {}
};

Alternatively, you can use default member initializer.

class MyClass {
    private:
        const int constVar = 10;
};

const member functions

A const member function is a member function that promises not to modify the object on which it is called.

  • const is added after the member function’s parameter list.
  • The member function can’t modify the object’s member variables.
class Student {
    private:
        string name;
    public:
        string get_name() const {
            return name;
        }
};

static member variables

static member variables are shared by all instances of the class.

class Student {
  private:
    static size_t student_total; // declaration only
    char *name;
    int born;
    bool male;

  public:
    Student(const char *initName, int initBorn, bool isMale) {
        student_total++;
        name = new char[1024];
        setName(initName);
        born = initBorn;
        male = isMale;
        cout << "Constructor: Person(const char, int , bool): student_total = "
             << student_total << endl;
    }

    ~Student() {
        student_total--;
        cout << "To destroy object: " << name;
        cout << ". Then " << student_total << " students are left" << endl;
        delete[] name;
    }
    // .. other methods ..
};

// definition of static member variable
size_t Student::student_total = 0;
  • static member variables are not part of the individual objects, but shared by all objects of the class.
    • They should be defined outside the class.
  • They can be accessed through the class name.

A static data member may be declared inline.

  • An inline static data member can be defined in the class definition. It does not need an out-of-class definition(C++17 and later).
struct Student {
    inline static int student_total = 0;
    // other members are omitted
};

Footnotes

  1. For simplicity, the code snippets in this lecture are put in the same file.↩︎

  2. See aggregate-initialization↩︎

  3. a general tip to make initilization easier: use brace {} everywhere↩︎