Использование массивов символьных строк: массивов указателей - они похожи на многомерные массивы? - PullRequest
4 голосов
/ 20 августа 2011

В последнее время я читаю 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, не выходя из области видимости.Я читал об этом в этой книге и в Интернете, но в этом примере я его не понимаю.

Главный вопрос, который у меня есть, это "как это работает?!?!".Я не понимаю, как вы можете создать массив указателей и фактически инициализировать их.Если я правильно помню, вы не можете сделать это с числовыми типами данных.Каждый указатель в «массиве указателей» похож на сам массив, содержащий отдельные символы, из которых состоят слова?Все это просто поражает меня.

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"};

Я думал, что когда вы создаете указатель, вы можете назначить его только другому предопределенному значению.Я запутался, что то, что кажется массивом указателей (в соответствии с этой книгой), инициализирует названия месяцев.Я не думал, что указатели действительно могут инициализировать значения.Массив динамически распределяет память?Является ли «недействительным» по существу эквивалентным «новому символу»;утверждение или что-то подобное?

Я попробую перечитать посты, если они ответят на мои вопросы, но я просто не понял первый раз.

Ответы [ 6 ]

4 голосов
/ 20 августа 2011

хорошо, давай по одной строке за раз.

char* int2month(int nMonth)

Эта строка, скорее всего, НЕПРАВИЛЬНА , потому что она говорит, что функция возвращает указатель на модифицируемую char (по соглашению это будет первый char элемент массива). Вместо этого он должен сказать char const* или const char* в качестве типа результата. Эти две спецификации означают одно и то же, а именно указатель на char, который вы не можете изменить.

{

Это просто открывающая скобка тела функции. Тело функции заканчивается на соответствующей закрывающей скобке.

//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 символов плюс нулевой байт в конце. Этот массив неявно преобразуется в указатель на его первый элемент, который называется типом decay . Этот конкретный спад, от строкового литерала до указателя на неконстантный 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" - это чудовище, которое называется Венгерская нотация . Он был изобретен для программирования на C, поддерживающего систему помощи в Microsoft's Programmer's Workbench. В современном программировании это не служит никакой полезной цели, но вместо этого просто получает простейший код, читаемый как бред. Вы действительно не хотите принять это.

return pszMonths[nMonth];

Это индексирование имеет формальное неопределенное поведение , также называемое ласково «UB», если nMonth - это значение 12, так как в индексе 12 нет элемента массива. На практике вы получу какой-то бредовый результат.

РЕДАКТИРОВАТЬ : о, я не заметил, что автор поместил название месяца "неверный" спереди, что составляет 13 элементов массива. как скрыть код ... я его не заметил, потому что он очень плохой и неожиданный; проверка на «недействительность» выполняется выше в функции.


} 

И это закрывающая скобка тела функции.

Приветствия и hth.,

2 голосов
/ 20 августа 2011

Чего вы ожидаете от того, что должен делать int2month?

У вас есть ментальная модель того, как выглядит память?Вот мое графическое представление памяти, например:

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

pszMonths - это массив, с которым вы уже должны быть знакомы.Однако элементы массива являются указателями.Вы должны следовать за стрелками до их значений, в этом случае строки.Этот вид косвенного представления необходим: это нелегко сделать с плоским представлением, потому что у каждого названия месяца есть своя, переменная длина.

Очень сложно сказать, где вы застряли, не имея большеобсуждение.Вы должны сказать больше.

[Редактировать]

Хорошо, вы сказали немного больше.Похоже, вам нужно немного больше узнать о программной модели Си.Когда ваша программа компилируется, она сводится к части кода и части данных.

Что входит в часть данных?Такие вещи, как строковые литералы.Каждый строковый литерал выложен где-то в памяти.Если ваш компилятор хорош, и если вы используете один и тот же литерал дважды, ваш компилятор не будет иметь двух копий, но будет использовать их повторно.

Вот небольшая программа для демонстрации.

#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. Вторая плохая функция здесь говорит:" Когда вы запускаете эту функцию: создайте временную переменную в стеке с достаточным пространством для записи"О, о"Теперь удалите временное пространство и верните адрес в стек ... о, подождите, этот адрес никуда не указывает, это хорошо ... "
2 голосов
/ 20 августа 2011

Возможно, построчное объяснение поможет.

/* 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];
} 

Первое, на что вы можете не обращать внимания, это то, что строковый литерал в вашей программе (материал с двойными кавычками) действительно имеет тип char* 1 .

Второе, что вы, возможно, не поняли, это то, что индексирование в массив char* s (то есть char* pszStrings[]) дает char*, который является строкой.

Причина, по которой вы можете вернуть что-либо из локальной области видимости в этом случае, заключается в том, что строковые литералы хранятся в программе во время компиляции и не уничтожаются. Например, это прекрасно:

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

И это почти похоже на это 2 :

int blah() { return 5; }

Во-вторых, когда у вас есть = {/* stuff */} после объявления массива, это называется списком инициализатора. Если вы укажете размер массива, как вы делаете, компилятор выяснит, насколько велик размер массива по количеству элементов в списке инициализатора. Таким образом, char* pszMonths[] означает «массив символов *», и, поскольку у вас есть "invalid", "January", "February" и т. Д. В списке инициализатора, а они равны char* s 1 , вы Вы просто инициализируете свой массив char* с некоторыми char* с. И вы ошиблись из-за невозможности сделать это с числовыми типами, потому что вы можете сделать это с любым типом, включая числовые типы и строки.

1 Это на самом деле не char*, это char const[x], и вы не можете изменить эту память, как вы могли бы с помощью char*, но это сейчас не важно для вас.

2 Это не совсем так, но если это поможет вам думать об этом таким образом, не стесняйтесь, пока вы не станете лучше в C ++ и сможете справиться с различными тонкостями, не умирая.

1 голос
/ 20 августа 2011

В C строки - это просто последовательности байтов, хранимые в последовательных ячейках памяти, байт 0 обозначает конец строки.Например,

char *s = "abcd"

приведет к тому, что компилятор выделит 2 области памяти: одну длиной в пять байтов (abcd плюс завершающую 0) и одну достаточно большую для хранения адреса первой).Второе расположение - переменная указателя, первое - это то, на что оно указывает.

Для массива строк компилятор снова выделяет две области памяти.Например,

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

strings будет иметь два указателя, а остальные местоположения будут иметь байты abc\0def\0.Тогда первый указатель указывает на a, а второй на d.

1 голос
/ 20 августа 2011

Этот код не возвращает pszMonths, но возвращает один из указателей, содержащихся в pszMonths. Они указывают на строковые литералы, которые остаются действительными даже при выходе из области видимости.

Одна часть этого кода, которая сбивает с толку, это то, что он возвращает char*, а не char const*. Это означает, что легко случайно изменить строки. Попытка сделать это приведет к неопределенному поведению.

Обычно строковые литералы реализуются путем размещения строк в разделе данных исполняемого файла. Это означает, что указатели на них всегда остаются действительными. Когда код в int2month выполняется, pszMonths заполняется указателями, но базовые данные находятся в другом месте исполняемого файла.

Как я уже говорил ранее, этот код очень небезопасен и не заслуживает того, чтобы быть закрепленным публикацией в книге. Строковые литералы могут быть связаны с char*, но на самом деле они состоят из char const s. Это позволяет очень легко случайно попытаться изменить их, что фактически приведет к неопределенному поведению. Единственная причина, по которой существует такое поведение, заключается в поддержании совместимости с C, и его никогда не следует использовать в новом коде.

0 голосов
/ 20 августа 2011

Прежде всего, давайте предположим, что char* можно заменить на string.

Итак:

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

Вы возвращаете указатель на char, потому что вы не можете вернуть массив char s в C или C ++.


В этой строке:

return "invalid";

"invalid" живет в памяти программы. Это значит, что он всегда рядом с тобой. (Но это неопределенное поведение, если вы попытаетесь изменить его напрямую, не используя strcpy() сначала! 1 )


Представьте себе это:

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

string szMarch = "March";

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

Вы видите, почему это массив char* с?


1 Если вы сделаете это:

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

char* szBar = "invalid"; // This *might* happen: szBar == "!?valid"
...