收藏 分享(赏)

深入剖析C++函数的参数传递机制.doc

上传人:ysd1539 文档编号:6661310 上传时间:2019-04-20 格式:DOC 页数:8 大小:85KB
下载 相关 举报
深入剖析C++函数的参数传递机制.doc_第1页
第1页 / 共8页
深入剖析C++函数的参数传递机制.doc_第2页
第2页 / 共8页
深入剖析C++函数的参数传递机制.doc_第3页
第3页 / 共8页
深入剖析C++函数的参数传递机制.doc_第4页
第4页 / 共8页
深入剖析C++函数的参数传递机制.doc_第5页
第5页 / 共8页
点击查看更多>>
资源描述

1、 深入剖析 C/C+函数的参数传递机制首先,这篇文章针对近期网友在 ARX 版块的提问,很多都是在调用 ARX 函数或者设计自定义函数时出现的困惑,为方便大家分析和理解问题,进而正确解决问题,我将个人的一些理解写成文字,希望对大家在做 ARX 程序设计时有所帮助。同时,这篇文章也为“ObjectARX 程序设计入门(2) ”作些准备工作。这篇文章与普通的 C/C+教材相比,可能要深入得多,阅读时应该细心。而对于未接触过 C 语言的读者来说,大概需要先阅读一般的 C+教材。我的看法, C+ 编程思想 和深入浅出 MFC一类的书对于初学者太过深入,而类似Visual C+ 6.0 从入门到精通的书

2、籍主要篇幅在介绍 VC 软件的使用方法而不是讲解 C+程序设计方法,它们都不适宜作 C+或 ARX 程序设计入门学习用书。我个人学习 C+使用的是南京大学出版社的书名就是C+ 教程 ,它是为 C 程序员编写的 C+教材,全书仅 130 多页,内容浅显但基本够用。不过那是上世纪 90 年代初出版的,现在大概不好找了,不过类似的(比如说大学教材)我想书店里还是有的。文章中的大部分内容是我个人的看法,一般的 C+书籍上找不到类似的说法与其比较,对于其正确性,我没有十足的把握。各位网友可以对此进行讨论或者批评。(只是不要真的用砖头砸,那样对于我英勇而忙碌的医护人员太不尊重,别再给他们添乱了。)C 语言

3、的函数入口参数,可以使用值传递和指针传递方式,C+ 又多了引用 (reference)传递方式。引用传递方式在使用上类似于值传递,而其传递的性质又象是指针传递,这是 C+初学者经常感到困惑的。为深入介绍这三种参数传递方式,我们先把话题扯远些:1、 C/C+函数调用机制及值传递:在结构化程序设计方法中,先辈们告诉我们,采用“自顶向下,逐步细化”的方法将一个现实的复杂问题分成多个简单的问题来解决。而细化到了最底层,就是“实现单一功能”的模块,在 C/C+中,这个最小的单元模块就是函数。然而,这些单个的模块(或者说函数) 组合起来要能完成一项复杂的功能,这就注定各个函数之间必然要有这样或那样的联系(

4、即耦合) 。而参数耦合是各个函数之间最为常见的耦合方式,也就是说,各个函数之间通常通过参数传递的方式来实现通讯。当我们设计或者调用一个函数时,首先要注意的是函数的接口,也就是函数的参数和返回值。调用一个函数就是将符合函数接口要求的参数传递给函数体,函数执行后返回一个值给调用者。(当然,C/C+允许 void 类型的参数和返回值。当返回值为 void 时,函数类似 Basic 的 Sub 子过程或者 Pascal 的 Procedure 过程。)函数的参数传递,就是将在函数体外部已赋值(或者至少已经定义并初始化) 的变量通过函数接口传递到函数体内部。根据变量种类的不同,有不同的参数传递方式:若传

5、递的参数是一个类对象(包括象 Int 和 float 这样的 C/C+内部数据类型),这种传递方式为值传递。C/C+这种以函数为主体的语言中,几乎所有的功能都是通过函数调用来实现的。 以下的 C/C+代码是如此的简单,可能你从未想过还有什么要分析的,但它确实是函数值传递方式的典型例子。float x = 0.254;float y = 3.1415;float z = x + y;以上代码编译执行时,第一步 float x,即声明一个实数变量。即将标志符 x 认为是一个实数变量,并调用float 类的初始化函数。当然你可能感觉不到它的存在,因为现在的 CPU 都直接支持浮点运算,它只是一条汇编

6、指令而已。初始化完成后,调用赋值函数:x.operator = (0.254); 不要奇怪以上函数的写法,它实际上与 x = 0.254; 效果完全相同,会产生同样的汇编代码。该函数首先根据变量 x 的数据类型分配合适的内存空间,并将该内存地址与标志符 x 关联。然后将立即数0.254 写入分配的内存。(这里借用汇编语言的术语,立即数可以理解为程序已指定的具体数值。) 然而,赋值函数的设计者并不能获知立即数 0.254 的数值,调用该函数时就必须通过参数传递的方法将数值通知给函数体。赋值函数接口大致是这样:float float:operator = (register float a);变量

7、 a 是在 CPU 寄存器中使用的临时变量。调用赋值函数时,将 0.254 送到寄存器变量 a 中,再将 a 值送到变量 x 所在的内存位置中。以上函数的返回值用于类似这样的链式表达式的实现:x = y = z;说了许多,好象十分复杂,其实赋值操作仅仅只是两条汇编代码:mov AX, 0.254 mov x, AX事实上,它之所以简单,仅仅是因为 float 是 CPU 能直接处理的数据类型。若以上代码中不是 float 类型数据赋值,而是更复杂的(比如说自定义 )类型数据,同样的赋值操作尽管是相同的步骤,但实际情况要复杂得多。因为寄存器容量限制,可能变量 a 无法作为寄存器变量存放,这样即使

8、是简单的赋值操作也要为函数的临时变量分配内存并初始化,在函数的返回时,临时变量又要析构(或者说从内存中释放) ,这也就是参数值传递方式的弱点之一:效率低。以后我们还可以看到,值传递方式还有其力所不能及的时候。上面的代码段中加法调用这样的函数,其参数传递方式同样是值传递:float:operator + (float a, float b);下面看一个稍微复杂的类,Complex 复数类。ObjectARX 程序设计中使用的大部份对象类型都将比这个类复杂。class Complexpublic:Complex operator = (Complex others); /赋值函数,事实上不声明系统

9、也会默认Complex operator + (Complex c1, Complex c2); /加法void Complex (float Re, float Im); /带参数的构造函数/当然,真正的复数类接口远比这复杂,为了说明问题,仅写出这三个接口函数。private:float Re; /复数的实部float Im; /复数的虚部 /类接口函数的实现应该并不复杂,在此略过。类的接口函数的参数仍然用值传递方式。当执行下列代码中的加法和赋值操作时,程序将要多次执行 Complex 类的构造函数和析构函数。Complex A(2.5, 3);Complex B(0.4, 2.5);Com

10、plex C = A + B;最后一句代码,首先声明一个 Complex 类对象 C,然后根据运算符优先级,执行加法运算,将对象 A,B传递给加法函数,这时 C+调用 Complex 类的默认构造函数声明两个临时变量,再调用默认的“拷贝构造函数”采用位拷贝的方法将对象 A,B 复制到临时变量,加法操作返回时,再将临时变量析构,返回值再用值传递方式传递给赋值函数。从以上执行过程可以看出,值传递方式效率低的关键在于临时变量的建立和析构。于是考虑,因为在调用函数时该变量已经在内存中存在,将这个已经存在的变量直接传递给函数体而不去声明和拷贝临时变量。这样,临时变量的构造、拷贝、析构等工作都被省略,从而

11、大大提高了函数效率。这便是使用 C/C+指针和引用传递机制的主要原因。另外,使用这样的函数参数传递机制,在函数体内部可以很轻易地修改变量的内容。(而使用值传递方式,函数体内部只能修改临时变量,没有办法修改这些外部变量本身的值。) 这样一方面增加了程序设计的灵活性,同时也给程序带来了安全隐患。当然,我们可以使用 const 声明防止变量的内容在函数体内部被修改,但这需要编程者有良好的编程风格和编程习惯。在介绍函数参数的指针和引用传递方式之前,先说一说指针和引用这两个概念。2、指针和引用在解释指针和引用之前,先看看普通变量是怎样在内存中存放的。声明变量后,编译程序要维护一张包括各种标识符的表。在这

12、张表内,每一个标识符,比如说变量名都应该有它的类型和在内存中的位置。在这要进一步说明几个问题,这些问题可能涉及多个计算机专业领域,我也不想在这作深入介绍,看不明白没有关系,不会影响您继续阅读这篇文章。首先,C/C+的内存分配有静态分配和动态分配两种机制。静态分配内存是由编译程序为标识符分配固定的内存地址,而动态分配机制是应用程序在进入内存后再根据程序使用内存的实际情况决定变量存放地址。这个话题非常复杂,不过进行 ObjectARX 程序设计好象不必太在意内存分配机制,让编译程序和 Windows 去管这件事吧。而且内存分配机制对于我们理解指针和引用不会造成影响。其次,标识符可以标识变量,也可以

13、标识函数入口。从而它的类型可以是 CPU 能直接处理的内部数据类型,也可以是用户自定义类型,还可以是函数类型。另外,由于标识符的类型不同,它占用内存的大小也各有差异。 “在内存中的位置”实际上指的是它占用的内存块的首地址。对于 80286 以上的计算机 ,内存地址由基址( 或段地址)加上偏移地址组成。基址是应用程序被调入内存时由操作系统分配,当然,编译程序把应用程序编译成多个段,从而要求操作系统对于不同的段分配不同的基址。而编译程序(哪怕是使用静态地址分配) 只能决定标识符存放的偏移地址,也就是说, “在内存中的位置”只是标识符占用内存的第一个字节的偏移地址。说了这么多,有一点需要记住,无论是

14、程序设计者还是编译程序都无法确知变量的内存中的实际位置。最后,这个标识符表要比上面说的复杂,我只选择了与目前讨论的问题有关的内容。好了,准备工作做了许多,让我们正式进入 C/C+指针和引用的神秘世界。指针变量其实质类似一个 int 整型变量。我们在源程序中这样声明一个指针变量:float *px;此时,标识符 px 指示的内存位置上存放的就是一个 int 类型整数,或者说,通过变量 px 可以访问到一个 int 类型整数,并且这个整数与指针指向的数据类型无关。在 ARX 程序中,甚至可以用这样的方式打印一个指针变量:acutPrintf(“指针变量 px 的值为%d”, px);当然,这个整数

15、值到底意味着什么,可以只有计算机(或者说操作系统) 自己知道,因为这个值表示的是指针指向的数据在内存中的位置。也就是说,不应该将指针变量与普通 int 整型混淆,例如,对指针进行四则运算将使用结果变得计算机和程序员都无法理解,尽管编译器允许你这样做。与普通变量不同,若在程序中声明指针变量的同时不进行初始化,系统会自动将指针变量初始化为 NULL。而声明普通变量,系统仅为其分配内存,而不做自动初始化,从而未初始化的变量值是不可预测的。当然,直接使用未初始化的指针决不是一个好程序(此时编译器会发出警告信息) ,其危害或隐患以后在说明内存管理技术时再讨论。在声明时初始化指针变量可以这样:float

16、*px = 0.254;这是初始化同时赋值,也可以使用 new 运算符进行初始化:float *px = new float;这种初始化方式经常用于不方便或不能直接赋值的复杂数据类型。上述语句执行时,首先分配一块可存放数据的内存区域 ,若要同时赋值,就调用赋值函数将数值写入刚分配的内存中。然后为标识符 px 分配一个 int 整型要占用的( 通常为 4 字节)内存空间,最后将分配的用于存放数据的内存首地址写入内存。注意:使用 new 运算符初始化指针,指针变量使用结束后应该用 delete 运算符释放其占用的内存。也就是说,new 运算符和 delete 运算符最好能成对使用。指针的初始化可以

17、在程序的任何位置进行, 比如:float x = 0.254;float *px;/其它语句,请注意不要在这里使用 px 指针px = /在这进行 px 指针的初始化工作上面最后一行代码是将变量 x 的地址赋值给指针 px。以上初始化指针的方法效率相差无几,读者可自行分析。/下面看看如何访问指针变量及复杂类的接口函数。pLine-setColorIndex(1); /将线对象的颜色设置为红色/layer()函数返回一个指向 char 类型的指针,先声明一个指针变量用于接收返回值char *pLayerName;pLayerName = pLine-layer(); acutPrintf(“n

18、红色的线对象在%s 图层上。 ”, pLayerName);这段代码不作深入分析,请读者注意 pLine 指针的声明和初始化过程以及 pLayerName 指针的赋值过程。注意到我们在上面初始化 px 指针时使用了int 编译程序编译以上代码时,在标识符表中添加一个 int 引用类型的标识符 m,它使用与标识符 n 相同的内存位置。这样对 m 的任何操作实际上就是对 n 的操作,反之亦然。注意,m 既不是 n 的拷贝,(这样的话,内存中应该的两块不同的区域,存放着完全相同的内容。),也不是指向 n 的指针,其实 m 就是 n 本身,只不过使用了另外一个名称而已。尽管指针和引用都是利用内存地址来

19、使用变量,它们之间还是有本质的区别:首先,指针变量(包括函数调用时的临时指针变量 )在编译和运行时要分配相当于一个 int 变量的内存空间以存放指针变量的值,尽管这个值表示的是指针指向的变量的地址。而引用与普通变量一样,标识符所指示的内存位置就是变量存放位置。这样不仅不需要在内存中分配一个 int 变量的内存空间( 尽管它可能微不足道),而且在使用中可以少一次内存访问。其次,由于标识符表在填写后就不能再被修改,因此引用在创建就必须初始化,并且初始化后,不能改变引用的关系。另外,引用初始化时,系统不提供默认设置,引用必须与合法的内存位置相关联。而这些特征对于指针而言都是不存在的。指针可以在程序任

20、何时刻初始化,初始化的指针在程序中也可以根据需要随时改变所指向的对象,(这只需要改写指针变量的值就可以了。 )当然,未初始化的指针变量系统会初始化为 NULL,而 NULL 引用是非法的。下面看一段类似文字游戏的程序:int I = 6;int J = 8;int /K 是 I 的引用K = J; /K 和 I 的值都变成了 8注意,由于引用关系不能被修改,语句 K = J;并不能将 K 修改为对 J 的引用,只是修改了 K 的值。实际上,声明并初始化引用后,可以把引用当作普通变量来使用,只不过在操作时会影响另外一个变量。以上代码仅仅只是解释引用的定义,并不能体现引用的价值。引用的主要功能在于

21、函数的参数(或者返回值)的传递。此主题相关图片如下:3、 函数参数的指针和引用传递机制先看一下简单的例子。void Func1(int x) /这个函数的参数使用值传递方式x = x + 10;/当参数类型更复杂时,指针和引用传递方式在效率等方面的优势更为明显/不过那样例子就不够“简单”了void Func2(int *x) /这个函数的参数使用指针传递方式*x = *x + 10; void Func3(int 以下代码调用这些函数:int n = 0;Func1(n);acutPrintf(“n = %d”, n); / n = 0Func2(acutPrintf(“n = %d”, n)

22、; /n = 10Func3(n);acutPrintf(“n = %d”, n); /n = 20以上代码段中,当程序调用 Func1()函数时,首先在栈(Stack)内分配一块内存用于复制变量 n。若变量 n 的类型复杂,甚至重载了该类的默认拷贝构造函数:CMyClass(const CMyClass 这个过程可能会比较复杂。程序进入函数 Func1()体内后,操作的是栈中的临时变量,当函数结束 (或者说返回)时,栈内变量被释放。而对于函数 Func1()来说的外部变量 n 并未起任何变化,因此随后的 acutPrintf 函数将输出 n = 0。程序调用函数 Func2()时,在栈内分配

23、内存用于存放临时的指针变量 x。然后用temp = *x;*x = *y;*y = temp;return true;以下代码调用该函数:int *a = 10;int *b = 15;if (swap(a, b)acutPrintf(“整数 a,b 已交换数据,a = %d, b = %d”, a, b);在以上代码中,swap()函数设计成与常见的 ARX 函数一致的风格,用一个 bool 类型返回函数执行状态。在调用函数时,由于变量 a 和 b 已经声明为指针,使用标识符 a 和 b 访问的是 int 类型变量的内存地址。使用引用传递参数,可以这样设计 swap()函数:bool swa

24、p(int temp = x;x = y;y = temp;return true;使用代码 swap(int a, int b)调用以上函数时,进入函数体内,x、y 分别是变量 a、b 的引用,对 x、y 操作就是操作变量 a、 b。函数返回后,变量 a、b 的值互相交换了。注意:以上代码只是交换两个变量(或者指针指向的变量) 的值。即将变量 a、b( 或指针 a、b 指向的变量)的修改为 b、a(或指针 b、a 指向的变量)的值,而不是将指针 a 指向原来指针 b 指向的变量。也就是说,swap()函数调用前后,指针 a 和 b 的值( 地址)并没有发生任何变化。 (当然,引用关系在任何时

25、候都不能修改。)要修改指针的地址值,应该使用指向指针的指针或者使用对指针的引用。这样设计和调用函数:bool swap(int *x, int *y); /使用指向指针的指针传递参数int *a = 10;int *b = 15;swap(或者:bool swap(int * /使用对指针的引用传递参数int *a = 10;int *b = 15;swap(a,b);在以上的两个 swap()函数以交换两个指针的值,使指针 a 指向原来指针 b 指向的变量,指针 b 指向原来指针 a 指向的变量。另外,由于引用关系不可修改,指向引用的指针和引用一个引用没有实际意义。若编译器允许它们存在,实际

26、上也会退化为普通指针(或对指针的引用 )和引用。这一点请读者自行分析。最后,我们看一个 ARX 程序中使用指针和引用传递参数的函数例子:AcDbDatabase *pDb = new AcDbDatabase();AcDbBlockTable *pBlkTbl;pDb-getBlockTable(pBlkTbl, AcDb:kForRead);从 ARX 帮助中可以查看到,getBlockTable() 函数的原型是:Acad:ErrorStatus getBlockTable( AcDbBlockTable*其中可以看到,函数的第一个参数是对一个 AcDbBlockTable 类型指针的引用,从而可以在函数体内部对指针 pBlkTbl 进行修改,使之指向 pDb 指针指向的图形数据库的块表。

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 企业管理 > 管理学资料

本站链接:文库   一言   我酷   合作


客服QQ:2549714901微博号:道客多多官方知乎号:道客多多

经营许可证编号: 粤ICP备2021046453号世界地图

道客多多©版权所有2020-2025营业执照举报