Section 12.1. Class Definitions and Declarations


12.1. Class Definitions and Declarations

Starting 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 Recap

In writing this class in Section 2.8 (p. 63) and Section 7.7 (p. 258), we already learned a fair bit about classes.

Most fundamentally, a class defines a new type and a new scope.



Class Members

Each 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.

Constructors

When 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 Functions

Member 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.

Exercises Section 12.1.1

Exercise 12.1:

Write a class named Person that represents the name and address of a person. Use a string to hold each of these elements.

Exercise 12.2:

Provide a constructor for Person that takes two strings.

Exercise 12.3:

Provide operations to return the name and address. Should these functions be const? Explain your choice.

Exercise 12.4:

Indicate which members of Person you would declare as public and which you would declare as private. Explain your choice.


12.1.2. 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 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 Encapsulation

In 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:

  • Members defined after a public label are accessible to all parts of the program. The data-abstraction view of a type is defined by its public members.

  • Members defined after a private label are not accessible to code that uses the class. The private sections encapsulate (e.g., hide) the implementation from code that uses the type.

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.

Advice: Concrete and Abstract Types

Not all types need to be abstract. The library pair class is a good example of a useful, well-designed class that is concrete rather than abstract. A concrete class is a class that exposes, rather than hides, its implementation.

Some classes, such as pair, really have no abstract interface. The pair type exists to bundle two data members into a single object. There is no need or advantage to hiding the data members. Hiding the members in a class like pair would only complicate the use of the type.

Even so, such types often have member functions. In particular, it is a good idea for any class that has data members of built-in or compound type to define constructor(s) to initialize those members. The user of the class could initialize or assign to the data members but it is less error-prone for the class to do so.


Different Kinds of Programming Roles

Programmers 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.

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



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.

Key Concept: Benefits of Data Abstraction and Encapsulation

Data abstraction and encapsulation provide two important advantages:

  • Class internals are protected from inadvertent user-level errors, which might corrupt the state of the object.

  • The class implementation may evolve over time in response to changing requirements or bug reports without requiring change in user-level code.

By defining data members only in the private section of the class, 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 affect the change may have. If data are public, then any function that directly accesses the data members of the old representation might be broken. It would be necessary to locate and rewrite all those portions of code that relied on the old representation before the program could be used again.

Similarly, if the internal state of the class is private, then changes to the member data can happen in only a limited number of places. The data is protected from mistakes that users might introduce. If there is a bug that corrupts the object's state, the places to look for the bug are localized: When data are private, only a member function could be responsible for the error. The search for the mistake is limited, greatly easing the problems of maintenance and program correctness.

If the data are private and if the interface to the member functions does not change, then user functions that manipulate class objects require no change.

Because changing a class definition in a header file effectively changes the text of every source file that includes that header, code that uses a class must be recompiled when the class changes.




12.1.3. More on Class Definitions

The 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.

Exercises Section 12.1.2

Exercise 12.5:

What are the access labels supported by C++ classes? What kinds of members should be defined after each access label? What, if any, are the constraints on where and how often an access label may appear inside a class definition?

Exercise 12.6:

How do classes defined with the class keyword differ from those defined as struct?

Exercise 12.7:

What is encapsulation? Why it is useful?


Multiple Data Members of the Same Type

As 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 Classes

In 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 Overloaded

Another 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 Functions

To 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 Functions

Member 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.

As with other inlines, the definition of an inline member function must be visible in every source file that calls the function. The definition for an inline member function that is not defined within the class body ordinarily should be placed in the same header file in which the class definition appears.



12.1.4. Class Declarations versus Definitions

A 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.

Exercises Section 12.1.3

Exercise 12.8:

Define Sales_item::avg_price as an inline function.

Exercise 12.9:

Write your own version of the Screen class presented in this section, giving it a constructor to create a Screen from values for height, width, and the contents of the screen.

Exercise 12.10:

Explain each member in the following class:

      class Record {          typedef std::size_t size;          Record(): byte_count(0) { }          Record(size s): byte_count(s) { }          Record(std::string s): name(s), byte_count(0) { }          size byte_count;          std::string name;      public:          size get_count() const { return byte_count; }          std::string get_name() const { return name; }      }; 


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.

An incomplete type can be used only in limited ways. Objects of the type may not be defined. An incomplete type may be used to define only pointers or references to the type or to declare (but not define) functions that use the type as a paremeter or return type.



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 Members

A 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;      }; 

A common use of class forward declarations is to write classes that are mutually dependent on one another. We'll see an example of such usage in Section 13.4 (p. 486).



Exercises Section 12.1.4

Exercise 12.11:

Define a pair of classes X and Y, in which X has a pointer to Y, and Y has an object of type X.

Exercise 12.12:

Explain the difference between a class declaration and definition. When would you use a class declaration? A class definition?


12.1.5. Class Objects

When 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 Type

After a class type has been defined, the type can be used in two ways:

  • Using the class name directly as a type name

  • Specifying the keyword class or struct, followed by the class name:

          Sales_item item1;       // default initialized object of type Sales_item      class Sales_item item1; // equivalent definition of item1 

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 Semicolon

We 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; 

Ordinarily, it is a bad idea to define an object as part of a class definition. Doing so obscures what's happening. It is confusing to readers to combine definitions of two different entitiesthe class and a variablein a single statement.





C++ Primer
C Primer Plus (5th Edition)
ISBN: 0672326965
EAN: 2147483647
Year: 2006
Pages: 223
Authors: Stephen Prata

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net