| Contents |

8 Struct

Trong C, có một thứ gọi là struct, một kiểu do người dùng định nghĩa được, giữ nhiều mẩu dữ liệu, có thể thuộc các kiểu khác nhau.

Đây là cách tiện lợi để gói nhiều biến vào làm một. Việc này có ích khi truyền biến cho hàm (chỉ cần truyền một thay vì nhiều), và hữu dụng để tổ chức dữ liệu và làm code dễ đọc hơn.

Nếu bạn đến từ ngôn ngữ khác, có thể bạn đã quen với khái niệm classobject. Những thứ này không tồn tại sẵn trong C69. Bạn có thể nghĩ về struct như class chỉ có các trường dữ liệu, không có method.

8.1 Khai báo một struct

Bạn có thể khai báo một struct trong code của mình như này:

struct car {
    char *name;
    float price;
    int speed;
};

Chuyện này thường được làm ở scope toàn cục bên ngoài các hàm để struct khả dụng toàn cục.

Khi làm vậy, bạn đang tạo một kiểu (type) mới. Tên kiểu đầy đủ là struct car. (Không phải chỉ car, viết thế không hoạt động.)

Chưa có biến nào kiểu đó, nhưng ta có thể khai báo vài biến:

struct car saturn;  // Variable "saturn" of type "struct car"

Giờ ta có biến chưa khởi tạo saturn70 kiểu struct car.

Ta nên khởi tạo nó! Nhưng làm sao đặt giá trị cho từng trường riêng lẻ?

Giống như nhiều ngôn ngữ khác lấy lại từ C, ta sẽ dùng toán tử chấm (.) để truy cập từng trường.

saturn.name = "Saturn SL/2";
saturn.price = 15999.99;
saturn.speed = 175;

printf("Name:           %s\n", saturn.name);
printf("Price (USD):    %f\n", saturn.price);
printf("Top Speed (km): %d\n", saturn.speed);

Ở các dòng đầu, ta đặt giá trị vào struct car, rồi ở đoạn sau, in các giá trị đó ra.

8.2 Khởi tạo Struct

Ví dụ ở mục trước hơi cồng kềnh. Chắc phải có cách tốt hơn để khởi tạo biến struct!

Bạn có thể làm với initializer bằng cách đặt giá trị cho các trường theo thứ tự chúng xuất hiện trong struct khi bạn định nghĩa biến. (Cách này không chạy sau khi biến đã được định nghĩa, phải xảy ra ngay lúc định nghĩa).

struct car {
    char *name;
    float price;
    int speed;
};

// Now with an initializer! Same field order as in the struct declaration:
struct car saturn = {"Saturn SL/2", 16000.99, 175};

printf("Name:      %s\n", saturn.name);
printf("Price:     %f\n", saturn.price);
printf("Top Speed: %d km\n", saturn.speed);

Việc các trường trong initializer phải cùng thứ tự nghe hơi rùng mình. Nếu ai đó đổi thứ tự trong struct car, có thể làm hỏng hết code khác!

Ta có thể cụ thể hơn với initializer:

struct car saturn = {.speed=175, .name="Saturn SL/2"};

Giờ nó độc lập với thứ tự trong khai báo struct. Code an toàn hơn hẳn.

Tương tự như initializer của mảng, bất kỳ field designator nào bị bỏ sót đều được khởi tạo về không (trong trường hợp này, đó là .price, tôi đã bỏ qua).

8.3 Truyền Struct cho hàm

Bạn có vài cách để truyền struct cho hàm.

  1. Truyền bản thân struct.
  2. Truyền con trỏ tới struct.

Nhớ rằng khi bạn truyền thứ gì đó cho hàm, một bản sao của thứ đó được tạo ra cho hàm thao tác, bất kể đó là bản sao của con trỏ, của int, của struct, hay bất cứ thứ gì.

Về cơ bản có hai trường hợp bạn muốn truyền con trỏ tới struct:

  1. Bạn cần hàm có thể thay đổi struct được truyền vào, và để những thay đổi đó hiện ra ở phía người gọi.
  2. struct hơi lớn và chép nó lên stack tốn hơn là chép một con trỏ71.

Vì hai lý do đó, truyền con trỏ tới struct cho hàm phổ biến hơn nhiều, dù truyền cả struct thì không hề phạm luật.

Thử truyền con trỏ, làm một hàm cho phép bạn đặt trường .price của struct car:

#include <stdio.h>

struct car {
    char *name;
    float price;
    int speed;
};

int main(void)
{
    struct car saturn = {.speed=175, .name="Saturn SL/2"};

    // Pass a pointer to this struct car, along with a new,
    // more realistic, price:
    set_price(&saturn, 799.99);

    printf("Price: %f\n", saturn.price);
}

Bạn nên có thể nghĩ ra signature của hàm set_price() chỉ bằng cách nhìn kiểu của các đối số ta có ở đó.

saturnstruct car, nên &saturn phải là địa chỉ của struct car, tức là con trỏ tới struct car, cụ thể là struct car*.

799.99float.

Nên khai báo hàm phải như này:

void set_price(struct car *c, float new_price)

Ta chỉ cần viết phần thân. Lần thử đầu tiên có thể là:

void set_price(struct car *c, float new_price) {
    c.price = new_price;  // ERROR!!
}

Cách đó không chạy vì toán tử chấm chỉ chạy trên struct… nó không chạy trên con trỏ tới struct.

Được rồi, ta có thể dereference biến c để “de-pointer” nó để tới bản thân struct. Dereference một struct car* cho ra struct car mà con trỏ trỏ tới, ta nên có thể dùng toán tử chấm lên đó:

void set_price(struct car *c, float new_price) {
    (*c).price = new_price;  // Works, but is ugly and non-idiomatic :(
}

Và chạy! Nhưng hơi lôi thôi khi gõ hết đám dấu ngoặc với dấu sao. C có một thứ đường cú pháp gọi là toán tử mũi tên (arrow operator) giúp chuyện đó.

8.4 Toán tử mũi tên

Toán tử mũi tên giúp tham chiếu tới các trường trong con trỏ tới struct.

void set_price(struct car *c, float new_price) {
    // (*c).price = new_price;  // Works, but non-idiomatic :(
    //
    // The line above is 100% equivalent to the one below:

    c->price = new_price;  // That's the one!
}

Vậy khi truy cập các trường, khi nào dùng chấm và khi nào dùng mũi tên?

8.5 Sao chép và trả về struct

Đây là phần dễ cho bạn!

Chỉ cần gán từ cái này sang cái kia!

struct car a, b;

b = a;  // Copy the struct

Và trả về một struct (thay vì trả về một con trỏ tới nó) từ hàm cũng tạo một bản sao tương tự vào biến nhận.

Đây không phải “deep copy”72. Tất cả các trường được chép y nguyên, kể cả con trỏ tới các thứ.

8.6 So sánh struct

Chỉ có một cách an toàn duy nhất: so sánh từng trường một.

Bạn có thể nghĩ có thể dùng memcmp()73, nhưng nó không xử lý được trường hợp có thể có padding bytes nằm lẫn trong đó.

Nếu bạn xoá struct về không trước bằng memset()74, thì nó có thể chạy, dù vẫn có thể có những phần tử lạ không so sánh như bạn mong75.


| Contents |