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

C++ hỗ trợ nạp chồng toán tử. Với một vài ngoại lệ, hầu hết các toán tử C++ có thể bị quá tải, khiến chúng có ý nghĩa đặc biệt đối với một số lớp nhất định. Ví dụ: một lớp định nghĩa danh sách liên kết có thể sử dụng toán tử + để thêm một đối tượng vào danh sách. Một lớp khác có thể sử dụng toán tử + theo một cách hoàn toàn khác. Khi một toán tử bị quá tải, không có ý nghĩa ban đầu nào của nó có ý nghĩa. Chỉ là một toán tử mới được định nghĩa cho một lớp đối tượng nhất định. Do đó, việc nạp chồng toán tử + để xử lý danh sách liên kết không làm thay đổi hành vi của nó đối với số nguyên.

Các hàm toán tử thường là thành viên hoặc bạn bè của lớp mà chúng được sử dụng. Mặc dù có nhiều điểm tương đồng, nhưng có một số khác biệt giữa cách thức nạp chồng các hàm toán tử thành viên và các hàm toán tử bạn bè. Trong phần này, chúng ta sẽ xem xét việc nạp chồng chỉ các hàm thành viên. Phần sau của chương này chúng ta sẽ chỉ ra cách nạp chồng các hàm toán tử bạn bè.

Để nạp chồng một toán tử, bạn phải xác định chính xác ý nghĩa của toán tử đó liên quan đến lớp mà nó được áp dụng. Để làm điều này, một hàm toán tử được xác định để chỉ định hành động của toán tử. Dạng viết chung của hàm toán tử cho trường hợp nó là thành viên của một lớp có dạng:

gõ class_name::operator#(argument_list)
{
// các hành động được xác định liên quan đến lớp
}

Ở đây, toán tử quá tải được thay thế cho ký hiệu # và kiểu chỉ định loại giá trị được toán tử trả về. Để dễ dàng hơn khi sử dụng toán tử tái định nghĩa trong
Trong các biểu thức phức tạp, giá trị trả về thường được chọn cùng loại với lớp mà toán tử được nạp chồng. Bản chất của danh sách đối số được xác định bởi một số yếu tố, như sẽ thấy bên dưới.

Để xem quá trình nạp chồng toán tử hoạt động như thế nào, hãy bắt đầu với ví dụ đơn giản. Nó tạo ra một lớp three_d chứa tọa độ của một đối tượng trong không gian ba chiều. Chương trình sau đây nạp chồng các toán tử + và = cho lớp three_d:

#bao gồm
lớp ba_d(

công cộng:
toán tử three_d+(ba_d t);
toán tử three_d=(ba_d t);
vô hiệu hiển thị();

};
// quá tải +
three_d three_d::operator+(ba_d t)
{
nhiệt độ ba_d;
temp.x = x+t.x;
temp.y = y+t.y;
temp.z = z+t.z;
trở lại nhiệt độ;
}
// quá tải =
three_d three_d::operator=(ba_d t)
{
x = t.x;
y = ty;
z = t.z;
trả lại *cái này;
}
// tọa độ đầu ra X, Y, Z
void three_d::show()
{
cout<< x << ", ";
cout<< у << ", ";
cout<< z << "\n";
}
// gán tọa độ

{
x = mx;
y = của tôi;
z = mz;
}
int chính()
{
ba_d a, b, c;
a.gán (1, 2, 3);
b.gán (10, 10, 10);
một chương trình();
b.show();
c = a+b; // cộng a và b
c.show();

c.show();

c.show();
b.show();
trả về 0;
}

Chương trình này hiển thị dữ liệu sau:

1, 2, 3
10, 10, 10
11, 12, 13
22, 24, 26
1, 2, 3
1, 2, 3

Nếu bạn xem xét kỹ chương trình này, có thể bạn sẽ ngạc nhiên khi thấy cả hai hàm toán tử chỉ có một tham số, mặc dù thực tế là chúng làm quá tải toán tử nhị phân. Điều này là do khi bạn nạp chồng một toán tử nhị phân bằng cách sử dụng hàm thành viên, chỉ có một đối số được truyền rõ ràng cho nó. Đối số thứ hai là con trỏ this, được truyền ngầm cho nó. Vâng, trong dòng

Nhiệt độ.x = x + t.x;

X tương ứng với this->x, trong đó x được liên kết với đối tượng gọi hàm toán tử. Trong mọi trường hợp, chính đối tượng ở bên trái của dấu hiệu phép toán sẽ gọi hàm toán tử. Đối tượng ở bên phải dấu hiệu phép toán được truyền cho hàm.

Khi nạp chồng một phép toán đơn nguyên, hàm toán tử không có tham số và khi nạp chồng phép toán nhị phân, hàm toán tử có một tham số. (Bạn không thể nạp chồng toán tử bộ ba được sao?:.) Trong mọi trường hợp, đối tượng gọi hàm toán tử được truyền ngầm bằng cách sử dụng con trỏ this.

Để hiểu cách hoạt động của việc nạp chồng toán tử, hãy phân tích cẩn thận cách thức hoạt động của chương trình trước đó, bắt đầu với toán tử + bị nạp chồng. Khi hai đối tượng thuộc loại three_d được hiển thị với toán tử +, các giá trị tọa độ tương ứng của chúng sẽ được thêm vào, như được hiển thị trong hàm operator+() được liên kết với lớp này. Tuy nhiên, xin lưu ý rằng hàm không sửa đổi giá trị của toán hạng. Thay vào đó, nó trả về một đối tượng kiểu three_d chứa kết quả của phép toán. Để hiểu tại sao toán tử + không thay đổi nội dung của đối tượng, bạn có thể tưởng tượng toán tử số học + được áp dụng tiêu chuẩn như sau: 10 + 12. Kết quả của thao tác này là 22, nhưng cả 10 và 12 đều không thay đổi. Mặc dù không có quy tắc nào cho phép toán tử nạp chồng không thể thay đổi giá trị của các toán hạng của nó, nhưng việc tuân theo quy tắc đó thường có ý nghĩa. Quay lại ví dụ này, toán tử + không nên thay đổi nội dung của toán hạng.

Một điểm quan trọng khác về việc nạp chồng toán tử phép cộng là nó trả về một đối tượng kiểu three_d. Mặc dù hàm có thể lấy bất kỳ kiểu C++ hợp lệ nào làm giá trị của nó, nhưng thực tế là nó trả về một đối tượng thuộc kiểu three_d cho phép toán tử + được sử dụng trong các biểu thức phức tạp hơn như a+b+c. Ở đây a+b tạo ra kết quả thuộc loại three_d. Giá trị này sau đó được thêm vào c. Nếu giá trị của tổng a+b là một giá trị thuộc loại khác thì chúng ta không thể cộng nó vào c.

Không giống như toán tử +, toán tử gán sửa đổi các đối số của nó. (Điều này, cùng với những điều khác, là ý nghĩa của phép gán.) Vì hàm operator=() được gọi bởi đối tượng ở bên trái dấu bằng, nên chính đối tượng này đã được sửa đổi
khi thực hiện một thao tác gán. Tuy nhiên, ngay cả toán tử gán cũng phải trả về một giá trị, vì trong cả C++ và C, toán tử gán tạo ra giá trị ở vế phải của đẳng thức. Vì vậy, để biểu thức có dạng sau

A = b = c = d;

Việc yêu cầu operator=() trả về đối tượng được trỏ bởi con trỏ this là hợp pháp, đối tượng này sẽ là đối tượng ở phía bên trái của toán tử gán. Nếu bạn làm theo cách này, bạn có thể thực hiện nhiều bài tập.

Có thể quá tải các toán tử một ngôi như ++ hoặc --. Như đã nêu trước đó, khi bạn nạp chồng toán tử một ngôi bằng cách sử dụng hàm thành viên, hàm thành viên sẽ không có đối số. Thay vào đó, thao tác được thực hiện trên một đối tượng gọi hàm toán tử bằng cách truyền ngầm con trỏ this. Ví dụ: bên dưới là phiên bản mở rộng của chương trình trước, định nghĩa toán tử tăng dần cho đối tượng thuộc loại three_d:

#bao gồm
lớp ba_d(
int x, y, z; // tọa độ 3D
công cộng:
toán tử three_d+(ba_d op2); // op1 được ngụ ý
toán tử three_d=(ba_d op2); // op1 được ngụ ý
toán tử three_d++(); // op1 cũng được ngụ ý
vô hiệu hiển thị();
void gán(int mx, int my, int mz);
};
// quá tải +
three_d three_d::operator+(ba_d op2)
{
nhiệt độ ba_d;
temp.x = x+op2.x; // phép cộng số nguyên
temp.у = y+op2.y; // và trong trong trường hợp này+ tiết kiệm
temp.z = z+op2.z; // giá trị ban đầu
trở lại nhiệt độ;
}
// quá tải =
three_d three_d::operator=(ba_d op2)
{
x = op2.x; // phép gán số nguyên
y = op2.y; // và trong trường hợp này = lưu
z = op2.z; // giá trị ban đầu
trả lại *cái này;
}
// nạp chồng toán tử một ngôi
three_d three_d::operator++()
{
x++;
y++;
z++;
trả lại *cái này;
}
// hiển thị tọa độ X, Y, Z
void three_d::show()
{
cout<< x << ", ";
cout<< у << ", ";
cout<< z << "\n";
}
// gán tọa độ
void three_d::gán (int mx, int my, int mz)
{
x = mx;
y = của tôi;
z = mz;
}
int chính()
{
ba_d a, b, c;
a.gán (1, 2, 3);
b.gán (10, 10, 10);
một chương trình();
b.show();
c = a+b; // cộng a và b
c.show();
c = a+b+c; // cộng a, b và c
c.show();
c = b = a; // chứng minh phép gán nhiều lần
c.show();
b.show();
++c; // tăng từ
c.show();
trả về 0;
}

Trong các phiên bản đầu tiên của C++, không thể xác định liệu một toán hạng có trước hay theo sau bởi toán tử ++ hoặc -- bị quá tải hay không. Ví dụ: đối với đối tượng O, hai hướng dẫn sau giống hệt nhau:

O++;
++O;

Tuy nhiên, các phiên bản sau này của C++ cho phép phân biệt giữa dạng tiền tố và hậu tố của toán tử tăng và giảm. Để làm điều này, chương trình phải xác định hai phiên bản của hàm operator++(). Một trong số chúng phải giống như trong chương trình trước. còn lại được khai báo như sau:

Toán tử Loc++(int x);

Nếu ++ đứng trước toán hạng thì toán tử++() sẽ được gọi. Nếu ++ theo sau toán hạng thì hàm operator++(int x) được gọi, trong đó x nhận giá trị 0.

Tác động của một toán tử quá tải lên lớp mà nó được định nghĩa không nhất thiết phải tương ứng với tác động của toán tử đó lên các kiểu có sẵn của C++ theo bất kỳ cách nào. Ví dụ, các nhà khai thác<< и >> khi được áp dụng cho cout và cin ít liên quan đến ảnh hưởng của chúng lên các biến kiểu số nguyên. Tuy nhiên, trong nỗ lực làm cho mã dễ đọc hơn và có cấu trúc tốt hơn, điều mong muốn là các toán tử quá tải phải phù hợp, nếu có thể, với ý nghĩa của các toán tử ban đầu. Ví dụ, toán tử + cho lớp three_d về mặt khái niệm tương tự như toán tử + cho các biến kiểu số nguyên. Chẳng hạn, có thể mong đợi một chút hữu ích từ toán tử + như vậy, tác dụng của nó đối với
lớp tương ứng sẽ giống với hành động của toán tử ||. Mặc dù bạn có thể gán cho toán tử quá tải bất kỳ ý nghĩa nào bạn chọn, nhưng để dễ sử dụng, điều mong muốn là ý nghĩa mới của nó phải liên quan đến ý nghĩa ban đầu.

Có một số hạn chế về quá tải toán tử. Đầu tiên, bạn không thể thay đổi quyền ưu tiên của toán tử. Thứ hai, bạn không thể thay đổi số toán hạng của toán tử. Cuối cùng, ngoại trừ toán tử gán, các toán tử tái định nghĩa được kế thừa bởi bất kỳ lớp dẫn xuất nào. Mỗi lớp phải định nghĩa rõ ràng toán tử = quá tải của riêng nó nếu nó được yêu cầu cho bất kỳ mục đích nào. Tất nhiên, các lớp dẫn xuất có thể làm quá tải bất kỳ toán tử nào, kể cả toán tử đã bị lớp cơ sở làm quá tải. Các toán tử sau không thể bị quá tải:
. :: * ?

Khái niệm cơ bản về nạp chồng toán tử

C#, giống như bất kỳ ngôn ngữ lập trình nào, có một bộ mã thông báo làm sẵn được sử dụng để thực hiện các thao tác cơ bản trên các loại có sẵn. Ví dụ: người ta biết rằng phép toán + có thể được áp dụng cho hai số nguyên để tính tổng của chúng:

// Phép toán + với số nguyên. int a = 100; int b = 240; int c = a + b; //s bây giờ bằng 340

Không có gì mới ở đây, nhưng bạn đã bao giờ nghĩ rằng thao tác + tương tự có thể được áp dụng cho hầu hết các kiểu dữ liệu có sẵn của C# chưa? Ví dụ: hãy xem xét mã này:

// Thao tác + với chuỗi. chuỗi si = "Xin chào"; chuỗi s2 = "thế giới!"; chuỗi s3 = si + s2; // s3 bây giờ chứa "Xin chào thế giới!"

Về cơ bản, chức năng của thao tác + chỉ dựa trên các kiểu dữ liệu được biểu thị (chuỗi hoặc số nguyên trong trường hợp này). Khi phép toán + được áp dụng cho các kiểu số, chúng ta thu được tổng số học của các toán hạng. Tuy nhiên, khi áp dụng thao tác tương tự cho các loại chuỗi, kết quả là nối chuỗi.

Ngôn ngữ C# cung cấp khả năng xây dựng các lớp và cấu trúc đặc biệt cũng phản hồi duy nhất với cùng một bộ mã thông báo cơ bản (như toán tử +). Hãy nhớ rằng tuyệt đối mọi toán tử C# tích hợp đều không thể bị quá tải. Bảng sau mô tả khả năng nạp chồng của các thao tác cơ bản:

Hoạt động C# Khả năng quá tải
+, -, !, ++, --, đúng, sai Tập hợp các toán tử đơn nguyên này có thể bị quá tải
+, -, *, /, %, &, |, ^, > Các hoạt động nhị phân này có thể bị quá tải
==, !=, <, >, <=, >= Các toán tử so sánh này có thể bị quá tải. C# yêu cầu nạp chồng chung các toán tử "like" (tức là< и >, <= и >=, == và!=)
Hoạt động không thể bị quá tải. Tuy nhiên, người lập chỉ mục cung cấp chức năng tương tự
() Hoạt động () không thể bị quá tải. Tuy nhiên, các phương pháp chuyển đổi đặc biệt cung cấp chức năng tương tự
+=, -=, *=, /=, %=, &=, |=, ^=, >= Toán tử gán ngắn không thể bị quá tải; tuy nhiên, bạn nhận được chúng một cách tự động bằng cách nạp chồng phép toán nhị phân tương ứng

Quá tải toán tử có liên quan chặt chẽ đến quá tải phương thức. Để nạp chồng một toán tử, hãy sử dụng từ khóa nhà điều hành, định nghĩa một phương thức toán tử, từ đó xác định hành động của toán tử liên quan đến lớp của nó. Có hai dạng phương thức toán tử: một dạng dành cho toán tử một ngôi, dạng kia dành cho toán tử nhị phân. Dưới đây là dạng chung cho từng biến thể của các phương pháp này:

// Dạng tổng quát của nạp chồng toán tử một ngôi. public static return_type operator op(parameter_type operand) ( // hoạt động ) // Dạng chung của nạp chồng toán tử nhị phân. toán tử return_type tĩnh công khai op(parameter_type1 toán hạng1, tham số_type2 toán hạng2) ( // hoạt động )

Ở đây op được thay thế bằng một toán tử tái định nghĩa, ví dụ + hoặc /, và return_type biểu thị loại giá trị cụ thể được trả về bởi thao tác đã chỉ định. Giá trị này có thể thuộc bất kỳ loại nào, nhưng thường được chỉ định là cùng loại với lớp mà toán tử được nạp chồng. Mối tương quan này làm cho việc sử dụng các toán tử nạp chồng trong biểu thức trở nên dễ dàng hơn. Đối với toán tử đơn nguyên toán hạng biểu thị toán hạng được truyền và đối với các toán tử nhị phân, điều tương tự cũng được biểu thị toán hạng1toán hạng2. Lưu ý rằng các phương thức toán tử phải có cả bộ xác định kiểu công khai và kiểu tĩnh.

Quá tải toán tử nhị phân

Chúng ta hãy xem việc sử dụng nạp chồng toán tử nhị phân bằng một ví dụ đơn giản:

Sử dụng hệ thống; sử dụng System.Collections.Generic; sử dụng System.Linq; sử dụng System.Text; namespace ConsoleApplication1 ( class MyArr ( // Tọa độ của một điểm trong không gian ba chiều 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; ) // Quá tải toán tử nhị phân + toán tử MyArr tĩnh công cộng +(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; ) // Quá tải toán tử nhị phân - toán tử MyArr tĩnh công khai -(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; ) ) class Program ( static void Main (string args) ( MyArr Point1 = new MyArr(1, 12, -4); MyArr Point2 = new MyArr(0, -3, 18); Console.WriteLine("Toạ độ của điểm đầu tiên: " + Point1.x + " " + Point1.y + " " + Point1.z); Console.WriteLine(" Tọa độ của điểm thứ hai: " + Point2.x + " " + Point2.y + " " + Point2. z + "\n"); MyArr Point3 = Point1 + Point2; Console.WriteLine("\nPoint1 + Point2 = " + Point3.x + " " + Point3.y + " " + Point3.z); Điểm3 = Điểm1 - Điểm2; Console.WriteLine("\nPoint1 - Point2 = " + Point3.x + " " + Point3.y + " " + Point3.z); Console.ReadLine(); ) ) )

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

Các toán tử một ngôi bị quá tải giống như các toán tử nhị phân. Tất nhiên, sự khác biệt chính là chúng chỉ có một toán hạng. Hãy hiện đại hóa ví dụ trước bằng cách thêm các nạp chồng toán tử ++, --, -:

Sử dụng hệ thống; sử dụng System.Collections.Generic; sử dụng System.Linq; sử dụng System.Text; namespace ConsoleApplication1 ( class MyArr ( // Tọa độ của một điểm trong không gian ba chiều 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; ) // Quá tải toán tử nhị phân + toán tử MyArr tĩnh công khai +(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; ) // Quá tải toán tử nhị phân - toán tử MyArr tĩnh công khai -(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; ) / / Quá tải toán tử một ngôi - public static MyArr operator -(MyArr obj1) ( MyArr arr = new MyArr(); arr.x = -obj1.x; arr.y = -obj1.y; arr.z = -obj1.z ; return arr; ) // Quá tải toán tử một ngôi ++ public static MyArr operator ++(MyArr obj1) ( obj1.x += 1; obj1.y += 1; obj1.z +=1; return obj1; ) / / Quá tải toán tử một ngôi -- toán tử MyArr tĩnh công khai --(MyArr obj1) ( obj1.x -= 1; obj1.y -= 1; obj1.z -= 1; trả về obj1; ) ) lớp Chương trình ( static void Main(string args) ( MyArr Point1 = new MyArr(1, 12, -4); MyArr Point2 = new MyArr(0, -3, 18); Console.WriteLine("Toạ độ của đầu tiên point: " + Point1.x + " " + Point1.y + " " + Point1.z); Console.WriteLine("Tọa độ của điểm thứ hai: " + Point2.x + " " + Point2.y + " " + Point2.z + "\n"); MyArr Point3 = Point1 + Point2; Console.WriteLine("\nPoint1 + Point2 = " + Point3.x + " " + Point3.y + " " + Point3.z); Point3 = Point1 - Point2; Console.WriteLine("Point1 - Point2 = " + Point3.x + " " + Point3.y + " " + Point3.z); Point3 = -Point1; Console.WriteLine("-Point1 =" + Point3 .x + " " + Point3.y + " " + Point3.z); Point2++; Console.WriteLine("Point2++ = " + Point2.x + " " + Point2.y + " " + Point2.z); Point2- -; Console.WriteLine("Point2-- = " + Point2.x + " " + Point2.y + " " + Point2.z); Console.ReadLine(); ) ) )

Mỗi ngành khoa học đều có những ký hiệu tiêu chuẩn giúp cho các ý tưởng trở nên dễ hiểu hơn. Ví dụ, trong toán học, đây là phép nhân, phép chia, phép cộng và các ký hiệu tượng trưng khác. Biểu thức (x + y * z) dễ hiểu hơn nhiều so với “nhân y, c, z và cộng với x”. Hãy tưởng tượng, cho đến thế kỷ 16, toán học không có ký hiệu tượng trưng, ​​​​tất cả các biểu thức đều được viết bằng lời nói như thể đó là một văn bản văn học có mô tả. Và tên gọi của các hoạt động quen thuộc với chúng ta thậm chí còn xuất hiện muộn hơn. Tầm quan trọng của ký hiệu tượng trưng ngắn gọn rất khó để đánh giá quá cao. Dựa trên những cân nhắc như vậy, tình trạng quá tải toán tử đã được thêm vào các ngôn ngữ lập trình. Hãy xem một ví dụ.

Ví dụ về nạp chồng toán tử

Gần giống như bất kỳ ngôn ngữ nào, C++ hỗ trợ nhiều toán tử làm việc với các kiểu dữ liệu được tích hợp trong tiêu chuẩn ngôn ngữ. Nhưng hầu hết các chương trình đều sử dụng các loại tùy chỉnh để giải quyết một số vấn đề nhất định. Ví dụ: toán học phức tạp hoặc được triển khai trong một chương trình bằng cách biểu diễn các số hoặc ma trận phức tạp dưới dạng các kiểu C++ tùy chỉnh. Các toán tử tích hợp không biết cách phân phối công việc của họ và thực hiện các thủ tục cần thiết trên các lớp người dùng, bất kể chúng có vẻ rõ ràng đến mức nào. Do đó, chẳng hạn, để cộng ma trận, một hàm riêng biệt thường được tạo. Rõ ràng, việc gọi sum_matrix(A, B) trong mã sẽ ít rõ ràng hơn so với việc gọi A + B.

Hãy xem xét một lớp ví dụ về số phức:

// biểu diễn một số phức dưới dạng một cặp số dấu phẩy động. lớp phức tạp ( double re, im; public: complex (double r, double i) :re(r), im(i) () // hàm tạo phức operator+(complex); // quá tải phép cộng toán tử phức*(complex); // quá tải nhân); void main() ( phức a( 1, 2 ), b( 3, 4 ), c(0, 0); c = a + b; c = a.operator+(b); ////hàm toán tử có thể là được gọi giống như bất kỳ hàm nào, mục này tương đương với a+b c = a*b + complex(1, 3); // Tuân thủ các quy tắc thông thường về mức độ ưu tiên của các phép tính cộng và nhân)

Theo cách tương tự, bạn có thể thực hiện, chẳng hạn như nạp chồng các toán tử đầu vào/đầu ra trong C++ và điều chỉnh chúng để hiển thị các cấu trúc phức tạp như ma trận.

Toán tử có sẵn để quá tải

Danh sách đầy đủ tất cả các toán tử có thể sử dụng cơ chế nạp chồng:

Như bạn có thể thấy từ bảng, việc nạp chồng có thể được chấp nhận đối với hầu hết các toán tử ngôn ngữ. Có thể không cần phải làm quá tải người vận hành. Điều này được thực hiện chỉ để thuận tiện. Do đó, không có hiện tượng nạp chồng toán tử trong Java chẳng hạn. Và bây giờ là về điểm quan trọng tiếp theo.

Các toán tử bị cấm quá tải

  • Độ phân giải phạm vi - "::";
  • Lựa chọn thành viên - ".";
  • Chọn thành viên thông qua con trỏ tới thành viên - “.*”;
  • Toán tử điều kiện bậc ba - "?:";
  • toán tử sizeof;
  • Toán tử Typeid.

Toán hạng bên phải của các toán tử này là tên chứ không phải giá trị. Do đó, việc cho phép chúng bị quá tải có thể dẫn đến việc viết nhiều cấu trúc không rõ ràng và sẽ làm phức tạp đáng kể cuộc sống của các lập trình viên. Mặc dù có nhiều ngôn ngữ lập trình cho phép nạp chồng tất cả toán tử - ví dụ như nạp chồng

Những hạn chế

Hạn chế quá tải của toán tử:

  • Bạn không thể thay đổi toán tử nhị phân thành toán tử một ngôi và ngược lại, cũng như không thể thêm toán hạng thứ ba.
  • Bạn không thể tạo các toán tử mới ngoài những toán tử đã tồn tại. Hạn chế này giúp loại bỏ nhiều sự mơ hồ. Nếu có nhu cầu về toán tử mới, bạn có thể sử dụng một hàm cho những mục đích này để thực hiện hành động được yêu cầu.
  • Hàm toán tử có thể là thành viên của một lớp hoặc có ít nhất một đối số kiểu do người dùng xác định. Các trường hợp ngoại lệ là toán tử mới và xóa. Quy tắc này cấm thay đổi ý nghĩa của biểu thức nếu chúng không chứa các đối tượng thuộc loại do người dùng xác định. Đặc biệt, bạn không thể tạo một hàm toán tử chỉ hoạt động trên con trỏ hoặc làm cho toán tử cộng hoạt động giống như phép nhân. Các ngoại lệ là các toán tử "=", "&" và "," cho các đối tượng lớp.
  • Hàm toán tử có thành viên đầu tiên là một trong các kiểu dữ liệu C++ tích hợp không thể là thành viên của lớp.
  • Tên của bất kỳ hàm toán tử nào đều bắt đầu bằng từ khóa operator, theo sau là ký hiệu tượng trưng của chính toán tử đó.
  • Các toán tử tích hợp được xác định theo cách có mối quan hệ giữa chúng. Ví dụ: các toán tử sau tương đương với nhau: ++x; x + = 1; x = x + 1. Sau khi xác định lại, kết nối giữa chúng sẽ không được bảo toàn. Lập trình viên sẽ phải quan tâm riêng đến việc duy trì sự hợp tác của họ theo cách tương tự với các kiểu mới.
  • Trình biên dịch không thể suy nghĩ. Các biểu thức z + 5 và 5 +z (trong đó z là số phức) sẽ được trình biên dịch xử lý khác nhau. Đầu tiên là "phức + số" và thứ hai là "số + phức". Do đó, mỗi biểu thức cần xác định toán tử cộng riêng.
  • Khi tìm kiếm định nghĩa toán tử, trình biên dịch không ưu tiên các hàm thành viên của lớp hoặc các hàm phụ trợ được xác định bên ngoài lớp. Đối với trình biên dịch chúng bằng nhau.

Giải thích các toán tử nhị phân và đơn nguyên.

Toán tử nhị phân được định nghĩa là hàm thành viên có một biến hoặc là hàm có hai biến. Đối với bất kỳ toán tử nhị phân @ nào trong biểu thức a@b, @ các cấu trúc sau đây là hợp lệ:

a.operator@(b) hoặc operator@(a, b).

Sử dụng ví dụ về lớp số phức, chúng ta hãy xem xét định nghĩa của các phép toán là các thành viên của lớp và các phép toán phụ.

Lớp phức tạp ( double re, im; public: complex& operator+=(complex z); complex& operator*=(complex z); ); //các hàm phụ trợ toán tử phức+(phức z1, phức z2); toán tử phức+(z phức, double a);

Toán tử nào được chọn và liệu nó có được chọn hay không, được xác định bởi các cơ chế bên trong của ngôn ngữ, điều này sẽ được thảo luận dưới đây. Điều này thường xảy ra bằng cách khớp loại.

Nói chung, việc lựa chọn mô tả một chức năng như một thành viên của một lớp hay bên ngoài nó là vấn đề sở thích. Trong ví dụ trên, nguyên tắc lựa chọn như sau: nếu thao tác thay đổi toán hạng bên trái (ví dụ: a + = b), thì hãy viết nó vào trong lớp và sử dụng việc truyền một biến đến địa chỉ để thay đổi trực tiếp; nếu thao tác không thay đổi bất cứ điều gì và chỉ trả về một giá trị mới (ví dụ: a + b) - hãy di chuyển nó ra ngoài phạm vi của định nghĩa lớp.

Định nghĩa về nạp chồng các toán tử một ngôi trong C++ diễn ra theo cách tương tự, với điểm khác biệt là chúng được chia thành hai loại:

  • toán tử tiền tố đặt trước toán hạng là @a, ví dụ: ++i. o được định nghĩa là a.operator@() hoặc operator@(aa);
  • toán tử hậu tố nằm sau toán hạng là b@, ví dụ: i++. o được định nghĩa là b.operator@(int) hoặc operator@(b, int)

Cũng giống như các toán tử nhị phân, khi khai báo toán tử ở cả bên trong và bên ngoài lớp, sự lựa chọn sẽ được thực hiện bởi các cơ chế C++.

Quy tắc lựa chọn nhà điều hành

Hãy áp dụng toán tử nhị phân @ cho các đối tượng x từ lớp X và y từ lớp Y. Các quy tắc để giải x@y sẽ như sau:

  1. nếu X là một lớp, hãy nhìn vào bên trong nó để biết định nghĩa operator@ là thành viên của X hoặc lớp cơ sở của X;
  2. xem bối cảnh trong đó biểu thức x@y được đặt;
  3. nếu X nằm trong không gian tên N, hãy tìm khai báo toán tử trong N;
  4. nếu Y nằm trong không gian tên M, hãy tìm khai báo toán tử trong M.

Nếu tìm thấy nhiều khai báo operator@ trong phần 1-4, việc lựa chọn sẽ được thực hiện theo các quy tắc để giải quyết các hàm bị quá tải.

Việc tìm kiếm các khai báo của toán tử một ngôi cũng diễn ra theo cách tương tự.

Định nghĩa rõ ràng về lớp phức tạp

Bây giờ chúng ta hãy xây dựng lớp số phức chi tiết hơn để chứng minh một số quy tắc đã nêu trước đó.

Lớp phức tạp ( double re, im; public: complex& operator+=(complex z) ( //hoạt động với các biểu thức như z1 += z2 re += z.re; im += z.im; return *this; ) complex& operator+= (double a) ( //hoạt động với các biểu thức như z1 += 5; re += a; return *this; ) complex (): re(0), im(0) () //constructor để khởi tạo mặc định. , tất cả các số phức được khai báo sẽ có giá trị ban đầu (0, 0) phức (double r): re(r), im(0) () // hàm tạo cho phép biểu diễn dạng phức z = 11; tương đương ký hiệu z = complex( 11); complex (double r, double i): re(r), im(i) () //constructor ); toán tử phức+(phức z1, z2 phức) ( //làm việc với các biểu thức như z1 + z2 complex res = z1; return res += z2; //sử dụng toán tử được định nghĩa là hàm thành viên ) toán tử phức+(z phức, double a) ( //xử lý các biểu thức có dạng z+2 phức res = z; return res += a; ) toán tử phức+(double a, z phức) ( // xử lý các biểu thức có dạng 7+z phức res = z; return res += a; ) //…

Như bạn có thể thấy từ đoạn mã, việc nạp chồng toán tử có một cơ chế rất phức tạp và có thể phát triển rất nhiều. Tuy nhiên, cách tiếp cận chi tiết này cho phép quá tải ngay cả đối với các cấu trúc dữ liệu rất phức tạp. Ví dụ: nạp chồng toán tử C++ trong một lớp mẫu.

Việc tạo các chức năng cho mọi người như thế này có thể tẻ nhạt và dễ xảy ra lỗi. Ví dụ: nếu bạn thêm loại thứ ba vào các chức năng đang được xem xét, bạn sẽ cần xem xét các hoạt động dựa trên sự kết hợp của ba loại. Bạn sẽ phải viết 3 hàm với một đối số, 9 với hai và 27 với ba. Do đó, trong một số trường hợp, việc thực hiện tất cả các chức năng này và giảm đáng kể số lượng của chúng có thể đạt được thông qua việc sử dụng chuyển đổi kiểu.

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

Toán tử lập chỉ mục "" phải luôn được xác định là thành viên của lớp vì nó làm giảm hành vi của một đối tượng thành một mảng. Trong trường hợp này, đối số lập chỉ mục có thể thuộc bất kỳ loại nào, cho phép bạn tạo, chẳng hạn như mảng kết hợp.

Toán tử gọi hàm "()" có thể được coi là một phép toán nhị phân. Ví dụ: trong cấu trúc “biểu thức (danh sách biểu thức)”, toán hạng bên trái của phép toán nhị phân () sẽ là “biểu thức” và toán hạng bên phải sẽ là danh sách biểu thức. Hàm operator()() phải là thành viên của lớp.

Toán tử chuỗi "," (dấu phẩy) được gọi trên các đối tượng nếu chúng có dấu phẩy bên cạnh. Tuy nhiên, toán tử không tham gia vào việc liệt kê các đối số của hàm.

Toán tử quy định "->" cũng phải được xác định là thành viên của hàm. Theo nghĩa của nó, nó có thể được định nghĩa là một toán tử hậu tố đơn nhất. Đồng thời, nó nhất thiết phải trả về một liên kết hoặc một con trỏ cho phép truy cập vào đối tượng.

Cũng chỉ được định nghĩa là thành viên của một lớp do liên kết của nó với toán hạng bên trái.

Toán tử gán "=", địa chỉ "&" và chuỗi "," phải được xác định trong khối công khai.

Điểm mấu chốt

Quá tải toán tử giúp triển khai một trong những khía cạnh chính của OOP về tính đa hình. Nhưng điều quan trọng là phải hiểu rằng quá tải không gì khác hơn là một cách gọi hàm khác. Mục tiêu của việc nạp chồng toán tử thường là để cải thiện khả năng hiểu mã hơn là cải thiện một số vấn đề nhất định.

Và điều đó không phải tất cả. Bạn cũng nên lưu ý rằng quá tải toán tử là một cơ chế phức tạp với nhiều cạm bẫy. Vì vậy, rất dễ mắc sai lầm. Đây là lý do chính tại sao hầu hết các lập trình viên khuyên không nên sử dụng nạp chồng toán tử và chỉ sử dụng nó như là phương sách cuối cùng và hoàn toàn tin tưởng vào hành động của mình.

  1. Chỉ có các toán tử nạp chồng mới mô phỏng được các ký hiệu quen thuộc. Để làm cho mã dễ đọc hơn. Nếu mã trở nên phức tạp hơn về cấu trúc hoặc khả năng đọc, bạn nên từ bỏ việc nạp chồng toán tử và sử dụng các hàm.
  2. Đối với các toán hạng lớn, để tiết kiệm dung lượng, hãy sử dụng các đối số kiểu tham chiếu const để truyền chúng.
  3. Tối ưu hóa giá trị trả về
  4. Để nguyên thao tác sao chép nếu nó phù hợp với lớp của bạn.
  5. Nếu tùy chọn sao chép mặc định không phù hợp, hãy thay đổi hoặc tắt tùy chọn sao chép một cách rõ ràng.
  6. Các hàm thành viên nên được ưu tiên hơn các hàm không phải thành viên trong trường hợp hàm đó yêu cầu quyền truy cập vào biểu diễn lớp.
  7. Chỉ định không gian tên và chỉ ra mối liên kết của các hàm với lớp của chúng.
  8. Sử dụng các hàm không phải thành viên cho các toán tử đối xứng.
  9. Sử dụng toán tử () cho các chỉ mục trong mảng nhiều chiều.
  10. Sử dụng chuyển đổi ngầm một cách thận trọng.

Trong Chương 15, chúng ta sẽ xem xét hai loại hàm đặc biệt: toán tử nạp chồng và chuyển đổi do người dùng xác định. Chúng cho phép sử dụng các đối tượng lớp trong các biểu thức theo cách trực quan giống như các đối tượng thuộc các kiểu có sẵn. Trong chương này, trước tiên chúng tôi phác thảo các khái niệm chung để thiết kế các toán tử quá tải. Tiếp theo, chúng tôi sẽ giới thiệu khái niệm về bạn bè của lớp có quyền truy cập đặc biệt và thảo luận lý do tại sao chúng được sử dụng, đặc biệt chú ý đến cách triển khai một số toán tử quá tải: gán, lập chỉ mục, gọi, mũi tên thành viên lớp, tăng và giảm và các toán tử chuyên biệt cho các toán tử lớp mới và xóa. Một loại hàm đặc biệt khác được thảo luận trong chương này là các hàm chuyển đổi thành viên (bộ chuyển đổi), tạo nên tập hợp các chuyển đổi tiêu chuẩn cho một loại lớp. Chúng được trình biên dịch sử dụng ngầm khi các đối tượng lớp được sử dụng làm đối số hàm thực tế hoặc toán hạng của các toán tử tích hợp hoặc quá tải. Chương này kết thúc bằng phần trình bày chi tiết về các quy tắc giải quyết tình trạng nạp chồng hàm, có tính đến việc truyền các đối tượng làm đối số, các hàm thành viên lớp và các toán tử nạp chồng.

15.1. Quá tải toán tử

Chúng tôi đã chỉ ra trong các chương trước rằng việc nạp chồng toán tử cho phép người lập trình giới thiệu các phiên bản riêng của các toán tử được xác định trước (xem Chương 4) cho các toán hạng kiểu lớp. Ví dụ, lớp String trong Phần 3.15 có nhiều toán tử bị nạp chồng. Dưới đây là định nghĩa của nó:

#bao gồm Chuỗi lớp; istream& toán tử>>(istream &, const String &); ostream& toán tử<<(ostream &, const String &); class String { public: // набор перегруженных конструкторов // для автоматической инициализации String(const char* = 0); String(const String &); // деструктор: автоматическое уничтожение ~String(); // набор перегруженных операторов присваивания String& operator=(const String &); String& operator=(const char *); // перегруженный оператор взятия индекса char& operator(int); // набор перегруженных операторов равенства // str1 == str2; bool operator==(const char *); bool operator==(const String &); // функции доступа к членам int size() { return _size; }; char * c_str() { return _string; } private: int _size; char *_string; };

Lớp String có ba bộ toán tử được nạp chồng. Đầu tiên là một tập hợp các toán tử gán:

Đầu tiên là toán tử gán bản sao. (Những điều này sẽ được thảo luận chi tiết trong Phần 14.7.) Câu lệnh sau hỗ trợ việc gán một chuỗi ký tự C cho một đối tượng có kiểu Chuỗi:

Tên chuỗi; tên = "Sherlock"; // sử dụng toán tử operator=(char *)

(Chúng ta sẽ xem xét các toán tử gán khác với các toán tử sao chép trong Phần 15.3.)

Trong tập thứ hai chỉ có một toán tử - lấy chỉ mục:

// nạp chồng toán tử chỉ mục char& operator(int);

Nó cho phép chương trình lập chỉ mục các đối tượng chuỗi lớp giống như mảng các đối tượng kiểu dựng sẵn:

Nếu (name != "S") cout<<"увы, что-то не так\n";

(Toán tử này được mô tả chi tiết trong Phần 15.4.)

Bộ thứ ba định nghĩa các toán tử đẳng thức được nạp chồng cho các đối tượng của lớp String. Một chương trình có thể kiểm tra sự bằng nhau của hai đối tượng như vậy hoặc một đối tượng và một chuỗi C:

// tập hợp các toán tử đẳng thức được nạp chồng // str1 == str2; toán tử bool==(const char *); toán tử bool==(const String &);

Các toán tử nạp chồng cho phép bạn sử dụng các đối tượng của một kiểu lớp với các toán tử được định nghĩa trong Chương 4 và thao tác chúng một cách trực quan như các đối tượng thuộc các kiểu có sẵn. Ví dụ, nếu chúng ta muốn định nghĩa hoạt động nối hai đối tượng String, chúng ta có thể triển khai nó như một hàm thành viên concat(). Nhưng tại sao lại là concat() mà không phải là add()? Tên chúng tôi chọn hợp lý và dễ nhớ nhưng người dùng vẫn có thể quên tên chúng tôi đặt cho hàm. Việc nhớ tên thường dễ dàng hơn nếu bạn xác định một toán tử tái định nghĩa. Ví dụ, thay vì concat() chúng ta sẽ gọi toán tử mới+=(). Toán tử này được sử dụng như sau:

#include "String.h" int main() ( String name1 "Sherlock"; String name2 "Holmes"; name1 += " "; name1 += name2; if (! (name1 == "Sherlock Holmes")) cout< < "конкатенация не сработала\n"; }

Một toán tử tái định nghĩa được khai báo trong phần thân của một lớp giống như một hàm thành viên thông thường, ngoại trừ tên của nó bao gồm toán tử từ khóa, theo sau là một trong nhiều toán tử được xác định trước trong C++ (xem Bảng 15.1). Đây là cách bạn có thể khai báo operator+=() trong lớp String:

Lớp String ( public: // tập hợp các toán tử được nạp chồng += String& operator+=(const String &); String& operator+=(const char *); // ... private: // ... );

và định nghĩa nó như thế này:

#bao gồm inline String& String::operator+=(const String &rhs) ( // Nếu chuỗi được tham chiếu bởi rhs không trống if (rhs._string) ( String tmp(*this); // phân bổ một vùng bộ nhớ // đủ để lưu trữ các chuỗi được nối _size += rhs._size; delete _string; _string = new char[ _size + 1 ]; // đầu tiên sao chép chuỗi gốc vào vùng đã chọn // sau đó nối chuỗi được tham chiếu bởi rhs strcpy vào cuối (_string, tmp._string) ; strcpy(_string + tmp._size, rhs._string); ) return *this; ) inline String& String::operator+=(const char *s) ( // Nếu con trỏ s không null if (s) ( String tmp(*this ); // cấp phát vùng nhớ đủ // để lưu trữ các chuỗi nối _size += strlen(s); delete _string; _string = new char[ _size + 1 ]; // bản sao đầu tiên chuỗi gốc vào vùng được phân bổ // sau đó nối vào cuối chuỗi C được tham chiếu bởi s strcpy(_string, tmp._string); strcpy(_string + tmp._size, s); ) return *this; )

15.1.1. Thành viên lớp và không phải thành viên

Chúng ta hãy xem xét kỹ hơn các toán tử đẳng thức trong lớp String của chúng ta. Toán tử đầu tiên cho phép bạn thiết lập đẳng thức giữa hai đối tượng và toán tử thứ hai cho phép bạn thiết lập đẳng thức giữa một đối tượng và chuỗi C:

#include "String.h" int main() ( Chuỗi hoa; // viết gì đó vào biến hoa if (flower == "lily") // đúng // ... else if ("tulip" == hoa ) // lỗi // ... )

Lần đầu tiên bạn sử dụng toán tử đẳng thức trong main(), toán tử nạp chồng==(const char *) của lớp String sẽ được gọi. Tuy nhiên, ở câu lệnh if thứ hai, trình biên dịch sẽ đưa ra thông báo lỗi. Có chuyện gì vậy?

Toán tử tái định nghĩa là thành viên của một lớp chỉ được sử dụng khi toán hạng bên trái là đối tượng của lớp đó. Vì trong trường hợp thứ hai, toán hạng bên trái không thuộc lớp String, trình biên dịch cố gắng tìm một toán tử có sẵn mà toán hạng bên trái có thể là chuỗi C và toán hạng bên phải có thể là một đối tượng của lớp String. Tất nhiên là nó không tồn tại nên trình biên dịch báo lỗi.

Nhưng bạn có thể tạo một đối tượng của lớp String từ một chuỗi C bằng cách sử dụng hàm tạo của lớp. Tại sao trình biên dịch không thực hiện chuyển đổi ngầm như sau:

Nếu (String("tulip") == hoa) //đúng: toán tử thành viên được gọi

Nguyên nhân là do nó không hiệu quả. Các toán tử quá tải không yêu cầu cả hai toán hạng phải cùng loại. Ví dụ: lớp Text định nghĩa các toán tử đẳng thức sau:

Lớp Text ( public: Text(const char * = 0); Text(const Text &); // tập hợp các toán tử đẳng thức được nạp chồng bool operator==(const char *) const; bool operator==(const String &) const; toán tử bool==(const Text &) const; // ... );

và biểu thức trong hàm main() có thể được viết lại như thế này:

If (Text("tulip") == hoa) // gọi Text::operator==()

Do đó, để tìm một toán tử đẳng thức phù hợp để so sánh, trình biên dịch sẽ phải xem qua tất cả các định nghĩa lớp để tìm kiếm một hàm tạo có thể truyền toán hạng bên trái sang một số kiểu của lớp. Sau đó, đối với mỗi loại này, bạn cần kiểm tra tất cả các toán tử đẳng thức được nạp chồng liên quan của nó để xem liệu có bất kỳ toán tử nào trong số chúng có thể thực hiện so sánh hay không. Và sau đó trình biên dịch phải quyết định sự kết hợp nào giữa hàm tạo và toán tử đẳng thức (nếu có) phù hợp nhất với toán hạng ở bên phải! Nếu bạn yêu cầu trình biên dịch thực hiện tất cả những hành động này, thời gian dịch của chương trình C++ sẽ tăng lên đáng kể. Thay vào đó, trình biên dịch chỉ xem xét các toán tử nạp chồng được xác định là thành viên của lớp toán hạng bên trái (và các lớp cơ sở của nó, như chúng tôi sẽ trình bày trong Chương 19).

Tuy nhiên, được phép định nghĩa các toán tử tái định nghĩa không phải là thành viên của lớp. Khi phân tích dòng trong main() gây ra lỗi biên dịch, các câu lệnh như vậy đã được tính đến. Do đó, sự so sánh trong đó chuỗi C xuất hiện ở phía bên trái có thể được thực hiện chính xác bằng cách thay thế các toán tử đẳng thức là thành viên của lớp String bằng các toán tử đẳng thức được khai báo trong phạm vi của không gian tên:

Toán tử Bool==(const String &, const String &); toán tử bool==(const String &, const char *);

Lưu ý rằng các toán tử nạp chồng toàn cục này có nhiều hơn một tham số so với các toán tử thành viên. Nếu toán tử là thành viên của một lớp thì con trỏ this được truyền ngầm làm tham số đầu tiên. Nghĩa là, đối với các toán tử thành viên, biểu thức

Hoa == "hoa huệ"

được trình biên dịch viết lại thành:

Flower.operator==("hoa huệ")

và toán hạng bên trái của bông hoa trong định nghĩa của toán tử thành viên quá tải có thể được tham chiếu bằng cách sử dụng this. (Chỉ báo này đã được giới thiệu trong Phần 13.4.) Trong trường hợp quá tải toán tử toàn cục, tham số biểu thị toán hạng bên trái phải được chỉ định rõ ràng.

Sau đó biểu thức

Hoa == "hoa huệ"

cuộc gọi của nhà điều hành

Toán tử Bool==(const String &, const char *);

Không rõ toán tử nào được gọi cho lần sử dụng thứ hai của toán tử đẳng thức:

"hoa tulip" == hoa

Chúng tôi chưa xác định toán tử quá tải như vậy:

Toán tử Bool==(const char *, const String &);

Nhưng đây là tùy chọn. Khi toán tử quá tải là một hàm trong một không gian tên, thì các chuyển đổi có thể có sẽ được xem xét cho cả tham số thứ nhất và thứ hai của nó (đối với toán hạng bên trái và bên phải), tức là. trình biên dịch diễn giải cách sử dụng thứ hai của toán tử đẳng thức là

Toán tử==(Chuỗi ("hoa tulip"), bông hoa);

và gọi toán tử nạp chồng sau để thực hiện phép so sánh: bool operator==(const String &, const String &);

Nhưng tại sao chúng tôi lại cung cấp toán tử nạp chồng thứ hai: bool operator==(const String &, const char *);

Việc chuyển đổi kiểu từ lớp C-string sang lớp String cũng có thể được áp dụng cho toán hạng bên phải. Hàm main() sẽ biên dịch không có lỗi nếu bạn chỉ định nghĩa một toán tử nạp chồng trong vùng tên chứa hai toán hạng Chuỗi:

Toán tử Bool==(const String &, const String &);

Tôi chỉ nên cung cấp toán tử này hoặc hai toán tử nữa:

Toán tử Bool==(const char *, const String &); toán tử bool==(const String &, const char *);

phụ thuộc vào chi phí chuyển đổi từ chuỗi C sang Chuỗi trong thời gian chạy lớn đến mức nào, tức là phụ thuộc vào “chi phí” của các lệnh gọi hàm tạo bổ sung trong các chương trình sử dụng lớp Chuỗi của chúng ta. Nếu toán tử đẳng thức được sử dụng thường xuyên để so sánh các chuỗi và đối tượng trong C thì tốt hơn là cung cấp cả ba tùy chọn. (Chúng ta sẽ quay lại vấn đề hiệu quả ở phần nói về bạn bè.

Chúng ta sẽ nói nhiều hơn về việc chuyển sang một kiểu lớp bằng cách sử dụng các hàm tạo trong Phần 15.9; Phần 15.10 thảo luận về việc cho phép nạp chồng hàm bằng cách sử dụng các phép biến đổi được mô tả và Phần 15.12 thảo luận về việc cho phép nạp chồng toán tử.)

Vì vậy, cơ sở để quyết định xem nên đặt toán tử là thành viên của một lớp hay thành viên của không gian tên là gì? Trong một số trường hợp, lập trình viên không có lựa chọn nào khác:

  • Nếu toán tử tái định nghĩa là thành viên của một lớp thì nó chỉ được gọi nếu toán hạng bên trái là thành viên của lớp đó. Nếu toán hạng bên trái thuộc loại khác, toán tử phải là thành viên của không gian tên;
  • Ngôn ngữ yêu cầu các toán tử gán ("="), chỉ mục (""), gọi ("()") và mũi tên truy cập thành viên ("->") phải được xác định là thành viên của lớp. Nếu không, thông báo lỗi biên dịch sẽ xuất hiện:
// lỗi: phải là thành viên của lớp char& operator(String &, int ix);

(Toán tử gán được thảo luận chi tiết hơn trong Phần 15.3, toán tử chỉ mục trong Phần 15.4, toán tử cuộc gọi trong Phần 15.5 và toán tử truy cập thành viên mũi tên trong Phần 15.6.)

Trong các trường hợp khác, quyết định được đưa ra bởi người thiết kế lớp. Các toán tử đối xứng, chẳng hạn như toán tử đẳng thức, được xác định tốt nhất trong một không gian tên nếu bất kỳ toán hạng nào có thể là thành viên của lớp (như trong Chuỗi).

Trước khi chúng ta kết thúc tiểu mục này, hãy định nghĩa các toán tử đẳng thức cho lớp String trong không gian tên:

Toán tử Bool==(const String &str1, const String &str2) ( if (str1.size() != str2.size()) return false; return strcmp(str1.c_str(), str2.c_str()) ? false: true ; ) toán tử bool nội tuyến==(const String &str, const char *s) ( return strcmp(str.c_str(), s) ? false: true ; )

15.1.2. Tên toán tử bị quá tải

Chỉ các toán tử ngôn ngữ C++ được xác định trước mới có thể bị quá tải (xem Bảng 15.1).

Bảng 15.1. Toán tử có thể quá tải

+ - * / % ^ & | ~ ! , = < > <= >= ++ -- << >> == != && || += -= /= %= ^= &= |= *= <= >>= () -> ->* mới xóa mới xóa

Người thiết kế một lớp không có quyền khai báo một toán tử có tên khác bị quá tải. Vì vậy, nếu bạn cố khai báo toán tử ** để tính lũy thừa, trình biên dịch sẽ đưa ra thông báo lỗi.

Bốn toán tử C++ sau đây không thể được nạp chồng:

// các toán tử không thể quá tải:: .* . ?:

Việc gán toán tử được xác định trước không thể thay đổi đối với các loại tích hợp. Ví dụ: bạn không được phép ghi đè toán tử cộng số nguyên tích hợp để kiểm tra tràn.

// lỗi: bạn không thể ghi đè toán tử cộng tích hợp int int operator+(int, int);

Bạn cũng không thể xác định các toán tử bổ sung cho các kiểu dữ liệu tích hợp, chẳng hạn như thêm toán tử+ vào tập hợp các toán tử tích hợp để thêm hai mảng.

Toán tử quá tải được xác định riêng cho toán hạng của một lớp hoặc kiểu liệt kê và chỉ có thể được khai báo là thành viên của một lớp hoặc không gian tên, lấy ít nhất một tham số lớp hoặc kiểu liệt kê (được truyền theo giá trị hoặc theo tham chiếu).

Các ưu tiên của người vận hành được xác định trước (xem Phần 4.13) không thể thay đổi được. Bất kể kiểu lớp và cách triển khai toán tử trong câu lệnh

X == y + z;

operator+ luôn được thực thi trước, sau đó là operator==; tuy nhiên, bạn có thể thay đổi thứ tự bằng dấu ngoặc đơn.

Tính chất được xác định trước của các toán tử cũng phải được bảo toàn. Ví dụ: toán tử NOT logic đơn nhất không thể được định nghĩa là toán tử nhị phân cho hai đối tượng Chuỗi. Việc triển khai sau đây không chính xác và sẽ dẫn đến lỗi biên dịch:

// không đúng: ! là toán tử một ngôi bool operator!(const String &s1, const String &s2) ( return (strcmp(s1.c_str(), s2.c_str()) != 0); )

Đối với các loại tích hợp, bốn toán tử được xác định trước ("+", "-", "*" và "&") được sử dụng làm toán tử đơn phân hoặc nhị phân. Ở bất kỳ khả năng nào trong số này, chúng có thể bị quá tải.

Tất cả các toán tử được nạp chồng ngoại trừ toán tử() đều không cho phép các đối số mặc định.

15.1.3. Thiết kế các toán tử quá tải

Các toán tử gán, địa chỉ và dấu phẩy có ý nghĩa được xác định trước nếu toán hạng là đối tượng của một loại lớp. Nhưng chúng cũng có thể bị quá tải. Ngữ nghĩa của tất cả các toán tử khác khi áp dụng cho các toán hạng đó phải được nhà phát triển chỉ định rõ ràng. Việc lựa chọn các toán tử được cung cấp tùy thuộc vào mục đích sử dụng dự kiến ​​của lớp.

Bạn nên bắt đầu bằng cách xác định giao diện chung của nó. Tập hợp các hàm thành viên public được hình thành dựa trên các thao tác mà lớp phải cung cấp cho người dùng. Sau đó, một quyết định được đưa ra là chức năng nào sẽ được thực hiện dưới dạng toán tử quá tải.

Sau khi xác định giao diện chung của một lớp, hãy kiểm tra xem có sự tương ứng logic giữa các thao tác và câu lệnh hay không:

  • isEmpty() trở thành toán tử LOGICAL NOT, toán tử!().
  • isEqual() trở thành toán tử đẳng thức, toán tử==().
  • copy() trở thành toán tử gán, operator=().

Mỗi toán tử có một số ngữ nghĩa tự nhiên. Do đó, nhị phân + luôn được liên kết với phép cộng và việc ánh xạ của nó tới một phép toán tương tự với một lớp có thể là một ký hiệu thuận tiện và ngắn gọn. Ví dụ, đối với một loại ma trận, phép cộng hai ma trận là một phần mở rộng hoàn toàn phù hợp của phép cộng nhị phân.

Một ví dụ về việc nạp chồng toán tử được sử dụng không chính xác là việc định nghĩa toán tử+() như một phép toán trừ, điều này vô nghĩa: ngữ nghĩa không trực quan rất nguy hiểm.

Một toán tử như vậy hỗ trợ tốt cho nhiều cách hiểu khác nhau. Một lời giải thích hoàn toàn rõ ràng và hợp lý về những gì operator+() thực hiện khó có thể làm hài lòng những người dùng lớp String, những người cho rằng nó phục vụ cho việc nối chuỗi. Nếu ngữ nghĩa của toán tử quá tải không rõ ràng thì tốt hơn là không cung cấp nó.

Sự tương đương về ngữ nghĩa của toán tử ghép và trình tự tương ứng của các toán tử đơn giản cho các kiểu dựng sẵn (ví dụ: sự tương đương của toán tử + theo sau là = và toán tử ghép +=) cũng phải được duy trì rõ ràng cho lớp . Giả sử rằng String có cả operator+() và operator=() được xác định để hỗ trợ các hoạt động ghép nối và sao chép theo thành viên:

Chuỗi s1("C"); Chuỗi s2("++"); s1 = s1 + s2; // s1 == "C++"

Nhưng điều này là không đủ để hỗ trợ toán tử gán ghép

S1 += s2;

Nó phải được xác định rõ ràng để hỗ trợ ngữ nghĩa dự kiến.

Bài tập 15.1

Tại sao phép so sánh sau đây không gọi toán tử bị quá tải==(const String&, const String&):

"sỏi" == "đá"

Bài tập 15.2

Viết các toán tử bất đẳng thức quá tải có thể được sử dụng trong các phép so sánh như vậy:

Chuỗi != Chuỗi Chuỗi != Chuỗi C Chuỗi C != Chuỗi

Giải thích lý do tại sao bạn quyết định thực hiện một hoặc nhiều câu lệnh.

Bài tập 15.3

Xác định các hàm thành viên của lớp Screen được triển khai trong Chương 13 (Phần 13.3, 13.4 và 13.6) có thể bị quá tải.

Bài tập 15.4

Giải thích tại sao các toán tử đầu vào và đầu ra bị quá tải định nghĩa cho lớp String trong Phần 3.15 lại được khai báo là hàm toàn cục chứ không phải là hàm thành viên.

Bài tập 15.5

Triển khai các toán tử đầu vào và đầu ra được nạp chồng cho lớp Screen từ Chương 13.

15.2. Bạn

Chúng ta hãy xem lại các toán tử đẳng thức được nạp chồng cho lớp String, được định nghĩa trong phạm vi không gian tên. Toán tử đẳng thức cho hai đối tượng String trông như thế này:

Toán tử Bool==(const String &str1, const String &str2) ( if (str1.size() != str2.size()) return false; return strcmp(str1.c_str(), str2.c_str()) ? false: ĐÚNG VẬY; )

So sánh định nghĩa này với định nghĩa của cùng một toán tử với hàm thành viên:

Bool String::operator==(const String &rhs) const ( if (_size != rhs._size) return false; return strcmp(_string, rhs._string) ? false: true; )

Chúng tôi đã phải sửa đổi cách truy cập các thành viên riêng tư của lớp String. Vì toán tử đẳng thức mới là hàm toàn cục chứ không phải hàm thành viên nên nó không có quyền truy cập vào các thành viên riêng của lớp String. Các hàm thành viên size() và c_str() được sử dụng để lấy kích thước của đối tượng String và chuỗi ký tự C cơ bản của nó.

Một cách triển khai khác là khai báo các toán tử đẳng thức toàn cục là bạn bè của lớp String. Nếu một hàm hoặc toán tử được khai báo theo cách này, nó sẽ được cấp quyền truy cập cho các thành viên không công khai.

Khai báo bạn bè (bắt đầu bằng từ khóa bạn bè) chỉ xảy ra trong định nghĩa lớp. Vì bạn bè không phải là thành viên của lớp khai báo mối quan hệ bạn bè nên không có gì khác biệt cho dù chúng được khai báo ở chế độ công khai, riêng tư hay được bảo vệ. Trong ví dụ bên dưới, chúng tôi quyết định đặt tất cả các khai báo như vậy ngay sau tiêu đề lớp:

Chuỗi lớp (friend bool operator==(const String &, const String &);friend bool operator==(const char *, const String &);friend bool operator==(const String &, const char *); public: // ... phần còn lại của lớp String);

Ba dòng này khai báo ba toán tử so sánh quá tải thuộc phạm vi toàn cục là bạn của lớp String và do đó định nghĩa của chúng có thể truy cập trực tiếp vào các thành viên riêng tư của lớp đó:

// các toán tử thân thiện truy cập trực tiếp vào các thành viên riêng // của lớp String bool operator==(const String &str1, const String &str2) ( if (str1._size != str2._size) return false; return strcmp(str1._string, str2 . _string) ? false: true; ) toán tử bool nội tuyến==(const String &str, const char *s) ( return strcmp(str._string, s) ? false: true; ) // v.v.

Có thể lập luận rằng việc truy cập trực tiếp vào các thành viên _size và _string là không cần thiết trong trường hợp này, vì các hàm dựng sẵn c_str() và size() vẫn hiệu quả và vẫn duy trì việc đóng gói, nghĩa là không cần phải khai báo các toán tử đẳng thức cho lớp String bạn bè của nó.

Làm thế nào để bạn biết nên biến một toán tử không phải thành viên thành bạn của lớp hay sử dụng các hàm truy cập? Nói chung, nhà phát triển nên giảm đến mức tối thiểu số lượng hàm được khai báo và toán tử có quyền truy cập vào biểu diễn bên trong của lớp. Nếu có các hàm truy cập mang lại hiệu quả như nhau thì chúng phải được ưu tiên, do đó cách ly các toán tử trong không gian tên khỏi những thay đổi đối với cách biểu diễn lớp, như được thực hiện đối với các hàm khác. Nếu người thiết kế lớp không cung cấp các chức năng truy cập cho một số thành viên và toán tử được khai báo trong không gian tên phải truy cập vào các thành viên này thì việc sử dụng cơ chế bạn bè là điều tất yếu.

Cách sử dụng phổ biến nhất của cơ chế này là cho phép các toán tử quá tải không phải là thành viên của một lớp truy cập vào các thành viên riêng của nó. Nếu không vì nhu cầu đảm bảo tính đối xứng của toán hạng bên trái và bên phải thì toán tử quá tải sẽ là hàm thành viên có toàn quyền truy cập.

Mặc dù các khai báo về bạn bè thường được sử dụng để chỉ các toán tử, nhưng đôi khi một hàm trong một không gian tên, hàm thành viên của một lớp khác hoặc thậm chí toàn bộ một lớp phải được khai báo theo cách này. Nếu một lớp được khai báo là bạn của lớp thứ hai thì tất cả các hàm thành viên của lớp thứ nhất đều có quyền truy cập vào các thành viên không công khai của lớp kia. Hãy xem xét điều này bằng cách sử dụng các hàm không phải toán tử làm ví dụ.

Một lớp phải khai báo với tư cách là bạn bè từng hàm trong số nhiều hàm bị quá tải mà nó muốn cấp quyền truy cập không hạn chế:

Extern ostream& storeOn(ostream &, Screen &); extern BitMap& storeOn(BitMap &, Screen &); // ... lớp Screen (friend ostream& storeOn(ostream &, Screen &);friend BitMap& storeOn(BitMap &, Screen &); // ... );

Nếu một hàm thao tác các đối tượng của hai các lớp khác nhau và nó cần quyền truy cập vào các thành viên không công khai của họ, khi đó một chức năng như vậy có thể được khai báo là bạn của cả hai lớp hoặc trở thành thành viên của một lớp và là bạn của lớp thứ hai.

Khai báo một hàm với tư cách là bạn của hai lớp sẽ như thế này:

Cửa sổ lớp học; // đây chỉ là một lớp khai báo Screen (friend bool is_equal(Screen &, Window &); // ... ); lớp Window (friend bool is_equal(Screen &, Window &); // ... );

Nếu chúng ta quyết định biến một hàm thành thành viên của một lớp và là bạn của lớp thứ hai, thì các khai báo sẽ được xây dựng như sau:

Cửa sổ lớp học; class Screen ( // copy() - thành viên của lớp Screen Screen& copy(Window &); // ... ); class Window ( // Screen::copy() - bạn của lớp Windowfriend Screen& Screen::copy(Window &); // ... ); Màn hình& Màn hình::copy(Window &) ( /* ... */ )

Một hàm thành viên của một lớp không thể được khai báo là bạn của một lớp khác trừ khi trình biên dịch nhìn thấy định nghĩa lớp của chính nó. Không phải lúc nào cũng khả thi. Giả sử Screen khai báo một số hàm thành viên của Window là bạn của nó và Window khai báo một số hàm thành viên của Screen theo cách tương tự. Trong trường hợp này, toàn bộ lớp Window được khai báo là bạn của Screen:

Cửa sổ lớp học; Màn hình lớp ( lớp bạn Window; // ... );

Giờ đây, các thành viên riêng tư của lớp Màn hình có thể được truy cập từ bất kỳ chức năng thành viên Window nào.

Bài tập 15.6

Triển khai các toán tử đầu vào và đầu ra được xác định cho lớp Screen trong Bài tập 15.5 dưới dạng bạn bè và sửa đổi định nghĩa của chúng để chúng truy cập trực tiếp vào các thành viên riêng tư. Việc triển khai nào tốt hơn? Giải thích vì sao.

15.3. Toán tử =

Việc gán một đối tượng cho một đối tượng khác cùng lớp được thực hiện bằng toán tử gán sao chép. (Trường hợp đặc biệt này đã được thảo luận ở Phần 14.7.)

Các toán tử gán khác có thể được định nghĩa cho một lớp. Nếu các đối tượng của một lớp cần được gán các giá trị thuộc loại khác với lớp này thì được phép định nghĩa các toán tử chấp nhận các tham số tương tự. Ví dụ: để hỗ trợ gán chuỗi C cho đối tượng String:

Xe dây("Volks"); car = "Người nghiên cứu";

chúng tôi cung cấp một toán tử chấp nhận tham số loại const char*. Hoạt động này đã được khai báo trong lớp của chúng tôi:

Lớp Chuỗi ( public: // toán tử gán cho char* String& operator=(const char *); // ... private: int _size; char *string; );

Toán tử này được thực hiện như sau. Nếu một đối tượng String được gán một con trỏ rỗng, nó sẽ trở thành "null". Ngược lại, nó được gán một bản sao của chuỗi C:

String& String::operator=(const char *sobj) ( // sobj - con trỏ null if (! sobj) ( _size = 0; delete _string; _string = 0; ) else ( _size = strlen(sobj); delete _string; _string = char mới[ _size + 1 ]; strcpy(_string, sobj); ) return *this; )

Chuỗi đề cập đến một bản sao của chuỗi C được trỏ bởi sobj. Tại sao lại là một bản sao? Bởi vì bạn không thể gán trực tiếp sobj cho thành viên _string:

Chuỗi = nức nở; // lỗi: gõ không khớp

sobj là một con trỏ tới const và do đó không thể được gán cho một con trỏ tới "không phải const" (xem phần 3.5). Hãy thay đổi định nghĩa của toán tử gán:

Chuỗi& Chuỗi::operator=(const *sobj) ( // ... )

Bây giờ _string đề cập trực tiếp đến chuỗi C được gửi tới sobj. Tuy nhiên, điều này đặt ra những vấn đề khác. Hãy nhớ lại rằng chuỗi C có kiểu const char*. Việc xác định một tham số là một con trỏ tới một giá trị không phải hằng khiến cho việc gán không thể thực hiện được:

Xe = "Thợ làm bánh"; // không được phép với operator=(char *) !

Vì vậy, không có sự lựa chọn. Để gán một chuỗi C cho một đối tượng thuộc loại Chuỗi, tham số phải thuộc loại const char*.

Việc lưu trữ một tham chiếu trực tiếp đến chuỗi C có địa chỉ sobj trong _string sẽ tạo ra các biến chứng khác. Chúng tôi không biết chính xác sobj đang chỉ vào cái gì. Đây có thể là một mảng các ký tự được sửa đổi theo cách mà đối tượng String không xác định được. Ví dụ:

Char ia = ("d", "a", "n", "c", "e", "r" ); Bẫy chuỗi = ia; // bẫy._string tham chiếu đến ia ia = "g"; // nhưng chúng ta không cần điều này: // cả ia và bẫy đều đã được sửa đổi._string

Nếu bẫy._string tham chiếu trực tiếp ia, thì đối tượng bẫy sẽ thể hiện hành vi đặc biệt: giá trị của nó có thể thay đổi mà không cần gọi các hàm thành viên của lớp String. Do đó, chúng tôi tin rằng việc phân bổ một vùng bộ nhớ để lưu trữ bản sao của giá trị chuỗi C sẽ ít nguy hiểm hơn.

Lưu ý rằng toán tử gán sử dụng xóa. Thành viên _string chứa tham chiếu đến mảng ký tự nằm trong vùng heap. Để tránh rò rỉ, bộ nhớ được phân bổ cho hàng cũ sẽ được giải phóng bằng cách xóa trước khi phân bổ bộ nhớ cho hàng mới. Vì _string đánh địa chỉ một mảng ký tự nên nên sử dụng phiên bản mảng của lệnh xóa (xem Phần 8.4).

Một lưu ý cuối cùng về toán tử gán. Kiểu trả về của nó là tham chiếu đến lớp String. Tại sao chính xác là liên kết? Vấn đề là đối với các kiểu có sẵn, toán tử gán có thể được xâu chuỗi:

// ghép các toán tử gán int iobj, jobj; iobj = jobj = 63;

Chúng được liên kết từ phải sang trái, tức là trong ví dụ trước, các bài tập được thực hiện như thế này:

Iobj = (jobj = 63);

Điều này cũng thuận tiện khi làm việc với các đối tượng của lớp String: ví dụ: cấu trúc sau được hỗ trợ:

Chuỗi ver, danh từ; động từ = danh từ = "đếm";

Phép gán đầu tiên từ chuỗi này gọi toán tử được xác định trước đó cho const char*. Kiểu của kết quả thu được phải sao cho có thể được sử dụng làm đối số cho toán tử gán sao chép của lớp String. Do đó, mặc dù tham số của toán tử này thuộc kiểu const char * nhưng nó vẫn trả về một tham chiếu đến một Chuỗi.

Toán tử gán có thể bị quá tải. Ví dụ: trong lớp String, chúng ta có tập hợp sau:

// tập hợp các toán tử gán được nạp chồng String& operator=(const String &); Chuỗi& toán tử=(const char *);

Một toán tử gán riêng biệt có thể tồn tại cho từng loại được phép gán cho đối tượng String. Tuy nhiên, tất cả các toán tử như vậy phải được định nghĩa là các hàm thành viên của lớp.

15.4. Toán tử chỉ mục

Chỉ mục operator() có thể được định nghĩa trên các lớp biểu diễn sự trừu tượng hóa của vùng chứa mà từ đó các phần tử riêng lẻ được lấy ra. Ví dụ về các vùng chứa như vậy bao gồm lớp String của chúng ta, lớp IntArray được giới thiệu trong Chương 2 hoặc mẫu lớp vectơ được xác định trong Thư viện chuẩn C++. Toán tử chỉ mục phải là một hàm thành viên của lớp.

Người dùng Chuỗi phải có khả năng đọc và viết các ký tự riêng lẻ của thành viên _string. Chúng tôi muốn hỗ trợ cách sử dụng các đối tượng của lớp này như sau:

Mục nhập chuỗi("ngang trọng"); Chuỗi mycopy; vì (int ix = 0; ix< entry.size(); ++ix) mycopy[ ix ] = entry[ ix ];

Toán tử chỉ mục có thể xuất hiện ở bên trái hoặc bên phải toán tử gán. Để ở phía bên trái, nó phải trả về giá trị l của phần tử được lập chỉ mục. Để làm điều này, chúng tôi trả lại một liên kết:

#bao gồm inine char& String::operator(int elem) const ( khẳng định(elem >= 0 && elem< _size); return _string[ elem ]; }

Đoạn mã sau gán ký tự "V" cho phần tử 0 của mảng màu:

Màu chuỗi ("tím"); màu [ 0 ] = "V";

Xin lưu ý rằng định nghĩa của toán tử sẽ kiểm tra xem chỉ mục có vượt quá giới hạn của mảng hay không. Hàm thư viện C khẳng định() được sử dụng cho việc này. Cũng có thể đưa ra một ngoại lệ cho biết giá trị của elem nhỏ hơn 0 hoặc lớn hơn độ dài của chuỗi C được tham chiếu bởi _string. (Việc nêu và xử lý các ngoại lệ đã được thảo luận ở Chương 11.)

15,5. Toán tử gọi hàm

Toán tử gọi hàm có thể bị quá tải đối với các đối tượng thuộc loại lớp. (Chúng ta đã thấy nó được sử dụng như thế nào khi thảo luận về các đối tượng hàm trong Phần 12.3.) Nếu một lớp được định nghĩa đại diện cho một thao tác, thì toán tử tương ứng sẽ bị nạp chồng để gọi nó. Ví dụ, để lấy giá trị tuyệt đối của một int, bạn có thể định nghĩa lớp absInt:

Lớp absInt ( public: int operator())(int val) ( int result = val< 0 ? -val: val; return result; } };

Toán tử operator() bị quá tải phải được khai báo là hàm thành viên với số lượng tham số tùy ý. Các tham số và giá trị trả về có thể thuộc bất kỳ loại nào được phép đối với các hàm (xem phần 7.2, 7.3 và 7.4). operator() được gọi bằng cách áp dụng danh sách các đối số cho một đối tượng của lớp mà nó được định nghĩa. Chúng ta sẽ xem xét cách nó được sử dụng trong một trong những thuật toán chung được mô tả trong chương. Trong ví dụ sau, thuật toán Transform() chung được gọi để áp dụng thao tác được xác định trong absInt cho từng phần tử của vectơ ivec, tức là. để thay thế một phần tử bằng giá trị tuyệt đối của nó.

#bao gồm #bao gồm int main() ( int ia = ( -0, 1, -1, -2, 3, 5, -5, 8 ); vectơ ivec(ia, ia+8); // thay thế từng phần tử bằng giá trị tuyệt đối của nó Transform(ivec.begin(), ivec.end(), ivec.begin(), absInt()); // ... )

Đối số thứ nhất và thứ hai của Transform() giới hạn phạm vi các phần tử mà thao tác absInt được áp dụng. Phần thứ ba chỉ ra phần đầu của vectơ nơi kết quả của việc áp dụng thao tác sẽ được lưu trữ.

Đối số thứ tư là một đối tượng absInt tạm thời được tạo bằng hàm tạo mặc định. Một bản khởi tạo của thuật toán Transform() chung được gọi từ main() có thể trông như thế này:

Vectơ typedef ::iterator iter_type; // khởi tạo biến đổi() // thao tác absInt được áp dụng cho phần tử vectơ int iter_type Transform(iter_type iter, iter_type Last, iter_type result, absInt func) ( while (iter != Last) *result++ = func(*iter++); // được gọi là absInt::operator() return iter; )

func là một đối tượng lớp cung cấp thao tác absInt, thay thế int bằng giá trị tuyệt đối của nó. Nó được sử dụng để gọi hàm operator() bị quá tải của lớp absInt. Toán tử này được truyền một đối số *iter, trỏ đến phần tử của vectơ mà chúng ta muốn lấy giá trị tuyệt đối.

15.6. Toán tử mũi tên

Toán tử mũi tên, cho phép truy cập vào các thành viên, có thể bị quá tải đối với các đối tượng lớp. Nó phải được định nghĩa như một hàm thành viên và cung cấp ngữ nghĩa con trỏ. Toán tử này thường được sử dụng nhiều nhất trong các lớp cung cấp "con trỏ thông minh" hoạt động tương tự như các toán tử tích hợp sẵn nhưng cũng hỗ trợ một số chức năng bổ sung.

Giả sử chúng ta muốn định nghĩa một kiểu lớp để biểu diễn một con trỏ tới một đối tượng Screen (xem Chương 13):

Lớp ScreenPtr ( // ... riêng: Screen *ptr; );

Định nghĩa của ScreenPtr phải sao cho một đối tượng của lớp đó được đảm bảo trỏ đến một đối tượng Screen: không giống như con trỏ tích hợp, nó không thể rỗng. Sau đó, ứng dụng có thể sử dụng các đối tượng thuộc loại ScreenPtr mà không cần kiểm tra xem chúng có trỏ đến bất kỳ đối tượng Screen nào hay không. Để làm điều này, bạn cần định nghĩa một lớp ScreenPtr bằng một hàm tạo, nhưng không có hàm tạo mặc định (các hàm tạo đã được thảo luận chi tiết trong Phần 14.2):

Lớp ScreenPtr ( public: ScreenPtr(const Screen &s) : ptr(&s) ( ) // ... );

Bất kỳ định nghĩa nào về một đối tượng của lớp ScreenPtr đều phải chứa một bộ khởi tạo - một đối tượng của lớp Screen mà đối tượng ScreenPtr sẽ tham chiếu đến:

ScreenPtr p1; // lỗi: lớp ScreenPtr không có hàm tạo mặc định Screen myScreen(4, 4); ScreenPtr ps(myScreen); // Phải

Để làm cho lớp ScreenPtr hoạt động giống như một con trỏ tích hợp, bạn cần xác định một số toán tử quá tải—các toán tử quy định (*) và mũi tên để truy cập các thành viên:

// nạp chồng các toán tử để hỗ trợ lớp hành vi con trỏ ScreenPtr ( public: Screen& operator*() ( return *ptr; ) Screen* operator->() ( return ptr; ) // ... ); Toán tử truy cập thành viên là một ngôi nên không có tham số nào được truyền cho nó. Khi được sử dụng như một phần của biểu thức, kết quả của nó chỉ phụ thuộc vào loại toán hạng bên trái. Ví dụ: trong lệnh point->action(); loại điểm đang được kiểm tra. Nếu nó là một con trỏ tới một loại lớp nào đó thì ngữ nghĩa của toán tử truy cập thành viên tích hợp sẽ được áp dụng. Nếu đây là một đối tượng hoặc một tham chiếu đến một đối tượng thì nó sẽ được kiểm tra xem lớp này có toán tử truy cập bị quá tải hay không. Khi toán tử mũi tên quá tải được xác định, nó sẽ được gọi trên một đối tượng điểm, nếu không thì câu lệnh sẽ không hợp lệ vì toán tử dấu chấm phải được sử dụng để truy cập các thành viên của chính đối tượng đó (bao gồm cả bằng tham chiếu). Toán tử mũi tên quá tải phải trả về một con trỏ tới kiểu lớp hoặc một đối tượng của lớp mà nó được định nghĩa. Nếu một con trỏ được trả về, ngữ nghĩa của toán tử mũi tên tích hợp sẽ được áp dụng cho nó. Ngược lại, quá trình tiếp tục đệ quy cho đến khi nhận được một con trỏ hoặc phát hiện ra lỗi. Ví dụ: đây là cách bạn có thể sử dụng đối tượng ps của lớp ScreenPtr để truy cập các thành viên Screen: ps->move(2, 3); Vì ở bên trái của toán tử mũi tên có một đối tượng kiểu ScreenPtr, nên một toán tử tái định nghĩa của lớp này được sử dụng để trả về một con trỏ tới đối tượng Screen. Sau đó, toán tử mũi tên tích hợp sẽ được áp dụng cho giá trị kết quả để gọi hàm thành viên move(). Dưới là chương trình nhỏđể kiểm tra lớp ScreenPtr. Một đối tượng thuộc loại ScreenPtr được sử dụng giống như bất kỳ đối tượng nào thuộc loại Screen*: #include #bao gồm #include "Screen.h" void printScreen(const ScreenPtr &ps) ( cout<< "Screen Object (" << ps->chiều cao()<< ", " << ps->chiều rộng()<< ")\n\n"; for (int ix = 1; ix <= ps->chiều cao(); ++ix) ( for (int iy = 1; iy<= ps->chiều rộng(); ++iy) cout<được(ix, iy); cout<< "\n"; } } int main() { Screen sobj(2, 5); string init("HelloWorld"); ScreenPtr ps(sobj); // Установить содержимое экрана string::size_type initpos = 0; for (int ix = 1; ix <= ps->chiều cao(); ++ix) cho (int iy = 1; iy<= ps->chiều rộng(); ++iy) ( ps->move(ix, iy); ps->set(init[ initpos++ ]); ) // In nội dung màn hình printScreen(ps); trả về 0; )

Tất nhiên, những thao tác như vậy với con trỏ tới đối tượng lớp không hiệu quả bằng làm việc với con trỏ có sẵn. Do đó, một con trỏ thông minh phải cung cấp chức năng bổ sung quan trọng đối với ứng dụng để chứng minh sự phức tạp của việc sử dụng nó.

15.7. Toán tử tăng và giảm

Tiếp tục phát triển cách triển khai lớp ScreenPtr được giới thiệu trong phần trước, chúng ta sẽ xem xét thêm hai toán tử nữa được hỗ trợ cho các con trỏ tích hợp và điều mong muốn có cho con trỏ thông minh của chúng ta: tăng (++) và giảm (- -). Để sử dụng lớp ScreenPtr để tham chiếu đến các thành phần của một mảng đối tượng Screen, bạn sẽ cần thêm một số thành viên bổ sung.

Trước tiên, chúng ta sẽ xác định một thành viên mới, kích thước, chứa số 0 (biểu thị rằng đối tượng ScreenPtr trỏ đến một đối tượng) hoặc kích thước của mảng được đối tượng ScreenPtr đánh địa chỉ. Chúng ta cũng cần một thành viên offset lưu trữ offset từ đầu mảng đã cho:

Lớp ScreenPtr ( public: // ... private: int size; // kích thước mảng: 0 nếu đối tượng duy nhất là int offset; // ptr offset từ đầu mảng Screen *ptr; );

Hãy sửa đổi hàm tạo của lớp ScreenPtr có tính đến chức năng mới và các thành viên bổ sung của nó. Người dùng lớp của chúng ta phải chuyển một đối số bổ sung cho hàm tạo nếu đối tượng được tạo trỏ tới một mảng:

Lớp ScreenPtr ( public: ScreenPtr(Screen &s, int arraySize = 0) : ptr(&s), size (arraySize), offset(0) ( ) riêng: int size; int offset; Screen *ptr; );

Đối số này đặt kích thước của mảng. Để duy trì chức năng tương tự, chúng tôi sẽ cung cấp giá trị mặc định bằng 0 cho chức năng đó. Do đó, nếu đối số thứ hai của hàm tạo bị bỏ qua, thành viên có kích thước sẽ là 0 và do đó đối tượng sẽ trỏ đến một đối tượng Màn hình duy nhất. Các đối tượng của lớp ScreenPtr mới có thể được định nghĩa như sau:

Sàng lọc myScreen(4, 4); ScreenPtr pobj(myScreen); // đúng: trỏ tới một đối tượng const int arrSize = 10; Màn hình *parray = Màn hình mới[ arrSize ]; ScreenPtr parr(*parray, arrSize); // đúng: trỏ tới một mảng

Bây giờ chúng ta đã sẵn sàng xác định các toán tử tăng và giảm quá tải trong ScreenPtr. Tuy nhiên, chúng có hai loại: tiền tố và hậu tố. May mắn thay, cả hai lựa chọn đều có thể được xác định. Đối với toán tử tiền tố, phần khai báo không có gì bất ngờ:

Lớp ScreenPtr ( public: Screen& operator++(); Screen& operator--(); // ... );

Các toán tử như vậy được định nghĩa là các hàm toán tử đơn nhất. Ví dụ: bạn có thể sử dụng toán tử tăng tiền tố như sau: const int arrSize = 10; Màn hình *parray = Màn hình mới[ arrSize ]; ScreenPtr parr(*parray, arrSize); vì (int ix = 0; ix

Các định nghĩa của các toán tử quá tải này được đưa ra dưới đây:

Screen& ScreenPtr::operator++() ( if (size == 0) ( cerr<<"не могу инкрементировать указатель для одного объекта\n"; return *ptr; } if (offset >= kích thước - 1) (cerr<< "уже в конце массива\n"; return *ptr; } ++offset; return *++ptr; } Screen& ScreenPtr::operator--() { if (size == 0) { cerr << "не могу декрементировать указатель для одного объекта\n"; return *ptr; } if (offset <= 0) { cerr << "уже в начале массива\n"; return *ptr; } --offset; return *--ptr; }

Để phân biệt tiền tố với các toán tử hậu tố, các khai báo của tiền tố sau có một tham số bổ sung kiểu int. Đoạn mã sau khai báo các biến thể tiền tố và hậu tố của toán tử tăng và giảm cho lớp ScreenPtr:

Lớp ScreenPtr ( public: Screen& operator++(); // toán tử tiền tố Screen& operator--(); Screen& operator++(int); // toán tử hậu tố Screen& operator--(int); // ... );

Dưới đây là cách triển khai có thể của các toán tử postfix:

Screen& ScreenPtr::operator++(int) ( if (size == 0) ( cerr<< "не могу инкрементировать указатель для одного объекта\n"; return *ptr; } if (offset == size) { cerr << "уже на один элемент дальше конца массива\n"; return *ptr; } ++offset; return *ptr++; } Screen& ScreenPtr::operator--(int) { if (size == 0) { cerr << "не могу декрементировать указатель для одного объекта\n"; return *ptr; } if (offset == -1) { cerr <<"уже на один элемент раньше начала массива\n"; return *ptr; } --offset; return *ptr--; }

Xin lưu ý rằng không cần đặt tên cho tham số thứ hai vì nó không được sử dụng trong định nghĩa toán tử. Bản thân trình biên dịch cung cấp một giá trị mặc định cho nó và có thể bỏ qua. Đây là một ví dụ về việc sử dụng toán tử postfix:

Const int ArraySize = 10; Màn hình *parray = Màn hình mới[ arrSize ]; ScreenPtr parr(*parray, arrSize); vì (int ix = 0; ix

Nếu bạn gọi nó một cách rõ ràng, bạn vẫn phải truyền giá trị của đối số nguyên thứ hai. Trong trường hợp lớp ScreenPtr của chúng tôi, giá trị này bị bỏ qua, vì vậy nó có thể là bất cứ thứ gì:

Parr.operator++(1024); // gọi toán tử postfix++

Các toán tử tăng và giảm quá tải được phép khai báo dưới dạng hàm bạn bè. Hãy thay đổi định nghĩa của lớp ScreenPtr cho phù hợp:

Lớp ScreenPtr ( // khai báo không phải thành viên your Screen& operator++(Screen &); // toán tử tiền tố bạn Screen& operator--(Screen &);friend Screen& operator++(Screen &, int); // toán tử hậu tố bạn Screen& operator-- ( Screen &, int); public: // định nghĩa thành viên );

Bài tập 15.7

Viết định nghĩa về các toán tử tăng và giảm quá tải cho lớp ScreenPtr, giả sử rằng chúng được khai báo là bạn của lớp.

Bài tập 15.8

Sử dụng ScreenPtr, bạn có thể biểu diễn một con trỏ tới một mảng các đối tượng của lớp Screen. Sửa đổi sự quá tải của operator*() và operator >() (xem Phần 15.6) để con trỏ không bao giờ xử lý một phần tử trước phần đầu hoặc sau phần cuối của mảng. Mẹo: Những toán tử này nên sử dụng các thành phần kích thước và offset mới.

15.8. Toán tử mới và xóa

Theo mặc định, việc phân bổ một đối tượng lớp từ một đống và giải phóng bộ nhớ mà nó chiếm giữ được thực hiện bằng cách sử dụng các toán tử toàn cục new() và delete() được xác định trong thư viện chuẩn C++. (Chúng ta đã thảo luận về các toán tử này trong Phần 8.4.) Nhưng một lớp có thể thực hiện chiến lược quản lý bộ nhớ của riêng nó bằng cách cung cấp các toán tử thành viên cùng tên. Nếu chúng được định nghĩa trong một lớp, chúng sẽ được gọi thay vì các toán tử toàn cục để phân bổ và giải phóng bộ nhớ cho các đối tượng của lớp này.

Hãy định nghĩa các toán tử new() và delete() trong lớp Screen của chúng ta.

Toán tử thành viên new() phải trả về giá trị kiểu void* và lấy tham số đầu tiên của nó là giá trị kiểu size_t, trong đó size_t là typedef được xác định trong tệp tiêu đề hệ thống. Đây là thông báo của anh ấy:

Khi new() được sử dụng để tạo một đối tượng của một kiểu lớp, trình biên dịch sẽ kiểm tra xem toán tử đó có được định nghĩa trong lớp đó hay không. Nếu có thì nó được gọi để cấp phát bộ nhớ cho đối tượng; nếu không thì toán tử toàn cục new() sẽ được gọi. Ví dụ: hướng dẫn sau

Màn hình *ps = Màn hình mới;

tạo một đối tượng Screen trong heap và vì lớp này có toán tử new() nên nó được gọi. Tham số size_t của toán tử được tự động khởi tạo thành giá trị bằng kích thước của Màn hình tính bằng byte.

Việc thêm hoặc xóa new() vào một lớp không ảnh hưởng đến mã người dùng. Lệnh gọi new trông giống nhau đối với cả toán tử toàn cục và toán tử thành viên. Nếu lớp Screen không có new() riêng thì lệnh gọi sẽ vẫn đúng, chỉ có toán tử toàn cục mới được gọi thay vì toán tử thành viên.

Bằng cách sử dụng toán tử phân giải phạm vi toàn cục, bạn có thể gọi toàn cục new() ngay cả khi lớp Screen xác định phiên bản của chính nó:

Màn hình *ps = ::Màn hình mới;

Khi toán hạng của delete là một con trỏ tới một đối tượng của một kiểu lớp, trình biên dịch sẽ kiểm tra xem toán tử delete() có được định nghĩa trong lớp đó hay không. Nếu có thì nó được gọi để giải phóng bộ nhớ; nếu không thì phiên bản toàn cục của toán tử sẽ được gọi. Hướng dẫn tiếp theo

Xóa ps;

Giải phóng bộ nhớ bị chiếm bởi đối tượng Screen được trỏ tới bởi ps. Vì Màn hình có toán tử thành viên delete() nên đây là nội dung được sử dụng. Tham số toán tử kiểu void* được tự động khởi tạo thành giá trị ps. Việc thêm delete() vào hoặc xóa nó khỏi một lớp không ảnh hưởng đến mã người dùng. Lệnh gọi xóa trông giống nhau đối với cả toán tử toàn cục và toán tử thành viên. Nếu lớp Screen không có toán tử delete() riêng thì lệnh gọi sẽ vẫn đúng, chỉ có toán tử toàn cục mới được gọi thay vì toán tử thành viên.

Bằng cách sử dụng toán tử phân giải phạm vi toàn cục, bạn có thể gọi toàn cục delete() ngay cả khi Màn hình có phiên bản riêng được xác định:

::xóa ps;

Nói chung, toán tử delete() được sử dụng phải khớp với toán tử new() mà bộ nhớ được phân bổ. Ví dụ: nếu ps trỏ đến một vùng bộ nhớ được phân bổ bởi new() toàn cục, thì nên sử dụng delete() toàn cục để giải phóng nó.

Toán tử delete() được xác định cho một loại lớp có thể nhận hai tham số thay vì một. Tham số đầu tiên vẫn phải thuộc loại void* và tham số thứ hai phải thuộc loại size_t được xác định trước (đừng quên bao gồm tệp tiêu đề):

Màn hình lớp ( public: // thay thế // void operator delete(void *); void operator delete(void *, size_t); );

Nếu có tham số thứ hai, trình biên dịch sẽ tự động khởi tạo nó với giá trị bằng kích thước tính bằng byte của đối tượng được tham số đầu tiên đánh địa chỉ. (Tùy chọn này rất quan trọng trong hệ thống phân cấp lớp, trong đó toán tử delete() có thể được kế thừa bởi một lớp dẫn xuất. Tính kế thừa sẽ được thảo luận chi tiết hơn trong chương này.)

Chúng ta hãy xem cách triển khai các toán tử new() và delete() trong lớp Screen một cách chi tiết hơn. Chiến lược phân bổ bộ nhớ của chúng tôi sẽ dựa trên danh sách liên kết Các đối tượng màn hình, phần đầu của đối tượng này được thành viên freeStore trỏ tới. Mỗi lần toán tử thành viên new() được gọi, đối tượng tiếp theo trong danh sách sẽ được trả về. Khi delete() được gọi, đối tượng sẽ được trả về danh sách. Nếu khi tạo một đối tượng mới, danh sách có địa chỉ tới freeStore trống thì toán tử toàn cục new() được gọi để lấy một khối bộ nhớ đủ để lưu trữ các đối tượng screenChunk của lớp Screen.

Cả screenChunk và freeStore đều chỉ được Screen quan tâm, vì vậy chúng tôi sẽ biến họ thành thành viên riêng tư. Ngoài ra, đối với tất cả các đối tượng được tạo trong lớp của chúng ta, giá trị của các thành viên này phải giống nhau và do đó, chúng phải được khai báo là tĩnh. Để hỗ trợ cấu trúc danh sách liên kết của các đối tượng Screen, chúng ta cần thành viên thứ ba tiếp theo:

Màn hình lớp ( public: void *operator new(size_t); void operator delete(void *, size_t); // ... riêng tư: Screen *next; static Screen *freeStore; static const int screenChunk; );

Đây là một triển khai có thể có của toán tử new() cho lớp Screen:

#include "Screen.h" #include // các thành viên tĩnh được khởi tạo // trong tệp nguồn chương trình, không phải trong tệp tiêu đề Screen *Screen::freeStore = 0; const int Màn hình::screenChunk = 24; void *Screen::operator new(size_t size) ( Screen *p; if (!freeStore) ( // danh sách liên kết trống: lấy một khối mới // toán tử toàn cục được gọi new size_t chunk = screenChunk * size; freeStore = p = diễn giải lại_cast< Screen* >(char mới [ đoạn ]); // đưa khối đã nhận vào danh sách cho (; p != &freeStore[ screenChunk - 1 ]; ++p) p->next = p+1; p->tiếp theo = 0; ) p = freeStore; freeStore = freeStore->next; trả lại p; ) Và đây là cách triển khai toán tử delete(): void Screen::operator delete(void *p, size_t) ( // chèn đối tượng “đã xóa” trở lại // vào danh sách trống (static_cast< Screen* >(p))->next = freeStore; freeStore = static_cast< Screen* >(P); )

Toán tử new() có thể được khai báo trong một lớp mà không cần xóa() tương ứng. Trong trường hợp này, các đối tượng được giải phóng bằng cách sử dụng toán tử chung cùng tên. Nó cũng được phép khai báo toán tử delete() mà không cần new(): các đối tượng sẽ được tạo bằng toán tử toàn cục cùng tên. Tuy nhiên, thông thường các toán tử này được triển khai đồng thời, như trong ví dụ trên, vì nhà phát triển lớp thường cần cả hai.

Chúng là các thành viên tĩnh của lớp, ngay cả khi lập trình viên không khai báo chúng một cách rõ ràng như vậy và phải tuân theo các hạn chế thông thường đối với các hàm thành viên đó: chúng không được truyền con trỏ this và do đó chỉ có thể truy cập trực tiếp vào các thành viên tĩnh. (Xem thảo luận về các hàm thành viên tĩnh trong Phần 13.5.) Lý do các toán tử này được đặt ở dạng tĩnh là vì chúng được gọi trước khi đối tượng lớp được xây dựng (new()) hoặc sau khi nó bị hủy (delete()).

Phân bổ bộ nhớ bằng toán tử new(), ví dụ:

Màn hình *ptr = Màn hình mới(10, 20);

// Mã giả trong C++ ptr = Screen::operator new(sizeof(Screen)); Màn hình::Màn hình(ptr, 10, 20);

Nói cách khác, toán tử new() của lớp trước tiên được gọi để phân bổ bộ nhớ cho đối tượng, sau đó đối tượng được khởi tạo bởi hàm tạo. Nếu new() không thành công, một ngoại lệ thuộc loại bad_alloc sẽ được đưa ra và hàm tạo không được gọi.

Giải phóng bộ nhớ bằng toán tử delete(), ví dụ:

Xóa ptr;

tương đương với việc thực hiện tuần tự các hướng dẫn sau:

// Mã giả trong C++ Screen::~Screen(ptr); Screen::operator delete(ptr, sizeof(*ptr));

Do đó, khi một đối tượng bị hủy, hàm hủy của lớp sẽ được gọi trước tiên, sau đó toán tử delete() được định nghĩa trong lớp sẽ được gọi để giải phóng bộ nhớ. Nếu ptr bằng 0 thì cả hàm hủy và hàm delete() đều không được gọi.

15.8.1. Toán tử mới và xóa

Toán tử new(), được định nghĩa trong tiểu mục trước, chỉ được gọi khi bộ nhớ được cấp phát cho một đối tượng. Vì vậy, trong hướng dẫn này new() của lớp Screen được gọi:

// được gọi là Screen::operator new() Screen *ps = new Screen(24, 80);

trong khi bên dưới toán tử toàn cục new() được gọi để phân bổ bộ nhớ từ vùng heap cho một mảng đối tượng thuộc loại Màn hình:

// được gọi là Screen::operator new() Screen *psa = new Screen;

Lớp này cũng có thể khai báo các toán tử new() và delete() để làm việc với mảng.

Toán tử thành viên new() phải trả về giá trị kiểu void* và lấy giá trị kiểu size_t làm tham số đầu tiên. Đây là thông báo của anh ấy cho Screen:

Màn hình lớp ( public: void *operator new(size_t); // ... );

Khi sử dụng new để tạo một mảng các đối tượng thuộc một kiểu lớp, trình biên dịch sẽ kiểm tra xem lớp đó có định nghĩa toán tử new() hay không. Nếu có thì nó được gọi để cấp phát bộ nhớ cho mảng; nếu không thì hàm new() toàn cục sẽ được gọi. Câu lệnh sau tạo ra một mảng gồm 10 đối tượng Screen trong heap:

Màn hình *ps = Màn hình mới;

Lớp này có toán tử new(), đó là lý do tại sao nó được gọi để cấp phát bộ nhớ. Tham số size_t của nó được tự động khởi tạo theo dung lượng bộ nhớ, tính bằng byte, cần thiết để chứa mười đối tượng Màn hình.

Ngay cả khi một lớp có toán tử thành viên new(), lập trình viên có thể gọi new() toàn cục để tạo một mảng bằng toán tử phân giải phạm vi toàn cục:

Màn hình *ps = ::Màn hình mới;

Toán tử delete(), là thành viên của lớp, phải có kiểu void và lấy void* làm tham số đầu tiên. Quảng cáo trên màn hình của anh ấy trông như thế này:

Màn hình lớp ( public: void operator delete(void *); );

Để xóa một mảng các đối tượng lớp, lệnh xóa phải được gọi như thế này:

Xóa ps;

Khi toán hạng xóa là một con trỏ tới một đối tượng của một kiểu lớp, trình biên dịch sẽ kiểm tra xem toán tử delete() có được định nghĩa trong lớp đó hay không. Nếu có thì nó được gọi để giải phóng bộ nhớ, nếu không thì phiên bản toàn cầu của nó sẽ được gọi. Một tham số kiểu void* được tự động khởi tạo thành giá trị của địa chỉ bắt đầu vùng nhớ chứa mảng.

Ngay cả khi một lớp có toán tử thành viên delete(), lập trình viên có thể gọi hàm delete() toàn cục bằng cách sử dụng toán tử phân giải phạm vi toàn cục:

::xóa ps;

Việc thêm hoặc xóa các toán tử new() hoặc delete() vào một lớp không ảnh hưởng đến mã người dùng: các lệnh gọi tới cả toán tử toàn cục và toán tử thành viên trông giống nhau.

Khi tạo một mảng, new() trước tiên được gọi để phân bổ bộ nhớ cần thiết, sau đó mỗi phần tử được khởi tạo bằng cách sử dụng một hàm tạo mặc định. Nếu một lớp có ít nhất một hàm tạo nhưng không có hàm tạo mặc định thì việc gọi toán tử new() được coi là một lỗi. Không có cú pháp để chỉ định các bộ khởi tạo phần tử mảng hoặc các đối số của hàm tạo lớp khi tạo một mảng theo cách này.

Khi một mảng bị hủy, hàm hủy lớp được gọi đầu tiên để hủy các phần tử, sau đó toán tử delete() được gọi để giải phóng tất cả bộ nhớ. Điều quan trọng là sử dụng đúng cú pháp. Nếu hướng dẫn

Xóa ps;

ps trỏ đến một mảng các đối tượng lớp, thì việc thiếu dấu ngoặc vuông sẽ khiến hàm hủy chỉ được gọi cho phần tử đầu tiên, mặc dù bộ nhớ sẽ được giải phóng hoàn toàn.

Toán tử thành viên delete() có thể có hai tham số thay vì một và tham số thứ hai phải thuộc loại size_t:

Màn hình lớp ( public: // thay thế // void operator delete(void*); void operator delete(void*, size_t); );

Nếu có tham số thứ hai, trình biên dịch sẽ tự động khởi tạo nó với giá trị bằng dung lượng bộ nhớ được phân bổ cho mảng tính bằng byte.

15.8.2. Toán tử cấp phát new() và toán tử delete()

Toán tử thành viên new() có thể bị quá tải với điều kiện là tất cả các khai báo đều có danh sách tham số khác nhau. Tham số đầu tiên phải thuộc loại size_t:

Màn hình lớp ( public: void *operator new(size_t); void *operator new(size_t, Screen *); // ... );

Các tham số còn lại được khởi tạo bởi các đối số phân bổ được đưa ra khi gọi new:

Void func(Màn hình *bắt đầu) ( // ... )

Phần biểu thức đứng sau từ khóa mới và được đặt trong dấu ngoặc đơn biểu thị các đối số phân bổ. Ví dụ trên gọi toán tử new(), có hai tham số. Giá trị đầu tiên được tự động khởi tạo theo kích thước của lớp Màn hình tính bằng byte và giá trị thứ hai là giá trị của đối số vị trí bắt đầu.

Bạn cũng có thể nạp chồng toán tử thành viên delete(). Tuy nhiên, toán tử như vậy không bao giờ được gọi từ biểu thức xóa. Quá tải delete() được trình biên dịch gọi ngầm nếu hàm tạo được gọi khi thực thi toán tử mới (đó không phải là lỗi đánh máy, thực ra chúng tôi muốn nói là mới) ném ra một ngoại lệ. Chúng ta hãy xem xét việc sử dụng delete() kỹ hơn.

Trình tự các hành động khi đánh giá một biểu thức

Màn hình *ps = màn hình mới (bắt đầu);

  1. Toán tử new(size_t, Screen*) được xác định trong lớp sẽ được gọi.
  2. Hàm tạo mặc định của lớp Screen được gọi để khởi tạo đối tượng đã tạo.

Biến ps được khởi tạo bằng địa chỉ của đối tượng Screen mới.

Giả sử rằng toán tử lớp new(size_t, Screen*) phân bổ bộ nhớ bằng cách sử dụng hàm new() toàn cục. Làm cách nào nhà phát triển có thể đảm bảo rằng bộ nhớ sẽ được giải phóng nếu hàm tạo được gọi ở bước 2 đưa ra một ngoại lệ? Để bảo vệ mã người dùng khỏi rò rỉ bộ nhớ, bạn nên cung cấp toán tử delete() bị quá tải chỉ được gọi trong trường hợp này.

Nếu một lớp có một toán tử bị quá tải với các tham số có kiểu khớp với new(), trình biên dịch sẽ tự động gọi nó để giải phóng bộ nhớ. Giả sử chúng ta có biểu thức sau với toán tử phân bổ mới:

Màn hình *ps = màn hình mới (bắt đầu);

Nếu hàm tạo mặc định của lớp Screen đưa ra một ngoại lệ, trình biên dịch sẽ tìm delete() trong phạm vi của Screen. Để tìm thấy một toán tử như vậy, các loại tham số của nó phải khớp với các loại tham số của lệnh gọi tới new(). Vì tham số đầu tiên của new() luôn thuộc loại size_t và tham số của toán tử delete() là void* nên các tham số đầu tiên không được tính đến khi so sánh. Trình biên dịch tìm toán tử delete() sau trong lớp Screen:

Xóa toán tử void(void*, Screen*);

Nếu tìm thấy một toán tử như vậy, nó sẽ được gọi để giải phóng bộ nhớ trong trường hợp new() ném ra một ngoại lệ. (Nếu không thì nó không được gọi.)

Người thiết kế một lớp quyết định có cung cấp một delete() tương ứng với một new() hay không tùy thuộc vào việc toán tử new() đó tự phân bổ bộ nhớ hay sử dụng bộ nhớ đã được phân bổ. Trong trường hợp đầu tiên, delete() phải được kích hoạt để giải phóng bộ nhớ nếu hàm tạo đưa ra một ngoại lệ; bằng không thì không cần tới nó.

Bạn cũng có thể nạp chồng toán tử phân bổ new() và toán tử delete() cho mảng:

Màn hình lớp ( public: void *operator new(size_t); void *operator new(size_t, Screen*); void operator delete(void*, size_t); void operator delete(void*, Screen*); // ... );

Toán tử new() được sử dụng khi các đối số phân bổ thích hợp được chỉ định trong biểu thức chứa new để phân bổ một mảng:

Void func(Screen *start) ( // được gọi là Screen::operator new(size_t, Screen*) Screen *ps = new (start) Screen; // ... )

Nếu hàm tạo đưa ra một ngoại lệ trong quá trình hoạt động của toán tử mới, thì hàm delete() tương ứng sẽ tự động được gọi.

Bài tập 15.9

Giải thích cách khởi tạo nào sau đây là sai:

Lớp iStack ( công khai: iStack(int dung lượng) : _stack(dung lượng), _top(0) () // ... riêng tư: int _top; vatcor< int>_cây rơm; ); (a) iStack *ps = iStack mới(20); (b) iStack *ps2 = new const iStack(15); (c) iStack *ps3 = iStack mới[ 100 ];

Bài tập 15.10

Điều gì xảy ra trong các biểu thức chứa mới và xóa sau đây?

Bài tập của lớp ( public: Bài tập(); ~Exercise(); ); Bài tập *pe = Bài tập mới; xóa ps;

Sửa đổi các biểu thức này để các toán tử toàn cục new() và delete() được gọi.

Bài tập 15.11

Giải thích lý do tại sao nhà phát triển lớp nên cung cấp toán tử delete().

15.9. Các phép biến đổi do người dùng xác định

Chúng ta đã thấy cách chuyển đổi kiểu áp dụng cho toán hạng của các kiểu dựng sẵn: Phần 4.14 đã xem xét vấn đề này bằng cách sử dụng toán hạng của các toán tử dựng sẵn làm ví dụ, và Phần 9.3 đã xem xét các đối số thực tế của một hàm được gọi để truyền chúng thành các kiểu tham số hình thức. Từ quan điểm này, hãy xem xét sáu phép toán cộng sau:

Char ch; sh ngắn;, int ival; /* trong mỗi thao tác một toán hạng * yêu cầu chuyển đổi kiểu */ ch + ival; ival + ch; ch + sh; ch + ch; ival + sh; sh + ival;

Các toán hạng ch và sh được mở rộng thành kiểu int. Khi thực hiện một thao tác, hai giá trị int được thêm vào. Việc mở rộng kiểu được trình biên dịch ngầm thực hiện và minh bạch đối với người dùng.

Trong phần này, chúng ta sẽ xem cách nhà phát triển có thể xác định các phép biến đổi tùy chỉnh cho các đối tượng thuộc loại lớp. Những chuyển đổi do người dùng xác định như vậy cũng được trình biên dịch tự động gọi khi cần thiết. Để biết tại sao chúng lại cần thiết, chúng ta hãy xem lại lớp SmallInt được giới thiệu trong Phần 10.9.

Hãy nhớ lại rằng SmallInt cho phép bạn xác định các đối tượng có thể lưu trữ các giá trị trong cùng phạm vi với một ký tự không dấu, tức là. từ 0 đến 255 và chặn các lỗi vượt quá giới hạn của nó. Trong tất cả các khía cạnh khác, lớp này hoạt động giống hệt như unsigned char.

Để có thể cộng và trừ các đối tượng SmallInt với các đối tượng khác cùng lớp hoặc với các giá trị của các kiểu có sẵn, chúng tôi triển khai sáu hàm toán tử:

Lớp SmallInt ( bạn toán tử+(const SmallInt &, int); bạn toán tử-(const SmallInt &, int); bạn toán tử-(int, const SmallInt &); bạn toán tử+(int, const SmallInt &); public: SmallInt(int ival) : value(ival) ( ) operator+(const SmallInt &); operator-(const SmallInt &); // ... riêng tư: int value; );

Các toán tử thành viên cung cấp khả năng cộng và trừ hai đối tượng SmallInt. Toán tử bạn bè toàn cục cho phép bạn thực hiện các thao tác này trên các đối tượng của một lớp nhất định và các đối tượng thuộc kiểu số học tích hợp. Chỉ cần sáu toán tử vì bất kỳ kiểu số học tích hợp nào cũng có thể được chuyển thành int. Ví dụ, biểu thức

giải quyết theo hai bước:

  1. Hằng số 3,14159 của loại double được chuyển đổi thành số nguyên 3.
  2. Toán tử+(const SmallInt &,int) được gọi và trả về giá trị 6.

Nếu chúng ta muốn hỗ trợ các toán tử logic và bitwise, cũng như các toán tử so sánh và toán tử gán phức hợp, thì cần nạp chồng toán tử đến mức nào? Bạn không thể đếm nó ngay lập tức. Sẽ thuận tiện hơn nhiều khi tự động chuyển đổi một đối tượng của lớp SmallInt thành một đối tượng có kiểu int.

Ngôn ngữ C++ có một cơ chế cho phép bất kỳ lớp nào chỉ định một tập hợp các phép biến đổi có thể áp dụng cho các đối tượng của nó. Đối với SmallInt, chúng ta sẽ định nghĩa một tập hợp đối tượng để gõ int. Đây là cách thực hiện của nó:

Lớp SmallInt ( public: SmallInt(int ival) : value(ival) ( ) // bộ chuyển đổi // SmallInt ==> toán tử int int() ( return value; ) // không cần các toán tử quá tải riêng tư: int value; );

Toán tử int() là một trình chuyển đổi thực hiện chuyển đổi do người dùng xác định, trong trường hợp này chuyển kiểu lớp thành kiểu int nhất định. Định nghĩa của trình chuyển đổi mô tả ý nghĩa của chuyển đổi và trình biên dịch phải làm gì để áp dụng nó. Đối với đối tượng SmallInt, mục đích của việc chuyển đổi thành int là trả về int được lưu trong thành viên giá trị.

Bây giờ một đối tượng của lớp SmallInt có thể được sử dụng ở bất cứ nơi nào int được cho phép. Giả sử rằng không còn toán tử nào bị quá tải và bộ chuyển đổi thành int được xác định trong SmallInt, thao tác cộng

SmallInt si(3); si+3.14159

giải quyết theo hai bước:

  1. Trình chuyển đổi lớp SmallInt được gọi, trả về số nguyên 3.
  2. Số nguyên 3 được mở rộng thành 3,0 và được thêm vào hằng số chính xác kép 3,14159, dẫn đến 6,14159.

Hành vi này phù hợp hơn với hành vi của toán hạng của các kiểu dựng sẵn so với các toán tử quá tải được xác định trước đó. Khi một int được thêm vào double, hai double sẽ được thêm vào (vì int mở rộng thành double) và kết quả là một số cùng loại.

Chương trình này minh họa việc sử dụng lớp SmallInt:

#bao gồm #include "SmallInt.h" int main() ( cout<< "Введите SmallInt, пожалуйста: "; while (cin >> si1) (cout<< "Прочитано значение " << si1 << "\nОно "; // SmallInt::operator int() вызывается дважды cout << ((si1 >127)? "hơn" : ((si1< 127) ? "меньше, чем " : "равно ")) <<"127\n"; cout << "\Введите SmallInt, пожалуйста \ (ctrl-d для выхода): "; } cout <<"До встречи\n"; }

Chương trình biên dịch cho kết quả như sau:

Vui lòng nhập SmallInt: 127

Đọc giá trị 127

Nó bằng 127

Vui lòng nhập SmallInt (ctrl-d để thoát): 126

Nó nhỏ hơn 127

Vui lòng nhập SmallInt (ctrl-d để thoát): 128

Nó lớn hơn 127

Vui lòng nhập SmallInt (ctrl-d để thoát): 256

***Lỗi phạm vi SmallInt: 256***

#bao gồm lớp SmallInt (bạn istream& toán tử>(istream &is, SmallInt &s); bạn ostream& toán tử<<(ostream &is, const SmallInt &s) { return os << s.value; } public: SmallInt(int i=0) : value(rangeCheck(i)){} int operator=(int i) { return(value = rangeCheck(i)); } operator int() { return value; } private: int rangeCheck(int); int value; };

Dưới đây là định nghĩa về các hàm thành viên bên ngoài nội dung lớp:

Istream& operator>>(istream &is, SmallInt &si) ( int ix; is >> ix; si = ix; // SmallInt::operator=(int) return is; ) int SmallInt::rangeCheck(int i) ( /* nếu ít nhất một bit được đặt ngoại trừ tám * đầu tiên thì giá trị quá lớn; báo cáo và thoát ngay lập tức */ if (i & ~0377) ( cerr< <"\n*** Ошибка диапазона SmallInt: " << i << " ***" << endl; exit(-1); } return i; }

15.9.1. Bộ chuyển đổi

Trình chuyển đổi là trường hợp đặc biệt của hàm thành viên lớp thực hiện chuyển đổi do người dùng định nghĩa từ một đối tượng sang một số loại khác. Trình chuyển đổi được khai báo trong nội dung lớp bằng cách chỉ định toán tử từ khóa theo sau là loại mục tiêu của chuyển đổi.

Tên theo sau từ khóa không nhất thiết phải là tên của một trong các loại có sẵn. Lớp Token hiển thị bên dưới định nghĩa một số bộ chuyển đổi. Một trong số chúng sử dụng typedef tName để chỉ định tên của kiểu và cái còn lại sử dụng kiểu lớp SmallInt.

#include "SmallInt.h" typedef char *tName; class Token ( public: Token(char *, int); operator SmallInt() ( return val; ) operator tName() ( return name; ) operator int() ( return val; ) // các thành viên public khác private: SmallInt val; tên nhân vật; );

Lưu ý rằng các định nghĩa về bộ chuyển đổi sang loại SmallInt và int là giống nhau. Bộ chuyển đổi Token::operator int() trả về giá trị của thành viên val. Vì val thuộc loại SmallInt, nên SmallInt::operator int() được sử dụng ngầm để chuyển đổi val thành loại int. Bản thân Token::operator int() được trình biên dịch sử dụng ngầm để chuyển đổi một đối tượng thuộc loại Token thành giá trị thuộc loại int. Ví dụ: bộ chuyển đổi này được sử dụng để truyền ngầm các đối số thực tế t1 và t2 của loại Token thành int của tham số hình thức của hàm print():

#include "Token.h" void print(int i) ( cout< < "print(int) : " < < i < < endl; } Token t1("integer constant", 127); Token t2("friend", 255); int main() { print(t1); // t1.operator int() print(t2); // t2.operator int() return 0; }

Sau khi biên dịch và chạy chương trình sẽ xuất ra những dòng sau:

In(int): 127 in(int): 255

Hình ảnh chung của bộ chuyển đổi như sau:

Kiểu toán tử();

trong đó loại có thể là loại có sẵn, loại lớp hoặc tên typedef. Không được phép chuyển đổi trong đó loại là mảng hoặc loại hàm. Bộ chuyển đổi phải là một hàm thành viên. Khai báo của nó không được chỉ định kiểu trả về hoặc danh sách các tham số:

Toán tử int(SmallInt &); // lỗi: không phải là thành viên của lớp SmallInt ( public: int operator int(); // error: trả về kiểu đã chỉ định toán tử int(int = 0); // error: danh sách tham số đã chỉ định // ... );

Bộ chuyển đổi được gọi là kết quả của việc chuyển đổi loại rõ ràng. Nếu giá trị đang được chuyển đổi có loại lớp có bộ chuyển đổi và loại bộ chuyển đổi này được chỉ định trong thao tác truyền thì nó được gọi là:

#include "Token.h" Token tok("function", 78); // ký hiệu hàm: được gọi là Token::operator SmallInt() SmallInt tokVal = SmallInt(tok); // static_cast: được gọi là Token::operator tName() char *tokName = static_cast< char * >(tok);

Bộ chuyển đổi Token::operator tName() có thể có tác dụng phụ không mong muốn. Nỗ lực truy cập trực tiếp vào thành viên riêng tư Token::name bị trình biên dịch gắn cờ là lỗi:

Char *tokName = tok.name; // lỗi: Token::name là thành viên riêng tư

Tuy nhiên, công cụ chuyển đổi của chúng tôi, bằng cách cho phép người dùng thay đổi trực tiếp Token::name, thực hiện chính xác những gì chúng tôi muốn bảo vệ. Nhiều khả năng điều này sẽ không xảy ra. Ví dụ: đây là cách sửa đổi như vậy có thể xảy ra:

#include "Token.h" Token tok("function", 78); char *tokName = tok; // đúng: chuyển đổi ngầm định *tokname = "P"; // nhưng bây giờ tên thành viên đã có Dấu câu!

Chúng tôi dự định cho phép quyền truy cập chỉ đọc vào đối tượng lớp Token đã được chuyển đổi. Do đó, trình chuyển đổi phải trả về loại const char*:

Typedef const char *cchar; Mã thông báo lớp ( public: operator cchar() ( return name; ) // ... ); // lỗi: không được phép chuyển đổi char* thành const char* char *pn = tok; const char *pn2 = tok; // Phải

Một giải pháp khác là thay thế kiểu char* trong định nghĩa Token bằng kiểu chuỗi từ thư viện chuẩn C++:

Mã thông báo lớp ( public: Token(string, int); operator SmallInt() ( return val; ) operator string() ( return name; ) operator int() ( return val; ) // các thành viên public khác private: SmallInt val; string tên; );

Ngữ nghĩa của trình chuyển đổi Token::operator string() là trả về một bản sao của giá trị (không phải con trỏ tới giá trị) của chuỗi biểu thị tên mã thông báo. Điều này ngăn chặn việc vô tình sửa đổi thành viên tên riêng của lớp Token.

Loại mục tiêu có phải khớp chính xác với loại trình chuyển đổi không? Ví dụ: đoạn mã sau có gọi trình chuyển đổi int() được xác định trong lớp Token không?

Khoảng trống bên ngoài calc (gấp đôi); Token tok("hằng số", 44); // Toán tử int() có được gọi không? Có // áp dụng chuyển đổi tiêu chuẩn int --> double calc(tok);

Nếu loại mục tiêu (trong trường hợp này là double) không khớp chính xác với loại trình chuyển đổi (int trong trường hợp của chúng tôi), thì trình chuyển đổi sẽ vẫn được gọi, miễn là có một chuỗi các chuyển đổi tiêu chuẩn dẫn đến loại mục tiêu từ trình chuyển đổi kiểu. (Các trình tự này được mô tả trong Phần 9.3.) Khi calc() được gọi, Token::operator int() được gọi để chuyển tok từ kiểu Token sang kiểu int. Sau đó, một chuyển đổi tiêu chuẩn được áp dụng để chuyển kết quả từ int sang double.

Sau khi chuyển đổi do người dùng xác định, chỉ cho phép chuyển đổi tiêu chuẩn. Nếu đạt được Loại mục tiêu Nếu cần một chuyển đổi tùy chỉnh khác, trình biên dịch sẽ không áp dụng bất kỳ chuyển đổi nào. Giả sử lớp Token không định nghĩa toán tử int() thì lệnh gọi sau sẽ thất bại:

Bên ngoài void calc(int); Token tok("con trỏ", 37); // nếu Token::operator int() không được xác định, // thì lệnh gọi này dẫn đến lỗi biên dịch calc(tok);

Nếu trình chuyển đổi Token::operator int() không được xác định thì việc truyền tok sang loại int sẽ yêu cầu gọi hai trình chuyển đổi do người dùng xác định. Đầu tiên, đối số thực tế tok sẽ phải được chuyển đổi từ loại Token sang SmallInt bằng trình chuyển đổi

Mã thông báo::toán tử SmallInt()

và sau đó chuyển đổi kết quả thành kiểu int - cũng sử dụng trình chuyển đổi tùy chỉnh

Mã thông báo::toán tử int()

Lệnh gọi calc(tok) bị trình biên dịch gắn cờ là lỗi vì không có sự chuyển đổi ngầm định từ Token sang int.

Nếu không có sự tương ứng logic giữa loại trình chuyển đổi và loại lớp thì mục đích của trình chuyển đổi có thể không rõ ràng đối với người đọc chương trình:

Class Date ( public: // thử đoán xem thành viên nào đang được trả về! operator int(); private: int tháng, ngày, năm; );

Bộ chuyển đổi int() của lớp Date sẽ trả về giá trị nào? Cho dù lý do cho một quyết định cụ thể có tốt đến đâu, người đọc sẽ không biết cách sử dụng các đối tượng Date, vì không có sự tương ứng logic rõ ràng giữa chúng và các số nguyên. Trong những trường hợp như vậy, tốt hơn hết là không nên xác định bộ chuyển đổi nào cả.

15.9.2. Trình xây dựng làm công cụ chuyển đổi

Một tập hợp các hàm tạo của lớp lấy một tham số duy nhất, chẳng hạn như SmallInt(int) của lớp SmallInt, xác định một tập hợp các chuyển đổi ngầm định thành các giá trị thuộc loại SmallInt. Do đó, hàm tạo SmallInt(int) chuyển đổi các giá trị kiểu int thành giá trị kiểu SmallInt.

Khoảng trống bên ngoài calc(SmallInt); int tôi; // cần phải chuyển đổi i thành giá trị kiểu SmallInt // điều này đạt được bằng cách sử dụng SmallInt(int) calc(i); Khi calc(i) được gọi, số i được chuyển đổi thành giá trị kiểu SmallInt bằng cách sử dụng hàm tạo SmallInt(int), được trình biên dịch gọi để tạo một đối tượng tạm thời thuộc kiểu mong muốn. Một bản sao của đối tượng này sau đó được chuyển tới calc() như thể lệnh gọi hàm đã được viết dưới dạng: // Mã giả C++ // tạo một đối tượng tạm thời thuộc loại SmallInt ( SmallInt temp = SmallInt(i); calc(temp) ; )

Dấu ngoặc nhọn trong ví dụ này cho biết thời gian tồn tại của đối tượng này: nó bị hủy khi hàm thoát.

Kiểu của tham số hàm tạo có thể là kiểu của một số lớp:

Số lớp ( public: // tạo một giá trị kiểu Number từ một giá trị kiểu SmallInt Number(const SmallInt &); // ... );

Trong trường hợp này, một giá trị thuộc loại SmallInt có thể được sử dụng ở bất kỳ nơi nào có giá trị thuộc loại Number hợp lệ:

Chức năng void bên ngoài(Số); SmallInt si(87); int main() ( // được gọi là Number(const SmallInt &) func(si); // ... )

Nếu một hàm tạo được sử dụng để thực hiện chuyển đổi ngầm thì loại tham số của nó có phải khớp chính xác với loại giá trị được chuyển đổi không? Ví dụ: đoạn mã sau có gọi SmallInt(int), được xác định trong lớp SmallInt, để chuyển dobj sang SmallInt không?

Khoảng trống bên ngoài calc(SmallInt); dobj đôi; // SmallInt(int) có được gọi không? Có // dobj được chuyển đổi từ double sang int // sử dụng chuyển đổi chuẩn calc(dobj);

Nếu cần, một chuỗi các chuyển đổi tiêu chuẩn sẽ được áp dụng cho đối số thực tế trước khi gọi hàm tạo thực hiện chuyển đổi do người dùng xác định. Khi gọi hàm calc(), phép chuyển đổi dobj tiêu chuẩn từ kiểu double sang kiểu int được sử dụng. Sau đó, để chuyển kết quả sang loại SmallInt, SmallInt(int) sẽ được gọi.

Trình biên dịch ngầm sử dụng một hàm tạo với một tham số duy nhất để chuyển đổi kiểu của nó thành kiểu của lớp mà hàm tạo đó thuộc về. Tuy nhiên, đôi khi sẽ thuận tiện hơn nếu hàm tạo Number(const SmallInt&) chỉ được gọi để khởi tạo một đối tượng thuộc loại Number thành một giá trị thuộc loại SmallInt và không bao giờ thực hiện chuyển đổi ngầm định. Để tránh việc sử dụng hàm tạo như vậy, hãy khai báo nó một cách rõ ràng:

Số lớp ( public: // không bao giờ sử dụng cho chuyển đổi ngầm Số rõ ràng (const SmallInt &); // ... );

Trình biên dịch không bao giờ sử dụng các hàm tạo rõ ràng để thực hiện chuyển đổi kiểu ẩn:

Chức năng void bên ngoài(Số); SmallInt si(87); int main() ( // lỗi: không có chuyển đổi ngầm từ SmallInt sang Number tồn tại func(si); // ... )

Tuy nhiên, hàm tạo như vậy vẫn có thể được sử dụng để chuyển đổi kiểu nếu nó được yêu cầu rõ ràng dưới dạng toán tử ép kiểu:

SmallInt si(87); int main() ( // lỗi: không có chuyển đổi ngầm từ SmallInt sang Number tồn tại func(si); func(Number(si)); // đúng: cast func(static_cast< Number >(si)); // đúng: truyền )

15.10. Lựa chọn chuyển đổi A

Chuyển đổi do người dùng xác định được triển khai dưới dạng trình chuyển đổi hoặc hàm tạo. Như đã đề cập, sau khi trình chuyển đổi thực hiện chuyển đổi, bạn được phép sử dụng chuyển đổi tiêu chuẩn để chuyển giá trị được trả về sang loại mục tiêu. Việc chuyển đổi được thực hiện bởi hàm tạo cũng có thể được bắt đầu bằng một chuyển đổi tiêu chuẩn để chuyển kiểu đối số thành loại tham số hình thức của hàm tạo.

Chuỗi chuyển đổi do người dùng xác định là sự kết hợp giữa chuyển đổi do người dùng xác định và chuyển đổi tiêu chuẩn cần thiết để truyền giá trị cho loại mục tiêu. Trình tự này trông giống như:

Trình tự các phép biến đổi chuẩn ->

Chuyển đổi do người dùng xác định ->

Trình tự các phép biến đổi chuẩn

trong đó chuyển đổi do người dùng xác định được thực hiện bởi trình chuyển đổi hoặc hàm tạo.

Có thể có hai chuỗi chuyển đổi tùy chỉnh khác nhau để chuyển đổi giá trị nguồn thành loại đích và sau đó trình biên dịch phải chọn chuỗi chuyển đổi tốt hơn. Chúng ta hãy xem làm thế nào điều này được thực hiện.

Một lớp được phép định nghĩa nhiều bộ chuyển đổi. Ví dụ: lớp Number của chúng ta có hai trong số chúng: toán tử int() và toán tử float(), cả hai đều có thể chuyển đổi một đối tượng kiểu Number thành giá trị kiểu float. Đương nhiên, bạn có thể sử dụng bộ chuyển đổi Token::operator float() để chuyển đổi trực tiếp. Nhưng Token::operator int() cũng phù hợp, vì kết quả ứng dụng của nó thuộc loại int và do đó có thể được chuyển đổi thành float bằng cách sử dụng chuyển đổi tiêu chuẩn. Phép biến đổi có mơ hồ không nếu có nhiều chuỗi như vậy? Hoặc một trong số chúng có thể được ưa thích hơn những cái khác?

Số lớp ( public: operator float(); operator int(); // ... ); Số số; float ff = num; // bộ chuyển đổi nào? toán tử float()

Trong những trường hợp như vậy, việc lựa chọn chuỗi biến đổi tốt nhất do người dùng xác định dựa trên phân tích chuỗi biến đổi được áp dụng sau bộ chuyển đổi. Trong ví dụ trước, bạn có thể sử dụng hai chuỗi sau:

  1. toán tử float() -> khớp chính xác
  2. toán tử int() -> chuyển đổi tiêu chuẩn

Như đã thảo luận trong Phần 9.3, kết hợp chính xác tốt hơn chuyển đổi tiêu chuẩn. Do đó, chuỗi đầu tiên tốt hơn chuỗi thứ hai, điều đó có nghĩa là bộ chuyển đổi Token::operator float() được chọn.

Có thể xảy ra trường hợp hai hàm tạo khác nhau được sử dụng để chuyển đổi một giá trị thành loại đích. Trong trường hợp này, chuỗi các phép biến đổi tiêu chuẩn trước lệnh gọi hàm tạo được phân tích:

Lớp SmallInt ( public: SmallInt(int ival) : value(ival) ( ) SmallInt(double dval) : value(static_cast< int >(dval)); ( ) ); thao tác void bên ngoài(const SmallInt &); int main() ( double dobj; manip(dobj); // đúng: SmallInt(double) )

Ở đây, lớp SmallInt định nghĩa hai hàm tạo - SmallInt(int) và SmallInt(double), có thể được sử dụng để thay đổi giá trị loại double thành đối tượng thuộc loại SmallInt: SmallInt(double) chuyển đổi trực tiếp double thành SmallInt và SmallInt( int) hoạt động với kết quả chuyển đổi tiêu chuẩn double thành int. Do đó, có hai chuỗi biến đổi do người dùng xác định:

  1. khớp chính xác -> SmallInt(double)
  2. chuyển đổi tiêu chuẩn -> SmallInt(int)

Vì kết quả khớp chính xác tốt hơn so với chuyển đổi tiêu chuẩn nên hàm tạo SmallInt(double) được chọn.

Không phải lúc nào cũng có thể quyết định trình tự nào là tốt nhất. Có thể xảy ra rằng tất cả chúng đều tốt như nhau, trong trường hợp đó chúng ta nói rằng phép biến đổi là không rõ ràng. Trong trường hợp này, trình biên dịch không áp dụng bất kỳ phép biến đổi tiềm ẩn nào. Ví dụ: nếu lớp Number có hai bộ chuyển đổi:

Số lớp ( public: operator float(); operator int(); // ... );

thì không thể chuyển đổi ngầm một đối tượng thuộc kiểu Number thành kiểu long. Hướng dẫn sau đây gây ra lỗi biên dịch vì việc lựa chọn chuỗi chuyển đổi do người dùng xác định không rõ ràng:

// lỗi: bạn có thể sử dụng cả float() và int() long lval = num;

Để chuyển đổi num thành giá trị kiểu long, có thể áp dụng hai chuỗi sau:

  1. toán tử float() -> chuyển đổi tiêu chuẩn
  2. toán tử int() -> chuyển đổi tiêu chuẩn

Vì trong cả hai trường hợp, việc sử dụng bộ chuyển đổi được theo sau bởi việc sử dụng chuyển đổi tiêu chuẩn, cả hai chuỗi đều tốt như nhau và trình biên dịch không thể chọn cái này hơn cái kia.

Với sự trợ giúp của việc truyền kiểu rõ ràng, lập trình viên có thể chỉ định thay đổi mong muốn:

// đúng: truyền rõ ràng long lval = static_cast (số);

Do thông số kỹ thuật này, bộ chuyển đổi Token::operator int() được chọn, theo sau là chuyển đổi tiêu chuẩn thành dài.

Sự mơ hồ trong việc lựa chọn trình tự các phép biến đổi cũng có thể phát sinh khi hai lớp xác định các phép biến đổi lẫn nhau. Ví dụ:

Lớp SmallInt ( public: SmallInt(const Number &); // ... ); Số lớp ( public: operator SmallInt(); // ... ); tính toán void bên ngoài (SmallInt); số bên ngoài; tính toán(số); // lỗi: có thể thực hiện được hai chuyển đổi

Đối số num được chuyển đổi thành SmallInt bằng hai những cách khác: bằng cách sử dụng hàm tạo SmallInt::SmallInt(const Number&) hoặc sử dụng trình chuyển đổi Number::operator SmallInt(). Vì cả hai thay đổi đều tốt như nhau nên lệnh gọi được coi là lỗi.

Để giải quyết sự mơ hồ, lập trình viên có thể gọi rõ ràng bộ chuyển đổi của lớp Number:

// đúng: lệnh gọi rõ ràng làm phân biệt tính toán (num.operator SmallInt());

Tuy nhiên, bạn không nên sử dụng việc truyền kiểu rõ ràng để giải quyết sự mơ hồ vì cả trình chuyển đổi và hàm tạo đều được xem xét khi chọn các chuyển đổi phù hợp cho việc truyền kiểu:

Tính toán(SmallInt(num)); // lỗi: vẫn mơ hồ

Như bạn có thể thấy, sự hiện diện số lượng lớn Những bộ chuyển đổi và hàm tạo như vậy không an toàn, đúng như vậy. nên được sử dụng một cách thận trọng. Bạn có thể hạn chế việc sử dụng hàm tạo khi thực hiện chuyển đổi tiềm ẩn (và do đó giảm khả năng xảy ra các hiệu ứng không mong muốn) bằng cách khai báo chúng một cách rõ ràng.

15.10.1. Một lần nữa về việc cho phép nạp chồng hàm

Chương 9 trình bày chi tiết cách giải quyết lệnh gọi hàm quá tải. Nếu các đối số thực tế của lệnh gọi thuộc loại lớp, một con trỏ tới một loại lớp hoặc một con trỏ tới các thành viên của lớp, thì nhiều hàm hơn được coi là ứng cử viên khả thi. Do đó, sự hiện diện của các đối số như vậy sẽ ảnh hưởng đến bước đầu tiên của quy trình giải quyết tình trạng quá tải - việc lựa chọn một tập hợp các hàm ứng cử viên.

Ở bước thứ ba của quy trình này, kết quả phù hợp nhất sẽ được chọn. Trong trường hợp này, việc chuyển đổi các loại đối số thực tế thành các loại tham số hình thức của hàm sẽ được xếp hạng. Nếu các đối số và tham số thuộc loại lớp thì tập hợp các phép biến đổi có thể có sẽ bao gồm các chuỗi các phép biến đổi do người dùng xác định, đồng thời xếp hạng chúng.

Trong phần này, chúng ta xem xét chi tiết cách các đối số thực tế và tham số hình thức của một loại lớp ảnh hưởng đến việc lựa chọn các hàm ứng viên và trình tự các phép biến đổi do người dùng định nghĩa ảnh hưởng như thế nào đến việc lựa chọn hàm đứng tốt nhất.

15.10.2. Chức năng ứng viên

Hàm ứng viên là hàm có cùng tên với hàm được gọi. Giả sử có một cuộc gọi như thế này:

SmallInt si(15); thêm(si, 566);

Hàm ứng viên phải được đặt tên là add. Những khai báo add() nào được tính đến? Những thứ có thể nhìn thấy tại điểm gọi.

Ví dụ: cả hai hàm add() được khai báo trong phạm vi toàn cục sẽ là ứng cử viên cho lệnh gọi sau:

Ma trận const& add(const ma trận &, int); cộng gấp đôi (gấp đôi, gấp đôi); int main() ( SmallInt si(15); add(si, 566); // ... )

Việc xem xét các hàm có phần khai báo hiển thị tại thời điểm gọi không bị giới hạn ở các lệnh gọi có đối số kiểu lớp. Tuy nhiên, đối với họ, việc tìm kiếm quảng cáo được thực hiện ở hai lĩnh vực hiển thị khác:

  • nếu đối số thực tế là một đối tượng của một kiểu lớp, một con trỏ hoặc tham chiếu đến một kiểu lớp hoặc một con trỏ tới một thành viên lớp và kiểu đó được khai báo trong không gian tên người dùng, thì các hàm được khai báo trong cùng một không gian đó và có cùng một không gian tên đặt tên như và gọi:
không gian tên NS ( class SmallInt ( /* ... */ ); class String ( /* ... */ ); String add(const String &, const String &); ) int main() ( // si has type class SmallInt: // lớp được khai báo trong không gian tên NS NS::SmallInt si(15); add(si, 566); // NS::add() - hàm ứng viên return 0; )

Đối số si có kiểu SmallInt, tức là Loại lớp được khai báo trong không gian tên NS. Do đó, add(const String &, const String &) được khai báo trong vùng tên này sẽ được thêm vào tập hợp các hàm ứng cử viên;

  • nếu đối số thực tế là một đối tượng của một loại lớp, một con trỏ hoặc tham chiếu đến một lớp hoặc một con trỏ tới một thành viên của một lớp và lớp đó có những người bạn có cùng tên với hàm được gọi thì chúng sẽ được thêm vào tập các hàm ứng viên:
  • không gian tên NS ( class SmallInt (friend SmallInt add(SmallInt, int) ( /* ... */ ) ); ) int main() ( NS::SmallInt si(15); add(si, 566); // function -friend add() - ứng viên trả về 0; )

    Đối số của hàm si có kiểu SmallInt. Hàm bạn bè của lớp SmallInt add(SmallInt, int) là thành viên của không gian tên NS, mặc dù nó không được khai báo trực tiếp trong không gian tên NS. Tìm kiếm thông thường trong NS sẽ không tìm thấy chức năng kết bạn. Tuy nhiên, khi add() được gọi với đối số loại lớp SmallInt, những người bạn của lớp đó được khai báo trong danh sách thành viên của nó cũng được tính đến và thêm vào tập hợp các ứng cử viên.

    Do đó, nếu danh sách đối số thực tế của hàm chứa một đối tượng, một con trỏ hoặc tham chiếu đến một lớp và các con trỏ tới các thành viên của lớp thì tập hợp các hàm ứng cử viên bao gồm tập hợp các hàm hiển thị tại điểm gọi hoặc được khai báo theo cùng một cách. không gian tên nơi nó được xác định là kiểu của lớp, hoặc được khai báo là bạn của lớp này.

    Hãy xem xét ví dụ sau:

    Không gian tên NS ( class SmallInt (friend SmallInt add(SmallInt, int) ( /* ... */ ) ); class String ( /* ... */ ); String add(const String &, const String &); ) ma trận const& add(const ma trận &, int); cộng gấp đôi (gấp đôi, gấp đôi); int main() ( // si thuộc loại class SmallInt: // lớp được khai báo trong không gian tên NS NS::SmallInt si(15); add(si, 566); // hàm bạn bè được gọi là return 0; )

    Ở đây các ứng cử viên là:

    • chức năng toàn cầu:
    ma trận const& add(const ma trận &, int) double add(double, double)
  • chức năng từ không gian tên:
  • NS::add(const String &, const String &)
  • chức năng kết bạn:
  • NS::add(SmallInt, int)

    Khi tình trạng quá tải được giải quyết, hàm bạn bè của lớp SmallInt NS::add(SmallInt, int) được chọn là hàm tốt nhất: cả hai đối số thực tế đều khớp chính xác với các tham số hình thức đã cho.

    Tất nhiên, hàm được gọi có thể có một số đối số của một kiểu lớp, một con trỏ hoặc tham chiếu đến một lớp hoặc một con trỏ tới một thành viên của lớp. Các loại lớp khác nhau được phép cho mỗi đối số này. Việc tìm kiếm các hàm ứng viên cho chúng được thực hiện trong không gian tên nơi lớp được định nghĩa và trong số các hàm bạn bè của lớp. Do đó, tập hợp kết quả các ứng cử viên để gọi một hàm với các đối số như vậy chứa các hàm từ không gian khác nhau tên và hàm bạn bè được khai báo trong các lớp khác nhau.

    15.10.3. Các hàm ứng viên để gọi một hàm trong phạm vi lớp

    Khi gọi một hàm như

    xảy ra trong phạm vi của một lớp (ví dụ: bên trong hàm thành viên), thì phần đầu tiên của tập ứng cử viên được mô tả trong tiểu mục trước (nghĩa là tập hợp bao gồm các khai báo hàm hiển thị tại điểm gọi) có thể chứa không chỉ là các hàm thành viên của lớp. Để xây dựng một tập hợp như vậy, độ phân giải tên được sử dụng. (Chủ đề này đã được thảo luận chi tiết trong phần 13.9 - 13.12.)

    Hãy xem một ví dụ:

    Không gian tên NS ( struct myClass ( void k(int); static void k(char*); void mf(); ); int k(double); ); void h(char); void NS::myClass::mf() ( h("a"); // toàn cầu h(char) k(4) được gọi; // myClass::k(int) được gọi)

    Như đã lưu ý trong phần 13.11, vòng loại NS::myClass:: được tra cứu trong thứ tự ngược lại: Khai báo hiển thị cho tên được sử dụng trong định nghĩa hàm thành viên mf() trước tiên được tra cứu trong lớp myClass và sau đó trong không gian tên NS. Hãy nhìn vào cuộc gọi đầu tiên:

    Khi phân giải tên h() trong định nghĩa hàm thành viên mf(), trước tiên các hàm thành viên myClass sẽ được tra cứu. Vì không có hàm thành viên nào có cùng tên trong phạm vi của lớp này, nên tìm kiếm đang được tiến hành trong không gian tên NS. Hàm h() cũng không có ở đó nên chúng ta chuyển sang phạm vi toàn cục. Kết quả là hàm toàn cục h(char), hàm ứng cử viên duy nhất hiển thị tại thời điểm gọi.

    Ngay khi tìm thấy quảng cáo phù hợp, việc tìm kiếm sẽ dừng lại. Do đó, tập hợp chỉ chứa những hàm có khai báo nằm trong phạm vi phân giải tên thành công. Điều này có thể được quan sát thấy trong ví dụ về việc xây dựng một tập các ứng viên để gọi

    Đầu tiên, việc tìm kiếm được thực hiện trong phạm vi của lớp myClass. Trong trường hợp này, hai hàm thành viên k(int) và k(char*) được tìm thấy. Vì tập ứng cử viên chỉ chứa các hàm được khai báo trong phạm vi phân giải thành công nên không gian tên NS không được tìm kiếm và hàm k(double) không được bao gồm trong tập này.

    Nếu cuộc gọi được phát hiện là không rõ ràng vì không có hàm phù hợp nhất trong tập hợp, trình biên dịch sẽ đưa ra thông báo lỗi. Các ứng cử viên phù hợp hơn với các đối số thực tế sẽ không được tìm kiếm trong phạm vi kèm theo.

    15.10.4. Trình tự xếp hạng của các phép biến đổi do người dùng xác định

    Đối số hàm thực tế có thể được chuyển ngầm sang loại tham số hình thức thông qua một chuỗi các chuyển đổi do người dùng xác định. Điều này ảnh hưởng như thế nào đến việc giải quyết tắc nghẽn? Ví dụ: nếu lệnh gọi hàm calc() sau đây được thực hiện thì hàm nào sẽ được gọi?

    Lớp SmallInt ( public: SmallInt(int); ); bên ngoài void calc (gấp đôi); bên ngoài void calc(SmallInt); int ival; int main() ( calc(ival); // calc() nào được gọi? )

    Hàm có tham số hình thức phù hợp nhất với loại đối số thực tế sẽ được chọn. Đây được gọi là chức năng phù hợp nhất hoặc chức năng đứng tốt nhất. Để chọn một hàm như vậy, các phép biến đổi ngầm áp dụng cho các đối số thực tế sẽ được xếp hạng. Hàm tồn tại tốt nhất được coi là hàm mà những thay đổi được áp dụng cho các đối số không tệ hơn bất kỳ hàm còn tồn tại nào khác và đối với ít nhất một đối số, chúng tốt hơn tất cả các hàm khác.

    Một chuỗi các phép biến đổi tiêu chuẩn luôn tốt hơn một chuỗi các phép biến đổi do người dùng xác định. Vì vậy, khi gọi calc() từ ví dụ trên, cả hai hàm calc() vẫn hợp lệ. calc(double) tồn tại vì có một chuyển đổi tiêu chuẩn từ loại đối số thực tế, int, sang loại tham số chính thức, double, và calc(SmallInt) tồn tại vì có một chuyển đổi do người dùng xác định từ int sang SmallInt mà sử dụng hàm tạo SmallInt(int). Vì vậy, hàm sống sót tốt nhất là calc(double).

    Làm thế nào để so sánh hai chuỗi chuyển đổi do người dùng xác định? Nếu họ sử dụng bộ chuyển đổi khác nhau hoặc các hàm tạo khác nhau thì cả hai chuỗi như vậy đều được coi là tốt như nhau:

    Số lớp ( public: operator SmallInt(); operator int(); // ... ); bên ngoài void calc(int); bên ngoài void calc(SmallInt); số bên ngoài; calc(num); // lỗi: mơ hồ

    Cả calc(int) và calc(SmallInt) sẽ tồn tại; cái đầu tiên vì bộ chuyển đổi Number::operator int() chuyển đổi một đối số thực tế của loại Number thành một tham số hình thức thuộc loại int, và cái thứ hai vì bộ chuyển đổi Number::operator SmallInt() chuyển đổi một đối số thực tế của loại Number thành một đối số hình thức tham số kiểu SmallInt. Vì các chuỗi biến đổi do người dùng xác định luôn có cùng thứ hạng nên trình biên dịch không thể chọn cái nào tốt hơn. Vì vậy, lệnh gọi hàm này không rõ ràng và dẫn đến lỗi biên dịch.

    Có một cách để giải quyết sự mơ hồ bằng cách chỉ định rõ ràng việc chuyển đổi:

    // chỉ định rõ ràng dàn diễn viên sẽ phân biệt calc(static_cast< int >(số));

    Việc truyền kiểu rõ ràng buộc trình biên dịch chuyển đổi đối số num thành kiểu int bằng cách sử dụng bộ chuyển đổi Number::operator int(). Đối số thực tế khi đó sẽ có kiểu int, khớp chính xác với hàm calc(int), được chọn là hàm tốt nhất.

    Giả sử bộ chuyển đổi Number::operator int() không được xác định trong lớp Number. Khi đó sẽ có thử thách phải không?

    // chỉ Số::toán tử SmallInt() calc(num); // vẫn còn mơ hồ?

    vẫn còn mơ hồ? Hãy nhớ lại rằng SmallInt cũng có một trình chuyển đổi có thể chuyển đổi giá trị loại SmallInt thành int.

    Lớp SmallInt ( public: operator int(); // ... );

    Chúng ta có thể giả định rằng hàm calc() được gọi bằng cách trước tiên chuyển đổi đối số thực tế num từ kiểu Number sang kiểu SmallInt bằng cách sử dụng bộ chuyển đổi Number::operator SmallInt(), sau đó truyền kết quả sang kiểu int bằng cách sử dụng SmallInt::operator SmallInt( ). Tuy nhiên, nó không phải vậy. Hãy nhớ lại rằng một chuỗi các phép biến đổi do người dùng xác định có thể bao gồm một số phép biến đổi tiêu chuẩn, nhưng chỉ có một phép biến đổi tùy chỉnh. Nếu bộ chuyển đổi Number::operator int() không được xác định thì hàm calc(int) không được coi là tồn tại vì không có sự chuyển đổi ngầm từ kiểu đối số thực tế num sang kiểu của tham số hình thức int.

    Do đó, trong trường hợp không có bộ chuyển đổi Number::operator int(), hàm duy nhất còn sót lại là calc(SmallInt), có lợi cho việc giải quyết cuộc gọi.

    Nếu hai chuỗi biến đổi do người dùng xác định sử dụng cùng một bộ chuyển đổi thì việc lựa chọn chuỗi biến đổi tốt nhất phụ thuộc vào chuỗi các biến đổi tiêu chuẩn được thực hiện sau lệnh gọi của nó:

    Lớp SmallInt ( public: operator int(); // ... ); thao tác void(int); thao tác void(char); SmallInt si(68); main() ( manip(si); // gọi manip(int) )

    Cả manip(int) và manip(char) đều là các hàm được thiết lập tốt; cái đầu tiên vì bộ chuyển đổi SmallInt::operator int() chuyển đổi đối số thực tế của loại SmallInt thành loại của tham số hình thức int, và cái thứ hai vì bộ chuyển đổi tương tự chuyển đổi SmallInt thành int, sau đó kết quả được chuyển thành char bằng cách sử dụng hàm chuyển đổi tiêu chuẩn. Trình tự các phép biến đổi do người dùng xác định trông như thế này:

    Manip(int) : toán tử int()->so khớp chính xác manip(int) : toán tử int()->chuyển đổi tiêu chuẩn

    Vì cả hai chuỗi đều sử dụng cùng một bộ chuyển đổi nên thứ hạng của chuỗi chuyển đổi tiêu chuẩn sẽ được phân tích để xác định chuỗi nào tốt hơn. Vì đối sánh chính xác tốt hơn so với chuyển đổi nên hàm tồn tại tốt nhất là manip(int).

    Chúng tôi nhấn mạnh rằng tiêu chí lựa chọn như vậy chỉ được chấp nhận khi sử dụng cùng một bộ chuyển đổi trong cả hai chuỗi chuyển đổi do người dùng xác định. Điều này khác với ví dụ ở cuối Phần 15.9, trong đó chúng tôi đã chỉ ra cách trình biên dịch chọn chuyển đổi do người dùng xác định của một số giá trị thành một loại đích nhất định: loại nguồn và đích đã được cố định và trình biên dịch phải chọn giữa nhiều người dùng khác nhau. -chuyển đổi được xác định từ loại này sang loại khác. Ở đây chúng ta xem xét hai chức năng khác nhau với các loại tham số hình thức khác nhau và các loại mục tiêu cũng khác nhau. Nếu vì hai các loại khác nhau các tham số yêu cầu các chuyển đổi khác nhau do người dùng xác định, chỉ có thể chọn loại này thay vì loại khác nếu sử dụng cùng một bộ chuyển đổi trong cả hai chuỗi. Nếu không đúng như vậy thì các chuyển đổi tiêu chuẩn sau khi áp dụng bộ chuyển đổi sẽ được đánh giá để chọn loại mục tiêu tốt nhất. Ví dụ:

    Lớp SmallInt ( public: operator int(); operator float(); // ... ); tính toán void(float); tính toán void(char); SmallInt si(68); main() ( tính toán(si); // mơ hồ )

    Cả hai hàm tính toán (float) và tính toán (int) đều là các hàm được thiết lập tốt. tính toán (float) - vì trình chuyển đổi SmallInt::operator float() chuyển đổi một đối số loại SmallInt thành kiểu tham số float và tính toán (char) - vì SmallInt::operator int() chuyển đổi một đối số loại SmallInt thành int loại, sau đó kết quả được chuyển thành loại char theo tiêu chuẩn. Như vậy có các trình tự:

    Tính toán(float) : toán tử float()->so khớp chính xác tính toán(char) : toán tử char()->chuyển đổi tiêu chuẩn

    Vì họ sử dụng các bộ chuyển đổi khác nhau nên không thể xác định hàm nào có tham số hình thức phù hợp hơn với lệnh gọi. Để chọn cái tốt hơn trong hai cái, thứ hạng của chuỗi các phép biến đổi tiêu chuẩn không được sử dụng. Cuộc gọi được trình biên dịch đánh dấu là không rõ ràng.

    Bài tập 15.12

    Các lớp Thư viện chuẩn C++ không có định nghĩa trình chuyển đổi và hầu hết các hàm tạo lấy một tham số duy nhất đều được khai báo rõ ràng. Tuy nhiên, nhiều toán tử quá tải được xác định. Tại sao bạn nghĩ rằng quyết định này được đưa ra trong quá trình thiết kế?

    Bài tập 15.13

    Tại sao toán tử đầu vào bị quá tải cho lớp SmallInt được xác định ở đầu phần này không được triển khai như sau:

    Istream& operator>>(istream &is, SmallInt &si) ( return (is >> is.value); )

    Bài tập 15.14

    Đưa ra các chuỗi biến đổi do người dùng xác định có thể có cho các lần khởi tạo sau. Kết quả của mỗi lần khởi tạo sẽ là gì?

    Lớp LongDouble ( toán tử double(); toán tử float(); ); bên ngoài LongDouble ldObj; (a) int ex1 = ldObj; (b) float ex2 = ldObj;

    Bài tập 15.15

    Kể tên ba tập hợp hàm ứng cử viên được xem xét khi giải quyết tình trạng quá tải hàm khi có ít nhất một trong các đối số của nó thuộc loại lớp.

    Bài tập 15.16

    Cái nào hàm tính toán() được chọn là người sống sót tốt nhất trong trường hợp này? Hiển thị trình tự các phép biến đổi cần thiết để gọi từng hàm và giải thích tại sao hàm này tốt hơn hàm kia.

    Lớp LongDouble ( public: LongDouble(double); // ... ); bên ngoài void calc(int); khoảng trống bên ngoài calc(LongDouble); dval đôi; int main() ( calc(dval); // hàm nào? )

    15.11. Độ phân giải quá tải và các hàm thành viên A

    Các hàm thành viên cũng có thể bị quá tải, trong trường hợp đó thủ tục giải quyết tình trạng quá tải cũng được sử dụng để chọn ra hàm tốt nhất còn tồn tại. Độ phân giải này rất giống với quy trình tương tự dành cho các hàm thông thường và bao gồm ba bước giống nhau:

    1. Lựa chọn các chức năng ứng cử viên.
    2. Lựa chọn các chức năng đã được thiết lập.

    Tuy nhiên, có những khác biệt nhỏ trong thuật toán sinh tập ứng cử viên và lựa chọn các hàm thành viên còn sót lại. Chúng ta sẽ xem xét những khác biệt này trong phần này.

    15.11.1. Khai báo các hàm thành viên bị quá tải

    Các hàm thành viên của lớp có thể bị quá tải:

    Lớp myClass ( public: void f(double); char f(char, char); // quá tải myClass::f(double) // ... );

    Giống như các hàm được khai báo trong một không gian tên, các hàm thành viên có thể có cùng tên với điều kiện là danh sách tham số của chúng khác nhau về số lượng tham số hoặc kiểu của chúng. Nếu khai báo của hai hàm thành viên chỉ khác nhau về kiểu trả về thì khai báo thứ hai được coi là lỗi biên dịch:

    Lớp myClass ( public: void mf(); double mf(); // lỗi: cái này không thể bị quá tải // ... );

    Không giống như các hàm trong không gian tên, các hàm thành viên chỉ được khai báo một lần. Ngay cả khi kiểu trả về và danh sách tham số của hai hàm thành viên giống nhau, trình biên dịch sẽ hiểu khai báo thứ hai là khai báo lặp lại không chính xác:

    Lớp myClass ( public: void mf(); void mf(); // lỗi: khai báo lặp lại // ... );

    Tất cả các hàm từ tập hợp quá tải phải được khai báo trong cùng một phạm vi. Vì vậy, các hàm thành viên không bao giờ làm quá tải các hàm được khai báo trong không gian tên. Ngoài ra, vì mỗi lớp có phạm vi riêng nên các hàm là thành viên của các lớp khác nhau sẽ không làm quá tải lẫn nhau.

    Tập hợp các hàm thành viên bị quá tải có thể chứa cả hàm tĩnh và hàm không tĩnh:

    Lớp myClass ( public: void mcf(double); static void mcf(int*); // quá tải myClass::mcf(double) // ... );

    Việc hàm thành viên tĩnh hay không tĩnh được gọi tùy thuộc vào kết quả của quá trình giải quyết tình trạng quá tải. Quá trình giải quyết trong trường hợp cả thành viên tĩnh và không tĩnh đều tồn tại sẽ được thảo luận chi tiết trong phần tiếp theo.

    15.11.2. Chức năng ứng viên

    Chúng ta hãy xem xét hai loại lệnh gọi hàm thành viên:

    Mc.mf(arg); pmc->mf(arg);

    trong đó mc là biểu thức của kiểu myClass và pmc là biểu thức của kiểu "con trỏ để gõ myClass". Tập ứng cử viên cho cả hai lệnh gọi bao gồm các hàm được tìm thấy trong phạm vi của lớp myClass khi tìm kiếm khai báo mf().

    Tương tự để gọi một hàm có dạng

    MyClass::mf(arg);

    tập ứng cử viên cũng bao gồm các hàm được tìm thấy trong phạm vi của lớp myClass khi tìm kiếm khai báo mf(). Ví dụ:

    Lớp myClass ( public: void mf(double); void mf(char, char = "\n"); static void mf(int*); // ... ); int main() ( myClass mc; int iobj; mc.mf(iobj); )

    Các ứng cử viên để gọi hàm trong main() đều là ba hàm thành viên mf() được khai báo trong myClass:

    Vô hiệu mf(gấp đôi); void mf(char, char = "\n"); khoảng trống tĩnh mf(int*);

    Nếu không có hàm thành viên nào có tên mf() được khai báo trong myClass thì tập ứng cử viên sẽ trống. (Trên thực tế, các hàm từ các lớp cơ sở cũng sẽ được xem xét. Chúng ta sẽ nói về cách chúng đi vào tập hợp này trong Phần 19.3.) Nếu không có ứng cử viên nào cho lệnh gọi hàm, trình biên dịch sẽ đưa ra thông báo lỗi.

    15.11.3. Tính năng kế thừa

    Hàm sống sót là hàm từ một tập hợp các ứng cử viên có thể được gọi với các đối số thực tế đã cho. Để nó tồn tại, phải có sự chuyển đổi ngầm giữa các loại đối số thực tế và các tham số hình thức. Ví dụ: class myClass ( public: void mf(double); void mf(char, char = "\n"); static void mf(int*); // ... ); int main() ( myClass mc; int iobj; mc.mf(iobj); // hàm thành viên mf() là gì? Không rõ ràng)

    Trong đoạn mã này để gọi mf() từ main() có hai hàm thường trực:

    Vô hiệu mf(gấp đôi); void mf(char, char = "\n");

    • mf(double) tồn tại vì nó chỉ có một tham số và có sự chuyển đổi tiêu chuẩn của đối số iobj kiểu int thành tham số kiểu double;
    • mf(char,char) tồn tại vì có một giá trị mặc định cho tham số thứ hai và có một chuyển đổi tiêu chuẩn từ đối số int iobj sang kiểu char của tham số chính thức đầu tiên.

    Khi chọn trạng thái tốt nhất, các hàm chuyển đổi loại áp dụng cho từng đối số thực tế sẽ được xếp hạng. Hàm tốt nhất được coi là hàm mà tất cả các phép biến đổi được sử dụng không tệ hơn bất kỳ hàm hiện có nào khác và đối với ít nhất một đối số, phép biến đổi như vậy tốt hơn tất cả các hàm khác.

    Trong ví dụ trước, mỗi hàm trong số hai hàm kế thừa đã sử dụng một chuyển đổi tiêu chuẩn để chuyển loại đối số thực tế thành loại tham số hình thức. Cuộc gọi được coi là không rõ ràng vì cả hai hàm thành viên đều giải quyết nó tốt như nhau.

    Bất kể loại lệnh gọi hàm nào, cả thành viên tĩnh và không tĩnh đều có thể được bao gồm trong tập còn tồn tại:

    Lớp myClass ( public: static void mf(int); char mf(char); ); int main() ( char cobj; myClass::mf(cobj); // chính xác hàm thành viên nào? )

    Ở đây, hàm thành viên mf() được gọi với tên lớp và toán tử phân giải phạm vi myClass::mf(). Tuy nhiên, cả đối tượng (với toán tử dấu chấm) lẫn con trỏ tới đối tượng (với toán tử mũi tên) đều không được chỉ định. Mặc dù vậy, hàm thành viên không tĩnh mf(char) vẫn được bao gồm trong tập còn lại cùng với thành viên tĩnh mf(int).

    Quá trình giải quyết quá tải sau đó tiếp tục bằng cách xếp hạng các chuyển đổi loại được áp dụng cho các đối số thực tế để chọn hàm tồn tại tốt nhất. Đối số cobj của kiểu char tương ứng chính xác với tham số hình thức mf(char) và có thể được mở rộng thành kiểu tham số hình thức mf(int). Vì thứ hạng của kết quả khớp chính xác cao hơn nên hàm mf(char) được chọn.

    Tuy nhiên, hàm thành viên này không tĩnh và do đó chỉ có thể được gọi thông qua một đối tượng hoặc một con trỏ tới một đối tượng của lớp myClass bằng cách sử dụng một trong các toán tử truy cập. Trong tình huống như vậy, nếu đối tượng không được chỉ định và do đó, việc gọi hàm là không thể (chính xác là trường hợp của chúng tôi), trình biên dịch sẽ coi đó là một lỗi.

    Một tính năng khác của các hàm thành viên phải được tính đến khi tạo một tập hợp các hàm thường trực là sự hiện diện của các chỉ định const hoặc dễ bay hơi trên các thành viên không tĩnh. (Những điều này sẽ được thảo luận trong Phần 13.3.) Chúng ảnh hưởng như thế nào đến quá trình giải quyết tắc nghẽn? Cho lớp myClass có các hàm thành viên sau:

    Lớp myClass ( public: static void mf(int*); void mf(double); void mf(int) const; // ... );

    Sau đó, cả hàm thành viên tĩnh mf(int*), hàm const mf(int) và hàm không phải const mf(double) đều được đưa vào tập hợp các ứng cử viên cho lệnh gọi được hiển thị bên dưới. Nhưng ai trong số họ sẽ được bao gồm trong số nhiều người sống sót?

    Int main() ( const myClass mc; double dobj; mc.mf(dobj); // hàm thành viên nào là mf()? )

    Xem xét các phép biến đổi cần áp dụng cho các đối số thực tế, chúng tôi thấy rằng các hàm mf(double) và mf(int) vẫn tồn tại. Kiểu double của đối số thực tế dobj tương ứng chính xác với kiểu của tham số hình thức mf(double) và có thể được chuyển thành kiểu của tham số mf(int) bằng cách sử dụng chuyển đổi tiêu chuẩn.

    Nếu bạn sử dụng toán tử truy cập dấu chấm hoặc mũi tên khi gọi một hàm thành viên, thì loại đối tượng hoặc con trỏ mà hàm được gọi sẽ được tính đến khi chọn các hàm vào tập hợp còn lại.

    mc là một đối tượng const mà chỉ có thể gọi các hàm thành viên const không tĩnh. Do đó, hàm thành viên không cố định mf(double) bị loại khỏi tập hợp các hàm còn sót lại và hàm duy nhất mf(int) vẫn còn trong đó, hàm này được gọi.

    Điều gì sẽ xảy ra nếu đối tượng const được sử dụng để gọi hàm thành viên tĩnh? Xét cho cùng, đối với một hàm như vậy, bạn không thể chỉ định một hàm xác định const hoặc biến động, vậy nó có thể được gọi thông qua một đối tượng không đổi không?

    Lớp myClass ( public: static void mf(int); char mf(char); ); int main() ( const myClass mc; int iobj; mc.mf(iobj); // có thể gọi hàm thành viên tĩnh không? )

    Các hàm thành viên tĩnh là chung cho tất cả các đối tượng của cùng một lớp. Họ chỉ có thể truy cập trực tiếp vào các thành viên lớp tĩnh. Do đó, các thành viên không tĩnh của đối tượng cố định mc không có sẵn cho mf(int) tĩnh. Vì lý do này, được phép gọi hàm thành viên tĩnh trên đối tượng const bằng cách sử dụng toán tử dấu chấm hoặc mũi tên.

    Do đó, các hàm thành viên tĩnh không bị loại trừ khỏi tập hợp các hàm còn tồn tại ngay cả khi có các chỉ định const hoặc stable trên đối tượng mà chúng được gọi. Các hàm thành viên tĩnh được coi là tương ứng với bất kỳ đối tượng hoặc con trỏ nào tới một đối tượng của lớp chúng.

    Trong ví dụ trên, mc là một đối tượng const, do đó hàm thành viên mf(char) bị loại khỏi tập hợp đó. Nhưng hàm thành viên mf(int) vẫn còn trong đó vì nó là tĩnh. Vì đây là chức năng duy nhất còn sót lại nên nó trở thành chức năng tốt nhất.

    15.12. Độ phân giải quá tải và câu lệnh A

    Các toán tử và bộ chuyển đổi quá tải có thể được khai báo trong các lớp. Giả sử trong quá trình khởi tạo gặp phải một toán tử cộng:

    SomeClass sc; int iobj = sc + 3;

    Làm thế nào trình biên dịch quyết định nên gọi toán tử quá tải trên SomeClass hay chuyển đổi toán hạng sc thành kiểu có sẵn rồi sử dụng toán tử có sẵn?

    Câu trả lời phụ thuộc vào nhiều toán tử và bộ chuyển đổi quá tải được định nghĩa trong SomeClass. Khi bạn chọn một toán tử để thực hiện phép cộng, quy trình phân giải quá tải hàm sẽ được áp dụng. Trong phần này chúng tôi sẽ giải thích cách quá trình này cho phép bạn chọn toán tử mong muốn khi toán hạng là đối tượng của một loại lớp.

    Giải quyết tình trạng quá tải tuân theo quy trình ba bước tương tự được trình bày trong Phần 9.2:

    • Lựa chọn các chức năng ứng cử viên.
    • Lựa chọn các chức năng đã được thiết lập.
    • Lựa chọn chức năng hiện có tốt nhất.
    • Hãy xem xét các bước này chi tiết hơn.

      Độ phân giải nạp chồng hàm không được áp dụng nếu tất cả toán hạng đều thuộc loại có sẵn. Trong trường hợp này, toán tử tích hợp được đảm bảo sẽ được sử dụng. (Việc sử dụng các toán tử có sẵn các toán hạng kiểu được mô tả trong Chương 4.) Ví dụ:

    lớp SmallInt ( public: SmallInt(int); ); Toán tử SmallInt+ (const SmallInt &, const SmallInt &); void func() ( int i1, i2; int i3 = i1 + i2; )

    Vì toán hạng i1 và i2 thuộc kiểu int chứ không phải kiểu lớp nên phép cộng sử dụng toán tử + có sẵn. Sự quá tải của operator+(const SmallInt &, const SmallInt &) bị bỏ qua, mặc dù các toán hạng có thể được chuyển sang SmallInt bằng cách sử dụng chuyển đổi do người dùng xác định dưới dạng hàm tạo SmallInt(int). Quá trình giải quyết tắc nghẽn được mô tả dưới đây không áp dụng trong những tình huống này.

    Ngoài ra, độ phân giải quá tải toán tử chỉ được sử dụng khi sử dụng cú pháp toán tử:

    Void func() ( SmallInt si(98); int iobj = 65; int res = si + iobj; // sử dụng cú pháp toán tử)

    Nếu bạn sử dụng cú pháp gọi hàm thay thế: int res = operator+(si, iobj); // cú pháp gọi hàm

    thì thủ tục giải quyết tình trạng quá tải cho các hàm trong không gian tên sẽ được áp dụng (xem Phần 15.10). Nếu cú ​​pháp gọi hàm thành viên được sử dụng:

    // cú pháp gọi hàm thành viên int res = si.operator+(iobj);

    thì thủ tục tương ứng cho các hàm thành viên sẽ hoạt động (xem Phần 15.11).

    15.12.1. Các hàm toán tử ứng viên

    Hàm toán tử được coi là ứng cử viên nếu nó có cùng tên với tên được gọi. Khi sử dụng toán tử cộng sau

    SmallInt si(98); int iobj = 65; int res = si + iobj;

    Hàm toán tử ứng cử viên là operator+. Những khai báo toán tử+ nào được tính đến?

    Có khả năng, nếu cú ​​pháp toán tử được sử dụng với các toán hạng thuộc loại lớp thì năm bộ ứng cử viên sẽ được xây dựng. Ba cách đầu tiên giống như khi gọi các hàm thông thường với các đối số kiểu lớp:

    • nhiều toán tử có thể nhìn thấy tại điểm gọi. Các khai báo hàm của operator+() hiển thị tại thời điểm sử dụng toán tử là ứng cử viên. Ví dụ: operator+() được khai báo trong phạm vi toàn cục là một ứng cử viên khi sử dụng operator+() bên trong main():
    Toán tử SmallInt+ (const SmallInt &, const SmallInt &); int main() ( SmallInt si(98); int iobj = 65; int res = si + iobj; // ::operator+() - hàm ứng viên )
  • một tập hợp các toán tử được khai báo trong một không gian tên trong đó loại toán hạng được xác định. Nếu toán hạng thuộc loại lớp và loại đó được khai báo trong một không gian tên người dùng thì các hàm toán tử được khai báo trong cùng một không gian tên và có cùng tên với toán tử được sử dụng sẽ được coi là ứng cử viên:
  • không gian tên NS ( class SmallInt ( /* ... */ ); SmallInt operator+ (const SmallInt&, double); ) int main() ( // si có kiểu SmallInt: // lớp này được khai báo trong không gian tên NS NS: :SmallInt si(15); // NS::operator+() - hàm ứng viên int res = si + 566; return 0; )

    Toán hạng si có kiểu SmallInt, được khai báo trong không gian tên NS. Do đó, phần quá tải của toán tử+(const SmallInt, double) được khai báo trong cùng một khoảng trắng sẽ được thêm vào tập ứng cử viên;

  • một tập hợp các toán tử được khai báo là bạn của các lớp mà toán hạng thuộc về. Nếu toán hạng thuộc về một loại lớp và định nghĩa của lớp này chứa các hàm bạn bè có cùng tên với toán tử được sử dụng thì chúng sẽ được thêm vào tập hợp các ứng cử viên:
  • không gian tên NS ( class SmallInt (friend SmallInt operator+(const SmallInt&, int) ( /* ... */ ) ); ) int main() ( NS::SmallInt si(15); // hàm bạn bè operator+() - ứng cử viên int res = si + 566; trả về 0; )

    Toán hạng si có kiểu SmallInt. Hàm toán tử operator+(const SmallInt&, int), là bạn của lớp này, là thành viên của không gian tên NS, mặc dù nó không được khai báo trực tiếp trong không gian đó. Tìm kiếm NS thông thường sẽ không tìm thấy hàm toán tử này. Tuy nhiên, khi bạn sử dụng operator+() với đối số kiểu SmallInt, các hàm bạn bè được khai báo trong phạm vi của lớp đó sẽ được đưa vào xem xét và thêm vào tập ứng cử viên. Ba tập hợp hàm toán tử ứng cử viên này được hình thành theo cách giống hệt như cách gọi đến các hàm thông thường với các đối số kiểu lớp. Tuy nhiên, khi sử dụng cú pháp toán tử, hai bộ nữa sẽ được tạo:

    • một tập hợp các toán tử thành viên được khai báo ở lớp toán hạng bên trái. Nếu toán hạng như vậy của operator+() thuộc loại lớp, thì các khai báo operator+() là thành viên của lớp đó sẽ được đưa vào tập hợp các hàm ứng cử viên:
    lớp myFloat ( myFloat(double); ); lớp SmallInt ( public: SmallInt(int); Toán tử SmallInt+ (const myFloat &); ); int main() ( SmallInt si(15); int res = si + 5.66; // toán tử thành viên operator+() là một ứng cử viên )

    Toán tử thành viên SmallInt::operator+(const myFloat &), được xác định trong SmallInt, được bao gồm trong tập hợp các hàm ứng viên để cho phép gọi tới operator+() trong main();

  • nhiều toán tử tích hợp. Xem xét các loại có thể được sử dụng với toán tử+() tích hợp, các ứng cử viên cũng là:
  • toán tử int+(int, int); toán tử kép+(double, double); Toán tử T*+(T*, I); Toán tử T*+(I, T*);

    Khai báo đầu tiên dành cho toán tử tích hợp để cộng hai giá trị của kiểu số nguyên, khai báo thứ hai dành cho toán tử cộng giá trị của kiểu dấu phẩy động. Thứ ba và thứ tư tương ứng với toán tử cộng tích hợp của các loại con trỏ, được sử dụng để thêm một số nguyên vào một con trỏ. Hai khai báo cuối cùng được trình bày dưới dạng ký hiệu và mô tả toàn bộ nhóm toán tử cài sẵn mà trình biên dịch có thể chọn làm ứng cử viên để xử lý các phép toán bổ sung.

    Bất kỳ bộ nào trong bốn bộ đầu tiên đều có thể trống. Ví dụ: nếu không có hàm có tên operator+() trong số các thành viên của lớp SmallInt thì tập thứ tư sẽ trống.

    Toàn bộ tập hợp các hàm toán tử ứng viên là tập hợp của năm tập con được mô tả ở trên:

    Không gian tên NS ( class myFloat ( myFloat(double); ); class SmallInt (friend SmallInt operator+(const SmallInt &, int) ( /* ... */ ) public: SmallInt(int); operator int(); SmallInt operator+ ( const myFloat &); // ... ); SmallInt operator+ (const SmallInt &, double); ) int main() ( // type si - class SmallInt: // Lớp này được khai báo trong không gian tên NS NS::SmallInt si(15); int res = si + 5.66; // toán tử+() nào? return 0; )

    Năm bộ này bao gồm bảy hàm toán tử ứng viên cho vai trò của toán tử+() trong hàm main():

      tập đầu tiên trống. Trong phạm vi toàn cục, cụ thể là khi toán tử+() được sử dụng trong hàm main(), không có khai báo nào về toán tử operator+() bị quá tải;
    • tập thứ hai chứa các toán tử được khai báo trong vùng tên NS, nơi định nghĩa lớp SmallInt. Có một toán tử trong không gian này: NS::SmallInt NS::operator+(const SmallInt &, double);
    • tập thứ ba chứa các toán tử được khai báo là bạn của lớp SmallInt. Điều này bao gồm NS::SmallInt NS::operator+(const SmallInt &, int);
    • tập thứ tư chứa các toán tử được khai báo là thành viên của SmallInt. Có cái này nữa: NS::SmallInt NS::SmallInt::operator+(const myFloat &);
    • bộ thứ năm chứa các toán tử nhị phân tích hợp:
    toán tử int+(int, int); toán tử kép+(double, double); Toán tử T*+(T*, I); Toán tử T*+(I, T*);

    Có, việc tạo nhiều ứng cử viên để giải quyết một toán tử được sử dụng bằng cú pháp toán tử thật tẻ nhạt. Nhưng sau khi nó được xây dựng, các hàm ổn định và hàm tốt nhất được tìm thấy, như trước đây, bằng cách phân tích các phép biến đổi được áp dụng cho toán hạng của các ứng cử viên đã chọn.

    15.12.2. Tính năng kế thừa

    Tập hợp các hàm toán tử đã thiết lập được hình thành từ tập hợp các ứng cử viên bằng cách chỉ chọn những toán tử có thể được gọi với các toán hạng đã cho. Ví dụ, ai trong số bảy ứng cử viên được tìm thấy ở trên sẽ sống sót? Toán tử được sử dụng trong ngữ cảnh sau:

    NS::SmallInt si(15); si + 5,66;

    Toán hạng bên trái có kiểu SmallInt và toán hạng bên phải là double.

    Ứng cử viên đầu tiên là một chức vụ thường trực cho sử dụng nhất định toán tử+():

    Toán hạng bên trái của loại SmallInt làm bộ khởi tạo tương ứng chính xác với tham số tham chiếu chính thức của quá tải toán tử này. Cái bên phải, thuộc loại double, cũng khớp chính xác với tham số hình thức thứ hai.

    Hàm ứng cử viên sau đây cũng sẽ tồn tại:

    NS::SmallInt NS::operator+(const SmallInt &, int);

    Toán hạng bên trái si của loại SmallInt làm bộ khởi tạo tương ứng chính xác với tham số tham chiếu hình thức của toán tử quá tải. Cái bên phải có kiểu int và có thể được chuyển thành kiểu của tham số hình thức thứ hai bằng cách sử dụng chuyển đổi tiêu chuẩn.

    Hàm ứng cử viên thứ ba cũng sẽ giữ nguyên:

    NS::SmallInt NS::SmallInt::operator+(const myFloat &);

    Toán hạng bên trái si có kiểu SmallInt, tức là loại lớp mà toán tử nạp chồng là thành viên. Cái bên phải có kiểu int và được chuyển sang lớp myFloat bằng cách sử dụng chuyển đổi do người dùng xác định dưới dạng hàm tạo myFloat(double).

    Các hàm được thiết lập thứ tư và thứ năm là các toán tử tích hợp:

    Toán tử int+(int, int); toán tử kép+(double, double);

    Lớp SmallInt chứa một bộ chuyển đổi có thể truyền một giá trị kiểu SmallInt thành kiểu int. Bộ chuyển đổi này được sử dụng cùng với toán tử tích hợp đầu tiên để chuyển đổi toán hạng bên trái thành int. Toán hạng thứ hai của loại double được chuyển đổi thành loại int bằng cách sử dụng chuyển đổi tiêu chuẩn. Đối với toán tử tích hợp thứ hai, bộ chuyển đổi chuyển toán hạng bên trái từ loại SmallInt sang loại int, sau đó kết quả được chuyển đổi thành double theo tiêu chuẩn. Toán hạng thứ hai thuộc loại double tương ứng chính xác với tham số thứ hai.

    Hàm tốt nhất trong số năm hàm còn sót lại này là hàm đầu tiên, operator+(), được khai báo trong không gian tên NS:

    NS::SmallInt NS::operator+(const SmallInt &, double);

    Cả hai toán hạng của nó đều khớp chính xác với các tham số.

    15.12.3. sự mơ hồ

    Sự hiện diện của các bộ chuyển đổi thực hiện chuyển đổi ngầm thành các kiểu dựng sẵn và các toán tử quá tải trong cùng một lớp có thể dẫn đến sự mơ hồ khi lựa chọn giữa chúng. Ví dụ: có định nghĩa sau về lớp String với hàm so sánh:

    Lớp String ( // ... public: String(const char * = 0); bool operator== (const String &) const; // không có toán tử operator== (const char *) );

    và việc sử dụng toán tử operator== này:

    Hoa dây("tulip"); void foo(const char *pf) ( // toán tử quá tải String::operator==() được gọi if (flower == pf) cout<< pf <<" is a flower!\en"; // ... }

    Sau đó khi so sánh

    Hoa == pf

    toán tử đẳng thức của lớp String được gọi là:

    Để chuyển đổi toán hạng bên phải của pf từ loại const char* thành loại Chuỗi của tham số operator==(), một chuyển đổi do người dùng xác định sẽ được áp dụng, gọi hàm tạo:

    Chuỗi(const char *)

    Nếu bạn thêm một trình chuyển đổi sang loại const char* vào định nghĩa lớp String:

    Lớp String ( // ... public: String(const char * = 0); bool operator== (const String &) const; operator const char*(); // new Converter );

    thì việc sử dụng minh họa của operator==() trở nên mơ hồ:

    // kiểm tra đẳng thức không còn biên dịch nữa! nếu (hoa == pf)

    Do có thêm bộ chuyển đổi operator const char*() nên toán tử so sánh được tích hợp sẵn

    cũng được coi là một tính năng đã được thiết lập. Với sự trợ giúp của nó, toán hạng bên trái của bông hoa kiểu Chuỗi có thể được chuyển đổi thành kiểu const char *.

    Hiện tại có hai hàm toán tử đã được thiết lập để sử dụng toán tử==() trong foo(). Cái đầu tiên

    String::operator==(const String &) const;

    yêu cầu chuyển đổi do người dùng xác định của toán hạng bên phải pf từ const char* sang String. Thứ hai

    Toán tử Bool==(const char *, const char *)

    yêu cầu chuyển đổi tùy chỉnh toán hạng bên trái của hoa từ Chuỗi sang const char*.

    Do đó, hàm sống sót đầu tiên sẽ tốt hơn cho toán hạng bên trái và hàm thứ hai tốt hơn cho toán hạng bên phải. Vì không có hàm tốt nhất nên lệnh gọi được trình biên dịch đánh dấu là không rõ ràng.

    Khi thiết kế một giao diện lớp bao gồm các khai báo của các toán tử, hàm tạo và bộ chuyển đổi được nạp chồng, bạn phải hết sức cẩn thận. Chuyển đổi do người dùng xác định được trình biên dịch áp dụng ngầm. Điều này có thể khiến các toán tử tích hợp bị lỗi khi giải quyết tình trạng quá tải đối với các toán tử có toán hạng loại lớp.

    Bài tập 15.17

    Kể tên năm bộ hàm ứng cử viên được xem xét khi giải quyết việc nạp chồng toán tử bằng các toán hạng kiểu lớp.

    Bài tập 15.18

    Toán tử+() nào sẽ được chọn làm toán tử thường trực tốt nhất cho toán tử cộng trong main()? Liệt kê tất cả các hàm ứng cử viên, tất cả các hàm còn tồn tại và các chuyển đổi loại sẽ được áp dụng cho các đối số cho từng hàm còn tồn tại.

    Không gian tên NS ( lớp phức tạp ( complex(double); // ... ); lớp LongDouble (friend LongDouble operator+(LongDouble &, int) ( /* ... */ ) public: LongDouble(int); operator double() ; Toán tử LongDouble+(const phức &); // ... ); Toán tử LongDouble

    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, bạn sẽ chú ý đến các toán tử C++ bị quá tải. 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à một 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.). 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à vật thể phức tạp Thật vô nghĩa khi so sánh 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 loại tùy chỉ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.