(筆記) struct對function可以call by value嗎?可以return一個struct嗎? (C/C++)

 
Abstract
C在傳遞較大型資料結構進function時,如array、string、struct時,都建議使用pointer的call by address,是否也能使用call by value呢?

Introduction
使用環境:Visual Studio 2010 / Visual C++ 10.0, Turbo C 2.0

C在傳遞資料進function時,就只有兩招,一招是call by value,一招是call by address(實際上也是一種call by value,只是它copy的是value的address,而不是value本身),一些較小型的型別如int、double,我們會使用call by value配合return,當然使用call by address亦可;而一些較大的型別,如string、array、struct,我們會使用call by address的方式,也就是只把pointer copy進stack,而不需將整個資料copy進stack,這樣比較有效率。

昨天好友Roger問我可以將struct以call by value的方式傳進function,並且return一個struct嗎?

在實驗之前,我們先用既有的認知做推理:

1.根據以往的經驗,struct都是以call by address的方式,所以不確定C compiler是否能接受struct call by value的寫法。(須實驗)

2.就算C compiler能接受語法,是真的call by value?還是只是call by address的syntax sugar? (須實驗)

3.就算C compiler能struct call by value,效率一定遠比struct call by address差,因為struct通常都很大,且需整個struct copy進stack,再整個struct從stack內copy出來。(這點可以確定)

接下來,我們來作實驗:

首先,我們來看正統的寫法,也就是struct call by address

struct_call_by_address.c / C

1 /*
2 (C) OOMusou 2011 http://oomusou.cnblogs.com
3
4 Filename : struct_call_by_address.c
5 Compiler : Visual C++ 10.0
6 Description : struct call by address in C
7 Release : 02/18/2011 1.0
8  */
9
10 #include <stdio.h>
11 #include <string.h> // strcpy()
12  
13 typedef struct {
14 int no;
15 char name[10];
16 } student, *pstudent;
17
18 pstudent struct_call_by_address(pstudent pboy) {
19 pboy->no = 10;
20 strcpy(pboy->name, "oomusou");
21 printf("in function:\n");
22 printf("no=%d, name=%s\n", pboy->no, pboy->name);
23 return pboy;
24 }
25
26  int main() {
27 student boy = {20, "John"};
28 pstudent pboy = 0;
29
30 printf("before function\n");
31 printf("no=%d, name=%s\n", boy.no, boy.name);
32 pboy = struct_call_by_address(&boy);
33 printf("after function\n");
34 printf("no=%d, name=%s\n", boy.no, boy.name);
35 printf("no=%d, name=%s\n", pboy->no, pboy->name);
36 }

執行結果

before function
no
=20, name=John
in function:
no
=10, name=oomusou
after function
no
=10, name=oomusou
no
=10, name=oomusou

13行

typedef struct {
int no;
char name[10];
} student,
*pstudent;

使用typedef重新定義struct型別student,並且順便定義pointer版本的pstudent,因為struct傳進function時一定用的到pointer,實務上都是兩個型別一起宣告在header file(*.h)中。

27行

student boy = {20, "John"};

宣告boy變數,並且一起做初始化。

28行

pstudent pboy = 0;

宣告pboy這個指向struct的pointer,並且馬上指定為0(或者NULL)避免wild pointer的發生,這是一個好習慣。

18行

pstudent struct_call_by_address(pstudent pboy) {

定義struct_call_by_address() function,注意傳進去的是pointer版本的struct,回傳的也是pointer版本的struct。

30行

printf("before function\n");
printf(
"no=%d, name=%s\n", boy.no, boy.name);

結果為

before function
no
=20, name=John

先印出執行function前的值,結果為27行剛出始化的27與John,符合預期。

32行

pboy = struct_pass_by_address(&boy);

呼叫struct_pass_by_address() function,由於要傳進去的是pointer,所以必須&boy;因為傳回的也是pointer,所以使用pboy。

19行

pboy->no = 10;
strcpy(pboy
->name, "oomusou");

實際去改變struct值,因為傳進function的為pointer pboy,所以要用->,這是C的語法規定;另外C的字串也規定要用strcyp(),無法用pboy->name = "oomusou"這種直覺的語法(C++可以),既然在這裡用C,就只好認了。

21行

printf("in function:\n");
printf(
"no=%d, name=%s\n", pboy->no, pboy->name);

結果為

in function:
no
=10, name=oomusou

印出在function內目前的值為何,因為剛改成10與oomusou,所以印出10與oomusou,符合預期。

33行

printf("after function\n");
printf(
"no=%d, name=%s\n", boy.no, boy.name);
printf(
"no=%d, name=%s\n", pboy->no, pboy->name);

結果為

after function
no
=10, name=oomusou
no
=10, name=oomusou

印出執行完function後的結果,由於我們是直接將pointer傳進去,所以function內所改的值是pointer所指的值,也就是實際改了原本struct的值,所以無論是印出原本struct的值:boy.no與boy.name,或者回傳struct pointer版本的pboy->no與pboy->name,其結果都是一樣的,也符合預期。

這裡要暫時岔開話題談談typedef與struct的應用,在看C爸爸的The C Programming Language [1]或者C Primer Plus [2]這兩本C的聖經時,常可看到直接使用struct而不使用typedef的範例,若以上程式改用這種寫法,將變成如下:

struct_pass_by_address_2.c / C

1 /*
2 (C) OOMusou 2011 http://oomusou.cnblogs.com
3
4 Filename : struct_call_by_address_2.c
5 Compiler : Visual C++ 10.0
6 Description : struct call by address in C
7 Release : 02/18/2011 1.0
8  */
9
10 #include <stdio.h>
11 #include <string.h> // strcpy()
12  
13  struct student {
14 int no;
15 char name[10];
16 };
17
18  struct student* struct_call_by_address(struct student* pboy) {
19 pboy->no = 10;
20 strcpy(pboy->name, "oomusou");
21 printf("in function:\n");
22 printf("no=%d, name=%s\n", pboy->no, pboy->name);
23 return pboy;
24 }
25
26  int main() {
27 struct student boy = {20, "John"};
28 struct student *pboy = 0;
29
30 printf("before function\n");
31 printf("no=%d, name=%s\n", boy.no, boy.name);
32 pboy = struct_call_by_address(&boy);
33 printf("after function\n");
34 printf("no=%d, name=%s\n", boy.no, boy.name);
35 printf("no=%d, name=%s\n", pboy->no, pboy->name);
36 }

13行

struct student {
int no;
char name[10];
};

直接宣告student這個struct型別,沒有使用typedef。

27行

struct student boy = {20, "John"};
struct student *pboy = 0;

導致要宣告struct變數與pointer時,都要加上struct這個keyword。

struct student* struct_call_by_address(struct student* pboy) {

在宣告function時也一樣,必須加上struct這個keyword。

以我trace code的經驗,實務上我是沒看過人這樣寫,因為隨時要加上struct修飾的寫法不太像C語言的風格,幸好C語言還有typedef,透過typedef來定義struct型別,還可順便連pointer版本一起定義,之後的宣告都會比較方便。

接下來回到本文的主題:struct call by value

struct_call_by_value.c / C

1 /*
2 (C) OOMusou 2011 http://oomusou.cnblogs.com
3
4 Filename : struct_call_by_value.c
5 Compiler : Visual C++ 10.0
6 Description : struct call by value in C
7 Release : 02/18/2011 1.0
8  */
9
10 #include <stdio.h>
11 #include <string.h> // strcpy()
12  
13 typedef struct {
14 int no;
15 char name[10];
16 } student, *pstudent;
17
18 student struct_call_by_value(student boy) {
19 boy.no = 10;
20 strcpy(boy.name, "oomusou");
21 printf("in function:\n");
22 printf("no=%d, name=%s\n", boy.no, boy.name);
23 return boy;
24 }
25
26  int main() {
27 student boy = {20, "John"};
28 student boy2;
29
30 printf("before function\n");
31 printf("no=%d, name=%s\n", boy.no, boy.name);
32 boy2 = struct_call_by_value(boy);
33 printf("after function\n");
34 printf("no=%d, name=%s\n", boy.no, boy.name);
35 printf("no=%d, name=%s\n", boy2.no, boy2.name);
36 }

執行結果

before function
no
=20, name=John
in function:
no
=10, name=oomusou
after function
no
=20, name=John
no
=10, name=oomusou

18行

student struct_call_by_value(student boy) {

以call by value的方式將整個struct傳進去,也將整個struct傳回來。

34行

printf("no=%d, name=%s\n", boy.no, boy.name);
printf(
"no=%d, name=%s\n", boy2.no, boy2.name);

結果為

no=20, name=John
no
=10, name=oomusou

其他的部份程式碼與結果都和call by address一樣,就不在解釋。

最重要的是這兩行。

boy.no與boy.name都與before function一樣,所以原本的struct未受function影響,符合call by value的觀念。

boy2.no與boy2.name與in function一樣,顯然boy2接收了function改變之後所return出來的值,這是一個重新copy出來新的struct。

簡單來說,在call by address中,boy與pboy兩個都是同一個struct,而call by value中,boy與boy2是兩個不同的struct。

回到一開始的兩個不確定的疑問:

1.根據以往的經驗,struct都是以call by address的方式,所以不確定C compiler是否能接受struct call by value的寫法。(須實驗)

2.就算C compiler能接受語法,是真的call by value?還是只是call by address的syntax sugar? (須實驗)

以目前Visual C++ 10.0來看,是能接受struct call by value的方式,而且function內更改的值也不會影響到原來的struct,所以compiler能接受struct call by value,且也不是call by address的syntax sugar。

不過在蔡明志譯的C Primer Plus 5/e中文精華增訂版[2]的p.634

在舊版C的某些實作,結構並無法傳遞參數給函數,但使用指向結構的指標卻可以

為了這句話,我特別找了Turbo C 2.0作測試,這是1989年在DOS上很有名的C Compiler,結果struct pass by value也順利通過,20幾年前的C compiler都能通過了,所以其他compiler應該也沒問題,不過那些在嵌入式平台或者8051的C compiler我就不確定了。

本來到此,整個討論就該結束了,但既然實驗到struct,就讓我想起array。

array是否能pass by value呢?

array_pass_by_value.c / C

1 /*
2 (C) OOMusou 2011 http://oomusou.cnblogs.com
3
4 Filename : array_call_by_value.c
5 Compiler : Visual C++ 10.0
6 Description : array call by value in C
7 Release : 02/18/2011 1.0
8  */
9
10 #include <stdio.h>
11
12  void array_call_by_value (int arr[]) {
13 arr[0] = 2;
14 printf("in function\n");
15 printf("%d %d %d\n", arr[0], arr[1], arr[2]);
16 }
17
18
19  int main() {
20 int arr[] = {1, 2, 3};
21
22 printf("before function\n");
23 printf("%d %d %d\n", arr[0], arr[1], arr[2]);
24 array_call_by_value(arr);
25 printf("after function\n");
26 printf("%d %d %d\n", arr[0], arr[1], arr[2]);
27 }

執行結果

before function
1 2 3
in function
2 2 3
after function
2 2 3

12行

void array_call_by_value (int arr[]) {

在語法看來,好像是array以pass by value傳進去,但執行結果卻相當詭異,in function是 2 2 3,after function也是 2 2 3,這告訴我們什麼?根本就是pass by address的syntax sugar,所以array是沒有pass by value的!!

或許你會問,那function可以return arry by value嗎?

int [] array_call_by_value (int arr[]) {
arr[
0] = 2;
printf(
"in function\n");
printf(
"%d %d %d\n", arr[0], arr[1], arr[2]);
}

很抱歉,這種寫法還沒執行已經是語法錯誤,C在這裡連syntax sugar也沒有,直接語法錯誤。

完整程式碼下載
struct_call_by_address.7z (標準struct以call by address傳給function,且使用typedef)
struct_call_by_address_2.7z (標準struct以call by address傳給function,不使用typedef)
struct_call_by_value.7z (struct以call by value傳給function)
array_call_by_value.7z (array以call by value傳給function,事實上是call by address的syntax sugar)

Conclusion
在本次的實驗,我們得到以下4個結論:

1.正統struct是以call by address的方式傳進function,速度較快,而且所有C compiler都可接受。

2.使用typedef一次定義無pointer與有pointer兩種型別,之後的宣告會較方便。

3.struct pass by value在絕大部分的C compiler都能通過,而且也不是pass by address的syntax sugar,不過在一些很老舊的C compiler很可能不支援。

4.array沒有pass by value,就算有也是pass by address的syntax sugar,而且根本連語法都不支援return array by value。

Reference
[1] Brian W. Kernighan, DEnnis M.Ritchie 1988, The C Programming Language, Pewnrixw Hall PTR
[2] 蔡明志 譯 2006, C Primer Plus 5/e 中文精華增訂版,碁峰資訊股份有限公司
[3] 蔡明志 2009, 指標的藝術, 碁峰資訊股份有限公司

全文完。

posted on 2011-02-18 20:37  真 OO无双  阅读(32199)  评论(1编辑  收藏

导航