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.

array_basics.c
// 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.

bounds_danger.c
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:

  • arr is equivalent to &arr[0] when passed to a function.
  • The receiving function cannot know the array's size β€” you have to pass a separate size parameter.
  • 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

find_max_min.c
#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.

2d_array.c
// 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

transpose.c
#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).

jagged.c
#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

string_types.c
// 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

⚠️ Why was 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

parse_csv.c
#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

safe_input.c
#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

buffer_overflow_demo.c
#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)

reverse_string.c
#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

palindrome.c
#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)

my_atoi.c
#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};.

πŸ“ Check your understanding β€” Lesson 6
What is wrong with the following code? char *str = "Hello"; str[0] = 'h';

Related lessons in this series

Lesson 7: Struct, union & the typedef keyword Lesson 5: Functions, recursion & variable scope in C Back to the C series roadmap (Vietnamese)