「这是我参与2022首次更文挑战的第25天,活动详情查看:2022首次更文挑战」
一、再谈构造函数
💦 构造函数体赋值
❓ 引出初始化列表 ❔
class A
{
public:
A(int a = 0)
{
_a = a;
}
private:
int _a;
};
class B
{
private:
int _b = 1;
A _aa;
};
int main()
{
B b;
return 0;
}
📝 说明
对于 B,我们不写构造函数,编译器会默认生成 —— 内置类型不处理,自定义类型会去调用它的默认构造函数处理 (无参的、全缺省的、编译器默认生成的),注意无参的和全缺省的只能存在一个,如果写了编译器就不会生成,如果不写编译器会默认生成。这里 C++ 有一个不好的处理 —— 内置类型不处理,自定义类型处理。针对这种问题,在 C++11 又打了一个补丁 —— 在内置类型后可以加上一个缺省值,你不初始化它时,它会使用缺省值初始化。这是 C++ 早期设计的缺陷。
class A
{
public:
A(int a = 0)
{
_a = a;
cout << "A(int a = 0)" << endl;
}
A& operator=(const A& aa)//不写也行,因为这里只有内置类型,默认生成的就可以完成
{
cout << "A& operator=(const A& aa)" << endl;
if(this != &aa)
{
_a = aa._a;
}
return *this;
}
private:
int _a;
};
class B
{
public:
B(int a, int b)
{
//_aa._a = a;//err:无法访问private成员
/*A aa(a);
_aa = aa;*/
_aa = A(a);//简化版,同上
_b = b;
}
private:
int _b = 1;
A _aa;
};
int main()
{
B b(10, 20);
return 0;
}
📝 说明
对上,_b只能初始化成1,_a只能初始化成0 ❓
这里可以显示的初始化,利用匿名对象来初始化 _a。
但是这种方法代价较大 (见下图)。
💦 初始化列表
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个 "成员变量" 后面跟一个放在括号中的初始值或表达式。
class A
{
public:
A(int a = 0)
{
_a = a;
cout << "A(int a = 0)" << endl;
}
A& operator=(const A& aa)
{
cout << "A& operator=(const A& aa)" << endl;
if(this != &aa)
{
_a = aa._a;
}
return *this;
}
private:
int _a;
};
class B
{
public:
B(int a, int b)
:_aa(a)
{
_b = b;
}
private:
int _b = 1;
A _aa;
};
int main()
{
B b(10, 20);
return 0;
}
📝说明
可以看到对比函数体内初始化,初始化列表初始化可以提高效率 —— 注意对于内置类型你使用函数体或初始化列表来初始化没有区别;但是对于自定义类型,使用初始化列表是更具有价值的。这里还要注意的是函数体内初始化和初始化列表是可以混着用的。
❓ 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化 ❔
什么成员是必须使用初始化列表初始化的 ❓
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
};
⚠ 注意
1️⃣ 每个成员变量在初始化列表 (同定义) 中只能出现一次 (初始化只能初始化一次)。
2️⃣ 类中包含以下成员,必须放在初始化列表位置进行初始化:
1、引用成员变量 (引用成员必须在定义的时候初始化)
2、const 成员变量 (const 类型的成员必须在定义的时候初始化)
3、自定义类型成员 (该类没有默认构造函数)
❓ 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中出现的先后次序无关 ❔
#include<iostream>
using namespace std;
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();
}
📝 说明
上面的程序输出 ❓
A. 1 1
B. 程序崩溃
C. 编译不通过
D. 1 随机值
如上程序的输出结果是 D 选项,因为 C++ 规定成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其初始化列表中出现的先后次序无关。实际中,建议声明顺序和初始化列表顺序保持一致,避免出现这样的问题。
💦 explicit关键字
class A
{
public:
A(int a)
:_a(a)
{
cout << "A(int a)" << endl;
}
A(const A& aa)
{
cout << "A(const A& aa)" << endl;
}
private:
int _a;
};
int main()
{
A aa1(1);
A aa2 = 1;
return 0;
}
📝 说明
A aa2 = 1; 同 A aa1(1); 这是 C++98 支持的语法,它本质上是一个隐式类型转换 —— 将 int 转换为 A,为什么 int 能转换成 A 呢 ? —— 因为它支持一个用 int 参数去初始化 A 的构造函数。它俩虽然结果是一样的,都是直接调用构造函数,但是对于编译器而言过程不一样。
🍳验证
🔑拓展
针对于编译器优化、底层机制这类知识可以去了解一下《深度探索C++对象模型》
❓ 如果不想允许这样的隐式类型转换的发生 ❔
这里可以使用关键字 explicit
explicit A(int a)
:_a(a)
{
cout << "A(int a)" << endl;
}
error C2440:无法从 int 转换成 A
❓ 多参数隐式类型转换 ❔
class A
{
public:
A(int a1, int a2)
:_a(a1)
{
cout << "A(int a1, int a2)" << endl;
}
A(const A& aa)
{
cout << "A(const A& aa)" << endl;
}
private:
int _a;
};
int main()
{
A aa1(1, 2);
//A aa2 = 1, 2;//???
A aa2 = {1, 2};
return 0;
}
📝说明
A aa2 = 1, 2; ???
明显 C++98 不支持多参数的隐式类型转换,但是 C++11 是支持的 —— A aa2 = {1, 2}; ,同样编译器依然会优化。
当我们使用 explicit 关键字限制时,它会 error C2440:无法从 initializer-list 转换为 A