ตัวดำเนินการโอเวอร์โหลด ผู้ประกอบการใหม่และลบ ตัวดำเนินการอ้างอิงแบบพอยน์เตอร์

ขอให้เป็นวันที่ดี!

ความปรารถนาที่จะเขียนบทความนี้ปรากฏขึ้นหลังจากอ่านโพสต์ 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>> เปลี่ยนอาร์กิวเมนต์ด้านซ้ายแล้ว

โอเปอเรเตอร์==, !=

  • ความหมาย: ทดสอบความเท่าเทียมกัน/ความไม่เท่าเทียมกัน ความหมายของความเท่าเทียมกันนั้นแตกต่างกันไปตามชนชั้น ไม่ว่าในกรณีใด ให้พิจารณาคุณสมบัติของความเท่าเทียมกันดังต่อไปนี้:
    1. การสะท้อนกลับเช่น ก == ก
    2. สมมาตรเช่น ถ้า a == b แล้ว b == a
    3. การสัญจร ได้แก่ ถ้า 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 ซึ่งแสดงถึงนาฬิกาจับเวลาและเก็บจำนวนวินาที:

#รวม << seconds << " seconds" << std::endl; } int seconds; }; Counter operator + (Counter c1, Counter c2) { return Counter(c1.seconds + c2.seconds); } int main() { Counter c1(20); Counter c2(10); Counter c3 = c1 + c2; c3.display(); // 30 seconds return 0; }

ในที่นี้ฟังก์ชันตัวดำเนินการไม่ได้เป็นส่วนหนึ่งของคลาส 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 วินาที) ( วินาที = วินาที; ) การแสดงเป็นโมฆะ () ( std::cout<< seconds << " seconds" << std::endl; } Counter operator + (Counter c2) { return Counter(this->วินาที + c2.วินาที);

) ตัวดำเนินการ 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; }

ผู้ดำเนินการที่ได้รับมอบหมาย

#รวม ตัวนับคลาส (สาธารณะ: ตัวนับ (int วินาที) ( วินาที = วินาที; ) การแสดงเป็นโมฆะ () ( std::cout<< seconds << " seconds" << std::endl; } Counter& operator += (Counter c2) { seconds += c2.seconds; return *this; } int seconds; }; int main() { Counter c1(20); Counter c2(10); c1 += c2; c1.display(); // 30 seconds return 0; }

การดำเนินการเพิ่มและลด

การกำหนดตัวดำเนินการเพิ่มและลดค่าใหม่อาจเป็นเรื่องท้าทายอย่างยิ่ง เนื่องจากเราจำเป็นต้องกำหนดทั้งแบบฟอร์มคำนำหน้าและคำต่อท้ายสำหรับตัวดำเนินการเหล่านี้ มากำหนดตัวดำเนินการที่คล้ายกันสำหรับประเภทตัวนับ:

#รวม ตัวนับคลาส (สาธารณะ: ตัวนับ (int วินาที) ( วินาที = วินาที; ) การแสดงเป็นโมฆะ () ( std::cout<< seconds << " seconds" << std::endl; } // префиксные операторы Counter& operator++ () { seconds += 5; return *this; } Counter& operator-- () { seconds -= 5; return *this; } // постфиксные операторы Counter operator++ (int) { Counter prev = *this; ++*this; return prev; } Counter operator-- (int) { Counter prev = *this; --*this; return prev; } int seconds; }; int main() { Counter c1(20); Counter c2 = c1++; c2.display(); // 20 seconds c1.display(); // 25 seconds --c1; c1.display(); // 20 seconds return 0; }

ตัวนับ& ตัวดำเนินการ ++ () ( วินาที += 5; return *this; )

ในฟังก์ชันเอง คุณสามารถกำหนดตรรกะบางอย่างสำหรับการเพิ่มค่าได้ ในกรณีนี้ จำนวนวินาทีจะเพิ่มขึ้น 5 วินาที

ตัวดำเนินการ Postfix ต้องส่งคืนค่าของออบเจ็กต์ก่อนที่จะเพิ่มค่า นั่นคือ สถานะก่อนหน้าของออบเจ็กต์ หากต้องการทำให้รูปแบบ postfix แตกต่างจากแบบฟอร์มคำนำหน้า เวอร์ชัน postfix จะได้รับพารามิเตอร์เพิ่มเติมประเภท int ซึ่งไม่ได้ใช้ แม้ว่าโดยหลักการแล้วเราจะสามารถใช้งานได้ก็ตาม

ตัวดำเนินการตัวนับ ++ (int) ( ตัวนับ prev = *this; +*this; return prev; )