最新文章请访问:SHIZHZ's Blogs
https://shizhz.me/
本文是对《K&R》第4章《Functions and Program Structure》的一个摘要。
1. 函数定义范式为:
return-type fun-name(arguments declarations)
{
declarations and statements
}
在函数定义中如果没有指定返回值,则默认为int型,通过-Wall编译器会显示相应的警告信息:
fun() {}
单独编译时:
$ gcc -c fun.c -Wall
fun.c:1:1: 警告:返回类型默认为‘int’ [-Wreturn-type]
fun.c: 在函数‘fun’中:
fun.c:1:1: 警告:在有返回值的函数中,控制流程到达函数尾 [-Wreturn-type]
-c选项告诉gcc不调用连接器(linker),只生成可重定位的目标文件,该文件格式为ELF,内容可通过readelf或objdump等二进制工具读取,更多二进制工具可参考GNU提供的binutils;-Wall选项告诉gcc显示所有警告信息。
2. 函数的声明、定义及调用:
函数的声明与定义需要一致,编译器通过检查函数名、返回值、参数列表来检查函数声明与定义的一致性,对于隐式声明的函数则不检查参数列表。函数调用前进行显示声明是好的编程习惯。
如果没有显示声明,则调用处包含了对函数的一个隐式声明,隐式声明的返回值为int,参数任意。如果该函数定义在同一个源文件中,则编译器会对返回值进行一致性检查:
#include <stdio.h>
int main (int argc, char const* argv[])
{
printf("result: %d\n", fun(5, 10));
return 0;
}
char* fun(int i)
{
printf("%d\n", i);
return "hello, world\n";
}
函数fun是在调用处隐式声明的,返回值为int,而定义时返回值为char *;编译时出错:
$ gcc -Wall main.c
main.c: 在函数‘main’中:
main.c:4:5: 警告:隐式声明函数‘fun’ [-Wimplicit-function-declaration]
main.c: 在文件作用域:
main.c:8:7: 错误:与‘fun’类型冲突
main.c:4:28: 附注:‘fun’的上一个隐式声明在此
如果隐式声明的函数定义在另一个源文件中,则编译器无法进行类型检查,因为编译器对各源文件是分别进行编译最后再链接的,如:
// file main.c
#include <stdio.h>
int main (int argc, char const* argv[])
{
printf("result: %d\n", fun(5, 10));
return 0;
}
// file fun.c
#include <stdio.h>
char* fun(int i)
{
printf("in fun: %d\n", i);
return "hello, world\n";
}
则编译时编译器只提示了存在函数隐式声明的警告信息,不会出错:
$ gcc -Wall main.c fun.c
main.c: 在函数‘main’中:
main.c:4:5: 警告:隐式声明函数‘fun’ [-Wimplicit-function-declaration]
但执行结果不可预料:
$ ./a.out
in fun: 5
result: 4195934
此处也可以证明编译器没有对参数列表进行检查,调用时传递了两个参数,而fun的定义只接收一个参数,实际结果是只有第一个int参数有效。
3. 外部变量(external variables)/全局变量(global variables)vs 内部变量(internal variables)/局部变量(local variables):
定义在任何函数外部的变量是外部/全局变量,函数参数及定义在函数内部的变量为改函数的内部/局部变量;前者可以被所有函数访问,后者只能被对应的函数访问。C中函数名是外部/全局变量;对于需要多个函数共享的数据,可以定义为外部/全局变量,需要同一个函数在多次调用时共享的数据,可以定义为static修饰的内部/局部变量。
变量初始化:局部变量存在与函数的运行栈中,不会被初始化,但被static修饰的局部变量不在栈中保存;全局变量根据是否已经初始化存在于不同的块中,已初始化的全局变量存在于目标文件的.data块,未初始化的全局变量存在于.bss块,其值初始化为0。
4. 变量作用域:
a. 全局变量在整个程序中可用;
b. static修饰的全局变量在其定义的单个文件范围内可用;
c. static修饰的局部变量在其定义的函数范围内可用,可在多次调用时共享数据:
#include <stdio.h>
int main (int argc, char const* argv[])
{
int fun(void); // 对fun函数的声明
fun();
fun();
fun();
fun();
return 0;
}
int fun(void)
{
static int counter; // counter会被初始化为0
printf("fun called %d times\n", ++counter);
}
运行结果:
$ ./a.out
fun called 1 times
fun called 2 times
fun called 3 times
fun called 4 times
d. 局部变量在其定义的{}块中可用;
#include <stdio.h>
int main (int argc, char const* argv[])
{
int i = 5;
{ // just a simple inner {} block
int i = 6;
printf("inner block: %d\n", i);
}
printf("outer block: %d\n", i);
return 0;
}
结果为:
$ ./a.out
inner block: 6
outer block: 5
可见如果在for,while或if等语句的{}块中所声明的变量,其作用域仅仅局限于该语句块中。
5. extern关键字:修饰变量声明,如果在源文件a.c中需要访问定义在其他源文件中的全部变量,则需要在a.c文件中对改变量进行声明,用extern关键字进行修饰意在告诉编译器改变量定义在别处,但在本文件中按照所声明的类型进行使用。好处有:1). 防止变量名冲突; 2). 编译器不会为被extern所修饰的全局变量分配内存空间,从而节约资源;
6. 变量名冲突:
a. 局部变量:在同级作用域内,不允许存在相同的变量名声明;
b. 全局变量:
首先需要理解的是编译器的工作流程为:预处理 --> 编译 --> 汇编 --> 链接。截止到汇编,编译器都是对单个源文件进行处理,生成可链接的目标文件(object file),每个目标文件都有对应的符号表记录了对全局变量的声明/定义/调用等信息;然后由链接器完成对所有全局变量的符号解析与重定位,生成最后的可执行文件。
链接器在处理全局变量的符号时,将其分为强(strong)符号与弱(weak)符号。函数名与已初始化的全局变量为强符号,未初始化的全部变量为弱符号,对于多处定义的符号,链接规则为:
i. 不允许有多个强符号;
// main.c
int var = 105;
int main (int argc, char const* argv[])
{
return 0;
}
// fun.c
int var = 105;
链接时出错:
$ gcc -Wall main.c fun.c
/tmp/cchyDXqW.o:(.data+0x0): multiple definition of `var'
/tmp/cc8RjM41.o:(.data+0x0):第一次在此定义
collect2: 错误:ld 返回 1
ii. 一个强符号与多个弱符号:选择强符号;
// main.c
#include <stdio.h>
int var = 105;
int var;
int fun();
int main (int argc, char const* argv[])
{
printf("result of fun: %d;\n", fun()); // 不将这两行写成 printf("result of fun: %d; var: %d\n", fun(), var); 是因为函数参数的解析顺序可不预料. 参见上一篇《kr学习笔记1》
printf("var: %d\n", var);
return 0;
}
// fun.c
int var;
int fun()
{
return var ++;
}
结果为:
$ ./a.out
result of fun: 105;
var: 106
iii. 多个弱符号:任意选择一个;
编译器通常不会报告发现多个同名弱符号的定义。
7. 头文件(.h 文件):
将整个工程需要多处使用的全局变量集中起来声明是非常好的选择,这样就可以在需要使用的源文件中直接引入即可,于是就诞生了头文件。
头文件的引入由预处理指令include完成,在C中,#符号代表这是一条预处理指令;自定义的头文件引入格式:
#include "user.h"
标准库中的头文件引入格式:
#include <stdlib.h>
8. 预处理器:
如前所述,预处理是编译的第一阶段,预处理指令由#符号标识。
#include 引入文件,预处理时读入对应文件的内容;
#define 宏定义,预处理时替换源文件中所有的宏;
#undef 取消宏定义;
条件包含:
#if !defined(EG)
#define EG
#endif
#ifdef或#ifndef
// do something
#elif
// do something else
#else
// do something default
#endif
可传递预处理参数给gcc,查看对应预处理结果。
________________
< 本篇终了:) >
----------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||-----w |
|| ||