12.1. Class Definitions and DeclarationsStarting from Chapter 1, our programs have used classes. The library types we've usedvector, istream, stringare all class types. We've also defined some simple classes of our own, such as the Sales_item and TextQuery classes. To recap, let's look again at the Sales_item class: class Sales_item { public: // operations on Sales_item objects double avg_price() const; bool same_isbn(const Sales_item &rhs) const { return isbn == rhs.isbn; } // default constructor needed to initialize members of built-in type Sales_item(): units_sold(0), revenue(0.0) { } private: std::string isbn; unsigned units_sold; double revenue; }; double Sales_item::avg_price() const { if (units_sold) return revenue/units_sold; else return 0; } 12.1.1. Class Definitions: A RecapIn writing this class in Section 2.8 (p. 63) and Section 7.7 (p. 258), we already learned a fair bit about classes.
Class MembersEach class defines zero or more members. Members can be either data, functions, or type definitions. A class may contain multiple public, private, and protected sections. We've already used the public and private access labels: Members defined in the public section are accessible to all code that uses the type; those defined in the private section are accessible to other class members. We'll have more to say about protected when we discuss inheritance in Chapter 15. All members must be declared inside the class; there is no way to add members once the class definition is complete. ConstructorsWhen we create an object of a class type, the compiler automatically uses a constructor (Section 2.3.3, p. 49) to initialize the object. A constructor is a special member function that has the same name as the class. Its purpose is to ensure that each data member is set to sensible initial values. A constructor generally should use a constructor initializer list (Section 7.7.3, p. 263), to initialize the data members of the object:
// default constructor needed to initialize members of built-in type
Sales_item(): units_sold(0), revenue(0.0) { }
The constructor initializer list is a list of member names and parenthesized initial values. It follows the constructor's parameter list and begins with a colon. Member FunctionsMember functions must be declared, and optionally may be defined, inside the class; functions defined inside the class are inline (Section 7.6, p. 256) by default. Member functions defined outside the class must indicate that they are in the scope of the class. The definition of Sales_item::avg_price uses the scope operator (Section 1.2.2, p. 8) to indicate that the definition is for the avg_price function of the Sales_item class. Member functions take an extra implicit argument that binds the function to the object on behalf of which the function is calledwhen we write trans.avg_price() we are calling the avg_price function on the object named trans. If TRans is a Sales_item object, then references to a member of the Sales_item class inside the avg_price function are to the members in trans. Member functions may be declared const by putting the const keyword following the parameter list: double avg_price() const; A const member may not change the data members of the object on which it operates. The const must appear in both the declaration and definition. It is a compile-time error for the const to be indicated on one but not the other. 12.1.2. Data Abstraction and EncapsulationThe 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 class designer must worry about how a class is implemented, but programmers that use the class need not know about these details. Instead, programmers who use a type need to know only the type's interface; they can think abstractly about what the type does rather than concretely about how the type works. Encapsulation is a term that describes the technique of combining lower-level elements to form a new, higher-level entity. A function is one form of encapsulation: The detailed actions performed by the function are encapsulated in the larger entity that is the function itself. Encapsulated elements hide the details of their implementationwe may call a function but have no access to the statements that it executes. In the same way, a class is an encapsulated entity: It represents an aggregation of several members, and most (well-designed) class types hide the members that implement the type. If we think about the library vector type, it is an example of both data abstraction and encapsulation. It is abstract in that to use it, we think about its interfaceabout the operations that it can perform. It is encapsulated because we have no access to the details of how the type is representated nor to any of its implementation artifacts. An array, on the other hand, is similar in concept to a vector but is neither abstract nor encapsulated. We manipulate an array directly by accessing the memory in which the array is stored. Access Labels Enforce Abstraction and EncapsulationIn C++ we use access labels (Section 2.8, p. 65) to define the abstract interface to the class and to enforce encapsulation. A class may contain zero or more access labels:
There are no restrictions on how often an access label may appear. Each access label specifies the access level of the succeeding member definitions. The specified access level remains in effect until the next access label is encountered or the closing right brace of the class body is seen. A class may define members before any access label is seen. The access level of members defined after the open curly of the class and before the first access label depend on how the class is defined. If the class is defined with the struct keyword, then members defined before the first access label are public; if the class is defined using the class keyword, then the members are private.
Different Kinds of Programming RolesProgrammers tend to think about the people who will run their applications as "users." Applications are designed for and evolve in response to feedback from those who ultimately "use" the applications. Classes are thought of in a similar way: A class designer designs and implements a class for "users" of that class. In this case, the "user" is a programmer, not the ultimate user of the application. Authors of successful applications do a good job of understanding and implementing the needs of the application's users. Similarly, well-designed, useful classes are designed with a close attention to the needs of the users of the class. In another way, the division between class designer and class user reflects the division between users of an application and the designers and implementors of the application. Users care only if the application meets their needs in a cost-effective way. Similarly, users of a class care only about its interface. Good class designers define a class interface that is intuitive and easy to use. Users care about the implementation only in so far as the implementation affects their use of the class. If the implementation is too slow or puts burdens on users of the class, then the users must care. In well-designed classes, only the class designer worries about the implementation. In simple applications, the user of a class and the designer of the class might be one and the same person. Even in such cases, it is useful to keep the roles distinct. When designing the interface to a class, the class designer should think about how easy it will be to use the class. When using the class, the designer shouldn't think about how the class works.
When referring to a "user," the context makes it clear which kind of user is meant. If we speak of "user code" or the "user" of the Sales_item class, we mean a programmer who is using a class in writing an application. If we speak of the "user" of the bookstore application, we mean the manager of the store who is running the application.
12.1.3. More on Class DefinitionsThe classes we've defined so far have been simple; yet they have allowed us to explore quite a bit of the language support for classes. There remain a few more details about the basics of writing a class that we shall cover in the remainder of this section.
Multiple Data Members of the Same TypeAs we've seen, class data members are declared similarly to how ordinary variables are declared. One way in which member declarations and ordinary declarations are the same is that if a class has multiple data members with the same type, these members can be named in a single member declaration. For example, we might define a type named Screen to represent a window on a computer. Each Screen would have a string member that holds the contents of the window, and three string::size_type members: one that specifies the character on which the cursor currently rests, and two others that specify the height and width of the window. We might define the members of this class as:
class Screen {
public:
// interface member functions
private:
std::string contents;
std::string::size_type cursor;
std::string::size_type height, width;
};
Using Typedefs to Streamline ClassesIn addition to defining data and function members, a class can also define its own local names for types. Our Screen will be a better abstraction if we provide a typedef for std::string::size_type:
class Screen {
public:
// interface member functions
typedef std::string::size_type index;
private:
std::string contents;
index cursor;
index height, width;
};
Type names defined by a class obey the standard access controls of any other member. We put the definition of index in the public part of the class because we want users to use that name. Users of class Screen need not know that we use a string as the underlying implementation. By defining index, we hide this detail of how Screen is implemented. By making the type public, we let our users use this name. Member Functions May Be OverloadedAnother way our classes have been simple is that they have defined only a few member functions. In particular, none of our classes have needed to define over-loaded versions of any of their member functions. However, as with nonmember functions, a member function may be overloaded (Section 7.8, p. 265). With the exception of overloaded operators (Section 14.9.5, p. 547)which have special rulesa member function overloads only other member functions of its own class. A class member function is unrelated to, and cannot overload, ordinary nonmember functions or functions declared in other classes. The same rules apply to overloaded member functions as apply to plain functions: Two overloaded members cannot have the same number and types of parameters. The function-matching (Section 7.8.2, p. 269) process used for calls of nonmember overloaded functions also applies to calls of overloaded member functions. Defining Overloaded Member FunctionsTo illustrate overloading, we might give our Screen class two overloaded members to return a given character from the window. One version will return the character currently denoted by the cursor and the other returns the character at a given row and column: class Screen { public: typedef std::string::size_type index; // return character at the cursor or at a given position char get() const { return contents[cursor]; } char get(index ht, index wd) const; // remaining members private: std::string contents; index cursor; index height, width; }; As with any overloaded function, we select which version to run by supplying the appropriate number and/or types of arguments to a given call: Screen myscreen; char ch = myscreen.get();// calls Screen::get() ch = myscreen.get(0,0); // calls Screen::get(index, index) Explicitly Specifying inline Member FunctionsMember functions that are defined inside the class, such as the get member that takes no arguments, are automatically treated as inline. That is, when they are called, the compiler will attempt to expand the function inline (Section 7.6, p. 256). We can also explicitly declare a member function as inline: class Screen { public: typedef std::string::size_type index; // implicitly inline when defined inside the class declaration char get() const { return contents[cursor]; } // explicitly declared as inline; will be defined outside the class declaration inline char get(index ht, index wd) const; // inline not specified in class declaration, but can be defined inline later index get_cursor() const; // ... }; // inline declared in the class declaration; no need to repeat on the definition char Screen::get(index r, index c) const { index row = r * width; // compute the row location return contents[row + c]; // offset by c to fetch specified character } // not declared as inline in the class declaration, but ok to make inline in definition inline Screen::index Screen::get_cursor() const { return cursor; } We can specify that a member is inline as part of its declaration inside the class body. Alternatively, we can specify inline on the function definition that appears outside the class body. It is legal to specify inline both on the declaration and definition. One advantage of defining inline functions outside the class is that it can make the class easier to read.
12.1.4. Class Declarations versus DefinitionsA class is completely defined once the closing curly brace appears. Once the class is defined, all the class members are known. The size required to store an object of the class is known as well. A class may be defined only once in a given source file. When a class is defined in multiple files, the definition in each file must be identical. By putting class definitions in header files, we can ensure that a class is defined the same way in each file that uses it. By using header guards (Section 2.9.2, p. 69), we ensure that even if the header is included more than once in the same file, the class definition will be seen only once.
It is possible to declare a class without defining it:
class Screen; // declaration of the Screen class
This declaration, sometimes referred to as a forward declaration, introduces the name Screen into the program and indicates that Screen refers to a class type. After a declaration and before a definition is seen, the type Screen is an incompete typeit's known that Screen is a type but not known what members that type contains.
A class must be fully defined before objects of that type are created. The class must be definedand not just declaredso that the compiler can know how much storage to reserve for an object of that class type. Similarly, the class must be defined before a reference or pointer is used to access a member of the type. Using Class Declarations for Class MembersA data member can be specified to be of a class type only if the definition for the class has already been seen. If the type is incomplete, a data member can be only a pointer or a reference to that class type. Because a class is not defined until its class body is complete, a class cannot have data members of its own type. However, a class is considered declared as soon as its class name has been seen. Therefore, a class can have data members that are pointers or references to its own type: class LinkScreen { Screen window; LinkScreen *next; LinkScreen *prev; };
12.1.5. Class ObjectsWhen we define a class, we are defining a type. Once a class is defined, we can define objects of that type. Storage is allocated when we define objects, but (ordinarily) not when we define types: class Sales_item { public: // operations on Sales_item objects private: std::string isbn; unsigned units_sold; double revenue; }; defines a new type, but does not allocate any storage. When we define an object Sales_item item; the compiler allocates an area of storage sufficient to contain a Sales_item object. The name item refers to that area of storage. Each object has its own copy of the class data members. Modifying the data members of item does not change the data members of any other Sales_item object. Defining Objects of Class TypeAfter a class type has been defined, the type can be used in two ways:
Both methods of referring to a class type are equivalent. The second method is inherited from C and is also valid in C++. The first, more concise form was introduced by C++ to make class types easier to use. Why a Class Definition Ends in a SemicolonWe noted on page 64 that a class definition ends with a semicolon. A semicolon is required because we can follow a class definition by a list of object definitions. As always, a definition must end in a semicolon: class Sales_item { /* ... */ }; class Sales_item { /* ... */ } accum, trans;
![]() |