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:
#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++ has evolved multiple initialization syntaxes. Uniform (brace) initialization{}is preferred in modern C++ because it prevents narrowing conversions:
#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المترجم (والقراء البشريين) أنه لا يجب تعديل القيمة بعد التهيئة. إنها واحدة من أهم الأدوات في C++ لكتابة تعليمات برمجية آمنة ذاتية التوثيق.
تتم قراءةconstمن اليمين إلى اليسار: const int*هو مؤشر إلى const int (لا يمكن تغيير int، بل يمكن تغيير المؤشر).int* constهو مؤشر ثابت لـ int (لا يمكن تغيير المؤشر، يمكن تغييره).const int* constهو مؤشر const إلى const int.
يذهب constexpr (C++11) إلى أبعد من ذلك: فهو يعني أن القيمة يجب أن تكون قابلة للحساب في وقت الترجمة. يتيح ذلك للمترجم تقييم التعبيرات أثناء الترجمة، مما يقلل تكلفة وقت التشغيل بالكامل. استخدمconstexprللثوابت المعروفة حقًا في وقت الترجمة.
يعد consteval (C++20) أكثر صرامة: فهو يشير إلى دالة يجب تقييمها في وقت الترجمة — واستدعاؤها في وقت التشغيل يعد خطأ في الترجمة.
يخبر مؤهلvolatileالمترجم أن قيمة المتغير قد تتغير خارج نطاق تحكم البرنامج (على سبيل المثال، سجلات الأجهزة، الإدخال / الإخراج المعين للذاكرة). يمنع بعض التحسينات. لا تستخدمvolatileللترابط — استخدمstd::atomicبدلاً من ذلك.
واحدة من أكثر الأخطاء الخبيثة في لغة C والنمط القديم C++ تأتي من التحويلات الضيقة — التحويلات الضمنية التي تفقد المعلومات. على سبيل المثال، يؤدي تعيينdoubleإلىintإلى اقتطاع الجزء الكسري بصمت. تعيينintكبير إلىshortيفيض بصمت.
تمنع تهيئة الأقواس{}تضييق التحويلات في وقت الترجمة. يعد هذا أحد أقوى الأسباب لتفضيله على تهيئة=. إذا كتبتint x{3.14};، فسوف يصدر المترجم خطأ. معint x = 3.14;، يتم اقتطاعه بصمت إلى 3.
وبالمثل، فإن خلط الأنواع الموقعة وغير الموقعة في العمليات الحسابية يؤدي إلى تحويلات ضمنية يمكن أن تؤدي إلى نتائج مذهلة. سيتم تحويلintالسلبي مقارنةً بـunsigned intإلى غير موقعة أولاً، ومن المحتمل أن يصبح رقمًا موجبًا كبيرًا جدًا.
- تلتقط تهيئة الأقواس {} التحويلات الضيقة في وقت الترجمة
- تتيح تهيئة المهمة (=) بصمت التضييق — حيث يتم فقدان البيانات دون سابق إنذار
- يؤدي خلط الأنواع الموقعة وغير الموقعة إلى تحويل ضمني إلى غير موقعة
- يصبح int السالب الذي تم تحويله إلى غير موقع رقمًا موجبًا كبيرًا جدًا
اكتب الأسماء المستعارة
تعمل الأسماء المستعارة للكتابة على تحسين إمكانية القراءة وتسهيل تغيير الأنواع لاحقًا. تفضلusingعلىtypedefفي C++ الحديث:
#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) للمؤشرات الفارغة لتجنب الغموض الزائد
- يستنتج تلقائي النوع من المُهيئ؛ يستعلم decltype عن نوع التعبير دون تقييمه
- استخدم constexpr لثوابت وقت الترجمة و"استخدام" للأسماء المستعارة للنوع
اختبار — اختبر معرفتك
(15 نقطة خبرة)1. ما هي ميزة تهيئة القوس`int x{42};`على تهيئة المهمة`int x = 42;`؟
2. لماذا يجب عليك استخدام`nullptr`بدلاً من`NULL`في C++؟
3. ماذا يستنتج`auto x = 42;`نوع`x`؟