Sự khác biệt giữa char s [] và char * s là gì?


506

Trong C, người ta có thể sử dụng một chuỗi ký tự trong một khai báo như thế này:

char s[] = "hello";

hoặc như thế này:

char *s = "hello";

Vì vậy, sự khác biệt là gì? Tôi muốn biết những gì thực sự xảy ra về thời gian lưu trữ, cả về thời gian biên dịch và thời gian chạy.



8
char * s = "hello", ở đây s có thể trỏ bất kỳ chuỗi nào khác vào thời gian chạy. Ý tôi là nó không phải là con trỏ không đổi, bạn có thể gán giá trị khác trong thời gian chạy p = "Nishant", trong khi s [] ở đây là con trỏ không đổi .. .. không thể sắp xếp lại một chuỗi khác nhưng chúng ta có thể gán giá trị ký tự khác tại s [index].
Nishant Kumar

Câu trả lời:


541

Sự khác biệt ở đây là

char *s = "Hello world";

sẽ đặt "Hello world"vào các phần chỉ đọc của bộ nhớ và tạo smột con trỏ tới đó làm cho bất kỳ thao tác ghi nào trên bộ nhớ này là bất hợp pháp.

Trong khi làm:

char s[] = "Hello world";

đặt chuỗi ký tự trong bộ nhớ chỉ đọc và sao chép chuỗi vào bộ nhớ mới được phân bổ trên ngăn xếp. Do đó làm

s[0] = 'J';

hợp pháp.


22
Chuỗi ký tự "Hello world"nằm trong "các phần chỉ đọc của bộ nhớ" trong cả hai ví dụ. Ví dụ với các điểm mảng ở đó, ví dụ với mảng sao chép các ký tự vào các thành phần mảng.
pmg

28
pmg: Trong trường hợp thứ hai, chuỗi ký tự không nhất thiết tồn tại trong bộ nhớ như một đối tượng liền kề duy nhất - nó chỉ là một trình khởi tạo, trình biên dịch hoàn toàn có thể phát ra một loạt các lệnh "tải byte ngay lập tức" có chứa các giá trị ký tự được nhúng trong họ
phê

10
Ví dụ mảng char không nhất thiết phải đặt chuỗi trên ngăn xếp - nếu nó xuất hiện ở cấp tệp, nó có thể sẽ nằm trong một loại phân đoạn dữ liệu được khởi tạo thay thế.
phê

9
Tôi muốn chỉ ra rằng char s = "xx" không được trong bộ nhớ chỉ đọc (một số hiện thực không có MMUs, ví dụ). Bản nháp n1362 c1x chỉ đơn giản nói rằng sửa đổi một mảng như vậy gây ra hành vi không xác định. Nhưng dù sao +1, vì dựa vào hành vi đó là một điều ngớ ngẩn.
paxdiablo

3
Tôi nhận được một biên dịch sạch trên một tệp chỉ chứa char msg[] = "hello, world!"; chuỗi kết thúc trong phần dữ liệu khởi tạo. Khi được tuyên bố char * constkết thúc trong phần dữ liệu chỉ đọc. gcc-4.5.3
gcbenison

152

Trước hết, trong các đối số chức năng, chúng hoàn toàn tương đương:

void foo(char *x);
void foo(char x[]); // exactly the same in all respects

Trong các bối cảnh khác, char *phân bổ một con trỏ, trong khi char []phân bổ một mảng. Trường hợp chuỗi đi đâu trong trường hợp trước, bạn yêu cầu? Trình biên dịch bí mật phân bổ một mảng ẩn danh tĩnh để giữ chuỗi ký tự. Vì thế:

char *x = "Foo";
// is approximately equivalent to:
static const char __secret_anonymous_array[] = "Foo";
char *x = (char *) __secret_anonymous_array;

Lưu ý rằng bạn không được cố gắng sửa đổi nội dung của mảng ẩn danh này thông qua con trỏ này; các hiệu ứng không được xác định (thường có nghĩa là một sự cố):

x[1] = 'O'; // BAD. DON'T DO THIS.

Sử dụng cú pháp mảng trực tiếp phân bổ nó vào bộ nhớ mới. Do đó sửa đổi là an toàn:

char x[] = "Foo";
x[1] = 'O'; // No problem.

Tuy nhiên, mảng chỉ tồn tại miễn là phạm vi liên quan của nó, vì vậy nếu bạn thực hiện điều này trong một hàm, đừng trả về hoặc rò rỉ một con trỏ tới mảng này - tạo một bản sao thay thế bằng strdup()hoặc tương tự. Nếu mảng được phân bổ trong phạm vi toàn cầu, tất nhiên, không có vấn đề.


72

Tuyên bố này:

char s[] = "hello";

Tạo một đối tượng - một charmảng có kích thước 6, được gọi s, khởi tạo với các giá trị 'h', 'e', 'l', 'l', 'o', '\0'. Mảng này được phân bổ trong bộ nhớ và thời gian tồn tại trong bao lâu, tùy thuộc vào nơi khai báo xuất hiện. Nếu khai báo nằm trong một hàm, nó sẽ tồn tại cho đến khi kết thúc khối mà nó được khai báo và gần như chắc chắn được phân bổ trên ngăn xếp; nếu nó nằm ngoài một hàm, nó có thể sẽ được lưu trữ trong một "phân đoạn dữ liệu được khởi tạo" được tải từ tệp thực thi vào bộ nhớ có thể ghi khi chương trình được chạy.

Mặt khác, tuyên bố này:

char *s ="hello";

Tạo hai đối tượng:

  • một mảng chỉ đọc gồm 6 chargiây chứa các giá trị 'h', 'e', 'l', 'l', 'o', '\0', không có tên và có thời lượng lưu trữ tĩnh (có nghĩa là nó tồn tại trong toàn bộ vòng đời của chương trình); và
  • một biến kiểu con trỏ-char, được gọi s, được khởi tạo với vị trí của ký tự đầu tiên trong mảng chỉ đọc, không tên.

Mảng chỉ đọc không tên thường nằm trong phân đoạn "văn bản" của chương trình, có nghĩa là nó được tải từ đĩa vào bộ nhớ chỉ đọc, cùng với chính mã. Vị trí của sbiến con trỏ trong bộ nhớ phụ thuộc vào nơi khai báo xuất hiện (giống như trong ví dụ đầu tiên).


1
Trong cả hai khai báo cho bộ nhớ "xin chào" được phân bổ vào thời gian của học sinh ?. Và một điều nữa char * p = "xin chào" ở đây "xin chào" được lưu trữ trong đoạn văn bản như bạn đã nêu trong câu trả lời của mình ... và còn về char [] = "xin chào" nó cũng sẽ lưu trữ đầu tiên trong phần phân đoạn văn bản và trong thời gian chạy, nó sẽ sao chép trong ngăn xếp như Rickard đã nêu trong câu trả lời. hãy làm rõ điểm này
Nishant Kumar

2
@Nishant: Trong char s[] = "hello"trường hợp, "hello"chỉ là một trình khởi tạo cho trình biên dịch biết cách khởi tạo mảng. Nó có thể hoặc không thể dẫn đến một chuỗi tương ứng trong phân đoạn văn bản - ví dụ: nếu scó thời lượng lưu trữ tĩnh thì có khả năng là trường hợp duy nhất "hello"sẽ nằm trong phân đoạn dữ liệu được khởi tạo - schính đối tượng . Ngay cả khi scó thời lượng lưu trữ tự động, nó có thể được khởi tạo bởi một chuỗi các cửa hàng theo nghĩa đen chứ không phải là một bản sao (ví dụ. movl $1819043176, -6(%ebp); movw $111, -2(%ebp)).
phê

Chính xác hơn, GCC 4.8 đưa nó vào .rodata, tập lệnh liên kết sau đó chuyển vào cùng phân khúc với .text. Xem câu trả lời của tôi .
Ciro Santilli 郝海东 冠状 病 事件

@caf Trong câu trả lời đầu tiên của Rickard, Nó được viết để char s[] = "Hello world";đặt chuỗi ký tự trong bộ nhớ chỉ đọc và sao chép chuỗi vào bộ nhớ mới được phân bổ trên ngăn xếp. Nhưng, câu trả lời của bạn chỉ nói về chuỗi ký tự được đặt trong bộ nhớ chỉ đọc và bỏ qua phần thứ hai của câu có nội dung : copies the string to newly allocated memory on the stack. Vì vậy, là câu trả lời của bạn không đầy đủ cho việc không chỉ định phần thứ hai?
KPMG

1
@AjaySinghNegi: Như tôi đã nêu trong các bình luận khác (với câu trả lời này và câu trả lời của Rickard), chuỗi trong char s[] = "Hellow world";chỉ là một trình khởi tạo và không nhất thiết phải được lưu trữ dưới dạng bản sao chỉ đọc riêng biệt. Nếu scó thời lượng lưu trữ tĩnh thì bản sao duy nhất của chuỗi có khả năng nằm trong phân đoạn đọc-ghi tại vị trí svà ngay cả khi không thì trình biên dịch có thể chọn khởi tạo mảng bằng các lệnh tải ngay lập tức hoặc tương tự thay vì sao chép từ một chuỗi chỉ đọc. Vấn đề là trong trường hợp này, chính chuỗi khởi tạo không có sự hiện diện của thời gian chạy.
phê

60

Đưa ra các tuyên bố

char *s0 = "hello world";
char s1[] = "hello world";

giả sử bản đồ bộ nhớ giả thuyết sau:

                    0x01 0x02 0x03 0x04
        0x00008000: 'h' 'e' 'l' 'l'
        0x00008004: 'o' '' 'w' 'o'
        0x00008008: 'r' 'l' 'd' 0x00
        ...
Sđd: 0x00010000: 0x00 0x00 0x80 0x00
s1: 0x00010004: 'h' 'e' 'l' 'l'
        0x00010008: 'o' '' 'w' 'o'
        0x0001000C: 'r' 'l' 'd' 0x00

Chuỗi ký tự "hello world"là một mảng gồm 12 phần tử char( const chartrong C ++) với thời lượng lưu trữ tĩnh, nghĩa là bộ nhớ cho nó được cấp phát khi chương trình khởi động và duy trì cho đến khi chương trình kết thúc. Cố gắng sửa đổi nội dung của một chuỗi ký tự gọi các hành vi không xác định.

Dòng

char *s0 = "hello world";

định nghĩa s0là một con trỏ charvới thời lượng lưu trữ tự động (có nghĩa là biến s0chỉ tồn tại cho phạm vi được khai báo) và sao chép địa chỉ của chuỗi ký tự ( 0x00008000trong ví dụ này) vào nó. Lưu ý rằng kể từ khi s0điểm đến một chữ chuỗi, nó không nên được sử dụng như một cuộc tranh cãi với bất kỳ chức năng mà sẽ cố gắng sửa đổi nó (ví dụ, strtok(), strcat(), strcpy(), vv).

Dòng

char s1[] = "hello world";

định nghĩa s1là mảng 12 phần tử của char(độ dài được lấy từ chuỗi ký tự) với thời lượng lưu trữ tự động và sao chép nội dung của chữ sang mảng. Như bạn có thể thấy từ bản đồ bộ nhớ, chúng tôi có hai bản sao của chuỗi "hello world"; sự khác biệt là bạn có thể sửa đổi chuỗi chứa trong s1.

s0s1có thể hoán đổi cho nhau trong hầu hết các bối cảnh; đây là trường hợp ngoại lệ:

sizeof s0 == sizeof (char*)
sizeof s1 == 12

type of &s0 == char **
type of &s1 == char (*)[12] // pointer to a 12-element array of char

Bạn có thể gán lại biến s0để trỏ đến một chuỗi ký tự khác hoặc đến một biến khác. Bạn không thể gán lại biến s1để trỏ đến một mảng khác.


2
Tôi nghĩ rằng bản đồ bộ nhớ giả thuyết làm cho nó dễ hiểu!
midnightBlue

32

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.

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

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).


15
char s[] = "hello";

tuyên bố slà một mảng trong charđó đủ dài để giữ bộ khởi tạo (5 + 1 chargiây) và khởi tạo mảng bằng cách sao chép các thành viên của chuỗi ký tự đã cho vào mảng.

char *s = "hello";

tuyên bố slà một con trỏ tới một hoặc nhiều (trong trường hợp này là nhiều hơn) charvà trỏ nó trực tiếp vào một vị trí cố định (chỉ đọc) có chứa chữ "hello".


1
Phương pháp nào thích hợp hơn để sử dụng trong các hàm nếu s sẽ không bị thay đổi, f (const char s []) hoặc f (const char * s)?
psihodelia

1
@psihodelia: Trong một khai báo hàm không có sự khác biệt. Trong cả hai trường hợp slà một con trỏ đến const char.
CB Bailey

4
char s[] = "Hello world";

Ở đây, slà một loạt các ký tự, có thể được ghi đè nếu chúng ta muốn.

char *s = "hello";

Một chuỗi ký tự được sử dụng để tạo các khối ký tự này ở đâu đó trong bộ nhớ mà con trỏ snày đang trỏ tới. Ở đây chúng ta có thể gán lại đối tượng mà nó đang trỏ tới bằng cách thay đổi điều đó, nhưng miễn là nó trỏ đến một chuỗi theo nghĩa đen thì khối ký tự mà nó trỏ tới không thể thay đổi.


@bo Persson Tại sao khối nhân vật không thể thay đổi trong trường hợp thứ hai?
Pankaj Mahato

3

Ngoài ra, hãy xem xét rằng, vì mục đích chỉ đọc, việc sử dụng cả hai là giống hệt nhau, bạn có thể truy cập một char bằng cách lập chỉ mục với []hoặc *(<var> + <index>) định dạng:

printf("%c", x[1]);     //Prints r

Và:

printf("%c", *(x + 1)); //Prints r

Rõ ràng, nếu bạn cố gắng làm

*(x + 1) = 'a';

Bạn có thể sẽ nhận được Phân đoạn lỗi, vì bạn đang cố truy cập vào bộ nhớ chỉ đọc.


Điều này là không có cách nào khác với x[1] = 'a';nó sẽ segfault là tốt (tất nhiên phụ thuộc vào nền tảng).
glglgl

3

Chỉ cần thêm: bạn cũng nhận được các giá trị khác nhau cho kích thước của chúng.

printf("sizeof s[] = %zu\n", sizeof(s));  //6
printf("sizeof *s  = %zu\n", sizeof(s));  //4 or 8

Như đã đề cập ở trên, cho một mảng '\0'sẽ được phân bổ là phần tử cuối cùng.


2
char *str = "Hello";

Các tập hợp ở trên str để trỏ đến giá trị bằng chữ "Xin chào" được mã hóa cứng trong hình ảnh nhị phân của chương trình, được gắn cờ là chỉ đọc trong bộ nhớ, có nghĩa là bất kỳ thay đổi nào trong chuỗi ký tự này là bất hợp pháp và điều đó sẽ gây ra lỗi phân đoạn.

char str[] = "Hello";

sao chép chuỗi vào bộ nhớ mới được phân bổ trên ngăn xếp. Do đó, thực hiện bất kỳ thay đổi trong nó được cho phép và hợp pháp.

means str[0] = 'M';

sẽ thay đổi str thành "Mello".

Để biết thêm chi tiết, xin vui lòng đi qua câu hỏi tương tự:

Tại sao tôi gặp lỗi phân đoạn khi ghi vào chuỗi được khởi tạo bằng "char * s" mà không phải là "char s []"?


0

Trong trường hợp:

char *x = "fred";

x là một giá trị - nó có thể được gán cho. Nhưng trong trường hợp:

char x[] = "fred";

x không phải là một giá trị, nó là một giá trị - bạn không thể gán cho nó.


3
Về mặt kỹ thuật, xlà một giá trị không thể sửa đổi. Trong hầu hết tất cả các bối cảnh, nó sẽ đánh giá một con trỏ tới phần tử đầu tiên của nó và giá trị đó là một giá trị.
phê

0
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

-1

Trong ánh sáng của các bình luận ở đây, rõ ràng là: char * s = "xin chào"; Là một ý tưởng tồi, và nên được sử dụng trong phạm vi rất hẹp.

Đây có thể là một cơ hội tốt để chỉ ra rằng "const đúng" là một "điều tốt". Bất cứ khi nào và bất cứ nơi nào bạn có thể, hãy sử dụng từ khóa "const" để bảo vệ mã của bạn, khỏi những người gọi hoặc lập trình viên "thoải mái", thường "thoải mái" nhất khi con trỏ phát huy tác dụng.

Đủ melodrama, đây là những gì người ta có thể đạt được khi tô điểm con trỏ bằng "const". (Lưu ý: Người ta phải đọc các khai báo con trỏ từ phải sang trái.) Dưới đây là 3 cách khác nhau để bảo vệ chính bạn khi chơi với con trỏ:

const DBJ* p means "p points to a DBJ that is const" 

- đó là, đối tượng DBJ không thể thay đổi thông qua p.

DBJ* const p means "p is a const pointer to a DBJ" 

- nghĩa là, bạn có thể thay đổi đối tượng DBJ thông qua p, nhưng bạn không thể thay đổi chính con trỏ p.

const DBJ* const p means "p is a const pointer to a const DBJ" 

- nghĩa là, bạn không thể tự thay đổi con trỏ p, cũng như không thể thay đổi đối tượng DBJ thông qua p.

Các lỗi liên quan đến đột biến const-ant đã cố gắng được bắt gặp tại thời điểm biên dịch. Không có không gian thời gian chạy hoặc hình phạt tốc độ cho const.

(Tất nhiên là bạn đang sử dụng trình biên dịch C ++?)

--DBJ


Điều này hoàn toàn chính xác, nhưng nó không liên quan gì đến câu hỏi. Và theo như giả định của bạn về trình biên dịch C ++, câu hỏi được gắn thẻ là C, không phải là C ++.
Fabio nói Phục hồi Monica

Không có gì xấu về char * s = "const chuỗi";
Paul Smith
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.