在C/C++中,char* arrayName[][]是指向指针的指针还是指向指针的指针的指针?

5

我原本认为多维数组是指向指针的指针,但也许我错了?

例如,我认为:

char * var = char var[]

char ** var = char* var[] 或者 char var[][]

char *** var = char var[][][] 或者 char* var[][] 或者 char** var[]

这样理解是错误的吗?我感到困惑是因为在一本简单的教材例子中,看到了一个 char*[][] 被强制转换为 char**。

我把例子贴在下面,有人能为我解释清楚吗?谢谢!


/* A simple dictionary. */
#include <stdio.h>
#include <string.h>
#include <ctype.h>

/* list of words and meanings */

char  *dic[][40] = {
    "atlas", "A volume of maps.",
    "car", "A motorized vehicle.",
    "telephone", "A communication device.",
    "airplane", "A flying machine.",
    "", ""  /* null terminate the list */
};

int main(void)
{
    char word[80], ch;
    char **p;

do {
    puts("\nEnter word: ");
    scanf("%s", word);
    p = (char **)dic;
    /* find matching word and print its meaning */
    do {
        if(!strcmp(*p, word)) {
            puts("Meaning:");
            puts(*(p+1));
            break;
            }

        if(!strcmp(*p, word)) break;

        p = p + 2;  /* advance through the list */
        } while(*p);

    if(!*p) puts("Word not in dictionary.");
    printf("Another? (y/n): ");
    scanf(" %c%*c", &ch);

    } while(toupper(ch) != 'N');

return 0;

}

1
请注意,在这种特定情况下,使用C++中的std::map比您在此处使用的更好。 - Billy ONeal
以上代码片段来自Herbert Schildt的《C语言完全参考手册》第212页,但我不理解这行代码的用途:*if(!strcmp(p, word)) break; 我认为这个语句是多余的。因为如果单词与*p中的当前元素匹配,那么它应该已经导致内部的do while循环被打破了。请就此问题给我指点迷津。 - Abhishek Ghosh
5个回答

11

C的规则如下:

6.3.2.1 左值、数组和函数设计器
...
3 除非它是 sizeof 运算符或一元 & 运算符的操作数,或者是用于初始化数组的字符串常量,否则具有类型“类型的数组”的表达式将被转换为具有类型“指向类型的指针”的表达式,该指针指向数组对象的初始元素而不是左值。如果数组对象具有寄存器存储类别,则行为未定义。

C++的语言略有不同:

4.2 数组到指针的转换 [conv.array]

1 具有类型“N个 T 的数组”或“未知边界的 T 的数组”的左值或右值可以转换为类型“指向 T 的指针”的右值。结果是指向数组的第一个元素的指针。
...
8.3.4 数组 [dcl.array]
...
7 对于多维数组遵循一致的规则。 如果 E 是一个 n 维数组,其秩为 i × j × ... × k,则在表达式中出现的 E 将被转换为一个(n−1)维数组的指针,其秩为 j × ... × k。 如果对此指针应用了*运算符,无论是显式还是隐式地作为下标,结果都是指向的(n−1)维数组,它本身立即被转换为指针。

所以以下所有内容都是正确的:

Declaration        Expression        Type             Decays to
-----------        ----------        ----             ---------
     T a[N]                 a        T [N]            T *
                           &a        T (*)[N]     
                           *a        T
                         a[i]        T

  T a[M][N]                 a        T [M][N]         T (*)[N]
                           &a        T (*)[M][N]  
                           *a        T [N]            T *
                         a[i]        T [N]            T *
                        &a[i]        T (*)[N]      
                        *a[i]        T
                      a[i][j]        T

T a[M][N][O]                a        T [M][N][O]      T (*)[M][N]
                           &a        T (*)[M][N][O]
                           *a        T [M][N]         T (*)[N]
                         a[i]        T [M][N]         T (*)[N]
                        &a[i]        T (*)[M][N]  
                        *a[i]        T [N]            T *
                      a[i][j]        T [N]            T *
                     &a[i][j]        T (*)[N]
                     *a[i][j]        T
                   a[i][j][k]        T

对于高维数组,模式应该是清晰的。

那么让我们来分析你的字典:

/* list of words and meanings */         

char  *dic[][40] = {         
    "atlas", "A volume of maps.",         
    "car", "A motorized vehicle.",         
    "telephone", "A communication device.",         
    "airplane", "A flying machine.",         
    "", ""  /* null terminate the list */         
};

这样做不会按照您的意愿设置字典;您基本上将其设置为一个包含40个指向字符的指针的1元素数组。如果您想要一组字符串对的数组,则声明应如下所示:

char *dic[][2] = 
{
  {"atlas", "A volume of maps"},
  {"car", "A motorized vehicle"},
  {"telephone", "A communication device"},
  {"airplane" , "A flying machine"},
  {NULL, NULL} // empty strings and NULLs are different things.  
}; 
dic的类型是"由2个指向char指针的2元素数组组成的5元素数组",或者说是 char *[5][2]。按照上述规则,表达式dic应该衰减为char *(*)[2]——一个指向指针的2元素数组的指针。
查找这个字典的函数应该像这样:
char *definition(char *term, char *(*dictionary)[2]) // *NOT* char ***dictionary
{
  while ((*dictionary)[0] != NULL && strcmp((*dictionary)[0], term) != 0)
    dictionary++;
  return (*dictionary)[1];
}

你可以在主函数中调用它,例如:
char *def = definition(term, dic);

请注意,在该函数中,我们必须在*dictionary表达式周围使用括号。数组下标运算符[]的优先级高于解引用运算符*,我们不想直接对dictionary进行下标运算,而是想对dictionary所指向的数组进行下标运算。

6
我理解多维数组是指针的指针,但也许我错了?
是的,你错了。数组和指针之间有区别。数组可以衰减为指针,但指针不会携带关于指向的数组大小或配置的状态。不要将这种自动衰减与数组和指针相同的想法混淆-它们并不相同。
char ** 是指向包含字符指针的内存块的指针,这些指针本身指向字符的内存块。char [][] 是一个包含字符的单个内存块。
如果你有一个char **并使用ptr[x][y]访问它,编译器会将其转换为*(*(ptr + x)+y)。如果你有一个char [][],编译器会将arr[x][y]转换为*(ptr + rowLength*y + x)。(注意:我对X和Y的顺序不是110%确定,但这并不影响我在这里要表达的观点)请注意,给定指针,编译器不知道数组的大小或维度,并且无法确定如果您将指针视为多维数组时的实际地址。 char *dic[][40]包含大小为40的字符指针数组的数组。因此,它根本不符合你那里的任务要求。

p = (char **)dic; <-- 这就是为什么强制类型转换是不好的。编译器告诉你,你真正想要对 dic 做的事情没有任何意义。但是由于你可以将指针转换为任何其他指针,因此强制转换成功,尝试以这种方式读取数据将导致未定义的行为。


6
您需要参考 '右左规则'。或者您可以在这里解密大部分类C声明。
所以, char *p[2][3] 被解析为 p是一个由2个元素组成的数组,每个元素都是由3个元素组成的数组,每个元素都是字符指针。([]比*更强) char (*p)[2][3] 被解析为
"p是一个指向2个元素的char数组的指针,其中每个元素都是一个由3个元素组成的char数组"。(括号绑定最强)

2

我对于使用 *[] 的组合记忆规则之一是关于 main 函数的签名。很好用! :-)

你的 dic 是一个包含40个元素数组的数组,每个元素都是指向 char 的指针。

#include <iostream>
#include <typeinfo>
using namespace std;

template< class Type, unsigned N >
void tellMeAbout( Type const (&)[N] )
{
    cout << "Well, it's an array of " << typeid( Type ).name() << ".\n";
}

int main()
{
    char  *dic[][40]    = { 0 };
    tellMeAbout( dic );
}

使用Visual C++,我得到了一个char *[40]的数组。

嗯,这是一个char *[40]的数组。

祝好!

——Alf


该代码无效。typeid仅需要在多态类型上有效。char *[40]不是多态的。仅因为它在MSVC中工作并不意味着它符合标准。此外,该问题被标记为C和C++,但OP的代码是C而不是C++,因此最好发布一个C解决方案。 - Billy ONeal
@Alf:C++0x还不是一个标准。 - Billy ONeal
@Billy:在C++98中大致相同的位置。啊,你逼我挂载一个不可靠的USB驱动器!好吧,它由C++98 §5.2.8/3定义。哎呀,这也太巧了吧!:-) 顺便问一下,Billy,是你将我的回答评分降低了吗?那你能把它再评高一些吗?;-) - Cheers and hth. - Alf
1
@Billy:这里是 C++98 §5.2.8/3 的措辞,与您的想法不兼容:“当 typeid 应用于表达式时,除了多态类类型的 lvalue 之外的表达式,结果引用表示表达式的静态类型的 type_info 对象。” 直接涵盖上述代码中使用情况的措辞在 C++98 §5.2.8/4 中,“当 typeid 应用于 type-id 时,结果引用表示 type-id 类型的 type_info 对象。”请注意,对多态类型的限制将与 §5.2.8/3 冲突。祝好运。 - Cheers and hth. - Alf
@nos: 是的,我应该提一下。而且它比那个更普遍。在C和C++中,数组类型在任何需要指针的上下文中都会转化为第一个元素的指针,例如写a+0。在C++中,当用作形式参数类型时,函数声明也会衰变为指针(我不知道C是否支持这种语法)。而在C++中,您可以通过引用传递数组来避免形参的数组到指针类型衰减,例如上面的“tellMeAbout” :-)正式参数。干杯, - Cheers and hth. - Alf
显示剩余3条评论

2

我没有详细查看,但我认为作者依靠C语言按照如下方式布置了一个二维字符串数组:

键、值、键、值、键、值在连续的内存中。然后将这个数组作为字符串的一维数组遍历 p = (char **)dic;

这是C语言的优点之一,同时也是潜在的问题——它拥有很多低级别的强大功能,但你必须用良好的代码来防止副作用。


这正是他想要的。我相信这段代码片段来自于《C语言程序设计》这本书。这本书似乎存在很多不清晰和不好的C语言实践。虽然这本书中的例子可能在大多数平台上都能运行,但一般情况下我会避免滥用内部C机制。KISS。 - in70x

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