c语言运算符

运算符

C 给我们提供了各种各样的运算符,我们可以用来操作数据。

特别地,我们可以识别不同分组的运算符:

  • 算术运算符
  • 比较运算符
  • 逻辑运算符
  • 复合赋值运算符
  • 位运算符
  • 指针运算符
  • 结构运算符
  • 混合运算符

在这一节中,我们将用两个假想的变量 a 和 b 举例,详细介绍所有这些运算符。

为了简单起见,我将不会介绍位运算符、结构运算符和指针运算符。

算术运算符

我将把这个小型分组分为二元运算符和一元运算符。

二元操作符需要两个操作数:

操作符名字示例
= 赋值 a = b
+ a + b
- a - b
* a * b
/ a / b
% 取模 a % b

一元运算符只需要一个操作数:

运算符名字示例
+ 一元加 +a
- 一元减 -a
++ 自增 a++ or ++a
-- 自减 a-- or --a

a++ 与 ++a 的区别在于:a++ 在使用 a 之后才自增它的值,而 ++a 会在使用 a 之前自增它的值。

例如:

int a = 2;
int b;
b = a++ /* b 为 2,a 为 3 */
b = ++a /* b 为 4,a 为 4 */

这也适用于递减运算符。

比较运算符

运算符名字示例
== 相等 a == b
!= 不相等 a != b
> 大于 a > b
< 小于 a < b
>= 大于等于 a >= b
<= 小于等于 a <= b

逻辑运算符

  • ! 非(例如:!a
  • && 与(例如:a && b
  • || 或(例如:a || b

这些运算符在使用布尔值时非常有用。

复合赋值运算符

当赋值与算术运算同时进行时,这些运算符非常有用。

运算符名字示例
+= 加且赋值 a += b
-= 减且赋值 a -= b
*= 乘且赋值 a *= b
/= 除且赋值 a /= b
%= 求模且赋值 a %= b

三目运算符

三目运算符是 C 中唯一一个使用三个操作数的运算符,并且它是表达条件的简便方法。

它看起来长这样:

<条件> ? <表达式> : <表达式>

示例:

若 a 的值为 true,就执行语句 b,否则执行语句 c

三目运算符的功能与 if/else 条件语句相同,但是它更短,还可以被内联进表达式。

sizeof 操作符

sizeof 运算符返回你传入的操作数的大小,获取某个数据类型的长度可以使用 sizeof 操作符。你可以传入变量,或者甚至是类型也可以。

使用示例:

#include <stdio.h>

int main(void) {
int age = 37;
short a = 10;
int b = 100;
int short_length = sizeof a;
int int_length = sizeof(b);
int long_length = sizeof(long);
int longlong_length = sizeof(long long);
printf("%ld\n", sizeof(age)); printf("%ld", sizeof(int));
printf("short=%d, int=%d, long=%d, longlong=%d\n",
      short_length, int_length, long_length, longlong_length);
return 0;
}

sizeof 操作符

获取某个数据类型的长度可以使用 sizeof 操作符,如下所示:

在 32 位环境以及 Win64 环境下的运行结果为:

short=2, int=4, long=4, longlong=8

在 64 位 Linux 和 macOS 下的运行结果为:

short=2, int=4, long=8, longlong=8

sizeof 用来获取某个数据类型或变量所占用的字节数,如果后面跟的是变量名称,那么可以省略( ),如果跟的是数据类型,就必须带上( )

需要注意的是,sizeof 是C语言中的操作符,不是函数,所以可以不带( ),后面会详细讲解。

运算符优先级

对于所有的这些运算符(以及我们还没有在本文中介绍的其它运算符,包括位运算符、结构运算符和指针运算符),我们在单个表达式中一起使用它们时必须要留意。

假如我们有这个运算:

int a = 2;
int b = 4;
int c = b + a * a / b - a;

c 的值是多少?我们在执行乘和除之前有进行加法操作吗?

这里是给我们解惑的一组规则。

按照顺序,优先级从低到高:

  • 赋值运算符 =
  • 二元运算符 + 和 -
  • 运算符 * 和 /
  • 一元运算符 + 和 -

运算符还具有关联规则,除了一元运算符和赋值运算符之外,该规则总是从左到右的。

在:表达式 int c = b + a * a / b - a 中,我们首先执行 a * a / b,由于是从左到右的,我们可以拆分为 a * a ,其结果 /b2 * 2 = 44 / 4 = 1

然后我们可以进行加法操作和减法操作:4 + 1 - 2。c 的值是 3

然而,在所有的示例中,我都想确保你意识到你可以使用括号让任何相似的表达式更易读和易理解。

括号的优先级比其它任何运算符都要高。

上述示例表达式可以被重写为:

int c = b + ((a * a) / b) - a;

并且我们不必考虑太多。

条件语句

任何编程语言都给程序员提供了进行选择的能力。

我们想要在一些情况下进行 X,而在其它情况下进行 Y。

我们想检查数据,根据数据的状态做选择。

C 给我们提供了两种方式。

第一种方式是带 else 的 if 语句,第二种是 switch 语句。

if

在 if 语句中,你可以在检查到条件为 true 的时候,执行花括号内的代码块:

int a = 1;
if (a == 1) {
  /* 进行一些操作 */
}

如果原始条件的结果是 false,你可以追加一个 else 块以不同的代码块:

int a = 1;
if (a == 2) {
  /* 进行一些操作 */
} else {
  /* 进行另一些操作 */
}

谨防一种常见的缺陷源——总是在比较中使用比较运算符 ==,而不是赋值运算符 =。如果你不这么做,除非参数为 0,否则 if 条件检查的结果将一直都是 true。例如,如果你这么做:

int a = 0;
if (a = 0) {
  /* 永远都不会被调用 */
}

为什么会这样呢?因为条件检查会寻找一个布尔类型的结果(比较的结果),数字 0 总是等于 false。其它的任何东西都是 true,包括负数。

通过将多个 if 语句堆叠在一起,你可以有多个 else 块:

int a = 1;
if (a == 2) {
  /* do something */
} else if (a == 1) {
  /* 进行一些操作 */
} else {
  /* 进行另一些操作 */
}

switch

如果你的检查需要使用非常多的 if/else/if 块,可能是因为你需要检查变量的具体值,这时 switch 语句对你来说就非常有用了。

你可以提供一个变量作为条件,然后为期望的每个值使用一个 case 入口点:

int a = 1;
switch (a) {
  case 0:
    /* 进行一些操作 */
    break;
  case 1:
    /* 进行另一些操作 */
    break;
  case 2:
    /* 进行另一些操作 */
    break;

当前一个 case 执行完后,为了避免下一个 case 被执行,我们需要在每个 case 的末尾使用 break 关键字。这种“级联”效果在某些创造性方法中非常有用的。

你可以在末尾添加一个“捕获所有的” case,名为 default

int a = 1;
switch (a) {
  case 0:
    /* 进行一些操作 */
    break;
  case 1:
    /* 进行另一些操作 */
    break;
  case 2:
    /* 进行另一些操作 */
    break;
  default:
    /* 处理所有其它的情况 */
    break;
}

循环

C 给我们提供了三种循环:For 循环、while 循环 和 do while 循环。它们都允许你在数组上进行迭代,但又各有不同。咱们仔细来看一看它们。

For 循环

第一种执行循环是 for 循环,它可能也是最常见的循环。

使用 for 关键字时,我们可以先定义循环的 规则,然后提供反复执行的那个代码块。

就像这样:

for (int i = 0; i <= 10; i++) {
  /* 反复执行的指令 */
}

(int i = 0; i <= 10; i++) 代码块包含与循环细节有关的三个部分:

  • 初始条件(int i = 0
  • 测试(i <= 10
  • 增长(i++

我们首先定义循环变量,本示例中为 ii 是循环中的一个常用变量名,j 是嵌套循环(循环内的循环)内使用的变量名。这只是一个惯例。

变量 i 的值被初始化为 0,并且第一次迭代执行完毕。然后 i 像增长部分(这个示例中是 i++,递增 1)所说的那样增长,并且所有的循环会一直重复,直到 i 的值达到数字 10。

在循环的主代码块内,我们可以访问变量 i,从而获知我们当前所处的是哪个迭代。这个程序应该打印 0 1 2 3 4 5 5 6 7 8 9 10

for (int i = 0; i <= 10; i++) {
  /* 反复执行的指令 */
  printf("%u ", i);
}

所以for 循环的执行规程为先执行初始条件,再执行测试部分,然后是反复执行的指令,最后执行增长自加部分。

循环可以从较高的数字开始,往较低的数字逼近,就像这样:

for (int i = 10; i > 0; i--) {
  /* 反复执行的指令 */
}

你也可以让循环变量的增量为 2 或者其它值:

for (int i = 0; i < 1000; i = i + 30) {
  /* 反复执行的指令 */
}

while 循环

while 循环 写起来比 for 循环要简单,因为它需要你在自己的部分做更多的事情。

使用 while 时,你只需要检查条件,而不用在循环开始时预先定义所有的循环数据(就像你在 for 循环中做的那样):

while (i < 10) {

}

这段代码假定 i 已经定义并且用某个值进行了初始化。

除非你在循环内的某些地方增加变量 i 的值,否则这个循环会变成一个 无限循环。无限循环非常糟糕,因为它会阻塞程序,从而使其它任何事情都不会发生。

对于一个“正确的” while 循环,这是你需要知道的:

int i = 0;
while (i < 10) {
  /* 做点事情 */

  i++;
}

其中有一个例外,我们将会在一分钟后看到它。在这之前,让我介绍下 do while

Do while 循环

while 循环非常棒,但是有些时候你可能需要做某件特定的事情:你总是想执行某个代码块,然后 可能 一直重复它。

这可以通过 do while 关键字来完成。它在某种程度上和 while 循环非常类似,但是会有些许不同:

int i = 0;
do {
  /* 做点事情 */
  i++;
} while (i < 10);

尽管条件检查在底部,但是包含注释 /* 做点事情 */ 的代码块总是会至少执行一次。

然后,只要 i 小于 10,我们都将会重复这个代码块。

使用 break 跳出循环

在所有的 C 循环内,不管循环的条件设置得如何,我们都有一种在某个时间立即跳出循环的方法。

这是通过 break 关键字来完成的。

这在很多情况下非常有用,你可能想检查某个变量的值,例如:

for (int i = 0; i <= 10; i++) {
  if (i == 4 && someVariable == 10) {
    break;
  }
}

对 while 循环(也适用于 do while 循环)来说,使用这种方式跳出循环非常有趣,因为我们可以创建一个看似无限的循环,不过我们可以在某个条件发生时结束这个循环。你可以在循环代码块里面定义它:

int i = 0;
while (1) {
  /* 做点事情 */

  i++;
  if (i == 10) break;
}

这种循环在 C 中非常普遍。

数组

数组是存储多个变量的变量。

在 C 中,数组中的每个值都必须有 相同的类型。这意味着你将会有 int 值组成的数组, double 值组成的数组,等等。

你可以像这样定义一个 int 型的数组:

int prices[5];

你必须总是声明数组的大小。C 没有提供开箱即用的动态数组(为此,你必须使用像链表这样的数据结构)。

你可以使用常量定义数组的大小:

const int SIZE = 5;
int prices[SIZE];

你可以在定义数组的时候进行初始化,就像这样:

int prices[5] = { 1, 2, 3, 4, 5 };

但是你也可以在定义数组之后为其赋值,用这种方式:

int prices[5];
prices[0] = 1;
prices[1] = 2;
prices[2] = 3;
prices[3] = 4;
prices[4] = 5;

或者使用循环,这更加实际:

int prices[5];

for (int i = 0; i < 5; i++) {
  prices[i] = i + 1;
}
prices[0]; /* 第一个数组项的值 */
prices[1]; /* 第二个数组项的值 */

数组的索引从 0 开始,所以一个有五个元素的数组,比如上面的 prices 数组,将会包含的数组项的范围为 prices[0] 到 prices[4]

有趣的是,C 数组中的所有元素都是顺序存放的,一个接一个。高级编程语言通常不会出现这种情况。

另一件有趣的事情是:数组的变量名,上述示例中的 prices,是一个指向数组中首个元素的 指针。因此,可以像普通指针一样使用数组。

稍后会介绍更多有关指针的内容。

字符串

在 C 中,字符串是一种特殊的数组:字符串是由 char 值组成的数组:

char name[7];

我在介绍 C 中的数据类型时介绍过 char 类型,但是简而言之,它通常用于存储 ASCII 表中的字母。

可以像初始化一个普通的数组那样初始化一个字符串:

char name[7] = { "F", "l", "a", "v", "i", "o" };

或者使用更加方便的字符串字面量(也被称为字符串常量),一组用双引号引起来的字符:

char name[7] = "Flavio";

你可以通过 printf() 打印字符串,使用 %s

printf("%s", name);

你有注意到“Flavio”是 6 个字符长,但是我定义了一个长度为 7 的数组吗?这是因为字符串中的最后一个字符必须是 0,它是字符串的终止符号,我们必须给它留个位置。

记住这个非常重要,尤其是当你操作字符串的时候。

说到操作字符串,C 提供了一个非常重要的标准库:string.h

这个库是必不可少的,因为它抽象了很多与字符串有关的底层细节,给我们提供了一组非常有用的函数。

你可以在程序中加载这个库,需要在文件顶部加上:

#include <string.h>

一旦你这么做了之后,你就可以访问函数:

  • strcpy():将一个字符串复制到另一个字符串
  • strcat():将一个字符串追加到另一个字符串
  • strcmp():比较两个字符串是否相等
  • strncmp():比较两个字符串的前 n 个字符
  • strlen():计算字符串的长度

还有很多很多其它的函数供你调用。

指针

在我看来,指针是 C 中最令人不解/最具挑战的部分。尤其当你是编程新手的时候,如果你是从像 Python 或 JavaScript 这样的高级语言来到 C 的,也会这样。

在这一节中,我想以最简单但又不模糊的方式介绍它们。

指针是某个内存块的地址,这个内存块包含一个变量。

当你像这样声明一个整数时:

int age = 37;

我们可以使用 & 运算符获取内存中该变量的地址值:

printf("%p", &age); /* 0x7ffeef7dcb9c */

我在 printf() 内声明 %p 格式来打印地址值。

我们可以将该地址赋给一个变量:

int address = &age;

当在声明中使用 int *address 时,我们并没有在声明一个整数值,而是在声明一个 指向一个整数的指针。

我们可以使用指针运算符获取该地址指向的变量的值:

int age = 37;
int *address = &age;
printf("%u", *address); /* 37 */

我们又一次使用指针运算符,但是由于这次它不是一个声明,所以它表示“该指针指向的变量的值”。

在这个示例中,我们声明了一个 age 变量,但是我们使用了一个指针来初始化它的值:

int age;
int *address = &age;
*address = 37;
printf("%u", *address);

在使用 C 时,你会发现很多东西都建立在这个简单的概念之上。所以自己运行一下上面的示例,确保你对它有所熟悉。

指针是一个非常好的机会,因为它们迫使我们考虑内存地址以及数据是如何组织的。

数组就是一个例子。当你声明一个数组时:

int prices[3] = { 5, 4, 3 };

prices 变量实际上是一个指向数组首个元素的指针。在这种情况下,你可以使用这个 printf() 函数获取第一个数组元素的值:

printf("%u", *prices); /* 5 */

我们可以通过给 prices 指针加一来获取第二个元素,这是一件非常酷的事情:

printf("%u",`_ `(prices + 1)); /* 4 */

这种做法对于所有的其它值也适用。

我们还可以进行很多非常美妙的字符串操作,因为字符串的底层就是数组。

我们还有很多其它的使用场景,包括传递对象或函数的引用,从而避免消耗更多的资源来进行复制。

函数

我们通过函数将代码组织成子例程,这样就可以:

  1. 给它一个名字
  2. 在需要它们的时候进行调用

从你的第一个程序(“Hello, World!”)开始,你就在使用 C 函数了:

#include <stdio.h>

int main(void) {
    printf("Hello, World!");
}

main() 函数是一个非常重要的函数,它是 C 程序的入口点。

这是另一个函数:

void doSomething(int value) {
    printf("%u", value);
}

函数有 4 个重要的方面:

  1. 它们有一个名字,所以我们可以在之后调用它们
  2. 它们声明一个返回值
  3. 它们可以有参数
  4. 它们有一个函数体,用花括号包裹

函数体是一组指令,任何时候,只要函数被调用,这组指令就会被执行。

如果函数没有返回值,你可以在函数名前面使用关键字 void。否则你就要声明该函数的返回值类型(整数为 int,浮点数为 float,字符串为 const char *,等等)。

函数返回值的数量不能超过一个。

函数可以有参数。它们是可选的。如果函数没有参数,我们就在括号内插入 void,就像这样:

void doSomething(void) {
  /* ... */
}

在这种情况下,当我们调用该函数时,括号内没有任何东西:

doSomething();

如果有一个参数,我们就声明该参数的类型和名字,就像这样:

void doSomething(int value) {
   /* ... */
}

当我们调用该函数时,我们会在括号内传递对应的参数,就像这样:

doSomething(3);

我们可以有多个参数,为此我们使用逗号对它们进行分隔,在声明和调用时都是这样:

void doSomething(int value1, int value2) {
   /* ... */
}

doSomething(3, 4);

参数是通过 拷贝 传递的。这意味着如果你修改 value1,它的值是在局部作用域内修改的。函数外的那个值,即我们在调用时传入的值,并不会改变。

如果你传入的参数为一个 指针,你可以修改该变量的值,因为你现在可以使用它的内存地址直接访问它。

你不能为参数定义默认值。C++ 是可以的(Arduino Language 程序也可以),但是 C 不行。

确保你在调用函数之前定义了该函数,否则编译器将会给出一个警告和一个错误:

➜  ~ gcc hello.c -o hello; ./hello
hello.c:13:3: warning: implicit declaration of
      function 'doSomething' is invalid in C99
      [-Wimplicit-function-declaration]
  doSomething(3, 4);
  ^
hello.c:17:6: error: conflicting types for
      'doSomething'
void doSomething(int value1, char value2) {
     ^
hello.c:13:3: note: previous implicit declaration
      is here
  doSomething(3, 4);
  ^
1 warning and 1 error generated.

你收到的警告与顺序有关,我之前有提到过这个。

错误与另一件事情有关。因为 C 没有在调用函数之前没有“看到”该函数的声明,所以它必须进行假设。并且,它假设该函数返回 int。然而该函数返回的是 void,因此出现了错误。

如果你将该函数的定义修改为:

int doSomething(int value1, int value2) {
  printf("%d %d\n", value1, value2);
  return 1;
}

你就只会得到警告,错误消失了:

➜  ~ gcc hello.c -o hello; ./hello
hello.c:14:3: warning: implicit declaration of
      function 'doSomething' is invalid in C99
      [-Wimplicit-function-declaration]
  doSomething(3, 4);
  ^
1 warning generated.

不管是何种情况,确保你在使用函数之前声明了它。要么将函数上移,要么在头文件中加入该函数的原型。

在函数内部,你可以声明变量:

void doSomething(int value) {
  int doubleValue = value * 2;
}

变量在调用该函数的那一刻创建,并且在函数退出的时候销毁。它对函数外面来说是不可见的。

在函数内部,你可以调用函数自己。这被称为 递归,它提供了特有的机会。

输入与输出

C 是一门小型语言,并且 C 的“内核”并不包含任何输入/输出(I/O)功能。

当然,这并不是 C 所独有的。语言内核与 I/O 无关是很常见的。

在 C 中,输入/输出由 C 的标准库通过一组定义在 stdio.h 头文件中的函数向我们提供。

你可以在 C 文件顶部使用:

#include <stdio.h>

导入这个库。

这个库给我们提供了很多其它的函数:

  • printf()
  • scanf()
  • sscanf()
  • fgets()
  • fprintf()

在描述这个函数干啥之前,我想先花一分钟讲一下 I/O 流。

在 C 中,我们有三种类型的 I/O 流:

  • stdin(标准输入)
  • stdout(标准输出)
  • stderr(标准错误)

借助 I/O 函数,我们始终可以和流一起工作。流是一个高级接口,可以代表一个设备或文件。从 C 的角度来看,我们在从文件读取和命令行读取没有任何差异:不论如何,它都是一个 I/O 流。

那是我们需要牢记的一件事情。

某些函数是为与特定的流一起工作而设计的,就像 printf()一样,我们用它来将字符串打印到 stdout。使用它更加通用的版本 fprintf() 时,我们可以指定我们要写到的流。

由于我最开始谈论的是 printf(),咱们现在就介绍它吧。

printf() 是你在学习 C 编程时最先使用的函数之一。

在它最简单的使用形式中,你给它传递一个字符串字面量:

printf("hey!");

并且程序会将该字符串的内容打印到屏幕上。

你可以打印一个变量的值。但是这有点棘手,因为你需要添加一个特殊的字符,一个占位符,它会根据变量的类型变化。例如,我们为有符号十进制整数使用 %d

int age = 37;

printf("My age is %d", age);

通过使用逗号,我现在可以打印多个变量:

int age_yesterday = 37;
int age_today = 36;
printf("Yesterday my age was %d and today is %d", age_yesterday, age_today);

还有其它像 %d 一样的格式指示符:

  • %c 用于字符
  • %s 用于字符串
  • %f 用于浮点数
  • %p 用于指针

还有很多。

我们可以在 printf() 中使用转义字符,比如 \n 可以用来让输出创建一个新行。

scanf()

printf() 被用作输出函数。我现在想介绍一个输入函数,这样我们就能完成所有的 I/O 操作:scanf()

这个函数被用来从用户运行的程序,从命令行获取一个值。

我们必须先定义一个变量,它将被用来存放我们从输入中获取的值:

int age;

然后我们调用 scanf(),传入两个参数:变量的格式(类型),和变量的地址:

scanf("%d", &age);

如果我们想在输入时获取一个字符串,还记得字符串名是一个指向第一个字符的指针,所以你不需要在它前面加上 &

char name[20];
scanf("%s", name);

这里是一个小程序,它同时使用了 printf() 和 scanf()

#include <stdio.h>
int main(void) {
  char name[20];
  printf("Enter your name: ");
  scanf("%s", name);
  printf("you entered %s", name);
}

变量作用域

当你在 C 程序中定义一个变量时,根据你声明它的位置,它会有一个不同的 作用域(scope)。

这意味着它将会在某些地方可用,而在其它地方不可用。

该位置决定了两种类型的变量:

  • 全局变量(global variables)
  • 局部变量(local variables)

这就是区别:在函数内部声明的变量就是局部变量,比如这个:

int main(void) {
  int age = 37;
}

局部变量只有在函数内才能访问,它们会在函数结束后不复存在。它们会被从内存中清除掉(有一些例外)。

定义在函数外部的变量就是全局变量,比如这个示例:

int age = 37;

int main(void) {
  /* ... */
}

全局变量可以从程序中的任何一个函数访问,它们在整个程序的执行过程中都是可用的,直到程序结束。

我提到过局部变量在函数结束之后就不再可用。

原因是局部变量默认是在 栈(stack) 上声明的,除非你使用指针在堆中显式地分配它们。但是这样一来,你就不得不自己管理内存了。

静态变量

在函数内部,你可以使用 static 关键字初始化一个 静态变量(static variable)。

我说了“在函数内部”,因为全局变量默认就是静态的,所以没有必要再添加这个关键字。

什么是静态变量?静态变量在没有声明初始值的时候会被初始化为 0,并且它会在函数调用中保持该值。

考虑这个函数:

int incrementAge() {
  int age = 0;
  age++;
  return age;
}

如果我们调用一次 incrementAge(),我们将会得到返回值 1。如果我们再调用一次,我们总是会得到 1,因为 age 是一个局部变量并且在每次调用该函数的时候都会被重新初始化为 0

如果我们将该函数改为:

int incrementAge() {
  static int age = 0;
  age++;
  return age;
}

现在我们每调用一次这个函数,我们就会得到一个增加了的值:

printf("%d\n", incrementAge());
printf("%d\n", incrementAge());
printf("%d\n", incrementAge());

将会给我们:

1
2
3

我们也可以在 static int age = 0; 中省略初始化 age 为 0 的代码,只写 static int age;,因为静态变量在创建时会自动设置为 0。

我们也可以有静态数组。这时,每一个数组元素都被初始化为 0:

int incrementAge() {
  static int ages[3];
  ages[0]++;
  return ages[0];
}

全局变量

在这一节中,我想多谈论一点 全局变量与局部变量 之间的差异。

局部变量 被定义在函数内部,只在该函数内可用。

就像这样:

#include <stdio.h>

int main(void) {
  char j = 0;
  j += 10;
  printf("%u", j); //10
}

j 在 main 函数之外的任何地方都不可用。

全局变量 定义在所有函数的外部,就像这样:

#include <stdio.h>

char i = 0;

int main(void) {
  i += 10;
  printf("%u", i); //10
}

全局变量可以被程序内的任何函数访问。该访问并不只局限于读取全局变量的值:任何函数都可以更新全局变量的值。

因此,全局变量是一种在函数间共享相同数据的一种方式。

局部变量的主要不同在于,分配给局部变量的内存会在函数结束之后立即释放。

全局变量只在程序结束时才会释放。

类型定义

C 中的 typedef 关键字允许你定义新的类型。

我们可以从 C 内置的类型开始创建自己的类型,使用这个语法:

typedef existingtype NEWTYPE

按照惯例,我们创建的新类型通常是大写的。

这样可以更加容易区分它,并且可以立即识别出它是一种类型。

例如,我们可以定义一个新的 NUMBER 类型,它还是 int

typedef int NUMBER

一旦你这么做了之后,你就可以定义新的 NUMBER 变量了:

NUMBER one = 1;

现在你可能会问:为什么?为什么不直接使用内置的 int 类型呢?

嗯,当两个东西搭配在一起的时候,typedef 会变得真的很有用:枚举类型和结构体。

枚举类型

使用 typedef 和 enum 关键字,我们可以定义具有指定值的类型。

这是 typedef 关键字最重要的使用场景之一。

这是枚举类型的语法:

typedef enum {
  //值……
}

按照惯例,我们创建的枚举类通常是大写的。

这里是一个简单的示例:

typedef enum {
  true,
  false
} BOOLEAN;

C 自带 bool 类型,所以这个示例并不实用,但是它会让你领悟到其中的精髓。

另一个示例是定义一周中的那几个日子:

typedef enum {
  monday,  
  tuesday,
  wednesday,
  thursday,
  friday,
  saturday,
  sunday
} WEEKDAY;

这里是使用这个枚举类的一个简单程序:

#include <stdio.h>

typedef enum {
  monday,  
  tuesday,
  wednesday,
  thursday,
  friday,
  saturday,
  sunday
} WEEKDAY;

int main(void) {
  WEEKDAY day = monday;

  if (day == monday) {
    printf("It's monday!"); 
  } else {
    printf("It's not monday"); 
  }
}

枚举定义中的每个枚举项在内部都与一个整数配对。所以在这个示例中 monday 是 0,tuesday 是 1,以此类推。

这意味着对应的条件可以是 if (day == 0) 而不是 if (day == monday),但是对于我们人类来说,使用名字比数字更合理,所以它是一个非常便利的语法。

结构体

利用 struct 关键字,我们可以使用基本的 C 类型创建复杂的数据结构。

结构体是一组由不同类型的值组成的集合。C 中的数组被限制为一种类型,所以结构体在很多用例中会显得非常有趣。

这里是结构体的语法:

struct <structname> {
  //变量……
};

示例:

struct person {
  int age;
  char *name;
};

通过将变量添加到右花括号之后,分号之前,你可以声明类型为该结构体的变量,就像这样:

struct person {
  int age;
  char *name;
} flavio;

或者多个变量也行,就像这样:

struct person {
  int age;
  char *name;
} flavio, people[20];

这次我声明一个名为 flavio 的 person 变量,以及一个具有 20 个 person 的名为 people 的数组。

我们也可以稍后再声明变量,使用这个语法:

struct person {
  int age;
  char *name;
};

struct person flavio;

我们可以在声明的时候初始化一个结构体:

struct person {
  int age;
  char *name;
};

struct person flavio = { 37, "Flavio" };

一旦定义了结构体,我们就可以使用一个点(.)来访问它里面的值了:

struct person {
  int age;
  char *name;
};

struct person flavio = { 37, "Flavio" };
printf("%s, age %u", flavio.name, flavio.age);

我们也可以使用点语法改变结构体中的值:

struct person {
  int age;
  char *name;
};

struct person flavio = { 37, "Flavio" };

flavio.age = 38;

结构体非常有用,因为它们既可以作为函数的参数,也可以作为函数的返回值,以及它们内部的嵌入变量。每个变量都有一个标签。

注意到结构体是 复制传递 的,这一点很重要,除非,当然你可以传递一个指向结构体的指针,这种情况下它就是引用传递。

使用 typedef,我们可以简化处理结构体时的代码。

咱们看一个示例:

typedef struct {
  int age;
  char *name;
} PERSON;

按照惯例,我们使用 typedef 创建的结构体通常是大写的。

现在,我们可以像这样声明一个新的 PERSON 变量:

PERSON flavio;

并且我们可以用这种方式在声明的时候初始化它们:

PERSON flavio = { 37, "Flavio" };
posted @ 2024-12-03 16:31  luckylan  阅读(177)  评论(0)    收藏  举报