C ตัวดำเนินการ



เลขคณิต

#include <stdio.h>

int main(void)
{
    printf("%d\n", 7 + 3);     // 10
    printf("%d\n", 7 - 3);     // 4
    printf("%d\n", 7 * 3);     // 21
    printf("%d\n", 7 / 3);     // 2   ← integer division! เศษถูกตัดทิ้ง
    printf("%d\n", 7 % 3);     // 1   ← เศษจากการหาร (modulo)
    return 0;
}

integer division — กับดักอันดับหนึ่ง

7 / 3 ได้ 2 ไม่ใช่ 2.333 เพราะ ตัวถูกดำเนินการทั้งคู่เป็น int ผลลัพธ์จึงเป็น int เศษหายไป

ถ้าต้องการทศนิยม ต้องมีตัวใดตัวหนึ่งเป็น floating point:

printf("%d\n", 7 / 3);         // 2   ← หาร int ได้ int, เศษหายตั้งแต่ตรงนี้
printf("%f\n", 7.0 / 3);       // 2.333333  ← 7.0 เป็น double, ผลเป็น double
printf("%f\n", (double)7 / 3); // 2.333333  ← cast ตัวตั้งก่อนหาร
printf("%f\n", 7 / (double)3); // 2.333333  ← cast ตัวหารก็ได้

int a = 7, b = 3;
printf("%f\n", (double)a / b); // 2.333333

อย่าเขียน printf("%f\n", 7 / 3);7 / 3 เป็น int (ค่า 2) แต่ %f อ่าน argument เป็น double ชนิดไม่ตรงกับ format เป็น undefined behavior ไม่ใช่ “ได้ 2.000000” อย่างที่หลายคนคิด และ (double)(7 / 3) ก็ไม่ช่วย เพราะหาร int จบก่อนแล้วค่อย cast (ได้ 2.0) — ต้อง cast ตัวถูกดำเนินการ ตัวใดตัวหนึ่งก่อนหาร เช่น (double)7 / 3

% ใช้กับ ชนิดจำนวนเต็ม เท่านั้น (int, long, long long, unsigned ฯลฯ — ไม่ใช่ float/double) เครื่องหมายของผลตามตัวตั้ง: -7 % 3 = -1

กำหนดค่าและเพิ่ม/ลด

int n = 10;
n += 5;    // 15
n -= 3;    // 12
n *= 2;    // 24
n /= 4;    // 6
n %= 4;    // 2

int i = 5;
printf("%d\n", i++);   // 5 (ใช้ค่าเดิม แล้วเพิ่มเป็น 6)
printf("%d\n", ++i);   // 7 (เพิ่มเป็น 7 ก่อน แล้วใช้)

อย่าเขียน i++ หลายครั้งในนิพจน์เดียว เช่น a[i++] = i++; — เป็น undefined behavior (ลำดับการประเมินไม่แน่นอน)

เปรียบเทียบ — คืน int (0 หรือ 1)

C11/C17 มีชนิด _Bool (และ bool จาก <stdbool.h>) — แต่ตัวดำเนินการ เปรียบเทียบ คืนผลเป็น int: 1 (จริง) หรือ 0 (เท็จ) ไม่ใช่ _Bool

printf("%d\n", 5 > 3);     // 1
printf("%d\n", 5 == 3);    // 0
printf("%d\n", 5 != 3);    // 1
printf("%d\n", 5 >= 5);    // 1

ในเงื่อนไข: 0 = เท็จ, ทุกค่าที่ไม่ใช่ 0 = จริง

if (3) printf("จริง\n");        // พิมพ์ — 3 ไม่ใช่ 0
if (-1) printf("จริงด้วย\n");   // พิมพ์ — -1 ไม่ใช่ 0
if (0) printf("ไม่พิมพ์\n");

ตรรกะ

int age = 25;
int hasLicense = 1;

if (age >= 18 && hasLicense) { /* ทั้งคู่จริง */ }
if (age < 13 || age > 60)    { /* อย่างน้อยหนึ่งจริง */ }
if (!hasLicense)             { /* ไม่มีใบขับขี่ */ }

&& และ || เป็น short-circuit — ถ้า && เจอตัวแรกเท็จ ไม่ประเมินตัวหลัง (สำคัญเวลาเช็ค p != NULL && p->x)

bitwise — ทำงานระดับบิต

unsigned int a = 0b1100;   // 12  (0b คือ binary literal, ส่วนขยาย GCC)
unsigned int b = 0b1010;   // 10

printf("%u\n", a & b);     // 8   (1000) AND
printf("%u\n", a | b);     // 14  (1110) OR
printf("%u\n", a ^ b);     // 6   (0110) XOR
printf("%u\n", ~a & 0xF);  // 3   (0011) NOT
printf("%u\n", a << 2);    // 48  เลื่อนซ้าย 2 บิต = คูณ 4
printf("%u\n", a >> 1);    // 6   เลื่อนขวา 1 บิต = หาร 2

ใช้บ่อยกับ flag:

#define FLAG_READ  (1 << 0)   // 0b001
#define FLAG_WRITE (1 << 1)   // 0b010
#define FLAG_EXEC  (1 << 2)   // 0b100

int perms = FLAG_READ | FLAG_WRITE;      // เปิด read + write
if (perms & FLAG_WRITE) { /* มีสิทธิ์ write */ }
perms &= ~FLAG_WRITE;                     // ปิด write

operator precedence — ลำดับความสำคัญ

C มีตารางลำดับที่ยาวและจำยาก — เมื่อสงสัย ใส่วงเล็บ:

int x = 2 + 3 * 4;         // 14 (คูณก่อน)
int y = (2 + 3) * 4;       // 20

// จุดที่พลาดบ่อย:
if (a & 1 == 0)            // ผิด! == มาก่อน & กลายเป็น a & (1 == 0) = a & 0
if ((a & 1) == 0)          // ถูก

int z = a << 1 + 2;        // a << 3 (+ มาก่อน <<) — มักไม่ใช่ที่ตั้งใจ
int w = (a << 1) + 2;      // ที่ตั้งใจ

ลำดับในกลุ่มนี้ จากสูงไปต่ำ: + -<< >>< <= > >=== !=&^|

  • << >> สูงกว่า relational/equality — 1 << 2 == 4 คือ (1 << 2) == 4
  • แต่ << >> ต่ำกว่า + -a << 1 + 2 คือ a << 3
  • & ^ | ต่ำกว่า == !=a & 1 == 0 คือ a & (1 == 0) (อย่างในตัวอย่างข้างบน)

ผสม bitwise กับตัวดำเนินการอื่นเมื่อไร ใส่วงเล็บให้ชัดทุกครั้ง

signed integer overflow = undefined behavior

int max = 2147483647;      // INT_MAX
int oops = max + 1;        // UB! ไม่ใช่ "วนไปเป็น -2147483648" ตามที่หลายคนคิด

ในทางปฏิบัติมักได้ค่าลบ แต่มาตรฐานบอกว่าเป็น undefined behavior — คอมไพเลอร์ปรับแต่งโดยสมมติว่าไม่มีทางเกิด อาจทำให้โปรแกรมทำอะไรก็ได้ unsigned overflow ต่างกัน — มันวนแบบกำหนดไว้ (modulo 2ⁿ) ไม่ใช่ UB

จับด้วย -fsanitize=undefined (บทที่ 29) ป้องกันด้วยการเช็คก่อนบวก — ต้องดูทั้งฝั่งบวกล้น (overflow) และฝั่งลบล้น (underflow):

#include <limits.h>

// a + b จะเกินช่วง int หรือไม่
if ((b > 0 && a > INT_MAX - b) ||      // ล้นด้านบวก
    (b < 0 && a < INT_MIN - b)) {      // ล้นด้านลบ
    /* overflow — จัดการ */
} else {
    int sum = a + b;
}

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

  • 7 / 2 คาดว่าได้ 3.5 — ได้ 3 (integer division) ต้อง cast อย่างน้อยตัวหนึ่งเป็น double
  • printf("%f", นิพจน์ที่เป็น int) — ชนิดไม่ตรงกับ % = UB ไม่ใช่แค่ค่าเพี้ยน cast argument ให้ตรง format เสมอ
  • if (a & 1 == 0)== ผูกก่อน & ใส่วงเล็บ (a & 1) == 0
  • i = i++ + ++i; — UB, ผลลัพธ์ไม่แน่นอน อย่าแก้ตัวแปรเดียวหลายครั้งในนิพจน์เดียว
  • signed overflow “จะวน” — เป็น UB ไม่ใช่พฤติกรรมที่รับประกัน ต่างจาก unsigned
  • >> กับ signed ค่าลบ — implementation-defined (มัก arithmetic shift) ใช้ unsigned เมื่อทำ bit manipulation
  • = แทน == ในเงื่อนไขif (x = 5) กำหนดค่าแล้วเป็นจริงเสมอ คอมไพเลอร์เตือนด้วย -Wall
  • หาร/mod ด้วย 0 — UB (มัก crash ด้วย SIGFPE)

สรุป

  • / ระหว่าง int สองตัว = ตัดเศษ ต้อง cast เป็น double ถ้าต้องการทศนิยม, % ใช้กับชนิดจำนวนเต็มเท่านั้น
  • เปรียบเทียบคืน int (0/1) ไม่ใช่ _Bool; ในเงื่อนไข 0 = เท็จ, ที่เหลือ = จริง
  • && || short-circuit; bitwise & | ^ ~ << >> สำหรับงานระดับบิต/flag
  • ลำดับ: << >> สูงกว่าการเปรียบเทียบ แต่ & ^ | ต่ำกว่า == !=ใส่วงเล็บเมื่อผสม bitwise
  • signed overflow = UB (จับด้วย -fsanitize=undefined), unsigned วนแบบกำหนดไว้

บทถัดไปว่าด้วยเงื่อนไข — if/else/switch และเรื่อง fallthrough ของ switch