[Imperative Programming + C] Bài 6 - Struct và Typedef

    Thông thường thì bước đầu tiên khi chúng ta bắt tay vào việc xây dựng một chương trình phần mềm sẽ là xác định đối tượng dữ liệu cần quản lý. Hay nói một cách khác là chúng ta cần định nghĩa một kiểu dữ liệu mô phỏng lại thông tin cần quản lý của các thực thể trong cuộc sống.

    Ví dụ điển hình là khi xây dựng một trang blog cá nhân đơn giản chúng ta đã định nghĩa kiểu dữ liệu Article gồm các trường id, title, content, v.v... để mô tả các bản ghi bài viết. Và hiển nhiên là điều này cũng cần thiết khi viết một chương trình viết trong C.

Định nghĩa Struct

    Chúng ta sẽ bắt đầu với từ khóa struct, khá giống với class trong JavaScript ở cấp độ sử dụng cơ bản. Ở đây chúng ta sẽ có một đoạn code ví dụ cách định nghĩa một kiểu dữ liệu tổ hợp tên là book.

# include <stdio.h>

struct book {
   int    id;
   char*  title;
};

void main () {
   struct book $laotzu = {
      .id = 0,
      .title = "Tao Te Ching"
   };

   fprintf (stdout, "@Id: %i \n", $laotzu.id);
   fprintf (stdout, "Title: %s \n", $laotzu.title);
}
gcc main.c -o main
main

@Id: 0
Title: Tao Te Ching

    Thao tác định nghĩa và sử dụng một struct thực sự rất quen thuộc và có nhiều điểm tương đồng với class trong JavaScript, đặc biệt là ở đoạn truy xuất các phần tử đóng gói bên trong bằng phép thực thi . Điểm duy nhất có phần hơi rườm rà đó là ở vị trí type-hint, chúng ta đang phải viết nguyên cả cụm từ struct book để biểu thị cho kiểu dữ liệu của biến laotzu.

Sử dụng Typedef

    Code đẹp và dễ đọc đã, mọi thứ logic khác sẽ để dành tìm hiểu sau. Mình có Google thêm về cách tạo ra tên tham chiếu cho các kiểu dữ liệu sẵn có thì C lại có cung cấp thêm một từ khóa typedef (type define). Từ khóa này sẽ giúp chúng ta tạo ra một tên gọi thay thế alias cho bất kỳ kiểu dữ liệu nào.

# include <stdio.h>

struct book {
   int   id;
   char  title[100];
};

typedef  struct book  book_t;

void main () {
   book_t $laotzu = {
      .id = 0,
      .title = "Tao Te Ching"
   };
   
   fprintf (stdout, "@Id: %i \n", $laotzu.id);
   fprintf (stdout, "Title: %s \n", $laotzu.title);
}

    Chúng ta cũng có thể nghĩ đến việc sử dụng typedef để đặt tên cho các kiểu pointer để có giao diện viết code tương đồng với JavaScript.

# include <stdio.h>

typedef  char*  String;

void main () {
   String $message = "Just be";
   fprintf (stdout, "%s", $message);
}
gcc main.c -o main
main

Just be

Struct Pointer

    Khi sử dụng struct để truyền vào một sub-program, thì một biến kiểu struct lại có phương thức hoạt động giống với các biến kiểu đơn giản như int, char, v.v... Chương trình con sẽ nhận được một bản sao của struct được truyền vào; Và code bên trong sub-program sẽ không thể thay đổi được nội dung của struct ban đầu.

# include <stdio.h>

struct book {
   int    id;
   char*  title;
};

typedef  struct book  book_t;

void updateid (book_t $copy) {
   $copy.id = 1;
}

void main () {
   book_t $laotzu = {
      .id = 0,
      .title = "Tao Te Ching"
   };
   
   updateid ($laotzu);
   fprintf (stdout, "Updated Id: %i", $laotzu.id);
}
gcc main.c -o main
main

Updated Id: 0

    Để chương trình con updateId có thể hoạt động được với logic mong muốn thì chúng ta có thể sửa lại sub-program này một chút và trả về struct bản sao để thay thế struct ban đầu.

# include <stdio.h>

struct book {
   int    id;
   char*  title;
};

typedef  struct book  book_t;

book_t updateid (book_t $copy) {
   $copy.id = 1;
   return $copy;
}

void main () {
   book_t $laotzu = {
      .id = 0,
      .title = "Tao Te Ching"
   };
   
   $laotzu = updateid ($laotzu);
   fprintf (stdout, "Updated Id: %i", $laotzu.id);
}
gcc main.c -o main
main

Updated Id: 1

    Hoặc một cách làm khác, đó là chúng ta có thể truyền địa chỉ tham chiếu của struct ban đầu vào chương trình con updateId. Và như vậy, code bên trong sub-program này sẽ có thể thay đổi nội dung của struct ban đầu. Tuy nhiên thao tác truy xuất tới các trường dữ liệu thông qua một biến con trỏ struct pointer sẽ có cách viết hơi rườm rà một chút.

# include <stdio.h>

struct book {
   int   id;
   char  title[100];
}

typedef  struct book  book_t;

void updateid (book_t* $reference) {
   (* $reference).id = 1;
}

void main () {
   book_t $laotzu = {
      .id = 0,
      .title = "Tao Te Ching"
   };
   
   updateid (& $laotzu);
   fprintf (stdout, "Updated Id: %i", $laotzu.id);
}
gcc main.c -o main
main

Updated Id: 1

    Ở đoạn (*reference).id thường được viết lại thành reference->id. Cách viết sử dụng ký hiệu mũi tên -> trông ngắn gọn hơn và thường được sử dụng nhiều hơn từ kết quả mà mình Google được. Tuy nhiên trong code ví dụ mình muốn mô tả logic hoạt động thông thường như khi chúng ta sử dụng các biến pointer trong những bài trước đó. 😄

Struct in JavaScript

    Trong JavaScript, các object thuần dữ liệu không có chứa các phương thức là dạng triển khai cao hơn của struct. Tuy nhiên khi truyền một object vào một sub-program thì JavaScript sẽ mặc định là truyền địa chỉ tham chiếu của object đó chứ không tự động tạo ra một object bản sao như cách xử lý struct mặc định của C.

class Book {
   constructor ($id, $title) {
      this.id = $id;
      this.title = $title;
   }
}

function updateId ($reference) {
   $reference.id = 1;
}

void function main () {
   var $laotzu = new Book (0, "Tao Te Ching");
   updateId ($laotzu);
   console.log ($laotzu.id);
} ();

    Mỗi một ngôn ngữ và môi trường lập trình đều có những đặc thù riêng và mình cảm thấy cách xử lý mặc định của C khi truyền một struct vào một sub-program cũng rất dễ hiểu. Tuy nhiên logic xử lý tự động tạo ra bản sao dữ liệu như thế này có phần hơi bất đồng bộ so với StringArray của chính môi trường C. Do đó nên mình đã tìm hiểu thêm một chút thông tin và chọn ra một quy ước convention về cách đặt tên và sử dụng struct để phù hợp với logic sử dụng của mình.

    Mục tiêu của convention này là để đồng bộ logic xử lý chung khi truyền các kiểu dữ liệu phức hợp vào các sub-program trong C. Đó là thao tác truyền một cấu trúc dữ liệu phức hợp như String, Array, hay một struct nào đó, sẽ luôn luôn mặc định là truyền vào địa chỉ tham chiếu từ một pointer. Như vậy chương trình con có thể quyết định lựa chọn:

  • Chỉnh sửa nội dung của cấu trúc dữ liệu ban đầu thông qua địa chỉ tham chiếu được truyền vào.
  • Hoặc chủ động tạo ra một cấu trúc dữ liệu bản sao kèm theo những cập nhật thay đổi để trả về.
# include <stdio.h>
# include <stdlib.h>
 
typedef char* String

struct book {
   int     id;
   String  title;
};

typedef  struct book  book_t;
typedef  book_t*  Book;

Book new_Book (
   int     $id,
   String  $title
) {
   Book $reference = malloc (sizeof (book_t));
   $reference->id = $id;
   $reference->title = $title;
   return $reference;
}

void delete_Book (Book $reference) {
   free ($reference);
}

void updateid (Book $reference) {
   $reference->id = 1;
}

void main () {
   Book $laotzu = new_Book (0, "Tao Te Ching");
   updateid ($laotzu);
   
   fprintf (stdout, "Updated Id: %i", $laotzu->id);
   delete_Book ($laotzu);
}

    Khi sử dụng convention này, các kiểu dữ liệu đại diện cho các cấu trúc sẽ đều được đặt tên với chữ cái đầu tiên được viết hoa để biểu thị cho kiểu biến con trỏ pointer và cần được khởi tạo qua một chương trình con new_Type như trong code ví dụ trên. Cũng khá thuận tiện và đằng nào thì khi chúng ta viết một chương trình bất kỳ cũng đều sẽ cần tạo ra các thư viện cung cấp các sub-program tiện ích cho mỗi kiểu dữ liệu; Thêm một chương trình constructordesctructor nho nhỏ thì chắc là cũng không có gì quá khó khăn. 😄

    [Imperative Programming + C] Bài 7 - Boolean & Switching

Nguồn: Viblo

Bình luận
Vui lòng đăng nhập để bình luận
Một số bài viết liên quan