MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C语言基础语法与编译过程解析

2023-09-111.5k 阅读

C语言基础语法

1. 数据类型

C语言提供了丰富的数据类型,用于存储不同种类的数据。

  • 基本数据类型
    • 整型:用于表示整数。在C语言中,常见的整型有int(通常为4字节)、short(通常为2字节)和long(通常为4字节或8字节,取决于系统)。例如:
int num1 = 10;
short num2 = 20;
long num3 = 30L; // L后缀表示long类型
  • 浮点型:用于表示小数。主要有float(单精度,通常为4字节)和double(双精度,通常为8字节)。float类型的数值精度有限,而double能提供更高的精度。示例:
float f1 = 3.14f; // f后缀表示float类型
double d1 = 3.141592653589793;
  • 字符型:用于表示单个字符,使用char类型,通常占1字节。字符型数据在内存中以ASCII码值的形式存储。例如:
char ch = 'A';
  • 布尔型:C99标准引入了<stdbool.h>头文件,提供了bool类型来表示布尔值(truefalse)。示例:
#include <stdbool.h>
bool flag = true;
  • 派生数据类型
    • 数组:是一组相同类型数据的集合。例如,定义一个整型数组:
int arr[5] = {1, 2, 3, 4, 5};
  • 指针:指针是一个变量,其值为另一个变量的内存地址。例如:
int num = 10;
int *ptr = &num; // ptr指向num的地址
  • 结构体:结构体是一种自定义的数据类型,它可以将不同类型的数据组合在一起。例如:
struct Person {
    char name[20];
    int age;
};
struct Person p1 = {"John", 30};
  • 联合体:联合体也是一种自定义数据类型,它允许不同的数据类型共享同一块内存空间。例如:
union Data {
    int i;
    float f;
};
union Data d1;
d1.i = 10;
// 此时d1.f的值是未定义的,因为内存布局按int处理后再按float解读会有问题

2. 变量与常量

  • 变量:变量是程序中用于存储数据的标识符。变量在使用前必须先声明其类型。例如:
int count;
float price;

变量可以在声明时初始化,也可以在后续代码中赋值。例如:

int num = 5;
num = 10;
  • 常量:常量是在程序执行过程中值不能被改变的量。C语言中有多种常量:
    • 整型常量:如10-20等。
    • 浮点型常量:如3.14-0.5等。
    • 字符常量:用单引号括起来的单个字符,如'a''Z'
    • 字符串常量:用双引号括起来的字符序列,如"Hello, world!"
    • 符号常量:使用#define预处理指令或const关键字定义的常量。例如:
#define PI 3.141592653589793
const int MAX_COUNT = 100;

3. 运算符

C语言提供了丰富的运算符,用于执行各种算术、逻辑、比较等操作。

  • 算术运算符:包括+(加)、-(减)、*(乘)、/(除)和%(取模)。例如:
int a = 10, b = 3;
int sum = a + b;
int quotient = a / b;
int remainder = a % b;

注意,整数除法会截断小数部分,例如10 / 3结果为3

  • 赋值运算符:最基本的是=,用于将右侧的值赋给左侧的变量。还有复合赋值运算符,如+=-=*=/=%=等。例如:
int num = 5;
num += 3; // 等价于 num = num + 3;
  • 比较运算符:用于比较两个值,结果为布尔值(真或假,在C语言中用1和0表示)。包括==(等于)、!=(不等于)、>(大于)、<(小于)、>=(大于等于)和<=(小于等于)。例如:
int a = 10, b = 20;
bool result1 = (a == b); // false
bool result2 = (a < b); // true
  • 逻辑运算符&&(逻辑与)、||(逻辑或)和!(逻辑非)。逻辑与和逻辑或运算符具有短路特性。例如:
int a = 5, b = 10;
bool result1 = (a > 10) && (b < 20); // false,因为a > 10为假,不再判断b < 20
bool result2 = (a < 10) || (b > 20); // true,因为a < 10为真,不再判断b > 20
bool result3 =!(a == b); // true
  • 位运算符:用于对二进制位进行操作。包括&(按位与)、|(按位或)、^(按位异或)、~(按位取反)、<<(左移)和>>(右移)。例如:
int a = 5; // 二进制为 00000101
int b = 3; // 二进制为 00000011
int result1 = a & b; // 二进制为 00000001,结果为1
int result2 = a << 2; // 二进制为 00010100,结果为20

4. 控制语句

  • 条件语句
    • if - else:根据条件执行不同的代码块。例如:
int num = 10;
if (num > 5) {
    printf("Number is greater than 5\n");
} else {
    printf("Number is less than or equal to 5\n");
}
  • switch - case:用于多分支选择。switch表达式的值与各个case常量表达式的值进行匹配,匹配成功则执行相应的代码块。例如:
int day = 3;
switch (day) {
    case 1:
        printf("Monday\n");
        break;
    case 2:
        printf("Tuesday\n");
        break;
    case 3:
        printf("Wednesday\n");
        break;
    default:
        printf("Invalid day\n");
}

注意,每个case分支后通常需要使用break语句来跳出switch结构,否则会继续执行下一个case分支的代码。

  • 循环语句
    • for循环:常用于已知循环次数的情况。例如,计算1到10的和:
int sum = 0;
for (int i = 1; i <= 10; i++) {
    sum += i;
}
printf("Sum is %d\n", sum);

for循环的三个部分分别是初始化表达式、条件表达式和递增/递减表达式。

  • while循环:先判断条件,条件为真则执行循环体。例如,打印1到5的数字:
int num = 1;
while (num <= 5) {
    printf("%d ", num);
    num++;
}
  • do - while循环:先执行一次循环体,然后判断条件,条件为真则继续执行循环体。例如:
int num = 1;
do {
    printf("%d ", num);
    num++;
} while (num <= 5);

do - while循环至少会执行一次循环体。

5. 函数

函数是C语言中实现模块化编程的重要工具,它将一段独立的代码封装起来,实现特定的功能。

  • 函数定义:函数由函数头和函数体组成。例如,一个简单的加法函数:
int add(int a, int b) {
    return a + b;
}

这里int是函数的返回类型,add是函数名,(int a, int b)是函数的参数列表,函数体中的return语句返回计算结果。

  • 函数调用:在其他函数中可以调用已定义的函数。例如:
int result = add(3, 5);
printf("Result of addition is %d\n", result);
  • 函数声明:在调用函数之前,需要先声明函数。函数声明通常放在源文件的开头或头文件中,它告诉编译器函数的名称、参数类型和返回类型。例如:
int add(int a, int b); // 函数声明
int main() {
    int result = add(3, 5);
    printf("Result of addition is %d\n", result);
    return 0;
}
int add(int a, int b) {
    return a + b;
}
  • 递归函数:函数可以调用自身,这种函数称为递归函数。例如,计算阶乘的递归函数:
int factorial(int n) {
    if (n == 0 || n == 1) {
        return 1;
    } else {
        return n * factorial(n - 1);
    }
}

C语言编译过程解析

1. 编译概述

C语言代码从源文件到可执行文件需要经过多个步骤,主要包括预处理、编译、汇编和链接。每个步骤都由相应的工具完成,在Linux系统下常用的编译器是GCC,在Windows系统下可以使用MinGW等工具。

2. 预处理

预处理是编译的第一个阶段,预处理器(如GCC中的cpp)会对源文件中的预处理指令进行处理。

  • 预处理指令
    • #include:用于包含头文件。头文件通常包含函数声明、类型定义等内容。例如,#include <stdio.h>包含标准输入输出库的头文件,使得程序可以使用printfscanf等函数。有两种包含头文件的方式:
      • 尖括号<>:用于包含系统头文件,预处理器会在系统指定的目录中查找头文件。
      • 双引号"":用于包含用户自定义的头文件,预处理器首先在当前目录中查找,若找不到再到系统目录中查找。
    • #define:用于定义宏。宏可以是常量宏,如#define PI 3.141592653589793,也可以是带参数的宏,例如:
#define SQUARE(x) ((x) * (x))
int result = SQUARE(5); // result为25

注意,带参数的宏在使用时要注意括号的使用,避免宏展开时出现错误。

  • #ifdef#ifndef#endif:用于条件编译。例如:
#ifdef DEBUG
    printf("Debug mode: This is a debug message\n");
#endif

这段代码只有在定义了DEBUG宏时才会执行printf语句。#ifndef则相反,只有在未定义指定宏时才执行相应代码。

预处理的结果是一个经过宏替换、头文件展开和条件编译处理后的中间文件,通常扩展名为.i。例如,对于源文件test.c,执行gcc -E test.c -o test.i命令会生成test.i文件,该文件包含了展开后的所有代码。

3. 编译

编译阶段,编译器(如GCC中的cc1)将预处理后的文件编译成汇编代码。编译器会对代码进行词法分析、语法分析、语义分析以及优化等操作。

  • 词法分析:将源程序的字符流按照词法规则识别成一个个单词。例如,对于代码int num = 10;,词法分析器会识别出int(关键字)、num(标识符)、=(运算符)、10(常量)和;(界符)等单词。

  • 语法分析:根据语法规则对单词序列进行分析,构建出语法树。例如,对于表达式a + b * c,语法分析器会构建出一棵反映该表达式运算顺序的语法树,乘法运算b * c的节点会在加法运算a + (b * c)的子节点位置。

  • 语义分析:检查语法结构的语义是否正确,例如类型检查。如果代码中写了int num = "hello";,语义分析会发现将字符串赋值给整型变量是错误的。

  • 优化:编译器会对代码进行优化,以提高代码的执行效率。例如,对于循环中的不变表达式,编译器可能会将其提到循环外部,减少重复计算。

编译的结果是生成汇编代码文件,通常扩展名为.s。例如,执行gcc -S test.i -o test.s命令会将test.i编译成test.s汇编代码文件。

4. 汇编

汇编阶段,汇编器(如GCC中的as)将汇编代码转换为目标机器的机器语言,生成目标文件,通常扩展名为.o。汇编代码是一种低级的符号化的机器语言,每条汇编指令对应一条或几条机器指令。

例如,对于x86架构的处理器,汇编代码mov eax, 10可能对应机器指令B8 0A 00 00 00(将立即数10加载到寄存器eax中)。汇编器会根据目标机器的指令集将汇编代码翻译成机器可执行的二进制指令,并生成目标文件。执行gcc -c test.s -o test.o命令会将test.s汇编代码文件生成test.o目标文件。

5. 链接

链接阶段,链接器(如GCC中的ld)将一个或多个目标文件以及所需的库文件链接成一个可执行文件。

  • 库文件:C语言程序常常需要使用标准库或其他第三方库。库文件分为静态库和共享库(动态库)。
    • 静态库:在链接时,链接器会将静态库中被程序调用的函数代码直接复制到可执行文件中。静态库通常扩展名为.a。例如,在Linux系统下,标准C库的静态库文件可能是libc.a
    • 共享库:共享库在链接时不会将其代码复制到可执行文件中,而是在程序运行时由系统动态加载。共享库通常扩展名为.so(在Linux系统下)或.dll(在Windows系统下)。共享库的优点是多个程序可以共享同一库的代码,节省内存空间。

链接器会解析目标文件中的符号引用,将各个目标文件和库文件中的代码和数据整合在一起,生成最终的可执行文件。例如,执行gcc test.o -o test命令会将test.o目标文件链接成可执行文件test。如果程序中使用了标准库函数,链接器会自动链接标准库文件。

通过以上预处理、编译、汇编和链接的过程,C语言源文件最终转化为可以在目标机器上运行的可执行文件。理解这些过程有助于程序员更好地优化代码、解决编译链接错误以及管理项目中的代码和库文件。