C ตัวแปรและชนิดข้อมูลพื้นฐาน



ประกาศตัวแปร

C ต้องประกาศ ชนิด ของตัวแปรเสมอ และประกาศก่อนใช้:

#include <stdio.h>

int main(void)
{
    int age = 30;
    double height = 172.5;
    char initial = 'S';
    int count;              // ประกาศไว้ก่อน ยังไม่กำหนดค่า

    count = 5;              // กำหนดค่าทีหลัง

    printf("%d %f %c %d\n", age, height, initial, count);
    return 0;
}

ชนิดข้อมูลพื้นฐาน

ชนิดเก็บอะไรขนาดทั่วไป (64-bit Linux)
charตัวอักษร / จำนวนเต็มเล็ก1 ไบต์
intจำนวนเต็ม4 ไบต์ (−2.1 พันล้าน ถึง 2.1 พันล้าน)
longจำนวนเต็มใหญ่8 ไบต์ (Linux 64-bit) / 4 ไบต์ (Windows)
long longจำนวนเต็มใหญ่มาก8 ไบต์
floatทศนิยม (ความละเอียดเดี่ยว)4 ไบต์ (~7 หลัก)
doubleทศนิยม (ความละเอียดคู่)8 ไบต์ (~15 หลัก)
_Bool (bool จาก <stdbool.h>)จริง/เท็จ1 ไบต์

ขนาดไม่ตายตัว — มาตรฐาน C รับประกันเป็น ช่วงค่าขั้นต่ำ ไม่ใช่จำนวนไบต์: char อย่างน้อย 8 บิต (sizeof(char) = 1 เสมอ แต่ “ไบต์” ไม่จำเป็นต้อง 8 บิต), int อย่างน้อย 16 บิต (±32767), long อย่างน้อย 32 บิต, long long อย่างน้อย 64 บิต เครื่อง/คอมไพเลอร์ต่างกันได้ ถ้าต้องการขนาดแน่นอนใช้ <stdint.h> (บทที่ 37)

sizeof

sizeof บอกขนาดเป็นไบต์ ปกติประเมินตอนคอมไพล์ (ยกเว้น operand เป็น VLA — อาร์เรย์ความยาวแปรผัน — จะประเมินตอนรัน):

#include <stdio.h>

int main(void)
{
    printf("char       : %zu ไบต์\n", sizeof(char));
    printf("int        : %zu ไบต์\n", sizeof(int));
    printf("long       : %zu ไบต์\n", sizeof(long));
    printf("float      : %zu ไบต์\n", sizeof(float));
    printf("double     : %zu ไบต์\n", sizeof(double));
    printf("int[10]    : %zu ไบต์\n", sizeof(int[10]));
    return 0;
}
char       : 1 ไบต์
int        : 4 ไบต์
long       : 8 ไบต์
float      : 4 ไบต์
double     : 8 ไบต์
int[10]    : 40 ไบต์

sizeof คืนชนิด size_t — ใช้ %zu ใน printf (ไม่ใช่ %d)

signed กับ unsigned

จำนวนเต็มเป็นได้ทั้งมีเครื่องหมายและไม่มี:

int a = -5;              // signed (ค่าเริ่มต้น)
unsigned int b = 10;     // 0 ถึง ~4.2 พันล้าน — ไม่มีค่าลบ

unsigned int c = 0;
c = c - 1;               // ไม่ error! วนไปเป็นค่าสูงสุด (4294967295)
printf("%u\n", c);       // 4294967295

unsigned มีประโยชน์เมื่อค่าเป็นลบไม่ได้จริง ๆ (ขนาด, จำนวน, index) แต่ระวังการลบที่ทำให้ต่ำกว่า 0 — มัน “วน” ไม่ error

char เป็นจำนวนเต็มด้วย

char เก็บเป็นรหัสตัวเลข — ทำเลขคณิตได้ (เครื่องทั่วไปใช้ ASCII ที่ 'A' = 65 แต่มาตรฐาน C ไม่ได้บังคับว่าต้องเป็น ASCII):

char letter = 'A';
printf("%d\n", letter);          // 65 บนเครื่องที่ใช้ ASCII
printf("%c\n", letter + 1);      // B
printf("%c\n", 'a' + 3);         // d

char digit = '7';
int value = digit - '0';         // 7  — เทคนิคแปลง char ตัวเลขเป็น int
printf("%d\n", value);

ตัวแปรที่ไม่ init มีค่าขยะ

ต่างจาก Python/Go — ตัวแปร local ที่ไม่กำหนดค่าเริ่มต้น ไม่ใช่ 0 แต่เป็นค่าที่ค้างอยู่ในหน่วยความจำตรงนั้น (ขยะ):

int x;
printf("%d\n", x);       // ค่าอะไรก็ได้ — และการอ่านมันคือ undefined behavior

กฎ: กำหนดค่าเริ่มต้นทุกตัวแปรตอนประกาศ int x = 0;

รับค่าจากผู้ใช้ด้วย scanf

#include <stdio.h>

int main(void)
{
    int age = 0;
    double weight = 0.0;

    printf("อายุ: ");
    if (scanf("%d", &age) != 1) {         // สังเกต & ข้างหน้าตัวแปร; เช็คค่าคืนเสมอ
        fprintf(stderr, "อ่านอายุไม่ได้\n");
        return 1;
    }

    printf("น้ำหนัก: ");
    if (scanf("%lf", &weight) != 1) {     // double ใช้ %lf ใน scanf (ต่างจาก printf ที่ใช้ %f)
        fprintf(stderr, "อ่านน้ำหนักไม่ได้\n");
        return 1;
    }

    printf("อายุ %d น้ำหนัก %.1f\n", age, weight);
    return 0;
}
อายุ: 25
น้ำหนัก: 60.5
อายุ 25 น้ำหนัก 60.5
  • ต้องมี & หน้าตัวแปร — scanf ต้องรู้ *ที่อยู่* ของตัวแปรเพื่อเขียนค่าลงไป (บทที่ 11 จะเข้าใจว่าทำไม) ลืม & = segfault
  • double ใช้ %lf ใน scanf แต่ %f ใน printf — น่าสับสน แต่จำไว้
  • scanf เปราะบางมาก — ถ้าผู้ใช้พิมพ์ตัวอักษรตอนคาด %d มันจะค้าง input ไว้ วน loop ไม่รู้จบ

วิธีที่ปลอดภัยกว่า: fgets + sscanf

char line[100];
int age;
char extra;

printf("อายุ: ");
// " %c" หลัง %d จับอักขระที่ไม่ใช่ช่องว่างตัวถัดไป — ถ้ามี แสดงว่า input เกิน ("25abc")
if (fgets(line, sizeof line, stdin) && sscanf(line, "%d %c", &age, &extra) == 1) {
    printf("อายุ %d\n", age);
} else {
    printf("อินพุตไม่ถูกต้อง\n");
}

อ่านทั้งบรรทัดด้วย fgets ก่อน แล้วค่อยแยกค่าด้วย sscanf — เช็คว่าคืน == 1 พอดี (ได้ age แต่ไม่มีอักขระเกิน); ถ้าได้ 2 แปลว่ามีขยะต่อท้ายเช่น 25abc การตรวจ input ตัวเลขแบบเข้มกว่านี้ใช้ strtol + เช็ค end pointer (บทที่ 35)

ค่าคงที่

const double PI = 3.14159265;
const int MAX_USERS = 100;

// PI = 3.0;             // error: assignment of read-only variable

#define BUFFER_SIZE 256   // มาโคร (บทที่ 25) — ไม่มีชนิด, แทนที่ token ก่อนคอมไพล์

const ดีกว่า #define สำหรับค่าคงที่ทั่วไป เพราะมีชนิดและ debugger เห็น

เรื่องที่คนมักพลาด

  • อ่านตัวแปรที่ยังไม่ init — ได้ค่าขยะ และเป็น UB กำหนดค่าเริ่มต้นเสมอ
  • scanf("%d", age) ลืม & — เขียนค่าลงที่อยู่มั่ว = segfault (บางทีก็รอด ทำให้ debug ยาก)
  • scanf("%f", &x) เมื่อ x เป็น double — ต้องเป็น %lf ใน scanf
  • ไม่เช็คค่าคืนของ scanf — อ่านไม่สำเร็จแล้วตัวแปรยังเป็นค่าขยะ ใช้ต่อ = ผลเพี้ยน/พฤติกรรมไม่แน่นอน เช็ค == จำนวน field เสมอ และกำหนดค่าเริ่มต้นให้ตัวแปร
  • unsigned ลบจนต่ำกว่า 0 — วนไปเป็นค่ามหาศาล ไม่ error ระวังใน loop for (unsigned i = n; i >= 0; i--) = infinite
  • หาร int สองตัวแล้วคาดว่าได้ทศนิยม5 / 2 เป็น 2 ไม่ใช่ 2.5 (บทหน้า)
  • %zu กับ sizeof — ใช้ %d แล้ว -Wall เตือน และผลอาจเพี้ยนบนบางเครื่อง
  • char เป็น signed หรือ unsigned ขึ้นกับเครื่อง — ถ้าเก็บค่า > 127 ใช้ unsigned char ให้ชัด

สรุป

  • ประกาศชนิดเสมอ, กำหนดค่าเริ่มต้นเสมอ (int x = 0;) — ไม่งั้นได้ค่าขยะ
  • char 1B, int 4B, double 8B (โดยทั่วไป — ไม่รับประกัน) เช็คด้วย sizeof + %zu
  • unsigned ไม่มีค่าลบ — การลบต่ำกว่า 0 จะวน ไม่ error
  • char ทำเลขคณิตได้ ('7' - '0' = 7)
  • scanf("%d", &x) — ต้องมี &, double ใช้ %lf; ปลอดภัยกว่าคือ fgets + sscanf
  • const สำหรับค่าคงที่

บทถัดไปว่าด้วยตัวดำเนินการ — เลขคณิต, integer division, bitwise และเรื่อง overflow