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;
}
- กล่องใหม่ทุกครั้งที่เรียก เริ่มที่ 0 แล้วบวกเป็น 1 ทุกครั้ง
- ตัวแปร
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
*****
ควรได้พีระมิดสี่แถว แต่ได้แถวเดียวที่ผิดรูป ไล่ดูทีละจังหวะ
- รอบแรก
mainตั้งiเป็น 1 แล้วเรียกprint_row(' ', 3)ฟังก์ชันพิมพ์ช่องว่าง 3 ช่อง ลูปข้างในจบตอนiเป็น 3 - บรรทัดที่ 18 คำนวณ
2 * i - 1ตอนนี้iเป็น 3 แล้ว จึงได้ 5 แทนที่จะเป็น 1 พิมพ์ดาว 5 ดวง ลูปข้างในจบตอนiเป็น 5 - กลับมาที่ลูปของ
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