4

我最近一直在为傻瓜阅读 C++,要么标题用词不当,要么他们没有指望我。在关于利用带有字符串的指针数组的部分中,它们显示了一个函数,我完全被难住了,不知道该转向哪里。

char* int2month(int nMonth)
{
//check to see if value is in rang
if ((nMonth < 0) || (nMonth > 12))
    return "invalid";

//nMonth is valid - return the name of the month
char* pszMonths[] = {"invalid", "January", "February", "March", "April", "May", "June", 
                     "July", "August", "September", "October", "November", "December"};

return pszMonths[nMonth];
} 

首先(但不是主要问题),我不明白为什么返回类型是指针以及如何在不超出范围的情况下返回 pszMonths 。我已经在这本书和网上阅读过它,但在这个例子中我没有得到它。

我的主要问题是“这是如何工作的?!?!”。我不明白如何创建一个指针数组并实际初始化它们。如果我没记错的话,你不能用数字数据类型来做到这一点。“指针数组”中的每个指针是否像一个数组本身,包含组成单词的各个字符?这整件事让我大吃一惊。

8 月 20 日 - 因为在我看来,试图帮助我的人对我的困惑实际上源于何处感到有些困惑,我会尝试更好地解释它。我特别关注的代码部分如下:

//nMonth is valid - return the name of the month
char* pszMonths[] = {"invalid", "January", "February", "March", "April", "May", "June", 
                 "July", "August", "September", "October", "November", "December"};

我认为当你创建一个指针时,你只能将它分配给另一个预定值。我很困惑,似乎是一个指针数组(按这里的书)初始化月份名称。我不认为指针实际上可以初始化值。数组是否动态分配内存?“无效”本质上等同于“新字符”;声明或类似的东西?

如果他们回答了我的问题,我会尝试重新阅读这些帖子,但我只是第一次不明白。

4

6 回答 6

4

好的,让我们一次取一行。
 

char* int2month(int nMonth)

这一行很可能是错误的,因为它说函数返回一个指向可修改的指针char(按照惯例,这将是char数组的第一个元素)。相反,它应该说char const*orconst char*作为结果类型。这两个规范的含义完全相同,即指向char您无法修改的 a 的指针。
 

{

这只是函数体的左大括号。函数体在相应的右大括号处结束。
 

//check to see if value is in rang

这是一条评论。它被编译器忽略。
 

if ((nMonth < 0) || (nMonth > 12))
    return "invalid";

在这里,return当且仅当条件if成立时,才会执行该语句。目的是以可预测的方式处理不正确的参数值。但是,检查可能是错误的,因为它允许值 0 和 12 作为有效值,这给出了总共 13 个有效值,而日历年只有 12 个月。

顺便说一句,从技术上讲,对于return语句,指定的返回值是一个包含 8 个元素的数组char,即 7 个​​字符加上末尾的空字节。该数组被隐式转换为指向其第一个元素的指针,称为类型衰减。这种特殊的衰减,从字符串文字到指向非 const 的指针char,在 C++98 和 C++03 中得到特别支持,以便与旧 C 兼容,但在即将到来的 C++0x 标准中无效。

这本书不应该教这些丑陋的东西;用于const结果类型。


 

//nMonth is valid - return the name of the month
char* pszMonths[] = {"invalid", "January", "February", "March", "April", "May", "June", 
                     "July", "August", "September", "October", "November", "December"};

该数组初始化再次涉及该衰减。它是一个指针数组。每个指针都用一个字符串字面量进行初始化,它的类型是一个数组,并衰减为指针。

顺便说一句,“psz”前缀是一个怪物,叫做Hungarian Notation。它是为 C 编程而发明的,支持 Microsoft Programmer's Workbench 中的帮助系统。在现代编程中,它没有任何用处,而只是将最简单的代码读成乱码。你真的不想采用那个。
 

return pszMonths[nMonth];

这个索引有正式的未定义行为,也被亲切地称为“UB”,如果nMonth是值 12,因为在索引 12 处没有数组元素。实际上你会得到一些乱码的结果。

编辑:哦,我没有注意到作者将月份名称“无效”放在前面,这使得 13 个数组元素。如何隐藏代码...我没有注意到它,因为它非常糟糕且出乎意料;对“无效”的检查在函数的上层进行。


 

} 

这是函数体的右大括号。

干杯&hth.,

于 2011-08-20T04:41:18.283 回答
2

也许逐行解释会有所帮助。

/* This function takes an int and returns the corresponding month
 0 returns invalid
 1 returns January
 2 returns February
 3 returns March
 ...
 12 returns December
*/
char* int2month(int nMonth)
{
// if nMonth is less than 0 or more than 12, it's an invalid number
if ((nMonth < 0) || (nMonth > 12))
    return "invalid";

// this line creates an array of char* (strings) and fills it with the names of the months
//
char* pszMonths[] = {"invalid",  // index 0
                     "January",  // index 1
                     "February", // index 2
                     "March",    // index 3
                     "April",    // index 4
                     "May",      // index 5
                     "June",     // index 6
                     "July",     // index 7
                     "August",   // index 8
                     "September",// index 9
                     "October",  // index 10
                     "November", // index 11
                     "December"  // index 12
                    };

// use nMonth to index the pszMonths array to return the appropriate month
// if nMonth is 1, returns January because pszMonths[1] is January
// if nMonth is 2, returns February because pszMonths[2] is February
// etc
return pszMonths[nMonth];
} 

您可能不知道的第一件事是程序中的字符串文字(带有双引号的东西)实际上属于1char*类型。

您可能没有意识到的第二件事是,对char*s (即char* pszStrings[])数组的索引会产生 a char*,它是一个字符串。

在这种情况下,您可以从本地范围返回某些内容的原因是因为字符串文字在编译时存储在程序中并且不会被破坏。例如,这很好:

char* blah() { return "blah"; }

这几乎就像这样做2

int blah() { return 5; }

其次,当你有= {/* stuff */}一个数组声明之后,它被称为初始化列表。如果您像您所做的那样忽略数组的大小,编译器会通过初始化列表中的元素数量来计算数组的大小。Sochar* pszMonths[]表示“一个 char* 数组”,因为在初始化列表中有"invalid", "January","February"等并且它们是char*s 1,所以你只是char*用一些char*s 初始化你的 s 数组。而且您记错了不能对数字类型执行此操作,因为您可以对任何类型执行此操作,包括数字类型和字符串。

1它不是真正的 a char*,它是 a char const[x],你不能像用 a 那样修改那个记忆char*,但这对你现在并不重要。

2并不是真的那样,但如果它可以帮助您以这种方式思考,请放心,直到您在 C++ 方面变得更好并且可以处理各种微妙之处而不会死去。

于 2011-08-20T04:31:05.803 回答
2

您对 int2month 应该做什么的期望是什么?

你有一个关于记忆是什么样子的心智模型吗?这是我对内存的图形表示,例如:

pszMonths =      [   .       ,     .   ,   .    , ...]
                     |             |       |
                     |             |       |
                     V             |       |   
                     "invalid"     |       V
                                   |    "February"
                                   V
                               "January"

pszMonths 是一个数组,您应该已经很熟悉了。不过,数组的元素是指针。您必须按照箭头向下找到它们的值,在这种情况下是字符串。这种间接表示是必要的:用平面表示来做到这一点并不容易,因为每个月份的名称都有自己的可变长度。

如果没有更多的讨论,很难说出你在哪里卡住了。你需要多说。

[编辑]

好吧,你说的有点多了。听起来您需要更多地了解 C 的程序模型。当您的程序编译时,它会缩减为代码部分和数据部分。

数据部分包含什么?字符串文字之类的东西。每个字符串文字都布置在内存中的某个位置。如果您的编译器很好,并且您两次使用相同的文字,您的编译器将不会有两个副本,但会重用它们。

这是一个小程序来演示。

#include <stdio.h>
int main(void) {
  char *name1 = "foo";
  char *name2 = "foo";
  char *name3 = "bar";

  printf("The address of the string in the data segment is: %d\n", (int) name1);
  printf("The address of the string in the data segment is: %d\n", (int) name2);
  printf("The address of the string in the data segment is: %d\n", (int) name3);
  return 0;
}

这是我运行这个程序时的样子:

$ ./a.out
The address of the string in the data segment is: 134513904
The address of the string in the data segment is: 134513904
The address of the string in the data segment is: 134513908

当您运行 C 程序时,程序的数据部分(当然还有程序的代码部分)会被加载到内存中。只要您的程序继续运行,任何指向数据位置的指针都是好的。特别是,指向数据中某处的指针在函数调用中是有效的。

更仔细地查看输出。name1 和 name2 是指向数据中相同位置的指针,因为它是相同的文字字符串。您的 C 编译器通常非常擅长保持数据的紧凑和完整,这就是为什么您可以看到“bar”的字节与“foo”的字节正好存储在一起的原因。

(我们看到的是一个低级细节,编译器可能并不总是将字符串文字并排打包:您的编译器可以自由地将这些字符串的表示形式放在几乎任何地方。但它是很高兴看到它在这里这样做。)

作为相关说明,这就是 C 程序可以执行以下操作的原因:

char* good_function() {
  char* msg = "ok";
  return msg;
}

但不能做这样的事情:

char* bad_function() {
  char msg[] = "uh oh";
  return msg;
}

这两个函数的含义完全不同!

  1. 第一个告诉编译器:“将此字符串存储在数据段中。当您运行此函数时,将地址返回给我数据段”。
  2. 这里的第二个坏函数说“当你运行这个函数时:在堆栈上创建一个临时变量,有足够的空间来写'uh oh'。现在弹出临时空间并将地址返回到堆栈中......哦,等等,那个地址没有指向任何好的地方,它是……”
于 2011-08-20T04:33:45.920 回答
1

在 C 中,字符串只是存储在顺序内存位置中的字节序列,字节 0 标记字符串的结尾。例如,

char *s = "abcd"

将导致编译器分配 2 个内存位置:一个 5 字节长(abcd加上终止0)和一个大到足以容纳第一个(s)的地址。第二个位置是一个指针变量,第一个是它所指向的。

对于字符串数组,编译器再次分配两个内存位置。为了

char *strings[] = {"abc", "def"}

strings其中将有两个指针,其他位置将有 bytes abc\0def\0。然后第一个指针指向a,第二个指向d

于 2011-08-20T04:39:17.107 回答
1

此代码不返回pszMonths,但它返回包含在pszMonths. 这些指向字符串文字,即使超出范围也仍然有效。

这段代码令人困惑的一部分是它返回 achar*而不是 a char const*。这意味着很容易意外修改字符串。尝试这样做会导致未定义的行为。

通常,字符串文字是通过将字符串放在可执行文件的数据部分中来实现的。这意味着指向它们的指针始终保持有效。当执行代码时int2month,pszMonths 被指针填充,但底层数据位于可执行文件的其他位置。

正如我之前所说,这段代码非常不安全,不值得被出版成一本书。字符串文字可以绑定到char*,但它们实际上是由char consts 组成的。这使得意外尝试修改它们变得非常容易,这实际上会导致未定义的行为。存在这种行为的唯一原因是为了保持与 C 的兼容性,并且永远不应该在新代码中使用它。

于 2011-08-20T04:35:42.293 回答
0

首先,我们假设char*可以替换为string.

所以:

string int2month(int nMonth)
{ /* ... */ }

您返回一个指针,char因为您不能char在 C 或 C++ 中返回一个 s 数组。


在这一行:

return "invalid";

"invalid"存在于程序的内存中。这意味着它一直在您身边。(但如果您尝试直接更改它而不strcpy()先使用它,这是未定义的行为!1


想象一下:

char* szInvalid = "invalid";
char* szJanuary = "January";
char* szFebruary = "February";

string szMarch = "March";

char* pszMonths[] = {szInvalid, szJanuary, szFebruary, szMarch};

你明白为什么它是一个char*s数组吗?


1如果您这样做:

char* szFoo = "invalid";
szFoo[0] = '!'; szFoo[1] = '?';

char* szBar = "invalid"; // This *might* happen: szBar == "!?valid"
于 2011-08-20T04:32:45.790 回答