1 / 42

# Lecture 8: Arrays - PowerPoint PPT Presentation

Lecture 8: Arrays. Little boxes, all the same. Need for Arrays. So far, each variable has been a scalar , containing exactly one value at any given time If your program needs to simultaneously store lots of data, it would need to have (and name) lots of scalar variables

I am the owner, or an agent authorized to act on behalf of the owner, of the copyrighted work described.

Lecture 8: Arrays

Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author.While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server.

- - - - - - - - - - - - - - - - - - - - - - - - - - E N D - - - - - - - - - - - - - - - - - - - - - - - - - -

## Lecture 8: Arrays

Little boxes, all the same

### Need for Arrays

• So far, each variable has been a scalar, containing exactly one value at any given time

• If your program needs to simultaneously store lots of data, it would need to have (and name) lots of scalar variables

• Instead, arrays are a mechanism that allows us to store a large number of data elements under a single name

• An array is a linear row of elements, each of which acts like an ordinary variable of the array element type

• Each element has an integer index giving its position in the array, and is accessed with this index

• 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("%d\n", a[30]); /* 900 */

return 0;

}

### Arrays in Memory

• In C, all arrays are homogeneous so that each element is of the same type given in the array declaration

• 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

• Contrast this to linked lists, where to get to the 100:th element you have to step through the elements 1, ..., 99

• 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

• 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, sumSq = 0, avg, avgSq;

int i;

for(i = 0; i < n; i++) {

sum += a[i];

sumSq += a[i] * a[i];

}

avg = sum / n;

avgSq = sumSq / n;

return avgSq - (avg * avg);

}

### Arrays as Function Parameters

• 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

• Indexing is actually a pointer operation, even though we think of it as an "array" operation

• 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

• 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

• 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

• How could it, when arrays don't even know their own size?

• Other languages have bounds checks that you can't turn off even when you are 110% sure your indexing remains safely within bounds

• Good example of the C design philosophy in which programmer never has to "pay" for anything that he doesn't explicitly use

• Many things (and more importantly, lack of things) in C are the way they are for this very reason

• At runtime, only raw data operations exists, with nothing of C language or its types needed at the runtime

### Arrays and Assignment

• In C, the size of statically declared arrays must be known and declared at the compile time

• The moment you declare an array, you can initialize it by listing the elements: int[] a = {1,2,3,4};

• 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)

• For this reason, no function can return an array as its result, since the caller could not assign the result anywhere

• Similarly, printf and scanf don't have placeholders to output or read an entire array at once

• 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

• Arrays are not pointers: int[] a is not same as int* a (for example, you can reassign the latter but not the former)

• After the array has been declared, using it in any expression treats the name of the array as a pointer to its first element

• A pointer to an entire array of integers has the exact same typeint* as a pointer to a scalar integer

• Array indexing is in reality a pointer operation and can be used with any pointer, not just those derived from arrays

• In fact, as the C compiler sees it, a[i] is merely syntactic sugar for the pointer arithmetic formula *(a + i)

• 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

• 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

• First, the operator sizeof that tells you how many bytes it takes to store something in your computer

• C standard does not dictate how many bytes of memory an int or a pointer is stored in, but this is machine-dependent

• Second, the initialization of an array

• 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

• (Puzzle: can you create a situation where &p == p ?)

### Pointer Arithmetic

• Pointer arithmetic means the ability to treat the memory address stored in a pointer as the integer that it really is

• The source of both low-level power and danger in C, since this allows you to do anything you want in the raw memory

• For a pointer p and an integer i, meaningful pointer arithmetic operations are p++, p += i, p--, p -= i

• Pointer arithmetic is convenient in that its increments are automatically multiplied by the element size in bytes

• 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

• Note again that p[3] is syntactic sugar for *(p + 3)

### More on Pointer Arithmetic

• If p and q are two pointers, pointer arithmetic allows us to compare them for equality and order

• 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

• Again, pointer arithmetic in arrays automatically operates in multiples of individual element size

• For this reason, pointer arithmetic is not possible for void*, since nothing is known about the element size

• 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

• 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

• This subarray can, for example, be passed as argument to any function that expects to be given an array

• 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)

• Just call foo(p + start, end - start + 1)

• p + start is a pointer to the first element of this subarray, and this subarray has exactly end - start + 1 elements

### Strings

• We finally have the mechanisms to talk about, use and modify strings, pieces of text

• In C, every string is an array of characters, but not every array of characters is a string.

• To be a string, the array must be null-terminated, meaning that it ends with the null character '\0'

• Note that '\0' is a different thing from a null pointer, and does it equal the zero digit character '0'

• To store the string "Hello", you need an array of at least six characters, not five

• The position of the null character in the array determines the length of the string, which may be less than the size of the array, allowing unused slack in the end

### String I/O

• The function printf offers the placeholder %s to output a null-terminated string

• Like all the other standard library functions for strings, this will keep going until it finds the null character to stop at

• If the null character is missing from your string, you will eventually get some sort of runtime memory error

• In scanf, the placeholder %s reads a string until the first whitespace character (or EOF)

• Alternatively, the function gets reads a string until the next newline character (or EOF)

• It is again the caller's duty to ensure that the array passed to scanf or gets to write in is big enough

### string.h

• The standard library string.h contains many useful methods for many common string operations

• strlen(s) returns the length of the string s, not including the null character in the end

• strcpy(tgt, src) copies the source string to the target, including the null character in the end

• strcat(tgt, src) is like strcpy, but appends source to the end of target instead of copying over it

• strchr(s, c) returns a pointer to the first occurrence of character c inside string s, or NULL if there aren't any

• Plus a bevy of others, easy enough to Google

• Again, caller bears the full responsibility for the parameter arrays having enough room for elements copied in

### Example: my_strcpy

/* Write a version as a finger exercise. */

char* my_strcpy(char* tgt, const char* src) {

char* old_tgt = tgt;

while(*tgt++ = *src++) { /* Clas-sick! */ }

return old_tgt; /* As specified */

}

/* Adding some clarifying parentheses and the implicit test of the condition being nonzero makes the loop perhaps more readable: while( (*(tgt++) = *(src++)) != 0) { }

*/

### The const qualifier

• A variable qualified to be const must be initialized at declaration, but after that, it can't be assigned to

• Used to define named constants, and to pretend that some data can't change inside a function

• (You can always change any data by creating a suitably typed pointer to it and changing it through that pointer)

• With pointers, we need to distinguish if const-ness refers to the pointer or to the data that it points to

• Four combinations with const1 char* p const2 based on which of the two places you put the const keyword

• const1 makes *p constant, const2 makes p constant

### strcmp

• One more function in string.h deserves closer study, the lexicographic order comparison strcmp

• For example, aardvark < zyzzyx, and 11111111 < 9

• Compared to equality comparison, order comparison is trickier since it has three possible results: <, =, >

• We can't use int as return value in truth value sense

• Instead, use int as return value so that its sign gives the answer: any negative for <, 0 for =, any positive > 0

• Absolute value does not matter

• To test if two strings are equal, use strcmp(s1, s2) == 0

### Dynamically Allocated Arrays

• In many problems, we cannot possibly know at compile time how big our arrays need to be at runtime

• If the program has to read in and simultaneously store all data in a user-supplied data file, we can't possibly know the size of this data file beforehand

• The standard library function malloc dynamically allocates an array from part of memory called the heap

• Unlike the stack where local variables are allocated in stack fashion, with dynamic memory allocations the order of allocations and deallocations is completely arbitrary

• Lifetimes of dynamically allocated arrays are independent of whatever function calls and returns the program does

### Using malloc

• As far as the C compiler's type system is concerned, there are no arrays anywhere in malloc

• When called, malloc reserves a sufficiently large block of continuous memory for your program's exclusive use, and returns a void* pointing to the first byte of this block

• This memory block contains whatever are in those bytes

• You can think of this block of raw memory as an "array", if that makes you feel more comfortable

• Remember that indexing and other array operations are really only pointer arithmetic in disguise anyway

• Since there are no arrays, there also aren't any stupid restrictions of arrays, such as not being allowed to assign them, or return them as results from functions

### calloc

• The memory allocated by malloc is not initialized, but contains whatever data used to be there left by the previous user of that memory

• Often we'd like the memory to be filled with zeros, and calloc works like malloc but also does exactly this

• Takes two parameters calloc(elements, size) to be able to use hardware filling several bytes at the time

• Note that if you treat the block as array of int or double, these elements are proper zeros (even as double)

• However, if you treat the block as an array of pointers, you can't portably assume that NULL is encoded as the integer 0, in spite of 0 being treated as NULL in the C language!

### Releasing Dynamic Memory

• The memory that you allocate with malloc is for your exclusive use until you explicitly release it back to the system using the standard library function free

• However, if you lose track of some dynamically allocated block, it will not get magically released just by itself

• At termination of process, all heap memory given to that process is released automatically

• Such memory leaks can be a problem, if your program is supposed to keep going for a long time, even years

• Many embedded systems are this way, and they usually don't come with that much RAM in the first place

### Dynamic Memory Example

void dynamic_memory_demo(int n) {

int* p; int i;

/* Give malloc the exact size of block in bytes. */

p = malloc(n * sizeof(int));

/* Now you can use this block any way you wish. */

for(i = 0; i < n; i++) { p[i] = i * i; }

/* And whatever else we might do. */

/* When done, release the block back to the system. */

free(p); /* No need to give the size, only the address */

}

### Between Scylla and Charybdis

• Your program is fully responsible for releasing each dynamically allocated block once it no longer uses that block, since C has no garbage collection

• Forget to release memory, and you get a memory leak

• As long as you don't run out of memory, memory leaks do not affect the correctness of the program, only its efficiency

• But release a block too early even though you still continue to use it, and you get a dangling reference

• Dangling references are far more dangerous in that they break the correctness of the program by corrupting data

• Even worse, the manifestation of these errors is highly system-dependent and time-delayed, so these bugs can be extremely difficult to hunt down and correct

### Resizing Dynamic Memory

• Even though size of a dynamic memory block is dynamically determined at malloc, it can't be resized later

• If the block turns out to be too small, allocate a new bigger block, copy the old data to new block, free the old block and continue using the new one

• Common technique in many data structures

• This common operation is offered in library function realloc

• Typical use: p = realloc(p, new_size);

• If there is slack memory available after the block, realloc might be able to use that, and not need to allocate and copy memory at all, saving execution time

• Copying using hardware instructions may also be faster than copying one element at the time

### Block Ownership

• If the block is to be allocated and released in the same function, forgetting to free it is easy enough to find and fix

• It gets difficult when the allocated block continues to be used from multiple directions for an unknown time until we can finally release it in some completely different function

• One common tool to keep track of the duty to release a block is to attach the notion of ownership to each block

• This notion is only a conceptual thinking tool; there is nothing actual in the memory about who "owns" what

• Write all your code so that if a function "owns" a block, it will either release this block before its execution terminates, or pass the ownership to the caller

### A Common Superstition

• Even some textbooks say that to avoid dangling references, you must follow each free(p); with p = NULL;

• Making a local variable NULL doesn't prevent dangling references, other than those caused by carelessness

• Dangling references occur when the same block is pointed to and used from many different directions

• The problem is to know when the last user of the block goes away (ceases to exist, reassigns the pointer etc.)

• In reference counting, you store somewhere in the block how many places it is used from, and try to remember to increment and decrement this count each time

• Can release the block when refcount becomes zero

### valgrind

• Pointer errors are problematic in that the time between something goes wrong and something wrong showing to the outside world can be long

• Manifestation of errors is often system-dependent

• Many commercial tools exist to help programmers debug C programs and chase down pointer errors

• The great free tool valgrind (sorry, Unix only) can be used to track any program whatsoever (even valgrind itself)

• valgrind keeps track of all pointer operations, and the very moment your program does anything hinky, it stops the program and reports what kind of violation it was

• At normal termination, outputs a report of memory use, remaining memory leaks etc.

### Example: Linear Search

/* The raw linear search to determine if the n-element array a contains the element x. Returns either the index of first occurrence, or -1, if the element x doesn't exist in array. */

int linear_search(int a[], int n, int x) {

int i = 0;

while(i < n && a[i] != x) { i++; }

return (i<n)? i: -1;

}

### Sentinel Search

/* To avoid having to check that we are still inside the array, make sure that we find the element x in the last location, if not earlier. Of course, we need to remember to restore the last element before returning.*/

int linear_search_sentinel(int a[], int n, int x) {

int i = 0, last = a[n-1];

a[n-1] = x;

while(a[i] != x) { i++; }

a[n-1] = last;

return (i<n-1 || last == x)? i: -1;

}

### Loop Unrolling

/* Check two elements during each iteration of the loop, instead of just one. Now the number of tests that we are still inside the array is halved, but this version requires that array size n is even. Can be applied with sentinel search. */

int linear_search(int a[], int n, int x) {

int i;

for(i = 0; i < n; i += 2) {

if(a[i] == x) return i;

if(a[i+1] == x) return i+1;

}

return -1;

}

### Cautionary Tale of Poor Shlemiel

• You need to get your long fence repainted, and busy person that you are, hire poor Shlemiel for the job

• The first morning, you give him a bucket of paint and a brush as tools to work with, and tell him to get to it

• The first day Shlemiel paints a total of 20 m of fence

• The second day is the same, but Shlemiel only paints 8 m of fence, so you tell him to shape up

• But the third day, Shlemiel only paints 3 m of fence

• Frustrated, the fourth day morning you wait behind the corner to catch him goofing off

• What is slowing poor Shlemiel down, even though he is a honest man and works as hard as he humanly can?

### Poor Shlemiel Writes Code

char* my_strchr(const char* s, char c) {

int i;

for(i = 0; i < strlen(s); i++) {

if(s[i] == c) { return &(s[i]); }

}

return NULL;

}

/* Moral of the story: Ask once, then remember. */

"A test as near foolproof as one could get of whether you understand something as well as you think is to express it as a computer program and then see if the program does what it is supposed to. Computers are not sycophants and won't make enthusiastic noises to ensure their promotion or camouflage what they don't know. What you get is what you said."

James P. Hogan