Hướng dẫn  Nhập Môn Lập Trình C - Phần 4

BinhHT
NHẬP MÔN LẬP TRÌNH C

c.png

CHƯƠNG 4: DỮ LIỆU MẢNG VÀ KỸ THUẬT XỬ LÝ CƠ BẢN

I. GIỚI THIỆU CHUNG

Mảng là kiểu dữ liệu có cấu trúc, dùng để chứa tập các phần tử cùng kiểu. Trong lập trình, kiểu mảng thường xuyên được sử dụng do dễ cài đặt, dễ dùng và đáp ứng nhu cầu lưu trữ dữ liệu trong các bài toán thực tế. Những ví dụ về dữ liệu có thể lưu thành mảng khi lập trình như: dãy 12 số nguyên dương dùng để biểu diễn số ngày trong mỗi tháng của năm; dãy 50 số thực biểu diễn điểm trung bình của 50 sinh viên trong lớp, dãy 200 chuỗi ký tự, trong đó mỗi chuỗi biểu thị tên của một người trong một công ty nào đó.

Trong ngôn ngữ C/C++, kiểu mảng được hỗ trợ dưới dạng mảng một chiều cho đến mảng nhiều chiều. Thực tế ta chỉ thường sử dụng mảng có tối đa là ba chiều và mảng một chiều được dùng nhiều nhất.

II. MẢNG MỘT CHIỀU

Mảng một chiều là dãy của nhiều phần tử giống nhau. Một cuộn phim chụp ảnh có 36 kiểu chính là mảng một chiều gồm 36 phần tử, là những tấm phim có cùng kích thước. Thao tác trên mảng một chiều (cũng như mảng nhiều chiều), với ngôn ngữ C/C++, rất linh động. Tuy nhiên, ở đây mình chỉ khảo sát các đặc tính cơ bản của kiểu dữ liệu này và ta sẽ dễ nhận thấy nét tương đồng trong một số ngôn ngữ lập trình cấp cao khác như Java, C#, Python...

II.1 Tạo mảng một chiều tĩnh

Một mảng được coi là mảng tĩnh nếu phần tử của nó là cố định khi mảng được khai báo, nghĩa là nếu chúng ta khai báo cho mảng 50 phần tử, thì mảng này chỉ lưu tối đa được 50 phần tử. Ta không thể cho mảng này lưu 60 hay 80 phần tử do mảng chỉ được khai báo 50 phần tử.

Để tạo ra một mảng tĩnh, chúng ta sẽ sử dụng câu lệnh khai báo với ba thông tin cần thiết sau:

  • Kiểu của mỗi phần tử được lưu trữ trong mảng.
  • Tên của (biến) mảng.
  • Số lượng các phần tử (hay kích thước) của mảng.
Câu lệnh khai báo sẽ có dạng như sau:
Kiểu_dữ_liệu Tên_mảng[Kích_thước];
Trong đó, Kiểu_dữ_liệu là mô tả kiểu của mỗi phần tử thuộc mảng, như là int, char, double, ... hay kiểu con trỏ. Tên_mảng mô tả tên biến mảng đang định nghĩa. Qui tắc đặt tên cho mảng tương tự như đặt tên biến. Kích_thước chỉ ra số lượng tối đa mà mảng có thể lưu trữ, giá trị này phải là một số nguyên dương.

Chú ý: Cặp dấu '[' và ']' hay dấu chấm phẩy ';' là cú pháp bắt buộc của câu lệnh.
Chẳng hạn dòng lệnh dưới đây khai báo mảng một chiều có tên là thang, gồm 12 phần tử là những số nguyên.

int thang[12];
Thực tế, đôi khi chúng ta không chắc sẽ sử dụng toàn bộ kích thước đã khai báo của mảng hay chỉ một phần trong số đó. Thậm chí, lượng bộ nhớ yêu cầu này cũng thay đổi cho mỗi lần thực thi. Bên cạnh đó, vì là mảng khai báo tĩnh nên tại thời diểm viết chương trình, chúng ta vẫn phải dự đoán một kích thước đủ lớn để có thể đáp ứng được nhu cầu. Vấn đề có thể giản lược bằng phát biểu sau:

"Hãy tạo mảng một chiều có n phần tử là những số nguyên"
Trong trường hợp này, người dùng sẽ nhập giá trị của n mỗi khi thực thi chương trình và hiện tại, giải pháp này được đề nghị như trong đoạn code sau. Hàm arrayInput( ) nhận vào số phần tử n của mảng a[] gồm tối đa N=50 phần tử bằng cách sử dụng vòng lặp từ while đến dấu ngoặc } sau break. Từ vòng lặp for đến hết là nhằm nhập vào từng phần tử của mảng a[].
C:
#include <stdio.h>
#define N 50 // Có thể thay thế bởi lệnh: const int N=50;

void arrayInput(int a[N], int& N)
{
    while(1) {
        printf("So phan tu can su dung (<= %d): ", N);
        scanf("%d",&n);
        if((n < 0) || (n > N))
            printf("Nhap sai, moi ban nhap lai...\n");
        else
            break;
    }
    for(int i = 0; i <n; i++) {
        printf("a[%d] =", i);
        scanf("%d",&a[i]);
    }
}

Về bản chất, câu lệnh define thực hiện một khai báo macro và khi biên dịch chương trình, trình biên dịch sẽ thay thế mọi sự xuất hiện của N bằng số 50. Chúng ta có thể thay thế dòng lệnh #define N 50 bằng lệnh const int N = 50;

II.2 Sử dụng mảng một chiều

Để sử dụng mỗi phần tử của mảng, chúng ta cần đến cái gọi là chỉ mục (index hay subscript). Với C/C++, giá trị của chỉ mục luôn là số nguyên thuộc về đoạn [0, Kích_thước - 1]. Như vậy, với biến mảng thang thì thang[0] sẽ là phần tử đầu tiên, còn thang[11] là phần tử cuối cùng. Lưu ý rằng mảng bắt đầu chỉ số là 0 và đến kích_thước - 1 chứ không phải bắt đầu là 1.

Chỉ mục không chỉ là hằng số mà nó còn có thể là một biểu thức số học. Tất nhiên kết quả của biểu thức này phải là một giá trị nguyên không âm. Những dòng lệnh bên dưới là hoàn toàn hợp lệ.
C:
int a[10], x, y;
x = 2;
y = 3;
a[(x + y) / 2 + 1] = 5 // a[3] = 5;
a[a[3] * 2 - 1] = 100; // a[9] = 100;
a[0] = a[1] = a[2] = a[9] / a[3];
Ngôn ngữ C/C++ không kiểm tra giới hạn của mảng trong thời gian biên dịch lẫn thực thi chương trình. Nói một cách khác, việc sử dụng chỉ số mảng vượt quá kích thước khai báo (ví dụ thang[12], thang[20], ... ) sẽ không khiến cho hệ thống đưa ra bất cứ cảnh báo lỗi nào và đây là một trong những nguyên nhân khiến chương trình hoạt động không ổn định hoặc thậm chí làm dừng chương trình. Để đảm bảo tính an toàn, trong mọi trường hợp, người lập trình phải hết sức chú ý đến chi tiết này trong khi làm việc với mảng.

Ngoài ra, trong C/C++, để sao chép dữ liệu từ mảng b sang mảng a, chúng ta không được phép gán trực tiếp hai mảng này bằng câu lệnh sau:
C:
#define N 50
...
int a[N], b[N];
a = b; // Lệnh sai, không được phép
Nguyên nhân ở đây chính là việc ngôn ngữ C/C++ xem tên biến mảng như là những biến con trỏ hằng. Nghĩa là biến a (hay biến b) luôn luôn chứa địa chỉ của phần tử đầu tiên của mảng &a[0] (hay &b[0]). Câu lệnh gán trên sẽ khiến cho giá trị của biến a bị thay đổi và vi phạm tính chất "hằng" của biến và tất nhiên, trình biên dịch sẽ báo lỗi trong trường hợp này.

Đoạn chương trình sau, lần lượt chép dữ liệu từng phần tử của mảng b sang mảng a sẽ được thực hiện yêu cầu gán mảng được đề ra ban đầu:
C:
for(int i = 0; i < N; i++)
    a[i] = b[i];

II.3 Nhập xuất mảng một chiều

II.3.1 Tính liên tục của vùng nhớ.


Trong C/C++, tất cả các kiểu mảng (một hay nhiều chiều) đều yêu cầu khối vùng nhớ liên tục (xem hình 7.1). Tính liên tục của vùng nhớ khiến cho kiểu dữ liệu mảng trở nên phổ biến và dễ sử dụng. Chương trình sẽ tận dụng được sức mạnh của vòng lặp for trong các xử lý liên quan đến kiểu mảng.

array.PNG

II.3.2 Nhập dữ liệu

Thông thường, khi một yêu cầu về nhập dữ liệu được đưa ra thì nghĩa là toàn bộ các phần tử của mảng sẽ được nhận giá trị mới. Tuy nhiên, chúng ta vẫn có thể nhập liệu một cách đơn lẻ. Bảng 7.2 là ví dụ về cách nhập giá trị cho mảng số nguyên có ba phần tử. Hai đoạn chương trình gần như tương đương nhau về số dòng và đều thỏa mãn yêu cầu đặt ra. Tuy nhiên, cách viết như trường hợp (b) mới thật sự mang tính lập trình có cấu trúc. Ưu điểm của nó sẽ thể hiện rõ khi số lượng phần tử đầu vào nhiều hơn hay số lượng này là một giá trị thay đổi cho mỗi lần thực thi chương trình. Chính sự liền kề của các đơn vị nhớ dành cho mỗi phần tử trong mảng giúp cho chương trình tận dụng được ưu thế của vòng lặp for.

Như vậy các dòng code của hàm viết ở đoạn code trên có nhiệm vụ nhập dữ liệu cho n phần tử (n <= N=50) là a[0], a[1], ..., a[n-1] của biến mảng a.
C:
// Trường hợp a
...
int a[3];
scanf("%d",&a[0]);
scanf("%d",&a[1]);
scanf("%d",&a[2]);
...
C:
// Trường hợp b
...
int a[3], i;
for(i = 0; i < 3; i++)
    scanf("%d",&a[i]);
...

II.3.3 Xuất dữ liệu

Tương ứng với dữ liệu đã được nhập vào ở đoạn code trước, đoạn code bên dưới trình bày mã nguồn xuất dữ liệu ra thiết bị xuất chuẩn (mặc định là màn hình). Đoạn code bên dưới là mã nguồn hàm xuất mảng a gồm n phần tử, với n <= N=50.
C:
#include <stdio.h>
#define N 50 // Có thể thay thế bởi lệnh: const int N = 50;

void arrayIntOutput(int a[N], int n)
{
    for(int i = 0; i < n; i++) {
        printf("%d", a[i]);
    }
}

II.4 Khởi gán mảng một chiều

Trước hết, cần phải chỉ rõ ràng việ khởi gán giá trị cho biến luôn được thực hiện tại dòng khai báo biến. Đối với mảng một chiều, có một số phương thức khởi gán như sau:

int a[5] = {1, 3, 5, 7, 9};

Trong trường hợp này, tất cả năm phần tử của mảng đều được nhận giá trị a[0] = 1, a[1] = 3, a[2] = 5, a[3] = 7 và a[4] = 9.

Đôi khi chúng ta không liệt kê đầy đủ các giá trị khởi gán, ví dụ như:

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

Lúc này, chỉ có ba phần tử đầu tiên nhận giá trị lần lượt là 1, 3 và 5. Những phần tử a[3] và a[4] còn lại sẽ tự động được khởi gán = 0. Lợi dụng tính chất này ta sẽ gán giá trị 0 cho mọi phần tử của mảng bằng câu lệnh đơn giản sau:
int a[5] = {0};
Đặc biệt, C/C++ cho phép khởi gán mà không cần chỉ rõ kích thước của mảng:
int a[] = {1, 3, 5};
Dựa vào số lượng các giá trị khởi gán mà trình biên dịch sẽ xác định chính xác kích thước mảng. Với khai báo trên, mảng a sẽ gồm ba phần tử. Để tăng tính linh động trong sử dụng, kỹ thuật sử dụng trong đoạn mã sau cho phép xác định kích thước của mảng:
C:
int a[] = {1, 3, 5}, n;
n = sizeof(a) / sizeof(a[0]);
for (int i = 0; i < n; i++)
        printf("%d", a[i]);
Toán tử sizeof cho ra kích thước của kiểu dữ liệu hoặc tên biến đi kèm và lệnh for theo sau sẽ hoàn toàn tương thích trong mọi trường hợp, khi người dùng thay đổi danh sách các giá trị khởi gán.

II.5 Truyền mảng một chiều cho hàm

Về bản chất, khi thực hiện truyền mảng a có N phần tử cho hàm con nào đó thì không phải tất cả các phần tử đều được truyền đi. Thay vào đó, chỉ duy nhất một thông tin được truyền, đó là địa chỉ bắt đầu của mảng hay địa chỉ của phần tử đầu tiên a[0]. Đây cũng chính là giá trị cảu bản thân biến con trỏ hằng a.

Các hàm arrayIntInput( )arrayIntOutput( ) trong đoạn code bên dưới có mảng một chiều a là đối số hình thức. Lời gọi hai hàm này được thực hiện tại các dòng mã nguồn arrayIntInput(b, m); arrayIntOutput(b, m);. Chú ý là khi biên dịch, trình biên dịch sẽ tự động loại bỏ hằng N ra khỏi khai báo đối số mảng kích thước N. Vì vậy đối số mảng của hàm ở dòng void arrayIntInput(int a[ ], int& ) và dòng void arrayIntOutput(int a[ ], int n) không cần ghi kích thước N.
C:
#include <stdio.h>
#define N 50 // Có thể thay thế bởi lệnh: const int N= 50;
void arrayIntInput(int a[], int& n)
{
    while(1) {
        printf("So phan tu can su dung (<= %d):", N);
        scanf("%d", &n);
        if ((n < 0) || (n > N))
            printf("Nhap sai, moi ban nhap lai...\n");
        else
            break;
    }
    for(int i = 0; i < n; i++) {
        printf("a[%i] =");
        scanf("%d", &a[i]);
    }
}
void arrayIntOutput(int a[], int n)
{
    for(int i = 0; i < n; i++) {
        printf("%d", a[i]);
    }
}
int main()
{
    int b[N], m;
    arrayIntInput(b, m);
    arrayIntOutput(b, m);
    return 0;
}
Việc truyền biến mảng một chiều là truyền giá trị địa chỉ vùng nhớ nên mọi thay đổi giá trị của các phần tử trong mảng khi gọi hàm đều cập nhật giá trị vào mảng gốc trước khi gọi hàm.

Mình đã trình bày xong phần cuối cùng của series này, còn rất nhiều nội dung nữa nhưng mình nghĩ vì đây là hướng dẫn nhập môn căn bản nên những cái nâng cao mình sẽ bỏ qua. Toàn bộ kiến thức hay những đoạn code trên được mình tóm lược và rút gọn vắn tắt từ sách Nhập Môn Lập Trình của ĐH KHTN. Series Nhập Môn Lập Trình C đến đây cũng đã kết thúc. Cám ơn các bạn đã đọc, chúc các bạn vui vẻ. Nếu có thắc mắc hay yêu cầu về bản quyền thì hãy liên hệ với mình qua Gmail: [email protected], nếu dính đến bản quyền mình sẽ gỡ toàn bộ series này xuống. Cám ơn mọi người
 
Sửa lần cuối:
Trả lời

keongot97

Gà con
Cảm ơn bạn