- Learn
- Computer Science & IT
- Programming
- Programming in C
- Arrays and Strings in C
Arrays and Strings in C
BeginnerLesson 3 of 8 in Programming in C Course
What you will learn
- Declare arrays of a fixed size, reach each element by its index, loop over them without stepping past the end, and share them with functions.
- Declare, initialize and index an arrayCreate arrays with a fixed number of elements, give them starting values, and read or change single elements by their index.
- Loop through arrays without going out of boundsVisit every element with a counting loop and use that pattern to sum, find the largest value, search, count and reverse.
- Pass arrays to functionsWrite functions that take arrays as parameters, pass the length alongside, and know when the function can change the caller's data.
- Declare, initialize and index an array
- Treat characters as small integer codes, store text in
chararrays that end with a null character, process it one character at a time, and see how UTF-8 text fits into bytes.- Treat characters as numeric codesUse the
chartype and character constants as numbers, convert digits and letters with arithmetic, and classify characters with<ctype.h>. - Store text as null-terminated stringsExplain how a C string is laid out in a
chararray, why it needs a terminating null character, and how string literals and arrays differ. - Process strings one character at a timeWalk through a string until its terminator to count, convert, reverse, filter and encrypt text, and finish every new string with
'\0'. - Explain how UTF-8 text is stored in char arraysRelate characters, Unicode code points and UTF-8 bytes, and predict what byte-based C functions do with text beyond ASCII.
- Treat characters as numeric codes
- Measure, copy, join, compare and search strings with
<string.h>, and make sure every destination has room for the result and its terminator.- Measure, copy and join stringsUse
strlen,strcpyandstrcatcorrectly and work out how much room each destination needs. - Compare and search stringsCompare strings with
strcmpandstrncmp, find characters and substrings, and split text into tokens. - Copy within limits and work with raw memoryUse the bounded functions
strncpy,strncatandsnprintfcorrectly, detect truncation, and copy, fill and compare whole arrays with the memory functions.
- Measure, copy and join strings
- Store tables and lists of strings in arrays with two or more dimensions, traverse them with nested loops, and pass them to functions.
- Declare and traverse two-dimensional arraysDeclare and initialize tables of values, explain how their rows are laid out in memory, and process rows and columns with nested loops.
- Pass tables to functions and store lists of stringsWrite functions that take two-dimensional arrays, and keep several strings either in a 2D
chararray or in an array of string pointers.
- Declare and traverse two-dimensional arrays
- Read the words typed after a program's name through
argcandargv, check how many there are, and turn them safely into numbers and options.- Read argc and argvDeclare
mainwith its two parameters, describe what each element ofargvholds, and check the argument count before using any argument. - Convert and validate numeric argumentsTurn argument strings into numbers with error checking, reject invalid input, and range-check values before using them.
- Read argc and argv
- Recognize how C programs read and write outside their arrays, why this is dangerous, and how to read and copy text so that it always fits.
- Recognize out-of-bounds access and its effectsExplain why C does not stop out-of-bounds access, what can happen when it occurs, and which mistakes cause it.
- Read and copy text within buffer limitsRead input with size limits, detect lines that are too long, and copy text only when it fits its destination.
- Recognize out-of-bounds access and its effects
Arrays and Strings in C study guide
1Store and process collections of values with arrays
Arrays let one loop work on many related values. The array fixes how much storage exists, while indexes select individual elements and a count records how many are in use. Learn how declarations establish starting values, how loops compute results without crossing a bound, and how functions share the caller’s elements. Small examples connect the code to the exact positions it reads or writes.
1.1 Declare, initialize and index an array
An array gives one name to a fixed number of values of the same type. Its elements sit next to each other in memory. Use an array when the same operation applies to many values, such as updating a set of scores.
Create the elements
int scores[5] = {90, 72, 85, 64, 99};
int primes[] = {2, 3, 5, 7};
int counts[6] = {0};
The first declaration creates five integers. The compiler counts four values in the second initializer, so primes has four elements. In the third, the first element is explicitly zero and every missing initializer also becomes zero. Similarly, int a[5] = {1, 2}; holds 1, 2, 0, 0, 0. A designated initializer, available since C99, chooses a position: int b[5] = {[2] = 7}; puts 7 in the third element and zero in the others.
An automatic local declaration such as int data[5]; reserves space without supplying starting values. Write each element before reading it. Arrays declared outside every function, and arrays declared static, are zero-initialized when no explicit initializer is supplied.
Reach one value by its index
Indexes start at zero. For five elements the valid positions are 0 through 4. scores[1] means the second score; it does not mean the first. An element can be used wherever an ordinary variable of its type can be used:
scores[1] = 75;
scores[1] += 5;
int last = scores[4];
The second score is now 80. The last is still 99. An index can be a variable or an expression, so scores[n - 1] reaches the last element when n is the number of elements and is positive. scores[5] has no element to reach.
Count elements and bytes
sizeof scores measures the entire array in bytes. On a system with 4-byte integers, the five elements occupy 20 bytes. Dividing by one element's size removes the dependence on the integer size:
size_t n = sizeof scores / sizeof scores[0];
printf("%zu\n", n);
This prints 5. size_t is the unsigned integer type used for sizes; %zu prints it. This calculation works where scores names the actual array. A function parameter does not carry the array's full size.
Keep size and values separate
A name such as #define NUM_SCORES 5 lets the declaration and its loops share one size. In C17, a local const int n = 5; int a[n]; instead creates a variable-length array: const does not make n an integer constant expression. Its size is chosen when the declaration runs and then stays fixed for that array's lifetime. It cannot use an initializer list. VLA support is optional in C11 and C17; C23 adds constexpr for compile-time constants.
Arrays cannot be assigned as wholes: b = a; is invalid. Copy their elements with a loop or a suitable memory function. For two separate arrays, a == b compares addresses, not element values. Compare corresponding elements to test value equality. A byte comparison with memcmp instead tests their stored representations; equal floating-point values such as positive and negative zero can have different representations.
Key Takeaways
- Array indexes run from 0 to one less than the element count.
- Missing initializer values become zero; uninitialized local elements must be written before use.
- Use
sizeof a / sizeof a[0]only whereais an actual array. - An array keeps its size for its lifetime.
- Copy and compare array values element by element.
1.2 Loop through arrays without going out of bounds
A counting loop connects an array's positions to an operation. Decide which elements hold useful data, then visit exactly those positions. The same pattern supports totals, searches and updates.
Visit the used elements
int t[5] = {12, 15, 9, 14, 10};
int sum = 0;
for (int i = 0; i < 5; i++) {
sum += t[i];
}
double average = sum / 5.0;
The loop uses indexes 0, 1, 2, 3 and 4. The total progresses through 12, 27, 36, 50 and 60, so the average is 12.0. Writing i <= 5 adds an invalid access at index 5. That extra pass is an off-by-one error.
For an average, make the division floating-point. With a total of 61, 61 / 5 gives the integer 12; 61 / 5.0 gives 12.2. Storing the first result in a double afterward cannot recover the fraction already discarded.
Find a maximum or a match
For a nonempty array, initialize the maximum from its first value:
int max = t[0];
for (int i = 1; i < 5; i++) {
if (t[i] > max) max = t[i];
}
The first comparison replaces 12 with 15. The remaining values are smaller, so 15 remains. Starting from zero would fail for a list such as {-8, -3, -11}: zero is larger than every stored value, yet it is not in the list.
A linear search checks values in order:
int found = -1;
for (int i = 0; i < 5; i++) {
if (t[i] == 14) {
found = i;
break;
}
}
Here found becomes 3. The value -1 marks no match because it cannot be a valid index. Report failure only after all candidates have been checked. Returning -1 after the first unequal element misses matches later in the array.
Count and rearrange
A frequency array stores one counter per category. For digits, int counts[10] = {0}; supplies counters 0 through 9. After verifying d >= 0 && d < 10, counts[d]++ increases the counter for digit d. The check protects the array when the digit value comes from input.
To reverse an array, exchange pairs from its ends:
for (int i = 0; i < 5 / 2; i++) {
int temp = t[i];
t[i] = t[5 - 1 - i];
t[5 - 1 - i] = temp;
}
The first swap exchanges 12 and 10. The second exchanges 15 and 14. The middle 9 stays in place, leaving {10, 14, 9, 15, 12}. Stop halfway: continuing through all five positions would swap every pair twice.
Track count separately from capacity
An array with room for 100 values may currently hold only 37 useful entries. Its capacity is 100; its count is 37. Process positions below count. To append one value, first check count < CAPACITY, store at a[count], then increment count. Looping to capacity can read positions that have never received useful values, even though those positions are physically inside the array.
Key Takeaways
- Use a strict
<loop bound. - Use floating-point division when an average may have a fraction.
- Initialize a nonempty array maximum from an element, including for all-negative data.
- A failed search is decided after all candidates have been checked.
- Validate a value before using it as an index.
- Reverse by swapping only the first half of the pairs.
- Count is the number in use; capacity is the available space.
1.3 Pass arrays to functions
A function can process an array without receiving a separate copy of every element. The call shares access to the caller's storage. Pass the number of elements alongside it so the function knows where to stop.
Supply the data and its length
#include <stddef.h>
double average(const int a[], size_t n) {
if (n == 0) return 0.0;
int sum = 0;
for (size_t i = 0; i < n; i++) {
sum += a[i];
}
return (double) sum / n;
}
The caller can declare int values[] = {7, 8, 10}; and call average(values, sizeof values / sizeof values[0]). The three additions produce 25; converting sum to double before division produces about 8.33. Returning 0.0 for no values is this function's chosen empty-input behavior. The small values in this example keep the integer sum in range.
In a parameter declaration, int a[] is adjusted to int *a. It receives the address of the first element. Even int a[10] does not check or enforce ten elements. The caller must supply a length that matches the available data.
Why sizeof loses the length
At the call site, sizeof values measures the array. Inside the function, sizeof a measures the pointer parameter. Where pointers occupy 8 bytes and integers occupy 4, dividing these sizes gives 2, regardless of the caller's element count. That result describes neither a two-element copy nor the original array. Keep the length as an explicit parameter.
size_t, declared by headers including <stddef.h>, is the unsigned type produced by sizeof. It can represent object sizes, and %zu prints it. Using it for the loop index avoids a signed index being compared with an unsigned length.
Decide whether elements may change
The const in const int a[] prevents assignments to elements through this parameter. It suits a function that computes a result from the data. A function meant to update elements omits it:
void zero(int a[], size_t n) {
for (size_t i = 0; i < n; i++) {
a[i] = 0;
}
}
After zero(values, 3), the caller's three elements all hold zero. The array still has three elements. A scalar parameter such as n is a separate copy; changing that copy would not change a caller's count variable.
To produce multiple results, let the caller provide an output array and its length. A function can fill out[i] with each result. C functions cannot return an array by value, and returning the address of a local array fails because that array stops existing when the function returns.
Count backward with an unsigned index
An unsigned index cannot become negative. In for (size_t i = n - 1; i >= 0; i--), the test is always true; after zero, the value wraps to a large positive number. With n == 0, even the initial subtraction wraps. Start from the count instead:
for (size_t i = n; i > 0; i--) {
printf("%d\n", a[i - 1]);
}
With three elements the accessed indexes are 2, 1 and 0. With zero elements the body never runs. Include <stdio.h> for printf.
Key Takeaways
- Pass an array length separately; a parameter has no recoverable element count.
- Writes through an array parameter change the caller’s elements.
- Use
constfor a parameter that only reads elements. - The caller supplies storage for multiple output values.
- Count down from
nand accessi - 1when usingsize_t.
2Represent text with characters and null-terminated strings
Text begins with character codes and ends with a storage rule: a C string stops at its first zero byte. Learn how characters, literals and writable arrays differ, then use loops to count and transform text. UTF-8 adds an essential distinction between a byte and a complete code point. Following both the text and its storage explains why a visible word can need more array positions than its letters suggest.
2.1 Treat characters as numeric codes
A C character is stored as a numeric code. That lets a program print a symbol, compare it with another symbol, or perform arithmetic on its code. The character set determines which number represents each symbol.
Read the code and the character
A char occupies one byte: sizeof(char) is 1 by definition. A character constant such as 'A' has type int. In the ASCII-based character set used here, its code is 65:
printf("%c %d\n", 'A', 'A');
The output is "A 65". %c displays the character; %d displays its number. ASCII assigns codes 0 through 127. Digits run from 48 to 57, uppercase letters from 65 to 90, lowercase letters from 97 to 122, and space has code 32.
Single quotes and double quotes create different things. 'A' is one character code. "A" is an array containing 'A' and a terminating zero byte. Store one character with char c = 'A'; and compare it using c == 'A'.
Write characters that need escapes
| C spelling | Character |
|---|---|
'\n' | Newline, ASCII code 10 |
'\t' | Tab, ASCII code 9 |
'\\' | Backslash |
'\'' | Single quote |
'\0' | Null character, code 0 |
The null character is different from the visible digit '0', whose ASCII code is 48. A string uses the null character to mark its end.
Turn codes into useful values
The C standard guarantees consecutive digit codes. After checking that c is a digit, c - '0' produces its value. Thus '7' - '0' is 7, not 55. Subtracting the starting code removes the digit's position within the character set.
ASCII letters are consecutive too, so subtracting 'A' converts an uppercase letter to a position from 0 to 25. A Caesar shift by three uses:
char shifted = (c - 'A' + 3) % 26 + 'A';
For 'X', subtracting 65 gives 23; adding three gives 26; the remainder after division by 26 is zero; adding 65 returns 'A'. Plain 'X' + 3 gives code 91, which is '['. Letter arithmetic depends on ASCII-style letter ordering; C guarantees consecutive digits but does not require consecutive letters.
Classify and convert with ctype.h
Include <ctype.h> for isdigit, isalpha, isalnum, isspace, isupper, islower, toupper and tolower. The tests return nonzero for a match and zero otherwise. toupper('q') returns 'Q'; toupper('3') leaves the digit unchanged. isspace recognizes separators such as space, tab and newline.
Plain char may be signed. A byte above 127 can then become a negative value, which is not a valid argument for these functions unless it is the special input marker EOF. When processing bytes from a string, convert first:
if (isalpha((unsigned char) c)) {
c = (char) toupper((unsigned char) c);
}
In the default "C" locale these functions recognize the basic letters, not whole multibyte UTF-8 characters. The cast makes the byte a valid argument; it does not decode UTF-8.
Key Takeaways
- Character constants use single quotes; string literals use double quotes.
\0is a zero-valued character, not the digit0.- Subtract
'0'from a digit character to obtain its value. - Letter-position arithmetic here uses ASCII ordering.
- Convert a string byte to
unsigned charbefore a ctype call. - A ctype test returns nonzero for a match, not necessarily 1.
2.2 Store text as null-terminated strings
A C string is a sequence of characters ending with '\0'. The terminator is part of its storage but is not part of its text length. C has no separate built-in string type: the characters occupy an array.
See the bytes in the array
char word[] = "cat";
char name[20] = "Ada";
word contains four bytes: 'c', 'a', 't', '\0'. Its array size is 4 and strlen(word) is 3. Include <string.h> for strlen. The larger name array contains the three letters, their terminator and sixteen more zero bytes. Its capacity is 20 while its current length is 3.
A character-by-character initializer can make the same string:
char word[] = {'c', 'a', 't', '\0'};
Leaving off the last element creates three characters without a string terminator. C also permits char word[3] = "cat";, which fills all three positions and omits the zero byte. Such an array is not a C string. A function such as strlen or printf with %s keeps reading until it encounters a zero byte, so it cannot safely consume that three-byte array as text.
Choose a modifiable array or a literal
| Declaration | Where the text is | Can its characters change? |
|---|---|---|
char a[] = "hello"; | A copy in array a | Yes |
const char *p = "hello"; | A string literal referred to by p | No |
a[0] = 'H'; changes the copy to "Hello". A literal must not be modified. Even if it is assigned to a plain char *, writing through that pointer has undefined behavior. Writing const char * makes that restriction visible to the compiler.
The compiler joins adjacent string literals: "Hello, " "world" gives the same text as "Hello, world". The resulting string still has one final terminator. This is useful for splitting a long literal over source lines.
Print and change the text
printf("%s\n", word);
printf("%c\n", word[0]);
The first call prints the whole string. The second prints its first character. A string literal in printf is surrounded by double quotes because it is text containing a format; %c receives one character code.
An element assignment changes one stored character. With char name[20] = "Ada";, setting name[0] = 'E'; produces "Eda". Setting name[1] = '\0'; afterward produces "E" as the string. The bytes for 'a' and the old terminator still exist later in the array, but string operations stop at the first zero.
An array cannot receive replacement text using name = "Bob"; after declaration. Copy characters into it, using a checked strcpy call or a loop, and include the terminator. The replacement's length must be less than the capacity.
Represent no text
The empty string "" still occupies one byte for '\0'. It has length zero and can be tested with name[0] == '\0'. An empty string is a real string with a terminator; a null pointer instead refers to no string storage.
Key Takeaways
- A C string ends at its first
'\0'. - Capacity includes the terminator; string length excludes it.
- A character array without a terminator cannot be passed as a C string.
- Array copies of literals are modifiable; literals themselves are not.
- Writing an earlier terminator shortens text without resizing its array.
2.3 Process strings one character at a time
A string's terminator gives a loop its stopping point. Visit the bytes before it to count, convert or filter text. When creating a new string, the program must write a new terminator itself.
Walk to the end once
for (size_t i = 0; s[i] != '\0'; i++) {
/* Process s[i]. */
}
At every pass, the test asks whether the current byte belongs to the text. It stops before the terminator. Calling strlen(s) in that test can scan the whole string again on each pass. Either test the current byte directly or compute size_t len = strlen(s); once when the length will stay unchanged.
For uppercase conversion, use a modifiable array and <ctype.h>:
for (size_t i = 0; s[i] != '\0'; i++) {
s[i] = (char) toupper((unsigned char) s[i]);
}
"Hello, World 42" becomes "HELLO, WORLD 42". Conversion preserves the comma, spaces and digits. The unsigned conversion makes every stored byte a valid ctype argument.
Count categories and word starts
The original "Hello, World 42" has 15 text bytes: 10 letters, two digits, two spaces and one comma. Separate counters can use isalpha, isdigit and isspace. Decide what a word means before counting it. For a run of letters, a new word starts when a letter follows a non-letter or is the first character:
int in_word = 0;
size_t words = 0;
for (size_t i = 0; s[i] != '\0'; i++) {
int letter = isalpha((unsigned char) s[i]) != 0;
if (letter && !in_word) words++;
in_word = letter;
}
The example has two letter-runs, "Hello" and "World". The digits do not form a word under this definition. Updating in_word every pass lets punctuation end a run.
Reverse only the text
For a single-byte string, obtain len once, then swap s[i] with s[len - 1 - i] while i < len / 2. Reversing "stressed" exchanges its first and last letters, then the next pair, until it becomes "desserts". The zero byte stays at index len. Starting a swap at s[len] would move that zero to the front. Reversing bytes this way is appropriate for ASCII text; multibyte characters need decoding before rearrangement.
Build a filtered result
Use different indexes for reading and writing, because some input bytes are skipped:
char in[] = "a1b2c3";
char out[sizeof in];
size_t j = 0;
for (size_t i = 0; in[i] != '\0'; i++) {
if (isalpha((unsigned char) in[i])) {
out[j++] = in[i];
}
}
out[j] = '\0';
The result is "abc". The destination has enough room because filtering cannot lengthen this input. Without out[j] = '\0';, the output has no defined string ending.
For a Caesar cipher, shift uppercase letters relative to 'A' and lowercase letters relative to 'a', then copy other bytes unchanged. With key 13, "Hello, World!" becomes "Uryyb, Jbeyq!". Repeating that shift restores the original because two shifts total 26. A comparison such as s[i] == 'a' tests one character; use strcmp to compare complete text.
Key Takeaways
- Test the current byte or cache the length to avoid repeated full scans.
- Keep separate read and write indexes when filtering.
- Write a terminator after building a new string.
- Reverse text bytes without moving the terminator.
- Define a word boundary before writing a word counter.
- Apply ASCII letter shifts separately to uppercase and lowercase letters.
2.4 Explain how UTF-8 text is stored in char arrays
Text length can mean a byte count or a count of Unicode code points. These counts often differ. A C char array exposes individual bytes, so its indexes and string functions do not automatically follow complete non-ASCII characters.
Separate characters, code points and bytes
A character encoding describes how text is stored. ASCII uses codes 0 through 127 for basic letters, digits, punctuation and control characters. Unicode assigns code points to characters from many writing systems: "é" has code point U+00E9 and "€" has U+20AC. UTF-8 encodes Unicode scalar values using one to four bytes.
ASCII code points retain their one-byte encodings. The precomposed "é" uses two bytes, 0xC3 and 0xA9; "€" uses three. Many individual emoji code points use four bytes. A displayed symbol can contain multiple code points, so even a code-point count is not always a count of visible symbols.
Measure a UTF-8 string
char s[] = "café";
printf("%zu %zu\n", strlen(s), sizeof s);
With UTF-8 execution text, this prints "5 6". The first three letters use three bytes and "é" uses two, making five text bytes. The terminator adds the sixth byte of storage. s[3] is the first byte of "é", not the entire letter.
A cut after four bytes would retain 0xC3 but remove its continuation byte 0xA9. The result would no longer be valid UTF-8. A buffer limit expressed in bytes therefore needs a character-boundary check if the program truncates UTF-8 text.
Count starting bytes in valid UTF-8
A continuation byte starts with the bits 10. Its value lies from 0x80 through 0xBF. A leading byte or ASCII byte does not have that pattern:
size_t points = 0;
for (size_t i = 0; s[i] != '\0'; i++) {
unsigned char c = (unsigned char) s[i];
if ((c & 0xC0) != 0x80) points++;
}
The mask 0xC0 keeps just the top two bits; 0x80 represents the pattern 10 in those positions. The test counts "café" as four code points by counting its four starting bytes. It assumes valid UTF-8 and does not validate malformed sequences or combine code points into visible symbols.
Choose operations that preserve the encoding
Copying a complete UTF-8 string preserves its bytes. Joining complete strings, comparing their byte sequences for equality, and searching for an ASCII character or complete UTF-8 substring also work. UTF-8 continuation bytes cannot be mistaken for ASCII characters or leading bytes.
strcmp orders unsigned byte values, so its order differs from language-specific alphabetical order. In UTF-8, ordinary accented Latin letters have leading bytes above ASCII 'z'. A byte comparison therefore puts those letters after 'z', regardless of dictionary conventions.
In the default "C" locale, toupper and isalpha process one byte and do not decode "é" into a letter. Casting to unsigned char makes the call valid but cannot make it a Unicode case conversion. Wide-character functions or a Unicode library are needed for that task. An ASCII-only Caesar loop can preserve accented letters by changing only bytes from A–Z or a–z.
The zero byte is UTF-8's encoding of U+0000; other encoded code points contain no zero byte. That fits C's terminator convention. UTF-16 can put zero bytes inside ordinary encoded text, so a UTF-16 byte array cannot be processed as an ordinary char string.
Key Takeaways
strlencounts bytes, including in UTF-8 text.- One Unicode code point may occupy several bytes.
- A char index can land inside an encoded character.
- Counting non-continuation bytes counts code points only in valid UTF-8.
- Bytewise equality works for identical encoded text; byte order is not dictionary order.
- Single-byte ctype calls do not decode UTF-8.
3Use the standard string functions
The standard library provides common text operations, but each function expects a particular kind of input and a particular meaning for its size argument. Learn to measure, copy, append, compare and search strings, then choose bounded formatting or byte operations where they fit. Checking capacity and interpreting return values turns a short function call into a predictable operation on known storage.
3.1 Measure, copy and join strings
The functions in <string.h> work with terminated text. They can find its length, copy it or append more text, but copying functions do not discover the destination's capacity. Work out the required storage before writing.
Measure length without the terminator
strlen(s) returns a size_t giving the number of bytes before the first zero byte. Thus strlen("hello") is 5 and strlen("") is zero. Print the result with %zu.
char buf[64] = "hi";
printf("%zu %zu\n", strlen(buf), sizeof buf);
This prints "2 64". The first result measures current text; the second measures available array storage. A later copy can change the first without changing the second. Both calls are useful here because buf is an actual array and already contains a valid string.
Copy a complete string
strcpy(dest, src) copies text and its final zero byte. The required capacity is the source length plus one:
char dest[12];
const char *src = "Maple";
if (strlen(src) < sizeof dest) {
strcpy(dest, src);
}
"Maple" uses five text bytes and one terminator, so it fits. The strict < matters: a source whose length equals the capacity leaves no position for its zero byte. On a failed check the program must handle the error before using the destination as a new string.
An array assignment such as dest = src; cannot replace its contents. Assigning one char * variable to another is legal, but only copies a reference to the same storage. It does not create another copy of the characters.
Append to an existing string
strcat(dest, src) first finds the terminator already in dest. It replaces that terminator with the first source byte and writes a new terminator after the appended text. Destination capacity must cover both current lengths plus one.
char full[16];
strcpy(full, "Ada");
strcat(full, " ");
strcat(full, "Lovelace");
The lengths grow from 3 to 4 to 12. The final "Ada Lovelace" needs 13 bytes of storage, leaving three bytes unused. A 12-byte destination would be one byte too small, despite matching the visible text length.
Never append to an uninitialized local buffer. char out[20]; strcat(out, "hi"); gives strcat no established text ending to find. Start with char out[20] = "";, write out[0] = '\0';, or make the first operation a checked copy.
Respect storage and scan costs
The source and destination must not overlap for strcpy or strcat. Moving text within the same buffer can create an overlap even when both locations are individually valid. memmove is the function intended for overlapping byte regions.
strlen must scan up to the terminator. strcat must scan the current destination before it appends. Repeated appends in a loop therefore rescan text that has already been built. When constructing many pieces, keep the current output length and write each piece at that index, checking the remaining capacity and maintaining the terminator. A known length is useful only while it still matches the actual contents.
Key Takeaways
strlenmeasures current text;sizeofan array measures its capacity.- A copy needs source length plus one byte.
- An append needs both string lengths plus one byte.
- Initialize the destination as a string before appending.
- Copying or appending with overlapping string regions is invalid.
3.2 Compare and search strings
String comparison answers questions about text, while string search finds a place inside that text. Neither task is performed by comparing array names with ==. Use the library result according to what it represents: a comparison sign, a length or a matching location.
Compare text and prefixes
strcmp(a, b) compares corresponding bytes as unsigned char values until a difference or the string ending. Its result is zero for equal text, negative when a comes first and positive when it comes later. Only the sign is specified; do not expect exactly -1 or 1.
if (strcmp(command, "quit") == 0) {
puts("Goodbye");
}
The explicit zero test means equal. A plain if (strcmp(command, "quit")) enters for unequal text because nonzero is true. Comparing command == "quit" would compare storage locations.
In ASCII order, "apple" comes before "banana" at the first letter. "Zebra" comes before "apple" because uppercase 'Z' has code 90 and lowercase 'a' has code 97. "app" comes before "apple" because its zero byte is encountered before the other's 'l'. This is byte ordering, not a dictionary comparison.
strncmp(a, b, n) compares at most n bytes. strncmp(s, "--", 2) == 0 recognizes a two-dash prefix but does not mean that the entire string is just "--". Standard C has no case-insensitive whole-string comparison function. A program can compare converted bytes itself; POSIX provides strcasecmp in <strings.h>.
Search for a location
Use strchr for the first occurrence of one character, strrchr for the last and strstr for the first complete substring. They return a pointer to the matching location or NULL if there is no match.
char name[] = "report.final.txt";
char *extension = strrchr(name, '.');
if (extension != NULL) {
printf("%s\n", extension);
}
This prints ".txt", including the matched dot. Starting from the first dot with strchr would print ".final.txt". strstr(name, "final") finds the suffix beginning "final.txt"; searching for "draft" gives NULL. Check for NULL before printing or reading a search result.
Locate a separator or split text
strcspn(s, set) counts the initial bytes that belong to none of the characters in set. With "oak:pine,birch" and the set ":,", it stops at the colon. For a valid input string, this pattern removes the first newline:
buf[strcspn(buf, "\n")] = '\0';
If there is no newline, the returned index is the string length, so the assignment rewrites the terminator already there.
strtok splits a modifiable string using any character in a delimiter set:
char colors[] = "red, green,blue";
char *token = strtok(colors, " ,");
while (token != NULL) {
puts(token);
token = strtok(NULL, " ,");
}
The tokens are "red", "green" and "blue". The first call supplies the array; later calls pass NULL to continue. The function writes zero bytes into the input and keeps hidden state between calls. Use writable storage, and do not expect the original separator-filled string to remain intact. Consecutive delimiters do not produce empty tokens.
Key Takeaways
- Use
strcmp(a, b) == 0for equal text. - Interpret a comparison result by sign, not exact magnitude.
- A prefix match does not establish whole-string equality.
- Search results need a null-pointer check.
strcspnreturns a prefix length;strtokmodifies its input and remembers its position.- String ordering follows unsigned byte codes.
3.3 Copy within limits and work with raw memory
A length limit is useful only when it describes the right quantity. Some functions limit source bytes, others destination capacity, and memory functions handle raw bytes without string endings. Read each function's contract before choosing its bound.
Know what n means
strncpy(dest, src, n) writes exactly n positions when the source is shorter, padding with zero bytes. When the source has at least n bytes of text, it copies those bytes without a terminator. For a positive-capacity array, a terminated truncating copy is:
strncpy(dest, src, sizeof dest - 1);
dest[sizeof dest - 1] = '\0';
The final assignment is essential when the source fills the limit. Truncating a name or command can still change its meaning, so reject overlong input when a complete copy is required.
strncat(dest, src, n) appends at most n text bytes and then a terminator. It does not receive total capacity. For a valid string already inside a destination array, the remaining text space is sizeof dest - strlen(dest) - 1. Passing sizeof dest as n can overflow it.
Format and detect truncation
snprintf, declared in <stdio.h>, takes total destination capacity. With capacity greater than zero it writes at most one fewer text bytes, then a terminator:
char buf[8];
int needed = snprintf(buf, sizeof buf, "%s-%d", "item", 2026);
if (needed < 0) {
puts("Formatting error");
} else if ((size_t) needed >= sizeof buf) {
puts("Text did not fit");
}
The full result "item-2026" has nine text bytes. The buffer stores "item-20" and its zero byte, while needed is 9. The return value counts the text that would have been produced, excluding the terminator. Check a negative result before converting it to size_t.
Copy and fill bytes
memcpy(dest, src, n) copies exactly n bytes, including any zero bytes, without searching for a terminator. For two same-size integer arrays, memcpy(b, a, sizeof a) copies their complete storage when the regions do not overlap. Where each of five integers occupies four bytes, that count is 20 bytes. memmove performs byte copying with defined behavior for overlapping regions.
memset(a, 0, sizeof a) fills every byte with zero and zeroes an integer array. Its second argument describes each byte, not each element. With four-byte integers, filling with 1 makes the bytes 01 01 01 01, giving 16843009 on the ordinary representation, not the integer 1. Use a loop to assign nonzero integer values.
memcmp(a, b, n) compares stored bytes. Zero means those byte sequences match. For value equality, compare elements: floating-point 0.0 and -0.0 compare equal while their representations can differ.
Recognize other library families
BSD's strlcpy and strlcat joined POSIX in 2024, but are not core C17 functions. They return a length they tried to create; a result at least as large as the supplied size signals truncation. strlcpy terminates when the size is positive. strlcat terminates only if the existing text ends within that size. They are available in glibc 2.38 and later. Optional Annex K functions such as strcpy_s are absent from many libraries, including glibc.
Key Takeaways
strncpymay omit the terminator;strncatneeds space for a new one.snprintfreturns the complete required text length, or a negative error.- A nonnegative formatting result at least as large as capacity means truncation.
- Memory functions count bytes and do not stop at zero.
- Use
memmovefor overlapping regions. memsetfills bytes;memcmpcompares representations.
4Organize data in multidimensional arrays
Tables organize values by more than one position. A C table stores equal-width rows in sequence, and nested loops let a program work by row or by column. Learn how to declare that shape, pass it to a function, and keep both indexes valid. Lists of text introduce a useful design choice: writable character rows reserve fixed space, while arrays of pointers refer to strings held elsewhere.
4.1 Declare and traverse two-dimensional arrays
A table needs a row index and a column index. In C, a two-dimensional array is an array whose elements are themselves arrays. Each row has the same number of elements, and each index has its own bound.
Declare rows and columns
int grid[3][4];
int m[2][3] = {{1, 2, 3}, {4, 5, 6}};
grid holds three rows with four integers each: twelve integers in total. Its row indexes are 0 through 2; its column indexes are 0 through 3. grid[1][2] reaches the third value in the second row.
Nested initializer braces show the row grouping. Missing initialized elements become zero. You can let the compiler count rows in int m[][3] = {{1, 2, 3}, {4, 5, 6}};, but the column count must be supplied. Without it the compiler could not determine where each row ends.
Follow row-major storage
C stores a complete row before starting the next. In grid, the first four stored integers are row 0, the next four are row 1 and the last four are row 2. grid[1][2] is the seventh integer from the start, at zero-based element position 6: one complete row of four, then two more elements.
Where integers occupy four bytes, one row uses sixteen bytes and the whole table uses forty-eight. At the declaration site:
size_t rows = sizeof grid / sizeof grid[0];
size_t cols = sizeof grid[0] / sizeof grid[0][0];
The first division gives 3 and the second gives 4. A row is an array in its own right, which is why sizeof grid[0] measures more than one integer.
Traverse and total the table
int sales[2][3] = {{4, 0, 7}, {5, 2, 1}};
int total = 0;
for (int r = 0; r < 2; r++) {
int row_sum = 0;
for (int c = 0; c < 3; c++) {
row_sum += sales[r][c];
}
printf("%d\n", row_sum);
total += row_sum;
}
The first row produces 11 and the second 8. Adding those row totals gives 19. Resetting row_sum inside the outer loop starts a fresh total for each row. If it were initialized only once before both loops, the second printed value would include the first row too.
For column totals, put the column loop outside and the row loop inside, still accessing sales[r][c]. The three totals become 9, 2 and 8, which again add to 19. Changing traversal order does not require changing the meaning of the two indexes. Row-first traversal follows contiguous storage and often improves cache use for large tables.
Keep each dimension within bounds
grid[c][r] swaps the meanings of the indexes. If c reaches 3, it requests a fourth row that does not exist. The fact that the complete object holds twelve integers does not permit crossing one row's bound with a column index.
Write two separate indexing operations: grid[1][2]. In C17, grid[1, 2] evaluates the comma expression and selects grid[2], a row, instead of one element. More dimensions apply the same idea repeatedly: int cube[2][3][4] has 24 integer elements, selected by three indexes.
Key Takeaways
- A two-dimensional array is an array of equal-width rows.
- Rows are stored one after another in row-major order.
- Keep row and column bounds separate.
- Reset a row or column accumulator when starting a new group.
- Only the first dimension may be inferred from an initializer.
4.2 Pass tables to functions and store lists of strings
A function receiving a table needs enough information to identify each row. A list of strings adds another choice: store the characters in equal-width rows, or store references to strings whose lengths differ.
Give a table parameter its row width
void double_grid(int rows, int g[][4]) {
for (int r = 0; r < rows; r++) {
for (int c = 0; c < 4; c++) {
g[r][c] *= 2;
}
}
}
The first dimension can be omitted because the row count is passed separately. The width 4 defines the type of each row. Without it, g[r][c] would have no known row spacing. A declaration with int g[][] is invalid. Changes made through this parameter remain in the caller's table; a read-only table parameter can use const int g[][4].
Since C99, compilers supporting variably modified parameters accept dimensions declared earlier in the parameter list:
void print_grid(int rows, int cols, int g[rows][cols]);
GCC and Clang support this C17 form. It allows one function to receive different row widths; pass positive dimensions that match the actual table. C23 requires support for these parameter types, while automatic variable-length array objects remain a separate optional feature. Fixed-width parameters are useful when compiler support is limited.
Store strings in rows
char days[7][10] = {
"Monday", "Tuesday", "Wednesday", "Thursday",
"Friday", "Saturday", "Sunday"
};
Each row has ten characters of storage, regardless of its current text length. "Wednesday" needs all ten positions: nine letters and a terminator. days[2] selects that whole row as a string; days[2][0] selects 'W'.
A row can be printed with %s, measured with strlen, or compared with strcmp. It can also receive a checked copy. strcpy(days[0], "Mon"); fits in ten bytes and changes row 0's text. The row stays ten bytes wide, and the next row stays in the same location. A replacement of ten text bytes would need eleven bytes and would not fit.
Store references to literals
const char *names[] = {"Ann", "Bartholomew", "Li"};
This declares an array of pointers. Each element refers to a literal with its own length plus a terminator. names[1] refers to "Bartholomew", and names[1][0] is 'B'. The literals must not be modified.
The pointer elements can change even though the literal characters cannot. names[0] = "Anna"; changes which literal the first element refers to; it does not extend "Ann" in place. In the 2D row form, an element is a fixed-width character array and cannot be reassigned that way. Choose writable rows for text edited in place, and references to literals for a list of fixed text choices. The command-line parameter argv uses an array-of-pointers shape, although its supplied argument characters are modifiable.
Key Takeaways
- Table parameters need every dimension after the first.
- A dimension passed separately must match the actual table.
- Each string row needs its own terminator within its fixed width.
- An array of pointers stores references, not equal-width character rows.
5Process command-line arguments
A command line supplies a program with text at startup. Argument counts, shell grouping and string operations let the program interpret that text without assuming an argument exists or already contains a number. Follow a checked path from argv to a usable value: verify presence, convert completely, detect conversion errors and enforce the range the program actually accepts.
5.1 Read argc and argv
Command-line arguments let the caller supply text when starting a program. The operating environment makes those strings available to main. Check how many arrived before indexing them.
Read the argument list
int main(int argc, char *argv[]) {
for (int i = 1; i < argc; i++) {
printf("%d: %s\n", i, argv[i]);
}
return 0;
}
Include <stdio.h> for the output. argc is the argument count and argv is the list of strings. The equivalent parameter spelling char **argv describes the same interface. The loop starts at 1 because index 0 normally holds the program's launch name.
On an ordinary shell, running ./greet Ada Lovelace supplies three strings: "./greet", "Ada" and "Lovelace". Thus argc is 3 and the last user argument is argv[2]. A null pointer sits at argv[argc], here argv[3]; it is a sentinel after the actual argument strings.
Let the shell group text
Quotes affect how the shell builds arguments. Running ./greet "Ada Lovelace" supplies one user argument containing a space. Its quote marks group the words and do not become part of the argument text. argc is now 2.
Unix-like shells can also expand patterns before the program starts. In ./count *.txt, the program commonly receives the names matched by the shell as separate arguments. It receives the resulting strings, not a request to expand the pattern itself. Exact expansion behavior depends on the shell and its settings.
Reject a missing or extra argument
If a program needs exactly one user argument, require argc == 2 before using it:
if (argc != 2) {
printf("Usage: %s name\n", argv[0]);
return 1;
}
printf("Hello, %s\n", argv[1]);
When argc is 1, argv[1] is the sentinel null pointer and cannot be passed to strlen or printed with %s. Indexes beyond the sentinel are outside the argument array.
Returning zero from main signals success. A nonzero status, such as the 1 above, tells the shell the program did not complete successfully. <stdlib.h> also supplies EXIT_SUCCESS and EXIT_FAILURE.
Treat each argument as a string
argv[i][0] reaches the argument's first character; strlen(argv[i]) measures its text bytes; strcmp(argv[i], "-v") == 0 checks a complete option. A numeric-looking argument is still text and must be converted before arithmetic.
argv[0] usually records how the program was started, but it may be empty when that information is unavailable. It is useful for messages, not as a fixed identity check. Argument characters may be modified during the program, but their supplied storage gives no room to assume a longer replacement will fit. To append an extension or otherwise lengthen text, copy it into a separately sized buffer first.
Key Takeaways
argccounts the supplied argument strings, including the normal launch-name entry.- Check the argument count before reading a user argument.
argv[argc]is a null pointer, not an empty argument string.- Shell quotes can keep spaces inside one argument.
- Use string operations for argument text and conversion for numbers.
5.2 Convert and validate numeric arguments
A numeric argument arrives as characters, not as an integer. Reliable conversion checks three separate questions: did any digits form a number, did the conversion use the required text, and is the resulting value allowed?
Avoid losing the error information
atoi, declared in <stdlib.h>, performs a simple conversion. It turns "13" into 13 but also turns "12abc" into 12. It returns zero both for "0" and for text such as "abc" that begins with no number. An out-of-range result is undefined behavior, so a digits-only test followed by atoi is still unsafe for very long input.
A form rule such as “one or more digits only” needs a separate scan. Reject an empty string and require each byte to satisfy isdigit((unsigned char) c). That rule accepts "13" and rejects "13a", "-5" and "". It checks spelling, not whether the value fits in a machine integer.
Use strtol and inspect where it stopped
#include <errno.h>
#include <stdlib.h>
char *end;
errno = 0;
long count = strtol(argv[1], &end, 10);
if (end == argv[1] || *end != '\0' || errno == ERANGE) {
puts("Enter a whole number");
return 1;
}
if (count < 1 || count > 100) {
puts("Count must be from 1 to 100");
return 1;
}
This fragment follows an argument-count check and uses <stdio.h> for puts. The 10 selects decimal conversion. end receives the location where conversion stopped; &end lets the function store that location. *end reads the character there. No pointer arithmetic is needed.
For "13a", the returned value is 13 but *end is 'a', so the text is rejected. For an empty string or "abc", end equals the starting argument, showing that no number was consumed. Both checks are needed: testing only the ending zero would mistakenly accept an empty string.
Before every conversion, reset errno to zero. A value outside the long range makes strtol return an endpoint and set ERANGE. Without the reset, an old error could appear to belong to this conversion. Check before narrowing the result to int; use INT_MIN and INT_MAX from <limits.h> when the full int range is the accepted domain.
Apply the program's own rule
strtol accepts leading whitespace and an optional sign. Decide whether those forms are allowed. If the interface requires digits alone, perform the nonempty digit scan as well as the conversion checks. After a successful conversion, check the application range before using a value as a count or index. "0" can be perfectly valid numeric text and still be invalid for a count that must be positive.
A Caesar program accepting a nonnegative key first checks argc, then its digits-only form, then the complete strtol result and range. Reducing a valid key with % 26 produces a shift from 0 through 25 before letter arithmetic. Key 27 has the effect of key 1. Perform the range check before a narrowing cast or any arithmetic that might overflow. For decimal values, strtod provides the corresponding end-position and range-error interface.
Key Takeaways
- Numeric-looking arguments still need conversion.
atoicannot distinguish valid zero from a conversion failure.- Check that
strtolread something and consumed all required text. - Reset
errno, checkERANGE, then check the application range. - A digits-only scan does not rule out overflow.
- Validate before narrowing a value or using it as an index.
6Prevent out-of-bounds access and buffer overflows
Bounds mistakes connect several earlier ideas: a loop can take one extra pass, text can need one extra byte, and a function can lose track of an array’s capacity. Learn to identify the invalid access before predicting its effects. Then read and copy input within known limits, checking success and completeness separately so a buffer that fits still contains the data the program intended to accept.
6.1 Recognize out-of-bounds access and its effects
Every array access needs a valid index. C does not require a bounds check at runtime, so the programmer must keep the index inside the object. An access outside its range has undefined behavior: the language gives no promised result.
Recognize an invalid access
For int a[5], valid indexes are 0 through 4. Index 5 is already too large and -1 is negative. A compiler may diagnose a visible mistake, but many invalid indexes depend on runtime input and are not rejected during compilation.
A faulty program may appear to work, crash or produce unexpected values. A write can corrupt nearby state; the symptom may occur later in an unrelated calculation. A read can access bytes outside the intended object. Neither operation has a reliable result. Repeating a successful run cannot establish that the access is valid.
Count every write, including the terminator
char name[8];
strcpy(name, "Christopher");
"Christopher" has eleven text bytes. Copying it also writes the terminator, so this call attempts twelve bytes in eight positions. That is a buffer overflow. On real systems, an overflow can damage other data or control information. Historically, attacks including the 1988 Morris worm exploited such input-handling bugs.
An overflow can be just one byte:
char s[5];
for (int i = 0; i <= 5; i++) {
s[i] = 'x';
}
The body runs six times and writes index 5. Changing the test to i < 5 keeps five character writes within the array, but it still does not create a string. For a string of four 'x' characters, write only indexes 0 through 3 and then store '\0' at index 4.
Validate computed indexes
A frequency table can turn a character into an index, but subtraction alone supplies no range guarantee:
int counts[26] = {0};
if (c >= 'a' && c <= 'z') {
counts[c - 'a']++;
}
With ASCII ordering, that test limits the result to 0 through 25. For 'A' or a punctuation byte, using c - 'a' without the test could create a negative index. Test the original value before indexing; checking after the write is too late.
For a table, validate the row and column separately. In int g[2][3], a column index of 3 is invalid even if the next row is adjacent in memory. Do not use the complete table's storage as permission to exceed a row's bound.
Use checks that fit the operation
An array loop uses a strict upper bound; an append checks count against capacity; a string copy checks text length plus one. Functions receive actual lengths or capacities from their callers. These checks address different quantities, so one hardcoded size cannot replace them all.
Compile with warnings such as -Wall -Wextra. GCC and Clang's -fsanitize=address -g can report many invalid accesses on executed paths and show the source location. Such testing helps find mistakes, but unexecuted paths still require reasoning about bounds. Review each indexed read and write by identifying its object, its bound and the condition that keeps that index valid.
Key Takeaways
- Out-of-bounds reads and writes have undefined behavior.
- A successful run is not evidence that an index is valid.
- A copied string includes its terminator in the write count.
- Validate data before deriving an array index.
- Check each table dimension independently.
- Warnings and sanitizers help detect bugs on tested paths.
6.2 Read and copy text within buffer limits
Input length is controlled by whoever supplies the input. Give reading functions a real buffer limit and check what they return. Then decide whether the data forms a complete line and whether it fits the program's own rules.
Read one word with a width
For char word[16];, scanf("%15s", word) reads at most fifteen non-whitespace characters and appends '\0'. The width leaves one of the sixteen positions for the terminator. %s without a width has no length limit; gets had the same fundamental problem for lines and was removed from C in C11.
Check that scanf returns 1 before using a word. It reads a word, not a whole line with spaces. Excess word bytes can remain unread, so a width protects storage without deciding whether the whole input is acceptable.
Read a line with fgets
char buf[8];
if (fgets(buf, sizeof buf, stdin) == NULL) {
return 1;
}
fgets reads at most seven characters here and writes a final zero byte. It keeps a newline when one is reached before the limit. A line containing "hello" and a newline becomes "hello\n" plus the terminator. A final line ending at end-of-file without a newline can still be a successful read. If no characters were read before end-of-file, or a read error occurred, the result is NULL; do not process that buffer as new input.
Distinguish a stored newline from a partial line
Find a newline before removing it:
size_t end = strcspn(buf, "\n");
int had_newline = buf[end] == '\n';
buf[end] = '\0';
Include <string.h>. If no newline was stored, end is the text length and the assignment rewrites the existing terminator. A missing newline can mean a partial line, an exact buffer fill before a newline, or a final line that ended at end-of-file. It alone cannot establish that more characters remain.
For the input line "abcdefghij" and an eight-byte buffer, the first read stores "abcdefg" and the next can read "hij\n". Treating the second chunk as a separate answer would split one logical input. A program can reject a line missing a required newline, or read and discard its remainder before asking again. State the input policy before implementing it.
For up to thirty ASCII name characters plus a newline and a terminator, use 32 bytes. For UTF-8, that is a byte limit: thirty displayed characters may need more storage.
Copy only after checking all requirements
char user[10];
if (argc != 2) return 1;
if (strlen(argv[1]) >= sizeof user) {
puts("Name too long");
return 1;
}
strcpy(user, argv[1]);
The argument-count check comes first. The length check then admits at most nine text bytes, leaving the tenth position for zero. A writing function should receive capacity explicitly, such as read_name(out, capacity), because sizeof out inside it measures a pointer parameter.
For numeric input, read a complete line and convert with strtol, checking the end position, range error and allowed value range. A failed scanf("%d", &n) can leave the offending text unread; blindly repeating it can fail forever on the same bytes. At minimum check its conversion count and handle the invalid input before retrying.
Key Takeaways
- Set a word-read width to one less than buffer capacity.
- Check a read result before using its buffer.
fgetsretains a newline only when it reaches one within the limit.- A missing newline does not by itself establish that input remains.
- Budget for text bytes, the newline when retained, and the terminator.
- Check argument presence before measuring and copying its text.
- Pass buffer capacity into a function that writes to it.
Arrays and Strings in C examples
1Follow an array from declaration to function call
13 steps
- 1IntroductionGive several values one name
An array holds a fixed number of elements of one type. A loop selects positions, while a function can work on the same storage through an array parameter.
Follow four sensor readings from their declaration through a total and an average. At each step, keep three quantities distinct: the number of elements, the last valid index and the number of bytes used for storage.
- 2ExampleRead the initial positions
int readings[4] = {11, 16, 13};There are four elements, with indexes 0 through 3. The initializer supplies 11, 16 and 13 at the first three positions. The omitted value becomes zero, so the array is
{11, 16, 13, 0}.The zero is an initialization rule, not a sign that the array has only three elements. A local array with no initializer would need its values written before use.
- 3ExplanationCount elements at the declaration
size_t n = sizeof readings / sizeof readings[0];The numerator measures all four elements and the denominator measures one. Where
intoccupies four bytes, this is sixteen divided by four, giving 4. The same expression works with another integer size.readings[3]is the last element.readings[4]is past the end even though the array's count is 4.size_tis the unsigned type used for this size result. - 4ChallengeChange the last reading
The declaration is still
int readings[4] = {11, 16, 13};. What assignment replaces the zero with 20, and what count results?---reveal---
Use
readings[3] = 20;. The resulting values are{11, 16, 13, 20}and the count stays 4. An element assignment changes a value, not the number of positions. Index 4 would be one position too far and cannot add storage to this array. - 5ExampleBuild the total one pass at a time
int sum = 0; for (size_t i = 0; i < n; i++) { sum += readings[i]; }Using
{11, 16, 13, 20}, the running sum becomes 11, 27, 40 and 60. After index 3, i becomes 4 and the test fails before another access.Starting sum at zero gives the first value a clean starting point. The strict upper bound lets the loop visit exactly the four existing elements.
- 6Key InsightPreserve a fractional average
For these four readings,
(double) sum / ngives 15.0. The cast happens before division, so the operation can produce a fractional result when the total changes.If the total were 61, integer division
sum / 4would produce 15, whilesum / 4.0would produce 15.25. Assigning the already truncated integer result to a double would only produce 15.0; it would not recover the discarded fraction. - 7TipChoose a valid starting candidate
A maximum search needs a starting value drawn from a nonempty array. For
{-6, -2, -9}, initializemaxto -6, then replace it with -2 when that larger value appears. The last comparison leaves -2 unchanged.Initializing max to zero would return zero, which is not present. For a linear search, the corresponding stopping rule matters too: one mismatch does not establish failure; later elements still need checking.
- 8ChallengeKeep a search moving
A search for 13 checks
{11, 16, 13, 20}. After checking index 0 it returns -1 because 11 differs from 13. What should change, and where is the match?---reveal---
Keep checking after a mismatch and return -1 only after all candidates fail. Here index 1 also differs, but index 2 matches. A successful search can stop there. The failed first comparison describes one element, not the complete array.
- 9ConnectionMove the average into a function
double mean(const int a[], size_t n);The array parameter shares access to the caller's elements. It does not carry their count, so the second parameter supplies it.
constprevents accidental writes through a in this read-only operation.Compute the count where
readingsis declared and callmean(readings, n). Inside mean,sizeof ameasures a pointer parameter instead of the caller's four-element array. - 10ExampleHandle the empty range before dividing
double mean(const int a[], size_t n) { if (n == 0) return 0.0; int sum = 0; for (size_t i = 0; i < n; i++) sum += a[i]; return (double) sum / n; }For the four small readings, the total remains 60 and the result is 15.0. For n equal to zero, the chosen result is 0.0 and no array access or division occurs. The caller must pass an accurate usable count.
- 11ChallengeExplain a changed caller array
A different function receives
int a[], size_t nand sets every visiteda[i]to zero. What happens to the caller's readings, and how could a total-only function prevent such writes?---reveal---
The caller's elements become zero because the function writes into the shared array storage. Its four positions still exist. A total-only function should use
const int a[], making element assignments through a invalid. Changing a scalar copy of n would not change a caller's count variable. - 12ChallengeUse only the readings that exist
A buffer has capacity 10, but only four readings are in use. A new reading must be appended before computing a mean. What checks and count should the program use?
---reveal---
Check that the used count, 4, is below capacity 10. Write at index 4, then update the count to 5. Pass that used count to mean so it processes exactly the five values. Passing the capacity would include positions that have not been supplied as readings.
- 13RecapConnect each count to its job
A declaration creates the positions, a loop visits the used ones, and a function receives the usable count explicitly. A read-only parameter keeps calculations from overwriting input.
When reviewing array code, track the first and last visited indexes and the state of each accumulator. For backward
size_tloops, start at n, continue while i is positive, and accessa[i - 1]so an empty range needs no subtraction from zero.
2Turn stored bytes into text operations
16 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionWatch both text and storage
- 2ExplanationRead a character as a code
- 3ChallengeWrap within the alphabet
- 4ExampleBuild a writable string
- 5Key InsightMove the ending without moving storage
- 6ChallengeLeave an ending position
- 7ExampleSeparate reading from writing
- 8ExampleFinish the new string
- 9ChallengeReverse letters without the terminator
- 10TipClassify bytes with valid arguments
- 11ExampleLay out a UTF-8 accented letter
- 12ExplanationRecognize continuation bytes
- 13ChallengeMeasure a new encoded word
- 14ConnectionChoose operations that keep characters whole
- 15ChallengeCheck a proposed text shortcut
- 16RecapLocate the end and the character boundaries
3Assemble, search and safely format text
14 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionIdentify each function’s contract
- 2ExampleStart with a complete copy
- 3ExampleAppend at the existing ending
- 4ChallengeDecide whether another append fits
- 5ExplanationUse zero to recognize equal text
- 6ExampleRead from a found location
- 7ChallengeInspect a prefix-based command check
- 8TipExpect tokenization to change text
- 9ExampleFormat within a known capacity
- 10ChallengeInterpret the same format in less space
- 11Key InsightA bound may count source bytes
- 12ExampleMove a region that overlaps
- 13ChallengePreserve a complete result after shifting
- 14RecapSeparate bytes from text results
4Read a table and choose a string-list layout
10 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionGive each position two indexes
- 2ExamplePlace values into rows
- 3ExampleReset a row total at the row start
- 4ChallengeChange the grouping, keep the indexes
- 5ExplanationTell a function the row width
- 6ExampleReserve one terminated string per row
- 7Key InsightReferences permit different string lengths
- 8ChallengeReplace text in the right storage
- 9ChallengeKeep both dimensions valid in a function
- 10RecapKeep the table shape visible
5Validate a command-line selection
11 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionReceive text before interpreting it
- 2ExampleMap a shell command to entries
- 3ExplanationCheck the count before conversion
- 4ChallengeGroup one name correctly
- 5TipA returned integer is not enough
- 6ExampleInspect a complete conversion
- 7ExplanationCheck the selection’s own range
- 8ChallengeCatch text that looks partly numeric
- 9Key InsightSpelling and value are separate rules
- 10ChallengeCombine grouping and numeric validation
- 11RecapCheck presence, conversion and purpose
6Repair a bounded input routine
13 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionBound the write and check the result
- 2ExampleFind the extra write
- 3Key InsightA valid last position can hold the ending
- 4ChallengeCheck an index derived from a byte
- 5ExplanationRead a word with terminator space
- 6ExampleRead one bounded line chunk
- 7ExampleObserve where a long line is split
- 8TipInspect a newline before removing it
- 9ChallengeBudget for a complete line
- 10ExampleCheck a copy before writing
- 11ChallengeSeparate a safe prefix from an accepted answer
- 12ConnectionUse diagnostics with bounds reasoning
- 13RecapKeep four checks distinct
Arrays and Strings in C practice questions
Set 1: Array values, loops and function calls10 Practice Questions
Question 1 of 10
a[3] after int a[5] = {6, 9};?Show answerHide answer
Question 2 of 10
int a[7]; where int occupies 4 bytes. What pair gives sizeof a and the last valid index?Show answerHide answer
Question 3 of 10
int a[] = {-12, -4, -9};, what value should a maximum search return?Show answerHide answer
Question 4 of 10
What does this code print?
int a[] = {3, 8, 5, 2};
int sum = 0;
for (int i = 1; i < 4; i++) sum += a[i];
printf("%d", sum);
Show answerHide answer
Question 5 of 10
{2, 9, 7, 4} returns -1 on the first unequal element. Where should its failure return occur?Show answerHide answer
Question 6 of 10
a[i] with a[5 - 1 - i] for i = 0 and i = 1, starting from {4, 7, 1, 8, 6}?Show answerHide answer
Question 7 of 10
void set_first(int a[]) executes a[0] = 11;. What is the first element of the caller’s array after the call?Show answerHide answer
Question 8 of 10
void f(int a[], size_t n), pointers occupy 8 bytes and integers 4 bytes. What is sizeof a / sizeof a[0]?Show answerHide answer
Question 9 of 10
size_t for n and i. It must print an array backward and work for n = 0. What parameter and loop meet both requirements?Show answerHide answer
Question 10 of 10
a[4] from int a[4]?Show answerHide answer
Set 2: Characters, string endings and UTF-810 Practice Questions
Save this lesson to see every answer and practice every set.
Question 1 of 10
"m8n2p" and writes them to a large enough output array. What must it write after the final p?Show answerHide answer
Save this lesson to see this answer.
Question 2 of 10
'6' - '0' produce?Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
('W' - 'A' + 5) % 26 + 'A'?Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
c be passed to isalpha without risking a negative byte argument?Show answerHide answer
Save this lesson to see this answer.
Question 5 of 10
char color[] = "amber";?Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
char s[8] = "planet"; s[3] = '\0'; s[0] = 'P';?Show answerHide answer
Save this lesson to see this answer.
Question 7 of 10
char tag[4] = "east"; printf("%s", tag); unsafe?Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
char s[] = "lamp"; by swapping s[i] with s[len - i] while i < len / 2, where len is strlen(s). What are the final strlen(s) and unchanged capacity?Show answerHide answer
Save this lesson to see this answer.
Question 9 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 10 of 10
41 C3 B1 42 00. A program writes zero at byte index 2. What are the new strlen and UTF-8 validity of the text?Show answerHide answer
Save this lesson to see this answer.
Set 3: Choose and check library operations10 Practice Questions
Save this lesson to see every answer and practice every set.
Question 1 of 10
char s[] = "wxyz"; memmove(s, &s[1], 4);, what does s contain as a string?Show answerHide answer
Save this lesson to see this answer.
Question 2 of 10
"silver"?Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
char label[10] = "blue"; strcat(label, "berry"); store its full result?Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
char out[20]; strcat(out, "go");. Why is this an invalid way to start building a string?Show answerHide answer
Save this lesson to see this answer.
Question 5 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
char *p = strrchr("archive.backup.bin", '.');, what text begins at p?Show answerHide answer
Save this lesson to see this answer.
Question 7 of 10
"on,,off;wait". Calls use strtok(s, ",;"), then repeat strtok(NULL, ",;") until NULL. What tokens are produced, and is s preserved?Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
char b[6]; int r = snprintf(b, sizeof b, "%s:%d", "job", 84); store and return?Show answerHide answer
Save this lesson to see this answer.
Question 9 of 10
"AB". The program runs strncat(b, "123456", sizeof b - strlen(b) - 1). What is the resulting text and its stored byte count including zero?Show answerHide answer
Save this lesson to see this answer.
Question 10 of 10
Show answerHide answer
Save this lesson to see this answer.
Set 4: Tables, text lists and startup input10 Practice Questions
Save this lesson to see every answer and practice every set.
Question 1 of 10
int table[4][6]?Show answerHide answer
Save this lesson to see this answer.
Question 2 of 10
sizeof table[0] for int table[3][5]?Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
{{2, 7, 4}, {6, 1, 5}}. A loop initializes sum once to zero before both loops, adds one row at a time, and prints after each row without resetting sum. What values print, and how is it corrected?Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 5 of 10
char names[3][width] to store "Iris", "Magnolia" and "Ash" as complete ASCII strings?Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
char rows[2][5] = {"oak", "elm"}; and const char *refs[] = {"oak", "elm"};. It must replace the first listed word with complete text "cedar". What treatment is valid for each layout?Show answerHide answer
Save this lesson to see this answer.
Question 7 of 10
./mark "West Room" 9. What are argc and argv[2]?Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 9 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 10 of 10
Show answerHide answer
Save this lesson to see this answer.
Set 5: Input limits and complete results10 Practice Questions
Save this lesson to see every answer and practice every set.
Question 1 of 10
char word[18]?Show answerHide answer
Save this lesson to see this answer.
Question 2 of 10
"pine\n" into a 10-byte array. What are strlen of the result and the index of its terminator?Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
char b[6] without other reads. What text does the second call store?Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 5 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
counts[c - 'a']++ unsafe for unchecked character input?Show answerHide answer
Save this lesson to see this answer.
Question 7 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
char out[6]. The call snprintf(out, sizeof out, "%s%d", "R-", 1234) is followed by if (r > sizeof out) reject();, where r is its nonnegative return. What is stored, does that test reject it, and what must change?Show answerHide answer
Save this lesson to see this answer.
Question 9 of 10
ERANGE and requires *end == '\0', but does not check end == start. What invalid input can still be accepted as zero?Show answerHide answer
Save this lesson to see this answer.
Question 10 of 10
41 C3 A9 E2 82 AC 00. A program copies it into char b[5], reserving its last position for zero. What are the resulting strlen, starting-byte count and encoding validity?Show answerHide answer
Save this lesson to see this answer.
Set 6: Trace and repair combined C operations10 Practice Questions
Save this lesson to see every answer and practice every set.
Question 1 of 10
int a[4] = {[2] = 9};?Show answerHide answer
Save this lesson to see this answer.
Question 2 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
int g[3][4], what is the zero-based element position of g[2][1] in row-major storage?Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
./pick "" blue. How do argv[1] and argv[argc] differ?Show answerHide answer
Save this lesson to see this answer.
Question 5 of 10
int a[], size_t n and executes a[0] = 0; n = 0;. The caller supplied {5, 8, 2} and count 3, then computes the integer sum of its elements divided by its count. What average and caller count result?Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 7 of 10
strncpy(b, "planet", sizeof b);. The program requires the complete original word. What repair satisfies the requirement?Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
int values[4] = {14, 28, 35, 49};. It fully converts "04" with decimal strtol, accepts selections 1–4, then reads values[selected]. It must allow all four selections and map selection 1 to the first value. What happens, and what index rule fixes it?Show answerHide answer
Save this lesson to see this answer.
Question 9 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 10 of 10
Show answerHide answer
Save this lesson to see this answer.
Stuck on your own question? Homework Help
Key Terms
A
Accumulator
sum at zero lets each sum += a[i] add the next array element to the current total.Application range
argc
argc gives the count of argument strings supplied to main, normally including the string used to launch the program../label red blue under a normal shell gives argc equal to 3: the launch name and two colors.argv
argv provides access to argument strings by index, followed by a null pointer at the position given by argc.argc, a program can use strcmp(argv[1], "-v") to recognize its first option.Array
int levels[6] reserves six integers; updating levels[2] changes the third stored value.Array of string pointers
const char *names[] = {"Al", "Mina"} stores two references and allows a reference to be replaced.Array parameter
int a[] still needs a separate length before it can loop over its input.ASCII
'8' - '0' produces 8 because digit character codes are consecutive.atoi
atoi converts an initial numeric portion to int without a reliable error report and has undefined behavior on overflow.atoi, so a returned number does not establish that the full input was numeric.B
Bit mask
0xC0 keeps the top two bits so a program can recognize the UTF-8 continuation pattern.Buffer
Buffer overflow
Byte order comparison
strcmp puts an accented Latin letter after ASCII z because its leading byte has a higher value.C
C string
char pet[] = "dog" contains three letters followed by a zero-valued character.Caesar shift
Capacity
Character classification
<ctype.h>.isdigit((unsigned char) c) gives a nonzero result when the stored byte represents a decimal digit.Character constant
int that represents a character code, such as the code for a letter.printf("%c %d", 'B', 'B') displays the letter B and the numeric value 66.Character encoding
Column
g[r][c].g[r][c].Command-line argument
Concatenation
Const-qualified parameter
const int a[] because adding the input values does not require changing them.Continuation byte
10 and follows a leading byte within a multibyte encoded code point.Conversion count
scanf successfully completed, used to check whether requested data was actually read.D
Delimiter
E
Element
int a[3] = {8, 1, 6}, the element a[1] holds 1 and can be assigned another integer.Empty string
char message[20] = "" provides a valid empty destination for a later append that fits its capacity.End pointer
strtol returns 9 but the end pointer refers to the z that was not consumed.End-of-file
fgets can still succeed if it read characters before reaching that condition.fgets buffer receive that final text.ERANGE
ERANGE identifies a range error reported through errno, such as a strtol result too large or small for long.errno to zero before conversion, then test for ERANGE before accepting a long digit sequence.Escape sequence
"A\nB" contains a newline between its letters; the backslash and n encode one character.Exit status
main signals successful completion.F
fgets
fgets reads bounded line input, retaining a reached newline and adding a terminator when it successfully reads text.Field width
%s conversion limits the number of input characters stored before the additional terminating zero byte.char word[10], the conversion %9s reserves the tenth available byte for the terminator.Filtering
Fixed-width string list
char labels[5][12], every label has twelve bytes of storage even when its current text is shorter.Frequency array
counts[4]++ adds one occurrence to that digit’s tally.I
In-place transformation
Index
Initializer
int hits[4] = {3} initializes the first counter to 3 and the other three counters to zero.L
Linear search
{3, 8, 2} for 2 succeeds at index 2 after the earlier unequal elements are checked.Locale
toupper in the "C" locale does not turn a complete UTF-8 accented letter into its uppercase form.M
memcmp
memcmp compares a fixed number of bytes and reports their ordering, testing storage representations rather than numeric values.memcmp finds different stored bytes.memcpy
memcpy copies exactly a requested number of bytes between nonoverlapping regions without interpreting string terminators.memcpy(b, a, sizeof a) copies the full storage of array a when b has enough room and is a separate region.memmove
memmove copies a requested byte count with defined behavior even when source and destination regions overlap.memmove.memset
memset fills a region with a repeated byte value, without assigning that numeric value separately to each array element.memset(counts, 0, sizeof counts) zeroes an integer array; byte value 1 would not assign integer 1.N
Nested loops
Null pointer
strchr(name, '.') != NULL before using its result as the beginning of a filename suffix.Null terminator
text[2] = '\0' makes string functions stop after the first two characters in that array.Numeric conversion
% 26.O
Off-by-one error
i <= n makes a zero-based n-element traversal attempt one extra access at index n.Out-of-bounds access
a[8] in an eight-element array is invalid even if a test run seems to return a useful value.Output array
out[i] while the caller keeps ownership of the array.P
Partial line
fgets chunks, so the second chunk should not become a new answer.R
Row
int table[2][5], table[1] denotes the second row, which contains five integer positions.Row width
int g[][6] accepts rows six integers wide, while the caller supplies the row count separately.Row-major order
grid[0][3] in a four-column table’s storage is grid[1][0].S
size_t
size_t is an unsigned integer type used for object sizes and returned by sizeof; it is commonly used for array lengths.printf("%zu", sizeof data) uses the size format to print the number of bytes occupied by data.snprintf
snprintf formats text within a supplied capacity and returns the full required text length or a negative error result.strcat
strcat appends a source string at a destination’s existing terminator and writes a new terminator after the combined text.strchr
strchr finds the first matching character in a string and returns its location, or a null pointer if no match exists.strchr finds the suffix ".part.ext", including the first dot.strcmp
strcmp compares terminated strings as unsigned bytes and returns a negative, zero or positive result according to their order.strcmp("oak", "oak") == 0 tests equal contents even when the two strings occupy separate storage.strcpy
strcpy copies a complete source string and its terminator into a nonoverlapping destination with enough available storage.char out[5].strcspn
strcspn measures the initial part of a string containing none of the characters supplied in its second argument.strcspn("fir:oak", ":,") returns 3 because the first listed separator occurs after the three letters of fir.String length
String literal
const char *label = "ready" refers to a literal; a writable copy can use char label[] = "ready".strlen
strlen scans a valid string to its terminator and returns the number of preceding bytes as a size_t value.char tag[30] = "oak", strlen(tag) is 3 while sizeof tag reports the full 30-byte capacity.strncat
strncat appends at most a specified number of source text bytes and then writes a terminator after them.strncmp
strncmp compares no more than a specified number of string bytes, stopping sooner at an ending or a difference.strncpy
strncpy copies up to a bound and pads shorter text with zeros, but does not terminate when the source fills that bound.strrchr
strrchr finds the last matching character in a string and returns its location, or a null pointer if no match exists.strrchr can locate the extension separator.strtol
strtol converts text to long using a chosen base and reports a stopping location and range error information.strtol(text, &end, 10) lets the program reject a leftover letter after otherwise valid decimal digits.Substring
strstr finds its first matching starting location.T
Tokenization
strtok does this by modifying the input and remembering a position.strtok(NULL, ",") call continues splitting the same writable input instead of restarting it.Truncation
Two-dimensional array
int board[4][6] provides 24 integers, reached through four valid row indexes and six valid column indexes.U
Undefined behavior
Unicode code point
Unsigned wraparound
size_t countdown cannot stop by becoming negative; use i > 0 and access position i - 1.Used count
UTF-8
V
Variable-length array
const int n = 6; int a[n]; creates a VLA because n is not a constant expression.Variably modified parameter
int g[rows][cols] after parameters defining positive matching row and column counts.W
Word boundary
About this lesson
Make your own Study Materials
Turn your notes, slides and documents into Study Guides, Practice Questions, Flashcards and 6 more tools.