25/05/2018, 09:52

Các mở rộng của C++ (phần 2)

Một trong các đặc tính nổi bật nhất của C++ là khả năng định nghĩa các giá trị tham số mặc định cho các hàm. Bình thường khi gọi một hàm, chúng ta cần gởi một giá trị cho mỗi tham số đã được định nghĩa trong hàm đó, chẳng hạn chúng ta có đoạn ...

Một trong các đặc tính nổi bật nhất của C++ là khả năng định nghĩa các giá trị tham số mặc định cho các hàm. Bình thường khi gọi một hàm, chúng ta cần gởi một giá trị cho mỗi tham số đã được định nghĩa trong hàm đó, chẳng hạn chúng ta có đoạn chương trình sau:

void MyDelay(long Loops); //prototype
    ...................
    void MyDelay(long Loops)
    {
    for(int I = 0; I < Loops; ++I)
    ;
    } 
    

Mỗi khi hàm MyDelay() được gọi chúng ta phải gởi cho nó một giá trị cho tham số Loops. Tuy nhiên, trong nhiều trường hợp chúng ta có thể nhận thấy rằng chúng ta luôn luôn gọi hàm MyDelay() với cùng một giá trị Loops nào đó. Muốn vậy chúng ta sẽ dùng giá trị mặc định cho tham số Loops, giả sử chúng ta muốn giá trị mặc định cho tham số Loops là 1000. Khi đó đoạn mã trên được viết lại như sau :

void MyDelay(long Loops = 1000); //prototype
    ..................
    void MyDelay(long Loops)
    {
    for(int I = 0; I < Loops; ++I)
    ;
    } 
    

Mỗi khi gọi hàm MyDelay() mà không gởi một tham số tương ứng thì trình biên dịch sẽ tự động gán cho tham số Loops giá trị 1000.

MyDelay(); // Loops có giá trị là 1000
    MyDelay(5000); // Loops có giá trị là 5000
    

Giá trị mặc định cho tham số có thể là một hằng, một hàm, một biến hay một biểu thức.

Ví dụ 2.11: Tính thể tích của hình hộp

1: #include <iostream.h>
    2: int BoxVolume(int Length = 1, int Width = 1, int Height = 1);
    3:
    4: int main()
    5: {
    6:   cout << "The tich hinh hop mac dinh: "
    7:   << BoxVolume() << endl << endl 
    8:   << "The tich hinh hop voi chieu dai=10,do rong=1,chieu cao=1:"
    9:   << BoxVolume(10) << endl << endl
    10:  << "The tich hinh hop voi chieu dai=10,do rong=5,chieu cao=1:"
    11:  << BoxVolume(10, 5) << endl << endl
    12:  << "The tich hinh hop voi chieu dai=10,do rong=5,chieu cao=2:"
    13:  << BoxVolume(10, 5, 2)<< endl;
    14:    return 0;
    15: }
    16: //Tính thể tích của hình hộp
    17: int BoxVolume(int Length, int Width, int Height)
    18: {
    19:    return Length * Width * Height;
    20: }
    

Chúng ta chạy ví dụ 2.11, kết quả ở hình 2.13

Kết quả của ví dụ 2.11 (Hình 2.13)

Chú ý:

Các tham số có giá trị mặc định chỉ được cho trong prototype của hàm và không được lặp lại trong định nghĩa hàm (Vì trình biên dịch sẽ dùng các thông tin trong prototype chứ không phải trong định nghĩa hàm để tạo một lệnh gọi).

Một hàm có thể có nhiều tham số có giá trị mặc định. Các tham số có giá trị mặc định cần phải được nhóm lại vào các tham số cuối cùng (hoặc duy nhất) của một hàm. Khi gọi hàm có nhiều tham số có giá trị mặc định, chúng ta chỉ có thể bỏ bớt các tham số theo thứ tự từ phải sang trái và phải bỏ liên tiếp nhau, chẳng hạn chúng ta có đoạn chương trình như sau:

int MyFunc(int a= 1, int b , int c = 3, int d = 4); //prototype sai!!!
    int MyFunc(int a, int b = 2 , int c = 3, int d = 4); //prototype đúng
    ...................
    MyFunc(); // Lỗi do tham số a không có giá trị mặc định
    MyFunc(1);// OK, các tham số b, c và d lấy giá trị mặc định
    MyFunc(5, 7); // OK, các tham số c và d lấy giá trị mặc định
    MyFunc(5, 7, , 8); // Lỗi do các tham số bị bỏ phải liên tiếp nhau
    

Trong C, hàm nhận tham số là con trỏ đòi hỏi chúng ta phải thận trọng khi gọi hàm. Chúng ta cần viết hàm hoán đổi giá trị giữa hai số như sau:

void Swap(int *X, int *Y); 
    {
    int Temp = *X;
    *X = *Y;
    *Y = *Temp; 
    } 
    

Để hoán đổi giá trị hai biến A B thì chúng ta gọi hàm như sau:

Swap(&A, &B);

Rõ ràng cách viết này không được thuận tiện lắm. Trong trường hợp này, C++ đưa ra một kiểu biến rất đặc biệt gọi là biến tham chiếu (reference variable). Một biến tham chiếu giống như là một bí danh của biến khác. Biến tham chiếu sẽ làm cho các hàm có thay đổi nội dung các tham số của nó được viết một cách thanh thoát hơn. Khi đó hàm Swap() được viết như sau:

void Swap(int &X, int &Y); 
    {
    int Temp = X;
    X = Y;
    Y = Temp ; 
    } 
    

Chúng ta gọi hàm như sau :

Swap(A, B);
    

Với cách gọi hàm này, C++ tự gởi địa chỉ của A B làm tham số cho hàm Swap(). Cách dùng biến tham chiếu cho tham số của C++ tương tự như các tham số được khai báo là Var trong ngôn ngữ Pascal. Tham số này được gọi là tham số kiểu tham chiếu (reference parameter). Như vậy biến tham chiếu có cú pháp như sau :

data_type & variable_name;

Trong đó:

data_type: Kiểu dữ liệu của biến.

variable_name: Tên của biến

Khi dùng biến tham chiếu cho tham số chỉ có địa chỉ của nó được gởi đi chứ không phải là toàn bộ cấu trúc hay đối tượng đó như hình 2.14, điều này rất hữu dụng khi chúng ta gởi cấu trúc và đối tượng lớn cho một hàm.

Một tham số kiểu tham chiếu nhận một tham chiếu tới một biến được chuyển cho tham số của hàm.(Hình 2.14)

Ví dụ 2.12: Chương trình hoán đổi giá trị của hai biến.

#include <iostream.h>
    //prototype
    void Swap(int &X,int &Y);
    int main()
    {
       int X = 10, Y = 5;
       cout<<"Truoc khi hoan doi: X = "<<X<<",Y = "<<Y<<endl;
       Swap(X,Y);
     cout<<"Sau khi hoan doi: X = "<<X<<",Y = "<<Y<<endl;
       return 0;
    }
    void Swap(int &X,int &Y)
    {
        int Temp=X;
        X=Y;
        Y=Temp;
    }
    

Chúng ta chạy ví dụ 2.12, kết quả ở hình 2.15

Kết quả của ví dụ 2.12 (Hình 2.15)

Đôi khi chúng ta muốn gởi một tham số nào đó bằng biến tham chiếu cho hiệu quả, mặc dù chúng ta không muốn giá trị của nó bị thay đổi thì chúng ta dùng thêm từ khóa const như sau :

int MyFunc(const int & X);

Hàm MyFunc() sẽ chấp nhận một tham số X gởi bằng tham chiếu nhưng const xác định rằng X không thể bị thay đổi.

Biến tham chiếu có thể sử dụng như một bí danh của biến khác (bí danh đơn giản như một tên khác của biến gốc), chẳng hạn như đoạn mã sau :

int Count = 1;
    int & Ref = Count;  //Tạo biến Ref như là một bí danh của biến Count
    ++Ref; //Tăng biến Count lên 1 (sử dụng bí danh của biến Count)
    

Các biến tham chiếu phải được khởi động trong phần khai báo của chúng và chúng ta không thể gán lại một bí danh của biến khác cho chúng. Chẳng hạn đoạn mã sau là sai:

int X = 1;
    int & Y; //Lỗi: Y phải được khởi động. 
    

Khi một tham chiếu được khai báo như một bí danh của biến khác, mọi thao tác thực hiện trên bí danh chính là thực hiện trên biến gốc của nó. Chúng ta có thể lấy địa chỉ của biến tham chiếu và có thể so sánh các biến tham chiếu với nhau (phải tương thích về kiểu tham chiếu).

Ví dụ 2.13: Mọi thao tác trên trên bí danh chính là thao tác trên biến gốc của nó.

#include <iostream.h>
    int main()
    {
       int X = 3;
       int &Y = X; //Y la bí danh của X
       int Z = 100;
       cout<<"X="<<X<<endl<<"Y="<<Y<<endl;
       Y *= 3;
       cout<<"X="<<X<<endl<<"Y="<<Y<<endl;
       Y = Z;
       cout<<"X="<<X<<endl<<"Y="<<Y<<endl;
       return 0;
    }
    

Chúng ta chạy ví dụ 2.13, kết quả ở hình 2.16

Hình 2.16: Kết quả của ví dụ 2.13

Ví dụ 2.14: Lấy địa chỉ của biến tham chiếu

#include <iostream.h>
    int main()
    {
       int X = 3;
       int &Y = X; //Y la bí danh của X
       cout<<"Dia chi cua X = "<<&X<<endl;
       cout<<"Dia chi cua bi danh Y= "<<&Y<<endl;
       return 0;
    }
    

Chúng ta chạy ví dụ 2.14, kết quả ở hình 2.17

Kết quả của ví dụ 2.14 (Hình 2.17)

Chúng ta có thể tạo ra biến tham chiếu với việc khởi động là một hằng, chẳng hạn như đoạn mã sau :

int & Ref = 45;

Trong trường hợp này, trình biên dịch tạo ra một biến tạm thời chứa trị hằng và biến tham chiếu chính là bí danh của biến tạm thời này. Điều này gọi là tham chiếu độc lập (independent reference).

Các hàm có thể trả về một tham chiếu, nhưng điều này rất nguy hiểm. Khi hàm trả về một tham chiếu tới một biến cục bộ của hàm thì biến này phải được khai báo là static, ngược lại tham chiếu tới nó thì khi hàm kết thúc biến cục bộ này sẽ bị bỏ qua. Chẳng hạn như đoạn chương trình sau:

int & MyFunc()
    {
    static int X = 200; //Nếu không khai báo là static thì điều này rất nguy hiểm.
    return X;
    }
    

Khi một hàm trả về một tham chiếu, chúng ta có thể gọi hàm ở phía bên trái của một phép gán.

Ví dụ 2.15:

1: #include <iostream.h>
    2: 
    3: int X = 4;
    4: //prototype
    5: int & MyFunc();
    6: 
    7: int main()
    8: {
    9:     cout<<"X="<<X<<endl;
    10:    cout<<"X="<<MyFunc()<<endl;
    11:    MyFunc() = 20; //Nghĩa là X = 20
    12:    cout<<"X="<<X<<endl;
    13:    return 0;
    14: }
    15:
    16: int & MyFunc()
    17: {
    18:    return X;
    19: }
    

Chúng ta chạy ví dụ 2.15, kết quả ở hình 2.18

Kết quả của ví dụ 2.15 (Hình 2.18)

Chú ý:

Mặc dù biến tham chiếu trông giống như là biến con trỏ nhưng chúng không thể là biến con trỏ do đó chúng không thể được dùng cấp phát động.

Chúng ta không thể khai báo một biến tham chiếu chỉ đến biến tham chiếu hoặc biến con trỏ chỉ đến biến tham chiếu. Tuy nhiên chúng ta có thể khai báo một biến tham chiếu về biến con trỏ như đoạn mã sau:

int X;
    int *P = &X;
    int * & Ref = P;
    

Với ngôn ngữ C++, chúng ta có thể đa năng hóa các hàm và các toán tử (operator). Đa năng hóa là phương pháp cung cấp nhiều hơn một định nghĩa cho tên hàm đã cho trong cùng một phạm vi. Trình biên dịch sẽ lựa chọn phiên bản thích hợp của hàm hay toán tử dựa trên các tham số mà nó được gọi.

Đa năng hóa các hàm (Functions overloading)

Trong ngôn ngữ C cũng như mọi ngôn ngữ máy tính khác, mỗi hàm đều phải có một tên phân biệt. Đôi khi đây là một điều phiền toái. Chẳng hạn như trong ngôn ngữ C, có rất nhiều hàm trả về trị tuyệt đối của một tham số là số, vì cần thiết phải có tên phân biệt nên C phải có hàm riêng cho mỗi kiểu dữ liệu số, do vậy chúng ta có tới ba hàm khác nhau để trả về trị tuyệt đối của một tham số:

int abs(int i);
    long labs(long l);
    double fabs(double d);
    

Tất cả các hàm này đều cùng thực hiện một chứa năng nên chúng ta thấy điều này nghịch lý khi phải có ba tên khác nhau. C++ giải quyết điều này bằng cách cho phép chúng ta tạo ra các hàm khác nhau có cùng một tên. Đây chính là đa năng hóa hàm. Do đó trong C++ chúng ta có thể định nghĩa lại các hàm trả về trị tuyệt đối để thay thế các hàm trên như sau :

int abs(int i);
    long abs(long l);
    double abs(double d);
    

Ví dụ 2.16:

1: #include <iostream.h>
    2: #include <math.h>
    3: 
    4: int MyAbs(int X);
    5: long MyAbs(long X);
    6: double MyAbs(double X);
    7:
    8: int main()
    9: {
    10:    int X = -7;
    11:    long Y = 200000l;
    12:    double Z = -35.678;
    13:    cout<<"Tri tuyet doi cua so nguyen (int) "<<X<<" la "
    14:        <<MyAbs(X)<<endl;
    15:    cout<<"Tri tuyet doi cua so nguyen (long int) "<<Y<<" la "
    16:        <<MyAbs(Y)<<endl;
    17:    cout<<"Tri tuyet doi cua so thuc "<<Z<<" la "
    18:        <<MyAbs(Z)<<endl;
    19:    return 0;
    20: }
    21:
    22: int MyAbs(int X)
    23: {
    24:    return abs(X);
    25: }
    26:
    27: long MyAbs(long X)
    28: {
    29:    return labs(X);
    30: }
    31:
    32: double MyAbs(double X)
    33: {
    34:    return fabs(X);
    35: }
    

Chúng ta chạy ví dụ 2.16 , kết quả ở hình 2.19

Kết quả của ví dụ 2.16 (Hình 2.19)

Trình biên dịch dựa vào sự khác nhau về số các tham số, kiểu của các tham số để có thể xác định chính xác phiên bản cài đặt nào của hàm MyAbs() thích hợp với một lệnh gọi hàm được cho, chẳng hạn như:

MyAbs(-7); //Gọi hàm int MyAbs(int) 
    MyAbs(-7l); //Gọi hàm long MyAbs(long) 
    MyAbs(-7.5); //Gọi hàm double MyAbs(double) 
    

Quá trình tìm được hàm được đa năng hóa cũng là quá trình được dùng để giải quyết các trường hợp nhập nhằng của C++. Chẳng hạn như nếu tìm thấy một phiên bản định nghĩa nào đó của một hàm được đa năng hóa mà có kiểu dữ liệu các tham số của nó trùng với kiểu các tham số đã gởi tới trong lệnh gọi hàm thì phiên bản hàm đó sẽ được gọi. Nếu không trình biên dịch C++ sẽ gọi đến phiên bản nào cho phép chuyển kiểu dễ dàng nhất.

MyAbs(‘c’); //Gọi int MyAbs(int)
    MyAbs(2.34f); //Gọi double MyAbs(double)
    

Các phép chuyển kiểu có sẵn sẽ được ưu tiên hơn các phép chuyển kiểu mà chúng ta tạo ra (chúng ta sẽ xem xét các phép chuyển kiểu tự tạo ở chương 3).

Chúng ta cũng có thể lấy địa chỉ của một hàm đã được đa năng hóa sao cho bằng một cách nào đó chúng ta có thể làm cho trình biên dịch C++ biết được chúng ta cần lấy địa chỉ của phiên bản hàm nào có trong định nghĩa. Chẳng hạn như:

int (*pf1)(int);
    long (*pf2)(long);
    int (*pf3)(double);
    pf1 = MyAbs; //Trỏ đến hàm int MyAbs(int)
    pf2 = MyAbs; //Trỏ đến hàm long MyAbs(long)
    pf3 = MyAbs; //Lỗi!!! (không có phiên bản hàm nào để đối sánh)
    

Các giới hạn của việc đa năng hóa các hàm:

 Bất kỳ hai hàm nào trong tập các hàm đã đa năng phải có các tham số khác nhau.

 Các hàm đa năng hóa với danh sách các tham số cùng kiểu chỉ dựa trên kiểu trả về của hàm thì trình biên dịch báo lỗi. Chẳng hạn như, các khai báo sau là không hợp lệ:

void Print(int X);
    int Print(int X);
    

Không có cách nào để trình biên dịch nhận biết phiên bản nào được gọi nếu giá trị trả về bị bỏ qua. Như vậy các phiên bản trong việc đa năng hóa phải có sự khác nhau ít nhất về kiểu hoặc số tham số mà chúng nhận được.

 Các khai báo bằng lệnh typedef không định nghĩa kiểu mới. Chúng chỉ thay đổi tên gọi của kiểu đã có. Chúng không ảnh hưởng tới cơ chế đa năng hóa hàm. Chúng ta hãy xem xét đoạn mã sau:

typedef char * PSTR;
    void Print(char * Mess);
    void Print(PSTR Mess);
    

Hai hàm này có cùng danh sách các tham số, do đó đoạn mã trên sẽ phát sinh lỗi.

 Đối với kiểu mảng và con trỏ được xem như đồng nhất đối với sự phân biệt khác nhau giữa các phiên bản hàm trong việc đa năng hóa hàm. Chẳng hạn như đoạn mã sau se phát sinh lỗi:

void Print(char * Mess);
    void Print(char Mess[]);
    

Tuy nhiên, đối với mảng nhiều chiều thì có sự phân biệt giữa các phiên bản hàm trong việc đa năng hóa hàm, chẳng hạn như đoạn mã sau hợp lệ:

void Print(char Mess[]);
    void Print(char Mess[][7]);
    void Print(char Mess[][9][42]);
    

. const và các con trỏ (hay các tham chiếu) có thể dùng để phân biệt, chẳng hạn như đoạn mã sau hợp lệ:

void Print(char *Mess);
    void Print(const char *Mess);
    

Đa năng hóa các toán tử (Operators overloading) :

Trong ngôn ngữ C, khi chúng ta tự tạo ra một kiểu dữ liệu mới, chúng ta thực hiện các thao tác liên quan đến kiểu dữ liệu đó thường thông qua các hàm, điều này trở nên không thoải mái.

Ví dụ 2.17: Chương trình cài đặt các phép toán cộng và trừ số phức

1: #include <stdio.h>
    2: /* Định nghĩa số phức */
    3: typedef struct
    4: {
    5:     double Real;
    6:     double Imaginary;
    7: }Complex;
    8:
    9:  Complex SetComplex(double R,double I);
    10: Complex AddComplex(Complex C1,Complex C2);
    11: Complex SubComplex(Complex C1,Complex C2);
    12: void DisplayComplex(Complex C);
    13:
    14: int main(void)
    15: {
    16:    Complex C1,C2,C3,C4;
    17:
    18:    C1 = SetComplex(1.0,2.0);
    19:    C2 = SetComplex(-3.0,4.0);
    20:    printf("
So phuc thu nhat:");
    21:    DisplayComplex(C1);
    22:    printf("
So phuc thu hai:");
    23:    DisplayComplex(C2);
    24:    C3 = AddComplex(C1,C2); //Hơi bất tiện !!! 
    25:    C4 = SubComplex(C1,C2);
    26:    printf("
Tong hai so phuc nay:");
    27:    DisplayComplex(C3);
    28:    printf("
Hieu hai so phuc nay:");
    29:    DisplayComplex(C4);
    30:    return 0;
    31: }
    32:
    33: /* Đặt giá trị cho một số phức */
    34: Complex SetComplex(double R,double I)
    35: {
    36:    Complex Tmp;
    37:
    38:    Tmp.Real = R;
    39:    Tmp.Imaginary = I;
    40:    return Tmp;
    41: }
    42: /* Cộng hai số phức */
    43: Complex AddComplex(Complex C1,Complex C2)
    44: {
    45:    Complex Tmp;
    46:
    47:    Tmp.Real = C1.Real+C2.Real;
    48:    Tmp.Imaginary = C1.Imaginary+C2.Imaginary;
    49:    return Tmp;
    50: }
    51:
    52: /* Trừ hai số phức */
    53: Complex SubComplex(Complex C1,Complex C2)
    54: {
    55:    Complex Tmp;
    56:
    57:    Tmp.Real = C1.Real-C2.Real;
    58:    Tmp.Imaginary = C1.Imaginary-C2.Imaginary;
    59:    return Tmp;
    60: }
    61:
    62: /* Hiển thị số phức */
    63: void DisplayComplex(Complex C)
    64: {
    65:    printf("(%.1lf,%.1lf)",C.Real,C.Imaginary);
    66: }
    

Chúng ta chạy ví dụ 2.17, kết quả ở hình 2.20

Kết quả của ví dụ 2.17 (Hình 2.20)

Trong chương trình ở ví dụ 2.17, chúng ta nhận thấy với các hàm vừa cài đặt dùng để cộng và trừ hai số phức 1+2i và –3+4i; người lập trình hoàn toàn không thoải mái khi sử dụng bởi vì thực chất thao tác cộng và trừ là các toán tử chứ không phải là hàm. Để khắc phục yếu điểm này, trong C++ cho phép chúng ta có thể định nghĩa lại chức năng của các toán tử đã có sẵn một cách tiện lợi và tự nhiên hơn rất nhiều. Điều này gọi là đa năng hóa toán tử. Khi đó chương trình ở ví dụ 2.17 được viết như sau:

Ví dụ 2.18:

1: #include <iostream.h>
    2: // Định nghĩa số phức
    3: typedef struct
    4: {
    5:     double Real;
    6:     double Imaginary;
    7: }Complex;
    8:
    9:  Complex SetComplex(double R,double I);
    10: void DisplayComplex(Complex C);
    11: Complex operator + (Complex C1,Complex C2);
    12: Complex operator - (Complex C1,Complex C2);
    13:
    14: int main(void)
    15: {
    16:    Complex C1,C2,C3,C4;
    17:
    18:    C1 = SetComplex(1.0,2.0);
    19:    C2 = SetComplex(-3.0,4.0);
    20:    cout<<"
So phuc thu nhat:";
    21:    DisplayComplex(C1);
    22:    cout<<"
So phuc thu hai:";
    23:    DisplayComplex(C2);
    24:    C3 = C1 + C2;
    25:    C4 = C1 - C2;
    26:    cout<<"
Tong hai so phuc nay:";
    27:    DisplayComplex(C3);
    28:    cout<<"
Hieu hai so phuc nay:";
    29:    DisplayComplex(C4);
    30:    return 0;
    31: }
    32:
    33: //Đặt giá trị cho một số phức
    34: Complex SetComplex(double R,double I)
    35: {
    36:    Complex Tmp;
    37:
    38:    Tmp.Real = R;
    39:    Tmp.Imaginary = I;
    40:    return Tmp;
    41: }
    42:
    43: //Cộng hai số phức
    44: Complex operator + (Complex C1,Complex C2)
    45: {
    46:    Complex Tmp;
    47:
    48:    Tmp.Real = C1.Real+C2.Real;
    49:    Tmp.Imaginary = C1.Imaginary+C2.Imaginary;
    50:    return Tmp;
    51: }
    52:
    53: //Trừ hai số phức
    54: Complex operator - (Complex C1,Complex C2)
    55: {
    56:    Complex Tmp;
    57:
    58:    Tmp.Real = C1.Real-C2.Real;
    59:    Tmp.Imaginary = C1.Imaginary-C2.Imaginary;
    60:    return Tmp;
    61: }
    62:
    63: //Hiển thị số phức
    64: void DisplayComplex(Complex C)
    65: {
    66:    cout<<"("<<C.Real<<","<<C.Imaginary<<")";
    67: }
    

Chúng ta chạy ví dụ 2.18, kết quả ở hình 2.21

Kết quả của ví dụ 2.18 (Hình 2.21)

Như vậy trong C++, các phép toán trên các giá trị kiểu số phức được thực hiện bằng các toán tử toán học chuẩn chứ không phải bằng các tên hàm như trong C. Chẳng hạn chúng ta có lệnh sau:

C4 = AddComplex(C3, SubComplex(C1,C2));
    

thì ở trong C++, chúng ta có lệnh tương ứng như sau:

C4 = C3 + C1 - C2;
    

Chúng ta nhận thấy rằng cả hai lệnh đều cho cùng kết quả nhưng lệnh của C++ thì dễ hiểu hơn. C++ làm được điều này bằng cách tạo ra các hàm định nghĩa cách thực hiện của một toán tử cho các kiểu dữ liệu tự định nghĩa. Một hàm định nghĩa một toán tử có cú pháp sau:

data_type operator operator_symbol ( parameters )
    { 
    ………………………………
    }
    

Trong đó: data_type: Kiểu trả về.

operator_symbol: Ký hiệu của toán tử.
    parameters: Các tham số (nếu có).
    

Trong chương trình ví dụ 2.18, toán tử + là toán tử gồm hai toán hạng (gọi là toán tử hai ngôi; toán tử một ngôi là toán tử chỉ có một toán hạng) và trình biên dịch biết tham số đầu tiên là ở bên trái toán tử, còn tham số thứ hai thì ở bên phải của toán tử. Trong trường hợp lập trình viên quen thuộc với cách gọi hàm, C++ vẫn cho phép bằng cách viết như sau:

C3 = operator + (C1,C2);
    C4 = operator - (C1,C2);
    

Các toán tử được đa năng hóa sẽ được lựa chọn bởi trình biên dịch cũng theo cách thức tương tự như việc chọn lựa giữa các hàm được đa năng hóa là khi gặp một toán tử làm việc trên các kiểu không phải là kiểu có sẵn, trình biên dịch sẽ tìm một hàm định nghĩa của toán tử nào đó có các tham số đối sánh với các toán hạng để dùng. Chúng ta sẽ tìm hiểu kỹ về việc đa năng hóa các toán tử trong chương 4.

Các giới hạn của đa năng hóa toán tử:

. Chúng ta không thể định nghĩa các toán tử mới.

. Hầu hết các toán tử của C++ đều có thể được đa năng hóa. Các toán tử sau không được đa năng hóa là :

Toán tử Ý nghĩa
:: Toán tử định phạm vi.
.* Truy cập đến con trỏ là trường của struct hay thành viên của class.
. Truy cập đến trường của struct hay thành viên của class.
?: Toán tử điều kiện
sizeof  

và chúng ta cũng không thể đa năng hóa bất kỳ ký hiệu tiền xử lý nào.

. Chúng ta không thể thay đổi thứ tự ưu tiên của một toán tử hay không thể thay đổi số các toán hạng của nó.

. Chúng ta không thể thay đổi ý nghĩa của các toán tử khi áp dụng cho các kiểu có sẵn.

. Đa năng hóa các toán tử không thể có các tham số có giá trị mặc định.

Các toán tử có thể đa năng hoá:

+ - * / % ^
! = < > += -=
^= &= |= << >> <<=
<= >= && || ++ --
() [] new delete & |
~ *= /= %= >>= ==
!= , -> ->*    

Các toán tử được phân loại như sau :

.Các toán tử một ngôi : * & ~ ! ++ -- sizeof (data_type)

Các toán tử này được định nghĩa chỉ có một tham số và phải trả về một giá trị cùng kiểu với tham số của chúng. Đối với toán tử sizeof phải trả về một giá trị kiểu size_t (định nghĩa trong stddef.h)

Toán tử (data_type) được dùng để chuyển đổi kiểu, nó phải trả về một giá trị có kiểu là data_type.

. Các toán tử hai ngôi: * / % + - >> << > <

>= <= == != & | ^ && ||

Các toán tử này được định nghĩa có hai tham số.

. Các phép gán: = += -= *= /= %= >>= <<= ^= |=

Các toán tử gán được định nghĩa chỉ có một tham số. Không có giới hạn về kiểu của tham số và kiểu trả về của phép gán.

. Toán tử lấy thành viên : ->

. Toán tử lấy phần tử theo chỉ số: []

. Toán tử gọi hàm: ()

Bài 1: Hãy viết lại chương trình sau bằng cách sử dụng lại các dòng nhập/xuất trong C++.

/* Chương trình tìm mẫu chung nhỏ nhất */

#include <stdio.h>
    int main()
    {
    int a,b,i,min;
    printf("Nhap vao hai so:");
    scanf("%d%d",&a,&b);
    min=a>b?b:a;
    for(i = 2;i<min;++i)
    if (((a%i)==0)&&((b%i)==0)) break;
    if(i==min) {
    printf("Khong co mau chung nho nhat");
    return 0;
    }
    printf("Mau chung nho nhat la %d
",i);
    return 0;
    }
    

Bài 2: Viết chương trình nhập vào số nguyên dương h (2<h<23), sau đó in ra các tam giác có chiều cao là h như các hình sau:

Bài 3: Một tam giác vuông có thể có tất cả các cạnh là các số nguyên. Tập của ba số nguyên của các cạnh của một tam giác vuông được gọi là bộ ba Pitago. Đó là tổng bình phương của hai cạnh bằng bình phương của cạnh huyền, chẳng hạn bộ ba Pitago (3, 4, 5). Viết chương trình tìm tất cả các bộ ba Pitago như thế sao cho tất cả các cạnh không quá 500.

Bài 4: Viết chương trình in bảng của các số từ 1 đến 256 dưới dạng nhị phân, bát phân và thập lục phân tương ứng.

Bài 5: Viết chương trình nhập vào một số nguyên dương n. Kiểm tra xem số nguyên n có thuộc dãy Fibonacci không?

Bài 6: Viết chương trình nhân hai ma trân Amxnvà Bnxp. Mỗi ma trận được cấp phát động và các giá trị của chúng phát sinh ngẫu nhiên (Với m, n và p nhập từ bàn phím).

Bài 7: Viết chương trình tạo một mảng một chiều động có kích thước là n (n nhập từ bàn phím). Các giá trị của mảng này được phát sinh ngẫu nhiên trên đoạn [a, b] với a và b đều nhập từ bàn phím. Hãy tìm số dương nhỏ nhất và số âm lớn nhất trong mảng; nếu không có số dương nhỏ nhất hoặc số âm lớn nhất thì xuất thông báo "không có số dương nhỏ nhất" hoặc "không có số âm lớn nhất".

Bài 8: Anh (chị) hãy viết một hàm tính bình phương của một số. Hàm sẽ trả về giá trị bình phương của tham số và có kiểu cùng kiểu với tham số.

Bài 9: Trong ngôn ngữ C, chúng ta có hàm chuyển đổi một chuỗi sang số, tùy thuộc vào dạng của chuỗi chúng ta có các hàm chuyển đổi sau :

int atoi(const char *s);
    

Chuyển đổi một chuỗi s thành số nguyên kiểu int.

long atol(const char *s);
    

Chuyển đổi một chuỗi s thành số nguyên kiểu long.

double atof(const char *s);
    

Chuyển đổi một chuỗi s thành số thực kiểu double.

Anh (chị) hãy viết một hàm có tên là aton (ascii to number) để chuyển đổi chuỗi sang các dạng số tương ứng.

Bài 10: Anh chị hãy viết các hàm sau:

Hàm ComputeCircle() để tính diện tích s và chu vi c của một đường tròn bán kính r. Hàm này có prototype như sau:

void ComputeCircle(float & s, float &c, float r = 1.0);
    

Hàm ComputeRectangle() để tính diện tích s và chu vi p của một hình chữ nhật có chiều cao h và chiều rộng w. Hàm này có prototype như sau:

void ComputeRectangle(float & s, float &p, float h = 1.0, float w = 1.0);
    

Hàm ComputeTriangle() để tính diện tích s và chu vi p của một tam giác có ba cạnh a,b và c. Hàm này có prototype như sau:

void ComputeTriangle(float & s, float &p, float a = 1.0, float b = 1.0, float c = 1.0);
    

Hàm ComputeSphere() để tính thể tích v và diện tích bề mặt s của một hình cầu có bán kính r. Hàm này có prototype như sau:

void ComputeSphere(float & v, float &s, float r = 1.0);
    

Hàm ComputeCylinder() để tính thể tích v và diện tích bề mặt s của một hình trụ có bán kính r và chiều cao h. Hàm này có prototype như sau:

void ComputeCylinder(float & v, float &s, float r = 1.0 , float h = 1.0);
    

Bài 11: Anh (chị) hãy viết thêm hai toán tử nhân và chia hai số phức ở ví dụ 2.18 của chương 2.

Bài 12: Một cấu trúc Date chứa ngày, tháng và năm như sau:

struct Date
    {
    int Day; //Có giá trị từ 1 → 31
    int Month; //Có giá trị từ 1 → 12
    int Year; //Biểu diễn bằng 4 chữ số.
    };
    

Anh (chị) hãy viết các hàm định nghĩa các toán tử : + - > >= < <= == != trên cấu trúc Date này.

Bài 13: Một cấu trúc Point3D biểu diễn tọa độ của một điểm trong không gian ba chiều như sau:

struct Point3D
    {
    float X;
    float Y;
    float Z; 
    };
    

Anh (chị) hãy viết các hàm định nghĩa các toán tử : + - == != trên cấu trúc Point3D này.

Bài 14: Một cấu trúc Fraction dùng để chứa một phân số như sau:

struct Fraction
    {
    int Numerator; //Tử số 
    int Denominator; //Mẫu số 
    };
    

Anh (chị) hãy viết các hàm định nghĩa các toán tử :

+ - * / > >= < <= == != 
    

trên cấu trúc Fraction này.

0