Important Correction
In the two versions of this article before November 24, 2014, I'd stated that virtual destructors weren't necessary in what was the HumanInteractions8/HumanInteractions9 example. That was incorrect, and I hope earlier readers are somehow made aware of my mistake so they don't make it themselves. I'm very sorry for the mis-statement.
Introduction
Throughout the years I've seen many people say teaching Object Oriented Programming (OOP) fundamentals with C++ isn't recommended because of the language's complexity. Java seems to be the standard, and while I have no gripe with the choice, I still find myself rolling my eyes every time I hear the first assertion.
C++ tutorials seem to be a dime a dozen, and other languages are more talked about these days, but I feel the urge to write a small-ish C++ OOP tutorial for a beginner, that gets to the heart of object orientation. I won't go into much of the complexities, because an introduction should not be something that focuses on items which aren't necessary. Tutorials of this nature aren't common, as far as I've seen, so let us begin.
If you are a beginner, skip to the next section now. The following paragraphs are for more advanced programmers to understand my approach.
In many ways, this is a response to articles like Akhil Mittal's, and several other OOP introductions I've read, as well as the claim against C++. As such, I've endeavored to keep jargon to an absolute minimum, and focus on things a beginner will need to know to start understanding the bigger C++ programming picture. I believe there is one programming book that takes an approach similar to the following, but I have forgotten the title. Everything else I've come across has not been beginner friendly in my opinion, and even though the following might also have a few rough spots, I've tried to make it something a new programmer can turn to for a relatively quick grasp of the necessary concepts.
Like many, my programming interest began long ago. Back then learning was often a matter of copying code from a magazine, and then running it and fixing all the typos introduced because of the tediousness of the manual typing. Surprisingly, even though the magazines were semi-professional, their code often contained bugs. Because of that, and other issues, many of those examples were initially over my head, but the iterative process of beating my noggin into a wall eventually brought insight. I believe that is the nature of true learning, and I hope the following pays tribute to that heritage without introducing the bugs that prolonged my own learning process (but also made it deeper).
Preliminaries
A programmer needs two things: a computer and a compiler. If your computer's Operating System (OS) is Windows and you are being cheap, I recommend Microsoft's Visual Studio Community Edition (VSCE). For the price of nothing Microsoft has made an absolutely fantastic tool available for our use. If you use another OS, I can't give any recommendations due to my own unfamiliarity with those platforms.
Once you have your compiler set up by using the standard installation procedure, the next step is to create a project in it. To do this in VSCE go to 'File -> New Project,' and select a 'Visual C++ -> General -> Empty Project.' Place it wherever you want on your system, although I recommend creating a forward-thinking subdirectory structure at this point. Of course, without experience you won't know what a forward-thinking approach is.
My recommendation is to place the projects for this tutorial into a subdirectory called "[User]\Programs\MyProgs\InitialLearning." For the following, one project will suffice, but you can make each step its own project if you wish, with each of them being a subdirectory off of the InitialLearning directory.
On my system the "Programs" subdirectory is only populated with subdirectories, and includes "Backups," "Libraries," "MyProgs," "OthersProgs," "Misc," and "TheoryAndExamples." The ultimate reason behind this layout is something you won't need for our work, but will need in the future. Complex programs almost always require external libraries, and this subdirectory arrangement results in an easier way to find those libs than if they are scattered all over your hard drive. Ultimately, that is fewer mouse clicks in Visual Studio.
Object Oriented Programming (OOP)
The purpose of coding is to solve problems, even if that is only to keep the user entertained for a while. OOP is a method of mirroring the problems in code as directly as possible. The best way to understand this statement is with an example, so let us model one: a human interaction.
For our fictional case we will represent two people talking to each other. All we want to happen is for one person to say "Good morning, Jennie!," and the other to respond, "Hello Joe!" So for the name of this project use "HumanInteraction." When creating a new project in Visual Studio, if you are in the 'InitialLearning' subdirectory mentioned earlier and you create a project named 'HumanInteraction', Visual Studio will create it in a subdirectory named 'HumanInteraction,' so you don't need to manually create the location.
In C++ we use the word class
to represent a desired type of object. We are modeling humans, so the C++ equivalent is class Human
. A routine, sometimes called 'function,' is the method classes use to accomplish tasks. In our case we want one to 'talk' to another, so 'talk' is a good descriptive name for our first function.
Objects have attributes, just like the real world. For instance, the attributes of the previous scenario are the people's names: 'Jennie' and 'Joe.'
Let us begin expressing a 'Human' object in C++, with a 'name' attribute. The following code includes things I haven't introduced yet, but you may be able to understand the fundamental concept of what is going on given the words used. They will be explained afterwards.
class Human {
private:
std::string nameC;
public:
Human(const std::string & name) : nameC(name) { }
};
The first new thing you will notice when reading from top to bottom is the opening brace ({). Braces are the main way to delineate objects and 'scopes' to the compiler. In this case everything between the opening brace and closing brace (the one with the ";" after it) is the definition of the 'Human' class.
By 'scope' in the previous paragraph, I mean that the items declared within the braces will be destroyed at the ending brace unless you take actions to keep that from happening. There might be more on this later, if this writing doesn't become too long.
The next item in the Human
declaration is the private:
. This means that items in that section are only available to the class itself. The person's name has been declared in this section, and a logical first thought upon hearing this is that your name is not 'private' to you - all your friends know it. That is true, but they have had interactions that made your name known to them. Most random strangers will have no idea about your identity unless you write it on a sticker and paste it on yourself.
It is a good idea to make as many class items as possible private
. This keeps other objects from modifying them recklessly. If you made the 'name' public any other object could change it at their whim - something you most definitely don't want!
That 'name' is declared as a std::string
. C++ has a standard library to make your life easier, and items in that library are 'contained' in what is called the std
namespace. I will not go into namespaces here, because it takes us too far from our current topic, and gets us into those 'complexities.' But I will mention that there is a method which enables you to eliminate the std::
in front of string
, and reduce typing. All that is needed is to add a line before the class:
using namespace std;
When you start dealing with header files (which I don't cover here), be aware that it is not a good idea to 'use' namespaces in them like this. You should explicitly specify everything in headers (i.e., "std::vector
"), but using namespace
s in .cpp files is only problematic if they are #include
d in other units as header files, which I, and almost everyone else I've seen, strongly discourage.
(Some people make an exception for the standard library, and do include using namespace std;
in header files, but I recommend having much more experience before you do so. I don't, because I think it's a bad habit.)
I should also explain the C
at the end of nameC
.
Large programs often become difficult to figure out, and it is helpful to be able to tell variables that are a permanent part of the class (such as 'name') from others that are only used within single functions. My method of delineating these is to use the C
at the end of the variable name in order to indicate it is part of the 'Class.' You will often come across code that uses a prepended m_
to do the same thing, where m_
stands for 'member.' A 'C' is fewer keystrokes than 'm_', and fewer strokes are better when you can get by with them, in my opinion.
Of course, this string
is an object we can place the person's name into. In our case, one of the names will be "Joe," and the other will be "Jennie." Strings are just characters placed back to back in a computer's memory.
The next line is public:
. As you might suspect from our private
conversation, items placed in this section can be accessed by other objects.
The Human
in the following line is a very important item. It is known as a 'constructor,' and is the method that must be called in order to create a Human
in the computer's memory. More could be said about constructors at this point, such as they can be made private if you want to make construction possible in a very specific way that usually isn't needed. And they can be overloaded, which we will discuss later.
In our case I've placed a const std::string & name
in parenthesis after Human
. This is an item the caller of the constructor must supply, and it is defined here in order to force the caller to send it. The const
qualifier indicates that the std::string
won't be modified by the routine. That qualification often allows the compiler to optimize the code for better speed and efficiency.
In C++, the item in the parenthesis of our our constructor is called an 'argument.' It isn't really 'arguing' about anything; the word was probably chosen because it meant something like 'talking point.' It is something passed between the 'talker' and the 'talkee.'
Interestingly, I'm unaware of a better word for this concept, even though 'argument' doesn't express the intended meaning very well. The phrase "communication point," which could be abbreviated "compoint" could be better, but 'argument' is accepted, and the word you will need to know. (Merriam-Webster's sixth definition probably originated after programming languages adopted the word.)
Following the closing parenthesis there is a colon, :
. Constructors needed a way of initializing class variables before running any code within the constructor body, in order to be more efficient, and everything between the ":
" and the following brace ({
) are C++'s way of doing that. They are called 'initialization lists.' In this one the nameC
class variable is set to whatever the calling object sets the name
argument to.
Then, following the initialization list, the body of the constructor is defined. In this case the constructor doesn't need to do anything, so it is empty. Thereby the { }
.
Finally, the class definition is closed with the "};
". You can also say that is the end of its scope, to use an earlier word.
Before filling in the talk
function let me introduce a routine required by every program in one way or another: the main
function. C++'s standards require every program to have one.
The main
function begins initiating - and finishes terminating - a majority of the objects and logic used in the program. It is the routine that will create and destroy our Humans
. You can consider it to be the 'God' function if you want.
I said main
initiates and terminates a majority of the objects. The exception to this are global
items, which I also won't go into here.
With all of these preliminaries out of the way, the entire program follows. To use it within Visual Studio, right click on the 'Source Files' filter in the right hand pane (unless you've modified the default screen layout) and select 'Add -> New Item.' Then select the 'C++ file (.cpp)' option. You can leave it named as 'Source,' or rename it 'main' or another name if you wish. Once you press the 'Add' button a new file appears in the editor window. Copy and paste the following into it:
#include <string>
#include <iostream>
class Human {
private:
std::string nameC;
public:
Human(const std::string & name) : nameC(name) { }
std::string name() const { return nameC; }
void talkTo(const Human & person) const {
std::cout << nameC << " says: Hello, " << person.name() << "!" << std::endl;
}
};
int main() {
Human joe("Joe");
Human jennie("Jennie");
joe.talkTo(jennie);
jennie.talkTo(joe);
std::cout << std::endl << "Press 'Enter' to exit";
std::cin.ignore();
return 0;
}
Download HumanInteractions1.zip - 9KB
If you press the 'Local Windows Debugger' button in Visual Studio, you should see the following:
Congratulations! You have just executed your first object oriented program. (Barring copy/paste errors, or other difficulties that should be fixable by going through the directions again.)
I should mention that the const
in the std::string name() const { return nameC; }
line tells the compiler that the function will not change the state of the object in any way. In this case we are simply returning a copy of the nameC
variable, and returning copies doesn't modify the original. Similar to the previously mentioned const
, this allows the compiler to optimize for additional speed in some cases. (The same holds true for the second const
after talkTo
.)
I should also briefly introduce cout
. It is simply the console output, which is the window in the screenshot. In the code, the <<
can be thought of as sending the items following it to the console. For instance, std::cout << std::endl << "Press 'Enter' to exit";
sends an endline (like a 'Return' on an old fashioned typewriter, or 'Enter' in a word processor), followed by the phrase "Press 'Enter' to exit." The <<
after std::endl
allows the following string to be strung together with the std::endl
.
If you want to reduce typing, the following is the program with the using namespace std;
included:
#include <string>
#include <iostream>
using namespace std;
class Human {
private:
string nameC;
public:
Human(const string & name) : nameC(name) { }
string name() const { return nameC; }
void talkTo(const Human & person) const {
cout << nameC << " says: Hello, " << person.name() << "!" << endl;
}
};
int main() {
Human joe("Joe");
Human jennie("Jennie");
joe.talkTo(jennie);
jennie.talkTo(joe);
cout << endl << "Press 'Enter' to exit";
cin.ignore();
return 0;
}
From the preceding discussion you might already have an idea of what is occurring in this program, but let me touch upon a few items.
First, as mentioned previously, everything is initiated in main
. A Human
named "Joe" is created, then "Jennie" is created. Joe 'talks' to Jennie, and she responds.
And note that I changed the function name to talkTo
instead of talk
as I said earlier. The reason is because I realized talkTo
is a more descriptive, richer name than talk
in this case. Better descriptions in the code itself make it easier to debug. The process of changing things this way is called 'refactoring.'
After Jennie and Joe 'talk,' the program waits for you to press 'Enter' so it can terminate.
One question you may have is why "Joe" is also referred to as joe
in the code, without capitalization. The answer is that the "Joe" (in quotes) is the 'attribute' of the computer object joe
. That attribute could be changed to anything, like "Mary", but the computer would still access it through the variable joe
.
Another valid question is why didn't I name the object Joe
instead of joe
? Habit and consistency, in order to make things simple for reading are the reasons.
I, and most other programmers, like to distinguish Object declarations from variables. An easy way to do so is to use Capitals for Object declarations, and camelCase for the instantiation of those declarations in variables. You will come across other methods of approaching this problem in your future, but the method I am presenting is used in many places.
There are also many other ways of indenting code. For example, one of the most used looks like this:
#include <string>
#include <iostream>
using namespace std;
class Human
{
private:
string nameC;
public:
Human(const string & name) : nameC(name) { }
string name() const { return nameC; }
void talkTo(const Human & person) const
{
cout << nameC << " says: Hello, " << person.name() << "!" << endl;
}
};
int main()
{
Human joe("Joe");
Human jennie("Jennie");
joe.talkTo(jennie);
jennie.talkTo(joe);
cout << endl << "Press 'Enter' to exit";
cin.ignore();
return 0;
}
I have my own logical reasons for using the earlier style - namely the indention level automatically delineates everything associated with an indention 'block.' I consider the closing brace to be part of the block, which makes finding the next indention level much simpler. But holy wars have raged over this issue, so when you see other styles, accept them and continue on.
Object Relationships
The preceding program introduced the heart of programming C++ in an object oriented manner. But there is one more topic I am compelled to add, because without an understanding of it your programming knowledge will be limited for some time.
Objects are often related to other objects, just as items in real life are related to other items. Many times you need to create a list of objects that are somehow similar, but not all the same, and then take different actions based upon those differences.
As an example, let us deal with a list of people saying 'Hi' to other people. I will tread on stereotypes, and do so badly, so bear with me, and feel free to correct all the mistakes in the comments.
As you know, almost every culture has its own language, or local accent. In a non-object-oriented programming language, dealing with these differences is usually much more difficult than doing so in object orient languages. In fact, the object-oriented approach gives a huge advantage in ease of coding as well as run time performance in most cases.
Imagine you have a room full of people, and imagine they say 'Hi' to each other. Let us make this into an unlikely scenario, but something that is fairly easy to mentally picture. These people are arranged in a circle, and the first person says "Hi" to all the other people, one at a time, starting with the person on their left, who can be considered to be person number 2. After #1 says "Hi" to everyone, #2 then says "Hi" to #1, then #3, #4, etc, until he/she finishes, and #3 repeats the process, then the next person, until everyone has greeted everyone else.
Now, to throw the final twist into it, instead of "Hi," the talker will use their own cultural word. I won't go into the complete scenarios, because I don't know them (for instance, in Japanese 'David' sounds more like 'Dahvid' if memory serves correctly).
To give an idea of how object orientation eases programming, I need to show the basics of how a non-OO approach tackles the problem. In that approach the program would have to iterate through the people and determine their nationality/culture while performing the talk
routine. C++ is perfectly capable of handling non-OO programs, so the loop to do so would look something like the following. (I've kept some object orientation elements in, to make it easier to understand.)
for (int talkingPerson=0; talkingPerson<numberOfPeople; ++talkingPerson) {
for (int talkedToPerson=0; talkedToPerson<numberOfPeople; ++talkedToPerson) {
if (talkingPerson != talkedToPerson) {
if (talkingPerson.culture() == "Japanese") cout << "Konnichiwa, " <<
talkedToPerson.name();
else if (talkingPerson.culture() == "German") cout << "Hallo, " <<
talkedToPerson.name();
else if (talkingPerson.culture() == "Hawaiian") cout << "Aloha, " <<
talkedToPerson.name();
}
}
}
As I said, I used elements of object orientation for ease of comprehension. It isn't important that you understand everything the code is doing, but it is important to note that the inner portion of the code, consisting of the if
statements (which I've bolded and italicized), quickly becomes very long as more cultures are involved. The computer will have to go through those checks every iteration, which takes a little time. That time is almost insignificant to us, because computers are incredibly fast today, but that time still passes, and can be reduced.
Object orientation greatly simplifies the code. The loop becomes:
for (int talkingPerson=0; talkingPerson<numberOfPeople; ++talkingPerson) {
for (int talkedToPerson=0; talkedToPerson<numberOfPeople; ++talkedToPerson) {
if (talkingPerson != talkedToPerson) { talkingPerson->talkTo(talkedToPerson);
}
}
}
That's it. All of the cultural if
processing is eliminated. And adding more cultures doesn't affect this portion of the code. Also, the time that is taken for each loop is pretty much the same whether one culture or a thousand are involved.
Pointers/References
Before developing a compilable 'talking' program for our new scenario, and showing the details involved, there is one more item I need to touch upon: the difference between ".
" and "->
" in C++. These are two methods of referring to objects and variables.
Everything you create in a program resides in memory somewhere. For example, let's take our earlier joe
. It was an object created in main
, and we accessed its functions with the ".
" syntax. (I'm talking about the line, joe.talkTo(jennie);
.)
As a vast understatement, let us say there are only 1,000 memory slots on the computer running that program. And the memory location where the joe
object begins is at #703. When the program is compiled (remember pressing the 'Local Windows Debugger' button in Visual Studio?), the executable knows how to directly access #703 when it is given the term 'joe
'. In those situations you simply use the ".
" syntax.
But there is one more way to access joe
if you wish, and that is through what is called a pointer
. It is perfectly acceptable to put the beginning location of joe
into another memory position, and use that secondary position to 'point' to joe
. To say this another way, we can place "703" into that other location. In fact, it is often quite useful to do this. When joe
is accessed that way, we use the "->
" syntax.
To simplify at the cost of not always being correct, but to give a general mental picture, if we are dealing with the real location of something, we use ".
". If we are dealing with a pointer to the location, we use "->
". (A hard rule that is much more correct is "If we are dealing with a pointer and don't dereference it, we always use the ->
nomenclature to access the functions associated with the object." But you don't know what 'dereferencing' is, yet, so pretend you didn't read that!)
The simple rule is simple, but it is easy to get confused due to the fluidity with which pointers can become non-pointers, and non-pointers (called references
in some circumstances, objects
, or variables
in others) can become pointers. These are transformed by the 'address of' operator, "&
", and the 'dereferencing' operator, "*
".
The 'address of' operator, "&
", is used to determine the memory location of a variable or object. And, as you may be guessing, the 'dereferencing' operator, "*
", is used to treat the value held in a pointer as the direct location of the variable or object. (Now you don't have to pretend any more.)
Unfortunately, both of these operators have secondary meanings in other contexts, to confuse the issue. (Or 'fortunately,' for the larger scope of C++, because they are valuable constructs, but they make your initial learning a little more difficult.) The 'address of' operator, "&
", can also be used to delineate a variable called a reference
, and the 'dereferencing' operator, "*
", can be used to indicate a variable that is a pointer
. With a little familiarity they are easy to tell apart, but it took me a while to become comfortable with them, so if you don't immediately get it, revisit the topic a few more times. You are not alone.
(As a general rule, if these symbols are on the left side of an '=
' sign they are declaring a pointer or reference variable. If they are on the right, they are dereferencing or taking the address of something.)
Let us redo the previous program to make Jennie and Joe talk to each other repetitiously, using these constructs.
#include <string>
#include <iostream>
using namespace std;
class Human {
private:
string nameC;
public:
Human(const string & name) : nameC(name) { }
string name() const { return nameC; }
void talkTo(const Human & person) const {
cout << nameC << " says: Hello, " << person.name() << "!" << endl;
}
};
int main() {
Human joe("Joe");
Human jennie("Jennie");
joe.talkTo(jennie);
jennie.talkTo(joe);
Human * pointerToJoe = &joe;
Human * pointerToJennie = &jennie;
pointerToJoe->talkTo(jennie);
pointerToJennie->talkTo(joe);
pointerToJoe->talkTo(*pointerToJennie);
pointerToJennie->talkTo(*pointerToJoe);
Human & referenceToJoe = *pointerToJoe;
Human & referenceToJennie = *pointerToJennie;
referenceToJoe.talkTo(referenceToJennie);
referenceToJennie.talkTo(referenceToJoe);
cout << endl << "Press 'Enter' to exit";
cin.ignore();
return 0;
}
Download HumanInteractions2.zip - 9KB
As you can see by the output, all of these are equivalent, it is just the syntax that differs:
Pointers and references are powerful, but demanding tools for your toolbelt. When used like the previous, they are safe, but as you get deeper into C++ there are many instances where pointers will bite you in the ankle with a venomous sting that will be remembered for some time. References also have some 'gotcha's associated with them, but they usually don't bite quite so hard. (Here's an article on pointers, and here's an article on the reference issues, for continued learning once you are done with this.)
Virtual(ly Magic!)
The reason I brought up pointers is because in C++ they can be used in a very powerful way: to call virtual functions
.
In order to understand virtual functions, let me step back and talk about another item: base classes and sub-classes.
Remember the set of if
statements that were going to rapidly grow as the number of cultures involved increased in our 'talking' scenario? Those statements were handling different types of Humans
, and an easy way to picture base classes and sub-classes is to think of a generic Human
as a base class, whereas a ChinesePerson
is a sub-class of Human
. (AmericanPerson
is also a sub-class, so no superiority is implied for anybody.)
Here is that relationship expressed in C++:
class Human {
};
class ChinesePerson : public Human {
};
There is no reason to stop there, although I will as far as code goes. We could derive NorthernChinesePerson
from ChinesePerson
, and further derive more specific types of northerners from that class if we so desired.
Given the previous code definition, the ChinesePerson
has access to all the public
items in Human
, and it can also access anything defined as protected
in the Human
class.
To give more insight, let me put together a quick program.
Pretty much every human can 'talk', so I will modify the original program around that to clarify things. First, let us make a program in which the base class has a talkTo
function as a public method:
#include <string>
#include <iostream>
using namespace std;
class Human {
private:
string nameC;
public:
Human(const string & name) : nameC(name) { }
string name() const { return nameC; }
void talkTo(const Human & person) const {
cout << nameC << " says: Hello, " << person.name() << "!" << endl;
}
};
class ChinesePerson : public Human {
public:
ChinesePerson(const string & name) : Human(name) { }
};
int main() {
Human joe("Joe");
ChinesePerson li("Li");
li.talkTo(joe);
joe.talkTo(li);
cout << endl << "Press 'Enter' to exit";
cin.ignore();
return 0;
}
Download HumanInteractions3.zip - 9KB
Upon running, you will see that this executes pretty much as before:
Both joe
and li
are able to talk to each other using the talkTo
function. But if talkTo
is moved into the private
section of Human
, only joe
will be able to use it. Change the Human
definition to the following, and try rerunning the program:
class Human {
private:
string nameC;
void talkTo(const Human & person) const {
cout << nameC << " says: " << "Hello, " << person.name() << "!" << endl;
}
public:
Human(const string & name) : nameC(name) { }
string name() const { return nameC; }
};
Now you get an error saying that Human::talkTo" is inaccessible
for the li.talkTo(joe);
line:
That is because Human
s, and only Human
s can access items in private
sections of their class. So let us make the talkTo
function public
again.
Now we start facing the interesting question we began with: how to get li
to talk in his own language. Right now all he can do is say "Hello, [name]".
The first iteration to a solution is to give ChinesePeople
their own talkTo
function:
#include <string>
#include <iostream>
using namespace std;
class Human {
protected:
string nameC;
public:
Human(const string & name) : nameC(name) { }
string name() const { return nameC; }
void talkTo(const Human & person) const {
cout << nameC << " says: Hello, " << person.name() << "!" << endl;
}
};
class ChinesePerson : public Human {
public:
ChinesePerson(const string & name) : Human(name) { }
void talkTo(const Human & person) const {
cout << nameC << " says: Néih hóu, " << person.name() << "!" << endl;
}
};
int main() {
Human joe("Joe");
ChinesePerson li("Li");
li.talkTo(joe);
joe.talkTo(li);
cout << endl << "Press 'Enter' to exit";
cin.ignore();
return 0;
}
Download HumanInteractions4.zip - 9KB
When you execute this program, notice the "Néih hóu":
The objects did talk to each other, but something got pretty messed up in translation!
The problem boils down to character sets, and how different languages are represented with 'ones' and 'zeros' in a computer's memory. I won't touch upon this subject, except to give a solution (without much explanation) to solve the issue.
Google, Bing, DuckDuckGo, and the other search engines are definitely one of a programmer's most used tools. A search for "visual studio console wstring" bring up this StackOverflow page, and it gives one method to circumvent the problem.
Experience told me that part of the issue was due to using std::string
instead of std::wstring
, which is why I chose that search term. To fix the issue, the program needs to be rewritten as follows:
#include <string>
#include <iostream>
#include <io.h>
#include <fcntl.h>
using namespace std;
class Human {
protected:
wstring nameC;
public:
Human(const wstring & name) : nameC(name) { }
wstring name() const { return nameC; }
void talkTo(const Human & person) const {
wcout << nameC << " says: Hello, " << person.name() << "!" << endl;
}
};
class ChinesePerson : public Human {
public:
ChinesePerson(const wstring & name) : Human(name) { }
void talkTo(const Human & person) const {
wcout << nameC << " says: Néih hóu, " << person.name() << "!" << endl;
}
};
int main() {
_setmode(_fileno(stdout), _O_U16TEXT);
Human joe(L"Joe"); ChinesePerson li(L"Li");
li.talkTo(joe);
joe.talkTo(li);
wcout << endl << "Press 'Enter' to exit";
cin.ignore();
return 0;
}
Download HumanInteractions5.zip - 9KB
When executed, the expected result is obtained:
Unfortunately we cannot use true Chinese characters without a lot more effort (as far as I know), so I will leave it at this textual state.
(Actually, looking at it a little more, possibly not so much is required. It seems all you would have to do is hack the registry so that consoles could use a font like Arial Unicode MS. If you ever want to play with that some more, I believe these are the characters for this instance: 你好. Now let's get back to work!)
Finally! The Introductions!
At last we have the background material to begin solving our 'talking' problem. Since I have not yet shown a virtual
example, let me first extend the previous to illustrate how the problem could be solved without it. We will fill in that long if
loop, and make some more modifications. Note that I have removed the talkTo
routine from the Human
class, in order to emphasize that each type of culture is doing its own 'talking.' I have also showed what it takes to iterate through a list, and talk that way:
#include <string>
#include <iostream>
#include <io.h>
#include <fcntl.h>
#include <vector>
using namespace std;
enum class Culture {
Chinese = 1, American, Unknown };
class Human {
protected:
wstring nameC;
Culture cultureC;
public:
Human(const wstring & name, Culture culture) : nameC(name), cultureC(culture) { }
wstring name() const { return nameC; }
Culture culture() const { return cultureC; }
};
class AmericanPerson : public Human {
public:
AmericanPerson(const wstring & name) : Human(name, Culture::American) { }
void talkTo(const Human & person) const {
wcout << nameC << " says: Hello, " << person.name() << "!" << endl;
}
};
class ChinesePerson : public Human {
public:
ChinesePerson(const wstring & name) : Human(name, Culture::Chinese) { }
void talkTo(const Human & person) const {
wcout << nameC << L" says: Néih hóu, " << person.name() << L"!" << endl;
}
};
int main() {
_setmode(_fileno(stdout), _O_U16TEXT);
AmericanPerson joe(L"Joe");
ChinesePerson li(L"Li");
li.talkTo(joe);
joe.talkTo(li);
wcout << endl << "Now talking by iterating over the list:" << endl << endl;
vector<Human*> people;
people.push_back(&li);
people.push_back(&joe);
for (int speaker=0, numPeople=people.size(); speaker<numPeople; ++speaker) {
for (int listener=0; listener<numPeople; ++listener) {
if (listener != speaker) {
Culture culture = people[speaker]->culture();
if (culture == Culture::American) {
AmericanPerson * ap = static_cast<AmericanPerson*>(people[speaker]);
ap->talkTo(*people[listener]);
}
else if (culture == Culture::Chinese) {
ChinesePerson * cp = static_cast<ChinesePerson*>(people[speaker]);
cp->talkTo(*people[listener]);
}
else {
wcout << "Culture unknown - programming error" << endl;
}
}
}
}
wcout << endl << "Press 'Enter' to exit";
cin.ignore();
return 0;
}
Download HumanInteractions6.zip - 9KB
The program is not so small anymore, but it shows the complexity of coding without virtual functions, and does what is desired:
The issue we now have is we wish to iterate through a list of Human
s, and have each one 'talk' in their turn, but the 'talking' function is dependent upon the type of Human
doing the talking. What we really want is a generic talkTo
routine that the sub-classes can implement however they wish, and that the computer can call automatically based on type.
This is where virtual functions
come in. Public routines (or protected routines, for that matter) marked virtual
can be overridden by the sub-classes, and when they are called through a pointer to the base class in which the function is declared, the program will figure out the correct function to call behind the scenes. I will add a few more types of Human
s, and solve the initial problem, and do so without the Culture enum
of the previous approach:
#include <string>
#include <iostream>
#include <vector>
#include <io.h>
#include <fcntl.h>
using namespace std;
class Human {
protected:
wstring nameC;
public:
Human(const wstring & name) : nameC(name) { }
wstring name() const { return nameC; }
virtual void talkTo(const Human & person) const { }
};
class AmericanPerson : public Human {
public:
AmericanPerson(const wstring & name) : Human(name) { }
void talkTo(const Human & person) const override {
wcout << nameC << " says: Hello, " << person.name() << "!" << endl;
}
};
class ChinesePerson : public Human {
public:
ChinesePerson(const wstring & name) : Human(name) { }
void talkTo(const Human & person) const override {
wcout << nameC << L" says: Néih hóu, " << person.name() << L"!" << endl;
}
};
class MexicanPerson : public Human {
public:
MexicanPerson(const wstring & name) : Human(name) { }
void talkTo(const Human & person) const override {
wcout << nameC << L" says: ¡Hola!, " << person.name() << L"!" << endl;
}
};
class GhettoSlanger : public Human {
public:
GhettoSlanger(const wstring & name) : Human(name) { }
void talkTo(const Human & person) const override {
wcout << nameC << L" says: Yo, Homey! Whassup?" << endl;
}
};
class JapanesePerson : public Human {
public:
JapanesePerson(const wstring & name) : Human(name) { }
void talkTo(const Human & person) const override {
wcout << nameC << L" says: Konnichiwa, " << person.name() << L"!" << endl;
}
};
int main() {
_setmode(_fileno(stdout), _O_U16TEXT);
AmericanPerson joe(L"Joe");
ChinesePerson li(L"Li");
MexicanPerson jose(L"Jose");
GhettoSlanger mark(L"Mark");
JapanesePerson hana(L"Hana");
vector<Human*> people;
people.push_back(&joe);
people.push_back(&li);
people.push_back(&jose);
people.push_back(&mark);
people.push_back(&hana);
for (int speaker=0, numPeople=people.size(); speaker<numPeople; ++speaker) {
for (int listener=0; listener<numPeople; ++listener) {
if (speaker != listener) {
people[speaker]->talkTo(*people[listener]);
}
}
wcout << endl;
}
wcout << "Press 'Enter' to exit";
cin.ignore();
return 0;
}
Download HumanInteractions7.zip - 9KB
And with that, the solution is complete. You have seen how to create objects in C++, and how virtual functions allow you to eliminate logic checks in many circumstances. What I have just showed you is at the heart of C++ Object Orientation, and is very useful in many circumstances. I hope this helps you achieve a modicum of mastery much faster than I was able to.
Before wrapping this up, I will mention that you will often see items being created through new
. Doing so would have eliminated some of the code in the previous main
function. new
creates an object somewhere in the computer's 'heap' memory, to give you another term for further study. It then returns the address to that position (in the form of a pointer) to you.
Knowing that, a first iteration would be the following, but it is incredibly naive, and you should NEVER DO IT! (Sorry for shouting, but it's kind of important.)
int main() {
_setmode(_fileno(stdout), _O_U16TEXT);
vector<Human*> people;
people.push_back(new AmericanPerson(L"Joe")); people.push_back(new ChinesePerson (L"Li"));
people.push_back(new MexicanPerson (L"Jose"));
people.push_back(new GhettoSlanger (L"Mark"));
people.push_back(new JapanesePerson (L"Hana"));
for (int speaker=0, numPeople=people.size(); speaker<numPeople; ++speaker) {
for (int listener=0; listener<numPeople; ++listener) {
if (speaker != listener) {
people[speaker]->talkTo(*people[listener]);
}
}
wcout << endl;
}
wcout << "Press 'Enter' to exit";
cin.ignore();
return 0;
}
The solution eliminates the need to address each person with a variable name, and the solution is shorter than the previous code, but the problem is that none of the objects will be deleted at the end of the scope this way. The earlier solutions always ended up calling what is called the destructor
of the objects, but this one doesn't.
One solution for that is to modify the program to this:
class Human {
protected:
wstring nameC;
public:
Human(const wstring & name) : nameC(name) { }
virtual ~Human() { }
wstring name() const { return nameC; }
virtual void talkTo(const Human & person) const { }
};
int main() {
_setmode(_fileno(stdout), _O_U16TEXT);
vector<Human*> people;
people.push_back(new AmericanPerson(L"Joe"));
people.push_back(new ChinesePerson (L"Li"));
people.push_back(new MexicanPerson (L"Jose"));
people.push_back(new GhettoSlanger (L"Mark"));
people.push_back(new JapanesePerson (L"Hana"));
for (int speaker=0, numPeople=people.size(); speaker<numPeople; ++speaker) {
for (int listener=0; listener<numPeople; ++listener) {
if (speaker != listener) {
people[speaker]->talkTo(*people[listener]);
}
}
wcout << endl;
}
wcout << "Press 'Enter' to exit";
cin.ignore();
while (!people.empty()) {
delete people.back();
people.pop_back();
}
return 0;
}
Download HumanInteractions8.zip - 9KB
Now, at the end, the objects will be deleted. But even this solution is not the best, because if bad things happen between the "people.push_back(new JapanesePerson (L"Hana"));
" line and the "while (!people.empty()) {
" line, the "while (!people.empty()) {
" line would never be reached, and the objects would be 'leaked,' as before.
The current best paradigm for solving this problem is to use some helper objects from the standard library. I won't go into the details - you can search online for the words used in the the next code block. When you see it in other people's work you will be better prepared to understand it from having seen it in this rewrite of our main
function:
#include <memory> //for 'unique_ptr'
int main() {
_setmode(_fileno(stdout), _O_U16TEXT);
vector<unique_ptr<Human>> people;
people.push_back(make_unique<AmericanPerson>(L"Joe"));
people.push_back(make_unique<ChinesePerson>(L"Li"));
people.push_back(make_unique<MexicanPerson>(L"Jose"));
people.push_back(make_unique<GhettoSlanger>(L"Mark"));
people.push_back(make_unique<JapanesePerson>(L"Hana"));
for (int speaker=0, numPeople=people.size(); speaker<numPeople; ++speaker) {
for (int listener=0; listener<numPeople; ++listener) {
if (speaker != listener) {
people[speaker]->talkTo(*people[listener]);
}
}
wcout << endl;
}
wcout << "Press 'Enter' to exit";
cin.ignore();
return 0;
}
Download HumanInteractions9.zip - 9KB
And that is everything I wish to share in this article, since my fingers are tired, and it's time to do something else. This writing has summarized many years of work and learning, and hopefully helps some of you grasp the concepts a little more easily than I did. I know I never covered header files, and a couple other items I brought up, but I will leave that for another edition/iteration, if it is called for. The previous concepts are far more important than the items I've left out.
Thanks for reading, and Happy Coding!
History
- November 24, 2014:
- In HumanInteractions9, everything I'd read gave me the impression that the
unique_ptr
s would keep track of type, so the correct destructors would be called without a virtual destructor. But intuition kicked in (of course, after the article was already public : rolleyes : ), so I tested. My conclusion was incorrect (the sources I read were probably correct - I just misread them). Therefore I added a virtual destructor back to the Human
class. I apologize for misleading earlier readers of this article, and hope my misstatement is made known to them so they don't create leaky code as a result of the earlier versions. - Corrected a comment about memory locations in HumanInteractions2.
- Broke up a heavy paragraph, and other minor edits.
- Changed recommendation from Visual Studio Express to Visual Studio Community Edition, which was made available after the first edition of this article was made public. Of course, if you want a smaller install that doesn't take up as much disk space, the Express Edition would be the way to go, but the Community Edition gives you more options that are worth the added space in my opinion.
- Improved
const
correctness in all the code, in order to set a better example.
- November 4, 2014: Added virtual destructor to what is now HumanInteractions8 example, which I overlooked in earlier version. (HumanInteractions9 was previously HumanInteractions8.)