Skip to content
VLSI Mentor

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    "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 char to 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

Azvya Education Pvt. Ltd.VLSI Mentor
three bytes, or eight
#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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
sizeof owned     3
sizeof borrowed  8
owned            hi
borrowed         hi
after owned[0] = 'H':  Hi

Both 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.

Two declarations compared side by side. On the left, char owned equals a literal is a declaration you own; below it, three bytes holding h, i and the terminator, described as the array being the storage and modifiable. On the right, const char star borrowed equals a literal is a declaration that borrows; below it, eight bytes holding one address, described as the pointer being the storage; and below that again, the literal's own bytes, which live for the whole program and must not be written to, reached by an arrow labelled points at.char owned[] = ...a declaration you ownconst char *borrowed = ...a declaration that borrows3 bytesh i \0 · the array IS the storage ·modifiable8 bytes: one addressthe pointer is the storagethe literal's byteslives for the whole program · do notwritepoints at12
Figure 1 — the structural difference the sizes report. On the left, the declaration creates an array and the literal's bytes are copied into it, so the array is the storage and the program may modify it. On the right, the declaration creates a pointer, and what it holds is the address of the literal's own bytes — storage that exists for the whole run of the program and must not be written to. The left column has one level; the right column has two, and the extra level is somebody else's memory.

3. The Rule

Modifying a string literal is undefined behaviour. The standard imposes no requirements on a program that attempts it.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    const char *readable = "hi";
    readable[0] = 'H';
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
error: read-only variable is not assignable

An 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 needWriteWhy
Text you will modifychar buffer[N] = "...";the array is your storage
Text you will only readconst char *p = "...";no copy; the literal already exists
A function parameter that reads textconst char *accepts arrays, pointers and inline literals
A function parameter that writes textchar * plus a sizeChapter 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:

Azvya Education Pvt. Ltd.VLSI Mentor
one parameter type, three kinds of argument
#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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
length_of(owned)    10
length_of(literal)  7
length_of("inline") 6

One 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?

Azvya Education Pvt. Ltd.VLSI Mentor
sharing, and the thing you can rely on
#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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
first == second ?   1
copy_a == copy_b ?  0

Read 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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

  1. A string literal causes an array of char to exist that lives for the whole program and that you do not own.
  2. char a[] = "hi" copies the literal's bytes into your own array — Chapter 7.6's third exception.
  3. char *p = "hi" stores an address; the characters stay where they were.
  4. sizeof tells them apart: 3 for the array, 8 for the pointer.
  5. Modifying a string literal is undefined behaviour.
  6. "It will crash" is common implementation behaviour from read-only storage, not a C rule.
  7. The default warning set is completely silent on char *p = "hi"; p[0] = 'H';.
  8. -Wwrite-strings reveals the literal's real type — const char[3] — and flags it at the declaration.
  9. const char * turns the run-time disaster into a compile-time error, and as a parameter accepts arrays, pointers and inline literals alike.
  10. 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 char that lives for the whole program and that you do not own.
  • char a[] = "hi" copies; char *p = "hi" stores an address. sizeof says 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-strings exposes the literal's type as const 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.