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

Biến

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

  • Phân biệt khai báo, khởi tạo và gán
  • Giải thích được vì sao đọc biến chưa khởi tạo là hành vi không xác định
  • Xác định phạm vi và thời gian sống của một biến bất kỳ
  • Đặt tên biến theo quy ước và tránh các tên bị cấm

Trong C, một biến không phải là cái tên trừu tượng chứa giá trị. Nó là một vùng nhớ thật, nằm ở một địa chỉ thật, rộng đúng bao nhiêu byte do kiểu quyết định. Nắm được điều này ngay từ bây giờ sẽ tiết kiệm cho bạn rất nhiều công sức khi tới phần con trỏ.

#Biến là gì

Tên biến chỉ là cách gọi thuận tiện cho một vùng nhớ. Kiểu quyết định vùng đó rộng bao nhiêu byte.

Khi bạn viết int diem = 85;, trình biên dịch làm ba việc: xin một vùng 4 byte trên ngăn xếp, ghi nhớ rằng cái tên diem ứng với vùng đó, và ghi giá trị 85 vào.

  • Kiểu quyết định vùng nhớ rộng bao nhiêu byte và các bit trong đó được hiểu như thế nào.
  • Tên chỉ tồn tại lúc biên dịch. Trong file chạy được không còn tên nào cả, chỉ còn địa chỉ.
  • Giá trị là các bit đang nằm trong vùng nhớ đó tại một thời điểm.

#Khai báo, khởi tạo, gán

Ba từ này hay bị dùng lẫn lộn, nhưng chúng là ba việc khác nhau.

int a;              /* KHAI BÁO: xin vùng nhớ, chưa đặt giá trị */
a = 10;             /* GÁN: ghi giá trị vào vùng nhớ đã có */

int b = 20;         /* KHỞI TẠO: khai báo và đặt giá trị cùng lúc */

int c, d, e;        /* khai báo nhiều biến cùng kiểu */
int f = 1, g = 2;   /* khởi tạo nhiều biến */
int h = 3, i;       /* h được khởi tạo, i thì không */
ViệcXảy ra khi nàoLàm gì
Khai báoMột lần duy nhấtXin vùng nhớ và đặt tên cho nó
Khởi tạoNgay lúc khai báoĐặt giá trị đầu tiên cho vùng nhớ
GánBao nhiêu lần cũng đượcGhi đè giá trị mới lên vùng nhớ

#Biến chưa khởi tạo

Đây là một trong những nguồn bug khó chịu nhất của C, vì chương trình vẫn chạy, chỉ là kết quả sai và thay đổi thất thường.

#include <stdio.h>

int main(void)
{
    int x;                    /* chưa khởi tạo */
    int tong = 0;

    printf("%d\n", x);       /* HÀNH VI KHÔNG XÁC ĐỊNH */

    for (int i = 1; i <= 5; ++i) {
        tong += i;            /* an toàn vì tong đã được đặt bằng 0 */
    }

    printf("%d\n", tong);

    return 0;
}

Biến cục bộ chưa khởi tạo nhận bất cứ giá trị nào đang có sẵn trong vùng nhớ đó, thường là rác từ lời gọi hàm trước. Nó có thể là 0, có thể là 32764, và có thể khác nhau giữa hai lần chạy.

Ngoại lệ: biến toàn cục và static

Chỉ biến cục bộ mới chứa rác. Biến toàn cục và biến static được hệ thống tự động đặt bằng 0 trước khi chương trình chạy.

int g;              /* toàn cục, tự động = 0 */

void f(void)
{
    static int s;   /* static, tự động = 0 */
    int cuc_bo;     /* cục bộ, chứa RÁC */
}

#Quy tắc đặt tên

Quy tắcVí dụ hợp lệVí dụ không hợp lệ
Chỉ chứa chữ cái, chữ số và gạch dướiso_luong, maxValue, _tmpso-luong, số_lượng
Không bắt đầu bằng chữ sốx1, lan_22x, 1_lan
Phân biệt hoa thườngage, Age, AGE là ba biến khác nhau
Không trùng từ khóaso_intint, return, while
Không bắt đầu bằng gạch dưới kép hoặc gạch dưới rồi chữ hoa_tmp__tmp, _Tmp

Quy ước dùng trong khóa học

int so_luong_sinh_vien;        /* biến và hàm: snake_case */
void tinh_diem_trung_binh(void);

#define MAX_SINH_VIEN 100          /* macro và hằng: IN HOA */

typedef struct SinhVien SinhVien;  /* kiểu: PascalCase */

static int g_dem;                  /* biến toàn cục: tiền tố g_ */

Quan trọng hơn quy ước cụ thể là sự nhất quán. Chọn một kiểu và giữ nguyên trong toàn bộ dự án.

Tên không nói lên điều gì
int a, b, c;
int tmp, data, thing;
int flag;
float x1, x2;
Tên tự giải thích
int so_sinh_vien, tong_diem, so_mon;
int diem_tam_thoi;
int da_khoi_tao;
float nghiem_1, nghiem_2;

#Phạm vi và thời gian sống

Hai khái niệm này liên quan nhưng không giống nhau. Phạm vi trả lời câu hỏi ở đâu nhìn thấy được cái tên. Thời gian sống trả lời câu hỏi vùng nhớ tồn tại trong bao lâu.

pham-vi.c
#include <stdio.h>

int g = 100;                  /* phạm vi file, sống suốt chương trình */

void f(void)
{
    static int lan_goi = 0;   /* phạm vi hàm, sống suốt chương trình */
    int cuc_bo = 0;           /* phạm vi hàm, sống trong một lần gọi */

    ++lan_goi;
    ++cuc_bo;

    printf("static=%d  cuc_bo=%d\n", lan_goi, cuc_bo);
}

int main(void)
{
    f();                      /* static=1  cuc_bo=1 */
    f();                      /* static=2  cuc_bo=1 */
    f();                      /* static=3  cuc_bo=1 */

    for (int i = 0; i < 3; ++i) {   /* i chỉ sống trong for */
        int t = i * 2;              /* t chỉ sống trong thân for */

        printf("%d ", t);
    }
    printf("\n%d\n", g);

    return 0;
}

Biến cuc_bo được tạo lại từ đầu ở mỗi lần gọi nên luôn in ra 1. Biến lan_goi chỉ được khởi tạo đúng một lần và giữ giá trị giữa các lần gọi, nên nó đếm được số lần hàm chạy. Cơ chế static sẽ được đào sâu ở Phần 3.

Loại biếnPhạm viThời gian sốngGiá trị ban đầu
Cục bộ trong khốiTrong khối đóTrong khối đóRác
Tham số hàmTrong thân hàmMột lần gọiGiá trị truyền vào
static trong hàmTrong hàm đóToàn chương trình0
Toàn cụcTừ chỗ khai báo tới hết fileToàn chương trình0

Tự làm thử

  1. Khai báo và in ra: tên (chuỗi), tuổi (int), chiều cao (float), giới tính (char). In kèm địa chỉ và kích thước của từng biến.
  2. Hoán đổi giá trị hai biến bằng biến tạm. Sau đó thử làm không dùng biến tạm theo hai cách: cộng trừ, và phép XOR. Chỉ ra nhược điểm của mỗi cách.
  3. Viết hàm dùng biến static để trả về số lần nó đã được gọi.
  4. Khai báo một biến cục bộ không khởi tạo rồi in ra, chạy năm lần. Ghi lại kết quả. Sau đó thêm -fsanitize=undefined và chạy lại.
  5. Viết chương trình có ba biến cùng tên x ở ba tầng khối lồng nhau, in giá trị ở từng tầng. Biên dịch với -Wshadow và đọc cảnh báo.

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

  • Biến là một vùng nhớ có tên, có kiểu quyết định độ rộng, và có địa chỉ.
  • Khai báo xin vùng nhớ, khởi tạo đặt giá trị đầu tiên, gán ghi đè giá trị mới.
  • Biến cục bộ chưa khởi tạo chứa rác và đọc nó là hành vi không xác định.
  • Biến toàn cục và biến static tự động bằng 0.
  • Phạm vi là nơi nhìn thấy tên, thời gian sống là khoảng thời gian vùng nhớ tồn tại.
  • Khai báo biến càng gần chỗ dùng càng tốt, và khởi tạo ngay tại đó.