Chapter 2: Basic Syntax and Fundamentals

Program Structure

Every C++ program has a standard structure:

// Preprocessor directives (before compilation)
#include <iostream>  // Standard library headers use <>
#include "myheader.h"  // Local headers use ""

// Namespace to avoid std:: prefix (optional, often avoided)
// using namespace std;

// Main function - program entry point
int main() {
    // Your code here
    return 0;  // 0 = success, non-zero = error
}

The main() Function

// Basic form
int main() {
    return 0;
}

// With command-line arguments
int main(int argc, char* argv[]) {
    // argc = argument count
    // argv = argument values (array of C-strings)
    for (int i = 0; i < argc; i++) {
        std::cout << "Arg " << i << ": " << argv[i] << std::endl;
    }
    return 0;
}

Data Types

Fundamental Types

#include <iostream>
#include <cstdint>  // For fixed-width integers

int main() {
    // Boolean
    bool flag = true;  // or false

    // Characters
    char c = 'A';           // 1 byte, ASCII
    wchar_t wc = L'Ω';      // Wide character
    char16_t c16 = u'好';   // UTF-16
    char32_t c32 = U'🎉';   // UTF-32

    // Integers (size varies by platform)
    short s = 32767;              // At least 16 bits
    int i = 2147483647;           // At least 16 bits (usually 32)
    long l = 2147483647L;         // At least 32 bits
    long long ll = 9223372036854775807LL;  // At least 64 bits

    // Fixed-width integers (guaranteed sizes)
    int8_t  i8  = 127;            // Exactly 8 bits
    int16_t i16 = 32767;          // Exactly 16 bits
    int32_t i32 = 2147483647;     // Exactly 32 bits
    int64_t i64 = 9223372036854775807LL;  // Exactly 64 bits

    // Unsigned variants
    unsigned int ui = 4294967295U;
    uint32_t u32 = 4294967295U;

    // Floating point
    float f = 3.14159f;           // ~7 decimal digits precision
    double d = 3.141592653589793; // ~15 decimal digits precision
    long double ld = 3.14159265358979323846L;

    // Size operator
    std::cout << "Size of int: " << sizeof(int) << " bytes\n";
    std::cout << "Size of double: " << sizeof(double) << " bytes\n";

    return 0;
}

Type Inference with auto

auto x = 42;        // int
auto y = 3.14;      // double
auto z = 3.14f;     // float
auto s = "hello";   // const char*
auto str = std::string("hello");  // std::string

Constants

// Compile-time constant (preferred)
constexpr int MAX_SIZE = 100;

// Runtime constant
const int size = getSize();  // Value determined at runtime

// Old C-style (avoid in new code)
#define OLD_MAX 100

Variables and Initialization

C++ has several initialization syntaxes:

int a;          // Uninitialized (dangerous! Contains garbage)
int b = 5;      // Copy initialization
int c(5);       // Direct initialization
int d{5};       // Brace initialization (C++11, preferred)
int e = {5};    // Copy-list initialization

// Brace initialization prevents narrowing conversions
int x = 3.14;   // Compiles (truncates to 3) - silent bug!
int y{3.14};    // ERROR: narrowing conversion - catches bugs

// For complex types
std::vector<int> v1 = {1, 2, 3};  // Initializer list
std::vector<int> v2{1, 2, 3};     // Same thing

Best Practice: Use brace initialization {} to catch type errors at compile time.

Operators

Arithmetic

int a = 10, b = 3;

int sum = a + b;      // 13
int diff = a - b;     // 7
int prod = a * b;     // 30
int quot = a / b;     // 3 (integer division truncates)
int rem = a % b;      // 1 (modulo)

double x = 10.0, y = 3.0;
double div = x / y;   // 3.333... (floating-point division)

// Compound assignment
a += 5;   // a = a + 5
a -= 2;   // a = a - 2
a *= 3;   // a = a * 3
a /= 2;   // a = a / 2

// Increment/Decrement
int i = 5;
i++;      // Post-increment: use i, then add 1
++i;      // Pre-increment: add 1, then use i
i--;      // Post-decrement
--i;      // Pre-decrement

Comparison and Logical

bool result;

// Comparison
result = (5 == 5);   // true (equality)
result = (5 != 3);   // true (inequality)
result = (5 < 10);   // true
result = (5 <= 5);   // true
result = (5 > 3);    // true
result = (5 >= 5);   // true

// Logical
result = true && false;  // false (AND)
result = true || false;  // true (OR)
result = !true;          // false (NOT)

// Short-circuit evaluation
// Second operand not evaluated if result determined by first
if (ptr != nullptr && ptr->isValid()) {
    // Safe: ptr->isValid() only called if ptr is not null
}

Bitwise

int a = 0b1010;  // 10 in binary
int b = 0b1100;  // 12 in binary

int and_result = a & b;   // 0b1000 = 8 (AND)
int or_result = a | b;    // 0b1110 = 14 (OR)
int xor_result = a ^ b;   // 0b0110 = 6 (XOR)
int not_result = ~a;      // Inverts all bits

int left = a << 2;   // 0b101000 = 40 (left shift)
int right = a >> 1;  // 0b0101 = 5 (right shift)

Control Flow

Conditionals

// if-else
if (condition) {
    // code
} else if (another_condition) {
    // code
} else {
    // code
}

// With initialization (C++17)
if (auto result = compute(); result > 0) {
    // result is in scope here
}
// result is out of scope here

// Ternary operator
int max = (a > b) ? a : b;

// switch
switch (value) {
    case 1:
        doSomething();
        break;
    case 2:
    case 3:
        doOtherThing();  // Handles both 2 and 3
        break;
    default:
        doDefault();
}

// switch with initialization (C++17)
switch (auto x = getValue(); x) {
    case 0: break;
    case 1: break;
}

Loops

// for loop
for (int i = 0; i < 10; i++) {
    std::cout << i << " ";
}

// Range-based for (C++11)
std::vector<int> nums = {1, 2, 3, 4, 5};
for (int n : nums) {
    std::cout << n << " ";  // Copy of each element
}
for (const int& n : nums) {
    std::cout << n << " ";  // Reference (no copy, read-only)
}
for (int& n : nums) {
    n *= 2;  // Modifiable reference
}

// while loop
int count = 0;
while (count < 10) {
    std::cout << count++ << " ";
}

// do-while loop (executes at least once)
do {
    std::cout << "Runs at least once\n";
} while (false);

// Loop control
for (int i = 0; i < 100; i++) {
    if (i == 5) continue;  // Skip rest of iteration
    if (i == 10) break;    // Exit loop entirely
}

Functions

Basic Functions

// Function declaration (prototype)
int add(int a, int b);

// Function definition
int add(int a, int b) {
    return a + b;
}

// Void function (no return value)
void printMessage(const std::string& msg) {
    std::cout << msg << std::endl;
}

// Default parameters
void greet(const std::string& name = "World") {
    std::cout << "Hello, " << name << "!\n";
}
// greet();         // "Hello, World!"
// greet("Alice");  // "Hello, Alice!"

Function Overloading

// Same name, different parameters
int add(int a, int b) {
    return a + b;
}

double add(double a, double b) {
    return a + b;
}

std::string add(const std::string& a, const std::string& b) {
    return a + b;
}

// Compiler chooses based on argument types
add(1, 2);          // Calls int version
add(1.5, 2.5);      // Calls double version
add("Hello", "!");  // Calls string version

Pass by Value vs Reference

// Pass by value (copy)
void modifyValue(int x) {
    x = 100;  // Only modifies local copy
}

// Pass by reference (modify original)
void modifyReference(int& x) {
    x = 100;  // Modifies original
}

// Pass by const reference (read-only, no copy)
void readOnly(const std::string& str) {
    std::cout << str;  // Efficient, no copy
    // str = "new";    // ERROR: can't modify const
}

int main() {
    int a = 5;
    modifyValue(a);      // a is still 5
    modifyReference(a);  // a is now 100

    // For large objects, always use const reference for read-only
    std::string bigString = "...";
    readOnly(bigString);  // No expensive copy
}

Inline Functions

// Suggests compiler to inline (replace call with body)
inline int square(int x) {
    return x * x;
}

Lambda Functions (C++11)

// Basic lambda
auto add = [](int a, int b) { return a + b; };
int result = add(3, 4);  // 7

// Lambda with capture
int multiplier = 10;
auto times = [multiplier](int x) { return x * multiplier; };
// [=] captures all by value
// [&] captures all by reference
// [x, &y] captures x by value, y by reference

// Lambda with explicit return type
auto divide = [](int a, int b) -> double {
    return static_cast<double>(a) / b;
};

// Generic lambda (C++14)
auto generic_add = [](auto a, auto b) { return a + b; };
generic_add(1, 2);      // Works with int
generic_add(1.5, 2.5);  // Works with double

Input/Output

Console I/O

#include <iostream>
#include <string>

int main() {
    // Output
    std::cout << "Hello, World!" << std::endl;  // endl flushes buffer
    std::cout << "Hello, World!\n";  // \n is often preferred (faster)

    // Chaining output
    std::cout << "Value: " << 42 << ", Pi: " << 3.14 << "\n";

    // Input
    int number;
    std::cout << "Enter a number: ";
    std::cin >> number;

    // String input (single word)
    std::string word;
    std::cin >> word;  // Reads until whitespace

    // Full line input
    std::string line;
    std::getline(std::cin, line);  // Reads entire line

    // Error handling
    if (std::cin.fail()) {
        std::cerr << "Invalid input!\n";  // stderr for errors
        std::cin.clear();  // Clear error flags
        std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
    }

    return 0;
}

Formatting Output

#include <iostream>
#include <iomanip>

int main() {
    double pi = 3.14159265358979;

    std::cout << std::fixed << std::setprecision(2) << pi << "\n";  // 3.14
    std::cout << std::scientific << pi << "\n";  // 3.14e+00

    std::cout << std::setw(10) << 42 << "\n";  // Right-aligned in 10 chars
    std::cout << std::left << std::setw(10) << 42 << "\n";  // Left-aligned

    std::cout << std::hex << 255 << "\n";  // ff
    std::cout << std::oct << 255 << "\n";  // 377
    std::cout << std::dec << 255 << "\n";  // 255 (back to decimal)

    std::cout << std::boolalpha << true << "\n";  // "true" instead of 1
}

Strings

C-Style Strings (avoid when possible)

// Null-terminated character arrays
char str1[] = "Hello";           // Array of 6 chars (includes \0)
const char* str2 = "World";      // Pointer to string literal

// Dangerous operations
char buffer[10];
strcpy(buffer, "Hello");         // Can overflow!
strcat(buffer, " World");        // Can overflow!
strlen(str1);                    // 5 (doesn't count \0)

C++ Strings (std::string)

#include <string>

int main() {
    // Creation
    std::string s1 = "Hello";
    std::string s2("World");
    std::string s3(5, 'a');  // "aaaaa"

    // Operations
    std::string combined = s1 + " " + s2;  // Concatenation
    s1 += "!";  // Append

    // Access
    char first = s1[0];       // No bounds checking
    char safe = s1.at(0);     // Throws if out of bounds
    char last = s1.back();
    char front = s1.front();

    // Size
    size_t len = s1.length();  // or s1.size()
    bool empty = s1.empty();

    // Comparison
    if (s1 == s2) { /* ... */ }
    if (s1 < s2) { /* lexicographic comparison */ }

    // Searching
    size_t pos = s1.find("llo");      // Position of substring
    if (pos != std::string::npos) {
        // Found
    }

    // Substrings
    std::string sub = s1.substr(0, 3);  // "Hel" (pos, length)

    // Modification
    s1.replace(0, 2, "Jell");  // "Jello!" (pos, length, new_str)
    s1.erase(4, 1);            // Remove 1 char at position 4
    s1.insert(4, "y");         // Insert at position 4

    // Conversion
    std::string numStr = std::to_string(42);
    int num = std::stoi("42");
    double d = std::stod("3.14");

    return 0;
}

String Views (C++17)

#include <string_view>

// Non-owning reference to string data (no allocation)
void printView(std::string_view sv) {
    std::cout << sv << "\n";
}

int main() {
    std::string str = "Hello, World!";
    std::string_view view = str;         // No copy
    std::string_view literal = "Test";   // Works with literals

    printView(str);      // Works
    printView("Hello");  // Works, no string construction
    printView(view);     // Works

    // Can create substring views without allocation
    std::string_view hello = view.substr(0, 5);
}

Arrays

C-Style Arrays

// Fixed size, stack allocated
int arr[5] = {1, 2, 3, 4, 5};
int arr2[] = {1, 2, 3};  // Size inferred (3)
int arr3[5] = {1, 2};    // Rest initialized to 0

// Access
int first = arr[0];
arr[1] = 10;

// Size (only works in same scope)
size_t size = sizeof(arr) / sizeof(arr[0]);  // 5

// Decay to pointer when passed to function (loses size info)
void processArray(int* arr, size_t size);  // Must pass size separately

std::array (C++11)

#include <array>

std::array<int, 5> arr = {1, 2, 3, 4, 5};

// Size is part of the type
size_t size = arr.size();  // 5
bool empty = arr.empty();

// Safe access
int val = arr.at(2);  // Bounds-checked

// Works with algorithms
std::sort(arr.begin(), arr.end());

// Can be passed by reference without decay
void process(const std::array<int, 5>& arr);

std::vector (Dynamic Array)

#include <vector>

// Creation
std::vector<int> v1;                    // Empty
std::vector<int> v2(5);                 // 5 elements, default value (0)
std::vector<int> v3(5, 10);             // 5 elements, all 10
std::vector<int> v4 = {1, 2, 3, 4, 5};  // Initializer list

// Size operations
v1.push_back(1);    // Add to end
v1.pop_back();      // Remove from end
v1.resize(10);      // Change size
v1.reserve(100);    // Pre-allocate capacity (optimization)
size_t size = v1.size();
size_t cap = v1.capacity();

// Access
int first = v1[0];      // No bounds check
int safe = v1.at(0);    // Bounds check
int& front = v1.front();
int& back = v1.back();

// Iteration
for (const auto& elem : v1) {
    std::cout << elem << " ";
}

// Insert and erase
v1.insert(v1.begin() + 2, 99);  // Insert 99 at position 2
v1.erase(v1.begin());           // Remove first element
v1.clear();                     // Remove all elements

Exercises

  1. Type Exploration

    • Write a program that prints the size of all fundamental types on your system
    • Test what happens with integer overflow
  2. Calculator

    • Create a simple calculator that reads two numbers and an operator (+, -, *, /)
    • Handle division by zero gracefully
  3. String Manipulation

    • Write a function that reverses a string
    • Write a function that counts vowels in a string
  4. Vector Practice

    • Read numbers from input until user enters -1
    • Store in vector, then print: sum, average, min, max

Previous: 01 - Tooling and Setup Next: 03 - Memory Management