注:该笔记基于B站up尚硅谷的C语言教程视频

【尚硅谷C语言零基础入门教程(宋红康c语言程序设计精讲,含C语言考研真题)】 https://www.bilibili.com/video/BV1Bh4y1q7Nt/?share_source=copy_web&vd_source=888f84d6f6569e62ce9b7c4648b24037


目录

1.函数的基本使用

1.1 函数的分类

1.2 函数的声明格式

        1.返回值类型

        2.函数名

        3.参数列表

        4.函数体

        5.return 语句

1.3 声明注意事项

1.4 函数的调用

2.进一步认识函数

2.1 关于main()

2.2 关于exit()

2.3 函数原型

3.参数传递机制

3.1 形参、实参

3.2 参数传递机制:值传递

3.3 参数传递机制:址传递

3.4 举例

3.5 C++中的引用传递

4.函数的高级用法

4.1 递归函数

4.2 可变参量

4.3 指针函数(返回值是指针)

4.4 函数指针(指向函数的指针)

4.5 回调函数

4.6 函数说明符

5.再谈变量

5.1 按声明位置的不同分类

局部变量与全局变量的对比:

全局变量使用建议:不在必要时不要使用全局变量。

5.2 按存储方式不同分类

5.2.1 动态(自动)存储方式

5.2.2 静态存储方式

5.3 其它变量修饰符

5.3.1 寄存器变量(register变量)

5.3.2 extern修饰变量

        在一个文件内扩展全局变量的作用域

        将全局变量的作用域扩展到其它文件

5.3.3 static修饰全局变量

5.3.4 const修饰变量

小结:变量类型及存储位置


1.函数的基本使用

1.1 函数的分类

  • 从程序执行的角度看
    • 主函数:main()函数
    • 子函数:非main()函数

        每个C应用程序有且必须仅有一个main()主函数。无论主函数写在什么位置,C程序总是从main()函数开始执行。main()函数可以调用其它的子函数,子函数之间可以相互调用任意多次。

  • 是否允许源文件外调用角度看
    • 内部函数
    • 外部函数
  • 从用户使用的角度看
    • 库函数(或标准函数),使用库函数时,必须包含相应的头文件。
    • 用户自定义函数

1.2 函数的声明格式

返回值类型 函数名(数据类型1 形参1,数据类型2 形参2,…,数据类型n 形参n){
        函数体;
}

说明:

        1.返回值类型

        函数调用后,是否需要在主调函数(比如main()函数)中得到一个确定的、返回的值,针对这个返回值的描述,就是返回值类型。返回值常常是一个计算的结果,或是用来作为判断函数执行状态(完成还是出错)的标记。函数按是否有返回值来分类的话,分为:

  • 无返回值类型:针对函数无返回值或明确不需返回值的情况,使用 void (即空类型)表示。
  • 有返回值的类型:指明具体的类型。比如, int、float、char等。如果省略,默认为int类型。
    • 有返回值类型,则需要在函数体内与“ return 返回值”搭配使用。返回值需要与返回值类型一致。如果返回值类型非 void,但被调函数中没有 return 语句,函数会返回一个不确定的值。
        2.函数名

        函数名,属于标识符。要遵循标识符的命名规则,同时要见名知意,以增强程序的可读性。

        3.参数列表

        函数名后面的圆括号里面,可以声明参数的类型和参数名。表示完成函数体功能时需要外部提供的数据列表。根据是否有参数,函数可以分为:

  • 无参函数,在调用无参函数时,主调函数不向被调用函数传递数据。但函数名后的()不能省略。
    • 举例:abort():立即终止程序的执行,不接受任何形参。
  • 有参函数,在调用函数时,主调函数在调用被调用函数时,通过参数向被调用函数传递数据。函数参数为多个参数时,其间用逗号隔开。
    • 举例:add(int m,int n),strcmp(const char *str1, const char*str2)
        4.函数体

        函数体要写在大括号{}里面,是函数被调用后要执行的代码。对于调用者来说,不了解函数体如何实现的,并不影响函数的使用。

        5.return 语句
  • return语句的作用:① 结束函数的执行 ②将函数运算的结果返回。
  • return语句后面就不能再写其它代码了,否则会报错。(与break、continue情况类似)
  • 下面分两种情况讨论:
    • 情况1:返回值类型不是void时,函数体中必须保证一定有return 返回值; 语句,并且要求该返回值结果的类型与声明的返回值类型一致或兼容。
    • 情况2:返回值类型是void时,函数体中可以没有return语句。如果要用return语句提前结束函数的执行,那么return后面不能跟返回值,直接写 return; 

1.3 声明注意事项

  • C程序中的所有函数都是相互独立的。一个函数并不从属于另一个函数,即函数不能嵌套定义
  • 同一个程序中函数不能重名,函数名用来唯一标识这一个函数。即在标准C语言中,不支持重载。

1.4 函数的调用

        调用函数时,需要传入实际的参数值。如果没有参数,只要在函数名后面加上圆括号即可。

说明:

  • 调用时,参数个数必须与函数声明里的参数个数一致,参数过多或过少都会报错。
  • 函数间可以相互调用,但不能调用main函数,因为main函数是被操作系统调用的,作为程序的启动入口。反之,main() 函数可以调用其它函数。
  • 函数的参数和返回值类型,会根据需要进行自动类型转换。

2.进一步认识函数

2.1 关于main()

  • main()的作用

        C 语言规定, main() 是程序的入口函数,即所有的程序一定要包含一个 main() 函数。程序总是从这个函数开始执行,如果没有该函数,程序就无法启动。main()函数可以调用其它函数,但其它函数不能反过来调用main()函数。main()函数也不能调用自己。

  • main() 的一般格式

int main() {
        //函数体(略)


        return 0;
}

        C 语言约定:返回值 0 表示函数运行成功;返回其它非零整数值,表示运行失败,代码出了问题。系统根据 main() 的返回值,作为整个程序的返回值,确定程序是否运行成功。

        正常情况下,如果 main() 里面省略 return 0 这一行,编译器会自动加上,即 main() 的默认返回值为0。所以,也可以声明如下:

int main() {
        //函数体(略)
}

  • main()函数的其它写法

        main()的声明中可以带有两个参数,格式如下。

int main(int argc, char *argv[]) {
        //函数体
}

        其中,形参argc,全称是argument count,表示传给程序的参数个数,其值至少是1;而argv,全称是argument value,argv[]则是指向字符串的指针数组。

        这种方式可以通过命令行的方式,接收指定的字符串传给参数argv。举例:

#include <stdio.h>

int main(int argc, char *argv[]) {
    printf("argc = %d\n",argc);
    //函数体
    for(int i = 0; i < argc; i++){
        printf("%s\n",argv[i]);
    }

    return 0;
}

使用命令行的方式进行赋值:

  • 方式1:使用CLion中的终端进行调试

  • 方式2:使用命令行执行

例题:

已知有以下sample.c程序的定义:

/*sample.c*/
#include <stdio.h>

int main(int argc,char *argv[]){
    printf("%c",*++argv[2]);

    return 0;
}

        将该程序编译成可执行文件sample后,若在命令行下输入如下命令: sample January February March 则该命令正确的输出是()。 A.J  B.a  C.F  D.e

答案:D

解析:

        sample是程序名称,占用argv[0]的位置,argv[2]对应的内容是February。

        [] 的优先级高于 ++(前缀递增),但 ++ 运算符作用于 argv[2] 这个指针本身。
        argv[2] 指向字符串 "February" 的首字符 'F'
        ++argv[2] 使指针向后移动一个字符,指向 'e'
        *++argv[2] 取该指针指向的字符,即 'e'

2.2 关于exit()

        exit() 函数用来终止整个程序的运行。一旦执行到该函数,程序就会立即结束。该函数的原型定义在头文件 stdlib.h 里面。

        exit() 可以向程序外部返回一个值,它的参数就是程序的返回值。一般来说,使用两个常量作为它的参数,这两个常量也是定义在stdlib.h 里面:

  • EXIT_SUCCESS (相当于 0)表示程序运行成功,正常结束;
  • EXIT_FAILURE   (相当于 1)表示程序异常中止。

        在main()函数结束时也会隐式地调用exit()函数,exit() 等价于使用return 语句。其它函数使用 exit() ,就是终止整个程序的运行,没有其它作用。
        C 语言还提供了一个 atexit() 函数,用来登记 exit() 执行时额外执行的函数,用来做一些退出程时的收尾工作。该函数的原型也是定义在头文件 stdlib.h 。

int atexit(void (*func)(void));

        atexit() 的参数是一个函数指针。注意,它的参数函数不能接受参数,也不能有返回值。

        举例:

void print(void) {
        printf("something wrong!\n");
}


atexit(print);
exit(EXIT_FAILURE);

        上例中, exit() 执行时会先自动调用 atexit() 注册的 print() 函数,然后再终止程序。

2.3 函数原型

        所谓函数原型(function prototype) ,就是函数在调用前提前告诉编译器每个函数的基本信息(它包括了返回值类型、函数名、参数个数、参数类型和参数顺序),其它信息都不需要(不用包括函数体、参数名),函数具体的实现放在哪里,就不重要了。在函数调用时,检查函数原型和函数声明是否一致,只要一致就可以正确编译、调用。

3.参数传递机制

3.1 形参、实参

  • 形参(formal parameter) :在定义函数时,函数名后面括号()中声明的变量称为形式参数,简称形参。
  • 实参(actual parameter) :在调用函数时,函数名后面括号()中使用的值/变量/表达式称为实际参数,简称实参。

说明:

  • 实参与形参的类型应相同或赋值兼容,个数相等、一 一对应。
  • 形参只是一个形式,在调用之前并不分配内存。函数调用时,系统为形参分配内存单元,然后将主调函数中的实参传递给被调函数的形参。被调函数执行完毕,通过return语句返回结果,系统将形参的内存单元释放。

3.2 参数传递机制:值传递

        值传递,又称传值方式、数据复制方式,就是把主调函数的实参值复制给被调用函数的形参,使形参获得初始值。接着在函数内对形参值的修改,不影响实参值。值传递,是单向传递,只能把实参的值传递给形参,而不能把形参的值再传回给实参。

  • 默认传递值的类型:基本数据类型 (整型类型、浮点类型,字符类型)、结构体、共用体、枚举类型。
  • 形参、实参各占独立的存储空间。函数在被调用时,给形参动态分配临时存储空间,函数返回释放。

3.3 参数传递机制:址传递

        地址传递,又称传地址方式、地址复制方式、指针传递,就是把实参地址常量进行复制,传送给形参。

  • 默认传递地址的类型:指针、数组。实参将地址传递给形参,二者地址值相同。
    • 当指针作为函数的形参时,实参传递给形参的是地址,在函数中通过形参保存的地址访问实参,进而在函数中通过地址对实参的修改影响到实参的值。这也称为双向传递
    • 当传递数组首元素地址时,即把实参数组的起始地址传递给形参。这样形参和实参数组就占用了共同的存储空间。在被调函数中,如果通过形参修改了数组元素值,调用函数后实参数组元素值也发生相应变化。

错误:

  • 错误方式1:
void swap(int *p1, int *p2) {
    int *temp;
    *temp = *p1;
    *p1 = *p2;
    *p2 = *temp;
}

        上述代码中存在错误。问题出在临时指针 temp 没有分配内存空间,因此不能正确保存变量的值。因此对*temp的赋值就没有意义。

  • 错误方式2:
void swap(int *p1, int *p2) { //形参是指针变量
    int *temp;
    temp = p1;
    p1 = p2;
    p2 = temp;
    //printf("*p1 = %d,*p2 = %d\n", *p1, *p2); //*p1 =8,*p2 = 6
}

        在函数内部,只是交换了指针变量本身的值,而没有影响到原始调用函数时传递给 swap 函数的指针变量。

3.4 举例

  • 举例1:多维数组名作为形参

有一个3×4的矩阵,求所有元素中的最大值。

#include <stdio.h>

#define N 3

int maxValue(int array[][4], int n) { //n:第一维的长度
    int max = array[0][0];
    for (int i = 0; i < n; i++)
        for (int j = 0; j < 4; j++)
            if (max < array[i][j])
                max = array[i][j]; //把较大值重新赋值给max

    return max;
}

int main() {
    int a[N][4] = { {11, 33, 3, 17},
                    {32, 54, 6, 68},
                    {24, 17, 34, 12}};
    printf("Max value is %d\n", maxValue(a, N));

    return 0;
}

说明:
        如果函数的参数是二维数组,那么除了第一维的长度可以当作参数传入函数,其他维的长度需要写入函数的定义。也就是说,在定义二维数组时,必须指定列数(即一行中包含几个元素) 。在第2维大小相同的前提下,形参数组的第1维可以与实参数组不同。因为C语言编译系统不检查第一维的大小。例如,实参数组定义为int score[5][10] ;而形参数组定义为int array[][10] ;或int array[8][10] ;均可以。这时形参数组和实参数组都是由相同类型和大小的一维数组组成的。

  • 举例2:变长数组作为参数
#include <stdio.h>
//int sum_array(int a[n],int n) { //报错
    // // ...
//}

int sumArray(int n, int a[n]) {
// ...
}

int main() {
    int a[] = {1, 3, 5, 7};
    int sum = sumArray(4, a);

    return 0;
}

说明:

        数组 a[n] 是一个变长数组,它的长度取决于变量 n 的值,只有运行时才能知道。所以,变量 n 作为参数时,顺序一定要在变长数组前面,这样运行时才能确定数组 a[n] 的长度,否则就会报错。
        因为函数原型可以省略参数名,所以变长数组的原型中,可以使用* 代替变量名,也可以省略变量名。

        int sumArray(int, int [*]);
        int sumArray(int, int []);

        变长数组作为函数参数有一个好处,就是多维数组的参数声明,可以把后面的维度省掉了。

        // 原来的写法
        int sumArray(int a[][4], int n);
        // 变长数组的写法
        int sumArray(int n, int m, int a[n][m]);

        说明:函数 sum_array() 的参数是一个多维数组,按照原来的写法,一定要声明第二维的长度。但是使用变长数组的写法,就不用声明第二维长度了,因为它可以作为参数传入函数。

3.5 C++中的引用传递

  • 情况1:传入数值类型变量
#include <iostream>
void f2(int &x) {
    x = x + 1;
}

int main() {
    int a = 1;
    f2(a);
    std::cout << "a = " << a << std::endl; // a = 2

    return 0;
}

        上述代码在c语言环境中执行会报错,必须在c++环境中执行。

        在这个C++版本中,我们使用了C++的#include <iostream>来代替C的 <stdio.h> ,并使用 std::cout 来替代printf 函数。此外,f1函数的参数类型也从指针 int *x 改为引用 int &x ,这是C++的引用特性,允许更直观地操作变量,而不需要使用指针。其他部分保持不变,代码仍然具有相同的功能,将整数a 的值增加1并打印结果。

  • 情况2:传入结构体变量

        如果传入的是结构体变量,如果内部需要改变结构体的成员,则需要如下声明:

void insert(SqList &L,int x){
        //修改L内部data[]数组的内容,则认为修改了L,因此需要传入引用型L
        //....
}

  • 情况3:传入指针型变量

        上述是普通变量的传入方式,如果传入的变量是指针型变量,且在函数内部需要对传入的指针进行改变,则:

void f(int *&x){ //指针型变量在函数体中需要改变的写法
        x++; //将指针 x 向后移动一个整数的大小。
}

        int *&x :这里 int * 表示指向整数的指针, & 表示引用,因此 int *&x 表示引用指向整数的指针。这个参数允许传递一个指向整数的指针,并且在函数内部可以改变指针的值。

举例1:

#include <iostream>

void f(int *&x) {
    x++;
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    int *ptr = arr; // 将指针指向数组的第一个元素

    std::cout << "Original value: " << *ptr <<std::endl;

    f(ptr); // 传递指针给函数,函数将指针移动到下一个元素
    std::cout << "New value: " << *ptr << std::endl;

    return 0;
}

注意:此程序实现的是传递指针给函数,函数将指针移动到下一个元素。指针变化前后指向的值打印出来。

举例2:将A、B两个链表合并成一个C,此时C发生了改变,需要引用型。A、B没发生改变,不需要引用型。

void merge(LNode *A,LNode *B,LNode *&C){
        LNode *p = A->next;
        LNode *q = B->next;
        LNode *r;
        C = A;
        //...
}

4.函数的高级用法

4.1 递归函数

  • 递归函数调用:函数自己调用自己的现象就称为递归。
  • 递归的分类:直接递归、间接递归。
    • 直接递归:函数自身调用自己。
    • 间接递归:可以理解为A()函数调用B()函数,B()函数调用C()函数,C()函数调用A()函数。

说明:

  • 递归函数包含了一种隐式的循环。
  • 递归函数会重复执行某段代码,但这种重复执行无须循环控制。
  • 递归一定要向已知方向递归,否则这种递归就变成了无穷递归,停不下来,类似于死循环。最终发生栈内存溢出。
  • C语言支持函数的递归调用。

举例1:计算1~n的和

// 递归函数,计算1到n的和
int getSum(int n) {
    // 基本情况:当n为1时,返回1
    if (n == 1) {
        return 1;
    } else {
    // 递归情况:将n与1到n-1的和相加
        return n + getSum(n - 1);
    }
}

int main() {
    int n;
    printf("输入一个正整数:");
    scanf("%d", &n);

    // 调用递归函数计算1到n的和
    int result = getSum(n);

    printf("1到%d的和为%d\n", n, result);

    return 0;
}

举例2:递归函数计算n!

// 递归函数,计算n的阶乘
int factorial(int n) {
    // 基本情况:当n为0或1时,阶乘为1
    if (n == 0 || n == 1) {
        return 1;
    } else {
    // 递归情况:n! = n * (n-1)!
        return n * factorial(n - 1);
    }
}

int main() {
    int n = 5;
    // 调用递归函数计算n的阶乘
    int result = factorial(n);

    printf("%d! = %d\n", n, result);

    return 0;
}

举例3:计算斐波那契数列(Fibonacci)的第n个值,斐波那契数列满足如下规律,

1,1,2,3,5,8,13,21,34,55,....

        即前两个数都是1,从第三个数开始,每个数等于前两个数之和。假设f(n)代表斐波那契数列的第n个值,那么f(n)满足: f(n) = f(n-2) +f(n-1); 其中,n >= 3。

// 递归函数,计算第n个斐波那契数
int fibonacciRecursion(int n) {
    if(n == 1 || n == 2){
        return 1;
    }else{
        return FibonacciRecursion(n - 1) +FibonacciRecursion(n - 2);
    }
}

        如果不使用递归函数,而是使用迭代的方式计算第n个斐波那契数列,如下:

int FibonacciIteration(int n){
    if(n == 1 || n == 2){
        return 1;
    }
    int a = 1;
    int b = 1;
    int temp;
    for(int i = 3; i <= n; i++){
        temp = a + b;
        a = b;
        b = temp;
    }

    /*
     *
     * 新的一项是前两项的和,将前面两项分别定义为a、b,即新的一项为a+b。
     * 计算的时候将b的值赋给a,a+b的值赋给b最终返回b的值即可。
     *
     */


    return b;
}

总结:

  • 使用递归函数大大简化了算法的编写。
  • 递归调用会占用大量的系统堆栈,内存耗用多,在递归调用层次多时速度要比循环慢的多,所以在使用递归时要慎重。
  • 在要求高性能的情况下尽量避免使用递归,递归调用既花时间又耗内存。考虑使用循环迭代。

举例4:走台阶问题

假如有10阶楼梯,小朋友每次只能向上走1阶或者2阶,请问对于n阶台阶一共有多少种不同的走法呢?
        阶数:1 2 3 4
        走法:1 2 3 5
        fun(n) = fun(n - 1) + fun(n - 2)

分析:

动态规划:用上一步的结果来计算下一步的结果
        当有n级台阶的时候,有两种走法,

        1)先走一级,那么接下来面对的就是n-1级台阶,面临的情况与之前n-1级台阶相同。
        2)先走两级,接下来面对的是n-2级台阶,面临的情况与n-2级台阶面临的情况相同。
        即可得出func(n) = func(n-1)+func(n-2)

4.2 可变参量

        有些函数的参数数量是不确定的,此时可以使用C语言提供的可变参数函数(Variadic Functions)。声明可变参数函数的时候,使用省略号 ... 表示可变数量的参数。最常见的例子:

#include <stdarg.h>
int printf(const char* format, ...);

        这里的 ... 表示可以传递任意数量的参数,但是它们都需要与format 字符串中的格式化标志相匹配。注意, ... 符号必须放在参数序列的结尾,否则会报错。

  • 可变参数函数的使用:
    • 为了使用可变参数,你需要引入<stdarg.h> 头文件。
    • 在函数中,需要声明一个va_list 类型的变量来存储可变参数。它必须在操作可变参数时,首先使用。
    • 使用va_start 函数来初始化va_list 类型的变量。它接受两个参数,参数1是可变参数对象,参数2是原始函数里面,可变参数之前的那个参数,用来为可变参数定位。
    • 使用va_arg 函数来逐个获取可变参数的值。每次调用后,内部指针就会指向下一个可变参数。它接受两个参数,参数1是可变参数对象,参数2是当前可变参数的类型。
    • 使用va_end 函数来结束可变参数的处理。

举例:

#include <stdio.h>
#include <stdarg.h>

// 可变参数函数,计算多个整数的平均值
double average(int count, ...) {
    va_list args;             // 声明一个va_list变量,存储可变参数
    va_start(args, count);    // 初始化va_list,指向可变参数的位置
    double sum = 0;

    for (int i = 0; i < count; i++) {
        int num = va_arg(args, int);     // 逐个获取整数参数
        sum += num;
    }
    va_end(args); // 结束可变参数的处理
    return sum / count;
}

int main() {
    double avg = average(5, 10, 20, 30, 40, 50); // 调用可变参数函数
    printf("Average: %lf\n", avg);

    return 0;
}

        可变参数函数,在编写各种工具函数和格式化输出函数时非常有用。但要小心确保传递的参数数量和类型与函数的预期相匹配,以避免运行时错误。

4.3 指针函数(返回值是指针)

        C语言允许函数的返回值是一个指针(地址),这样的函数称为指针函数。

        指针函数的定义的一般格式

返回值类型 *函数名(形参列表) {
        函数体        //函数体中的 return 命令须返回一个地址。

举例1:获取两个字符串中较长的那个字符串

#include <stdio.h>
#include <string.h>

char *maxLengthStr(char *str1, char *str2) { //函数返回char * (指针)
    printf("\nstr1的长度%d,str2的长度%d", strlen(str1),
    strlen(str2));
    if (strlen(str1) >= strlen(str2)) {
        return str1;
    } else {
        return str2
    }
}

int main() {
    char str1[30], str2[30];
    printf("请输入第1个字符串:");
    gets(str1);
    printf("请输入第2个字符串:");
    gets(str2);

    char *str;
    str = maxLengthStr(str1, str2);
    printf("\nLonger string: %s \n", str);

    return 0;
}

拓展:编写函数char *maxLlen (char *string[],int n),用于查找多个字符串中的最长字符串,并返回该字符串的地址。

#include <stdio.h>
#include <string.h>

char *maxLen(char *[], int);

int main() {
    char *pString[5] = {"Atlanta1996", "Sydney2000",
                        "Beijing2008", "London2012", "RIO2016"};
    puts(maxLen(pString, 5));

    return 0;
}

char *maxLen(char *string[], int n) {
    int posion, maxLen;
    maxLen = strlen(string[0]);
    for (int i = 1; i < n; i++){
        if (maxLen < strlen(string[i])){
            maxLen = strlen(string[i]);
            posion = i;
        }
    }

    return string[posion];
}

举例2:动态分配内存

#include <stdio.h>
#include <stdlib.h>

int *func() {
    int *n = (int *)malloc(sizeof(int)); //分配动态内存
    if (n != NULL) {    //检查分配是否成功
        *n = 100;
    }

    return n;
}

int main() {
    int *p = func();
    int n = *p;
    printf("value = %d\n", n);
    
    free(p); // 释放动态分配的内存,以免出现内存泄漏

    return 0;
}

错误写法:

int *func() {
    int n = 100;

    return &n;
}
int main() {
    int *p = func();
    int n = *p;
    printf("value = %d\n", n);

    return 0;
}

        在 func() 函数中,声明了一个整数变量 n,然后返回其地址 &n 。但是,一旦 func() 函数执行完毕,局部变量 n将被销毁,其地址也将无效。这意味着在 main() 函数中,尝试访问 p 指向的地址时,它实际上已经不再是一个有效的内存位置,这会导致未定义的行为。

        如果确实希望返回局部变量的地址,除了使用malloc()函数的方式之外,还可以给局部变量添加static 修饰,此时数据空间在静态数据区分配,静态变量在程序的生命周期内都存在,不会像局部变量那样在函数执行完毕后被销毁。比如:

int *func() {
    static int n = 100;

    return &n;
}

int main() {
    int *p = func();
    int n = *p;
    printf("value = %d\n", n);

    return 0;
}

4.4 函数指针(指向函数的指针)

        一个函数本身就是一段内存里面的代码,总是占用一段连续的内存区域。这段内存区域也有首地址,把函数的这个首地址(或称入口地址)赋予一个指针变量,使指针变量指向函数所在的内存区域,然后通过指针变量就可以找到并调用该函数。这种指针就是函数指针。

        函数指针,就是指向函数的指针。

格式:

返回值类型  (*指针变量名)(参数列表);

        其中,参数列表中可以同时给出参数的类型和名称,也可以只给出参数的类型,省略参数的名称。

举例:

void print(int a) {
    printf("%d\n", a);
}

int main() {
    void (*print_ptr)(int); //1
    print_ptr = &print; //2

    return 0;
}

        注释1处,变量 print_ptr 是一个函数指针,它可以指向函数返回值类型为void且有1个整型参数的函数。
        注释2处,print_ptr 指向函数 print() 的地址。函数 print() 的地址可以用 &print 获得。

        注意: (*print_ptr) 的小括号一定不能省略,否则因为函数参数 (int) 的优先级高于 * ,整个结构就变成了函数原型: void*print_ptr(int) , void * 成为返回类型,变为指针函数。

举例:用函数指针来实现对函数的调用,返回两个整数中的最大值。

int max(int a, int b) {
    return a > b ? a : b;
}

int main() {
    int x, y;
    int (*pmax)(int, int) = &max; // 使用函数指针

    printf("输入两个整数:");
    scanf("%d %d", &x, &y);

    int maxVal = (*pmax)(x, y);
    printf("较大值为: %d\n", maxVal);

    return 0;
}
  •         函数名本身就是指向函数代码的指针,通过函数名就能获取函数地址。也就是说, print 和 &print 等价。

void (*print_ptr)(int) = &print;
// 或
void (*print_ptr)(int) = print;

        注意:

  • 对指向函数的指针变量不能进行算术运算,如p+n,p++,p--等运算是无意义的。
  • 用函数名调用函数,只能调用所指定的一个函数,而通过指针变量调用函数比较灵活,可以根据不同情况先后调用不同的函数。

4.5 回调函数

        回调函数是一个作为参数传递给另一个函数的函数。它被期望在某个特定时刻(或特定条件满足时)被“主函数”回过头来调用。

        原理简述如下: 有一个函数(假设函数名为fun),它有两个形参(x1和x2),定义x1和x2为指向函数的指针变量。在调用函数fun时,实参为两个函数名f1和f2,给形参传递的是函数f1和f2的入口地址。这样在函数fun中就可以调用f1和f2函数了。

举例1:使用回调函数的方式,给一个整型数组int arr[10] 赋10个随机数。

#include <stdio.h>
#include <stdlib.h>

// 回调函数
void initArray(int *array, int arrayLen, int (*f)()) {
    for (int i = 0; i < arrayLen; i++)
        array[i] = (*f)();
}

// 获取随机值
int getRandomValue() {
    return rand();
}

int main() {
    int arrLen = 10;
    int myArray[arrLen];
    initArray(myArray, arrLen, &getRandomValue);

    //遍历数组
    for (int i = 0; i < 10; i++) {
        printf("%d ", myArray[i]);
    }
    printf("\n");

    return 0;
}

举例2:有两个整数a和b,由用户输入1,2或3。如输入1,程序就给出a和b中的大者,输入2,就给出a和b中的小者,输入3,则求a与b之和。

#include <stdio.h>

int fun(int x, int y, int (*p)(int, int)); //fun函数声明
int max(int, int); //max函数声明
int min(int, int); //min函数声明
int add(int, int); //add函数声明

int main() {
    int a = 10, b = 20, n;
    printf("please choose 1,2 or 3:");
    scanf("%d", &n); //输入1,2或3之一
    switch(n){
        case 1:
            fun(a, b, max); //输入1时调用max函数
            break;
        case 2:
            fun(a, b, min); //输入2时调用min函数
            break;
        case 3:
            fun(a, b, add); //输入3时调用add函数
            break;
    }

    return 0;
}

int fun(int x, int y, int (*p)(int, int)){ //定义fun函数
    int result;
    result = (*p)(x, y);
    printf("%d\n", result); //输出结果
}

int max(int x, int y){ //定义max函数
    int z;
    if (x > y)
        z = x;
    else
        z = y;
    printf("max=");

    return z; //返回值是两数中的大者
}

int min(int x, int y){ //定义min函数
    int z;
    if(x < y)
        z = x;
    else
        z = y;
    printf("min=");

    return z; //返回值是两数中的小者
}

int add(int x, int y){ //定义add函数
    int z;
    z = x + y;
    printf("sum=");

    return z; //返回值是两数之和
}

4.6 函数说明符

  • 内部函数(静态函数)

        如果在一个源文件中定义的函数只能被本文件中的函数调用,而不能被同一源程序其他文件中的函数调用,这种函数称为内部函数。此时,内部函数需要使用static修饰。

        定义内部函数的一般形式是:

static 类型说明符 函数名(<形参表>)

        说明:f()函数只能被本文件中的函数调用,在其他文件中不能调用此函数。但此处static的含义并不是指存储方式,而是指对函数的调用范围只局限于本文件。允许不同文件定义同名函数互不干扰。

  • 外部函数

        外部函数在整个源程序中都有效,只要定义函数时,在前面加上extern 关键字即可。

        其定义的一般形式为:

extern 类型说明符 函数名(<形参表>)

        因为函数与函数之间都是并列的,函数不能嵌套定义,所以函数在本质上都具有外部性质。因此在定义函数省去extern说明符时,则隐含为外部函数。

        main()函数外部函数的原型使用前需要给出,并用 extern 说明该函数的定义来自其它文件。

5.再谈变量

5.1 按声明位置的不同分类

  • 局部变量(Local Variable)

        函数体内定义的变量或函数的形参,都是内部变量,称为局部变量。局部变量只能在定义它的函数中使用。

  • 全局变量(Global Variable)

        在函数之外定义的变量就是外部变量,称为全局变量(或全程变量)

        一个程序中,凡是在全局变量之后定义的函数,都可以使用在函数之前定义的全局变量。也就是说,一个全局变量,可以被多个函数使用,但并不一定能被所在程序中的每一个函数使用。

注意:
        如果全局变量与函数中定义的局部变量重名,则在函数内部调用此同名的变量,默认是局部变量(就近原则)。

利用全局变量传递数据:

  • 利用全局变量进行函数间的数据传递,简单而运行效率高。
  • 全局变量使用过多增加了函数间联系的复杂性,降低了函数的独立性。
局部变量与全局变量的对比:
  • 作用域
    • 局部变量:它的作用域只能在其定义的函数或代码块内部,超出该范围将无法访问。
    • 全局变量:它的作用域默认是整个程序,也就是所有的代码文件。
  • 访问权限
    • 局部变量:由于局部变量的作用域仅限于定义它们的函数或代码块,只有在该范围内才能访问它们。其他函数无法直接访问局部变量。
    • 全局变量:全局变量可以被程序中的任何函数访问,只要它们在被访问之前已经被声明。
  • 生命周期
    • 局部变量:局部变量的生存周期仅限于定义它们的函数或代码块的执行时间。它们在函数或代码块执行结束后会被销毁。
    • 全局变量:全局变量的生命周期从程序开始运行直到程序结束。它们在程序整个运行期间都存在。
  • 初始值
    • 局部变量:系统不会对其默认初始化,必须对局部变量初始化后才能使用,否则,程序运行后可能会异常退出。
    • 全局变量:如果没有显式初始化,它们会被自动、默认初始化为零或空值,具体取决于数据类型。
数据类型 默认初始化值
int 0
char '\0'或0
float 0.0f
double 0.0
指针 NULL
  • 内存中的位置
    • 局部变量:保存在中,函数被调用时才动态地为变量分配存储单元。
    • 全局变量:保存在内存的全局存储区中,占用静态的存储单元。
全局变量使用建议:不在必要时不要使用全局变量。
  1. 占用内存时间长:全局变量在程序的全部执行过程中都占用存储单元,而不是仅在需要时才开辟单元。
  2. 降低了函数、程序的可靠性和通用性:函数中引用全局变量会降低可靠性与通用性,使其执行受外部影响,移植时还需连带全局变量,易引发命名冲突。应通过参数传递使函数相对封闭,以提高程序的可移植性和可读性。
  3. 程序容易出错:全局变量过多时,各函数均可随时修改其值,导致难以追踪变量状态,易引发错误,因此应限制使用。

5.2 按存储方式不同分类

        C语言变量有数据类型和存储类别两个属性。存储类别决定存储方式(静态动态),也决定了生命周期:静态存储的变量在程序运行期间全程存在,动态存储的变量仅在所在函数调用时临时分配,调用结束后即释放。

5.2.1 动态(自动)存储方式

        动态存储方式:在程序运行期间根据需要进行动态的分配存储空间的方式,数据存放在动态存储区。

  • 在动态存储区中存放以下数据:
    • 函数形参:在调用函数时给形参分配存储空间。
    • 函数中定义的局部变量且没有用关键字static声明的变量,即自动变量。
    • 函数调用时的返回地址等。

        在调用该函数时,系统会给这些变量分配存储空间,在函数调用结束时就自动释放这些存储空间。因此这类局部变量称为自动变量。自动变量用关键字auto 作存储类别的声明。

       auto 可省略,不写则隐含指定为自动变量,属于动态存储方式。大多数变量为此类,其生命周期与所在函数的执行周期一致。

auto int b = 3; //等价于int b = 3;

        普通局部变量在每次函数调用时重新初始化,不保留上次运行的值,且分配的存储空间地址可能不同。

5.2.2 静态存储方式

        静态存储方式:在程序运行期间数据存放在静态存储区。它们在程序整个运行期间都不释放,故生命周期存在于程序的整个运行过程。

        局部变量,使用static修饰以后,则使用静态存储方式。

  • 若希望函数调用结束后局部变量的值不消失、存储单元不释放,以便下次调用时保留原值,则应用 static 将其声明为静态局部变量。
  • 静态局部变量未赋初值时自动初始化为0,自动变量未赋初值时值为不确定。
  • 静态局部变量在编译时赋初值一次,程序运行后不再重复赋值;自动变量则在每次函数调用时赋初值。
  • 静态局部变量虽在函数调用结束后依然存在,但因其局部作用域,其他函数无法引用。

        全局变量存放在静态存储区(不包括extern修饰和malloc函数分配的方式),程序开始执行时分配、结束时释放,在整个执行过程中占据固定存储单元,不动态分配。

  • 普通全局变量在整个工程中可见,其他文件可通过 extern 声明后使用,且不可重复定义同名变量。
  • 静态全局变量仅对当前文件可见,其他文件不可访问,但可定义同名变量互不影响。将无需共享的全局变量用 static 修饰,可降低模块耦合,避免命名冲突。

        使用静态存储的弊端及建议:

  • 静态存储长期占用内存不释放,而动态存储可让同一单元为多个变量复用,更节约内存。
  • 静态局部变量因保留上次调用值,调用次数多时难以判断当前值,降低可读性。

        若非必要,不要频繁使用静态局部变量。

5.3 其它变量修饰符

5.3.1 寄存器变量(register变量)

        register关键字提示编译器将局部变量存入寄存器以提高访问效率,适用于频繁访问的变量。但该关键字仅为建议,是否实现由编译器决定。随着编译器优化能力增强,能自动将频繁使用的变量放入寄存器,如今用 register 声明的必要性已不大,且寄存器变量无法取地址。

5.3.2 extern修饰变量

        在一个文件内扩展全局变量的作用域

        外部变量作用域从定义处开始至文件结束,定义前的函数无法引用。若需提前引用,可用 extern 声明将作用域扩展至此,之后便可合法使用。

举例:调用函数,求3个整数的最大值。

#include <stdio.h>

int max(); //函数原型
extern int a, b, c; //把外部变量a,b,c的作用域扩展到从此处开始

int main() {
    printf("输入3个整数:");
    scanf("%d %d %d", &a, &b, &c); //输入3个整数给a,b,c
    printf("max is %d\n", max());

    return 0;
}

int a, b, c; //定义外部变量a,b,c

int max() {
    int m;
    m = (a > b) ? a : b; //把a和b中的大者放在m中
    m = (m > c) ? m : c; //将a,b,c三者中的大者放在m中

    return m; //返回m的值
}

注意:

  • 建议将外部变量定义放在所有引用它的函数之前,以避免使用 extern 声明
  • 用 extern 声明外部变量时,类型名可省略,因为这是声明而非定义,只需写出变量名即可。
        将全局变量的作用域扩展到其它文件

        多文件共享同一外部变量时,不能重复定义。正确做法是:在一个文件中定义,另一文件中用 extern 声明,避免连接时出现“重复定义”错误。编译和连接时,系统通过 extern 识别变量具有外部链接,可从其他文件找到定义,并将作用域扩展至本文件,从而合法引用。但用 extern 扩展全局变量作用域需慎重,因一个文件中的修改会影响另一文件,进而干扰函数执行结果。

        系统在编译过程中遇到extern时,

  • 先在本文件中找外部变量的定义,如果找到,就在本文件中扩展作用域;
  • 如果找不到,就在连接时从其它文件中找外部变量的定义。如果从其它文件中找到了,就将作用域扩展到本文件;
  • 如果再找不到,就按出错处理。

5.3.3 static修饰全局变量

        将全局变量的作用域限制在本文件中,用 static 修饰的全局变量称为静态全局变量,仅限本文件使用。在多模块开发中,各文件可独立定义同名静态全局变量,互不干扰。

        为外部变量加上 static 成为静态外部变量,可限制其作用域于本文件,既方便模块化开发,也防止被其他文件误用。

5.3.4 const修饰变量

  • 常量变量声明:
void func1() {
    const int myConstant = 42;
    myConstant = 23; //报错,因为myConstant是常量
}

        const 声明的整数常量在程序执行期间不可修改,任何尝试修改的操作都会引发编译错误。

  • 指向常量的指针:
void func2() {
    int num1 = 10;
    const int *ptr;
    ptr = &num1;
    *ptr = 20; //报错

}

        const int *ptr 表示指针指向的内容不可修改,但指针本身可以指向其他地址。

  • 常量指针:
void func(int* const p) {
    int x = 13;
    p = &x; // 报错
}

        int* const p表示指针本身是常量,指向的地址不可改变,但该地址中的内容可以修改

        如果想同时限制修改 ptr 和 *ptr ,需要使用两个 const 。

void func3() {
    int num1 = 10;
    const int *const ptr = &num1;

    int num2 = 20;
    ptr = &num2; //报错

    *ptr = 20; //报错
}
  • 常量参数:
void func4(const int param) {
    param = 20; //报错
}

        这个函数声明中,param 是一个常量参数,参数在函数内部不可修改。

  • 常量数组:
void func5() {
    const int arr[] = {1, 2, 3, 4};
    arr[0] = 10; //报错
    arr[1] = 20; //报错
}

        用 const 修饰的数组,其每个元素均为常量,不可修改。

  • 常量结构体:
struct Point {
    const int x;
    const int y;
};

        用 const 修饰的结构体变量,其所有成员均为常量,不可修改。

总结:

        const 关键字有助于编程中的可读性和代码的安全性,编译器能够在编译时捕获对常量的非法修改。在编写代码时,用 const 表明该值不可修改的意图。

小结:变量类型及存储位置

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐