Trong C, * là một toán tử, hoặc một phần của một loại trong một khai báo?


9

Trong C, *được gọi là toán tử gián tiếp hoặc toán tử dereference. Tôi hiểu làm thế nào nó hoạt động khi nó được sử dụng trong một tuyên bố. Nó có ý nghĩa để viết *phoặc * p, xem xét rằng nó là một toán tử đơn nguyên.

Tuy nhiên, đôi khi trong một tuyên bố, a *được sử dụng.

void move(int *units) { ... }

hoặc là

int *p = &x;

Nó có vẻ lạ đối với tôi rằng một toán tử được sử dụng ở đây. Bạn không thể làm những việc như

void move(int units++) { ... }

nơi ++cũng là một nhà điều hành đơn nguyên. Đây có phải *cũng là toán tử gián tiếp, hay nó *có một ý nghĩa khác, nơi nó nói điều gì đó về loại con trỏ? (Nếu đây là trường hợp, tôi đang xem xét sử dụng ví dụ int* unitstrong khai báo và mặt int x = *p;khác cho rõ ràng.)

Trong này câu trả lời người ta nói rằng

Tiêu chuẩn C chỉ định nghĩa hai nghĩa cho toán tử *:

  • toán tử gián tiếp
  • toán tử nhân

Tôi cũng đã thấy mọi người tuyên bố rằng đó *thực sự là một phần của loại hình này. Điều này làm tôi bối rối.


1
Trong void move(int *units) { ... }, nó là một toán tử gián tiếp. Được coi là một phần của loại, nó cũng có thể được viết void move(int* units) { ... }, mặc dù tôi thích phong cách trước đây. Bạn đọc cả hai như "con trỏ int." Xem thêm stackoverflow.com/a/8911253
Robert Harvey

Câu trả lời:


16

Các mảnh nhỏ nhất của ngôn ngữ C là thẻ từ vựng , chẳng hạn như từ khóa (ví dụ int, if, break), định danh (ví dụ move, units) và những người khác.

*là một yếu tố từ vựng được gọi là dấu chấm câu (ví dụ như {}()+). Một dấu chấm câu có thể có ý nghĩa khác nhau tùy thuộc vào cách nó được sử dụng:

Dấu chấm câu là một biểu tượng có ý nghĩa cú pháp và ngữ nghĩa độc lập. Tùy thuộc vào ngữ cảnh, nó có thể chỉ định một thao tác được thực hiện (...) trong trường hợp đó được gọi là toán tử

Trong chương C11 tiêu chuẩn 6.5 về các biểu thức, *được định nghĩa là toán tử đơn nguyên (phần 6.5.3.2, đối với chỉ định) và là toán tử nhân (phần 6.5.5), chính xác như bạn đã mô tả. Nhưng *chỉ được hiểu là toán tử theo các quy tắc ngữ pháp tạo ra các biểu thức hợp lệ.

Nhưng cũng có một chương 6.2 về các khái niệm, giải thích rằng các con trỏ tới một loại là các loại dẫn xuất. Phần 6.7.6.1 về khai báo con trỏ chính xác hơn:

nếu, trong khai báo '' T D1 '', D1 có dạng
* type-Qualifier-list-opt D
và loại được chỉ định để nhận dạng trong khai báo '' T D '' là '' dẫn xuất-khai báo-type-list T '', sau đó loại được chỉ định cho nhận dạng là '' con trỏ khai báo-loại khai báo-loại-danh sách loại-danh sách thành T ''.

Điều này thực sự là *một phần của một loại dẫn xuất ( *có nghĩa là có "con trỏ", nhưng nó luôn cần một loại khác để nói với loại nào nó chỉ vào một cái gì đó).

Ghi chú: Không có mâu thuẫn trong việc này. Bạn sử dụng yếu tố ngôn ngữ từ vựng khác nhau hàng ngày khi bạn nói tiếng Anh: "một tay cầm " chỉ định một cái gì đó và "để xử lý " có nghĩa là làm một cái gì đó, hai cách sử dụng ngữ pháp hoàn toàn khác nhau của cùng một yếu tố từ vựng.


Điều đó có nghĩa là cách giải thích này chỉ là một cách phổ biến để xem xét nó (từ câu trả lời khác): int * một câu lệnh này có thể được hiểu là đọc: khai báo một biến, được đặt tên là a, khi bị hủy đăng ký là kiểu int. -Khi thực tế, * có ý nghĩa Độc lập trong một tuyên bố
Torm

@Torm chính xác!
Christophe

7

Trong C, phần không xác định trong một khai báo được đọc tốt hơn như là một phần của biến hơn là một phần của loại. Nếu tôi có:

int *a;

tuyên bố này có thể được hiểu là đọc: khai báo một biến, được đặt tên a, khi bị hủy đăng ký là loại int.

Điều này rất quan trọng khi nhiều biến được khai báo cùng một lúc:

int *a, b;   (1)
int* a, b;   (2)

Cả hai khai báo đều tương đương, và trong cả hai abkhông cùng loại - alà một con trỏ tới một int và blà một int. Nhưng khai báo (2) trông như thể "kiểu con trỏ" là một phần của kiểu khi nó thực sự là khai báo *a.


Đó là một điểm rất tốt để đưa ra điều này, bởi vì thường có sự nhầm lẫn giữa 1 và 2. Tuy nhiên loại a là "con trỏ tới int", vì vậy ngay cả khi * chỉ áp dụng cho a, nó chắc chắn là một phần của loại . Ví dụ, bạn có thể sử dụng nó trong một typedef (điều mà bạn không thể làm cho trình khởi tạo).
Christophe

Đã tìm kiếm một cuộc tranh luận tốt trong việc quyết định giữa hai tuyên bố, điều này đóng đinh nó. Tôi hiếm khi khai báo biến là một danh sách mặc dù vậy, từ giờ trở đi tôi sẽ áp dụng hình thức đầu tiên là rõ ràng nhất trong mọi trường hợp. Cảm ơn !
Newtopian

4

Để thêm vào các câu trả lời đúng khác:

Nó chỉ là một biểu thức khai báo phản ánh các biểu thức sử dụng tiềm năng. IIRC, tôi nghĩ rằng Kernigan hoặc Ritchie gọi đó là một thử nghiệm! Đây là một số văn bản hỗ trợ bên lề:

Trở lại những năm 70, khi Kernighan và Ritchie đang viết Ngôn ngữ lập trình C, họ đã khá thành thật về cách khai báo có thể nhanh chóng trở nên khó đọc bằng con mắt không được huấn luyện:

C đôi khi được định nghĩa cho cú pháp khai báo của nó, đặc biệt là các cú pháp liên quan đến con trỏ tới các hàm. Cú pháp là một nỗ lực để thực hiện khai báo sử dụng đồng ý ; nó hoạt động tốt cho các trường hợp đơn giản, nhưng nó có thể gây nhầm lẫn cho những trường hợp khó hơn, bởi vì các khai báo không thể được đọc từ trái sang phải và vì dấu ngoặc đơn được sử dụng quá mức. [...]

(Nhấn mạnh của tôi.)

http://codinghighway.com/2013/12/29/the-absolute-definitive-guide-to-decodes-c-declarations/

Vì vậy, vâng, bạn đã đúng, đây thực sự là các toán tử được áp dụng cho các biểu thức khai báo, mặc dù như bạn chỉ quan sát thấy một số toán tử có ý nghĩa (ví dụ ++ không).

Một ngôi sao như vậy thực sự là một phần của loại biến cá nhân được khai báo; tuy nhiên, như các áp phích khác đã lưu ý, nó không (chính xác hơn là không thể) được chuyển sang các biến khác được khai báo cùng một lúc (nghĩa là trong cùng một tuyên bố khai báo) - mỗi biến bắt đầu với cùng một loại cơ sở như mục tiêu (cuối cùng), và sau đó lấy con trỏ, cơ hội để áp dụng hoặc không), các toán tử hàm và hàm để hoàn thành kiểu của nó.


Đây là lý do tại sao các lập trình viên có kinh nghiệm có xu hướng thích (1) int *p;so với int* p;và (2) chỉ khai báo một biến duy nhất cho mỗi khai báo, mặc dù ngôn ngữ cho phép nhiều hơn.

Thêm vào đó int (*p);là một tuyên bố pháp lý, các parens này chỉ đơn giản là nhóm và trong trường hợp đơn giản này, và không có gì khác hơn int *p. Điều này giống như nói một biểu thức (không khai báo) (i) >= (0)giống như i >= 0, vì bạn luôn có thể thêm hợp pháp ().

Tuy nhiên, tôi đề cập đến điều đó như một ví dụ khác về lý do tại sao các lập trình viên thích một hình thức hơn hình thức khác, vì vậy hãy xem xét int (*p);so với int(* p);. Có lẽ bạn có thể thấy cách sử dụng dấu ngoặc đơn, dấu sao, v.v. cho biến không phải kiểu cơ sở (nghĩa là bằng cách thêm tất cả dấu ngoặc đơn cho phép).


1

Trong khai báo hàm này:

void move(int *units);

đó *là một phần của loại, đó là int*. Nó không phải là một nhà điều hành. Đó là lý do tại sao nhiều người sẽ viết nó như sau:

void move(int* units);

1

Chỉ có *, []()các nhà khai thác có bất kỳ ý nghĩa trong tờ khai (C ++ cho biết thêm &, nhưng ta sẽ không đi vào đó ở đây).

Trong tờ khai

int *p;       

những int-ness của pđược xác định bởi các Bộ xác định kiểu int, trong khi con trỏ-Ness của pđược xác định bởi các declarator *p .

Các loại của plà "con trỏ đến int"; loại này được chỉ định đầy đủ bởi sự kết hợp của bộ xác định loại intcộng với bộ khai báo *p.

Trong một khai báo, người khai báo giới thiệu tên của điều được khai báo ( p) cùng với thông tin loại bổ sung không được cung cấp bởi trình xác định loại ("con trỏ tới"):

T v;     // v is a single object of type T, for any type T
T *p;    // p is a pointer to T, for any type T
T a[N];  // a is an N-element array of T, for any type T
T f();   // f is a function returning T, for any type T

Điều này rất quan trọng - con trỏ-ness, mảng-ness và hàm-hàm được chỉ định như một phần của bộ khai báo, không phải là bộ xác định kiểu 1 . Nếu bạn viết

int* a, b, c;

nó sẽ được phân tích thành

int (*a), b, c;

vì vậy asẽ chỉ được khai báo như một con trỏ tới int; bcđược khai báo là ints thường xuyên .

Các *, []()các nhà khai thác có thể được kết hợp để tạo ra các loại tùy tiện phức tạp:

T *a[N];      // a is an N-element array of pointers to T
T (*a)[N];    // a is a pointer to an N-element array of T
T *(*f[N])(); // f is an N-element array of pointers to functions 
              // returning pointer to T

T *(*(*(*f)[N])())[M]  // f is a pointer to an N-element array of pointers
                       // to functions returning pointers to M-element
                       // arrays of pointers to T

Lưu ý rằng *, []()tuân theo các quy tắc ưu tiên tương tự trong các khai báo mà chúng thực hiện trong các biểu thức. *a[N]được phân tích cú pháp như *(a[N])trong cả khai báo và biểu thức.

Điều thực sự quan trọng cần nhận ra ở đây là hình thức khai báo khớp với hình thức biểu thức trong mã. Quay trở lại ví dụ ban đầu của chúng tôi, chúng tôi có một con trỏ tới một số nguyên có tên p. Nếu chúng tôi muốn truy xuất giá trị số nguyên đó, chúng tôi sử dụng *toán tử để hủy đăng ký p, như vậy:

x = *p;

Kiểu của biểu thức *pint, theo sau từ khai báo

int *p;

Tương tự, nếu chúng ta có một mảng các con trỏ tới doublevà chúng ta muốn lấy một giá trị cụ thể, chúng ta lập chỉ mục vào mảng và kết quả lại:

y = *ap[i];

Một lần nữa, loại biểu thức *ap[i]double, xuất phát từ khai báo

double *ap[N];

Vậy tại sao không ++đóng một vai trò trong một bản tuyên bố như *, []hoặc ()? Hoặc bất kỳ nhà điều hành khác thích +hoặc ->hoặc &&?

Vâng, về cơ bản, bởi vì định nghĩa ngôn ngữ nói như vậy. Nó chỉ dành riêng *, []()để chơi bất kỳ vai trò trong một tuyên bố, kể từ khi bạn có để có thể để xác định con trỏ, mảng, và các loại chức năng. Không có loại "tăng-này" riêng biệt, vì vậy không cần phải ++là một phần của tuyên bố. Không có "Bitwise-này" loại, vì vậy không cần thiết phải unary &, |, ^, hoặc ~là một phần của một bản tuyên bố một trong hai. Đối với các loại sử dụng .toán tử chọn thành viên, chúng tôi sử dụng các thẻ structuniontrong khai báo. Đối với các loại sử dụng ->toán tử, chúng tôi sử dụng các thẻ structunionkết hợp với *toán tử trong trình khai báo.


  1. Tất nhiên, bạn có thể tạo tên typedef cho các loại con trỏ, mảng và hàm, như
    typedef int *iptr;
    iptr a,b,c; // all three of a, b, and c are pointers to int
    
    nhưng một lần nữa, đó là công cụ khai báo *iptr chỉ định tính chất con trỏ của tên typedef.


1

Tiêu chuẩn C chỉ định nghĩa hai nghĩa cho *toán tử:

  • toán tử gián tiếp
  • toán tử nhân

Đúng là có hai ý nghĩa cho * toán tử . Điều đó không đúng vì đó là những ý nghĩa duy nhất của * mã thông báo .

Trong một tuyên bố như

int *p;

những *không phải là một nhà điều hành; đó là một phần của cú pháp của một tuyên bố.

Cú pháp khai báo của C nhằm phản ánh cú pháp biểu thức của nó, vì vậy khai báo trên có thể được đọc là " *pthuộc loại int", từ đó chúng ta có thể suy ra đó plà loại int*. Tuy nhiên, đây không phải là một quy tắc vững chắc và nó không được nêu ở bất kỳ đâu trong tiêu chuẩn. Thay vào đó, cú pháp khai báo được định nghĩa riêng, tách biệt với cú pháp của biểu thức. Đó là lý do tại sao, ví dụ, hầu hết các toán tử có thể xuất hiện trong biểu thức không thể xuất hiện với cùng ý nghĩa trong khai báo (trừ khi chúng là một phần của biểu thức là một phần của khai báo, giống như +toán tử trong int array[2+2];).

Một trường hợp "gương khai sử dụng" nguyên tắc không hoàn toàn công việc là:

int arr[10];

nơi arr[10]sẽ là loại int nếu nó tồn tại .

Và trên thực tế, có một cách sử dụng *mã thông báo khác. Một tham số mảng có thể được khai báo với [*]để chỉ ra một mảng có độ dài thay đổi. (Điều này đã được thêm vào trong C99.) Đây *không phải là phép nhân cũng không phải là sự quy định. Tôi nghi ngờ nó được lấy cảm hứng từ cú pháp ký tự đại diện.


Một vị trí lớn hơn, khái niệm "khai báo sau sử dụng" bị phá vỡ là với khởi tạo con trỏ. Hiệu ứng của một khai báo int *p = 0;trông tương tự như cách sử dụng * p = 0; `nhưng ý nghĩa rất khác nhau.
supercat

@supercat: Chắc chắn rồi. việc khởi tạo luôn khớp với loại đối tượng (trong trường hợp này int*).
Keith Thompson
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.