C语言基础知识点总结

此博客的目的主要是总结一下关于C语言的相关知识点,便于知识点的梳理,能更好的

目录

一、数据类型

1.1 位,字节,进制

1.1.1 位(Bit)

1.1.2 字节(Byte)

1.1.3 进制(Numeric Base)

1.1.4 应用和联系

1.2 基本数据类型及其常见范围大小

1.3 浮点型数据

1.3.1 浮点型数据的优点

1.3.2 浮点型数据的缺点

1.3.3 使用场景选择

1.3.4 总结

二、运算符

2.1 运算符说明及优先级

1. 算术运算符

2. 关系运算符

3. 逻辑运算符

4. 位运算符

5. 赋值运算符

6. 其他运算符

三、流程控制

3.1 顺序结构

3.2 选择结构

3.2.1 if语句

3.2.2 switch语句

3.3 循环结构

3.3.1 for 循环

3.3.2 while 循环

3.3.3 do-while 循环

3.4 注意事项

四、函数

4.1 函数定义与声明

4.1.1 函数定义

4.1.2 函数声明

4.2 函数参数与返回值

4.2.1 参数

4.2.2 返回值

4.3 函数调用

4.4 函数指针与数组作为函数参数

4.4.1 函数指针

4.4.2 数组作为函数参数

4.5 特殊函数

4.5.1 main函数

4.5.2 库函数

4.6 注意事项

五、数据结构

5.1 数组(Array)

5.2 结构体(Struct)

5.3 指针(Pointer)

5.4 链表(Linked List)

5.5 栈(Stack)

5.6. 队列(Queue)

六、预处理指令

七、关键字

7.1 数据类型关键字

7.2 控制语句关键字

7.3 存储类型关键字

7.4 其它关键字

八、文件操作

8.1  打开和关闭文件

8.2 读写文件

8.3 文件位置操作

8.4 文件和目录操作

8.5 其他相关函数

九、内存分配

9.1 内存分配的基本概念

9.2 内存分配函数

十、编译流程

10.1 预处理阶段

10.2 编译阶段

10.3 汇编阶段

10.4 链接阶段

10.5 总结

十一、错误处理

11.1 错误码和错误处理函数

11.2 返回值检查

11.3 异常处理

11.4 日志记录

11.5 信号处理

十二、其他


一、数据类型

1.1 位,字节,进制

1.1.1 位(Bit)

        是计算机存储和传输数据的最小单位。它只能表示两种状态:0 或 1。一个位可以表示一个二进制数的最小单元。8个位(bit)可以组成一个字节(byte)。 

1.1.2 字节(Byte)

        字节是计算机中存储数据的基本单位,通常由8个位(bit)组成。字节是计算机中处理数据的基本单元,不同的数据类型在内存中占用的字节数可能不同。

  • 字节的大小:一个字节可以存储256种不同的值(从0到255),或者表示一个字符(例如ASCII码中的一个字符)。
  • 常见用途:字节单位常用于描述文件大小、内存使用、网络数据传输等。

1.1.3 进制(Numeric Base)

        进制是一种表示数字的方式,它决定了数字中的基数和对应的符号。在计算机中,常见的进制有二进制(base-2)、十进制(base-10)、八进制(base-8)和十六进制(base-16)。

  • 二进制(base-2):由0和1组成的进制,是计算机内部数据处理的基础。每一位都表示2的幂次方。
  • 八进制(base-8):由0到7组成的进制,每一位代表8的幂次方。
  • 十进制(base-10):我们平常使用的十进制,由0到9组成,每一位代表10的幂次方。
  • 十六进制(base-16):由0到9以及A到F(或a到f)组成,每一位代表16的幂次方。在计算机编程中常用来表示字节值,因为十六进制可以更紧凑地表示大数字。

1.1.4 应用和联系

  • 数据存储和处理:计算机以位和字节为基础单位存储和处理数据。
  • 进制转换:在计算机编程和数据处理中,经常需要进行不同进制之间的转换,特别是二进制、十进制和十六进制之间的转换。
  • 字节单位:字节常用来表示文件大小、内存占用等,而位则用于描述数据传输速率等。

1.2 基本数据类型及其常见范围大小

        常见的数据类型有:整数类型,浮点数类型,布尔型

类型类型说明字节取值范围
char字符型1-128~127
unsigned char无符号字符型10~255
short短整型,比int类型占用的空间小2-32768~32767
int整型4-2,147,483,648 到 2,147,483,647
unsigned int无符号整型40 到 4,294,967,295
long长整型4-214783648~-214783647
unsigned long无符号长整型40~4294967295
long long长长整型8-2^63~2^63 - 1
float单精度浮点型41.2E-38到3.4E+38
double双精度浮点型82.3E-308到1.7E+308
long double扩展精度浮点型取值范围和字节大小依赖于编译器和操作系统。
bool布尔型10~1

注:“e±38”表示10的±38次方,但实际范围可能会略有不同,具体取决于编译器和硬件的实现

1.3 浮点型数据

        在Linux(或者任何操作系统中),包括单精度(float)和双精度(double)数据类型的原因主要涉及到对数据精度和存储空间的权衡。 

1.3.1 浮点型数据的优点

  1. 表示范围广泛:浮点型数据可以表示非常大或非常小的数值,远远超过整型数据的表示范围。
  2. 精度高:相比于整型数据,浮点型数据能够提供更高的精度,特别是在处理小数时。
  3. 灵活性:浮点型数据在表示数值时更加灵活,可以精确表示很多无法通过整型数据精确表示的小数。
  4. 广泛应用:在科学计算、图形处理、游戏开发、金融分析等多个领域,浮点型数据都是不可或缺的。

1.3.2 浮点型数据的缺点

  1. 精度损失:由于浮点型数据是基于二进制表示的,因此无法精确表示所有的小数。在进行运算时,可能会出现精度损失或舍入误差。
  2. 性能开销:浮点运算通常比整数运算更耗时,因为浮点运算涉及到更复杂的算法和硬件支持。
  3. 存储空间:相比于整型数据,浮点型数据占用更多的存储空间。特别是在处理大量数据时,这可能会成为性能瓶颈。

1.3.3 使用场景选择

  1. 单精度float

    • 适用场景:当处理的数据范围较小,且对精度的要求不高时,可以选择单精度浮点数。例如,在图形渲染、游戏开发等领域,单精度浮点数通常已经足够满足需求。
    • 优点:占用存储空间小,计算速度可能更快(取决于处理器架构)。
    • 缺点:精度相对较低,可能无法满足高精度计算的需求。
  2. 双精度double

    • 适用场景:在科学计算、金融分析等领域,需要处理非常大或非常小的数值,且对计算结果的精度有严格要求时,应选择双精度浮点数。
    • 优点:精度高,能够表示更大范围的数值。
    • 缺点:占用存储空间大,计算速度可能较慢(相比于单精度)。

1.3.4 总结

        在选择浮点型数据时,需要根据具体的应用场景和性能要求来权衡精度、存储空间和计算效率。对于大多数日常应用来说,单精度浮点数已经足够满足需求;而在需要高精度计算的场合,则应选择双精度浮点数。同时,开发者还应注意浮点数的精度损失问题,并在必要时采取适当的措施来减少误差。

        另外,值得注意的是,Linux系统本身并不直接决定浮点型数据的优缺点和使用场景,这些是由浮点型数据的内在特性和应用场景的需求共同决定的。Linux只是提供了一个运行环境和一系列工具,使得开发者能够在其中编写、编译和运行使用浮点型数据的程序。

二、运算符

2.1 运算符说明及优先级

1. 算术运算符

运算符描述结合性示例
+加法左到右a + b
-减法左到右a - b
*乘法左到右a * b
/除法左到右a / b
%取模(求余数)左到右a % b

2. 关系运算符

运算符描述结合性示例
==等于左到右a == b
!=不等于左到右a != b
>大于左到右a > b
<小于左到右a < b
>=大于等于左到右a >= b
<=小于等于左到右a <= b

3. 逻辑运算符

运算符描述结合性示例
&&逻辑与左到右a && b
||逻辑或左到右a || b
!逻辑非右到左!a

4. 位运算符

运算符描述结合性示例
&按位与左到右a & b
|按位或左到右a | b
^按位异或左到右a ^ b
~按位取反右到左~a
<<左移左到右a << b
>>右移左到右a >> b

5. 赋值运算符

运算符描述结合性示例
=赋值右到左a = b
+=加后赋值右到左a += b
-=减后赋值右到左a -= b
*=乘后赋值右到左a *= b
/=除后赋值右到左a /= b
%=取模后赋值右到左a %= b
<<=左移后赋值右到左a <<= b
>>=右移后赋值右到左a >>= b
&=按位与后赋值右到左a &= b
^=按位异或后赋值右到左a ^= b
|=按位或后赋值右到左a |= b

6. 其他运算符

  • 条件运算符(三元运算符)

  • condition ? expr1 : expr2;
    
  • 逗号运算符

  • expr1, expr2;
    
  • sizeof运算符

  • sizeof(type); // 返回type类型的大小
    

三、流程控制

3.1 顺序结构

        C语言的顺序结构是程序设计中最基本、最简单的结构。顺序结构按照代码的书写顺序从前到后执行,即程序从上到下依次执行语句,直到结束。在顺序结构中,没有特定的控制流语句(如分支语句或循环语句)来改变执行流程,代码的执行顺序完全取决于语句在程序中的排列顺序。

顺序结构的特点包括:

  1. 自顶向下执行:程序从第一条语句开始执行,按照语句在代码中的顺序逐条执行,直到最后一条语句执行完毕。

  2. 语句块:在C语言中,语句块以花括号{}包裹,表示一个执行单元。顺序结构中的语句可以包含在一个或多个语句块中。

  3. 注释:C语言支持两种注释方式,单行注释以//开始,多行注释以/*开始,以*/结束。注释用于增加程序的可读性,但不会被编译器执行。

  4. 语句类型:顺序结构中的语句可以是表达式语句、函数调用语句、空语句或复合语句等。表达式语句通常包括赋值表达式,函数调用语句用于调用函数执行特定任务,空语句实际上不执行任何操作,复合语句则包含多条语句作为一个执行单元。

  5. 数据输入输出:虽然C语言本身没有直接的输入输出语句,但可以通过调用标准库中的输入输出函数(如printfscanf)来实现数据的输入输出。这些函数调用语句在顺序结构中按照书写顺序执行。

示例代码:

#include <stdio.h>

int main() {
    int a = 5; // 定义并初始化变量a
    int b = 10; // 定义并初始化变量b
    int sum = a + b; // 计算a和b的和,并将结果赋值给变量sum
    printf("The sum of %d and %d is %d\n", a, b, sum); // 输出结果
    return 0; // 程序正常结束
}

在这个示例中,程序按照顺序结构执行了变量定义、赋值、计算和输出操作。这是顺序结构的一个典型应用。顺序结构是构成更复杂程序结构(如分支结构和循环结构)的基础。

3.2 选择结构

        C语言中的选择结构允许程序根据特定条件来执行不同的代码块。选择结构主要有两种形式:if语句和switch语句。

3.2.1 if语句

if语句是C语言中最基本的选择结构,用于根据一个或多个条件来执行不同的代码块。if语句可以有以下几种形式:

  • 单分支:如果条件为真(非零),则执行相应的语句块。

    if (条件表达式) {
        // 条件为真时执行的语句块
    }
    
  • 双分支:如果条件为真,则执行第一个语句块;否则,执行第二个语句块。

    if (条件表达式) {
        // 条件为真时执行的语句块
    } else {
        // 条件为假时执行的语句块
    }
    
  • 多分支:通过结合使用else if语句,可以实现更多的分支选择。

    if (条件表达式1) {
        // 条件表达式1为真时执行的语句块
    } else if (条件表达式2) {
        // 条件表达式1为假且条件表达式2为真时执行的语句块
    } else {
        // 所有条件表达式都为假时执行的语句块
    }
    

3.2.2 switch语句

switch语句是另一种选择结构,它允许一个变量或表达式与多个常量值进行比较,并根据匹配的结果执行不同的代码块。switch语句通常用于处理多个分支的情况,且当条件表达式是整型或字符型时最为有效。

switch (表达式) {
    case 常量1:
        // 表达式等于常量1时执行的语句块
        break; // 用于退出switch语句
    case 常量2:
        // 表达式等于常量2时执行的语句块
        break;
    // 可以有更多的case语句
    default:
        // 没有任何case匹配时执行的语句块
}

注意,每个case语句块后面通常需要跟一个break语句来跳出switch结构,否则程序会继续执行下一个case语句块(称为“穿透”),这通常不是预期的行为。

3.3 循环结构

        在C语言中,循环结构允许程序多次执行特定的代码块,直到满足退出循环的条件为止。主要的循环结构包括 for 循环、while 循环和 do-while 循环。

3.3.1 for 循环

for 循环是C语言中最常用的循环结构,它允许程序根据指定的循环条件重复执行一段代码块。

for (initialization; condition; increment/decrement) {
    // 循环体:只要条件为真,重复执行这里的语句
}
  • initialization:循环开始前执行的语句,通常用于初始化计数器。
  • condition:循环条件,如果为真(非零),则继续执行循环体;如果为假(0),则退出循环。
  • increment/decrement:每次循环后执行的语句,通常用于更新计数器。

例如,计算1到10的和可以使用 for 循环实现:

int sum = 0;
for (int i = 1; i <= 10; ++i) {
    sum += i;
}

3.3.2 while 循环

while 循环在每次执行循环体前先检查条件是否为真,只有在条件为真时才执行循环体。

while (condition) {
    // 只要条件为真,重复执行这里的语句
}

例如,计算1到10的和可以使用 while 循环实现:

int sum = 0;
int i = 1;
while (i <= 10) {
    sum += i;
    ++i;
}

3.3.3 do-while 循环

do-while 循环先执行一次循环体,然后在每次执行循环体前检查条件是否为真,只有在条件为真时才继续执行循环。

do {
    // 先执行这里的语句至少一次
} while (condition);

例如,计算1到10的和可以使用 do-while 循环实现:

int sum = 0;
int i = 1;
do {
    sum += i;
    ++i;
} while (i <= 10);

3.4 注意事项

  • 使用 for 循环通常更适合在已知循环次数时使用,因为它将循环初始化、条件判断和递增/递减操作集中在一起。
  • whiledo-while 循环适合在条件不明确或需要至少执行一次循环体时使用。
  • 在使用任何循环时,要确保循环体内的条件最终会使循环结束,以避免无限循环。

四、函数

         在Linux下使用C语言时,函数是构成程序的基本单元,用于实现特定的功能。以下是对Linux下C语言函数基础知识的详细解析:

4.1 函数定义与声明

4.1.1 函数定义

  • 函数定义包括函数头和函数体两部分。
  • 函数头:包含函数返回类型、函数名和参数列表。函数名后面必须有一对圆括号,圆括号内为形式参数列表,参数列表中的参数称为形式参数(简称形参)。
  • 函数体:位于函数头下面的花括号内,由一系列语句构成,用于实现函数的具体功能。

4.1.2 函数声明

  • 函数声明通常放在源文件的开始部分,用于告诉编译器函数的存在、函数名、参数类型和返回类型。这样,编译器就可以在调用函数之前知道函数的相关信息。
  • 对于标准库函数,通常通过包含相应的头文件来声明。
  • 对于自定义函数,声明格式为:返回类型 函数名(参数类型1 参数名1, 参数类型2 参数名2, ...);
// 函数声明
int add(int a, int b);  // 声明函数 add,返回类型为 int,参数为两个 int 类型

// 函数定义
int add(int a, int b) {  // 定义函数 add,实现对两个整数的加法
    return a + b;
}

4.2 函数参数与返回值

4.2.1 参数

  • 函数参数分为形式参数(形参)和实际参数(实参)。
  • 形参:函数定义时指定的参数,用于接收实参的值。
  • 实参:函数调用时传递给函数的实际值。
  • 在C语言中,实参和形参之间的数据传递是单向的,采用“值传递”方式。这意味着形参的改变不会影响到实参。

4.2.2 返回值

  • 函数可以通过return语句返回一个值给调用者。
  • 如果函数不需要返回值,则可以使用void类型。
  • 返回值类型应与函数头中声明的类型一致。

4.3 函数调用

  • 一般调用: 直接使用函数名后跟圆括号(可包含实参)来调用函数。
  • 嵌套调用: 在一个函数体内调用另一个函数。
  • 链式调用: 一个函数的返回值作为另一个函数的实参。
  • 递归调用: 函数直接或间接调用自身。递归调用需要有明确的终止条件,否则会导致无限递归。

4.4 函数指针与数组作为函数参数

4.4.1 函数指针

  • 函数指针用于存储函数的地址,通过函数指针可以调用函数。
  • 定义格式:返回类型 (*函数指针名)(参数类型列表);

4.4.2 数组作为函数参数

  • 数组名作为函数参数时,实际上传递的是数组首元素的地址。
  • 可以在函数内部通过指针操作数组元素。

4.5 特殊函数

4.5.1 main函数

  • 每个C程序都有一个main函数,作为程序的入口点。
  • main函数的返回类型通常是int,表示程序的退出状态。

4.5.2 库函数

  • Linux下C语言提供了丰富的标准库函数,如stdio.h中的输入输出函数、stdlib.h中的内存分配函数等。
  • 使用库函数前需要包含相应的头文件。

4.6 注意事项

  • 在定义函数时,注意函数名的唯一性和参数列表的匹配。
  • 在调用函数时,确保函数已经被声明或定义。
  • 注意内存管理和指针操作的安全性,避免野指针和内存泄漏等问题。

函数在C语言中是组织和模块化代码的基本单元,了解函数的定义、参数传递方式、返回值、指针、递归、内联和函数库等基础知识点对于有效地编写和理解C语言程序至关重要。 

五、数据结构

        在Linux下使用C语言时,基础数据结构是编程中不可或缺的一部分,它们用于组织和存储数据,以便高效地处理信息。以下是一些常见的基础数据结构及其详细说明:

5.1 数组(Array)

  • 定义:数组是一种基础的数据结构,用于存储固定大小的同类型元素序列。
  • 特点:元素通过索引访问,索引通常是从0开始的整数。
  • 用途:适用于需要快速访问数据项的场景,如排序、搜索等。
  • 示例int arr[10]; 声明了一个包含10个整数的数组。

5.2 结构体(Struct)

  • 定义:结构体是一种复合数据类型,允许将不同类型的数据项组合成一个单一的类型。
  • 特点:结构体中的每个数据项称为成员,可以是基本数据类型或其他结构体。
  • 用途:用于表示具有多个属性的复杂数据对象,如学生信息、员工记录等。
  • 示例
struct Person {
    char name[50];
    int age;
    float height;
};

5.3 指针(Pointer)

  • 定义:指针是存储变量地址的变量。
  • 特点:通过指针可以间接访问和操作内存中的数据。
  • 用途:动态内存分配、函数参数传递、链表实现等。
  • 示例int *ptr; 声明了一个指向整数的指针变量。

5.4 链表(Linked List)

  • 定义:链表是一种动态数据结构,由一系列节点组成,每个节点包含数据部分和指向下一个节点的指针。
  • 特点:链表中的元素在内存中不必连续存储,通过指针进行连接。
  • 用途:适用于需要频繁插入和删除操作的场景。
  • 类型:单向链表、双向链表、循环链表等。

5.5 栈(Stack)

  • 定义:栈是一种后进先出(LIFO)的数据结构。
  • 特点:只能在一端(栈顶)进行插入和删除操作。
  • 用途:函数调用、表达式求值、括号匹配等。
  • 实现:可以使用数组或链表实现。

5.6. 队列(Queue)

  • 定义:队列是一种先进先出(FIFO)的数据结构。
  • 特点:在一端(队尾)进行插入操作,在另一端(队头)进行删除操作。
  • 用途:任务调度、缓冲区管理等。
  • 实现:可以使用数组或链表实现。

        以上数据结构在Linux下的实现可以通过标准C库提供的基本类型和手动实现来完成。对于更复杂的数据结构和算法,可以考虑使用第三方库或自行实现,根据具体需求进行选择和优化。在实际应用中,需要充分考虑性能、内存管理和线程安全等因素。

六、预处理指令

        在C语言中,预处理指令是以#符号开头的特殊指令,它们在编译之前由预处理器执行。预处理器对源代码进行文本替换、条件编译、文件包含等操作,从而生成一个更接近于编译器所需格式的源代码。预处理指令不生成机器代码,也不检查语法错误,它们主要影响的是源代码的结构。下面是一些常见的预处理指令:

  • 文件包含(Include):

        #include指令用于包含(或插入)另一个文件的内容到当前文件中。这对于代码重用和模块化编程非常重要。有两种形式:

#include <filename>:用于包含标准库头文件或其他由编译器提供的头文件。
#include "filename":用于包含用户自定义的头文件。

  •  宏定义(Macro Definition):

        #define指令用于定义宏。宏可以是简单的标识符替换,也可以是带参数的宏(宏函数)。宏定义在预处理阶段进行文本替换,不占用程序运行时间。
示例:

#define PI 3.14159 或 #define MAX(x,y) ((x) > (y) ? (x) : (y))

  • 条件编译(Conditional Compilation):

        条件编译指令允许程序根据不同的条件包含或排除代码段。这对于编写跨平台代码或根据编译时条件(如调试模式与发布模式)包含不同代码非常有用。
常用的条件编译指令包括:

#ifdef、#ifndef、#endif:用于检查宏是否已定义。
#if、#elif、#else、#endif:提供更复杂的条件编译功能,允许使用表达式来控制代码块的包含与否。
#undef:用于取消宏的定义。

  •  行控制(Line Control):

        #line指令允许你重新设定编译器报告的当前行号和文件名。这对于生成的源代码(如由某些工具自动生成)特别有用,因为它可以帮助调试器正确报告源代码位置。

  • 错误消息(Error Directive):

        #error指令允许你在预处理阶段生成一个编译错误消息。这可以用于检查编译条件是否满足,如果不满足,则阻止编译过程。

  •  编译指令(Pragma Directive):

        #pragma指令不是标准C语言的一部分,但它被许多编译器支持,用于提供编译器特定的指令。这些指令通常用于控制编译器的行为,比如优化选项、警告信息等。

        预处理指令是C语言中非常强大的特性,它们允许开发者在编译之前对源代码进行灵活的修改和定制,从而提高了代码的可移植性、可维护性和灵活性。

七、关键字

        C语言的关键字是编程语言中保留的特定词汇,它们具有特定的含义和用途,不能用作变量名、函数名或其他标识符。C语言的关键字共有32个,根据它们的作用,可以分为以下几类:

7.1 数据类型关键字

这些关键字用于声明变量的类型:

  • char:声明字符型变量或函数返回值类型
  • int:声明整型变量或函数
  • float:声明浮点型变量或函数返回值类型
  • double:声明双精度浮点型变量或函数返回值类型
  • long:声明长整型变量或函数返回值类型
  • short:声明短整型变量或函数
  • signed:声明有符号类型变量或函数
  • unsigned:声明无符号类型变量或函数
  • _Bool(C99新增):布尔类型
  • _Complex__Imaginary_(C99新增):复数类型

注意:_Complex__Imaginary_ 在C99标准中引入,但通常使用compleximaginary(需要包含头文件complex.h)来使用复数。

7.2 控制语句关键字

这些关键字用于控制程序的流程:

  • if:条件语句
  • else:条件语句否定分支
  • switch:用于开关语句
  • case:开关语句分支
  • default:开关语句中的“默认”分支
  • while:循环语句的循环条件
  • do:循环语句的循环体(至少执行一次)
  • for:一种循环语句
  • break:跳出当前循环
  • continue:结束当前循环,开始下一轮循环
  • goto:无条件跳转语句

7.3 存储类型关键字

这些关键字用于声明变量的存储类型:

  • auto:声明自动变量(默认)
  • static:声明静态变量
  • register:声明寄存器变量(编译器可忽略此请求)
  • extern:声明变量或函数是在其它文件或本文件的其他位置定义

7.4 其它关键字

  • sizeof:计算数据类型或变量长度(即所占字节数)
  • typedef:用以给数据类型取别名
  • volatile:说明变量在程序执行中可被隐含地改变
  • enum:声明枚举类型
  • struct:声明结构体类型
  • union:声明共用体类型

        C语言的关键字是区分大小写的,并且它们的使用必须符合C语言的语法规则。这些关键字构成了C语言的基础,使得开发者能够编写出结构清晰、功能强大的程序。

八、文件操作

在Linux下,使用C语言进行文件操作涉及到一些系统调用和标准库函数。下面是一些常用的文件操相关函数和系统调用:

8.1  打开和关闭文件

  •  open()

#include <fcntl.h>
int open(const char *pathname, int flags, mode_t mode);

功能:打开或创建一个文件。

参数:

          pathname:文件路径名。

          flags:打开标志,例如 O_RDONLY、O_WRONLY、O_RDWR、O_CREAT、O_APPEND 等。 

         mode:文件权限(只有在创建文件时才有效),例如 S_IRUSR、S_IWUSR 等。

返回值:文件描述符或者出错时返回 -1。

  • clese()

#include <unistd.h>
int close(int fd);

功能:关闭一个打开的文件。

参数:文件描述符 fd

返回值:成功返回 0,出错返回 -1。

8.2 读写文件

  • read()

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);

功能:从文件描述符 fd 所指向的文件中读取数据到 buf 中。

参数:

    fd:文件描述符。

    buf:存放读取数据的缓冲区。

    count:要读取的字节数。

返回值:实际读取的字节数,或者出错时返回 -1。

  • write()

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);

功能:将 buf 中的数据写入文件描述符 fd 所指向的文件中。

参数:

   fd:文件描述符。

   buf:待写入数据的缓冲区。

   count:要写入的字节数。

返回值:实际写入的字节数,或者出错时返回 -1。

8.3 文件位置操作

  •  lseek()

#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);

功能:在文件中移动文件描述符 fd 的读写位置。

参数:

   fd:文件描述符。

   offset:移动的偏移量。

   whence:起始位置,可以是 SEEK_SET(文件开头)、SEEK_CUR(当前位置)、SEEK_END(文件末尾)。

返回值:返回新的文件位置(相对于文件开头的偏移量),或者出错时返回 -1。

8.4 文件和目录操作

  • unlink()

#include <unistd.h>
int unlink(const char *pathname);

功能:删除文件的目录项,减少其链接数。

参数:文件路径名 pathname

返回值:成功返回 0,出错返回 -1。

  • mkdir()

#include <sys/stat.h>
int mkdir(const char *pathname, mode_t mode); 

功能:创建一个新的目录。

参数:

   pathname:目录路径名。

   mode:目录权限。

返回值:成功返回 0,出错返回 -1。

  • rmdir()

#include <unistd.h>
int rmdir(const char *pathname); 

功能:删除一个目录。

参数:目录路径名 pathname

返回值:成功返回 0,出错返回 -1。

8.5 其他相关函数

  • rename()

#include <stdio.h>
int rename(const char *oldpath, const char *newpath);

功能:重命名文件或者移动文件。

参数:

   oldpath:旧文件名或者旧路径。

   newpath:新文件名或者新路径。

返回值:成功返回 0,出错返回 -1。

  •  stat()

#include <sys/stat.h>
int stat(const char *pathname, struct stat *statbuf);

功能:获取文件的状态信息。

参数:

   pathname:文件路径名。

   statbuf:用于存放状态信息的结构体。

返回值:成功返回 0,出错返回 -1。

        这些函数是基本的文件操作和目录操作函数,使用它们可以在C语言程序中实现文件的创建、打开、关闭、读写、移动、删除等功能

      此外还有标准库函数(如fopen()fread()fwrite()fclose()等)可以对文件进行操作 处理。

示例程序:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/time.h>

#define BUF_SIZE 1024

int main() {
    int fd; // 文件描述符
    int new_fd; // 新文件描述符
    ssize_t num_written, num_read;
    off_t offset;
    struct stat file_info;
    char buffer[BUF_SIZE];

    // 创建或打开文件 example.txt,以读写方式打开,不存在则创建,文件权限为644
    fd = open("example.txt", O_RDWR | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
    if (fd == -1) {
        perror("open");
        exit(EXIT_FAILURE);
    }

    // 向文件中写入数据
    num_written = write(fd, "Hello, Linux System Calls!\n", 26);
    if (num_written == -1) {
        perror("write");
        close(fd);
        exit(EXIT_FAILURE);
    }

    // 使用 lseek 设置文件偏移量到文件开头处
    offset = lseek(fd, 0, SEEK_SET);
    if (offset == -1) {
        perror("lseek");
        close(fd);
        exit(EXIT_FAILURE);
    }

    // 读取文件内容
    num_read = read(fd, buffer, BUF_SIZE);
    if (num_read == -1) {
        perror("read");
        close(fd);
        exit(EXIT_FAILURE);
    }
    buffer[num_read] = '\0'; // 添加字符串结束符

    printf("Read from file: %s\n", buffer);

    // 获取文件信息
    if (fstat(fd, &file_info) == -1) {
        perror("fstat");
        close(fd);
        exit(EXIT_FAILURE);
    }

    printf("File size: %ld bytes\n", file_info.st_size);
    printf("File permissions: ");
    printf((S_ISDIR(file_info.st_mode)) ? "d" : "-");
    printf((file_info.st_mode & S_IRUSR) ? "r" : "-");
    printf((file_info.st_mode & S_IWUSR) ? "w" : "-");
    printf((file_info.st_mode & S_IXUSR) ? "x" : "-");
    printf((file_info.st_mode & S_IRGRP) ? "r" : "-");
    printf((file_info.st_mode & S_IWGRP) ? "w" : "-");
    printf((file_info.st_mode & S_IXGRP) ? "x" : "-");
    printf((file_info.st_mode & S_IROTH) ? "r" : "-");
    printf((file_info.st_mode & S_IWOTH) ? "w" : "-");
    printf((file_info.st_mode & S_IXOTH) ? "x" : "-");
    printf("\n");

    // 关闭文件
    if (close(fd) == -1) {
        perror("close");
        exit(EXIT_FAILURE);
    }

    // 重命名文件
    if (rename("example.txt", "new_example.txt") == -1) {
        perror("rename");
        exit(EXIT_FAILURE);
    }

    printf("File renamed successfully.\n");

    // 打开重命名后的文件
    new_fd = open("new_example.txt", O_RDONLY);
    if (new_fd == -1) {
        perror("open");
        exit(EXIT_FAILURE);
    }

    // 读取重命名后的文件内容并输出到标准输出
    while ((num_read = read(new_fd, buffer, BUF_SIZE)) > 0) {
        if (write(STDOUT_FILENO, buffer, num_read) != num_read) {
            perror("write");
            close(new_fd);
            exit(EXIT_FAILURE);
        }
    }

    if (num_read == -1) {
        perror("read");
        close(new_fd);
        exit(EXIT_FAILURE);
    }

    // 关闭新文件
    if (close(new_fd) == -1) {
        perror("close");
        exit(EXIT_FAILURE);
    }

    return 0;
}

编译和运行程序:

将以上代码保存为 file_operations_advanced.c 文件。

在终端中使用以下命令编译程序:

gcc file_operations_advanced.c -o file_operations_advanced

然后运行生成的可执行文件:

./file_operations_advanced

        这样就能看到程序创建文件 example.txt,向其中写入内容,进行定位、获取文件信息、重命名文件为 new_example.txt,然后读取重命名后的文件内容并输出到标准输出

九、内存分配

        在Linux系统中,内存分配是系统编程和应用程序开发中非常重要的一部分。下面是关于Linux中内存分配的总结:

9.1 内存分配的基本概念

        内存分配是指程序在运行时获取和释放内存的过程。在Linux中,内存分配通常涉及到以下几个方面:

  • 用户空间和内核空间:Linux系统将内存划分为用户空间和内核空间。用户空间用于运行用户程序,而内核空间用于操作系统内部和驱动程序的执行。

  • 虚拟内存:Linux通过虚拟内存管理来实现内存分配。每个进程看到的地址空间都是虚拟的,操作系统负责将虚拟地址映射到物理内存。

9.2 内存分配函数

        在Linux中,常用的内存分配函数包括:malloc / calloc / realloc / free:这些函数属于C标准库,用于动态分配和释放内存。它们的实现可以通过系统调用 brk()mmap() 来获取内存。

malloc(size_t size)

  • 功能:分配 size 字节的内存空间。
  • 返回值:返回一个指向分配内存起始地址的指针。如果分配失败,则返回 NULL

calloc(size_t num, size_t size)

  • 功能:分配 num 个大小为 size 字节的连续内存空间,总共分配 num * size 字节。
  • 返回值:返回一个指向分配内存起始地址的指针。分配成功后,内存被初始化为零。

realloc(void *ptr, size_t size)

  • 功能:重新分配之前分配的内存空间 ptr,大小调整为 size 字节。
  • 返回值:返回一个指向重新分配后的内存起始地址的指针。如果扩展失败,原始内存块保持不变,并返回 NULL

free(void *ptr)

  • 功能:释放之前通过 malloccallocrealloc 函数分配的内存空间。
  • 参数:ptr 是指向要释放的内存块的指针。如果 ptrNULL,则不执行任何操作。

注:使用这些内存分配函数时,需要注意以下几点:

  • 检查返回值:始终检查分配函数的返回值,确保内存分配成功。

  • 避免内存泄漏:每次分配内存后,确保及时释放不再需要的内存块,避免内存泄漏。

  • 处理分配失败:分配函数可能会返回 NULL 或错误码,应该适当处理这些错误情况。

  • 内存对齐:对于某些硬件或特定操作,确保分配的内存按照要求进行适当的对齐,以提高性能和兼容性。

通过合理使用这些函数和系统调用,可以实现高效、可靠的内存管理,满足各种应用程序的需求。

十、编译流程

Linux中的编译过程涉及从源代码到可执行程序的多个步骤,主要包括预处理、编译、汇编、链接等阶段。下面详细说明每个阶段的工作和关键步骤:

10.1 预处理阶段

预处理阶段由预处理器处理,其主要任务是对源代码进行预处理,生成经过宏展开、包含文件展开等处理后的中间代码。关键步骤包括:

  • 宏展开:将所有的宏(例如 #define 定义的宏)在代码中展开。
  • 头文件包含:将 #include 指令包含的头文件内容插入到源文件中。
  • 条件编译:处理 #ifdef#ifndef#if#else#endif 等条件编译指令。
  • 注释删除:删除源文件中的注释。

生成的输出通常是以 .i 结尾的预处理后的源代码文件。

10.2 编译阶段

编译阶段由编译器处理,将预处理后的源代码转换为汇编代码。主要步骤包括:

  • 词法分析:将源代码分割成一个个标记(token),如关键字、标识符、常量等。
  • 语法分析:根据编程语言的语法规则分析标记的结构,生成抽象语法树(AST)。
  • 语义分析:检查语法树是否符合语言的语义规则,如类型检查。
  • 优化:对生成的中间代码进行优化,包括提高执行速度、减少代码大小等优化。

生成的输出通常是以 .s 结尾的汇编代码文件。

10.3 汇编阶段

汇编阶段由汇编器处理,将汇编代码翻译成机器可以执行的指令。主要步骤包括:

  • 将汇编指令翻译为机器码:将每条汇编语句翻译成对应的机器码指令。
  • 生成目标文件:将翻译后的机器码以二进制形式存储在目标文件中。通常目标文件以 .o 结尾。

10.4 链接阶段

链接阶段由链接器处理,将多个目标文件和库文件链接成一个完整的可执行程序。主要步骤包括:

  • 符号解析:解析目标文件和库文件中使用的符号(函数名、全局变量等),确定其在内存中的位置。
  • 地址重定位:将代码和数据的地址映射到正确的内存位置,处理符号引用。
  • 生成可执行文件:将所有的目标文件和库文件链接起来,生成最终的可执行程序文件。

在Linux中,常见的链接器是 GNU ld(GNU链接器)。

10.5 总结

编译过程是将高级语言源代码转换为机器语言可执行文件的关键过程,涉及预处理、编译、汇编和链接等多个阶段,每个阶段都有特定的输入和输出。深入理解编译过程有助于开发者更好地理解代码背后的工作原理,并优化程序的性能和可维护性。

十一、错误处理

        在Linux系统中,错误处理是编程中不可或缺的一部分,特别是在系统编程、系统管理、应用程序开发等领域。Linux系统提供了多种机制来处理错误,主要包括以下几个方面:

11.1 错误码和错误处理函数

        Linux系统定义了大量的错误码(errno),每个错误码对应一个特定的错误情况。这些错误码通常定义在 <errno.h> 头文件中,可以在程序中使用全局变量 errno 访问最近一次系统调用失败的错误码。常见的错误码包括:

  • EINTR:系统调用被信号中断。
  • EINVAL:无效参数。
  • ENOMEM:内存不足。
  • 等等。

        程序可以通过错误码来判断和处理不同的错误情况,例如使用 perror() 函数输出错误信息,或者使用 strerror() 函数将错误码转换为可读的错误描述。

#include <stdio.h>
#include <errno.h>
#include <string.h>

int main() {
    FILE *fp = fopen("nonexistent_file.txt", "r");
    if (fp == NULL) {
        perror("Error opening file");
        fprintf(stderr, "Error code: %d\n", errno);
        fprintf(stderr, "Error message: %s\n", strerror(errno));
    }
    return 0;
}

11.2 返回值检查

        系统调用和库函数通常会返回指示成功或失败的值。在调用这些函数后,程序应该检查返回值以确定操作是否成功。如果失败,可以通过错误码来进一步诊断和处理错误。

#include <stdio.h>
#include <unistd.h>

int main() {
    if (access("/path/to/file", F_OK) == -1) {
        perror("Error accessing file");
        fprintf(stderr, "Error code: %d\n", errno);
    }
    return 0;
}

11.3 异常处理

        在程序中使用异常处理可以有效地处理一些特定的错误情况,例如内存分配失败、系统调用中断等。C语言本身并不支持异常处理,但可以通过一些技巧来模拟异常处理的机制,比如使用 setjmp()longjmp() 函数来实现非局部跳转。

11.4 日志记录

        在系统编程中,日志记录是一种常见的错误处理方式。通过记录详细的日志信息,可以帮助开发者诊断和排查程序运行时出现的问题,尤其是在分布式系统和长时间运行的服务中。

11.5 信号处理

        Linux系统中的信号机制允许进程处理各种异步事件,包括错误情况。通过注册信号处理函数,程序可以在收到特定信号时采取相应的措施,例如保存数据、清理资源等。

        Linux系统中的错误处理是程序设计中重要的一部分,开发者需要熟悉错误码、返回值检查、异常处理、日志记录等多种技术,以确保程序能够在面对各种异常情况时表现稳定和可靠

十二、其他

        linux下的编程,除了上述总结的,还有结构体,链表,排序算法,动态库,静态库,进程,线程,网络编程等等。后续会慢慢整理总结。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值