Presentation is loading. Please wait.

Presentation is loading. Please wait.

Chapter 11 Defining Abstract Data Types. Objectives Learn how to use create objects that employ dynamic memory management. Explore creating objects that.

Similar presentations


Presentation on theme: "Chapter 11 Defining Abstract Data Types. Objectives Learn how to use create objects that employ dynamic memory management. Explore creating objects that."— Presentation transcript:

1 Chapter 11 Defining Abstract Data Types

2 Objectives Learn how to use create objects that employ dynamic memory management. Explore creating objects that behave in standard and transparent ways. Introduce copy constructors Introduce operator overloading Introduce destructors Develop a class that performs like a simplified vector.

3 C++ It has been discovered that C++ provides a remarkable facility for concealing the trival details of a program – such as where its bugs are. – David Keppel All new features added to C++ are intended to fix previously new features added to C++. – David Jameson

4 Defining Classes In Chapter 9 we looked at creating a Student_info type but it did not behave is a standard way when it came to making copies, assignments, etc. In Chapter 10 we had a homework problem about creating a string list class but again it did not behave in a standard way. In addition we were limited in the number of strings and the total number of characters. In this chapter we will explore how to design classes that avoid these limitations.

5 Vec Class In this chapter we will create a stripped down version of the vector class which we will call Vec. Consider the following vector code. vector vs; vector v(100); vector ::const_iterator b, e; vector ::size_type i = 0; for (i = 0; i != vs.size(); ++i) cout << vs[i].name(); b = vs.begin(); e = vs.end();

6 Vec Class If we want our Vec class to behave similarly what do we need? – To use templates so we can store any type. – To be able to specify a size in our declaration. – Be able to create iterators. – Have a special type to index our structure. – Actually be able to index our structure. – Have begin and end iterators. – etc.

7 Template Class Since we want to use the same template type for all the functions in the class, let’s create a template class. template class Vec { public: // interface private: // implementation } Vec is a class with one type parameter T.

8 Template Class Since Vec is a template class, when we create an object we will need to specify the type. Vec v1; Vec v2; This would create two complete instantiations of the Vec class. – One that stores ints. – One that stores strings.

9 Data Storage Let’s start with the idea that we will store our data in a dynamically allocated array. – This won’t be able to grow and shrink as necessary but we can try to deal with that later. We will need to keep track of how many elements we have stored. We also need to know “one past” the last element.

10 Data Storage template class Vec { public: // interface private: T* data; T* limit; }

11 Delayed Memory Allocation We will deal with exactly how we will allocated and deallocate memory later. For now we will assume that there are private methods that will deal with this. We will write the rest of the code using these nonexistent functions. We are delaying writing these so we will know exactly what we need when we do write them. These functions will manage data and limit. The public functions will be able to use data and limit but not modify them.

12 Abstract Data Type In doing things this way we are separating the interface from the implementation. If we want to change the implementation later it will be relatively easy. What we are doing right now is creating an abstract data type. We are focusing on what the new type will do and not on how it will do it.

13 Constructors We need two constructors. vector vs; vector v(100); We need to make sure that data and limit are valid and do what we expect. In the case where we create entries we need to allocate space to hold them. We will use the default constructor for T, whatever that is. There will be a option for the user to specify a different initial value for the new entries.

14 Constructors The default constructor won’t do much, it will just call a create function which we will need to write later. Vec() { create(); } The second constructor will be similar, it will pass its parameters on to a different version of create. Vec(std::size_t n, const T& val = T()) { create(n, val); }

15 Constructors Vec(std::size_t n, const T& val = T()) { create(n, val); } The first parameter is the size of the Vec to create. The second parameter is a reference to a T value to use to initialize the created entries. This second parameter has a default value using the default constructor for the T class. This effectively creates two constructors for the price of one. – One accepts a single parameter – the size. – The other accepts both a size and an initial value. Both these parameters are passed on to create which we will write later.

16 explicit When we create a constructor with parameters, we often worry about automatic conversions that could cause us problems. Vec vi(100); // call constructor Vec vi = 100; // this is different We will talk more about what this second line does in a moment. The explicit keyword will prevent us from using the constructor unless it is explicitly called. explicit Vec(std::size_t n, const T& val = T()) { create(n, val); }

17 Type Definitions It is a good idea to provide types that our users can use. This will make things easier to use and hide some of the implementation details. We need types for following. – Iterators – Constant iterators – The type for the difference between iterators – The size of a Vec – The type of object the container stores – A reference to the type stored – A constant reference to the type stored

18 Type for Iterators What type should we use for our iterators? We need to be able to dereference an iterator to get an entry in our Vec. We need for operations like ++ to work. We could make a whole new class to contain our iterators. We will use plain pointers as iterators. Implementing and using these types our class has grown.

19 Class with Types template class Vec { public: typedef T* iterator; typedef const T* const_iterator; typedef size_t size_type; typedef T value_type; typedef std::ptrdiff_t difference_type; typedef T& reference; typedef const T& const_reference Vec() { create(); } explicit Vec(size_type n, const T& val = T()) { create(n, val); } private: iterator data; iterator limit; }

20 size method We need for our Vec class to have a size method that will return the number of entries (as a Vec::size_type ). size_type size() const { return limit – data; } Note that this is a const method because it doesn’t change anything. It simply returns the difference between the iterators that bracket the data. This will automatically be converted from ptrdiff_t to size_type.

21 Indexing We want our Vec class to support indexing. To do this we need to redefine what x[i] means when x is a Vec. Technically, x[i] is a function call. This type of function is called an operator. x[i] is the same as x.operator[] (i). When we redefine an operator to work on different types it is called operator overloading.

22 Operator Overloading To define indexing for our class we define a new public version of operator[]. What should it return? Two possibilities. – Make a copy of the appropriate entry and return its value. – Return a reference to the appropriate entry. If we return a reference then we can use the indexing for both reading and writing.

23 Operator Overloading But what if someone wants to index a constant Vec ? Returning a reference wouldn’t be allowed because we could use it to modify the Vec. The solution is to have two versions of operator[]. T& operator[] (size_type i) { return data[i]; } const T& operator[] (size_type i) const { return data[i]; } This returns a reference to the corresponding element of the underlying array.

24 Returning Iterators We need a begin and an end function. We will need constant and non-constant versions of both. iterator begin() { return data; } const_iterator begin() const { return data; } iterator end() { return limit; } const_iterator end() const { return limit; }

25 Copy Control At this point we would have a working class once we defined our private functions. – two versions of create –push_back –clear It would also be nice to be able to control what happens when we copy a Vec. – If x and y are two Vec objects, what does y = x do? – Can we create a new Vec as a copy of another? – What happens when we pass a Vec by value to a function?

26 Copy Control When a variable is passed to a function by value, a copy is made. We would like for the following code to work as expected. Vec x; double d = median(x); This copies x into a parameter in median. Here is another example. String line; Vec words = split(line); Here a copy of the return value from split is made into the word Vec.

27 Copy Constructor We may also want to make an explicit copy. Vec vs1; Vec vs2 = vs1; Both implicit and explicit copies are done by a copy constructor. The copy constructor will take a reference to a Vec as its only argument. – We must use a reference here because this function will define what it means to pass by value. – We don’t yet know what a pass by value is! That is the point of this function.

28 explict Explained Notice the following line is actually a call to a constructor. Vec vs2 = vs1; This line could be replaced with Vec vs2(vs1); On the other hand consider the following line. Vec vs = 100; This doesn’t make sense, 100 is not a list. However this next line already calls one of our constructors. Vec vs(100); This difference is why we needed the explicit keyword.

29 How to Copy If we just copy the data members of the structure we will simply be copying two pointers. The two “different” copies will actually still share the same data array. Modifying one will also change the other! This doesn’t sound like much of a copy.

30 How to Copy We need to make a new copy of the data array for the duplicate.

31 How to Copy Since this will involve memory management, we will hand this job over to yet another version of create. Vec (const Vec& v) { create(v.begin(), v.end()); } This new version of create will accept two iterators as parameters and initialize the new Vec by copying the entries in the range [begin, end).

32 Assignment C++ uses the symbol = in two different ways. The first invokes the copy constructor. Vec vs1; Vec vs2 = vs1; The second uses the assignment operator. Vec vs1; Vec vs2; vs2 = vs1;

33 operator= There can be several versions of the operator= depending on what is on the right hand side. – The assignment operator is the one where a Vec is on the right hand side. This version accepts reference to a constant Vec as a parameter. This is different from the copy constructor because we must destroy the existing information before making the copy.

34 operator= We have the same issues with memory management as with the copy constructor. One additional issue is what to do if the user does the following. Vec vs; vs = vs; If we destroy the entries before making a copy in this case all the data will be lost.

35 Overloading the Assignment Operator template Vec & Vec ::operator=(const Vec& rhs) { if (&rhs != this) { uncreate(); create(rhs.begin(), rhs.end()); } return *this; }

36 Overloading the Assignment Operator This is a function that is begin defined outside the class header. First, we need to define a template. template Next we specify the return type. Vec & This is not necessary for a simple assignment. x = y; But it is necessary so that lines like the following will work. x = y = z;

37 Overloading the Assignment Operator Next, we specify that we are overloading the operator = for the Vec class and accepting a constant reference to a Vec as a parameter. Vec ::operator=(const Vec& rhs) Notice that when we specify that the class belongs in the Vec class we need to specify the template type. For the rest of the function we can just refer to this as Vec (the is implicit). This is why the parameter has const Vec& rhs and not const Vec & rhs.

38 Overloading the Assignment Operator The operator = is a binary operator. Consider this line. x=y; This is the same as the following. x.operator=(y); The function “belongs” to the left hand operand and the right hand operand is the parameter.

39 this In order to determine if this is a case of self- assignment we use the keyword this. Inside a member function, this is a pointer to the current object. If a pointer to the right hand side is equal to this then we don’t need to do anything. It is important to handle the self assignment as a special case.

40 Making the Copy To make the copy we first erase the current contents of the data array using an new management function uncreate that we will need to write. Finally, we use the create function as we did in the copy constructor to make the copy. We return a pointer to this (the left hand side). – The reason we do this is the right hand side may not be something that exists outside of the assignment.

41 Function Declaration Since we are defining our function outside the class definition, we need to put a declaration for our function in the public section. Vec& operator=(const Vec&); Notice that we do not need the here because it is implicit in the class definition.

42 Assignment VS Initialization There are two ways to use = in our code. – In initialization a variable is begin created and initialized (copied) from an existing value. – In assignment an existing variable is destroyed and an new value is copied in its place. string url_ch = “~;/?:@=&$-_.+!’(),”; This is initialization. string y; This is initialization using default constructor. y = url_ch; This is assignment

43 Assignment VS Initialization The two operations behave differently. With initialization a constructor is called. – Data may be copied but none is erased. – = may be used but it is not part of an expression because there is no return value. Assignment is done via overloading operator =. – Data must be deleted before the copy occurs. – Must include = and this is in an expression so there should be a return value.

44 Assignment VS Initialization Suppose we had a function with the following declaration. Vec split(const string&); The return value for this function is a Vec (not a reference). When we return from this function, the copy constructor will be called to make a temporary copy as the return value. Next we assign the value to a variable with the following line. v = split(line); This calls the overloaded operator=. So in this case we have both initialization and assignment.

45 Deallocating Memory Our class dynamically allocates the array that holds its contents. When a Vec object goes out of scope and is destroyed, this memory will still be allocated. Many languages support garbage collection that would eventually free up this memory. C++ is not one of them. We need to deallocate the memory.

46 Memory Leaks If we do not deallocated the memory we will have what is called a memory leak. The program probably won’t crash immediately. The memory won’t be available for further use. Over time the program will require more and more resources. Performance may suffer. The program may eventually run out of memory and crash. Some programs may need to run for days, months or years. Small leaks may accumulate.

47 Destructors A destructor is a function that is automatically called when a variable is destroyed. You do not directly call a destructor, it happens automatically. The name for the destructor is the name of the class preceded by a tilde ~. It has no parameters and no return value. In our case the destructor needs to call the uncreate function. ~Vec() { uncreate(); }

48 Default Constructors and Destructors If we do not define constructors and destructors they will be created for us. Each data element in a class will be created using its default constructor. Each will be destroyed using its default destructor. If we create any type of constructor for ourselves then the automatic constructor will not be created.

49 The Rule of Three A class that uses dynamic memory needs to pay close attention to the rule of three. – Copy constructor T::T(const T&) – Destructor T::~T() – Assignment Operator T& T::operator=(const T&)

50 Different Sized Vec Objects We have one issue, we need to be able to create Vec objects that contain different numbers of entries. We will create a large array and keep track of which entries are currently being used. We can make it bigger by allocating a larger array and copying in the old entries but this is expensive and we don’t want to do it often. This will add another pointer to our implementation. This pointer will point to the location one past the initialized elements of the Vec.

51 avail Pointer We will need to rewrite our size and end functions. We can also add the push_back function so that we can add entries to a Vec. –Push_back will use two new management functions, grow and unchecked_append.

52 Vec Definition template class Vec { public: typedef T* iterator; typedef const T* const_iterator; typedef size_t size_type; typedef T value_type; typedef T& reference; typedef const T& const_reference; Vec() { create(); } explicit Vec(size_type n, const T& t = T()) { create(n, t); } Vec(const Vec& v) { create(v.begin(), v.end()); } Vec& operator=(const Vec&);// as defined in 11.3.2/196 ~Vec() { uncreate(); } T& operator[](size_type i) { return data[i]; } const T& operator[](size_type i) const { return data[i]; }

53 Vec Definition void push_back(const T& t) { if (avail == limit) grow(); unchecked_append(t); } size_type size() const { return avail - data; } // changed iterator begin() { return data; } const_iterator begin() const { return data; } iterator end() { return avail; } // changed const_iterator end() const { return avail; } // changed void clear() { uncreate(); }

54 Vec Definition private: iterator data; iterator avail; iterator limit; };

55 Allocating Memory If we use the built-in new keyword to allocate memory then it also automatically initializes the memory using the default constructor. We don’t necessarily want this. – What if our template parameter T doesn’t have a default constructor? – We want to be able to initialize using other values. – We would need to initialize the entire array, not just the part that is to be utilized.

56 allocator The header provides a class called allocator that will do what we want. This header provides functions to do the following. – Allocate memory. – Construct and initialize new objects in this memory. – Destroy objects. – Deallocate memory. There are also functions to run through allocated memory and perform the following. – Copy in initial values. – Initialize all values to a fixed initial value.

57 allocator The four member functions are as follows. –T* allocate(size_t) – returns a pointer to enough allocated memory to hold the given number of elements of type T. –deallocate(T*, size_t) – frees the memory of the given size at the given location. –construct(T*, const T&) – creates an object with the given value at the given location. –destroy(T*) – runs the destructor for the object at the given location.

58 allocator There are two nonmember functions that we can use to initialize a range of objects in an allocated array. –template For uninitialized_copy(In, In, For) – This function is very similar to the standard copy function. It accepts two input iterators that delimit the values to copy and a forward iterator to the allocate block where the values are to be copied. –templace uninitialized_fill(For, For, const T&) – This function runs through the allocated memory between two forward iterators and initializes them to the given value.

59 Memory Management Functions We will add an allocator member to our class. It will be named alloc. We will use the alloc methods to implement our memory management functions. – Three versions of create –uncreate –grow –unchecked_append

60 Memory Management Functions private: iterator data; iterator avail; iterator limit; std::allocator alloc; void create(); void create(size_type, const T&); void create(const_iterator, const_iterator); void uncreate(); void grow(); void unchecked_append(const T&); };

61 Class Invariants Class invariants are conditions that should always be true about our class. In this case we have 4 important class invariants. We need to keep these in mind when writing our memory management functions. –data will always point to the first initialized object. If there is no initialized object this will be 0. –avail will always point to the location one past the last initialized object. If there is no initialized object this will be 0. –limit will always point to the location one past the last allocated object. If there are is no allocated memory this will be 0. –data ≤ avail ≤ limit

62 create First, the default constructor needs to be able to create an empty Vec. This uses the version of create that has no parameters. It will not allocate or initialize any memory. Keeping the invariants in mind we get the following. template void Vec ::create() { data = avail = limit = 0; }

63 create The next version of create will initialize n entries to a specified value. This is used for the parameterized versions of the constructor. template void Vec ::create(size_type n, const T& val) { data = alloc.allocate(n); limit = avail = data + n; std::uninitialized_fill(data, limit, val); }

64 create template void Vec ::create(size_type n, const T& val) { data = alloc.allocate(n); limit = avail = data + n; std::uninitialized_fill(data, limit, val); } First, we use alloc to allocate the memory for our new object. The allocate function returns a pointer to the allocated memory, this is data. Next, we set up limit and avail according to the invariants. Finally, we use uninitialized_fill to initialize the allocated memory to the value val.

65 create The final version of create is used by the copy constructor and operator=. It is used create a Vec as a copy of an existing Vec. template void Vec ::create(const_iterator i, const_iterator j) { data = alloc.allocate(j - i); limit = avail = std::uninitialized_copy(i, j, data); }

66 create template void Vec ::create(const_iterator i, const_iterator j) { data = alloc.allocate(j - i); limit = avail = std::uninitialized_copy(i, j, data); } First, we determine how much memory we will need and allocate it, setting data. Second, we use uninitialized_copy to make the copy and set up limit and avail.

67 create Notice that the three different versions of create do not allocate any more memory than is needed. This makes sense because often we make a copy of something and use it to process information but never need to make it bigger. Think about all the copying done in passing parameters and return values. A lot of uses of the Vec class would not involve calling push_back.

68 uncreate The uncreate function needs to run destructors on all the elements and then free up the space in the array. template void Vec ::uncreate() { if (data) { iterator it = avail; while (it != data) alloc.destroy(--it); alloc.deallocate(data, limit - data); } data = limit = avail = 0; }

69 uncreate If there is no data then there is nothing to do. The function creates an iterator at the back of the initialized array. It then loops backward through the initialized array, calling alloc.destroy on each element. Notice the pre-decrement. This is important here. Once all the elements are destroyed, the array is freed using alloc.deallocate so it can be used for something else later.

70 grow We call grow when we are getting ready to add an element to our Vec. The push_back function checks to see if there is room for another element; if there is not is calls grow. grow then allocates an array big enough to hold all the old data plus the new data. It then copies all the old entries into new array. data, limit and avail are all assigned values in the new array. Finally the old array is deallocated.

71 grow We do not want to do this too often because making the copy is expensive in time. To reduce the number of calls to grow we will double the amount of allocated memory. The reasoning here is that longer Vec are more likely to have a larger number of elements added to them.

72 grow template void Vec ::grow() { size_type new_size = max(2 * (limit - data), ptrdiff_t(1)); iterator new_data = alloc.allocate(new_size); iterator new_avail = std::uninitialized_copy(data, avail, new_data); uncreate(); data = new_data; avail = new_avail; limit = data + new_size; }

73 grow If the Vec is empty then data = limit = 0. This is why we compare select the max of the difference between these and 1 when picking the new size. Next we allocate memory for our new array. We copy the old array into the new one using uninitialized_copy. Note we need iterators to both array to do this. Finally, we deallocate the old array using uncreate and reassign data, avail and limit to the new larger array.

74 unchecked_append The unchecked_append function adds a new entry to the array. It assumes that there is room for this new entry— this is why it is “unchecked”. We need to be sure to check to see if there is room before calling this function. template void Vec ::unchecked_append(const T& val) { alloc.construct(avail++, val); }

75 Homework Chapter 11 (page 210) Total 15 pts possible. – 11-0 – 11-1 (paper, 7 pts) – 11-3 (paper, 5 pts) – 11-6 (email, 15 pts)

76 Project 3 11-5 Add static variables to the Student_info class. (e.g. static int myVar = 0; ) These variables are shared among all the objects created. You will need to create constructors, a copy constructor, an assignment operator, and a destructor that all increment the static variables. At the end of the program you can print out the static variable values. If they are public you can get at them using the class name (e.g Student_info::myVar ). Write a brief report summarizing your findings. (email, 35 pts)


Download ppt "Chapter 11 Defining Abstract Data Types. Objectives Learn how to use create objects that employ dynamic memory management. Explore creating objects that."

Similar presentations


Ads by Google