// STUHUB · C++ DATA STRUCTURES

Arrays in Memory, Parameters, and Structs vs Classes: the C++ Layer Under Every Data Structure

Where an array actually sits in memory and how to calculate the address of arr[i] and A[i][j] in row-major and column-major order; why arrayA = arrayB, arrayA == arrayB and cout << list compile but do the wrong thing; why an array argument is always by reference and must carry its size separately; what really separates a struct from a class; and the cin/getline facts that make a correct program print an empty name. Every listing compiles clean and prints real output.

Introduction

Linked lists, stacks, queues, trees and graphs all sit on top of four C++ mechanisms: an array, a function parameter, a pointer, and a struct or class. Pointers have a page of their own. This page is the other three, plus the arithmetic that connects them to physical memory.

It is worth an evening on its own because this is the layer that gets asked as calculation rather than as description. "Given a base address of 1000 and 4-byte integers, what is the address of A[2][3]?" has one number for an answer, and you either know which quantity gets multiplied by the number of columns or you do not. The same is true of the three whole-array operations that compile and silently do the wrong thing, and of the one-word difference between struct and class that a multiple-choice question can be built on.

The last section is different in character but belongs here: the input-handling behaviour that turns a correct program into one that prints an empty name. It costs marks in lab work far more often than any algorithm does.

Every program below was compiled with g++ -std=c++17 -Wall -Wextra and run; the outputs quoted are real. The one exception is flagged where it appears, and the warnings it produces are the point of it.

An array is a fixed number of same-typed boxes, side by side

The definition worth being able to write out: an array is a collection of a fixed number of components, all of the same data type, stored in contiguous (adjacent) memory locations. Three phrases in that sentence each do real work.

  • Fixed number. The length is decided when the array is created and never changes. For a static array it must be a compile-time constant, which is why int list[n]; with a variable n is not standard C++. If the size is only known at run time you need a dynamic array: int* dyn = new int[n];, paired with delete[] dyn;.
  • All of the same data type. Arrays are homogeneous. The moment you need a record with an int, a string and a double in it, you need a struct — that is what heterogeneous means and it is the standard exam contrast.
  • Contiguous memory. Elements are laid out back to back with no gaps. This is the whole reason indexing is O(1): the machine does not search for arr[i], it computes where it is. That is called random access, as opposed to the sequential access a linked list forces on you, where reaching the fifth node means walking through four others.

Indexing always starts at 0, so a ten-element array has valid indices 0 through 9 and arr[10] is out of bounds — C++ will not stop you, it will just read or write whatever happens to live there.

One initialisation rule shows up constantly and is easy to get backwards: if you supply fewer initialisers than the array has slots, the remaining elements are set to zero, not left as garbage. int list2[10] = {8, 5, 12}; gives you 8 5 12 0 0 0 0 0 0 0. Supply no initialiser list at all and the contents genuinely are garbage. Supply = {0} and everything is zeroed.

cpp
#include <iostream>

int main() {
    const int SIZE = 10;

    int num[5];                       // 5 ints, contents are garbage
    int list[SIZE] = {0};             // all 10 set to 0
    int list2[SIZE] = {8, 5, 12};     // [0]=8 [1]=5 [2]=12, the other 7 are 0
    double sales[] = {12.25, 32.50, 16.90, 23.0, 45.68};   // size inferred = 5

    list[3] = 10;                     // random access write
    list[6] = 35;
    list[5] = list[3] + list[6];      // 45

    std::cout << "list2 = ";
    for (int i = 0; i < SIZE; ++i) std::cout << list2[i] << ' ';
    std::cout << "\nlist[5] = " << list[5] << '\n';
    std::cout << "elements in sales = " << sizeof(sales) / sizeof(sales[0]) << '\n';

    int n = 4;
    int* dyn = new int[n];            // size decided at run time: heap array
    for (int i = 0; i < n; ++i) dyn[i] = i * i;
    std::cout << "dyn[3] = " << dyn[3] << '\n';
    delete[] dyn;                     // every new[] needs a delete[]

    (void)num;                        // declared to show the syntax; never read
}

/* Output:

list2 = 8 5 12 0 0 0 0 0 0 0
list[5] = 45
elements in sales = 5
dyn[3] = 9
*/

The address of arr[i] is a calculation, not a mystery

Because the elements are adjacent and all the same size, the position of any element is pure arithmetic:

Address of arr[i] = base address + i × sizeof(element)

The base address is where the array starts, which is the same thing as &arr[0] — that is why the formula gives the base back when i is 0. The name arr on its own also evaluates to that address, a fact that matters a great deal two sections from now.

Worked example 1. int arr[10]; with base address 2000 and sizeof(int) == 4.

ExpressionWorkingAddress
&arr[0]2000 + 0 × 42000
&arr[5]2000 + 5 × 42020
&arr[9]2000 + 9 × 42036

The whole array occupies 10 × 4 = 40 bytes, spanning 2000 to 2039 inclusive. Notice the last element begins at 2036 and the last byte is 2039; mixing those two up is the usual way to lose the mark.

Worked example 2. char name[20]; with base 1500 and sizeof(char) == 1. Then &name[12] = 1500 + 12 × 1 = 1512, and the array spans 1500 to 1519. With one-byte elements the index and the offset happen to coincide, which is exactly why a question that uses char is testing something easier than one that uses double.

Two habits that make these questions safe. First, write the formula down before substituting anything — most errors are arithmetic done in your head. Second, sanity-check with the total: the last valid element must start at base + (n - 1) × size, and base + n × size is one past the end of the array.

Two dimensions: row-major versus column-major

A 2-D array is still one flat contiguous block. The only question is what order the elements were flattened in, and there are two conventions:

OrderFormula for A[i][j]Used by
Row-majorbase + (i × COLS + j) × sizeof(element)C, C++, Java — a whole row at a time
Column-majorbase + (j × ROWS + i) × sizeof(element)Fortran, MATLAB, most linear-algebra libraries

The way to remember which multiplier goes where, without memorising two near-identical formulas: you multiply by however many elements you must skip to cross one whole row, or one whole column. In row-major layout the rows are stored one after another, so stepping from row i to row i + 1 skips COLS elements — hence i × COLS. In column-major layout the columns are stored one after another, so stepping one column along skips ROWS elements — hence j × ROWS. Once you can say which direction is stored consecutively, the formula reconstructs itself.

Worked example. int A[4][5]; — 4 rows, 5 columns — with base address 1000 and 4-byte integers. Find &A[2][3].

  • Row-major: 1000 + (2 × 5 + 3) × 4 = 1000 + 13 × 4 = 1052
  • Column-major: 1000 + (3 × 4 + 2) × 4 = 1000 + 14 × 4 = 1056

The whole array is 4 × 5 × 4 = 80 bytes, spanning 1000 to 1079 under either convention — only the order of the elements inside changes, never the total. C++ gives you the row-major answer; a question that asks for both is checking that you can see the difference rather than that you memorised one line.

The practical consequence, for later: since C++ stores rows consecutively, a nested loop that runs col on the inside and row on the outside walks memory in order and hits the cache on nearly every read. Swap the loops and you jump a whole row's width on every step. Same result, several times slower on a large matrix.

Proving it: a program that prints the real offsets

Formulas are easy to accept and easy to get backwards under pressure, so here is the machine confirming them. The program takes the address of each element, subtracts the base, and prints the result next to what the formula predicts. The two columns are identical, on both the 1-D array and the 2-D one.

The absolute address is printed too, but be clear about what it is worth: it changes on every run because the operating system places the stack differently each time. The offsets never change, and those are what the formula is actually about. In an exam you are given an invented base address for exactly this reason.

One line in the output does the whole job of the previous section: sizeof(m[0]) is 20 bytes for int m[4][5], which is COLS × sizeof(int). m[0] is a whole row, and a row is the unit you skip when you increase i by one. That is the row-major formula, measured rather than asserted.

cpp
#include <cstddef>
#include <iomanip>
#include <iostream>

int main() {
    int arr[10] = {0};
    const char* base = reinterpret_cast<const char*>(arr);

    std::cout << "sizeof(int)   = " << sizeof(int) << " bytes\n";
    std::cout << "sizeof(arr)   = " << sizeof(arr) << " bytes  (10 x 4)\n";
    std::cout << "arr           = " << static_cast<const void*>(arr) << "   <- base address\n";
    std::cout << "&arr[0]       = " << static_cast<const void*>(&arr[0]) << "   <- the same value\n\n";

    std::cout << " i   &arr[i] - base   i * sizeof(int)\n";
    for (int i = 0; i < 10; ++i) {
        std::ptrdiff_t offset = reinterpret_cast<const char*>(&arr[i]) - base;
        std::cout << std::setw(2) << i
                  << std::setw(15) << offset
                  << std::setw(17) << i * static_cast<int>(sizeof(int)) << '\n';
    }

    int m[4][5] = {};
    const char* mbase = reinterpret_cast<const char*>(m);
    std::cout << "\nint m[4][5]  ROWS = 4, COLS = 5\n";
    std::cout << "sizeof(m)     = " << sizeof(m) << " bytes\n";
    std::cout << "sizeof(m[0])  = " << sizeof(m[0]) << " bytes  <- one whole row\n\n";
    std::cout << " i  j   &m[i][j] - base   (i*COLS + j) * 4\n";
    const int probe[][2] = {{0,0},{0,4},{1,0},{2,3},{3,4}};
    for (const auto& p : probe) {
        int i = p[0], j = p[1];
        std::ptrdiff_t offset = reinterpret_cast<const char*>(&m[i][j]) - mbase;
        std::cout << std::setw(2) << i << std::setw(3) << j
                  << std::setw(17) << offset
                  << std::setw(20) << (i * 5 + j) * 4 << '\n';
    }
}

/* Output (the base address differs on every run; the offsets never do):

sizeof(int)   = 4 bytes
sizeof(arr)   = 40 bytes  (10 x 4)
arr           = 0x16ba022b0   <- base address
&arr[0]       = 0x16ba022b0   <- the same value

 i   &arr[i] - base   i * sizeof(int)
 0              0                0
 1              4                4
 2              8                8
 3             12               12
 ...
 9             36               36

int m[4][5]  ROWS = 4, COLS = 5
sizeof(m)     = 80 bytes
sizeof(m[0])  = 20 bytes  <- one whole row

 i  j   &m[i][j] - base   (i*COLS + j) * 4
 0  0                0                   0
 0  4               16                  16
 1  0               20                  20
 2  3               52                  52
 3  4               76                  76
*/

Three things you cannot do to a whole array

C++ does not support aggregate operations on arrays: you cannot treat an array as a single value the way you can a string or a struct. The trap is that two of the three attempts compile, so nothing tells you at the time.

What you writeWhat actually happensWhat to write instead
arrayA = arrayB;Does not compile at all. An array name is not an assignable value.for (int i = 0; i < n; ++i) arrayA[i] = arrayB[i];
if (arrayA == arrayB)Compiles. Both names decay to pointers, so this compares two base addresses — always false for two distinct arrays, however identical their contents.Loop and compare element by element, returning false on the first mismatch
cout << list;Compiles. Prints the base address, not the elements.for (int i = 0; i < n; ++i) cout << list[i] << ' ';
cin >> list;Not valid for a numeric array.Read into list[i] inside a loop

The reason all of this happens is one rule: in almost any expression, an array name decays to a pointer to its first element. arrayA == arrayB is therefore a pointer comparison, and cout << list picks the void* overload of operator<<. Recent compilers do warn on the comparison — clang and GCC both emit -Warray-compare, and comparing arrays this way is deprecated in C++20 — but a warning is not an error and plenty of build setups never show it.

The one exception is char. cout << word; for a char word[8] prints the characters, because operator<< has a dedicated overload for const char* that treats it as a null-terminated string. So the rule you may have absorbed from printing C-strings is the special case, and the address behaviour is the norm. The program below shows both side by side. (It compares through explicit pointers rather than writing a == b directly, purely to keep the build warning-free — the semantics are exactly the same.)

cpp
#include <iostream>

int main() {
    int a[5] = {1, 2, 3, 4, 5};
    int b[5] = {1, 2, 3, 4, 5};

    std::cout << "cout << a  gives " << a << '\n';
    std::cout << "cout << b  gives " << b << '\n';

    int* pa = a;
    int* pb = b;
    std::cout << "a == b     gives " << (pa == pb)
              << "   (address comparison, not contents)\n";

    bool same = true;
    for (int i = 0; i < 5; ++i) {
        if (a[i] != b[i]) { same = false; break; }
    }
    std::cout << "element-by-element equal: " << same << '\n';

    for (int i = 0; i < 5; ++i) b[i] = a[i];
    std::cout << "after the copy loop, b = ";
    for (int i = 0; i < 5; ++i) std::cout << b[i] << ' ';
    std::cout << '\n';

    char word[8] = "queue";
    std::cout << "cout << word gives " << word
              << "   (char arrays are the exception)\n";
}

/* Output (the two addresses differ every run; nothing else does):

cout << a  gives 0x16ae222c0
cout << b  gives 0x16ae222a0
a == b     gives 0   (address comparison, not contents)
element-by-element equal: 1
after the copy loop, b = 1 2 3 4 5
cout << word gives queue   (char arrays are the exception)
*/

Passing to a function: arrays go by reference, and their size does not go at all

Start with the ordinary case, because the array case is defined against it.

Pass by valuePass by reference
What is passedA copy of the argument's dataThe address of the argument — the parameter is an alias for it
Syntaxvoid inc(int x)void grow(int& x)
Effect on the caller's variableNone; changes stay inside the functionThe function operates on the original
Use it whenThe function only needs to read(a) you must change the caller's variable, (b) you need to "return" more than one value, (c) copying the object would be expensive

Now the rule that catches people: arrays are always passed by reference, even though you never write &. void printArr(int arr[], int size) looks like it takes a copy and does not. The array name decays to a pointer, the function receives that pointer, and every write through it lands in the caller's array. There is no such thing as accidentally passing an array by value in C++.

Two consequences follow directly.

  1. The size does not travel with it. The parameter is a pointer, and a pointer knows nothing about how many elements it leads to. This is why the size is conventionally passed as a second parameter. It is also why int arr[], int arr[100] and int* arr are the same parameter type — a number written inside the brackets of a parameter is ignored by the compiler entirely.
  2. sizeof stops working inside the function. In main, sizeof(data) / sizeof(data[0]) gives the true length. Inside a function taking int arr[], sizeof(arr) is the size of a pointer — 8 bytes on a 64-bit build — so the same expression yields 2 for an array of int, no matter how long the array really is. It is a wrong answer that looks like a plausible one.

If you want the compiler to stop the function modifying the array, say so: int sumOf(const int arr[], int size). That is a habit worth having, and it documents intent better than a comment.

cpp
#include <iostream>

// These three parameter lists are the SAME type: int*.
void byBrackets(int arr[]);
void bySizedBrackets(int arr[100]);
void byPointer(int* arr);

void scaleAll(int arr[], int size) {   // no '&' anywhere, yet the caller sees the change
    for (int i = 0; i < size; ++i) arr[i] *= 10;
}

void sizeInside(int* arr) {
    std::cout << "inside the function, sizeof(arr) = " << sizeof(arr)
              << " bytes -- that is one pointer, not the array\n";
}

void byValue(int x)  { x = 99; std::cout << "  (inside byValue, the copy is now " << x << ")\n"; }
void byRef(int& x)   { x = 99; }

int main() {
    int data[4] = {1, 2, 3, 4};

    std::cout << "in main, sizeof(data) = " << sizeof(data)
              << " bytes, so length = " << sizeof(data) / sizeof(data[0]) << '\n';
    sizeInside(data);

    scaleAll(data, 4);
    std::cout << "after scaleAll: ";
    for (int i = 0; i < 4; ++i) std::cout << data[i] << ' ';
    std::cout << '\n';

    int age = 20;
    byValue(age);
    std::cout << "after byValue(age): " << age << '\n';
    byRef(age);
    std::cout << "after byRef(age):   " << age << '\n';
}

/* Output:

in main, sizeof(data) = 16 bytes, so length = 4
inside the function, sizeof(arr) = 8 bytes -- that is one pointer, not the array
after scaleAll: 10 20 30 40
  (inside byValue, the copy is now 99)
after byValue(age): 20
after byRef(age):   99
*/

struct versus class: one default, and nothing else

This comes up as a direct question often enough to be worth stating in one sentence: the only language-level difference between a struct and a class in C++ is the default access specifier — struct members are public by default, class members are private by default. (There is a second, rarely-tested default: inheritance is public for a struct and private for a class.)

Everything else people believe distinguishes them is false. Both can have constructors, both can have destructors, both can have member functions, both can inherit and be inherited from, both can be accessed through a pointer with p->field or (*p).field, and both hold heterogeneous data — fields of different types, which is exactly what an array cannot do.

structclass
Default member accesspublicprivate
Default inheritancepublicprivate
Constructors / destructorsYesYes
Member functionsYesYes
Inheritance, polymorphismYesYes
Field typesHeterogeneousHeterogeneous
Conventional useA plain record: data grouped under one nameData plus behaviour, with the internals hidden behind an interface

"A struct cannot have a constructor" is the classic wrong answer, and it is wrong in a way that matters: linked-list node types are routinely written as structs with constructors, which is what lets you write new Node(value) instead of allocating and then assigning each field.

The convention that survives the fact that the two are nearly identical: reach for struct when the thing is a bundle of data every part of the program may touch — a list node, a coordinate pair — and for class when there is an invariant to protect, so that the data is private and the only way in is through member functions you wrote. This is encapsulation: packaging data and operations together while hiding the implementation from the user.

One lifetime rule belongs here because it breaks linked lists specifically. A node created with new NodeType lives on the heap and survives after the function that created it returns. A node created as a local variable lives on the stack and is destroyed the instant the function ends — so returning a pointer to it hands the caller an address that is already invalid. Every node you intend to keep in a list must come from new.

cpp
#include <iostream>

struct StructNode {      // members are public by default
    int info;
    StructNode* link;
};

class ClassNode {        // members are private by default...
public:                  // ...so the access level has to be stated
    int info;
    ClassNode* link;
};

class Hidden {
    int secret = 42;     // no 'public:' above it, so this is private
public:
    int peek() const { return secret; }
};

int main() {
    StructNode a{1, nullptr};
    ClassNode  b{2, nullptr};
    Hidden     h;

    std::cout << "a.info = " << a.info << '\n';
    std::cout << "b.info = " << b.info << '\n';
    std::cout << "h.peek() = " << h.peek() << '\n';
    // std::cout << h.secret;   // compiler error: 'secret' is private
}

/* Output:

a.info = 1
b.info = 2
h.peek() = 42
*/

Constructor, destructor, and the words that go with them

These definitions are worth being able to produce verbatim, because they are asked as fill-in-the-blank and drag-the-word items where a near-miss earns nothing.

TermDefinition
ClassA blueprint describing the attributes and operations of a kind of object
ObjectA specific instance of a class, with concrete values for its attributes
Data member / attributeA variable declared inside a class that represents a property of the object
Member function / methodA function declared inside a class that represents an operation of the object
ConstructorA member function called automatically when an object is created, to initialise it. Same name as the class, no return type
Constructor overloadingHaving more than one constructor, differing in the number and/or types of parameters
DestructorA member function that runs automatically when an object is destroyed, to release resources. Named ~ followed by the class name; no parameters, no return type, and a class can have only one
this pointerAn implicit pointer inside a member function that refers to the calling object itself
Scope resolution operator ::Used to define a member function outside its class: void Rectangle::setSize(int a, int b) { ... }
static memberDeclared static; exists even when no object of the class does, and is reached as ClassName::member

The rule that most often surprises people: if you declare no constructor at all, the compiler supplies a default one. The moment you declare any constructor, that free default disappears. So adding Node(int value) to a struct makes Node n; stop compiling unless you also write Node() = default;. This is a real source of "but it compiled yesterday" errors when someone adds a convenience constructor to a node type.

The program below makes the automatic part visible. Nothing in main calls a constructor or a destructor by name; they run on creation and on destruction, and the destructors fire in reverse order of construction as the objects go out of scope. That last detail is why a destructor is the right place to free a linked list: the object owning the list is destroyed, the destructor runs, and the nodes get released without the caller remembering to ask.

cpp
#include <iostream>

// A struct: members are public unless you say otherwise.
struct NodeType {
    int info;
    NodeType* link;
    NodeType(int value) : info(value), link(nullptr) {}   // structs CAN have constructors
    ~NodeType() { std::cout << "  destroying node " << info << '\n'; }
};

// A class: members are private unless you say otherwise.
class Counter {
public:
    Counter() : count_(0) { std::cout << "Counter() ran automatically\n"; }
    Counter(int start) : count_(start) { std::cout << "Counter(int) ran, start = " << start << '\n'; }
    ~Counter() { std::cout << "~Counter() ran, final value = " << count_ << '\n'; }

    void bump() { ++count_; }
    int value() const { return count_; }

private:
    int count_;      // private: main() cannot touch this directly
};

int main() {
    NodeType n(7);
    std::cout << "n.info = " << n.info
              << ", n.link is null? " << (n.link == nullptr) << '\n';

    Counter zero;          // picks the no-argument constructor
    zero.bump();
    std::cout << "zero.value() = " << zero.value() << '\n';

    Counter c(5);          // picks the int constructor -- overloading
    c.bump();
    c.bump();
    std::cout << "c.value() = " << c.value() << '\n';
    // c.count_ = 100;   // would not compile: count_ is private

    std::cout << "-- leaving main, destructors run in reverse order --\n";
}

/* Output:

n.info = 7, n.link is null? 1
Counter() ran automatically
zero.value() = 1
Counter(int) ran, start = 5
c.value() = 7
-- leaving main, destructors run in reverse order --
~Counter() ran, final value = 7
~Counter() ran, final value = 1
  destroying node 7
*/

The vocabulary the theory questions are written in

Short-answer and definition questions draw on a small fixed set of terms. They are dull and they are cheap marks.

Abstract Data Type (ADT). A data declaration packaged together with the operations that are meaningful for that data, where the operations are defined at a formal, logical level without being tied to implementation details. Asked as "state the three items an ADT must include", the answer is: (1) declaration of the data, (2) implementation of the operations, and (3) encapsulation of the data and the operations.

The standard illustration is the queue. A queue can be backed by an array, by a linked list, or by a file. If the queue is presented as an ADT, the user should not have to know which — as long as they can enqueue and dequeue, how the data is stored makes no difference to them. That separation between what the operations mean and how they are implemented is the entire point of the term.

The three complexity cases. Each describes a function of the input size n:

  • Best case — the minimum number of steps taken on any instance of size n.
  • Worst case — the maximum number of steps taken on any instance of size n.
  • Average case — the average number of steps taken over instances of size n.

Efficiency is measured against two resources, time and space, and a good algorithm balances both rather than minimising one at the other's expense. Time complexity is how long a process takes as a function of n; space complexity is how much memory it uses as a function of n.

The classification tree, which several definitions hang off:

text
Data structures
|-- Primitive      : int, float, char, bool, double, pointer
`-- Non-primitive
    |-- Linear     : array, linked list, stack, queue
    `-- Non-linear : tree, graph

Linear means the elements sit in a sequence, one after another. Non-linear means they are arranged hierarchically, with one element connected to several others — things that branch.

TermDefinition
DataRaw facts and figures processed by a computer to produce information
Data structureAn organisation of information, usually in memory, for better algorithm efficiency
AlgorithmA clearly specified set of simple instructions to be followed to solve a problem
Atomic (scalar) dataData we choose to treat as a single, non-decomposable entity, e.g. the integer 1234
Composite (structured) dataData that can be broken into subfields that have meaning, e.g. a student record
Primitive data structureA basic type operated on directly by machine instructions — the built-in types
Non-primitive data structureA structure built by the programmer from primitive types: array, list, stack, queue, tree, graph
Static data structureSize fixed at compile time, memory allocated once, cannot grow or shrink (static array)
Dynamic data structureSize decided at run time, memory allocated and released on demand with new / delete (linked list)
HomogeneousAll elements of the same type (array, stack, queue)
HeterogeneousElements may be of different types (struct, class)
Random accessReaching any element directly via its index, without traversal
Sequential accessReaching an element only by stepping through the ones before it
Contiguous memoryElements stored back to back in adjacent locations, which is what makes index access direct

Worth noticing: whether a piece of data is atomic or composite is a decision, not a property. A phone number is atomic if your program only ever stores and prints it whole, and composite if it splits out the area code. Saying so is usually what separates a full-mark answer from a half one.

Input handling: where working programs go wrong

This last section is not theory. It is the set of facts that turn a correct program into one that prints an empty name, and it costs more marks in practice than any algorithm on this page.

cin >> stops at whitespace; getline stops at the newline. cin >> name; reads up to the first space, tab or newline and leaves the rest in the buffer, so "John Smith" gives you "John". getline(cin, name); reads the whole line including spaces. If a field can contain a space, you need getline.

The leftover newline. This is the actual bug. cin >> age; extracts the digits and leaves the newline you pressed sitting in the buffer. The next getline sees that newline immediately, decides the line is over, and returns an empty string — with no error, no exception, and nothing in the output to explain it. The fix is to discard the rest of the line before the getline:

cpp
cin.ignore(numeric_limits<streamsize>::max(), '\n');   // needs <limits>

That means "discard characters until you have thrown away a newline, however many that takes". The short form cin.ignore(); throws away exactly one character, which is enough when you know the newline is next and not enough when it might not be.

NeedWrite
Read a whole line, spaces includedgetline(cin, line);
Discard the rest of the current linecin.ignore(numeric_limits<streamsize>::max(), '\n');
Clear a stream's error flags after a failed readcin.clear();
Test whether the last read failedcin.fail()true when the user typed letters into an int
Read a single character, whitespace includedcin.get(ch);
Show exactly two decimal placescout << fixed << setprecision(2); — needs <iomanip>
Set the field width of the next outputcout << setw(12);

fixed forces decimal notation instead of scientific, and setprecision(2) then means "two digits after the point" rather than "two significant figures" — the two manipulators are almost always needed together, and both stay in effect until changed. setw(n) applies to the next output item only, which is why it is repeated inside table-printing loops.

Recovering from a failed numeric read takes both calls, in this order: cin.clear(); to reset the error state, then cin.ignore(...) to throw away the offending characters. Skip the clear and every later read fails silently; skip the ignore and the bad input is still sitting there waiting to fail again, usually as an infinite loop.

The program below was fed 20, John Smith, 30, Ada Lovelace on standard input. The first block shows the bug and then proves the line was never consumed by reading it with a second getline. The second block does it correctly.

cpp
#include <iomanip>
#include <ios>
#include <iostream>
#include <limits>
#include <string>

int main() {
    // ---- the bug: input is "20" then "John Smith" ----
    int age = 0;
    std::cin >> age;                        // reads 20, LEAVES the '\n' in the buffer
    std::string name;
    std::getline(std::cin, name);           // sees that '\n' at once, stops immediately
    std::cout << "age       = " << age << '\n';
    std::cout << "name      = [" << name << "]   length " << name.size()
              << "   <- empty, and no error was reported\n";

    std::string recovered;
    std::getline(std::cin, recovered);      // the line was never consumed; here it is
    std::cout << "recovered = [" << recovered << "]\n\n";

    // ---- the fix: input is "30" then "Ada Lovelace" ----
    int qty = 0;
    std::cin >> qty;
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
    std::string fullName;
    std::getline(std::cin, fullName);
    std::cout << "qty       = " << qty << '\n';
    std::cout << "fullName  = [" << fullName << "]   length " << fullName.size() << "\n\n";

    // ---- formatting ----
    double price = 1234.5;
    std::cout << "default             " << price << '\n';
    std::cout << std::fixed << std::setprecision(2);
    std::cout << "fixed + setprecision(2)  " << price << '\n';
    std::cout << "setw(12)            [" << std::setw(12) << price << "]\n";
}

/* Input fed on stdin:  20 / John Smith / 30 / Ada Lovelace
   Output:

age       = 20
name      = []   length 0   <- empty, and no error was reported
recovered = [John Smith]

qty       = 30
fullName  = [Ada Lovelace]   length 12

default             1234.5
fixed + setprecision(2)  1234.50
setw(12)            [     1234.50]
*/

Exercises with solutions

Work through each question before opening the solution below it.

Exercise 1

Address arithmetic, both conventions. An integer array is declared int A[6][8]; with base address 3000, where sizeof(int) is 4 bytes.

  1. How many bytes does the whole array occupy, and what is the address of its last byte?
  2. What is &A[3][5] in row-major order?
  3. What is &A[3][5] in column-major order?
  4. Which one would a C++ program actually give you, and why?
  5. Separately: double d[20]; has base address 4000 and sizeof(double) is 8. What is &d[12], and what is the address one past the end of the array?

Solution

1. Total size. 6 × 8 = 48 elements × 4 bytes = 192 bytes, occupying 3000 to 3191 inclusive. The last element, A[5][7], begins at 3000 + (5 × 8 + 7) × 4 = 3000 + 47 × 4 = 3188, and its four bytes run 3188–3191. Distinguish the address of the last element from the address of the last byte; questions ask for both.

2. Row-major. COLS = 8, so crossing one whole row skips 8 elements:

3000 + (3 × 8 + 5) × 4 = 3000 + 29 × 4 = 3000 + 116 = 3116

3. Column-major. ROWS = 6, so crossing one whole column skips 6 elements:

3000 + (5 × 6 + 3) × 4 = 3000 + 33 × 4 = 3000 + 132 = 3132

4. C++ is row-major, so a real program gives 3116. The elements of A[0] sit at offsets 0 through 28, then A[1] starts at offset 32 — rows are stored one after another, which is exactly why the row index is the one multiplied by the column count. A quick way to check yourself: in row-major, A[i][j] and A[i][j+1] must be adjacent, so changing j by 1 must change the address by one element. It does: j is added unscaled. In column-major the same is true of changing i.

5. The 1-D case. &d[12] = 4000 + 12 × 8 = 4000 + 96 = **4096**. The array is 20 × 8 = 160 bytes spanning 4000 to 4159, so one past the end is 4000 + 20 × 8 = 4160. That address is legal to compute and form a pointer to, and illegal to dereference — it is where end() points in every standard container.

Exercise 2

Read the output. Without running it, say exactly what this program prints, and explain each line. Then say what would happen if the two pointer variables were removed and the comparison written directly as p == q.

cpp
#include <iostream>

void mystery(int arr[], int n) {
    for (int i = 0; i < n; ++i) arr[i] += 1;
}

int main() {
    int p[4] = {2, 4, 6, 8};
    int q[4] = {2, 4, 6, 8};

    int* pp = p;
    int* qq = q;
    std::cout << (pp == qq) << '\n';
    std::cout << (p[0] == q[0]) << '\n';

    mystery(p, 4);
    std::cout << p[0] << ' ' << p[3] << '\n';

    int total = 0;
    for (int i = 0; i < 4; ++i) total += p[i];
    std::cout << total << '\n';
}

Solution

The output is:

text
0
1
3 9
24

Line 1 — 0. pp and qq hold the base addresses of two different arrays, so they are different values and the comparison is false. The contents are identical and that is completely irrelevant: comparing arrays never compares contents. This is the single most useful thing to take from the aggregate-operations rule.

Line 2 — 1. p[0] == q[0] compares two int values, 2 against 2, which is true. Indexing gets you elements, and elements compare the way you expect. The difference between lines 1 and 2 is the whole trap in miniature.

Line 3 — 3 9. mystery has no & in its parameter list, yet the caller's array is modified: the array decayed to a pointer, mystery wrote through it, and those writes went straight into p. Every element gained 1, so p is now {3, 5, 7, 9}. An array parameter is always by reference; there is no way to pass one by value.

Line 4 — 24. The sum of 3 + 5 + 7 + 9, using the modified values.

Writing p == q directly. Semantically identical — both arrays decay to pointers and you get the same address comparison, so it still prints 0. The difference is diagnostic: clang and GCC emit a -Warray-compare warning for a direct comparison of two array names, and the construct is deprecated in C++20, precisely because it is almost never what the author meant. Going through explicit pointers silences the warning without changing the behaviour, which is why the version above compiles clean. If you see that warning in your own code, the fix is a comparison loop, not a cast.

Exercise 3

Fix the length function. A student writes a helper to find how many elements are in an array and cannot understand why it always returns 2.

cpp
int lengthOf(int arr[]) {
    return sizeof(arr) / sizeof(arr[0]);
}

Explain precisely where the 2 comes from, say whether the compiler gives any warning, and rewrite the code so that summing an array works correctly. Also write a function that swaps two int variables belonging to the caller, and explain why the array case needs no & but the swap does.

Solution

Where the 2 comes from. Inside the function, arr is not an array — it is a pointer. int arr[] as a parameter is just another spelling of int* arr, and any size you write in those brackets is discarded by the compiler. So sizeof(arr) is the size of a pointer, 8 bytes on a typical 64-bit build, while sizeof(arr[0]) is sizeof(int), 4 bytes. 8 / 4 = 2, for every array of int you ever pass, regardless of its real length. On a 32-bit build the same code returns 1. The value is a plausible-looking small number, which is what makes the bug survive testing.

Does the compiler warn? Yes, and it is unusually clear about it. Both clang and GCC produce two diagnostics here:

text
warning: sizeof on array function parameter will return size of 'int *'
         instead of 'int[]' [-Wsizeof-array-argument]
warning: 'sizeof (arr)' will return the size of the pointer, not the array
         itself [-Wsizeof-pointer-div]

They only appear if warnings are switched on. Compile with -Wall -Wextra as a matter of course; this is exactly the class of bug it exists to catch.

The fix: the size is a separate parameter. The length is only computable where the real array is in scope — in main, or wherever it was declared.

cpp
int sumOf(const int arr[], int size) {
    int total = 0;
    for (int i = 0; i < size; ++i) total += arr[i];
    return total;
}

void swapTwo(int& a, int& b) { int t = a; a = b; b = t; }

int main() {
    int data[6] = {1, 2, 3, 4, 5, 6};
    int n = sizeof(data) / sizeof(data[0]);   // 6 -- correct here, and only here
    std::cout << sumOf(data, n) << '\n';      // 21

    int x = 3, y = 9;
    swapTwo(x, y);
    std::cout << x << ' ' << y << '\n';       // 9 3
}

const on the array parameter is worth adding whenever the function only reads: it makes the compiler enforce what the comment would otherwise merely claim.

Why the asymmetry. An int argument is a plain value, so by default the function gets a copy and the caller's variable is untouchable; & is what turns the parameter into an alias for the original, and without it swapTwo would swap two local copies and appear to do nothing. An array argument is never copied in the first place — the array name decays to a pointer to its first element, that pointer is what the function receives, and every write through it lands in the caller's memory. The reference behaviour comes free with the decay, and the cost of that free behaviour is the lost size.

Exercise 4

Constructors, destructors, and lifetime. Give the exact output of this program, in order, and explain why dtor 2 appears where it does and dtor 1 appears where it does.

cpp
#include <iostream>

class Tracker {
public:
    Tracker(int id) : id_(id) { std::cout << "ctor " << id_ << '\n'; }
    ~Tracker() { std::cout << "dtor " << id_ << '\n'; }
private:
    int id_;
};

void useOne() {
    Tracker local(2);
    std::cout << "inside useOne\n";
}

int main() {
    Tracker first(1);
    useOne();
    Tracker* onHeap = new Tracker(3);
    std::cout << "before delete\n";
    delete onHeap;
    std::cout << "end of main\n";
}

Then answer two follow-ups: what happens if the delete is removed, and what breaks if useOne is changed to return &local; so that a caller can use the object afterwards?

Solution

The output:

text
ctor 1
ctor 2
inside useOne
dtor 2
ctor 3
before delete
dtor 3
end of main
dtor 1

Why dtor 2 lands there. local is a stack object. Its destructor runs automatically the moment control leaves useOne, before main continues — so dtor 2 prints between inside useOne and ctor 3. Nobody wrote a line to make that happen; scope exit is the trigger.

Why dtor 1 is last. first is also a stack object, but its scope is the whole of main, so it is destroyed only when main ends — after end of main has been printed. With several stack objects in one scope, destruction runs in reverse order of construction, so the first thing built is the last thing torn down.

Why dtor 3 sits between the two prints. A heap object created with new ignores scope entirely. Its destructor runs when, and only when, you call delete. That is the point of the heap and the reason linked-list nodes live there: the node has to outlive the function that created it.

Follow-up 1: remove the delete. dtor 3 never prints and the Tracker is leaked. The program still exits cleanly and prints a plausible-looking result, which is why leaks go unnoticed — the operating system reclaims the pages at exit and nothing complains. In a long-running program, or a linked list that allocates a node per insertion, the same omission is fatal. This is the reason a container that owns nodes must free them in its destructor.

Follow-up 2: return &local;. local is destroyed at the closing brace of useOne, so the returned address points at a stack slot that no longer belongs to anyone — a dangling pointer. Reading it is undefined behaviour, and the cruel part is that it usually appears to work: the bytes are often still intact until the next function call reuses that region of stack, so the bug surfaces later and somewhere else. Compilers do catch this specific shape (-Wreturn-local-addr), and AddressSanitizer catches it at run time.

The general rule this exercise is really about: if an object must survive the function that made it, it has to come from new, and something must own the job of deleting it. Every node in every linked structure on this site follows that rule.

Published and updated 2026-08-09 · DUOCODE TECHNOLOGY