ข้อแตกต่างระหว่างอาร์เรย์และตัวชี้ถ่านใน C คืออะไร?


216

ฉันพยายามทำความเข้าใจพอยน์เตอร์ใน C แต่ตอนนี้ฉันสับสนกับสิ่งต่อไปนี้:

  • char *p = "hello"

    นี่คือการชี้ตัวชี้ถ่านที่อาร์เรย์ตัวละครเริ่มต้นที่ชั่วโมง

  • char p[] = "hello"

    นี่คืออาร์เรย์ที่จัดเก็บสวัสดี

ความแตกต่างเมื่อฉันส่งตัวแปรทั้งสองไปยังฟังก์ชั่นนี้คืออะไร?

void printSomething(char *p)
{
    printf("p: %s",p);
}

5
สิ่งนี้จะไม่ถูกต้อง: char p[3] = "hello";สตริงการเริ่มต้นยาวเกินไปสำหรับขนาดของอาร์เรย์ที่คุณประกาศ สะกดผิด?
Cody Gray

16
หรือเพียงแค่char p[]="hello";จะพอเพียง!
deepdive


1
สำเนาที่เป็นไปได้ของchar s [] และ char * s ใน C คืออะไร? จริงอยู่สิ่งนี้จะถามเฉพาะเกี่ยวกับพารามิเตอร์ของฟังก์ชั่น แต่ไม่charเฉพาะเจาะจง
Ciro Santilli 法轮功冠状病六四事件法轮功

1
คุณต้องเข้าใจว่าพวกเขาแตกต่างกันโดยพื้นฐาน สิ่งธรรมดาสามัญเพียงอย่างเดียวคือฐานของ arry p [] เป็นตัวชี้ const ที่เปิดใช้งานการเข้าถึงอาร์เรย์ p [] ผ่านตัวชี้ p [] เองมีหน่วยความจำสำหรับสตริงในขณะที่ * p เพียงชี้ไปยังที่อยู่ขององค์ประกอบแรกของเพียงหนึ่ง CHAR (เช่น. ชี้ไปที่ฐานของสตริงที่จัดสรรแล้ว) หากต้องการอธิบายเรื่องนี้ให้ดีขึ้นโปรดพิจารณาด้านล่าง: char * cPtr = {'h', 'e', ​​'l', 'l', 'o', '\ 0'}; ==> นี่เป็นข้อผิดพลาดเนื่องจาก cPtr เป็นตัวชี้เฉพาะอักขระถ่าน cBuff [] = {'h', 'e', ​​'l', 'l', 'o', '\ 0'}; ==> นี่คือโอเค bcos cBuff เป็นอาร์เรย์ char
Ilavarasan

คำตอบ:


223

char*และchar[] เป็นประเภทที่แตกต่างกันแต่ก็ไม่ปรากฏในทุกกรณีทันที นี่เป็นเพราะอาร์เรย์สลายตัวเป็นพอยน์เตอร์ซึ่งหมายความว่าหากมีการchar[]ระบุประเภทของประเภทที่char*คาดว่าจะมีประเภทใดประเภทหนึ่งคอมไพเลอร์จะแปลงอาร์เรย์เป็นตัวชี้ไปยังองค์ประกอบแรกโดยอัตโนมัติ

ฟังก์ชั่นตัวอย่างของคุณprintSomethingคาดว่าจะมีตัวชี้ดังนั้นถ้าคุณพยายามที่จะส่งผ่านอาร์เรย์ไปยังสิ่งนี้:

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

คอมไพเลอร์อ้างว่าคุณเขียนสิ่งนี้:

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

บางสิ่งบางอย่างเปลี่ยนไปจาก 2012 เป็นตอนนี้ สำหรับอาร์เรย์อักขระ "s" จะพิมพ์อาร์เรย์ทั้งหมด .. เช่น "hello"
Bhanu Tez

@BhanuTez ไม่วิธีการจัดเก็บข้อมูลและสิ่งที่ทำกับข้อมูลนั้นเป็นความกังวลแยกต่างหาก ตัวอย่างนี้พิมพ์สตริงทั้งหมดเนื่องจากเป็นวิธีprintfจัดการกับ%sสตริงรูปแบบ: เริ่มต้นจากที่อยู่ที่ให้ไว้และดำเนินการต่อไปจนกระทั่งพบกับตัวสิ้นสุดของ null หากคุณต้องการพิมพ์อักขระเพียงตัวเดียวคุณสามารถใช้%cสตริงรูปแบบได้
iX3

แค่อยากถามว่าchar *p = "abc";อักขระ NULL \0ถูกต่อท้ายโดยอัตโนมัติในกรณีของอักขระถ่าน [] หรือไม่
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 [] เป็นประเภทที่แตกต่างกันและพวกเขาจะจัดการแตกต่างกันโดยคอมไพเลอร์ (ตัวชี้ = ที่อยู่ + การเป็นตัวแทนของประเภทของตัวชี้, array = ตัวชี้ + ความยาวไม่จำเป็นของอาร์เรย์ถ้าเป็นที่รู้จักตัวอย่างเช่น ) รายละเอียดสามารถพบได้ในมาตรฐาน และในระดับของ runtime ไม่มีความแตกต่างระหว่างพวกเขา (ในแอสเซมเบลอร์ดีเกือบดูด้านล่าง)

นอกจากนี้ยังมีคำถามที่เกี่ยวข้องในคำถามที่พบบ่อย C :

ถาม : ความแตกต่างระหว่างการเริ่มต้นเหล่านี้คืออะไร

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

โปรแกรมของฉันขัดข้องถ้าฉันพยายามกำหนดค่าใหม่ให้กับ p [i]

ตอบ : สตริงตัวอักษร (คำที่เป็นทางการสำหรับสตริงที่มีเครื่องหมายคำพูดคู่ในแหล่งข้อมูล C) สามารถใช้ได้สองวิธีแตกต่างกันเล็กน้อย:

  1. ในฐานะ initializer สำหรับอาร์เรย์ char เช่นเดียวกับในการประกาศถ่าน a [] มันระบุค่าเริ่มต้นของตัวละครในอาร์เรย์นั้น (และถ้าจำเป็นขนาดของมัน)
  2. ที่อื่นมันกลายเป็นอาเรย์ที่ไม่มีชื่อของสแตติกและอาเรย์ที่ไม่ได้ตั้งชื่อนี้อาจถูกเก็บไว้ในหน่วยความจำแบบอ่านอย่างเดียวซึ่งไม่สามารถแก้ไขได้ ในบริบทการแสดงออกอาร์เรย์จะถูกแปลงทันทีเป็นตัวชี้ตามปกติ (ดูหัวข้อ 6) ดังนั้นการประกาศครั้งที่สองจะเริ่มต้น p เพื่อชี้ไปที่องค์ประกอบแรกของอาร์เรย์ที่ไม่มีชื่อ

คอมไพเลอร์บางตัวมีสวิตช์ควบคุมว่าตัวอักษรสตริงสามารถเขียนได้หรือไม่ (สำหรับการคอมไพล์รหัสเก่า) และบางตัวอาจมีตัวเลือกที่ทำให้สตริงตัวอักษรได้รับการปฏิบัติอย่างเป็นทางการเป็นอาร์เรย์ของ const char

ดูคำถาม 1.31, 6.1, 6.2, 6.8 และ 11.8b

ข้อมูลอ้างอิง: K & R2 Sec. 5.5 หน้า 104

วินาที ISO 6.1.4, วินาที 6.5.7

เหตุผลหลัก 3.1.4

H&S Sec. 2.7.4 หน้า 31-2


ในขนาดของ (q) ทำไม q ไม่สลายตัวเป็นพอยน์เตอร์เนื่องจาก @ จอนกล่าวถึงในคำตอบของเขา?
แกรี่

@garyp q ไม่สลายตัวเป็นพอยน์เตอร์เนื่องจาก sizeof เป็นโอเปอเรเตอร์ไม่ใช่ฟังก์ชั่น (แม้ว่า sizeof เป็นฟังก์ชั่น q จะสลายตัวเฉพาะในกรณีที่ฟังก์ชั่นคาดว่าจะเป็นตัวชี้ถ่าน)
GiriB

ขอบคุณ แต่ printf ("% u \ n" แทนที่จะเป็น printf ("% zu \ n" ฉันคิดว่าคุณควรจะลบ z
Zakaria

33

ข้อแตกต่างระหว่าง char array vs char pointer ใน C คืออะไร?

C99 N1256 ฉบับร่าง

มีการใช้ตัวอักษรสตริงของอักขระสองแบบ:

  1. เริ่มต้นchar[]:

    char c[] = "abc";      

    นี่คือ "มายากลเพิ่มเติม" และอธิบายไว้ที่ 6.7.8 / 14 "การเริ่มต้น":

    อาเรย์ของประเภทตัวละครอาจเริ่มต้นได้โดยสตริงตัวอักษรตัวเลือกล้อมรอบในวงเล็บ อักขระต่อเนื่องของสตริงอักขระตามตัวอักษร (รวมถึงอักขระ null สิ้นสุดหากมีที่ว่างหรือถ้าอาร์เรย์มีขนาดไม่ทราบค่า) เริ่มต้นองค์ประกอบของอาร์เรย์

    ดังนั้นนี่เป็นเพียงทางลัดสำหรับ:

    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 ไบต์หรือรหัสของค่าศูนย์จะถูกผนวกเข้ากับลำดับอักขระหลายไบต์ที่เป็นผลมาจากสตริงตัวอักษรหรือตัวอักษร ลำดับอักขระแบบมัลติไบต์จะถูกใช้เพื่อเริ่มต้นอาร์เรย์ของระยะเวลาการจัดเก็บแบบสแตติกและความยาวเพียงพอที่จะมีลำดับ สำหรับตัวอักษรสตริงตัวอักษรองค์ประกอบอาร์เรย์มีประเภทถ่านและจะเริ่มต้นด้วยไบต์แต่ละลำดับอักขระ multibyte [... ]

    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 การดำเนินการเอลฟ์

โปรแกรม:

#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" ;-)
Ciro Santilli 郝海东冠状冠状病六四事件法轮功

9

คุณไม่ได้รับอนุญาตให้เปลี่ยนเนื้อหาของค่าคงที่สตริงซึ่งเป็นสิ่งแรกที่pชี้ไป ประการที่สองpคืออาร์เรย์ที่เริ่มต้นด้วยค่าคงที่สตริงและคุณสามารถเปลี่ยนเนื้อหาได้


6

สำหรับกรณีเช่นนี้เอฟเฟกต์จะเหมือนกัน: คุณจะต้องผ่านที่อยู่ของอักขระตัวแรกในสตริงอักขระ

เห็นได้ชัดว่าการประกาศไม่เหมือนกัน

ต่อไปนี้ตั้งค่าหน่วยความจำสำหรับสตริงและตัวชี้อักขระแล้วเริ่มต้นตัวชี้เพื่อชี้ไปที่อักขระแรกในสตริง

char *p = "hello";

ในขณะที่ต่อไปนี้ตั้งค่าหน่วยความจำเพียงสำหรับสตริง ดังนั้นจึงสามารถใช้หน่วยความจำน้อยลง

char p[10] = "hello";

codeplusplus.blogspot.com/2007/09/… "อย่างไรก็ตามการกำหนดค่าเริ่มต้นให้กับประสิทธิภาพและการปรับพื้นที่ว่างสำหรับอาร์เรย์"
leef

@leef: ฉันคิดว่าขึ้นอยู่กับว่าตัวแปรตั้งอยู่ที่ไหน หากอยู่ในหน่วยความจำแบบคงที่ฉันคิดว่าเป็นไปได้ที่อาเรย์และข้อมูลจะถูกเก็บไว้ในอิมเมจ EXE และไม่จำเป็นต้องมีการเริ่มต้นเลย มิฉะนั้นใช่มันอาจช้าลงหากต้องจัดสรรข้อมูลและคัดลอกข้อมูลแบบคงที่ไปแล้ว
Jonathan Wood

3

เท่าที่ฉันจำได้อาร์เรย์เป็นกลุ่มพอยน์เตอร์ ตัวอย่างเช่น

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

เป็นคำสั่งที่แท้จริง


2
ฉันจะอธิบายอาร์เรย์ว่าเป็นตัวชี้ไปยังที่อยู่ของบล็อกของหน่วยความจำ ดังนั้นเหตุผลที่จะนำคุณไปยังสมาชิกที่สองของ*(arr + 1) arrหาก*(arr)ชี้ไปยังที่อยู่หน่วยความจำ 32 บิตเช่นbfbcdf5eจากนั้น*(arr + 1)ชี้ไปที่bfbcdf60(ไบต์ที่สอง) ดังนั้นทำไมการออกนอกขอบเขตของอาเรย์จะนำไปสู่ผลลัพธ์ที่แปลก ๆ หากint a = 24;เป็นที่อยู่การbfbcdf62เข้าถึงarr[2]อาจส่งคืน24โดยสมมติว่า segfault ไม่ได้เกิดขึ้นก่อน
Braden สุดยอด

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"จำไว้ว่ามีถ่าน '\ 0' ในตอนท้ายของ "สตริง" ในซี

อย่างไรก็ตามอาร์เรย์ใน C เป็นเพียงตัวชี้ไปยังวัตถุแรกของวัตถุปรับในหน่วยความจำ s ที่แตกต่างกันเท่านั้นที่อยู่ในความหมาย ในขณะที่คุณสามารถเปลี่ยนค่าของตัวชี้ให้ชี้ไปยังตำแหน่งอื่นในอาเรย์หลังจากสร้างแล้วจะชี้ไปที่ตำแหน่งเดียวกันเสมอ
นอกจากนี้เมื่อใช้อาร์เรย์ "ใหม่" และ "ลบ" จะทำโดยอัตโนมัติสำหรับคุณ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.