LV01-02-C语言-变量与运算符

本文主要是C语言基础——变量与运算符相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
Windows windows11
Ubuntu Ubuntu16.04的64位版本
VMware® Workstation 16 Pro 16.2.3 build-19376536
SecureCRT Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日
开发板 正点原子 i.MX6ULL Linux阿尔法开发板
uboot NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03)
linux内核 linux-4.15(NXP官方提供)
STM32开发板 正点原子战舰V3(STM32F103ZET6)
点击查看本文参考资料
参考方向 参考原文
------
点击查看相关文件下载
--- ---

一、变量

前边我们知道,数据都会以二进制的形式存在于内存中,那想要使用这个数据怎么办?现实生活中我们会找一个盒子或者袋子来存放物品,既显得整洁,也方便以后找到。计算机也是这样,我们需要先在内存中找一块区域,规定用它来存放某个数据,并起一个名字,方便以后查找。这块区域的名字,就叫做变量。变量在内存空间中的首地址,称为变量的地址

1. 变量的定义

在 C 语言中,声明变量的一般形式是:

1
<存储类型> <数据类型 > <变量名> ;

【说明】

存储类型 有四种,分别是auto、register、static和extern
数据类型 可以是基本数据类型,也可以是自定义的数据类型
变量名 由字母、数字、下划线组成,不能以数字开头,不能和C语言的关键字重名。
例如,
1
2
int a;  /* 声明了一个整型变量 a */
char b; /* 声明了一个字符型变量 b */

【注意】

(1)变量在声明的时候可以一次声明多个。例如,

1
int a, b, c;

(2)声明变量是一条语句,注意后边的分号( ; )不可缺少。

2. 变量的使用

2.1 变量的引用

当我们想要使用一个变量的时候,就直接使用变量名即可。例如,

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

int main(int argc, const char *argv[])
{
int a;

printf("a = %d\n", a);

return 0;
}

此时在命令行执行以下命令:

1
gcc test.c -Wall # 编译程序

这个时候会发现程序有警告:

1
test.c:7:2: warning: ‘a’ is used uninitialized in this function [-Wuninitialized]

说的就是这个变量没有进行初始化,下边我们就来看一看如何初始化变量。

2.2 变量的赋值

我们上边仅仅是定义了一个变量,还没有放数据进去,在编译的时候就报了警告。变量的赋值有两种方式:

  • 在声明变量之后再进行单独赋值
1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

int main(int argc, const char *argv[])
{
int a;
a = 10;
printf("a = %d\n", a);

return 0;
}

此时在终端执行以下命令:

1
2
gcc test.c -Wall # 编译程序
./a.out # 执行可执行文件

这个时候会发现终端有以下信息输出:

1
a = 10
  • 在声明变量的同时进行赋值

这也被称为变量的初始化

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

int main(int argc, const char *argv[])
{
int a = 10;

printf("a = %d\n", a);

return 0;
}

此时在终端执行以下命令:

1
2
gcc test.c -Wall # 编译程序
./a.out # 执行可执行文件

这个时候会发现终端有以下信息输出:

1
a = 10

其实下边的写法是等价的

1
2
3
4
int a = 10
// 等价于
int a;
a = 10;

3. 变量的作用域

变量作用域的定义:程序中可以访问一个指示符的一个或者多个区域,即变量出现的有效区域决定了程序的哪些部分通过变量名来访问变量。其实我自己的理解就是:作用域就是这个变量的生存区域,在这个区域内变量有效,区域外变量无法访问。

3.1 变量作用域类别

  • (1)函数原型作用域

它指的是在声明函数原型时所指定的参数标识符的作用范围。因为作用范围是小括号内,所以函数原型声明中的标识符可以与函数定义中说明的标识符名称不同。只要让函数声明和函数定义中小括号内每个变量的类型及数目一致即可,也可以省略掉参数名(函数定义在后,调用在前),例如,

1
int func(int a, char b);
  • (2)块作用域

块作用域也称为局部作用域,也就是语句块的作用域。一般在函数中起到分割的作用。

1
2
3
4
5
6
7
8
{
int a = 5; /* a的作用域起始处 */
if(a > 0)
{
int b; /* b的作用域起始处 */
b = a - 3;
} /* b的作用域结束处 */
}/* a的作用域结束处 */
  • (3)函数作用域

在函数内部定义的一些变量,只能在函数内部使用,一旦离开了这个函数,就必须重新定义。其实可以把函数看做一个块,然后根据作用域的定义。例如,

1
2
3
4
5
6
7
8
9
int func(int x, int y)
{
int a = 5; /* a的作用域起始处 */
if(a > 0)
{
int b; /* b的作用域起始处 */
b = a - 3;
} /* b的作用域结束处 */
}/* a的作用域结束处 */
  • (4)文件作用域

在所有函数外定义的标识符称为全局标识符,定义的变量称为全局变量。全局标识符的作用域是文件作用域,即从它声明开始到文件结束都是可见的。标识符的文件作用域一般有三种情况:

①、全局常量或全局变量的作用域是从定义开始到源程序文件结束。例如,

1
2
3
4
5
6
int a = 10;
int main()
{
// ... ...
return 0;
}

②、函数的定义中包含了函数声明,所以一旦声明了函数原型,函数标识符的作用域就从定义开始到源程序文件结束。例如,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int func1();  /* 函数 fun1 的作用域从此开始到文件结束 */
int func2() /* 函数 fun2 的作用域从此开始到文件结束 */
{
// ... ...
return 0;
}
int main()
{
// ... ...
return 0;
}

int func1()
{
// ... ...
return 0;
}

③、还有一种在头文件中定义的标识符,预编译时,编译器会将头文件的内容在源文件的相应位置展开,所以在头文件中定义的标识符的作用域可以看成从 #include 头文件开始的位置到源程序文件结束。

3.2 作用域的重叠会发生什么?

标识符的作用域完全相同时,不允许出现相同的标识符名,而当标识符有不同作用域时允许标识符同名。如果是作用域嵌套的情况下,如果内层和外层的作用域声明了同名的标识符,那么在外层作用域中声明的标识符对于该内层作用域时不可见的。也就是说,在内层中声明的变量 i 和外层变量 i 无关,当内层变量改变时,与之同名的外层变量的值不受影响。例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

int main(int argc, char *argv[])
{
int a = 5;
{
int a = 7;
printf("in{} :a=%d\n", a);
a = 10;
}
printf("out{}: a=%d\n", a);

return 0;
}

我们编译后运行,会得到以下输出情况:

1
2
in{} :a=7
out{}: a=5

3.3 不同存储类型变量的作用域

存储类别 存储期 作用域 声明方式
auto 动态 块内
register 动态 块内,使用关键字 register
static(局部) 静态 块内,使用关键字 static
static(全局) 静态 文件内部 所有函数外,使用关键字 static
extern 静态 文件外部 所有函数外

4. 变量的存储模型

变量是程序中数据的存储空间的抽象。变量的存储方式(也可以被称之为存储期)可分为静态存储动态存储两种:

  • 静态存储变量,通常是在程序编译时就分配一定的存储空间并一直保特不变,直至整个程序结束。全局变量的存储方式就属于这种存储方式
  • 动态存储变量,是在程序执行过程中使用它时才分配存储空间,使用完毕的时候内存空间将会被立即释放。

变量的存储类型大概分为四种:自动类型寄存器类型静态类型外部类型。变量的存储模型若由作用域、链接点和存储期(存储期描述的是变量在内存中的生存时间)三大属性来描述的话,变量的存储模型可以分为五种,这五种其实就相当于把四种存储类型细分了,下边将详细介绍。

4.1 自动类型

自动变量就是指非静态局部变量,自动变量声明的语法格式为

1
[auto] 数据类型 变量名;

auto 为存储类说明符,它是可选的,一般我们会选择省略。说明这个变量为自动变量,这种自动变量具有动态存储期、代码块的作用域和空链接

  • 自动变量的特点

(1)自动变量的作用域仅限于定义该变量的模块内。在函数中定义的自动变量,只在该函数内有效。在复合语句中定义的自动变量,只在该复合语句中有效。

(2)自动变量属于动态存储方式,只有在定义该变量的函数被调用时才给它分配存储单元,开始它的生存期。函数调用结束,释放存储单元,结束生存期。因此函数调用结束之后,自动变量的值不能保留。在复合语句中定义的自动变量,在退出复合语句后也不能再使用,否则将引起错误。

(3)由于自动变量的作用域和生存期都局限于定义它的模块内(函数或复合语句内),因此不同的模块中允许使用同名的变量而不会混淆。即使在函数内定义的自动变量也可与该函数内部的复合语句中定义的自动变量同名,但我们应尽量避免使用这种方式。

【注意】

(1)代码块或者函数头部定义的变量,可以使用存储类修饰符 auto 来明确标识属于自动存储类型。若没有 auto 修饰,默认也是自动类型。

(2) auto 型变量如果没有进行初始化,那么它默认是随机值

4.2 寄存器类型

在一个代码块内(或在一个函数头部作为参量)使用修饰符 register 声明的变量属于寄存器存储类。一般声明格式为:

1
register 数据类型 变量名;

register 修饰符暗示编译程序相应的变量将被频繁使用,如果可能的话,应将其保存在 CPU 的寄存器中,从而加快其存取速度。该类与自动存储类相似,具有自动存储期、代码块作用域和空链接

  • 使用 register 修饰符有几点限制:

(1) register 变量必须是能被 CPU 寄存器所接受的类型,这通常意味者 register 变量必须是一个单个的值,并且其长度应小于或等于整型的长度,这与处理器的类型有关。

(2)声明为 register 仅仅是一个请求,而非命令,因此变量仍然可能是普通的自动变量,没有放在寄存器中。

(3)只有局部变量和形参可以作为 register 变量,全局变量不行:

(4)实际上有些系统并不把 register 变量存放在奇存器中,而优化的编译系统则可以自动识别使用频繁的变量而把它们放在奇存器中。

【注意】

(1)不能用 & 来获取 register 变量的地址。

(2) register 型变量如果没有进行初始化,那么它默认是随机值

4.3 静态类型

静态变量的类型说明符是 static 。它分为两种,一种是静态局部变量(静态、空链接),一种是静态全局变量(静态、内部链接)。静态局部变量属于静态存储方式。

4.3.1静态、空链接

在一个代码块内使用存储类修饰符 static 声明的局部变量属于静态空链接存储类。该类具有静态存储期、代码块作用域和空链接。一般格式如下:

1
2
3
4
int func()
{
static 数据类型 变量名;
}

静态变量的存储空间是在编译完成后就分配了,并且在程序运行的全部过程中都不会撤销。但是属于静态存储方式的变量不一定就是静态变量。例如,外部变量虽属于静态存储方式,但不一定是静态变量,必须由 static 加以定义后才能称为静态外部变量,或称静态全局变量。

image-20220402162636518

静态局部变量在函数内定义,它的生存期为整个程序执行期间,但是其作用域仍与自动变量相同,只能在定义该变量的函数内使用该变量。退出该函数后,尽管该变量还继续存在,但不能使用它。静态局部变量生存周期与作用域如下图:

image-20220402163051101

根据静态局部变量的特点,可以看出它是一种生存期为整个程序运行期的变量。虽然离开定义它的函数后不能使用,但如再次调用定义它的函数时,它又可以继续使用,并且保留了上次被调用后的值。因此,当多次调用一个函数且要求在调用之前保留某些变量的值时,可以考虑采用静态局部变量。虽然用全局变量也可以达到上述目的,但全局变量有时会造成意外的副作用,因此仍以采用静态局部变量更好些。

【注意】

(1)可以对构造类静态局部量赋初值,例如数组。若未赋初值,则由系统自动初始化为 0 。基本数据类型的静态局部变量若在说明时未赋初值,则系统自动赋予 0 。

4.3.2 静态、内部链接

全局变量在关键字之前再加上 static 就构成了静态的全局变量,属于静态、内部链接存储类。与静态、外部链接存储类不同的是,具有内部链接,使得这种变量仅能被与它在同一个文件的函数使用。一般格式如下:

1
2
3
4
5
static 数据类型 变量名;
int main()
{

}

这样的变量也是只会在在编译时初始化一次。如若未明确初始化,它的初始值被设定为 0 。

非静态全局变量的作用域是整个源程序,但当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的;而静态全局变量则限制了其作用域,即只在定义该变量的源文件内有效,在同一源程序的其他源文件中不能使用。由于静态全局变量的作用域局限于一个源文件内,只能被该源文件内的函数使用,因此可以避免在其他源文件中引起错误。两者作用域区别如下图:

image-20220402165150484

4.4 外部类型

未使用 static 修饰的全局变量属于静态、外部链接存储类。具有静态存储期文件作用域外链接。一般格式如下:

1
2
3
4
5
数据类型 变量名;
int main()
{

}

只会在在编译时初始化一次。如若未明确初始化,它的初始值也被设定为 0 。在使用外部变量的函数中使 extern 关键字来再次声明。如果是在其他文件中定义的,则必须使用 extern 。

二、常量

上边介绍了变量,接下来看一看常量吧。常量是固定值,在程序执行期间不会改变。这些固定的值,又叫做字面量。需要注意的常量一旦定义,是不可以被修改的。

1. 整数常量

整数常量可以是十进制、八进制或十六进制的常量。前缀指定基数: 0x 或 0X 表示十六进制, 0 表示八进制,不带前缀则默认表示十进制。

整数常量也可以带一个后缀,后缀是 U 和 L 的组合, U 表示无符号整数( unsigned ), L 表示长整数( long )。后缀可以是大写,也可以是小写, U 和 L 的顺序任意。

点击查看实例
75十进制
0253八进制
0x4b十六进制
50整数
50u无符号整数
50l长整数
50ul无符号长整数

2. 浮点常量

浮点常量由整数部分、小数点、小数部分和指数部分组成。我们可以使用小数形式或者指数形式来表示浮点常量。使用小数形式表示时,必须包含整数部分、小数部分,或同时包含两者。当使用指数形式表示时, 必须包含小数点、指数,或同时包含两者。一般格式如下:

1
[+ or -]<整数部分><小数点><小数部分><e或者E>[+ or -]<指数>

例如:

1
2
3
4
5
6
7
3.14159       /* 正确的 */
314159E-5L /* 正确的 */
-3.14e18 /* 正确的 */

510E /* 错误的:不完整的指数 */
210f /* 错误的:没有小数或指数 */
.e55 /* 错误的:缺少整数或分数 */

3. 字符常量

字符常量是指用单引号 ‘ ‘ 括起来的一个字符,例如 ‘a’ 、 ‘b’ 等。C 语言中还存在一种特殊的字符常量——转义字符。转义字符以反斜线 \ 开头,后跟一个或几个字符。转义字符具有特定的含义,不同于字符原有的意义,故称“转义”字符。

点击查看常用转义字符
转义字符含义
\a警报铃声
\b退格键
\f换页符,将当前位置移动到下页开头
\n换行符
\r回车
\t水平制表符
\v垂直制表符
\ooo1~3位八进制数所代表的字符
\xhh1~2位十六进制数所代表的字符
\\\ 字符

【注意】

(1)字符常量只能用单引号括起来,不能用双引号或其他括号。

(2)字符常量只能是单个字符,不能是字符串。

(3)字符可以是字符集中任意字符。但数字被定义为字符型之后就不能参与数值运算。例如 5 和 ‘5’ 是不同的, ‘5’ 是字符常量,不能直接参与运算,而只能以其 ASCII 码值( 053 )来参与运算。

4. 字符串常量

字符串常量是指用一对双引号( “ “ )括起来的一串字符,双引号只起定界作用,双引号括起的字符串中不能是双引号( “ “ )和反斜杠( \ )。例如 “YES&NO” 、 “2-21” 和 “a” 等都是合格的字符串常量。在 C 语言中,字符串常量在内存中存储时系统自动在字符串的末尾加一个字符串结束标志,即 ASCII 值为 0 的字符 NULL ,通常用 \0 表示。

【注意】在 C 语言中,长度为 n 个字符的字符串常量在内存中占有 n + 1 个字节的存储空间。例如,

1
2
'a'  这是一个字符常量,占用一个字节空间
"a" 这是一个字符串常量,占用两个字节空间(多了一个'\0')

5. 定义常量

5.1 define 定义常量

define 是一个预处理关键字,它的实际用途大于定义常量这一功能,其他的功能后边还会详细解释。通过 define 定义常量的一般格式为:

1
#define identifier value
  • identifier 为符号名,必须符合标识符的命名规则。
  • value 为替换列表,可以是任意的字符序列,如数字、字符、字符串、表达式等。

例如,

1
2
3
4
#define LENGTH 10   
#define WIDTH 5
#define BEEP '\a'
#define MSG "Hello World!"

5.2 const 定义常量

const 是 C 语言中的一个关键字,可以借助该关键字来定义常量,创建常量的格式如下:

1
const dataType name = value;
const 定义常量的关键字
dataType 数据类型,基本数据类型都可以,如int、char、float等
name 常量名称
value 常量的值
**【注意】**

(1) const 和 dataType 位置可以互换,例如,

1
2
const int a = 10;
int const a = 10;

上边两种定义等价,但是一般推荐 const 在前。

(2)由于常量一旦被创建后其值就不能再改变,所以常量必须在定义的同时赋值(初始化),后面的任何赋值行为都将引发错误。

三、运算符

1. 相关概念

C 语言中表示运算的符号称为运算符,运算的对象称为操作数。对一个操作数进行运算的运算符称为单目运算符,对两个操作数进行运算的运算符称为双目运算符三目运算符是对三个操作数进行运算。用运算符和括号可以将操作数连接起来组成表达式

2. 运算符与优先级

点击查看运算符及其优先级
优先级 运算符 名称(含义) 使用形式 结合规律 说明
1 [] 数组下标 数组名称[常量表达式] 左→右 ——
() 圆括号 (表达式)
函数名(形参表)
——
. 成员选择(对象) 对象.成员名 ——
-> 成员选择(指针) 对象指针->成员名 ——
2 - 负号运算符 -表达式 左←右 单目运算符
~ 按位取反运算符 ~表达式
++ 自增运算符 ++变量名
变量名++
-- 自减运算符 --变量名
变量名--
* 取值运算符(指针运算符) *指针变量
& 取地址运算符 &变量名
! 逻辑非运算符 !表达式
(类型) 强制类型转换 (数据类型)表达式 ——
sizeof 长度运算符 sizeof(表达式) ——
3 * 乘法运算符 表达式 * 表达式 左→右 双目运算符
/ 除法运算符 表达式 / 表达式
% 求余运算符(取模) 整型表达式 % 整型表达式
4 + 加法运算符 表达式 + 表达式 左→右 双目运算符
- 减法运算符 表达式 - 表达式
5 << 左移运算符 变量 << 表达式 左→右 双目运算符
>> 右移运算符 变量 >> 表达式
6 > 大于 表达式 > 表达式 左→右 双目运算符
>= 大于等于 表达式 >= 表达式
< 小于 表达式 < 表达式
<= 小于等于 表达式 <= 表达式
7 == 等于运算符 表达式 == 表达式 左→右 双目运算符
!== 不等于运算符 表达式 !== 表达式
8 & 按位与运算符 表达式 & 表达式 左→右 双目运算符
9 ^ 按位异或运算符 表达式 ^ 表达式 左→右 双目运算符
10 | 按位或运算符 表达式 | 表达式 左→右 双目运算符
11 && 逻辑与运算符 表达式 && 表达式 左→右 双目运算符
12 || 逻辑或运算符 表达式 || 表达式 左→右 双目运算符
13 ? : 条件运算符 表达式1 ? 表达式2 : 表达式3 左←右 三目运算符
14 = 赋值运算符 变量 = 表达式 左←右 ——
/= 除后赋值运算符 变量 /= 表达式 ——
*= 乘后赋值运算符 变量 *= 表达式 ——
%= 取模后赋值运算符 变量 %= 表达式 ——
+= 加后赋值运算符 变量 += 表达式 ——
-= 减后赋值运算符 变量 -= 表达式 ——
<<= 左移后赋值运算符 变量 <<= 表达式 ——
>>= 右移后赋值运算符 变量 >>= 表达式 ——
&= 按位与后赋值运算符 变量 &= 表达式 ——
^= 按位异或后赋值运算符 变量 ^= 表达式 ——
|= 按位或后赋值运算符 变量 |= 表达式 ——
15 , 逗号运算符 表达式1, 表达式2, …, 表达式n 左→右 ——
【注意】:
  • 优先级数字越小,优先级越高。
  • 同一优先级的运算符,运算次序由结合方向决定
  • 通常情况下取模运算( mod )和求余( rem )运算的目标结果相同,在大多数的编程语言里,都用 % 符号表示取模或者求余运算。不过还是要十分注意当前环境下 % 运算符的具体意义,因为在有负数存在的情况下,两者的结果可能是不一样的。

四、位逻辑运算

1. 与运算

ABA & B
111
100
010
000

2. 或运算

ABA | B
111
101
011
000

3. 非运算

A!A
10
01

4. 异或运算

ABA ^ B
110
101
011
000
【**相同为0,不同为1**】

5. 同或运算

AB!(A ^ B)
111
100
010
001
【**相同为1,不同为0**】

6. 逻辑运算应用

6.1 位清 0

有的时候我们需要将一个数据的某一位清 0 而又不希望影响其他位的数据,我们可以这样来写:

1
a = a & (~(1 << n));  /* 将 a 的第 n 位清 0 ,其他位保持不变,n 从 0 开始计算 */

【语句分析】假设 a 为 32 位数据, n 为第 3 位,目的是将第 3 位清 0 ,而不影响其他位:

1
2
3
4
5
6
7
n               : 3
(1 << n) : 00000000 00000000 00000000 00001000

(~(1 << n)) : 11111111 11111111 11111111 11110111
a : 11111111 11111111 11111111 11111111

a & (~(1 << n)) : 11111111 11111111 11111111 11110111
image-20220731101731680

6.2 位置 1

有的时候我们需要将一个数据的某一位置 1 而又不希望影响其他位的数据,我们可以这样来写:

1
a = a | (1 << n);  /* 将 a 的第 n 位置 1 ,其他位保持不变, n 从 0 开始计算 */

【语句分析】假设 a 为 32 位数据, n 为第 3 位,目的是将第 3 位置 1 ,而不影响其他位:

1
2
3
4
5
6
n            : 3 
(1 << n) : 00000000 00000000 00000000 00001000

a : 00000000 00000000 00000000 00000000

a | (1 << n) : 00000000 00000000 00000000 00001000
image-20220731101932124

6.3 应用实例

【问题】unsigned int a; 将a的第[7:4]位置为0101,其他位保持不变。

【分析过程】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
******** ******** ******** ********
******** ******** ******** 0101****
unsigned int a : 11111111 00000000 00000000 10101111
/* 1.先清零 */
0xF : 00000000 00000000 00000000 00001111
(0xF << 4) : 00000000 00000000 00000000 11110000
~(0xF << 4) : 11111111 11111111 11111111 00001111
a : 11111111 00000000 00000000 10101111
a & (~(0xF << 4)) : 11111111 00000000 00000000 00001111

/* 2.再置位 */
0x5 : 00000000 00000000 00000000 00000101
(0x5 << 4) : 00000000 00000000 00000000 01010000
a : 11111111 00000000 00000000 00001111
a | (0x5 << 4) : 11111111 00000000 00000000 01011111

=> a = a & (~(0xF << 4)) | (0x5 << 4);
image-20220731103108642