献给初学者的C语言复习自查

本文详细解释了C/C++中的声明与定义区别,涉及变量的存储类别(自动、静态、外部和寄存器),以及全局变量、局部变量、静态局部变量和静态全局变量的概念。重点讲解了函数、函数指针和回调函数的使用。

       

         如果你是初学者,如果你对我已写下的板块中的这些知识存在某些困惑,那么相信我,你的所有困惑都将能在我的这篇文章中得到解答。带着你的疑惑放心的看下去吧,我的朋友。

—————————————————————————————————————————————————————

基础知识

声明和定义的区别

声明包含两种:①定义性声明(defining declaration)     ————“定义”

                         ②引用性声明(referencing declaration)————“狭义上的声明”

定义性声明如int a,在声明a的同时还要要给a分配空间。引用性声明如extern int a,只是扩展了别处定义的变量a的作用域,并不需要给a额外分配空间。

一般我们将不需要分配空间的声明称之为“声明”,这是狭义上的声明。将需要分配空间的的声明称之为“定义”,所以定义是声明的一种特例。

变量

1.如果没有扩展某个变量的作用域,那么这个变量只能在其被定义后才能使用,一直到其作用域结束。这与函数同理,若某函数的定义在main函数之后,那么想要在main函数中调用该函数,必须要在main函数前进行声明

2.在代码编译与运行时,内存会划分为:

        (1)程序区

        (2)静态存储区(相较动态存储区,静态存储区空间较少)

        (3)动态存储区

从变量在内存中的存储位置来看,变量分为:

        (1)动态存储变量

        (2)静态存储变量

其中动态存储区的变量会在函数调用结束后清除内存,而静态储存区的变量在分配空间以后,会在程序的整个运行过程都占据着固定内存,直到程序运行结束以后才会被释放内存。

3.存储类别的定义:变量与函数的可访问性(即作用域范围)与生存期。

存储类别来看,可分为以下四种类型:

                ①auto(自动的)

                ②static(静态的)

                ③extern(外部的)

                ④register(寄存器的)

        (1)有的人在之前学习时会存在误解,认为只有使用static修饰的变量才储存在静态存储区,但这是错误的。比如全局变量,没有用static修饰,但也是储存在静态存储区,用extern扩展的变量也是全局变量,也储存在静态存储区。而register修饰的变量虽然储存在CPU的内部寄存器中而非内存里,但与auto修饰的变量有相同的作用域和生存期,所以是动态存储的。

        (2)注意:是动态存储与在动态存储区这两者不是等价的。动态存储时指动态分配内存,调用结束后就将内存回收,而动态存储区是程序运行时内存的一块分区,在这个分区内的变量都是进行动态存储的。(静态存储同理)

局部变量和全局变量

        复合语句和形参都存在于函数中,所以局部变量也可以说成是在函数内定义的变量。只不过复合语句内的局部变量的作用域与生存期仅限于该复合语句。

1.从使用角度来说:局部变量就是仅能在其被定义的函数内使用的变量,全局变量是在函数外定义的且能被其他函数所共用的变量。由于局部变量与全局变量是在两个不同区域定义的,当局部变量与全局变量重名时,在局部变量的作用域内,局部变量的值会覆盖全局变量的值。也就是局部变量的优先级高于全局变量。但尽量确保不会有重名,否则程序可能出现很多问题。

2.一般而言,非必要则不使用全局变量,因为:

        (1)需要考虑函数内的局部变量与全局变量是否重名

        (2)同一个源文件中全局变量为所有函数所共用,所以在任意函数中修改了全局变量的值,会导致其他函数中对应全局变量的值也发生改变。

这些都会使函数的通用性降低。

3.全局变量(外部变量)有两类:

                ①在函数外定义的变量(存储在静态存储区,默认初值是0,记住它不是auto存储类!)

                ②在函数外用extern扩展的外部变量(是外部源文件中的全局变量)

        (1)全局变量的存储类别是static类别,但默认为外部链接属性,用static进行修饰则可以修改为内部链接属性。

4.局部变量分为:

                ①在函数内定义的变量

                ②静态局部变量(用static修饰的局部变量)

        (1)用static修饰的局部变量虽然不是全局变量,但只要所在函数被调用过,就与全局变量一样在程序运行结束以后才被释放空间。

        (2)静态局部变量的初始化表达式必须是一个常量或者常量表达式。

        (3)函数体内如果在定义静态变量的同时进行了初始化,则以后程序不再进行初始化操作(出现在函数内部的静态变量初始化语句只有在第一次调用才执行)。而自动变量(即用auto修饰的变量)赋初值则是每次调用函数都重新给一次初值,即执行一次赋值语句。

auto关键字

(1)所有局部变量(即函数内定义的变量)的默认存储类都是auto存储类,且auto只能修饰局部变量(即只能在函数内使用auto),由此可知我们基本不会用到auto这个关键字(能修饰的对象都是默认被这个关键字修饰了)。

(2)在函数内定义的自动变量,函数调用结束后就会被清除内存,而在main函数中定义的不声明存储类别的变量虽然也是动态存储的,但main函数是程序运行的入口,是最先进栈的函数,所以直到程序运行结束才会被释放。

static关键字

用static修饰的变量都会存储在静态存储区,除此之外还会发生什么?:

        (1)修饰局部变量:静态局部变量(初始化只会进行一次,在函数调用结束后仍在内存中占据固定空间,且保留调用结束后当前的值。只能在定义它的函数内使用)

        (2)修饰全局变量:静态全局变量(使得该全局变量仅能在当前源文件中使用)

        (3)修饰函数:静态函数,其作用有:

                ①仅限当前源文件使用(从外部链接属性变为了内部链接属性)

                ②外部有同名函数时不会发生冲突(当进行模块化编程时)

        1.具体解释:static修饰全局变量的作用:如果不想某个全局变量被其他源文件所引用,则用static修饰。用static来修饰函数也是同样的意思。因为全局变量和函数都有外部链接属性,只要被定义,就能通过extern的方式被其他源文件所调用。但如果用static进行修饰,则会将它们的外部链接属性变为内部连接属性,也即仅在自身源文件中使用。

        2.一定要记住用static修饰的则会存储在静态存储区,这一部分的内存是在程序运行结束以后才会被释放。这相当于扩展了局部变量和函数的生存期。同时还会将全局变量与函数的外部链接属性改变为内部链接属性。
       3. 能被extern扩展的对象有:全局变量、函数。其原因是这两者有外部链接属性,但当这两者被static修饰后,其外部链接属性就变为了内部链接属性,也即被static修饰的全局变量与函数无法被extern扩展。

         4.与动态存储变的量定义后,若未进行初始化,则初始值不确定这一点不同的是,由static定义的变量(即静态存储区)若未进行初始化,则其默认值是各个数据类型意义中的0.如int就是“0”,char就是“\0”。

extern关键字

extern:其作用是扩展某个变量的作用域,这个变量可以是自身源文件中定义的全局变量,也可以是与自身源文件在同一个文件目录下的其他源文件中定义过的全局变量。

再次强调:全局变量是存储在静态存储区,auto只会默认修饰且只能修饰局部变量。

register关键字

1.修饰对象:局部变量。

2.这个关键字命令编译器将变量存在CPU内部寄存器中而不是通过内存寻址访问以提高效率。这种存储只适用于需要大量、快速访问的变量,如计数器。但是寄存器数量有限,不容易申请到空间,当空间申请失败时会同auto,存储在动态存储区。且变量的最大尺寸取决于寄存器的大小。

        实际上现代计算机运行速度都十分之快,且好的编译系统能够自动识别需要存放在寄存器中的变量,所以一般也不会去使用register关键字。

3.编译器可以选择忽视register关键字,因为使用变量时,在寄存器中并不一定比在内存中更快。

4.register存储类被的变量与auto存储类别的变量具有相同的作用域和生命周期。

函数

在对函数进行声明时,参数列表可以只写数据类型,不需要写形参名。

函数指针与指针函数

1.函数指针:本质是指针,只是是指向函数的指针。每个函数都有其入口地址,函数指针就是指向函数入口地址的指针(函数的入口地址就是函数名)。所以直接:“函数指针变量 = 函数名”即可以将指针指向函数。

2.指针函数:本质是函数,只不过返回值类型是一个指针。

举例:

(1)       ① 指针函数: int* f (int x,int y);          

        这是一个名叫f的函数,其返回值类型是int类型的指针。

(2)   ②函数指针:int (*f)(int x,int y)           

        括号的优先级更高,这种形式的定义就是说定义了一个指针f,f可以指向一切参数列表为(int x,int y),返回值类型为int类型的函数。这就相当于将指针当做变量,函数当做定义域,但凡是满足参数列表为(int x,int y),返回值类型为int类型的函数都在定义域内。只要让指针f指向函数,那么调用该函数只需在指针f后面写上对应参数即可调用。

         比如有两个int类型的变量a、b,函数add的定义是int add(int a,int b),想要调用add来处理a、b只需要f = add(指针f指向函数add),f(a,b)即可;

其实最简单的判别方法就是看*修饰的是返回值类型还是标识符。

函数指针的使用示例:

int add(int a,int b);
{
    return a + b;
}

//函数指针的定义:
int (*f)(int a,int b);   //①
//int (*f)(int ,int );     ②   
//int (*f) ();             ③这三种都是正确的定义方式,但只建议使用①和②


int main()
{
    f = add;                //让指针f指向函数add


//函数指针的调用
    int a = f(1,2);
  //int a = (*add)(1,2);
  //int a = (*f)(1,2);
//注释的两种写法也没问题,但建议使用第一种方式

    printf("a:%d",a);
    return 0;
}

        
      
     //int ret = p(10, 15);       
     //int ret = (*max)(10,15);
     //int ret = (*p)(10,15);
     

关于为什么要使用函数指针,笔者偷个懒,直接引用:
        “那么,有不少人就觉得,本来很简单的函数调用,搞那么复杂干什么?其实在这样比较简单的代码实现中不容易看出来,当项目比较大,代码变得复杂了以后,函数指针就体现出了其优越性。
举个例子,如果我们要实现数组的排序,我们知道,常用的数组排序方法有很多种,比如快排,插入排序,冒泡排序,选择排序等,如果不管内部实现,你会发现,除了函数名不一样之外,返回值,包括函数入参都是相同的,这时候如果要调用不同的排序方法,就可以使用指针函数来实现,我们只需要修改函数指针初始化的地方,而不需要去修改每个调用的地方(特别是当调用特别频繁的时候)。”

        函数指针另一个典型的应用就是回调函数,这个对于初学者而言其实并不着急去了解,有兴趣的可以点开下方链接,那篇文章中有简单的提及。
————————————————
PS:其实写这部分内容很多地方都借鉴了下面这篇文章。
原文链接:https://blog.csdn.net/u010280075/article/details/88914424

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值