Перейти к содержимому

Типы, ценности и система типов

исследовать C++Основные типы, целые числа фиксированной ширины, формы инициализации, автодедукция и система const, которая обеспечивает правильность во время компиляции.

Оригинальное название: Your First Line of Defense

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 сообщает компилятору (и читателям), что значение не должно быть изменено после инициализации. Это один из самых важных инструментов в C++ для написания безопасного, самодокументирующегося кода.

const читается справа налево: const int* является указателем на const int (int не может быть изменен, указатель может). int* const - это указатель const на int (указатель не может быть изменен, int может). const int* const является указателем на const int.

constexpr (C++11) идет дальше: это означает, что значение должно быть вычислено во время компиляции. Это позволяет компилятору оценивать выражения во время компиляции, полностью исключая затраты на время выполнения. Используйте constexpr для констант, которые действительно известны во время компиляции.

consteval (C++20) является еще более строгим: он обозначает функцию, которая должна быть оценена во время компиляции - вызов его во время выполнения является ошибкой компиляции.

Классификатор volatile сообщает компилятору, что значение переменной может изменяться вне контроля программы (например, аппаратные регистры, I/O с картой памяти). Это предотвращает определенные оптимизации. Не используйте volatile для резьбы — вместо этого используйте std::atomic.

Подводный камень

Одна из самых коварных ошибок в C и C++ происходит из сужающихся конверсий — неявных конверсий, которые теряют информацию. Например, назначение double на int незаметно усекает дробную часть. Назначение большого int на short бесшумно переполняется.

Срочная инициализация {} предотвращает сужение конверсий во время компиляции. Это одна из самых сильных причин, чтобы предпочесть его инициализации =. Если вы пишете int x{3.14};, компилятор выдаст ошибку. С int x = 3.14; он бесшумно усечен до 3.

Точно так же смешивание подписанных и неподписанных типов в арифметике вызывает неявные преобразования, которые могут привести к неожиданным результатам. Отрицательный int по сравнению с unsigned int будет сначала преобразован в неподписанный, потенциально став очень большим положительным числом.

  • Брастинг инициализации {} улавливает сужение конверсий во время компиляции
  • Назначение инициализации (=) бесшумно позволяет сужать — данные теряются без предупреждения
  • Смешивание подписанных и неподписанных типов вызывает неявное преобразование в неподписанные.
  • Отрицательное значение, преобразованное в неподписанное, становится очень большим положительным числом.

Типовые алиазы

Типовые псевдонимы улучшают читаемость и облегчают изменение типов позже. Предпочтение using по сравнению с typedef в современном C++:

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) для нулевых указателей, чтобы избежать двусмысленности перегрузки.
  • автоматически выводит тип из инициализатора; деклап запрашивает тип выражения, не оценивая его
  • Использование constexpr для констант времени компиляции и «использование» для псевдонимов типа

Тест — проверьте свои знания

(15 XP)

1. В чем преимущество инициализации скобки `int x{42};` перед инициализацией назначения `int x = 42;`?

2. Зачем использовать `nullptr` вместо `NULL` в C++?

3. Что такое `auto x = 42;`, чтобы определить тип `x`?