Các kiểu dữ liệu trong C++: số nguyên, số thực, char, bool

kiểu dữ liệu trong C++

Trong bài này chúng ta sẽ tìm hiểu các kiểu dữ liệu trong C++: số nguyên, số thực, ký tự, kiểu logic, mỗi kiểu chiếm bao nhiêu byte và lưu được giá trị tới đâu. Chọn sai kiểu dữ liệu là nguồn gốc của rất nhiều lỗi khó tìm như tràn số, chia ra kết quả sai hay so sánh số thực không bằng nhau, nên bài này mình sẽ chạy thử từng trường hợp cho các bạn thấy tận mắt nhé.

Bài 3 trong Serie Lập trình C++ từ A tới Z

Kiểu dữ liệu là gì?

Kiểu dữ liệu (data type) cho trình biên dịch biết 2 điều về một biến: cần dành ra bao nhiêu byte bộ nhớ, và các bit trong đó được hiểu như thế nào.

Các bạn hình dung bộ nhớ như dãy ngăn tủ. Kiểu dữ liệu giống như loại hộp đựng: hộp đựng bút thì nhỏ, hộp đựng giày thì to. Bỏ đôi giày vào hộp bút là không vừa, cũng như cố cất số 5 tỷ vào kiểu int vậy.

C++ chia kiểu dữ liệu thành 3 nhóm:

Sơ đồ 3 nhóm kiểu dữ liệu trong C++: kiểu cơ bản, kiểu dẫn xuất và kiểu do người dùng định nghĩa

Bài này tập trung vào nhóm kiểu cơ bản. Mảng, con trỏ, tham chiếu và các kiểu tự định nghĩa như class sẽ có bài riêng trong serie.

Các kiểu dữ liệu cơ bản trong C++

Kiểu Kích thước thường gặp Lưu gì Ví dụ
bool 1 byte Đúng hoặc sai true, false
char 1 byte 1 ký tự 'A', '7'
short 2 byte Số nguyên nhỏ -300
int 4 byte Số nguyên 2020
long long 8 byte Số nguyên rất lớn 9000000000
float 4 byte Số thực, khoảng 7 chữ số chính xác 3.14f
double 8 byte Số thực, khoảng 15 chữ số chính xác 3.14159

Số nguyên còn có bản unsigned (không dấu) như unsigned int, chỉ lưu số từ 0 trở lên nhưng giới hạn trên lớn gấp đôi.

Kiểm tra kích thước bằng sizeof

Chuẩn C++ không quy định cứng mỗi kiểu bao nhiêu byte, mà chỉ quy định kích thước tối thiểu. Muốn biết chính xác trên máy mình thì dùng toán tử sizeof:

#include <iostream>

int main()
{
    std::cout << "bool:        " << sizeof(bool) << " byte\n";
    std::cout << "char:        " << sizeof(char) << " byte\n";
    std::cout << "short:       " << sizeof(short) << " byte\n";
    std::cout << "int:         " << sizeof(int) << " byte\n";
    std::cout << "long:        " << sizeof(long) << " byte\n";
    std::cout << "long long:   " << sizeof(long long) << " byte\n";
    std::cout << "float:       " << sizeof(float) << " byte\n";
    std::cout << "double:      " << sizeof(double) << " byte\n";
    std::cout << "long double: " << sizeof(long double) << " byte\n";
    return 0;
}

Kết quả trên máy Linux 64-bit của mình:

Kết quả sizeof các kiểu dữ liệu C++ trên Linux 64-bit: int 4 byte, long 8 byte, double 8 byte

Lưu ý: Kiểu long là 8 byte trên Linux và macOS 64-bit, nhưng chỉ 4 byte trên Windows (kể cả khi dùng g++ của MSYS2). Code chạy đúng trên máy này có thể tràn số trên máy khác. Cần số nguyên 8 byte thì dùng hẳn long long, hoặc các kiểu cố định kích thước ở cuối bài.

Giới hạn giá trị của kiểu số nguyên

Mỗi kiểu chỉ lưu được giá trị trong một khoảng nhất định. Thay vì học thuộc, C++ có sẵn std::numeric_limits trong thư viện <limits> để hỏi:

#include <iostream>
#include <limits>

int main()
{
    std::cout << "int nho nhat: " << std::numeric_limits<int>::min() << '\n';
    std::cout << "int lon nhat: " << std::numeric_limits<int>::max() << '\n';
    std::cout << "unsigned int lon nhat: " << std::numeric_limits<unsigned int>::max() << '\n';
    std::cout << "long long lon nhat: " << std::numeric_limits<long long>::max() << '\n';
    return 0;
}

Giới hạn của int, unsigned int và long long in ra bằng std::numeric_limits

int 4 byte lưu được khoảng từ âm 2,1 tỷ tới 2,1 tỷ. Con số này nghe thì lớn, nhưng dân số thế giới, số tiền tính bằng đồng hay số mili giây trong 1 tháng đều vượt quá rồi đấy.

Tràn số: khi giá trị vượt giới hạn

Chuyện gì xảy ra nếu cộng thêm 1 vào giá trị lớn nhất của int?

#include <iostream>

int main()
{
    int luotXem = 2147483647;      // gia tri lon nhat cua int
    luotXem = luotXem + 1;
    std::cout << "Luot xem: " << luotXem << '\n';

    unsigned int soSach = 0;
    soSach = soSach - 1;
    std::cout << "So sach: " << soSach << '\n';
    return 0;
}

Tràn số: int lớn nhất cộng 1 thành số âm, unsigned int 0 trừ 1 thành 4294967295

Lượt xem đang dương bỗng thành số âm, còn số sách từ 0 trừ 1 thành hơn 4 tỷ. Đây gọi là tràn số (overflow). Nó giống đồng hồ công tơ mét trên xe máy: chạy quá 99999 km thì quay về 00000.

Hai trường hợp này khác nhau một chút:

  • Với kiểu unsigned, quay vòng là hành vi được chuẩn C++ quy định, lần nào cũng ra đúng 4294967295.
  • Với kiểu có dấu như int, tràn số là undefined behavior (hành vi không xác định). Lần này in ra số âm, nhưng trình biên dịch được phép làm bất cứ điều gì, kể cả tối ưu bỏ luôn đoạn code của các bạn.

Lưu ý: g++ không báo lỗi hay cảnh báo gì khi tràn số lúc chạy. Cách phòng là chọn kiểu đủ lớn ngay từ đầu, ví dụ đếm tiền, đếm mili giây thì dùng long long. Và cẩn thận khi trừ 2 số unsigned: kết quả âm sẽ thành số dương rất lớn.

Số thực: float và double

Số thực trong máy tính được lưu ở dạng nhị phân, nên nhiều số thập phân quen thuộc như 0.1 không lưu chính xác được. Thử in 0.1 với 20 chữ số sau dấu phẩy:

#include <iostream>
#include <iomanip>

int main()
{
    float  f = 0.1f;
    double d = 0.1;

    std::cout << std::setprecision(20);
    std::cout << "float  0.1 = " << f << '\n';
    std::cout << "double 0.1 = " << d << '\n';
    std::cout << "0.1 + 0.2 == 0.3 ? " << (0.1 + 0.2 == 0.3) << '\n';
    return 0;
}

Sai số số thực: float và double không lưu chính xác 0.1, phép so sánh 0.1 + 0.2 == 0.3 cho kết quả sai

Giống như viết 1/3 ra số thập phân thì chỉ được 0.3333… chứ không bao giờ đủ, máy tính viết 0.1 ra nhị phân cũng bị cắt bớt. double sai số nhỏ hơn float rất nhiều, nhưng vẫn có sai số. Vì vậy 0.1 + 0.2 == 0.3 cho kết quả 0, tức là sai.

Mặc định các bạn nên dùng double. Chỉ dùng float khi cần tiết kiệm bộ nhớ, ví dụ mảng hàng triệu phần tử, hoặc trên vi điều khiển không có phần cứng tính số double.

Lưu ý: Không so sánh 2 số thực bằng ==. Hãy kiểm tra hiệu của chúng có đủ nhỏ không, ví dụ std::abs(a - b) < 1e-9. Còn tiền tệ thì nên lưu bằng số nguyên theo đơn vị nhỏ nhất (đồng, xu) để không bị sai số.

Chia số nguyên và ép kiểu với static_cast

Một lỗi rất hay gặp khi chia 2 số nguyên:

#include <iostream>

int main()
{
    int tong = 7, soNguoi = 2;

    double sai = tong / soNguoi;
    double dung = static_cast<double>(tong) / soNguoi;

    std::cout << "tong / soNguoi = " << sai << '\n';
    std::cout << "static_cast<double>(tong) / soNguoi = " << dung << '\n';
    return 0;
}

Chia 2 số nguyên 7 / 2 ra 3, ép kiểu static_cast double ra 3.5

Dù biến sai có kiểu double, phép chia tong / soNguoi vẫn là chia 2 số int, nên kết quả bị cắt phần thập phân thành 3 trước khi gán. Muốn ra 3.5 thì phải đổi ít nhất một số hạng sang double trước khi chia.

Việc đổi kiểu này gọi là ép kiểu (type casting). C++ dùng static_cast<kiểu_mới>(giá_trị). Nếu đã học C thì các bạn sẽ quen kiểu viết (double)tong. Cách viết đó vẫn chạy, nhưng trong C++ nên dùng static_cast vì nó dễ tìm khi đọc code, và trình biên dịch kiểm tra chặt hơn.

Kiểu char và bool

char lưu 1 ký tự, viết trong cặp nháy đơn 'A'. Bên dưới, char thực chất là một số nguyên nhỏ: mã của ký tự trong bảng ASCII. bool chỉ có 2 giá trị true và false.

#include <iostream>

int main()
{
    char kyTu = 'A';
    bool conSach = true;

    std::cout << "kyTu = " << kyTu << '\n';
    std::cout << "Ma ASCII cua kyTu = " << static_cast<int>(kyTu) << '\n';
    std::cout << "kyTu + 1 = " << static_cast<char>(kyTu + 1) << '\n';
    std::cout << "conSach = " << conSach << '\n';
    std::cout << std::boolalpha << "conSach = " << conSach << '\n';
    return 0;
}

Kiểu char in ra ký tự A và mã ASCII 65, kiểu bool in ra 1 và true với boolalpha

Mặc định std::cout in bool thành 1 hoặc 0. Thêm std::boolalpha thì in ra chữ true, false cho dễ đọc.

Phân biệt 'A' (nháy đơn, kiểu char, đúng 1 ký tự) với "A" (nháy kép, là một chuỗi). Muốn lưu cả câu như tên sách thì dùng std::string, mình sẽ dành hẳn một bài cho kiểu này.

Kiểu số nguyên cố định kích thước trong cstdint

Như đã thấy, long mỗi máy một khác. Khi cần chắc chắn số bit, ví dụ đọc dữ liệu từ file, gửi qua mạng hay làm việc với thanh ghi vi điều khiển, C++ có các kiểu trong thư viện <cstdint>:

#include <iostream>
#include <cstdint>

int main()
{
    std::uint8_t  doSang = 255;          // dung 8 bit, 0..255
    std::int16_t  nhietDo = -40;         // dung 16 bit co dau
    std::uint32_t maSach = 4000000000u;  // dung 32 bit khong dau

    std::cout << "doSang  = " << +doSang << " (" << sizeof(doSang) << " byte)\n";
    std::cout << "nhietDo = " << nhietDo << " (" << sizeof(nhietDo) << " byte)\n";
    std::cout << "maSach  = " << maSach << " (" << sizeof(maSach) << " byte)\n";
    return 0;
}

Các kiểu uint8_t, int16_t, uint32_t trong cstdint và kích thước 1, 2, 4 byte

Tên kiểu đọc là hiểu: int16_t là số có dấu 16 bit, uint8_t là số không dấu 8 bit. Dấu + trước doSang có tác dụng in ra số 255 thay vì ký tự, vì uint8_t thực chất là unsigned char.

Các bạn học lập trình vi điều khiển sẽ gặp các kiểu này ở khắp nơi, ví dụ trong thư viện HAL của STM32.

Áp dụng vào chương trình thư viện

Xuyên suốt serie này chúng ta sẽ dần xây một chương trình quản lý thư viện sách. Bước đầu tiên: chọn kiểu dữ liệu cho thông tin của một cuốn sách.

#include <iostream>
#include <string>

int main()
{
    std::string tenSach = "Lap trinh C++ co ban";
    int namXuatBan = 2020;
    int soTrang = 356;
    double giaBan = 125000.0;
    bool conTrongKho = true;

    std::cout << "Ten sach:   " << tenSach << '\n';
    std::cout << "Nam XB:     " << namXuatBan << '\n';
    std::cout << "So trang:   " << soTrang << '\n';
    std::cout << "Gia ban:    " << giaBan << " dong\n";
    std::cout << "Con hang:   " << std::boolalpha << conTrongKho << '\n';
    return 0;
}

Chương trình in thông tin một cuốn sách với các kiểu string, int, double, bool

Năm xuất bản và số trang không bao giờ quá 2 tỷ nên int là đủ. Còn trạng thái trong kho chỉ có còn hoặc hết nên dùng bool. Riêng giá bán mình tạm dùng double, bài tập dưới đây sẽ để các bạn suy nghĩ xem có nên đổi không.

Bài tập

  1. Chạy chương trình sizeof trên máy của bạn. Kết quả có giống của mình không? Nếu dùng Windows, long được mấy byte?
  2. Một video có 3 tỷ lượt xem. Nên lưu số lượt xem bằng kiểu gì? Viết chương trình lưu và in con số đó ra.
  3. Viết chương trình nhập điểm 3 môn (số nguyên), in ra điểm trung bình có phần thập phân.
  4. Giá sách nên lưu bằng double hay số nguyên? Hãy giải thích dựa vào phần sai số số thực.

Lời giải sẽ có trong bài tổng hợp bài tập C++ cơ bản nhé.

Kết

Hiểu các kiểu dữ liệu trong C++ giúp các bạn tránh được 3 lỗi kinh điển: tràn số, chia số nguyên mất phần thập phân, và so sánh số thực bằng ==. Cả 3 lỗi đều không bị g++ báo, nên chỉ có cách chọn kiểu cho đúng ngay từ đầu.

Quy tắc nhanh: số nguyên thông thường dùng int, số lớn dùng long long, số thực dùng double, cần đúng số bit thì dùng <cstdint>. Các bạn từng học kiểu dữ liệu trong lập trình C sẽ thấy phần lớn giống nhau, C++ chỉ bổ sung thêm bool, std::string và cách ép kiểu static_cast.

Bài trước chúng ta đã học cú pháp C++ cơ bản, bài tiếp theo sẽ đi vào cách khai báo biến và hằng với const, constexpr, auto. Có thắc mắc gì các bạn để lại bình luận bên dưới nhé.

Rate this post

Để lại một bình luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *