Tại sao toán tử mũi tên (->) trong C tồn tại?


264

.Toán tử dot ( ) được sử dụng để truy cập một thành viên của struct, trong khi toán tử mũi tên ( ->) trong C được sử dụng để truy cập một thành viên của struct được tham chiếu bởi con trỏ trong câu hỏi.

Bản thân con trỏ không có bất kỳ thành viên nào có thể được truy cập bằng toán tử dấu chấm (thực tế nó chỉ là một số mô tả một vị trí trong bộ nhớ ảo nên nó không có bất kỳ thành viên nào). Vì vậy, sẽ không có sự mơ hồ nếu chúng ta chỉ định nghĩa toán tử dấu chấm để tự động hủy bỏ con trỏ nếu nó được sử dụng trên một con trỏ (một thông tin được biết đến bởi trình biên dịch tại thời gian biên dịch afaik).

Vậy tại sao những người sáng tạo ngôn ngữ quyết định làm cho mọi thứ phức tạp hơn bằng cách thêm toán tử dường như không cần thiết này? Quyết định thiết kế lớn là gì?


1
Liên quan: stackoverflow.com/questions/221346/ cấp - cũng vậy, bạn có thể ghi đè ->
Giảm

16
@Chris Đó là về C ++, điều này tất nhiên tạo ra sự khác biệt lớn. Nhưng vì chúng ta đang nói về lý do tại sao C được thiết kế theo cách này, hãy giả vờ rằng chúng ta trở lại vào những năm 1970 - trước khi C ++ tồn tại.
Bí ẩn

5
Dự đoán tốt nhất của tôi là, toán tử mũi tên tồn tại để thể hiện trực quan "xem nó! Bạn đang xử lý một con trỏ ở đây"
Chris

4
Nhìn thoáng qua, tôi cảm thấy câu hỏi này rất lạ. Không phải tất cả mọi thứ được thiết kế chu đáo. Nếu bạn giữ phong cách này trong cả cuộc đời, thế giới của bạn sẽ đầy những câu hỏi. Câu trả lời đã nhận được nhiều phiếu nhất là thực sự nhiều thông tin và rõ ràng. Nhưng nó không đánh vào điểm chính của câu hỏi của bạn. Theo phong cách câu hỏi của bạn, tôi có thể hỏi quá nhiều câu hỏi. Ví dụ: từ khóa 'int' là tên viết tắt của 'số nguyên'; tại sao từ khóa 'đôi' cũng không ngắn hơn?
Junwanghe

1
@junwanghe Câu hỏi này thực sự đại diện cho mối quan tâm hợp lệ - tại sao .nhà điều hành có quyền ưu tiên cao hơn *nhà điều hành? Nếu không, chúng ta có thể có * ptr.member và var.member.
milleniumorms

Câu trả lời:


358

Tôi sẽ diễn giải câu hỏi của bạn thành hai câu hỏi: 1) tại sao ->thậm chí tồn tại và 2) tại sao .không tự động hủy bỏ con trỏ. Câu trả lời cho cả hai câu hỏi có nguồn gốc lịch sử.

Tại sao ->thậm chí tồn tại?

Trong một trong những phiên bản đầu tiên của ngôn ngữ C (mà tôi sẽ gọi là CRM cho " Hướng dẫn tham khảo C ", đi kèm với Unix phiên bản thứ 6 vào tháng 5 năm 1975), nhà điều hành ->có ý nghĩa rất riêng, không đồng nghĩa *.kết hợp

Ngôn ngữ C được mô tả bởi CRM rất khác so với ngôn ngữ C hiện đại ở nhiều khía cạnh. Trong CRM, các thành viên cấu trúc đã triển khai khái niệm toàn cầu về bù byte , có thể được thêm vào bất kỳ giá trị địa chỉ nào mà không hạn chế kiểu. Tức là tất cả tên của tất cả các thành viên cấu trúc có ý nghĩa toàn cầu độc lập (và, do đó, phải là duy nhất). Ví dụ bạn có thể khai báo

struct S {
  int a;
  int b;
};

và tên asẽ là viết tắt của offset 0, trong khi tên bsẽ đại diện cho offset 2 (giả sử intloại kích thước 2 và không có phần đệm). Ngôn ngữ yêu cầu tất cả các thành viên của tất cả các cấu trúc trong đơn vị dịch phải có tên duy nhất hoặc viết tắt cho cùng một giá trị offset. Ví dụ: trong cùng một đơn vị dịch bạn có thể khai báo thêm

struct X {
  int a;
  int x;
};

và điều đó sẽ ổn, vì tên asẽ luôn thay cho offset 0. Nhưng tuyên bố bổ sung này

struct Y {
  int b;
  int a;
};

sẽ chính thức không hợp lệ, vì nó đã cố gắng "xác định lại" alà offset 2 và blà offset 0.

Và đây là nơi mà ->toán tử xuất hiện. Vì mỗi tên thành viên cấu trúc có ý nghĩa toàn cầu tự cung cấp riêng, ngôn ngữ hỗ trợ các biểu thức như thế này

int i = 5;
i->b = 42;  /* Write 42 into `int` at address 7 */
100->a = 0; /* Write 0 into `int` at address 100 */

Nhiệm vụ đầu tiên được trình biên dịch diễn giải là "lấy địa chỉ 5, thêm offset 2cho nó và gán 42cho intgiá trị tại địa chỉ kết quả". Tức là ở trên sẽ gán 42cho intgiá trị tại địa chỉ 7. Lưu ý rằng việc sử dụng ->này không quan tâm đến loại biểu thức ở phía bên trái. Phía bên trái được hiểu là một địa chỉ số giá trị (có thể là một con trỏ hoặc một số nguyên).

Loại mánh khóe này là không thể với *.kết hợp. Bạn không thể làm

(*i).b = 42;

*iđã là một biểu thức không hợp lệ. Các *nhà điều hành, vì nó hoàn toàn tách biệt ., đặt ra yêu cầu loại nghiêm ngặt hơn về toán hạng của nó. Để cung cấp khả năng giải quyết giới hạn này, CRM đã giới thiệu ->toán tử, độc lập với loại toán hạng bên trái.

Như Keith đã lưu ý trong các nhận xét, sự khác biệt giữa ->*+ .kết hợp này là điều mà CRM đang đề cập đến là "thư giãn yêu cầu" trong 7.1.8: Ngoại trừ việc nới lỏng yêu cầu E1thuộc loại con trỏ, biểu thức E1−>MOShoàn toàn tương đương với(*E1).MOS

Sau đó, trong K & R C, nhiều tính năng được mô tả ban đầu trong CRM đã được làm lại đáng kể. Ý tưởng về "thành viên cấu trúc như định danh bù toàn cầu" đã bị loại bỏ hoàn toàn. Và chức năng của ->toán tử trở nên hoàn toàn giống với chức năng *.sự kết hợp.

Tại sao không thể .tự động hóa con trỏ?

Một lần nữa, trong phiên bản CRM của ngôn ngữ, toán hạng bên trái của .toán tử được yêu cầu phải là một giá trị . Đó là yêu cầu duy nhất được áp dụng cho toán hạng đó (và đó là điều làm cho nó khác với ->, như đã giải thích ở trên). Lưu ý rằng CRM không yêu cầu toán hạng bên trái .phải có kiểu cấu trúc. Nó chỉ yêu cầu nó phải là một giá trị, bất kỳ giá trị nào . Điều này có nghĩa là trong phiên bản CRM của C, bạn có thể viết mã như thế này

struct S { int a, b; };
struct T { float x, y, z; };

struct T c;
c.b = 55;

Trong trường hợp này, trình biên dịch sẽ ghi 55vào một intgiá trị được định vị ở byte-offset 2 trong khối bộ nhớ liên tục được gọi là c, mặc dù kiểu struct Tkhông có trường có tên b. Trình biên dịch sẽ không quan tâm đến loại thực tế c. Tất cả những gì nó quan tâm là đó clà một giá trị: một loại khối bộ nhớ có thể ghi.

Bây giờ lưu ý rằng nếu bạn đã làm điều này

S *s;
...
s.b = 42;

mã sẽ được coi là hợp lệ (vì scũng là một giá trị) và trình biên dịch chỉ đơn giản là cố gắng ghi dữ liệu vào schính con trỏ , ở byte-offset 2. Không cần phải nói, những thứ như thế này có thể dễ dàng dẫn đến tràn bộ nhớ, nhưng ngôn ngữ đã không quan tâm đến nó với những vấn đề như vậy.

Tức là trong phiên bản ngôn ngữ mà ý tưởng đề xuất của bạn về quá tải toán tử .cho các loại con trỏ sẽ không hoạt động: toán tử .đã có ý nghĩa rất cụ thể khi được sử dụng với các con trỏ (với các con trỏ lvalue hoặc với bất kỳ giá trị nào). Đó là chức năng rất kỳ lạ, không có nghi ngờ. Nhưng nó đã ở đó vào lúc đó.

Tất nhiên, chức năng kỳ lạ này không phải là lý do rất mạnh để chống lại việc giới thiệu .toán tử quá tải cho con trỏ (như bạn đề xuất) trong phiên bản làm lại của C - K & R C. Nhưng nó đã không được thực hiện. Có thể tại thời điểm đó, có một số mã kế thừa được viết bằng phiên bản C của CRM phải được hỗ trợ.

(URL cho Hướng dẫn tham khảo 1975 C có thể không ổn định. Một bản sao khác, có thể có một số khác biệt tinh tế, có ở đây .)


10
Và phần 7.1.8 của Tài liệu tham khảo C được trích dẫn có nội dung "Ngoại trừ việc nới lỏng yêu cầu mà E1 phải là loại con trỏ, biểu thức '' E1−> MOS '' hoàn toàn tương đương với '' (* E1) .MOS ' '. "
Keith Thompson

1
Tại sao nó không phải *ilà một giá trị của một số loại mặc định (int?) Tại địa chỉ 5? Sau đó (* i) .b sẽ hoạt động theo cách tương tự.
Random832

5
@Leo: Chà, một số người thích ngôn ngữ C là trình biên dịch cấp cao hơn. Vào thời kỳ đó trong lịch sử C, ngôn ngữ thực sự là một trình biên dịch cấp cao hơn.
AnT

29
Huh. Vì vậy, điều này giải thích tại sao nhiều cấu trúc trong UNIX (ví dụ struct stat:) tiền tố các trường của chúng (ví dụ st_mode:).
icktoofay

5
@ perfectionm1ng: Có vẻ như bell-labs.com đã bị chiếm đoạt bởi Alcatel-Lucent và các trang gốc đã biến mất. Tôi đã cập nhật liên kết đến một trang web khác, mặc dù tôi không thể nói người đó sẽ ở lại bao lâu. Dù sao, googling cho "hướng dẫn tham khảo ritchie c" thường tìm thấy tài liệu.
AnT

46

Ngoài các lý do lịch sử (tốt và đã được báo cáo), cũng có một vấn đề nhỏ với các toán tử ưu tiên: toán tử chấm có mức độ ưu tiên cao hơn toán tử sao, vì vậy nếu bạn có cấu trúc chứa con trỏ tới struct chứa con trỏ tới struct ... Hai cái này tương đương nhau:

(*(*(*a).b).c).d

a->b->c->d

Nhưng thứ hai rõ ràng là dễ đọc hơn. Toán tử mũi tên có mức ưu tiên cao nhất (giống như dấu chấm) và liên kết từ trái sang phải. Tôi nghĩ rằng điều này rõ ràng hơn việc sử dụng toán tử dấu chấm cho cả con trỏ để cấu trúc và cấu trúc, bởi vì chúng ta biết loại từ biểu thức mà không cần phải xem khai báo, thậm chí có thể nằm trong một tệp khác.


2
Với các kiểu dữ liệu lồng nhau chứa cả cấu trúc và con trỏ để cấu trúc, điều này có thể làm cho mọi thứ trở nên khó khăn hơn khi bạn phải suy nghĩ về việc chọn toán tử phù hợp cho mỗi lần truy cập. Bạn có thể kết thúc với ab-> c-> d hoặc a-> bc-> d (tôi gặp vấn đề này khi sử dụng thư viện freetype - tôi cần tìm kiếm mã nguồn mọi lúc). Ngoài ra, điều này không giải thích lý do tại sao không thể để trình biên dịch tự động hóa con trỏ khi xử lý các con trỏ.
Askaga

3
Mặc dù sự thật bạn nói là chính xác, nhưng chúng không trả lời câu hỏi ban đầu của tôi theo bất kỳ cách nào. Bạn giải thích sự bằng nhau của a-> và * (a). các ký hiệu (đã được giải thích nhiều lần trong các câu hỏi khác) cũng như đưa ra một tuyên bố mơ hồ về thiết kế ngôn ngữ có phần tùy ý. Tôi không tìm thấy câu trả lời của bạn rất hữu ích, do đó, downvote.
Askaga

16
@effeffe, OP đang nói rằng ngôn ngữ có thể dễ dàng được hiểu a.b.c.d(*(*(*a).b).c).d, làm cho ->toán tử trở nên vô dụng. Vì vậy, phiên bản của OP ( a.b.c.d) có thể đọc được như nhau (so với a->b->c->d). Đó là lý do tại sao câu trả lời của bạn không trả lời câu hỏi của OP.
Shahbaz

4
@Shahbaz Đó có thể là trường hợp của một lập trình viên java, một lập trình viên C / C ++ sẽ hiểu a.b.c.da->b->c->dnhư hai điều rất khác nhau: Thứ nhất là một bộ nhớ duy nhất truy cập vào một đối tượng phụ lồng nhau (chỉ có một đối tượng bộ nhớ duy nhất trong trường hợp này ), thứ hai là ba truy cập bộ nhớ, đuổi theo con trỏ qua bốn đối tượng khác biệt có khả năng. Đó là một sự khác biệt lớn trong cách bố trí bộ nhớ và tôi tin rằng C đã đúng khi phân biệt hai trường hợp này rất rõ ràng.
cmaster - phục hồi monica

2
@Shahbaz Tôi không có ý nói rằng là một sự xúc phạm đối với các lập trình viên java, họ chỉ đơn giản là sử dụng một ngôn ngữ với các con trỏ hoàn toàn ẩn. Nếu tôi được đưa lên làm lập trình viên java, tôi có thể nghĩ giống như vậy ... Dù sao, tôi thực sự nghĩ rằng toán tử quá tải mà chúng ta thấy trong C là ít hơn tối ưu. Tuy nhiên, tôi thừa nhận rằng tất cả chúng ta đã bị hư hỏng bởi các nhà toán học, những người tự do làm quá tải các toán tử của họ cho hầu hết mọi thứ. Tôi cũng hiểu động lực của họ, vì tập hợp các biểu tượng có sẵn là khá hạn chế. Tôi đoán, cuối cùng, đó chỉ là câu hỏi mà bạn vẽ đường ...
cmaster - khôi phục monica

19

C cũng làm tốt công việc không làm bất cứ điều gì mơ hồ.

Chắc chắn dấu chấm có thể bị quá tải có nghĩa là cả hai thứ, nhưng mũi tên đảm bảo rằng lập trình viên biết rằng anh ta đang hoạt động trên một con trỏ, giống như khi trình biên dịch sẽ không cho phép bạn trộn hai loại không tương thích.


4
Đây là câu trả lời đơn giản và chính xác. C chủ yếu cố gắng tránh quá tải mà IMO là một trong những điều tốt nhất về C.
jforberg

10
Rất nhiều thứ trong C là mơ hồ và mờ nhạt. Có các chuyển đổi kiểu ngầm định, các toán tử toán học bị quá tải, lập chỉ mục chuỗi thực hiện một số thứ hoàn toàn khác nhau tùy thuộc vào việc bạn lập chỉ mục một mảng nhiều chiều hay một mảng con trỏ và bất cứ điều gì có thể là một macro ẩn bất cứ điều gì (quy ước đặt tên theo cấp trên giúp C nhưng ' t).
PSkocik
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.