Struct là gì? bản chất và cách sử dụng

Sử dụng struct trong chương trình

OHM, lần trước chúng ta đã bàn về Đệ Quy rồi, bây giờ chúng ta lại quay về vấn đề cơ bản của ngôn ngữ: STRUCT.
Trong bài này mình sẽ bàn về những vấn đề cơ bản nhất về struct và kèm theo những ví dụ cũng cơ bản không kém. Do bài dài nên mình không đủ thời gian dịch và update hằng ngày nên mọi người bookmark lại và đọc theo ngày nhé.
Bây giờ hãy mở đầu Struct bằng một ví dụ minh họa bên dưới nhé:

#include <iostream>
using namespace std;
struct inflatable
{
    char name[20];
    float volume;
    double price;
};

int main()
{
    inflatable guest =
    {
        "Glorious Gloria",    //namevalue
        1.88,                //volum value
        29.99                //price value
    };                        //guest is structure variable of type inflatable
    inflatable pal =
    {
        "Audacious Arthur",
        3.12,
        32.99
    };                        //pal is second variable of type inflatable
    //Note: som implementations require using static inflatable guest =
    cout << "Expand your guest list with " << guest.name;
    cout << " and " << pal.name << endl;
    //pal.name is the name member of the pal variable
    cout << "You can have both for $" << guest.price + pal.price << endl;
    return 0;
}

Code mình build bằng Visual 2013 nên nếu những ai build bằng IDE khác thì có thể thay đổi chút ít để phù hợp, và dưới đây là kết quả của đoạn code trên:

Expand your gues list with Glorious Gloria and Audacious Arthur!
You can have both for $62.98!

Lưu ý về chương trình
Một điều cực kỳ tối quan trọng là cách mà bạn khai báo dữ liệu Struct trong ví dụ trên. Có hai lựa chọn cho chương trình structur.cpp. Bạn có thể đặt khai báo bên trong hàm main() và ngoài hàm. Và bạn cũng nên nhớ rằng struct chúng ta cũng có thể gọi nó là 1 kiểu dữ liệu được định nghĩa riêng. Điều này cũng có nghĩa rằng nó cũng có dạng “Biến nội bộ” và “Biến toàn cục”. Dựa vào vị trí đặt của Struct thì chúng ta có thể suy ra đươc biến Struct này là biến nội bộ hay biến toàn cục. Tùy vào vị trí đặt Struct thì chúng ta có cách sử dụng khác nhau
Notice: Trong bài này mình tạm gọi Struct là 1 biến để ngắn gọn nhé.
Biến Struct toàn cục: là loại biến đặt bên ngoài hàm, được khai báo sau khi khai báo thư viện và namspace, loại biến này sử dụng được cho mọi hàm trong chương trình và chúng ta gọi tên tiếng anh mĩ miều là external declaration
Biến Struct cục bộ: là loại biến được đặt bên trong hàm, được khai báo trong 1 hàm cố định và không được truy xuất bởi hàm khác, nói cách khác đó là biến chỉ sử dụng được bởi các funtion đặt chung dấu {} với nó.

Ví dụ

int main()
{
    struct ID
    {
        int a;
        int b;
    };
}

Như các bạn thấy đấy, Struct cũng được máy tính đối xử như 1 biến vậy, nó có thể được khai báo cục bộ hoặc toàn cục. Với cách khai báo toàn cục thì nó có thể được sử dụng bởi tất cả các hàm.
Quay trở lại với ngôn ngữ C++, ngôn ngữ này không khuyến khích sử dụng biến toàn cục (external variable) nhưng nó khuyến khích sử dụng các struct extenal variable (có thể gọi là cấu trúc toàn cục).

Bây giờ chúng ta lại nhìn cách mình sử dụng struct bênt trên. Cách mà mình khởi tạo 1 biến:

inflatable guest = 
{ 
    "Glorious Gloria",    //namevalue
    1.88,                //volum value
    29.99                //price value
};    

Giống như cách bạn truyền tham số vào một mảng, bạn cần để trong một dấu {} và cách nhau bởi một dấu phẩy. Mỗi giá trị được đặt trong một dòng. Tuy nhiên bạn có thể đặt tất cả vào cùng 1 dòng mà không xảy ra lỗi. Nhưng nhớ rằng phải ngăn cách chúng bằng dấu phẩy, đó là quy tắc nếu bạn không muốn gặp lỗi khi compile bằng bất cứ complier nào.

inflatable duck = {"daphne", 0.12, 9.98};

Bạn có thể khởi tạo mỗi giá trị của struct bằng các kiểu dữ liệu thích hợp. Trong ví dụ mình khái báo bên trên, biến name mình đã khai báo một mãng kiểu char, nhưng nếu sử dụng C++ thì bạn có thể khai báo kiểu string, điều này không ảnh hưởng tới cách mà chương trình chạy.

Mỗi giá trị của Struct được đối xử như là một biến của kiểu dữ liệu. Như vậy, khi ta sử dụng biến pal.price thì đây là một biến kiểu double và pal.name là một kiểu char[]. Và khi mà ta sử dụng lệnh cout để xuất dữ liệu ra màn hình với biến pal.name thì máy tính sẽ tự động chọn kiểu string (hay char[]). Cũng như thế, pal.name cũng là một mảng ký tự và chúng ta có thể sử dụng nó như là một mảng. Khi bạn truy xuất giá trị tại pal.name[0] thì bạn sẽ nhận được một giá tị ‘A’, nhưng khi bạn truy xuất pal[0] thì nó hoàn toàn vô nghĩa bởi vì pal là một struct chứ không phải là một mảng (array).


KHAI BÁO STRUCT TRONG C++11


Giống như mảng, C++11 mở rộng cách mà ta có thể khai báo và sử dụng struct một cách linh hoạt. Thay vì chúng ta cần phải sử dụng dấu “=” thì nay chúng ta có thể quên nó đi.

inflatable duck {"daphne", 0.12, 9.98};

Tiếp theo, mỗi thành phần chưa được gán trị thay vì nó sẽ là một giá trị rác thì nay mặc định sẽ là 0. Ví dụ, khi mà khai báo một biến có kiểu inflatable mayor thì giá trị ta nhận về khi truy xuất mayor.price hay mayor.volume sẽ là 0.
Tuy nhiên ta có mayor.name được định nghĩa là một mảng ký tự thì tất cả các byte trong mảng mayor.name sẽ được đặt là 0 (hoặc gọi cách khác đây là ký tự NULL).
Cách mà ta khai báo biến mayor để có các giá trị trên là:

inflatable mayor{};

LIỆU STRUCT CÓ THỂ SỬ DỤNG ĐƯỢC CLASS STRING KHÔNG?

Liệu chúng ta có thể dùng các đối tượng bên trong Class String để sử dụng cho mảng ký tự name? Tất nhiên, bạn có thể khai báo một structure giống thế này:

struct inflatable
{
    std::string name;
    float volume;
    double price;
};

Tất nhiên câu trả lời là có thể rồi trừ khi bạn đang sử dụng một IDE hay compiler bản cũ hoặc đã lỗi thời từ lâu thì nó không được hỗ trợ khai báo structure với class string.

Hãy chắc chắn rằng structure bạn khai báo có quyền truy cập vào namespace std. Bạn có thể khai báo sau khi khai báo thư viện hoặc trong khi khai báo struct như cách mà mình làm bên trên, sử dụng: std: :string

MỘT SỐ THUỘC TÍNH KHÁC CỦA STRUCT

C++ tạo cho người dùng một môi trường đơn giản nhất có thể để sử dụng “built-in-type”. Ví dụ, bạn có thể sử dụng cấu trúc như là một đối số, và bạn có thể sử dụng structure như một kiểu dữ liệu của hàm và return giá trị của structure đó. Tất nhiên bạn có thể sử dụng các operator (=) để gán một giá trị của struct cho một biến khác cùng với kiểu đã khai báo. Ví dụ:

char[50] palprice = pal.prices;

Như chúng ta thấy đấy, các “member” được khai báo trong struct cũng được đối xử như là một variable. Bây giờ chúng ta hãy xem ví dụ tiếp theo bên dưới

#include <iostream>
using namespace std;
struct inflatable
{
    char name[20];
    float volume;
    double price;
};

int main()
{
    inflatable bouquet =
    {
        "sunflower",
        0.20,
        12.49
    };
    inflatable pal =
    {
        "Audacious Arthur",
        3.12,
        32.99
    };
    inflatable choice;
    cout << "bouquet: " << bouquet.name << "for $" << bouquet.price << endl;
    choice = bouquet;
    cout << "choice: " << choice.name << "for $" << choice.price << endl;
    return 0;
}

Và đây là kết quả của đoạn code bên trên:

Bouquet: sunflower for $12.49
Choice: sunflower for $12.49

Như bạn đã thấy, cả hai giá trị của Bouquet và Choice đều bằng nhau. Bạn có thể phối hợp định nghĩa 1 cấu trúc và gán thẳng variable cho nó. Để làm được việc này, bạn khai báo thẳng variable sau khi khai báo một struct

struct perks
{
    int key_number;
    char car[12];
} mr_smith, ms_jones;

Mặc dù cách này vẫn giúp chương trình hoạt động bình thường, tuy nhiên các lập trình viên đều chia ra vì viết giống vậy rất khó để theo dõi chương trình và sửa lỗi.

Một cách khác là bạn có thể tạo dựng 1 struct không tên và gọi một variable sau khi đã định nghĩa xong struct. Như ví dụ dưới đây:

struct perks
{
    int key_number;
    char car[12];
} mr_glitz =
{
    7,
    "Packard"
};

Trong đoạn code này:

struct
{
    int x;
    int y;
} position;

Chương trình sẽ tạo dựng một biến struct và gọi nó là position. Bạn có thể truy xuất các “member” của position bằng “membership operation”, ví dụ như là position.x, nhưng nếu bạn sử dụng cách khai báo này thì bạn sẽ chỉ có duy nhất một biên postion có thể sử dụng struct này. Và sau đó bạn không thể tạo một biến khác có thể sử dụng chung struct như thế. Nói cách khác, biến position là đặc biệt.

MẢNG STRUCT

Trong struct inflatable được định nghĩa ở đầu bàu có chứa một mảng (char name[]). Nó cũng có khả năng tạo mảng mà các phần tử là struct. Kỹ thuật để tạo nên một mảng kiểu struct chính là cách mà ta hay sử dụng để tạo 1 mảng của 1 kiểu dữ liệu nào đó.

inflatable gifts[100]; // array of 100 inflatable structures

Đây là dòng lệnh để tạo ra một mảng có 100 phần tử kiểu > inflatable. Vì thế mỗi phần tử trong mảng từ gifts[0] tới gifts[99] đều có kiểu của struct inflatable:

cin >> gifts[0].volume; // use volume member of first struct
cout << gifts[99].price << endl; // display price member if last struct

Nhưng hãy nhớ rằng gifts là tên của mảng chứ không phải là variable, việc truy xuất gift.price sẽ vô nghĩa và bạn sẽ nhanh chóng bị báo lỗi bởi compiler.
Để khai báo một mảng struct, ta cũng có thể ghép chúng lại bằng cách bên dưới:

inflatable guests[2] =        // khai báo mảng kiểu struct
{
    {"Bambi", 0.5, 21.99},        //giá trị đầu tiên của mảng
    {"Godzilla", 2000, 565.99}    //giá trị tiếp theo
}

Như cách mọi người thường dùng, bạn có thể định nghĩa bằng cách bạn thích. Ví dụ, cả hai cách định nghĩa có thể đặt ở chung 1 dòng hay tách từng dòng để dễ đọc.

Ví dụ tiếp theo sẽ hướng dẫn cách mà bạn sẽ sử dụng và truy xuất dữ liệu sử dụng dấu chấm “.” (dot operation)

#include <iostream>
struct inflatable
{
    char name[20];
    float volume;
    double price;
};
int main()
{
    using namespace std;
    inflatable guests[2] =
    {
        {"Bambi, 0.5, 21.99"},
        {"Godzilla, 2000, 565.99"}
    };
    cout << "The guests" << guests[0].name << " and " << guests[1].name;
    cout << "\n Have a combined volume of " << guests[0].volume + guests[1].volume << " 2000.5 cubic feet.\n";
    return 0;
}

Và đây là kết quả sau khi chương trình chạy:

The guests Bambi and Godzilla
Have a combined volume of 2000.5 cubic feet.

BIT FIELDS TRONG STRUCT

C++ cũng như C cho phép bạn chỉ định số lượng bit mà bạn muốn sử dụng. Điều này thực sự hữu dụng khi bạn tạo một data structure, bạn có thể chỉ định bạn cần bao nhiêu bộ nhớ. Vùng Fields này nên là một biến hoặc một kiểu dữ liệu ta không dùng tới. Ví dụ sau:

struct torgle_register
{
    unsigned in SN : 4;    // dùng 4 bit cho giá trị SN;
    unsigned int : 4;    // 4 bit này không dùng tới
    bool goodIn : 1;    // valid input (1 bit)
    bool goodTorgle : 1    // successful torgling
};

Bạn có thể định nghĩa fields cực kỳ linh hoạt, và bạn có thể sử dụng ký hiệu để truy cập các byte field:

Torge_register tr = {14, true, false};
...
if(tr.goodIn)
...

Bit Fields thường được sử dụng trong lập trình cấp thấp cho các vi xử lý. Tuy nhiên bạn có thể sử dụng nó như là 1 flag để đánh dấu khi sử dụng pointer.

UNIONS

Union là một kiểu dữ liệu cũng khá giống struct về cách định nghĩa cùng như cách sử dụng. Nhưng nó khác Struct tại một thời điểm nó chỉ lưu được một giá trị duy nhất.
Quay trở lại các ví dụ trước, ta có 1 struct tên inflatable có chứa nhiều kiểu dữ liệu bên trong. Ta có gọi một “biến” với tên pal và ta truy xuất dữ liệu theo pal.name, pal.prices.... và dữ liệu được truy xuất được lưu vào các biến của pal.name, pal.prices...

Vậy Union lưu dữ liệu thế nào?
Union chỉ lưu 1 dữ liệu tại một thời điểm. Để hiểu rõ hơn thì bạn hãy xem ví dụ bên dưới đây về union:

union one4all
{
    int int_val;
    long long_val;
    double double_val;
};

Nhìn sơ lược bạn có thể thấy rằng union one4all chỉ có thể lưu giữ 3 kiểu dữ liệu là interger, long và double. Và chúng ta hãy tiếp tục những dòng code tiếp theo:

one4all pail;
pail.int_val = 15;
cout << pail.int_val << endl;
pail.double_val = 1.38;
cout << pail.double_val << endl;
cout << pail.int_val << endl;

Như chúng ta thấy, pail chỉ lưu giá trị int tại 1 thời điểm và tại một thời điểm khác (pail.double_val) thì giá trị cuả int sẽ bị mất. Biến sẽ chỉ lưu trữ giá trị tại thời điểm nó được gọi. Bởi vì union chỉ lưu trữ một giá trị tại một thời điểm, nó không có đủ bộ nhớ để giữ những giá trị khác. Vì thế, kích thước bộ nhớ của Union là kích thước của biến có kiểu dữ liệu lớn nhất.

Mục đích của việc sử dụng union là để tiết kiệm bộ nhớ khi mà dữ liệu đầu vào có thể có nhiều định dạng nhưng không bao giờ được sử dụng đồng thời. Ví dụ, giar sử bạn đang quản lý một “hỗn tạp” các kiểu dữ liệu, một vài thứ trong đó nên sử dụng int ID, một số khác thì dùng string ID, một khác nữa là double, bla bla bla. Nếu bạn sử dụng các khai báo biến bình thường thì bạn cần khai báo rất nhiều biến và rất nhiều kiểu dữ liệu. Tuy nhiên với union bạn có thể làm theo cách bên dưới đây:

#include <iostream>
using namespace std;
struct widget
{
    char brand[20];
    int type;
    union ID
    {
        long id_num;
        char id_char[20];
    } id_val;
};
int main()
{
    widget prize;
    if (prize.type == 1)
    {
        cin >> prize.id_val.id_num;
    }
    else
    {
        cin >> prize.id_val.id_char;
    }
}

Anonymous union không có tên; bản chất đó là nhiều biến nhưng được lưu vào một vùng nhớ nhất định:

union ID
{
    long id_num;
    char id_char[20];
} id_val;

Bởi vì union là Anonymous, các biến id_num và id_char được đối xử như hai “biến con” của prize và được lưu trong 1 vùng nhớ nhất định. Nó chỉ cần một cái tên trung gian để truy xuất vào vùng nhớ đó (là id_num hay id_char) tùy thuộc vào lập trình viên chọn cái nào để sử dụng.
Union thường được sử dụng để tiết kiệm bộ nhớ. Nó có vẻ không cần thiết khi mà bộ nhớ RAM ngày nay thực sự lớn và lên tới hàng gigabyte và hàng terabytes cho ổ đĩa. Nhưng không dừng lại ở đó, hằng ngày có rất nhiều lập trình viên đang sử dụng union như cứu cánh trong lĩnh vực hệ thống nhúng, nơi mà bộ nhớ và cả ram bị giới hạn rất là nhiều.

26 Likes

Khi nào trong struct phải sử dụng * còn khi nào ko cần ?
Mình đọc trong phần Treap thì thấy họ dùng * mà ko hiểu dùng bình thường thì khác gì ko ?
Cảm ơn mọi người nhiều !

Bạn học tới con trỏ chưa? :smiley:

1 Like

Mình đã học về con trỏ nhưng phần Node trong Treap :
struct Node
{
int priority;
int sz;
int value;
Node *l,*r;
};
như này thì tại sao phải cần con trỏ nhỉ , mình tưởng chỉ cần bình thường là đủ ?

Vì không thể tính được size của 1 struct khi struct đó tự lồng chính nó, nên type đó không hợp lệ. Còn ngôn ngữ nào viết như vậy thì toàn là reference cả thôi.

1 Like

Thank rogp10 , mình hiểu rồi

Bài viết chưa nói về endian khi ghi struct ra file binary và memory aligment trong struct

Vậy trong Mảng struct muốn chuyển vị trị cảu 2 phần tử thì thao tác như nào…?

Cứ tạo 1 biến phụ rồi swap như bình thường thôi.

1 Like

ok em hiểu vấn đề, cám ơn bác ^^

A post was split to a new topic: Sử dụng struct trên nhiều file cpp như thế nào?

1 Like
83% thành viên diễn đàn không hỏi bài tập, còn bạn thì sao?