基础部分
- A stream is a sequence of characters read from or written to an IO device.
- To handle input, we use an object of type istream named cin; for output, ostream named cout. Also, cerr, clog.
- All the names defined by standard library are in the std namespace.
- The value returned from main is a status indicator. A nonzero has a meaning defined by system.
- The « operator takes 2 operands: left-hand is an ostream object, the other is a value, it returns its left-hand operand, so we can use « twice.
1
2
3
4
5
6
7
8
9
|
int main()
{
cout << "Enter two numbers:" << endl;
int v1 = 0, v2 = 0;
cin >> v1 >> v2;
cout << "The sum of " << v1 << " and " << v2<< " is " << v1 + v2 << endl;
return 0;
}
|
- End-of-File from keyboard, control-z on windows, control-d on linux.
- while(cin » value) , it tests if istream is valid. It returns false when we hit end-of-file or invalid input.
- “iostream.h”, this is old C head file, not belong to namespace std. The new declaration is #include .
变量和类型
- The type of an object determines what operations can be performed on it.
- C++ is a statically typed language, type checking is done at compile time.
- The types int, short, long, long long are all signed. Especially, char and signed char are not the same. char is signed on some machines and unsigned on others, so explicitly specify it.
- In an unsigned type, all bits represent the value. An 8-bit unsigned char can hold values from 0 to 255.
- Signed values are automatically converted to unsigned. So don’t mix signed and unsigned types.
- 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.
- we can write 20 in three ways:
1
2
3
|
20 /* decimal */
024 /* octal */
0x14 /* hexadecimal */
|
- The compiler appends a null character(’\0’) to every string literal. Literal ‘A’ : single character. String literal “A” : an array of two characters, letter A and ‘\0’.
- Escape Sequences:
1
2
3
4
5
6
7
8
9
10
11
|
\n newline
\t horizontal tab
\v vertical tab
\a alert
\b backspace
\" double quote
\' single quote
\\ backslash
\? question mark
\r carriage return
\f formfeed
|
we can also use a generalized escape sequence:
1
2
3
4
5
6
|
\7 bell
\12 newline
\40 blank
\0 null
\115 'M'
\x4d 'M'
|
- if a “" is followed by more than 3 octal digits, only the first 3 are associated with the “".
1
2
|
\1234 two characters, octal value 123 and character 4
\x1234 single, 16-bit character, four hexadecimal digits
|
- specify the type of a literal, override default type:
1
2
3
4
|
L 'a' wide character literal, type is wchar_t
42ULL unsigned integer literal, unsigned long long
1E-3F single-precision floating-point literal, type is float
3.14159L extended-precision floating-point literal, type is long double
|
- A variable provides us with named storage that our programs can manipulate.
- Most generally, an object is a region of memory that can contain data and has a type.
- Initialization and assignment are different operations in C++.
- Initialization forms:
1
2
3
4
|
int sold = 0;
int sold = {0};
int sold{0};
int sold(0);
|
- C++ 11 Initialization:
1
2
3
|
long double ld = 3.1415926536;
int a{ld}, b = {ld}; // error, narrowing conversion required
int c(ld), d = ld; // ok, but value will be truncated
|
-
Variables defined outside any function body are initialized to zero. Variables of built-in type defined inside a function are uninitialized, the value of it is undefined. Objects of class type that we do not explicitly initialize have a value that is defined by the class.
-
A declaration makes a name known to the program. A definition creates the associated entity. To obtain a declaration that is not a definition, we add the “extern” keyword and may not provide an explicit initializer. An extern that has an initializer is a definition. And, it is an error to provide an initializer on an extern inside a function.
1
|
extern double pi = 3.1416; // definition
|
- A reference defines an alternative name for an object. A reference type “refers to” another type. When we define a reference, we bind the reference to its initializer. There is no way to rebind a reference to a different object, so references must be initialized, a reference is not an object.
1
2
|
int i = 1024, i2 = 2048;
int &r = i, r2 = i2; // r is a reference bound to i, r2 is an int
|
-
C++ 11, a new kind of reference: an “rvalue reference“,it uses && to define, directly use temporary objects.
-
A pointer holds the address of another object. Unlike a reference, a pointer is an object in its own right. Pointers can be assigned and copied; a single pointer can point to several different objects over its lifetime. Unlike a reference, a pointer need not be initialized at the time it is defined.
1
2
|
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 address. Hence, we may not define a pointer to a reference.
-
The value stored in a pointer can be in one of four states:
(1) It can point to an object
(2) It can point to the location just immediately past the end of an object
(3)It can be a null pointer, indicating that it is not bound to any object
(4)It can be invalid; values other than the preceding three are invalid.
It is an error to copy or try to access the value of an invalid pointer. The result of it is undefined.
-
Some symbols have multiple meanings. Such as & and *, are used as both an operator in an expression and as part of a declaration. This is determined by the context.
1
2
3
4
5
|
int i = 42;
int &r = i; // & is part of a declaration
int *p; // * is part of a declaration
p = &i; // & as the address-of operator
*p = i; // * as the deference operator
|
- C++ 11, nullptr is a literal that has a special type that can be converted to any other pointer type. Old programs use a preprocessor variable named NULL. The preprocessor is a program runs before compiler. Morden C++ programs should avoid using NULL and use nullptr instead.
- The type void* is a special pointer type that can hold the address of any object.
- It is common misconception to think that the type modifier (* or &) applies to all variables in a statement.
1
2
|
int* p1, p2; // legal but might be misleading
int *p1, *p2; // both p1 and p2 are pointers to int
|
- We can store the address of a pointer in another pointer. A reference is not an object. Hence, we may not have a pointer to a reference. However, because a pointer is an object, we can define a reference to a pointer. The easiest way to understand the type of r is read the definition from right to left.
1
2
3
4
5
|
int i = 42;
int *p;
int *&r = p; // r is a reference to the pointer p
r = &i;
*r = 0;
|
- Const objects are local to a file. To share a const object among multiple files, define variable as extern.
- A reference to const May refer to an object that is not const.
- A const pointer must be initialized, and once initialized, its value may not be changed.
- P is a const pointer to const, neither the value of the object addressed by p nor the address stored in p can be changed.
- We use the term top-level const to indicate that the pointer iteself is a const, can appear in any object type. When a pointer can point to a const object, we refer to that const as a low-level const.
- A constant expression is an expression whose value cannot change and that can be evaluated at compile time.
1
2
3
4
|
const int max_files = 20; // max_files is a constant expression
const int limit = max_files + 1; // limit is a constant expression
int staff_size = 27; // staff_size is not a constant expression
const int sz = get_size(); // sz is not a constant expression
|
- C++ 11, to verify that a variable is a constant expression by declaring the variable in a constexpr declaration. Variables declared as constexpr are implicitly const and must be initialized by constant expressions.
1
2
3
|
constexpr int mf = 20; // 20 is a constant expression
constexpr int limit = mf + 1; // mf + 1 is a constant expression
constexpr int sz = size(); // ok only if size is a constexpr function
|
- C++ 11, we can use constexpr functions in the initializer of a constexpr variable. When we define a pointer in a constexpr declaration, the constexpr applies to the pointer.
1
2
3
4
5
|
const int *p = nullptr; // p is a pointer to a const int
constexpr int *q = nullptr; // q is a const pointer to int
constexpr int i = 42; // type of i is const int
constexpr const int *p = &i; // p is a constant pointer to the const int i
|
- A type alias is a name that is a synonym for another type. We often use “typedef” to define. C++ 11, an alias declaration starts with the keyword using followed by alias name.
1
2
3
4
5
6
7
|
typedef double wages; // wages is a synonym for double
typedef wages base, *p; // base is a synonym for double, p for double*
using SI = Sales_item; // SI is a synonym for Sales_item
wages hourly, weekly; // same as double hourly, weekly;
SI item; // same as Sales_item item
|
- C++ 11, we can use “auto” to let the compiler to deduce the type from the initializer. When we define multiple variables, they must have consistent types.
1
2
3
4
5
|
auto i = 0, *p = &i; // ok: i is int and p is a pointer to int
auto sz = 0, pi = 3.14; // error: inconsistent types for sz and pi
int i = 0, &r = i;
auto a = r; // a is an int (r is an alias for i, which has type int)
|
auto ordinarily ignores top-level consts.
1
2
3
4
5
|
const int ci = i, &cr = ci;
auto b = ci; // b is an int (top-level const in ci is dropped)
auto c = cr; // c is an int (cr is an alias for ci whose const is top-level)
auto d = &i; // d is an int*(& of an int object is int*)
auto e = &ci; // e is const int*(& of a const object is low-level const)
|
If we want the deduced type to have a top-level const, say explicityly.
1
2
|
const auto f = ci; // deduced type of ci is int; f has type const int
auto &g = ci; // g is a const int& that is bound to ci
|
As usual, the initializers must provide consistent auto-deduced types.
1
2
3
4
5
6
|
// k is int; l is int&
auto k = ci, &l = i;
// m is a const int&;p is a pointer to const int
auto &m = ci, *p = &ci;
// error: type deduced from i is int; type deduced from &ci is const int
auto &n = i, *p2 = &ci;
|
- C++ 11, we can get the type of variable or expression by using “decltype” specifier.
1
|
decltype(f()) sum = x; // sum has whatever type f returns
|
Here, the compiler does not call f, but uses the type it returns.
1
2
3
|
int i = 42, *p = &i, &r = i; // decltype of an expression can be a reference type
decltype(r + 0) b; // ok: addition yields an int; b is an (uninitialized) int
decltype(*p) c; // error: c is int& and must be initialized
|
The deference operator is an example of an expression for which decltype returns a reference. The deduction done by decltype depends on the form of its given expression, that is, enclosing the name of a variable in parentheses affects the type returned by decltype.
1
2
3
|
// decltype of a parenthesized variable is always a reference
decltype((i)) d; // error: d is int& and must be initialized
decltype(i) e; // ok: e is an (uninitialized) int
|
-
C++ 11, we can supply an in-class initializer for a data member. In-class initializers must either be enclosed inside curly braces or follow an = sign.
-
The most common technique for making it safe to include a header multiple times relies on the preprocessor. It is a program that runs before the compiler and changes the source text of our programs.
Strings, Vectors, Arrays
-
A string is a variable-length sequence of characters. A vector holds a variable-length sequence of objects of a given type. Like other built-in types, arrays represent facilities of the hardware, so arrays are less convenient.
-
A using declaration lets us use a name from a namespace without qualifying the name with namespace name. Once the using declaration has been made, we can access name directly. We can also use separate declaration. Headers should not include using Declarations.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
#include <iostream>
// using declarations for names from the standard library
using std::cin;
using std::cout; using std::endl;
int main()
{
cout << "Enter two numbers:" << endl;
int v1, v2;
cin >> v1 >> v2;
cout << "The sum of " << v1 << " and " << v2
<< " is " << v1 + v2 << endl;
return 0;
}
|
- Here, this is an example of namespace.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
namespace CompanyName
{
namespace Engine
{
class Node;
}
namespace MyGame
{
class Tutorial;
class TutorialManager
{
public:
TutorialManager();
~TutorialManager();
private:
static void RemoveNode(Engine::Node* node);
Tutorial* m_currentTut;
}
}
}
|
- A string is a variable-length sequence of characters. It is defined in the std namespace.
1
2
3
4
5
6
7
8
9
10
|
#include <string>
using std::string;
int main() {
string s1; // default initialization; s1 is the empty string
string s2 = s1; // s2 is a copy of s1
string s3 = "hiya"; // s3 is a copy of the string literal
string s4(10, 'c'); // s4 is cccccccccc
string s8 = string(10, 'c'); // copy initialization
}
|
-
The size() function returns a string::size_type value. The string class and other library types defines several companion types, such as size_type. Although we don’t know the precise type of string::size_type, we do know that it is an unsigned type big enough to hold the size of any string.
-
The string class comparing strategy:
(1) If two strings have different lengths and if every character in the shorter string is equal to the corresponding character of the longer string, then the shorter string is less than the longer one.
(2) If any characters at corresponding positions in the two strings differ, then the result of the string comparing the first character at which the strings differ.
1
2
3
4
5
6
|
string str1;
getline(cin, str1); // read a line
cout << str1.size() << endl; // <<, >> operator
string str2(5, 'c');
string str3 = str1 + "," + str2; // =, + operator
bool result = str1 < str2; // <, > compare operator
|
- C++ 11, we can use a Range for to change characters in a string.
Caution: Subscripts are Unchecked. We need to check that our subscript is less than size().
1
2
3
4
5
6
7
8
9
10
|
string cc("Some String");
for (auto &c:cc)
{
c = tolower(c); // c is a reference, so the assignment changes the char
}
if (!cc.empty())
{
cc[0] = toupper(cc[0]); // use subscript operator []
}
cout << cc << endl;
|
- A vector is a collection of objects, all of which have the same type. A vector is often referred to as a container because it “contains” other objects. A vector is a class template. C++ has both class and function templates. When we use a template, we specify what kind of class or function we want the compiler to instantiate.
1
2
3
4
5
6
7
8
9
10
11
|
#include <string>
#include <vector>
using std::vector;
using std::string;
int main() {
vector<int> ivec; // ivec holds objects of type int
vector<vector<string>> file; // vector whose elements are vectors
return 0;
}
|
- If we use braces and there is no way to use the initializers to initialize the object, then those values will be used to construct the objects.
1
2
3
4
5
6
7
8
9
10
|
vector<int> v1; // initial empty
vector<int> v2(v1); // copy elements of v1 into v2
vector<int> v3(3, 0); // v3 has 3 elements with value 0
vector<int> v4(3); // v4 has 3 elements, each initialized to 0
vector<int> v5{1,2,3}; // C++ 11, initialize from a list of elements
vector<string> articles = {"a", "an", "the"};
vector<string> svec(5, "hello");
vector<string> v7{10}; // v7 has 10 default-initialized elements
vector<string> v8{10, "hi"}; // v8 has 10 elements with value "hi"
|
- The body of a range for must not change the size of the sequence over which it is iterating. The subscript operator [] on vector (and string) fetches an existing element; it does not add an element. If the container is empty, the iterators returned by begin and end are equal, they are both off-the-end iterators. In general, we do not know the precise type that an iterator has. As with pointers, we can dereference an iterator to obtain the element denoted by an iterator. Like a const pointer, a const_iterator may read but not write the element it denotes.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
vector<int> nums = {1,2,3,4,5,6,7,8};
nums.push_back(9);
for (auto &i : nums)
{
i *= i;
cout << i << "\t";
}
vector<int>::size_type length = nums.size();
cout << endl << length << endl;
vector<int>::iterator it;
for (it = nums.begin(); it != nums.end(); it++)
{
*it *= 10;
cout << *it << "\t";
}
|
It is important to realize that loops that use iterators should not add elements to the container.
- An array is a data structure that is similar to the library vector type. Unlike a vector, arrays have fixed size; we cannot add elements to an array. A default-initialized array of built-in type that is defined inside a function will have undefined values. We cannot initialize an array as a copy of another array.
1
2
3
4
5
6
7
8
9
|
const unsigned sz = 3;
int ia1[sz] = {0,1,2}; // array of three ints with values 0, 1, 2
int a2[] = {0, 1, 2}; // an array of dimension 3
int a3[5] = {0, 1, 2}; // equivalent to a3[] = {0, 1, 2, 0, 0}
string a4[3] = {"hi", "bye"}; // same as a4[] = {"hi", "bye", ""}
char x1[] = {'C', '+', '+'}; // list initialization, no null
char x2[] = {'C', '+', '+', '\0'}; // list initialization, explicit null
char x3[] = "C++"; // null terminator added automatically
|
- When we use a variable to subscript an array, we normally define that variable to have type size_t. size_t is a machine-specific unsigned type that is guaranteed to be large enough to hold the size of any object. As with string and vector, it is up to the programmer to ensure that the subscript value is in range. When we use an array, the compiler ordinarily converts the array to a pointer.
1
2
3
4
5
6
7
8
9
10
|
string nums[] = {"one", "two", "three"}; // array of strings
string *p2 = nums; // equivalent to p2 = &nums[0]
int ia[] = {0,1,2,3,4,5,6,7,8,9}; // ia is an array of ten ints
auto ia2(&ia[0]); // now it's clear that ia2 has type int*
decltype(ia) ia3 = {0,1,2,3,4,5,6,7,8,9};// ia3 is an array of ten ints
int arr[] = {0,1,2,3,4,5,6,7,8,9};
int *p = arr; // p points to the first element in arr
++p; // p points to arr[1]
|
- C++ 11, the new function begin returns a pointer to the first, and end returns a pointer one past the last element.
1
2
3
4
5
6
7
8
9
|
#include <iterator>
using namespace std;
int main() {
int ia[] = {0,1,2,3,4,5,6,7,8,9}; // ia is an array of ten ints
int *beg = begin(ia); // pointer to the first element in ia
int *last = end(ia); // pointer one past the last element in ia
ptrdiff_t n = last - beg; // the result of subtracting two pointers
return 0;
}
|
- Unlike subscripts for vector and string, the index of the built-in subscript operator is not an unsigned type.
1
2
3
4
|
int ia[] = {0,2,4,6,8}; // array with 5 elements of type int
int *p = &ia[2]; // p points to the element indexed by 2
int k = p[-2]; // p[-2] is the same element as ia[0]
cout << *p << "\t" << k << endl; // 4 0
|
- C-style character strings are not a type, they are stored in character arrays and are null terminated. By null-terminated we mean that the last character in the string is followed by a null character ‘\0’. The standard C library provides a set of functions operated on C-style strings. However, to use these functions, we can easily miscalculate the size needed for the parameters. In addition to being safer, it is more efficient to use library string rather than C-style strings.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
char ca[] = {'c', '+', '+'}; // not null terminated
char ca2[] = "c++";
const char* ca3 = "c++";
cout << strlen(ca) << "\t" << strlen(ca2) << "\t" << strlen(ca3) << endl;
if (strcmp(ca, ca3) == 0) cout << "equal" << endl; // compare c-style string
char cp1[] = "c"; // if cp1[] = "", strcpy and strcat will be undefined
strcpy(cp1, ca3); // copy ca3 into cp1, size of cp1 should be big enough to hold ca3
strcat(cp1, ca2); // concatenate ca2 onto cp1
cout << cp1 << "\t" << strlen(cp1) << endl;
const char* src = "hi";
char* dest = new char[strlen(src)];
strncpy(dest, src, strlen(src));// safer such as strcpy_s, but not enough
cout << strlen(src) << "\t" << dest << endl;
|
- C++ may have to interface to code that uses arrays and C-style strings. C++ library offers some facilities to make it easier.
1
2
3
4
5
|
string s("hello world"); // initialize a string from a string literal
const char *str = s.c_str(); // the value returned by c_str may be invalid
int arr[] = {0, 1, 2, 3, 4};
vector<int> ivec(begin(arr), end(arr)); // copy array to vector
|
- Strictly speaking, there are no multidimensional arrays in C++,they are actually arrays of arrays.
1
2
3
4
5
6
7
8
9
10
|
int ia[3][4]; // array of size 3; each element is an array of ints of size 4
int arr[10][20][30] = {0}; // initialize all elements to 0
int ax[3][4] = { // three elements; each element is an array of size 4
{0, 1, 2, 3},
{4, 5, 6, 7},
{8, 9, 7, 8}
};
int axy[3][4] = {0,1,2,3,4,5,6,7,8,9,7,8}; // the nested braces are optional
int ix[3][4] = {{ 0 }, { 4 }, { 8 }};
int ixy[3][4] = {0,3,6,9}; // initialize row 0, the remaining elements are 0
|
- C++ 11, we can use a range for with Multidimensional Arrays. Remember that a multidimensional array is really an array of arrays.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
int ia[2][3] = {0,1,2,3,4,5};
// the innermost array must be references, we can only iterator on arrays, not pointers
for (auto &row : ia)
{
for (auto &col : row) // you can remove '&' here
{
cout << col << "\t" ;
col += 10;
}
}
cout << endl;
for (int i = 0; i < 6; i++)
{
cout << *(*ia + i) << "\t";
}
cout << endl;
// we should avoid to write the type of a pointer into an array by using auto or decltype
for (auto p = begin(ia); p != end(ia); ++p)
{
for (auto q = begin(*p); q != end(*p); ++q)
cout << *q << "\t";
}
int *ip[4]; // array of pointers to int
int (*ipx)[4]; // pointer to an array of four ints
|
表达式
-
Every expression in C++ is either an rvalue or an lvalue. When we use an object as rvalue, we use the object’s value. When we use an object as an lvalue, we use the object’s identity(its location in memory).
-
Precendence specifies how the operands are grouped. It says nothing about the order. In the following expression int i = f1() * f2(); We have no way of knowing whether f1 will be called before f2 or vice versa. For operators that do not specify evaluation order, it is an error for an expression to refer to and change the same object. For example, the « operator makes no guarantees about when or how its operands are evaluated.
1
2
|
int i = 0;
cout << i << " " << ++i << endl; // undefined
|
There are 4 operators that do guarantee the order in which operands are evaluated. The logical AND (&&) operator guarantees its left-hand operand is evaluated first and its right-hand operator is evaluated only if the left-hand operand is true; logical OR(||) operator ; conditional (? :) operator; comma (,) operator.
-
Order of operand evaluation is independent of precedence and associativity. In an expression such as: f() + g() * h() + j() There are no guarantees as to the order in which these functions are called.
(1) when in doubt, parenthesize expressions to force the grouping
(2) if you change the value of an operand, don’t use that operand elsewhere in the same expression
an important exception to the second rule occurs when the subexpression that changes the operand is itself. For example, in *++iter, the value of iter is the operand to the dereference operator, order is not an issue.
-
C++ 11, the modulus operator is defined so that if m and n are integers and n is nonzero, then (m/n)*n + m%n is equal to m. By implication, if m%n is nonzero, it has the same sign as m.
1
2
3
4
5
6
7
8
9
10
|
int i = 1024;
int k = -i; // k is -1024
bool b = true;
bool b2 = -b; // b2 is 1
short short_value = 32767; // max value if shorts are 16 bits
short_value += 1; // this calculation overflows
cout << short_value << endl; // -32768
// if m%n is nonzero, it has same sign as m
cout << -21 % -8 << '\t' << 21 % -5 << endl;
|
- Arithmetic and pointer operand with a value of zero are false, all other values are true. The logical AND and OR (&& ||) operators always evaluate their left operand before the right. The right operand is evaluated if and only if the left operand does not determine the result, this strategy is known as short-circuit evaluation. This can be used for checking index of array:
1
2
3
4
5
6
7
8
9
10
11
12
|
vector<string> text = {
"Don't start to wonder why.",
"You know I love you",
"when I lie to you"
};
for (const auto &s:text){
cout << s;
if (s.empty() || s[s.size() - 1] == '.')
cout << endl;
else
cout << " ";
}
|
- C++ 11, we can use a braced initializer list on the right-hand side. Assignment has lower precedence.
1
2
3
4
5
6
7
8
|
int k = { 3 }; // no narrowing conversion
vector<int> vi; // initial empty
vi = {0, 1, 2}; // use vector operator
int v = k = 0; // assignment is right associative
int i;
if((i = k) != 3) i++; // assignment has lower precedence, so need parentheses
cout << i << endl;
|
- The increment(++) and decrement(–) provide two forms of operators: prefix and postfix.
The prefix form increments its operand and yields the changed object as its result.
The postfix operator increments the operand but yields a copy of the original value as its result.
Use postfix operator only when necessary.
1
2
3
4
5
|
vector<int> v = { 2, 1, 0, -1, -2 };
auto pbeg = v.begin();
// print elements up to the first negative value
while (pbeg != v.end() && *pbeg >= 0)
cout << *pbeg++ << endl;
|
- The dot and arrow operators provide for member access.
1
2
|
string s = "a str", *p = &s;
cout << s.size() << ' ' << (*p).size() << ' ' << p->size();
|
- The conditional operator lets use embed simple if-else logic inside an expression. It has the following form:
condition ? expr1 : expr2; Nested conditionals quickly become unreadable. Do not nest more than two or three. The conditional operator has fairly low precedence. When we embed a conditional expression in a large expression, we usually must parenthesize the conditional sub expression.
1
2
3
4
5
|
int score = 68;
string grade = (score > 90) ? "high pass"
: (score < 60) ? "fail" : "pass";
cout << ((score < 60) ? "fail" : "pass") << endl; // print pass
cout << (score < 60) ? "fail" : "pass"; // print 0, cout << (score < 60)
|
- There are no guarantees for how the sign bit is handled, we should use unsigned types with bitwise operators. Bits that are shifted off the end are discarded. The left-shift operator («) inserts 0-valued bits on the right. The behavior of the right-shift operator (») depends on the type of the left-hand operand: if that operand is unsigned, the operator inserts 0-valued bits on the left. if it is signed, the result is implementation defined, either copies of the sign bit or 0-valued bits are inserted on the left.
1
2
3
|
unsigned i = 35;
cout << (1 << i) << endl; // 35 & 31 = 3; 1 << 3, depend on CPU, this is undefined
cout << (1 << 35) << endl; // directly compute
|
The result of above code is strange, because the right operand is too large. Also, there are other bitwise operators: ~ (NOT), & (AND), ^ (XOR), | (OR)
- To handle bit operation, we can also use bitset library.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
#include <iostream>
#include <bitset>
using namespace std;
int main()
{
bitset<8> bs(6);
cout << bs << endl; // 00000110
cout << bs.count() << endl; // 2, the size of bit '1'
cout << bs.size() << endl; // 8
bitset<8> tt(6);
tt.set(0); // set the bit to 1, from right to left
tt.reset(2); // set the bit to 0
cout << tt << endl; // 00000011
if (tt.test(0)) cout << "test if the bit is 1" << endl;
if (tt.any()) cout << "there is at least one '1' bit" << endl;
if (tt.none()) cout << "all bits are 0" << endl;
bitset<8> vs("011");
cout << vs.to_ulong() << endl; // get bit value, 3
bitset<16> vec(0xffff);
vec.flip(0); // reverse the bit
vec[1].flip();
cout << vec << endl; // 1111111111111100
return 0;
}
|
- The sizeof operator returns the size, in bytes, of an expression or a type name.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
int a = 1, *p = &a;
size_t value = sizeof a; // return type is size_t
cout << value << endl; // size of int
cout << sizeof p << endl; // size of pointer, the value depend on system
cout << sizeof *p << endl; // size of type to which p points
struct User
{
int id;
string name;
bool hasExpired;
};
cout << sizeof(int) << "\t" << sizeof(string) << "\t"
<< sizeof(bool) << "\t" << sizeof(User) << endl;
char array[10];
char ax[] = "abcd";
cout << sizeof(array) << endl;
cout << sizeof(ax) << endl; // sizeof will include '/0'
vector<char> vec = {'a', 'b'};
cout << sizeof(vec); // size of the fixed part of this vector
|
sizeof char or an expression of type char is guaranteed to be 1.
sizeof a reference type returns the size of an object of the referenced type.
sizeof a pointer returns the size needed hold a pointer.
sizeof a dereferenced pointer returns the size of an object of the type to which the pointer points.
sizeof an array is the size of the entire array. Note that sizeof does not convert the array to a pointer.
sizeof a string or a vector returns only the size of the fixed part of these types.
- The comma operator takes two operands, which it evaluates from left to right.
1
2
3
4
5
6
7
8
9
10
11
12
|
vector<int> ivec = {5, 6, 7};
vector<int>::size_type cnt = ivec.size();
// both ix and cnt are changed through the loop
for (vector<int>::size_type ix = 0; ix != ivec.size(); ++ix, --cnt)
{
ivec[ix] = cnt;
cout << ivec[ix] << "\t";
}
int x = 0, y = 0;
x == 0 ? ++x, ++y : --x, --y;
cout << endl << x << "\t" << y << endl; // 1 0
|
-
The arithmetic conversions, convert one arithmetic type to another. If both operands have the same signedness, then the operand with the smaller type is converted to the larger type. When the signedness differs and the type of the unsigned operand is the same as or larger than that of the signed operand, the signed operand is converted to unsigned. The remaining case is when the signed operand has a larger type than the unsigned operand, the result is machine dependent. If all values in the unsigned type fit in the larger type, then the unsigned type is converted to signed type. If the values don’t fit, then the signed operand is converted to unsigned type. For example, if the operands are long and unsigned int, and int and long have the same size, the long will be converted to unsigned int. If the long type has more bits, then the unsigned int will be converted to long.
-
In addition to the arithmetic conversions, there are several additional kinds of implicit conversions.
(1) Array to Pointer Conversions: when we use an array, the array is automatically converted to a pointer to the first element in that array. This conversion is not performed when an array is used with decltype or as the operand of the address-of(&), sizeof, or typeid operators. The conversion is also omitted when we initialize a reference to an array.
(2) Pointer Conversions: a constant integral value of 0 and the literal nullptr can be converted to any pointer; a pointer to any nonconst type can be converted to void*, and a pointer to any type can be converted to a const void*.
(3) Conversions to bool: If the pointer or arithmetic value is zero, the conversion yields false, any other yields true.
(4) Conversion to const: we can convert a pointer to a nonconst type or a pointer to the corresponding const type. If T is a type, we can convert a pointer or a reference to T into a pointer or reference to const T.
(5) Conversions Defined by Class Types: the compiler will apply automatically. This can be done by define constructor, but not recommend to define this conversion.
-
Sometimes we want to explicitly force an object to be converted to a different type. To do so, we use a cast to do an explicit conversion. A named cast has the following form: cast-name<type> (expression); The type is the target type, and expression is the value to be cast. If type is a reference, the result is an lvalue.
(1) static_cast, any well-defined type conversion, other than those involving low-level const, can be requested using a static_cast. It is also useful to perform a conversion that the compiler will not generate automatically.
(2) const_cast, change a low-level const in its operand, or say that convert a const object to nonconst type. Once we have cast away the const of an object, the compiler will no longer prevent us from writing to that object.
(3) reinterpret_cast, generally performs a low-level reinterpretation of the bit pattern of its operands. A reinterpret_cast is inherently machine dependent. So avoid casts.
(4) dynamic_cast, used in combination with inheritance and run-time type identification.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
int i = 2, j = 1;
double slope = static_cast<double>(j) / i;
cout << slope << endl;
double d = 1.5;
void* p = &d;
double *dp = static_cast<double*>(p);
cout << *dp << endl;
const int constant = 1;
int* modifier = const_cast<int*>(&constant);
*modifier = 2; // constant can not be really modified
cout << constant << endl; // 1
|
- Statement and Exception
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
|
#include <iostream>
#include <vector>
#include <limits>
#include <stdexcept>
using namespace std;
class MyException{
public:
MyException(string msg) { err_msg = msg;}
void ShowErrorMsg() { cerr << err_msg << endl;}
private:
string err_msg;
};
void TestException(){
throw MyException("MyException!");
}
void TestStdException(char c){
if (c < numeric_limits<char>::max())
throw invalid_argument("exception: argument too large");
}
int main() {
// if else
int grade = 75;
int level;
if (grade < 60) {
level = 0;
}
else if (grade >= 60 && grade < 80) {
level = 1;
}
else {
level = 2;
}
// switch, break
switch(level) {
case 0:
cout << "Grade : Fail" << endl;
break;
case 1:
cout << "Grade : Pass" << endl;
break;
case 2:
cout << "Grade : Excellent" << endl;
break;
default:
cout << "Unknown level value" << endl;
break;
}
// while statement
int array[] = {99, 98, 97};
auto beg = begin(array);
while (beg != end(array)){
cout << *beg << "\t";
++beg;
}
cout << endl;
// do while
do{
--beg;
cout << *beg << "\t";
}while(beg != begin(array));
cout << endl;
// range for, continue
vector<int> v = {7, 8, 9};
for (auto &r : v)
{
if (r % 2 == 0)
{
continue;
}
r *= 2;
}
// for statement
for (int i = 0; i < v.size(); ++i)
{
cout << v[i] << "\t";
}
cout << endl;
// exception
try{
TestException();
}catch(MyException& e){
e.ShowErrorMsg();
}
try{
TestStdException(256);
}catch(invalid_argument& e){
cerr << e.what() << endl;
}
return 0;
}
|
函数
-
Arguments are the initializers for a function’s parameters. The first argument initializes the first parameter, the second argument initializes the second parameter, and so on. Although we know which argument initializes which parameter, we have no guarantees about the order in which arguments are evaluated. Parameter names are optional. However, there is no way to use an unnamed parameter. Therefore, parameters ordinarily have names.
-
Objects defined outside any function exist throughout the program’s execution. Such objects are created when the program starts and are not destroyed until the program ends. The lifetime of a local variable depends on how it is defined. Each local static object is initialized before the first time execution passes through the object’s definition. Local statics are not destroyed when a function ends; they are destroyed when the program terminates. If a local static has no explicit initializer, it is value initialized, meaning that local statics of built-in type are initialized to zero.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
#include <iostream>
using namespace std;
size_t count_calls()
{
static size_t ctr = 0; // value will persist across calls
return ++ctr;
}
int main() {
for (size_t i = 0; i != 10; ++i)
cout << count_calls() << "\t";
cout << endl;
return 0;
}
|
-
When a parameter is a reference, we say that its corresponding argument is “passed by reference” or that the function is “called by reference.” Reference parameters let us effectively return multiple results. When the argument value is copied, the parameter and argument are independent objects. We say such arguments are “passed by value” or alternatively that the function is “called by value.” When we copy a pointer, the value of the pointer is copied. After the copy, the two pointers are distinct. However, We can change the value of that object by assigning through the pointer.
-
When we copy an argument to initialize a parameter, top-level consts are ignored. Arrays have two special properties that affect how we define and use functions that operate on arrays: We cannot copy an array, and when we use an array it is usually converted to a pointer.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
#include <iostream>
using namespace std;
//parameter is a reference to an array
void print(int (&arr)[3])
{
for (auto elem : arr)
cout << elem << endl;
}
int main(int argc, char **argv) {
//the size of an array is part of its type
int arr[] = {0, 1, 2};
print(arr);
// arguments begin in argv[1], argv[0] contains program name
cout << argv[0] << endl;
cout << argc << endl;
return 0;
}
|
- We can write a function that takes an unknown number of arguments of a single type by using an initializer_list parameter. An initializer_list is a library type that represents an array of values of the specified type. Ellipsis parameters are in C++ to allow programs to interface to C code that uses a C library facility named varargs. Generally an ellipsis parameter should not be used for other purposes.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
#include <iostream>
#include <stdarg.h>
using namespace std;
void error_msg(int errCode, initializer_list<string> il)
{
cout << "ErrorCode:" << errCode << endl;
for (auto elem : il)
cout << elem << "\t";
cout << endl;
}
void foo(int errCode, const char* desc, ...)
{
cout << desc << ":" << errCode << endl;
char* temp;
va_list arg_ptr;
// va_start must appear in front of va_arg, desc is the last parameter
va_start(arg_ptr, desc);
while(temp = va_arg(arg_ptr, char*))
{
cout << temp << "\t";
}
va_end(arg_ptr);
}
int main() {
error_msg(1, {"are", "you", "ok"});
cout << endl;
foo(1, "Error", "No,", "thank", "you", nullptr);
return 0;
}
|
- Never Return a Reference or Pointer to a Local Object. When a function completes, its storage is freed. C++ 11, functions can return a braced list of values. We cannot return an array, but a function can return a pointer or a reference to an array. C++ 11, to simplify the declaration of a pointer to an array, we can use a trailing return type. Trailing returns can be defined for any function, but are most userful for functions with complicated return types.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
|
#include <iostream>
#include <vector>
#include <stdlib.h>
using namespace std;
// C++ 11, list initializing the return value
vector<string> process()
{
return {"functionX", "okay"};
}
int testFunc(int a)
{
return a;
}
int (*ptr)[10];
// a function that returns a pointer to an array
int (*testPFunc1(int i))[10]
{
return ptr;
}
// a trailing return type
// a function takes an int argument and returns a pointer to an array of ten ints
auto testPFunc2(int i) -> int(*)[10]
{
return ptr;
}
int odd[] = {1, 3, 5, 7, 9};
int even[] = {0, 2, 4, 6, 8};
decltype(odd) *arrPtrFunc(int i)
{
return (i % 2) ? &odd : &even; // returns a pointer to the array
}
int main() {
for (auto s:process())
cout << s << " ";
cout << endl;
typedef int INT;
using arrTS = int[10];
typedef int arrT[10]; // arrT is a synonym for the type array of 10 ints
typedef int (*arrP)[10]; // a pointer to an array
typedef int (*arrFunc)(int i); // a pointer named arrFunc to a function
typedef const char* cpstr;
arrT arr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
arrP ap = &arr;
INT a = (*ap)[9];
arrFunc foo = testFunc;
cout << foo(a) << endl;
int(*funcArr[10])(int a);
funcArr[0] = testFunc;
cout << funcArr[0](100) << endl;
ptr = &arr;
arrP p1 = testPFunc1(0);
cout << (*p1)[9] << endl;
arrP p2 = testPFunc2(0);
cout << (*p2)[9] << endl;
cpstr s = "hello";
cout << s << endl;
// this returned value from main is a status indicator
if (true)
return EXIT_SUCCESS;
else
return EXIT_FAILURE;
}
|
- If we declare a name in an inner scope, that name hides uses of that name declared in an outer scope. Names do not overload across scopes. In C++, name lookup happens before type checking.
1
2
3
4
5
6
7
8
9
10
11
|
#include <iostream>
using namespace std;
void print(double i) { cout << i << endl; }
void print(int i) { cout << i << endl;}
int main() {
void print(int); // new scope: hides previous instance of print
print(3.14);
return 0;
}
|
-
A function specified as inline (usually) is expanded “in line” at each call. The inline is only a request to the compiler. The compiler may choose to ignore this request. This mechanism is meant to optimize small, straight-line functions that are called frequently. Many compilers will not inline a recursive function. A 75-line function will not be expanded inline.
-
A constexpr function is a function that can be used in a constant expression. The return type and the type of each parameter in an expression must be a literal type, and the function body must contain exactly one return statement. In order to be able to expand the function immediately, constexpr functions are implicitly inline.
1
2
3
4
|
constexpr int new_sz() { return 42; }
// scale(arg) is a constant expression if arg is a constant expression
constexpr size_t scale(size_t cnt) { return new_sz() * cnt; }
|
A constexpr function is not required to return a constant expression. Unlike other functions, inline and constexpr functions may be defined multiple times. As a result, inline and constexpr functions normally are defined in headers.
- To conditionally execute debugging code, there are two preprocessor facilities: assert and NDEBUG. assert is a preprocessor macro, that preprocessor variable acts like an inline function. Preprocessor names are managed by preprocessor, not the compiler. As a result, we use preprocessor names directly and not provide a using declaration for assert. The behavior assert depends on the status of a preprocessor variable named NDEBUG. If NDEBUG is defined, assert does nothing. We can “turn off” debugging by providing a #define to define NDEBUG.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
#include <iostream>
#include <cassert>
using namespace std;
void print()
{
// __func__, the C++ compiler defines
// __FILE__ ... the preprocessor defines in debugging
#ifndef NDEBUG
cout << __FILE__ << endl; // name of the file
cout << __LINE__ << endl; // current line number
cout << __TIME__ << endl; // the time the file was compiled
cout << __DATE__ << endl; // the date the file was compiled
cerr << __func__ << endl; // name of the function
#endif
}
int main() {
print();
assert(true);
return 0;
}
|
- The first step of function matching identifies the candidate functions. A candidate function is a function with the same name as the called function. The second step selects the viable functions from candidate functions. To be viable, a function must have the same number of parameters, and the type of each argument must match or can be converted. The third step determines which viable function provides the best match for the call. An exact match is better than a match that requires a conversion.
1
2
|
f(double, double);
f(int, int);
|
For call f(5.6, 3.14), f(double, double) is a better match.
For call f(42, 2.56), the compiler will reject this call because it is ambiguous.
Also, for func(long) and func(float), call func(3.14) is ambiguous.
- A function’s type is determined by its return type and parameters type, not the function’s name. When we use the name of a function as a value, the function is automatically converted to a pointer.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
|
#include <iostream>
using namespace std;
bool compareLength(const string& a, const string& b){
return a.length() > b.length();
}
// equivalent declare, function will automatically be converted to pointer as argument
void printBigger1(const string& s1, const string& s2,
bool (*pf)(const string&, const string&));
void printBigger(const string& s1, const string& s2,
bool pf(const string&, const string&)){
if (pf(s1, s2))
cout << s1 << endl;
else
cout << s2 << endl;
}
typedef bool Func(const string&, const string&); // function type
typedef decltype(compareLength) Func2; // equivalent type
void printBigger2(const string&, const string&, Func);
// decltype returns a function type, if we want a pointer, we must add * ourselves.
typedef bool (*FuncP)(const string&, const string&); // pointer type
typedef decltype(compareLength) *FuncP2; // equivalent type
void printBigger3(const string&, const string&, FuncP2);
// pf points to a function returning bool
bool (*pf)(const string&, const string&);
// declares a function named pf that returns a bool*
bool *bf (const string&, const string&);
using F = int(int*, int); // F is a function type
using PF = int(*)(int*, int); // PF is a pointer type
// from the inside out, f1 has a parameter list, so f1 is a function
// f1 is preceded by a * so f1 returns a pointer
// the type of that pointer itself has a parameter list
// so the pointer points to a function, the function returns an int
int (*f1(int))(int*, int);
auto f2(int) -> int (*)(int*, int); // use a trailing return
int main() {
// equivalent assignment and call
pf = compareLength;
cout << pf("123", "12") << endl;
pf = &compareLength;
cout << pf("123", "12") << endl;
cout << (*pf)("123", "12") << endl;
cout << endl;
printBigger("123", "12", compareLength);
cout << endl;
// just call the declared function, so the parentheses is important
cout << *bf("12", "123") << endl;
return 0;
}
bool result = false;
bool* bf(const string& a, const string& b){
result = a.length() > b.length();
return &result;
}
|
Classes
-
In C++ we use classes to define our own data types. The fundamental ideas behind classes are data abstraction and encapsulation. Data abstraction is a programming technique that relies on the separation of interface and implementation. The interface of a class consists of the operations that users of the class can execute. The implementation includes the class data members, the bodies of functions that constitute the interface. Encapsulation enforces the separation of a class interface and implementation. A class that is encapsulated hides its implementation, users can use the interface but have no access to the implementation. A class that uses data abstraction and encapsulation defines an abstract data type.
-
Member functions must be declared inside the class, and may be defined inside or outside the class body. Functions defined in the class are implicitly inline. Member functions access the object on which they were called through an extra implicit parameter named this. When we call a member function, “this” is initialized with the address of the object on which the function was invoked. Any direct use of a member of the class is assumed to be an implicit reference through “this”. Because “this” is intended to always refer to “this” object, “this” is a const pointer, we cannot change the address that “this” holds. By default, the type of “this” is a const pointer to the nonconst version of the class type. So we cannot bind “this” to a const object. This fact, in turn, means that we cannot call an ordinary member function on a const object. A const following the parameter list indicates that this is a pointer to const, member functions that use const in this way are const member functions. It means that const member functions cannot change the object on which they are called. Objects that are const, and references or pointers to const objects, may call only const member functions.
-
The job of a constructor is to initialize the data members of a class object. A constructor is run whenever an object of a class type is created. Constructors have the same name as the class, no return type. Classes control default initialization by defining a special constructor, known as the default constructor. The default constructor is one that takes no arguments. If our class does not explicitly define any constructors, the compiler will implicitly define the default constructor:
(1) If there is an in-class initializer, use it to initialize the member
(2) Otherwise, default-initialize the member.
The compiler generates a default constructor automatically only if a class declares no constructors. Classes that have members of built-in or compound type usually rely on the synthesized default constructor only if all such members have in-class initializers. If a class has a member that has a class type, and that class doesn’t have a default constructor, then the compiler can’t initialize that member. For such cases, we must define our own constructor. Constructors should not override in-class initializers except to use a different initial value.
A class can allow another class or function to access its nonpublic members by making that class or function a friend. Friend declarations may appear anywhere in the class.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
#include <iostream>
using namespace std;
// The only difference between struct and class is the default access level
// by default, members are public in struct, private in class
struct Sales_data
{
friend istream &read(istream&, Sales_data&);
friend ostream &print(ostream&, const Sales_data&);
public:
Sales_data() = default; // C++ 11, the default constructor
Sales_data(const string &s) : bookNo(s) { }
Sales_data(const string &s, unsigned n, double p): bookNo(s),
units_sold(n), revenue(p*n) { }
Sales_data(istream &);
string isbn() const { return this->bookNo; }
Sales_data& combine(const Sales_data&);
private:
double avg_price() const { return units_sold ? revenue/units_sold : 0; }
string bookNo; // default initializes bookNo to empty string
unsigned units_sold = 0; // in-class initializer
double revenue = 0.0;
};
// the scope operator :: means that this function is declared in Sales_data
Sales_data& Sales_data::combine(const Sales_data &rhs) {
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this; // return the object on which the function was called
}
Sales_data add(const Sales_data &lhs, const Sales_data &rhs){
Sales_data sum = lhs;
sum.combine(rhs);
return sum;
}
ostream &print(ostream &os, const Sales_data &item){
os << item.isbn() << " " << item.units_sold << " "
<< item.revenue << " " << item.avg_price() << endl;
return os;
}
istream &read(istream &is, Sales_data &item){
double price = 0;
is >> item.bookNo >> item.units_sold >> price;
item.revenue = price * item.units_sold;
return is;
}
Sales_data::Sales_data(istream &is) {
read(is, *this);
}
int main() {
cout << "Please input data: bookNo, units_sold, price" << endl;
Sales_data item1(cin);
print(cout, item1);
return 0;
}
|
- A mutable data member is never const, even when it is a member of a const object.
When we provide an in-class initializer, we must do so following an = sign or inside braces.
It is important to understand that friendship is not transitive.
Member function definitions are processed after the compiler processes all of the declarations in the class. If function definitions were processed at the same time as the member declarations, then we would have to order the member functions so that they referred only to names already seen.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
|
#include <iostream>
#include <vector>
using namespace std;
class Screen; // forward declaration
class Link_screen
{
// to use the incomplete class Screen, first we need forward declaration
// then we can only define pointers or references to that class
Screen* window;
Link_screen *next;
Link_screen *prev;
};
class Screen
{
// friendship is not transitive, so if class Window_mgr has its own friends,
// those friends have no special access to Screen
friend class Window_mgr;
public:
typedef string::size_type pos;
Screen() = default; // needed because Screen has another constructor
Screen(pos ht, pos wd, char c) : height(ht), width(wd), contents(ht * wd, c) { }
Screen& set(char);
Screen& set(pos, pos, char);
char get() const { return contents[cursor]; } // get the character at the cursor
inline char get(pos r, pos c) const;
Screen& move(pos r, pos c);
void some_member() const;
Screen& display(ostream& os) { do_display(os); return *this; }
const Screen& display(ostream& os) const { do_display(os); return *this; }
private:
void do_display(ostream& os) const { os << contents << endl; }
pos cursor = 0;
pos height = 0, width = 0; // the row number and column number
string contents;
mutable size_t access_ctr; // may change even in a const object
};
// the returned reference is lvalue
inline Screen& Screen::set(char c) {
contents[cursor] = c;
return *this;
}
inline Screen& Screen::set(pos r, pos col, char ch) {
contents[r*width + col] = ch;
return *this;
}
char Screen::get(pos r, pos c) const {
pos row = r * width;
return contents[row + c]; // return character at the given column
}
// specify inline on the definition
inline Screen& Screen::move(pos r, pos c) {
pos row = r * width;
cursor = row + c; // move cursor to the column within that row
return *this;
}
void Screen::some_member() const {
++access_ctr; // keep a count of the calls to any member function
}
class Window_mgr
{
public:
using ScreenIndex = vector<Screen>::size_type;
void clear(ScreenIndex); // reset the Screen at the given position to all blanks
ScreenIndex addScreen(const Screen&);
private:
vector<Screen> screens { Screen(24, 80, ' ') }; // use = sign or inside braces
};
void Window_mgr::clear(ScreenIndex i) {
Screen &s = screens[i];
s.contents = string(s.height * s.width, ' ');
}
// :: is needed before ScreenIndex
Window_mgr::ScreenIndex Window_mgr::addScreen(const Screen &s) {
screens.push_back(s);
return screens.size() - 1;
}
int main() {
class Screen myScreen(2, 5, '0'); // this "class" word is useless
const Screen blank(2, 5, 'X');
myScreen.set('#').display(cout);
blank.display(cout);
Screen::pos ht = 2, wd = 4; // use the scope operator "::"
Screen scr(ht, wd, '-');
scr.display(cout);
return 0;
}
|
The parameter name will hide the class member name which has the same name with it. So don’t use a member name for a parameter or other local variable. If the compiler doesn’t find the name in function or class scope, it looks for the name in the surrounding scope.
- We can often, but not always, ignore the distinction between whether a member is initialized or assigned. We must use the constructor initializer list to provide values for members that are const, reference, or of a class type that does not have a default constructor.
1
2
3
4
5
6
7
8
|
class ConstRef {
public:
ConstRef(int ii);
private:
int i;
const int ci;
int &ri;
};
|
For example, we write below code.
1
|
ConstRef::ConstRef(int ii) { i = ii; ci = ii; ri = i; }
|
This will be wrong, the right way is using the constructor initializer.
1
|
ConstRef::ConstRef(int ii): i(ii), ci(ii), ri(i) { }
|
The constructor initializer list sepcifies only the values, not the order in which those initializations are performed. Members are initialized in the order in which they appear in the class definition: the first member is initialized first, then next. It is a good idea to write constructor initializers in the same order as the members are declarde. Moreover, when possible, avoid using members to initialize other members.
- C++ 11, the use of constructor initializers let us define the delegating constructors. A delegating constructor uses another constructor from its own class to perform its initialization.
1
2
3
4
5
6
7
8
9
10
11
12
|
class Sales_data{
public:
Sales_data(string s, unsigned cnt, double price):
bookNo(s), units_sold(cnt), revenue(cnt*price){ }
// delegating constructor
Sales_data():Sales_data("", 0, 0) { }
Sales_data(string s):Sales_data(s, 0, 0) { }
private:
string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
|
- The default constructor is used automatically whenever an object is default or value initialized. It happens
(1)When we define nonstatic variables or arrays at block scope without initializers
(2)When a class that has members of class type uses the synthesized default constructor
(3)When members of class type are not explicitly initialized in a constructor initializer list
(4)During array initialization when we provide fewer initializers than the size of the array
(5)When we define a local static object without an initializer
(6)When we explicitly request value initialization by writing an expressions of the form T() where T is the name of a type uses an argument of this kind to value initialize its element initializer.
Classes must have a default constructor in order to be used in these contexts. What may be less obvious is the impact on classes that have data members that do not have default constructor.
1
2
3
4
5
6
7
8
9
10
11
12
|
class NoDefault{
public:
NoDefault(string&); // this class has no default constructor
};
struct A{
NoDefault my_mem;
};
A a; // error: cannot synthesize a constructor for A
struct B{
B(){ } // error: no initializer for b_member
NoDefault b_member;
};
|
The following declaration of obj is wrong. We should remove the empty parentheses.
1
2
3
4
|
class Sales_data {
};
Sales_data obj(); // ok, but defines a function, not an object
|
- For automatically classs type conversion, only one class-type conversion is allowed. We can prevent use of a constructor in a context that requires an implict conversion by declaring constructor as explicit. The explicit keyword is meaningful only on constructors that can be called with a single argument. The explicit keyword is used only on the constructor declaration inside the class. When a constructor is declarde explicit, it can be used only with the direct form of initialization.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
#include <iostream>
using namespace std;
class Sales_data{
public:
Sales_data() = default;
Sales_data(const string &s, unsigned n, double p):
bookNo(s), units_sold(n), revenue(p*n){ }
// this constructor can not be used to implicitly create a Sales_data object
explicit Sales_data(const string &s):Sales_data(s, 0, 0) { }
Sales_data& combine(const Sales_data&);
private:
string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
Sales_data& Sales_data::combine(const Sales_data &rhs) {
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}
int main() {
Sales_data item;
// first, only one class-type conversion is allowed, so "string" can not be removed
// second, the explicit Sales_data constructor will prevent implicit conversion
//item.combine(string(""));
item.combine(Sales_data("")); // force conversion
// explicit constructors can be used only for direct initialization
//Sales_data item2 = string("");
return 0;
}
|
- An aggregate class gives users direct access to its members and has special initialization syntax. It requires
(1)All of its data members are public
(2)It does not define any constructors
(3)It has no in-class initializers
(4)It has no base classes or virtual functions
We can initialize the data members of an aggregate class by providing a braced list of member initializers. If the list of initializers has fewer elements than the class has members, the trailing members are value initialized.
1
2
3
4
5
|
struct Data{
int ival;
string s;
};
Data val = {0, "Anna"};
|
- An aggregate class whose data members are all of literal type is a literal class. A nonaggregate class, that meets the following restrictions, is also a literal class:
(1)The data members all must have literal type
(2)The class must have at least one constexpr constructor
(3)If a data member has an in-class initializer, the initializer for a member of built-in type must be a constant expression, or if the member has class type, the initializer must use the member’s own constexpr constructor
(4)The class must use default definition for its destructor, which is the member that destroys objects of the class type.
A constexpr constructor can be declared as = default. Otherwise, a constexpr constructor must meet the requirements of a constructor—meaning the only excutable statement it can have is a return statement. As a result, the body of a constexpr constructor is typically empty with the keyowd constexpr.
1
2
3
4
5
6
7
8
9
10
|
class Debug {
public:
// a constexpr constructor must initialize every data member
constexpr Debug(bool b = true): hw(b), io(b), other(b){}
constexpr Debug(bool h, bool i, bool o): hw(h), io(i), other(0){}
private:
bool hw; // hardware errors other than IO errors
bool io; // IO errors
bool other; // other errors
};
|
- Classes sometimes need members that are associated with the class, rather than with individual objects of the class type. We say a member is associated with the class by adding the keyword static to its declaration. Static member functions are not bound to any object; they do not have a this pointer. As a result, static member functions may not be declared as const, and we may not refer to this in the body of a static member. Even though static members are not part of an object, we can use an object, reference or pointer to access static member.
Because static data members are not part of individual objects of the class type, they are not defined when we create objects of the class. As a result, they are not initialized by the class constructors. In general, we may not initialize a static member inside the class. Instead, we must define and initialize each static data member outside the class body. The best way to ensure that the object is defined once is to put the definition of static data members in the same file.
We can provide in-class initializers for static members that have const integral type and must do so for static members that are constexprs of literal type. The initializers must be constant expressions. If the member is used only in contexts where the compiler can substitute the member’s value, then an initialized const or constexpr static need not be separately defined. Even if a const static data member is initialized in the class body, that member ordinarily should be defined outside the class definition.
A static data member can have incomplete type. In particular, a static data member can have the same type as the class type of which it is a member. A nonstatic data member is restricted to being declared as a pointer or a reference. Another difference between static and ordinary member is that we can use a static member as a default argument.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
#include <iostream>
using namespace std;
class Account {
public:
// we can use a static member as a default argument
Account(double newAmount, double newPeriod = period):amount(newAmount){}
double getAmount() { return amount; }
void calculateAmount() { amount += amount * interestRate; }
static double getRate() { return interestRate; }
static void setRate(double newRate) { interestRate = newRate; }
private:
double amount;
static double interestRate;
static constexpr int period = 30; // If it can be substituted, it need not be separated
static Account account1; // ok, static member can have incomplete type
Account* account2; // ok, pointer member can have incomplete type
};
// defined outside the class definition
double Account::interestRate = 1.0f;
int main() {
Account account(100);
Account::setRate(2.0f);
account.calculateAmount();
cout << account.getAmount() << endl;
return 0;
}
|
The IO Library
- To support different kinds of IO processing, the library defines a collection of IO types:
1
2
3
4
5
6
7
8
9
10
11
|
iostream istream, wistream reads from a stream
ostream, wostream writes to a stream
iostream, wiostream reads and writes a stream
fstream ifstream, wifstream reads from a file
ofstream, wofstream writes to a file
fstream, wfstream reads and writes to a file
sstream istringstream, wistringstream reads from a string
ostringstream wostringstream writes to a string
stringstream, wstringstream reads and writes a string
|
The library lets us ignore differences among these kinds of streams by using inheritance. The types ifstream and istringstream inherit from istream. Thus, we can use objects of type ifstream or istringstream as if they were istream objects. For example, we can call getline on an ifstream or istringstream object, and we can use » to read data from an ifstream or istringstream.
Because we can’t copy the IO types, we cannot have a parameter or return type that is one of the stream types. Functions that do IO typically pass and return the stream through references. Reading or writing an IO object change its state, so the reference must not be const. The library can let us manipulate the condition state of a stream:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
strm::iostate strm is one of the IO types. iostate is a machine-dependent
integral type that represents the condition state of a stream
strm::badbit iostate value used to indicate that a stream is corrupted
strm::failbit iostate value used to indicate that an IO operation failed
strm::eofbit iostate value used to indicate that a stream hit end-of-file
strm::goodbit iostate value used to indicate that a stream is not in an error state,
it is 0
s.eof() true if eofbit in the stream s is set
s.fail() true if failbit or badbit in the stream s is set
s.bad() true if badbit in the stream s is set
s.good() true if the stream s is in a valid state
s.clear() reset all condition values in the stream s to valid state, return void
s.clear(flags) reset the condition of s to flags.Type of flags is iostate,return void
s.setstate(flags) add specified condition to s. Types of flags is iostate, return void
s.rdstate() return current condition of s as a iostate value
|
Once an error has occurred, subsequent IO operations will fail. The easiest way to determine the state of a stream:
1
|
while (cin >> word) // ok: read operation successful...
|
The while condition checks the state of the stream returned from the » expression. This only tells us whether the stream is valid. If we want to know why the stream is valid, we can convey information with the iostate. To turn off a single condition, we use the rdstate member and the bitwise operators to produce the desired new state.
1
2
|
// turns off failbit and badbit but all other bits unchanged
cin.clear(cin.rdstate() & ~cin.failbit & ~cin.badbit);
|
Each output stream manages a buffer, which it uses to hold the data that the program reads and writes. Using a buffer allows the OS to combine several output operations into a single system-level write. There are several conditions that cause the buffer to be flushed, such as the program completes normally, the buffer become full, we use a flush operation such as endl. Or an output stream might be tied to another stream. In this case, the buffer of the tied stream is flushed whenever the tied stream is read or written. By default, cin and cerr are both tied to cout. Hence, reading cin or writing to cerr flushes the buffer in cout. If we want to flush after every output, we can use the unitbuf manipulator.
1
2
3
4
5
6
7
|
cout << unitbuf; // all writes will be flushed immediately
cout << nounitbuf; // returns to normal buffering
cout << "hello" << endl; // writes data and a newline, then flushes the buffer
cout << "year" << ends; // writes data and a null, then flushes the buffer
cout << "world" << flush; // writes data, then flushes the buffer
cout << "123" << endl;
|
There are two versions of tie: one version takes no argument and returns a pointer to the output stream, if any, to which this object is currently tied. The function returns the null pointer if the stream is not tied. The second version of tie takes a pointer to an ostream and ties itself to that ostream. That is, x.tie(&o) ties the stream x to the output stream o. We can tie either an istream or an ostream object to another ostream:
1
2
3
4
|
cin.tie(&cout); // the library has tied cin and cout for us
ostream *old_tie = cin.tie(nullptr); // cin is no longer tied
cin.tie(&cerr); // reading cin flushes cerr, not cout
cin.tie(old_tie); // reestablish normal tie
|
To tie a given stream to a new output stream, we pass tie a pointer to the new stream. To untie the stream completely, we pass a null pointer. Each stream can be tied to at most one stream at a time. However, multiple streams can tie themselves to the same ostream.
- In addition to the behavior that they inherit from iostream types, the types defined in fstream has more operations.
1
2
3
4
5
6
7
8
9
|
fstream fstrm; create an unbound file stream.
fstream fstrm(s); create an fstream and open the file named s.
fstream fstrm(s, mode) like above, but open s in the given mode.
fstrm.open(s) open the file and bind the file to fstrm
fstrm.open(s, mode) like above
fstrm.close() close the file to which fstream is bound
fstrm.is_open() return a bool indicating whether the file associated with fstrm
was successfully opened and has not been closed.
|
When we want to read or write a file, we define a file stream object and associate that object with the file. Each file stream class defines a member function named open that does whatever system-specific operations are required to locate the given file and open it for reading or writing as appropriate.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
#include <iostream>
#include <fstream>
using namespace std;
int main() {
string fileName = "d://a.txt";
ifstream input(fileName); // construct an ifstream and open the given file
if (!input)
{
cout << "open fail:" << input.good() << endl;
}
ofstream output; // output file stream that is not associated with any file
output.open(fileName + ".copy");
if (output) // if open fail, failbit is set
{
cout << "open success:" << output.good() << endl;
}
return 0;
}
|
Once a file stream has been opened, it remains associated with the specified file. Indeed, calling open on a file stream that is already open will fail and set failbit. Subsequent attempts to use that file stream will fail. To associate a file stream with a different file, we must first close the existing file.
Each stream has an associated file mode that represents how the file may be used. The following are some file modes:
1
2
3
4
5
6
|
in open for input
out open for output
app seek to the end before every write
ate seek to the end immediately after the open
trunc truncate the file
binary do IO operations in binary mode
|
We can supply a file mode whenever we open a file. The modes that we can specify have the following restrictions:
(1)out may be set only for an ofstream or fstream object
(2)in may be set only for an ifstream or fstream object
(3)trunc may be set only when out is also specified.
(4)app mode may be specified so long as trunc is not. If app is specified, the file is always opened in output mode.
(5)by default, a file opened in out mode is truncated even if we do not specify trunc. To preserve the contents of a file opened with out, either we specify app, or we must also specify in and the file is open for both input and output.
(6)the ate and binary mode may be specified on any file stream object type and in combination with any other file modes.
Each file stream type defines a default file mode that is used whenever we do not otherwise specify a mode. Files associated with an ifstream are opened in in mode; files associated with an ofstream are opened in out mode; and files associated with an fstream are opened with both in and out modes. By default, when we open an ofstream, the contents of the file are discarded. We can use app mode to prevent this.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
#include <iostream>
#include <fstream>
using namespace std;
int main() {
string fileName = "d://a.txt";
ofstream out1(fileName); // out and trunc are implicit
out1.close();
ofstream out2(fileName, ofstream::out | ofstream::trunc);
out2.close();
ofstream app1(fileName, ofstream::app); // out is implicit
app1.close();
ofstream app2(fileName, ofstream::app | ofstream::out);
app2.close();
ofstream out; // no file mode is set
out.open(fileName);
out.close(); // close out so we can use it for a different file
out.open(fileName + ".copy", ofstream::app);
out.close();
return 0;
}
|
The file mode of a given stream may change each time a file is opened.
- The istringstream type reads a string, ostringstream writes a string, and stringstream reads and writes the string. In addition to the operations they inherit from iostream, there are some new members:
1
2
3
4
|
sstream strm; strm is an unbound stringstream. sstream is one of the types.
sstream strm(s); strm is an sstream that holds a copy of the string s.
strm.str() returns a copy of the string that strm holds
strm.str(s) copies the string s into strm. returns void.
|
Note that fstream and sstream share the interface to iostream, but they have no other interrelationship. In particular, we cannot use open and close on a stringstream, nor can we use str on an fstream. An istringstream is often used when we have some work to do on an entire line, and other work to do with individual words within a line. An ostringstream is useful when we need to build up our output a little at a time but do not want to print output until later.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
#include <iostream>
#include <vector>
#include <sstream>
#include <fstream>
using namespace std;
struct PersonInfo {
string name;
vector<string> phones;
};
int main() {
string line, word;
vector<PersonInfo> people;
// example: lee 15288488907 6235106
while (getline(cin, line)){
if (people.size() >= 3) break;
PersonInfo info;
istringstream record(line); // bind record to the line we read
record >> info.name;
while (record >> word){
info.phones.push_back(word);
}
people.push_back(info);
}
ostringstream formatted;
for (const auto &entry: people){
formatted << entry.name;
for (const auto &p: entry.phones){
formatted << " " << p;
}
formatted << endl;
}
string fileName = "d://a.txt";
ofstream out(fileName);
out << formatted.str();
return 0;
}
|
Sequential Containers
- A container holds a collection of objects of a specified type. The sequential containers let the programmer control the order in which the elements are stored and accessed. The order corresponds to the position at which elements are put into container.
1
2
3
4
5
6
7
8
9
10
11
12
|
vector flexible-size array. supports fast random access.
inserting or deleting elements other than at the back may be slow.
deque double-ended queue. supports fast random access.
fast insert/delete at front or back.
list doubly linked list, supports only bidirectional sequential access.
fast insert/delete at any point in the list.
forward_list singly linked list. supports only sequential access in one direction.
fast insert/delete at any point in the list.
array fixed-size array. support fast random access.
cannot add or remove elements.
string a specialized container, similar to vector, that contains characters.
fast random access. fast insert/delete at the back.
|
The sequential containers, which are listed above, all provide fast sequential access to their elements. However, these containers offer different performance trade-offs relative to:
(1)The costs to add or delete elements to the container
(2)The costs to perform nonsequential access to elements of the container
With the exception of array, which is a fixed-size container, the containers provide efficient, flexible memory management. We can add and remove elements, growing and shrinking the size of the container.
-
For example, string and vector hold their elements in contiguous memory. Because elements are contiguous, it is fast to compute the address of an element from its index. However, adding or removing elements in the middle of one of these containers takes time: All the elements after the one inserted or removed have to be moved to maintain contiguity. Moreover, adding an element can sometimes require that additional storage be allocated. In that case, every element must be moved into the new storage.
-
The list and forward_list containers are designed to make it fast to add or remove an element anywhere in the container. In exchange, these types do not support random access to elements: we can access an element only by iterating through the container. Moreover, the memory overhead for these containers is often substantial, when compared to vector, deque and array.
-
A deque is a more complicated data structure. Like string and vector, deque supports fast random access. As with string and vector, adding or removing elements in the middle of a deque is a expensive operation. However, adding or removing elements at either end of the deque is a fast operation, comparable to adding an element to a list or forward_list.
-
C++ 11, the forward_list and array types were added by the new standard. An array is a safer, easier-to-use alternative to built-in arrays. Like built-in arrays, library array have fixed size. As a result, array does not support operations to add and remove elements or to resize the container. A forward_list is intended to be comparable to the best handwritten, singly linked list. Consequently, forward_list does not have the size operation because storing or computing its size would entail overhead compared to a handwritten list.
-
A few rules of thumb that apply to selecting which container to use:
(1)Unless you have a reason to use another container, use a vector
(2)If your program has lots of small elements and space overhead matters, don’t use list or forward_list
(3)If the program requires random access to elements, use a vector or a deque
(4)If the program needs to insert or delete elements in the middle of the container, use a list or forward_list
(5)If the program needs to insert or delete elements at the front and the back, but not in the middle, use a deque
(6)If the program needs to insert elements in the middle of the container only while reading input, and subsequently needs random access to the elements: First, decide whether you actually need to add elements in the middle of a container. It is often easier to append to a vector and then call the library sort function to reorder the container when you’re done with input. If you must insert into the middle, consider using a list for the input phase. Once the input is complete, copy the list into a vector.
What if the program needs random access and needs to insert and delete elements in the middle of the container ? This decision will depend on the relative cost of accessing the elements in a list or forward_list versus the cost of inserting or deleting elements in a vector.
- All the iterators on the standard container types let us access an element by providing the dereference operator. Similarly, the iterators for the library containers all define the increment operator to move from one element to the next. The exception is that the forward_list iterators do not support the decrement(–) operator. An iterator range is denoted by a pair of iterators each of which refers to an element, or to one past the last element, in the same container. These two iterators, often refered to as begin and end. This element range is called a left-inclusive interval. The standard notation is: [begin, end).
(1)If begin equals end, the range is empty
(2)If begin is not equal to end, there is at least one element in the range
(3)We can increment begin some number of times until begin == end
There are several version of begin and end: The versions with an r return reverse iterators, with a c return the const version of the related iterator. The functions that do not begin with a c are overloaded. That is, there are actually two members named begin. One is a const member that returns the container’s const_iterator type. The other is nonconst and returns the container’s iterator type. Similarly for rbegin, end and rend.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
#include <iostream>
#include <list>
using namespace std;
int main() {
list <string> a = {"Milton", "Shakespeare", "Austen"};
auto it1 = a.begin(); // list<string>::iterator
auto it2 = a.rbegin(); // list<string>::reverse_iterator
auto it3 = a.cbegin(); // list<string>::const_iterator
auto it4 = a.crbegin(); // list<string>::const_reverse_iterator
cout << *it1 << ends << *it2 << ends << *it3 << ends << *it4 << endl;
return 0;
}
|
The c versions were introduced by new standard to support using auto with begin and end functions.
Note: when write access is not needed, use cbegin and cend.
- We can create a new container by directly copying the container, or copying a range of elements denoted by a pair of iterators, but the container and element types must match. We can use the constructor that takes a size argument if the element type is a built-in type or a class type that has a default constructor. If the element type does not have a default constructor, then we must specify an explicit element initializer along with the size. The constructors that take a size are valid only for sequential containers. Although we cannot copy or assign objects of built-in array types, there is no such restriction on array.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
#include <iostream>
#include <list>
#include <forward_list>
#include <vector>
#include <deque>
#include <array>
using namespace std;
int main() {
// list initialize a container
list<string> authors = {"Milton", "Shakespeare", "Austen"};
vector<const char*> articles = {"a", "an", "the"};
list<string> list1(authors); // copy container with constructor, type must match
forward_list<string> words(articles.begin(), articles.end()); // convert to string
vector<int> ivec1(10, -1); // 10 elements, each initialized to -1
list<string> svec1(10, "hi"); // 10 strings, each element is "hi"
forward_list<int> ivec2(10); // 10 elements, each initialized to 0
deque<string> svec2(10); // 10 elements, each initialized to an empty string
array<int, 10> arr1; // array holds 10 ints, all elements are 0
array<string, 10> arr2; // array holds 10 strings
array<int, 10> arr3 = {0, 1, 2}; // remaining elements are 0
int digits[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; // can not be copied
arr1 = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
array<int, 10> arr4 = arr1; // array container can be copied
//---------------------------------assign--------------------------------------------
// because the existing elements are replaced, the iterators passed to assign must not
// refer to the container on which assign is called
forward_list<string> myWords;
myWords.assign(articles.begin(), articles.end()); // use assign to replace elements
list<string> slist1(1); // one element, which is the empty string
slist1.assign(10, "xy"); // 10 elements, each one is "xy"
cout << slist1.empty() << " " << slist1.size() << " " << slist1.max_size() << endl;
//---------------------------------swap----------------------------------------------
// The swap operation exchanges the contents of two containers of the same type
// excepting array, swap does not copy, delete, or insert any elements, so it is fast
// It is best to use the nonmember version of swap rather than member version of swap
vector<string> sv1(10);
vector<string> sv2(24);
swap(sv1, sv2);
cout << sv1.size() << " " << sv2.size() << endl;
return 0;
}
|
- Comparing two containers performs a pairwise comparison of the elements.
(1)If both containers are the same size and all the elements are equal, then they are equal; otherwise not equal.
(2)If the containers have different size but every element of the smaller one is equal to the corresponding element of the larger one, then the smaller one is less than the other.
(3)If neither container is an initial subsequence of the other, then the comparison depends on comparing the first unequal elements.
1
2
3
4
5
6
7
8
|
vector<int> v1 = {1, 3, 5, 7, 9, 12};
vector<int> v2 = {1, 3, 9};
vector<int> v3 = {1, 3, 5, 7};
vector<int> v4 = {1, 3, 5, 7, 9, 12};
cout << (v1 < v2) << endl; // true
cout << (v1 < v3) << endl; // false
cout << (v1 == v4) << endl; // true
cout << (v1 == v2) << endl; // false
|
- The previous section covered operations common to all containers. We’ll cover operations to sequential containers. Excepting array, all of the library containers provide flexible memory management. The following is the operator to access elements in a sequential container.
1
2
3
4
5
6
7
8
|
c.back() Returns a reference to the last element in c.
Undefined if c is empty.
c.front() Returns a reference to the first element in c.
Undefined if c is empty.
c[n] Returns a reference to the element indexed by unsigned n.
Undefined if n >= c.size().
c.at[n] Returns a reference to the element indexed by n.
If the index is out of range, throws out_of_range exception.
|
Container operations may invalidate iterators. An invalidated pointer, reference, or iterator is one that no longer denotes an element, using it is a serious programming error as using an uninitialized pointer. After adding elements to a container
(1)iterator, pointer, and reference to a vector or string are invalid if the container was reallocated. If no reallocation happens, indirect references to elements before insertion remain valid; those to elements after insertion are invalid.
(2)iterator, pointer, and reference to a deque are invalid if we add elements anywhere but at the front or back. If we add at the front or back, iterators are invalidated, but references and pointers to existing elements are not
(3)iterator, pointer, and reference to a list or forward_list remain valid When we add or remove elements in a vector or string, or add elements or remove any but the first element in a deque, the iterator returned by end() is always invalidated. Thus, loops that add or remove elements should always call end() rather than use a stored copy.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
|
#include <iostream>
#include <list>
#include <forward_list>
#include <vector>
#include <deque>
#include <stack>
#include <queue>
using namespace std;
template <class T>
void print(T s)
{
for (auto e: s)
cout << e << " ";
cout << endl;
}
struct Item {
Item() = default;
Item(int _id, string _name): id(_id), name(_name){}
int id;
string name;
};
int main() {
//------------------------push_back----------------------------
string numStr = "45";
numStr.push_back('6');
cout << numStr << endl;
//-------------------------push_front---------------------------
// list, forward_list, deque
list<int> list1 = {1, 2};
list1.push_back(3);
list1.push_front(0);
print(list1);
//------------------------insert--------------------------------
vector<string> svec1 = {"world"};
svec1.insert(svec1.begin(), "hello");
svec1.insert(svec1.end(), 3, "-"); // insert three elements
vector<string> svec2 = {"how", "are", "you"};
// must not refer to the same container as the one we are changing
// C++ 11, insert that take a count or a range return an iterator to the first element
// that was inserted. If no elements are inserted, return its first parameter
auto iter = svec1.insert(svec1.end(), svec2.end() - 3, svec2.end());
print(svec1); // hello world - - - how are you
svec1.insert(iter, "ok");
print(svec1); // hello world - - - ok how are you
//---------------emplace_front, emplace, emplace_back------------
// when we call push or insert, we pass objects and those objects are copied into
// container. C++ 11, emplace members use arguments to construct an element
// directly in space managed by container
deque<int> nums;
nums.emplace_front(1);
nums.emplace_front(0);
nums.emplace_back(2);
print(nums);
vector<Item> items;
items.emplace_back(); // use the Item default constructor
items.emplace_back(1, "Tom"); // convert to Item type implicitly
items.emplace(items.begin(), 2, "Newbie");
items.push_back(Item(3, "Jenny"));
auto item_size = items.size();
cout << "item size:" << item_size << endl;
//-----Access Elements Operator: back(), front(), [n], at(n)-----
cout << endl;
if (!items.empty())
{
Item first = items.front();
Item last = items.back();
cout << "first id:" << first.id << " name:" + items.at(0).name << endl;
cout << "last id:" << last.id << " name:" + items[item_size-1].name << endl;
}
//---------------------pop_front, pop_back-----------------------
// pop_front and pop_back functions remove the first and last element
// forward_list has only push_front, so it also has only pop_front, no size operation
// There is no push_front for vector and string
vector<int> rlist = {1, 2, 3};
rlist.pop_back();
cout << endl;
print(rlist);
//-------------------------erase, clear--------------------------
// erase member remove element at a specified point in the container
// return an iterator referring to the location after the removed element
list<int> elist = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
auto it = elist.begin();
while( it != elist.end()) {
if (*it % 2)
it = elist.erase(it); // if *it is odd, remove it
else
++it; // it *it is even, increment it
}
elist.erase(elist.begin(), elist.end()); // remove all elements
elist.clear(); // also remove all elements
cout << "elist:";
print(elist);
//-----forward_list: insert_after, emplace_after, erase_after-----
// forward_list also defines before_begin, returns an off-the-beginning iterator
forward_list<int> flist = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
auto prev = flist.before_begin();
auto curr = flist.begin();
while (curr != flist.end()){
if (*curr % 2)
curr = flist.erase_after(prev);
else {
prev = curr;
++curr;
}
}
cout << "flist:";
print(flist);
//-------------------------resize--------------------------
// If current size is greater than requested size, elements are deleted from the back
// If the current size is less than new size, elements are added to the back
list<int> zlist(10, 1);
zlist.resize(15);
cout << endl;
print(zlist);
zlist.resize(20, 2);
print(zlist);
zlist.resize(5);
print(zlist);
//------iterator, reference, pointer may be invalidated------
vector<int> vi = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
for (auto it = vi.begin(); it != vi.end();) // we can not store vi.end() to use it here
{
if (*it % 2 == 0) {
it = vi.erase(it);
}
else {
// duplicate current element
it = vi.insert(it, *it);
// insert inserts before it, then refresh iterator to prevent invalidated it
it += 2;
}
}
cout << endl;
cout << "vi:";
print(vi);
//-------------------reserve, shrink_to_fit------------------
// The reserve tell the container how many elements it can hold before allocating more
// space. The reserve affects only how much memory the vector preallocate. It changes
// capacity only if the requested space exceeds the current capacity. Resize operation
// changes only the number of elements in the container, it may also change capacity.
// C++ 11, we can call shrink_to_fit to ask a deque, vector, or string to return
// unneeded memory. Remember that:
// shrink_to_fit() is only a request, there is no guarantee it will work.
vector<int> ivx= {1, 2, 3, 4, 5};
cout << endl;
cout << "size:" << ivx.size() << " capacity:" << ivx.capacity() << endl;
ivx.push_back(6); // often double the current capacity
cout << "size:" << ivx.size() << " capacity:" << ivx.capacity() << endl;
ivx.reserve(100);
ivx.shrink_to_fit();
cout << "size:" << ivx.size() << " capacity:" << ivx.capacity() << endl;
//-----------------------string: substr-----------------------
// substr throws an out_of_range exception if position exceeds the size of the string
const char* cp = "Hello World"; // null terminated array
char noNullArray[] = {'H', 'i'};
string s1(cp); // copy up to the null in cp
string s2(noNullArray, 2); // copy two characters
string s3(noNullArray); // undefined
string s4(cp + 6, 5);
cout << endl;
cout << "s4:" << s4 << endl;
string s5(s1, 6, 5);
string s6 = s1.substr(0, 5);
string s7 = s1.substr(6);
cout << s6 << " " << s7 << endl;
string xx = "a";
xx.insert(xx.size(), 2, 'b'); // insert two characters
cout << "xx:" << xx << endl;
xx.erase(xx.size() - 1, 1); // erase last one characters
cout << "xx:" << xx << endl;
const char* cx = "Go Go Go, Oh lai Oh lai Oh Lai";
string cxp;
cxp.assign(cx, 5); // assign 5 characters starting with the one pointed to by cx
cout << "cxp:" << cxp << endl;
cxp.insert(cxp.size(), cx + 5); // insert before the element at cxp[size()]
cout << "cxp:" << cxp << endl;
string cxps = "[]";
cxps.insert(1, cxp, 10, cxp.size());//insert cxp to cxps at the position before cxps[1]
cout << "cxps:" << cxps << endl;
//--string: append, replace, find, rfind, find_first_of, find_last_of--
string as = "C++";
as.insert(as.size(), " Primer");
as.append(" 4th Edition");
as.replace(as.find("4"), 1, "5");
cout << endl;
cout << as << endl;
// find the first position that is a number
string::size_type pos = as.find_first_of("0123456789");
if (pos != string::npos){
cout << "position:" << pos << " value:" << as[pos] << endl;
}
string ns = "999123abc321";
// find first position that is not in numbers
pos = ns.find_first_not_of("0123456789");
// find from right to left, with a start position argument
auto rpos = ns.rfind("1", ns.size() - 2);
cout << ns[pos] << '\t' << rpos << endl << endl;
//----------------------string: compare---------------------
string c1 = "abc";
cout << "equal value:" << c1.compare("abc") << endl;
cout << "greater value:" << c1.compare("ab") << endl;
cout << "less value:" << c1.compare("abcd") << endl;
string c2 = "1abc2";
// position from 0 in c1, from 1 in c2, both 3 elements
cout << c1.compare(0, 3, c2, 1, 3) << endl << endl;
//---------------to_string, stoi, stof, stod----------------
// If the string can't be converted to a number, throw an invalid_argument exception
// If the conversion generates a value that can't be represented,
// throw out_of_range exception
int i = 42;
//string s = to_string(i);
//double d = stod(s);
//--Sequential Container Adaptors: stack, queue, priority_queue--
// adaptor: Library type, function, or iterator that, given a type, function,
// or iterator, make it act like another. both stack and queue are implemented
// on deque, priority_queue is implemented on a vector.
deque<int> deq = {1, 2, 3};
stack<int> stk(deq); // copy from deq
stk.push(4);
stk.pop();
// we can also use other container to implement a stack by naming a container as a
// second type argument. There are constraints on which containers can be used for
// a given adaptor, all of the adaptors require ability to add and remove elements.
// So they cannot be built on array. Similarly, we cannot use forward_list, because
// all of the adaptors require operations that add, remove, or access the last
// element in the container.
// (1) A stack requires push_back, pop_back, and back, so we can use any of the
// remaining types for it.
// (2) The queue adaptor requires back, push_back, front, and push_front, so it
// can be built on a list or deque but not on a vector.
// (3) A priority_queue requires random access, front, push_back, pop_back. It can
// be built on a vector or a deque but not on a list.
stack<string, vector<string>> str_stk;
cout << "stk top value:" << stk.top() << endl;
queue<int> que(deq);
cout << "que front value:" << que.front() << endl;
cout << "que back value:" << que.back() << endl;
priority_queue<int, vector<int>, less<int> >pq;
pq.push(3); pq.push(2); pq.push(4); pq.push(1);
while (!pq.empty()) { cout << pq.top() << ends; pq.pop(); }
cout << endl;
return 0;
}
|
When we use these operations, we must remember that the containers use different strategies for allocating elements and that affect performance. Adding elements anywhere but at the end of a vector or string, or anywhere but the beginning or end of a deque, requires elements to be moved. Moreover, adding elements to a vector or string may cause the entire object to be reallocated. Reallocating an object requires allocating new memory and moving elements from the old space to the new.
Generic Algorithms
- Most of algorithms are defined in the algorithm header, some numeric algorithms are defined in the numeric header.
- Iterators make the algorithm container independent … but algorithm depend on element-type operations. Algorithms never execute container operations, they operate in terms of iterators and iterator operations. The aim of this design is to separate algorithms and operation provided by member function. Cause algorithms operate on iterators, not containers. Thus, an algorithm cannot (directly) add or remove elements.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
|
#include <iostream>
#include <algorithm>
#include <list>
#include <sstream>
using namespace std;
vector<string> split(const string& s, char delim) {
vector<string> elems;
stringstream ss(s);
string item;
while (getline(ss, item, delim)) {
elems.push_back(item);
}
return elems;
}
int main() {
//---------------------------------find---------------------------------
int v1 = 42;
vector<int> s1 = {30, 40, 42, 50};
auto result1 = find(s1.begin(), s1.end(), v1); // find in vector
cout << "The value " << v1 << (result1 == s1.end() ? " is not present" :
" is present") << endl;
char v2 = 'v';
string s2 = "very much";
// find in string, with iterator range
auto result2 = find(s2.begin() + 1, s2.end(), v2);
cout << "The value " << v2 << (result2 == s2.end() ? " is not present" :
" is present") << endl;
char v3 = 'x';
char s3[] = {'a', 'v', 'a'};
auto result3 = find(begin(s3), end(s3), v3); // find in built-in array
cout << "The value " << v3 << (result3 == end(s3) ? " is not present" :
" is present") << endl;
//---------------------------------count---------------------------------
char v4 = 'i';
string s4 = "This is a good idea!";
// count nums of v4 in s4
cout << "count result:" << count(s4.begin(), s4.end(), v4) << endl;
//------------------------------accumulate------------------------------
// initial sum is 0
cout << "accumulate int:" << accumulate(s1.cbegin(), s1.cend(), 0) << endl;
vector<string> v5 = {"x", "y"};
cout << "accumulate string:" << accumulate(v5.cbegin(), v5.cend(), string("")) << endl;
vector<double> dd = {1, 2, 3.5};
// ".f" is important
cout << "accumulate double:" << accumulate(dd.begin(), dd.end(), 0.f) << endl;
//---------------------------------equal---------------------------------
// whether two sequences hold the same values, element type can be different as we use
// == to compare. It assumes that the second sequence is at least as big as the first.
list<const char*> v6 = {"x", "y", "z"};
cout << "equal result:" << equal(v5.cbegin(), v5.cend(), v6.cbegin()) << endl;
//------------------------------fill, fill_n------------------------------
fill(v6.begin(), v6.end(), "-");
cout << v6.front() << endl;
vector<int> v7; // empty vector
fill_n(v7.begin(), v7.size(), 0); // reset all elements to 0
//back_inserter return an insert iterator bound that container. When we assign through
//the iterator, the assignment calls push_back to add an element with the given value
//to the container
auto it = back_inserter(v7);
*it = 35;
cout << "v7 size:" << v7.size() << " value:" << v7[0] << endl;
//frequently use back_inserter to create an iterator to use as destination of an
//algorithm. fill_n cannot change the size of container, back_inserter will use
//the member "push_back" of the container to change container size,
//but back_inserter is an iterator adaptor
v7.clear();
fill_n(back_inserter(v7), 10, 0); // append 10 elements
for_each(v7.begin(), v7.end(),[](int n){ cout << n << " ";});
cout << endl;
//---------------------------------copy---------------------------------
int v8[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int v8x[sizeof(v8)/sizeof(*v8)];
// copy v8 into v8x, return iterator past the last element in v8x
auto ret = copy(begin(v8), end(v8), v8x);
//-------------------------replace, replace_copy-------------------------
replace(begin(v8x), end(v8x), 0, 9); // replace any element 0 with 9
for_each(begin(v8x), end(v8x), [](int n){ cout << n << " ";});
cout << endl;
//if we do not want to change origional sequence, we can call replace_copy
vector<string> v9 = {"e", "e", "q", "q"};
vector<string> v9x;
// result will store at v9x, v9 is not changed
replace_copy(v9.begin(), v9.end(), back_inserter(v9x), "e", "q");
for_each(v9x.begin(), v9x.end(),[](string s){ cout << s << " ";});
cout << endl;
//---------------------------------sort, unique---------------------------------
vector<string> words = split("the quick red fox jumps over the slow red turtle", ' ');
sort(words.begin(), words.end());
//Once sorted, the unique rearrange the input range to "eliminate" adjacent duplicated
//entries. size of words is unchanged, "eliminate" means that it overwrites adjacent
//duplicates so that the unique elements appear at the front of the sequence.
//note: algorithm only operate on iterators, not containers.
//So they cannot(directly) add or remove elements.
auto end_unique = unique(words.begin(), words.end());
//because algorithms cannot do container operations,
//we'll use erase to actually remove elements
words.erase(end_unique, words.end());
for_each(words.begin(), words.end(), [](string s){ cout << s << " ";});
cout << endl;
return 0;
}
|
- Algorithms compare elements default use either the element type’s < or == operator. The library also defines versions of these algorithms that let us supply our own operation to use in place of the default operator.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
|
#include <iostream>
#include <algorithm>
#include <sstream>
#include <iterator>
using namespace std;
class Employee {
public:
Employee(int _age, string _name) : age(_age), name(_name) { }
int age;
string name;
};
// this class is used for sorting, also it can record some states if you need
class EmployeeSortByAge{
public:
bool operator () (const Employee &lhs, const Employee &rhs){
return lhs.age < rhs.age;
}
};
// this function is just for sorting
bool isShorter(const string &s1, const string &s2){
return s1.size() < s2.size();
}
int main() {
//-----------------------------stable_sort, sort-----------------------------
string centence = "what do you think about this problem";
istringstream ss(centence);
vector<string> svec;
copy(istream_iterator<string>(ss), istream_iterator<string>(), back_inserter(svec));
// sort by alphabetical order
sort(svec.begin(), svec.end());
// stable_sort preserve the physical order of semantically equivalent values, while
// sort doesn't. stable_sort maintains the original alphabetical order among those
// elements that have the same length. Actually, we can not see difference
// here if we use sort. This makes me confused ... Where is the equality ?
stable_sort(svec.begin(), svec.end(), isShorter);
copy(svec.begin(), svec.end(), ostream_iterator<string>(cout, " "));
cout << endl;
vector<Employee> employees1 = {
Employee(108, "Zaphod"),
Employee(32, "Arthur"),
Employee(108, "Ford"),
};
// the 3rd parameter is a function object that takes two arguments.
sort(employees1.begin(), employees1.end(), EmployeeSortByAge());
for_each(employees1.begin(), employees1.end(), [](const Employee &e) { cout << e.age
<< ", " << e.name << "\n"; });
int arr[] = {2, 4, 1};
// greater_equal, less, less_equal, equal_to, not_equal_to
sort(begin(arr), end(arr), greater<int>());
for_each(begin(arr), end(arr), [](int n) { cout << n << " ";});
cout << endl << endl;
//-----------------------------partition-----------------------------
//the partition will separate elements into two parts with the condition
vector<int> values = {21, -5, 33, 15, 68, 2, -1, -9, 100, 50};
// you can use stable_partition if you care about original order
auto firstIt = partition(values.begin(), values.end(), [](int value) {
return value < 0; });
for_each(values.begin(), values.end(), [](int v) { cout << v << " "; });
cout << endl;
//-----------------------------partial_sort-----------------------------
partial_sort(firstIt, values.end() - 3, values.end());
for_each(values.begin(), values.end(), [](int v) { cout << v << " "; });
cout << endl;
//-----------------------------nth_element-----------------------------
//Rearranges elements in the range, in such a way that the element at the nth element
//is the element that would be in that position in a sorted sequence.
vector<int> elements = {9, 8, 1, 5, 3, 7, 0};
nth_element(elements.begin(), elements.begin() + elements.size()/2, elements.end());
for_each(elements.begin(), elements.end(), [](int v) { cout << v << " "; });
cout << endl;
return 0;
}
|
- We can pass any kind of callable object to an algorithm. An object or expression is callable if we can apply the call operator to it. That is, if e is a callable expression, we can write e (args). The only callables we’ve used so far are functions and function pointers. There are two other kinds of callables: classes that overload the function-call operator (), which is also named function object, and lambda expressions.
C++ 11, A lambda expression represents a callable unit of code. It can be thought of as an unnamed, inline function.
[capture list] (parameter list) -> return type { function body }
Unlike a function, lambdas may be defined inside a function. The capture list is an list of local variables defined in the enclosing function; return type, parameter list, and function body are the same as ordinary function.
We can omit either or both of the parameter list and return type but must include capture list and function body: auto f = [] { return 42; }
If we omit the return type, the lambda has an inferred return type that depends on the code in the function body. Unlike a function, lambda may not have default arguments. Also, a lambda may use a variable local to its surrounding function only if the lambda captures that variable in its capture list. When we pass a lambda to a function, we are defining both a new type and an object of that type.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
|
#include <iostream>
#include <algorithm>
#include <functional>
using namespace std;
bool check_size (const string &s, int maxSize){
return s.size() <= maxSize;
}
bool is_shorter (const string &ls, const string &rs){
return ls.size() < rs.size();
}
ostream &print (ostream &os, const string &s, char c){
return os << s << c;
}
int main() {
//-------------------------------Lambda------------------------------
vector<string> words = {"to", "find", "a", "good", "job"};
vector<string> words_copy = words;
// first sort by size using lambda
sort(words.begin(), words.end(), [] (const string& ls, const string& rs) {
return ls.size() < rs.size(); });
int sz = 3;
// then find the first value that make the predict true. The local static variable
// need not be put in capture list. Here we cannot use a function in place of
// the lambda because the lambda need the local variable sz.
// To solve this problem, we can use a new library function named bind
auto it_wc = find_if(words.begin(), words.end(), [sz] (const string &a) {
return a.size() >= sz; });
// finally print result using for_each
for_each(it_wc, words.end(), [] (const string &s) { cout << s << " "; });
cout << endl << endl;
//-----------------------Capture by Reference or Value---------------------
size_t v1 = 42;
//capture by value that is copied when the lambda is created
auto f = [v1] { return v1; };
//capture by reference object that must exist at the time the lambda is executed
auto fx = [&v1] { return v1; };
v1 = 0;
//If possible, avoid capturing pointers or references
cout << f() << " " << fx() << endl << endl;
//-----------------------------Implicit Captures----------------------------
char c = ' ';
ostream &os = cout;
// c explicitly captured by value, other variables implicitly captured by reference
for_each(words_copy.begin(), words_copy.end(), [&, c] (const string &s) {
os << s << c; });
cout << endl;
// os explicitly captured by reference, other variables implicitly captured by value
// when we mix implicit and explicit captures, the first item in the capture list
// must be an & or =
for_each(words_copy.begin(), words_copy.end(), [=, &os] (const string &s) {
os << s << c; });
cout << endl << endl;
//------------------------------Mutable Lambdas-----------------------------
//If we want to change a variable captured by value , we must add the keyword mutable
v1 = 42;
auto fm = [v1] () mutable { return ++v1; };
v1 = 0;
cout << fm() << endl << endl;
//------------------------------Lambda Return Type---------------------------
vector<int> vi = {-2, -1, 0, 1, 2};
long negativeNumberNums = count_if(vi.begin(), vi.end(), [] (int i) { return i < 0; });
cout << negativeNumberNums << endl;
// replace negative value with its absolute value
transform(vi.begin(), vi.end(), vi.begin(), [] (int i) { return i < 0 ? -i : i; });
//Here we must define a return type for the lambda, otherwise compiler cannot deduce
//the return typeactually, the gcc compiler can compile if we don't add a return type
transform(vi.begin(), vi.end(), vi.begin(), [] (int i) -> int
{ if (i < 0) return -i; else return i; });
cout << endl;
//------------------------------------bind, ref--------------------------------
//bind and placeholders are defined in functional header.
using std::placeholders::_1;
using std::placeholders::_2;
//sort by size, this bind may be unnecessary but it invert the meaning of isShorter
sort(words_copy.begin(), words_copy.end(), bind(is_shorter, _2, _1));
//the bind generates a callable object that binds the second argument of check_size to
//the value of sz
auto it_wc_2= find_if(words_copy.begin(), words_copy.end(), bind(check_size, _1, sz));
//Here we can't use bind directly, because bind copies its arguments and we cannot
//copy an ostream. If we want to pass an object to bind without copying it, we must
//use the library ref function. The ref function returns an object that contains
//the given reference and that is itself copyable
//Like bind, ref and cref functions are defined in the functional header.
for_each(it_wc_2, words_copy.end(), bind(print, ref(os), _1, ' '));
cout << endl;
return 0;
}
|
- In addition to iterators that are defined for each of the containers, there are some other iterators:
(1)Insert iterators: These iterators are bound to a container and can be used to insert elements.
(2)Stream iterators: bound to input or output streams and can be used to iterate through associated IO stream.
(3)Reverse iterators: These iterators move backward, rather than forward. Only forward_list doesn’t have.
(4)Move iterators: These iterators move rather than copy their elements.
When we assign a value through an insert iterator, the iterator calls a container operation to add an element. The operations the insert iterators support are: it = t, *it, ++it, it++ There are three kinds of inserters: back_inserter, front_inserter, inserter. We can use front_inserter only if the container has push_front. Similarly, we can use back_inserter only if it has push_back.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
|
#include <iostream>
#include <algorithm>
#include <iterator>
using namespace std;
template <class T>
void print(T elements)
{
for (auto e: elements) cout << e << " ";
cout << endl;
}
int main() {
//------------------------------inserter-----------------------------
vector<int> v1;
vector<int> v2 = {1, 5, 6, 6};
// v1 must have member function push_back, and we cannot use front_inserter here
auto it1 = back_inserter(v1);
*it1 = 1;
// insert ahead of v1.begin(), duplicated elements in v2 will not be inserted
unique_copy(v2.cbegin(), v2.cend(), inserter(v1, v1.begin()));
print(v1);
//----------------istream_iterator, ostream_iterator-----------------
// An istream_iterator reads an input stream, ostream_iterator writes an output stream
// we can create an istream_iterator for any type that has an input operator >>
// Similarly, we can define an ostream_iterator for any type that has an output
// operator <<
istream_iterator<int> in_iter(cin), eof; // reads ints from cin
vector<int> v3(in_iter, eof); // construct vector from an iterator range
//while (in_iter != eof) v3.push_back(*in_iter++); // equivalently
ostream_iterator<int> out_iter(cout, " ");
for (auto e: v3) *out_iter++ = e; // assignment writes element to cout
cout << endl;
for (auto e: v3) out_iter = e; // equivalently, but not good style
cout << endl;
copy(v3.begin(), v3.end(), out_iter); // print elements easily
cout << endl;
//------------------------reverse iterators---------------------------
// reverse iterator is an iterator that traverses a container backward, from the last
// element toward the first.
// reverse iterator inverts the meaning of increment (++it) and decrement (--it)
// we obtain a reverse iterator by calling rbegin, rend, crbegin, and crend members.
vector<int> v4 = {0, 1, 2, 3, 4, 5};
for (auto r_iter= v4.crbegin(); r_iter != v4.crend(); ++r_iter) cout << *r_iter << " ";
cout << endl;
sort(v4.rbegin(), v4.rend()); // sort in reverse
print(v4);
//we can define a reverse iterator only from an iterator that support -- as well as ++
// The forward_list and stream iterators do not support
// To transform a reverse iterator to an ordinary iterator, we can use base member
string line = "we can fix it, hide it,or just remove it";
auto rcomma = find(line.crbegin(), line.crend(), ',');
cout << string(rcomma.base(), line.cend()) << endl;
return 0;
}
|
- The iterator operations required by the algorithms are grouped into five iterator categories:
1
2
3
4
5
|
Input iterator Read, but not write; single-pass, increment only
Output iterator Write, but not read; single-pass, increment only
Forward iterator Read and write; multi-pass, increment only
Bidirectional iterator Read and write; multi-pass, increment and decrement
Random-access iterator Read and write; multi-pass, full iterator arithmetic
|
A second way to classify algorithms is by whether they read, write, or reorder the elements in the sequence.
(1)Input iterators: can read elements in a sequence. An input iterator must provide
A. equality and inequality operators ==, != to compare iterators
B. prefix and postfix increment ++ to advance the iterator
C. dereference operator * to read an element; deference may appear only on right-hand side of assignment
D. the arrow operator -> as a synonym for (*it).member to dereference the iterator and fetch a member
Input iterators may be used only sequentially, only for single-pass algorithms. The find and accumulate algorithms require input iterators; istream_iterator is input iterator.
(2)Output iterators: write rather than read elements. Output iterators must provide
A. prefix and postfix increment ++ to advance the iterator
B. deference *, which may appear only as the left-hand side of an assignment
We may assign to a given value of an output iterator only once. Output iterators may be used only for single-pass algorithms. Iterators used as a destination are typically output iterators. The third parameter to copy is an output iterator; the ostream_iterator type is an output iterator.
(3)Forward iterators: can read and write a given sequence. They move in only one direction through the sequence. Forward iterators support all operations of both input iterators and output iterators. Moreover, they can read or write the same element multiple times. Therefore, we can use the saved state of a forward iterator. Hence, algorithms that use forward iterators may make multiple passes through the sequence. The replace algorithm requires a forward iterator; iterators on forward_list are forward iterators.
(4)Bidirectional iterators: can read and write a sequence forward or backward. In addition to supporting all the operations of a forward iterator, a bidirectional iterator also supports the prefix and postfix decrement -- operator. The reverse algorithm requires bidirectional iterators, and aside from forward_list, the library containers supply iterators that meet the requirements for a bidirectional iterator.
(5)Random-access iterators: provide constant-time access to any position in the sequence. These iterators support all functionality of bidirectional iterators. In addition, random-access iterators support
A. the relational operations <, <=, >, >= to compare relative positions of two iterators
B. Addition and subtraction operations +, +=, - , -= on an iterator and an integral value.
C. The subtraction operator - when applied to two iterators, which yields the distance between two iterators
D. The subscript operator iter[n] as a synonym for *(iter + n)
The sort algorithms require random-access iterator. Iterators for array, deque, string, and vector are random-access iterators, as are pointers when used to access elements of a built-in array.
- Algorithm Parameter Patterns. Most of algorithms have one of the following forms:
1
2
3
4
|
alg(beg, end, other args);
alg(beg, end, dest, other args);
alg(beg, end, beg2, other args);
alg(beg, end, beg2, end2, other args);
|
The alg is the name of the algorithm, beg and end denote the input range. In addition to these iterator parameters, some algorithms take additional, noniterator parameters.
(1)Algorithms with a single destination iterator. A dest parameter is an iterator that denotes a destination in which the algorithm can write its output. Algorithms assume that it is safe to write as many elements as needed. More commonly, dest is bound to an insert iterator or an ostream_iterator.
(2)Algorithms with a second input sequence. The input range denoted by [beg, end), and a second input range denoted by [beg2, end2). These algorithms assume that the range starting at beg2 is at least as large as the one denoted by beg and end.
- Separate from parameter conventions, the algorithms also conform to a set of naming and overload conventions.
(1)Use Overloading to Pass a Predicate. Algorithms that take a predicate to use in place of the < or == operator, typically are overloaded. One version of the function uses the element type’s operator to compare elements; the second takes an extra parameter that is a predicate to use in place of < or ==
1
2
|
unique(beg, end); // uses the == operator to compare the elements
unique(beg, end, comp); // uses comp to compare the elements
|
(2)Algorithms with _if Versions. Algorithms take an element value typically have a second named version that take a predicate in place of the value. These algorithms provide a named version rather than an overloaded one because both versions of the algorithms take the same number of arguments.
1
2
|
find(beg, end, val); // find the first instance of val in the input range
find_if(beg, end, pred); // find the first instance for which pred is true
|
(3)Algorithms with _copy Versions. By default, algorithms that rearrange elements write the rearranged elements back into the given input range. These algorithms provide a second version that writes to a specified output destination.
1
2
|
reverse(beg, end); // reverse the elements in the input range
reverse_copy(beg, end, dest);// copy elements in reverse order into dest
|
Some algorithms provide both _copy and _if versions. They take a destination iterator and a predicate.
1
2
3
4
|
// removes the odd elements from v1
remove_if(v1.begin(), v1.end(), [](int i) { return i % 2; });
// copies only the even elements from v1 into v2; v1 is unchanged
remove_copy_if(v1.begin(), v1.end(), back_inserter(v2), [](int i) { return i % 2; });
|
- Unlike other containers, list and forward_list define several algorithms as members. In particular, the list types define their own versions of sort, merge, remove, reverse, and unique. The generic sort algorithm requires random-access iterators, so sort can not be used.
The list-specific algorithms can achieve much better performance than the corresponding generic versions. Most of the list-specific algorithms are similar to generic algorithms. However, an important difference is that the list versions change the underlying container. For example, the remove algorithm do remove the elements.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
#include <iostream>
#include <list>
#include <forward_list>
#include <algorithm>
using namespace std;
int main() {
int arr[] = {6, 4, 1, 3, 2, 5};
// range constructor [first, last)
list<int> lst1(arr, arr + 3);
list<int> lst2(arr + 3, arr + 6);
//-------------------------merge-----------------------
// all elements will be combined into lst1, lst2 will be clear
lst1.merge(lst2);
for_each(lst1.begin(), lst1.end(), [] (int n) { cout << n << " "; });
cout << endl;
cout << "lst2 size: " << lst2.size() << endl;
// generic merge algorithm, the input sequences are unchanged
char first[] = {'a', 'b', 'c'};
char second[] = {'x', 'y', 'z'};
vector<char> vec(6);
merge(first, first + 3, second, second + 3, vec.begin());
for_each(vec.begin(), vec.end(), [] (int n) { cout << n << " "; });
cout << endl;
//------------------------remove_if-----------------------
// list-specific algorithms do remove elements, the erase operation is not needed
lst1.remove_if([](int i) { return i % 2; });
for_each(lst1.begin(), lst1.end(), [] (int n) { cout << n << " "; });
cout << endl;
//--------------------------sort---------------------------
lst1.sort();
for_each(lst1.begin(), lst1.end(), [] (int n) { cout << n << " "; });
cout << endl << endl;
//--------------------------splice-------------------------
list<int> lst3(arr + 3, arr + 5);
for_each(lst3.begin(), lst3.end(), [] (int n) { cout << n << " "; });
cout << endl;
// move all elements from lst3 to lst1
lst1.splice(lst1.end(), lst3);
for_each(lst1.begin(), lst1.end(), [] (int n) { cout << n << " "; });
cout << endl;
cout << "lst3 size: " << lst3.size() << endl;
// move one element denoted by last to lst3
auto last = --lst1.end();
lst3.splice(lst3.begin(), lst1, last);
for_each(lst1.begin(), lst1.end(), [] (int n) { cout << n << " "; });
cout << endl;
// move a range of elements into lst3
lst3.splice(lst3.end(), lst1, lst1.begin(), lst1.end());
for_each(lst3.begin(), lst3.end(), [] (int n) { cout << n << " "; });
cout << endl << endl;
//------------------------splice_after----------------------
forward_list<int> flst1 = {4, 5, 6};
forward_list<int> flst2 = {9, 8, 7};
// move elements after flst1.begin(), so flst1.end() can not be used here
flst1.splice_after(flst1.begin(), flst2);
for_each(flst1.begin(), flst1.end(), [] (int n) { cout << n << " "; });
cout << endl;
return 0;
}
|
Associative Containers
- Elements in an associative container are stored and retrieved by a key; elements in a sequential container are stored and accessed sequentially by their position in the container. Associative containers support efficient lookup and retrieval by a key. The two primary associative-container types are map and set.
The elements in a map are key-value pairs: The key serves as an index into the map, the value represents the data associated with that index. A set element contains only a key; a set supports efficient queries as to whether a given key is present.
In the eight associative containers, each container is
(1) a set or a map
(2) requires unique keys or allow multiple keys
(3) stores the elements in order or not.
1
2
3
4
5
6
7
8
9
|
map associative array; holds key-value pairs
set container in which the key is the value
multimap map in which a key can appear multiple times
multiset set in which a key can appear multiple times
unordered_map map organized by a hash function
unordered_set set organized by a hash function
unordered_multimap Hashed map; keys can appear mutliple times
unordered_multiset Hashed set; keys can appear multiple times
|
- For the ordered containers: map, multimap, set, and multiset—the key type must define a way to compare the elements. By default, the library use < operator to compare the keys. The library type pair is defined in utility header, it holds two data members named first and second.
Note: the value_type of a map is a pair and that we can change the value but not the key member of that pair.
When we use an iterator to traverse a map, multimap, set, or multiset, the iterators yield elements in ascending key order.
C++ 11, the easiest way to create a pair is to use brace initialization inside the argument list.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
|
#include <iostream>
#include <map>
#include <set>
#include <vector>
#include <sstream>
#include <iterator>
#include <algorithm>
using namespace std;
// keep only words, transform words to lower case
void prepareForSentence(string &sentence) {
sentence.replace(sentence.find("'s"), 2, " ");
for (auto &s : sentence) {
if ((s < 'A' || s > 'Z') && (s < 'a' || s > 'z')){
s = ' ';
}
}
transform(sentence.begin(), sentence.end(), sentence.begin(), ::tolower);
}
struct Sales_data
{
public:
Sales_data() = default; // C++ 11, the default constructor
Sales_data(const string &s, unsigned n, double p): bookNo(s), units_sold(n),
revenue(p*n) { }
string isbn() const { return this->bookNo; }
double avg_price() const { return units_sold ? revenue/units_sold : 0; }
private:
string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
bool compareIsbn(const Sales_data &lhs, const Sales_data &rhs) {
return lhs.isbn() < rhs.isbn(); }
pair<string, int> process(vector<string> &v){
if (v.empty())
return pair<string, int>(); // explicitly constructed return value
else if (v.size() == 1)
return make_pair(v.front(), v.front().size());
else if (v.size() == 2)
return pair<string, int>(v.back(), v.back().size());
else
return {v.back(), v.back().size()}; // list initialize
};
int main() {
//-----------------------------------map, set------------------------------------
// Although set type define both the iterator and const_iterator type, they give us
// read-only access to the set elements. Just as we cannot change the key part of
// a map element, the keys in a set are const.
string sentence = "A \"map\" is a collection of key-value pairs. For example, each"
" pair might contain a person's name as a key and a phone number as its value.";
prepareForSentence(sentence);
istringstream ss(sentence);
vector<string> words;
copy(istream_iterator<string>(ss), istream_iterator<string>(), back_inserter(words));
set<string> exclude = {"its", "pairs"}; // using a set is better thant a vector here
map<string, size_t> word_count = {{"a", 0}, {"of", 0}};
for_each(words.begin(), words.end(), [&word_count, exclude] (string s) {
if (exclude.find(s) == exclude.end()) ++word_count[s]; });
// pair is a template type that holds two data elements named first and second
// iterators yield elements in ascending key order
typedef pair<string, size_t> StrCount;
for_each(word_count.begin(), word_count.end(), [] (StrCount w) {
cout << w.first << " " << w.second << endl; });
//---------------Adding elements to set: insert, emplace
vector<int> vec = {2, 4, 6, 8};
set<int> set2;
set2.insert(vec.cbegin(), vec.cend());
set2.insert({1, 3, 5, 7});
// result indicate whether the element is inserted
pair<set<int>::iterator, bool> result = set2.insert(1);
cout << endl << "insert result:" << *result.first << " " << result.second << endl;
multiset<int> mset2(set2.begin(), set2.end());
// For multiset and multimap, insert return an iterator to the new element
auto mit = mset2.insert(1);
cout << "insert value:" << *mit << endl;
//----------------Remove elements: erase
// return size_type indicating the number of elements removed
multiset<int>::size_type rr = mset2.erase(1);
cout << "erase result:" << rr << endl;
// return an iterator to element after begin() or end().
auto afterIt = mset2.erase(mset2.begin());
mset2.erase(mset2.begin(), mset2.end()); // remove all elements
//----------------Adding elements to map: insert
word_count.insert({"true", 1});
word_count.insert(make_pair("false", 1));
word_count.insert(pair<string, size_t>("lose", 1));
word_count.insert(map<string, size_t>::value_type("win", 1));
//---------------subscript operation for map and unordered_map that is not const
// If the key is not present, a new element is created and inserted,
// the associated value is value initialized
cout << "mapped value:" << word_count["Anna"] << endl;
auto mappedResult = word_count["Anna"] = 10;
cout << "mapped value:" << mappedResult << endl;
//---------------------------------multimap, multiset------------------------------
vector<int> ivec = {1, 2, 1, 2, 1};
set<int> iset(ivec.begin(), ivec.end());
multiset<int> miset(ivec.begin(), ivec.end());
cout << endl << "iset:" << iset.size() << " miset:" << miset.size() << endl;
// We can't directly define a multiset of Sales_data because it doesn't have
// a < operator. Also, we cannot define a map from list<int>::iterator to int.
// But vector<int>::iterator is ok. However, we can use our own operation by
// defining the multiset with two types: key type, comparison type.
using compareType = bool (*)(const Sales_data &lhs, const Sales_data &rhs);
multiset<Sales_data, compareType> books(compareIsbn);
// When we use decltype to form a function pointer, we must add * to indicate
// that we are using a pointer to the given function type. We initialize
// bookstore from compareIsbn means that when we add elements to
// bookstore, those bookstore will be ordered by calling compareIsbn.
multiset<Sales_data, decltype(compareIsbn)*> bookstore(compareIsbn);
bookstore.insert(Sales_data("a10", 10, 50));
bookstore.insert(Sales_data("a52", 5, 30));
bookstore.insert(Sales_data("a05", 13, 20));
for_each(bookstore.begin(), bookstore.end(), [](Sales_data data) {
cout << data.isbn() << " " << data.avg_price() << endl;});
//------------find, count, lower_bound, upper_bound, equal_range
set<int> se = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
cout << endl << "set find:" << *se.find(0) << ", count:" << se.count(0) << endl;
// Using find Instead of Subscript for the map without change it
if (word_count.find("foobar") == word_count.end())
cout << "foobar is not in the map" << endl;
// When a multimap or multiset has multiple elements of a given key,
// those elements will be adjacent
multiset<int> ms(ivec.begin(), ivec.end());
int search_item = 2;
auto entries = ms.count(search_item);
auto ms_it = ms.find(search_item);
cout << endl << "find elements:";
while (entries) {
cout << *ms_it << " ";
++ms_it; // advance to the next element
--entries;
}
cout << endl;
// If the key is in the container, the iterator returned from lower_bound will refer
// to the first instance of that key and the iterator returned from upper_bound
// will refer just after the last instance of the key. If the element is not in the
// multimap, then lower_bound and upper_bound will return equal iterators; both will
// refer to the point at which the key can be inserted without disrupting the order.
// Note: If lower_bound and upper_bound return the same iterator, then the given key
// is not in the container
for (auto beg = ms.lower_bound(search_item), end = ms.upper_bound(search_item);
beg != end; ++beg)
cout << *beg << " ";
cout << endl;
// If the key is present, the first iterator refers to the first instance of the key
// and the second iterator refers one past the last instance of the key. If no
// matching element is found, both the first and second
// iterators refer to the position where this key can be inserted.
for (auto pos = ms.equal_range(search_item); pos.first != pos.second; ++pos.first)
cout << *pos.first << " ";
cout << endl;
//---------------------------------------pair---------------------------------------
pair<string, size_t> counter; // value initialized, empty string and 0
pair<string, string> author{"James", "Joyce"}; // initialize with defined value
// C++ 11, we can list initialize a return value to return a pair
vector<string> st = {"hello", "world"};
pair<string, int> unknown = process(st);
cout << endl << "pair:" << unknown.first << " " << unknown.second << endl << endl;
//----------------------Associative Container Additional Type Aliases----------------
set<string>::value_type v1; // v1 is a string
set<string>::key_type v2; // v2 is a string
map<string, int>::value_type v3; // v3 is a pair<const string, int>
map<string, int>::key_type v4; // v4 is a string
map<string, int>::mapped_type v5; // v5 is an int
//---------------------The Word Transformation Problem
map<string, string> trans_map = {
{"brb", "be right back"},
{"k", "okay?"},
{"y", "why"},
{"r", "are"},
{"u", "you"},
{"pic", "picture"},
{"thk", "thanks!"},
{"18r", "later"},
};
string msg = "y dont u send me a pic";
istringstream stream(msg);
string word;
bool firstword = true;
while (stream >> word) {
if (firstword)
firstword = false;
else
cout << " ";
string transformed = word;
auto map_it = trans_map.find(word);
if (map_it != trans_map.cend())
transformed = map_it->second;
cout << transformed;
}
cout << endl;
return 0;
}
|
- Rather than using a comparison operation to organize their elements, the unordered associative containers use a hash function and the key type’s == operator. Although hashing gives better average case performance in principle, achieving good results in practice often requires a fair bit of performance testing and tweaking. As a result, it is usually easier and better performance to use an ordered container.
The unordered containers are organized as a collection of buckets, each of which holds zero or more elements. These containers use a hash function to map elements to buckets. The container puts all of its elements with a given hash value into the same bucket. As a result, the performance of an unordered container depends on the quality of its hash function and on the number and size of its buckets.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
c.bucket_count() Number of buckets in use
c.max_bucket_count() Largest number of buckets this container can hold
c.bucket_size(n) Number of elements in the nth bucket
c.bucket(k) Bucket in which elements with key k would be found
local_iterator Iterator type that can access elements in a bucket
const_local_iterator const version of the bucket iterator
c.begin(n), c.end(n) Iterator to the first, one past the last element in bucket n
c.cbegin(n), c.cend(n) Return const_local_iterator
c.load_factor() average number of elements per bucket. Returns float.
c.max_load_factor() average bucket size that c tries to maintain. c add buckets
to keep load_factor <= max_load_factor. Returns float.
c.rehash(n) Reorganize storage so that bucket_count >= n and
bucket_count > size/max_load_factor
c.reserve(n) Reorganize so that c can hold n elements without a rehash
|
By default, unordered containers use == operator on the key type to compare elements. They also use an object of type hash<key_type> to generate the hash code for each element. The library supplies versions of the hash template for the built-in types, including pointers. It also define hash for some library types, including string and smart pointer. If we want to define an unordered container that uses a our own class type for its key type, we must supply our own version of the hash template. Instead of using the default hash, we can use a strategy similar to the one we used to override the default comparison operation on keys for the ordered containers.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
#include <iostream>
#include <unordered_set>
using namespace std;
struct Sales_data
{
public:
Sales_data() = default;
Sales_data(const string &s, unsigned n, double p): bookNo(s), units_sold(n),
revenue(p*n) { }
string isbn() const { return this->bookNo; }
double avg_price() const { return units_sold ? revenue/units_sold : 0; }
private:
string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
// To use Sales_data as the key of unordered container, we need to supply functions to
// replace both the == operator and to calculate a hash code.
size_t hasher(const Sales_data &sd)
{
return hash<string>()(sd.isbn());
}
bool eqOp(const Sales_data &lhs, const Sales_data &rhs)
{
return lhs.isbn() == rhs.isbn();
}
int main() {
using SD_multiset= unordered_multiset<Sales_data, decltype(hasher)*, decltype(eqOp)*>;
// bucket size and pointers to hash function and equality operator
SD_multiset bookstore(42, hasher, eqOp);
return 0;
}
|
The advantages of an unordered container:
(1)userful when when we have a key type which there is no obvious ordering relationship among the elements
(2)userful for applications in which the cost of maintaining the elements in order is prohibitive
The advantages of the ordered version:
(1)iterators for ordered containers access elements in order by key
(2)we can directly define an ordered container that uses a our own class types for its key type
Dynamic Memory
- In addition to supporting automatic and static objects, C++ lets us allocate objects dynamically. Dynamically allocated objects have a lifetime that is independent of where they are created; they exist until they are explicitly freed. To make using dyanmic objects safer, the library define two smart pointer types that manage dynamically allocated objects. Smart pointers ensure that objects to which they point are automatically freed when it is appropriate to do so.
Our programs have used only static or stack memory. Static memory is used for local static objects, for class static data members, and for variables defined outside any function. Stack memory is used for nonstatic objects defined inside functions. Objects allocated in static or stack memory are automatically created and destroyed by the compiler. Stack objects exist only while the block in which they are defined is executing; static objects are allocated before they are used, and they are destroyed when the program ends. In addition to static or stack memory, every program also has a pool of memory that it can use. This memory is referred to as the free store or heap. Programs use heap for objects they dynamically allocate—that is, for objects that the program allocates at run time. Program must explicitly destroy such objects when they are no longer needed.
-
In C++, dynamic memory is managed through a pair of operators: new, which allocates, and optionally initializes, an object in dynamic memory and returns a pointer to that object; and delete, which takes a pointer to a dynamic object, destroys that object, and frees the associated memory.
Dynamic memory is problematic because it is surprisingly hard to ensure that we free memory at the right time. Either we forget to free the memory, in which case we have a memory leak, or we free the memory when there are still pointers referring to that memory, in which case we have a pointer refers to memory that is invalid.
C++ 11, the new library provides two smart pointer types that manage dynamic objects. A smart pointer acts like a regular pointer with the important exception that it automatically deletes the objects to which it points. Two kinds of smart pointers: shared_ptr, which allows multiple pointers to refer to the same object, and unique_ptr, which “owns” the object to which it points. The library also defines a companion class named weak_ptr that is a weak reference to an object managed by a shared_ptr. All three are defined in the memory header.
-
We can think of a shared_ptr as if it has an associated counter, usually referred to as a reference count. The counter is incremented when we use the shared_ptr to initialize another shared_ptr, when we use it as the right-hand operand of an assignment, or when we pass it to or return it from a function by value. The counter is decremented when we assign a new value to the shared_ptr and when the shared_ptr itself is destroyed, such as when a local shared_ptr goes out of scope. Once a shared_ptr counter goes to zero, the shared_ptr automatically frees the object that it manages.
Note: If you put shared_ptr in a container, and you subsequently need to use some, but not all, of the elements, remeber to erase the elements you no longer need.
Attention: If there is a circular reference, the shared_ptr object can not be recycled. Also, shared_ptr is not appropriate for parallel programming, this can be solved by locking, but the performance is still a problem. (this paragraph is not in the book)
Programs tend to use dynamic memory for one of three purposes:
(1)They don’t know how many objects they’ll need
(2)They don’t know the precise type of the objects they need
(3)They want to share data between several objects
One common reason to use dynamic memory is to allow multiple objects to share the same state.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
|
#include <iostream>
#include <memory>
#include <vector>
using namespace std;
class StrBlob {
public:
typedef vector<string>::size_type SIZE_TYPE;
// allocate an empty vector
StrBlob(): data(make_shared<vector<string>>()) {}
// use initializer_list as parameter
StrBlob(initializer_list<string> il): data(make_shared<vector<string>>(il)) {}
SIZE_TYPE size() const { return data->size(); }
bool empty() const { return data->empty(); }
void push_back(const string &t) { data->push_back(t); }
void pop_back() { check(0, "pop_back on empty StrBlob"); data->pop_back(); }
string& front() { check(0, "front on empty StrBlob"); return data->front(); }
string& back() { check(0, "back on empty StrBlob"); return data->back(); }
private:
shared_ptr<vector<string>> data;
void check(SIZE_TYPE i, const string &msg) const; // throws msg if data[i] is invalid
};
void StrBlob::check(SIZE_TYPE i, const string &msg) const {
if (i >= data->size())
throw out_of_range(msg);
}
int main() {
//-----------------------------shared_ptr----------------------------
// can point at a string, default holds a null pointer
shared_ptr<string> p1;
if (p1 && p1->empty())
{
*p1 = "hi"; // dereference to assign a new value
}
//----------------------------make_shared-----------------------------
// allocate and initialize an object in dynamic memory and return a shared_ptr
// that point to that object
shared_ptr<int> p3 = make_shared<int>(42);
shared_ptr<string> p4 = make_shared<string>(10, '9');
shared_ptr<int> p5 = make_shared<int>();
// use auto to define an object to hold the result of make_shared
auto p6 = make_shared<vector<string>>();
auto p = make_shared<int>(42);
auto q(p); // p and q point to the same object
auto r = make_shared<int>(42);
// increase count for object to which q point;
// reduce count of object to which r had pointed.
r = q;
StrBlob b1;
{
StrBlob b2 = {"a", "an", "the"};
b1 = b2; // b1 and b2 share the same data, no deep copy
b2.push_back("about");
}
cout << "b1 size:" << b1.size() << endl;
return 0;
}
|
- By default, dynamically allocated objects are default initialized, which means that objects of built-in or compound type have undefined value; objects of class type are initialized by their default constructor.
C++ 11, we can initialize a dynamically allocated object using direct initialization. We can use traditional construction, and under the new standard, we can also use list initialization.
For class type (such as string) that define their own constructor, requesting value initialization is of no consequence; regardless of form, the object is initialized by the default constructor.
In the case of built-in type the difference is significant; a value-initialized object of built-in type has a well-defined value but a default-initialized object does not.
C++ 11, when we use auto to deduce the type of object we want to allocate, we must use parentheses, not curly braces.
Once a program has used all of its available memory, new expressions will fail. If new is unable to allocate requested storage, it throws an exception of type bad_alloc. We can prevent new from throwing an exception by using a different form of new which is referred to as placement new. A placement new expression lets us pass additional arguments to new.
We return memory to the system through a delete expression. A delete expression takes a pointer to the object we want to free. A delete expression peforms two actions: It destroys the object to which its given pointer points, and it frees the corresponding memory.
The pointer we pass to delete must either point to dynamically allocated memory or be a null pointer. Deleting a pointer to memory that was not allocated by new, or deleting the same pointer value more than once, is undefined.
Memory that is managed through a shared_ptr is automatically deleted when last shared_ptr is destroyed. A dynamic object managed through a built-in pointer exists until it is explicitly deleted. Functions that return pointers to dynamic memory put a burden on their callers, the caller must remember to delete the memory.
There are three common problems with using new and delete to manage dynamic memory:
(1)Forgetting to delete memory, known as “memory leak” because the memory is never returned to the free store.
(2)Using an object after is has been deleted. This error can sometimes be detected by making the pointer null after the delete.
(3)Deleting the same memory twice. Then the free store may be corrupted.
You can avoid all of these error-prone problems by using smart pointers exclusively.
When we delete a pointer, that pointer becomes invalid. Although it is invalid, the pointer continues to hold the address of the dynamic memory. After delete, the pointer becomes what is referred to as a dangling pointer. A dangling pointer is one that refers to memory that once held an object but no longer does so. We can avoid this problem by deleting the memory associated with a pointer just before the pointer itself goes out of scope. If we need to keep the pointer around, we can assign nullptr to the pointer after we use delete.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
#include <iostream>
#include <vector>
using namespace std;
int* factory () {
return new int(1024); // caller is responsible for deleting this memory
}
int main() {
int *pi = new int; // pi points to a dynamically allocated, unnamed, uninitialized int
int *pi2 = new int(); // value initialized to 0
string *ps = new string; // default initialized to empty string
string *ps2 = new string(); // value initialized to empty string
int *pix = new int(1024); // traditional construction with parentheses
string *psx = new string(10, '9');
vector<int> *pv = new vector<int>{0,1,2}; // list initialize with curly braces
auto ap = new auto(2); // must use parentheses when using auto, ap is int*
const int *pci = new const int(1024); // use new to allocate const object
int *p1 = new int; // if allocation fail, throw std::bad_alloc
int *p2 = new (nothrow) int; // if allocation fail, return a null pointer
delete p1; // the deleting object must point to dynamically allocated object or be null
delete p2;
p2 = nullptr; // do this can prevent the dangling pointer
delete pci; // delete the const object, but we cannot make it nullptr
{
int *pf = factory();
//remember to free memory that we no longer need.
//Or the pointer will be a dangling pointer.
delete pf;
}
{
int *p(new int(42));
auto q = p;
delete p;
p = nullptr; // reset p has no effect on q which may be a dangling pointer
}
return 0;
}
|
- Don’t mix ordinary pointers and smart pointers. It is dangerous to use a built-in pointer to access an object owned by a smart pointer, because we may not know when that object is destroyed.
The function get() return a built-in pointer to the object that the smart pointer is managing. The code that use the return from get must not delete that pointer. And, never use get to initialize or assign to another smart pointer.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
#include <iostream>
#include <memory>
using namespace std;
void process(shared_ptr<int> ptr){
// ptr is created and initialized when process is called
// use ptr
// ptr goes out of scope and is destroyed
}
int main() {
// use direct initialization, can not use implicitly convert: p1 = new int(1024);
shared_ptr<int> p2(new int(42));
process(p2); // ok, after process, reference count of p2 is 1
int *x(new int(1024)); // dangerous, x is a plain pointer
// legal, but the memory will be deleted by this temporary shared_ptr
process(shared_ptr<int>(x));
int j = *x; // undefined: x is a dangling pointer
shared_ptr<int> p1(new int(32));
{
int *p = p1.get(); // ok, but don't use p in any way that might delete its pointer
{
// undefined: two independent shared_ptr point to the same memory
shared_ptr<int>(p);
// block ends, p is destroyed
}
int foo = *p; // undefined: the memory was freed
}
if (!p2.unique())
p2.reset(new int(24)); // if not alone, allocate a new copy
*p2 += 10; // now that we know we're the only pointer, okay to change this object
return 0;
}
|
- The program that use exception handling to continue processing after an exception occurs need to ensure that resources are properly freed. One easy way to make sure resources are freed is to use smart pointers. In contrast, memory that we manage directly is not automatically freed when an exception occurs.
1
2
3
4
5
6
|
void f()
{
int *ip = new int(42);
// code that throws an exception that is not caught inside this function
delete ip; // if exception occurs, this memory can never be freed
}
|
- By default, shared_ptr assume that they point to dynamic memory. Hence, by default, when a shared_ptr is destroyed, it executes delete on the pointer. When we create a shared_ptr, we can pass an optional argument that points to a deleter function. Then the deleter function in place of delete will be called when the shared_ptr is destroyed.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
#include <iostream>
#include <memory>
using namespace std;
struct Connection {
string ip;
int port;
Connection(string _ip, int _port) : ip(_ip), port(_port) {}
void Connect() { cout << "connect!" << endl;}
void Disconnect() { cout << "disConnect!" << endl;}
};
void EndConnection(Connection* conn) {
conn->Disconnect();
cout << "endConnection" << endl;
}
void f(){
shared_ptr<Connection> pConn(new Connection("127.0.0.1", 80), EndConnection);
pConn->Connect();
// if forget to call Disconnect, it still can be called
// by the deleter function when shared_ptr is destroyed
}
int main() {
f();
return 0;
}
|
-
To use smart pointer correctly, we must adhere to a set of conventions:
(1)don’t use the same built-in pointer value to initialize (or reset) more than one smart pointer
(2)don’t delete the pointer returned from get()
(3)don’t use get() to initialize or reset another smart pointer
(4)if you use a pointer returned by get(), remember that the pointer will become invalid when the last corresponding smart pointer goes away.
(5)if you use a smart pointer to manage a resource other than memory allocated by new, remember to pass a deleter.
-
C++11, a unique_ptr “owns” the object to which it points. Unlike shared_ptr, only one unique_ptr at a time can point to a given object. The object to which a unique_ptr points is destroyed when the unique_ptr is destroyed. When we define a unique_ptr, we bind it to a pointer returned by new. As with shared_ptr, we must use the direct form of initialization. Because a unique_ptr owns the object to which it points, unique_ptr does not support ordinary copy or assignment. But we can transfer ownership from one (nonConst) unique_ptr to another by calling release or reset.
There is one exception to the rule that we cannot copy a unique_ptr: We can copy or assign a unique_ptr that is about to be destroyed. In such cases, the compiler does a special kind of “copy”.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
#include <iostream>
#include <memory>
using namespace std;
// return a copy of unique_ptr from a function
unique_ptr<int> clone(int p) {
unique_ptr<int> ret(new int(p));
return ret;
}
int main() {
unique_ptr<string> p1(new string("Stegosaurus"));
// release makes p1 null, transfer ownership from p1 to p2
unique_ptr<string> p2(p1.release());
unique_ptr<string> p3(new string("Trex"));
p2.reset(p3.release()); // reset delete the memory to which p2 had pointed
auto p = p2.release(); // ok, but we must remember to delete(p)
return 0;
}
|
auto_ptr: earlier library included a class named auto_ptr that had some properties of unique_ptr. In particular, it was not possible to store an auto_ptr in a container, nor could we return one from a function. Although auto_ptr is still part of the standard library, programs should use unique_ptr instead.
The way unique_ptr manages its deleter is different from the way shared_ptr does.
1
2
3
4
5
6
7
8
9
10
|
void f(){
// unique_ptr<objT, delT> p(new objT, fcn);
// p points to an object of type objT and
// uses an object of type delT to free that object
unique_ptr<Connection, decltype(EndConnection)*> pConn(
new Connection("127.0.0.1", 80), EndConnection);
pConn->Connect();
// if forget to call Disconnect, it still can be called by
// the deleter function when pConn is destroyed
}
|
- A weak_ptr is a smart pointer that does not control the lifetime of the object to which it points. Instead, a weak_ptr points to an object that is managed by a shared_ptr. Binding a weak_ptr to a shared_ptr does not change the reference count of that shared_ptr. Once the last shared_ptr pointing to the object goes away, the object itself will be deleted. The object will be deleted even if there are weak_ptr pointing to it.
Because the object might no longer exist, we cannot use a weak_ptr to access its object directly. To access the object, we must call lock. The lock function checks whether the object to which the weak_ptr points still exists. If so, lock return a shared_ptr to the shared object.
For example, the StrBlobPtr class will store a weak_ptr to the data member. By using a weak_ptr, we don’t affect the lifetime of that vector. However, we can prevent the user from attempting to access a vector that no longer exists.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
#include <iostream>
#include <vector>
#include <memory>
using namespace std;
class StrBlobPtr {
public:
StrBlobPtr(): curr(0) {}
StrBlobPtr(shared_ptr<vector<string>> &data, size_t sz = 0): wptr(data), curr(sz) {}
string& deref() const;
StrBlobPtr& incr();
private:
shared_ptr<vector<string>> check(size_t i, const string &msg) const;
weak_ptr<vector<string>> wptr;
size_t curr;
};
shared_ptr<vector<string>> StrBlobPtr::check(size_t i, const string &msg) const {
auto ret = wptr.lock(); // is the vector still around ?
if (!ret)
throw runtime_error("unbound StrBlobPtr");
if (i >= ret->size())
throw out_of_range(msg);
return ret; // otherwise, return a shared_ptr to the vector
}
string& StrBlobPtr::deref() const {
auto p = check(curr, "dereference past end");
return (*p)[curr];
}
StrBlobPtr& StrBlobPtr::incr() {
check(curr, "increment past end of StrBlobPtr");
++curr;
return *this;
}
int main() {
auto p = make_shared<int>(42);
weak_ptr<int> wp(p); // wp weakly shares with p; use count in p is unchanged
if (shared_ptr<int> np = wp.lock()){
// inside if, np shares its object with p
}
return 0;
}
|
- The new and delete operators allocate objects one at a time. Some applications need allocate storage for many objects at once. The language defines a second kind of new expression that allocates and initializes an array of objects. The library includes a template class named allocator that lets us separate allocation from initialization. Classes that use the containers can use the default versions of operations for copy, assignment, and destruction. Classes that allocate dynamic arrays must define their own versions of these operations to manage the associated memory. So do not allocate dynamic arrays until you have read the “Copy Control” chapter.
Alghough it is common to refer to memory allocated by new T[] as a “dynamic array”, but we do not get an object with an array type. Instead, we get a pointer to the element type of the array. Becausethe allocated memory does not have an array type, we cannot call begin or end on a dynamic array. Also, we cannot use a range for to process the elements.
1
2
3
4
5
|
int size = 3;
int *pia = new int[size]; // pia points to the first of these ints
typedef int arrT[10]; // arrT names the type array of ints
int *p = new arrT;
|
Note: It is important to remember that what we call a dynamic array does not have an array type.
By default, objects allocated by new are default initialized. We can value initialize the elements in an array by following the size with an empty pair of parentheses. Under the new standard, we can provide a braced list of element initializers. When we list initialize an object of built-in type, if there are fewer initializers than elements, the remaining elements are value initialized. If there are more initializers than the given size, then the new expression throw an exception of type bad_array_length. Like bad_alloc, this type is defined in the new header.
We cannot supply an element initializer inside the parentheses, this means that we cannot use auto to allocate an array. It is legal to dynamically allocate an empty array. The new will return a valid, nonzero pointer for zero-element array. We can use this pointer in ways that we use an off-the-end iterator.
1
2
3
4
|
int *pia = new int[10]; // uninitialized int
int *pia2= new int[10](); // value initialized to 0
int *pia3= new int[10] {0,1,2,3,4,5,6,7,8}; //remaining elements are value initialized to 0
int *pia4= new int[0]; // ordinary array can not use 0 for size
|
To free a dynamic array, we use a special form of delete that includes an empty pair of square brackets []. Elements in an array are destroyed in reverse order. That is, the last element is destroyed first. The brackes when we delete a pointer to an array is essential, it is undefined if we omit the brackets.
1
2
3
4
|
typedef int arrT[42];
int *p = new arrT;
// bracket is necessary, p must point to a dynamically allocated array or be null
delete []p;
|
The library provides a version of unique_ptr that can manage arrays allocated by new. When a unique_ptr points to an array, we cannot use the dot and arrow member access operator, but we can use the subscript operator to access the elements. Unlike unique_ptr, shared_ptr provide no direct support for managing a dynamic array. If we want to use a shared_ptr to manage a dynamic array, we must provide our own deleter.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// for array, we cannot use the dot and the arrow member access operator
unique_ptr<int[]> up(new int[10]);
for (int i = 0; i != 10; ++i)
{
//When a unique_ptr points to an array, we can use subscript operator to
//access elements
up[i] = i;
}
up.release(); // automatically use delete[] to destroy it
// we must supply a deleter to use a shared_ptr
shared_ptr<int> sp(new int[10], [](int *p) { delete []p; });
// shared_ptr don't have subscript operator and don't support pointer arithmetic
for (int i = 0; i != 10; ++i)
{
*(sp.get() + i) = i; // use get to get a built-in pointer
}
sp.reset(); // use delete[] to free the array
|
- The library allocator class lets us separate allocation from construction. It provides type-aware allocation of raw, unconstructed, memory. When an allocator object allocates memory, it allocates memory that is appropriately sized and aligned to hold objects of the given type.
The memory an allocator allocates is unconstructed. We use this memory by constructing objects in that memory. It is an error to use raw memory in which an object has not been constructed.
When we finished using the objects, we must destroy the elements we constructed, which we do by calling destroy on each constructed elememt. We may destroy only elements that are acutally constructed. Once destroyed, we can either reuse the memory or return it to the system by calling deallocate. The library also defines two algorithms that can construct objects in uninitialized memory.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
#include <iostream>
#include <memory>
#include <vector>
using namespace std;
int main() {
allocator<string> alloc;
int size = 3;
auto const p = alloc.allocate(size); // allocate unconstructed memory for these strings
auto q = p; // q will point to one past the last constructed element
alloc.construct(q++); // *q is the empty string
alloc.construct(q++, 10, 'c');
alloc.construct(q++, "hi");
while (q != p)
alloc.destroy(--q); // destroy elements that are actually constructed
// Once elements have been destroyed, we can either reuse the memory or return
// it to system. p cannot be null, it must point to memory allocated by
// allocate; the size must be the same.
alloc.deallocate(p, size);
vector<int> vi = {32, 64, 128};
allocator<int> allocI;
// allocate twice as many elements as vi holds
unsigned long sizeI = vi.size() * 2;
auto pI = allocI.allocate(sizeI);
// construct elements starting at pI, the destination iterator must denote
// unconstructed memory
// unlike copy, uninitialized_copy constructs elements in its destination
// like copy, uninitialized_copy return its destination iterator
auto qI = uninitialized_copy(vi.begin(), vi.end(), pI);
// initialize remaining elements to 7
uninitialized_fill_n(qI, vi.size(), 7);
allocI.deallocate(pI, sizeI);
return 0;
}
|
- To conclude, we’ll implement a simple text-query program. This program let us search a given file for words that might occur in it. The result will be the number of times the word occurs and a list of lines on which taht word appears.
Alghough we could write our program using vector, set, and map directly, it will be more useful if we define a more abstract solution. We’ll start by designing a class to hold the input file in a way that makes querying the file easy. This class, which we’ll name TextQuery, will hold a vector and a map. The vector will hold the text of input file; the map will associate each word to the set of line numbers on which that word appears. We define a second class, named QueryResult, to hold the result of a query. Because the data that a QueryResult needs are stored in a TextQuery object, we have to decide how to access them. We could avoid making copies by returning iterators into the TextQuery object. However, what happens if the TextQuery object is destroyed. The best way is using shared_ptr to reflect the sharing in our data structures.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
|
#include <iostream>
#include <vector>
#include <set>
#include <map>
#include <memory>
#include <sstream>
#include <fstream>
using namespace std;
class QueryResult {
friend ostream& print(ostream&, const QueryResult&);
public:
using line_no = vector<string>::size_type;
QueryResult(string s, shared_ptr<set<line_no>> p, shared_ptr<vector<string>> f):
sought(s), lines(p), file(f) {}
private:
string sought; // word this query represents
shared_ptr<set<line_no>> lines; // lines it's on
shared_ptr<vector<string>> file; // input file
};
class TextQuery {
public:
using line_no = vector<string>::size_type;
TextQuery(istream &is);
QueryResult query(const string& sought) const;
private:
shared_ptr<vector<string>> file; // input file
// map of each word to the set of the lines in which that word appears
map<string, shared_ptr<set<line_no>>> wm;
};
TextQuery::TextQuery(istream &is): file(new vector<string>)
{
string text;
while (getline(is, text)) {
file->push_back(text);
unsigned long n = file->size() - 1; // the current line number
istringstream line(text);
string word;
while (line >> word) { // for each word in that line
auto &lines = wm[word]; // lines is a reference
// that pointer is null the first time we see word
if (!lines)
lines.reset(new set<line_no>);
lines->insert(n);
}
}
}
QueryResult TextQuery::query(const string &sought) const
{
// we'll return a pointer to this set if we don't find sought
static shared_ptr<set<line_no>> nodata(new set<line_no>);
// use find and not a subscript to avoid adding words to wm
auto loc = wm.find(sought);
if (loc == wm.end())
return QueryResult(sought, nodata, file);
else
return QueryResult(sought, loc->second, file);
}
string make_plural(size_t ctr, const string &word, const string &ending)
{
return (ctr > 1) ? word + ending : word;
}
ostream &print(ostream &os, const QueryResult &qr)
{
os << qr.sought << " occurs " << qr.lines->size() << " " <<
make_plural(qr.lines->size(), "time", "s") << endl;
for (auto num: *qr.lines)
os << "\t(line " << num + 1 << ") " << *(qr.file->begin() + num) << endl;
return os;
}
int main() {
string paragraph =
"nice to meet you\n"
"welcome to my office\n"
"it is very big\n"
"what do you think about it\n"
"do you like it";
istringstream file(paragraph);
ifstream file2("d:/a.txt");
TextQuery q(file);
QueryResult qr = q.query("you");
print(cout, qr);
return 0;
}
|