Arrays, Character Data, and StringsIntroductionArrays and strings are among the most important data structures in C. They allow a program to represent collections of related values, process data systematically, and communicate with external devices and files. In embedded systems, signal-processing software, communication protocols, compilers, operating systems, and scientific applications, arrays provide the foundation for buffers, matrices, lookup tables, sampled signals, and memory-mapped data. C treats arrays and strings closely but not identically. An array is a fixed-size, contiguous sequence of objects of the same type. A C string is a sequence of characters terminated by a null character, written as \0. Understanding this distinction is essential because C does not automatically store array bounds or prevent out-of-range access. Learning ObjectivesAfter studying this chapter, the learner should be able to: - explain the representation and properties of one-dimensional and multidimensional arrays;
- declare, initialize, access, and modify arrays safely;
- relate array indexing to address arithmetic and pointer expressions;
- process character data using character constants and library functions;
- create, compare, copy, and transform C strings;
- distinguish arrays from pointers and identify common memory errors;
- apply arrays and strings to an embedded or communication-oriented problem;
- evaluate efficiency, portability, safety, and maintainability of array-based programs.
1. Array FundamentalsAn array declaration specifies an element type, an identifier, and a fixed number of elements. For example: int samples[8]; This declaration reserves space for eight consecutive objects of type int. The valid indices are 0 through 7. If sizeof(int) is 4 bytes on a particular implementation, the array occupies 32 bytes, excluding possible alignment considerations relevant to larger objects. The number of elements is obtained by: size_t n = sizeof samples / sizeof samples[0]; This expression is valid where samples is an actual array, not after it has been passed to a function as a parameter. 1.1 Declaration and InitializationAn array may be initialized when it is defined: int a[5] = {10, 20, 30, 40, 50};
int b[] = {3, 6, 9}; /* size inferred as 3 */
int c[5] = {1, 2}; /* remaining elements become zero */
int d[5] = {0}; /* all elements become zero */The initializer must not contain more elements than the declared size. An array with automatic storage duration that is not initialized contains indeterminate values; reading such values can produce undefined behavior. Objects with static storage duration are initialized to zero when no explicit initializer is supplied. Arrays may contain any object type, including structures and other arrays. They cannot contain functions, and an array cannot be assigned to another array using the assignment operator. 1.2 Indexing and BoundsFor an array a of length n, the valid access range is: 0 ≤ i < n where i is the index. C does not check this condition at run time. Accessing a[n] or a negative index is outside the array and results in undefined behavior. Consequences range from apparently correct execution to data corruption, security vulnerabilities, or program termination. A robust loop therefore uses the array length explicitly: for (size_t i = 0; i < n; ++i) {
printf("%d\n", a[i]);
}The type size_t, defined by standard headers such as <stddef.h> and used by sizeof, is appropriate for object sizes and array indices. Care is required when comparing it with signed integers, because implicit conversions can produce unexpected results. 1.3 Memory Layout and Address ArithmeticArray elements are contiguous and have equal spacing. In the expression a[i], the index is scaled by the size of the element type. Conceptually: address(a[i]) = address(a[0]) + i × sizeof(a[0]) The expression is an explanatory model of address calculation; C pointer arithmetic is defined in terms of array elements rather than raw byte arithmetic. The equality a[i] == *(a + i) holds for valid indices. The expression a + i points to the ith element, and dereferencing it obtains that element. Pointer arithmetic is valid within an array object or one position beyond its last element. The one-past pointer may be formed and compared, but it must not be dereferenced. 2. Passing Arrays to FunctionsWhen an array is used as a function argument, it is converted to a pointer to its first element. Thus, the function does not automatically receive the array length. int sum(const int values[], size_t n) {
int total = 0;
for (size_t i = 0; i < n; ++i)
total += values[i];
return total;
}The parameter const int values[] is adjusted by the language to const int *values. The caller must supply the correct length. The const qualifier documents that the function will not modify the elements through this pointer. A useful interface for a modifiable array is: void scale(double data[], size_t n, double factor) {
for (size_t i = 0; i < n; ++i)
data[i] *= factor;
}The function can modify the caller's elements because it receives their addresses. It cannot determine the array's capacity using sizeof data; that expression measures the pointer parameter. Modern C also supports variable-length array notation in some contexts, for example: void print_matrix(size_t rows, size_t cols,
int matrix[rows][cols]);The dimensions are supplied by the caller, and the second dimension is needed to calculate offsets. Variable-length arrays are optional in newer C standards and may be unsuitable for systems with strict stack constraints. Dynamic allocation or fixed-capacity designs may be preferable in embedded applications. 3. Multidimensional ArraysA two-dimensional array is an array whose elements are themselves arrays: int matrix[3][4]; This object contains three rows and four columns, for a total of 12 int elements. C stores the elements in row-major order: all elements of row 0, followed by row 1, and so forth. For a matrix with R rows and C columns, the element at row r and column c is valid when: 0 ≤ r < R and 0 ≤ c < C Its linear offset from the first element is: offset = r × C + c This formula assumes zero-based indexing and contiguous row-major storage. It explains why a function receiving a two-dimensional array must know the column dimension. void add_bias(int rows, int cols, int m[rows][cols], int bias) {
for (int r = 0; r < rows; ++r)
for (int c = 0; c < cols; ++c)
m[r][c] += bias;
}A three-dimensional array can represent, for example, a sequence of image frames, where the dimensions may mean frame, row, and column. Higher dimensions are legal, but data layout and cache behavior should be considered carefully. Example: Matrix MultiplicationIf A has dimensions m × p and B has dimensions p × n, their product C = AB has dimensions m × n. Each result element is: C[i][j] = ∑(k = 0 to p − 1) A[i][k] × B[k][j] The operation is defined only when the number of columns in A equals the number of rows in B. A direct implementation is: void multiply(size_t m, size_t p, size_t n,
const double A[m][p],
const double B[p][n],
double C[m][n]) {
for (size_t i = 0; i < m; ++i) {
for (size_t j = 0; j < n; ++j) {
C[i][j] = 0.0;
for (size_t k = 0; k < p; ++k)
C[i][j] += A[i][k] * B[k][j];
}
}
}The direct algorithm performs approximately m × p × n multiplications and a similar number of additions. For large matrices, loop ordering, blocking, vectorization, and specialized numerical libraries can substantially affect performance. 4. Character DataA character constant such as 'A', '7', or '?' has type int in C, although it represents a character value. The execution character set determines the numerical encoding. ASCII is common, but portable programs should not assume that every character set has identical numerical values. The header <ctype.h> provides functions including isalpha, isdigit, isspace, toupper, and tolower. Their argument must either be EOF or a value representable as unsigned char. Passing a negative char value directly can cause undefined behavior on implementations where char is signed. Safe usage is: if (isalpha((unsigned char)ch))
ch = (char)toupper((unsigned char)ch);The type unsigned char is also useful for raw bytes, binary protocol fields, and sensor data. Character text and arbitrary binary data should not be confused: binary data may contain zero bytes and need not obey string conventions. 5. C StringsA C string is not a distinct built-in type. It is a contiguous sequence of characters ending with a null character whose value is zero. For example: char word[] = "sensor"; The compiler allocates seven characters: s, e, n, s, o, r, and \0. Therefore, the array capacity must include the terminator. char a[6] = "sensor"; /* incorrect: no room for '\0' */
char b[7] = "sensor"; /* correct */
char c[] = "sensor"; /* size inferred as 7 */ A string literal has static storage duration and should not be modified. This is incorrect: char *p = "read-only?";
/* p[0] = 'R'; undefined behavior */ Use an array when modification is required: char message[] = "read-only?";
message[0] = 'R'; The length of a valid null-terminated string is obtained with strlen from <string.h>. Its value excludes the null character. The storage capacity is a separate concept and must be tracked by the programmer. 5.1 Essential String Functions| Function | Purpose | Important condition |
|---|
strlen(s) | Finds string length | s must point to a terminated string | strcmp(a, b) | Lexicographically compares strings | Returns negative, zero, or positive; do not assume exact nonzero values | strcpy(dst, src) | Copies a string | Destination must have sufficient space and must not improperly overlap | strncpy(dst, src, n) | Copies up to n characters | May not append \0 if source length is at least n | strcat(dst, src) | Appends one string | Destination must have adequate remaining capacity | fgets(buf, size, stream) | Reads bounded text | May retain the newline if one is read |
The frequently criticized functions strcpy and strcat are not intrinsically invalid, but they provide no capacity checking. In security-sensitive code, explicit length management or carefully designed bounded interfaces are preferable. A common safe input pattern is: char line[80];
if (fgets(line, sizeof line, stdin) != NULL) {
line[strcspn(line, "\n")] = '\0';
}strcspn requires <string.h> and finds the first position containing a character from the second string. The assignment removes a trailing newline when present; if no newline occurs, it replaces the terminating null character with itself.
5.2 Comparing Strings CorrectlyThe expression a == b compares addresses when a and b are pointers; it does not compare their text. Use: if (strcmp(a, b) == 0) {
/* strings contain the same sequence of characters */
}Lexicographic comparison follows the implementation's character ordering. It is therefore unsuitable for locale-aware human-language collation without additional facilities. 5.3 Arrays of StringsAn array of pointers can store strings of different lengths: const char *commands[] = {"START", "STOP", "RESET"};The pointers refer to string literals and should therefore be const. A two-dimensional character array stores characters in fixed-width rows: char labels[3][10] = {"ADC", "UART", "GPIO"};The latter allocates 30 characters and permits modification of each row, provided assignments remain within each row's capacity. The former generally uses less space for short strings but requires careful management of the pointed-to storage. 6. Dynamic Arrays and OwnershipFixed arrays are suitable when capacity is known at compile time. Dynamic arrays are useful when the required size is discovered at run time. The standard allocation functions are declared in <stdlib.h>: int *values = malloc(n * sizeof *values);
if (values == NULL) {
/* allocation failed */
}
/* use values[0] through values[n - 1] */
free(values);A robust allocation must consider multiplication overflow before requesting n × sizeof values bytes. For example, when n is externally controlled, check that n <= SIZE_MAX / sizeof values, with SIZE_MAX available from an appropriate standard header on the implementation. calloc(n, size) allocates space for n objects and initializes the allocated bytes to zero. realloc can resize an allocation, but assigning its result directly to the only pointer risks losing the original block if reallocation fails:
int *temporary = realloc(values, new_n * sizeof *values);
if (temporary != NULL) {
values = temporary;
n = new_n;
}Dynamic arrays require an ownership policy: one component must know who is responsible for freeing the allocation and when. Leaks, double frees, use-after-free, and dangling pointers are design failures, not merely syntax errors. 7. Safety, Efficiency, and Professional PracticeArray and string defects are frequent sources of vulnerabilities. Buffer overflows can overwrite control data, corrupt neighboring objects, or expose confidential information. Reliable practice includes: - maintaining explicit capacity and current length;
- validating every external length before indexing or copying;
- reserving space for the null terminator;
- using
const to express read-only access; - compiling with warnings and treating important warnings as errors;
- using sanitizers, static analysis, unit tests, and fuzz testing;
- documenting ownership, lifetime, encoding, and units of array contents;
- avoiding assumptions about
sizeof(int), character encoding, or endianness; - checking integer multiplication for allocation overflow;
- separating text processing from binary protocol processing.
The asymptotic complexity of a linear search through n elements is O(n) in the worst case. Binary search is O(log n), but it requires sorted data and careful midpoint and boundary handling. In practical embedded systems, a linear search over a small table may be simpler and faster than a more complex algorithm because of lower constant overhead. 8. Applied Case: UART Command Buffer in an Embedded ControllerBackgroundConsider an illustrative embedded controller that receives commands through a UART interface. A command is a line of ASCII text such as SET RATE 9600 or STATUS. The controller must buffer the input, detect line completion, prevent overflow, and dispatch valid commands. ProblemUART input arrives one character at a time. The software cannot assume that a complete command arrives in one operation. A careless implementation may write beyond its buffer, fail to terminate the string, or treat arbitrary binary bytes as text. ApproachA bounded line-accumulation routine can maintain a buffer, its capacity, and its current length: #include <stdbool.h>
#include <stddef.h>
bool append_char(char buffer[], size_t capacity,
size_t *length, char ch) {
if (buffer == NULL || length == NULL || capacity == 0)
return false;
if (ch == '\n' || ch == '\r') {
buffer[*length] = '\0';
return true;
}
if (*length + 1 >= capacity)
return false; /* would leave no room for '\0' */
buffer[*length] = ch;
++*length;
buffer[*length] = '\0';
return true;
}The condition *length + 1 >= capacity reserves one position for the terminating null character. In production firmware, the caller should distinguish a completed command from an overflow event and reset the buffer after either event. Parsing should then validate the command vocabulary and numeric ranges before changing hardware state. Analysis and OutcomeThis design has constant work per received character, O(1), and uses fixed memory, which is predictable for a real-time system. It avoids dynamic allocation and ensures that every successfully maintained buffer is terminated. However, it does not by itself authenticate commands, handle Unicode, or protect against communication noise. A deployed system may need framing, checksums, timeouts, rate limiting, and access control. Learning PointsThe case demonstrates that array capacity, string length, input validation, and protocol semantics are separate concerns. A correct string operation is not automatically a correct communication protocol. Safety must be designed at the interface boundary. 9. Contemporary and Research PerspectivesModern C development increasingly combines language-level discipline with tooling. AddressSanitizer, UndefinedBehaviorSanitizer, compiler diagnostics, formal static analyzers, and fuzzers can detect many invalid accesses and lifetime errors. These tools supplement, but do not replace, a correct ownership and bounds model. In signal processing and machine learning at the edge, arrays often represent vectors, tensors, and sampled streams. Performance depends on memory locality, alignment, data layout, and numerical representation. A row-major matrix may be natural in C, whereas a library or accelerator may prefer another layout. Interoperability therefore requires explicit documentation of dimensions, strides, signedness, and byte order. AI-assisted code generation can produce array and string routines quickly, but generated code must be reviewed for termination assumptions, integer overflow, incorrect pointer ownership, and nonportable behavior. Testing with adversarial lengths and malformed input is especially important. Chapter SummaryArrays are fixed-size, contiguous collections whose valid indices range from zero through one less than their length. Indexing is closely related to pointer arithmetic, but bounds are not checked by C. Functions receiving arrays generally receive pointers, so length and capacity must be passed explicitly. Multidimensional arrays use row-major layout, and their trailing dimensions are required for correct address calculation. Character data uses character values and byte-oriented objects, whereas a C string is a null-terminated character sequence. String capacity must include the terminator, and string comparison requires functions such as strcmp, not pointer equality. Library functions must be used with valid arguments, and external input must be bounded and validated. Dynamic arrays introduce allocation, resizing, ownership, and lifetime concerns. Professional C programming combines explicit size management, const correctness, overflow checks, static analysis, sanitizers, testing, and careful documentation. In embedded communication systems, these principles support predictable memory use and robust protocol handling. Key TermsArray: A contiguous sequence of objects of identical type. Index: An integer position used to select an array element. Bounds: The valid lower and upper limits of an array index. Pointer arithmetic: Address computation in units of the pointed-to object type. One-past pointer: A pointer to the position immediately after an array, which may be formed but not dereferenced. Multidimensional array: An array whose elements are themselves arrays. Row-major order: Storage order in which complete rows are stored consecutively. Character constant: A source-language notation representing a character value. Null character: The character \0 used to terminate a C string. String literal: A quoted character sequence supplied by the C implementation. String capacity: The total storage available for characters, including a terminator when needed. String length: The number of characters before the null character. const: A qualifier expressing that an object must not be modified through a particular access path.
size_t: An unsigned type used for object sizes and related indices.
Dynamic allocation: Obtaining storage during program execution. Ownership: Responsibility for managing an object's lifetime and releasing its storage. Buffer overflow: An access or write beyond an object's allocated bounds. Undefined behavior: Behavior for which the C standard imposes no requirements. Review QuestionsShort Answer Questions- Why must a C string usually have one more array element than its visible characters?
- What is the relationship between
a[i] and *(a + i)? - Why must an array length normally be passed separately to a function?
- What is the difference between string length and string capacity?
- Why is
strcmp(a, b) == 0 preferred to a == b for comparing string contents?
Long Answer Questions- Explain array declaration, initialization, indexing, storage layout, and bounds with suitable C examples.
- Discuss one-dimensional and multidimensional arrays, including row-major storage and function parameters.
- Explain C strings, string literals, null termination, and the safe use of common
<string.h> functions. - Describe dynamic array allocation, resizing, ownership, and the major memory-management failures that can occur.
- Analyze the role of arrays and strings in embedded communication systems, including safety and performance considerations.
Analytical / Application Questions- Design a bounded command-buffer interface for a UART device. Specify its state variables, overflow policy, termination rule, and testing strategy.
- A program receives an externally supplied element count and allocates an array of structures. Identify the arithmetic, allocation, indexing, and ownership checks required before processing the data.
- Compare a two-dimensional character array with an array of character pointers for storing device labels. Recommend one for fixed-capacity embedded firmware and justify the decision.
Multiple Choice Questions with AnswersQuestion 1For int a[5], which index is valid for the last element? A. 0 B. 4 C. 5 D. 6 Correct Answer: B. 4 Question 2What terminates a conventional C string? A. A newline character B. A space character C. The null character \\0 D. The character EOF Correct Answer: C. The null character \\0 Question 3What does strcmp(a, b) == 0 indicate? A. The pointers have different addresses B. The strings contain the same character sequence C. The strings have different lengths D. The first string is alphabetically greater Correct Answer: B. The strings contain the same character sequence Question 4In a function parameter, an array such as int values[] is generally adjusted to what? A. An integer object B. A structure C. A pointer to the first element D. A copied array with automatic bounds checking Correct Answer: C. A pointer to the first element Question 5For char word[] = "sensor";, how many characters are allocated? A. 6 B. 7 C. 8 D. The size of a pointer Correct Answer: B. 7 Question 6Which function is designed to obtain the length of a null-terminated string? A. sizeof B. strcpy C. strlen D. strcmp Correct Answer: C. strlen Question 7For a matrix declared as int m[3][4], what is the valid range of the column index? A. 0 through 2 B. 1 through 4 C. 0 through 3 D. 1 through 3 Correct Answer: C. 0 through 3 Question 8Why is const char *p = "ABC"; appropriate for a string literal pointer? A. It guarantees the literal is dynamically allocated B. It indicates the pointed-to characters must not be modified through p C. It makes the string automatically mutable D. It removes the terminating null character Correct Answer: B. It indicates the pointed-to characters must not be modified through p Question 9What must be reserved when storing a six-character string in a character array? A. Exactly six elements only B. At least seven elements, including space for \\0 C. One element for each word D. No terminator if strlen is used Correct Answer: B. At least seven elements, including space for \\0 Question 10Which practice best reduces the risk of a buffer overflow during input? A. Ignoring the input length B. Using a fixed buffer without a terminator C. Validating capacity and reserving space for the null character D. Comparing input pointers with == Correct Answer: C. Validating capacity and reserving space for the null character
|