函数定义

函数定义默认是全局函数,工程作用域,加上 static 为文件作用域。内核中常常使用可以防止函数名重复,加上 static 来规范。

函数原型的作用

函数原型只在编译时起作用,等编译完成之后就没有作用了,函数调用就会换成函数地址

extern 关键字

extern关键字扩展了C变量和C函数的可见性。

  1. 声明变量或函数仅声明变量或函数存在于程序中的某个位置,但未为它们分配内存。变量或函数的声明起着重要的作用-告诉程序其类型将是什么。

    1. 如果是函数声明,它还会告诉程序参数,其数据类型,这些参数的顺序以及函数的返回类型。这就是声明。
  2. 关于定义,当我们定义一个变量或函数时,除了声明所做的一切外,它还为该变量或函数分配内存。因此,我们可以将定义视为声明的超集(或声明作为定义的子集)。

  3. 变量或函数可以声明多次,但只能定义一次。

函数的 extern

extern 关键字将功能的可见性扩展到整个程序,因此该功能可以在整个程序的任何文件中的任何位置使用(调用),只要这些文件包含该功能的声明即可。(在适当的位置声明了函数,编译器知道该函数的定义存在于其他地方,并且可以继续编译文件)。这就是extern功能。

变量的 extern

在函数内定义的变量是局部变量,而在函数之外定义的变量则称为外部变量,外部变量也就是我们所讲的全局变量。它的存储方式为静态存储,其生存周期为整个程序的生存周期。全局变量可以为本文件中的其他函数所共用,它的有效范围为从定义变量的位置开始到本源文件结束。

  1. int main() {
  2. extern int var;
  3. printf("%d\n", var);
  4. }
  5. int var = 10;

然而,如果全局变量不在文件的开头定义,有效的作用范围将只限于其定义处到文件结束。如果在定义点之前的函数想引用该全局变量,则应该在引用之前用关键字 extern 对该变量作“外部变量声明”,表示该变量是一个已经定义的外部变量。有了此声明,就可以从“声明”处起,合法地使用该外部变量。

如果整个工程由多个源文件组成,在一个源文件中想引用另外一个源文件中已经定义的外部变量,同样只需在引用变量的文件中用 extern 关键字加以声明即可。

空参数的处理

  1. int main (void) {};

在 C 语言中对于空参数需要指定 void 关键字,代表不可以接受参数,但是如果不写 void 代表可以传任意个参数,对于空参数的处理和大部分语言都不一样。

函数的返回值

函数返回一个值的时候,首先会将值拷贝到寄存器,然后再把寄存器中的值拿出来放到外面容器中。如果有一个很大的值比如结构体,就需要频繁的移动和复制。

这个时候就可以提前开辟一块内存,然后把内存地址发送给函数。

自动变量

我们之前定义的变量可以称为自动变量,和下面定义是等价的,auto 是变量的类型,int 是变量类型数据的类型,自动变量是指自动创建和自动销毁的变量。

  1. auto int a = 0;

作用域

C 语言有函数作用域,块作用域,函数原型作用域,文件作用域(工程作用域)

下面的 size 就是函数原型作用域

  1. double sort(int size, int array[size]);

文件作用域除了直接在文件内定义之外还有下面的定义方法,静态变量默认值为 1。

  1. int main() {
  2. static int a;
  3. }

静态变量在 main 函数执行前就分配了空间,程序结束后释放空间,静态全局变量只能在当前文件使用,即使 extern 也不可以用,只有这个和全局变量是不一样的,全局变量可以在另一个文件 extern 后使用。

重复定义

局部变量的重复定义会报错,但是全局变量的重复定义,可以被视为一个是定义一个是声明,但是多次初始化会报错。为了区分我们可以吧表示声明的语句前面加上 extern 。

  1. int a;
  2. int a;
  3. int main() {
  4. return(0);
  5. }

寄存器变量

  1. register int a;

和普通变量一样正常使用,不同的地方体现在编译完成之后生成的汇编指令,寄存器不去内存拿值那么就会省下很多性能,一般不需要自己声明,而是编译器根据情况来选择。

函数的变长参数

C 语言并没有处理变长参数的语法,因此必须把变长参数的信息在第一个参数里传递进来,例如 printf 虽然可以传多个参数,但是参数的个数都会在格式化符中有体现。所以说,通常情况下,第一个参数是必不可少的。

  1. void HandleVarargs(int arg_count, ...) {
  2. va_list args;
  3. va_start(args, arg_count);
  4. for (int i = 0; i < arg_count; ++i) {
  5. int arg = va_arg(args, int);
  6. printf("%d: %d\n", i, arg);
  7. }
  8. va_end(args);
  9. }

… 可以理解为当调用函数传入更多参数时,编译器不报错,在以上函数中, 用到了以下几个宏:va_list
,va_start,va_arg,va_end。

  1. typedef char* va_list;

定义一个 char 类型的指针 args

  1. va_start(args, arg_count);

va_start 意思是将变长参数的指针赋值给 args ,但是为什么需要变长参数的指针但是传入的时候确实 arg_count 呢?

那是因为函数第一步就是栈中初始化形参,所以知道了 arg_count 的地址就知道了第一个变长参数的地址,此时 args 就是第一个变长参数的地址。

image.png

  1. va_arg(args, int);

对 (int)*(args + sizeof(int)) ,因为 args 是 char 类型的指针,所以加 8 相当于加 8 字节,并且会把下一个参数的地址赋值给 args。

  1. va_end(args);

va_end 来清除 args 指向的空间, 否则会发生内存泄漏问题。

vprintf

  1. vprintf("...", va_list);

vprintf 解析字符串中的格式化符号拿到变长参数的个数,之后进行遍历输出

变长参数小结

1、类型不安全,上面我们把所有的参数当作 int 来处理了
2、需要传入第一个参数,里面需要明确变长参数的个数

递归

sum(5) = 1 + 2 + 3 + 4 + 5 = 15

  1. function recsum(x) {
  2. if (x === 1) {
  3. return x;
  4. } else {
  5. return x + recsum(x - 1);
  6. }
  7. }
  8. recsum(5)
  9. 5 + recsum(4)
  10. 5 + (4 + recsum(3))
  11. 5 + (4 + (3 + recsum(2)))
  12. 5 + (4 + (3 + (2 + recsum(1))))
  13. 5 + (4 + (3 + (2 + 1)))
  14. 15

在函数返回前,里面的所有子调用当前都无法返回,意味着将占用大量的内存空间

  1. function tailrecsum(x, running_total = 0) {
  2. if (x === 0) {
  3. return running_total;
  4. } else {
  5. return tailrecsum(x - 1, running_total + x);
  6. }
  7. }

尾递归,可以看到每次子调用都对之前的函数调用栈没有依赖

  1. tailrecsum(5, 0)
  2. tailrecsum(4, 5)
  3. tailrecsum(3, 9)
  4. tailrecsum(2, 12)
  5. tailrecsum(1, 14)
  6. tailrecsum(0, 15)
  7. 15

尽管尾部调用优化是ECMAScript 2015规范的一部分,但大多数JavaScript解释器并不支持它。但是 C 语言支持尾递归。

在尾部递归中,先执行计算,然后执行递归调用,将当前步骤的结果传递到下一个递归步骤。这导致最后一个语句的形式为(返回(递归函数参数))。一旦你准备好执行下一个递归步骤,你就不再需要当前堆栈帧了。使用编写得当的编译器,你永远不会在尾部递归调用中出现堆栈溢出。只需为下一个递归步骤重用当前堆栈框架。