第3章 数据类型、运算符和表达式

C 语言的数据类型

基本数据类型

C 语言的基本数据类型是构建其他数据结构的基础,包括整型、浮点型和字符型。以下是详细介绍:

整型

整型用于表示整数,包括有符号和无符号两种形式:

类型说明存储大小(典型)值范围存储方式
char字符型1 字节-128 到 127 或 0 到 255ASCII 编码
unsigned char无符号字符型1 字节0 到 255无符号二进制
signed char有符号字符型1 字节-128 到 127补码表示
short短整型2 字节-32768 到 32767补码表示
unsigned short无符号短整型2 字节0 到 65535无符号二进制
int整型4 字节-2147483648 到 2147483647补码表示
unsigned int无符号整型4 字节0 到 4294967295无符号二进制
long长整型4 或 8 字节-2147483648 到 2147483647 或更大补码表示
unsigned long无符号长整型4 或 8 字节0 到 4294967295 或更大无符号二进制
long long双长整型8 字节-9223372036854775808 到 9223372036854775807补码表示
unsigned long long无符号双长整型8 字节0 到 18446744073709551615无符号二进制

浮点型

浮点型用于表示带有小数部分的数值:

类型说明存储大小值范围精度
float单精度浮点型4 字节约 ±3.4e-38 到 ±3.4e+38约 7 位有效数字
double双精度浮点型8 字节约 ±1.7e-308 到 ±1.7e+308约 15-17 位有效数字
long double长双精度浮点型8、10 或 16 字节因编译器而异因编译器而异
浮点数的 IEEE 754 标准表示

浮点数在计算机中按照 IEEE 754 标准存储:

  • 单精度 (float):1 位符号位,8 位指数位,23 位尾数位
  • 双精度 (double):1 位符号位,11 位指数位,52 位尾数位

这种表示方法可以表示非常大或非常小的数值,但也会导致一些精度问题,例如:

1
2
3
4
float a = 0.1;
float b = 0.2;
float c = a + b;
// c 可能不等于 0.3,而是接近 0.3 的一个值

字符型

字符型用于表示单个字符:

  • ASCII 编码:标准 ASCII 码使用 7 位,表示 128 个字符
  • 扩展 ASCII:使用 8 位,表示 256 个字符
  • Unicode:可以表示更多字符,C11 标准支持 Unicode 字符

类型修饰符

C 语言提供了以下类型修饰符,用于修改基本数据类型的属性:

  • signed - 有符号类型,表示可以存储正数、负数和零(默认)
  • unsigned - 无符号类型,表示只能存储非负数
  • short - 短类型,通常使用较少的内存
  • long - 长类型,通常使用较多的内存

类型定义

使用 typedef 关键字可以为现有类型创建别名,提高代码的可读性和可维护性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 为基本类型创建别名
typedef unsigned int uint;
typedef long long int64;
typedef unsigned long long uint64;
typedef float float32;
typedef double float64;

// 使用别名定义变量
uint count = 100;
int64 large_number = 9223372036854775807LL;
float32 pi = 3.14159f;

// 为结构体创建别名
typedef struct {
int x;
int y;
} Point;

Point p = {10, 20};

派生类型

C 语言还支持以下派生类型:

  • 数组 - 相同类型元素的集合
  • 指针 - 存储内存地址的变量
  • 结构体 - 不同类型元素的集合
  • 联合体 - 可以存储不同类型值的特殊类型
  • 枚举 - 一组命名的整型常量
  • 函数 - 执行特定任务的代码块

变量和常量

变量

变量是存储数据的内存位置,具有名称、类型和值。

变量声明

变量声明告诉编译器变量的名称和类型,但不分配内存:

1
2
3
4
5
6
7
8
9
10
// 声明单个变量
int age;
float salary;
char grade;

// 声明多个同类型变量
int x, y, z;

// 声明外部变量
extern int global_var;

变量定义

变量定义不仅声明变量,还为其分配内存并可选地初始化:

1
2
3
4
5
6
7
// 定义并初始化变量
int count = 0;
float pi = 3.14159;
char initial = 'A';

// 定义多个同类型变量并初始化
int a = 1, b = 2, c = 3;

变量命名规则

  • 变量名只能包含字母、数字和下划线
  • 变量名不能以数字开头
  • 变量名区分大小写(如 countCount 是不同的变量)
  • 变量名不能是 C 语言的关键字
  • 变量名应该具有描述性,便于理解代码

变量的作用域

变量的作用域是指变量在程序中可访问的区域:

  • 局部变量:在函数或代码块内部定义,只在定义它的函数或代码块内可见
  • 全局变量:在所有函数外部定义,在整个程序中可见
  • 静态局部变量:在函数内部定义,使用 static 修饰,在函数调用之间保持值
  • 块作用域变量:在代码块内部定义,只在该代码块内可见
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 全局变量
int global_var = 100;

void function1() {
// 局部变量
int local_var = 10;

// 静态局部变量
static int static_var = 5;
static_var++;

printf("local_var: %d, static_var: %d, global_var: %d\n",
local_var, static_var, global_var);
}

void function2() {
// 块作用域变量
{ int block_var = 20;
printf("block_var: %d, global_var: %d\n", block_var, global_var);
}
// block_var 在此处不可见
printf("global_var: %d\n", global_var);
}

变量的生命周期

变量的生命周期是指变量存在的时间:

  • 自动变量:局部变量默认是自动变量,在进入作用域时创建,离开作用域时销毁
  • 静态变量:使用 static 修饰的变量,在程序开始时创建,程序结束时销毁
  • 全局变量:在程序开始时创建,程序结束时销毁
  • 动态分配的变量:使用 malloc() 等函数分配的变量,在显式释放前一直存在

常量

常量是固定值,在程序执行过程中不会改变。

字面常量

字面常量是直接出现在代码中的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 整数常量
123 // 十进制
0123 // 八进制(以 0 开头)
0x123 // 十六进制(以 0x 或 0X 开头)
123L // 长整型
123UL // 无符号长整型
123LL // 长long整型
123ULL // 无符号长long整型

// 浮点常量
3.14 // 双精度
3.14f // 单精度
3.14L // 长双精度
1.23e-4 // 科学计数法(1.23 × 10^-4)
1.23E+5 // 科学计数法(1.23 × 10^5)

// 字符常量
'A' // 单字符
'\n' // 转义字符(换行)
'\t' // 转义字符(制表符)
'\\' // 转义字符(反斜杠)
'\x41' // 十六进制转义字符(ASCII 码为 65 的字符,即 'A')
'\u0041' // Unicode 转义字符(C11 标准)

// 字符串常量
"Hello" // 字符串
"Line 1\nLine 2" // 包含转义字符的字符串
"" // 空字符串

符号常量

使用 #define 预处理指令定义符号常量:

1
2
3
4
5
6
7
8
9
10
11
12
// 定义数值常量
#define PI 3.14159
#define MAX_SIZE 100
#define MIN_AGE 18

// 定义字符串常量
#define MESSAGE "Hello, World!"
#define ERROR_MSG "An error occurred"

// 定义表达式常量
#define AREA(r) (PI * (r) * (r))
#define MAX(a, b) ((a) > (b) ? (a) : (b))

const 修饰符

使用 const 修饰符创建常量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 基本类型常量
const int MAX_AGE = 100;
const float PI = 3.14159;
const char INITIAL = 'A';

// 指针常量
const int* p1; // 指向常量的指针,不能通过 p1 修改所指向的值
int* const p2; // 常量指针,不能修改 p2 指向的地址
const int* const p3; // 指向常量的常量指针

// 数组常量
const int arr[] = {1, 2, 3, 4, 5};
// 不能修改 arr 中的元素

// 结构体常量
const struct {
int x;
int y;
} point = {10, 20};
// 不能修改 point 的成员

枚举常量

使用 enum 关键字定义枚举类型,枚举类型的成员是常量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 定义枚举类型
enum Weekday {
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY,
SUNDAY
};

// 使用枚举常量
enum Weekday today = WEDNESDAY;

// 自定义枚举值
enum Color {
RED = 1,
GREEN = 2,
BLUE = 4
};

// 使用自定义枚举值
enum Color my_color = GREEN;

常量的使用场景

  • 字面常量:直接在代码中使用的固定值
  • 符号常量:使用 #define 定义的常量,适用于在多个地方使用的值
  • const 常量:使用 const 修饰符定义的常量,具有类型检查
  • 枚举常量:使用 enum 定义的常量,适用于一组相关的常量值

运算符

算术运算符

算术运算符用于执行基本的数学运算:

运算符描述示例结果
+加法5 + 38
-减法5 - 32
*乘法5 * 315
/除法5 / 31(整数除法)
/除法5.0 / 3.01.666...(浮点数除法)
%取模(求余数)5 % 32
++自增(前缀)++aa+1,先自增后使用
++自增(后缀)a++a,先使用后自增
--自减(前缀)--aa-1,先自减后使用
--自减(后缀)a--a,先使用后自减

赋值运算符

赋值运算符用于将值赋给变量:

运算符描述示例等同于
=简单赋值a = ba = b
+=加法赋值a += ba = a + b
-=减法赋值a -= ba = a - b
*=乘法赋值a *= ba = a * b
/=除法赋值a /= ba = a / b
%=取模赋值a %= ba = a % b
<<=左移赋值a <<= ba = a << b
>>=右移赋值a >>= ba = a >> b
&=按位与赋值a &= ba = a & b
^=按位异或赋值a ^= ba = a ^ b
`=`按位或赋值`a

关系运算符

关系运算符用于比较两个值,返回布尔值(0 表示假,非 0 表示真):

运算符描述示例结果
==等于5 == 30(假)
!=不等于5 != 31(真)
<小于5 < 30(假)
>大于5 > 31(真)
<=小于等于5 <= 30(假)
>=大于等于5 >= 31(真)

逻辑运算符

逻辑运算符用于组合布尔值,返回布尔值:

运算符描述示例结果特点
&&逻辑与a && b只有当 a 和 b 都为真时才为真短路求值
``逻辑或`a
!逻辑非!a当 a 为假时为真,反之亦然一元运算符

短路求值

逻辑与和逻辑或运算符具有短路求值的特点:

  • 逻辑与:如果第一个操作数为假,则不会计算第二个操作数
  • 逻辑或:如果第一个操作数为真,则不会计算第二个操作数
1
2
3
4
5
6
7
8
9
10
11
12
13
int a = 0, b = 5;

// 逻辑与短路:a 为 0(假),所以不会计算 b++
if (a && b++) {
// 不会执行这里
}
// b 仍然是 5

// 逻辑或短路:a 为 0(假),所以会计算 b++
if (a || b++) {
// 会执行这里,因为 b++ 为 5(真)
}
// b 现在是 6

位运算符

位运算符用于操作整数的二进制位:

运算符描述示例二进制表示结果
&按位与5 & 3101 & 011001 (1)
``按位或`53`
^按位异或5 ^ 3101 ^ 011110 (6)
~按位取反~5~0000010111111010 (-6)
<<左移5 << 1101 << 11010 (10)
>>右移(有符号)5 >> 10101 >> 10010 (2)
>>右移(有符号)-5 >> 11011 >> 11101 (-3)
>>>右移(无符号,C++)5 >>> 10101 >>> 10010 (2)

条件运算符

条件运算符(三元运算符)是 C 语言中唯一的三元运算符:

1
condition ? expression1 : expression2

如果 condition 为真,则返回 expression1 的值,否则返回 expression2 的值。

1
2
3
4
5
6
7
int x = 10, y = 20;
int max = (x > y) ? x : y; // max = 20
int min = (x < y) ? x : y; // min = 10

// 嵌套条件运算符
int a = 5, b = 10, c = 15;
int largest = (a > b) ? ((a > c) ? a : c) : ((b > c) ? b : c);

逗号运算符

逗号运算符用于分隔表达式,从左到右计算,返回最后一个表达式的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 基本用法
int a, b, c;
a = (b = 10, c = 20, b + c); // a = 30, b = 10, c = 20

// 在 for 循环中使用
for (int i = 0, j = 10; i < 10; i++, j--) {
printf("i: %d, j: %d\n", i, j);
}

// 在函数参数中使用
void func(int x, int y) {
printf("x: %d, y: %d\n", x, y);
}

int main() {
int a = 5;
func(a++, a++); // 行为未定义,因为参数求值顺序不确定
return 0;
}

地址运算符

地址运算符用于获取变量的内存地址:

运算符描述示例结果
&取地址&a变量 a 的内存地址
*间接寻址(解引用)*p指针 p 指向的值

成员运算符

成员运算符用于访问结构体和联合体的成员:

运算符描述示例结果
.直接成员访问s.member结构体 s 的成员 member
->间接成员访问p->member指针 p 指向的结构体的成员 member

运算符优先级和结合性

运算符的优先级决定了表达式中运算的执行顺序,结合性决定了具有相同优先级的运算符的执行顺序:

优先级运算符结合性描述
1() [] -> .从左到右括号、数组下标、结构体成员
2! ~ ++ -- - + * & sizeof (type)从右到左逻辑非、按位取反、自增、自减、一元加减、间接寻址、取地址、 sizeof、类型转换
3* / %从左到右乘法、除法、取模
4+ -从左到右加法、减法
5<< >>从左到右左移、右移
6< <= > >=从左到右小于、小于等于、大于、大于等于
7== !=从左到右等于、不等于
8&从左到右按位与
9^从左到右按位异或
10``从左到右
11&&从左到右逻辑与
12``
13?:从右到左条件运算符
14= += -= *= /= %= <<= >>= &= ^= `=`从右到左
15,从左到右逗号运算符

表达式

表达式是由运算符和操作数组成的序列,计算后产生一个值。

表达式的类型

根据表达式的结果类型,表达式可以分为:

  • 算术表达式:结果为数值
  • 关系表达式:结果为布尔值
  • 逻辑表达式:结果为布尔值
  • 位表达式:结果为整数
  • 赋值表达式:结果为赋值后变量的值
  • 条件表达式:结果为两个表达式之一的值
  • 逗号表达式:结果为最后一个表达式的值
  • 函数调用表达式:结果为函数的返回值

表达式求值

表达式求值的过程遵循以下规则:

  1. 运算符优先级:高优先级的运算符先执行
  2. 运算符结合性:相同优先级的运算符按照结合性执行
  3. 括号:括号内的表达式先执行
  4. 副作用:表达式执行过程中可能产生副作用,如修改变量的值
  5. 序列点:表达式中某些点,确保之前的副作用已完成

副作用和序列点

副作用是指表达式执行过程中对状态的修改,如修改变量的值:

1
2
int a = 5;
int b = a++; // 副作用:a 的值增加 1

序列点是表达式执行过程中的某些点,确保之前的所有副作用已完成:

  • 分号(;)
  • 逗号运算符(,)
  • 逻辑与运算符(&&)的左操作数之后
  • 逻辑或运算符(||)的左操作数之后
  • 条件运算符(?:)的第一个操作数之后
  • 函数调用的所有参数求值之后,函数体执行之前

在序列点之间,多次修改同一个变量可能导致未定义的行为:

1
2
int a = 5;
int b = a++ + a++; // 未定义行为,因为在一个序列点之间多次修改 a

类型转换

隐式类型转换

当不同类型的操作数进行运算时,C 语言会自动进行类型转换,称为隐式类型转换。转换规则如下:

  1. 整数提升:char、short 类型会被提升为 int 类型
  2. 常用算术转换:当两个操作数类型不同时,会转换为共同的类型
    • 如果一个操作数是 long double,则另一个也转换为 long double
    • 否则,如果一个操作数是 double,则另一个也转换为 double
    • 否则,如果一个操作数是 float,则另一个也转换为 float
    • 否则,进行整数提升,然后:
      • 如果一个操作数是 unsigned long long,则另一个也转换为 unsigned long long
      • 否则,如果一个操作数是 long long,另一个是 unsigned long,则都转换为 unsigned long long
      • 否则,如果一个操作数是 long long,则另一个也转换为 long long
      • 否则,如果一个操作数是 unsigned long,则另一个也转换为 unsigned long
      • 否则,如果一个操作数是 long,另一个是 unsigned int,则都转换为 unsigned long 或 long(取决于大小)
      • 否则,如果一个操作数是 long,则另一个也转换为 long
      • 否则,都转换为 unsigned int
1
2
3
4
5
6
7
8
9
10
int a = 10;
float b = 3.14;
float c = a + b; // a 被转换为 float 类型

char ch = 'A';
int d = ch; // ch 被提升为 int 类型(整数提升)

unsigned int e = 100;
int f = -50;
unsigned int g = e + f; // f 被转换为 unsigned int 类型

显式类型转换(强制类型转换)

使用强制类型转换可以显式地将一个类型转换为另一个类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 基本类型转换
float a = 3.14;
int b = (int)a; // b = 3,截断小数部分

int c = 100;
float d = (float)c; // d = 100.0

// 指针类型转换
int x = 5;
void* p = (void*)&x; // 将 int* 转换为 void*
int* q = (int*)p; // 将 void* 转换为 int*

// 结构体类型转换(需要谨慎使用)
struct A {
int x;
};

struct B {
int x;
int y;
};

struct A a1 = {10};
struct B* b1 = (struct B*)&a1; // 危险的转换,可能导致未定义行为

表达式示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 算术表达式
int sum = 10 + 5 * 2; // sum = 20(乘法优先级高于加法)
float average = (float)sum / 3; // average = 6.666...(强制类型转换)

// 关系表达式
int x = 10, y = 20;
bool is_greater = x > y; // is_greater = false

// 逻辑表达式
bool has_money = true;
bool has_time = false;
bool can_shop = has_money && has_time; // can_shop = false

// 位表达式
unsigned int a = 0b1010; // 10
unsigned int b = 0b1100; // 12
unsigned int c = a & b; // c = 0b1000(8)
unsigned int d = a | b; // d = 0b1110(14)
unsigned int e = a ^ b; // e = 0b0110(6)
unsigned int f = a << 1; // f = 0b10100(20)

// 赋值表达式
int g = 5;
int h = (g += 3); // g = 8,h = 8

// 条件表达式
int max = (x > y) ? x : y; // max = 20

// 逗号表达式
int i, j;
i = (j = 10, j + 5); // j = 10,i = 15

// 函数调用表达式
int square(int n) {
return n * n;
}

int result = square(5); // result = 25

类型限定符

C 语言提供了以下类型限定符,用于限定变量的属性:

const 限定符

const 限定符表示变量的值不能修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 基本类型常量
const int MAX_VALUE = 1000;
// MAX_VALUE = 2000; // 错误:不能修改 const 变量

// 指针常量
const int* p1; // 指向常量的指针,不能通过 p1 修改所指向的值
int* const p2 = &x; // 常量指针,不能修改 p2 指向的地址
const int* const p3 = &x; // 指向常量的常量指针

// 数组常量
const int arr[] = {1, 2, 3, 4, 5};
// arr[0] = 10; // 错误:不能修改 const 数组的元素

// 函数参数
void print_value(const int* value) {
// *value = 10; // 错误:不能修改 const 参数
printf("%d\n", *value);
}

// 函数返回值
const char* get_message() {
return "Hello, World!";
}

volatile 限定符

volatile 限定符表示变量的值可能被外部因素修改,如硬件、中断处理程序等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 硬件寄存器
volatile int* port = (volatile int*)0x12345678;

// 多线程共享变量
volatile int flag = 0;

// 信号处理程序修改的变量
volatile sig_atomic_t signal_received = 0;

void signal_handler(int signum) {
signal_received = 1;
}

// 防止编译器优化
volatile int counter = 0;
void delay(int milliseconds) {
counter = milliseconds;
while (counter > 0) {
// 空循环
}
}

restrict 限定符

restrict 限定符表示指针是访问所指向对象的唯一方式,用于优化编译器生成的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 内存复制函数
void copy_memory(int* restrict dest, const int* restrict src, size_t count) {
for (size_t i = 0; i < count; i++) {
dest[i] = src[i];
}
}

// 矩阵乘法
void multiply_matrices(float* restrict result,
const float* restrict matrix1,
const float* restrict matrix2,
size_t size) {
// 矩阵乘法实现
}

_Atomic 限定符

_Atomic 限定符(C11 标准)表示变量是原子的,用于多线程编程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdatomic.h>

// 原子变量
_Atomic int counter = 0;

// 原子操作
void increment_counter() {
atomic_fetch_add(&counter, 1);
}

int get_counter() {
return atomic_load(&counter);
}

// 原子结构体
typedef struct {
_Atomic int x;
_Atomic int y;
} AtomicPoint;

AtomicPoint p;

小结

本章详细介绍了 C 语言的数据类型、运算符和表达式,包括:

  • 数据类型:基本数据类型(整型、浮点型、字符型)、类型修饰符、类型定义、派生类型
  • 变量和常量:变量的声明和定义、变量的作用域和生命周期、常量的类型和使用场景
  • 运算符:算术运算符、赋值运算符、关系运算符、逻辑运算符、位运算符、条件运算符、逗号运算符等
  • 表达式:表达式求值、副作用和序列点、类型转换
  • 类型限定符:const、volatile、restrict、_Atomic

这些是 C 语言编程的基础,掌握这些概念对于后续学习控制语句、函数、数组和指针等高级内容至关重要。在实际编程中,需要根据具体情况选择合适的数据类型,合理使用运算符和表达式,以及正确处理类型转换和副作用。

通过本章的学习,你应该能够:

  • 理解 C 语言的数据类型系统
  • 正确声明和使用变量和常量
  • 掌握各种运算符的使用方法和优先级
  • 理解表达式求值的规则和类型转换
  • 合理使用类型限定符提高代码的安全性和效率

在接下来的章节中,我们将学习 C 语言的控制语句,包括条件语句、循环语句和跳转语句,这些是实现程序逻辑的重要工具。