Яка різниця між масивом char та char pointer у 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";вистачило б!
deepdive


1
можливий дублікат Чому різниця між char s [] та char * s у C? Щоправда, це також запитує конкретно про параметр функції, але це не charконкретно.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

1
вам потрібно зрозуміти, що вони принципово різні. Єдина спільність у цьому полягає в тому, що база масиву p [] - це const покажчик, який дозволив отримати доступ до масиву p [] через покажчик. p [] сам утримує пам'ять для рядка, тоді як * p просто вказує на адресу першого елемента всього ONE CHAR (тобто вказує на основу вже виділеного рядка). Щоб краще проілюструвати це, розглянемо нижче: char * cPtr = {'h', 'e', ​​'l', 'l', 'o', '\ 0'}; ==> це помилка, оскільки cPtr - покажчик лише символу char cBuff [] = {'h', 'e', ​​'l', 'l', 'o', '\ 0'}; ==> Це нормально, bcos cBuff сам по собі - це масив
чарів

Відповіді:


223

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 є відповідне питання :

З : Яка різниця між цими ініціалізаціями?

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 сек. 5,5 с. 104

ISO сек. 6.1.4, розд. 6.5.7

Обґрунтування розд. 3.1.4

Секція H&S 2.7.4 С. 31-2


Чому розмір (q), чому q не перетворюється на покажчик, як згадує @Jon у своїй відповіді?
garyp

@garyp q не занепадає в покажчик, тому що sizeof є оператором, а не функцією (навіть якщо sizeof була функцією, q занепадає лише в тому випадку, якщо функція очікувала покажчика char).
GiriB

дякую, але printf ("% u \ n" замість printf ("% zu \ n", я думаю, вам слід видалити z.
Zakaria

33

Яка різниця між масивом char та char pointer у 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";

визначає "прості" об'єкти масиву char sта tелементи яких ініціалізуються літеральними рядками символів.

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

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

Вміст масивів може змінюватися. З іншого боку, декларація

char *p = "abc";

визначає pз типом "покажчик на char" та ініціалізує його для вказівки на об'єкт типу "масив char" довжиною 4, елементи якого ініціалізовані літеральним рядком символів. Якщо буде здійснена спроба pзмінити вміст масиву, поведінка не визначена.

Реалізація ELF GCC 4.8 x86-64

Програма:

#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 мову UB" ;-)
Чіро Сантіллі郝海东冠状病六四事件法轮功

9

Вам не дозволяється змінювати вміст константи рядка, на що pвказують перші . Другий p- це масив, ініціалізований зі строковою константою, і ви можете змінювати його вміст.


6

Для таких випадків ефект такий же: Ви в кінці передаєте адресу першого символу в рядку символів.

Декларації, очевидно, не однакові.

Далі виділяється пам'ять для рядка, а також вказівник символів, а потім ініціалізується вказівник, щоб вказати на перший символ у рядку.

char *p = "hello";

Тоді як наступне виділяє пам'ять лише для рядка. Таким чином, він може використовувати менше пам'яті.

char p[10] = "hello";

codeplusplus.blogspot.com/2007/09/… "Однак, ініціалізація змінної вимагає величезної продуктивності та пробілу для масиву"
leef

@leef: Я думаю, це залежить від того, де знаходиться змінна. Якщо він знаходиться в статичній пам'яті, я думаю, що можливо масив і дані зберігаються у зображенні EXE і не вимагають ініціалізації взагалі. Інакше, так, звичайно, це може бути повільніше, якщо дані потрібно розподілити, а потім статичні дані потрібно скопіювати.
Джонатан Вуд

3

Наскільки я пам’ятаю, масив - це фактично група покажчиків. Наприклад

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

є правдивим твердженням


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 - це лише вказівник на перший об'єкт коригуючих об'єктів у пам'яті. єдині різні s в семантиці. при цьому ви можете змінити значення вказівника, щоб вказати на інше місце в пам'яті, масив після створення завжди буде вказувати на одне і те ж місце.
також при використанні масиву "новий" та "видалити" автоматично робиться для вас.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.