LAB 7 - Pointers - Dynamic Memory Allocation
LAB 7 - Pointers - Dynamic Memory Allocation
name[20];
id[5];
state[10];
balance;
amt_due;
void main(void)
{
customer fla_cust = {"John Doe","123a6","NY", 1000.00,150.00};
.
.
}
The structure variable fla_cust initializes name, id, and state to character strings, while the balance
and amt_due members are initialized to floating-point values.
The members of a structure are referred to by using the dot operator, which is simply the period
character (.). The dot operator is used between the variable name and the member name, as in the following
example:
fla_cust.balance = 2334.66;
fla_cust.amt_due = 100.00;
In this preceding example, the structure's character string name, id, and state members are
initialized in the declaration. Values are assigned the numeric members balance and amt_due in separate
statements that use the dot operator.
You can also use the dot operator to refer to the contents of the structure elements, as in the
following printf() statements:
printf("\nCustomer name : $%s", fla_cust.name);
printf("\nID
: $%s", fla_cust.id);
The string input function gets() is a convenient way to input values to structure members. You can
use gets() as follows to input a character string into a structure member (that is a char array):
gets(structure_variable_name.member_name);
Be sure to provide enough space in the structure definition for the maximum size of an input
string.
You can also use gets() to input numeric values into a numeric structure member. But, you must
translate the input using one of the text-to-numeric conversion functions. You would do so as shown here
(assumes the structure member is of type int or float):
char tmp[size];
structure_variable_name.member_name = atoi(gets(tmp));
structure_variable_name.member_name = atof(gets(tmp));
In the preceding example, a character array tmp is created to hold the input string from the gets()
function. The string is converted to the correct data type by using the string-conversion functions (either
atoi() or atof()). The resulting numeric value is then assigned to the structure member.
Arrays of Structures
Creating an array of structures is similar to creating any other array type. Two steps are involved:
1) Define the structure.
2) Declare one or more array variables of the structure type.
When you're using a pointer to a structure, the arrow operator takes the place of the dot operator.
The arrow operator consists of the hyphen (-) character, followed by the greater-than (>) character, to form
the following sequence: -> . Following the previous example, the following statements:
printf("\nCustomer name : %s ",ptr->name);
printf("\nAmount due
: %.2f",ptr->amt_due);
will print the string in the name member and the floating-point value in the amt_due member of the fla_cust
structure.
Nested Structures
To create a structure that contains another structure as one of its members, you must:
1) Define the structure that is to be nested.
2) Declare a structure variable of the type to be nested.
3) Define the structure that will hold the nested structure.
For example, the following code illustrates the creation of a nested structure:
struct address{
char street[40];
char city[20];
char state[4];
};
struct customer{
char name[20];
struct address addr;
float balance;
float amt_due;
};
In this preceding example, a new structure, address, is defined. The variable addr of the structure
type address is declared within the customer structure. In the customer-structure definition, the variable
addr is one of the structure members. The definition uses the keyword struct, followed by the structure type
(address) and a structure variable name (addr).
Using the dot operator between each structure-member notation, as in the following example can
access members of a nested structure:
gets(fla_cust.addr.street);
In this statement, input is obtained from the user and stored in the street member of the structure
nested in the fla_cust variable. Conversely, to print the same member of the nested structure, the notation of
the printf() statement would be:
printf("\nStreet : %s", fla_cust.addr.street);
The ANSI C standard specifies that nested structures can be up to 15 levels deep. Check with your
compiler's documentation to determine how many levels of nesting you can use.
program and each of the other functions, it can figure out what memory will be needed when the program
runs.
When the program is loaded, it can request the needed memory from the operating system before the
program actually begins to run. The operating systems reserve the needed memory locations by stacking
one variable on top of another in memory, in a tight, neat block. Because of the way this process works, this
part of memory is known as the stack. Memory reserved within the stack cannot be freed up until the
program quits running.
Some programs need to use large chunks of memory to hold data, but they only need those chunks for
a short period of time. Rather than tie up all that memory the entire time the program is running, such
programs can temporarily allocate storage locations from another portion of memory, known as the heap.
When the program is done using a particular chunk of heap memory, it simply tells the operating system
that it is done, and the system returns that memory to the heap, where it can be freely doled out to other
needy programs. For best utilization of memory, clearly having a heap is a good idea.
As the name implies, memory within the heap is not nearly as ordered as that within the stack. With
various programs allocating and deallocating memory from the heap, it quickly becomes a mess. With little
chunks of memory in use in various locations throughout the heap, the heap can quickly become filled with
holes, like Swiss cheese. This makes it difficult to allocate large blocks of memory from the heap when
they are needed. To solve this problem, operating systems periodically perform the task of "compacting the
heap," in which allocated memory is shuffled around to free up large blocks as much as possible.
returned the C standard states that this pointer can be converted to any type. The size_t argument type is
defined in stdlib.h and is an unsigned type.
char *cp;
cp = malloc(100);
The above code attempts to get 100 bytes and assigns the start address to cp.
If we want to allocate 100 ints, how many bytes is that? If we know how big ints are on our machine
(i.e. depending on whether we're using a 16- or 32-bit machine) we could try to compute it ourselves, but
it's much safer and more portable to let C compute it for us. The sizeof operator, which computes the size,
in bytes, of a variable or type. It's just what we need when calling malloc. To allocate space for 100 ints, we
could call
int *ip = malloc(100 * sizeof(int));
The use of the sizeof operator tends to look like a function call, but it's really an operator, and it does its
work at compile time.
Since we can use array indexing syntax on pointers, we can treat a pointer variable after a call to
malloc almost exactly as if it were an array. In particular, after the above call to malloc initializes ip to
point at storage for 100 ints, we can access ip[0], ip[1], ... up to ip[99]. This way, we can get the effect of
an array even if we don't know until run time how big the ``array'' should be.
The examples so far have all had a significant omission: they have not checked malloc's return value.
Obviously, no real computer has an infinite amount of memory available, so there is no guarantee that
malloc will be able to give us as much memory as we ask for. If we call malloc(100000000), or if we call
malloc(10) 10,000,000 times, we're probably going to run out of memory.
When malloc is unable to allocate the requested memory, it returns a null pointer. A null pointer,
remember, points definitively nowhere. It's a ``not a pointer'' marker; it's not a pointer you can use.
Therefore, whenever you call malloc, it's vital to check the returned pointer before using it! If you call
malloc, and it returns a null pointer, and you go off and use that null pointer as if it pointed somewhere,
your program probably won't last long. Instead, a program should immediately check for a null pointer, and
if it receives one, it should at the very least print an error message and exit, or perhaps figure out some way
of proceeding without the memory it asked for. But it cannot go on to use the null pointer it got back from
malloc in any way, because that null pointer by definition points nowhere. (``It cannot use a null pointer in
any way'' means that the program cannot use the * or [] operators on such a pointer value, or pass it to any
function that expects a valid pointer.)
A call to malloc, with an error check, typically looks something like this:
int *ip = malloc(100 * sizeof(int));
if(ip == NULL) {
printf("out of memory\n");
exit or return
}
After printing the error message, this code should return to its caller, or exit from the program entirely;
it cannot proceed with the code that would have used ip.
Of course, in our examples so far, we've still limited ourselves to ``fixed size'' regions of memory,
because we've been calling malloc with fixed arguments like 10 or 100. However, since the sizes are now
values which can in principle be determined at run-time, we've at least moved beyond having to recompile
the program (with a bigger array) to accommodate longer lines, and with a little more work, we could
arrange that the ``arrays'' automatically grew to be as large as required.
Some C compilers may require to cast the type of conversion. The (int *) means coercion to an integer
pointer. Coercion to the correct pointer type is very important to ensure pointer arithmetic is performed
correctly. It is advisable to use it as a means of ensuring that the code is totally correct.
It is good practice to use sizeof() even if you know the actual size you want -- it makes for device
independent (portable) code.
sizeof can be used to find the size of any data type, variable or structure. Simply supply one of these as
an argument to the function.
int i;
struct COORD {float x,y,z};
typedef struct COORD PT;
sizeof(int);
sizeof(i);
sizeof(struct COORD);
sizeof(PT);
All above calls are acceptable.
Suppose there is needed to be read a line of input into a custom-size array. For his situation using
malloc is advisable.
#include <stdlib.h>
char *line;
int linelen = 100;
line = malloc(linelen);
/* incomplete -- malloc's return value not checked */
getline(line, linelen);
malloc is declared in <stdlib.h>, so we #include that header in any program that calls malloc. A ``byte''
in C is, by definition, an amount of storage suitable for storing one character, so the above invocation of
malloc gives us exactly as many chars as we ask for. We could illustrate the resulting pointer like this:
The 100 bytes of memory (not all of which are shown) pointed to by line are those allocated by malloc.
(They are brand-new memory, conceptually a bit different from the memory which the compiler arranges to
have allocated automatically for our conventional variables. The 100 boxes in the figure don't have a name
next to them, because they're not storage for a variable we've declared.)
As another example, we might have occasion to allocate a piece of memory, and to copy a string into it
with strcpy:
char *p = malloc(15);
/* incomplete -- malloc's return value not checked */
strcpy(p, "Hello, world!");
When copying strings, remember that all strings have a terminating \0 character. If you use strlen to
count the characters in a string for you, that count will not include the trailing \0, so you must add one
before calling malloc:
There are two additional memory allocation functions, calloc() and realloc(). Their prototypes are
given below:
void *calloc(size_t num_elements, size_t element_size};
void *realloc( void *ptr, size_t new_size);
malloc does not initialize memory (to zero) in any way. If you wish to initialize memory then use
calloc. calloc there is slightly more computationally expensive but, occasionally, more convenient than
malloc. Also note the different syntax between calloc and malloc in that calloc takes the number of desired
elements, num_elements, and element_size, element_size, as two individual arguments.
Thus to assign 100 integer elements that are all initially zero you would do:
int *ip;
ip = (int *) calloc(100, sizeof(int));
realloc is a function which attempts to change the size of a previous allocated block of memory. The
new size can be larger or smaller. If the block is made larger then the old contents remain unchanged and
memory is added to the end of the block. If the size is made smaller then the remaining contents are
unchanged.
If the original block size cannot be resized then realloc will attempt to assign a new block of memory
and will copy the old block contents. Note a new pointer (of different value) will consequently be returned.
You must use this new value. If new memory cannot be reallocated then realloc returns NULL.
Thus to change the size of memory allocated to the *ip pointer above to an array block of 50 integers
instead of 100, simply do:
it's now ``available,'' and a later call to malloc might give that memory to some other part of your program.
If the variable p is a global variable or will otherwise stick around for a while, one good way to record the
fact that it's not to be used any more would be to set it to a null pointer:
free(p);
p = NULL;
Now we don't even have the pointer to the freed memory any more, and (as long as we check to see
that p is non-NULL before using it), we won't misuse any memory via the pointer p.
When thinking about malloc, free, and dynamically-allocated memory in general, remember again the
distinction between a pointer and what it points to. If you call malloc to allocate some memory, and store
the pointer which malloc gives you in a local pointer variable, what happens when the function containing
the local pointer variable returns? If the local pointer variable has automatic duration (which is the default,
unless the variable is declared static), it will disappear when the function returns. But for the pointer
variable to disappear says nothing about the memory pointed to! That memory still exists and, as far as
malloc and free are concerned, is still allocated. The only thing that has disappeared is the pointer variable
you had which pointed at the allocated memory.
The hard thing about pointers is not so much manipulating them as ensuring that the memory they
point to is valid. If you inadvertently access or modify the memory it points to, you can damage other parts
of your program, or (in some cases) other programs or the operating system itself!
When we use pointers to simple variables there's not much that can go wrong. When we use pointers
into arrays and begin moving the pointers around, we have to be more careful, to ensure that the rolling
pointers always stay within the bounds of the array(s). When we begin passing pointers to functions, and
especially when we begin returning them from functions (as in the strstr function) we have to be more
careful still, because the code using the pointer may be far removed from the code which owns or allocated
the memory.
One particular problem concerns functions that return pointers. Where is the memory to which the
returned pointer points? Is it still around by the time the function returns? The strstr function returns either
a null pointer (which points definitively nowhere, and which the caller presumably checks for) or it returns
a pointer which points into the input string, which the caller supplied, which is pretty safe. One thing a
function must not do, however, is return a pointer to one of its own, local, automatic-duration arrays.
Remember that automatic-duration variables (which includes all non-static local variables), including
automatic-duration arrays, are deallocated and disappear when the function returns. If a function returns a
pointer to a local array, that pointer will be invalid by the time the caller tries to use it.
Finally, when we're doing dynamic memory allocation with malloc, realloc, and free, we have to be
most careful of all. Dynamic allocation gives us a lot more flexibility in how our programs use memory,
although with that flexibility comes the responsibility that we manage dynamically allocated memory
carefully. The possibilities for misdirected pointers and associated mayhem are greatest in programs that
make heavy use of dynamic memory allocation. You can reduce these possibilities by designing your
program in such a way that it's easy to ensure that pointers are used correctly and that memory is always
allocated and deallocated correctly. (If, on the other hand, your program is designed in such a way that
meeting these guarantees is a tedious nuisance, sooner or later you'll forget or neglect to, and maintenance
will be a nightmare.)
2 Examples
Example1.
#include <stdio.h>
#include <stdlib.h>
void alloc(float **arr,int num){
int size;
size=sizeof(float)*num;
*arr=(float *)malloc(size);
}
void print(float *arr, int num){
for(int i=0;i<num;i++){
printf("\n %d : %.2f", i, arr[i]);
}
}
int main() {
int num1;
float *arr;
printf("\nEnter a number of values: ");
fflush(stdout);
scanf("%d",&num1);
alloc(&arr,num1);
for(int i=0;i<num1;i++){
printf("\n arr[%d] = ", i);
scanf("%f", &arr[i]);
}
print(arr,num1);
return 0;
}
Example2.
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
int** matrix(int **x, int n);
void free_matrix(int **x, int n);
int main(void)
{
int **x;
int n;
scanf("%d", &n);
x = matrix(x, n);
for (int i = 0 ; i < n ; i++)
{
for (int j = 0 ; j < n ; j++)
x[i][j] = i + j;
}
for (int i = 0 ; i < n ; i++)
{
for (int j = 0 ; j < n ; j++)
printf("%d ", x[i][j]);
printf("\n");
}
free_matrix(x, n);
10
getche();
return 0;
}
int** matrix(int **x, int n)
{
x = (int**) malloc(n * sizeof(int*));
for (int i = 0 ; i < n ; i++){
x[i] = (int*) malloc(n * n * sizeof(int));
x[i] -= n;
}
return x;
}
void free_matrix(int **x, int n)
{
int i;
x += n;
for (i = 0 ; i < n ; i++)
free(x[i] + n);
free(x);
}
3 Assignments
I.
1. Write a program that reads a number that says how many structures are to be stored in an array, creates
an array to fit the exact size of the data and then reads in that many numbers into the array.
2. Write a program that implements a:
2.1 simply linked list
2.2 double linked list
2.3. circular list
3. Write a program that concatenates two linked list objects of characters. The program should include
function concatenate, which takes references to both list objects as arguments and concatenates the second
list to the first list.
4. Write a program that merges two ordered list objects of integers into a single ordered list object of
integers. Function merge should receive references to each of the list objects to be merged and reference to
a list object into which the merged elements will be placed.
5. Write a program that inserts 25 random integers from 0 to 100 in order in a linked list. The program
should calculate the sum of the elements and the floating-point average of the elements.
6. Write a program that creates a linked list of 10 characters and creates a second list object containing a
copy of the first list, but in reverse order.
7. Write a program that inputs a line of text and uses a stack to print the line reversed.
8. Write a program that uses a stack to determine if a string is a palindrome (i.e., the string is spelled
identically backward and forward). The program should ignore spaces and punctuation.
II.
1. Write a program that converts an ordinary infix arithmetic expression (assume a valid expression is
entered) with single-digit integers such as
(6 + 2) * 5 - 8 / 4
to a postfix expression. The postfix version of the preceding infix expression is
62+5*84/-
11
obs: The program should read the expression into character array infix and use modified versions of the
stack functions implemented in this chapter to help create the postfix expression in character array postfix.
2. Write a program that evaluates a postfix expression (assume it is valid) such as
62+5*84/obs: The program should read a postfix expression consisting of digits and operators into a character array.
Using modified versions of the stack functions implemented earlier in this chapter, the program should scan
the expression and evaluate it.
3. Modify the postfix evaluator program so that it can process integer operands larger than 9.
III.
1. Write a program that implements a binary tree and:
2.1 Counts the number of nodes;
2.2 Counts the number of leaves;
2.3 Finds the number of levels of the tree;
2.4 Displays the tree in preorder/inorder/preorder fashion;
2. Write a program that implements a binary search tree.
2.1 Counts the number of nodes;
2.2 Counts the number of leaves;
2.3 Finds the number of levels of the tree;
2.4 Deletes a node from the tree;
2.5 Displays the tree in preorder/inorder/preorder fashion;
12