Bài 13: Trừu tượng hóa dữ liệu (phần 2) - Cài đặt Struct nâng cao
Đây là phần 2 của bài học về Cấu trúc (Struct) trong C++. Để hiểu rõ bài viết này, mời bạn đọc hãy xem lại bài viết phần 1 tại đây.
I. Hàm khởi tạo (Constructor)
1. Hàm khởi tạo mặc định (Default constructor)
Khi tạo ra một biến kiểu struct
, ta thường đơn giản khai báo bằng cú pháp {Tên_cấu_trúc} {Tên_biến_cấu_trúc};
(đã học ở bài trước). Tuy nhiên, trên thực tế khi ta tạo một biến cấu trúc, thì chương trình C++ đã ngầm gọi một hàm đặc biệt, có vai trò khởi tạo giá trị ban đầu cho các biến thành viên của struct
, hàm này gọi là Hàm khởi tạo (Constructor).
Lấy ví dụ, ta có một struct
như sau:
struct Book
{
int id;
string book_name, author;
};
Nếu ta khai báo một biến kiểu theo cú pháp cơ bản ta sẽ viết:
Book book1;
Đối với cách khai báo này, một biến được tạo ra mà không có các giá trị khởi tạo cho các trường của struct
. Tuy nhiên, khi ta tạo ra struct
thì phiên bản đầy đủ của nó sẽ là như sau:
struct Book
{
int id;
string book_name, author;
Book()
{
id = 0;
book_name = author = ""; // Chuỗi rỗng.
}
};
Khi tạo một biến kiểu struct
mà không cung cấp giá trị khởi tạo cho các trường, một hàm khởi tạo mặc định sẽ được gọi, hàm này có vai trò khởi tạo các trường của biến tạo ra bằng giá trị mặc định của chúng (nếu có). Trong trường hợp này, hàm Book()
là hàm khởi tạo mặc định của struct
sẽ được gọi, nhưng ta không cần viết hàm này khi khai báo struct
vì nó được gọi mặc định. Vì thế, khi ta thử in ra giá trị của các trường trong biến ta vẫn sẽ thu được kết quả và xâu rỗng.
2. Hàm khởi tạo có tham số (Parameterized constructor)
Khi muốn tạo ra các biến struct
đồng thời truyền vào dữ liệu luôn cho các trường thành viên, ta có thể sử dụng các hàm khởi tạo có tham số. Cú pháp hàm khởi tạo này như sau:
{Tên_struct}({Danh_sách_tham_số_truyền_vào})
{
{Khởi_tạo_các_trường_bằng_tham_số_tương_ứng};
}
Khi đã sử dụng hàm khởi tạo có tham số, các bạn nên khai báo thêm một hàm khởi tạo mặc định rỗng để tránh gặp các lỗi phát sinh khi khai báo biến struct
. Hàm khởi tạo rỗng này giúp ta vẫn có thể khai báo được một biến mà không truyền vào tham số nào cả.
Chẳng hạn, nếu ta muốn một hàm khởi tạo trước các giá trị và cho struct
cú pháp như sau:
struct Book
{
int id;
string book_name, author;
Book() {}
Book(int _id, string _book_name)
{
id = _id;
book_name = _book_name;
}
};
Và khi khởi tạo một biến struct
, ta sẽ đồng thời truyền vào cho nó các giá trị tương ứng với hàm khởi tạo đã viết (vẫn có thể không dùng tham số, vì hàm khởi tạo mặc định vẫn tồn tại):
Book book1; // Sử dụng hàm khởi tạo mặc định.
Book book2(1, "Đất rừng phương Nam"); // Sử dụng hàm khởi tạo có tham số.
Lúc này, sẽ có và các trường là xâu rỗng; còn sẽ có và trường Đất rừng phương Nam
.
Trong cùng một struct
có thể tồn tại nhiều hàm khởi tạo có tham số với số lượng tham số khác nhau, phục vụ cho các mục đích khởi tạo khác nhau. Với struct
nói trên, ta có thể thêm một vài hàm khởi tạo khác như sau:
struct Book
{
int id;
string book_name, author;
Book(int _id)
{
id = _id;
}
Book(int _id, string _book_name)
{
id = _id;
book_name = _book_name;
}
Book(int _id, string _book_name, string _author)
{
id = _id;
book_name = _book_name;
author = _author;
}
};
Lúc này, ta có thể khởi tạo một biến kiểu theo 4 cách khác nhau:
Book book1; // Constructor mặc định.
Book book2(1); // Constructor số 1.
Book book3(2, "Đất rừng phương Nam"); // Constructor số 2.
Book book4(3, "Lều chõng", "Ngô Tất Tố"); // Constructor số 3.
Tuy nhiên, kĩ thuật này không cần thiết lắm trong các bài toán của lập trình thi đấu. Thay vào đó, ta có thể viết trực tiếp một hàm thành viên để nhập dữ liệu trong thân struct
, các bạn sẽ hiểu rõ hơn qua phần II.
II. Các hàm thành viên của Struct
Không chỉ đơn giản là lưu trữ một bản ghi gồm nhiều dữ liệu khác nhau, struct
trong C++ còn có sức mạnh vô cùng to lớn trong việc cài đặt các cấu trúc dữ liệu tích hợp cùng các thao tác của nó.
Hãy tưởng tượng các bạn có một cấu trúc thể hiện đối tượng "Con mèo". Một con mèo sẽ có các thông tin: Tên, tuổi, giống loài; và ta có thể thực hiện các hành động sau với một con mèo: Kêu, Đưa ra tên, Đưa ra giống loài và Tính giá tiền phụ thuộc vào giống loài.
Tất cả những thông tin và hành động trên có thể được đóng gói lại trong một struct
thể hiện con mèo như sau:
struct Cat
{
string name;
int age;
string type;
Cat(string _name, int _age, string _type)
{
name = _name;
age = _age;
type = _type;
}
void shout()
{
cout << "Meow...meow" << endl;
}
void get_name()
{
cout << name << endl;
}
void get_type()
{
cout << type << endl;
}
int get_cost()
{
if (type == "Short-haired")
return 10000;
else if (type == "Long-haired")
return 20000;
else if (type == "Short-legged")
return 30000;
}
};
Cách lập trình với struct
như trên là một phần của kĩ thuật Lập trình hướng đối tượng (OOPs), thường sẽ được đào tạo ở bậc Đại học. Trong kĩ thuật này, ta sẽ tạo ra một struct
đi kèm với các dữ liệu và các hành động sẽ thao tác với dữ liệu đó (dưới dạng các hàm thành viên của struct
). Ở đây chúng ta chỉ học một phần rất nhỏ của kĩ thuật này để giúp cho việc sử dụng struct
được chuyên nghiệp và có nhiều ích lợi hơn.
Sau khi tạo ra một struct
hoàn chỉnh như trên, giờ ta sẽ tạo ra một biến thể hiện một con mèo:
Cat cat1("Tom", "2", "Short-haired");
Các hàm thành viên của một struct
cũng được truy cập bằng toán tử .
giống như các trường thông tin. Mỗi khi một biến kiểu được tạo ra, nó sẽ sở hữu toàn bộ các thông tin cũng như các hàm được cài đặt ở struct
:
cat1.shout();
cat1.get_name();
cat1.get_type();
cout << cat1.get_cost() << endl;
Kết quả chạy đoạn chương trình:
Meow...meow
Tom
Short-haired
10000
Như vậy, mỗi khi chương trình phát sinh lỗi liên quan tới các biến thuộc kiểu ta sẽ chỉ tập trung sửa lỗi ở bên trong struct
mà thôi. Điều này giúp chương trình dễ dàng bảo trì, sửa lỗi, bởi vì khi cần thay đổi một phần của chương trình, ta chỉ cần tập trung sửa đổi logic của những struct
liên quan tới phần đó mà không cần quan tâm tới các struct
khác.
III. Bài tập áp dụng
1. Thao tác phân số
Đề bài
Cho hai phân số và .
Yêu cầu: Hãy in ra tổng, hiệu, tích và thương của hai phân số trên?
Input:
- Một dòng duy nhất chứa bốn số nguyên .
Ràng buộc:
- .
Output:
- In ra trên bốn dòng, mỗi dòng là một phân số lần lượt thể hiện tổng - hiệu - tích - thương của hai phân số đã cho. Các kết quả không cần phải tối giản.
Sample Input:
3 5
4 9
Sample Output:
47 45
7 45
12 45
27 20
Ý tưởng
Đây là một bài toán rất đơn giản, chúng ta hoàn toàn có thể khai báo một struct
thể hiện phân số, rồi viết các hàm đơn lẻ để thực hiện từng phép toán theo yêu cầu đề bài.
Tuy nhiên, việc lập trình sẽ trở nên khoa học hơn, nếu như ta "đính kèm" các phép toán vào trong struct
thể hiện phân số, như vậy struct
này sẽ bao gồm luôn cả các thao tác cộng, trừ, nhân và chia hai phân số. Thậm chí cả thao tác nhập xuất dữ liệu cho phân số cũng nên được cài đặt dưới dạng một hàm thành viên trong struct
.
Trong code mẫu bên dưới, hai hàm nhập xuất sẽ được cài đặt luôn trong struct
, vì thế nên hàm khởi tạo là không cần thiết. Khi nhập dữ liệu cho phân số, ta sẽ gọi hàm input
thay vì nhập dữ liệu ở ngoài rồi khai báo phân số thông qua hàm khởi tạo.
Các hàm tính toán sẽ được cài đặt một cách hơi đặc biệt:
- Do tổng, hiệu, tích và thương của hai phân số cũng lại là một phân số, nên các hàm thao tác sẽ có kiểu trả về chính là kiểu phân số (ở đây là
struct
). - Trong các hàm này, tham số truyền vào lại là một phân số khác (biến ). Tức là khi gọi hàm cộng của phân số chẳng hạn, thì thực tế ta cần cộng với một phân số . Vì vậy, các hàm này sẽ có tham số thể hiện rằng phép tính này được thực hiện giữa hai
struct
cùng kiểu . Các tham số được truyền kiểu tham chiếu để đẩy nhanh tốc độ chương trình, và thêm từ khóaconst
để tránh bất kì sự thay đổi nào tác động lên tham số thực sự ở bên ngoài. - Kết quả của các hàm sẽ được lưu vào một phân số, nên ta lại khai báo một biến kiểu để lưu kết quả, rồi trả ra chính biến đó cho hàm. Hoặc có thể trả về kết quả bằng cặp dấu
{}
để thể hiện kết quả có nhiều hơn trường dữ liệu.
Các cài đặt cụ thể khác, các bạn hãy xem ở code mẫu để hiểu rõ hơn!
Code mẫu
#include <bits/stdc++.h>
using namespace std;
struct Frac
{
int a, b;
void input()
{
cin >> a >> b;
}
void output()
{
cout << a << ' ' << b << endl;
}
// Cộng phân số này với phân số other.
Frac add(const Frac& other)
{
Frac sum;
sum.a = a * other.b + b * other.a;
sum.b = b * other.b;
return sum;
// Có thể viết ngắn: return {a * other.b + b * other.a, b * other.b};
}
// Trừ phân số này với phân số other.
Frac subtract(const Frac& other)
{
Frac sub;
sub.a = a * other.b - b * other.a;
sub.b = b * other.b;
return sub;
// Có thể viết ngắn: return {a * other.b - b * other.a, b * other.b};
}
// Nhân phân số này với phân số other.
Frac multiply(const Frac& other)
{
Frac mul;
mul.a = a * other.a;
mul.b = b * other.b;
return mul;
// Có thể viết ngắn: return {a * other.a, b * other.b};
}
// Chia phân số này với phân số other.
Frac divide(const Frac& other)
{
Frac div;
div.a = a * other.b;
div.b = b * other.a;
return div;
// Có thể viết ngắn: return {a * other.b, b * other.a};
}
};
int main()
{
// Khai báo hai phân số f1, f2 đại diện cho a/b và c/d. Sau đó gọi các hàm.
Frac f1, f2;
f1.input();
f2.input();
Frac sum = f1.add(f2);
Frac sub = f1.subtract(f2);
Frac mul = f1.multiply(f2);
Frac div = f1.divide(f2);
sum.output();
sub.output();
mul.output();
div.output();
return 0;
}
2. Xử lý tam giác
Đề bài
Cho một tam giác trong mặt phẳng tọa độ . Mỗi đỉnh của tam giác được thể hiện bằng hoành độ và tung độ.
Yêu cầu: Hãy tính diện tích tam giác, đồng thời cho biết tam giác này có dạng đều, vuông cân, cân, vuông hay thường?
Input:
- Gồm ba dòng, mỗi dòng gồm hai số nguyên thể hiện tọa độ một đỉnh của tam giác.
Ràng buộc:
- .
- Dữ liệu đảm bảo ba đỉnh cho trước tạo thành một tam giác.
Output:
- Dòng đầu tiên in ra diện tích tam giác, làm tròn tới chữ số sau dấu chấm thập phân.
- Dòng thứ hai in ra dạng của tam giác là
Deu
,Vuong can
,Can
,Vuong
hoặcThuong
tương ứng.
Sample Input:
1 2
5 5
5 -1
Sample Output:
12.00
Can
Ý tưởng
Trong bài toán này, có tới hai dạng dữ liệu có thể được trừu tượng hóa thành các struct
: Điểm trên mặt phẳng tọa độ và Tam giác.
Đầu tiên, ta sẽ viết một struct
để thể hiện một điểm trên mặt phẳng tọa độ, đi kèm với các thao tác nhập dữ liệu và tính khoảng cách giữa hai điểm (bởi vì ta nhận thấy sẽ phải dùng tới công thức này trong khi làm việc với tam giác).
Tiếp đến, struct
sẽ đại diện cho tam giác trong mặt phẳng tọa độ và nó sẽ có thành viên là điểm kiểu . Ngoài ra, khoảng cách giữa các điểm này (chính là các cạnh tam giác) cũng sẽ phải sử dụng, nên ta sử dụng thêm các biến phụ là để lưu độ dài các cạnh tam giác. Giá trị của biến này được khởi tạo ngay khi nhập dữ liệu các đỉnh xong.
Việc nhập dữ liệu cho các đỉnh tam giác thực tế là gọi lại hàm nhập dữ liệu điểm đủ lần, vì vậy hàm input()
trong struct
sẽ gọi lại hàm input()
trong struct
. Tương tự với ba biến .
Để tính diện tính tam giác, ta sử dụng công thức Heron:
<center>
Với là nửa chu vi của tam giác
</center>Cuối cùng, phần phức tạp nhất là kiểm tra loại tam giác. Ta biết rằng, để kiểm tra tính chất đều, cân hay vuông đều sẽ phải sử dụng thao tác so sánh bằng giữa các cạnh của tam giác. Tuy nhiên, do các giá trị đều là các số thực, nếu như so sánh trực tiếp chúng với nhau bằng toán tử ==
sẽ không chính xác do vấn đề sai số của số thực. Để giải quyết điều này, ta sẽ bình phương các giá trị lên rồi so sánh với nhau, như vậy sẽ tránh được vấn đề số thực sinh ra do hàm sqrt()
.
Code mẫu
#include <bits/stdc++.h>
using namespace std;
struct Point
{
int x, y;
void input()
{
cin >> x >> y;
}
double sqr(int n)
{
// Trả về n^2, nhân thêm với 1.0 để ép kiểu thành số thực.
return n * n * 1.0;
}
double distance(const Point& other)
{
return sqrt(sqr(x - other.x) + sqr(y - other.y));
}
};
struct Triangle
{
Point a, b, c;
double d1, d2, d3;
void input()
{
a.input();
b.input();
c.input();
d1 = a.distance(b);
d2 = b.distance(c);
d3 = c.distance(a);
}
double get_area()
{
double p = (d1 + d2 + d3) / 2;
return sqrt(p * (p - d1) * (p - d2) * (p - d3));
}
string get_type()
{
// Bình phương d1, d2, d3 lên để loại bỏ phần thập phân.
d1 = (int) (d1 * d1);
d2 = (int) (d2 * d2);
d3 = (int) (d3 * d3);
if (d1 == d2 && d2 == d3)
return "Deu";
else if ((d1 == d2 || d2 == d3) && (d1 == d2 + d3 || d2 == d1 + d3 || d3 == d1 + d2))
return "Vuong can";
else if (d1 == d2 || d2 == d3 || d1 == d3)
return "Can";
else if (d1 == d2 + d3 || d2 == d1 + d3 || d3 == d1 + d2)
return "Vuong";
else
return "Thuong";
}
};
int main()
{
Triangle t;
t.input();
cout << fixed << setprecision(2) << t.get_area() << endl;
cout << t.get_type();
return 0;
}
IV. Tài liệu tham khảo
All rights reserved