Dynamic Allocation and Arrays
So far, all arrays in this module have had a fixed size allocated at compile time. It is possible and very common to create arrays dynamically at run time using the new operator. The array you create still has a fixed size, but this size can be specified at run time when you know how many elements you need. The following exercise shows how to create an array dynamically and then use it.
1. Open Visual C++/Studio, and create a new CLR Console Application project named Dynamic.
|
2. Open the source file Dynamic.cpp and add the following code to the main() function:
int main(array<System::String ^> ^args)
{
// Create an array dynamically
int* pa = new int[10];
// Fill the array
for(int i=0; i<10; i++)
pa[i] = i*2;
// Print the array content
for(int j=0; j<10; j++)
Console::Write(" {0}", pa[j]);
Console::WriteLine();
// Get rid of the array once we’re finished with it
delete pa;
return 0;
}
3. Build and run your project. The expected output is shown below.
You’ve previously used the new operator to create .NET reference types, but the operator is also used in traditional C++ code to allocate memory dynamically at run time. The syntax is new, followed by the type of the array and the dimension in square brackets. Once the array has been created, you’re returned a pointer to the start of the array. You can see that dynamic arrays are accessed in exactly the same way as statically allocated arrays, using the square brackets notation. This use of a pointer with array notation underlines the relationship between pointers and arrays, as explained in the “How Do Native Arrays Work?” earlier in this module.
Notice the call to delete just before the program exits. Allocating an array dynamically in traditional C++ doesn’t create a managed object, so there is no garbage collection associated with this array. So, to use memory efficiently, you have to remember to deallocate memory once you’ve finished with the array. Strictly speaking, the call is unnecessary here because all allocated memory is freed up when the program exits. However, in any real-world program, you need to manage your memory carefully to make sure all memory is freed up at an appropriate point. Once you’ve called delete on a pointer, you must not use the pointer again because the memory it points to is no longer allocated to you. If you try to use a pointer after freeing up the memory it points to, you can expect to get a run-time error.
Problems with Manual Memory Management
Manual memory management is widely considered to be the single biggest cause of bugs in C and C++ programs and it’s the driving force behind the development of the garbage collection mechanisms in languages such as C#. If it’s up to the programmers to call delete on every piece of memory they allocate, mistakes are going to be made. Two main problems are associated with manual memory management:
Not freeing up memory. This problem is normally the less serious of the two, and it results in a program taking up more memory than it needs, a process known as memory leakage. In extreme cases, the amount of extra memory consumed by an application can reach the point where memory leakage starts to interfere with other applications or even the operating system.
Freeing up memory inappropriately. In a complex program, it might not be obvious where a particular piece of memory should be freed up or whose responsibility it is to free it. If delete gets called too soon and another piece of code tries to use the dynamically allocated array, you can expect a run-time error. The same is true if anyone attempts to call delete on the same pointer more than once.
Although manual memory allocation using new and delete lets you do some very clever things, these two problems were the impetus behind the development of garbage collectors, which make the system track the use of dynamically allocated memory and free it up when no one else is using it.
Managed Extension for C++: __gc Arrays, The Old Syntax
For the Managed Extension for C++ (already deprecated) , the .NET Framework has extended the C++ array model by adding __gc arrays. As you might expect from the use of the __gc keyword, a __gc array is a dynamic array that is allocated on the .NET heap and is subject to the usual garbage collection rules. Unlike standard C++ arrays, subscripting in __gc arrays is not a synonym for pointer arithmetic. You can create a __gc array in a very similar way to a traditional dynamic array:
Int32 gcArray[] = new Int32[10];
Notice that the array type is the .NET Int32 rather than the built-in int type. Notice also the way that gcArray has been declared: it’s no longer a pointer, as with traditional arrays, but it’s a managed object. All __gc arrays inherit from System::Array, so any method or property of System::Array can be directly applied to the __gc array. See the section “The .NET Array Class” later in this module for details about System::Array and how to use it.
Using the __gc and __nogc Keywords
For old syntax, you can use the __gc and __nogc keywords to determine whether a managed or an unmanaged array is going to be created. Normally, creating an array of primitive types will result in an unmanaged array. However, you can use the __gc keyword to create a managed array of a primitive type, as shown in the following code:
// Create an unmanaged array of ints
int* arr = new int[10];
// Create a managed array of ints
int arr1 __gc[] = new int __gc[10];
The __gc[] syntax will create an array of primitive types that is subject to the usual garbage collection mechanism. In a similar way, you can use __nogc to create traditional unmanaged arrays of .NET types, provided that the type corresponds to one of the C++ primitive types.
// Create an unmanaged array of Int32
Int32 arr1 __nogc[10];
This array is not a managed array object, and it won’t be garbage collected. In addition, because it isn’t an array object, it doesn’t support any of the functionality of the System::Array class.
Managed Array (array)
The array keyword lets you create a dynamic array that is allocated on the common language runtime heap. The information for managed array is:
Syntax | [qualifiers] [cli::]array<[qualifiers]type1[, dimension]>^ var = gcnew [cli::]array<type2[, dimension]>(val[,val...]) |
|
|
Parameters | Description |
dimension [optional] | The number of dimensions of the array. The default is 1. The maximum is 32. |
qualifiers [optional] | A storage class specifier. Valid keywords for qualifier are: mutable, volatile, const, extern, and static. |
val | A comma-separated list of the size of your array or an aggregate initializer. There should be one size val for each dimension (as in multidimensional array). |
type1 | The type of the array variable. Valid types are managed reference types (type^), managed value types (type), and native pointers (type*). A tracking handle (^) is always required after the closing angle bracket (>) in the declaration. |
type2 | The type of the values initializing the array. Commonly, type1 and type2 will be the same types. However, it is possible for the types to be different, as long as there is a conversion from type2 to type1. For example, if type2 is derived from type1. Like type1, valid types are managed reference types, managed value types, and native pointers. However, a tracking handle is not allowed after the closing angle bracket (>). |
var | The name of the array variable. |
Table 1 |
The number of elements of the array is not part of the type. A single array variable can refer to arrays of different sizes. Like standard C++, the indices of a managed array are zero-based, and a managed array is subscripted using ordinary C++ array brackets. Unlike standard C++, subscripting is not a synonym for pointer arithmetic and is not commutative. A managed array is itself a managed object. It is actually a pointer into the common language runtime heap. As a managed object, it has the same restrictions as a managed class. Most notably, the element type cannot be a native C++ class that is not a POD type. array is in the cli namespace. Take note that the array is a template based and if you familiar with the Standard Template Library (STL) then you should not face any problem in understanding this section. All managed arrays inherit from System::Array. Any method or property of System::Array can be applied directly to the array variable. When allocating an array whose element type is pointer-to a managed class, the elements are 0-initialized. When allocating an array whose element type is a value type for example Val, the default constructor for Val is applied to each array element. Simple example is shown below.
|
The Old Syntax vs the new Syntax (The Managed Extension for C++ vs New Managed C++ Syntax)
The syntax for declaring, instantiating, and initializing a managed array has changed from Managed Extensions for C++ (shown in the previous section) to Visual C++ 2005. The declaration of a CLR array object in Managed Extensions was a slightly non-intuitive extension of the standard array declaration in which a __gc keyword was placed between the name of the array object and its possibly comma-filled dimension, as in the following pair of examples as explained in previous section:
void PrintValues(Object* myArr __gc[]);
void PrintValues(int myArr __gc[,,]);
This has been simplified in the new syntax, in which we use a template-like declaration that suggests the STL vector declaration. The first parameter indicates the element type. The second parameter specifies the array dimension (with a default value of 1, so only multiple dimensions require a second argument). The array object itself is a tracking handle and so must be given a hat. If the element type is also a reference type, that, too, then must be so marked. For example, the above example, when expressed in the new syntax, looks as follows:
void PrintValues(array<Object^>^ myArr);
void PrintValues(array<int,3>^ myArr);
Because a reference type is a tracking handle rather than an object, it is possible to specify a CLR array as the return type of a function. It is not possible to specify the native array as the return type of a function. The syntax for doing this in Managed Extensions was somewhat non-intuitive. For example:
Int32 f() [];
int GetArray() __gc[];
In Visual C++ 2005, the declaration is much simpler for the human reader to parse. For example,
array<Int32>^ f();
array<int>^ GetArray();
The shorthand initialization of a local managed array is supported in both versions of the language. For example:
int GetArray() __gc[]
{
int a1 __gc[] = { 1, 2, 3, 4, 5 };
Object* myObjArray __gc[] = {
__box(26), __box(27), __box(28), __box(29), __box(30)
};
return a1;
}
is considerably simplified in the new syntax (note that because boxing is implicit in the new syntax, the __box operator has been eliminated.
array<int>^ GetArray()
{
array<int>^ a1 = {1,2,3,4,5};
array<Object^>^ myObjArray = {26,27,28,29,30};
return a1;
}
Because an array is a CLR reference type, the declaration of each array object is a tracking handle. Therefore, it must be allocated on the CLR heap. The shorthand notation hides the managed heap allocation. Here is the explicit form of an array object initialization under Managed Extensions:
Object* myArray[] = new Object*[2];
String* myMat[,] = new String*[4,4];
Under the new syntax, the new expression, recall, is replaced with gcnew. The dimension sizes are passed as parameters to the gcnew expression, as follows:
array<Object^>^ myArray = gcnew array<Object^>(2);
array<String^,2>^ myMat = gcnew array<String^,2>(4,4);
In the new syntax, an explicit initialization list can follow the gcnew expression; this was not supported in Managed Extensions. For example:
// explicit initialization list following gcnew
// was not supported in Managed Extensions
array<Object^>^ myArray = gcnew array<Object^>(4){ 5, 4, 2, 3 };