代码编译运行环境:VS2012+Win32+Debug
初始化是编码过程中的重要操作,往往由于被忽略,导致使用未初始化的变量(或内存区域),将程序置于不确定的状态,产生各种bug,严重影响的程序的健壮性。正确地理解和使用初始化操作,是对每一位合格程序员的基本要求。
在给初始化下定义前。先弄清楚两个概念:申明与定义。编程过程中申明与定义包括变量、函数和类型的申明和定义。具体含义参见我的另一篇blog:申明与定义的区别。
变量的申明:指明变量所属类型与变量名称的过程。如:extern int a;
变量的定义:指明变量所属类型、变量名称、分配空间以及完成初始化操作的过程。如:int a=1;或者int a(1);
变量的初始化:为数据对象或变量赋初值的做法。可以看出,初始化是变量定义的一部分。定义一个变量时,一定会包括变量的初始化操作。
观察以上概念的定义,可以清楚地看出变量的申明、定义和初始化的区别与联系,请牢记在心,切不可混淆。
初始化与赋值是不同的操作。初始化是使变量(对象)第一次具备初值的过程。而赋值则是改变一个已经存在的变量(对象)的值的过程。
对于基本数据类型的变量来说,变量的初始化与赋值的实现方式上差不多,如:
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);等。
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()不能判断指针的合法性,将会产生很严重的错误,但编译可以通过。最好的解决方法是使用指前,将其指向一个对象,即去掉注释部分。
在某些时候,初始化强制由编码者来完成,没有初始化会导致编译错误。如: (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]悬挂指针.百度百科