一、普通传参

普通传参时,其实就是开辟一个形参空间,然后将实参数据复制过去,
此时形参是独立的变量空间,修改形参时,实参不会有任何改变

int fun(int a)
{
    ...
}

int main()
{
    int a = 20;
    fun(a);

    return 0;
}

二、传递指针

int fun(int *p1, int **p2)
{
    ...
}

int main(void)
{
    int a  = 20;
    int *p = &a;

    fun(p, &p);

    return 0;
}

与普通传参的相似之处

传递指针与传递普通参数没有本质区别,指针形参也是独立的空间,然后将实参复制给形参,
唯一不同的时,传递指针,复制的数值是指针

与普通传参的不同之处

普通传参修改形参本身,并不会改变实参的值,但是不同之处就在于,由于传递的是指针,
所以可以通过*解引用,实现对实参的访问

所以指针传参,与普通传参没有本质区别,都是将数据传递给独立的形参空间,
唯一不同的是,如果是普通传参的话,不能访问实参,如果传递的是指针的话,可以通过解引用来访问实参


三、什么时候传递指针

使用指针的通病
学会指针之后,很多同学惯于事事皆指针,当然传递指针肯定都没问题,
但事实上有些时候只需“普通传参”即可搞定的

传递指针的原则(什么时候传递指针呢?)
只要符合以下两个原则中某个或者两个时,就需要传递指针。

1. 原则1:修改原则

如果被调函数需要修改实参的值时,就必须传递实参变量的指针,否则是修改不了的

void fun(int **p)
{
    *p = malloc(sizeof(int));  //修改p,让p指向malloc的空间
}

int main(void)
{
    int *p = NULL;
    fun(&p);
}

2. 原则2:效率原则

传参时,凡是大片的空间,都需要传递指针,这样效率更高

1)结构体
其实是可以对结构体进行普通传参的,

struct student
{
    int num;
    char name[40];
    float scaore;
    ...
}; 

int fun(struct student stu)
{
    ...
}

int main(void)
{
    struct student stu = {9527, "zhangsan", 99.0, ...};
    fun(stu);
}

在例子中,fun的形参stu是一个结构体变量,实参stu中所有的内容会全部复制到形参stu中,
显然直接传递结构体变量,是很浪费存储空间,特别是在实际应用中,一个大的结构体往往会有十几个,
甚至几十个成员,如果是这样的话,真的非常浪费栈空间

但是如果传递指针的话,就不存在这个问题,因为传递指针时,形参只需要4或者8个字节,
通过这个指针,我们一样可以从实参空间中读取数据

int fun(struct student *stup)
{
    ...
}

fun(&stu);

2)传递数组
数组往往都是很大的一片连续空间,我们传递数组时,如果传递的是整个数组的话,形参也必须是一个数组空间,
然后将值复制进去,显然很浪费栈空间,因此在C语法中,传递数组时都是传递指针

int fun(int *buf, 10)
{
    ...
}

int main(void)
{
    int buf[10] = {0,1,2,3,4,5,6,7,8,9};
    fun(buf, 10);
}

在传参时,buf代表的是数组的第一个元素的指针,也即第一个元素的第一个字节的地址,与&buf[0]等价

3)传递函数指针
其实传递函数指针也可以归属到效率这一原则中,因为函数代码的存储空间也是一大片空间,
所以你不可能把整个代码全部复制给对方,一个没有传递函数代码的这种操作,另一个是站在效率的角度来说,
这也是不允许的,所以只能传递函数指针

不管是满足以上两个原则中的哪一种,还是两个都满足,此时我们就需要传递指针了,除此以外的就使用普通传参


四、再说说传参

1. 形参空间

我们之前讲到过,总体上,形参开辟于“栈”中,但是也有特殊情况,其实在前面的章节提到过,

这里再提一下。
· Windows

c程序在Windows下运行时,由于Windows系统基本都是运行intel的处理器上的,
intel处理器的寄存器相对偏少,所以形参都在栈中。其实只要是寄存器偏少的情况,形参都是在栈中

· 嵌入式Linux

嵌入式Linux系统,大多是运行在ARM处理器上,ARM处理器的寄存器相对偏多,因此如果形参个数<=4
那么形参就开辟于寄存器中,之所以放在寄存器中,主要是因为寄存器的访问速度比内存快,
只有当形参数量超过4个时,多余的形参才开辟在栈中。其实只要是寄存器比较多的,都是这样的情况

2. 如何减少形参数量

不要没事就传参,只有当有必要时才传参,要有意识的控制形参数量

如果实在没办法,必须要传递很多参数时,我们可以使用结构体将形参都封装起来,然后传递结构体指针,
如此一来原本需要开辟很多形参的情况,现在只需要开辟一个“指针形参”来存放“结构体指针”即可

比如:

fun的形参非常多,此时使用结构体封装封装后,就变成了如下形式:

struct student 
{
    ...
};

struct formal_parameter
{
    int a;
    int b;
    int c;
    int *pva;
    char buf[];
    struct student stu;
};

int fun(struct formal_parameter *stup)
{
    ...
}

int main(void)
{
    int va = 100;

    struct formal_parameter form_para = {10, 20, 30.5, &va, "zhangsan", &stu};

    int fun(&form_para); // 封装成结构体再传参


    return 0;
}

从例子可以看出,原本形参需要几十个字节的空间,现在节省为了48字节的指针变量空间

总结,我们在什么时候需要传递一个结构体:

  • 1)必须传递一个结构体时,比如例子中的struct student结构体
    绝大部分传递结构体的情况都是这种,属于正常需求。
  • 2)使用结构体对传参进行封装
    这种情况不常见,只有当函数参数特别多时才会这么办。

五、参数不确定

有同学可能会疑问,还有无法确定参数的情况吗,其实是有的,参数的不确定有两方面:

  • 不确定参数数量
  • 不确定参数类型

当我们遇到这种情况时,就使用void *指针,此时可以传递任何你想传递的参数类型

例子1:c线程的

struct student 
{
    int num;
    char buf[40];
    float score;
};

void fun(int type, void *p)
{
    if(type == 0)
    {
        printf("%d\n", (int)p);
    }
    else if(type == 1)
    {
        *((float *)p) = 200.456;
    }
    else if(type == 2)
    {
        struct student *tmp = (struct student *)p;
        printf("%d\n", tmp->num);
        printf("%s\n", tmp->name);
        printf("%f\n", tmp->score);
    }
}

void fun1(void)
{
    fun(0, (void *)100);
}

void fun2(void)
{
    struct student stu = {9527, "zhangsan", 99.0};
    fun(2, (void *)&stu);
}

int main(void)
{
    fun1();
    fun2();

    return 0;
}

fun函数代码是if else结构,只有一个条件成立,所以只处理一种情况,调用fun函数时只是处理某一种情况,
那就应该根据情况,值传递某一种参数过去,也就是说fun的参数不确定,要根据实际情况来定,

此时就是使用void *来统一,你想传递什么都可以,只要在fun中将类型强制转换为你要的类型即可

从例子可以看出,就算你想通过void *来直接传递整形也是可以的,因为int的大小是4个字节,
void *的大小要么是48个字节,所以存放整形的内容完全没问题,只要再强制转换为int型即可


例子2:c线程库中的pthread_create函数

int pthread_create(pthread_t *thread, pthread_attr_t const *attr,
void *(*start_routine) (void *), void *arg);

有关c线程的这个函数,我们在《Linux系统编程/网络编程》第9章有详细讲解,具体情况请看这部分课程

第四个参数之所以为void *,就是因为传参不确定,你可以传递任何你你要参数,如果传递的参数很多,
那就封装为结构体,然后传递结构体指针,使用参数的一方,只要对应的将其强制转换回来即可


六、传递指针的风险

什么是指针风险

为了提高传参效率,所以必须传递指针,但是并不想修改实参的内容,
但是由于传递的是指针,所以存在潜在误改实参的风险


如何规避指针风险

(1)使用普通传参

如果能够使用普通传参时就使用普通传参,因为普通传参不存在这种情况

(2)传递指针,使用const来防止

const 修饰指针时有三种情况

  • int const *pp可以改,但是p中指针所指向空间不能改
  • int * const pp不可改,但是p中指针所指向的空间可以修改
  • int * const * const pp和指向的空间都不可以修改
int fun(struct student const *stup)
{
    ...
}

int main(void)
{
    struct student stu = {9527, "zhangsan", 99.0, ...};

    fun(&stu);
}

stup一旦被&stu初始化后,通过stup操作实参时,只能读实参开空间,不能写

如果你想stup一直指向stu,不希望被修改为指向别的空间,那就需要将stup也变成不可修改的,
此时定义形式就改为

struct student const * const stup

不过一般来说没有这种需求,只要不误改实参空间就可以了

使用const修改指针形参的情况非常多,特别是当调用的是库函数和OS API时,更是比比皆是,
在讲《Linux系统/网络编程》课程,我们讲LinuxOS API时,指针形参就有大量的使用const
我们自己传递指针时,为了让指针的使用更安全,我们也要学会使用const