Week 2 Mảng

Chào mừng các bạn!

  • Trong buổi học trước, chúng ta đã tìm hiểu về C, một ngôn ngữ lập trình dạng văn bản.

  • Tuần này, chúng ta sẽ xem xét kỹ hơn về các thành phần nền tảng bổ sung nhằm hỗ trợ mục tiêu tìm hiểu về lập trình từ cấp độ thấp nhất lên.

  • Về cơ bản, bên cạnh những kiến thức thiết yếu về lập trình, khóa học này còn xoay quanh việc giải quyết vấn đề. Do đó, chúng ta cũng sẽ tập trung sâu hơn vào cách tiếp cận các vấn đề trong khoa học máy tính.

  • Đến cuối khóa học, các bạn sẽ học được cách sử dụng những thành phần nền tảng đã đề cập ở trên để giải quyết rất nhiều vấn đề khác nhau trong khoa học máy tính.

Cấp độ Đọc

  • Một trong những vấn đề thực tế mà chúng ta sẽ giải quyết trong khóa học này là tìm hiểu về cấp độ đọc.

  • Với sự giúp đỡ của một số bạn học, chúng tôi đã trình bày các bài đọc ở nhiều cấp độ đọc khác nhau.

  • Chúng ta sẽ định lượng các cấp độ đọc trong tuần này như một trong số nhiều thử thách lập trình dành cho các bạn.

Biên dịch

Mã hóa (Encryption) là hành động ẩn văn bản gốc khỏi những con mắt tò mò. Giải mã (Decrypting), khi đó, là hành động lấy một đoạn văn bản đã mã hóa và đưa nó trở lại dạng mà con người có thể đọc được.

Một đoạn văn bản đã mã hóa có thể trông như sau:

U  I  J  T   J  T   D  T  5  0
  • Hãy nhớ lại tuần trước, các bạn đã tìm hiểu về trình biên dịch (compiler), một chương trình máy tính chuyên dụng giúp chuyển đổi mã nguồn (source code) thành mã máy (machine code) để máy tính có thể hiểu được.

Ví dụ, bạn có thể có một chương trình máy tính trông như thế này:

#include

int main(void)
{
    printf("hello, world\n");
}

Một trình biên dịch sẽ lấy đoạn mã trên và chuyển nó thành mã máy như sau:

VS Code, môi trường lập trình được cung cấp cho bạn với tư cách là sinh viên CS50, sử dụng một trình biên dịch có tên là clang hoặc ngôn ngữ C.

  • Bạn có thể nhập lệnh sau vào cửa sổ terminal để biên dịch mã của mình: clang -o hello hello.c.

Các đối số dòng lệnh (Command-line arguments) được cung cấp tại dòng lệnh cho clang dưới dạng -o hello hello.c.

  • Chạy ./hello trong cửa sổ terminal, chương trình của bạn sẽ hoạt động như mong đợi.

Hãy xem xét đoạn mã sau từ tuần trước:

#include
#include

int main(void)
{
    string name = get_string("What's your name? ");
    printf("hello, %s\n", name);
}
  • Để biên dịch mã này, bạn có thể nhập clang -o hello hello.c -lcs50.

  • Nếu bạn nhập make hello, nó sẽ chạy một lệnh thực thi clang để tạo ra tệp đầu ra mà bạn có thể chạy với tư cách là người dùng.

  • VS Code đã được lập trình sẵn để make sẽ chạy nhiều đối số dòng lệnh cùng với clang nhằm tạo sự thuận tiện cho bạn.

  • Mặc dù phần trên được đưa ra như một minh họa để bạn hiểu sâu hơn về quy trình và khái niệm biên dịch mã, việc sử dụng make trong CS50 là hoàn toàn ổn và đúng như kỳ vọng!

  • Quá trình biên dịch bao gồm bốn bước chính, bao gồm:

Đầu tiên, tiền xử lý (preprocessing) là nơi các tệp tiêu đề (header files) trong mã của bạn, được chỉ định bởi dấu # (chẳng hạn như #include <cs50.h>), được sao chép và dán vào tệp của bạn một cách hiệu quả. Trong bước này, mã từ cs50.h được sao chép vào chương trình của bạn. Tương tự, giống như mã của bạn chứa #include <stdio.h>, mã nằm trong stdio.h ở đâu đó trên máy tính của bạn sẽ được sao chép vào chương trình. Bước này có thể được hình dung như sau:

  string get_string(string prompt);
  int printf(string format, ...);

  int main(void)
  {
      string name = get_string("What's your name? ");
      printf("hello, %s\n", name);
  }

Thứ hai, biên dịch (compiling) là nơi chương trình của bạn được chuyển đổi thành hợp ngữ (assembly code). Bước này có thể được hình dung như sau:

...
main:
    .cfi_startproc
# BB#0:
    pushq    %rbp
.Ltmp0:
    .cfi_def_cfa_offset 16
.Ltmp1:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
.Ltmp2:
    .cfi_def_cfa_register %rbp
    subq    $16, %rsp
    xorl    %eax, %eax
    movl    %eax, %edi
    movabsq    $.L.str, %rsi
    movb    $0, %al
    callq    get_string
    movabsq    $.L.str.1, %rdi
    movq    %rax, -8(%rbp)
    movq    -8(%rbp), %rsi
    movb    $0, %al
    callq    printf
    ...

Thứ ba, hợp dịch (assembling) bao gồm việc trình biên dịch chuyển đổi hợp ngữ của bạn thành mã máy. Bước này có thể được hình dung như sau:

01111111010001010100110001000110
00000010000000010000000100000000
00000000000000000000000000000000
00000000000000000000000000000000
00000001000000000011111000000000
00000001000000000000000000000000
00000000000000000000000000000000
00000000000000000000000000000000
00000000000000000000000000000000
00000000000000000000000000000000
10100000000000100000000000000000
00000000000000000000000000000000
00000000000000000000000000000000
01000000000000000000000000000000
00000000000000000100000000000000
00001010000000000000000100000000
01010101010010001000100111100101
01001000100000111110110000010000
00110001110000001000100111000111
01001000101111100000000000000000
00000000000000000000000000000000
00000000000000001011000000000000
11101000000000000000000000000000
00000000010010001011111100000000
00000000000000000000000000000000
00000000000000000000000001001000
...

Cuối cùng, trong bước liên kết (linking), mã từ các thư viện bạn đã sử dụng cũng được chuyển đổi thành mã máy và kết hợp với mã của bạn. Tệp thực thi cuối cùng sau đó sẽ được tạo ra.

Gỡ lỗi

  • Mọi người đều sẽ mắc lỗi khi lập trình.

Gỡ lỗi (Debugging) là quá trình tìm kiếm và loại bỏ các lỗi (bug) khỏi mã của bạn.

  • Một trong những kỹ thuật gỡ lỗi mà bạn sẽ sử dụng trong khóa học này là gỡ lỗi kiểu vịt cao su (rubber duck debugging), nơi bạn có thể trò chuyện với một vật vô tri (hoặc với chính mình) để giúp suy nghĩ thấu đáo về mã của mình và lý do tại sao nó không hoạt động như mong đợi. Khi gặp khó khăn với mã, hãy thử nói to vấn đề đó với một chú vịt cao su theo đúng nghĩa đen. Nếu bạn không muốn nói chuyện với một chú vịt nhựa nhỏ, bạn có thể nói chuyện với một người nào đó ở gần bạn!

  • Chúng tôi đã tạo ra CS50 Duck và CS50.ai làm các công cụ có thể giúp bạn gỡ lỗi mã của mình.

Hãy xem xét hình ảnh từ tuần trước:

Hãy xem xét đoạn mã sau đây có chứa một lỗi được cố tình đưa vào:

// Buggy example for printf

#include

int main(void)
{
    for (int i = 0; i  3; i++)
    {
        printf("#\n");
    }
}

Lưu ý rằng mã này in ra bốn khối thay vì ba.

  • Nhập code buggy0.c vào cửa sổ terminal và viết đoạn mã trên.

  • Khi chạy mã này, bốn viên gạch xuất hiện thay vì ba viên như dự định.

printf là một cách rất hữu ích để gỡ lỗi mã của bạn. Bạn có thể sửa đổi mã của mình như sau:

// Buggy example for printf

#include

int main(void)
{
    for (int i = 0; i  3; i++)
    {
        printf("i is %i\n", i);
        printf("#\n");
    }
}

Hãy chú ý cách mã này xuất giá trị của i trong mỗi lần lặp của vòng lặp để chúng ta có thể gỡ lỗi mã của mình.

Chạy mã này, bạn sẽ thấy nhiều câu lệnh, bao gồm i is 0, i is 1, i is 2, và i is 3. Thấy điều này, bạn có thể nhận ra rằng cần phải sửa mã thêm như sau:

#include

int main(void)
{
    for (int i = 0; i  3; i++)
    {
        printf("#\n");
    }
}

Lưu ý rằng <= đã được thay thế bằng <`.

Đoạn mã này có thể được cải thiện thêm như sau:

// Buggy example for debug50

#include
#include

void print_column(int height);

int main(void)
{
    int h = get_int("Height: ");
    print_column(h);
}

void print_column(int height)
{
    for (int i = 0; i  height; i++)
    {
        printf("#\n");
    }
}

Lưu ý rằng việc biên dịch và chạy mã này vẫn dẫn đến lỗi.

  • Để giải quyết lỗi này, chúng ta sẽ sử dụng một công cụ mới.

  • Công cụ thứ hai trong việc gỡ lỗi được gọi là trình gỡ lỗi (debugger), một công cụ phần mềm được các lập trình viên tạo ra để giúp truy tìm lỗi trong mã.

  • Trong VS Code, một trình gỡ lỗi đã được cấu hình sẵn cho bạn.

Để sử dụng trình gỡ lỗi này, trước tiên hãy thiết lập một điểm dừng (breakpoint) bằng cách nhấp vào bên trái một dòng mã, ngay bên trái của số dòng. Khi nhấp vào đó, bạn sẽ thấy một chấm đỏ xuất hiện. Hãy coi đây như một biển báo dừng, yêu cầu trình biên dịch tạm dừng để bạn có thể xem xét những gì đang xảy ra trong phần mã này.

  • Thứ hai, chạy debug50 ./buggy0. Bạn sẽ nhận thấy sau khi trình gỡ lỗi hoạt động, một dòng mã của bạn sẽ sáng lên với màu vàng. Theo nghĩa đen, mã đã tạm dừng tại dòng đó. Hãy lưu ý ở góc trên bên trái cách tất cả các biến cục bộ được hiển thị, bao gồm h, hiện chưa có giá trị. Ở trên cùng của cửa sổ, bạn có thể nhấp vào nút step over, và nó sẽ tiếp tục di chuyển qua mã của bạn. Hãy chú ý cách giá trị của h tăng lên.

  • Mặc dù công cụ này sẽ không chỉ trực tiếp lỗi của bạn ở đâu, nhưng nó sẽ giúp bạn chạy chậm lại và xem mã của mình đang thực thi từng bước như thế nào. Bạn có thể sử dụng step into như một cách để xem xét kỹ hơn các chi tiết trong đoạn mã bị lỗi của mình.

Mảng

  • Trong Tuần 0, chúng ta đã nói về các kiểu dữ liệu như bool, int, char, string, v.v.

  • Mỗi kiểu dữ liệu yêu cầu một lượng tài nguyên hệ thống nhất định:

bool 1 byte

int 4 byte

long 8 byte

float 4 byte

double 8 byte

char 1 byte

string ? byte

Bên trong máy tính, bạn có một lượng bộ nhớ hữu hạn.

Về mặt vật lý, trên bộ nhớ của máy tính, bạn có thể hình dung cách các loại dữ liệu cụ thể được lưu trữ. Bạn có thể hình dung rằng một kiểu char, vốn chỉ yêu cầu 1 byte bộ nhớ, có thể trông như sau:

Tương tự, một kiểu int, yêu cầu 4 byte, có thể trông như sau:

Chúng ta có thể tạo một chương trình để khám phá các khái niệm này. Trong terminal, hãy nhập code scores.c và viết mã như sau:

// Averages three (hardcoded) numbers

#include

int main(void)
{
    // Scores
    int score1 = 72;
    int score2 = 73;
    int score3 = 33;

    // Print average
    printf("Average: %f\n", (score1 + score2 + score3) / 3.0);
}

Lưu ý rằng số ở bên phải là một giá trị số thực dấu phẩy động 3.0, để kết quả tính toán cuối cùng được trả về dưới dạng số thực.

  • Chạy make scores, chương trình sẽ thực thi.

Bạn có thể hình dung cách các biến này được lưu trữ trong bộ nhớ:

Mảng (Arrays) là một chuỗi các giá trị được lưu trữ liên tiếp nhau trong bộ nhớ.

int scores[3] là một cách để yêu cầu trình biên dịch cung cấp cho bạn ba vị trí liên tiếp trong bộ nhớ với kích thước của kiểu int để lưu trữ ba giá trị scores. Xem xét chương trình của chúng ta, bạn có thể sửa lại mã như sau:

// Averages three (hardcoded) numbers using an array

#include
#include

int main(void)
{
    // Scores
    int scores[3];
    scores[0] = 72;
    scores[1] = 73;
    scores[2] = 33;

    // Print average
    printf("Average: %f\n", (scores[0] + scores[1] + scores[2]) / 3.0);
}

Lưu ý rằng scores[0] truy cập vào giá trị tại vị trí bộ nhớ này bằng cách truy cập chỉ số (indexing into) mảng có tên là scores tại vị trí 0 để xem giá trị nào được lưu trữ ở đó.

Bạn có thể thấy rằng, mặc dù đoạn mã trên hoạt động, vẫn còn cơ hội để cải thiện mã của chúng ta. Hãy sửa lại mã như sau:

// Averages three numbers using an array and a loop

#include
#include

int main(void)
{
    // Get scores
    int scores[3];
    for (int i = 0; i  3; i++)
    {
        scores[i] = get_int("Score: ");
    }

    // Print average
    printf("Average: %f\n", (scores[0] + scores[1] + scores[2]) / 3.0);
}

Hãy chú ý cách chúng ta truy cập chỉ số vào scores bằng cách sử dụng scores[i] với i được cung cấp bởi vòng lặp for.

Chúng ta có thể đơn giản hóa hoặc trừu tượng hóa việc tính toán giá trị trung bình. Sửa đổi mã của bạn như sau:

// Averages three numbers using an array, a constant, and a helper function

#include
#include

// Constant
const int N = 3;

// Prototype
float average(int length, int array[]);

int main(void)
{
    // Get scores
    int scores[N];
    for (int i = 0; i  N; i++)
    {
        scores[i] = get_int("Score: ");
    }

    // Print average
    printf("Average: %f\n", average(N, scores));
}

float average(int length, int array[])
{
    // Calculate average
    int sum = 0;
    for (int i = 0; i  length; i++)
    {
        sum += array[i];
    }
    return sum / (float) length;
}

Lưu ý rằng một hàm mới có tên là average đã được khai báo. Thêm vào đó, một hằng số const có giá trị N cũng được khai báo. Quan trọng nhất, hãy chú ý cách hàm average nhận int array[], nghĩa là trình biên dịch truyền một mảng vào hàm này.

  • Mảng không chỉ đóng vai trò là vật chứa: Chúng còn có thể được truyền qua lại giữa các hàm.

Chuỗi

  • Một string đơn giản là một mảng các biến có kiểu char: một mảng các ký tự.

Để khám phá charstring, hãy nhập code hi.c trong cửa sổ terminal và viết mã như sau:

// Prints chars

#include

int main(void)
{
    char c1 = 'H';
    char c2 = 'I';
    char c3 = '!';

    printf("%c%c%c\n", c1, c2, c3);
}

Lưu ý rằng lệnh này sẽ xuất ra một chuỗi các ký tự.

Tương tự, hãy thực hiện sửa đổi sau đối với mã của bạn:

#include

int main(void)
{
    char c1 = 'H';
    char c2 = 'I';
    char c3 = '!';

    printf("%i %i %i\n", c1, c2, c3);
}

Lưu ý rằng các mã ASCII sẽ được in ra bằng cách thay thế %c bằng %i.

Xem xét hình ảnh sau, bạn có thể thấy một chuỗi là một mảng các ký tự bắt đầu bằng ký tự đầu tiên và kết thúc bằng một ký tự đặc biệt gọi là ký tự NUL:

Hình dung điều này trong hệ thập phân, mảng của bạn sẽ trông như sau:

Để hiểu rõ hơn về cách một string hoạt động, hãy sửa lại mã của bạn như sau:

// Treats string as array

#include
#include

int main(void)
{
    string s = "HI!";
    printf("%c%c%c\n", s[0], s[1], s[2]);
}

Hãy chú ý cách câu lệnh printf hiển thị ba giá trị từ mảng có tên là s.

Như trước, chúng ta có thể thay thế %c bằng %i như sau:

// Prints string's ASCII codes, including NUL

#include
#include

int main(void)
{
    string s = "HI!";
    printf("%i %i %i %i\n", s[0], s[1], s[2], s[3]);
}

Lưu ý rằng lệnh này in ra các mã ASCII của chuỗi, bao gồm cả ký tự NUL.

Giả sử chúng ta muốn nói cả HI!BYE!. Hãy sửa đổi mã của bạn như sau:

// Multiple strings

#include
#include

int main(void)
{
    string s = "HI!";
    string t = "BYE!";

    printf("%s\n", s);
    printf("%s\n", t);
}

Lưu ý rằng hai chuỗi đã được khai báo và sử dụng trong ví dụ này.

Bạn có thể hình dung điều này như sau:

Chúng ta có thể cải thiện mã này hơn nữa. Sửa đổi mã của bạn như sau:

// Array of strings

#include
#include

int main(void)
{
    string words[2];

    words[0] = "HI!";
    words[1] = "BYE!";

    printf("%s\n", words[0]);
    printf("%s\n", words[1]);
}

Lưu ý rằng cả hai chuỗi đều được lưu trữ trong một mảng duy nhất có kiểu string.

Chúng ta có thể hợp nhất hai chuỗi của mình thành một mảng các chuỗi.

#include
#include

int main(void)
{
    string words[2];

    words[0] = "HI!";
    words[1] = "BYE!";

    printf("%c%c%c\n", words[0][0], words[0][1], words[0][2]);
    printf("%c%c%c%c\n", words[1][0], words[1][1], words[1][2], words[1][3]);
}

Lưu ý rằng một mảng words đã được tạo ra. Đó là một mảng các chuỗi. Mỗi từ được lưu trữ trong words.

Độ dài chuỗi

Một vấn đề phổ biến trong lập trình, và cụ thể hơn là trong C, là tìm độ dài của một mảng. Chúng ta có thể thực hiện điều này trong mã như thế nào? Nhập code length.c vào cửa sổ terminal và viết mã như sau:

// Determines the length of a string

#include
#include

int main(void)
{
    // Prompt for user's name
    string name = get_string("Name: ");

    // Count number of characters up until '\0' (aka NUL)
    int n = 0;
    while (name[n] != '\0')
    {
        n++;
    }
    printf("%i\n", n);
}

Lưu ý rằng mã này lặp cho đến khi tìm thấy ký tự NUL.

Mã này có thể được cải thiện bằng cách trừu tượng hóa việc đếm vào một hàm như sau:

// Determines the length of a string using a function

#include
#include

int string_length(string s);

int main(void)
{
    // Prompt for user's name
    string name = get_string("Name: ");
    int length = string_length(name);
    printf("%i\n", length);
}

int string_length(string s)
{
    // Count number of characters up until '\0' (aka NUL)
    int n = 0;
    while (s[n] != '\0')
    {
        n++;
    }
    return n;
}

Lưu ý rằng một hàm mới có tên là string_length sẽ đếm các ký tự cho đến khi gặp NUL.

Vì đây là một vấn đề rất phổ biến trong lập trình, các lập trình viên khác đã tạo ra mã trong thư viện string.h để tìm độ dài của một chuỗi. Bạn có thể tìm độ dài của chuỗi bằng cách sửa đổi mã của mình như sau:

// Determines the length of a string using a function

#include
#include
#include

int main(void)
{
    // Prompt for user's name
    string name = get_string("Name: ");
    int length = strlen(name);
    printf("%i\n", length);
}

Lưu ý rằng mã này sử dụng thư viện string.h, được khai báo ở đầu tệp. Thêm vào đó, nó sử dụng một hàm từ thư viện đó có tên là strlen, hàm này tính toán độ dài của chuỗi được truyền vào.

  • Mã của chúng ta có thể kế thừa thành quả của các lập trình viên đi trước và sử dụng các thư viện mà họ đã tạo ra.

ctype.h là một thư viện hữu ích khác. Giả sử chúng ta muốn tạo một chương trình chuyển đổi tất cả các ký tự thường thành ký tự hoa. Trong cửa sổ terminal, hãy nhập code uppercase.c và viết mã như sau:

// Uppercases a string

#include
#include
#include

int main(void)
{
    string s = get_string("Before: ");
    printf("After:  ");
    for (int i = 0, n = strlen(s); i  n; i++)
    {
        if (s[i] >= 'a' && s[i]  'z')
        {
            printf("%c", s[i] - 32);
        }
        else
        {
            printf("%c", s[i]);
        }
    }
    printf("\n");
}

Lưu ý rằng mã này duyệt qua từng giá trị trong chuỗi. Chương trình xem xét từng ký tự. Nếu ký tự đó là chữ thường, nó sẽ trừ đi giá trị 32 để chuyển đổi thành chữ hoa.

Nhớ lại kiến thức từ tuần trước, bạn có thể nhớ bảng giá trị ASCII này:

      0
      NUL
      16
      DLE
      32
      SP
      48
      0
      64
      @
      80
      P
      96
      `
      112
      p

      1
      SOH
      17
      DC1
      33
      !
      49
      1
      65
      A
      81
      Q
      97
      a
      113
      q

      2
      STX
      18
      DC2
      34
      ”
      50
      2
      66
      B
      82
      R
      98
      b
      114
      r

      3
      ETX
      19
      DC3
      35
      #
      51
      3
      67
      C
      83
      S
      99
      c
      115
      s

      4
      EOT
      20
      DC4
      36
      $
      52
      4
      68
      D
      84
      T
      100
      d
      116
      t

      5
      ENQ
      21
      NAK
      37
      %
      53
      5
      69
      E
      85
      U
      101
      e
      117
      u

      6
      ACK
      22
      SYN
      38
      &
      54
      6
      70
      F
      86
      V
      102
      f
      118
      v

      7
      BEL
      23
      ETB
      39
      ’
      55
      7
      71
      G
      87
      W
      103
      g
      119
      w

      8
      BS
      24
      CAN
      40
      (
      56
      8
      72
      H
      88
      X
      104
      h
      120
      x

      9
      HT
      25
      EM
      41
      )
      57
      9
      73
      I
      89
      Y
      105
      i
      121
      y

      10
      LF
      26
      SUB
      42
      *
      58
      :
      74
      J
      90
      Z
      106
      j
      122
      z

      11
      VT
      27
      ESC
      43
      +
      59
      ;
      75
      K
      91
      [
      107
      k
      123
      {

      12
      FF
      28
      FS
      44
      ,
      60
      <
      76
      L
      92
      \
      108
      l
      124

      13
      CR
      29
      GS
      45
      -
      61
      =
      77
      M
      93
      ]
      109
      m
      125
      }

      14
      SO
      30
      RS
      46
      .
      62
      >
      78
      N
      94
      ^
      110
      n
      126
      ~

      15
      SI
      31
      US
      47
      /
      63
      ?
      79
      O
      95
      _
      111
      o
      127
      DEL
  • Khi một ký tự thường được trừ đi 32, kết quả sẽ là phiên bản viết hoa của chính ký tự đó.

Mặc dù chương trình thực hiện đúng những gì chúng ta muốn, nhưng có một cách dễ dàng hơn bằng cách sử dụng thư viện ctype.h. Hãy sửa đổi chương trình của bạn như sau:

// Uppercases string using ctype library (and an unnecessary condition)

#include
#include
#include
#include

int main(void)
{
    string s = get_string("Before: ");
    printf("After:  ");
    for (int i = 0, n = strlen(s); i  n; i++)
    {
        if (islower(s[i]))
        {
            printf("%c", toupper(s[i]));
        }
        else
        {
            printf("%c", s[i]);
        }
    }
    printf("\n");
}

Lưu ý rằng chương trình duyệt qua từng ký tự của chuỗi. Hàm toupper nhận vào s[i]. Mỗi ký tự (nếu là chữ thường) sẽ được chuyển thành chữ hoa.

Đáng lưu ý là toupper tự động biết chỉ chuyển đổi chữ thường thành chữ hoa. Do đó, mã của bạn có thể được đơn giản hóa như sau:

// Uppercases string using ctype library

#include
#include
#include
#include

int main(void)
{
    string s = get_string("Before: ");
    printf("After:  ");
    for (int i = 0, n = strlen(s); i  n; i++)
    {
        printf("%c", toupper(s[i]));
    }
    printf("\n");
}

Lưu ý rằng đoạn mã này chuyển một chuỗi thành chữ hoa bằng cách sử dụng thư viện ctype.

  • Bạn có thể đọc về tất cả các tính năng của thư viện ctype trên Trang hướng dẫn.

Đối số dòng lệnh

Đối số dòng lệnh (Command-line arguments) là những đối số được truyền vào chương trình của bạn tại dòng lệnh. Ví dụ, tất cả các câu lệnh bạn đã nhập sau clang đều được coi là đối số dòng lệnh. Bạn có thể sử dụng các đối số này trong chính chương trình của mình!

Trong cửa sổ terminal, hãy nhập code greet.c và viết mã như sau:

// Uses get_string

#include
#include

int main(void)
{
    string answer = get_string("What's your name? ");
    printf("hello, %s\n", answer);
}

Lưu ý rằng chương trình này nói hello với người dùng.

Tuy nhiên, sẽ thật tuyệt nếu có thể nhận các đối số trước khi chương trình thực thi? Sửa đổi mã của bạn như sau:

// Prints a command-line argument

#include
#include

int main(int argc, string argv[])
{
    if (argc == 2)
    {
        printf("hello, %s\n", argv[1]);
    }
    else
    {
        printf("hello, world\n");
    }
}

Lưu ý rằng chương trình này biết cả argc (số lượng đối số dòng lệnh) và argv (một mảng các ký tự được truyền vào dưới dạng đối số tại dòng lệnh).

  • Vì vậy, theo cú pháp của chương trình này, việc thực thi ./greet David sẽ dẫn đến việc chương trình nói hello, David.

Bạn có thể in từng đối số dòng lệnh bằng lệnh sau:

// Prints command-line arguments

#include
#include

int main(int argc, string argv[])
{
    for (int i = 0; i  argc; i++)
    {
        printf("%s\n", argv[i]);
    }
}

Trạng thái thoát

  • Khi một chương trình kết thúc, một mã thoát đặc biệt sẽ được cung cấp cho máy tính.

  • Khi một chương trình kết thúc mà không có lỗi, mã trạng thái 0 sẽ được trả về máy tính. Thông thường, khi xảy ra lỗi khiến chương trình kết thúc, máy tính sẽ nhận được mã trạng thái là 1.

Bạn có thể viết một chương trình minh họa điều này bằng cách nhập code status.c và viết mã như sau:

// Returns explicit value from main

#include
#include

int main(int argc, string argv[])
{
    if (argc != 2)
    {
        printf("Missing command-line argument\n");
        return 1;
    }
    printf("hello, %s\n", argv[1]);
    return 0;
}

Lưu ý rằng nếu bạn không cung cấp ./status David, bạn sẽ nhận được mã trạng thái thoát là 1. Tuy nhiên, nếu bạn cung cấp ./status David, bạn sẽ nhận được mã trạng thái thoát là 0.

  • Bạn có thể nhập echo $? trong terminal để xem trạng thái thoát của lệnh vừa chạy gần nhất.

  • Bạn có thể hình dung cách mình sử dụng các phần của chương trình trên để kiểm tra xem người dùng có cung cấp đúng số lượng đối số dòng lệnh hay không.

Mật mã học

  • Mật mã học (Cryptography) là nghệ thuật mã hóa và giải mã thông điệp.

  • Bây giờ, với nền tảng là mảng, ký tự (char) và chuỗi (string), bạn có thể thực hiện mã hóa và giải mã thông điệp.

Văn bản gốc (plaintext) và một khóa (key) được cung cấp cho một mật mã (cipher), tạo ra văn bản đã mã hóa.

  • Khóa là một đối số đặc biệt được truyền vào mật mã cùng với văn bản gốc. Mật mã sử dụng khóa để đưa ra quyết định về cách triển khai thuật toán mã hóa của nó.

  • Tuần này, các bạn sẽ thực hiện các thử thách lập trình tương tự như trên.

Tổng kết

Trong bài học này, bạn đã tìm hiểu thêm chi tiết về việc biên dịch và cách dữ liệu được lưu trữ bên trong máy tính. Cụ thể, bạn đã học được…

  • Một cách tổng quát, cách một trình biên dịch hoạt động.

  • Cách gỡ lỗi mã bằng bốn phương pháp.

  • Cách sử dụng mảng trong mã của bạn.

  • Cách mảng lưu trữ dữ liệu tại các phần liên tiếp nhau của bộ nhớ.

  • Cách chuỗi đơn giản là các mảng ký tự.

  • Cách tương tác với mảng trong mã của bạn.

  • Cách các đối số dòng lệnh có thể được truyền vào chương trình của bạn.

  • Các thành phần cơ bản của mật mã học.

Hẹn gặp lại các bạn vào buổi học tới!