ชมรมโดรน

8. ตัวชี้ คลาส และ pArduino->take_off()

อธิบายจากศูนย์ว่า pArduino->take_off() คืออะไร: ตัวชี้ ลูกศร -> คลาส และวัตถุ

ยังไม่เคยเขียนโปรแกรม?

คำว่าตัวแปร ฟังก์ชัน วัตถุ และเมธอด อธิบายจากศูนย์ไว้ที่ อ่านโค้ดให้ออก อ่านหน้านั้นก่อนจะง่ายกว่ามาก

ตัวชี้และคลาส สรุปก่อนอ่าน

  • ตัวชี้ ตัวแปรที่เก็บ "เลขที่บ้าน" ของตัวแปรอื่น แทนที่จะเก็บค่าเอง
  • คลาส พิมพ์เขียว วัตถุ ของจริงที่สร้างจากพิมพ์เขียว เหมือนแบบแปลนรีโมทกับรีโมทตัวที่วางบนโต๊ะ
  • ในโค้ดโดรน pArduino->take_off() อ่านว่า "ตามเลขที่บ้านของ pArduino ไปกดปุ่ม take_off" ถ้าอ่านหน้าอื่นจนชิน หน้านี้คือคำตอบว่าทำไมต้องมีลูกศร

ทั้งโปรเจกต์ของชมรมมีบรรทัดหน้าตาแบบนี้เต็มไปหมด

pArduino->take_off();
pArduino->set_height(120);
pArduino->move_forward_cm(100);

หน้านี้จะแกะทีละชิ้นว่า pArduino, -> และ take_off() คืออะไร โดยเริ่มจากสิ่งที่เล็กที่สุด

1. ขั้นที่ 1: ตัวแปรมีที่อยู่

ตัวแปรทุกตัวอยู่ในหน่วยความจำ ณ ตำแหน่งหนึ่ง เหมือนบ้านมีเลขที่ ตัวแปร x = 42 ก็อยู่บ้านเลขที่ใดที่หนึ่ง เช่น 0x0200 (ตัวเลขนี้เป็นตัวอย่าง จริงๆ ชิปเลือกเอง)

ตัวชี้ (pointer) คือตัวแปรชนิดพิเศษที่เก็บ "เลขที่บ้าน" ของตัวแปรอื่น แทนที่จะเก็บค่า

ตัวชี้ (pointer) เก็บที่อยู่ของตัวแปรอื่นx เก็บค่า 42 ที่ที่อยู่หนึ่ง p เก็บที่อยู่นั้นและชี้ไปที่ xหน่วยความจำคือแถวช่องที่มีเลขที่อยู่x = 42ชนิด intที่อยู่ 0x0200p = 0x0200ชนิด int*ชี้ไปที่int* p = &x; // &x = "ที่อยู่ของ x"*p = 99; // เข้าไปที่ที่อยู่ที่ p ถือ แล้วเขียนทับ → x เป็น 99
x เก็บค่า 42 ที่ที่อยู่ 0x0200 ส่วน p เก็บเลขที่อยู่นั้น จึง 'ชี้' ไปที่ x
int x = 42;
int *p = &x;    // & = "ขอที่อยู่ของ x"  → p เก็บ 0x0200
*p = 99;        // * = "ไปที่ที่อยู่นี้"  → เขียน 99 ลงบ้านของ x
Serial.println(x);   // พิมพ์ 99
  • int *p ประกาศ p เป็นตัวชี้ไปยัง int
  • &x คือที่อยู่ของ x
  • *p คือ "ของที่อยู่ในบ้านที่ p ชี้"

ตัวอย่างชีวิตจริง: ที่อยู่กับตัวบ้าน

ถ้าคุณให้เพื่อน "เลขที่ 12 ซอย 3" เพื่อนยังต้องเดินไปที่บ้านถึงจะเจอตัวบ้าน ตัวชี้ก็เหมือนที่อยู่ ยังไม่ใช่ตัวของ และเพื่อนที่ได้ที่อยู่เดียวกันเข้าไปแก้ของในบ้านได้

2. ขั้นที่ 2: คลาสคือพิมพ์เขียว

เมื่อมีข้อมูลหลายชิ้นและฟังก์ชันที่ใช้กับข้อมูลนั้นอยู่ด้วยกัน เรารวมเป็น คลาส (class) คลาสเป็นแค่ "พิมพ์เขียว" ยังไม่ใช่ของจริง

// protocol.hpp (ย่อ)
class Arduino {
private:
  static char m_i2cData[6];
public:
  Arduino(void);
  void take_off(void);
  void set_height(uint16_t z);
  void move_forward_cm(uint16_t distance_cm, uint8_t speed = 2);
  ...
};
  • ข้อมูลในคลาส เช่น m_i2cData เรียกว่า สมาชิก (member)
  • ฟังก์ชันในคลาส เช่น take_off เรียกว่า เมธอด (method)
  • Arduino(void) ชื่อเหมือนคลาส คือ คอนสตรักเตอร์ (constructor) รันอัตโนมัติตอนสร้างของจริง
  • private: ภายนอกแตะไม่ได้ public: ภายนอกเรียกได้ เราเรียก take_off() ได้ แต่แตะ m_i2cData ตรงๆ ไม่ได้

ตัวอย่างชีวิตจริง: พิมพ์เขียวบ้านกับบ้านจริง

พิมพ์เขียวบอกว่าบ้านมีกี่ห้อง แต่ไม่มีใครอาศัยในกระดาษ ต้องสร้างบ้านขึ้นมา บ้านที่สร้างแล้วคือ วัตถุ (object) จากพิมพ์เขียวเดียวสร้างได้หลายหลัง

3. ขั้นที่ 3: สร้างวัตถุ

บรรทัดนี้ใน protocol.cpp สร้างวัตถุ Arduino จริงหนึ่งตัว

// protocol.cpp
Arduino *pArduino = new Arduino();

อ่านจากขวาไปซ้าย

  1. new Arduino() จองที่ใน RAM สร้างวัตถุขึ้นมา รันคอนสตรักเตอร์ (ซึ่งเรียก Wire.begin(0x33)) แล้วคืน ที่อยู่ ของวัตถุนั้น
  2. Arduino *pArduino ประกาศตัวชี้ชื่อ pArduino (p ย่อจาก pointer)
  3. = เก็บที่อยู่นั้นไว้ใน pArduino

และใน protocol.hpp มีบรรทัด extern Arduino *pArduino; บอกไฟล์อื่นๆ ว่า "มีตัวชี้ชื่อนี้อยู่ที่อื่นแล้ว ใช้ได้เลย" ทุกไฟล์จึงเรียก pArduino ได้

คลาส Arduino, วัตถุ และตัวชี้ pArduinoคลาสคือพิมพ์เขียว new Arduino() สร้างวัตถุหนึ่งตัวใน RAM pArduino เก็บที่อยู่ของวัตถุนั้น ลูกศร -> เรียกเมธอดclass Arduino (พิมพ์เขียว)ข้อมูล: m_i2cData[6], m_remoteKeyvoid take_off();void set_height(uint16_t z);void move_forward_cm(...);…อีกกว่า 30 เมธอดใน protocol.hppnewวัตถุ Arduino (ของจริง 1 ตัว)อยู่ที่ RAM ที่อยู่ 0x0300 (ตัวอย่าง)m_i2cData = [ ... ]m_remoteKey = NONE (0)สร้างตอนเริ่มโปรแกรม ที่ protocol.cpppArduino = 0x0300ตัวชี้ที่ถือที่อยู่ของวัตถุpArduino->take_off();"ไปที่วัตถุที่ pArduino ชี้อยู่แล้วสั่งเมธอด take_off ของมัน"-> คือ "ตามตัวชี้ไป แล้วเข้าถึงสมาชิก"
คลาสคือพิมพ์เขียว new สร้างวัตถุจริงหนึ่งตัวใน RAM pArduino เก็บที่อยู่ของวัตถุนั้น

4. ขั้นที่ 4: ลูกศร ->

มีตัวชี้ถึงวัตถุแล้ว ต้องการเรียกเมธอดของวัตถุ ถ้าถือวัตถุตรงๆ ใช้จุด . ถ้าถือแค่ตัวชี้ ใช้ลูกศร ->

pArduino->take_off();     // pArduino เป็นตัวชี้ → ใช้ ->
(*pArduino).take_off();   // เขียนยาวแบบเดียวกัน: ตามตัวชี้ไปที่วัตถุ แล้วใช้ .

สองบรรทัดนี้ทำเรื่องเดียวกัน -> คือทางลัดของ (*...). อ่านว่า "ตามตัวชี้ไปที่วัตถุ แล้วเรียกเมธอดนี้"

ดังนั้น pArduino->take_off(); อ่านเป็นภาษาคนได้ว่า

ไปที่วัตถุ Arduino ที่ pArduino ชี้อยู่ แล้วสั่งเมธอด take_off

และข้างในเมธอดนั้นทำแค่นี้ (จากหน้า I2C)

// protocol.cpp
void Arduino::take_off(void) {
  m_i2cData[0]++;
  m_i2cData[1] = Arduino::F_ARM;
}

Arduino::take_off อ่านว่า "เมธอด take_off ของคลาส Arduino" เครื่องหมาย :: บอกว่าฟังก์ชันนี้เป็นของคลาสไหน

5. วัตถุอีกตัว: drone

คลาสที่สองของโปรเจกต์คือ DroneEasy ห่อ pArduino อีกชั้นให้อ่านง่ายขึ้น

// DroneEasy.cpp
DroneEasy drone;                    // วัตถุธรรมดา ไม่ใช่ตัวชี้

void DroneEasy::takeOff() const { pArduino->take_off(); }

drone เป็นวัตถุโดยตรง จึงเรียกด้วยจุด drone.takeOff() ส่วนข้างในเรียก pArduino->take_off() ผ่านตัวชี้ เทียบสองบรรทัดนี้

เขียนความหมาย
drone.takeOff();วัตถุ drone โดยตรง ใช้ .
pArduino->take_off();ตัวชี้ไปยังวัตถุ ใช้ ->

ทั้งสองจบที่เมธอด Arduino::take_off() ตัวเดียวกัน ส่วน drone.raw() คืนวัตถุ Arduino ตัวนั้นออกมาเป็น reference (return *pArduino;) เผื่ออยากเรียกฟังก์ชันระดับล่างโดยไม่ต้องใช้ ->

6. ทำคลาสของตัวเอง

ตัวอย่างสั้นๆ ที่ทดสอบคอมไพล์สำหรับ Nano แล้ว แสดงตัวชี้กับคลาสในตัวเดียว

class Counter {
public:
  int value = 0;
  void add(int n) { value += n; }
};

Counter counter;                 // วัตถุ
Counter *pCounter = &counter;    // ตัวชี้ไปที่วัตถุเดียวกัน

void setup() {
  Serial.begin(9600);
  pCounter->add(5);              // เหมือน counter.add(5)
  counter.add(5);
  Serial.println(counter.value); // 10
}

void loop() {}

เรียกผ่านตัวชี้หรือผ่านชื่อวัตถุ ได้ผลต่อวัตถุตัวเดียวกัน จึงบวกไปรวม 10

ลองทำ

  1. ใน protocol.hpp นับว่าคลาส Arduino มีเมธอดที่ขึ้นต้นด้วย move_ กี่ตัว (คำตอบ: 8 ตัว คือ move_forward_cm, backward, left, right และเวอร์ชัน _fast อย่างละ 4)
  2. เปลี่ยน pArduino->set_height(120); ให้เขียนด้วย drone ตามที่ DroneEasy.hpp ประกาศไว้ (คำตอบ: drone.setHeight(120);)
  3. เพิ่มเมธอด reset() ให้ Counter ที่ตั้ง value = 0 แล้วลองเรียกผ่านทั้ง . และ ->

พังตรงไหนบ่อย

  • ใช้ . กับตัวชี้ pArduino.take_off() ขึ้น request for member ... in non-class type ต้องเปลี่ยนเป็น ->
  • ใช้ -> กับวัตถุ drone->takeOff() ผิดเหมือนกัน ต้องเป็น .
  • ตัวชี้ไม่ชี้ไปไหน ประกาศ Arduino *p; แล้วเรียก p->take_off() ก่อนกำหนดค่า ชิปเขียนมั่ว บอร์ดค้างหรือรีเซ็ต
  • ลืม & int *p = x; ผิด ต้อง &x
  • แตะสมาชิก private ขึ้น ... is private within this context

On this page