C++ Programming

Types, Initialization, Constants and Operators

PGCP-AC

C++ is statically typed: every expression has a type known during translation. A type determines the values an object can represent, the operations that are valid, its storage requirements and how its bits are interpreted. Many defects that appear to be arithmetic mistakes actually begin with a poor type choice, an uninitialized object or an implicit conversion.

1. Objects, values and types

An object is a region of storage with a type and lifetime. A variable is a named object or reference. The declaration states the type:

int count = 12;
double price = 49.50;
char grade = 'A';
bool available = true;

The type controls representation and operations. An int participates in integer arithmetic, while a double represents floating-point values. A character literal such as 'A' is different from the string literal "A".

C++ types can be grouped as:

  • fundamental types, including arithmetic types, void and std::nullptr_t;
  • compound types, including pointers, references, arrays, functions, enumerations, classes and unions;
  • library types, such as std::string, std::vector and std::optional, which are class types.

2. Integer types

The standard integer types include signed and unsigned forms of char, short, int, long and long long. Their exact widths depend on the implementation, but the standard requires a minimum range and a nondecreasing size relationship.

sizeof(char) is always 1. A C++ byte therefore means the size of char, not necessarily eight bits. CHAR_BIT from <climits> states the number of bits in a byte. On common systems it is 8.

#include <climits>
#include <iostream>

std::cout << sizeof(int) << '\n';
std::cout << CHAR_BIT << '\n';

Use fixed-width types from <cstdint>, such as std::int32_t, only when the implementation provides the exact width and the data format needs it. For ordinary counts and arithmetic, choose a natural type whose range and signedness match the meaning.

3. Signed and unsigned arithmetic

A signed integer can represent negative and positive values. An unsigned integer represents values from zero through a maximum and uses modulo arithmetic. If an unsigned value one greater than its maximum is produced, it wraps modulo one more than the maximum.

Signed overflow is undefined behaviour:

int maximum = std::numeric_limits<int>::max();
// int invalid = maximum + 1; // undefined behaviour

Unsigned wrapping is defined, but that does not make accidental wraparound correct. Subtracting from zero can produce a very large value.

Mixing signed and unsigned operands can convert the signed value to unsigned:

int index = -1;
std::size_t size = 10;
bool result = index < size; // may be false after conversion

Keep related quantities in compatible types. Use std::size_t for container sizes and indexes when appropriate, but handle sentinel values without mixing -1 into unsigned arithmetic.

4. Floating-point types

float, double and long double represent finite approximations, infinities where supported and special values such as NaN. Many decimal fractions cannot be represented exactly in binary:

double value = 0.1 + 0.2;
// value is commonly close to, but not exactly, 0.3

Do not compare calculated floating-point results for exact equality unless exact representation is known. A tolerance policy must account for the scale and purpose of values; one fixed epsilon is not suitable for every calculation.

Floating-point division by zero commonly produces infinity or NaN under supported IEC 60559 behaviour, whereas integer division by zero is undefined behaviour.

5. Character and Boolean types

char stores a character-sized integer value. Whether plain char is signed or unsigned is implementation-defined. Use signed char or unsigned char when signedness is important for byte-like arithmetic.

wchar_t, char16_t and char32_t support other code-unit forms. C++17 also uses ordinary char storage for UTF-8 string literals. Text encoding is a separate concept from the size of one character type.

bool contains true or false. Numeric zero converts to false, while nonzero numeric values convert to true. In arithmetic, bool undergoes integral promotion, but using Boolean values as numbers usually obscures intent.

6. Type size, alignment and limits

sizeof(T) gives the storage size of type T in bytes. sizeof expression normally does not evaluate its operand:

int i = 4;
auto bytes = sizeof(i++); // i remains 4

An exception involves certain variable-length constructs that standard C++ does not ordinarily support. For normal objects, sizeof is a compile-time constant.

alignof(T) reports the alignment requirement. Padding may make a class larger than the sum of member sizes so each member and array element has correct alignment.

std::numeric_limits<T> supplies type properties:

auto low = std::numeric_limits<int>::lowest();
auto high = std::numeric_limits<int>::max();

Avoid hard-coded assumptions about int width or floating precision.

7. Initialization forms

C++ provides several initialization syntaxes:

int a = 10;     // copy initialization
int b(10);      // direct initialization
int c{10};      // direct-list initialization
int d = {10};   // copy-list initialization
int e{};        // value initialization, zero for int

Brace initialization rejects many narrowing conversions:

double rate = 3.8;
// int count{rate}; // error: narrowing
int count = rate;   // allowed conversion, fractional part lost

Braces also work for aggregate initialization and consistently express an intentional initial value. However, constructors taking std::initializer_list receive special preference, so braces and parentheses can select different constructors for class types.

8. Default and value initialization

A local fundamental object declared without an initializer has an indeterminate value:

int total; // indeterminate
// using total here can be undefined behaviour

Objects with static or thread storage duration are zero-initialized before other initialization. Class objects run a selected constructor. Arrays apply initialization to their elements.

Initialize every object before use. int value{}; reliably produces zero, while std::string text{}; produces an empty string. Initialization at declaration reduces the period in which an invalid state can exist.

9. Type deduction with auto

auto asks the compiler to deduce a static type from an initializer:

auto count = 10;       // int
auto price = 10.0;     // double
auto name = "C++";    // const char*

It does not create a dynamically typed variable. After deduction, count remains int.

Top-level const and references are normally adjusted during value deduction:

const int original = 7;
auto copy = original;        // int
const auto fixed = original; // const int
auto& reference = original;  // const int&

Use auto when the type is evident from the initializer, lengthy, iterator-like or required to preserve an expression's exact type. Avoid it when hiding the type makes domain meaning unclear.

10. const objects

const prevents modification through the declared name:

const double tax_rate = 0.18;
// tax_rate = 0.20; // error

A const object must be initialized. Constness can appear at different levels with pointers:

const int* pointer_to_const = &value;
int* const const_pointer = &value;
const int* const both_const = &value;

The first cannot modify the int through the pointer. The second cannot point elsewhere. The third has both restrictions.

Const member functions promise not to modify the observable non-mutable state of their object and can be called through const objects.

11. constexpr and compile-time values

constexpr declares a value or function usable in constant expressions when its inputs permit:

constexpr int square(int value) {
    return value * value;
}

constexpr int cells = square(8);

A constexpr function may also run at runtime when called with nonconstant data. In C++17, if constexpr discards a branch during template instantiation.

const means an object cannot be modified through that declaration. It does not always mean the value was computed at compile time. constexpr imposes stronger constant-expression requirements.

12. Literals and suffixes

Literal spelling influences type:

42       // int when representable
42L      // long
42LL     // long long
42U      // unsigned int
3.14     // double
3.14f    // float
'A'      // char
"A"     // array of const char

Integer literals can be decimal, octal, hexadecimal or binary. Digit separators improve readability: 1'000'000. Escape sequences represent special characters, while raw string literals preserve backslashes and line breaks conveniently.

Suffixes should reflect the desired type rather than silence conversion warnings.

13. Arithmetic operators

Arithmetic operators include +, -, *, / and %. With two integer operands, division produces an integer result:

int a = 7 / 2;    // 3
int b = -7 / 2;   // -3, truncates toward zero
int r = -7 % 2;   // -1

The quotient truncates toward zero and the remainder satisfies the language relationship between dividend, quotient and divisor. Division or remainder by zero is undefined.

If either operand is floating-point after conversions, floating division occurs:

double average = total / static_cast<double>(count);

14. Increment, decrement and assignment

Prefix increment changes the object and yields the incremented result. Postfix increment changes the object but yields the earlier value:

int i = 5;
int a = ++i; // i = 6, a = 6
int b = i++; // b = 6, i = 7

When the old value is unnecessary, prefix form is conventional, especially for iterators.

Compound assignment such as x += y performs one assignment to x and applies conversion in relation to x's type. Chained assignment groups right to left:

a = b = 0;

Avoid modifying one scalar object multiple times in a single expression when sequencing is unclear. Simple statements are easier to verify.

15. Relational and logical operators

Relational operators produce bool: ==, !=, <, <=, > and >=.

Logical AND and OR short-circuit:

if (pointer != nullptr && pointer->valid()) {
    // second operand is evaluated only when pointer is non-null
}

&& skips its right operand when the left is false. || skips the right operand when the left is true. This supports guarded access and efficient conditions.

Do not confuse assignment = with equality ==. A compiler warning can catch suspicious assignments in conditions.

16. Bitwise and shift operators

Bitwise operators work on integral representations:

  • &: AND;
  • |: OR;
  • ^: XOR;
  • ~: complement;
  • <<: left shift;
  • >>: right shift.
unsigned permissions = 0b0011;
permissions |= 0b0100;  // set a bit
permissions &= ~0b0010; // clear a bit
bool set = (permissions & 0b0100) != 0;

Use unsigned types for bit manipulation. Invalid shift counts and overflowing signed shifts can produce undefined behaviour. Stream insertion and extraction also use << and >>; overload resolution distinguishes stream operations from integer shifts.

17. Conditional and comma operators

The conditional operator selects one of two expressions:

int absolute = value < 0 ? -value : value;

It is an expression and has a result type determined from its operands. Use it for concise value selection, not deeply nested control flow.

The comma operator evaluates its left operand, discards that result, then evaluates and yields its right operand. Commas also separate function arguments and declarations; those commas are punctuation, not necessarily the comma operator.

18. Precedence, associativity and evaluation order

Precedence determines grouping. Multiplication groups more tightly than addition:

int result = 2 + 3 * 4; // 14

Associativity resolves grouping among operators at the same precedence level. Assignment associates right to left.

Precedence does not generally determine the order in which operand expressions are evaluated. Parentheses make grouping explicit but do not impose every desired sequencing rule. When side effects interact, split work into separate statements.

Readable parentheses are useful even when precedence rules would produce the same grouping.

19. Implicit conversions

Before many operations, smaller integer types undergo integral promotion. The usual arithmetic conversions then bring operands to a common type. These rules explain results that cannot be inferred only from the written types.

Conversions can lose information:

  • floating to integer discards the fractional part and can be undefined if out of range;
  • wider integer to narrower integer may change value;
  • signed and unsigned mixing can reinterpret a negative value as a large unsigned value;
  • integer to floating point may lose precision.

Compile with conversion warnings and express intentional narrowing explicitly after validating range.

20. Casts

C++ named casts communicate intent:

  • static_cast<T> performs checked compile-time conversions between related value forms;
  • dynamic_cast<T> performs runtime-checked polymorphic conversions;
  • const_cast<T> changes cv-qualification;
  • reinterpret_cast<T> performs low-level reinterpretation with strict limitations.
double ratio = static_cast<double>(part) / whole;

C-style casts can attempt several cast categories and hide dangerous conversions. Prefer named casts. A cast does not make an invalid operation safe; removing const from an originally const object and modifying it remains undefined.

21. Enumerations

An unscoped enum introduces enumerator names into its surrounding scope and permits more implicit conversions. A scoped enumeration keeps names qualified and does not implicitly convert to int:

enum class Direction : unsigned char {
    north, east, south, west
};

Direction direction = Direction::north;

The optional underlying type controls representation where an external format requires it. Enum values should represent a closed conceptual set. Use explicit conversion when an integer representation is truly required.

22. Type aliases

A type alias gives an existing type another name:

using StudentId = std::uint64_t;

StudentId remains the same type as std::uint64_t; the alias does not create strong type safety. If mixing identifiers must be prevented, define small wrapper classes with distinct types.

Aliases are especially useful for template types:

template<class T>
using List = std::vector<T>;

23. Practical considerations

  1. Initialize local fundamental objects before use.
  2. Do not assume every platform has 32-bit int or eight-bit bytes.
  3. Signed overflow is undefined; unsigned arithmetic wraps modulo its range.
  4. Brace initialization detects many narrowing conversions.
  5. auto deduces a fixed compile-time type.
  6. Integer division truncates toward zero.
  7. Short-circuit operators can guard unsafe access.
  8. Signed–unsigned comparison can convert a negative operand unexpectedly.
  9. Precedence controls grouping, not all evaluation order.
  10. A type alias improves naming but does not create a distinct type.

24. Worked expression trace

const int source = 7;
auto copy = source;
double result = copy / 2;

auto deduces int for copy because top-level const is removed during value deduction. Both operands of copy / 2 are int, so integer division produces 3. That int is then converted to double, making result 3.0 rather than 3.5. Converting before division changes the arithmetic:

double result = static_cast<double>(copy) / 2; // 3.5

Continue learning

Related notes

Put this topic into timed practice

Open mock tests when you want full-exam pacing, or keep drilling in practice mode.