前言

什么是初始化?为什么要初始化?静态变量和局部变量的初始化又有什么区别?实际应用中应该怎么做?本文将一一回答这些问题。

什么是初始化

初始化指的是对数据对象或者变量赋予初始值。例如:

int value = 8; //声明整型变量并初始化为8
int arr[] = {1,2,3}; //声明整型数组arr,并初始化其值为1,2,3

为什么要初始化

我们来看一个示例程序。
test0.c程序清单如下:

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    int sum;
    int randNum;
    while(10 > sum)
    {
        randNum =  rand() % 10;         sum += randNum;
        printf("rand num is %d,sum is %d\n",randNum,sum);
    }
    printf("the final sum is %d\n",sum);
    return 0;
}

程序随机产生0到9的数字,使得sum的值大于或等于10时,退出程序。
编译并运行:

gcc  -o test0 test0.c
./test0

运行结果如下(每次运行结果可能不同):

rand num is 3,sum is -4040865
rand num is 6,sum is -4040859
rand num is 7,sum is -4040852
rand num is 5,sum is -4040847
rand num is 3,sum is -4040844
rand num is 5,sum is -4040839
(省略其他内容)

从运行结果来看,程序并没有达到我们的预期,这是为什么呢?

很多读者可能已经知道,问题在于声明sum之后,没有为其赋初始值,在这样的情况下,sum的值是随机的,因此在一开始sum可能是一个很小的负数,导致多次循环出现。很显然,初始化避免使用了变量的“脏值”。而将sum的声明改成如下定义即可:

int sum = 0;

如果将sum声明为静态变量,情况又会如何呢?

//test1.c
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    static int sum;
    int randNum;
    while(10 > sum)
    {
        randNum =  rand() % 10;         sum += randNum;
        printf("rand num is %d,sum is %d\n",randNum,sum);
    }
    printf("the final sum is %d\n",sum);
    return 0;
}

编译并运行:

rand num is 3,sum is 3
rand num is 6,sum is 9
rand num is 7,sum is 16
the final sum is 16

在这种情况下,程序是能够符合我们预期的结果,这又是为什么呢?原因在于静态变量会被默认初始化。例如,int类型会被初始化为0。那么问题来了:

  • 为什么局部变量未初始化的时候的值是“脏值”?

  • 静态变量和局部变量为什么又不一样呢?

在解答上面这两个问题之前,我们需要简单了解一下程序的存储空间布局。

程序的存储空间布局

C程序主要由以下几部分组成:

  • 正文段。即机器指令部分,为防止意外被修改,设为只读。

  • 初始化数据段。它包含了程序中需要明确赋初值的静态变量。

  • 未初始化数据段。它包含了程序中未赋初值的或初始化为0的静态变量,在程序开始执行之前,内核将此段中的数据初始化为0。

  • 栈。它保存了自动(局部)变量以及函数调用所要的信息。

  • 堆。用于动态内存分配。例如使用malloc函数进行内存分配。

其中,正文段和数据段的内容是“静态”的,因为在程序被编译出来之后,在整个程序地址就确定了,而堆栈中的内容是”动态”变化的,它随着进行的运行而不断变化着,再加上栈随机化的策略,使得程序每次运行时,栈的地址也是不确定的。

局部变量和静态变量的初始化有何不同

有了前面的铺垫,就很好理解两者的差别了。
未初始化的局部变量位于栈中,它的位置是不确定的,因此其值也是不确定的。当然,在windows下它的值是0xcccccccc,而“烫”字在MBCS字符集中的值为0xcccccccc,你说巧不巧?

而静态变量就不一样的,它的地址是确定的,并且存放在了数据段,而程序在运行之前,未初始化数据段的内容可以很方便地统一被初始化为0。这也就解释了前面的两个示例程序的结果为什么会不一样。我们加上一些打印,来看一看是否真的如此?

//test2.c
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    static int sum;
    int randNum;
    while(10 > sum)
    {
        randNum =  rand() % 10;         sum += randNum;
        printf("rand num is %d,sum is %d\n",randNum,sum);
    }
    printf("the final sum is %d\n",sum);
    printf("sum addr %p,randNum addr %p\n",&sum,&randNum);
    return 0;
}

编译并运行:

gcc -o test2 test2.c

运行结果1:

rand num is 3,sum is 3
rand num is 6,sum is 9
rand num is 7,sum is 16
the final sum is 16
sum addr 0x60104c,randNum addr 0x7ffd0ea8cf54

运行结果2:

rand num is 3,sum is 3
rand num is 6,sum is 9
rand num is 7,sum is 16
the final sum is 16
sum addr 0x60104c,randNum addr 0x7ffff5e3ddb4

在这里,sum是静态局部变量,而randNun是局部变量(自动变量),因此可以发现,sum的地址值总是不变的,而randNum的值却不断变化着。我们也可以通过nm命令查看sum的地址:

nm test2 |grep sum
000000000060104c b sum.2805

总结

我们来总结一下本文的主要内容:

  • 如果变量是静态的,它会被初始化为0;如果变量是自动的,它不会被初始化。

  • 静态的变量包括全局变量、静态全局变量、静态局部变量。

  • 使用局部变量之前对其进行初始化,避免使用“脏值”。

  • 从可读性考虑,静态变量也建议显示初始化。

  • 初始化为0的静态变量仍然存在未初始化数据段中(BSS段)。

送几句熟悉的话给大家:

手持两把锟斤拷,
口中疾呼烫烫烫。
脚踏千朵屯屯屯,
笑看万物锘锘锘。

思考

test1.c的代码运行结果每次都一样吗?为什么?该如何修改才能使得每次的运行结果不一样?

栈随机化的作用是什么?

推荐阅读:

C语言入坑指南-数组之谜

一个命令帮你对文本排序

如何理解 Linux shell中“2>&1”?

推荐一款强大的在线编译器

Linux常用命令--系统状态篇

关注公众号【编程珠玑】,第一时间获取更多原创技术文章

C语言入坑指南-被遗忘的初始化的更多相关文章

  1. C语言入坑指南-缓冲区溢出

    前言 缓冲区溢出通常指的是向缓冲区写入了超过缓冲区所能保存的最大数据量的数据.如果说之前所提到的一些问题可能只是影响部分功能的实现,那么缓冲区溢出将可能会造成程序运行终止,被不安全代码攻击等严重问题, ...

  2. C语言入坑指南-数组之谜

    前言 在C语言中,数组和指针似乎总是"暧昧不清",有时候很容易把它们混淆.本文就来理一理数组和指针之间到底有哪些异同. 数组回顾 在分析之前,我们不妨回顾一下数组的知识.数组是可以 ...

  3. ElasticSearch入坑指南之概述及安装

    ---恢复内容开始--- ElasticSearch入坑指南之概述及安装 了解ElasticSearch ElasticSearch(简称ES)基于Lucene的分布式全文检索引擎.使用ES可以实现近 ...

  4. Rust入坑指南:核心概念

    如果说前面的坑我们一直在用小铲子挖的话,那么今天的坑就是用挖掘机挖的. 今天要介绍的是Rust的一个核心概念:Ownership.全文将分为什么是Ownership以及Ownership的传递类型两部 ...

  5. Rust入坑指南:鳞次栉比

    很久没有挖Rust的坑啦,今天来挖一些排列整齐的坑.没错,就是要介绍一些集合类型的数据类型."鳞次栉比"这个标题是不是显得很有文化? 在Rust入坑指南:常规套路一文中我们已经介绍 ...

  6. Rust入坑指南:齐头并进(上)

    我们知道,如今CPU的计算能力已经非常强大,其速度比内存要高出许多个数量级.为了充分利用CPU资源,多数编程语言都提供了并发编程的能力,Rust也不例外. 聊到并发,就离不开多进程和多线程这两个概念. ...

  7. electron入坑指南

    electron入坑指南 简介 electron 实际集成chrome浏览器和node环境, 运行你写的网页 app 基本目录结构 index.html 名称可以不是index, 这个文件与普通网页的 ...

  8. Elasticsearch入坑指南之RESTful API

    Elasticsearch入坑指南之RESTful API Tags:Elasticsearch ES为开发者提供了非常丰富的基于Http协议的Rest API,通过简单的Rest请求,就可以实现非常 ...

  9. eclipse中导入外部包却无法查看对应源码或Javadoc的入坑指南

    eclipse中导入外部包却无法查看对应源码或Javadoc的 入坑指南 出现这个错误的原因是,你虽然导入了.jar包,但没有配置对应的Javadoc或源码路径,所以在编辑器中无法查看源 码和对应AP ...

随机推荐

  1. 响应者链UIResponder-扩大UIButton的点击范围

    在开发中,我们经常看到有按钮等的点击,会出现响应事件.按钮->view->ViewController->UIWindow->UIApplication,这就形成了一个响应链. ...

  2. C#.NET和C++结构体Socket通信与数据转换

    最近在用C#做一个项目的时候,Socket发送消息的时候遇到了服务端需要接收C++结构体的二进制数据流,这个时候就需要用C#仿照C++的结 构体做出一个结构来,然后将其转换成二进制流进行发送,之后将响 ...

  3. [javaEE] web应用的目录结构&配置虚拟主机

    myWebSite | |-- 静态资源和JSP文件都可以直接放在web应用目录下,浏览器可以直接访问 |-- WEB-INF 浏览器没有办法直接访问 |-- classes 动态web运行时的cla ...

  4. git获取远程服务器的指定分支

    昨天糗大了...进入新公司,公司服务器上有Online为线上版本,开发版本默认的为Master,本地clone的开发版为master,公司用的git 自动部署(puh后服务器自动更新了代码...这个有 ...

  5. Java中枚举的使用

    Java中枚举其实就是静态常量,今天发现枚举里面其实还能加方法,学习了下, 代码如下: package org.pine.test; import java.util.HashMap; import ...

  6. div和span标签

    ------------------------ ------------------------ ------------------------ 块级标签独占一行,不和别的标签在同一行显示:行内标 ...

  7. 如何为你的 Vue 项目添加配置 Stylelint

    如何为你的 Vue 项目添加配置 Stylelint 现在已经是 9102 年了,网上许多教程和分享帖都已经过期,照着他们的步骤来会踩一些坑,如 stylelint-processor-html 已经 ...

  8. Flask路由与蓝图Blueprint

    需求分析: 当一个庞大的系统中有很多小模块,在分配路由的时候怎么处理呢?全部都堆到一个py程序中,调用@app.route? 显然这是很不明智的,因为当有几十个模块需要写路由的时候,这样程序员写着写着 ...

  9. .net 前端gb2312编码,后台获取参数乱码(因为表单提交的时候是utf-8编码 则在后台读取参数时会出现乱码)

    在表单中设置编码 ' accept-charset="utf-8"  '即可: <form id="login_submit" action=" ...

  10. Javascript数组系列四之数组的转换与排序Sort方法

    今天我们继续来介绍 Javascirpt 数组中的方法,也是数组系列的第四篇文章,因为数组的方法众多,每篇文章我们都对数组的每个方法都有比较细致的描述,只要你能够从中成长一点点,那我们的目的就达到了, ...