Bài tập

Kiểu cấu trúc (Struct) trong C – Liên minh trong C

Huy Erick

Trong ngôn ngữ lập trình C, kiểu cấu trúc là một thành phần quan trọng giúp tổ chức và lưu trữ các biến có cùng một tên trong một đối tượng duy nhất. Trên cơ...

Trong ngôn ngữ lập trình C, kiểu cấu trúc là một thành phần quan trọng giúp tổ chức và lưu trữ các biến có cùng một tên trong một đối tượng duy nhất. Trên cơ sở đó, chúng ta có thể tạo các biến thuộc kiểu cấu trúc và truy cập vào thành viên của chúng.

1. Kiểu cấu trúc trong C

Để hiểu về kiểu cấu trúc trong C, chúng ta cần tìm hiểu cách định nghĩa và sử dụng kiểu cấu trúc thông qua các ví dụ cụ thể.

1.1 Cách định nghĩa cấu trúc trong C

Trước khi tạo các biến thuộc kiểu cấu trúc trong C, chúng ta cần xác định kiểu dữ liệu của nó. Để định nghĩa một kiểu cấu trúc trong C, ta sử dụng từ khóa struct.

Cú pháp của cấu trúc trong C như sau:

struct structureName
{
    dataType member1;
    dataType member2;
    ...
};

Ví dụ:

struct Person
{
    char name[50];
    int citNo;
    float salary;
};

Trong ví dụ trên, kiểu struct Person được định nghĩa. Lúc này, chúng ta có thể tạo ra các biến thuộc kiểu này.

1.2 Tạo các biến cấu trúc trong C

Khi một kiểu cấu trúc trong C được khai báo, chúng ta cần tạo ra các biến để làm việc với kiểu cấu trúc đó. Chúng ta có thể tạo các biến cấu trúc theo cách sau:

struct Person
{
    char name[50];
    int citNo;
    float salary;
} person1, person2, p[20];

Trong ví dụ trên, 2 biến person1person2, cùng với một mảng p gồm 20 phần tử thuộc kiểu struct Person, được tạo ra.

1.3 Truy cập vào các thành viên của một cấu trúc

Để truy cập vào các thành viên của một cấu trúc trong C, chúng ta sử dụng hai loại toán tử sau:

  • . - Toán tử thành viên dot
  • -> - Toán tử thành viên arrow (sẽ được giới thiệu trong phần sau)

Ví dụ, để truy cập vào salary của person2, ta phải sử dụng person2.salary.

// Program to add two distances (feet-inch)
#include 
struct Distance
{
    int feet;
    float inch;
};
int main()
{
    struct Distance dist1, dist2, sum;
    printf("1st distance\n");
    printf("Enter feet: ");
    scanf("%d", &dist1.feet);
    printf("Enter inch: ");
    scanf("%f", &dist1.inch);
    printf("2nd distance\n");
    printf("Enter feet: ");
    scanf("%d", &dist2.feet);
    printf("Enter inch: ");
    scanf("%f", &dist2.inch);

    // adding feet
    sum.feet = dist1.feet + dist2.feet;

    // adding inches
    sum.inch = dist1.inch + dist2.inch;

    // changing to feet if inch is greater than 12
    while (sum.inch >= 12)
    {
        ++sum.feet;
        sum.inch = sum.inch - 12;
    }
    printf("Sum of distances = %d'- %.1f\"", sum.feet, sum.inch);
    return 0;
}

Trong ví dụ này, chúng ta sử dụng hai biến cấu trúc dist1dist2 để lưu trữ hai khoảng cách. Chúng ta cộng hai khoảng cách lại với nhau và in ra tổng của chúng.

1.4 Từ khóa typedef trong C

Chúng ta có thể sử dụng từ khóa typedef để tạo tên bí danh cho các kiểu dữ liệu trong C, bao gồm cả các kiểu cấu trúc. Việc này giúp đơn giản hóa cú pháp khai báo biến.

Ví dụ:

struct Distance
{
    int feet;
    float inch;
};

typedef struct Distance distances;

int main()
{
    distances d1, d2;
}

Cách khai báo biến d1d2 trong ví dụ trên tương đương với cách khai báo biến dist1dist2 trong các ví dụ trước.

1.5 Cấu trúc lồng nhau

Trong C, chúng ta có thể tạo một cấu trúc lồng trong một cấu trúc khác. Ví dụ:

struct complex
{
    int imag;
    float real;
};

struct number
{
    struct complex comp;
    int integers;
};

struct number num1, num2;

Giả sử chúng ta muốn gán giá trị 11 cho thành viên imag của biến num2. Ta có thể thực hiện như sau: num2.comp.imag = 11.

1.6 Lợi ích của việc sử dụng cấu trúc trong C

Giả sử chúng ta muốn lưu trữ thông tin của một người bao gồm tên, số CMND và mức lương. Nếu sử dụng các biến riêng lẻ, chúng ta có thể tạo ra các biến name, IdNosalary riêng biệt để lưu trữ thông tin này.

Tuy nhiên, nếu chúng ta muốn lưu trữ thông tin của nhiều người, việc tạo ra các biến riêng lẻ sẽ trở nên khó khăn và không hiệu quả. Trong trường hợp này, chúng ta cần tạo một bộ sưu tập chứa các thông tin liên quan trong một cấu trúc đơn lẻ, ví dụ như Person, và sử dụng nó cho tất cả mọi người.

2. Cấu trúc và con trỏ trong C

Trong phần này, chúng ta sẽ tìm hiểu cách sử dụng con trỏ để truy cập vào các thành viên của cấu trúc trong C. Chúng ta cũng sẽ biết cách cấp phát bộ nhớ tự động cho các kiểu cấu trúc trong C.

Tuy nhiên, trước đó, chúng ta cần xem qua các phần dưới đây để tìm hiểu cách sử dụng con trỏ với các biến cấu trúc và cấu trúc trong C:

  • Biến con trỏ trong C
  • Cấu trúc trong C

2.1 Tạo biến con trỏ trỏ đến cấu trúc trong C

Để tạo một biến con trỏ trỏ đến cấu trúc trong C, ta sử dụng dấu * để khai báo biến con trỏ và tên cấu trúc.

Ví dụ:

struct name {
    member1;
    member2;
    ...
};
int main() {
   struct name *ptr, Harry;
}

Trong ví dụ này, ptr là một biến con trỏ trỏ đến cấu trúc name.

Ví dụ: Truy cập vào các thành viên bằng biến con trỏ

Để truy cập vào các thành viên của một cấu trúc bằng biến con trỏ, chúng ta sử dụng toán tử ->.

#include 
struct person
{
    int age;
    float weight;
};
int main()
{
    struct person *personPtr, person1;
    personPtr = &person1;
    printf("Enter age: ");
    scanf("%d", &(personPtr->age));
    printf("Enter weight: ");
    scanf("%f", &(personPtr->weight));
    printf("Displaying:\n");
    printf("Age: %d\n", personPtr->age);
    printf("Weight: %.2f", personPtr->weight);
    return 0;
}

Trong ví dụ này, địa chỉ của person1 được lưu trữ trong biến con trỏ personPtr bằng cách sử dụng personPtr = &person1;. Bây giờ, chúng ta có thể truy cập vào các thành viên của person1 bằng biến con trỏ personPtr.

2.2 Cấp phát bộ nhớ động cho cấu trúc

Trong một số trường hợp, chúng ta cần cấp phát bộ nhớ động cho một kiểu cấu trúc trong quá trình chạy. Để làm điều này, chúng ta có thể sử dụng hàm malloc() để cấp phát bộ nhớ cho các biến cấu trúc.

Ví dụ: Cấp phát bộ nhớ động cho cấu trúc

#include 
#include 
struct person {
    char name[50];
    int age;
};
int main()
{
    struct person *ptr;
    int n;
    printf("Enter the number of persons: ");
    scanf("%d", &n);
    // allocating memory for n numbers of struct person
    ptr = (struct person*) malloc(n * sizeof(struct person));
    for (int i = 0; i  n; ++i) {
        printf("Enter name and age respectively: ");
        scanf("%s %d", (ptr+i)->name, &(ptr+i)->age);
    }
    printf("Displaying Information:\n");
    for (int i = 0; i  n; ++i)
        printf("Name: %s\nAge: %d\n\n", (ptr+i)->name, (ptr+i)->age);
    return 0;
}

Trong ví dụ này, chúng ta sử dụng hàm malloc() để cấp phát bộ nhớ cho n biến cấu trúc thuộc kiểu struct person. Sau đó, chúng ta nhập thông tin cho các biến cấu trúc và hiển thị thông tin đó.

3. Cấu trúc và hàm trong lập trình C

Trong phần này, chúng ta sẽ tìm hiểu cách truyền biến cấu trúc vào hàm và làm việc với kết quả trả về của hàm.

3.1 Truyền cấu trúc vào hàm

Trong ngôn ngữ lập trình C, chúng ta có thể truyền cấu trúc vào một hàm. Để làm điều này, chúng ta cần khai báo biến đối số của hàm là một cấu trúc. Ví dụ:

#include 
struct student {
    char name[50];
    int age;
};

void display(struct student s);

int main() {
    struct student s1;
    printf("Enter name: ");
    scanf("%[^\n]%*c", s1.name);
    printf("Enter age: ");
    scanf("%d", &s1.age);
    display(s1); // passing struct as an argument
    return 0;
}

void display(struct student s) {
    printf("\nDisplaying information:\n");
    printf("Name: %s\n", s.name);
    printf("Age: %d\n", s.age);
}

Trong ví dụ này, chúng ta khai báo một biến s1 thuộc kiểu struct student, rồi nhập thông tin cho biến đó. Sau đó, chúng ta truyền biến s1 vào hàm display(). Hàm này sẽ hiển thị thông tin của biến cấu trúc.

3.2 Lấy lại cấu trúc từ một hàm

Chúng ta cũng có thể lấy lại cấu trúc từ một hàm trong ngôn ngữ lập trình C. Để làm điều này, chúng ta cần khai báo kiểu trả về của hàm là một cấu trúc. Ví dụ:

#include 
struct student
{
    char name[50];
    int age;
};

struct student getInformation();

int main()
{
    struct student s;
    s = getInformation();
    printf("\nDisplaying information:\n");
    printf("Name: %s\n", s.name);
    printf("Age: %d\n", s.age);
    return 0;
}

struct student getInformation()
{
    struct student s1;
    printf("Enter name: ");
    scanf("%[^\n]%*c", s1.name);
    printf("Enter age: ");
    scanf("%d", &s1.age);
    return s1;
}

Trong ví dụ này, chúng ta gọi hàm getInformation() và gán kết quả trả về cho biến s. Hàm này sẽ trả về một biến cấu trúc thuộc kiểu struct student, và chúng ta sẽ hiển thị thông tin của biến đó.

3.3 Truyền cấu trúc bằng cách tham chiếu

Chúng ta cũng có thể truyền cấu trúc bằng cách tham chiếu, tương tự như cách chúng ta truyền các biến thuộc kiểu xây dựng sẵn. Trong quá trình truyền bằng cách tham chiếu, chúng ta truyền địa chỉ bộ nhớ của biến cấu trúc vào hàm.

#include 
typedef struct Complex
{
    float real;
    float imag;
} complex;

void addNumbers(complex c1, complex c2, complex *result);

int main()
{
    complex c1, c2, result;
    printf("For first number,\n");
    printf("Enter real part: ");
    scanf("%f", &c1.real);
    printf("Enter imaginary part: ");
    scanf("%f", &c1.imag);
    printf("For second number, \n");
    printf("Enter real part: ");
    scanf("%f", &c2.real);
    printf("Enter imaginary part: ");
    scanf("%f", &c2.imag);
    addNumbers(c1, c2, &result);
    printf("\nresult.real = %.1f\n", result.real);
    printf("result.imag = %.1f", result.imag);
    return 0;
}

void addNumbers(complex c1, complex c2, complex *result)
{
    result->real = c1.real + c2.real;
    result->imag = c1.imag + c2.imag;
}

Trong ví dụ này, ba biến cấu trúc c1, c2result cùng nhau được truyền vào hàm addNumbers(). Tại đây, biến result được truyền bằng cách tham chiếu. Khi biến result trong hàm addNumbers() thay đổi, biến result trong hàm main() cũng thay đổi theo.

4. Liên minh trong C

Liên minh (union) là một kiểu dữ liệu do người dùng định nghĩa trong ngôn ngữ lập trình C. Liên minh có chức năng tương tự như cấu trúc, nhưng có một số điểm khác biệt. Cấu trúc cấp phát đủ không gian để lưu trữ tất cả các thành viên của nó, trong khi liên minh chỉ cấp phát không gian để lưu trữ thành viên lớn nhất.

4.1 Cách định nghĩa liên minh

Chúng ta sử dụng từ khóa union để định nghĩa một liên minh trong C.

Ví dụ:

union car
{
    char name[50];
    int price;
};

Trong ví dụ này, kiểu union car được định nghĩa. Lúc này, chúng ta có thể tạo ra các biến thuộc kiểu này.

4.2 Tạo các biến liên minh

Khi một liên minh được định nghĩa, nó tạo ra một kiểu do người dùng định nghĩa. Tuy nhiên, liên minh không được cấp phát bộ nhớ. Để cấp phát bộ nhớ cho một kiểu liên minh cụ thể và làm việc với nó, chúng ta tạo các biến như sau:

union car
{
    char name[50];
    int price;
} car1, car2, *car3;

Trong ví dụ này, biến liên minh car1car2, cùng với biến con trỏ car3 thuộc kiểu union car, được tạo ra.

4.3 Truy cập vào các thành viên của một liên minh

Để truy cập vào các thành viên của một liên minh trong C, chúng ta sử dụng toán tử .. Để truy cập vào các thành viên qua biến con trỏ, chúng ta sử dụng toán tử ->.

Ví dụ, để truy cập price của car1, ta sử dụng car1.price.

#include 
union Job
{
    float salary;
    int workerNo;
} j;
int main()
{
    j.salary = 12.3;
    j.workerNo = 100;
    printf("Salary = %.1f\n", j.salary);
    printf("Number of workers = %d", j.workerNo);
    return 0;
}

Trong ví dụ này, chúng ta gán giá trị 12.3 cho thành viên salary của biến j. Khi j.workerNo được gán giá trị, j.salary sẽ không còn giữ giá trị 12.3 nữa.

4.4 Sự khác biệt giữa liên minh và cấu trúc

Trong ngôn ngữ lập trình C, có sự khác biệt về kích thước giữa biến liên minh và biến cấu trúc. Điều này do cấu trúc cấp phát đủ không gian để lưu trữ tất cả các thành viên của nó, trong khi liên minh chỉ cấp phát không gian để lưu trữ thành viên lớn nhất.

Ví dụ: So sánh kích thước của biến liên minh và biến cấu trúc

#include 
union unionJob
{
    char name[32];
    float salary;
    int workerNo;
} uJob;
struct structJob
{
    char name[32];
    float salary;
    int workerNo;
} sJob;
int main()
{
    printf("size of union = %ld bytes\n", sizeof(uJob));
    printf("size of structure = %ld bytes\n", sizeof(sJob));
    return 0;
}

Trong ví dụ này, chúng ta so sánh kích thước của biến liên minh uJob và biến cấu trúc sJob. Kích thước của uJob là 32 byte, trong khi kích thước của sJob là 40 byte.

Sự khác biệt kích thước này xuất phát từ việc rằng kích thước của biến liên minh thường sẽ bằng kích thước của thành viên lớn nhất. Trong ví dụ này, kích thước của thành viên lớn nhất, name[32], là 32 byte. Tất cả các thành viên trong cùng một liên minh chia sẻ cùng một vùng nhớ.

1