news 2026/8/2 6:57:46

C/C++指针传递核心原理:二级指针与内存模型详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C/C++指针传递核心原理:二级指针与内存模型详解

1. 项目概述:指针传递的“变”与“不变”

在C/C++的世界里,指针传递这个话题,老手觉得是基本功,新手却常常一头雾水,尤其是在“什么时候能改变外部指针本身”这个问题上栽跟头。我见过太多代码,函数里一通操作猛如虎,结果出来一看,外边的指针纹丝不动,该是NULL还是NULL。这背后的核心,其实是对“值传递”这一根本机制的深刻理解。无论你传递的是intstruct还是一个指针变量,C/C++默认都是拷贝一份值(这个值可能是整数、结构体的副本,或者是一个内存地址的数值)扔给函数。理解了这一点,很多困惑就迎刃而解了。

简单来说,这个内容要解决的就是:当你把一个指针作为参数传给函数时,你究竟能改变什么,不能改变什么,以及当你确实需要改变外部指针本身(比如在函数内动态分配内存并让外部指针指向它)时,应该怎么做。这直接关系到内存管理的正确性、函数接口的设计清晰度,以及避免内存泄漏和悬空指针等经典问题。无论是正在学习指针基础的学生,还是工作中需要编写或维护底层库、性能敏感模块的开发者,彻底搞懂指针传递都是绕不开的一课。接下来,我们就抛开那些笼统的说法,深入到汇编视角和内存模型里,把“改变”与“不改变”的边界画得清清楚楚。

2. 核心原理:从内存模型理解“值传递”的本质

2.1 指针变量与指针所指向的内存

这是理解一切的基础,必须分清楚。一个指针变量,例如int *p,它本身是存储在栈上(或静态区)的一个内存单元,这个单元里存放的值是一个内存地址。而“指针所指向的内存”,是指这个地址所对应的那片内存区域,里面存放着实际的数据(比如一个整数)。

当我们说“改变指针”,通常有两种含义:

  1. 改变指针变量本身的值:也就是让这个指针变量里存储的地址变成另一个地址。例如p = &b;或者p = (int*)malloc(sizeof(int));
  2. 改变指针所指向的内存内容:指针变量里存的地址没变,但通过这个地址(即解引用*p)去修改它指向的那片内存里的数据。例如*p = 100;

函数参数传递,传递的是“指针变量”这个变量自身的值(即一个地址数值),而不是“指针变量”这个容器。

2.2 函数调用栈帧与参数拷贝

当调用一个函数void func(int *arg)并传入p时,发生的过程可以这样拆解:

  1. 为函数func在调用栈上创建一个新的栈帧。
  2. 计算实参p的值。p的值是一个地址,假设是0x7ffeed0
  3. 将这个值0x7ffeed0拷贝一份,压入func的栈帧中,作为形参arg的初始值。
  4. 函数func内部操作的arg,是它自己栈帧上的一个局部变量(形参),只是这个局部变量的初始值被设置成了外部p的值的副本。

这个过程就是经典的“值传递”。argp的一个副本,它们俩在内存中是两个独立的变量,只是此刻存储的地址值相同。这就好比我把我的家庭住址(一个值)写在一张纸条上复印了一份给你,你拿到了地址的副本。你可以根据这个地址找到我的家(解引用),并搬动我家的家具(修改指向的内存)。但是,无论你在你的纸条上怎么涂改,把我家的地址改成电影院,我手里的原始纸条上的地址是不会变的(无法改变外部指针变量本身)。

2.3 图解:三种典型场景的内存状态

我们通过三个典型场景来可视化这个过程。

场景一:试图改变外部指针(失败案例)

void tryChangePtr(int *ptr) { int newValue = 100; ptr = &newValue; // 错误!这只改变了形参ptr的指向 } int main() { int a = 10; int *p = &a; printf("Before: *p = %d, p points to %p\n", *p, (void*)p); tryChangePtr(p); printf("After: *p = %d, p points to %p\n", *p, (void*)p); // p未改变 return 0; }

内存模型解读:

  • main栈帧中,变量a位于地址0x1000,值为10。指针变量p位于地址0x2000,其存储的值是0x1000
  • 调用tryChangePtr(p)时,将p的值0x1000拷贝给形参ptrptrtryChangePtr栈帧(地址0x3000)上的一个变量。
  • 函数内执行ptr = &newValue;newValue是函数内的局部变量,假设在地址0x4000。这条语句将ptr这个变量(在0x3000)里存储的值从0x1000改成了0x4000
  • 函数返回后,tryChangePtr的栈帧被销毁,newValueptr都不复存在。main中的p(在0x2000)其值始终是0x1000,从未被改变。

场景二:成功修改指针所指内存内容

void modifyTarget(int *ptr) { *ptr = 999; // 正确!通过地址副本找到了原始内存并修改 } int main() { int a = 10; int *p = &a; modifyTarget(p); printf("a = %d\n", a); // 输出 999 return 0; }

内存模型解读:

  • 传递过程同上,ptr获得了p的值的副本0x1000
  • 函数内执行*ptr = 999;。CPU 会进行“解引用”:取出ptr变量里的值0x1000,然后向内存地址0x1000写入数据999
  • 地址0x1000main中变量a所在的位置。因此,a的值被成功修改。这里改变的是指针指向的内存,而非指针变量本身。

场景三:通过二级指针改变外部指针

void allocateMemory(int **pptr) { *pptr = (int*)malloc(sizeof(int)); // 正确!通过二级指针修改了一级指针的值 **pptr = 1234; } int main() { int *p = NULL; allocateMemory(&p); // 传入指针p的地址 printf("*p = %d\n", *p); // 输出 1234 free(p); return 0; }

内存模型解读:

  • main中,指针p位于地址0x2000,其值为NULL(0x0)。
  • &p获取的是变量p的地址,即0x2000。这个地址值被拷贝给allocateMemory的形参pptr。所以pptr是一个二级指针,它存储的值是0x2000
  • 函数内执行*pptr = ...。解引用pptr:取出pptr的值0x2000,然后向这个地址(即main中的p变量所在处)写入一个新的地址值(malloc返回的堆内存地址,假设为0x5000)。
  • 这样,就直接修改了mainp这个指针变量本身的值,使其从NULL变成了0x5000

注意:场景三中在函数内部分配内存,调用者负责释放,这是一种常见模式。但务必注意职责清晰,避免内存泄漏。更好的现代C++做法是使用智能指针,将所有权语义自动化。

3. 何时需要及如何改变外部指针

理解了原理,我们就能系统地回答“何时”以及“如何”改变外部指针。

3.1 需要改变外部指针的典型场景

  1. 动态内存分配:这是最经典的场景。函数内部需要为指针分配新的内存(如malloc,new,calloc),并让调用者的指针指向这块新内存。例如,创建链表节点、读取整个文件到缓冲区、初始化一个复杂结构体。
  2. 指针重新指向:函数需要根据条件,让外部的指针指向另一个已存在的有效对象(可能是静态变量、全局变量或另一个传入的参数),而不是它原来指向的对象。
  3. 指针置空或销毁:在释放内存或资源后,将外部的指针设置为NULL,防止其成为悬空指针(Dangling Pointer)。这是一个非常重要的安全实践。
  4. 实现“输出参数”:当函数需要返回多个值,而其中一个值是指针类型时。虽然可以通过返回指针来实现,但使用指针的指针(或引用)作为参数,可以使函数签名更清晰(区分输入和输出),或者在错误时返回NULL而通过输出参数返回结果。

3.2 如何改变:二级指针与指针引用

**方法一:使用二级指针 (int)这是C语言中的标准做法,如场景三所示。

  • 函数签名void func(TYPE **pptr)
  • 调用方式func(&ptr);
  • 函数内操作*pptr = new_value;
  • 优点:概念清晰,C/C++通用,是底层库和C接口的常见模式。
  • 缺点:语法稍显复杂,容易因多级解引用而出错(如写成**pptr来赋值,而本意是*pptr)。

方法二:使用指针的引用 (int&)* 这是C++特有的、更优雅的方式。

  • 函数签名void func(TYPE* &ptrRef)
  • 调用方式func(ptr);// 直接传指针本身
  • 函数内操作ptrRef = new_value;
  • 优点:语法简洁,直观。ptrRef就是外部指针ptr的一个别名,操作它就像操作ptr本身。
  • 缺点:仅适用于C++。对于不熟悉引用的人来说,可能掩盖了“参数可能被修改”的事实(通常需要通过函数名或注释说明)。

两种方法对比与选择建议

特性二级指针 (int **)指针引用 (int*&)
语言C / C++C++ Only
语法清晰度明确显示了参数可能被修改(需要取地址&)简洁,但修改意图可能被隐藏
可读性对于明确要修改指针的场景很直观更像在操作指针本身,代码更干净
错误风险易混淆*pptr**pptr的用法较低,用法和普通指针几乎一致
适用场景C代码、C/C++混合项目、底层API纯C++项目,追求代码简洁性

实操心得:在纯C++项目中,我个人更倾向于使用指针引用,它让代码更整洁。但在编写供C调用的库接口,或者团队中C背景成员较多时,二级指针是更安全、更通用的选择。无论哪种,在函数注释中务必明确指出该参数是“输出参数”,并说明其内存管理责任(谁分配、谁释放)。

3.3 示例:一个完整的动态数组创建函数

让我们用一个例子融合上述场景。假设我们要写一个函数,读取一个文件中的所有整数到一个动态数组中,并将数组和大小返回给调用者。

C语言版本(使用二级指针)

#include <stdio.h> #include <stdlib.h> // 返回值表示成功与否,数组和大小通过输出参数返回 int read_ints_from_file(const char* filename, int** out_array, int* out_count) { if (!filename || !out_array || !out_count) { return 0; // 无效参数 } FILE* fp = fopen(filename, "r"); if (!fp) return 0; int capacity = 10; int count = 0; int* array = (int*)malloc(capacity * sizeof(int)); if (!array) { fclose(fp); return 0; } int value; while (fscanf(fp, "%d", &value) == 1) { if (count >= capacity) { capacity *= 2; int* new_array = (int*)realloc(array, capacity * sizeof(int)); if (!new_array) { free(array); fclose(fp); return 0; } array = new_array; } array[count++] = value; } fclose(fp); // 关键步骤:通过二级指针改变外部指针 *out_array = array; // 让调用者的指针指向我们分配的内存 *out_count = count; // 设置大小 return 1; // 成功 } int main() { int* my_array = NULL; int count = 0; if (read_ints_from_file("data.txt", &my_array, &count)) { printf("Read %d numbers:\n", count); for (int i = 0; i < count; ++i) { printf("%d ", my_array[i]); } printf("\n"); free(my_array); // 调用者负责释放 } else { printf("Failed to read file or allocate memory.\n"); } return 0; }

C++版本(使用指针引用和vector,更现代)

#include <iostream> #include <fstream> #include <vector> bool read_ints_from_file_cpp(const std::string& filename, std::vector<int>& out_vec) { std::ifstream infile(filename); if (!infile.is_open()) return false; out_vec.clear(); int value; while (infile >> value) { out_vec.push_back(value); } return !out_vec.empty(); // 或根据实际需求判断 } // 如果坚持要用指针和大小(例如与C API交互),使用引用 bool read_ints_from_file_raw(const std::string& filename, int*& out_ptr, int& out_size) { std::vector<int> vec; if (!read_ints_from_file_cpp(filename, vec)) { out_ptr = nullptr; out_size = 0; return false; } out_size = vec.size(); out_ptr = new int[out_size]; // 在堆上分配 std::copy(vec.begin(), vec.end(), out_ptr); return true; } int main() { // 现代C++首选vector std::vector<int> data; if (read_ints_from_file_cpp("data.txt", data)) { for (int num : data) std::cout << num << ' '; std::cout << '\n'; } // 原始指针版本(需手动管理内存) int* raw_array = nullptr; int raw_size = 0; if (read_ints_from_file_raw("data.txt", raw_array, raw_size)) { // 使用 raw_array... delete[] raw_array; // 务必释放 } return 0; }

重要提示:在C++中,除非有极特殊的性能要求或与C库交互,否则应优先使用std::vectorstd::unique_ptr等智能指针和容器。它们自动管理内存,彻底避免了手动传递和修改原始指针的复杂性及风险。上面展示原始指针方法主要是为了说明原理。

4. 常见陷阱、调试技巧与VSCode实战

理解了正确做法,我们更要看清那些容易踩的坑。很多指针相关的问题在编译时不会报错,直到运行时才以“段错误”或“数据错误”的形式出现。

4.1 五大经典陷阱与避坑指南

  1. 陷阱一:误以为能改变一级指针就是本章开头提到的失败案例。牢记:想改变什么,就传递它的地址。想改变int,传int*;想改变int*,就得传int**

  2. 陷阱二:返回局部变量的地址

    int* dangerous_func() { int local = 42; return &local; // 严重错误!函数返回后local的内存失效。 }

    避坑:永远不要返回局部变量或临时对象的地址或引用。需要返回指针时,应返回指向动态内存(malloc/new)、静态内存或传入参数的指针。

  3. 陷阱三:解引用空指针或未初始化指针

    int *p; // 未初始化,野指针 *p = 10; // 未定义行为,很可能崩溃 int *q = NULL; *q = 20; // 解引用空指针,必然崩溃

    避坑:定义指针时立即初始化为NULL或有效地址。在使用前总是检查指针是否为NULL

  4. 陷阱四:内存泄漏与重复释放当函数通过二级指针成功分配内存后,调用者必须记得在适当的时候freedelete。忘记释放导致泄漏;释放后再次使用或释放(未置空)导致未定义行为。避坑:遵循“谁分配,谁释放”原则可能模糊,更佳原则是“所有权清晰”。使用函数注释明确责任。在C++中,使用智能指针(std::unique_ptr)可以自动化这个过程。

    void safe_func(std::unique_ptr<int>& ptr_ref) { ptr_ref.reset(new int(100)); // 自动管理旧内存,分配新内存 } // 无需手动delete
  5. 陷阱五:指针类型不匹配与字节对齐void*强制转换为其他类型指针时,或进行指针算术运算时,要确保内存对齐和类型大小正确。避坑:在C中,对void*赋值不需要强制转换,但从void*转为其他指针需要。在C++中,static_cast比C风格转换更安全。进行指针运算时,牢记p+1移动的是sizeof(*p)字节。

4.2 调试技巧:在VSCode中洞察指针

现代IDE让指针调试直观很多。以VSCode配置C/C++环境为例(这也是热搜词里的高频问题)。

首先,确保你的VSCode配置好了C/C++调试环境:

  1. 安装扩展:C/C++(Microsoft官方扩展)。
  2. 编写一个简单的test_pointer.c文件,包含上述某个示例代码。
  3. F5选择调试环境,通常选择gccclang来生成和调试活动文件。VSCode会自动生成一个launch.jsontasks.json。确保tasks.json中的编译命令包含了调试信息-g,例如:
    "args": [ "-fdiagnostics-color=always", "-g", // 这是关键!生成调试符号 "${file}", "-o", "${fileDirname}/${fileBasenameNoExtension}" ],
  4. 在函数调用、指针赋值等关键行设置断点。

调试时的关键观察窗:

  • 变量窗口 (Variables):可以看到所有局部变量的值。对于指针变量,它会显示其存储的地址值(如0x7ffeed0)和其指向的值(如果地址有效,如*(int*)0x7ffeed0 = 10)。
  • 监视窗口 (Watch):添加自定义表达式。例如,添加&p可以查看指针变量p自身的地址。添加*p可以持续观察解引用的值。这对于跟踪二级指针非常有用:添加pptr*pptr**pptr来层层观察。
  • 内存窗口 (Memory):在VSCode中,你可以在调试控制台输入-exec x/10xw 0x7ffeed0这样的GDB命令(如果后端是GDB),来查看从某个地址开始的内存内容。这能让你最直观地看到内存的原始布局。
  • 调用堆栈 (Call Stack):清晰地展示函数调用链,帮助你理解当前栈帧的上下文,区分不同函数中的同名局部变量(如mainpfuncp)。

实操心得:当程序在指针操作处崩溃(如Segmentation fault)时,第一时间在崩溃前的语句设断点。运行到断点后,在变量窗检查可疑指针的值:

  • 如果是0x0(nil),那就是解引用了空指针。
  • 如果是一个很小的、奇怪的地址(如0x1),可能是未初始化的野指针。
  • 如果地址看起来正常,但访问出错,可能是内存已被释放(悬空指针)。这时可以尝试在内存窗查看该地址是否可读。

4.3 解读编译与构建输出

热搜词里提到了一个编译问题:section .bootdsp2su.out(.text) at 0100010h falls in unconfigured memory (skipped)。这本身不是一个C/C++语法或指针错误,而是一个链接器(Linker)警告,通常出现在嵌入式开发或使用特定链接脚本时。

  • 含义:链接器在尝试将名为.bootdsp2su.out的输出段中的.text段(代码段)放置到内存地址0x0100010时,发现这个地址在你的目标芯片或模拟器的内存映射中未被定义或配置(unconfigured memory)。因此链接器跳过了这个放置请求(skipped)。
  • 与指针的关系:这个警告本身不直接导致指针错误。但是,在嵌入式系统中,如果你错误地配置了内存区域,导致代码或数据被放到了不该放的地方,那么后续通过指针访问这些区域就可能导致硬件异常或不可预知的行为。
  • 解决方法:这需要检查你的链接脚本(.ld文件)或IDE中的内存配置,确保所有使用的内存区域(如FLASH, RAM)都在目标设备的有效地址范围内,并且大小定义正确。对于桌面普通C/C++开发,通常不会遇到此问题。

对于更常见的指针问题,编译器(如gcc)通常会给出警告:

  • warning: function returns address of local variable-> 对应陷阱二。
  • warning: ‘p’ is used uninitialized in this function-> 对应陷阱三。 养成编译时开启并关注所有警告的习惯(-Wall -Wextra),可以提前消灭大量潜在的指针bug。

5. 高级话题:指针传递在复杂数据结构中的应用

掌握了基础,我们可以看看指针传递在更复杂场景下的威力,这能帮助我们设计出更高效、更清晰的API。

5.1 修改链表或树结构的头节点/根节点

这是二级指针的经典应用场景。在链表插入、删除节点,特别是可能改变头节点本身时(如在空链表中插入第一个节点,或删除头节点),需要传递头指针的地址。

// 链表节点定义 typedef struct Node { int data; struct Node* next; } Node; // 错误示范:只传一级指针,无法改变main中的head void insert_front_bad(Node* head, int value) { Node* new_node = create_node(value); new_node->next = head; head = new_node; // 这只改变了形参head,外部的head没变 } // 正确示范:传递二级指针 void insert_front(Node** head_ref, int value) { Node* new_node = create_node(value); new_node->next = *head_ref; // 新节点指向原头节点 *head_ref = new_node; // 让外部的头指针指向新节点 } // 删除链表头节点 void delete_front(Node** head_ref) { if (*head_ref == NULL) return; Node* temp = *head_ref; // 保存原头节点 *head_ref = (*head_ref)->next; // 外部头指针指向下一个节点 free(temp); // 释放原头节点 } int main() { Node* head = NULL; // 初始为空链表 insert_front(&head, 10); // 插入后,head指向新节点 insert_front(&head, 20); delete_front(&head); // 删除头节点20,head现在指向节点10 // ... 后续操作和清理 return 0; }

5.2 在函数间传递“句柄”或“上下文”

在许多库的设计中(如热搜词里提到的memcached有C和C++接口),你会看到一个不透明的指针(void*或某个结构体指针)被用作“句柄”。这个句柄在初始化函数中被创建并返回(或通过输出参数设置),然后在后续的一系列函数调用中传递。这些函数内部通过这个指针来访问和修改一个庞大的上下文结构体。

// 一个简化的上下文管理器示例 typedef struct ContextImpl Context; // 前向声明,隐藏实现细节 // 创建上下文,通过输出参数返回句柄 int context_create(Context** ctx_out) { Context* ctx = (Context*)malloc(sizeof(Context)); if (!ctx) return ERROR_NO_MEMORY; // ... 初始化ctx的各个字段 ... *ctx_out = ctx; // 关键:通过二级指针输出 return SUCCESS; } // 使用上下文 int context_do_something(Context* ctx, int param) { if (!ctx) return ERROR_INVALID_HANDLE; // 通过ctx指针访问和修改内部状态 // ctx->internal_data = ...; return SUCCESS; } // 销毁上下文 int context_destroy(Context** ctx_ref) { if (!ctx_ref || !*ctx_ref) return ERROR_INVALID_ARG; free(*ctx_ref); *ctx_ref = NULL; // 关键:将外部指针置空,防止悬空指针 return SUCCESS; } int main() { Context* my_ctx = NULL; int ret = context_create(&my_ctx); // 获取句柄 if (ret == SUCCESS) { context_do_something(my_ctx, 42); // 传递句柄使用 context_destroy(&my_ctx); // 销毁并置空 // 此时my_ctx == NULL,安全 } return 0; }

这种模式实现了信息隐藏和资源的安全生命周期管理。context_destroy函数接受二级指针,确保了在释放内存后能将调用者的指针变量置为NULL,这是一个非常好的实践。

5.3 指针与常量限定符(const)的协作

const和指针结合,能表达丰富的语义,是编写健壮接口的关键。

  • const int* pint const* p: 指向常量的指针。指针本身可以改变指向,但不能通过它修改所指内存的内容。常用于只读访问传入的数据。
  • int* const p: 常量指针。指针本身(存储的地址)不能改变,但可以通过它修改所指内存的内容。较少单独使用。
  • const int* const p: 指向常量的常量指针。既不能改指向,也不能改内容。
  • int const* const* pp: 指向“指向常量的指针”的常量指针。多级指针与const的组合,用于保护多层数据。

当使用二级指针传递时,const可以保护不同层级的数据:

// 这个函数承诺不会修改外部指针(pptr)本身,也不会修改pptr指向的那个指针,但可能会修改最终指向的整数? // 实际上,由于最里层是const int,所以整数也不能改。 void func(const int* const* pptr) { // *pptr = something; // 错误:不能修改*pptr (因为*pptr是const) // **pptr = 100; // 错误:不能修改**pptr (因为**pptr是const) int x = **pptr; // 正确:可以读取 const int* new_ptr = &x; // pptr = &new_ptr; // 正确:可以修改形参pptr本身(最外层),但这不影响外部 }

正确使用const可以借助编译器检查,防止意外修改,并作为函数契约的文档。在设计API时,应尽可能使用最严格的const限定。

指针传递的精髓在于对内存和地址操作的直接与灵活。从最基本的“值传递”理解起,到熟练运用二级指针和引用来操控指针变量本身,再到在复杂数据结构和API设计中游刃有余,这条路径需要大量的实践和思考。我个人的体会是,每当你对指针操作感到不确定时,就画一张内存布局图,把变量、地址、值都标出来,思路会立刻清晰。在C++中,虽然智能指针和容器极大地减少了原始指针的使用场景,但理解这些底层机制,对于阅读遗留代码、进行底层系统编程、以及真正理解C++对象模型,仍然是不可或缺的基石。最后,善用调试器,让机器告诉你内存中究竟发生了什么,这是验证理解、定位问题最快的方式。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/2 6:50:32

React获取DOM元素全解析:掌握Refs与Hook核心用法

一、React中获取DOM元素的背景与核心思想 1.1 为什么需要获取DOM元素 React的核心思想是声明式渲染与虚拟DOM, 通常情况下我们不需要直接操作真实DOM。但在某些特定场景下, 我们仍然需要直接获取真实的DOM节点来进行命令式操作。这些场景包括但不限于: (1) 管理焦点, 文本选择或…

作者头像 李华
网站建设 2026/8/2 6:48:23

逆向工程中的编码与加密算法识别与实战分析

1. 从“看热闹”到“看门道”&#xff1a;逆向中的编码与加密刚入门逆向那会儿&#xff0c;我总盯着那些花里胡哨的算法库和复杂的汇编跳转&#xff0c;觉得那才是“真功夫”。后来踩坑踩多了才明白&#xff0c;真正卡住新手脖子的&#xff0c;往往不是那些高深的虚拟机保护或者…

作者头像 李华
网站建设 2026/8/2 6:42:59

30B开源模型登顶科研榜:揭秘AI科研助手部署与微调实战

1. 项目概述&#xff1a;当30B开源模型站上科研之巅最近AI圈里有个事儿挺有意思&#xff0c;一个叫UniPat AI的团队&#xff0c;带着他们30B参数规模的开源模型&#xff0c;冲上了OpenAI搞的那个科研榜单的榜首。这事儿一出&#xff0c;不少朋友跑来问我&#xff0c;这“30B”到…

作者头像 李华
网站建设 2026/8/2 6:42:12

具身大模型空间智能评估新范式:从理论到实践的全面解析

1. 项目概述&#xff1a;一场关于智能“身体”的深度体检最近在ICLR 2024上看到一篇论文&#xff0c;标题是“李曼玲、李飞飞、吴佳俊等联手&#xff1a;评估具身大模型的新范式&#xff01;”&#xff0c;业内朋友讨论得挺多。简单来说&#xff0c;这篇工作提出了一套名为“Th…

作者头像 李华
网站建设 2026/8/2 6:41:14

5-数据库-SQL注入-报错注入-day13

MySQL 报错注入 — 原理与注入流程 ⚠️ 免责声明&#xff1a;本文旨在教学网络安全知识&#xff0c;帮助开发者理解SQL注入漏洞原理及防御方法。所有技术内容仅用于授权测试和安全研究&#xff0c;严禁用于任何非法攻击或未经授权的渗透测试。读者应遵守法律法规&#xff0c;因…

作者头像 李华
网站建设 2026/8/2 6:40:15

电工证学习笔记(三)

电工证 一、触电安全核心考点 人体触电电流参考值&#xff08;均为统计平均值&#xff0c;个体存在差异&#xff09; 50mA&#xff1a;公认致命电流&#xff1b;30mA&#xff1a;容易引发心室颤动、心肺功能停止&#xff1b; 部分人群耐受度高&#xff0c;部分人群低于30mA即出…

作者头像 李华