Programming 版 (精华区)
发信人: netiscpu (说不如做), 信区: Programming
标 题: C++ 编程准则与忠告 之 Classes
发信站: 紫 丁 香 (Sun Jul 26 11:19:35 1998), 转信
7 Classes
Back to index
7.1 Considerations Regarding Access Rights
Back to index
Rule 22 Never specify public or protected member data in a class.
The use of public variables is discouraged for the following reasons:
1 A public variable represents a violation of one of the basic
principles of object-oriented programming, namely, encapsulation of
data. For example, if there is a class of the type BankAccount, in
which account_balance is a public variable, the value of this variable
may be changed by any user of the class. However, if the variable has
been declared private, its value may be changed only by the member
functions of the class.
2 An arbitrary function in a program can change public data which may
lead to errors that are difficult to locate.
3 If public data is avoided, its internal representation may be
changed without users of the class having to modify their code. A
principle of class design is to maintain the stability of the public
interface of the class. The implementation of a class should not be a
concern for its users.
The use of protected variables in a class are not recommended, since
its variables become visible to its derived classes. The names of
types or variables in a base class may then not be changed since the
derived classes may depend on them. If a derived class, for some
reason, must access data in its base class, one solution may be to
make a special protected interface in the base class, containing
functions which return private data. This solution would not imply any
degradation of performance if the functions are defined inline.
The use of structs is also discouraged since these only contain public
data. In interfaces with other languages (such as C), it may, however,
be necessary to use structs.
Exception to Rule 22: In interfaces with other languages (such as C),
it may be necessary to use structs having public data.
Example 22 The correct way to encapsulate data so that future
changes are possible.
// Original class:
class Symbol {};
class OldSymbol : public Symbol {};
class Priority
{
public:
// returns pd
int priority();
// returns symbol
class Symbol* getSymbol() const;
// ...
private:
int pd;
OldSymbol symbol;
};
// Modified class:
// The programmer has chosen to change the private data from an int
// to an enum. A user of the class `Priority' does not have to change
// any code, since the enum return-value from the member function
// priority() is automatically converted to an int.
class Symbol {};
class NewSymbol : public Symbol {};
enum Priority { low, high, urgent };
class Priority
{
public:
// Interface intact through implicit cast, returns priority_data
Priority priority();
// Interface intact, object of new subclass to symbol returned
class Symbol* getSymbol() const;
// ...
private:
Priority priority_data; // New representation/name of internal data
NewSymbol symbol;
};
7.2 Inline Functions
Back to index
Rec. 29 Access functions are to be inline.
Rec. 30 Forwarding functions are to be inline.
Rec. 31 Constructors and destructors must not be inline.
The normal reason for declaring a function inline is to improve its
performance.
Small functions, such as access functions, which return the value of a
member of the class and so-called forwarding functions which invoke
another function should normally be inline.
Correct usage of inline functions may also lead to reduced size of
code.
Warning: functions, which invoke other inline functions, often become
too complex for the complier to be able to make them inline despite
their apparent smallness.
This problem is especially common with constructors and destructors. A
constructor always invokes the constructors of its base classes and
member data before executing its own code. Always avoid inline
constructors and destructors!
7.3 Friends
Back to index
Rec. 32 Friends of a class should be used to provide additional
functions that are best kept outside of the class.
Operations on an object are sometimes provided by a collection of
classes and functions.
A friend is a nonmember of a class, that has access to the nonpublic
members of the class. Friends offer an orderly way of getting around
data encapsulation for a class. A friend class can be advantageously
used to provide functions which require data that is not normally
needed by the class.
Suppose there is a list class which needs a pointer to an internal
list element in order to iterate through the class. This pointer is
not needed for other operations on the list. There may then be reason,
such as obtaining smaller list objects, for an list object not to
store a pointer to the current list element and instead to create an
iterator, containing such a pointer, when it is needed.
One problem with this solution is that the iterator class normally
does not have access to the data structures which are used to
represent the list (since we also recommend private member data).
By declaring the iterator class as a friend, this problem is avoided
without violating data encapsulation.
Friends are good if used properly. However, the use of many friends
can indicate that the modularity of the system is poor.
7.4 const Member Functions
Back to index
Rule 23 A member function that does not affect the state of an object
(its instance variables) is to be declared const.
Rule 24 If the behaviour of an object is dependent on data outside
the object, this data is not to be modified by const member functions.
Member functions declared as const may not modify member data and are
the only functions which may be invoked on a const object. (Such an
object is clearly unusable without const methods). A const declaration
is an excellent insurance that objects will not be modified (mutated)
when they should not be. A great advantage that is provided by C++ is
the ability to overload functions with respect to their const-ness.
(Two member functions may have the same name where one is const and
the other is not).
Non-const member functions are sometimes invoked as so-called
'lvalues' (as a location value at which a value may be stored). A
const member function may never be invoked as an 'lvalue'.
The behaviour of an object can be affected by data outside the object.
Such data must not be modified by a const member function.
At times, it is desirable to modify data in a const object (such a
having a cache of data for performance reasons). In order to avoid
explicit type conversions from a const type to a non-const type, the
only way is to store the information outside the object. (See example
55). This type of data should be seen as external data which does not
affect the behaviour of the class.
Exception to Rule 23: No exceptions.
Exception to Rule 24: No exceptions.
Example 23 const-declared access functions to internal data in a
class
class SpecialAccount : public Account
{
public:
int insertMoney();
// int getAmountOfMoney(); No! Forbids ANY constant object to
// access the amount of money.
int getAmountOfMoney() const; // Better!
// ...
private:
int moneyAmount;
};
Example 24 Overloading an operator/function with respect to
const-ness
#include
#include
static unsigned const cSize = 1024;
class InternalData {};
class Buffer
{
public:
Buffer( char* cp );
// Inline functions in this class are written compactly so the example
// may fit on one page. THIS is NOT to be done in practice (See Rule 21).
// A. non-const member functions: result is an lvalue
char& operator[]( unsigned index ) { return buffer[index]; }
InternalData& get() { return data; }
// B. const member functions: result is not an lvalue
char operator[]( unsigned index ) const { return buffer[index]; }
const InternalData& get() const { return data; }
private:
char buffer[cSize];
InternalData data;
};
inline Buffer::Buffer( char* cp )
{
strncpy( buffer , cp , sizeof( buffer ) );
}
main()
{
const Buffer cfoo = "peter"; // This is a constant buffer
Buffer foo = "mary"; // This buffer can change
foo[2]='c'; // calls char& Buffer::operator[](unsigned)
cfoo[2] = 'c' // ERROR: cfoo[2] is not an lvalue.
// cfoo[2] means that Buffer::operator[](unsigned) const is called.
cout << cfoo[2] << ":" << foo[2] << endl; // OK! Only rvalues are needed
foo.get() = cfoo.get();
cfoo.get() = foo.get(); // ERROR: cfoo.get() is not an lvalue
}
7.5 Constructors and Destructors
Back to index
Rule 25 A class which uses "new" to allocate instances managed by the
class, must define a copy constructor.
Rule 26 All classes which are used as base classes and which have
virtual functions, must define a virtual destructor.
Rec. 33 Avoid the use of global objects in constructors and
destructors.
A copy constructor is recommended to avoid surprises when an object is
initialized using an object of the same type. If an object manages the
allocation and deallocation of an object on the heap (the managing
object has a pointer to the object to be created by the class'
constructor), only the value of the pointer will be copied. This can
lead to two invocations of the destructor for the same object (on the
heap), probably resulting in a run-time error.
The corresponding problem exists for the assignment operator (`=').
See 7.6: Assignment Operators.
If a class, having virtual functions but without virtual destructors,
is used as a base class, there may be a surprise if pointers to the
class are used. If such a pointer is assigned to an instance of a
derived class and if delete is then used on this pointer, only the
base class' destructor will be invoked. If the program depends on the
derived class' destructor being invoked, the program will fail.
In connection with the initialization of statically allocated objects,
it is not certain that other static objects will be initialized (for
example, global objects). This is because the order of initialization
of static objects which is defined in various compilation units, is
not defined in the language definition. There are ways of avoiding
this problem, all of which require some extra work.
You must know what you are doing if you invoke virtual functions from
a constructor in the class. If virtual functions in a derived class
are overridden, the original definition in the base class will still
be invoked by the base class' constructor. Override, then, does not
always work when invoking virtual functions in constructors. See
Example 30.
Exception to Rule 25: Sometimes, it is desired to let objects in a
class share a data area. In such a case, it is not necessary to define
a copy constructor. Instead, it is necessary to make sure that this
data area is not deallocated as long as there are pointers to it.
Exception to Rule 26: No exceptions.
Example 25 Definition of a "dangerous" class not having a copy
constructor
#include
class String
{
public:
String( const char* cp = ""); // Constructor
~String(); // Destructor
// ...
private:
char* sp;
// ...
};
String::String(const char* cp) : sp( new char[strlen(cp)] ) // Constructor
{
strcpy(sp,cp);
}
String::~String() // Destructor
{
delete sp;
}
// "Dangerous" String class
void
main()
{
String w1;
String w2 = w1;
// WARNING: IN A BITWISE COPY OF w1::sp,
// THE DESTRUCTOR FOR W1::SP WILL BE CALLED TWICE:
// FIRST, WHEN w1 IS DESTROYED; AGAIN, WHEN w2 IS DESTROYED.
}
Example 26 "Safe" class having copy constructor and default
constructor
#include
class String
{
public:
String( const char* cp = ""); // Constructor
String( const String& sp ); // Copy constructor
~String(); // Destructor
// ...
private:
char* sp;
// ...
};
String::String( const char* cp ) : sp( new char[strlen(cp)] ) // Constructor
{
strcpy(sp,cp);
}
String::String( const String& stringA ) : sp( new char[strlen(stringA.sp)] )
{
strcpy(sp,stringA.sp);
}
String::~String() // Destructor
{
delete sp;
}
// "Safe" String class
void
main()
{
String w1;
String w2 = w1; // SAFE COPY: String::String( const String& ) CALLED.
}
Example 27 Definitions of classes not having virtual destructors
class Fruit
{
public:
~Fruit(); // Forgot to make destructor virtual!!
// ...
};
class Apple : public Fruit
{
public:
~Apple(); // Destructor
// ...
};
// "Dangerous" usage of pointer to base class
class FruitBasket
{
public:
FruitBasket(); // Create FruitBasket
~FruitBasket(); // Delete all fruits
// ...
void add(Fruit*); // Add instance allocated on the free store
// ...
private:
Fruit* storage[42]; // Max 42 fruits stored
int numberOfStoredFruits;
};
void
FruitBasket::add(Fruit* fp)
{
// Store pointer to fruit
storage[numberOfStoredFruits++] = fp;
}
FruitBasket::FruitBasket() : numberOfStoredFruits(0)
{
}
FruitBasket::~FruitBasket()
{
while (numberOfStoredFruits>0)
{
delete storage[--numberOfStoredFruits]; // Only Fruit::~Fruit is called !!
}
}
Example 28 Dangerous use of static objects in constructors
// Hen.hh
class Egg;
class Hen
{
public:
Hen(); // Default constructor
~Hen(); // Destructor
// ...
void makeNewHen(Egg*);
// ...
};
// Egg.hh
class Egg { };
extern Egg theFirstEgg; // defined in Egg.cc
// FirstHen.hh
class FirstHen : public Hen
{
public:
FirstHen(); // Default constructor
// ...
};
extern FirstHen theFirstHen; // defined in FirstHen.cc
// FirstHen.cc
FirstHen theFirstHen; // FirstHen::FirstHen() called
FirstHen::FirstHen()
{
// The constructor is risky because theFirstEgg is a global object
// and may not yet exist when theFirstHen is initialized.
// Which comes first, the chicken or the egg ?
makeNewHen(&theFirstEgg);
}
Example 29 One way of ensuring that global objects have been
initialized
// WARNING!!! THIS CODE IS NOT FOR BEGINNERS !!!
// PortSetup.hh
class PortSetup
{
public:
PortSetup(); // Constructor: initializes flag
void foo(); // Only works correctly if flag is 42
private:
int flag; // Always initialized to 42
};
extern PortSetup portSetup; // Must be initialized before use
// Create one instance of portSetupInit in each translation unit
// The constructor for portSetupInit will be called once for each
// translation unit. It initializes portSetup by using the placement
// syntax for the "new" operator.
static
class PortSetupInit
{
public:
PortSetupInit(); // Default constructor
private:
static int isPortSetup;
} portSetupInit;
// PortSetup.cc
#include "PortSetup.hh"
#include
// ...
PortSetupInit::PortSetupInit() // Default constructor
{
if (!isPortSetup)
{
new (&portSetup) PortSetup;
isPortSetup = 1;
}
}
Example 30 Override of virtual functions does not work in the base
class' constructors
class Base
{
public:
Base(); // Default constructor
virtual void foo() { cout << "Base::foo" << endl; }
// ...
};
Base::Base()
{
foo(); // Base::foo() is ALWAYS called.
}
// Derived class overrides foo()
class Derived : public Base
{
public:
virtual void foo() { cout << "Derived::foo" << endl; } //foo is overridden
// ...
};
main()
{
Derived d; // Base::foo() called when the Base-part of
// Derived is constructed.
}
7.6 Assignment Operators
Back to index
Rule 27 A class which uses "new" to allocate instances managed by the
class, must define an assignment operator.
Rule 28 An assignment operator which performs a destructive action
must be protected from performing this action on the object upon which
it is operating.
Rec. 34 An assignment operator ought to return a const reference to
the assigning object.
An assignment is not inherited like other operators. If an assignment
operator is not explicitly defined, then one is automatically defined
instead. Such an assignment operator does not perform bit-wise copying
of member data; instead, the assignment operator (if defined) for each
specific type of member data is invoked. Bit-wise copying is only
performed for member data having primitive types.
One consequence of this is that bit-wise copying is performed for
member data having pointer types. If an object manages the allocation
of the instance of an object pointed to by a pointer member, this will
probably lead to problems: either by invoking the destructor for the
managed object more than once or by attempting to use the deallocated
object. See also Rule 25.
If an assignment operator is overloaded, the programmer must make
certain that the base class' and the members' assignment operators are
run.
A common error is assigning an object to itself (a = a). Normally,
destructors for instances which are allocated on the heap are invoked
before assignment takes place. If an object is assigned to itself, the
values of the instance variables will be lost before they are
assigned. This may well lead to strange run-time errors. If a = a is
detected, the assigned object should not be changed.
If an assignment operator returns "void", then it is not possible to
write a = b = c. It may then be tempting to program the assignment
operator so that it returns a reference to the assigning object.
Unfortunately, this kind of design can be difficult to understand. The
statement (a = b) = c can mean that a or b is assigned the value of c
before or after a is assigned the value of b. This type of code can be
avoided by having the assignment operator return a const reference to
the assigned object or to the assigning object. Since the returned
object cannot be placed on the left side of an assignment, it makes no
difference which of them is returned (that is, the code in the above
example is no longer correct).
Exception to Rule 27: Sometimes, it is desirable to allow objects in a
class to share a data area. In such cases, it is not necessary to
define an assignment operator. Instead, it is necessary to make sure
that the shared data area is no deallocated as long as there are
pointers to it.
Exception to Rule 28: No exceptions.
Example 31 Incorrect and correct return values from an assignment
operator
void
MySpecialClass::operator=( const MySpecialClass& msp ); // Well ...?
MySpecialClass&
MySpecialClass::operator=( const MySpecialClass& msp ); // No!
const MySpecialClass&
MySpecialClass::operator=( const MySpecialClass& msp ); // Recommended
Example 32 Definition of a class with an overloaded assignment
operator
class DangerousBlob
{
public:
const DangerousBlob& operator=( const DangerousBlob& dbr );
// ...
private:
char* cp;
};
// Definition of assignment operator
const DangerousBlob&
DangerousBlob::operator=( const DangerousBlob& dbr )
{
if ( this != &dbr ) // Guard against assigning to the "this" pointer
{
delete cp; // Disastrous if this == &dbr
}
// ...
}
7.7 Operator Overloading
Back to index
Rec. 35 Use operator overloading sparingly and in a uniform manner.
Rec. 36 When two operators are opposites (such as == and !=), it is
appropriate to define both.
Operator overloading has both advantages and disadvantages. One
advantage is that code which uses a class with overloaded operators
can be written more compactly (more readably). Another advantage is
that the semantics can be both simple and natural. One disadvantage in
overloading operators is that it is easy to misunderstand the meaning
of an overloaded operator (if the programmer has not used natural
semantics). The extreme case, where the plus-operator is re-defined to
mean minus and the minus-operator is re-defined to mean plus, probably
will not occur very often, but more subtle cases are conceivable.
Designing a class library is like designing a language! If you use
operator overloading, use it in a uniform manner; do not use it if it
can easily give rise to misunderstanding.
If the operator != has been designed for a class, then a user may well
be surprised if the operator == is not defined as well.
7.8 Member Function Return Types
Back to index
Rule 29 A public member function must never return a non-const
reference or pointer to member data.
Rule 30 A public member function must never return a non-const
reference or pointer to data outside an object, unless the object
shares the data with other objects.
By allowing a user direct access to the private member data of an
object, this data may be changed in ways not intended by the class
designer. This may lead to reduced confidence in the designer's code:
a situation to be avoided.
A worse risk is having pointers which point to deallocated memory.
Rule 29 and Rule 30 attempt to avoid this situation.
Note that we do not forbid the use of protected member functions which
return a const reference or pointer to member data. If protected
access functions are provided, the problems described in 7.1 are
avoided.
Exception to Rule 29: No exceptions.
Exception to Rule 30: No exceptions.
Example 33 Never return a non-const reference to member data from a
public function.
class Account
{
public:
Account( int myMoney ) : moneyAmount( myMoney ) {};
const int& getSafeMoney() const { return moneyAmount; }
int& getRiskyMoney() const { return moneyAmount; } // No!
// ...
private:
int moneyAmount;
};
Account myAcc(10); // I'm a poor lonesome programmer a long way from home
myAcc.getSafeMoney() += 1000000; // Compilation error: assignment to constant
myAcc.getRiskyMoney() += 1000000; // myAcc::moneyAmount = 1000010 !!
7.9 Inheritance
Back to index
Rec. 37 Avoid inheritance for parts-of relations.
Rec. 38 Give derived classes access to class type member data by
declaring protected access functions.
A common mistake is to use multiple inheritance for parts-of relations
(when an object consists of several other objects, these are inherited
instead of using instance variables. This can result in strange class
hierarchies and less flexible code. In C++ there may be an arbitrary
number of instances of a given type; if inheritance is used, direct
inheritance from a class may only be used once.
A derived class often requires access to base class member data in
order to create useful member functions. The advantage in using
protected member functions is that the names of base class member data
are not visible in the derived classes and thus may be changed. Such
access functions should only return the values of member data
(read-only access). This is best done by simply invoking const
functions for the member data.
The guiding assumption is that those who use inheritance know enough
about the base class to be able to use the private member data
correctly, while not referring to this data by name. This reduces the
coupling between base classes and derived classes.
--
Enjoy Linux!
-----It's FREE!-----
※ 来源:.紫 丁 香 bbs.hit.edu.cn.[FROM: mtlab.hit.edu.cn]
Powered by KBS BBS 2.0 (http://dev.kcn.cn)
页面执行时间:412.587毫秒