为什么<const char*>和<const char[]>的内存或指针行为非常不同? 为什么<const char*>和<const char[]>的内存或指针行为非常不同?

3

我在尝试使用C++编程时,发现使用 const char*const char[] 会导致代码表现出不同的行为。如果我的问题表述不太清晰,请谅解,因为我对代码中发生的事情并不十分明确。

#include <iostream>                                                                                                        
#include <vector>                                                                                                          

// This version uses <const char[3]> for <myStr>.
// It does not work as expected.                                                                                                                      
struct StrStruct                                                                                                           
{                                                                                                                  
    const char myStr[3];                                                                                                     
};                                                                                                                         
       
// This program extracts all the string elements in <strStructList> and copy them to <strListCopy>                                                                      
int main()
{
    StrStruct strStruct1{"ab"};
    StrStruct strStruct2{"de"};
    StrStruct strStruct3{"ga"};
                                                                                                                           
    std::vector<StrStruct> strStructList{strStruct1, strStruct2, strStruct3};
    std::vector<const char*>  strListCopy{};
                                                                                                                           
    for (StrStruct strStructEle : strStructList)                                                                           
    {                                                                                                                      
        strListCopy.push_back(strStructEle.myStr);                                                                         
                                                                                                                           
        std::cout << "Memory address for the string got pushed back in is "                                                
                  << &strStructEle.myStr << std::endl;                                                                     
        std::cout << "Memory address for the first element of the string got pushed back in is "                           
                  << (void *) &strStructEle.myStr[0] << "\n" <<std::endl;                                                          
    }                                                                                                                      
    
    std::cout << "Show content of <strListCopy>:" << std::endl;                                                                                                                     
    for (const char*& strEle : strListCopy)                                                                                
    {                                                                                                                      
        std::cout << strEle << std::endl;                                                                                  
    }                                                                                                                                                                                            
}

以下是它的输出:
Memory address for the string got pushed back in is [address#99]
Memory address for the first element of the string got pushed back in is [address#99]

Memory address for the string got pushed back in is [address#99]
Memory address for the first element of the string got pushed back in is [address#99]

Memory address for the string got pushed back in is [address#99]
Memory address for the first element of the string got pushed back in is [address#99]

Show content of <strListCopy>:
ga
ga
ga

然而,如果我只是简单地改变 StrStruct 的实现方式:
从:
// This version uses <const char[3]> for <myStr>.
// It does not work as expected. 
struct StrStruct                                                                                                           
{                                                                                                                  
    const char myStr[3];                                                                                                     
};

to

// This version uses <const char*> for <myStr>.
// It works as expected.                                                                                                                      
struct StrStruct                                                                                                           
{                                                                                                                  
    const char* myStr;                                                                                                     
};

程序的输出如下:

Memory address for the string got pushed back in is [address#10]
Memory address for the first element of the string got pushed back in is [address#1]

Memory address for the string got pushed back in is [address#10]
Memory address for the first element of the string got pushed back in is [address#2]

Memory address for the string got pushed back in is [address#10]
Memory address for the first element of the string got pushed back in is [address#3]

Show content of <strListCopy>:
ab
de
ga

我困惑的是以下问题:

  1. 为什么在第一个版本中所有字符串都具有相同的值?我尝试在for each循环中使用const strStruct&代替strStruct,这解决了问题,但我不明白原因。

  2. 为什么const char*const char[]表现如此不同?我认为它们在很大程度上相似,因为:

const char myChars[] = "abcde";                                                                                     
const char* myCharsCopy = myChars;                                                                                  
                                                                                                                        
std::cout << myChars << " vs "  << myCharsCopy << std::endl;  

它打印出abcde vs abcde并且你可以直接将const char[]的值分配给const char*,而不会出现任何错误。

  1. 为什么将const char[]改为const char*可以解决这个问题?

5
for (StrStruct strStructEle : strStructList)会创建结构体的临时副本。您正在存储对这些副本的指针,并且似乎由于它们在循环结束时超出了作用域,程序一直在重复使用相同的位置,因此所有指针都指向同一位置。使用for (StrStruct & strStructEle : strStructList)可以得到原始向量中的引用,而不是它们的副本。 - user4581301
2
当你复制 struct { char x[8]; } 时,你复制了字符串;当你复制 struct { char *x; } 时,你复制了字符串的地址。 - Yakov Galka
2个回答

1
需要理解其余内容的基础知识:

数组和衰变

struct StrStruct
{
    const char myStr[3];
};

包含数据
struct StrStruct
{
    const char * myStr;
};

指向数据的点。 数组会衰变为指针,但它们本身不是指针。
const char myChars[] = "abcde";

创建一个恰好大小为6个字符(5个字母和一个空字符)的数组来存储“abcde”,并将字符串复制到该数组中。请注意,这不一定是const类型的。
const char* myCharsCopy = myChars;

定义了一个指向char的指针,并将其赋值为数组myChars。在此过程中,myChars自动衰减为指针。 myCharsCopy不是myChars的副本;它仅仅保存myChars的地址。请注意,只要myCharsconstmyCharsCopy就必须是const。还要注意,您不能对数组进行赋值,并且除非将其放入另一个数据结构中,否则几乎不可能复制数组。通常您可以做的最好的事情是将数组中的内容复制到另一个数组中(根据目标和数组是否为以空字符结尾的字符数组,使用memcpystrcpy)。
请注意,在许多用法中,例如函数参数,const char[]const char*意味着相同的事情。
void func(const char a[], // accepts constant pointer to char
          const char * b) // also accepts constant pointer to char

这些东西之所以变得非常奇怪,原因在于它们(大多数)在 1970 年代是有很好的意义的。今天我强烈建议您使用库容器,例如 std::vectorstd::array,而不是原始数组。

基于范围的 for 循环

基于范围的 for 循环对列表中的项进行副本操作,除非您另行指定。在循环体内

for (StrStruct strStructEle : strStructList)

在循环的第一次迭代中,strStructEle 不是 strStruct1,甚至不是存储在 strStructList 中的 strStructEle 的副本,它是一个全新的、完全相同的对象。该副本会在循环体结束时被销毁,释放所占用的存储空间。
for (StrStruct & strStructEle : strStructList)

循环将操作对strStructList中项目的引用,因此不会进行复制。

现在您已经了解情况...

要点1

为什么第一个版本中所有字符串都具有相同的值?我尝试在for each循环中使用const strStruct&而不是strStruct,这解决了问题,但我不明白原理。

由于

struct StrStruct
{
    const char myStr[3];
};

包含数据,当您复制一个StrStruct时。代码在此处复制数据结构。
std::vector<StrStruct> strStructList{strStruct1, strStruct2, strStruct3};

而且,更重要的是对于输出来说,在这里。
for (StrStruct strStructEle : strStructList) // strStructEle copied, so data in it is copied
{
    strListCopy.push_back(strStructEle.myStr); //strStructEle.myStr decays to pointer, 
                                               // and pointer is stored in strListCopy
                                               // this is not really a copy it's a pointer 
                                               // to data stored elsewhere
    std::cout << "Memory address for the string got pushed back in is "
              << &strStructEle.myStr << std::endl; // print address of array
    std::cout << "Memory address for the first element of the string got pushed back in is "
              << (void *) &strStructEle.myStr[0] << "\n" <<std::endl;
                  // prints address of the first item in the array, the same as the array
} // strStructEle is destroyed here, so the stored pointer is now invalid. 
  // Technically anything can happen at this point

但在这种情况下,可能发生的任何事情似乎是存储在循环的下一次迭代中被重用的strStructEle。这就是为什么所有存储的指针看起来都相同的原因。它们确实是相同的。它们是不同的对象,它们在不同的时间点都驻留在相同的位置。所有这些对象都已过期,因此即使是查看它们也是不明智的const strStruct& "修复"了这个问题,因为没有进行复制。每次迭代都在不同的位置上操作不同的对象,而不是在相同位置上操作不同的对象。
第3点
为什么将const char[]更改为const char*可以解决问题?
如果myStr是指针而不是数组,那么情况就不同了。
for (StrStruct strStructEle : strStructList) // strStructEle copied, so data in it is copied
                                             // BUT! The data in it is a pointer to data 
                                             // stored elsewhere that is NOT copied
{
    strListCopy.push_back(strStructEle.myStr); //strStructEle.myStr is a pointer and is 
                                               // directly stored in strListCopy
                                               // this is still not a copy 
    std::cout << "Memory address for the string got pushed back in is "
              << &strStructEle.myStr << std::endl; // print address of pointer, not what 
                                                   // it points at
    std::cout << "Memory address for the first element of the string got pushed back in is "
              << (void *) &strStructEle.myStr[0] << "\n" <<std::endl;
                  // prints address of the first item pointed to by the pointer, 
                  // and will be a totally different address
}

点2

为什么const char *和const char[]的行为如此不同?我认为它们由于以下原因在很大程度上是相同的...

这是上面解释的数组衰减的结果。

另外:

vector在直接包含(和拥有)它们收集的数据时表现最佳。它们处理所有内存管理,并将数据保持在一个漂亮、易于缓存的块中。


0

基于评论区的Yakov Galkauser4581301

在回答问题之前,有几个需要澄清的事情:

const char*const char[]的区别:

从概念上来说

const char*是指向const char的指针。

const char[]是字符数组本身。

从代码角度来看

const char*存储一个内存地址,而且它自己的内存地址与它所存储的不同。

const char[]存储数组中第一个元素的内存地址,而且它自己的内存地址与它所存储的相同。

const char myCharsArray[] = "abcde";      // Writing like this guarrentees you have an null terminator at the end   
const char* myCharsPointer = "qwert\0";                                                                                
                                                                                                                           
std::cout << "The memory address for <myCharsArray> is "                                                               
              << &myCharsArray                                                                                          
              << std::endl;;                                                                                            
                                                                                                                        
std::cout << "The memory address for the first element in <myCharArray> is "                                        
              << (void *) &myCharsArray[0]                                                                              
              << std::endl;                                                                                             
                                                                                                                           
                                                                                                                        
std::cout << "The memory address for <myCharsPointer> is "                                                          
              << &myCharsPointer 
              << std::endl;                                                                          
                                                                                                                        
std::cout << "The memory address for the first element in <myCharsPointer> is "                                     
              << (void *) &myCharsPointer[0]                                                                            
              << std::endl;

它的输出是这样的:

The memory address for <myCharsArray> is [address#10]
The memory address for the first element in <myCharArray> is [address#10]
The memory address for <myCharsPointer> is [address#88]
The memory address for the first element in <myCharsPointer> is [address#99]

回答这三个问题:
问题1:
在第一个版本中,`std::vector::push_back` 一直添加复制的字符数组中第一个元素的地址,该地址也是 `strStructEle.myStr` 本身的地址,而该地址从未更改。最终,列表是一堆内存地址,其值完全相同。
通过使用 `const strStruct&`,使用对原始内容的引用。因此,它们独特且真实的内存地址被复制到列表中。
问题2:
如上所述的差异。
问题3:
它允许传递原始字符数组的原始内存地址,而不是复制原始字符数组的内容,然后是临时对象的内存地址。

1
并不是 push_back 复制了数组,而是 for (StrStruct strStructEle : strStructList) 也复制了数组,并且在作用域结束时释放了数组使用的空间,以便在下一次循环迭代中重用。 - user4581301
2
注意:为了让事情更加混乱,在函数参数中,“const char []”实际上意味着“const char *”; 你不能通过值传递数组。在OP的情况下,将其嵌入结构中,它实际上是一个数组,但当在某些上下文中声明为数组的东西实际上不是数组时,这种混淆是可以理解的。 - ShadowRanger
@user4581301 我只是想知道人们从哪里了解这些东西。我正在尝试自学C++,这是一段非常有趣的旅程。你有什么推荐的资源吗? - Jaxon
1
这是一个由 Stack Overflow 用户普遍认为高质量的书籍精选列表。我不是 C++ Primer 的忠实粉丝,因为第五版中的一些信息已经过时超过10年了。不幸的是,第六版似乎被 COVID 或 2020 年 C++ 语言标准更新的变化所耽搁。一旦你掌握了语言基础知识,就可以阅读《Effective Modern C++》。之后,尽情阅读你认为有趣和有用的内容。 - user4581301

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接