如果我可以编辑一下...我认为这对于初学者来说是一个看似困难的问题,并且如上所述应该在任何尝试编码之前就在设计审查中指出问题。它告诉您在 C++ 中做一些不好/典型/惯用/不恰当的事情,并用妨碍实际开发逻辑的问题分散您的注意力。
考虑您编写的核心算法(Antonio 已更正):可以理解和讨论该算法,而无需担心 A 和 B 是如何实际传入以供此代码使用的,或者它到底是哪种集合。如果它们是 std::vector、std::array 或原始 C 数组,则用法相同。同样,如何从代码中返回结果?您在此处填充ans,它如何进入和/或退出代码并返回到main 并不相关。
原始 C 数组不是 C++ 中的一等对象,对于它们如何作为参数传递有特殊规则(从 C 继承)。
返回更糟糕,返回动态大小的东西是 C 语言中的一个主要问题,像这样的内存管理是错误和安全漏洞的主要来源。我们想要的是值语义。
其次,使用数组和下标在 C++ 中不是惯用的。您使用 迭代器 并抽象出集合的确切性质。如果您有兴趣编写本身不处理内存管理的超高效后端代码(它由处理所涉及的实际集合的其他代码调用),它看起来像 std::merge 这是一个古老的函数回到 90 年代初。
template< class InputIt1, class InputIt2, class OutputIt >
OutputIt merge( InputIt1 first1, InputIt1 last1,
InputIt2 first2, InputIt2 last2,
OutputIt d_first );
您可以找到具有相似签名的其他人,它们将两个不同的范围用于输入和输出到第三个区域。如果你完全像这样写addp,你可以用硬编码大小的原始C数组调用它:
int8_t A[] {0,0,0,1,1,1};
int8_t B[] {1,0,1,1,0,1};
int8_t C[ ??? ];
using std::begin; std::end;
addp (begin(A),end(A), begin(B), end(B), begin(C));
请注意,由调用者准备足够大的输出区域,并且没有错误检查。
但是,相同的代码可以用于向量,甚至是不同容器类型的任意组合。这可以通过传递插入迭代器来填充std::vector。但在这个特殊的算法中,这很困难,因为你是在逆序计算它。
std::array
改进原始 C 数组的情况,您可以使用std::array 类,它完全相同的数组,但没有奇怪的传递/返回规则。它实际上只是一个包装结构内的原始 C 数组。请参阅此文档:https://en.cppreference.com/w/cpp/container/array
所以你可以写成:
using BBBNum1 = std::array<int8_t, 6>
BBBNum1 addp (const BBBNum1& A, const BBBNum1& B) { ... }
里面的代码可以和你一样使用A[i]等,但也可以通过A.size()获取大小。这里的问题是输入的长度相同,输出也相同(不是大 1)。使用模板,它可以写成灵活的长度,但仍然只在编译时指定。
std::vector
vector 类似于一个数组,但具有运行时长度。它是动态的,是您应该在 C++ 中使用的首选集合。
using BBBNum2 = std::vector<int8_t>
BBBNum2 addp (const BBBNum2& A, const BBBNum2& B) { ... }
同样,该函数内部的代码可以引用B[j]等,并使用B.size()与array集合完全相同。但是现在,大小是一个运行时属性,并且每个属性都可以不同。
您可以通过将大小作为构造函数参数来创建结果,就像在我的第一篇文章中一样,然后您可以按值返回 vector。请注意,如果您编写,编译器将有效地执行此操作,并且实际上不必复制任何内容:
auto C = addp (A, B);
现在开始真正的工作
好的,既然这种干扰至少已经消失了,您可以担心实际编写实现。我希望您确信使用 vector 代替 C 原始数组不会影响您的问题逻辑,甚至不会影响使用下标的(可用)语法。特别是由于问题涉及伪代码,我将其对“数组”的使用解释为“合适的可索引集合”,而不是具体的原始 C 数组类型。
同时处理 2 个序列并处理不同长度的问题实际上是一个通用的想法。在 C++20 中,Range 库有一些东西可以快速解决这个问题。旧的第 3 方库也存在,您可能会发现它称为 zip 或类似名称。
但是,让我们从头开始编写它。
您想从两个输入中一次读取一个项目,但要巧妙地使其看起来它们的长度相同。你不想写三遍相同的代码,或者详细说明 A 更短或 B 可能更短的情况......只是抽象出它们一起读取的想法,如果一个用完它提供零.
这是它自己的一段代码,可以应用于 A 和 B 两次。
class backwards_bit_reader {
const BBBnum2& x;
size_t index;
public:
backwards_bit_reader(const BBBnum2& x) : x{x}, index{x.size()} {}
bool done() const { return index == 0; }
int8_t next()
{
if (done()) return 0; // keep reading infinite leading zeros
--index;
return x[index];
}
};
现在您可以编写如下内容:
backwards_bit_reader A_in { A };
backwards_bit_reader B_in { B };
while (!A_in.done() && !B_in.done()) {
const a = A_in.next();
const b = B_in.next();
const c = a+b+carry;
carry = c/2; // update
C[--k]= c%2;
}
C[0]= carry; // the final bit, one longer than the input
它可以写得更紧凑,但这很清楚。
另一种方法
问题是,写backwards_bit_reader 是否超出了您目前所学的范围?在不重复语句的情况下,您还能如何将相同的逻辑应用于 A 和 B?
您应该学会识别有时称为“代码异味”的东西。多次重复相同的代码块,重复相同的步骤而没有任何改变,但它应用于哪个变量,应该被视为丑陋和不可接受的。
如果它们的长度不同,您至少可以通过确保 B 始终是较长的情况来减少这些情况。如果不是这种情况,请交换 A 和 B,作为初步步骤。 (实际上实现那个井是另一个题外话)
但是逻辑仍然几乎是重复的,因为您必须处理进位一直传播到最后的可能性。刚才你有 2 个副本而不是 3 个。
至少在外观上扩展较短的循环是编写循环的唯一方法。
这个问题有多现实?
它被简化到了愚蠢的地步,但是如果不是在base 2中完成而是具有更大的值,这实际上是在实现多精度算术,这是人们想要做的真实事情。这就是为什么我将BBBNum 上面的类型命名为“Bad Binary Bignum”。
了解实际的内存范围并希望代码快速和优化也是您有时想要做的事情。 BigNum 就是一个例子。您经常在字符串处理中看到这一点。但是我们想要创建一个高效的后端,它在不知道它是如何分配的情况下对内存进行操作,以及调用它的更高级别的包装器。
例如:
void addp (const int8_t* a_begin, const int8_t* a_end,
const int8_t* b_begin, const int8_t* b_end,
int8_t* result_begin, int8_t* result_end);
将使用提供的范围进行输出,不知道也不关心它是如何分配的,并采用任何连续范围的输入,而不关心使用什么类型的容器来管理它,只要它是连续的。请注意,正如您在 std::merge 示例中看到的那样,传递 begin 和 end 而不是 begin 和 size 更为惯用。
但是你有如下辅助函数:
BBBNum2 addp (const BBBNum2& A, const BBBNum2& B)
{
BBBNum result (1+std::max(A.size(),B.size());
addp (A.data(), A.data()+A.size(), B.data(), B.data()+B.size(), C.data(), C.data()+C.size());
}
现在普通用户可以使用向量和动态创建的结果来调用它,但它仍然可以调用数组、预分配的结果缓冲区等。