目录
·初始化列表:
explicit
static修饰的静态成员变量
友元:
友元类:
内部类
匿名对象
拷贝对象时的一些优化:
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
如代码所示:
代码中的这部分就是初始化列表,初始化列表也可以完成构造函数的任务。
并且构造函数函数体内的初始化列表可以混合着一起使用。
我们写一个栈的构造函数:
Stack(int capacity = 4)
: _a((int*)malloc(sizeof(int)*capacity))
, _top(0)
, _capacity(capacity)
{
if(_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
}
同样,我们也可以这样写:
我们可以把_a的初始化放到函数体内:
Stack(int capacity = 4)
: _top(0)
, _capacity(capacity)
{
_a = (int*)malloc(sizeof(int)*capacity);
if(_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
}
当然,初始化列表并不能够解决全部的初始化问题。
假如我们想要把我们动态申请的_a的这部分空间全部置为空,我们的初始化列表就完成不了。
例如:
Stack(int capacity = 4)
: _top(0)
, _capacity(capacity)
{
_a = (int*)malloc(sizeof(int)*capacity);
if(_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
memset(_a, 0, sizeof(int)*capacity);
}
总结:初始化列表是在构造函数体之外写的一种初始化方法,但是初始化列表并不能够解决全部的初始化问题,所以我们需要和构造函数的函数体内容一起配合使用。
问题:初始化列表的一个变量可以被重复初始化吗?
答:不能,例如:
_top被初始化了两次。
这里就会报错。
问题:既然构造函数体能够解决全部的问题,而初始化列表只能解决一部分问题,那初始化还起到什么作用呢?什么场景下需要使用初始化列表呢?
答:例如const修饰的成员变量
class B
{
public:
B()
{
_n = 10;
}
private:
const int _n;
};
int main()
{
B b;
return 0;
}
对于这样的一串代码,我们进行编译:
报错的原因是这样:
int main()
{
const int a;
return 0;
}
例如这样的一串代码,我们进行编译:
报错的原因是const修饰的变量是静态变量是不可以修改的,所以我们必须在其定义时进行赋值:
同理:
我们的_n定义发生在我们创建对象的时候
_n的定义再细节一些:定义是在构造函数体内还是初始化列表呢?
答:定义是在初始化列表中进行的,我们的初始化列表并没有写这个静态变量的初始化,所以就会报错。
如何解决这个问题呢?
答:我们可以在初始化列表中进行初始化:
总结:所有的成员变量进行初始化时都要经过初始化列表,并且成员变量的定义发生在初始化列表中。
我们再写一个内置类型的成员变量,不显示写初始化列表,看初始化列表对内置类型如何处理:
例如:
_m内置类型被初始化成了随机值。
得出结论:对于内置类型,假如我们不显示写初始化列表时,初始化列表会把内置类型初始化成为随机值。
假如我们在声明位置给_m一个缺省值,_m的值会变成什么?
所以,缺省值对于初始化列表也是同样适用的。
但是,假如我们对内置类型进行显示初始化列表呢?
假如我们显示初始化列表了,那我们给的缺省值就不起任何作用了。
总结:如果没有在初始化列表中显示初始化,对于内置类型,假如有缺省值就用缺省值,没有缺省值就用随机值。
那对于自定义类型呢?
例:
我们在类B中的成员变量中添加一个内置类型的A,A是这样的一个类:
class A
{
public:
A(int a)
:_a(a)
{}
private:
int _a;
};
我们可以发现,A没有默认构造函数。
默认构造函数的定义:我们不写参数也可以调用的函数叫做默认构造函数,例如无参,全缺省,或者不写编译器默认给的。
我们进行编译:
会报错。
但是假如我们在类A中写一个默认构造函数呢?
我们进行调试:
成功调用了默认构造函数。
总结:假如我们没有在初始化列表中显示初始化时,对于自定义类型,假如我们的自定义类型有默认构造函数,调用默认构造函数,假如没有默认构造函数,就报错。
那我们该如何显示的在初始化列表中初始化自定义类型呢?
例如:
和内置类型的初始化是相同的
这里就相当于显示调用类A的构造函数。
我们写一个之前熟知的用两个栈来实现队列:
class MyQueue {
public:
MyQueue()
{}
void push(int x)
{
_pushST.Push(x);
}
private:
Stack _pushST;
Stack _popST;
size_t _size = 0;
};
我们的构造函数和初始化列表就这样写:
因为我们没有在初始化列表中显示初始化,所以对于自定义类型,默认调用其构造函数,假如没有构造函数的话就报错。对于内置类型,有缺省值的用缺省值,没有缺省值的用随机值。
假如我们删除栈的默认构造函数:
例如:
我们进行调用:
这个时候就会显示没有默认构造函数可用。
尽管我们没有创建对象,但是只要我们写了初始化列表,我们就会检查对应的自定义类型是否会有构造函数,没有的话,就会报错。
所以什么情况下需要使用初始化列表:当我们是自定义类型,并且我们没有写默认构造函数时,这时候,我们需要自己写初始化列表。
我们先恢复栈的默认构造:
定义一个队列的对象:
我们看是否可以实现初始化:
可以完成初始化。
假如我们要求MyQueue需要输入一个参数:
我们输入一个参数看是否能完成初始化:
我们进行调试:
我们发现,并没有完成我们期望的初始化。原因是什么?
答:无论我们显示写不写初始化列表,我们的全部成员变量都会走初始化列表,对于自定义类型的化,调用其默认构造,这里的默认构造就是栈的默认构造,我们的栈的默认构造的capacity,对于内置类型我们有缺省值就用缺省值,没有缺省值的话,就用默认构造。
那我们该如何显示初始化列表呢?
答:
总结:有哪些成员必须要显示初始化?
答:1:const修饰的静态成员变量
2:引用成员变量
3:没有写默认构造函数的自定义类型
注意:尽量去使用初始化列表去初始化,因为无论你是否使用初始化列表,对于自定义类型,一定会使用初始化列表去初始化。
我们来写一道题目:
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 随机值
原因如下:我们的初始化列表的初始化顺序是根据成员变量的书写顺序进行的,所以我们先初始化的是_a2,_a2的结果是_a1,但是_a1这时候还没有被初始化,_a1这时候的结果是随机值。
所以_a2的结果就是随机值。
我们再初始化_a1,_a1是_a的拷贝,所以_a1的结果为1.
我们打印的顺序是先打印_a1,再打印_a2,所以我们的结果就是1和随机值。
判断题
一个类如果不提供默认构造的话就会报错?
答:错误,例如:
class A
{
public:
A(int a)
/*:_a1(a)
, _a2(_a1)*/
{}
void Print(){
cout << _a1 << " " << _a2 << endl;
}
private:
int _a2;
int _a1;
};
我们进行编译:
原因:我们并通过该类来创建对象
当我们使用该类来创建对象,并且不输入参数时,就会报错:
这个时候进行运行就会报错。
当我们注释掉自己写的构造函数时,就不会报错:
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;
aa.Print();
return 0;
}
因为默认构造函数分为三种:
1:全缺省
2:无参
3:我们不写,编译器默认的。
这里我们不写,就会执行编译器默认的构造函数。
我们再写一个例子来加深印象:
class A
{
public:
A(int a)
:_a1(a)
, _a2(_a1)
{}
void Print(){
cout << _a1 << " " << _a2 << endl;
}
private:
int _a2;
int _a1;
};
class B
{
private:
A _aa;
};
int main()
{
B bb;
return 0;
}
我们这里没有创建对象,就不会报错
但是我们假如创建了对象就会报错:
原因是什么,我们进行分析:
答:首先,我们先创建一个对象bb,对象bb中有一个成员变量_aa,我们没有显示写初始化,对于自定义类型会调用其默认构造:
而这里又没有默认构造,就会报错。
class A
{
public:
A(int a)
:_a1(a)
, _a2(_a1)
{}
void Print(){
cout << _a1 << " " << _a2 << endl;
}
private:
int _a2;
int _a1;
};
class B
{
public:
B()
{}
private:
A _aa;
};
int main()
{
/*B bb;*/
return 0;
}
但是我们这里没有创建对象也会报错。
原因是什么?
答:原因是只要我们这里写了
只要我们显示写了这部分,我们就会首先进行初始化列表(无论我们是否创建对象),对于自定义类型会调用其默认构造。
但是我们并没有默认构造,所以就会报错。
总结:情况1:没有显示写初始化列表,对于自定义类型,会调用其默认构造,假如没有默认构造,就会报错(创建过对象)
情况2:只显示了初始化列表的框架,对于自定义类型,会调用其默认构造,假如没有默认构造,就会报错(无论是否创建对象)
注意:能用初始化列表就用初始化列表,因为初始化列表一定不会错。
2:尽量给一个函数提供默认构造。
例:
对于这个日期类,我们一般是这样创建对象的:
但是现在我们c++98中又添加了一种写法:
我们可以这样写。
这种写法如何理解呢?
答:我们可以类比整型和浮点型之间的赋值:
我们可以对这里这样理解:首先把浮点型强制类型转换为整型,赋给一个临时变量tmp,这个临时变量的类型是整型,然后再把该临时变量赋给d。
我们可以这样理解成这样的代码:
这里的意思是我们首先把2022当作参数创建一个临时日期类对象tmp,然后再进行拷贝构造,把tmp拷贝给d2.
这里相当于把构造和拷贝构造隐式类型转换成为了一个构造
我们再举一个例子:
为什么加上了const就没有错误提示符,而没有加const就有错误提示符呢?
答:因为我们首先通过参数2022创建一个临时日期类对象,该日期类对象不可修改,具有常性,我们的引用的类型不对等,所以我们要加上const把类型也转换成具有常性的日期类对象来接收。
我们用内置类型也可以解释:
这类相对简便的写法单参数的对于单参数的构造函数是支持的。
假如我们不想让这类隐式类型转换发生,我们可以加一个explicit
这时候,我们的这种写法就会报错了。
我们就不能再使用这种隐式类型转换了。
是不是仅仅只有一个参数的构造函数才能使用这种隐式类型转换呢?
答:并不是,例如,我们可以这样写:
class Date
{
public:
/*explicit Date(int year)
:_year(year)*/
Date(int year, int month = 1,int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
虽然我们有3个参数,但是因为我们的构造函数的三个参数有两个参数是缺省值,所以我们依然可以只输入一个参数完成构造。
我们进行实验:
没有报错,我们的假设成功。
当然全缺省的也是可以的:
也是可以运行成功的。
如果是多参数呢?
例如:
我们这样写明显不可以。
那这样呢?
这样写也是不可以的。
这样写可以吗?
这样写是可以的。
这两种写法对应的结果其实是一样的,只是过程不同。
总结:c++98支持单参数的隐式类型转换,而c++11才支持了多参数的隐式类型转换
隐式类型转换途中生成的临时变量是具有常性的。
explicit修饰的构造函数不能使用隐式类型转换
面试题:实现一个类,计算类中一共创建了多少个对象。
class A
{
public:
A(int a = 0)
:_a(a)
{}
private:
int _a;
};
int main()
{
A aa1(1);
A aa2 = 2;
return 0;
}
统计以下A类型的对象创建了多少个。
我们可以分析出一个结论:以A类型创建的对象不是构造出来的就是拷贝构造出来的。
所以我们可以这样写:
int N = 0;
class A
{
public:
A(int a = 0)
:_a(a)
{
++N;
}
A(const A&aa)
:_a(aa._a)
{
++N;
}
private:
int _a;
};
int main()
{
A aa1(1);
A aa2 = 2;
A aa3 = aa1;
cout << N << endl;
return 0;
}
我们的思路是这样的:我们先创建一个全局变量N,当调用构造函数或者调用拷贝构造函数时,我们的N++,我们就可以检查出一个创建了多少个对象了。
我们构造了2个,拷贝构造了1个,所以打印的结果应该为3
说明我们的编译器经过了优化,假如我们的编译器不进行优化,
这里就是一个构造和一个拷贝构造,那我们编译的结果就是4了。
但是这里有一些问题需要注意以下:
例如:我们多写一个函数:
这个函数的参数是一个类对象。
我们在main函数中进行调用:
进行运行:
我们进行运行:
我们可以发现 ,当我们调用了F1函数,N的值就加了1,原因是什么呢?
因为我们调用函数需要传递参数,我们需要传递的是一个类对象的参数,传递参数的过程本身就是一种拷贝,所以就会调用拷贝构造,调用拷贝构造的话,对应的N就加1.
传值传参会调用拷贝构造,那传值返回会吗?
答:会,例如:
我们先写一个传值返回的函数:
A F2()
{
A aa;
return aa;
}
我们在main函数的函数体内对F2进行调用:
我们进行编译:
3,4我们已经解释过了,那这里的6的原因是什么呢?
答:
因为我们在函数体内创建了一个对象,会调用构造函数,然后再对对象进行返回,返回给返回值本身,这里本质上也发生了拷贝,所以会调用拷贝构造,所以这个函数既调用了构造函数又调用了拷贝构造,所以N+2.
我们使用引用传参:
这个时候,我们继续运行:
我们可以发现,传引用传参相对于传值传参的作用在于减少了拷贝构造。
假设我们传引用返回呢?
但是本质上,这里传引用是错误的,因为函数调用完毕时,aa就会被释放,对于释放的空间我们再引用返回,返回的值就是被释放的空间。
不过,我们这里强制的传引用返回:
传引用返回也可以减少拷贝构造。
总结:传引用传参和传引用返回都会减少拷贝构造函数的产生。
但是,这里的全局变量N其实存在有一些问题的。
首先,这里的全局变量N什么时候在什么地方都可以修改:
我们进行编译:
我们总结以下影响变量的生命周期的都有哪些?
答:1:局部变量在栈里面
2:静态变量在静态区,既会影响生命周期又会影响链接属性
链接属性指的是链接过程中是否会进入符号表。
3:malloc申请的空间在堆上
4:常量:存储在常量区,代码段。
局部的静态变量和全局的静态变量的区别是什么?
答:生命周期都是全局,局部的静态变量的作用域是局部,全局的静态变量的作用域是全局,局部的静态变量的生命周期是局部。
我们可以在类里面设置一个静态变量:
类里面的静态变量有什么特点:
1:收到类域的限制
2:生命周期是全局的。
我们提出一个问题:类里面的静态变量存在于对象里吗?
aa1和aa2都有_a,那么aa1和aa2有没有N?
答:没有,因为对象aa1和aa2都是局部变量,局部变量在栈上面,而静态变量在静态区,两者肯定不能相互包含
两者的关系是类对象共享一个N,我们对一个类对象中的N进行修改,所有对象中的N都发生了改变。
这里的N可以给缺省值吗?
答:不可以,因为这里的N只是一个声明,静态变量是在定义时进行初始化的,所以我们这里不可以给缺省值。
我们在哪里对静态变量进行初始化呢?
答:我们可以在类外面对类里面的静态变量进行初始化
类里面的静态变量什么特点
答:生命周期是全局的,因为类是不会销毁的。
作用域受类域限制。
我们先把类里面的静态变量设置为public的:
对于公开的类的静态变量,我们有几种访问方法?
答:两种,1:我们可以用域操作限定符来进行访问:
2:我们也可以用对象来进行访问:
这里的意思并不是说对象aa1中含有静态变量N,而是说aa1属于类A,类A中包含了静态变量N,然后我们可以对N进行访问。
我们也可以用指针来访问N
例如:
当然对于空指针也是可以访问的:
原因是只要表示我们这里的ptr的类型是A*,就可以对N进行访问。
那假设我们的静态变量是私有的,我们怎么进行处理?
答:我们可以用java的写法,在类里面写一个成员函数来取出类里面的成员变量。
假设我们要访问N时,我们需要这样写:
但是有对象的时候,我们可以顺便调用,但是没对象的时候,假如我们要调用函数的话,还需要额外创建一个对象,并且只要我们创建了一个对象,我们的N值就会++,怎么处理这种情况?
答:这个时候,我们可以写一个静态成员函数。
写一个静态成员函数就能解决我们的问题,因为当我们是静态成员函数,静态成员函数没有this指针,所以我们不需要创建对象也可以调用函数
当然,即使我们有对象时,我们也可以用普通的方法:
这种写法也是正确的。
这种方法叫做突破类域的方法。
静态成员函数能够访问其他的成员变量吗?
答:不可以,例如:
因为没有this指针,所以我们无法访问其他的成员变量。
总结:在类里面我们设置静态变量,这个静态变量有以下特点:
1:生命周期是全局的。
2:受类域限制。
3:不在对象里,而被所有对象所共享
我们也可以设置静态成员函数来取出受private限制的静态变量
特点
1:静态成员函数没有this指针,所以不需要对象也可以调用
2:有对象情况下依然可以调用。
3:因为没有this指针,只能访问静态成员变量,不能访问其他的变量。
我们做一道算法题目:
正常,我们用循环就可以求出n的阶乘,但是这里并没有求出,我们该怎么处理呢?
答:我们可以采用刚刚的思路,写一个类,写一个静态成员变量,调用n次构造函数即可。
class Sum {
public:
Sum() {
_ret += _i;
++_i;
}
static int GetRet() {
return _ret;
}
private:
static int _i;
static int _ret;
};
int Sum::_i = 1;
int Sum::_ret = 0;
class Solution {
public:
int Sum_Solution(int n) {
Sum arr[n];
return Sum::GetRet();
}
};
我们进行测试。
我们对代码进行分析:
我们要求n的阶乘,我们的思想是创建一个类,在类里面写一个构造函数,在构造函数中写两个静态变量,用这两个静态变量来实现阶乘,既然用了类的静态变量,我们就需要写一个静态成员函数来取出静态变量,然后我们进行输出,我们可以用变长数组的方法,这里的变长数组的意思就是创建n个对象,每创建一个对象,我们就求出对应的阶乘,然后我们求出的就是n的阶乘。
问题:
为什么这里需要使用类的静态变量,为什么不能用类的普通变量
答:因为类的静态变量具有共享的性质,也就是任何一个对象使类的静态变量改变时,这个改变对于所有其他对象的静态变量都适用
类的静态变量怎么使用?
答:静态变量的声明放到类里面,静态变量的定义放到类外面,注意静态变量的定义不需要再加static,但是需要我们的域操作限定符。然后我们在类里面写一个静态成员函数,作用是取出我们要求的静态变量。
题目:
要求我们创建的对象只能在栈上面:
答:我们先写一个简单的类。
class A
{
public:
A(int a = 0)
:_a(a)
{}
private:
int _a;
};
要求我们用这个类创建的对象只能在栈上面。
我们思考:我们创建的对象都能在什么地方?
大概分为这三部分,假如我们要求只能在栈上创建空间,我们就需要限制静态区和堆创建的对象。
我们可以从他们三个的共同点做起:他们三个都需要调用构造函数。
我们可以把构造函数设为私有的:
那假设我们要创建对象,我们只有一种方法了,那就是在类中创建对象,也就是设置一个成员函数,在该成员函数中创建对象,然后然后该对象即可。
现在,假如我们要创建对象,我们需要这样写:
但是我们发现了一处错误:那就是假如我们需要调用GetObj函数时,我们需要创建一个对象才可以调用,但是我们又只有调用函数才能创建对象,所以就产生了矛盾。
这时候,我们想起了static,静态成员函数没有this指针,调用的时候不需要对象。
这个时候,我们就不需要创建对象既可以调用函数了:
这个时候,我们就实现了只能在栈上创建对象:
因为假如我们要创建对象,只能调用静态成员函数,通过接收静态成员函数的返回值来创建对象,但是我们的静态成员函数内部是创建的对象是在栈上面的,所以我们保证了创建的对象都在栈上面。
总结:我们创建的对象可以在栈,堆,静态区。
我们了解了一种题型:限制创建的对象的位置。
我们的大体思路分为以下即可步骤
1:限制构造函数的使用
2:创建成员函数来调用构造函数
3:使用静态成员函数,不需要创建对象
4:实现限制创建对象的位置。
class Date
{
public:
Date(int year, int month ,int day)
:_year(year)
, _month(month)
, _day(day)
{}
void SetTimeOfDate(int hour, int minute, int second)
{
_t._hour = hour;
_t._minute = minute;
_t._second = second;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
class Time
{
friend class Date;
public:
Time(int hour = 0, int minute = 0, int second = 0)
:_hour(hour)
, _minute(minute)
, _second(second)
{}
private:
int _hour;
int _minute;
int _second;
};
如代码所示:Time类就是Date类的友元类。
假如一个函数是一个类的友元函数,那么这个函数就可以访问这个类里面的所有成员变量,友元类的所有成员函数都可以是另一个类的友元函数,也就是说Date类的所有成员函数都可以访问Time类的所有成员变量。
例如这里:
友元函数是单向的,不具有交换性:
我们的Date是Time的友元类
所以
所以,Date的成员函数可以访问Time的成员变量,而Time的成员函数不能访问Date的成员变量。
我们举一个例子:
class A
{
private:
int _a;
public:
class B
{
int _b;
};
};
int main()
{
cout << sizeof(A) << endl;
return 0;
}
打印的结果是4还是8?
原因如下:我们的类A中只有一个成员变量,是一个整型,占4个字节。
而我们的类B虽然是类A的内部类,但是类B和类A本质上没有所属关系。
那么我们创建一个类A对象中有没有类B的成员?
答:没有,例如:
对象aa中并没有类B的成员变量。
这两个类就相当于两个独立的类。
B类受A的类域和访问限定符的限制。
例如:
会报错
我们要创建B类对象需要这样创建:
内部类是外部类的友元函数:
例如:
内部类可以访问外部类的成员变量。
而外部类不能访问内部类的成员函数。
内部类可以直接访问外部类的静态成员变量并且不需要创建对象。
例如:
原因是静态成员变量可以是类所共享的,可以突破类域。因为类B是类A的友元类,所以B可以直接访问类A的静态成员变量。
我们之前写的那道面试题,我们可以用内部类的方式解决:
我们可以把Sum类写成Solution的内部类
int Sum::_ret = 0;
int Sum::_i = 1;
class Solution {
public:
int Sum_Solution(int n) {
Sum arr[n];
return Sum::GetRet();
}
private:
class Sum {
public:
Sum() {
_ret += _i;
_i++;
}
static int GetRet() {
return _ret;
}
private:
static int _i;
static int _ret;
};
};
但是我们的外部类是无法访问内部类的静态成员变量的。
所以我们可以把静态成员变量写道外面的类里面。
因为内部类是外部类的友元函数,所以内部类可以访问这些静态成员变量。
然后我们把类的初始化的域作用限定符改以下即可。
总结:内部类
1:内部类是外部类的友元函数
2:外部类无法访问内部类的成员变量
3:外部类所占空间的大小不包含内部类。
4:内部类可以直接访问外部类的静态成员变量,无需船舰对象。
我们先写一个类
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
对于这个类,创建对象的方法有哪些?
第一个就是普通的构造,第二个是隐式类型转换,本质上是构造和拷贝构造的集合。
我们可以写两个匿名对象:
匿名对象只存在于创建它的那一行。
而有名对象的生命周期在所在的局部域,这里就在main函数中。
例如,我们进行调试:
接下俩,我们来创建第一个匿名对象。
我们可以发现,刚过了创建匿名对象这一行,匿名对象的构造和析构函数就都进行完毕了,所以匿名对象的生命周期在创建它的那一行。
匿名对象在一些情况下是有意义的:
例如:
class Solution{
public:
int Sum_Solution(int n){
return n;
}
};
int main()
{
Solution so;
so.Sum_Solution(10);
return 0;
}
对于这个类,我们本来的目的只是为了调用函数,返回n值,但是假如我们需要调用函数的话,还需要创建一个又名对象。
这个时候,匿名对象就派上了用场:
我们可以这样写:我们不需要对对象起名字了,并且1行就解决了我们的问题。
我们再举一个例子:
class A
{
public:
A(int a=1)
{}
private:
int _a = 0;
};
A F()
{
A ret(10);
return ret;
}
假如我们要传对象返回时,我们需要创建一个对象,然后返回。
有了匿名对象,我们可以这样写:
这样就方便多了。
总结:匿名对象的特性
1:写法:类名(+参数)
2:生命周期:创建对象的那一行
3:匿名对象用于调用类中函数和传值返回类中。
例如,我们写一个类
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
A(const A&aa)
:_a(aa._a)
{
cout << "A(const A& aa" << endl;
}
A&operator=(const A& aa)
{
cout << "A& operator=(const A& aa)" << endl;
if (this != &aa)
{
_a = aa._a;
}
return *this;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
我们这样创建参数就是进行优化:
int main()
{
A aa1 = 1;
return 0;
}
我们知道,这样写的本质是构造加上拷贝构造,假如编译器进行了优化,那我们就只进行构造:
我们没有进行拷贝构造,编译器执行了优化。
传参也可以优化
正常情况下的传参:
void f1(A aa)
{}
int main()
{
/*A aa1 = 1;
return 0;*/
A aa1;
f1(aa1);
return 0;
}
我们进行运行的结果应该是一个构造加上一个拷贝构造。
如图所示。
我们可以这样写来进行优化:
这时候,我们进行编译:
我们只调用了一个构造函数。
假如我们在这里传一个引用呢?
然后我们进行调用:
我们进行编译:
和上次的结果相同,但是
注意:这个写法在有些编译器下是不支持的,因为有些编译器会把匿名对象看作常性,我们不能用引用来接收常数,所以会报错。
最后一个优化:
A f2()
{
A aa;
return aa;
}
int main()
{
f2();
}
我们进行编译:正常的情况下应该是构造加上拷贝构造。
这样不可以优化,但是这种写法呢?
A f2()
{
A aa;
return aa;
}
int main()
{
A ret=f2();
}
这种写法实际上经历了三次构造
1:创建对象aa的构造
2:返回aa时会调用拷贝构造
3:A ret=f2()这里也会调用拷贝构造。
所以这里应该是构造 拷贝构造 拷贝构造。
我们进行编译:
我们发现,编译器优化掉了一个拷贝构造函数。
那我们看这样写可以优化吗?
A f2()
{
A aa;
return aa;
}
int main()
{
A ret;
ret = f2();
}
我们进行分析:
首先,我们先创建一个对象ret,调用构造函数。
然后调用f2函数,先创建一个对象aa,调用构造函数
然后返回aa,调用拷贝构造,然后释放掉aa。
然后调用赋值重载:
可以发现,这种写法不能优化。
假如我们不用对对象的数据进行修改时,我们可以这样写:
A f3()
{
return A(10);
}
int main()
{
A ret = f3();
return 0;
}
这里实际上发生了构造,拷贝构造,拷贝构造。
但是我们这里进行了优化:
编译器只执行了一个构造。
总结:创建对象时,如何进行优化
1:能不创建“中间商”就不创建“中间商”
2:能直接返回匿名对象就直接返回匿名对象。