将数组的所有元素初始化为相同的数字

Initialize all the elements of an array to the same number

本文关键字:初始化 数字 元素 数组      更新时间:2023-10-16

前段时间,我的老老师发布了这段代码,说这是将数组初始化为相同数字的另一种方法(当然不是零)。

在这种情况下是三个。

他说,这种方式比for循环略好。为什么需要左移运算符?为什么我需要另一个长数组? 我不明白这里发生了什么。

int main() {
short int A[100];
long int v = 3;
v = (v << 16) + 3;
v = (v << 16) + 3;
v = (v << 16) + 3;
long *B = (long*)A;
for(int i=0; i<25; i++)
B[i] = v;
cout << endl;
print(A,100);
}

有很多方法可以用相同的值填充数组,如果您担心性能,则需要进行测量。

C++有一个专用函数来用值填充数组,我会使用它(在#include <algorithm>#include <iterator>之后):

std::fill(std::begin(A), std::end(A), 3);

你不应该低估优化编译器可以用这样的事情做什么。

如果你有兴趣看看编译器是做什么的,那么Matt Godbolt的编译器资源管理器是一个非常好的工具,如果你准备学习一点汇编程序。从这里可以看到,编译器可以优化对 12 个(和位)128 位存储的fill调用,并展开任何循环。由于编译器了解目标环境,因此无需在源代码中编码任何特定于目标的假设即可执行此操作。

他假设longshort长四倍(这不能保证;他应该使用int16_t和int64_t)。

他占用更长的内存空间(64 位),并用四个短(16 位)值填充它。他通过将位移动 16 个空格来设置值。

然后,他想将短裤数组视为长整型数组,因此他可以通过仅执行 25 次循环迭代而不是 100 次循环迭代来设置 100 个 16 位值。

这就是你的老师的想法,但正如其他人所说,这种投射是不确定的行为。

真是一堆猪圈。

  1. 对于初学者来说,v将在编译时计算。

  2. 取消引用B跟随long *B = (long*)A;的行为是未定义的,因为这些类型不相关。B[i]是对B的取消引用。

  3. 假设
  4. longshort大四倍是没有任何道理的。

以简单的方式使用for循环,并信任编译器进行优化。请漂亮,上面放糖。

这个问题有C++标签(没有 C 标签),所以这应该以C++风格完成:

// C++ 03
std::vector<int> tab(100, 3);
// C++ 11
auto tab = std::vector<int>(100, 3);
auto tab2 = std::array<int, 100>{};
tab2.fill(3);

此外,老师正在尝试比编译器更聪明,它可以做令人兴奋的事情。做这样的技巧是没有意义的,因为如果配置正确,编译器可以为您完成:

  • 您的代码程序集
  • 删除了勾号的代码程序集
  • 阵列方法
  • 矢量方法

如您所见,每个版本的-O2结果代码(几乎)相同。在-O1的情况下,技巧会有所改善。

所以底线,你必须做出选择:

  • 编写难以阅读的代码,不使用编译器优化
  • 编写可读代码并使用-O2

使用 Godbolt 站点试验其他编译器和配置。 另请参阅最新的cppCon演讲。

正如其他答案所解释的那样,该代码违反了类型别名规则,并做出了标准无法保证的假设。

如果您真的想手动进行此优化,这将是一种具有明确定义行为的正确方法:

long v;
for(int i=0; i < sizeof v / sizeof *A; i++) {
v = (v << sizeof *A * CHAR_BIT) + 3;
}
for(int i=0; i < sizeof A / sizeof v; i++) {
std:memcpy(A + i * sizeof v, &v, sizeof v);
}

关于对象大小的不安全假设通过使用sizeof来修复,而混叠冲突通过使用std::memcpy来修复,无论底层类型如何,它都有明确定义的行为。

也就是说,最好保持代码简单,让编译器发挥其魔力。


为什么我需要左移运算符?

关键是用较小整数的多个副本填充较大的整数。如果你把一个两字节的值s写成一个大的整数l,然后将位向左移动两个字节(我的固定版本应该更清楚这些幻数的来源),那么你将有一个整数,其中包含构成值s的两个字节副本。重复此操作,直到l中的所有字节对都设置为这些相同的值。要进行移位,您需要换档操作员。

当将这些值复制到包含双字节整数数组的数组上时,单个副本会将多个对象的值设置为较大对象的字节值。由于每对字节具有相同的值,因此数组中较小的整数也是如此。

为什么我需要另一个long数组?

没有long数组。只有一组short.

老师向您展示的代码是一个格式不正确的程序,不需要诊断,因为它违反了指针实际上指向它们声称指向的事物的要求(也称为"严格别名")。

举个具体的例子,编译器可以分析你的程序,注意A没有直接写入,也没有short写入,并证明A一旦创建就从未被更改过。

在C++标准下,所有这些弄乱B都可以证明无法在格式良好的程序中修改A

for(;;)循环甚至范围循环可能会优化为A的静态初始化。 在优化编译器下,教师的代码将针对未定义的行为进行优化。

如果你真的需要一种方法来创建用一个值初始化的数组,你可以使用这个:

template<std::size_t...Is>
auto index_over(std::index_sequence<Is...>) {
return [](auto&&f)->decltype(auto) {
return f( std::integral_constant<std::size_t, Is>{}... );
};
}
template<std::size_t N>
auto index_upto(std::integral_constant<std::size_t, N> ={})
{
return index_over( std::make_index_sequence<N>{} );
}
template<class T, std::size_t N, T value>
std::array<T, N> make_filled_array() {
return index_upto<N>()( [](auto...Is)->std::array<T,N>{
return {{ (void(Is),value)... }};
});
}

现在:

int main() {
auto A = make_filled_array<short, 100, 3>();
std::cout << "n";
print(A.data(),100);
}

在编译时创建填充数组,不涉及循环。

使用 godbolt,您可以看到数组的值是在编译时计算的,并且在我访问第 50 个元素时提取了值 3。

然而,这是矫枉过正(和 c++14)。

我认为他试图通过同时复制多个数组元素来减少循环迭代的次数。正如其他用户在这里已经提到的,此逻辑将导致未定义的行为。

如果这一切都是为了减少迭代,那么通过循环展开,我们可以减少迭代次数。但对于这种较小的阵列来说,它不会明显更快。

int main() {
short int A[100];
for(int i=0; i<100; i+=4)
{
A[i] = 3;
A[i + 1] = 3;
A[i + 2] = 3;
A[i + 3] = 3;
}
print(A, 100);
}

您可以使用std::array而不是内置数组以及一个make_array函数模板,该模板将返回一个将所有元素设置为3(或其他给定数字)的std::array,如下所示。

template<std::size_t N> std::array<int, N> constexpr make_array(int val)
{
std::array<int, N> tempArray{};
for(int &elem:tempArray)
{
elem = val;
}
return tempArray;
}
int main()
{
//-------------------------------V-------->number of elements  
constexpr auto arr  = make_array<8>(5);
//-------------------------------^---->value of element to be initialized with

//lets confirm if all objects have the expected value 
for(const auto &elem: arr)
{
std::cout << elem << std::endl; //prints all 5 
}

}

工作演示