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) คือตัวแปรชนิดพิเศษที่เก็บ "เลขที่บ้าน" ของตัวแปรอื่น แทนที่จะเก็บค่า
int x = 42;
int *p = &x; // & = "ขอที่อยู่ของ x" → p เก็บ 0x0200
*p = 99; // * = "ไปที่ที่อยู่นี้" → เขียน 99 ลงบ้านของ x
Serial.println(x); // พิมพ์ 99int *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();อ่านจากขวาไปซ้าย
new Arduino()จองที่ใน RAM สร้างวัตถุขึ้นมา รันคอนสตรักเตอร์ (ซึ่งเรียกWire.begin(0x33)) แล้วคืน ที่อยู่ ของวัตถุนั้นArduino *pArduinoประกาศตัวชี้ชื่อpArduino(pย่อจาก pointer)=เก็บที่อยู่นั้นไว้ในpArduino
และใน protocol.hpp มีบรรทัด extern Arduino *pArduino; บอกไฟล์อื่นๆ ว่า "มีตัวชี้ชื่อนี้อยู่ที่อื่นแล้ว ใช้ได้เลย" ทุกไฟล์จึงเรียก 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
ลองทำ
- ใน
protocol.hppนับว่าคลาสArduinoมีเมธอดที่ขึ้นต้นด้วยmove_กี่ตัว (คำตอบ: 8 ตัว คือmove_forward_cm,backward,left,rightและเวอร์ชัน_fastอย่างละ 4) - เปลี่ยน
pArduino->set_height(120);ให้เขียนด้วยdroneตามที่DroneEasy.hppประกาศไว้ (คำตอบ:drone.setHeight(120);) - เพิ่มเมธอด
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