Chapter 7 Classes
The ideas behind classes (abstract data types): data abstraction (defining interface and implementation) and encapsulation (separating interface and implementation).
7.1 Defining Abstract Data Types
Designing the Sales_data Class
- Some operations should not be defined as member functions, but some should. Refer to section 14.1 for details.
Defining the Revised Sales_data Class
- Member functions must be declared inside the class. They may be defined inside the class itself or outside the class body.
- Functions defined in the class are implicitly
inline. void foo(); void foo() {}is not allowed in the class body: class member cannot be redeclared (unlike ordinary functions).
- Functions defined in the class are implicitly
- With the exception of
staticmember functions (section 7.6), when we call a member function, an extra implicit parameterthisis initialized with the address of the object. That is to say, calling a member function is equivalent to calling an ordinary function defined in the class scope with the argumentthis.- Any direct use of a member in the class is assumed to be an implicit reference through
this. thisis aconstpointer.
- Any direct use of a member in the class is assumed to be an implicit reference through
- Adding
constafter the parameter list indicates thatthisis a pointer toconst: these member functions areconstmember functions. These functions can be called onconstobjects/references or pointers toconstobjects (only pointers toconstcan be bound to aconstobject).- If defined outside the class body, the
constqualifier must be repeated.
- If defined outside the class body, the
*thiscan be used to return the object itself (as a lvalue reference).
Defining Nonmember Class-Related Functions
- Ordinarily, nonmember functions that are part of the interface of a class should be declared in the same header as the class itself.
Constructors
- A constructor is run whenever an object of a class type is created. A class may have multiple overloaded constructors. They have the same name as the class and no return type.
- Constructors may not be declared as
const: the object assumes itsconstness after construction. Therefore, constructors can write toconstobjects during construction. - A default constructor is one that takes no arguments. It is used to default-initialize an object (without an initializer).
- A synthesized default constructor is defined by the compiler if the class does not explicitly define any constructors. It uses the in-class initializers if any, otherwise default-initializes each member.
- In other words, if a class requires control to initialize an object in one case, it is likely to require control in all cases.
- The synthesized default constructor may default-initialize members built-in/compound types to undefined values. They should have in-class initializers or an explicit default constructor.
- If a class has a member of a class type without a default constructor, the compiler will not generate a synthesized default constructor for that class.
- Refer to section 13.1.6 for other cases that prevent the compiler from generating a synthesized default constructor.
- Under C++11,
= defaultexplicitly asks the compiler to generate the synthesized default constructor. It can appear inside or outside the class body. - The constructor initializer list specifies initial values for data members. It appears after the parameter list and before the function body (starting with a colon).
- The initial value is wrapped in parentheses or curly braces (same semantics as value initialization).
- When a member is omitted from the constructor initializer list, it is implicitly initialized using the same process as the synthesized default constructor.
- The members are initialized before the constructor body is executed (whether or not there is a constructor initializer list).
- More to be discussed in sections 7.5 (constructors revisited), 15.7 (derived class constructors), 18.1.3 (exception handling), and Chapter 13 (copy control).
Copy, Assignment, and Destruction
- Ordinarily, if not explicitly defined, the compiler generates synthesized operations for copying, assignment, and destruction: copying/assigning/destroying each member of the object.
- They are not reliable similarly to constructors (e.g. dynamic memory, refer to section 13.1.4).
- More to be discussed in Chapter 13 (copy control).
7.2 Access Control and Encapsulation
- Access specifiers enforce encapsulation:
public: members accessible to all parts of the program.private: members accessible to member functions.
- If we use
struct, members arepublicby default. If we useclass, members areprivateby default. Always useclassif we intend to haveprivatemembers.
Friends
- A class allows another class or function to access its non-
publicmembers by making it a friend: include its declaration preceded byfriend. - Friends are not members and are not affected by access control.
- A friend declaration only specifies access. It is not a declaration of the function: in order to be called, it must be declared separately.
- Sometimes this is not enforced, but it is good practice to do so.
- We usually declare the friend function in the same header as the class.
7.3 Additional Class Features
Class Members Revisited
- A class can have local names for types via
typedeforusing. They are subject to the same access controls as any other member. They must appear before usage. - Member functions defined inside the class are automatically
inline(implicit). We can also explicitly declare a member function asinlineas part of its declaration inside the class body (explicit) or its definition outside the class body (inlineon definition).- It is legal to have
inlineon both declaration and definition, but specifying it only on the definition outside the class body is preferred. - Similar to ordinary
inlinefunctions,inlinemember functions should be defined in the same header as the class definition.
- It is legal to have
- A
mutabledata member is neverconst, even when it is a member of aconstobject or accessed in aconstmember function (via a pointer toconst). Aconstmember function may change amutablemember.
Functions That Return *this
- Returning
*this(a lvalue reference) allows chained calls to member functions. - A
constmember function that returns*thisshould have a return type that is a reference toconst:*thisis aconstobject. - Overloading can be based on
const: it virtually declares the implicitthisparameter to be a pointer toconstor not.- The overloaded versions may call the same
constprivate utility function to reuse common code: the non-constthispointer can be converted to a pointer toconst. This is useful in maintaining well-designed programs.
- The overloaded versions may call the same
Class Types
- Two classes are different even if they have exactly the same member list.
- We can refer to a class type directly with its name or use the name following the keyword
classorstruct:Sales_data item,class Sales_data item,struct Sales_data item(even if the class is defined with keywordclass). - We can declare a class without defining it (forward declaration). After the declaration, the type is an incomplete type.
- We can define pointers or references to such types, and we can declare but not define functions that use an incomplete type as a parameter or return type. A class must be defined before we create objects of that type (determine storage size) or access a member (determine its layout).
- A class cannot have data members of its own type, but it can have member that are pointers or references to its own type: the class is considered declared after its class name has been seen.
- Data members can be specified to be of a class type only if the class has been defined with one exception: static class members (section 7.6).
class T;
class A {
T t; // error: T is an incomplete type
T *pt; // ok: pointer to an incomplete type
static T st; // ok: static member of an incomplete type
};
Friendship Revisited
- By designating another class as a friend, the member functions of that class can access all the members of the class granting friendship.
- Classes and nonmember functions need not have been declared before they are used in a friend declaration. The name is implicitly assumed to be part of the surrounding scope.
- We can define the friend function inside the class body, but we must still provide a declaration outside the class itself to make that function visible - even to the class where it is defined.
struct X {
// This definition does not declare f
friend void f() { /* friend function can be defined here */ }
X() { f(); } // error: no declaration for f
void g();
void h();
};
void X::g() { return f(); } // error: f hasn't been declared
void f(); // declares the function defined inside X
void X::h() { return f(); } // ok: declaration for f is now in scope
- We may declare a single member function of another class as a friend.
- This requires careful structuring of the program to accommodate interdependencies (ordering of declarations and definitions): declaration of that class must appear before the class granting friendship.
- Overloaded functions are still different functions - friendship applies to only the specified function.
- Friendship is not transitive, reciprocal, or inherited. Each class controls its own friends.
7.4 Class Scope
- Every class defines its own new scope. This is why we must use the scope operator
::to define member functions outside the class.- Once the compiler sees the class name in out-of-class function definitions, the rest of the code (parameter list and function body) is in the scope of the class.
- Since the return type appears before the class name, the class must be specified. After that, the class must also be specified for the function name.
Window_mgr::ScreenIndex Window_mgr::add_Screen(const Screen &s) {
screens.push_back(s);
return screens.size() - 1;
}
Name Lookup and Class Scope
- The compiler processes classes in two steps - compiling member declarations first, and processing member function bodies next (after the entire class has been seen). Therefore, member functions may use other members in their bodies regardless of where they appear.
- Normal name lookup process:
- Look for a declaration in the block where the name is used. Only names declared before the use are considered.
- Look for a declaration in the enclosing scope before the block.
- Proceed to other enclosing scopes.
- The program is in error.
- Block-scope name lookup inside member definitions:
- Look for a declaration inside the member function. Only names declared before the use are considered.
- Look for a declaration inside the class. All members are considered.
- Look for a declaration in the enclosing scope before the member function definition.
- Proceed to other enclosing scopes.
- The program is in error.
- If necessary, we can use the scope operator
::to specify the scope where a name is defined:C::mindicates the member variable, and::mindicates the global variable.
int height;
class Screen {
public:
typedef std::string::size_type pos;
void f(pos height) {
cursor = width * height; // height is the parameter
cursor = width * this->height; // height is the member
cursor = width * Screen::height; // height is the member
cursor = width * ::height; // height is the global variable
}
void set(pos);
private:
pos cursor = 0;
pos height = 0, width = 0;
};
Screen::pos verify(Screen::pos);
void Screen::set(pos ht) {
height = verify(ht); // ok: verify() appears before it is used
}
class Account {
Money f2() { return 0; } // error: Money not defined yet
void f3() { Money m = 0; } // ok: Money used in definition
typedef double Money;
Money f1() { return 0; } // ok: Money defined before use
};
- Type names are special: if a member uses a type name from an outer scope, then the class may not subsequently redefine that name (even with the same definition).
- The compiler may quietly accept such code even though the program is in error.
- The best practice is to put definitions of type names at the beginning of a class.
typedef double Money;
class Account {
Money f() { return 0; } // Money from the outer scope
typedef double Money; // error: redefinition of Money
};
7.5 Constructors Revisited
Constructor Initializer List
- Members are initialized in the order in which they appear in the class definition. The order of initialization or whether they appear in the constructor initializer list does not matter.
- In other words, the compiler initializes members in the order of their declarations: use the value from the constructor initializer list if specified; otherwise, use the in-class initializer if specified; otherwise, default-initialize.
- This may lead to problems if one member depends on another for initialization. The compiler may or may not issue a warning. Therefore, it is a good idea to write constructor initializers in the same order as the member declarations, and to avoid using members to initialize other members.
- Some members must be initialized and not assigned in the constructor body:
const, reference, or of a class type without a default constructor. - A constructor that supplies default arguments for all its parameters also defines the default constructor.
Delegating Constructors
- A delegating constructor uses another constructor from the class to perform initialization. The constructor initializer list must have a single entry: the name of the class itself, and its arguments match another constructor.
- The constructor initializer list and the function body of the delegated-to constructor are both executed before returning to the function body of the delegating constructor.
The Role of the Default Constructor
- The default constructor is used both for default initialization and value initialization.
- Default initialization happens:
- When we define non-
staticvariables or arrays at block scope without initializers, they are default-initialized. - When we define a class with class-type members, and the synthesized default constructor is used, then each member is default-initialized (if no in-class initializer is specified).
- When we define a class with class-type members, and we define a default constructor, then each member not in the constructor initializer list is default-initialized (if no in-class initializer is specified).
- When we define non-
- Value initialization happens:
- When we provide fewer initializers than the size of the array, the remaining elements are value-initialized.
- When we define a local
staticobject without an initializer, it is value-initialized. - When we explicitly request value initialization:
T(),T obj{},T{}. std::allocator<T>::constructalso uses value-initialization (refer to section 12.2 for details), which is widely used in the standard library (std::vector, etc.).
T obj()defines a function instead of an object. UseT objinstead for default initialization.
Implicit Class-Type Conversions
- Every constructor that can be called with a single argument defines an implicit conversion to the class type. They are called converting constructors.
- Implicit conversions work well with function calls with reference-to-
constparameters: conversion creates a temporary object, which can bind to a reference toconst. std::stringhas a converting constructor that takes aconst char *argument - this is why they can be used interchangeably in many situations.
- Implicit conversions work well with function calls with reference-to-
- Only one class-type conversion is allowed.
std::string null_book = "9-999-99999-9"; // implicit conversion: const char * -> string
item.combine(null_book); // implicit conversion: string -> Sales_data
item.combine("9-999-99999-9"); // error: two conversions required
item.combine(string("9-999-99999-9")); // ok: explicit conversion: string -> Sales_data
item.combine(Sales_data("9-999-99999-9")); // ok: explicit conversion: const char * -> string
- Class-type conversions may be undesirable. We can prevent the use of a constructor for implicit conversions by declaring it as
explicit(not added on definition).explicitis meaningful only for constructors that can be called with a single argument.
- Implicit conversions happen when we perform copy initialization
T t = a. Direct initializationT t(a)directly calls the constructor, soexplicitconstructors still can be used for direct initialization. - We can use
static_castto explicitly force a conversion even if the constructor isexplicit:static_cast<T>(a)is equivalent toT(a)(cast to a temporary object).
Aggregate Classes
- A class is an aggregate class if:
- All of its data members are
public. - It has no user-defined constructors.
- It has no in-class initializers.
- It has no base classes or virtual functions.
- All of its data members are
- Data members of an aggregate class can be initialized with a braced list of member initializers. Trailing members are value-initialized if fewer initializers are provided than members.
- Inherited from C, the initializers can also be in the format of
.{member} = value. This allows initializing members in a different order.
struct Data {
int i;
std::string s;
};
Data d1 = { 0, "Anna" };
Data d2 = { .s = "Anna", .i = 0 };
Data d3 = { 0 }; // s is value-initialized to ""
Literal Classes
- Only literal classes may have
constexprmember functions. They are implicitlyconst(no longer true in C++14). - An aggregate class is a literal class if its data members are all of literal type.
- A non-aggregate class is a literal class if:
- The data members are all of literal type.
- It has at least one
constexprconstructor. - If a data member has an in-class initializer, it must be a constant expression for built-in types or use a
constexprconstructor for class types. - The class has the default destructor.
- A
constexprconstructor can either be declared as= defaultor have an empty body. It must initialize every data member with a constant expression or using aconstexprconstructor.
class A { // A is a literal class
public:
// constructors: default or with an empty body
constexpr A() = default;
constexpr A(int w, int h) : width(w), height(h) {}
// destructor: default (synthesized)
// literal classes may have constexpr member functions
constexpr int area() const { return width * height; }
private:
// literal-type members: constant expression initializers
int width = 1, height = 1;
};
constexpr A a;
constexpr int area = a.area();
7.6 static Class Members
staticmember functions are not bound to any object; they do not have athispointer. Therefore, they cannot be declared asconst. Also, explicit and implicit uses ofthisare not allowed, including using a non-staticmember.staticmembers can be accessed in the following ways:- Directly through the scope operator:
T::m. - Through an object, reference, or pointer:
t.m,pt->m. - Inside member functions, use directly by name.
- Directly through the scope operator:
staticmember functions can be defined inside or outside the class body:staticis not repeated on definition outside the class body.- Non-
conststaticdata members should be defined and initialized outside the class body.- This is different from ordinary data members: although in the same form (
T m = value), ordinary data members need in-class initializers used when constructing an object, whilestaticdata members need to be initialized only once. If initialized inside the class body, they may be included in multiple files, leading to multiple definitions. staticmember functions may be used to initializestaticdata members. No qualifier is needed on the function name - it is in the scope of the class.- The best practice is to define and initialize
staticdata members in the same file as ordinary non-inline member functions.
- This is different from ordinary data members: although in the same form (
constintegralstaticdata members may have in-class initializers, andconstexprstaticdata members must have in-class initializers. The initializers must be constant expressions.- They can be used immediately after definition - even inside the class itself.
- Even if a
const/constexprstaticdata member is initialized in the class body, it should still be defined outside the class body if it is used in a context where the value cannot be substituted with the constant expression. The out-of-class definition does not specify an initial value.
// Example: in-class initializers
class A {
static const int a; // ok
static const int b = 0; // ok: const integral may have an initializer
static constexpr int c; // error: constexpr must have an initializer
static constexpr int d = 0; // ok
};
const int A::a = 0;
// Example 1: no out-of-class definition needed
class Account {
static constexpr int period = 30;
double daily_tbl[period]; // the only use of period: no extra definition needed
};
// Example 2: out-of-class definition needed
class Account2 {
static constexpr int period = 30;
};
constexpr int Account2::period; // definition outside the class body
void func(const int &);
func(Account2::period); // period cannot be substituted: definition is needed
- A
staticdata member can have incomplete type. In particular, it can be an instance of the class itself. - A
staticdata member can be used as a default argument. Using a non-staticmember as a default argument provides no object from which to obtain the value and so is an error.
class Screen {
public:
Screen &clear(char = bg);
private:
static const char bg;
};