Understanding char and String data types - data conversion

I’m trying to wrap my head around char’s and string’s and String’s, and when they are null terminated vs when they are individual bytes of data.

I know the following will work:

String keys[12] = {"1", "2", "3", "4", "5", "6", "7", "8", "9", "*", "0", "#"};

foo(keys[4]);  // pass “5” to function
   .
   .
   .

void foo(String s) {
  Serial.println(s);   // will print 5
}

But if I have a character array

keys[] = “123456789*0#”;

And want to pass:

foo(keys[4]);   // pass “5” - probably just one byte

It will generate a compiler error, and also since in this case keys[4] is a byte, it won’t be null terminated.

If I wanted to do it this way, what would I need to do to make this function call work?

if you declare a char-array like this then there is the variable-type missing.
Does this compile?

should'nt it be
char keys[] = “123456789*0#”;
?

the declaration of your function "foo" has to have char as the type for the parameter
void foo(char myChar) {
}

For future questions you should post always the complete sketch.
And in case of a compiler-error the full compiler-log

shouldn't it be
char keys[] = “123456789*0#”;
?

Yes, sorry. I was trying to boil the question down to its simplest form and lost some information.

I'm not so much concerned with the compiler error (it's valid), just trying to find out if the foo() function is defined to receive a String and a char parameter, and I want to pass one character to it out of a text sequence such as

char keys[] = “123456789*0#”;
foo(keys[4]);

How would I go about converting keys[4] to something that foo() would accept?

Thanks.

Yes and now you should post all the information.

That depends on the declaration of your function foo!

post your complete sketch then I can see what you have coded wrong.

You are working on an informatic project and what is most needed in an informatic project is information.

In your case this means post your complete sketch.
What is so hard about posting your complete sketch?

The complete sketch is 598 lines and it's working using the parameter structure at the top of the original question.

All I'm trying to find out is, given that foo() expects a String parameter, what data conversion would need to be done to pass one character to it in the second example. It is a learning question about data conversion, not a programming issue.

It is one byte. Use

void foo (char s) {

and have foo() do something appropriate with the byte.

It is best to avoid using the String class and objects on Arduinos, especially AVR-based ones like the Uno, as Strings cause memory problems and program crashes.

Hello IraSch

Check this small example:

char keys[] = {"123456789 * 0#"};
void setup()
{
  Serial.begin(115200);
  foo(keys[4]);
}
void loop() {}

void foo(char number)
{
  Serial.println(number);
}

Have a nice day and enjoy coding in C++.

1 Like

strip it down to that what you want to know.

posting just some lines might result in responses which doesn't answer your question.

call by reference might do what you want:

String keys[12] = {"1", "2", "3", "4", "5", "6", "7", "8", "9", "*", "0", "#"};

void foo(String s) {
  Serial.println(s);   // will print 5
}


char keyb[] = "123456789 * 0#";
void bar(char &c) {
  Serial.println(c);
}

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


  foo(keys[4]);  // pass “5” to function
  bar(keyb[4]); 

}

void loop() {
  // put your main code here, to run repeatedly:

}

One critical point is that println is overloaded to take lots of different arguments

    size_t println(const String &s);
    size_t println(const char[]);
    size_t println(char);

So this works

char keys[] = "123456789*0#";
// String keys[12] = {"1", "2", "3", "4", "5", "6", "7", "8", "9", "*", "0", "#"};

void foo(String s) {
  Serial.println(s);
}

void foo(char c) {
  Serial.println(c);
}

void setup() {
  Serial.begin(115200);
  foo(keys[4]);
}

void loop() {
}
1 Like

when you do this

or let the compiler decide the size

String keys[] = {"1", "2", "3", "4", "5", "6", "7", "8", "9", "*", "0", "#"};

you actually define an array of 12 Strings instances (each entry in the array is a String instance that could grow independently of each other. you could do keys[4] = "Hello World"; for example.)

if you call foo(keys[4]); and foo() is defined as

void foo(String s) {
  Serial.println(s);   // will print 5
}

that's OK since keys[4] is indeed a String (capital S, an instance).

What the function receives is actually a copy of the String that was in the array, if you modify it, you don't modify the String that's in the array.


if you define

you are defining an array of chars (each entry in the array is a char) and you left the compiler decide the size and offered as an initialisation list a cString which is null terminated. So keys actually has 13 bytes, the last one being the trailing null char you don't see but is part of the cString as per the C/C++ standard when you use the double quotes for a text (a string literal).

if you wanted only 12 characters, then you would need to do

char keys[12] = {'1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '0', '#'};
or
char keys[] = {'1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '0', '#'};
(note the single quotes for char, whereas double quotes are for cStrings)

then the initialisation is actually made of 12 individual chars and so that's the size of the array.

In both cases, keys[4] is actually just a char, it's not a String. When you call the foo function with keys[4] , which is a char , the compiler looks for a matching function signature that can accept a char as an argument. It does not find one but it finds that foo is defined to accept a String parameter, so it's trying to find an implicit conversion from char to String — but there are none and so it results in a compilation error.

You would need to call foo(String(keys[4])); and then the parameter is actually a String instance and you initialized this String through the String constructor which accepts a char.

alternatively, you could define a second version of the foo() function which signature would show it accepts a char as a parameter instead of a String as shown by @kenb4

1 Like

depending on what microcontroller you use.
Using Strings can eat up all RAM and then start to overwrite other variables which leads to real strange code-behaviour that might only occur from time to time.
Which means it is very hard to debug.

Each time you do any kind of String-operation like

  • passing a parmater,
  • assigning a String to another String
  • adding some characters to a String,

another piece of RAM is occupied.
If your loop runs 10000 of times this leads to program-crashes.

So if ever possible don't use String (The one with the capital S). on small RAM-microcontrollers like Arduino Uno or Mega.

Ecxept you really know how to avoid total RAM-occupation cause by Strings

If you are free to choose a c_string (array of char with null-termination
or an array of char (without null-termination

Use one of them. But don't use String.

I kinda agree that using cStrings is fine for most of what we do here (non nul terminated char arrays are not strings, just arrays)

Yet, there are some misconceptions in what you state.

No. worse case scenario is that the heap reaches the stack, which is trapped and you get a stack overflow error and crash. Otherwise what happens is that you run out of memory and the String operations just do not complete and you are not told so, so your code can't really take that into account.

  • you can pass parameter by reference so no copy
  • assigning a String to another String is indeed duplicating the memory — as for any other type (an int, a struct, ...). So not sure what's your point.
  • adding some characters to a String can be done somewhat more gently to the heap if you use += with the String class but of course you need more space to accommodate the extra character, as you would with a cString

if you use more memory than you have yes.
if your Strings are local variables, they are freed-up upon exiting the loop, so memory is claimed and unless you've done weird stuff with local and global variables and poked holes into the heap, it's not as bad as you make it sound since the memory is available again for the next round.

So to get a clearer picture of what is happenening
how about a demo-code that shows pretty complex String-operations that will make the code

a. not crash
b. poking holes into the heap so the heap will grow and keep growing over time.

my very rough estimation is:

as soon as you do repeated re-assigning Strings of different lengths sometimes longer sometimes smaller this will cause the heap to grow and grow

sorry for OT:

I don't see how this is helps to answer the TO opening post.

Read about and use reserve() - Arduino Reference or open your own thread.

Title: Understanding char and String data types - data conversion
The title asks for dataconversion.
Depending on how and where the conversion is done this might lead to problems.
It will help to understand how to safely use Strings.

"estimations" do not work :slight_smile: technical proof is what matters and understanding what is at play.

A String object does not shrink. So if you have a global instance that you want to duplicate through unoptimised code like aString = aString + "0123456789"; then you are asking for a temporary area that is as big as the aString variable and then expanding it with the operation's parameter before changing the back end of aString and freeing the original buffer which is returned to the heap. That's when things can "break" (operation will silently fail) if there is not a large enough free block in the heap to accommodate the operation.

Of coures, code fails too if your Strings grow beyond the memory limit, then sure you run out of heap space. That's like any other variable, you can't use more memory than you have.

But if what you do is grow (within memory limits) and discard the strings (local variables which get freed up) then the various tiny bits of heap areas that used to be disconnected become reunited as one big area that you can tap into again during the next loop.

Doing "weird stuffs" consists in allocating and not freeing a byte here and there to prevent the disconnected areas in the heap to become united again.

here is a visual representation of what happens in the heap as you grow a String


the allocation function keeps tracks of blocks and will reuse larger blocks as they come free/reunited, so it's not just eating up all the way to the end of memory and then get stuck.

but if you start poking holes (the green cells) with memory that does not get freed-up then you limit the ability of your String to expand as you prevent the reunion of the areas

Totally agree. Hence the question how does code look like that is
a. safe
b. leads to crashes

Now that you get what happens, go for it :slight_smile:

On an AVR it's hard to lead to a crash as the String class just stops doing what's being ask (like concatenating strings etc) if there is not enough memory. The code won't crash because of the String, it will just misbehave as the expected operation won't have been done.

Thank you for the answer. I could have sworn I tried String(keys[4]) when I first saw the compiler error and it also failed compilation. But this String() conversion does work as it should.

The sketch runs on a MKR1000 and I had already changed to use char instead or String daa type when the issue first came up. But I wanted to learn how to deal with the function call data type conversion should the need arise again. And now I know - use String().

Thanks again!

Glad if this helped