📄 ch05.htm
字号:
<tt> return 0;</tt><tt>}</tt></pre><p>The function <tt>identify</tt> can receive any object that is publicly derived from class <tt>base</tt> -- even objects of subclasses that were defined after <tt>identify</tt> was compiled.</p><p>Dynamic binding has numerous advantages. In this example, it enables the user to extend the functionality of <tt>base</tt> without having to modify <tt>identify</tt> in any way. In procedural and object-based programming, such flexibility is nearly impossible. Furthermore, the underlying mechanism of dynamic binding is automatic. The programmer doesn't need to implement the code for runtime lookup and dispatch of a virtual function, nor does he or she need to check the dynamic type of the object.</p><h2> <a name="Heading6">Techniques Of Object-Oriented Programming</a></h2><p>Up until now, the discussion has focused on the general characteristics of object-oriented programming and design. This part presents C++-specific practical techniques and guidelines of object-oriented programming.</p><h3> <a name="Heading7">Class Design</a></h3><p>Classes are the primary unit of abstraction in C++. Finding the right classes during analysis and design is perhaps the most important phase in the lifetime of an object-oriented software system. The common guidelines for finding classes state that a class should represent a real-world object; others maintain that nouns in natural languages should represent classes. This is true to some extent, but nontrivial software projects often have classes that exist nowhere except the programming domain. Does an exception represent a real-world object? Do function objects (which are discussed in Chapter 10, "STL and Generic Programming") and smart pointers have an equivalent outside the programming environment? Clearly, the relationship between real-world entities and objects is not 1:1.</p><h4> Finding the Classes</h4><p>The process of finding the right classes is mostly derived from the functional requirements of the application domain. That is, a designer can decide to represent a concept as a class (rather than, for example, a member function within a different class or a global function) when it serves the needs of the application. This is usually done by means of CRC (Class, Responsibility, Collaboration) cards or any other method.</p><h4> Common Design Mistakes with Classes</h4><p>No two object-oriented languages are alike. The programming language also affects the design. As you learned in Chapter 4, "Special Member Functions: Default Constructor, Copy Constructor, Destructor, and Assignment Operator," C++ has a distinct symmetry between constructors and destructors that most other object-oriented languages do not have. Objects in C++ can automatically clean up after themselves. C++ also enables you to create local objects with automatic data storage. In other languages, objects can only be created on heap memory. C++ is also one of just a few languages that support multiple inheritance. C++ is a strongly-typed language with static type checking. As much as design gurus insist on separating pure design from implementation artifacts (that is, language-specific behavior), such language-specific features do affect the overall design. But of course, design mistakes do not result only from the interference of other languages.</p><p>Object-orientation is not a panacea. Some common pitfalls can lead to monstrous applications that need constant maintenance, that perform unsatisfactorily, and that only eventually -- or never -- reach production. Some of these design mistakes are easy to detect.</p><h4> Gigantic Classes</h4><p>There are no standardized methods for measuring the size of a class. However, many small specialized classes are preferred to a bulky single class that contains hundreds of member functions and data members. But such bulky classes do get written. Class <tt>std::string</tt> has a fat interface of more than 100 member functions; clearly, this is an exception to the rule and, to be honest, many people consider this to be a compromise between conflicting design approaches. Still, ordinary programs rarely use all these members. More than once I've seen programmers extending a class with additional member functions and data members instead of using more plausible object-oriented techniques such as subclassing. As a rule, a class that exceeds a 20-30 member function count is suspicious.</p><p>Gigantic classes are problematic for at least three reasons: Users of such classes rarely know how to use them properly; the implementation and interface of such classes tend to undergo extensive changes and bug-fixes; and they are not good candidates for reuse because the fat interface and intricate implementation details can fit only a very limited usage. In a sense, large classes are very similar to large functions -- they are noncohesive and difficult to maintain.</p><h4> Exposing Implementation Details</h4><p>Declaring data members with public access is, almost without exception, a design flaw. Still, even vendors of popular frameworks resort to this deprecated programming style. It might be tempting to use public data members because it saves the programmer the bother of writing trivial <i>accessors</i> and <i>mutators</i> (<i>getters</i> and <i>setters</i>, respectively). This approach cannot be recommended, however, because it results in maintenance difficulties and it compromises the class's reliability. Users of such classes tend to rely heavily on their implementation details; even if they normally avoid such dependencies, they might feel that the exposure of the implementation details implies that they are not supposed to change. Sometimes there is no other choice -- the class implementer has not defined any other method of accessing data members of a class. The process of modifying or extending such classes becomes a maintenance nightmare. Infrastructure components, such as <tt>Date</tt> or <tt>string</tt> classes, can be used dozens of times within a single source file. It is not hard to imagine what it is like when dozens of programmers, each producing dozens of source files, have to chase every source line that refers to any one of these classes. This is exactly what caused the notorious Year 2000 Bug. If, on the other hand, data members are declared <tt>private</tt>, users cannot access them directly. When the implementation details of the class are modified, only accessors and mutators need to be modified, but the rest of the code remains intact. </p><p>There is another danger in exposing implementation details. Due to indiscriminate access to data members and helper functions, users can inadvertently tamper with the object's internal data members. They might delete memory (which is supposed to be deleted by the destructor), or they might change the value of a file handle, and so on, with disastrous results. Therefore, it is always a better design choice to hide implementation details of an object.</p><h3> <a name="Heading8">The "Resource Acquisition Is Initialization" Idiom</a></h3><p>Many objects of various kinds share a similar characterization: They must be acquired by means of initialization prior to their usage; then they can be used, and then they have to be released explicitly. Objects such as <tt>File</tt>, <tt>CommunicationSocket</tt>, <tt>DatabaseCursor</tt>, <tt>DeviceContext</tt>, <tt>OperatingSystem</tt>, and many others have to be opened, attached, initialized, constructed, or booted, respectively, before you can use them. When their job is done, they have to be flushed, detached, closed, released, or logged out, respectively. A common design mistake is to have the user request explicitly for the initialization and release operations to take place. A much better choice is to move all initialization action into the constructor and all release actions into the destructor. This technique is called <i>resource acquisition is initialization</i> (<i>The C++ Programming Language, 3rd ed.</i>, page 365). The advantage is a simplified usage protocol. Users can start using the object right after it has been created, without bothering with whether the object is valid or whether further arbitrary initialization actions have to be taken. Furthermore, because the destructor also releases all its resources, users are free from that hassle too. Please note that this technique usually requires an appropriate exception handling code to cope with exceptions that are thrown during construction of the object.</p><h2> <a name="Heading9">Classes and Objects</a></h2><p>Unlike some other object-oriented programming languages, C++ makes a clear distinction between a class, which is a user-defined type, and an object, which is an instance thereof. There are several features for manipulating the state of a class rather than the state of individual objects. These features are discussed in the following sections.</p><h4> Static Data Members</h4><p>A static member is shared by all instances of its class. For that reason, it is sometimes termed a <i>class variable</i>. Static members are useful in synchronization objects. For example, a file lock can be implemented using a <tt>static</tt> data member. An object that is trying to access this file has to check first whether the file is being processed by another user. If the file is available, the object turns the flag on and user can process the file safely. Other users are not allowed to access the file until the flag is reset to false. When the object that is processing the file is finished, it has to turn off the flag, enabling another object to access it.</p><pre><tt>class fileProc</tt><tt>{</tt><tt>private:</tt><tt> FILE *p;</tt><tt> static bool Locked<cite>;</cite></tt><tt>public<b>:</b></tt><tt>//...</tt><tt><b><tt> </tt></b>bool isLocked () const;</tt><tt> //...</tt><tt>};</tt><tt>bool fileProc::Locked<cite>;</cite></tt></pre><h4> Static Member Functions</h4><p>A static member function in a class can access only other static members of its class.. Unlike ordinary member functions, a static member function can be invoked even when no object instance exists. For example</p><pre><tt>class stat</tt><tt>{</tt><tt>private:</tt><tt> int num;</tt><tt>public:</tt><tt> stat(int n = 0) {num=n;}</tt><tt> static void print() {cout <<"static member function" <<endl;</tt><tt>};</tt><tt>int main()</tt><tt>{</tt><tt> stat::print(); //no object instance required</tt><tt> stat s(1);</tt><tt> s.print();//still, a static member function can be called from an object</tt><tt> return 0;</tt><tt>}</tt></pre><p>Static members are used in the following cases:</p><ul> <li> <p> When all other data members of an object are also static</p> </li> <p></p> <li> When the function does not depend on any other object member (like <tt>print()</tt>, in the previous example)</li> <p></p> <li> As a wrapper of a global function</li></ul><p></p><h4> A Pointer to Member Cannot Refer To a Static Member Function</h4><p>It is illegal to assign the address of a static class member to a pointer to member. However, you can take the address of a static member function of a class and treat it as if it were an ordinary function. For example</p><pre><tt>class A</tt><tt>{</tt><tt>public:</tt><tt> static void f();</tt><tt>};</tt><tt>int main()</tt><tt>{</tt><tt> void (*p) () = &A::f; //OK, ordinary pointer to function</tt><tt>}</tt></pre><p>You can do this because a static member function is essentially an ordinary function, which doesn't take an implicit <tt>this</tt> argument.</p><h4> Defining a Class Constant</h4><p>When you need a constant integer member in a class, the easiest way to create one is by using a <tt>const</tt> <tt>static</tt> member of an integral type; unlike other <tt>static</tt> data members, such a member can be initialized within the class body (see also Chapter 2, "Standard Briefing: The Latest Addenda to ANSI/ISO C++"). For example</p><pre><tt>class vector</tt><tt>{</tt><tt>private:</tt><tt> int v_size;</tt><tt> const static int MAX 1024; //a single MAX is shared by all vector objects</tt><tt> char *p;</tt><tt>public:</tt><tt> vector() {p = new char[MAX]; }</tt><tt> vector( int size)</tt><tt> {</tt><tt> if (size <= MAX)</tt><tt> p = new char[size] ;</tt><tt> else</tt><tt> p = new char[MAX];</tt><tt> }</tt><tt>};</tt></pre><h2> <a name="Heading10">Designing Class Hierarchies</a></h2><p>After identifying a set of potential classes that might be required for the application, it is important to correctly identify the interactions and relationships among the classes to specify inheritance, containment, and ownership. The design of class hierarchies, as opposed to designing concrete types, requires additional considerations that are discussed in this section. </p><h3> <a name="Heading11">Private Data Members Are Preferable To Protected Ones</a></h3><p>Data members of a class are usually a part of its implementation. They can be replaced when the internal implementation of the class is changed; therefore, they need to be hidden from other classes. If derived classes need to access these data members, they need to use accessor methods instead of directly accessing data members of a base class. Consequently, no modification is required for derived classes when a change is made in the base class. </p><p>Here's an example:</p><pre><tt>class Date</tt><tt>{</tt><tt>private:</tt><tt> int d,m,y //how a date is represented is an implementation detail</tt><tt>public:</tt><tt> int Day() const {return d; }</tt><tt>};</tt><tt>class DateTime : public Date</tt><tt>{</tt><tt>private:</tt><tt> int hthiss;</tt><tt> int minutes;</tt><tt> int seconds;</tt><tt>public:</tt><tt>//...additional member functions</tt><tt>};</tt></pre><p>Now assume that class <tt>Date</tt> is used mostly on display devices, so it has to supply some method of converting its <tt>d</tt>,<tt>m</tt>,<tt>y</tt>
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -