A class containing no data members and no member functions is an empty class. It's defined like this:
class PlaceHolder {};
What's it good for? It can serve as a place-holder for a yet-to-be defined class. For instance, it can be used as an interface class serving as a base for other classes; instead of waiting for it's full implementation to be completed, it can be used this way in the interim. An empty class can also be used as a means of forcing derivation relationship among classes which are not originally descended from one base class. This is called a bottom-up design. Finally, it can be used to create a type for a dummy argument to distinguish between overloaded version of a function. In fact, operator new is overloaded exactly like that:
#include
using namespace std;
void main()
{
try {
int *p = new int[100]; //standard exception-throwing version of new
}
catch(bad_alloc & new_failure) {/*..*/}
int *p = new (nothrow) int [100]; //standard exception-free version of new; returns NULL if fails
if (p) {/*..*/}
}
The nothrow arguments is of type nothrow_t, which is an empty class by itself.
165. Calling a Function at Program's Startup
Certain applications need to invoke startup functions that run before the application starts. For example, polling, billing, and logger functions must be invoked before the actual program begins. The easiest way to achieve this is by calling these functions from a constructor of a global object. Because global objects are conceptually constructed before the program's outset, these functions will run before main() starts. For example:
class Logger
{
public:
Logger()
{
log_user_activity();
}
};
Logger log; // global instance
int main()
{
record * = read_log();
//.. application code
}
The global object log is constructed before main starts. During its construction, log invokes the function log_user_activity(). When main() starts, it can read data from the log file.
166. Structs as a shorthand for public classes
Traditionally, structs serve as data aggregates. However, in C++ a struct can have constructor(s), destructor and member functions just like a class. The only difference between the two is the default access type: a class has by default private access type to its members and derived objects, whereas a struct has by default public access to its members and derived objects. Therefore, structs can be used as a shorthand for classes whose members are all public rather than being confined to the traditional role of 'plain old data' containers. A good example for that is the case of abstract classes:
struct File { //all members are implicitly public
virtual int Read() = 0;
File(FILE *);
virtual ~File();
};
class TextFile: File {//implicit public inheritance; File is a struct
string path; //private member
//...other private members
public:
int Flush();
int Read();
};
class UnicodeFile : TextFile { //implicit private inheritance
//...
};
167. A base class destructor should be virtual
If a class may serve as a base class for others, its destructor should be virtual. This ensures RTTI support for objects of this class and objects derived from it, and more important, you ensure that the correct destructor is always called, even in the following case:
class Base{
char *p;
Base() { p = new char [200]; }
~ Base () {delete [] p; }
//...
};
class Derived : public Base {
char *q;
Derived() { q = new char[300]; }
~Derived() { delete [] q; }
//...
};
void destroy (Base & b) { delete &b; }
int main()
{
Base *pb = new Derived(); //200 + 300 bytes have been allocated
//... meddle with pb
destroy (*pb); //Oops! Base's destructor is called but not //Derived's; memory leak.
//Were Base's destructor virtual, the //correct destructor would be called.
}
168. 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.
169. Creating an Array of Objects in the Absence of a Default Constructor
Consider the following class:
class A // lacks default constructor
{
public:
A(int x, int y);
};
Class A doesn't have a default constructor. Therefore, the following array declaration will not compile:
A arr[10]; //error; no default constructor for class A
You can still create arrays of class A. However, you'll have to use an explicit initialization list in this case:
A a[3] = { A(0,0), A(0,0), A(0,0) }; // ok
Note that in the declaration of the array a, every element must be explicitly initialized. This is tedious, especially if you create a large array. Furthermore, you cannot create dynamic arrays of objects of a class that lacks a default constructor:
A * pa = new A[2]; // error
Therefore, if you define a constructor for a class, remember to define a default constructor as well.
170. What's Wrong with Inheriting from a Class that Has no Virtual Destructor?
Classes having a non-virtual destructor aren't meant to be derived from (such classes are usually known as "concrete classes"). std::string, std::complex, and all STL containers are concrete classes. Why is inheriting from such classes not recommended? When you use public inheritance, you create an is-a relationship between the base class and the derived class. Consequently, pointers and references to base can actually point to a derived object. However, because the destructor isn't virtual, C++ will not call the entire destructor chain when you delete such an object. Foe example:
class A
{
public:
~A() // non virtual
{
// ...
}
};
class B: public A{ // bad; inheriting a non virtual dtor
public:
~B()
{
// ...
}
};
int main()
{
A * p = new B; // seemingly OK
delete p; // oops, B's dtor not called!
}
The result of failing to invoke an object's destructor is undefined behavior. Therefore, you shouldn't use publicly inherit from concrete classes.
171. Using Memset On Class Objects
It's common practice in C, to do a memset on structures, in order to initialize all member variables to some default value, usually NULL. Similarly, you can use memset to initialize class objects. But what happens if the class contains some virtual functions?
Let's take an example:
class GraphicsObject{
protected:
char *m_pcName;
int m_iId;
//etc
public:
virtual void Draw() {}
virtual int Area() {}
char* Name() { return m_pcName;}
};
class Circle: public GraphicsObject{
void Draw() { /*draw something*/ }
int Area() { /*calculate Area*/ }
};
void main()
{
GraphicsObject *obj = new Circle; //Create an object
memset((void *)obj,NULL,sizeof(Circle)); //memset to 0
obj->Name(); //Non virtual function call works fine
obj->Draw(); //Crashes here
}
This results in a crash, because every object of a class containing virtual functions contains a pointer to the virtual function table(vtbl). This pointer is used to resolve virtual function calls, at run time and for dynamic type casting. The pointer is hidden, and is not accessible to programmers by normal means. When we do a memset, the value of this pointer also gets overwritten, which, in turn, results in a crash, if a virtual function is called.
To avoid such mysterious crashes, memset should not be used on the objects of a class with virtual functions. Instead use the default constructor or an init routine to initialize member variables.
172. 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.
No comments:
Post a Comment