Tại sao tôi gặp lỗi phân đoạn khi viết vào một char char * sv được khởi tạo với một chuỗi ký tự, nhưng không phải là char char s []]?


286

Đoạn mã sau nhận lỗi seg trên dòng 2:

char *str = "string";
str[0] = 'z';  // could be also written as *str = 'z'
printf("%s\n", str);

Trong khi điều này hoạt động hoàn toàn tốt:

char str[] = "string";
str[0] = 'z';
printf("%s\n", str);

Đã thử nghiệm với MSVC và GCC.


1
Thật buồn cười - nhưng điều này thực sự biên dịch và chạy hoàn hảo khi sử dụng trình biên dịch windows (cl) trên dấu nhắc lệnh của nhà phát triển phòng thu trực quan. Làm tôi bối rối trong giây lát ...
David Refaeli

Câu trả lời:


240

Xem C FAQ, Câu hỏi 1.32

Q : sự khác biệt giữa các khởi tạo này là gì?
char a[] = "string literal";
char *p = "string literal";
Chương trình của tôi gặp sự cố nếu tôi cố gán giá trị mới p[i].

A : Một chuỗi ký tự (thuật ngữ chính thức cho một chuỗi trích dẫn kép trong nguồn C) có thể được sử dụng theo hai cách hơi khác nhau:

  1. Là trình khởi tạo cho một mảng char, như trong khai báo char a[], nó chỉ định các giá trị ban đầu của các ký tự trong mảng đó (và, nếu cần, kích thước của nó).
  2. Bất cứ nơi nào khác, nó biến thành một mảng ký tự không tên, tĩnh và mảng không tên này có thể được lưu trữ trong bộ nhớ chỉ đọc và do đó không nhất thiết phải được sửa đổi. Trong ngữ cảnh biểu thức, mảng được chuyển đổi cùng một lúc thành một con trỏ, như thường lệ (xem phần 6), do đó, khai báo thứ hai khởi tạo p để trỏ đến phần tử đầu tiên của mảng không tên.

Một số trình biên dịch có một công tắc kiểm soát xem các chuỗi ký tự có thể ghi được hay không (để biên dịch mã cũ) và một số trình biên dịch có thể có các tùy chọn để khiến các chuỗi ký tự được xử lý chính thức như các mảng const char (để bắt lỗi tốt hơn).


7
Một vài điểm khác: (1) segfault xảy ra như được mô tả, nhưng sự xuất hiện của nó là một chức năng của môi trường chạy; nếu cùng một mã trong một hệ thống nhúng, việc ghi có thể không có hiệu lực hoặc nó thực sự có thể thay đổi s thành z. (2) Vì chuỗi ký tự chuỗi không thể ghi, trình biên dịch có thể tiết kiệm không gian bằng cách đặt hai trường hợp "chuỗi" vào cùng một vị trí; hoặc, nếu một nơi nào khác trong mã bạn có "một chuỗi khác", thì một đoạn bộ nhớ có thể hỗ trợ cả hai nghĩa đen. Rõ ràng, nếu mã sau đó được phép thay đổi các byte đó, các lỗi lạ và khó có thể xảy ra.
greggo

1
@greggo: Điểm tốt. Ngoài ra còn có một cách để làm điều này trên các hệ thống có MMU bằng cách sử dụng mprotectđể bảo vệ chỉ đọc sóng (xem tại đây ).

Vì vậy, char * p = "blah" thực sự tạo ra một mảng tạm thời? Lạ.
rahul tyagi

1
Và sau 2 năm viết bằng C ++ ... TIL
zeboidlund

@rah Khoaagi có nghĩa là gì?
Suraj Jain

105

Thông thường, chuỗi ký tự được lưu trữ trong bộ nhớ chỉ đọc khi chương trình được chạy. Điều này là để ngăn bạn vô tình thay đổi một chuỗi hằng. Trong ví dụ đầu tiên của bạn, "string"được lưu trữ trong bộ nhớ chỉ đọc và *strtrỏ đến ký tự đầu tiên. Segfault xảy ra khi bạn cố gắng thay đổi ký tự đầu tiên thành 'z'.

Trong ví dụ thứ hai, chuỗi "string"được trình biên dịch sao chép từ trang chủ chỉ đọc của nó vào str[]mảng. Sau đó thay đổi ký tự đầu tiên được cho phép. Bạn có thể kiểm tra điều này bằng cách in địa chỉ của từng địa chỉ:

printf("%p", str);

Ngoài ra, in kích thước của strví dụ thứ hai sẽ cho bạn thấy rằng trình biên dịch đã phân bổ 7 byte cho nó:

printf("%d", sizeof(str));

13
Bất cứ khi nào sử dụng "% p" trên printf, bạn nên bỏ con trỏ thành void * như trong printf ("% p", (void *) str); Khi in size_t bằng printf, bạn nên sử dụng "% zu" nếu sử dụng tiêu chuẩn C mới nhất (C99).
Chris Young

4
Ngoài ra, dấu ngoặc đơn với sizeof chỉ cần thiết khi lấy kích thước của một loại (đối số sau đó trông giống như một biểu mẫu). Hãy nhớ rằng sizeof là một toán tử, không phải là một hàm.
thư giãn


34

Hầu hết các câu trả lời này đều đúng, nhưng chỉ cần thêm một chút rõ ràng hơn ...

"Bộ nhớ chỉ đọc" mà mọi người đang đề cập đến là đoạn văn bản theo thuật ngữ ASM. Đó là cùng một vị trí trong bộ nhớ nơi các hướng dẫn được tải. Đây là chỉ đọc vì lý do rõ ràng như bảo mật. Khi bạn tạo một char * được khởi tạo thành một chuỗi, dữ liệu chuỗi được biên dịch vào đoạn văn bản và chương trình sẽ khởi tạo con trỏ để trỏ vào đoạn văn bản. Vì vậy, nếu bạn cố gắng thay đổi nó, kaboom. Phân đoạn.

Khi được viết dưới dạng một mảng, trình biên dịch sẽ đặt dữ liệu chuỗi khởi tạo vào phân đoạn dữ liệu, đó là cùng một nơi với các biến toàn cục của bạn và như vậy trực tiếp. Bộ nhớ này có thể thay đổi, vì không có hướng dẫn trong phân đoạn dữ liệu. Lần này khi trình biên dịch khởi tạo mảng ký tự (vẫn chỉ là char *), nó sẽ trỏ vào phân đoạn dữ liệu thay vì phân đoạn văn bản mà bạn có thể thay đổi một cách an toàn trong thời gian chạy.


Nhưng không phải sự thật là có thể có các triển khai cho phép sửa đổi "bộ nhớ chỉ đọc" sao?
Pacerier

Khi được viết dưới dạng một mảng, trình biên dịch sẽ đặt dữ liệu chuỗi khởi tạo vào phân đoạn dữ liệu nếu chúng là tĩnh hoặc toàn cục. Mặt khác (ví dụ đối với một mảng tự động bình thường) nó đặt trên ngăn xếp, trong khung ngăn xếp của hàm chính. Chính xác?
SE

26

Tại sao tôi gặp lỗi phân đoạn khi ghi vào chuỗi?

Dự thảo C99 N1256

Có hai cách sử dụng khác nhau của chuỗi ký tự:

  1. Khởi tạo char[]:

    char c[] = "abc";      

    Đây là "nhiều phép thuật hơn" và được mô tả tại 6.7.8 / 14 "Khởi tạo":

    Một mảng các kiểu ký tự có thể được khởi tạo bởi một chuỗi ký tự bằng chữ, được tùy ý đặt trong dấu ngoặc nhọn. Các ký tự liên tiếp của chuỗi ký tự bằng chữ (bao gồm ký tự null kết thúc nếu có chỗ hoặc nếu mảng có kích thước không xác định) khởi tạo các phần tử của mảng.

    Vì vậy, đây chỉ là một phím tắt cho:

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

    Giống như bất kỳ mảng thông thường khác, ccó thể được sửa đổi.

  2. Ở mọi nơi khác: nó tạo ra một:

    Vì vậy, khi bạn viết:

    char *c = "abc";

    Điều này tương tự như:

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

    Lưu ý các diễn viên ngầm từ char[]đến char *, luôn luôn hợp pháp.

    Sau đó, nếu bạn sửa đổi c[0], bạn cũng sửa đổi __unnamed, đó là UB.

    Điều này được ghi lại ở 6.4.5 "Chuỗi ký tự":

    5 Trong giai đoạn dịch 7, một byte hoặc mã có giá trị 0 được thêm vào từng chuỗi ký tự đa dòng kết quả từ một chuỗi ký tự hoặc bằng chữ. Chuỗi ký tự đa bào sau đó được sử dụng để khởi tạo một mảng thời lượng và độ dài lưu trữ tĩnh vừa đủ để chứa chuỗi. Đối với các ký tự chuỗi ký tự, các thành phần mảng có kiểu char và được khởi tạo với các byte riêng lẻ của chuỗi ký tự đa dòng [...]

    6 Không xác định được liệu các mảng này có khác biệt hay không với điều kiện các phần tử của chúng có các giá trị phù hợp. Nếu chương trình cố gắng sửa đổi một mảng như vậy, hành vi không được xác định.

6.7.8 / 32 "Khởi tạo" đưa ra một ví dụ trực tiếp:

VÍ DỤ 8: Tuyên bố

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

định nghĩa các đối tượng mảng char "đơn giản" stcó các phần tử được khởi tạo bằng chuỗi ký tự.

Tuyên bố này là giống hệt với

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

Nội dung của các mảng có thể sửa đổi. Mặt khác, tuyên bố

char *p = "abc";

định nghĩa pvới kiểu "con trỏ tới char" và khởi tạo nó để trỏ đến một đối tượng có kiểu "mảng char" có độ dài 4 có các phần tử được khởi tạo với một chuỗi ký tự bằng chữ. Nếu một nỗ lực được thực hiện để sử dụng pđể sửa đổi nội dung của mảng, hành vi không được xác định.

Triển khai ELF GCC 4.8 x86-64

Chương trình:

#include <stdio.h>

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

Biên dịch và dịch ngược:

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

Đầu ra chứa:

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

Kết luận: GCC lưu trữ char*nó trong .rodataphần, không phải trong .text.

Nếu chúng ta làm tương tự cho char[]:

 char s[] = "abc";

chúng tôi đạt được:

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

vì vậy nó được lưu trữ trong ngăn xếp (liên quan đến %rbp).

Tuy nhiên, lưu ý rằng tập lệnh liên kết mặc định đặt .rodata.texttrong cùng một phân đoạn, đã thực thi nhưng không có quyền ghi. Điều này có thể được quan sát với:

readelf -l a.out

trong đó có:

 Section to Segment mapping:
  Segment Sections...
   02     .text .rodata

17

Trong mã đầu tiên, "chuỗi" là hằng số chuỗi và hằng số chuỗi không bao giờ được sửa đổi vì chúng thường được đặt vào bộ nhớ chỉ đọc. "str" ​​là một con trỏ đang được sử dụng để sửa đổi hằng số.

Trong mã thứ hai, "chuỗi" là một trình khởi tạo mảng, sắp xếp ngắn gọn cho

char str[7] =  { 's', 't', 'r', 'i', 'n', 'g', '\0' };

"str" ​​là một mảng được phân bổ trên ngăn xếp và có thể được sửa đổi tự do.


1
Trên ngăn xếp, hoặc phân đoạn dữ liệu nếu strlà toàn cầu hoặc static.
Gauthier

12

Bởi vì kiểu "whatever"trong ngữ cảnh của ví dụ thứ nhất là const char *(ngay cả khi bạn gán nó cho một char không phải là const *), điều đó có nghĩa là bạn không nên thử và viết cho nó.

Trình biên dịch đã thực thi điều này bằng cách đặt chuỗi vào một phần chỉ đọc của bộ nhớ, do đó việc ghi vào nó sẽ tạo ra một segfault.


8

Để hiểu lỗi hoặc vấn đề này, trước tiên bạn nên biết sự khác biệt b / w con trỏ và mảng, vì vậy trước tiên tôi đã giải thích cho bạn sự khác biệt b / w chúng

mảng chuỗi

 char strarray[] = "hello";

Trong mảng bộ nhớ được lưu trữ trong các ô nhớ liên tục, được lưu trữ dưới dạng [h][e][l][l][o][\0] =>[]ô nhớ có kích thước 1 char và các ô nhớ liên tục này có thể được truy cập bằng tên có tên strarray tại đây. Ở đây, chính chuỗi strarraychứa tất cả các ký tự của chuỗi được khởi tạo cho nó. trường hợp ở đây "hello" để chúng ta có thể dễ dàng thay đổi nội dung bộ nhớ của nó bằng cách truy cập từng ký tự theo giá trị chỉ mục của nó

`strarray[0]='m'` it access character at index 0 which is 'h'in strarray

và giá trị của nó thay đổi thành 'm'giá trị strarray thay đổi thành "mello";

Một điểm cần lưu ý ở đây là chúng ta có thể thay đổi nội dung của mảng chuỗi bằng cách thay đổi ký tự theo từng ký tự nhưng không thể khởi tạo chuỗi khác trực tiếp đến chuỗi như thế strarray="new string"là không hợp lệ

Con trỏ

Như chúng ta đều biết con trỏ trỏ đến vị trí bộ nhớ trong bộ nhớ, con trỏ chưa được khởi tạo chỉ đến vị trí bộ nhớ ngẫu nhiên và sau khi khởi tạo trỏ đến vị trí bộ nhớ cụ thể

char *ptr = "hello";

ở đây con trỏ ptr được khởi tạo thành chuỗi "hello"là chuỗi không đổi được lưu trữ trong bộ nhớ chỉ đọc (ROM) vì vậy "hello"không thể thay đổi vì nó được lưu trữ trong ROM

và ptr được lưu trữ trong phần ngăn xếp và trỏ đến chuỗi không đổi "hello"

vì vậy ptr [0] = 'm' không hợp lệ vì bạn không thể truy cập bộ nhớ chỉ đọc

Nhưng ptr có thể được khởi tạo trực tiếp đến giá trị chuỗi khác vì nó chỉ là con trỏ để nó có thể được trỏ đến bất kỳ địa chỉ bộ nhớ biến nào của kiểu dữ liệu của nó

ptr="new string"; is valid

7
char *str = "string";  

Các tập hợp ở trên strđể chỉ đến giá trị bằng chữ "string"được mã hóa cứng trong hình ảnh nhị phân của chương trình, có thể được gắn cờ là chỉ đọc trong bộ nhớ.

Vì vậy, str[0]=đang cố gắng ghi vào mã chỉ đọc của ứng dụng. Tôi đoán đây có lẽ là phụ thuộc vào trình biên dịch.


6
char *str = "string";

cấp phát một con trỏ cho một chuỗi ký tự mà trình biên dịch đang đặt vào một phần không thể sửa đổi trong tệp thực thi của bạn;

char str[] = "string";

cấp phát và khởi tạo một mảng cục bộ có thể sửa đổi


chúng ta có thể viết int *b = {1,2,3) như chúng ta viết char *s = "HelloWorld"không?
Suraj Jain

6

Câu hỏi thường gặp về C mà @matli liên kết với đề cập đến nó, nhưng chưa có ai khác ở đây, vì vậy để làm rõ: nếu một chuỗi ký tự (chuỗi trích dẫn kép trong nguồn của bạn) được sử dụng ở bất kỳ đâu ngoài việc khởi tạo một mảng ký tự (ví dụ: @ Ví dụ thứ hai của Mark, hoạt động chính xác), chuỗi đó được trình biên dịch lưu trữ trong bảng chuỗi tĩnh đặc biệt , gần giống với việc tạo biến tĩnh toàn cục (tất nhiên chỉ đọc) về cơ bản là ẩn danh (không có tên "biến" "). Phần chỉ đọc là phần quan trọng và là lý do tại sao các ví dụ mã đầu tiên của @ Mark là segfaults.


chúng ta có thể viết int *b = {1,2,3) như chúng ta viết char *s = "HelloWorld"không?
Suraj Jain

4

Các

 char *str = "string";

dòng xác định một con trỏ và trỏ nó vào một chuỗi bằng chữ. Chuỗi ký tự không thể ghi được vì vậy khi bạn thực hiện:

  str[0] = 'z';

bạn nhận được một lỗi seg. Trên một số nền tảng, nghĩa đen có thể nằm trong bộ nhớ có thể ghi nên bạn sẽ không thấy segfault, nhưng đó là mã không hợp lệ (dẫn đến hành vi không xác định) bất kể.

Dòng:

char str[] = "string";

phân bổ một mảng các ký tự và sao chép chuỗi ký tự vào mảng đó, có thể ghi hoàn toàn, vì vậy bản cập nhật tiếp theo không có vấn đề gì.


chúng ta có thể viết int *b = {1,2,3) như chúng ta viết char *s = "HelloWorld"không?
Suraj Jain

3

Các chuỗi ký tự như "chuỗi" có thể được phân bổ trong không gian địa chỉ của tệp thực thi của bạn dưới dạng dữ liệu chỉ đọc (cung cấp hoặc lấy trình biên dịch của bạn). Khi bạn chạm vào nó, nó sẽ phát hiện ra rằng bạn đang ở trong khu vực đồ tắm và cho bạn biết với một lỗi seg.

Trong ví dụ đầu tiên của bạn, bạn đang nhận được một con trỏ tới dữ liệu const đó. Trong ví dụ thứ hai của bạn, bạn đang khởi tạo một mảng gồm 7 ký tự với một bản sao của dữ liệu const.


2
// create a string constant like this - will be read only
char *str_p;
str_p = "String constant";

// create an array of characters like this 
char *arr_p;
char arr[] = "String in an array";
arr_p = &arr[0];

// now we try to change a character in the array first, this will work
*arr_p = 'E';

// lets try to change the first character of the string contant
*str_p = 'G'; // this will result in a segmentation fault. Comment it out to work.


/*-----------------------------------------------------------------------------
 *  String constants can't be modified. A segmentation fault is the result,
 *  because most operating systems will not allow a write
 *  operation on read only memory.
 *-----------------------------------------------------------------------------*/

//print both strings to see if they have changed
printf("%s\n", str_p); //print the string without a variable
printf("%s\n", arr_p); //print the string, which is in an array. 

1

Ở nơi đầu tiên, strlà một con trỏ trỏ vào "string". Trình biên dịch được phép đặt chuỗi ký tự vào các vị trí trong bộ nhớ mà bạn không thể ghi vào mà chỉ có thể đọc. (Điều này thực sự đã kích hoạt cảnh báo, vì bạn đang gán const char *cho a char *. Bạn đã tắt cảnh báo chưa, hoặc bạn đã bỏ qua chúng?)

Ở vị trí thứ hai, bạn đang tạo một mảng, đó là bộ nhớ mà bạn có toàn quyền truy cập và khởi tạo nó với "string". Bạn đang tạo char[7](sáu cho các chữ cái, một cho chấm dứt '\ 0') và bạn làm bất cứ điều gì bạn muốn với nó.


@Ferruccio ,? Có consttiền tố tạo các biến Chỉ đọc
EsmaeelE

Trong C chuỗi chữ có loại char [N], không const char [N], vì vậy không có cảnh báo. (Bạn có thể thay đổi điều đó trong gcc ít nhất bằng cách vượt qua -Wwrite-strings.)
melpomene

0

Giả sử các chuỗi là,

char a[] = "string literal copied to stack";
char *p  = "string literal referenced by p";

Trong trường hợp đầu tiên, nghĩa đen sẽ được sao chép khi 'a' đi vào phạm vi. Ở đây 'a' là một mảng được xác định trên stack. Điều đó có nghĩa là chuỗi sẽ được tạo trên ngăn xếp và dữ liệu của nó được sao chép từ bộ nhớ mã (văn bản), thường chỉ đọc (đây là cách triển khai cụ thể, trình biên dịch có thể đặt dữ liệu chương trình chỉ đọc này vào bộ nhớ có thể ghi ).

Trong trường hợp thứ hai, p là một con trỏ được xác định trên stack (phạm vi cục bộ) và tham chiếu một chuỗi ký tự (dữ liệu chương trình hoặc văn bản) được lưu trữ ở nơi khác. Thông thường sửa đổi bộ nhớ như vậy là không thực hành tốt cũng không được khuyến khích.


-1

Đầu tiên là một chuỗi không đổi không thể sửa đổi. Thứ hai là một mảng với giá trị khởi tạo, vì vậy nó có thể được sửa đổi.


-2

Lỗi phân đoạn xảy ra khi bạn cố truy cập vào bộ nhớ không thể truy cập được.

char *str là một con trỏ tới một chuỗi không thể thay đổi (lý do để nhận segfault).

trong khi đó char str[]là một mảng và có thể sửa đổi ..

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.