C++ Tutorial Part II - Advanced: Silan Liu
C++ Tutorial Part II - Advanced: Silan Liu
Silan Liu
4. EXCEPTION HANDLING13
4.1 What is Exception Handling13
4.2 Do Not Use Exception Handling on Normal Application Code13
4.3 try Block13
4.4 throw13
4.5 Throw Point13
4.6 catch Block14
4.7 Rethrow an Exception14
4.8 Exception Specifications14
4.9 Stack Unwinding15
4.10 Exception Thrown from Constructor and Destructor16
4.11 Resource Leak17
4.12 Insufficient Memory when Using new17
4.13 Standard Library Exception Hierarchy17
4.14 Difference Between C++ and Java’s Exception Handling18
5. DATA STRUCTURES19
5.1 Data Structure19
5.2 Linked List19
5.3 Tree19
5.4 Struct19
5.5 Bitwise Operators19
5.6 Example: Display a Number in Binary Form20
5.7 Bit Field20
5.8 Character Handling Library20
5.9 String Conversion Methods21
5.10 String Handling Library “string.h”21
5.11 Error Message Method22
6. THE PREPROCESSOR23
6.1 #include23
6.2 Conditional Compilation24
6.3 #define Symbols for Conditional Compilation24
6.4 #define for Constants vs. C++ Constants25
6.5 #define for Macros vs. C++ Inline Template Functions25
6.6 Line Numbers26
6.7 Predefined Symbolic Constants26
7. C - LIKE CODE27
7.1 Redirecting Input & Output on UNIX/DOS Systems27
7.2 Variable-Length Argument List27
7.3 Command-Line Arguments28
7.4 Program Termination with Exit and Atexit28
7.5 Type Qualifier volatile28
7.6 Suffixes for Primitives28
7.7 Signal Handling28
7.8 C-Style Dynamic Memory Allocation29
7.9 Unconditional Branch: Goto29
7.10 Union30
7.11 Linkage Specification extern "C"30
7.12 __stdcall31
7.13 typedef31
class Base1 {
public:
void Hi1()
{ printf("Hi from Base1!\n"); }
BYTE a1[100];
};
class Base2 {
public:
void Hi2()
{ printf("Hi from Base2!\n"); }
BYTE a2[100];
};
class Base3 {
public:
void Hi3()
{ printf("Hi from Base3!\n"); }
BYTE a3[100];
};
void main()
{
Derived * pDerived = new Derived;
Base1 * pBase1 = (Base1 *)pDerived;
pBase1->Hi1();
Base2 * pBase2 = (Base2 *)pDerived;
pBase2->Hi2();
Base3 * pBase3 = (Base3 *)pDerived;
pBase3->Hi3();
}
The memory footprint of an object of class Derived and the addresses of the pointers will be like:
pDerived
pBase1 pBase2 pBase3
↓ ↓ ↓
a1 a2 a3 a
0 100 200 300 400
Fig 1. Memory footprint of a non-polymorphic type
because only the class which declares the method can implement it, compiler will directly link the call to the
address of the specific method at compile time.
pBase2->Hi2();
A late-binding process involves the following activities:
Compiler adds a hidden vPtr member to the class, and generates one unique vtable for the class.
At compilation time, when compiler sees the definition of a class with virtual methods, it will build a virtual
table (vtable) for the class, which is an array of function pointers to the implementations of all the virtual
methods, and add a hidden data member vPtr to the class definition as the FIRST data member.
Now suppose the methods of classes in Fig. 1 (Hi, Hi1, Hi2, Hi3) are all virtual functions. The memory
footprint of an object of class Derived becomes:
pDerived
pBase1 pBase2 pBase3
↓ ↓ ↓
vPtr a1 vPtr2 a2 vPtr3 a3 a
0 4 104 108 208 212
312 412
a1 are data members of base class 1, a2 are of base class2, …, a are of base class 1.
Fig. 2. Memory footprint of a polymorphic type object
As you can see in the memory footprint, if you use a Base2 pointer to receive a Derived object, for example,
this pointer will point to memory offset 104 as pBase2 does.
Note that each Derive object will have its own memory footprint, with the same structure but in different
memory locations. However, the vPtrs will all be pointing to the same method implementations, in other
words, the vPtr2 of two instances will contain the same address.
The derived-class and the first base class shares the same vPtr, which points to their shared merged vtable
(see following section “Inheritance of Base-class vPtrs” for details). The rest of the base classes have their
own vPtrs.
Note that no matter how complicated the inheritance hierarchy is, a function pointer in the vtable always
points to the latest/lowest implementation of the virtual function in the inheritance hierarchy.
Compiler generates code to do dynamic binding using the vtable.
At compilation time, when compiler sees a call to a virtual method thourgh a pointer (pBase2->Hi2( )), it
knows that the address of the function is only known at run time, so it will not try to find the implementation
of the function. Instead, it knows that the pointer (pBase2) will be pointing to a vPtr at run time. So it
generates code to go through the vPtr to find the vtable (whose composition is already know from the type
of the pointer), and go to a certain entry of that vtable, fatch that function pointer, and make the call.
At run time, when an object is created out of this class definition, its vPtr member will be assigned the address of
the class’s vtable.
pDerived
pBase2 pBase3 pBase1
↓ ↓ ↓
vPtr a2 vPtr a3 a1 a
0 4 104 108 208 308 408
Fig. 3. Memory footprint of a polymorphically mixed type
You can see that the memory block of polymorphic base classes (Base2 and Base3) are moved to the front, while
the non-polymorphic base classes (Base1) are moved to the back.
pDerived->Hi3();
even if it is implemented in class Derived, the program will still go to the vPtr of Base3 (offset 208 in the
footprint), then to Base3’s vtable, then to Hi3’s function pointer, which points back to the implementation in
class Derived. We can prove it by setting Base3’s vPtr in offset 208 to 0. Thus the address of Base3’s vtable is
lost, and error will cause the program to be shut down:
int main()
{ Base * ptr = new Multi; }
#include "stdafx.h"
struct Base1 {
virtual void Hi()
{ printf("Hi from Base1!\n"); }
};
struct Base2 {
virtual void Hi()
{ printf("Hi from Base2!\n"); }
};
template < class T1, class T2 > // T1 & T2 are called “Type Parameter”.
void print(T1 a, T2 b)
{ cout << a << b; }
Then, when the compiler sees a method call
print(3.0, ‘M’)
it will match T1 with 3.0 i.e. float, and T2 with ‘M’ i.e. char. Then it will generate a brand new method from the
template:
int main()
{
Test<float, int> t1(5.0, 5); // When creating object
Test<char, float> t2('X', 5.0);
t1.print();
t2.print();
}
Suppose “Test” is the name of a class template with type parameter “X”. When you declare new object of class
“Test” in the class definition of another class, always use the following format:
Test<X> t1;
When creating a new object, use the following format:
Test<int> t1;
Test<float> t2;
int main()
{ Test<int, 3> a1;
Test<float, 5> a2;
a1.print();
a2.print();
}
The use non-type parameter to create template classes with different memory sizes reminds us of using dynamic
memory allocation. To create array in a normal way requires you to determine the memory size when writing
your class; by using non-type parameter you can let the client who uses your code decide it; by using dynamic
memory allocation you can decide the memory size at run time. So the flexibility is different.
3.5 Templates and Friends
Inside a class template X that had been declared with
4.4 throw
A throw keyword specifies an operand which can be of any type. The throw statement is actually passing
information to the exception handler in the catch block. This information can be either only be the type of the
thrown object – in many cases it is enough, or plus the extra information carried in the thrown object. The thrown
object can be a class object or just a character string:
int main()
{
try
{ throw ("Testing string!"); }
catch (char * s)
{ cout << s << endl; }
}
catch(...)
will catch all exceptions.
♦ catch through inheritance
Due to inheritance hierarchies, a derived-class object can be caught by a handler specifying either the derived-
class type or the base-class type.
As a general rule, put the specific type handlers in the front, such as derived-class type handlers, and put the
generic handlers after, such as base-class type handlers, and put “catch (... )” at the last. If you reverse the
sequence, the exceptions will all be caught by generic handlers and will never get to the specific ones.
A “void *” handler will catch all exceptions of pointer type.
♦ Release the Resource in catch Block
When an exception happens, the try block terminates and all automatic objects inside the try block are destroyed
before the handler begins executing. However, it is possible that some allocated resources are not yet released in
the try block. The catch handler, if possible, should release these resources, e.g., delete the space allocated by
new, and close any files opened in the try block.
♦ Copy Constructor of the Thrown Object
The handler must have access to the copy constructor of the thrown object. It means that the header file of the
thrown object should be included.
throw;
A rethrown exception will exit the outer try block and search the following handlers.
“catch (... )” can be used to perform recovery that doesn’t depend on exception types, such as releasing common
resources. Then it may rethrow the exception to alert more specific catch handlers.
#include <iostream>
#include <stdexcept>
int main()
{
try { method1(); }
catch ( runtime_error e )
{ cout << "Exception caught: " << e.what() << endl; }
class Employee {
...
Image * const m_pImage;
AudioClip * const m_pAudio;
};
class Employee {
...
Image * const m_pImage;
AudioClip * const m_pAudio;
};
Such pointer members must be initialized in the member initializer, where you can not put try and catch blocks.
The ultimate solution is: use auto_ptr< > class or similar classes to wrap the raw pointers, so that they become
local objects:
class Employee {
...
auto_ptr<Image> m_image;
auto_ptr<AudioClip> m_audio;
};
As we already know, when a constructor is terminated by exception, all local objects are destroyed by calling
their destructors. The destructor of auto_ptr< > will delete the object pointed by the wrapped pointer.
delete ptr;
Header file of auto_ptr is <memory>.
5.3 Tree
A tree is a structure whose nodes each connected to one upstream node and many down-stream nodes. A binary
tree is a structure which is connected to one upstream and two downstream nodes. A tree has a root node.
5.4 Struct
A structure is a simplified class with all public members:
struct Card {
char * face;
char * suit;
};
Structure members are not necessarily stored in consecutive bytes of memory. If the word length of the computer
is 2 bytes, and one data member is only one-byte long, there will be a one-byte hole between this member and the
next member. Because the value stored in the hole is undefined, a structure can not be compared.
A structure is initialized the same way as an array:
if(c % 8 == 0)
cout << ‘ ‘;
}
struct Card {
unsigned face : 4;
unsigned suit : 2;
unsigned color: 1;
};
Therefore, three members are stored in one byte. The number of bits of one member is decided by the maximum
number it will represent. For example, the number of faces of cards is 13, less than 16, so member “face” can be
defined with four bits.
Bit field members are accessed the same way as normal members. But they can not have addresses, and “&” can
not be used, because there is no unique address of each part of one word.
Unnamed bit field with a width is used as padding, nothing can be stored in these padding bits:
structure Example {
unsigned a : 13;
unsigned : 3;
unsigned b : 4;
};
Unnamed bit field with 0 width is used to align the next bit field with next word:
structure Example {
unsigned a : 13;
unsigned : 0;
unsigned b : 4;
};
Although bit fields save space, it consumes more machine time, because extra codes have to be generated to deal
with them. This is one of the many examples of space-time trade off that occur in computer science.
int main()
{
char x[] = "123 456 789";
cout << (char *)memmove(x, x+4, 3);
}
then x will become "456 456 789".
int memcmp(const void * s1, const void s2, size_t n)
Compares the first n characters of object s1 and s2, and returns 0, <0, or >0.
void * memchr(const void * s, int c, size_t n)
Locates the first occurrence of c in the first n characters of object s, or return 0 if not found.
void * memset(void * s, int c, size_t n)
Set the first n characters of object s to character c, and return a pointer to the result:
int main()
{
char x[] = "AAAA";
cout << (char *)memset(x, 'B', 2);
}
result will be "BBAA".
6.1 #include
The #include preprocessor directive causes a copy of a specific file to be pasted in place of the directive before
compilation. It is used when another file contains the type definition that you want to use. For global variables
and functions contained in another file, you do not include that file. Instead you inform the compiler with the
following line before you use it that it is a global variable or function and the compiler should find it out itself:
#include <filename>
#include “filename”
File included in “< >” will be searched in the system’s pre-defined locations, while the file included by quotes
will be first searched in the current directory, then the pre-defined locations. “< >” is used for standard library
files.
Most included files are *.h header files. But other files such as *.cpp source files can also be included, as long as
the definition you want to invoke.
If header file A makes use of a type defined in file B, normally it should include B before using that type. Such a
file can be called “self-contained”. Not all files are “self-contained”. Some library header files are very
comprehensive and contains lots of type definitions. Different definitions may use other definitions defined in
other files. If we want such files to be self contained, we may virtually end up with each header file including all
other header files. Considering that most clients include such a header file just to use one or two definitions in it,
it will be a big waste of space. Therefore, such a header may decide not to include some other files. If you want
to include use type A defined in file X, and type A uses type B which is defined in file Y, it is your duty to insure
that file Y is included in front of file X:
#if expression
...
#endif
Two special expression are:
#ifdef
#ifndef
Conditional compilation is commonly used as a debugging aid. You can first use
#define DEBUG 1
then enclose all debugging codes in
#if DEBUG
#endif
Then after debugging is finished you only need to change “1” to “0” to disable all debugging codes.
#define <constant>
it will create a constant named "CONSTANT" in the scope of this file. Then "defined CONSTANT" will return
true. Therefore, you can use the following predirectives to wrap some code:
#undef <constant>
A preprocessor constant can also be defined in a global scope. In Visual C++, the constants you put in the
"Project | Settings | C/C++ | Preprocessor directives" field will be defined in global scope. Some commonly used
constants like "WIN32" and "_DEBUG" are defined in that field. When you choose "Win32 Debug" in "Project |
Settings", the "_DEBUG" constant will be added automatically. If you choose "Win32 Release", constant
"NDEBUG" will be added. Then lots of Win32 macros such as assert and your own code may check this
constants and decide what to behave. For example, if "NDEBUG" is defined, the code within assert will be
skipped to increase performance:
void assert(...)
{
#if defined NDEBUG
// assertion work to be done
#enif
}
Look at the following example. Suppose you have two classes defined in two different header files
class A
{
public:
A()
{
#if defined _SAYHELLO
printf("Hellow from class A!\n");
#else
printf("Hi from class A!\n");
#endif
}
};
class B
{
public:
B()
{
#if defined _SAYHELLO
printf("Hellow from class B!\n");
#else
printf("Hi from class B!\n");
#endif
}
};
If you put "#define _SAYHELLO" in one of the header files, only one constructor will say "Hello" and the other
will say "Hi". If you put "_SAYHELLO" into the project setting, both constructors will say "Hello".
private:
const static int I;
In the implementation file, you have to initialize it as
private:
enum {I = 5};
float a[I];
m(buf);
It will become
cout <<m(12);
it becomes
#line 100
#line 100 “file1.c”
The first line causes the following line numbers to start from 100, and all compiler messages is written into
“file1.c”. It is used to make the messages produced by syntax errors and compiler warnings more meaningful.
$ program1 | program2
This causes the output of program 1 to become the input of program 2.
♦ Redirect Output
va_end(a);
return sum;
}
int main()
{
int a = 1, b = 2, c = 3, d = 4;
cout << sum(1, a);
cout << sum(2, a, b);
cout << sum(3, a, b, c);
cout << sum(4, a, b, c, d);
}
va_list is a type. a is an object of va_list type, which will be used by other macros to get each argument from the
list.
va_start initializes the va_list object, taking 2 arguments: the ap1 object and the last argument before the "...".
va_arg(a, int) takes two arguments: the va_list object and the type of the object expected in the argument list, and
returns the next argument in the argument list.
va_end(ap1) facilitate a normal return to the caller.
7.3 Command-Line Arguments
In many systems – UNIX and DOS in particular – it is possible to pass arguments to main from a command line.
This is achieved by including two parameters in main's parameter list: integer argc indicates the number of
passed arguments, and argv is an array of strings to hold the arguments. This method can be used to pass
information to a program, such as options and filename.
Command line:
#include <iostream>
#include <signal>
int main()
{
signal(SIGFPE, handling);
int a = 3, b;
cout << "Please input b as 0: \n";
cin >> b;
cout << a/b << endl;
}
int main()
{
fstream file("test.txt", ios::out);
bottom: file.close();
return 0;
}
Output will be:
** Outer loop: i = 0
Inner loop: j = 0
Inner loop: j = 1
Inner loop: j = 2
Inner loop: j = 3
** Outer loop: i = 1
Inner loop: j = 0
Inner loop: j = 1
Inner loop: j = 2
Inner loop: j = 3
** Outer loop: i = 2
Inner loop: j = 0
Inner loop: j = 1
Inner loop: j = 2
7.10 Union
A union can have several members of different types including user-defined types. But these different members
are actually sharing the same block of memory space. At one time a union can only hold one member.
At different times during a program’s execution, not all objects are relevant in the meantime. So a union
combines a group of objects which are never used at one time to save space. The number of bytes used to store a
union is at least the size of the largest member.
A union is declared in the same way as a struct or a class:
union Number {
int x;
float y;
};
Number n1;
n1.x = 33;
The following operations can be performed on an union:
Assignment (between same type of unions);
Take reference (&);
Access union members through “.” and “->”;
Unions can not be compared with each other for the same reason as structures.
An union can also have constructor, destructor or other methods, just like a class. But there is no inheritance or
virtual issues for union.
An anonymous union is a union without a type name. Such a union can not be used to define objects later, but
itself defines an object, whose members can be directly accessed in its scope just like normal variables, without
using “.” or “->” operators:
union {
int x;
float y;
};
x = 33;
extern “C”
{ method prototypes }
7.12 __stdcall
This is to indicate the calling convention which is used to call Win32 API functions. The callee cleans the stack,
so the compiler makes vararg functions __cdecl. Functions that use this calling convention require a function
prototype. The following list shows the implementation of this calling convention:
Argument-passing order: Right to left.
Argument-passing convention: By value, unless a pointer or reference type is passed.
Stack-maintenance responsibility: Called function pops its own arguments from the stack.
Name-decoration convention: An underscore (_) is prefixed to the name. The name is followed by the at
sign (@) followed by the number of bytes (in decimal) in the argument list.
Therefore, the function declared as int func( int a, double b ) is decorated as
follows: _func@12
7.13 typedef
typedef is used to assign an alias to an existing data type. Then this synonym can be used in place of the original
data type:
struct Employee {
LPSTR name;
LPSTR dept;
short age;
};
When you create an instance of the struct, you have to say
typedef struct {
LPSTR name;
LPSTR dept;
short age;
} Employee;
when you create an instance you can say
Employee e1;
8. CLASS STRING & STRING STREAM PROCESSING
s1.assign(s2);
s1.assign(s2, start, number);
The second call assigns “number” of characters of s2 to s1, starting from subscript "start":
A character in a string can be accessed by
s1[2] = 'a'; // or
s1.at[2] = 'a';
[ ] operator doesn't provide range check, while at method does.
string s1 = “Frank”;
string s2 = s1 + “ Liu”;
s2 will be “Frank Liu”. Or you can explicitly call string’s append method:
s1.append(" Liu"); // or
s1.append(s2, start, end);
The second call appends part of s2 to s1, from subscript “start” to “end”.
==
!=
> & >=
< & <=
Or you can explicitly call string’s compare method
int result = s1.compare(s2);
which returns the result as positive if s1 > s2, zero if s1 == s2, negative if s2 < s2.
result = s1.compare(start1, length, s2, start2, length); // or
result = s1.compare(start1, length, s2);
compare part of s2 with part of s1. s1 starts from "start1", s2 starts from "start2", number of characters is
"length".
8.6 Sub-string
cout << s1.substr(start, length);
method substr returns a string made from s1, starting from "start" and number of characters is "length".
looks for string "ABC" in string s1, and returns the subscript of the location, or string::npos.
result = s1.rfind("ABC");
searches string s1 backwards.
result = s1.find_first_of("ABC");
searches for the first occurrence in s1 of any character contained in "ABC". Searching starts from the beginning
of s1.
result = s1.find_last_of("ABC");
searches for the last occurrence in s1 of any character contained in "ABC". Searching starts from the end of s1.
result = s1.find_first_not_of("ABC");
searches for the first character in s1 which is not contained in "ABC".
result = s1.find_last_not_of("ABC");
searches for the last character in s1 which is not contained in "ABC".
9.2 Iterator
Iterators are sophisticated pointers pointing to container elements. Except for vectors which can manipulate its
elements through integer subscripts (such as v[1], v[2]), all container methods and STL algorithms manipulate
container elements through iterators.
There are two kinds of container methods: methods which return iterators and methods which modifies the
elements.
Iterator-returning methods return iterators pointing to some special elements such as the first or the last element,
or an element which conforms to a certain rule (e.g., the one pointing to an element which is equal to the
passed object). With these returned iterators, container's element modifying methods and STL algorithms can
then modify the elements through them.
Element-modifying methods have quite limited functionality – much of the job is done through independent STL
algorithms, which are more abstracted from containers.
Iterators are designed to act as a medium to separate "how" from "what": new STL algorithms and client
applications are supposed to be developed with only iterators, without knowing the type of the container and the
type of its elements. Java achieved this goal. All containers hold only one type: class Object, and all containers
use one type of iterator.
However, C++ did not achieve such a complete abstraction. When you create an iterator, you have to specify the
type of container it belongs to – a vector, a map, a multiset, etc., and the type of the container element - integer,
string, or other user-defined types.
We can not create an iterator pointing to a certain element in a container. We can only ask a container to return an
interator pointing to a certain element.
Normal arrays are random access containers, pointer to arrays i.e. array names are random access iterators.
vector<int>::reverse_iterator p1;
9.6 IO Iterators
There are other two types of iterators which do not belong to any container: istream_iterator and
ostream_iterator. They are used to input and output a certain type of object in a type-safe manner from/to an IO
object such as cin and cout.
9.7 Operations Supported by Different Iterators
All iterators:
++p
p++
Input iterators:
*p
p1 = p2
p1 == p2
p1 != p2
Output iterators:
*p
p1 = p2
Forward iterators:
provide all the methodality of both input and output iterators.
Bidirectional iterators:
-- p
p —-
Random access iterators
p + i
p – i
p += i
p -= i
p[i]
p1 < p2
p1 <= p2
p1 > p2
p1 >= p2
9.9 vector
vector is the most commonly used container in STL.
Class vector provides a data structure with contiguous memory locations. This enables efficient, direct access to
any element via subscript operator [ ] like arrays. So a vector is a more intelligent and complex array.
A vector returns random access iterators.
Because only random access iterators support “<” operator. So it is always safer to use “!=” operator, which is
supported by all iterators except for output iterators.
Header file of vectors is <vector>.
♦ Element insertion and deletion
Because vector elements occupies contiguous memory, it is OK to insert or delete new element at the back, but
expensive at the front or middle, for the entire portion of the vector after the inserted or deleted element have to
be moved to keep it contiguous.
Suppose you have a vector of 9 elements, now you delete the 5th. The vector will call element’s assignment
operator to assign the 6th element to the 5th, the 7th to 6th, ... , finally the 9th to the 8th. Then it will delete the
last element. So you can see, to keep a contiguous memory a great deal of work needs to be done if you delete or
add from the middle. In contrast, for a linked list, all you need to do is to point the pointer in the 4th element to
the 6th. Therefore, if you need to frequently insert and delete from the middle, use a linked list.
When an element is inserted, the compiler will first call copy constructor to create a new element at the end, and
use assignment operator to assign the second last to the last, and so on, and finally assign the object which is to be
inserted to the original object which is at the insertion point.
vector's elements can be accessed with subscription just like arrays. Operator [ ] does not perform range check,
but method at does.
If a new element is added to a full vector, the vector increases its size automatically – some would double its
size, so would increase a certain amount.
♦ Two more methods of its own
vector has two more methods of its own:
capacity: vector’s capacity is not always its number of elements. When a full vector receives a new element, it
may double its size. So if a vector has 4 elements and one is added, it will have a size of 5 and capacity of 8.
resize: if you think the doubled capacity consumes too much memory, you can use it to resize the vector.
♦ Sample codes and explanations:
This is to insert the first 6 elements of vector v2 into the vector v1, to start as from the 4th element of vector.
istream_iterator<int> input(cin);
int a, b;
a = *input;
++ input;
b = *input;
This is to declare an istream_iterator to input integers from cin.
bool myCompare(T a, T b)
{ return a < b; }
9.11 List
Class list is implemented as a doubly-linked list, so it can not be randomly accessed, and it only supports
bidirectional iterators. As said before, because the list elements are not stored contiguous and only connected one
by one through doubly links, it is convenient to insert or delete an element at any location of the list.
Header file of lists is <list>.
♦ Methods
splice: remove elements from a list and insert into another
push_front: insert an element at the front
pop_front: remove an element at the front
remove
unique
merge
reverse
sort
♦ Sample program
l1.splice(l1.end(), l2);
This is to remove all the elements of l2 and insert them into l1 before the iterator position, which is the end.
l1.unique();
This is to remove duplicated elements in the list. Before this operation the list must already be in sorted order. A
second version of “unique” method takes an argument as a predicate method, to determine whether two elements
are equal.
l1.swap(l2); // exchange the contents of l1 with l2
l1.assign(l2.begin(), l2.end());
This is to replace the content of l1 with content of l2 in the specified range.
9.12 Deque
Class deque is designed to combine the advantages of vector and list together. Like a vector, a deque can be
randomly accessed via subscripts, and like a list, elements can be conveniently inserted and deleted at both ends
of the deque. Because of this combination, a deque iterator must be more intelligent than a vector iterator.
Insertion and deletion at the middle of a deque is optimized to minimize the number of elements copied.
Header file of deque is <deque>.
deque has two more methods of its own:
push_front
pop_front
9.16 multiset
The elements themselves are used as keys. The ordering of the elements is decided by a comparator method
object such as “less<Type>”.
The type of the key must support appropriate comparison, e.g., keys sorted with “less<Type>” must support
operator<.
A multiset container supports bidirectional iterators.
Header file of multiset is <set>.
♦ Sample Program
typedef multiset<int, less<int> > ims1; // declare an alias of multiset type
ims1 m1; // create an empty multiset
cout << m1.count(15); // count the number of keys with value of 15
m1.insert(15);
This is to insert 15 into the multiset. The second version of insert takes an iterator and a value as arguments and
begins the search for the insertion point from the iterator position specified.
m1.insert(a, a + SIZE);
The above third version takes two iterators as arguments to specify a range of values from another container to
add into the multiset:
9.17 set
The methods of set are identical to multiset, except that a set must not have duplicated keys.
typedef set< double, less<double> > double_set;//define an alias of a set type
double_set s1(a, a + size); // create a set out of part of an array
pair< double_set1::const_iterator, bool > p;
p = s1.insert(13.8);
Method "insert" will first search for the element of 13.8. If element found, it will return an iterator to the found
element and a false value to indicate that the value was not inserted. If the element is not found, it will insert that
value in the set, return an iterator to the inserted element and a true value to indicate the successful insertion.
9.18 multimap
Elements of maps are sorted and organized very similar to sets. Many of their methods are the same. The only
difference is that in a map a separate key is used to represent a value. Both the key and the value can be of any
type. Just as the sets, the ordering of the keys is determined by a predicate method such as less<Type>. The
maps support bidirectional iterators.
Multimaps allows duplicated keys, so you can have duplicated values with the same key. This is called "one-to-
many" relationship.
typedef multimap< int, double, less<int> > mmid1;
mmid1 m1;
m1.insert( mmid1::value_type( 15, 2.73 ) );
"value_type" is one of those pre-defined types just like iterators, const_iterators. It represents the type used in
the container.
9.19 map
Duplicated keys are not allowed in a map, so only a single value can be associated with each key. This is called
a "one-to-one mapping".
Because of this "one-to-one mapping", you can specify the key and get back the associated value quickly. A map
is also called an "associative array", for you can provide the key in subscript operator [] and locate the
element. Insertion and deletion can be done anywhere efficiently.
typedef map< int, double, less<int> > mid;
mid m1;
m1.insert( mid::value_type(15, 2.73) );
m1[13] = 8.93;
When key 13 is in the map, operator[ ] returns a reference to the element, so that it can be assigned 8.93. If key
13 is not in the map, operator[ ] inserts the key and returns a reference to the value.
9.21 Stack
A stack enables insertion and deletion at the same end of the underlying data structure, commonly referred to as a
last-in-first-out data structure. A stack can be implemented with any of the sequence containers: vector, list and
deque. By default it is deque. For best performance, use vector or deque as the underlying container.
All stack methods are implemented as inline to avoid an extra method call.
Header file of stack is <stack>.
♦ Methods
push: insert an element by calling underlying container method push_back
pop: remove an element by calling pop_back
top: return a reference to the top element by calling back
empty: determine if the stack is empty by calling empty
size: return the size of the stack by calling stack
♦ Sample Program
9.22 queue
A queue enables insertion at one end of the underlying container, and deletion from the other end. This is
referred to as FIFO data structure. A queue can be implemented with deque and list. By default it is deque. It
can perform better than list.
All queue methods are implemented as inline to avoid an extra method call.
Header file of queue is <queue>.
♦ Methods
push: insert at the back by calling underlying container method push_back
pop: remove at the front by calling pop_front (that is the reason why vector can not be used)
front: return a reference to the first element by calling front
back: return a reference to the last element by calling back
empty
size
9.23 priority_queue
A priority_queue can be implemented with vector and deque. By default it is vector. A priority_queue is a
sorted vector. When adding elements to priority_queue, the elements are automatically inserted in priority
order, so that the highest priority element i.e. the largest element is always the first to be removed. This is
accomplished by using a sorting technique called "heapsort", and such a data structure is called a "heap".
The ordering of the elements is performed with comparator method object less<Type>. Programmer can also
supply their own comparator.
♦ Methods
push: insert an element at the proper location, by calling push_back then sorting the container using heapsort
pop: remove the highest-priority element by calling pop_back
top: return a reference to the top element by calling front
empty
size
fill_n(c1.begin(), 5, ‘A’);
set the first 5 elements to be ‘A’. Takes at least output iterators.
generate_n(c1.begin(), 5, generate);
set first 5 elements. Takes at least output iterator.
bool myJudge(int x)
{ return x > 9; }
eliminates all elements that the user-defined unary predicate method will return true. It does the same thing on
eliminated elements like remove. Takes at least forward iterators.
9.37 Heapsort
Heapsort is a sorting method, in which an array of elements is arranged into a special binary tree called a heap.
The key features of a heap are that the largest element is always at the top of the heap, and the values of the
children of any node in the binary tree are always less than or equal to that node’s value. Such a heap is called a
maxheap.
make_heap(v1.begin(), v1.end());
take the values and create a heap, so that it can be sorted. Only takes random-access iterators – therefore only can
be used on arrays, vectors and deques.
sort_heap(v1.begin(), v1.end());
sort a sequence which had been arranged in a heap. Only takes random-access iterators.
push_heap(v1.begin(), v1.end());
add a new value into a heap. Each time push_heap is called, it assumes that the last element in the vector is the
one newly added and all the rest elements have already been arranged as a heap. Only takes random-access
iterators.
for ( i = 0; i < v1.size(); ++i)
pop_heap(v1.begin(), v1.end() – i);
swap the top element with the one before v1.end – i. Finally results in a sorted sequence. The method assumes
that the range of values specified by the two arguments has already been a heap.
10.1 bool
In C++ zero represents false and non-zero represents true. But bool type true and false is clearer and is preferred.
class Base {
public:
Method1() {};
};
if(dynamic_cast<Derived *>(ptr) == 0)
♦ const_cast
const_cast casts away const or volatile. It only works on pointers, not objects. You can use it to modify a data
member in a const method:
#ifndef POINTER_H
#define POINTER_H
return *this;
}
~ Pointer()
{ if(owner == true) delete ptr; }
private:
Type * ptr;
bool owner;
};
#endif
Such a pointer object is fully intelligent: it allows shallow copy – multiple pointer objects pointing to the same
server object – to avoid the copy of pointed objects which is most probably unnecessary and time consuming. It
knows to delete the server object to which it is pointing, and meanwhile avoiding multiple deleting on the same
object.
The key point of this class is an extra data member “owner”, representing whether a pointer object is the owner of
its server object. There may be multiple pointer objects pointing to the same server object, but among them there
is only one owner, and only the owner will delete the server object.
To accomplish this, in its copy constructor and assignment operator, it transfers the ownership from “right” object
to “this” object, setting the right object’s owner to be false. Considering that the right object in these two methods
must be constant (otherwise it can not be applied on constant objects), we need const_cast to cast away the const
of the right object before we modify its data member “owner”.
♦ reinterpret_cast
reinterpret_cast is used for cases that yeilds implementation-dependent results, such as casing between function
pointer types.
Because it allows you to do nonstandard strange conversions, dangerous manipulations can be done, and the
result may be machine-dependent.
10.3 Namespace
Each namespace defines a scope where global identifiers and variables are placed:
namespace Test1 {
const double PI = 3.14159;
const double E = 2.71828
void print();
namespace Inner {
enum Day {Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday};
}
}
void Test1::print()
{ cout << PI << Inner::Monday; }
Notice that unlike classes, there is no semicolon after the definition.
Namespace can be nested.
The definition of a namespace must either occupy the scope or be nested within other namespace.
Because method “print” is a member of namespace Test1, it can directly access other data members of Test1, and
access nested namespace Inner through its name.
To use a namespace member, the member’s name must be qualified with the namespace name and binary scope
resolution operator:
int main()
{
Base1 * pBase1 = new Derived;
const type_info & t1 = typeid(pBase1);
const type_info & t2 = typeid(*pBase1);
cout << "typeid(pBase1) = " << t1.name() << endl;
cout << "typeid(*pBase1) = " << t2.name() << endl;
}
If class Base and Derived are polymorphic types i.e. they have virtual functions, the output will be:
class Base {
public:
Base(int a) : member(a)
{ cout << "Base constructor called with " << a << endl; }
int member;
};
int main()
{ test(333); }
The output will be:
class Base {
public:
explicit Base(int a) : member(a)
{ cout << "Base constructor called with " << a << endl; }
int member;
};
int main()
{
test( static_cast<Base>(333) );
}
class Person {
private:
char * m_strName;
public:
Person(char * name = NULL) : m_strName(name)
{}
~Person()
{
if(m_strName != 0)
delete m_strName;
}
char * Detach()
{
char * temp;
temp = m_strName;
m_strName = 0;
return temp;
}
}