I am having trouble getting my head around pointers in C++, specifically how they behave when passed around or used within objects. As far as my understanding goes as long as pointers, or more specifically values/objects pointed to - do not go out of scope, I should be able to use that pointer.
I have been playing around with some test code on my Arduino Uno and I am bumping into behaviour I cannot explain.
I have read rather a lot on the topic of pointers in C++ and thought I had a good grasp of it. However, practice doesn't seem to match the theory in my head. I don't know if this is a lack of understanding or if the Arduino platform has idiosyncrasies that I am not aware of.
In my test code I have created two classes, Alpha
and Beta
. Alpha simply holds a C-type string in a member variable called alpha
. Beta
is designed to hold two Alpha
instances in an array*.
*While this is "toy" code, the general structure mirrors my actual project. I have used this toy code as it more succinctly describes the set up and the issue I am facing.
pointers.h
#import "Arduino.h"
class Alpha {
private:
char alpha[20];
public:
Alpha();
~Alpha();
char* getAlpha(char* s, int size=20);
void setAlpha(char* value, int size=20);
void debug();
};
class Beta {
private:
Alpha** alphas;
public:
Beta();
~Beta();
Alpha* getAlpha(int index);
void setAlpha(Alpha* alpha, int index);
void doStuffWithAlpha(int index);
};
pointers.cpp
#import "pointers.h"
Alpha::Alpha() {
setAlpha("Initial");
}
Alpha::~Alpha() {}
char* Alpha::getAlpha(char* s, int size/*=20*/) {
*s = *alpha;
return s; //return not strictly needed here since s is passed in
}
void Alpha::setAlpha(char* value, int size/*=20*/) {
strncpy(alpha, value, size-1);
alpha[size-1] = '0円';
}
void Alpha::debug() {
Serial.println(alpha);
}
Beta::Beta() {
alphas = new Alpha*[2];
}
Beta::~Beta() {}
Alpha* Beta::getAlpha(int index) {
return alphas[index];
}
void Beta::setAlpha(Alpha* alpha, int index) {
alphas[index] = alpha;
}
void Beta::doStuffWithAlpha(int index) {
Alpha* alpha = getAlpha(index);
/* Alpha* alpha = alphas[index]; */ //This doesn't work either - same outcome.
alpha->debug();
}
sketch.ino
#import "pointers.h"
Alpha a1;
Alpha a2;
Beta b1;
void setup() {
Serial.begin(9600);
Serial.println("SETUP()");
Serial.println();
a1 = Alpha();
a1.setAlpha("Hello");
Serial.println("----- a1.debug -----");
Serial.println("Expected: Hello");
Serial.print("Actual: "); a1.debug();
Serial.println();
a2 = Alpha();
a2.setAlpha("Goodbye");
Serial.println("----- a2.debug -----");
Serial.println("Expected: Goodbye");
Serial.print("Actual: "); a2.debug();
Serial.println();
Beta b1 = Beta();
b1.setAlpha(&a1, 0);
b1.setAlpha(&a2, 1);
Serial.println("----- b1.doStuffWithAlpha -----");
Serial.println("Expected: Hello");
Serial.print("Actual: "); b1.doStuffWithAlpha(0);
Serial.println("Expected: Goodbye");
Serial.print("Actual: "); b1.doStuffWithAlpha(1);
Serial.println();
}
void loop() {
Serial.println("LOOP()");
Serial.println();
Serial.println("----- a1.debug -----");
Serial.println("Expected: Hello");
Serial.print("Actual: "); a1.debug();
Serial.println();
Serial.println("----- b1.doStuffWithAlpha(0) ------");
Serial.println("Expected: Hello");
Serial.print("Actual: "); b1.doStuffWithAlpha(0);
Serial.println();
Serial.println("----- a2.debug -----");
Serial.println("Expected: Goodbye");
Serial.print("Actual: "); a2.debug();
Serial.println();
Serial.println("----- b1.doStuffWithAlpha(1) ------");
Serial.println("Expected: Goodbye");
Serial.print("Actual: "); b1.doStuffWithAlpha(1);
Serial.println();
delay(10000);
}
This sketch is quite straight-forward. It creates 2 Alpha
objects and a Beta
object and passes the two Alphas into Beta. The myriad Serial.print()
statements produces the output below.
output
SETUP()
----- a1.debug -----
Expected: Hello
Actual: Hello
----- a2.debug -----
Expected: Goodbye
Actual: Goodbye
----- b1.doStuffWithAlpha -----
Expected: Hello
Actual: Hello
Expected: Goodbye
Actual: Goodbye
//everything is fine up to this point...
LOOP()
----- a1.debug -----
Expected: Hello
Actual: Hello
----- b1.doStuffWithAlpha(0) ------
Expected: Hello
Actual: CÃoçëyc=Lä^ÜŽYw ̧Nv†Ý¿ //uh oh...
----- a2.debug -----
Expected: Goodbye
Actual: Goodbye
----- b1.doStuffWithAlpha(1) ------
Expected: Goodbye
Actual: ÿß/ï //uh oh again...
Everything works as expected when running a1
or a2.debug()
or b1.doStuffWithAlpha()
in the setup()
function.
The result of a1.debug()
and a2.debug()
works as expected in the loop()
function.
The issue, as you can see, is when running b1.doStuffWithAlpha(0)
or b1.doStuffWithAlpha(1)
. The output is usually junk, which suggests to me the pointer has been mangled and points in the wrong place.
I don't understand what is happening between the transition from setup()
to loop()
. At first I thought the issue was the "Hello" and "Goodbye" strings going out of scope, but Alpha::setAlpha()
makes a copy of the C-string. Also, accessing Alpha::debug()
in loop()
works, as shown above.
I have tried variations on the above code, such as using a struct or an int for alpha
instead of a C-string and the results are the same: Mangled values.
Have I done something obviously silly that my scant C++ experience hasn't picked up on, or is there some fundamental Arduino shenanigans at play?
1 Answer 1
Problem is local shadow variable b1
in setup() function declared as Beta b1 = Beta();
. In loop()
is considered uninitialized global variable b1
.
-
1I agree that is probably the problem. Also in your destructor for Beta you should
delete
the pointers you allocate in the constructor. I'm not wildly keen on doingnew
in a constructor at static scope either because of the static initialization order fiasco.2015年09月10日 21:06:35 +00:00Commented Sep 10, 2015 at 21:06 -
@TMa I can't believe I missed that! I must have been looking at my own code for too long! @NickGammon I agree that I should have a destructor, it was left out for just this example. Regarding the use of
new
in a constructor, how else would I do it? Reading the link you gave, the initialisation fiasco only affects classes/types declared in different compilation units. My code has both classes defined in the same file.Etzeitet– Etzeitet2015年09月17日 10:50:15 +00:00Commented Sep 17, 2015 at 10:50 -
If the project ever gets large enough that you have more than one compilation unit, it may affect you. What you can do is have a
begin
method which is called fromsetup
(eg. like Serial). Alternatively (or as well) you could set the pointers to NULL in the constructor (which is safe) and then allocate them withnew
when they are needed.2015年09月17日 20:38:25 +00:00Commented Sep 17, 2015 at 20:38