🌱 Embedded C - Data Type (Kiểu Dữ Liệu) và Những Điều Cần Biết

🌱 Embedded C - Data Type (Kiểu Dữ Liệu) và Những Điều Cần Biết

    Trong lập trình C, Data Type (Kiểu dữ liệu) là một trong những khái niệm nền tảng mà bất kỳ lập trình viên nào cũng cần nắm vững. Ở bài viết trước chúng ta đã bàn đến khái niệm Variable/Biến và có nhắc đến Data Type – thứ quyết định biến của bạn lưu trữ dữ liệu như thế nào, chiếm bao nhiêu bộ nhớ, và cách CPU hiểu dữ liệu đó ra sao.

    Đặc biệt, trong Embedded C, việc chọn kiểu dữ liệu phù hợp không chỉ giúp code đúng mà còn tối ưu hóa tài nguyên trên các vi điều khiển (MCU) có bộ nhớ và sức mạnh xử lý hạn chế.

    Hãy cùng khám phá Data Type trong C qua bài viết này, với những góc nhìn thực tế và ví dụ minh họa!


Mục Lục


Data Type - Kiểu Dữ Liệu Là Gì?

    Data Type (kiểu dữ liệu) trong C xác định loại dữ liệu mà một biến có thể lưu trữ, kích thước của biến đó trên bộ nhớ (tính bằng byte), và cách CPU diễn giải dữ liệu đó. Chẳng hạn, khi bạn khai báo một biến int temp = 25;, kiểu dữ liệu int cho compiler biết rằng:

  • Biến temp sẽ lưu một số nguyên.
  • Nó chiếm một số byte trên bộ nhớ (thường là 2 hoặc 4 byte, tùy kiến trúc).
  • CPU sẽ xử lý giá trị 25 như một số nguyên, không phải số thực hay ký tự.

    Trong lập trình nhúng, việc hiểu và chọn đúng Data Type là cực kỳ quan trọng, vì nó ảnh hưởng trực tiếp đến hiệu suấtsự tiết kiệm tài nguyên của hệ thống.

    Các đặc điểm quan trọng khi nhắc đến một Data Type:

  • Size - Kích thước bộ nhớ: Kích thước của biến được tạo theo kiểu dữ liệu đó (1 byte, 2 bytes, 4 bytes, ...) khi nằm trên bộ nhớ, phụ thuộc vào ngôn ngữ và kiến trúc hệ thống (32-bit hoặc 64-bit).
  • Range - Rải giá trịquy định phạm vi giá trị mà biến có thể chứa.
  • Format Specifier - Định dạng: Sử dụng để chỉ định định dạng đầu ra hoặc đầu vào của dữ liệu khi làm việc với các hàm như printf, scanf (trong C/C++) hoặc các phương thức định dạng chuỗi.
  • Các toán tử hỗ trợ: Kiểu dữ liệu xác định các phép toán có thể áp dụng, như cộng, trừ, so sánh, hoặc nối chuỗi, ...

    C hỗ trợ khá nhiều kiểu dữ liệu, để dễ hiểu, mình chia thành 3 loại sau (Một số tài liệu C có thể có cách chia khác):

C Data types


Primary Data Type - Các Kiểu Dữ Liệu "Nguyên Thủy" Trong C

    Primary Data Type - là các kiểu dữ liệu cơ bản được C cung cấp, một số kiểu dữ liệu có thể kể đến như int, char, float, double, void, bool (được hỗ trợ trong C99 trở đi thông qua thư viện <stdbool.h>).

    Các kiểu dữ liệu này rất cơ bản và đã có trong nhiều tài liệu học lập trình C, nên mình sẽ không đi sâu, chỉ liệt kê thông qua bảng sau:

Kiểu dữ liệu Kích thước (byte) Phạm vi giá trị Format Specifier
char 1 -128 đến 127 (signed) hoặc 0 đến 255 (unsigned) %c
int 4 -2,147,483,648 đến 2,147,483,647 %d hoặc %i
float 4 ±3.4E-38 đến ±3.4E+38 (6 chữ số thập phân chính xác) %f hoặc %e
double 8 ±1.7E-308 đến ±1.7E+308 (15 chữ số thập phân chính xác) %lf hoặc %le
bool 1 true (1) hoặc false (0) %d (thường dùng để hiển thị 0 hoặc 1)
void Không có kích thước Không lưu trữ giá trị Không có format specifier

Lưu ý: Các biến khi được tạo ra bởi các kiểu dữ liệu này, thực chất khi lưu trên bộ nhớ cũng ở dạng binary cả! Chỉ khác là khi chúng ta sử dụng chúng (cộng, trừ, nhân, chia, in ra giá trị, ...) thì cách lấy giá trị sẽ phụ thuộc vào từng kiểu dữ liệu.

char thực chất là số nguyên

    Kiểu dữ liệu char vẫn lưu trữ giống như quy tắc lưu trữ số nguyên ở trên bộ nhớ, chỉ khác là khi chúng ta đọc ra hoặc biểu diễn chúng, giá trị số nguyên đó sẽ được mã hóa theo bảng mã ASCII (Chuẩn ISO/IEC 10646) để phục vụ mục đích biểu diễn các ký tự.

Ví dụ bảng mã ASCII
Ví dụ bảng mã ASCII

    Ví dụ: Khi tạo một biến char var = 'A', thì biến var này đang có giá trị trên bộ nhớ là 65 (0b01000001). Nhưng lúc chúng ta đọc/in ra với format specifier là %c thì máy tính sẽ hiển thị là kí tự 'A'. Nhưng nó vẫn chỉ là số nguyên, vì chúng ta có thể thực hiện các phép toán trên nó như một số nguyên khác.

char var = 'A';
var = var + 1;
printf("var = %c\n", var);    // 'B'
printf("var = %d\n", var);    // 66

    Trong lập trình nhúng, nên sử dụng char thay cho int để tiết kiệm bộ nhớ khi cần khi lưu trữ các số nguyên với kích thước nhỏ.

Cách lưu trữ số float trên bộ nhớ

    Số thực Float được biểu diễn theo dựa trên tiêu chuẩn IEEE 754, gọi là cách biểu diễn dấu phẩy động. Vì phần kiến thức khá dài nên mình đã tách ra một bài viết khá chi tiết về Biểu diễn Dấu phẩu động - Floating Point tại đây!

Điều đặc biệt về void data type

    void biểu thị "không có giá trị" hoặc "không có kiểu dữ liệu cụ thể". Nó được sử dụng để chỉ ra rằng một hàm hoặc con trỏ không trả về giá trị hoặc không liên quan đến một kiểu dữ liệu xác định. Tức là không thể tạo một biến đơn giản theo kiểu void - vì nó không chứa giá trị cũng như không có kích thước!

void var;   // invalid

    void sẽ liên quan đến phần Pointer và Function, mình sẽ có một bài viết chi tiết về void sau các bài viết về 2 phần kiến thức trên!

bool cũng khá thú vị

    Các phiên bản trước C99, khi muốn biểu diễn logic true/false, dev thường chỉ đơn giản sử dụng 0 (false) và 1 (true). Nhưng các số khác 0 cũng có thể hiểu là true, vì nó thỏa mãn điều kiện true với các phép toán tử logic và câu lệnh điều kiện. Vì vậy đôi khi có thể gây nhầm lẫn - số 16 cũng là true nhưng không phải là 1.

    Từ bản C99 trở đi, C cung cấp kiểu nguyên thủy _Bool, thường chiếm 1 byte trong bộ nhớ (trên hầu hết các hệ thống). Kiểu bool được định nghĩa trong thư viện <stdbool.h> để thay cho _Bool.

    Phạm vi giá trị: Chỉ nhận giá trị 0 (false) hoặc 1 (true). Nếu gán một giá trị khác (ví dụ, số nguyên hoặc số thực), trình biên dịch sẽ chuyển đổi:

  • Giá trị 0 được coi là false.
  • Bất kỳ giá trị khác 0 nào được chuyển thành 1 (true).
_Bool b = 3.14; // b = 1 (true, vì 3.14 != 0)
b = 0.0;        // b = 0 (false, vì 0.0 == 0)


Type modifiers - Các bổ trợ cho kiểu Primary Data Type

    Để bổ trợ cho các kiểu dữ liệu Primary Data Type, C cung cấp thêm một số Keyword gọi là Type modifiers để có thể chỉnh sửa các kiểu dữ liệu sẵn có nhằm đảm bảo mục đích sử dụng của dev. Các từ khóa như signedunsigned, long, short, const, volatile sẽ đi kèm với các kiểu dữ liệu Primary:

long, short

    Trong lập trình C nhúng, với kiểu dữ liệu int, chúng ta sẽ mất 4 byte bộ nhớ, để tiết kiệm, khi lưu các số nguyên nhỏ, chúng ta có thể chọn char. Nhưng char chỉ lưu trữ được giá trị tối đa là 127 (255 với unsigned char). Khi cần lưu các giá trị cỡ 300, hay 1000, nếu dùng char thì không đủ, dùng int thì tốn bộ nhớ. Vì vậy C sinh ra từ khóa shortnhằm đáp ứng các nhu cầu khác nhau về hiệu suất, bộ nhớ, và phạm vi giá trị trong lập trình. short int chỉ chiếm 2 byte trên bộ nhớ.

    long thì ngược lại, khi cần mở rộng hơn để lưu trữ các giá trị vượt khỏi int. Ví dụ long long int sẽ chiếm 8 byte trên bộ nhớ.

signedunsigned

    Trong C, kiểu dữ liệu char và int mặc định là các kiểu dữ liệu có dấu (signed), chúng sẽ có bit đầu tiên biểu thị là bit dấu - theo cách biểu diễn mã bù 2 - Two's complement. Đối với embedded thì kiểu dữ liệu này có 2 vấn đề:

  • Mất đi 1 bit để làm bit dấu và cách biểu diễn không tự nhiên. Trong khi các thanh ghi của vi điều khiển thì các bit độc lập nhau về mặt chức năng.
  • Rải giá trị số dương bị thu hẹp, trong khi ta thường sử dụng số dương hơn. Dễ thấy với kiểu char, 8 bit có thể biểu thị 256 giá trị (mong muốn từ 0-255 nếu chỉ dùng số dương). Nhưng kiểu char mặc định chỉ có rải từ -128 đến 127.
    Từ đó kiểu unsigned ra đời để phục vụ cho các số nguyên không dấu, và được sử dụng cực kỳ phổ biến trong Embedded.

const

    Từ khóa const là một type qualifier được sử dụng để khai báo các biến hoặc đối tượng mà giá trị của chúng không thể thay đổi sau khi được khởi tạo. Nó giúp tăng tính an toàn, rõ ràng, và tối ưu hóa code.

    const có thể được áp dụng trong nhiều ngữ cảnh khác nhau, bài viết này sẽ chỉ nói đến các biến đơn giản với Primary Data Type. Khi một biến được khai báo với const, nó phải được khởi tạo ngay tại thời điểm khai báo, và giá trị của nó không thể thay đổi sau đó bởi phép gán.

#include <stdio.h>

int main() {
    const int MAX = 100; // const variable
    printf("MAX: %d\n", MAX);

    // MAX = 200; // Compile Error: Can not assign the value of const variable
    return 0;
}

volatile

    Từ khóa volatile là một type qualifier được sử dụng để khai báo rằng giá trị của một biến có thể thay đổi bất ngờ, ngoài luồng điều khiển của chương trình. Điều này báo hiệu cho trình biên dịch không được tối ưu hóa các truy cập đến biến đó, đảm bảo rằng mọi thao tác đọc/ghi đều được thực hiện trực tiếp trên bộ nhớ.

    Từ khóa nay khá nâng cao và quan trọng nên mình có một bài viết riêng tại đây!

    ➥ Avoid Compiler Optimize với Volatile Keyword trong C

User-Defined Data Type - Kiểu dữ liệu người dùng tự định nghĩa

    Lưu số nguyên chúng ta có kiểu int, lưu số thực có kiểu float, vậy khi lưu thông tin của một người (tên, tuổi, chiều cao) thì chúng ta sẽ cần tạo ra 3 biến khác nhau, sẽ khó quản lý. Vì vậy, C sinh ra các kiểu dữ liệu để chúng ta có thể quản lý các thông tin phức hợp như trên, mà chỉ cần tạo ra một biến. Nhưng nhu cầu sử dụng quản lý các thành phần là khác nhau giữa mỗi người dùng, vì vậy C để người dùng tự định nghĩa kiểu dữ liệu họ muốn!

    ➤ User-Defined Data Types (kiểu dữ liệu do người dùng tự định nghĩa) là các kiểu dữ liệu được lập trình viên tạo ra để đáp ứng nhu cầu tổ chức và quản lý dữ liệu phức tạp hơn so với các kiểu dữ liệu nguyên thủy (int, char, float, v.v.) hoặc kiểu dẫn xuất cơ bản (mảng, con trỏ).

    Các kiểu dữ liệu này cho phép định nghĩa cấu trúc dữ liệu tùy chỉnh, giúp mã nguồn trở nên rõ ràng, dễ bảo trì và tái sử dụng. Trong C, các kiểu dữ liệu do người dùng tự định nghĩa chủ yếu bao gồm Structure (struct), Union (union), và Enumeration (enum).

    Phần này cũng là một topic lớn nên mình sẽ có các bài viết khác để giới thiệu chi tiết hơn.

    Ví dụ về struct:

  1. #include <stdio.h>
  2. #include <string.h>
  3. // Create a Struct Data Type
  4. struct Person {
  5. char name[50];
  6. int age;
  7. float height;
  8. };
  9. int main() {
  10. // Init a Struct variable
  11. struct Person person1;
  12. strcpy(person1.name, "Nguyen Van A");
  13. person1.age = 25;
  14. person1.height = 1.75;
  15. // Access and print the elements of struct
  16. printf("Name: %s, Age: %d, Height: %.2f\n", person1.name, person1.age, person1.height);
  17. return 0;
  18. }

Thư viện stdint.h

    Trong Embedded C, phần lớn thời gian dev sẽ làm việc với giá trị thanh ghi, với các giá trị đếm timer, giá trị period, frequency, duty, ... tất cả đều là số nguyên không dấu (unsigned), và có thể biểu diễn bởi các kiểu dữ liệu : int, short, char, long long int. Dễ thấy các kiểu dữ liệu này có 1 số không hề cố định về kích thước (phụ thuộc vào kiến trúc máy), và khó nhận biết về kích thước khi sử dụng, có thể dẫn đến những sự nhầm lẫn cũng như lỗi trong quá trình sử dụng.

    Vì vậy từ C99, C cung cấp thư viện stdint.h nhằm định nghĩa các kiểu số nguyên chuẩn theo kích thước để tiện cho dev sử dụng.

    Cụ thể, các kiểu dữ liệu chính trong stdint.h bao gồm:

Kiểu dữ liệu trong stdint.h Kiểu dữ liệu tương ứng Ý nghĩa
int8_t signed char Số nguyên có dấu, 8-bit, giá trị từ -128 đến 127.
int16_t short hoặc signed short Số nguyên có dấu, 16-bit, giá trị từ -32,768 đến 32,767.
int32_t int hoặc signed int Số nguyên có dấu, 32-bit, giá trị từ -2,147,483,648 đến 2,147,483,647.
int64_t long long hoặc signed long long Số nguyên có dấu, 64-bit, giá trị từ -9,223,372,036,854,775,808 đến 9,223,372,036,854,775,807.
uint8_t unsigned char Số nguyên không dấu, 8-bit, giá trị từ 0 đến 255.
uint16_t unsigned short Số nguyên không dấu, 16-bit, giá trị từ 0 đến 65,535.
uint32_t unsigned int Số nguyên không dấu, 32-bit, giá trị từ 0 đến 4,294,967,295.
uint64_t unsigned long long Số nguyên không dấu, 64-bit, giá trị từ 0 đến 18,446,744,073,709,551,615.
int_leastN_t Tùy compiler Số nguyên có dấu, đảm bảo ít nhất N bit (N = 8, 16, 32, 64). Dùng khi cần kích thước tối thiểu.
uint_leastN_t Tùy compiler Số nguyên không dấu, đảm bảo ít nhất N bit (N = 8, 16, 32, 64). Dùng khi cần kích thước tối thiểu.
int_fastN_t Tùy compiler Số nguyên có dấu, ít nhất N bit, tối ưu cho tốc độ xử lý. Dùng khi cần hiệu suất cao.
uint_fastN_t Tùy compiler Số nguyên không dấu, ít nhất N bit, tối ưu cho tốc độ xử lý. Dùng khi cần hiệu suất cao.
intptr_t Tùy compiler Số nguyên có dấu đủ lớn để chứa con trỏ. Dùng để lưu địa chỉ bộ nhớ.
uintptr_t Tùy compiler Số nguyên không dấu đủ lớn để chứa con trỏ. Dùng để lưu địa chỉ bộ nhớ.
intmax_t Tùy compiler Số nguyên có dấu có kích thước lớn nhất được hỗ trợ bởi trình biên dịch.
uintmax_t Tùy compiler Số nguyên không dấu có kích thước lớn nhất được hỗ trợ bởi trình biên dịch.
INT8_MIN, INT8_MAX - Macro xác định giá trị nhỏ nhất (-128) và lớn nhất (127) của int8_t.
UINT8_MAX - Macro xác định giá trị lớn nhất (255) của uint8_t.
INT16_MIN, INT16_MAX - Macro xác định giá trị nhỏ nhất (-32,768) và lớn nhất (32,767) của int16_t.
UINT16_MAX - Macro xác định giá trị lớn nhất (65,535) của uint16_t.
INT32_MIN, INT32_MAX - Macro xác định giá trị nhỏ nhất (-2,147,483,648) và lớn nhất (2,147,483,647) của int32_t.
UINT32_MAX - Macro xác định giá trị lớn nhất (4,294,967,295) của uint32_t.
INT64_MIN, INT64_MAX - Macro xác định giá trị nhỏ nhất (-9,223,372,036,854,775,808) và lớn nhất (9,223,372,036,854,775,807) của int64_t.
UINT64_MAX - Macro xác định giá trị lớn nhất (18,446,744,073,709,551,615) của uint64_t.

Từ khóa typedef

    Các kiểu dữ liệu trong stdint.h đơn giản cũng chỉ là đổi tên theo các kiểu dữ liệu có sẵn (Primary Data Type và Type Modifier). C cung cấp từ khóa typedef rất thuận tiện để định nghĩa tên cho một kiểu dữ liệu mới.

typedef existing_type new_type_name;
// existing_type: Kiểu dữ liệu gốc (có thể là kiểu cơ bản như int, char, hoặc kiểu phức tạp như cấu trúc, mảng, con trỏ).
// new_type_name: Tên mới cho kiểu dữ liệu.

    Ví dụ một số type được định nghĩa trong stdint.h:

/* 7.18.1.1  Exact-width integer types */
typedef signed char int8_t;
typedef unsigned char   uint8_t;
typedef short  int16_t;
typedef unsigned short  uint16_t;
typedef int  int32_t;
typedef unsigned   uint32_t;
__MINGW_EXTENSION typedef long long  int64_t;
__MINGW_EXTENSION typedef unsigned long long   uint64_t;

Video về C Data Type

Kết Luận

    Data Type là nền tảng để lập trình viên kiểm soát cách dữ liệu được lưu trữ và xử lý trong C, đặc biệt trong lập trình nhúng. Việc chọn đúng kiểu dữ liệu không chỉ giúp code đúng mà còn tối ưu hóa bộ nhớ và hiệu suất trên các vi điều khiển hạn chế tài nguyên. Hãy nhớ: nhỏ gọn, chính xác, và phù hợp với phần cứng là ba yếu tố then chốt khi làm việc với Data Type trong Embedded C.

    Trong các bài tiếp theo, chúng ta sẽ khám phá sâu hơn về Storage ClassPointer, những khái niệm quan trọng không thể thiếu trong lập trình nhúng. Nếu bạn có câu hỏi hoặc góp ý, hãy để lại comment bên dưới nhé.

>>>>>> Follow ngay <<<<<<<

Để nhận được những bài học miễn phí mới nhất nhé 😊
Chúc các bạn học tập tốt 😊

Nguyễn Văn Nghĩa

Mình là một người thích học hỏi và chia sẻ các kiến thức về Nhúng IOT.

Đăng nhận xét

Mới hơn Cũ hơn
//