C++函数

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 0
static 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以及宏定义实现复杂的符合表达式
当然这些表达式往往让人理解起来很困难。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值