【C】函数
初识函数
为什么要自己定义函数?
因为随着程序规模的变大,都免不了会遇到下面这些问题:
- main函数变得相当冗杂,程序可读性差
- 程序复杂度不断提高,编程变成了头脑风暴
- 代码前后关联度高,修改代码往往牵一发而动全身
- 变量的命名都成了问题(因为简单的名字都用完了啊,小明,小红,旺财,阿福,隔壁家老王这些名字都用过了,为了不重复命名,只能小明2号,小红3号这样……)
- 为了在程序中多次实现某功能,不得不重复多次写相同的代码
- ……
标准库函数
C 语言的标准库中还为我们提供了很多实现各种功能的函数,有处理字符串的,有数学计算的,有输出输入的,有进程管理的,有信号、接口处理的等等
详细参考C语言标准库函数分类
有了这些函数,我们就不用去关注内部的实现细节,只需要将注意力放在程序的实现逻辑上即可。
比如要打印字符串到屏幕上,我们只需要知道调用printf函数并给它传递要打印的内容即可,至于它内部是怎么做到的,我们不需要理会。
设计程序
先动手设计一个程序,要求每调用一次函数程序求会在屏幕上打印一个由“#”组成的字母C
#include<stdio.h>
void print_C(); //函数的声明
void print_C() //函数的定义
{
printf(" ###### \n");
printf("## ##\n");
printf("## \n");
printf("## \n");
printf("## \n");
printf("## ##\n");
printf(" ###### \n");
}
int main()
{
print_C(); //调用函数
printf("\n");
print_C(); //再次调用函数
return 0;
}
函数的定义
C 语言要求函数必须“先定义,再调用”,定义函数的格式如下:
类型名 函数名(参数列表)
{
函数体
}
- 类型名就是函数的返回值,如果这个函数不准备返回任何数据,那么需要写上 void(void 就是无类型,表示没有返回值)。
- 函数名就是函数的名字,一般我们根据函数实现的功能来命名,比如 print_C 就是“打印C”的意思,一目了然。
- 参数列表指定了参数的类型和名字,如果这个函数没有参数,那么这个位置直接写上小括号即可"()"。
- 函数体就是指定函数的具体实现过程,是函数中最重要的部分。
函数的声明
所谓声明(Declaration),就是告诉编译器我要使用这个函数,你现在没有找到它的定义不要紧,请不要报错,稍后我会把定义补上。
有时候,你可能会发现即使不写函数的声明,程序也是可以正常执行的。但如果你把函数的定义写在调用之后,那么编译器可能就会找不着北了:
#include<stdio.h>
void print_C()
{
printf(" ###### \n");
printf("## ##\n");
printf("## \n");
printf("## \n");
printf("## \n");
printf("## ##\n");
printf(" ###### \n");
}
int main()
{
print_C(); //调用函数
return 0;
}
运行结果
######
## ##
##
##
##
## ##
######
在调用前定义,不加声明也可以正常运行。再来试试在调用后定义函数
int main()
{
print_C(); //调用函数
return 0;
}
void print_C()
{
printf(" ###### \n");
printf("## ##\n");
printf("## \n");
printf("## \n");
printf("## \n");
printf("## ##\n");
printf(" ###### \n");
}
运行结果:
warning: conflicting types for 'print_C';
note: previous implicit declaration of 'print_C' with type 'void()'
######
## ##
##
##
##
## ##
######
不声明函数,且先调用函数再定义,程序执行后会弹出提醒(一些比较旧的编译器甚至会报错)。
这是因为程序的编译时从上到下执行的,所以从原则上来说,函数必须“先定义,再调用”。向上边例子反其道而行就会出问题。但在实际开发中,经常会在函数定义之前使用它们,这个时候就需要提前声明。
声明函数的格式非常简单,只需要去掉函数定义中的函数体再加上分号(;)即可
作为一个良好的编程习惯,建议无论如何都把函数的声明写上比较合适。
函数的参数和返回值
有时候,函数需要接收用户传入的数据,那么就需要使用函数的参数。根据需求,函数的参数数量可以有多个,类型也可以各不相同。
如果函数不需要参数,建议定义时在函数名后边的小括号中写上void,明确表示该函数无参数
而函数的返回值通常是反馈了函数的计算结果,也可以是函数的执行结果(比如成功或失败)。
请参考以下两个例子:
- 编写一个函数 sum,由用户输入参数 n,计算 1+2+3+...+(n-1)+n 的结果并返回
int sum(int n);
int sum(int n) //int类型函数,返回值为int型
{
int result = 0;
do
{
result += n;
}while(n-- > 0);
return result;
}
int main()
{
int n,result;
printf("请输入n的值:");
scanf("%d", &n);
result = sum(n); //调用sum函数,传入参数n
printf("1+2+3+...+(n-1)+n的结果是: %d\n", result);
//也可以直接在printf函数里调用sum函数,把result改成sum(n)即可
return 0;
}
运行结果:
请输入n的值: 100 //输入100
1+2+3+...+(n-1)+n的结果是: 5050
-
编写一个函数 max,接收两个整型参数,并返回它们中的较大的值
int max(int, int); //声明时可以不写参数名字,但必须要写参数的类型 int max(int x, int y) //参数x,y称为形式参数,也称形参 { if (x > y) { return x; // 程序一旦执行return语句,表明函数返回,后边的代码不会继续执行 }else { return y; } } int main() { int a, b, c; //main函数里的变量名和其他函数的变量名是不会冲突的 //所以和其他函数的变量同名也没事 printf("请输入两个整数(用空格隔开):"); scanf("%d%d",&a,%b); c = max(a,b); //调用max函数,传入参数a,b; 参数a,b是实际参数,也就是实参 printf("它们中较大的值是: %d\n",c); return 0; }
运行结果
请输入两个整数(用空格隔开):44 100 它们中较大的值是: 100
注意:程序一旦执行
return
语句,表明函数返回,后边的代码不会继续执行
参数和指针
函数是一种封装的方法,函数的设计应遵从一个函数仅实现一个功能的原则
参数和返回值
- 函数在定义的时候通过参数列表来指定参数的数量和类型,参数使得函数变得更加的灵活,传入不同的参数可以让函数实现更为丰富的功能。如果你的函数确实不需要参数,建议使用 void 进行强调。
- 函数的类型名事实上就是指定函数的返回值。通常这些函数会通过返回值来说明该函数是否调用成功。最后,如果你的函数确实不需要返回值,那么就用 void 表示不返回。
形参和实参
- 形参 形式参数
- 实参 实际参数
形参就是形式参数,函数定义的时候写的参数就叫形参,因为那时候它只是作为一个占位符而已。而实参就是你在真正调用这个函数的时候,传进去的数值。
形参和实参的功能说白了就是用作数据传送。当发生函数调用时,实参的值会传送给形参,并且这种传输具有单向性(也就是不能把形参的值回传给实参)。另外,形参变量只有在函数被调用时才会分配内存,调用结束后,立刻释放内存,所以形参变量只有在函数内部有效,不能在函数外部使用。
传值和传址
指针也是一个变量,所以可以通过参数将指针传递给函数。下面举个例子对比一下,引进指针的参数有什么意义。
首先用不使用指针的例子演示
void swap(int x, int y);
void swap(int x, int y)
{
int temp;
printf("In swap, 互换前: x = %d, y = %d\n", x, y);
temp = x;
x = y;
y = temp;
printf("In swap, 互换后: x = %d, y = %d\n", x, y);
}
int main()
{
int x = 3, y = 5;
printf("In main, 互换前: x = %d, y = %d\n", x, y);
swap(x, y);
printf("In main, 互换后: x = %d, y = %d\n", x, y);
return 0;
}
运行结果
In main, 互换前: x = 3, y = 5
In swap, 互换前: x = 3, y = 5
In swap, 互换后: x = 5, y = 3
In main, 互换后: x = 3, y = 5
可以看到,在main函数内,调用完swap函数后并不会修改实参x和y的值,只有形参发生变化,而实参没变。我们再把传入的变量改成指针试一下:
void swap(int *x, int *y);
void swap(int *x, int *y)
{
int temp;
printf("In swap, 互换前: x = %d, y = %d\n", *x, *y);
temp = *x; //解引用指针x的值赋给temp
*x = *y;
*y = temp;
printf("In swap, 互换后: x = %d, y = %d\n", *x, *y);
}
int main()
{
int x = 3, y = 5;
printf("In main, 互换前: x = %d, y = %d\n", x, y);
swap(&x, &y); //指针存放的是地址,所以这里要传入两个地址
printf("In main, 互换后: x = %d, y = %d\n", x, y);
return 0;
}
运行结果
In main, 互换前: x = 3, y = 5
In swap, 互换前: x = 3, y = 5
In swap, 互换后: x = 5, y = 3
In main, 互换后: x = 5, y = 3
可以看到,形参和实参都进行了互换。在C语言中,每个函数都有独立的作用域,它们的内部都是互相独立的,它们的变量只在函数内部生效,不同函数之间无法直接访问对方的变量。而使用指针,实际上就是将实参的地址传递给形参,传完后形参和实参都指向同一个对象,所以对形参的修改会影响到实参。
总结:
- 传值:传值实际是把实参的值赋给形参,相当于copy。那么对形参的修改不会影响到形参
- 传址:传址实际是传值的一种特殊方式,只是他传递的是地址,不是普通的赋值。传地址之后实参和形参都会指向同一个对象,因此对形参的修改会影响到实参
- 指针也是一个变量,所以它可以通过参数传递给函数。
- 无论是传值还是传址,都只是将实参的值拷贝给形参。
传数组
其实并不存在将整个数组作为参数传递的方式,你虽然这么写了,但对方接收到的只是一个地址(相当于传递数组的第一个元素的地址)而已
举个例子
void get_array(int a[10]);
void get_array(int a[10])
{
int i;
for (i=0;i<10;i++)
{
printf("a[%d] = %d\n", i, a[i]);
}
}
int main()
{
int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
get_array(a);
return 0;
}
运行结果
a[0] = 1
a[1] = 2
a[2] = 3
a[3] = 4
a[4] = 5
a[5] = 6
a[6] = 7
a[7] = 8
a[8] = 9
a[9] = 0
虽然结果打印出来了,但这不意味这它把整个数组都传递进去了,我们修改一下代码
void get_array(int a[10]);
void get_array(int a[10])
{
int i;
a[5] = 520;
for (i=0;i<10;i++)
{
printf("a[%d] = %d\n", i, a[i]);
}
}
int main()
{
int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
int i;
get_array(a);
printf("在mian函数里边再打印一次...\n");
for (i=0;i<10;i++){
printf("a[%d] = %d\n", i, a[i]);
}
return 0;
}
我们在get_array
函数中将传递进来的数组中第六个元素a[5]
的值修改为520,并在main
函数中调用完get_array
后再打印一次数组中的元素,如果调用时是将整个数组传递(拷贝)进去,那么就会像传值的方式一样,main
函数中数组的值不会发生变化。运行一下结果
a[0] = 1
a[1] = 2
a[2] = 3
a[3] = 4
a[4] = 5
a[5] = 520
a[6] = 7
a[7] = 8
a[8] = 9
a[9] = 0
在mian函数里边再打印一次...
a[0] = 1
a[1] = 2
a[2] = 3
a[3] = 4
a[4] = 5
a[5] = 520
a[6] = 7
a[7] = 8
a[8] = 9
a[9] = 0
运行完我们可以发现,数组在get_array
和main
函数中都发生了改变,这就说明在传递数组时,并不是将整个数组作为参数传递进去。实际上,传递过去的只有数组的第一个元素的地址(首地址),get_array
函数只接收了一个地址。
void get_array(int b[]); //传递进来的实际上是一个地址,中括号内可以不写
void get_array(int b[])
{
printf("sizeof b = %d\n", sizeof(b));
}
int main()
{
int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
printf("sizeof a = %d\n", sizeof(a));
get_array(a);
return 0;
}
运行结果
sizeof a = 40 //10个整形元素的数组占40个字节
sizeof b = 8 //64位系统指针占8个字节,32位系统指针占4个字节
可变参数
实现可变参数,需要包含一个头文件叫:<stdarg.h>
这个头文件中有三个宏和一个类型是我们需要用到的,一个类型是 va_list
,三个宏,一个是 va_start
,一个是 va_arg
,还有一个是 va_end
。这里的va就是 variable-argument(可变参数)的缩写。
注:va_start()
的第二个参数是函数的最后一个参数名,而不是参数的数量。详见下面代码注释。
#include<stdio.h>
#include<stdarg.h>
int sum(int n, ...); // "..."是占位符,表示参数个数不确定
int sum(int n, ...)
{
int i, sum = 0;
va_list vap; //定义参数列表
va_start(vap, n);//初始化参数列表,需要两个参数(参数列表,参数名)
//如果是 int sum(int gg, ...); 则这里应该是 va_start(vap, gg);
for (i=0;i<n;i++)
{
sum += va_arg(vap, int);//获取参数的值
}
va_end(vap); //收尾工作,关闭参数列表
return sum;
}
int main()
{
int result;
result = sum(3,1,2,3);
printf("result = %d\n",result);
result = sum(5,1,2,3,4,5);
printf("result2 = %d\n",result);
return 0;
}
运行结果
result = 6
result2 = 15
指针函数和函数指针
指针函数
我们说函数的类型,事实上指的就是函数的返回值。根据需求,一个函数可以返回字符型、整型和浮点型这些类型的数据,当然,它还可以返回指针类型的数据。定义的时候只需要跟定义指针变量一样,在类型后边加一个星号即可。
所以,用指针变量作为函数的返回值就是指针函数。
char *getWord(char);
char *getWord(char c)
{
switch(c)
{
case 'A': return "Apple"; //实际返回字符串第一个字符A的地址
case 'B': return "Banana";
case 'C': return "Cat";
case 'D': return "Dog";
default : return "None";
}
}
int main()
{
char input;
printf("请输入一个字母 : ");
scanf("%c",&input);
printf("%s\n",getWord(input));
return 0;
}
运行结果
请输入一个字母 : A
Apple
... //只演示一种,其他结果不作演示
C语言中通常是没有一个专门的类型来定义字符串的,一般都用char类型的指针(char *
)来定义字符串,char类型的指针用来指向字符串第一个字符(首地址)。上面程序中getWord
函数的返回值类型是字符串,所以函数类型可以定义为字符类型指针(char *
)
不要返回局部变量的指针
先来看一下这段代码
char *getWord(char);
char *getWord(char c)
{
char str1[] = "Apple";
char str2[] = "Banana";
char str3[] = "Cat";
char str4[] = "Dog";
char str5[] = "None";
switch(c)
{
case 'A': return str1; //line 11
case 'B': return str2; //line 12
case 'C': return str3; //line 13
case 'D': return str4; //line 14
default : return str5; //line 15
}
}
int main()
{
char input;
printf("请输入一个字母 : ");
scanf("%c",&input);
printf("%s\n",getWord(input));
return 0;
}
运行结果
In function 'getWord':
line11: warning: function returns address of local variable [-Wreturn-local-addr]
line12: warning: function returns address of local variable [-Wreturn-local-addr]
line13: warning: function returns address of local variable [-Wreturn-local-addr]
line14: warning: function returns address of local variable [-Wreturn-local-addr]
line15: warning: function returns address of local variable [-Wreturn-local-addr]
请输入一个字母 : A
能看到程序运行出来得不出想要的结果,并且弹出警告function returns address of local variable
(函数返回一个局部变量的地址)。
为啥不能返回局部变量的地址呢?函数内部定义的变量我们称之为局部变量,局部变量的作用域(就是它的有效范围)仅限于函数内部,出了函数它就什么都不是了。
所以一定要注意不要返回局部变量的指针。
函数指针
指针函数,它是个函数;函数指针,就应该是个指针。顾名思义,它就是一个指向函数的指针。
- 指针函数 ->
int *p();
- 函数指针 ->
int (*p)();
注意:本质上,函数表示法就是指针表示法,因为函数的名字经过求值会变成函数的地址。所以在定义了函数指针后,给它传递一个已经被定义的函数名,即可通过该指针进行调用
int square(int);
int square(int num)
{
return num*num;
}
int main()
{
int num;
int (*fp)(int); //定义一个函数指针,
printf("请输入一个整数 : ");
scanf("%d", &num);
fp = square; //将函数指针fp指向函数square
//此处也可以写成fp = □ 之所以省略&是因为函数名本身就相当于一个地址
printf("%d * %d = %d\n",num,num,(*fp)(num));
return 0;
}
运行结果
请输入一个整数 : 5
5 * 5 = 25
函数指针作为参数
函数指针也可以作为参数进行传递,举个例子:
int add(int, int);
int sub(int, int);
int calc(int (*fp)(int, int), int, int);
//声明calc函数,第一个参数是一个函数指针
int add(int num1, int num2)
{
return num1 + num2 ;
}
int sub(int num1, int num2)
{
return num1 - num2 ;
}
int calc(int (*fp)(int, int), int num1, int num2)
{
return (*fp)(num1,num2); //调用函数指针,传入num1和num2
}
int main()
{
printf("3 + 5 = %d\n", calc(add, 3, 5));
//调用calc函数,参数指针fp指向函数add
printf("3 - 5 = %d\n", calc(sub, 3, 5));
return 0;
}
运行结果
3 + 5 = 8
3 - 5 = -2
函数指针作为返回值
让函数指针作为返回值,我们举个例子:现在让用户输入一个表达式,然后程序根据用户输入的运算符来决定调用 add 还是 sub 函数进行运算
int add(int, int);
int sub(int, int);
int calc(int (*)(int, int), int, int); //声明时,参数里指针名也可以省略
int (*select(char))(int, int); //运算方向 左->右
//①*select(char) 声明函数select,有一个参数是char类型,返回值是一个指针
//②去掉函数名及对应参数int (*)(int, int), 返回的指针为整形且有两个参数
int add(int num1, int num2)
{
return num1 + num2 ;
}
int sub(int num1, int num2)
{
return num1 - num2 ;
}
int calc(int (*fp)(int, int), int num1, int num2)
{
return (*fp)(num1,num2); //调用函数指针,传入num1和num2
}
int (*select(char op))(int, int)
{
switch(op)
{
case '+' : return add;
case '-' : return sub;
}
}
int main()
{
int num1, num2;
char op;
int (*fp)(int, int);
printf("请输入一个式子(如1+3) : ");
scanf("%d%c%d", &num1, &op, &num2);
fp = select(op);
printf("%d %c %d = %d\n", num1, op, num2,calc(fp,num1,num2));
return 0;
}
运行结果
请输入一个式子(如1+3) : 1+5
1 + 5 = 6
局部变量和全局变量
局部变量
局部变量,也称内部变量,是指在一个函数内部或复合语句内部定义的变量。我们学习函数的时候发现,不同函数之间定义的变量,它们是无法相互进行访问的。
分析一下这段代码
int main()
{
int i = 520;
printf("before, i = %d\n",i);
for (int i=0;i<10;i++) //C99新标准,允许在 for 语句的第一个表达式部分声明变量,它的作用范围仅限于复合语句的内部。
{
printf("%d\n",i);
}
printf("after, i = %d\n",i);
return 0;
}
运行结果
before, i = 520
0
1
2
3
4
5
6
7
8
9
after, i = 520
程序运行正常。值得一提的是,这里 for 语句因为定义了同名的i变量,所以它屏蔽了第一个定义的i变量。
注意:C 语言允许在程序的任意位置声明变量
全局变量
-
在函数里面定义的变量,我们叫局部变量。那么,在函数外边定义的变量,我们就叫全局变量,也叫外部变量。
-
有时候,我们可能需要在多个函数中使用共同的一个变量,那么就会用到全局变量。因为全局变量可以被本程序中其他函数所共用的。
void a(); void b(); void c(); int count = 0; //count定义在所有函数外,为全局变量 void a() { count++; } void b() { count++; } void c() { count++; } int main() { a(); b(); c(); b(); printf("count = %d",count); return 0; }
运行结果
count = 4
-
与局部变量不同,如果不对全局变量进行初始化,那么它会自动初始化为0
-
如果在函数的内部存在一个与全局变量同名的局部变量,编译器并不会报错,而是再函数中屏蔽全局变量(也就是说在这个函数中,全局变量不起作用)。
void func(); int a, b = 520; ////全局变量a未初始化,自动初始化为0 void func() { int b; //局部变量 a = 880; //将全局变量a初始化为880 b = 120; //将局部变量b初始化为120 printf("In func, a= %d, b = %d\n", a,b);//屏蔽外部变量b,打印局部变量b } int main() { printf("In main, a = %d, b = %d\n",a,b); func(); printf("In main, a = %d, b = %d\n",a,b); return 0; }
运行结果
In main, a = 0, b = 520 In func, a= 880, b = 120 In main, a = 880, b = 520
-
如果一个全局变量,在函数定义之后才被定义。
void func(); void func() { count ++; } int count = 0; int main() { func(); printf("%d\n,count"); return 0; }
运行结果
'count' undeclared (first use in this function)
程序报错,提示全局变量
count
未被声明
extern关键字
extern关键字相当于告诉编译器:这个变量我在后边定义了,你先别急着报错。使用extern关键字对全局变量进行修饰,就可以在定义完函数后再定义全局变量。
void func();
void func()
{
extern count;
count ++;
}
int count = 0;
int main()
{
func();
printf("%d\n,count");
return 0;
}
运行结果
1
不要大量的使用全局变量
- 使用全局变量会使你的程序占用更多的内存,因为全局变量从被定义时候开始,直到程序退出才被释放。
- 污染命名空间,虽然局部变量会屏蔽全局变量,但这样一来也会降低程序的可读性,人们往往很难一下子判断出每个变量的含义和作用范围。
- 提到了程序的耦合性,牵一发而动全身,时间久了,代码长了,你都不知道全局变量被哪些函数修改过。
作用域和链接属性
作用域
- 当变量被定义在程序的不同位置时,它的作用范围是不一样的。这个作用范围就是我们所说的作用域。
- C 语言编译器可以确认 4 种不同类型的作用域:
- 代码块作用域
- 文件作用域
- 原型作用域
- 函数作用域
代码块作用域(block scope)
-
在代码块中定义的变量,具有代码块作用域。作用范围是从变量定义的位置开始,到标志该代码块结束的右大括号(})处
-
尽管函数的形式参数不在大括号内定义,但其同样具有代码块作用域,隶属于包含函数体的代码块
int main(void) //void,表示函数没有参数 { int i = 100; // i1 { //代码块1 开始 int i = 110; // i2 作用域代码块1 { //代码块2 开始 int i = 120; // i3 作用域代码块2 printf("i = %d\n",i); //打印i3 } { //代码块3 开始 与代码块2同级 printf("i = %d\n",i); //打印i2 int i = 130; // i4 作用域代码块3 printf("i = %d\n",i); //打印i4 } printf("i = %d\n",i); //打印i2 } printf("i = %d\n",i); //打印i1 //打印顺序: i3->i2->i4->i2->i1 return 0; }
运行结果
i = 120 i = 110 i = 130 i = 110 i = 100
文件作用域(file scope)
-
任何在代码块之外声明的标识符都具有文件作用域,作用范围是从它们的声明位置开始,到文件的结尾处都是可以访问的。
-
另外,函数名也具有文件作用域,因为函数名本身也是在代码块之外。
void func(void); //声明函数 int main(void) { extern int count; //extern关键字 func(); count++; printf("In main, count = %d\n",count); return 0; } int count; void func(void) { count++; printf("In func, count = %d\n",count); }
原型作用域(prototype scope)
-
原型作用域只适用于那些在函数原型中声明的参数名。我们知道函数在声明的时候可以不写参数的名字(但参数类型是必须要写上的),其实多尝试你还可以发现,函数原型的参数名还可以随便写一个名字,不必与形式参数相匹配(当然,这样做毫无意义)。允许你这么做,只是因为原型作用域起了作用。
void func(int a, int b, int c); void func(int d, int e, int f) { }
函数作用域(function scope)
- 函数作用域只适用于 goto 语句的标签,作用将 goto 语句的标签限制在同一个函数内部,以及防止出现重名标签。
定义和声明
- 当一个变量被定义的时候,编译器为变量申请内存空间并填充一些值。
- 当一个变量被声明的时候,编译器就知道该变量被定义在其他地方。
- 声明通知编译器该变量名及相关的类型已存在,不需要再为此申请内存空间。
- 局部变量既是定义又是声明
- 定义只能来一次,否则就叫做重复定义某个同名变量;而声明可以有很多次。
链接属性
简单的来说,编译器将你的源文件变成可执行程序需要经过两个步骤:编译和链接。编译过程主要是将你写的源代码生成机器码格式的目标文件,而链接过程则是将相关的库文件添加进来(比如你在源文件中调用了 stdio
库的 printf
函数,那么在这个过程中,就把printf
函数的代码添加进来),然后整合成一个可执行程序。详细参考:编译器的工作流程
大型的程序都有好些个源文件构成,在不同文件中的同名标识符,编译器是通过链接属性来处理的。
在 C 语言中,链接属性一共有三种:
-
external(外部的)-- 多个文件中声明的同名标识符表示同一个实体
-
internal(内部的)-- 单个文件中声明的同名标识符表示同一个实体
-
none(无)-- 声明的同名标识符被当作独立不同的实体(比如函数的局部变量,因为它们被当作独立不同的实体,所以不同函数间同名的局部变量并不会发生冲突)
-
只有具备文件作用域的标识符才能拥有 external 或 internal 的链接属性,其他作用域的标识符都是 none 属性
-
默认情况下,具备文件作用域的标识符拥有 external 属性。也就是说该标识符允许跨文件访问。对于 external 属性的标识符,无论在不同文件中声明多少次,表示的都是同一个实体。
//文件《test.c》代码 #include<stdio.h> #include"a.c" #include"b.c" #include"c.c" void a(void); void b(void); void c(void); int count; int main(void) { a(); b(); c(); printf("count = %d\n",count); return 0; }
//文件《a.c》代码 extern int count; void a(void) { count++; }
//文件《b.c》代码 extern int count; void b(void) { count++; }
//文件《c.c》代码 extern int count; void c(void) { count++; }
-
使用 static 关键字可以使得原先拥有 external 属性的标识符变为 internal 属性。这里有两点需要注意:
-
使用 static 关键字修改链接属性,只对具有文件作用域的标识符生效(对于拥有其他作用域的标识符是另一种功能)
-
链接属性只能修改一次,也就是说一旦将标识符的链接属性变为 internal,就无法变回 external 了
static int B;
-
生存期和存储类型
生存期
- C 语言的变量拥有两种生存期,分别是静态存储期和自动存储期。
- 具有文件作用域的变量具有静态存储期(比如全局变量),函数名也拥有静态存储期。具有静态存储期的变量在程序执行期间将一直占据存储空间,直到程序关闭才释放;
- 具有代码块作用域的变量一般情况下具有自动存储期(比如局部变量和形式参数),具有自动存储期的变量在代码块结束时将自动释放存储空间。
存储类型
- C 语言变量的作用域、链接属性和生存期,总得来说,这些都是由变量的存储类型来定义的。变量的存储类型其实是指存储变量值的内存类型,C 语言提供了 5 种不同的存储类型,分别是:
- auto
- register
- static
- extern
- typedef
自动变量(auto)
-
在代码块中声明的变量默认的存储类型就是自动变量,使用关键字 auto 来描述。所以函数中的形式参数、局部变量,包括复合语句的中定义的局部变量都是自动变量。自动变量拥有代码块作用域,自动存储期和空连接属性。
int main() { auto int i, j, k; return 0; }
-
由于这是默认的存储类型,所以不写
auto
也是完全没问题的 -
强调局部变量屏蔽同名的全局变量时,最好写上
auto
int i; int main() { auto int i; return 0; }
寄存器变量(register)
寄存器是存在于 CPU 的内部的,CPU 对寄存器的读取和存储可以说是几乎没有任何延迟。
-
将一个变量声明为寄存器变量,那么该变量就有可能被存放于CPU的寄存器中。为什么这里说有可能呢?因为CPU的寄存器空间是十分有限,所以编译器并不会让你将所有声明为register的变量都放到寄存器中。事实上,有可能所有的register关键字都被忽略,因为编译器有自己的一套优化方法,会权衡哪些才是最常用的变量。在编译器看来,它觉得它比你更了解程序。而那些被忽略的register变量,它们会变成普通的自动变量。
-
所以寄存器变量和自动变量在很多方面的是一样的,它们都拥有代码块作用域,自动存储期和空链接属性。
-
注意:当你将变量声明为寄存器变量,那么你就没办法通过取值运算符(&)获得该变量的地址。
int main() { register int i = 520; printf("Addr of i : %p\n",&i); return 0; }
运行结果
error: address of register variable 'i' requested
静态局部变量(static)
static 用于描述具有文件作用域的变量或函数时,表示将其链接属性从 external 修改为 internal,它的作用范围就变成了仅当前源文件可以访问。
-
默认情况下,局部变量是 auto 的,具有自动存储期的变量。如果使用static 来声明局部变量,那么就可以将局部变量指定为静态局部变量。
-
static 使得局部变量具有静态存储期,所以它的生存期与全局变量一样,直到程序结束才释放。
void func(void); void func(void) { static int count = 0; //不赋值也会自动初始化为0 printf("count = %d\n",count); count++; } int main() { int i; for (i=0;i<10;i++) { func(); } return 0; }
运行结果
count = 0 count = 1 count = 2 count = 3 count = 4 count = 5 count = 6 count = 7 count = 8 count = 9 //去掉static则全部为0
static 和 extern
作用于文件作用域的 static 和 extern,static关键字使得默认具有external链接属性的标识符变成 interna 链接属性,而 extern 关键字是用于告诉编译器这个变量或函数在别的地方已经定义过了,先去别的地方找找,不要急着报错。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 如何使用 Uni-app 实现视频聊天(源码,支持安卓、iOS)
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)