..........
|
![]() |
Have a Great Spring Break! |
Here are some hints on how to think about objects as an abstraction. These will be immediately useful in your problem sets, but also in design projects you encounter in the future.
Interface Design
As mentioned before, when we work with mutable data we have to start dealing with some thorny issues. The most thorny is the question of Reference vs Value or identity vs value. When we pass an object to some function, we need to consider whether that function needs to be able to make a permanent change to the object. If it does then we must give it the "identity" of the object - in other words pass by reference.
Passing By Reference Vs Value (in C++)
We define a class related helper function (i.e. private static)
Void
Dog::testmutation( Dog &dogbyref, Dog dogbyvalue){
// notice the syntax (we treat both argument names similarly)
dogbyref.setOwner("mutated owner for dogref");
dogbyvalue.setOwner("mutated owner for dogvalue");
}
Now we call this from within testsuite
void
Dog::testsuite() {
cout << "\n TESTING REFERENCE vs VALUE \n";
Dog newdog("Juno", "Samoyed");
Dog foo("Harvey", "Retriever");
newdog.setOwner("Matt Welsh");
foo.setOwner("Radhika Nagpal");
cout << "Juno owner: " << newdog.getOwner() << endl;
cout << "Harvey owner: " << foo.getOwner() << endl;
Dog::testmutation(newdog,foo);
cout << "Juno owner: " << newdog.getOwner() << endl;
cout << "Harvey owner: " << foo.getOwner() << endl;
}
What will happen?
The above code behaves similar to expectations. Both foo and newdog are created within the scope of testsuite. But when testsuite calls testmutation, it gives the identity of newdog and the value of foo. What this means is that foo is "copied" into the scope of testmutation. Within the scope of testmutation, both input arguments are modified. However the modification for the second argument was on a copy of foo. Thus when testmutation completes, that modified copy is lost.
This gives us one way to indicate whether we intend mutability (reference) or not (value). However this copying can be very costly... especially as out objects get more complex and large. It can also get complicated because as we create more complex objects we will have to create copy constructors to explain to the compiler exactly what it means to copy something. Finally, sometimes we mean for something unique to be unchangeable (like a filename) and the idea of copying to prevent that is not the right idea.
Instead though, if we pass by reference we don't have to copy but we expose ourselves to unexpected change. It would still be nice if we could indicate that an object is meant to be treated as immutable. To do this we can use the keyword const. One way of thinking about const is that you are telling the compiler that your intention is for the data not to be modified and the compiler can check whether this is true.
DOG INTERFACE
string getName();
string getName2() const; // This method promises to not modify selfobject
DOG IMPLEMENTATION
string Dog::getName2() const {return name;}
USAGE
void testconst(const Dog &dogin){
// return dogin.getName();
// The above returns an error since getName *could* modify dogin
return dogin.getName2();
}
Moral of the Story: In C++ we encourage that you to pass objects by reference. There are many subtle effects that can break by incorrect copying. Instead, you can use const to protect important data -- however you pay a different cost because you have to disallow any functions that may mutate their input, which means alot of consts floating around. But sometimes we can design interfaces to be better about this, for example all selectors here could be declared const. We will see more about const as we go along.
Another thing to keep in mind is the "scope" of the names and identities of objects you create -- where and when are they meaningful? Here are two things to keep in mind
Dog::testsuite(){
Dog foo("Harvey", "Retriever");
Dog *fooref = new Dog("Rufus", "Saint Bernard");
foo.printInfo();
fooref->printInfo();
delete fooref;
}
Within testsuite, I can call other functions and pass foo by reference (e.g. testmutation)
At the end of testsuite, foo is automatically deleted.
Within testsuite, I can use fooref very similarly.
But At the end, I must delete fooref otherwise I have a memory leak
Or I could have testsuite return fooref
and someone else can remember and be responsible for deleting it
Its important to use the second method sparingly, because it will
require you to do alot more management of memory yourself. However as
we will see the second method is very important in constructors that
need to dynamically allocate memory that must persist after the
constructr is finished.In that case it is important to create an
appropriate destructor that deletes the memory allocated by construct.
In the last lecture, we saw some things we could do in object-oriented programming.
Classes can also be used to act as group for related functionlity. In this case we are never meant to create objects of this class:
class Mathclass {
public:
static int sqrt(int i);
static int exponentiate(int i, int n);
private:
Mathclass(); // the constructor is declared private
}
USAGE Mathclass::sqrt(5);
Building good classes and objects are important, but the key power
comes from the ability to compose objects in different ways -- without
losing encapsulation (i.e. abstraction barriers). For example, we can
create:
By creating objects that have different relationships to each other, we can design complex things and still have modularity and clarity.
ADDRESS INTERFACE
Class Address {
public:
// CONSTRUCTORS AND DESTRUCTORS
Address();
Address(string street, string city, string zip);
~Address();
string getAddress(); // SELECTORS
void setAddress(string street, string city, string zip); // MUTATORS
void printAddress(); // METHODS
static void testsuite(); // TESTING
private:
string street;
string city;
string zip;
// NOTE: STATIC INFORMATION
// I could associate some static information with this "class"
// E.g. a list of valid zipcodes, or a ptr to a database for checking addresses
};
ADDRESS IMPLEMENTATION
// Constructors
//---------------
// Rather than duplicate effort and code over several functions
// we will keep all the effortin one function called setAddress (why?)
Address::Address(){
setAddress("Not assigned", "Not assigned", "Not assigned");
}
Address::Address(string astreet, string acity, string azip){
setAddress(astreet,acity,azip);
}
// DESTRUCTORS
Address::~Address() {} // default
// SELECTORS
string
Address::getAddress(){
// string are interesting, and easy to use
// "+" implies concatenation
string tmp = (street + ", " + city + ", " + zip);
return tmp;
}
// MUTATORS
void
Address::setAddress(string astreet, string acity, string azip){
street=astreet;
city=acity;
zip=azip;
// Note: this is a very simplistic class implementation
// I could for example check for a valid zip code (or "Not Assigned")
}
// METHODS
void
Address::printAddress(){
cout << "ADDRESS: " << getAddress() << endl;
}
// TESTING
void
Address::testsuite(){
Address addr;
addr.printAddress();
addr.setAddress("Oxford Street", "Cambridge", "02139");
addr.printAddress();
}
Now we can use this address object in our Dog Class
DOG INTERFACE
public:
string getAddress();
void setAddress(string street, string city, string zip);
private:
Address addr;
DOG IMPLEMENTATION
string Dog::getAddress() {return addr.getAddress();}
void Dog::setAddress(string street, string city, string zip){
addr.setAddress(street, city, zip);
}
Why is it useful to have a separate Address Class, rather than have street, city and zip fields directly in Dog Class?
So far we have seen how to create objects, and how objects can include other objects. Now lets build another slightly more complicated type of data structure -- a "collection" -- whose purpose in life is to collect other objects. Arrays, lists, queues, etc are all examples of this type of data structure. In Scheme, lists were a powerful and natural data structure. In C, arrays are a powerful and natural data structure but have lots of problems. In C++, we will use vectors which are a class that implement some nice features we might have liked with arrays
Nice Array (or Arrays that don't bite!)
How would we design such a class, given C like arrays? Actually its not that different from designing other classes. We simply hide things behind private and only expose to the public the functions we'd like them to use. In C++, we are actually provided with a special data structure called vector that will allow us to do many of the things above. Here is how we can use a vector
VECTORS
// CREATE AN EMPTY VECTOR AND ADD ELEMENTS
vector<string> SS;
cout << SS.size() << endl; // size should be zero
SS.push_back("try"); // adds new element at end (increasing the size)
cout << SS.size() << endl; // size is now 1
// CREATE A VECTOR OF SIZE 10 WITH DEFAULT ELEMENTS
vector<string> SS2(10,"none");
cout << SS2[2] << endl; // access nth element
SS2[2] = "hello"; // assign the nth element
cout << SS2[2] << endl;
SS2[0] = "first"; // vector bounds
SS2[SS2.size() - 1] = "last";
cout << SS2.front() << endl; // better way to access first/last
cout << SS2.back() << endl;
cout << endl;
ITERATORS
// Method 1: naive forloop over indices
cout << endl << "START LISTING ELEMENTS (forloop)" << endl;
for(int i=0; i < SS2.size(); i++) {
cout << SS2[i] << endl;
}
// Method 2: iterator abstraction ("foreach" abstraction)
// For many complex containers it should be possible to ITERATE through the elements
// it would be nice to have a common abstraction for this idea that
// makes it easy for the programmer to use (pointers are not easy...)
// think of the iterator (like SSiter below) as a well behaved pointer
cout << endl << "START LISTING ELEMENTS (iterator)" << endl;
vector<string>::iterator SSiter;
for(SSiter = SS2.begin(); SSiter != SS2.end(); SSiter++) {
cout << *SSiter << endl;
}
}
The vector class has many of the properties that we asked for before: we can create it with default values, we can grow and shrink it on will, it checks bounds and provides protection. These are things we can alrady do. But it also allows us to give it type as a parameter. Vector is implemented in C++ using something called templates that allow us to create classes that can operate over many types. As we will see next lecture, there are several ways to achieve this type of functionality.
For now, we can also see how to create vectors of Dogs. Dogs are more complicated that integers and strings, they are "mutable". So we can experiment with how it works.
// VECTORS OF DOGS
//------------------
cout << endl << "VECTORS OF DOGS" << endl;
vector <Dog> dogshow; // create a vector of Dog objects
Dog brownie("brownie","poodle"); // Create a dog
dogshow.push_back(brownie); // what happens when we add it to the vector?
// TESTING REFERENCE VS VALUE
// Did we add a direct reference to brownie or did we copy brownie?
// Will both brownie and the dogshow[0] change?
// ANS: ??
dogshow[0].setOwner("NEWGUY");
brownie.printInfo();
dogshow[0].printInfo();
// Another way to add a Dog without creating a temporary name
dogshow.push_back(Dog("blackie","poodle"));
dogshow[1].printInfo();
// Note that all the dog objects (including blackie) and their memory
// will dissappear once we exist this function (i.e. testdogvectors).
// TESTING ITERATORS
cout << endl << "TESTING ITERATORS (VECTORS OF DOGS)" << endl;
vector<Dog>::iterator showiter;
for(showiter = dogshow.begin(); showiter != dogshow.end(); showiter++) {
// note that this is effectively similar to dogshow[index].setAddress
(*showiter).setAddress("Wardrobe","Spare","Oom");
(*showiter).printInfo();
}
Instead of vectors of Dogs, we can create vectors with references to Dogs. This is especially useful when the goal is to have vectors of existing objects that are managed by someone else.
// VECTOR OF POINTERS TO DOGS
//----------------------------
// Another way of doing things except with some subtle differences
cout << endl << "VECTORS OF DOG POINTERS" << endl;
vector <Dog *> dogshow2;
Dog scooby("scooby","mutt"); // Create a dog
dogshow2.push_back(&scooby); // Adding a reference to scooby
dogshow2[0]->setOwner("NEWGUY");
// Will scooby change? (ANS: ??)
scooby.printInfo();
dogshow2[0]->printInfo();
// Another way to add a Dog, by allocating global memory
// But this one is more problematic....
dogshow2.push_back(new Dog("benji","mutt"));
dogshow2[1]->printInfo();
// We are now responsible for deleting this memory (benji)
// and being careful not to try and use it after the delete
// note that scooby does not suffer from the same problem
delete dogshow2[1];
}
Now we can create a higher level class, DogShow, that contains a collection of Dog records. Internally DogShow may be implemented as a vector. But we may choose to expose only as much of the vector interface as we want.
class DogShow {
public:
// CONSTRUCTORS
DogShow(string showname);
// DESTRUCTORS
~DogShow();
// MUTATORS
// Add a new dog, but must supply all the info at add time
// Cannot modify any dog records because I won't give you access
void addNewDog(string name,
string breed,
string owner,
DogCategory category);
// METHODS
int totalDogs();
void printShowInfo();
// other possibility: allow an iterator over dogshow
// TESTING
static void testsuite();
static void testvectors();
static void testDogVectors();
private:
string showname;
vector<Dog> ivec;
int total;
}