初始化与赋值
初始化与赋值作者:lyb661 时间:190128
有人可能觉得,初始化和赋值这个话题三言两语就能说完。但是我在读书的过程中,接触到很多相关的问题,越来越认识到初始化与赋值在C++中的重要性,对这个问题也越来越感兴趣。所以早就想写点相关的东西,但一动笔,就觉得这个话题涉及面之广。举凡基本类型声明定义,数组、指针和引用,函数参数传递,类的复制控制等等都与初始化与赋值有关。下面是早就列出提提纲的一部分。
在讨论这个主题之前,先做一个试验:假定有一个简单的类 class student 与一个局部整型变量 n,测试一下未初始化的输出结果。我用的codeblocks 16.1版,输出结果令人大吃一惊!
///未初始化变量的输出测验
#include <iostream>
#include<string>
using namespace std;
class student
{
int num;
string name;
double score;
public:
void print_data()const;
//other member functions
};
void student::print_data()const
{
cout<<num<<" "<<name<<" "<<score<<endl;
}
//int a;
//static double d;
//extern int b;
int main()
{
int n;
cout<<n<<endl; //output1-
student st1;
st1.print_data(); //output2-
cout<<endl;
return 0;
}
/*
Unexpected results:
output1(n):1985614178
output2(num,name,score):2293512 -3.41519e-134
*/
n的值竟达到19多亿,student的学号与分数也出奇地大。反正不是我们能够想象的数字。如果这个整型数字出现在我们的银行卡上面,那该有多么令人惊喜!可注定如学生的姓名一样一场空!(学生姓名使用了标准库的string类型,由于它有定义良好的默认构造函数,所以它初始化为"")。
假定我们声明了一个变量,当时忘了初始化,而在以后也没记得给它一个有意义的值,然后竟直接使用了,那么这个小小的变量带来的问题将影响到整个程序,如果因此给你的工程造成不可预测的灾难,那岂不是太冤枉啦!所以记得一个变量要做到:在真正使用它的地方和时候让它恰如其分地出现,并且给它一个良好的初始值,比如
int n=0;
char ch=0;
101.初始化
C++变量初始化规则。
对于内置类型的变量,例如,整型(如int,bool,char),浮点型(如float,double),复合类型(指针型如int*,引用型如char&)等,定义在所有函数(包括main函数)之外的全局变量与静态变量(static)编译器会给一个默认的初始值0,枚举类型(enum)的第一个枚举变量也会初始化为0。至于局部变量的初始化则是未定义的,即编译器不会自动对它们进行初始化。
自定义数据如类类型的变量如果有良好的默认构造函数,那么将依构造函数来完成初始化。否则其数据成员如果是内置类型,则依据局部变量的初始化规则来初始化;如果是类类型的,有默认构造函数的依据构造函数来初始化。例如上例中的student之成员name初始化为空串,我们能想象得到,string的构造函数应该这样:
string::string(const char* cstring="");
stroustrup 在他的《C++程序设计语言》一书中曾提到,一个变量默认值应该是在所有可能值之外的。他并举例 class date 即日期类取默认值:0年0月0天。而人类文明史中日期确实没有0年,也没有0月和0日。
那么我们怎样初始化一个变量,给它一个默认值呢?就是0嘛!
内置类型如int,double,char,甚至指针型如int*,char*,都可以初始化为0。而指针初始化为0是有意义的。在声明之初让它不指向任何对象,赋值为0确实是一个很好的选择!引用类型如int&,char&等在使用之初必须初始化,否则会引起错误。如,
int n=0;
int& t=n;
初始化内置类型变量的方法有,直接初始化与复制初始化。例如,
int n=0; //复制初始化
int n(0); //直接初始化
在内置类型的变量中,这两种方法是没有区别,但是在自定义类型如类类型中复制初始化与直接初始化有时则有不一样的意义,这在后面再讨论。
102.赋值
现在有许多参考书中提到初始化与赋值区别的问题。
《C++ Primer》第四版中曾说到初始化不是赋值,并说初始化指创建变量并给它赋初始值,而赋值则是擦除对象的当前值并用新值代替。这种以时间先后来区分初始化与赋值的方式是没有意义的。其实初始化与赋值可以从形式上来区分:我们知道,C++是强类型语言,每个变量都是有类型的。当声明一个变量并给它一个初始值时,需要指出而且必须指出类型,就是要带有类型声明符;而赋值时则不需要而且也不许再带有类型声明符,否则就是一个重复定义的错误。例如,
int n=0; //这是初始化
n=1024; //这是赋值
int n=1024; //错误:重复定义
103.声明与定义
我们也可以用这个办法来区分声明与定义的概念。
C语言中绝大多数的声明同时也是定义,这从形式上是不好分别的,如
int n;
char ch='a';
尽管前一个变量没有初始值,但它也是一个定义。正如上文中提到的那个整型变量编译器已经给它一个值,虽然不是我们希望的值。一个变量的定义就是它在内存申请了存储。不是定义的声明在C++中很少见的。以下三种情形都不是定义的声明:如,
extern int m;
void swap(double x,double y);
class student{//……};
第一种情形是一个内置类型变量的声明,它表明 m 需要在某个地方加以定义。因为C++编程有时需要一个变量在多个文件中使用,所以它常在一个文件中声明,而在另一个文件中加以定义。这样可以避免重复定义的错误。
第二种情形是函数的声明,它是一个函数的原型。带有一个返回类型,一个函数名,和一个圆括号内的参数表(其中的形参的名称是可选的,也即形参可以省略,但必须指出形参的类型,并且每个形参的类型都必须指明)。
第三种是一个类的声明。类声明以大括号结束,并带有分号。这个规定使类声明可以在一个程序中多次出现。这算是C++中约定俗成的设置。
至于静态全局变量,如,
static int a;
尽管a需要在某个地方赋值,但它其实也是一个定义。因为编译器给了它一个默认的0值。
C++有一个规定:一个变量可以有多次声明,但有且只有一次定义。如果你拿不准一个变量是声明还是定义的话,就重复它一次,编译器就会告诉你它是一个声明,还是一个定义。
104.类类型的初始化
类类型通常使用默认的构造函数来完成初始化。
C++被称为带类的C,可是刚接触类的人常常为怎样构建类感到困惑,他们不知道如何设置数据成员与成员函数。因此大多数人会建立许多辅助函数来帮助工作。即如上文中的student类,可能需要set_data(),get_data(),print_data()之类的辅助函数,来读、写和输出。但这样做既不方便,也不美观。其实C++对类的构建有许多默认的函数如构造函数,复制构造函数,析构函数,赋值操作符等等。但是由编译器合成的默认构造函数的工作并不能令人满意。如,我们要建立一个student对象,如,
student st1;
为此编译器提供默认构造函数还能胜任。如,
student::student(){} //方案1
但是我们对这个学生对象的学号大小,姓甚名谁,学分多少却一无所知。所以要更好的设计学生类,我们需要自
己动手来建立构造函数。如,
student(int nu,string na,double sc); //方案2
或者这样,
student(int nu=0,string na="noname",double sc=0.0); //方案3
但无论采取哪种方式,自定义的构造函数都会覆盖编译器合成的默认构造函数,即我们不能再建立一个无默认值的学生对象,如
student st1; //这将引起一个错误
所以既能建立一个空的学生对象,又能设置学生对象的数据成员,就必须方案1加上方案2、方案3中的任一个一起来工作。
student st1; //可以
student st2(122600,"LinMingyuan",87.5); //也行
现在我们有另一种选择,就是构造函数初始化列表。即,
student(int nu=0,const string& na="none",double sc=0.0):name(na),number(nu),scores(sc){} //方案4
现在只需要这一个就能完成以上所需要的的所有工作了。
一个定义良好的类需要有一个默认的构造函数,构造函数初始化列表是一个不错的选择。当然,一个类的设计可
能需要多个构造函数,这另当别论。
补充:如果按照一般的写法,构造函数初始化列表应该如下:
student():num(0),name("none"),score(0.0){}
但是这个无参的构造函数不支持用字面值来初始化一个对象,如,
student st2(122600,"LinMingyuan",87.5);
所以对它加以改造。
(未完待续)