函数定义
函数定义默认是全局函数,工程作用域,加上 static 为文件作用域。内核中常常使用可以防止函数名重复,加上 static 来规范。
函数原型的作用
函数原型只在编译时起作用,等编译完成之后就没有作用了,函数调用就会换成函数地址
extern 关键字
extern关键字扩展了C变量和C函数的可见性。
声明变量或函数仅声明变量或函数存在于程序中的某个位置,但未为它们分配内存。变量或函数的声明起着重要的作用-告诉程序其类型将是什么。
- 如果是函数声明,它还会告诉程序参数,其数据类型,这些参数的顺序以及函数的返回类型。这就是声明。
关于定义,当我们定义一个变量或函数时,除了声明所做的一切外,它还为该变量或函数分配内存。因此,我们可以将定义视为声明的超集(或声明作为定义的子集)。
变量或函数可以声明多次,但只能定义一次。
函数的 extern
extern 关键字将功能的可见性扩展到整个程序,因此该功能可以在整个程序的任何文件中的任何位置使用(调用),只要这些文件包含该功能的声明即可。(在适当的位置声明了函数,编译器知道该函数的定义存在于其他地方,并且可以继续编译文件)。这就是extern功能。
变量的 extern
在函数内定义的变量是局部变量,而在函数之外定义的变量则称为外部变量,外部变量也就是我们所讲的全局变量。它的存储方式为静态存储,其生存周期为整个程序的生存周期。全局变量可以为本文件中的其他函数所共用,它的有效范围为从定义变量的位置开始到本源文件结束。
int main() {
extern int var;
printf("%d\n", var);
}
int var = 10;
然而,如果全局变量不在文件的开头定义,有效的作用范围将只限于其定义处到文件结束。如果在定义点之前的函数想引用该全局变量,则应该在引用之前用关键字 extern 对该变量作“外部变量声明”,表示该变量是一个已经定义的外部变量。有了此声明,就可以从“声明”处起,合法地使用该外部变量。
如果整个工程由多个源文件组成,在一个源文件中想引用另外一个源文件中已经定义的外部变量,同样只需在引用变量的文件中用 extern 关键字加以声明即可。
空参数的处理
int main (void) {};
在 C 语言中对于空参数需要指定 void 关键字,代表不可以接受参数,但是如果不写 void 代表可以传任意个参数,对于空参数的处理和大部分语言都不一样。
函数的返回值
函数返回一个值的时候,首先会将值拷贝到寄存器,然后再把寄存器中的值拿出来放到外面容器中。如果有一个很大的值比如结构体,就需要频繁的移动和复制。
这个时候就可以提前开辟一块内存,然后把内存地址发送给函数。
自动变量
我们之前定义的变量可以称为自动变量,和下面定义是等价的,auto 是变量的类型,int 是变量类型数据的类型,自动变量是指自动创建和自动销毁的变量。
auto int a = 0;
作用域
C 语言有函数作用域,块作用域,函数原型作用域,文件作用域(工程作用域)
下面的 size 就是函数原型作用域
double sort(int size, int array[size]);
文件作用域除了直接在文件内定义之外还有下面的定义方法,静态变量默认值为 1。
int main() {
static int a;
}
静态变量在 main 函数执行前就分配了空间,程序结束后释放空间,静态全局变量只能在当前文件使用,即使 extern 也不可以用,只有这个和全局变量是不一样的,全局变量可以在另一个文件 extern 后使用。
重复定义
局部变量的重复定义会报错,但是全局变量的重复定义,可以被视为一个是定义一个是声明,但是多次初始化会报错。为了区分我们可以吧表示声明的语句前面加上 extern 。
int a;
int a;
int main() {
return(0);
}
寄存器变量
register int a;
和普通变量一样正常使用,不同的地方体现在编译完成之后生成的汇编指令,寄存器不去内存拿值那么就会省下很多性能,一般不需要自己声明,而是编译器根据情况来选择。
函数的变长参数
C 语言并没有处理变长参数的语法,因此必须把变长参数的信息在第一个参数里传递进来,例如 printf 虽然可以传多个参数,但是参数的个数都会在格式化符中有体现。所以说,通常情况下,第一个参数是必不可少的。
void HandleVarargs(int arg_count, ...) {
va_list args;
va_start(args, arg_count);
for (int i = 0; i < arg_count; ++i) {
int arg = va_arg(args, int);
printf("%d: %d\n", i, arg);
}
va_end(args);
}
… 可以理解为当调用函数传入更多参数时,编译器不报错,在以上函数中, 用到了以下几个宏:va_list
,va_start,va_arg,va_end。
typedef char* va_list;
定义一个 char 类型的指针 args
va_start(args, arg_count);
va_start 意思是将变长参数的指针赋值给 args ,但是为什么需要变长参数的指针但是传入的时候确实 arg_count 呢?
那是因为函数第一步就是栈中初始化形参,所以知道了 arg_count 的地址就知道了第一个变长参数的地址,此时 args 就是第一个变长参数的地址。
va_arg(args, int);
对 (int)*(args + sizeof(int)) ,因为 args 是 char 类型的指针,所以加 8 相当于加 8 字节,并且会把下一个参数的地址赋值给 args。
va_end(args);
va_end 来清除 args 指向的空间, 否则会发生内存泄漏问题。
vprintf
vprintf("...", va_list);
vprintf 解析字符串中的格式化符号拿到变长参数的个数,之后进行遍历输出
变长参数小结
1、类型不安全,上面我们把所有的参数当作 int 来处理了
2、需要传入第一个参数,里面需要明确变长参数的个数
递归
sum(5) = 1 + 2 + 3 + 4 + 5 = 15
function recsum(x) {
if (x === 1) {
return x;
} else {
return x + recsum(x - 1);
}
}
recsum(5)
5 + recsum(4)
5 + (4 + recsum(3))
5 + (4 + (3 + recsum(2)))
5 + (4 + (3 + (2 + recsum(1))))
5 + (4 + (3 + (2 + 1)))
15
在函数返回前,里面的所有子调用当前都无法返回,意味着将占用大量的内存空间
function tailrecsum(x, running_total = 0) {
if (x === 0) {
return running_total;
} else {
return tailrecsum(x - 1, running_total + x);
}
}
尾递归,可以看到每次子调用都对之前的函数调用栈没有依赖
tailrecsum(5, 0)
tailrecsum(4, 5)
tailrecsum(3, 9)
tailrecsum(2, 12)
tailrecsum(1, 14)
tailrecsum(0, 15)
15
尽管尾部调用优化是ECMAScript 2015规范的一部分,但大多数JavaScript解释器并不支持它。但是 C 语言支持尾递归。
在尾部递归中,先执行计算,然后执行递归调用,将当前步骤的结果传递到下一个递归步骤。这导致最后一个语句的形式为(返回(递归函数参数))。一旦你准备好执行下一个递归步骤,你就不再需要当前堆栈帧了。使用编写得当的编译器,你永远不会在尾部递归调用中出现堆栈溢出。只需为下一个递归步骤重用当前堆栈框架。