Trying to understand const char usage

I am in that precarious position of "having a little knowledge is dangerous" !

This pertains to a code snippet from the VirtualWire library which i'm trying to comprehend.

This simple line; const char *msg = "hello";
which is then used in; vw_send((uint8_t *)msg, strlen(msg));

Ultimately, i want to know how to assign a string (or a value) from somewhere else to msg so that it can be transmitted by vw_send.

The problem being, there seems to be little reference to const and it's usage in the Arduino environment and have had to read up on a few C++ pages which were quite daunting for someone only dealing in Arduino programming language.

I'll start with a few questions and perhaps will come up with more as i try to better grasp the concept and usage of const which i gather is to do with efficient memory usage (?)

  1. If const is used for values that will not change, why was it used in the VirtualWire code like above ? It means it's locked to be "hello" and cannot be used by assigning it a value from another variable for eg.
[color=blue]msg = text_from_somewhere;[/color]
  1. is msg actually (just) a pointer to the 'h' (in "hello" which would be an array - yet declared as char ?) and then vw_send simply reads the subsequent characters (bytes?) until strlen(msg) ?

I won't mind pedantry in pointing out inaccuracies of the language/terms that i'm using as i'm really trying to understand this from first principles (although probably not too far back/deep until it involves understanding memory allocation. (i've been reading a bit (probably not enough) on Pointers in C++ !!)

Thanks in advance for your patience.

const does nothing more than tell the compiler that variable is to be read-only, and cannot be changed. It is a constant.

The "msg" in the argument list of a function does NOT refer to any specific variable. It is the LOCAL name, to be used WITHIN the function to refer to that argument. When you call the funciton, you pass the name of the actual variable you want the function to use.

char *msg1 = "This is msg1";
char *msg2 = "This is msg2";

void pmsg(char *msg)
{
    Serial.println(msg);
}

void loop()
{
    pmsg(msg1);   // Print msg1
    msg(msg2);    // Print msg2
}

Regards,
Ray L..

const char *msg = "hello";

msg is a pointer to an immutable string

BabyGeezer:

  1. If const is used for values that will not change, why was it used in the VirtualWire code like above ? It means it's locked to be "hello" and cannot be used by assigning it a value from another variable for eg.
[color=blue]msg = text_from_somewhere;[/color][/tt

[/quote]

Because it's a very stripped down, simple example program. Deliberately so.

BabyGeezer:

  1. If const is used for values that will not change, why was it used in the VirtualWire code like above ? It means it's locked to be "hello" and cannot be used by assigning it a value from another variable for eg.
[color=blue]msg = text_from_somewhere;[/color]

And if the example had been written like that, you'd be here asking "Why did they provide an example that does not compile?" or "Where does 'text_from_somewhere' come from?".
Examples are typically created to demonstrate ONE basic concept, with as little other extraneous information as possible. The fact that msg is declared as a const has NOTHING to do with the function itself. You can pass ANY string, with ANY name, coming from ANYWHERE to the function, not JUST the one named "msg". So, the fact that msg is declared const is totally irrelevent.

Regards,
Ray L.

RayLivingston:
...
So, the fact that msg is declared const is totally irrelevent.

Regards,
Ray L.

Thanks, this actually helped clarify it !

i am still unsure as to usage of const and thought it was essential for the library function, but declaring 'char *' is what is more important.

i get that it's simplified for the example - but i am running into problems of variable types when i try to replace const char *msg = "hello"; with a variable ; that blue code is what i'm trying to get at.

from blind trial & error, i found that the following compiles okay;

 char text_from_somewhere[1];
 text_from_somewhere[0]=incomingByte;  // from incomingByte = Serial.read();
 char *msg = text_from_somewhere;

it seems rather ugly - is there a more elegant version ?

What are you actually trying to send, a single character or a string?

BabyGeezer:

 char text_from_somewhere[1];

text_from_somewhere[0]=incomingByte;  // from incomingByte = Serial.read();
char *msg = text_from_somewhere;

Well.... that code is kind of nonsense.... text_from_somewhere is ALREADY of type char *, so why do you want to also set msg to the same value? You can simply use text_from_somewhere instead of msg. Also, if text_from_somewhere is intended to be a string, for example to be passed to Serial.print(), it MUST be terminated with a ';' character. Otherwise, functions like Serial.print have no way of knowing WHERE the string ends, and will keep sending characters until it FINDS a '\0' somewhere in memory. And a single character array is an utterly pointless construct. Just use char.

Regards,
Ray L.

aarg:
What are you actually trying to send, a single character or a string?

i'm trying to understand

const char *msg = "hello";

which is then used in;

vw_send((uint8_t *)msg, strlen(msg));

if it's a single character, how should it be, and if it's a string then what should be changed.

i'm still trying to grasp the usage of that asterisk - "hello" is a whole string, but it's being represented by *msg - as i asked earlier, is that then "just" a pointer to the 'h' ?

Yes, msg is just a pointer to the first character of the string. Serial.print would print the entire string by doing something VERY similar of this:

void serialPrint(char *s)
{
    while (*s)    // while the character pointed to by s is not '\0'
    {
        Serial.print(*s);   // print the character pointed to by s
        s++;                // Increment s, so it points to the next character
    }
}

Regards,
Ray L.

RayLivingston:
Well.... that code is kind of nonsense....

of course it is - i said it was from blindly doing trial and error because i have no idea what the correct way is !

RayLivingston:
text_from_somewhere is ALREADY of type char *

are you sure ?!

if it's

 char* msg = incomingByte;
...
...
 vw_send((uint8_t *)msg, strlen(msg));

the error is "invalid conversion from 'char' to 'char*' "

if it's

 char msg = incomingByte;
...
...
 vw_send((uint8_t *)msg, strlen(msg));

the error is "invalid conversion from 'char' to 'const char*' " (don't know how that came into the mix suddenly... :confused: )

RayLivingston:
You can simply use text_from_somewhere instead of msg.

no i cannot;

char incomingByte = 0;
incomingByte = Serial.read(); // this is what text_from_somewhere actually is
...
...
vw_send((uint8_t *)incomingByte, strlen(incomingByte));

the error is "invalid conversion from 'char' to 'const char*' "

and if i change the declaration of incomingByte, then;

char* incomingByte = 0;
incomingByte = Serial.read();

the error is "invalid conversion from 'int' to 'char*' "

BabyGeezer:
of course it is - i said it was from blindly doing trial and error because i have no idea what the correct way is !
are you sure ?!

Well, I've only been doing c programming for about 35 years, so, yeah, I'm pretty sure.

BabyGeezer:
if it's

 char* msg = incomingByte;

...
...
vw_send((uint8_t *)msg, strlen(msg));



the error is "invalid conversion from 'char' to 'char*' "

Of course it is, because you're trying to assign incomingByte, which is a char, to msg, which is a POINTER to a char. The compiler does not know how to convert a char to a pointer to a char, because that conversion makes no sense. If you want to make msg POINT TO incomingByte, then you either:

msg = &incomingByte;  // to force msg to point to incomingByte

or,

*msg = incomingByte;   // to copy incomingBye to the character pointed to by msg

or,

msg[0] = incomingByte;   // to copy incomingBye to the character of the array pointed to by msg

These last two assume msg ALREADY points to a valid char or array. A pointer, if not initialized, points to either whatever is at address 0 in memory, or some random location in memory. Writing to either of those is bad...

BabyGeezer:
if it's

 char msg = incomingByte;

...
...
vw_send((uint8_t *)msg, strlen(msg));



the error is "invalid conversion from 'char' to '**const** char*' " (don't know how __*that*__ came into the mix suddenly... :/ )

Because vw_send does NOT take a char as its first argument, it takes a const char *. Again, the conversion you're asking the compiler to make does not make sense.

You can write:

 vw_send(&msg, strlen(msg));

But, while that will compile, it will almost certainly not work as you want, since most functions that take a char* as an argument expect a pointer to a NULL-terminated string, NOT to a single char.

BabyGeezer:
no i cannot;

char incomingByte = 0;

incomingByte = Serial.read(); // this is what text_from_somewhere actually is
...
...
vw_send((uint8_t *)incomingByte, strlen(incomingByte));



the error is "invalid conversion from 'char' to 'const char*' "

This is exactly the same problem as the previous example...

BabyGeezer:
and if i change the declaration of incomingByte, then;

char* incomingByte = 0;

incomingByte = Serial.read();



the error is "invalid conversion from 'int' to 'char*' "

Your are creating a pointer to char, and setting it's value to zero, so it points to the first byte of memory. You then attempt to take the result of the Serial.read, and shove it into the pointer, so you now have a poitner that points to somewhere in memory, who knows where. That is a recipe for stomping on random bytes of memory. IF you want to USE the pointer, you must make sure it points to valid, allocated memory. You CAN do this:

char* incomingByte = &0;
*incomingByte = Serial.read();

The first line creates the pointer, then allocates a byte of memory either on the stack or the heap (depending on where in your program this line appears), then sets the value of that byte to 0, and finally sets the value of the pointer incomingByte to the address of that byte. NOW you can assign the value returned by the Serial.read to that byte of memory, as the second line does. The second line says call Serial.read(), then put the value it returns into the memory location pointed to by incomingByte. The '', when it appears in front of the name of a pointer, references the memory location pointed to by the pointer. Without the '' the pointer name references the value of the pointer itself. The '&' character, when it appears in front of any variable name, references the MEMORY ADDRESS of that variable, rather than the variables value. So '&' must be used when setting the value of a pointer so it points to a specific variable.

Regards,
Ray L.

Regards,
Ray L.

2 Likes

RayLivingston:
Well, I've only been doing c programming for about 35 years, so, yeah, I'm pretty sure.

Thank you for your patience and the comprehensive reply, i will take the time to really let it sink in to understand usage of pointers, referencing and dereferencing, etc.

In the meantime, could i just ask/clarify a few points;

RayLivingston:
...
These last two assume msg ALREADY points to a valid char or array. A pointer, if not initialized, points to either whatever is at address 0 in memory, or some random location in memory. Writing to either of those is bad...

Thank you for clarifying all the possibilities, it will help my understanding; but in this particular case, msg has not yet been declared/initialized.

I guess it's really just a matter of making vw_send use incomingByte

RayLivingston:
Because vw_send does NOT take a char as its first argument, it takes a const char *.

which brings me back to confusion, as we had determined that 'const' (in the example) was irrelevant earlier ?
what actually determined that msg needs a const char * ?
the function is declared like so;

uint8_t vw_send(uint8_t* buf, uint8_t len)

no reference to const anywhere, or am i missing something ?

FWIW - here's the code from inside VirtualWire.cpp that i think is relevant;

// VirtualWire.cpp
//
// Virtual Wire implementation for Arduino
// See the README file in this directory fdor documentation
//
// Changes:
// 2008-05-25: fixed a bug that could prevent messages with certain
//  bytes sequences being received (false message start detected)
//
// Author: Mike McCauley (mikem@open.com.au)
// Copyright (C) 2008 Mike McCauley
// $Id: VirtualWire.cpp,v 1.4 2009/03/31 20:49:41 mikem Exp mikem $

//#include "WProgram.h"

#include "VirtualWire.h"
#include <util/crc16.h>

...

// Wait until transmitter is available and encode and queue the message
// into vw_tx_buf
// The message is raw bytes, with no packet structure imposed
// It is transmitted preceded a byte count and followed by 2 FCS bytes
uint8_t vw_send(uint8_t* buf, uint8_t len)
{
    uint8_t i;
    uint8_t index = 0;
    uint16_t crc = 0xffff;
    uint8_t *p = vw_tx_buf + VW_HEADER_LEN; // start of the message area
    uint8_t count = len + 3; // Added byte count and FCS to get total number of bytes

    if (len > VW_MAX_PAYLOAD)
	return false;

    // Wait for transmitter to become available
    vw_wait_tx();

    // Encode the message length
    crc = _crc_ccitt_update(crc, count);
    p[index++] = symbols[count >> 4];
    p[index++] = symbols[count & 0xf];

    // Encode the message into 6 bit symbols. Each byte is converted into 
    // 2 6-bit symbols, high nybble first, low nybble second
    for (i = 0; i < len; i++)
    {
	crc = _crc_ccitt_update(crc, buf[i]);
	p[index++] = symbols[buf[i] >> 4];
	p[index++] = symbols[buf[i] & 0xf];
    }

    // Append the fcs, 16 bits before encoding (4 6-bit symbols after encoding)
    // Caution: VW expects the _ones_complement_ of the CCITT CRC-16 as the FCS
    // VW sends FCS as low byte then hi byte
    crc = ~crc;
    p[index++] = symbols[(crc >> 4)  & 0xf];
    p[index++] = symbols[crc & 0xf];
    p[index++] = symbols[(crc >> 12) & 0xf];
    p[index++] = symbols[(crc >> 8)  & 0xf];

    // Total number of 6-bit symbols to send
    vw_tx_len = index + VW_HEADER_LEN;

    // Start the low level interrupt handler sending symbols
    vw_tx_start();

    return true;
}

...

BabyGeezer:
I am in that precarious position of "having a little knowledge is dangerous" !

This pertains to a code snippet from the VirtualWire library which i'm trying to comprehend.

This simple line;

const char *msg = "hello";

which is then used in;

vw_send((uint8_t *)msg, strlen(msg));

Ultimately, i want to know how to assign a string (or a value) from somewhere else to msg so that it can be transmitted by vw_send.

The problem being, there seems to be little reference to const and it's usage in the Arduino environment and have had to read up on a few C++ pages which were quite daunting for someone only dealing in Arduino programming language.

I'll start with a few questions and perhaps will come up with more as i try to better grasp the concept and usage of const which i gather is to do with efficient memory usage (?)

  1. If const is used for values that will not change, why was it used in the VirtualWire code like above ? It means it's locked to be "hello" and cannot be used by assigning it a value from another variable for eg.
[color=blue]msg = text_from_somewhere;[/color]
  1. is msg actually (just) a pointer to the 'h' (in "hello" which would be an array - yet declared as char ?) and then vw_send simply reads the subsequent characters (bytes?) until strlen(msg) ?

I won't mind pedantry in pointing out inaccuracies of the language/terms that i'm using as i'm really trying to understand this from first principles (although probably not too far back/deep until it involves understanding memory allocation. (i've been reading a bit (probably not enough) on Pointers in C++ !!)

Thanks in advance for your patience.

Perhaps a diagram might help your understanding:

msg: 16 bit/2 byte variable that contains an unsigned integer that corresponds to a physical address in the CPU's memory space, i.e. it is a 'pointer' to another location in memory. In the example it points to address 2 / 10 (in binary)
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
<- Address 0 -><- Address 1 ->

Haven't bothered to change the 1s and 0s
+---+---+---+---+---+---+---+---+
| 0 | 1 | 1 | 0 | 0 | 1 | 0 | 1 | Address 2 - > contains ASCII code for 'h'
+---+---+---+---+---+---+---+---+
| 0 | 1 | 1 | 0 | 0 | 1 | 0 | 1 | Address 3 - > contains ASCII code for 'e'
+---+---+---+---+---+---+---+---+
| 0 | 1 | 1 | 0 | 0 | 1 | 0 | 1 | Address 4 - > contains ASCII code for 'l'
+---+---+---+---+---+---+---+---+
| 0 | 1 | 1 | 0 | 0 | 1 | 0 | 1 | Address 5 - > contains ASCII code for 'l'
+---+---+---+---+---+---+---+---+
| 0 | 1 | 1 | 0 | 0 | 1 | 0 | 1 | Address 6 - > contains ASCII code for 'o'
+---+---+---+---+---+---+---+---+
| 0 | 1 | 1 | 0 | 0 | 1 | 0 | 1 | Address 7 - > contains ASCII code 0 which is called a null terminator
+---+---+---+---+---+---+---+---+ and denotes the end of the string.

Urrgh! That diagram turned out messy. Hold that thought for a bit.

Try this instead.

It is a word document but I had to zip it so that it would be accepted as an attachment.

Diagram.zip (9.66 KB)

BabyGeezer:
...which brings me back to confusion, as we had determined that 'const' (in the example) was irrelevant earlier ?
what actually determined that msg needs a const char * ?
the function is declared like so;

uint8_t vw_send(uint8_t* buf, uint8_t len)

no reference to const anywhere, or am i missing something ?

Don't get so hung up on the const, just accept that the compiler is right, since it pretty much always is.

The PURPOSE of const, in the context of a function argument, is to tell the compiler that the function should not be allowed to write that argument. That's it. It is useful to the person writing the function, because if he screws up and tries to modify the argument, the compiler will throw an error. It's useful to the person calling the function, because it makes it clear that the function will NOT modify the argument, so he/she can feel safe in passing a variable that must not be modified. That's it.

For example:

#include <ESP8266WiFi.h>

void setup() {
  // put your setup code here, to run once:

}

void someFunc(char *s, const char *t)
{
    Serial.print(s);
    Serial.print(t);
}

void loop() {
char *msg1 = "hello";
const char *msg2 = "goodbye";

someFunc(msg1, msg2);  // No error
someFunc(msg1, msg1);  // No error, since char * can be converted to const char *
someFunc(msg2, msg2);  // Gives "can't convert const char * to char *"

}

The first call to someFunc is fine, because both arguments are already of the correct type.
The second call the someFunc is fine, because the compiler KNOWS to convert char * to const char *
The third call to someFunc causes an error, because the first argument should be char *, but is actually const char *. The compiler CANNOT convert const char * to char *, because char * is writeable, while const char * is NOT writeable. So there is NO valid conversion.

You may also, in some cases, need to do an explicit type cast, by preceding the variable name in the call to a function with the desired type enclosed in parens. The compiler will allow you to do this even in cases where the resulting conversion will NOT be correct. For example, in the above, you can chage the third call to someFunc to this:

someFunc((char *)msg2, msg2);  // Over-rid "can't convert const char * to char *", and make msg2 writeable

The program WILL now compile. BUT, you've just given someFunc permission to write to msg2, even though you've declared it as const. So, use explicit casts with care, as you can shoot yourself in the foot, and create some VERY hard to find bugs.

Regards,
Ray L.

1 Like

RayLivingston:
Don't get so hung up on the const, just accept that the compiler is right, since it pretty much always is.

i certainly wouldn't dream of challenging the compiler at it's game ! :smiley:

i'll just have to accept that it makes the determination based on how everything else has been declared.

RayLivingston:
The PURPOSE of const, in the context of a function argument, is to tell the compiler that the function should not be allowed to write that argument. That's it. It is useful to the person writing the function, because if he screws up and tries to modify the argument, the compiler will throw an error. It's useful to the person calling the function, because it makes it clear that the function will NOT modify the argument, so he/she can feel safe in passing a variable that must not be modified. That's it.

i really appreciate you explaining it in precise language and giving various examples where it can and cannot work. (i still have to fully grasp the concept of pointers before it all comes together though.)

am i correct to understand that the part in bold means, "it can only read that argument" ?

RayLivingston:
You may also, in some cases, need to do an explicit type cast, by preceding the variable name in the call to a function with the desired type enclosed in parens. The compiler will allow you to do this even in cases where the resulting conversion will NOT be correct. For example, in the above, you can chage the third call to someFunc to this:

someFunc((char *)msg2, msg2);  // Over-rid "can't convert const char * to char *", and make msg2 writeable

The program WILL now compile. BUT, you've just given someFunc permission to write to msg2, even though you've declared it as const.

am i correct to think that this is what is happening with vw_send ?

vw_send([color=blue](uint8_t *)[/color]msg, strlen(msg));

that part in blue is a type cast, correct ?

Once again, thank you very much for your patience in explaining the concept.

Well, you can only read or write variables, and const prohibits writing, so, yes, you can only read const variables.

Yes, (uint8_t *)msg is type casting msg to type uint8_t *.

If you declare:

char *charPtr;
char charVar;

then use charPtr in expressions:

charPtr ==> the VALUE of varPtr, which is a MEMORY ADDRESS
charVar = 'A'; // Make the VALUE of charVar == 'A'

&charVar ==> the MEMORY ADDRESS of variable charVar which can be assigned to a char * like charPtr
charPtr = &charVar; // Make charPtr POINT TO charVar

*charPtr ==> the VALUE of the variable POINTED TO by pointer charPtr, called "de-referencing" the pointer
char x;
charPtr = &charVar;
x = *charPtr; // this is EXACTLY the same as:
x = charVar;

Regards,
Ray L.

1 Like

RayLivingston:
Yes, (uint8_t *)msg is type casting msg to type uint8_t *.

which means, it forces whatever is being passed into msg to be a pointer type. (i hope i'm finally getting this !)

Again - thanks for explaining pointers so clearly and much more concise than the tutorials i've been reading.