ใน C หนึ่งสามารถใช้ตัวอักษรสตริงในการประกาศเช่นนี้:
char s[] = "hello";
หรือเช่นนี้
char *s = "hello";
ดังนั้นความแตกต่างคืออะไร? ฉันต้องการที่จะรู้ว่าสิ่งที่เกิดขึ้นจริงในแง่ของระยะเวลาการจัดเก็บทั้งในเวลารวบรวมและเวลาทำงาน
ใน C หนึ่งสามารถใช้ตัวอักษรสตริงในการประกาศเช่นนี้:
char s[] = "hello";
หรือเช่นนี้
char *s = "hello";
ดังนั้นความแตกต่างคืออะไร? ฉันต้องการที่จะรู้ว่าสิ่งที่เกิดขึ้นจริงในแง่ของระยะเวลาการจัดเก็บทั้งในเวลารวบรวมและเวลาทำงาน
คำตอบ:
ความแตกต่างที่นี่ก็คือ
char *s = "Hello world";
จะวาง"Hello world"
ในส่วนอ่านอย่างเดียวของหน่วยความจำและทำให้s
ตัวชี้ไปยังที่ทำให้การเขียนใด ๆ ในหน่วยความจำนี้ผิดกฎหมาย
ในขณะที่ทำ:
char s[] = "Hello world";
วางสตริงตัวอักษรในหน่วยความจำแบบอ่านอย่างเดียวและคัดลอกสตริงไปยังหน่วยความจำที่จัดสรรใหม่บนสแต็ก จึงทำให้
s[0] = 'J';
ถูกกฎหมาย
"Hello world"
อยู่ใน "ส่วนอ่านอย่างเดียวของหน่วยความจำ" ในทั้งสองตัวอย่าง ตัวอย่างที่มีอาเรย์ชี้ไปที่นั่นตัวอย่างของอาเรย์จะคัดลอกอักขระไปยังองค์ประกอบอาเรย์
char msg[] = "hello, world!";
สตริงสิ้นสุดในส่วนข้อมูลเริ่มต้น เมื่อประกาศว่าchar * const
จะลงเอยในส่วนข้อมูลแบบอ่านอย่างเดียว gcc-4.5.3
ก่อนอื่นในฟังก์ชันอาร์กิวเมนต์พวกเขาจะเทียบเท่า:
void foo(char *x);
void foo(char x[]); // exactly the same in all respects
ในบริบทอื่น ๆchar *
จัดสรรตัวชี้ในขณะที่char []
จัดสรรอาร์เรย์ สตริงจะไปไหนในกรณีก่อนหน้าคุณถาม? คอมไพเลอร์จัดสรรอาร์เรย์แบบไม่ระบุชื่อแบบสแตติกเพื่อเก็บสตริงตามตัวอักษรอย่างลับ ๆ ดังนั้น:
char *x = "Foo";
// is approximately equivalent to:
static const char __secret_anonymous_array[] = "Foo";
char *x = (char *) __secret_anonymous_array;
โปรดทราบว่าคุณจะต้องไม่พยายามแก้ไขเนื้อหาของอาร์เรย์ที่ไม่ระบุชื่อผ่านตัวชี้นี้ ผลกระทบที่ไม่ได้กำหนด (มักหมายถึงความผิดพลาด):
x[1] = 'O'; // BAD. DON'T DO THIS.
การใช้ไวยากรณ์อาร์เรย์จะจัดสรรให้โดยตรงในหน่วยความจำใหม่ ดังนั้นการปรับเปลี่ยนจึงปลอดภัย:
char x[] = "Foo";
x[1] = 'O'; // No problem.
อย่างไรก็ตามอาเรย์จะอยู่ได้ตราบใดที่ขอบเขตที่ต่อกันดังนั้นหากคุณทำสิ่งนี้ในฟังก์ชั่นอย่าส่งคืนหรือทำให้พอยเตอร์ชี้ไปที่อาเรย์นี้ทำสำเนาแทนด้วยstrdup()
หรือคล้ายกัน หากมีการจัดสรรอาเรย์ในขอบเขตทั่วโลกแน่นอนไม่มีปัญหา
คำประกาศนี้:
char s[] = "hello";
สร้างหนึ่งในวัตถุ - เป็นchar
อาร์เรย์ของขนาด 6 เรียกว่าs
, initialised 'h', 'e', 'l', 'l', 'o', '\0'
ด้วยค่า ที่จัดสรรอาร์เรย์นี้ในหน่วยความจำและนานเท่าไรขึ้นอยู่กับที่ประกาศจะปรากฏขึ้น หากการประกาศอยู่ภายในฟังก์ชั่นมันจะยังคงอยู่จนกว่าจะสิ้นสุดของบล็อกที่มีการประกาศในและเกือบจะแน่นอนได้รับการจัดสรรในกอง; ถ้ามันอยู่นอกฟังก์ชั่นมันอาจจะถูกเก็บไว้ใน "ส่วนเริ่มต้นข้อมูล" ที่โหลดจากไฟล์ปฏิบัติการลงในหน่วยความจำที่เขียนได้เมื่อเรียกใช้โปรแกรม
ในทางตรงกันข้ามการประกาศนี้:
char *s ="hello";
สร้างวัตถุสองรายการ :
char
s ที่มีค่า'h', 'e', 'l', 'l', 'o', '\0'
ซึ่งไม่มีชื่อและมีระยะเวลาในการจัดเก็บข้อมูลแบบคงที่ (หมายถึงว่ามันอาศัยสำหรับชีวิตทั้งหมดของโปรแกรม); และs
ร์ซึ่งถูกกำหนดค่าเริ่มต้นด้วยตำแหน่งของอักขระตัวแรกในอาเรย์แบบอ่านอย่างเดียวที่ไม่มีชื่อโดยทั่วไปแล้วอาเรย์แบบอ่านอย่างเดียวที่ไม่มีชื่อจะอยู่ในส่วน "ข้อความ" ของโปรแกรมซึ่งหมายความว่าโหลดจากดิสก์ลงในหน่วยความจำแบบอ่านอย่างเดียวพร้อมกับรหัสเอง ตำแหน่งของs
ตัวแปรพอยน์เตอร์ในหน่วยความจำขึ้นอยู่กับว่าการประกาศปรากฏขึ้นที่ใด (เช่นในตัวอย่างแรก)
char s[] = "hello"
กรณีที่"hello"
เป็นเพียงผู้เริ่มต้นบอกคอมไพเลอร์ว่าควรจะเริ่มต้นอาร์เรย์ มันอาจหรือไม่อาจส่งผลให้สตริงที่สอดคล้องกันในส่วนของข้อความ - ตัวอย่างเช่นถ้าs
มีระยะเวลาการจัดเก็บแบบคงที่ก็มีโอกาสที่อินสแตนซ์เดียวของ"hello"
จะอยู่ในส่วนข้อมูลเริ่มต้น - วัตถุs
เอง แม้ว่าจะs
มีระยะเวลาการเก็บข้อมูลอัตโนมัติ แต่ก็สามารถเริ่มต้นได้ด้วยลำดับของที่เก็บที่แท้จริงแทนที่จะเป็นสำเนา (เช่นmovl $1819043176, -6(%ebp); movw $111, -2(%ebp)
)
.rodata
.text
ดูคำตอบของฉัน
char s[] = "Hello world";
ใส่สตริงตัวอักษรในหน่วยความจำแบบอ่านอย่างเดียวและคัดลอกสตริงไปยังหน่วยความจำที่จัดสรรใหม่บนสแต็ก แต่คำตอบของคุณเพียง copies the string to newly allocated memory on the stack
แต่พูดเกี่ยวกับการใส่สตริงตัวอักษรในหน่วยความจำอ่านอย่างเดียวและข้ามส่วนที่สองของประโยคที่กล่าวว่า: ดังนั้นคำตอบของคุณไม่สมบูรณ์สำหรับการไม่ระบุส่วนที่สอง?
char s[] = "Hellow world";
นั้นเป็นเพียงตัวเริ่มต้นและไม่จำเป็นต้องเก็บไว้เป็นสำเนาแบบอ่านอย่างเดียวเลย หากs
มีระยะเวลาการจัดเก็บแบบคงที่สำเนาเดียวของสตริงน่าจะอยู่ในส่วนอ่าน - เขียนที่ตำแหน่งของs
และแม้ว่าจะไม่ใช่จากนั้นคอมไพเลอร์อาจเลือกที่จะเริ่มต้นอาร์เรย์ด้วยคำแนะนำโหลดทันทีหรือคล้ายกันมากกว่าการคัดลอก จากสตริงอ่านอย่างเดียว ประเด็นก็คือในกรณีนี้สตริงตัวเริ่มต้นเองไม่มีสถานะรันไทม์
รับการประกาศ
char *s0 = "hello world";
char s1[] = "hello world";
สมมติแผนที่หน่วยความจำสมมุติต่อไปนี้:
0x01 0x02 0x03 0x04 0x00008000: 'h' 'e' 'l' 'l' 0x00008004: 'o' '' 'w' 'o' 0x00008008: 'r' 'l' 'd' 0x00 ... s0: 0x00010000: 0x00 0x00 0x80 0x00 s1: 0x00010004: 'h' 'e' 'l' 'l' 0x00010008: 'o' '' 'w' 'o' 0x0001000C: 'r' 'l' 'd' 0x00
สตริงตัวอักษร"hello world"
เป็นอาร์เรย์ 12 องค์ประกอบของchar
( const char
ใน C ++) ที่มีระยะเวลาการจัดเก็บแบบคงที่ซึ่งหมายความว่าหน่วยความจำสำหรับมันจะถูกจัดสรรเมื่อโปรแกรมเริ่มต้นขึ้นและยังคงจัดสรรจนกว่าโปรแกรมจะสิ้นสุดลง ความพยายามที่จะแก้ไขเนื้อหาของสตริงที่แท้จริงเรียกพฤติกรรมที่ไม่ได้กำหนด
เส้น
char *s0 = "hello world";
กำหนดs0
เป็นตัวชี้ไปchar
ด้วยระยะเวลาการจัดเก็บอัตโนมัติ (หมายถึงตัวแปรที่s0
มีอยู่เฉพาะสำหรับขอบเขตที่มีการประกาศ) และคัดลอกที่อยู่ของสตริงตัวอักษร ( 0x00008000
ในตัวอย่างนี้) ไปที่มัน หมายเหตุว่าตั้งแต่s0
ชี้ไปยังตัวอักษรสตริงมันไม่ควรถูกนำมาใช้เป็นอาร์กิวเมนต์ฟังก์ชั่นใด ๆ ที่จะพยายามที่จะปรับเปลี่ยนได้ (เช่น, strtok()
, strcat()
, strcpy()
ฯลฯ )
เส้น
char s1[] = "hello world";
กำหนดs1
เป็นอาร์เรย์ 12 องค์ประกอบของchar
(ความยาวถูกนำมาจากสตริงตัวอักษร) ด้วยระยะเวลาการจัดเก็บอัตโนมัติและคัดลอกเนื้อหาของตัวอักษรไปยังอาร์เรย์ ที่คุณสามารถดูจากแผนที่หน่วยความจำที่เรามีสองสำเนาของสตริง"hello world"
; s1
ความแตกต่างคือการที่คุณสามารถปรับเปลี่ยนสตริงที่มีอยู่ใน
s0
และs1
สามารถใช้แทนกันได้ในบริบทส่วนใหญ่ นี่คือข้อยกเว้น:
sizeof s0 == sizeof (char*)
sizeof s1 == 12
type of &s0 == char **
type of &s1 == char (*)[12] // pointer to a 12-element array of char
คุณสามารถกำหนดตัวแปรs0
ให้ชี้ไปที่สตริงตัวอักษรอื่นหรือตัวแปรอื่น คุณไม่สามารถกำหนดตัวแปรs1
ให้ชี้ไปยังอาร์เรย์อื่นได้
C99 N1256 ฉบับร่าง
มีการใช้ตัวอักษรสตริงของอักขระสองแบบ:
เริ่มต้นchar[]
:
char c[] = "abc";
นี่คือ "มายากลเพิ่มเติม" และอธิบายไว้ที่ 6.7.8 / 14 "การเริ่มต้น":
อาเรย์ของประเภทตัวละครอาจเริ่มต้นได้โดยสตริงตัวอักษรตัวเลือกล้อมรอบด้วยวงเล็บปีกกา อักขระต่อเนื่องของสตริงอักขระตามตัวอักษร (รวมถึงอักขระ null สิ้นสุดหากมีที่ว่างหรือถ้าอาร์เรย์มีขนาดไม่ทราบค่า) เริ่มต้นองค์ประกอบของอาร์เรย์
ดังนั้นนี่เป็นเพียงทางลัดสำหรับ:
char c[] = {'a', 'b', 'c', '\0'};
เช่นเดียวกับอาเรย์ทั่วไปอื่น ๆ ที่c
สามารถแก้ไขได้
ทุกที่อื่น: มันสร้าง:
ดังนั้นเมื่อคุณเขียน:
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
อย่างไรก็ตามโปรดทราบว่าสคริปต์ตัวเชื่อมโยงเริ่มต้นจะใส่.rodata
และ.text
อยู่ในส่วนเดียวกันซึ่งได้ดำเนินการแล้ว แต่ไม่มีสิทธิ์ในการเขียน สามารถสังเกตได้ด้วย:
readelf -l a.out
ซึ่งประกอบด้วย:
Section to Segment mapping:
Segment Sections...
02 .text .rodata
ถ้าเราทำเช่นเดียวกันสำหรับchar[]
:
char s[] = "abc";
เราได้รับ:
17: c7 45 f0 61 62 63 00 movl $0x636261,-0x10(%rbp)
ดังนั้นมันจึงถูกเก็บไว้ในสแต็ก (สัมพันธ์กับ%rbp
)
char s[] = "hello";
ประกาศว่าs
เป็นอาร์เรย์char
ที่มีความยาวพอที่จะเก็บ initializer (5 + 1 char
s) และเริ่มต้นอาร์เรย์โดยการคัดลอกสมาชิกของสตริงตัวอักษรที่กำหนดลงในอาร์เรย์
char *s = "hello";
ประกาศs
ที่จะเป็นตัวชี้ไปยังหนึ่งหรือมากกว่า (ในกรณีนี้มากขึ้น) char
และจุดโดยตรงที่คงที่ (อ่านอย่างเดียว) "hello"
สถานที่ที่มีอักษร
s
const char
char s[] = "Hello world";
นี่s
คืออาร์เรย์ของตัวละครซึ่งสามารถเขียนทับได้หากเราต้องการ
char *s = "hello";
สตริงตัวอักษรจะใช้ในการสร้างบล็อกตัวละครเหล่านี้บางแห่งในหน่วยความจำซึ่งตัวชี้นี้s
จะชี้ไปที่ เราสามารถกำหนดวัตถุที่ชี้ไปที่ใหม่ได้โดยเปลี่ยนสิ่งนั้น แต่ตราบใดที่มันชี้ไปที่สตริงตัวอักษรบล็อกของอักขระที่มันชี้ไม่สามารถเปลี่ยนแปลงได้
นอกจากนี้ให้พิจารณาว่าสำหรับวัตถุประสงค์แบบอ่านอย่างเดียวการใช้ทั้งสองอย่างนี้เหมือนกันคุณสามารถเข้าถึง char โดยการทำดัชนีด้วย[]
หรือจัด*(<var> + <index>)
รูปแบบ:
printf("%c", x[1]); //Prints r
และ:
printf("%c", *(x + 1)); //Prints r
เห็นได้ชัดว่าถ้าคุณพยายามที่จะทำ
*(x + 1) = 'a';
คุณอาจจะได้รับการแบ่งกลุ่มผิดพลาดในขณะที่คุณพยายามเข้าถึงหน่วยความจำแบบอ่านอย่างเดียว
x[1] = 'a';
segfault เช่นกัน (ขึ้นอยู่กับแพลตฟอร์มของหลักสูตร)
เพียงเพิ่ม: คุณยังได้รับค่าที่แตกต่างกันสำหรับขนาดของพวกเขา
printf("sizeof s[] = %zu\n", sizeof(s)); //6
printf("sizeof *s = %zu\n", sizeof(s)); //4 or 8
ดังกล่าวข้างต้นสำหรับอาร์เรย์'\0'
จะถูกจัดสรรเป็นองค์ประกอบสุดท้าย
char *str = "Hello";
ด้านบนตั้งค่า str to ชี้ไปที่ค่าตัวอักษร "Hello" ซึ่งเป็นรหัสตายตัวในอิมเมจไบนารีของโปรแกรมซึ่งถูกตั้งค่าสถานะเป็นหน่วยความจำแบบอ่านอย่างเดียวหมายความว่าการเปลี่ยนแปลงใด ๆ ในตัวอักษรสตริงนี้ผิดกฎหมาย
char str[] = "Hello";
คัดลอกสตริงไปยังหน่วยความจำที่จัดสรรใหม่บนสแต็ก ดังนั้นการเปลี่ยนแปลงใด ๆ ในนั้นจะได้รับอนุญาตและถูกกฎหมาย
means str[0] = 'M';
จะเปลี่ยน str เป็น "Mello"
สำหรับรายละเอียดเพิ่มเติมโปรดอ่านคำถามที่คล้ายกัน:
char *s1 = "Hello world"; // Points to fixed character string which is not allowed to modify
char s2[] = "Hello world"; // As good as fixed array of characters in string so allowed to modify
// s1[0] = 'J'; // Illegal
s2[0] = 'J'; // Legal
ในแง่ของความคิดเห็นที่นี่มันควรจะชัดเจนว่า: char * s = "hello"; เป็นความคิดที่ไม่ดีและควรใช้ในขอบเขตที่แคบมาก
นี่อาจเป็นโอกาสที่ดีที่จะชี้ให้เห็นว่า "ความถูกต้องของ const" เป็น "สิ่งที่ดี" คุณสามารถใช้คำสำคัญ "const" เพื่อปกป้องรหัสของคุณได้ทุกเมื่อและทุกเวลาจากผู้โทรหรือโปรแกรมเมอร์ที่ "ผ่อนคลาย" ซึ่งมักจะ "ผ่อนคลาย" ที่สุดเมื่อพอยน์เตอร์เริ่มเล่น
เรื่องประโลมโลกที่เพียงพอนี่คือสิ่งที่เราจะทำได้เมื่อ adorning pointers ด้วย "const" (หมายเหตุ: หนึ่งต้องอ่านการประกาศของตัวชี้จากขวาไปซ้าย) ต่อไปนี้เป็นวิธีที่แตกต่างกัน 3 วิธีในการป้องกันตัวเองเมื่อเล่นกับพอยน์เตอร์:
const DBJ* p means "p points to a DBJ that is const"
- นั่นคือวัตถุ DBJ ไม่สามารถเปลี่ยนผ่าน p
DBJ* const p means "p is a const pointer to a DBJ"
- นั่นคือคุณสามารถเปลี่ยนวัตถุ DBJ ผ่าน p แต่คุณไม่สามารถเปลี่ยนตัวชี้ p ได้
const DBJ* const p means "p is a const pointer to a const DBJ"
- นั่นคือคุณไม่สามารถเปลี่ยนตัวชี้ p และไม่สามารถเปลี่ยนวัตถุ DBJ ผ่าน p
ข้อผิดพลาดที่เกี่ยวข้องกับการกลายพันธุ์ของ const-ant พยายามที่จะถูกรวบรวมในเวลารวบรวม ไม่มีพื้นที่ว่างรันไทม์หรือปรับความเร็วสำหรับ const
(สมมติว่าคุณใช้คอมไพเลอร์ C ++ ใช่ไหม)
--DBJ