A true "ARRAY OF STRINGS". Here's how I did it using pointers. :)

My project needed an “Array of Strings” to simplify the code, allowing me to manipulate several strings at once using “for” loops, instead of having to type a separate line for each string I want to read, write, or edit.

But when I googled for info on [arduino “array of strings”], I was astonished to find each reference taking me instead to examples of character arrays being loaded with constants, such as:

char* myStrings={“This is string 1”, “This is string 2”, “This is string 3”,
“This is string 4”, “This is string 5”,“This is string 6”};

That won’t work for me at all; because, (1) Constants are constants, “hard wired” so to speak, not something you’re sketch can change as needed. And (2) they are going into char arrays, not Strings, which means Adruino’s rich list of String-manipulation functions won’t work on them.

One reason for using something so “carved in stone”, is that they use much less of Arduino’s precious memory. And this is fine if you don’t plan to manipulate them further within your program.

A second reason is that a true “Array of Strings” requires using pointers; and so many people find the subject of pointers rather mind-boggling, even frightening.

Well, I look at it this way: A pointer is simply the address where an object in your program lives, no different from the address where I live. The mind-boggling part is that if I simply told someone “801 Main St.”, they’d have no idea what I was talking about. It could be the location of a house, a grocery, a fire station, a water park, or most anything else. And even worse, “801 Main St.” doesn’t even tell them how far or in which direction!

But three tools in the Arduino language make using pointers a lot simpler than it might at first seem: (1) The ‘&’ symbol is like writing the address down on a piece of paper and handing it to someone through the ‘=’ symbol. (2) Even better yet, the ‘*’ symbol actually takes you to that address, where you can knock on the door, repaint the front porch, or whatever you have in mind. (No road-map required.)

And finally, (3) you’d probably like to know WHAT you’re being taken to before you hitch a ride, and not wind up at the city dump after expecting a fancy restaurant. Arduino takes care of this too, by naming the kind of pointer before you use it. For example, the word “String” in the line, “String * fString[8];” below is the promise that when later used with “fString”, the ‘*’ symbol will take you to a “String” (not the city dump, or even a “char” array). And a promise is a promise! If you try to give it the address to something else, your sketch won’t compile; and an error message will inform you that was NOT a string!

So really, pointers aren’t that difficult at all. You just need to write down the address where you want to go (using ‘&’), ask for sure if it’s what you think (in this case, “String”), and then hop aboard the ‘*’ bus to enjoy the ride all the way there. :slight_smile:

Here’s a little sketch to demonstrate using a real “Array of Strings”, (and of course, our friend, the pointer).

// To see the result of this sketch after loading it, open the 
// serial monitor and then press the reset button.

String str = "^John Smith^Susie Maye^Tom Jones^Sarrah Jones^"
             "Martha Mayes^Brenda Howard^Dan Evens^";
String s0,s1,s2,s3,s4,s5,s6;// <-- Create real strings

String * fString[8];// <-- The '*' says this will be an array of pointers,
                    // while the word 'String' says these pointers will
                    // point to real Strings.
                    
int strIndex = 0;

String splitStr()
{
  if (strIndex == -1)
    return "";
  byte i = str.indexOf('^',strIndex)+1;
  strIndex = str.indexOf('^',i);
  if (strIndex == -1)
    return "";
  else
    return str.substring(i,strIndex);
}

void setup()
{
  Serial.begin(9600);
  while (!Serial) {};
  
  fString[0] = & s0;// <-- The '&' symbol sends just the String's pointer
  fString[1] = & s1;//     through the '=' symbol to the array.
  fString[2] = & s2;//
  fString[3] = & s3;// <-- We still must load the real Strings into the
  fString[4] = & s4;//     array, one by one.
  fString[5] = & s5;//
  fString[6] = & s6;//
}

void loop()
{
   byte i;
   for (i=0; i<7; i++)//       <-- Now we can use the "Array of Strings" to
     *fString[i] = splitStr();//   make life simple, using "for" loops!
  Serial.println(str);
  Serial.println();

  // Although I only printed these strings below, you can use
  //  any of the many String functions on these "*fString[n]"
  //  Strings. Available string functions are listed here:
  // http://www.arduino.cc/en/Reference/StringObject

  for (i=0; i<7; i++)
    Serial.println(*fString[i]);
  while (true) {};
}
1 Like

If your preference is to use String objects over a simple block of memory, why not just use a real 'Array of Strings'

String array[] = { 
  "John Smith", 
  "Susie Maye", 
  "Tom Jones", 
  "Sarrah Jones", 
  "Martha Mayes", 
  "Brenda Howard", 
  "Dan Evens"
};

You could still have the names separate (and in PROGMEM), then load them in.

1 Like

You can have an array of pointers to char array strings, but unless they have a fixed maximum length, you need to allocate and de-allocate memory for them as you go, which is possible but not something for the person who lacks attention to detail.

I must be missing something here… What you have created is NOT an “array of Strings”, it is an array of pointers to Strings. Why not simply create a true array of Strings:

String fString[8];    // This is a REAL array of Strings
...

for (i=0; i<7; i++)//       <-- Now we can use the "Array of Strings" to
     fString[i] += splitStr();//   make life simple, using "for" loops!

Regards,
Ray L.

CosmickGold:
And (2) they are going into char arrays, not Strings, which means Adruino’s rich list of String-manipulation functions won’t work on them.

A lot of people here, me included, advise against the “rich” String manipulation class. Too easy to fragment memory.

Anyway, an array of strings can be done like this:

const int NUMBER_OF_ELEMENTS = 10;
const int MAX_SIZE = 12;

char descriptions [NUMBER_OF_ELEMENTS] [MAX_SIZE] = { 
 { "Furnace on" }, 
 { "Furnace off" }, 
 { "Set clock" }, 
 { "Pump on" }, 
 { "Pump off" }, 
 { "Password:" }, 
 { "Accepted" }, 
 { "Rejected" }, 
 { "Fault" }, 
 { "Service rqd" }, 
 };


void setup ()
  {
  Serial.begin (115200);
  Serial.println ();
  
  for (int i = 0; i < NUMBER_OF_ELEMENTS; i++)
    Serial.println (descriptions [i]);
  }  // end of setup

void loop ()
  {
    
  }  // end of loop

Now that is an array of strings, in RAM, which you can change later. Yes, they are fixed sizes. Or you can make an array of pointers and malloc/free them as required, if you aren’t too concerned about fragmentation.

Comment #3 above, by RayLivingston, is the perfect answer for me, much better than what I explained at the top. Thank you for showing me the best way to achieve my goals.


The next two comments below, #6 and #7 by Nick Gammon, are due to my having re-arranged my posts after being given the pefrect solution. So I've now put my first post back the way it was, and won't "do that again". (Of course, that makes comments #6 and #7 below seem a little strange. Sigh )

Since my original post is now "obsolete", I've moved it down to become comment #5, to keep a record of it.

This thread is now incredibly confusing, please don't do that again.

Let me see:

  • Accepted solution now in original post
  • Suggestion in reply #2
  • Original question in reply #5

Have you been watching Doctor Who? Do you like time travel? Do you want the solution, then the question?

See the forum posting guidelines. Changing the original post in a radical way makes all the responses after it look stupid.

Well, it turns out Nick Gammon was right. He said above:

A lot of people here, me included, advise against the “rich” String manipulation class. Too easy to fragment memory.

I had my set of seven strings – each only eight characters long – working perfectly. But when I added the next group of functions to my sketch, my Arduino memory whent all to heck, just like he said it would!

So I’ve had to program it all over again; this time, using char arrays.

Hoping it might help someone, here are some things I’ve found out about char arrays:

I used a little of Nick Gammon’s code from above to set it up:

const int NUMBER_OF_ELEMENTS = 7;
const int MAX_SIZE = 9;
char fNames [NUMBER_OF_ELEMENTS] [MAX_SIZE];

The MAX_SIZE must be one larger than you’re actually going to use. (I’m using 8 characters in each line, so the MAX_SIZE must one greater, 9.)

This line:
fNames[3] = fNames[5];
won’t work at all. Forget about the ‘=’ sign. It just gives the error message:
“invalid array assignment”.

However char array’s DO work with “return” (thank goodness!), as in the following:

String getAPhrase(byte i)
{
  i = min(i,8);
  return fNames[i];
}

void loop()
{
  Serial.println(getAPhrase(7));
}

Notice that the return type on the above function is “String”, not “char”. I’m not sure why that’s necessary, but it works.

Below is a function I’m actually using in my sketch, that show’s a way of working with “char arrays” instead of “an array of Strings”.

(FYI: The below function is in an Arduino with an LCD displaying the resulting file names in a menu. And it reads these names in from another Arduino, attached to an SD Card with the files.)

void getFNames()
{
  byte sNum=0; // "sNum" means "String Number"
  byte cNum=0; // "cNum" means "Character Number"
  char c;
  moreFilesB = true;
    // After requesting a list of files, wait for the reply.
  while (waveSerial.available() == 0) {;}
    // Read one character at a time, only accept the first seven file names.
  while ((waveSerial.available() > 0) && (sNum < 8))
  {
    // Place the character in 'c', to be tested in several ways.
    char c = char(waveSerial.read());
    if (c == ':')  // If it is a ':', there are no more files (nor characters coming);
    {
      // If still "true" a "More Files >" item will show in the menu. So,
      moreFilesB = false; 
      break;
    }
      // If its a '^' you are betweein files. Prepair for the next file name.
    else if (c == '^')
    {
      sNum++;
      cNum = 0;
    }
    else
    {
      if (cNum < 8) // Only accept the first eight characters of the file's name.
      {
        // If just starting the name, first blank out any previous name with spaces.
        // Otherwise, if a previous name was longer, the end might still show.
        if (cNum == 0) 
          for (byte i=0; i<8; i++)
            fNames[sNum][i] = ' ';
        fNames[sNum][cNum] = c; //Enter the file's name, one character at a time.
        cNum++;
      }
    }
    // If we check for more characters before before the next one arrives,
    // we will exit this function early. So a delay to wait for it is important.
    delay(10); 
  }
  // Now fill the remaining char array's with spaces, so old names wont show.
  for (; sNum<7; sNum++)
    for (cNum=0; cNum<8; cNum++)
      fNames[sNum][cNum] = ' ';
  drawMenu(6);
}

This line:
fNames[3] = fNames[5];
won't work at all. Forget about the '=' sign. It just gives the error message:
"invalid array assignment".

Not sure why you'd want to, unless you're using your pool as a scratch pad. But to get around this you use strcpy():

strcpy( fNames[3], fNames[5] );
String getAPhrase(byte i)

{
  i = min(i,8);
  return fNames[i];
}

Wouldn't bother with String class at all. Just return a char *:

char *getAPhrase(byte i)
{
  i = min(i,8);
  return fNames[i];
}

Cool techniques.

I'd seen strcpy(), but didn't try it, thining it required a library. It does not, and your test line:

strcpy( fNames[3], fNames[5] );

only ate 12 bytes of memory!

Also, changing to your "char *getAPhrase" example saved a whooping 354 bytes! But I need to know how to set the receiving end to accept it. (I tried, but got error messages one way and garbage out the other.)

Show me your new code where you are using it, and we should be able to spot the error and why its happening.

Chances are your expecting a String on the other side and calling String related methods. These can all be switched out to use C strings, be leaner, meaner and quicker :wink:

strcpy does require a library, but it is a library of functions, not objects, so only there is not extra overhead. The compiler is smart enough to only pull in the code it uses.

Good to know, Keith. I had wondered about that. I’m especially glad to know the compiler is smart enough to leave out the “junk”. Are you meaning that the compiler would pull in “objects”, even when they were not used?


Thank you for your kind offer, Tammy!

I pulled out the different kinds of functions that use “String”, and made the below sketch with them. Just ignore everything except the few “String” involvements.

If you can make everything work “leaner, meaner and quicker”, this will be a major help to my current sketch, as well as sketches I write in the future. …and that’s a really exciting thought!

A way to see if you’ve succeed is to compare what it prints now, with what it prints when you’re done.

At the moment, it prints:

start…
Name: 12312345.DMA
filename: 12312345.DMA
Furnace off ~
Set clock ~
Pump on ~
Pump off ~
Password: ~
Accepted ~
Rejected ~
00:01
00:02
00:03

#define bcd2bin(h,l)    (((h)*10) + (l))

typedef struct ds1302_struct
{
 uint8_t Seconds:4;      // low decimal digit 0-9
 uint8_t Seconds10:3;    // high decimal digit 0-5
 uint8_t CH:1;           // CH = Clock Halt
 uint8_t Minutes:4;
 uint8_t Minutes10:3;
 uint8_t reserved1:1;
 union
 {
   struct
   {
     uint8_t Hour:4;
     uint8_t Hour10:2;
     uint8_t reserved2:1;
     uint8_t hour_12_24:1; // 0 for 24 hour format
   } h24;
   struct
   {
     uint8_t Hour:4;
     uint8_t Hour10:1;
     uint8_t AM_PM:1;      // 0 for AM, 1 for PM
     uint8_t reserved2:1;
     uint8_t hour_12_24:1; // 1 for 12 hour format
   } h12;
 };
 uint8_t Date:4;           // Day of month, 1 = first day
 uint8_t Date10:2;
 uint8_t reserved3:2;
 uint8_t Month:4;          // Month, 1 = January
 uint8_t Month10:1;
 uint8_t reserved4:3;
 uint8_t Day:3;            // Day of week, 1 = first day (any day)
 uint8_t reserved5:5;
 uint8_t Year:4;           // Year, 0 = year 2000
 uint8_t Year10:4;
 uint8_t reserved6:7;
 uint8_t WP:1;             // WP = Write Protect
};

unsigned long Mils = millis();
unsigned long clockTicks;

#include <SD.h>
File myFile;

const int NUMBER_OF_ELEMENTS = 10;
const int MAX_SIZE = 12;
char fNames [NUMBER_OF_ELEMENTS] [MAX_SIZE] = { 
{ "Furnace on" }, 
{ "Furnace off" }, 
{ "Set clock" }, 
{ "Pump on" }, 
{ "Pump off" }, 
{ "Password:" }, 
{ "Accepted" }, 
{ "Rejected" }, 
{ "Fault" }, 
{ "Service rqd" }, 
};

const String sp = "                              ";

String title(byte i)
{
 return fNames[i];
}

String FileDate()
{
 ds1302_struct rtc;
 rtc.Month10 = 1;
 rtc.Month = 2;
 rtc.Date10 = 3;
 rtc.Date = 1;
 rtc.h24.Hour10 = 2;
 rtc.h24.Hour = 3;
 rtc.Minutes10 = 4;
 rtc.Minutes = 5;

 char buffer[80];     // the code uses 70 characters.
 //DS1302_clock_burst_read( (uint8_t *) &rtc);
 sprintf( buffer, "%02d%02d%02d%02d.DMA", \
   bcd2bin( rtc.Month10, rtc.Month), \
   bcd2bin( rtc.Date10, rtc.Date), \
   bcd2bin( rtc.h24.Hour10, rtc.h24.Hour), \
   bcd2bin( rtc.Minutes10, rtc.Minutes));
 return buffer;
}

void doClock()
{
 String s = "";
 if ((millis() - Mils) > 1000)
 {
   Mils = millis();
   unsigned long ticks = Mils - clockTicks;
   unsigned long minutes = ticks / 60000;
   ticks = ticks % 60000;
   unsigned long seconds = ticks / 1000;
   if (minutes < 10)
     s = "0";
   s += minutes;
   s += ":";
   if (seconds < 10)
     s += "0";
   s += seconds;
   Serial.println(s);
 }
}

boolean openFile()
{
 String Name = FileDate();
 Serial.print("Name: ");
 Serial.println(Name);
 char filename[Name.length()+1];
 Name.toCharArray(filename, sizeof(filename));
 myFile = SD.open(filename, FILE_WRITE);
 Serial.print("filename: ");
 Serial.println(filename);
}

void setup()
{
 Serial.begin(9600);
 while (!Serial) {}; 
 Serial.println("start...");
 openFile();
 clockTicks = millis();
 byte i = 1;
 while ((i<8) && (title(i) != "        "))
 {
   Serial.println("  "+title(i)+sp+'~');
   i++;
 }
}

void loop()
{
 doClock();
}

No, just that an object has a bit more overhead than a function.

Not tidy, but I think thats all String occurrences killed off.

#include <SPI.h>
#include <SD.h>

#define bcd2bin(h,l)    (((h)*10) + (l))

typedef struct ds1302_struct
{
  uint8_t Seconds: 4;     // low decimal digit 0-9
  uint8_t Seconds10: 3;   // high decimal digit 0-5
  uint8_t CH: 1;          // CH = Clock Halt
  uint8_t Minutes: 4;
  uint8_t Minutes10: 3;
  uint8_t reserved1: 1;
  union
  {
    struct
    {
      uint8_t Hour: 4;
      uint8_t Hour10: 2;
      uint8_t reserved2: 1;
      uint8_t hour_12_24: 1; // 0 for 24 hour format
    } h24;
    struct
    {
      uint8_t Hour: 4;
      uint8_t Hour10: 1;
      uint8_t AM_PM: 1;     // 0 for AM, 1 for PM
      uint8_t reserved2: 1;
      uint8_t hour_12_24: 1; // 1 for 12 hour format
    } h12;
  };
  uint8_t Date: 4;          // Day of month, 1 = first day
  uint8_t Date10: 2;
  uint8_t reserved3: 2;
  uint8_t Month: 4;         // Month, 1 = January
  uint8_t Month10: 1;
  uint8_t reserved4: 3;
  uint8_t Day: 3;           // Day of week, 1 = first day (any day)
  uint8_t reserved5: 5;
  uint8_t Year: 4;          // Year, 0 = year 2000
  uint8_t Year10: 4;
  uint8_t reserved6: 7;
  uint8_t WP: 1;            // WP = Write Protect
};

unsigned long Mils = millis();
unsigned long clockTicks;

File myFile;

const int NUMBER_OF_ELEMENTS = 10;
const int MAX_SIZE = 12;
char fNames [NUMBER_OF_ELEMENTS] [MAX_SIZE] = {
  { "Furnace on" },
  { "Furnace off" },
  { "Set clock" },
  { "Pump on" },
  { "Pump off" },
  { "Password:" },
  { "Accepted" },
  { "Rejected" },
  { "Fault" },
  { "Service rqd" },
};

//const String sp = "                              ";

char *title(byte i)
{
  return fNames[i];
}

char *FileDate()
{
  ds1302_struct rtc;
  rtc.Month10 = 1;
  rtc.Month = 2;
  rtc.Date10 = 3;
  rtc.Date = 1;
  rtc.h24.Hour10 = 2;
  rtc.h24.Hour = 3;
  rtc.Minutes10 = 4;
  rtc.Minutes = 5;

  char buffer[80];     // the code uses 70 characters.
  //DS1302_clock_burst_read( (uint8_t *) &rtc);
  sprintf( buffer, "%02d%02d%02d%02d.DMA", \
           bcd2bin( rtc.Month10, rtc.Month), \
           bcd2bin( rtc.Date10, rtc.Date), \
           bcd2bin( rtc.h24.Hour10, rtc.h24.Hour), \
           bcd2bin( rtc.Minutes10, rtc.Minutes));
  return buffer;
}

void doClock()
{
  //String s = "";
  if ((millis() - Mils) > 1000)
  {
    char time_string[16];
    Mils = millis();
    unsigned long ticks = Mils - clockTicks;
    unsigned long minutes = ticks / 60000;
    ticks = ticks % 60000;
    unsigned long seconds = ticks / 1000;
    /* if (minutes < 10)
       s = "0";
     s += minutes;
     s += ":";
     if (seconds < 10)
       s += "0";
     s += seconds;*/
    sprintf( time_string, "%2d:%2d", minutes, seconds );
    Serial.println( time_string );
  }
}

boolean openFile()
{
  char *name_p = FileDate();
  Serial.print("Name: ");
  Serial.println(name_p);
  //char filename[Name.length()+1];
  //Name.toCharArray(filename, sizeof(filename));
  myFile = SD.open(name_p, FILE_WRITE);
  Serial.print("filename: ");
  Serial.println(name_p);
}

void setup()
{
  Serial.begin(9600);
  while (!Serial) {};
  Serial.println("start...");
  openFile();
  clockTicks = millis();
  byte i = 1;
  while ((i < 8) && (title(i) != "        "))
  {
    Serial.print( "  " );
    Serial.print( title(i) );
    Serial.println( "                              ~" );
    i++;
  }
}

void loop()
{
  doClock();
}

I certainly appreciate you fixing the program for me Tammy, replacing memory-consuming Strings with better options. Your work made quite an improvement.

The only thing that still had a problem was the clock. There was a leading space instead of a leading '0', and the second "%2d" wouldn't work, always printing just a zero.

// The need for a leading '0' was fixed by replacing:

"%2d:%2d"

// with:

"%02d:%02d"

// And all the digits began working when I replaced:

   unsigned long minutes = ticks / 60000;
   ticks = ticks % 60000;
   unsigned long seconds = ticks / 1000;

// with:

   unsigned int minutes = ticks / 60000;
   ticks = ticks % 60000;
   unsigned int seconds = ticks / 1000;

//  reducing the "long" to just an "int".

//  Evidently, sprintf() couldn't handle the size of two longs.

Thanks to you, I'm back on track and moving forward again. :slight_smile: :slight_smile:

Well, I ran into another string problem I hadn't thought to ask about; namely:

(1) How do I read strings in and out of functions?
(2) How do I combine several strings into one long string, and pass the combination to a function?

With help from Google, I've found an asnwer, and am now sharing it.


Previously, with the String class, I could combine strings with a '+' sign, and move them with an '=' sign; like this:

string4 = string1 + string2;

and later add more with:

string4 += string3;

Similarly, I could give them to a function like this:

myFunction(string1+string2+string3);

I almost fell back to using just one String-class string, to combine my strings. But when I looked at the compiled dfference, my "waking up" the String class with that one litte string had eaten 1414 more byes of memory! No way! So I continued my search until I fould the below answer, and this sketch works perfectly.

char buffer [50];
char str [] = " and stay" ;

void loadBuffer(char one[], char two[], char three[])
{
  strcpy(buffer, one);
  strcat(buffer, two);
  strcat(buffer, three);
}

void printing(char s[])
{
  Serial.println(s);
}

char *myCase(byte i)
{
  switch (i)
  {
    case 1: return "Eat healthy";
    case 2: return "Love life";
  }
}

void setup()
{
  Serial.begin(9600);
  while (!Serial) {};  
  loadBuffer(myCase(1),str," happy.");
  printing(buffer);
  loadBuffer(myCase(2)+str," happy.");
  printing(buffer);
}

void loop() {}

The secret is to create a buffer big enough to hold the combined strings. Then use this "loadBuffer" function. It's output is a Zero-terminated C string; exactly what's needed.

Also notice the use of pointer '*' and brackets '' required to make things work.

(And of course, if there is yet a better way than this, let me know and I'll switch to it!)

Don't fall back on using String class, the convenience isn't worth the overhead.

If your strings that you're 'loaded' are coming from a variable unknown source (ie website, network or anywhere you don't have control over the contents), it might be worth checking and verifying they fit your buffer:

void loadBuffer(char one[], char two[], char three[])
{
  int combined_length = strlen(one)+strlen(two)+strlen(three)+1;

  if( combined_length > sizeof( buffer ))
  {
    Serial.println( "LoadBuffer strings exceed size of destination buffer, not copying" );
    buffer[0] = 0;
    return;
  }
  strcpy(buffer, one);
  strcat(buffer, two);
  strcat(buffer, three);
}

Hey yeah!

(Tammy to the rescue, once again!)

In my case, these strings are coming from a customer-removable/replaceable SD-Card. So you're right; longer strings could be read in that would overwrite memory belonging to other objects.

I'm adding your "string-size checker", right now.


OK, I added it, and all works great.

The output from "loadBuffer" goes to an LCD card, so that's where I need the error message to display. To achieve that, I've modified your "loadBuffer" code to the below, and tested the result:

void loadBuffer(char one[], char two[], char three[])
{
  int sSize = strlen(one)+strlen(two)+strlen(three)+1;
  if( sSize > sizeof(buffer))
    strcpy(buffer,"String too long!");
  else
  {
    strcpy(buffer, one);
    strcat(buffer, two);
    strcat(buffer, three);
  }
}

There's another, unexpected benefit coming from not using the String class: I see my LCD display now loads text at least 50% faster. Wow!