390 likes | 403 Views
Learn about aggregation and pointers in object-oriented programming, including relationships between classes, constructors for embedded objects, and examples like a Timer and Soda Machine. Gain insights into pointers, dynamic memory allocation, and more.
E N D
Recitation Week 6 Object Oriented Programming COP3330 / CGS5409
Today’s Recitation • Aggregation / Composition • Pointer Review • Dynamic Memory Allocation
Aggregation / Composition • Aggregation (or composition) is a relationship between objects • Implemented by embedding an object of one class type (or a pointer or reference) inside as member data of another class type • This is the idea of objects embedded inside other objects (components making up the "aggregate")
Aggregation / Composition • Often known as the "has-a" relationship: • We might place an Engine object inside a Car object as member data, because a "car has an engine" • We could place 52 Card objects as member data of class Deck, because a "deck has 52 cards"
Timer Example • class Timer • A Timer object consists of two Displays; one for hours and one for minutes • class Display • A Display object stores and displays a single integer.
Class Display class Display { public: • Display(intlim); // Initialize a new Display object. void increment(); // Add 1 to value. boolsetValue(intval); // Set the value. intgetValue() const; // Return the current value. intgetLimit() const; // Return the limit void show() const; // Show the current value. private: constint LIMIT; // largest possible value int value; // current value (0 .. limit - 1) };
Class Timer class Timer { public: Timer(); // Initialize a new Timer object 00:00 Timer(int h, int m); // Initialize a new Timer object // to h hours and m minutes void increment(); // Add 1 minute to timer. bool set(int h, int m); // Set Timer to h hours and m minutes void show() const; // Show hours and minutes. Timer add(const Timer& t) const; // add two Timer objects together private: Display hours, // two displays, one for hours, minutes; // and one for minutes };
Relationship between Classes • Two Display objects are used as member data in the Timer class declaration – variables: hours and minutes class Timer { public: // public member functions private: // Display objects declared as private data of a Timer object Display hours, minutes; };
Context Matters! • Always pay attention to context, including who is the intended "user" of any given class. In the case of aggregation, the containing class is the "user" of the embedded objects. • The user of the Timer is the main program (so the main program will be calling the timer's functions) • The user of the Display objects is the Timer object (so the Timer object will be calling Display functions through the objects hours and minutes, because those variables are in scope)
Constructors for Embedded Objects • When an object is created, its constructor runs, but also must invoke the constructors of any embedded objects. • If nothing special is done, it will invoke the default constructor, if there is one. • To invoke a constructor with parameters for an embedded object, use the initialization list
Another Example: Soda Machine • A SodaMachine object is a that contains (and handles communications among) other embedded objects: • A ChangeCounter, to manage transactions • 5 Dispenser objects, for each type offered • Full source code: • http://www.cs.fsu.edu/~jestes/cop3330/examples/machine/
Class CoinCounter class CoinCounter { public: CoinCounter(int initial = 100); // Initialize a counter, setting // available change. intCurrentAmount(); // Report amount tendered so far. void AcceptCoin(intamt); // Handle coin insertion. void TakeAll(); // Accept all coins in response to // a sale. void DispenseChange(intamt); // Return change, if possible. private: int amount; // the amount tendered so far int available; // the amount available for // making change };
Class Dispenser class Dispenser { public: Dispenser(intnum = 24); // Initialize with // default numcans. boolHandleButton(); // Try to make a sale private: intnumCans; // the number of cans // available };
Class SodaMachine class SodaMachine { public: SodaMachine(); // Initialize a machine. void DoCommand(char cmd); // process a command from the user private: CoinCounter counter; // A soda machine contains a coin dispenser, Dispenser cola, // five can dispensers, lite, root, orange, water; int price; // and the price of a can of soda. void DoCoin(char cmd); // Handle a coin event. void DoSelection(char cmd); // Handle a drink button press. };
What is a Pointer? • A pointer is a variable that stores a memory address. Pointers are used to store the addresses of other variables or memory items. Pointers are very useful for another type of parameter passing, usually referred to as Pass By Address. Pointers are essential for dynamic memory allocation.
Declaring Pointers • Pointer declarations use the * operator. They follow this format: typeName * variableName; int n; // declaration of a variable n int * p; // declaration of a pointer, // called p • In the example above, p is a pointer, and its type will be specifically be referred to as "pointer to int", because it stores the address of an integer variable. We also can say its type is: int*
Declaring Pointers • The type is important. While pointers are all the same size, as they just store a memory address, we have to know what kind of thing they are pointing TO. double * dptr; // a pointer to a double char * c1; // a pointer to a character float * fptr; // a pointer to a float • Note: Sometimes the notation is confusing, because different textbooks place the * differently. The three following declarations are equivalent: int *p; int* p; int * p; • All three of these declare the variable p as a pointer to an int.
Declaring Pointers • Another tricky aspect of notation: Recall that we can declare mulitple variables on one line under the same type, like this: int x, y, z; // three variables of type int • Since the type of a "pointer-to-int" is (int *), we might ask, does this create three pointers? int* p, q, r; // what did we just create? • NO! This is not three pointers. Instead, this is one pointer and two integers. If you want to create mulitple pointers on one declaration, you must repeat the * operator each time: int* p, * q, * r; // three pointers-to-int int* p, q, r; // p is a pointer, q and r // are ints
Pointer Dereferencing • Once a pointer is declared, you can refer to the thing it points to, known as the target of the pointer, by "dereferencing the pointer". To do this, use the unary * operator: int * ptr; // ptr is now a // pointer-to-int // Notation: // ptr refers to the pointer itself // *ptr the dereferenced pointer – // refers now to the TARGET
Pointer Dereferencing • Suppose that ptr is the above pointer. Suppose it stores the address 1234. Also suppose that the integer stored at address 1234 has the value 99. cout << "The pointer is: " << ptr; // prints the // pointer cout << "The target is: " << *ptr; // prints the // target // Output: // The pointer is: 1234 // exact printout varies // The target is: 99 • Note: the exact printout of an address may vary based on the system. Some systems print out addresses in hexadecimal notation (base 16). • pdeclare.cpp -- an example illustrating the declaration and dereferencing of pointers
Pointer Dereferencing • Pointers don't always have valid targets. A pointer refers to some address in the program's memory space. • A program's memory space is divided up into segments • Each memory segment has a different purpose. Some segments are for data storage, but some segments are for other things, and off limits for data storage • If a pointer is pointing into an "out-of-bounds" memory segment, then it does NOT have a valid target (for your usage) • IMPORTANT: If you try to dereference a pointer that doesn't have a valid target, your program will crash with a segmentation fault error. This means you tried to go into an off-limits segment • Don't dereference a pointer until you know it has been initialized to a valid target!
Null Pointer • There is a special pointer whose value is 0. It is called the null pointer • You can assign 0 into a pointer: ptr = 0; • The null pointer is the only integer literal that may be assigned to a pointer. You may NOT assign arbitrary numbers to pointers: int * p = 0; // okay. assignment of null pointer to p int * q; q = 0; // okay. null pointer again. int * z; z = 900; // BAD! cannot assign other literals to pointers! double * dp; dp = 1000; // BAD!
Null Pointer • The null pointer is never a valid target, however. If you try to dereference the null pointer, you WILL get a segmentation fault. • So why use it? The null pointer is typically used as a placeholder to initialize pointers until you are ready to use them (i.e. with valid targets), so that their values are known. • If a pointer's value was completely unknown -- random memory garbage -- you'd never know if it was safe to dereference • If you make sure your pointer is ALWAYS set to either a valid target, or to the null pointer, then you can test for it: if (ptr != 0) // safe to dereference cout << *ptr; • pnull.cpp -- example illustrating the null pointer
Pointers of the same type • It is also legal to assign one pointer to another, provided that they are the same type: int * ptr1, * ptr2; // two pointers of type int ptr1 = ptr2; // can assign one to other • Although all pointers are addresses (and therefore represented similarly in data storage), we want the type of the pointer to indicate what is being pointed to. Therefore, C treats pointers to different types AS different types themselves. int * ip; // pointer to int char * cp; // pointer to char double * dp; // poitner to double
Pointers of the same type • These three pointer variables (ip, dp, cp) are all considered to have different types, so assignment between any of them is illegal. The automatic type coercions that work on regular numerical data types do not apply: ip= dp; // ILLEGAL dp= cp; // ILLEGAL ip= cp; // ILLEGAL • As with other data types, you can always force a coercion by performing an explicit cast operation. With pointers, you would usually use reinterpret_cast. Be careful that you really intend this, however! ip= reinterpret_cast<int* >(dp);
The "address of" operator • Recall, the & unary operator, applied to a variable, gives its address: int x; // the notation &x means "address of x" • This is the best way to attach a pointer to an existing variable: int* ptr; // a pointer intnum; // an integer ptr= # // assign the address of num into // the pointer // now ptr points to "num"! • pinit.cpp -- example illustrating pointer initialization with the address-of operator, and of assigning pointers to other pointers
Memory Allocation • There are two ways that memory gets allocated for data storage: • Compile Time (or static) Allocation • Memory for named variables is allocated by the compiler • Exact size and type of storage must be known at compile time • For standard array declarations, this is why the size has to be constant • Dynamic Memory Allocation • Memory allocated "on the fly" during run time • dynamically allocated space usually placed in a program segment known as the heap or the free store • Exact amount of space or number of items does not have to be known by the compiler in advance. • For dynamic memory allocation, pointers are crucial
Dynamic Memory Allocation • We can dynamically allocate storage space while the program is running, but we cannot create new variable names "on the fly" • For this reason, dynamic allocation requires two steps: • Creating the dynamic space. • Storing its address in a pointer (so that the space can be accessed) • To dynamically allocate memory in C++, we use the new operator.
Dynamic Memory De-allocation • De-allocation is the "clean-up" of space being used for variables or other data storage • Compile time variables are automatically de-allocated based on their known extent (this is the same as scope for "automatic" variables) • It is the programmer's job to de-allocate dynamically created space • To de-allocate dynamic memory, we use the delete operator
Allocating space with new • To allocate space dynamically, use the unary operator new, followed by the type being allocated. new int; // dynamically allocates an int new double; // dynamically allocates a double • If creating an array dynamically, use the same form, but put brackets with a size after the type: new int[40]; // dynamically allocates an array of 40 ints new double[size]; // dynamically allocates array of size doubles • Note that the size can be a variable!
Allocating space with new • These statements above are not very useful by themselves, because the allocated spaces have no names! BUT, the new operator returns the starting address of the allocated space, and this address can be stored in a pointer: int * p; // declare a pointer p p = new int; // dynamically allocate an int and load address into p double * d; // declare a pointer d d = new double; // dynamically allocate a double and load address into d // we can also do these in single line statements intx = 40; int * list = new int[x]; float * numbers = new float[x+10]; • Notice that this is one more way of initializing a pointer to a valid target (and the most important one).
Accessing dynamically created space • So once the space has been dynamically allocated, how do we use it? • For single items, we go through the pointer. Dereference the pointer to reach the dynamically created target: int * p = new int; // dynamic integer, pointed // to by p *p = 10; // assigns 10 to the // dynamic integer cout << *p; // prints 10
Accessing dynamically created space • For dynamically created arrays, you can use either pointer-offset notation, or treat the pointer as the array name and use the standard bracket notation: double * numList = new double[size];// dynamic array for (int i = 0; i < size; i++) numList[i] = 0; // initialize array vars to 0 numList[5] = 20; // bracket notation *(numList + 7) = 15; // pointer-offset notation // means same as numList[7]
De-allocation of dynamic memory • To deallocate memory that was created with new, we use the unary operator delete. The one operand should be a pointer that stores the address of the space to be deallocated: int* ptr = new int; // dynamically created int // ... delete ptr; // deletes space that ptr points to • Note that the pointer ptr still exists in this example. That's a named variable subject to scope and extent determined at compile time. It can be reused: ptr= new int[10]; // point p to a brand new array • To de-allocate a dynamic array, use this form: delete [] name_of_pointer;
De-allocation of dynamic memory • Example: int * list = new int[40]; // dynamic array delete [] list; // de-allocates the array list = 0; // reset list to null pointer • After de-allocating space, it's always a good idea to reset the pointer to null unless you are pointing it at another valid target right away. • To consider: So what happens if you fail to de-allocate dynamic memory when you are finished with it? (i.e. why is de-allocation important?)
Application Example: Dynamically resizing an array • If you have an existing array, and you want to make it bigger (add array cells to it), you cannot simply append new cells to the old ones. • Arrays are stored in consecutive memory, and you never know whether or not the memory immediately after the array is already allocated for something else. For that reason, the process takes a few more steps. Here is an example using an integer array. Let's say this is the original array: int * list = new int[size];
Application Example: Dynamically resizing an array • Suppose I want to resize this so that the array called list has space for 5 more numbers (presumably because the old one is full). • There are four main steps. • 1. Create an entirely new array of the appropriate type and of the new size. (You'll need another pointer for this). int* temp = new int[size + 5]; • 2. Copy the data from the old array into the new array (keeping them in the same positions). This is easy with a for-loop. for (int i = 0; i < size; i++) temp[i] = list[i];
Application Example: Dynamically resizing an array • 3. Delete the old array -- you don't need it anymore! delete [] list; // deletes array pointed to by "list" • 4. Change the pointer. You still want the array to be called "list" (its original name), so change the list pointer to the new address. list = temp; • That's it! The list array is now 5 larger than the previous one, and it has the same data in it that the original one had. But, now it has room for 5 more items.