基础语法
本章介绍 C 语言的基础语法,包括变量、数据类型、运算符和表达式。这些是编写任何 C 程序的基础。
程序结构
一个标准的 C 程序由预处理指令、函数定义和语句组成。让我们从一个简单的程序开始:
#include <stdio.h> // 预处理指令:包含标准输入输出头文件
int main(void) { // main 函数:程序入口
printf("Hello, C!\n"); // 函数调用:输出字符串
return 0; // 返回语句:表示程序正常结束
}
程序执行的流程:
- 预处理器处理
#include指令,将头文件内容插入 - 程序从
main函数开始执行 printf函数输出字符串到控制台return 0表示程序正常结束
main 函数是每个 C 程序的入口点,操作系统调用它来启动程序。返回值 0 表示成功,非零值表示出错。
注释
注释用于解释代码,编译器会忽略注释内容。C 语言支持两种注释方式:
/*
* 多行注释
* 可以跨越多行
* 用于详细的说明
*/
// 单行注释,从 // 到行末的内容都是注释
int main(void) {
// 这是一个单行注释
printf("Hello\n"); // 行末注释
/*
这是多行注释
可以写很多内容
*/
return 0;
}
注释的最佳实践:
- 解释为什么这样做,而不是做了什么
- 保持注释与代码同步更新
- 避免无意义的注释
变量
变量是存储数据的命名内存位置。使用变量前必须先声明。
变量声明与初始化
int age; // 声明一个整型变量
age = 25; // 赋值
int score = 100; // 声明并初始化
int a, b, c; // 同时声明多个变量
int x = 1, y = 2; // 声明并初始化多个变量
C99 标准之后,可以在代码块的任何位置声明变量,但建议在使用前就近声明。
变量命名规则
- 只能包含字母、数字和下划线
- 必须以字母或下划线开头
- 不能使用关键字(如
int、if、while等) - 区分大小写(
age和Age是不同的变量)
int player_score; // 合法:下划线分隔
int PlayerScore; // 合法:驼峰命名
int _count; // 合法:下划线开头
int MAX_SIZE; // 合法:常量风格
// int 2nd_place; // 非法:数字开头
// int my-var; // 非法:包含连字符
// int int; // 非法:使用关键字
命名建议:
- 使用有意义的名称,描述变量的用途
- 保持命名风格一致
- 常量使用全大写字母
- 变量名使用小写字母,单词间用下划线分隔
数据类型
C 语言是强类型语言,每个变量都有确定的数据类型。数据类型决定了变量占用的内存大小和可以存储的值的范围。
整数类型
整数类型用于存储不带小数的数值:
| 类型 | 存储大小 | 值范围 |
|---|---|---|
char | 1 字节 | -128 到 127 或 0 到 255 |
short | 2 字节 | -32,768 到 32,767 |
int | 4 字节 | -2,147,483,648 到 2,147,483,647 |
long | 4 或 8 字节 | 至少 -2,147,483,648 到 2,147,483,647 |
long long | 8 字节 | -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 |
可以使用 sizeof 运算符查看类型或变量的大小:
#include <stdio.h>
int main(void) {
printf("char: %zu 字节\n", sizeof(char));
printf("short: %zu 字节\n", sizeof(short));
printf("int: %zu 字节\n", sizeof(int));
printf("long: %zu 字节\n", sizeof(long));
printf("long long: %zu 字节\n", sizeof(long long));
return 0;
}
有符号与无符号
整数类型默认是有符号的,可以存储正数和负数。使用 unsigned 关键字声明无符号类型,只能存储非负数,但正数范围扩大一倍:
int signed_num = -100; // 有符号整数
unsigned int unsigned_num = 100; // 无符号整数
unsigned short us = 65535; // 无符号短整型
unsigned long ul = 4000000000UL; // 无符号长整型
无符号类型的值范围:
| 类型 | 值范围 |
|---|---|
unsigned char | 0 到 255 |
unsigned short | 0 到 65,535 |
unsigned int | 0 到 4,294,967,295 |
unsigned long long | 0 到 18,446,744,073,709,551,615 |
浮点类型
浮点类型用于存储带小数的数值:
| 类型 | 存储大小 | 有效数字 | 值范围 |
|---|---|---|---|
float | 4 字节 | 6-7 位 | 约 ±3.4E38 |
double | 8 字节 | 15-16 位 | 约 ±1.7E308 |
long double | 10-16 字节 | 18-19 位或更多 | 取决于实现 |
float pi = 3.14159f; // 单精度,f 后缀
double e = 2.718281828; // 双精度,默认类型
long double ld = 3.14159265358979L; // 扩展精度,L 后缀
浮点数的注意事项:
- 浮点数运算可能存在精度误差
- 不要直接比较两个浮点数是否相等
double是浮点运算的默认类型
十进制浮点类型(C23 可选特性):
二进制浮点数(float、double)在表示十进制小数时存在精度问题,这在金融计算中是不可接受的。C23 引入了十进制浮点类型来解决这个问题:
#include <decimal.h> // 可选头文件
_Decimal32 price = 19.99df; // 十进制 32 位
_Decimal64 amount = 1000.00dd; // 十进制 64 位
_Decimal128 total = 12345.67dl; // 十进制 128 位
为什么需要十进制浮点?
// 二进制浮点的精度问题
double a = 0.1;
double b = 0.2;
double c = a + b;
printf("%.17f\n", c); // 0.30000000000000004,不是精确的 0.3!
// 十进制浮点:精确计算
_Decimal32 x = 0.1df;
_Decimal32 y = 0.2df;
_Decimal32 z = x + y; // 精确等于 0.3df
适用场景:
| 场景 | 推荐类型 |
|---|---|
| 科学计算 | double、long double |
| 金融计算 | _Decimal64、_Decimal128 |
| 图形处理 | float |
| 一般应用 | double |
检查是否支持:
#ifdef __STDC_IEC_60559_DFP__
// 支持十进制浮点
_Decimal64 money = 99.99dd;
#else
// 不支持,使用其他方案
double money = 99.99; // 注意精度问题
#endif
字符类型
char 类型用于存储单个字符,实际上存储的是字符的 ASCII 码值:
char ch = 'A'; // 字符常量,单引号
char digit = '7'; // 字符 '7',不是数字 7
char newline = '\n'; // 转义字符
printf("字符: %c, ASCII码: %d\n", ch, ch); // 输出: 字符: A, ASCII码: 65
常用转义字符:
| 转义字符 | 含义 |
|---|---|
\n | 换行 |
\t | 水平制表符 |
\\ | 反斜杠 |
\' | 单引号 |
\" | 双引号 |
\0 | 空字符(字符串结束符) |
布尔类型
C99 标准引入了 _Bool 类型和 <stdbool.h> 头文件:
#include <stdbool.h>
bool is_valid = true; // 真
bool is_empty = false; // 假
在 C99 之前,使用整数表示布尔值:0 表示假,非零表示真。
枚举类型
枚举(enum)是一组命名的整数常量,用于定义一组相关的值:
基本用法:
enum Weekday {
SUNDAY, // 默认值为 0
MONDAY, // 1
TUESDAY, // 2
WEDNESDAY, // 3
THURSDAY, // 4
FRIDAY, // 5
SATURDAY // 6
};
enum Weekday today = WEDNESDAY;
printf("今天是星期%d\n", today); // 今天是星期3
指定值:
enum Month {
JAN = 1, // 从 1 开始
FEB = 2,
MAR = 3,
// ...
DEC = 12
};
enum Status {
OK = 200,
NOT_FOUND = 404,
SERVER_ERROR = 500
};
C23 新特性:固定底层类型:
C23 允许显式指定枚举的底层类型,这在需要精确控制内存布局时非常有用:
// 指定底层类型为 unsigned char(1 字节)
enum SmallEnum : unsigned char {
A = 0,
B = 1,
C = 255 // 最大值 255
};
// 指定底层类型为 unsigned long long(8 字节)
enum LargeEnum : unsigned long long {
BIG_VALUE = 0xFFFFFFFFFFFFFFFFULL
};
// 应用场景:网络协议或文件格式
enum PacketType : uint8_t {
TYPE_DATA = 0x01,
TYPE_ACK = 0x02,
TYPE_NAK = 0x03
};
固定底层类型的优势:
| 特性 | 传统枚举 | 固定类型枚举(C23) |
|---|---|---|
| 类型大小 | 编译器决定 | 程序员指定 |
| 可移植性 | 平台相关 | 跨平台一致 |
| 内存布局 | 不确定 | 精确控制 |
| 适用场景 | 一般编程 | 协议、嵌入式 |
// 传统枚举:大小不确定
enum Traditional { VAL1, VAL2 };
printf("传统枚举大小: %zu\n", sizeof(enum Traditional)); // 可能是 4
// 固定类型枚举:大小确定
enum Fixed : uint8_t { VAL3, VAL4 };
printf("固定类型枚举大小: %zu\n", sizeof(enum Fixed)); // 一定是 1
类型限定符
const 限定符表示变量的值不能被修改:
const int MAX_SIZE = 100; // 常量,必须初始化
const double PI = 3.14159; // 常量
// MAX_SIZE = 200; // 错误:不能修改常量
volatile 限定符告诉编译器该变量可能被意外修改,不要优化:
volatile int hardware_register; // 硬件寄存器
运算符
运算符用于执行各种操作,如算术运算、比较、逻辑运算等。
算术运算符
int a = 10, b = 3;
int sum = a + b; // 加法: 13
int diff = a - b; // 减法: 7
int product = a * b; // 乘法: 30
int quotient = a / b; // 整数除法: 3(截断小数部分)
int remainder = a % b; // 取模: 1
double x = 10.0, y = 3.0;
double result = x / y; // 浮点除法: 3.333...
整数除法会截断小数部分,如果需要精确结果,至少有一个操作数应为浮点数。
自增与自减运算符
int i = 5;
i++; // 后置自增,i 变为 6
++i; // 前置自增,i 变为 7
i--; // 后置自减,i 变为 6
--i; // 前置自减,i 变为 5
// 前置与后置的区别
int a = 5;
int b = a++; // b = 5, a = 6(先赋值,后自增)
int c = ++a; // c = 7, a = 7(先自增,后赋值)
关系运算符
关系运算符用于比较两个值,返回 1(真)或 0(假):
int a = 10, b = 5;
int r1 = (a == b); // 等于: 0(假)
int r2 = (a != b); // 不等于: 1(真)
int r3 = (a > b); // 大于: 1(真)
int r4 = (a < b); // 小于: 0(假)
int r5 = (a >= b); // 大于等于: 1(真)
int r6 = (a <= b); // 小于等于: 0(假)
注意区分 =(赋值)和 ==(相等比较)。
逻辑运算符
int a = 1, b = 0;
int r1 = (a && b); // 逻辑与: 0(假)
int r2 = (a || b); // 逻辑或: 1(真)
int r3 = !a; // 逻辑非: 0(假)
int r4 = !b; // 逻辑非: 1(真)
逻辑运算符的短路求值:
int a = 0;
// 如果第一个操作数为假,&& 后面的表达式不会执行
if (a != 0 && 10 / a > 1) { // 不会发生除零错误
// ...
}
位运算符
位运算符直接操作整数的二进制位:
unsigned int a = 0b1100; // 12
unsigned int b = 0b1010; // 10
unsigned int r1 = a & b; // 按位与: 0b1000 (8)
unsigned int r2 = a | b; // 按位或: 0b1110 (14)
unsigned int r3 = a ^ b; // 按位异或: 0b0110 (6)
unsigned int r4 = ~a; // 按位取反
unsigned int r5 = a << 2; // 左移: 0b110000 (48)
unsigned int r6 = a >> 2; // 右移: 0b11 (3)
位运算的常见应用:
// 设置某一位
flags |= (1 << 3);
// 清除某一位
flags &= ~(1 << 3);
// 切换某一位
flags ^= (1 << 3);
// 检查某一位
if (flags & (1 << 3)) {
// 第 3 位为 1
}
赋值运算符
int a = 10;
a += 5; // 等价于 a = a + 5,结果: 15
a -= 3; // 等价于 a = a - 3,结果: 12
a *= 2; // 等价于 a = a * 2,结果: 24
a /= 4; // 等价于 a = a / 4,结果: 6
a %= 4; // 等价于 a = a % 4,结果: 2
a &= 0x0F; // 等价于 a = a & 0x0F
a |= 0x01; // 等价于 a = a | 0x01
条件运算符(三目运算符)
条件运算符是 C 语言中唯一的三元运算符:
int a = 10, b = 20;
int max = (a > b) ? a : b; // 如果 a > b,取 a,否则取 b
等价于:
int max;
if (a > b) {
max = a;
} else {
max = b;
}
sizeof 运算符
sizeof 返回类型或变量的大小(字节数):
int a = 10;
printf("int 大小: %zu\n", sizeof(int)); // 类型
printf("a 的大小: %zu\n", sizeof(a)); // 变量
printf("表达式大小: %zu\n", sizeof(a + 1.0)); // 表达式
运算符优先级
当表达式中有多个运算符时,按优先级顺序计算。优先级从高到低:
| 优先级 | 运算符 | 结合性 |
|---|---|---|
| 1 | () [] -> . | 左到右 |
| 2 | ! ~ ++ -- + - * & sizeof | 右到左 |
| 3 | * / % | 左到右 |
| 4 | + - | 左到右 |
| 5 | << >> | 左到右 |
| 6 | < <= > >= | 左到右 |
| 7 | == != | 左到右 |
| 8 | & | 左到右 |
| 9 | ^ | 左到右 |
| 10 | | | 左到右 |
| 11 | && | 左到右 |
| 12 | || | 左到右 |
| 13 | ?: | 右到左 |
| 14 | = += -= *= /= %= &= ^= |= <<= >>= | 右到左 |
| 15 | , | 左到右 |
建议:不确定时使用括号明确优先级。
类型转换
隐式类型转换
编译器自动进行的类型转换:
int a = 10;
double b = 3.5;
double c = a + b; // a 自动转换为 double,结果: 13.5
char ch = 'A';
int code = ch; // char 自动转换为 int
转换规则:较小的类型自动转换为较大的类型。
显式类型转换(强制转换)
使用 (类型) 语法进行强制转换:
int a = 10, b = 3;
double result = (double)a / b; // 结果: 3.333...
double x = 3.7;
int y = (int)x; // 截断,结果: 3
表达式与语句
表达式
表达式是由运算符和操作数组成的式子,可以计算出一个值:
10 // 常量表达式
a // 变量表达式
a + b // 算术表达式
a > b // 关系表达式
a && b // 逻辑表达式
func() // 函数调用表达式
语句
语句是程序执行的基本单位,以分号结尾:
int a = 10; // 声明语句
a = a + 1; // 赋值语句
printf("hello"); // 函数调用语句
; // 空语句
复合语句(代码块)用花括号括起来:
{
int a = 10;
a = a + 1;
printf("%d\n", a);
}
输入输出基础
printf 输出
printf 用于格式化输出:
int age = 25;
double pi = 3.14159;
char ch = 'A';
printf("年龄: %d\n", age); // 输出整数
printf("圆周率: %.2f\n", pi); // 输出浮点数,保留2位小数
printf("字符: %c\n", ch); // 输出字符
printf("字符串: %s\n", "hello"); // 输出字符串
常用格式说明符:
| 格式符 | 说明 |
|---|---|
%d | 有符号十进制整数 |
%u | 无符号十进制整数 |
%f | 浮点数 |
%lf | double 类型 |
%c | 单个字符 |
%s | 字符串 |
%x | 十六进制(小写) |
%X | 十六进制(大写) |
%o | 八进制 |
%p | 指针地址 |
%% | 百分号 |
宽度和精度控制:
printf("%5d\n", 42); // 宽度5,右对齐: " 42"
printf("%-5d\n", 42); // 宽度5,左对齐: "42 "
printf("%05d\n", 42); // 宽度5,用0填充: "00042"
printf("%.2f\n", 3.14159); // 精度2位: "3.14"
printf("%8.2f\n", 3.14); // 宽度8,精度2: " 3.14"
scanf 输入
scanf 用于读取用户输入:
int age;
printf("请输入年龄: ");
scanf("%d", &age); // 注意 & 取地址符
printf("你的年龄是: %d\n", age);
double price;
printf("请输入价格: ");
scanf("%lf", &price); // double 使用 %lf
printf("价格是: %.2f\n", price);
char name[50];
printf("请输入姓名: ");
scanf("%s", name); // 数组名本身就是地址,不需要 &
printf("你好, %s\n", name);
scanf 返回成功读取的项目数,可以用于错误检查:
int a, b;
int result = scanf("%d %d", &a, &b);
if (result == 2) {
printf("读取成功: %d, %d\n", a, b);
} else {
printf("输入格式错误\n");
}
C23 新特性
C23(ISO/IEC 9899:2024)是 C 语言的最新标准,引入了许多现代化的特性。本节介绍 C23 的主要新特性,帮助你编写更安全、更现代的 C 代码。
nullptr 关键字
C23 引入了 nullptr 关键字作为空指针常量,替代传统的 NULL 宏和 0。
为什么需要 nullptr?
在 C23 之前,空指针通常使用 NULL 宏或整数 0 表示,这可能导致类型安全问题:
// 传统方式的问题
void func(int value); // 接受 int 参数
void func(int* ptr); // 接受指针参数
func(NULL); // 调用哪个函数?NULL 是 0 还是指针?
nullptr 的使用:
int* ptr = nullptr; // 明确的空指针
if (ptr == nullptr) {
printf("ptr 是空指针\n");
}
// nullptr 有自己的类型 nullptr_t
nullptr 的类型是 nullptr_t,可以隐式转换为任何指针类型,但不能转换为整数类型:
int* p1 = nullptr; // 正确
char* p2 = nullptr; // 正确
// int n = nullptr; // 错误!不能转换为整数
nullptr_t 类型:
#include <stddef.h> // C23 中 nullptr_t 定义在此
nullptr_t np = nullptr; // np 只能是 nullptr
int* ptr = np; // 可以转换为其他指针类型
constexpr 关键字
C23 引入了 constexpr 关键字,用于声明编译时常量。这与 const 不同:const 表示运行时不可修改,而 constexpr 表示编译时就确定值。
基本用法:
constexpr int MAX_SIZE = 100;
constexpr double PI = 3.14159265358979;
constexpr int ARR[3] = {1, 2, 3};
// constexpr 变量必须在声明时初始化
constexpr int value; // 错误!必须初始化
constexpr 与 const 的区别:
const int runtime_value = get_value(); // 运行时初始化
constexpr int compile_value = 100; // 编译时已知
int arr1[runtime_value]; // 错误(C99 之前),变长数组
int arr2[compile_value]; // 正确,编译时大小已知
constexpr 的限制:
// 只能用于标量类型、数组、结构体
constexpr int num = 10;
constexpr double pi = 3.14;
constexpr char str[] = "hello";
constexpr struct Point { int x, y; } point = {1, 2};
// 不能用于需要运行时计算的值
// constexpr int random_value = rand(); // 错误!
位精确整数 _BitInt
C23 引入了 _BitInt(N) 类型,允许精确指定位宽的整数。这在需要精确控制内存布局或与非标准硬件交互时非常有用。
基本用法:
_BitInt(8) byte_val; // 精确 8 位整数
_BitInt(16) word_val; // 精确 16 位整数
_BitInt(32) dword_val; // 精确 32 位整数
_BitInt(128) big_val; // 128 位整数
unsigned _BitInt(7) val7; // 7 位无符号整数(范围 0-127)
为什么使用 _BitInt?
// 场景一:与非标准硬件交互
// 某些硬件寄存器是 12 位或 24 位
_BitInt(12) register_value;
// 场景二:精确控制内存布局
struct Header {
_BitInt(3) version;
_BitInt(5) flags;
_BitInt(24) length;
};
// 场景三:需要比标准类型更大的整数
_BitInt(256) huge_number;
限制:
// N 必须至少为 1
_BitInt(0) zero; // 错误!
// 位宽有限制,具体取决于实现
// 通常至少支持到 _BitInt(128)
二进制整数常量
C23 正式标准化了二进制整数常量,使用 0b 或 0B 前缀:
int binary = 0b1010; // 10
int flags = 0b00001111; // 低 4 位为 1
int mask = 0b11110000; // 高 4 位为 1
// 可以与其他前缀组合
unsigned int hex_binary = 0b1010U;
long big_binary = 0b11111111L;
应用场景:
// 位掩码定义更清晰
#define READ_PERMISSION 0b0001
#define WRITE_PERMISSION 0b0010
#define EXECUTE_PERMISSION 0b0100
#define ADMIN_PERMISSION 0b1000
// 硬件寄存器操作
#define UART_ENABLE 0b00000001
#define UART_PARITY 0b00000010
#define UART_STOPBITS 0b00000100
u8 字符常量
C23 引入了 u8 前缀的字符常量,用于表示 UTF-8 编码的单个字符。
基本用法:
// u8 字符常量(C23)
char8_t ch = u8'A'; // ASCII 字符
char8_t chinese = u8'中'; // 中文字符(UTF-8 编码)
// 注意:u8 字符常量的类型是 char8_t(需要 <uchar.h>)
// 在 C23 之前,u8 只能用于字符串字面量
u8 字符串字面量的类型变化:
在 C23 中,u8 字符串字面量的类型从 char[] 变为 char8_t[]:
#include <uchar.h> // C23 新增
// C23 之前
// u8"hello" 的类型是 char[]
// C23 中
// u8"hello" 的类型是 char8_t[]
const char8_t* str = u8"你好,世界";
// 与普通字符串的区别
const char* normal = "hello"; // char*
const char8_t* utf8 = u8"hello"; // char8_t*(明确表示 UTF-8)
为什么需要 u8 字符常量?
// 场景一:明确表示 UTF-8 编码
char8_t euro = u8'€'; // 明确是 UTF-8 编码
// 场景二:与平台编码无关
// 普通字符常量的编码取决于平台
// u8 字符常量保证是 UTF-8
兼容性处理:
// 如果需要兼容旧版本
#if __STDC_VERSION__ >= 202311L
// C23 或更高
const char8_t* str = u8"hello";
#else
// 旧版本
const char* str = u8"hello"; // 类型不同但用法类似
#endif
数字分隔符
C23 引入了单引号 ' 作为数字分隔符,提高大数字的可读性:
// 整数分隔
int million = 1'000'000;
long big_num = 1'234'567'890L;
// 十六进制分隔
int hex = 0xFF'FF'FF'FF;
int hex_addr = 0x1234'5678;
// 二进制分隔(特别有用)
int binary = 0b1111'0000'1111'0000;
int flags = 0b0000'0001'0000'0000;
// 浮点数分隔
double pi = 3.141'592'653'589'793;
double big_double = 1'000'000.5;
使用建议:
// 按 3 位分组(十进制)
int thousand = 1'000;
int million = 1'000'000;
// 按 4 位分组(十六进制和二进制)
int color = 0xFF'00'FF; // RGB 颜色
int mask = 0b1111'0000'1111'0000; // 位掩码
属性(Attributes)
C23 引入了标准属性语法,使用双括号 [[...]] 形式,用于向编译器提供额外信息。
[[nodiscard]] - 忽略返回值警告:
[[nodiscard]] int get_important_value(void);
int main(void) {
get_important_value(); // 警告:忽略返回值
int value = get_important_value(); // 正确
return 0;
}
// 可以添加消息
[[nodiscard("必须检查返回值,可能失败")]] int open_file(const char* path);
[[maybe_unused]] - 抑制未使用警告:
[[maybe_unused]] static int helper_function(void) {
return 42;
}
int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) {
return 0;
}
[[deprecated]] - 标记弃用:
[[deprecated]] void old_function(void);
[[deprecated("请使用 new_function() 代替")]]
void legacy_api(void);
int main(void) {
old_function(); // 警告:函数已弃用
return 0;
}
[[fallthrough]] - 标记 switch 穿透:
switch (value) {
case 1:
do_something();
[[fallthrough]]; // 明确表示有意穿透
case 2:
do_another_thing();
break;
}
[[noreturn]] - 函数不返回:
[[noreturn]] void fatal_error(const char* msg) {
fprintf(stderr, "错误: %s\n", msg);
exit(1);
}
[[noreturn]] void infinite_loop(void) {
while (1) {
// 永不返回
}
}
[[unsequenced]] - 无副作用函数:
C23 新增的属性,表示函数对相同参数总是返回相同结果,且没有任何副作用。这允许编译器进行更激进的优化:
// 纯计算函数,无副作用
[[unsequenced]] int square(int x) {
return x * x;
}
// 可以安全地被优化:相同参数只计算一次
int a = square(5); // 第一次计算
int b = square(5); // 编译器可能直接复用之前的結果
// 错误示例:有副作用的函数不能标记为 unsequenced
// [[unsequenced]] int bad_func(int x) {
// printf("计算中\n"); // 副作用!
// return x * 2;
// }
[[reproducible]] - 可重现函数:
表示函数对相同参数总是返回相同结果,但可能有读取全局状态等"无害"副作用:
int config_value = 100;
// 可以读取全局状态,但不修改
[[reproducible]] int get_adjusted(int x) {
return x * config_value; // 读取全局变量
}
// 与 [[unsequenced]] 的区别:
// [[unsequenced]]:完全无副作用
// [[reproducible]]:可以有只读副作用
属性对比:
| 属性 | 相同输入相同输出 | 读取全局状态 | 修改全局状态 | I/O 操作 |
|---|---|---|---|---|
| 无属性 | 不保证 | 允许 | 允许 | 允许 |
[[reproducible]] | 保证 | 允许 | 禁止 | 禁止 |
[[unsequenced]] | 保证 | 禁止 | 禁止 | 禁止 |
typeof 运算符
C23 引入了 typeof 运算符,可以获取表达式的类型。这在泛型编程中非常有用。
基本用法:
int value = 42;
typeof(value) copy = value; // copy 的类型是 int
double d = 3.14;
typeof(d) another_double; // another_double 的类型是 double
// 数组类型
int arr[10];
typeof(arr) arr_copy; // int[10]
泛型宏:
// 通用的交换宏
#define SWAP(a, b) do { \
typeof(a) temp = a; \
a = b; \
b = temp; \
} while (0)
int x = 1, y = 2;
SWAP(x, y); // 交换整数
double d1 = 1.5, d2 = 2.5;
SWAP(d1, d2); // 交换浮点数
typeof_unqual:
typeof_unqual 与 typeof 类似,但会移除类型限定符(const、volatile、restrict):
const int value = 42;
typeof(value) a; // const int
typeof_unqual(value) b; // int(移除了 const)
auto 类型推断
C23 的 auto 关键字可以用于类型推断(注意:与 C++ 的 auto 语义不同,C 中 auto 原本是存储类说明符)。
基本用法:
auto x = 42; // int
auto y = 3.14; // double
auto str = "hello"; // char*
auto ptr = &x; // int*
// 复杂类型的简化
auto compare = compare_int; // 函数指针,自动推断类型
限制:
// 必须有初始化器
auto x; // 错误!无法推断类型
// 不能用于函数参数
void func(auto x); // 错误!
true 和 false 成为关键字
C23 中 true 和 false 成为关键字,不再需要 <stdbool.h> 头文件:
// C23 之前
#include <stdbool.h>
bool flag = true;
// C23 可以直接使用
bool flag = true; // true 是关键字
bool valid = false; // false 是关键字
单参数 static_assert
C23 允许 static_assert 只有一个参数(消息可选):
// C11/C17:必须有两个参数
static_assert(sizeof(int) == 4, "int must be 4 bytes");
// C23:消息可选
static_assert(sizeof(int) == 4);
static_assert(sizeof(void*) == 8, "需要 64 位平台");
空初始化器
C23 允许使用空初始化器 {} 进行零初始化:
int arr[5] = {}; // 所有元素初始化为 0
struct Point { int x, y; };
struct Point p = {}; // x = 0, y = 0
// 等价于
int arr2[5] = {0};
struct Point p2 = {0};
新的预处理器指令
C23 引入了几个新的预处理器指令:
#elifdef 和 #elifndef:
#ifdef DEBUG
#define LOG_LEVEL 3
#elifdef VERBOSE
#define LOG_LEVEL 2
#elifndef RELEASE
#define LOG_LEVEL 1
#else
#define LOG_LEVEL 0
#endif
#warning:
#warning "此功能尚未完全实现"
#ifdef DEPRECATED_API
#warning "DEPRECATED_API 将在下一版本移除"
#endif
#embed(可选特性):
```embed` 指令可以在编译时嵌入文件内容:
// 嵌入文件内容
const unsigned char data[] = {
#embed "image.bin"
};
// 限制嵌入大小
const unsigned char header[] = {
#embed "file.bin" limit(64)
};
VA_OPT 宏
C23 标准化了 __VA_OPT__,用于可变参数宏中处理空参数:
#define DEBUG_PRINT(fmt, ...) \
printf(fmt __VA_OPT__(,) __VA_ARGS__)
DEBUG_PRINT("hello"); // printf("hello")
DEBUG_PRINT("value: %d", 42); // printf("value: %d", 42)
C23 编译器支持
要使用 C23 特性,需要较新的编译器:
| 编译器 | 最低版本 | 支持程度 |
|---|---|---|
| GCC | 13+ | 大部分特性 |
| Clang | 16+ | 大部分特性 |
| MSVC | 19.39+ | 部分特性 |
编译选项:
# GCC
gcc -std=c23 program.c
# Clang
clang -std=c23 program.c
# 或使用 c2x(过渡期)
gcc -std=c2x program.c
迁移建议
从旧标准迁移到 C23:
- 使用 nullptr 替代 NULL:更安全、更明确
- 使用 constexpr 定义常量:编译时检查更严格
- 使用属性标记代码意图:提高代码可读性
- 使用数字分隔符:大数字更易读
- 逐步采用新特性:不必一次全部迁移
// 传统代码
#define MAX_SIZE 100
int* ptr = NULL;
// C23 风格
constexpr int MAX_SIZE = 100;
int* ptr = nullptr;
小结
本章介绍了 C 语言的基础语法:
- 程序结构:预处理指令、main 函数、语句
- 变量:声明、初始化、命名规则
- 数据类型:整数、浮点、字符、布尔类型
- 运算符:算术、关系、逻辑、位运算、赋值
- 类型转换:隐式转换和强制转换
- 基本输入输出:printf 和 scanf
- C23 新特性:nullptr、constexpr、_BitInt、属性、typeof 等
下一章将学习 控制流程,包括条件语句和循环语句。