首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >C语言学习笔记

C语言学习笔记

原创
作者头像
Vaeeeee
修改2026-04-19 18:32:39
修改2026-04-19 18:32:39
860
举报

前言

由于最近开始接触嵌入式开发,经常需要阅读和编写C代码,感觉自己对这方面还不够熟悉。因此结合嵌入式相关场景再系统地学习一遍C语言,以便扎实巩固自己的基础知识。

相关工具概览

类别

工具/名称

作用

说明

编译器

GCC / MinGW / Clang

将 C 代码翻译成机器能执行的二进制程序

GCC(GNU Compiler Collection):最常用跨平台开源编译器 - MinGW:Windows 上 GCC 的移植版本,包含编译器和必要工具 - Clang:LLVM 提供的 C/C++ 编译器,速度快、诊断信息好

调试器

GDB / LLDB

用于调试程序,检查变量、单步执行、追踪错误

GDB(GNU Debugger):最常用 C/C++ 调试器 - LLDB:LLVM 提供的调试器

IDE / 编辑器

VS Code / CLion / Dev-C++ / Code::Blocks

提供代码编辑、编译、调试一体化界面

方便写代码、运行和调试 - 可以配置不同的编译器

辅助工具

Make / CMake / Ninja

管理项目编译流程,自动化生成可执行文件

Make:最经典的构建工具 - CMake:跨平台生成 Makefile 或 VS 工程文件 - Ninja:快速构建系统

  • 因此这里我们选择配置 Vscode + MinGW 的方式来作为C语言的运行环境。

配置开发环境

  • Vscode的下载安装这里不过多介绍
  • MinGW安装: 进入该网址选择相应的版本下载:https://github.com/niXman/mingw-builds-binaries/releases(MSVCRT 在所有 Windows 版本上均可用,从 Windows 10 起,支持 UCRT。若支持 UCRT 则建议选择 UCRT。),解压到相应文件夹,并配置环境变量:...\mingw64\bin
  • 验证安装是否成功:
代码语言:bash
复制
gcc --version
g++ --version
gdb --version

输入上面代码都能输出相关信息表示安装成功。

数据类型

主要分为三类:

  • 基本数据类型:int,char, float, double
  • 派生数据类型:数组、指针、结构体、联合
  • 空类型:void

类型

关键字

占用字节 (常见)

位数

取值范围

示例

字符型

char

1 字节

8 位

-128 ~ 127 (有符号) 0 ~ 255 (无符号 unsigned char)

char a = 'A'; unsigned char b = 255;

短整型

shortshort int

2 字节

16 位

-32,768 ~ 32,767 0 ~ 65,535 (unsigned)

short x = 123; unsigned short y = 65000;

整型

int

4 字节 (常见)

32 位

-2,147,483,648 ~ 2,147,483,647 0 ~ 4,294,967,295 (unsigned)

int num = 1000; unsigned int u = 4000000000;

长整型

long

4 字节 (32 位系统) 8 字节 (64 位系统)

32/64 位

范围随系统而变

long l = 123456789;

长长整型

long long

8 字节

64 位

-9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807

long long big = 9000000000000000;

单精度浮点

float

4 字节

32 位

~1.2E-38 ~ 3.4E+38 (约 6 位有效数字)

float pi = 3.14f;

双精度浮点

double

8 字节

64 位

~2.3E-308 ~ 1.7E+308 (约 15 位有效数字)

double pi = 3.1415926535;

长双精度

long double

12/16 字节 (编译器不同)

≥ 80 位

更高精度

long double bigPi = 3.141592653589793238L;

空类型

void

void func() {}

  • 数组类型

语法:类型名 数组名元素个数; int numbers5;表示它在内存中连续分配 5 个 int 大小的空间;

数组名会自动转换为一个指向首元素的指针,但是注意:数组名不是指针变量,而是一个固定地址的符号常量;它本身不能被修改(例如不能写 numbers++)。

代码语言:c
复制
numbers      → 等价于 &numbers[0]
  • 格式说明符 一个完整的格式说明符通常包含以下部分:%[标志][宽度][.精度][长度修饰符]类型说明符 掌握代码过程中的到输入输出非常重要,C中常用的格式说明符如下:

说明符

用途

示例

%d, %i

有符号十进制整数

printf("%d", 42)

%u

无符号十进制整数

printf("%u", 100U)

%o

无符号八进制整数

printf("%o", 64)

%x, %X

无符号十六进制整数

printf("%x", 255)

%f, %F

十进制浮点数

printf("%f", 3.14)

%e, %E

科学计数法

printf("%e", 123000.0)

%g, %G

自动选择格式

printf("%g", 3.14)

%a, %A

十六进制浮点数

printf("%a", 255.0)

%c

字符

printf("%c", 'A')

%s

字符串

printf("%s", "Hello")

%p

指针地址

printf("%p", &var)

%n

输出字符计数

printf("Hi%n", &count)

%%

百分号字符

printf("50%%")

%hd, %hu

short 类型

printf("%hd", (short)10)

%ld, %lu

long 类型

printf("%ld", 1000000L)

%lld, %llu

long long 类型

printf("%lld", 123456789LL)

%zu

size_t 类型

printf("%zu", sizeof(int))

常见格式化操作

宽度和对齐控制

代码语言:c
复制
#include <stdio.h>

int main() {
    int n = 123;
    float f = 3.14;
    
    printf("|%5d|\n", n);     // 宽度5,右对齐:|  123|
    printf("|%-5d|\n", n);    // 宽度5,左对齐:|123  |
    printf("|%8.2f|\n", f);   // 宽度8,2位小数:|    3.14|
    printf("|%-8.2f|\n", f);  // 宽度8,左对齐:|3.14    |
    
    return 0;
}

动态宽度和精度

代码语言:c
复制
#include <stdio.h>
int main() {
    int width = 10;
    int precision = 3;
    float value = 3.14159;
    
    // 使用 * 指定动态宽度和精度
    printf("|%*.*f|\n", width, precision, value);  // |     3.142|
    
    // 也可以只动态指定一个
    printf("|%*.2f|\n", width, value);  // |      3.14|
    printf("|%10.*f|\n", precision, value);  // |     3.142|
    
    return 0;
}

注意事项:

  • C语言在进行运算时,先根据操作数类型决定运算方式,例如
代码语言:c
复制
int a =10;
int b = 4;
float c = a/b;  // c = 2.00000

当两个数都是int,先进行整数除法,再进行float。

  • 使用 scanf 时要注意:某些格式(尤其是%c、%s、%[ ])不会自动跳过空白字符,容易误读前一次输入留下的换行符,需要手动处理空白或加空格跳过。
代码语言:c
复制
#include <stdio.h>

int main()
{
    int a;
    char b;

    scanf("%d", &a);
    scanf("%c", &b);

    printf("%d -", a);
    printf("%c -", b);
}

// 输入
2
// 输出
2 -
 -

字符串

  • 实现字符串切片的简单方式(注意会修改原字符串):
代码语言:c
复制
#include <stdio.h>

int main()
{
    char str[] = "ABCDEFG";

    // 取出1:3的字符BCD
    str[4] = '\0';                 // 直接在索引4处截断
    printf("结果: %s\n", str + 1); // 从索引1开始输出
    return 0;
}
// BCD
  • 字符转换为对应的整数值:减去 '0';同理, 整数值转换为对应数字字符:加上 0
代码语言:c
复制
#include <stdio.h>

int main() {
    char c = '5';
    int digit = c - '0';  // '5' - '0' = 53 - 48 = 5
    
    printf("字符 '%c' 转换为整数值: %d\n", c, digit);
    // 输出:字符 '5' 转换为整数值: 5
    
    return 0;
}

宏定义

  • 常量定义:用于避免魔鬼数字
代码语言:c
复制
#define BUFFER_SIZE 256      // 缓冲区大小

// 使用
uint8_t buffer[BUFFER_SIZE];
  • 函数宏(Function-like Macros):没有返回值、没有类型检查,纯文本替换
代码语言:c
复制
#define MAX(a, b) ((a) > (b) ? (a) : (b))

int main() {
    int x = MAX(3+1, 5*2); 
    // 预处理后:((3+1) > (5*2) ? (3+1) : (5*2))
    return 0;
}
  • 注意事项: 1、每个参数都用()包裹,整个宏体用() 包裹
代码语言:c
复制
#define func(x) x*x
int res = func(3+1); 
// 替换后:3+1*3+1 = 3+3+1=7(错误,预期是16)

//正确写法:#define func(x) ((x)*(x))

2、宏名和括号之间不能有空格

代码语言:c
复制
#define MAX (a,b)  // 错误!变成普通宏,不是函数宏
#define MAX(a,b)   // 正确
  • 适用场景: 1、调试日志
代码语言:c
复制
#include <stdio.h>
// 调试等级
#define DEBUG_LEVEL_INFO   3
#define DEBUG_LEVEL_WARN   2
#define DEBUG_LEVEL_ERROR  1
#define CURRENT_DEBUG_LEVEL DEBUG_LEVEL_INFO

#define DEBUG_PRINT(level, fmt, ...) do { \
    if (level <= CURRENT_DEBUG_LEVEL) { \
        printf("[%s] " fmt "\r\n", __FUNCTION__, ##__VA_ARGS__); \
    } \
} while(0)

// __FUNCTION__  C/C++编译器的预定义标识符,表示当前函数的名称
// __VA_ARGS__ 表示可变参数列表,## 是预处理连接符的特殊用法
// 使用
int main() {
    int adc_value = 5;
    int bus_id = 6;
    DEBUG_PRINT(DEBUG_LEVEL_INFO, "ADC value: %d", adc_value);
    DEBUG_PRINT(DEBUG_LEVEL_ERROR, "I2C timeout on bus %d", bus_id);

}

关键字

关键字

作用/说明

auto

自动变量,局部变量默认存储类型

break

跳出循环或 switch

case

switch 分支标签

char

字符类型

const

常量修饰符

continue

跳过本次循环,进入下一次循环

default

switch 默认分支

do

do...while 循环开始

double

双精度浮点数类型

else

if 条件语句的否定分支

enum

枚举类型

extern

声明外部变量或函数

float

单精度浮点数类型

for

for 循环

goto

无条件跳转语句

if

条件语句

inline

内联函数提示(C99 后)

int

整型

long

长整型

register

寄存器变量提示

restrict

指针限定符(C99 后,用于优化)

return

从函数返回值

short

短整型

signed

有符号修饰符

sizeof

求数据类型或变量占用字节数

static

静态存储类型

struct

结构体

switch

多分支选择语句

typedef

类型重定义

union

联合体

unsigned

无符号修饰符

void

无类型/无返回值函数

volatile

告诉编译器变量可能随时被改变,禁止优化

while

while 循环

const

  • 用于修饰 变量或指针,表示其值在初始化后不能被修改。 用途:提高代码安全性 | 告诉编译器优化空间 | 常用在函数参数中防止意外修改
代码语言:c
复制
#include <stdio.h>

int main()
{
    const int a = 10;   
    const int *p  = &a;   // 指向常量的指针
    int b = 20;
    p = &b;   // 指针可以改变 *p = 20
    // *p = 20;   // 指针指向的值不能改
}


int main()
{
    int a = 10;
    int * const p = &a;   
    *p = 20;   // 
    // int b = 30;
    // p = &b;   // 指针不能改变
}

与宏的对比:

#define PI 3.14:宏,不占内存,编译时替换

const double PI = 3.14: 是 类型安全的常量,可参与调试、类型检查

  • volatile:告诉编译器 不要对这个变量做优化,因为它的值可能在程序执行的任何时刻被 外部因素修改。主要有两个作用: 1.防止编译器将变量缓存到寄存器中。 2.强制每次使用变量时都从内存中读取最新值。

有些情况下编译器为了优化性能,会做一些假设和优化,比如下面这段代码,如果编译器认为 x 在循环内没有被改变,它可能会优化成 死循环,永远读取缓存寄存器里的 x,而不会去内存检查。但是,x 的值可能被 中断、硬件或其他线程修改,这就会出问题。

代码语言:c
复制
int x = 0;
while (x == 0) {
    // do something
}

常见的使用方式如下:

代码语言:c
复制
#include <stdio.h>
#include <stdbool.h>

volatile int flag = 0;

void set_flag() {
    flag = 1;  // 外部函数修改 flag
}

int main() {
    while(flag == 0) {
        pass
    }
    printf("Flag is set!\n");
    return 0;
}

enum

  • 用于定义一组具名的整型常量,提高代码可读性
  • 枚举变量定义:
代码语言:c
复制
// 方法1:先定义类型,后定义变量
enum uart_baudrate {
    BAUD_9600  = 9600,
    BAUD_19200 = 19200,
    BAUD_115200 = 115200
};
enum uart_baudrate uart_speed;

// 方法2:定义类型同时定义变量
enum i2c_speed {
    I2C_STANDARD = 100000,
    I2C_FAST     = 400000,
    I2C_HIGH     = 3400000
} i2c_bus_speed;

// 方法3:匿名枚举(常用)
enum {
    LED_OFF = 0,
    LED_ON  = 1
} led_state;
  • 适用场景: 1、状态机
代码语言:c
复制
typedef enum {
    SYS_INIT,
    SYS_RUN,
    SYS_LOW_POWER,
    SYS_FAULT
} SysState;

SysState system_state;

void state_machine(void)
{
    switch(system_state) {
        case SYS_INIT:
            // 初始化硬件
            break;
        case SYS_RUN:
            // 正常运行
            break;
        case SYS_LOW_POWER:
            // 进入低功耗
            break;
        case SYS_FAULT:
            // 故障处理
            break;
    }
}

内存管理⭐

  • 一个运行中的 C 程序在操作系统看来大致有这些区域: 文本段 (.text):存放程序代码(只读)。 只读数据 (.rodata):字面量、const 常量等(通常只读) 已初始化数据段 (.data):全局/静态变量且被初始化的部分 未初始化数据段 (.bss):全局/静态变量未初始化或为 0 的部分 堆(heap):动态分配(malloc、calloc、realloc、free)得到的内存,通常向高地址增长或由 mmap 分配。 栈(stack):函数调用帧、局部变量、返回地址等 内存映射区(mmap)/共享库映射:动态库、映射文件、匿名映射等。

栈 (stack):自动分配内存,函数退出即释放

核心特性:

  • 自动分配与释放
  • 后进先出
  • 高速访问
  • 空间有限:过大的局部变量可能导致栈溢出

优点:无需手动管理内存,速度快,不会内存泄漏。

缺点:生命周期固定(函数结束即释放),空间有限。

堆 (heap): 手动分配和释放

核心特性:

  • 手动分配与释放:使用malloc/calloc/realloc分配,free释放。
  • 动态生命周期
  • 碎片化问题:频繁分配和释放可能导致内存碎片,降低空间利用率
  • 慢速访问:需通过指针间接访问,效率低于栈

1、 malloc:适合已知需要多少字节、或者手动初始化的情况。

例如需要分配5个整数的数组时:

代码语言:c
复制
#include <stdio.h>
#include <stdlib.h>
int main(){
    int *arr = malloc(5 * sizeof(int));
    for (int i = 0; i<5; i++){
        arr[i] = i * i;
    }
    for (int i = 0; i<5; i++){
        printf("%d ", arr[i]);
    }
    free(arr);
}
// 0 1 4 9 16 

对于这段代码有几个地方需要注意:

①这里的arr[i]*(arr + i) 完全等价的。但是也有些不同的地方:

代码语言:c
复制
#include <stdio.h>
int main(){
    int arr[5] = {1, 2, 3, 4, 5};
    int *ptr = arr;

    //大小不同
    printf("sizeof(arr) = %zu bytes\n", sizeof(arr));
    printf("sizeof(*ptr) = %zu bytes\n", sizeof(*ptr));

    //地址不同
    printf("&arr = %p\n", (void*)&arr);
    printf("&ptr = %p\n", (void*)&ptr); // 这是ptr变量自己的地址

    //赋值行为不同
    int var = 10;
    ptr = &var;
    printf("*ptr = %d\n", *ptr); // ptr可以指向别处
    // arr = &var; //错误,arr 不能指向别处
}

// sizeof(arr) = 20 bytes
// sizeof(*ptr) = 4 bytes
// &arr = 00000000005FFE60
// &ptr = 00000000005FFE58
// *ptr = 10

②如果末尾不释放内存,运行也不会报错,为什么要手动释放?

  • ❗首先要了解 内存泄漏 的概念:简单来说就是内存“还在”,但程序“再也找不到”它。例如:
代码语言:c
复制
#include <stdio.h>
#include <stdlib.h>

int main() {
    int *p = malloc(sizeof(int) * 5);  // 动态分配20字节内存
    p = NULL;  // 丢失了原来的指针

    // 原来那块内存还在内存中,但我们再也访问不到了
    // 没有free,也不会有错误提示
    return 0;
}

由于小程序运行时间短时没问题,但对于长时间运行的程序(例如服务器、嵌入式系统、驱动)不断循环分配内存的逻辑 会出现这种现象:内存占用越来越高 → 最终系统内存耗尽 → 程序崩溃或性能下降。💣

  • 内存泄漏常见场景:

场景

示例

问题

忘记 free

int *p = malloc(100); 没有对应 free(p)

占用未释放

指针丢失

p = malloc(100); p = NULL;

无法再释放

提前返回

在函数中多处 return 没有释放内存

逻辑遗漏

多次分配

p = malloc(100); p = malloc(200);

原内存被覆盖

  • 简单的泄漏和正确释放的例子:
代码语言:c
复制
// ❌ 有泄漏
#include <stdlib.h>

void test() {
    int *arr = malloc(10 * sizeof(int));
    // 使用完后直接返回
    return;
}

// ✅ 正确释放
#include <stdlib.h>

void test() {
    int *arr = malloc(10 * sizeof(int));
    // 使用arr...
    free(arr);   // 释放内存
    arr = NULL;  // 避免野指针
}
  • 这里又引出了一个概念:野指针(或称悬空指针,表示指向无效内存地址的指针。这个内存地址可能已经被释放、回收,或者从未合法分配) ❗一个被释放的指针如果不置为NULL,就会变成一颗定时炸弹。

③如果分配的是5个整数的数组,但是 访问了 10 个元素会出现什么情况?

代码语言:c
复制
#include <stdio.h>
#include <stdlib.h>
int main(){
    int *arr = malloc(5 * sizeof(int));
    for (int i = 0; i<10; i++){
        arr[i] = i * i; 
    }
    for (int i = 0; i<10; i++){
        printf("%d ", arr[i]);
    }
    free(arr);
}
//0 1 4 9 16 25 36 49 64 81 
  • 可以发现最终输出了10个数据,但是程序一直没有终止!这是因为 内存越界写入 导致了“未定义行为”。
  • 当我们在访问arr[5]时,程序会往 arr之后的内存写数据,这块内存可能属于堆管理器(malloc系统)或别的变量。C语言不会阻止你这么干,但后果是 可能破坏了堆结构。🧨 那么当 free() 尝试释放时,就会“踩到雷” 💥,导致:程序死循环、崩溃、退出异常、甚至内存碎片混乱。
  • 总结

函数名

功能

返回值

初始化

是否可调整大小

malloc(size_t size)

分配一块大小为 size 字节的内存

指向分配内存的指针,失败返回 NULL

❌ 不会初始化,内容不确定(是“脏数据”)

calloc(size_t nmemb, size_t size)

分配 nmemb × size 字节内存

指向分配内存的指针,失败返回 NULL

✅ 全部置 0

realloc(void *ptr, size_t new_size)

改变一块已分配内存的大小

新的指针,失败返回 NULL

✅/❌ 保留原有数据,新增部分未初始化

free(void *ptr)

释放由以上函数分配的内存

2、 calloc:分配内存并自动初始化为 0

ptr = (type *)calloc(number_of_elements, size_of_each_element); 分配 number_of_elements × size_of_each_element 字节,内存内容初始化为 0。

代码语言:c
复制
#include <stdio.h>
#include <stdlib.h>
int main(){ 
    int *arr  = calloc(5, sizeof(int));//分配5个int大小的内存,并初始化为0
    for (int i  = 0;i<5;i++){
        printf("%d ", arr[i]);
    }
    free(arr);
    return 0;
}
// 输出:0 0 0 0 0

3、 realloc:改变已经分配的内存大小。

ptr = (type *)realloc(ptr, new_size_in_bytes);

  • 可以扩大或缩小已有内存块。
  • 扩大后,新的内存内容可能未初始化(不保证为 0)。
  • 如果原内存块无法扩展,realloc 会自动分配新内存并复制原内容,然后释放原内存。
代码语言:c
复制
#include <stdio.h>
#include <stdlib.h>
int main(){
    int *arr = malloc(3 * sizeof(int));
    for (int i = 0; i<3; i++)
        arr[i] = i * i; // 初始化为 0 1 4
    
    // 重新分配内存为5个int
    arr = realloc(arr, 5 * sizeof(int));
    arr[3] = 10;
    arr[4] = 20;
    for (int i = 0; i < 5; i++)
        printf("%d ", arr[i]); // 输出:0 1 4 10 20
    free(arr);
    return 0;
}

指针(本质就是存储内存地址的整数)

指针类型分类

类别

举例

功能概述

1️⃣ 普通指针

int *p

指向普通变量

2️⃣ 空指针(NULL指针)

int *p = NULL;

不指向任何有效地址

3️⃣ 野指针(Dangling Pointer)

未初始化或已释放内存后继续使用的指针

危险,导致崩溃或未定义行为

4️⃣ void 指针(通用指针)

void *p

可指向任意类型数据

5️⃣ 指向指针的指针

int **pp

存放另一个指针的地址

6️⃣ 指向常量的指针

const int *p

不能修改所指数据

7️⃣ 常量指针

int *const p

指针本身不能改,内容可以改

8️⃣ 指向常量的常量指针

const int *const p

指针和内容都不能改

9️⃣ 数组指针

int (*p)[5]

指向一个数组

🔟 函数指针

int (*pf)(int, int)

指向一个函数

1️⃣1️⃣ 结构体指针

struct Stu *sp

指向结构体变量

1️⃣2️⃣ 指针数组

int *arr[3]

存放多个指针的数组

1️⃣3️⃣ 指向字符串常量的指针

char *p = "Hello";

指向常量字符串

1️⃣4️⃣ 指向文件的指针

FILE *fp

用于文件操作(fopen 等)

1️⃣5️⃣ 指向函数返回堆内存的指针

int *p = malloc(10*sizeof(int));

动态内存管理

空指针

  • 表示空指针的方法:
代码语言:c
复制
#include <stdio.h>
#include <stdlib.h>
int main(){
    int *p1 = NULL;      // 最常用的方式
    int *p2 = 0;         // 直接赋值为0
    int *p3 = (void *)0; // 显式类型转换

    printf("p1 = %p\n", (void *)p1);
    printf("p2 = %p\n", (void *)p2);
    printf("p3 = %p\n", (void *)p3);
}
// p1 = 0000000000000000 
// p2 = 0000000000000000 
// p3 = 0000000000000000
  • 应用场景: 1️⃣ 指针初始化:在定义指针时立即初始化为NULL,避免野指针
代码语言:c
复制
#include <stdio.h>
#include <stdlib.h>

int main()
{
    // ❌ 危险:未初始化的指针(野指针)
    // int *dangerous_ptr;

    // ✅ 安全:初始化为空指针
    int *safe_ptr = NULL;

    // 检查后再使用
    if (safe_ptr == NULL)
    {
        safe_ptr = (int *)malloc(sizeof(int));
        *safe_ptr = 100;
    }

    free(safe_ptr);
    safe_ptr = NULL; // 释放后重新置空

    return 0;
}

2️⃣动态内存分配失败检查

在前面内存管理 中提到了malloc、calloc、realloc这些内存分配函数,当需要检查是否分配成功时,可以使用空指针。

代码语言:c
复制
#include <stdio.h>
#include <stdlib.h>

int main()
{
    int *ptr = (int*)malloc(100 * sizeof(int));

    if (ptr == NULL)
    {
        printf("Memory allocation failed.\n");
        return 1;
    }
    printf("Memory allocated successfully.\n");
    // ...
    free(ptr);
    ptr = NULL;

}
  • 不要解引用空指针:
代码语言:c
复制
int *p = NULL;
// *p = 10;  // 错误!
  • 所有指针类型都可以设为NULL
  • 总结: 总是初始化指针为NULL 在使用指针前检查是否为NULL 释放内存后立即将指针设为NULL

野指针

  • 指向无效内存地址的指针。这个内存可能已经被释放、回收,或者从未合法分配。
  • 容易产生野指针的场景: 1️⃣ 函数返回局部变量地址
代码语言:c
复制
// 返回了局部变量的地址,而栈是自动回收的,导致local这个局部变量的指针变成了野指针
int *create_dangling_pointer()
{
    int local = 42;
    return &local; // 返回后local的内存被回收,指针悬空
}

正确做法:

代码语言:c
复制
int *create_valid_pointer() {
    int *dynamic = (int*)malloc(sizeof(int));
    *dynamic = 42;
    return dynamic; // 调用者需要负责释放

2️⃣多指针共享同一内存

代码语言:c
复制
void shared_memory_danger()
{
    int *ptr1 = (int *)malloc(sizeof(int));
    int *ptr2 = ptr1; // 两个指针指向同一内存

    *ptr1 = 100;
    free(ptr1);  // 释放内存
    ptr1 = NULL; // ptr1安全了, 但ptr2现在变成了野指针!
    // printf("%d\n", *ptr2); // 未定义行为
}
  • 总结: 释放后立即置空 使用"所有权"概念:即上面的ptr1释放内存后,ptr2不应该在使用;ptr1作为主指针,而ptr2作为临时指针,不负责管理内存。

void 指针

  • 通用类型指针,能指向任何数据类型的内存地址,但在解引用前必须进行类型转换
代码语言:c
复制
#include <stdio.h>

int main()
{
    int int_value = 42;
    float float_value = 3.14f;
    char char_value = 'A';
    char str[] = "Hello";

    void *void_ptr; // 声明void指针
    void_ptr = &int_value; // 指向整型
    // printf("void_ptr = %d\n", *void_ptr); // 错误!不能直接解引用void指针
    printf("void_ptr = %d\n", *(int *)void_ptr); // 正确:需要类型转换

    void_ptr = &float_value;             // 指向浮点型
    void_ptr = &char_value; // 指向字符型
    void_ptr = str; // 指向数组
    return 0;
}
  • 不能进行指针算术
代码语言:c
复制
#include <stdio.h>
int main()
{
    int arr[3] = {10, 20, 30};
    void *void_ptr = arr;

    // void_ptr++;  // 错误!void指针不能进行算术运算

    // 正确:转换为具体类型后再运算
    int *int_ptr = (int *)void_ptr;
    int_ptr++;
    printf("%d\n", *int_ptr); // 输出: 20

    return 0;
}
  • 主要适用场景 1️⃣ 通用函数接口(例如内存操作函数) 在标准库中,malloc、memcpy、memset 等函数的参数或返回值都是 void *;

memcpy函数的源码:

2️⃣ 通用数据传递(如线程、回调函数、自定义数据结构)

代码语言:c
复制
#include <pthread.h>
#include <stdio.h>

void *worker(void *arg) {
    int *num = (int *)arg;   // 将 void* 强转回原类型
    printf("Thread received: %d\n", *num);
    return NULL;
}

int main() {
    pthread_t tid;
    int value = 42;

    pthread_create(&tid, NULL, worker, &value);  // 传入任意类型指针
    pthread_join(tid, NULL);

    return 0;
}

线程函数的参数 void *arg 可以是任意类型。在线程内部,再强制转换回正确的类型使用;

这使得线程函数可以处理任意类型的数据结构,而不必固定类型。

指向指针的指针

  • 指针的指针:存储的是另一个指针变量的地址。
代码语言:c
复制
int main()
{
    int value = 10;         // 普通整型变量
    int *ptr1 = &value;     // 指针,指向整型变量
    int **ptr2 = &ptr1;     // 指向指针的指针,指向指针ptr1
    printf("value值为:%d\n", value);
    printf("value内存地址为:%p\n", ptr1);
    printf("ptr1地址为:%p\n", ptr2);
    printf("ptr2存储的值为%p", *ptr2);
}
// value值为:10
// value内存地址为:00000000005FFE74
// ptr1地址为:00000000005FFE68
// ptr2存储的值为00000000005FFE74
  • 主要适用场景 1️⃣ 动态二维数组
代码语言:c
复制
#include <stdlib.h>
int main(){
    int rows = 3, cols = 4;

    // 创建二维动态数组
    int **array = (int **)malloc(rows * sizeof(int *));
    for(int i = 0; i < rows; i++){
        array[i] = (int *)malloc(cols * sizeof(int));
    }

    // 使用数组
    array[1][2] = 99; // 

    // 释放内存
    for(int i=0; i < rows; i++){
        free(array[i]);
    }
    free(array);
}
代码语言:sql
复制
[ array 指向 ]
+------+------+------+   <- 存放 3 个 int*(每个元素是一个指针,指向每行的首地址)
| p0   | p1   | p2   |
+------+------+------+

array -> [ p0 | p1 | p2 ]
           |    |    |
           v    v    v
         [4 ints][4 ints][4 ints]

2️⃣ 修改指针本身

代码语言:c
复制
#include <stdlib.h>
void changePointer(int **pp)
{
    static int newValue = 100;
    *pp = &newValue; // 修改指针指向的内容
}

int main()
{
    int num = 50;
    int *ptr = &num;

    printf("修改前: %d\n", *ptr); // 50
    changePointer(&ptr);          // 传递指针的地址
    printf("修改后: %d\n", *ptr); // 100
    return 0;
}

指向常量的指针

  • 指向常量的指针:指针指向的数据不能被修改,但指针本身可以指向其他地址。
  • 主要适用场景 1️⃣ 函数参数 - 防止意外修改
代码语言:c
复制
#include <stdio.h>

// 这个函数承诺不会修改传入的数组
void printArray(const int *arr, int size)
{
    for (int i = 0; i < size; i++)
    {
        printf("%d ", arr[i]);
        // arr[i] = 0;  // ❌ 编译错误!安全保护
    }
    printf("\n");
}
int main()
{
    int numbers[] = {1, 2, 3, 4, 5};
    printArray(numbers, 5);
    return 0;
}

2️⃣ 硬件寄存器访问

代码语言:c
复制
// 只读的硬件寄存器
const volatile int *readonly_register = (const int*)0x1000;

int readHardwareValue() {
    return *readonly_register;  // 只能读取,不能写入
    // *readonly_register = 1;  // ❌ 硬件保护
}
  • 总结:这种指针在需要读取但不修改数据的场景中特别有用。

常量指针

  • 指针本身存储的地址是常量,不可改变,语法:[类型] * const [指针名]
  • 主要适用场景: 1️⃣ 在函数内部,固定指向某个对象,并确保指针不被意外修改。
代码语言:c
复制
// 例如,在嵌入式系统中,指向一个固定的、只读的硬件寄存器或ROM中的配置数据
const ConfigTable *const system_config = &default_config;

// 函数内部使用全局静态对象时,避免指针被误改
static int buffer[128];
int * const p = buffer;   // p 永远指向这个 buffer

2️⃣ 固定地址硬件寄存器映射

代码语言:c
复制
// 假设寄存器布局如下:
typedef struct {
    volatile uint32_t MODER;   // 模式寄存器
    volatile uint32_t ODR;     // 输出数据
    volatile uint32_t BSRR;    // 置位/复位
} GPIO_Regs;

// 指针不能修改 → 防止破坏寄存器映射
// 指向的内容(寄存器)可以读写 → 正常访问硬件

// 此时用 指向固定地址的指针常量 映射:
#define GPIOA_BASE 0x40021000UL
GPIO_Regs * const GPIOA = (GPIO_Regs *)GPIOA_BASE;

// 这样一来,GPIOA = xxx 会报错;但 GPIOA->MODER = ... 可正常修改寄存器内容
  • 总结:常量指针用于固定指针本身,防止它偏移或被重新赋值,常用于硬件寄存器映射、固定内存资源绑定等场景,使程序更安全、更清晰。

指向常量的常量指针

  • 指针本身存储的地址是常量,不可改变;同时,指针指向的内容也是常量,不可通过该指针修改。
  • 语法:const [类型] * const [指针名][类型] const * const [指针名]
  • 主要适用场景: 1️⃣只读资源的安全访问 同时固定指针指向和内容,适用于全局只读数据、配置表、资源句柄等。
代码语言:c
复制
// 例如,系统全局配置表,既不能修改表内容,也不能让指针指向其他表
const ConfigTable * const system_config = &default_config;

// 下面的操作都是非法的:
// system_config = &backup_config;   // 错误:指针本身是常量
// system_config->baudrate = 9600;    // 错误:指向的内容是常量

2️⃣ 保护静态常量池中的字符串或数据

嵌入式系统中,字符串字面量或只读数据通常存放在ROM或Flash中,不允许修改,且地址固定。

  • 总结:指向常量的常量指针是限制最强的指针形式——指针本身不能改,指向的内容也不能通过该指针改。常用于:
    • ROM/Flash中的只读资源
    • 系统全局配置表
指向常量的常量指针 VS 指向常量的指针
  • 资源地址固定且内容只读:用指向常量的常量指针
  • 需要切换不同的只读对象:用指向常量的指针

数组指针

  • 指针本身存储的是整个数组的地址,步长为整个数组的大小
  • 语法:[类型] (*[指针名])[数组长度]
  • 主要适用场景:

1️⃣ 二维数组传参与遍历

代码语言:c
复制
// 处理固定列数的二维数组(矩阵)
void process_matrix(int rows, int (*matrix)[4]) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < 4; j++) {
            matrix[i][j] *= 2;   // 像普通二维数组一样使用
        }
    }
}

// 调用示例
int grid[3][4] = {
    {1, 2, 3, 4},
    {5, 6, 7, 8},
    {9, 10, 11, 12}
};
process_matrix(3, grid);  // 数组名自动退化为指向首行的指针;grid数组最终所有元素乘2

2️⃣ 跳过整个数组行(步长操作)

代码语言:c
复制
int main()  
{
    // 利用数组指针的步长特性,快速跳转到指定行
    int scores[4][3] = {
        {90, 85, 88},
        {78, 92, 86},
        {95, 89, 91},
        {82, 88, 84}};

    int (*row)[3] = scores;    // 指向第0行
    row++;                     // 跳转到第1行(步长 = 12字节,3个int)
    printf("%d\n", (*row)[1]); // 输出第1行第2列:92

    row += 2;                  // 跳转到第3行
    printf("%d\n", (*row)[0]); // 输出第3行第1列:82
}

注意区分

  • int (*p)[5] → 数组指针,指向包含5个int的数组
  • int *p[5] → 指针数组,包含5个int指针的数组

函数指针

  • 指针本身存储的是函数的入口地址,可以通过它调用函数
  • 语法:[返回类型] (*[指针名])(参数类型列表)
  • 适用场景: 1️⃣ 回调函数(把函数当作参数传给另一个函数)
代码语言:c
复制
// 利用函数指针决定做什么运算
int calculate(int a, int b, int (*op)(int, int))
{
    return op(a, b);
}

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

int sub(int x, int y)
{
    return x - y;
}
int main()
{
    printf("%d\n", calculate(10, 5, add)); // 函数名自动转换为函数指针,等价于&add
    printf("%d\n", calculate(10, 5, sub));  
    return 0;
}
// 15
// 5

2️⃣ 中断向量表

代码语言:c
复制
// 中断向量表的本质:一个函数指针数组
void (*isr[3])(void);  // 3个中断的处理函数

// 中断0的处理函数
void handler_for_irq0(void) {
    printf("中断0发生了\n");
}

// 中断1的处理函数
void handler_for_irq1(void) {
    printf("中断1发生了\n");
}

int main() {
    // 注册:告诉系统,当中断0发生时,调用handler_for_irq0
    isr[0] = handler_for_irq0;
    isr[1] = handler_for_irq1;
    
    // 模拟硬件:当中断0发生时
    isr[0]();  // 自动调用 handler_for_irq0
    // 输出:中断0发生了
}
  • 总结:函数指针就是指向函数的指针,可以像普通变量一样传递和存储
注意区分:
  • int(*p)(int,int):函数指针, p 是一个指针,指向返回 int、参数是 (int, int) 的函数 ;
  • int*p(int,int):普通函数声明,p 是一个函数,返回值类型:int*(整型指针);
  • int(*p[3l)(int,int):函数指针数组,p 是一个数组,数组大小为3,每个元素:函数指针(指向返回 int、两个 int 参数的函数)

结构体指针

  • 指针本身存储结构体变量的地址,可以通过它访问结构体成员
  • 语法:[结构体类型] *[指针名]
  • 适用场景:

1️⃣ 访问硬件寄存器

代码语言:c
复制
// 定义GPIO寄存器的结构体(映射到真实硬件地址)
typedef struct {
    volatile unsigned int MODER;    // 模式寄存器
    volatile unsigned int ODR;      // 输出数据寄存器
    volatile unsigned int IDR;      // 输入数据寄存器
} GPIO_TypeDef;

// 定义指针指向真实的硬件地址(比如0x40020000)
#define GPIOA_BASE  0x40020000
GPIO_TypeDef *GPIOA = (GPIO_TypeDef *)GPIOA_BASE;

// 通过结构体指针操作寄存器
int main() {
    GPIOA->MODER = 0x01;    // 设置GPIO为输出模式
    GPIOA->ODR = 0x01;      // 输出高电平
    
    unsigned int value = GPIOA->IDR;  // 读取输入值
}

指向字符串常量的指针

  • 指针本身存储字符串常量的首地址,可以通过它访问字符串内容,但不能修改字符串内容
  • 语法:const char *[指针名]char const *[指针名]
  • 适用场景: 1️⃣ 只读的提示信息(如串口输出)
代码语言:c
复制
//如使用STM32单片机
#include "stm32f1xx_hal.h"  // STM32的库
extern UART_HandleTypeDef huart2;  // 串口2的句柄

// 指向字符串常量的指针(字符串存在ROM/Flash中,不可修改)
const char *welcome_msg = "System Ready\r\n";
const char *error_msg = "Error: Sensor failed\r\n";

// 串口发送函数
void uart_send_byte(uint8_t data) {
    HAL_UART_Transmit(&huart2, &data, 1, 100);  // 发送1个字节
}

// 发送字符串
void uart_send_string(const char *str) {
    while (*str != '\0') {
        uart_send_byte(*str);  // 发送字符
        str++;
    }
}

const char *welcome_msg = "System Ready\r\n";

int main() {
    uart_send_string(welcome_msg);  // 输出:System Ready
    uart_send_string(error_msg);      // 输出:Error: Sensor failed
}

指向文件的指针

  • 指针本身存储文件信息结构体的地址,通过它可以读写文件
  • 语法:FILE *[指针名]
  • 主要适用场景: 1️⃣ 读取配置文件 or 写入日志数据
代码语言:c
复制
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main() {
	FILE* fp;
	char buffer[100];

	// 打开文件
	fp = fopen("config.txt", "r");
	if (fp != NULL) {
		fgets(buffer, 100, fp);
		printf("%s\n", buffer);

		fclose(fp);

		return 0;
	}
}
代码语言:c
复制
#include <stdio.h>

void write_log(const char *message) {
    FILE *fp = fopen("log.txt", "a");  
    
    if (fp != NULL) {
        fprintf(fp, "%s\n", message);  // 写入内容
        fclose(fp);
    }
}

int main() {
    write_log("系统启动");
    write_log("温度:25°C");
    write_log("系统关闭");
    return 0;
}

总结

  • 长期更新...

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 相关工具概览
    • 配置开发环境
  • 数据类型
    • 字符串
  • 宏定义
  • 关键字
    • const
    • enum
  • 内存管理⭐
    • 栈 (stack):自动分配内存,函数退出即释放
    • 堆 (heap): 手动分配和释放
  • 指针(本质就是存储内存地址的整数)
    • 指针类型分类
      • 空指针
      • 野指针
      • void 指针
      • 指向指针的指针
      • 指向常量的指针
      • 常量指针
      • 指向常量的常量指针
      • 数组指针
      • 函数指针
      • 结构体指针
      • 指向字符串常量的指针
      • 指向文件的指针
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档