函数对象
一元函数
只对一个参数进行操作的函数称为一元函数。一元函数的功能可能很简单,如在屏幕上显示元素,如下所示:
// A unary function
template <typename elementType>
void FuncDisplayElement (const elementType& element)
{
cout << element << ' ';
};
该函数也可采用另一种表现形式,即其实现包含在类或结构的 operator() 中:
// Struct that can behave as a unary function
template <typename elementType>
struct DisplayElement
{
void operator () (const elementType& element) const
{
cout << element << ' ';
}
};
这两种实现都可用于 STL 算法 for_each() 中,用于将集合中的内容显示在屏幕上,每次显示一个元素,代码如下:
#include <algorithm>
#include <iostream>
#include <vector>
#include <list>
using namespace std;
// struct that behaves as a unary function
template <typename elementType>
struct DisplayElement
{
void operator () (const elementType& element) const
{
cout << element << ' ';
}
};
int main()
{
vector <int> numsInVec{ 0, 1, 2, 3, -1, -9, 0, -999 };
cout << "Vector of integers contains: " << endl;
for_each(numsInVec.begin(), // Start of range
numsInVec.end(), // End of range
DisplayElement<int>()); // Unary function object
// Display the list of characters
list <char> charsInList{ 'a', 'z', 'k', 'd' };
cout << endl << "List of characters contains: " << endl;
for_each(charsInList.begin(),
charsInList.end(),
DisplayElement<char>());
return 0;
}
打印如下:
Vector of integers contains:
0 1 2 3 -1 -9 0 -999
List of characters contains:
a z k d
第 9~15 行包含函数对象 DisplayElement,它实现了 operator()。在第 22~24 行,将这个函数对象用于了 STL 算法 std::for_each。for_each 接受 3 个参数:第 1 个参数指定范围的起点;第 2 个参数指定范围的终点;第 3 个参数是要对指定范围内的每个元素调用的函数。换句话说,这些代码将对 vectornumsInVec 中的每个元素调用 DisplayElement::operator()。
C++11 引入了 lambda 表达式,即匿名函数对象。上面的代码如果不使用结构 struct DisplayElement<T>,而使用 lambda 表达式,可极大地简化代码。第 22~24 行可以改成这样:
for_each(numsInVec.begin(),
numsInVec.end(),
[](int& element) {cout << element << ' '; });
lambda 表达式将在后面深入学习。
上面的代码也可以不使用结构 DisplayElement,而使用函数 FuncDisplayElement:
for_each (charsInList.begin(),
charsInList.end(),
FuncDisplayElement<char>);
效果是一样的。
但是,如果能够使用结构的对象来存储信息,则使用在结构中实现的函数对象的优点将显现出来。这是 FuncDisplayElement 不像结构那么强大的地方,因为结构除 operator() 外还可以有成员属性。看下面的代码:
#include<algorithm>
#include<iostream>
#include<vector>
using namespace std;
template<typename elementType>
struct DisplayElementKeepCount
{
int count;
DisplayElementKeepCount() : count(0) {} // constructor
void operator()(const elementType& element)
{
++count;
cout << element << ' ';
}
};
int main()
{
vector<int> numsInVec{ 22, 2017, -1, 999, 43, 901 };
cout << "Displaying the vector of integers: " << endl;
DisplayElementKeepCount<int> result;
result = for_each(numsInVec.begin(),
numsInVec.end(),
DisplayElementKeepCount<int>());
cout << endl << "Functor invoked " << result.count << " times";
return 0;
}
DisplayElementKeepCount 相比前一个版本稍做了修改,operator() 不再是 const 成员 函数,因为它对成员 count 进行递增(修改),以记录自己被调用用于显示数据的次数。跟上一个示例最大的不同是:这里将 DisplayElementKeepCount 用作 for_each 的返回值,然后可以打印里面的成员变量 count。
一元谓词
返回布尔值的一元函数是谓词,下面代码中的 IsMultiple 里的 operator() 返回布尔值,可用作一元谓词,在 std::find_if() 中使用一元谓词 IsMutilple,在 vector 中查找一个能被用户提供的除数整除的元素:
#include <algorithm>
#include <vector>
#include <iostream>
using namespace std;
// A structure as a unary predicate
template <typename numberType>
struct IsMultiple
{
numberType Divisor;
IsMultiple(const numberType& divisor)
{
Divisor = divisor;
}
bool operator () (const numberType& element) const
{
// Check if the divisor is a multiple of the divisor
return ((element % Divisor) == 0);
}
};
int main()
{
vector <int> numsInVec{ 25, 26, 27, 28, 29, 30, 31 };
cout << "The vector contains: 25, 26, 27, 28, 29, 30, 31" << endl;
cout << "Enter divisor (> 0): ";
int divisor = 2;
cin >> divisor;
// Find the first element that is a multiple of divisor
auto element = find_if(numsInVec.begin(),
numsInVec.end(),
IsMultiple<int>(divisor));
if (element != numsInVec.end())
{
cout << "First element in vector divisible by " << divisor;
cout << ": " << *element << endl;
}
return 0;
}
打印如下:
The vector contains: 25, 26, 27, 28, 29, 30, 31
Enter divisor (> 0): 7
First element in vector divisible by 7: 28
一元谓词被大量用于 STL 算法中。例如,算法 std::partition() 使用一元谓词来划分范围,算法 stable_partition() 也使用一元谓词来划分范围,但保持元素的相对顺序不变。诸如 std::find_if() 等查找函数以及 std::remove_if() 等删除元素的函数也使用一元谓词,其中 std::remove_if() 删除指定范围内满足谓词条件的元素。
二元函数
如果函数 f(x, y) 根据输入参数返回一个值,它将很有用。这种二元函数可用于对两个操作数执行运算,如加、减、乘、除等。
看下面的代码,这里的 operator() 接受两个参数并返回它们的积。在 std::transform() 等算法中,可使用该二元函数计算两个容器内容的乘积:
#include <vector>
#include <iostream>
#include <algorithm>
template <typename elementType>
class Multiply
{
public:
elementType operator () (const elementType& elem1,
const elementType& elem2)
{
return (elem1 * elem2);
}
};
int main()
{
using namespace std;
vector <int> multiplicands{ 0, 1, 2, 3, 4 };
vector <int> multipliers{ 100, 101, 102, 103, 104 };
// A third container that holds the result of multiplication
vector <int> vecResult;
// Make space for the result of the multiplication
vecResult.resize(multipliers.size());
transform(multiplicands.begin(), // range of multiplicands
multiplicands.end(), // end of range
multipliers.begin(), // multiplier values
vecResult.begin(), // holds result
Multiply <int>()); // multiplies
cout << "The contents of the first vector are: " << endl;
for (size_t index = 0; index < multiplicands.size(); ++index)
cout << multiplicands[index] << ' ';
cout << endl;
cout << "The contents of the second vector are: " << endl;
for (size_t index = 0; index < multipliers.size(); ++index)
cout << multipliers[index] << ' ';
cout << endl;
cout << "The result of the multiplication is: " << endl;
for (size_t index = 0; index < vecResult.size(); ++index)
cout << vecResult[index] << ' ';
return 0;
}
运行后打印如下:
The contents of the first vector are:
0 1 2 3 4
The contents of the second vector are:
100 101 102 103 104
The result of the multiplication is:
0 101 204 309 416
在这个示例中,使用算法 std::transform 将两个范围的内容相乘,并将结果存储在第三个范围中。在这里,这三个范围分别存储在类型为 std::vector 的 multiplicands、multipliers 和 vecResult 中。在第 28~32 行,使用 std::transform() 将 multiplicands 中的每个元素与 multipliers 中对应的元素相乘,并将结果存储在 vecResult 中。乘法运算是通过调用二元函数 Multiply::operator() 执行的,对源范围和目标范围内的每个元素都调用了该函数。operator() 的返回值保存在 vecResult 中。
二元谓词
接受两个参数并返回一个布尔值的函数是二元谓词。这种函数用于诸如 std::sort() 等 STL 函数中,如下代码使用了一个二元谓词,它将两个字符串都转换为小写,再对其进行比较:
#include <algorithm>
#include <string>
using namespace std;
class CompareStringNoCase
{
public:
bool operator () (const string& str1, const string& str2) const
{
string str1LowerCase;
// Assign space
str1LowerCase.resize(str1.size());
// Convert every character to the lower case
transform(str1.begin(), str1.end(), str1LowerCase.begin(),
::tolower);
string str2LowerCase;
str2LowerCase.resize(str2.size());
transform(str2.begin(), str2.end(), str2LowerCase.begin(),
::tolower);
return (str1LowerCase < str2LowerCase);
}
};
该二元谓词可用于算法 std::sort(),对包含在字符串 vector 中的动态数组进行排序,代码如下:
// Insert class CompareStringNoCase from Listing 21.6 here
#include <vector>
#include <iostream>
template <typename T>
void DisplayContents(const T& container)
{
for (auto element = container.cbegin();
element != container.cend();
++element)
cout << *element << endl;
}
int main()
{
// Define a vector of string to hold names
vector <string> names;
// Insert some sample names in to the vector
names.push_back("jim");
names.push_back("Jack");
names.push_back("Sam");
names.push_back("Anna");
cout << "The names in vector in order of insertion: " << endl;
DisplayContents(names);
cout << "Names after sorting using default std::less<>: " << endl;
sort(names.begin(), names.end());
DisplayContents(names);
cout << "Sorting using predicate that ignores case:" << endl;
sort(names.begin(), names.end(), CompareStringNoCase());
DisplayContents(names);
return 0;
}
打印如下:
The names in vector in order of insertion:
jim
Jack
Sam
Anna
Names after sorting using default std::less<>:
Anna
Jack
Sam
jim
Sorting using predicate that ignores case:
Anna
Jack
jim
Sam
第二次是在使用默认排序谓词 less<T> 重新排序(如第 29 行所示)后进行的;输出表明,jim 没有紧跟在 Jack 后面,这是因为使用 string::operator<排序时区分大小写。为确保 jim 紧跟在 Jack 后面(虽然大小写不同),最后一次显示内容前,使用了排序谓词 CompareStringNoCase<>,如第 33 行所示。
很多 STL 算法都使用二元谓词。例如,删除相邻重复元素的 std::unique( )、排序算法 std::sort( )、排序并保持相对顺序的 std::stable_sort( )以及对两个范围进行操作的 std::transform( ),这些 STL 算法都需要使用二元谓词。
lambda 表达式
可将 lambda 表达式视为包含公有 operator() 的匿名结构(或类),从这种意义上说,lambda 表达式属于上一节介绍的函数对象。
如何定义 lambda 表达式
lambda 表达式的定义必须以方括号([])打头。这些括号告诉编译器,接下来是一个 lambda 表达式。方括号的后面是一个参数列表,该参数列表与不使用 lambda 表达式时提供给operator() 的参数列表相同。
一元函数对应的 lambda 表达式
与一元 operator(Type) 对应的 lambda 表达式接受一个参数,其定义如下:
[](Type paramName) { // lambda expression code here; }
请注意,如果您愿意,也可按引用传递参数:
[](Type& paramName) { // lambda expression code here; }
如下代码演示了如何在算法 for_each() 中使用 lambda 表达式来显示标准模板库(STL)容器的内容:
#include <algorithm>
#include <iostream>
#include <vector>
#include <list>
using namespace std;
int main()
{
vector <int> numsInVec{ 101, -4, 500, 21, 42, -1 };
list <char> charsInList{ 'a', 'h', 'z', 'k', 'l' };
cout << "Display elements in a vector using a lambda: " << endl;
// Display the array of integers
for_each(numsInVec.cbegin(), // Start of range
numsInVec.cend(), // End of range
[](const int& element) {cout << element << ' '; }); // lambda
cout << endl;
cout << "Display elements in a list using a lambda: " << endl;
// Display the list of characters
for_each(charsInList.cbegin(), // Start of range
charsInList.cend(), // End of range
[](auto& element) {cout << element << ' '; }); // lambda
return 0;
}
输出:
Display elements in a vector using a lambda:
101 -4 500 21 42 -1
Display elements in a list using a lambda:
a h z k l
上面第二个 lambda 表达式通过关键字 auto 利用了编译器的类型自动推断功能,遵循 C++14 的编译器都支持这种对 lambda 表达式的改进,也就是说,编译器将这样解读上述 lambda 表达式:
for_each (charsInList.cbegin (), // Start of range
charsInList.cend (), // End of range
[](const char& element) {cout << element << ' '; } );
一元谓词对应的 lambda 表达式
谓词可帮助你做出决策。一元谓词是返回 bool 类型(true 或 false)的一元表达式。lambda 表达式也可返回值,例如,下面的 lambda 表达式在 num 为偶数时返回 true:
[](int& num) {return ((num % 2) == 0); }
在这里,返回值的性质让编译器知道该 lambda 表达式的返回类型为 bool。
在算法中,可将 lambda 表达式用作一元谓词。例如,可在 std::find_if( )中使用上述 lambda 表达式找出集合中的偶数:
#include<algorithm>
#include<vector>
#include<iostream>
using namespace std;
int main()
{
vector<int> numsInVec{ 25, 101, 2017, -50 };
auto evenNum = find_if(numsInVec.cbegin(),
numsInVec.cend(), // range to find in
[](const int& num){return ((num % 2) == 0); });
if (evenNum != numsInVec.cend())
cout << "Even number in collection is: " << *evenNum << endl;
return 0;
}
输出:
Even number in collection is: -50
上述代码不但演示了如何将 lambda 表达式用作一元谓词,还在该 lambda 表达式中使用了 const。务必使用 const 来限定输入参数,在输入参数为引用时尤其如此。这样可避免无意间修改容器中元素的值。
通过捕获列表接受状态变量的 lambda 表达式
在前面的代码中,你创建了一个一元谓词,它在整数能被 2 整除时返回 true。如果要让它更通用,在数字能被用户指定的除数整除时返回 true,该如何办呢?为此,需要让 lambda 表达式接受该“状态”—除数:
int divisor = 2; // initial value
…
auto element = find_if (begin of a range,
end of a range,
[divisor](int dividend){return (dividend % divisor) == 0; } );
一系列以状态变量的方式传递的参数([…])也被称为 lambda 表达式的捕获列表(capture list)。
下面的代码演示了如何使用 lambda 表达式,根据状态变量在集合中查找可被用户提供的除数整除的元素:
#include <algorithm>
#include <vector>
#include <iostream>
using namespace std;
int main()
{
vector <int> numsInVec{ 25, 26, 27, 28, 29, 30, 31 };
cout << "The vector contains: {25, 26, 27, 28, 29, 30, 31}";
cout << endl << "Enter divisor (> 0): ";
int divisor = 2;
cin >> divisor;
// Find the first element that is a multiple of divisor
vector <int>::iterator element;
element = find_if(numsInVec.begin()
, numsInVec.end()
, [divisor](int dividend){return (dividend % divisor) == 0; });
if (element != numsInVec.end())
{
cout << "First element in vector divisible by " << divisor;
cout << ": " << *element << endl;
}
return 0;
}
输出如下:
The vector contains: {25, 26, 27, 28, 29, 30, 31}
Enter divisor (> 0): 7
First element in vector divisible by 7: 28
divisor 是一个状态变量,相当于前面的 IsMultiple::Divisor,因此状态变量类似于 C++11 之前的函数对象类中的成员。您可以将状态传递给 lambda 表达式,并根据状态的性质相应地使用它。
lambda 表达式的通用语法
lambda 表达式总是以方括号打头,并可接受多个状态变量,为此可在捕获列表([…])中指定这些状态变量,并用逗号分隔:
[stateVar1, stateVar2](Type& param) { // lambda code here; }
如果要在 lambda 表达式中修改这些状态变量,可添加关键字 mutable:
[stateVar1, stateVar2](Type& param) mutable { // lambda code here; }
这样,便可在 lambda 表达式中修改捕获列表([])中指定的变量,但离开 lambda 表达式后,这些修改将无效。要确保在 lambda 表达式内部对状态变量的修改在其外部也有效,应按引用传递它们:
[&stateVar1, &stateVar2](Type& param) { // lambda code here; }
lambda 表达式还可接受多个输入参数,为此可用逗号分隔它们:
[stateVar1, stateVar2](Type1& var1, Type2& var2) { // lambda code here; }
如果要向编译器明确地指定返回类型,可使用->,如下所示:
[stateVar1, stateVar2](Type1 var1, Type2 var2) -> ReturnType
{ return (value or expression ); }
最后,复合语句({})可包含多条用分号分隔的语句,如下所示:
[stateVar1, stateVar2](Type1 var1, Type2 var2) -> ReturnType
{
Statement 1;
Statement 2;
return (value or expression);
}
如果 lambda 表达式包含多行代码,您必须显式地指定返回类型。
二元函数对应的 lambda 表达式
二元函数接受两个参数,还可返回一个值。与之等价的 lambda 表达式如下:
[...](Type1& param1Name, Type2& param2Name) { // lambda code here; }
下面的代码演示了一个 lambda 表达式,并在 std::transform() 中使用它将两个等长的 vector 中对 应的元素相乘,再将结果存储到第三个 vector 中:
#include <vector>
#include <iostream>
#include <algorithm>
int main()
{
using namespace std;
vector <int> vecMultiplicand{ 0, 1, 2, 3, 4 };
vector <int> vecMultiplier{ 100, 101, 102, 103, 104 };
// Holds the result of multiplication
vector <int> vecResult;
// Make space for the result of the multiplication
vecResult.resize(vecMultiplier.size());
transform(vecMultiplicand.begin(), // range of multiplicands
vecMultiplicand.end(), // end of range
vecMultiplier.begin(), // multiplier values
vecResult.begin(), // range that holds result
[](int a, int b) {return a * b; }); // lambda
cout << "The contents of the first vector are: " << endl;
for (size_t index = 0; index < vecMultiplicand.size(); ++index)
cout << vecMultiplicand[index] << ' ';
cout << endl;
cout << "The contents of the second vector are: " << endl;
for (size_t index = 0; index < vecMultiplier.size(); ++index)
cout << vecMultiplier[index] << ' ';
cout << endl;
cout << "The result of the multiplication is: " << endl;
for (size_t index = 0; index < vecResult.size(); ++index)
cout << vecResult[index] << ' ';
return 0;
}
运行后打印如下;
The contents of the first vector are:
0 1 2 3 4
The contents of the second vector are:
100 101 102 103 104
The result of the multiplication is:
0 101 204 309 416
二元谓词对应的 lambda 表达式
返回 true 或 false、可帮助决策的二元函数被称为二元谓词。这种谓词可用于 std::sort() 等排序算法中,这些算法对容器中的两个值调用二元谓词,以确定将哪个放在前面。与二元谓词等价的 lambda 表达式的通用语法如下:
[...](Type1& param1Name, Type2& param2Name) { // return bool expression; }
如下代码演示了如何将 lambda 表达式用于排序:
#include <algorithm>
#include <string>
#include <vector>
#include <iostream>
using namespace std;
template <typename T>
void DisplayContents(const T& input)
{
for (auto element = input.cbegin();
element != input.cend();
++element)
cout << *element << endl;
}
int main()
{
vector <string> namesInVec{ "jim", "Jack", "Sam", "Anna" };
cout << "The names in vector in order of insertion: " << endl;
DisplayContents(namesInVec);
cout << "Order after case sensitive sort: " << endl;
sort(namesInVec.begin(), namesInVec.end());
DisplayContents(namesInVec);
cout << "Order after sort ignoring case:" << endl;
sort(namesInVec.begin(), namesInVec.end(),
[](const string& str1, const string& str2) -> bool // lambda
{
string str1LC; // LC = lowercase
// Assign space
str1LC.resize(str1.size());
// Convert every character to the lower case
transform(str1.begin(), str1.end(), str1LC.begin(), ::tolower);
string str2LC;
str2LC.resize(str2.size());
transform(str2.begin(), str2.end(), str2LC.begin(), ::tolower);
return (str1LC < str2LC);
} // end of lambda
); // end of sort
DisplayContents(namesInVec);
return 0;
}
输出:
The names in vector in order of insertion:
jim
Jack
Sam
Anna
Order after case sensitive sort:
Anna
Jack
Sam
jim
Order after sort ignoring case:
Anna
Jack
jim
Sam
这个 lambda 表达式表明,lambda 表达式可跨越多行,但必须显式地指定返回类型,如第 29 行所示。
在 lambda 表达式中,可使用捕获列表来传递局部变量:
[Var1, Var2, ... N](Type& Param1, ... ) { ...expression ;}
要传递所有的局部变量,可使用如下语法:
[=](Type& Param1, ... ) { ...expression ;}
看如下示例代码:
#include<iostream>
using namespace std;
int main() {
int x = 10;
int y = 20;
auto lambda1 = [x, y]() {
return x + y;
};
auto lambda2 = [=]() {
return x + y;
};
cout << "lambda1:" << lambda1() << endl;
cout << "lambda2:" << lambda2() << endl;
}
STL 算法
什么是 STL 算法
查找、搜索、删除和计数是一些通用算法,其应用范围很广。STL 通过通用的模板函数提供了这些算法以及其他的很多算法,可通过迭代器对容器进行操作。要使用 STL 算法,程序员必须包含头文件 <algorithm>。
STL 算法的分类
STL 算法分两大类:非变序算法与变序算法。
非变序算法
不改变容器中元素的顺序和内容的算法称为非变序算法。主要有以下这些:
| 算法 | 描述 |
|---|---|
| 计数算法 | |
| count() | 在指定范围内查找值与指定值匹配的所有元素 |
| count_if() | 在指定范围内查找值满足指定条件的所有元素 |
| 搜索算法 | |
| search() | 在目标范围内,根据元素相等性(即运算符==)或指定二元谓词搜索第一个满足条件的元素 |
| search_n() | 在目标范围内搜索与指定值相等或满足指定谓词的 n 个元素 |
| find() | 在给定范围内搜索与指定值匹配的第一个元素 |
| find_if() | 在给定范围内搜索满足指定条件的第一个元素 |
| find_end() | 在指定范围内搜索最后一个满足特定条件的元素 |
| find_first_of() | 在目标范围内搜索指定序列中的任何一个元素第一次出现的位置;在另一个重载版本中,它搜索满足指定条件的第一个元素 |
| adjacent_find() | 在集合中搜索两个相等或满足指定条件的元素 |
| 比较算法 | |
| equal() | 比较两个元素是否相等或使用指定的二元谓词判断两者是否相等 |
| mismatch() | 使用指定的二元谓词找出两个元素范围的第一个不同的地方 |
| lexicographical_compare() | 比较两个序列中的元素,以判断哪个序列更小 |
变序算法
变序算法改变其操作的序列的元素顺序或内容,如下列出了 STL 提供的一些最有用的变序算法。
| 算法 | 描述 |
|---|---|
| 初始化算法 | |
| fill() | 将指定值分配给指定范围中的每个元素 |
| fill_n() | 将指定值分配给指定范围中的前 n 个元素 |
| generate() | 将指定函数对象的返回值分配给指定范围中的每个元素 |
| generate_n() | 将指定函数的返回值分配给指定范围中的前 n 个元素 |
| 修改算法 | |
| for_each() | 对指定范围内的每个元素执行指定的操作。当指定的参数修改了范围时,for_each 将是变序算法 |
| transform() | 对指定范围中的每个元素执行指定的一元函数 |
| 复制算法 | |
| copy() | 将一个范围复制到另一个范围 |
| copy_backward() | 将一个范围复制到另一个范围,但在目标范围中将元素的排列顺序反转 |
| 删除算法 | |
| remove() | 将指定范围中包含指定值的元素删除 |
| remove_if() | 将指定范围中满足指定一元谓词的元素删除 |
| remove_copy() | 将源范围中除包含指定值外的所有元素复制到目标范围 |
| remove_copy_if() | 将源范围中除满足指定一元谓词外的所有元素复制到目标范围 |
| unique() | 比较指定范围内的相邻元素,并删除重复的元素。该算法还有一个重载版本,它使用二元谓词来判断要删除哪些元素 |
| unique_copy() | 将源范围内的所有元素复制到目标范围,但相邻的重复元素除外 |
| 替换算法 | |
| replace() | 用一个值来替换指定范围中与指定值匹配的所有元素 |
| replace_if() | 用一个值来替换指定范围中满足指定条件的所有元素 |
| 排序算法 | |
| sort() | 使用指定的排序标准对指定范围内的元素进行排序,排序标准由二元谓词提供。排序可能改变相等元素的相对顺序 |
| stable_sort() | 类似于 sort,但在排序时保持相对顺序不变 |
| partial_sort() | 将源范围内指定数量的元素排序 |
| partial_sort_copy() | 将源范围内的元素复制到目标范围,同时对它们排序 |
| 分区算法 | |
| partition() | 在指定范围中,将元素分为两组:满足指定一元谓词的元素放在第一个组中,其他元素放在第二组中。不一定会保持集合中元素的相对顺序 |
| stable_partition() | 与 partition 一样将指定范围分为两组,但保持元素的相对顺序不变 |
| 可用于有序容器的算法 | |
| binary_search() | 用于判断一个元素是否存在于一个排序集合中 |
| lower_bound() | 根据元素的值或二元谓词判断元素可能插入到排序集合中的第一个位置,并返回一个指向该位置的迭代器 |
| upper_bound() | 根据元素的值或二元谓词判断元素可能插入到排序集合中的最后一个位置,并返回一个指向该位置的迭代器 |
使用 STL 算法
根据值或条件查找元素
STL 算法 find() 和 find_if() 前面已经用了很多次了,就不再赘述了。
计算包含给定值或满足给定条件的元素数
算法 std::count() 和 count_if() 计算给定范围内的元素数。std:: count() 计算包含给定值(使用相等运算符==进行测试)的元素数:
size_t numZeroes = count (numsInVec.cbegin (), numsInVec.cend (), 0);
cout << "Number of instances of '0': " << numZeroes << endl;
std::count_if() 计算这样的元素数,即满足通过参数传递的一元谓词(可以是函数对象,也可以是 lambda 表达式):
// Unary predicate:
template <typename elementType>
bool IsEven (const elementType& number)
{
return ((number % 2) == 0); // true, if even
}
...
// Use the count_if algorithm with the unary predicate IsEven:
size_t numEvenNums = count_if (numsInVec.cbegin (),
numsInVec.cend (), IsEven <int> );
cout << "Number of even elements: " << numEvenNums << endl;
在集合中搜索元素或序列
如下代码演示了如何在容器中查找元素,但有时需要查找序列或模式。在这种情况下,应使用 search() 或 search_n()。search() 用于在一个序列中查找另一个序列:
auto range = search (numsInVec.cbegin(), // Start range to search in
numsInVec.cend(), // End range to search in
numsInList.cbegin(), // start range to search
numsInList.cend() ); // End range to search for
search_n() 用于在容器中查找 n 个相邻的指定值:
auto partialRange = search_n (numsInVec.cbegin(), // Start range
numsInVec.cend(), // End range
3, // num items to be searched for
9); // value to search for
这两个函数都返回一个迭代器,它指向找到的第一个模式;使用该迭代器之前,务必将其与 end() 进行比较。如下代码演示了 search() 和 search_n() 的用法:
#include <algorithm>
#include <vector>
#include <list>
#include <iostream>
using namespace std;
template <typename T>
void DisplayContents(const T& container)
{
for (auto element = container.cbegin();
element != container.cend();
++element)
cout << *element << ' ';
cout << endl;
}
int main()
{
vector <int> numsInVec{ 2017, 0, -1, 42, 10101, 25, 9, 9, 9 };
list <int> numsInList{ -1, 42, 10101 };
cout << "The contents of the sample vector are: " << endl;
DisplayContents(numsInVec);
cout << "The contents of the sample list are: " << endl;
DisplayContents(numsInList);
cout << "search() for the contents of list in vector:" << endl;
auto range = search(numsInVec.cbegin(), // Start range to search in
numsInVec.cend(), // End range to search in
numsInList.cbegin(), // Start range to search for
numsInList.cend()); // End range to search for
// Check if search found a match
if (range != numsInVec.end())
{
cout << "Sequence in list found in vector at position: ";
cout << distance(numsInVec.cbegin(), range) << endl;
}
cout << "Searching {9, 9, 9} in vector using search_n(): " << endl;
auto partialRange = search_n(numsInVec.cbegin(), // Start range
numsInVec.cend(), // End range
3, // Count of item to be searched for
9); // Item to search for
if (partialRange != numsInVec.end())
{
cout << "Sequence {9, 9, 9} found in vector at position: ";
cout << distance(numsInVec.cbegin(), partialRange) << endl;
}
return 0;
}
打印如下:
The contents of the sample vector are:
2017 0 -1 42 10101 25 9 9 9
The contents of the sample list are:
-1 42 10101
search() for the contents of list in vector:
Sequence in list found in vector at position: 2
Searching {9, 9, 9} in vector using search_n():
Sequence {9, 9, 9} found in vector at position: 6
在这个例子中首先定义了两个容器:一个 vector 和一个 list。第 30 行使用 search() 在 vector 中查找 list。由于要在整个 vector 中查找 list 的全部内容,因此使用了这两个容器类的成员方法 cbegin() 和 cend() 返回的迭代器来指定范围。这表明迭代器在算法和容器之间搭建了桥梁;在算法看来,提供迭代器的容器的特征无关紧要,因为它只使用迭代器,因此能够在 vector 中无缝地查找 list 的全部内容。第 43 行使用 search_n() 搜索序列 {9,9,9} 在 vector 中首次出现的位置。
将容器中的元素初始化为指定值
STL 算法 fill() 和 fill_n() 用于将指定范围的内容设置为指定值。fill() 将指定范围内的元素设置为指定值:
vector <int> numsInVec (3);
// fill all elements in the container with value 9
fill (numsInVec.begin (), numsInVec.end (), 9);
顾名思义,fill_n() 将 n 个元素设置为指定的值,接受的参数包括起始位置、元素数以及要设置的值:
fill_n (numsInVec.begin () + 3, /*count*/ 3, /*fill value*/ -9);
注意到这里使用的是 begin() 和 end(),跟前面的常量迭代器 cbegin() 和 cend() 是不同的,是因为算法 fill() 修改容器中的元素,而常量迭代器禁止修改它们指向的元素,因此使用常量迭代器无法实现目标。
使用 std::generate() 将元素设置为运行阶段生成的值
函数 fill() 和 fill_n() 将集合的元素设置为指定的值,而 generate() 和 generate_n() 等 STL 算法用于将集合的内容设置为一元函数返回的值。
可使用 generate() 将指定范围内的元素设置为生成器函数返回的值:
generate (numsInVec.begin (), numsInVec.end (), // range
rand); // generator function
generate_n() 与 generate() 类似,但您指定的是要设置的元素数,而不是闭区间:
generate_n (numsInList.begin (), 5, rand);
因此,可使用这两种算法将容器设置为文件的内容或随机值,看下面的示例:
#include <algorithm>
#include <vector>
#include <list>
#include <iostream>
#include <ctime>
int main()
{
using namespace std;
srand(time(NULL)); // seed random generator using time
vector <int> numsInVec(5);
generate(numsInVec.begin(), numsInVec.end(), // range
rand); // generator function
cout << "Elements in the vector are: ";
for (size_t index = 0; index < numsInVec.size(); ++index)
cout << numsInVec[index] << " ";
cout << endl;
list <int> numsInList(5);
generate_n(numsInList.begin(), 3, rand);
cout << "Elements in the list are: ";
for (auto element = numsInList.begin();
element != numsInList.end();
++element)
cout << *element << ' ';
return 0;
}
运行后打印如下:
Elements in the vector are: 12836 14889 57 14367 17631
Elements in the list are: 18746 9349 24835 0 0
第 10 行使用了随机数生成器并将当前时间作为种子,这意味着每次运行这个应用程序时,输出都可能不同。第 13 行使用函数 generate() 将 vector 的所有元素都设置为随机值,而第 22 行将 list 的前 3 个元素设置为生成器函数 rand() 提供的随机值。注意到函数 generate() 接受一个范围作为输入,并为该范围内的每个元素调用指定的函数对象 rand();而 generate_n() 接受起始位置,调用指定的函数对象 rand() count 次,以设置容器中 count 个元素的值。容器中不在指定范围内的元素不受影响。
使用 for_each() 处理指定范围内的元素
算法 for_each() 对指定范围内的每个元素执行指定的一元函数对象,其用法如下:
fnObjType retValue = for_each (start_of_range,
end_of_range,
unaryFunctionObject);
也可使用接受一个参数的 lambda 表达式代替一元函数对象。
返回值表明,for_each() 返回用于对指定范围内的每个元素进行处理的函数对象(functor)。这意味着使用结构或类作为函数对象可存储状态信息,并在 for_each() 执行完毕后查询这些信息:
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
template <typename elementType>
struct DisplayElementKeepcount
{
int count;
DisplayElementKeepcount() : count(0) {}
void operator () (const elementType& element)
{
++count;
cout << element << ' ';
}
};
int main()
{
vector <int> numsInVec{ 2017, 0, -1, 42, 10101, 25 };
cout << "Elements in vector are: " << endl;
DisplayElementKeepcount<int> functor =
for_each(numsInVec.cbegin(), // Start of range
numsInVec.cend(), // End of range
DisplayElementKeepcount<int>());// functor
cout << endl;
// Use the state stored in the return value of for_each!
cout << "'" << functor.count << "' elements displayed" << endl;
}
这里演示了如何使用 for_each(),还指出通过它返回的函数对象(functor)的成员 count,可以获得它被调用的次数。
使用 std::transform() 对范围进行变换
std::for_each() 和 std::transform() 很像,都对源范围内的每个元素调用指定的函数对象。然而,std::transform() 有两个版本,第一个版本接受一个一元函数,常用于将字符串转换为大写或小写(使用的一元函数分别是 toupper() 和 tolower()):
string str ("THIS is a TEst string!");
transform (str.cbegin(), // start source range
str.cend(), // end source range 408
strLowerCaseCopy.begin(), // start destination range
::tolower); // unary function
第二个版本接受一个二元函数,让 transform() 能够处理一对来自两个不同范围的元素:
// sum elements from two vectors and store result in a deque
transform (numsInVec1.cbegin(), // start of source range 1
numsInVec1.cend(), // end of source range 1
numsInVec2.cbegin(), // start of source range 2
sumInDeque.begin(), // store result in a deque
plus<int>()); // binary function plus
不像 for_each() 那样只处理一个范围,这两个版本的 transform() 都将指定变换函数的结果赋给指定的目标范围。如下代码演示了 std::transform() 的用法:
#include <algorithm>
#include <string>
#include <vector>
#include <deque>
#include <iostream>
#include <functional>
int main()
{
using namespace std;
string str("THIS is a TEst string!");
cout << "The sample string is: " << str << endl;
string strLowerCaseCopy;
strLowerCaseCopy.resize(str.size());
transform(str.cbegin(), // start source range
str.cend(), // end source range
strLowerCaseCopy.begin(), // start dest range
::tolower); // unary function
cout << "Result of 'transform' on the string with 'tolower':" << endl;
cout << "\"" << strLowerCaseCopy << "\"" << endl << endl;
// Two sample vectors of integers...
vector<int> numsInVec1{ 2017, 0, -1, 42, 10101, 25 };
vector<int> numsInVec2(numsInVec1.size(), -1);
// A destination range for holding the result of addition
deque <int> sumInDeque(numsInVec1.size());
transform(numsInVec1.cbegin(), // start of source range 1
numsInVec1.cend(), // end of source range 1
numsInVec2.cbegin(), // start of source range 2
sumInDeque.begin(), // start of dest range
plus<int>()); // binary function
cout << "Result of 'transform' using binary function 'plus': " << endl;
cout << "Index Vector1 + Vector2 = Result (in Deque)" << endl;
for (size_t index = 0; index < numsInVec1.size(); ++index)
{
cout << index << " \t " << numsInVec1[index] << "\t+ ";
cout << numsInVec2[index] << " \t = ";
cout << sumInDeque[index] << endl;
}
return 0;
}
打印如下:
The sample string is: THIS is a TEst string!
Result of 'transform' on the string with 'tolower':
"this is a test string!"
Result of 'transform' using binary function 'plus':
Index Vector1 + Vector2 = Result (in Deque)
0 2017 + -1 = 2016
1 0 + -1 = -1
2 -1 + -1 = -2
3 42 + -1 = 41
4 10101 + -1 = 10100
5 25 + -1 = 24
复制和删除操作
STL 提供了三个重要的复制函数:copy()、copy_if() 和 copy_backward()。copy 沿向前的方向将源范围的内容赋给目标范围:
auto lastElement = copy (numsInList.cbegin(), // start source range
numsInList.cend(), // end source range
numsInVec.begin()); // start dest range
copy_if() 是 C++11 新增的,仅在指定的一元谓词返回 true 时才复制元素:
// copy odd numbers from list into vector
copy_if (numsInList.cbegin(), numsInList.cend(),
lastElement, // copy position in dest range
[](int element){return ((element % 2) == 1);});
copy_backward() 沿向后的方向将源范围的内容赋给目标范围:
copy_backward (numsInList.cbegin (),
numsInList.cend (),
numsInVec.end ());
remove() 将容器中与指定值匹配的元素删除:
// Remove all instances of '0', resize vector using erase()
auto newEnd = remove (numsInVec.begin (), numsInVec.end (), 0);
numsInVec.erase (newEnd, numsInVec.end ());
remove_if() 使用一个一元谓词,并将容器中满足该谓词的元素删除:
// Remove all odd numbers from the vector using remove_if
newEnd = remove_if (numsInVec.begin (), numsInVec.end (),
[](int num) {return ((num % 2) == 1);} ); //predicate
numsInVec.erase (newEnd, numsInVec.end ()); // resizing
替换值以及替换满足给定条件的元素
STL 算法 replace() 与 replace_if() 分别用于替换集合中等于指定值和满足给定条件的元素。replace() 根据比较运算符==的返回值来替换元素:
cout << "Using 'std::replace' to replace value 5 by 8" << endl;
replace (numsInVec.begin (), numsInVec.end (), 5, 8);
replace_if() 需要一个用户指定的一元谓词,对于要替换的每个值,该谓词都返回 true:
cout << "Using 'std::replace_if' to replace even values by -1" << endl;
replace_if (numsInVec.begin (), numsInVec.end (),
[](int element) {return ((element % 2) == 0); }, -1);
排序、在有序集合中搜索以及删除重复元素
在实际的应用程序中,经常需要排序以及在有序范围内(出于性能考虑)进行搜索。经常需要对一组信息进行排序,为此可使用 STL 算法 sort():
sort (numsInVec.begin (), numsInVec.end ()); // ascending order
这个版本的 sort() 将 std::less<> 用作二元谓词,而该谓词使用 vector 存储的数据类型实现的运算符<。您可使用另一个重载版本,以指定谓词,从而修改排列顺序:
sort (numsInVec.begin (), numsInVec.end (),
[](int lhs, int rhs) {return (lhs > rhs);} ); // descending order
同样,在显示集合的内容前,需要删除重复的元素。要删除相邻的重复值,可使用 unique():
auto newEnd = unique (numsInVec.begin (), numsInVec.end ());
numsInVec.erase (newEnd, numsInVec.end ()); // to resize
要进行快速查找,可使用 STL 算法 binary_search(),这种算法只能用于有序容器:
bool elementFound = binary_search (numsInVec.begin (), numsInVec.end (), 2011);
if (elementFound)
cout << "Element found in the vector!" << endl;
程序清单 23.10 使用 STL 算法 std::sort( )将一个范围排序,使用 std::binary_search( )在有序的范围
内进行搜索,然后使用 std::unique( )删除相邻的重复元素(执行 sort( )后,重复的元素将彼此相邻)。
binary_search( )算法只能用于经过排序的容器。
将范围分区
std::partition() 将输入范围分为两部分:一部分满足一元谓词;另一部分不满足:
bool IsEven (const int& num) // unary predicate
{
return ((num % 2) == 0);
}
...
partition (numsInVec.begin(), numsInVec.end(), IsEven);
然而,std::partition() 不保证每个分区中元素的相对顺序不变。在相对顺序很重要,需要保持不变时,应使用 std::stable_partition():
stable_partition (numsInVec.begin(), numsInVec.end(), IsEven);
在有序集合中插入元素
将元素插入到有序集合中时,将其插入到正确位置很重要。为了满足这种需求,STL 提供了lower_bound() 和 upper_bound() 等函数:
auto minInsertPos = lower_bound (names.begin(), names.end(),
"Brad Pitt");
// alternatively:
auto maxInsertPos = upper_bound (names.begin(), names.end(),
"Brad Pitt");
lower_bound() 和 upper_bound() 都返回一个迭代器,分别指向在不破坏现有顺序的情况下,元素可插入到有序范围内的最前位置和最后位置。
使用算法 remove( )、remove_if( )或 unique( )后,务必使用容器的成员方法 erase( )调整容器的大小。 调用 unique() 删除重复的相邻值之前,别忘了使用 sort()对容器进行排序。sort() 确保包含相同值的元素彼此相邻,这样 unique()才能发挥作用。
std::sort() 可用于 list,用法与用于其他顺序容器一样。然而,list 需要保持一个特殊特征:对 list 的操作不会导致现有迭代器失效,而 std::sort() 不能保证该特征得以保持。因此,STL list 通过成员函数 list::sort() 提供了 sort 算法。应使用该函数,因为它确保指向 list 中元素的迭代器不会失效,即使元素的相对位置发生了变化。
自适应容器:栈和队列
STL 提供了一些这样的容器,即使用其他容器模拟栈和队列的行为。这种内部使用一种容器但呈现另一种容器的行为特征的容器称为自适应容器(adaptive container)。
使用 STL stack 类
STL stack 是一个模板类,要使用它,必须包含头文件 <stack>。它是一个泛型类,允许在顶部插入和删除元素,而不允许访问中间的元素。从这种角度看,std::stack 的行为很像一叠盘子。
在有些 STL 实现中,std::stack 的定义如下:
template <
class elementType,
class Container=deque<Type>
> class stack;
参数 elementType 是 stack 存储的对象类型。第二个模板参数 Container 是 stack 使用的默认底层容器实现类。stack 默认在内部使用 std::deque 来存储数据,但可指定使用 vector 或 list 来存储数据。因此,实例化整型栈的代码类似于下面这样:
std::stack <int> numsInStack;
要创建存储类(如 Tuna)对象的栈,可使用下述代码:
std::stack <Tuna> tunasInStack;
要创建使用不同底层容器的栈,可使用如下代码:
std::stack <double, vector <double>> doublesStackedInVec;
这里同样实例化了一个用于存储 double 元素的 stack,但将第二个模板参数(stack 在内部使用的集合类)指定为 vector。如果没有指定第二个模板参数,stack 将自动使用默认的 std::deque。
stack 改变了另一种容器(如 deque、list 或 vector)的行为,通过限制元素插入或删除的方式实现 其功能,从而提供严格遵守栈机制的行为特征。下表解释了 stack 类的公有成员函数并演示了如何将这些函数用于整型栈。
| 函数 | 描述 |
|---|---|
| push() | 在栈顶插入元素 numsInStack.push (25); |
| pop() | 删除栈顶的元素 numsInStack.pop(); |
| empty() | 检查栈是否为空并返回一个布尔值 if (numsInStack.empty()) DoSomething(); |
| size() | 返回栈中的元素数 size_t numElements = numsInStack.size(); |
| top() | 获得指向栈顶元素的引用 cout << "Element at the top = " << numsInStack.top(); |
使用 STL queue 类
STL queue 是一个模板类,要使用它,必须包含头文件<queue>。queue 是一个泛型类,只允许在末尾插入元素以及从开头删除元素。queue 不允许访问中间的元素,但可以访问开头和末尾的元素。从这种意义上说,std::queue 的行为与超市收银台前的队列极其相似。
std::queue 的定义如下:
template <
class elementType,
class Container = deque<Type>
> class queue;
其中 elementType 是 queue 对象包含的元素的类型。Container 是 std::queue 用于存储其数据的集合类型,可将该模板参数设置为 std::list、vector 或 deque,默认为 deque。实例化整型 queue 的最简单方式如下:
std::queue <int> numsInQ;
如果要创建这样的 queue,即其元素类型为 double,并使用 std::list(而不是默认的 queue)存储这些元素,可以像下面这样做:
std::queue <double, list <double>> dblsInQInList;
与 stack 一样,也可使用一个 queue 来实例化另一个 queue:
std::queue<int> copyQ(numsInQ);
与 std::stack 一样,std::queue 的实现也是基于 STL 容器 vector、list 或 deque 的。queue 提供了几个成员函数来实现队列的行为特征。下表解释了 queue 的成员函数。
| 函数 | 描述 |
|---|---|
| push() | 在队尾(即最后一个位置)插入一个元素 numsInQ.push(25); |
| pop() | 将队首(即最开始位置)的元素删除 numsInQ.pop(); |
| front() | 返回指向队首元素的引用 cout << "Element at front: " << numsInQ.front(); |
| back() | 返回指向队尾元素(即最后插入的元素)的引用 cout << "Element at back: " << numsInQ. back(); |
| empty() | 检查队列是否为空并返回一个布尔值 if (numsInQ.empty()) cout << “The queue is empty!”; |
| size() | 返回队列中的元素数 size_t nNumElements = numsInQ.size(); |
使用 STL 优先级队列
class priority_queueSTL priority_queue 是一个模板类,要使用它,也必须包含头文件<queue>。priority_queue 与 queue 的不同之处在于,包含最大值(或二元谓词认为是最大值)的元素位于队首,且只能在队首执行操作。
std::priority_queue 类的定义如下:
template <
class elementType,
class Container=vector<Type>,
class Compare=less<typename Container::value_type>
>
class priority_queue
其中 elementType 是一个模板参数,指定了优先级队列将包含的元素的类型。第二个模板参数指定 priority_queue 在内部将使用哪个集合类来存储数据,第三个参数让程序员能够指定一个二元谓词,以帮助队列判断哪个元素应位于队首。如果没有指定二元谓词,priority_queue 类将默认使用 std::less,它使用运算符 < 比较对象。
要实例化整型 priority_queue,最简单的方式如下:
std::priority_queue <int> numsInPrioQ;
如果要创建一个这样的 priority_queue,即其元素类型为 double,且按小到大的顺序存储在 std::deque 中,则可这样做:
priority_queue <int, deque <int>, greater <int>> numsInDescendingQ;
与 stack 一样,也可使用一个 priority_queue 来实例化另一个 priority_queue:
std::priority_queue <int> copyQ(numsInPrioQ);
queue 提供了成员函数 front( )和 back( ),但 priority_queue 没有。下标简要地介绍了 priority_queue 的成员函数。
| 函数 | 描述 |
|---|---|
| push() | 在优先级队列中插入一个元素 numsInPrioQ.push(10); |
| pop() | 删除队首元素,即最大的元素 numsInPrioQ.pop(); |
| top() | 返回指向队列中最大元素(即队首元素)的引用 numsInPrioQ. cout << "The largest element inpriority queue is: " << numsInPrioQ.top ( ); |
| empty() | 检查优先级队列是否为空并返回一个布尔值 if (numsInPrioQ.empty( )) cout << “The queue is empty!”; |
| size() | 返回优先级队列中的元素个数 size_t nNumElements = numsInPrioQ.size(); |
使用 STL 位标志
位是存储设置与标志的高效方法。标准模板库(STL)提供了可帮助组织与操作位信息的类。
bitset 类
std::bitset 是一个 STL 类,用于处理以位和位标志表示的信息。std::bitset 不是 STL 容器类,因为它不能调整长度。这是一个实用类,针对处理长度在编译阶段已知的位序列进行了优化。要使用 std::bitset 类,必须包含头文件<bitset>。
实例化这个模板类时,必须通过一个模板参数指定实例需要管理的位数:
bitset <4> fourBits; // 4 bits initialized to 0000
还可将 bitset 初始化为一个用字符串字面量(char*)表示的位序列:
bitset <5> fiveBits("10101"); // 5 bits 10101
使用一个 bitset 来实例化另一个 bitset 非常简单:
bitset <8> fiveBitsCopy(fiveBits);
如下演示了一些实例化 bitset 类的方式:
#include <bitset>
#include <iostream>
#include <string>
int main()
{
using namespace std;
bitset <4> fourBits; // 4 bits initialized to 0000
cout << "Initial contents of fourBits: " << fourBits << endl;
bitset <5> fiveBits("10101"); // 5 bits 10101
cout << "Initial contents of fiveBits: " << fiveBits << endl;
bitset <6> sixBits(0b100001); // C++14 binary literal
cout << "Initial contents of sixBits: " << sixBits << endl;
bitset <8> eightBits(255); // 8 bits initialized to long int 255
cout << "Initial contents of eightBits: " << eightBits << endl;
// instantiate one bitset as a copy of another
bitset <8> eightBitsCopy(eightBits);
return 0;
}
打印如下:
Initial contents of fourBits: 0000
Initial contents of fiveBits: 10101
Initial contents of sixBits: 100001
Initial contents of eightBits: 11111111
该示例演示了 4 种创建 bitset 对象的方式:通过默认构造函数将位序列初始化为 0(如第 9 所示);通过 C 风格字符串指定位序列的字符串表示(如第 12 行所示);通过 unsigned long 指定二进制序列对应的十进制值(如第 15 和 18 行所示);使用复制构造函数(如第 22 行所示)。注意,在每个实例中,都需要通过一个模板参数指定位序列包含的位数。位数在编译阶段指定,而不是动态的。指定 bitset 的位数后,便不能插入更多的位,而不像 vector 那样调整在编译阶段指定的长度。
注意到第 15 行使用了二进制字面量 0b100001。前缀 0b 或 0B 告诉编译器,接下来的是一个整数的二进制表示。这种字面量是 C++14 新增的。
前面介绍了运算符,运算符最重要的作用是提高类的可用性。std::bitset 提供一些很有用的运算符,如下表所示:
| 运算符 | 描述 |
|---|---|
| 运算符<< | 将位序列的文本表示插入到输出流中 cout << fourBits; |
| 运算符>> | 将一个字符串插入到 bitset 对象中 “0101” >> fourBits; |
| 运算符& | 执行按位与操作 bitset <4> result (fourBits1 & fourBits2); |
| 运算符| | 执行按位或操作 bitwise <4> result (fourBits1 | fourBits2); |
| 运算符^ | 执行按位异或操作 bitwise <4> result (fourBits1 ^ fourBits2); |
| 运算符~ | 执行按位取反操作 bitwise <4> result (~fourBits1); |
| 运算符 >>= | 执行按位右移操作 fourBits >>= (2); //右移两位 |
| 运算符 <<= | 执行按位左移操作 fourBits <<= (2); //左移两位 |
| 运算符[N] | 返回指向位序列中第(N+1)位的引用 fourBits [2] = 0; // 将第 3 位设置为 0 bool bNum = fourBits [2]; //读取第 3 位 |
除这些运算符外,std::bitset 还提供了|=、&=、^=和~=等运算符,用于对 bitset 对象执行按位操作。
位可以存储两种状态:要么是设置为(1),要么是重置(0)。要对 bitset 的内容进行操作,可使用下表列出的成员函数对 bitset 中的一位或所有位进行操作。
| 函数 | 描述 |
|---|---|
| set() | 将序列中的所有位都设置为 1 fourBits.set(); //现在序列包含 1111 |
| set(N, val=1) | 将第 N+1 位设置为 val 指定的值(默认为 1) fourBits.set (2, 0); // 将第 3 位设置为 0 |
| reset() | 将序列中的所有位都重置为 0 fourBits.reset(); // 现在序列包含 0000 |
| reset(N) | 将偏移位置为(N+1)的位清除 fourBits.reset(2); //现在第 3 位的值为 0 |
| flip() | 将位序列中的所有位取反 fourBits.flip(); // 0101 将变为 1010 |
| size() | 返回序列中的位数 size_t numBits = fourBits.size(); // 返回 4 |
| count() | 返回序列中值为 1 的位数 size_t numBitsSet = fourBits.count(); size_t numBitsReset = fourBits.size() – fourBits.count(); |
下面的代码演示了这些成员方法和运算符的用法:
#include <bitset>
#include <string>
#include <iostream>
int main()
{
using namespace std;
bitset <8> inputBits;
cout << "Enter a 8-bit sequence: ";
cin >> inputBits; // store user input in bitset
cout << "Num 1s you supplied: " << inputBits.count() << endl;
cout << "Num 0s you supplied: ";
cout << inputBits.size() - inputBits.count() << endl;
bitset <8> inputFlipped(inputBits); // copy
inputFlipped.flip(); // toggle the bits
cout << "Flipped version is: " << inputFlipped << endl;
cout << "Result of AND, OR and XOR between the two:" << endl;
cout << inputBits << " & " << inputFlipped << " = ";
cout << (inputBits & inputFlipped) << endl; // bitwise AND
cout << inputBits << " | " << inputFlipped << " = ";
cout << (inputBits | inputFlipped) << endl; // bitwise OR
cout << inputBits << " ^ " << inputFlipped << " = ";
cout << (inputBits ^ inputFlipped) << endl; // bitwise XOR
return 0;
}
运行后打印如下:
Enter a 8-bit sequence: 10110101
Num 1s you supplied: 5
Num 0s you supplied: 3
Flipped version is: 01001010
Result of AND, OR and XOR between the two:
10110101 & 01001010 = 00000000
10110101 | 01001010 = 11111111
10110101 ^ 01001010 = 11111111
STL bitset 的缺点之一是不能动态地调整长度。仅当在编辑阶段知道序列将存储多少位时才能使用 bitset。为了克服这种缺点,STL 向程序员提供了 vector<bool>类(在有些 STL 实现中为 bit_vector)。
vector<bool>
vector<bool> 是对 std::vector 的部分具体化,用于存储布尔数据。这个类可动态地调整长度,因此程序员无需在编译阶段知道要存储的布尔标志数。要使用 std::vector<bool>类,必须包含头文件<vector>。
实例化 vector<bool> 的方式与实例化 vector 类似,有一些方便的重载构造函数可供使用:
vector <bool> boolFlags1;
例如,可创建一个这样的 vector,即它最初包含 10 个布尔元素,且每个元素都被初始化为 1(即 true):
vector <bool> boolFlags2 (10, true);
还可使用一个 vector<bool> 创建另一个 vector<bool>:
vector <bool> boolFlags2Copy (boolFlags2);
vector提供了函数 flip( ),用于将序列中的布尔值取反,这与函数 bitset<>::flip( )很像。
除这个方法外,vector<bool> 与 std::vector 极其相似,例如,可使用 push_back 将标志位插入到序列中。下面的代码更详细地演示了这个类的用法。
#include <vector>
#include <iostream>
using namespace std;
int main()
{
vector <bool> boolFlags(3); // instantiated to hold 3 bool flags
boolFlags[0] = true;
boolFlags[1] = true;
boolFlags[2] = false;
boolFlags.push_back(true); // insert a fourth bool at the end
cout << "The contents of the vector are: " << endl;
for (size_t index = 0; index < boolFlags.size(); ++index)
cout << boolFlags[index] << ' ';
cout << endl;
boolFlags.flip();
cout << "The contents of the vector are: " << endl;
for (size_t index = 0; index < boolFlags.size(); ++index)
cout << boolFlags[index] << ' ';
cout << endl;
return 0;
}
打印如下:
The contents of the vector are:
1 1 0 1
The contents of the vector are:
0 0 1 0
push_back。虽然第 7 行将 boolFlags 初始化为包含 3 个标志,但可动态地添加标志,如第 12 行所示;而使用 std::bitset 时,标志数是在编译阶段指定的,不能增加。
理解智能指针
C++ 程序员并非一定要使用常规指针,而可使用智能指针来管理堆(或自由存储区)中的内存。
什么是智能指针
简单地说,C++ 智能指针是包含重载运算符的类,其行为像常规指针,但智能指针能够及时、妥善地销毁动态分配的数据,并实现了明确的对象生命周期,因此更有价值。
与其他现代编程语言不同,C++ 在内存分配、释放和管理方面向程序员提供了全面的灵活性。不幸的是,这种灵活性是把双刃剑,一方面,它使 C++成为一种功能强大的语言,另一方面,它让程序员能够制造与内存相关的问题,如动态分配的对象没有正确地释放时将导致内存泄露。例如:
SomeClass* ptrData = anObject.GetData ();
/*
Questions: Is object pointed by ptrData dynamically allocated using new?
If so, who calls delete? Caller or the called?
Answer: No idea!
*/
ptrData->DoSomething();
在上述代码中,没有显而易见的方法获悉 ptrData 指向的内存:
- 是否是从堆中分配的,因此最终需要释放;
- 是否由调用者负责释放;
- 对象的析构函数是否会自动销毁该对象。
虽然这种不明确性可通过添加注释以及遵循编码实践来部分缓解,但这些机制太松散,无法有效地避免因滥用动态分配的数据和指针而导致的错误。
鉴于使用常规指针以及常规的内存管理方法存在的问题,当 C++程序员需要管理堆(自由存储区)中的数据时,并非一定要使用它们,而可在程序中使用智能指针,以更智能的方式分配和管理内存:
smart_pointer<SomeClass> spData = anObject.GetData();
// Use a smart pointer like a conventional pointer!
spData->Display ();
(*spData).Display ();
// Don't have to worry about de-allocation
// (the smart pointer's destructor does it for you)
智能指针的行为类似常规指针(这里将其称为原始指针),但通过重载的运算符和析构函数确保动态分配的数据能够及时地销毁,从而提供了更多有用的功能。
智能指针是如何实现的
这个问题暂时可以简化为:“智能指针 spData 是如何做到像常规指针那样的?” 答案如下:智能指针类重载了解除引用运算符(*)和成员选择运算符(->),让程序员可以像使用常规指针那样使用它们。另外,为了让您能够在堆中管理各种类型,几乎所有良好的智能指针类都是模板类,包含其功能的泛型实现。由于是模板,它们是通用的,可以根据要管理的对象类型进行具体化。下面的代码是一个简单智能指针类的实现。
template <typename T>
class smart_pointer
{
private:
T* rawPtr;
public:
smart_pointer(T* pData) : rawPtr(pData) {} // constructor
~smart_pointer() { delete rawPtr; }; // destructor
// copy constructor
smart_pointer(const smart_pointer & anotherSP);
// copy assignment operator
smart_pointer& operator= (const smart_pointer& anotherSP);
T& operator* () const // dereferencing operator
{
return *(rawPtr);
}
T* operator-> () const // member selection operator
{
return rawPtr;
}
};
该智能指针类实现了两个运算符:*和->,如第 15~18 行及第 20~23 行所示,它们让这个类能够用作常规意义上的“指针”。例如,如果有一个 Tuna 类,则可这样对该类型的对象使用智能指针:
smart_pointer <Tuna> smartTuna(new Tuna);
smartTuna->Swim();
// Alternatively:
(*smartTuna).Swim();
这个 smart_pointer 类还没有实现使其非常智能,从而胜于常规指针的功能。构造函数(如第 7 行所示)接受一个指针,并将其保存到该智能指针类内部的一个指针对象中。析构函数释放该指针,从而实现了自动内存释放。
智能指针类型
内存资源管理(即实现的内存所有权模型)是智能指针类与众不同的地方。智能指针决定在拷贝和赋值时如何处理内存资源。最简单的实现通常会导致性能问题,而最快的实现可能并非适合所有应用程序。因此,在应用程序中使用智能指针前,程序员应理解其工作原理。
智能指针的分类实际上就是内存资源管理策略的分类,可分为如下几类:
- 深拷贝;
- 写时拷贝(Copy on Write,COW);
- 引用计数;
- 引用链接;
- 破坏性拷贝。
深拷贝
在实现深拷贝的智能指针中,每个智能指针实例都保存一个它管理的对象的完整副本。每当智能指针被拷贝时,将拷贝它指向的对象(因此称为深拷贝)。每当智能指针离开作用域时,将(通过析构函数)释放它指向的内存。
虽然基于深拷贝的智能指针看起来并不比按值传递对象优越,但在处理多态对象时,其优点将显现出来。如下所示,使用智能指针可避免切除(slicing)问题:
// Example of Slicing When Passing Polymorphic Objects by Value
// Fish is a base class for Tuna and Carp, Fish::Swim() is virtual
void MakeFishSwim (Fish aFish) // note parameter type
{
aFish.Swim(); // virtual function
}
// ... Some function
Carp freshWaterFish;
MakeFishSwim (freshWaterFish); // Carp will be 'sliced' to Fish
// Slicing: only the Fish part of Carp is sent to MakeFishSwim()
如果程序员选择使用深复制智能指针,便可解决切除问题,看如下代码:
template <typename T>
class deepcopy_smart_ptr
{
private:
T* object;
public:
//... other functions
// copy constructor of the deepcopy pointer
deepcopy_smart_ptr(const deepcopy_smart_ptr& source)
{
// Clone() is virtual: ensures deep copy of Derived class object
object = source->Clone();
}
// copy assignment operator
deepcopy_smart_ptr& operator= (const deepcopy_smart_ptr& source)
{
if (object)
delete object;
object = source->Clone();
}
};
可以看到,deepcopy_smart_ptr 在第 10~14 行实现了一个拷贝构造函数,使得能够通过函数 Clone()函数对多态对象进行深拷贝—类必须实现函数 Clone()。另外,它还实现了拷贝赋值运算符,如第 17~23 行所示。为了简单起见,这里假设基类 Fish 实现的虚函数为 Clone()。通常,实现深拷贝模型的智能指针通过模板参数或函数对象提供该函数。
下面是 deepcopy_smart_ptr 的一种用法:
deepcopy_smart_ptr<Carp> freshWaterFish(new Carp);
MakeFishSwim (freshWaterFish); // Carp will not be 'sliced'
构造函数实现的深复制将发挥作用,确保传递的对象不会出现切除问题—虽然从语法上说,目标函数 MakeFishSwim() 只要求基类部分。
基于深拷贝的机制的不足之处在于性能。对有些应用程序来说,这可能不是问题,但对于其他很多应用程序来说,这可能导致程序员不使用智能指针,而将指向基类的指针(常规指针 Fish*)传递给函数,如 MakeFishSwim()。其他指针类型以各种方式试图解决这种性能问题。
写时复制机制
写时拷贝机制(Copy on Write,COW)试图对深拷贝智能指针的性能进行优化,它共享指针,直到首次写入对象。首次调用非 const 函数时,COW 指针通常为该非 const 函数操作的对象创建一个副本,而其他指针实例仍共享源对象。
COW 深受很多程序员的喜欢。实现 const 和非 const 版本的运算符*和->,是实现 COW 指针功能的关键。非 const 版本用于创建副本。
重要的是,选择 COW 指针时,在使用这样的实现前务必理解其实现细节。否则,拷贝时将出现拷贝得太少或太多的情况。
引用计数智能指针
引用计数是一种记录对象的用户数量的机制。当计数降低到零后,便将对象释放。因此,引用计数提供了一种优良的机制,使得可共享对象而无法对其进行拷贝。如果读者使用过微软的 COM 技术,肯定知道引用计数的概念。
这种智能指针被拷贝时,需要将对象的引用计数加 1。至少有两种常用的方法来跟踪计数:
- 在对象中维护引用计数;
- 引用计数由共享对象中的指针类维护。
前者称为入侵式引用计数,因为需要修改对象以维护和递增引用计数,并将其提供给管理对象的智能指针。COM 采取的就是这种方法。后者是智能指针类将计数保存在自由存储区(如动态分配的整型),拷贝时拷贝构造函数将这个值加 1。
因此,使用引用计数机制,程序员只应通过智能指针来处理对象。在使用智能指针管理对象的同时让原始指针指向它是一种糟糕的做法,因为智能指针将在它维护的引用计数减为零时释放对象,而原始指针将继续指向已不属于当前应用程序的内存。引用计数还有一个独特的问题:如果两个对象分别存储指向对方的指针,这两个对象将永远不会被释放,因为它们的生命周期依赖性导致其引用计数最少为 1。
引用链接智能指针
引用链接智能指针不主动维护对象的引用计数,而只需知道计数什么时候变为零,以便能够释放对象。
之所以称为引用链接,是因为其实现是基于双向链表的。通过拷贝智能指针来创建新智能指针时,新指针将被插入到链表中。当智能指针离开作用域进而被销毁时,析构函数将把它从链表中删除。与引用计数的指针一样,引用链接指针也存在生命周期依赖性导致的问题。
破坏性拷贝
破坏性拷贝是这样一种机制,即在智能指针被拷贝时,将对象的所有权转交给目标指针并重置原来的指针。
destructive_copy_smartptr <SampleClass> smartPtr(new SampleClass());
SomeFunc (smartPtr); // Ownership transferred to SomeFunc
// Don't use smartPtr in the caller any more!
虽然破坏性拷贝机制使用起来并不直观,但它有一个优点,即可确保任何时刻只有一个活动指针指向对象。因此,它非常适合从函数返回指针以及需要利用其“破坏性”的情形。
下面的代码是一种破坏性拷贝指针的实现,它没有采用推荐的标准 C++ 编程方法。
template <typename T>
class destructivecopy_ptr
{
private:
T* object;
public:
destructivecopy_ptr(T* input) :object(input) {}
~destructivecopy_ptr() { delete object; }
// copy constructor
destructivecopy_ptr(destructivecopy_ptr& source)
{
// Take ownership on copy
object = source.object;
// destroy source
source.object = 0;
}
// copy assignment operator
destructivecopy_ptr& operator= (destructivecopy_ptr& source)
{
if (object != source.object)
{
delete object;
object = source.object;
source.object = 0;
}
}
};
int main()
{
destructivecopy_ptr<int> num(new int);
destructivecopy_ptr<int> copy = num;
// num is now invalid
return 0;
}
拷贝构造函数在拷贝后将源指针设置为 NULL,这就是“破坏性拷贝”的由来。赋值运算符亦如此。因此在第 35 行被赋给另一个指针后,num 就不再有效,这种行为不符合赋值操作的目的。
对破坏性拷贝智能指针的实现来说,它不同于大多数 C++类,该智能指针类的拷贝构造函数和赋值运算符不能接受 const 引用,因为它在拷贝源引用后使其无效。这不仅不符合传统拷贝构造函数和赋值运算符的语义,还让智能指针类的用法不直观。拷贝或赋值后销毁源引用不符合预期。鉴于这种智能指针销毁源引用,这也使得它不适合用于 STL 容器,如 std::vector 或其他任何动态集合类。这些容器需要在内部拷贝内容,这将导致指针失效。由于种种原因,不在程序中使用破坏性拷贝智能指针是明智的选择。C++标准一直支持 auto_ptr,它是一种基于破坏性复制的智能指针。C++11 终于摒弃了该智能指针,现在您应使用 std::unique_ptr。
使用 std::unique_ptr
std::unique_ptr 是 C++11 新增的,与 auto_ptr 稍有不同,因为它不允许拷贝和赋值。要使用 std:unique_ptr,必须包含头文件<memory>。
unique_ptr 是一种简单的智能指针,类似于程序清单 26.1 所示的智能指针,但其复制构造函数和
如下代码演示了其用法。
#include <iostream>
#include <memory> // include this to use std::unique_ptr
using namespace std;
class Fish
{
public:
Fish() { cout << "Fish: Constructed!" << endl; }
~Fish() { cout << "Fish: Destructed!" << endl; }
void Swim() const { cout << "Fish swims in water" << endl; }
};
void MakeFishSwim(const unique_ptr<Fish>& inFish)
{
inFish->Swim();
}
int main()
{
unique_ptr<Fish> smartFish(new Fish);
smartFish->Swim();
MakeFishSwim(smartFish); // OK, as MakeFishSwim accepts reference
unique_ptr<Fish> copySmartFish;
// copySmartFish = smartFish; // error: operator= is private
return 0;
}
输出:
Fish: Constructed!
Fish swims in water
Fish swims in water
Fish: Destructed!
从输出可知,虽然 smartFish 指向的对象是在 main() 中创建的,但它被自动销毁,您无需调用 delete 运算符。这是 unique_ptr 的行为:当指针离开作用域时,将通过析构函数释放它拥有的对象。注意到第 24 行将 smartFish 作为参数传递给了 MakeFishSwim(),这样做不会导致拷贝,因为 MakeFishSwim() 的参数为引用,如第 14 行所示。如果删除第 14 行的引用符号 &,将出现编译错误,因为拷贝构造函数是私有的。同样,不能像第 26 行那样将一个 unique_ptr 对象赋给另一个 unique_ptr 对象,因为拷贝赋值运算符是私有的。
总之,unique_ptr 比 C++11 已摒弃的 auto_ptr 更安全,因为拷贝和赋值不会导致源智能指针对象无效。它在销毁时释放对象,可帮助您进行简单的内存管理。
unique_ptr 不支持赋值:
copySmartFish = smartFish; // error: operator= is private
然而,它确实支持移动语义,因此一种可行的选项如下:
unique_ptr<Fish> sameFish (std::move(smartFish));
// smartFish is empty henceforth
编写需要捕获 unique_ptr 的 lambda 表达式时,可在 C++14 支持的捕获中使用 std::move(),如下所示:
std::unique_ptr<char> alphabet(new char);
*alphabet = 's';
auto lambda = [capture = std::move(alphabet)]()
{ std::cout << *capture << endl; };
// alphabet is empty henceforth as contents have been 'moved'
lambda();
编写使用多个线程的应用程序时,可考虑使用 std::shared_ptr 和 std::weak_ptr,它们可帮助您实现线程安全和引用计数对象共享。
C++ 标准库提供的智能指针并不能满足所有程序员的需求,这就是还有很多其他智能指针库的原因。www.boost.org 提供了一些经过测试且文档完善的智能指针类,还有很多其他的实用类。
使用流进行输入和输出
流的概述
假设您要开发一个程序,它从磁盘读取数据,将数据显示到屏幕上,从键盘读取用户输入以及将数据存储到磁盘中。在这种情况下,倘若不管数据来自或前往什么设备或位置,都能以相同的方式处理读写操作,那该有多好!这正是 C++ 流提供的功能。
C++ 流是读写(输入和输出)逻辑的通用实现,让你能够用统一的模式读写数据。不管是磁盘或键盘读取数据,还是将输入写入显示器或磁盘,这些模式都相同。您只需使用合适的流类,类的实现将负责处理与设备和操作系统相关的细节。
再来看一下你编写的第一个程序中相关的代码行:
std::cout << "Hello World!" << std::endl;
std:cout 是 ostream 类的一个对象,用于输出到控制台。要使用 std::cout,需要包含提供它的头文件<iostream>,这个头文件还提供了 std::cin,让你能够从流中读取数据。
那么,我说流让您能够以一致的方式访问不同的设备时,是什么意思呢?如果要将 Hello World 写入文本文件,可将同样的语法用于文件流对象 fsHello:
fsHello << "Hello World!" << endl; // "Hello World!" into a file stream
正如您看到的,选择正确的流类后,将 Hello World 写入文件与将其显示到屏幕上并没有太大的不同。
用于写入流时,运算符 << 被称为流插入运算符,可将其用于写入屏幕、文件等。用于将流中的数据写入变量时,运算符 >> 被称为流提取运算符,可将其用于从键盘、文件等读取输入。
重要的 C++流类和流对象
C++ 提供了一组标准类和头文件,可帮助你执行重要而常见的输入/输出操作。下表列出了常用的 C++ 流类:
| 类/对象 | 用途 |
|---|---|
| cout | 标准输出流,通常被重定向到控制台 |
| cin | 标准输入流,通常用于将数据读入变量 |
| cerr | 用于显示错误信息的标准输出流 |
| fstream | 用于操作文件的输入和输出流,继承了 ofstream 和 ifstream |
| ofstream | 用于操作文件的输出流类,即用于创建文件 |
| ifstream | 用于操作文件的输入流类,即用于读取文件 |
| stringstream | 用于操作字符串的输入和输出流类,继承了 istringstream 和 ostringstream,通常用于在字符串和其他类型之间进行转换 |
cout、cin 和 cerr 分别是流类 ostream、istream 和 ostream 的全局对象。由于是全局对象,它们在 main() 开始之前就已初始化。
使用流类时,可指定为你执行特定操作的控制符(manipulator)。std::endl 就是一个这样的控制符,你一直在使用它来插入换行符:
std::cout << "This lines ends here" << std::endl;
下表列出了其他几个控制符和标志。
| 类/对象 | 用途 |
|---|---|
| 输出控制符 | |
| endl | 插入一个换行符 |
| ends | 插入一个空字符 |
| 基数控制符 | |
| dec | 让流以十进制方式解释输入或显示输出 |
| hex | 让流以十六进制方式解释输入或显示输出 |
| oct | 让流以八进制方式解释输入或显示输出 |
| 浮点数表示控制符 | |
| fixed | 让流以定点表示法显示数据 |
| scientific | 让流以科学表示法显示数据 |
| <iomanip> 控制符 | |
| setprecision | 设置小数精度 |
| setw | 设置字段宽度 |
| setfill | 设置填充字符 |
| setbase | 设置基数,与使用 dec、hex 或 oct 等效 |
| setiosflag | 通过类型为 std::ios_base::fmtflags 的掩码输入参数设置标志 |
| resetiosflag | 将 std::ios_base::fmtflags 参数指定的标志重置为默认值 |
std::cout
如下代码演示了如何使用 cout 和 <iomanip> 控制符以十进制、十六进制和八进制格式显示整数:
#include <iostream>
#include <iomanip>
using namespace std;
int main()
{
cout << "Enter an integer: ";
int input = 0;
cin >> input;
cout << "Integer in octal: " << oct << input << endl;
cout << "Integer in hexadecimal: " << hex << input << endl;
cout << "Integer in hex using base notation: ";
cout << setiosflags(ios_base::hex | ios_base::showbase | ios_base::uppercase);
cout << input << endl;
cout << "Integer after resetting I/O flags: ";
cout << resetiosflags(ios_base::hex | ios_base::showbase | ios_base::uppercase);
cout << input << endl;
return 0;
}
输出:
Enter an integer: 253
Integer in octal: 375
Integer in hexadecimal: fd
Integer in hex using base notation: 0XFD
Integer after resetting I/O flags: 253
第 15 行使用了 setiosflags() 让 cout 以十六进制方式(并使用大写字母)显示该数字,其结果是 cout 将 253 显示为 OXFD。第 19 行使用了 resetiosflags(),其效果是再次使用 cout 显示该整数时,将显示为十进制。要将显示整数时使用的基数改为十进制,也可使用下面这种方式:
cout << dec << input << endl; // displays in decimal
对于诸如 Pi 等数字,可指定 cout 显示它们时使用的精度(小数点后面的位数),还可指定以定点表示法或科学表示法显示它们。如下代码演示了使用 cout 以定点表示法和科学表示法显示 Pi 和圆面积:
#include <iostream>
#include <iomanip>
using namespace std;
int main()
{
const double Pi = (double)22.0 / 7;
cout << "Pi = " << Pi << endl;
cout << endl << "Setting precision to 7:" << endl;
cout << setprecision(7);
cout << "Pi = " << Pi << endl;
cout << fixed << "Fixed Pi = " << Pi << endl;
cout << scientific << "Scientific Pi = " << Pi << endl;
cout << endl << "Setting precision to 10:" << endl;
cout << setprecision(10);
cout << "Pi = " << Pi << endl;
cout << fixed << "Fixed Pi = " << Pi << endl;
cout << scientific << "Scientific Pi = " << Pi << endl;
cout << endl << "Enter a radius: ";
double radius = 0.0;
cin >> radius;
cout << "Area of circle: " << 2 * Pi*radius*radius << endl;
return 0;
}
输出:
Pi = 3.14286
Setting precision to 7:
Pi = 3.142857
Fixed Pi = 3.1428571
Scientific Pi = 3.1428571e+000
Setting precision to 10:
Pi = 3.1428571429e+000
Fixed Pi = 3.1428571429
Scientific Pi = 3.1428571429e+000
Enter a radius: 9.99
Area of circle: 6.2731491429e+002
输出表明,第 11 行和第 17 行分别将精度设置为 7 和 10 后,显示的 Pi 值不同。另外,控制符 scientific 导致计算得到的圆面积被显示为 6.2731491429e+002。
可使用 setw() 控制符来设置字段宽度,插入到流中的内容将在指定宽度内右对齐。在这种情况下,还可使用 setfill() 指定使用什么字符来填充空白区域,如下代码使用控制符 setw() 设置字段宽度,并使用 setfill() 指定填充字符:
#include <iostream>
#include <iomanip>
using namespace std;
int main()
{
cout << "Hey - default!" << endl;
cout << setw(35); // set field width to 25 columns
cout << "Hey - right aligned!" << endl;
cout << setw(35) << setfill('*');
cout << "Hey - right aligned!" << endl;
cout << "Hey - back to default!" << endl;
return 0;
}
打印如下:
Hey - default!
Hey - right aligned!
***************Hey - right aligned!
Hey - back to default!
从输出可知,第 12 行导致使用 setfill() 指定的星号来填充文本前的空白区域。
std::cin
使用 cin 可将标准输入读取到 int、double 和 char 变量中,也可将输入直接写入 char 数组(C 风格字符串):
cout << "Enter a line: " << endl;
char charBuf [10] = {0}; // can contain max 10 chars
cin >> charBuf; // Danger: user may enter more than 10 chars
写入 C 风格字符串缓冲区时,务必不要超越缓冲区的边界,以免导致程序崩溃或带来安全隐患,这至关重要。因此,将输入读取到 char 数组(C 风格字符串)时,下面是一种更好的方法:
cout << "Enter a line: " << endl;
char charBuf[10] = {0};
cin.get(charBuf, 9); // stop inserting at the 9th character
这种将文本插入到 char 数组(C 风格字符串)的方式更安全,代码如下:
#include<iostream>
#include<string>
using namespace std;
int main()
{
cout << "Enter a line: " << endl;
char charBuf[10] = { 0 };
cin.get(charBuf, 9);
cout << "charBuf: " << charBuf << endl;
return 0;
}
输出:
Enter a line:
Testing if I can cross the bounds of the buffer
charBuf: Testing i
从输出可知,只将用户输入的前 9 个字符读取到了 char 缓冲区中,这是因为第 9 行使用的是 cin:get() 处理长度给定的缓冲区时,这是最安全的方式。但是,尽可能不要使用 char 数组;而应使用 std::string 而不是 char*。
你甚至可使用它将用户输入的字符串直接读取到 std::string 中:
#include<iostream>
#include<string>
using namespace std;
int main()
{
cout << "Enter your name: ";
string name;
cin >> name;
cout << "Hi " << name << endl;
return 0;
}
输出:
Enter your name: Siddhartha Rao
Hi Siddhartha
输出表明,只显示了我的名字,而不是整个输入字符串,为什么会这样呢?这是因为 cin 遇到空白后停止插入。要读取整行输入(包括空白),需要使用 getline():
string name;
getline(cin, name);
std::fstream
C++ 提供了 std::fstream,旨在以独立于平台的方式访问文件。std::fstream 从 std::ofstream 那里继承了写入文件的功能,并从 std::ifstream 那里继承了读取文件的功能。换句话说,std::fstream 提供了读写文件的功能。要使用 std::fstream 类或其基类,需要包含头文件 <fstream>。
要使用 fstream、ofstream 或 ifstream 类,需要使用方法 open() 打开文件:
fstream myFile;
myFile.open("HelloFile.txt",ios_base::in|ios_base::out|ios_base::trunc);
if (myFile.is_open()) // check if open() succeeded
{
// do reading or writing here
myFile.close();
}
open() 接受两个参数:第一个是要打开的文件的路径和名称(如果没有提供路径,将假定为应用程序的当前目录);第二个是文件的打开模式。在上述代码中,指定了模式 ios_base::trunc(即便指定的文件存在,也重新创建它)、ios_base::in(可读取文件)和 ios_base::out(可写入文件)。
注意到在上述代码中使用了 is_open(),它检测 open() 是否成功。
保存到文件时,必须使用 close()关闭文件流。
还有另一种打开文件流的方式,那就是使用构造函数:
fstream myFile("HelloFile.txt",ios_base::in|ios_base::out|ios_base::trunc);
如果只想打开文件进行写入,可使用如下代码:
ofstream myFile("HelloFile.txt", ios_base::out);
如果只想打开文件进行读取,可使用如下代码:
ifstream myFile("HelloFile.txt", ios_base::in);
无论是使用构造函数还是成员方法 open() 来打开文件流,都建议您在使用文件流对象前,使用 open() 检查文件打开操作是否成功。
可在下述各种模式下打开文件流。
- ios_base::app:附加到现有文件末尾,而不是覆盖它。
- ios_base::ate:切换到文件末尾,但可在文件的任何地方写入数据。
- ios_base::trunc:导致现有文件被覆盖,这是默认设置。
- ios_base::binary:创建二进制文件(默认为文本文件)。
- ios_base::in:以只读方式打开文件。
- ios_base::out:以只写方式打开文件。
有打开的文件流后,便可使用运算符 << 向其中写入文本:
#include<fstream>
#include<iostream>
using namespace std;
int main()
{
ofstream myFile;
myFile.open("HelloFile.txt", ios_base::out);
if (myFile.is_open())
{
cout << "File open successful" << endl;
myFile << "My first text file!" << endl;
myFile << "Hello file!";
cout << "Finished writing to file, will close now" << endl;
myFile.close();
}
return 0;
}
输出:
File open successful
Finished writing to file, will close now
HelloFile.txt 文件的内容如下:
My first text file!
Hello file!
要读取文件,可使用 fstream 或 ifstream,并使用标志 ios_base::in 打开它。如下代码演示了如何前面创建的文件 HelloFile.txt。
#include<fstream>
#include<iostream>
#include<string>
using namespace std;
int main()
{
ifstream myFile;
myFile.open("HelloFile.txt", ios_base::in);
if (myFile.is_open())
{
cout << "File open successful. It contains: " << endl;
string fileContents;
while (myFile.good())
{
getline(myFile, fileContents);
cout << fileContents << endl;
}
cout << "Finished reading file, will close now" << endl;
myFile.close();
}
else
cout << "open() failed: check if file is in right folder" << endl;
return 0;
}
输出:
File open successful. It contains:
My first text file!
Hello file!
Finished reading file, will close now
写入二进制文件的流程与前面介绍的流程差别不大,重要的是在打开文件时使用 ios_base::binary 标志。通常使用 ofstream::write 和 ifstream::read 来读写二进制文件,代码如下所示。
#define _CRT_SECURE_NO_WARNINGS
#include<fstream>
#include<iomanip>
#include<string>
#include<iostream>
using namespace std;
struct Human
{
Human() {};
Human(const char* inName, int inAge, const char* inDOB) : age(inAge)
{
strcpy(name, inName);
strcpy(DOB, inDOB);
}
char name[30];
int age;
char DOB[20];
};
int main()
{
Human Input("Siddhartha Rao", 101, "May 1916");
ofstream fsOut("MyBinary.bin", ios_base::out | ios_base::binary);
if (fsOut.is_open())
{
cout << "Writing one object of Human to a binary file" << endl;
fsOut.write(reinterpret_cast<const char*>(&Input), sizeof(Input));
fsOut.close();
}
ifstream fsIn("MyBinary.bin", ios_base::in | ios_base::binary);
if (fsIn.is_open())
{
Human somePerson;
fsIn.read((char*)&somePerson, sizeof(somePerson));
cout << "Reading information from binary file: " << endl;
cout << "Name = " << somePerson.name << endl;
cout << "Age = " << somePerson.age << endl;
cout << "Date of Birth = " << somePerson.DOB << endl;
}
return 0;
}
输出:
Writing one object of Human to a binary file
Reading information from binary file:
Name = Siddhartha Rao
Age = 101
Date of Birth = May 1916
在 main() 函数中先创建了结构 Human 的一个实例 Input(该结构包含属性 name、age 和 DOB),并使用 ofstream 将其持久化到磁盘中的二进制文件 MyBinary.bin 中。接下来,使用另一个类型为 ifstream 的流对象读取这些信息。输出的 name 等属性是从二进制文件中读取的。该示例还演示了如何使用 ifstream::read 和 ofstream::write 来读写文件。注意到第 31 行使用了 reinterpret_cast,它让编译器将结构解释为 char*。第 40 行使用 C 风格类型转换方式,这与第 31 行的类型转换方式等价。
假设您有一个字符串,它包含字符串值 45,如何将其转换为整型值 45 呢?如何将整型值 45 转换为字符串 45 呢?C++提供的 stringstream 类是最有用的工具之一,让您能够执行众多的转换操作。要使用 std::stringstream 类,需要包含头文件 <sstream>。
#include<fstream>
#include<sstream>
#include<iostream>
using namespace std;
int main()
{
cout << "Enter an integer: ";
int input = 0;
cin >> input;
stringstream converterStream;
converterStream << input;
string inputAsStr;
converterStream >> inputAsStr;
cout << "Integer Input = " << input << endl;
cout << "String gained from integer = " << inputAsStr << endl;
stringstream anotherStream;
anotherStream << inputAsStr;
int Copy = 0;
anotherStream >> Copy;
cout << "Integer gained from string, Copy = " << Copy << endl;
return 0;
}
输出:
Enter an integer: 45
Integer Input = 45
String gained from integer = 45
Integer gained from string, Copy = 45
这段代码让用户输入一个整型值,并使用运算符 << 将其插入到一个 stringstream 对象中,如第 13 行所示;然后,您使用提取运算符将这个整数转换为 string,如第 15 行所示。接下来,您将存储在 inputAsStr 中的字符串转换为整数,并将其存储到 Copy 中。
异常处理
导致异常的原因
异常可能是外部因素导致的,如系统没有足够的内存;也可能是应用程序内部因素导致的,如使用的指针包含无效值或除数为零。为了向调用者指出错误,有些模块引发异常。
使用 try 和 catch 捕获异常
成功分配内存时,默认形式的 new 返回一个指向该内存单元的有效指针,但失败时将引发异常。如下代码演示了如何捕获使用 new 分配内存时可能引发的异常,并在计算机不能分配请求的内存时进行处理。
#include <iostream>
using namespace std;
int main()
{
cout << "Enter number of integers you wish to reserve: ";
try
{
int input = 0;
cin >> input;
// Request memory space and then return it
int* numArray = new int[input];
delete[] numArray;
}
catch (...)
{
cout << "Exception occurred. Got to end, sorry!" << endl;
}
return 0;
}
输出:
Enter number of integers you wish to reserve: -1
Exception occurred. Got to end, sorry!
这里我请求为 −1 个整数预留内存。这很荒谬,但用户经常做荒谬的事。如果没有异常处理,该程序将以讨厌的方式终止。但由于有异常处理程序,程序显示了一条提示消息:Got to end, sorry!。
上面的代码演示了 try 块和 catch 块的用法。catch() 像函数一样接受参数,参数…意味着 catch 块将捕获所有的异常。然而,在这个示例中,您可能想指定特定的异常类型 std::bad_alloc,因为这是 new 失败时引发的异常。通过捕获特定类型的异常,有助于处理这种类型的异常,如显示一条消息,准确地指出出现了什么问题。
如果异常的类型是已知的,更推荐捕获这种类型的异常,这样你就能根据异常类型执行有针对性的处理,比如向用户提示一条准确的信息:
#include <iostream>
#include<exception> // include this to catch exception bad_alloc
using namespace std;
int main()
{
cout << "Enter number of integers you wish to reserve: ";
try
{
int input = 0;
cin >> input;
// Request memory space and then return it
int* numArray = new int[input];
delete[] numArray;
}
catch (std::bad_alloc& exp)
{
cout << "Exception encountered: " << exp.what() << endl;
cout << "Got to end, sorry!" << endl;
}
catch (...)
{
cout << "Exception encountered. Got to end, sorry!" << endl;
}
return 0;
}
输出:
Enter number of integers you wish to reserve: -1
Exception encountered: bad array new length
Got to end, sorry!
跟前面的代码相比,这段代码能够提供应用程序中断的准确原因,即 bad array new length。捕获类型为 bad_alloc& 的异常,如第 17~21 行所示,这种异常是由 new 引发的。
你还可以使用关键字 throw 自己在代码中引发异常:
#include<iostream>
using namespace std;
double Divide(double dividend, double divisor)
{
if (divisor == 0)
throw "Dividing by 0 is a crime";
return (dividend / divisor);
}
int main()
{
cout << "Enter dividend: ";
double dividend = 0;
cin >> dividend;
cout << "Enter divisor: ";
double divisor = 0;
cin >> divisor;
try
{
cout << "Result is: " << Divide(dividend, divisor);
}
catch (const char* exp)
{
cout << "Exception: " << exp << endl;
cout << "Sorry, can't continue!" << endl;
}
return 0;
}
输出:
Enter dividend: 2011
Enter divisor: 0
Exception: Dividing by 0 is a crime
Sorry, can't continue!
通过捕获类型为 char*的异常(第 25 行),可捕获调用函数 Divide() 可能引发的异常(第 7 行)。另外,这里没有将整个 main() 都放在 try{}; 中,而只在其中包含可能引发异常的代码。这是一种不错的做法,因为异常处理可能降低代码的执行性能。
异常处理的工作原理
前面你在函数 Divide() 中抛出了一个类型为 char* 的异常,并在函数 main() 中使用处理程序 catch(char*) 捕获它。每当你使用 throw 引发异常时,编译器都将查找能够处理该异常的 catch(Type)。异常处理逻辑首先检查引发异常的代码是否包含在 try 块中,如果是,则查找可处理这种异常的 catch(Type)。如果 throw 语句不在 try 块内,或者没有与引发的异常兼容的 catch(),异常处理逻辑将继续在调用函数中寻找。因此,异常处理逻辑沿调用栈向上逐个地在调用函数中寻找,直到找到可处理异常的 catch(Type)。在退栈过程的每一步中,都将销毁当前函数的局部变量,因此这些局部变量的销毁顺序与创建顺序相反,看下面的代码:
#include <iostream>
using namespace std;
struct StructA
{
StructA() { cout << "StructA constructor" << endl; }
~StructA() { cout << "StructA destructor" << endl; }
};
struct StructB
{
StructB() { cout << "StructB constructor" << endl; }
~StructB() { cout << "StructB destructor" << endl; }
};
void FuncB() // throws
{
cout << "In Func B" << endl;
StructA objA;
StructB objB;
cout << "About to throw up!" << endl;
throw "Throwing for the heck of it";
}
void FuncA()
{
try
{
cout << "In Func A" << endl;
StructA objA;
StructB objB;
FuncB();
cout << "FuncA: returning to caller" << endl;
}
catch (const char* exp)
{
cout << "FuncA: Caught exception: " << exp << endl;
cout << "Handled it, will not throw to caller" << endl;
// throw; // uncomment this line to throw to main()
}
}
int main()
{
cout << "main(): Started execution" << endl;
try
{
FuncA();
}
catch (const char* exp)
{
cout << "Exception: " << exp << endl;
}
cout << "main(): exiting gracefully" << endl;
return 0;
}
打印:
main(): Started execution
In Func A
StructA constructor
StructB constructor
In Func B
StructA constructor
StructB constructor
About to throw up!
StructB destructor
StructA destructor
StructB destructor
StructA destructor
FuncA: Caught exception: Throwing for the heck of it
Handled it, will not throw to caller
main(): exiting gracefully
这里 main() 调用了 FuncA(),FuncA() 调用了 FuncB(),而 FuncB() 抛出异常,如第 22 行所示。函数 FuncA() 和 main() 都能处理这种异常,因为它们都包含 catch(const char*)。抛出异常的 FuncB() 没有 catch() 块,因此 FuncB() 引发的异常将首先由 FuncA() 中的 catch 块(第 35~40 行)处理,因为是 FuncA( )调用了 FuncB()。注意到 FuncA() 认为这种异常不严重,没有继续将其传播给 main()。因此,在 main() 看来,就像没有问题发生一样。如果解除对第 39 行的注释,异常将传播给 FuncA 的调用者,即 main() 也将收到这种异常。
输出指出了对象的创建顺序(与实例化它们的代码的排列顺序相同),还指出了引发异常后对象被销毁的顺序(与实例化顺序相反)。不仅在引发异常的 FuncB() 中创建的对象被销毁,在调用 FuncB() 并处理异常的 FuncA() 中创建的对象也被销毁。
引发异常时将对局部对象调用析构函数。如果因出现异常而被调用的析构函数也引发异常,将导致应用程序异常终止。
前面捕获 std::bad_alloc 时,实际上是捕获 new 引发的 std::bad_alloc 对象。std::bad_alloc 继承了 C++标准类 std::exception,而 std::exception 是在头文件中声明的。
下述重要异常类都是从 std::exception 派生而来的:
- bad_alloc:使用 new 请求内存失败时引发。
- bad_cast:试图使用 dynamic_cast 转换错误类型(没有继承关系的类型)时引发。
- ios_base::failure:由 iostream 库中的函数和方法引发。
std::exception 类是异常基类,它定义了虚方法 what(),这个方法详细地描述了导致异常的原因。前面的示例中通过打印 exp.what() 提供了信息: bad array new length,让用户知道什么地方出了问题。由于 std::exception 是众多异常类型的基类,因此可使用 catch(const exception&) 捕获所有将 std::exception 作为基类的异常:
void SomeFunc()
{
try
{
// code made exception safe
}
catch (const std::exception& exp) // catch bad_alloc, bad_cast, etc
{
cout << "Exception encountered: " << exp.what() << endl;
}
}
可以自定义异常继承 std::exception, 这样做的好处在于现有的异常处理程序 catch(const std::exception&) 不但能捕获 bad_alloc、bad_cast 等异常,还能捕获自定义异常:
#include <exception>
#include <iostream>
#include <string>
using namespace std;
class CustomException : public std::exception
{
string reason;
public:
// constructor, needs reason
CustomException(const char* why) :reason(why) {}
// redefining virtual function to return 'reason'
virtual const char* what() const throw()
{
return reason.c_str();
}
};
double Divide(double dividend, double divisor)
{
if (divisor == 0)
throw CustomException("CustomException: Dividing by 0 is a crime");
return (dividend / divisor);
}
int main()
{
cout << "Enter dividend: ";
double dividend = 0;
cin >> dividend;
cout << "Enter divisor: ";
double divisor = 0;
cin >> divisor;
try
{
cout << "Result is: " << Divide(dividend, divisor);
}
catch (exception& exp)// catch CustomException, bad_alloc, etc
{
cout << exp.what() << endl;
cout << "Sorry, can't continue!" << endl;
}
return 0;
}
输出:
Enter dividend: 2011
Enter divisor: 0
CustomException: Dividing by 0 is a crime
Sorry, can't continue!
这里在发生异常时抛出了 CustomException 类的实例,CustomException 继承了 std::exception,实现了虚函数 what(),如第 14~17 所示;该函数返回引发异常的原因。在 main() 中,第 30~44 行的 catch(exception&) 不但处理异常 CustomException,还处理 bad_alloc 等其他将 exception 作为基类的异常。
注意这里虚方法 CustomException::what() 的声明(如第 14 行所示):
virtual const char* what() const throw()
它以 throw() 结尾,这意味着这个函数本身不会引发异常。这是对异常类的一个重要约束,如果您在该函数中包含一条 throw 语句,编译器将发出警告。如果函数以 throw(int) 结尾,意味着该函数可能引发类型为 int 的异常。
线程
你可以使用操作系统提供的信号量(semaphore)和互斥量(mutex)来同步线程。
互斥量(互斥同步对象)通常用于避免多个线程同时访问同一段代码。换句话说,互斥量指定了一段代码,其他线程要执行它,必须等待当前执行它的线程结束并释放该互斥量。接下来,下一个线程获取该互斥量,完成其工作,并释放该互斥量。从 C++11 起,C++通过类 std::mutex 提供了一种互斥量实现,这个类位于头文件 <mutex> 中。
通过使用信号量,可指定多少个线程可同时执行某个代码段。只允许一个线程访问的信号量被称为二值信号量(binary semaphore)。