строки

В Си для представления строк используются массивы символов.

Каждая строка в качестве завершающего символа содержит символ \0 т.н. нулевой символ / нулевой байт.

char message[] = "Hello";
size_t length = sizeof(message)/sizeof(char);   // 6 символов
for(size_t i=0; i<length; i++){
  printf("%d ", message[i]); // 72 101 108 108 111 0
}
Если бы мы определяли массив message не как строку, а именно как массив символов, то последним элементом должен был бы идти нулевой символ:
char message[] = {'H', 'e', 'l', 'l', 'o', '\0'};

Явное определение длинны

// a b c \0 - все верно
char abc[4] = "abc";
 
// нулевой байт будет отброшен
char abc[3] = "abc";
 
// лишнее место заполниться нулевыми байтами
char abc[5] = "abc";
 
// поведение определено компилятором
// например gcc отбросит лишние символы
char abc[2] = "abc";

Строка как указатель

Но в языке Си также для представления строк можно использовать указатели на тип char:

#include <stdio.h>
 
int main(void) {
  char *hello = "Hello METANIT.COM!";
  printf("%s", hello);
  return 0;
}

Заданные таким образом строки изменять нельзя, в том числе указателями.

В языке Си для работы со строками применяется такой механизм как string interning или интернирование строк. В этом случае строки в виде строковых литералов сохраняются в приложении в секции .rodata (read-only data), которые предназначены для данных только для чтения, а строковые литералы рассматриваются как неизменяемые данные.

Указатель на строковой литерал содержит адрес первого бита литерала в этой секции неизменяемых данных

Если два указателя с разными идентификаторами будут ссылаться на одинаковые строковые литералы - они будут ссылаться на один и тот же адрес в памяти.
Как и любому указателю, такой строке можно задать значение NULL, но это будет не то же самое, что "" тк такая строка не будет содержать нулевого байта.