前面我们讲过构造函数初始化还可以使用初始化列表。那么初始化列表是如何使用的呢?
Date(int& x, int year = 1, int month = 1, int day = 1)
:_year(year)
,_month(month)
,_day(day)
{}
class Date
{
public:
Date()
:_ref(x)
,_n(1)
{}
private:
int& _ref; // 引⽤成员变量
const int _n; // const成员变量
};
#include<iostream>
using namespace std;
class A
{
public:
A(int a)
:_a1(a)
, _a2(_a1)
{}
void Print() {
cout << _a1 << " " << _a2 << endl;
}
private:
int _a2 = 2;
int _a1 = 2;
};
int main()
{
A aa(1);
aa.Print();
}
上面的程序中运行结果是什么?
实例化对象时传值为1传参,调用构造函数,通过初始化列表将_a1初始化为1,然后再把_a2初始化为1。调用Print成员函数,打印后发现值为1 和随机值。这是为什么呢?
需要注意初始化列表是跟类中声明顺序走,因此是先把_a2初始化为_a1(_a1还没初始化),再将_a1初始化为1。
int main()
{
int a = 1;
const double& b = a;
return 0;
}
在上面这段程序中,定义了int类型的变量a,double类型的b引用a,此时会发生隐式类型转换,而这里的类型转换会产生一个临时对象,这个临时对象具有常性,因此需要加const。
在C++11后还支持多参数的类型转换,用{ ... }的方式。
如在后续使用的map中,往对象m中插入1和2时,隐式类型转换成value_type类型,即pair<...,...>
#include<map>
#include<iostream>
using namespace std;
int main()
{
map<int, int> m;
m.insert({ 1,2 });
return 0;
}
class Date
{
private:
static int _ret;
}
//类外进行初始化
int Date::_ret=0;
此题限制了for、while、if等关键词的使用,让我们无法通过递归等方式来解决。此题可以用static成员来解决问题。
定义两个static成员,一个_count来记录最终结果,_i表示1、2、3...,那么求n!时,只需要调用n次构造函数,由于是静态成员,生命周期被延长了,使得_i能表示1、2...,每次将_i的值给_count即得最终答案。
class Sum
{
public:
Sum()
{
_count+=_i;
_i++;
}
static int GetSum()
{
return _count;
}
private:
static int _i;
static int _count;
};
int Sum::_i=1;
int Sum::_count=0;
class Solution {
public:
int Sum_Solution(int n) {
Sum a[n];
return Sum::GetSum();
}
};
上文实现Date类的流输出和流输入时就需要定义友元函数。
class Date
{
//友元
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
}
ostream& operator<<(ostream& out, const Date& d);
istream& operator>>(istream& in, Date& d);
但是小编并不建议多用友元函数,虽然其提供了便利。但是友元的随意访问破坏了封装,会增加耦合度。 在软件工程中强调“高内聚,低耦合”的概念,耦合性越高,模块的独立性就越差。
之前在传参的时候,往往需要先定义一个有名对象再来传参。但是这过于麻烦。于是C++中定义了一个匿名对象:用类型(实参) 定义出来的对象。匿名对象⽣命周期只在当前⼀⾏,⼀般临时定义⼀个对象当前⽤⼀下即可。
匿名对象作缺省参数
class A
{
public:
A(int a=0)
:_a(a)
{
cout << "A(int a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
void func2(const A& aa = A())
调试可以发现匿名对象生命周期只有这一行,我们可以看到下一行他就会自动调用析构函数 。
当然可以通过const引用延长匿名对象的生命周期,匿名对象跟着引用走。
const A& ref2 = A();
class A
{
public:
A(int a = 1)
:_a1(a)
{
cout << "A()" << endl;
}
A(const A& d)
:_a1(d._a1)
{
cout << "A(const A& d)" << endl;
}
A& operator=(const A& aa)
{
cout << "A& operator=(const A& aa)" << endl;
if (this != &aa)
{
_a1 = aa._a1;
}
return *this;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a1 = 1;
};
int main()
{
// 构造+拷贝-》优化为直接构造
A aa1 = 1;
return 0;
}
在上面这段程序中,我们期望的是1调用构造函数,然后再调用拷贝构造初始化对象aa1,但是通过调试发现调用完构造函数并未调用拷贝构造。这是为什么呢?
现代编译器会 为了尽可能提⾼程序的效率 ,在不影响正确性的情况下会尽可能减少⼀些传参和传返
回值的过程中 可以省略的拷⻉ 。且优化情况由各个编译器处理。尤其新⼀点的编译器对于连续⼀个表达式步骤中的连续拷⻉会进⾏合并优化。
隐式类型,连续构造 + 拷⻉构造 -> 优化为直接构造
void f1(A aa)
{
//...
}
int main()
{
f1(1);
return 0;
}
在上面这段程序中,1要隐式类型转换成A类类型对象,因为是传值传参要调用拷贝构造。但是,通过调试发现仅仅调用了拷贝构造,即被编译器优化了。
接着,我们来看看编译器对下面程序的优化程度。
A f2()
{
A aa;
return aa;
}
int main()
{
A aa2 = f2();
return 0;
}
在上面这段代码中(类并未写出),构造出了对象aa后,返回对象aa,由于是在f2的栈区中,不能直接返回,编译器理应会拷贝构造一个临时对象,再将临时对象拷贝给aa2对象。但是通过调试返回时编译器并未调用拷贝构造函数,更厉害的是在vs这个编译器下进行跨行合并优化,将构造的局部对象aa和拷贝的临时对象和接收返回值对象aa2优化为一个直接构造。
接着我们在linux下运行此程序,编译时⽤ g++ test.cpp -fno-elide-constructors可关闭编译器优化。
可以发现编译器先是调用了构造函数,返回时调用拷贝构造给临时对象,再将临时对象拷贝构造给对象aa2。