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

Is a char array more efficient than a char pointer in C?

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

char* str1;
char str2[10];

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

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

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

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

str1 可用于指向任何 char * 数据类型。例如,字符串文字或以 [=17=].

结尾的 char 数组

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

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

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

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

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

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

I understand that using char* gives a pointer to the first character in str1

不,它给出了一个指向分配在别处的单个字符的指针。尽管在您为其分配地址之前它不会指向任何有意义的地方。对于数组,它通常会设置为指向数组的第一个字符。

I recall that in some encodings it can be more

不,根据定义,一个字符总是 1 个字节。一些异国情调的系统可能每个字节有 16 位等等。这无关紧要,除非您对异国情调的 DSP 等进行编程。至于其他字符编码,wchar_t完全是另一个话题。

whereas in the case of char* we're really just saying allocate a pointer to a single byte

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

But what happens if the string we assign to str1 is more than a single byte, how is that allocated?

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

How much more work is needed to appropriately allocate that?

这取决于你要分配什么。

Because of the uncertainty from the compiler's point of view when dealing with char pointers

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

is it more efficient to use a char array when I either know the length ahead of time or want to limit the length to start with?

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

好的,让我们逐条分析你的问题。

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

I understand that using char* gives a pointer to the first character in str1

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

while char[10] results in an array with a length of 10 bytes (because char is a single byte... I recall that in some encodings it can be more but let's just assume one byte to keep it simple).

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

My question is when actually assigning values to them the data obviously has to be stored somewhere and in the case of char[10] we're telling the compiler upfront to allocate ten bytes (...)

大体上是正确的。

But what happens if the string we assign to str1 is more than a single byte, how is that allocated? How much more work is needed to appropriately allocate that? Plus, what happens if we want to reassign str1 to be something longer than what was previously assigned, how is that allocated?

这真的要视情况而定。给定以下情况:

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

什么都没有真正分配。只是 ptr 的内容发生了变化,就像整数一样。 请注意,在这种情况下,它可以是 dereferenced/passed 作为 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;

现在,指针还可以post指向动态分配的内存。 但它不必。

所以下面的代码

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

c2 = c1;
c2 = &single;

完全有效。

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

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

此外,请记住

*(p+2) = //whatever else

相当于:

p[2] = //whatever else

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

But what happens if the string we assign to str1 is more than a single byte, how is that allocated?

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]
                        +---+

现在我们可以使用 str:

buffer 中存储一个字符串
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或一元&运算符的操作数,或者它是用于在声明中初始化字符数组的字符串文字,表达式 类型 "N-element array of `T`" 将被转换或 "decay" 为类型 "pointer to `T`" 的表达式,表达式的值将是数组第一个元素的地址。

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

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

然后这些 class 的 puts

puts( str2 );
puts( str1 );

将以与编写

相同的方式等效
for ( size_t i = 0; str2[i] != '[=11=]'; i++ )
{
    putchar( str2[i] );
}

for ( size_t i = 0; str1[i] != '[=12=]'; 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;
}

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

但是这个函数

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 );

等价,两者都将 return 值 5 即字符串的长度 "Hello".

已经有足够的答案来解释杂项。

当 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);

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

Is a char array more efficient than a char pointer in C?

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

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

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

就原始分配而言,分配数组总是比调用 malloc 动态分配一些内存指向。 (但是,大多数时候,由于固定的编译时限制令人无法忍受的麻烦,无论如何都会有对动态内存分配的压倒性偏好。)

就复制而言,任何调用之间通常不会有任何区别

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

对于数组 a 和指针 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 代码。

最后,就像问“哪个效率更高?”时的情况一样,找到实际答案的唯一方法是针对您的问题陈述并使用您的编译器和处理器对其进行编码,然后执行仔细测量。 所以 许多因素会影响效率问题,通常不可能做出准确的预测。


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