The reason arrays are passed to functions as pointers is efficiency, so often the justification for transgressions against good software engineering practice. The Fortran I/O model is tortuous because it was thought "efficient" to re-use the existing (though clumsy and already obsolete) IBM 704 assembler I/O libraries. Comprehensive semantic checking was excluded from the Portable C Compiler on the questionable grounds that it is more "efficient" to implement lint as a separate program. That decision has been implicitly revoked by the enhanced error checking done by most ANSI C compilers.
The array/pointer equivalence for parameters was done for efficiency. All non-array data arguments in C are passed "by value" (a copy of the argument is made and passed to the called function; the function cannot change the value of the actual variable used as an argument, just the value of the copy it has). However, it is potentially very expensive in memory and time to copy an array; and most of the time, you don't actually want a copy of the array, you just want to indicate to the function which particular array you are interested in at the moment. One way to do this might be to allow parameters to have a storage specifier that says whether it is passed by value or by reference, as occurs in Pascal. It simplifies the compiler if the convention is adopted that all arrays are passed as a pointer to the start, and everything else is passed by making a copy of it. Similarly the return value of a function can never be an array or function, only a pointer to an array or function.
Some people like to think of this as meaning that all C arguments are call-by-value by default, except arrays and functions; these are passed as call-by-reference parameters. Data can be explicitly passed as call-by-reference by using the "address of" operator. This causes the address of the argument to be sent, rather than a copy of the argument. In fact, a major use of the address-of operator & is simulating call-by-reference. This "by reference" viewpoint isn't strictly accurate, because the implementation mechanism is explicit—in the called procedure you still only have a pointer to something, and not the thing itself. This makes a difference if you take its size or copy it.
Figure 9-3 shows the steps involved in accessing a subscripted array parameter.
Note well that this is identical to Diagram C on page 101, showing how a subscripted pointer is looked up. The C language permits the programmer to declare and refer to this parameter as either an array (what the programmer intends to pass to the function) or as a pointer (what the function actually gets). The compiler knows that whenever a formal parameter is declared as an array, inside the function it will in fact always be dealing with a pointer to the first element of an array of unknown size. Thus, it can generate the correct code, and does not need to distinguish between cases.
No matter which of these forms the programmer writes, the function doesn't automatically know how many elements there are in the pointed-to thing. There has to be some convention, such as a NUL end marker or an additional parameter giving the array extent. This is not true in, for example, Ada, where every array carries around with it a bunch of information about its element size, dimensions, and indices.
Given these definitions:
func( int * turnip){...}
or
func( int turnip[]){...}
or
func( int turnip[200]){...}
int my_int; /* data definitions */
int * my_int_ptr;
int my_int_array[10];
you can legally call any of the function prototypes above with any of the following arguments. They are often used for very different purposes:
Actual Argument in Call | Type | Common Purpose |
---|---|---|
func(&my_int ); | Address of an integer | "Call-by-reference" of an int |
func( my_int_ptr ); | A pointer to an integer | To pass a pointer |
func( my_int_array ); | An array of integers | To pass an array |
func(&my_int_array[i] ); | Address of an element of int array | To pass a slice of an array |
Conversely, if you are inside func(), you don't have any easy way of telling which of these alternative arguments, and hence with which purpose, the function was invoked. All arrays that are function arguments are rewritten by the compiler at compiletime into pointers. Therefore, any reference inside a function to an array parameter generates code for a pointer reference. Figure 9-3 on page 247 shows what this means in practice.
Interestingly, therefore, there is no way to pass an array itself into a function, as it is always automatically converted into a pointer to the array. Of course, using the pointer inside the function, you can pretty much do most things you could have done with the original array. You won't get the right answer if you take the size of it, though.
You thus have a choice when declaring such a function. You can define the parameter as either an array or a pointer. Whichever you choose, the compiler notices the special case that this object is a function argument, and it generates code to dereference the pointer.
|
It takes some discipline to keep all this straight! Our preference is always to define the parameter as a pointer, since that is what the compiler rewrites it to. It's questionable programming style to name something in a way that wrongly represents what it is. But on the other hand, some people feel that:
int table[] | instead of | int *table |
explains your intentions a bit better. The notation table[] makes plain that there are several more int elements following the one that table points to, suggesting that the function will process them all.
Note that there is one thing that you can do with a pointer that you cannot do with an array name: change its value. Array names are not modifiable l-values; their value cannot be altered. See Figure 9-4 (the functions have been placed side by side for comparison; they are all part of the same file).
pointer argument array argument pointer non-argument |
The statement array = array2; will cause a compiletime error along the lines of "cannot change the value of an array name". But it is valid to write arr = array2; because arr, though declared as an array, is actually a pointer.
Pointers occur in many C programs as references to arrays , and also as elements of arrays. A pointer to an array type is called an array pointer for short, and an array whose elements are pointers is called a pointer array.
For the sake of example, the following description deals with an array of int. The same principles apply for any other array type, including multidimensional arrays.
To declare a pointer to an array type, you must use parentheses, as the following example illustrates:
int (* arrPtr)[10] = NULL; // A pointer to an array of
// ten elements with type int.
Without the parentheses, the declaration int * arrPtr[10]; would define arrPtr as an array of 10 pointers to int. Arrays of pointers are described in the next section.
In the example, the pointer to an array of 10 int elements is initialized with NULL. However, if we assign it the address of an appropriate array, then the expression *arrPtr yields the array, and (*arrPtr)[i] yields the array element with the index i. According to the rules for the subscript operator, the expression (*arrPtr)[i] is equivalent to *((*arrPtr)+i) (see "Memory Addressing Operators" in Chapter 5). Hence **arrPtr yields the first element of the array, with the index 0.
In order to demonstrate a few operations with the array pointer arrPtr, the following example uses it to address some elements of a two-dimensional array—that is, some rows of a matrix (see "Matrices" in Chapter 8):
int matrix[3][10]; // Array of three rows, each with 10 columns.
// The array name is a pointer to the first
// element; i.e., the first row.
arrPtr = matrix; // Let arrPtr point to the first row of
// the matrix.
(*arrPtr)[0] = 5; // Assign the value 5 to the first element of the
// first row.
//
arrPtr[2][9] = 6; // Assign the value 6 to the last element of the
// last row.
//
++arrPtr; // Advance the pointer to the next row.
(*arrPtr)[0] = 7; // Assign the value 7 to the first element of the
// second row.
After the initial assignment, arrPtr points to the first row of the matrix, just as the array name matrix does. At this point you can use arrPtr in the same way as matrix to access the elements. For example, the assignment (*arrPtr)[0] = 5 is equivalent to arrPtr[0][0] = 5 or matrix[0][0] = 5.
However, unlike the array name matrix, the pointer name arrPtr does not represent a constant address, as the operation ++arrPtr shows. The increment operation increases the address stored in an array pointer by the size of one array—in this case, one row of the matrix, or ten times the number of bytes in an int element.
If you want to pass a multidimensional array to a function, you must declare the corresponding function parameter as a pointer to an array type. For a full description and an example of this use of pointers, see "Arrays as Function Arguments" in Chapter 8.
One more word of caution: if a is an array of ten int elements, then you cannot make the pointer from the previous example, arrPtr, point to the array a by this assignment:
arrPtr = a; // Error: mismatched pointer types.
The reason is that an array name, such as a, is implicitly converted into a pointer to the array's first element, not a pointer to the whole array. The pointer to int is not implicitly converted into a pointer to an array of int. The assignment in the example requires an explicit type conversion, specifying the target type int (*)[10] in the cast operator:
arrPtr = (int (*)[10])a; // OK
You can derive this notation for the array pointer type from the declaration of arrPtr by removing the identifier (see "Type Names" in Chapter 11). However, for more readable and more flexible code, it is a good idea to define a simpler name for the type using typedef:
typedef int ARRAY_t[10]; // A type name for "array of ten int elements".
ARRAY_t a, // An array of this type,
*arrPtr; // and a pointer to this array type.
arrPtr = (ARRAY_t *)a; // Let arrPtr point to a.
Pointers have a unique relationship with arrays: they can represent them completely. This is true with multidimensional arrays as well as flat ones. Using a single index with a two-dimensional array specifies a single row of values. That is, the value of that subscript is a flat array. This flat array can be stored in a pointer:
int stuff[5][5];
int *p = stuff[0];
The above code causes ‘p’ to point to the first value of the first row of ‘stuff’. The pointer ‘p’ can now be used as if it was a flat array:
p[0] = 7;
cout << p[0] << endl;
And pointer arithmetic can also be applied to it to move through the row:
p++;
*p = 8;
p++;
*p = 9;
This has been seen before. The concept of a pointer to a list of values is interesting, but what about a pointer to a pointer of a list of values? A pointer pointer is just that. It is basically the pointer version of a two dimensional array and can be declared by preceding the pointer name with an additional asterisk:
int **pp;
A pointer pointer is practically equivalent to a two-dimensional array and the two can associated with one another:
pp = stuff;
pp[0][0] = 10;
The pointer pointer can be used as if it was a two-dimensional array. It can also be used as a flat array if it is dereferenced:
(*pp)[0] = 10;
A pointer pointer can take the place of a multidimensional array in function parameters as well and it is easier to pass than an array. A pointer pointer is identical to a normal pointer: its value is a memory address. However, the value of a pointer pointer is the address of another pointer variable. The address contained by that pointer variable is the location of an actual value.
1.多维数组元素的地址
(1)概述
设有一个二维数组a,有3行4列,它的定义为:
int a[3][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};
则有下列表格(假设数组的首地址为2000):
表示形式 |
含 义 |
地 址 |
a |
二维数组名,指向一维数组a[0],即第0行首地址 |
2000 |
a[0],*(a+0),*a |
第0行第0列元素地址 |
2000 |
a+1,*a[1] |
第1行首地址 |
2008 |
a[1],*(a+1) |
第1行第0列元素a[1][0]的地址 |
2008 |
a[1]+2,*(a+1)+2,&a[1][2] |
第1行第2列元素a[1][2]的地址 |
2012 |
*(a[1]+2),*(*(a+1)+2),a[1][2] |
第1行第2列元素a[1][2]的值 |
7 |
(2)举例
输出二维数组有关的值。
程序参见:li10-11.c
2.指向多维数组元素的指针变量
(1)指向多维数组元素的指针变量
①举例
用指针变量输出二维数组元素的值。
程序参见:li10-12.c
②说明
计算a[i][j]在数组中的相对位置的计算公式为:
i*m+j (其中m为二维数组的列数)
(2)指向由m个元素组成的一维数组的指针变量
①举例
用指针变量输出二维数组任一行任一列元素的值。
程序参见:li10-13.c
②说明
程序中的“int (*p)[4]”表示p是一个指针变量,它指向包含4个整型元素的一维数组。
*(p+2)+3是a数组第2行第3列元素地址,这是指向列的指针;
*(*(p+2)+3)是a[2][3]的值。
3.用指向数组的指针作函数参数
(1)概述
一维数组名可以作为函数参数传递,多维数组名也可以作为函数参数传递。在用指针变量作形参以接受实参数组名传递来的地址时,有两种方法:
①用指向变量的指针变量;
②用指向一维数组的指针变量。
(2)举例
例1:有一个班,3个学生,各学4门课,计算总平均分数,并检索输出第n个学生的成绩单。
①算法分析
②程序参见:li10-14.c
例2:在例1的基础上,查找有一门以上课程不及格的学生,显示出他们的全部课程的成绩。
①算法分析
②程序参见:li10-15.c
(3)结论
通过指针变量存取数组元素速度快,且程序简明。用指针变量作形参,可以允许数组的行数不同,因此数组与指针常常是紧密联系的,使用熟练的话可以使程序质量提高,且编写程序方便灵活。