Exercise 22 Lập Trình C – Learn C The Hard Way


Exercise 22: The Stack, Scope, and Globals

Khái niệm về scope (phạm vi hay tầm vực) khá là khó hiểu cho những người mới bước vào lập trình. Khái niệm này bắt nguồn từ khái niệm stack hay hệ thống stack (chúng ta sẽ học stack sớm), và làm cách nào mà stack lưu trữ các biến tạm (temporaty variables).

Trong bài học hôm nay, chúng ta sẽ học về scope bằng cách chúng ta hiểu được làm thế nào mà các biến được lưu trên stack, hay stack làm việc thế nào (chúng ta còn gọi là stack data structure).

Chúng ta không nên vội vàng khi học về các khái niệm này, chúng ta cần phải từ từ hiểu được nơi mà các biến được tạo ra, tồn tại, và giải phóng. Và từ từ chúng ta sẽ hiểu được khái niệm về scope, từ đó khả năng lập trình của chúng ta sẽ tăng tiến rất nhiều.

Bài này chúng ta sẽ có 3 file:

ex22.h file header để cài đặt các biến external và một vài function.

ex22.c đây không phải là file main mà các bạn thường thực hành như những bài khác, mà file này chứa các định nghĩa (define) của các hàm mà được khởi tạo (declare), nhớ phân biệt 2 khái niệm này, declare là trong file .h, còn define là trong file .c.

ex22_main.c đây là file main chính hiệu chúng ta cần. Và trong file này sẽ mô tả về scope mà chúng ta sẽ từ từ nghiên cứu và hiểu ra nó.

ex22.h and ex22.c

Đầu tiên chúng ta xem nội dung file ex22.h nơi mà chúng ta define một số biến và hàm.

#ifndef _ex22_h
#define _ex22_h

// makes THE_SIZE in ex22.c available to other .c files
extern int THE_SIZE;

// gets and sets an internal static variable in ex22.c
int get_age();
void set_age(int age);

// updates a static variable that's inside update_ratio
double update_ratio(double ratio);

void print_size();

#endif

Điều quan trong trong file trên làm cho các bạn không hiểu chính là dòng thứ 5, và các bạn sẽ được học sau.

Chúng ta cùng xem file ex22.c:

#include <stdio.h>
#include "ex22.h"
#include "dbg.h"

int THE_SIZE = 1000;
static int THE_AGE = 37;

int get_age()
{
     return THE_AGE;
}

void set_age(int age)
{
     THE_AGE = age;
}

double update_ratio(double new_ratio)
{
    static double ratio = 1.0;

    double old_ratio = ratio;
    ratio = new_ratio;

    return old_ratio;
}

void print_size()
{
    log_info("I think size is: %d", THE_SIZE);
}

Ok, bây giờ chúng ta sẽ đi giải thích một chút về các vấn đề trên 2 file này:

extern Từ khóa này nói cho compiler biết rằng “biến này đã tồn tại, nhưng nó tồn tại ở một nơi nào đó (một file khác)”. Trong trường hợp này là biến THE_SIZE. Chuyện này để tránh việc chúng ta define 2 lần gây ra lỗi ở giai đoạn linker.

static (file) Từ khóa này nó ngược với từ extern, nhưn trong trường hợp này, biến THE_AGE chỉ được sử dụng trong file ex22.c. Đây là static ở trong file, và chúng ta còn một khái niệm biến static trong hàm nữa. Chúng ta sẽ được học ở phần sau.

static (function) Nếu bạn declare một biến trong một hàm với từ khóa static, thì biến static này sẽ không thay đổi giá trị khi chúng ta thoát khỏi hàm đó. Trường hợp trên là biến ratio.

Trong 2 file trên, chúng ta cần tìm hiểu những thứ sau:

THE_SIZE Đây là biến được khai báo extern, và chúng ta cũng sử dụng nó ở ex22_main.c.

get_age and set_age Đây là 2 hàm dùng để lấy giá trị và set giá trị của biến THE_AGE, bạn không thể truy cập THE_AGE trực tiếp, mà phải thông qua 2 hàm này.

update_ratio Hàm này dùng để lấy giá trị mới của biến ratio và trả về giá trị của ratio.

print_size Hàm này sẽ in giá trị của THE_SIZE hiện tại ra.

ex22_main.c

Tìm hiểu thật kĩ file này để nắm rõ khái niệm scope như được đề cập ở đầu bài.

#include "ex22.h"
#include "dbg.h"

const char *MY_NAME = "Zed A. Shaw";

void scope_demo(int count)
{
    log_info("count is: %d", count);

    if (count > 10) {
        int count = 100;	// BAD! BUGS!

        log_info("count in this scope is %d", count);
    }

    log_info("count is at exit: %d", count);

    count = 3000;

    log_info("count after assign: %d", count);
}

int main(int argc, char *argv[])
{
    // test out THE_AGE accessors
    log_info("My name: %s, age: %d", MY_NAME, get_age());

    set_age(100);

    log_info("My age is now: %d", get_age());

    // test out THE_SIZE extern
    log_info("THE_SIZE is: %d", THE_SIZE);
    print_size();

    THE_SIZE = 9;

    log_info("THE SIZE is now: %d", THE_SIZE);
    print_size();

    // test the ratio function static
    log_info("Ratio at first: %f", update_ratio(2.0));
    log_info("Ratio again: %f", update_ratio(10.0));
    log_info("Ratio once more: %f", update_ratio(300.0));

    // test the scope demo
    int count = 4;
    scope_demo(count);
    scope_demo(count * 20);

    log_info("count after calling scope_demo: %d", count);

    return 0;
}

Bây giờ chúng ta sẽ giải thích một chút về file này:

Dòng 4 Từ khóa const nói lên đây là một biến hằng số (constant variable).

Dòng 6 Một hàm đơn giản mô tả về các vấn đề scope trong hàm.

Dòng 8 In giá trị của biến count khi nó mới được truyền vào function.

Dòng 10 Câu lệnh if này để bắt đầu một khối mới (scope block), và trong khối này có một biến count khác, và đây là một biến mới, hoàn toàn khác với biến count ở ngoài. Bạn cũng có thể hiểu câu lệnh if giống như là một hàm mới vậy.

Dòng 11 Biến count là một biến local ở trong khối if và nó khác với biến count ở ngoài.

Dòng 13 In biến count local ở trong if ra, và dĩ nhiên kết quả là 100.

Dòng 16 Đây là biến count được truyền vào hàm, và giá trị của biến này khác với biến count ở trong khối if.

Dòng 18 tới 20 Chúng ta set một giá trị mới cho biến count (3000) và chúng ta in nó ra màn hình.

Điều rút ra ở đây là bạn không nên dùng biến như thế, nó chính là một BUG, hãy đặt một tên khác. Chúng ta dùng như thế để các bạn có thể hiểu được khái niệm cơ bản của scope.

Bây giờ chúng ta xem hàm main:

Dòng 26 In ra màn hình giá trị hiện tại của MY_NAME và lấy THE_AGE từ ex22.c bằng cách sử dụng hàm get_age.

Dòng 27 tới 30 Đặt lại giá trị của THE_AGE và in nó ra màn hình.

Dòng 33 tới 39 Chúng ta làm tương tự với biến THE_SIZE.

Dòng 42 tới 44 Chúng ta xử lý biến static ratio thông qua hàm update_ratio, biến này sẽ giữ giá trị khi gọi hàm nhiều lần.

Dòng 46 tới 51 Và cuối cùng, chúng ta chạy hàm scope_demo 2 lần và bạn có thể nhìn thấy cách scope hoạt động, rất là đơn giản. Chạy nó đi, bạn sẽ hiểu ra vấn đề.

Nếu tới đây bạn vẫn chưa hiểu, 1 là bạn chưa chạy nó, 2 là bạn chưa nghiên cứu nó nhiều. Vậy hãy quay lại chạy và nghiên cứu thêm đi.

What You Should See

Thay vì chúng ta sử dụng Makefile, bạn thử chạy nó bằng command line như bên dưới. Nghĩa là bạn chạy bằng tay đấy.  Và bạn sẽ nhìn thấy nó chạy như thế này:

$ cc -Wall -g -DNDEBUG  -c -o ex22.o ex22.c
$ cc -Wall -g -DNDEBUG   ex22_main.c ex22.o   -o ex22_main
$ ./ex22_main
[INFO] (ex22_main.c:26) My name: Zed A. Shaw, age: 37
[INFO] (ex22_main.c:30) My age is now: 100
[INFO] (ex22_main.c:33) THE_SIZE is: 1000
[INFO] (ex22.c:32) I think size is: 1000
[INFO] (ex22_main.c:38) THE SIZE is now: 9
[INFO] (ex22.c:32) I think size is: 9
[INFO] (ex22_main.c:42) Ratio at first: 1.000000
[INFO] (ex22_main.c:43) Ratio again: 2.000000
[INFO] (ex22_main.c:44) Ratio once more: 10.000000
[INFO] (ex22_main.c:8) count is: 4
[INFO] (ex22_main.c:16) count is at exit: 4
[INFO] (ex22_main.c:20) count after assign: 3000
[INFO] (ex22_main.c:8) count is: 80
[INFO] (ex22_main.c:13) count in this scope is 100
[INFO] (ex22_main.c:16) count is at exit: 80
[INFO] (ex22_main.c:20) count after assign: 3000
[INFO] (ex22_main.c:51) count after calling scope_demo: 4

À, bạn phải hiểu được file dbg.h nhé, cái này nằm trong một bài trước, bạn nên đọc nó trước khi qua bài này, và chắc chắn bạn phải đọc nó để có thể học bài này, dùng mấy lệnh như log_info. Chúng ta đi qua phần mới.

Scope, Stack, and Bugs

Đây là một vài luật để các bạn tránh gặp BUG khi viết code C:

  • Không được dùng 2 biến giống tên nhau ở 2 scope khác nhau, sẽ gây ra nhầm lẫn và BUG, như biến count trong bài này.
  • Tránh sử dụng quá nhiều biến global (biến toàn cục),  nhất là thông qua nhiều file. Nếu bạn bắt buộc phải dùng nó, thì bạn phải sử dụng một hàm như get_age, get_age để truy cập nó. Và điều này không áp dụng cho biến hằng, bởi vì biến hằng là read-only.
  • Khi có sự ngờ vực, thì bạn nên đặt biến này trong HEAP, thay vì trong stack hay một nơi này đó, đặt trong HEAP thì dùng malloc.
  • Không sử dụng các hàm chứa các biến static như update_ratio. Nó hiếm khi hữu ích cho bạn, và nó thường gây đau đớn cho bạn, code bạn sẽ không như bạn mong đợi.
  • Tránh sử dụng lại hàm có tham số. Vì nó có thể gây ra hiểu lầm cho bạn.

Bạn hãy thực hành nhiều nó. Có điều kiện thì kiếm bộ full video clip và giáo trình của tác giả để hiểu thêm về bài học.

Leave a comment