Tại sao C và C ++ hỗ trợ gán mảng thành viên trong cấu trúc, nhưng không phải nói chung?


87

Tôi hiểu rằng việc chỉ định mảng theo từng thành viên không được hỗ trợ, vì vậy những điều sau sẽ không hoạt động:

int num1[3] = {1,2,3};
int num2[3];
num2 = num1; // "error: invalid array assignment"

Tôi chỉ chấp nhận điều này là thực tế, nhận thấy rằng mục đích của ngôn ngữ là cung cấp một khung kết thúc mở và để người dùng quyết định cách triển khai một cái gì đó chẳng hạn như sao chép một mảng.

Tuy nhiên, những điều sau đây hoạt động:

struct myStruct { int num[3]; };
struct myStruct struct1 = {{1,2,3}};
struct myStruct struct2;
struct2 = struct1;

Mảng num[3]được gán thành viên khôn ngoan từ thể hiện của nó trong struct1, vào thể hiện của nó trong struct2.

Tại sao việc gán mảng theo thành viên được hỗ trợ cho cấu trúc, nhưng không phải nói chung?

chỉnh sửa : Nhận xét của Roger Pate trong chuỗi std :: string trong struct - Vấn đề sao chép / gán? dường như chỉ ra hướng chung của câu trả lời, nhưng tôi không biết đủ để xác nhận nó.

sửa 2 : Nhiều phản hồi xuất sắc. Tôi chọn Luther Blissett vì tôi chủ yếu thắc mắc về lý do triết học hoặc lịch sử đằng sau hành vi, nhưng việc tham khảo tài liệu kỹ thuật liên quan của James McNellis cũng rất hữu ích.


6
Tôi đang làm điều này có cả C và C ++ làm thẻ, bởi vì điều này bắt nguồn từ C. Ngoài ra, câu hỏi hay.
GManNickG

4
Có thể cần lưu ý rằng cách đây rất lâu trong C, việc gán cấu trúc nói chung là không thể thực hiện được và bạn phải sử dụng memcpy()hoặc tương tự.
ggg

Chỉ một chút FYI ... boost::array( boost.org/doc/libs/release/doc/html/array.html ) và bây giờ std::array( en.cppreference.com/w/cpp/container/array ) là các lựa chọn thay thế tương thích với STL cho các mảng C cũ lộn xộn. Họ hỗ trợ sao chép-gán.
Emile Cormier

@EmileCormier Và họ là - tada! - các cấu trúc xung quanh mảng.
Peter - Khôi phục Monica

Câu trả lời:


46

Đây là công việc của tôi:

Sự phát triển của ngôn ngữ C cung cấp một số thông tin chi tiết về sự phát triển của kiểu mảng trong C:

Tôi sẽ cố gắng phác thảo mảng:

Tiền thân của C là B và BCPL không có kiểu mảng riêng biệt, một khai báo như:

auto V[10] (B)
or 
let V = vec 10 (BCPL)

sẽ khai báo V là một con trỏ (không định kiểu) được khởi tạo để trỏ tới vùng không sử dụng gồm 10 "từ" của bộ nhớ. B đã được sử dụng *cho hội nghị con trỏ và có [] ký hiệu viết tay ngắn, *(V+i)có nghĩa là V[i], giống như trong C / C ++ ngày nay. Tuy nhiên, Vkhông phải là một mảng, nó vẫn là một con trỏ phải trỏ đến một số bộ nhớ. Điều này gây ra rắc rối khi Dennis Ritchie cố gắng mở rộng B với các loại cấu trúc. Anh ấy muốn các mảng trở thành một phần của cấu trúc, như trong C ngày nay:

struct {
    int inumber;
    char name[14];
};

Nhưng với khái niệm B, BCPL về mảng là con trỏ, điều này sẽ yêu cầu nametrường chứa một con trỏ phải được khởi tạo trong thời gian chạy tới vùng nhớ 14 byte trong cấu trúc. Vấn đề khởi tạo / bố trí cuối cùng đã được giải quyết bằng cách cung cấp cho mảng một cách xử lý đặc biệt: Trình biên dịch sẽ theo dõi vị trí của mảng trong cấu trúc, trên ngăn xếp, v.v. mà không thực sự yêu cầu con trỏ tới dữ liệu hiện thực hóa, ngoại trừ trong các biểu thức liên quan đến mảng. Việc xử lý này cho phép hầu hết tất cả mã B vẫn chạy và là nguồn của quy tắc "mảng chuyển đổi thành con trỏ nếu bạn nhìn vào chúng" . Đây là một bản hack khả năng tương thích, hóa ra rất tiện dụng, vì nó cho phép các mảng có kích thước mở, v.v.

Và đây là dự đoán của tôi tại sao không thể gán mảng: Vì mảng là con trỏ trong B, bạn chỉ cần viết:

auto V[10];
V=V+5;

để căn cứ lại một "mảng". Điều này bây giờ vô nghĩa, bởi vì cơ sở của một biến mảng không phải là một giá trị nữa. Vì vậy, việc gán này không được phép, điều này đã giúp bắt được một số chương trình làm điều này phục hồi trên các mảng đã khai báo. Và sau đó khái niệm này bị mắc kẹt: Vì các mảng không bao giờ được thiết kế để trở thành hạng nhất của hệ thống loại C, chúng hầu như được coi như những con thú đặc biệt sẽ trở thành con trỏ nếu bạn sử dụng chúng. Và từ một quan điểm nhất định (bỏ qua rằng mảng C là một sự tấn công giả mạo), việc không cho phép gán mảng vẫn có ý nghĩa: Một mảng mở hoặc một tham số hàm mảng được coi như một con trỏ không có thông tin về kích thước. Trình biên dịch không có thông tin để tạo phép gán mảng cho chúng và việc gán con trỏ là bắt buộc vì lý do tương thích.

/* Example how array assignment void make things even weirder in C/C++, 
   if we don't want to break existing code.
   It's actually better to leave things as they are...
*/
typedef int vec[3];

void f(vec a, vec b) 
{
    vec x,y; 
    a=b; // pointer assignment
    x=y; // NEW! element-wise assignment
    a=x; // pointer assignment
    x=a; // NEW! element-wise assignment
}

Điều này không thay đổi khi bản sửa đổi của C vào năm 1978 đã thêm phép gán cấu trúc ( http://cm.bell-labs.com/cm/cs/who/dmr/cchanges.pdf ). Mặc dù các bản ghi các kiểu riêng biệt trong C, không thể gán chúng vào đầu K&R C. Bạn phải sao chép chúng thành viên bằng memcpy và bạn chỉ có thể chuyển các con trỏ tới chúng dưới dạng tham số hàm. Phép gán (và truyền tham số) bây giờ được định nghĩa đơn giản là bản ghi nhớ của bộ nhớ thô của cấu trúc và vì điều này không thể phá vỡ mã exsisting nên nó đã được bổ sung một cách dễ dàng. Là một tác dụng phụ không mong muốn, điều này đã ngầm giới thiệu một số kiểu gán mảng, nhưng điều này đã xảy ra ở đâu đó bên trong một cấu trúc, vì vậy điều này thực sự không thể đưa ra các vấn đề với cách sử dụng mảng.


Thật tệ khi C đã không xác định cú pháp, ví dụ: int[10] c;để làm cho giá trị choạt động như một mảng mười mục, thay vì như một con trỏ đến mục đầu tiên của mảng mười mục. Có một số tình huống hữu ích để có thể tạo một typedef phân bổ không gian khi được sử dụng cho một biến, nhưng chuyển một con trỏ khi được sử dụng như một đối số của hàm, nhưng việc không thể có giá trị của kiểu mảng là một điểm yếu đáng kể về ngữ nghĩa bằng ngôn ngữ.
supercat

Thay vì nói "con trỏ phải trỏ đến một số bộ nhớ", điểm quan trọng là bản thân con trỏ phải được lưu trữ trong bộ nhớ như một con trỏ thông thường. Điều này xuất hiện trong phần giải thích sau của bạn, nhưng tôi nghĩ rằng điều đó làm nổi bật sự khác biệt chính tốt hơn. (Trong C hiện đại, tên của một biến mảng không đề cập đến một khối bộ nhớ, vì vậy đó không phải là sự khác biệt Đó chính là con trỏ tự nó không phải là một cách logic lưu trữ bất cứ nơi nào trong máy trừu tượng..)
Peter Cordes

Xem sự ác cảm của C đối với các mảng để biết tóm tắt lịch sử tốt đẹp.
Peter Cordes

31

Liên quan đến các toán tử gán, tiêu chuẩn C ++ cho biết như sau (C ++ 03 §5.17 / 1):

Có một số toán tử gán ... tất cả đều yêu cầu giá trị có thể sửa đổi làm toán hạng bên trái của chúng

Một mảng không phải là một giá trị có thể sửa đổi.

Tuy nhiên, việc gán cho một đối tượng kiểu lớp được định nghĩa đặc biệt (§5.17 / 4):

Việc gán cho các đối tượng của một lớp được định nghĩa bởi toán tử gán sao chép.

Vì vậy, chúng tôi xem xét toán tử gán bản sao được khai báo ngầm cho một lớp làm gì (§12.8 / 13):

Toán tử gán bản sao được định nghĩa ngầm cho lớp X thực hiện gán từng thành viên cho các subobject của nó. ... Mỗi subobject được gán theo cách phù hợp với kiểu của nó:
...
- nếu subobject là một mảng, mỗi phần tử được gán, theo cách thích hợp với loại phần tử
...

Vì vậy, đối với một đối tượng kiểu lớp, các mảng được sao chép chính xác. Lưu ý rằng nếu bạn cung cấp toán tử gán bản sao do người dùng khai báo, bạn không thể tận dụng điều này và bạn sẽ phải sao chép từng phần tử của mảng.


Lập luận tương tự trong C (C99 §6.5.16 / 2):

Toán tử gán phải có giá trị có thể modi làm toán hạng bên trái của nó.

Và §6.3.2.1 / 1:

Giá trị có thể modi là giá trị không có kiểu mảng ... [các ràng buộc khác tuân theo]

Trong C, việc gán đơn giản hơn nhiều so với trong C ++ (§6.5.16.1 / 2):

Trong phép gán đơn giản (=), giá trị của toán hạng bên phải được chuyển đổi thành kiểu của biểu thức gán và thay thế giá trị được lưu trữ trong đối tượng được chỉ định bởi toán hạng bên trái.

Để gán các đối tượng kiểu cấu trúc, các toán hạng bên trái và bên phải phải có cùng kiểu, vì vậy giá trị của toán hạng bên phải chỉ được sao chép vào toán hạng bên trái.


1
Tại sao mảng là bất biến? Hay đúng hơn, tại sao phép gán không được định nghĩa đặc biệt cho các mảng giống như khi nó ở kiểu lớp?
GManNickG

1
@GMan: Đó là câu hỏi thú vị hơn, phải không. Đối với C ++, câu trả lời có thể là "bởi vì đó là cách nó tồn tại trong C," và đối với C, tôi đoán đó chỉ là do cách ngôn ngữ phát triển (tức là lý do là lịch sử, không phải kỹ thuật), nhưng tôi đã không còn sống khi hầu hết điều đó diễn ra, vì vậy tôi sẽ để người khác hiểu rõ hơn trả lời phần đó :-P (FWIW, tôi không thể tìm thấy bất cứ điều gì trong tài liệu cơ sở C90 hoặc C99).
James McNellis

2
Có ai biết định nghĩa của "modifiable lvalue" ở đâu trong tiêu chuẩn C ++ 03 không? Nó phải ở §3.10. Chỉ mục cho biết nó được xác định trên trang đó, nhưng không phải. Lưu ý (không quy chuẩn) tại §8.3.4 / 5 cho biết "Không thể sửa đổi các đối tượng của kiểu mảng, xem 3.10," nhưng §3.10 không một lần sử dụng từ "mảng".
James McNellis

@James: Tôi cũng đang làm như vậy. Nó dường như đề cập đến một định nghĩa đã bị loại bỏ. Và vâng, tôi luôn muốn biết lý do thực sự đằng sau tất cả, nhưng nó có vẻ là một bí ẩn. Tôi đã nghe những điều như "ngăn mọi người làm việc kém hiệu quả bằng cách vô tình gán mảng", nhưng điều đó thật nực cười.
GManNickG

1
@GMan, James: Gần đây đã có một cuộc thảo luận trên comp.lang.c ++ groups.google.com/group/comp.lang.c++/browse_frm/thread/… nếu bạn đã bỏ qua và vẫn quan tâm. Rõ ràng nó không phải vì một mảng không phải là một giá trị trái sửa đổi (một mảng chắc chắn là một giá trị trái và tất cả lvalues không const là sửa đổi), nhưng vì =yêu cầu một rvalue trên RHS và một mảng không thể là một rvalue ! Việc chuyển đổi lvalue-to-rvalue bị cấm đối với mảng, được thay thế bằng lvalue-to-pointer. static_castkhông tốt hơn trong việc tạo ra một giá trị bởi vì nó được định nghĩa theo các thuật ngữ giống nhau.
Potatoswatter

2

Trong liên kết này: http://www2.research.att.com/~bs/bs_faq2.html có một phần về gán mảng:

Hai vấn đề cơ bản với mảng là

  • một mảng không biết kích thước của chính nó
  • tên của một mảng chuyển đổi thành một con trỏ đến phần tử đầu tiên của nó theo cách khiêu khích nhỏ nhất

Và tôi nghĩ đây là sự khác biệt cơ bản giữa mảng và cấu trúc. Biến mảng là một phần tử dữ liệu cấp thấp với kiến ​​thức bản thân hạn chế. Về cơ bản, nó là một đoạn bộ nhớ và một cách để lập chỉ mục vào nó.

Vì vậy, trình biên dịch không thể phân biệt giữa int a [10] và int b [20].

Tuy nhiên, các cấu trúc không có sự mơ hồ giống nhau.


3
Trang đó nói về việc truyền mảng cho các hàm (không thể thực hiện được, vì vậy nó chỉ là một con trỏ, đó là ý của anh ấy khi anh ấy nói rằng nó mất kích thước). Điều đó không liên quan gì đến việc gán mảng cho mảng. Và không, một biến mảng không chỉ "thực sự" là một con trỏ đến phần tử đầu tiên, nó là một mảng. Mảng không phải là con trỏ.
GManNickG

Cảm ơn bạn đã nhận xét, nhưng khi tôi đọc phần đó của bài báo, anh ấy nói trước rằng các mảng không biết kích thước của chính nó, sau đó sử dụng một ví dụ trong đó các mảng được truyền làm đối số để minh họa thực tế đó. Vì vậy, khi mảng được chuyển dưới dạng đối số, chúng có bị mất thông tin về kích thước của chúng hay không hoặc chúng không bao giờ có thông tin để bắt đầu. Tôi giả định cái sau.
Scott Turley

3
Trình biên dịch có thể cho biết sự khác biệt giữa hai mảng có kích thước khác nhau - hãy thử in sizeof(a)so với sizeof(b)hoặc chuyển atới void f(int (&)[20]);.
Georg Fritzsche

Điều quan trọng là phải hiểu rằng mỗi kích thước mảng tạo thành kiểu riêng của nó. Các quy tắc về truyền tham số đảm bảo rằng bạn có thể viết các hàm "chung chung" của người nghèo lấy các đối số mảng có kích thước bất kỳ, với chi phí cần phải truyền kích thước riêng biệt. Nếu không phải như vậy (và trong C ++ bạn có thể - và phải! - xác định các tham số tham chiếu cho các mảng có kích thước cụ thể), bạn sẽ cần một hàm cụ thể cho từng kích thước khác nhau, rõ ràng là vô nghĩa. Tôi đã viết về nó trong một bài đăng khác .
Peter - Khôi phục Monica

0

Tôi biết, tất cả những người đã trả lời đều là chuyên gia về C / C ++. Nhưng tôi nghĩ, đây là lý do chính.

num2 = num1;

Ở đây bạn đang cố gắng thay đổi địa chỉ cơ sở của mảng, địa chỉ này không được phép.

và tất nhiên, struct2 = struct1;

Ở đây, đối tượng struct1 được gán cho một đối tượng khác.


Và việc gán cấu trúc cuối cùng sẽ chỉ định thành viên mảng, đặt ra cùng một câu hỏi. Tại sao cái này được phép mà không phải cái kia, khi nó là một mảng trong cả hai trường hợp?
GManNickG

1
Đã đồng ý. Nhưng cái đầu tiên bị ngăn bởi trình biên dịch (num2 = num1). Cái thứ hai không bị ngăn bởi trình biên dịch. Điều đó tạo ra sự khác biệt rất lớn.
nsivakr

Nếu các mảng có thể được gán, num2 = num1sẽ hoạt động hoàn toàn tốt. Các phần tử của num2sẽ có cùng giá trị của phần tử tương ứng của num1.
juanchopanza

0

Một lý do khác không nỗ lực hơn nữa đã được thực hiện để tăng cường mảng trong C có lẽ là phân mảng sẽ không được hữu ích. Mặc dù nó có thể dễ dàng đạt được trong C bằng cách gói nó trong một cấu trúc (và địa chỉ của cấu trúc có thể được chuyển thành địa chỉ của mảng hoặc thậm chí là địa chỉ của phần tử đầu tiên của mảng để xử lý thêm) tính năng này hiếm khi được sử dụng. Một lý do là các mảng có kích thước khác nhau không tương thích, điều này làm hạn chế lợi ích của việc gán hoặc, liên quan, chuyển đến các hàm theo giá trị.

Hầu hết các hàm có tham số mảng trong ngôn ngữ mà mảng là kiểu hạng nhất được viết cho mảng có kích thước tùy ý. Sau đó, hàm thường lặp qua số phần tử đã cho, một thông tin mà mảng cung cấp. (Trong C, thành ngữ, tất nhiên, để truyền một con trỏ và một số phần tử riêng biệt.) Một hàm chấp nhận một mảng chỉ có một kích thước cụ thể là không cần thiết thường xuyên, vì vậy không cần nhiều. (Điều này thay đổi khi bạn có thể để nó cho trình biên dịch để tạo một hàm riêng biệt cho bất kỳ kích thước mảng nào đang xảy ra, như với các mẫu C ++; đây là lý do tại sao lại std::arrayhữu ích.)

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.