Você está na página 1de 12

Laboratory Module 7

Pointers Structures and Dynamic Memory Allocation


Purpose:

To understand how dynamic allocation of memory works


To see how memory can be freed
To see how memory may be allocated with initialization
To see how the size of a previous allocated block of memory may be changed

1 Preparation Before Lab


1.1 Structures
The C language enables you to create several different custom data types. The most common of these
is the structure. A structure is a collection or grouping of variables, under a name defined by the
programmer. The variables in a structure can be of the same or of different types, but are usually logically
related in some respect. For example, a structure of the user-defined type customer might contain type char
array variables for the name and address, and type float variables for an account balance or a billing
amount.
There are several steps involved in creating and using a structure:
1) You must create a template, or definition, of what the structure will contain. This template becomes
your new user-defined data type.
2) As with any of the built-in data types, you must declare variables that will be of your new data type.
3) Finally, you need to assign values to and access the variables that make up the structure type.
The variables that comprise the structure are called the members of the structure. Some references refer
to the structure members as elements.
Creating a Structure
To create the new data type (the structure), you need to define a template containing the variables that make
up the type. The format for creating a structure definition is:
struct type_name {
type variable_name;
type variable_name;
type variable_name;
type variable_name;
} structure_variable_name;
1) The keyword struct indicates that you are defining a new data-type template.
2) The type_name is the user-defined name given to the new data type.
3) The statements between the braces are the declarations for the members of the structure. You'll need
one each of these statements for the members of your structure.
4) The structure_variable_name is the declaration of the variable that will be of your custom struct type.
Structure Members
You declare a structure member using the same format as for the declaration of a "normal" C
variable. The type is followed by the variable name, and is terminated with a semi-colon.
The members that make up a structure are usually of the common C data types. However, you can
also use user-defined types (like other structures) as members of your structures.

Initializing Structure Members


As with character strings declared independently of a structure, you can initialize the character
strings that are structure members as part of the structure-variable declaration. Numeric members of a
structure can also be initialized in the declaration. One format for initializing the members of a structure is:
typedef struct {
char
char
char
float
float
} customer;

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.

The format for defining and declaring an array of structures is:


struct type_name {
type
variable_name;
type
variable_name;
} structure_variable_name[size];
Referring to and working with an array of structures and the individual structure members requires
the use of notation similar to the array notation that you use with standard arrays. The notation to refer to an
individual structure in the array is:
array_name[element_number]
Each member of the array is a structure. So, the preceding notation refers to the structure as a whole. To
refer to a member within an individual structure in the array, the format is:
array_name[element_number].structure_member_name
Passing Structures to Functions
You can pass the value of a structure member to a function by using a pass-by-value notation. In
such a situation, you would be using the value of the member without altering its value. When you're
passing a structure member to a function, the types must match in each of the following:
1) The structure definition,
2) The function prototype,
3) The function definition and,
4) The function call.
You can also pass the address of a structure member to a function. You would do this when you're
intent is to alter the value of the structure member.
Most often, you will define your structures in header files and then include those header files in your
source files that make use of the structure. The reason for this is that the definition of a structure must
appear before the prototypes of any functions that pass or return the structure and the compiler needs to see
the structure type before the structure is used. Putting these definitions into a header file ensures that your
program will meet these requirements because you always put the statements to include header files at the
beginning of your programs.
Whenever you create an instance of a structure, pass a structure into a function, or return a structure
from a function, you can use the keyword struct with the structure name.
It is typical to declare structure definitions in a header file (outside of all functions) and include them
in source files. The structure definition must come before the function prototypes that either pass or return
the structure.
Although you can pass an entire structure to a function, there are drawbacks to the process.
1) The members of the structure are accessed by value only. You cannot modify the original structure
members.
2) More importantly, when a structure is passed to a function, the overhead involved with the stack can
reduce run-time performance and cause your program to degrade.
Fortunately, C enables you to pass a pointer to a structure to overcome these problems.
Using Structure Pointers
By using a structure pointer that contains the address of a structure, you can access and modify the
members of a structure.
You can use pointers to structures, just as you would use pointers to other variable types. The
structure pointer must be declared to be of the structure type you have defined. The format for creating a
pointer to a structure is:
struct structure_type_name *pointer_name;
The assignment statement for the pointer is similar to other pointer assignments:
ptr = &fla_cust;
This statement places the address of the fla_cust structure in the ptr pointer.
To use a pointer to refer to and access the values of structure members, you must use notation with
the arrow operator.

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.

1.2 Dynamic Memory Allocation


Up to this point, when memory has been needed by a program, it has been set aside by declaring the
desired type of variables. For variables declared in any function, space in memory is set aside to hold the
value assigned to the variable. This memory is reserved until the function finishes.
Because the function main() is the first function invoked and is resident until the program terminates,
any variables in main() exist for the entire duration of the program. For local variables, created in a function
other than main(), memory is set aside and retained until the function finishes.
This may not always be the optimal way to allocate memory. Fortunately, you can instead write your
programs obtain memory as they are running. With dynamic memory allocation, memory is not reserved or
set aside at the start of the program; rather, it is allocated on an as-needed basis.
The dynamic memory allocation functions, in conjunction with their pointer arguments, are used to
support many programming structures, such as linked lists and binary trees.
When a program is compiled, much of the memory locations needed for the program to hold variable
and constant data can be determined in advance. As the compiler works through the main part of the

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.

Figure 1. Where dynamic storage is provided in memory.


The two most important functions involved in the dynamic allocation process are malloc()and
free().These functions reside in the stdlib.h header file.
1) malloc()allocates memory.
2) free()releases memory when it is no longer needed.
Dynamic allocation is a pretty unique feature to C (amongst high level languages). It enables us to
create data types and structures of any size and length to suit our programs need within the program.
The function malloc is most commonly used to attempt to ``grab'' a continuous portion of memory. It is
defined by:
void *malloc(size_t number_of_bytes)
That is to say it returns a pointer of type void * that is the start in memory of the reserved portion of
size number_of_bytes. If memory cannot be allocated a NULL pointer is returned. Since a void * is

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:

char *somestring, *copy;


...
copy = malloc(strlen(somestring) + 1); /* +1 for \0 */
/* incomplete -- malloc's return value not checked */
strcpy(copy, somestring);

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:

ip = (int *) calloc( ip, 50);


Memory allocated with malloc lasts as long as you want it to. It does not automatically disappear when
a function returns, as automatic-duration variables do, but it does not have to remain for the entire duration
of your program, either. Just as you can use malloc to control exactly when and how much memory you
allocate, you can also control exactly when you deallocate it.
In fact, many programs use memory on a transient basis. They allocate some memory, use it for a
while, but then reach a point where they don't need that particular piece any more. Because memory is not
inexhaustible, it's a good idea to deallocate (that is, release or free) memory you're no longer using.
Dynamically allocated memory is deallocated with the free function. If p contains a pointer previously
returned by malloc, you can call
free(p);
which will ``give the memory back'' to the stock of memory (sometimes called the ``arena'' or ``pool'')
from which malloc requests are satisfied. Calling free is sort of the ultimate in recycling: it costs you almost
nothing, and the memory you give back is immediately usable by other parts of your program.
(Theoretically, it may even be usable by other programs.)
Freeing unused memory is a good idea, but it's not mandatory. When your program exits, any memory
which it has allocated but not freed should be automatically released. If your computer were to somehow
``lose'' memory just because your program forgot to free it, that would indicate a problem or deficiency in
your operating system.
Naturally, once you've freed some memory you must remember not to use it any more. After calling
free(p); it is probably the case that p still points at the same memory. However, since we've given it back,

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

Você também pode gostar