【C/C++语言入门篇】系(8)列-- 深入函数【下篇】

版权声明:此文章转载自51CTO

原文链接:http://masefee.blog.51cto.com/1737284/814008

如需转载请联系听云College团队成员小尹 邮箱:yinhy#tingyun.com

好了。返回值我们就说完了。下面说参数。

参数可以有多个,还可以有不定参数,比如我们常用的printf函数就是不定参数。也就是动态的参数个数哈。

固定的参数个数多个和一个是一样的道理,我在这里只列举一个参数的情况或者两个参数的情况。

void fun( int* p );
void fun( int a );
void fun( void );
void fun( int* p, int size );

上面我没有写返回值,返回值不用说了。在了解参数之前我们先看一个例子:

void fun( int var )

{

    var = 100;

}

int main( void )

{

    int a = 1;

    fun( a );

    return 0;

}

在这个程序中,我们调用了fun函数,试图去改变a的值。但是出乎意料的是,在调用了fun函数后a的值改变。这是为什么?可能很多初学的读者一直很纳闷。或者就死记硬背这样不会改变a的值。我们在研究一个东西只有知道了本质才能记得更牢,而且不用记都会一直明白。那么我们先说说a没有被改变的原因。

也许大家都听说过值传递,地址传递,引用传递。引用传递我们在本篇不说,那牵涉到C++的相关概念了。以后我们在讲引用的时候再说。

那么先说说什么叫值传递。

我们通过上面的内容了解到了栈内存,也就是临时数据存放的地方。函数内部的临时变量都是放在这里面的。这里传参数,又不得不明白一点就是。不管我们传的是指针还是值。程序在调用函数之前都会先将参数压入函数内部的栈空间里。意思就是说函数会把这些参数当着函数内部的临时变量来处理。这里将参数压入我们函数内部所在的栈空间里的过程叫传递,压入的地方(内存地址)里的值通常称为参数的副本。这里别想到游戏里面下FB哈,总结出来的意思就是说,我们在跟函数传参数的时候会将参数一个一个压入到函数内部所在的栈内存中。这里的压入也可以理解成向栈内存里面写值。

上面的fun( a ),首先是将a的值压入到栈内存,比如0x0012ffec这个内存里。这个内存地址下面的值就是1,也就是通常所说的a的副本(克隆体)。然后执行到函数内部的var = 100; 这里的var所取值的内存地址就是0x0012ffec,也就是传进来的参数的那个内存地址。这一切都是编译器给安排好的。然后我们将这个0x0012ffec内存地址里面的值赋值为100。好了,var变成了100。之后函数fun便执行完毕了。到这里大家可能已经知道为什么a的值不会改变了。原因就是函数内部只知道去改变0x0012ffec这个内存地址里面的值,而改变了这个值并不会影响到a,因为a又属于main函数的局部变量,a所在的内存地址并不是0x0012ffec。0x0012ffec这个地址之所以能够将a的值传进函数是因为在压参数的时候是将a的值1拷贝到0x0012ffec内存里。注意这里是拷贝。

那么,到这里我们想了想,要是我们想改变a的值怎么办呢?如下:

void fun( int* var )
{
    *var = 100;
}
int main( void )
{
    int a = 1;
    fun( &a );
    return 0;
}

用指针就可以将a 的值改变。大家又疑惑了。为什么这里指针就能改变呢?原因跟上面一样,首先我们传入的是a的内存地址,比如是0x0012ffff,将这个地址传给了函数,通过我们上面知道,虽然是传的地址,可它还是将这个地址当着值压入函数内部栈空间,比如压到了0x0012eeee这个内存里。注意每个函数都有自己独立的那块栈空间提供给自己用,用完就丢弃。所以这里压入后的内存地址跟变量a本身的内存地址不可能相同。然后我们再看fun函数,它是一个指针取值操作然后再赋值为100。看看流程,首先var我们知道它的内存地址就是0x0012eeee(上面说的编译器安排的),而这个内存地址里面的值就是0x0012ffff这个内存地址。var是一个指针,在前面指针篇我们知道var有它自己的内存地址(这里就是0x0012eeee),它自己又保存了它所指向的内存地址(这里就是0x0012ffff)。这里这个内存地址也就是传进来的a变量的地址,我们在间接访问(*var)时,实际就是操作的a变量本身。因此这里将会直接指向a的地址将其值改变为100。

这个例子在我没有打招呼的情况下我们已经就讲了地址传递的方法。地址传递就是将一个变量的地址传递给函数,函数内部在访问压入的这个参数时,读写的是外部变量的地址值。因此可以改变传入参数的值。

问题二:假如上面的程序中a是一个指针,我们将a传进函数fun,然后在fun函数里改变指针的指向(指针的值)。外面的a指针是否会改变? 为什么? (提示:原理跟上面一样,必要时用二级指针进行地址传递)

说到数组,我们又不得不想到如果我们想传一个一维数组到函数内部,供函数取值或者写值。又该怎么做?

void fun( int* a )
{
    a[ 0 ] = 100;
    a[ 1 ] = 100;
    a[ 2 ] = 100;
}
int main( void )
{
    int array[ 3 ] = { 1, 2, 3 };
    fun( array );
    return 0;
}

以上代码中,我们的意图是想将array的值改成100。我们的目的达到了,结果一切正常。为什么呢?可能有的读者已经被上面的值传递和地址传递给弄混了。在这里我们不用多想,就应该知道这里传入的是array数组的首地址,在函数内部会将这个地址里面的值进行修改,然后加上偏移逐个修改。这里也是通过地址直接操作的。原理跟上面一样我就不多说了。这里的fun函数是我知道array数组有3个元素的情况下,假如不知道,那么我们就该再添加一个数组元素个数的参数。这样既安全又得体。比如:void fun( int* pArray, int size );

fun( array, 3 ); 这样函数内部就不会怕读写越界了。

问题三:怎么传二维数组到函数内部?

下面我就来举一个越界带来的可怕后果之一:

void fun( void )
{
    printf( "I'm Come In!!!/n" );
}
 
int main( void )
{
    int array[ 1 ] = { 1 };
    array[ 3 ] = ( unsigned int )fun;
    
    return 0;
}

就上面一个简单的程序,已经诠释了一个经典的缓冲区溢出攻击基本原理了。先解释下程序,这里定义了一个数组array,它是有一个元素,下面的一句    array[ 3 ] = ( unsigned int )fun; 我这里是故意将fun函数的地址越界赋值给array数组后面的第3个内存地址里。占用4个字节。这样做的目的,大家运行了便知道,神奇般的在我没有调用fun函数的情况下进入了fun函数并输出了I‘m Come In!!!字符串。可能很多人就傻了,为什么会这样?我这里并没有调用。

原因很简单,我们每个函数在执行完以后都会跳转回来,回到调用此函数的下一条语句继续往下执行,函数之所以能跳转回来是因为我们在调用函数的时候就已经将要返回到的代码地址给保存到函数栈内存中了。我这里将数组写越界的目的就是为了将这个返回地址值改变成我的目标函数fun函数的地址(函数也是有首地址的)。这里强制类型转换fun函数首地址为无符号整数覆盖掉main函数的返回地址。这样在main函数返回时便会跳转到fun函数并执行该函数。输出字符串。我们可以联想一下,假如这个fun函数是我们的黑客想操作一些事情的函数,那将是非常危险的。这里就是经典的“缓冲区溢出攻击”的基本原理。

假如我这里不是array数组,而是一个字符数组,我们在strcpy的时候没有检查长度,黑客通过修改函数传入的字符串参数,让其拷贝越界,覆盖掉返回地址,覆盖的内容就是黑客自己实现的函数的地址。我们程序将神不知鬼不觉的调用它的函数。当然上面我写的这个在执行输出后,fun函数在返回时,由于不是正常调用,他的返回地址没有谁给他压入,将返回到错误的地址最后崩溃掉。这里我没有处理堆栈平衡和返回地址。处理之后将不会崩溃,跟正常流程一样顺利。

上面说了越界缓冲区溢出乱调函数,也是为了引入函数指针,上面的例子我们初识函数也是有自己的地址的。既然有地址,那么指针必然就成立。既然是指针,又是普通函数,那么我随便怎么转换该指针都没有问题。这也是CC++的魅力所在。我上面就轻轻松松转换成了无符号整数然后覆盖了返回地址。是不是很方便?那么我们再看看正规的函数指针定义:

void fun( void )
{
    printf( "I'm Come In!!!/n" );
}
 
int main( void )
{
    typedef void( *PFUN )( void );   // 定义函数指针,这里使用typedef别名,PFUN就被声明为void返回,无参数类型函数的指针
    PFUN pfun = fun;
    
    ( *pfun )(); 
    pfun();           // 两种调用方式都是一样的
    
    return 0;
}

上面大家已经知道了函数指针的定义了吧,语法很简单。先定义一个函数指针pfun,将值赋值为fun函数的地址,函数名也代表函数指针,此指针就是指向的fun函数开始的代码地址。这里是代码地址。在我们的exe中,每一句代码都是有自己的代码地址的。这里的代码值的是汇编每条指令。这里我们不追究,只需要知道函数也是有首地址的。可以赋值给函数指针乃至任何一个指针。只不过赋值给函数指针之后我们就可以像( *pfun )();   pfun();这样调用它。跟函数调用没有什么区别。 假如你给我将fun函数赋值给一个void*指针p:

void* p = ( void* )fun;  
p();   // error

这样将是错误的,原因就不用说了吧。天下人都知道。

函数指针也很灵活,同样也可以由参数,有返回值。跟普通函数没有上面区别。

问题四:定义一个有参数,有返回值的函数指针,并调用它。

将函数指针作为参数也是有必要了解的:

typedef void( *PFUN )( void );
void fun( void )
{
    printf( "I'm Come In!!!/n" );
}
 
void call_func( PFUN pFun )
{
    pFun(); 
}
 
int main( void )
{
    call_func( fun );
    return 0;
}

上面的代码,反映了将函数指针作为参数传递给一个函数,让这个函数在另外一个地方被执行。这个过程通常称为回调。fun可以称为回调函数。我们将fun的函数指针传递给call_func,然后call_func再调用这个fun函数。原理大家清楚了吧。

回调函数在大型的项目中使用得非常多,最直接的就是我们的WIN32的消息回调函数。我们需要注册我们自己定义的函数给操作系统,这里的注册其实就是操作系统提供了一个函数指针给我们。我们将提供的这个函数指针赋值为我们自定义的函数的指针。操作系统内部又在不断的调用这个函数指针。因此我们就可以让操作系统调用我们的自定义函数了。大家可以自己试着写写这样的调用模型。比如一个函数指针的链表,里面存放了很多函数指针,我们遍历调用这个链表里面的所有函数指针。这些指针我们都赋值成我们想要调用的函数。

这里值得大家注意的是,使用函数指针的时候一定要小心,比如:

typedef int ( *PFUN )( void );
void fun( void )
{
    printf( "I'm Come In!!!/n" );
}
 
int call_func( PFUN pFun )
{
    int a = pFun(); 
    return a;
}
 
int main( void )
{
    int ret = call_func( ( PFUN )fun );
    return 0;
}

我将fun函数强制转换成int返回类型的函数指针,然后调用。这样执行完成后,ret的值将是废弃的。不可预测的。原因很简单,fun函数是没有返回值的。这里的返回值具体会是读取的哪儿的值我们就不在这里讲解了,知道有这么回事就可以了。这里假如不强制转换,编译器也只是会给一个警告而已。这种用法是绝对错误的。所以我们在使用回调函数的时候一定要注意参数的函数指针是声明的指向什么类型的函数。

另外函数的可变参数这里就不讲了,这不是重点,只是语法而已。大家通过查阅资料就可以明白了。

好了,函数我们就介绍完了。大家好好理解。有点长。又写了我5个小时左右。。。。休息。。

想阅读更多技术文章,请访问听云技术博客,访问听云官方网站感受更多应用性能优化魔力。

关于作者

郝淼emily

重新开始,从心开始

我要评论

评论请先登录,或注册