• 数组与指针的区别,以及在STL中传递数组/指针


    数组和指针在作为实参传入T[] 或T*的形参时没有区别

    void f(int pi[]) { cout << sizeof(pi) << endl; }
    
    int a[5] = { 1,2,3,4,5 };
    f(a);
    

    上述代码输出的是4(32位系统)或8(64位系统),总之不是sizeof(int) * 5(数组大小)。

    为什么明明形参是数组形式的int [],实际上和指针形式的int *无异呢?关键原因就在于,数组是不能作为左值的

    函数传参并不是相当于赋值而是相当于初始化。于是,原因在于,数组不能直接用来初始化另一个数组,当然,也不能作为左值。

    数组只能用初始化列表来初始化,字符数组可以例外地用字符串字面值初始化。

    也就是说,你不能定义两个数组,比如int a[10], b[10];然后用a = b; 来给a赋值。

    而函数的实参传给形参是要做1次赋值的,虽然很多书上区分了所谓的传值和传地址,但实际上两者是一样的,传地址不过是在指针类型之间进行赋值,见下述代码

    void f1(int i) { i = 100; }
    void f2(int* pi) { *pi = 100; }
    
    int main()
    {
         int i = 0, j = 0, k = 0;
         f1(i);  // 类似于 int i0 = i; i0 = 100;
         f2(&j); // 类似于 int* p = &j; int* p0 = p; *p0 = 100;
         return 0;
    }
    

    但是,在C++里传递数组也是可行的,见下面代码

    template <int N> void f(int (*pi)[N]) { cout << sizeof(*pi) << endl; }
    
    int main()
    {
         int a[3] = { 1,2,3 };
         f(&a);
         return 0;
    }
    

    输出结果是12,即sizeof(int)*3。

    像这样蹩脚的传递数组指针,当然,更简单的方式是使用引用,把f()的参数*pi改成&pi,然后sizeof(*pi)改成sizeof(pi);main()里的f(&a)可以直接用f(a),两者本质上是一样的,只不过简化了代码。虽然需要强调的是引用和指针(以及java等语言中的引用)也有一些差异,这里不详细讨论。

    这样的做法本质是像下面这样。

    int a[3] = { 1,2,3 };
    
    int (*p)[3] = a;  // 形参传递给实参
    cout << sizeof(p) << endl;  // 函数体内的代码
    

    OK,这里就可以正视数组和指针的区别了,对于数组int a[N];

    a的类型是int [N],不能作为左值(也就是不能直接给a赋值),那么&a取得的则是指向int [N]的指针,表示为int (*)[N],大小为N*sizeof(int)。

    其实指针本质上就是地址,之所以有各种各样的指针类型,是为了让编译器了解,你如果想用*p来取得指针指向的对象,到底该读取多大内存?

    char s[5] = "1234";
    char* pch = &s[0];
    cout << *pch << endl;  // 1
    short* psh = &s[0];
    cout << *psh << endl;  // 12849
    int* pi = (int*)&s[0];
    cout << *pi << endl;     // 875770417
    

    如果把s[3]和s[4]都置为''(对应ASCII码为0),那么*pi和*psh结果一样,都是12849。注意,这里是因为我的机器是小端的,数据的低位保存在内存的低地址中,我让高位变成0了,自然就和没有高位没区别了。

    0x0000abcd  int*   或char (*)[4]

    0x       abcd  short*或char (*)[2]

    0x          cd   char*

    打个比方,内存中布局就类似上面那样(abcd是我随便定的值),3个指针都指向同样的地址,但是类型不同导致编译器了解你在解引用(*p)时,到底想从内存中读取多少,到底是从当前地址开始1位,2位还是4位?

    再来看看在STL里的应用,STL的一个重要概念是迭代器,而指针也是一种迭代器(随机访问迭代器),在STL函数库(<algorithm>)中,往往接收的类型是用2个迭代器表示的数据范围(比如数组int a[3]; 用a和a+3就能表示首尾),而STL提供了传递函数的方法(可以是函数对象、函数指针或lambda表达式),这些函数接收的参数却不是迭代器,而是迭代器解引用后的对象。

    因为我们实际上是要对容器中存储的数据进行操作,而不是去对数据存放的位置进行操作,取得存放位置的目的只是为了取得数据,这些工作在STL函数内部就完成了,调用者只需要告诉STL函数该如何处理数据即可(不需告诉STL函数怎么取得地址)。比如下面给出的for_each示例代码

    template<class InputIterator, class Function>
      Function for_each(InputIterator first, InputIterator last, Function fn)
    {
      while (first!=last) {
        fn (*first);
        ++first;
      }
      return fn;      // or, since C++11: return move(fn);
    }
    

    对于数组int a[N];只需要写std::for_each(a, a + N, [](int& i) { i = rand(); });就可以批量生成随机数了(当然,实现这个功能有个更好的函数是generate)

    不过对于二维数组来说,一切都要更为复杂了,虽然很少用到,但是在处理二维字符数组表示的C风格字符串列表时,还是需要知道这点。

    char str[M][N]; 

    其中M是字符串数量上限,N是字符串长度上限,假如要批量打印str表示的字符串列表,代码如下

    std::for_each(&str[0], &str[M], [](char (&s)[N]) { printf("%s
    ", s); }
    

    还是按照上面的分析方法,str类型是char [M][N],那么str[0]类型是char [N],&str[0]类型是char (*)[N],是指向N个字符的指针类型,也就是说对这样类型的指针p,每次执行++p时会移动N个字节(sizeof(char)),str[0][0](即*str[0])移动N个字节就到了str[0][N],也就是str[M][0](即*str[1])。

    那么迭代器的类型是char (*)[N],那么解引用的类型就是char (&)[N],即引用一个长度为N的字符数组。

    再回顾之前说的,数组在作为实参传递给形参T[]时,实际上传递的是数组的首地址,也就是说,printf接收的实际上是&s[0],即char*类型,因此可以用%s输出。

    然而实际上还有个陷阱,比如我要对这些字符串进行排序。

    std::sort(&str[0], &str[M], 
         [](char (&s1)[N], char (&s2)[N]) { return strcmp(s1, s2) < 0; }
    

    比如这里传入的迭代器是指针类型char (*)[N],而自定义排序函数接收的也是解引用的类型char (&)[N],编译时却报了几十行错误。我昨天在测试ls -l的排序方法时在这里纠结了很久。

    原因在于std::sort的内部实现,对于这种比较排序,内部实现经常有交换操作。

    比如模板参数Iter表示迭代器类型,在函数内部可能就会有这样的代码

    typedef std::iterator_traits<Iter>::value_type T; // 迭代器指向对象的类型
    Iter it1, it2;
    if (*it1 > *it2)
    {     // 交换迭代器指向的对象
          T temp = *it1;
          *it1 = *it2;
          *it2 = temp;
    }
    

    这里的Iter是char (*)[N]的话,类型T就是char [N],也就是数组类型。

    问题来了,再回顾我这篇博客全篇唯一加红加粗的文字——数组是不能作为左值的。

    那么,如果这里的类型T是char*呢?当然就是可以交换了,只不过原数组还是没变。

    解决方式即定义一个长度为M的数组,数组元素类型是char (*)[N],然后再排序

  • 相关阅读:
    Linux基础之什么是Linux
    JavaWeb之AJAX
    JavaWeb之XML
    JavaWeb之Servlet组件
    JavaWeb之JSP
    JavaWeb之HTTP概述
    JavaWeb之开发环境搭建
    JavaWeb之JQuery
    JavaWeb之javaScript
    ddd领域驱动
  • 原文地址:https://www.cnblogs.com/Harley-Quinn/p/6596021.html
Copyright © 2020-2023  润新知