Đề cương ôn tập OOP - C++

date
Jan 5, 2024
slug
oop-cpp
status
Published
tags
C++
summary
Chào bạn! Trong hành trình khám phá ngôn ngữ lập trình C++, bạn đã đặt ra những câu hỏi khá đa dạng về các khái niệm quan trọng của lập trình hướng đối tượng. Chúng ta đã thảo luận về hàm bạn, các từ khóa quy định mức độ truy cập, hàm tạo và hàm hủy, cùng với mục đích và ưu điểm của việc nạp chồng toán tử. Những kiến thức này không chỉ giúp bạn xây dựng và hiểu rõ về cú pháp C++, mà còn giúp bạn áp dụng chúng hiệu quả trong việc phát triển các ứng dụng phức tạp. Hãy tiếp tục khám phá và áp dụng những kiến thức này để trở thành một lập trình viên C++ thành thạo!
type
Post
Last updated
Jan 8, 2024 01:40 PM

LÝ THUYẾT

Câu 1 : Hàm bạn (friend) là gì? Điểm khác biệt so với hàm (Method)

Trong ngôn ngữ lập trình C++, "hàm bạn" (friend function) là một khái niệm cho phép một hàm không thuộc về lớp được khai báo là bạn của lớp đó. Hàm bạn có thể truy cập các thành viên private và protected của lớp mà nó là bạn của.
Dưới đây là một ví dụ đơn giản để minh họa cách sử dụng hàm bạn:
CODE
#include <iostream>

class MyClass {
private:
    int privateMember;

public:
    MyClass(int value) : privateMember(value) {}

    // Khai báo hàm bạn
    friend void friendFunction(const MyClass& obj);
};

// Định nghĩa hàm bạn
void friendFunction(const MyClass& obj) {
    // Hàm bạn có thể truy cập privateMember của lớp MyClass
    std::cout << "Value of privateMember: " << obj.privateMember << std::endl;
}

int main() {
    MyClass myObj(42);

    // Gọi hàm bạn
    friendFunction(myObj);

    return 0;
}
Trong ví dụ này, friendFunction là một hàm bạn của lớp MyClass. Nó có quyền truy cập vào privateMember của lớp mặc dù nó không phải là một phương thức của lớp.
Khác biệt chính giữa hàm bạn và phương thức là:
  • Vị trí khai báo: Hàm bạn được khai báo bên ngoài lớp, trong khi phương thức được khai báo và định nghĩa bên trong lớp.
  • Truy cập vào thành viên: Hàm bạn có thể truy cập private và protected thành viên của lớp mà nó là bạn của, trong khi phương thức có thể truy cập tất cả các thành viên của lớp mà nó thuộc về.

Câu 2 : Phân biệt PUBLIC, PRIVATE, PROTECTED

Trong ngôn ngữ lập trình C++, ba từ khóa quan trọng để quản lý quyền truy cập của thành viên lớp và kế thừa là public, private, và protected. Dưới đây là mô tả về từng từ khóa:
  1. Public (Công khai):
      • Các thành viên được khai báo là public có thể truy cập từ bất kỳ đâu trong chương trình.
      • Public thành viên của một lớp là những thành viên mà bạn muốn công bố và chia sẻ với bên ngoài lớp.
class Example {
public:
    int publicMember;  // Công khai, có thể truy cập từ bất kỳ đâu
};
  1. Private (Riêng tư):
      • Các thành viên được khai báo là private chỉ có thể truy cập từ bên trong cùng một lớp.
      • Private thành viên thường được sử dụng để bảo vệ dữ liệu và triển khai chi tiết cài đặt của lớp.
class Example {
private:
    int privateMember;  // Riêng tư, chỉ có thể truy cập từ bên trong lớp
};
  1. Protected (Bảo vệ):
      • Protected giống như private, nhưng các thành viên protected có thể được truy cập từ các lớp con (kế thừa) của lớp chứa chúng.
      • Protected thường được sử dụng khi bạn muốn chia sẻ một số thông tin với các lớp con mà không muốn công khai nó cho tất cả bên ngoài.
class Example {
protected:
    int protectedMember;  // Bảo vệ, có thể truy cập từ lớp con
};
Dưới đây là một ví dụ đơn giản về cách các từ khóa này hoạt động cùng với kế thừa:
CODE
class Base {
public:
    int publicMember;
private:
    int privateMember;
protected:
    int protectedMember;
};

class Derived : public Base {
    // Ở đây, publicMember và protectedMember có thể truy cập từ lớp con Derived
    // privateMember không thể truy cập từ lớp con
};

int main() {
    Base obj;
    obj.publicMember = 10;     // Có thể truy cập publicMember từ bên ngoài
    // obj.privateMember = 20; // Lỗi! Không thể truy cập privateMember từ bên ngoài
    // obj.protectedMember = 30; // Lỗi! Không thể truy cập protectedMember từ bên ngoài

    Derived derivedObj;
    derivedObj.publicMember = 10;    // Có thể truy cập publicMember từ lớp con
    // derivedObj.privateMember = 20; // Lỗi! privateMember vẫn không thể truy cập từ lớp con
    derivedObj.protectedMember = 30; // Có thể truy cập protectedMember từ lớp con

    return 0;
}
Trong kế thừa, public, private, và protected đều có ảnh hưởng đến cách thành viên của lớp cơ sở được truy cập từ lớp dẫn xuất.

Câu 3 : Mục đích của việc sử dụng GETTER

Trong ngôn ngữ lập trình C++, getter là một phương thức công cộng của một lớp được thiết kế để đọc giá trị của một thuộc tính private hoặc protected của lớp đó. Sử dụng getter có một số mục đích quan trọng:
  1. Bảo vệ dữ liệu:
      • Bằng cách giữ các thành viên dữ liệu private hoặc protected, lớp có thể kiểm soát cách dữ liệu được truy cập và cập nhật.
      • Getter cho phép truy cập an toàn và kiểm soát vào giá trị của các thuộc tính, thay vì để bất kỳ đoạn mã nào cũng có thể trực tiếp đọc và sửa đổi chúng.
  1. Kiểm soát truy cập:
      • Getter cung cấp một cách để kiểm soát cách bên ngoài lớp có thể đọc dữ liệu.
      • Nếu bạn chỉ cần cho phép đọc giá trị mà không muốn chấp nhận sự sửa đổi, bạn có thể tạo một getter mà chỉ trả về giá trị mà không có khả năng sửa đổi.
class Example {
private:
    int privateData;

public:
    // Getter cho privateData
    int getPrivateData() const {
        return privateData;
    }
};
  1. Encapsulation (Đóng gói):
      • Sử dụng getter giúp tạo ra sự đóng gói (encapsulation) bằng cách che giấu chi tiết cài đặt bên trong của lớp.
      • Nếu bạn muốn thay đổi cách dữ liệu được lưu trữ mà không ảnh hưởng đến bên ngoài, bạn chỉ cần thay đổi getter mà không làm thay đổi code bên ngoài.
class Example {
private:
    int privateData;

public:
    // Getter cho privateData
    int getPrivateData() const {
        // Có thể thay đổi cách dữ liệu được trả về mà không ảnh hưởng đến người sử dụng
        return privateData * 2;
    }
};
  1. Thực hiện logic trước khi trả giá trị:
      • Bạn có thể thực hiện logic nâng cao trong getter trước khi trả về giá trị.
      • Điều này bao gồm việc kiểm tra điều kiện, tính toán giá trị dựa trên dữ liệu hiện tại, hoặc bất kỳ xử lý nào khác trước khi trả về giá trị cho người gọi.
class Example {
private:
    int privateData;

public:
    // Getter có thực hiện logic trước khi trả về giá trị
    int getProcessedData() const {
        // Thực hiện một số logic xử lý trước khi trả về giá trị
        return privateData * 2;
    }
};
Tóm lại, việc sử dụng getter trong C++ giúp bảo vệ và kiểm soát truy cập đến dữ liệu của lớp, tạo sự đóng gói, và cung cấp cơ hội thực hiện logic trước khi trả giá trị.

Câu 4 : Hàm tạo là gì? (Contructor)

Trong ngôn ngữ lập trình C++, hàm tạo (constructor) là một loại hàm đặc biệt được gọi tự động khi một đối tượng của một lớp được tạo ra. Hàm tạo có cùng tên với tên của lớp và không có kiểu trả về (kể cả kiểu void). Hàm tạo thường được sử dụng để khởi tạo giá trị cho các thành viên dữ liệu của đối tượng và thực hiện các công việc khởi tạo khác.
Cú pháp của hàm tạo như sau:
class ClassName {
public:
    // Hàm tạo
    ClassName(parameters) {
        // Khởi tạo các thành viên dữ liệu và thực hiện các công việc khác nếu cần
    }

    // Các thành viên và phương thức khác của lớp
};
Dưới đây là một ví dụ đơn giản về việc sử dụng hàm tạo:
CODE
#include <iostream>

class MyClass {
private:
    int value;

public:
    // Hàm tạo
    MyClass(int initValue) {
        value = initValue;
        std::cout << "Constructor called with value: " << initValue << std::endl;
    }

    // Phương thức khác của lớp
    void display() {
        std::cout << "Value: " << value << std::endl;
    }
};

int main() {
    // Tạo đối tượng và gọi hàm tạo
    MyClass obj(42);

    // Gọi phương thức của đối tượng
    obj.display();

    return 0;
}
Trong ví dụ này, hàm tạo MyClass(int initValue) được gọi khi một đối tượng của lớp MyClass được tạo ra. Hàm tạo này thực hiện việc khởi tạo giá trị của thành viên dữ liệu value và in ra một thông điệp. Sau đó, phương thức display() được gọi để hiển thị giá trị của đối tượng.

Câu 5 : Kế thừa là gì? Ưu điểm của kế thừa

Kế thừa là một khái niệm quan trọng trong lập trình hướng đối tượng (OOP) trong C++ và nhiều ngôn ngữ lập trình khác. Kế thừa cho phép một lớp (lớp con hoặc lớp dẫn xuất) sử dụng các thành viên (thuộc tính và phương thức) của một lớp khác (lớp cơ sở hoặc lớp cha).
Cú pháp kế thừa trong C++:
class BaseClass {
    // Các thành viên của lớp cơ sở
};

class DerivedClass : public BaseClass {
    // Các thành viên của lớp dẫn xuất
};
Ưu điểm của kế thừa:
  1. Tái sử dụng mã (Code Reusability): Kế thừa cho phép sử dụng lại mã nguồn đã được viết từ lớp cơ sở. Các thành viên của lớp cơ sở có thể được sử dụng trong lớp dẫn xuất mà không cần phải viết lại mã nguồn.
  1. Đơn giản hóa và Tổ chức mã nguồn (Simplification and Organization of Code): Kế thừa giúp tổ chức mã nguồn một cách logic và giảm độ phức tạp của mã. Các lớp con chỉ cần thêm hoặc ghi đè những thành viên cần thiết, không cần phải định nghĩa lại tất cả.
  1. Mở rộng (Extensibility): Kế thừa cung cấp khả năng mở rộng chương trình. Bạn có thể thêm các phương thức mới hoặc mở rộng các phương thức đã có từ lớp cơ sở mà không làm ảnh hưởng đến mã nguồn hiện tại.
  1. Tính linh hoạt (Flexibility): Kế thừa cung cấp tính linh hoạt trong việc thay đổi và cải tiến chương trình. Bạn có thể thay đổi hành vi của lớp dẫn xuất bằng cách ghi đè các phương thức của lớp cơ sở.
  1. Hiệu suất (Performance): Sử dụng kế thừa có thể giảm sự trùng lặp mã nguồn và làm tăng hiệu suất chương trình vì mã chỉ cần được duy trì một lần.
Tuy nhiên, cũng cần lưu ý rằng sử dụng kế thừa một cách quá mức có thể dẫn đến các vấn đề như sự phụ thuộc mạnh mẽ giữa các lớp, sự phức tạp của hệ thống, và khả năng mất rõ ràng về cấu trúc của chương trình. Điều quan trọng là chọn cách sử dụng kế thừa một cách cẩn thận và có lợi cho thiết kế chương trình.

Câu 6 : Các hàm đặc trưng của lập trình hướng đối tượng là gì?

Lập trình hướng đối tượng (OOP) có một số đặc trưng chính, và có những hàm đặc trưng thường được sử dụng trong quá trình phát triển ứng dụng hướng đối tượng. Dưới đây là một số hàm đặc trưng này:
  1. Constructor (Hàm tạo):
      • Hàm tạo là một hàm đặc trưng của mỗi lớp trong OOP. Nó được gọi khi một đối tượng được tạo ra.
      • Hàm tạo thường được sử dụng để khởi tạo giá trị cho các thành viên dữ liệu của đối tượng.
class Example {
public:
    // Constructor
    Example() {
        // Khởi tạo giá trị cho các thành viên dữ liệu
    }
};
  1. Destructor (Hàm hủy):
      • Hàm hủy được gọi khi một đối tượng sắp bị hủy. Nó được sử dụng để giải phóng bộ nhớ và thực hiện các công việc dọn dẹp khác trước khi đối tượng bị hủy.
class Example {
public:
    // Destructor
    ~Example() {
        // Thực hiện các công việc hủy
    }
};
  1. Copy Constructor (Hàm sao chép):
      • Hàm sao chép được gọi khi một đối tượng mới được tạo bằng cách sao chép từ một đối tượng đã tồn tại.
      • Thường được sử dụng để sao chép giá trị của các thành viên dữ liệu từ một đối tượng khác.
class Example {
public:
    // Copy Constructor
    Example(const Example& other) {
        // Sao chép giá trị từ other vào đối tượng hiện tại
    }
};
  1. Operator Overloading (Nạp chồng toán tử):
      • Cho phép định nghĩa lại cách các toán tử hoạt động với đối tượng của lớp.
      • Ví dụ: +, -, *, / có thể được nạp chồng để thực hiện các phép toán tùy chỉnh trên đối tượng.
Ví dụ
class Complex {
public:
    double real, imag;

    // Operator Overloading
    Complex operator+(const Complex& other) {
        Complex result;
        result.real = real + other.real;
        result.imag = imag + other.imag;
        return result;
    }
};
  1. Getter và Setter:
      • Getter là hàm được sử dụng để đọc giá trị của một thuộc tính private.
      • Setter là hàm được sử dụng để thiết lập giá trị của một thuộc tính private.
Ví dụ
class Example {
private:
    int data;

public:
    // Getter
    int getData() const {
        return data;
    }

    // Setter
    void setData(int value) {
        data = value;
    }
};
Đây chỉ là một số hàm đặc trưng trong lập trình hướng đối tượng. Các hàm này cùng nhau giúp xây dựng cấu trúc và chức năng của đối tượng trong OOP.

Câu 7 : Hàm huỷ là gì? Mục đích của hàm gì.

Hàm hủy (destructor) là một hàm đặc biệt trong lập trình hướng đối tượng được sử dụng để giải phóng tài nguyên và thực hiện các công việc dọn dẹp khi một đối tượng sắp bị hủy, tức là khi kết thúc vòng đời của đối tượng. Hàm hủy có tên giống như tên của lớp nhưng được tiền đặt bởi dấu ~ (ký hiệu tilde).
Cú pháp:
class ClassName {
public:
    // Constructor
    ClassName() {
        // Khởi tạo
    }

    // Destructor
    ~ClassName() {
        // Thực hiện các công việc dọn dẹp, giải phóng tài nguyên
    }
};
Mục đích của hàm hủy:
  1. Giải phóng bộ nhớ:
      • Một trong những mục đích quan trọng của hàm hủy là giải phóng bộ nhớ đã được cấp phát cho đối tượng trong quá trình sống.
      • Điều này làm tránh việc xảy ra memory leak (rò rỉ bộ nhớ) trong chương trình.
  1. Dọn dẹp tài nguyên:
      • Nếu đối tượng sử dụng các tài nguyên khác như mở tệp, mở kết nối cơ sở dữ liệu, hoặc các tài nguyên hệ thống khác, hàm hủy có thể được sử dụng để đóng tệp, đóng kết nối, hoặc thực hiện các bước dọn dẹp khác.
  1. Gọi hàm hủy cho các thành viên đối tượng:
      • Nếu một đối tượng chứa các thành viên là đối tượng khác, hàm hủy của đối tượng con sẽ được tự động gọi trước khi hàm hủy của đối tượng cha được gọi.
      • Điều này giúp đảm bảo rằng các tài nguyên của đối tượng con được giải phóng đúng cách trước khi đối tượng cha bị hủy.
Ví dụ
#include <iostream>

class Resource {
public:
    Resource() {
        std::cout << "Resource acquired." << std::endl;
    }

    ~Resource() {
        std::cout << "Resource released." << std::endl;
    }
};

class Example {
private:
    Resource* resource;

public:
    Example() : resource(new Resource()) {
        // Khởi tạo
    }

    ~Example() {
        // Dọn dẹp tài nguyên
        delete resource;
    }
};

int main() {
    Example obj;  // Hàm hủy của Example sẽ được gọi khi obj ra khỏi phạm vi
    return 0;     // In ra "Resource acquired." và "Resource released."
}
Hàm hủy giúp đảm bảo rằng các tài nguyên và trạng thái của đối tượng được quản lý một cách đúng đắn khi đối tượng kết thúc vòng đời của mình.
Câu 8 : Mục đích của việc nạp trồng các toán tử?
Nạp chồng toán tử (Operator overloading) là một khái niệm trong lập trình hướng đối tượng (OOP) cho phép bạn định nghĩa lại cách một toán tử hoạt động với các đối tượng của lớp mà bạn tạo. Mục đích chính của việc nạp chồng toán tử là tạo ra một cách hiểu quả và tự nhiên để thực hiện các phép toán trên đối tượng.
Dưới đây là một số mục đích chính của việc nạp chồng toán tử:
  1. Tăng độ hiểu quả của mã:
      • Nạp chồng toán tử giúp tăng độ hiểu quả của mã nguồn, làm cho chương trình trở nên dễ đọc hơn và gần gũi với ngôn ngữ tự nhiên.
      • Thay vì sử dụng các phương thức tên gọi phức tạp, bạn có thể sử dụng các toán tử quen thuộc, giảm thiểu sự phức tạp trong mã nguồn.
  1. Hỗ trợ cú pháp tự nhiên:
      • Nạp chồng toán tử giúp tạo ra cú pháp tự nhiên và logic khi thực hiện các phép toán trên các đối tượng.
      • Ví dụ, bạn có thể nạp chồng toán tử + để thực hiện cộng hai đối tượng của lớp một cách tương tự như cộng hai số nguyên.
Ví dụ
class Complex {
public:
    double real, imag;

    // Nạp chồng toán tử cộng
    Complex operator+(const Complex& other) {
        Complex result;
        result.real = real + other.real;
        result.imag = imag + other.imag;
        return result;
    }
};

Complex a = {1.0, 2.0};
Complex b = {2.0, 3.0};
Complex c = a + b; // Sử dụng toán tử cộng thay vì phương thức
  1. Tích hợp với ngôn ngữ:
      • Việc nạp chồng toán tử giúp tích hợp các đối tượng của lớp vào ngôn ngữ và cú pháp của ngôn ngữ đó một cách tự nhiên.
      • Điều này làm cho việc sử dụng các đối tượng của lớp giống với việc sử dụng các kiểu dữ liệu cơ bản như số nguyên, số thực, v.v.
Ví dụ
Matrix A, B, C;
C = A + B; // Sử dụng toán tử cộng để thực hiện phép cộng ma trận
  1. Hỗ trợ quy tắc đối xử chuẩn (Canonical behavior):
      • Nạp chồng toán tử giúp đảm bảo rằng các đối tượng của lớp có thể thực hiện các phép toán theo cách mà người lập trình mong đợi.
      • Ví dụ, nếu bạn nạp chồng toán tử ==, !=, bạn có thể so sánh hai đối tượng để kiểm tra bằng nhau hoặc khác nhau một cách tự nhiên.
Ví dụ
class Point {
public:
    int x, y;

    // Nạp chồng toán tử so sánh bằng
    bool operator==(const Point& other) {
        return (x == other.x && y == other.y);
    }

    // Nạp chồng toán tử so sánh khác
    bool operator!=(const Point& other) {
        return !(*this == other);
    }
};

Point p1 = {1, 2};
Point p2 = {1, 2};
if (p1 == p2) {
    // Hai điểm bằng nhau
}
Tóm lại, việc nạp chồng toán tử giúp làm cho mã nguồn trở nên rõ ràng, hiểu quả và dễ đọc hơn khi thực hiện các phép toán trên các đối tượng của lớp.

ÔN TẬP NẠP CHỒNG

Toán tử “ = “

class ThiSinh {
// ... (các phần khác)
public:
	ThiSinh(); // Khai báo hàm tạo mặc định

	ThiSinh& ThiSinh::operator=(const ThiSinh& other) {
    if (this != &other) {  // Kiểm tra để tránh gán cho chính đối tượng
        maThiSinh = other.maThiSinh;
        hoTen = other.hoTen;
        diemToan = other.diemToan;
        diemLy = other.diemLy;
        diemHoa = other.diemHoa;
    }
    return *this;  // Trả về tham chiếu đến đối tượng hiện tại
	}
};

Toán tử gán (operator=) trong C++ thường được định nghĩa để sao chép dữ liệu từ một đối tượng đã tồn tại sang đối tượng khác của cùng kiểu. Khi bạn định nghĩa toán tử gán, việc sử dụng const và tham chiếu (&) giúp đảm bảo tính đúng đắn và hiệu quả của mã.
  1. Tham chiếu (&): Sử dụng tham chiếu giúp tránh tạo ra một bản sao của đối tượng khi truyền tham số. Thay vì truyền bằng giá trị (copy), tham chiếu sẽ truyền một địa chỉ đến đối tượng, giảm độ phức tạp và tăng hiệu suất. Ngoài ra, nếu bạn không sử dụng tham chiếu (&), mỗi khi gọi toán tử gán, một bản sao của đối tượng sẽ được tạo ra, tăng độ phức tạp và có thể làm giảm hiệu suất.
  1. const: Khi bạn sử dụng const trong khai báo toán tử gán, bạn đang nói với trình biên dịch rằng toán tử gán này không thay đổi giá trị của đối tượng nguồn (bên phải của phép gán). Điều này giúp bảo vệ đối tượng nguồn từ bất kỳ thay đổi nào trong quá trình gán, đồng thời cung cấp tính chất hằng số (const correctness) cho toán tử.
  1. *this: là một cách để trả về đối tượng hiện tại (đang được gọi). Khi bạn đang triển khai toán tử gán (operator=) trong một lớp, việc trả về *this cho phép bạn chuỗi các phép gán lại với nhau.
Ví dụ:
ThiSinh a, b, c;
// ...
a = b = c;
Trong ví dụ trên, nếu toán tử gán trả về một giá trị khác (ví dụ: không phải là *this), thì sự chuỗi gán này sẽ không hoạt động đúng. Khi bạn trả về *this, nó thực sự trả về một tham chiếu đến đối tượng hiện tại, cho phép tiếp tục gán giá trị cho đối tượng đó.
Nếu không sử dụng *this và thay vào đó trả về một giá trị khác (ví dụ: return someOtherObject;), thì chuỗi gán sẽ không hoạt động đúng và có thể dẫn đến kết quả không mong muốn hoặc lỗi biên dịch.

BÀI TẬP

Câu 1: Viết chương trình thực hiện các yêu cầu sau:

  1. Khai báo lớp đa thức với các thuộc tính: bậc đa thức, các hệ số tương ứng.
  1. Xây dựng các phương thức: nhập, xuất một đối tượng đa thức.
  1. Định nghĩa toán tử +, - hai đa thức. Thực hiện cộng, trừ hai đa thức và in kết quả ra màn hình.
CODE
#include <bits/stdc++.h>
using namespace std;

//-----------------------------[DEFINE]-----------------------------//
int MOD = 1e9 + 7;
using ll = long long;

//-----------------------------[DEBUG]-----------------------------//

#define show(args...) describe(#args,args);
template<typename T>
void describe(string var_name, T value) {
    clog << var_name << " = " << value << " " << endl;
}

template<typename T, typename... Args>
void describe(string var_names, T value, Args... args) {
    string::size_type pos = var_names.find(',');
    string name = var_names.substr(0, pos);
    var_names = var_names.substr(pos + 1);
    clog << name << " = " << value << " | ";
    describe(var_names, args...);
}
//-----------------------------[END-DEBUG]-----------------------------//


//-----------------------------[DECLARATION]-----------------------------//

class Polynomial
{
private:
    int maxPower; // Bac lon nhat cua da thuc
    int coefficient[10] = {0}; // Mang chua he so cua da thuc theo bac
public:
    
    friend istream& operator >> (istream& in, Polynomial& x){
        cout << "   NHAP HE SO LON NHAT CUA DA THUC : ";
        in >> x.maxPower;

        for(int i = x.maxPower; i >= 0 ; i--){
            cout << "   HE SO " << "x^" << i << " : ";  
            in >> x.coefficient[i];
        }

        return in;
    }

    friend ostream& operator << (ostream& out, Polynomial x){
        
        for(int i = x.maxPower; i >= 0; i--){
            if(i == 0){
                out << x.coefficient[i] << endl;
            }
            else{
                if(x.coefficient[i] == 1){
                    out << "x^" << i << " + ";
                }
                else if(x.coefficient[i] != 0){
                   out << x.coefficient[i] << "x^" << i << " + ";
                }
            }
        }

        return out;
    }

    friend Polynomial operator + (Polynomial x, Polynomial y){
        Polynomial result;
        result.maxPower = max(x.maxPower, y.maxPower);

        for(int i = 0; i <= result.maxPower; i++){
            result.coefficient[i] = x.coefficient[i] + y.coefficient[i];
        }

        return result;
    }

};

//-----------------------------[FUNCTION]-----------------------------//



//-----------------------------[END]-----------------------------//


int main(){
    Polynomial x, y;

    cout << "NHAP DA THUC SO 1 : " << endl;
    cin >> x;
    cout << x;

    cout << "NHAP DA THUC SO 2 : " << endl;
    cin >> y;
    cout << y;

    cout << "KET QUA DA THUC 1 + DA THUC 2 : " << endl;
    cout << x + y;
}