(筆記) 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, 指標的藝術, 碁峰資訊股份有限公司
全文完。