Best way to handle string/char manipulation in an object

Hello, as an exercise in learning C++ and decent OOP in C++, I'm creating a class that I can use to easily manipulate specified areas on an OLED display. I could create a new print area, set the value (string or int), then modify it, movie it, etc, as needed.

The issue I'm having is how I store the string (char, not String), and how I modify it by appending/prepending values to it.

Here are the sketch files I've created for this post (removed the code that wasn't relevant:
I'm executing this on a new Arduino Nano Every (not sure if that matters).

OledText.hpp:

#pragma once

#ifndef OLED_TEXT_H
#define OLED_TEXT_H

// Including "settings" here just to make sharing it simpler
constexpr unsigned int  VALUE_TYPE_NONE     = 0;  
constexpr unsigned int  VALUE_TYPE_CHAR     = 1;  
constexpr unsigned int  VALUE_TYPE_INT      = 2;  

#include <Arduino.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1351.h>
#include <SPI.h>
#include <ArduinoSTL.h>
//#include <map>

#endif

class OledText {
public: 
  char *printerVal; 

  Adafruit_SSD1351 * OLEDHandler;

  OledText(Adafruit_SSD1351 &OLEDHandler);

  // Set/update the print containers display value to a string, then display it
  void print(char charValue[]);
  //void print(char *charValue);
  //void print(char charValue[25]);
  
  // Set/update the print containers display value to an integer, then display it
  void print(long intValue);

  // Function to re-print whatever value/type is saved (will be useful if the
  // printed text needs to be "reloaded" for various reasons)
  void print();


  void prepend(char *prependTxt);
  //void prepend(char prependTxt[25]);
  //void prepend(char prependTxt[]);

};

OledText.cpp:

#pragma once

#include "OledText.hpp"

OledText::OledText(Adafruit_SSD1351 &OLEDHandler) : OLEDHandler (&OLEDHandler) {}

// If user were to set/change the printer to a CHAR...
void  OledText::print( char charValue[] )
{
  this->printerVal = charValue;
  //strcpy(this->printerVal, charValue);

  this->print();
}

// User provides a new NUMERICAL value, store it as a char
void  OledText::print( long intValue )
{
  char cstr[16];
  itoa( intValue, cstr, 10 );

  this->printerVal = cstr;

  this->print();
}


// This would print the stored value in the specified area, but for this forum
// post, I'm just outputting the value to serial
void OledText::print()
{
  std::cout << this->printerVal << std::endl;

  return;
}

// User wants to prepend (or append, in a diff function) a string
// to the beginning of the char value..
void OledText::prepend( char prependTxt[] )
{
  if ( strlen( this->printerVal ) == 0 )
  {
    // Nothing to append anything to, just switch to a regular print
   return this->print( prependTxt );
  }

  char newCharValue[ strlen(prependTxt) + strlen(this->printerVal) +1 ];  // add +1 for end?...

  strcpy( newCharValue, prependTxt );
  strcat( newCharValue, this->printerVal );

  // Print the new char (with the prefix prepended)
  this->print( newCharValue );
}

Char_Prepend_Sketch.ino:

#include <Arduino.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1351.h>
#include <SPI.h>
#include "OledText.hpp"

Adafruit_SSD1351 oled = Adafruit_SSD1351(128, 128, &SPI, 10, 9, -1 );
OledText Greeting(oled);

void setup() {
  Serial.begin(115200);

  //
  // Bunch of irrelevant Adafruit_SSD1351 init code removed.
  //

  Serial.print("\nExpected output: 1234\n\t-> ");
  Greeting.print(1234);

  Serial.print("\nExpected output: World\n\t-> ");
  Greeting.print("World");

  Serial.print("\nExpected output: Hello World\n\t-> ");
  Greeting.prepend("Hello ");

  Serial.print("\nExpected output: Hayy, Hello World\n\t-> ");
  Greeting.prepend("Hayy, ");

  Serial.print("\nExpected output: Goodbye\n\t-> ");
  Greeting.print("Goodbye");


  Serial.print("\nExpected output: Goodbye\n\t-> ");
  Greeting.print();
}


void loop(){}

Basically, I should be able to create the OledText instance Greeting, then use Greeting.print("World") to set the local variable this->printerVal to World, then use Greeting.prepend("Hello, ") to prepend the Hello to the beginning of printerVal, making it Hello, World.

And that part actually works. It's when I prepend another value onto the end of it that it seems to break.
Here's the output of the above code (which also outputs what I expect to see):
Screen Shot 2023-08-19 at 9.26.06 PM

I have a feeling that maybe the fact the class member printerVal is a pointer, I just read on The Evils of Arduino Strings | Majenko's Hardware Hacking Blog that pointers strings shouldn't ever be modified, and ideally they should always be constants. But I need to be able to modify this, and I've tried passing it by reference. I also tried changing it to just char printerVal[]; but then I got an error about unbound arrays needing to be last in the class, so I moved it there and got some other errors.

I'm sure there are a thousand issues with the above code, but only recently started learning C++ (or Arduinos version of it), so don't judge it too harshly, lol. I do plan on reading the C book that Dennis Ritchie wrote (sooner rather than later, ideally).

Thanks in advance!
-J

imho you need a fixed size buffer in your class which can be used as temporary storage for your c-string.

const size_t buffersize = 42;
char buffer[buffersize];

and then you can use strcp/strcat to manipulate the buffer - but only up to the buffersize - nullterminator.

You can’t assume that the data you were sent to print have an infinite lifetime. Local variables will get removed from the stack for example and the pointer you might have kept is pointing now to garbage.

As @noiasca said you need to allocate enough memory for the area you want to maintain and take ownership of the content within the instance

Here is my go to reference for string functions. https://cplusplus.com/reference/cstring/

And character handleing. https://cplusplus.com/reference/cctype/

And C++ libraries.
https://cplusplus.com/reference/

Yeah, that's what I was thinking :-\ I wasn't sure what size I should use, I could obviously make it a config value that can be modified easily though.

Yeah, I think that's what was happening a bit ago. I had the timestamp in the upper right hand corner getting updated every second, and then it looked like other values in different areas were getting populated with the same value.

Yeah, I was reading through the cstring one, but I didn't see the cctypes info, I'll def look through that.

Thanks!
-J

Another way to do this is to use the SafeString-library
which offers almost the same comfort as the Strings.
It is based on array of chars. The name SafeString is program.
You cannot write to SafeStrings out of boundaries.

Though of your intention is to learn how to use cstrings with all details including proper boundary-checking and safe use of pointers etc.
The SafeString-library would only be of use to analyse how it is done inside the library

https://www.forward.com.au/pfod/ArduinoProgramming/SafeString/index.html

best regards Stefan

Hey, that's very neat. I learn best by example (or at least that's what I keep telling myself), so ill use this a bit to see what functions can do what I want, then ill check out how those are developed.

I was using the String class a while ago, but people kept saying "Don't use it, you'll regret it", and I thought everyone was just exaggerating it a bit.
Nope, it eventually ended up being more work than not using it. So I stay away from it. And when reading the wordpress page I posted above (The Evils of Arduino Strings), it really made it abundantly clear just how bad it is.

Strings are a bit of a tricky area on the Arduino. The String object was created to make working with blocks of text easier for people that don’t really know what they are doing when it comes to low level C++ programming. However, if you don’t know what you’re doing when it comes to low level C++ programming then it is very easy to abuse the String object in such a way that it makes everything about your sketch fragile and unstable.
...
So now you can see why the String class, which was created for use by people who don’t have advanced programming skills, is not a good thing for people who don’t have advanced programming skills to use.

Thanks again, everyone.

Whilst I don’t use it, It’s not as bad as the article make it sound if you avoid some of the pitfalls (use reserve, use multiple += instead of a long list of +, pass by reference when you can, use globals etc…)

Alright, I was able to get it working with the below code:

OledText.hpp:

#pragma once

#ifndef OLED_TEXT_H
#define OLED_TEXT_H

#include <Arduino.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1351.h>
#include <SPI.h>
#include <ArduinoSTL.h>
#include <cstddef>

// Including "settings" here just to make sharing it simpler
constexpr unsigned int  VALUE_TYPE_NONE     = 0;  
constexpr unsigned int  VALUE_TYPE_CHAR     = 1;  
constexpr unsigned int  VALUE_TYPE_INT      = 2;  
//constexpr unsigned int  MAX_CHAR_LENGTH     = 60;
const size_t MAX_CHAR_LENGTH                = 60; 


class OledText {
public: 
  Adafruit_SSD1351 * OLEDHandler;

  OledText(Adafruit_SSD1351 &OLEDHandler);

  char printerVal[MAX_CHAR_LENGTH]; 

  // Set/update the print containers display value to a string, then display it
  void print(char *charValue);
  //void print(char charValue[]);
  //void print(char charValue[25]);
  
  // Set/update the print containers display value to an integer, then display it
  void print(long intValue);

  // Function to re-print whatever value/type is saved (will be useful if the
  // printed text needs to be "reloaded" for various reasons)
  void print();

  // Function to prepend some text to the value currently in the print container
  void prepend(char *prependTxt);
  //void prepend(char prependTxt[25]);
  //void prepend(char prependTxt[]);
};

#endif // OLED_TEXT_H

OledText.cpp:

#pragma once

#include "OledText.hpp"

OledText::OledText(Adafruit_SSD1351 &OLEDHandler) : OLEDHandler (&OLEDHandler) {}

// If user were to set/change the printer to a CHAR...
void  OledText::print( char charValue[] )
{
  // char * strncpy ( char * destination, const char * source, size_t num );
  //strncpy(charValue, this->printerVal, MAX_CHAR_LENGTH);

  //this->printerVal = charValue;
  strcpy(this->printerVal, charValue);

  this->print();
}

// User provides a new NUMERICAL value, store it as a char
void  OledText::print( long intValue )
{
  char cstr[MAX_CHAR_LENGTH];
  ltoa( intValue, cstr, 10 );

  this->print(cstr);
}


// This would print the stored value in the specified area, but for this forum
// post, I'm just outputting the value to serial
void OledText::print()
{
  std::cout << this->printerVal << std::endl;
}

// User wants to prepend (or append, in a diff function) a string
// to the beginning of the char value..
void OledText::prepend( char prependTxt[] )
{
  if ( strlen( this->printerVal ) == 0 )
  {
    // Nothing to append anything to, just switch to a regular print
   return this->print( prependTxt );
  }

  char newCharValue[ strlen(prependTxt) + strlen(this->printerVal) +1 ];  // add +1 for end?...
  //char newCharValue[ MAX_CHAR_LENGTH ];
  strcpy( newCharValue, prependTxt );
  strcat( newCharValue, this->printerVal );

  // Print the new char (with the prefix prepended)
  this->print( newCharValue );
}

And the output:

Expected output: 1234
	-> 1234

Expected output: World
	-> World

Expected output: Hello World
	-> Hello World

Expected output: Hayy, Hello World
	-> Hayy, Hello World

Expected output: Goodbye
	-> Goodbye

Expected output: Goodbye
	-> Goodbye

Which is exactly what I wanted :slight_smile:

Still have a question though..

In the header file, I do set the size of printerVal via:

char printerVal[MAX_CHAR_LENGTH];

But when I declare the methods OledText::print() and OledText::prefix(), I take in a pointer parameter:

void print(char *charValue);
...
void prepend(char *prependTxt);

And initially, in the cpp file I was defining those two methods like so:

void  OledText::print( char *charValue ) {
    ...
}

void OledText::prepend( char *prependTxt ){
    ...
}

But that resulted in quite a few errors. I changed it to the following and it worked fine:

void  OledText::print( char charValue[] ) {
    ...
}

void OledText::prepend( char prependTxt[] ){
    ...
}

Is this because if I define the parameters as a pointer in the methods in the cpp file (as well as the hpp file) basically turns it into a "pointer to a pointer"?

Seems like these data members should be 'private':

The 'this->' notation is superfluous.

Perhaps you should post those error messages?

Yes, agreed. They are in the sketch im actually using for my project. This sketch is one I created quickly to simplify it to make posting it here easier. But yes, they will be private

Do you mean because it works without the this->? Or do you mean that I should use this. (dot) instead?

But yeah, I agree. I use to code in PHP and Perl back in the day, and I got use the this->var. But I suppose I should get in the habit of only using it when necessary.

Well, apparently I was doing something other than just using a pointer instead of a char array in the methods parameters, because to re-trigger the error I changed the .cpp file to:

#pragma once

#ifndef OLED_TEXT_H
#define OLED_TEXT_H

#include <Arduino.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1351.h>
#include <SPI.h>
#include <ArduinoSTL.h>
#include <cstddef>

// Including "settings" here just to make sharing it simpler
//constexpr unsigned int  VALUE_TYPE_NONE     = 0;  

const size_t MAX_CHAR_LENGTH                = 60; 


class OledText {
public: 
  Adafruit_SSD1351 * OLEDHandler;

  OledText(Adafruit_SSD1351 &OLEDHandler);

  char printerVal[MAX_CHAR_LENGTH]; 

  // Set/update the print containers display value to a string, then display it
  void print(char *charValue);
  //void print(char charValue[]);
  //void print(char charValue[25]);
  
  // Set/update the print containers display value to an integer, then display it
  void print(long intValue);

  // Function to re-print whatever value/type is saved (will be useful if the
  // printed text needs to be "reloaded" for various reasons)
  void print();

  // Function to prepend some text to the value currently in the print container
  void prepend(char *prependTxt);
  //void prepend(char prependTxt[25]);
  //void prepend(char prependTxt[]);

};
#endif // OLED_TEXT_H

And it seems to work perfectly fine... lol.


So which would be more appropriate? Using a pointer in the method parameters and the class variables?

P.S. In my last post I said I thought that having a pointer in both meant I would be pointing the pointer to a pointer, but just remembered that strcpy takes care of that for me, copying the value letter by letter.

Yes. When coding member functions, instead of this->memberName,
just use memberName.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.