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 String
và Array
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 constructor
và desctructor
nho nhỏ thì chắc là cũng không có gì quá khó khăn.