-
Notifications
You must be signed in to change notification settings - Fork 0
ex2
In contrast to some languages (e.g., Python) that allow the programmer to code with little concern for how memory is used, C programming affords relatively direct control over how memory is used and accessed. Therefore, if you are coming from a language such as Python, understanding memory usage in C may take some effort.
Memory--often random access memory (RAM) at the hardware level--enables you to store variables and functions. Whenever you declare a variable, the program requests memory for that variable. For example, on my machine the int is 32 bits, so writing int my_variable; will generate a request for 4 bytes (1 byte = 8 bits) of memory at run time.
We can think of memory being laid out as a very large array of ones and zeros, each of which is called a bit. Typically this array is divided into blocks of 8, each of which is called a byte. Associated with each byte is a numerical address. Here is a visualization of memory organization:
address |-byte-|
0 00000000
1 00000000
2 00000000
3 00000000
4 00000000
...
8729315 00000000
8729316 00000000
... 00000000
You won't usually see memory addresses written as decimals, like they are in the above example. Instead, you will see them written in hexadecimal. The prefix "0x" emphasizes that the values are base-16.
address |-byte-|
0x000000000000 00000000
0x000000000001 00000000
... 00000000
0x000000000009 00000000
0x00000000000a 00000000
0x00000000000b 00000000
0x00000000000c 00000000
0x00000000000d 00000000
0x00000000000e 00000000
0x00000000000f 00000000
0x000000000010 00000000
0x000000000011 00000000
...
0x7ffce8f78a2c 00000000
0x7ffce8f78a2d 00000000
0x7ffce8f78a2e 00000000
0x7ffce8f78a2f 00000000
0x7ffce8f78a30 00000000
...
Let's say I declare a char, which is typically 1 byte, and assign a value of 'a', which corresponds to a numerical value of 61: char my_character = 'a';. I can find the memory address of my_character using the & operator:
printf("%p\n", &my_character);
(Note that the %p format specifier is used for printing memory addresses.) When executed, the above code gave me the following output:
0x7ffce8f78a2c
If I were to look at the specified address, I would see something like the following:
address |-byte-|
...
0x7ffce8f78a2b 00110001
0x7ffce8f78a2c 00111101 <- the char I declared; note that 00111101 is 61 in binary.
0x7ffce8f78a2d 00001011 <- "junk" values; this memory is just some unknown part of my program.
0x7ffce8f78a2e 00000000
0x7ffce8f78a2f 00000000
...
Memory in C programs can be divided into two categories: the stack and the heap. Stack memory is allocated when you declare a variable, and it is automatically freed for reuse when the function in which the variable was declared returns. Heap memory is allocated when the programmer specifically asks for a block of raw memory, which can be done using the malloc (memory allocate) function, included in stdlib.h. Whereas stack memory is automatically freed, heap memory must be manually freed using the free function. The malloc function returns the address of the block of raw memory; this address should be stored in a pointer.
Pointers are types of variables that hold a memory address. When we write C code, we also include extra information that tells the computer how to interpret the memory address--for example, whether the memory holds int, float, or other values. The syntax to declare a pointer is to give the type of whatever is stored at the memory address followed by a * symbol. For example, the following code declares a pointer called my_pointer, with which we intend to store the memory address of an int:
int* my_pointer;Similarly, we could declare a pointer to other types:
float* my_float_pointer;
double* my_double_pointer;
char* my_character_pointer;
void* my_void_pointer;
int** my_pointer_to_a_pointer_to_an_int;Declaring a pointer as shown above does not allocate memory at the location to which the pointer points; the pointer current stores a "junk" value. We can use the "address of" operator to set the value of my_pointer to a valid memory address:
int* my_pointer;
int some_value = 4;
my_pointer = &some_value;Now, my_pointer stores the memory address of the variable some_value. To be clear, my_pointer now has a value such as 0x7ffda5f660dc. To obtain the value stored at that memory address, we need to dereference the pointer, which can be done using either of the following syntaxes:
*my_pointer;
my_pointer[0];Thus, writing printf("%d\n", my_pointer[0]); would result in 4 being printed to the terminal.
Rather than just pointing to a single value, a pointer can point to an entire block of memory. Suppose we do the following:
n_elements = 10;
int* my_array = (int*)malloc(n_elements*sizeof(int));The above code creates my_array, which is a pointer to an int. The value of my_array is set to the expression on the right hand size of the = sign, which we can break down piece-by-piece. First, the sizeof function calculates the size (in bytes) of the specified type. On my machine, the int is 32-bits, so sizeof(int) returns 4 (since there are 8 bits per byte). Multiplying sizeof(int) by n_elements gives the size of the block of memory needed to store n_elements values of type int: in my case, 40 bytes. We pass this size to the malloc command, which allocates the desired amount of memory and returns the memory address of the start of the block of memory. We write (int*) to cast the memory address such that its type is "pointer to an int", and we assign that value to the pointer my_array.
Now that we have allocated a block of memory for 10 integers and stored the memory address of the 0th integer in my_array, we can assign values to the memory address:
my_array[0] = 1;
my_array[1] = 4;
my_array[2] = 9;Now, the first 4 bytes of the block of memory hold the value 1, the second four bytes of the block of memory hold the value 4, and the third four bytes of the block of memory hold the value 9. A more elegant way to look at this, is that the block of memory is a array of integers, and elements 0, 1, and 2 have are 1, 4, and 9, respectively. The rest of the elements remain uninitialized and hold junk values.
Using pointers, we could (for example) do vector addition:
#include <stdio.h>
#include <stdlib.h>
// Add vectors. c[i] = a[i] + b[i]
void
vector_add(int* a, int* b, int* c, int length)
{
int i;
for (i=0;i<length;i++) {
c[i] = a[i] + b[i];
}
}
int
main (void)
{
// The number of elements in each vector
int length = 10;
// Allocate memory for vectors a, b, and c
int* a = (int*)malloc(length*sizeof(int));
// Check that memory was successfully allocated
if (a == NULL) {
fprintf(stderr, "Memory error. Unable to allocate memory for vector 'a'.\n");
exit(EXIT_FAILURE);
}
int* b = (int*)malloc(length*sizeof(int));
if (b == NULL) {
fprintf(stderr, "Memory error. Unable to allocate memory for vector 'b'.\n");
exit(EXIT_FAILURE);
}
int* c = (int*)malloc(length*sizeof(int));
if (c == NULL) {
fprintf(stderr, "Memory error. Unable to allocate memory for vector 'c'.\n");
exit(EXIT_FAILURE);
}
// Initialize a, b, and c. We set a[i] = b[i] = i, and we set c[i] = 0
int i;
for (i=0;i<length;i++) {
a[i] = i;
b[i] = i;
c[i] = 0;
}
// Test vector_add
vector_add(a, b, c, length);
// Print out c to check the result
for (i=0;i<length;i++) {
printf("Element %d of c is: %d\n", i, c[i]);
}
// Finally, free the memory for a, b, and c.
free(a);
free(b);
free(c);
}Take note of the following:
- The function
vector_addreturns typevoid. Since the function stores its output in one of its arguments (c), it does not need to return a value. - When we allocate memory, it is important to check that memory allocation was successful. If memory was not available,
mallocwill return aNULLpointer, for which we explicitly check. - If memory allocation fails, we can print to
stderrrather thanstdoutby using thefprintffunction together withstderras an argument. - If memory allocation fails, we force the program to exit using
exit(EXIT_FAILURE), which tells the system that the program did not complete correctly. - After we are done with
a,b, andc, we free the associated memory. Failure to free memory may cause a memory leak, in which a program continually consumes more memory. This is a bad thing!