C++ Primer 第七章 类
- 7.1. Defining Abstract Data Types
- Data abstraction and Encapsulation
- Difference between abstraction and encapsulation
- Defining Member Functions
- Introducing this
- Introducing const Member Functions
- Class Scope and Member Functions
- Defining a Member Function outside the Class
- Defining a Function to Return “This” Object
- Defining Nonmember Class-Related Functions
- Constructors
- The Synthesized Default Constructor
- Some Classes Cannot Rely on the Synthesized Default Constructor
- Defining the Sales_data Constructors
- What = default Means
- Constructor Initializer List
- C++11 List Initialization
- Defining a Constructor outside the Class Body
- Constructor Initializer List
- Constructor Initializers Are Sometimes Required
- Order of Member Initialization
- Delegating Constructors
- The Role of the Default Constructor
- Using the Default Constructor
- Implicit Class-Type Conversions
- Only One Class-Type Conversion Is Allowed
- Class-Type Conversions Are Not Always Useful
- Suppressing Implicit Conversions Defined by Constructors
- explicit Constructors Can Be Used Only for Direct Initialization
- Explicitly Using Constructors for Conversions
- Library Classes with explicit Constructors
- 7.2. Access Control and Encapsulation
- 7.3. Additional Class Features
- 7.4. Class Scope
- 7.5. Constructors Revisited
- 7.6. static Class Members
7.1. Defining Abstract Data Types
Data abstraction and Encapsulation
The fundamental ideas behind classes are data abstraction and encapsulation.
Data abstraction is a programming (and design) 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’ 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’ 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.
A class that uses data abstraction and encapsulation defines an abstract data type.
Difference between abstraction and encapsulation
参考:Abstraction, Encapsulation, and Information Hiding
If encapsulation was “the same thing as information hiding,” then one might make the argument that “everything that was encapsulated was also hidden.” This is obviously not true. For example, even though information may be encapsulated within record structures and arrays, this information is usually not hidden (unless hidden via some other mechanism).
Another example of encapsulated, but not hidden, information is the
(highly undesirable) “block of global information” technique reminiscent of FORTRAN’s common blocks. Unfortunately, it is quite easy in some object-oriented languages to create blocks of global data in the form of classes. Specifically, it is possible to create classes with nothing but constants and variables in their public interfaces, i.e., there are no operations in the interface. (For reasons why this is undesirable, see discussions of “module coupling,” e.g., [Myers,1978] and [Yourdon and Constantine, 1979].)
It is indeed true that encapsulation mechanisms such as classes allow
some information to be hidden. However, these same encapsulation mechanisms also allow some information to be visible. Some even allow
varying degrees of visibility, e.g., C++'s public, protected, and private members.
One could argue that abstraction is a technique that helps us identify which specific information should be visible, and which information should be hidden. Encapsulation is then the technique for packaging the information in such a way as to hide what should be hidden, and make visible what is intended to be visible.
Defining Member Functions
Member functions must be declared inside the class.
Member functions may be defined inside the class itself or outside the class body.
Nonmember functions that are part of the interface, such as add, read, and print, are declared and defined outside the class.
Functions defined in the class are implicitly inline.
Introducing this
Because this is intended to always refer to “this” object, this is a const pointer. We cannot change the address that this holds.
Introducing const Member Functions
std::string isbn() const { return bookNo; }
The purpose of that const is to modify the type of the implicit this pointer.
By default, the type of this is a const pointer to the nonconst version of the class type.
Although this is implicit, it follows the normal initialization rules, which means that (by default) we cannot bind this to a const object.
This fact, in turn, means that we cannot call an ordinary member function on a const object.
A const following the parameter list indicates that this is a pointer to const.
Member functions that use const in this way are const member functions.
The fact that this is a pointer to const means that const member functions cannot change the object on which they are called.
Class Scope and Member Functions
The compiler processes classes in two steps—the member declarations are compiled first, after which the member function bodies, if any, are processed.
Thus, member function bodies may use other members of their class regardless of where in the class those members appear.
Defining a Member Function outside the Class
double Sales_data::avg_price() const {
if (units_sold)
return revenue/units_sold;
else
return 0;
}
Defining a Function to Return “This” Object
Sales_data& Sales_data::combine(const Sales_data &rhs)
{
units_sold += rhs.units_sold; // add the members of rhs into
revenue += rhs.revenue; // the members of ''this'' object
return *this; // return the object on which the function was called
}
The built-in assignment operators return their left-hand operand as an lvalue.
To return an lvalue, our combine function must return a reference.
Because the left-hand operand is a Sales_data object, the return type is Sales_data&.
Defining Nonmember Class-Related Functions
Functions that are conceptually part of a class, but not defined inside the class, are typically declared (but not defined) in the same header as the class itself.
// input transactions contain ISBN, number of copies sold, and sales price
istream &read(istream &is, Sales_data &item)
{
double price = 0;
is >> item.bookNo >> item.units_sold >> price;
item.revenue = price * item.units_sold;
return is;
}
ostream &print(ostream &os, const Sales_data &item)
{
os << item.isbn() << " " << item.units_sold << " "
<< item.revenue << " " << item.avg_price();
return os;
}
The IO classes are types that cannot be copied, so we may only pass them by reference.
Constructors
-
The job of a constructor is to initialize the data members of a class object.
-
A constructor is run whenever an object of a class type is created.
-
Constructors have the same name as the class.
-
Unlike other functions, constructors have no return type.
-
Like other functions, constructors have a (possibly empty) parameter list and a (possibly empty) function body.
-
A class can have multiple constructors.
-
Like any other overloaded function, the constructors must differ from each other in the number or types of their parameters.
-
Unlike other member functions, constructors may not be declared as const.
-
When we create a const object of a class type, the object does not assume its “constness” until after the constructor completes the object’s initialization.
Thus, constructors can write to const objects during their construction.
The Synthesized 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 synthesized default constructor.
Some Classes Cannot Rely on the Synthesized Default Constructor
-
The compiler generates the default for us only if we do not define any other constructors for the class.
-
For some classes, the synthesized default constructor does the wrong thing.
- Objects of built-in or compound type (such as arrays and pointers) that are defined inside a block have undefined value when they are default initialized.
-
Sometimes the compiler is unable to synthesize one.
For example, if a class has a member that has a class type, and that class doesn’t have a default constructor, then the compiler can’t initialize that member.
Defining the Sales_data Constructors
struct Sales_data {
// constructors added
Sales_data() = default;
Sales_data(const std::string &s): bookNo(s) { }
Sales_data(const std::string &s, unsigned n, double p):
bookNo(s), units_sold(n), revenue(p*n) { }
Sales_data(std::istream &);
// other members as before
std::string isbn() const { return bookNo; }
Sales_data& combine(const Sales_data&);
double avg_price() const;
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
What = default Means
Sales_data() = default;
First, note that this constructor defines the default constructor because it takes no arguments.
We are defining this constructor only because we want to provide other constructors as well as the default constructor.
If we want the default behavior, we can ask the compiler to generate the constructor for us by writing = default after the parameter list.
Constructor Initializer List
Sales_data(const std::string &s): bookNo(s) { }
Sales_data(const std::string &s, unsigned n, double p):
bookNo(s), units_sold(n), revenue(p*n) { }
The constructor initializer is a list of member names, each of which is followed by that member’s initial value in parentheses (or inside curly braces).
When a member is omitted from the constructor initializer list, it is implicitly initialized using the same process as is used by the synthesized default constructor.
C++11 List Initialization
With C++11, you can use the list-initialization syntax with classes:
Stock hot_tip = {"Derivatives Plus Plus", 100, 45.0};
Stock jock {"Sport Age Storage, Inc"};
Stock temp {};
The braced lists in the first two declarations match the following constructor:
Stock::Stock(const std::string & co, long n = 0, double pr = 0.0);
In addition, C++11 offers a class called std::initializer_list that can be used as a
type for a function or method parameter.
Defining a Constructor outside the Class Body
Sales_data::Sales_data(std::istream &is)
{
read(is, *this); // read will read a transaction from is into this object
}
In this constructor there is no constructor initializer list, although technically speaking, it would be more correct to say that the constructor initializer list is empty.
Even though the constructor initializer list is empty, the members of this object are still initialized before the constructor body is executed.
Constructor Initializer List
When we define variables, we typically initialize them immediately rather than defining them and then assigning to them:
string foo = "Hello World!"; // define and initialize
string bar; // default initialized to the empty string
bar = "Hello World!"; // assign a new value to bar
If we do not explicitly initialize a member in the constructor initializer list, that member is default initialized before the constructor body starts executing.
Constructor Initializers Are Sometimes Required
Members that are const or references must be initialized.
Similarly, members that are of a class type that does not define a default constructor also must be initialized.
class ConstRef {
public:
ConstRef(int ii);
private:
int i;
const int ci;
int &ri;
};
Like any other const object or reference, the members ci and ri must be initialized.
// error: ci and ri must be initialized
ConstRef::ConstRef(int ii)
{ // assignments:
i = ii; // ok
ci = ii; // error: cannot assign to a const
ri = i; // error: ri was never initialized
}
By the time the body of the constructor begins executing, initialization is complete.
// ok: explicitly initialize reference and const members
ConstRef::ConstRef(int ii): i(ii), ci(ii), ri(i) { }
Order of Member Initialization
-
Members are initialized in the order in which they appear in the class definition: The first member is initialized first, then the next, and so on.
-
The order in which initializers appear in the constructor initializer list does not change the order of initialization.
class X {
int i;
int j;
public:
// undefined: i is initialized before j
X(int val): j(val), i(j) { }
};
When possible, avoid using members to initialize other members.
Delegating Constructors
A delegating constructor uses another constructor from its own class to perform its initialization.
In a delegating constructor, the member initializer list has a single entry that is the name of the class itself.
Like other member initializers, the name of the class is followed by a parenthesized list of arguments.
The argument list must match another constructor in the class.
class Sales_data {
public:
// nondelegating constructor initializes members from corresponding arguments
Sales_data(std::string s, unsigned cnt, double price):
bookNo(s), units_sold(cnt), revenue(cnt*price) {
}
// remaining constructors all delegate to another constructor
Sales_data(): Sales_data("", 0, 0) {}
Sales_data(std::string s): Sales_data(s, 0,0) {}
Sales_data(std::istream &is): Sales_data()
{ read(is, *this); }
// other members as before
};
The Role of the Default Constructor
Default initialization happens:
-
When we define nonstatic variables or arrays at block scope without initializers.
-
When a class that itself has members of class type uses the synthesized default constructor.
-
When members of class type are not explicitly initialized in a constructor initializer list.
Value initialization happens:
-
During array initialization when we provide fewer initializers than the size of the array.
-
When we define a local static object without an initializer.
-
When we explicitly request value initialization by writing an expressions of the form T() where T is the name of a type (The vector constructor that takes a single argument to specify the vector’s size uses an argument of this kind to value initialize its element initializer.)
Using the Default Constructor
Sales_data obj(); // oops! declares a function, not an object
Sales_data obj2; // ok: obj2 is an object, not a function
Implicit Class-Type Conversions
Every constructor that can be called with a single argument defines an implicit conversion to a class type.
Such constructors are sometimes referred to as converting constructors.
struct Sales_data {
// constructors added
Sales_data() = default;
Sales_data(const std::string &s): bookNo(s) { }
Sales_data(const std::string &s, unsigned n, double p):
bookNo(s), units_sold(n), revenue(p*n) { }
Sales_data(std::istream &);
// other members as before
std::string isbn() const { return bookNo; }
Sales_data& combine(const Sales_data&);
double avg_price() const;
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
Sales_data item;
string null_book = "9-999-99999-9";
// constructs a temporary Sales_data object
// with units_sold and revenue equal to 0 and bookNo equal to null_book
item.combine(null_book);
Only One Class-Type Conversion Is Allowed
The compiler will automatically apply only one class-type conversion.
For example, the following code is in error because it implicitly uses two conversions:
// error: requires two user-defined conversions:
// (1) convert "9-999-99999-9" to string
// (2) convert that (temporary) string to Sales_data
item.combine("9-999-99999-9");
If we wanted to make this call, we can do so by explicitly converting the character string to either a string or a Sales_data object:
// ok: explicit conversion to string, implicit conversion to Sales_data
item.combine(string("9-999-99999-9"));
// ok: implicit conversion to string, explicit conversion to Sales_data
item.combine(Sales_data("9-999-99999-9"));
Class-Type Conversions Are Not Always Useful
// uses the istream constructor to build an object to pass to combine
item.combine(cin);
This code implicitly converts cin to Sales_data.
This conversion executes the Sales_data constructor that takes an istream. That constructor creates a (temporary) Sales_data object by reading the standard input. That object is then passed to combine.
This Sales_data object is a temporary. We have no access to it once combine finishes. Effectively, we have constructed an object that is discarded after we add its value into item.
Suppressing Implicit Conversions Defined by Constructors
We can prevent the use of a constructor in a context that requires an implicit conversion by declaring the constructor as explicit:
class Sales_data {
public:
Sales_data() = default;
Sales_data(const std::string &s, unsigned n, double p):
bookNo(s), units_sold(n), revenue(p*n) { }
explicit Sales_data(const std::string &s): bookNo(s) { }
explicit Sales_data(std::istream&);
// remaining members as before
};
item.combine(null_book); // error: string constructor is explicit
item.combine(cin); // error: istream constructor is explicit
The explicit keyword is meaningful only on constructors that can be called with a single argument.
Constructors that require more arguments are not used to perform an implicit conversion, so there is no need to designate such constructors as explicit.
The explicit keyword is used only on the constructor declaration inside the class. It is not repeated on a definition made outside the class body:
// error: explicit allowed only on a constructor declaration in a class header
explicit Sales_data::Sales_data(istream& is)
{
read(is, *this);
}
explicit Constructors Can Be Used Only for Direct Initialization
One context in which implicit conversions happen is when we use the copy form of initialization (with an =).
We cannot use an explicit constructor with this form of initialization; we must use direct initialization:
Sales_data item1 (null_book); // ok: direct initialization
// error: cannot use the copy form of initialization
// with an explicit constructor
Sales_data item2 = null_book;
Explicitly Using Constructors for Conversions
Although the compiler will not use an explicit constructor for an implicit conversion, we can use such constructors explicitly to force a conversion:
// ok: the argument is an explicitly constructed Sales_data object
item.combine(Sales_data(null_book));
// ok: static_cast can use an explicit constructor
item.combine(static_cast<Sales_data>(cin));
Library Classes with explicit Constructors
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.2. Access Control and Encapsulation
7.2.1. Friends
Introduction
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.
Declarations for Friends
A 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.
Reasons for using friends
Friends come in three varieties:
- Friend functions
- Friend classes
- Friend member functions
Often, overloading a binary operator for a class generates a need for friends.
// mytime2.h -- Time class after operator overloading
#ifndef MYTIME2_H_
#define MYTIME2_H_
class Time
{
private:
int hours;
int minutes;
public:
Time();
Time(int h, int m = 0);
Time operator*(double n) const;
void Show() const;
};
#endif
// mytime2.cpp -- implementing Time methods
#include <iostream>
#include "mytime2.h"
Time::Time()
{
hours = minutes = 0;
}
Time::Time(int h, int m )
{
hours = h;
minutes = m;
}
Time Time::operator*(double mult) const
{
Time result;
long totalminutes = hours * mult * 60 + minutes * mult;
result.hours = totalminutes / 60;
result.minutes = totalminutes % 60;
return result;
}
void Time::Show() const
{
std::cout << hours << " hours, " << minutes << " minutes";
}
The multiplication operator combines a Time value with a double value.This restricts how the operator can be used. Remember, the left operand is the invoking object. That is:
A = B * 2.75;
translates to the following member function call:
A = B.operator*(2.75);
But what about the following statement?
A = 2.75 * B; // cannot correspond to a member function
Conceptually, 2.75 * B should be the same as B * 2.75, but the first expression cannot correspond to a member function because 2.75 is not a type Time object.
One way around this difficulty is to use a nonmember function. (Remember, most operators can be overloaded using either member or nonmember functions.) A nonmember function is not invoked by an object; instead, any values it uses, including objects, are explicit arguments.Thus, the compiler could match the expression
A = 2.75 * B; // cannot correspond to a member function
to the following nonmember function call:
A = operator*(2.75, B);
The function would have this prototype:
Time operator*(double m, const Time & t);
Using a nonmember function solves the problem of getting the operands in the desired order (first double and then Time), but it raises a new problem: Nonmember functions can’t directly access private data in a class.
But there is a special category of nonmember functions, called friends, that can access private members of a class.
Creating Friends
- Placing a prototype in the class declaration and prefix the declaration with the keyword friend:
// goes in class declaration
friend Time operator*(double m, const Time & t);
This prototype has two implications:
- Although the operator()* function is declared in the class declaration, it is not a member function. So it isn’t invoked by using the membership operator.
- Although the operator()* function is not a member function, it has the same access rights as a member function.
- Writing the function definition. Because it is not a member function, you don’t use the
Time::qualifier. Also you don’t use thefriendkeyword in the definition.
Are Friends Unfaithful to OOP?
You should think of friend functions as part of an extended interface for a class.
By using both a friend function and a class method, you can express either operation with the same user interface.
Only a class declaration can decide which functions are friends, so the class declaration still controls which functions access private data.
In short, class methods and friends are simply two different mechanisms for expressing a class interface.
Actually, you can write this particular friend function as a non-friend by altering the definition so that it switches which value comes first in the multiplication:
Time operator*(double m, const Time & t)
{
return t * m; // use t.operator*(m)
}
The original version accessed t.minutes and t.hours explicitly, so it had to be a friend.
This version only uses the Time object t as a whole, letting a member function handle the private values, so this version doesn’t have to be a friend.
Nonetheless, there are reasons to make this version a friend, too:
-
It ties the function in as part of the official class interface.
-
If you later find a need for the function to access private data directly, you only have to change the function definition and not the class prototype.
friend class
Any method of the friend class can access private and protected members of the original class.
Also you can be more restrictive and designate just particular member functions of a class to be friends to another class.
// tv.h -- Tv and Remote classes
#ifndef TV_H_
#define TV_H_
class Tv
{
public:
friend class Remote; // Remote can access Tv private parts
enum {Off, On};
enum {MinVal,MaxVal = 20};
enum {Antenna, Cable};
enum {TV, DVD};
Tv(int s = Off, int mc = 125) : state(s), volume(5),
maxchannel(mc), channel(2), mode(Cable), input(TV) {}
void onoff() {state = (state == On)? Off : On;}
bool ison() const {return state == On;}
bool volup();
bool voldown();
void chanup();
void chandown();
void set_mode() {mode = (mode == Antenna)? Cable : Antenna;}
void set_input() {input = (input == TV)? DVD : TV;}
void settings() const; // display all settings
private:
int state; // on or off
int volume; // assumed to be digitized
int maxchannel; // maximum number of channels
int channel; // current channel setting
int mode; // broadcast or cable
int input; // TV or DVD
};
class Remote
{
private:
int mode; // controls TV or DVD
public:
Remote(int m = Tv::TV) : mode(m) {}
bool volup(Tv & t) { return t.volup();}
bool voldown(Tv & t) { return t.voldown();}
void onoff(Tv & t) { t.onoff(); }
void chanup(Tv & t) {t.chanup();}
void chandown(Tv & t) {t.chandown();}
void set_chan(Tv & t, int c) {t.channel = c;}
void set_mode(Tv & t) {t.set_mode();}
void set_input(Tv & t) {t.set_input();}
};
#endif
// tv.cpp -- methods for the Tv class (Remote methods are inline)
#include <iostream>
#include "tv.h"
bool Tv::volup()
{
if (volume < MaxVal)
{
volume++;
return true;
}
else
return false;
}
bool Tv::voldown()
{
if (volume > MinVal)
{
volume--;
return true;
}
else
return false;
}
void Tv::chanup()
{
if (channel < maxchannel)
channel++;
else
channel = 1;
}
void Tv::chandown()
{
if (channel > 1)
channel--;
else
channel = maxchannel;
}
void Tv::settings() const
{
using std::cout;
using std::endl;
cout << "TV is " << (state == Off? "Off" : "On") << endl;
if (state == On)
{
cout << "Volume setting = " << volume << endl;
cout << "Channel setting = " << channel << endl;
cout << "Mode = "
<< (mode == Antenna? "antenna" : "cable") << endl;
out << "Input = "
<< (input == TV? "TV" : "DVD") << endl;
}
}
//use_tv.cpp -- using the Tv and Remote classes
#include <iostream>
#include "tv.h"
int main()
{
using std::cout;
Tv s42;
cout << "Initial settings for 42\" TV:\n";
s42.settings();
s42.onoff();
s42.chanup();
cout << "\nAdjusted settings for 42\" TV:\n";
s42.settings();
Remote grey;
grey.set_chan(s42, 10);
grey.volup(s42);
grey.volup(s42);
cout << "\n42\" settings after using remote:\n";
s42.settings();
Tv s58(Tv::On);
s58.set_mode();
grey.set_chan(s58,28);
cout << "\n58\" settings:\n";
s58.settings();
return 0;
}
Friend Member Functions
You do have the option of making just selected class members friends to another class rather than making the entire class a friend.
You need to be careful about the order in which you arrange the various declarations and definitions.
The way to make Remote::set_chan() a friend to the Tv class is to declare it as a friend in the Tv class declaration:
class Tv
{
friend void Remote::set_chan(Tv & t, int c);
...
};
However, for the compiler to process this statement, it needs to have already seen the Remote definition. Otherwise, it won’t know that Remote is a class and that set_chan() is a method of that class. This suggests putting the Remote definition above the Tv definition. But the fact that Remote methods mention Tv objects means that the Tv definition should appear above the Remote definition. Part of the way around the circular dependence is to use a forward declaration. To do so, you insert the following statement above the Remote definition:
class Tv; // forward declaration
class Remote { ... }; // Tv-using methods as prototypes only
class Tv { ... }; // put Remote method definitions here
The Remote prototypes look like this:
void onoff(Tv & t);
All the compiler needs to know when inspecting this prototype is that Tv is a class, and the forward declaration supplies that information.
// tvfm.h -- Tv and Remote classes using a friend member
#ifndef TVFM_H_
#define TVFM_H_
class Tv; // forward declaration
class Remote
{
public:
enum State{Off, On};
enum {MinVal,MaxVal = 20};
enum {Antenna, Cable};
enum {TV, DVD};
private:
int mode;
public:
Remote(int m = TV) : mode(m) {}
bool volup(Tv & t); // prototype only
bool voldown(Tv & t);
void onoff(Tv & t) ;
void chanup(Tv & t) ;
void chandown(Tv & t) ;
void set_mode(Tv & t) ;
void set_input(Tv & t);
void set_chan(Tv & t, int c);
};
class Tv
{
public:
friend void Remote::set_chan(Tv & t, int c);
enum State{Off, On};
enum {MinVal,MaxVal = 20};
enum {Antenna, Cable};
enum {TV, DVD};
Tv(int s = Off, int mc = 125) : state(s), volume(5),
maxchannel(mc), channel(2), mode(Cable), input(TV) {}
void onoff() {state = (state == On)? Off : On;}
bool ison() const {return state == On;}
bool volup();
bool voldown();
void chanup();
void chandown();
void set_mode() {mode = (mode == Antenna)? Cable : Antenna;}
void set_input() {input = (input == TV)? DVD : TV;}
void settings() const;
private:
int state;
int volume;
int maxchannel;
int channel;
int mode;
int input;
};
// Remote methods as inline functions
inline bool Remote::volup(Tv & t) { return t.volup();}
inline bool Remote::voldown(Tv & t) { return t.voldown();}
inline void Remote::onoff(Tv & t) { t.onoff(); }
inline void Remote::chanup(Tv & t) {t.chanup();}
inline void Remote::chandown(Tv & t) {t.chandown();}
inline void Remote::set_mode(Tv & t) {t.set_mode();}
inline void Remote::set_input(Tv & t) {t.set_input();}
inline void Remote::set_chan(Tv & t, int c) {t.channel = c;}
#endif
7.3. Additional Class Features
Class Declarations
mutable Data Members
A mutable data member is never const, even when it is a member of a const object.
class Screen {
public:
void some_member() const;
private:
mutable size_t access_ctr; // may change even in a const object
// other members as before
};
void Screen::some_member() const
{
++access_ctr; // keep a count of the calls to any member function
// whatever other work this member needs to do
}
Despite the fact that some_member is a const member function, it can change the value of access_ctr.
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.
The nonconst version will not be viable for const objects;
we can only call const member functions on a const object. We can call either version on a nonconst object, but the nonconst version will be a better match.
7.4. Class Scope
7.4.1. Name Lookup and Class Scope
name lookup
Class definitions are processed in two phases:
- First, the member declarations are compiled.
- Function bodies are compiled only after the entire class has been seen.
Because member function bodies are not processed until the entire class is seen, they can use any name defined inside the class.
Names used in declarations, including names used for the return type and types in the parameter list, must be seen before they are used.
If a member declaration uses a name that has not yet been seen inside the class, the compiler will look for that name in the scope(s) in which the class is defined. For example:
typedef double Money;
string bal;
class Account {
public:
Money balance() { return bal;}
private:
Money bal;
// ...
};
When the compiler sees the declaration of the balance function, it will look for a declaration of Money in the Account class. The compiler considers only declarations inside Account that appear before the use of Money.
Because no matching member is found, the compiler then looks for a declaration in the enclosing scope(s). In this example, the compiler will find the typedef of Money. That type will be used for the return type of the function balance and as the type for the data member bal.
On the other hand, the function body of balance is processed only after the entire class is seen. Thus, the return inside that function returns the member named bal, not the string from the outer scope.
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.
/*
note: this code is for illustration purposes only
and reflects bad practice
it is generally a bad idea to use the same name
for a parameter and a member
*/
int height; // defines a name subsequently used inside Screen
class Screen {
public:
typedef std::string::size_type pos;
void dummy_fcn(pos height) {
cursor = width * height; // which height? the parameter
}
private:
pos cursor = 0;
pos height = 0, width = 0;
};
In this case, the height parameter hides the member named height. If we wanted to override the normal lookup rules, we can do so:
// bad practice: names local to member functions
// shouldn't hide member names
void Screen::dummy_fcn(pos height) {
cursor = width * this->height; // member height
// alternative way to indicate the member
cursor = width * Screen::height; // member height
}
After Class Scope, Look in the Surrounding Scope
If the compiler doesn’t find the name in function or class scope, it looks for the name in the surrounding scope.
If we want the name height from the outer scope, we can ask for it explicitly using the scope operator:
// bad practice: don't hide names
// that are needed from surrounding scopes
void Screen::dummy_fcn(pos height) {
cursor = width * ::height;// which height? the global one
}
Even though the outer object is hidden, it is still possible to access that object by using the scope operator.
Names Are Resolved Where They Appear within a File
When a member is defined outside its class, the third step of name lookup includes names declared in the scope of the member definition as well as those that appear in the scope of the class definition. For example:
Type Names Are Special
Ordinarily, an inner scope can redefine a name from an outer scope even if that name has already been used in the inner scope.
However, 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 {
public:
Money balance() { return bal; } // uses Money from the outer scope
private:
typedef double Money; // error: cannot redefine Money
Money bal;
// ...
};
Definitions of type names usually should appear at the beginning of a class.
That way any member that uses that type will be seen after the type name
has already been defined.
7.5. Constructors Revisited
7.5.5. Aggregate Classes
An aggregate class gives users direct access to its members and has special initialization 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 initializers
- It has no base classes or virtual functions, which are class-related features.
For example, the following class is an aggregate:
struct Data {
int ival;
string s;
};
We can initialize the data members of an aggregate class by providing a braced list of member initializers:
// val1.ival = 0; val1.s = string("Anna")
Data val1 = { 0, "Anna" };
As with initialization of array elements, if the list of initializers has fewer elements than the class has members, the trailing members are value initialized.
7.5.6. Literal Classes
-
Unlike other classes, classes that are literal types may have function members that are constexpr.
-
Such members must meet all the requirements of a constexpr function. These member functions are implicitly const.
-
An aggregate class whose data members are all of literal type is a literal class.
-
A nonaggregate class, that meets the following restrictions, is also a literal class:
- The data members all must have literal type.
- The class must have at least one constexpr constructor.
- If a data member has an in-class initializer, the initializer for a member of built-in type must be a constant expression, or if the member has class type, the initializer must use the member’s own constexpr constructor.
- The class must use default definition for its destructor, which is the member that destroys objects of the class type.
constexpr Constructors
Although constructors can’t be const, constructors in a literal class can be constexpr functions.
A constexpr constructor can be declared as = default (or as a deleted function).
A constexpr constructor must meet the requirements of a constructor—meaning it can have no return statement—and of a constexpr function—meaning the only executable statement it can have is a return statement.
As a result, the body of a constexpr constructor is typically empty.
We define a constexpr constructor by preceding its declaration with the keyword constexpr:
class Debug {
public:
constexpr Debug(bool b = true): hw(b), io(b), other(b) {
}
constexpr Debug(bool h, bool i, bool o):
hw(h), io(i), other(o) {
}
constexpr bool any() { return hw || io || other; }
void set_io(bool b) { io = b; }
void set_hw(bool b) { hw = b; }
void set_other(bool b) { hw = b; }
private:
bool hw; // hardware errors other than IO errors
bool io; // IO errors
bool other; // other errors
};
A constexpr constructor must initialize every data member.
The initializers 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:
7.6. static Class Members
Declaring static Members
class Account {
public:
void calculate() { amount += amount * interestRate; }
static double rate() { return interestRate; }
static void rate(double);
private:
std::string owner;
double amount;
static double interestRate;
static double initRate();
};
-
We say a member is associated with the class by adding the keyword static to its declaration.
-
Like any other member, static members 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.
-
Static member functions are not bound to any object; they do not have a this pointer.
As a result, static member functions may not be declared as const, and we may not refer to this in the body of a static member.
Using a Class static Member
double r;
r = Account::rate(); // access a static member using the scope operator
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;
// equivalent ways to call the static member rate function
r = ac1.rate(); // through an Account object or reference
r = ac2->rate(); // through a pointer to an Account object
Member functions can use static members directly, without the scope operator:
class Account {
public:
void calculate() { amount += amount * interestRate; }
private:
static double interestRate;
// remaining members as before
};
Defining static Members
When we define a static member outside the class, we do not repeat the static keyword. The keyword appears only with the declaration inside the class body。
Because static data members are not part of individual objects of the class type, they are not defined when we create objects of the class.
As a result, they are not initialized by the class’ constructors.
Moreover, in general, we may not initialize a static member inside the class.
Instead, we must define and initialize each static data member outside the class body.
Like any other object, a static data member may be defined only once.
Like global objects, static data members are defined outside any function.
Hence, once they are defined, they continue to exist until the program completes.
In-Class Initialization of static Data Members
Ordinarily, class static members may not be initialized in the class body.
However, we can provide in-class initializers for static members that have const integral type and must do so for static members that are constexprs of literal type.
The initializers must be constant expressions.
Such members are themselves constant expressions; they can be used where a constant expression is required.
static Members Can Be Used in Ways Ordinary Members Can’t
Static members exist independently of any other object.
A static data member can have incomplete type.
In particular, a static data member can have the same type as the class type of which it is a member.
A nonstatic data member is restricted to being declared as a pointer or a reference to an object of its class:
class Bar {
public:
// ...
private:
static Bar mem1; // ok: static member can have incomplete type
Bar *mem2; // ok: pointer member can have incomplete type
Bar mem3; // error: data members must have complete type
};
Another difference between static and ordinary members is that we can use a static member as a default argument.
class Screen {
public:
// bkground refers to the static member
// declared later in the class definition
Screen& clear(char = bkground);
private:
static const char bkground;
};
A nonstatic data member may not be used as a default argument because its value is part of the object of which it is a member.
本文概述了C++中抽象数据类型(Data Abstraction and Encapsulation)的概念,包括接口与实现分离、成员函数定义和使用,以及类的抽象数据类型定义。重点讲解了构造函数的种类、默认构造器、友元、访问控制和静态成员的使用。深入探讨了类的作用域、访问修饰符,以及类相关函数和常量成员函数的区别。
1701

被折叠的 条评论
为什么被折叠?



