C语言内存精讲系列(二十七):C 语言变量的存储类别和生存期(上)

之前我们学过变量的数据类型 —— 比如int占 4 字节、char占 1 字节,它决定了变量 “存多大的数据”。但今天要讲的 “存储类别”,是变量的另一个核心属性:它决定了变量 “存在内存的哪个地方”“能活多久”。这就像我们生活里 “东西放哪个抽屉”“这东西能放多久不丢”,底层逻辑直接影响程序的运行效率和正确性。

一、先搞懂底层:变量存在内存的哪几个 “抽屉” 里?

首先我们要明确:程序运行时,操作系统会给它分配一块 “虚拟地址空间”,里面专门用来存变量的区域主要有 4 个 ——静态数据区(含全局数据区 + 常量区)、栈区、堆区。这四个区域的 “内存管理规则” 完全不同,直接决定了变量的 “生存期”(从创建到销毁的时间)。

我们用一个表格先把这四个区域的核心特点理清楚:

内存区域分配时机释放时机存放的变量类型分配 / 释放方式核心特点
静态数据区程序启动时(编译后)程序完全结束后全局变量、静态变量、常量自动分配 / 自动释放空间固定,一旦分配不会变化
栈区程序运行中(执行到代码)变量所在代码块结束后局部变量、函数参数自动分配 / 自动释放空间动态变化,效率高
堆区程序运行中(手动调用函数)程序运行中(手动调用函数)动态内存变量(按需申请的变量)手动分配 / 手动释放空间灵活可控,生存期由程序员决定

举个生活化的例子:

  • 静态数据区就像 “家里的衣柜”—— 衣服(变量)从你出门前(程序启动)就放好,直到回家(程序结束)才拿走,中间一直占着位置;
  • 栈区就像 “超市的购物车”—— 你选商品(执行代码)时推上车(分配内存),结账后(代码块结束)购物车就还给超市(释放内存),别人可以接着用;
  • 堆区就像 “租来的储物间”—— 你需要时主动联系房东(调用函数)租下(分配内存),不用时必须主动退租(调用函数释放),否则会一直占用,且储物间大小可按需选择。

 

二、控制变量 “放哪里” 的 4 个关键字

C 语言提供了 4 个关键字,让我们能主动控制变量的存储类别,分别是autostaticregisterextern。今天我们重点讲前三个和 “堆区动态变量”,extern涉及模块化开发,后续再细讲。

1. auto变量:默认的 “栈区变量”,不用特意写

auto的意思是 “自动”,它是局部变量的默认存储类别—— 也就是说,我们平时写的局部变量,不管加不加auto,都默认存在栈区,生存期就是 “所在代码块(比如函数、循环)执行的时间”。

比如下面两段代码,效果完全一样:

#include <stdio.h>
int main() {
    // 没写auto,默认是auto变量,存在栈区
    int a = 10;  
    // 写了auto,和上面完全一样,没必要多此一举
    auto int b = 20;  

    printf("a=%d, b=%d\n", a, b); // 输出a=10, b=20
    // main函数结束后,a和b所在的栈区内存自动释放
    return 0;
}

这里要注意一个细节:栈区变量的 “分配时机” 是 “执行到定义代码时”。比如下面的代码,c虽然定义了,但因为if(0)的条件不成立,永远不会执行到int c=30;,所以c不会在栈区分配内存:

#include <stdio.h>
int main() {
    if (0) { // 条件不成立,代码块不会执行
        int c = 30; // c不会分配栈内存
    }
    // printf("%d\n", c); // 报错:c未定义(因为没分配内存)
    return 0;
}

2. static变量:存在 “静态数据区”,能 “活” 到程序结束

static的意思是 “静态”,它的核心作用是 “把变量放到静态数据区”—— 不管这个变量是全局的还是局部的,只要加了static,就会存在静态数据区,生存期延长到 “整个程序运行的时间”。

这里有两个关键特性,我们结合代码来理解,先看第一个:静态局部变量,函数结束后不销毁

代码示例 1:用静态变量实现 “累加求和”

比如我们要计算 1+2+3+...+100,用static变量就能很方便地实现 “每次调用函数时,变量值不重置”:

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

// 定义求和函数,参数n是当前要加的数
int sum(int n) {
    // 静态局部变量:加了static,存在静态数据区
    // 注意1:静态数据区变量默认初始化为0,即使不写=0也没问题
    // 注意2:这行代码只执行一次(程序启动时初始化),后续调用sum不执行
    static int result = 0; 
    
    result += n; // 每次调用sum,都在之前的result基础上加n
    return result; // 返回当前累加结果
}

int main() {
    int final_result, i; // 普通局部变量,存在栈区
    
    // 循环100次,每次调用sum(i),把i累加到result中
    for (i = 1; i <= 100; i++) {
        final_result = sum(i); // 每次调用sum后,final_result更新为当前总和
    }
    
    // 输出最终结果:1+2+...+100=5050
    printf("1+2+3+...+99+100 = %d\n", final_result);
    system("pause"); // 让程序暂停,方便查看结果
    return 0;
}

我们来拆解这个代码的底层逻辑:

  1. 程序启动时:静态变量result在静态数据区分配内存,并初始化为 0(只执行这一次);
  2. 第一次调用sum(1)result = 0 + 1 = 1,返回 1,sum函数结束后,result在静态数据区不销毁;
  3. 第二次调用sum(2)result = 1 + 2 = 3,返回 3,依然不销毁;
    ...
  4. 第一百次调用sum(100)result = 4950 + 100 = 5050,返回 5050;
  5. 程序结束后:静态数据区的result才被释放,栈区的final_resultimain结束后就释放了。

静态变量的第二个关键特性:作用域不变,只在定义它的代码块内有效

比如上面的代码中,sum函数里的static int result,虽然存在静态数据区,但它的 “作用域” 仅限于sum函数内部 —— 在main函数里,我们不能直接访问result,必须通过sum函数的返回值间接获取。这就像 “衣柜里的衣服虽然一直放着,但只有你能拿,别人拿不到”。

如果我们在main里写printf("%d\n", result);,编译器会直接报错 “result未定义”,因为result的作用域只在sum函数内。

3. register变量:存在 “CPU 寄存器” 里,速度更快

我们先想一个问题:CPU 处理数据时,是从 “内存” 读数据快,还是从 “自己身上的寄存器” 读数据快?答案肯定是寄存器 —— 内存相当于 “家里的抽屉”,寄存器相当于 “手上的口袋”,从口袋里拿东西肯定比跑回房间开抽屉快。

register关键字的作用,就是 “建议编译器把变量放到 CPU 的通用寄存器里”,减少 CPU 从内存读取数据的次数,从而提高程序运行效率。它主要用在 “使用非常频繁的变量” 上,比如循环的增量变量(for循环里的i)。

代码示例 2:用register变量优化 “计算 π 的近似值”

我们用一个经典的公式计算 π 的近似值:π ≈ 4*(1 - 1/3 + 1/5 - 1/7 + 1/9 - ...),循环次数越多,结果越精确。这里的循环变量i要被使用 1 亿次,用register优化很合适:

#include <stdio.h>
// getch()函数需要的头文件,用于程序暂停查看结果
#include <conio.h>

int main() {
    // 关键:把循环变量i定义为register变量,建议编译器放CPU寄存器
    // 理由:i要循环1亿次,每次都要被访问,放寄存器能减少内存读取
    register int i = 0;  
    
    double sign = 1.0;  // 控制正负号,初始为正
    double res = 0.0;   // 存储累加结果
    double ad = 1.0;    // 每次要加的项(1, -1/3, 1/5...)

    // 循环1亿次,计算近似值(次数越多,π越精确)
    for (i = 1; i <= 100000000; i++) {
        res += ad;          // 累加当前项
        sign = -sign;       // 切换正负号(正→负→正...)
        ad = sign / (2*i + 1); // 计算下一个要加的项
    }

    res *= 4;  // 根据公式,最终结果乘以4得到π的近似值
    printf("pi is %f\n", res); // 输出结果,约为3.141593
    getch();   // 暂停程序,避免窗口一闪而过
    return 0;
}

这段代码中,register int i的底层优化逻辑是:

  • 如果没有register:CPU 每次判断i <= 1e8、执行i++时,都要从栈区读取i的值,1 亿次循环就要读 1 亿次内存;
  • 有了register(且编译器支持):i被放到 CPU 寄存器里,1 亿次访问都直接从寄存器读,速度会快很多(尤其是循环次数极大时,差距更明显)。

关于register变量的 4 个关键注意事项(必须记牢)

  1. 只有局部变量和函数参数能定义为register
    全局变量和静态变量不能用register,因为它们的生存期是 “整个程序”,而寄存器数量有限,不可能长期占用一个寄存器给全局变量。

  2. static变量不能和register一起用
    一个变量只能有一个存储类别 ——static是 “放静态数据区”,register是 “放寄存器”,两者冲突,比如register static int a = 10;会直接报错。

  3. 只有 “小类型” 变量适合register
    寄存器的长度通常和 CPU 字长一致(比如 32 位 CPU 寄存器是 4 字节,64 位是 8 字节),所以int(4 字节)、char(1 字节)、short(2 字节)适合用register;而double(8 字节,部分 CPU 支持)、long long(8 字节)或结构体(更大)不推荐,因为可能放不下寄存器,编译器会自动当成普通变量处理。

  4. 编译器有 “最终决定权”
    即使你写了register,如果寄存器已经被其他变量占用,或者编译器判断这个变量 “不常用”,也会自动忽略register,把变量放到栈区 ——register只是 “建议”,不是 “强制”。现在很多优秀的编译器(比如 GCC、Clang)还会自动识别频繁使用的变量(比如循环变量),即使你没写register,也会主动放到寄存器里优化。

4. 堆区变量:手动管理的 “灵活内存”,生存期由程序员决定

前面讲的静态数据区、栈区、寄存器变量,要么是 “自动分配释放”(栈区、静态数据区),要么是 “编译器建议分配”(寄存器),而堆区变量完全由程序员手动控制—— 需要时调用函数申请内存,不用时必须调用函数释放内存,是唯一能 “按需灵活调整大小” 的内存区域。

堆区的核心价值是解决 “静态数据区不灵活、栈区空间有限” 的问题:比如要存储 “用户输入的不确定数量的学生信息”(用户可能输入 10 个,也可能输入 1000 个),静态数据区和栈区无法动态适配,这时候就需要堆区。

堆区变量的 “分配 / 释放” 工具:3 个核心函数

C 语言标准库(stdlib.h头文件)提供了 3 个关键函数,用于堆区内存的操作:

  1. malloc(size_t size):申请堆内存

    • 参数size:要申请的 “字节数”(比如申请 1 个int的内存,就是sizeof(int));
    • 返回值:成功时返回 “堆内存的起始地址”(需强制转换为对应类型指针),失败时返回NULL
  2. calloc(size_t n, size_t size):申请并初始化堆内存

    • 参数n:元素个数,size:单个元素字节数;
    • 总申请字节数 = n * size,且会自动把内存初始化为 0(malloc申请的内存是 “垃圾值”)。
  3. free(void *ptr):释放堆内存

    • 参数ptrmalloc/calloc申请的堆内存地址;
    • 必须手动调用,否则会导致 “内存泄漏”(堆内存一直被占用,直到程序结束)。

代码示例 3:用堆区存储 “不确定数量的学生成绩”

比如我们要实现 “让用户输入学生人数,再输入对应成绩,最后计算平均分”,学生人数由用户决定,适合用堆区动态申请内存:

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

int main() {
    int student_num; // 学生人数,栈区变量
    int *score_ptr;  // 指向堆内存的指针,栈区变量(指针本身在栈区,指向的内容在堆区)
    int sum = 0;     // 成绩总和,栈区变量
    int i;           // 循环变量,栈区变量

    // 1. 让用户输入学生人数
    printf("请输入学生人数:");
    scanf("%d", &student_num);

    // 2. 申请堆内存:存储student_num个int类型的成绩
    // 注意:必须判断是否申请成功(返回NULL表示失败)
    score_ptr = (int *)malloc(student_num * sizeof(int));
    if (score_ptr == NULL) {
        printf("堆内存申请失败!\n");
        exit(1); // 退出程序,避免后续错误
    }

    // 3. 让用户输入每个学生的成绩,存入堆内存
    printf("请输入%d个学生的成绩:\n", student_num);
    for (i = 0; i < student_num; i++) {
        // 用指针下标访问堆内存:score_ptr[i] 等价于 *(score_ptr + i)
        scanf("%d", &score_ptr[i]);
        sum += score_ptr[i]; // 累加成绩
    }

    // 4. 计算并输出平均分
    printf("学生成绩平均分:%.2f\n", (double)sum / student_num);

    // 5. 释放堆内存(关键!必须手动释放,否则内存泄漏)
    free(score_ptr);
    score_ptr = NULL; // 释放后把指针置为NULL,避免野指针(后续误操作会报错)

    system("pause");
    return 0;
}

我们拆解堆区变量的底层逻辑:

  1. 变量存储位置:

    • student_numsumiscore_ptr(指针本身)都在栈区;
    • score_ptr指向的 “student_numint的内存” 在堆区,是手动申请的动态内存。
  2. 内存生命周期:

    • 栈区变量:main函数结束后自动释放;
    • 堆区内存:malloc调用时分配,free调用时释放(如果不写free,堆内存会一直占用,直到程序结束 —— 这就是 “内存泄漏”,多次运行会耗尽系统内存)。

堆区变量的 3 个核心注意事项(避坑关键)

  1. 申请后必须判断是否成功(返回值是否为NULL
    若系统内存不足,malloc/calloc会返回NULL,如果直接用NULL访问堆内存,程序会崩溃。所以必须加if (ptr == NULL)的判断(如代码示例 3)。

  2. 释放后必须把指针置为NULL
    free(score_ptr)只是 “释放堆内存”,但score_ptr(指针本身)还在栈区,依然指向原来的堆地址(此时该地址已无效)—— 这就是 “野指针”。若后续误写*score_ptr = 90,会访问无效内存导致崩溃,所以释放后要加score_ptr = NULL

  3. 不能重复释放或释放非堆内存

    • 重复释放:free(score_ptr); free(score_ptr);会报错(堆内存已释放,再次释放非法);
    • 释放非堆内存:int a = 10; free(&a);会报错(&a是栈区地址,free只能释放堆地址)。

 

三、总结:存储类别决定生存期,关键字 + 函数控制存储位置

最后我们用一张图把今天的核心知识点串起来,帮大家巩固记忆:

简单来说:

  • 想让变量 “用完就丢,不占长期内存”,用默认的auto(栈区);
  • 想让变量 “长期存在,下次调用还能复用值”,用static(静态数据区);
  • 想让变量 “访问超快,频繁使用不耗时”,用register(寄存器,需满足条件);
  • 想让变量 “大小灵活,按需分配”,用堆区(malloc/calloc申请,free释放)。

下节课我们会讲extern关键字,以及动态内存分配(堆区变量),大家可以先预习一下malloc函数的基本用法~

 

内容概要:本文提出了一种结合在线鲁棒主成分分析(RPCA)模型与长短期记忆(LSTM)循环网络的商品需求预测方法,并提供了完整的Python代码实现。该方法首先利用RPCA模型对原始商品需求时间序列进行分解,分离出低秩的潜在趋势成分与稀疏的异常波动成分,有效实现数据去噪与异常值修正,提升输入数据的鲁棒性;随后将净化后的数据输入LSTM网络,充分挖掘时间序列中的长期依赖关系与时序模式,从而提高对未来需求的预测精度。整个模型设计针对实际商业场景中普遍存在的数据噪声大、波动剧烈、突发性事件干扰等问题,展现出较强的稳定性与预测能力。文中通过实验验证了该混合模型在多个指标上优于传统统计模型及单一LSTM模型,体现了其在复杂环境下的优越性能。; 适合人群:具备一定Python编程能力机器学习基础知识,从事数据分析、供应链管理、电商运营、零售优化及相关领域研究的研发人员或研究生;特别适合关注时间序列预测、深度学习建模以及鲁棒数据处理技术的技术人员。; 使用场景及目标:①应用于电商平台、零售企业或制造行业中的销量预测,以支持库存优化、生产计划制定与物流调度决策;②为科研工作者提供一种融合鲁棒统计与深度学习的预测建模范例,推动高噪声环境下预测算法的创新与复现研究;③帮助开发者深入理解RPCA与LSTM的集成机制,掌握复杂预测模型的构建、训练与调优流程。; 阅读建议:建议读者结合所提供的Python代码逐步实现模型,重点理解RPCA在数据预处理阶段的作用机制以及LSTM网络的结构设计与超参数配置。学习过程中应在真实或模拟数据集上复现实验结果,对比不同参数设置下的模型表现,以深化对模型内在工作原理的理解。同时可进一步探索其他深度学习模型(如GRU、Transformer)与鲁棒分解方法(如VMD、STL)的融合可能性,拓展应用场景。
内容概要:本文针对传统三电平并网逆变器在谐波抑制、电网不平衡适应性及动态响应方面的不足,提出一种基于有源中点箝位(ANPC)三电平拓扑的高性能并网控制策略。通过融合双极性倍频脉宽调制(DPWMA)、正负序分离锁相技术与电网电压前馈控制,构建一体化控制系统。ANPC拓扑具备开关损耗均衡、中点电位可控、输出谐波低等优势,为系统性能提升提供硬件基础;DPWMA调制有效提升等效开关频率,显著降低输出电压电流的低次谐波含量,优化稳态电能质量;正负序分离锁相技术可精准提取电网正序分量,实现不平衡工况下的精确同步,保障并网电流对称性;电网电压前馈控制则提前补偿电网扰动,大幅缩短动态调节时间,抑制电压骤变引起的电流畸变与功率冲击。文章通过Simulink仿真平台对稳态、电网不平衡及动态切换等多种工况进行全面验证,结果表明该复合控制策略能显著提升系统的电能质量、运行稳定性与工况适应能力,适用于新能源发电、工业变频等大功率高质量并网应用场景。; 适合人群:具备电力电子与电力系统基础知识,从事新能源并网、电能质量治理、大功率变流器控制等方向研究的研究生、科研人员及工程技术人员。; 使用场景及目标:①研究高电能质量要求下的大功率并网逆变器系统设计方法;②掌握DPWMA调制、正负序分离锁相、电网电压前馈等先进控制技术的原理与协同机制;③提升在电网电压不平衡、动态扰动等复杂工况下的系统稳定控制能力;④为实际工程应用或学术研究提供可复现的仿真模型与技术解决方案。; 阅读建议:此资源以Simulink仿真为核心,结合理论分析与性能验证,建议读者结合文中控制策略的原理讲解,动手搭建仿真模型,重点理解DPWMA调制逻辑、正负序分解算法及前馈控制的实现方式,并通过不同工况下的仿真对比,深入掌握各模块对系统性能的贡献。
源码链接: https://pan.quark.cn/s/7d0192dd9e83 【定制系统更新包 A300】是一款专门为联想A300手机设计的系统升级文件,其核心功能在于改善设备的运行表现并实现个性化调整。在信息技术领域中,刷机这一术语指的是对手机、平板等智能终端的操作系统进行更换或升级,通常目的是为了解锁更多功能、加快处理速度或解决原装系统存在的缺陷。针对此特定的更新包,我们着重分析以下几个关键点: 1. **官方系统固件的定制化版本**:ROM(只读存储器)在移动设备中代表存储系统数据的非易失性存储,官方ROM是由设备生产商发布的初始系统软件。而定制化版本则表明该更新包在官方版本的基础上进行了调整改进,可能包含对系统核心、应用程序、用户界面等部分的修改。 2. **用户界面定制功能**:更新包内含的个性化选项允许用户依据个人偏好调整手机的外观操作环境,如图标样式、桌面背景、启动应用等,从而创造更加独特的操作感受。这些定制可能涉及对系统底层设置的深入修改,例如字型更换、主题设计等。 3. **系统稳定性响应速度的提升**:这是该更新包的主要优势之一,意味着经过优化的系统能够保证设备在运行过程中的可靠性,减少系统崩溃或运行迟缓的情况,同时增强操作的流畅性,从而提升用户日常操作的满意度。 4. **电源管理脚本的集成**:省电脚本是一种自动监控设备能耗的程序,通过调节硬件参数、关闭非必要进程等方式降低电池消耗。在此次更新包中,该脚本被整合进系统,旨在延长手机的续航能力,对于电池容量有限或经常需要外出的用户尤为适用。 5. **运行效能的强化**:这表明更新包的另一核心目标在于增强设备的处理能力,可能涉及提升中央处理器的运算效率、改进内存使用策略、减...
代码转载自:https://pan.quark.cn/s/db56051ee6da 依据所提供的文档资料,能够归纳出以下与“寻求一个字符串中连续出现频率最高的子串”相关的基础知识: ### 一、问题的阐述与剖析 #### 1.1 问题背景 在计算机科学领域中,字符串操作是一项普遍且关键的工作。本议题聚焦于给定字符串,识别其中连续出现频率最高的子串。 #### 1.2 问题陈述 假定存在一个输入字符串 `str`,目标在于找出该字符串中出现频率最高的一个或多个连续子串,并统计它们的出现频次。 #### 1.3 输入输出格式 - **输入**:一个字符串 `str`。 - **输出**:连续出现频率最高的子串及其对应的出现次数。 ### 二、算法的构思与执行 #### 2.1 基本理念 遍历字符串的所有可能子串,并借助某种数据结构来追踪每个子串的出现频次。通过对比所有子串的出现频次,从而识别出出现频次最高的子串。 #### 2.2 具体执行步骤 1. **初始化**:设定一个字符串 `str` 来存储输入的字符串,以及一个辅助变量 `tep` 来暂存当前子串。 2. **外层循环**:从字符串长度减去1至1的逆向遍历,每次循环的 `i` 代表子串的长度。 3. **内层循环**:从0至字符串长度减去当前子串长度的遍历,每次循环的 `j` 指示子串的起始位置。 4. **子串提取**:运用 `substr` 方法从位置 `j` 开始截取长度为 `i` 的子串并存储至 `tep` 中。 5. **子串检测**:借助 `find` `rfind` 方法分别确定子串在字符串中的初始出现位置 `t` 与最终出现位置 `num`。 6. **判定条件**:若 `t` 与...
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值