C语言笔记(三)

二十、指针

20.1、内存是如何存放变量的

通过变量名对变量进行访问和存储是为了方便程序员而设计的,其实在内存中完全没有存储变量名的必要。

因为编译器知道具体每一个变量名对应的存放地址,所以当你读取某个变量的时候,编译器就会找到变量名所在的地址,并根据变量的类型读取相应范围的数据。
在这里插入图片描述

20.2、指针和指针变量

通常我们所说的指针,就是地址的意思。C 语言中有专门的指针变量用于存放指针,跟普通变量不同,指针变量存储的是一个地址。
指针变量也有类型,它的类型就是存放的地址指向的数据类型。
在这里插入图片描述
看图,上边我们又定义了两个指针变量:pa 和 pb,因为它们是指针变量,所以它们在内存中存放的是地址。这里我们分别存放了变量 a 和 f 的地址。

在我们的编译系统中,指针变量是占 4 个字节的空间,也就是说一个地址是占 4 个字节的空间。

20.3、定义指针变量

定义指针变量跟普通变量十分相似,只是中间多了一个星号(*)。

char *pa;
int *pb;

左侧的数据类型表示指针变量中存放的地址指向的内存单元的数据类型。

比如刚才的图中,指针变量 pa 中存放字符变量 a 的地址,所以 pa 应该定义为字符型指针;而指针变量 pb 中存放的是整型变量 f 的地址,所以 pb 就定义为整型指针。

这点一定要注意,因为不同数据类型所占的内存空间不同,如果指定错误了,那么在访问指针变量指向的数据时就会出错。

20.4、取地址运算符和取值运算符

如果需要获取某个变量的地址,可以使用取地址运算符(&):

char *pa = &a;
int *pb = &f;

如果需要访问指针变量指向的数据,可以使用取值运算符(*):

printf("%c, %d\n", *pa, *pb);

这里要注意的是取值运算符跟定义指针用的都是星号(*),这属于符号的重用,在不同的地方有不同的意义:在定义时表示定义一个指针变量;在其他位置表示获取指针变量指向的变量的值。

直接通过变量名来访问变量的值,我们称之为直接访问;通过指针变量这样的形式来访问变量的值,我们称之为间接访问,所以取值运算符有时候也叫间接运算符。

20.5、避免访问未初始化的指针

#include <stdio.h>

int main()
{
    int *a;

    *a = 123;

    return 0;
}

类似于上边这样的代码是很危险的,因为指针变量 a 到底指向哪里,我们没办法知道。这个道理就跟访问未初始化的变量一样,它的值是随机的。

这在指针变量里会很危险,因为后边代码对一个未知地址进行赋值,那么你可能会覆盖到系统的一些关键代码。不过你也别高兴得太早,因为系统通常都不会允许你这么干,程序这时候会被终止并报错。

更危险的是,偶尔这个指针变量里随机存放的是一个合法的地址,那么接下来的赋值就会导致那个位置的值莫名其妙地被修改。这种类型的 Bug 是非常难以排查的。

所以,在对指针进行间接访问时,必须确保它们已经被正确地初始化。

二十一、指针和数组

21.1、数组不是指针

数组名是数组第一个元素的地址,也是数组的首地址。

21.2、指向数组的指针

int a[] = {1, 2, 3, 4, 5};
int *p;
p = a; // 语句1
p = &a[0]; // 语句2

因为数组名即数组第一个元素的地址,所以语句 1 和语句 2 是等价的,都是将数组 a 的首地址存放到指针变量 p 中。

21.3、指针的运算

当指针指向数组元素的时候,我们可以对指针变量进行加减运算,这样做的意义相当于指向距离指针所在位置向前或向后第 n 个元素。

比如 p+1 表示指向 p 指针指向的元素的下一个元素;p-1 则表示指向上一个元素。

需要郑重强调的是:p+1 并不是简单地将地址加 1,而是指向数组的下一个元素。

21.4、数组和指针相似点

在这里插入图片描述
在这里插入图片描述

二十二、指针数组和数组指针

22.1、什么是左值(lvalue)和右值(rvalue)?

  • lvalue 是用于识别或定位存储位置的标识符
#include <stdio.h>

int main()
{
        int a = 5;

        ++(a++);

        return 0;
}

会得到如下错误
在这里插入图片描述

  • (a++) 是先将变量 a 的值(5)做为整个表达式的值返回,再将 a 自增 1(类似于 a = a + 1)。
  • 所以这里 ++(a++); 相当于 ++(5), a = a + 1;
  • 那当然要报错啦,5 是一个常量,当然不能给你 5 = 5 + 1

C 语言的术语 lvalue 指用于识别或定位一个存储位置的标识符。(注意:左值同时还必须是可改变的
其实 rvalue 的发明完全是为了搭配 lvalue,rvalue 你可以理解为 readable value,即任何可读取的值都被认为是右值(非左值)。

22.2、指针和数组的区别

  • 指针是左值()
  • 数组名只是一个地址常量,它不可以被修改,所以数组名不是左值。

22.3、指针数组

int *p1[5];

上边是一个指针数组,我们可以从运算符的优先级和结合性进行分析:
数组下标的优先级要比取值运算符的优先级高,所以先入为主,p1 被定义为具有 5 个元素的数组。那么数组元素的类型呢?是整型吗?显然不是,因为还有一个星号,所以它们应该是指向整型变量的指针。
在这里插入图片描述
结论:指针数组是一个数组,每个数组元素存放一个指针变量。

#include <stdio.h>

int main()
{
        char *p1[5] = {
                "让编程改变世界 -- 鱼C工作室",
                "Just do it -- NIKE",
                "一切皆有可能 -- 李宁",
                "永不止步 -- 安踏",
                "One more thing... -- 苹果"
        };
        int i;

        for (i = 0; i < 5; i++)
        {
                printf("%s\n", p1[i]); //不能加*,加*表示取字符
        }

        return 0;
}

22.4、数组指针

int (*p2)[5];

上边是一个数组指针,我们同样可以从运算符的优先级和结合性进行分析:
因为圆括号和数组下标位于同一个优先级队列,所以我们就要看先来后到的问题了。由于它们的结合性都是从左到右,所以 p2 先被定义为一个指针变量。那么它指向谁?还能有谁?后边还紧跟着一个具有 5 个元素的数组,p2 指向的就是它。由于指针变量的类型事实上就是它所指向的元素的类型,所以这个 int 就是定义数组元素的类型为整型。
在这里插入图片描述
结论:数组指针是一个指针,它指向的是一个数组。

#include <stdio.h>

int main()
{
        int temp[5] = {1, 2, 3, 4, 5};
        int (*p2)[5] = &temp;
        int i;

        for (i = 0; i < 5; i++)
        {
                printf("%d\n", *(*p2 + i));
        }

        return 0;
}

[fishc@localhost s1e23]$ gcc test4.c && ./a.out
1
2
3
4
5

二十三、指针和二维数组

23.1、C语言没有真正意义上的二维数组

在C语言中,二维数组的实现,只是简单地通过“线性扩展”的方式进行。

如图所示,int b[4][5]; 就是定义 4 个元素,每个元素都是一个包含 5 个整型变量的一维数组。它在内存中依然是以线性的形式存储。
在这里插入图片描述

23.2、二维数组三个问题

假设我们定义了二维数组 int array[4][5]:
在这里插入图片描述

  • array 表示的是什么?
    指向包含5个元素的数组的指针
    在这里插入图片描述
  • *(array + 1) 表示的是什么?
    在这里插入图片描述
    在这里插入图片描述在这里插入图片描述
  • *( *(array+1)+3)表示的是什么?
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    结论
    在这里插入图片描述

23.3、 数组指针和二维数组

要用指针来指向二维数组,需要使用数组指针的形式。

#include <stdio.h>

int main()
{
        int array[2][3] = {{0, 1, 2}, {3, 4, 5}};
        int (*p)[3] = array;

        printf("**(p+1): %d\n", **(p+1));
        printf("**(array+1): %d\n", **(array+1));
        printf("array[1][0]: %d\n", array[1][0]);
        printf("*(*(p+1)+2): %d\n", *(*(p+1)+2));
        printf("*(*(array+1)+2): %d\n", *(*(array+1)+2));
        printf("array[1][2]: %d\n", array[1][2]);

        return 0;
}

在这里插入图片描述

二十四、void指针和NULL指针

24.1、void 类型

void 即的字面意思是“无类型”,定义变量的时候,我们通过类型来决定该变量所占的内存空间。

比如在我们的系统中,char 类型占 1 个字节,int 类型占 4 个字节,double 类型占 8 个字节等。

那 void 既然是无类型,我们就不应该用它来定义一个变量,如果你一定要这么做,那么程序就会给你报错!

24.2、void 指针

void 指针我们把它称之为通用指针,就是可以指向任意类型的数据。也就是说,任何类型的指针都可以赋值给 void 指针。
提示:

  • 不要直接对 void 指针进行解引用,因为编译器不知道它所指向的数据类型
  • 使用 void 指针一定要小心,由于 void 指针可以包罗万象的特性,间接使得不同类型的指针转换变为合法

24.3、NULL 指针

在 C 语言中,如果一个指针不指向任何数据,我们就称之为空指针,用 NULL 表示。
NULL 其实是一个宏定义:

#define NULL ((void *)0)

因为在大部分系统中,地址 0 通常是一个不被使用的地址。所以,如果一个指针指向 NULL,那么就意味着该指针不指向任何东西。
一个良好的编程习惯是:当你还不清楚要将指针初始化为什么地址时,请将它初始化 NULL;在对指针进行解引用时,先检查该指针是否为 NULL。这种策略可以为你今后编写大型程序节省大量的调试时间。

24.4、NULL 不是 NUL

在 ASCII 字符表中看到 NUL 的字符,但它不是 NULL。NUL 是 ASCII 字符的缩写,表示的是字符串结束符 ‘\0’。虽然它的值也是 0,但两者的目的和用途是完全不同的。
NULL 用于指针和对象,表示指向一个不被使用的地址。而 ‘\0’ 我们非常熟悉了,它表示字符串的结尾。所以两者的概念是不同的

二十五、指向指针的指针

25.1、指向指针的指针

只要你懂得了指针的概念,那么指向指针的指针也就没什么了不起了。

#include <stdio.h>

int main()
{
        int num = 520;
        int *p = &num;
        int **pp = &p;

        ……

        return 0;
}

上边代码中,p 定义的是指向整型的指针,也就是说指针变量 p 里边存放的是整型变量 num 的地址。

所以,*p 就是对指针进行解引用,得到了 num 的值。

而 pp 就是指向指针的指针,pp 存放的是指针变量 p 的地址。

所以,对pp进行一层解引用(*pp)得到的是 p 的值(num 的地址),对 pp 进行两层解引用(**pp)相当于对 p 进行一层解引用(*p),得到整型变量 num 的值。

25.2、指针数组和指向指针的指针

使用指向指针的指针来指向数组指针,至少有两个优势:

  • 避免重复分配内存(虽然我们进行了多个分类,但每本书的书名只占据一个存储位置,没有浪费)
  • 只需要进行一处修改(如果其中一个书名需要修改,我们只需要修改一个地方即可)

这样,代码的灵活性和安全性都有了显著地提高。

25.3、数组指针和二维数组

使用数组指针来指向二维数组

#include <stdio.h>

int main()
{
        int array[3][4] = {
                {0, 1, 2, 3},
                {4, 5, 6, 7},
                {8, 9, 10, 11}};
        int (*p)[4];
        int i, j;

        p = array;
        for (i = 0; i < 3; i++)
        {
                for (j = 0; j < 4; j++)
                {
                        printf("%2d ", *(*(p+i)+j));
                }
                printf("\n");
        }

        return 0;
}

p 是一个指向数组的指针,这个数组包含了 4 个元素,所以 p的跨度是 4 * sizeof(int),然后把二维数组第一个元素的地址赋值给 p。这时,p+1 恰好是二维数组一行的跨度。对 p+1 进行解引用,得到的就是二维数组第二行第一个元素的首地址。所以我们可以通过数组指针的方式来访问二维数组。

二十六、常量和指针

26.1、const 关键字

可以将变量变成具有常量一样的特性。这就是 —— const 关键字。
在它的修饰下,变量就会失去可修改的特性,也就是变成只读的属性。

const int price = 520;
const char a = 'a';
const float pi = 3.14;

26.2、指向常量的指针

万能的指针当然也可以指向被 const 修饰过的变量,这就意味着不能通过指针来修改它所引用的值。
这时候,如果尝试修改指针引用的值,那么我们将被告知程序无法通过编译;但如果只是修改指针的指向,那么编译器并不会阻止你这么做。

#include <stdio.h>

int main()
{
        int num = 520;
        const int cnum = 880;
        const int *pc = &cnum;

        printf("cnum: %d, &cnum: %p\n", cnum, &cnum);
        printf("*pc: %d\n, pc: %p", *pc, pc);

        *pc = 1024;  // 尝试修改 *pc 的值,报错
        printf("*pc: %d\n", *pc);

        return 0;
}

总结:

  • 指针可以修改为指向不同的常量
  • 指针可以修改为指向不同的变量
  • 可以通过解引用来读取指针指向的数据
  • 不可以通过解引用修改指针指向的数据

26.3、指向非常量的常量指针

指向常量的指针,不能改变的是指针指向的值,但指针本身是可以被修改的。如果要让指针也不可变,那么可以使用常量指针。
同样是使用 const 关键字修饰即可,只是位置稍微发生了变化:

#include <stdio.h>

int main()
{
        int num = 520;
        const int cnum = 880;
        int *const p = &num;

        *p = 1024; // 这是可以的
        printf("*p: %d\n", *p);

        p = &cnum; // 这是禁止的
        printf("*p: %d\n", *p);

        return 0;
}

它有如下特性:

  • 指针自身不可以被修改
  • 指针指向的值可以被修改

26.4、指向常量的常量指针

就是在刚才的基础上进一步限制,让常量指针指向的值也是常量:

#include <stdio.h>

int main()
{
        int num = 520;
        const int cnum = 880;
        const int *const p = &cnum;

        *p = 1024;
        printf("*p: %d\n", *p);

        p = &cnum;
        printf("*p: %d\n", *p);

        return 0;
}

这样指针自身不能被改变,它所指向的数据也不能通过对指针进行解引用来修改。其实这种霸气十足的限制在平时很少派上用场啦。并且我们发现——如果初始化时,指向的对象不是 const 修饰的变量,那么我们仍然可以通过变量名直接修改它的值:

#include <stdio.h>

int main()
{
        int num = 520;
        const int cnum = 880;
        const int *const p = &num;

        printf("*p: %d\n", *p);

        num = 1024;
        printf("*p: %d\n", *p);

        /*
        *p = 1024;
        printf("*p: %d\n", *p);

        p = &cnum;
        printf("*p: %d\n", *p);
        */

        return 0;
}

总结:

  • 指针自身不可以被修改
  • 指针指向的值也不可以被修改

26.5、指向“指向常量的常量指针”的指针

#include <stdio.h>

int main()
{
        int num = 520;
        const int cnum = 880;
        const int *const p = &cnum;
        const int *const *pp = &p;

        printf("pp: %p, &p: %p\n", pp, &p);
        printf("*pp: %p, p: %p, &cnum: %p\n", *pp, p, &cnum);
        printf("**pp: %d, *p: %d, cnum: %d\n", **pp, *p, cnum);

        return 0;
}

二十七、函数

27.1、为什么要自己定义函数

因为随着程序规模的变大,都免不了会遇到下面这些问题:

  • main 函数变得相当冗杂,程序可读性差
  • 程序复杂度不断提高,编程变成了头脑风暴
  • 代码前后关联度高,修改代码往往牵一发而动全身
  • 变量的命名都成了问题(因为简单的名字都用完了啊,小明,小红,旺财,阿福,隔壁家老王这些名字都用过了,为了不重复命名,只能小明2号,小红3号这样……)
  • 为了在程序中多次实现某功能,不得不重复多次写相同的代码
  • ……

27.2、标准库函数

C 语言的标准库中还为我们提供了很多实现各种功能的函数,有处理字符串的,有数学计算的,有输出输入的,有进程管理的,有信号、接口处理的……
都在这里了-》标准库函数
有了这些函数,我们就不用去关注内部的实现细节,只需要将注意力放在程序的实现逻辑上即可。

比如要打印字符串到屏幕上,我们只需要知道调用 printf 函数并给它传递要打印的内容即可,至于它内部是怎么做到的,我们不需要理会。

27.3、函数的定义

C 语言要求函数必须“先定义,再调用”,定义函数的格式如下:

类型名  函数名(参数列表)
{
        函数体
}
  • 类型名就是函数的返回值,如果这个函数不准备返回任何数据,那么需要写上 void(void 就是无类型,表示没有返回值)。
  • 函数名就是函数的名字,一般我们根据函数实现的功能来命名,比如 print_C 就是“打印C”的意思,一目了然。
  • 参数列表指定了参数的类型和名字,如果这个函数没有参数,那么这个位置直接写上小括号即可(())。
  • 函数体就是指定函数的具体实现过程,是函数中最重要的部分。

27.4、函数的声明

所谓声明(Declaration),就是告诉编译器我要使用这个函数,你现在没有找到它的定义不要紧,请不要报错,稍后我会把定义补上。

有时候,你可能会发现即使不写函数的声明,程序也是可以正常执行的。但如果你把函数的定义写在调用之后,那么编译器可能就会找不着北了:

#include <stdio.h>

int main(void)
{
        print_C();

        return 0;
}

void print_C(void)
{
        printf(" ###### \n");
        printf("##    ##\n");
        printf("##      \n");
        printf("##      \n");
        printf("##      \n");
        printf("##    ##\n");
        printf(" ###### \n");
}

程序执行后会弹出提醒(一些比较旧的编译器甚至会报错):
在这里插入图片描述
这是因为程序的编译时从上到下执行的,所以从原则上来说,函数必须“先定义,再调用”。向上边例子反其道而行就会出问题。但在实际开发中,经常会在函数定义之前使用它们,这个时候就需要提前声明。

声明函数的格式非常简单,只需要去掉函数定义中的函数体再加上分号(;)即可:

#include <stdio.h>

void print_C(void);

int main(void)
{
        print_C();

        return 0;
}

void print_C(void)
{
        printf(" ###### \n");
        printf("##    ##\n");
        printf("##      \n");
        printf("##      \n");
        printf("##      \n");
        printf("##    ##\n");
        printf(" ###### \n");
}

作为一个良好的编程习惯,建议大家还是无论如何都把函数的声明写上比较合适。

27.5、 函数的参数和返回值

有时候,函数需要接收用户传入的数据,那么就需要使用函数的参数。根据需求,函数的参数数量可以有多个,类型也可以各不相同。

而函数的返回值通常是反馈了函数的计算结果,也可以是函数的执行结果(比如成功或失败)
编写一个函数 sum,由用户输入参数 n,计算 1+2+3+…+(n-1)+n 的结果并返回。

#include <stdio.h>

int sum(int n);

int sum(int n)
{
        int result = 0;

        do
        {
                result += n;
        } while(n-- > 0);

        return result;
}

int main(void)
{
        int n;

        printf("请输入n的值:");
        scanf("%d", &n);

        printf("1+2+3+...+(n-1)+n的结果是:%d\n", sum(n));

        return 0;
}

在这里插入图片描述

27.6、如果函数不需要参数,建议定义时在函数名后边的小括号中写上 void,明确表示该函数无参数。

二十八、参数和指针

28.1、参数和返回值

类型名  函数名(参数列表)
{
        函数体
}

函数在定义的时候通过参数列表来指定参数的数量和类型,参数使得函数变得更加的灵活,传入不同的参数可以让函数实现更为丰富的功能。如果你的函数确实不需要参数,建议使用 void 进行强调。

函数的类型名事实上就是指定函数的返回值。你一个函数,实现了一个功能,经常是要反馈结果的,比如我传给你两个数字 1 和 2,你将它们进行复杂的计算之后把结果 3 返回给我。当然,在现实开发中也并不是所有的函数都有计算结果可以返回,比如你调用一个函数用于在窗口中绘制一个矩形,那么它就没有什么所谓的计算结果需要返回了,所以通常这些函数会通过返回值来说明该函数是否调用成功。最后,如果你的函数确实不需要返回值,那么就用 void 表示不返回。

28.2、形参和实参

形参就是形式参数,函数定义的时候写的参数就叫形参,因为那时候它只是作为一个占位符而已。而实参就是你在真正调用这个函数的时候,传进去的数值。

形参和实参的功能说白了就是用作数据传送。当发生函数调用时,实参的值会传送给形参,并且这种传输具有单向性(也就是不能把形参的值回传给实参)。另外,形参变量只有在函数被调用时才会分配内存,调用结束后,立刻释放内存,所以形参变量只有在函数内部有效,不能在函数外部使用。

28.3、传值和传址

指针也是一个变量,所以它可以通过参数传递给函数。

无论是传值还是传址,都只是将实参的值拷贝给形参。

28.4、传数组

其实并不存在将整个数组作为参数传递的方式,你虽然这么写了,但对方接收到的只是一个地址(相当于传递数组的第一个元素的地址)而已。

28.5、可变参数

实现可变参数,需要包含一个头文件叫:<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);

        return 0;
}

在这里插入图片描述
事实上 va_start、va_arg 还有 va_end 都是宏定义,它们的背后仍然是通过指针来实现的。这个后边在讲解别名的和宏定义的时候再深入剖析。现在知道怎么定义一个支持可变参数的函数即可。

二十九、指针函数和函数指针

29.1、指针函数

我们说函数的类型,事实上指的就是函数的返回值。根据需求,一个函数可以返回字符型、整型和浮点型这些类型的数据,当然,它还可以返回指针类型的数据。定义的时候只需要跟定义指针变量一样,在类型后边加一个星号即可。

所以,用指针变量作为函数的返回值就是指针函数。

29.2、不要返回局部变量的指针

为啥不能返回局部变量的地址呢?大家还记得前几节课我们说过的,函数内部定义的变量我们称之为局部变量,局部变量的作用域(就是它的有效范围)仅限于函数内部,出了函数它就什么都不是了。关于这一点我们在随后的“作用域和生存期”章节会有详细的介绍。

大家一定要注意,跟我一起读:不要返回局部变量的指针。

29.3、函数指针

相信经过数组那一个章节的洗礼,大家应该都掌握了这其中的命名套路了。指针函数,它是个函数;函数指针,就应该是个指针。没错,从名字我们不难发现真相:它就是一个指向函数的指针。

指针函数 -> int *p();

函数指针 -> int (*p)();

注:本质上,函数表示法就是指针表示法,因为函数的名字经过求值会变成函数的地址。所以在定义了函数指针后,给它传递一个已经被定义的函数名,即可通过该指针进行调用。

29.4、函数指针作为参数

函数指针也可以作为参数进行传递

#include <stdio.h>

int add(int, int);
int sub(int, int);
int calc(int (*fp)(int, 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);
}

int main()
{
        printf("3 + 5 = %d\n", calc(add, 3, 5));
        printf("3 - 5 = %d\n", calc(sub, 3, 5));

        return 0;
}

在这里插入图片描述

29.5、函数指针作为返回值

举个例子:让用户输入一个表达式,然后程序根据用户输入的运算符来决定调用 add 还是 sub 函数进行运算。

#include <stdio.h>

int add(int, int);
int sub(int, int);
int calc(int (*fp)(int, int), int, int);
int (*select(char op))(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);
}

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+2):");
        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;
}

在这里插入图片描述

三十、局部变量和全局变量

30.1、局部变量

在开始讲解函数之前,我们所理解的变量就是在内存中开辟一个存储数据的位置,并给它起个名字。因为那时候只有一个 main 函数,所以那时我们对它的作用范围一无所知,觉得定义了变量就随时随地可以使用它……直到我们学习函数才发现,不同函数之间定义的变量,它们是无法相互进行访问的。

30.2、 C99 新标准

C99 标准允许在 for 语句的第一个表达式部分声明变量,它的作用范围仅限于复合语句的内部。
在这里插入图片描述
for 语句因为定义了同名的i变量,所以它屏蔽了第一个定义的i变量。换句话说,在 for 语句的循环体中,无法直接访问到外边的i变量。

30.3、C 语言允许在程序的任意位置声明变量

允许变量在需要时才声明是一件非常棒的事情,因为如果当你的函数体非常庞大的时候,你总不会愿意往前翻好几个屏幕去看某个变量 i、j、k 是什么意思吧。

有些朋友可能会有所担心:“要是到处定义变量,那一不小心重复定义了怎么办?”,其实这是编译器应该关心的问题(编译器会找出重复定义的变量并报错),你放心使用就可以了。

30.4、全局变量

在函数里边定义的,我们叫局部变量;在函数外边定义的,我们叫外部变量,也叫全局变量。有时候,我们可能需要在多个函数中使用共同的一个变量,那么就会用到全局变量。因为全局变量可以被本程序中其他函数所共用的。

30.5、全局变量初始化

与局部变量不同,如果不对全局变量进行初始化,那么它会自动初始化为 0。

30.6、当局部变量和全局变量同名时

如果在函数的内部存在一个与全局变量同名的局部变量,编译器并不会报错,而是在函数中屏蔽全局变量(也就是说在这个函数中,全局变量不起作用)。

30.7、如果一个全局变量在使用之后才被定义

那么你可以在对该全局变量进行访问之前,使用 extern 关键字对该变量名进行修饰。这样就相当于告诉编译器:这个变量我在后边定义了,你先别急着报错。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值