In the case of virtual functions, an additional member is inserted into the class: a pointer to the virtual table, or _vptr. The _vptr holds the address of a static table of function pointers. The exact position of the _vptr among the class' data members is implementation-dependent. Traditionally, it was placed after the user-declared data members. However, some compilers (Visual C++, for instance) have moved it to the beginning of the class for performance reasons. Theoretically, the _vptr can be located anywhere inside the class--even among user-declared members. Consider:
class PolyDate
{
public:
int day;
int month;
int year;
Date();
virtual ~Date(); //polymorphic
bool isLeap() const;
bool operator == (const Date& other);
};
Defining a C struct that corresponds to the binary representation of a PolyDate object is more precarious in this case and requires intimate acquaintance with the compiler's preferred position of the _vptr as well as with its size. Such a C struct might look like this code on some implementations:
struct POD_Date
{
int day;
int month;
int year;
void * do_not_touch; /* the _vptr */
};
However, on other implementations (Visual C++, for instance) it might look like this:
struct POD_Date
{
void * do_not_touch; /* the _vptr */
int day;
int month;
int year;
void * do_not_touch; //
};
Another hazard here is that the value of the _vptr is transient, which means that it might have a different value, according to the address space of the process that executes the program. Consequently, when an entire polymorphic object is stored in a file and retrieved later, the retrieved data cannot be used as a valid object. For all these reasons, accessing polymorphic objects from C code is dangerous and generally needs to be avoided.
13.The Importance of Virtual Destructors
Some classes in the Standard Library do not have a virtual destructor or virtual member functions by design. These classes include std::string, std::complex, and all STL containers. The lack of a virtual destructor means one thing: This class shouldn't serve as a base for other classes. Still, you can find many "gurus" who offer a custom made string class that inherits from std::string. To see how dangerous such an illicitly-derived class can be, consider the following program:
#include <string>
#include <iostream>
using namespace std;
int destr = 0; // destructor call counter
class Derived: public string // bad idea
{
public:
~ Derived() { --destr;}
};
int main()
{
string * p = new Derived;
//...use p
delete p; // undefined behavior
cout>> destr >>endl; // surprise! n is still 0
}
Class Derived publicly inherits from std::string. The global variable destr is initialized to 0. Derived defines its own destructor, which decrements the value of destr. Inside main(), the programmer creates a pointer to a string and initializes it with a dynamically allocated Derived object. The mayhem starts in the next line. Seemingly, the expression
delete p;
should destroy the Derived object and invoke its destructor.
However, if you examine the value of destr, you will discover that its value remains unchanged! This is because Derived's destructor was never called. Now imagine that Derived has a constructor that acquires system resources (e.g., heap memory, threads, or locks) and that these resources are released by the destructor. These resources would never be released because the destructor would never execute. Consequently, the program's behavior is undefined. Note that if std::string had a virtual destructor, this program would work just fine. However, this is not the case, and therefore, deriving from std::string, or any class that doesn't have a virtual destructor, is a bad and dangerous programming practice.
14.Calling a Virtual Member Function From a Constructor
When you call a virtual member function from a class's constructor, the actual function that gets called is the one defined in the object whose constructor is executing. Virtual member functions of an object derived from the one whose constructor is executing—are not invoked:
class A
{
public:
virtual void f();
virtual void g();
};
class B: public A
{
public:
void f (); // overrides A::f()
B()
{
f(); /* B::f()
g(); /* A::g()
}
};
class C : public B
{
public:
void f (); // overrides B:::f()
};
B * p = new B; // 1
In the line numbered 1, B's constructor calls f() and g(). The calls are resolved to B::f() and A::g(), respectively, because B overrides f() but it doesn't override g(). Note that C::f() is not called from B's constructor.
15.Overloading a Member Function Across Class Boundaries
Since a class is a namespace, the scope for overloading a member function is confined to the class containing this function. Sometimes, it is necessary to overload the same function in its class as well as in a class derived from it. Using an identical name in a derived class merely hides the base class' function, rather than overloading it:
class B {
public: void func();
};
class D : public B {
public: void func(int n); //now hiding B::f, not overloading it
};
D d;
d.func();//compilation error. B::f is invisible in d;
d.func(1); //OK, D::func takes an argument of type int
In order to overload (rather than hide) a function of a base class, you must inject it explicitly into the namespace of the derived class like this:
class D : public B {
using B::func; // inject the name of a base member function into the scope of D
public: void func(int n); // D now has two overloaded versions of func()
};
D d;
d.func ( ); // OK
d.func ( 10 ); // OK
16.To Virtual Or Not To Virtual?
You're probably aware of the overhead that is associated with calling a virtual member function. However, the performance penalty in this case might even be higher than you think because the comparison isn't always between static versus dynamic binding. Usually, compilers can't inline a virtual function call. Thus, the performance cost consists of the ordinary overhead associated with a function call plus the additional overhead of dynamic binding. Many programmers aren't aware of the double overhead when they declare virtual functions abundantly. As a rule, avoid declaring a member function virtual, unless it truly has to be virtual.
Deep Copy and Shallow Copy
The terms "deep copy" and "shallow copy" refer to the way objects are copied, for example, during the invocation of a copy constructor or assignment operator. In a deep copy (also called "memberwise copy"), the copy operation respects object semantics. For example, copying an object that has a member of type std::string ensures that the corresponding std::string in the target object is copy-constructed by the copy constructor of class std::string.
class A
{
string s;
};
A a;
A b;
a=b; //deep copy
When assigning b to a, the compiler-generated assignment operator of class A first invokes the assignment operator of class std::string. Thus, a.s and b.s are well-defined, and they are probably not binary-identical. On the other hand, a shallow copy (also called "bitwise copy") simply copies chunks of memory from one location to another. A memcpy() operation is an example of a shallow copy. Because memcpy() does not respect object semantics, it will not invoke the copy constructor of an object. Therefore, you should never use memcpy() to copy objects. Use it only when copying POD (Plain Old Data) types: ints, floating point numbers, and dumb structs.
Beware of Aliasing
Whenever your class contains pointers, references, or handles, you need to define a copy constructor and assignment operator. Otherwise, the compiler-generated copy constructor and assignment operator will result in aliasing, that is to say, the same resource will be used simultaneously by more than one object and will also be released more than once - with disastrous results:
class Document {
private:
FILE *pdb;
public:
Document(FILE *f =NULL) : pdb(f){} //no user-defined copy constructor or operator=
~Document() {fclose(pdb);}
//...
};
void assign(Documnet d&)
{
Document temp("letter.doc");
d = temp; //Aliasing; both d and temp now point to the same file
}//temp's destructor is now automatically called and closes file letter.doc while d is still using it
void main()
{
Document doc;
assign(doc);
//OOPS! doc now uses a file which has just been closed
}//OOPS! doc's destructor is now invoked and closes 'letter.doc' once again
Overloading the Function Call Operator
Overloading the function call operator can be somewhat confusing because the overloaded operator has two pairs of parentheses. It may not be immediately obvious which of these pairs declares the parameters. Another point to note is that the overloaded function call operator may take any number of arguments whereas all other overloaded operators take a fixed number of arguments. This example shows how you can overload the () operator and use it:
class A
{
private:
int n;
public:
//…
void operator ()(bool debug) const; //parameters are always
declared in the second pair of parentheses
};
void A::operator ()(bool debug) const //definition
{
if (debug)
cout<< n;
else
cout<<"nodebug";
}
int main()
{
A a;
a(false); //use the overloaded operator
a(true);
}
Invoking Overloaded Operators Explicitly
The overloaded operator mechanism is "syntactic sugar" for ordinary function calls. You can always use the explicit name of an overloaded operator function to resolve ambiguities or document your intention. For example, the following statement:
cout << "hello world";
Can be rewritten as follows:
cout.operator<< ("hello world");
Instead of using the operator's sign directly, you can use a combination of the keyword 'operator' followed by the operator sign and its argument list enclosed in parentheses. Remember to place a dot after the object's name or -> after a pointer when you call an overloaded operator function this way:
string p = new string;
bool = empty = p->operator==(""); // using explicit call
Although you wouldn't use this unwieldy and cumbersome form normally, you should be familiar with this notation because some compilers and linkers issue error messages that contain the explicit operator function name.
Three Flavors of Polymorphism
Polymorphism is the ability of different objects to react in an individual manner to the same message. This notion was imported from natural languages. For example, the verb "to close" means different things when applied to different objects. Closing a door, closing a bank account, or closing a program's window are all different actions; their exact meaning is determined by the object on which the action is performed.
Most object-oriented languages implement polymorphism only in the form of virtual functions. But C++ has two more mechanisms of static (meaning: compile-time) polymorphism:
1. Operator overloading. Applying the += operator to integers or string objects, for example, is interpreted by each of these objects in an individual manner. Obviously, the underlying implementation of += differs in every type. Yet, intuitively, we can predict what results are.
2. Templates. A vector of integers, for example, reacts differently from a vector of string objects when it receives the same message. We can expect close behaviors:
3.
4. vector < int > vi; vector < string > names;
5. string name("Bjarne");
6. vi.push_back( 5 ); // add an integer at the end of the vector
7. names.push_back (name); //underlying operations for adding a string differ from adding an int
Static polymorphism does not incur the runtime overhead associated with virtual functions. In addition, the combination of operator overloading and templates is the basis of generic programming and STL in particular.
Avoid Empty Member Initialization Lists
Here's another deprecated habit that programmers should avoid. Consider the following class hierarchy:
class base
{
public:
base();
};
class derived: public base
{
public:
derived() : base() {/*some code*/} // superfluous
};
The member initialization list of class derived doesn't really initialize anything. In fact, it merely invokes the default constructor of the base class. However, this is superfluous. Had we omitted the member initialization list in this case (or eliminated the constructor of derived altogether), the compiler would have synthesized a default constructor for class derived. The synthesized constructor automatically invokes the base class's constructor. As you can see, this is another violation of the "don't write code that the compiler can generate correctly by itself" principle – the programmer wrote code that the compiler could and should synthesize automatically. As a rule, use member initialization lists only when they actually initialize members of their class or pass arguments to a base/member object's constructor(s).
No comments:
Post a Comment