Chapter 7. Classes

7.1 Defining Abstract Data Types

Data abstraction is a programming technique that relies on the separation of interface and implementation. The interface of a class consists of the operations that users of the class can execute. The implementation includes the class's data members, the bodies of the functions that constitute the interface, and any functions needed to define the class that are not intended for general use.

Encapsulation enforces the separation of a class's interface and implementation. A class that is encapsulated hides its implementation, users of the class can use the interface but have no access to the implementation.

7.1.1 Designing the Class

C++ programmers tend to speak of users interchangeably as users of the application or users of a class.

7.1.2 Defining the Revised Class

Functions defined in the class are implicitly inline.

The member function's body can be defined inside or outside of the class body. We can use the dot operator to fetch the member of the object.

Member functions access the object on which they were called through an extra, implicit parameter named this. When we call a member function, this is initialised with the address of the object on which the function was invoked.

std::string isbn() const { return this->bookNo; }

this a a const pointer, we cannot change the address that this holds. this is implicit and does not appear in the parameter list. There is no place to indicate that this should be a pointer to const. The language resolves this problem by letting us put const after the parameter list of a member function. Member functions that use const in this way are const member functions:

std::string Sales_data::isbn(const Sales_data *const this) { ... }

The definitions of the member functions of a class are nested inside the scope of the class itself.

The member's definition must match its declaration when we define a member function outside the class body.

Some functions define operations that are conceptually part of the interface of the class, but they are not part of the class itself.

7.1.4 Constructors

Classes control object initialisation by defining one or more special member function known as constructor.

Constructors have the same name as the class. Unlike other functions, constructor have no return type. Like other functions, constructor have a parameter list and a function body. A class can have multiple constructors. Constructors may not be declared as const.

Classes control default initialisation by defining a special constructor, known as the default constructor. The default constructor is one that takes no arguments. If our class does not explicitly define any constructors, the compiler will implicitly define the default constructor for us. The compiler-generated constructor is known as the synthesised default constructor.

  • The compiler generates a default constructor automatically only if a class declares no constructors.

  • The second reason to define the default constructor is that for some classes, the synthesised default constructor does the wrong thing.

  • A third reason that some classes must define their own default constructor is that sometimes the compiler is unable to synthesise one.

Constructors have no return type, so this definition starts with the name of the function we are defining. Members that do not appear in the constructor initialiser list are initialised by the corresponding in-class initialiser or are default initialised.

7.1.5 Copy, Assignment, and Destruction

In addition to defining how objects of the class type are initialised, classes also control what happens when we copy, assign, or destroy objects of the class type. If we do not define these operations, the compiler will synthesise them for us. The synthesised versions are unlikely to work correctly for classes that allocate resources that reside outside the class objects themselves.

The synthesised version for copy, assignment, and destruction work correctly for classes that have vector or string members.

7.2 Access Control and Encapsulation

C++ uses access specifiers to enforce encapsulation:

  • Members defined after a public specifier are accessible to all parts of the program. The public members define the interface to the class.

  • Members defined after a private specifier are accessible to the member functions of the class but are not accessible to code that uses the class. The private sections encapsulate the implementation.

The only difference between struct and class is the default access level. If we use the struct keyword, the members defined before the first access specifier are public; if we use class, then the members are private.

7.2.1 Friends

A class can allow another class or function to access its nonpublic members by making that class or function a friend. A class makes a function its friend by including a declaration for that function preceded by the keyword friend:

class Sales_data {
    friend Sales_data add(const Sales&, const Sales&);
};

Friend declarations may appear only inside a class definition; they may appear anywhere in the class. Friends are not members of the class and are not affected by the access control of the section in which they are declared.

Encapsulation provides two important advantages:

  • User code cannot inadvertently corrupt the state of an encapsulated object.

  • The implementation of an encapsulated class can change over time without requiring changes in user-level code.

To make a friend visible to users of the class, we usually declare each friend in the same header as the class itself.

7.3 Additional Class Feature

7.3.1 Class Members Revisited

Defining a Type Member:

class Screen {
public:
    typedef std::string::size_type pos;
    using pos = std::string::size_type;
};

Member Functions of Class

It can ask the compiler to synthesise the default constructor's definition by use = default.

class Screen {
public:
    Screen() = default;
};

Making Members inline

Classes often have small functions that can benefit from being inlined. Member functions defined inside the class are automatically inline.

mutable Data Members

A mutable data member is never const, even when it is a member of a const object.

class Screen {
public:
    mutable size_t access_ctr;
};

Initialiser for Data Members of Class Type

Under the new standard the best way to specify this default value is as an in-class initialiser:

class Window_mgr {
private:
    std::vector<Screen> screens{Screen(24, 80, ' ')};
};

7.3.2 Function That Return *this

Functions that return a reference are lvalues, which means that they return the object itself, not a copy of the object.

inline Screen &Screen::set(char c) {
    contents[cursor] = c;
    return *this;
}

A const member function that returns *this as a reference should have a return type that is reference to const.

Overloading Based on const

We can overload a member function based on whether it is const for the same reasons that we can overload a function based on whether a pointer parameter points to const.

class Screen {
public:
    Screen &display();
    const Screen &display() const;
};

7.3.3 Class Types

The forward declaration introduces the class name Screen into the program and indicated that Screen refers to a class type. After a declaration and before a definition is seen, the type Screen is an incomplete type, which is known that Screen is a class type but not known that members that type contains.

The incomplete type is limited: we can define pointer or references to such type, and we can declare functions that use an incomplete type as a parameter or return type.

7.3.4 Friendship Revisited

A friend function can be defined inside the class body.

class Screen {
    friend class Window_mgr;
    friend void Window_mgr::clear();
};

Even if we define the function inside the class, we must still provide a declaration outside of the class itself to make that function visible.

7.4 Class Scope

The name lookup has been relatively straightforward:

  • First, look for a declaration of the name in the block in which the name was used. Only names declared before the use are considered.

  • If the name isn't found, look in the enclosing scope.

  • If no declaration is found, then the program is in error.

Member function definitions are processed after the compiler processes all of the declarations in the class.

Type Name Are Special

In a class, if a member uses a name from an outer scope and that name is a type, then the class may not subsequently redefine that name:

typedef double Money;
class Account {
private:
    typedef double Money;  // error: cannot redefine Money
};

Although it is an error to redefine a type name, compilers are not required to diagnose this error.

Normal Block-Scope Name Lookup inside Member Definitions

A name used in the body of a member function is resolved as follows:

  • First, look for a declaration of the name inside the member function. As usual, only declarations in the function body that precede the use of the name are considered.

  • If the declaration is not found inside the member function, look for a declaration inside the class. All the members of the class are considered.

  • If a declaration for the name is not found in the class, look for a declaration that is in scope before the member function definition.

void Screen::dummy_fcn() {
    this->height; // member height
    Screen::height; // member height
    height;    // member height
    ::height;    // the global height
}

7.5 Constructors Revisited

7.5.1 Constructor Initialiser List

We must use the constructor initialiser list to provide values for members that are const, reference, or of a class type that does not have a default constructor.

// explicitly initialise reference and const members
ConstRef::ConstRef(int ii): i(ii), ci(ii), ri(i) {}

By routinely using constructor initialisers, you can avoid being surprised by compile-time errors when you have a class with a member that requires a constructor initialiser.

Members are initialised in the order in which they appear in the class definition. The order of initialisation often doesn't matter. However, if one member is initialised in terms of another, then the order in which members are initialised is crucially important.

It is a good idea to write constructor initialiser in the same order as the members are declared. Moreover, when possible, avoid using members to initialise other members.

A constructor that supplies default arguments for all its parameters also defines the default constructor.

7.5.2 Delegating Constructors

A delegating constructor uses another constructor from its own class to perform its initialisation.

7.5.3 The Role of the Default Constructor

Default initialisation happens:

  • When we define non-static variable or arrays at block scope without initialisers.

  • When a class that itself has members of class type uses the synthesised default constructor.

  • When members of class type are not explicitly initialised in a constructor initialiser list.

Value initialisation happens:

  • During array initialisation when we provide fewer initialiser than the size of the array.

  • When we define a local static object without an initialiser.

  • When we explicitly request value initialisation by writing an expressions of the form T() where T is the name of a type.

7.5.4 Implicit Class-Type Conversions

The compiler will automatically apply only one class-type conversion.

We can prevent the use of a constructor in a context that requires an implicit conversion by declaring the constructor as explicit.

One context in which implicit conversions happen is when we use the copy form of initialisation. We cannot use nan explicit constructor with this form of initialisation; we must use direct initialisation:

Sales_data item1(null_book); // ok, direct initialisation
// error, cannot use the copy form of initialisation with an explicit constructor
Sales_data item2 = null_book;

Some of the library classes that we've used have single-parameter constructors:

  • The string constructor that takes a single parameter of type const char* is not explicit.

  • The vector constructor that takes a size is explicit.

7.5.5 Aggregate Classes

An aggregate class gives users direct access to its members and has special initialisation syntax. A class is an aggregate if:

  • All of its data members are public.

  • It does not define any constructors.

  • It has no in-class initialisers.

  • It has no base classer or virtual functions.

We can initialise the data members of an aggregate class by providing a braced list of member initialiser, the initialisers must appear in declaration order of the data members.

Data val1 = {0, "Anna"};

It is worth noting that there are three significant drawbacks to explicitly initialising the members of an object of class type:

  • It requires that all the data members of the class be public.

  • It puts the burden on the user of the class to correctly initialise every member of every object.

  • If a member is added or removed, all initialisations have to be updated.

7.5.6 Literal Classes

Although constructors can't be const, constructors in a literal class can be constexpr function. Indeed, a literal class must provide at least one constexpr constructor.

A constexpr constructor can be declared as = default .

We define a constexpr constructor by preceding its declaration with the keyword constexpr:

class Debug {
public:
    constexpr Debug(bool b = true): hw(b) {}
};

A constexpr constructor must initialise every data member. The initialisers must either use a constexpr constructor or be a constant expression.

A constexpr constructor is used to generate objects that are constexpr and for parameters or return types in constexpr functions:

constexpr Debug ip_sub(false, true, false);

7.6 static Class Members

The static member can be public or private. The type of a static data member can be const, reference, array, class type, and so forth.

The static members of a class exist outside any object. Objects do not contain data associated with static data members. Similarly, static member functions are not bound to any objects; they do not have a this pointer.

class Account {
public:
    static void rate(double);
};

double r;
// the static member can be accessed directly by the scope operator
r = Account::rate();

// Even though static members are not part of the objects of its class,
//  we can use an object, reference, or pointer of the class type
//  to access a static member:
Account ac1;
Account *ac2 = &ac1;
r = ac1.rate();
r = ac2->rate();

Member function can use static members directly, without the scope operator.

The static member function can be defined inside or outside of the class body. The keyword appears only with the declaration inside the class body.

The static data member must be defined initialised outside the class body.

The in-class initialisers for static members that have const integral type and must do so for static members that are constexpr of literal type. The initialisers must be constant expressions.

If the member is used only in contexts where the compiler can substitute the member's value, then an initialised const or constexpr static need not be separately defined.

If an initialiser is provided inside the class, the member's definition must not specify an initial value:

// definition of a static member with no initialiser
constexpr int Account::period;

A static data member can have incomplete type, it can have the same type as the class type of which it is a member. A non-static data member is restricted to being declared as a pointer or a reference to an object of its class.

Last updated

Was this helpful?