跳到主要内容

基础语法

本章介绍 C 语言的基础语法,包括变量、数据类型、运算符和表达式。这些是编写任何 C 程序的基础。

程序结构

一个标准的 C 程序由预处理指令、函数定义和语句组成。让我们从一个简单的程序开始:

#include <stdio.h>          // 预处理指令:包含标准输入输出头文件

int main(void) { // main 函数:程序入口
printf("Hello, C!\n"); // 函数调用:输出字符串
return 0; // 返回语句:表示程序正常结束
}

程序执行的流程:

  1. 预处理器处理 #include 指令,将头文件内容插入
  2. 程序从 main 函数开始执行
  3. printf 函数输出字符串到控制台
  4. 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 标准之后,可以在代码块的任何位置声明变量,但建议在使用前就近声明。

变量命名规则

  • 只能包含字母、数字和下划线
  • 必须以字母或下划线开头
  • 不能使用关键字(如 intifwhile 等)
  • 区分大小写(ageAge 是不同的变量)
int player_score;   // 合法:下划线分隔
int PlayerScore; // 合法:驼峰命名
int _count; // 合法:下划线开头
int MAX_SIZE; // 合法:常量风格

// int 2nd_place; // 非法:数字开头
// int my-var; // 非法:包含连字符
// int int; // 非法:使用关键字

命名建议:

  • 使用有意义的名称,描述变量的用途
  • 保持命名风格一致
  • 常量使用全大写字母
  • 变量名使用小写字母,单词间用下划线分隔

数据类型

C 语言是强类型语言,每个变量都有确定的数据类型。数据类型决定了变量占用的内存大小和可以存储的值的范围。

整数类型

整数类型用于存储不带小数的数值:

类型存储大小值范围
char1 字节-128 到 127 或 0 到 255
short2 字节-32,768 到 32,767
int4 字节-2,147,483,648 到 2,147,483,647
long4 或 8 字节至少 -2,147,483,648 到 2,147,483,647
long long8 字节-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 char0 到 255
unsigned short0 到 65,535
unsigned int0 到 4,294,967,295
unsigned long long0 到 18,446,744,073,709,551,615

浮点类型

浮点类型用于存储带小数的数值:

类型存储大小有效数字值范围
float4 字节6-7 位约 ±3.4E38
double8 字节15-16 位约 ±1.7E308
long double10-16 字节18-19 位或更多取决于实现
float pi = 3.14159f;          // 单精度,f 后缀
double e = 2.718281828; // 双精度,默认类型
long double ld = 3.14159265358979L; // 扩展精度,L 后缀

浮点数的注意事项:

  • 浮点数运算可能存在精度误差
  • 不要直接比较两个浮点数是否相等
  • double 是浮点运算的默认类型

十进制浮点类型(C23 可选特性)

二进制浮点数(floatdouble)在表示十进制小数时存在精度问题,这在金融计算中是不可接受的。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

适用场景

场景推荐类型
科学计算doublelong 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浮点数
%lfdouble 类型
%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 正式标准化了二进制整数常量,使用 0b0B 前缀:

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_unqualtypeof 类似,但会移除类型限定符(constvolatilerestrict):

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 中 truefalse 成为关键字,不再需要 <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 特性,需要较新的编译器:

编译器最低版本支持程度
GCC13+大部分特性
Clang16+大部分特性
MSVC19.39+部分特性

编译选项:

# GCC
gcc -std=c23 program.c

# Clang
clang -std=c23 program.c

# 或使用 c2x(过渡期)
gcc -std=c2x program.c

迁移建议

从旧标准迁移到 C23:

  1. 使用 nullptr 替代 NULL:更安全、更明确
  2. 使用 constexpr 定义常量:编译时检查更严格
  3. 使用属性标记代码意图:提高代码可读性
  4. 使用数字分隔符:大数字更易读
  5. 逐步采用新特性:不必一次全部迁移
// 传统代码
#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 等

下一章将学习 控制流程,包括条件语句和循环语句。