Understanding Pointers, References, and Dynamic Memory Allocation in C: Examples, Study notes of Computer Programming

An in-depth explanation of pointers, references, and dynamic memory allocation in c. It covers the concept of pointers, their usage, call by reference, and dynamic memory allocation using the new operator. The document also discusses the importance of de-allocating memory and the consequences of not doing so.

Typology: Study notes

2011/2012

Uploaded on 08/06/2012

anchal
anchal 🇮🇳

4.6

(9)

95 documents

1 / 13

Toggle sidebar

This page cannot be seen from the preview

Don't miss anything!

bg1
469
r

Lecture No. 39
Reading Material
Deitel & Deitel - C++ How to Program Chapter. 5, 6, 7
Revise old topics
Summary
51) Pointers
52) References
53) Call by Value
54) Call by Reference
55) Dynamic Memory Allocation
56) Assignment and Initialization
57) Copy constructor
58) Example
59) Rules for Using Dynamic Memory Allocation
60) Usage of Copy Constructor
61) Summary
62) Exercise
In this lecture, we will review the concepts like pointers, references and memory
allocation, discussed in the previous lectures. The review of these topics will help
enhance our understanding of the programming. Let’s discuss these topics one by one.
Pointers
Pointer is a special type of variable that contains a memory address. It is not a
variable that contains a value, rather an address of the memory that is contained inside
a pointer variable.
In C++ language, variables can be without type. Either we can have a void pointer or
these can be typed. So we can have a pointer to an integer, a pointer to a character and
a pointer to a float etc. Now we have a user-defined data type, which we call classes,
so we can have pointers to classes.
docsity.com
pf3
pf4
pf5
pf8
pf9
pfa
pfd

Partial preview of the text

Download Understanding Pointers, References, and Dynamic Memory Allocation in C: Examples and more Study notes Computer Programming in PDF only on Docsity!

r 39

Lecture No. 39

Reading Material

Deitel & Deitel - C++ How to Program Chapter. 5, 6, 7

Revise old topics

Summary

  1. Pointers
  2. References
  3. Call by Value
  4. Call by Reference
  5. Dynamic Memory Allocation
  6. Assignment and Initialization
  7. Copy constructor
  8. Example
  9. Rules for Using Dynamic Memory Allocation
  10. Usage of Copy Constructor
  11. Summary
  12. Exercise

In this lecture, we will review the concepts like pointers, references and memory allocation, discussed in the previous lectures. The review of these topics will help enhance our understanding of the programming. Let’s discuss these topics one by one.

Pointers

Pointer is a special type of variable that contains a memory address. It is not a variable that contains a value, rather an address of the memory that is contained inside a pointer variable. In C++ language, variables can be without type. Either we can have a void pointer or these can be typed. So we can have a pointer to an integer, a pointer to a character and a pointer to a float etc. Now we have a user-defined data type, which we call classes, so we can have pointers to classes.

docsity.com

While talking about pointers, we actually refer to pointing to an area in memory. A pointer to an integer, points to a location opted by an integer in memory. If there is an array of integers in the memory, we can still use a pointer to point to the beginning of the array. This is a simple manipulation. To have the address of a memory location, we use & sign. The ‘&’ sign is also used in references. So we have to be very cautious while making its use. To further overcome this ambiguity, we will now recapture the concept of reference.

References A reference can be considered as a special type of pointer as it also contains memory address. There are some differences between pointers and references. Pointers may point to nothing while references always have to point to something. A reference is like an alias for an object or a variable. The references should be used when we are implementing the call by reference. This helps us make our syntax easier as we can implement the call by reference with out using the * operator.

Call by Value Whenever we call a function and pass an argument, an object or variable to the function, then by the default rule of C and C++, it is a call by value. It means that the original data remains at its place and a temporary copy of it is made and passed to the function. Whatever the function does with this copy, the original value, in the calling function, remains intact. This is a call by value.

Call by Reference If we want a function to change something in the original object variable or whatever, that variable or object by reference would be passed. To do this, we don’t make temporary copy of that object or variable. Rather, the address of the variable is sent. When the function manipulates it, the original object will be manipulated, effecting change in its values. The use of call by reference is also important for the sake of efficiency. If we have a large object, sending of its copy will be something insufficient. It will occupy a large space on the stack. Here, we can use call by reference instead of call by value only for efficiency while we need not to change the original object. For this, we use a keyword const that means that a const (constant) reference is being passed. The function can use its values but cannot change it. Now we come to the dynamic memory allocation.

Dynamic Memory Allocation In C language, we have a method to allocate dynamic memory. In it, while executing the program, we allocate some memory from the free store (heap) according to our need, use it and after using, send it back to the free store. This, dynamic memory allocation, is a very common function in programming. While writing C++ language, it was decided that it should not be implemented by a function call. There should be native operators, supposed to be very efficient. These operators are new and delete. We allocate memory with the new operator from the free store. It returns a pointer. For example, if we say p is a pointer to an integer, the statement will be written as

int *p ; Now we write p = new int ;

docsity.com

For example, in case of Matrix class, we will like to tell the number of rows and columns of the object of the Matrix in the constructor, so that the constructor could allocate the memory for that object. A keyword new is used for this purpose. Here, we realize that in case of data member of this class Matrix, fixed rows and columns cannot be used in the class definition. We cannot define a two-dimensional array inside the class as it negates the concept of extensibility. Here, inside the class we define something like int *m; which means m is a pointer to an integer. It is the data member of the class. In the constructor of the class, we say that it will take two integer arguments, rows and columns. Whenever we declare an object, which is an instantiation of the class, the constructor of the class is called. Now in the constructor of this class we want to allocate memory from the free store equal to the memory required by m x n (m multiply by n) integers. So if we say a Matrix of 3 rows and 3 columns then we require memory for 9 (3 * 3) integers. If we say four rows and five columns then we require a memory for 20 (4 * 5) integers. Now it is very simple, inside the constructor we will write

m = new int [rows * columns] :

Thus we created a Matrix to which we tell the number of rows and columns, through variables, during the execution of the program. When we created an object of the class its constructor is called to which the variable values are passed and by multiplying these variables (number of rows and columns), we allocate a memory for these integers by the new operator and its address is assigned to m. Thus the object is initialized and the required memory is available to it. Now we can use it as a two dimensional array and can put values inside it. Now the class definition of the class Matrix can be written as under.

class Matrix { private: int *m; int row, col; public: Matrix(int rows, int cols) { m = new int[rows * cols]; } };

There is a requirement that if the constructor of a class allocates the memory, it is necessary to write a destructor of that class. We have to provide a destructor for that class, so that when that object ceases to exist, the memory allocated by the constructor, is returned to the free store. It is critically important. Otherwise, when the object is destroyed, there will be an unreferenced block of memory. It cannot be used by our program or by any other program. It’s a memory leak that should be avoided. So whenever we have a class in which the constructor allocates dynamic memory, it is necessary to provide a destructor that frees the memory. Freeing the memory is an easy process. We have no need to remember that how many rows and columns were used to allocate the memory. We simply use the delete operator with empty brackets and the pointer that points to the allocated memory. We write this as follows

docsity.com

delete [] m ;

This statement frees the allocated memory (whatever its size is) that is being pointed by m.

Assignment and Initialization

Let us discuss the assignment and initialization. We do initialization as

int i = 0 ;

This is declaring an integer and initializing it. The second way to do this is

int i ; i = 0 ;

This has the same effect as the first statement but the behavior is different. At first, a space is allocated for i before assigning a value to it.. The same applies whenever we create an object. We can either create an object, initialize it at the creation time that means constructor is being called, or we can create an object and then assigns values to its data members later. This is usually done either by set functions or with the assignment statements. Here the thing to differentiate is that if we have two objects of a class, say Matrix, m1 and m2. We have, in some way, created m1 , its rows and columns have been allocated, and values have been put in these rows and columns. Now somewhere in the program, if we write

m2 = m1 ;

It is an assignment statement. If we have not defined the overloaded operator for assignment, the default assignment of C will be carried out. The default assignment is a member-to-member assignment. Now let’s again look at the construct for the Matrix class. In it, we have only one data member i.e. a pointer to an integer. We have written int *m ; in the class definition. So in m1 , there is a pointer to an integer but m is a fully qualified developed object. It is, let’s say, a 5 x 5 matrix and has values for its entities. Now when we write

m2 = m1 ;

m2 has also a pointer m to an integer as its own data member. If we have not written an overloaded assignment operator for this class, the value of m of the object m1 will be assigned to m of the object m2. Now we have two objects, having pointer variables that hold the same address. So these are pointing to the same region in the memory. There arise many problems with this. Firstly, if we destroy m1, the destructor of m1 is called and it frees the memory. So the memory being pointed by m of the object m has gone back to the free store. Now what happens to m2? There is a pointer in m2 , pointing to the same memory, which has been sent to the free store by the destructor of m1. The memory is no longer allocated. It has been sent to the free store. So we have a serious problem. We don’t want two objects to point to the same region in the memory. Therefore, we write an assignment operator of our own. This assignment operator is such that whenever we do some object assignment, it allocates separate

docsity.com

inside the constructor and allocate it a memory. Then the values of memory of the original object are copied into this memory. Now the pointer of this new object will point to this new location. As the constructor returns nothing and just creates a new object, so there is no return statement in the constructor. Thus, this copy constructor is completed. The syntax of this constructor is given below

Matrix::Matrix ( const Matrix &other ) { size = other.size ; // size is a function to determine the memory allocated by object m = new int [size] ; copyvalues ( m, other ) ; }

In this case, it creates a new object that actually creates memory. It does not make the shallow copy so there is no copy of the pointer value. In fact, the pointer has a new value. But the values pointed to by this pointer (i.e. the values in the new allocated memory) are the copy of the values in the memory of the original object, and then this fully constructed object is returned. Now we have the facility of copy constructor. With it, we can define new objects based on the existing objects. This copy constructor is necessary for the objects in which the memory is allocated dynamically. We can use this copy constructor without causing any problems. Suppose we have written as

Matrix a (3,3) ;

We have defined a constructor that takes two arguments rows and columns, as a matrix ‘initializer’. We allocate memory for these rows and columns and create an object. Then somewhere, later in the program, We write

Matrix b (a) ;

This statement makes a complete copy of already created object a and a new object b is created. This new object has its own pointer and own memory allocation. This memory location is different from the memory allocated inside the matrix a. Now if a dies then b is still a valid object. So a copy constructor is critically useful. It is used when we want to create a duplicate copy of an object. It is always used whenever we want to do a call by value into a function to which an object is being passed, and the object is of a class in which we have allocated the memory dynamically.

Example Let’s look at an example to further understand these concepts. We write a class String. It has a data member c that is a pointer to a character. We write a member function (copy function) of the class, which can copy values in the character array of the class. There is a constructor that will allocate a space for the character arrays i.e. string. The starting address of this array will be stored in data member of the class i.e. in a pointer. We have not written any copy constructor. Now we want to make two objects of this String class, say, s1 and s2. We create object s1 and assign a string to it. We write it as

docsity.com

String s1 (“test1”) ;

Now after this we write

String s2 = s1 ;

Thus we create a new object s2. The values of s2 are initialized with the values of s. As we have written no copy constructor, C will provide the default copy constructor. Now if we display the string of s2 , it will be the same as of s1. Now use the copy function to assign new values to the string inside the object s1. So we write

s1.copy(“A new string”) ;

Thus we write a new string in s1. Now again if we display the string of s2 by writing

s2.print ;

We will see that it displays the same value, assigned to s1 in the previous statement. The reason is that the default copy constructor has done member-to-member copy. It has copied the value of the character pointer to the pointer of s2 and thus both pointers are pointing to the same memory location. Now there is need of providing a copy constructor for a class like this. We also have to provide a destructor as we are doing memory allocation inside the constructor of the class.

Following is the code of the example in which we provide a copy constructor. We create an object based on an existing object. The copy constructor creates an object with full copy of the existing object with its values in a new memory location.

/*This program has a copy constructor and demonstrate the use of it. We create a new object by passing it an existing object, this calls the copy constructor and thus creates a complete copy of the passing object, and has its values in new location of memory. */

#include <iostream.h> #include <stdlib.h>

// class definition class String { char *c; public: // copy function void copy (char *s) { c = s ; } // getting the length of the string

docsity.com

The following is the output of the program which shows the use of copy constructor. The string of s1 is test The string of s2 is test The string of s1 is A new string The string of s2 is test

The other affected part is the assignment operator itself. We know that there are dangers in the assignment operators of a class in which memory is being allocated. We cannot write an assignment operator for such a class blindly. When we write an assignment operator for such a class, that operator must first look at the fact whether there is self-assignment being done. Suppose we have an integer i. We have written as int i ; i = 10 ; And down to this we write i = i ; There is no problem with it. It is a legal statement. It is complicated if we do such assignment through pointers. In such a case, pointer is pointing to itself and even it has no problem. But when we do this with objects that have allocated dynamic memory, the method of assignment is changed. Let’s take the example of Matrix. We have an object of Matrix, say m1 , which has three rows and three columns. Another object, say m2 , has five rows and five columns. Somewhere in the program we write m1 = m2 ; Here m2 is a big object while m1 is a small one. We want to assign the big object to the smaller one. The assignment operator for this type of class, first de-allocates the memory reserved by the left-hand side object. It frees this by the delete operator. Then it will determine the memory required by the object on right hand side. It will get that memory from the free store by using new operator. When it gets that memory, it will copy the values and thus the statement m1 = m2 ; becomes effective. So assignment has a requirement. Now if we say m1 = m1 ; We have defined assignment operator. This operator will delete the memory allocated by m1 (i.e. object on L.H.S.). Now it wants to determine the memory allocated by the object on the right hand side, which in this case, is the same i.e. m1. Its memory has been deleted. So here we get a problem. To avoid such problem, whenever we write an assignment operator, for objects of the class that has done memory allocation. After this, we do other things. We have discussed the example in which we create an object of Matrix. We create it using a copy constructor by giving it another object. The syntax of it we have written as

Matrix m2 (m1) ;

This is the syntax of creating an object based on an existing object. We can write it in the following fashion. Matrix m2 = m1 ;

While this statement, we should be very clear that it is not an assignment only. It is also a construction. So whenever we are using initialization, the assignment operator

docsity.com

seems as equal to operator. But actually assignment operator is not called. Think about it logically that why assignment operator is not called? The assignment operator is called for the existing objects. There should be some object on the left- hand side, which will call the assignment operator. When we have written the declaration line

Matrix m2 = m1 ;

The m2 object has not constructed yet. This object of Matrix does not exist at the time of writing this statement. So it cannot be calling the assignment function or assignment operator. This is an example of the use of a copy constructor. Thus, there are two different ways to write it. Remember that whenever we create an object and initialize it in the declaration line, it calls the copy constructor.

Let’s talk about another danger faced by the programmers when they do not provide copy constructor. The ordinary constructor is there which allocates memory for the objects of this class. Suppose we do a call by value to a function. Here, we know that a temporary copy of the object is made and provided to the function. The function does manipulations with this copy. When the function returns that temporary copy is destroyed. As no copy constructor is there, a shallow copy, with values of pointers, is made. The destructor should be there as we do memory allocation in the class. Now suppose that there is a destructor for that class. Now when this temporary object destroys its destructor executes and de-allocates the memory. Now as it was a shallow copy so its pointers were pointing to the same memory as of the original object. In this way, actually, the memory of the original object is de-allocated. So the pointer of the original object now points to nothing. Thus, in the process of function call, we destroyed the original object as it is an invalid object now. Its pointer is pointing to an unknown memory location. This is a subtle but very critical. This can be avoided by providing a copy constructor, which actually constructs a fully formed object with its own memory. That temporary object will go to the function. When it is destroyed, its destructor will de-allocate this memory. However, the original object will remain the same.

Rules for Using Dynamic Memory Allocation Whenever we have a class in which we do dynamic memory allocation, there are some rules that should be followed.

First, we must define a constructor for it. Otherwise, we will not be able to carry out dynamic memory allocation. This constructor should be such that it gets memory from the free store, initializes the object properly, sets the value of the pointer and returns a fully constructed object.

Secondly, we must write an assignment operator for that class. This assignment operator should first check the self-assignment and then make a deep copy.. So that a properly constructed object should be achieved..

Thirdly, as we are doing dynamic memory allocation in the constructor, it is necessary to provide a destructor. This destructor should free the allocated memory. These three rules are must to follow.

docsity.com

We must provide a copy constructor that is able to create fully formed copies of the objects. That means is should not only make the copies of the values of the pointers but it should give the pointers new values by allocating new memory for the object. And should copy the values of the original object into this new memory location. We must provide an assignment operator. This operator should be able to check the self-assignment and can assign one object to the other in a proper fashion using the concept of deep copy and not a shallow copy. So we allocate memory space then copy element by element in this allocated memory. And finally we must do de-allocation which means whenever we destroy an object and it goes out of scope, we should free the memory allocated by that object. To do the memory de-allocation we must provide the destructor of the class in which we free (delete) the memory by using deletes operator.

Exercise You should write small programs to examine the working of these rules. You can check this if we allocate memory and do not delete it in the destructor. Then the next time, when we execute the program it will allocate a new memory. We can find that which memory is assigned by displaying the value of the pointer (not the value it points too). It will be a number with 0x-notation i.e. it will be in hexadecimal. We don’t care about the exact value but we will find that if we have provided a proper destructor. Then on the same computer, in the same session, we execute the program, a specific address of memory will be assigned to the program. With the proper destructor, we stop the program and then again start it. Nine out of ten times, we get the same memory. That means we will see the same address. Nine times out of ten is because the operating system can use this memory somewhere else between the times of two executions of the program. If we do not provide a destructor i.e. we do not deallocate the memory, it is necessary that each time we will get a new memory. The previous memory is being wasted. You can prove it by yourselves by writing small programs.

docsity.com