前言
本节我们来进行类和对象的收尾内容的学习,学习完本节的内容我们的C++水平就算入门了,本节部分内容仅作了解即可,因为不同的编译器行为不同,有些编译器比较激进,有些编译器比较保守。那么废话不多说,我们正式进入今天的学习
1.内部类
1.1匿名对象的概念
在C++中除了用常规的形式定义一个对象,还支持使用匿名对象的形式定义对象。用匿名对象定义一个对象的时候不会存在与函数声明冲突。
与函数声明冲突的定义形式:
class A
{
private:
int _a;
public:
A(int a = 1)
:_a(a)
{
cout << "A(int a)" << endl;
}
A(const A& a)
{
cout << "A(const A& a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
};
int main(void)
{
A aa1();
//不能够这样定义对象,因为编译器无法识别这是一个定义了的对象还是一个函数声明
return 0;
}
我们不能像上述代码一样去定义一个对象,因为编译器无法识别这是一个函数声明还是一个对象的定义
但是我们用如下的形式去定义一个对象是可行的:
int main(void)
{
A();
return 0;
}
通过这种形式定义的对象叫做匿名对象,这种定义方式也不存在和函数声明相冲突的情况。匿名对象的这个概念和临时对象有相似之处
匿名对象的特点如下:
1.匿名对象的生命周期只在当前这一行
1.2匿名对象的作用
了解了匿名对象这一个概念以后,就会有很多人会产生疑问:匿名对象的存在有什么意义?可以解决哪些问题?
C++中设置匿名对象这一个概念必然是有他的道理的,下面我们就来逐步分析匿名对象的作用:
我们先用上一节所做的练习题来举一个例子:
class Solution
{
public:
class Sum
{
public:
Sum()
{
_ret += _i;
_i++;
}
};
int Sum_Solution(int n)
{
Sum a[n];
return _ret;
}
private:
static int _i;
static int _ret;
};
int Solution::_i = 1;
int Solution::_ret = 0;
如果我们不知道匿名对象这一个概念,我们可能会采取以下的形式来调用对象中的成员函数:
int main(void)
{
Solution st;
cout << st.Sum_Solution(10) << endl;
return 0;
}
在这种情况下,我们只需要使用一个对象中的一个成员函数,但是我们还需要因此去特意定义一个对象,这样就会显得很麻烦
假设我们使用匿名对象来调用成员函数,代码整体就会显得更加简洁,避免了代码冗余的情况:
int main(void)
{
cout << Solution().Sum_Solution(10) << endl;
return 0;
}
由此我们可以知道,在某些特定的情况下使用匿名对象会更加方便
2.C++中的sort排序
我们在C语言的学习阶段中遇到排序的问题通常使用qsort、冒泡排序等……但是在C++中,我们基板上不使用qsort来进行排序,C++通常使用sort来进行排序。
sort排序和qsort排序在底层的实现中都是采用的快排,sort排序主要用于对数组和容器进行排序,sort的功能相比qsort而言会更加强大。
假设存在一个数组需要我们去排序,要是我们采取qsort来进行排序,我们还需要写一个函数,并且传入函数指针等一系列变量,会比较麻烦。而我们用sort来排序就会方便很多。
使用sort进行排序之前,我们需要包含一个头文件:<algorithm> ,在使用sort函数的时候我们只需要传入需要排序的数组的起始位置的指针,以及数组结束位置的下一个位置的指针就可以对这个数组进行排序,sort排序的默认排序顺序是升序:
int main(void)
{
int a[] = { 5,8,9,6,3,1,2,4,7,0 };
sort(a, a + 10);
for (int i = 0; i < 10; i++)
{
cout << a[i] << " ";
}
return 0;
}
此时有人可能会有疑问:假设我们需要以降序的形式来对数组数据进行排序该怎么办呢?
此时我们就还是需要写一个函数,并且将函数指针传入到sort函数的第三位:
bool myfunction(int i, int j) { return(i > j); }
int main(void)
{
int a[] = { 5,8,9,6,3,1,2,4,7,0 };
sort(a, a + 10, myfunction);
for (int i = 0; i < 10; i++)
{
cout << a[i] << " ";
}
return 0;
}
其实还有另外一种写法,这里我们只做了解,具体细节以后学习模板的时候会详细说明:
int main(void)
{
int a[] = { 5,8,9,6,3,1,2,4,7,0 };
greater<int> gt;
sort(a, a + 10, gt);
for (int i = 0; i < 10; i++)
{
cout << a[i] << " ";
}
return 0;
}
或者使用刚才所学习的匿名对象的形式,这样会更加简洁:
int main(void)
{
int a[] = { 5,8,9,6,3,1,2,4,7,0 };
sort(a, a + 10, greater<int>());
for (int i = 0; i < 10; i++)
{
cout << a[i] << " ";
}
return 0;
}
3.对象拷贝时编译器的优化
现在大部分编译器为了尽可能提高程序的效率,会在不影响代码的正确性的情况下尽可能减少一些传参和传返回值过程中可以省略的拷贝
对于编译器是如何优化的,C++标准没有严格规定,各个编译器会根据自身的情况进行处理。当前主流的编译器对于一个表达式步骤中的连续拷贝会进行合并优化,有一些更加激进的表达式甚至会跨行跨表达式合并优化
3.1 传参过程中
下面我们来举一个例子:
class A
{
public:
A(int a = 0)
:_a1(a)
{
cout << "A(int a)" << endl;
}
A(const A& aa)
{
cout << "A(const A& aa)" << 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;
};
void f1(A aa)
{}
A f2()
{
A aa;
return aa;
}
int main(void)
{
A aa1 = 1;
return 0;
}
根据我们之前所学习的知识,我们可以知道:如果在定义 aa1 对象的时候编译器不进行优化和处理时,数值1就会构造生成一个A的临时对象,然后用数值1构造的对象去拷贝构造 aa1 ,所以如果编译器完全不对其进行处理时,定义 aa1 会调用到构造函数和拷贝构造函数。
但是当我们运行代码,我们可以看到它没有调用拷贝构造函数,因为编译器对其进行了优化和处理:
这种情况下,编译器省略了临时对象的产生,直接调用构造函数。但是有一种写法不能省略掉临时对象的产生:
int main(void)
{
//A aa1 = 1;
const A& aa2 = 1;
return 0;
}
这里 aa2 运行的结果与 aa1 运行的结果是一样的,但是 aa2 在生成的时候编译器无法对其优化, aa2 在生成的过程中只要进行构造,而不要进行拷贝构造,因为它进行的是一个引用。
aa1 和 aa2 对象在定义的过程中按结果来看都是只走了一次构造函数,而没有走拷贝构造函数,而它们在本质上是存在区别的,aa1 只走了构造函数是编译器优化后的结果,而 aa2 只走了构造函数是因为 aa2 只需要走构造函数
我们再来看一种情况:
void f1(A aa)
{}
int main(void)
{
A aa3;
f1(aa3);
return 0;
}
我们可以看到:在 f1 函数中,我们使用到了传值传参,我们通过之前的学习知道传值传参的过程中会产生一个拷贝。假设我们先定义一个 aa3 对象,在把 aa3 对象传给 f1 函数,实参传递给形参的过程中会有一个拷贝。那么这个拷贝编译器会对其进行优化吗?
答案是不会
(这是常规主流编译器的处理方式,不排除有一些激进的编译器因为将其进行优化)
因为在一般的情况下编译器的优化规则是要在一个连续的过程当中才会进行优化,而 aa3 对象是已经构造好了的,此时再把 aa3 对象作为实参传递给 f1 函数的时候,就会走拷贝构造函数。
那如果我们想减少传参过程中对形参的拷贝构造的话该怎么办呢?答案是传引用,因为传引用中,形参是实参的别名,此时就不会进行拷贝构造。
如果我们使用匿名对象传参会有什么样的结果呢?我们来验证一下:
int main(void)
{
f1(A(1));
return 0;
}
此时的情况和之前 aa1 的情况一样,本来应该走构造函数和拷贝构造函数的,但是因为编译器的优化,就只走了构造函数
假设我们以下面的形式进行传参的时候又会发生什么呢?
int main(void)
{
f1(1);
return 0;
}
单参数的构造函数在构造的时候走的是隐式类型转换。在上面的情况下,会用数值1产生一个临时对象,再用产生的这个临时对象去拷贝构造。此时编译器就会对其进行优化,会采取直接构造的形式。
3.2 传返回值过程中
在返回时⼀个表达式中,连续拷贝构造 + 拷贝构造-> 优化⼀个拷贝构造。例如VS2019;⼀些编译器优化得更厉害,进行跨行合并优化,直接变为构造。例如VS2022
(因为我使用的是VS2022,所以下面的例子都是在VS2022环境下编译器的优化行为)
A f2()
{
A aa;
return aa;
}
int main(void)
{
f2();
cout << endl;
return 0;
}
我们知道 f2 是一个传值传参的函数,在 f2 中定义了一个局部对象 aa,并且 f2 会返回这个局部对象 aa。我们知道:传值传参返回的过程中会产生一个临时对象。所以按理来说,定义对象 aa 的时候会调用构造函数,而在返回 aa 的时候因为需要生成一个临时对象就会调用拷贝构造函数。因为我使用的是VS2022,编译器会对其进行优化,并且优化行为比较激进,它直接进行了跨行合并优化,将其变为构造。
在有些情况下我们可能会这样去写代码:
class A
{
public:
A(int a = 0)
:_a1(a)
{
cout << "A(int a)" << endl;
}
A(const A& aa)
{
cout << "A(const A& aa)" << 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;
}
void Print()
{
cout << "A::Print->" << _a1 << endl;
}
private:
int _a1 = 1;
};
假设我们在A中的内部写了一个打印的函数,我们在调用 Print 成员函数的过程中不想去接收对象,因为在调用函数的过程中会生成一个临时对象,所以我们可以直接用这个返回的临时对象来调用成员函数 Print 。然而这种情况只存在于VS2019那些相对保守的编译器中,VS2022的优化比较激进,它会直接省略 aa 对象的生成,直接用数值1去生成一个临时对象,再用临时对象去调用 Print 函数
(有人可能会问:为什么是省略 aa 对象的生成而不是临时对象的生成?其实我们看析构函数的位置就可以看出来,析构函数出现在最后一行,此时代表的是析构的临时对象。aa 对象的生命周期应该是在 Print 函数之前就被销毁了)
int main(void)
{
f2().Print();
cout << endl;
return 0;
}
若是在VS2019的环境下:
假设我们来重载一个前置++,我们来看一下编译器是否还是会对其进行优化?
class A
{
public:
A(int a = 0)
:_a1(a)
{
cout << "A(int a)" << endl;
}
A(const A& aa)
{
cout << "A(const A& aa)" << 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;
}
void Print()
{
cout << "A::Print->" << _a1 << endl;
}
A& operator++()
{
++_a1;
return *this;
}
private:
int _a1 = 1;
};
A f2()
{
A aa;
++aa;
return aa;
}
int main(void)
{
f2().Print();
cout << endl;
return 0;
}
我们可以看到,编译器仍然对其进行了优化,并且在优化的过程中保证了数值_a1的正确性
假设我们定义了一个 ret 对象去接收 函数 f2 返回的对象:
A f2()
{
A aa(1);
return aa;
}
int main(void)
{
A ret = f2();
return 0;
}
此时应该先会用1去构造生成对象 aa,再用对象 aa去拷贝构造生成临时对象并返回,在返回以后还会调用一次拷贝构造,用返回的临时对象拷贝构造 ret 对象。这样就会非常麻烦。
此时在VS2022环境下,因为编译器行为比较激进,他会直接省略掉多次的拷贝构造行为,只使用数值1来进行一次构造生成 ret 对象:
而在VS2019的环境下:
在VS2019的环境下,它省略掉了临时对象的生成,直接使用 aa 对象去构造 ret 对象
要是像如下的情况一样,编译器就不会有太多的优化行为(VS2022)【若是VS2019环境下就完全不会有任何的优化行为】:
A f2()
{
A aa(1);
return aa;
}
int main(void)
{
A ret;
ret = f2();
return 0;
}
因为在一个函数中,如果有连续的拷贝构造和赋值重载,编译器的优化行为就不会那么激进(VS2022)。此时数值1构造生成了对象 aa ,而对象 aa 在这里也充当了临时对象(因为可以看到 aa 出了作用域没有被析构),再用临时对象去进行赋值
在VS2019环境下,编译器不进行任何优化:
结尾
本节就是类和对象的最后一节内容,学习到这里,C++我们就基本上可以说入门了。这一块的知识我们仅作了解即可,不需要作太高的要求,下一节我们要来学习C++的内存管理,希望能给你带来帮助,谢谢您的浏览!!!