Let's start with a simple program that defines and uses one class, Thing. We'll call it testThing.cpp.
Our goal is to split this one file up, so that other programmers can more easily use our Thing class. We will take the Thing class itself and put it into two files, one that just has the class definition and any related prototypes. Note that our class definition will not have any method definitions, just their prototypes. The other file will hold the actual definitions of the methods and again, associated functions.
Below is our starting program, named testThing.cpp. Yeah, it's really simple. [To avoid clutter, after this I will leave out the file comment, #includes and using namespace statement, unless relevant.]
// testThing.cpp
#include <iostream>
using namespace std;
class Thing {
friend ostream& operator<<(ostream& os, const Thing& theThing) {
os << "Thing: val = " << theThing.val;
return os;
}
public:
Thing(int val) : val(val) {}
void setVal(int val) { this->val = val; }
int getVal() const { return val; }
private:
int val;
};
int main() {
Thing thingOne(17);
cout << thingOne << endl;
thingOne.setVal(42);
cout << thingOne << endl;
cout << "thingOne's val: " << thingOne.getVal() << endl;
}
Our first step is to rewrite this program so that all of the fuction definitions in the class are moved outside of it, a put at the end of the file. Let's do them one at a time.
friend
with the definition, only with the
prototype.
The changes are boldfaced
class Thing {
friend ostream& operator<<(ostream& os, const Thing& theThing);
public:
Thing(int val) : val(val) {}
void setVal(int val) { this->val = val; }
int getVal() const { return val; }
private:
int val;
};
int main() {
Thing thingOne(17);
cout << thingOne << endl;
thingOne.setVal(42);
cout << thingOne << endl;
cout << "thingOne's val: " << thingOne.getVal() << endl;
}
ostream& operator<<(ostream& os, const Thing& theThing) {
os << "Thing: val = " << theThing.val;
return os;
}
Next we will move the method setVal. What will get left behind? Just the prototype. In this case, that means we just have to "lop off" the body of the function, replacing it with a semi-colon.
And then there's the function definition, that we will put at the end. Let's take a look at what we will have if we just move it as is:
class Thing {
friend ostream& operator<<(ostream& os, const Thing& theThing);
public:
Thing(int val) : val(val) {}
void setVal(int val);
int getVal() const { return val; }
private:
int val;
};
int main() {
Thing thingOne(17);
cout << thingOne << endl;
thingOne.setVal(42);
cout << thingOne << endl;
}
ostream& operator<<(ostream& os, const Thing& theThing) {
os << "Thing: val = " << theThing.val;
return os;
}
// This doesn't quite work!
void setVal(int val) { this->val = val; }
When the compiler see the definition for setVal, it will be
confused. It doesn't know that you had in mind providing the
definition for the method setVal. It will figure this is
just some regular non-method function you are defining. But in that
case, there shouldn't be a this
. With my compiler, this
generates the following error message:
testThing.cpp:28:24: error: invalid use of 'this' outside of a non-static member
function
void setVal(int val) { this->val = val; }
^
1 error generated.
How to fix that? We tell the compiler that the function we are defining is actually part of the Thing class. To do that, we qualify the method name with the name of the class. The new function definition will look like:
void Thing::setVal(int val) { this->val = val; }
Next we will do the same thing except with th getter,
getVal. However, there is one more thing to concern us here, what to
do with the const
on the method?
The question to answer is, whether the const is part of the implementation of the method, or does it identify which method we are talking about. If you have seen that methods can be overloaded, purely based on whether or not they are const, then you will realize that the const does indeed identify which method.
The result then of movng the defintion to the end, qualifying the function name in the definition, and leaving behind just a prototype is:
class Thing {
friend ostream& operator<<(ostream& os, const Thing& theThing);
public:
Thing(int val) : val(val) {}
void setVal(int val);
int getVal() const;
private:
int val;
};
int main() {
Thing thingOne(17);
cout << thingOne << endl;
thingOne.setVal(42);
cout << thingOne << endl;
cout << "thingOne's val: " << thingOne.getVal() << endl;
}
ostream& operator<<(ostream& os, const Thing& theThing) {
os << "Thing: val = " << theThing.val;
return os;
}
void Thing::setVal(int val) { this->val = val; }
int Thing::getVal() const { return val; }
That leaves us with just the constructor to move. Moving the definition to the end is not a problem. Just remember to qualify the constructor's name, as we did with the other methods.
But there is a question as to how we should specify the prototype. What's the question? Should the initialization list be part of the prototype or not? The answer comes down to, is it part of the implementation or does it identify which constructor we are using. As you know, it is possible to set the field val insice the constructor body. And it would still be the "same" constructor. Conclusion? The initializatin list should not be part of the prototype. Below is the result, and the end of the first stage in our efforts towards separate compilation.
class Thing {
friend ostream& operator<<(ostream& os, const Thing& theThing);
public:
Thing(int val);
void setVal(int val);
int getVal() const;
private:
int val;
};
int main() {
Thing thingOne(17);
cout << thingOne << endl;
thingOne.setVal(42);
cout << thingOne << endl;
cout << "thingOne's val: " << thingOne.getVal() << endl;
}
ostream& operator<<(ostream& os, const Thing& theThing) {
os << "Thing: val = " << theThing.val;
return os;
}
void Thing::setVal(int val) { this->val = val; }
int Thing::getVal() const { return val; }
Thing::Thing(int val) : val(val) {}
On to the next step. We will create two new files, a header file that we will call Thing.h, and an implementation file that we will call Thing.cpp.
The header file will basically just hold the class defintion as shown above. Yes, it will need a little more bookkeeping stuff, like include files.
The implementaton file will hold those four function definitions that we put at the end of the program.
Oh, let's not forget testThing.cpp! What will be left? Not the class definition. And not those four function defintions. Let's look at what the testThing.cpp would look like then:
// testThing.cpp
#include <iostream>
using namespace std;
// We have a small problem...
int main() {
Thing thingOne(17);
cout << thingOne << endl;
thingOne.setVal(42);
cout << thingOne << endl;
cout << "thingOne's val: " << thingOne.getVal() << endl;
}
What's the small problem mentioned in the comment?
When the compiler is compiling this file, it has never heard of Thing, doesn't now it has a constructor, or method getVal and setVal, or an output operator.
We fix that problem by putting in a #include for the new header file:
// testThing.cpp
#include <iostream>
#include "Thing.h"
using namespace std;
int main() {
Thing thingOne(17);
cout << thingOne << endl;
thingOne.setVal(42);
cout << thingOne << endl;
cout << "thingOne's val: " << thingOne.getVal() << endl;
}
Notice that when we write #include's for our own header files, we put the file's name in quotes, instead of in angle brackets.
What about the implementation file? So far all we have put in it are the definitions of those four functions. I'll throw a comment at the beginning of the file to identify it:
// Thing.cpp
ostream& operator<<(ostream& os, const Thing& theThing) {
os << "Thing: val = " << theThing.val;
return os;
}
void Thing::setVal(int val) { this->val = val; }
int Thing::getVal() const { return val; }
Thing::Thing(int val) : val(val) {}
When the compiler looks at that file, trying to compile the functions, it is going to have a few problems. First, looking only at it and nothing else, it won't know about ostream or string. Or even the class Thing, itself! So, let's put in #includes for those headers, along with our usual "using namespace".
// thing.cpp
#include "Thing.h"
#include <iostream>
#include <string>
using namespace std;
ostream& operator<<(ostream& os, const Thing& theThing) {
os << "Thing: val = " << theThing.val;
return os;
}
void Thing::setVal(int val) { this->val = val; }
int Thing::getVal() const { return val; }
Thing::Thing(int val) : val(val) {}
That's about all we need for the implementation file. We will come back to it later when we want to discuss putting out code in a namespace.
Before we more completely away from that implementation file, consider the first line, aside from the comment. It includes the implementation file. So, the first thing the compiler is actually going to look at before it gets to the rest of the implementation file, is the header file. What have we got there? Just the class definition what we took out of testThing.cpp.
// Thing.h
class Thing {
friend ostream& operator<<(ostream& os, const Thing& theThing);
public:
Thing(int val);
void setVal(int val);
int getVal() const;
private:
int val;
};
Consider that now these lines of code are being read by the compiler before it has seen anything else about the program. As before, you can see that the compiler will be wondering what some of the symbols mean. Well, ok, just one symbol, ostream. Yes we need a #include.
// Thing.h
#include <iostream>
class Thing {
friend ostream& operator<<(ostream& os, const Thing& theThing);
public:
Thing(int val);
void setVal(int val);
int getVal() const;
private:
int val;
};
Of course, that's not quite enough. However, for reasons we will
discuss later, we do not put using namespace
statements in header files. Yeah, it makes for a minor hassle. What
do we do instead? Qualify the symbols, such
as ostream
, with their namespace. Anywhere we
have ostream
, we will replace it
with std::ostream
. That changes our header file to
// Thing.h
#include <iostream>
class Thing {
friend std::ostream& operator<<(std::ostream& os, const Thing& theThing);
public:
Thing(int val);
void setVal(int val);
int getVal() const;
private:
int val;
};
With that, our files will work. Assuming you have correctly added them to your IDE, or compile them correctly with your command-line compiler, tje program should build and run fine.
There are still a couple of issues we should address. The first is "include guards". And the second is namespaces. We'll get to them...
Maintained by John Sterling (john.sterling@nyu.edu). Last updated Mar. 7th, 2018