Chuyển đến nội dung

Types, Values & the Type System

Explore C++'s fundamental types, fixed-width integers, initialization forms, auto deduction, and the const system that enforces correctness at compile time.

The Type System: Your First Line of Defense

C++ is a statically typed language — every expression has a type known at compile time. The type system is not just bookkeeping; it is your most powerful tool for catching bugs before your code ever runs. A well-typed C++ program carries its invariants in the type system itself, making entire classes of errors impossible.

The type of a variable determines three things: how much memory it occupies, what values it can represent, and what operations are valid on it. Getting types right is the foundation of correct C++ code.

Fundamental Types

C++ provides a set of built-in types that map directly to hardware capabilities. Their exact sizes vary by platform, but the standard guarantees minimum ranges:

types.cppc++23
#include <iostream>
#include <cstdint>   // Fixed-width integer types
#include <limits>    // std::numeric_limits

int main() {
    // Boolean
    bool is_valid = true;           // true or false, typically 1 byte

    // Character types
    char letter = 'A';             // At least 8 bits, holds ASCII
    char8_t  u8ch  = u8'A';       // C++20: exactly 8 bits, UTF-8
    char16_t u16ch = u'\u00E9';   // At least 16 bits, UTF-16
    char32_t u32ch = U'\U0001F600'; // At least 32 bits, UTF-32

    // Integer types (sizes are minimums, typical x86-64 shown)
    short       s = 32000;         // At least 16 bits (16)
    int         i = 2'000'000;     // At least 16 bits (32) — digit separator C++14
    long        l = 100'000L;      // At least 32 bits (64 on Linux, 32 on Windows)
    long long  ll = 9'000'000'000'000'000'000LL;  // At least 64 bits

    // Unsigned variants
    unsigned int ui = 42u;         // Cannot be negative

    // Floating-point types
    float       f = 3.14f;         // ~7 decimal digits precision
    double      d = 3.141592653589793; // ~15 decimal digits precision
    long double ld = 3.14159265358979323846L;

    // Fixed-width types from <cstdint> — use these when size matters
    std::int32_t  exact = 42;      // Exactly 32 bits, guaranteed
    std::uint64_t big   = 18'446'744'073'709'551'615ULL; // Exactly 64 bits
    std::int_fast32_t fast = 42;   // At least 32 bits, fastest available
    std::size_t index = 0;         // Unsigned, suitable for sizes and indices

    std::cout << "int: " << sizeof(int) << " bytes, max = "
              << std::numeric_limits<int>::max() << '\n';
}
Đầu ra

Nhấn Chạy để biên dịch và chạy ví dụ này.

Initialization: The Right Way

C++ đã phát triển nhiều cú pháp khởi tạo. Khởi tạo đồng nhất (nẹp){}được ưu tiên trong C++ hiện đại vì nó ngăn chặn việc thu hẹp chuyển đổi:

initialization.cppc++23
#include <iostream>
#include <string>

int main() {
    // Direct initialization
    int a(42);              // OK but can be confused with function declaration

    // Copy initialization
    int b = 42;             // OK, familiar from C

    // Uniform (brace) initialization — preferred in modern C++
    int c{42};              // Direct-list-initialization
    int d = {42};           // Copy-list-initialization

    // Brace initialization PREVENTS narrowing conversions
    // double to int loses precision — the compiler will ERROR:
    // int narrow{3.14};    // ERROR: narrowing conversion
    int ok = 3.14;          // Compiles! Silently truncates to 3 (dangerous)

    // Value initialization (zero for built-in types)
    int zero{};             // Guaranteed to be 0
    double dzero{};         // Guaranteed to be 0.0
    bool bfalse{};          // Guaranteed to be false

    // auto deduction
    auto x = 42;            // int
    auto y = 3.14;          // double
    auto z = 'A';           // char
    auto s = std::string{"hello"};  // std::string (not const char*!)

    // decltype — queries the type of an expression
    decltype(x) another = 100;  // same type as x → int
    decltype(auto) ref = (x);   // int& because (x) is an lvalue expression

    std::cout << "zero-initialized int: " << zero << '\n';
    std::cout << "auto-deduced string: " << s << '\n';
}
Đầu ra

Nhấn Chạy để biên dịch và chạy ví dụ này.

Thực hành tốt

Trong C, con trỏ null được biểu thị bằng macroNULL, macro này thường mở rộng thành0hoặc(void*)0. Điều này gây ra sự mơ hồ trong C++ vì0cũng làinthợp lệ. Hãy xem xét một hàm bị quá tải cho cảintvà một loại con trỏ - truyềnNULL(là0) sẽ gọi tình trạng quá tảiintchứ không phải con trỏ.

C++11 đã giới thiệu nullptr, một từ khóa thuộc loạistd::nullptr_tcó thể chuyển đổi hoàn toàn thành bất kỳ loại con trỏ nào nhưng không thành số nguyên. Luôn sử dụngnullptrthay vìNULLhoặc0cho con trỏ null.

Tương tự, hãy ưu tiên'\0'(ký tự null) khi ý bạn là ký tự có giá trị 0 thay vì0để truyền đạt ý định rõ ràng.

  • Luôn sử dụng nullptr thay vì NULL hoặc 0 cho con trỏ null
  • nullptr có loại riêng (std::nullptr_t) và tránh sự mơ hồ quá tải
  • NULL là macro C mở rộng thành 0 - nó là số nguyên, không phải con trỏ
  • Sử dụng '\0' cho ký tự null để phân biệt với số nguyên 0

vòng loại const và loại

Vòng loạiconstcho trình biên dịch (và trình đọc của con người) biết rằng không được sửa đổi giá trị sau khi khởi tạo. Đây là một trong những công cụ quan trọng nhất trong C++ để viết mã tự ghi lại, an toàn.

constđược đọc từ phải sang trái:const int*là một con trỏ tới const int (int không thể thay đổi nhưng con trỏ thì có thể).int* constlà con trỏ const tới int (con trỏ không thể thay đổi, int có thể).const int* constlà con trỏ const tới const int.

constexpr (C++11) còn đi xa hơn: nó có nghĩa là giá trị phải có thể tính toán được tại thời gian biên dịch. Điều này cho phép trình biên dịch đánh giá các biểu thức trong quá trình biên dịch, loại bỏ hoàn toàn chi phí thời gian chạy. Sử dụngconstexprcho các hằng số thực sự được biết đến tại thời điểm biên dịch.

consteval (C++20) thậm chí còn chặt chẽ hơn: nó đánh dấu một hàm phải được đánh giá tại thời điểm biên dịch — gọi nó trong thời gian chạy là một lỗi biên dịch.

Bộ định tínhvolatilecho trình biên dịch biết rằng giá trị của biến có thể thay đổi ngoài tầm kiểm soát của chương trình (ví dụ: các thanh ghi phần cứng, I/O được ánh xạ bộ nhớ). Nó ngăn chặn tối ưu hóa nhất định. Không sử dụngvolatileđể phân luồng — thay vào đó hãy sử dụngstd::atomic.

Lỗi thường gặp

Một trong những lỗi nguy hiểm nhất trong C và C++ kiểu cũ xuất phát từ thu hẹp chuyển đổi — chuyển đổi ngầm làm mất thông tin. Ví dụ: việc gándoublechointsẽ âm thầm cắt bớt phần phân số. Việc chỉ địnhintlớn choshortbị tràn âm thầm.

Khởi tạo cú đúp{}ngăn chặn việc thu hẹp chuyển đổi tại thời điểm biên dịch. Đây là một trong những lý do mạnh mẽ nhất để thích nó hơn việc khởi tạo=. Nếu bạn viếtint x{3.14};, trình biên dịch sẽ phát ra lỗi. Vớiint x = 3.14;, nó âm thầm cắt ngắn thành 3.

Tương tự, việc trộn lẫn các loại có dấu và không dấu trong số học gây ra các chuyển đổi tiềm ẩn có thể tạo ra kết quả đáng ngạc nhiên.intâm so vớiunsigned intsẽ được chuyển đổi thành không dấu trước tiên, có khả năng trở thành số dương rất lớn.

  • Khởi tạo dấu ngoặc nhọn {} thu hẹp chuyển đổi tại thời điểm biên dịch
  • Khởi tạo nhiệm vụ (=) âm thầm cho phép thu hẹp - dữ liệu bị mất mà không có cảnh báo
  • Trộn các loại đã ký và không dấu gây ra chuyển đổi ngầm định thành không dấu
  • Một int âm được chuyển đổi thành unsigned sẽ trở thành một số dương rất lớn

Nhập bí danh

Bí danh loại cải thiện khả năng đọc và giúp thay đổi loại sau này dễ dàng hơn. Thíchusinghơntypedeftrong C++ hiện đại:

aliases.cppc++23
#include <cstdint>
#include <vector>
#include <string>

// Modern style (using) — preferred
using Byte = std::uint8_t;
using StudentId = std::int32_t;
using Names = std::vector<std::string>;

// Old style (typedef) — equivalent but harder to read with complex types
typedef std::uint8_t OldByte;
typedef std::vector<std::string> OldNames;

// using works with templates; typedef does not easily
template <typename T>
using Vec = std::vector<T>;

int main() {
    Byte b{255};
    StudentId sid{12345};
    Names names{"Alice", "Bob", "Charlie"};
    Vec<double> values{1.0, 2.0, 3.0};
}
Đầu ra

Nhấn Chạy để biên dịch và chạy ví dụ này.

Điểm chính
  • Mỗi biểu thức C++ đều có một loại được xác định tại thời điểm biên dịch - loại xác định kích thước, giá trị hợp lệ và các hoạt động hợp lệ
  • Sử dụng các loại có chiều rộng cố định từ (int32_t, uint64_t) khi kích thước chính xác quan trọng
  • Ưu tiên khởi tạo dấu ngoặc nhọn {} - nó ngăn chặn các chuyển đổi thu hẹp nguy hiểm
  • Sử dụng nullptr (không phải NULL hoặc 0) cho con trỏ null để tránh sự mơ hồ quá tải
  • tự động suy ra kiểu từ bộ khởi tạo; decltype truy vấn loại biểu thức mà không đánh giá nó
  • Sử dụng constexpr cho các hằng số thời gian biên dịch và 'sử dụng' cho các bí danh loại

Câu đố — Kiểm tra kiến thức

(15 XP)

1. Ưu điểm của việc khởi tạo cú đúp`int x{42};`so với khởi tạo nhiệm vụ`int x = 42;`là gì?

2. Tại sao bạn nên sử dụng`nullptr`thay vì`NULL`trong C++?

3. `auto x = 42;`suy ra loại`x`là gì?