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

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

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

Defining Your Own Data Types

property String^ default[int]

{

// Retrieve indexed property value String^ get(int index)

{

if(index >= Names->Length)

throw gcnew Exception(L”Index out of range”); return Names[index];

}

}

};

The idea of the Name class is to store a person’s name as an array of individual names. The constructor accepts an arbitrary number of arguments of type String^ that are then stored in the Names field so a Name object can accommodate any number of names.

The indexed property here is a default indexed property because the name is specified by the default keyword. If you supplied a regular name in this position in the property specification it would be a named indexed property. The square brackets following the default keyword indicate that it is indeed an indexed property and the type they enclose — type int in this case — is the type of the index values that is to be used when retrieving values for the property. The type of the index does not have to be a numeric type and you can have more than one index parameter for accessing indexed property values.

For an indexed property accessed by a single index, the get() function must have a parameter specifying the index that is of the same as the type that appears between the square brackets following the property name. The set() function for such an indexed property must have two parameters: the first parameter is the index, and the second is the new value to be set for the property corresponding to the first parameter.

Let’s see how indexed properties behave in practice.

Try It Out

Using a Default Indexed Property

Here’s an example that makes use of a slightly extended version of the Name class:

//Ex7_17.cpp : main project file.

//Defining and using default indexed properties

#include “stdafx.h”

using namespace System;

ref class Name

{

private: array<String^>^ Names;

public:

Name(...array<String^>^ names) : Names(names) {}

// Scalar property specifying number of names property int NameCount

389

Chapter 7

{

int get() {return Names->Length; }

}

// Indexed property to return names property String^ default[int]

{

String^ get(int index)

{

if(index >= Names->Length)

throw gcnew Exception(L”Index out of range”); return Names[index];

}

}

};

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

{

Name^ myName = gcnew Name(L”Ebenezer”, L”Isaiah”, L”Ezra”, L”Inigo”, L”Whelkwhistle”);

// List the names

for(int i = 0 ; i < myName->NameCount ; i++) Console::WriteLine(L”Name {0} is {1}”, i+1, myName[i]);

return 0;

}

This example produces the following output:

Name 1 is Ebenezer

Name 2 is Isaiah

Name 3 is Ezra

Name 4 is Inigo

Name 5 is Whelkwhistle

Press any key to continue . . .

How It Works

The Name class in the example is basically the same as in the previous section but with a scalar property with the name NameCount added that returns the number of names in the Name object. In main() you first create a Name object with five names:

Name^ myName = gcnew Name(L”Ebenezer”, L”Isaiah”, L”Ezra”, L”Inigo”, L”Whelkwhistle”);

The parameter list for the constructor in the Name class starts with an ellipsis so it accepts any number of arguments. The arguments that are supplied when it is called will be stored in the elements of the names array, so initializing the Names field with names makes Names reference the names array. In the previous statement, you supply five arguments to the constructor, so the Names field in the object referenced by myName is an array with five elements.

390

Defining Your Own Data Types

You access the properties for myName in a for loop to list the names the object contains:

for(int i = 0 ; i < myName->NameCount ; i++) Console::WriteLine(L”Name {0} is {1}”, i+1, myName[i]);

You use the NameCount property value to control the for loop. Without this property you would not know how many names there are to be listed. Within the loop the last argument to the WriteLine() function accesses the ith indexed property. As you see, accessing the default indexed property just involves placing the index value in square brackets after the myName variable name. The output demonstrates that the indexed properties are working as expected.

The indexed property is read-only because the Name class only includes a get() function for the property. To allow properties to be changed you could add a definition for the set() function for the default indexed property like this:

ref class Name

{

//Code as before...

//Indexed property to return names property String^ default[int]

{

String^ get(int index)

{

if(index >= Names->Length)

throw gcnew Exception(L”Index out of range”); return Names[index];

}

void set(int index, String^ name)

{

if(index >= Names->Length)

throw gcnew Exception(L”Index out of range”); Names[index] = name;

}

}

};

You can now use the ability to set indexed properties by adding a statement in main() to set the last indexed property value:

Name^ myName = gcnew Name(L”Ebenezer”, L”Isaiah”, L”Ezra”,

L”Inigo”, L”Whelkwhistle”); myName[myName->NameCount - 1] = L”Oberwurst”; // Change last indexed property

// List the names

for(int i = 0 ; i < myName->NameCount ; i++) Console::WriteLine(L”Name {0} is {1}”, i+1, myName[i]);

With this addition you’ll see from the output for the new version of the program that the last name has indeed been updated by the statement assigning a new value to the property at index position myName->NameCount-1.

391

Chapter 7

You could also try adding a named indexed property to the class:

ref class Name

{

//Code as before...

//Indexed property to return initials property wchar_t Initials[int]

{

wchar_t get(int index)

{

if(index >= Names->Length)

throw gcnew Exception(L”Index out of range”); return Names[index][0];

}

}

};

The indexed property has the name Initials because its function is to return the initial of a name specified by an index. You define a named indexed property in essentially the same way as a default indexed property but with the property name replacing the default keyword.

If you recompile the program and execute it once more, you see the following output.

Name 1 is

Ebenezer

Name 2

is

Isaiah

Name 3

is

Ezra

Name

4

is

Inigo

Name

4

is

Oberwurst

The initials are: E.I.E.I.O.

Press any key to continue . . .

The initials are produced by accessing the named indexed property in the for loop and the output shows that they work as expected.

More Complex Indexed Properties

As mentioned, indexed properties can be defined so that more than one index is necessary to access a value and the indexes need not be numeric. Here’s an example of a class with such an indexed property:

enum class Day{Monday, Tuesday, Wednesday,Thursday, Friday, Saturday, Sunday};

// Class defining a shop

 

ref class Shop

 

{

 

public:

 

property String^ Opening[Day, String^]

// Opening times

{

 

String^ get(Day day, String^ AmOrPm)

 

{

 

switch(day)

 

{

 

case Day::Saturday:

// Saturday opening:

392

Defining Your Own Data Types

if(AmOrPm == L”am”)

 

 

return L”9:00”;

//

morning is 9 am

else

 

 

return L”14:30”;

//

afternoon is 2:30 pm

break;

 

 

case Day::Sunday:

// Saturday opening:

return L”closed”;

//

closed all day

break;

 

 

default:

 

 

if(AmOrPm == L”am”)

// Monday to Friday opening:

return L”9:30”;

//

morning is 9:30 am

else

 

 

return L”14:00”;

//

afternoon is 2 pm

break;

 

 

}

}

}

};

The class representing a shop has an indexed property specifying opening times. The first index is an enumeration value of type Day that identifies the day of the week and the second index is a handle to a string that determines whether it is morning or afternoon. You could output the Opening property value for a Shop object like this:

Shop^ shop = gcnew Shop;

Console::WriteLine(shop->Opening[Day::Saturday, L”pm”]);

The first statement creates the Shop object and the second displays the opening time for the shop for Saturday afternoon. As you see, you just place the two index values for the property between square brackets and separate them by a comma. The output from the second statement is the string “14:30”. If you can dream up a reason why you need them, you could also define indexed properties with three or more indexes in a class.

Static Properties

Static properties are similar to static class members in that they are defined for the class and are the same for all objects of the class type. You define a static property by adding the static keyword in the definition of the property. Here’s how you might define a static property in the Length class you saw earlier:

value class Length

{

// Code as before...

public:

static property String^ Units

{

String^ get() { return L”feet and inches”; }

}

};

393

Chapter 7

This is a simple property that makes available the units assumed by the class as a string. You access a static property by qualifying the property name with the name of the class, just as you would any other static member of the class:

Console::WriteLine(L”Class units are {0}.”, Length::Units);

Static class properties exist whether or not any objects of the class type have been created. This differs from instance properties, which are specific to each object of the class type. Of course, if you have defined a class object, you can access a static property using the variable name. If you have created a Length object with the name len for example, you could output the value of the static Units property with the statement:

Console::WriteLine(L”Class units are {0}.”, len.Units);

For accessing a static property in a reference class through a handle to an object of that type you would use the -> operator.

Reserved Property Names

Although properties are different from fields, the values for properties still have to be stored somewhere and the storage locations need to be identified somehow. Internally properties have names created for the storage locations that are needed, and such names are reserved in a class that has properties so you must not use these names for other purposes.

If you define a scalar or named indexed property with the name NAME in a class, the names get_NAME and set_NAME are reserved in the class so you must not use them for other purposes. Both names are reserved regardless of whether or not you define the get() and set() functions for the property. When you define a default indexed property in a class, the names get_Item and set_Item are reserved. The possibility of there being reserved names that use underscore characters is a good reason for avoiding the use of the underscore character in your own names in a C++/CLI program.

initonly Fields

Literal fields are a convenient way of introducing constants into a class, but they have the limitation that their values must be defined when you compile the program. C++/CLI also provides initonly fields in a class that are constants that you can initialize in a constructor. Here’s an example of an initonly field in a skeleton version of the Length class:

value class Length

{

private: int feet;

int inches;

public:

 

initonly int inchesPerFoot;

// initonly field

// Constructor

 

Length(int ft, int ins) :

 

feet(ft), inches(ins),

// Initialize fields

inchesPerFoot(12)

// Initialize initonly field

{}

 

};

 

394

Defining Your Own Data Types

Here the initonly field has the name inchesPerFoot and is initialized in the initializer list for the constructor. This is an example of a non-static initonly field and each object will have its own copy, just like the ordinary fields, feet and inches. Of course, the big difference between initonly fields and ordinary fields is that you cannot subsequently change the value of an initonly field — after it has been initialized, it is fixed for all time. Note that you must not specify an initial value for a non-static initonly field when you declare it; this implies that you must initialize all non-static initonly fields in a constructor.

You don’t have to initialize non-static initonly fields in the constructor’s initializer list — you could do it in the body of the constructor:

Length(int ft, int ins) :

 

feet(ft), inches(ins),

// Initialize fields

{

 

inchesPerFoot = 12;

// Initialize initonly field

}

 

 

 

Now the field is initialized in the body of the constructor. You would typically pass the value for a nonstatic initonly field as an argument to the constructor rather than use an explicit literal as we have done here because the point of such fields is that their values are instance specific. If the value is known when you write the code you might as well use a literal field.

You can also define an initonly field in a class to be static, in which case it is shared between all members of the class and if it is a public initonly field, it’s accessible by qualifying the field name with the class name. The inchesPerFoot field would make much more sense as a static initonly field — the value really isn’t going to vary from one object to another. Here’s a new version of the Length class with a static initonly field:

value class Length

{

private: int feet;

int inches;

public:

initonly static int inchesPerFoot = 12;

// Static initonly field

// Constructor

 

Length(int ft, int ins) :

 

feet(ft), inches(ins)

// Initialize fields

{}

 

};

 

Now the inchesPerFoot field is static, and it has its value specified in the declaration rather than in the constructor initializer list. Indeed, you are not permitted to set values for static fields of any kind in the constructor. If you think about it, this makes sense because static fields are shared among all objects of the class and therefore setting values for such fields each time a constructor is called would conflict with this notion.

You now seem to be back with initonly fields only being initialized at compile time where literal fields could do the job anyway; however, you have another way to initialize static initonly fields at runtime — through a static constructor.

395

Chapter 7

Static Constructors

A static constructor is a constructor that you declare using the static keyword and that you use to initialize static fields and static initonly fields. A static constructor has no parameters and cannot have an initializer list. A static constructor is always private, regardless of whether or not you put it in a public section of the class. You can define a static constructor for value classes and for reference classes. You cannot call a static constructor directly — it will be called automatically prior to the execution of a normal constructor. Any static fields that have initial values specified in their declarations will be initialized prior to the execution of the static constructor. Here’s how you could initialize the initonly field in the Length class using a static constructor:

value class Length

{

private: int feet;

int inches;

// Static constructor

static Length() { inchesPerFoot = 12; }

public:

 

initonly static int inchesPerFoot;

// Static initonly field

// Constructor

 

Length(int ft, int ins) :

 

feet(ft), inches(ins)

// Initialize fields

{}

 

}

 

This example of using a static constructor has no particular advantage over explicit initialization of inchesPerFoot, but bear in mind that the big difference is that the initialization is now occurring at runtime and the value could be acquired from an external source.

Summar y

You now understand the basic ideas behind classes in C++. You’re going to see more and more about using classes throughout the rest of the book. The key points to keep in mind from this chapter are:

A class provides a means of defining your own data types. They can reflect whatever types of objects your particular problem requires.

A class can contain data members and function members. The function members of a class always have free access to the data members of the same class. Data members of a C++/CLI class are referred to as fields.

Objects of a class are created and initialized using functions called constructors. These are automatically called when an object declaration is encountered. Constructors may be overloaded to provide different ways of initializing an object.

Classes in a C++/CLI program can be value classes or ref class.

396

Defining Your Own Data Types

Variables of a value class type store data directly whereas variables referencing ref class objects are always handles.

C++/CLI classes can have a static constructor defined that initializes the static members of a class.

Members of a class can be specified as public, in which case they are freely accessible by any function in a program. Alternatively, they may be specified as private, in which case they may only be accessed by member functions or friend functions of the class.

Members of a class can be defined as static. Only one instance of each static member of a class exists, which is shared amongst all instances of the class, no matter how many objects of the class are created.

Every non-static object of a class contains the pointer this, which points to the current object for which the function was called.

In non-static function members of a value class type, the this pointer is an interior pointer whereas in a ref class type it is a handle.

A member function that is declared as const has a const this pointer, and therefore cannot modify data members of the class object for which it is called. It also cannot call another member function that is not const.

You can only call const member functions for a class object declared as const.

Function members of value classes and ref classes cannot be declared as const.

Using references to class objects as arguments to function calls can avoid substantial overheads in passing complex objects to a function.

A copy constructor, which is a constructor for an object initialized with an existing object of the same class, must have its parameter specified as a const reference.

You cannot define a copy constructor in a value class because copying value class objects is always done by member-by-member copying.

Exercises

You can download the source code for the examples in the book and the solutions to the following exercises from http://www.wrox.com.

1.Define a struct Sample that contains two integer data items. Write a program which declares two object of type Sample, called a and b. Set values for the data items that belong to a and then check that you can copy the values into b by simple assignment.

2.Add a char* member to struct Sample in the previous exercise called sPtr. When you fill in the data for a, dynamically create a string buffer initialized with “Hello World!” and make a.sptr point to it. Copy a into b. What happens when you change the contents of the character buffer pointed to by a.sPtr and then output the contents of the string pointed to by b.sPtr? Explain what is happening. How would you get around this?

397

Chapter 7

3.Create a function which takes a pointer to an object of type Sample as an argument, and which outputs the values of the members of any object Sample that is passed to it. Test this function by extending the program that you created for the previous exercise.

4.Define a class CRecord with two private data members that store a name up to 14 characters long and an integer item number. Define a getRecord() function member of the CRecord class that will set values for the data members by reading input from the keyboard and a putRecord() function member that outputs the values of the data members. Implement the getRecord() function so that a calling program can detect when a zero item number is entered. Test your CRecord class with a main() function that reads and outputs CRecord objects until a zero item number is entered.

5.Write a class called CTrace that you can use to show you at run-time when code blocks have been entered and exited, by producing output like this:

function ‘f1’ entry ‘if’ block entry ‘if’ block exit function ‘f1’ exit

6.Can you think of a way to automatically control the indentation in the last exercise, so that the output looks like this?

function ‘f1’ entry ‘if’ block entry ‘if’ block exit

function ‘f1’ exit

7.Define a class to represent a push-down stack of integers. A stack is a list of items that permits adding (‘pushing’) or removing (‘popping’) items only from one end and works on a last-in, first-out principle. For example, if the stack contained [10 4 16 20], pop() would return 10, and the stack would then contain [4 16 20]; a subsequent push(13) would leave the stack as [13 4 16 20]. You can’t get at an item that is not at the top without first popping the ones above it. Your class should implement push() and pop() functions, plus a print() function so that you can check the stack contents. Store the list internally as an array, for now. Write a test program to verify the correct operation of your class.

8.What happens with your solution to the previous exercise if you try to pop() more items than you’ve pushed, or save more items than you have space for? Can you think of a robust way to trap this? Sometimes you might want to look at the number at the top of the stack without removing it; implement a peek() function to do this.

9.Repeat Ex7-4 but as a CLR console program using ref classes.

398