认识初始化

代码编译运行环境: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 条评论
登录 后参与评论

相关文章

  • 网易游戏技术岗在线编程题(一)

    小易经常沉迷于网络游戏.有一次,他在玩一个打怪升级的游戏,他的角色的初始能力值为 a.在接下来的一段时间内,他将会依次遇见n个怪物,每个怪物的防御力为b1,b2...

    Dabelv
  • Linux 命令(76)—— kill 命令

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 ...

    Dabelv
  • C++11就地初始化与列表初始化

    在C++11中,结构体或类的数据成员在申明时可以直接赋予一个默认值,初始化的方式有两种,一是使用等号“=”,二是使用大括号列表初始化的方式。注意,使用参考如下代...

    Dabelv
  • C++学习总结1——几个基本概念

    最近我在做毕设。写程序的时候,总是被C++里面的指针搞得头昏脑胀。刚开始的时候还有些浮躁,不想静下心来仔细看看指针使用的细节。过了几天发现只在Visual St...

    王云峰
  • AAAI 2018 | 港中文-商汤联合论文:自监督语义分割的混合与匹配调节

    机器之心
  • 编程小白 | 每日一练(88)

    这道理放在编程上也一并受用。在编程方面有着天赋异禀的人毕竟是少数,我们大多数人想要从编程小白进阶到高手,需要经历的是日积月累的学习,那么如何学习呢?当然是每天都...

    小林C语言
  • 以图搜图:基于机器学习的反向图像检索

    原标题 | Reverse Image Search with Machine Learning

    AI研习社
  • GAN和PS合体会怎样?东京大学图像增强新研究:无需配对图像,增强效果还可解释

    过去,若是有大量的原始图像和增强图像,那么我们就可以用类似CNN的方法进行训练,来让图像变得更美。

    量子位
  • 用 CNN 分 100,000 类图像

    [Title]:Dual-Path Convolutional Image-Text Embedding

    AI研习社
  • 对抗样本的反思:仅仅设置更小的扰动阈值 ε,或许并不够

    对抗样本是各种机器学习系统需要克服的一大障碍。它们的存在表明模型倾向于依赖不可靠的特征来最大限度的提高性能,如果受到干扰,可能会导致错误分类,带来潜在的灾难性后...

    AI科技评论

扫码关注云+社区

领取腾讯云代金券