您的位置:首页 > 其它

初始化那点小事

2015-08-29 18:32 471 查看
代码编译运行环境: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.未初始化带来的问题

使用未初始化的指针会带来的潜在风险。如果一个指针既不为空,也没有被设置为指向一个已知的对象,则这样的指针称为悬挂指针(dangling 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并没有显示定义参数为空的构造函数,而编译器也并非在未显示定义任何构造函数时一定为类合成默认的构造函数,即使合成了默认的构造函数,对成员变量初始化的值也是随机的,没有意义的。所以,在很多编译器(如GJGPP)下,以上程序如法通过编译,但在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;
    }
}


VS2012下编译时报error C2360: “j”的初始化操作由“case”标签跳过。由于C++没有强制switch语句的各case分支使用break,所以在一个case分支中定义的变量是可能被其他分支的语句使用的。由于case分支被执行的随机性,无法保证变量获得初值。解决办法:

(1)除非只有一个case分支,否则不要在case分支中定义局部变量;

(2)可以将case分支至于代码块中,用大括号包围,限制case分支定义的变量的作用域在代码块作用域中。

修改为:
case 1:{int j=5;break;}


参考文献

[1] C++高级进阶教程.陈刚.武汉大学出版社

[2] /article/2594247.html

[3]http://zhidao.baidu.com/link?url=YJW2PS2ldRJVoqI41Y7juaLIIeXB4oH4653DaDnz_v84nWUQFh_PlQZItT4GJh7hFgmJbqJAzUeELeeKAA5Una
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: