前面一篇我们介绍了结构体,这篇终于能够介绍函数了。为什么这么说呢?因为函数非常重要。就这么简单。嘿嘿!之所以在这时才讲函数,是因为本篇将联系到前面的每一篇,这样函数才能体现的透彻。那我们就迫不及待的切入正题。

 

从第一篇Helloworld开始到现在,就没有脱离函数。那就是我们的main函数。main函数也是一个普通的函数,只不过通常把它作为我们写的程序的入口。也就是说我们就当它最先执行。那这样一来为什么说它又是一个普通的函数呢?原因是我们可以通过写代码改变这个入口。让我们的程序一开始不执行main函数而先执行我们自定义的函数。具体怎么实现不是本篇的内容,大家知道有这么回事便可。记得main函数并不是一个特殊的函数,它只是被认为的定为程序的入口函数而已。

 

那么,什么是函数?通俗的理解,它就是一段代码块,被我们将零散的语句集中在一起而用于支持某个功能。比如我们的strcpy也是一个函数,这个函数的作用是字符串拷贝。它里面有很多语句。这些语句被用一个函数的形式集中在一起而已。说到这里又不得不强调一点,那就是我们在接触一个新的东西的时候尽量往其本质想,这样便不会感到抽象和陌生。就比如函数,我们就理解它就是一个代码块集中管理的方案。一个函数名,参数列表加返回值用大括号将代码括起来就成了函数。虽然是括起来了,但是函数可以说是不存在的。当编译器将我们的CC++代码编译成汇编语言的时候,每个函数都只是一段代码,什么函数名,参数列表,返回值将不再清晰可见。那就是一段集中在一块儿的代码。我们也就这么理解。至于为什么在CC++语法上函数要有名字、参数、返回值。这点是可以理解的。因为是高级语言嘛,这样一来代码的模块性将很强。总不可能这样写哟:

有函数:

void fun( void )

{

     int a = 0;

}

 

void fun1( void )

{

     int b = 0;

}

 

int main( void )

{

      if ( ... )

         fun();

      else

         fun1();

     

       return 0;

}

 

没函数:

int main( void )

{

      if ( ... )

         goto fun;

      else

         goto fun1;

     

      return 0;

 

   

      fun:

            int a = 0;

            return 0;

     

      fun1:

            int b = 0;

            return 0;

}

 

上面的代码显而易见,函数的基本作用就得以体现了。模块化方便管理与维护。

 

知道了函数的概念及其本质后,我们再看具体的一些用法。首先从返回值上面说。

void         fun();

int*         fun();

struct A   fun();

int           fun();

char        fun();

看到上面不同返回值的函数。有指针,有类型,有空,有字符,还有结构体。C语言只能返回一个值,如果想返回多个,就只能用地址的形式返回给调用者了。其它花骚的办法原理也都差不多。这里不一一说明。函数的返回值在CC++层面上都是用return关键字进行返回的。返回值是为了能够被需要一些结果的调用者获得这个结果值。返回值在很多时候非常重要。void类型就不用返回,如果想在函数中间某处就返回的话可以这样:

void fun()

{

    int a = 0;

    return;          // 执行完a = 0就直接返回了,这里不返回任何值,只是充当一个结束此函数的作用。

    a = 10;

}

函数一旦return后,不管是否有返回值,都立即结束。因此,return通常用来返回值也用来终结函数。上面诸多返回值类型,我们只需要看两个就够了,一是返回指针,而是返回结构体。我们将一一追究起本质和一些注意事项。

 

首先看返回指针:

int* fun( void )
{
    int a = 100;
    return &a;
}

看上面这个程序,返回指针的形式很简单。直接return a变量的地址。在外层调用的时候可以:

int*  p = fun();

这样便把p指向了函数返回的地址上。

 

假如我们要返回一个数组,在前面我们讲了指针和数组。我们又知道不可能有 int[] fun( void )或者 int[ 3 ] fun( void )这样的函数定义。那么我们便联想到了指针和数组的共性,我们是否可以返回一个数组的首地址?然后再调用者取得这个首地址。在我们知道数组大小的情况下就能挨个访问这个数组的每一个元素。有的人又会问,假如不知道大小了怎么办呢?不知道大小基本是不可能的。你的大小是否可以使用宏定义或者全局变量呢?我们为何要往死胡同里面钻呢?对吧。

因此,就有如下代码:

int* fun( void )

{

    int a[ 3 ] = { 1, 2, 3 };

    return a;

}

在外层:

int* pArray = fun();

a = pArray[ 0 ];

是不是很方便。所以我们在使用指针和数组的时候要灵活。C语言虽然不能返回多个值,但是我们有办法实现这个功能。

 

写到这里,大家知道了返回指针,欣喜若狂。运行之。结果意料之外的事情发生了。为什么返回的指针、数组元素乱了?数据错误了?

这里就牵涉到一个注意事项了。

 

我们上面写的这两个返回指针的函数,都是有问题的。说它是错误的也完全不过分。

为什么这么说呢?大家仔细观察,我返回的数组和返回的a的地址都是属于临时变量的地址。语法上这两个函数确实没有问题,错误的原因就在于我返回了临时数据的内存地址。所谓临时变量,也就是生命周期比较短,这里的数组和a在函数结束后生命便终结了。所以称之为临时变量。既然生命终结了,那么这块内存将会被重新利用。就会被任意代码或者操作重新赋值。这里就是所谓的栈内存。这里的栈不是数据结构里面的那个栈。这里通常指存放临时变量的内存空间。一般很小,默认是1MB,也有2MB的。这个可以自己设置。这里就不多说了。假如有这样一段代码:

int* fun( void )
{
    int a = 100;
    return &a;
}

int* fun1( void )
{
    int b = 200;
    return &b;
}

 

int main( void )
{
    int* p;
    int* p1;
    int aa, bb;

 

    p = fun();
    p1 = fun1();

 

    aa = *p;
    bb = *p1;

 

    return 0;
}

 

在我的机器上,这两个函数fun和fun1由于代码基本相似。我故意构造了一个能够体现栈内存被修改的错误。在这个程序结束后,aa和bb的值都是200。为什么?原因很简单,我们在调用了fun函数后,p指向的栈内存比如是0x0012ffd4,当调用了fun1后,因为fun1跟fun区别很小,临时变量b所在的栈内存地址刚好也被指定到了0x0012ffd4这个内存地址上。p1也便指向了这个内存地址。所以这里aa和bb必然是相同的值了。为什么是200原因也很简单,临时变量b把0x0012ffd4这个内存地址下的值赋值成了200,便覆盖了之前的100。

 

那么,如果我要改变这两个函数,让它们不会出错该怎么办呢?如下:

int* fun( void )
{
    int* a = ( int* )malloc( sizeof( int ) );

    *a  = 100;


    return a;
}

这样的话就不存在被覆盖了,大家知道这里使用的malloc函数申请的空间,此函数申请的空间将不在栈空间上,而是在堆内存中。我们不手工调用free函数,这个内存值将永远存在。知道程序结束被回收。当然这样做的的话,在外层获得了这个a指针,在使用完后。记得把它free调。不然将造成内存泄露(一直申请,用完不释放,内存被占用逐渐耗尽。)。

 

问题一:写出正确的返回数组的函数fun1。

 

在了解了指针返回后,可能有的朋友会提问假如我要返回二级指针该怎么写呢?我这里只说一句,二级指针也是指针,没有什么特别的。跟以及指针同样一个道理返回,记得一点指针变量也可以是临时变量。具体还不清楚的话建议看看前面两篇关于指针的文章。

 

好了,返回指针说完了,再来说返回结构体。

大家由于看了上面的返回指针,心里可能就会在猜想了。结构体以一组成员的集合,跟数组类似。我们要返回的时候,是不是也必须得用指针的方式返回首地址呢?或者还有其它方法?先看程序:

struct A
{
    int a;
    int b;
    int c;
};

 

struct A fun( void )
{
    struct A a = { 1, 2, 3 };
    return a;
}

 

int main( void )
{
    struct A ret = fun();

    return 0;
}

 

有这样一段代码,我们的目的是想返回临时的结构体变量a的值给main函数里面的临时变量ret。这里我故意强调了临时变量这个词。希望不要引起大家的误解。这里虽然a是一个临时变量,但是我返回变量a到ret中,并不是指向。而是拷贝。意思就是说将临时变量a的3个成员值拷贝到ret变量的对应的成员里。跟指针是有区别的。我们前面说了C语言是不能返回多个值的,要返回就用指针。那么这里我没有用指针很明显的返回了3个值1、2、3.这是为什么呢? 答案可能在这里讲不是怎么适合,我先说在这里,能理解就理解。不能理解就记住结构体变量返回能实现返回多个值,就把结构体变量当着是一个值,不要想到它的成员。那么其本质上来说,结构体变量是怎么返回3个值的呢?

 

原因在于,这里C语言默认帮我们做了很多事情,在后台其实还是返回的只是一个地址,也就是结构体变量a的首地址,这个首地址不是存储在我们定义的变量上的,而是通过CPU寄存器传递的。然后将寄存器指向的那个内存地址的值赋值给ret变量的成员a,然后再寄存器所指向的地址+偏移(这里是4,都是int型)就是b所在的内存地址,然后将b的值取出来赋值给ret中的b。c也是一个道理。这样就把值传递过来了。我们可以理解为编译器编译后,程序会在内存中构建一个临时的结构体。把函数要返回的结构体变量里面的值都复制到这个临时的结构体里。我们是看不到这个结构体的。在函数执行完成后将这个临时结构体的值赋值给我们的接收变量。这里可能有点不好理解,什么是临时结构体,我之前不是一直强调本质吗。结构体就是一块连续的内存空间,我们这里A结构体占用12个字节,因此我可以随便在内存的某个地方构建一个12字节的空间。放置这个结构体的3个成员的值。所以这里叫临时结构体。

 

说到这里,又得提醒一点了。这里我们要返回多个结构体变量的话,同样也可以采用指针。原理跟上面基本类型指针返回一个道理。也存在临时栈内存的问题。返回指针(返回地址)跟返回值(拷贝)大家要区分清楚。

 

 

【C++语言入门篇】系列:

【C++语言入门篇】-- HelloWorld思考

【C/C++语言入门篇】-- 基本数据类型

【C/C++语言入门篇】-- 调试基础

【C/C++语言入门篇】-- 深入指针

【C/C++语言入门篇】-- 数组与指针

【C/C++语言入门篇】-- 结构体

【C/C++语言入门篇】-- 深入函数【上篇】

【C/C++语言入门篇】-- 深入函数【下篇】

【C/C++语言入门篇】-- 位运算【上篇】

【C/C++语言入门篇】-- 位运算【下篇】

【C/C++语言入门篇】-- 剖析浮点数

【C/C++语言入门篇】-- 文件操作【上篇】

【C/C++语言入门篇】-- 文件操作【中篇】

【C/C++语言入门篇】-- 文件操作【下篇】