User Tools

Site Tools


cpp_the_basics

Table of Contents

CPP The Basics

Return to Tour of C++, 3rd Edition by Bjarne Stroustrup, C++ Fundamentals, C++ Bibliography - C++ People, C++, C++ DevOps - C++ SRE - C++ CI/CD, Cloud Native C++ - C++ Microservices - Serverless C++, C++ Security - C++ DevSecOps, Functional C++, C++ Concurrency, C++ Data Science - C++ and Databases, C++ Machine Learning, C++ Courses, C++ Glossary, Awesome C++, C++ GitHub, C++ Topics

Fair Use Source: B0B8S35JWV, (TrCppBS 2022)

“ (TrCppBS 2022)

The Basics

“The first thing we do, let’s kill all the language lawyers.” – Henry VI, Part II

1 CPP The Basics

1.1 CPP Introduction

1.2 CPP Programs

1.3 CPP Functions

1.4 CPP Types, Variables, and Arithmetic

1.5 CPP Scope and Lifetime

1.6 CPP Constants

1.7 CPP Pointers, Arrays, and References

1.8 CPP Tests

1.9 CPP Mapping to Hardware

1.10 CPP Advice

1.1 Introduction

This chapter informally presents the notation of CPP, CPP’s model of memory and computation, and the basic mechanisms for organizing code into a program. These are the language facilities supporting the styles most often seen in C and sometimes called procedural programming.

1.3 Functions

The main way of getting something done in a CPP program is to call a function to do it. Defining a function is the way you specify how an operation is to be done. A function cannot be called unless it has been declared.

A function declaration gives the name of the function, the type of the value returned (if any), and the number and types of the arguments that must be supplied in a call. For example:

Click here to view code image

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

In a function declaration, the return type comes before the name of the function and the argument types come after the name enclosed in parentheses.

The semantics of argument passing are identical to the semantics of initialization (§3.4.1). That is, argument types are checked and implicit argument type conversion takes place when necessary (§1.4). For example:

Click here to view code image

double s2 = sqrt(2); // call sqrt() with the argument double{2}

double s3 = sqrt(“three”); // error: sqrt() requires an argument of type double

The value of such compile-time checking and type conversion should not be underestimated.

A function declaration may contain argument names. This can be a help to the reader of a program, but unless the declaration is also a function definition, the compiler simply ignores such names. For example:

Click here to view code image

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. For example:

Click here to view code image

double get(const vector<double>& vec, int index); // type: double(const vector<double>&,int)

A function can be a member of a class (§2.3, §5.2.1). For such a member function, the name of its class is also part of the function type. For example:

Click here to view code image

char& String::operator[](int index); // type: char& String::(int)

We want our code to be comprehensible because that is the first step on the way to maintainability. The first step to comprehensibility is to break computational tasks into meaningful chunks (represented as functions and classes) and name those. Such functions then provide the basic vocabulary of computation, just as the types (built-in and user-defined) provide the basic vocabulary of data. The CPP standard algorithms (e.g., find, sort, and iota) provide a good start (Chapter 13). Next, we can compose functions representing common or specialized tasks into larger computations.

The number of errors in code correlates strongly with the amount of code and the complexity of the code. Both problems can be addressed by using more and shorter functions. Using a function to do a specific task often saves us from writing a specific piece of code in the middle of other code; making it a function forces us to name the activity and document its dependencies. If we cannot find a suitable name, there is a high probability that we have a design problem.

If two functions are defined with the same name, but with different argument types, the compiler will choose the most appropriate function to invoke for each call. For example:

Click here to view code image

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 alternative functions could be called, but neither is better than the other, the call is deemed ambiguous and the compiler gives an error. For example:

Click here to view code image

void print(int,double);

void print(double,int);

void user2()

{

print(0,0); // error: ambiguous

}

Defining multiple functions with the same name is known as function overloading and is one of the essential parts of generic programming (§8.2). When a function is overloaded, each function of the same name should implement the same semantics. The print() functions are an example of this; each print() prints its argument.

1.4 Types, Variables, and Arithmetic

Every name and every expression has a type that determines the operations that may be performed on it. For example, the declaration

int inch;

specifies that inch is of type int; that is, inch is an integer variable.

A declaration is a statement that introduces an entity into the program and specifies its type:

A type defines a set of possible values and a set of operations (for an object).

An object is some memory that holds a value of some type.

A value is a set of bits interpreted according to a type.

A variable is a named object.

CPP offers a small zoo of fundamental types, but since I’m not a zoologist, I will not list them all. You can find them all in reference sources, such as the [Cppreference] on the Web. Examples are:

Click here to view code image

bool // Boolean, possible values are true and false

char // character, for example, 'a', 'z', and 9

int // integer, for example, -273, 42, and 1066

double // double-precision floating-point number, for example, -273.15, 3.14, and 6.626e-34

unsigned // non-negative integer, for example, 0, 1, and 999 (use for bitwise logical operations)

Each fundamental type corresponds directly to hardware facilities and has a fixed size that determines the range of values that can be stored in it:

Images

A char variable is of the natural size to hold a character on a given machine (typically an 8-bit byte), and the sizes of other types are multiples of the size of a char. The size of a type is implementation-defined (i.e., it can vary among different machines) and can be obtained by the sizeof operator; for example, sizeof(char) equals 1 and sizeof(int) is often 4. When we want a type of a specific size, we use a standard-library type aliase, such as int32_t (§17.8).

Numbers can be floating-point or integers.

Floating-point literals are recognized by a decimal point (e.g., 3.14) or by an exponent (e.g., 314e-2).

Integer literals are by default decimal (e.g., 42 means forty-two). A 0b prefix indicates a binary (base 2) integer literal (e.g., 0b10101010). A 0x prefix indicates a hexadecimal (base 16) integer literal (e.g., 0xBAD12CE3). A 0 prefix indicates an octal (base 8) integer literal (e.g., 0334).

To make long literals more readable for humans, we can use a single quote (’) as a digit separator. For example, ð is about 3.14159’26535’89793’23846’26433’83279’50288 or if you prefer hexadecimal notation 0x3.243F’6A88’85A3’08D3.

1.4.1 Arithmetic

The arithmetic operators can be used for appropriate combinations of the fundamental types:

Click here to view code image

x+y // plus

+x // unary plus

x-y // minus

-x // unary minus

x*y // multiply

x/y // divide

x%y // remainder (modulus) for integers

So can the comparison operators:

Click here to view code image

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

Furthermore, logical operators are provided:

Click here to view code image

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)

A bitwise logical operator yields a result of the operand type for which the operation has been performed on each bit. The logical operators && and || simply return true or false depending on the values of their operands.

In assignments and in arithmetic operations, CPP performs all meaningful conversions between the basic types so that they can be mixed freely:

Click here to view code image

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

}

The conversions used in expressions are called the usual arithmetic conversions and aim to ensure that expressions are computed at the highest precision of their operands. For example, an addition of a double and an int is calculated using double-precision floating-point arithmetic.

Note that = is the assignment operator and == tests equality.

In addition to the conventional arithmetic and logical operators, CPP offers more specific operations for modifying a variable:

Click here to view code image

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

These operators are concise, convenient, and very frequently used.

The order of evaluation is left-to right for x.y, x→y, x(y), x[y], x«y, x»y, x&&y, and x||y. For assignments (e.g., x+=y), the order is right-to-left. For historical reasons realated to optimization, the order of evaluation of other expressions (e.g., f(x)+g(y)) and of function arguments (e.g., h(f(x),g(y))) is unfortunately unspecified.

1.4.2 Initialization

Before an object can be used, it must be given a value. CPP offers a variety of notations for expressing initialization, such as the = used above, and a universal form based on curly-brace-delimited initializer lists:

Click here to view code image

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

The = form is traditional and dates back to C, but if in doubt, use the general {}-list form. If nothing else, it saves you from conversions that lose information:

Click here to view code image

int i1 = 7.8; // i1 becomes 7 (surprise?)

int i2 {7.8}; // error: floating-point to integer conversion

Unfortunately, conversions that lose information, narrowing conversions, such as double to int and int to char, are allowed and implicitly applied when you use = (but not when you use {}). The problems caused by implicit narrowing conversions are a price paid for C compatibility (§19.3).

A constant (§1.6) 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, and Orc_warrior) can be defined to be implicitly initialized (§5.2.1).

When defining a variable, you don’t need to state its type explicitly when the type can be deduced from the initializer:

Click here to view code image

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

With auto, we tend to use the = because there is no potentially troublesome type conversion involved, but if you prefer to use {} initialization consistently, you can do that instead.

We use auto where we don’t have a specific reason to mention the type explicitly. “Specific reasons” include:

The definition is in a large scope where we want to make the type clearly visible to readers of our code.

The type of the initializer isn’t obvious.

We want to be explicit about a variable’s range or precision (e.g., double rather than float).

Using auto, we avoid redundancy and writing long type names. This is especially important in generic programming where the exact type of an object can be hard for the programmer to know and the type names can be quite long (§13.2).

1.5 Scope and Lifetime

A declaration introduces its name into a scope:

Local scope: A name declared in a function (§1.3) or lambda (§7.3.2) is called a local name. Its scope extends from its point of declaration to the end of the block in which its declaration occurs. A block is delimited by a { } pair. Function argument names are considered local names.

Class scope: A name is called a member name (or a class member name) if it is defined in a class (§2.2, §2.3, Chapter 5), outside any function (§1.3), lambda (§7.3.2), or enum class (§2.4). Its scope extends from the opening { of its enclosing declaration to the matching }.

Namespace scope: A name is called a namespace member name if it is defined in a namespace (§3.3) outside any function, lambda (§7.3.2), class (§2.2, §2.3, Chapter 5), or enum class (§2.4). Its scope extends from the point of declaration to the end of its namespace.

A name not declared inside any other construct is called a global name and is said to be in the global namespace.

In addition, we can have objects without names, such as temporaries and objects created using new (§5.2.2). For example:

Click here to view code image

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 (initialized) before it is used and will be destroyed at the end of its scope. For a namespace object the point of destruction is the end of the program. For a member, the point of destruction is determined by the point of destruction of the object of which it is a member. An object created by new “lives” until destroyed by delete (§5.2.2).

1.6 Constants

CPP supports two notions of immutability (an object with an unchangeable state):

const: meaning roughly “I promise not to change this value.” This is used primarily to specify interfaces so that data can be passed to functions using pointers and references without fear of it being modified. The compiler enforces the promise made by const. The value of a const may be calculated at run time.

constexpr: meaning roughly “to be evaluated at compile time.” This is used primarily to specify constants, to allow placement of data in read-only memory (where it is unlikely to be corrupted), and for performance. The value of a constexpr must be calculated by the compiler.

For example:

Click here to view code image

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 (§1.7)

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, that is, in an expression that will be evaluated by the compiler, it must be defined constexpr or consteval. For example:

Click here to view code image

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 can be used for non-constant arguments, but when that is done the result is not a constant expression. We allow a constexpr function to be called with non-constant-expression arguments in contexts that do not require constant expressions. That way, we don’t have to define essentially the same function twice: once for constant expressions and once for variables. When we want a function to be used only for evaluation at compile time, we declare it consteval rather than constexpr. For example:

Click here to view code image

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

Functions declared constexpr or consteval are CPP’s version of the notion 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, but they can have loops and use their own local variables. For example:

Click here to view code image

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 (§1.7.1)

res *= x;

++i;

}

return res;

}

In a few places, constant expressions are required by language rules (e.g., array bounds (§1.7), case labels (§1.8), template value arguments (§7.2), and constants declared using constexpr). In other cases, compile-time evaluation is important for performance. Independent of performance issues, the notion of immutability (an object with an unchangeable state) is an important design concern.

1.7 Pointers, Arrays, and References

The most fundamental collection of data is a contiguously allocated sequence of elements of the same type, called an array. This is basically what the hardware offers. An array of elements of type char can be declared like this:

Click here to view code image

char v[6]; // array of 6 characters

Similarly, a pointer can be declared like this:

Click here to view code image

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 (§1.6). A pointer variable can hold the address of an object of the appropriate type:

Click here to view code image

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.” We can represent that graphically like this:

Images

Consider printing the elements of an array:

Click here to view code image

void print()

{

int v1[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

for (auto i=0; i!=10; ++i) // print elements

cout « v[i] « '\n';

// …

}

This for-statement can be read as “set i to zero; while i is not 10, print the ith element and increment i.” CPP also offers a simpler for-statement, called a range-for-statement, for loops that traverse a sequence in the simplest way:

Click here to view code image

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';

// …

}

The first range-for-statement can be read as “for every element of v, from the first to the last, place a copy in x and print it.” Note that we don’t have to specify an array bound when we initialize it with a list. The range-for-statement can be used for any sequence of elements (§13.1).

If we didn’t want to copy the values from v into the variable x, but rather just have x refer to an element, we could write:

Click here to view code image

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 similar to a pointer, except that you don’t need to use a prefix * to access the value referred to by the reference. Also, a reference cannot be made to refer to a different object after its initialization.

References are particularly useful for specifying function arguments. For example:

Click here to view code image

void sort(vector<double>& v); // sort v (v is a vector of doubles)

By using a reference, we ensure that for a call sort(my_vec), we do not copy my_vec. Therefore, it really is my_vec that is sorted and not a copy of it.

When we don’t want to modify an argument but still don’t want the cost of copying, we use a const reference (§1.6); that is, a reference to a const. For example:

Click here to view code image

double sum(const vector<double>&)

Functions taking const references are very common.

When used in declarations, operators (such as &, *, and [ ]) are called declarator operators:

Click here to view code image

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 is a function taking an argument of type A returning a result of type T

1.7.1 The Null Pointer

We try to ensure that a pointer always points to an object so that dereferencing it is valid. When we don’t have an object to point to or if we need to represent the notion of “no object available” (e.g., for an end of a list), we give the pointer the value nullptr (“the null pointer”). There is only one nullptr shared by all pointer types:

Click here to view code image

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:

Click here to view code image

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;

}

We can advance a pointer to point to the next element of an array using ++ and also leave out the initializer in a for-statement if we don’t need it.

The definition of count_x() assumes that the char* is a C-style string, that is, that the pointer points to a zero-terminated array of char. The characters in a string literal are immutable, so to handle count_x(“Hello!”), I declared count_x() a const char* argument.

In older code, 0 or NULL is typically used instead of nullptr. However, using nullptr eliminates potential confusion between integers (such as 0 or NULL) and pointers (such as nullptr).

In the count_x() example, we are not using the initializer part of the for-statement, so we can use the simpler while-statement:

Click here to view code image

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) {

if (*p==x)

++count;

++p;

}

return count;

}

The while-statement executes until its condition becomes false.

A test of a numeric value (e.g., while (*p) in count_x()) is equivalent to comparing the value to 0 (e.g., while (*p!=0)). A test of a pointer value (e.g., if (p)) is equivalent to comparing the value to nullptr (e.g., if (p!=nullptr)).

There is no “null reference.” A reference must refer to a valid object (and implementations assume that it does). There are obscure and clever ways to violate that rule; don’t do that.

1.8 Tests

CPP provides a conventional set of statements for expressing selection and looping, such as if-statements, switch-statements, while-loops, and for-loops. For example, here is a simple function that prompts the user and returns a Boolean indicating the response:

Click here to view code image

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”) is used for input; cin is the standard input stream (Chapter 11). The type of the right-hand operand of » determines what input is accepted, and its right-hand operand is the target of the input operation. The \n character at the end of the output string represents a newline (§1.2.1).

Note that the definition of answer appears where it is needed (and not before that). A declaration can appear anywhere a statement can.

The example could be improved by taking an n (for “no”) answer into account:

Click here to view code image

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, called case-labels, must be distinct, and if the value tested does not match any of them, the default is chosen. If the value doesn’t match any case-label and no default is provided, no action is taken.

We don’t have to exit a case by returning from the function that contains its switch-statement. Often, we just want to continue execution with the statement following the switch-statement. We can do that using a break statement. As an example, consider an overly clever, yet primitive, parser for a trivial command video game:

Click here to view code image

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;

// … more actions …

default:

cout « “I freeze!\n”;

}

move(current+delta*scale);

update_display();

}

}

}

Like a for-statement (§1.7), an if-statement can introduce a variable and test it. For example:

Click here to view code image

void do_something(vector<int>& v)

{

if (auto n = v.size(); n!=0) {

// … we get here if n!=0 …

}

// …

}

Here, the integer n is defined for use within the if-statement, initialized with v.size(), and immediately tested by the n!=0 condition after the semicolon. A name declared in a condition is in scope on both branches of the if-statement.

As with the for-statement, the purpose of declaring a name in the condition of an if-statement is to keep the scope of the variable limited to improve readability and minimize errors.

The most common case is testing a variable against 0 (or the nullptr). To do that, simply leave out the explicit mention of the condition. For example:

Click here to view code image

void do_something(vector<int>& v)

{

if (auto n = v.size()) {

// … we get here if n!=0 …

}

// …

}

Prefer to use this terser and simpler form when you can.

1.9 Mapping to Hardware

CPP offers a direct mapping to hardware. When you use one of the fundamental operations, the implementation is what the hardware offers, typically a single machine operation. For example, adding two ints, x+y executes an integer add machine instruction.

A CPP implementation sees a machine’s memory as a sequence of memory locations into which it can place (typed) objects and address them using pointers:

Images

A pointer is represented in memory as a machine address, so the numeric value of p in this figure would be 103. If this looks much like an array (§1.7), that’s because an array is CPP’s basic abstraction of “a contiguous sequence of objects in memory.”

The simple mapping of fundamental language constructs to hardware is crucial for the raw low-level performance for which C and CPP have been famous for decades. The basic machine model of C and CPP is based on computer hardware, rather than some form of mathematics.

1.9.1 Assignment

An assignment of a built-in type is a simple machine copy operation. Consider:

Click here to view code image

int x = 2;

int y = 3;

x = y; // x becomes 3; so we get x==y

This is obvious. We can represent that graphically like this:

Images

The two objects are independent. We can change the value of y without affecting the value of x. For example, x=99 will not change the value of y. Unlike Java, C#, and other languages, but like C, that is true for all types, not just for ints.

If we want different objects to refer to the same (shared) value, we must say so. For example:

Click here to view code image

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

We can represent that graphically like this:

Images

I arbitrarily chose 88 and 92 as the addresses of the ints. Again, we can see that the assigned-to object gets the value from the assigned object, yielding two independent objects (here, pointers), with the same value. That is, p=q gives p==q. After p=q, both pointers point to y.

A reference and a pointer both refer/point to an object and both are represented in memory as a machine address. However, the language rules for using them differ. Assignment to a reference does not change what the reference refers to but assigns to the referenced object:

Click here to view code image

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

We can represent that graphically like this:

Images

To access the value pointed to by a pointer, you use *; that is implicitly done for a reference.

After x=y, we have x==y for every built-in type and well-designed user-defined type (Chapter 2) that offers = (assignment) and == (equality comparison).

1.9.2 Initialization

Initialization differs from assignment. In general, for an assignment to work correctly, the assigned-to object must have a value. On the other hand, the task of initialization is to make an uninitialized piece of memory into a valid object. For almost all types, the effect of reading from or writing to an uninitialized variable is undefined. Consider references:

Click here to view code image

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, we cannot have an uninitialized reference; if we could, then that r2=99 would assign 99 to some unspecified memory location; the result would eventually lead to bad results or a crash.

You can use = to initialize a reference but please don’t let that confuse you. For example:

Click here to view code image

int& r = x; // bind r to x (r refers to x)

This is still initialization and binds r to x, rather than any form of value copy.

The distinction between initialization and assignment is also crucial to many user-defined types, such as string and vector, where an assigned-to object owns a resource that needs to eventually be released (§6.3).

The basic semantics of argument passing and function value return are that of initialization (§3.4). For example, that’s how we get pass-by-reference (§3.4.1).

1.10 Advice

The advice here is a subset of the CPP Core Guidelines [Stroustrup,2015]. References to guidelines look like this [CG: ES.23], meaning the 23rd rule in the Expressions and Statement section. Generally, a core guideline offers further rationale and examples.

[1] Don’t panic! All will become clear in time; §1.1; [CG: In.0].

[2] Don’t use the built-in features exclusively. Many fundamental (built-in) features are usually best used indirectly through libraries, such as the ISO CPP standard library (Chapters 9–18); [CG: P.13].

[3] #include or (preferably) import the libraries needed to simplify programming; §1.2.1.

[4] You don’t have to know every detail of CPP to write good programs.

[5] Focus on programming techniques, not on language features.

[6] The ISO CPP standard is the final word on language definition issues; §19.1.3; [CG: P.2].

[7] “Package” meaningful operations as carefully named functions; §1.3; [CG: F.1].

[8] A function should perform a single logical operation; §1.3 [CG: F.2].

[9] Keep functions short; §1.3; [CG: F.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; [CG: F.4].

[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; [CG: F.4].

[14] Understand how language primitives map to hardware; §1.4, §1.7, §1.9, §2.3, §5.2.2, §5.4.

[15] Use digit separators to make large literals readable; §1.4; [CG: NL.11].

[16] Avoid complicated expressions; [CG: ES.40].

[17] Avoid narrowing conversions; §1.4.2; [CG: ES.46].

[18] Minimize the scope of a variable; §1.5, §1.8.

[19] Keep scopes small; §1.5; [CG: ES.5].

[20] Avoid “magic constants”; use symbolic constants; §1.6; [CG: ES.45].

[21] Prefer immutable data; §1.6; [CG: P.10].

[22] Declare one name (only) per declaration; [CG: ES.10].

[23] Keep common and local names short; keep uncommon and nonlocal names longer; [CG: ES.7].

[24] Avoid similar-looking names; [CG: ES.8].

[25] Avoid ALL_CAPS names; [CG: ES.9].

[26] Prefer the {}-initializer syntax for declarations with a named type; §1.4; [CG: ES.23].

[27] Use auto to avoid repeating type names; §1.4.2; [CG: ES.11].

[28] Avoid uninitialized variables; §1.4; [CG: ES.20].

[29] Don’t declare a variable until you have a value to initialize it with; §1.7, §1.8; [CG: ES.21].

[30] When declaring a variable in the condition of an if-statement, prefer the version with 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; [CG: ES.101] [CG: ES.106].

[33] Keep use of pointers simple and straightforward; §1.7; [CG: ES.42].

[34] Use nullptr rather than 0 or NULL; §1.7; [CG: ES.47].

[35] Don’t say in comments what can be clearly stated in code; [CG: NL.1].

[36] State intent in comments; [CG: NL.2].

[37] Maintain a consistent indentation style; [CG: NL.4].

Fair Use Sources

C++: C++ Fundamentals, C++ Inventor - C++ Language Designer: Bjarne Stroustrup in 1985; C++ Keywords, C++ Built-In Data Types, C++ Data Structures (CPP Containers) - C++ Algorithms, C++ Syntax, C++ OOP - C++ Design Patterns, Clean C++ - C++ Style Guide, C++ Best Practices ( C++ Core Guidelines (CG)) - C++ BDD, C++ Standards ( C++ 23, C++ 20, C++ 17, C++ 14, C++ 11, C++ 03, C++ 98), Bjarne Stroustrup's C++ Glossary, CppReference.com, CPlusPlus.com, ISOcpp.org, C++ Compilers (Compiler Explorer, MinGW), C++ IDEs, C++ Development Tools, C++ Linter, C++ Debugging, C++ Modules ( C++20), C++ Packages, C++ Package Manager ( Conan - the C/C++ Package Manager), C++ Standard Library, C++ Libraries, C++ Frameworks, C++ DevOps - C++ SRE, C++ CI/CD ( C++ Build Pipeline), C++ Data Science - C++ DataOps, C++ Machine Learning, C++ Deep Learning, Functional C++, C++ Concurrency, C++ History, C++ Topics, C++ Bibliography, Manning C++ Series, C++ Courses, CppCon, C++ Research, C++ GitHub, Written in C++, C++ Popularity, C++ Awesome , C++ Versions. (navbar_cplusplus – see also navbar_cpp_containers, navbar_cppcon, navbar_cpp_core_guidelines, navbar_cpp23, navbar_cpp20, navbar_cpp17, navbar_cpp14, navbar_cpp11)


© 1994 - 2024 Cloud Monk Losang Jinpa or Fair Use. Disclaimers

SYI LU SENG E MU CHYWE YE. NAN. WEI LA YE. WEI LA YE. SA WA HE.


cpp_the_basics.txt · Last modified: 2024/04/28 03:46 by 127.0.0.1