رفتن به محتوا

انواع، مقادیر و سیستم نوع

انواع بنیادی ‎C++‎، اعداد صحیح با عرض ثابت، اشکال مقداردهی اولیه، استنتاج auto و سیستم const که صحت را در زمان کامپایل تضمین می‌کند را بررسی کنید.

سیستم نوع: اولین خط دفاعی شما

‎C++‎ یک زبان با نوع ایستا است — هر عبارت نوعی دارد که در زمان کامپایل مشخص است. سیستم نوع صرفاً دفترداری نیست؛ بلکه قدرتمندترین ابزار شما برای شناسایی باگ‌ها قبل از اجرای کد است. یک برنامه ‎C++‎ با نوع‌گذاری صحیح، ثابت‌های خود را در سیستم نوع حمل می‌کند و دسته‌های کاملی از خطاها را غیرممکن می‌سازد.

نوع یک متغیر سه چیز را تعیین می‌کند: چقدر حافظه اشغال می‌کند، چه مقادیری می‌تواند نمایش دهد، و چه عملیاتی روی آن معتبر است. درست کردن نوع‌ها پایه کد صحیح ‎C++‎ است.

انواع بنیادی

‎C++‎ مجموعه‌ای از انواع داخلی ارائه می‌دهد که مستقیماً به قابلیت‌های سخت‌افزار نگاشت می‌شوند. اندازه دقیق آن‌ها بسته به پلتفرم متفاوت است، اما استاندارد حداقل محدوده‌ها را تضمین می‌کند:

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';
}
خروجی

برای کامپایل و اجرای این نمونه، روی اجرا بزنید.

مقداردهی اولیه: روش صحیح

‎C++‎ نحوهای مقداردهی اولیه متعددی تکامل داده است. مقداردهی یکنواخت (آکولادی) {} در ‎C++‎ مدرن ترجیح داده می‌شود زیرا از تبدیل‌های باریک‌کننده جلوگیری می‌کند:

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';
}
خروجی

برای کامپایل و اجرای این نمونه، روی اجرا بزنید.

بهترین شیوه

در C، اشاره‌گر تهی توسط ماکروی NULL نمایش داده می‌شود که معمولاً به 0 یا (void*)0 گسترش می‌یابد. این در ‎C++‎ ابهام ایجاد می‌کند زیرا 0 همچنین یک int معتبر است. تابعی را در نظر بگیرید که هم برای int و هم برای یک نوع اشاره‌گر سربارگذاری شده — ارسال NULL (که 0 است) سربار int را فراخوانی می‌کند، نه سربار اشاره‌گر.

‎C++‎11 کلمه کلیدی nullptr را معرفی کرد، با نوع std::nullptr_t که به صورت ضمنی به هر نوع اشاره‌گر قابل تبدیل است اما به اعداد صحیح قابل تبدیل نیست. همیشه از nullptr به جای NULL یا 0 برای اشاره‌گرهای تهی استفاده کنید.

به همین ترتیب، وقتی منظورتان کاراکتر با مقدار صفر است، از '\0' (کاراکتر تهی) به جای 0 استفاده کنید تا نیت خود را به وضوح بیان کنید.

  • همیشه از nullptr به جای NULL یا 0 برای اشاره‌گرهای تهی استفاده کنید
  • nullptr نوع خاص خود (std::nullptr_t) را دارد و از ابهام سربارگذاری جلوگیری می‌کند
  • NULL یک ماکروی C است که به 0 گسترش می‌یابد — یک عدد صحیح است، نه یک اشاره‌گر
  • از '\0' برای کاراکتر تهی استفاده کنید تا آن را از عدد صحیح 0 متمایز کنید

const و تعیین‌کننده‌های نوع

تعیین‌کننده const به کامپایلر (و خوانندگان انسانی) می‌گوید که یک مقدار پس از مقداردهی اولیه نباید تغییر کند. این یکی از مهم‌ترین ابزارها در ‎C++‎ برای نوشتن کد ایمن و خودمستند است.

const از راست به چپ خوانده می‌شود: const int* اشاره‌گری به int ثابت است (int قابل تغییر نیست، اشاره‌گر قابل تغییر است). int* const اشاره‌گر ثابتی به int است (اشاره‌گر قابل تغییر نیست، int قابل تغییر است). const int* const اشاره‌گر ثابتی به int ثابت است.

constexpr (از ‎C++‎11) فراتر می‌رود: به این معناست که مقدار باید در زمان کامپایل قابل محاسبه باشد. این به کامپایلر امکان می‌دهد عبارات را در حین کامپایل ارزیابی کند و هزینه زمان اجرا را کاملاً حذف کند. از constexpr برای ثابت‌هایی که واقعاً در زمان کامپایل مشخص هستند استفاده کنید.

consteval (از ‎C++‎20) حتی سخت‌گیرانه‌تر است: تابعی را مشخص می‌کند که باید در زمان کامپایل ارزیابی شود — فراخوانی آن در زمان اجرا خطای کامپایل است.

تعیین‌کننده volatile به کامپایلر می‌گوید که مقدار یک متغیر ممکن است خارج از کنترل برنامه تغییر کند (مثلاً ثبات‌های سخت‌افزاری، ورودی/خروجی نگاشت‌شده به حافظه). این مانع بهینه‌سازی‌های خاصی می‌شود. از volatile برای چندنخی استفاده نکنید — به جای آن از std::atomic استفاده کنید.

دام

یکی از موذی‌ترین باگ‌ها در C و ‎C++‎ سبک قدیمی از تبدیل‌های باریک‌کننده (Narrowing Conversions) ناشی می‌شود — تبدیل‌های ضمنی که اطلاعات را از دست می‌دهند. مثلاً، انتساب یک double به یک int بی‌صدا بخش اعشاری را حذف می‌کند. انتساب یک int بزرگ به یک short بی‌صدا سرریز می‌کند.

مقداردهی آکولادی {} تبدیل‌های باریک‌کننده را در زمان کامپایل جلوگیری می‌کند. این یکی از قوی‌ترین دلایل ترجیح آن بر مقداردهی با = است. اگر بنویسید int x{3.14};، کامپایلر خطا صادر می‌کند. با int x = 3.14;، بی‌صدا به ۳ بریده می‌شود.

به همین ترتیب، ترکیب انواع علامت‌دار و بدون‌علامت در عملیات حسابی باعث تبدیل‌های ضمنی می‌شود که می‌توانند نتایج شگفت‌آوری تولید کنند. یک int منفی در مقایسه با یک unsigned int ابتدا به بدون‌علامت تبدیل می‌شود و ممکن است به عدد مثبت بسیار بزرگی تبدیل شود.

  • مقداردهی آکولادی {} تبدیل‌های باریک‌کننده را در زمان کامپایل شناسایی می‌کند
  • مقداردهی انتسابی (=) بی‌صدا اجازه باریک‌شدن می‌دهد — داده بدون هشدار از دست می‌رود
  • ترکیب انواع علامت‌دار و بدون‌علامت باعث تبدیل ضمنی به بدون‌علامت می‌شود
  • یک int منفی تبدیل‌شده به بدون‌علامت به عدد مثبت بسیار بزرگی تبدیل می‌شود

نام مستعار نوع (Type Alias)

نام‌های مستعار نوع خوانایی را بهبود می‌بخشند و تغییر انواع را در آینده آسان‌تر می‌کنند. در ‎C++‎ مدرن using را بر typedef ترجیح دهید:

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};
}
خروجی

برای کامپایل و اجرای این نمونه، روی اجرا بزنید.

نکات کلیدی
  • هر عبارت ‎C++‎ نوعی دارد که در زمان کامپایل مشخص است — نوع اندازه، مقادیر معتبر و عملیات معتبر را تعیین می‌کند
  • از انواع با عرض ثابت مانند int32_t و uint64_t استفاده کنید وقتی اندازه دقیق مهم است
  • مقداردهی آکولادی {} را ترجیح دهید — از تبدیل‌های باریک‌کننده خطرناک جلوگیری می‌کند
  • از nullptr (نه NULL یا 0) برای اشاره‌گرهای تهی استفاده کنید تا از ابهام سربارگذاری جلوگیری شود
  • auto نوع را از مقداردهی‌کننده استنتاج می‌کند؛ decltype نوع یک عبارت را بدون ارزیابی آن پرس‌وجو می‌کند
  • از constexpr برای ثابت‌های زمان کامپایل و از 'using' برای نام‌های مستعار نوع استفاده کنید

آزمون — دانش خود را بسنجید

(15 امتیاز)

1. مزیت مقداردهی آکولادی `int x{42};` نسبت به مقداردهی انتسابی `int x = 42;` چیست؟

2. چرا باید در ‎C++‎ از `nullptr` به جای `NULL` استفاده کنید؟

3. `auto x = 42;` نوع `x` را به چه استنتاج می‌کند؟