1. Trang chủ
  2. » Công Nghệ Thông Tin

Thinking in C plus plus (P14) ppt

50 187 0
Tài liệu đã được kiểm tra trùng lặp

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

THÔNG TIN TÀI LIỆU

Thông tin cơ bản

Định dạng
Số trang 50
Dung lượng 180,59 KB

Các công cụ chuyển đổi và chỉnh sửa cho tài liệu này

Nội dung

Functions that don’t automatically inherit Not all functions are automatically inherited from the base class into the derived class.. But, and this is an important point, as soon as yo

Trang 1

Because we can now guarantee that we know exactly what kind of

objects are in the container, the destructor works correctly and the

ownership problem is solved – or at least, one approach to the

ownership problem Here, if you push( ) a string pointer onto the

StringStack, then (according to the semantics of the StringStack)

you’re also passing ownership of that pointer to the StringStack If

you pop( ) the pointer, you not only get the pointer, but you also

get ownership of that pointer Any pointers that are left on the

StringStack when its destructor is called are then deleted by that

destructor And since these are always string pointers and the

delete statement is working on string pointers instead of void

pointers, the proper destruction happens and everything works

correctly

There is a drawback: this class works only for string pointers If you

want a Stack that works with some other kind of object, you must

write a new version of the class so that it works only with your new

kind of object This rapidly becomes tedious, and is finally solved

using templates, as you will see in Chapter 16

We can make an additional observation about this example: it

changes the interface of the Stack in the process of inheritance If

the interface is different, then a StringStack really isn’t a Stack, and

you will never be able to correctly use a StringStack as a Stack

This makes the use of inheritance questionable here; if you’re not

creating a StringStack that is-a type of Stack, then why are you

inheriting? A more appropriate version of StringStack will be

shown later in this chapter

Functions that don’t automatically

inherit

Not all functions are automatically inherited from the base class

into the derived class Constructors and destructors deal with the

creation and destruction of an object, and they can know what to

Trang 2

do with the aspects of the object only for their particular class, so all the constructors and destructors in the hierarchy below them must

be called Thus, constructors and destructors don’t inherit and must

be created specially for each derived class

In addition, the operator= doesn’t inherit because it performs a

constructor-like activity That is, just because you know how to

assign all the members of an object on the left-hand side of the =

from an object on the right-hand side doesn’t mean that assignment will still have the same meaning after inheritance

In lieu of inheritance, these functions are synthesized by the

compiler if you don’t create them yourself (With constructors, you can’t create any constructors in order for the compiler to synthesize the default constructor and the copy-constructor.) This was briefly described in Chapter 6 The synthesized constructors use

memberwise initialization and the synthesized operator= uses

memberwise assignment Here’s an example of the functions that are synthesized by the compiler:

Trang 3

public:

// Default GameBoard constructor called:

Game() { cout << "Game()\n"; }

// You must explicitly call the GameBoard

// copy-constructor or the default constructor

// is automatically called instead:

Game(const Game& g) : gb(g.gb) {

cout << "Game(const Game&)\n";

}

Game(int) { cout << "Game(int)\n"; }

Game& operator=(const Game& g) {

// You must explicitly call the GameBoard

// assignment operator or no assignment at

// all happens for gb!

gb = g.gb;

cout << "Game::operator=()\n";

return *this;

}

class Other {}; // Nested class

// Automatic type conversion:

operator Other() const {

cout << "Game::operator Other()\n";

// Default base-class constructor called:

Checkers() { cout << "Checkers()\n"; }

// You must explicitly call the base-class

// copy constructor or the default constructor

// will be automatically called instead:

Checkers(const Checkers& c) : Game(c) {

cout << "Checkers(const Checkers& c)\n";

}

Checkers& operator=(const Checkers& c) {

// You must explicitly call the base-class

// version of operator=() or no base-class

// assignment will happen:

Trang 4

//! d1 = go; // Operator= not synthesized

// for differing types

Checkers c1, c2(c1);

c1 = c2;

} ///:~

The constructors and the operator= for GameBoard and Game

announce themselves so you can see when they’re used by the

compiler In addition, the operator Other( ) performs automatic type conversion from a Game object to an object of the nested class

Other The class Chess simply inherits from Game and creates no

functions (to see how the compiler responds) The function f( ) takes an Other object to test the automatic type conversion

function

In main( ), the synthesized default constructor and

copy-constructor for the derived class Chess are called The Game

versions of these constructors are called as part of the call hierarchy Even though it looks like inheritance, new

constructor-constructors are actually synthesized by the compiler As you

might expect, no constructors with arguments are automatically created because that’s too much for the compiler to intuit

The operator= is also synthesized as a new function in Chess using

memberwise assignment (thus, the base-class version is called) because that function was not explicitly written in the new class

Trang 5

And of course the destructor was automatically synthesized by the

compiler

Because of all these rules about rewriting functions that handle

object creation, it may seem a little strange at first that the

automatic type conversion operator is inherited But it’s not too

unreasonable – if there are enough pieces in Game to make an

Other object, those pieces are still there in anything derived from

Game and the type conversion operator is still valid (even though

you may in fact want to redefine it)

operator= is synthesized only for assigning objects of the same type

If you want to assign one type to another you must always write

that operator= yourself

If you look more closely at Game, you’ll see that the

copy-constructor and assignment operators have explicit calls to the

member object copy-constructor and assignment operator You will

normally want to do this because otherwise, in the case of the

copy-constructor, the default member object constructor will be used

instead, and in the case of the assignment operator, no assignment

at all will be done for the member objects!

Lastly, look at Checkers, which explicitly writes out the default

constructor, copy-constructor, and assignment operators In the

case of the default constructor, the default base-class constructor is

automatically called, and that’s typically what you want But, and

this is an important point, as soon as you decide to write your own

copy-constructor and assignment operator, the compiler assumes

that you know what you’re doing and does not automatically call

the base-class versions, as it does in the synthesized functions If

you want the base class versions called (and you typically do) then

you must explicitly call them yourself In the Checkers

copy-constructor, this call appears in the constructor initializer list:

Checkers(const Checkers& c) : Game(c) {

Trang 6

In the Checkers assignment operator, the base class call is the first

line in the function body:

Game::operator=(c);

These calls should be part of the canonical form that you use

whenever you inherit a class

Inheritance and static member functions

static member functions act the same as non-static member

functions:

1 They inherit into the derived class

2 If you redefine a static member, all the other overloaded

functions in the base class are hidden

3 If you change the signature of a function in the base class, all the base class versions with that function name are hidden (this is really a variation of the previous point)

However, static member functions cannot be virtual (a topic

covered thoroughly in Chapter 15)

Choosing composition vs inheritance

Both composition and inheritance place subobjects inside your new class Both use the constructor initializer list to construct these

subobjects You may now be wondering what the difference is

between the two, and when to choose one over the other

Composition is generally used when you want the features of an existing class inside your new class, but not its interface That is,

you embed an object to implement features of your new class, but the user of your new class sees the interface you’ve defined rather than the interface from the original class To do this, you follow the

Trang 7

typical path of embedding private objects of existing classes inside

your new class

Occasionally, however, it makes sense to allow the class user to

directly access the composition of your new class, that is, to make

the member objects public The member objects use access control

themselves, so this is a safe thing to do and when the user knows

you’re assembling a bunch of parts, it makes the interface easier to

understand A Car class is a good example:

//: C14:Car.cpp

// Public composition

class Engine {

public:

void start() const {}

void rev() const {}

void stop() const {}

void rollup() const {}

void rolldown() const {}

};

class Door {

public:

Window window;

void open() const {}

void close() const {}

Trang 8

Because the composition of a Car is part of the analysis of the

problem (and not simply part of the underlying design), making

the members public assists the client programmer’s understanding

of how to use the class and requires less code complexity for the creator of the class

With a little thought, you’ll also see that it would make no sense to

compose a Car using a “vehicle” object – a car doesn’t contain a

vehicle, it is a vehicle The is-a relationship is expressed with

inheritance, and the has-a relationship is expressed with

composition

Subtyping

Now suppose you want to create a type of ifstream object that not

only opens a file but also keeps track of the name of the file You

can use composition and embed both an ifstream and a string into

the new class:

Trang 9

FName1(const string& fname)

: fileName(fname), file(fname.c_str()) {

assure(file, fileName);

named = true;

}

string name() const { return fileName; }

void name(const string& newName) {

if(named) return; // Don't overwrite

cout << file.name() << endl;

// Error: close() not a member:

//! file.close();

} ///:~

There’s a problem here, however An attempt is made to allow the

use of the FName1 object anywhere an ifstream object is used by

including an automatic type conversion operator from FName1 to

an ifstream& But in main, the line

file.close();

will not compile because automatic type conversion happens only

in function calls, not during member selection So this approach

won’t work

A second approach is to add the definition of close( ) to FName1:

void close() { file.close(); }

This will work if there are only a few functions you want to bring

through from the ifstream class In that case you’re only using part

of the class, and composition is appropriate

But what if you want everything in the class to come through? This

is called subtyping because you’re making a new type from an

Trang 10

existing type, and you want your new type to have exactly the same interface as the existing type (plus any other member

functions you want to add), so you can use it everywhere you’d use the existing type This is where inheritance is essential You can see that subtyping solves the problem in the preceding example

string name() const { return fileName; }

void name(const string& newName) {

if(named) return; // Don't overwrite

Trang 11

Now any member function available for an ifstream object is

available for an FName2 object You can also see that non-member

functions like getline( ) that expect an ifstream can also work with

an FName2 That’s because an FName2 is a type of ifstream; it

doesn’t simply contain one This is a very important issue that will

be explored at the end of this chapter and in the next one

private inheritance

You can inherit a base class privately by leaving off the public in

the base-class list, or by explicitly saying private (probably a better

policy because it is clear to the user that you mean it) When you

inherit privately, you’re “implementing in terms of;” that is, you’re

creating a new class that has all of the data and functionality of the

base class, but that functionality is hidden, so it’s only part of the

underlying implementation The class user has no access to the

underlying functionality, and an object cannot be treated as a

instance of the base class (as it was in FName2.cpp)

You may wonder what the purpose of private inheritance is,

because the alternative of using composition to create a private

object in the new class seems more appropriate private inheritance

is included in the language for completeness, but if for no other

reason than to reduce confusion, you’ll usually want to use

composition rather than private inheritance However, there may

occasionally be situations where you want to produce part of the

same interface as the base class and disallow the treatment of the

object as if it were a base-class object private inheritance provides

this ability

Publicizing privately inherited members

When you inherit privately, all the public members of the base

class become private If you want any of them to be visible, just say

their names (no arguments or return values) in the public section of

the derived class:

//: C14:PrivateInheritance.cpp

Trang 12

class Pet {

public:

char eat() const { return 'a'; }

int speak() const { return 2; }

float sleep() const { return 3.0; }

float sleep(int) const { return 4.0; }

};

class Goldfish : Pet { // Private inheritance

public:

Pet::eat; // Name publicizes member

Pet::sleep; // Both overloaded members exposed

Thus, private inheritance is useful if you want to hide part of the

functionality of the base class

Notice that giving the name of an overloaded function exposes all the versions of the overloaded function in the base class

You should think carefully before using private inheritance instead

of composition; private inheritance has particular complications

when combined with runtime type identification (this is the topic of

a chapter in Volume 2 of this book, downloadable from

www.BruceEckel.com)

protected

Now that you’ve been introduced to inheritance, the keyword

protected finally has meaning In an ideal world, private members

would always be hard-and-fast private, but in real projects there

are times when you want to make something hidden from the

Trang 13

world at large and yet allow access for members of derived classes

The protected keyword is a nod to pragmatism; it says, “This is

private as far as the class user is concerned, but available to anyone

who inherits from this class.”

The best approach is to leave the data members private – you

should always preserve your right to change the underlying

implementation You can then allow controlled access to inheritors

of your class through protected member functions:

int read() const { return i; }

void set(int ii) { i = ii; }

You will find examples of the need for protected in examples later

in this book, and in Volume 2

Trang 14

protected inheritance

When you’re inheriting, the base class defaults to private, which means that all of the public member functions are private to the user of the new class Normally, you’ll make the inheritance public

so the interface of the base class is also the interface of the derived

class However, you can also use the protected keyword during

inheritance

Protected derivation means “implemented-in-terms-of” to other classes but “is-a” for derived classes and friends It’s something you don’t use very often, but it’s in the language for completeness

Operator overloading & inheritance

Except for the assignment operator, operators are automatically inherited into a derived class This can be demonstrated by

inheriting from C12:Byte.h:

// Constructors don't inherit:

Byte2(unsigned char bb = 0) : Byte(bb) {}

// operator= does not inherit, but

// is synthesized for memberwise assignment

// However, only the SameType = SameType

// operator= is synthesized, so you have to

// make the others explicitly:

Byte2& operator=(const Byte& right) {

Trang 15

return *this;

}

};

// Similar test function as in C12:ByteTest.cpp:

void k(Byte2& b1, Byte2& b2) {

TRY2(+) TRY2(-) TRY2(*) TRY2(/)

TRY2(%) TRY2(^) TRY2(&) TRY2(|)

TRY2(<<) TRY2(>>) TRY2(+=) TRY2(-=)

TRY2(*=) TRY2(/=) TRY2(%=) TRY2(^=)

TRY2(&=) TRY2(|=) TRY2(>>=) TRY2(<<=)

TRY2(=) // Assignment operator

TRYC2(<) TRYC2(>) TRYC2(==) TRYC2(!=) TRYC2(<=)

TRYC2(>=) TRYC2(&&) TRYC2(||)

Trang 16

The test code is identical to that in C12:ByteTest.cpp except that

Byte2 is used instead of Byte This way all the operators are

verified to work with Byte2 via inheritance

When you examine the class Byte2, you’ll see that the constructor must be explicitly defined, and that only the operator= that assigns

a Byte2 to a Byte2 is synthesized; any other assignment operators

that you need you’ll have to synthesize on your own

Multiple inheritance

You can inherit from one class, so it would seem to make sense to inherit from more than one class at a time Indeed you can, but whether it makes sense as part of a design is a subject of continuing debate One thing is generally agreed upon: You shouldn’t try this until you’ve been programming quite a while and understand the language thoroughly By that time, you’ll probably realize that no matter how much you think you absolutely must use multiple inheritance, you can almost always get away with single

Incremental development

One of the advantages of inheritance and composition is that these support incremental development by allowing you to introduce new code without causing bugs in existing code If bugs do appear, they are isolated within the new code By inheriting from (or composing with) an existing, functional class and adding data members and member functions (and redefining existing member functions

Trang 17

during inheritance) you leave the existing code – that someone else

may still be using – untouched and unbugged If a bug happens,

you know it’s in your new code, which is much shorter and easier

to read than if you had modified the body of existing code

It’s rather amazing how cleanly the classes are separated You don’t

even need the source code for the member functions in order to

reuse the code, just the header file describing the class and the

object file or library file with the compiled member functions (This

is true for both inheritance and composition.)

It’s important to realize that program development is an

incremental process, just like human learning You can do as much

analysis as you want, but you still won’t know all the answers

when you set out on a project You’ll have much more success –

and more immediate feedback – if you start out to “grow” your

project as an organic, evolutionary creature, rather than

constructing it all at once like a glass-box skyscraper2

Although inheritance for experimentation is a useful technique, at

some point after things stabilize you need to take a new look at

your class hierarchy with an eye to collapsing it into a sensible

structure3 Remember that underneath it all, inheritance is meant to

express a relationship that says, “This new class is a type of that old

class.” Your program should not be concerned with pushing bits

around, but instead with creating and manipulating objects of

various types to express a model in the terms given you from the

Trang 18

Upcasting

Earlier in the chapter, you saw how an object of a class derived

from ifstream has all the characteristics and behaviors of an

ifstream object In FName2.cpp, any ifstream member function

could be called for an FName2 object

The most important aspect of inheritance is not that it provides member functions for the new class, however It’s the relationship expressed between the new class and the base class This

relationship can be summarized by saying, “The new class is a type

of the existing class.”

This description is not just a fanciful way of explaining inheritance – it’s supported directly by the compiler As an example, consider a

base class called Instrument that represents musical instruments and a derived class called Wind Because inheritance means that all

the functions in the base class are also available in the derived class, any message you can send to the base class can also be sent to the

derived class So if the Instrument class has a play( ) member

function, so will Wind instruments This means we can accurately say that a Wind object is also a type of Instrument The following

example shows how the compiler supports this notion:

//: C14:Instrument.cpp

// Inheritance & upcasting

enum note { middleC, Csharp, Cflat }; // Etc

class Instrument {

public:

void play(note) const {}

};

// Wind objects are Instruments

// because they have the same interface:

class Wind : public Instrument {};

void tune(Instrument& i) {

//

i.play(middleC);

Trang 19

What’s interesting in this example is the tune( ) function, which

accepts an Instrument reference However, in main( ) the tune( )

function is called by handing it a reference to a Wind object Given

that C++ is very particular about type checking, it seems strange

that a function that accepts one type will readily accept another

type, until you realize that a Wind object is also an Instrument

object, and there’s no function that tune( ) could call for an

Instrument that isn’t also in Wind (this is what inheritance

guarantees) Inside tune( ), the code works for Instrument and

anything derived from Instrument, and the act of converting a

Wind reference or pointer into an Instrument reference or pointer

is called upcasting

Why “upcasting?”

The reason for the term is historical and is based on the way class

inheritance diagrams have traditionally been drawn: with the root

at the top of the page, growing downward (Of course, you can

draw your diagrams any way you find helpful.) The inheritance

diagram for Instrument.cpp is then:

Instrument

Wind

Casting from derived to base moves up on the inheritance diagram,

so it’s commonly referred to as upcasting Upcasting is always safe

because you’re going from a more specific type to a more general

type – the only thing that can occur to the class interface is that it

can lose member functions, not gain them This is why the compiler

Trang 20

allows upcasting without any explicit casts or other special

notation

Upcasting and the copy-constructor

If you allow the compiler to synthesize a copy-constructor for a derived class, it will automatically call the base-class copy-

constructor, and then the copy-constructors for all the member objects (or perform a bitcopy on built-in types) so you’ll get the right behavior:

Parent(int ii) : i(ii) {

cout << "Parent(int ii)\n";

}

Parent(const Parent& b) : i(b.i) {

cout << "Parent(const Parent&)\n";

}

Parent() : i(0) { cout << "Parent()\n"; }

friend ostream&

operator<<(ostream& os, const Parent& b) {

return os << "Parent: " << b.i << endl;

Member(int ii) : i(ii) {

cout << "Member(int ii)\n";

}

Member(const Member& m) : i(m.i) {

cout << "Member(const Member&)\n";

}

friend ostream&

operator<<(ostream& os, const Member& m) {

Trang 21

return os << "Member: " << m.i << endl;

Child(int ii) : Parent(ii), i(ii), m(ii) {

cout << "Child(int ii)\n";

cout << "calling copy-constructor: " << endl;

Child c2 = c; // Calls copy-constructor

cout << "values in c2:\n" << c2;

} ///:~

The operator<< for Child is interesting because of the way that it

calls the operator<< for the Parent part within it: by casting the

Child object to a Parent& (if you cast to a base-class object instead

of a reference you will usually get undesirable results):

return os << (Parent&)c << c.m

Since the compiler then sees it as a Parent, it calls the Parent

version of operator<<

You can see that Child has no explicitly-defined copy-constructor

The compiler then synthesizes the copy-constructor (since that is

one of the four functions it will synthesize, along with the default

constructor – if you don’t create any constructors – the operator=

and the destructor) by calling the Parent copy-constructor and the

Member copy-constructor This is shown in the output

Trang 22

However, if you try to write your own copy-constructor for Child

and you make an innocent mistake and do it badly:

Child(const Child& c) : i(c.i), m(c.m) {}

then the default constructor will automatically be called for the

base-class part of Child, since that’s what the compiler falls back on

when it has no other choice of constructor to call (remember that

some constructor must always be called for every object, regardless

of whether it’s a subobject of another class) The output will then be:

To repair the problem you must remember to properly call the base-class copy-constructor (as the compiler does) whenever you write your own copy-constructor This can seem a little strange-looking at first but it’s another example of upcasting:

Trang 23

Child(const Child& c)

: Parent(c), i(c.i), m(c.m) {

cout << "Child(Child&)\n";

}

The strange part is where the Parent copy-constructor is called:

Parent(c) What does it mean to pass a Child object to a Parent

constructor? But Child is inherited from Parent, so a Child

reference is a Parent reference The base-class copy-constructor call

upcasts a reference to Child to a reference to Parent and uses it to

perform the copy-construction When you write your own copy

constructors you’ll almost always want to do the same thing

Composition vs inheritance (revisited)

One of the clearest ways to determine whether you should be using

composition or inheritance is by asking whether you’ll ever need to

upcast from your new class Earlier in this chapter, the Stack class

was specialized using inheritance However, chances are the

StringStack objects will be used only as string containers and

never upcast, so a more appropriate alternative is composition:

Trang 24

additional type checking happens at compile time

Although it tends to be more confusing, you could also use private

inheritance to express “implemented in terms of.” This would also solve the problem adequately One place it becomes important, however, is when multiple inheritance might be warranted In that case, if you see a design in which composition can be used instead

of inheritance, you may be able to eliminate the need for multiple inheritance

Pointer & reference upcasting

In Instrument.cpp, the upcasting occurs during the function call – a

Wind object outside the function has its reference taken and

becomes an Instrument reference inside the function Upcasting

can also occur during a simple assignment to a pointer or reference: Wind w;

Instrument* ip = &w; // Upcast

Instrument& ir = w; // Upcast

Trang 25

Like the function call, neither of these cases requires an explicit

the compiler can deal with ip only as an Instrument pointer and

nothing else That is, it cannot know that ip actually happens to

point to a Wind object So when you call the play( ) member

function by saying

ip->play(middleC);

the compiler can know only that it’s calling play( ) for an

Instrument pointer, and call the base-class version of

Instrument::play( ) instead of what it should do, which is call

Wind::play( ) Thus, you won’t get the correct behavior

This is a significant problem; it is solved in Chapter 15 by

introducing the third cornerstone of object-oriented programming:

polymorphism (implemented in C++ with virtual functions)

Summary

Both inheritance and composition allow you to create a new type

from existing types, and both embed subobjects of the existing

types inside the new type Typically, however, you use

composition to reuse existing types as part of the underlying

implementation of the new type and inheritance when you want to

force the new type to be the same type as the base class (type

equivalence guarantees interface equivalence) Since the derived

class has the base-class interface, it can be upcast to the base, which

is critical for polymorphism as you’ll see in Chapter 15

Ngày đăng: 05/07/2014, 19:20

TỪ KHÓA LIÊN QUAN

TÀI LIỆU CÙNG NGƯỜI DÙNG

TÀI LIỆU LIÊN QUAN