C++函数

来源:互联网 发布:平顶山java培训班经验 编辑:程序博客网 时间:2024/06/06 12:35
1. 函数(方法),是程序最小的算法组织单元,将程序一个大的算法进行分解成一个个函数的小算法叠合而成,不管什么程序,程序的执行过程都是函数相互嵌套调用的过程。在面向对象的语言中,本质还是函数和方法,类只是一种以对象进行管理和编程的手段。     2. 程序分解成函数的好处   (1)代码重复利用(2)节省存储空间(3)便于模块设计,提高开发效率但是不能将算法无休止的分解成为各种函数模块,注意把握一个度,不然不但没有提高编程效率,反而还较低了效率。3. 全局函数和成员函数(方法)(1)c中,所有的函数都是全局函数,全局函数涉及链接域的问题(2)c++中,包含了C中的全局函数(有链接域),也包含了类中的成员函数本章后续讲解中提到的函数,默认为全局函数。   4. 函数的定义和函数原型声明(1)函数定义     (1)函数头:函数名尽量以动词开头,表明函数动作    (2)函数体(2)函数声明函数头;    (1)在函数原型中尽量包含参数名,参数名能够起到很好的说明作用,在编程    风格良好的代码中,函数原型中的的参数名称具有极强的说明作用。    (2)函数定义本身也是一种声明,与变量的声明类似,也是有关强弱符号的问题,    主要就是为了在程序进行链接的时候,便于函数链接域的扩展。    (3)函数声明尽量放在头文件中,头文件的作用就是用于暂存各种声明        (1)预处理类的声明        (2)各种自定义类型的声明:结构体,联合体,类        (3)各种函数声明        (4)内联函数的定义        (5)全局变量的声明(3)C语言中,不能有同名的全局函数,但是在C++中,可以有同名的函数,只要参数    列表不同即可,在c++中,域名+函数名+参数列表来表示一个唯一的函数签名。5. 参数和变元(实参)(1)c与c++中函数调用    传参和返回值都允许进行隐式的强制转换,但是很有可能会出现数据丢失的    情况,所以要求尽可能将实参与形参的类型进行统一,必要时可以使用()    和reinterpret_cast<>()等强制转换符号将实参进行显示的转换。(2)函数参数传递的几种情况    (1)普通值传递(C)        (1)好处,防止修改          (2)缺点,传递结构体和数组组合类型等效率低    (2)特殊值传递(指针或地址)(C)        (1)好处效率高,函数可以修改变元(实参),对于数组/结构和对象            来说,很合适使用指针传递        (2)缺点,可能不小心会修改实参,这个时候就需要const进行修饰        (3)指针传递中经典案例:一维数组和多维数组的传参    (3)使用引用进行传参(c++才有的方式)        (1)效率最高的传参方式,实参和形参是同一个,不会复制,没有副本空间        (2)引用的负面问题            传递引用时实参的写法与传递一个普通值得写法相同,导致调用该函数时            很难区分实参的值会不会修改,非常不利于程序代码的清晰阅读。        (3)如果函数形参是非常量引用的话,不允许实参是常量(或者是const,或者是这类的数值常量(宏常量))        (4)使用引用进行传参是为了提高了效率,如何防止实参被修改的情况呢?使用const,如果形参            是const引用,实参为常量或者变量都可以        (5)引用只能被初始化,不能被赋值,而且编译器要求引用必须被初始化。传递引用,其实就是初始化引用,            每次函数被调用时,引用类型的形参都会被重新初始化。(3)指针和引用    (1)引用的效率会高于指针    (2)不管是使用引用还是使用指针,只要不涉及修改实参的操作,就一定要使用const进行限制    (3)指针类型的形参是一个新的变量空间,使用时需要使用*解引用(寻找指向的空间)。        引用只是一个别名,形参和实参是同一个变量空间,使用时不需要解引用。    (4)指针可以为NULL,但是引用一般情况不许为NULL(除了NULL的引用),因此使用        指针前,最好是对指针先进行是否为空的判断,这在前面讲指针的时候特意提到过。        特别是在返回指针做回左值时,一定要保证指针的有效性。6. main函数(1)main函数被谁调用的被启动代码,在编译时,编译器会自动加入启动代码,启动代码有系统(OS)加载器调用。(2)main函数的两个参数main函数的两个参数用于接收命令行输入的参数,在windows或者ubuntu下输入命令行时,有输入参数的需求,所以我们应该理解,执行的各种命令其实就是一个可执行程序。命令行执行程序时,至少有一个参数,那就是程序名自己。(1)argc:argument count,命令行输入的参数个数(2)argv:argument vector,一个字符串指针数组,存放命令行输入的参数(字符串)的地址测试用例    using namespace std;void fun(const int &argc, const char ** &argv) {        //for(int i=0; i<argc; i++) {        for(int i=0; argv[i] != NULL; i++) {              cout<< argv[i] << endl;        }   }int main(int argc, char **argv){        fun(argc, argv);        return 0;}7.给函数指定默认参数值注意,在c中不支持默认参数设置。(1)形参的给默认参数值从右边向做左边逐个给值,要求连续,不能跳跃(2)如果函数只有定义没有声明,就将默认参数值盖在定义中,如果有函数声明    就将默认参数值给在函数声明中,定义中就不要再给默认值。(3)默认值的好处是,如果不给传参时,函数可以使用默认数值,这个在c++中的构造函数中经常使用这样的操作。(4)当使用默认形参参数值时,调用者可以从右向左连续省略部分函数参数。(5)形参为引用时,也是可以为其指定默认值int  aa = 200;void fun(int a, int b=10, int &c=aa) {        printf("a = %d, b = %d\n", a, b);               printf("c = %d\n", c);}int main(void){        fun(1);        return 0;}8. 函数返回值(1)如果返回值不是void,必须要使用return语句,否者可写可不写。(2)函数返回值使用情况    (1)可以舍弃    (2)如果返回的是一个普通值(非指针和引用类的值),都作为左值使用    (3)如果返回值为一个引用或者指针时,可以作为左值使用,也可以作为        右值使用,但是指针作为右值使用时,必须解引用,而且指针一定        不能为空。如果返回的指针和引用是常量时,注意是不能进行赋值修改。测试用例(3)函数返回值必须注意的情况    (1)不能返回自动局部变量,当然相反就可以返回静态局部变量    (2)不能返回自动局部变量的引用,相反可以返回静态局部变量的引用测试用例返回一个字符串指针和返回一个局部字符串数组9. 在函数中返回分配的自由存储空间(手动存储区)的地址(1)这个在讲链表的节点创建时,已经使用过了。10. 全局函数函数链接域(作用域)的问题    类的成员函数在讨论范围外,这里只讨论全局函数。(1)默认定义函数时前面都有一个extern关键字,表示函数是外链接的,表示作用域扩展到了    其它的文件中,但是其它的文件在使用时,需要进行声明,函数定义是强符号,声明是弱符号,    在相同的命名空间中,强符号只能有一个,声明(弱符号可以有多个),所以在整个全局    命名空间中,函数定义只能有一个,声明有多个,将强弱符号统一的过程就是链接函数的过程。(2)将extern改为static修饰函数    当c或者c++程序实现的程序超过一定体量之后,如果我们将所有的全局函数都命名在全局命名    空间的话,换句话说让全局函数都具备外链接属性时,不可能不会遇到命名冲突的函数,那怎    么办呢,我们往往都会将那些不需要在其它文件的被访问的函数使用static修饰,将其链接属    性改为内链接,这样一来开,即便是其他文件的名字与本文件某个static修饰的文件的名字冲    突了,也不会有任何问题。    对于全局变量来说也有着与函数一样的情况,这一点在后面继续讲解。11. 内联函数(1)函数的问题    实际上函数的调用的开销是比较大的,主要是需要开辟函数栈空间存放函数状态和返回地址,    当然还有各种局部变量,但是当一个函数不涉及循环,而且代码量非常短,在5句话以内,    而且函数调用还非常的频繁,这样的函数调用的效率是很低的。(2)早期使用带参宏定义来解决前面提到的问题#include <iostream>#include <string>using namespace std;#if 0static int compare_fun(int a, int b){        return a>b ? a : b;}#endif#define compare_fun(a, b) ((a)>(b)?(a):(b))int main(int argc, char **argv){        cout << compare_fun(10, 6) << endl;        return 0;} 宏定义之所以能够解决这样的问题的原因是,宏展开后,代码直接被填充在了宏被调用的位置,实际上不存在函数调用的开销。但是使用带参宏的问题是,宏定义不会对参数进行参数类型的检查,不利于代码排错(可以想见,在强类型的语言中,类型参数在编译检查时是多么重要)。(3)内联函数的出现为了解决上述矛盾,寄希望代码想宏定义一样实现替换提高效率,也希望像函数一样能够实现参数的类型检查,C语言在后来提出了内联函数,内联函数结合了函数和宏的双从特点。#include <iostream>#include <string>using namespace std;static inline int compare_fun(int a, int b){        return a>b ? a : b;}int main(int argc, char **argv){        cout << compare_fun(10, 6) << endl;        return 0;}(4)内联函数的一些需要注意的问题(1)c和c++都有内联函数(2)对于C和C++全局函数而言,只有给了inline关键字修饰后的才是内联函数。(3)但是在C++中,需要注意,如果成员函数直接定义在类的内部的话,不管使不使inline关键字的话    都是内联函数,当然编译器自己再做一次判断,该函数适不适合编译成内联函数。但是如果只    是将成员函数的声明放在了类里面,函数的定义放在了外部的话,只有给函数加上inline关键字    ,该函数才会变成内联函数,在后续的课程中还会再次讲这个问题。(4)内联函数一般都是写在头文件中    实际上多有的函数定义都可以写在头文件中,但是实际情况是,我们所有的全局函数的定义都是    定义在cpp文件中,只是将声明放在了头文件中,因为具有外部链接(A文件定义,B文件可以使用)      的函数在全局作用域中只能有一个函数的定义,如果将普通全局函数的定在了头文件中的话,会导    致同一个函数被多次定义的情况,编译器是不允许的。    总结:在同一个编译单元中,类/结构体等的类型定义,以及内联函数的定义,在同一个编译单元中只能    有一次,但是可以同时出现在不同的编译单元中。    但是对于普通全局函数定义和全局变量定义(强符号),在全局名中定义只能有一次,但是声明    可以有多个,也就是说除了允许在一个编译单元内重复外,也不允许在多个编译单元中重复定义。    例子1:    static inline int compare_fun(int a, int b)    {            return a>b ? a : b;    }    static inline int compare_fun(int a, int b)    {            return a>b ? a : b;    }    int main(int argc, char **argv)    {            cout << compare_fun(10, 6) << endl;            return 0;    }    例子分析:在这个文件中,内联函数多次定义出错    例子2:    a.h文件    #ifndef H_A_H    #define H_A_H    int compare_fun(int a, int b)    {            return a>b ? a : b;    }    #endif    a.cpp文件         #include <iostream>    #include <string>    #include "a.h" //------------------------    using namespace std;    int main(int argc, char **argv)    {            cout << compare_fun(10, 6) << endl;            return 0;    }    b.cpp文件    #include <iostream>    #include <string>    #include "a.h"//------------------------    using namespace std;    int fun()    {            cout << compare_fun(10, 6) << endl;            return 0;    }           编译:g++ a.cpp b.cpp    编译错误:multiple definition of `compare_fun(int, int)'    例子分析:    在本例中,比较函数int compare_fun(int a, int b)是一个全局作用域的函数(没有static限制),    而且将其定义写在了a.h头文件中。当a.cpp包含这个头文件时,a.cpp中会定义一个这个全局函数,    当b.cpp包含b.h这个头文件时,也会在b.cpp中也会定义一个这个全局函数。g++对这两个文件进行    编译链接时,整个程序中会出现来两份全局的compare_fun函数,我们前面讲过,在程序中是不允许    出现多个全局函数的重复定义的,因此会报错误。    因此对于全局函数来说,在整个程序中定义只能有一份,其它文件需要使用时,只需要包含其声明就可以了。    本例如何修改:    (1)方法1:int compare_fun(int a, int b){ ...... }函数改为static int compare_fun(int a, int b){ ...... }        将其链接域改为内链接,其不再是全局可见,这样一来,a.cpp和b.cpp中的compare_fun函数,是属于        各自文件范围内的函数,不会冲突,通过前面的讲解,这一点应该很好理解。    (2)方法2:将int compare_fun(int a, int b){ ...... }函数改为inline int compare_fun(int a, int b){ ......}        因为内联函数与普通的函数不一样,内联函数在整个程序中可以有多份定义,不会报错重复定义的错误,        但是必须注意的是,在同一个文件中,同一个内联函数的定义只能有一份,这在前面已经讲过了。12. 静态局部变量(1)(2)内存的各种不同的管理方式    (1)栈        (1)栈:也成为自动变量区        (2)用于开辟函数栈        (3)自动局部变量(普通类型的,类类型的对象)都是开辟与栈中            int fun() {                (auto) int a = 100;                 string str("hello");            }        (4)生命周期:函数栈的生命周期与函数执行的时间同,函数执行完毕,函数栈释放后,开辟于栈中的            变量空间就被释放        (5)作用域:自动局部变量,从定义的位置开始到函数结尾,不能通过声明去修改其作用域(2)堆    (1)也被称为自由存储区,或者叫手动存储区    (2)malloc和new所开辟的内存空间均来自于堆空间,需要使用free和delete释放        int fun() {            int a = new int;            int *p = (int *)malloc(sizeof(int));            string *str = new string("hello");        }    (3)生命周期:开辟空间开始到释放空间期间有效    (5)作用域:与指向对空间的指针的作用域有关,就看指针是全局的还是局部的(3)静态区    (1)静态数据区        (1).bss:开辟没有初始化的静态变量(全局变量和静态局部变量)空间,会自动初始化为0        (2).data:开辟初始化了静态变量空间(全局变量和静态局部变量)        (3).rodata:存放常量,比如字符串常量就是存放在这个区域中    (2)静态代码区            (1).text:存放代码12. typedef与函数之间(1)在真实的c语言实现的大型工程项目中,往往会使用typedef以及宏定义实现复杂的符合表达式当然这些表达式往往让人理解起来很困难。