【问题标题】:Is a char array more efficient than a char pointer in C?char 数组是否比 C 中的 char 指针更有效?
【发布时间】:2021-11-21 09:05:01
【问题描述】:

我试图了解这两个 char 声明之间的内在差异:

char* str1;
char str2[10];

我知道使用char* 会给出一个指向str1 中第一个字符的指针,而char[10] 会产生一个长度为10 个字节的数组(因为char 是一个单字节......我记得在某些编码中它可以更多,但我们只假设一个字节以保持简单)。

我的问题是,当实际为它们赋值时,数据显然必须存储在某个地方,在char[10] 的情况下,我们预先告诉编译器分配十个字节,而在char* 的情况下,我们'真的只是说分配一个指向单个字节的指针。但是如果我们分配给str1 的字符串不止一个字节会发生什么,那是如何分配的呢?还需要多少工作才能适当地分配它?另外,如果我们想将 str1 重新分配为比之前分配的更长的东西会发生什么,它是如何分配的?

由于在处理char pointers 时从编译器的角度来看存在不确定性,当我提前知道长度或想要限制开始长度时,使用char array 是否更有效?

【问题讨论】:

  • 如果您的意思是“char *s = "Hello"; 中发生了什么分配”,则没有。字符串文字"Hello" 嵌入到可执行文件中,您只需让s 指向它。一般来说,如果你想看看这些东西是如何工作的,你应该看看生成的程序集。
  • 你从哪里得到这个语法char[10] str2;
  • 看看comp.lang.c FAQ的第6节。
  • 可以使用 malloc 进行大分配以避免堆栈溢出。可以在堆栈上进行少量分配以获得更好的缓存性能。
  • @hacks,我猜来自 C#?

标签: arrays c pointers dynamic-memory-allocation


【解决方案1】:

在讨论一般性能时,分配、访问时间和复制时间是分开的。您似乎最关心分配。

但是这里有很多误解。数组用于存储。指针用于指向存储在别处的东西。您不能将任何数据存储在指针中,您只能将地址存储到其他地方分配的数据。

所以比较指针或数组几乎是无稽之谈,因为它们是不同的东西。类似于“我应该住在街道地址的房子里,还是应该住在写有街道地址的标志上”。

我知道使用 char* 会给出指向 str1 中第一个字符的指针

不,它给出了一个指向分配在其他地方的单个字符的指针。尽管在您为其分配地址之前,它并没有指向任何有意义的地方。如果是数组,它通常会被设置为指向数组的第一个字符。

我记得在某些编码中它可以更多

不,每个定义的字符总是 1 个字节。一些奇特的系统可能每字节有 16 位或类似的。除非您对奇异的 DSP 等进行编程,否则这无关紧要。至于其他字符编码,还有wchar_t,这完全是一个不同的话题。

而在 char* 的情况下,我们实际上只是说分配一个指向单个字节的指针

不,我们告诉它为指针本身分配空间。其大小通常在 2 到 8 个字节之间,具体取决于特定系统的地址总线宽度。

但是如果我们分配给 str1 的字符串不止一个字节会发生什么,它是如何分配的呢?

随你喜欢。您可以将其分配给只读字符串文字、静态存储持续时间变量、本地自动存储变量或动态分配的变量。指针本身不知道也不关心。

还需要多少工作才能适当分配?

这取决于你要分配什么。

因为在处理 char 指针时从编译器的角度来看是不确定的

那是什么不确定性?指针就是指针,编译器对它们的处理与其他变量没有太大区别。

当我提前知道长度或想要限制开始的长度时,使用 char 数组是否更有效?

您需要使用数组,因为数据不能凭空存储。同样,数据不能存储在“指针”中。

【讨论】:

  • 好吧,我认为这个答案帮助我理解了我在哪里感到困惑以及为什么我的问题可能没有最有意义。谢谢
【解决方案2】:

但是如果我们分配给 str1 的字符串不止一个字节会发生什么,它是如何分配的呢?

str1最终必须指向另一个char数组——是否自动分配,比如

char buffer[10];
char *str1 = buffer; // equivalent to &buffer[0]

或动态:

char *str1 = malloc( sizeof *str1 * 10 );

或通过其他方法。所有str1 存储都是内存中某处char 对象的地址。您实际上并没有将任何内容保存到str1,而是将其保存到str1 指向的任何内容。假设以下声明:

char *str;
char buffer[10];

我们在内存中有这样的东西:

      char *            char
      +---+             +---+
 str: | ? |     buffer: | ? | buffer[0]
      +---+             +---+
                        | ? | buffer[1]
                        +---+
                         ...
                        +---+
                        | ? | buffer[9]
                        +---+

首先,我们将buffer的第一个元素的地址赋值给str1

str = buffer;

现在我们的图片是这样的:

      char *            char
      +---+             +---+
 str: |   | --> buffer: | ? | buffer[0]
      +---+             +---+
                        | ? | buffer[1]
                        +---+
                         ...
                        +---+
                        | ? | buffer[9]
                        +---+

现在我们可以使用strbuffer中存储一个字符串:

strcpy( str, "foo" );

给我们

      char *            char
      +---+             +---+
 str: |   | --> buffer: |'f'| buffer[0]
      +---+             +---+
                        |'o'| buffer[1]
                        +---+
                        |'o'| buffer[2] 
                        +---+
                        | 0 | buffer[3]
                        +---+
                         ...
                        +---+
                        | ? | buffer[9]
                        +---+

“那么,”你问自己,“我们为什么要为指针而烦恼?为什么不直接将字符串存储到buffer?这样不是更有效率吗?”

是的,通常我们会直接存储到buffer 并避免指针的开销如果这是一个选项。然而,有时这不是一种选择。我们在以下情况下通过指针工作:

  • 数组是动态分配的——在这种情况下,我们别无选择,只能通过指针:
    char *str = malloc( sizeof *str * 10 );
    strcpy( str, "foo" );
    
  • 数组作为参数传递给函数 - 由于衰减规则,当您将数组作为函数参数传递时,函数实际接收的是指向第一个元素的指针(所有数组类型都是如此,而不仅仅是字符数组):
    void foo( char *str, size_t max_size )
    {
      strncpy( str, "this is a test", max_size );
      str[max_size-1] = 0;
    }
    
  • 我们正在使用指针来遍历 char []char * 的数组:
    char table[][10] = { "foo", "bar", "bletch", "blurga", "" };
    ...
    char *p = table[0];
    while ( strlen( p ) )
      printf( "%s\n", p++ );
    ...
    
    当然,我们可以只使用数组表示法,根本不用指针:
    size_t i = 0;
    while ( strlen( table[i] ) )
      printf( "%s\n", table[i++] );
    
    有时使用数组表示法更有意义,有时使用指针更有意义 - 取决于手头的问题。

  1. 除非它是sizeof 或一元& 运算符的操作数,或者它是用于在声明中初始化字符数组的字符串文字,表达式 类型为“`T` 的 N 元素数组”将被转换或“衰减”为“指向 `T` 的指针”类型的表达式,表达式的值将是第一个元素的地址大批。

【讨论】:

  • 好。清晰而详细的解释。
【解决方案3】:

char* str1;str1 声明为指向char 数据类型的指针。它不会为一个字节分配内存。但是编译器为这个变量分配了sizeof(char*)字节。

str1 可用于指向任何char * 数据类型。例如,以\0 结尾的字符串文字或char 数组。

不知道你说的char数组比char指针效率高吗是什么意思。两种数据类型都不同,并且有不同的用例。 问这个问题听起来像是在问int 类型是否比double 类型更有效?这没有任何意义。

另一方面,char[10] str2; 不是有效的 C 语法。我猜你的意思是char str2[10];,这将str2 声明为10 个char 的数组。该变量可以存储10种char数据类型。

str1str2 是两种不同的数据类型。

【讨论】:

    【解决方案4】:

    好的,让我们逐个回答你的问题。

    char* str1; //this is a pointer
    char str2[10]; //that is an array of 10 characters
    char[10] str2; //that is compilation error
                   //possibly your mistook C for Java
    

    我知道使用 char* 会给出指向 str1 中第一个字符的指针

    char* str1 是一个指向字符的指针。它可以指向连续的内存位置,例如C 风格的字符串,但不是必须的。它也可能指向单个字符。

    while char[10] 生成一个长度为 10 字节的数组(因为 char 是一个单字节...我记得在某些编码中它可以是 更多,但为了简单起见,我们只假设一个字节)。

    是的,没错。根据定义的位置,数组可以位于堆栈上,也可以位于数据段中(如果它是全局的);不过,这是一个实现细节。

    我的问题是,当实际为它们赋值时,数据显然必须存储在某个地方,在 char[10] 的情况下,我们预先告诉编译器分配十个字节 (...)

    这通常是正确的。

    但是如果我们分配给 str1 的字符串不止一个会发生什么 字节,如何分配?还需要做多少工作 适当分配?另外,如果我们想重新分配会发生什么 str1 比以前分配的要长,如何 那分配?

    这真的取决于具体情况。鉴于以下情况:

    char s1[] = "foo";
    char s2[] = "bar";
    char* ptr;
    ptr = &s1[1]; //points to first o
    ptr = &s2[2]; //points to r
    

    没有真正分配。简单地改变 ptr 的内容,就像整数一样。 请注意,在这种情况下,它可以作为 C 样式的字符串取消引用/传递。

    但是,在下面的一个中,它不能:

    char c1 = 'a';
    char c2 = 'b';
    char* ptr;
    ptr = &c1; //points to a
    ptr = &c2; //points to b
    

    现在,如果是立即字符串:

    const char* s = "foo"; //should be const char* actually
    

    字符串最有可能作为全局常量存储在二进制文件中,并且 s 指向它的开始。 它的心智模型可能类似于:

    //globals
    const char someCompilerGeneratedName[] = "foo";
    
    //then the pointer:
    const char* s = &someCompilerGeneratedName[0];
    
    //Note that arrays decay to pointers, 
    //i.e. array name denotes address of its 1st element
    //the one below is equivalent:
    const char* s = someCompilerGeneratedName;
    
    

    现在,指针也可以发布到动态分配的内存。 但这不是必须的。

    所以下面的代码

    char single = 'c';
    char* c1 = malloc(10*sizeof(char));
    char* c2;
    
    c2 = c1;
    c2 = &single;
    

    完全有效。

    从性能的角度来看: 先测量。这里没有简单的答案。

    现在,如果您要问的是堆分配与堆栈分配,那就是另一回事了。但我想说:先测量。堆分配通常被认为较慢(通常如此),但通常它们的开销无论如何都可以忽略不计。

    另外,请记住

    *(p+2) = //whatever else
    

    相当于:

    p[2] = //whatever else
    

    所以有时可能只是可读性的问题。

    【讨论】:

    • “是的,这是正确的。取决于它的定义位置...”,(在第二个引用部分下)你的意思是说char[10] 是正确的? (上面你说是编译器错误。)
    • @ryyker 我假设 char[10] 是代码 sn-p 之外的类型签名。
    【解决方案5】:

    表达式中使用的数组被隐式转换(极少数例外)为指向其第一个元素的指针。例如,如果你写

    char[10] str2 = "你好"; char* str1 = str2;

    那么这些类的puts

    puts( str2 );
    puts( str1 );
    

    效率与写作方式相同

    for ( size_t i = 0; str2[i] != '\0'; i++ )
    {
        putchar( str2[i] );
    }
    

    for ( size_t i = 0; str1[i] != '\0'; i++ )
    {
        putchar( str1[i] );
    }
    

    这些声明可能会有所不同

    char[10] str2 = "Hello";
    char* str1 = "Hello";
    

    在第一种情况下,数组 str2 由字符串字面量初始化,您可以更改存储的字符串,例如

    str2[0] = 'h';
    

    在第二种情况下,指针str1 指向一个具有静态存储持续时间且不可更改的字符串文字。所以如果你会写

    str1[0] = 'h';
    

    那么这个语句将调用未定义的行为。

    另一方面,如果你会写下面的函数

    char * f( void )
    {
        char str2[10] = "Hello";
        return str2;
    }
    

    那么返回的指针将无效,因为声明的数组在退出函数后将不存在。

    但是这个函数

    char * f( void )
    {
        char* str1 = "Hello";
        return str1;
    }
    

    将是正确的,因为具有静态存储持续时间的字符串文字在退出函数后将处于活动状态。

    如果你要声明的话

    char* str1 = "Hello";
    

    那么表达式 sizeof( str1 ) 将产生等于 4 或 9 的指针大小,具体取决于所使用的系统。

    但如果你会写

    char str2[10] = "Hello";
    

    那么表达式 sizeof( str2 ) 将产生等于 10 的数组大小。

    然而这个函数调用

    strlen( str1 );
    

    strlen( str2 );
    

    同样有效,两者都会返回值5,即字符串"Hello"的长度。

    【讨论】:

      【解决方案6】:

      有足够的答案解释杂项方面。

      当 C++ 首次与 STL 库一起出现时,string,我们确实遇到了速度和内存问题:字符串太多。

      所以我自己实现了string

      char* ptr_to_actual_content;
      char small_content[16];
      
      ptr_to_actual_content = size < sizeof(small_content) ? small_content : malloc(size);
      

      因为现在对于小字符串没有发生额外的分配,性能和速度的提升是令人难以置信的巨大。 (顺便说一句,没有内存泄漏。)

      【讨论】:

        【解决方案7】:

        char 数组是否比 C 中的 char 指针更高效?

        这真是一个不可能回答的问题。数组或指针没有内在的效率。在特定编译器下,对于特定处理器架构,对于特定操作,一个或另一个可能具有更好或更差的效率。但没有绝对的保证。

        同样重要的是,对于手头的问题,其中一个或另一个可能具有更好或更差的功能。或者对你来说更好或更坏的便利,程序员。这些因素也非常重要,可能比原始效率更重要。

        我对您的建议是,您真正了解了在 C 中使用数组和指针的不同方式,并且——最重要的是——真正理解 "correspondence between arrays and pointers" 的概念。我认为,只有在获得这种理解之后,您才能做出任何有意义的区分,哪些区分对于特定问题可能“更有效”。还要意识到,实际的效率差异(如果有的话)可能非常微小。

        就原始分配而言,分配数组(这主要发生在编译时,几乎没有运行时开销 [脚注])总是比调用 malloc 动态分配一些内存要快指向。 (但是,在很多时候,无论如何都会对动态内存分配产生压倒性的偏好,因为固定的编译时间限制令人无法忍受。)

        在复制方面,任何调用之间通常不会有任何区别

        memcpy(a, p, n);
        memcpy(p, a, n);
        memcpy(a1, a2, n);
        memcpy(p1, p2, n);
        

        用于数组a 和指针p。另一方面,加速内存复制的最佳方法是根本不复制内存,因此从某种意义上说,指针可以大大更有效率,因为它们可以让你做类似的事情 p>

        p = a;
        

        p1 = p2;
        

        而是(显然您不能对数组执行此操作)。

        最后,还有原始访问的问题。在机器语言级别上,需要执行的指令之间会有很大差异

        x = a[i];
        

        y = p[i];
        

        但我不能告诉你哪个会更快,因为它往往因处理器和编译器而异。

        人们曾经担心使用“数组样式”迭代整个数组是否更快:

        for(i = 0; i < n; i++)
            sum += a[i];
        

        或“指针样式”:

        for(p = a; p < &a[n], p++)
            sum += *p;
        

        曾几何时,其中一种可能会显着提高效率——尽管同样,答案取决于处理器架构,并且往往会随着时间而变化。今天,我相信优化编译器已经足够聪明,可以选择最有效的机器代码来发出,不管你用哪种方式编写 C 代码。

        最后,当询问“哪个更有效?”时,通常情况下,找到实际答案的唯一方法是编写代码,用于您的问题陈述并使用您的编译器和处理器,并执行仔细的测量. 影响效率问题的因素很多,通常无法做出准确的预测。


        脚注:我说过数组分配“主要发生在编译时,几乎没有运行时开销”。有一个例外,它涉及函数本地的大型数组。如果操作系统必须在最后一分钟分配更多的堆栈空间,那么这些可能需要大量时间来“分配”,如果它们太大,它们也可能会失败。如此大的本地数组通常不被推荐。

        【讨论】:

          猜你喜欢
          • 2021-01-26
          • 2019-09-13
          • 2012-03-19
          • 1970-01-01
          • 2021-12-14
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多