There's a problem that crops up over and over when people write object oriented (OO) code for Arduino. When you try to instantiate an object of a class which in turn instantiates an object of another class; things go wildly wrong. The error messages you get are cryptic and, unless you're a fair C++ programmer, useless.
An example of when this might be required is this. I'm creating a GPS class to interpret data that comes from a GPS device. To my mind, the GPS object should instantiate its own serial object so it can receive data and do what it wants with it. It shouldn't be the job of the main sketch to go get the serial data and pass it to the GPS object - as would happen if the main sketch instantiated two objects, one for serial, one for GPS. I'm going to do a walk-through on how to get the main sketch to instantiate an object of a class, and as that object is being created, let the new object create its own object of a different class.
Have a look at my last post for the code for ClassSketchPlus. Create a new project with this code. Once you've created the project close the Arduino IDE and rename the TestClass.h and TestClass.cpp files to First.h and First.cpp. Copy First.h and First.cpp and rename the copies Second.h and Second.cpp. When you re-open the project you'll need to do some search-and-replace operations:
- In the main sketch file, replace TestClass with First
- In both First files, replace TestClass with First
- In both Second files, replace TestClass with Second
In the Arduino IDE you should now see these tabs.
If you compile, sorry "Verify", everything should be sweet. But the class Second is doing nothing. It's not included by either the PDE file or First.h and so has no effect. So let's add it to the First.h file - add it in First.h above the class First line:
This too should compile fine. OK, so how do we instantiate an object of the Second class? Let's add a private member for that. Remember this is just a reference that can refer to an object. Adding this line doesn't create an object anywhere; the reference points to nothing. The First.h file now looks like this:
#include "Second.h" // ADDED!
Second widget; // ADDED!
Now if we try to compile, we've stuffed it.
First.cpp: In constructor 'First::First(int)':First.cpp:2: error: no matching function for call to 'Second::Second()'/Second.h:7: note: candidates are: Second::Second(int)/Second.h:5: note: Second::Second(const Second&)
Great! So what's that mean? I could try to explain, but we know we haven't created an object for widget to refer to. So let's try to do it. I'm going to show my ignorance here, but I went to First.cpp and added lines to the constructor to try to create an object for widget.
itsNumber = newNumber;
Second widget(7); // Didn't work.
// widget = Second(7); // Didn't work either.
Remember the New C++ keyword doesn't exist in Arduino unless you implement it yourself. So widget = New Second(9); isn't going to work. The secret sauce is an initialiser list, or initializer list of you're American! :)
In the first line of the implementation for the First class' constructor (in First.cpp), append a colon and a constructor call for widget, supplying any required parameters. Like this:
First::First(int newNumber) : widget(9)
itsNumber = newNumber;
Compile it. You like, yes?
Of course this can be extended to any number of classes. Each initialiser list can instantiate multiple objects. Each object being instantiated can instantiate its own objects. I plan to take this to the extreme: the main sketch will instantiate a LifeClock object which will create multiple objects each creating multiple objects. I've mapped out about 5 levels deep in my planning.
A number of improvements are beckoning. First, a constructor for the First class should have parameters that are immediately passed (in the initialiser list) to the Second object. This is instead of using hard-coded numbers. Also, note this method does not require that both First and Second header files be included at the main sketch level. If this were necessary as you "nested" your classes you would have to keep track of the dependencies and ensure your include list was correct.
Taking a short-cut to the next level, I've developed a quick test program that's an evolution of what we've seen so far. Download the code here. It shows classes First and Second in action and passing parameters between constructors using an initialiser list on the First constructor.
Finally, I want to go back and explain the error message I skipped past earlier. The first two lines of the message say that the constructor in the First class (which is called when the line First thing(6); is reached in the main sketch) can't find a constructor in the Second class matching the signature Second::Second(). Note that the Second constructor has no parameters. What the? Why is it looking for a constructor for Second? You'll find that no matter what you put inside First's constructor, this error will remain. It seems that as soon as the constructor for First is reached, a default constructor for Second is called unless an initialiser list says otherwise. The initialiser list we put in calls the correct constructor for Second (the one with an integer parameter) and provides the value for that parameter. So all is well.
If an initialiser list is not used, a "no parameters constructor" is assumed. Thus there are two instances where initialiser lists are not required to create an object from another object. If your "second" class has a constructor with no parameters, it will be called anyway. If your "second" class has no constructors, then the implicit no-parameter constructor is called successfully anyway (it exists even if you don't code it). Of course if you code more than one constructor, be careful! Without an initialiser list, the no-parameters constructor will always be called regardless of your code inside the "first" class' constructor.
As I'm choosing to do my Arduino development in an OO fashion, you should see this all through my code as the project progresses. Look for further examples in my future posts.
That is all!