Kiểu cấu trúc (structs)
####Chào các bạn đang theo dõi khóa học lập trình trực tuyến C++.
Như các bạn đã biết, việc lập trình ứng dụng phần mềm chỉ đơn giản là tạo ra chương trình máy tính dùng để giải quyết một vấn đề nào đó trong cuộc sống. Để giải quyết được vấn đề, chúng ta cần cung cấp cho chương trình một lượng dữ liệu cần thiết, các câu lệnh sẽ xử lý các dữ liệu được đưa vào và cho ra kết quả mà người dùng mong muốn.
Dữ liệu chúng ta thu thập được trong cuộc sống cần được biểu diễn theo một định dạng nào đó mà máy tính có thể hiểu được, và ngôn ngữ lập trình cung cấp cho chúng ta điều này, đó chính là kiểu dữ liệu. Mỗi kiểu dữ liệu sẽ có một định dạng khác nhau, và khi tạo ra những thực thể từ những kiểu dữ liệu khác nhau, chúng sẽ có định dạng khác nhau dựa trên kiểu dữ liệu mô tả chúng. Ví dụ kiểu số thực (float, double) sẽ có định dạng phần thập phân trong khi kiểu số nguyên (int, long, …) thì không có.
Việc chọn kiểu dữ liệu phù hợp để lưu trữ dữ liệu cần xử lý là rất quan trọng. Nhưng số lượng kiểu dữ liệu mà một ngôn ngữ lập trình (trong đó có C++) hổ trợ sẵn là khá hạn chế trong khi có những dữ liệu đặc tả cho vấn đề trong cuộc sống lại rất phức tạp. Ví dụ mình muốn biểu diễn một vài thông tin cá nhân của mình trên máy tính, mình có thể làm như sau:
std::string name;
std::string currentJob;
std::string homeAddress;
int birthYear;
int birthMonth;
int birthDay;
float height;
float weight;
//...............
Nhìn vào đoạn chương trình trên, các bạn có thể thấy mình cần sử dụng đến 3 kiểu dữ liệu khác nhau để tạo ra 8 biến chỉ để lưu trữ một lượng thông tin cá nhân không đầy đủ của một cá thể nào đó. Những biến này hoàn toàn độc lập với nhau, giả sử những biến này được khai báo tại những vị trí khác nhau trong chương trình sẽ rất khó quản lý. Và sẽ rắc rối hơn nếu chúng ta muốn lưu trữ thông tin cá nhân của nhiều hơn một người, lúc này chúng ta cần khai báo thêm 8 biến tương tự như trên, hoặc sử dụng mảng một chiều như sau:
std::string name[10];
std::string currentJob[10];
std::string homeAddress[10];
int birthYear[10];
int birthMonth[10];
int birthDay[10];
float height[10];
float weight[10];
//...............
Như các bạn thấy, việc quản lý chương trình trở nên phức tạp so với những người mới học lập trình. Do đó, việc tự định nghĩa một kiểu dữ liệu mới phù hợp cho đặc thù của chương trình của mỗi người là điều cần thiết. Rất may mắn, ngôn ngữ C++ hổ trợ chúng ta tự định nghĩa kiểu dữ liệu mới từ những kiểu dữ liệu built-in. Kiểu dữ liệu mới mà chúng ta sẽ định nghĩa được tạo thành từ một hoặc một nhóm kiểu dữ liệu xây dựng sẵn để tạo ra một tập hợp các biến thuộc cùng nhóm, những biến cùng nhóm này dùng để lưu trữ các dữ liệu có liên quan với nhau trong kiểu dữ liệu mới. Chúng ta gọi kiểu dữ liệu tập hợp này là struct.
###Struct
Một struct (viết tắt của structure) cho phép chúng ta nhóm nhiều biến của nhiều kiểu dữ liệu khác nhau để lưu trữ một tập hợp các dữ liệu cần thiết cho việc mô tả một đơn vị nào đó.
#####Khai báo struct
Để khai báo một cấu trúc mới (kiểu dữ liệu mới), chúng ta sử dụng từ khóa struct. Mặc dù một struct là một kiểu dữ liệu do lập trình viên tự định nghĩa, nó cũng cần được khai báo theo một cú pháp nhất định để compiler có thể hiểu được. Dưới đây là cú pháp để tạo ra một struct mới:
struct <name_of_new_type>
{
<variables>;
};
Trong đó:
- struct là từ khóa mà ngôn ngữ C++ cung cấp.
-
name_of_new_type
sẽ là tên của kiểu dữ liệu mới. Sau khi khai báo xong một struct, chúng ta có thể dùng tên struct để khai báo biến như những kiểu dữ liệu thông thường. -
variables
là danh sách các biến dùng để lưu trữ dữ liệu phù hợp với yêu cầu lưu trữ dữ liệu của một đơn vị nào đó.
Mình lấy một ví dụ để các bạn có thể dễ hình dung hơn:
#include <iostream>
#include <string>
struct VietNamPeople
{
__int32 ID;
std::string name;
__int16 age;
float height;
float weight;
bool isStudent;
};
Như vậy, mình vừa định nghĩa xong một struct có tên là VietNamPeople, struct này bây giờ được coi là một kiểu dữ liệu mới là một tập hợp các biến ID, name, age, height, weight và isStudent. Những biến này được đặt vào chung một nhóm và mỗi biến sẽ lưu trữ một phần thông tin của một đơn vị là một con người Việt Nam.
Các bạn cần lưu ý về phạm vi sử dụng của kiểu dữ liệu tự định nghĩa cũng tương tự phạm vi sử dụng của biến trong chương trình, nhưng việc định nghĩa kiểu dữ liệu không yêu cầu hệ điều hành cấp phát bộ nhớ nên hoàn toàn không làm ảnh hưởng đến tài nguyên của hệ thống. Do đó, chúng ta nên định nghĩa kiểu dữ liệu mới cho phạm vi toàn cục (global scope) để kiểu dữ liệu mới này có thể được sử dụng trong toàn bộ file, thậm chí là sử dụng trong những file mã nguồn khác trong cùng project.
Khi các biến được đặt trong struct, chúng ta gọi chúng là trường dữ liệu của struct (fields). Một trường dữ liệu là một thành phần trong tập hợp các biến lưu trữ dữ liệu cần thiết cho một đơn vị. Ví dụ kiểu dữ liệu VietNamPeople có 6 trường dữ liệu là ID, name, age, height, weight và isStudent. Compiler có thể hiểu rằng khi một biến được tạo ra từ kiểu dữ liệu VietNamPeople, ví dụ:
VietNamPeople leTranDat;
thì lúc này, leTranDat chỉ là một cái tên của một đơn vị được tạo thành từ tập hợp các trường dữ liệu ID, name, age, height, weight và isStudent mà mình đã định nghĩa cho kiểu dữ liệu VietNamPeople. Các bạn có thể hình dung như thế này:
Đây chỉ là hình ảnh minh họa cho việc tổ chức dữ liệu các trường bên trong một biến kiểu VietNamPeople sau khi được tạo ra. Trên thực tế các trường sẽ có kích thước khác so với dự đoán (khi đến phần nâng cao của struct mình sẽ trình bày về vấn đề này), nhưng đối với các bạn mới học thì chưa cần quan tâm những điều này.
Chúng ta có thể sử dụng kiểu dữ liệu VietNamPeople mà mình định nghĩa ở trên để tạo ra nhiều biến struct khác nhau:
VietNamPeople leTranDat;
VietNamPeople dayNhauHoc;
VietNamPeople ngoDoanTuan;
//.....................
Như những biến thông thường, các biến struct này sẽ được cấp phát bộ nhớ tùy vào cách chọn kỹ thuật cấp phát.
#####Khởi tạo cho biến struct
Khởi tạo giá trị cho các trường dữ liệu trong một biến struct rắc rối hơn khởi tạo giá trị cho biến thông thường một chút. Ngôn ngữ C++ đã hổ trợ cho chúng ta một cách nhanh hơn là sử dụng một initializer list. Nó cho phép các bạn khởi tạo một hoặc nhiều trường dữ liệu trong một khai báo biến struct. Ví dụ:
struct Employee
{
__int32 ID;
std::string name;
__int32 age;
__int32 year_of_exp;
};
//................
Employee leTranDat = { 1, "Le Tran Dat", 28, 5 };
Các trường dữ liệu được khởi tạo lần lượt từ trên xuống dưới như trong phần định nghĩa struct có tên Employee. Lúc này, biến leTranDat sẽ chứa các thông tin được khởi tạo lần lượt là: ID
= 1, name
= “Le Tran Dat”, age
= 28 và year_of_exp
= 5.
Nếu initializer list không cung cấp đủ dữ liệu cho các trường dữ liệu, giá trị mặc định sẽ được dùng để khởi tạo. Ví dụ:
Employee newEmp = { 1, "new employee" }; //age = 0, year_of_exp = 0 by default
Một tập hợp các giá trị của một biến struct được gọi là một Record (bản ghi).
#####Truy cập các trường dữ liệu của biến struct
Xem xét về struct Employee mình đã định nghĩa ở trên:
struct Employee
{
__int32 ID;
std::string name;
__int32 age;
__int32 year_of_exp;
};
Kiểu dữ liệu Employee mô tả rằng mỗi biến kiểu Employee được tạo ra sẽ bao gồm 4 trường dữ liệu là ID, name, age và year_of_exp
. Như vậy, bất kỳ biến nào có kiểu Employee đều có đủ 4 trường dữ liệu trên.
Muốn truy xuất đến các trường dữ liệu của một biến struct, chúng ta sử dụng member selection operator (dấu chấm). Dưới đây là một ví dụ:
Employee leTranDat;
leTranDat.ID = 1;
leTranDat.name = "Le Tran Dat";
leTranDat.age = 28;
leTranDat.year_of_exp = 5;
Visual studio sẽ hổ trợ chúng ta liệt kê tất cả các trường dữ liệu của một biến struct khi sử dụng member selection operator.
Các trường dữ liệu của một biến struct cũng là những biến thông thường, nhưng nó được gói gọn bên trong một biến struct, nên chúng ta phải sử dụng tên của biến struct và member selection operator để truy xuất đến chúng.
Như vậy, thông qua tên biến struct, các trường dữ liệu được nhóm lại giúp chúng ta biết được trường dữ liệu đó được dùng cho đơn vị nào, điều này giúp chúng ta dễ dàng tổ chức chương trình ở quy mô lớn hơn.
Vì các trường dữ liệu của một biến struct cũng là những biến thông thường, chúng ta cũng có thể sử dụng chúng để tính toán, so sánh, …
Employee leTranDat = { 1, "Le Tran Dat", 28, 5 };
Employee juniorEmp = { 2, "New employee", 25, 1 };
if (leTranDat.year_of_exp > juniorEmp.year_of_exp)
{
std::cout << leTranDat.name << " has more experience than " << juniorEmp.name << std::endl;
}
Các trường dữ liệu của một struct sẽ tồn tại cùng với biến struct cho đến khi biến struct ra khỏi phạm vi sử dụng và bị hủy. Do đó, khi biến struct còn tồn tại, chúng ta vẫn có thể truy xuất đến các trường dữ liệu của nó.
#####Nhập và xuất dữ liệu cho biến struct
Cũng tương tự như nhập xuất dữ liệu cho biến thông thường, chỉ khác là chúng ta cần sử dụng thêm tên biến struct và member selection operator để compiler biết chúng ta nhập xuất cho trường dữ liệu của đơn vị nào. Ví dụ:
Employee emp;
//Input
std::cout << "Enter ID: ";
std::cin >> emp.ID;
std::cout << "Enter name: ";
std::getline(std::cin, emp.name);
std::cout << "Enter age: ";
std::cin >> emp.age;
std::cout << "Enter year of experience: ";
std::cin >> emp.year_of_exp;
//Output
std::cout << "===================================" << std::endl;
std::cout << emp.ID << std::endl;
std::cout << emp.name << std::endl;
std::cout << emp.age << std::endl;
std::cout << emp.year_of_exp << std::endl;
std::cout << "===================================" << std::endl;
#####Structs và function
Một ưu điểm khi sử dụng struct là chúng ta không cần truyền tất cả các trường dữ liệu của một đơn vị nào đó mà chỉ cần sử dụng một biến struct làm tham số cho hàm. Ví dụ:
struct Vector2D
{
float x;
float y;
};
void printVector2D(Vector2D vec)
{
std::cout << "(" << vec.x << "," << vec.y << ")" << std::endl;
}
int main()
{
Vector2D vec = { 1, 4 };
printVector2D(vec);
return 0;
}
Trong ví dụ trên, mình sử dụng kiểu truyền dữ liệu giá trị nên tham số Vector2D vec
của hàm printVector2D không làm thay đổi giá trị gốc của đối số. Các bạn cũng có thể thử truyền đối số là biến struct theo kiểu tham chiếu hoặc con trỏ, về mặt cơ bản, biến struct cũng là một biến có địa chỉ cụ thể nên chúng ta làm hoàn toàn tương tự như biến thông thường.
void normalize(Vector2D &vec)
{
float length = sqrt((vec.x * vec.x) + (vec.y * vec.y));
vec.x = vec.x / length;
vec.y = vec.y / length;
}
int main()
{
Vector2D vec = { 1, 4 };
printVector2D(vec);
normalize(vec);
printVector2D(vec);
return 0;
}
Kiểu struct cũng có thể được dùng làm kiểu trả về của hàm. Ví dụ:
Vector2D addTwoVector(Vector2D vec1, Vector2D vec2)
{
Vector2D result = { vec1.x + vec2.x, vec1.y + vec2.y };
return result;
}
int main()
{
Vector2D vec1 = { 1, 2 };
Vector2D vec2 = { 2, 2 };
Vector2D result = addTwoVector(vec1, vec2);
return 0;
}
Chúng ta có thể làm như trên vì ngôn ngữ C++ cho phép chúng ta gán biến struct cho một biến cùng kiểu struct khác. Ví dụ:
Vector2D vec1 = { 1, 2 };
Vector2D vec2 = vec1;
Nhưng chúng ta không nên sử dụng phép gán trực tiếp như vậy, vì có thể trong struct còn có các yếu tố phức tạp khác như con trỏ, hoặc struct khác, … và dễ gây ra sai sót. Quay trở lại với chủ đề mình đang trình bày.
Chúng ta còn có thể định nghĩa các hàm bên trong phần định nghĩa của struct. Ví dụ:
struct Vector2D
{
float x;
float y;
void normalize()
{
float length = sqrt(x * x + y * y);
x = x / length;
y = y / length;
}
};
Trong ví dụ trên, hàm normalize được định nghĩa trong cùng khối lệnh của struct Vector2D, nên nó có thể trực tiếp truy cập đến biến x và y và thao tác với chúng. Nhưng x và y của struct Vector2D vẫn đang còn ở mức khái niệm, chỉ khi nào struct Vector2D được dùng để khai báo biến, biến x và y cũng như hàm normalize mới được tạo ra. Như mình đã nói ở trên, việc định nghĩa một kiểu dữ liệu mới chỉ là định nghĩa những dữ liệu sẽ tồn tại trong biến struct nếu nó được tạo ra.
Hàm normalize được tạo ra nhưng chỉ được sử dụng khi một biến struct cụ thể gọi đến nó bằng member selection operator.
int main()
{
Vector2D vec = { 1, 4 };
vec.normalize();
printVector2D(vec);
return 0;
}
Hàm normalize trong struct Vector2D chỉ có thể được gọi thông qua một biến struct cụ thể. Như vậy, khi chúng ta muốn chuẩn hóa một Vector2D, chúng ta không cần truyền biến struct kiểu Vector2D vào hàm normalize(Vector2D)
nữa mà chỉ cần gọi hàm normalize
được định nghĩa trong chính nó. Đây cũng là một ưu điểm khi sử dụng struct.
Mình lấy thêm một ví dụ nữa về hàm được định nghĩa bên trong struct để các bạn dễ hình dung:
struct Vector2D
{
float x;
float y;
void setPosition(float X, float Y)
{
x = X;
y = Y;
}
void normalize()
{
float length = sqrt(x * x + y * y);
x = x / length;
y = y / length;
}
};
Mình vừa thêm vào struct Vector2D hàm setPosition(float, float)
, lúc này mình không cần sử dụng initializer list để khởi tạo cho một biến kiểu Vector2D nữa, mà mình sẽ gọi hàm setPosition(float, float)
.
int main()
{
Vector2D vec;
vec.setPosition(1, 4);
printVector2D(vec);
return 0;
}
Điều này cũng làm tăng thêm ý nghĩa cho mã nguồn chương trình, giúp code của các bạn dễ đọc hơn. Chúng ta sẽ còn nói đến việc định nghĩa hàm bên trong struct trong những bài học tiếp theo.
#####Nested structs
Struct là một tập hợp các kiểu dữ liệu dùng để tạo nên một kiểu dữ liệu mới, và một kiểu struct cũng là một kiểu dữ liệu, nên chúng ta có thể sử dụng một kiểu struct khác để làm một trường dữ liệu cho struct cần tạo ra. Ví dụ:
struct Birthday
{
__int32 day;
__int32 month;
__int32 year;
};
struct Employee
{
__int32 ID;
std::string name;
Birthday birthday;
__int32 year_of_exp;
};
Trong trường hợp này, mình thay thế trường dữ liệu age bằng một trường dữ liệu kiểu Birthday đã được định nghĩa ở trên. Chúng ta có thể khởi tạo giá trị cho nested struct trên như sau:
Employee emp = { 1, "Le Tran Dat", {1, 2, 2000}, 5 };
Để truy xuất đến giá trị thực của trường dữ liệu birthday, chúng ta cần sử dụng thêm 1 lần member selection operator.
Employee emp = { 1, "Le Tran Dat", {1, 2, 2000}, 5 };
std::cout << emp.ID << std::endl;
std::cout << emp.name << std::endl;
std::cout << emp.birthday.day << "/" << emp.birthday.month << "/" <<emp.birthday.year << std::endl;
std::cout << emp.year_of_exp << std::endl;
###Tổng kết
Trong bài học này, chúng ta đã cùng nhau tìm hiểu cách để tạo ra một kiểu dữ liệu mới bằng từ khóa struct cung cấp bởi ngôn ngữ C++, một số thao tác cơ bản với biến struct.
Struct là một khái niệm quan trọng trong ngôn ngữ C/C++, hiểu được structs là một bước quan trọng để tiếp cận hướng phát triển chương trình theo mô hình hướng đối tượng. Struct giúp chúng ta tổ chức chương trình hiệu quả hơn.
###Bài tập cơ bản
1/ Các bạn hãy định nghĩa kiểu dữ liệu PhanSo đại diện cho kiểu phân số. Qua đó, viết chương trình cho phép người dùng thực hiện các phép cộng, trừ, nhân, chia 2 phân số.
2/ Viết chương trình thực hiện phân tích thống kê cho một lớp học khoảng 20 sinh viên. Thông tin của mỗi sinh viên bao gồm ID, tên, tuổi, điểm tổng kết học kì 1, điểm tổng kết học kì 2. Những thông tin cần thống kê bao gồm:
- Điểm trung bình cuối năm của cả lớp.
- Điểm tổng kết cuối năm của sinh viên nào là cao nhất.
- Liệt kê danh sách những sinh viên có tiến bộ trong học tập (điểm tổng kết học kì 2 cao hơn điểm tổng kết học kì 1).
Hẹn gặp lại các bạn trong bài học tiếp theo trong khóa học lập trình C++ hướng thực hành.
Mọi ý kiến đóng góp hoặc thắc mắc có thể đặt câu hỏi trực tiếp tại diễn đàn.
www.daynhauhoc.com
ai có thể chỉ cho mình cách để cộng, trừ, nhân, chia 2 phân số này mà không dùng hàm không?
tức là chỉ cần dùng toán tử +, -, *, / là có thể tính được luôn ấy.
Cái này bạn phải định nghĩa toán tử cho nó nhé, dùng operator.
Phép trừ, nhân, chia cũng như thế
dạ mình cảm ơn nhiều ạ
Cho mình hỏi phần nâng cao của struct ở chỗ nào vậy admin? bạn nào biết chỉ mình nha. Cám ơn
Em làm bài 1 như thế này mong mọi người góp ý
bạn vào đây kham khảo nhé http://www.oktot.com/struct-phan-so/