[0]zero-index clytie0x/zero-index

1.3 จำนวนเต็มใหญ่แค่ไหน

กล่อง int มีขนาดตายตัว เก็บเลขได้ถึงค่าหนึ่งเท่านั้น ถ้าผลคำนวณใหญ่เกิน โปรแกรมจะไม่ฟ้องอะไร แต่ได้คำตอบผิดเงียบๆ เรื่องนี้ทำให้คนเสียคะแนนในการแข่งขันมาแล้วนับไม่ถ้วน เพราะโจทย์ชอบให้ตัวเลขใหญ่ถึงหลักพันล้าน หน้านี้จะทำให้มันพังให้เห็นกับตา แล้วแก้ด้วยชนิด long long

int เก็บได้แค่ไหน

ในหน้า 0.1 เราเห็นว่าข้างในคอมพิวเตอร์มีแค่ 0 กับ 1 เลขหนึ่งหลักแบบนี้เรียกว่า bit และ 8 bit รวมกันเรียกว่า byte กล่อง int ใช้ 4 byte หรือ 32 bit

32 bit เรียงได้ 232 แบบ ประมาณ 4.3 พันล้านแบบ แบ่งครึ่งหนึ่งให้เลขติดลบ อีกครึ่งให้ 0 กับเลขบวก ค่าที่แน่นอนดูได้จาก limits.h ซึ่งมีค่าคงที่ INT_MAX กับ INT_MIN ให้

#include <stdio.h>
#include <limits.h>

int main()
{
    printf("int max = %d\n", INT_MAX);
    printf("int min = %d\n", INT_MIN);
    return 0;
}
int max = 2147483647
int min = -2147483648

ไม่ต้องท่องเลขเต็ม จำว่า int เก็บได้ประมาณ 2 × 109 คือสองพันล้าน เป็นเลขสิบหลัก

เกินแล้วเกิดอะไร

ลองเอาค่ามากสุดของ int มาบวกหนึ่ง

#include <stdio.h>

int main()
{
    int money;
    scanf("%d", &money);
    money = money + 1;
    printf("%d\n", money);
    return 0;
}
2147483647
-2147483648

บวกหนึ่งแล้วกลายเป็นค่าติดลบที่น้อยที่สุด เหมือนเลขไมล์รถที่วิ่งถึง 999999 แล้ววนกลับไปเริ่มใหม่ ต่างกันตรงที่ int วนไปฝั่งติดลบ อาการที่ค่าเกินขอบเขตของชนิดนี้เรียกว่า overflow (ล้น)

ตัวอย่างที่เจอจริงกว่านั้น คือการคูณ ที่ดินกว้าง 50000 เมตร ยาว 50000 เมตร พื้นที่คือ 2500000000 ตารางเมตร เกินสองพันล้านไปแล้ว

#include <stdio.h>

int main()
{
    int width, length;
    scanf("%d %d", &width, &length);
    printf("area = %d\n", width * length);
    return 0;
}
50000 50000
area = -1794967296

พื้นที่ติดลบ ไม่มี error ไม่มี warning คอมไพล์ผ่านด้วย -Wall สบายๆ ถ้าส่งโปรแกรมนี้ ชุดทดสอบที่ตัวเลขเล็กจะผ่านหมด และตกเฉพาะชุดที่ตัวเลขใหญ่ ซึ่งหาสาเหตุยากถ้าไม่รู้เรื่องนี้

อย่าพึ่งผลที่วนกลับ

มาตรฐานภาษา C ไม่ได้รับประกันว่า int ที่ล้นจะวนกลับแบบนี้เสมอ เรียกว่าเป็น undefined behavior บนเครื่องเราเห็นวนกลับ แต่ compiler มีสิทธิ์ทำอย่างอื่นได้ สิ่งที่ต้องจำจึงไม่ใช่ว่าล้นแล้วได้เลขอะไร แต่คือ อย่าปล่อยให้ล้นเลย

long long กล่องที่ใหญ่กว่า

long long เป็นจำนวนเต็มเหมือน int แต่ใช้ 8 byte หรือ 64 bit เก็บได้กว้างกว่ามาก

#include <stdio.h>
#include <limits.h>

int main()
{
    printf("long long max = %lld\n", LLONG_MAX);
    printf("long long min = %lld\n", LLONG_MIN);
    return 0;
}
long long max = 9223372036854775807
long long min = -9223372036854775808

ประมาณ 9 × 1018 เป็นเลขสิบเก้าหลัก ใช้ long long ต้องเปลี่ยนสามจุด

  1. ประกาศเป็น long long
  2. รับค่าด้วย %lld แทน %d (มาจาก long long decimal)
  3. พิมพ์ด้วย %lld แทน %d
#include <stdio.h>

int main()
{
    long long width, length;
    scanf("%lld %lld", &width, &length);
    long long area = width * length;
    printf("area = %lld\n", area);
    return 0;
}
50000 50000
area = 2500000000

กับดัก int คูณ int

หลายคนแก้แค่ตัวแปรที่เก็บผล แบบนี้

#include <stdio.h>

int main()
{
    int width, length;
    scanf("%d %d", &width, &length);
    long long area = width * length;
    printf("area = %lld\n", area);
    return 0;
}
50000 50000
area = -1794967296

ยังผิดอยู่ เพราะคอมพิวเตอร์ทำบรรทัดที่ 7 สองจังหวะเหมือนที่เห็นในหน้า 1.1 จังหวะแรกคำนวณ width * length ซึ่งเป็น int คูณ int ผลจึงเป็น int และล้นไปตั้งแต่ตรงนี้ จังหวะที่สองค่อยเอาค่าที่ผิดแล้วไปใส่กล่อง long long กล่องใหญ่แค่ไหนก็ช่วยไม่ได้

วิธีที่ปลอดภัยที่สุดตอนนี้คือ ประกาศตัวแปรที่จะเอามาคูณเป็น long long ตั้งแต่ต้น แบบโปรแกรมก่อนหน้า อีกวิธีคือแปลงชนิดเฉพาะจุด ซึ่งจะได้เรียนในหน้า 1.4

เมื่อไรต้องใช้ long long

โจทย์ทุกข้อบอกขอบเขตของข้อมูลเข้า เช่น 1 ≤ A, B ≤ 1000000000 ก่อนเขียนให้ถามตัวเองว่า ผลที่ใหญ่ที่สุดที่เป็นไปได้คือเท่าไร แล้วเทียบกับ 2 × 109

โจทย์ผลมากสุดใช้
A + B เมื่อ A, B ≤ 1092 × 109int พอดีๆ
A + B + C เมื่อ A, B, C ≤ 1093 × 109long long
A × B เมื่อ A, B ≤ 104108int
A × B เมื่อ A, B ≤ 1051010long long
A × B เมื่อ A, B ≤ 1091018long long ยังพอ
A × B × C เมื่อ A, B, C ≤ 1091027ไม่พอทั้งคู่ โจทย์แบบนี้มักให้ตอบเป็นเศษจากการหาร ซึ่งจะเรียนในบทที่ 12

ถ้าคำนวณแล้วไม่แน่ใจ ใช้ long long ไปเลย ข้อเสียคือใช้หน่วยความจำมากขึ้นเท่าตัว ซึ่งจะเริ่มมีผลตอนเก็บตัวเลขเป็นล้านตัวในบทที่ 5

ลองทำให้พัง

แบบที่ 1 เปลี่ยนชนิดแล้วลืมเปลี่ยน %d

ในโปรแกรม land ที่ใช้ long long แล้ว แก้บรรทัด printf กลับเป็น %d

land.c:8:21: warning: format '%d' expects argument of type 'int', but argument 2 has type 'long long int' [-Wformat=]
    8 |     printf("area = %d\n", area);
      |                    ~^     ~~~~
      |                     |     |
      |                     int   long long int
      |                    %lld

compiler บอกชัดว่า %d รอ int แต่ได้ long long int และแนะให้ใช้ %lld ถ้าดื้อรันต่อ จะได้ area = -1794967296 เหมือนตอนยังไม่แก้ เพราะ printf หยิบไปแค่ครึ่งเดียวของกล่อง

ลืมเปลี่ยน %d ใน scanf ก็ได้คำเตือนคล้ายกัน คือ expects argument of type 'int *', but argument 2 has type 'long long int *' แบบนี้อันตรายกว่า เพราะ scanf เขียนแค่ครึ่งกล่อง อีกครึ่งเป็นค่าขยะ บนเครื่องที่เขียนเอกสารนี้บังเอิญได้คำตอบถูก แต่เปลี่ยนเครื่องหรือเปลี่ยนโปรแกรมนิดเดียวอาจผิดได้ เห็น warning เรื่อง format เมื่อไร แก้ทันที

แบบที่ 2 ใส่เลขใหญ่เกินลงใน int ตรงๆ

#include <stdio.h>

int main()
{
    int population = 3000000000;
    printf("%d\n", population);
    return 0;
}
-1294967296

คอมไพล์ด้วย -Wall แล้วไม่มีคำเตือนเลย ทั้งที่สามพันล้านใส่ใน int ไม่ได้ -Wall ช่วยจับได้หลายอย่าง แต่ไม่ใช่ทุกอย่าง เรื่องขนาดของตัวเลข น้องต้องคิดเองจากขอบเขตในโจทย์

สรุป

  • int เก็บได้ตั้งแต่ -2147483648 ถึง 2147483647 ประมาณ 2 × 109
  • long long เก็บได้ประมาณ 9 × 1018 รับและพิมพ์ด้วย %lld
  • เกินขอบเขตเรียกว่า overflow ไม่มี error ไม่มี warning แค่ได้คำตอบผิด
  • int * int ล้นตั้งแต่ตอนคูณ เก็บผลใน long long ทีหลังไม่ช่วย
  • ก่อนเขียน คำนวณผลที่ใหญ่ที่สุดจากขอบเขตในโจทย์ เกิน 2 × 109 ใช้ long long