[Imperative Programming + C] Bài 15 – Simplicity DSA List (mở đầu)

Oh.. vậy là cái project nho nhỏ simplicity-dsa-c đã phải tạm dừng một thời gian khá dài để nhường chỗ cho các bài viết chính của Series có trọng tâm là giới thiệu các mô hình lập trình chính. Sau khi hoàn thành xong Sub-Series Procedural và quay trở lại chạm vào Elm lần nữa

Oh.. vậy là cái project nho nhỏ simplicity-dsa-c đã phải tạm dừng một thời gian khá dài để nhường chỗ cho các bài viết chính của Series có trọng tâm là giới thiệu các mô hình lập trình chính. Sau khi hoàn thành xong Sub-Series Procedural và quay trở lại chạm vào Elm lần nữa thì mình mới nhớ ra là đang để dành các bài viết về cấu trúc lưu trữ List ở đây và quết định thực hiện tiếp một vài bài viết về simplicity-dsa-c.

Đây cũng xem như là để tạo khoảng trống thời gian cho việc ôn lại cú pháp căn bản của Elm trước khi tiếp tục tìm hiểu về việc xây dựng SPA và nếu như bạn là một người bắt đầu đồng hành từ Sub-Series Declarative hoặc Functional thì cũng sẽ có thời gian để lướt lại các bài viết về Elm trong Sub-Series Declarative. Và sau khi thực hiện xong một vài bài viết về List trong cái mini project ở đây để tìm hiểu về các cấu trúc dữ liệu ở cấp độ tổng quan thì chúng ta sẽ quay lại với Elm tại Sub-Series Declarative.

List

Khi nói tới việc tổ chức và lưu trữ dữ liệu ở dạng tập hợp các bản ghi hay các giá trị cùng kiểu thì ở các ngôn ngữ Procedural người ta thường nhắc tới cấu trúc mảng Array đầu tiên, thế nhưng trong các ngôn ngữ Functional thì câu trả lời phổ biến nhất lại là List với điểm khác biệt căn bản là các List không bị giới hạn về dung lượng lưu trữ và không hỗ trợ tham chiếu bằng trị số chỉ vị trí index của các phần tử.

Lý do tại sao các ngôn ngữ Functional lại sử dụng List phổ biến như vậy thì chúng ta sẽ để dành cho những bài viết sau cùng, sau khi đã biết cách tổ chức lưu trữ dữ liệu bằng cấu trúc này. Và để bắt đầu tìm hiểu về List thì chúng ta sẽ xuất phát với ví dụ lưu trữ dữ liệu trong mảng đơn thuần literal array của C. Chúng ta vẫn sẽ xem xét trường hợp mảng chưa định kiểu với các ô nhớ là các biến con trỏ void* để có khả năng lưu trữ một giá trị primitive hoặc một địa chỉ tham chiếu reference.

#include<stdio.h>#include<stdlib.h>typedefdouble val;typedefvoid* ref;intmain(int $argc
   ,char** $argv
   ){int $capacity =9;void* $literal_array =malloc(8* $capacity);

      val* $val_pointer = $literal_array;*($val_pointer +0)='A';

      ref* $ref_pointer = $literal_array;*($ref_pointer +1)="Message";printf("Stored character: %c n",(char) $val_pointer[0]);printf("Stored string: %s n", $ref_pointer[1]);return0;}

Như vậy là chúng ta đã tạo ra một cấu trúc mảng đơn thuần với 9 ô nhớ có độ rộng bằng nhau và là 8 bit để có thể lựa chọn lưu trữ địa chỉ tham chiếu hoặc giá trị primitive nào đó không quá lớn và trong biên độ của double. Sau đó chúng ta đã sử dụng ô nhớ đầu tiên qua kiểu con trỏ val* để lưu vào đó một giá trị primitive là ký tự 'A' và sử dụng ô nhớ tiếp theo qua kiểu con trỏ ref* để lưu vào đó địa chỉ tham chiếu của chuỗi "Message".

Mục đích của chúng ta khi tạo ra List vẫn là tạo ra các ô nhớ void* để có thể lựa chọn lưu trữ linh động như vậy. Tuy nhiên vì đặc đính của List là không giới hạn dung lượng lưu trữ vì vậy nên các ô nhớ void* lúc này sẽ không được cấp phát với số lượng cố định từ đầu và vì vậy cũng sẽ không có vị trí liền kề nhau trong bộ nhớ đệm của máy tính.

Chính vì vậy nên trong trường hợp này khi sử dụng ô nhớ void* đầu tiên qua kiểu con trỏ val* hay ref* có độ rộng là 8 bit thì chúng ta cũng sẽ không thể thực hiện thao tác bước tới các ô nhớ liên tiếp như kiểu pointer + 0, pointer + 1, pointer + 2, v.v… Và đây có lẽ là lý do căn bản để giải thích cho việc cấu trúc List mặc định không hỗ trợ truy xuất các phần tử lưu trữ thông qua các trị số chỉ vị trí index.

Oh.. nếu vậy thì làm thế nào để lặp qua nội dung của một List giống như khi chúng ta đang có địa chỉ tham chiếu của phần tử đầu tiên của một mảng Array? Hay làm thế nào để chúng ta có thể bước lần lượt tới các phần tử tiếp theo đã được lưu trữ trong cấu trúc List?

Item

Giải pháp để di chuyển từ ô nhớ void* đầu tiên tới ô nhớ tiếp theo lúc này là chúng ta sẽ phải có thêm các thông tin mô tả bổ trợ cho mỗi ô nhớ lưu trữ dữ liệu của List. Cụ thể là mỗi ô nhớ void* lúc này sẽ được bọc trong một struct kèm theo các trường lưu trữ địa chỉ tham chiếu của các ô nhớ liền trước và liền sau trong List.

Thông thường thì người ta thường gọi các struct như thế này là nguyên tố element hoặc nút giao node, bởi vì thiết kế vỏ bọc này còn được sử dụng trong các cấu trúc dữ liệu khác mà chúng ta sẽ tìm hiểu sau này có tên gọi đại loại như tree. Tuy nhiên ở đây mình gọi là item trong trường hợp sử dụng với List để dễ liên tưởng tới các danh sách nội dung mà chúng ta thấy trong cuộc sống thường nhật hoặc các danh sách unordered list trong HTML.

typedef double val;
typedef void* ref;

typedef struct item {
   struct item* prev;
   void*        data; 
   struct item* next;
} item_struct;

typedef item_struct* Item;

Như vậy là đứng từ vị trí của mỗi Item đang chứa một ô nhớ void* để lưu trữ dữ liệu thì chúng ta có thể di chuyển tới một Item liền kề trước đó prev hoặc di chuyển tới Item tiếp theo next. Bây giờ chúng ta hãy thử tạo ra một vài Itemđược kết nối với nhau và lặp qua các nội dung lưu trữ trong một lượt thao tác.

#include<stdio.h>#include<stdlib.h>#include<stddef.h>#include"list.h"

Sau khi nạp các thư viện cần thiết thì chúng ta cần tự động hóa thao tác khởi tạo struct và trả về con trỏ Item..

Item item_new(void){  Item $item =malloc(sizeof(item_struct));
      $item->data =malloc(8);

      $item->prev =NULL;
      $item->next =NULL;return $item;}

Rồi tới hàm main chứa code chạy thử..

intmain(int $argc
   ,char** $argv
   ){  Item $first =item_new();*((val*) $first->data)=0;// - - - - - - - - -

      Item $second =item_new();*((val*) $second->data)=1;

      $first->next = $second;
      $second->prev = $first;// - - - - - - - - -

      Item $last =item_new();*((val*) $last->data)=2;

      $second->next = $last;
      $last->prev = $second;// - - - - - - - - -

      Item $cursor = $first;
      loop :if($cursor !=NULL){printf("Stored data: %f n",*((val*) $cursor->data));
            $cursor = $cursor->next;// ---goto loop;}return0;}

Và ở cửa sổ dòng lệnh..

cd simplicity-dsa-c
gcc srcmain.c -o binmain
binmain

Stored data: 0.000000 
Stored data: 1.000000
Stored data: 2.000000

Coding Convention

Bài viết mở đầu giới thiệu về cấu trúc List của chúng ta tới đây xem như đã hoàn thành và các phương thức thao tác với List chúng ta sẽ để dành cho các bài viết tiếp theo. Còn trong phần Coding Convention thì mình muốn cập nhật lại một số quy ước riêng mà mình sử dụng trong code của simplicity-dsa-c về việc đặt tên biến và cấu trúc thư mục chứa các tệp mã nguồn.

Cụ thể là sau khi sử dụng ngôn ngữ Ada ở lớp mẫu giáo thì thói quen đọc code của mình bị thay đổi theo và rất cần có sự tương phản tốt giữa tên biến và các từ khóa của ngôn ngữ. Vì vậy nên mặc dù không thể sử dụng các tên định danh theo kiểu Uppercase_Name của Ada ở đây thì mình cũng đặt thử vào phía trước tên của các biến cục bộ và các tham số của các sub-program ký hiệu $ thường dùng của một số ngôn ngữ khác như XQuery, PHP, hay một số thư viện JavaScript.

Tuy nhiên vị trí tên các trường dữ liệu của struct thì vẫn sẽ sử dụng cách đặt tên thông thường và không có thêm ký hiệu $ để phân biệt với các tham số và các biến cục bộ của các sub-program và mặt khác là để cú pháp tham chiếu gọn gàng hơn một chút. Thay vì $item->$first thì chúng ta có $item->first và ký hiệu $ chủ yếu là để đọc đoạn khai báo các biến nhanh hơn bởi có xuất hiện thêm các tên định kiểu dữ liệu ở phía trước.

Còn lại thì nhìn chung tên của các biến và các sub-program vẫn sẽ sử dụng kiểu lowercase_name như từ đầu project tới giờ. Quy ước đặt tên này có lẽ là rất thân thuộc nếu như bạn tới với Sub-Series này từ nguồn nào đó khác như Google tìm kiếm và đã biết tới ngôn ngữ PHP trước đó. Còn về mặt cấu trúc thư mục chứa mã nguồn thì đây là cấu trúc mà mình đang sử dụng hiện tại:

simplicity-dsa-c
├── bin
│   └── main.exe
└── src
    ├── array
    │   ├── concat.c
    │   ├── destroy.c
    │   ├── from.c
    │   ├── new.c
    │   └── slice.c
    ├── array.h
    ├── book
    │   ├── destroy.c
    │   └── new.c
    ├── book.h
    ├── list
    │   ├── item
    │   │   └── ...
    │   └── ...
    ├── list.h
    └── main.c

Source code: GitHub.com

Mỗi một kiểu dữ liệu sẽ có một thư mục đại diện riêng ví dụ array, list, v.v… Cái book là từ ví dụ của những bài viết trước đó và có thể sẽ không phải sử dụng lại nhưng mình vẫn tạm thời giữ lại cho đến khi thực hiện xong cái mini project này. Bên cạnh đó là các tệp khai báo header.h sẽ được đặt cùng cấp với các thư mục đại diện để cú pháp sử dụng sẽ không phải viết tên của thư mục đại diện khi #include, ví dụ code ở main thay vì #include "list/list.h" thì chúng ta chỉ cần viết #include "list.h".

[Imperative Programming + C] Bài 16 – Simplicity DSA List (tiếp theo)

Nguồn: viblo.asia

Bài viết liên quan

WebP là gì? Hướng dẫn cách để chuyển hình ảnh jpg, png qua webp

WebP là gì? WebP là một định dạng ảnh hiện đại, được phát triển bởi Google

Điểm khác biệt giữa IPv4 và IPv6 là gì?

IPv4 và IPv6 là hai phiên bản của hệ thống địa chỉ Giao thức Internet (IP). IP l

Check nameservers của tên miền xem website trỏ đúng chưa

Tìm hiểu cách check nameservers của tên miền để xác định tên miền đó đang dùn

Mình đang dùng Google Domains để check tên miền hàng ngày

Từ khi thông báo dịch vụ Google Domains bỏ mác Beta, mình mới để ý và bắt đầ