Variables and Managed C Data Types


Variables and Managed C++ Data Types

One of the key differences between traditional C++ and Managed C++, believe it or not, falls at this low level of the language. If you have worked with C++, then it may come as a little surprise that the data types int, long, float, and so on are no more. They have been replaced with .NET built-in value types. To simplify things for traditional C++ programmers, Managed C++ allows the use of the old data types, but they are, in fact, just aliases.

Alas, I'm getting ahead of myself. I'll start at the beginning, and that would be how to create or, more accurately, declare variables.

Declaring Variables

To use a variable in Managed C++, you must first declare it. The minimum declaration of a variable consists of a data type and a variable name:

 Int32 counter; Double yCoord; 

Variable declarations can go almost anywhere in the code body of a Managed C++ program. One of the few criteria for declarations is that they have to occur before the variable is used. It once was required that all declarations occur as the first statements of a function due to C++'s original C background. You will still see this in practice today, as some programmers feel it makes the code cleaner to read. Personally, I prefer to place the variable closer to where it is first used—that way, I don't have to scroll to the top of every function to see how I declared something. How you code it is up to you. Following the standards of your company is always a good rule of thumb or, if you are coding on your own, stay consistent. You will find that it will save you time down the line.

There is an assortment of more complex declarations. For example, you can string together several comma-delimited variable names at the same time:

 Int32 x, y, z; 

A special data type called a pointer, which I'll explain later, requires an asterisk (*) in front of the variable name or after the data type:

 String* name; String *name; 

You might think of these as saying "String pointer called name" or "name points to a String." They are equivalent. There is a complication with string pointers, as shown here:

 Int32* isPointer, isNOTaPointer; 

The preceding line actually declares a pointer to an Int32 and a variable of type Int32. This is probably not what you are expecting. If you want two pointers to Int32, you need to declare it like this:

 Int32 *aPointer, *anotherPointer; 

You have two possible ways to initialize the variable within the declaration statement. The first is by using a standard assignment:

 Int32 counter = 0; Double yCoord = 300.5; 

The second is by using what is known as functional notation, as it resembles the calling of a function passing the initialization value as a parameter. In Managed C++, you should probably call this constructor initialization, as you are actually calling the data type's constructor to create these variables:

 Int32 counter(0); Double yCoord(300.5); 

Again, use caution when initializing a variable within the declaration statement using standard assignment. This code may not do what you expect:

 Int32 x, y, z = 200; 

Only z is initialized to 200; all the other variables take on the default value of the data type. Enter the following to code this so that all variables are initialized to 200:

 Int32 x = 200, y = 200, z = 200; 

It is always a good thing to initialize your variables before you use them. If you don't initialize a variable, its contents can be almost anything when it is used. To help remind you of this, the compiler displays a warning about uninitialized variables while it is compiling.

Variable Name Restrictions

For those of you with a C++ background, there are really no big changes here. Variable names consist of upper- and lowercase letters, digits from 0 to 9, and the underscore character (_). The variable name must start with a letter or an underscore character. Also, variable names cannot be the same as C++ reserved keywords, including all variable names starting with two underscores, which C++ has also reserved. Table 2-1 contains a list of all C++ reserved keywords.

Table 2-1: C++ Reserved Keywords

KEYWORDS

asm

auto

bool

_Bool

break

case

catch

char

class

_Complex

const

const_cast

continue

default

delete

do

double

dynamic_cast

else

enum

explicit

export

extern

false

float

for

friend

goto

if

_Imaginary

inline

int

long

mutable

namespace

new

operator

private

protected

public

register

reinterpret_cast

restrict

return

short

signed

sizeof

static

static_cast

struct

switch

template

this

throw

true

try

typedef

typeid

typename

_typeof

union

unsigned

using

virtual

void

volatile

wchar_t

while

Variables should probably be self-descriptive. However, there is nothing stopping you from writing a program that uses variable names starting with a0000 and continuing through z9999. If you do this, though, don't ask me to debug it for you.

There are also people who think that you should use Hungarian notation for variable names. This notation allows other programmers to read your code and know the data type by the prefix attached to its name. I find this notation cumbersome and don't use it myself unless, of course, company standards dictate its use.

Predefined Data Types

All data types, even the simplest ones, are truly objects in Managed C++. This differs from traditional C++, where primitive types such as int, float, and double were strictly stored values of data types themselves.

As a Managed C++ programmer, you have the luxury of programming simple data types just like you would in traditional C++, knowing that you can box them, making them objects if needed. I cover boxing later in this chapter.

Predefined data types fall into two different types: built-in value types and reference types. Built-in value types are the data types that default to just storing their values for efficiency but can be boxed to become full objects. Reference types, on the other hand, are always objects.

Built-in Value Types

All the standard C++ data types are available to the Managed C++ programmer. Or at least so it appears. In reality, the standard data types are just an alias for the .NET Framework's built-in value types. There is basically only one major difference between Managed C++'s built-in types and their traditional C++ aliases: Managed C++ value types default to being garbage collected when defined as pointers, whereas traditional C++ data types don't. Because this is the case, pointers that you allocate using int need to be deleted. Those you create with Int32 don't need to be deleted, as they are automatically garbage collected.

Note

Throughout this book, I refer to the built-in value types only by their .NET names. I personally feel the .NET names are more descriptive, but feel free to use the standard C++ aliases if you are more comfortable with them.

There are five distinct groups of built-in value types:

  1. Integer

  2. Floating point

  3. Decimal

  4. Boolean

  5. Character

Programmers with a C++ background should readily recognize four of these groups. Decimal, most probably, is new to all. Let's go over all of them so that there are no surprises.

Integer Types

Eight different integer types are provided to Managed C++ programmers. These can all be broken down into unsigned and signed numbers. (In other words, can negative numbers be represented or just positive numbers?) Table 2-2 shows the integer types.

Table 2-2: Integer Point Built-in Value Types

C++ ALIAS

CLASS LIBRARY

DESCRIPTION

RANGE

unsigned char

System::Byte

8-bit unsigned integer

0 to 255

char

System::SByte

8-bit signed integer

-128 to 127

short

System::Int16

16-bit signed integer

-32,768 to 32,767

unsigned short

System::UInt16

16-bit unsigned integer

0 to 65,535

int or long

System::Int32

32-bit signed integer

-2,147,483,648 to 2,147,483,647

unsigned int or long

System::UInt32

32-bit unsigned integer

0 to 4,294,967,295

__int64

System::Int64

64-bit signed integer

-9,223,372,036,854,775,808 to 9,223,372,036,854,775,807

unsigned __int64

System::UInt64

64-bit unsigned integer

0 to 18,446,744,073,709,551,615

Byte and SByte are the smallest of the integer types, as they are only made up of 1 byte, hence their names. Their Managed C++ aliases are unsigned char and char, respectively. A Byte can range from 0 to 255, and an SByte can range from -128 to 127 inclusive. In traditional C++, char usually represents ASCII characters.

Caution

The Managed C++ alias char is not the same as the .NET Framework class library System::Char. A char is an 8-bit unsigned integer that frequently represents an ASCII character, whereas a System:: Char is a 16-bit Unicode character.

The remainder of the integer types have fairly self-descriptive .NET Framework class library names, with their type and size merged into their name. Int16 are 16-bit integers, UInt16 are unsigned 16-bit integers, and so on. Personally, I think these names make more sense than short, int, and long. Plus, long and int are the same size (4 bytes), so you have to throw in __Int64.

Unlike traditional C++, Managed C++ integer types have predetermined bit sizes. This is probably done to enable future platform independence, which isn't possible with the definition of the integer types provided by traditional C++. With the traditional C++ definition of integer types, there's often confusion about how many bits make up a long. On some platforms it's 32 bits and on others it's 64 bits.

There is nothing complex about declaring integer type variables. Whenever you declare an integer type variable in Managed C++, it is immediately initialized to the value of zero. This differs from traditional C++ compilers, where the initialization is optional and up to the compiler. For traditional C++, it is possible that the value of a variable remains uninitialized and, thus, contains just about any numeric value.

To initialize integer types, you simply declare a variable and assign it a character: octal, decimal, or hexadecimal literal. You will examine literals later in this chapter.

Listing 2-1 is a simple piece of code showing integer types in action.

Listing 2-1: Integer Types in Action

start example
 #using <mscorlib.dll> using namespace System; Int32 main(void) {     Byte w = 'F';                 // Initialize using character literal     Int16 x(123);                 // Initializing using Functional Notation     Int32 y = 456789;             // Decimal literal assigned     Int64 z = 0x9876543210;       // Hex literal assigned     Console::WriteLine ( w );     // Write out a Byte     Console::WriteLine ( x );     // Write out a Int16     Console::WriteLine ( y );     // Write out a Int32     Console::WriteLine ( z );     // Write out a Int64     Console::WriteLine ( z.ToString("X") ); // Write out a Int64 in Hex     return 0; } 
end example

Figure 2-2 shows the results of this little program.

click to expand
Figure 2-2: Results of IntegerTypes.exe

For those of you from traditional C++ backgrounds, the ToString() appended to the integer variables in the Console:: WriteLine() method might be a little confusing. Remember, in Managed C++, integer types are objects and have several methods attached to them, and ToString() happens to be one of them. This is also part of the reason I use the .NET Framework class library name for my declaration instead of the alias, as it serves to remind me of this.

Floating-Point Types

Only two different floating-point types are provided by Managed C++. Table 2-3 describes the details of each.

Table 2-3: Floating-Point Built-in Value Types

C++ ALIAS

CLASS LIBRARY

DESCRIPTION

SIGNIFICANT DIGITS

RANGE

float

System::Single

32-bit single-precision floating point

7

significant digits 1.5 10-45 to 3.4 1038

double

System::Double

64-bit double-precision floating point

15

significant digits 5.0 10-324 to 1.7 10308

The .NET Framework class library System::Single is the smaller of the two floating-point types available to Managed C++. It is stored using 32 bits. Its alias for C++ programmers is the better-known float type. Singles can represent numbers from 1.5 10-45 to 3.4 1038, but only seven of the digits are significant.

The System::Double class library, which is the larger of the two, uses 64 bits of storage. Its alias is double. Doubles can represent numbers from 5.0 10-324 to 1.7 10308, but only 15 of the digits are significant.

Listing 2-2 is a simple piece of code showing floating-point types in action.

Listing 2-2: Floating-Point Types in Action

start example
 #using <mscorlib.dll> using namespace System; Int32 main(void) {     Single w = 123.456f;   // standard decimal notation     Single x = 7890e3f;    // exponent notation     Double y = 34525425432525764765.76476476547654; // too big will truncate     Double z = 123456789012345e-300; // exponent will be reset     Console::WriteLine ( w ); // Write out Single     Console::WriteLine ( x ); // Write out Single with more zeros     Console::WriteLine ( y ); // Write out Double truncated     Console::WriteLine ( z ); // Write out Double shift back decimal     return 0; } 
end example

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

click to expand
Figure 2-3: Results of FloatingPoint.exe

The .NET Framework class library Double is the default value used by most methods that deal with floating-point numbers. You might think Single would be a better choice, being small and thus faster. This is true, but other than for division, the speed of Double and Single operations do not differ much, and the accuracy gained by Double usually is more important than the slight difference in speed.

Decimal Type

Only one decimal type is supported by Managed C++. This type has no traditional C++ equivalent and thus has no alias. Table 2-4 describes the decimal type.

Table 2-4: Decimal Built-in Value Type

CLASS LIBRARY

DESCRIPTION

SIGNIFICANT DIGITS

RANGE

System::Decimal

128-bit high-precision decimal notation

28

7.9 10-28 to 7.9 1028

This built-in type was designed specifically for financial calculations. Basically, it is a number with 28 significant digits. Within those 28 digits, you can place a decimal. In other words, you can place a very big number in a System::Decimal that will have a small fractional area, or you can make a very small number with a very big fractional part.

System::Decimals are not a native C++ data type and, as such, they need a little magic to get them initialized if the number of significant digits you want to capture is larger than 15. The significance of 15 is that it is the number of significant digits provided by a Double, the closest data type available to initialize a Decimal.

Here are three ways to load a number with more than 15 significant digits (there are other ways, I'm sure):

  1. The first method is to load the digits into a String and convert the String to Decimal.

  2. The second method is to use the Decimal constructor:

     public: Decimal(    Int32 lo,              // The low 32 bits of a 96-bit integer.    Int32 mid,             // The middle 32 bits of a 96-bit integer.    Int32 hi,              // The high 32 bits of a 96-bit integer.    bool isNegative,       // false is positive    unsigned char scale    // A power of 10 ranging from 0 to 28. ); 

  3. The third method is to add two Doubles together using the combined significant digits of both to make up the Decimal.

All three of these methods are shown in Listing 2-3. Also, for grins and giggles, I decided to use the Decimal method GetBits() to break the Decimal into its parts and then use the constructor to put it back together again.

Listing 2-3: Decimal Types in Action

start example
 #using <mscorlib.dll> using namespace System; Int32 main(void) {     Decimal w = System::Convert::ToDecimal(S"123456789012345678901.2345678");     Console::WriteLine( w );     Decimal x = 0.1234567890123456789012345678;      // will get truncated     Decimal y = 0.0000000000000000789012345678;      // works fine     Console::WriteLine ( x.ToString() );     Console::WriteLine ( y.ToString() );     // Decimal constructor     Decimal z(0xeb1f0ad2, 0xab54a98c, 0, false, 0);  // = 12345678901234567890     Console::WriteLine ( z.ToString() );     // Create a 28 significant digit number     Decimal a = 123456789012345000000.00000000;     Decimal b = 678901.23456780;     Decimal c = -(a + b);     // Break it up into 4 parts     Int32 d[] = Decimal::GetBits(c);     // Reassemble using Decimal constructor     Decimal e(d[0], d[1], d[2],                     // digits                ((d[3] & 0x80000000) == 0x80000000), // sign                ((d[3] >> 16) & 0xff) );               // decimal location     Console::WriteLine ( c.ToString() );       // display pre broken Decimal     Console::WriteLine ( d[0].ToString() );    // display part 1     Console::WriteLine ( d[1].ToString() );    // display part 2     Console::WriteLine ( d[2].ToString() );    // display part 3     Console::WriteLine ( d[3].ToString("X") ); // display part 4 in hex     Console::WriteLine ( e.ToString() );       // display reassembled Decimal     return 0; } 
end example

Figure 2-4 shows the results of this program.

click to expand
Figure 2-4: Results of Decimal.exe

Even though the Decimal type was designed for financial calculations, there's nothing stopping you from using it for other calculations where the number of significant digits is important and you'd rather have an exception than a rounding during calculation.

Boolean Type

Only one Boolean type is provided by Managed C++. Table 2-5 describes the details of it.

Table 2-5: Boolean Built-in Value Type

C++ ALIAS

CLASS LIBRARY

VALUES

bool

System::Boolean

true |not o or false 10

The System::Boolean built-in value type has the Managed C++ alias of bool. Booleans can only have a value of true or false.

Managed C++ is a little lenient when it comes to initializing Booleans, as it allows them to be assigned with the value of zero for false and any number other than zero for true. The compiler does give a warning if the value assigned is not one of the following: true, false, 1, or 0.

Listing 2-4 is a simple piece of code showing the Boolean type in action.

Listing 2-4: Boolean Type in Action

start example
 #using <mscorlib.dll> using namespace System; Int32 main(void) {     Boolean a = 18757;    // will give a warning but sets it to true     Boolean b = 0;        // false     Boolean c = true;     // obviously true     Boolean d = false;    // obviously false     Console::WriteLine ( a );     Console::WriteLine ( b );     Console::WriteLine ( c );     Console::WriteLine ( d );     return 0; } 
end example

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

click to expand
Figure 2-5: Results of Boolean.exe

Character Type

Only one character type is provided by Managed C++. Table 2-6 describes the details of this character type.

Table 2-6: Character Built-in Value Type

C++ ALIAS

CLASS LIBRARY

VALUE

wchar_t

System::Char

A single 16-bit Unicode character

The .NET Framework class library System::Char is a 16-bit Unicode character, which has a Managed C++ alias of__wchar_t (or wchar_t, if the Zc:wchar_t flag is set on the compiler).

Listing 2-5 is a simple piece of code showing the Char type in action.

Listing 2-5: Char Type in Action

start example
 #using <mscorlib.dll> using namespace System; Int32 main(void) {     Char a = L'A';         // character literal 'A'     Char b = L'\x0041';    // Unicode notation also an 'A'     Console::WriteLine ( a.ToString() );     Console::WriteLine ( b.ToString() );     return 0; } 
end example

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

click to expand
Figure 2-6: Results of Chars.exe

Not long ago, all Windows programs used ASCII, an 8-bit, English-only character set. Unfortunately, this was not very helpful for languages such as Chinese, which requires more than the 256-character limit imposed by ASCII. To try to solve this obvious problem, a new encoding protocol was developed called Unicode, within which many character sets could be defined. Unicode uses 16 bits to represent each character instead of ASCII's 8. ASCII is a subset of Unicode.

Caution

Traditional C++ programmers must be wary of the C++ alias char, as it is not the same as the .NET Framework's class library Char. A char is an 8-bit ASCII character, whereas a Char is a 16-bit Unicode character.

Reference Types

As a Managed C++ programmer, you can think of reference types as pointers that you can't manipulate the address of and don't have to worry about deleting. In other languages such as C#, there are no such things as pointers, or so we're told. This isn't entirely true. The fact is, references are pointers. It's just that the syntax of the language (C# in this case) doesn't allow you to manipulate a reference as you can a pointer.

Managed C++ also restricts pointer manipulation for reference types. In other words, pointer arithmetic is illegal for references. This doesn't mean you can't do pointer arithmetic on regular nonreference pointers if you need to, though.

All Managed C++ developers will deal with two predefined reference types. One is the Object type, the root of all classes in the .NET Framework class library. The other is the String type.

Object Type

The System::Object is the root type of the entire .NET Framework class library hierarchy. In other words, every object found in the .NET Framework ultimately has, as a parent, the Object type.

Because all objects in the .NET Framework derive from System::Object, all objects inherit several general-purpose methods, such as

  • Object(): A constructor that creates a new instance of an object

  • Equals(): Compares two object instances to see if they are equal

  • GetHashCode(): Returns a hash code for the object

  • GetType(): Returns the data type of the object

  • ReferenceEquals(): Checks if two instances of an object are the same

  • ToString(): Returns a string representation of the object

A developer can replace a few of these methods. For example, replacing ToString() allows an object to represent itself in a user-readable fashion.

String Type

As a Managed C++ programmer, you will probably become very intimate with System::String. Many of your programs will involve character strings. The String type was built to handle them. Traditional C++ programmers should forget character arrays—you now have a powerful, predefined built-in data type to work with. Plus, it is completely garbage collected. In other words, no more memory leaks due to working with strings.

Being a reference type, strings are allocated to the heap and referenced using a pointer. String types are also immutable, meaning their value cannot be modified once they have been created. This combination allows for the optimized capability of multiple String objects representing the same character string to reference the same heap location. When a String object changes, a completely new character string is allocated to the heap and, if the original String object was not referenced by any other String object, it will be garbage collected.

Listing 2-6 is a little program showing the String type in action.

Listing 2-6: String Type in Action

start example
 #using <mscorlib.dll> using namespace System; Int32 main(void) {     // Create some strings     String *s1 = S"This will ";     String *s2 = S"be a ";     String *s3 = S"String";     Console::WriteLine(String::Concat(s1, s2, s3));     // Create a copy, then concatenate new text     String *s4 = s2;     s4 = String::Concat(s4, S"new ");     Console::WriteLine(String::Concat(s1, s4, s3));     // Replace stuff in a concatenated string     String *s5 = String::Concat(s1, s2, s3)->Replace(S"i", S"*");     Console::WriteLine(s5);     // Insert into a string     String *s6 = s3->Insert(2, S"range St");     Console::WriteLine(String::Concat(s1, s2, s6));     // Remove text from strings     s1 = s1->Remove(4, 5);  // remove from middle and overwriting     s2 = s2->Remove(0, 3);  // remove from start and overwriting     Console::WriteLine(String::Concat(s1, S"is ", s2, s3));     return 0; } 
end example

Figure 2-7 shows the results of this little program.

click to expand
Figure 2-7: Results of StringFun.exe

User-Defined Data Types

With Managed C++, you can create your own data types. Like predefined data types, user-defined data types fall into two groups: value types or reference types. As I pointed out earlier, value types are placed directly on the stack, whereas reference types are placed on the heap and referenced via the stack.

Value Types

Only three kinds of user-defined value types can be created using Managed C++:

  • __value enum

  • __value struct

  • __value class

The __value enum type is simply a named constant. The __value struct and __value class types are identical, except that the default access __value struct members are public, whereas __value class members are private.

__value enum

Enums are really nothing more than a programmer's tool for making source code more self-descriptive. Basically, enums allow the programmer to group common words together and use them instead of numeric literals. Personally, I think reading code that has descriptive enums as opposed to a bunch of numbers is much easier. So, I use enums. But hey, it's your code.

Like the built-in value types already discussed, enums default to being placed on the stack but will, in fact, become true objects when boxed. Unlike the built-in value types, which will on many occasions do the boxing for you, you must manually box the enum whenever it is to be accessed as an object. I cover boxing in more detail later in this chapter.

The following example creates an enum of all the primary colors. Then the function prints the string equivalent of the primary color enum using a switch statement. You will look at the switch statement later in this chapter.

The System::Object from which enums are derived provides a simpler way of doing this exact same thing. The ToString() method for enums prints out the enum name as a character string.

Listing 2-7 is a little program showing enums in action.

Listing 2-7: Enums in Action

start example
 #using <mscorlib.dll> using namespace System; __value enum PrimeColors { Red, Blue, Yellow }; Int32 main(void) {     PrimeColors color;     color = PrimeColors::Blue;     switch (color) // Old way     {         case PrimeColors::Red :             Console::WriteLine(S"Red");             break;         case PrimeColors::Blue :             Console::WriteLine(S"Blue");             break;         case PrimeColors::Yellow :             Console::WriteLine(S"Yellow");             break;     }     Console::WriteLine(__box(color)->ToString()); // New way     return 0; } 
end example

Figure 2-8 shows the results of this program.

click to expand
Figure 2-8: Results of Enums.exe

__ value struct

The __value struct type is very similar to the class type, which I cover briefly later in this chapter and in detail in Chapter 3. Basically, a struct (without the __value) is a way of combining data types and methods into one unit and placing it onto the heap.

With Managed C++, programmers now have the ability to move a struct from the heap and place it on the stack. Using the keyword __value in front of the keyword struct does this. Being on the stack, a struct can be created and copied a little more efficiently than the class, which has to deal with heap memory management.

The __value struct is Managed C++'s way of providing programmers with a method of creating their own value types, thus allowing for expansion beyond the basic built-in value types.

All __value structs are derived from the .NET Framework class library's System::ValueType, which allows for the __value struct's ability to be placed on the stack. A __value struct can inherit from only interfaces, and no class or struct can inherit from a __value struct.

Listing 2-8 is a simple example of a __value struct called Coord3D. It is made up of three Doubles and a constructor, and it overrides the ToString() method. You will look at constructors and overriding in Chapter 3. The main() function creates the two copies of Coord3D on the stack, one using the default constructor and the other using the one user-defined constructor. Notice that to assign a __value struct to another, you simply use the equal sign (=).

Listing 2-8: Struct in Action

start example
 #using <mscorlib.dll> using namespace System; __value struct Coord3D {     Double x;     Double y;     Double z;     Coord3D (Double x, Double y, Double z)     {         this->x = x;         this->y = y;         this->z = z;     }     String *ToString()     {         return String::Format(S"{0},{1},{2}", x.ToString(), y.ToString(),                                                  z.ToString());     } }; int main(void) {     Coord3D coordA;     Coord3D coordB(1,2,3);     coordA = coordB;   // Assign is simply an =     coordA.x += 5.5; //   Operations work just like usual     coordA.y *= 2.7;     coordA.z /= 1.3;     Console::WriteLine(coordB.ToString());     Console::WriteLine(coordA.x);     Console::WriteLine(coordA.y);     Console::WriteLine(coordA.z);     return 0; } 
end example

Figure 2-9 shows the results of this program.

click to expand
Figure 2-9: Results of ValueStruct.exe

Reference Types

User-defined reference types are basically data types a programmer develops that are accessed using their address, and where the actual data object is located on the heap. All reference types in Managed C++ can be defined so that they are garbage collected.

Managed C++ provides four kinds of user-defined reference types: arrays, classes, interfaces, and delegates.

Arrays

Arrays, like all other data types in Managed C++, are objects, unlike their traditional C++ counterparts, which are simply pointers into heap memory. In fact, the only resemblance between a Managed C++ and a traditional C++ array is its single-dimension syntax.

All Managed C++ arrays are garbage collected. Also, they can be made up of any data type that derives from System::Object. If you recall, that is every data type in the .NET Framework class library.

Managed C++ arrays have specific dimensions which, when violated, will generate an exception. All arrays are derived from a System:: Array object, which provides them with many helpful methods and properties, in particular the Count property for single-dimension arrays and the GetLength() method for single- or multidimensional arrays. Both of these provide the dimensions of the array.

There are no stack base declarations of Managed C++ arrays using subscripts, like in traditional C++. All Managed C++ arrays are references and created on the heap. For example, the following code will generate an error:

 Int32 errorArray[5];    // Oops stack based arrays not allowed 

Note

It is still possible to create stack-based declarations of unmanaged C++ arrays, just as you would in traditional C++, as that syntax is still available to you. But then you lose all the benefits of Managed C++ development.

An array can have many dimensions. The basic syntax to declare an array is as follows:

 data-type array-name[,,,]; Int32 array4dim[3,4,5,6]; 

where the number of commas determines the number of dimensions or, basically, the number of commas plus one.

In the case of a single-dimensional array, no commas are placed between the square brackets:

 data-type array-name[]; Int32 array1dim[]; 

To initialize an array, you need to use the new keyword because the array will be allocated to the heap. Follow the new keyword with the data type and then give the array dimensions within square brackets:

 Int32 a[] = new Int32[5]; String d[,] = new String*[4,3]; 

Unlike traditional C++, subscripting is not a synonym for pointer arithmetic, and it is not commutative. Thus, the only way to access data from an array is by using subscripts with all dimensions starting at a value of zero.

Two very helpful static methods of the System:: Array are Sort() and Reverse(), which provide quick ways to sort and reverse the order of the elements in an array. Reverse() is shown in the following example.

Listing 2-9 is a program showing Managed C++ arrays in action.

Listing 2-9: Managed C++ Arrays in Action

start example
 #using <mscorlib.dll> using namespace System; Int32 main(void) {     // Single dimension     Int32 a[] = new Int32[5];     String *b[] = new String*[5];     // Initializing an array with incrementing numbers     for (Int32 i = 0; i < a->Count; i++)     {         a[i] = i;     }     // Initializing an array with incrementing number strings     for (Int32 i = 0; i < b->Count; i++)     {         b[i] = a[i].ToString();     }     // Write out the array     for (Int32 i = 0; i < b->Count; i++)     {         Console::WriteLine(b[i]);     }     // Reverse the order of the array and write it out     Console::WriteLine(S"");  // Separate to make easier to read     Array::Reverse(b);      // reverse the array order     for (Int32 i = 0; i < b->Count; i++)     {         Console::WriteLine(b[i]);     }     // Multi dimension     Int32 c[,] = new Int32[4,3];     // Initialize the array so that the 10s digit is the x column value     // and the 1s digit is the y value     for (Int32 x = 0; x < c->GetLength(0); x++)     {         for (Int32 y = 0; y < c->GetLength (1); y++)         {             c[x,y] = (x*10)+y;         }     }     // Write out the 2 dimensional array     Console::WriteLine(S""); // Separate to make easier to read     for (Int32 x = 0; x < d->GetLength(0); x++)     {         for (Int32 y = 0; y < d->GetLength(1); y++)         {             Console::Write(c[x,y]);             Console::Write(S"\t");         }         Console::WriteLine();     }     return 0; } 
end example

Figure 2-10 shows the results of this little program.

click to expand
Figure 2-10: Results of Arrays.exe

Classes

A class is a fundamental building block of most Managed C++ programs. Classes are made up of data members, properties, and methods. Basically, classes are designed to provide the object-oriented nature of the Managed C++ programming language. In other words, they provide encapsulation, inheritance, and polymorphism.

Chapter 3 covers classes in great detail.

Interfaces

An interface is a collection of methods and properties, without actual definitions, placed into a single unit. In other words, an interface has no implementations for its own methods and properties. You might want to think of an interface as a binding contract of all the methods and properties that an inheriting class must provide.

Chapter 3 covers interfaces.

Delegates

A delegate is a reference type that acts as a "function pointer" that can be bound to either an instance or a static method within a Managed C++ class. Delegates can be used whenever a method needs to be called in a dynamic nature, and they are usually used as callback functions or for handling events within .NET Framework applications.

You will examine delegates in Chapter 4.

Boxing and Unboxing

Boxing is the Managed C++ technique for converting value types into reference types. And, conversely, unboxingis the technique for converting reference types into value types.

The default form of storage for the .NET Framework built-in value types is on the stack, as a simple value. In this form, a data type cannot access its methods, such as ToString(), as the value type needs to be in an object (reference) format. To remedy this, the value type automatically gets boxed whenever the ToString() method is called. For example, the following two lines are equivalent for all built-in value types:

 __box(x)->ToString(); x.ToString(); 

This boxing is more apparent in user-defined value types, as this automatic conversion does not take place and the programmer has to do it manually. For user-defined value types, only the previous manually boxed version is allowed. The other one generates an error.

Caution

The created boxed object is a copy of the value type. Therefore, any modifications made to the boxed object will not be reflected in the contents of the value type.

It is possible to create a boxed reference by placing the prefix __box in front of the data type:

    __box Int32 *bx; 

Unboxing a reference type back into its value type simply requires a type cast. Personally, I've never had a reason to do this. I cover type casting later in this chapter.

    bx = __box(x);                         // Boxing    Int32 y  = static_cast<Int32>(*bx);  // Unboxing 

Type Modifiers and Qualifiers

Three modifiers and one data type qualifier are provided to Managed C++ programmers. Basically, they provide a little information to help define the variables they precede.

auto

This modifier tells the compiler that it should create the variable when entering a block and destroy it when exiting the block. If this sounds like most variables to you, you would be right, as it is the default modifier for all variables. Placing the auto keyword in front of variables is optional. In fact, I have never seen it used myself, but if you like typing, here is how you would use it in a program:

 auto Int32 normalInteger; 

const

The const qualifier tells the compiler that the variable that it is associated with cannot change during execution. It also means that objects pointed to by a const pointer cannot be changed. Basically, constants are the opposite of variables. The syntax to create a const data type is simply this:

 const Int32 integerConstant = 42; 

Note that you need to initialize a const at the time of declaration.

Caution

Managed C++ does not support const member methods on managed data types. For example, Boolean GetFlag() const {} is not allowed within a __value struct or class and thus by default an interface.

extern

The extern modifier tells the compiler that the variable is defined elsewhere, usually in a different file, and will be added in when the final executable or library is linked together. It tells the compiler how to define a variable without actually allocating any storage for it. You will see this variable modifier usually when a global variable is used in more than one source file (I discuss this later in the chapter).

Note

An error will occur during the linking of the application if an external variable is not defined in some other source file.

Using the extern modifier looks like this:

 extern Int32 externVariable; 

static

The static modifier has four meanings based on where it is used.

When the static modifier is applied to a global variable, the variable's global nature is restricted to the source file in which it is declared. In other words, the variable is accessible to all functions, classes, and so on declared within the file, but an extern variable or class in another source file will not have access to it.

When the static modifier is applied to a variable within a function (I cover functions later in the chapter), then the variable will not go out of scope or be deleted when the function exits. This means that the next time the function is called, the static variable will retain the same value it had when the function was left the previous time.

When the static modifier is applied to a variable within a class (I discuss classes in Chapter 3), then only one copy of the variable is created, and it is shared by all instances of the class.

When the static modifier is applied to a method within a class, then the method is accessible without the need to instantiate the class.

Here are some basic examples of the static modifier in use:

 static Int32 staticVariable; static void staticFunction ( Int32 arg) { } 

Type Conversions

Any time the data type on the left side of an assignment statement has a different data type than the evaluated result of the right side, a type conversion will take place. When the only data types used in the statement are built-in value types, then the conversion will happen automatically. Unfortunately, converting automatically may not always be a good thing, especially if the left side data type is smaller, as the resulting number may lose significant digits. For example, when assigning a UInt16 to a Byte, the following problem may occur:

 UInt16 a = 43690; Byte b = a;      // b now equals 170 not 43690. 

Here is what happened. UInt16 is a 16-bit number, so 43690 decimal represented as a 16-bit number is 1010 1010 1010 1010 in binary. Byte is an 8-bit number, so only the last 8 bits of the UInt16 can be placed into the Byte. Thus, the Byte now contains 1010 1010 in binary, which happens to only equal 170 decimal.

The Managed C++ compiler will notify you when this type of error may occur. Being warned, the compiler and, subsequently, the program it generates go merrily on their way.

If you don't want the warning, but you still want to do this type of conversion, then you can do something called an explicit cast. Basically, it's the programmer's way of saying "Yes, I know, but I don't care." To code an explicit cast, you use the following syntax:

 static_cast<data-type-to-convert-to>(expression) Byte b = static_cast<Byte>(a); 

In Managed C++, when resolving an expression, all data types that make up the expression must be the same. If the expression is made up of more than one type, then type conversion occurs to make all the data types the same. Basically, if all the data types are integer types, then the data types get converted to an Int32 or Int64 data type. If a data type is float, then all get converted to a Single or Double.

All these types of conversions happen automatically. There are cases, though, where you may want all data types to be converted to a data type of your choosing. Here again, you use explicit casting, as shown here:

 Double realA = 23.67; Double realB = 877.12; Int32 intTotal = static_cast<Int32>(realA) + static_cast<Int32>(realB); 

Variable Scope

There are basically two different scopes: global and local. There are subtleties that might bend these scopes a bit, but they're something most programmers don't care about.

Global scope for a variable means that it is declared outside of all functions, classes, and structures that make up a program, even the main() functions. They are created when the program is started and exist for the entire lifetime of the program. All functions, classes, and structures can access global variables. The static modifier has the capability to restrict a global variable to only the source file in which it is declared.

Local variables are local to the block of code that they are declared in. This means that local variables exist within the opening and closing curly brackets within which they were declared. Most commonly, local variables are declared within a function call, but it is perfectly acceptable to declare them within flow control and looping constructs, which you will learn about later in this chapter. It is also valid to create a block of code only to reduce the scope of a variable.

The following code shows some global and local variable declarations:

 Int32 globalVar; Int32 main() {     Int32 localFunctionVar;     { Int32 localToOwnBlock; } } 

Namespaces

Some programmers program in an isolated world where their code is the only code. Others use code from many sources. A problem with using code from many sources is that there is a very real possibility that the same names for classes, functions, and so on, can be used by more than one source.

To allow for the same names to be used by multiple sources, namespaces were created. Namespaces create a local scope declarative region for variables, functions, classes, and structures. In other words, namespaces allow programmers to group their code under a unique name.

Creating a namespace simply requires combining all of the code within a named region, such as

 namespace MyNamespace {     // classes, structs, functions, namespace-global variables } 

It is possible to use the same namespace across multiple source code files. The compiler will combine them into one namespace.

To reference something out of a namespace requires the use of the :: operator. For example:

 MyNamespace::NSfunc(); 

Typing the namespace repeatedly can get very tiring, so Managed C++ allows the programmer to bring a namespace into the local scope using

 using namespace MyNamespace; 

Now, with the namespace brought into local scope, the function NSfunc from the previous example can be accessed just like any other function of local scope:

 NSfunc(); 

Caution

Bringing multiple namespaces into the local scope could cause duplicate function, class, and struct names to occur.




Managed C++ and. NET Development
Managed C++ and .NET Development: Visual Studio .NET 2003 Edition
ISBN: 1590590333
EAN: 2147483647
Year: 2005
Pages: 169

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net