cv.cm

Lập trình · Ngôn ngữ lập trình

Hướng dẫn học lập trình C cơ bản cho người mới

8 phút đọc

Trả lời nhanh

C là ngôn ngữ biên dịch nhỏ gọn, nhanh, cho phép truy cập trực tiếp bộ nhớ thông qua con trỏ. Viết main trong tệp .c, biên dịch bằng gcc hello.c -o hello rồi chạy ./hello. Các ý tưởng cốt lõi là kiểu có kích thước cố định, hàm, mảng và chuỗi kết thúc bằng ký tự null, con trỏ và địa chỉ, quản lý bộ nhớ thủ công với malloc và free, và xử lý lỗi qua giá trị trả về.

Mở Đổi nhị phân

C là gì và dùng để làm gì

C được Dennis Ritchie tạo ra tại Bell Labs vào đầu thập niên 1970 để viết hệ điều hành Unix. Nó được ISO chuẩn hóa (C99, C11, C17 và C23 là các bản chính). C biên dịch thành mã máy với rất ít runtime, nên chạy hầu như ở mọi nơi. Nó được dùng cho nhân hệ điều hành (Linux), firmware nhúng, cơ sở dữ liệu (SQLite), runtime của các ngôn ngữ (CPython viết bằng C) và thư viện hiệu năng cao. Học C cũng giúp bạn hiểu bộ nhớ và máy tính thực sự hoạt động ra sao.

Cài trình biên dịch và chạy Hello, world

Trên Linux hãy cài gcc (ví dụ sudo apt install build-essential); trên macOS chạy xcode-select --install để có clang (cũng gọi được là gcc); trên Windows dùng MSYS2, WSL hoặc Visual Studio.

#include <stdio.h>
int main(void) {
    printf("Hello, world\n");
    return 0;
}
gcc -Wall -Wextra -std=c17 hello.c -o hello
./hello
# Hello, world

#include nạp các khai báo từ tệp header, ở đây là thư viện nhập xuất chuẩn. Trả về 0 từ main nghĩa là thành công. Luôn biên dịch với cảnh báo được bật (-Wall -Wextra).

Biến và các kiểu dữ liệu cơ bản

Mỗi biến có một kiểu quy định kích thước và ý nghĩa của nó. Kích thước phụ thuộc vào nền tảng, trừ khi chuẩn quy định khác; <stdint.h> cung cấp các kiểu có độ rộng chính xác như int32_t.

KiểuKích thước thường gặpVí dụĐịnh dạng printf
char1 bytechar c = 'A';%c
int4 byteint n = 42;%d
long long8 bytelong long big = 1LL << 40;%lld
double8 bytedouble x = 3.14;%f
bool1 bytebool ok = true; (C99 <stdbool.h>)%d
size_t8 byte trên hệ 64 bitsize_t len = sizeof(arr);%zu
#include <stdio.h>
int main(void) {
    int count = 7;
    double avg = count / 2;     /* 3.0: integer division first */
    double avg2 = count / 2.0;  /* 3.5 */
    printf("%d %.1f %.1f\n", count, avg, avg2);  /* 7 3.0 3.5 */
    printf("%zu\n", sizeof(int));                /* 4 on most systems */
    return 0;
}

Điều khiển luồng và hàm

Hàm phải được khai báo trước khi dùng. Đối số được truyền theo giá trị: hàm nhận một bản sao.

#include <stdio.h>
int square(int x) {
    return x * x;
}
int main(void) {
    int score = 72;
    if (score >= 90) {
        printf("A\n");
    } else if (score >= 70) {
        printf("B\n");              /* B */
    } else {
        printf("C\n");
    }
    for (int i = 0; i < 3; i++) {
        printf("%d ", square(i));   /* 0 1 4 */
    }
    printf("\n");
    int n = 10;
    while (n > 1) n /= 2;
    printf("%d\n", n);              /* 1 */
    return 0;
}

C còn có do/while, switch với nhãn case (nhớ break), và goto, chủ yếu dùng để dọn dẹp trong các nhánh lỗi.

Mảng và chuỗi

Mảng là một số lượng phần tử cố định nằm cạnh nhau. C không kiểm tra giới hạn và không lưu độ dài, nên bạn phải truyền độ dài riêng. Chuỗi là mảng char kết thúc bằng một byte không '\0'.

#include <stdio.h>
#include <string.h>
int sum(const int *values, size_t n) {
    int total = 0;
    for (size_t i = 0; i < n; i++) total += values[i];
    return total;
}
int main(void) {
    int nums[] = {3, 1, 2, 5};
    size_t n = sizeof nums / sizeof nums[0];
    printf("%zu items, sum %d\n", n, sum(nums, n));  /* 4 items, sum 11 */
    char name[16] = "Ada";
    strncat(name, " Lovelace", sizeof name - strlen(name) - 1);
    printf("%s (%zu chars)\n", name, strlen(name));  /* Ada Lovelace (12 chars) */
    return 0;
}

Khi truyền mảng vào hàm, nó suy biến (decay) thành con trỏ tới phần tử đầu tiên, đó là lý do sum nhận const int * và một độ dài.

Tính năng đặc trưng: con trỏ

Con trỏ giữ địa chỉ bộ nhớ của một giá trị khác. &x cho địa chỉ của x, còn *p đọc hoặc ghi giá trị tại địa chỉ p. Con trỏ cho phép hàm sửa biến của bên gọi, chia sẻ dữ liệu lớn mà không sao chép, và xây dựng các cấu trúc dữ liệu liên kết.

#include <stdio.h>
void swap(int *a, int *b) {
    int tmp = *a;
    *a = *b;
    *b = tmp;
}
int main(void) {
    int x = 1, y = 2;
    int *p = &x;
    *p = 10;                      /* changes x */
    swap(&x, &y);
    printf("x=%d y=%d\n", x, y);  /* x=2 y=10 */
    int arr[3] = {7, 8, 9};
    int *q = arr;
    printf("%d %d\n", *(q + 1), q[2]);  /* 8 9: pointer arithmetic */
    return 0;
}

Bộ nhớ động với malloc và free

Biến cục bộ nằm trên stack và biến mất khi hàm trả về. Với dữ liệu mà kích thước chỉ biết lúc chạy, hoặc phải tồn tại lâu hơn hàm, hãy cấp phát trên heap bằng malloc và giải phóng bằng free. Mỗi malloc thành công cần đúng một free.

#include <stdio.h>
#include <stdlib.h>
int main(void) {
    size_t n = 5;
    int *squares = malloc(n * sizeof *squares);
    if (squares == NULL) {
        fprintf(stderr, "out of memory\n");
        return 1;
    }
    for (size_t i = 0; i < n; i++) squares[i] = (int)(i * i);
    printf("%d\n", squares[4]);  /* 16 */
    free(squares);
    squares = NULL;              /* avoid using a dangling pointer */
    return 0;
}

Các hàm liên quan là calloc (cấp phát và xóa về 0) và realloc (đổi kích thước). Hãy nhóm các giá trị liên quan bằng struct, ví dụ struct point { int x; int y; };.

Xử lý lỗi

C không có ngoại lệ. Hàm báo lỗi qua giá trị trả về (thường là -1, NULL hoặc mã trạng thái), và nhiều hàm thư viện còn đặt biến toàn cục errno. Hãy kiểm tra mọi giá trị trả về có thể thất bại.

#include <errno.h>
#include <stdio.h>
#include <string.h>
int main(void) {
    FILE *f = fopen("missing.txt", "r");
    if (f == NULL) {
        fprintf(stderr, "cannot open: %s\n", strerror(errno));
        return 1;   /* cannot open: No such file or directory */
    }
    fclose(f);
    return 0;
}

Một chương trình nhỏ hoàn chỉnh: đếm dòng và từ

Chương trình này hoạt động như một wc thu nhỏ: nó đọc tệp được chỉ định trên dòng lệnh và in ra số dòng, số từ và số byte.

#include <ctype.h>
#include <stdio.h>
int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "usage: %s FILE\n", argv[0]);
        return 1;
    }
    FILE *f = fopen(argv[1], "r");
    if (f == NULL) {
        perror(argv[1]);
        return 1;
    }
    long lines = 0, words = 0, bytes = 0;
    int c, in_word = 0;
    while ((c = fgetc(f)) != EOF) {
        bytes++;
        if (c == '\n') lines++;
        if (isspace(c)) {
            in_word = 0;
        } else if (!in_word) {
            in_word = 1;
            words++;
        }
    }
    fclose(f);
    printf("%ld lines, %ld words, %ld bytes\n", lines, words, bytes);
    return 0;
}

Biên dịch bằng gcc -Wall -Wextra wc.c -o wc và chạy ./wc notes.txt. Lưu ý c là int, không phải char, để nó chứa được giá trị đặc biệt EOF.

Những lỗi thường gặp của người mới

  • Dùng = thay cho == trong điều kiện. Cảnh báo (-Wall) bắt được hầu hết trường hợp.
  • Quên chừa chỗ cho ký tự kết thúc '\0' khi định kích thước bộ đệm chuỗi.
  • Dùng gets hoặc scanf("%s", buf) không giới hạn. Hãy dùng fgets(buf, sizeof buf, stdin) thay thế.
  • Trả về con trỏ tới biến cục bộ, vốn không còn tồn tại sau khi hàm trả về.
  • Dùng sai định dạng printf so với kiểu, chẳng hạn %d cho size_t.

Học tiếp ở đâu

Hãy dùng mục C của cppreference.com làm tài liệu tham khảo cho ngôn ngữ và thư viện chuẩn, và đọc các trang của nhóm làm việc ISO (WG14) về chính bản chuẩn. Luyện tập với struct, danh sách liên kết, tệp header và chương trình nhiều tệp build bằng make. Một trình gỡ lỗi (gdb hoặc lldb) cùng Valgrind hoặc AddressSanitizer sẽ giúp bạn tiết kiệm hàng giờ. Sau đó khám phá C++ hoặc Rust, xây dựng trên cùng các ý tưởng nhưng an toàn hơn.

Câu hỏi thường gặp

C có còn đáng học không?

Có. C vẫn được dùng cho hệ điều hành, thiết bị nhúng và thư viện đòi hỏi hiệu năng, và nó dạy về bộ nhớ, con trỏ và cách mã đã biên dịch chạy, điều có ích cho mọi ngôn ngữ khác.

C và C++ khác nhau thế nào?

C++ bắt đầu như một phần mở rộng của C và thêm lớp, template, tham chiếu, ngoại lệ và một thư viện chuẩn lớn. C vẫn nhỏ gọn và thủ tục. Phần lớn mã C gần như là C++ hợp lệ, nhưng chúng là hai chuẩn riêng biệt.

Vì sao chương trình C của tôi sập với lỗi segmentation fault?

Nó truy cập vùng nhớ không thuộc về mình, thường qua con trỏ null, chưa khởi tạo hoặc đã giải phóng, hoặc do lập chỉ mục vượt quá cuối mảng. Hãy chạy nó trong trình gỡ lỗi hoặc với AddressSanitizer để tìm ra đúng dòng.

Nên dùng chuẩn C nào?

C17 được các trình biên dịch hiện nay hỗ trợ rộng rãi, còn C23 là chuẩn mới nhất. Hãy chỉ định phiên bản rõ ràng, ví dụ -std=c17, để hành vi nhất quán.

Tất cả hướng dẫn lập trình