C语言学习教程(二):本系列教程第6-9章。
6-C Variables
变量(Variables)
只不过是我们的程序可以操作的存储区域的名称。C语言中的每一个变量,都有一个特定的类型,这个类型决定了该变量在内存中的大小和布局、可以存储在该内存中的值的范围、以及可以应用于变量的一组操作。
变量的名称可以由字母、数字和下划线组成。变量必须以字母或下划线开头。大写和小写是不同的,因为C区分大小写。
基于上一章介绍的C数据类型,有以下几种基本变量类型:
Type | Description |
---|---|
char | 通常是1个字节,这是一个整数类型。 |
int | 通常是4个字节,这是一个整数类型。 |
float | 单精度浮点值。 |
double | 双精度浮点值。 |
void | 表示没有任何类型。 |
C 语言还允许定义各种其他类型的变量,我们将在后续章节中介绍,如枚举类型、指针类型、数组类型、结构体、联合体等。在本章中,我们只学习基本的变量类型。
(1)变量的定义
变量的定义(Variable Definition)
意味着要告诉编译器在什么位置为变量创建存储,以及如何创建变量的存储。
变量的定义需要指定一种数据类型
并且包含一个或多个变量的列表
。如下所示:
|
|
在这里,type
必须是合法的C数据类型,包括char、w_char、int、float、double、bool或任何用户定义的对象等。variable_list
可以由一个或多个由逗号分隔的标识符名称组成。
下面显示了一些有效的变量定义:
|
|
第二行int i, j, k;
声明并定义了变量i、j和k。这告诉编译器创建int类型的变量i、j和k。
变量可以在定义的时候被初始化(分配一个初始值)。初始化器由一个等号
和一个常量表达式
组成。如下所示:
|
|
下面是一些示例:
|
|
(2)变量的声明
变量的声明(Variable Declaration)
意味着要告诉编译器保证存在一个具有指定类型和名称的变量,以便编译器继续进行进一步编译,而无需有关该变量的完整详细信息。变量的声明
只有在编译时才有意义,编译器在链接程序时需要实际的变量声明。
为变量分配地址和存储空间的称为定义
,不分配地址的称为声明
。
变量定义
:用于为变量分配存储空间,还可以为变量指定初始值。程序中变量有且仅有一个定义。
变量声明
:用于向程序表明变量的类型和名字。
(1)定义也是声明,当定义变量的时候,我们也向程序声明了它的类型和名字。
(2)声明不是定义,可以通过extern关键字声明变量而不定义它。extern关键字声明的变量不会分配存储空间。
|
|
|
|
如果声明有初始化时,那么它可被当作是定义,此时声明也是定义了,即使声明标记为extern。
|
|
此句话虽然使用了extern,但是这条语句还是定义了pi,分配并初始化了存储空间。
从广义的角度来讲声明中包含着定义,即定义是声明的一个特例,所以并非所有的声明都是定义。例如:int number
它既是声明,又是定义。然而对于extern int number
来讲,它只是声明不是定义。
(3)Example
观察下面的示例,其中变量名在文件头部均已被声明(Declaration),但是定义和初始化都在 main 函数中。
|
|
运行结果:
$ gcc -o test test.c
$ ./test
value of c : 30
value of d : 23.333334
相同的概念适用于函数声明
,我们在声明时提供函数名称,然后可以在其他任何地方给出其实际的定义。
|
|
运行结果:
$ gcc -o test1 test1.c
$ ./test1
value of i is: 0
(4)Lvalues and Rvalues
我们在 C/C++ 编程中并不会经常用到左值(lvalue)
和右值(rvalue)
两个术语。然而一旦遇见,又常常不清楚它们的含义。最可能出现这两个术语的地方是在编译错误或警告的信息中。
例如,一个例子是使用gcc
编译以下代码时:
|
|
编译结果:
|
|
上面的错误信息中提到了左值(lvalue)。
另一个例子是当你用g++
编译以下代码时:
|
|
编译结果:
|
|
同样的,错误信息中提到了术语右值(rvalue)。
一个精简版的左值和右值的定义如下:
左值
:表示了一个占据内存中某个可识别的位置的对象(也就是一个地址)。左值既能够出现在等号的左边,也能出现在等号的右边。左值一般为可寻址的变量。
右值
:一个表达式不是左值就是右值,那么,右值就是一个不表示内存中某个可识别位置的对象的表达式。右值只能出现在等式的右边。右值一般为不可寻址的常量。
下面来看一下相关的示例:
|
|
赋值操作需要左操作数是一个左值。var
变量是一个有内存位置的对象,因此它是左值。
然而,下面的写法是错误的:
4 = var; //错误!
(var + 1) = 4; //错误!
常量4
和表达式(var + 1)
都不是左值(也就是说,它们都是右值),因为它们没有可识别的内存位置。也就是说,它们只存在于计算过程中的每个临时寄存器中,因此赋值给它们是没有任何意义的(我们赋值到了一个不存在的位置)。
如需了解更多的有关左值和右值的知识,请参考:https://nettee.github.io/posts/2018/Understanding-lvalues-and-rvalues-in-C-and-C/
7-C Constants and Literals
常量(Constants)
是指程序在执行期间不会发生改变的固定值。这些固定值也被称为字面量(Literals)
。
常量可以是任何基本数据类型,比如:整数常量、浮点常量、字符常量、字符串常量、枚举常量等。
(1)整数常量
整数常量(Integer literals)可以是十进制、八进制或十六进制常量。前缀指定基数:0x
或0X
表示十六进制、0
表示八进制、不带前缀则默认表示十进制。
整数常量也可以带一个后缀,它是U
和L
的组合,分别表示无符号
和长整数
。后缀可以是大写或小写,并且U和L可以是任意顺序的。
下面是整数常量的一些示例:
|
|
下面是比较常见的一些整数常量:
|
|
(2)浮点常量
浮点常量(Floating-point literals)由整数部分
、小数点
、小数部分
和指数部分
4个部分组成。浮点常量通常使用小数形式
或指数形式
来表示。
当浮点变量使用小数形式
来表示时,包含整数部分
、小数点
和小数部分
。该表示方法可以省略小数点前面的数字(整数部分)或者小数点后面的数字(小数部分),但不能同时省略。示例如下:
|
|
当浮点变量使用指数形式
来表示时,包含整数部分
、小数点
和指数部分
。带符号的指数是用 e 或 E 引入的。该表示方法可以省略小数点前面的数字(整数部分)。示例如下:
|
|
float(单精度浮点值)格式:1位符号,8位指数,23位小数。
double(双精度浮点值)格式:1位符号,11位指数,52位小数。
(3)字符常量
字符常量(Character constants)是括在单引号中的,例如:'x'
,字符常量可以存储在char
类型的简单变量中。
字符常量可以是一个普通的字符(例如'x'
)、一个转义字符(例如'\t'
)或一个通用的字符(例如'\u02C0'
)。
在 C 中,有一些特定的字符,当它们前面有反斜杠时,它们就具有特殊的含义。比如:换行符(\n
)或制表符(\t
)等。下表列出了一些这样的转义字符:
转义序列 | 含义 |
---|---|
\\ | \ 字符 |
\' | ’ 字符 |
\" | " 字符 |
? | ? 字符 |
\a | 警报铃声(Alert or bell) |
\b | 退格键(Backspace) |
\f | 换页符(Form feed) |
\n | 换行符(Newline) |
\r | 回车(Carriage return) |
\t | 水平制表符(Horizontal tab) |
\v | 垂直制表符(Vertical tab) |
\ooo | 一到三位的八进制数 |
\xhh . . . | 一个或多个数字的十六进制数 |
下面是转义字符的简单示例:
|
|
运行结果:
|
|
(4)字符串常量
字符串常量(String literals)是括在双引号中的。字符串常量包含类似于字符常量的字符:普通的字符、转义字符和通用的字符。
下面是字符串常量的示例。
|
|
上面这三种形式所输出的字符串是相同的,运行代码如下:
|
|
运行结果:
|
|
(5)常量的定义
在C中,有两种简单的方式定义常量:
- 使用
#define
预处理器 - 使用
const
关键字
下面是使用#define
预处理器定义常量的格式:
|
|
具体示例如下:
|
|
程序运行结果:
|
|
下面是使用const
关键字定义常量的格式:
|
|
具体示例如下:
|
|
程序运行结果:
$ gcc -o test4 test4.c
$ ./test4
value of area: 50
注意:用大写字母定义常量是一个很好的编程习惯。
8-C Storage Classes
存储类(Storage Classes)定义C程序中变量/函数的范围(可见性)和生命周期。
这些存储类说明符放置在它们所修饰的类型之前。下面是C程序中可用的存储类:
- auto
- register
- static
- extern
(1)auto存储类
auto
存储类是所有局部变量默认的存储类。
下面的示例定义了两个带有相同存储类的变量。auto只能用在函数内,即auto只能修饰局部变量。
|
|
(2)register存储类
register
存储类用于定义存储在寄存器中而不是内存中的局部变量。这意味着变量的最大尺寸等于寄存器的大,而且不能对它使用一元运算符"&"(因为它没有内存位置)。
|
|
寄存器只能用于需要快速访问的变量,比如计数器。
还需要注意的是,定义register
存储类并不意味着变量一定会被存储在寄存器中,它意味着可能存储在寄存器中,这取决于硬件和实现的限制。
(3)static存储类
static
存储类指示编译器在程序的生命周期内始终保持该局部变量的存在,而不是在每次进入和离开作用域时创建和销毁它。因此,使用static
修饰局部变量可以在函数调用之间保持局部变量的值。
static
存储类也可以用于修饰全局变量。当static
修饰全局变量时,会使变量的作用域限制在声明它的文件内。
在C编程中,当对类数据成员(class data member)使用static
修饰时,它只会导致该成员的一个副本被其类的所有对象共享。
下面是使用static
存储类的示例:
|
|
当前你可能看不懂这个例子,因为这里用到了函数和全局变量。目前这两个概念我们还没有讲解,即使你现在不能完全理解,也没有关系,后续的章节我们会详细讲解。上面的代码执行结果:
|
|
在下面的示例中,我们可以看到static
与auto
的区别:
|
|
运行结果:
|
|
static定义的变量在其作用域下是有记忆的。
(4)extern存储类
extern
存储类用于提供对所有程序文件可见的全局变量的引用。当使用extern
时,无法初始化变量,因为它所做的只是将变量名称指向先前定义的存储位置。
当有多个文件且定义了一个可以在其他文件中使用的全局变量或函数时,可以在其他文件中使用extern
来得到已定义的变量或函数的引用。可以这么理解,extern
是用来在另一个文件中声明一个全局变量或函数。
当有两个或多个文件共享相同的全局变量或函数时,最常使用extern
修饰符。示例如下:
第一个文件:main.c
|
|
第二个文件:write.c
|
|
运行结果:
|
|
9-C Operators
运算符(Operators)是一种符号,它告诉编译器执行特定的数学或逻辑操作。C语言内置了丰富的运算符,提供了以下几种运算符。
- 算数运算符(Arithmetic Operators)
- 关系运算符(Relational Operators)
- 逻辑运算符(Logical Operators)
- 位运算符(Bitwise Operators)
- 赋值运算符(Assignment Operators)
- 杂项运算符(Misc Operators)
本节会逐一介绍算数运算符、关系运算符、逻辑运算符、位运算符、赋值运算符和其他运算符。
(1)算数运算符
下面显示了C语言支持的所有算数运算符(Arithmetic Operators)。假设变量A的值为10,变量B的值为20,那么:
Operator | Description | Example |
---|---|---|
+ | 把两个操作数相加 | A + B 得到 30 |
- | 从第一个操作数中减去第二个操作数 | A - B 得到 -10 |
* | 把两个操作数相乘 | A * B 得到 200 |
/ | 分子除以分母 | B / A 得到 2 |
% | 取模运算符,整数除法后的余数 | B % A 得到 0 |
++ | 自增运算符,整数值增加 1 | A++ 得到 A 的值为11 |
– | 自减运算符,整数值减少 1 | A– 得到 A 的值为9 |
尝试以下示例来了解 C 编程语言中可用的所有算术运算符:
|
|
运行结果:
|
|
对于a++
和++a
而言,本质上都是a = a + 1
,区别是先赋值后运算还是先运算后赋值。
对于a--
和--a
而言,本质上都是a = a - 1
,区别是先赋值后运算还是先运算后赋值。
以下示例展示了a++
、++a
、a--
和--a
的区别。
|
|
运行结果:
|
|
(2)关系运算符
下表显示了C语言支持的所有关系运算符(Relational Operators)。假设变量A的值为10,变量B的值为20,那么:
Operator | Description | Example |
---|---|---|
== | 检查两个操作数的值是否相等,如果相等则条件为真。 | (A == B) 为假。 |
!= | 检查两个操作数的值是否相等,如果不相等则条件为真。 | (A != B) 为真。 |
> | 检查左操作数的值是否大于右操作数的值,如果是则条件为真。 | (A > B) 为假。 |
< | 检查左操作数的值是否小于右操作数的值,如果是则条件为真。 | (A < B) 为真。 |
>= | 检查左操作数的值是否大于或等于右操作数的值,如果是则条件为真。 | (A >= B) 为假。 |
<= | 检查左操作数的值是否小于或等于右操作数的值,如果是则条件为真。 | (A <= B) 为真。 |
尝试以下示例来了解 C 编程语言中可用的所有关系运算符:
|
|
运行结果:
|
|
(3)逻辑运算符
下表显示了C语言支持的所有逻辑运算符(Logical Operators)。假设变量A的值为10,变量B的值为0,那么:
Operator | Description | Example |
---|---|---|
&& | 逻辑与运算符。如果两个操作数都非零,则条件为真。 | (A && B) 为假。 |
|| | 逻辑或运算符。如果两个操作数中有任意一个非零,则条件为真。 | (A || B) 为真。 |
! | 逻辑非运算符。用来逆转操作数的逻辑状态。如果条件为真则逻辑非运算符将使其为假。如果条件为假则逻辑非运算符将使其为真。 | !(A && B) 为真。 |
尝试以下示例来了解 C 编程语言中可用的所有逻辑运算符:
|
|
运行结果:
|
|
(4)位运算符
位运算符(Bitwise Operators)作用于位,并逐位执行操作。
假设A=60,B=13,现在以二进制形式表示它们:
A = 0011,1100
B = 0000,1101
下表显示了 C 语言支持的位运算符。假设变量A的值为60,变量B的值为13,则:
Operator | Description | Example |
---|---|---|
& | 按位与运算符,按二进制位进行"与"运算。 | (A & B)将得到12,即为0000,1100 |
| | 按位或运算符,按二进制位进行"或"运算。 | (A | B)将得到61,即为0011,1101 |
^ | 按位异或运算符,按二进制位进行"异或"运算。 | (A ^ B)将得到49,即为0011,0001 |
~ | 按位取反运算符,按二进制位进行"取反"运算。 | (~A)将得到-61,即为1100,0011。 |
« | 按位左移运算符。将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补0)。 | A « 2将得到240,即为1111,0000 |
» | 按位右移运算符。将一个数的各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃。 | A » 2将得到15,即为0000,1111 |
按位与(&)运算规则:
0 & 0 = 0;
0 & 1 = 0;
1 & 0 = 0;
1 & 1 = 1;
只有两个操作数均为1时,结果才为1。否则为0。
按位或(|)运算规则:
0 | 0 = 0;
0 | 1 = 1;
1 | 0 = 1;
1 | 1 = 1;
只要有一个操作数为1,结果就为1。否则为0。
按位异或(^)运算规则:
0 ^ 0 = 0;
0 ^ 1 = 1;
1 ^ 0 = 1;
1 ^ 1 = 0;
相同的为0,不同的为1。
按位取反(~)运算规则:
~0 = 1;
~1 = 0;
注意这里的取反0和1为二进制位形式的0和1,不是整数0,1,2,3…
比如:~ 0001,0011 结果为:1110,1100。
按位左移(«)运算规则:
将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补0)。
A = 0011,1100
A << 2 = 1111,0000
按位左移(»)运算规则:
将一个数的各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃。
A = 0011,1100
A >> 2 = 0000,1111
&(按位与)
、|(按位或)
、^(异或)
和~(取反)
的真值表如下所示:
p | q | p & q | p | q | p ^ q | ~p |
---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 1 |
0 | 1 | 0 | 1 | 1 | 1 |
1 | 1 | 1 | 1 | 0 | 0 |
1 | 0 | 0 | 1 | 1 | 0 |
尝试以下示例来了解 C 编程语言中可用的所有位运算符:
|
|
运行结果:
|
|
(5)赋值运算符
下表显示了C语言支持的所有赋值运算符(Assignment Operators)。
Operator | Description | Example |
---|---|---|
= | 赋值运算符,把右边操作数的值赋给左边操作数。 | C = A 相当于 把 A 的值赋给 C |
+= | 加且赋值运算符,把右边操作数加上左边操作数的结果赋值给左边操作数。 | C += A 相当于 C = C + A |
-= | 减且赋值运算符,把左边操作数减去右边操作数的结果赋值给左边操作数。 | C -= A 相当于 C = C - A |
*= | 乘且赋值运算符,把左边操作数乘以右边操作数的结果赋值给左边操作数。 | C *= A 相当于 C = C * A |
/= | 除且赋值运算符,把左边操作数除以右边操作数的结果赋值给左边操作数。 | C /= A 相当于 C = C / A |
%= | 求模且赋值运算符,把左边操作数除以右边操作数的余数赋值给左边操作数。 | C %= A 相当于 C = C % A |
«= | 左移且赋值运算符 | C «= 2 等同于 C = C « 2 |
»= | 右移且赋值运算符 | C »= 2 等同于 C = C » 2 |
&= | 按位与且赋值运算符 | C &= 2 等同于 C = C & 2 |
^= | 按位异或且赋值运算符 | C ^= 2 等同于 C = C ^ 2 |
|= | 按位或且赋值运算符 | C |= 2 等同于 C = C | 2 |
尝试以下示例来了解 C 编程语言中可用的所有赋值运算符:
|
|
运行结果:
|
|
(6)杂项运算符
下表列出了 C 语言支持的其他一些重要的运算符,包括sizeof
、&
、*
和?:
。
Operator | Description | Example |
---|---|---|
sizeof() | 返回变量的大小。 | sizeof(a) 将返回4,其中a是整数类型。 |
& | 返回变量的地址。 | &a 将获得变量的实际内存地址。 |
* | 指向一个变量。 | *a 将指向一个变量。 |
? : | 条件表达式 | 如果条件为真(?),则值为X。如果为假(:),则值为Y。 |
(7)C中的运算符优先级
运算符的优先级确定表达式中项的组合。这会影响到一个表达式如何计算。某些运算符比其他运算符有更高的优先级,例如:乘法运算符的优先级高于加法运算符。
比如:x = 7 + 3 * 2
。在这里 x 被赋值为 13,而不是 20;因为运算符*
的优先级比+
高,所以首先计算乘法 3*2,最后再加上7。
下表将按优先级从高到低列出各个运算符。具有较高优先级的运算符出现在表格的上面,具有较低优先级的运算符出现在表格的下面。在表达式中,较高优先级的运算符会优先被计算。
类别 | 运算符 | 结合性 |
---|---|---|
后缀 | () [] -> . ++ -- | 从左到右 |
一元 | + - ! ~ ++ -- (type)* & sizeof | 从右到左 |
乘除 | * / % | 从左到右 |
加减 | + - | 从左到右 |
移位 | « » | 从左到右 |
关系 | < <= > >= | 从左到右 |
相等 | == != | 从左到右 |
位与 AND | & | 从左到右 |
位异或 XOR | ^ | 从左到右 |
位或 OR | | | 从左到右 |
逻辑与 AND | && | 从左到右 |
逻辑或 OR | || | 从左到右 |
条件 | ?: | 从右到左 |
赋值 | = += -= *= /= %= >>= «= &= ^= |= | 从右到左 |
逗号 | , | 从左到右 |
尝试以下示例来了解 C 编程语言中运算符的优先级:
|
|
运次结果:
|
|