Home Company Services Experience Process Articles FAQs Contact Us

C++ Notes


1.    Member Functions

Auto-Generated Members

A C++ compiler will automatically generate the following member functions for any class (except the one with reference and const data members) that does not provide the definitions itself:

-         default constructor (only when there are no other constructors)

-         copy constructor with shallow-copy (only when used)

-         non-virtual destructor with shallow-copy (only when used)

-         assignment operator with shallow-copy (only when used)

-         read-only address-of operator (only when used)

-         modifiable address-of operator (only when used)

 

To prevent the C++ compiler from generating the undesirable member functions (e.g. shallow-copy assignment operator and the corresponding destructor, constructors without initialization list), define them yourself. If you don’t want to define those functions and don’t want the C++ compiler to generate them either, define the functions and make them private.

Copy Constructor and Copy Assignment

            class A {

                        public:

                                    A (const A& a);

                                    A& operator= (const A& a);

                        private:

                                    int m_id;

            };

            A::A (const A& a) {

                        m_id = a.m_id;

            }

            A& A::operator= (const A& a) {

                        if (this != &a) {

                                    m_id = a.m_id;

                        }

                        return *this;

            }

 

Copy constructor is used for variable definition, pass-by-value (function argument, function return and exception handling) and member initialization list. For example,

            A a1;

            A a2 = a1;        // Invoke copy constructor.

            const A operator + (const A* a1, const A* a2);            // pointer passed by value

            try { throw A; } catch (A&) { }           

// exception object passed by value even the type is defened as reference

 

Copy assignment is used when the content of an object is replaced by the content of another, for example,

            A a1;

            A a2

            a2 = a1;           // Invoke copy assignment.

Functions That Cannot Be Members

In order to preserve the natural usage syntax such as,

cout << x << endl;

Operator << cannot be a class member function, and the return and one of the arguments shall be ostream&. Furthermore, since the definition of operator << needs to access many internal data of the target object, it is convenient to declare operator << as a friend function of the target class.

            class A {

                private:

                    int m_id;

 

                friend ostream& operator << (ostream& os, const A& a);

            };

 

            ostream& operator << (ostream& os, const A& a)

            {

                os << a.m_id;

                return os;

            }

2.    Overloading

Function overloading is when two functions use the same names but with different signatures. Function signature includes the name, the constness, and the number, type and order of function arguments. Note that function signature does not include the return type.

 

Function overloading can be done at global level, name space level or at class level. For example,

            namespace gui {

                        void print (int);

                        void print (float);

                        class Panel {

                        public:

                                    void print (int);

                                    void print (float);

                        };

                        class Window {

                        public:

                                    const Panel* getPanel() const;

                                    Panel* getPanel();

                        };

            }

Functions defined at different levels or scopes won’t interfere with each other, with the exception that overloaded function in the derived class will hide the function of the same name in the base class, no matter the base class function is virtual or not. For example,

            class A { public: virtual void print (int); };

            class B : public A { public: void print (float); };

 

            B* pb = new B;

            pb->print(1);    // Error! A::print() is invisible in pb.

 

3.    Overriding

Overriding is when the derived class provides its own implementation for a function already defined (with the same signature) in the base class. The overriding functions are statically bound if the base class function is not virtual. Statically overriding functions are invoked based on the type of the object variable, not what the object really is.

 

            class A { public: void print (int i) { cout << “A:” << i << endl; } };

            class B : public A { public: void print (int i) { cout << “B:” << i << endl; } };

 

            A* pb = new B;

            Pb->print(1);                // A:1

 

Even though pb actually points to a class B object, since it is defined as pointer of class A, the print () of class A will be invoked. Note that statically overriding function in the derived class will hide all the functions with the same name in the base class, unless they are redefined in the derived class.

 

To invoke the function based on the actual object, dynamic overriding is needed, through virtual function; that is, define the base class function as virtual.

 

            class A { public: virtual void print (int i) { cout << “A:” << i << endl; } };

            class B : public A { public: void print (int i) { cout << “B:” << i << endl; } };

 

            A* pb = new B;

            Pb->print(1);                // B:1

 

Since virtual function is intended to be overridden, it is ok for virtual function not to have implementation at all (the so-called pure virtual function). The class has or inherits at least one pure virtual function cannot be instantiated (one way of becoming abstract class). Note you can still provide implementation for a pure virtual function, but the only way you can invoke that function is through the class name (followed by “::”).

4.    Polymorphism

The run-time invocation of virtual functions is implemented through the virtual function table. Roughly speaking, any class with at least one non-inline non-pure virtual function will have a virtual function table. The virtual function table, vtbl, contains one pointer for each virtual function. For the base class, the function pointer points to the implementation of the virtual function in by the base class. For the derived class, the function pointer points to the implementation of the virtual function provided by the derived class or by the base class if the derived class does not provide one. On the other hand, each object of the virtual function class contains a pointer to the virtual function table, vptr. When a virtual function is invoked on an object pointer or reference, the program follows the object’s vptr to the class’s vtbl, finds the function pointer in vtbl, and invokes the function. In this way, no matter what type the object reference or pointer is (the base class or the derived class), the function invoked is determined by the vtbl of the class of the actually object. A class won’t have vtbl unless it defines at least one virtual function.

 

Even though virtual functions are dynamically bound, the default parameters are not. If the base class virtual function uses a default parameter, the derived class virtual function uses a different default parameter, and the function is invoked on the derived class object using the base class pointer, the derived class implementation will be called with the default parameter defined in the base class function. So the lesson is not to assign default parameters to virtual functions or at least not to override virtual functions that use default parameters.

 

Dynamic binding won’t happen for member function arguments either; in other words, the check of arguments as part of signature matching is strictly static. For example,

            class A;

            class B : public A { };

            class C { public: void operation (B*); };

 

            A* ap = new B;

            C* cp = new C;

            cp->operation (ap); // Error! No match for operation (B*).

                                            // even though ap is actually of class B.

 

Polymorphism (or data abstraction) is basically the same interface with different implementations. Strictly speaking, polymorphism is only through the dynamic overriding of virtual functions in the public base class. A good practice is to separate the interface definition from the object implementation, using the so-called interface class or protocol class, that is, a base class contains only pure virtual functions and not data.

 

With the increasing popularity of template, people start to consider overriding virtual function the run-time polymorphism, and consider the parameterization of template the compile-time or parametric polymorphism. The difference is: you use run-time polymorphism to define different behaviors for different subtypes, and use parametric polymorphism to define a set of behaviors that are common for a suite of peer types.

 

Private inheritance is not considered polymorphism because the run-time binding or run-time type identification won’t happen for private inheritance. Private inheritance is the re-use of implementation. Compared with other re-use techniques, it has the advantage of accessing the protected members of the base class and overriding the virtual functions of the base class.

 

Polymorphism does not change the type of the pointer based on what the object really is. This would have direct impact on things like pointer arithmetic. For example, if the pointer p is the type of the base class, but points to the object which is actually an instance of the derived class, p++ will move the point by the size of the base class, not the derived class. If you assume p++ still points to an object of the derived class and try to invoke a function on p++, very likely you will get the wrong answer.

5.    Access Control

A friend function is declared inside a class as a friend but defined outside the class (either in the scope of the name space or another class). It does not matter whether the function is declared as private or public. It is considered part of the interface of the class.

 

The friends of a class include the friend functions of the class and the member functions of the friend classes of the class.

 

The private members can be used only by the member functions and the friends of the class. They cannot be used by any derived classes.

 

The protected members can only be used by the member functions and the friends of the class. Plus, for public and protected inheritances, the protected members of the base class can be used by the member functions and the friends of the derived class as well as further derived classes; for private inheritance, the protected members of the base class can only be used by the member functions and the friends of the derived class.

 

The public members can be used by any member functions as well as any non-member functions. Plus, for public and protected inheritance, the public members of the base class can be used by the member functions and the friends of the derived class as well as further derived classes; for private inheritance, the public members of the base class can only be used by the member functions and the friends of the derived class.

 

For private inheritance, only the member functions and the friends of the derived class can convert the pointer of the derived class to the pointer of the base class.

 

For protected inheritance, only the member functions and friends of the derived class as well as further derived classes can convert pointer of the derived class to the pointer to the base class.

 

For public inheritance, any function can convert the pointer of the derived class to the pointer to the base class. Note that run-time polymorphism (or virtual function dynamic binding) only works for public inheritance.

 

By default the class members are private, and class heritance is also private. On the contrary, the members of a struct are public by default.

6.     Data Types

Fundamental Types

Type bool is used to express the result of logical operations, e.g., bool b = 1==2. It has two values: true and false. When converted to integer, true is 1 and false is 0. When converted from integer, nonzero is true and 0 is false. When converted from pointer, nonzero value is true and 0 is false.

 

Character type char has 8 bits and 256 values. Whether the value is 0 to 255 or -127 to 127 is implementation dependent. The function int (char) will return the integer value of a char.

 

Type void is more of a syntactical symbol. Used as return of a function, it means the function won’t return anything. Used as a pointer, it means the pointer points to an unknown type.

 

C++ uses the smallest bit-field (and no larger than the size of an integer) to hold an enum type, and all the values represented by the bit-field are valid for the enum type even though they are not explicitly defined by the enum. For example, enum e1 { a = 1, b = 2 } will take 2 bits and all the integer values from 0 to 3, enum e2 { a = -1, b = 2 } will take 3 bits and all the integer values from -1 to 3. An integer within the valid range of an enum can be converted to the enum with function such as e1 (2) and e2 (7).

 

A string is assigned to char* s or const char* s. In either case, the size of the string is the number of characters plus one, and none of the characters in the string can be modified. That is, the definition of char* str = “Hello World!” has the same effect as const char* str = “Hello World!” - You cannot change the contents of str after it is initialized. This is different from an array of characters, defined as char ac [], which can be modified through array index or pointer. A string is also statically allocated, therefore when returned from a function (and the function exits), the string is still valid in memory.

            const char* getLogo () { return “Welcome!”; }

 

Note that C++ does not provide the built-in macro NULL as in C. It uses plain 0. Even though 0 can be considered either integer 0 or null pointer, if pass 0 as an argument to the function f, f (int) is the match, not f (void*) or f (char*) etc.

Type Conversion

The static_cast operator converts one pointer type to another “freely”, such as an enumeration to an integral type, or a floating-pointer type to an integral type. The result is very close to the C-style cast. It does not check if the conversion result is correct. The static_cast from one object pointer to another is valid only when the two objects belong to the same class hierarchy. The dynamic_cast converts pointer types with the run-time type identification. If the type conversion is successful, the converted pointer is returned; otherwise, a null pointer is returned. The dynamic_cast from one object pointer to another is valid only when the two objects belong to the same class hierarchy. The const_cast converts a const type pointer to a non-const type pointer or vice versa, but the const_cast may fail if the object is originally declared as const, not just passed in as a const argument, or the const is enforced by the registers. The reinterpret_cast handles conversions between unrelated types (usually for function pointers) and the result is almost always implementation dependent. Therefore reinterpret_cast is rarely portable.

 

            class A { public: virtual void doIt () = 0; }

            class B : public A { public: void doIt () { cout << “It is B.” << endl; } };

            class C : public A { public: void doIt () { cout << “It is C.” << endl; } };

 

            vector<A*> as;

            for (vector<A*>::iterator i = as.begin(); i != as.end(); ++i) {     

                        if (B* pb = dynamic_cast<B*> (*i)) pb->doIt ();

                        else if (C* pc = dynamic_cast<C*> (*i)) pc->doIt ();

                        else cout << “Unknown type.” << endl;

            }

The way of defining B* pb and C* pc in if-else here is only valid when used along with dynamic_cast.

 

Under certain circumstances, implicit type conversion can generate confusion (unwanted type conversion) and ambiguity (multiple constructors or conversion operators are equally qualified, multiple inheritance from the base classes that define the same member functions). To prevent the implicit type conversion, add key word explicit to the constructor. If the case of multiple-inheritance induced ambiguity, change accessibility won’t solve the problem. One way to solve that is to add base class name when invoking the method.

7.    Memory Management

Declaration Scope

A name declared outside any scope (namespace, class, struct, enum, function, try block, or any block of code bounded by “{” and “}”) is global, that is, it extends from the point of declaration to the end of the file (or can be extended to other files with “extern” declaration). Any name declared within a scope extends from the point of declaration to the end of the scope. For nested scopes, a name declared in the inner scope hides the same name declared in the outer scope. Global name can be hidden too, and only hidden global name can be accessed by explicitly using “::” in front of the name.

 

Within the same scope, a name cannot be declared twice, no matter they are declared as the same type or not. An “extern” declared name cannot be the same as a global name in the file. A function body cannot declare the same name as the argument name passed in since the arguments are considered in the same scope of the function body. Exception handling code has the similar situation.

Local Variables

            A a;

Local objects are stored on the stack of the invoked function (also called automatic objects). The objects are created when the definitions are encountered and destroyed when they are out of scope.

 

C++ guarantees that when a local variable goes out of scope, either normal exit or exception, the program will release the memory associated with that variable. This is useful to guarantee release of resource such as file handle and mutexes.

 

            class Lock {

            public:

                        Lock () { getMutex (); }

                        ~Lock () { releaseMutex () ; }

            };

 

            // Put a block just for the protected section.

            {

                        Lock lock;

                        // Put critical section here.

            }

Local Static Variables

            static A a;

Local static objects are allocated the first time the variable definition is encountered in the containing function. The objects are valid through all the subsequent calls to that function. For more than one static variable, the program remembers the sequence they are allocated and de-allocate them in the reverse order when the program exits.

Non-local Static Variables

They are static variables defined outside the scope of the functions, that is, they are global, namespace, file scope and member variables. They are constructed before main () is invoked and destructed after main () exits. Static member variable is declared in class definition and initialized once and only once outside the class definition but in the same file scope as the class definition. For example,

In file a.h,

            class A {

               public:

                  static int m_a;

               private:

                  static const int m_b;

            };

In file a.cpp,

            #include “a.h

            int A:m_a = 1;

            const int A::m_b = 2;

 

Access control of member variable (public, protected or private) does not constrain the initialization of static members. Note that static const integral member can also be initialized right at the declaration in the class definition (see below).

           

There is no control upon the sequence of initializing non-local static variables from different translation units. The solution for that is to wrap non-local static variables as local static variables, such as using the Singleton pattern.

Free Store

            A *ap = new A;

            delete ap;

Objects allocated by new are said to be on the free store, or on the heap, or in the dynamic memory. They exist beyond the scope of the variable and can be de-allocated by delete. Forgetting to free dynamically allocated memory is the major source of memory leak, especially during exceptions or other unexpected actions. STL provides template auto_ptr<> to deal with that issue. An automatic auto_ptr<> object wraps up any type of dynamically allocated object and de-allocate that object when it goes out of scope.

            template <class T>

            class auto_ptr {

            public:

                        auto_ptr (T* p) : m_p (p) { }

                        ~auto_ptr () { delete p; }

                        T* operator -> () const;

                        T& operator* () const;

            private:

                        T* m_p;

            };

 

            class A {

public:

                        void print () { cout << “Hello world.” << endl; }

};

 

{

auto_ptr<A> pa (new A);

pa->print ();                 // Hello world.

}

 

So the auto_ptr<> kinds of owns the dynamic object. Since auto_ptr<> does not have the bookkeeping capability (e.g., reference count), there can be no more than one auto_ptr<> for any given dynamic object. When auto_ptr<> is copied or assigned to another auto_ptr<>, the ownership of the underlying object is also transferred to the new auto_ptr<>, and the original auto_ptr<> ends up pointing to null. The content of the original auto_ptr<> is actually changed, in other words, auto_ptr<> can never by copied or assigned. That is why auto_ptr<> shall never be used as element of STL container.

 

Frequent allocation and de-allocation of dynamic memory may lead to memory fragmentation, which prevents the free memory blocks from being used because they are not contiguous. One way of minimizing fragmentation is to allocate big chunk of memory instead of small blocks, for example, allocate an array of objects as one contiguous chunk instead of allocating for each object individually. Another way is to reuse a pre-allocated memory pool instead of allocating and de-allocating every time the class is instantiated.

Member Variables

            class A {

                        string m_s;

                        B m_b;

                        A (const string& s, const B& b) : m_s (s), m_b (b) { }

            };

Constructors of member classes are called in the order they are declared in the containing the class, and before the constructor of the containing class. Destructors of member classes are called in the reverse order they are declared in the containing the class, and after the destructor of the containing class. Since the sequence is determined by the declaration order, not the order they are in the initialization list, it is a good habit to make the initialization list use the same order as the declaration.

 

The order of data member declaration also has impact on the size of the class because of memory alignment. Computer hardware fetches data in the unit of bytes determined by the width of data bus. On a 32-bit computer, data fetching is in the unit of four bytes. If a variable, for example, int, has four bytes and is declared across the boundary of two fetching units, the operating system will pad some bytes to the previous data until the int variable aligns with the second fetching unit, so that the hardware can fetch the int variable in just one maching operation. The result is, some bytes will be padded in and the total size is almost always larger than it shall be. To minimize padding, try to declare data member in the order that large data are naturally aligned and small data are located between large data.

 

            struct A {

                        bool b1;           // 1 byte + 3 pad bytes

                        int i2;                // 4 bytes

                        char c3;            // 1 byte + 3 pad bytes

            };

 

            struct B {

                        bool b1;           // 1 byte

                        char c2;            // 1 byte + 2 pad bytes

                        int i3;                // 4 bytes

            };

 

There are four ways to initialize the member variables:

(1)   Only static constant integral member (including bool and char) can be initialized in the class definition, with a constant-expression initializer.

class A {

            static const int m_id = 0;

            static const int m_year = getYear();       // Error, non-constant initializer

};

This is similar to but better than the older technique,

class A {

            enum { m_id = 0 };

};

(2)   Non-static constant variable, reference type, and class without default constructor can only be initialized in the initialization list.

class A {

            const int m_id;

            B b;      // Cannot be instantiated here so must be put in initialization list.

            B& rb;

            A (int id, const B& b) : m_id (id), b (id), rb (b) { }

};

(3)   Static members initialized in the implementation file before the main () or by static member functions.

class A {

            static int m_id;

            static const float m_float;

            static double m_double;

            static void init () { m_double = 1.0; }

            static void setId (int id);

};

            int A::m_id = 0;

            const A::m_float = 0.1;

            void A::setId (int id) { A::m_id = id; }

(4)   Other members can be initialized in the constructors or member functions.

 

Static data member of a class states that there is at most one copy of the data for and shared by all the instances of the class. Static data member is usually declared in class definition (the header file) and defined in class implementation (the .cpp file). However, if the data member is constant and is of type int, bool or char, it can be initialized with a value at declaration. C++ makes this exception so that the data member can be used later in the class definition (such as the size of an array), the same effect by an “enum” type. Static data member can be accessed through an object reference or the class name (separated by “::”).

Delete Object

Most C++ compilers support deleting a null pointer, which simply does nothing. However, certain implementation of C++ compiler cannot cope with deleting a null pointer. Plus, delete an object twice or access an object after it has been deleted are both serious errors. So a good practice is always to check the pointer before delete it and set the pointer to zero after delete it.

            if (ap) { delete ap; ap = 0; }

 

Delete will eventually invoke the destructor, which is almost always a virtual function. Otherwise, the destructor of any derived class will not be invoked correctly.

            class A { ~A () { … } };

            class B : public A { ~B () { … } };

            A *bp = new B ();

            delete bp;         // Error – only A’s destructor will be called.

 

As indicated by C++ language, if delete a derived class object through a base class pointer when the base class does not have a virtual destructor, the result is undefined. The right way is to make A’s destructor virtual: virtual ~A () { … }. One convenient way to make an abstract class is to make the destructor a pure virtual function. However, the implementation of virtual functions comes with a cost, you may choose to make the destructor non-virtual if there are no other virtual functions in the class and you are sure that the class would never have any derived classes.

 

If the destructor contains any code (do something before release the memory), put all the code in try block, have at least one catch (…) block, and never re-throw in the catch block. This will prevent any exception from blowing out of the destructor and make sure the memory will always be released.

 

Use delete [] for an array of object pointers.

            A *ap = new A();

            delete ap;

            A *aps = new A[10];

            delete [] aps;

8.    Reference

A reference is an alternative name for an object. It can be considered the dereferencing of a const pointer, with the following differences:

-         Reference must be initialized when it is declared, with a variable of the same type whose address can be taken. For example,

int i = 0;

int& ri = i;

int& ri1;            // Error, ri1 is not initialized.

int& ri2 = 0;     // Error, the address of 0 cannot be taken

-         The value of reference cannot be changed after initialization.

-         Reference cannot be operated upon, ri++ does not increment ri; instead, it increases the value of i from 0 to 1. However, the following statements are correct.

            extern int& ri3; // ri3 is initialized somewhere else.

            const int& ri4 = 0;        // Equivalent to const int& ri4 = int (0);

 

When used as argument to a function, reference has the similar result of a pointer – the function does not make a local copy of the argument, and the value of the variable passed to the function is actually changed. Compared with passing by pointer, passing by reference does not have to check if the reference refers to null because there is no such thing as null reference. To avoid changing the value of the variable, the argument shall be declared as const reference.

 

Another difference between passing argument as value and as reference is that passing as value slice off the dynamic binding – if the object of the derived class is passed in by value as the base class, when inside the function a member function is invoked on the object, only the base class implementation will be called, even though that member function is virtual in the base class and has been overridden in the derived class. To preserve the dynamic binding, always pass the argument as reference.

 

There are three things to pay attention when pass by reference. First, find out which object the reference actually refers to, as in the case of assignment operator. Second, make sure the object still exists over the whole life of the function that uses the reference. Third, if the object takes very little data, you may choose to pass by value because reference is usually implemented as pointer, which costs some memory and machine time.

 

In the case of exception handling, when an exception is thrown, the try block passes control to the catch block and never returns back. If the catch argument is pointer, the program simply value passes the pointer of the created exception object to the catch block (make a copy of the pointer). In this case, the exception object must exist longer than the try block, by making the exception object static or allocating in on the heap (with new operator. Because the catch block does know how the exception object is created therefore cannot determine whether delete it or not, plus allocate memory on heap may not be doable during exception, using pointer as catch argument is not recommended. If the catch argument is value or reference, the program will make a temporary copy of the exception object created in the try block in case the try block goes out of scope. If the argument is value, the temporary copy is value-passed to the catch block (another copy). If the argument is reference, the temporary copy is passed by reference and no second copy is made. Also considering the slicing problem, reference is better for catch argument.

 

For syntax reasons, reference is more intuitive to use as the return type than pointer. To use reference as the return value, make sure that the object being referred to won’t be deleted after the function exit (such as the local variables). It is also not a good practice to use new to create an object in the function and use the reference to the object as the return value. It is hard to use delete to de-allocate an object that is allocated inside a function and returned by reference. The good candidate object to return as a reference is an object that already exists outside the function. To prevent the caller from changing the contents of the returned object, the return type can be const reference.

9.    Const

A const member function cannot change the state (or non-mutable non-static data member) of the object. One usage of const member function is to control the accessibility of member functions based on whether the object is const or not. A const member function can be invoked for both const and non-const objects, but a non-const member function can be invoked only for non-const objects.

 

Another usage of const is to overload member function – const is part of the function’s signature. Sometimes it is convenient to have two member functions only different in return values. Since return value is not part of the signature, member function cannot be overloaded based on const in the return value. To work around, make the function itself const, for example,

            class MyString {

                        public:

                                    char& operator[] (int index) { return *(data + index); }

                                    const char& operator[] (int index) const { return *(data + index); }

                        private:

                                    char* data;

            };

 

If the const member function returns a non-const pointer or reference, the caller can still change the state of the object through that pointer or reference. Strictly speaking, that is beyond the control of the member functions, but good habit is to only return const pointer or reference from a const member function.

 

As we know, const int * ci = 0; or int const * ci = 0; means the content of memory pointed by ci cannot be modified, int * const cpi = 0; means cpi cannot be changed to point to a different memory location, and const int * const cpci = 0; or int const * const cpci = 0; means neither the content or the pointer can be changed. We frequently see the function declaration as void print (const A* ap); but not void print (const A * const ap); The reason is that pointer ap is value-copied into the function. No matter how the pointer may be changed in the function, the original pointer outside the function won’t be changed at all.

10.                       Declaration, definition and implementation

Class declaration is to declare that there is a class in the current scope,

            class A;

 

Class definition is to provide the definition of a class, including the modifier, data member, function member and accessibility.  It is usually placed in the head file,

class A : public BaseClass {

public:

                                    void printIt ();

private:

                                    int m_id;

            };

Since class definition is also a declaration, class declaration without definition is also called forward definition.

 

Some operations only need the declaration and others require the definition. Declaration satisfied operations are:

            - declare the class as a friend class

            - declare an object reference of the class

            - declare an object pointer of the class

            - define a new type (typedef) using the class

To use declaration satisfied operations without providing declaration will get a "Non-defined class type" error at compilation for certain compilers.

 

Definition required operations are:

            - declare the class as a base class

            - declare an object instance of the class

            - invoke a function of the class through any type of object reference

            - access a data of the class through any type of object reference

To use definition required operations without providing declaration will get a "Non-defined class type" error at compilation. To use definition required operations without providing definition will get an "Incomplete class implement" error at compilation.

 

Class implementation is to provide the implementation of its member functions. It is usually in the .cpp file, or can come with the class definition as inline functions.  Class implementation is not needed at compilation time, but required at linking time. For example,

void A::printIt () { cout << i << endl; }

 

Class implementation is located in a global or name space scope, therefore class name must be pre-pended otherwise the functions become a global or name space one, even though the function’s signature can be the same as one of the class.

 

Implementation required operations are:

            - declare an object value of the class

            - invoke a function of the class through any type of object reference

To use implementation required operations without providing implementation will get an "Unsolved symbols" error at linking time.

11.                       Instantiating template class

Template class is like a macro for a similar type of classes. The declaration is like:

            template <class T> class A_template;

 

The definition is like:

            template <class T>

class A_template : public BaseClass {

public:

                                    void printIt ();

private:

                                    T t;

            };

 

The implementation is like:

            template <class T>

void A_template <T>::printIt () { std::cout << t << std::endl; }

 

The declaration, definition and implementation of a template class shall be put in the same file scope; the same is for template functions.

 

A template class will translate to a suite of concrete classes after real types are provided to substitute the template type. The declaration of the concrete class is simply like:

            class A_template <int>;

 

The definition and implementation of the concrete class are generated by the compiler dynamically – the so-called instantiating the template class. The compiler does instantiation only once in one scope and only definition required operations and implementation required operations will trigger the instantiation. For example,

            typedef A_template <int> int_temp;      // no instantiation, not instantiated

            A_template <int> *it;                            // no instantiation, not instantiated

            A_template <int> i;                               // instantiation

            A_template <int> j;                               // no instantiation, already instantiated

            { j.printIt(); }                                        // no instantiation, already instantiated

 

We can see that compiler will make a note after it instantiates the template class for one particular type and will reuse the generated definition and implementation for all the same type classes in the scope. Any further instantiation will be shielded.  Note that instantiation can also be shielded by concrete class declaration such as

            class A_template <int>;

 

because when compiler sees such a declaration it assumes that the class has been instantiated and no further instantiation will be attempted.  Therefore if instantiation has not happened before the declaration in the scope, compiler will not generate the definition and implementation of the concrete class and any definition required operation will give an "Incomplete class implement" error.

12.                       Heap Memory Allocation

C++ prefers not to use malloc () to allocate memory on the heap. Instead, it defines the global operator new, where size must be the first mandatory argument:

            void* operator new (size_t) throw (std::bad_alloc);

            void* operator new (size_t, const nothrow_t&) throw ();           // throw no exception

 

When memory is exhausted, the first operator will throw a bad_alloc exception. 

try { void* ap = ::operator new (100);

        if (0 == ap) cerr << “Memory exhausted.” << endl;   // no chance to execute

}

catch (std::bad_alloc) { abort (); }

 

but the second one will return a null pointer, to be compatible with the old style.

            void* ap = ::operator new (100, nothrow);

            if (0 == ap) cerr << “Memory exhausted.” << endl;   // old style check

 

Here nothrow is an allocator defined by C++. Before the operator new throws the bad_alloc exception, it will invoke function new_handler, defined as

            typedef  void (*new_handler) ();

 

and set by

namespace std {

new_handler set_new_handler (new_handler) throw ();

// throw no exception

}

 

The returned new_handler is the original handler before the new one is set. To bypass the handler, set it to null, set_new_handler (0);

 

C++ also defines the new operator to instantiate a class on the heap. The new operator first calls the operator new to allocate memory on heap, and then invokes the corresponding constructor to initialize the object. Conceptually, A* ap = new A; will do

            void* p = ::operator new (sizeof (A));

            A::A ();

            return static_cast<A*> (p);

 

The new operator can never be overridden, but the operator new can, and classes can even define their own (must be static method). It uses the similar syntax as the global operator new, and behaves in the similar way.

            void* A::operator new (size_t) throw (bad_alloc);

            void* A::operator new (size_t, const nothrow_t&) throw ();      // throw no exception

 

            try { A* ap = new A; }

            catch (std::bad_alloc) { abort (); }

 

            A* ap = new (nothrow) A;

            if (0 == ap) { abort (); }

 

A class can also define its own operator new with different arguments. For example, it can take new_handler as an argument and change the handler before invoking the global operator new.

            template <class T> class CustomizedOperatorNew {

            public:

                        static void * operator new (size_t, new_handler);          // throw any exception

            }

 

            template <T>

void CustomizedOperatorNew <T>::operator new (size_t size, new_handler nh) {

            new_handle old_nh = std::set_new_handler (nh);

            void* p;

            try {

                        p = ::operator new (size);          // try nh before throw bad_alloc

            }

            catch (std::bad_alloc)    {

                        std::set_new_handler (old_nh);

                        throw;

            }

            std::set_new_handler (old_nh);

            return p;

}

 

class A : public CustomizedOperatorNew<A> { };

 

A* ap = new (outOfMemory) A;

13.                       Data Initialization Sequence

  1. Non-local variables are initialized when the program is loaded into memory. In the same translation unit, they are initialized in the order of their definition, but there is no way to control the initialization order of non-local variables from different translation units. The initialization could start execution of some program even before the main () starts.
  2. The main () starts execution when it is encountered.
  3. Local static variables are initialized the first time the definitions are encountered in the containing functions.
  4. To instantiate a class, first invoke the operator new to allocate the memory for the whole class if the class will be instantiated on heap, then invoke the constructor of the direct base class, and finally invoke the constructor of the class itself.
  5. The constructor initializes data members in the initialization list, in the order they are declared in class definition. For class type data members in the initialization list, their own copy constructors are invoked to instantiate them.
  6. After that, data members not in the initialization list will be instantiated with their default constructors.
  7. Finally the body of the constructor will be invoked, which may assign values to the data members that are instantiated in the previous step (with assignment operator).

 

Note that within the constructor body, the member classes can be assigned values. But at this moment, those member classes have already be instantiated (with default constructor), so in the containing class’s constructor, they are assigned the new values. This way is less efficient than initializing the member classes directly in the initialization list.

14.                       Miscellaneous

Replace preprocessor symbol definition “#define” with constant data type “const”, e.g.

replace             #define PI 3.1416

with                  const double PI = 3.1416;

In this way PI will be recognized by the compiler and listed in the symbol table for debugger.

 

Replace preprocessor macro with inline template function, e.g.,

replace             #define max (x, y)         ((x) > (y) ? (a) : (b))

with                  template<class T>

                        inline const T& max (const T& x, const T& y)

                        { return x > y ? x : y; }

Using function instead of macro can avoid the classic max (x++, y+20) type of macro expansion pitfall. Since inline in C++ is just a hint rather than command, the function may end up as a normal one, especially when that inline function is a virtual functions. Even that, you just pay the little price of function invocation.

 

*p++ takes the value of p first, then increments p by one. For example, the simple statement while (*p++ = *q++); copies a zero-terminated string q to p.

 

Define a pointer to data member or function member, for example,

            class A {

            public:

                        int m_id;

                        int getId ();

            };

 

            int A::*pdm = &A::m_id;

            int (A::*pfm) ();

            pfm = &A::getId ();

 

Then use the pointer to access the class members.

 

            A a;

            A* pa = &a;

            a.*pdm = 10;

            (pa->*pfm) ();              // return 10

 

The way of declaring operators sometimes can only be remembered.

 

            class Complex {

            public:

                        Complex& operator= (const Complex&);

                        const Complex& operator+ (const Complex&);

                        const Complex& operator+= (const Complex&);

                        Complex& operator++ ();        // prefix

                        // Increase value of the object and returns the object itself.

                        // Not return a const so that p++ = q; is allowed.

                        Complex operator++ (int);        // postfix

                        // Copy the original object, increase value of object and returns the copy.                                   // Not return a const so that p++ = q; is allowed.

                        Complex& operator* ();           // prefix

                        void operator();            // define a function object.

                        bool operator== (const Complex&) const;

                        operator int ();              // type conversion defines no return type.

                        operator const int () const;

                        static void* operator new (size_t);

                        // Complex* cp = new Complex;

                        static void* operator new (size_t, new_handler);

            };

 

            const Complex operator + (const Complex&, const Complex&);

 

The function typeid () returns the class type of an object. For example,

            class A;

            class B : public A;

 

            A a;

            B b;

            const type_info& object_type = typeid (a);

            object_type == typeid (b);        // false


Jerry Zhong, December 1999.