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

static

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

  • Phân biệt hai nghĩa của static tùy theo vị trí khai báo
  • Dùng biến static trong hàm để nhớ giá trị giữa các lần gọi
  • Dùng static ở phạm vi file để đóng gói
  • Đọc được bảng năm lớp lưu trữ của C

Từ khóa static là ví dụ rõ nhất trong C về chuyện một từ mang hai nghĩa chẳng liên quan gì tới nhau. Nghĩa nào áp dụng phụ thuộc hoàn toàn vào chỗ bạn đặt nó. Hiểu nhầm chỗ này là chuyện rất phổ biến.

#Một từ khóa, hai nghĩa

Đặt ở đâuNghĩa là gìĐổi cái gì
Trong thân hàmBiến sống suốt chương trình, nhớ giá trị giữa các lần gọiVòng đời, phạm vi giữ nguyên
Ngoài mọi hàmTên chỉ nhìn thấy được trong file nàyLiên kết, vòng đời giữ nguyên
static int a = 0;         /* ngoài hàm: giới hạn trong file */

void f(void)
{
    static int b = 0;     /* trong hàm: nhớ giá trị giữa các lần gọi */
}

static void g(void) { }   /* ngoài hàm: hàm chỉ dùng nội bộ */

Điểm chung duy nhất của hai nghĩa là cả hai đều liên quan tới thứ gì đó tồn tại lâu dài, nhưng thao tác chúng thực hiện là hoàn toàn khác nhau. Hãy học chúng như hai từ khóa riêng biệt tình cờ được viết giống nhau.

#Nghĩa 1: static trong hàm

nho-gia-tri.c
#include <stdio.h>

void khong_static(void)
{
    int n = 0;            /* tạo lại và gán lại mỗi lần gọi */

    n++;
    printf("%d ", n);
}

void co_static(void)
{
    static int n = 0;     /* khởi tạo đúng một lần trong cả chương trình */

    n++;
    printf("%d ", n);
}

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

    for (int i = 0; i < 3; ++i) co_static();
    printf("\n");

    return 0;
}
terminal
./nho-gia-tri
1 1 1 
1 2 3 
Biến static trong hàm
Vùng nhớ nằm ở .data hoặc .bss chứ không phải trên ngăn xếp, nên nó sống suốt chương trình. Nhưng cái tên vẫn chỉ nhìn thấy được trong khối chứa nó.
int n = 0;static int n = 0;
Nằm ở đâuNgăn xếp.data nếu khác 0, .bss nếu bằng 0
Khởi tạo khi nàoMỗi lần vào hàmMột lần duy nhất, trước khi main chạy
Giá trị sau khi hàm kết thúcMấtGiữ nguyên
Giá trị mặc định nếu không gánRácBằng 0
Giá trị khởi tạo được phép là gìBất kỳ biểu thức nàoChỉ hằng biết được lúc biên dịch
An toàn đa luồngKhông

Ba trường hợp dùng đúng

/* 1. Sinh mã định danh tăng dần */
int lay_id(void)
{
    static int id = 0;

    return ++id;
}

/* 2. Khởi tạo trễ một bảng tra tốn công tính */
const double *bang_sin(void)
{
    static double bang[360];
    static int    da_tinh = 0;

    if (!da_tinh) {
        for (int i = 0; i < 360; ++i)
            bang[i] = sin(i * 3.14159265358979 / 180.0);

        da_tinh = 1;
    }

    return bang;
}

/* 3. Đếm số lần một hàm được gọi, dùng khi gỡ lỗi */
void ham_nghi_ngo(void)
{
    static unsigned long lan = 0;

    if (++lan % 100000 == 0)
        fprintf(stderr, "da goi %lu lan\n", lan);
}

#Nghĩa 2: static ngoài hàm

Liên kết nội bộ
Tên có liên kết nội bộ chỉ tồn tại trong đơn vị biên dịch chứa nó. Trình liên kết không thấy tên đó, nên file khác không thể tham chiếu tới.
file1.c
static int   bi_mat = 42;              /* chỉ file1.c thấy */
static void  ham_noi_bo(void) { }      /* chỉ file1.c gọi được */

int          cong_khai = 10;           /* file khác dùng extern là thấy */
void         ham_cong_khai(void) { }
file2.c
extern int bi_mat;        /* biên dịch qua, nhưng liên kết hỏng */
extern int cong_khai;     /* dùng được bình thường */

void f(void)
{
    printf("%d\n", cong_khai);
    printf("%d\n", bi_mat);
}
terminal
gcc file1.c file2.c main.c -o app
/usr/bin/ld: file2.o: in function `f':
file2.c:8: undefined reference to `bi_mat'

Kiểm chứng bằng bảng ký hiệu

terminal
gcc -c file1.c && nm file1.o
0000000000000000 d bi_mat
0000000000000004 D cong_khai
000000000000000b T ham_cong_khai
0000000000000000 t ham_noi_bo
Ký tự trong nmNghĩa
THàm ở vùng mã, liên kết ngoài, file khác gọi được
tHàm ở vùng mã, liên kết nội bộ, chỉ file này
DDữ liệu đã khởi tạo, liên kết ngoài
dDữ liệu đã khởi tạo, liên kết nội bộ
BDữ liệu chưa khởi tạo ở .bss, liên kết ngoài
UKý hiệu chưa xác định, cần tìm ở file khác

Quy tắc dễ nhớ: chữ hoa nghĩa là công khai, chữ thường nghĩa là nội bộ. Đây là công cụ nhanh nhất để kiểm tra một file đối tượng thật sự xuất ra những gì.

Mọi thứ đều công khai
/* thongke.c */
int  bo_dem_tam[1024];
int  so_sanh(const void *a, const void *b) { ... }
void sap_xep_noi_bo(int *a, size_t n)      { ... }
double trung_vi(int *a, size_t n)          { ... }

/* Bốn tên lọt ra toàn chương trình. File khác vô tình đặt trùng
   tên so_sanh là hỏng lúc liên kết. */
Chỉ mở ra thứ cần dùng bên ngoài
/* thongke.c */
static int  bo_dem_tam[1024];
static int  so_sanh(const void *a, const void *b) { ... }
static void sap_xep_noi_bo(int *a, size_t n)      { ... }

double trung_vi(int *a, size_t n)                 { ... }

/* Chỉ trung_vi lọt ra ngoài, đúng bằng nội dung của thongke.h. */

#extern và liên kết

Từ khóa extern nói rằng tên này tồn tại ở đâu đó, hãy để trình liên kết đi tìm. Nó là một khai báo chứ không phải định nghĩa, nên không cấp phát vùng nhớ nào.

cauhinh.h
#ifndef CAUHINH_H
#define CAUHINH_H

extern int g_muc_do_chi_tiet;    /* khai báo, đặt trong file tiêu đề */

#endif
cauhinh.c
#include "cauhinh.h"

int g_muc_do_chi_tiet = 1;       /* định nghĩa, đúng một lần, trong một file .c */

Ba mức liên kết trong C

Mức liên kếtAi cóTrình liên kết thấy không
Liên kết ngoàiHàm và biến ở phạm vi file, mặc địnhCó, mọi file đều tham chiếu được
Liên kết nội bộThứ ở phạm vi file có staticKhông, chỉ file đó dùng
Không liên kếtBiến cục bộ, tham số hàmKhông, tên chỉ tồn tại lúc biên dịch

#Bảng năm lớp lưu trữ

Cách khai báoNằm ở đâuSống bao lâuAi thấy được
int x; trong hàmNgăn xếpTrong khốiKhối chứa nó
static int x; trong hàm.data hoặc .bssToàn chương trìnhKhối chứa nó
int x; ngoài hàm.data hoặc .bssToàn chương trìnhToàn chương trình
static int x; ngoài hàm.data hoặc .bssToàn chương trìnhChỉ file này
extern int x;Không cấp phát, chỉ tham chiếuTheo nơi định nghĩa thậtNơi có khai báo
register int x;Gợi ý thanh ghiTrong khốiKhối chứa nó, không lấy địa chỉ được
_Thread_local int x;Vùng riêng của luồngTheo vòng đời luồngTùy nơi khai báo
Hai cột đầu là thứ bạn viết, ba cột sau là hệ quả. Đọc bảng này theo chiều từ nơi khai báo sang hệ quả.

Bài tập tự kiểm tra

do-ban.c
#include <stdio.h>

int a = 1;
static int b = 2;

void f(void)
{
    int c = 3;
    static int d = 4;

    c++;
    d++;
    printf("%d %d %d\n", c, d, a + b);
}

int main(void)
{
    f();
    f();
    f();

    return 0;
}

Trước khi chạy, hãy tự trả lời: chương trình in ra ba dòng gì, biến nào nằm ở .data, biến nào ở .bss, và biến nào không xuất hiện trong bảng ký hiệu của file đối tượng. Đáp án ở ngay dưới.

terminal
./do-ban
4 5 3
4 6 3
4 7 3
gcc -c do-ban.c && nm do-ban.o | grep -v ' U '
0000000000000000 D a
0000000000000004 d b
0000000000000008 d d.0
0000000000000000 T f
0000000000000020 T main

Biến c không hề xuất hiện vì nó không có liên kết. Biến d xuất hiện với một hậu tố do trình biên dịch tự thêm để tránh trùng tên với biến d có thể có ở hàm khác. Cả ba biến còn lại là a, bd đều nằm ở .data vì đều có giá trị khởi tạo khác 0.

Tự làm thử

  1. Viết int lay_id(void) dùng biến static, gọi năm lần và in kết quả.
  2. Viết hàm trả về con trỏ tới bộ đệm static, gọi nó hai lần trong cùng một printf và giải thích kết quả nhận được.
  3. Tạo hai file, mỗi file có một hàm static trùng tên nhau. Chứng minh rằng liên kết vẫn thành công. Sau đó bỏ static và xem lỗi.
  4. Chạy nm lên file đối tượng của bạn, đối chiếu từng ký hiệu với bảng chữ hoa chữ thường trong bài.
  5. Đặt một biến toàn cục có giá trị đầu bằng 0 và một biến có giá trị đầu khác 0, dùng size để xem vùng nào tăng lên.

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

  • static trong hàm làm biến sống suốt chương trình và nhớ giá trị giữa các lần gọi, phạm vi vẫn chỉ trong khối.
  • static ngoài hàm giới hạn tên trong một file. Đây là cách C làm việc đóng gói.
  • Giá trị khởi tạo của biến static phải là hằng biết được lúc biên dịch.
  • Trả về con trỏ tới bộ đệm static là cái bẫy: lời gọi sau ghi đè kết quả của lời gọi trước. Hãy để người gọi cấp bộ đệm.
  • File tiêu đề chỉ chứa extern, định nghĩa thật nằm ở đúng một file .c. Dùng nm để kiểm chứng ký hiệu nào công khai.