
Beginning Visual C++ 2005 (2006) [eng]-1
.pdf
Class Inheritance and Virtual Functions
Destructors and Finalizers in Reference Classes
You can define a destructor for a reference class in the same way as you define a constructor for a native C++ class. The destructor for a reference class is called when the handle goes out of scope or the object is part of another object that is being destroyed. You can also apply the delete operator to a handle for a reference class object and that results in the destructor being called. The primary reason for implementing a destructor for a native C++ class is to deal with data members allocated on the heap, but obviously that doesn’t apply to reference classes so there is less need to define a destructor in a ref class. You might do this when objects of the class are using other resources that are not managed by the garbage collector, such as files that need to be closed in an orderly fashion when an object is destroyed. You can also clean up such resources in another kind of class member called a finalizer.
A finalizer is a special kind of function member of a reference class that is called automatically by the garbage collector when destroying an object. Note that the finalizer is not called for a class object if the destructor was called explicitly or was called as a result of applying the delete operator to the object. In a derived class, finalizers are called in the same sequence as destructor calls would be, so the finalizer for the basest class is called first, followed by the finalizers for successive classes in the hierarchy with the finalizer for the most derived class being called last.
You define a finalizer in a class like this:
public ref class MyClass
{
//Finalizer definition !MyClass()
{
//Code to clean-up when an object is destroyed...
}
//Rest of the class definition...
};
You define a finalizer function in a class in a similar way to a destructor, but with ! instead of the ~ that you use preceding the class name for a destructor. Similar to a destructor, you must not supply a return type for a finalizer, and the access specifier for a finalizer will be ignored. You can see how destructors and finalizers operate with a little example.
Try It Out |
Finalizers and Destructors |
This example shows when destructors and finalizers get called in an application:
//Ex9_20.cpp : main project file.
//Finalizers and destructors
#include “stdafx.h”
using namespace System;
ref class MyClass
}
public:
549

Chapter 9
// Constructor
MyClass(int n) : value(n){}
//Destructor ~MyClass()
{
Console::WriteLine(“MyClass object({0}) destructor called.”, value);
}
//Finalizer
!MyClass()
{
Console::WriteLine(“MyClass object({0}) finalizer called.”, value);
}
private: int value;
};
int main(array<System::String ^> ^args)
{
MyClass^ obj1 = gcnew MyClass(1); MyClass^ obj2 = gcnew MyClass(2); MyClass^ obj3 = gcnew MyClass(3); delete obj1;
obj2->~MyClass();
Console::WriteLine(L”End Program”); return 0;
}
The output from this example is:
MyClass object(1) destructor called.
MyClass object(2) destructor called.
End Program
MyClass object(3) finalizer called.
Press any key to continue . . .
How It Works
The MyClass class has a constructor, a destructor, and a finalizer. The destructor and finalizer just write output to the command line so you know when each is called. You are also able to tell for which object the finalizer or destructor was called because they output the value of the value field.
In the main() function, you create three objects of type MyClass encapsulating values 1, 2, and 3 to distinguish them. You then apply the delete operator to obj1 and then explicitly call the destructor for obj2. This results in the first two lines of output that is generated by the destructor calls for the objects that results from these explicit actions.
The next line of output is produced by the statement preceding the return statement in main(), so the last line of output generated by the finalizer for obj3 occurs after the end of main(). The output shows that constructors are called when you delete an object or call its destructor explicitly, the destructor for
550

Class Inheritance and Virtual Functions
the object will be executed, and these operations also suppress the execution of the finalizers for the objects. The object referenced by obj3 is destroyed by the garbage collector when the program ends so the finalizer gets called to clean up any non-managed resources.
Thus, if a class has a finalizer and a destructor, only one of these is called when the object is destroyed, the destructor is called if you programmatically destroy the object, and the finalizer is called if it dies naturally by going out of scope. You can also deduce from this that if you rely on a finalizer to clean up after your objects have been destroyed, you should not explicitly delete the objects.
If you comment out the statements in main() that destroy obj1 and obj2, you will see that the finalizers for these objects are called by the garbage collector when the program ends. On the other hand, if you comment out the finalizer from MyClass, you will see that the destructor for obj3 does not get called by the garbage collector so no clean up occurs. You can conclude that if you want to be sure that unmanaged resources used by an object are taken care of regardless of how an object is terminated, you should implement both a destructor and a finalizer in the class.
Generic Classes
C++/CLI provides you with the capability for defining generic classes where a specific class is instantiated from the generic class type at run-time. You can define generic value classes, generic reference classes, generic interface classes, and generic delegates. You define a generic class using one or more type parameters in a similar way to generic functions that you saw in Chapter 6.
For example, here’s how you could define a generic version of the Stack class you saw in Ex9_14:
//Stack.h for Ex9_21
//A generic pushdown stack
generic<typename T> ref class Stack
{
private:
// Defines items to store in the stack
ref struct Item |
|
{ |
|
T Obj; |
// Handle for the object in this item |
Item^ Next; |
// Handle for next item in the stack or nullptr |
// Constructor
Item(T obj, Item^ next): Obj(obj), Next(next){}
}; |
|
Item^ Top; |
// Handle for item that is at the top |
public:
// Push an object on to the stack void Push(T obj)
{
Top = gcnew Item(obj, Top); // Create new item and make it the top
}
// Pop an object off the stack
551

Chapter 9
T Pop() |
|
{ |
|
if(Top == nullptr) |
// If the stack is empty |
return T(); |
// return null equivalent |
T obj = Top->Obj; Top = Top->Next; return obj;
//Get object from item
//Make next item the top
}
};
The generic version of the class now has a type parameter, T. Note that you could use the class keyword instead of the typename keyword when specifying the parameter — there is no difference between them in this context. A type argument replaces T when the generic class type is used; T is replaced by the type argument through the definition of the class so a major advantage over the original version is that the generic class type is much safer without losing any of its flexibility. The Push() member of the original class accepts any handle, so you could happily push a mix of objects of type MyClass^, String^, or indeed any handle type onto the same stack, whereas an instance of the generic type accepts only objects of the type specified as the type argument of objects of a type that have the type argument as a base.
Look at the implementation of the Pop() function. The original version returned nullptr if the top item in the stack was null, but you can’t return nullptr for a type parameter because the type argument could be a value type. The solution is to return T(), which is a no-arg constructor call for type T. This results in the equivalent of 0 for a value type and nullptr for a handle.
Note that you can specify constraints on a generic class type parameter using the where keyword in the same way as you did for generic functions in Chapter 6.
You could create a stack from the Stack<> generic type that stores handles to Box objects like this:
Stack<Box^>^ stack = gcnew Stack<Box^>;
The type argument Box^ goes between the angled brackets and the statement creates a Stack<Box^> object on the CLR heap. This object allows handles of type Box^ to be pushed onto the stack as well as handles of any type that has Box as a direct or indirect base class. You can try this out with a revised version of Ex9_14.
Try It Out |
Using a Generic Class Type |
Create a new CLR console program with the name Ex9_21 and then copy the header files Container.h, Box.h, and GlassBox.h from Ex9_14 to the directory for this project. Add these headers to the project by right-clicking Header Files in the Solution Explorer tab and selecting Add > Existing Item from the context menu. You can then add a new header file, Stack.h, to the project and enter the generic Stack class definition that you saw in the previous section. Don’t forget the #pragma once directive at the beginning of the file.
//Ex9_21.cpp : main project file.
//Using a nested class to define a stack
#include “stdafx.h”
552

|
|
Class Inheritance and Virtual Functions |
|
|
|
|
|
|
|
#include “Box.h” |
// For Box and Container |
|
#include “GlassBox.h” |
// For GlassBox (and Box and Container) |
|
#include “Stack.h” |
// For the generic stack class |
|
using namespace System; |
|
int main(array<System::String ^> ^args)
{
array<Box^>^ boxes = { gcnew Box(2.0, 3.0, 4.0), gcnew GlassBox(2.0, 3.0, 4.0), gcnew Box(4.0, 5.0, 6.0), gcnew GlassBox(4.0, 5.0, 6.0)
};
Console::WriteLine(L”The array of boxes have the following volumes:”);
for each(Box^ box in boxes) |
|
box->ShowVolume(); |
// Output the volume of a box |
Console::WriteLine(L”\nNow pushing the boxes on the stack...”); |
|
Stack<Box^>^ stack = gcnew Stack<Box^>; |
// Create the stack |
for each(Box^ box in boxes) |
|
stack->Push(box); |
|
Console::WriteLine(
L”Popping the boxes off the stack presents them in reverse order:”);
Box^ item;
while((item = stack->Pop()) != nullptr) safe_cast<Container^>(item)->ShowVolume();
// Try the generic Stack type storing integers |
|
Stack<int>^ numbers = gcnew Stack<int>; |
// Create the stack |
Console::WriteLine(L”\nNow pushing integers on to the stack:”); for(int i = 2 ; i<=12 ; i += 2)
{
Console::Write(L”{0,5}”,i); numbers->Push(i);
}
int number;
Console::WriteLine(L”\n\nPopping integers off the stack produces:”); while((number = numbers->Pop()) != 0)
Console::Write(L”{0,5}”,number);
Console::WriteLine(); return 0;
}
This example produces the following output:
The array of boxes have the following volumes:
CBox usable volume is 24
CBox usable volume is 20.4
553

Chapter 9
CBox usable volume is 120
CBox usable volume is 102
Now pushing the boxes on the stack...
Popping the boxes off the stack presents them in reverse order:
CBox usable volume is 102
CBox usable volume is 120
CBox usable volume is 20.4
CBox usable volume is 24
Now pushing integers |
on to the stack: |
||||
2 |
4 |
6 |
8 |
10 |
12 |
Popping integers off the stack produces: 12 10 8 6 4 2
Press any key to continue . . .
How It Works
The stack to store Box object handles is defined in the statement:
Stack<Box^>^ stack = gcnew Stack<Box^>; |
// Create the stack |
The type parameter is Box^ so the stack stores handles to Box object or handles to GlassBox objects. The code to push objects on to the stack and to pop them off again is exactly the same as in Ex9_14 and the output is the same, too. The difference here is that you could not push an object on to the stack if the type did not have Box as a direct or indirect base class so the generic type guarantees that all the objects are Box objects.
Storing integers now requires a new Stack<> object:
Stack<int>^ numbers = gcnew Stack<int>; |
// Create the stack |
The original version used the same non-generic Stack object to store Box object references and integers thus demonstrating how type safety was completely lacking in the operation of the stack. Here you specify the type argument for the generic class as the value type, int, so only objects of this type are accepted by the Push() function.
The loops that pop objects off the stack demonstrate that returning T() in the Pop() function does indeed return 0 for type int and nullptr for the handle type Box^.
Generic Interface Classes
You can define generic interfaces in the same way as you define generic reference classes and a generic reference class can be define in terms of a generic interface. To show how this works, you can define a generic interface that can be implemented by the generic class, Stack<>. Here’s a definition for a generic interface:
// Interface for stack operations generic<typename T> public interface class IStack
{
void Push(T obj); |
// Push an item on to the stack |
T Pop(); |
|
};
554

Class Inheritance and Virtual Functions
This interface has two functions identifying the push and pop operations for a stack.
The definition of the generic Stack<> class that implements the IStack<> generic interface is:
generic<typename T> ref class Stack : IStack<T>
{
private:
// Defines items to store in the stack
ref struct Item |
|
{ |
|
T Obj; |
// Handle for the object in this item |
Item^ Next; |
// Handle for next item in the stack or nullptr |
// Constructor
Item(T obj, Item^ next): Obj(obj), Next(next){}
}; |
|
Item^ Top; |
// Handle for item that is at the top |
public:
// Push an object on to the stack
virtual void Push(T obj) |
|
{ |
|
Top = gcnew Item(obj, Top); |
// Create new item and make it the top |
} |
|
// Pop an object off the stack |
|
virtual T Pop() |
|
{ |
|
if(Top == nullptr) |
// If the stack is empty |
return T(); |
// return null equivalent |
T obj = Top->Obj; |
// Get object from item |
Top = Top->Next; |
// Make next item the top |
return obj; |
|
} |
|
}; |
|
The changes from the previous generic Stack<> class definition are shaded. In the first line of the generic class definition the type parameter, T is used as the type argument to the interface IStack so the type argument used for the Stack<> class instance also applies to the interface. The Push() and Pop() functions in the class now have to be specified as virtual because the functions are virtual in the interface. You could add a header file containing the IStack interface to the previous example and amend the generic Stack<> class definition to the example and recompile the program to see it operating with a generic interface.
Generic Collection Classes
A collection class is a class that organizes and stores objects in a particular way; a linked list and a stack are typical examples of collection classes. The System::Collections::Generic namespace contains a wide range of generic collection classes that implement strongly typed collections. The generic collection classes available include the following:
555

Chapter 9
Type |
Description |
|
|
List<T> |
Stores items of type T in a simple list that can grow in size automatically |
|
when necessary. |
LinkedList<T> |
Stores items of type T in a doubly linked list. |
Stack<T> |
Stores item of type T in a stack, which is a first-in last-out storage mecha- |
|
nism. |
Queue<T> |
Stores items of type T in a queue, which is a first-in first-out storage |
|
mechanism. |
Dictionary<K,V> |
Stores key/value pairs where the keys are of type K and the values are of |
|
type V. |
|
|
I won’t go into details of all these, but I’ll mention briefly just three that you are most likely to want to use in your programs. I’ll use examples that store value types for simplicity, but of course the collection classes work just as well with reference types.
List<T> — A Generic List
List<T> defines a generic list that automatically increases in size when necessary. You can add items to a list using the Add() function and you can access items stored in a List<T> using an index, just like an array. Here’s how you define a list to store values of type int:
List<int> numbers = gcnew List<int>;
This has a default capacity, but you could specify the capacity you require. Here’s a definition of a list with a capacity of 500:
List<int> numbers = gcnew List<int>;
You can add objects to the list using the Add() functions:
for(int i = 0 ; i<1000 ; i++)
numbers->Add( 2*i+1);
This adds 1000 integers to the numbers list. The list grows automatically if its capacity is less than 1000. When you want to insert an item in an existing list, you can use the Insert() function to insert the item specified by the second argument at the index position specified by the first argument. Items in a list are indexed from zero like an array.
You could sum the contents of the list like this:
int sum = 0;
for(int i = 0 ; i<numbers->Count ; i++) sum += numbers[i];
Count is a property that returns the current number of items in the list. The items in the list may be accessed through the default indexed property and you can get and set values in this way. Note that you
556

Class Inheritance and Virtual Functions
cannot increase the capacity of a list using the default indexed property. If you use an index outside the current range of items in the list, an exception is thrown.
You could also sum the items in the list like this:
for each(int n in numbers)
sum +=n;
You have a wide range of other functions you can apply to a list including functions for removing elements, and sorting and searching the contents of the list.
LinkedList<T> — A Generic Doubly Linked List
LinkedList<T> defines a linked list with forward and backward pointers so you can iterate through the list in either direction. You could define a linked list that stores floating-point like this:
LinkedList<double>^ values = gcnew LinkedList<double>;
You could add values to the list like this:
for(int i = 0 ; i<1000 ; i++)
values->AddLast(2.5*i);
The AddLast() function adds an item to the end of the list. You can add items to the beginning of the list by using the AddFirst() function. Alternatively, you can use the AddHead() and AddTail() functions to do the same things.
The Find() function returns a handle of type LinkedListNode<T>^ to a node in the list containing the value you pass as the argument to Find(). You could use this handle to insert a new value before or after the node that you found. For example:
LinkedListNode<double>^ node = values->Find(20.0); // |
Find node containing 20.0 |
|
if(node != nullptr) |
|
|
values->AddBefore(node, 19.9); |
// Insert |
19.1 before node |
|
|
|
The first statement finds the node containing the value 20.0. If it does not exist, the Find() function returns nullptr. The last statement executed if node is not null adds a new value of 19.9 before node. You could use the AddAfter() function to add a new value after a given node. Searching a linked list is relatively slow because it is necessary to iterate through the elements sequentially.
You could sum the items in the list like this:
double sumd = 0;
for each(double v in values) sumd += v;
The for each loop iterates through all the items in the list and accumulates the total in sum.
The Count property returns the number of items in the linked list and the Head and Tail properties return the values of the first and last items. The First and Last properties are alternatives to Head and Tail.
557

Chapter 9
Dictionary<TKey, TValue> — A Generic Dictionary Storing Key/Value Pairs
The generic Dictionary<> collection class requites two type arguments, the first is the type for the key and the second is the type for the value associated with the key. A dictionary is especially useful when you have pairs of objects that you want to store where one object is a key to accessing the other object. A name and a phone number are an example of a key value pair that you might want to store in a dictionary because you would typically want to retrieve a phone number using a name as the key. Suppose you have defined Name and PhoneNumber classes to encapsulate names and phone numbers respectively. You can define a dictionary to store name/number pairs like this:
Dictionary<Name^, PhoneNumber^>^ phonebook = gcnew Dictionary<Name^, PhoneNumber^>;
The two type arguments are Name^ and PhoneNumber^ so the key is a handle for a name and the value is a handle for a phone number.
You can add an entry in the phonebook dictionary like this:
Name^ name = gcnew Name(“Jim”, “Jones”);
PhoneNumber^ number = gcnew PhoneNumber(914, 316, 2233); phonebook->Add(name, number); // Add name/number pair to dictionary
To retrieve an entry in a dictionary you can use the default indexed property(for example:
try
{
PhoneNumber^ theNumber = phonebook[name];
}
catch(KeyNotFoundFoundException^ knfe)
{
Console::WriteLine(knfe);
}
You supply the key as the index value for the default indexed property, which in this case is a handle to a Name object. The value is returned if the key is present, or an exception of type KeyNotFoundException is thrown if the key is not found in the collection; therefore, whenever you are accessing a value for a key that may not be present, the code should be in a try block.
A Dictionary<> object has a Keys property that returns a collection containing the keys in the dictionary as well as a Values property that returns a collection containing the values. The Count property returns the number of key/value pairs in the dictionary.
Let’s try some of these in a working example.
Try It Out |
Using Generic Collection Classes |
This example exercises the three collection classes you have seen:
//Ex9_22.cpp : main project file.
//Using generic collection classes
#include “stdafx.h”
using namespace System;
558