欢迎来到博主的专栏——c++编程
博主ID:代码小豪
初始化
怎样才算一个变量被创建了呢?当一个变量被声明时,编译器会给这个变量生成一个内存空间,当一个变量得到其相应的空间时,这个变量就算是被创建了。
但是这个空间内的数据是混乱的,因为我们还没有为这个空间赋予一个有意义的值,若是一个变量的内存空间的数据没有意义,那么使用它会带来错误。
long i;
cout << i << endl;//使用了未初始化的变量
char* str=(char*)malloc(sizeof(20));
cout << str << endl;//使用了未初始化的空间
解决方法很简单,当变量创建之后进行赋值就能解决未初始化的问题了。
int i;
i = 0;
char* str=(char*)malloc(sizeof(20));
memset(str, 0, 20);//为str内的空间赋值
但这并不能称作初始化,因为变量在创建和赋值之间存在一个空档期,在这个期间内数据仍然是混乱的,只是我们创建完变量就为其赋值,导致这个空档期特别短,所以不会影响对这个变量的使用。
真正的初始化是在变量创建的同时,为这个内存空间填充一个初值,这个期间是不存在变量未初始化的空档期的。
int i=0;//初始化
char* str=(char*)calloc(sizeof(char), sizeof(20));//初始化
先声明变量再赋值,和初始化变量的区别在内置类型中,体现不出来,但是在对象当中是有一定区别的。
我们目前已知的初始化对象的方法是通过构造函数实现初始化,比如在下列类中。
class a
{
public:
a()//构造函数
{
_a = 0;
}
private:
int _a;
};
我们创建一个a类的对象,在对象创建时,会调用构造函数将成员变量完成初始化。
a a1;
但是实际上成员变量的创建时间并不是与构造函数调用时是同步的,当我们创建对象时,首先对象会优先创建成员,接着在调用构造函数,然后根据构造函数的定义完成赋值,这就和变量先声明再赋值的这个过程是类似的,并不是像初始化一样直接将初值填充至内存中,中间存在一个空档期。
但是对象在实例化出来之后会立即调用构造函数,因此这个创建到赋值之间的空档期也是非常短,因此可以将构造函数看为是对对象的初始化。
但是在这种场景中就能看出构造函数并非就是初始化。我们将a类的对象设置一个const类型的成员变量。
class a
{
public:
a()
{
_a = 0;
}
private:
const int _a;
};
因为成员变量_a是一个const类型的变量,那么当这个const类型的变量初始化后是不能通过赋值进行修改的。否则编译器会报错。
但是这个const类型的变量又不能是未定义的数据,我们要为其附上初值,才能正常使用这个成员变量。但是构造函数并不能起到初始化这个变量的作用,此时我们就要用上初始化列表了
初始化列表
初始化列表用在构造函数的参数之后,用冒号(:)开始,用(,)分割变量。每个待初始化的参数需要()设置初始化的值,接着才是构造函数的函数体({}).
以类a为例
class a
{
public:
a()
:_a1(2)
,_a2(0)
{
_a2 = 4;
}
private:
const int _a1;
int _a2;
};
此时_a1被初始化成2,_a2初始化成0,接着在构造函数中将_a2赋值为4.
初始化列表的规则
变量只能初始化一次,因此初始化列表中出现的成员变量也只限一次。
class a
{
public:
a()
:_a1(2)
,_a2(0)
,_a2(4)//error
{
}
private:
const int _a1;
int _a2;
};
初始化列表中大部分类型的变量都是可以放在构造函数当中进行初始化的,只有以下三种场景的成员变量才必须使用初始化列表。
(1)const修饰的成员变量
(2)引用类型的成员变量
(3)没有默认构造函数的自定义类型变量
这都是因为这些变量需要完成初始化才能使用,但是构造函数并不能提供初始化变量的功能,因此需要出现在初始化列表当中实现初始化。
class A
{
public:
A(int a)
:_a(a)
{}
private:
int _a;
};
class B
{
public:
B(int a, int ref)
:_aobj(a)
, _ref(ref)
, _n(10)
{}
private:
A _aobj; // 没有默认构造函数
int& _ref; // 引用
const int _n; // const
};
基类成员的初始化
在上述规则当中,const类型和引用类型的初始化方式比较容易理解,但是基类(自定义类型)的初始化方式则比较复杂。我们用stack类作为基类为例。
typedef int STDateType;
class stack
{
stack(int n)
{
_stack = (STDateType*)malloc(sizeof(STDateType) * n);
_top = 0;
_capacity = n;
}
private:
STDateType* _stack;
int _top;
int _capacity;
};
class _stack//stack的派生类
{
public:
_stack()
{
_size = 0;
}
private:
stack _S;//基类成员_s
int _size;
};
我们并未在派生类_stack上显示使用初始化列表,这是不是说明这个类没有初始化列表呢?并不是,我们先来思考这个问题:当我们实例化对象时,编译器是如何为成员进行初始化的呢?
当我们定义了初始化列表时,编译器会按照初始化列表为我们初始化对象,但是当我们没有定义初始化列表时,编译器该按照什么进行初始化的呢?
我们要首先明白一件事, 实例化的对象的初始化一定要按照初始化列表才能完成初始化的,但是在前面的文章当中的例子都没有使用初始化列表,也能完成初始化。原因如下
当我们没有定义初始化列表时,编译器会生成一个隐式调用的初始化列表。这个初始化列表会为每个成员进行初始化。根据不同类型的成员,编译器会默认执行以下操作
(1)内置类型的成员(除const和引用)。编译器会用一个默认的值为其实现初始化。
(2)基类的成员对象,编译器会调用这个类的默认构造函数为其实现初始化。
因此我们可以反证上面的结论。由于const、引用、没有默认构造函数的基类成员,编译器生成的初始化列表不能完成这些类型的初始化,因此我们得出结论,const、引用、没有构造函数的基类成员,都需要用用户定义的初始化列表完成初始化。
再将目光回到_stack类中。
由于基类stack没有默认构造函数(只有一个带参的构造函数),因此,想要在派生类_stack中为类进行初始化,就需要自定义一个初始化列表。
在内置类型中,初始化列表的参数代表的是将这个参数作为这个变量的初值,
自定义类型的参数则代表初始化这个对象使用的构造函数的参数。
比如:
typedef int STDateType;
class stack
{
stack(int n)
{
_stack = (STDateType*)malloc(sizeof(STDateType) * n);
_top = 0;
_capacity = n;
}
private:
STDateType* _stack;
int _top;
int _capacity;
};
class _stack//stack的派生类
{
public:
_stack()
:_S(2)
//初始化_S时,调用stack类中的构造函数,并传递参数(2)
{
_size = 0;
}
private:
stack _S;//基类成员_s
int _size;
};
如此就能完成基类对象的初始化了。
对象的初始化
以类a为例,对象的初始化方法可以分为以下几种
class a
{
a(int n = 3)//默认构造函数
{
_a1 = 3;
}
a(const a& A)//拷贝构造函数
{
_a1 = A._a1;
}
a& operator=(const a& A)//赋值重载函数
{
_a1 = A._a1;
return *this;
}
private:
int _a1;
};
对象的初始化方法可以参考内置类型的变量初始化。
int a;
int a;
a = 1;
int a = 1;
方法1,调用默认构造函数初始化。
a a1;
这种方法在对象实例化时会调用默认构造函数。完成成员变量的初始化。
方法2,使用拷贝构造函数实现初始化。
a a1;//a1已经被创建
a a2 = a1;//a2调用拷贝构造函数,拷贝a1
方法3,创建对象后调用幅值重载函数实现初始化。
a a1;//a1已经被创建
//……
a a2;//创建a2
a2 = a1;//调用赋值重载函数完成
方法4,使用构造函数完成初始化。
int main()
{
a a1 = 6;
}
前面三种方法在前面的文章都有提到,这次主要重点讲解第4中方法、
这种方法看起来是非常奇怪的,为什么一个int类型的常量可以给a类的对象完成初始化呢?
根据前面所讲的知识,我们可以分解成三个步骤。
(1)int类型的6被转换成a类型的对象
(2)转换类型的赋值,会产生一个临时变量,这个临时变量是a类型的对象,其成员会调用构造函数,而且参数为6,相当于这样初始化成员。
a tmp(6);
(3)a1将会调用拷贝构造函数,拷贝这个临时对象。
这种理解方式是对的,也能对前面讲解的语法知识完成闭环,但实际上并非如此。
由于初始化规定如下:
初始化就是对象创建的同时使用初值直接填充对象的内存单元,因此不会有数据类型的转换等中间郭晨个,也就不会产生临时变量。
而赋值是在对象创建之后任何时候都能调用的函数,赋值重载函数的参数是const a&,因此需要发生类型转换
根据初始化的规定可知,上述情况是不会产生临时对象的,只有方法(3)的情景下才会发生产生临时对象的情况。如:
a a1;
a1 = 6;
那么编译器采取了这种很取巧的方式进行初始化。我们先来想想这个6转换类型后生成的tmp是什么样的?是不是这样啊。
a tmp(6);
那么与其让a1拷贝tmp,是不是也能用这种方式生成一模一样的对象。如下:
a a1(6);
所以编译器对于这种方式的初始化并没有让6进行类型转换,而是直接将6作为参数调用对象的构造函数。那么我们可以得出一下结论。
a a1(6);
a a1 = 6;
这两种初始化方式是没有区别的。
关于这部分的内容有点难懂,主要是语法的规则导致对象的初始化有这么多弯弯绕绕,讲这部分的目的是因为在有些代码会使用这种初始化方法,与其在后面的章节弯弯绕绕的讲,不如在这篇文章中一起讲述,如果不能很好理解,希望大伙记住这个结论:
a a1(6);
a a1 = 6;