C Part I · Module 8 · Strings
String Literals and String Pointers
Two declarations that look like minor stylistic variants of each other produce objects with different sizes, different lifetimes and different rules about what you are allowed to do with them. Both print the same text, both compare the same way, and both are accepted without comment by a compiler running every warning this curriculum has recommended so far. The difference only becomes visible when something tries to modify the text, at which point one of them works and the other is undefined behaviour — and because the compiler says nothing, the failure usually arrives at run time, on someone else's machine, long after the line was written. This chapter is about one character of syntax and the structural difference behind it, which is why it is the only chapter in this module marked critical.
Intermediate18 min readCPart IFundamentalsStringsPointers
Part I · Module 8 · Chapter 8.2 · String Literals and String Pointers
1. What a String Literal Is
"hello"Chapter 8.1 used literals only as array initialisers and never asked what one is on its own. The answer matters:
A string literal causes an array of
charto exist, holding the characters and a terminator, which lives for the entire run of the program.
Two consequences follow immediately, and the whole chapter is about them.
It is an array, so Chapter 7.6's conversion rule applies — in almost every expression it converts to a pointer to its first character.
The program does not own it. You did not declare it, it is not inside any function, and you are not permitted to modify it.
Chapter 7.6 §3 listed the one place the conversion does not happen, and deferred it to this chapter: when a string literal initialises a char array, the characters are copied into the array rather than the array being made to point at them. That exception is the entire difference between the two declarations below.
2. The Two Declarations
#include <stdio.h>
int main(void)
{
char owned[] = "hi";
const char *borrowed = "hi";
printf("sizeof owned %zu\n", sizeof owned);
printf("sizeof borrowed %zu\n", sizeof borrowed);
printf("owned %s\n", owned);
printf("borrowed %s\n", borrowed);
owned[0] = 'H';
printf("after owned[0] = 'H': %s\n", owned);
return 0;
}Output:
sizeof owned 3
sizeof borrowed 8
owned hi
borrowed hi
after owned[0] = 'H': HiBoth print hi. The sizeof results are the tell.
char owned[] = "hi"; is an array declaration. Chapter 7.6's exception applies, so the literal's three bytes are copied into a new array that belongs to this function. sizeof is 3 because the array is the storage. Modifying it is ordinary array assignment, and the output shows it working.
const char *borrowed = "hi"; is a pointer declaration. No exception applies, so the literal converts to a pointer to its first character, and that address is what gets stored. sizeof is 8 because the object declared here is a pointer; the characters live somewhere else entirely.
3. The Rule
Modifying a string literal is undefined behaviour. The standard imposes no requirements on a program that attempts it.
char *p = "hi";
p[0] = 'H'; /* undefined behaviour */This is the same classification Chapter 6.4 gave an out-of-range array access and Chapter 7.4 gave a null dereference, and it is treated the same way here: the program above is not run anywhere in this chapter, because whatever it printed would be one accident rather than something C promises.
What typically happens is worth knowing, with the right label attached. Implementations commonly place string literals in storage the operating system marks read-only, so an attempt to write there stops the program. That is common implementation behaviour, not a rule — and on a target without such protection, the write may quietly succeed and corrupt a literal that other parts of the program are still using.
4. Why const char * Is the Right Type
char *writable = "hi"; /* legal C, and a trap */
const char *readable = "hi"; /* says what is true */Both compile. The first is a char * aimed at storage you may not write to, so the type says one thing and reality says another — and Chapter 7.11 established exactly what const char * means: I will not write through this pointer.
That is not merely documentation here. It converts a run-time catastrophe into a compile-time error:
const char *readable = "hi";
readable[0] = 'H';error: read-only variable is not assignableAn error, before the program exists. The same mistake through a char * is undefined behaviour discovered at run time, if at all.
5. Choosing Between Them
The decision is about one question: does this text need to change?
| You need | Write | Why |
|---|---|---|
| Text you will modify | char buffer[N] = "..."; | the array is your storage |
| Text you will only read | const char *p = "..."; | no copy; the literal already exists |
| A function parameter that reads text | const char * | accepts arrays, pointers and inline literals |
| A function parameter that writes text | char * plus a size | Chapter 7.7's rule, and Chapter 8.5's |
The third row is why const char * appears in almost every string function signature you will ever read:
#include <stdio.h>
#include <stddef.h>
size_t length_of(const char *text)
{
size_t n = 0;
while (text[n] != '\0') {
n++;
}
return n;
}
int main(void)
{
char owned[] = "modifiable";
const char *literal = "literal";
printf("length_of(owned) %zu\n", length_of(owned));
printf("length_of(literal) %zu\n", length_of(literal));
printf("length_of(\"inline\") %zu\n", length_of("inline"));
return 0;
}Output:
length_of(owned) 10
length_of(literal) 7
length_of("inline") 6One parameter type accepted all three. The array converted by Chapter 7.6's rule; the const char * was already the right type; and the inline literal converted the same way the array did.
This is Chapter 7.11's asymmetry in its most useful form: a const char * parameter accepts everything, and a char * parameter would have rejected the literal. A function that only reads text should say const char *, and one that does not has restricted its callers for no benefit.
6. Are Two Identical Literals the Same Object?
#include <stdio.h>
int main(void)
{
const char *first = "shared";
const char *second = "shared";
char copy_a[] = "shared";
char copy_b[] = "shared";
printf("first == second ? %d\n", first == second);
printf("copy_a == copy_b ? %d\n",
(const char *)copy_a == (const char *)copy_b);
return 0;
}Output from this implementation:
first == second ? 1
copy_a == copy_b ? 0Read the two lines very differently.
The first result is unspecified. C permits an implementation to store identical literals once and share them, or to store them separately. This run shared them; another compiler, or the same one with different settings, may not. Do not write code whose correctness depends on either answer — and note that the sharing is also a second reason not to modify a literal, since a write through one pointer could change text that an unrelated part of the program is reading.
The second result is guaranteed. copy_a and copy_b are two array declarations, so they are two distinct objects with distinct addresses — Chapter 6.1's result, unchanged. Each got its own copy of the characters.
That asymmetry is the structural difference restated: arrays are always yours and always separate; literals are shared storage whose arrangement is the implementation's business.
7. Lifetime
One more property distinguishes them, and it becomes important in Module 9.
A string literal lives for the whole run of the program. It is not created when control reaches the declaration and not destroyed when a function returns.
An array declared inside a function lives as long as that call, exactly as Chapter 5.7's frame model describes.
The practical consequence, previewed here and made precise in Chapter 9.11:
const char *always_safe(void) { return "constant text"; } /* fine */
const char *never_do_this(void) { char local[] = "text"; return local; } /* broken */The first returns a pointer to a literal, which still exists after the function returns. The second returns a pointer to an array that stopped existing the moment the function did. Both compile; only one is usable. The storage-duration vocabulary that makes this precise is Chapter 9.3's, and the specific hazard is Chapter 9.11's.
8. What You Should Be Able to Say Now
- A string literal causes an array of
charto exist that lives for the whole program and that you do not own. char a[] = "hi"copies the literal's bytes into your own array — Chapter 7.6's third exception.char *p = "hi"stores an address; the characters stay where they were.sizeoftells them apart: 3 for the array, 8 for the pointer.- Modifying a string literal is undefined behaviour.
- "It will crash" is common implementation behaviour from read-only storage, not a C rule.
- The default warning set is completely silent on
char *p = "hi"; p[0] = 'H';. -Wwrite-stringsreveals the literal's real type —const char[3]— and flags it at the declaration.const char *turns the run-time disaster into a compile-time error, and as a parameter accepts arrays, pointers and inline literals alike.- Whether identical literals are shared is unspecified; that two arrays are distinct objects is guaranteed.
9. Exercises
Exercise 1 — give the sizes. For each, state sizeof and say whether the text may be modified.
char a[] = "abcd";
const char *b = "abcd";
char c[10] = "abcd";
char *d = "abcd";Exercise 2 — classify the statements. For each, say whether it is legal, a compile error, or undefined behaviour.
char a[] = "hi"; a[0] = 'H';
const char *b = "hi"; b[0] = 'H';
char *c = "hi"; c[0] = 'H';
const char *d = "hi"; d = "bye";
char e[] = "hi"; e = "bye";The last one is not about literals at all — say which chapter's rule rejects it.
Exercise 3 — explain the sizes. sizeof owned is 3 and sizeof borrowed is 8 for text that is identical. Explain both numbers in one sentence each, without using the word literal.
Exercise 4 — choose the declaration. For each requirement, give the declaration you would write and one sentence of justification.
- a greeting that will never change
- a buffer that will hold a label assembled at run time, up to 31 characters
- a parameter for a function that counts vowels in text
- a parameter for a function that converts text to upper case in place
Exercise 5 — read the diagnostic. -Wwrite-strings reports the type of "hi" as const char[3]. Explain what each of the three parts of that type tells you, and why the conversion to char * is the thing being flagged.
Exercise 6 — find the silent bug. This compiles cleanly at the curriculum's usual warning level and is broken. Say what is wrong, what would reveal it at compile time, and give two different repairs.
char *label = "temperature";
label[0] = 'T';
printf("%s\n", label);Exercise 7 — reason about sharing. first == second printed 1 on one implementation. Say whether you may rely on that, what the standard says, and give one concrete reason the sharing makes modifying a literal worse than it would otherwise be.
Exercise 8 — predict the lifetime bug. Of the two functions in §7, one returns a usable pointer and one does not. Say which, explain why in terms of Chapter 5.7's frame model, and say what the caller would observe.
10. Summary
- A string literal is an array of
charthat lives for the whole program and that you do not own. char a[] = "hi"copies;char *p = "hi"stores an address.sizeofsays 3 and 8.- Modifying a literal is undefined behaviour; the usual crash is the operating system, not C.
- The default warning set is silent;
-Wwrite-stringsexposes the literal's type asconst char[3]. const char *makes the mistake a compile-time error and, as a parameter, accepts every kind of string argument.- Use an array for text you will modify and a
const char *for text you will only read. - Sharing of identical literals is unspecified; distinctness of two arrays is guaranteed.
- A literal outlives every function; a local array does not — Chapters 9.3 and 9.11.
Two chapters have now established what a string is and where its bytes live, and both have counted characters with a hand-written loop rather than calling anything. That loop is worth writing properly, and so are the two operations that come immediately after it — copying text from one place to another, and deciding whether two pieces of text are the same. The standard library supplies all three, and this curriculum deliberately asks you to implement them first, because doing so converts the array-and-pointer relationship from a rule you can recite into one you can feel.
Standards & specifications
- Governing standard
- ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)
Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C Programming curriculum.
