Best practices with c strings -> char a[] vs char *b

Hi all,

I would like to get some guidance in the use of c strings in my firmware.
In the past, I would use String variables, even knowing that in embedded devices this could cause trouble (memory leaks? unsure).

So I understand this is not recommended:

String a = "string not recommended";

Anyhow, these days I’m trying to stay away, but I came across something I’d like to understand.

What is recommended to use with Particle devices, char b[] or char *b?

Example:

char amessage[] = "now is the time";  // <- is this the recommended?
char *pmessage = "now is the time";  // <- or is this the way to go?

Reading on the Internet, this is what I got from this StackOverflow answer:

(There’s) a subtle difference. Essentially, the former:

char amessage[] = "now is the time";

Defines an array whose members live in the current scope’s stack space, whereas:

char *pmessage = "now is the time";

Defines a pointer that lives in the current scope’s stack space, but that references memory elsewhere (in this one, “now is the time” is stored elsewhere in memory, commonly a string table).

So my question is: what version is better to use with Particle devices?

Thanks!
Gustavo.

1 Like

@gusgonnet when declaring an array, the name of the array is implicitly a pointer to the array data. So, amessage is a char pointer to the array data, which is the same as *pmessage (ie amessage == pmessage).

I prefer the array definition since I can easily index it like an array but also use its name as a pointer. With the pointer definition, it’s not as pretty. For example amessage[5] can be done with the pointer, for example, as *(pmessage+5).

2 Likes

perfect, thanks for the input, Paul.

In regards to memory use, would you know if there is a benefit with one vs the other?
Thanks again,
Gustavo

@gusgonnet, nope, no difference. The use of const with an array definition will place the array in program memory and not have to be loaded in RAM.

1 Like

Thank you!

There is also one difference you need to keep in mind.
With an array sizeof(arr) will return the actual byte count of the array but with sizeof(ptr) you will only ever get the size of the pointer (usually 4 byte) but not the size of the buffer reserved for your string.

Also the stackoverflow statement about arrays living on the stack is only true for local variables but not for globals, but that doesn’t come out in the quote.

Another thing would be the common use of either.
With char* you’d typically not reuse the same memory location to assign a new value but rather assign a new address to the pointer to have it point at a different string, with arrays you are definitely stating that you wish to reuse the same space.

While you can reuse the space referenced by the pointer the lack of information about the originally allocated space there will pose a problem once you replaced the original string with something shorter. While you can use strlen() on the original string to get the length of the buffer (minus 1) as long it lives, once you replaced it with something shorter, you won’t know how much space you just “lost”.
Hence const char* is pretty much the same as const char[] but for non-const I’d make a strong distinction between the two around the intended use and way to assign new values.

3 Likes

Superbe, thank you as well Andreas!

1 Like

This is not the case. amessage is definitely not equal to pmessage. And the array notation amessage[5] can be used just as easily with pmessage[5].

The declaration char *pmessage = "now is the time"; does not declare a local copy to point to. Even though this is not declared const it is still pointing towards the const string literal which most (all?) compilers should provide a warning about. Trying to modify a string literal through pmessage won’t work and might also crash your program depending on platform.

char *foo = "a const string";
const char *bar = "a const string";
char baz[] = "a const string";

foo[0] = 'X'; // compiler allows but won't work, may crash on some platforms
// bar[0] = 'Y';  // not allowed by compiler due to const
baz[0] = 'Z'; // works!

Log.info("%s %p %lu", foo, foo, sizeof(foo));
Log.info("%s %p %lu", bar, bar, sizeof(bar));
Log.info("%s %p %lu", baz, baz, sizeof(baz));

with the following output

0000001461 [app] INFO: a const string 0xf2c1b 4
0000001462 [app] INFO: a const string 0xf2c1b 4
0000001462 [app] INFO: Z const string 0x200172d8 15

The foo/bar pointers have the same value despite the change in const declaration. Both point towards a non-mutable string literal. The baz variable with array type gets a copy of the string literal in RAM and that copy can be modified.

1 Like

Hey Joel, thank you for your input (and logs!).

Just to double check my understanding, if in my firmware I use the following syntax:

I’m good to go.
Is that statement correct?

Thanks,
Gustavo.

@joel, geesh, I should know better than explain stuff late at night. What was meant was that amessage and pmessage will behave similarly in the context of the example.

I always like to code with explicit definitions wherever possible when I share code. The example of char *foo = "a const string" is an example of a (variable) pointer pointing to an implicit const (aka immutable) string. However, const char foo[] = "a const string" explicitly declares foo` a immutable pointer to an immutable string. To many readers, I believe this is clearer.

However, in both cases and due to the nature of C pointers, one can easily index outside of the const foo array or anywhere in memory with char *foo pointer. Pointers can be cruel :frowning_face:

@gusgonnet, that declaration will create the baz array in RAM and copy the (implicitly const) string “a const string” to it. You would use this if you want your char array initialized but expect to change its content later.

Using const char baz[] = "a const string" will keep both the array pointer baz and its content in flash (immutable). No modifications can be made. This would be used for keeping fixed messages to display, or print for example, or for storing a lookup table.

1 Like

Thanks for all the explanations above - they somehow make sense.

I do have a follow up query though: why the hell are String variables available if they supposedly cause so much trouble?
I while back I had a problem with a program where I was using String variables and was told to change them all to const char arrays which I did. It did not solve the problem - I found the actual mistake in the code (one of the timers). Since then I programmed another Photon with the same code but using String variables and have been running it side by side with the Photon running the const char code, and after 3 months, no problems.

When I started out with C we only had char arrays and pointers and it was fantastic when Strings were introduced. Anyway, are String variables really such a problem?

Two of the reasons:

  1. When used consciously they add a lot of convenience and readability for the programmer
  2. They are a fundamental concept of the Wiring/Arduino “dialect” many people got used to and hence cannot be omitted when committing to that “dialect”

They don’t have to be but are often overlooked as potential cause when debugging some ephemeral bug.
Whenever you have a bug that creeps up after some unknown time without your device doing anything different than normal heap fragmentation could be one of the underlying causes but often escapes the user’s attention.

Personally, I’d rather miss out on some convenience than having to track down a bug that’s not reliably reproducible nor directly caused by my immediate code.

4 Likes