Cpp Primer Chapter 2
Posted
on
in
book
• 3008 words
• 15 minute read
Tags:
cpp, cpp primer
C++ language specifics
Python check types at run time, whereas C++ is statically typed language — type-check is done at compile time. As a consequence, the compiler must know the type of every name used in the program.
Primitive Buit-in Types
C++ defines a set of primitive types that include the arithmetic types and a special type named void. The arithmetic types represent characters, integers, boolean values, and floating-point numbers. The void type has no associated values and can be used in only a few circumstances, most commonly as the return type for functions that do not return a value.
Arithmetic Types
- bool: boolean (NA)
- char: character (8 bits)
- wchar_t: wide character (16 bits)
- char16_t: Unicode character (16 bits)
- char32_t: Unicode character (32 bits)
- short: short integer (16 bits)
- int: integer (16 bits)
- long: long integer (32 bits)
- long long: long integer (64 bits)
- float: single-precision floating-point (6 significant digits)
- double: double-precision floating-point (10 significant digits)
- long double: extended-precision floating-point (10 significant digits)
Typically, floats
are represented in one word (32 bits), doubles
in two words (64 bits), and long double
in either three or four words (96 or 128 bits). The float
and double
types typically yield about 7 and 16 significant digits, respectively.
Except for bool and the extended character types, the integral types may be signed or unsigned. A signed type represents negative or positive numbers (including zero); an unsigned type represents only values greater than or equal to zero .The type unsigned int
may be abbreviated as unsigned
.
A short note to use char
Do not use plain char
or bool
in arithmetic expression. Use them only to hold characters or truth values. Computations using char
are especially problematic because char
is signed
on some machine and unsigned
on others. If you need a tiny integer, explicitly specify either signed char
or unsigned char
.
Type conversion
bool b = 42; // b is true: anything not 0 is true
int i = b; // i has value 1
i = 3.14; // i has value 3: cast down
double pi = i; // pi has value 3.0
unsigned char c = -1; // assuming 8-bit chars, c has value 255: there is no negative for unsigned
signed char c2 = 256; // assuming 8-bit chars, the value of c2 is undefined
Why unsigned char c = -1
will get 255?
If we assign an out-of-range value to an object of unsigned type, the result is the remainder of the value modulo the number of values the target type can hold. For example, an 8-bit unsigned char can hold values from 0 through 255, inclusive. If we assign a value outside this range, the compiler assigns the remainder of that value modulo 256. Therefore, assigning –1 to an 8-bit unsigned char gives that object the value 255. Note that modulo mentioned here is logical, arithmetic modulo operation:
a mod n is a/n = r (remainder)
therefore, a mod n = a - r * n
so, -1 mod 256 is
-1/256 = 1
=> -1 - (-1) * 256
=> 255
Reference: https://www.calculators.org/math/modulo.php
Don’t use unsigned
in loop
for (int i = 10; i >= 0; --i)
std::cout << i << std::endl;
We might think we could rewrite this loop using an unsigned. After all, we don’t plan to print negative numbers. However, this simple change in type means that our loop will never terminate:
for (unsigned u = 10; u >= 0; --u)
std::cout << u << std::endl;
u
can never be less than 0; the condition will always succeed. Consider what happens when u
is 0. On that iteration, we’ll print 0 and then execute the expression in the for loop. That expression, —u
, subtracts 1 from u
. That result, -1, won’t fit in an unsigned value. As with any other out-of-range value, -1 will be transformed to an unsigned value. Assuming 32-bit ints, the result of —u
, when u
is 0, is 4294967295.
Literal
If we write what appears to be a negative decimal literal, for example, -42
, the minus sign is not part of the literal. The minus sign is an operator that negates the value of its (literal) operand.
The type of a string literal is array of constant chars:
'a'
: character literal"hello world"
: string literal
Compiler will append a null character ('\0') at the end of every string literal, so the length is always string literal size + 1.
Specify the Type of a Literal
We can override the default type of an integer, floating-point, or character literal by supplying a suffix or prefix as listed below:
L'a' // wide character literal, type is wchar_t
u8"hi!" // utf-8 string literal (utf-8 encodes a Unicode character in 8 bits)
42ULL // unsigned integer literal, type is unsigned long long
1E-3F // single-precision floating-point literal, type is float
3.14159L // extended-precision floating-point literal, type is long double
Default Initialization
Uninitialized objects of built-in type defined inside a function body have undefined value. However, the library string class says that if we do not supply an initializer, then the resulting string is the empty string.
Variable
int units_sold = {0};
int units_sold{0};
The use of curly braces to initialize a variable is called List Initialization. The compiler will not let us list initialize variables of built-in type if the initializer might lead to the loss of information:
long double ld = 3.1415;
int a{ld}, b = {ld}; // error: narrowing conversion required
int c(ld), d = ld; // ok: not using list init but the value will be truncated
Reference
When we define a reference, instead of copying the initializer’s value, we bind the reference to its initializer.
int ival = 1024;
int &refVal = ival;
cout << refVal << endl; // 1024
refVal = 2;
cout << ival << endl; // 2
When we assign to a reference, we are assigning to the object to which the reference is bound. When we fetch the value of a reference, we are really fetching the value of the object to which the reference is bound. The type of a reference and the object to which the reference refers must match exactly
int &refVal4 = 10; // error: initializer must be an object
double dval = 3.14;
int &refVal5=dval;// error: initializer must be an int object
int &refVal6; // error: a reference must be initialized
Pointers
A pointer holds the address of another object. We get the address of an object by using the address-of operator (the & operator):
int ival = 42;
int *p = &ival; // p holds the address of ival; p is a pointer to ival
Because references are not objects, they don’t have addresses. Hence, we may not define a pointer to a reference.
Using a Pointer to Access an Object
When a pointer points to an object, we can use the dereference operator (the * operator) to access that object:
int ival = 42;
int *p = &ival; // p holds the address of ival; p is a pointer to ival
int *p2 = ival; // illegal to assign an int variable to a pointer
cout << *p; // * yields the object to which p points; prints 42
Dereferencing a pointer yields the object to which the pointer points. We can assign to that object by assigning to the result of the dereference:
*p = 0; // * yields the object; we assign a new value to ival through p
cout << *p;// prints 0
p = &ival; // it will pointing to ival's address
*
can have multiple meanings:
- After type:
int *p
meansp
is a pointer, and it will point to an address - Everything else:
p = &ival
meansp
will point to an object with a referece butp
is already declared
Null Pointers
Older programs sometimes use a preprocessor variable named NULL, which the cstdlib
header defines as 0. Modern C++ programs generally should avoid using NULL and use nullptr instead.
void*
Pointers
The type void* is a special pointer type that can hold the address of any object. Like any other pointer, a void* pointer holds an address, but the type of the object at that address is unknown:
double obj = 3.14, *pd = &obj;
// ok: void* can hold the address value of any data pointer type
void *pv = &obj; // obj can be an object of any type
pv = pd; // pv can hold a pointer to any type
Difference between Pointers and References
key difference:
- a reference is another name of an already existing object. a pointer is an object in its own right.
- Once initialized, a reference remains bound to its initial object. There is no way to rebind a reference to refer to a different object. a pointer can be assigned and copied.
- a reference always get the object to which the reference was initially bound. a single pointer can point to several different objects over its lifetime.
- a reference must be initialized. a pointer need not be initialized at the time it is defined.
const
Qualifier
By default, const
Objects are local to a file. Sometimes we have a const variable that we want to share across multiple files but whose initializer is not a constant expression. In this case, we don’t want the compiler to generate a separate variable in each file. Instead, we want the const object to behave like other (nonconst) variables. We want to define the const in one file, and declare it in the other files that use that object.
To define a single instance of a const variable, we use the keyword extern on both its definition and declaration(s):
// file_1.cc defines and initializes a const that is accessible to other files
extern const int bufSize = fcn();
// file_1.h
extern const int bufSize;// same bufSize as defined in file_1.cc
To share a const
object among multiple files, you must define the variable as extern
.
const
and Pointers
Like a reference to const, a pointer to const may not be used to change the object to which the pointer points. We may store the address of a const object only in a pointer to const:
const double pi = 3.14; // pi is const; its value may not be changed
double *ptr = π // error:ptr is a plain pointer
const double *cptr = π // ok:cptr may point to a double that is const
*cptr = 42; // error: cannot assign to *cptr
const
pointer
For example:
int errNumb = 0;
int *const curErr = &errNumb; // curErr will always point to errNumb
const double pi = 3.14159;
const double *const pip = π // pip is a const pointer to a const object
In this case, the symbol closest to curErr
is const, which means that curErr
itself will be a const object. The next symbol in the declarator is *, which means that curErr
is a const pointer. Finally, the base type of the declaration completes the type of curErr
, which is a const pointer to an object of type int. Similarly, pip is a const pointer to an object of type const double.
Neither the value of the object addressed by pip
nor the address stored in pip
can be changed. On the other hand, curErr
addresses a plain, nonconst int. You cannot make it point to something else.
Use const pointer to reference the same address in hardware: https://stackoverflow.com/a/219956
We use the term top-level const to indicate that the pointer itself is a const. When a pointer can point to a const object, we refer to that const as a low-level const. In general, a pointer is usually a lower-level const, and the others are top-level const.
int i = 0;
int *const p1 = &i; // we can't change the value of p1; const is top-level
const int c1 = 42; // we cannot change ci; const is top-level
const int *p2 = &ci; // we can change p2; const is low-level
const int *const p3 = p2; // right-most const is top-level, left-most is not
const int &r = ci; // const in reference types is always low-level
constexpr
and Constant Expression
Only literal types can be declared by constexpr
: arithmetic, reference, and pointer types are literal types. Variables defined inside a function are not stored at a fixed adress, so we cannot use a constexpr
pointer to point to such variables. On the other hand, the address of an object defined outside of any function is a constant expression, and so may be used to initialize a constexpr pointer.
Types
There are two ways to define type aliasing:
- via
typedef
:typedef double wages;
— wages is a synonym fordouble
- via alias declaration:
using SI = Sales_item;
whereSale_item
is a type, and we useusing
keyword.
Difference between decltype
and auto
decltype
is the only context in which a variable defined as a reference is not treated as a synonym for the object to which it refers.decltype
needs to deal with parentheses:decltype((i)) d;
error:d
isint&
and must be initialized;decltype(i) e;
ok:e
is an (uninitialized)int
Defining Our Own Data Structures
Defining our Sales_data
class:
// Sale_data.h
struct Sales_data {
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
Don’t forget the semicolumn at the end of the
struct
definition
Revisit chapter 1 and we know that each input contains:
0-201-78345-X 3 20.00
0-201-78345-X 2 25.00
An ISBN, the count of how many books were sold, and the price at which each book was sold.
Read and add two books
Completed code (exercise 2.41):
Read single book
#include <iostream>
#include <string>
struct Sale_data {
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
int main() {
Sale_data book;
double price;
std::cin >> book.bookNo >> book.units_sold >> price;
book.revenue = book.units_sold * price;
std::cout << book.bookNo << " " << book.units_sold << " " << book.revenue
<< " " << price;
return 0;
}
Read two books and add them together
#include <iostream>
#include <string>
struct Sale_data {
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
int main() {
Sale_data book1, book2;
double price1, price2;
std::cin >> book1.bookNo >> book1.units_sold >> price1;
std::cin >> book2.bookNo >> book2.units_sold >> price2;
book1.revenue = book1.units_sold * price1;
book2.revenue = book2.units_sold * price2;
if (book1.bookNo == book2.bookNo) {
unsigned totalCnt = book1.units_sold + book2.units_sold;
double totalRevenue = book1.revenue + book2.revenue;
std::cout << book1.bookNo << " " << totalCnt << " " << totalRevenue << " ";
if (totalCnt != 0)
std::cout << totalRevenue / totalCnt << std::endl;
else
std::cout << "(no sales)" << std::endl;
return 0;
} else {
std::cerr << "Data must refer to same ISBN" << std::endl;
return -1; // indicate failure
}
}
Read multiple books and calculate the revenue
#include <iostream>
#include <string>
struct Sale_data {
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
int main() {
Sale_data total;
double totalPrice;
if (std::cin >> total.bookNo >> total.units_sold >> totalPrice) {
total.revenue = total.units_sold * totalPrice;
Sale_data trans;
double transPrice;
while (std::cin >> trans.bookNo >> trans.units_sold >> transPrice) {
trans.revenue = trans.units_sold * transPrice;
if (total.bookNo == trans.bookNo) { // same book
total.units_sold += trans.units_sold;
total.revenue += trans.revenue;
} else { // start calculating new book
std::cout << total.bookNo << " " << total.units_sold << " "
<< total.revenue << " ";
if (total.units_sold != 0) // calculate the average revenue
std::cout << total.revenue / total.units_sold << std::endl;
else
std::cout << "(no sales)" << std::endl;
total.bookNo = trans.bookNo;
total.units_sold = trans.units_sold;
total.revenue = trans.revenue;
}
}
std::cout << total.bookNo << " " << total.units_sold << " " << total.revenue
<< " ";
if (total.units_sold != 0)
std::cout << total.revenue / total.units_sold << std::endl;
else
std::cout << "(no sales)" << std::endl;
return 0;
} else {
std::cerr << "No data?!" << std::endl;
return -1; // indicate failure
}
}
This program must ensure the input is sorted by bookNo
.
Header files
Some note for writing header files:
- The program that uses the header file will use the already included library (string.h for example).
- Whenever a header is updated, the source files that use that header must be recompiled to get the new or changed declarations.
Terms
Escape sequence: some characters, such as backspace or control characters, have no visible image
- newline: \n
- vertical tab: \v
- baskslash: \
- carriage return: \r
- horizontal tab: \t
- backspace: \b
- question mark: ?
- formfeed: \f
- alert (bell): \a
- double quote: \"
- single quote: \'
Separate compilation: To allow programs to be written in logical parts, C++ supports what is commonly known as separate compilation. Separate compilation lets us split our programs into several files, each of which can be compiled independently.
A variable declaration specifies the type and name of a variable. A variable definition is a declaration. In addition to specifying the name and type, a definition also allocates storage and may provide the variable with an initial value.
To use a variable in more than one file requires declarations that are separate from the variable’s definition. To use the same variable in multiple files, we must define that variable in one—and only one—file. Other files that use that variable must declare—but not define—that variable.
extern int ix = 1024; // definition int iy; // definition extern int iz; // declaration
Preprocessor: The preprocessor — which C++ inherits from C — is a program that runs before the compiler and changes the source of text of our programs. C++ programs also use the preprocessor to define header guards. Preprocessor variables have one of two possible states: defined or not defined. The #define directive takes a name and defines that name as a preprocessor variable. There are two other directives that test whether a given preprocessor variable has or has not been defined: #ifdef is true if the variable has been defined, and #ifndef is true if the variable has not been defined. If the test is true, then everything following the #ifdef or #ifndef is processed up to the matching #endif.