跳到主要内容

C++ 生成与变换

引言

在C++ STL(标准模板库)中,生成与变换算法是一组非常实用的工具,它们允许我们以声明式的方式对容器中的元素进行批量操作。本文将详细介绍这些算法,包括如何使用它们来创建新值或转换现有值,这对于数据处理和函数式编程风格的C++代码尤为重要。

生成算法

generate 和 generate_n

generategenerate_n是STL中用于填充容器的两个重要算法。它们通过调用指定的生成器函数来创建新值。

generate

generate算法在指定范围内的每个元素上应用一个生成器函数,并用生成的值替换原有元素。

语法:

cpp
void generate(ForwardIterator first, ForwardIterator last, Generator gen);

参数:

  • first, last: 定义操作范围的迭代器
  • gen: 不接受参数并返回要生成的值的函数对象

示例:

cpp
#include <algorithm>
#include <iostream>
#include <vector>

int main() {
// 创建一个包含5个元素的向量
std::vector<int> v(5);

// 使用generate填充向量,每个元素都是7
std::generate(v.begin(), v.end(), []() { return 7; });

// 打印向量
for (int n : v) {
std::cout << n << " ";
}
std::cout << "\n"; // 输出: 7 7 7 7 7

// 使用递增的计数器填充向量
int count = 0;
std::generate(v.begin(), v.end(), [&count]() { return count++; });

// 打印向量
for (int n : v) {
std::cout << n << " ";
}
std::cout << "\n"; // 输出: 0 1 2 3 4

return 0;
}

generate_n

generate_n算法与generate类似,但它指定要生成的元素数量而不是结束迭代器。

语法:

cpp
OutputIterator generate_n(OutputIterator first, Size n, Generator gen);

参数:

  • first: 开始填充的迭代器位置
  • n: 要生成的元素数量
  • gen: 生成器函数对象

示例:

cpp
#include <algorithm>
#include <iostream>
#include <vector>

int main() {
std::vector<int> v(3);

// 使用generate_n填充前2个元素,每个元素都是42
std::generate_n(v.begin(), 2, []() { return 42; });

// 打印向量
for (int n : v) {
std::cout << n << " ";
}
std::cout << "\n"; // 输出: 42 42 0

return 0;
}
提示

generate_n在处理固定数量元素时特别有用,而generate适合处理容器的整个范围或特定子范围。

变换算法

transform

transform算法是STL中最强大的算法之一,它将指定操作应用于范围内的元素,并将结果存储到目标范围。

单输入范围的transform

语法:

cpp
OutputIterator transform(InputIterator first1, InputIterator last1, 
OutputIterator result, UnaryOperation op);

参数:

  • first1, last1: 输入范围的迭代器
  • result: 输出范围的起始迭代器
  • op: 一元操作函数对象

示例:

cpp
#include <algorithm>
#include <iostream>
#include <vector>

int main() {
std::vector<int> v = {1, 2, 3, 4, 5};
std::vector<int> result(v.size());

// 对每个元素应用平方操作
std::transform(v.begin(), v.end(), result.begin(),
[](int x) { return x * x; });

// 打印结果
for (int n : result) {
std::cout << n << " ";
}
std::cout << "\n"; // 输出: 1 4 9 16 25

return 0;
}

双输入范围的transform

transform的另一个版本可以处理两个输入范围,对来自两个范围的元素对应用二元操作。

语法:

cpp
OutputIterator transform(InputIterator1 first1, InputIterator1 last1,
InputIterator2 first2, OutputIterator result,
BinaryOperation binary_op);

参数:

  • first1, last1: 第一个输入范围的迭代器
  • first2: 第二个输入范围的起始迭代器
  • result: 输出范围的起始迭代器
  • binary_op: 二元操作函数对象

示例:

cpp
#include <algorithm>
#include <iostream>
#include <vector>

int main() {
std::vector<int> v1 = {1, 2, 3, 4, 5};
std::vector<int> v2 = {10, 20, 30, 40, 50};
std::vector<int> result(v1.size());

// 将v1和v2中对应元素相加
std::transform(v1.begin(), v1.end(), v2.begin(), result.begin(),
[](int a, int b) { return a + b; });

// 打印结果
for (int n : result) {
std::cout << n << " ";
}
std::cout << "\n"; // 输出: 11 22 33 44 55

return 0;
}
警告

使用双输入范围版本的transform时,必须确保第二个输入范围至少与第一个输入范围一样长,否则可能会导致未定义行为。

实际应用案例

案例1: 生成随机数列

使用generate生成一个包含随机数的向量:

cpp
#include <algorithm>
#include <iostream>
#include <vector>
#include <random>

int main() {
// 创建一个随机数生成器
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> dis(1, 100);

// 创建一个包含10个元素的向量
std::vector<int> random_numbers(10);

// 使用generate填充随机数
std::generate(random_numbers.begin(), random_numbers.end(),
[&]() { return dis(gen); });

// 打印结果
std::cout << "随机数列: ";
for (int num : random_numbers) {
std::cout << num << " ";
}
std::cout << "\n";

return 0;
}

案例2: 处理图像数据

假设我们有一个简单的灰度图像,用二维向量表示。我们可以使用transform对图像应用阈值处理:

cpp
#include <algorithm>
#include <iostream>
#include <vector>

// 简单的二维向量来模拟图像
using GrayscaleImage = std::vector<std::vector<int>>;

// 显示图像
void printImage(const GrayscaleImage& img) {
for (const auto& row : img) {
for (int pixel : row) {
std::cout << (pixel > 0 ? "■" : "□") << " ";
}
std::cout << "\n";
}
}

int main() {
// 创建一个简单的8x8灰度图像
GrayscaleImage image = {
{10, 20, 30, 40, 50, 60, 70, 80},
{15, 25, 35, 45, 55, 65, 75, 85},
{5, 15, 25, 35, 45, 55, 65, 75},
{20, 30, 40, 50, 60, 70, 80, 90},
{25, 35, 45, 55, 65, 75, 85, 95},
{30, 40, 50, 60, 70, 80, 90, 100},
{35, 45, 55, 65, 75, 85, 95, 105},
{40, 50, 60, 70, 80, 90, 100, 110}
};

std::cout << "原始图像:\n";
printImage(image);

// 对每一行应用阈值处理
for (auto& row : image) {
std::transform(row.begin(), row.end(), row.begin(),
[](int pixel) { return pixel > 50 ? 1 : 0; });
}

std::cout << "\n阈值处理后的图像:\n";
printImage(image);

return 0;
}

案例3: 函数组合

使用transform可以轻松地组合多个函数操作,创建数据处理管道:

cpp
#include <algorithm>
#include <iostream>
#include <vector>
#include <string>
#include <cctype>

int main() {
std::vector<std::string> names = {"john", "ALICE", "Bob", "charlie"};
std::vector<std::string> normalized(names.size());

// 将所有名字标准化: 首字母大写,其余小写
std::transform(names.begin(), names.end(), normalized.begin(),
[](std::string name) {
// 先将所有字符转为小写
std::transform(name.begin(), name.end(), name.begin(),
[](unsigned char c) { return std::tolower(c); });

// 将首字母转为大写
if (!name.empty()) {
name[0] = std::toupper(name[0]);
}

return name;
});

// 打印结果
std::cout << "标准化后的名字:\n";
for (const auto& name : normalized) {
std::cout << name << "\n";
}

return 0;
}

总结

STL中的生成和变换算法为处理容器中的数据提供了强大而灵活的方式:

  1. 生成算法generategenerate_n)用于填充容器,非常适合初始化或重设容器的值。

  2. 变换算法transform)允许我们以函数式编程风格对元素应用操作,既可以处理单个范围,也可以组合两个范围。

  3. 这些算法与 lambda 表达式结合使用时特别强大,可以创建简洁而表达力强的代码。

  4. 实际应用中,这些算法可以用于数据预处理、信号处理、图像处理等多种场景。

通过掌握这些算法,你可以编写出更加简洁、高效且易于理解的C++代码,尤其是在处理大型数据集时。

练习

  1. 使用generate创建一个包含斐波那契数列前10个数的向量。

  2. 使用transform将一个字符串向量中的每个字符串转换为其长度值。

  3. 使用双范围版本的transform计算两个数值向量的点积(对应元素相乘后求和)。

  4. 结合generate_nback_inserter向一个空向量添加10个随机数字。

  5. 使用transform实现一个简单的加密/解密函数,对字符串中的每个字符应用Caesar密码(每个字符向前或向后移动固定位数)。

进一步阅读

  • C++ Reference: std::generate
  • C++ Reference: std::generate_n
  • C++ Reference: std::transform
  • 《Effective STL》by Scott Meyers
  • 《C++ Templates: The Complete Guide》by David Vandevoorde and Nicolai M. Josuttis