Lecture 8 Arrays Little boxes all the same
Lecture 8: Arrays Little boxes, all the same
Need for Arrays a. So far, each variable has been a scalar, containing exactly one value at any given time b. If your program needs to simultaneously store lots of data, it would need to have (and name) lots of scalar variables c. Instead, arrays are a mechanism that allows us to store a large number of data elements under a single name d. An array is a linear row of elements, each of which acts like an ordinary variable of the array element type e. Each element has an integer index giving its position in the array, and is accessed with this index f. Indices start from 0, so the last index is always one less than the size of the array
Array Example #include <stdio. h> int main() { int i; int a[100]; /* An array with 100 integer elements. */ /* Fill the array. */ for(i = 0; i < 100; i++) { a[i] = i * i; } /* Access the element in position 30. */ printf("%dn", a[30]); /* 900 */ return 0; }
Arrays in Memory a. In C, all arrays are homogeneous so that each element is of the same type given in the array declaration b. This allows indexing to be a fast random access operation so that accessing the element in any location you need is equally fast, independent of the location index c. Contrast this to linked lists, where to get to the 100: th element you have to step through the elements 1, . . . , 99 d. In practice, most functions access array elements in sequential order, since this is the simplest way to do things when every element needs to be accessed once, but the order in which this done doesn't matter e. Only the array elements are stored in memory, but these bytes have no information about the array size or type
Example: Sum of Elements /* The first example function that gets an array as parameter. Since the array does not know its size, these functions must always receive this size as an additional int parameter. */ int sum(int a[], int n) { int sum = 0, i; for(i = 0; i < n; i++) { /* The indices of n-element array */ sum += a[i]; } return sum; }
Example: Minimum Element double minimum(double[] a, int n) { double m = a[0]; int i; for(i = 1; i < n; i++) { if(a[i] < m) { m = a[i]; } } return m; }
Example: Statistical Variance double variance(double a[], int n) { double sum = 0, sum. Sq = 0, avg. Sq; int i; for(i = 0; i < n; i++) { sum += a[i]; sum. Sq += a[i] * a[i]; } avg = sum / n; avg. Sq = sum. Sq / n; return avg. Sq - (avg * avg); }
Arrays as Function Parameters a. When some function receives an array as a parameter, it really receives a pointer to the first element of this array, even though the array syntax pretends that this parameter is an array, not a pointer to one element b. Indexing is actually a pointer operation, even though we think of it as an "array" operation c. These pointers are passed by value just like everything else in C, so the function and its caller share the same array through two different pointers d. If the function modifies the actual array that was given to it as parameter, this change persists to the caller after the function execution has terminated
Example: Reversing An Array /* Reverses the array elements "in place", meaning that the contents of the original array are modified, instead of writing the result to a second array of the same size. */ void reverse(int a[], int n) { int i = 0, j = n - 1, tmp; while(i < j) { tmp = a[i]; a[i] = a[j]; a[j] = tmp; i++; j--; } }
Example: Copying Arrays /* Copy the contents of array src to array tgt. It is caller's responsibility to ensure that the target array has sufficient size to accommodate the n elements of source array. */ void array_copy(int tgt[], int src[], int n) { int i; for(i = 0; i < n; i++) { tgt[i] = src[i]; } }
Interlude: C Philosophy a. Unlike many other languages, C does not enforce bounds checks on array indices, so that indexing an array out of bounds would be a runtime error b. How could it, when arrays don't even know their own size? c. Other languages have bounds checks that you can't turn off even when you are 110% sure your indexing remains safely within bounds d. Good example of the C design philosophy in which programmer never has to "pay" for anything that he doesn't explicitly use e. Many things (and more importantly, lack of things) in C are the way they are for this very reason f. At runtime, only raw data operations exists, with nothing of C language or its types needed at the runtime
Arrays and Assignment a. In C, the size of statically declared arrays must be known and declared at the compile time b. The moment you declare an array, you can initialize it by listing the elements: int[] a = {1, 2, 3, 4}; c. After that, the array itself cannot be assigned to; in fact, any attempt to assign to the entire array is a syntax error (it is OK to assign to individual elements) d. For this reason, no function can return an array as its result, since the caller could not assign the result anywhere e. Similarly, printf and scanf don't have placeholders to output or read an entire array at once f. You can, of course, write functions to do all these things, or use functions defined in the standard library
Example: Filling an Array void input_array(int a[], int n) { int i; for(i = 0; i < n; i++) { printf("Enter value for element %d: ", i); scanf("%d", &(a[i])); } } /* The address-of operator & can give you the address of an individual element of the array. If you apply the operator & to the entire array, you get the memory location of the first element of the array. */
Arrays and Pointers a. Arrays are not pointers: int[] a is not same as int* a (for example, you can reassign the latter but not the former) b. After the array has been declared, using it in any expression treats the name of the array as a pointer to its first element c. A pointer to an entire array of integers has the exact same type int* as a pointer to a scalar integer d. Array indexing is in reality a pointer operation and can be used with any pointer, not just those derived from arrays e. In fact, as the C compiler sees it, a[i] is merely syntactic sugar for the pointer arithmetic formula *(a + i) f. You can try this by writing i[a] instead of a[i], to verify that both compile and work exactly the same
Three Exceptions That Prove the Rule a. There are three situations where using an array in an expression treats it as an array, instead of a pointer to the first element of that array b. First, the operator sizeof that tells you how many bytes it takes to store something in your computer c. C standard does not dictate how many bytes of memory an int or a pointer is stored in, but this is machine-dependent d. Second, the initialization of an array e. Third, the address-of operator applied to an array doesn't really do anything, since &a == a by definition, whereas in general for a pointer p, it would be that &p != p f. (Puzzle: can you create a situation where &p == p ? )
Pointer Arithmetic a. Pointer arithmetic means the ability to treat the memory address stored in a pointer as the integer that it really is b. The source of both low-level power and danger in C, since this allows you to do anything you want in the raw memory c. For a pointer p and an integer i, meaningful pointer arithmetic operations are p++, p += i, p--, p -= i d. Pointer arithmetic is convenient in that its increments are automatically multiplied by the element size in bytes e. If p is a pointer to the first element of the array, p + 3 gives you a pointer to the fourth element of that same array, regardless of the array element type f. Note again that p[3] is syntactic sugar for *(p + 3)
More on Pointer Arithmetic a. If p and q are two pointers, pointer arithmetic allows us to compare them for equality and order b. If p > q and both point inside the same array, p - q is a meaningful operation and gives you the number of elements from p to q, the latter bound exclusive c. Again, pointer arithmetic in arrays automatically operates in multiples of individual element size d. For this reason, pointer arithmetic is not possible for void*, since nothing is known about the element size e. Even though adding an integer to a pointer is useful pointer arithmetic, adding two pointers would not make any sense
Example: Array Sum With Pointers int sum(int* p, int n) { int sum = 0; while(n > 0) { sum += *p; p++; /* Traverse the array by pointer, not indexing. */ n--; } return sum; } /* Knowing how ++ and -- work, we could shorten this code by a couple of lines by combining some operations, but it would become less clear, and not faster at all. */
Example: Array Copy With Pointers void array_copy(int* tgt, int* src, int n) { int i; for(i = 0; i < n; i++) { *tgt = *src; tgt++; src++; } } /* The three statements inside the loop could be combined into a less clear single statement *(tgt++) = *(src++); */
Subarray Slicing a. The pointer to an array really being a pointer to its first element has the advantage of allowing easy slicing any subarray to be treated as an array b. This subarray can, for example, be passed as argument to any function that expects to be given an array c. Assume p is a pointer to the first element of the array and n its number of elements, and we want to pass the subarray from start to end (inclusive) to function foo(int* a, int n) d. Just call foo(p + start, end - start + 1) e. p + start is a pointer to the first element of this subarray, and this subarray has exactly end - start + 1 elements
Strings a. We finally have the mechanisms to talk about, use and modify strings, pieces of text b. In C, every string is an array of characters, but not every array of characters is a string. c. To be a string, the array must be null-terminated, meaning that it ends with the null character '