7.2. Access Control and Encapsulation
FundamentalAt this point, we have defined an interface for our class; but nothing forces users to use that interface. Our class is not yet encapsulated—users can reach inside a Sales_data
object and meddle with its implementation. In C++ we use access specifiers to enforce encapsulation:
- Members defined after a
public
specifier are accessible to all parts of the program. Thepublic
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. Theprivate
sections encapsulate (i.e., hide) the implementation.
Redefining Sales_data
once again, we now have
class Sales_data {
public: // access specifier added
Sales_data() = default;
Sales_data(const std::string &s, unsigned n, double p):
bookNo(s), units_sold(n), revenue(p*n) { }
Sales_data(const std::string &s): bookNo(s) { }
Sales_data(std::istream&);
std::string isbn() const { return bookNo; }
Sales_data &combine(const Sales_data&);
private: // access specifier added
double avg_price() const
{ return units_sold ? revenue/units_sold : 0; }
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
The constructors and member functions that are part of the interface (e.g., isbn
and combine
) follow the public
specifier; the data members and the functions that are part of the implementation follow the private
specifier.
A class may contain zero or more access specifiers, and there are no restrictions on how often an access specifier may appear. Each access specifier specifies the access level of the succeeding members. The specified access level remains in effect until the next access specifier or the end of the class body.
Using the class
or struct
Keyword
We also made another, more subtle, change: We used the class
keyword rather than struct
to open the class definition. This change is strictly stylistic; we can define a class type using either keyword. The only difference between struct
and class
is the default access level.
A class may define members before the first access specifier. Access to such members depends on how the class is defined. 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
.
As a matter of programming style, when we define a class intending for all of its members to be public
, we use struct
. If we intend to have private
members, then we use class
.
INFO
The only difference between using class
and using struct
to define a class is the default access level.
INFO
Exercises Section 7.2
Exercise 7.16: What, if any, are the constraints on where and how often an access specifier may appear inside a class definition? What kinds of members should be defined after a public
specifier? What kinds should be private
?
Exercise 7.17: What, if any, are the differences between using class
or struct
?
Exercise 7.18: What is encapsulation? Why is it useful?
Exercise 7.19: Indicate which members of your Person
class you would declare as public
and which you would declare as private
. Explain your choice.
7.2.1. Friends
FundamentalNow that the data members of Sales_data
are private
, our read, print
, and add
functions will no longer compile. The problem is that although these functions are part of the Sales_data
interface, they are not members of the class.
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 declarations for nonmember Sales_data operations added
friend Sales_data add(const Sales_data&, const Sales_data&);
friend std::istream &read(std::istream&, Sales_data&);
friend std::ostream &print(std::ostream&, const Sales_data&);
// other members and access specifiers as before
public:
Sales_data() = default;
Sales_data(const std::string &s, unsigned n, double p):
bookNo(s), units_sold(n), revenue(p*n) { }
Sales_data(const std::string &s): bookNo(s) { }
Sales_data(std::istream&);
std::string isbn() const { return bookNo; }
Sales_data &combine(const Sales_data&);
private:
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
// declarations for nonmember parts of the Sales_data interface
Sales_data add(const Sales_data&, const Sales_data&);
std::istream &read(std::istream&, Sales_data&);
std::ostream &print(std::ostream&, const Sales_data&);
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. We’ll have more to say about friendship in § 7.3.4 (p. 279).
TIP
Ordinarily it is a good idea to group friend declarations together at the beginning or end of the class definition.
INFO
Key Concept: Benefits of Encapsulation
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.
By defining data members as private
, the class author is free to make changes in the data. If the implementation changes, only the class code needs to be examined to see what effect the change may have. User code needs to change only when the interface changes. If the data are public
, then any code that used the old data members might be broken. It would be necessary to locate and rewrite any code that relied on the old representation before the program could be used again.
Another advantage of making data members private
is that the data are protected from mistakes that users might introduce. If there is a bug that corrupts an object’s state, the places to look for the bug are localized: Only code that is part of the implementation could be responsible for the error. The search for the mistake is limited, greatly easing the problems of maintenance and program correctness.
Although user code need not change when a class definition changes, the source files that use a class must be recompiled any time the class changes.
Declarations for Friends
TrickyA friend declaration only specifies access. It is not a general declaration of the function. If we want users of the class to be able to call a friend function, then we must also declare the function separately from the friend declaration.
To make a friend visible to users of the class, we usually declare each friend (outside the class) in the same header as the class itself. Thus, our Sales_data
header should provide separate declarations (aside from the friend declarations inside the class body) for read
, print
, and add
.
INFO
Many compilers do not enforce the rule that friend functions must be declared outside the class before they can be used.
Some compilers allow calls to a friend
function when there is no ordinary declaration for that function. Even if your compiler allows such calls, it is a good idea to provide separate declarations for friend
s. That way you won’t have to change your code if you use a compiler that enforces this rule.
INFO
Exercises Section 7.2.1
Exercise 7.20: When are friends useful? Discuss the pros and cons of using friends.
Exercise 7.21: Update your Sales_data
class to hide its implementation. The programs you’ve written to use Sales_data
operations should still continue to work. Recompile those programs with your new class definition to verify that they still work.
Exercise 7.22: Update your Person
class to hide its implementation.