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

Beginning Visual C++ 2005 (2006) [eng]-1

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

Defining Your Own Data Types

Again, this uses the indirect member selection operator. This is the typical notation used by most programmers for this kind of operation, so from now on I’ll use it universally.

Try It Out

Pointers to Classes

Let’s try exercising the indirect member access operator a little more. We will use the example Ex7_10.cpp as a base, but change it a little.

//Ex7_13.cpp

//Exercising the indirect member access operator #include <iostream>

using std::cout; using std::endl;

class CBox

// Class definition at global scope

{

 

public:

 

// Constructor definition

CBox(double lv = 1.0, double bv = 1.0, double hv = 1.0)

{

cout << endl << “Constructor called.”;

m_Length = lv;

// Set values of

m_Width = bv;

// data members

m_Height = hv;

 

}

 

//Function to calculate the volume of a box double Volume() const

{

return m_Length*m_Width*m_Height;

}

//Function to compare two boxes which returns true (1)

//if the first is greater that the second, and false (0) otherwise int Compare(CBox* pBox) const

{

return this->Volume() > pBox->Volume();

}

private:

 

double m_Length;

// Length of a box in inches

double m_Width;

// Width of a box in inches

double m_Height;

// Height of a box in inches

};

 

int main()

 

{

 

CBox boxes[5];

// Array of CBox objects declared

CBox match(2.2, 1.1, 0.5);

// Declare match box

CBox cigar(8.0, 5.0, 1.0);

// Declare cigar Box

369

Chapter 7

CBox* pB1 = &cigar;

// Initialize pointer to cigar object address

CBox* pB2 = 0;

 

// Pointer to CBox initialized to null

cout << endl

 

 

<< “Address of cigar is “ << pB1

// Display address

<< endl

 

 

<< “Volume of cigar is “

 

 

<< pB1->Volume();

 

// Volume of object pointed to

pB2 = &match;

if(pB2->Compare(pB1)) // Compare via pointers cout << endl

<< “match is greater than cigar”;

else

cout << endl

<< “match is less than or equal to cigar”;

pB1 = boxes;

// Set to address of array

boxes[2] = match;

// Set 3rd element to match

cout << endl

// Now access thru pointer

<< “Volume of boxes[2] is “ << (pB1 + 2)->Volume();

cout << endl; return 0;

}

If you run the example, the output looks something like that shown here:

Constructor called. Constructor called. Constructor called. Constructor called. Constructor called. Constructor called. Constructor called.

Address of cigar is 0012FE20 Volume of cigar is 40

match is less than or equal to cigar Volume of boxes[2] is 1.21

Of course, the value of the address for the object cigar may well be different on your PC.

How It Works

The only change to the class definition isn’t one of great substance. You have only modified the Compare() function to accept a pointer to a CBox object as an argument, and now we know about const member functions we declare it as const because it doesn’t alter the object. The function main() merely exercises pointers to CBox type objects in various, rather arbitrary, ways.

Within the main()function you declare two pointers to CBox objects after declaring an array, Boxes, and the CBox objects cigar and match. The first, pB1, is initialized with the address of the object cigar, and the second, pB2, is initialized to NULL. All of this uses the pointer in exactly the same way you would when you’re applying a pointer to a basic type. The fact that you are using a pointer to a type that you have defined yourself makes no difference.

370

Defining Your Own Data Types

You use pB1 with the indirect member access operator to generate the volume of the object pointed to, and the result is displayed. You then assign the address of match to pB2 and use both pointers in calling the compare function. Because the argument of the function Compare() is a pointer to a CBox object , the function uses the indirect member selection operator in calling the Volume() function for the object.

To demonstrate that you can use address arithmetic on the pointer pB1 when using it to select the member function, you set pB1 to the address of the first element of the array of type CBox, boxes. In this case, you select the third element of the array and calculate its volume. This is the same as the volume of match.

You can see from the output that there were seven calls of the constructor for CBox objects: five due to the array Boxes, plus one each for the objects cigar and match.

Overall, there’s virtually no difference between using a pointer to a class object and using a pointer to a basic type, such as double.

References to Class Objects

References really come into their own when they are used with classes. As with pointers, there is virtually no difference between the way you declare and use references to class objects and the way in which we’ve already declared and used references to variables of basic types. To declare a reference to the object cigar, for instance, you would write this:

CBox& rcigar = cigar;

// Define reference to object cigar

To use a reference to calculate the volume of the object cigar, you would just use the reference name where the object name would otherwise appear:

cout << rcigar.Volume();

// Output volume of cigar thru a reference

As you may remember, a reference acts as an alias for the object it refers to, so the usage is exactly the same as using the original object name.

Implementing a Copy Constructor

The importance of references is really in the context of arguments and return values in functions, particularly class member functions. Let’s return to the question of the copy constructor as a first toe in the water. For the moment, I’ll sidestep the question of when you need to write your own copy constructor and concentrate on the problem of how you can write one. I’ll use the CBox class just to make the discussion more concrete.

The copy constructor is a constructor that creates an object by initializing it with an existing object of the same class. It therefore needs to accept an object of the class as an argument. You might consider writing the prototype like this:

CBox(CBox initB);

Now consider what happens when this constructor is called. If you write this declaration:

CBox myBox = cigar;

371

Chapter 7

this generates a call of the copy constructor as follows:

CBox::CBox(cigar);

This seems to be no problem, until you realize that the argument is passed by value. So, before the object cigar can be passed, the compiler needs to arrange to make a copy of it. Therefore, it calls the copy constructor to make a copy of the argument for the call of the copy constructor. Unfortunately, since it is passed by value, this call also needs a copy of its argument to be made, so the copy constructor is called and so on and so on. You end up with an infinite number of calls to the copy constructor.

The solution, as I’m sure you have guessed, is to use a const reference parameter. You can write the prototype of the copy constructor like this:

CBox(const CBox& initB);

Now, the argument to the copy constructor doesn’t need to be copied. It is used to initialize the reference parameter, so no copying takes place. As you remember from the discussion on references, if a parameter to a function is a reference, no copying of the argument occurs when the function is called. The function accesses the argument variable in the caller function directly. The const qualifier ensures that the argument can’t be modified in the function.

This is another important use of the const qualifier. You should always declare a reference parameter of a function as const unless the function will modify it.

You could implement the copy constructor as follows:

CBox::CBox(const CBox& initB)

{

m_Length = initB.m_Length;

m_Width = initB.m_Width; m_Height = initB.m_Height;

}

This definition of the copy constructor assumes that it appears outside of the class definition. The constructor name is therefore qualified with the class name using the scope resolution operator. Each data member of the object being created is initialized with the corresponding member of the object passed as an argument. Of course, you could equally well use the initialization list to set the values of the object.

This case is not an example of when you need to write a copy constructor. As you have seen, the default copy constructor works perfectly well with CBox objects. I will get to why and when you need to write your own copy constructor in the next chapter.

C++/CLI Programming

The C++/CLI programming language has its own struct and class types. In fact C++/CLI allows the definition of two different struct and class types that have different characteristics; value struct types and value class types, and ref struct types and ref class types. Each of the two word combinations, value struct, ref struct, value class, and ref class, is a keyword and distinct

372

Defining Your Own Data Types

from the keywords struct and class; value and ref are not by themselves keywords. As in native C++, the only difference between a struct and a class in C++/CLI is that members of a struct are public by default whereas members of a class are private by default. One essential difference between value classes (or value structs) and reference classes (or ref structs) is that variables of value class types contain their own data whereas variables to access reference class types must be handles and therefore contain an address.

Note that member functions of C++/CLI classes cannot be declared as const. Another difference from native C++ is that the this pointer in a non-static function member of a value class type T is an interior pointer of type interior_ptr<T>, whereas the this pointer in a ref class type T is a handle of type T^. You need to keep this in mind when returning the this pointer from a C++/CLI function or storing it in a local variable. There are three other restrictions that apply to both value classes and reference classes:

A value class or ref class cannot contain fields that are native C++ arrays or native C++ class types.

Friend functions are not allowed.

A value class or ref class cannot have members that are bit-fields.

You have already heard back in Chapter 4 that the fundamental type names such as type int and type double are shorthand for value class types in a CLR program. When you declare a data item of a value class type, memory for it will be allocated on the stack but you can create value class objects on the heap using the gcnew operator, in which case the variable that you use to access the value class object must be a handle. For example:

double pi = 3.142;

// pi is stored on the stack

int^ lucky = gcnew int(7);

// lucky is a handle and 7 is stored on the heap

double^ two = 2.0;

// two is a handle, and 2.0 is stored on the heap

 

 

You can use any of these variables in arithmetic expression but you must use the * operator to dereference the handle to access the value. For example:

Console::WriteLine(L”2pi = {0}”, *two*pi);

Note that you could write the product as pi**two and get the right result but it is better to use parentheses in such instances and write pi*(*two), as this makes the code clearer.

Defining Value Class Types

I won’t discuss value struct types separately from value class types as the only difference is that the members of a value struct type are public by default whereas value class members are private by default. A value class is intended to be a relatively simple class type that provides the possibility for you to define new primitive types that can be used in a similar way to the fundamental types; however, you won’t be in a position to do this fully until you learn about a topic called operator overloading in the next chapter. A variable of a value class type is created on the stack and stores the value directly, but as you have already seen, you can also use a tracking handle to reference a value class type stored on the CLR heap.

373

Chapter 7

Take an example of a simple value class definition:

// Class representing a height value class Height

{

private:

// Records the height in feet and inches int feet;

int inches;

public:

//Create a height from inches value Height(int ins)

{

feet = ins/12; inches = ins%12;

}

//Create a height from feet and inches Height(int ft, int ins) : feet(ft), inches(ins){}

};

This defines a value class type with the name Height. It has two private fields that are both of type int that record a height in feet and inches. The class has two constructors — one to create a Height object from a number of inches supplied as the argument, and the other to create a Height object from both a feet and inches specification. The latter should really check that the number of inches supplied as an argument is less than 12, but I’ll leave you to add that as a fine point. To create a variable of type Height you could write:

Height tall = Height(7, 8);

// Height is 7 feet 8 inches

This creates the variable, tall, containing a Height object representing 7 feet 8 inches; this object is created by calling the constructor with two parameters.

Height baseHeight;

This statement creates a variable, baseHeight, that will be automatically initialized to a height of zero. The Height class does not have a no-arg constructor specified and because it is a value class, you are not permitted to supply one in the class definition. There will be a no-arg constructor included automatically in a value class that will initialize all value type fields to the equivalent of zero and all fields that are handles to nullptr and you cannot override the implicit constructor with your own version. It’s this default constructor that will be used to create the value of baseHeight.

There are a couple of other restrictions on what a value class can contain:

You must not include a copy constructor in a value class definition.

You cannot override the assignment operator in a value class (I’ll discuss how you override operators in a class in Chapter 8).

Value class objects are always copied by just copying fields and the assignment of one value class object to another is done in the same way. Value classes are intended to be used to represent simple objects

374

Defining Your Own Data Types

defined by a limited amount of data so for objects that don’t fit this specification or where the value class restrictions are problematical you should use ref class types to represent them.

Let’s take the Height class for a test drive.

Try It Out

Defining and Using a Value Class Type

Here’s the code to exercise the Height value class:

//Ex7_14.cpp : main project file.

//Defining and using a value class type

#include “stdafx.h”

using namespace System;

// Class representing a height value class Height

{

private:

// Records the height in feet and inches int feet;

int inches;

public:

//Create a height from inches value Height(int ins)

{

feet = ins/12; inches = ins%12;

}

//Create a height from feet and inches Height(int ft, int ins) : feet(ft), inches(ins){}

};

int main(array<System::String ^> ^args)

{

Height myHeight = Height(6,3); Height^ yourHeight = Height(70); Height hisHeight = *yourHeight;

Console::WriteLine(L”My height is {0}”, myHeight); Console::WriteLine(L”Your height is {0}”, yourHeight); Console::WriteLine(L”His height is {0}”, hisHeight); return 0;

}

Executing this program results in the following output:

My height is Height

Your height is Height

His height is Height

Press any key to continue . . .

375

Chapter 7

How It Works

Well, the output is a bit monotonous and perhaps less than we were hoping for, but let’s come back to that a little later. In the main() function, you create three variables with the following statement:

Height myHeight = Height(6,3);

Height^ yourHeight = Height(70);

Height hisHeight = *yourHeight;

The first variable is of type Height so the object that represents a height of 6 feet 3 inches is allocated on the stack. The second variable is a handle of type Height^ so the object representing a height of 5 feet 10 inches is created on the CLR heap. The third variable is another stack variable that is a copy of the object referenced by yourHeight. Because yourHeight is a handle, you have to dereference it to assign it to the hisHeight variable and the result is that hisHeight contains a duplicate of the object referenced by yourHeight. Variables of a value class type always contain a unique object so two such variables cannot reference the same object; assigning one variable of a value class type to another always involves copying. Of course, several handles can reference a single object and assigning the value of one handle to another simply copies the address (or nullptr) from one to the other so that both objects reference the same object.

The output is produced by the three calls of the Console::WriteLine() function. Unfortunately the output is not the values of the value class objects, but simply the class name. So how did this come about? It was optimistic to expect the values to be produced — after all, how is the compiler to know how they should be presented? Height objects contain two values — which one should be presented as the value? The class has to have a way to make the value available in this context.

The ToString() Function in a Class

Every C++/CLI class that you define has a ToString() function — I’ll explain how this comes about in the next chapter when I discuss class inheritance — that returns a handle to a string that is supposed to represent the class object. The compiler arranges for the ToString() function for an object to be called whenever it recognizes that a string representation of an object is required and you can call it explicitly if necessary. For example, you could write this:

double pi = 3.142;

Console::WriteLine(pi.ToString());

This outputs the value of pi as a string and it is the ToString() function that is defined in the System::Double class that provides the string. Of course, you would get the same output without explicitly calling the ToString() function.

The default version of the ToString() function that you get in the Height class just outputs the class name because there is no way to know ahead of time what value should be returned as a string for an object of your class type. To get an appropriate value output by the Console::WriteLine() function in the previous example, you must add a ToString() function to the Height class that presents the value of an object in the form that you want.

Here’s how the class looks with a ToString() function:

// Class representing a height value class Height

376

Defining Your Own Data Types

{

private:

// Records the height in feet and inches int feet;

int inches;

public:

//Create a height from inches value Height(int ins)

{

feet = ins/12; inches = ins%12;

}

//Create a height from feet and inches Height(int ft, int ins) : feet(ft), inches(ins){}

//Create a string representation of the object virtual String^ ToString() override

{

return feet + L” feet “+ inches + L” inches”;

}

};

The combination of the virtual keyword before the return type for ToString() and the override keyword following the parameter list for the function indicates that this version of the ToString() function overrides the version of the function that is present in the class by default. You’ll hear a lot more about this in Chapter 8. Our new version of the ToString() function now output a string expressing a height in feet and inches. If you add this function to the class definition in the previous example, you get the following output when you compile and execute the program:

My height is 6 feet 3 inches

Your height is 5 feet 10 inches

His height is 5 feet 10 inches

Press any key to continue . . .

This is more like what you were expecting to get before. You can see from the output that the WriteLine() function quite happily deals with an object on the CLR heap that you references through the yourHeight handle, as well as the myHeight and hisHeight objects that were created on the stack.

Literal Fields

The factor 12 that you use to convert from feet to inches and vice versa is a little troubling. It is an example of what is called a “magic number,” where a person reading the code has to guess or deduce its significance and origin. In this case it’s fairly obvious what the 12 is but there will be many instances where the origin of a numerical constant in a calculation is not so apparent. C++/CLI has a literal field facility for introducing named constants into a class that will solve the problem in this case. Here’s how you can eliminate the magic number from the code in the single-argument constructor in the Height class:

value class Height

{

private:

377

Chapter 7

// Records the height in feet and inches int feet;

int inches;

literal int inchesPerFoot = 12;

public:

//Create a height from inches value Height(int ins)

{

feet = ins/ inchesPerFoot; inches = ins% inchesPerFoot;

}

//Create a height from feet and inches Height(int ft, int ins) : feet(ft), inches(ins){}

//Create a string representation of the object virtual String^ ToString() override

{

return feet + L” feet “+ inches + L” inches”;

}

};

Now the constructor uses the name inchesPerFoot instead of 12, so there is no doubt as to what is going on.

You can define the value of a literal field in terms of other literal fields as long as the names of the fields you are using to specify the value are defined first. For example:

value class Height

{

// Other code...

literal int inchesPerFoot = 12;

literal double millimetersPerInch = 25.4;

literal double millimetersPerFoot = inchesPerFoot*millimetersPerInch;

// Other code...

}

Here you define the value for the literal field millimetersPerFoot as the product of the other two literal fields. If you were to move the definition of the millimetersPerFoot field so that it precedes either or both of the other two, the code would not compile.

Defining Reference Class Types

A reference class is comparable to a native C++ class in capabilities and does not have the restrictions that a value class has. Unlike a native C++ class, however, a reference class does not have a default copy constructor or a default assignment operator. If your class needs to support either of these operators, you must explicitly add a function for the capability — you’ll see how in the next chapter.

378