Variable conversion to const char* or const uint8_t for PubSubClient

I'd like to know how to convert various variable types to send via the MQTT PubSubClient by O'Leary
I'm reading about pointers and de-referencing but I'm struggling to understand it all. In PubSubClient.h I see the 5 overloaded publish functions.

   boolean publish(const char* topic, const char* payload);
   boolean publish(const char* topic, const char* payload, boolean retained);
   boolean publish(const char* topic, const uint8_t * payload, unsigned int plength);
   boolean publish(const char* topic, const uint8_t * payload, unsigned int plength, boolean retained);
   boolean publish_P(const char* topic, const uint8_t * payload, unsigned int plength, boolean retained);

What is the difference between const char* and const uint8_t *?
What are the proper ways to convert bytes/integers/floats to send as the payload?

I came up with the following to publish my ESP's IP to a topic. Is there a smarter way?

      String IP = WiFi.localIP().toString();
      char ip[16];
      IP.toCharArray(ip, 16);
      mqttClient.publish("test/relay/ip", ip, 1);

What's a good way to setup my code to allow for string concatenation for the topics?

What is the difference between const char* and const uint8_t *?

1 byte = 8 bits
Each bit can either be a 1 (on) or 0 (off) - on/off as in a light switch.
So uint8_t and const char* are both variables that a 1 byte in size.

So if we do this in code 'uint8_t nI = 0;' then you can picture it like this.

+--+--+--+--+--+--+--+--+
nI: | 0 | 0 |0 | 0 | 0 |0 | 0 | 0|
+--+--+--+--+--+--+--+--+

In this example 'nI' is simply a convenient label for chunk of cpu memory that is exactly 1 byte/8 bits in length. 'nI' is simply a proxy for a physical address in the CPU's memory space.

Remember that a computer's memory is organized as a very large array of bytes someting like this:

+--+--+--+--+--+--+--+--+
nI: | 0 | 0 |0 | 0 | 0 |0 | 0 | 0| Address 0
+--+--+--+--+--+--+--+--+
| 0 | 0 |0 | 0 | 0 |0 | 0 | 0| Address 1
+--+--+--+--+--+--+--+--+
| 0 | 0 |0 | 0 | 0 |0 | 0 | 0| Address 2
+--+--+--+--+--+--+--+--+
| 0 | 0 |0 | 0 | 0 |0 | 0 | 0| Address 3
+--+--+--+--+--+--+--+--+
| 0 | 0 |0 | 0 | 0 |0 | 0 | 0| Address 4
+--+--+--+--+--+--+--+--+
| 0 | 0 |0 | 0 | 0 |0 | 0 | 0| Address 5
+--+--+--+--+--+--+--+--+
| 0 | 0 |0 | 0 | 0 |0 | 0 | 0|
+--+--+--+--+--+--+--+--+
| 0 | 0 |0 | 0 | 0 |0 | 0 | 0|
+--+--+--+--+--+--+--+--+
| 0 | 0 |0 | 0 | 0 |0 | 0 | 0|
+--+--+--+--+--+--+--+--+
| 0 | 0 |0 | 0 | 0 |0 | 0 | 0|
+--+--+--+--+--+--+--+--+
| 0 | 0 |0 | 0 | 0 |0 | 0 | 0|
+--+--+--+--+--+--+--+--+
| 0 | 0 |0 | 0 | 0 |0 | 0 | 0|
+--+--+--+--+--+--+--+--+
| 0 | 0 |0 | 0 | 0 |0 | 0 | 0|
+--+--+--+--+--+--+--+--+

uint8_t is interpreted as an integer between the values 0 and 255.
And remember that the computer represents integers in binary not decimal:

DECIMAL BINARY
0 0
1 1
2 10
3 11
4 100
5 101
6 110
7 111
8 1000
9 1001
10 1010
. .
. .
. .
255 11111111
256 100000000

So if we did this in code 'uint8_t nI = 10' then you could picture it like this:

+--+--+--+--+--+--+--+--+
nI: | 0 | 0 |0 | 0 | 1 |0 | 1 | 0|
+--+--+--+--+--+--+--+--+

Note that 256 requires 9 bits to represent it in binary, but a uint8_t variable only has 8 bits available.

This is called overflow and if you do this 'uint8_t nI = 256' in code and then println nI you will find that you get '0' in serial monitor.

If you do this in code 'char c = 0' then you can also picture it the same:

+--+--+--+--+--+--+--+--+
c | 0 | 0 |0 | 0 | 0 |0 | 0 | 0|
+--+--+--+--+--+--+--+--+

Except that, rather than being interpreted as a number between 0 and 255, c is interpreted as a character from the ascii table:

If you do this in code 'char *str;' then you can picture it like so

+--+--+--+--+--+--+--+--+
str | 0 | 0 |0 | 0 | 0 |0 | 1 | 0| address in computers memory space
| +--+--+--+--+--+--+--+--+
|
|
| +--+--+--+--+--+--+--+--+
| | 0 | 0 |0 | 0 | 0 |0 | 0 | 0| Address 0
| +--+--+--+--+--+--+--+--+
| | 0 | 0 |0 | 0 | 0 |0 | 0 | 0| Address 1
| +--+--+--+--+--+--+--+--+
+>| 0 | 0 |0 | 0 | 0 |0 | 0 | 0| Address 2
+--+--+--+--+--+--+--+--+
| 0 | 0 |0 | 0 | 0 |0 | 0 | 0| Address 3
+--+--+--+--+--+--+--+--+
| 0 | 0 |0 | 0 | 0 |0 | 0 | 0| Address 4
+--+--+--+--+--+--+--+--+
| 0 | 0 |0 | 0 | 0 |0 | 0 | 0| Address 5
+--+--+--+--+--+--+--+--+
| 0 | 0 |0 | 0 | 0 |0 | 0 | 0|
+--+--+--+--+--+--+--+--+
| 0 | 0 |0 | 0 | 0 |0 | 0 | 0|
+--+--+--+--+--+--+--+--+
| 0 | 0 |0 | 0 | 0 |0 | 0 | 0|
+--+--+--+--+--+--+--+--+
| 0 | 0 |0 | 0 | 0 |0 | 0 | 0|
+--+--+--+--+--+--+--+--+
| 0 | 0 |0 | 0 | 0 |0 | 0 | 0|
+--+--+--+--+--+--+--+--+
| 0 | 0 |0 | 0 | 0 |0 | 0 | 0|
+--+--+--+--+--+--+--+--+
| 0 | 0 |0 | 0 | 0 |0 | 0 | 0|
+--+--+--+--+--+--+--+--+

'str' contains the address of a separate chunk of computer memory - in the above example str is 'pointing to' the byte of computer memory that has the address '2'

A char* variable is interpreted specially. Unlike a uint8_t variable, the size is not built in to the datatype.

A uint8_t variable is ALWAYS 1 byte in size.

But a char* variable could be any size depending on how you initialize it, so you always need to pass the length along with the the pointer to any functions.

So for example if you do this in code 'char *str = new char[5]' then you can picture it like this:

+--+--+--+--+--+--+--+--+
str | 0 | 0 |0 | 0 | 0 |0 | 1 | 0| address in computers memory space (2)
| +--+--+--+--+--+--+--+--+
|
|
| +--+--+--+--+--+--+--+--+
| | 0 | 0 |0 | 0 | 0 |0 | 0 | 0| Address 0
| +--+--+--+--+--+--+--+--+
| | 0 | 0 |0 | 0 | 0 |0 | 0 | 0| Address 1
| +--+--+--+--+--+--+--+--+
+>| 0 | 0 |0 | 0 | 0 |0 | 0 | 0| Address 2 str[0]
+--+--+--+--+--+--+--+--+
| 0 | 0 |0 | 0 | 0 |0 | 0 | 0| Address 3 str[1]
+--+--+--+--+--+--+--+--+
| 0 | 0 |0 | 0 | 0 |0 | 0 | 0| Address 4 str[2]
+--+--+--+--+--+--+--+--+
| 0 | 0 |0 | 0 | 0 |0 | 0 | 0| Address 5 str[3]
+--+--+--+--+--+--+--+--+
| 0 | 0 |0 | 0 | 0 |0 | 0 | 0| Address 6 str[4]
+--+--+--+--+--+--+--+--+
| 0 | 0 |0 | 0 | 0 |0 | 0 | 0|
+--+--+--+--+--+--+--+--+
| 0 | 0 |0 | 0 | 0 |0 | 0 | 0|
+--+--+--+--+--+--+--+--+
| 0 | 0 |0 | 0 | 0 |0 | 0 | 0|
+--+--+--+--+--+--+--+--+
| 0 | 0 |0 | 0 | 0 |0 | 0 | 0|
+--+--+--+--+--+--+--+--+
| 0 | 0 |0 | 0 | 0 |0 | 0 | 0|
+--+--+--+--+--+--+--+--+
| 0 | 0 |0 | 0 | 0 |0 | 0 | 0|
+--+--+--+--+--+--+--+--+

Now char* variables also are usually (but not necessarily) used to represent strings of characters or readable text.

So the rule is that the very last element, in the example above it is Address 6 str[4], must contain the value 0 - a so called null terminator that denotes the end of the readable text.