[0]zero-index clytie0x/zero-index

8.4 ขอบเขตของตัวแปร

พอโปรแกรมมีหลายฟังก์ชัน คำถามที่ต้องตอบให้ได้คือ ตัวแปรตัวหนึ่ง ใช้ได้ตรงไหนบ้าง และ อยู่นานแค่ไหน ช่วงของโค้ดที่ชื่อตัวแปรใช้ได้เรียกว่า ขอบเขต (scope) หน้านี้จะดูตัวแปรสามแบบ local global และ static กับกรณีที่ชื่อซ้ำกัน ซึ่งเป็นต้นเหตุของบั๊กที่หายากที่สุดแบบหนึ่งในโค้ดแข่งขัน เพราะคอมไพล์ผ่าน ไม่มีคำเตือน และได้คำตอบผิดแบบดูเหมือนถูก

ตัวแปร local อยู่ในฟังก์ชันของมัน

ตัวแปรที่ประกาศในฟังก์ชัน รวมถึงพารามิเตอร์ เรียกว่า ตัวแปร local (ตัวแปรเฉพาะที่) ใช้ได้แค่ในฟังก์ชันนั้น ฟังก์ชันอื่นมองไม่เห็น ฟังก์ชันต่างกันจึงตั้งชื่อตัวแปรซ้ำกันได้โดยไม่ชนกัน

#include <stdio.h>

int count_passed(int scores[], int n)
{
    int count = 0;
    for (int i = 0; i < n; i++) {
        if (scores[i] >= 50) {
            count++;
        }
    }
    return count;
}

int count_perfect(int scores[], int n)
{
    int count = 0;
    for (int i = 0; i < n; i++) {
        if (scores[i] == 100) {
            count++;
        }
    }
    return count;
}

int main()
{
    int scores[6] = {45, 100, 72, 50, 100, 38};
    int count = 6;
    printf("students = %d\n", count);
    printf("passed = %d\n", count_passed(scores, count));
    printf("perfect = %d\n", count_perfect(scores, count));
    printf("students = %d\n", count);
    return 0;
}
students = 6
passed = 4
perfect = 2
students = 6

โปรแกรมนี้มี count สามตัว i สองตัว และ n สองตัว ทุกตัวเป็นคนละกล่องกัน count_passed นับไปถึง 4 ก็ไม่ไปแตะ count ของ main ที่เป็น 6 บรรทัดสุดท้ายจึงยังพิมพ์ 6 ได้

เรื่องนี้ทำให้เขียนฟังก์ชันได้สบายใจ ตอนเขียน count_perfect ไม่ต้องไปเช็กเลยว่าที่อื่นใช้ชื่อ count หรือ i ไปแล้วหรือยัง ฟังก์ชันหนึ่งตัวเหมือนห้องหนึ่งห้อง ของที่วางในห้องไม่ปนกับห้องอื่น

ขอบเขตแคบกว่าฟังก์ชันก็มี ตัวแปรที่ประกาศในปีกกาของ if หรือลูป ใช้ได้แค่ในปีกกานั้น อย่าง i ที่ประกาศในวงเล็บของ for หายไปตอนลูปจบ ซึ่งเห็นแล้วในแบบที่ 3 ของหน้า 4.1 กฎทั่วไปคือ ตัวแปรใช้ได้ตั้งแต่บรรทัดที่ประกาศ จนถึงปีกกาปิดของปีกกาคู่ที่ครอบมันอยู่

local เกิดใหม่ทุกครั้งที่เรียก

ตัวแปร local ไม่ได้มีอยู่ตลอดเวลา มันถูกสร้างตอนฟังก์ชันถูกเรียก และหายไปตอนฟังก์ชันจบ เรียกครั้งต่อไปก็ได้กล่องใหม่ ค่าจากครั้งก่อนไม่เหลือ ถ้าอยากให้ตัวแปรในฟังก์ชันจำค่าข้ามการเรียกได้ ให้ใส่คำว่า static หน้าการประกาศ

#include <stdio.h>

int next_ticket_local()
{
    int number = 0; // (1)!
    number++;
    return number;
}

int next_ticket()
{
    static int number = 0; // (2)!
    number++;
    return number;
}

int main()
{
    for (int i = 0; i < 3; i++) {
        int a = next_ticket_local();
        int b = next_ticket();
        printf("local %d, static %d\n", a, b);
    }
    return 0;
}
  1. กล่องใหม่ทุกครั้งที่เรียก เริ่มที่ 0 แล้วบวกเป็น 1 ทุกครั้ง
  2. ตัวแปร static ถูกสร้างครั้งเดียวตอนโปรแกรมเริ่ม และไม่หายไปตอนฟังก์ชันจบ บรรทัดนี้จึงใส่ค่า 0 แค่ครั้งแรกครั้งเดียว ครั้งต่อไปข้ามไปเลย
local 1, static 1
local 1, static 2
local 1, static 3

static เหมือนเครื่องกดบัตรคิวที่จำเลขล่าสุดไว้ แต่ยังเป็นของฟังก์ชันนั้นคนเดียว ฟังก์ชันอื่นมองไม่เห็น number ตัวนี้ ในโจทย์แข่งขันไม่ค่อยได้ใช้ แต่ควรรู้ไว้เพราะจะเจอในโค้ดของคนอื่น และใช้นับว่าฟังก์ชันถูกเรียกไปกี่ครั้งตอนหาบั๊ก

ตัวแปร global ทุกฟังก์ชันเห็น

ตัวแปรที่ประกาศนอกทุกฟังก์ชันเรียกว่า ตัวแปร global ซึ่งน้องใช้มาตั้งแต่หน้า 5.5 กับอาเรย์ใหญ่ ทุกฟังก์ชันที่อยู่ใต้บรรทัดประกาศใช้มันได้ อยู่ตลอดตั้งแต่โปรแกรมเริ่มจนจบ และเริ่มต้นเป็น 0 เสมอ

โค้ดแข่งขันจำนวนมากเก็บข้อมูลเข้าไว้ใน global แล้วให้ทุกฟังก์ชันใช้ตรงๆ ไม่ต้องส่งเป็นพารามิเตอร์

#include <stdio.h>

int n;
int heights[100000];

void read_input()
{
    scanf("%d", &n);
    for (int i = 0; i < n; i++) {
        scanf("%d", &heights[i]);
    }
}

int tallest()
{
    int best = heights[0];
    for (int i = 1; i < n; i++) {
        if (heights[i] > best) {
            best = heights[i];
        }
    }
    return best;
}

int count_taller_than(int limit)
{
    int count = 0;
    for (int i = 0; i < n; i++) {
        if (heights[i] > limit) {
            count++;
        }
    }
    return count;
}

int main()
{
    read_input();
    printf("tallest = %d\n", tallest());
    printf("over 160 = %d\n", count_taller_than(160));
    return 0;
}
6
158 171 149 165 160 177
tallest = 177
over 160 = 3

ข้อดีคือสั้น read_input เขียนลง n ได้เลยโดยไม่ต้องใช้ pointer และไม่ต้องส่ง heights กับ n ทุกครั้งที่เรียก ข้อเสียคือฟังก์ชันพวกนี้ผูกติดกับอาเรย์ heights ตัวเดียว ถ้าโจทย์มีสองห้องแบบหน้า 8.3 ก็ต้องเขียน tallest สองชุด และพออ่านโค้ด เห็นแค่ tallest() ไม่รู้ว่ามันใช้ข้อมูลอะไรบ้าง ต้องเปิดไปดูข้างใน

ชื่อซ้ำกัน ตัวที่ใกล้กว่าชนะ

ถ้าในฟังก์ชันประกาศตัวแปร local ชื่อเดียวกับตัวแปร global ภาษา C ไม่ห้าม แต่ในฟังก์ชันนั้น ชื่อนั้นจะหมายถึงตัว local เสมอ ตัว global ถูกบังจนมองไม่เห็น อาการนี้เรียกว่า shadowing (บังเงา)

#include <stdio.h>

int score = 100;

void show_local()
{
    int score = 5;
    printf("in show_local: %d\n", score);
}

void show_global()
{
    printf("in show_global: %d\n", score);
}

int main()
{
    show_local();
    show_global();
    printf("in main: %d\n", score);
    return 0;
}
in show_local: 5
in show_global: 100
in main: 100

show_local มี score ของตัวเองจึงเห็น 5 ส่วน show_global กับ main ไม่มี local ชื่อนี้ จึงเห็นตัว global ที่เป็น 100 กฎคือ compiler หาชื่อจากปีกกาที่ใกล้ที่สุดก่อน แล้วค่อยขยายออกไปข้างนอกทีละชั้น เจอตัวไหนก่อนใช้ตัวนั้น

-Wall ไม่เตือนเรื่องนี้ ถ้าอยากให้เตือน ต้องเพิ่มตัวเลือก -Wshadow เอง ในหน้า Compiler settings ของ Code::Blocks แท็บ Other compiler options พิมพ์ -Wshadow ลงไปได้ ใช้ตอนหาบั๊กแล้วจะช่วยได้มาก ตัวอย่างที่อันตรายจริงอยู่ในแบบที่ 3 ข้างล่าง

เมื่อไรควรใช้ global

ทั้งสองแบบใช้ได้ คนเขียนโปรแกรมแข่งขันส่วนใหญ่ใช้ผสมกันตามหลักนี้

ข้อมูลควรเป็นเหตุผล
อาเรย์ขนาดใหญ่ตามขอบเขตโจทย์globalตัวแปรใน main ใช้ได้รวมราว 2 MB ตามหน้า 5.5 และ global เริ่มเป็น 0 ให้เอง
ข้อมูลเข้าที่ทุกฟังก์ชันต้องใช้ เช่น แผนที่ของโจทย์กริดglobal ได้ไม่ต้องส่งต่อกันทุกชั้น
ตัวนับลูป ผลรวมชั่วคราว ตัวแปรพักค่าlocal เสมอถ้าเป็น global ฟังก์ชันจะแย่งกันใช้ ดูแบบที่ 2
ข้อมูลที่ฟังก์ชันเดียวต้องทำงานกับหลายชุดพารามิเตอร์เรียกซ้ำกับข้อมูลคนละชุดได้ แบบสองห้องในหน้า 8.3

ถ้าตัดสินใจใช้ global แล้ว ให้ตั้งชื่อที่ไม่ซ้ำกับตัวแปร local ใดๆ และ ห้ามประกาศชื่อเดิมซ้ำใน main นี่คือกฎข้อที่ทำให้คนเสียคะแนนมากที่สุดของหน้านี้

ลองทำให้พัง

แบบที่ 1 ใช้ตัวแปร local ของฟังก์ชันอื่น

#include <stdio.h>

void read_scores(int n)
{
    int total = 0;
    for (int i = 0; i < n; i++) {
        int score;
        scanf("%d", &score);
        total += score;
    }
}

int main()
{
    read_scores(3);
    printf("total = %d\n", total);
    return 0;
}
total.c:16:28: error: 'total' undeclared (first use in this function)
   16 |     printf("total = %d\n", total);
      |                            ^~~~~
total.c:16:28: note: each undeclared identifier is reported only once for each function it appears in

total เป็น local ของ read_scores และหายไปตั้งแต่ฟังก์ชันจบ main จึงไม่รู้จักชื่อนี้เลย บรรทัด note บอกว่า error แบบนี้จะขึ้นครั้งเดียวต่อชื่อต่อฟังก์ชัน ถึงจะใช้ total หลายจุดก็ตาม วิธีแก้ที่ถูกคือให้ read_scores ส่ง total กลับมาด้วย return

แบบที่ 2 ตัวนับลูปเป็น global

เอาโปรแกรมพีระมิดจากหน้า 8.1 มาประกาศ i ไว้ข้างนอกตัวเดียว ให้ทั้ง main และ print_row ใช้ร่วมกัน

#include <stdio.h>

int i;

void print_row(char symbol, int count)
{
    for (i = 0; i < count; i++) {
        printf("%c", symbol);
    }
}

int main()
{
    int height;
    scanf("%d", &height);
    for (i = 1; i <= height; i++) {
        print_row(' ', height - i);
        print_row('*', 2 * i - 1);
        printf("\n");
    }
    return 0;
}
4
   *****

ควรได้พีระมิดสี่แถว แต่ได้แถวเดียวที่ผิดรูป ไล่ดูทีละจังหวะ

  1. รอบแรก main ตั้ง i เป็น 1 แล้วเรียก print_row(' ', 3) ฟังก์ชันพิมพ์ช่องว่าง 3 ช่อง ลูปข้างในจบตอน i เป็น 3
  2. บรรทัดที่ 18 คำนวณ 2 * i - 1 ตอนนี้ i เป็น 3 แล้ว จึงได้ 5 แทนที่จะเป็น 1 พิมพ์ดาว 5 ดวง ลูปข้างในจบตอน i เป็น 5
  3. กลับมาที่ลูปของ main บวก i เป็น 6 ซึ่งเกิน 4 ลูปจบทันที

สองลูปใช้ตัวนับกล่องเดียวกัน ลูปข้างในจึงเขียนทับตัวนับของลูปข้างนอก คอมไพล์ผ่านไม่มีคำเตือน และถ้าทดสอบด้วยค่าที่บังเอิญได้ผลถูกก็จะไม่รู้ตัวเลย ตัวนับลูปต้องเป็น local เสมอ ประกาศในวงเล็บของ for แบบ for (int i = 0; ...) ติดเป็นนิสัยไว้

แบบที่ 3 ประกาศ n ซ้ำใน main

ในโปรแกรม global-data ย้ายการอ่านข้อมูลมาไว้ใน main แทน read_input แล้วเผลอประกาศ int n; ไว้ใน main ด้วย ซึ่งเป็นนิสัยที่ติดมาจากบทก่อนๆ

#include <stdio.h>

int n;
int heights[100000];

int tallest()
{
    int best = heights[0];
    for (int i = 1; i < n; i++) {
        if (heights[i] > best) {
            best = heights[i];
        }
    }
    return best;
}

int count_taller_than(int limit)
{
    int count = 0;
    for (int i = 0; i < n; i++) {
        if (heights[i] > limit) {
            count++;
        }
    }
    return count;
}

int main()
{
    int n;
    scanf("%d", &n);
    for (int i = 0; i < n; i++) {
        scanf("%d", &heights[i]);
    }
    printf("tallest = %d\n", tallest());
    printf("over 160 = %d\n", count_taller_than(160));
    return 0;
}
6
158 171 149 165 160 177
tallest = 158
over 160 = 0

scanf ใน main อ่าน 6 ใส่ n ตัว local ส่วน n ตัว global ยังเป็น 0 เหมือนตอนโปรแกรมเริ่ม tallest กับ count_taller_than มองเห็นแค่ตัว global จึงคิดว่ามีข้อมูล 0 ตัว ลูปของ count_taller_than ไม่ทำงานเลยได้ 0 ส่วน tallest ได้ 158 ซึ่งคือ heights[0] ที่ใช้เป็นค่าเริ่มต้น ผิดแต่ดูเหมือนตัวเลขปกติ

บั๊กแบบนี้หาด้วยตายากมาก เพราะทุกบรรทัดดูถูกต้อง และอาเรย์ heights ก็มีข้อมูลครบ ถ้าคอมไพล์ด้วย -Wall -Wshadow compiler จะชี้ให้เห็นทันที

heights.c:30:9: warning: declaration of 'n' shadows a global declaration [-Wshadow]
   30 |     int n;
      |         ^
heights.c:3:5: note: shadowed declaration is here
    3 | int n;
      |     ^

สรุป

  • ตัวแปร local ประกาศในฟังก์ชันหรือในปีกกา ใช้ได้แค่ในนั้น ฟังก์ชันต่างกันใช้ชื่อซ้ำกันได้โดยไม่ชนกัน
  • local เกิดใหม่ทุกครั้งที่เรียกฟังก์ชัน ใส่ static ถ้าต้องการให้จำค่าข้ามการเรียก
  • ตัวแปร global ประกาศนอกฟังก์ชัน ทุกฟังก์ชันข้างล่างเห็น อยู่ตลอดโปรแกรม และเริ่มเป็น 0
  • ชื่อซ้ำกัน ตัวที่ประกาศใกล้ที่สุดชนะ -Wall ไม่เตือน ต้องเปิด -Wshadow เอง
  • อาเรย์ใหญ่และข้อมูลที่ใช้ร่วมกันเป็น global ได้ ตัวนับลูปและค่าชั่วคราวต้องเป็น local เสมอ
  • มี global ชื่อไหนแล้ว ห้ามประกาศชื่อนั้นซ้ำใน main