C quá tải của hoạt động so sánh chuỗi. Quá tải các hoạt động nhị phân. Quá tải các toán tử đơn nguyên

Cập nhật cuối cùng: 20.10.2017

Quá tải toán tử cho phép bạn xác định các hành động mà toán tử sẽ thực hiện. Quá tải liên quan đến việc tạo một hàm có tên chứa toán tử Word và ký hiệu của toán tử bị quá tải. Hàm toán tử có thể được định nghĩa là thành viên của một lớp hoặc bên ngoài lớp.

Bạn chỉ có thể nạp chồng các toán tử đã được xác định trong C++. Bạn không thể tạo toán tử mới.

Nếu hàm toán tử được định nghĩa là chức năng riêng biệt và không phải là thành viên của lớp thì số tham số của hàm đó trùng với số toán hạng của toán tử. Ví dụ: hàm đại diện cho toán tử một ngôi sẽ có một tham số và hàm đại diện cho toán tử nhị phân sẽ có hai tham số. Nếu một toán tử lấy hai toán hạng thì toán hạng thứ nhất được truyền cho tham số đầu tiên của hàm và toán hạng thứ hai được truyền cho tham số thứ hai. Trong trường hợp này, ít nhất một trong các tham số phải đại diện cho loại lớp

Hãy xem một ví dụ với lớp Counter, đại diện cho đồng hồ bấm giờ và lưu trữ số giây:

#bao gồm << 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; }

Ở đây, hàm toán tử không phải là một phần của lớp Counter và được định nghĩa bên ngoài lớp đó. Hàm này nạp chồng toán tử cộng cho loại Bộ đếm. Nó là nhị phân nên cần có hai tham số. TRONG trong trường hợp này chúng tôi thêm hai đối tượng Counter. Hàm này cũng trả về một đối tượng Counter lưu trữ tổng số giây. Về bản chất, thao tác cộng ở đây là cộng số giây của cả hai đối tượng:

Toán tử bộ đếm + (Bộ đếm c1, Bộ đếm c2) ( return Bộ đếm(c1.seconds + c2.seconds); )

Không cần thiết phải trả về một đối tượng lớp. Nó cũng có thể là một đối tượng thuộc kiểu nguyên thủy có sẵn. Và chúng ta cũng có thể định nghĩa quá tải toán tử bổ sung:

Toán tử int + (Bộ đếm c1, int s) ( return c1.seconds + s; )

Phiên bản này thêm đối tượng Counter vào một số và trả về số đó. Do đó, toán hạng bên trái của phép toán phải có kiểu Counter và toán hạng bên phải phải có kiểu int. Và, ví dụ, chúng ta có thể áp dụng phiên bản toán tử này như sau:

Bộ đếm c1(20); int giây = c1 + 25; // 45 std::cout<< seconds << std::endl;

Ngoài ra, các hàm toán tử có thể được định nghĩa là thành viên của các lớp. Nếu một hàm toán tử được định nghĩa là thành viên của một lớp thì toán hạng bên trái được truy cập thông qua con trỏ this và biểu thị đối tượng hiện tại, còn toán hạng bên phải được truyền cho hàm tương tự dưới dạng tham số duy nhất:

#bao gồm lớp Bộ đếm ( public: Counter(int sec) ( giây = sec; ) void display() ( std::cout<< seconds << " seconds" << std::endl; } Counter operator + (Counter c2) { return Counter(this->giây + c2.giây); ) toán tử int + (int s) ( return this->seconds + s; ) int giây; ); int main() ( Bộ đếm c1(20); Bộ đếm c2(10); Bộ đếm c3 = c1 + c2; c3.display(); // 30 giây int giây = c1 + 25; // 45 return 0; )

Trong trường hợp này, chúng ta truy cập toán hạng bên trái trong các hàm toán tử thông qua con trỏ this.

Những toán tử nào cần được xác định lại ở đâu? Các toán tử gán, lập chỉ mục (), gọi (()), truy cập một thành viên lớp bằng con trỏ (->) nên được định nghĩa là các hàm thành viên lớp. Các toán tử thay đổi trạng thái của một đối tượng hoặc được liên kết trực tiếp với đối tượng đó (tăng, giảm) cũng thường được định nghĩa là các hàm thành viên của lớp. Tất cả các toán tử khác thường được định nghĩa là các hàm riêng lẻ chứ không phải là thành viên của một lớp.

Toán tử so sánh

Một số toán tử bị quá tải theo cặp. Ví dụ: nếu chúng ta xác định toán tử == thì chúng ta cũng phải xác định toán tử !=. Và khi xác định toán tử< надо также определять функцию для оператора >. Ví dụ: hãy quá tải các toán tử này:

Toán tử bool == (Bộ đếm c1, Bộ đếm c2) ( return c1.seconds == c2.seconds; ) toán tử bool != (Bộ đếm c1, Bộ đếm c2) ( return c1.seconds != c2.seconds; ) toán tử bool > ( Bộ đếm c1, Bộ đếm c2) ( return c1.seconds > c2.seconds; ) toán tử bool< (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 >c2; // đúng std::cout<< b1 << std::endl; std::cout << b2 << std::endl; return 0; }

Toán tử gán

#bao gồm lớp Bộ đếm ( public: Counter(int sec) ( giây = sec; ) void display() ( 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; }

Các thao tác tăng giảm

Việc xác định lại các toán tử tăng và giảm có thể đặc biệt khó khăn, vì chúng ta cần xác định cả dạng tiền tố và hậu tố cho các toán tử này. Hãy xác định các toán tử tương tự cho loại Bộ đếm:

#bao gồm lớp Bộ đếm ( public: Counter(int sec) ( giây = sec; ) void display() ( 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; }

Bộ đếm& toán tử++ () ( giây += 5; return *this; )

Trong chính hàm đó, bạn có thể xác định một số logic để tăng giá trị. Trong trường hợp này, số giây tăng thêm 5.

Các toán tử postfix phải trả về giá trị của đối tượng trước mức tăng, tức là trạng thái trước đó của đối tượng. Để làm cho dạng hậu tố khác với dạng tiền tố, các phiên bản hậu tố nhận được một tham số bổ sung kiểu int, tham số này không được sử dụng. Mặc dù về nguyên tắc chúng ta có thể sử dụng nó.

Toán tử bộ đếm++ (int) ( Bộ đếm prev = *this; ++*this; return prev; )

Hôm nay chúng ta sẽ làm quen với một tính năng tuyệt vời của bất kỳ ngôn ngữ C++ nào - nạp chồng toán tử. Nhưng trước tiên, hãy tìm hiểu tại sao điều này lại cần thiết.

Với các kiểu cơ bản, bạn có thể sử dụng bất kỳ toán tử nào: +, -, *, ++, = và nhiều toán tử khác. Ví dụ:

int a=2,b=3,c;
c = a + b;

Ở đây, thao tác + trước tiên được thực hiện trên các biến kiểu int, sau đó kết quả được gán cho biến c bằng thao tác =. Bạn không thể làm điều này với các lớp học. Hãy tạo một lớp đơn giản:

mã bằng ngôn ngữ c++ Bộ đếm lớp ( public: int counter; Counter() : c(0) () ); Bộ đếm a,b,c; a.bộ đếm = 2; b.bộ đếm = 3; c = a + b;

Ở đây trình biên dịch sẽ đưa ra một lỗi ở dòng cuối cùng: nó không biết chính xác cách hành động khi sử dụng các phép toán = và + trên các đối tượng của lớp Counter. Bạn có thể giải quyết vấn đề này như thế này:

mã bằng ngôn ngữ c++ lớp Bộ đếm ( public: int counter; Counter() : count(0) () void AddCounters (Counter& a, Counter& b) ( counter = a.counter + b.counter; ) ); Bộ đếm a,b,c; a.bộ đếm = 2; b.bộ đếm = 3; c.AddCounters(a,b);

Đồng ý, việc sử dụng các phép toán + và = trong trường hợp này sẽ làm cho chương trình dễ hiểu hơn. Vì vậy, đây là để sử dụng hoạt động tiêu chuẩn C++ có các lớp, các thao tác này cần được nạp chồng.

Quá tải các toán tử đơn nguyên

Hãy bắt đầu với các thao tác đơn giản - đơn nhất. Trước hết, chúng tôi quan tâm đến sự gia tăng. Khi sử dụng thao tác này trên các kiểu cơ bản, một biến có thể được cộng hoặc trừ:

mã bằng ngôn ngữ c++ int a=1; ++a; // a = 2; --Một; // a = 1;

Bây giờ hãy dạy lớp Counter cách sử dụng preincrement:

mã bằng ngôn ngữ c++ class Counter ( private: counter; public: Counter() : counter(0) () void operator++ () ( counter += 1; // Bạn cũng có thể counter++ hoặc ++counter - // điều đó không thành vấn đề trong trường hợp này . ) ); Bộ đếm a; ++a; ++a; ++a;

Mã này hoạt động. Khi sử dụng thao tác ++ (điều quan trọng là dấu này nằm trước mã định danh!) trên một đối tượng của lớp Counter, biến counter của đối tượng a sẽ tăng lên.

Trong ví dụ này, chúng ta nạp chồng toán tử ++. Điều này được thực hiện bằng cách tạo một phương thức bên trong lớp. Tính năng quan trọng duy nhất của phương pháp này là tên định danh. Tên định danh cho các hoạt động quá tải bao gồm từ khóa toán tử và tên của hoạt động. Trong tất cả các khía cạnh khác, phương pháp này được định nghĩa giống như bất kỳ phương pháp nào khác.

Sử dụng các toán tử quá tải với các loại tùy chỉnh rất đơn giản - giống như với các kiểu dữ liệu thông thường.

Quá tải toán tử postfix

Ví dụ về hoạt động postfix:

int a = 3;
một++;
Một--;

Tức là ở đây dấu hiệu hoạt động được đặt sau tên định danh. Để sử dụng các thao tác postfix với các kiểu dữ liệu tùy chỉnh, bạn cần rất ít:

mã bằng ngôn ngữ c++ công khai: void operator++ () ( counter += 1; ) void operator++ (int) ( counter += 1; )

Sự khác biệt duy nhất giữa hoạt động tiền tố và hoạt động hậu tố là từ khóa int trong danh sách đối số. Nhưng int không phải là một đối số! Từ này nói rằng toán tử postfix bị quá tải. Bây giờ toán tử ++ có thể được sử dụng cả trước và sau mã định danh đối tượng:

Bộ đếm a;
++a;
++a;
một++;
một++;

Quá tải hoạt động nhị phân

Nạp chồng các toán tử hai đối số rất giống với nạp chồng các toán tử nhị phân:

mã bằng ngôn ngữ c++ Toán tử bộ đếm+ (Bộ đếm t) ( Bộ đếm tổng; summ.counter = bộ đếm + t.counter; trả về tổng; ) Bộ đếm c1,c2,c3; c1.counter = 3; c2.bộ đếm = 2; c3 = c1 + c2;

Biến nào sẽ gọi hàm toán tử+? Trong các toán tử nhị phân bị quá tải, phương thức của toán hạng bên trái luôn được gọi. Trong trường hợp này, phương thức operator+ gọi đối tượng c1.

Chúng ta truyền một đối số cho phương thức. Đối số luôn là toán hạng đúng.

Ngoài ra, trong trường hợp này, thao tác + phải trả về một số kết quả để gán nó cho đối tượng c3. Chúng tôi trả về một đối tượng Counter. Giá trị trả về được gán cho biến c3.

Lưu ý rằng chúng ta đã nạp chồng toán tử + nhưng không nạp chồng toán tử =! Tất nhiên bạn cần thêm phương thức này vào lớp Counter:

mã bằng ngôn ngữ c++ Toán tử bộ đếm= (Bộ đếm t) ( Gán bộ đếm; bộ đếm = t.counter; gán.counter = t.counter; gán lại; )

Bên trong phương thức này, chúng ta đã tạo một biến gán bổ sung. Biến này được sử dụng để bạn có thể làm việc với đoạn mã sau:

Bộ đếm c1(5),c2,c3;
c3 = c2 = c1;

Tuy nhiên, bạn có thể sử dụng các phương pháp tinh tế hơn cho giá trị trả về mà chúng ta sẽ sớm làm quen.

Chúng ta đã đề cập đến những điều cơ bản về việc sử dụng nạp chồng toán tử. Trong tài liệu này, các toán tử C++ quá tải sẽ được trình bày cho bạn. Mỗi phần được đặc trưng bởi ngữ nghĩa, tức là. hành vi mong đợi. Ngoài ra, các cách điển hình để khai báo và thực hiện các toán tử sẽ được trình bày.

Trong các ví dụ về mã, X cho biết loại do người dùng xác định mà toán tử được triển khai. T là loại tùy chọn, do người dùng xác định hoặc tích hợp sẵn. Các tham số của toán tử nhị phân sẽ được đặt tên là lhs và rhs. Nếu một toán tử được khai báo là một phương thức lớp, khai báo của nó sẽ có tiền tố X:: .

toán tử=

  • Định nghĩa từ phải sang trái: Không giống như hầu hết các toán tử, operator= có tính kết hợp đúng, tức là. a = b = c có nghĩa là a = (b = c).

Sao chép

  • Ngữ nghĩa: bài tập a = b . Giá trị hoặc trạng thái của b được truyền cho a . Ngoài ra, một tham chiếu đến a được trả về. Điều này cho phép bạn tạo các chuỗi như c = a = b.
  • Quảng cáo điển hình: X& X::operator= (X const& rhs) . Có thể có các loại đối số khác nhưng chúng không được sử dụng thường xuyên.
  • Triển khai điển hình: X& X::operator= (X const& rhs) ( if (this != &rhs) ( //thực hiện sao chép phần tử thông minh, hoặc: X tmp(rhs); //copy constructor swap(tmp); ) return *this; )

Di chuyển (kể từ C++ 11)

  • Ngữ nghĩa: gán a = tạm thời() . Giá trị hoặc trạng thái của giá trị phù hợp được gán cho a bằng cách di chuyển nội dung. Một tham chiếu đến a được trả về.
  • : X& X::operator= (X&& rhs) ( //lấy can đảm từ rhs return *this; )
  • Trình biên dịch được tạo operator= : Trình biên dịch chỉ có thể tạo hai loại toán tử này. Nếu toán tử không được khai báo trong lớp, trình biên dịch sẽ cố gắng tạo các toán tử sao chép và di chuyển công khai. Kể từ C++ 11, trình biên dịch có thể tạo toán tử mặc định: X& X::operator= (X const& rhs) = default;

    Câu lệnh được tạo chỉ đơn giản là sao chép/di chuyển phần tử đã chỉ định nếu thao tác đó được cho phép.

toán tử+, -, *, /, %

  • Ngữ nghĩa: các phép tính cộng, trừ, nhân, chia, chia có số dư. Một đối tượng mới có giá trị kết quả được trả về.
  • Khai báo và thực hiện điển hình: Toán tử X+ (X const lhs, X const rhs) ( X tmp(lhs); tmp += rhs; return tmp; )

    Thông thường, nếu toán tử+ tồn tại, thì cũng nên nạp chồng toán tử+= để sử dụng ký hiệu a += b thay vì a = a + b . Nếu operator+= không bị quá tải, quá trình triển khai sẽ giống như thế này:

    X operator+ (X const& lhs, X const& rhs) ( // tạo một đối tượng mới biểu thị tổng của lhs và rhs: return lhs.plus(rhs); )

Toán tử một ngôi+, —

  • Ngữ nghĩa: dấu dương hoặc dấu âm. operator+ thường không làm gì cả và do đó hầu như không được sử dụng. toán tử- trả về đối số có dấu ngược lại.
  • Khai báo và thực hiện điển hình: X X::operator- () const ( return /* bản sao âm của *this */; ) X X::operator+ () const ( return *this; )

nhà điều hành<<, >>

  • Ngữ nghĩa: Trong các kiểu có sẵn, các toán tử được sử dụng để dịch chuyển bit đối số bên trái. Việc quá tải các toán tử này với chính xác ngữ nghĩa này là rất hiếm; điều duy nhất tôi nghĩ đến là std::bitset . Tuy nhiên, ngữ nghĩa mới đã được giới thiệu để làm việc với các luồng và việc nạp chồng các câu lệnh I/O là khá phổ biến.
  • Khai báo và thực hiện điển hình: vì bạn không thể thêm các phương thức vào các lớp iostream tiêu chuẩn, nên các toán tử dịch chuyển cho các lớp bạn xác định phải được nạp chồng dưới dạng các hàm tự do: ostream& operator<< (ostream& os, X const& x) { os << /* the formatted data of rhs you want to print */; return os; } istream& operator>> (istream& is, X& x) ( SomeData sd; SomeMoreData smd; if (is >> sd >> smd) ( rhs.setSomeData(sd); rhs.setSomeMoreData(smd); ) return lhs; )

    Ngoài ra, loại toán hạng bên trái có thể là bất kỳ lớp nào hoạt động giống như một đối tượng I/O, nghĩa là toán hạng bên phải có thể là loại có sẵn.

    MyIO & MyIO::nhà điều hành<< (int rhs) { doYourThingWith(rhs); return *this; }

Toán tử nhị phân&, |, ^

  • Ngữ nghĩa: Hoạt động bit “và”, “hoặc”, “độc quyền hoặc”. Những toán tử này rất hiếm khi bị quá tải. Một lần nữa, ví dụ duy nhất là std::bitset .

toán tử+=, -=, *=, /=, %=

  • Ngữ nghĩa: a += b thường có nghĩa giống như a = a + b . Hành vi của các nhà khai thác khác là tương tự.
  • Định nghĩa và cách thực hiện điển hình: Vì thao tác sửa đổi toán hạng bên trái nên việc truyền kiểu ẩn là không mong muốn. Do đó, các toán tử này phải được nạp chồng như các phương thức lớp. X& X::operator+= (X const& rhs) ( //áp dụng các thay đổi cho *this return *this; )

toán tử&=, |=, ^=,<<=, >>=

  • Ngữ nghĩa: tương tự như operator+= , nhưng dành cho các phép toán logic. Các toán tử này hiếm khi bị quá tải như operator| vân vân. nhà điều hành<<= и operator>>= không được sử dụng cho các thao tác I/O vì toán tử<< и operator>> đã thay đổi đối số bên trái.

toán tử==, !=

  • Ngữ nghĩa: Kiểm tra sự bình đẳng/bất bình đẳng. Ý nghĩa của sự bình đẳng rất khác nhau tùy theo giai cấp. Trong mọi trường hợp, hãy xem xét các tính chất sau của đẳng thức:
    1. Tính phản xạ, tức là một == một .
    2. Tính đối xứng, tức là nếu a == b thì b == a .
    3. Tính chuyển tiếp, tức là nếu a == b và b == c thì a == c .
  • Khai báo và thực hiện điển hình: bool operator== (X const& lhs, X cosnt& rhs) ( return /* kiểm tra xem có bằng nhau không */ ) bool operator!= (X const& lhs, X const& rhs) ( return !(lhs == rhs); )

    Việc triển khai thứ hai của operator!= tránh sự lặp lại mã và loại bỏ mọi sự mơ hồ có thể có liên quan đến hai đối tượng bất kỳ.

nhà điều hành<, <=, >, >=

  • Ngữ nghĩa: kiểm tra tỷ lệ (nhiều hơn, ít hơn, v.v.). Nó thường được sử dụng nếu thứ tự của các phần tử được xác định duy nhất, nghĩa là sẽ không có ý nghĩa gì khi so sánh các đối tượng phức tạp với một số đặc điểm.
  • Khai báo và thực hiện điển hình: toán tử bool< (X const& lhs, X const& rhs) { return /* compare whatever defines the order */ } bool operator>(X const& lhs, X const& rhs) ( return rhs< lhs; }

    Toán tử triển khai> sử dụng toán tử< или наоборот обеспечивает однозначное определение. operator<= может быть реализован по-разному, в зависимости от ситуации . В частности, при отношении строго порядка operator== можно реализовать лишь через operator< :

    Toán tử Bool== (X const& lhs, X const& rhs) ( return !(lhs< rhs) && !(rhs < lhs); }

toán tử++, –

  • Ngữ nghĩa: a++ (tăng sau) tăng giá trị lên 1 và trả về nghĩa. ++a (tăng trước) trả về mới nghĩa. Với toán tử giảm dần-- mọi thứ đều tương tự.
  • Khai báo và thực hiện điển hình: X& X::operator++() ( //preincrement /* bằng cách nào đó tăng lên, ví dụ *this += 1*/; return *this; ) X X::operator++(int) ( //postincrement X oldValue(*this); + +(*cái này); trả về oldValue; )

nhà điều hành()

  • Ngữ nghĩa: thực thi một đối tượng hàm (functor). Thường được sử dụng không phải để sửa đổi một đối tượng mà để sử dụng nó như một hàm.
  • Không hạn chế về tham số: Không giống như các toán tử trước, trong trường hợp này không có hạn chế nào về số lượng và loại tham số. Một toán tử chỉ có thể được nạp chồng như một phương thức lớp.
  • Quảng cáo mẫu: Foo X::operator() (Bar br, Baz const& bz);

nhà điều hành

  • Ngữ nghĩa: truy cập các phần tử của một mảng hoặc vùng chứa, ví dụ như trong std::vector , std::map , std::array .
  • Thông báo: Loại tham số có thể là bất cứ thứ gì. Kiểu trả về thường là tham chiếu đến những gì được lưu trữ trong vùng chứa. Thông thường toán tử bị quá tải ở hai phiên bản, const và không phải const: Element_t& X::operator(Index_t const& index); const Element_t& X::operator(Index_t const& index) const;

nhà điều hành!

  • Ngữ nghĩa: phủ định theo nghĩa logic.
  • Khai báo và thực hiện điển hình: bool X::operator!() const ( return !/*some đánh giá về *this*/; )

bool toán tử rõ ràng

  • Ngữ nghĩa: Sử dụng trong ngữ cảnh logic. Thường được sử dụng với con trỏ thông minh.
  • Thực hiện: rõ ràng X::operator bool() const ( return /* nếu điều này đúng hoặc sai */; )

toán tử&&, ||

  • Ngữ nghĩa: logic “và”, “hoặc”. Các toán tử này chỉ được xác định cho kiểu boolean tích hợp và hoạt động trên cơ sở lười biếng, nghĩa là đối số thứ hai chỉ được xem xét nếu đối số thứ nhất không xác định kết quả. Khi quá tải, thuộc tính này bị mất nên các toán tử này hiếm khi bị quá tải.

Toán tử một ngôi*

  • Ngữ nghĩa: Vô hiệu hóa con trỏ. Thường bị quá tải đối với các lớp có con trỏ và vòng lặp thông minh. Trả về một tham chiếu đến nơi đối tượng trỏ tới.
  • Khai báo và thực hiện điển hình: T& X::operator*() const ( return *_ptr; )

toán tử->

  • Ngữ nghĩa: Truy cập một trường bằng con trỏ. Giống như toán tử trước, toán tử này bị quá tải để sử dụng với các con trỏ và vòng lặp thông minh. Nếu gặp toán tử -> trong mã của bạn, trình biên dịch sẽ chuyển hướng lệnh gọi đến toán tử-> nếu kết quả của loại tùy chỉnh được trả về.
  • Thực hiện thông thường: T* X::operator->() const ( return _ptr; )

toán tử->*

  • Ngữ nghĩa: truy cập vào một con trỏ tới trường bằng con trỏ. Toán tử lấy một con trỏ tới một trường và áp dụng nó cho bất cứ thứ gì *this trỏ tới, vì vậy objPtr->*memPtr giống với (*objPtr).*memPtr . Rất hiếm khi được sử dụng.
  • Có thể triển khai: bản mẫu T& X::operator->*(T V::* memptr) ( return (operator*()).*memptr; )

    Ở đây X là con trỏ thông minh, V là loại được trỏ đến bởi X và T là loại được trỏ đến bởi con trỏ trường. Không có gì ngạc nhiên khi toán tử này hiếm khi bị quá tải.

Toán tử đơn nhất&

  • Ngữ nghĩa: toán tử địa chỉ. Toán tử này rất hiếm khi bị quá tải.

nhà điều hành

  • Ngữ nghĩa: Toán tử dấu phẩy tích hợp áp dụng cho hai biểu thức sẽ đánh giá cả hai theo thứ tự viết và trả về giá trị của biểu thức thứ hai. Không nên quá tải nó.

người điều hành~

  • Ngữ nghĩa: Toán tử đảo ngược bitwise. Một trong những toán tử hiếm khi được sử dụng nhất.

Toán tử đúc

  • Ngữ nghĩa: Cho phép truyền ngầm hoặc rõ ràng các đối tượng lớp sang các kiểu khác.
  • Thông báo: //chuyển đổi sang T, rõ ràng hoặc ẩn X::operator T() const; // chuyển đổi rõ ràng sang U const& rõ ràng X::operator U const&() const; //chuyển đổi sang V& V& X::operator V&();

    Những khai báo này trông lạ vì chúng thiếu kiểu trả về. Nó là một phần của tên nhà điều hành và không được chỉ định hai lần. Thật đáng để nhớ rằng một số lượng lớn những bóng ma ẩn giấu có thể đòi hỏi lỗi không mong muốn trong hoạt động của chương trình.

toán tử mới, mới, xóa, xóa

Các toán tử này hoàn toàn khác với tất cả các toán tử trên vì chúng không hoạt động với các kiểu do người dùng xác định. Quá tải của chúng rất phức tạp và do đó sẽ không được xem xét ở đây.

Phần kết luận

Ý tưởng chính là thế này: đừng quá tải các toán tử chỉ vì bạn biết cách thực hiện. Chỉ làm quá tải chúng trong trường hợp có vẻ tự nhiên và cần thiết. Nhưng hãy nhớ rằng nếu bạn nạp chồng một toán tử thì bạn sẽ phải nạp chồng các toán tử khác.

Cập nhật lần cuối: 12/08/2018

Cùng với các phương thức, chúng ta cũng có thể nạp chồng các toán tử. Ví dụ: giả sử chúng ta có lớp Counter sau:

Bộ đếm lớp ( Giá trị int công khai ( get; set; ) )

Lớp này đại diện cho một bộ đếm, giá trị của nó được lưu trữ trong thuộc tính Value.

Và giả sử chúng ta có hai đối tượng của lớp Counter - hai bộ đếm mà chúng ta muốn so sánh hoặc thêm dựa trên thuộc tính Value của chúng, sử dụng các phép toán so sánh và cộng tiêu chuẩn:

Bộ đếm c1 = Bộ đếm mới (Giá trị = 23); Bộ đếm c2 = Bộ đếm mới (Giá trị = 45); kết quả bool = c1 > c2; Bộ đếm c3 = c1 + c2;

Nhưng trên khoảnh khắc này Không có sự so sánh hay bổ sung nào cho các đối tượng Counter. Những thao tác này có thể được sử dụng trên một số kiểu nguyên thủy. Ví dụ: theo mặc định chúng ta có thể thêm giá trị số, nhưng làm thế nào để thêm đối tượng các loại phức tạp- trình biên dịch không biết các lớp và cấu trúc. Và để làm được điều này, chúng ta cần nạp chồng các toán tử mà chúng ta cần.

Quá tải toán tử bao gồm việc định nghĩa một phương thức đặc biệt trong lớp mà chúng ta muốn định nghĩa một toán tử cho các đối tượng của nó:

Toán tử (tham số) return_type tĩnh công khai ( )

Phương thức này phải có các công cụ sửa đổi tĩnh công khai vì quá tải toán tử sẽ được sử dụng cho tất cả các đối tượng của lớp này. Tiếp theo là tên của kiểu trả về. Kiểu trả về đại diện cho loại đối tượng mà chúng ta muốn nhận. Ví dụ: do việc thêm hai đối tượng Counter, chúng tôi hy vọng sẽ thu được một đối tượng Counter mới. Và kết quả của việc so sánh cả hai, chúng ta muốn nhận được một đối tượng thuộc loại bool, cho biết biểu thức điều kiện là đúng hay sai. Nhưng tùy thuộc vào nhiệm vụ, kiểu trả về có thể là bất cứ thứ gì.

Sau đó, thay vì tên của phương thức, sẽ có từ khóa operator và chính toán tử đó. Và sau đó các tham số được liệt kê trong ngoặc đơn. Toán tử nhị phân lấy hai tham số, toán tử một ngôi lấy một tham số. Và trong mọi trường hợp, một trong các tham số phải đại diện cho kiểu - lớp hoặc cấu trúc mà toán tử được xác định.

Ví dụ: hãy nạp chồng một số toán tử cho lớp Counter:

Bộ đếm lớp ( public int Value ( get; set; ) public static Toán tử bộ đếm +(Bộ đếm c1, Bộ đếm c2) ( trả về Bộ đếm mới ( Value = c1.Value + c2.Value ); ) toán tử bool tĩnh công khai >(Bộ đếm c1, Bộ đếm c2) ( return c1.Value > c2.Value; ) toán tử bool tĩnh công khai<(Counter c1, Counter c2) { return c1.Value < c2.Value; } }

Vì tất cả các toán tử quá tải đều là nhị phân, nghĩa là chúng được thực hiện trên hai đối tượng nên có hai tham số cho mỗi lần quá tải.

Vì trong trường hợp của phép cộng, chúng ta muốn cộng hai đối tượng của lớp Counter, nên toán tử chấp nhận hai đối tượng của lớp này. Và vì chúng ta muốn nhận được một đối tượng Counter mới nhờ phép cộng, nên lớp này cũng được sử dụng làm kiểu trả về. Tất cả các hành động của toán tử này đều bắt nguồn từ việc tạo một đối tượng mới, thuộc tính Value của đối tượng này kết hợp các giá trị của thuộc tính Value của cả hai tham số:

Toán tử bộ đếm tĩnh công cộng +(Bộ đếm c1, Bộ đếm c2) ( trả về Bộ đếm mới ( Value = c1.Value + c2.Value ); )

Hai toán tử so sánh cũng đã được định nghĩa lại. Nếu chúng ta ghi đè một trong các toán tử so sánh này thì chúng ta cũng phải ghi đè lên toán tử thứ hai trong số các toán tử này. Bản thân các toán tử so sánh so sánh các giá trị của thuộc tính Giá trị và tùy thuộc vào kết quả so sánh, trả về giá trị đúng hoặc sai.

Bây giờ chúng ta sử dụng các toán tử nạp chồng trong chương trình:

Tĩnh void Main(string args) ( Bộ đếm c1 = Bộ đếm mới ( Value = 23 ); Bộ đếm c2 = Bộ đếm mới ( Value = 45 ); kết quả bool = c1 > c2; Console.WriteLine(result); // false Bộ đếm c3 = c1 + c2; Console.WriteLine(c3.Value); // 23 + 45 = 68 Console.ReadKey(); )

Điều đáng chú ý là vì định nghĩa toán tử về cơ bản là một phương thức, nên chúng ta cũng có thể nạp chồng phương thức này, nghĩa là tạo một phiên bản khác cho nó. Ví dụ: hãy thêm một toán tử khác vào lớp Counter:

Toán tử int tĩnh công khai +(Bộ đếm c1, int val) ( return c1.Value + val; )

Phương thức này cộng giá trị của thuộc tính Value và một số nhất định, trả về tổng của chúng. Và chúng ta cũng có thể sử dụng toán tử này:

Bộ đếm c1 = Bộ đếm mới (Giá trị = 23); int d = c1 + 27; // 50 Console.WriteLine(d);

Cần lưu ý rằng khi quá tải, những đối tượng được truyền cho người vận hành thông qua các tham số sẽ không thay đổi. Ví dụ: chúng ta có thể định nghĩa toán tử tăng dần cho lớp Counter:

Toán tử bộ đếm tĩnh công cộng ++(Bộ đếm c1) ( c1.Value += 10; return c1; )

Vì toán tử là một ngôi nên nó chỉ lấy một tham số - một đối tượng của lớp trong đó toán tử nàyđược xác định Nhưng đây là một định nghĩa không chính xác về mức tăng, vì toán tử không được thay đổi giá trị của các tham số của nó.

Và một cách nạp chồng chính xác hơn của toán tử tăng sẽ như thế này:

Toán tử bộ đếm tĩnh công khai ++(Bộ đếm c1) ( trả về Bộ đếm mới ( Value = c1.Value + 10 ); )

Nghĩa là, một đối tượng mới được trả về có chứa giá trị tăng dần trong thuộc tính Giá trị.

Trong trường hợp này, chúng ta không cần xác định các toán tử riêng biệt cho tiền tố và hậu tố tăng (cũng như giảm), vì một triển khai sẽ hoạt động trong cả hai trường hợp.

Ví dụ: chúng tôi sử dụng thao tác tăng tiền tố:

Bộ đếm bộ đếm = Bộ đếm mới() ( Giá trị = 10 ); Console.WriteLine($"(counter.Value)"); // 10 Console.WriteLine($"((++counter).Value)"); // 20 Console.WriteLine($"(counter.Value)"); // 20

Đầu ra của bảng điều khiển:

Bây giờ chúng tôi sử dụng tăng hậu tố:

Bộ đếm bộ đếm = Bộ đếm mới() ( Giá trị = 10 ); Console.WriteLine($"(counter.Value)"); // 10 Console.WriteLine($"((counter++).Value)"); // 10 Console.WriteLine($"(counter.Value)"); // 20

Đầu ra của bảng điều khiển:

Cũng cần lưu ý rằng chúng ta có thể ghi đè các toán tử đúng và sai. Ví dụ: hãy định nghĩa chúng trong lớp Counter:

Bộ đếm lớp ( public int Value ( get; set; ) public static bool operator true(Counter c1) ( return c1.Value != 0; ) public static bool operator false(Counter c1) ( return c1.Value == 0; ) // nội dung còn lại của lớp)

Các toán tử này bị quá tải khi chúng ta muốn sử dụng một đối tượng thuộc một loại làm điều kiện. Ví dụ:

Bộ đếm bộ đếm = Bộ đếm mới() ( Giá trị = 0 ); if (bộ đếm) Console.WriteLine(true); khác Console.WriteLine(false);

Khi nạp chồng các toán tử, bạn phải tính đến việc không phải tất cả các toán tử đều có thể bị nạp chồng. Cụ thể, chúng ta có thể nạp chồng các toán tử sau:

    toán tử một ngôi +, -, !, ~, ++, --

    toán tử nhị phân +, -, *, /, %

    các phép toán so sánh ==, !=,<, >, <=, >=

    toán tử logic &&, ||

    toán tử gán +=, -=, *=, /=, %=

Và có một số toán tử không thể được nạp chồng, ví dụ, toán tử đẳng thức = hoặc toán tử ba ngôi?:, cũng như một số toán tử khác.

Bạn có thể tìm thấy danh sách đầy đủ các toán tử bị quá tải trong tài liệu msdn

Khi nạp chồng toán tử, chúng ta cũng cần nhớ rằng chúng ta không thể thay đổi mức độ ưu tiên của toán tử hoặc tính kết hợp của nó, chúng ta không thể tạo toán tử mới hoặc thay đổi logic của toán tử theo loại, là mặc định trong .NET.

Ngày tốt!

Mong muốn viết bài viết này xuất hiện sau khi đọc bài đăng, vì nhiều chủ đề quan trọng không được đề cập trong đó.

Điều quan trọng nhất cần nhớ là việc nạp chồng toán tử chỉ là Một cách thuận tiện các lệnh gọi hàm, vì vậy đừng quá lo lắng về việc quá tải toán tử. Nó chỉ nên được sử dụng khi nó giúp việc viết mã dễ dàng hơn. Nhưng không quá nhiều đến mức gây khó khăn cho việc đọc. Rốt cuộc, như bạn đã biết, mã được đọc thường xuyên hơn nhiều so với việc viết. Và đừng quên rằng bạn sẽ không bao giờ được phép quá tải các toán tử song song với các kiểu có sẵn; khả năng quá tải chỉ khả dụng đối với các kiểu/lớp do người dùng xác định.

Cú pháp quá tải

Cú pháp nạp chồng toán tử rất giống với việc định nghĩa một hàm có tên operator@, trong đó @ là định danh toán tử (ví dụ +, -,<<, >>). Hãy xem xét ví dụ đơn giản nhất:
lớp Số nguyên ( riêng tư: int value; public: Integer(int i): value(i) () const Toán tử số nguyên+(const Integer& rv) const ( return (value + rv.value); ) );
Trong trường hợp này, toán tử được đóng khung như một thành viên của một lớp, đối số xác định giá trị nằm ở bên phải của toán tử. Nói chung, có hai cách chính để nạp chồng các toán tử: các hàm toàn cục thân thiện với lớp hoặc các hàm nội tuyến của chính lớp đó. Chúng tôi sẽ xem xét phương pháp nào tốt hơn cho toán tử nào ở cuối chủ đề.

Trong hầu hết các trường hợp, các toán tử (ngoại trừ các toán tử có điều kiện) trả về một đối tượng hoặc một tham chiếu đến loại mà các đối số của nó thuộc về (nếu các loại khác nhau thì bạn quyết định cách diễn giải kết quả đánh giá của toán tử).

Quá tải các toán tử đơn nguyên

Hãy xem các ví dụ về nạp chồng toán tử một ngôi cho lớp Số nguyên được xác định ở trên. Đồng thời, hãy định nghĩa chúng dưới dạng các hàm thân thiện và xem xét các toán tử tăng và giảm:
class Integer ( private: int value; public: Integer(int i): value(i) () //unary +friend const Integer& operator+(const Integer& i); //unary -friend const Integer operator-(const Integer& i) ; // tăng tiền tố bạn const Integer& operator++(Integer& i); // tăng hậu tố bạn const Số nguyên operator++(Số nguyên& i, int); // giảm tiền tố bạn bè const Integer& operator--(Số nguyên& i); // giảm tiền tố bạn bè const Toán tử số nguyên--(Số nguyên& i, int); ); // dấu cộng đơn nhất không làm gì cả. const Integer& operator+(const Integer& i) ( return i.value; ) const Integer operator-(const Integer& i) ( return Integer(-i.value); ) //phiên bản tiền tố trả về giá trị sau khi tăng const Integer& operator++(Integer& i) ( i.value++; return i; ) //phiên bản postfix trả về giá trị trước số tăng const Integer operator++(Integer& i, int) ( Integer oldValue(i.value); i.value++; return oldValue; ) // phiên bản tiền tố trả về giá trị sau khi giảm const Integer& operator--(Integer& i) ( i.value--; return i; ) //phiên bản postfix trả về giá trị trước khi giảm const Toán tử số nguyên--(Integer& i, int) ( Integer oldValue(i. value); i .value--;return oldValue; )
Bây giờ bạn đã biết cách trình biên dịch phân biệt giữa phiên bản tiền tố và hậu tố của mức tăng và giảm. Trong trường hợp nó nhìn thấy biểu thức ++i thì hàm operator++(a) được gọi. Nếu nó nhìn thấy i++ thì operator++(a, int) sẽ được gọi. Nghĩa là, hàm operator++ bị quá tải được gọi và đây là mục đích của tham số int giả trong phiên bản postfix.

Toán tử nhị phân

Chúng ta hãy xem cú pháp nạp chồng các toán tử nhị phân. Hãy nạp chồng một toán tử trả về giá trị l, một toán tử điều hành có điều kiện và một toán tử tạo một giá trị mới (hãy xác định chúng trên toàn cầu):
lớp Số nguyên ( riêng tư: giá trị int; public: Số nguyên(int i): value(i) () bạn const Số nguyên operator+(const Số nguyên& trái, const Số nguyên& phải); bạn Số nguyên& toán tử+=(Số nguyên& trái, const Số nguyên& phải); bạn bè toán tử bool==(const Integer& left, const Integer& right); ); const Toán tử số nguyên+(const Số nguyên& left, const Số nguyên& phải) ( return Integer(left.value + right.value); ) Integer& operator+=(Integer& left, const Integer& right) ( left.value += right.value; return left; ) bool operator==(const Integer& left, const Integer& right) ( return left.value == right.value; )
Trong tất cả các ví dụ này, các toán tử đều được nạp chồng cho cùng một kiểu, tuy nhiên, điều này là không cần thiết. Ví dụ: bạn có thể nạp chồng việc thêm loại Số nguyên và Float được xác định bởi tính tương tự của nó.

Đối số và giá trị trả về

Như bạn có thể thấy, các ví dụ sử dụng nhiều cách khác nhau truyền đối số cho hàm và trả về giá trị toán tử.
  • Nếu đối số không được sửa đổi bởi toán tử, chẳng hạn như trong trường hợp dấu cộng đơn phân, thì đối số đó phải được chuyển dưới dạng tham chiếu đến một hằng số. Nói chung, điều này đúng với hầu hết các toán tử số học (cộng, trừ, nhân...)
  • Kiểu giá trị trả về phụ thuộc vào bản chất của toán tử. Nếu toán tử phải trả về một giá trị mới thì một đối tượng mới phải được tạo (như trong trường hợp dấu cộng nhị phân). Nếu bạn muốn ngăn một đối tượng bị sửa đổi dưới dạng giá trị l, thì bạn cần trả về nó dưới dạng một hằng số.
  • Toán tử gán phải trả về tham chiếu đến phần tử đã thay đổi. Ngoài ra, nếu bạn muốn sử dụng toán tử gán trong các cấu trúc như (x=y).f(), trong đó hàm f() được gọi cho biến x, sau khi gán nó cho y, thì đừng trả về một tham chiếu đến không đổi, chỉ cần trả về một tham chiếu.
  • Các toán tử logic sẽ trả về int ở mức tệ nhất và tốt nhất là bool.

Tối ưu hóa giá trị trả về

Khi tạo các đối tượng mới và trả về chúng từ một hàm, bạn nên sử dụng ký hiệu tương tự như ví dụ về toán tử cộng nhị phân được mô tả ở trên.
return Integer(left.value + right.value);
Thành thật mà nói, tôi không biết tình huống nào có liên quan đến C++11; tất cả các đối số khác đều hợp lệ cho C++98.
Thoạt nhìn, điều này trông giống với cú pháp tạo một đối tượng tạm thời, nghĩa là dường như không có sự khác biệt giữa đoạn mã trên và đoạn mã này:
Nhiệt độ số nguyên(left.value + right.value); trở lại nhiệt độ;
Nhưng trên thực tế, trong trường hợp này, hàm tạo sẽ được gọi ở dòng đầu tiên, sau đó hàm tạo sao chép sẽ được gọi, hàm tạo này sẽ sao chép đối tượng và sau đó, khi giải nén ngăn xếp, hàm hủy sẽ được gọi. Khi sử dụng mục nhập đầu tiên, trình biên dịch ban đầu sẽ tạo đối tượng trong bộ nhớ mà nó cần được sao chép vào, do đó lưu các lệnh gọi đến hàm tạo và hàm hủy sao chép.

Người điều hành đặc biệt

C++ có các toán tử có cú pháp và phương thức nạp chồng cụ thể. Ví dụ: toán tử lập chỉ mục. Nó luôn được định nghĩa là thành viên của một lớp và vì đối tượng được lập chỉ mục có mục đích hoạt động giống như một mảng nên nó sẽ trả về một tham chiếu.
Toán tử dấu phẩy
Các toán tử "đặc biệt" cũng bao gồm toán tử dấu phẩy. Nó được gọi trên các đối tượng có dấu phẩy bên cạnh (nhưng nó không được gọi trong danh sách đối số hàm). Việc tạo ra một trường hợp sử dụng có ý nghĩa cho toán tử này không phải là điều dễ dàng. Habrauser trong phần bình luận cho bài viết trước về tình trạng quá tải.
Toán tử quy chiếu con trỏ
Việc nạp chồng các toán tử này có thể hợp lý cho các lớp con trỏ thông minh. Toán tử này nhất thiết phải được định nghĩa là một hàm lớp và một số hạn chế được áp đặt cho nó: nó phải trả về một đối tượng (hoặc một tham chiếu) hoặc một con trỏ cho phép truy cập vào đối tượng.
Toán tử gán
Toán tử gán nhất thiết phải được định nghĩa là một hàm lớp vì về bản chất nó được liên kết với đối tượng ở bên trái dấu "=". Việc xác định toán tử gán trên toàn cầu sẽ giúp có thể ghi đè hành vi mặc định của toán tử "=". Ví dụ:
class Integer ( private: int value; public: Integer(int i): value(i) () Integer& operator=(const Integer& right) ( //kiểm tra khả năng tự gán if (this == &right) ( return *this; ) value = right.value; return *this; ) );

Như bạn có thể thấy, khi bắt đầu hàm, một kiểm tra được thực hiện để tự gán. Nói chung, trong trường hợp này, việc tự chiếm đoạt là vô hại, nhưng sự việc không phải lúc nào cũng đơn giản như vậy. Ví dụ: nếu đối tượng lớn, bạn có thể dành nhiều thời gian cho sao chép không cần thiết hoặc khi làm việc với con trỏ.

Toán tử không thể quá tải
Một số toán tử trong C++ không bị quá tải chút nào. Rõ ràng điều này đã được thực hiện vì lý do an ninh.
  • Toán tử chọn thành viên lớp ".".
  • Toán tử để hủy tham chiếu một con trỏ tới thành viên lớp ".*"
  • Trong C++ không có toán tử lũy thừa (như trong Fortran) "**".
  • Cấm xác định toán tử của riêng bạn (có thể có vấn đề với việc xác định mức độ ưu tiên).
  • Ưu tiên của nhà điều hành không thể thay đổi
Như chúng ta đã tìm ra, có hai cách sử dụng toán tử - dưới dạng hàm lớp và hàm toàn cục thân thiện.
Rob Murray, trong cuốn sách Chiến lược và chiến thuật C++ của mình, đã định nghĩa những khuyến nghị sau đây bằng cách lựa chọn hình thức toán tử:

Tại sao vậy? Thứ nhất, một số toán tử ban đầu bị hạn chế. Nói chung, nếu không có sự khác biệt về ngữ nghĩa trong cách xác định toán tử thì tốt hơn nên thiết kế nó như một hàm lớp để nhấn mạnh sự kết nối, ngoài ra hàm sẽ là nội tuyến. Ngoài ra, đôi khi có thể cần phải biểu diễn toán hạng bên trái như một đối tượng của lớp khác. Có lẽ ví dụ nổi bật nhất là việc định nghĩa lại<< и >> cho các luồng I/O.

Văn học

Bruce Eckel - Triết học C++. Giới thiệu về C++ tiêu chuẩn.

Thẻ:

  • C++
  • quá tải toán tử
  • quá tải toán tử
Thêm thẻ