ขอให้เป็นวันที่ดี!
ความปรารถนาที่จะเขียนบทความนี้ปรากฏขึ้นหลังจากอ่านโพสต์ Overloading C++ Operators เนื่องจากไม่ได้กล่าวถึงหัวข้อสำคัญหลายหัวข้อในนั้น
สิ่งสำคัญที่สุดที่ต้องจำไว้ก็คือการที่ผู้ปฏิบัติงานทำงานหนักเกินไปนั้นมากกว่านั้น วิธีที่สะดวกการเรียกใช้ฟังก์ชัน ดังนั้นอย่ากังวลกับการโอเวอร์โหลดของโอเปอเรเตอร์ ควรใช้เมื่อจะทำให้การเขียนโค้ดง่ายขึ้นเท่านั้น แต่ก็ไม่มากจนทำให้อ่านยาก อย่างที่ทราบกันดีว่าโค้ดถูกอ่านบ่อยกว่าที่เขียนมาก และอย่าลืมว่าคุณจะไม่ได้รับอนุญาตให้โอเวอร์โหลดโอเปอเรเตอร์ควบคู่กับประเภทที่มีอยู่แล้วภายใน ความเป็นไปได้ของการโอเวอร์โหลดนั้นมีให้สำหรับประเภท/คลาสที่ผู้ใช้กำหนดเท่านั้น
ไวยากรณ์โอเวอร์โหลด
ไวยากรณ์การโอเวอร์โหลดของตัวดำเนินการคล้ายกันมากกับการกำหนดฟังก์ชันที่เรียกว่า ตัวดำเนินการ@ โดยที่ @ คือตัวระบุตัวดำเนินการ (เช่น +, -,<<, >- ลองพิจารณาดู ตัวอย่างที่ง่ายที่สุด:คลาสจำนวนเต็ม ( ส่วนตัว: int value; สาธารณะ: Integer(int i): ค่า(i) () const ตัวดำเนินการจำนวนเต็ม+(const Integer& rv) const ( return (ค่า + rv.value); ) );
ใน ในกรณีนี้ตัวดำเนินการจะถูกจัดกรอบเป็นสมาชิกของคลาส อาร์กิวเมนต์จะกำหนดค่าที่อยู่ทางด้านขวาของตัวดำเนินการ โดยทั่วไป มีสองวิธีหลักในการโอเวอร์โหลดตัวดำเนินการ: ฟังก์ชั่นระดับโลกเป็นมิตรกับคลาส หรือฟังก์ชันอินไลน์ของคลาสเอง เราจะพิจารณาว่าวิธีใดดีกว่าสำหรับตัวดำเนินการตัวใดในตอนท้ายของหัวข้อ
ในกรณีส่วนใหญ่ ตัวดำเนินการ (ยกเว้นแบบมีเงื่อนไข) จะส่งคืนออบเจ็กต์ หรือการอ้างอิงถึงประเภทที่มีอาร์กิวเมนต์อยู่ (หากประเภทแตกต่างกัน คุณจะต้องตัดสินใจว่าจะตีความผลลัพธ์ของการประเมินตัวดำเนินการอย่างไร)
การโอเวอร์โหลดตัวดำเนินการ unary
ลองดูตัวอย่างการโอเวอร์โหลดตัวดำเนินการเอกนารีสำหรับคลาส Integer ที่กำหนดไว้ข้างต้น ในเวลาเดียวกัน เรามากำหนดมันในรูปแบบของฟังก์ชันที่เป็นมิตรและพิจารณาตัวดำเนินการลดและเพิ่ม:คลาสจำนวนเต็ม ( ส่วนตัว: int value; สาธารณะ: Integer(int i): ค่า(i) () //unary + friend const Integer& โอเปอเรเตอร์+(const Integer& i); //unary - เพื่อน const โอเปอเรเตอร์จำนวนเต็ม-(const Integer& i) ; // เพิ่มคำนำหน้าเพื่อน const จำนวนเต็ม& ตัวดำเนินการ ++ (จำนวนเต็ม & i); // เพื่อนที่เพิ่มขึ้น postfix const ตัวดำเนินการจำนวนเต็ม ++ (จำนวนเต็ม & i, int);เพื่อน const ตัวดำเนินการจำนวนเต็ม--(Integer& i, int); - //unary plus ไม่ทำอะไรเลย const Integer& โอเปอเรเตอร์+(const Integer& i) ( return i.value; ) const Integer โอเปอเรเตอร์-(const Integer& i) ( return Integer(-i.value); ) // เวอร์ชันคำนำหน้าส่งคืนค่าหลังจากเพิ่มค่า const Integer& โอเปอเรเตอร์++(Integer& i) ( i.value++; return i; ) //เวอร์ชัน postfix ส่งคืนค่าก่อนการเพิ่ม const ตัวดำเนินการจำนวนเต็ม++(Integer& i, int) ( Integer oldValue(i.value); i.value++; return oldValue; ) // เวอร์ชันคำนำหน้าส่งคืน ค่าหลังการลดค่า const Integer& ตัวดำเนินการ--(Integer& i) ( i.value--; return i; ) // เวอร์ชัน postfix ส่งคืนค่าก่อนการลดค่า const ตัวดำเนินการจำนวนเต็ม--(Integer& i, int) ( Integer oldValue(i. ค่า) i .value--;
ตอนนี้คุณรู้แล้วว่าคอมไพลเลอร์แยกความแตกต่างระหว่างการลดและการเพิ่มขึ้นเวอร์ชันคำนำหน้าและ postfix ได้อย่างไร ในกรณีที่เห็นนิพจน์ ++i ตัวดำเนินการฟังก์ชัน++(a) จะถูกเรียก หากเห็น i++ แสดงว่ามีการเรียกตัวดำเนินการ++(a, int) นั่นคือ มีการเรียกฟังก์ชันโอเปอเรเตอร์++ ที่โอเวอร์โหลด และนี่คือสิ่งที่พารามิเตอร์จำลอง int ในเวอร์ชัน postfix ใช้สำหรับ
ตัวดำเนินการไบนารี
ลองดูที่ไวยากรณ์สำหรับการโอเวอร์โหลดตัวดำเนินการไบนารี ลองโอเวอร์โหลดโอเปอเรเตอร์หนึ่งตัวที่ส่งคืนค่า l หนึ่งตัว ตัวดำเนินการแบบมีเงื่อนไขและตัวดำเนินการหนึ่งรายที่สร้างค่าใหม่ (มากำหนดกันทั่วโลก):คลาสจำนวนเต็ม ( ส่วนตัว: int value; สาธารณะ: จำนวนเต็ม(int i): ค่า(i) () เพื่อน const ตัวดำเนินการจำนวนเต็ม+(const Integer& ซ้าย, const Integer& right); เพื่อน Integer& ตัวดำเนินการ+=(จำนวนเต็ม& ซ้าย, const Integer& ขวา); เพื่อน ตัวดำเนินการบูล==(const Integer& ซ้าย, const Integer& ขวา); const Integer โอเปอเรเตอร์+(const Integer& left, const Integer& right) ( return Integer(left.value + right.value); ) Integer& โอเปอเรเตอร์+=(Integer& left, const Integer& right) ( left.value += right.value; return left; ) ตัวดำเนินการบูล==(const Integer& left, const Integer& right) ( return left.value == right.value; )
ในตัวอย่างทั้งหมดเหล่านี้ โอเปอเรเตอร์มีการโอเวอร์โหลดสำหรับประเภทเดียวกัน อย่างไรก็ตาม สิ่งนี้ไม่จำเป็น คุณสามารถยกตัวอย่างการเพิ่มของเรามากเกินไป พิมพ์จำนวนเต็มและกำหนดไว้ในลักษณะลอยตัว
อาร์กิวเมนต์และค่าที่ส่งคืน
อย่างที่คุณเห็นตัวอย่างที่ใช้ วิธีต่างๆการส่งผ่านอาร์กิวเมนต์ไปยังฟังก์ชันและส่งกลับค่าตัวดำเนินการ- ถ้าอาร์กิวเมนต์ไม่ได้รับการแก้ไขโดยตัวดำเนินการ ในกรณี เช่น เครื่องหมายบวกแบบเอกนารี จะต้องส่งผ่านเป็นการอ้างอิงไปยังค่าคงที่ โดยทั่วไปนี่เป็นเรื่องจริงสำหรับเกือบทุกคน ตัวดำเนินการทางคณิตศาสตร์(การบวก ลบ คูณ...)
- ประเภทของค่าตอบแทนจะขึ้นอยู่กับลักษณะของตัวดำเนินการ หากตัวดำเนินการต้องส่งคืนค่าใหม่ ก็จำเป็นต้องสร้าง วัตถุใหม่(เช่นในกรณีของไบนารี่พลัส) หากคุณต้องการป้องกันไม่ให้วัตถุถูกแก้ไขเป็นค่า l คุณจะต้องคืนค่าวัตถุนั้นเป็นค่าคงที่
- ผู้ดำเนินการที่ได้รับมอบหมายจะต้องส่งคืนการอ้างอิงไปยังองค์ประกอบที่เปลี่ยนแปลง นอกจากนี้ หากคุณต้องการใช้ตัวดำเนินการกำหนดค่าในโครงสร้าง เช่น (x=y).f() โดยที่ฟังก์ชัน f() ถูกเรียกใช้สำหรับตัวแปร x หลังจากกำหนดให้กับ y แล้ว อย่าส่งคืนการอ้างอิงไปยังค่าคงที่ เพียงส่งคืนข้อมูลอ้างอิง
- ตัวดำเนินการเชิงตรรกะควรคืนค่า int ที่แย่ที่สุด และค่าบูลที่ดีที่สุด
การเพิ่มประสิทธิภาพมูลค่าผลตอบแทน
เมื่อสร้างออบเจ็กต์ใหม่และส่งคืนจากฟังก์ชัน คุณควรใช้สัญลักษณ์ที่คล้ายกับตัวอย่างตัวดำเนินการไบนารีบวกที่อธิบายไว้ข้างต้นส่งกลับจำนวนเต็ม (left.value + right.value);
พูดตามตรง ฉันไม่รู้ว่าสถานการณ์ใดที่เกี่ยวข้องกับ C++11 ข้อโต้แย้งเพิ่มเติมทั้งหมดนั้นใช้ได้กับ C++98
เมื่อมองแวบแรก สิ่งนี้จะคล้ายกับไวยากรณ์สำหรับการสร้างวัตถุชั่วคราว นั่นคือ ดูเหมือนว่าจะไม่มีความแตกต่างระหว่างโค้ดด้านบนกับสิ่งนี้:
อุณหภูมิจำนวนเต็ม (left.value + right.value); อุณหภูมิกลับ;
แต่ในความเป็นจริง ในกรณีนี้ ตัวสร้างจะถูกเรียกในบรรทัดแรก จากนั้นตัวสร้างการคัดลอกจะถูกเรียก ซึ่งจะคัดลอกวัตถุ จากนั้นเมื่อคลี่คลายสแต็ก ตัวทำลายล้างจะถูกเรียก เมื่อใช้รายการแรก คอมไพลเลอร์จะสร้างอ็อบเจ็กต์ในหน่วยความจำที่ต้องการคัดลอกในตอนแรก ซึ่งจะช่วยประหยัดการเรียกไปยังตัวสร้างการคัดลอกและตัวทำลายล้าง
ผู้ดำเนินการพิเศษ
C++ มีตัวดำเนินการที่มีไวยากรณ์เฉพาะและวิธีการโอเวอร์โหลด ตัวอย่างเช่น ตัวดำเนินการจัดทำดัชนี มันถูกกำหนดให้เป็นสมาชิกของคลาสเสมอ และเนื่องจากอ็อบเจ็กต์ที่ถูกจัดทำดัชนีมีวัตถุประสงค์ให้ทำงานเหมือนอาร์เรย์ จึงควรส่งคืนการอ้างอิงตัวดำเนินการจุลภาค
ตัวดำเนินการ "พิเศษ" ยังรวมถึงตัวดำเนินการลูกน้ำด้วย มันถูกเรียกบนวัตถุที่มีเครื่องหมายจุลภาคอยู่ข้างๆ (แต่ไม่ถูกเรียกในรายการอาร์กิวเมนต์ของฟังก์ชัน) การสร้างกรณีการใช้งานที่มีความหมายสำหรับผู้ปฏิบัติงานรายนี้ไม่ใช่เรื่องง่าย Habrowser AxisPod ในความคิดเห็นต่อบทความก่อนหน้าเกี่ยวกับการโอเวอร์โหลดพูดคุยเกี่ยวกับสิ่งหนึ่งตัวดำเนินการอ้างอิงแบบพอยน์เตอร์
การโอเวอร์โหลดตัวดำเนินการเหล่านี้สามารถพิสูจน์ได้สำหรับคลาสตัวชี้อัจฉริยะ โอเปอเรเตอร์นี้จำเป็นต้องถูกกำหนดให้เป็นฟังก์ชันคลาส และมีข้อ จำกัด บางประการ: ต้องส่งคืนอ็อบเจ็กต์ (หรือข้อมูลอ้างอิง) หรือตัวชี้ที่อนุญาตให้เข้าถึงอ็อบเจ็กต์ผู้ดำเนินการมอบหมาย
ตัวดำเนินการมอบหมายงานจำเป็นต้องถูกกำหนดให้เป็นฟังก์ชันคลาสเนื่องจากมีการเชื่อมโยงภายในกับวัตถุทางด้านซ้ายของ "=" การกำหนดตัวดำเนินการที่ได้รับมอบหมายใน ทั่วโลกจะทำให้สามารถแทนที่พฤติกรรมเริ่มต้นของตัวดำเนินการ "=" ได้ ตัวอย่าง:class Integer ( private: int value; public: Integer(int i): value(i) () Integer& โอเปอเรเตอร์=(const Integer& right) ( //ตรวจสอบการกำหนดตัวเองถ้า (this == &right) ( return *this; ) value = right.value; return *สิ่งนี้;
อย่างที่คุณเห็นที่จุดเริ่มต้นของฟังก์ชันจะมีการตรวจสอบการกำหนดตนเอง โดยทั่วไป ในกรณีนี้ การจัดสรรตนเองนั้นไม่เป็นอันตราย แต่สถานการณ์ก็ไม่ได้ง่ายเสมอไป ตัวอย่างเช่น หากวัตถุมีขนาดใหญ่ คุณสามารถใช้เวลากับมันได้มาก การคัดลอกที่ไม่จำเป็นหรือเมื่อทำงานกับพอยน์เตอร์
ตัวดำเนินการที่ไม่โอเวอร์โหลด
โอเปอเรเตอร์บางตัวใน C++ ไม่ได้โอเวอร์โหลดเลย เห็นได้ชัดว่าสิ่งนี้เกิดขึ้นด้วยเหตุผลด้านความปลอดภัย- โอเปอเรเตอร์การเลือกสมาชิกคลาส "."
- ตัวดำเนินการสำหรับการยกเลิกการอ้างอิงตัวชี้ไปยังสมาชิกคลาส ".*"
- C++ ไม่มีตัวดำเนินการยกกำลัง (เช่นใน Fortran) "**"
- ห้ามมิให้กำหนดตัวดำเนินการของคุณเอง (อาจมีปัญหาในการกำหนดลำดับความสำคัญ)
- ลำดับความสำคัญของผู้ปฏิบัติงานไม่สามารถเปลี่ยนแปลงได้
Rob Murray ได้ให้คำจำกัดความไว้ในหนังสือ C++ Strategies and Tactics ของเขา คำแนะนำต่อไปนี้โดยเลือกแบบฟอร์มผู้ประกอบการ:
ทำไมจึงเป็นเช่นนี้? ประการแรก โอเปอเรเตอร์บางตัวจะถูกจำกัดในตอนแรก โดยทั่วไป หากไม่มีความแตกต่างทางความหมายในการกำหนดตัวดำเนินการ ก็ควรออกแบบให้เป็นฟังก์ชันคลาสเพื่อเน้นการเชื่อมต่อ จะดีกว่า นอกจากนี้ฟังก์ชันจะอยู่ในบรรทัดด้วย นอกจากนี้ บางครั้งอาจจำเป็นต้องแสดงตัวถูกดำเนินการทางด้านซ้ายเป็นอ็อบเจ็กต์ของคลาสอื่น น่าจะเป็นที่สุด ตัวอย่างที่ส่องแสง- แทนที่<< и >> สำหรับสตรีม I/O
วรรณกรรม
Bruce Eckel - ปรัชญา C++ รู้เบื้องต้นเกี่ยวกับมาตรฐาน C ++แท็ก: เพิ่มแท็ก
เราได้กล่าวถึงพื้นฐานของการใช้โอเปอเรเตอร์โอเวอร์โหลดแล้ว ในเอกสารนี้ เราจะนำเสนอตัวดำเนินการ C++ ที่โอเวอร์โหลดให้คุณทราบ แต่ละส่วนมีลักษณะเฉพาะด้วยความหมายเช่น พฤติกรรมที่คาดหวัง นอกจากนี้ จะแสดงวิธีทั่วไปในการประกาศและใช้งานตัวดำเนินการ
ในตัวอย่างโค้ด X ระบุประเภทที่ผู้ใช้กำหนดซึ่งตัวดำเนินการจะถูกนำไปใช้ T เป็นประเภททางเลือก ทั้งที่ผู้ใช้กำหนดหรือในตัว พารามิเตอร์ของตัวดำเนินการไบนารีจะมีชื่อว่า lhs และ rhs หากมีการประกาศโอเปอเรเตอร์เป็นวิธีการเรียน การประกาศจะขึ้นต้นด้วย X::
โอเปอเรเตอร์=
- ความหมายจากขวาไปซ้าย: ไม่เหมือนกับตัวดำเนินการส่วนใหญ่ ตัวดำเนินการ= มีความเชื่อมโยงที่ถูกต้อง กล่าวคือ a = b = c หมายถึง a = (b = c)
สำเนา
- ความหมาย: การมอบหมาย a = b . ค่าหรือสถานะของ b ถูกส่งผ่านไปยัง a นอกจากนี้ การอ้างอิงถึง a จะถูกส่งกลับ สิ่งนี้ช่วยให้คุณสร้างเชนเช่น c = a = b
- โฆษณาทั่วไป: X& X::operator= (X const& rhs) อาร์กิวเมนต์ประเภทอื่นๆ เป็นไปได้ แต่ไม่ได้ใช้บ่อยนัก
- การใช้งานทั่วไป: X& X::operator= (X const& rhs) ( if (this != &rhs) ( //ทำการคัดลอกองค์ประกอบอย่างชาญฉลาด หรือ: X tmp(rhs); //copy Constactor swap(tmp); ) return *this; )
ย้าย (ตั้งแต่ C ++ 11)
- ความหมาย: การมอบหมาย a = temporary() . ค่าหรือสถานะของค่าที่ถูกต้องถูกกำหนดให้กับ a โดยการย้ายเนื้อหา การอ้างอิงถึง a จะถูกส่งกลับ
- : X& X::operator= (X&& rhs) ( //รับความกล้าจาก rhs return *this; )
- คอมไพเลอร์สร้างขึ้นโอเปอเรเตอร์= : คอมไพเลอร์สามารถสร้างโอเปอเรเตอร์นี้ได้เพียงสองประเภทเท่านั้น หากไม่ได้ประกาศตัวดำเนินการในคลาส คอมไพลเลอร์จะพยายามสร้างสำเนาสาธารณะและย้ายตัวดำเนินการ ตั้งแต่ C++11 คอมไพลเลอร์สามารถสร้างตัวดำเนินการเริ่มต้นได้: X& X::operator= (X const& rhs) = default;
คำสั่งที่สร้างขึ้นเป็นเพียงการคัดลอก/ย้าย องค์ประกอบที่ระบุหากอนุญาตให้ดำเนินการดังกล่าวได้
ตัวดำเนินการ +, -, *, /, %
- ความหมาย: การดำเนินการบวก ลบ คูณ หาร หารด้วยเศษ ออบเจ็กต์ใหม่ที่มีค่าผลลัพธ์จะถูกส่งกลับ
- การประกาศและการนำไปใช้โดยทั่วไป: ตัวดำเนินการ X+ (X const lhs, X const rhs) ( X tmp(lhs); tmp += rhs; return tmp; )
โดยทั่วไป ถ้ามีตัวดำเนินการ+ ก็สมเหตุสมผลที่จะโอเวอร์โหลดตัวดำเนินการ+= เพื่อใช้สัญลักษณ์ a += b แทน a = a + b หากโอเปอเรเตอร์+= ไม่ได้โอเวอร์โหลด การใช้งานจะมีลักษณะดังนี้:
X โอเปอเรเตอร์+ (X const& lhs, X const& rhs) ( // สร้างอ็อบเจ็กต์ใหม่ที่แสดงผลรวมของ lhs และ rhs: return lhs.plus(rhs); )
ตัวดำเนินการ Unary+, —
- ความหมาย: เครื่องหมายบวกหรือลบ โอเปอเรเตอร์+ มักจะไม่ทำอะไรเลย จึงไม่ค่อยได้ใช้ โอเปอเรเตอร์- ส่งคืนอาร์กิวเมนต์ที่มีเครื่องหมายตรงกันข้าม
- การประกาศและการนำไปใช้โดยทั่วไป: X X::operator- () const ( return /* สำเนาเชิงลบของ *this */; ) X X::operator+ () const ( return *this; )
ตัวดำเนินการ<<, >>
- ความหมาย: ในประเภทบิวท์อิน ตัวดำเนินการจะถูกใช้เพื่อเลื่อนบิตอาร์กิวเมนต์ด้านซ้าย การโอเวอร์โหลดตัวดำเนินการเหล่านี้ด้วยความหมายนี้เป็นสิ่งที่หาได้ยาก สิ่งเดียวที่อยู่ในใจคือ std::bitset อย่างไรก็ตาม มีการนำความหมายใหม่มาใช้สำหรับการทำงานกับสตรีม และคำสั่ง I/O ที่โอเวอร์โหลดเป็นเรื่องปกติ
- การประกาศและการนำไปใช้โดยทั่วไป: เนื่องจากคุณไม่สามารถเพิ่มวิธีการให้กับคลาส iostream มาตรฐานได้ ตัวดำเนินการ shift สำหรับคลาสที่คุณกำหนดจะต้องโอเวอร์โหลดเป็นฟังก์ชันฟรี: ostream& ตัวดำเนินการ<< (ostream& os, X const& x) {
os << /* the formatted data of rhs you want to print */;
return os;
}
istream& operator>> (istream& คือ, X& x) ( SomeData sd; SomeMoreData smd; if (คือ >> sd >> smd) ( rhs.setSomeData(sd); rhs.setSomeMoreData(smd); ) ส่งคืน lhs; )
นอกจากนี้ ประเภทของตัวถูกดำเนินการทางด้านซ้ายอาจเป็นคลาสใดๆ ที่ควรทำงานเหมือนกับอ็อบเจ็กต์ I/O กล่าวคือ ตัวถูกดำเนินการที่ถูกต้องอาจเป็นชนิดในตัว
MyIO& MyIO::ตัวดำเนินการ<< (int rhs) { doYourThingWith(rhs); return *this; }
ตัวดำเนินการไบนารี&, |, ^
- ความหมาย: การดำเนินการบิต “และ”, “หรือ”, “เฉพาะหรือ” ตัวดำเนินการเหล่านี้มีการโอเวอร์โหลดน้อยมาก อีกครั้ง std::bitset ตัวอย่างเดียวเท่านั้น
โอเปอเรเตอร์+=, -=, *=, /=, %=
- ความหมาย: a += b มักจะหมายถึงเช่นเดียวกับ a = a + b พฤติกรรมของผู้ปฏิบัติงานรายอื่นก็คล้ายคลึงกัน
- คำจำกัดความทั่วไปและการนำไปใช้: เนื่องจากการดำเนินการแก้ไขตัวถูกดำเนินการด้านซ้าย จึงไม่แนะนำให้เลือกประเภทที่ซ่อนอยู่ ดังนั้นตัวดำเนินการเหล่านี้จะต้องโอเวอร์โหลดเป็นวิธีการเรียน X& X::operator+= (X const& rhs) ( //ใช้การเปลี่ยนแปลงกับ *this return *this; )
ตัวดำเนินการ&=, |=, ^=,<<=, >>=
- ความหมาย: คล้ายกับตัวดำเนินการ+= แต่สำหรับการดำเนินการเชิงตรรกะ โอเปอเรเตอร์เหล่านี้มีการโอเวอร์โหลดไม่มากเท่ากับโอเปอเรเตอร์| ฯลฯ ตัวดำเนินการ<<= и operator>>= ไม่ได้ใช้สำหรับการดำเนินการ I/O เนื่องจากตัวดำเนินการ<< и operator>> เปลี่ยนอาร์กิวเมนต์ด้านซ้ายแล้ว
โอเปอเรเตอร์==, !=
- ความหมาย: ทดสอบความเท่าเทียมกัน/ความไม่เท่าเทียมกัน ความหมายของความเท่าเทียมกันนั้นแตกต่างกันไปตามชนชั้น ไม่ว่าในกรณีใด ให้พิจารณาคุณสมบัติของความเท่าเทียมกันดังต่อไปนี้:
- การสะท้อนกลับเช่น ก == ก
- สมมาตรเช่น ถ้า a == b แล้ว b == a
- การสัญจร ได้แก่ ถ้า a == b และ b == c แล้ว a == c
- การประกาศและการนำไปใช้โดยทั่วไป: ตัวดำเนินการ bool== (X const& lhs, X cosnt& rhs) ( return /* ตรวจสอบสิ่งที่หมายถึงความเท่าเทียมกัน */ ) ตัวดำเนินการ bool!= (X const& lhs, X const& rhs) ( return !(lhs == rhs); )
การใช้งานครั้งที่สองของโอเปอเรเตอร์!= หลีกเลี่ยงการทำซ้ำโค้ดและกำจัดความคลุมเครือที่เป็นไปได้เกี่ยวกับสองอ็อบเจ็กต์ใดๆ
ตัวดำเนินการ<, <=, >, >=
- ความหมาย: ตรวจสอบอัตราส่วน (มาก น้อย ฯลฯ) โดยทั่วไปจะใช้หากมีการกำหนดลำดับขององค์ประกอบโดยไม่ซ้ำกัน นั่นก็คือ วัตถุที่ซับซ้อนไม่มีเหตุผลที่จะเปรียบเทียบกับคุณลักษณะหลายประการ
- การประกาศและการนำไปใช้โดยทั่วไป: ตัวดำเนินการบูล< (X const& lhs, X const& rhs) {
return /* compare whatever defines the order */
}
bool operator>(X const& lhs, X const& rhs) ( กลับ rhs< lhs;
}
กำลังดำเนินการ> ใช้ตัวดำเนินการ< или наоборот обеспечивает однозначное определение. operator<= может быть реализован по-разному, в зависимости от ситуации . В частности, при отношении строго порядка operator== можно реализовать лишь через operator< :
ตัวดำเนินการบูล== (X const& lhs, X const& rhs) ( return !(lhs< rhs) && !(rhs < lhs); }
ตัวดำเนินการ++, –
- ความหมาย: a++ (หลังการเพิ่มขึ้น) จะเพิ่มค่า 1 และส่งคืน เก่าความหมาย. ++a (การเพิ่มขึ้นล่วงหน้า) ส่งคืน ใหม่ความหมาย. ด้วยตัวดำเนินการลด -- ทุกอย่างจะคล้ายกัน
- การประกาศและการนำไปใช้โดยทั่วไป: X& X::operator++() ( //preincreator /* เพิ่มขึ้น เช่น *this += 1*/; return *this; ) X X::operator++(int) ( //postincreation X oldValue(*this); + +(*สิ่งนี้); ส่งคืนค่าเก่า;
ตัวดำเนินการ ()
- ความหมาย: การดำเนินการของวัตถุฟังก์ชัน (functor) โดยทั่วไปแล้วจะไม่ใช้เพื่อแก้ไขวัตถุ แต่เพื่อใช้เป็นฟังก์ชัน
- ไม่มีข้อจำกัดเกี่ยวกับพารามิเตอร์: ไม่เหมือนกับตัวดำเนินการก่อนหน้านี้ ในกรณีนี้ ไม่มีข้อจำกัดเกี่ยวกับจำนวนและประเภทของพารามิเตอร์ ตัวดำเนินการสามารถโอเวอร์โหลดได้เฉพาะวิธีการเรียนเท่านั้น
- ตัวอย่างโฆษณา: Foo X::operator() (Bar br, Baz const& bz);
ตัวดำเนินการ
- ความหมาย: เข้าถึงองค์ประกอบของอาร์เรย์หรือคอนเทนเนอร์ เช่น ใน std::vector , std::map , std::array
- ประกาศ: ประเภทพารามิเตอร์สามารถเป็นอะไรก็ได้ ประเภทการส่งคืนมักจะอ้างอิงถึงสิ่งที่เก็บไว้ในคอนเทนเนอร์ บ่อยครั้งที่ตัวดำเนินการโอเวอร์โหลดในสองเวอร์ชัน const และ non-const: Element_t& X::operator(Index_t const& index); const Element_t& X::ตัวดำเนินการ (Index_t const& ดัชนี) const;
โอเปอเรเตอร์!
- ความหมาย: การปฏิเสธในแง่ตรรกะ
- การประกาศและการนำไปใช้โดยทั่วไป: bool X::operator!() const ( return !/*การประเมินบางอย่างของ *this*/; )
บูลตัวดำเนินการที่ชัดเจน
- ความหมาย: ใช้ในบริบทเชิงตรรกะ ส่วนใหญ่มักใช้กับพอยน์เตอร์อัจฉริยะ
- การนำไปปฏิบัติ: ชัดเจน X::operator bool() const ( return /* ถ้าเป็นจริงหรือเท็จ */; )
ตัวดำเนินการ&&, ||
- ความหมาย: ตรรกะ "และ", "หรือ" ตัวดำเนินการเหล่านี้ถูกกำหนดไว้สำหรับประเภทบูลีนในตัวเท่านั้น และทำงานแบบ Lazy นั่นคือ อาร์กิวเมนต์ที่สองจะได้รับการพิจารณาก็ต่อเมื่ออาร์กิวเมนต์แรกไม่ได้กำหนดผลลัพธ์ เมื่อโอเวอร์โหลด คุณสมบัตินี้จะหายไป ดังนั้นตัวดำเนินการเหล่านี้จึงไม่ค่อยมีโอเวอร์โหลด
ตัวดำเนินการ Unary*
- ความหมาย: การอ้างอิงตัวชี้ โดยทั่วไปแล้วจะโอเวอร์โหลดสำหรับคลาสที่มีตัวชี้และตัววนซ้ำอัจฉริยะ ส่งกลับการอ้างอิงไปยังจุดที่วัตถุชี้
- การประกาศและการนำไปใช้โดยทั่วไป: T& X::operator*() const ( return *_ptr; )
โอเปอเรเตอร์->
- ความหมาย: เข้าถึงฟิลด์ด้วยตัวชี้ เช่นเดียวกับก่อนหน้านี้ โอเปอเรเตอร์นี้มีการใช้งานมากเกินไปเพื่อใช้กับพอยน์เตอร์และตัววนซ้ำอัจฉริยะ หากพบตัวดำเนินการ -> ในโค้ดของคุณ คอมไพเลอร์จะเปลี่ยนเส้นทางการเรียกไปยังตัวดำเนินการ -> หากผลลัพธ์ของประเภทที่กำหนดเองถูกส่งคืน
- การใช้งานตามปกติ: T* X::operator->() const ( return _ptr; )
ตัวดำเนินการ->*
- ความหมาย: เข้าถึงตัวชี้ไปยังฟิลด์ด้วยตัวชี้ ตัวดำเนินการนำตัวชี้ไปที่ฟิลด์และนำไปใช้กับ *this ชี้ไปที่ใด ๆ ดังนั้น objPtr->*memPtr จึงเหมือนกับ (*objPtr).*memPtr ไม่ค่อยได้ใช้มาก
- การนำไปปฏิบัติที่เป็นไปได้: แม่แบบ
T& X::ตัวดำเนินการ->*(T V::* memptr) ( return (ตัวดำเนินการ*()).*memptr; ) โดยที่ X คือตัวชี้อัจฉริยะ V คือประเภทที่ X ชี้ไป และ T คือประเภทที่ชี้โดยตัวชี้ภาคสนาม ไม่น่าแปลกใจเลยที่โอเปอเรเตอร์นี้ไม่ค่อยมีการโอเวอร์โหลด
ตัวดำเนินการ Unary&
- ความหมาย: ตัวดำเนินการที่อยู่ โอเปอเรเตอร์นี้มีการโอเวอร์โหลดน้อยมาก
ตัวดำเนินการ
- ความหมาย: ตัวดำเนินการเครื่องหมายจุลภาคในตัวที่ใช้กับสองนิพจน์จะประเมินทั้งตามลำดับการเขียนและส่งกลับค่าของนิพจน์ที่สอง ไม่แนะนำให้โอเวอร์โหลด
โอเปอเรเตอร์~
- ความหมาย: ตัวดำเนินการผกผันระดับบิต หนึ่งในตัวดำเนินการที่ไม่ค่อยได้ใช้มากที่สุด
ผู้ดำเนินการหล่อ
- ความหมาย: อนุญาตให้ส่งวัตถุคลาสโดยนัยหรือชัดเจนไปยังประเภทอื่น
- ประกาศ: //แปลงเป็น T, X::operator T() const; // การแปลงอย่างชัดเจนเป็น U const& ชัดเจน X:: ตัวดำเนินการ U const&() const; // แปลงเป็น V& V& X :: ตัวดำเนินการ V&();
การประกาศเหล่านี้ดูแปลกเนื่องจากไม่มีประเภทการคืนสินค้า เป็นส่วนหนึ่งของชื่อตัวดำเนินการและไม่ได้ระบุสองครั้ง มันคุ้มค่าที่จะจดจำสิ่งนั้น จำนวนมากผีที่ซ่อนอยู่อาจนำมาซึ่ง ข้อผิดพลาดที่ไม่คาดคิดในการทำงานของโปรแกรม
โอเปอเรเตอร์ ใหม่ ใหม่ ลบ ลบ
โอเปอเรเตอร์เหล่านี้แตกต่างอย่างสิ้นเชิงจากโอเปอเรเตอร์ข้างต้นทั้งหมดเนื่องจากใช้งานไม่ได้ ประเภทที่กำหนดเอง- การโอเวอร์โหลดนั้นซับซ้อนมาก ดังนั้นจะไม่ได้รับการพิจารณาที่นี่
บทสรุป
แนวคิดหลักคือ: อย่าโอเวอร์โหลดตัวดำเนินการเพียงเพราะคุณรู้วิธีการทำ บรรทุกมากเกินไปเฉพาะในกรณีที่ดูเหมือนเป็นธรรมชาติและจำเป็นเท่านั้น แต่จำไว้ว่าหากคุณโอเวอร์โหลดโอเปอเรเตอร์ตัวหนึ่ง คุณจะต้องโอเวอร์โหลดตัวอื่นด้วย
ขอให้เป็นวันที่ดี!
ความปรารถนาที่จะเขียนบทความนี้ปรากฏขึ้นหลังจากอ่านโพสต์เนื่องจากไม่ได้กล่าวถึงหัวข้อสำคัญหลายหัวข้อ
สิ่งสำคัญที่สุดที่ต้องจำไว้คือ การโอเวอร์โหลดของตัวดำเนินการเป็นเพียงวิธีที่สะดวกกว่าในการเรียกใช้ฟังก์ชัน ดังนั้นอย่ากังวลกับการโอเวอร์โหลดของตัวดำเนินการ ควรใช้เมื่อจะทำให้การเขียนโค้ดง่ายขึ้นเท่านั้น แต่ก็ไม่มากจนทำให้อ่านยาก อย่างที่ทราบกันดีว่าโค้ดถูกอ่านบ่อยกว่าที่เขียนมาก และอย่าลืมว่าคุณจะไม่ได้รับอนุญาตให้โอเวอร์โหลดโอเปอเรเตอร์ควบคู่กับประเภทในตัว การโอเวอร์โหลดสามารถทำได้เฉพาะกับประเภท/คลาสที่ผู้ใช้กำหนดเท่านั้น
ไวยากรณ์โอเวอร์โหลด
ไวยากรณ์การโอเวอร์โหลดของตัวดำเนินการคล้ายกันมากกับการกำหนดฟังก์ชันที่เรียกว่า ตัวดำเนินการ@ โดยที่ @ คือตัวระบุตัวดำเนินการ (เช่น +, -,<<, >- ลองดูตัวอย่างง่ายๆ:คลาสจำนวนเต็ม ( ส่วนตัว: int value; สาธารณะ: Integer(int i): ค่า(i) () const ตัวดำเนินการจำนวนเต็ม+(const Integer& rv) const ( return (ค่า + rv.value); ) );
ในกรณีนี้ ตัวดำเนินการจะถูกจัดกรอบเป็นสมาชิกของคลาส อาร์กิวเมนต์จะกำหนดค่าที่อยู่ทางด้านขวาของตัวดำเนินการ โดยทั่วไป มีสองวิธีหลักในการโอเวอร์โหลดตัวดำเนินการ: ฟังก์ชันโกลบอลที่เป็นมิตรต่อคลาส หรือฟังก์ชันอินไลน์ของคลาสเอง เราจะพิจารณาว่าวิธีใดดีกว่าสำหรับตัวดำเนินการตัวใดในตอนท้ายของหัวข้อ
ในกรณีส่วนใหญ่ ตัวดำเนินการ (ยกเว้นแบบมีเงื่อนไข) จะส่งคืนออบเจ็กต์ หรือการอ้างอิงถึงประเภทที่มีอาร์กิวเมนต์อยู่ (หากประเภทแตกต่างกัน คุณจะต้องตัดสินใจว่าจะตีความผลลัพธ์ของการประเมินตัวดำเนินการอย่างไร)
การโอเวอร์โหลดตัวดำเนินการ unary
ลองดูตัวอย่างการโอเวอร์โหลดตัวดำเนินการเอกนารีสำหรับคลาส Integer ที่กำหนดไว้ข้างต้น ในเวลาเดียวกัน เรามากำหนดมันในรูปแบบของฟังก์ชันที่เป็นมิตรและพิจารณาตัวดำเนินการลดและเพิ่ม:class Integer ( ส่วนตัว: int value; public: Integer(int i): value(i) () //unary + friend const Integer& โอเปอเรเตอร์+(const Integer& i); //unary - เพื่อน const Integer โอเปอเรเตอร์-(const Integer& i) ; // คำนำหน้าเพิ่มเพื่อน const Integer& โอเปอเรเตอร์--(Integer& i); ฉัน int); //unary plus ไม่ทำอะไรเลย const Integer& โอเปอเรเตอร์+(const Integer& i) ( return i.value; ) const Integer โอเปอเรเตอร์-(const Integer& i) ( return Integer(-i.value); ) // เวอร์ชันคำนำหน้าส่งคืนค่าหลังจากเพิ่มค่า const Integer& โอเปอเรเตอร์++(Integer& i) ( i.value++; return i; ) //เวอร์ชัน postfix ส่งคืนค่าก่อนการเพิ่ม const ตัวดำเนินการจำนวนเต็ม++(Integer& i, int) ( Integer oldValue(i.value); i.value++; return oldValue; ) // เวอร์ชันคำนำหน้าส่งคืน ค่าหลังการลดค่า const Integer& ตัวดำเนินการ--(Integer& i) ( i.value--; return i; ) // เวอร์ชัน postfix ส่งคืนค่าก่อนการลดค่า const ตัวดำเนินการจำนวนเต็ม--(Integer& i, int) ( Integer oldValue(i. ค่า); ฉัน .value--; กลับค่าเก่า;
ตอนนี้คุณรู้แล้วว่าคอมไพลเลอร์แยกความแตกต่างระหว่างการลดและการเพิ่มขึ้นเวอร์ชันคำนำหน้าและ postfix ได้อย่างไร ในกรณีที่เห็นนิพจน์ ++i ตัวดำเนินการฟังก์ชัน++(a) จะถูกเรียก หากเห็น i++ แสดงว่ามีการเรียกตัวดำเนินการ++(a, int) นั่นคือ มีการเรียกฟังก์ชันโอเปอเรเตอร์++ ที่โอเวอร์โหลด และนี่คือสิ่งที่พารามิเตอร์จำลอง int ในเวอร์ชัน postfix ใช้สำหรับ
ตัวดำเนินการไบนารี
ลองดูที่ไวยากรณ์สำหรับการโอเวอร์โหลดตัวดำเนินการไบนารี เรามาโอเวอร์โหลดตัวดำเนินการหนึ่งตัวที่ส่งคืนค่า l ตัวดำเนินการแบบมีเงื่อนไขหนึ่งตัว และตัวดำเนินการหนึ่งตัวที่สร้างค่าใหม่ (มากำหนดกันทั่วโลก):คลาสจำนวนเต็ม ( ส่วนตัว: int value; สาธารณะ: จำนวนเต็ม(int i): ค่า(i) () เพื่อน const ตัวดำเนินการจำนวนเต็ม+(const Integer& ซ้าย, const Integer& right); เพื่อน Integer& ตัวดำเนินการ+=(จำนวนเต็ม& ซ้าย, const Integer& ขวา); เพื่อน ตัวดำเนินการบูล==(const Integer& ซ้าย, const Integer& ขวา); const Integer โอเปอเรเตอร์+(const Integer& left, const Integer& right) ( return Integer(left.value + right.value); ) Integer& โอเปอเรเตอร์+=(Integer& left, const Integer& right) ( left.value += right.value; return left; ) ตัวดำเนินการบูล==(const Integer& left, const Integer& right) ( return left.value == right.value; )
ในตัวอย่างทั้งหมดเหล่านี้ โอเปอเรเตอร์มีการโอเวอร์โหลดสำหรับประเภทเดียวกัน อย่างไรก็ตาม สิ่งนี้ไม่จำเป็น ตัวอย่างเช่น คุณสามารถโอเวอร์โหลดการเพิ่มประเภทจำนวนเต็มและจำนวนทศนิยมที่กำหนดโดยความคล้ายคลึงกันได้
อาร์กิวเมนต์และค่าที่ส่งคืน
ดังที่คุณเห็น ตัวอย่างใช้วิธีที่แตกต่างกันในการส่งผ่านอาร์กิวเมนต์ไปยังฟังก์ชันและส่งกลับค่าตัวดำเนินการ- ถ้าอาร์กิวเมนต์ไม่ได้รับการแก้ไขโดยตัวดำเนินการ ในกรณี เช่น เครื่องหมายบวกแบบเอกนารี จะต้องส่งผ่านเป็นการอ้างอิงไปยังค่าคงที่ โดยทั่วไป สิ่งนี้เป็นจริงสำหรับตัวดำเนินการทางคณิตศาสตร์เกือบทั้งหมด (การบวก การลบ การคูณ...)
- ประเภทของค่าตอบแทนจะขึ้นอยู่กับลักษณะของตัวดำเนินการ หากตัวดำเนินการต้องส่งคืนค่าใหม่ จะต้องสร้างออบเจ็กต์ใหม่ (เช่นในกรณีของไบนารีบวก) หากคุณต้องการป้องกันไม่ให้วัตถุถูกแก้ไขเป็นค่า l คุณจะต้องคืนค่าวัตถุนั้นเป็นค่าคงที่
- ผู้ดำเนินการที่ได้รับมอบหมายจะต้องส่งคืนการอ้างอิงไปยังองค์ประกอบที่เปลี่ยนแปลง นอกจากนี้ หากคุณต้องการใช้ตัวดำเนินการกำหนดค่าในโครงสร้าง เช่น (x=y).f() โดยที่ฟังก์ชัน f() ถูกเรียกใช้สำหรับตัวแปร x หลังจากกำหนดให้กับ y แล้ว อย่าส่งคืนการอ้างอิงไปยังค่าคงที่ เพียงส่งคืนข้อมูลอ้างอิง
- ตัวดำเนินการเชิงตรรกะควรคืนค่า int ที่แย่ที่สุด และค่าบูลที่ดีที่สุด
การเพิ่มประสิทธิภาพมูลค่าผลตอบแทน
เมื่อสร้างออบเจ็กต์ใหม่และส่งคืนจากฟังก์ชัน คุณควรใช้สัญลักษณ์ที่คล้ายกับตัวอย่างตัวดำเนินการไบนารีบวกที่อธิบายไว้ข้างต้นส่งกลับจำนวนเต็ม (left.value + right.value);
พูดตามตรง ฉันไม่รู้ว่าสถานการณ์ใดที่เกี่ยวข้องกับ C++11 ข้อโต้แย้งเพิ่มเติมทั้งหมดนั้นใช้ได้กับ C++98
เมื่อมองแวบแรก สิ่งนี้จะคล้ายกับไวยากรณ์สำหรับการสร้างวัตถุชั่วคราว นั่นคือ ดูเหมือนว่าจะไม่มีความแตกต่างระหว่างโค้ดด้านบนกับสิ่งนี้:
อุณหภูมิจำนวนเต็ม (left.value + right.value); อุณหภูมิกลับ;
แต่ในความเป็นจริง ในกรณีนี้ ตัวสร้างจะถูกเรียกในบรรทัดแรก จากนั้นตัวสร้างการคัดลอกจะถูกเรียก ซึ่งจะคัดลอกวัตถุ จากนั้นเมื่อคลี่คลายสแต็ก ตัวทำลายล้างจะถูกเรียก เมื่อใช้รายการแรก คอมไพลเลอร์จะสร้างอ็อบเจ็กต์ในหน่วยความจำที่ต้องการคัดลอกในตอนแรก ซึ่งจะช่วยประหยัดการเรียกไปยังตัวสร้างการคัดลอกและตัวทำลายล้าง
ผู้ดำเนินการพิเศษ
C++ มีตัวดำเนินการที่มีไวยากรณ์เฉพาะและวิธีการโอเวอร์โหลด ตัวอย่างเช่น ตัวดำเนินการจัดทำดัชนี มันถูกกำหนดให้เป็นสมาชิกของคลาสเสมอ และเนื่องจากอ็อบเจ็กต์ที่ถูกจัดทำดัชนีมีวัตถุประสงค์ให้ทำงานเหมือนอาร์เรย์ จึงควรส่งคืนการอ้างอิงตัวดำเนินการจุลภาค
ตัวดำเนินการ "พิเศษ" ยังรวมถึงตัวดำเนินการลูกน้ำด้วย มันถูกเรียกบนวัตถุที่มีเครื่องหมายจุลภาคอยู่ข้างๆ (แต่ไม่ถูกเรียกในรายการอาร์กิวเมนต์ของฟังก์ชัน) การสร้างกรณีการใช้งานที่มีความหมายสำหรับผู้ปฏิบัติงานรายนี้ไม่ใช่เรื่องง่าย Habrauser ในความคิดเห็นต่อบทความก่อนหน้าเกี่ยวกับการโอเวอร์โหลดตัวดำเนินการอ้างอิงแบบพอยน์เตอร์
การโอเวอร์โหลดตัวดำเนินการเหล่านี้สามารถพิสูจน์ได้สำหรับคลาสตัวชี้อัจฉริยะ โอเปอเรเตอร์นี้จำเป็นต้องถูกกำหนดให้เป็นฟังก์ชันคลาส และมีข้อ จำกัด บางประการ: ต้องส่งคืนอ็อบเจ็กต์ (หรือข้อมูลอ้างอิง) หรือตัวชี้ที่อนุญาตให้เข้าถึงอ็อบเจ็กต์ผู้ดำเนินการมอบหมาย
ตัวดำเนินการมอบหมายงานจำเป็นต้องถูกกำหนดให้เป็นฟังก์ชันคลาสเนื่องจากมีการเชื่อมโยงภายในกับวัตถุทางด้านซ้ายของ "=" การกำหนดตัวดำเนินการมอบหมายทั่วโลกจะทำให้สามารถแทนที่พฤติกรรมเริ่มต้นของตัวดำเนินการ "=" ได้ ตัวอย่าง:class Integer ( private: int value; public: Integer(int i): value(i) () Integer& โอเปอเรเตอร์=(const Integer& right) ( //ตรวจสอบการกำหนดตัวเองถ้า (this == &right) ( return *this; ) value = right.value; return *สิ่งนี้;
อย่างที่คุณเห็นที่จุดเริ่มต้นของฟังก์ชันจะมีการตรวจสอบการกำหนดตนเอง โดยทั่วไป ในกรณีนี้ การจัดสรรตนเองนั้นไม่เป็นอันตราย แต่สถานการณ์ก็ไม่ได้ง่ายเสมอไป ตัวอย่างเช่น หากวัตถุมีขนาดใหญ่ คุณอาจเสียเวลากับการคัดลอกที่ไม่จำเป็น หรือเมื่อทำงานกับพอยน์เตอร์ได้มาก
ตัวดำเนินการที่ไม่โอเวอร์โหลด
โอเปอเรเตอร์บางตัวใน C++ ไม่ได้โอเวอร์โหลดเลย เห็นได้ชัดว่าสิ่งนี้เกิดขึ้นด้วยเหตุผลด้านความปลอดภัย- โอเปอเรเตอร์การเลือกสมาชิกคลาส "."
- ตัวดำเนินการสำหรับการยกเลิกการอ้างอิงตัวชี้ไปยังสมาชิกคลาส ".*"
- C++ ไม่มีตัวดำเนินการยกกำลัง (เช่นใน Fortran) "**"
- ห้ามมิให้กำหนดตัวดำเนินการของคุณเอง (อาจมีปัญหาในการกำหนดลำดับความสำคัญ)
- ลำดับความสำคัญของผู้ปฏิบัติงานไม่สามารถเปลี่ยนแปลงได้
Rob Murray ในหนังสือ C++ Strategies and Tactics ของเขา ได้กำหนดแนวปฏิบัติต่อไปนี้ในการเลือกแบบฟอร์มตัวดำเนินการ:
ทำไมจึงเป็นเช่นนี้? ประการแรก โอเปอเรเตอร์บางตัวจะถูกจำกัดในตอนแรก โดยทั่วไป หากไม่มีความแตกต่างทางความหมายในการกำหนดตัวดำเนินการ ก็ควรออกแบบให้เป็นฟังก์ชันคลาสเพื่อเน้นการเชื่อมต่อ จะดีกว่า นอกจากนี้ฟังก์ชันจะอยู่ในบรรทัดด้วย นอกจากนี้ บางครั้งอาจจำเป็นต้องแสดงตัวถูกดำเนินการทางด้านซ้ายเป็นอ็อบเจ็กต์ของคลาสอื่น ตัวอย่างที่โดดเด่นที่สุดน่าจะเป็นคำจำกัดความใหม่<< и >> สำหรับสตรีม I/O
วรรณกรรม
Bruce Eckel - ปรัชญา C++ รู้เบื้องต้นเกี่ยวกับมาตรฐาน C ++แท็ก:
- ซี++
- ผู้ประกอบการโอเวอร์โหลด
- ผู้ประกอบการโอเวอร์โหลด
พื้นฐานผู้ปฏิบัติงานโอเวอร์โหลด
C# ก็เหมือนกับภาษาการเขียนโปรแกรมอื่นๆ คือมีชุดโทเค็นสำเร็จรูปที่ใช้ในการดำเนินการ การดำเนินงานขั้นพื้นฐานมากกว่าประเภทในตัว ตัวอย่างเช่น เป็นที่ทราบกันดีว่าการดำเนินการ + สามารถใช้กับจำนวนเต็มสองตัวเพื่อให้ได้ผลรวม:
// การดำเนินการ + พร้อมจำนวนเต็ม int = 100; int ข = 240; int c = a + b; //s ตอนนี้เท่ากับ 340
ไม่มีอะไรใหม่ที่นี่ แต่คุณเคยคิดบ้างไหมว่าการดำเนินการ + เดียวกันสามารถนำไปใช้กับประเภทข้อมูลในตัวของ C# ส่วนใหญ่ได้ ตัวอย่างเช่น พิจารณาโค้ดนี้:
// การดำเนินการ + พร้อมสตริง string si = "สวัสดี"; string s2 = "โลก!"; สตริง s3 = si + s2; // s3 ตอนนี้มี "Hello world!"
โดยพื้นฐานแล้ว ฟังก์ชันการทำงานของการดำเนินการ + นั้นขึ้นอยู่กับประเภทข้อมูลที่แสดง (สตริงหรือจำนวนเต็มในกรณีนี้) โดยไม่ซ้ำกัน เมื่อใช้การดำเนินการ + ประเภทตัวเลขเราได้รับ ผลรวมทางคณิตศาสตร์ตัวถูกดำเนินการ แต่เมื่อดำเนินการแบบเดียวกันแล้ว ประเภทสตริงเราจะได้การต่อสตริงเข้าด้วยกัน
ภาษา C# มอบความสามารถในการสร้างคลาสและโครงสร้างพิเศษที่ตอบสนองเฉพาะกับโทเค็นพื้นฐานชุดเดียวกัน (เช่น ตัวดำเนินการ +) โปรดทราบว่าโอเปอเรเตอร์ C# ในตัวทุกตัวไม่สามารถโอเวอร์โหลดได้ ตารางต่อไปนี้อธิบายความสามารถในการโอเวอร์โหลดของการทำงานพื้นฐาน:
การทำงานของ C# | ความเป็นไปได้ของการโอเวอร์โหลด |
+, -, !, ++, --, จริง, เท็จ | ตัวดำเนินการ unary ชุดนี้สามารถโอเวอร์โหลดได้ |
+, -, *, /, %, &, |, ^, > | การดำเนินการไบนารี่เหล่านี้สามารถโอเวอร์โหลดได้ |
==, !=, <, >, <=, >= | ตัวดำเนินการเปรียบเทียบเหล่านี้อาจมีการโอเวอร์โหลด C# ต้องการการโอเวอร์โหลดร่วมกันของตัวดำเนินการ "like" (เช่น< и >, <= и >=, == และ!=) |
การดำเนินการไม่สามารถโอเวอร์โหลดได้ อย่างไรก็ตาม ตัวสร้างดัชนีมีฟังก์ชันการทำงานที่คล้ายคลึงกัน | |
() | การดำเนินการ () ไม่สามารถโอเวอร์โหลดได้ อย่างไรก็ตาม วิธีการแปลงแบบพิเศษมีฟังก์ชันการทำงานเหมือนกัน |
+=, -=, *=, /=, %=, &=, |=, ^=, >= | ตัวดำเนินการกำหนดสั้นไม่สามารถโอเวอร์โหลดได้ อย่างไรก็ตาม คุณจะได้รับมันโดยอัตโนมัติโดยการโอเวอร์โหลดการดำเนินการไบนารีที่เกี่ยวข้อง |
การโอเวอร์โหลดของตัวดำเนินการมีความสัมพันธ์อย่างใกล้ชิดกับการโอเวอร์โหลดของวิธีการ สำหรับการโอเวอร์โหลดของผู้ปฏิบัติงาน ให้ใช้ คำหลัก ตัวดำเนินการซึ่งกำหนดวิธีการของตัวดำเนินการ ซึ่งจะกำหนดการกระทำของตัวดำเนินการที่สัมพันธ์กับคลาสของมัน วิธีการดำเนินการมีสองรูปแบบ: รูปแบบหนึ่งสำหรับตัวดำเนินการเอกภาค และอีกรูปแบบหนึ่งสำหรับตัวดำเนินการไบนารี ด้านล่างนี้คือแบบฟอร์มทั่วไปสำหรับรูปแบบต่างๆ ของวิธีการเหล่านี้:
// แบบฟอร์มทั่วไปโอเปอเรเตอร์โอเวอร์โหลดแบบ unary ตัวดำเนินการ return_type แบบคงที่สาธารณะ op (ตัวถูกดำเนินการประเภทพารามิเตอร์) ( // การดำเนินการ ) // รูปแบบทั่วไปของตัวดำเนินการไบนารีโอเวอร์โหลด ตัวดำเนินการ return_type แบบคงที่สาธารณะ op (parameter_type1 operand1, parameter_type2 operand2) ( // การดำเนินการ)
ในที่นี้ op จะถูกแทนที่ด้วยตัวดำเนินการที่โอเวอร์โหลด เช่น + หรือ / และ return_typeย่อมาจาก ประเภทเฉพาะค่าที่ส่งคืนโดยการดำเนินการที่ระบุ ค่านี้สามารถเป็นประเภทใดก็ได้ แต่มักจะระบุว่าเป็นประเภทเดียวกันกับคลาสที่ตัวดำเนินการโอเวอร์โหลด ความสัมพันธ์นี้ช่วยให้ใช้ตัวดำเนินการโอเวอร์โหลดในนิพจน์ได้ง่ายขึ้น สำหรับตัวดำเนินการเอกภาค ตัวถูกดำเนินการหมายถึงตัวถูกดำเนินการที่ถูกส่งผ่าน และสำหรับตัวดำเนินการไบนารีก็จะแสดงเช่นเดียวกัน ตัวถูกดำเนินการ1และ ตัวถูกดำเนินการ2- โปรดทราบว่าวิธีการของตัวดำเนินการจะต้องมีทั้งตัวระบุประเภทสาธารณะและแบบคงที่
ตัวดำเนินการไบนารีมากเกินไป
ลองดูที่การใช้ตัวดำเนินการไบนารีโอเวอร์โหลดโดยใช้ตัวอย่างง่ายๆ:
การใช้ระบบ; ใช้ System.Collections.Generic; ใช้ System.Linq; ใช้ System.Text; namespace ConsoleApplication1 (คลาส MyArr ( // พิกัดของจุดในพื้นที่สามมิติ public int x, y, z; public MyArr(int x = 0, int y = 0, int z = 0) ( this.x = x; this.y = y; this.z = z; ตัวดำเนินการไบนารี+ ตัวดำเนินการ MyArr แบบคงที่สาธารณะ +(MyArr obj1, MyArr obj2) ( MyArr arr = new MyArr(); arr.x = obj1.x + obj2.x; arr.y = obj1.y + obj2.y; arr.z = obj1.z + obj2.z; return arr; ) // โอเวอร์โหลดตัวดำเนินการไบนารี - ตัวดำเนินการ MyArr สาธารณะแบบคงที่ - (MyArr obj1, MyArr obj2) ( MyArr arr = new MyArr (); arr.x = obj1.x - obj2 x ; arr.y = obj1.y - obj2.y; arr.z = obj1.z - obj2.z; คอนโซลกลับ WriteLine("พิกัดของจุดแรก: " + Point1.x + " " + Point1.y + " " + Point1.z); .WriteLine("พิกัดของจุดที่สอง: " + Point2.x + " " + Point2 .y + " " + Point2.z + "\n"); MyArr Point3 = Point1 + Point2; = " + Point3.x + " " + Point3.y + " " + Point3.z); Point3 = Point1 - Point2 ; Console.WriteLine("\nPoint1 - Point2 = " + Point3.x + " " + Point3.y + " " + Point3.z); Console.ReadLine();
การโอเวอร์โหลดตัวดำเนินการ unary
ตัวดำเนินการ Unary มีการโอเวอร์โหลดในลักษณะเดียวกับตัวดำเนินการไบนารี ความแตกต่างหลักๆ แน่นอนก็คือ พวกมันมีตัวถูกดำเนินการเพียงตัวเดียวเท่านั้น มาปรับปรุงตัวอย่างก่อนหน้านี้ให้ทันสมัยโดยเพิ่มโอเปอเรเตอร์โอเวอร์โหลด ++, --, -:
การใช้ระบบ; ใช้ System.Collections.Generic; ใช้ System.Linq; ใช้ System.Text; namespace ConsoleApplication1 (คลาส MyArr ( // พิกัดของจุดในพื้นที่สามมิติ public int x, y, z; public MyArr(int x = 0, int y = 0, int z = 0) ( this.x = x; this.y = y; this.z = z; ) // โอเวอร์โหลดตัวดำเนินการไบนารี + ตัวดำเนินการ MyArr แบบคงที่สาธารณะ + (MyArr obj1, MyArr obj2) ( MyArr arr = new MyArr (); arr.x = obj1.x + obj2 .x; arr. y = obj1.y + obj2.y; arr.z = obj1.z + obj2.z; return arr; ) // โอเวอร์โหลดตัวดำเนินการไบนารี่ - (MyArr obj1, MyArr obj2) ( MyArr arr = new MyArr (); arr.x = obj1.x - obj2.x; arr.y = obj1.y - obj2.y; arr.z = obj1.z - obj2.z ) // โอเวอร์โหลด unary ตัวดำเนินการ - ตัวดำเนินการ MyArr คงที่สาธารณะ - (MyArr obj1) ( MyArr arr = new MyArr (); arr.x = -obj1.x; arr.y = -obj1.y; arr.z = -obj1.z; return arr; ) // การโอเวอร์โหลดตัวดำเนินการ unary ++ ตัวดำเนินการ MyArr คงที่สาธารณะ ++(MyArr obj1) ( obj1.x += 1; obj1.y += 1; obj1.z +=1; return obj1; ) // การโอเวอร์โหลด unary ตัวดำเนินการ -- ตัวดำเนินการ MyArr แบบคงที่สาธารณะ -- (MyArr obj1) (obj1.x -= 1;
obj1.y -= 1;
obj1.z -= 1;
กลับ obj1;
หากฟังก์ชันตัวดำเนินการถูกกำหนดให้เป็นฟังก์ชันแยกต่างหากและไม่ได้เป็นสมาชิกของคลาส จำนวนพารามิเตอร์ของฟังก์ชันนั้นจะเหมือนกับจำนวนตัวถูกดำเนินการของตัวดำเนินการ ตัวอย่างเช่น ฟังก์ชันที่แสดงถึงตัวดำเนินการไบนารีจะมีพารามิเตอร์ 1 ตัว และฟังก์ชันที่แสดงถึงตัวดำเนินการไบนารีจะมีพารามิเตอร์ 2 ตัว หากตัวดำเนินการรับตัวถูกดำเนินการสองตัว ตัวถูกดำเนินการตัวแรกจะถูกส่งผ่านไปยังพารามิเตอร์แรกของฟังก์ชัน และตัวถูกดำเนินการตัวที่สองจะถูกส่งผ่านไปยังพารามิเตอร์ตัวที่สอง ในกรณีนี้ อย่างน้อยหนึ่งพารามิเตอร์จะต้องแสดงถึงประเภทคลาส
ลองดูตัวอย่างด้วยคลาส Counter ซึ่งแสดงถึงนาฬิกาจับเวลาและเก็บจำนวนวินาที:
#รวม
ในที่นี้ฟังก์ชันตัวดำเนินการไม่ได้เป็นส่วนหนึ่งของคลาส Counter และถูกกำหนดไว้ภายนอก ฟังก์ชันนี้โอเวอร์โหลดตัวดำเนินการเพิ่มเติมสำหรับชนิดตัวนับ มันเป็นไบนารี ดังนั้นจึงต้องใช้พารามิเตอร์สองตัว ในกรณีนี้ เรากำลังเพิ่มวัตถุตัวนับสองตัว ฟังก์ชันนี้ยังส่งกลับวัตถุตัวนับที่เก็บจำนวนวินาทีทั้งหมดอีกด้วย โดยพื้นฐานแล้ว การดำเนินการเพิ่มเติมที่นี่เกี่ยวข้องกับการบวกวินาทีของวัตถุทั้งสอง:
ตัวดำเนินการตัวนับ + (ตัวนับ c1, ตัวนับ c2) ( ตัวนับกลับ (c1.วินาที + c2.วินาที); )
ไม่จำเป็นต้องส่งคืนอ็อบเจ็กต์คลาส นอกจากนี้ยังสามารถเป็นอ็อบเจ็กต์ประเภทดั้งเดิมที่มีอยู่แล้วภายในได้ และเรายังสามารถกำหนดโอเปอเรเตอร์โอเวอร์โหลดเพิ่มเติมได้:
ตัวดำเนินการ Int + (ตัวนับ c1, int s) ( return c1.seconds + s; )
เวอร์ชันนี้จะเพิ่มวัตถุตัวนับให้กับตัวเลขและส่งกลับตัวเลขด้วยเช่นกัน ดังนั้น ตัวถูกดำเนินการทางด้านซ้ายของการดำเนินการต้องเป็นประเภท Counter และตัวถูกดำเนินการทางขวาต้องเป็นประเภท int และตัวอย่าง เราสามารถใช้โอเปอเรเตอร์เวอร์ชันนี้ได้ดังนี้:
ตัวนับ c1(20); int วินาที = c1 + 25; // 45 std::cout<< seconds << std::endl;
นอกจากนี้ ฟังก์ชันตัวดำเนินการสามารถกำหนดเป็นสมาชิกของคลาสได้ หากฟังก์ชันตัวดำเนินการถูกกำหนดให้เป็นสมาชิกของคลาส ตัวถูกดำเนินการทางซ้ายจะสามารถเข้าถึงได้ผ่านตัวชี้นี้และแสดงถึงอ็อบเจ็กต์ปัจจุบัน และตัวถูกดำเนินการทางขวาจะถูกส่งผ่านไปยังฟังก์ชันที่คล้ายกันเป็นพารามิเตอร์เดียว:
#รวม
) ตัวดำเนินการ int + (int s) ( กลับสิ่งนี้ -> วินาที + s; ) วินาที int; - int main() ( ตัวนับ c1(20); ตัวนับ c2(10); ตัวนับ c3 = c1 + c2; c3.display(); // 30 วินาที int วินาที = c1 + 25; // 45 กลับ 0; )
ตัวดำเนินการใดควรกำหนดใหม่ที่ไหน ตัวดำเนินการของการกำหนด การจัดทำดัชนี () การเรียก (()) การเข้าถึงสมาชิกของคลาสด้วยตัวชี้ (->) ควรถูกกำหนดให้เป็นฟังก์ชันสมาชิกของคลาส ตัวดำเนินการที่เปลี่ยนสถานะของวัตถุหรือเกี่ยวข้องโดยตรงกับวัตถุ (การเพิ่มขึ้น การลดลง) มักจะถูกกำหนดให้เป็นฟังก์ชันสมาชิกของคลาสด้วย โอเปอเรเตอร์อื่นๆ ทั้งหมดมักถูกกำหนดให้เป็นฟังก์ชันเดี่ยวๆ ไม่ใช่สมาชิกของคลาส
ตัวดำเนินการเปรียบเทียบ
มีโอเปอเรเตอร์จำนวนหนึ่งโอเวอร์โหลดเป็นคู่ ตัวอย่างเช่น หากเรากำหนดตัวดำเนินการ == เราก็จะต้องกำหนดตัวดำเนินการ != ด้วย และเมื่อกำหนดตัวดำเนินการแล้ว< надо также определять функцию для оператора >- ตัวอย่างเช่น ลองโอเวอร์โหลดตัวดำเนินการเหล่านี้:
ตัวดำเนินการบูล == (ตัวนับ c1, ตัวนับ c2) ( กลับ c1.seconds == c2.seconds; ) ตัวดำเนินการบูล != (ตัวนับ c1, ตัวนับ c2) ( กลับ c1.seconds != c2.seconds; ) ตัวดำเนินการบูล > ( ตัวนับ c1, ตัวนับ c2) ( return c1.seconds > c2.seconds; ) ตัวดำเนินการบูล< (Counter c1, Counter c2) { return c1.seconds < c2.seconds; } int main() { Counter c1(20); Counter c2(10); bool b1 = c1 == c2; // false bool b2 = c1 >ค2; // true std::cout<< b1 << std::endl; std::cout << b2 << std::endl; return 0; }
ผู้ดำเนินการที่ได้รับมอบหมาย
#รวม
การดำเนินการเพิ่มและลด
การกำหนดตัวดำเนินการเพิ่มและลดค่าใหม่อาจเป็นเรื่องท้าทายอย่างยิ่ง เนื่องจากเราจำเป็นต้องกำหนดทั้งแบบฟอร์มคำนำหน้าและคำต่อท้ายสำหรับตัวดำเนินการเหล่านี้ มากำหนดตัวดำเนินการที่คล้ายกันสำหรับประเภทตัวนับ:
#รวม
ตัวนับ& ตัวดำเนินการ ++ () ( วินาที += 5; return *this; )
ในฟังก์ชันเอง คุณสามารถกำหนดตรรกะบางอย่างสำหรับการเพิ่มค่าได้ ในกรณีนี้ จำนวนวินาทีจะเพิ่มขึ้น 5 วินาที
ตัวดำเนินการ Postfix ต้องส่งคืนค่าของออบเจ็กต์ก่อนที่จะเพิ่มค่า นั่นคือ สถานะก่อนหน้าของออบเจ็กต์ หากต้องการทำให้รูปแบบ postfix แตกต่างจากแบบฟอร์มคำนำหน้า เวอร์ชัน postfix จะได้รับพารามิเตอร์เพิ่มเติมประเภท int ซึ่งไม่ได้ใช้ แม้ว่าโดยหลักการแล้วเราจะสามารถใช้งานได้ก็ตาม
ตัวดำเนินการตัวนับ ++ (int) ( ตัวนับ prev = *this; +*this; return prev; )