Pointers and Arrays


In C#, pointers and arrays are related. For example, the name of an array without any index generates a pointer to the start of the array. Consider the following program:

 /* An array name without an index yields a pointer to the    start of the array. */ using System; class PtrArray {   unsafe public static void Main() {     int[] nums = new int[10];     fixed(int* p = &nums[0], p2 = nums) {       if(p == p2)         Console.WriteLine("p and p2 point to same address.");     }   } }

The output is shown here:

 p and p2 point to same address.

As the output shows, the expression

 &nums[0]

is the same as

 nums

Since the second form is shorter, most programmers use it when a pointer to the start of an array is needed.

Indexing a Pointer

When a pointer refers to an array, the pointer can be indexed as if it were an array. This syntax provides an alternative to pointer arithmetic that can be more convenient in some situations. Here is an example:

 // Index a pointer as if it were an array. using System; class PtrIndexDemo {   unsafe public static void Main() {     int[] nums = new int[10];     // index pointer     Console.WriteLine("Index pointer like array.");     fixed (int* p = nums) {       for(int i=0; i < 10; i++)         p[i] = i; // index pointer like array       for(int i=0; i < 10; i++)         Console.WriteLine("p[{0}]: {1} ", i, p[i]);     }     // use pointer arithmetic     Console.WriteLine("\nUse pointer arithmetic.");     fixed (int* p = nums) {       for(int i=0; i < 10; i++)         *(p+i) = i; // use pointer arithmetic       for(int i=0; i < 10; i++)         Console.WriteLine("*(p+{0}): {1} ", i, *(p+i));     }   } }

The output is shown here:

 Index pointer like array. p[0]: 0 p[1]: 1 p[2]: 2 p[3]: 3 p[4]: 4 p[5]: 5 p[6]: 6 p[7]: 7 p[8]: 8 p[9]: 9 Use pointer arithmetic. *(p+0): 0 *(p+1): 1 *(p+2): 2 *(p+3): 3 *(p+4): 4 *(p+5): 5 *(p+6): 6 *(p+7): 7 *(p+8): 8 *(p+9): 9

As the program illustrates, a pointer expression with this general form:

 *(ptr + i)

can be rewritten using array-indexing syntax like this:

 ptr[i]

There are two important points to understand about indexing a pointer: First, no boundary checking is applied. Thus, it is possible to access an element beyond the end of the array to which the pointer refers. Second, a pointer does not have a Length property. So, using the pointer, there is no way of knowing how long the array is.

Pointers and Strings

Although strings are implemented as objects in C#, it is possible to access the characters in a string through a pointer. To do so, you will assign a pointer to the start of the string to a char* pointer using a fixed statement like this:

 fixed(char* p = str) { // ...

After the fixed statement executes, p will point to the start of the array of characters that make up the string. This array is null-terminated, which means that it ends with a 0. You can use this fact to test for the end of the array. Null-terminated character arrays are the way that strings are implemented in C/C++. Thus, obtaining a char* pointer to a string allows you to operate on strings in much the same way that you do in C/C++.

Here is a program that demonstrates accessing a string through a char* pointer:

 // Use fixed to get a pointer to the start of a string. using System; class FixedString {   unsafe public static void Main() {     string str = "this is a test";     // Point p to start of str.     fixed(char* p = str) {       // Display the contents of str via p.       for(int i=0; p[i] != 0; i++)         Console.Write(p[i]);     }     Console.WriteLine();   } }

The output is shown here:

 this is a test

Multiple Indirection

You can have a pointer point to another pointer that points to the target value. This situation is called multiple indirection, or pointers to pointers. Pointers to pointers can be confusing. Figure 19-1 helps clarify the concept of multiple indirection. As you can see, the value of a normal pointer is the address of the variable that contains the value desired. In the case of a pointer to a pointer, the first pointer contains the address of the second pointer, which points to the variable that contains the value desired.

image from book
Figure 19-1: Single and multiple indirection

Multiple indirection can be carried on to whatever extent desired, but more than a pointer to a pointer is rarely needed. In fact, excessive indirection is difficult to follow and prone to conceptual errors.

A variable that is a pointer to a pointer must be declared as such. You do this by using an additional asterisk. For example, the following declaration tells the compiler that q is a pointer to a pointer of type int:

 int** q;

You should understand that q is not a pointer to an integer, but rather a pointer to an int pointer.

To access the target value indirectly pointed to by a pointer to a pointer, you must apply the asterisk operator twice, as in this example:

 using System; class MultipleIndirect {   unsafe public static void Main() {     int x;    // holds an int value     int* p;  // holds an int pointer     int** q; // holds a pointer to an int pointer     x = 10;     p = &x; // put address of x into p     q = &p; // put address of p into q     Console.WriteLine(**q); // display the value of x   } }

The output is the value of x, which is 10. In the program, p is declared as a pointer to an int and q as a pointer to an int pointer.

One last point: Do not confuse multiple indirection with high-level data structures, such as linked lists, that use pointers. These are two fundamentally different concepts.

Arrays of Pointers

Pointers can be arrayed like any other data type. The declaration for an int pointer array of size 3 is

 int * [] ptrs = new int * [3];

To assign the address of an int variable called var to the third element of the pointer array, write

 ptrs[2] = &var;

To find the value of var, write

 *ptrs[2]

sizeof

When working in an unsafe context, you might occasionally find it useful to know the size, in bytes, of one of C#’s value types. To obtain this information, use the sizeof operator. It has this general form:

 sizeof(type)

Here, type is the type whose size is being obtained. The sizeof operator can be used only in an unsafe context. Thus, it is intended primarily for special-case situations, especially when working with a blend of managed and unmanaged code.

stackalloc

You can allocate memory from the stack by using stackalloc. It can be used only when initializing local variables and has this general form:

 type *p = stackalloc type[size]

Here, p is a pointer that receives the address of the memory that is large enough to hold size number of objects of type. stackalloc must be used in an unsafe context.

Normally, memory for objects is allocated from the heap, which is a region of free memory. Allocating memory from the stack is the exception. Variables allocated on the stack are not garbage-collected. Rather, they exist only while the block in which they are declared is executing. When the block is left, the memory is freed. One advantage to using stackalloc is that you don’t need to worry about the memory being moved about by the garbage collector.

Here is an example that uses stackalloc:

 // Demonstrate stackalloc. using System; class UseStackAlloc {   unsafe public static void Main() {     int* ptrs = stackalloc int[3];     ptrs[0] = 1;     ptrs[1] = 2;     ptrs[2] = 3;     for(int i=0; i < 3; i++)       Console.WriteLine(ptrs[i]);   } }

The output is shown here:

 1 2 3

Creating Fixed-Size Buffers

C# 2.0 expanded the use of the fixed keyword to enable you to create fixed-size, one-dimensional arrays. In the C# documentation, these are referred to as fixed-size buffers. A fixed-size buffer is always a member of a struct. The purpose of a fixed-size buffer is to allow the creation of a struct in which the array elements that make up the buffer are contained within the struct. Normally, when you include an array member in a struct, only a reference to the array is actually held within the struct. By using a fixed-size buffer, you cause the entire array to be contained within the struct. This results in a structure that can be used in situations in which the size of a struct is important, such as in mixed-language programming, interfacing to data not created by a C# program, or whenever a non-managed struct containing an array is required. Fixed-size buffers can only be used within an unsafe context.

To create a fixed-size buffer, use this form of fixed:

 fixed type buf-name[size];

Here, type is the data type of the array, buf-name is the name of the fixed-size buffer, and size is the number of elements in the buffer. Fixed-size buffers can only be specified inside a struct.

To understand why a fixed-size buffer might be useful, consider a situation in which you want to pass bank account information to an account management program that is written in C++. Further assume that each account record uses the following organization:

name

An 8-bit, ASCII character string, 80 bytes long

balance

A double, 8 bytes long

ID

A long, 8 bytes long

In C++, each structure, itself, contains the name array. This differs from C#, which would normally just store a reference to the array. Thus, to represent this data in a C# struct requires the use of a fixed-size buffer, as shown here:

 // Use a fixed-size buffer. unsafe struct FixedBankRecord {   public fixed byte name[80]; // create a fixed-size buffer   public double balance;   public long ID; }

By using a fixed-size buffer for name, each instance of FixedBankRecord will contain all 80 bytes of the name array, which is the way that a C++ struct would be organized. Thus, the overall size of FixedBankRecord is 96, which is the sum of its members. Here is a program that demonstrates this fact:

 // Demonstrate a fixed-size buffer. using System; // Create a fixed-size buffer. unsafe struct FixedBankRecord {   public fixed byte name[80]; // create a fixed-size buffer   public double balance;   public long ID; } class FixedSizeBuffer {   // mark Main as unsafe   unsafe public static void Main() {     Console.WriteLine("Size of FixedBankRecord is " +                        sizeof(FixedBankRecord));   } }

The output is shown here:

 Size of FixedBankRecord is 96

Although the size of FixedBankRecord is the exact sum of its members, this may not be the case for all structs that have fixed-size buffers. C# is free to pad the overall length of a structure so that it aligns on an even boundary, for efficiency reasons. Therefore, the overall length of a struct might be a few bytes larger than the sum of its fields, even when fixed-size buffers are used. In most cases, an equivalent C++ struct would also use the same padding. However, be aware that a difference in this regard may be possible.

One last point: In the program, notice how the fixed-size buffer for name is created:

 public fixed byte name[80]; // create a fixed-size buffer

Pay special attention to how the dimension of the array is specified. The brackets containing the array size follow the array name. This is C++-style syntax, and it differs from normal C# array declarations. This statement allocates 80 bytes of storage within each FixedBankAccount object.




C# 2.0(c) The Complete Reference
C# 2.0: The Complete Reference (Complete Reference Series)
ISBN: 0072262095
EAN: 2147483647
Year: 2006
Pages: 300

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