Data Types in C++
Explore fundamental types, std::string, std::array, and initializer lists for clean, safe C++ code.
Fundamental Types
C++ inherits its fundamental types from C, with some additions. These are the building blocks every C++ program is made of, and understanding their sizes and behavior matters because C++ does not hide the hardware from you — integer overflow, floating-point precision limits, and signed/unsigned mismatches are all real concerns you need to reason about.
#include <iostream>
#include <climits> // INT_MAX, LLONG_MAX, etc.
#include <cfloat> // DBL_MAX, FLT_EPSILON, etc.
// Integer types
int a = 42; // typically 32-bit, at least 16-bit
short b = 100; // at least 16-bit
long c = 100000L; // at least 32-bit
long long d = 9000000000LL; // at least 64-bit
unsigned int e = 300u; // non-negative only — doubles the positive range
// Floating-point types
float f = 3.14f; // 32-bit, ~7 decimal digits of precision
double g = 3.14159265; // 64-bit, ~15 decimal digits — default for FP
long double h = 3.14L; // 80-bit or 128-bit, platform-dependent
// Character types
char ch = 'A'; // 8-bit, may be signed or unsigned
wchar_t wch = L'€'; // wide character
char8_t u8ch = u8'x'; // UTF-8 code unit (C++20)
char16_t u16ch = u'€'; // UTF-16 code unit
char32_t u32ch = U'€'; // UTF-32 code unit
// Boolean
bool flag = true; // true or false; sizeof(bool) is typically 1
// void — not a variable type, used for functions that return nothing
void do_work() { /* ... */ }
// Check sizes at runtime — these may vary by platform and compiler
std::cout << "int: " << sizeof(int) << " bytes\n"; // 4
std::cout << "long long: " << sizeof(long long) << " bytes\n"; // 8
std::cout << "double: " << sizeof(double) << " bytes\n"; // 8
Integer Literals
C++14 added two readability features for numeric literals that you should use freely: digit separators and binary literals. Both are purely for human readers — the compiler ignores the separators.
int decimal = 255;
int hex = 0xFF; // hexadecimal
int octal = 0377; // octal
int binary = 0b11111111; // binary (C++14)
// Digit separators (C++14) — improve readability of large numbers
long long billion = 1'000'000'000LL;
double pi_approx = 3.141'592'653'589;
int bitmask = 0b1111'0000'1111'0000;
Fixed-Width Integer Types
The sizes of int, long, etc. vary by platform — long is 32 bits on Windows but 64 bits on Linux. When you need exact sizes, use the types from <cstdint>. This is critical for network protocols, binary file formats, and hardware register interfaces where a wrong size means corrupt data.
#include <cstdint>
int8_t a = 127; // exactly 8-bit signed
uint8_t b = 255; // exactly 8-bit unsigned
int16_t c = 32767;
uint16_t d = 65535;
int32_t e = 2'147'483'647;
uint32_t f = 4'294'967'295u;
int64_t g = 9'223'372'036'854'775'807LL;
uint64_t h = 18'446'744'073'709'551'615ULL;
// Fast and least variants — when you care about performance more than exact size
int_fast32_t fast; // fastest type >= 32 bits on this platform
int_least16_t least; // smallest type >= 16 bits
// Pointer-sized integer (useful for offsets, array sizes)
intptr_t ptr_val; // signed, same size as a pointer
uintptr_t uptr_val; // unsigned
size_t sz = 0; // unsigned, result of sizeof — use for array indices/sizes
ptrdiff_t diff; // signed difference between two pointers
Use int32_t when writing network protocols, binary file formats, or interfacing with hardware registers where byte layout must be exact.
std::string vs C-Strings
C-strings are char arrays terminated by a null byte ('\0'). They are error-prone: no bounds checking, manual memory management, easy to overflow. std::string wraps a dynamically allocated buffer, manages its own memory, and provides a rich API. The performance difference is negligible for most code, and the safety difference is enormous.
#include <string>
#include <iostream>
// C-string (avoid in modern C++)
const char* cstr = "Hello"; // string literal — immutable
char buf[32] = "Hello"; // stack-allocated, fixed-size
// strcat(buf, " World, this is a very long string!"); // buffer overflow!
// std::string — safe and expressive
std::string s = "Hello";
s += ", World!"; // append — grows automatically
s.append(" How are you?");
std::cout << s.length() << "\n"; // 27
std::cout << s.substr(7, 5) << "\n"; // World
// Finding and replacing
size_t pos = s.find("World");
if (pos != std::string::npos) {
s.replace(pos, 5, "C++");
}
std::cout << s << "\n"; // Hello, C++! How are you?
// String comparison — lexicographic, works with < > == operators
std::string a = "apple";
std::string b = "banana";
if (a < b) std::cout << a << " comes first\n";
// Converting to/from numbers (C++11)
int n = std::stoi("42");
double d = std::stod("3.14");
std::string ns = std::to_string(1234);
// String views (C++17): non-owning reference — accepts std::string or literal, zero allocation
#include <string_view>
void print_name(std::string_view name) {
// name can be a std::string, a string literal, or a substring
// no copy or allocation is made
std::cout << name << "\n";
}
print_name("Alice"); // no allocation
print_name(s.substr(0, 5)); // no allocation
Prefer std::string_view for function parameters that only read a string — it accepts both std::string and string literals with zero overhead.
std::array vs C-Arrays
C-style arrays have a fundamental flaw: they decay to a pointer when passed to functions, silently losing their size information. This is the source of countless buffer overrun bugs in C code. std::array is a zero-overhead wrapper that preserves the size as part of the type and integrates cleanly with every standard library algorithm.
#include <array>
#include <algorithm>
#include <iostream>
// C-style array — size is lost when passed to functions
int c_arr[5] = {5, 3, 1, 4, 2};
// sizeof(c_arr) == 20 here, but inside a function it decays to int*
// std::array — size is part of the type, never lost
std::array<int, 5> arr = {5, 3, 1, 4, 2};
std::cout << arr.size() << "\n"; // 5 — always available
std::cout << arr.front() << "\n"; // 5
std::cout << arr.back() << "\n"; // 2
// Range checking — .at() throws std::out_of_range, operator[] does not
arr.at(10); // throws std::out_of_range
arr[10]; // undefined behavior — no check
// Works with all standard algorithms
std::sort(arr.begin(), arr.end());
std::ranges::sort(arr); // C++20 — cleaner
for (int x : arr) std::cout << x << " "; // 1 2 3 4 5
// std::array is copyable and equality-comparable — C arrays are neither
std::array<int, 5> arr2 = arr;
bool equal = (arr == arr2); // true
std::vector
std::vector is a dynamically-sized array — the single most-used container in C++. It stores elements contiguously in memory for cache-friendly access, grows automatically when you add elements, and is compatible with every standard algorithm. When in doubt about which container to use, start with vector.
#include <vector>
std::vector<int> v; // empty — no heap allocation yet
v.push_back(1);
v.push_back(2);
v.emplace_back(3); // constructs in-place — prefer over push_back
std::vector<int> v2 = {10, 20, 30, 40, 50};
std::vector<int> v3(10, 0); // 10 elements, all zero
v2.resize(3); // now {10, 20, 30}
v2.reserve(100); // preallocate capacity — avoids reallocations when size is known
std::cout << v2.size() << "\n"; // 3
std::cout << v2.capacity() << "\n"; // >= 100
Initializer Lists
C++11 introduced uniform initialization syntax: brace-enclosed lists that can initialize almost anything. This is valuable because it means you can initialize all container types the same way, making code consistent and reducing the chance of accidentally calling the wrong constructor.
#include <vector>
#include <map>
#include <set>
// All containers accept initializer lists
std::vector<int> v = {1, 2, 3, 4, 5};
std::map<std::string, int> m = {{"a", 1}, {"b", 2}, {"c", 3}};
std::set<double> s = {3.14, 2.71, 1.41};
// Structs and classes — aggregate initialization
struct Point { int x, y; };
Point p{10, 20};
// Nested initialization — readable and concise
std::vector<Point> points = {{0, 0}, {1, 2}, {3, 4}};
enum class
Plain C-style enum has two problems: it pollutes the enclosing namespace with its enumerator names, and its values implicitly convert to int, silently enabling nonsensical arithmetic. enum class fixes both. It requires you to name the enum when using its values, and it does not convert to int without an explicit cast.
#include <iostream>
// Old plain enum — pollutes enclosing namespace, implicit int conversion
enum Direction { North, South, East, West }; // North is in global scope
// int x = North; // silently converts to int — easy source of bugs
// Modern enum class — scoped and type-safe
enum class Color { Red, Green, Blue };
enum class Status { Ok, NotFound, ServerError };
Color c = Color::Red; // must qualify with Color::
Status s = Status::NotFound;
// No implicit conversion to int — the compiler prevents nonsensical operations
// int n = Color::Red; // ERROR — good!
int n = static_cast<int>(Color::Red); // explicit cast is fine
// Custom underlying type (default is int)
enum class Flags : uint8_t { None = 0, Read = 1, Write = 2, Exec = 4 };
// Switch on enum class — compiler warns if you miss a case
switch (c) {
case Color::Red: std::cout << "Red\n"; break;
case Color::Green: std::cout << "Green\n"; break;
case Color::Blue: std::cout << "Blue\n"; break;
}
enum class is the right choice in virtually all new C++ code. It avoids the accidental integer promotion and namespace pollution that makes plain enum a maintenance headache.