Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Pro Visual C++-CLI And The .NET 2.0 Platform (2006) [eng]-1

.pdf
Скачиваний:
70
Добавлен:
16.08.2013
Размер:
24.18 Mб
Скачать

98

C H A P T E R 3 O B J E C T - O R I E N T E D C + + / C L I

The syntax of defining member variables is identical to that of ordinary variables except for one important thing: You cannot initialize a variable in its definition. I explain how to initialize variables later in this chapter when I cover constructors. The definition of a variable is simply a data type and a variable name, or a comma-delimited list of variable names:

ref class varExample

{

int x;

String ^str1, ^str2;

};

In C++/CLI, member variables can be either managed data types or a pointer to an unmanaged data type.

Unsafe Code The pointer to an unmanaged data type causes the entire ref class to become unsafe.

Member variables can be public, protected, or private. With C++/CLI and ref classes, public member variables should be handled with care, especially if invalid values in these variables will cause problems in the program’s execution. A better solution is to make them private (or protected, so that inherited access can still access them directly), and then make public properties to them for external methods to access. Properties can, if coded correctly, perform validation on the data entered into the variable. Otherwise, they work just like normal member variables. I cover properties later in this chapter.

Static Member Variables

Static member variables are variables that provide ref class-wide storage. In other words, the same variable is shared by all instances of a ref class. At first glance, you might wonder why you would want a shared member variable across all instances of and ref class type. A couple of reasons that you will frequently come across for using static member variable is as a counter of how many instances of the ref class have been created and how many of those instances are currently active. Though, I’m sure you will come up with several other reasons to use them.

To define a static member variable in a ref class, simply define it as static and assign a value to it in the ref class definitions, like this:

ref class staticVar

{

static int staticVar = 3;

};

You might be wondering how initializing the variable within the ref class can work, as it would appear that the value would be reset for each instance of the ref class. Fortunately, this is not the case; only the first time that the ref class is instantiated is the variable created and initialized.

Member Methods

A member method is simply a fancy term that means that the function is declared within a ref class. Everything you learned about functions in the previous chapter is applicable to member methods. You might consider revisiting Chapter 2’s section on functions if you are uncertain how they are defined or how they work.

C H A P T E R 3 O B J E C T - O R I E N T E D C + + / C L I

99

Like all members of a ref class, member methods can be public, protected, or private. Public methods are accessible outside of the ref class and are the workhorse of interclass communication. It is via methods that ref classes pass messages, requesting and being requested to perform some type of functionality. Protected member methods are the same as private member methods except that inherited ref classes have access to them. Private ref classes encapsulate the functionality provided by the ref class, as they are not accessible from outside the ref class except via some public member method that uses it.

Just as a quick recap, Listing 3-3 is a public member method that calls a protected member method that calls a private member method.

Listing 3-3. Member Methods in Action

using namespace System;

ref class MethodEx

{

public:

void printPublic(int num)

{

for (int i = 0; i < num; i++)

{

Console::WriteLine( "Public" );

}

printProtected(num/2);

}

protected:

void printProtected(int num)

{

for (int i = 0; i < num; i++)

{

Console::WriteLine( "Protected" );

}

printPrivate(num/2);

}

private:

void printPrivate(int num)

{

for (int i = 0; i < num; i++)

{

Console::WriteLine( "Private" );

}

}

};

int main()

{

MethodEx ex;

ex.printPublic(4);

 

 

 

 

//

ex.printProtected(4);

//

Error

cannot

access

//

ex.printPrivate(4);

//

Error

cannot

access

}

100

C H A P T E R 3 O B J E C T - O R I E N T E D C + + / C L I

Figure 3-4 shows the results of this little program.

Figure 3-4. Results of MethodEx.exe

Static Member Methods

Static member methods are methods that have ref class scope. In other words, they exist without your having to create an instance of the ref class. Because they are not associated with any particular instance of a ref class, they can use only static member variables, which also are not associated with a particular instance. For the same reason, a static member method cannot be a virtual member method, as virtual member methods are also associated with ref class instances.

Coding static member methods is no different from coding normal member methods, except that the function declaration is prefixed with the static keyword.

Listing 3-4 uses a static member method to print out a static member variable. Oh, by the way, WriteLine() is also a static member method.

Listing 3-4. Static Member Methods and Variables in Action

using namespace System;

ref class StaticTest

{

private:

static int x = 42; public:

static int get_x()

{

return x;

}

};

void main()

{

Console::WriteLine ( StaticTest::get_x() );

}

Figure 3-5 shows the results of this little program.

C H A P T E R 3 O B J E C T - O R I E N T E D C + + / C L I

101

Figure 3-5. Results of StaticMethodEx.exe

You might have noticed that to access the static member method, you use the ref class name and the :: operator instead of the . or -> operator. The reason is because you’ve not created an object, so you’re effectively accessing the namespace tree.

ref class Constructors

The ref class constructor is a special ref class method that is different in many ways from the member method. In C++/CLI, a constructor is called whenever a new instance of a ref class is created. Instances of ref classes are created using the operator gcnew. Memory for the instance is allocated on the managed heap that is maintained by the CLR.

The purpose of the constructor is to get the object to an initialized state. There are two ways that the actual initialization process can take place, within the constructor or in a separate method that the constructor calls. Normally, you would use the first method if the ref class instance is only initialized once. The second method would be used if the ref class is reused and needs to be reinitialized at a later time. When this reinitialization takes place the initialization method is called, as you can’t call the constructor method directly.

The ref class constructor process differs from the unmanaged class constructor process in that, for ref class constructors, all member variables are initialized to zero before the actual constructor is called. (Although this is helpful, initialization to zero is not always what you need. You might want to initialize to a specific value.) Thus, even if the constructor does nothing, all member variables will still have been initialized to zero or the data type’s equivalent. For example, the DateTime data type initializes to 1/1/0001 12:00:00 a.m., which is this data type’s equivalent of zero.

A ref class constructor method always has the same name as the ref class itself. A ref class constructor method does not return a value and must not be defined with the void return type.

A constructor method can take any number of parameters. Note that a constructor method needs to have public accessibility to be accessible by the gcnew operator.

If no constructor method is defined for a ref class, then a default constructor method is generated. This constructor method does nothing of its own, except it calls the constructor method of its parent and sets all member variables to a zero value. If you define a constructor method, then a default constructor method will not be generated. Thus, if you create a constructor method with parameters and you expect the ref class to be able to be created without parameters, then you must manually create your own default zero-parameter constructor method.

A special construct of a constructor method is the initializer list. It’s a list of variables that need to be initialized before the constructor method itself is called. You can use it to initialize the ref class’s own variables as well; in fact, it’s more efficient to do it this way, but it’s also much harder to read in this format. The most common use of an initializer list is to initialize a parent ref classes by way of one of the parent’s constructors. The syntax for an initializer list involves simply placing a

102

C H A P T E R 3 O B J E C T - O R I E N T E D C + + / C L I

colon (:) and a comma-delimited list of functional notation variable declarations between the constructor method’s declaration and the constructor method’s implementation:

Constructor (int x, int y, int z) : var1(x, y), var2(z) { }

A new constructor type has been added in C++/CLI, the copy constructor. The copy constructor initializes a ref class object to be a copy of an existing ref class object of the same type. This type of constructor should not be anything new to developers who have already been coding in C++, except that the syntax has change a little. You now use the % operator instead of the & operator.

Listing 3-5 shows the constructors for a ref class called ChildClass inherited from ParentClass.

Listing 3-5. Constructors in Action

using namespace System;

// Parent Class

ref class ParentClass

{

public:

//Default constructor that initializes ParentVal to a default value ParentClass() : PVal(10) { }

//A constructor that initializes ParentVal to a passed value ParentClass(int inVal) : PVal(inVal) { }

//Copy Constructor

ParentClass(const ParentClass %p) : PVal(p.PVal) {}

int PVal;

};

// Child class that inherits form ParentClass ref class ChildClass : public ParentClass

{

public:

// Default

constructor that initializes ChildVal to a default value

ChildClass

() : CVal(20) {};

// default constructor

// A constructor that initialized CVal to a passed value ChildClass(int inVal) : CVal(20) {};

//A constructor that initialized the parent class with a passed value

//and initializes ChildVal to a another passed value

ChildClass (int inVal1, int inVal2) : ParentClass(inVal1), CVal(inVal2) { }

// copy constructor

ChildClass(const ChildClass %v) : ParentClass(v.PVal), CVal(v.CVal) { }

int CVal;

};

C H A P T E R 3 O B J E C T - O R I E N T E D C + + / C L I

103

void main()

 

 

{

 

 

ParentClass p1(4);

// Constructor

ParentClass p2 = p1;

//

Copy Constructor

p1.PVal = 2;

//

Change original, new unchanged

Console::WriteLine("p1.PVal=[{0}] p2.PVal=[{1}]", p1.PVal, p2.PVal);

ChildClass ^c1

= gcnew ChildClass(5,6); //

Constructor

ChildClass

c2

= *c1;

//

Copy Constructor

c1->CVal =

12;

 

// Change original, new unchanged

Console::WriteLine("c1=[{0}/{1}] c2=[{2}/{3}]", c1->PVal, c1->CVal, c2.PVal, c2.CVal);

}

Figure 3-6 shows the results of this little program.

Figure 3-6. Results of Constructors.exe

Static ref class Constructors

In traditional C++, the syntax for initializing static member variables was rather cumbersome. It forced you to define it in the ref class and then initialize it outside the ref class before the main() function was called. You saw that with ref classes you could directly assign a value to a static member variable—but what happens if you need something more elaborate than a simple assignment? C++/CLI has provided a new construct for ref classes called the static ref class constructor.

The static ref class constructor’s purpose is to initialize static member variables normally with something more complex than a simple assignment, but not necessarily. Any ref class can have a static ref class constructor, though it only really makes sense if the ref class has static member variables, because the static ref class constructor is not allowed to initialize any nonstatic member variables.

When the static ref class constructor is invoked it is undefined, but it is guaranteed to happen before any instances of the ref class are created or any references are made to any static members of the ref class.

If you recall, it is possible to initialize static member variables directly in the definition of the ref class. If you use the static ref class constructor, then these default values are overwritten by the value specified by the static ref class constructor.

104

C H A P T E R 3 O B J E C T - O R I E N T E D C + + / C L I

The static ref class constructor syntax is identical to the default constructor syntax, except that the static keyword is placed in front. This means that a static ref class constructor cannot take any parameters.

In the following example, the ref class Test is made up of two static member variables initialized to 32 and a static ref class constructor that overwrites the first constant with the value of 42:

ref class Test

{

public:

static Test()

{

value1 = 42;

}

static int value1 = 32; static int value2 = 32;

};

By the way, you can have both static and nonstatic (normal, I guess) constructor methods in the same ref class.

Destructors

Destructors serve two purposes in C++/CLI. The first is the deallocating of memory previously allocated to a heap by either the new or gcnew operator. The second purpose is the often overlooked releasing of system resources, either managed or unmanaged.

In versions prior to C++/CLI, Managed Extensions for C++ only handled the deallocating of managed memory and managed resources. To handle the releasing of unmanaged memory or unmanaged resources, it forced the developer to inherit from a .NET Framework interface IDisposable. If this means nothing to you, don’t worry—things have gotten a whole lot easier.

Memory Management Destructors

All objects allocated on the managed heap using gcnew need to be deallocated. You have a choice about how to do this. You can call the delete operator on the handle of the ref class object, and the managed memory will be deallocated immediately in reverse order to which it was allocated. In other words, if the object allocated internal objects, then they would be deallocated first.

To start the deallocation process when a program is finished with the object, you simply call the delete operator on the object:

delete classname;

This is new to C++/CLI and is what is known as deterministic cleanup. The programmer now has complete control of when things are finally cleaned up.

The other choice is that the developer does not have to give up the garbage collection functionality of the CLR, if it doesn’t matter when the memory is finally deallocated. In this case, the program is not required to call the delete operator; it simply has does nothing. It will be the job of the CLR to detect when an object is no longer being accessed and then garbage collect it. In most cases, this choice is the best, and there is really no need to call the delete operator because CLR garbage collection works just fine.

The destructor method, which the delete operator calls, has the same syntax as the default constructor method except that a tilde (~) is placed before the destructor method’s name:

~Test() {}

// destructor

C H A P T E R 3 O B J E C T - O R I E N T E D C + + / C L I

105

Note For the delete operator to be able to access the destructor, the destructor needs public access.

Within the destructor, you would call the delete operator for any objects that the ref class needs to clean up. If your ref class doesn’t allocate anything while it was running, then there is no need to create a destructor as a default one will be generated for you.

Resource Management Destructors

Cleaning up managed resources like String or an ArrayList is handled just like managed memory resources.

Unmanaged resources, like open handles to files, kernel objects, or database objects, are a little trickier. The reason is if you don’t specifically code the closing of these objects, then they don’t get closed until the program ends or sometimes not until the machine is rebooted. Yikes!

Surprisingly, the CLR does not even have any explicit runtime support for cleaning up unmanaged resources. Instead, it is up to the programmer to implement a pattern for resource management based on a .NET Framework core interface IDisposable. The pattern is to place all resources that needed to be cleaned up within the Dispose() method exposed by the IDisposable interface and then call the Dispose() method when the resources are no longer needed. A little cumbersome, if you ask me.

With Managed Extensions for C++ version 1.1 and prior, that is exactly how you would have had to code unmanaged resource cleanup. In version C++/CLI, things have become a whole lot easier. With the addition of deterministic cleanup to C++/CLI, the delete operator now implements the IDisposable interface pattern automatically for you. Therefore, all you need to do to clean up your unmanaged resources is to add code to your ref class’s destructor and then call the delete operator when the object and the resources it's accessing are no longer needed. Simple, right? Well, there is a catch.

What if you forget to call the delete operator? The answer is, unfortunately, that the destructor and subsequently the IDisposable interface pattern are not called. This means the unmanaged resources are not cleaned up. Ouch! So much for .NET’s great ability to clean up after itself, right?

Fortunately, this is not the end of the story. The CLR’s garbage collection process has not yet occurred. This process will deallocate all managed objects whenever it gets around to that chore (you have no control of this). As an added bonus, C++/CLI has made things easier by providing an interface directly with the CLR garbage collection process (for when this finally does happen) called the Finalize destructor.

The Finalize destructor method is called by the CLR, when the CLR detects an object that needs to be cleaned up. An elegant solution, don’t you think? Well, the elegance doesn’t end there. The CLR, before it calls the Finalize destructor, checks to see if the delete operator has been already called on the object and, if so, does not even waste its time on calling the Finalize destructor.

What does this boil down to? You can clean up unmanaged resource yourself, or if you don’t care when cleanup finally does occur (or you forget to do it), the CLR will do the cleanup for you. Nice, huh?

The Finalize destructor has the same syntax as the standard destructor, except an exclamation point (!) is used instead of a tilde (~), and it has to have protected access:

protected:

 

!Test() {}

// Finalize destructor

Note The Finalize destructor must have protected access.

106

C H A P T E R 3 O B J E C T - O R I E N T E D C + + / C L I

Here is how you code destructor logic, if you want all your bases covered for an object that has managed and unmanaged memory and resources to clean up:

ref class ChildClass : public ParentClass

{

public:

~Test()

{

// free all managed and unmanaged resources and memory

}

protected:

!Test()

{

// free all unmanaged resources and memory only

}

}

The managed cleanup code is only found in the deterministic cleanup destructor, whereas unmanaged cleanup is found in both the deterministic cleanup and Finalize destructor. One thing you will find is that there is usually duplicate unmanaged memory and resource cleanup code in both of these destructors. Most likely, you will write an additional method, which these destructors call to eliminate this duplication.

Virtual Methods

Virtual methods are the cornerstone of polymorphism, as they allow different child ref classes derived from a common base ref class to respond to the same method call in a way specific to each child ref class. Polymorphism occurs when a virtual method is called through a base ref class handle. For example:

BaseClass ^BaseObject = gcnew ChildClass()

BaseObject->DoStuff() // will call the Child class version instead of the Base // class version as long as DoStuff is declared a virtual.

This works because when the call is made, it is the type of the actual object pointed to that determines which copy of the virtual method is called.

Technically, when you declare a virtual method, you are telling the compiler that you want dynamic or runtime binding to be done on any method with an identical signature in a derived ref class. To make a method virtual, you simply need to place the keyword virtual in front of the method declaration.

virtual void Speak () {}

Any method that you declare as virtual will automatically be virtual for any directly or indirectly derived ref class.

Normally, in a standard virtual animal example, you would first declare a base ref class Animal with a virtual method of Speak(). You then create specific animal-type ref classes derived from Animal and override the virtual method Speak(). In the main() function, you would create an array of Animal objects and assign specific animal derived objects to it. Finally, you would loop through the Animal array. Because the Speak() method is virtual, the actual object type assigned to the Animal array determines which Speak() to execute.

C H A P T E R 3 O B J E C T - O R I E N T E D C + + / C L I

107

There are two methods of overriding a virtual function: implicit and explicit (or named). You can also hide the virtual override sequence and start a new one, or you can simply stop the virtual sequence altogether.

Implicit Virtual Overriding

For implicit overriding, the method signature of the base ref class must be the same as the derived ref class including the prefix virtual. This means that the name of the method and the number of parameters and their types must be identical. The return type of the method need not be identical, but it must at least be derived from the same type as that of the base method’s return type. Also, you need to append the new keyword override after the parameters:

virtual void Speak () override

{

}

Hiding Virtual Overriding

Usually, if a parent defines a method as virtual, there is usually a good reason. I haven’t yet had a reason to do this, but if you really want to overrule a parent ref class and hide a method from propagating virtual overriding to its children, you can. To do so, add the keyword new after the method declaration:

void Speak() new

{

}

You can also hide virtual overriding propagation and start a new one from the current ref class by making the above member method virtual:

virtual void Speak() new

{

}

To me, both of these method declarations go against proper OOP, but they are available if there is good reason.

Explicit or Named Virtual Overriding

Explicit or named overriding allows you to assign a method with a different name to a virtual function. To do this, you need to declare the overriding method as virtual and then assign the name of the virtual method being overridden:

ref class Puppy : public Dog

{

public:

virtual void Yip () = Dog::Speak

{

}

};

One handy feature of explicit overriding is that it allows you to overcome a virtual sequence that has been hidden by the new operator, as an explicit overriding does not need to override a direct parent. It can override an indirect parent, occurring before the virtual method sequence hide point, by specifying the ref class name of a grandparent (or great grandparent, or . . .) along with the method being overridden: