🔥个人主页:Quitecoder
🔥专栏:c++笔记仓
朋友们大家好,本篇文章我们来到初始化列表,隐式类型转换以及explicit的内容
1.初始化列表
1.1构造函数体赋值
在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值
1.2初始化列表
class Date {
public:
Date(int year,int month,int day)
:_year(year)
,_month(month)
,_day(day)
{}
private:
int _year;
int _month;
int _day;
};
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个成员变量后面跟一个放在括号中的初始值或表达式
那么,为什么要使用初始化列表呢?它的优势在哪里呢?
我们来看构造函数对于下面类的初始化:
class Date2 {
public:
Date2(int year, int month, int day)
{
_n=10;
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
const int _n;
};
我们发现const成员变量并不能用函数体进行初始化
int _year;
int _month;
int _day;
这三个成员既可以在函数体,又可以在初始化列表,但是类中包含以下成员,必须放在初始化列表位置进行初始化:
- 引用成员变量
- const成员变量
- 自定义类型成员(且该类没有默认构造函数时)
int _year;
int _month;
int _day;
const int _n;
我们知道,这个只是一个声明,定义是对象实例化时候完成的,有些成员,必须在定义的时候进行初始化
初始化列表中的每个元素都直接对应一个成员变量或基类,允许在构造函数体执行之前对这些成员或基类进行初始化。这对于const成员变量、引用类型成员变量以及某些没有默认构造函数的类型尤其重要
Date2(int year, int month, int day)
:_n(1)
{
_year = year;
_month = month;
_day = day;
}
初始化列表是每个成员变量定义初始化的位置
class Date2 {
public:
Date2(int year, int month, int day)
:_n(1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
const int _n;
};
没有在初始化列表中显式初始化
_year
、_month
、和_day
这三个成员变量,它们仍然会在初始化列表阶段被默认初始化,然后在构造函数体内被赋新的值
对于基本类型(如
int
),如果它们未在类的初始化列表中显式初始化,则它们会进行默认初始化。对于类内的基本类型成员变量,默认初始化意味着不进行初始化(保留未定义值),除非它们是静态存储持续时间的对象(例如全局或静态变量,它们会被初始化为零)。然而,对于自动存储持续时间(如函数内的局部变量)的对象,如果未显式初始化,则其值是未定义的。在类构造函数中,成员变量的行为类似于局部变量,如果不在初始化列表中显式初始化,它们将不会被自动初始化
_n
是通过初始化列表初始化的,因为它是const
类型的,必须在那里初始化。而_year
、_month
、和_day
虽然没有在初始化列表中被显式赋值,但它们会在构造函数体开始执行前完成默认初始化(对于基本数据类型,这意味着它们的初始值是未定义的)。然后,在构造函数体内,它们被赋予新的值
因此,可以说成员变量_year
、_month
、和_day
先经历了默认初始化(在这个场景下,这意味着它们的值是未定义的),然后在构造函数体内被赋值
我们不妨提到前面讲的声明时给缺省值:
private:
int _year=1;
int _month;
int _day;
const int _n;
缺省值的本质就是给初始化列表用的
Date2(int year, int month, int day)
: _n(1), _year(year), _month(month), _day(day)
{
// 构造函数体可以留空,因为所有成员变量都已经在初始化列表中初始化
}
在这个版本中,所有成员变量都是通过初始化列表直接初始化的,这是推荐的做法,特别是对于复杂类型或类类型的成员变量
引用类型必须在定义的时候初始化,所以也得使用初始化列表
class A
{
public:
A(int a=0)
:_a(a)
{}
private:
int _a;
};
class Date2 {
public:
Date2(int year, int month, int day,int x)
:_n(1)
,_year(year)
,_month(month)
,_day(day)
,_ref(x)
{
}
private:
int _year=1;
int _month;
int _day;
const int _n;
int& _ref;
A aa;
};
这里aa也会走初始化列表,来调用它的默认构造函数
我们可以在初始化列表来直接控制自定义类型的初始化
Date2(int year, int month, int day,int x)
:_n(1)
,_year(year)
,_month(month)
,_day(day)
,_ref(x)
,aa(1)
{
}
初始化列表和构造函数共同定义了类对象的初始化行为。初始化列表提供了一种高效、直接初始化成员变量和基类的方式,而构造函数则完成剩余的初始化逻辑和设置,比如动态开辟一个数组进行赋值的时候,就用到函数体
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
我们来看下面的代码:
class A
{
public:
A(int a)
:_a1(a)
,_a2(_a1)
{}
void Print() {
cout<<_a1<<" "<<_a2<<endl;
}
private:
int _a2;
int _a1;
};
int main() {
A aa(1);
aa.Print();
}
这个结果是什么呢?
结果是1和一个随机值
在这个例子中,A
类有两个整型成员变量:_a1
和_a2
。在构造函数中,_a1
被初始化为传入的参数a
的值,而_a2
被初始化为_a1
的值。
然而,成员变量的初始化顺序是由它们在类中声明的顺序决定的,而不是它们在初始化列表中出现的顺序。在A
类中,_a2
在_a1
之前声明,因此_a2
会先于_a1
初始化。
这意味着当_a2(_a1)
执行时,_a1
还没有被初始化,所以_a2
的值是未定义的。然后,_a1
被初始化为1
因此,当调用aa.Print();
时,输出的第一个值(_a2
的值)是未定义的,而第二个值(_a1
的值)是1
。在实际执行时,未定义的值可能是内存中该位置的任何值,这取决于编译器和运行时环境。
要修正这个问题,应该按照成员变量在类中声明的顺序初始化它们,或者更改成员变量的声明顺序以反映期望的初始化顺序。例如:
class A {
public:
A(int a)
:_a1(a) // 现在_a1首先初始化
,_a2(_a1) // 然后是_a2
{}
void Print() {
cout << _a1 << " " << _a2 << endl;
}
private:
int _a1; // 声明顺序改为先_a1
int _a2; // 然后是_a2
};
在这个修改后的版本中,_a1
会先被初始化为1
,然后_a2
会被初始化为_a1
的值,即1
。所以Print
函数会输出1 1
。
1.2.1隐式类型转换与复制初始化
我们再来看下面的类:
class C
{
public:
C(int x)
:_x(x)
{}
private:
int _x;
};
int main()
{
C cc1(1);
C cc2 = 2;
return 0;
}
C cc2 = 2;
为什么cc2能直接赋值呢?
在C++中,如果一个类的构造函数只需要一个参数(或所有参数除了第一个外都有默认值),那么这个构造函数允许从构造函数参数类型到类类型的隐式转换。这种转换使得单个值可以被视为是该类的一个实例,即使没有显式地调用构造函数
C cc1(1);
- 这行代码直接调用了
C
类的构造函数,使用1
作为参数创建了cc1
对象。
C cc2 = 2;
- 这行代码演示了隐式类型转换。虽然看起来像是将整数
2
赋值给cc2
,实际上C++编译器解释为使用2
作为参数调用C
类的构造函数来初始化cc2
。这是因为C(int x)
构造函数允许从int
到C
的隐式转换。
复制初始化是C++中一种对象初始化的方式,它与直接初始化有所不同,但在某些情况下可以产生类似的效果。理解复制初始化对于深入理解C++的对象构造和赋值语义非常重要。接下来,我们将通过详细说明来解释复制初始化的概念,以及为什么在某些情况下可以通过直接赋值的方式来初始化对象
复制初始化的基本概念
复制初始化通常发生在使用=
操作符进行对象初始化的场景中。不同于直接初始化(直接调用构造函数),复制初始化涉及到源对象到目标对象的潜在类型转换和赋值操作
C obj = value;
在上述代码中,value
可以是与C
类型兼容的任何值或对象。复制初始化的过程如下:
-
类型转换(如果必要):如果
value
不是C
类型的对象,则编译器会尝试使用value
调用C
的构造函数(或explicit
关键字修饰的构造函数除外),以创建一个临时的C
类型对象。这一步是隐式类型转换的一部分。 -
调用拷贝构造函数:编译器接下来会使用这个临时对象(如果第一步创建了临时对象的话)作为参数调用
C
的拷贝(或移动)构造函数,来初始化obj
。如果源对象就是C
类型,并且没有发生类型转换,那么这一步将直接用源对象来初始化obj
。 -
优化:在很多情况下,编译器可以应用(拷贝消除)优化来避免真正创建临时对象和执行拷贝(或移动)操作,直接在
obj
的存储位置构造对象
为什么可以直接赋值?
class C
{
public:
C(int x)
:_x(x)
{}
private:
int _x;
};
C cc2 = 2;
这里的2
是一个整型字面量,不是C
类型的对象。复制初始化的过程大致如下:
- 类型转换:编译器使用
2
调用C
的构造函数创建一个临时的C
类型对象。 - 拷贝构造函数:这个临时对象然后用于初始化
cc2
。但实际上,由于优化,这一步可能被省略,2
直接用于在cc2
的位置构造C
对象。
我们不妨来看看它是否调用了拷贝构造:
class C
{
public:
C(int x)
:_x(x)
{}
C(const C& cc)
{
cout << "use copy" << endl;
}
private:
int _x;
};
这里就被编译器优化了,同一个表达式连续步骤的构造,一般会被合并为一个
因此,尽管代码看起来像是将
2
直接赋值给C
类型的对象cc2
,实际上则是通过编译器优化,直接在cc2
的存储位置用2
构造了一个C
对象。
来看下面的代码:
class C
{
public:
C(int x)
:_x(x)
{}
private:
int _x;
};
int main()
{
C& cc3 = 2;
return 0;
}
C& cc3 = 2;
试图将一个整型字面量2
赋给C
类型的引用cc3
。这行代码会导致编译错误,原因如下:
-
引用的基本要求:在C++中,引用必须绑定到一个已经存在的对象上。引用本质上是对象的别名,它不能像指针那样独立存在
-
引用与临时对象:尽管临时对象(如通过类型转换创建的临时
C
对象)可以被绑定到const
引用上(即const C&
),但它们不能直接绑定到非const
引用(C&
)上。这是为了防止通过非const
引用对临时对象进行修改,因为这种修改通常没有意义(临时对象在表达式结束后就销毁了)。 -
正确的用法:如果你的意图是创建一个
C
类型的临时对象,并将其绑定到引用上,正确的语法应该使用const
引用,如下:const C& cc3 = C(2); // 或者 const C& cc3 = 2; // 依赖于C(int)构造函数的隐式类型转换
这两种方式都是可行的,它们创建了一个
C
类型的临时对象,并将其绑定到const
引用cc3
上。由于引用是const
的,你不能通过cc3
修改对象的状态。
要解决原代码中的问题,需要确保使用const
引用来引用临时对象,或者创建一个非临时的C
对象并将其赋给一个非const
引用。例如:
C cc4(2);
C& cc3 = cc4; // cc3引用cc4
在这个修正后的示例中,cc4
是一个非临时的C
对象,cc3
是一个类型为C&
的引用,它直接引用(或绑定到)cc4
上
这个真正好处我们在后面会用到:
class Stack
{
public:
void Push(const C& c)
{
//
}
};
比如我们想要在栈这个容器中压入c类型的对象有两种方式:
Stack st;
C cc3(3);
st.Push(cc3);
st.Push(4);
直接用隐式类型转换就方便了很多
1.3explicit关键字
如果不想让隐式类型转换发生,我们就需要用 explicit
修饰构造函数,禁止类型转换
单参构造函数,没有使用explicit修饰,具有类型转换作用
C++11及以后版本版本支持多个参数隐式类型转换
class A
{
public:
//explicit A(int a1, int a2)
A(int a1, int a2)
:_a1(a1)
,_a2(a2)
{}
private:
int _a1;
int _a2;
};
int main()
{
A aa={1,2};
return 0;
}
不想让隐式类型转换发生,可以加上explicit关键字