C / ARRAYS
char arrays versus true C strings
Tell a plain char array from a NUL-terminated C string, size buffers for the extra byte, and print or copy arrays that have no terminator safely.
What you will learn
- Tell capacity (sizeof) from content length (strlen) and keep sizeof >= strlen + 1
- Predict which of char s[] = "cat", {'c','a','t'} and s[3] = "cat" is a real string
- Add the missing '\0' yourself after filling a char array element by element
- Print or copy non-terminated arrays with %.*s and memcpy instead of %s and strcpy
Understanding char arrays versus true C strings
A char array is nothing but a block of N bytes, and C keeps no length record beside it. What the standard library calls a string is a convention layered on that storage: the string is the run of bytes starting at a given address and ending at the first byte whose value is zero. That zero byte, written '\0', sits inside the array as an ordinary element, which is why a single 32-byte array can hold a two-character string now and a twenty-character one later.
The three ways of filling a char array do not produce the same thing. char s[] = "cat" reserves four bytes, because the compiler counts the letters and appends the terminator for you; char s[3] = {'c','a','t'} reserves exactly three and contains no terminator at all, so it is a char array but not a string. The trap is char s[3] = "cat", which C accepts without complaint: when the literal needs exactly as many bytes as the array, the terminator is silently discarded, and you are left with three letters that no string function can safely touch.
This matters because every library call that takes a char * and no count reads forward until it meets a zero byte: strlen, strcpy, strcat, puts and printf's %s all work that way. Hand one of them an array with no terminator and it walks past the last element into whatever memory follows, so sizeof (a capacity the compiler computes at the declaration) is safe while strlen (a length discovered by scanning at run time) is the one that can run away. When an array genuinely has no terminator, do not pretend otherwise: bound the operation instead, with a precision like %.3s, or with memcpy and an explicit byte count.
<stdio.h>
<string.h>
int main(void)
{
char literal[] = "cat"; /* 4 bytes: c a t \0 */
char letters[3] = { 'c', 'a', 't' }; /* 3 bytes, no \0 at all */
char fixed[4];
printf("literal: sizeof %zu, strlen %zu, %s\n",
sizeof literal, strlen(literal), literal);
/* letters has no terminator, so %s would read past its last byte */
printf("letters: sizeof %zu, printed as %.3s\n", sizeof letters, letters);
memcpy(fixed, letters, 3);
fixed[3] = '\0'; /* now it is a real string */
printf("fixed: sizeof %zu, strlen %zu, %s\n",
sizeof fixed, strlen(fixed), fixed);
return 0;
}
In C a string is not a type but a convention: a char array only becomes a string once a '\0' byte marks where its contents end.
Worked examples
The initialiser that fits too well
Shows that a string literal exactly as long as the array loses its terminator without any error.
<stdio.h>
<string.h>
int main(void)
{
char tight[2] = "hi"; /* the '\0' does not fit and is dropped */
char roomy[3] = "hi"; /* h, i, '\0' */
printf("tight: size %zu, printed as %.2s\n", sizeof tight, tight);
printf("roomy: size %zu, strlen %zu, printed as %s\n",
sizeof roomy, strlen(roomy), roomy);
return 0;
}
Example explained
Line 1char tight[2] = "hi"; is valid C, and most compilers accept it silently; a C++ compiler would reject it.
Line 2sizeof tight is 2, so both letters fit and nothing is left for the terminator; strlen(tight) would read past the array.
Line 3%.2s caps the read at two bytes, which is what makes printing tight defined behaviour.
Line 4roomy is one byte larger, so strlen reports 2 (content) while sizeof reports 3 (storage).
Moving the terminator
Demonstrates that the string length depends only on where the zero byte is, not on the array's size or its other contents.
<stdio.h>
<string.h>
int main(void)
{
char buf[16] = "hello world";
printf("%s | strlen %zu | sizeof %zu\n", buf, strlen(buf), sizeof buf);
buf[5] = '\0';
printf("%s | strlen %zu | sizeof %zu\n", buf, strlen(buf), sizeof buf);
printf("bytes 6 and 10 are still %c and %c\n", buf[6], buf[10]);
return 0;
}
Example explained
Line 1The initialiser copies 12 bytes (11 letters plus '\0') and zero-fills the remaining 4, so buf is a valid string with spare capacity.
Line 2strlen(buf) is 11 because it counts bytes up to the first zero; sizeof is 16 because that is the declared storage and it never changes.
Line 3buf[5] = '\0'; alters one byte, and that alone makes every %s and strlen see a five-character string.
Line 4buf[6] and buf[10] still hold 'w' and 'd': the terminator hides the tail from string functions, it does not erase it.
Important notes
'\0' is the byte with value zero; the character '0' is byte 48, so writing s[i] = '0' terminates nothing.
A missing terminator often appears harmless in small test programs because the memory just after the array happens to be zero; the code is still wrong and will misbehave once the surroundings change.
Common mistakes
Writing char name[5] = "hello"; because the word has five letters: the terminator is dropped without a diagnostic, and the next printf("%s", name) prints the five letters plus whatever bytes follow the array.
Filling a char array in a loop and forgetting the final s[i] = '\0': strlen then reports the distance to some unrelated zero byte, so the string looks longer than what was written, or the program crashes.
Allocating a copy with strlen(src) bytes instead of strlen(src) + 1: strcpy writes the terminator one byte past the end of the destination and corrupts whatever lives there.
Try it yourself
Change, predict, then run
Declare char letters[4] = {'f','i','s','h'}; and print it with printf("%.4s\n", letters). Then declare char fish[5], memcpy the four bytes in, set fish[4] = '\0', and print sizeof and strlen for fish to see where the extra byte goes.
Open the C workspaceCheck your understanding
Given char word[6] = "hello"; followed by word[5] = '!';, what happens at printf("%s\n", word);?
- It prints hello! because word has six bytes of storage.
- It prints hello, because printf stops at the array's declared size.
- Behaviour is undefined: the assignment overwrote the only zero byte, so printf keeps reading past the array.
- The assignment is rejected at compile time, since index 5 is reserved for the terminator.
Show answer
%s receives only a pointer and stops at the first zero byte, and word[5] was that byte, so printf now reads whatever follows the array. Option 0 is tempting because sizeof word really is 6, but that size exists only in the compiler's view of the declaration; nothing about it reaches printf at run time.