学习笔记,从Java到C语言入门,总结一篇

177 阅读41分钟

从Java到C语言入门笔记

前言

在现代编程领域,Java 和 C语言作为两种广泛使用的编程语言,各自扮演着重要的角色。作为一名 Java 语言的从业者,我深知两者在语法、底层逻辑以及应用场景上的显著差异。虽然 Java 和 C 在许多方面没有直接关系,但它们之间的学习关系却可以为我们提供一个更加高效的学习路径。

作为一名 Java 从业者,你是否曾好奇过,为什么 Java 没有指针概念?为什么 Java 要用 JVM “保护”你的指针操作?为什么你的对象在堆内存中看似安全,却失去了对内存的直接掌控感?

而类似 Java 的一些现代编程语言都在努力隐藏的底层细节,如指针,内存回收,却是C语言,或者说是嵌入式、操作系统等领域的必备生存技能。

当你习惯了 GC 的自动呵护,C语言将带你重回原始森林,我们学习C语言之后,每一块内存的生死都由你亲手掌控,通过 malloc 与 free 的博弈,你将真正理解 Java 堆栈内存划分的本质,甚至可以对 JVM 的 GC 策略产生新的认知。

在接下来的内容中,我将逐步介绍 C 语言的结构、数据类型、逻辑控制、指针与内存管理、函数、结构体以及标准库等方面的知识。在每一部分,我会强调 C 语言的独特之处,结合 Java 的相关知识,让您在学习过程中能够快速建立起对 C 语言的认知。

一、C语言的结构

1.1 头文件与函数

在 C 语言中,头文件(Header Files)是包含函数声明、宏定义和类型定义的文件。它们通常以 .h 为扩展名,主要用于提供接口供其他程序文件使用。

在 Java 中,使用 import 语句来引入类和包,例如 import java.util.ArrayList 只是声明了类的路径以便在当前类中使用。

当你在C代码中写下 #include <stdio.h>,预处理器会直接将头文件内容复制到当前文件中:

#include <stdio.h> // 标准输入输出头文件
#include <stdlib.h> // 标准库头文件

int main(int argc, char *argv[]) {
    // 程序逻辑
    return 0;
}

C 语言的头文件引入主要有两种方式:

  1. 使用尖括号 <> 来引入标准库头文件,编译器会在系统的标准库目录中查找这些文件。例如:
#include <stdlib.h> // 引入标准库头文件

2. 使用双引号 "" 来引入用户自定义的头文件,编译器首先在当前项目目录查找该文件,如果找不到,再去标准库目录中查找。例如:

#include "log.h" // 引入当前项目中的日志头文件

这种方式适用于项目中自定义的头文件,如 log.h、utils.h 等。使用双引号时,可以灵活地将项目相关的头文件组织在一起,便于项目的管理和维护。

简单来说我们使用系统的库就用尖括号 <> 来引入,如果使用我们自定义的头文件就用使用双引号 "" 来引入。

C 语言程序的执行从 main() 函数开始,这是程序的入口点,main() 函数的返回值通常是整数,返回 0 表示程序成功执行,返回非零值则表示错误。当从 main() 函数返回时,程序的执行结束,操作系统会根据返回值确定程序的运行状态。

这一点和 Java 类似就不多说了。

1.3 注释风格与代码规范

注释在代码中起着重要的文档作用,C 语言提供了两种注释风格:

单行注释:使用 // 开始,直到行末为止。

多行注释:使用 /* 开始,使用 */ 结束,可以跨越多行。

// 这是一个单行注释

/*
这是一个多行注释
可以用于详细描述
*/

C99 标准引入了对单行注释的支持,因此在现代 C 语言开发中,单行注释已广泛使用。良好的注释风格和代码规范对于提高代码的可读性和可维护性至关重要。

这一点和 Java 类似就不多说了。

1.4 作用域规则(全局变量 vs Java类变量)

作用域是变量的可见性范围。C 语言中的作用域规则主要包括全局变量和局部变量的区别。

全局变量:在函数外部定义的变量,可以在文件中的任何函数中访问。全局变量的生命周期贯穿整个程序的运行,使用时需要谨慎,以免引起命名冲突和其他不可预料的错误。

int globalVar = 10; // 全局变量

extern int globalVar; // 声明外部全局变量

void function() {
    printf("%d\n", globalVar); // 可以访问
}

注意如果要整个程序可见需用extern声明。

静态全局变量

static int fileScopedVar = 100; // 仅当前文件可见

类似Java的 private static 但作用域是文件.

局部变量:在函数内部定义的变量,仅在该函数内有效,超出函数范围后便会被销毁。

void function() {
    int localVar = 5; // 局部变量
    printf("%d\n", localVar);
}
1.5 define与常量定义

宏定义的底层逻辑就是文本的替换,在 C 语言中,我们可以使用 #define 预处理指令来定义常量。它在编译期间替换文本,不占用内存。

#define PI 3.14159

int main() {
    double radius = 5.0;
    double area = PI * radius * radius; // 使用定义的常量 PI
    printf("Area of the circle: %.2f\n", area);
    return 0;
}

当然 define 除了定义文本变量,也可以是表达式或函数。

表达式:

#define SQUARE(x) ((x) * (x))

int main() {
    int result = SQUARE(2 + 3); // 现在正确扩展为 ((2 + 3) * (2 + 3)) = 25
    printf("Result: %d\n", result); // 输出: 25
    return 0;
}

函数替换:

#define MAX(a, b) ((a) > (b) ? (a) : (b))

int main() {
    int x = 10, y = 20;
    int maxVal = MAX(x, y); // 替换为 ((10) > (20) ? (10) : (20))
    printf("Max value: %d\n", maxVal); // 输出: Max value: 20
    return 0;
}

需要注意的是 define 宏在使用时不进行类型检查,因此在使用时必须谨慎,若出现错误,可能难以追踪,对于复杂的表达式和逻辑,建议使用函数替代宏以提高安全性和可读性。总之就是用于简单易读的场景。

二、C语言的基本数据类型

经典的C语言数据类型:

image.png

2.1 基本数据类型
int       : 占据的内度存大小是2byte
short int : 占据的内度存大小是4byte
long int  : 占据的内度存大小是4byte
float     : 占据的内度存大小是4byte
double    : 占据的内度存大小是8byte
char      : 占据的内度存大小是1byte

unsigned int 取值范围          :0~65535
unsigned short int 取值范围    :0~65535
unsigned long int 取值范围     :0~4294967295

int 取值范围           :-32768~+32767
short int 取值范围     :-32768~+32767
long int 取值范围      :-2147483648~+2147483647

float(单精度)float 小数部分取值范围:最多只能精确到小数点后 6 位;
double(双精度) double 小数部分取值范围:最多只能精确到小数点后 15 位;

最常用的整型, 实型与字符型(char,int,float,double):

image.png

整型数据是指不带小数的数字(int,short int,long int, unsigned int, unsigned short int,unsigned long int):

image.png

C语言的类型灵活性

signed int a = -10;    // 有符号整型(默认)
unsigned int b = 4294967295; // 无符号整型(0~2^32-1)

对比Java的固定符号:Java的 int 始终为有符号,char 隐式无符号(但以UTF-16存储)

临界值陷阱:

unsigned char c = 255; // 取值范围 0~255
c = c + 1;            // 溢出为0(Java会编译报错)

C语言注意不要超过取值范围。

  • sizeof运算符的实际意义

sizeof运算符:类型大小的“标尺”

#include <stdio.h>

int main() {
    printf("Size of int: %zu bytes\n", sizeof(int));
    printf("Size of char: %zu bytes\n", sizeof(char));
    printf("Size of double: %zu bytes\n", sizeof(double));
    return 0;
}

Java的 Integer.SIZE 固定为32位(与平台无关),而C的 sizeof(int) 可能因编译器或系统变化。

在不同的平台和编译器中,基本数据类型的大小可能会有所不同,因此使用 sizeof 可以提高程序的可移植性。

2.2 格式化输出类型

C 语言提供了多种格式化输出选项,使用 printf 函数可以输出不同类型的数据,常见的格式化类型包括:

  • %d:输出带符号的十进制整数。
  • %u:输出无符号的十进制整数。
  • %f:输出浮点数。
  • %c:输出字符。
  • %s:输出字符串。
  • %p:输出指针。

示例:

#include <stdio.h>

int main() {
    int a = 10;
    float b = 5.5;
    char c = 'A';
    char str[] = "Hello, World!";

    printf("Integer: %d\n", a);
    printf("Float: %.2f\n", b);
    printf("Character: %c\n", c);
    printf("String: %s\n", str);
    
    return 0;
}

我们最常用的还是 %d %f %s 这些,通过格式化输出提供了灵活的方式来显示数据,使得调试和用户交互更加友好。

2.3 自动类型转换

语言支持自动类型转换,通常在表达式中,编译器会根据操作数的大小进行转换

  • 小类型向大类型的自动转换:如 char 可以自动转换为 int,int 可以自动转换为 double。
  • 大类型向小类型不能自动转换:如 double 不会自动转换为 int,因为这可能导致精度丢失。
#include <stdio.h>

int main() {
    char ch = 'A';            // char 类型
    int i = ch;              // 自动转换为 int
    double d = 5.5;          // double 类型
    int j = d;               // 不允许,需强制转换

    printf("Character as int: %d\n", i);
    printf("Double: %f\n", d);
    // printf("Int from double: %d\n", j); // 错误:会提示需要强制类型转换

    return 0;
}

2.4 强制类型转换

强制类型转换是指在 C 语言中,使用类型转换运算符 () 来显式地将一个数据类型转换为另一个类型。这可以在需要更改数据类型时使用

#include <stdio.h>

int main() {
    double d = 9.7;
    int i = (int)d; // 强制转换为 int,结果为 9

    printf("Double: %f\n", d);
    printf("Converted int: %d\n", i); // 输出: 9

    return 0;
}

强制类型转换可以用于控制数据的精度和范围

与Java的差异:

Java强制转换仅限继承关系((String)obj),而C允许任意类型转换,包括指针和整数互转

// 危险操作:指针类型转换
int num = 0x12345678;
char *p = (char*)# 
printf("%x", *p); // 输出78(小端模式,Java无法直接操作)
2.5 数组的操作

数组是 C 语言中用于存储固定大小的元素集合的一种数据结构。数组的操作包含多个方面:

在 C 语言中,数组名在大多数情况下被视为指向数组第一个元素的指针。

#include <stdio.h>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    int *p = arr; // 数组名 arr 被视为指针

    printf("First element: %d\n", *p); // 输出: 1
    p++; // 指针移动
    printf("Second element: %d\n", *p); // 输出: 2

    return 0;
}

数组的指针移动会指向数组内部的元素,我们在使用数组时要注意数组名与指针之间的关系,避免引发混淆和错误。

数组的长度:

#include <stdio.h>

int main() {
    int arr[] = {1, 2, 3, 4, 5}; // 正确的数组声明
    int length = sizeof(arr) / sizeof(arr[0]); // 计算数组长度

    printf("Length of the array: %d\n", length); // 输出数组长度
    return 0;
}

数组的遍历:

#include <stdio.h>

int main() {
    int arr[] = {1, 2, 3, 4, 5}; // 正确的数组声明
    int length = sizeof(arr) / sizeof(arr[0]); // 计算数组长度

    printf("Length of the array: %d\n", length); // 输出数组长度

    // 遍历并打印数组的每个值
    printf("Elements of the array: ");
    for (int i = 0; i < length; i++) {
        printf("%d ", arr[i]); // 打印每个元素
    }
    printf("\n"); // 换行

    return 0;
}

可以说数组和 Java 有相似的更多的是不同,我们还是需要注意差异化。

关于遍历与指针的问题,这里先简单过一下,我们后面会着重介绍。

2.6 字符串的操作

这里单独来一小章说一下 C 语言的字符串,在 C 语言中没有真正的字符串类型,字符串本质是 char 数组,字符串是以空字符 '\0' 结尾的字符数组。

这使得字符串在 C 语言中的操作与数组的操作有很多相似之处。

在 C 语言中,字符串通常通过字符数组定义,并且以 '\0' 作为结束标志。例如:

char str[] = "Hello, World!";

这里,str 是一个字符数组,实际存储的内容是 {'H', 'e', 'l', 'l', 'o', ',', ' ', 'W', 'o', 'r', 'l', 'd', '!', '\0'}。注意,字符串的长度并不是字符数组的大小,而是数组中实际字符数(不包括 '\0')。

字符串初始化与赋值的陷阱:

char stackStr[] = "Hello"; // 栈区数组,可修改
char *ptrStr = "Hello";    // 指针指向常量区,不可修改
stackStr = 'h';        // 合法
ptrStr = 'h';          // 运行时崩溃(Java的String类似常量区)

但是我们可以把指针指向一个字符串数组,也就是栈区数组

char arr = "Hi"; // 剩余元素自动填充\0
char *p = arr;       // p指向栈区数组
p = 'h';         // 合法

这样就是可以的。

标准库函数中字符串的常用操作API:

strlen:返回字符串的长度(不包括 '\0')

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

int main() {
    char str[] = "Hello";
    printf("Length of string: %lu\n", strlen(str)); // 输出: 5
    return 0;
}

strcpy:将源字符串复制到目标字符串,注意目标字符串必须足够大以存储源字符串及其结束符。

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

int main() {
    char src[] = "Hello";
    char dest[20]; // 确保目标数组足够大
    strcpy(dest, src);
    printf("Copied string: %s\n", dest); // 输出: Hello
    return 0;
}

strcat:将源字符串附加到目标字符串的末尾,这里也需要注意前者的容器是否足够大。

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

int main() {
    char str1[20] = "Hello, ";
    char str2[] = "World!";
    strcat(str1, str2);
    printf("Concatenated string: %s\n", str1); // 输出: Hello, World!
    return 0;
}

strcmp:比较两个字符串,返回值小于、等于或大于零,表示字典序的关系

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

int main() {
    char str1[] = "apple";
    char str2[] = "banana";
    int result = strcmp(str1, str2);
    if (result < 0) {
        printf("'%s' is less than '%s'\n", str1, str2);
    } else if (result > 0) {
        printf("'%s' is greater than '%s'\n", str1, str2);
    } else {
        printf("'%s' is equal to '%s'\n", str1, str2);
    }
    return 0;
}

strchr:查找字符在字符串中首次出现的位置

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

int main() {
    char str[] = "Hello, World!";
    char *ptr = strchr(str, 'W');
    if (ptr != NULL) {
        printf("Found 'W' at position: %ld\n", ptr - str); // 输出: 7
    } else {
        printf("'W' not found\n");
    }
    return 0;
}

由于之前我们说过,C语言没有类似 Java 那样的 String 字符串对象,C语言的都是Char 数组,所以我们可以对字符串做遍历:

#include <stdio.h>

int main() {
    char str[] = "Hello, World!"; // 定义一个字符串

    printf("Traversing the string:\n");
    
    // 使用 for 循环遍历字符串
    for (int i = 0; str[i] != '\0'; i++) {
        printf("Character at index %d: %c\n", i, str[i]); // 打印当前字符和索引
    }

    return 0;
}

在实际编程中,这种遍历方式非常常用,可以用于查找字符、进行字符替换、或者进行其他字符串操作。

三、C语言的逻辑控制

在 C 语言中,逻辑控制用于控制程序的执行流。通过不同的控制结构,程序可以根据条件执行不同的代码段,重复执行某些操作,并处理错误情况。以下是 C 语言中常用的逻辑控制结构。

3.1 自增与自减运算符

自增运算符 ++ 和自减运算符 -- 用于对变量进行加一和减一的操作。

自增运算符:

形式:++variable 或 variable++

前缀自增(++variable):在使用变量前增加 1。

后缀自增(variable++):在使用变量后增加 1。

自减运算符:

形式:--variable 或 variable--

前缀自减(--variable):在使用变量前减少 1。

后缀自减(variable--):在使用变量后减少 1。

#include <stdio.h>

int main() {
    int a = 5;
    printf("a: %d\n", a++);  // 输出: 5
    printf("a: %d\n", a);     // 输出: 6

    int b = 5;
    printf("b: %d\n", ++b);   // 输出: 6
    printf("b: %d\n", b);     // 输出: 6

    return 0;
}

3.2 赋值运算符

赋值运算符 = 用于将右侧的值赋给左侧的变量。除了基本的赋值运算符外,C 语言还提供了复合赋值运算符,如 +=, -=, *=, /=, %= 等。

#include <stdio.h>

int main() {
    int x = 10;
    x += 5; // 等价于 x = x + 5
    printf("x: %d\n", x); // 输出: 15

    x *= 2; // 等价于 x = x * 2
    printf("x: %d\n", x); // 输出: 30

    return 0;
}

3.3 关系运算符

关系运算符用于比较两个值,返回布尔值(真或假)。常用的关系运算符有:

  • ==:相等
  • !=:不相等
  • :大于

  • <:小于
  • =:大于等于

  • <=:小于等于
#include <stdio.h>

int main() {
    int a = 5, b = 10;
    if (a < b) {
        printf("a is less than b\n"); // 输出: a is less than b
    }
    if (a != b) {
        printf("a is not equal to b\n"); // 输出: a is not equal to b
    }

    return 0;
}

3.4 逻辑运算符

逻辑运算符用于对布尔值进行逻辑运算,主要包括:

  • &&:逻辑与(AND)
  • ||:逻辑或(OR)
  • !:逻辑非(NOT)
#include <stdio.h>

int main() {
    int a = 1, b = 0;

    if (a && !b) {
        printf("Both conditions are true\n"); // 输出: Both conditions are true
    }

    if (a || b) {
        printf("At least one condition is true\n"); // 输出: At least one condition is true
    }

    return 0;
}

3.5 三目运算符

三目运算符(条件运算符)是一种简洁的条件表达式,形式为 condition ? expression1 : expression2。如果 condition 为真,则计算 expression1,否则计算 expression2。

#include <stdio.h>

int main() {
    int a = 5;
    int b = (a < 10) ? a : 10; // 如果 a 小于 10,b 赋值为 a,否则赋值为 10
    printf("b: %d\n", b); // 输出: b: 5

    return 0;
}

3.6 分支逻辑 if else

if 语句用于根据条件的真值执行不同的代码块。基本结构如下:

#include <stdio.h>

int main() {
    int score = 85;

    if (score >= 90) {
        printf("Grade: A\n");
    } else if (score >= 80) {
        printf("Grade: B\n");
    } else {
        printf("Grade: C\n");
    }

    return 0;
}

3.7 遍历循环 while 与 do while 与 for

while 循环在条件为真时重复执行代码块。

#include <stdio.h>

int main() {
    int i = 0;
    while (i < 5) {
        printf("%d ", i);
        i++;
    }
    return 0; // 输出: 0 1 2 3 4
}

do while 循环至少会执行一次代码块,然后检查条件。

#include <stdio.h>

int main() {
    int i = 0;
    do {
        printf("%d ", i);
        i++;
    } while (i < 5);
    return 0; // 输出: 0 1 2 3 4
}

for 循环提供了一种更紧凑的语法形式用于遍历。

#include <stdio.h>

int main() {
    for (int i = 0; i < 5; i++) {
        printf("%d ", i);
    }
    return 0; // 输出: 0 1 2 3 4
}

3.8 逻辑分支 switch

switch 语句用于根据变量的不同值执行不同的代码块。每个 case 后跟要比较的值。

#include <stdio.h>

int main() {
    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("Other day\n");
    }

    return 0; // 输出: Wednesday
}
3.9 无条件分支 goto

goto 语句用于无条件地跳转到程序中的某个标签。虽然可以实现简单的跳转,但过多的使用会导致代码难以理解,通常应慎用。

#include <stdio.h>

int main() {
    int i = 0;

    loop_start:
    if (i < 5) {
        printf("%d ", i);
        i++;
        goto loop_start; // 跳转到 loop_start 标签
    }

    return 0; // 输出: 0 1 2 3 4
}

最后讲一下goto争议与合理使用场景(错误处理跳转)

使用 goto 可能导致程序的控制流变得复杂,使代码难以理解和维护,同时在处理错误时,goto 语句可以实现从深层嵌套中快速跳出,简化错误处理逻辑。所以在统一处理错误的场景倒是很适用。

#include <stdio.h>
#include <stdlib.h>

int main() {
    FILE *file = fopen("example.txt", "r");
    if (file == NULL) {
        goto error; // 文件打开失败,跳转到错误处理
    }

    // 文件操作...
    fclose(file);
    return 0;

error:
    printf("Error opening file\n");
    return 1;
}

在以上示例中,如果文件打开失败,程序会跳转到 error 标签,执行相应的错误处理代码。

总的来说,逻辑控制这一章节和 Java 的逻辑控制很类似,我这里就是简单的过一下。

四、C语言的指针与内存

4.1 指针基础

指针的格式:

type *pointer_name;
  • type 是指针所指向的数据类型。
    • 表示这是一个指针。

示例:

int *ptr;    // 声明一个指向整数的指针
double *dptr; // 声明一个指向双精度浮点数的指针

要使指针指向某个变量,可以使用取地址运算符 &

int a = 10;
ptr = &a; // ptr 现在指向变量 a 的地址

简单来说就是获取此变量在内存中的地址。

指针取值:

int value = *ptr; // 通过指针获取 a 的值

指针的类型决定了解引用后返回的值的类型。

指针宽度与平台相关性:

printf("指针大小: %zu\n", sizeof(p)); // 32位系统输出4,64位输出8

野指针灾难

int *wild_ptr;       //  未初始化的指针
*wild_ptr = 100;     // 可能覆盖关键内存区域

未初始化的指针,指向随机地址,可能导致覆盖其他内存区域。推荐初始化指针不用的时候设置NULL

指针本质:直接操作内存地址。

指针提供了一种直接内存操作的方式。在 C 语言中,指针可以用于高效的数组处理、动态内存管理以及函数参数传递等场景。与 Java 等语言不同,C 语言没有内置的安全机制来防止指针错误,因此程序员需要小心使用。

4.2 动态内存管理

C 语言支持动态内存分配,程序员可以在运行时根据需要分配和释放内存。这主要通过标准库函数 malloc、calloc 和 free 实现。

malloc:分配指定字节数的未初始化内存。

int *arr = (int *)malloc(5 * sizeof(int)); // 分配足够存储 5 个整数的内存

calloc:分配并初始化为零的内存。

int *arr = (int *)calloc(5, sizeof(int)); // 分配并初始化为 0

使用 free 函数释放之前分配的内存

free(arr); // 释放动态分配的内存

在 Java 中,内存管理是自动的,使用垃圾回收器(GC)来回收不再使用的对象。与此相比,C 语言的手动内存管理需要开发者自行负责内存的分配和释放,因此更容易出现内存泄漏、野指针等问题。

堆栈的概念:

image.png

栈内存,由编译器自动分配/释放,通常很小,适合存储小数据(如局部变量)。

// 示例1:栈上分配int和数组
void stackExample() {
    int a = 10; // 栈内存分配(自动回收)
    char buffer; // 栈数组(可能溢出)
}

// 危险案例:栈溢出
void stackOverflow() {
    int huge[1024 * 1024]; // 尝试分配4MB栈内存(Windows崩溃)
}

堆内存,堆内存通常比栈内存大,适合存储大型对象(如结构体数组、动态数组等),使用动态内存分配,内存由程序员通过 malloc、calloc 等函数手动分配。

// 示例2:堆上分配结构体
struct Student {
    int id;
    char name;
};

void heapExample() {
    // 栈上分配结构体指针(4/8字节)
    struct Student *s = malloc(sizeof(struct Student));
    s->id = 1001;
    strcpy(s->name, "Alice");
    free(s); // 必须手动释放
}

// 示例3:堆上分配大数组
void safeBigData() {
    int *bigArray = malloc(1024 * 1024 * sizeof(int)); // 4MB堆内存
    // 使用后必须释放
    free(bigArray);
}

4.3 指针与数组

在 C 语言中,数组名代表数组的首地址,数组可以通过指针进行访问和操作。数组和指针之间有密切的关系,数组的元素可以用指针进行遍历。

int arr[] = {1, 2, 3, 4, 5};
int *ptr = arr; // ptr 指向数组的首元素

for (int i = 0; i < 5; i++) {
    printf("%d ", *(ptr + i)); // 通过指针访问数组元素
}

前面的数组变量我们使用的 for 循环的方式,这里我们通过指针的方式,通过指针的运算变动进行的遍历,并通过 * 取出当前指针的值。

4.4 指针与字符串

通过指针遍历字符串的例子:

#include <stdio.h>

int main() {
    char str[] = "Hello, World!";
    char *ptr = str; // 指向字符串的首地址

    while (*ptr != '\0') { // 直到遇到结束符
        printf("%c ", *ptr); // 打印当前字符
        ptr++; // 移动到下一个字符
    }

    return 0;
}

其实和 Java 的 charAt 很像,只是 Java 隐藏了底层实现,C 直接操作内存。

所以指针是 C 语言的灵魂,但也是危险之源,需严格遵循“谁分配谁释放”原则,动态内存管理必须配对使用malloc/free。

五、C语言的函数

在 C 语言中,函数是基本的代码组织单位,可以将重复使用的代码封装在一起,提高代码的可读性和可维护性。函数可以接收参数、返回值,并可以通过指针实现更复杂的功能。

5.1 自定义函数与参数

自定义函数的基本语法如下:

return_type function_name(parameter_type parameter1, parameter_type parameter2, ...) {
    // 函数体
    return return_value; // 如果 return_type 不是 void,则必须返回值
}
  • return_type:函数返回值的数据类型。
  • function_name:自定义的函数名称。
  • parameter_type:参数的数据类型。
  • parameter1、parameter2:函数的输入参数。

示例代码:

#include <stdio.h>

// 自定义函数,计算两个整数的和
int add(int a, int b) {
    return a + b;
}

int main() {
    int result = add(5, 3);
    printf("Result: %d\n", result); // 输出: Result: 8
    return 0;
}

函数的缺省:参数的默认值

int sub(int x=100,int y=5)
{
    return (x-y);
}

半缺省,从右往左

int sub(int x,int y=5)
{
    return (x-y);
}

不定长函数?可变参数?用的比较少,了解即可。主要是通过下面的方式定义可变参数和取出可变参数

  • va_list va_arg
  • va_start
  • va_end
#include <stdio.h>
#include <stdarg.h>

// 不定长函数,第一个参数是字符串,...是可变参数
void WriteFrmtd(char *format, ...)
{
    // 用于存储可变参数列表
    va_list args;
    // 指向函数参数列表中的第一个可选参数,赋值传参的第一个固定参数
    va_start(args, format);
    // 打印,用于根据提供的格式字符串 format 打印输出,这里没有用va_arg取值,直接吧可变参数放入系统API中使用了
    vprintf(format, args);
    // 清空参数列表, 并置参数指针args无效.
    va_end(args);
}

// 不定长函数,第一个参数是字符串,...是可变参数,这里是用va_arg取值的方式
void WriteFrmtd2(char *format, ...)
{
    // 用于存储可变参数列表
    va_list args;
    // 指向函数参数列表中的第一个可选参数,赋值传参的第一个固定参数
    va_start(args, format);
    //上面是直接调用可变参数,这里我可以直接用va_arg把可变参数取出来
    printf("WriteFrmtd2 content : %s ,var arg : %s \n", format, va_arg(args, char *));
    // 清空参数列表, 并置参数指针args无效.
    va_end(args);
}

// 第一个参数是10,也就是遍历的数量,后面...才是可变参数,va_arg 从可变参数中取值
int Sum(int Num, ...)
{
    int S = 0, T;
    va_list ap;
    va_start(ap, Num);
    for (int i = 0; i < Num; ++i)
    {
        T = va_arg(ap, int);
        S += T;
    }
    return S;
}

// 第一个参数是2,遍历的数量,后面的...才是可变参数,va_arg 从可变参数中取值
void myPrintf(int Num, ...)
{
    va_list ap;
    va_start(ap, Num);
    for (int i = 0; i < Num; i++)
    {
        char *T = va_arg(ap, char *);
        printf("myPrintf:%s\n", T);
    }
}

int main()
{
    WriteFrmtd("%d variable argument\n", 1);
    WriteFrmtd("%d variable %s\n", 2, "arguments");

    WriteFrmtd2("Newki", "New bee");

    myPrintf(2, "Hello", "World");
    printf("%d\n", Sum(10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10));

    return (0);
}
5.2 形参与实参

在 C 语言中,函数参数的传递有两种方式:值传递与指针传递。

值传递:函数接收参数的副本,修改副本不会影响原始变量。

#include <stdio.h>

void increment(int num) {
    num++; // 只修改 num 的副本
}

int main() {
    int a = 5;
    increment(a);
    printf("Value of a: %d\n", a); // 输出: Value of a: 5
    return 0;
}

这里就分为形参与实参,简单的说在使用值传递时,实参的值会被复制到形参中。函数内部对形参的任何修改都不会影响实际参数的值。换句话说,函数对形参的操作只是在内存中创建了一个副本。


void modify(int param) {
    param = 100; // 修改形参(栈上的副本)
}

int main() {
    int actual = 50;
    modify(actual);
    printf("%d", actual); // 输出50(未改变实参)
}

函数调用时,实参的值被复制到栈帧中的形参位置,形参是独立副本

指针传递:函数接收参数的地址,允许在函数内部修改原始变量

#include <stdio.h>

void increment(int *num) {
    (*num)++; // 修改原始变量
}

int main() {
    int a = 5;
    increment(&a); // 传递 a 的地址
    printf("Value of a: %d\n", a); // 输出: Value of a: 6
    return 0;
}

相对而言,指针传递允许函数直接访问和修改实参的值。通过传递指向变量的指针,函数可以更改原始变量的值。这是通过传递地址来实现的,使得函数可以操作原始数据。

5.3 函数指针与指针函数

函数指针:是指向函数的指针,可以用来动态选择要调用的函数。

return_type (*pointer_name)(parameter_type1, parameter_type2, ...);

示例代码:

#include <stdio.h>

// 自定义函数
int add(int a, int b) {
    return a + b;
}

int main() {
    // 定义函数指针
    int (*func_ptr)(int, int) = add;
    
    // 通过函数指针调用函数
    int result = func_ptr(5, 10);
    printf("Result: %d\n", result); // 输出: Result: 15
    return 0;
}

指针函数是返回指针的函数

type *function_name(parameter_type parameter);

示例:

#include <stdio.h>
#include <stdlib.h>

// 返回指向动态分配整数的指针
int *create_array(size_t size) {
    return (int *)malloc(size * sizeof(int)); // 动态分配内存
}

int main() {
    int *arr = create_array(5); // 创建一个数组

    // 初始化数组
    for (size_t i = 0; i < 5; i++) {
        arr[i] = i + 1;
    }

    // 打印数组
    for (size_t i = 0; i < 5; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    free(arr); // 释放内存
    return 0;
}

函数指针可以当做Kotlin的高阶函数一样,可以当参数传递给另一个函数,更加的灵活

int add(int x, int y)
{
    return x + y;
}

//如果不用指针,直接用函数是无法当做参数的
void execute(int a, int b, int (*operation)(int, int))
{
    int result = operation(a, b);
    printf("Result: %d\n", result);
}

使用:  execute(5, 3, &add); 

5.4 递归函数

递归函数是直接或间接调用自身的函数。递归通常有两个重要部分:基准情况和递归情况。

#include <stdio.h>

// 递归函数:计算阶乘
int factorial(int n) {
    if (n <= 1) {
        return 1; // 基准情况
    } else {
        return n * factorial(n - 1); // 递归情况
    }
}

int main() {
    int num = 5;
    printf("Factorial of %d: %d\n", num, factorial(num)); // 输出: Factorial of 5: 120
    return 0;
}

  1. 递归时必须有基准情况来避免无限递归
  2. 递归的每一层调用都会占用栈内存,过深的递归可能导致栈溢出
5.5 回调函数实现(对比Java接口)

回调函数是将函数作为参数传递给另一个函数。C 语言中,回调函数的实现方式与 Java 中的接口概念类似。

#include <stdio.h>

// 回调函数类型
typedef void (*Callback)(int);

// 实现一个函数,接收回调函数
void process(int value, Callback cb) {
    cb(value); // 调用回调函数
}

// 回调函数实现
void my_callback(int result) {
    printf("Callback called with value: %d\n", result);
}

int main() {
    process(10, my_callback); // 传递回调函数
    return 0; // 输出: Callback called with value: 10
}

示例2:

//相当于匿名的内部函数,在C语言中表现为匿名的函数指针
void populate_array(int *array, size_t arraySize, int(*getNextValue)(void)) {
    printf("array 地址:%p \n", array);
    for (size_t i = 0; i < arraySize; i++) {
        array[i] = getNextValue();
        printf(" array[%zu] ,存储值:%d \n", i, array[i]);
    }
} 

//获取一个随机数
int getNextRandomValue(void) {
    return rand();
} 

 //调用
void test7(void) {
    //回调函数
    int array[10];
    printf("Int array 地址:%p \n", array);
    int count  =(sizeof(array)/sizeof(array[0]));
    printf("===============数组存储========================\n");
    populate_array(array, count, getNextRandomValue);
    printf("===============遍历数组========================\n");
    for (int i = 0; i < count; ++i) {
        printf(" array[%d] , 对应值为:%d \n", i, array[i]);
    } 
}

扩展 typedef 的概念

如果用了 typedef 相当于定义一个类型,需要用类型(对象)取接收

typedef  int (*callBackFunc)(char* name);

int callback(char* name)
{
    printf("我名字是%s....\n",name);
    return 1;
}

int main()
{
   callBackFunc ff = callback;
    ff((char*)"胡歌");
    ff((char*)"刘亦菲");
   return 0;
}

如果不用 typedef ,相当于全局变量,直接赋值

int (*nameCallack)(char *name);

int callback(char* name)
{
    printf("我名字是%s....\n",name);
    return 1;
}

int main()
{
    nameCallack = callback;
    nameCallack((char *)"Hu ge");
    nameCallack((char *)"Newki");

    return 0;
}

在 Callback 这个场景,个人还是推荐使用 typedef 定义 “回调” 函数。

六、C语言的结构体

6.1 结构体的声明与定义

结构体的声明与初始化

// 方式1:先声明后定义
struct Point {
    int x;
    int y;
};
struct Point p1 = {10, 20}; // 顺序初始化

// 方式2:声明时定义
struct Book {
    char title;
    double price;
} book1, book2; // 同时定义变量

指定初始化

struct Student s = {
    .name = "Alice",
    .id = 1001,
    .score = 90.5 // 顺序无关
};

示例:

struct weapon
{
    // 武器名字
    char name[20];
    // 攻击力
    int atk;
    // 价格
    int price;
}

int main()
{
    struct weapon weapon_1 = {"weapon_name", 100, 200};
    // 访问结构体里的成员,使用点
    printf("%s\n,%d\n", weapon_1.name, ++weapon_1.price);

    struct weapon weapon_2;
    strcpy(weapon_2.name, "AK47");
    weapon_2.atk = 200;
    weapon_2.price = 2000;
}

这里演示了两种类型的结构体的创建与初始化。

6.2 结构体与函数与指针

结构体可以作为函数的参数传递。可以使用值传递,也可以使用指针传递

示例代码(值传递):

#include <stdio.h>

struct Person {
    char name[50];
    int age;
};

void displayPerson(struct Person p) {
    printf("Name: %s, Age: %d\n", p.name, p.age);
}

int main() {
    struct Person person1 = {"Bob", 25};
    displayPerson(person1); // 传递结构体的副本
    return 0;
}

示例代码(指针传递):

#include <stdio.h>

struct Person {
    char name[50];
    int age;
};

void updateAge(struct Person *p, int newAge) {
    p->age = newAge; // 使用指针修改原始结构体
}

int main() {
    struct Person person1 = {"Charlie", 22};
    printf("Before update: Name: %s, Age: %d\n", person1.name, person1.age);
    
    updateAge(&person1, 30); // 传递结构体的地址
    printf("After update: Name: %s, Age: %d\n", person1.name, person1.age);
    return 0;
}

结构体指针:

可以创建指向结构体的指针,使用结构体指针可以更灵活地访问和修改结构体的成员。

struct Person *p = &person1; // 创建指向 person1 的指针
printf("Name: %s, Age: %d\n", p->name, p->age); // 使用 '->' 访问成员

示例:

#include <stdio.h>
struct weapon
{
    // 武器名字
    char name[20];
    // 攻击力
    int atk;
    // 价格
    int price;
} weapon_100;

int main()
{
    int a = 0;
    float b = 0.0;

    struct weapon weapon_1 = {"weapon_name", 100, 200};
    // 访问结构体里的成员,使用点
    printf("%s\n,%d\n", weapon_1.name, ++weapon_1.price);

    // 每个元素都是一个结构体类型的数据
    struct weapon weapon_2[2] = {{"weapon_name1", 50, 100}, {"weapon_name2", 99, 200}};
    printf("%s\n%d\n", weapon_2[0].name, weapon_2[1].atk);

    // 结构体也有指针
    struct weapon *w; // 定义一个指向weapon的w结构体指针  和 int *i一样的 struct weapon 代表的就是一个数据类型
    w = &weapon_1;    // 具体指向weapon_1的内存地址

    // (*w).name;
    // w->name; // 实际上是(*w).name 的简写,(*w).name 和 w->name 都是有效的方式来访问指针结构体的成员属性
    printf("%s %s\n", (*w).name, w->name);
    //通过对象和指针都能访问结构体中的属性,一个是用.一个是用->
    return 0;
}

结构体的属性访问,直接点 weapon.price

结构体指针访问属性 (*w).name (指针取值)再点或者 w->name 直接用指针对象->表达式

6.3 define 预处理器

在 C 语言中,预处理器用于处理源代码中的指令,通常以 # 开头。预处理器指令在编译前处理源代码,常见的预处理器指令包括:

  • #include:用于包含头文件。
  • #define:用于定义宏。
  • #ifdef / #ifndef:用于条件编译。

这里主要讲 define ,之前我们讲过变量与函数的定义

#include <stdio.h> // 包含标准输入输出库

#define PI 3.14  // 定义宏 PI

int main() {
    printf("Value of PI: %.2f\n", PI); // 输出: Value of PI: 3.14
    return 0;
}

这里主要讲结构体中的 define 的定义

typedef struct Books {
    char *title;    // 将 title 定义为字符指针
    char *author;   // 将 author 定义为字符指针
    char *subject;  // 将 subject 定义为字符指针
    int book_id;
} Book;

通过使用 typedef,您可以为结构体类型创建一个新的名称(别名)。在您的示例中,您将结构体 Books 的类型重命名为 Book,如下所示:

Book book1; // 直接使用 Book,而不需要 struct Books

示例:

#include <stdio.h>
#include <stdlib.h>

typedef struct Books {
    char *title;
    char *author;
    char *subject;
    int book_id;
} Book;

int main() {
    // 使用 typedef 创建 Book 类型的结构体变量
    Book book1;

    // 为书籍的信息分配内存并赋值
    book1.title = "C Programming";
    book1.author = "Dennis Ritchie";
    book1.subject = "Programming Languages";
    book1.book_id = 101;

    // 输出书籍的信息
    printf("Title: %s\n", book1.title);
    printf("Author: %s\n", book1.author);
    printf("Subject: %s\n", book1.subject);
    printf("Book ID: %d\n", book1.book_id);

    return 0;
}

使用 typedef 为结构体定义别名(如 Book),可以方便后续使用,省略 struct 关键字,这样的做法提高了代码的可读性和简洁性,并使得程序更易于维护。

6.4 匿名结构体

匿名结构体是没有名称的结构体,通常用于快速定义结构体而不需要在程序的其他地方引用

#include <stdio.h>

// 定义匿名结构体
struct {
    char name[50];
    int age;
} person;

int main() {
    snprintf(person.name, sizeof(person.name), "David");
    person.age = 28;

    printf("Name: %s, Age: %d\n", person.name, person.age); // 输出: Name: David, Age: 28
    return 0;
}

另一个示例:

struct {
    int x;
    int y;
} point; // 直接定义变量,无类型名

// 嵌套匿名结构体
struct Car {
    char model;
    struct { // 匿名引擎结构体
        int cylinders;
        float power;
    } engine;
};

struct Car car;
car.engine.cylinders = 4; // 直接访问
6.5 嵌套结构体

嵌套结构体是指一个结构体的成员是另一个结构体。这种方式可以更好地组织复杂的数据。

#include <stdio.h>

// 定义 Address 结构体
struct Address {
    char street[100];
    char city[50];
};

// 定义 Person 结构体,其中包含 Address 结构体
struct Person {
    char name[50];
    int age;
    struct Address addr; // 嵌套结构体
};

int main() {
    struct Person person1 = {"Eve", 35, {"123 Main St", "Wonderland"}};

    // 输出结构体成员
    printf("Name: %s, Age: %d\n", person1.name, person1.age);
    printf("Address: %s, %s\n", person1.addr.street, person1.addr.city); // 输出: Address: 123 Main St, Wonderland
    return 0;
}

在这个示例中,Person 结构体的成员 addr 是另一个结构体 Address,用于存储地址信息。

C 语言结构体 VS Java对象

image.png

结构体并不是对象,算是半(伪)对象,结构体只是数据聚合而已,用于组织数据,更像是一种简单的数据结构,对结构体的操作通常是通过函数来完成。

6.6 联合体

联合体(Union)是C语言中一种特殊的复合数据类型,虽然使用频率低于结构体,但在内存敏感型场景和硬件级编程中具有不可替代的作用。

image.png

union Data {                struct Data {
    int i;      // 4字节       int i;      // 4字节
    float f;    // 4字节       float f;    // 4字节
    char str;//20字节      char str;//20字节
};                         }; // 总大小=28字节
// 总大小=20字节(取最大成员)

当您需要一个变量来表示多种不同类型的数据时,联合体是一个理想选择。同时联合体也是可以和结构体结合使用的:

结构体嵌套联合体:

#include <stdio.h>

typedef union {
    int intValue;
    float floatValue;
    char charValue;
} Data;

typedef struct {
    char type; // 'i' for int, 'f' for float, 'c' for char
    Data value;
} Variant;

int main() {
    Variant v1, v2, v3;

    // 存储整数
    v1.type = 'i';
    v1.value.intValue = 10;

    // 存储浮点数
    v2.type = 'f';
    v2.value.floatValue = 3.14;

    // 存储字符
    v3.type = 'c';
    v3.value.charValue = 'A';

    // 打印结果
    printf("Variant 1: Type %c, Value %d\n", v1.type, v1.value.intValue);
    printf("Variant 2: Type %c, Value %.2f\n", v2.type, v2.value.floatValue);
    printf("Variant 3: Type %c, Value %c\n", v3.type, v3.value.charValue);

    return 0;
}

例如嵌入式开发中直接操作硬件寄存器,联合体嵌套结构体:

typedef union {
    struct {
        uint32_t mode    : 3;  // 低3位
        uint32_t enable  : 1;  // 第4位
        uint32_t reserved: 28; // 高28位
    } bits;
    uint32_t raw; // 完整32位寄存器
} ControlReg;

// 操作寄存器位域
ControlReg reg;
reg.bits.mode = 0x5;
reg.bits.enable = 1;
printf("寄存器值: 0x%08X\n", reg.raw);

虽然在实际编程中联合体的使用频率可能不如结构体高,但在某些特定场景下如在嵌入式开发、协议解析等领域,需要节省内存或处理多种数据类型时,联合体是非常有用的工具。

七、C语言准库重点模块

7.1 字符串处理

相关头文件

#include <string.h>

常用函数

  1. strlen:计算字符串长度
  2. strcpy:字符串复制
  3. strcat:字符串连接
  4. strcmp:字符串比较
  5. strstr:查找子字符串

在第二章的字符串处理我们已经讲过,这里不做过多的演示

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

int main() {
    char str1[50] = "Hello, ";
    char str2[] = "World!";

    // 连接字符串
    strcat(str1, str2);
    printf("Concatenated String: %s\n", str1); // 输出: Hello, World!

    // 计算字符串长度
    printf("Length: %zu\n", strlen(str1)); // 输出: Length: 13

    return 0;
}

7.2 内存管理

相关头文件

#include <stdlib.h>

常用函数

  1. malloc:动态分配内存
  2. calloc:分配并初始化内存
  3. realloc:重新分配内存
  4. free:释放内存

在第四章我们讲过对应的API使用,这里不做过多的演示

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *arr = (int *)malloc(5 * sizeof(int)); // 动态分配内存
    if (arr == NULL) {
        fprintf(stderr, "Memory allocation failed\n");
        return 1;
    }

    for (int i = 0; i < 5; i++) {
        arr[i] = i + 1; // 初始化数组
    }

    for (int i = 0; i < 5; i++) {
        printf("%d ", arr[i]); // 输出: 1 2 3 4 5
    }
    printf("\n");

    free(arr); // 释放内存
    return 0;
}

7.3 数学运算

相关头文件

#include <math.h>

常用函数

  1. pow:计算幂
  2. sqrt:平方根
  3. sin, cos, tan:三角函数
  4. fabs:绝对值

示例:

#include <stdio.h>
#include <math.h>

int main() {
    double base = 2.0;
    double exponent = 3.0;

    // 计算 2 的 3 次方
    printf("Power: %.2f\n", pow(base, exponent)); // 输出: Power: 8.00

    return 0;
}

7.4 输入输出

相关头文件

#include <stdlib.h>

常用函数

  1. printf:格式化输出
  2. scanf:格式化输入
  3. fprintf:输出到文件
  4. fscanf:从文件中读取格式化输入
#include <stdio.h>

int main() {
    int num;
    printf("Enter an integer: ");
    scanf("%d", &num); // 读取输入
    printf("You entered: %d\n", num); // 输出输入的整数
    return 0;
}
7.5 错误处理

相关头文件

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

常用功能

  1. errno:全局变量,用于指示最近的错误类型
  2. perror:打印错误信息

示例:

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

int main() {
    FILE *file = fopen("nonexistent.txt", "r");
    if (file == NULL) {
        perror("Error opening file"); // 输出: Error opening file: No such file or directory
        return errno;
    }
    fclose(file);
    return 0;
}

7.6 时间处理

除了随机数生成,C 语言还提供了一些时间处理的功能。

相关头文件

#include <time.h>

常用函数

  1. time:获取当前时间
  2. difftime:计算两个时间点之间的差值
  3. strftime:格式化时间
#include <stdio.h>
#include <time.h>

int main() {
    time_t currentTime;
    time(&currentTime); // 获取当前时间
    printf("Current time: %s", ctime(&currentTime)); // 输出当前时间
    return 0;
}

根据以上的常用 API,我们可以写一个 Demo 应用一下,用户数据计算与日志记录:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <errno.h>
#include <time.h>

// 自定义数据结构体(内存管理+结构体应用)
typedef struct {
    double x;
    double y;
    char *description; // 动态字符串
} Point;

int main() {
    // ==================== 输入处理 ====================
    int num_points = 0;
    printf("请输入要创建的点数量: ");
    if (scanf("%d", &num_points) != 1 || num_points <= 0) {
        fprintf(stderr, "错误:无效输入\n");
        return EXIT_FAILURE;
    }

    // ==================== 内存管理 ====================
    Point *points = (Point*)malloc(num_points * sizeof(Point));
    if (!points) {
        perror("内存分配失败");
        return errno;
    }

    // ==================== 文件操作 ====================
    FILE *log_file = fopen("operation.log", "a");
    if (!log_file) {
        perror("无法打开日志文件");
        free(points);
        return errno;
    }

    // ==================== 时间处理 ====================
    time_t raw_time;
    struct tm *time_info;
    char time_buffer[30]; // 修正为字符数组

    time(&raw_time);
    time_info = localtime(&raw_time);
    strftime(time_buffer, sizeof(time_buffer), "[%Y-%m-%d %H:%M:%S]", time_info);

    // 写入日志头
    fprintf(log_file, "\n%s 开始处理%d个点\n", time_buffer, num_points);

    // ==================== 数据处理 ====================
    for (int i = 0; i < num_points; i++) {
        // 输入坐标
        printf("请输入第%d个点的坐标(x y): ", i+1);
        if (scanf("%lf %lf", &points[i].x, &points[i].y) != 2) {
            fprintf(stderr, "错误:坐标输入无效\n");
            fclose(log_file);
            free(points);
            return EXIT_FAILURE;
        }

        // ==================== 字符串处理 ====================
        char desc[20]; // 使用字符数组
        const char *prefix = "Point_";
        snprintf(desc, sizeof(desc), "%s%d", prefix, i + 1); // 安全字符串拼接

        // 动态内存分配字符串
        points[i].description = (char*)malloc(strlen(desc) + 1);
        if (!points[i].description) {
            perror("描述内存分配失败");
            fclose(log_file);
            for (int j = 0; j < i; j++) free(points[j].description);
            free(points);
            return errno;
        }
        strcpy(points[i].description, desc);

        // ==================== 数学运算 ====================
        double distance = sqrt(pow(points[i].x, 2) + pow(points[i].y, 2));
        double angle = atan2(points[i].y, points[i].x) * 180 / M_PI;

        // ==================== 结果输出 ====================
        printf("点 %s:\n", points[i].description);
        printf("  距原点的距离: %.2f\n", distance);
        printf("  极角角度: %.2f°\n", angle);

        // 写入日志文件
        fprintf(log_file, "%s 处理结果:\n", points[i].description);
        fprintf(log_file, "  坐标: (%.2f, %.2f)\n", points[i].x, points[i].y);
        fprintf(log_file, "  极坐标: (r=%.2f, θ=%.2f°)\n", distance, angle);
    }

    // ==================== 资源清理 ====================
    fprintf(log_file, "%s 处理完成\n", time_buffer);
    fclose(log_file);
    
    for (int i = 0; i < num_points; i++) {
        free(points[i].description);
    }
    free(points);

    printf("处理完成,结果已保存到operation.log\n");
    return EXIT_SUCCESS;
}

当然标准库中还有很多其他的API这里就不过多的介绍了。

总结

通过本系列文章的学习,我们从Java开发者的视角出发,逐步探索了C语言的核心特性与底层机制。以下是各章节的要点回顾与总结:

第一章:C语言的结构

  1. 头文件机制:C语言通过头文件实现模块化,与Java的import机制不同,头文件本质是代码复制。
  2. 入口函数main:C程序的起点,接受命令行参数(argc, argv),与Java的main方法类似但更底层。
  3. 作用域与常量:C语言的作用域规则与Java类似,但常量定义通过#define或const实现,需注意与Javafinal的区别。

第二章:C语言的基本数据类型

  1. 数据类型与转换:C语言的数据类型直接映射硬件,需注意有符号/无符号类型的差异,以及自动类型转换的规则。
  2. 数组与指针:数组名本质是指针,操作灵活但易出错,需警惕数组越界和指针运算的陷阱。
  3. 字符串操作:C语言中字符串是字符数组,需手动管理内存,标准库提供了常用函数(如strcpy、strcat),但需注意安全性。

第三章:C语言的逻辑控制

  1. 运算符与流程控制:C语言的运算符与Java类似,但需注意自增/减运算符的前后置差异,以及switch的fall-through特性。
  2. goto的使用:虽然goto在C语言中可用,但应限制在错误处理等特定场景,避免滥用导致代码难以维护。

第四章:C语言的指针与内存

  1. 指针的本质:指针是C语言的核心特性,直接操作内存地址,提供了极大的灵活性,但也带来了高风险(如野指针、内存泄漏)。
  2. 动态内存管理:通过malloc、free等函数手动管理内存,需严格配对使用,避免内存泄漏和悬垂指针。

第五章:C语言的函数

  1. 函数与参数传递:C语言通过值传递参数,但可通过指针模拟引用传递,实现类似Java对象引用的效果。
  2. 函数指针:函数指针是C语言实现多态的核心工具,可用于回调函数等高级功能,但需注意类型匹配问题。

第六章:C语言的结构体

  1. 结构体的定义与使用:结构体是C语言组织复杂数据的主要方式,支持嵌套和匿名结构体,但缺乏封装性。
  2. 结构体与指针:通过结构体指针可实现高效的数据操作,但需注意内存对齐和指针运算的细节。

第七章:C语言标准库重点模块

  1. 文件I/O:通过fopen、fread等函数实现文件操作,需手动管理文件指针和缓冲区。
  2. 时间与随机数:C语言提供time.h和stdlib.h库处理时间和随机数生成,但需注意平台差异和随机数质量。
  3. 字符串与内存操作:标准库提供了丰富的字符串和内存操作函数,但需警惕缓冲区溢出和内存越界问题。

C语言作为一门经典的编程语言,至今仍在操作系统、嵌入式系统、高性能计算等领域发挥着重要作用

通过学习C语言,您不仅能掌握底层编程技能,还能更深入地理解计算机系统的工作原理。如果您对底层开发感兴趣,可以进一步学习嵌入式开发(如STM32)、操作系统(如Linux内核)或编译原理等相关知识。

推荐大家学习基础语法之后可以去 Github 找一些代码看,或者找 Deepseek 写一下代码看看,

如果要更进一步,可以看看大佬写的实战项目,可以从嵌入式社区找一些代码学习和练手,代码风格比较好的可以推荐某原子和某火的代码,比如LED控制代码之类的简单实战代码开始看起。【资料很多】

同时也可以关注一下我的专栏,后期我也会厚颜发一些 C/C++ 和编译相关的笔记。

最后,如果我的文章有错别字,不通顺的,或者代码、注释、有错漏的地方,同学们都可以指出修正。

如果感觉本文对你有一点的启发和帮助,还望你能点赞支持一下,你的支持对我很重要!

Ok,那么这一期就此完结了。