C строки путаницы - PullRequest
       44

C строки путаницы

19 голосов
/ 24 января 2010

Я сейчас изучаю C и немного запутался с массивами символов - строками.

char name[15]="Fortran";

Нет проблем с этим - это массив, который может содержать (до?) 15 символов

char name[]="Fortran";

C подсчитывает количество символов для меня, поэтому мне не нужно - аккуратно!

char* name;

Хорошо. Что теперь? Все, что я знаю, это то, что в нем может содержаться большое количество символов, которые назначаются позже (например, через ввод пользователя), но

  • Почему они называют это указателем на символ? Я знаю указатели как ссылки на переменные
  • Это "оправдание"? Находит ли это какое-либо иное применение, чем в char *?
  • Что это на самом деле? Это указатель? Как вы используете это правильно?

спасибо заранее, ламы

Ответы [ 7 ]

33 голосов
/ 24 января 2010

Я думаю, что это можно объяснить таким образом, поскольку картинка стоит тысячи слов ...

Мы начнем с char name[] = "Fortran", который является массивом символов, длина известнаво время компиляции 7, если быть точным, верно?Неправильно!это 8, так как '\ 0' является нулевым завершающим символом, все строки должны иметь его.

char name[] = "Fortran";
+======+     +-+-+-+-+-+-+-+--+
|0x1234|     |F|o|r|t|r|a|n|\0|
+======+     +-+-+-+-+-+-+-+--+ 

Во время компиляции и компоновщик дал символу name адрес памяти0x1234.Используя оператор индекса, например, name[1], компилятор знает, как вычислить, где в памяти находится символ со смещением, 0x1234 + 1 = 0x1235, и это действительно 'o'.Это достаточно просто, кроме того, со стандартом ANSI C размер типа данных char составляет 1 байт, что может объяснить, как среда выполнения может получить значение этой семантики name[cnt++], предполагая, что cnt является int eger и имеет значение 3, например, время выполнения автоматически увеличивается на единицу, и, считая от нуля, значение смещения равно 't'.Это просто пока все хорошо.

Что произойдет, если name[12] был выполнен?Ну, код либо вылетит, либо вы получите мусор, поскольку граница массива составляет от индекса / смещения 0 (0x1234) до 8 (0x123B).Все, что после этого не принадлежит переменной name, это называется переполнением буфера!

Адрес name в памяти равен 0x1234, как в примере, если вы должны были сделать это:

printf("The address of name is %p\n", &name);

Output would be:
The address of name is 0x00001234

Для краткости и в соответствии с примером адреса памяти 32-битные, поэтому вы видите дополнительные 0.Справедливо?Хорошо, давайте двигаться дальше.

Теперь перейдем к указателям ... char *name - указатель на тип char ....

Редактировать: Имы инициализируем его в NULL, как показано Спасибо Дэну за указание на небольшую ошибку ...

char *name = (char*)NULL;
+======+     +======+ 
|0x5678| ->  |0x0000|    ->    NULL
+======+     +======+ 

Во время компиляции / компоновки name ни на что не указывает, ноимеет адрес времени компиляции / ссылки для символа name (0x5678), фактически это NULL, адрес указателя name неизвестен, следовательно, 0x0000.

Теперь запомните , это очень важно, адрес символа известен во время компиляции / компоновки, но адрес указателя неизвестен при работе с указателями любого типа

Предположим, что мы делаем это:

name = (char *)malloc((20 * sizeof(char)) + 1);
strcpy(name, "Fortran");

Мы позвонили malloc, чтобы выделить блок памяти для 20 байтов, нет, это не 21, причина, по которой я добавил 1 к размеру, заключается в нулевом завершающем символе '\ 0',Предположим, что во время выполнения адрес был 0x9876,

char *name;
+======+     +======+          +-+-+-+-+-+-+-+--+
|0x5678| ->  |0x9876|    ->    |F|o|r|t|r|a|n|\0|
+======+     +======+          +-+-+-+-+-+-+-+--+

Так что, когда вы делаете это:

printf("The address of name is %p\n", name);
printf("The address of name is %p\n", &name);

Output would be:
The address of name is 0x00005678
The address of name is 0x00009876

Теперь, это иллюзия, что ' массивы и указателито же самое происходит здесь '

Когда мы делаем это:

char ch = name[1];

Что происходит во время выполнения, это:

  1. Адрес символаname ищется
  2. Получает адрес памяти этого символа, то есть 0x5678.
  3. По этому адресу содержит другой адрес, адрес указателя на память и извлекает его, то есть 0x9876
  4. Получить смещение на основе значения индекса 1 и добавить его к адресу указателя, то есть 0x9877, чтобы получить значение по этому адресу памяти, т. Е. 'O' и присвоить ch.

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

Помните , массив типа T всегда будет распадаться на указатель первого элемента типа T .

Когда мы сделаем это:

char ch = *(name + 5);
  1. Адрес символа name ищется
  2. Получает адрес памяти этого символа, то есть 0x5678.
  3. По этому адресу содержит другой адрес, адрес указателя на память и извлекает его0x9876
  4. Получите смещение на основе значения 5 и добавьте его к адресу указателя, то есть 0x987A, чтобы получить значение по этому адресу памяти, т. е. 'r' и присвоено ch.

Кстати, вы также можете сделать это с массивом символов также ...

Более того, использование операторов индекса в контексте массива, т. Е. char name[] = "..."; и name[subscript_value], в действительности совпадает с * (name + subscript_value). т.е.

name[3] is the same as *(name + 3)

А поскольку выражение *(name + subscript_value) является коммутативным , то есть наоборот,

*(subscript_value + name) is the same as *(name + subscript_value)

Следовательно, это объясняет, почему в одном из приведенных выше ответов вы можете написать это так ( несмотря на это, практика не рекомендуется, даже если она вполне законна! )

3[name]

Хорошо, как мне получить значение указателя? Вот для чего используется *, Предположим, что указатель name имеет адрес памяти указателя 0x9878, опять же, ссылаясь на приведенный выше пример, вот как это достигается:

char ch = *name;

Это означает, что получить значение, на которое указывает адрес памяти 0x9878, теперь ch будет иметь значение 'r'. Это называется разыменованием. Мы просто разыменовали указатель name, чтобы получить значение и присвоить его ch.

Кроме того, компилятор знает, что sizeof(char) равно 1, следовательно, вы можете выполнять операции увеличения / уменьшения указателя, как это

*name++;
*name--;

Указатель автоматически поднимается / опускается в результате на единицу.

Когда мы делаем это, предполагая, что адрес памяти указателя равен 0x9878:

char ch = *name++;

Какое значение * name и каков адрес, ответ: *name теперь будет содержать 't' и присвоит его ch, а адрес памяти указателя будет 0x9879.

Здесь вы также должны быть осторожны, в том же принципе и духе, что и то, что было сказано ранее относительно границ памяти в самой первой части (см. «Что произойдет, если имя [12] исполнится» в выше) результаты будут такими же, т.е. код вылетает и горит!

Теперь, что произойдет, если мы освободим блок памяти, на который указывает name, вызвав функцию C free с name в качестве параметра, т.е. free(name):

+======+     +======+ 
|0x5678| ->  |0x0000|    ->    NULL
+======+     +======+ 

Да, блок памяти освобождается и возвращается в среду выполнения для использования другим предстоящим исполнением кода malloc.

Теперь, это то место, где вступает в игру общее обозначение Ошибка сегментации , поскольку name ни на что не указывает, что происходит, когда мы разыменовываем это, т.е.

char ch = *name;

Да, код будет аварийно завершать работу с «ошибкой сегментации», это часто встречается в Unix / Linux. Под окнами появится диалоговое окно в виде строк «Неустранимая ошибка» или «Произошла ошибка с приложением, вы хотите отправить отчет в Microsoft?» .... если указатель не был malloc d и любая попытка разыменования, гарантированно потерпит крах и сгорит.

Также: запомните, для каждого malloc есть соответствующий free, если нет соответствующего free, у вас есть утечка памяти, в которой память выделяется, но не освобождается.

И вот, у вас это есть, вот как работают указатели и как массивы отличаются от указателей, если вы читаете учебник, в котором говорится, что они одинаковые, оторвите эту страницу и порвите ее! :)

Надеюсь, это поможет вам в понимании указателей.

3 голосов
/ 24 января 2010

Это указатель. Это означает, что это переменная, которая содержит адрес в памяти. Он «указывает» на другую переменную.

Он на самом деле не может - сам по себе - содержать большое количество символов. Сам по себе он может содержать только один адрес в памяти. Если вы назначите ему символы при создании, он выделит место для этих символов, а затем укажет на этот адрес. Вы можете сделать это так:

char* name = "Mr. Anderson";

Это на самом деле почти так же, как это:

char name[] = "Mr. Anderson";

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

char *name;
name = malloc(256*sizeof(char));
strcpy(name, "This is less than 256 characters, so this is fine.");

В качестве альтернативы вы можете назначить ему функцию strdup(), например:

char *name;
name = strdup("This can be as long or short as I want.  The function will allocate enough space for the string and assign return a pointer to it.  Which then gets assigned to name");

Если вы используете указатель символов таким образом - и назначаете ему память, вы должны освободить память, содержащуюся в имени, перед его переназначением. Как это:

if(name)
    free(name);
name = 0;

Убедитесь, что имя действительно является допустимой точкой, прежде чем пытаться освободить его память. Это то, что делает заявление if.

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

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

char *name;

char joe[] = "joe";
char bob[] = "bob";

name = joe;

printf("%s", name);

name = bob;
printf("%s", name);

Это то, что часто происходит, когда вы передаете статически распределенный массив в функцию, принимающую символьный указатель. Например:

void strcpy(char *str1, char *str2);

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

char buffer[256];
strcpy(buffer, "This is a string, less than 256 characters.");

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

О чем следует помнить при работе в функции. Если у вас есть функция, которая возвращает символьный указатель, не возвращайте указатель на статический массив символов, выделенный в функции. Это выйдет за рамки, и у вас будут проблемы. Повторите, не делайте этого:

char *myFunc() {
    char myBuf[64];
    strcpy(myBuf, "hi");
    return myBuf;
}

Это не сработает. Вы должны использовать указатель и выделить память (как показано ранее) в этом случае. Распределенная память сохранится даже тогда, когда вы выйдете из области действия функций. Только не забудьте освободить его, как упоминалось ранее.

Это оказалось немного более энциклопедическим, чем я предполагал, надеюсь, это поможет.

Отредактировано для удаления кода C ++. Я смешиваю их так часто, что иногда забываю.

2 голосов
/ 24 января 2010

Это действительно сбивает с толку. Важно понимать и различать, что char name[] объявляет массив, а char* name объявляет указатель. Это разные животные.

Однако массив в C может быть неявно преобразован в указатель на его первый элемент. Это дает вам возможность выполнять арифметику указателей и выполнять итерацию по элементам массива (не имеет значения элементы какого типа, char или нет). Как уже упоминалось, для доступа к элементам массива вы можете использовать как оператор индексации, так и арифметику указателя. Фактически, оператор индексирования - это просто синтаксический сахар (другое представление того же выражения) для арифметики указателей.

Важно различать разницу между массивом и указателем на первый элемент массива. Можно запросить размер массива, объявленного как char name[15], используя оператор sizeof:

char name[15] = { 0 };
size_t s = sizeof(name);
assert(s == 15);

но если вы примените sizeof к char* name, вы получите размер указателя на вашей платформе (то есть 4 байта):

char* name = 0;
size_t s = sizeof(name);
assert(s == 4); // assuming pointer is 4-bytes long on your compiler/machine

Кроме того, две формы определений массивов элементов char эквивалентны:

char letters1[5] = { 'a', 'b', 'c', 'd', '\0' };
char letters2[5] = "abcd"; /* 5th element implicitly gets value of 0 */

Двойственная природа массивов, неявное преобразование массива в указатель на его первый элемент на языке C (и C ++), указатель может использоваться как итератор для обхода элементов массива:

/ *skip to 'd' letter */
char* it = letters1;
for (int i = 0; i < 3; i++)
    it++;
2 голосов
/ 24 января 2010

char *name, само по себе, не может содержать символы . Это важно.

char *name просто объявляет, что name - это указатель (то есть переменная, значением которой является адрес), которая будет использоваться для хранения адреса одного или нескольких символов в некоторый момент позже в программе. Однако он не выделяет места в памяти для фактического хранения этих символов и не гарантирует, что name даже содержит действительный адрес. Точно так же, если у вас есть объявление типа int number, невозможно узнать значение number, пока вы не установите его явно.

Так же, как после объявления значения целого числа, вы могли бы позже установить его значение (number = 42), после объявления указателя на символ, вы могли бы позже установить его значение в качестве действительного адреса памяти, содержащего символ или последовательность символов - что вас интересует.

2 голосов
/ 24 января 2010

В C строка на самом деле является просто массивом символов, как вы можете видеть по определению. Однако внешне любой массив - это просто указатель на его первый элемент, см. Ниже тонкие тонкости. В С нет проверки диапазона, диапазон, который вы указываете в объявлении переменной, имеет значение только для выделения памяти для переменной.

a[x] - это то же самое, что и *(a + x), т. Е. Разыменование указателя a увеличивается на x.

, если вы использовали следующее:

char foo[] = "foobar";
char bar = *foo;

бар будет установлен на 'f'

Чтобы избежать путаницы и избежать вводящих в заблуждение людей, несколько дополнительных слов о более сложной разнице между указателями и массивами, спасибо avakar:

В некоторых случаях указатель фактически семантически отличается от массива, (не исчерпывающего) списка примеров:

//sizeof
sizeof(char*) != sizeof(char[10])

//lvalues
char foo[] = "foobar";
char bar[] = "baz";
char* p;
foo = bar; // compile error, array is not an lvalue
p = bar; //just fine p now points to the array contents of bar

// multidimensional arrays
int baz[2][2];
int* q = baz; //compile error, multidimensional arrays can not decay into pointer
int* r = baz[0]; //just fine, r now points to the first element of the first "row" of baz
int x = baz[1][1];
int y = r[1][1]; //compile error, don't know dimensions of array, so subscripting is not possible
int z = r[1]: //just fine, z now holds the second element of the first "row" of baz

И, наконец, забавные мелочи; поскольку a[x] эквивалентно *(a + x), вы можете использовать, например, «3 [a]» для доступа к четвертому элементу массива a. То есть следующий код является абсолютно допустимым и выведет 'b' четвертый символ строки foo.

#include <stdio.h>

int main(int argc, char** argv) {
  char foo[] = "foobar";

  printf("%c\n", 3[foo]);

  return 0;
}
2 голосов
/ 24 января 2010

char * name это просто указатель. Где-то вдоль строки памяти должно быть выделено, и адрес этой памяти сохранен в name .

  • Может указывать на один байт памяти и быть «истинным» указателем на один символ.
  • Может указывать на непрерывную область памяти, которая содержит несколько символов.
  • Если эти символы заканчиваются нулевым терминатором, low и вот, у вас есть указатель на строку.
1 голос
/ 24 января 2010

Один фактический объект массива, а другой ссылка или указатель на такой объект массива.

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

Разницу можно увидеть в значении &name. В первых двух случаях это то же значение, что и просто name, но в третьем случае это другой тип, называемый указатель на указатель на символ или **char, и это адрес сам указатель. То есть это двойной косвенный указатель.

#include <stdio.h>

char name1[] = "fortran";
char *name2 = "fortran";

int main(void) {
    printf("%lx\n%lx %s\n", (long)name1, (long)&name1, name1);
    printf("%lx\n%lx %s\n", (long)name2, (long)&name2, name2);
    return 0;
}
Ross-Harveys-MacBook-Pro:so ross$ ./a.out
100001068
100001068 fortran
100000f58
100001070 fortran
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...