Just like in C, we can create objects on either the stack or the heap in C++. If we use the keyword "new", that means the object will be on the heap.
String s1("foo"); // on the stack
String s2 = new String("bar"); // on the heap
We wrote a BankAccount class which helped illustrate a few more things about classes in C++:
Static fields. A bank account has an associated ID number, which is automatically generated when the account is created. To accomplish this behavior, we added a static field accountCount_ to the BankAccount class. The "static" keyword means that there is only ONE variable for all objects of this class, not one per object like normal fields. Therefore we can use the accountCount_ to generate a unique ID number in the constructor by using that count as the ID and then incrementing the count by 1. The static field also has to be initialized in the .cpp file (see files for details).
Deleted constructors. Remember from last lecture that C++ automatically generates a "copy constructor" for your class if you do not provide one. But for a bank account, making copies of the account would be a really bad thing! We don't want to allow bank accounts to be copied, so we can declare a copy constructor in the header file and set that constructor "= delete;", which means we "delete" it and prevent it from being used anywhere in the code.
Pure virtual functions. A "bank account" is a general concept; if you go into the bank and ask to open a bank account, they'll ask "what kind?" There are savings accounts, checking accounts, retirement accounts, etc. Each of these types of accounts has a slightly different implementation of withdrawal. All accounts do have the ability to withdraw, but since each type of account has a different implementation, we'll declare the withdraw() function but NOT provide an implementation. We do this as follows by declaring the function "virtual" and setting it equal to 0:
virtual void withdraw(int amount) = 0;
This is called a "pure virtual" function, and it make this BankAccount class equivalent to Java's abstract class! Any subclass of BankAccount will have to implement the withdraw() function.As we discussed last lecture, one of the keystons of object-oriented programming is inheritance, or the sharing of behavior from a "base class" to a "derived class" (this is the terminology we use in C++). We did a demonstration of how this works by writing a SavingsAccount class.
class SavingsAccount : public BankAccount {
public:
SavingsAccount(double interestRate, std::string name);
double getInterestRate() const;
// Deducts the amount from this account, but throws a runtime_error if there
// are no more available transactions for the month.
virtual void withdraw(int amount) override;
private:
double interestRate_;
int numTransactionsInMonth_;
};
Consider the following mystery problem: what does the program print?
#include <iostream>
using namespace std;
class A {
public:
A() { cout << "a()" << endl; }
~A() { cout << "~a" << endl; }
void m1() { cout << "a1" << endl; }
void m2() { cout << "a2" << endl; }
};
class B : public A {
public:
B() { cout << "b()" << endl; }
~B() { cout << "~b" << endl; }
void m1() { cout << "b1" << endl; }
void m2() { cout << "b2" << endl; }
};
int main() {
B* x = new B();
x->m1();
x->m2();
delete x;
}
The output:
a()
b()
b1
b2
~b()
~a()
We see here another fact about inheritance: constructors and destructors EXTEND, instance functions OVERRIDE. When we created a new B, the A() constructor was called AUTOMATICALLY before the B() constructor was called, just as the destructor for A was called automatically after the destructor for B. The base class always bookends the constructor/destructor call. The instance functions (m1/m2), on the other hand, do NOT extend, but replace the behavior in the base class.
If we modify the variable type of X, does anything change?
int main() {
A* x = new B();
x->m1();
x->m2();
delete x;
}
x is now of type A, which is allowed because B extends A. Let's see the output. We would expect that the output would be the same as before.
a()
b()
a1
a2
~a()
What happened?!? We see the constructors are called as expected, but then the rest of the functions are just A's functions, not B's! But the object was of type B.
We said that objects are DATA and BEHAVIOR. We know how to represent data - it's basically the same as structs in C - but how do objects have behavior? Where in memory do those behaviors live?
One option is that the code for the behavior lives in the object itself. So in memory, the object x might be represented as
int m
int n
code for m1
code for m2
with the code stored as if it was data in the object itself. However this is not optimal because if this is the case, then EVERY OBJECT has a COPY of the code inside of it - we're wasting a lot of memory, because for a given class, the code will be the same for all objects of that class.
Instead, we could store pointers to code!
int m
int n
pointer to m1's code
pointer to m2's code
Now each object has just a pointer to the code, which is much smaller than the code itself. But still, this isn't optimal! We are repeating all the pointers for all the functions in every object; what if there are many functions? That will be a lot of pointers. Instead, we can take advantage of the fact that for a particular class, all of the functions will be the same. We can have a single "function table" that stores the functions for the class, and then for a particular object we will look up the code in the appropriate function table.
Now we can begin to understand why the mystery did not perform as expected. When the variable was of type A, we looked up the function table for type A and called the function of A.
However, we really want to execute B's m1 and m2 regardless of the type of the variable storing the object! How can we do this? C++ adds the "virtual" keyword which is a special signal in the function table that indicates "hey, even if the variable is of type A, you really need to go look at the function table of the ACTUAL OBJECT TYPE and not the variable." The virtual specifier is essentially a flag that modifies the act of resolving the function in the function table. We can fix our example like so:
#include <iostream>
using namespace std;
class A {
public:
A() { cout << "a()" << endl; }
virtual ~A() { cout << "~a" << endl; }
virtual void m1() { cout << "a1" << endl; }
virtual void m2() { cout << "a2" << endl; }
};
class B : public A {
public:
B() { cout << "b()" << endl; }
virtual ~B() { cout << "~b" << endl; }
virtual void m1() override { cout << "b1" << endl; }
virtual void m2() override { cout << "b2" << endl; }
};
Now it will output what we expect:
a()
b()
b1
b2
~b()
~a()
Destructors should ALWAYS be declared virtual. Why? Because if there is any data that needs to be cleaned up in a derived class, it is VITAL that the derived class's constructor is called regardless of the type of the variable.
The proper terminology for what we've just described is the "virtual function table", and the act of looking up which code to execute is called "dynamic dispatch".
We talked briefly about the "auto" keyword and lambdas; look into these more on your own if you are interested. Modern C++ has a ton of extensions.