This is the foundation of everything that follows: how a C++ program is built, how its basic building blocks (functions, types, variables, pointers, references) behave, and — the thread that runs through the whole course — how each construct maps onto the machine. C++ is famous for being close to the hardware, and the whole book is an argument that understanding that mapping is what lets you write code that is fast, correct, and predictable. Mission tie-in: this chapter gives you the procedural core you will need before classes (§2), templates (§6–8), and the standard library (§9–18) make any sense.
Three things to internalize from Chapter 1:
x + y is an
integer-add machine instruction; a pointer is a machine address. Prefer
{}-initialization, nullptr, and const to their
older, sloppier alternatives.
C++ is a compiled language. The source text is processed by a compiler into object files, which a linker combines into an executable program. A real program typically consists of many source files.
source files → compiler → object files → linker → executable
An executable is created for a specific hardware/system combination — it will not run, say, on both an Android device and a Windows PC. When we speak of C++ portability, we mean portability of source code: the same source can be successfully compiled and run on a variety of systems.
The ISO C++ standard defines two kinds of entities:
char,
int) and control flow (e.g. for-, while-statements).
vector,
map) and I/O (e.g. <<, getline()).
Crucially, the standard library is ordinary C++ code provided by every implementation — it can be (and is) written in C++ itself, with only trivial uses of machine code for things like thread context switching. That C++ can host its own standard library is proof that it is expressive and efficient enough for the most demanding systems programming tasks.
C++ is a statically typed language. The type of every entity (object, value, name, expression) must be known to the compiler at its point of use. The type of an object determines (a) the set of operations applicable to it and (b) its layout in memory.
The minimal C++ program defines main, takes no arguments, and does nothing:
int main() { } // the minimal C++ program
Curly braces { } express grouping in C++; here they delimit the function body.
The double slash // begins a comment that extends to the end of the line. A
comment is for the human reader; the compiler ignores it.
Every C++ program must have exactly one global function named
main().
The program starts by executing it. The int
value it returns is the program's return value to "the system": 0 (or nothing) means
success, nonzero means failure. Not every environment uses it — Linux/Unix do, but
Windows-based environments rarely do.
Here is a program that actually produces output:
import std;
int main()
{
std::cout << "Hello, World!\n";
}
The line import std; makes the standard library declarations available (a C++20
feature; presenting the whole library as one module std is not yet standard).
Without it, the expression below would make no sense. The operator << ("put
to") writes its second argument onto its first: here the string literal
"Hello, World!\n" is written onto the standard output stream
std::cout.
A string literal is a sequence of characters surrounded by double quotes.
Inside one, a backslash followed by another character denotes a single "special character":
\n is the newline character, so the output is Hello, World! followed
by a newline.
The std:: prefix says the name cout lives in the standard-library
namespace (details in §3.3 of the book). If import std; gives
you trouble, the conventional alternative — which has worked on all implementations since 1998
— is:
#include <iostream> // include the declarations for the I/O stream library
int main()
{
std::cout << "Hello, World!\n";
}
Essentially all executable code lives in functions and is called directly or indirectly from
main(). Here is a two-function example, using
using namespace std; so the std:: qualifier can be dropped:
import std;
using namespace std; // make names from std visible without std::
double square(double x) // square a double-precision floating-point number
{
return x * x;
}
void print_square(double x)
{
cout << "the square of " << x << " is " << square(x) << "\n";
}
int main()
{
print_square(1.234); // print: the square of 1.234 is 1.52276
}
A return type of void indicates that a function does not return a value.
The main way to get something done in a C++ program is to call a function; defining a function is how you specify an operation. A function cannot be called unless it has been declared.
A function declaration gives the function's name, the type of the value it returns (if any), and the number and types of its arguments. The return type comes before the name; the argument types come after the name in parentheses:
Elem* next_elem(); // no argument; return a pointer to Elem (an Elem*)
void exit(int); // int argument; return nothing
double sqrt(double); // double argument; return a double
Argument passing has the same semantics as initialization: argument types are checked, and implicit type conversion happens when necessary. The value of this compile-time checking and conversion should not be underestimated:
double s2 = sqrt(2); // OK: call sqrt() with the argument double{2}
double s3 = sqrt("three"); // error: sqrt() requires an argument of type double
A declaration may include argument names — a help to readers — but unless the declaration is also a definition, the compiler ignores such names:
double sqrt(double d); // return the square root of d
double square(double); // return the square of the argument
The type of a function consists of its return type followed by the sequence of its argument types in parentheses:
double get(const vector<double>& vec, int index); // type: double(const vector<double>&, int)
A function can be a member of a class; then the class name is part of the function type too:
char& String::operator[](int index); // type: char& String::(int)
Comprehensibility is the first step toward maintainability, and it comes from breaking
computational tasks into meaningful, named chunks — the functions provide the
basic vocabulary of computation, just as types provide the vocabulary of data. The standard
algorithms (find, sort, iota, …) are a good place to
start composing such functions.
Code error rate correlates with amount and complexity of code. Both are addressed by using more, shorter functions. And if you cannot find a suitable name for an operation, there is a high probability you have a design problem.
Defining multiple functions with the same name but different argument types is function overloading. The compiler picks the most appropriate one for each call:
void print(int); // takes an integer argument
void print(double); // takes a floating-point argument
void print(string); // takes a string argument
void user()
{
print(42); // calls print(int)
print(9.65); // calls print(double)
print("Barcelona"); // calls print(string)
}
If two alternatives match a call but neither is better, the call is ambiguous and the compiler gives an error:
void print(int, double);
void print(double, int);
void user2()
{
print(0, 0); // error: ambiguous
}
Overloaded functions must implement the same semantics. Each
print() prints its argument — that is the whole point. Overloading is one of
the essential parts of generic programming (§8.2).
Every name and every expression has a type that determines the operations that may be performed on it. A declaration is a statement that introduces an entity into the program and specifies its type. Four terms to keep precise:
| Term | Definition |
|---|---|
| Type | Defines a set of possible values and a set of operations (for an object). |
| Object | Some memory that holds a value of some type. |
| Value | A set of bits interpreted according to a type. |
| Variable | A named object. |
C++ offers a small zoo of fundamental types. The ones you will meet constantly:
| Type | Meaning | Example literals |
|---|---|---|
bool |
Boolean; possible values true and false |
— |
char |
Character (typically an 8-bit byte) | 'a', 'z', '9' |
int |
Integer | -273, 42, 1066 |
double |
Double-precision floating-point number | -273.15, 3.14, 6.626e-34 |
unsigned |
Non-negative integer | 0, 1, 999 |
Each fundamental type corresponds directly to hardware facilities and has a fixed size that
determines the range of values it can store. A char is the natural size to hold a
character on a given machine, and every other type's size is a multiple of the size of a
char. Sizes are implementation-defined and obtained with the
sizeof operator: sizeof(char) is 1, sizeof(int) is
often 4. When you need a guaranteed size, use a standard-library type alias such as
int32_t (§17.8).
3.14) or an exponent (314e-2).
0b binary (0b10101010), 0x hexadecimal
(0xBAD12CE3), 0 octal (0334).
To make long literals readable, use a single quote ' as a digit separator — π
is about 3.14159'26535'89793'23846'26433'83279'50288, or in hex
0x3.243F'6A88'85A3'08D3.
Arithmetic operators, usable for appropriate combinations of the fundamental types:
x + y // plus +x // unary plus
x - y // minus -x // unary minus
x * y // multiply
x / y // divide
x % y // remainder (modulus) for integers
Comparison operators:
x == y // equal
x != y // not equal
x < y // less than
x > y // greater than
x <= y // less than or equal
x >= y // greater than or equal
Logical operators — note the bitwise family operates on the bits of its operands and
yields a value of the operand type, while logical operators simply return
true/false based on operand truthiness:
x & y // bitwise and
x | y // bitwise or
x ^ y // bitwise exclusive or
~x // bitwise complement
x && y // logical and
x || y // logical or
!x // logical not (negation)
In assignments and arithmetic, C++ performs all meaningful conversions between the basic types so they can be mixed freely:
void some_function() // function that doesn't return a value
{
double d = 2.2; // initialize floating-point number
int i = 7; // initialize integer
d = d + i; // assign sum to d
i = d * i; // assign product to i; beware: truncating the double d*i to an int
}
These conversions are the usual arithmetic conversions: expressions are
computed at the highest precision of their operands, so
double + int is done in double-precision. And remember: = is
assignment, == tests equality.
Convenient compound operators for modifying a variable:
x += y; // x = x + y
++x; // increment: x = x + 1
x -= y; // x = x - y
--x; // decrement: x = x - 1
x *= y; // scaling: x = x * y
x /= y; // scaling: x = x / y
x %= y; // x = x % y
Order of evaluation:
x.y, x->y,
x(y), x[y], x<<y, x>>y,
x&&y, x||y.
x += y).f(x) + g(y) and
the arguments of h(f(x), g(y)) can be evaluated in any order (for historical
reasons related to optimization).
Before an object can be used, it must be given a value. C++ offers several notations: the
traditional = form (dating back to C) and a universal curly-brace-delimited
initializer list form:
double d1 = 2.3; // initialize d1 to 2.3
double d2 {2.3}; // initialize d2 to 2.3
double d3 = {2.3}; // initialize d3 to 2.3 (the = is optional with { ... })
complex<double> z = 1; // a complex number with double-precision floating-point scalars
complex<double> z2 {d1, d2};
complex<double> z3 = {d1, d2}; // the = is optional with { ... }
vector<int> v {1, 2, 3, 4, 5, 6}; // a vector of ints
If in doubt, use the general { }-list form. At the very least
it saves you from narrowing conversions — conversions that lose
information, such as double to int and int to
char:
int i1 = 7.8; // i1 becomes 7 (surprise?)
int i2 {7.8}; // error: floating-point to integer conversion
Implicit narrowing is allowed with = but not with {}; the problems
it causes are a price paid for C compatibility (§19.3).
A constant cannot be left uninitialized, and a variable should only be left
uninitialized in extremely rare circumstances.
Don't introduce a name until you have a suitable value for it. User-defined types
(such as string, vector, Matrix,
Motor_controller, or Orc_warrior) can be defined to initialize
themselves implicitly.
When the type can be deduced from the initializer, you don't need to state it explicitly. With
auto, the = form is typical (no risky conversion is involved), but
{} works too:
auto b = true; // a bool
auto ch = 'x'; // a char
auto i = 123; // an int
auto d = 1.2; // a double
auto z = sqrt(y); // z has the type of whatever sqrt(y) returns
auto bb {true}; // bb is a bool
We use auto where there is no specific reason to mention the type. "Specific
reasons" to spell it out:
double rather than
float).
auto avoids redundancy and long type names — especially important in generic
programming, where the exact type can be hard to know and the names can be long (§13.2).
A declaration introduces its name into a scope:
| Scope | What it covers | Extent |
|---|---|---|
| Local scope | Names declared in a function or lambda — including function argument names | From point of declaration to end of the enclosing { } block |
| Class scope | Member names declared in a class, outside any function/lambda/enum class | From the opening { to the matching } |
| Namespace scope | Namespace member names declared in a namespace, outside any function/lambda/class/enum class | From declaration to end of the namespace |
A name not declared inside any other construct is a global name in the
global namespace. We can also have objects without names —
temporaries and objects created with new:
vector<int> vec; // vec is global (a global vector of integers)
void fct(int arg) // fct is global (names a global function)
// arg is local (names an integer argument)
{
string motto {"Who dares wins"}; // motto is local
auto p = new Record{"Hume"}; // p points to an unnamed Record (created by new)
// ...
}
struct Record {
string name; // name is a member of Record (a string member)
// ...
};
An object must be constructed before it is used and is destroyed at the end of its
scope.
A namespace object dies at the end of the program; a member dies with the object that
contains it; an object created by new lives until destroyed by
delete (§5.2.2).
C++ supports two notions of immutability (an object with unchangeable state):
| Keyword | Meaning | Use |
|---|---|---|
const |
"I promise not to change this value." Compiler enforces the promise. May be computed at run time. | Specifying interfaces so data can be passed by pointer/reference without fear of modification. |
constexpr |
"To be evaluated at compile time." Value must be computed by the compiler. | Named constants, placing data in read-only memory (unlikely to be corrupted), performance. |
constexpr int dmv = 17; // dmv is a named constant
int var = 17; // var is not a constant
const double sqv = sqrt(var); // sqv is a named constant, possibly computed at run time
double sum(const vector<double>&); // sum will not modify its argument
vector<double> v {1.2, 3.4, 4.5}; // v is not a constant
const double s1 = sum(v); // OK: sum(v) is evaluated at run time
constexpr double s2 = sum(v); // error: sum(v) is not a constant expression
For a function to be usable in a constant expression (an expression evaluated
by the compiler), it must be declared constexpr (or consteval):
constexpr double square(double x) { return x * x; }
constexpr double max1 = 1.4 * square(17); // OK: 1.4*square(17) is a constant expression
constexpr double max2 = 1.4 * square(var); // error: var is not a constant, so square(var) is not a constant
const double max3 = 1.4 * square(var); // OK: may be evaluated at run time
A constexpr function may be called with non-constant arguments in contexts that
don't require constant expressions — the result just isn't a constant expression. That way you
don't define the same function twice. When a function should be usable only at
compile time, declare it consteval:
consteval double square2(double x) { return x * x; }
constexpr double max1 = 1.4 * square2(17); // OK: 1.4*square(17) is a constant expression
const double max3 = 1.4 * square2(var); // error: var is not a constant
constexpr/consteval functions are C++'s version of pure
functions.
They cannot have side effects and can only use information passed to them as arguments — in
particular they cannot modify non-local variables. They can have loops and their
own local variables:
constexpr double nth(double x, int n) // assume 0 <= n
{
double res = 1;
int i = 0;
while (i < n) { // while-loop: do while the condition is true
res *= x;
++i;
}
return res;
}
Constant expressions are required by language rules in a few places: array bounds
(§1.7), case labels (§1.8), template value arguments (§7.2), and
constexpr declarations. Elsewhere compile-time evaluation is a performance win.
Independent of performance, immutability is an important design concern in its own right.
The most fundamental collection of data is a contiguously allocated sequence of elements of the same type — an array. This is basically what the hardware offers:
char v[6]; // array of 6 characters
char* p; // pointer to character
In declarations, [ ] means "array of" and * means "pointer to". All
arrays have 0 as their lower bound, so v has six elements, v[0] to
v[5]. The size of an array must be a constant expression. A pointer variable can
hold the address of an object of the appropriate type:
char* p = &v[3]; // p points to v's fourth element
char x = *p; // *p is the object that p points to
In an expression, prefix unary * means "contents of" and prefix unary
& means "address of". (A pointer is just a machine address — this is the
hardware mapping from §1.9.)
Printing the elements of an array with a classic for-statement:
void print()
{
int v[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
for (auto i = 0; i != 10; ++i) // print elements
cout << v[i] << '\n';
// ...
}
Read it as "set i to zero; while i is not 10, print the
i-th element and increment i." C++ also offers a simpler
range-for for traversing a sequence in the simplest way:
void print2()
{
int v[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
for (auto x : v) // for each x in v
cout << x << '\n';
for (auto x : {10, 21, 32, 43, 54, 65}) // for each integer in the list
cout << x << '\n';
// ...
}
Read the first as "for every element of v, from the first to the last, place a
copy in x and print it." Note the array bound is deduced from the
initializer list. Range-for works for any sequence (§13.1).
If we don't want copies but want to modify elements, use a reference:
void increment()
{
int v[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
for (auto& x : v) // add 1 to each x in v
++x;
// ...
}
In a declaration, the unary suffix & means "reference to". A
reference is like a pointer, except you don't need a prefix
* to access the referred-to value, and it
cannot be made to refer to a different object after initialization.
References are especially useful for function arguments — no copying:
void sort(vector<double>& v); // sort v (v is a vector of doubles)
// call sort(my_vec): we do NOT copy my_vec, so it really is my_vec that gets sorted
When you don't want to modify an argument but still don't want the cost of copying, use a
const reference (a reference to a const):
double sum(const vector<double>&); // functions taking const references are very common
When used in declarations, operators like &, *, and
[ ] are called declarator operators:
| Declaration | Meaning |
|---|---|
T a[n] |
T[n]: a is an array of n Ts |
T* p |
T*: p is a pointer to T |
T& r |
T&: r is a reference to T |
T f(A) |
T(A): f takes an A and returns a T
|
We try to ensure a pointer always points to an object so dereferencing is valid. When there is
no object (e.g. end of a list), we give the pointer the value
nullptr — "the null pointer". There is one nullptr
shared by all pointer types:
double* pd = nullptr;
Link<Record>* lst = nullptr; // pointer to a Link to a Record
int x = nullptr; // error: nullptr is a pointer not an integer
It is often wise to check that a pointer argument actually points to something:
int count_x(const char* p, char x)
// count the number of occurrences of x in p[]
// p is assumed to point to a zero-terminated array of char (or to nothing)
{
if (p == nullptr) return 0;
int count = 0;
for (; *p != 0; ++p)
if (*p == x)
++count;
return count;
}
A pointer can be advanced to the next array element with ++, and the initializer
of a for-statement can be omitted when not needed. This
count_x() assumes the char* is a C-style string — a
pointer to a zero-terminated array of char. The characters of a string literal
are immutable, so the argument is declared const char* to accept
count_x("Hello!").
Since we don't use the initializer part, a while-statement is simpler:
int count_x(const char* p, char x)
// count the number of occurrences of x in p[]
// p is assumed to point to a zero-terminated array of char (or to nothing)
{
if (p == nullptr) return 0;
int count = 0;
while (*p) { // the while-statement executes until its condition becomes false
if (*p == x) ++count;
++p;
}
return count;
}
Tests are shorthand for comparisons. A numeric test (while (*p)) is equivalent to comparing to 0 (while (*p != 0)). A pointer test (if (p)) is equivalent to comparing to nullptr (if (p != nullptr)). In
older code, 0 or NULL was used instead of nullptr;
nullptr eliminates confusion between integers and pointers.
There is no "null reference" — a reference must refer to a valid object, and implementations assume it does. There are obscure ways to violate that rule; don't.
C++ provides the conventional set of selection and looping statements:
if, switch, while, and for. Here is a
function that prompts the user and returns a Boolean:
bool accept()
{
cout << "Do you want to proceed (y or n)?\n"; // write question
char answer = 0; // initialize to a value that will not appear on input
cin >> answer; // read answer
if (answer == 'y') return true;
return false;
}
To match the << output operator ("put to"), the
>> operator ("get from") reads input; cin is the standard
input stream. The type of the right-hand operand determines what input is accepted, and that
operand is the target of the input. Note answer is declared where it is needed —
a declaration can appear anywhere a statement can.
The example improves by handling "n" explicitly with a switch:
bool accept2()
{
cout << "Do you want to proceed (y or n)?\n"; // write question
char answer = 0; // initialize to a value that will not appear on input
cin >> answer; // read answer
switch (answer) {
case 'y':
return true;
case 'n':
return false;
default:
cout << "I'll take that for a no.\n";
return false;
}
}
A switch-statement tests a value against a set of constants. Those constants, the
case-labels, must be distinct. If the value matches none of them,
default is chosen; if there is no default, no action is taken.
You needn't exit a case by returning — often you just continue after the
switch using a break. Consider this primitive parser for a trivial
command video game:
void action()
{
while (true) {
cout << "enter action:\n"; // request action
string act;
cin >> act; // read characters into a string
Point delta {0, 0}; // Point holds an {x, y} pair
for (char ch : act) {
switch (ch) {
case 'u': // up
case 'n': // north
++delta.y;
break;
case 'r': // right
case 'e': // east
++delta.x;
break;
default:
cout << "I freeze!\n";
}
move(current + delta * scale);
update_display();
}
}
}
Like a for-statement, an if-statement can introduce and test a
variable in one go:
void do_something(vector<int>& v)
{
if (auto n = v.size(); n != 0) { // n initialized with v.size(), then tested
// ... we get here if n != 0 ...
}
// ...
}
Here n is defined for use within the if, initialized with
v.size(), and immediately tested by the condition after the semicolon. A name
declared in a condition is in scope on both branches. The purpose is to keep the variable's
scope limited — readability and fewer errors.
The most common test is against 0 (or nullptr). Leave out the explicit
condition:
void do_something(vector<int>& v)
{
if (auto n = v.size()) { // ... we get here if n != 0 ...
// ...
}
}
Prefer this terser form when you can.
C++ offers a direct mapping to hardware. When you use a fundamental operation, the
implementation is what the hardware offers — typically a single machine instruction. Adding
two ints, x + y, executes an integer-add machine instruction.
A C++ implementation sees a machine's memory as a sequence of memory locations into which it can place (typed) objects and address them with pointers. A pointer is represented in memory as a machine address (e.g. the numeric value 103 in a figure). If this sounds like an array, that's because an array is C++'s basic abstraction of "a contiguous sequence of objects in memory."
This simple mapping is the source of C/C++'s raw low-level performance. The basic machine model is based on computer hardware, not on mathematics.
An assignment of a built-in type is a simple machine copy:
int x = 2;
int y = 3;
x = y; // x becomes 3; so we get x == y
The two objects are independent: changing y won't affect x.
Unlike Java, C#, and other languages, but like C, this is true for all types, not
just ints.
If we want different objects to refer to the same (shared) value, we must say so — with pointers:
int x = 2;
int y = 3;
int* p = &x;
int* q = &y; // p != q and *p != *q
p = q; // p becomes &y; now p == q, so (obviously) *p == *q
Again, the assigned-to object gets a copy of the value from the assigned object — here two
independent pointers with the same value, both pointing to y.
A reference and a pointer both refer/point to an object and are both machine addresses in memory, but their rules differ. Assignment to a reference does not change what it refers to — it assigns to the referred object:
int x = 2;
int y = 3;
int& r = x; // r refers to x
int& r2 = y; // r2 refers to y
r = r2; // read through r2, write through r: x becomes 3
To access what a pointer points to you use *; for a reference that is done
implicitly. And after x = y, x == y holds for every built-in type
and every well-designed user-defined type that offers = and ==.
Initialization differs from assignment. For an assignment to work, the assigned-to object must already have a value; initialization's job is to turn an uninitialized piece of memory into a valid object. For almost all types, reading or writing an uninitialized variable is undefined. Consider references:
int x = 7;
int& r {x}; // bind r to x (r refers to x)
r = 7; // assign to whatever r refers to
int& r2; // error: uninitialized reference
r2 = 99; // assign to whatever r2 refers to
Fortunately, an uninitialized reference is illegal — if r2 = 99 were allowed, it
would write 99 to some unspecified memory location, eventually causing bad results or a crash.
Don't let the = form confuse you — int& r = x; is still
initialization; it binds r to x, not a value copy.
The initialization/assignment distinction is crucial for user-defined types such as
string and vector, where an assigned-to object owns a resource
that must eventually be released (§6.3). And because argument passing and function value
return have initialization semantics, this is exactly how pass-by-reference comes about
(§3.4).
Stroustrup closes the chapter with a subset of the C++ Core Guidelines. All 37 items, with the section where each is introduced:
| # | Guideline | § |
|---|---|---|
| 1 | Don't panic! All will become clear in time. | 1.1 |
| 2 | Don't use the built-in features exclusively; prefer libraries such as the ISO C++ standard library. | — |
| 3 |
#include or (preferably) import the libraries needed to simplify
programming.
|
1.2.1 |
| 4 | You don't have to know every detail of C++ to write good programs. | — |
| 5 | Focus on programming techniques, not on language features. | — |
| 6 | The ISO C++ standard is the final word on language definition issues. | 19.1.3 |
| 7 | "Package" meaningful operations as carefully named functions. | 1.3 |
| 8 | A function should perform a single logical operation. | 1.3 |
| 9 | Keep functions short. | 1.3 |
| 10 | Use overloading when functions perform conceptually the same task on different types. | 1.3 |
| 11 |
If a function may have to be evaluated at compile time, declare it constexpr.
|
1.6 |
| 12 | If a function must be evaluated at compile time, declare it consteval. |
1.6 |
| 13 |
If a function may not have side effects, declare it constexpr or
consteval.
|
1.6 |
| 14 | Understand how language primitives map to hardware. | 1.4, 1.7, 1.9 |
| 15 | Use digit separators to make large literals readable. | 1.4 |
| 16 | Avoid complicated expressions. | — |
| 17 | Avoid narrowing conversions. | 1.4.2 |
| 18 | Minimize the scope of a variable. | 1.5, 1.8 |
| 19 | Keep scopes small. | 1.5 |
| 20 | Avoid "magic constants"; use symbolic constants. | 1.6 |
| 21 | Prefer immutable data. | 1.6 |
| 22 | Declare one name (only) per declaration. | — |
| 23 | Keep common and local names short; keep uncommon and nonlocal names longer. | — |
| 24 | Avoid similar-looking names. | — |
| 25 | Avoid ALL_CAPS names. | — |
| 26 | Prefer the { }-initializer syntax for declarations with a named type. |
1.4 |
| 27 | Use auto to avoid repeating type names. |
1.4.2 |
| 28 | Avoid uninitialized variables. | 1.4 |
| 29 | Don't declare a variable until you have a value to initialize it with. | 1.7, 1.8 |
| 30 |
In an if-condition declaration, prefer the implicit test against 0 or
nullptr.
|
1.8 |
| 31 |
Prefer range-for loops over for-loops with an explicit loop
variable.
|
1.7 |
| 32 | Use unsigned for bit manipulation only. |
1.4 |
| 33 | Keep use of pointers simple and straightforward. | 1.7 |
| 34 | Use nullptr rather than 0 or NULL. |
1.7 |
| 35 | Don't say in comments what can be clearly stated in code. | — |
| 36 | State intent in comments. | — |
| 37 | Maintain a consistent indentation style. | — |
1. Which initialization form catches narrowing conversions?
The curly-brace { } list form The traditional = form Only for conversions to char2. After int x=2, y=3; x = y;, what holds?
3. Assignment through a reference r = r2; does what?
4. constexpr versus consteval:
C++ ⇒ compiled, statically typed ; portability ⇒ of source code, not binaries
source → compiler → object files → linker → executable
Two entity kinds ⇒ core language features + standard-library components
std library ⇒ ordinary C++ code → proves language is systems-strong
main() ⇒ exactly one global entry point ; return → exit status (0 ok / nonzero fail ; Windows ignores)
cout << x ⇒ "put to" ; writes x onto stream ; string literal ⇒ quoted text ; \n ⇒ newline
import std; vs #include <iostream> (fallback) ; std:: ⇒ namespace qualification
void ⇒ function returns nothing ; // ⇒ comment (ignored by compiler)
cannot call a function before it is declared
declaration ⇒ return type + name + arg types ; args pass ⇒ initialization
semantics → type-checked + implicit conversion (sqrt(2) ok ;
sqrt("three") error)
function type ⇒ double(const vector<double>&, int) ;
member fn type includes class name
overloading ⇒ same name, different arg types → compiler picks best ; both equal → ambiguous error
no suitable name → likely design problem ; short functions → fewer errors
type ⇒ set of values + operations ; object ⇒ memory holding a value ; value ⇒ bits per type ; variable ⇒ named object
a) bool ⇒ true/false ; b) char ⇒ character (1 byte) ; c) int ⇒ integer ; d) double ⇒ double-precision float ; e) unsigned ⇒ non-negative int (bitwise only)
sizeof ⇒ bytes ; char==1 ; int often 4 ; fixed sizes
→ implementation-defined ; int32_t for guaranteed size
literals ⇒ decimal default ; 0b binary ; 0x hex ;
0 octal ; ' digit separator (eg
3.14159'26535)
+ - * / % ; comparisons == != < > <= >= ; bitwise & | ^ ~ ; logical && || !
= ⇒ assignment ; == ⇒ equality test
mixed types → usual arithmetic conversions at highest operand precision (eg double + int → double)
i = d * i → truncates double to int (narrowing)
compound ops ⇒ += -= *= /= %= ++ --
eval order ⇒ left-to-right for . -> () [] << >> && || ; right-to-left for = ; unspecified elsewhere
a) = ⇒ traditional (C legacy)
b) { } ⇒ general form ; blocks narrowing (eg
int i2 {7.8} error ; int i1 = 7.8 → 7)
narrowing ⇒ lossy conversion (double→int, int→char) ; allowed with = , not { } ; price of C compatibility
auto ⇒ type deduced from initializer ; spell out type when large scope / unclear init / precision matters
don't declare a name until you have a value
a) local ⇒ function/lambda block ; b) class ⇒ member of class ; c) namespace ⇒ namespace member ; d) global ⇒ global namespace
objects destroyed at end of scope ; namespace obj → end of program ; member → with owning object ; new → until delete
unnamed objects ⇒ temporaries + new
const ⇒ "I promise not to change" ; runtime ok ; interfaces
constexpr ⇒ compile-time value ; read-only memory ; performance
consteval ⇒ compile-time only (cannot take runtime args)
constexpr fn ⇒ pure ; no side effects ; loops + locals ok ; non-const args → not a constant expression
const-expr required ⇒ array bounds, case labels, template value args, constexpr declarations
array ⇒ contiguous same-type sequence (hardware abstraction) ; bound starts at 0 ; size = constant expression
* p ⇒ contents of ; & v ⇒ address of
range-for ⇒ for (auto x : v) copy ; for (auto& x : v) mutate
reference ⇒ alias ; no * needed ; cannot rebind ; const ref ⇒ read
without copy (eg sum(const vector<double>&))
declarators ⇒ T a[n] array ; T* p pointer ;
T& r reference ; T f(A) function
nullptr ⇒ one null value shared by all pointer types
test pointer → if (p) ≡ p != nullptr ; test value
→ while (*p) ≡ *p != 0
C-style string ⇒ zero-terminated char array ; use const char* for string
literals
0/NULL older code × → prefer nullptr (no int/pointer confusion)
no null reference ⇒ must refer to a valid object
if / switch / while / for
switch ⇒ test against constant case-labels (distinct) ; default when no match ; break to continue after
condition-init ⇒ if (auto n = v.size(); n != 0) ; name in scope on
both branches
prefer implicit test ⇒ if (auto n = v.size())
fundamental ops ⇒ single machine instruction ; memory ⇒ sequence of locations ; pointer ⇒ machine address
assignment ⇒ value copy ; objects stay independent (all types, like C)
p = q → both pointers now point to same object
reference assignment ⇒ writes through ref, never rebinds
initialization ≠ assignment ; init makes raw memory a valid object ; uninitialized use ⇒ undefined
args + return ⇒ initialization semantics → how pass-by-reference works
Primary source: Stroustrup, B. (2022). A Tour of C++, 3rd ed.,
Chapter 1: "The Basics." Addison-Wesley.
Reference:
Chapter 1 Quick Reference & Glossary —
keep it beside you while you study.
Recommended supplement: cppreference on
fundamental types,
operator precedence, and
functions.
Questions? Ask your agent — your teacher — about anything unclear: a quiz answer, a code example, or how a construct maps to the machine. Follow-ups are expected, not optional.