Arrays and strings are the two most heavily used foundational data structures in C. Understanding how they behave at the memory level lets you write efficient code, avoid serious security bugs such as buffer overflows, and prepares you for the more advanced topics of pointers and dynamic allocation.
1. One-dimensional arrays
Declaring and initialising
An array is a collection of elements of the same data type, stored contiguously in memory. The index starts at 0.
// Cach 1: Khai bao roi gan gia tri tung phan tu
int scores[5];
scores[0] = 85;
scores[1] = 92;
scores[2] = 78;
scores[3] = 90;
scores[4] = 88;
// Cach 2: Khoi tao truc tiep khi khai bao
int scores[] = {85, 92, 78, 90, 88}; // Compiler tu dong tinh size = 5
// Cach 3: Khoi tao mot phan (phan tu con lai = 0)
int data[10] = {1, 2, 3}; // data[3]..data[9] deu bang 0
// Cach 4: Khoi tao tat ca bang 0
int zeros[100] = {0};
C does NOT check array bounds (no bounds checking)
Unlike Java or Python, C has no array bounds checking whatsoever at run time. Accessing outside an array is undefined behaviour β the program may run perfectly normally, return a garbage value, or crash immediately.
int arr[3] = {10, 20, 30};
// NGUY HIEM: Truy cap ngoai pham vi β Undefined Behavior!
printf("%d\n", arr[5]); // Doc gia tri rac tu vung nho khong thuoc mang
arr[-1] = 99; // Ghi de len vung nho khong thuoc mang β co the crash
Array decay β an array degrades into a pointer
When you pass an array into a function, it automatically "decays" into a pointer to its first element. That means:
arris equivalent to&arr[0]when passed to a function.-
The receiving function cannot know the array's size β you have to pass a separate
sizeparameter. -
The
sizeof(arr)/sizeof(arr[0])trick only works in the scope where the array was declared, not inside a function that received it.
Finding the largest and smallest value in an array
#include <stdio.h>
void findMaxMin(int arr[], int size, int *max, int *min) {
*max = arr[0];
*min = arr[0];
for (int i = 1; i < size; i++) {
if (arr[i] > *max) *max = arr[i];
if (arr[i] < *min) *min = arr[i];
}
}
int main() {
int numbers[] = {42, 17, 93, 8, 56, 71, 3, 88};
int size = sizeof(numbers) / sizeof(numbers[0]); // = 8, chi dung o day
int max, min;
findMaxMin(numbers, size, &max, &min);
printf("Max = %d, Min = %d\n", max, min); // Max = 93, Min = 3
return 0;
}
Note how the initialisation is done: both max and min start from
arr[0] rather than from 0. Starting from 0 breaks the moment the array is entirely
negative β a classic bug where the program still runs and still prints a result, only the result is
wrong.
2. Multi-dimensional arrays
Declaring a 2D array
A 2D array is really an array of arrays. In memory the elements are laid out in row-major order β the whole of row 0 first, then row 1, then row 2, and so on.
// Khai bao ma tran 3x4
int matrix[3][4] = {
{1, 2, 3, 4}, // Hang 0
{5, 6, 7, 8}, // Hang 1
{9, 10, 11, 12} // Hang 2
};
// Truy cap phan tu: matrix[hang][cot]
printf("%d\n", matrix[1][2]); // In ra: 7
Memory layout
A 2D array int m[3][4] is actually stored contiguously in memory like this:
Memory addresses (row-major order): ββββββ¬βββββ¬βββββ¬βββββ¬βββββ¬βββββ¬βββββ¬βββββ¬βββββ¬βββββ¬βββββ¬βββββ βm[0]βm[0]βm[0]βm[0]βm[1]βm[1]βm[1]βm[1]βm[2]βm[2]βm[2]βm[2]β β[0] β[1] β[2] β[3] β[0] β[1] β[2] β[3] β[0] β[1] β[2] β[3] β ββββββΌβββββΌβββββΌβββββΌβββββΌβββββΌβββββΌβββββΌβββββΌβββββΌβββββΌβββββ€ β 1 β 2 β 3 β 4 β 5 β 6 β 7 β 8 β 9 β 10 β 11 β 12 β ββββββ΄βββββ΄βββββ΄βββββ΄βββββ΄βββββ΄βββββ΄βββββ΄βββββ΄βββββ΄βββββ΄βββββ βββββ Row 0 ββββββΊ βββββ Row 1 ββββββΊ βββββ Row 2 ββββββΊ
Matrix transpose
#include <stdio.h>
#define ROWS 3
#define COLS 4
int main() {
int matrix[ROWS][COLS] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
int transposed[COLS][ROWS]; // Dao kich thuoc
for (int i = 0; i < ROWS; i++) {
for (int j = 0; j < COLS; j++) {
transposed[j][i] = matrix[i][j];
}
}
printf("Ma tran chuyen vi:\n");
for (int i = 0; i < COLS; i++) {
for (int j = 0; j < ROWS; j++) {
printf("%3d ", transposed[i][j]);
}
printf("\n");
}
return 0;
}
Jagged arrays via an array of pointers
C does not support jagged arrays directly the way Java does. You can, however, simulate one with
an array of pointers, where each pointer points to an array of a different size
(allocated dynamically with malloc).
#include <stdio.h>
#include <stdlib.h>
int main() {
int *rows[3]; // Mang 3 con tro
int sizes[] = {2, 5, 3}; // Moi hang co kich thuoc khac nhau
for (int i = 0; i < 3; i++) {
rows[i] = (int*) malloc(sizes[i] * sizeof(int));
for (int j = 0; j < sizes[i]; j++) {
rows[i][j] = (i + 1) * 10 + j; // Gan gia tri vi du
}
}
// In ra mang rang cua
for (int i = 0; i < 3; i++) {
printf("Hang %d (%d phan tu): ", i, sizes[i]);
for (int j = 0; j < sizes[i]; j++) {
printf("%d ", rows[i][j]);
}
printf("\n");
free(rows[i]); // Giai phong bo nho
}
return 0;
}
The difference from a real 2D array: here each row is its own separate allocation, so
the rows do not sit next to each other in memory. In exchange, every row can be any length you like β
and the price is that you must free() each row individually before releasing the array of
pointers.
3. Strings in C
C has no separate string data type the way Java or Python do. A string in C is simply
an array of characters terminated by the null character '\0'
(which has ASCII value 0).
String literal vs char array
// Cach 1: Con tro tro toi string literal (CHI DOC β nam trong text segment)
char *greeting = "Hello";
// greeting[0] = 'h'; // NGUY HIEM! Undefined behavior β co the crash
// Cach 2: Mang ky tu (CO THE GHI β nam tren stack)
char name[] = "Hello";
name[0] = 'h'; // OK! name bay gio la "hello"
// Cach 3: Khai bao tuong minh voi null terminator
char city[6] = {'H', 'a', 'N', 'o', 'i', '\0'};
// Cach 4: Khai bao mang voi kich thuoc lon hon
char buffer[100] = "Hello"; // 5 ky tu + '\0', 94 byte con lai = 0
The memory picture of a string
char name[] = "Hello";
Index: [0] [1] [2] [3] [4] [5]
βββββββ¬ββββββ¬ββββββ¬ββββββ¬ββββββ¬ββββββ
β 'H' β 'e' β 'l' β 'l' β 'o' β '\0'β
β 72 β 101 β 108 β 108 β 111 β 0 β β ASCII value
βββββββ΄ββββββ΄ββββββ΄ββββββ΄ββββββ΄ββββββ
β²
Null terminator
(marks the end of the string)
β οΈ Common mistakes:
-
Forgetting the null terminator:
char s[5] = {'H','e','l','l','o'};β there is no'\0', so the string functions will read straight past the end of the array. - Buffer overflow: copying a long string into a short array without checking the size.
4. The <string.h> library β string handling functions
<string.h> provides the basic functions for working with strings and raw memory.
Here is a quick reference:
| Function | What it does | Watch out for |
|---|---|---|
strlen(s) |
Returns the string length (excluding '\0') |
O(n) β it has to walk the whole string |
strcpy(dest, src) |
Copies src into dest | β οΈ Does not check the size of dest! |
strncpy(dest, src, n) |
Copies at most n characters | Does not add '\0' automatically if src >= n |
strcat(dest, src) |
Appends src to the end of dest | β οΈ dest must have enough room |
strncat(dest, src, n) |
Appends at most n characters | Always adds '\0' after appending |
strcmp(s1, s2) |
Compares two strings (returns 0 if equal) | Case sensitive |
strncmp(s1, s2, n) |
Compares at most the first n characters | |
strchr(s, c) |
Finds the first occurrence of c in s | Returns a pointer or NULL |
strstr(haystack, needle) |
Finds the substring needle inside haystack | Returns a pointer or NULL |
strtok(s, delim) |
Splits a string into tokens | β οΈ Modifies the original string! Not thread-safe |
memcpy(dest, src, n) |
Copies n bytes from src to dest | Unsafe if the regions overlap |
memmove(dest, src, n) |
Copies n bytes (safe when they overlap) | Slightly slower than memcpy |
memset(s, c, n) |
Sets the first n bytes of s to the value c | Commonly used to zero an array |
A security warning
gets() removed from C11?gets() reads an entire input line
without checking the size of the buffer, producing a severe
buffer overflow vulnerability. It is one of the most exploited security holes in
the history of software. Use fgets() instead, always.
Example: parsing a CSV line with strtok
#include <stdio.h>
#include <string.h>
int main() {
char csv_line[] = "Nguyen Van A,25,Ha Noi,Developer";
char *token;
int field = 0;
const char *labels[] = {"Ten", "Tuoi", "Thanh pho", "Nghe nghiep"};
// Lan goi dau tien: truyen chuoi goc
token = strtok(csv_line, ",");
while (token != NULL) {
printf("%s: %s\n", labels[field], token);
field++;
// Cac lan goi tiep theo: truyen NULL de tiep tuc tach
token = strtok(NULL, ",");
}
return 0;
}
// Ket qua:
// Ten: Nguyen Van A
// Tuoi: 25
// Thanh pho: Ha Noi
// Nghe nghiep: Developer
The thing most worth remembering here is that strtok
modifies the original string in place: it replaces each delimiter with
'\0' and returns a pointer to each piece. So never call it on a string literal, and if
you still need the original text, copy it first.
5. Character arrays and safe string input
Comparing the ways of reading a string
| Method | Reads spaces? | Checks the buffer? | Safe? |
|---|---|---|---|
gets(buf) |
Yes | NO | β Removed in C11 |
scanf("%s", buf) |
No (stops at whitespace) | NO (by default) | β οΈ Dangerous |
fgets(buf, size, stdin) |
Yes | YES (the size parameter) | β Recommended |
A safe input pattern with fgets
#include <stdio.h>
#include <string.h>
int main() {
char name[50];
printf("Nhap ho ten cua ban: ");
// fgets doc toi da 49 ky tu + '\0', bao gom ca dau cach
if (fgets(name, sizeof(name), stdin) != NULL) {
// Xoa ky tu xuong dong '\n' o cuoi (fgets giu lai no)
size_t len = strlen(name);
if (len > 0 && name[len - 1] == '\n') {
name[len - 1] = '\0';
}
printf("Xin chao, %s!\n", name);
}
// Mau an toan: fgets + sscanf de doc so
char line[100];
int age;
printf("Nhap tuoi: ");
if (fgets(line, sizeof(line), stdin) != NULL) {
if (sscanf(line, "%d", &age) == 1) {
printf("Tuoi cua ban la: %d\n", age);
} else {
printf("Du lieu nhap khong hop le!\n");
}
}
return 0;
}
Demonstrating the buffer overflow hole
#include <stdio.h>
#include <string.h>
int main() {
char password[8] = "secret";
char input[8];
printf("Nhap mat khau: ");
// NGUY HIEM: gets() khong kiem tra kich thuoc
// Neu nguoi dung nhap hon 7 ky tu, se tran sang bien password!
// gets(input); // KHONG BAO GIO DUNG HAM NAY!
// AN TOAN: Dung fgets thay the
fgets(input, sizeof(input), stdin);
size_t len = strlen(input);
if (len > 0 && input[len-1] == '\n') input[len-1] = '\0';
if (strcmp(input, password) == 0) {
printf("Truy cap thanh cong!\n");
} else {
printf("Sai mat khau!\n");
}
return 0;
}
This program may well run "normally" on your machine β and that is precisely what makes it dangerous. The overflowing data has already overwritten neighbouring memory; it simply has not caused visible damage yet. This is why buffer overflow bugs often sit quietly for years before somebody works out how to exploit them.
6. Advanced string techniques
Reversing a string in place (the two-pointer technique)
#include <stdio.h>
#include <string.h>
void reverseString(char *str) {
int left = 0;
int right = strlen(str) - 1;
while (left < right) {
// Hoan doi 2 ky tu o 2 dau
char temp = str[left];
str[left] = str[right];
str[right] = temp;
left++;
right--;
}
}
int main() {
char word[] = "Hello World";
printf("Truoc: %s\n", word);
reverseString(word);
printf("Sau: %s\n", word); // "dlroW olleH"
return 0;
}
Checking for a palindrome
#include <stdio.h>
#include <string.h>
#include <ctype.h>
int isPalindrome(const char *str) {
int left = 0;
int right = strlen(str) - 1;
while (left < right) {
// Bo qua ky tu khong phai chu/so
while (left < right && !isalnum(str[left])) left++;
while (left < right && !isalnum(str[right])) right--;
// So sanh khong phan biet hoa thuong
if (tolower(str[left]) != tolower(str[right])) {
return 0; // Khong phai palindrome
}
left++;
right--;
}
return 1; // La palindrome
}
int main() {
printf("'racecar': %s\n", isPalindrome("racecar") ? "Palindrome" : "Khong");
printf("'hello': %s\n", isPalindrome("hello") ? "Palindrome" : "Khong");
printf("'A man a plan a canal Panama': %s\n",
isPalindrome("A man a plan a canal Panama") ? "Palindrome" : "Khong");
return 0;
}
Implementing atoi yourself (string to integer)
#include <stdio.h>
#include <ctype.h>
int myAtoi(const char *str) {
int result = 0;
int sign = 1;
int i = 0;
// Bo qua khoang trang o dau
while (isspace(str[i])) i++;
// Xu ly dau + hoac -
if (str[i] == '+' || str[i] == '-') {
sign = (str[i] == '-') ? -1 : 1;
i++;
}
// Chuyen tung ky tu so thanh gia tri
while (isdigit(str[i])) {
result = result * 10 + (str[i] - '0');
i++;
}
return sign * result;
}
int main() {
printf("' -42' => %d\n", myAtoi(" -42")); // -42
printf("'12345' => %d\n", myAtoi("12345")); // 12345
printf("'+100' => %d\n", myAtoi("+100")); // 100
printf("'abc' => %d\n", myAtoi("abc")); // 0
return 0;
}
Character handling with <ctype.h>
<ctype.h> provides utility functions for testing and converting characters:
| Function | What it does | Example |
|---|---|---|
toupper(c) |
Converts to upper case | toupper('a') β 'A' |
tolower(c) |
Converts to lower case | tolower('Z') β 'z' |
isdigit(c) |
Is it a digit (0-9)? | isdigit('5') β true |
isalpha(c) |
Is it a letter? | isalpha('x') β true |
isalnum(c) |
Is it a letter or a digit? | isalnum('@') β false |
isspace(c) |
Is it whitespace? | isspace(' ') β true |
π₯ Download the sample source: arrays_strings.c
A small challenge for you
Write a program that counts how many times each character appears (case-insensitively) in a string
typed at the keyboard, using a counting array int count[26] = {0};.
char *str = "Hello"; str[0] = 'h';
Comments