В чем разница между массивом char и указателем в C?

216

Я пытаюсь понять указатели в C, но я в настоящее время путаю со следующим:

  • char *p = "hello"

    Это указатель на символ, указывающий на массив символов, начиная с h .

  • char p[] = "hello"

    Это массив, который хранит привет .

Какая разница, когда я передаю обе эти переменные в эту функцию?

void printSomething(char *p)
{
    printf("p: %s",p);
}
дизель
источник
5
Это недопустимо: char p[3] = "hello";строка инициализатора слишком длинна для размера объявленного вами массива. Опечатка?
Коди Грей
16
Или просто char p[]="hello";будет достаточно!
глубокое погружение
1
Возможный дубликат В чем разница между char s [] и char * s в C? Правда, это также спрашивает конкретно о параметре функции, но это не является charконкретным.
Сиро Сантилли 郝海东 冠状 病 六四 事件 法轮功
1
нужно понимать, что они принципиально разные. единственная общность в этом состоит в том, что основанием arry p [] является константный указатель, который позволяет получить доступ к массиву p [] через указатель. p [] сам хранит память для строки, тогда как * p просто указывает на адрес первого элемента, равного ONE CHAR (т. е. указывает на базу уже выделенной строки). Чтобы лучше проиллюстрировать это, рассмотрим ниже: char * cPtr = {'h', 'e', ​​'l', 'l', 'o', '\ 0'}; ==> это ошибка, так как cPtr - указатель только на символ char cBuff [] = {'h', 'e', ​​'l', 'l', 'o', '\ 0'}; ==> Это нормально, bcos cBuff сам по себе является массивом символов
Ilavarasan

Ответы:

224

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

Ваша примерная функция printSomethingожидает указатель, поэтому, если вы попытаетесь передать ей массив следующим образом:

char s[10] = "hello";
printSomething(s);

Компилятор делает вид, что вы написали это:

char s[10] = "hello";
printSomething(&s[0]);
Джон
источник
Что-то изменилось с 2012 года по настоящее время. Для массива символов "s" печатает весь массив .. т.е. "привет"
Bhanu Tez
@BhanuTez Нет, как данные хранятся и что с ними делается, это отдельная проблема. В этом примере печатается вся строка, потому что именно так printfобрабатывается %sстрока формата: начинайте с указанного адреса и продолжайте до тех пор, пока не встретите нулевой терминатор. Если вы хотите напечатать только один символ, вы можете использовать %c, например, строку формата.
iX3
Просто хотел спросить, автоматически ли добавляется char *p = "abc";символ NULL, \0как в случае с массивом char []?
KPMG
почему я могу установить, char *name; name="123";но могу сделать то же самое с intтипом? И после использования %cдля печати name, вывод нечитаемой строка: ?
TomSawyer
83

Посмотрим:

#include <stdio.h>
#include <string.h>

int main()
{
    char *p = "hello";
    char q[] = "hello"; // no need to count this

    printf("%zu\n", sizeof(p)); // => size of pointer to char -- 4 on x86, 8 on x86-64
    printf("%zu\n", sizeof(q)); // => size of char array in memory -- 6 on both

    // size_t strlen(const char *s) and we don't get any warnings here:
    printf("%zu\n", strlen(p)); // => 5
    printf("%zu\n", strlen(q)); // => 5

    return 0;
}

foo * и foo [] - это разные типы, и они обрабатываются компилятором по-разному (указатель = адрес + представление типа указателя, массив = указатель + необязательная длина массива, если он известен, например, если массив размещен статически ), подробности можно найти в стандарте. И на уровне времени выполнения нет никакой разницы между ними (в ассемблере, ну почти, см. Ниже).

Кроме того, есть связанный вопрос в C FAQ :

Q : В чем разница между этими инициализациями?

char a[] = "string literal";   
char *p  = "string literal";   

Моя программа падает, если я пытаюсь назначить новое значение p [i].

A : Строковый литерал (формальный термин для строки в двойных кавычках в C-источнике) может использоваться двумя немного различными способами:

  1. Как инициализатор для массива char, как в объявлении char a [], он определяет начальные значения символов в этом массиве (и, если необходимо, его размер).
  2. В любом другом месте он превращается в безымянный статический массив символов, и этот безымянный массив может храниться в постоянной памяти и поэтому не может быть изменен. В контексте выражения массив, как обычно, сразу преобразуется в указатель (см. Раздел 6), поэтому второе объявление инициализирует p, чтобы указывать на первый элемент безымянного массива.

Некоторые компиляторы имеют переключатель, управляющий тем, доступны ли строковые литералы для записи или нет (для компиляции старого кода), а у некоторых могут быть параметры, позволяющие формально обрабатывать строковые литералы как массивы const char (для лучшего обнаружения ошибок).

См. Также вопросы 1.31, 6.1, 6.2, 6.8 и 11.8b.

Ссылки: K & R2 Sec. 5,5 р. 104

ISO Sec. 6.1.4, гл. 6.5.7

Обоснование с. 3.1.4

H & S Sec. 2.7.4 с. 31-2

JJJ
источник
В sizeof (q), почему q не распадается на указатель, как @Jon упоминает в своем ответе?
Гарип
@garyp q не превращается в указатель, потому что sizeof является оператором, а не функцией (даже если sizeof была функцией, q будет затухать, только если функция ожидает указатель на символ).
GiriB
спасибо, но printf ("% u \ n" вместо printf ("% zu \ n", я думаю, вам следует удалить z.
Закария
33

В чем разница между массивом char и char в C?

Тяга C99 N1256

Существует два различных варианта использования строковых литералов символов:

  1. Инициализировать char[]:

    char c[] = "abc";      

    Это «больше волшебства», и описано в 6.7.8 / 14 «Инициализация»:

    Массив символьного типа может быть инициализирован литералом символьной строки, необязательно заключенным в фигурные скобки. Последовательные символы литерала символьной строки (включая завершающий нулевой символ, если есть место или если массив имеет неизвестный размер) инициализируют элементы массива.

    Так что это просто ярлык для:

    char c[] = {'a', 'b', 'c', '\0'};

    Как и любой другой обычный массив, cможет быть изменен.

  2. Везде: генерирует:

    Поэтому, когда вы пишете:

    char *c = "abc";

    Это похоже на:

    /* __unnamed is magic because modifying it gives UB. */
    static char __unnamed[] = "abc";
    char *c = __unnamed;
    

    Обратите внимание на неявное приведение от char[]к char *, которое всегда допустимо.

    Затем, если вы измените c[0], вы также измените __unnamed, что является UB.

    Это описано в 6.4.5 «Строковые литералы»:

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

    6 Не указано, различаются ли эти массивы при условии, что их элементы имеют соответствующие значения. Если программа пытается изменить такой массив, поведение не определено.

6.7.8 / 32 «Инициализация» приводит прямой пример:

ПРИМЕР 8: Декларация

char s[] = "abc", t[3] = "abc";

определяет понятие «простые» объекты массив символов sи tчьи элементы инициализируются с символьной строки литералов.

Эта декларация идентична

char s[] = { 'a', 'b', 'c', '\0' },
t[] = { 'a', 'b', 'c' };

Содержимое массивов может быть изменено. С другой стороны, декларация

char *p = "abc";

определяется pс типом «указатель на символ» и инициализирует его, чтобы указать на объект с типом «массив символа» длиной 4, элементы которого инициализируются литералом символьной строки. Если предпринята попытка использовать pдля изменения содержимого массива, поведение не определено.

GCC 4.8 x86-64 реализация ELF

Программа:

#include <stdio.h>

int main(void) {
    char *s = "abc";
    printf("%s\n", s);
    return 0;
}

Компилировать и декомпилировать:

gcc -ggdb -std=c99 -c main.c
objdump -Sr main.o

Выход содержит:

 char *s = "abc";
8:  48 c7 45 f8 00 00 00    movq   $0x0,-0x8(%rbp)
f:  00 
        c: R_X86_64_32S .rodata

Вывод: GCC хранит char*его в .rodataразделе, а не в .text.

Если мы сделаем то же самое для char[]:

 char s[] = "abc";

мы получаем:

17:   c7 45 f0 61 62 63 00    movl   $0x636261,-0x10(%rbp)

поэтому он сохраняется в стеке (относительно %rbp).

Однако обратите внимание, что скрипт компоновщика по умолчанию помещает .rodataи .textв тот же сегмент, который имеет исполняемый файл, но не имеет разрешения на запись. Это можно наблюдать с:

readelf -l a.out

который содержит:

 Section to Segment mapping:
  Segment Sections...
   02     .text .rodata
Сиро Сантилли 郝海东 冠状 病 六四 事件 法轮功
источник
2
@ leszek.hanusz Неопределенное поведение stackoverflow.com/questions/2766731/… Google "C language UB" ;-)
Сиро Сантилли 郝海东 冠状 病 六四 事件 法轮功
9

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

potrzebie
источник
6

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

Заявления, очевидно, не то же самое, хотя.

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

char *p = "hello";

В то время как следующее выделяет память только для строки. Так что на самом деле он может использовать меньше памяти.

char p[10] = "hello";
Джонатан Вуд
источник
codeplusplus.blogspot.com/2007/09/… «Однако инициализация переменной требует огромных потерь производительности и пространства для массива»
leef
@leef: я думаю, это зависит от того, где находится переменная. Если он находится в статической памяти, я думаю, что массив и данные могут храниться в образе EXE и вообще не требовать никакой инициализации. В противном случае, да, конечно, это может быть медленнее, если данные должны быть распределены, а затем статические данные должны быть скопированы.
Джонатан Вуд
3

Насколько я помню, массив - это фактически группа указателей. Например

p[1]== *(&p+1)

это верное утверждение

CosminO
источник
2
Я бы описал массив как указатель на адрес блока памяти. Отсюда и *(arr + 1)сводит вас ко второму члену arr. Если *(arr)указывает на 32-битный адрес памяти, например bfbcdf5e, тогда *(arr + 1)указывает на bfbcdf60(второй байт). Следовательно, почему выход из области видимости массива приведет к странным результатам, если ОС не перестанет работать. Если он int a = 24;находится по адресу bfbcdf62, то доступ arr[2]может вернуться 24, при условии, что вначале не произошла ошибка.
Брэден Бест
3

От APUE , раздел 5.14:

char    good_template[] = "/tmp/dirXXXXXX"; /* right way */
char    *bad_template = "/tmp/dirXXXXXX";   /* wrong way*/

... Для первого шаблона имя выделяется в стеке, потому что мы используем переменную массива. Однако для второго имени мы используем указатель. В этом случае только память самого указателя находится в стеке; компилятор организует сохранение строки в доступном только для чтения сегменте исполняемого файла. Когда mkstempфункция пытается изменить строку, возникает ошибка сегментации.

Цитируемый текст соответствует объяснениям @Ciro Santilli.

стог
источник
1

char p[3] = "hello"? следует char p[6] = "hello"помнить, что в конце строки в C. есть символ '\ 0'

в любом случае, массив в C - это просто указатель на первый объект настраиваемых объектов в памяти. единственные различия в семантике. в то время как вы можете изменить значение указателя, чтобы оно указывало на другое место в памяти, массив после создания всегда будет указывать на одно и то же место.
также при использовании массива «new» и «delete» автоматически выполняются за вас.

Роу Гавирел
источник