Kiến thức cơ bản về Lớp (Class) và Đối tượng (Object) trong C++

By 08/01/2021Tháng Sáu 9th, 2021C/C++, KIẾN THỨC LẬP TRÌNH

1. Object và Class trong C++

Trong các bài hướng dẫn trước, chúng ta đã tìm hiểu về hàm và biến. Đôi khi, bạn nên đặt các  hàm và dữ liệu liên quan ở một nơi sao cho hợp lý và dễ thao tác hơn.

Giả sử, chúng ta cần lưu trữ chiều dài, chiều rộng và chiều cao của một căn phòng hình chữ nhật và tính diện tích, thể tích của nó.

Để giải quyết nhiệm vụ này, chúng ta có thể tạo ba biến: length, breadth, và height với các hàm tính toán calculateArea() và calculateVolume().

Tuy nhiên, trong C++, thay vì tạo các biến và hàm riêng biệt, chúng ta cũng có thể gói các dữ liệu và hàm liên quan này vào một nơi duy nhất (bằng cách tạo các đối tượng – object). Mô hình lập trình này được gọi là lập trình hướng đối tượng.

Nhưng để có thể tạo các đối tượng và sử dụng chúng trong C++, trước tiên chúng ta hãy đi tìm hiểu về các lớp (class trong C++).

1.1. Lớp trong C++

Một lớp trong C++ là gì?

Class trong C++ hay còn gọi là lớp trong C++  là một bản thiết kế cho đối tượng.

Chúng ta có thể coi lớp như một bản phác thảo (nguyên mẫu) của một ngôi nhà. Nó chứa tất cả các chi tiết về sàn nhà, cửa ra vào, cửa sổ v.v… Dựa trên những mô tả này, ta đi xây dựng ngôi nhà. Ngôi nhà chính là đối tượng.

Tạo một lớp

Một lớp được định nghĩa trong C++ bằng cách sử dụng từ khóa class, theo sau là tên của lớp.

Phần thân của lớp được xác định bên trong dấu ngoặc nhọn và được kết thúc bằng dấu chấm phẩy ở cuối cùng.

class className {

// some data

// some functions

};

Ví dụ:

class Room {

public:

double length;

double breadth;

double height;

double calculateArea(){

return length * breadth;

}

double calculateVolume(){

return length * breadth * height;

}

};

Ở đây, tôi đã xác định một lớp có tên là Room.

Các biến length, breadth, và height được khai báo bên trong lớp được gọi là thành phần dữ liệu. Và các hàm calculateArea() và calculateVolume() được gọi là các hàm thành phần của một lớp.

1.2. Đối tượng trong C++

Khi một lớp được định nghĩa, nó chỉ mô tả cho đối tượng được xác định, mà không có bộ nhớ hoặc nơi lưu trữ nào được cấp phát.

Để sử dụng dữ liệu và các hàm truy cập được xác định trong lớp, ta cần phải tạo các đối tượng.

Cú pháp xác định đối tượng trong C++

className objectVariableName;

Chúng ta có thể tạo đối tượng của lớp Room (ở ví dụ bên trên) theo cách bên dưới:

// sample function

void sampleFunction() {

// create objects

Room room1, room2;

}

int main(){

// create objects

Room room3, room4;

}

Ở đây, hai đối tượng room1 và room2 của lớp room được tạo bên trong sampleFunction(). Tương tự, đối tượng room3 và room4 được tạo trong main().

Như chúng ta thấy, chúng ta có thể tạo các đối tượng của một lớp trong bất kỳ hàm nào của chương trình. Ta cũng có thể tạo các đối tượng của một lớp trong chính lớp đó hoặc trong các lớp khác.

Ngoài ra, chúng ta có thể tạo bao nhiêu đối tượng tùy thích từ một lớp duy nhất.

1.3. Truy cập dữ liệu và hàm thành phần trong C++

Chúng ta có thể truy cập dữ liệu và hàm thành phần của một lớp bằng cách sử dụng toán tử . (dấu chấm). Ví dụ:

room2.calculateArea();

Nó sẽ gọi hàm calculateArea() bên trong lớp Room cho đối tượng room2.

Tương tự, dữ liệu thành phần có thể được gọi bằng cách:

room1.length = 5.5;

Trong trường hợp này, nó khởi tạo biến length của room1 là 5.5.

Ví dụ 1: Đối tượng và lớp trong chương trình C++

// Program to illustrate the working of

// objects and class in C++ Programming

#include <iostream>

using namespace std;

// create a class

class Room {

public:

double length;

double breadth;

double height;

double calculateArea() {

return length * breadth;

}

double calculateVolume() {

return length * breadth * height;

}

};

int main() {

// create object of Room class

Room room1;

// assign values to data members

room1.length = 42.5;

room1.breadth = 30.8;

room1.height = 19.2;

// calculate and display the area and volume of the room

cout << “Area of Room =  ” << room1.calculateArea() << endl;

cout << “Volume of Room =  ” << room1.calculateVolume() << endl;

return 0;

}

Đầu ra

Area of Room =  1309

Volume of Room =  25132.8

Trong chương trình này, tôi đã sử dụng lớp room và đối tượng room1 của nó để tính diện tích và thể tích của căn phòng.

Trong main(), tôi gán giá trị length, breadth, height bằng code:

room1.length = 42.5;

room1.breadth = 30.8;

room1.height = 19.2;

Sau đó tôi gọi hàm calculateArea() và calculateVolume() để thực hiện các phép tính cần thiết.

Cần lưu ý về việc sử dụng từ khóa public trong chương trình. Nó có nghĩa là các thành phần là công khai và có thể được gán ở bất cứ đâu trong chương trình.

Với nhu cầu của tôi, tôi cũng có thể tạo thành phần riêng tư (không công khai) bằng cách sử dụng từ khóa private. Thành phần riêng của một lớp chỉ có thể được truy cập bên trong lớp đó. Ví dụ:

class Test {

private:

int a;

void function1() { }

public:

int b;

void function2() { }

}

Here, a and function1() are private and are. Thus they cannot be accessed from outside the class.

Mặt khác, b và function2 có thể được truy cập từ mọi nơi trong chương trình.

Ví dụ 2: Sử dụng từ khóa public và private trong lớp

// Program to illustrate the working of

// public and private in C++ Class

#include <iostream>

using namespace std;

class Room {

private:

double length;

double breadth;

double height;

public:

// function to initialize private variables

void getData(double len, double brth, double hgt) {

length = len;

breadth = brth;

height = hgt;

}

double calculateArea() {

return length * breadth;

}

double calculateVolume() {

return length * breadth * height;

}

};

int main() {

// create object of Room class

Room room1;

// pass the values of private variables as arguments

room1.getData(42.5, 30.8, 19.2);

cout << “Area of Room =  ” << room1.calculateArea() << endl;

cout << “Volume of Room =  ” << room1.calculateVolume() << endl;

return 0;

}

Đầu ra

Area of Room =  1309

Volume of Room =  25132.8

Ví dụ trên gần giống với ví dụ đầu tiên, ngoại trừ việc các biến lớp bây giờ là private.

Vì các biến bây giờ là riêng tư nên tôi không thể truy cập chúng trực tiếp từ main(). Do đó, đoạn code sau sẽ không hợp lệ:

// invalid code

obj.length = 42.5;

obj.breadth = 30.8;

obj.height = 19.2;

Thay vào đó, ta sử dụng hàm public getData() để khởi tạo các biến private thông qua các tham số hàm double len, double brth và double hgt.

2. Hàm khởi tạo trong C++ (Constructor)

Hàm khởi tạo là một loại hàm thành phần đặc biệt được gọi tự động khi một đối tượng được tạo.

Trong C++, một hàm khởi tạo có cùng tên với tên của lớp và nó không có kiểu trả về. Ví dụ:

class  Wall {

public:

// create a constructor

Wall() {

// code

}

};

Ở đây, hàm Wall() là một hàm khởi tạo của lớp Wall. Lưu ý rằng hàm khởi tạo:

  • Có tên giống với tên lớp
  • Không có kiểu trả về
  • Và là hàm công khai (public)

2.1. Hàm khởi tạo mặc định trong C++

Một hàm khởi tạo không có tham số được gọi là hàm khởi tạo mặc định. Trong ví dụ trên, Wall() là một hàm khởi tạo mặc định.

Ví dụ 1: Hàm khởi tạo mặc định

// C++ program to demonstrate the use of default constructor

#include <iostream>

using namespace std;

// declare a class

class  Wall {

private:

double length;

public:

// create a constructor

Wall() {

// initialize private variables

length = 5.5;

cout << “Creating a wall.” << endl;

cout << “Length = ” << length << endl;

}

};

int main() {

// create an object

Wall wall1;

return 0;

}

Đầu ra

Creating a Wall

Length = 5.5

Ở đây, khi đối tượng wall1 được tạo, hàm khởi tạo Wall() được gọi. Nó thiết lập biến length của đối tượng là 5.5.

Lưu ý: nếu chúng ta chưa xác định hàm khởi tạo trong lớp, thì trình biên dịch C++ sẽ tự động tạo một hàm khởi tạo mặc định với code rỗng và không có tham số.

2.2. Hàm khởi tạo tham số trong C++

Trong C++, một hàm khởi tạo với các tham số được gọi là hàm khởi tạo tham số. Đây là phương thức được ưa chuộng để khởi tạo dữ liệu thành phần.

Ví dụ 2: Hàm khởi tạo tham số

// C++ program to calculate the area of a wall

#include <iostream>

using namespace std;

// declare a class

class Wall {

private:

double length;

double height;

public:

// create parameterized constructor

Wall(double len, double hgt) {

// initialize private variables

length = len;

height = hgt;

}

double calculateArea() {

return length * height;

}

};

int main() {

// create object and initialize data members

Wall wall1(10.5, 8.6);

Wall wall2(8.5, 6.3);

cout << “Area of Wall 1: ” << wall1.calculateArea() << endl;

cout << “Area of Wall 2: ” << wall2.calculateArea() << endl;

return 0;

}

Đầu ra

Area of Wall 1: 90.3

Area of Wall 2: 53.55

Ở đây, tôi đã tạo một hàm khởi tạo tham số Wall() có 2 tham số: double len và double hgt. Các giá trị chứa trong các tham số này được sử dụng để khởi tạo các biến thành phần là length và height.

Khi tạo một đối tượng của lớp Room, tôi chuyển các giá trị cho các biến thành phần làm đối số bằng code:

Wall wall1(10.5, 8.6);

Wall wall2(8.5, 6.3);

Với các biến thành phần được khởi tạo như vậy, bây giờ tôi có thể tính toán diện tích của bức tường với hàm calculateArea().

2.3. Hàm khởi tạo sao chép trong C++

Hàm khởi tạo sao chép trong C++ được sử dụng để sao chép dữ liệu của một đối tượng này sang một đối tượng khác.

Ví dụ 3: Hàm khởi tạo sao chép

#include <iostream>

using namespace std;

// declare a class

class Wall {

private:

double length;

double height;

public:

// parameterized constructor

Wall(double len, double hgt) {

// initialize private variables

length = len;

height = hgt;

}

// copy constructor with a Wall object as parameter

Wall(Wall &obj) {

// initialize private variables

length = obj.length;

height = obj.height;

}

double calculateArea() {

return length * height;

}

};

int main() {

// create an object of Wall class

Wall wall1(10.5, 8.6);

// print area of wall1

cout << “Area of Room 1: ” << wall1.calculateArea() << endl;

// copy contents of room1 to another object room2

Wall wall2 = wall1;

// print area of wall2

cout << “Area of Room 2: ” << wall2.calculateArea() << endl;

return 0;

}

Đầu ra

Area of Room 1: 90.3

Area of Room 2: 90.3

Trong chương trình này, chúng ta đã sử dụng một hàm khởi tạo sao chép để sao chép nội dung của một đối tượng của lớp Wall sang một đối tượng khác. Code của hàm khởi tạo sao chép là:

Room(Room &obj) {

length = obj.length;

height = obj.height;

}

Lưu ý rằng tham số của hàm khởi tạo này có địa chỉ của một đối tượng của lớp Wall.

Sau đó, chúng ta gán giá trị của các biến của đối tượng đầu tiên cho các biến tương ứng của đối tượng thứ hai. Đây là cách nội dung của đối tượng được sao chép.

Trong main() ta tạo hai đối tượng wall1 và wall2, rồi sao chép nội dung của đối tượng đầu tiên sang đối tượng thứ hai bằng code:

Wall wall2 = wall1;

Lưu ý: Một hàm khởi tạo chủ yếu được sử dụng để khởi tạo các đối tượng. Chúng cũng được sử dụng để chạy mã mặc định khi một đối tượng được tạo.

3. Đối tượng và hàm trong C++

Trong lập trình C++, chúng ta có thể truyền các đối tượng cho một hàm theo cách tương tự như truyền các đối số thông thường.

Ví dụ 1: Truyền đối tượng vào hàm

// C++ program to calculate the average marks of two students

#include <iostream>

using namespace std;

class Student {

public:

double marks;

// constructor to initialize marks

Student(double m) {

marks = m;

}

};

// function that has objects as parameters

void calculateAverage(Student s1, Student s2) {

// calculate the average of marks of s1 and s2

double average = (s1.marks + s2.marks) / 2;

cout << “Average Marks = ” << average << endl;

}

int main() {

Student student1(88.0), student2(56.0);

// pass the objects as arguments

calculateAverage(student1, student2);

return 0;

}

Đầu ra

Average Marks = 72

Ở đây, tôi đã truyền 2 đối tượng student là student1 và student2 làm đối số vào hàm calculateAverage().

https://cdn.programiz.com/sites/tutorial2program/files/cpp-pass-object-to-function.png

Ví dụ 2: Đối tượng trả về từ một hàm

#include <iostream>

using namespace std;

class Student {

public:

double marks1, marks2;

};

// function that returns object of Student

Student createStudent() {

Student student;

// Initialize member variables of Student

student.marks1 = 96.5;

student.marks2 = 75.0;

// print member variables of Student

cout << “Marks 1 = ” << student.marks1 << endl;

cout << “Marks 2 = ” << student.marks2 << endl;

return student;

}

int main() {

Student student1;

// Call function

student1 = createStudent();

return 0;

}

Đầu ra

Marks1 = 96.5

Marks2 = 75

https://cdn.programiz.com/sites/tutorial2program/files/cpp-return-object-from-function.png

Trong chương trình này, tôi đã tạo hàm createStudent() để trả về đối tượng của lớp student.

Tôi đã gọi createStudent() từ hàm main().

// Call function

student1 = createStudent();

Ở đây, tôi đang lưu trữ đối tượng được trả về bởi createStudent() trong student1.

Toán tử nạp chồng trong C++

Trong C++, chúng ta có thể thay đổi cách thức hoạt động của các toán tử đối với các kiểu do người dùng xác định như đối tượng và cấu trúc. Đây được gọi là toán tử nạp chồng. Ví dụ:

Giả sử tôi tạo ba đối tượng c1, c2 và kết quả từ một lớp tên Complex đại diện cho các số phức.

Vì việc nạp chồng toán tử cho phép ta thay đổi cách hoạt động của toán tử, chúng ta có thể xác định lại cách hoạt động của toán tử + và sử dụng nó để thêm các số phức c1 và c2 bằng cách dùng đoạn code sau:

result = c1 + c2;

Thay vì sử dụng:

result = c1.addNumbers(c2);

Nó sẽ làm cho code của ta trực quan và dễ hiểu hơn.

Lưu ý: Ta không thể sử dụng toán tử nạp chồng cho các kiểu dữ liệu cơ bản như int, float, char, v.v…

3.1. Cú pháp của toán tử nạp chồng trong C++

Để nạp chồng một toán tử, chúng ta cần sử dụng một hàm toán tử đặc biệt:

class className {

… .. …

public

returnType operator symbol (arguments) {

… .. …

}

… .. …

};

Ở đây:

  • returnType là kiểu trả về của hàm
  • operator là từ khóa
  • symbol là toán tử tôi muốn chồng, như: +, <, -, ++, v.v…
  • arguments là các đối số được truyền cho hàm

3.2. Toán tử nạp chồng trong toán tử đơn nguyên

Toán tử đơn nguyên chỉ hoạt động trên một toán hạng. Toán tử tăng ++ và toán tử giảm – là các ví dụ về toán tử đơn nguyên.

Ví dụ 1: Toán tử (đơn nguyên) nạp chồng ++

// Overload ++ when used as prefix

#include <iostream>

using namespace std;

class Count {

private:

int value;

public:

// Constructor to initialize count to 5

Count() : value(5) {}

// Overload ++ when used as prefix

void operator ++ () {

++value;

}

void display() {

cout << “Count: ” << value << endl;

}

};

int main() {

Count count1;

// Call the “void operator ++ ()” function

++count1;

count1.display();

return 0;

}

Đầu ra

Count: 6

Ở đây, khi tôi sử dụng ++count1;toán tử void operator ++ () được gọi. Nó làm tăng thuộc tính giá trị của đối tượng count1 lên 1.

Lưu ý: Khi chúng ta nạp chồng toán tử, chúng ta có thể sử dụng nó để hoạt động theo bất kỳ cách nào chúng ta muốn. Ví dụ, tôi có thể dùng ++ để tăng giá trị lên 100.

Tuy nhiên, nếu làm vậy code của tôi sẽ khó hiểu hơn. Với tư cách là một lập trình viên, tôi cần sử dụng toán tử nạp chồng một cách đúng đắn theo cách nhất quán và trực quan nhất có thể.

Ví dụ trên chỉ hoạt động khi ++ được sử dụng làm tiền tố. Để làm cho ++ hoạt động như một hậu tố, tôi cần sử dụng cú pháp này:

void operator ++ (int) {

// code

}

Int bên trong dấu ngoặc đơn là cú pháp được sử dụng để dùng các toán tử đơn nguyên làm hậu tố, nó không phải là một tham số hàm.

Ví dụ 2: Toán tử (đơn nguyên) nạp chồng ++

// Overload ++ when used as prefix and postfix

#include <iostream>

using namespace std;

class Count {

private:

int value;

public:

// Constructor to initialize count to 5

Count() : value(5) {}

// Overload ++ when used as prefix

void operator ++ () {

++value;

}

// Overload ++ when used as postfix

void operator ++ (int) {

++value;

}

void display() {

cout << “Count: ” << value << endl;

}

};

int main() {

Count count1;

// Call the “void operator ++ (int)” function

count1++;

count1.display();

// Call the “void operator ++ ()” function

++ count1;

count1.display();

return 0;

}

Đầu ra

Count: 6

Count: 7

Ví dụ 2 hoạt động khi ++ được sử dụng như cả tiền tố và hậu tố. Tuy nhiên, nó không hoạt động nếu tôi làm như sau:

Count count1, result;

// Error

result = ++count1;

Code này không hoạt động là do kiểu trả về của hàm toán tử là void. Để giải quyết vấn đề này, ta có thể đặt Count làm kiểu trả về của hàm toán tử.

// return Count when ++ used as prefix

Count operator ++ () {

// code

}

// return Count when ++ used as postfix

Count operator ++ (int) {

// code

}

Ví dụ 3: Giá trị trả về từ hàm toán tử (toán tử ++)

#include <iostream>

using namespace std;

class Count {

private:

int value;

public

:

// Constructor to initialize count to 5

Count() : value(5) {}

// Overload ++ when used as prefix

Count operator ++ () {

Count temp;

// Here, value is the value attribute of the calling object

temp.value = ++value;

return temp;

}

// Overload ++ when used as postfix

Count operator ++ (int) {

Count temp;

// Here, value is the value attribute of the calling object

temp.value = ++value;

return temp;

}

void display() {

cout << “Count: ” << value << endl;

}

};

int main() {

Count count1, result;

// Call the “Count operator ++ ()” function

result = ++count1;

result.display();

// Call the “Count operator ++ (int)” function

result = count1++;

result.display();

return 0;

}

Đầu ra

Count: 6

Count: 7

Ở đây, tôi đã sử dụng code sau để nạp chồng toán tử tiền tố:

// Overload ++ when used as prefix

Count operator ++ () {

Count temp;

// Here, value is the value attribute of the calling object

temp.value = ++value;

return temp;

}

Code nạp chồng toán tử hậu tố cũng tương tự như vậy. Lưu ý rằng tôi đã tạo một đối tượng temp và trả về giá trị của nó cho hàm toán tử.

Lưu ý thêm code:

temp.value = ++value;

Biến value thuộc về đối tượng count1 trong hàm main() vì count1 đang gọi hàm, trong khi temp.vallue thuộc đối tượng temp.

3.3. Toán tử nạp chồng trong toán tử nhị phân

Toán tử nhị phân hoạt động trên hai toán hạng. Ví dụ:

result = num + 9;

Ở đây, + là một toán tử nhị phân hoạt động trên các toán hạng num và 9.

Khi tôi nạp chồng toán tử nhị phân cho các kiểu do người dùng xác định bằng cách dùng code:

obj3 = obj1 + obj2;

Hàm toán tử được gọi bằng cách sử dụng đối tượng obj1 và obj2 được truyền như một đối số cho hàm.

Ví dụ 4: Nạp chồng toán tử nhị phân

// C++ program to overload the binary operator +

// This program adds two complex numbers

#include <iostream>

using namespace std;

class Complex {

private:

float real;

float imag;

public:

// Constructor to initialize real and imag to 0

Complex() : real(0), imag(0) {}

void input() {

cout << “Enter real and imaginary parts respectively: “;

cin >> real;

cin >> imag;

}

// Overload the + operator

Complex operator + (const Complex& obj) {

Complex temp;

temp.real = real + obj.real;

temp.imag = imag + obj.imag;

return temp;

}

void output() {

if (imag < 0)

cout << “Output Complex number: ” << real << imag << “i”;

else

cout << “Output Complex number: ” << real << “+” << imag << “i”;

}

};

int main() {

Complex complex1, complex2, result;

cout << “Enter first complex number:\n”;

complex1.input();

cout << “Enter second complex number:\n”;

complex2.input();

// complex1 calls the operator function

// complex2 is passed as an argument to the function

result = complex1 + complex2;

result.output();

return 0;

}

Đầu ra

Enter first complex number:

Enter real and imaginary parts respectively: 9 5

Enter second complex number:

Enter real and imaginary parts respectively: 7 6

Output Complex number: 16+11i

Trong chương trình này, hàm toán tử là:

Complex operator + (const Complex& obj) {

// code

}

Thay vào đó, ta cùng có thể viết hàm này như sau:

Complex operator + (Complex obj) {

// code

}

Tuy nhiên:

  • Sử dụng & làm cho code của tôi hiệu quả bằng cách tham chiếu đối tượng complex2 thay vì tạo một đối tượng trùng lặp bên trong hàm toán tử.
  • Dùng const cũng được xem là lựa chọn tốt vì nó ngăn không cho hàm toán tử sửa đổi complex2.

https://cdn.programiz.com/sites/tutorial2program/files/cpp-operator-overloading.png

Những điều cần nhớ về toán tử nạp chồng trong C++

  1. Hai toán tử = và & đã được nạp chồng mặc định trong C++. Ví dụ: để sao chép các đối tượng của cùng một lớp, ta có thể sử dụng trực tiếp toán tử = mà không cần tạo một hàm toán tử.
  2. Việc nạp chồng toán tử không thể thay đổi mức độ ưu tiên và tính liên kết của các toán tử. Tuy nhiên, nếu chúng ta muốn thay đổi thứ tự đánh giá thì nên sử dụng dấu ngoặc đơn.
  3. Có 4 toán tử không thể nạp chồng trong C++, đó là:
  • :: (phân giải phạm vi)
  • . (member selection)
  • .* (member selection thông qua con trỏ đến hàm)
  • ?: (toán tử bậc ba)

Trên đây là những kiến thức cơ bản về Class trong C++. Hi vọng bài viết này sẽ đem đến cho bạn cái nhìn rõ ràng hơn về Class trong C++ để từ đó bạn có thể tạo các đối tượng và sử dụng chúng trong C++ một cách nhanh chóng và hiệu quả.

Để lại một câu trả lời