C的程序结构
C 程序主要包括以下部分:
- 预处理器指令
- 函数
- 变量
- 语句 & 表达式
- 注释
下面是一个简单的示例:
#include <stdio.h> // 预处理指令,包含标准输入输出库
int main() { // 主函数,程序从这里开始执行
/*
注释示例
*/
int a = 5; // 定义一个整型变量a,并赋值为5
int b = 3; // 定义一个整型变量b,并赋值为3
int sum = 0; // 定义一个整型变量sum,用于存储a和b的和
sum = a + b; // 表达式,将a和b相加,并将结果赋值给sum
// 输出结果
printf("Sum of a and b is: %d\n", sum); // 输出a和b的和,使用%d占位符表示整数
return 0; // 返回值为0,表示程序成功结束
}
//表示单行注释,/* … */表示多行注释。
变量
c语言中,凡是局部的未被初始化的变量,其在内存中的值不可预测,如果想要其在内存的部分被全部初始化为零,必需在声明的时候进行部分初始化或全部初始化。对于全局变量和被static修饰的局部变量,在内存中会被全部初始化为0。
#include <stdio.h>
int main() {
int a;
printf("%d\n", a);
return 0;
}
对于上述的代码,不仅可以正确执行,每次的结果也可能是不同的,也可能有相同的。
基本数据类型
- 整形:
short
、int
、long
、long long
- 字符:
char
- 浮点型:
float
、double
、long double
所占用的字节大小如下:
win32 | win64 | linux32 | linux64 | |
---|---|---|---|---|
char |
1 | 1 | 1 | 1 |
short |
2 | 2 | 2 | 2 |
int |
4 | 4 | 4 | 4 |
long |
4 | 4 | 4 | 8 |
long long |
8 | 8 | 8 | 8 |
float |
4 | 4 | 4 | 4 |
double |
8 | 8 | 8 | 8 |
long double |
8 | 8 | 12 | 16 |
语法
// 先定义在赋值
int a;
a = 1;
float b;
b = 2.0
// 定义并且赋值
int a = 1;
float b = 2;
类型转换
若由范围小的转范围大的,会由编译器隐式的转换,否则需要进行手动指定,强制类型转换会因为范围可能会导致数据溢出,如:
short a = 1;
short b = 2;
float c = a + b;
// c为float类型,但是a+b为short类型,被隐式的转换为float型
int a = 1;
int b = 2;
short c = (int) a + b;
// c为short类型,但是a+b为int类型,被隐式的转换为short型
枚举
枚举是一种用户自定义的整数类型,它将一组相关的常量命名为符号名称。每个符号名称默认与一个整数值关联,值从0开始自动递增。
语法
//enum enumName {name1, name2, ...}
enum Weekday { Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday };
- 枚举定义了
Weekday
这一类型。 - 默认情况下,
Sunday = 0, Monday = 1, Tuesday = 2
,依此类推。 - 可以给枚举值显式赋值,如
Monday = 2
。
使用示例
enum Weekday today;
today = Monday;
printf("%d", today); // 输出 1
派生类型
数组
数组是一组相同类型的数据集合,存储在连续的内存位置。数组的大小必须在定义时指定。
语法
// 声明:
type arrayName[arraySize];
// 初始化:
type arrayName[arraySize] = {value1, value2, .., valuen} // 静态初始化(初始化时赋值)
type arrayName[arraySize] = {value1, value2} // 部分初始化(只初始化部分元素,其他元素默认为0或NULL)
type arrayName[] = {value1,value2, ...} // 动态初始化(编译器自动计算元素数量)
// 访问并修改数组元素
arrayName[index] = value;
使用示例
int arr[5] = {1, 2, 3, 4, 5}; // 明确初始化所有元素
int arr[5] = {1, 2}; // arr = {1, 2, 0, 0, 0}
int arr[] = {1, 2, 3, 4, 5}; // 数组大小为5
arr[2] = 10;
多重数组
以二重数组为例,二重数组可以用<DataType> variable[size1][size2];
的方式进行定义,可以理解为DataType数组的数组,赋值的时候元素也是数组。
访问的时候通过variable[i][j]
的方式访问,可以推广到n维数组。
字符串
在C语言中,字符串并不是一种独立的数据类型,而是由字符数组实现的。字符串是由一系列字符组成的,并以一个 空字符 ('\0'
) 结尾,用来标识字符串的结束。
基本结构
c语言对字符串的赋值提供了一个优化:
char 数组名[] = "字符串内容";
char 数组名[arraySize] = "字符串内容"; // arraysize大于等于字符串的长度
示例
// 一般赋值
char str[9] = {'a','b','c','d','e','f','\0'};
char str[7] = {'a','b','c','d','e','f','\0'};
char str[] = {'a','b','c','d','e','f','\0'};
// 优化的赋值
char str[] = "abcdef";
char str[7] = "abcdef";
char str[9] = "abcdef";
字符串常量
字符串常量存放到rodata或rdata段,不可更改,和用static
和const
修饰的字符串也有一些不同。
基本结构
char *pointerName = "str";
其中pointerName
为指向字符串的指针,指针的值为在rdata段"str"存放的首地址。
示例
char *str = "fff团";
要点
- 字符串常量即使定义在函数中,也会被定义到rdata段或rodata段,而不会被放到堆栈中,等同于被
static
和const
修饰。 - 字符串常量之间如果存在相同的,则会直接使用已经存在的,以节约空间,但使用仅限于由通过上述所阐述的结构定义的字符串常量,以及通过函数参数传递的字符串常量。对于通过被
static
和const
修饰的字符串所定义的,并不会参与,并且字符串之间也不存在。 - 字符串常量在作为函数参数的时候,可以直接传递一个字符串。虽然看起来是直接传递字符串,但实际上传递的是一个指向字符串常量的指针。
结构体
结构体是一种用户定义的数据类型,它可以将不同类型的变量组合成一个实体。可以用来定义一个对象的多种属性。
语法
// 声明结构体类型
struct 结构体名 {
数据类型 成员名1;
数据类型 成员名2;
...
};
// 定义结构体变量
struct 结构体名 structName;
// 结构体变量的初始化:
struct 结构体名 structName = {成员1, 成员2, ...}; // 可以在声明变量的同时对结构体成员进行初始化
// 也可以逐个成员赋值
// 结构体成员通过 . 运算符访问
structName.成员名1 = 成员1;
structName.成员名2 = 成员2;
...
说明
-
结构体名:结构体的名称,用来声明该类型的变量。
-
成员名:结构体中的每个元素(字段),可以是不同类型的数据。
使用示例
// 声明结构体类型
struct Person {
char name[50]; // 姓名
int age; // 年龄
};
struct Person person1; // 声明结构体变量
// 初始化结构体:
struct Person person1 = {"John", 25}; // 可以在声明变量的同时对结构体成员进行初始化
// 也可以逐个成员赋值
strcpy(person1.name, "John"); // 初始化成员 name
person1.age = 25; // 初始化成员 age
// 访问结构体成员
printf("Name: %s, Age: %d\n", person1.name, person1.age)
共用体
共用体与结构体类似,但不同点是共用体的所有成员共享同一块内存空间。这意味着在同一时间内只能保存一个成员的值。
语法
union Data {
int i;
float f;
char str[20];
};
说明
Data
是一个共用体类型,包含三个成员:i
(整数)、f
(浮点数)、str
(字符串)。- 由于成员共享内存,所以修改一个成员会影响其他成员。
使用示例
union Data data;
data.i = 10;
printf("i: %d\n", data.i);
data.f = 220.5;
printf("f: %f\n", data.f);
strcpy(data.str, "Hello");
printf("str: %s\n", data.str);
指针
对于C代码int a = 1;
,a在虚拟内存空间中有唯一的地址,指针就是内存地址,不同的地方在于C中的指针保存了对应类型的长度。和如下汇编代码相比:
section .data:
db a 1
其中的(a)只保存了对应的地址,相当于给地址换种表示。
int *b = &a;
其中的b被写入到内存中,也有相应的地址,并且在进行运算时,C编译器会加上n个数据类型的长度,例如:
int a[3] = {1, 2, 3};
int *b = a;
b += 1;
// 这里b实际上移动了4
section .data:
dw a 1,2,3
section .text
mov eax, [a]
mov ebx, [a + 1] ;这里a只是在原来的地址上+1
语法
// 针对变量的指针
DataType name = value;
DataType *pointer_name = &name;
DataType name = *pointer_name; // 通过指针获取变量的值
// 函数指针
return_type function_name(parameter_types datas) {
return parameter;
}
return_type (*pointer_name)(parameter_types);
(*pointer_name)(params); // 通过指针调用函数
DataType
:变量的类型
return_type
:函数的返回类型。
pointer_name
:指针的名称。
parameter_types
:函数的参数类型列表。
示例
#include <stdio.h>
// 定义一个普通函数
int add(int a, int b) {
return a + b;
}
int main() {
// 变量指针
int num = 10;
int *p_num = # // 定义一个指向 num 的指针
printf("num 的值: %d\n", num);
printf("通过指针 p_num 访问 num 的值: %d\n", *p_num);
// 使用指针更改 num 的值
*p_num = 20;
printf("通过指针更改 num 的值后,num = %d\n", num);
// 函数指针
int (*func_ptr)(int, int); // 定义一个指向函数的指针
func_ptr = &add; // 将 add 函数的地址赋值给指针
int result = func_ptr(5, 3); // 通过指针调用函数
printf("通过函数指针调用 add(5, 3) 的结果: %d\n", result);
return 0;
}
运算
指针与整数的运算:
指针可以和整数进行运算,改变的大小为指针指向类型的长度。
int arr[] = {1, 2, 3, 4};
int* ptr = arr;
ptr = ptr + 2; // ptr 现在指向 arr[2],等价于 arr + 2
指针自增与自减:
指针也可以进行自增 (++
) 和自减 (--
) 操作。
int arr[] = {10, 20, 30};
int* ptr = arr;
ptr++; // ptr 现在指向 arr[1]
ptr--; // ptr 现在指向 arr[0]
void指针
void * 为**"无类型指针"**,可以指向任意类型的数据。在使用的时候必须转换为其它类型的指针。
类型转换
基本结构为:(新类型*) 指针
int a = 1024;
int* int_ptr = &a;
char* char_ptr = (char*) int_ptr; // 将 int* 转换为 char*
printf("First byte of int: %d\n", *char_ptr); // 打印 a 的第一个字节
类型转换不会改变指针指向的地址,但是会改变指针指向类型的长度。
数组指针和指针数组
对于指针数组,以int型为例,和一般的数组结构相同:
int *p[10];
对于数组的指针,如果是一维数组,可以用int *
进行表示和运算,但是对于多重数组的定义如下:
int (*p)[10]
上述的代码创建了一个指针,指向的是长度为10的int型的数组。
指针的优化
对于结构体指针,可以通过**->代替*和.**更快访问结构体的元素。
typedef struct {
int age;
char name[50];
} Person;
Person p = {19, "张某"};
Person *s = &p;
printf("%d %s", p->age, p->name);
函数指针调用函数的时候也可以通过pointer_name(params)
的方式。
int add(int a, int b) {
return a + b;
}
int main() {
int (*func)(int, int) = add;
int a = func(3, 4);
}
在大多数情况下,数组名(如 arr
)可以作为指向数组第一个元素的指针。
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr; // 数组名 arr 表示的是数组首元素的地址
void printArray(int *arr, int size) {
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
int main() {
int arr[] = {1, 2, 3, 4, 5};
int size = sizeof(arr) / sizeof(arr[0]);
printArray(arr, size); // 传递数组,实际上是传递指针
int arr[10][10] = {{1,2}, {3,4}, {5,6}};
printf("%d", *((*arr))+1); // 会输出2
return 0;
}
同样的一个int
类型的指针也可以直接作为数组使用:
int main()
{
int arr[10];
int *p = arr;
p[0] = 1;
p[1] = 2;
printf("%d %d", p[0], p[1]);
}
函数名可以直接作为指向该函数的指针,例如:
int add(int a, int b) {
return a + b;
}
int main() {
int result;
// 函数名 add 可以视作指向 add 函数的指针
result = add(2, 3); // 直接调用
result = (*add)(2, 3); // 通过函数名作为指针调用
printf("Result: %d\n", result); // 输出 5
return 0;
}
所以在赋值和使用的时候,可以更方遍:
int (*func_ptr)(int, int) = add;
int result = func_ptr(2, 3); // 通过指针调用函数
printf("Result: %d\n", result); // 输出 5
函数指针作为函数的参数,可以直接传入函数的名称:
#include <stdio.h>
// 定义一个执行操作的函数
void executeOperation(int a, int b, int (*operation)(int, int)) {
printf("Result: %d\n", operation(a, b));
}
// 定义两个操作函数
int add(int a, int b) {
return a + b;
}
int multiply(int a, int b) {
return a * b;
}
int main() {
// 传递 add 和 multiply 函数的地址
executeOperation(3, 4, add); // 调用 add 函数
executeOperation(3, 4, multiply); // 调用 multiply 函数
return 0;
}
运算符
算术运算符
C 语言中的算术运算符用于执行基本的数学运算。
+
:加法运算符,用于将两个值相加。-
:减法运算符,用于将右操作数从左操作数中减去。*
:乘法运算符,用于将两个值相乘。/
:除法运算符,用于将左操作数除以右操作数。%
:取模运算符,用于计算左操作数除以右操作数的余数。++
:自增运算符,用于数字自增加一。–-
:自减运算符,用于数字自减减一。
a++
与 ++a
的区别在于:a++
在使用 a
之后才自增它的值,而 ++a
会在使用 a
之前自增它的值。
比较运算符
比较运算符用于比较两个值之间的关系,并返回一个布尔值(true
或 false
)。
==
:等于运算符,用于检查两个值是否相等。!=
:不等于运算符,用于检查两个值是否不相等。>
:大于运算符,用于检查左操作数是否大于右操作数。<
:小于运算符,用于检查左操作数是否小于右操作数。>=
:大于等于运算符,用于检查左操作数是否大于或等于右操作数。<=
:小于等于运算符,用于检查左操作数是否小于或等于右操作数。
逻辑运算符
逻辑运算符用于执行逻辑运算,操作数和结果都是布尔值。
&&
:逻辑与运算符,当且仅当两个操作数都为true
时结果才为true
。||
:逻辑或运算符,当任一操作数为true
时结果就为true
。!
:逻辑非运算符,用于取反操作数的值,即将true
变为false
,将false
变为true
。
复合赋值运算符
复合赋值运算符用于在执行运算的同时进行赋值操作。
+=
:加法赋值运算符,等同于a = a + b
。-=
:减法赋值运算符,等同于a = a - b
。*=
:乘法赋值运算符,等同于a = a * b
。/=
:除法赋值运算符,等同于a = a / b
。%=
:取模赋值运算符,等同于a = a % b
。
三元运算符
形式上是<条件> ? <表达式> : <表达式>
,示例:若 a
的值为 true
,就执行语句 b
,否则执行语句 c
。
sizeof
sizeof
是一个运算符,给出某个类型或变量在内存中所占据的字节数,它是一个静态运算符。
条件语句
if 语句
if
语句用于在满足给定条件时执行一段代码块。
语法
if (condition) {
// 如果条件为真,则执行这里的代码块
}
示例
#include <stdio.h>
int main() {
int num = 10;
if (num > 0) {
printf("The number is positive.\n");
}
return 0;
}
else if 语句
else if
语句用于在满足多个条件时执行不同的代码块。
语法
if (condition1) {
// 如果条件1为真,则执行这里的代码块
}
else if (condition2) {
// 如果条件1不满足,但条件2为真,则执行这里的代码块
}
示例
#include <stdio.h>
int main() {
int num = 0;
if (num > 0) {
printf("The number is positive.\n");
}
else if (num < 0) {
printf("The number is negative.\n");
}
else {
printf("The number is zero.\n");
}
return 0;
}
else 语句
else
语句用于在前面的条件都不满足时执行一段代码块。
语法
if (condition) {
// 如果条件为真,则执行这里的代码块
}
else {
// 如果前面的条件都不满足,则执行这里的代码块
}
示例
#include <stdio.h>
int main() {
int num = -5;
if (num > 0) {
printf("The number is positive.\n");
}
else {
printf("The number is not positive.\n");
}
return 0;
}
switch 语句
在 C 语言中,switch
语句是一种用于多分支选择的控制语句。它允许根据表达式的值选择性地执行不同的代码块。
语法
switch (expression) {
case constant1:
// 当表达式的值等于 constant1 时执行这里的代码
break;
case constant2:
// 当表达式的值等于 constant2 时执行这里的代码
break;
...
case constantN:
// 当表达式的值等于 constantN 时执行这里的代码
break;
default:
// 当表达式的值与所有 case 常量都不匹配时执行这里的代码
}
expression
:一个整型表达式,其值将被用来匹配case
中的常量。case constant1, case constant2, ..., case constantN
:各个可能的常量值,用来与表达式的值进行比较。default
:可选的标签,当表达式的值与所有case
中的常量值都不匹配时执行相应的代码。
示例
#include <stdio.h>
int main() {
int choice;
printf("Enter a number between 1 and 3: ");
scanf("%d", &choice);
switch (choice) {
case 1:
printf("You chose option 1.\n");
break;
case 2:
printf("You chose option 2.\n");
break;
case 3:
printf("You chose option 3.\n");
break;
default:
printf("Invalid choice.\n");
}
return 0;
}
注意事项
- 每个
case
标签都必须是整型常量表达式。 - 每个
case
块中的代码执行后,必须使用break
语句来退出switch
语句,否则将继续执行下一个case
块。 default
标签是可选的,用于处理未在case
中列出的值。
循环语句
for 循环
for
循环用于执行固定次数的循环。
语法
for (initialization; condition; increment/decrement) {
// 循环体
}
initialization
:初始化表达式,用于初始化循环控制变量。condition
:循环条件,只要条件为真(非零),循环就会继续执行;如果条件为假(零),循环终止。increment/decrement
:增量或减量表达式,用于更新循环控制变量。
示例
#include <stdio.h>
int main() {
int i;
for (i = 0; i < 5; i++) {
printf("%d ", i);
}
return 0;
}
while 循环
while
循环用于执行条件为真时的循环,条件在循环体开始执行之前进行检查。
语法
while (condition) {
// 循环体
}
condition
:循环条件,只要条件为真(非零),循环就会继续执行;如果条件为假(零),循环终止。
示例
#include <stdio.h>
int main() {
int i = 0;
while (i < 5) {
printf("%d ", i);
i++;
}
return 0;
}
do-while 循环
do-while
循环用于执行循环体至少一次,然后在每次循环结束后检查循环条件是否满足。
语法
do {
// 循环体
} while (condition);
condition
:循环条件,只要条件为真(非零),循环就会继续执行;如果条件为假(零),循环终止。
示例
#include <stdio.h>
int main() {
int i = 0;
do {
printf("%d ", i);
i++;
} while (i < 5);
return 0;
}
函数
基本结构
返回类型 函数名(参数类型 参数名, ...);
// 函数定义 (函数的具体实现)
返回类型 函数名(参数类型 参数名, ...) {
// 函数体,执行代码
return 返回值;
}
// 函数调用
函数名(参数);
解释:
- 返回类型:函数返回值的数据类型,如果函数不返回值则用
void
。 - 函数名:定义函数的名称。
- 参数列表:传递给函数的数据,参数类型和名称指定函数接受什么类型的数据。
- 函数体:函数执行的代码块。
- 返回值:返回给调用者的值,返回类型应与函数声明中的返回类型匹配。
使用示例
#include <stdio.h>
// 函数声明
int add(int a, int b);
int main() {
int result = add(5, 10); // 调用函数
printf("Result: %d\n", result);
return 0;
}
// 函数定义
int add(int a, int b) {
return a + b;
}
递归
递归是指一个函数在其定义中直接或间接地调用自身的编程方式。递归通常分为两个部分:
- 递归结束条件:确保递归不会一直进行下去的条件,也称为基准条件。
- 递归步骤:每次递归调用时,问题规模逐步减小,最终达到基准条件。
基本结构
返回类型 函数名(参数)
{
if (结束条件) // 递归结束条件
{
// 基本情况处理
return 返回值;
}
else
{
// 递归步骤
return 函数名(递归的参数);
}
}
示例
// 计算阶乘
int factorial(int n)
{
if (n == 0 || n == 1) // 基本情况
return 1;
else
return n * factorial(n - 1); // 递归步骤
}
// 计算斐波那契数列
int fibonacci(int n)
{
if (n == 0) // 基本情况
return 0;
else if (n == 1) // 基本情况
return 1;
else
return fibonacci(n - 1) + fibonacci(n - 2); // 递归步骤
}
可变参数
C 语言通过 stdarg.h
头文件提供了一组宏来处理可变参数,这些宏包括:
va_list
:声明一个变量来访问可变参数列表。va_start
:初始化一个va_list
变量,使其指向可变参数列表的第一个参数。va_arg
:用于获取可变参数,并且会自动调整指向下一个参数的位置。va_end
:结束可变参数的处理,释放相关资源。
基本结构
#include <stdarg.h> // 包含可变参数处理的宏
返回类型 函数名(固定参数类型, ...) {
// 可变参数处理
}
示例
#include <stdio.h>
#include <stdarg.h>
// 定义一个可变参数函数,计算传入的整数的和
int sum(int count, ...) {
int total = 0;
// 定义可变参数列表变量
va_list args;
// 初始化可变参数列表,'count' 是最后一个固定参数
va_start(args, count);
// 逐个读取可变参数
for (int i = 0; i < count; i++) {
// 获取下一个参数,并假设参数类型是 int
int num = va_arg(args, int);
total += num;
}
// 结束可变参数的处理
va_end(args);
return total;
}
int main() {
// 调用可变参数函数,传入不同数量的参数
printf("Sum of 2, 3, 4: %d\n", sum(3, 2, 3, 4)); // 输出 9
printf("Sum of 10, 20: %d\n", sum(2, 10, 20)); // 输出 30
return 0;
}
va_list args
:声明了一个 va_list
类型的变量 args
,用于存储参数列表。
va_start(args, count)
:初始化 args
,将其指向可变参数列表的第一个参数。count
是最后一个已知的固定参数,编译器通过它找到可变参数的起始位置。
va_arg(args, int)
:从 args
中读取下一个参数,并假设它是 int
类型。
va_end(args)
:清理 args
资源。必须在结束访问可变参数后调用。
指针作为返回值
和其它的变量作为返回值,函数指针作为返回值的函数可以被const修饰:
const int* f(int n) {
static int m = n + 1;
return &m;
}
// 这样表示返回的指针指向的内容不能被改变,在创建指针的时候也必须使用const int*
函数指针在作为返回值的时候,函数的结构也有不同:
基本结构
指针指向函数的返回值 (*函数名称(函数参数))(指针指向函数的参数) {
...
}
示例
int (*opertation(char op))(int, int) {
return _operate(op)
}
也可以通过typedef
的方式创建函数:
typedef int(*operation)(int, int); //为int(*)(int, int)类型的指针创建一个别名operation
operation f(char op) {
return _operate(op);
}
字符串常量作为参数
字符串常量可以直接作为函数参数传递。在调用函数时传递一个字符串常量,例如 "Hello"
,实际上传递的是该字符串常量的指针,指向字符串常量在内存中的起始地址。
示例
void printMessage(const char *msg) {
printf("%s\n", msg);
}
printMessage("Hello, World!");
这里,字符串 "Hello, World!"
被视为一个指针类型的常量,即 const char *
,指向该字符串在内存中的起始位置。
虽然看起来是直接传递字符串,但实际上传递的是一个指向字符串常量的指针。因此,函数内修改 msg
指针的地址不会影响原始字符串地址,但试图通过 msg
修改字符串内容是错误的行为。传递给函数的指针参数应定义为 const char*
,避免对只读字符串内容的修改。
main函数的参数
在 C 语言中,main
函数是程序的入口点,它的参数可以接收命令行传入的参数。
int main(int argc, char *argv[]);
argc
:表示传递给main
函数的命令行参数的数量,argc
的值至少为 1,因为程序名称会被当作第一个参数传递给程序。argv
:是一个指向字符指针数组的指针,argv[0]
是程序的名称或路径(通常是程序的执行文件名)。
如果在命令行输入:program arg1 arg2 arg3
,那么:
argv[0]
将是"program"
(程序的名称或路径)。argv[1]
将是"arg1"
。argv[2]
将是"arg2"
。argv[3]
将是"arg3"
。
修饰符
signed
:默认用于所有整数类型,表示整数可以为负或正数。
unsigned
:表示整数只能为非负数,因此可以扩展正数的表示范围。
const
:定义一个常量,表示它的值在初始化后不能再被修改。
const int number = 1; // 定义一个int型的常量number,不可被修改
const int *p = &number; // 定义一个指向number的指针,指针指向的内容不可变
int* const p = &number; // 定义一个指向number的指针,指针的地址不可变
const int* const p = &number; // 定义一个指向number的指针,指针的地址不可变,指针指向的内容不可变
volatile
:告诉编译器该变量的值可能会被外部因素修改,因此不能对该变量进行优化。
int i = 1;
int a = i;
...
volatile int z = i;
// 上述所有代码不会被优化为int a = 1;的形式,因为该i指向的地址的值可能会被外部因素修改。
restrict
:用于指针声明,表示该指针是唯一访问某一特定内存区域的指针。
void foo(int *restrict a, int *restrict b) {
*a = *b;
}
//上述代码在不加restrict时,a和b可能会指向同一块内存,a和b被修饰hou,告诉编译器不会发生这种情况,有助于优化。
typedef
:用于为已有的类型创建新的别名。
// typedef 现有类型 新类型名称;
typedef unsigned int uint; // 定义了 uint 作为 unsigned int 的别名
uint a = 1;
typedef enum {
False,
True
} _Bool
_Bool boolen = False;
// 定义_Bool作为枚举的别名
typedef int (*Operation)(int, int); // 定义Operation作为int (*)(int, int)指针的别名
Operation f(int number) {}
inline
:用于内联函数。
inline int add(int x, int y) {
return x+y;
}
int a = add(3, 5);
//如果不加inline,在进行函数调用的时候先进行跳转,在执行,在跳转回原地址。对于add这种简单又高效的函数,执行时间甚至可能小于跳转的时间。加上inline后,int a = add(3,5);会被直接替换成int a = 3 + 5;效率更高。
static
:修饰一个局部变量,该变量的 存储持续性 变为 整个程序的生命周期,但它的 作用域 仍然是局部的,即只在定义它的函数内可见。修饰修饰一个全局变量,变量的 作用域 被限制在定义它的文件内,即它只能在当前源文件中访问,不能被其他文件引用。修饰一个函数,该函数的 作用域 也被限制在定义它的源文件内,无法从其他源文件调用。
预处理指令
#include
:包含头文件。
- 语法:
#include <header.h>
:从系统目录中搜索头文件。#include "header.h"
:从当前目录开始搜索头文件。
#define
:定义宏或常量。
- 语法:
#define MACRO_NAME value
:定义常量宏。#define MACRO_NAME(params) value
:定义带参数的宏。#define MACRO_NAME(params, ...) value(params, __VA_ARGS__)
:定义变参宏
#undef
:取消宏定义。
- 语法:
#undef MACRO_NAME
#ifdef
:如果宏已被定义则编译代码。
- 语法:
#ifdef MACRO_NAME
#ifndef
:如果宏未被定义则编译代码。
- 语法:
#ifndef MACRO_NAME
#if
:条件编译,根据表达式的值决定是否编译代码。
- 语法:
#if expression
#elif
:#else if
的缩写,用于在 #if
条件失败后指定新的条件。
- 语法:
#elif expression
#else
:当之前的 #if
、#ifdef
或 #ifndef
条件失败时执行。
#endif
:结束条件编译块。
#error
:在编译时生成错误消息。
- 语法:
#error "Error message"
#pragma
:为编译器提供特殊指令,如 #pragma once
防止头文件被多次包含。
defined
运算符:用于在 #if
中检查宏是否已定义。
- 语法:
#if defined(MACRO_NAME)