c++ 学习笔记---函数

来源:互联网 发布:如何将ubuntu安装到u盘 编辑:程序博客网 时间:2024/05/17 22:55

c++ 学习笔记 --函数

2014.10.06

refer:

http://www.cnblogs.com/dzhanjie/archive/2011/07/07/2100340.html

如有错误,欢迎拍砖~


函数是程序员定义的函操作。


c++是编译型、静态强类型语言,在编译时会做类型检查。


编译型和解释型


我们先看看编译型,其实它和汇编语言是一样的:也是有一个负责翻译的程序来对我们的源代码进行转换,生成相对应的可执行代码。这个过程说得专业一点,就称为编译(Compile),而负责编译的程序自然就称为编译器(Compiler)。如果我们写的程序代码都包含在一个源文件中,那么通常编译之后就会直接生成一个可执行文件,我们就可以直接运行了。但对于一个比较复杂的项目,为了方便管理,我们通常把代码分散在各个源文件中,作为不同的模块来组织。这时编译各个文件时就会生成目标文件(Object   file)而不是前面说的可执行文件。一般一个源文件的编译都会对应一个目标文件。这些目标文件里的内容基本上已经是可执行代码了,但由于只是整个项目的一部分,所以我们还不能直接运行。待所有的源文件的编译都大功告成,我们就可以最后把这些半成品的目标文件“打包”成一个可执行文件了,这个工作由另一个程序负责完成,由于此过程好像是把包含可执行代码的目标文件连接装配起来,所以又称为链接(Link),而负责链接的程序就叫……就叫链接程序(Linker)。链接程序除了链接目标文件外,可能还有各种资源,像图标文件啊、声音文件啊什么的,还要负责去除目标文件之间的冗余重复代码,等等,所以……也是挺累的。链接完成之后,一般就可以得到我们想要的可执行文件了。 

上面我们大概地介绍了编译型语言的特点,现在再看看解释型。噢,从字面上看,“编译”和“解释”的确都有“翻译”的意思,它们的区别则在于翻译的时机安排不大一样。打个比方:假如你打算阅读一本外文书,而你不知道这门外语,那么你可以找一名翻译,给他足够的时间让他从头到尾把整本书翻译好,然后把书的母语版交给你阅读;或者,你也立刻让这名翻译辅助你阅读,让他一句一句给你翻译,如果你想往回看某个章节,他也得重新给你翻译。 

两种方式,前者就相当于我们刚才所说的编译型:一次把所有的代码转换成机器语言,然后写成可执行文件;而后者就相当于我们要说的解释型:在程序运行的前一刻,还只有源程序而没有可执行程序;而程序每执行到源程序的某一条指令,则会有一个称之为解释程序的外壳程序将源代码转换成二进制代码以供执行,总言之,就是不断地解释、执行、解释、执行……所以,解释型程序是离不开解释程序的。像早期的BASIC就是一门经典的解释型语言,要执行BASIC程序,就得进入BASIC环境,然后才能加载程序源文件、运行。解释型程序中,由于程序总是以源代码的形式出现,因此只要有相应的解释器,移植几乎不成问题。编译型程序虽然源代码也可以移植,但前提是必须针对不同的系统分别进行编译,对于复杂的工程来说,的确是一件不小的时间消耗,况且很可能一些细节的地方还是要修改源代码。而且,解释型程序省却了编译的步骤,修改调试也非常方便,编辑完毕之后即可立即运行,不必像编译型程序一样每次进行小小改动都要耐心等待漫长的Compiling…Linking…这样的编译链接过程。不过凡事有利有弊,由于解释型程序是将编译的过程放到执行过程中,这就决定了解释型程序注定要比编译型慢上一大截,像几百倍的速度差距也是不足为奇的。 

编译型与解释型,两者各有利弊。前者由于程序执行速度快,同等条件下对系统要求较低,因此像开发操作系统、大型应用程序、数据库系统等时都采用它,像C/C++、Pascal/Object   Pascal(Delphi)、VB等基本都可视为编译语言,而一些网页脚本、服务器脚本及辅助开发接口这样的对速度要求不高、对不同系统平台间的兼容性有一定要求的程序则通常使用解释性语言,如Java、JavaScript、VBScript、Perl、Python等等。 

但既然编译型与解释型各有优缺点又相互对立,所以一批新兴的语言都有把两者折衷起来的趋势,例如Java语言虽然比较接近解释型语言的特征,但在执行之前已经预先进行一次预编译,生成的代码是介于机器码和Java源代码之间的中介代码,运行的时候则由JVM(Java的虚拟机平台,可视为解释器)解释执行。它既保留了源代码的高抽象、可移植的特点,又已经完成了对源代码的大部分预编译工作,所以执行起来比“纯解释型”程序要快许多。而像VB6(或者以前版本)、C#这样的语言,虽然表面上看生成的是.exe可执行程序文件,但VB6编译之后实际生成的也是一种中介码,只不过编译器在前面安插了一段自动调用某个外部解释器的代码(该解释程序独立于用户编写的程序,存放于系统的某个DLL文件中,所有以VB6编译生成的可执行程序都要用到它),以解释执行实际的程序体。C#(以及其它.net的语言编译器)则是生成.net目标代码,实际执行时则由.net解释系统(就像JVM一样,也是一个虚拟机平台)进行执行。当然.net目标代码已经相当“低级”,比较接近机器语言了,所以仍将其视为编译语言,而且其可移植程度也没有Java号称的这么强大,Java号称是“一次编译,到处执行”,而.net则是“一次编码,到处编译”。呵呵,当然这些都是题外话了。总之,随着设计技术与硬件的不断发展,编译型与解释型两种方式的界限正在不断变得模糊。

动态语言和静态语言
  通常我们所说的动态语言、静态语言是指动态类型语言和静态类型语言。

(1)动态类型语言:动态类型语言是指在运行期间才去做数据类型检查的语言,也就是说,在用动态类型的语言编程时,永远也不用给任何变量指定数据类型,该语言会在你第一次赋值给变量时,在内部将数据类型记录下来。Python和Ruby就是一种典型的动态类型语言,其他的各种脚本语言如VBScript也多少属于动态类型语言。

(2)静态类型语言:静态类型语言与动态类型语言刚好相反,它的数据类型是在编译其间检查的,也就是说在写程序时要声明所有变量的数据类型,C/C++是静态类型语言的典型代表,其他的静态类型语言还有C#、JAVA等。

对于动态语言与静态语言的区分,套用一句流行的话就是:Static typing when possible, dynamic typing when needed。

强类型定义语言和弱类型定义语言

(1)强类型定义语言:强制数据类型定义的语言。也就是说,一旦一个变量被指定了某个数据类型,如果不经过强制转换,那么它就永远是这个数据类型了。举个例子:如果你定义了一个整型变量a,那么程序根本不可能将a当作字符串类型处理。强类型定义语言是类型安全的语言。

(2)弱类型定义语言:数据类型可以被忽略的语言。它与强类型定义语言相反, 一个变量可以赋不同数据类型的值。

强类型定义语言在速度上可能略逊色于弱类型定义语言,但是强类型定义语言带来的严谨性能够有效的避免许多错误。另外,“这门语言是不是动态语言”与“这门语言是否类型安全”之间是完全没有联系的。
例如:Python是动态语言,是强类型定义语言(类型安全的语言); VBScript是动态语言,是弱类型定义语言(类型不安全的语言); JAVA是静态语言,是强类型定义语言(类型安全的语言)。

c++中的代码 int num = 0.0;int num = "abcd"; 前者编译器帮你完成了自动转换,后者是编译不过的。自动转换自动转换发生在不同数据类型的量混合运算时,由编译系统自动完成;强制类型转换是通过类型转换运算来实现的。无论是强制转换还是自动转换,都只是为了本次运算的需要而对变量的数据长度进行的临时性转换,而不改变数据说明时对该变量定义的类型。

什么时候编译器会做自动转换

1.算数转换:将二元操作符的两个操作数转换为同一类型,也使表达式的值具有相同类型,基本都是类型提升(short转为int,int转为double);

2.指针转换,指任何类型对象的指针都可以转化为void*、整形0可以转化为任意指针类型、指针与算数值都可以转化为bool、非const对象初始化const对象、非const对象的指针转化为const对象的指针、标准库类型到内嵌类型(标准输出输入到bool等)。

3.类类型转换

函数调用时不会发生实参到形参的自动转化(函数重载另算)。


c++函数可以声明多次:函数返回值、名字、形参表相同,那么是函数的多次声明;如果名字与形参表相同,而返回值不同,编译器会报错,编译器会认为这是函数重载,而不是一个函数的多次声明。


c++中名字查找发生在类型检查之前。


参数传递:

指针是抽象的,是c语言帮助我们做寻址的,对于机器来说指针是透明的,指针只有程序员才能看懂。例如int *a;a只是一个符号(名字)对我们来说符号a代表一个地址,而编译器将符号a映射为一个地址,在汇编中是不存在a这个符号的。源码转化为汇编后可以看到,在堆栈里,我们理解的指针(地址)只是相对于栈顶的偏移(offset),而不是实际的虚拟寻址空间的一个地址;在堆中,指针(地址)则是虚拟寻址空间的一个地址(并不是物理地址,虚拟地址空间到物理地址空间存在一个映射,其翻译由操作系统完成)。

函数调用时,实参就是对函数形参的初始化。声明变量时直接赋值就叫初始化,分两种,直接初始化和复制初始化,函数实参对形参的初始化属于前者。

非引用形参表示对应实参的局部副本。A()调用B()时,在函数堆栈里,向B()传递的实参会被复制到B()的栈帧,因此无论如何B()也不可能修改A()中传入的实参。

指针形参也一样,指针本身不会发生改变,但是指针指向的内容可以改变。函数形参是const 指针时,函数调用可以传入const指针,也可以传入非const指针。这是因为c++继承c用语言的机制,对指针初始化时,可以将指向const对象的指针初始化为指向非const对象的指针,但是不可以将指向非const对象的指针初始化为指向const对象的指针,防止通过指向非const对象的指针修改const对象。举例:

const int *a; 

int *b;

a = b;//正确

b = a;//error 编译不能通过

函数指针形参最好声明为const类型。


const类型的非引用非指针形参,函数调用时,既可以传入const实参,也可以是非const实参;非const类型的非引用非指针形参,函数调用时,既可以传入const实参,也可以是非const实参。这是因为可以使用const对象初始化非const对象,也可以使用非const对象初始化const对象。

如果两个函数声明返回值、函数名字相同,而形参表相同,只是其中一个函数的一个或多个形参是const类型的,那么编译器会认为这两个函数是相同的试一次函数多次声明(不会报错),而不会认为是函数重载;但如果是两个函数声明返回值、函数名字相同,而形参表相同,只是其中一个函数的一个或多个形参是const类型的,编译器会认为这两个函数定义是相同的,会报错(多次定义,redefines xxx)。举例:

void test(int num){};

void test(const int num){};//编译报错,fefines to test(int)

这是c++为了支持对c语言的兼容,在c语言中const形参与非const形参函数对于函数定义来说无区别,调用时依然遵循不允许修改的原则。


当需要改变实参的值、实参很大、没办法赋值实参时,要使用引用及指针类型的形参。函数只能返回一个值,当需要返回多个值时,使用指针、引用形参。

当传递int、short、char小对象且无需修改其值时,使用复制实参(在64位系统中指针占64位,而int是32位,传递int比传递int*好,而引用也是通过指针实现的,因此类似)。


传递指针的引用:

void test(int *& a);

对int *&a的定义应从右到左理解,a是个引用,与指向int的指针关联。

如果写成int &*a编译器会报错,无法声明指向 ‘int&’ 的指针。


数组形参:

void test(int *a);

void test(int a[]);

void test(int a[10]);

以上三个函数声明是等价的,编译器会将其理解为void test(int *a);也就是说数组形参(即使是声明了数组的大小)会被转为为指针,在汇编中可以看到,编译器会将我们的对数组的访问翻译成指针的计算,例如:我们代码写成a[8],我们要访问数组的第九个对象,实际翻译成汇编后a[8]代表a+sizef(int)*8。

特别需要注意,即使在函数形参中设置的数组的大小,编译器因为将其理解为指针类型形参,所以不会帮你做数组的边界检查,例如:

void test(int a[10])

{

cout  << a[100] << endl;//编译器不会报错

这是最好使用引用数组形参,引用数组形参不会将形参转化为指针,而是传递数组本身,这时编译器会帮你做边界检查,例如:

void test(int (&a)[10])

{

cout  << a[100] << endl;//编译器会报错

}

&a两侧的()是必须的,因为[]比引用由更高的优先级。

c++能用引用就不用指针,因为引用更安全更自然(更容易被人[不是机器]理解)。


多维数组的传递:

非引用数组形参的类型检查只是确保实参与形参类型一致,但不会检查实实参的大小。

void test(char num[][10]);

void test(char (*num)[10]);

以上两个函数等价。第二个函数中的(*num)括号不能省略,因为num是一个指向char[10]的指针,而char *num[10]中的num是一个指针数组。


传递给函数的数组如何保证不越界:

1.数组本身设置设置一个标记来检测数组的结束,如c风格字符串以\0结尾;

2.向函数传递数组第一个与最后一个元素的指针(标准库就是这么做的);

3.显示传递数组的大小(c风格)。


return语句分两种:

return;

return expression;

第一种只能在返回值为void类型的函数中使用,当然void返回值的函数可以不写return,编译器会帮你加上这一句。

main函数是个例外,虽然返回值不是void类型的,但是可以没有return语句,编译器会默认加上return 0。main函数返回值可以表示程序运行结束的状态,需要包含c++库头文件<cstdlib>,然后可以使用c++定义的两个预处理变量EXIT_FAILTURE与EXIT_SUCCESS。


函数调用时会在调用函数处创建一个临时对象(temporary)用来保存函数调用的返回值。


千万不要返回局部对象的引用或指针:

因为在函数调用结束时,该函数的栈帧一般是会被操作系统收回的,因此原来保存局部对象的地址处现在不知道保存什么值了。


引用返回一个左值,例:

char &test(string &str)

{

return str[0];

}

可以这样调用函数:test(str) = ‘A’;因为函数返回值是个引用,因此可以函数返回值赋值。


函数声明包括:函数返回值、函数名、形参列表。


默认实参,例:

void test(int num = 10,char ch = 'A');

函数调用时可以使用如下方式:

test();//使用默认实参10,A

test(int xxx);//使用参数xxx与A

test (‘B’);//使用参数‘B’的ascii值,A

函数调用的实参按位置解析。

默认实参也可以是表达式。

可以再函数声明或函数定义中指定默认实参,但是在一个文件中,只能指定默认实参一次。


内联函数可以避免函数调用的开销,包括寄存器保存调用函数的状态、回复调用函数的状态、实参对形参的赋值等。内联函数定义最好放在头文件中。


类成员函数:

编译器隐式地将类内定义的成员函数当做内联函数。


类成员函数的形参的第一个是调用该函数的类对象的指针。


const成员函数是对普通函数的重载,因为形参表不同,其形参this*是const类型的对象指针,编译器可以通过实参是否为const来确定调用那个函数。这与非类成员函数不同。

const类对象、指针、引用只能调用const成员函数。


出现在相同作用域的两个函数,如果有相同的名字不同的形参表,则称重载函数。

函数重载的三个步骤:

1.确定候选函数,即函数名相同的函数;

2.从候选函数中选择可行函数,可行函数是函数形参与函数调用的实参个数相同,且类型匹配(或可悲隐式转换);

3.寻找最佳匹配。匹配原则为:至少有一个实参的匹配优于其他可行函数、每个实参的匹配不劣于其他可行函数。如果找不到最佳匹配,编译器提示错误,该调用具有二义性。


函数指针:

typedef void* (*test)(int num);

test是一个函数指针,指向返回值为void*、参数为int的函数。

c++允许使用函数指针指向重载的函数,指针的类型必须与重载函数的某个版本精确匹配,例:

void test(double);

void test(int);

void (*func_ptr)(int) = &test;//func_ptr是指向test(int)的指针。











0 0