【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_arraymain函数中都发生了改变,这就说明在传递数组时,并不是将整个数组作为参数传递进去。实际上,传递过去的只有数组的第一个元素的地址(首地址),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 = &square; 之所以省略&是因为函数名本身就相当于一个地址
    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(无)-- 声明的同名标识符被当作独立不同的实体(比如函数的局部变量,因为它们被当作独立不同的实体,所以不同函数间同名的局部变量并不会发生冲突)

  • 只有具备文件作用域的标识符才能拥有 externalinternal 的链接属性,其他作用域的标识符都是 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

作用于文件作用域的 staticexternstatic关键字使得默认具有external链接属性的标识符变成 interna 链接属性,而 extern 关键字是用于告诉编译器这个变量或函数在别的地方已经定义过了,先去别的地方找找,不要急着报错。

posted @   芝麻凛  阅读(0)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 如何使用 Uni-app 实现视频聊天(源码,支持安卓、iOS)
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
点击右上角即可分享
微信分享提示