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;
}

对于上述的代码,不仅可以正确执行,每次的结果也可能是不同的,也可能有相同的。

基本数据类型

  • 整形shortintlonglong long
  • 字符:char
  • 浮点型:floatdoublelong 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段,不可更改,和用staticconst修饰的字符串也有一些不同。

基本结构

char *pointerName = "str";

其中pointerName为指向字符串的指针,指针的值为在rdata段"str"存放的首地址。

示例

char *str = "fff团";

要点

  1. 字符串常量即使定义在函数中,也会被定义到rdata段或rodata段,而不会被放到堆栈中,等同于被staticconst修饰。
  2. 字符串常量之间如果存在相同的,则会直接使用已经存在的,以节约空间,但使用仅限于由通过上述所阐述的结构定义的字符串常量,以及通过函数参数传递的字符串常量。对于通过被staticconst修饰的字符串所定义的,并不会参与,并且字符串之间也不存在。
  3. 字符串常量在作为函数参数的时候,可以直接传递一个字符串。虽然看起来是直接传递字符串,但实际上传递的是一个指向字符串常量的指针。

结构体

结构体是一种用户定义的数据类型,它可以将不同类型的变量组合成一个实体。可以用来定义一个对象的多种属性。

语法

// 声明结构体类型
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; // 定义一个指向 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 之前自增它的值。

比较运算符

比较运算符用于比较两个值之间的关系,并返回一个布尔值(truefalse)。

  • ==:等于运算符,用于检查两个值是否相等。
  • !=:不等于运算符,用于检查两个值是否不相等。
  • >:大于运算符,用于检查左操作数是否大于右操作数。
  • <:小于运算符,用于检查左操作数是否小于右操作数。
  • >=:大于等于运算符,用于检查左操作数是否大于或等于右操作数。
  • <=:小于等于运算符,用于检查左操作数是否小于或等于右操作数。

逻辑运算符

逻辑运算符用于执行逻辑运算,操作数和结果都是布尔值。

  • &&:逻辑与运算符,当且仅当两个操作数都为 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)