Truyền một mảng làm đối số cho một hàm trong C


86

Tôi đã viết một hàm chứa mảng làm đối số và gọi nó bằng cách truyền giá trị của mảng như sau.

void arraytest(int a[])
{
    // changed the array a
    a[0]=a[0]+a[1];
    a[1]=a[0]-a[1];
    a[0]=a[0]-a[1];
}

void main()
{
    int arr[]={1,2};
    printf("%d \t %d",arr[0],arr[1]);
    arraytest(arr);
    printf("\n After calling fun arr contains: %d\t %d",arr[0],arr[1]);
}

Những gì tôi tìm thấy là mặc dù tôi đang gọi arraytest()hàm bằng cách truyền các giá trị, bản sao gốc của đã int arr[]bị thay đổi.

Bạn có thể vui lòng giải thích tại sao không?


1
Bạn đang đi qua các mảng bằng cách tham khảo nhưng bạn sửa đổi nội dung của nó - vì vậy tại sao bạn đang nhìn thấy một sự thay đổi trong dữ liệu
Shaun Wilde

main()phải trở về int.
underscore_d

Câu trả lời:


137

Khi truyền một mảng dưới dạng tham số, điều này

void arraytest(int a[])

có nghĩa chính xác giống như

void arraytest(int *a)

vì vậy bạn đang sửa đổi các giá trị trong main.

Vì lý do lịch sử, mảng không phải là công dân hạng nhất và không thể được chuyển theo giá trị.


3
Ký hiệu nào tốt hơn trong trường hợp nào?
Ramon Martinez

20
@Ramon - Tôi sẽ sử dụng tùy chọn thứ hai, vì nó có vẻ ít khó hiểu hơn và chỉ ra rằng bạn không nhận được bản sao của mảng.
Bo Persson

2
Bạn có thể giải thích các "lý do lịch sử"? Tôi cho rằng đi ngang qua các giá trị sẽ cần một bản sao và do đó, một sự lãng phí bộ nhớ .. cảm ơn
Jacquelyn.Marquardt

5
@lucapozzobon - Ban đầu C không có bất kỳ giá trị chuyển nào, ngoại trừ các giá trị đơn lẻ. Cho đến khi structđược thêm vào ngôn ngữ, điều này đã được thay đổi. Và sau đó đã được coi là quá muộn để thay đổi các quy tắc cho mảng. Đã có 10 người dùng. :-)
Bo Persson

1
... có nghĩa chính xác giống như void arraytest(int a[1000])vv ... Câu trả lời mở rộng ở đây: stackoverflow.com/a/51527502/4561887 .
Gabriel Staples

9

Nếu bạn muốn truyền một mảng một chiều làm đối số trong một hàm , bạn sẽ phải khai báo một tham số chính thức theo một trong ba cách sau và cả ba phương thức khai báo đều tạo ra kết quả tương tự vì mỗi phương thức cho trình biên dịch biết rằng một con trỏ số nguyên đang hoạt động để được nhận .

int func(int arr[], ...){
    .
    .
    .
}

int func(int arr[SIZE], ...){
    .
    .
    .
}

int func(int* arr, ...){
    .
    .
    .
}

Vì vậy, bạn đang sửa đổi các giá trị ban đầu.

Cảm ơn !!!


Tôi đang tìm ví dụ thứ hai của bạn, bạn có thể nói rõ hơn những ưu điểm của từng phương pháp là gì không?
Puck

8

Bạn không chuyển mảng dưới dạng bản sao. Nó chỉ là một con trỏ trỏ đến địa chỉ mà phần tử đầu tiên của mảng nằm trong bộ nhớ.


7

Bạn đang chuyển địa chỉ của phần tử đầu tiên của mảng


7

Mảng trong C, trong hầu hết các trường hợp, được chuyển đổi thành một con trỏ đến phần tử đầu tiên của chính mảng đó. Và chi tiết hơn, các mảng được truyền vào các hàm luôn được chuyển đổi thành con trỏ.

Đây là một trích dẫn từ K & R2nd :

Khi một tên mảng được chuyển cho một hàm, những gì được truyền là vị trí của phần tử ban đầu. Trong hàm được gọi, đối số này là một biến cục bộ và do đó, một tham số tên mảng là một con trỏ, tức là một biến có chứa địa chỉ.

Viết:

void arraytest(int a[])

có cùng nghĩa với cách viết:

void arraytest(int *a)

Vì vậy, mặc dù bạn không viết nó một cách rõ ràng, nó giống như bạn đang truyền một con trỏ và vì vậy bạn đang sửa đổi các giá trị trong chính.

Để biết thêm, tôi thực sự khuyên bạn nên đọc cái này .

Hơn nữa, bạn có thể tìm thấy các câu trả lời khác trên SO tại đây


6

Bạn đang chuyển giá trị của vị trí bộ nhớ của thành viên đầu tiên của mảng.

Do đó, khi bạn bắt đầu sửa đổi mảng bên trong hàm, bạn đang sửa đổi mảng ban đầu.

Hãy nhớ rằng đó a[1]*(a+1).


1
Tôi cho rằng có () mất tích * a + 1 nên * (a + 1)
ShinTakezou

@Shin Cảm ơn, đã lâu rồi tôi không chơi với C.
alex

6

Trong C, ngoại trừ một số trường hợp đặc biệt, một tham chiếu mảng luôn "phân rã" thành một con trỏ đến phần tử đầu tiên của mảng. Do đó, không thể chuyển một mảng "theo giá trị". Một mảng trong một lời gọi hàm sẽ được chuyển đến hàm dưới dạng một con trỏ, tương tự như việc truyền mảng bằng tham chiếu.

CHỈNH SỬA: Có ba trường hợp đặc biệt như vậy khi một mảng không phân rã thành một con trỏ đến phần tử đầu tiên của nó:

  1. sizeof akhông giống như sizeof (&a[0]).
  2. &akhông giống với &(&a[0])(và không hoàn toàn giống với &a[0]).
  3. char b[] = "foo"không giống như char b[] = &("foo").

Nếu tôi truyền một mảng cho một hàm. Ví dụ, tôi đã tạo một mảng int a[10]và gán giá trị ngẫu nhiên cho mỗi phần tử. Bây giờ nếu tôi chuyển mảng này cho một hàm sử dụng int y[]hoặc int y[10]hoặc int *y. Và sau đó trong hàm đó tôi sử dụng sizeof(y)Câu trả lời sẽ là con trỏ byte đã được cấp phát. Vì vậy, trong trường hợp này, nó sẽ phân rã như một con trỏ, Sẽ rất hữu ích nếu bạn cũng bao gồm điều này. Xem này postimg.org/image/prhleuezd
Suraj Jain

Nếu tôi sử dụng sizeofhàm trong hàm trong mảng ban đầu chúng ta đã xác định thì nó sẽ phân rã dưới dạng một mảng, nhưng nếu tôi truyền vào hàm khác thì có sizeoftoán tử sử dụng nó sẽ phân rã dưới dạng con trỏ.
Suraj Jain


Tôi biết điều này là cũ. Hai câu hỏi nếu ai đó tình cờ nhìn thấy điều này :) 1. @ThomSmith đã viết rằng &akhông hoàn toàn giống như &a[0]khi alà một mảng. Làm sao vậy? Trong chương trình thử nghiệm của tôi, cả hai đều giống nhau, cả trong hàm nơi khai báo mảng và khi được truyền vào một hàm khác. 2. Nhà văn viết rằng " char b[] = "foo"không giống với char b[] = &("foo")". Đối với tôi, sau này thậm chí không biên dịch. Chỉ tôi thôi à?
Aviv Cohn

6

Truyền một mảng nhiều chiều làm đối số cho một hàm. Truyền một mảng mờ làm đối số ít nhiều là nhỏ. Chúng ta hãy xem xét trường hợp thú vị hơn của việc truyền một mảng 2 dim. Trong C, bạn không thể sử dụng một con trỏ để xây dựng con trỏ ( int **) thay vì 2 mảng mờ. Hãy làm một ví dụ:

void assignZeros(int(*arr)[5], const int rows) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < 5; j++) {
            *(*(arr + i) + j) = 0;
            // or equivalent assignment
            arr[i][j] = 0;
        }
    }

Ở đây tôi đã chỉ định một hàm nhận đối số đầu tiên là một con trỏ đến một mảng 5 số nguyên. Tôi có thể chuyển dưới dạng đối số bất kỳ 2 mảng mờ nào có 5 cột:

int arr1[1][5]
int arr1[2][5]
...
int arr1[20][5]
...

Bạn có thể nảy ra ý tưởng xác định một hàm tổng quát hơn có thể chấp nhận bất kỳ mảng 2 mờ nào và thay đổi chữ ký hàm như sau:

void assignZeros(int ** arr, const int rows, const int cols) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            *(*(arr + i) + j) = 0;
        }
    }
}

Mã này sẽ biên dịch nhưng bạn sẽ gặp lỗi thời gian chạy khi cố gắng gán các giá trị theo cách giống như trong hàm đầu tiên. Vì vậy, trong C một mảng nhiều chiều không giống như con trỏ tới con trỏ ... tới con trỏ. An int(*arr)[5]là một con trỏ tới mảng 5 phần tử, an int(*arr)[6] là một con trỏ tới mảng 6 phần tử và chúng là một con trỏ đến các kiểu khác nhau!

Vâng, làm thế nào để xác định các đối số hàm cho các kích thước cao hơn? Đơn giản, chúng ta chỉ làm theo mẫu! Đây là chức năng tương tự được điều chỉnh để lấy một mảng 3 chiều:

void assignZeros2(int(*arr)[4][5], const int dim1, const int dim2, const int dim3) {
    for (int i = 0; i < dim1; i++) {
        for (int j = 0; j < dim2; j++) {
            for (int k = 0; k < dim3; k++) {
                *(*(*(arr + i) + j) + k) = 0;
                // or equivalent assignment
                arr[i][j][k] = 0;
            }
        }
    }
}

Bạn có thể mong đợi như thế nào, nó có thể coi là đối số bất kỳ 3 mảng mờ nào có 4 phần tử trong chiều thứ hai và 5 phần tử trong chiều thứ ba. Bất cứ điều gì như thế này sẽ ổn:

arr[1][4][5]
arr[2][4][5]
...
arr[10][4][5]
...

Nhưng chúng tôi phải chỉ định tất cả các kích thước thứ nguyên cho đến kích thước đầu tiên.


5

Sử dụng mảng tiêu chuẩn trong C với kiểu tự nhiên phân rã từ mảng thành ptr

@Bo Persson tuyên bố chính xác trong câu trả lời tuyệt vời của anh ấy ở đây :

Khi truyền một mảng dưới dạng tham số, điều này

void arraytest(int a[])

có nghĩa chính xác giống như

void arraytest(int *a)

Tuy nhiên, tôi xin nói thêm rằng hai biểu mẫu trên cũng có:

  1. có nghĩa chính xác giống như

     void arraytest(int a[0])
    
  2. có nghĩa là giống hệt như

     void arraytest(int a[1])
    
  3. có nghĩa là giống hệt như

     void arraytest(int a[2])
    
  4. có nghĩa là giống hệt như

     void arraytest(int a[1000])
    
  5. Vân vân.

Trong mọi ví dụ về mảng ở trên, kiểu tham số đầu vào giảm dần thànhint * và có thể được gọi mà không có cảnh báo và không có lỗi, ngay cả khi -Wall -Wextra -Werrorđã bật tùy chọn xây dựng (xem repo của tôi tại đây để biết chi tiết về 3 tùy chọn xây dựng này), như điều này:

int array1[2];
int * array2 = array1;

// works fine because `array1` automatically decays from an array type
// to `int *`
arraytest(array1);
// works fine because `array2` is already an `int *` 
arraytest(array2);

Như một vấn đề của thực tế, giá trị "kích thước" ( [0], [1], [2], [1000], vv) bên trong tham số mảng ở đây là dường như chỉ dành riêng cho mục đích thẩm mỹ / tự tài liệu, và có thể là bất kỳ số nguyên dương ( size_tgõ tôi nghĩ) mà bạn muốn!

Tuy nhiên, trong thực tế, bạn nên sử dụng nó để chỉ định kích thước tối thiểu của mảng mà bạn mong đợi hàm nhận được, để khi viết mã, bạn có thể dễ dàng theo dõi và xác minh. Tiêu chuẩn MISRA-C-2012 ( mua / tải xuống bản PDF 236 trang / phiên bản 2012 của tiêu chuẩn với giá £ 15,00 tại đây ) cho đến nay (đã nhấn mạnh thêm):

Quy tắc 17.5 Đối số hàm tương ứng với một tham số được khai báo là có kiểu mảng phải có một số phần tử thích hợp.

...

Nếu một tham số được khai báo là một mảng với kích thước xác định, thì đối số tương ứng trong mỗi lệnh gọi hàm phải trỏ vào một đối tượng có ít nhất bao nhiêu phần tử như mảng.

...

Việc sử dụng bộ khai báo mảng cho một tham số hàm xác định giao diện hàm rõ ràng hơn so với việc sử dụng con trỏ. Số lượng phần tử tối thiểu mà hàm mong đợi được nêu rõ ràng, trong khi điều này là không thể với một con trỏ.

Nói cách khác, họ khuyên bạn nên sử dụng định dạng kích thước rõ ràng, mặc dù tiêu chuẩn C về mặt kỹ thuật không thực thi - nó ít nhất giúp làm rõ với bạn với tư cách là nhà phát triển và với những người khác sử dụng mã, mảng kích thước mà hàm đang mong đợi bạn để vượt qua.


Buộc loại an toàn trên các mảng trong C

Như @Winger Sendon đã chỉ ra trong một bình luận bên dưới câu trả lời của tôi, chúng ta có thể buộc C xử lý một kiểu mảng là khác nhau dựa trên kích thước mảng !

Đầu tiên, bạn phải nhận ra rằng trong ví dụ của tôi ở trên, sử dụng int array1[2];như sau: các arraytest(array1);nguyên nhân array1tự động phân rã thành một int *. TUY NHIÊN, nếu bạn lấy địa chỉ của array1 thay thế và gọi arraytest(&array1), bạn sẽ có hành vi hoàn toàn khác! Bây giờ, nó KHÔNG phân rã thành một int *! Thay vào đó, kiểu &array1int (*)[2], có nghĩa là "con trỏ đến một mảng kích thước 2 của kiểu int" hoặc "con trỏ đến một mảng kích thước 2 kiểu int" . Vì vậy, bạn có thể FORCE C để kiểm tra độ an toàn của kiểu trên một mảng, như sau:

void arraytest(int (*a)[2])
{
    // my function here
}

Cú pháp này khó đọc, nhưng tương tự như cú pháp của con trỏ hàm . Công cụ trực tuyến, cdecl , cho chúng ta biết điều đó int (*a)[2]có nghĩa là: "khai báo một con trỏ là con trỏ tới mảng 2 của int" (con trỏ tới mảng 2 ints). Đừng nhầm lẫn điều này với phiên bản KHÔNG có ngoặc đơn:, int * a[2]có nghĩa là: "khai báo a là mảng 2 của con trỏ tới int" (mảng 2 con trỏ tới int).

Bây giờ, hàm này &YÊU CẦU bạn gọi nó bằng toán tử địa chỉ ( ) như thế này, sử dụng làm tham số đầu vào là ĐIỂM ĐẾN MỘT SỐ ĐẾN ĐÚNG KÍCH THƯỚC !:

int array1[2];

// ok, since the type of `array1` is `int (*)[2]` (ptr to array of 
// 2 ints)
arraytest(&array1); // you must use the & operator here to prevent
                    // `array1` from otherwise automatically decaying
                    // into `int *`, which is the WRONG input type here!

Tuy nhiên, điều này sẽ tạo ra một cảnh báo:

int array1[2];

// WARNING! Wrong type since the type of `array1` decays to `int *`:
//      main.c:32:15: warning: passing argument 1 of ‘arraytest’ from 
//      incompatible pointer type [-Wincompatible-pointer-types]                                                            
//      main.c:22:6: note: expected ‘int (*)[2]’ but argument is of type ‘int *’
arraytest(array1); // (missing & operator)

Bạn có thể kiểm tra mã này ở đây .

Để buộc trình biên dịch C biến cảnh báo này thành lỗi, do đó bạn PHẢI luôn gọi arraytest(&array1);chỉ sử dụng mảng đầu vào có kích thước kiểu đúng ( int array1[2];trong trường hợp này), hãy thêm -Werrorvào các tùy chọn xây dựng của bạn. Nếu chạy mã kiểm tra ở trên trên onlinegdb.com, hãy thực hiện việc này bằng cách nhấp vào biểu tượng bánh răng ở trên cùng bên phải và nhấp vào "Cờ trình biên dịch bổ sung" để nhập tùy chọn này. Bây giờ, cảnh báo này:

main.c:34:15: warning: passing argument 1 of ‘arraytest’ from incompatible pointer type [-Wincompatible-pointer-types]                                                            
main.c:24:6: note: expected ‘int (*)[2]’ but argument is of type ‘int *’    

sẽ chuyển thành lỗi bản dựng này:

main.c: In function ‘main’:
main.c:34:15: error: passing argument 1 of ‘arraytest’ from incompatible pointer type [-Werror=incompatible-pointer-types]
     arraytest(array1); // warning!
               ^~~~~~
main.c:24:6: note: expected ‘int (*)[2]’ but argument is of type ‘int *’
 void arraytest(int (*a)[2])
      ^~~~~~~~~
cc1: all warnings being treated as errors

Lưu ý rằng bạn cũng có thể tạo con trỏ "loại an toàn" cho các mảng có kích thước nhất định, như sau:

int array[2];
// "type safe" ptr to array of size 2 of int:
int (*array_p)[2] = &array;

... nhưng tôi không nhất thiết phải khuyến nghị điều này, vì nó gợi cho tôi rất nhiều trò hề trong C ++ được sử dụng để buộc an toàn loại ở mọi nơi, với chi phí đặc biệt cao về độ phức tạp cú pháp ngôn ngữ, độ dài và khó khăn trong việc lưu trữ mã, và điều mà tôi không thích và đã nói nhiều lần trước đây (ví dụ: xem "Suy nghĩ của tôi trên C ++" tại đây ).


Để biết thêm các bài kiểm tra và thử nghiệm, hãy xem thêm liên kết ngay bên dưới.

Người giới thiệu

Xem các liên kết ở trên. Cũng thế:

  1. Thử nghiệm mã của tôi trực tuyến: https://onlinegdb.com/B1RsrBDFD

2
void arraytest(int (*a)[1000])tốt hơn vì sau đó trình biên dịch sẽ báo lỗi nếu kích thước sai.
Winger Sendon

@WingerSendon, tôi biết có một số tinh tế tôi cần phải xác minh ở đây, và cú pháp đó là khó hiểu (giống như một cú pháp chức năng ptr là khó hiểu), vì vậy tôi mất nhiều thời gian của tôi và cuối cùng tôi đã cập nhật câu trả lời của tôi với một phần mới lớn với tựa đề Forcing type safety on arrays in C, bao gồm của bạn điểm.
Gabriel Staples

0

Mảng luôn được chuyển bằng tham chiếu nếu bạn sử dụng a[]hoặc *a:

int* printSquares(int a[], int size, int e[]) {   
    for(int i = 0; i < size; i++) {
        e[i] = i * i;
    }
    return e;
}

int* printSquares(int *a, int size, int e[]) {
    for(int i = 0; i < size; i++) {
        e[i] = i * i;
    }
    return e;
}

Tôi ủng hộ điều này. Tôi không chắc tại sao nó bị từ chối.
Gabriel Staples
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.