+5

Bài 13: Trừu tượng hóa dữ liệu (phần 1) - Giới thiệu Struct và các thao tác cơ bản

I. Kết hợp các dữ liệu khác kiểu với nhau

Xét trường hợp chúng ta cần lưu trữ thông tin của nhiều cuốn sách trong thư viện, mỗi cuốn sách có nhiều loại thông tin như: Tiêu đề, tác giả, thể loại và ID. Trong các kiểu dữ liệu nguyên thủy ta đã học, không có kiểu nào có thể lưu trữ cùng lúc nhiều thông tin như thế. Khi đó, lập trình viên sẽ phải tìm cách tạo ra các kiểu dữ liệu mới để lưu trữ tất cả thông tin của một cuốn sách (là kiểu dữ liệu tự định nghĩa - đã đề cập các bài trước). Trong C++, Cấu trúc (struct)Lớp (class) chính là hai loại dữ liệu hỗ trợ cho người dùng làm như vậy.

Tuy nhiên, trong phạm vi của lập trình cơ bản, struct sẽ được sử dụng chủ yếu trong việc tạo ra các kiểu dữ liệu tự định nghĩa, và mức độ sử dụng cũng rất đơn giản. Ta có thể hiểu struct là một dạng bản ghi, dùng để lưu trữ những thông tin chung nhất của nhiều đối tượng dữ liệu, ví dụ như mọi bản ghi của các sinh viên đại học trong hệ thống của nhà trường đều phải có tên, mã số sinh viên, ngày tháng năm sinh và ngành học; hoặc như ví dụ về lưu trữ các cuốn sách ở trên,...

II. Định nghĩa một cấu trúc

1. Định nghĩa và khai báo các biến cấu trúc

Để định nghĩa một cấu trúc, ta sử dụng cú pháp như sau:

struct {Tên_cấu_trúc}
{
    {Định_nghĩa_thành_viên_1};
    {Định_nghĩa_thành_viên_2};
    ...
    {Định_nghĩa_thành_viên_n};
} {Danh_sách_các_biến_cấu_trúc};

Trong đó, {Tên_cấu_trúc} là một định danh tùy ý người dùng đặt (thông thường tên cấu trúc sẽ viết hoa chữ cái đầu tiên). {Định_nghĩa_thành_viên} là một khai báo biến nào đó ở các kiểu dữ liệu khác nhau, còn {Danh_sách_các_biến_cấu_trúc} là các định danh tên biến.

Ví dụ: Định nghĩa một cấu trúc Books\text{Books} lưu thông tin về tên, tác giả, thể loại và ID của một cuốn sách, sau đó khai báo ba biến cấu trúc thể hiện ba cuốn sách:

struct Books
{
    int book_id;
    string book_name, author, category; 
} book_1, book_2, book_3;

Ngoài cách khai báo liền các biến cấu trúc như trên, người dùng có thể khai báo biến cấu trúc ở bất kỳ đâu trong chương trình bằng cú pháp: {Tên_cấu_trúc} {Tên_biến_cấu_trúc}; như ví dụ dưới đây:

int main()
{
    Books book_1, book_2, book_3;
	
    return 0;
}

2. Khai báo các biến cấu trúc với từ khóa typedef

Từ khóa typedef là một từ khóa rất hữu ích trong lập trình thi đấu, nó giúp người dùng đặt ra các tên mới cho những kiểu dữ liệu, bao gồm cả kiểu dữ liệu nguyên thủy lẫn kiểu dữ liệu người dùng tự định nghĩa. Bạn có thể định nghĩa một struct với tên cố định bằng cú pháp:

typedef struct
{
    {Định_nghĩa_các_thành_viên_của_cấu_trúc};
} {Tên_cấu_trúc};

Ví dụ, cấu trúc Books\text{Books} ở trên có thể viết lại như sau:

typedef struct 
{
    int book_id;
    string book_name, author, category; 
} Books;

Kể từ khai báo này trở đi, các biến cấu trúc cũng có thể được khai báo bằng cú pháp giống như khai báo một biến kiểu nguyên thủy thông thường:

Books book_1, book_2, book_3;

Tuy nhiên, cách khai báo này với struct không quá phổ biến vì nó không giúp cho lập trình viên giảm được về công sức lập trình. Chỉ khi ta có những kiểu dữ liệu quá dài, ví dụ như pair hay unsigned long long mà không muốn viết dài như vậy, thì từ khóa typedef sẽ phát huy tác dụng, bằng cách đặt lại tên mới ngắn gọn hơn cho kiểu đó:

typedef unsigned long long ULL;

int main()
{
    ULL a, b; // Thay vì khai báo unsigned long long thì có thể sử dụng định danh ULL.
}

III. Các thao tác cơ bản với cấu trúc

1. Truy cập các thành viên của một biến cấu trúc

Để truy cập tới các thành viên trong một cấu trúc, ta sử dụng cú pháp sau:

{Tên_biến_cấu_trúc}.{Tên_thành_viên}

Khi truy cập theo cách này, người dùng có thể sử dụng thành viên được truy cập tới như một biến đơn thông thường, kết hợp vào các phép toán và câu lệnh khác nhau. Ví dụ, ta có thể nhập xuất tên sách của biến cấu trúc book_1\text{book\_1} có kiểu là Books\text{Books} bằng cú pháp:

int main()
{
    Books book_1;

    cin >> book_1.book_name;	
    cout << book_1.book_name;
}

2. Gán giá trị cho một biến cấu trúc

Biến cấu trúc cung cấp một phép toán gán rất hữu ích. Khi muốn gán giá trị cho các thành viên của một biến cấu trúc gồm nn thành viên, ta có thể sử dụng cú pháp sau:

{Tên_biến_cấu_trúc} = { {Giá_trị_1}, {Giá_trị_2},..., {Giá_trị_n} };

Khi đó, các {Giá_trị_1}, {Giá_trị_2},..., {Giá_trị_n} sẽ lần lượt được gán cho các {Thành_viên_1}, {Thành_viên_2},..., {Thành_viên_n} của biến struct. Lấy ví dụ:

#include <bits/stdc++.h>

using namespace std;

struct Books
{
    string book_name;
    int book_id, cost;
};

int main()
{
    Books book_1 = {"Truyện Cổ tích Việt Nam", 1, 25000}; // Gán giá trị cho các thành viên cấu trúc.
    
    cout << "Mã số sách là: " << book_1.book_id << endl;
    cout << "Tên sách là: " << book_1.book_name << endl;
    cout << "Giá bán sách: " << book_1.cost << endl;
}

Biên dịch và chạy đoạn chương trình trên, ta có kết quả là:

Mã số sách là: 1
Tên cuốn sách là: Truyện Cổ tích Việt Nam
Giá bán sách: 25000

3. Truyền cấu trúc vào hàm như một tham số

Ta có thể truyền cấu trúc vào hàm dưới dạng một tham số giống như những biến thông thường, và cũng sẽ có truyền tham chiếu hoặc truyền tham trị. Cú pháp truyền không có gì khác biệt:

{Kiểu_hàm} {Tên_hàm}( {Tên_cấu_trúc} {Tên_biến_tham_số} )

Nếu như muốn truyền tham chiếu, chỉ cần thêm toán tử & phía trước tên biến tham số. Còn các đối tượng trong cấu trúc thì chỉ giống như biến thông thường nên có thể kết hợp với các câu lệnh như các biến.

Ví dụ: Dưới đây là một đoạn chương trình cho phép nhập vào chiều dài và chiều rộng của NN hình chữ nhật khác nhau và sử dụng một hàm để tính diện tích của các hình chữ nhật đó:

#include <bits/stdc++.h>

using namespace std;

struct Rectangle // Cấu trúc lưu thông tin về hình chữ nhật.
{
    int length; // Chiều dài.
    int width; // Chiều rộng.
    int area; // Diện tích.
} rec[50]; // Định nghĩa một mảng chứa các hình chữ nhật.

void get_area(Rectangle & rec) // Tính diện tích của một HCN.
{
    rec.area = rec.length * rec.width;
}

int main()
{
    cin >> N;
    for (int i = 1; i <= N; ++i)
    {
	cin >> rec[i].length >> rec[i].width;

	get_area(rec[i]); // Truyền tham trị để cập nhật trực tiếp diện tích lên thành viên area của biến rec[i].
    }
    
    for (int i = 1; i <= N; ++i)
    {
        cout << "Diện tích hình chữ nhật thứ " << i << " là: ";
	cout << rec[i].area << endl;
    }
}  

Hãy thử biên dịch và chạy chương trình trên với Input dưới đây:

3
15 16
10 5
3 4

Chúng ta sẽ thu được output như sau:

Diện tích hình chữ nhật thứ 1 là: 240
Diện tích hình chữ nhật thứ 2 là: 50
Diện tích hình chữ nhật thứ 3 là: 12

IV. Bài tập minh họa

1. Quản lý sinh viên

Đề bài

Định nghĩa một cấu trúc Student để quản lý sinh viên của một lớp học gồm NN sinh viên khác nhau. Các thông tin cần quản lý bao gồm: Mã số sinh viên, Năm sinh, Điểm thi giữa kỳ, Điểm thi cuối kỳ, Điểm trung bình, Xếp hạng. Cho trước các thông tin: Mã số sinh viên, Năm sinh, Điểm thi giữa kỳ, Điểm thi cuối kỳ của NN sinh viên theo thứ tự từ 11 tới NN.

Yêu cầu: Hãy viết các hàm thực hiện các công việc sau:

  • Đưa ra số lượng sinh viên có năm sinh từ 19991999 trở về sau.
  • Tính điểm trung bình của mỗi sinh viên theo công thức:

For performance reasons, math blocks are limited to 1000 characters. Try splitting up this block, or include an image instead.

  • Tính diện tích tam giác với tọa độ ba đỉnh lần lượt là (x1,y1),(x2,y2)(x_1, y_1), (x_2, y_2) và (x3,y3)(x_3, y_3):

    (x2x1)×(y3y1)(x3x1)×(y2y1)2\frac{\big|(x_2 - x_1) \times (y_3 - y_1) - (x_3 - x_1) \times (y_2 - y_1)\big|}{2}

Cài đặt

#include <bits/stdc++.h>

using namespace std;

struct point
{
    int x, y;
};

struct triangle
{
    point p1, p2, p3;
};

// Nhập tọa độ điểm trong tam giác.
void input(triangle& t)
{
    cin >> t.p1.x >> t.p1.y;
    cin >> t.p2.x >> t.p2.y;
    cin >> t.p3.x >> t.p3.y;
}

// Tính khoảng cách giữa hai điểm trên mặt phẳng tọa độ.
double distance(point p1, point p2)
{
    return sqrt((p1.x - p2.x) * (p1.x - p2.x) * 1.0 + (p1.y - p2.y) * (p1.y - p2.y) * 1.0);
}

// Kiểm tra xem ba điểm đã nhập vào có thỏa mãn là một tam giác hay không.
bool check_triangle(double d1, double d2, double d3)
{
    return (d1 > 0 && d2 > 0 && d3 > 0 && d1 + d2 > d3 && d1 + d3 > d2 && d2 + d3 > d1);
}

// Tính chu vi tam giác.
double calc_perimeter(double d1, double d2, double d3)
{
    return d1 + d2 + d3;
}

// Tính diện tích tam giác.
double calc_area(triangle t)
{
    return 0.5 * fabs((t.p2.x - t.p1.x) * (t.p3.y - t.p1.y) * 1.0 - (t.p3.x - t.p1.x) * (t.p2.y - t.p1.y) * 1.0);
}

int main()
{
    Triangle t;
    input(t);

    if (!check_triangle(t))
        cout << "NO";
    else
        cout << "YES" << endl << fixed << setprecision(3) << calc_perimeter(t) << ' ' << calc_area(t);
        
    return 0;
}

Để tiếp tục theo dõi phần 2 của bài viết về Cấu trúc trong C++, mời các bạn nhấn vào đây.

V. Tài liệu tham khảo


©️ Tác giả: Vũ Quế Lâm từ Viblo


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí