Question
How can I correctly create an array of strings in C? I tried the following code:
char (*a[2])[14];
a[0] = "blah";
a[1] = "hmm";
GCC reports an assignment from incompatible pointer type warning. Why does this happen, what is the correct way to create the array, and why might printf(a[1]); still appear to print hmm?
Short Answer
You will learn that C strings are character arrays ending in \0, and that an “array of strings” can be represented either as an array of pointers to string literals or as a two-dimensional character array that owns writable character storage. You will also see why the original declaration has a different type than expected and why printf(a[1]); is unsafe.
Concept
A C string is an array of char values terminated by a null character ('\0'). For example, the literal "blah" occupies five characters in memory:
b l a h \0
C has no built-in string type, so an array of strings is usually created in one of two ways:
- An array of pointers: each element points to the first character of a string.
- A two-dimensional character array: each row contains storage for one string.
Your declaration is neither of these common forms:
char (*a[2])[14];
Read it from the identifier outward:
a[2]meansais an array with two elements.*a[2]means each element is a pointer.(*a[2])[14]means each pointer points to an array of 14charvalues.
Therefore, a[0] has type char (*)[14]: a pointer to an entire 14-character array. But "blah", in most expressions, becomes a pointer to its first character, with type char * in traditional C terminology (and it should be treated as non-modifiable). A char * is not the same as a char (*)[14], so the compiler warns you.
Choosing the right representation matters because it determines ownership and mutability:
- Use pointers when the strings already exist, such as string literals or command-line arguments.
- Use a two-dimensional array when your program needs its own fixed-size writable storage for every string.
Mental Model
Think of a string as a row of letter tiles ending with a special stop tile ('\0').
An array of string pointers is like a list of addresses to existing rows of tiles. The rows may be in different places and may have different lengths.
words
+---------+ +---+---+---+---+---+
| address | ---> | b | l | a | h | \0|
+---------+ +---+---+---+---+---+
| address | ---> | h | m | m | \0|
+---------+ +---+---+---+---+
A two-dimensional character array is like reserving identical 14-tile slots in one cabinet. Every row has space for at most 13 visible characters plus its stop tile.
words[0]: [ b l a h \0 _ _ _ _ _ _ _ _ _ ]
words[1]: [ h m m \0 _ _ _ _ _ _ _ _ _ _ ]
Your original code instead creates a list of pointers to whole 14-tile rows. A normal string pointer points to one tile—the first character—not to a complete row object, which is why the types do not match.
Syntax and Examples
Option 1: Array of pointers to string literals
Use this when the strings are fixed text that you do not need to modify.
#include <stdio.h>
int main(void) {
const char *a[2] = {"blah", "hmm"};
printf("%s\n", a[0]);
printf("%s\n", a[1]);
return 0;
}
a contains two const char * pointers. Each pointer refers to the first character of a string literal.
Use const char * because modifying a string literal is undefined behavior:
const char *text = "blah";
/* text[0] = 'B'; */ /* Invalid: do not modify a string literal. */
You can change which string a pointer refers to:
const char *a[2] = {"blah", "hmm"};
a[0] = "updated";
Option 2: A writable two-dimensional array
Use this when each string needs storage owned by the array and must be editable.
#include <stdio.h>
#include <string.h>
int main(void) {
char a[2][14] = {"blah", "hmm"};
strcpy(a[0], "hello");
printf("%s\n", a[0]);
printf("%s\n", a[1]);
return 0;
}
Here, a[0] is an actual array of 14 characters. It can hold at most 13 non-null characters because one position is required for the terminating \0.
You cannot assign a new string directly to an array after initialization:
char a[2][14] = {"blah", "hmm"};
/* a[0] = "hello"; */ /* Invalid: arrays cannot be assigned. */
Copy characters into the row instead, ensuring the destination is large enough.
Step by Step Execution
Consider this writable array example:
char words[2][14] = {"blah", "hmm"};
printf("%s\n", words[1]);
Step by step:
words[2][14]reserves space for two rows, each containing 14charvalues.- The initializer
"blah"is copied intowords[0]as{'b', 'l', 'a', 'h', '\0', ...}. - The initializer
"hmm"is copied intowords[1]as{'h', 'm', 'm', '\0', ...}. - In this context,
words[1]is converted to a pointer to its first character:&words[1][0]. %stellsprintfto read characters starting at that pointer until it finds\0.- The output is:
hmm
Now compare the pointer version:
const char *words[2] = {"blah", "hmm"};
printf("%s\n", words[1]);
wordsreserves space for two pointers.words[1]stores the address of the first character of the literal"hmm".%sfollows that address and prints until\0.
Both forms work with %s, but their memory layout, ownership, and ability to modify text are different.
Real World Use Cases
- Menu labels and fixed messages: Use
const char *labels[] = {"Open", "Save", "Quit"};for text that does not change. - Days, months, or status names: Store a lookup table of fixed strings, such as HTTP method names or log-level labels.
- Form fields and buffers: Use
char fields[10][64];when a program must store and edit up to 10 user-provided values. - Parsing input files: Read fixed-width names or identifiers into a two-dimensional buffer.
- Command-line arguments:
argvis effectively an array of pointers to character sequences, commonly declared aschar *argv[]inmain. - Embedded systems: Fixed-size two-dimensional arrays are useful when dynamic allocation is undesirable and maximum text lengths are known.
Real Codebase Usage
Developers usually make the ownership rule clear in the type.
Read-only lookup tables
static const char *const error_names[] = {
"OK",
"NOT_FOUND",
"TIMEOUT"
};
The first const means the characters should not be changed. The second const means the pointers in the table should not be reassigned.
Fixed-size writable records
#define MAX_USERS 100
#define NAME_SIZE 32
char user_names[MAX_USERS][NAME_SIZE];
Before copying external input, code validates length so there is room for \0.
#include <stdio.h>
#include <string.h>
int set_name(char destination[32], const char *source) {
if (source == NULL) {
return 0;
}
if (strlen(source) >= 32) {
return 0;
}
strcpy(destination, source);
return 1;
}
This uses a guard clause: invalid input returns early, and the copy happens only when it is safe.
Safe output
Always give printf a fixed format string when printing a string:
printf("%s\n", user_names[0]);
This avoids treating data as formatting instructions.
Common Mistakes
Declaring the wrong pointer type
This declaration creates pointers to arrays, not pointers to characters:
char (*a[2])[14];
Use one of these instead:
const char *a[2] = {"blah", "hmm"};
char a[2][14] = {"blah", "hmm"};
Modifying a string literal
This compiles on many systems but has undefined behavior:
char *message = "hello";
message[0] = 'H';
Use writable array storage when modification is needed:
char message[] = "hello";
message[0] = 'H';
Using printf(a[1])
Do not write:
printf(a[1]);
printf expects its first argument to be a format string. If the stored text contains %, printf tries to interpret it as a conversion specification. This can produce incorrect output or undefined behavior.
Write:
printf("%s\n", a[1]);
It may appear to work with "hmm" only because that particular text contains no % characters. It does not prove that the original incompatible assignment is correct.
Forgetting room for the null terminator
This array is too small for "hello" because the string needs six characters including \0:
char word[5] = "hello"; /* Invalid: no room for '\0'. */
Correct:
char word[6] = "hello";
Copying an oversized string into a row
char names[2][5];
strcpy(names[0], "hello"); /* Overflow: needs 6 characters. */
Check the destination capacity before copying, and ensure the result is null-terminated.
Comparisons
| Representation | Example declaration | Stores | Can change characters? | Best for |
|---|---|---|---|---|
| Array of string pointers | const char *a[2] | Two pointers | No, for literals | Fixed labels and lookup tables |
| Two-dimensional char array | char a[2][14] | 28 character slots | Yes | Fixed-capacity writable strings |
| Pointer to an array | char (*p)[14] | One pointer to a 14-char row | Depends on pointed storage | Passing or referring to rows of a 2D array |
A pointer to an array is useful, but it has a different job:
char rows[2][14] = {"blah", "hmm"};
char (*p)[14] = rows;
printf("%s\n", p[1]);
Here rows converts to a pointer to its first 14-character row, so it correctly matches char (*)[14]. In the original code, a string literal does not have that pointer-to-row type.
Cheat Sheet
/* Read-only strings: array of pointers */
const char *words[] = {"blah", "hmm"};
printf("%s\n", words[1]);
/* Writable strings: fixed-size rows */
char words[2][14] = {"blah", "hmm"};
words[0][0] = 'B';
printf("%s\n", words[0]);
/* Pointer to one 14-character row */
char (*row_pointer)[14];
- A C string ends with
\0. char text[] = "hi";creates writable array storage.const char *text = "hi";points at a string literal; do not modify it.char table[rows][columns]needs one extra column for\0.- Use
printf("%s", text), notprintf(text). - Arrays cannot be assigned after declaration; copy into them instead.
char *andchar (*)[N]are different pointer types.
FAQ
Why is char (*a[2])[14] different from char a[2][14]?
char a[2][14] allocates two arrays of 14 characters. char (*a[2])[14] allocates two pointers, each intended to point to an array of 14 characters.
Should I use char * or const char * for string literals?
Use const char *. A string literal must not be modified, even though older C code often uses char *.
Why does printf(a[1]) seem to work?
It happens to work when the text contains no % characters, but it is unsafe because printf treats the text as a format string. Use printf("%s", a[1]).
Can strings in char a[2][14] have different lengths?
Yes. Each row has capacity for up to 13 visible characters plus \0; unused positions are simply not part of the string.
Can I assign a[0] = "new text" to a two-dimensional array?
No. a[0] is an array, and arrays are not assignable in C. Copy the characters with a size-checked operation instead.
How do I pass a two-dimensional array to a function?
The column size is part of the element type, so include it in the parameter:
void print_words(size_t count, char words[][14]);
What does char (*p)[14] mean?
It means p points to an array of 14 char values. It is commonly used to point at a row in a two-dimensional array.
Mini Project
Description
Create a small status-message table for a command-line program. The program uses an array of pointers for fixed status labels, prints each label safely, and shows how to select a message by index.
Goal
Build a program that stores and prints several read-only status messages using an array of string pointers.
Requirements
Create an array containing at least four status messages.
Print every message with a loop.
Use const char * for the message pointers.
Print strings with printf and the %s format specifier.
Check that a requested message index is within the array bounds.
Keep learning
Related questions
2D Array Loop Order and Cache Performance in C
Learn why swapping nested loops changes 2D array performance in C, using row-major memory layout, cache locality, and practical benchmarks.
Array-to-Pointer Conversion in C and C++ Explained
Learn what array-to-pointer conversion means in C and C++, how array decay works, and how it differs from a pointer to an array.
Building More Fault-Tolerant Embedded C++ Applications for Radiation-Prone ARM Systems
Learn practical C++ and compile-time techniques to reduce soft-error damage in embedded ARM systems exposed to radiation.