Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 9.217 phút đọc

Khởi tạo mảng

Sau bài này bạn sẽ làm được

  • Chọn đúng cách khởi tạo cho từng tình huống
  • Dùng khởi tạo có chỉ định để lập bảng tra
  • Sao chép mảng bằng memcpy hoặc vòng lặp
  • Hiểu vì sao mảng có độ dài thay đổi bị cấm trong khóa học này

Một mảng chưa khởi tạo chứa rác, và rác trong mảng khó phát hiện hơn rác trong biến đơn rất nhiều vì bạn thường chỉ nhìn vài phần tử đầu. Bài này đi hết mọi cách khởi tạo mảng mà C cho phép.

#Sáu cách khởi tạo

khoi-tao.c
int a[5] = { 1, 2, 3, 4, 5 };   /* 1. đầy đủ */
int b[5] = { 1, 2 };            /* 2. thiếu, phần còn lại tự bằng 0 */
int c[5] = { 0 };               /* 3. thành ngữ: cả mảng bằng 0 */
int d[]  = { 1, 2, 3 };         /* 4. tự suy kích thước, ở đây là 3 */
int e[5] = { };                 /* 5. C23, cả mảng bằng 0 */
int f[5];                       /* 6. KHÔNG khởi tạo, chứa rác */
Cách viếtNội dung mảngKhi nào dùng
{ 1, 2, 3, 4, 5 }1 2 3 4 5Biết trước toàn bộ giá trị
{ 1, 2 }1 2 0 0 0Chỉ vài phần tử đầu có giá trị riêng
{ 0 }0 0 0 0 0Muốn cả mảng bằng 0, cách phổ biến nhất
{ 1 }1 0 0 0 0Cẩn thận, chỉ phần tử đầu bằng 1
không có gìrác rác rác rác rácChỉ khi bạn sẽ ghi đè toàn bộ ngay sau đó

Vì sao mảng toàn cục thì khác

int g[5];              /* toàn cục: chắc chắn 0 0 0 0 0 */

int main(void)
{
    int l[5];          /* cục bộ: rác */

    return 0;
}

Lý do đã nói ở Bài 8.8: mảng toàn cục nằm ở vùng .bss và được hệ điều hành xóa sạch lúc nạp. Mảng cục bộ nằm trên ngăn xếp, và ngăn xếp chứa những gì lời gọi hàm trước để lại.

terminal
# Cách nhanh nhất để xóa một mảng cục bộ đã dùng
man memset
void *memset(void *s, int c, size_t n);
#include <string.h>

int a[1000];

memset(a, 0, sizeof a);      /* điền 0 vào toàn bộ mảng */

#Khởi tạo có chỉ định

Khởi tạo có chỉ định
Cú pháp của C99 cho phép ghi rõ chỉ số của phần tử được khởi tạo, thay vì phụ thuộc vào thứ tự. Mọi phần tử không được nhắc tới đều bằng 0.
int a[10] = { [0] = 1, [5] = 50, [9] = 90 };
/* kết quả: 1 0 0 0 0 50 0 0 0 90 */

Cú pháp này tỏa sáng khi lập bảng tra. So sánh hai cách viết cùng một bảng số ngày trong tháng:

Đếm vị trí bằng mắt, rất dễ lệch
/* Phần tử đầu là số 0 giả để tháng 1 rơi vào chỉ số 1.
   Ai đọc cũng phải đếm ngón tay để biết 30 nào là tháng nào. */
int ngay[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
Ghi rõ chỉ số, đọc là hiểu
int ngay[13] = {
    [1]  = 31, [2]  = 28, [3]  = 31, [4]  = 30,
    [5]  = 31, [6]  = 30, [7]  = 31, [8]  = 31,
    [9]  = 30, [10] = 31, [11] = 30, [12] = 31,
};

/* Chỉ số 0 tự bằng 0, và mỗi dòng tự nói lên tháng nào bao nhiêu ngày. */

Ba ứng dụng thường gặp

/* 1. Bảng phân loại ký tự, 256 phần tử nhưng chỉ vài chỗ khác 0 */
int la_toan_tu[256] = {
    ['+'] = 1, ['-'] = 1, ['*'] = 1, ['/'] = 1, ['%'] = 1,
};

if (la_toan_tu[(unsigned char)c])
    xu_ly_toan_tu(c);

/* 2. Bảng ánh xạ mã lỗi sang thông báo */
const char *thong_bao[8] = {
    [0] = "Thanh cong",
    [1] = "Khong mo duoc file",
    [2] = "Het bo nho",
    [7] = "Loi khong xac dinh",
};

/* 3. Bảng tra thay cho chuỗi if else if dài */
int diem_chu[128] = {
    ['A'] = 4, ['B'] = 3, ['C'] = 2, ['D'] = 1, ['F'] = 0,
};

#Khởi tạo mảng ký tự

char s1[6] = "hello";     /* {'h','e','l','l','o','\0'} cần đúng 6 ô */
char s2[]  = "hello";     /* kích thước tự suy ra là 6 */
char s3[5] = "hello";     /* hợp lệ nhưng KHÔNG có ký tự kết thúc */
char s4[10] = "hi";       /* {'h','i','\0',0,0,0,0,0,0,0} */

#Không gán mảng bằng dấu bằng

int a[5] = { 1, 2, 3, 4, 5 };
int b[5];

b = a;      /* lỗi biên dịch: assignment to expression with array type */

Lý do là cơ chế phân rã mảng ở Bài 8.7. Trong biểu thức b = a, vế phải a phân rã thành con trỏ, còn vế trái b không phải một biến có thể gán. Mảng là một vùng nhớ có tên, không phải một giá trị bạn di chuyển được.

Cách sao chépViết thế nàoGhi chú
Vòng lặpfor (i = 0; i < n; ++i) b[i] = a[i];Rõ ràng, hoạt động với mọi kiểu
memcpymemcpy(b, a, sizeof a);Nhanh nhất, hai vùng không được chồng lấn
memmovememmove(b, a, sizeof a);Chậm hơn chút, an toàn cả khi hai vùng chồng lấn
Bọc trong structtypedef struct { int v[5]; } Mang5;Struct thì gán bằng dấu bằng được
sao-chep.c
#include <string.h>

int a[5] = { 1, 2, 3, 4, 5 };
int b[5];

memcpy(b, a, sizeof a);        /* dùng sizeof a, KHÔNG phải sizeof b */

/* Mẹo: mảng bọc trong struct thì gán được */
typedef struct { int v[5]; } Mang5;

Mang5 x = { { 1, 2, 3, 4, 5 } };
Mang5 y = x;                   /* hợp lệ, chép cả 20 byte */

#Mảng có độ dài thay đổi

int n;

scanf("%d", &n);

int a[n];        /* mảng có độ dài thay đổi, tính năng của C99 */
Mảng độ dài thay đổi
int n;

if (scanf("%d", &n) != 1) return 1;

int a[n];               /* n = 10000000 là sập, không cứu được */
Cấp phát động, học ở Chương 14
int n;

if (scanf("%d", &n) != 1 || n <= 0 || n > 1000000) {
    fprintf(stderr, "n phai trong khoang 1 toi 1000000\n");
    return 1;
}

int *a = malloc((size_t)n * sizeof *a);

if (a == NULL) {
    fprintf(stderr, "Khong du bo nho\n");
    return 1;
}

/* ... dùng a ... */

free(a);

Cho tới khi học Chương 14, cách làm trong khóa này là khai báo mảng đủ lớn với một hằng số và kiểm tra dữ liệu vào không vượt quá nó.

enum { N_TOI_DA = 1000 };

int    a[N_TOI_DA];
size_t n;

if (scanf("%zu", &n) != 1 || n == 0 || n > N_TOI_DA) {
    fprintf(stderr, "So phan tu phai tu 1 toi %d\n", N_TOI_DA);
    return 1;
}

Tự làm thử

  1. In toàn bộ nội dung của int a[5] = { 1, 2 }; int b[5]; ở dạng cục bộ, so sánh kết quả.
  2. Dùng memset điền giá trị 1 vào mảng int, in ra và giải thích con số nhận được.
  3. Viết bảng tra số ngày trong tháng bằng khởi tạo có chỉ định, kèm xử lý năm nhuận.
  4. Lập bảng int la_nguyen_am[256] bằng khởi tạo có chỉ định, dùng nó để đếm nguyên âm trong một chuỗi.
  5. Thử gán b = a giữa hai mảng, chép lại thông báo lỗi, rồi sửa bằng cả ba cách trong bảng.
  6. Viết chương trình dùng mảng độ dài thay đổi với n nhập từ bàn phím, thử n bằng mười triệu và quan sát kết quả.

Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.

Tóm tắt

  • Nếu có bất kỳ giá trị khởi tạo nào, mọi phần tử không được nhắc tới đều bằng 0. Đó là lý do { 0 } xóa sạch mảng.
  • { 1 } chỉ làm phần tử đầu bằng 1, không làm cả mảng bằng 1.
  • Khởi tạo có chỉ định của C99 làm bảng tra dễ đọc và không thể đếm lệch vị trí.
  • memset điền theo byte, chỉ dùng an toàn với 0 và âm một.
  • Mảng không gán được bằng dấu bằng. Dùng vòng lặp hoặc memcpy với đối số thứ ba tính bằng byte.
  • Không dùng mảng có độ dài thay đổi. Nó nằm trên ngăn xếp và không có cách kiểm tra thất bại.