【问题标题】:Printing argv with forrange loop使用 forrange 循环打印 argv
【发布时间】:2020-04-06 16:06:40
【问题描述】:
int main(int argc, const char** argv) {

    std::cout << "Hello" << std::endl;

    char arr2d[][4] = {"ABC", "DEF"};

    for (char *i : arr2d)
    {
        std::cout << i << std::endl;
    }

在这里,我将 forrange 的工作评估为:“对于arr2d 中的每个字符数组,将其打印到控制台”。这是有效的,所以,至少我的理解应该是正确的。上面代码sn-p的输出是,

muyustan@mint:~/Desktop/C_Files/oop$ g++ main.cpp -o main && ./main
Hello
ABC
DEF

正如预期的那样。

但是,如果我尝试这个,

int main(int argc, const char** argv) {

    std::cout << "Hello" << std::endl;

    char arr2d[][4] = {"ABC", "DEF"};

    for (const char *i : argv)
    {
        std::cout << i << std::endl;
    }

首先 IDE 会警告我,

这个基于范围的“for”语句需要一个合适的“begin”函数,但没有找到

如果我尝试编译,我会得到:

muyustan@mint:~/Desktop/C_Files/oop$ g++ main.cpp -o main && ./main
main.cpp: In function ‘int main(int, const char**)’:
main.cpp:30:26: error: ‘begin’ was not declared in this scope
     for (const char *i : argv)
                          ^~~~
main.cpp:30:26: note: suggested alternative:
In file included from /usr/include/c++/7/string:51:0,
                 from /usr/include/c++/7/bits/locale_classes.h:40,
                 from /usr/include/c++/7/bits/ios_base.h:41,
                 from /usr/include/c++/7/ios:42,
                 from /usr/include/c++/7/ostream:38,
                 from /usr/include/c++/7/iostream:39,
                 from main.cpp:1:
/usr/include/c++/7/bits/range_access.h:105:37: note:   ‘std::begin’
   template<typename _Tp> const _Tp* begin(const valarray<_Tp>&);
                                     ^~~~~
main.cpp:30:26: error: ‘end’ was not declared in this scope
     for (const char *i : argv)
                          ^~~~
main.cpp:30:26: note: suggested alternative:
In file included from /usr/include/c++/7/string:51:0,
                 from /usr/include/c++/7/bits/locale_classes.h:40,
                 from /usr/include/c++/7/bits/ios_base.h:41,
                 from /usr/include/c++/7/ios:42,
                 from /usr/include/c++/7/ostream:38,
                 from /usr/include/c++/7/iostream:39,
                 from main.cpp:1:
/usr/include/c++/7/bits/range_access.h:107:37: note:   ‘std::end’
   template<typename _Tp> const _Tp* end(const valarray<_Tp>&);

那么,为什么 argv 的行为与我的 arr2d[][4] 不同?不都是char指针(char数组还是字符串(?))的指针吗?

如果我的理解有问题,使用 forrange 打印 argv 的成分应该是什么结构?

【问题讨论】:

  • 不,argv 是一个指向 char 的指针。 arr2dchars 的数组。 数组不是指针.
  • @MilesBudnek 这是我被告知错误的事情,当时我在处理 C 时,在某个地方我读到“数组也是指针!”。
  • 您听说这很可能与在某些情况下衰减为指针的数组有关(参见例如stackoverflow.com/questions/1461432/what-is-array-decaying),但是将它们视为等价是错误的,并且是代码中许多误解和错误的根源
  • 那么你将如何定义一个指针数组呢?要使用范围循环检查 argv 的内容,请检查 this
  • 还要注意 C 不是 C++。

标签: c++ argv


【解决方案1】:

这是我被告知错误的事情,当时我在处理 C 时,在某个地方我读到“数组也是指针!”。

关于该声明,有几点必须理解。

  1. 在大多数情况下,数组衰减为指针,但数组仍然不同于指针。

    • 当用作sizeof的参数时,以下两个将导致不同的答案。

      char const* ptr = "Some text.";
      char array[] = "some text.";
      
      std::cout << sizeof(ptr) << std::endl;    // prints sizeof the pointer.
      std::cout << sizeof(array) << std::endl;  // prints sizeof the array.
      
    • 用作addressof 运算符的参数时。

      char const* ptr1 = "Some text.";
      char array[] = "some text.";
      
      char const** ptr2 = &ptr1;      // OK.
      char** ptr3 = &array;           // Error. Type mismatch.
      char (*ptr4}[11] = &array;      // OK.
      
  2. 二维数组可以衰减为指向一维数组的指针,但不会衰减为指向指针的指针。

     int array1[10];
     int* ptr1 = array1;          // OK. Array decays to a pointer
    
    
     int array2[10][20];
     int (*ptr2)[20] = array2;    // OK. 2D array decays to a pointer to 1D array.
     int**  ptr3 = array2;        // Error. 2D array does not decay to a pointer to a pointer.
    

【讨论】:

    【解决方案2】:

    它们的行为不同,因为它们有不同的类型。这让初学者感到困惑,但是:

    char **
    

    是指向char 的指针。实际上,在argv 的情况下,它指向一个指针序列,每个指针都指向一个以 nul 结尾的字符串(它们是字符序列)。

    迭代这些的问题是这些序列的大小是未知的。编译器无法知道argc 与上述第一个序列有关。

    但是:

    char arr2d[][4] = {"ABC", "DEF"};
    

    解析为类型:

    char [2][4]
    

    这是char的数组数组。在这种情况下,大小是已知的 (2),因此您可以对其进行迭代。

    最后,编译器抱怨std::begin,因为基于范围的for循环被转换成不同的等效代码,使用std::begin等来进行迭代。

    【讨论】:

    • 让我试着解释一下我的理解。所以,你无法知道char ** 的结尾,这是问题出在哪里吗?但是对于char *,它的终点默认是已知的,因为它是空终止的(\0),对吧?
    • @muyustan 有点!对于char *,编译器也不知道,但它可以工作,因为您正在调用std::cout &lt;&lt; i,这意味着打印从那里开始的字符串假设它是一个\0-终止的字符串(如果不是因为你弄错了,那么它是未定义的行为:程序将开始打印垃圾,直到找到\0 或在访问无效内存地址时崩溃)。
    • \0 插入到“字符串”的末尾通常发生在后台,例如,在启动char str[] = "hello" 时,对吧?因为我自己从来没有插入过xd
    • @muyustan 这取决于你做什么,对于你刚刚写的情况(字符串文字),它被添加了。一些功能也添加了它。其他人没有。如果您自己将字符串写入内存,则必须这样做。这就是为什么通常最好使用 std::string 代替,它会为您处理事情,除非您有特殊要求。
    【解决方案3】:

    如果您想将基于范围的for 应用到argv,首先创建一个包含参数的向量可能是最简单的:

    #include <iostream>
    #include <vector>
    
    int main(int argc, char **argv){ 
        std::vector<std::string> args(argv, argv+argc);
    
        for (auto const &arg : args) {
            std::cout << arg << "\n"; // don't use `endl`.
        }
    }
    

    argv 而言,与二维数组相比,区别相当简单。当你有一个数组声明时,数组可以通过引用模板来传递,模板可以计算出它的大小:

    template <class T, size_t N>
    size_t array_size(T (&array)[N]) {
        return N;
    }
    
    int foo[2][3];
    std::cout << array_size(foo) << "\n";
    
    char bar[12][13][14];
    std::cout << array_size(bar) << "\n";
    

    ...但是,argv 没有这样的静态可见定义,编译器可以从中推断出它的大小。在典型情况下,在main 之外运行的代码会检查命令行并动态分配它。

    【讨论】:

    • 我正要要求一个更解释性的答案,我现在将检查您编辑的版本。我还要说您似乎忘记添加#include &lt;vector&gt;
    • @usr:这取决于。如果这就是你所做的一切,那么可能是的。但是如果你用它做很多事情,把它放到一个向量中是完全值得的。当然,您也可以选择std::vector&lt;char *&gt; args(argv, argv+argc) 的中间地带。
    • 或介于两者之间,例如std::vector&lt;std::string_view&gt;
    • for (std::string_view s : std::span{argv, argc}) 在 C++20 中
    【解决方案4】:

    range-for 表达式适用于迭代器(指针是一种迭代器),它需要一个指向范围开头和结尾的迭代器。它通过将范围传递给std::beginstd::end 来获得这些。

    arr2d 的类型是char[2][4]。作为一个数组,它携带有关其大小的信息作为其类型的一部分。 std::beginstd::end 有模板重载,它们分别接受对数组的引用并返回指向其第一个和最后一个元素的指针。

    argv 的类型是char**。它只是一个指向char 的指针。编译器不知道这些指针中的任何一个指向数组的第一个元素,并且这些指针不携带有关它们指向的数组长度的信息。因此,没有接受指针的std::beginstd::end 的重载,因为std::end 无法仅从指针确定数组的末尾相对于开头的位置。

    要使用范围为的指针,您必须提供有关数组长度的信息。在这种情况下,您可以在数组上构造一个简单的视图,因为您从 argc 知道它的长度:

    template <typename T>
    class PointerRange
    {
    private:
        T* ptr_;
        std::size_t length_;
    
    public:
        PointerRange(T* ptr, std::size_t length)
            : ptr_{ptr},
              length_{length}
        {
        }
    
        T* begin() const { return ptr_; }
        T* end() const { return ptr_ + length_; }
    };
    
    int main(int argc, char** argv)
    {
        for (char* arg : PointerRange(argv, argc)) {
            std::cout << arg << "\n";
        }
    }
    

    Live Demo

    一旦 C++20 可用,std::span 可以代替上面定义的 PointerRange

    int main(int argc, char** argv)
    {
        for (std::string_view arg : std::span{argv, argc}) {
            std::cout << arg << "\n";
        }
    }
    

    【讨论】:

    • 甚至 C++20 中的 for (std::string_view s : std::span{argv, argc})
    • @Jarod42 是的,刚刚改了。我永远不能保持范围的东西直。
    • 好吧,这个答案现在远远高于我的水平,但是我大致理解的是:为了使 forrange 循环工作,可迭代的长度(或开始和结束)(我不知道知道正确的词)必须定义。因此,对于 2D 数组,它是已知的,但是对于 char **,它是未知的。这是问题的基础。我是否至少抓住了正确的轨道?
    • @muyustan 是的,这是正确的轨道。我进行了编辑以添加更多信息。基本上,数组的大小是已知的,但指针可以指向任何东西。你必须告诉范围——数组指向多长时间才能使用指针,你可以通过将指针包装在某种范围类中来做到这一点。
    【解决方案5】:

    如果你的编译器有 span 头文件(现在不是很多),我想这会起作用。

    // Example program
    #include <iostream>
    #include <string_view>
    #include <span>
    
    int main(int argc, char **argv)
    {
      for (std::string_view s : std::span{argv, argc})
      {
          std::cout << s << std::endl;
      }
      return 0;
    }
    

    我的示例的唯一开销是让 string_view 找到空终端。我试过 goldbolt.org 但似乎没有一个编译器可以找到 span 头文件。所以请轻视我的建议。

    【讨论】:

      猜你喜欢
      • 2016-09-09
      • 1970-01-01
      • 2020-09-11
      • 1970-01-01
      • 2021-09-29
      • 2017-04-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多