认识初始化

代码编译运行环境:VS2012+Win32+Debug


初始化是编码过程中的重要操作,往往由于被忽略,导致使用未初始化的变量(或内存区域),将程序置于不确定的状态,产生各种bug,严重影响的程序的健壮性。正确地理解和使用初始化操作,是对每一位合格程序员的基本要求。

1.什么是初始化

在给初始化下定义前。先弄清楚两个概念:申明与定义。编程过程中申明与定义包括变量、函数和类型的申明和定义。具体含义参见我的另一篇blog:申明与定义的区别

变量的申明:指明变量所属类型与变量名称的过程。如:extern int a;

变量的定义:指明变量所属类型、变量名称、分配空间以及完成初始化操作的过程。如:int a=1;或者int a(1);

变量的初始化:为数据对象或变量赋初值的做法。可以看出,初始化是变量定义的一部分。定义一个变量时,一定会包括变量的初始化操作。

观察以上概念的定义,可以清楚地看出变量的申明、定义和初始化的区别与联系,请牢记在心,切不可混淆。

2.初始化与赋值的区别

初始化与赋值是不同的操作。初始化是使变量(对象)第一次具备初值的过程。而赋值则是改变一个已经存在的变量(对象)的值的过程。

对于基本数据类型的变量来说,变量的初始化与赋值的实现方式上差不多,如:

    int i=5;     //初始化
    int i; i=5;  //赋值

都是利用赋值符号将特定的值写入变量i中。但对于构造数据类型的对象,初始化和赋值的操作在实现方式上有很大的区别。以类的对象的举例如下:

#include <iostream>
using namespace std;

class String
{
private:
    char* s;
    unsigned int len;
    unsigned int capacity;

public:
    String(char* str)
    {
        len=strlen(str);
        capacity=len+1;
        s=new char[capacity];
        strcpy(s,str);
    }

    String& operator=(char* str)
    {
        if(strlen(str)+1>capacity)
        {
            delete[] s;
            capacity=strlen(str)+1;
            s=new char[capacity];
        }
        strcpy(s,str);
        len=strlen(str);
        return *this;
    }
    void show()
    {
        cout<<s<<endl;
    }
};

int main(int argc,char* argv[])
{
    String name("John");
    name.show();
    name="Johnson";
    name.show();

    getchar();
}

这个程序实现了非标准的String类。该对象实现的功能有C风格的字符串初始化、C风格的字符串的赋值和输出的功能。

对于对象来说,初始化语句的语法形式与赋值不同。赋值只能通过赋值操作符“=”进行,对象的初始化必一般采用在圆括号中给出初始化参数的形式来完成。

赋值操作是使用默认的按位复制的方式或者是由重载operator=操作符来完成,而对象的初始化必须由构造函数来完成。

在以上String类的设计中,构造函数只需要根据传入的参数字符串的长度来分配空间就可以了,而赋值操作符重载函数则需要考虑传入的参数字符串的长度,然后决定是否要释放原来空间并申请新的空间。可见,构造函数和赋值操作的逻辑也是有很大的差别。

C++中,基本类型的变量也可以当做对象来处理,因此基本类型的变量可以采用类似默认构造函数的形式进行初始化。例如int i(2);和double d(2.5);等。

3.未初始化带来的问题

C/C++规定了变量的定义一定要完成初始化操作,通常情况下,并没有规定初始化操作必须由程序员来完成,如果编码者在定义变量时未赋予有意义的初始值,那么变量的初始化则由编译器来完成,变量的初始值将处于不确定状态。使用初始值不确定的变量,会带来巨大的风险,例如使用未初始化的指针变量往往会导致程序崩溃。如果一个指针既不为空,也没有被设置为指向一个已知的对象,则这样的指针称为悬挂指针(Dangling Pointer),有时也称为野指针(Wild Pointer),即“无法正常使用”之意。如果使用,则给程序的运行带来不稳定性和不可预知的错误。

#include <iostream>   
using namespace std;   
void f(int *p);  
int main()   
{   
    //int a = 10;   
    int *i;  
    //i = &a;   
    f(i);  
    cout<<*i;  
    return 0;  
}   
void f(int *p)
{  
    cout<<p;  
    if(p!=0)  
        *p = 100;  
} 

当控制函数执行到f()中时候,f()不能判断指针的合法性,将会产生很严重的错误,但编译可以通过。最好的解决方法是使用指前,将其指向一个对象,即去掉注释部分。

4.编译时与初始化相关的错误

在某些时候,初始化强制由编码者来完成,没有初始化会导致编译错误。如: (1)定义常变量,必须同时完成初始化; (2)由于引用本质是指针常量,所以定义引用时也必须同时初始化; (3)定义构造类型的常对象时,相应的构造函数必须存在。考察如下程序:

class A
{
    int num;
public:
    void show()const
    {
        cout<<num<<endl;
    }
};

int main(int argc,char* argv[])
{
    const A a;
    a.show();
}

此程序定义了一个常对象a,然后调用其常函数show()。但是类A并没有显示定义参数为空的构造函数,而编译器也并非在未显示定义任何构造函数时一定为类合成默认的构造函数,即使合成了默认的构造函数,对成员变量初始化的值也是随机的,没有意义的。所以,在很多编译器(如GCC)下,以上程序如法通过编译,但在VC++中,程序能够通过编译,但运行结果没有任何意义。所以,如果要生成常对象,必须显示定义其对应的构造函数,完成对象的初始化工作。

还有一种情况,由于程序的控制结构可能导致某些变量无法初始化,也将引起编译错误。最常见的就是goto语句与switch语句。见如下程序:

int main(int argc,char* argv[])
{
    int i;
    cin>>i;
    if(i==8)
        goto disp;
    int j=9;
disp:
    cout<<i+j<<endl;
    getchar();
}

这个程序在很多编译器下无法通过编译,即使通过编译,运行时也会出现问题。原因是goto语句会跳过变量j的初始化语句,即使j被分配空间(很多编译器集中分配临时变量的空间),也无法获得初值。

再看另外一例子:

int main(int argc,char* argv[])
{

    int i;
    cin>>i;
    switch(i)
    {
        case 1:int j=5;break;
        case 2:cout<<"Hello"<<endl;
    }
}

GNU C++和VC++下编译时都会报类似于“j的初始化操作由case标签跳过”的错误。由于C++没有强制switch语句的各case分支使用break,所以在一个case分支中定义的变量是可能被其他分支的语句使用的。由于case分支被执行的随机性,无法保证变量获得初值。解决办法: (1)除非只有一个case分支,否则不要在case分支中定义局部变量; (2)可以将case分支至于代码块中,用大括号包围,限制case分支定义的变量的作用域在代码块作用域中。 修改为:

case 1:
{ 
    int j=5;
    break;
}

参考文献

[1] C++高级进阶教程.陈刚.武汉大学出版社 [2] C++中的作用域与生命周期 [3]悬挂指针.百度百科

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏blackheart的专栏

[C#6] 3-null 条件运算符

0. 目录 C#6 新增特性目录 1. 老版本的代码 1 namespace csharp6 2 { 3 internal class Perso...

229100
来自专栏Modeng的专栏

Javascript数组系列五之增删改和强大的 splice()

今天是我们介绍数组系列文章的第五篇,也是我们数组系列的最后一篇文章,只是数据系列的结束,所以大家不用担心,我们会持续的更新干货文章。

14020
来自专栏怀英的自我修炼

Java漫谈9

上次聊String的时候聊到了String为什么可以在不new的情况下创建,说实话,这个问题我也没有答案,直到看到了这篇帖子,才敢说知道了为什么。 《Java ...

36990
来自专栏Python爬虫实战

Python数据类型之字典(上)

之前系列文章介绍了Python简单数据类型和序列数据类型,本文来学习一种新的映射数据类型:字典。

9310
来自专栏编程

机器学习之Python基础(一)

标题 Python语言特点 基本数据类型 循环 文件IO 函数 1 1 1 Python是一种面向对象的解释型计算机程序设计语言。它有着代码简洁、可读性强的特点...

20680
来自专栏锦小年的博客

python学习笔记4.2-python高级之迭代器

迭代是Python中最强有力的特性之一,同时对编程人员来说,也是最难理解的一种用法。其实从高层次来看,迭代就是一种处理序列中元素的方式。通过自定义迭代对象可以...

227100
来自专栏互联网技术栈

JVM解读-方法区

方法区,Method Area, 对于习惯在HotSpot虚拟机上开发和部署程序的开发者来说,很多人愿意把方法区称为“永久代”(Permanent Genera...

42240
来自专栏JetpropelledSnake

Python入门之迭代器/生成器/yield的表达方式/面向过程编程

 本章内容     迭代器     面向过程编程       一、什么是迭代       二、什么是迭代器       三、迭代器演示和举例       四、生...

30890
来自专栏nummy

python operator模块学习

operator模块是python中内置的操作符函数接口,它定义了一些算术和比较内置操作的函数。operator模块是用c实现的,所以执行速度比python代码...

8720
来自专栏WD学习记录

数据结构与算法2016-06-03

一个算法调用自己来完成它的部分工作,在解决某些问题时,一个算法需要调用自身。如果一个算法直接调用自己或间接调用自己,就称这个算法是递归的。根据调用方式的不同,它...

8520

扫码关注云+社区

领取腾讯云代金券