一文带你深入浅出C变量与存储类别

目录

前言

🍉1. 变量详解

🍉1.1 程序与内存的关系

🍉1.2 变量定义与声明

🍉1.2.1 什么是变量

🍉1.2.2 如何定义变量

🍉1.2.3 变量命名规则

🍉1.2.4 为什么要定义变量

🍉1.2.5 变量定义的本质

🍉1.2.6 变量声明的本质

🍉2. 存储类别

🍉2.1 前置铺垫

🍉2.2 作用域

🍉2.2.1 块作用域

🍉2.2.2 函数作用域

🍉2.2.3 函数原型作用域

🍉2.2.4 文件作用域

🍉2.3 链接

🍉2.4 存储期

🍉2.4.1 静态存储期

🍉2.4.2 线程存储期

🍉2.4.3自动存储期

🍉3. 存储类别

🍉3.1 自动变量

🍉3.1.1 自动变量的初始化

🍉3.1.2 小插曲——auto关键字

🍉3.1.3 自动变量的特点

🍉3.1.4 关于块嵌套问题

🍉3.2 寄存器变量

🍉3.2.1 硬件简单介绍

🍉3.2.2 变量声明与特点

🍉3.3 静态变量

🍉3.3.1 块作用域的静态变量

🍉3.3.2 外部链接的静态变量

🍉3.3.3 内部链接的静态变量

🍉3.3.4 存储类别小结

🍉3.3.5函数的存储类别

🍉3.4 多文件

🍉3.4.1 extern 在多文件下的理解与使用

🍉4. 类型限定符

🍉4.1 const限定符

🍉4.1.1 const修饰变量的意义

🍉4.1.2 const修饰的注意事项

🍉4.1.3 const修饰指针

🍉4.1.4 const与非const修饰的类型赋值问题

🍉4.1.5 const修饰函数参数

🍉4.1.6 const修饰函数返回值

🍉4.2 volatile限定符

🍉敬请期待更好的作品吧~


前言

        本文主要分享一波C语言中变量的深入理解以及变量和函数的存储类别,较为深入地探讨存储类别的二三事,由于笔者水平有限,难免会有纰漏,读者各取所需即可。

 

给你点赞,加油加油!

 

        阅读本文前可以去打个基础,这篇博文的常量与变量的讲解:一文带你深入浅出C语言数据http://t.csdn.cn/SzQ4t

        以及这篇博文的左值、右值与数据对象的讲解:

一文带你深入浅出C语言运算符、表达式和语句http://t.csdn.cn/oVHK2

🍉1. 变量详解

🍉1.1 程序与内存的关系

        运行程序的方式,当然可以用vs直接启动

        当然,也可以在vs项目中,找到代码生成的二进制可执行程序,双击即可。

        所以,我们的角色是写代码,编译器的角色是把文本代码变成二进制可执行程序。

        双击不就是windows下启动程序的做法吗?

        那么启动程序的本质是什么呢?

        将程序数据,加载到内存中,让计算机运行!

        那么程序未被加载时在哪里呢?

        程序没有被加载时存放在硬盘中。

        那么为什么要加载到内存中呢?

        为了更快,CPU访问内存要比硬盘快。

🍉1.2 变量定义与声明

🍉1.2.1 什么是变量

        在内存中开辟特定大小的空间,用来保存数据。

        所有的变量,本质都是要在内存某个位置开辟的。

        变量是在程序运行的时候才会被开辟的,而程序运行的时候要被加载到内存中,所以变量必须得在内存中开辟。

🍉1.2.2 如何定义变量

int x = 10;

char c = 'a';

double d = 3.14;

        类型 变量名 = 初始值;

        或者不赋初始值。

🍉1.2.3 变量命名规则

        见名知义。

        大小驼峰命名法。

        全局变量命名在前面加个g,比如g_val。

        不能仅靠大小写区分变量。

        不要和函数名出现同名。

        所有的宏定义、枚举常量、只读变量(const修饰)全用大写字母命名,用下划线分割单词。

        比如:

        const int MAX_LENGTH = 100;

        #define FILE_PATH "/user/tmp"

        循环变量本身不具有特定意义,一般通用i,j,k。

        定义局部变量一定要初始化,不然有可能是随机值。

🍉1.2.4 为什么要定义变量

        计算机是为了解决人计算能力不足的问题而诞生的。即,计算机是为了进行计算的。

        而计算,就需要数据。

        而要计算,任何一个时刻,不是所有的数据都要立马被计算。

        举个例子:

        如同要吃饭,不是所有的饭菜都要立马被你吃掉。饭要一口一口吃,那么你还没有吃到的饭菜,就需要暂时放在盘子里。

        这里的盘子,就如同变量,饭菜如同变量里面的数据。

        换句话说,为何需要变量?因为有数据需要暂时被保存起来,等待后续处理。

        那么,为什么吃饭要盘子?我想吃一口菜了,直接去锅里找不行吗?当然行,但是效率低。

        因为我们吃饭的地方,和做饭的地方,是比较"远"的。

🍉1.2.5 变量定义的本质

        我们现在已知:

        1. 程序运行,需要加载到内存中

        2. 程序计算,需要使用变量

        那么,定义变量的本质:在内存中开辟一块空间,用来保存数据。

        (为何一定是内存:因为定义变量,也是程序逻辑的一部分,程序已经被加载到内存)

🍉1.2.6 变量声明的本质

        举个关于女朋友的例子:

        班里有个女生叫小芳,你们宿舍一共六人,无一例外都喜欢这个女生,但是只有你有勇气去告白,你对她说:我宣你,做我女朋友吧。小芳答应了,这时候她就是你的女朋友了(定义)。然后你屁颠屁颠地跑回宿舍,一边手舞足蹈一边一个一个地告诉你的其他五个舍友小芳已经是你的女朋友了(声明)。

        其实吧,定义就是创建存在,声明就是广而告知。

        定义只能有一次,就好比你对一个人表白成功后不会再次表白一样,因为已经确立创建了关系,最起码也可以维持一段时间,不需要重复表白;声明可以有多次,你可以和这个人说,也可以和那个人说,给别人说主要是向外表明这段关系的存在。

🍉2. 存储类别

🍉2.1 前置铺垫

       程序中的数据存储在内存中,从硬件方面来看,被存储的每个值都要占用一定的物理内存,而C语言就把这样的一块内存称为对象(区别于面向对象编程的对象)。对象可以存储一个或多个值。

        好了,对象里面存入了值,如何使用呢?程序需要一种方法访问对象,可以通过定义变量来完成:

        int val = 10;

        该定义创建了一个名为val的标识符,由这个标识符来指定特定的对象的内容。也就是我们需要给定一个名称才能很好地找到并使用我们想要用的对象的值,要是没有命名的话谁知道哪个对象放的是想要的值。

        变量名不是指定对象的唯一途径,还可以通过指针解引用来指定,比如

       int *p = &val;

       *p = 2;

      还可以通过什么指定对象呢?其实还有字面量,何谓字面量?字面量简单来说就是各种类型数据的值,是除了符号常量以外的常量,比如123,'a',"hello world",6.66等等。实际上常量也存储在内存中,只是存的地方是一个只读区域。所以字面量也可以指定对象。

      之前的博客有提到过左值,详情请移步至:

      左值又被称为对象定位值,顾名思义,就是可以用来定位对象的值,也就是这个值可以指定特定的对象,而可以指定对象的有哪些东西呢?综合前面讲的,有以下几种:

        标识符

        某些表达式

        字面量

        定义一个数组

        int rank[10];

        那么rank + 2 * val这个表达式是不是左值呢?

        不是,它既不是标识符,又没有指定对象,也就不是左值,实际上这个表达式的值是临时的,如果没有赋值给某个变量,语句结束后就消失了。但是*(rank + 2 * val)这个表达式就是左值,因为它制定了特定的对象,原本是指针偏移得到的地址,解引用后就指向对应地址处的数值,也就指向了特定内存位置的值。

        如果可以使用左值改变对象中的值,该左值就是一个可修改的左值。而一般不可修改的左值有两类,一类是const修饰的变量,另一类是字面量。

        可以用存储期(生命周期)描述对象,指的是对象在内存中保留了多长时间。标识符用于访问对象,可以用作用域和链接描述标识符,它们表明了程序的哪些部分可以使用标识符。不同的存储类别具有不同的存储器,作用域和链接。

        标识符可以在源代码的多文件中共享、可以用于特定文件的任意函数中、可以仅限于特定函数中使用,甚至可以只在函数中的某部分使用。

        对象可存在于整个程序的执行期,也可以仅存在于它所在函数的执行期,这取决于它的存储期。

我们先学习一波作用域、链接和存储期,再具体介绍存储类别。

🍉2.2 作用域

        作用域描述的是程序中可访问标识符的区域。

        一个C变量的作用域有以下几类:

        块作用域

        函数作用域

        函数原型作用域

        文件作用域

🍉2.2.1 块作用域

        块是用一对花括号{ }括起来的代码区域。例如,整个函数是一个块,而函数中的任意复合语句也是一个块。

        定义在块作用域的变量具有块作用域,其可见范围是从定义处到包含该定义的块的末尾。

        我们使用的局部变量(包括函数形参)都具有块作用域

        示例:

int add(int x, int y)
{
	return x + y;
}
int main()
{
	int a = 10;
	if (10 == a)
	{
		int b = 5;
		for (int i = 0; i < b; i++)
		{
			char ch = 'k';
			printf("%c ", ch);
		}
		printf("%d ", add(a, b));
	}
	return 0;
}

分析一下:

        定义在内层块里面的变量,作用域仅限于其所在块,也就只有内层块中的代码才能访问。

        以前,具有块作用域的变量都必须在块的开头定义,C99标准放宽了这一限制,允许在块中的任意位置声明变量。因此可以在for()的圆括号内的条件初始化的位置定义变量。

🍉2.2.2 函数作用域

        仅用于goto语句的标签,这意味着一个标签无论在函数内的任何地方出现,其作用域都延伸至整个函数,并且不可跨函数

🍉2.2.3 函数原型作用域

        用于函数原型中的形参,从形参定义处到原型声明结束。

        比如:

        int mighty(int, double);

        编译器在处理函数原型的形参时只关心它的类型,而形参名通常无关紧要,即使有形参名也不必与函数定义中的形参名相匹配。

🍉2.2.4 文件作用域

        变量定义在函数外面,具有文件作用域,范围从它的定义处到该定义所在的文件末尾,也就是说可以在该文件内任何地方该变量均可见,均可使用该变量。

        由于这样的变量可用于多个函数,我们称之为全局变量

        关于翻译单元

🍉2.3 链接

        链接简单来说就是将源文件编译生成的目标文件与链接库进行链接。

        C变量有三种链接属性:

        外部链接

        内部链接

        无链接

        具有块作用域、函数作用域或函数原型作用域的变量都是无链接变量,这意味着这些变量属于定义它们的块、函数或原型独有。

        具有文件作用域的变量可以是外部链接或内部链接。

        外部链接变量可以在多文件程序中使用也就是可以在多个翻译单元中使用,内部链接变量只能在一个翻译单元中使用。

        如何知道文件作用域变量是内部链接还是外部链接?看其定义时是否被存储类别关键字static修饰,比如

int giants = 6; // 具有外部链接属性
static int gogers = 5; //具有内部链接属性

        总结一下:

        作用域是单个翻译单元内的概念,而链接属性是翻译单元层面的概念,它们两个共同决定变量的可见性,也就是说决定在哪些地方可以使用该变量。

🍉2.4 存储期

        作用域和链接描述了标识符的可见性,而存储期则描述了通过这些标识符访问的对象的生命周期。

        C对象有四种存储期:

        静态存储期

        线程存储期

        自动存储期

        动态分配存储期

🍉2.4.1 静态存储期

        在程序的执行期间一直存在。所有文件作用域变量都具有静态存储期,关键字static只表明了文件作用域变量的链接属性而非存储期。

        而局部变量被static修饰后生命周期发生变化而作用域不变,具有静态存储期。

🍉2.4.2 线程存储期

        具有线程存储期的对象,从被声明到线程结束一直存在。线程存储期用于并发程序设计,程序执行可被分为多个线程。

🍉2.4.3自动存储期

        块作用域的变量通常都具有自动存储期。当程序进入定义这些变量的块时,为这些变量分配内存;当退出块时,释放之前分配的内存。比如:一个函数调用结束后,其变量占用的内存可用于存储下一个被调用函数的变量。

与函数栈帧有关,感兴趣的可以移步至作者写的这篇博客:一文带你深入浅出函数栈帧

        变长数组稍有不同,它的存储期从声明处到块的末尾,而不是从块的开始到块的末尾。

        总结一下:

        全局变量是定义在函数外面,具有文件作用域和静态存储期的变量。

        局部变量是定义在块内的,具有局部作用域(块作用域、函数作用域或函数原型作用域)和自动存储期的变量。

        然而,局部变量也能具有静态存储期,只需要在定义时前面加上一个static关键字即可。

        比如:

int main()
{
    static int ct = 10;
    . . .
    return 0;
}

        变量ct存储在静态内存中,在程序执行期间一直存在,但是其作用域未发生改变,仍然是main()函数块中。不过值得注意的是,可以给其他函数提供地址以间接访问该对象。

🍉3. 存储类别

        不同存储类别变量的总结表格:

🍉3.1 自动变量

        对应表格的第一行,默认情况下,声明在块内或函数头中的任何变量都属于自动存储类别。实际上,局部变量,自动变量,临时变量,都是一回事,我们统称局部变量。

🍉3.1.1 自动变量的初始化

        自动变量并不会自动初始化,必须显式初始化,比如

int main()
{
    int repid;
    int tents = 10;
}

        变量repid的内容可能是随机值,具体是什么值与编译器和函数栈帧的初始化有关,反正不手动初始化的话放的就是一个垃圾值。

🍉3.1.2 小插曲——auto关键字

        关键字auto是存储类别说明符之一,用来修饰自动变量。

        如何使用:一般在代码块中定义的变量,即局部变量,默认都是auto修饰的,不过一般省略,所以现在在C语言里基本上用不到该关键字。

🍉3.1.3 自动变量的特点

        块作用域和无链接属性意味着只有在变量定义所在的块中才能通过变量名访问该变量(当然,使用函数传参或函数返回值这类间接方式也可以实现变量值在块之间的传递)。不同函数中可以使用同名变量,因为一般而言这些变量存储位置不同,不是一回事儿,而且作用域都限制在彼此的函数内,互相之间不会冲突。

        举个例子,就好比咱们班里有个叫张三的,隔壁班也有一个同名的,可他们是同一个人吗?压根就不是。但是他们在各自的班级里正常上课互不干扰,老师同学们也都能分得清他们。

        变量具有自动存储期意味着,程序在进入该变量声明所在的块时变量存在,而程序在退出该块时变量消失,原来该变量所占用的内存位置现在可以被其他变量使用。

🍉3.1.4 关于块嵌套问题

        块中的声明的变量仅限于该块及其包含的块使用。

        那要是内层块中声明的变量与外层块中的变量同名会怎样?

        实际上,内层块会隐藏外层块的定义,直到离开内层块之后才可见。

例子:

        程序的输出:

分析:

🍉3.2 寄存器变量

        变量通常储存在计算机内存中,而寄存器变量则储存在CPU的寄存器中。

🍉3.2.1 硬件简单介绍

        其实,CPU主要是负责进行计算的硬件单元,但是为了方便运算,一般第一步需要先把数据从内存读取到CPU内,那么也就需要CPU具有一定的数据临时存储能力。注意:CPU并不是当前要计算了,才把特定数据读到CPU里面,那样太慢了。

        所以现代CPU内,都集成了一组叫做寄存器的硬件,用来做临时数据的保存。

存储金字塔:

        距离CPU越近的存储硬件,速度越快。

        寄存器存在的意义:在硬件层面上,提高计算机的运算效率。因为不需要从内存里读取数据啦。

🍉3.2.2 变量声明与特点

        寄存器变量使用存储类别关键字register声明,比如:

int main()
{
    register int quick;
}

register 修饰变量

        尽量将所修饰变量放入CPU寄存器中(不一定),从而达到提高效率的目的。

        那么什么样的变量,可以采用register呢?

1. 局部的(全局会导致CPU寄存器被长时间占用)

2. 不会被写入寄存器的(写入就需要写回内存,后续还要读取检测的话,register的意义在哪呢?)

3. 高频被读取的(提高效率所在)

4. 如果要使用,请不要大量使用,因为寄存器数量有限

特点

        与普通变量相比,访问和处理寄存器变量的速度更快。

        注意点:寄存器变量有没有地址呢?

        答案是没有,因为地址是内存的概念,而寄存器和内存完全是两码事。

        绝大多数方面,寄存器变量和自动变量都一样,它们都是块作用域、无链接属性和自动存储期的。

        声明寄存器变量与直接命令相比更像是一种请求,编译器必须根据寄存器或最快可用的内存的数量来衡量你的请求,或者直接忽略你的请求,不一定会如你所愿的。这种情况下,寄存器变量就与自动变量几乎没有区别,只是仍旧不能对该变量使用地址运算符。

        可声明为register的数据类型有限,例如,处理器中的寄存器可能没有足够大的空间来储存double类型的值。

        关键字register其实不用管,因为现在的编译器,已经很智能了,能够进行比人更好的代码优化。只是早期编译器需要人为指定register,来进行手动优化,现在不需要了。

🍉3.3 静态变量

        对于静态变量,你可能对它存在一个误解:是不是意味着该变量不可变呀。No!

        实际上,静态的意思是该变量在内存中原地不动(静态数据区),而并不是说它的值不变。

🍉3.3.1 块作用域的静态变量

        这个其实就是前面提到过的static修饰的局部变量,它和自动变量唯一的区别就是存储期不同——被改变成了静态存储期,也就是说出了它的定义所在的块后变量仍然存在。

        不能在函数的形参中使用static:

        int wont(static int flu);//不允许

例子:

void fun1()
{
    int i = 0;
    i++;
    printf("no static: i=%d\n", i);
}

void fun2()
{
    static int i = 0;
    i++;
    printf("has static: i=%d\n", i);
}

int main()
{
    for (int i = 0; i < 10; i++)
    {
        fun1();
    }
    for (int i = 0; i < 10; i++)
    {
        fun2();
    }

    return 0;
}

为什么会这样?

        首先看fun1(),里面的i变量是局部变量,在函数时开辟空间并初始化,而在函数调用结束后释放空间而销毁,每次调用时i都是重新创建并赋值的,所以循环调用打印出来的结果相同。

        而在fun2()中的i变量就不一样了,从结果看出,很明显上一次调用结束后i的值依旧保存着,接着可以在下一次调用中继续使用,你可能会觉得不是有static int i = 0;嘛,每次进来不都赋值为0了吗?实际上你需要区分一下初始化和赋值,那是初始化,而初始化只会初始化一次!

        static修饰局部变量,变量的生命周期变成静态存储期。(作用域不变)

为什么局部变量具有自动存储期而static修饰局部变量具有全局性呢?

        这跟数据在C程序地址空间(不是内存)内的存储有关:

        局部变量存储在栈区,而static修饰的局部变量存储位置相较于原局部变量发生了改变,存储在静态数据区了,因此生命周期发生了改变。        

        我们由此也可以看出,生命周期和变量存储位置有较为紧密的关系。

🍉3.3.2 外部链接的静态变量

        外部链接的静态变量具有文件作用域、外部链接属性以及静态存储期,也被称为外部变量。

        一般把变量的定义放在所有函数外面便创建了外部变量。

1.初始化外部变量

        外部变量可以被显式初始化,但如果未手动初始化的话它们会被自动初始化为0。这一原则同样适用于外部定义的数组元素。

        只能使用常量表达式初始化文件作用域变量:

2.使用外部变量

        在同一源文件下使用外部变量一般不需要特别声明,因为该变量本身具有文件作用域,全文件范围内可见嘛,不过要是块中定义了同名变量的话,当程序运行进入块中时,会暂时隐藏外部变量,采用“就近原则”使用块中的同名变量,直到程序运行出了该块为止。

        这时候如果就想要在块中使用外部变量的话,就需要使用存储类别关键字extern修饰一下,以此声明该变量,这样就表示你在这使用的是外部变量而非块中同名变量。

        如果一个源代码文件使用的外部变量定义在另一个源代码文件中,则必须使用extern在该文件中声明该变量。使用extern声明并不会引起存储空间的分配,而是指示编译器该变量的定义不在这里,用它来引用外部定义。

🍉3.3.3 内部链接的静态变量

        该类型变量具有静态存储期、文件作用域和内部链接。在所有函数外用static修饰而定义的变量就是内部链接的静态变量。

🍉3.3.4 存储类别小结

🍉3.3.5函数的存储类别

        函数也有存储类别,分为外部函数、静态函数以及C99新增的内联函数(本文不讲)。

        外部函数可以在其他文件使用,而静态函数只能用于其定义所在的文件。

        默认情况下,函数为外部函数,在其他文件中用extern声明后就可以被使用,并且函数声明即使没有extern也会默认为extern修饰的,除非遇到static修饰。

        静态函数就是受到static修饰的函数,相当于屏蔽了函数的外部连接属性,受限于其定义所在文件。

🍉3.4 多文件

        当程序由多个翻译单元组成时,体现出区别内部链接和外部链接的重要性。

        复杂的C程序通常由多个单独的源文件组成,文件之间可能要共享外部变量,C通过在一个文件中定义,在其他文件中进行声明来引用以实现共享。注意要使用关键字extern声明。

🍉3.4.1 extern 在多文件下的理解与使用

        .h:我们称之为头文件,一般包含函数声明,变量声明,宏定义,typedef,struct,头文件等内容,也就是把可能会多次用到的东西全部放在一块,在组织项目结构的时候,能减少大型项目地维护成本问题。

        .h基本都是要被多个源文件包含的,头文件又有可能被重复包含

        .c: 我们称之为源文件,一般包含函数实现,变量定义等

        编译器发出警告针对的是编译期间的问题,在main.c中使用test.c的函数show()实际上是在链接之后才实现的,也就是说,在链接之前,这两个源文件各自不知道对方的内容,所以main.c中无法找到show()函数,而它又不知道show()在test.c中,就认为是未定义的函数,发出警告。要解决这个问题就要用extern声明show函数。

        并且g_val变量已经在test.c中定义了,变量只能定义一次,所以在main.c中只能声明而不能赋值(赋值就相当于再次定义初始化了)。声明并没有开辟空间,所有变量声明时,不能设置初始值。

        要在别的源文件中使用其他源文件中的变量和函数,要么就声明一下比如extern int g_val; ,要么就把变量定义在头文件中,方便各个源文件使用,不过要注意在定义时用static修饰一下,不然会报错。

extern int g_val;//变量声明必须带上extern

        因为int g_val很有可能会被编译器认为是变量定义,只是没有初始化而已,为了避免这种摸棱两可的二义性,必须得带上extern。

extern void show();//函数声明建议带上extern

        实际上void show();还是函数声明,函数是不是定义取决于后面有没有函数体,所以其实不加上也还是声明,只不过为了清晰明了,最好就把extern加上。

        将函数声明和函数定义分别放在头文件和源文件是为了分离,可以把源文件打包成静态库,这样别人只能通过头文件知道函数功能,也就是别人只能调用而不知道具体实现细节。

        比如:

🍉4. 类型限定符

 

🍉4.1 const限定符

可以用来:

//const修饰一般变量
const int a = 10;

//const修饰数组
const int arr[4] = {1, 2, 3, 4};

//const修饰指针
int a = 10;
const int* p1 = &a;
int*const p2 = &a;

        const用来修饰变量为常变量,也就是使其具有常量性质而不能被直接修改值(如赋值、自增或自减),而间接可以改,比如使用指针解引用。

        所以const修饰的变量并非是真的不可被修改的常量。

🍉4.1.1 const修饰变量的意义

        1. 让编译器进行直接修改式的检查,如果发现被直接修改的话会报错,也就是不想后续让别人或自己直接修改特定变量的值。

        2. 也属于一种“自描述”含义,告诉其他程序员(正在改你代码或者阅读你代码的人)这个变量后续过程中不要修改。

🍉4.1.2 const修饰的注意事项

        const修饰的变量,可以作为数组定义的一部分吗?

int main()
{
    const int n = 100;
    int arr[n];
    system("pause");
    return 0;
}

        在vs(标准C)下直接报错了,而在gcc(GNU扩展)下可以

        但我们一切向标准看齐,就认为是不可以。

        const修饰的变量只能在定义的时候直接初始化,不能二次赋值。为什么?

        因为一旦被修饰了就不能再被直接修改。

🍉4.1.3 const修饰指针

常量指针

        const 类型 * ptr

        如const int * ptr,而int const* ptr这样写也没问题。

        为什么要叫常量指针?意味着它指向的是常量吗?

        比如:

        int a = 10;
        const int* ptr = &a;

        这样一来就不能通过解引用ptr来改变a的值了,也就是对于指针来说指向的是常量(不可变更),实际上a还是变量。

指针常量

        类型* const ptr

        如int* const ptr

        为什么要叫指针常量呢?真的变成常量了吗?

        比如:

        int a = 10;
        int const*ptr = &a;

        这样一来ptr的值不能改变了,也就是ptr只能指向a了,而ptr还是变量。

总结:

 const修饰指针变量的时候:

        1. const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。但是指针变量本身的内容可变。

        2. const如果放在*的右边,修饰的是指针变量本身,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。

常量指针常量

        const 类型 * const ptr

        综合了常量指针和指针常量的特点,也就是既不能改变指针的内容,又不能改变指针指向的内容。

        

举个日常生活中的例子来加深理解:

🍉4.1.4 const与非const修饰的类型赋值问题

例1

int a = 10;
const int* p = &a;
int *q = p;

        编译器会报警告

例2

int *p = &a;
const int *q = p;

        编译器不报警告

小经验:

        把一个类型限定不怎么严格的变量赋值给另一个类型限定非常严格的变量,编译器不报警。

        把一个类型限定比较严格的变量赋值给另一个类型限定不怎么严格的变量,编译器会报警。

        比如上面的例1,要不报警的话就要把指针q用const修饰一下:const int *q。

为什么会这样呢?

        编译器为什么会报警?仅仅是类型不同才报警的吗?那例2又不会报警,这样说不通。

        编译器报警是因为检测到存在危险的行为,例1中p被const修饰就表明不想用指针改变a的值,也就是严格限定了p的行为,然而把p的值赋给另一个没有const修饰也就是限定不怎么严格的指针q其实就是危险行为,因为q可以直接解引用去修改a的值,相当于放宽了原来的某种限制,编译器就认为该行为有危险,可能是用户的疏忽导致的,需要报警提示用户。

举个例子

🍉4.1.5 const修饰函数参数

        在C中,任何函数参数都一定要形成临时变量,即使是传址。

        const修饰函数参数一般修饰的是指针变量,因为只有传址才有可能解引用修改到主函数中的实参,不然形参也只是实参的一份临时拷贝,改变形参并不会影响实参。

        而用const修饰也就意味着不想在调用的函数内部修改到实参的值。

例子:

//该函数我只想让它执行打印功能
void print(const int* p)
{
    printf("This is %d.", *p);
    *p = 10;//编译器会报错
}


int main()
{
    int n = 100;
    int* q = &n;
    print(q);
    return 0;
}

        const修饰后变为不可修改的左值。

🍉4.1.6 const修饰函数返回值

例子:

//告诉编译器,告诉函数调用者,不要试图通过指针修改返回值指向的内容
const int* test()
{
    static int g_var = 100;
    return &g_var;
}

int main()
{
    int *p = test(); //有告警
    //const int *p = test(); //需要用const int*类型接受,这样在语法/语义上限制了返回值,不能直接修改函数的返回值
    *p = 200;
    printf("%d\n", *p);
    return 0;
}

        一般内置类型(基本类型)返回,加const无意义,因为对于传值引用,形参只是实参的一份临时拷贝,改变形参并不会影响实参。

🍉4.2 volatile限定符

        在汇编角度,在Linux平台给大家对比演示一下加还是不加volatile的作用,让大家看明白。

        首先,代码会被编译器进行如下的优化

        而这个关键字volatile,是不希望代码被编译器优化,以达到稳定访问内存的目的。

不加 volatile

int pass = 1;
int main()
{
    while(pass)
    {
        ;
    }
    return 0;
}

看看优化后的反汇编代码

        我们发现,优化后的代码很会“偷懒”,编译器检测到了pass的值为1且无变动,所以知道了这会是一个死循环,于是不再一次一次从内存中读入数值再检测,那得多麻烦不是吗,它就直接自己跳转自己,一直跳也就死循环了。

加上volatile

volatile int pass = 1;
int main()
{
    while(pass)
    {
        ;
    }
    return 0;
}

        结论: volatile 忽略编译器的优化,保持内存可见性。

        const volatile int a = 10;

        在vs和gcc 4.8中都能编译通过。

        const是在编译期间起效果。

        volatile在编译期间主要影响编译器,形成不优化的代码,进而影响运行,故:编译和运行都起效果。

        const要求你不要进行写入就可以。volatile意思是你读取的时候,每次都要从内存读,两者并不冲突。

        虽然volatile就叫做易变关键字,但这里仅仅是描述它修饰的变量可能会变化,要编译器注意不要做优化,并不是它要求对应变量变化!这点要特别注意。


🍉敬请期待更好的作品吧~

感谢观看,你的支持就是对我最大的鼓励,阁下何不成人之美,点赞收藏关注走一波~ 

 

 

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

桦秋静

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值