类和对象(下)(2)
static成员
• ⽤static修饰的成员变量,称之为静态成员变量,静态成员变量⼀定要在类外进⾏初始化。
• 静态成员变量为当前类的所有对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区。
#include<iostream>
using namespace std;
class A
{
public:
A()
{
++_scount;
}
A(const A& t)
{
++_scount;
}
~A()
{
--_scount;
}
private:
// 类⾥⾯声明
static int _scount;
//不能在这里给缺省值,因为这个缺省值是给初始化列表用的。但是这个值不会走初始化列表。
};
它不存在对象里面。
从sizeof计算的结果我们可以看出,它果然不是存在对象里面的。
它这样初始化:
// 类外⾯初始化
int A::_scount = 0;
• ⽤static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针。
// 实现⼀个类,计算程序中创建出了多少个类对象?
class A
{
public:
A()
{
++_scount;
}
A(const A& t)
{
++_scount;
}
~A()
{
--_scount;
}
static int GetACount()//静态成员函数
{
return _scount;
}
private:
// 类⾥⾯声明
static int _scount;
//不能在这里给缺省值,因为这个缺省值是给初始化列表用的。但是这个值不会走初始化列表。
};
// 类外⾯初始化
int A::_scount = 0;
int main()
{
//cout << sizeof(A) << endl;
//
cout << A::GetACount() << endl;
A a1, a2;
//代码块,出去后析构时--
{
A a3(a1);
cout << A::GetACount() << endl;
}
cout << A::GetACount() << endl;
return 0;
}
• 静态成员函数中可以访问其他的静态成员,但是不能访问⾮静态的,因为没有this指针。
可以看到我们的静态成员函数无法访问⾮静态的成员变量_a,因为没有this指针。
• ⾮静态的成员函数,可以访问任意的静态成员变量和静态成员函数。
//非静态的访问静态的,可以随便访问
void func()
{
cout << _scount << endl;
cout << GetACount()<< endl;
}
• 突破类域就可以访问静态成员,可以通过类名::静态成员或者对象.静态成员来访问静态成员变量和静态成员函数。
cout << A::GetACount() << endl;
cout << a1.GetACount() << endl;
• 静态成员也是类的成员,受public、protected、private访问限定符的限制。
• 静态成员变量不能在声明位置给缺省值初始化,因为缺省值是个构造函数初始化列表的,静态成员变量不属于某个对象,不⾛构造函数初始化列表。
一道题目,可以帮助感受:
当然一些编译器如VS是不支持变长数组的。
我们看一下这个问题:
构造:
局部的静态变量,无论是自定义类型还是内置类型,都是在第一次走到运行位置时才会初始化,而不是main函数之前就初始化。只有全局的静态变量才会在main函数之前就初始化。
析构:
注意静态变量d 的生命周期是全局的;后定义的先析构,所以b比a先析构,d比c先析构。
友元
• 友元提供了⼀种突破类访问限定符封装的⽅式,友元分为:友元函数和友元类,在函数声明或者类声明的前面加friend,并且把友元声明放到⼀个类的⾥⾯。
• 外部友元函数可访问类的私有和保护成员,友元函数仅仅是⼀种声明,他不是类的成员函数。
class B
{
// 友元声明
friend void func(const A& aa, const B& bb);
private:
int _b1 = 3;
int _b2 = 4;
};
void func(const A& aa, const B& bb)
{
cout << aa._a1 << endl;
cout << bb._b1 << endl;
}
• 友元函数可以在类定义的任何地⽅声明,不受类访问限定符限制。
• ⼀个函数可以是多个类的友元函数。
前置声明
看下面这个代码:
class B;//如果没有这个前置声明,A的友元函数声明中编译器不认识B
class A
{
//友元声明
friend void func(const A& aa,const B& bb);
private:
int _a1 = 1;
int _a2 = 2;
};
• 友元类中的成员函数都可以是另⼀个类的友元函数,都可以访问另⼀个类中的私有和保护成员。
class A
{
//友元声明
private:
int _a1 = 1;
int _a2 = 2;
};
class B
{
public:
void fun1(const A& aa)
{
cout<<aa._a1<<endl;
cout<<_b1<<endl;
}
void func2(const A& aa)
{
cout<<aa._a2<<endl;
}
}
• 友元类的关系是单向的,不具有交换性,⽐如A类是B类的友元,但是B类不是A类的友元。
• 友元类关系不能传递,如果A是B的友元,B是C的友元,但是A不是C的友元。
• 有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多⽤
内部类
• 如果⼀个类定义在另⼀个类的内部,这个内部类就叫做内部类。内部类是⼀个独⽴的类,跟定义在全局相⽐,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。
class A
{
private:
static int _k;
int _h = 1;
public:
class B//B默认就是A的友元
{
public:
void foo(const A& a)
{
cout << _k << endl;//B是A的友元,可以访问A的私有
cout << a._h << endl;
}
private:
int _b = 1;
};
};
int main()
{
cout<<sizeof(A)<<endl;
A::B b;
return 0;
}
A的大小为4而不是8。
• 内部类默认是外部类的友元类。
• 内部类本质也是⼀种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使⽤,那么可以考虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其他地⽅都⽤不了。
所以刚才那道题可以这样写:
也就是把Sum变为Solution的专属内部类,然后把两个静态成员变量变为在Solution中而不是Sum内部。
匿名对象
我们之前说过:
int main()
{
A aa1;
A aa1();//编译器无法识别是函数声明还是对象定义
A();//但可以这样定义对象
A(1);//也可以传参初始化匿名对象
}
最后一种写法,就是匿名对象。匿名对象和之前总提的临时对象都是编译器自己生成的没有名字的对象。与之对应的就是有名对象。
• ⽤类型(实参)定义出来的对象叫做匿名对象,相⽐之前我们定义的类型对象名(实参)定义出来的叫有名对象
那么这个匿名对象有什么用呢?
class Solution
{
public:
int Sum_Solution(int n)
{
//...
return n;
}
};
int main()
{
Solution st;
cout<<st.Sum_Solution(10)<<endl;
cout<<Solution().Sum_Solution(10)<<endl;
}
可以看到我们缩成了一句。无需定义有名对象再调用。
• 匿名对象⽣命周期只在当前⼀⾏,⼀般临时定义⼀个对象当前⽤⼀下即可,就可以定义匿名对象。
拓展知识:
对象拷贝时的编译器优化
• 现代编译器会为了尽可能提高程序的效率,在不影响正确性的情况下会尽可能减少⼀些传参和传返回值的过程中可以省略的拷⻉。
• 如何优化C++标准并没有严格规定,各个编译器会根据情况⾃⾏处理。当前主流的相对新⼀点的编译器对于连续⼀个表达式步骤中的连续拷⻉会进⾏合并优化,有些更新更"激进"的编译器还会进行跨行跨表达式的合并优化。
- 隐式类型转换时的优化
#include<iostream>
using namespace std;
class A
{
public:
A(int a = 0)
:_a1(a)
{
cout << "A(int a)" << endl;
}
A(const A& aa)
:_a1(aa._a1)
{
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;
};
int main()
{
A aa = 1;//按理说是一个构造加拷贝构造
return 0;
}
但是看结果我们发现合并为了一个构造:
//但是这样就无法省略
int main()
{
A aa1 = 1;//省略为直接去构造
const A& aa2 = 1;//这一句无法省略
return 0;
}
因为前一句是用1去构造一个A类型的临时对象,再用临时对象去拷贝构造aa1,⼀个表达式步骤中的连续拷⻉**会进⾏合并优化。但是,下一句代码中没有拷贝构造这个过程,是用1去构造一个A类型的临时对象后,aa2直接变成这个临时对象的别名。(临时对象具有常性,所以要用const)。
- 传参时的优化
void f1(A aa)
{}
int main()
{
A aa1;
f1(aa1);
return 0;
}
结果:
可以看到,没有进行优化。默认构造和拷贝构造(传值传参会调用拷贝构造)都进行了。
我们想要减少这个拷贝构造的办法是:
将函数的形参改为引用,而不是用传值传参。
形参是实参的别名。现在就没有拷贝构造了。
void f1(A aa)
{}
int main()
{
f1(A(1));//匿名对象
return 0;
}
在这里,A(1)是构造,f1()再去拷贝构造。
可以看到,结果是合并为了只有一次构造。
如果写成这样,A有单参数的构造函数,可以隐式类型转换:用1构造一个A类型的临时对象。因为是f1(1),传值传参,所以再去**拷贝构造。**所以这里又是一个连续的构造+拷贝构造,编译器进行了合并优化。
f1(A(1));//匿名对象
cout << endl;
f1(1);
cout << endl;
可以看到,这两种写法都触发了合并优化,而它们的共同的就在于⼀个表达式步骤中的连续拷⻉。
当然,有的更激进的编译器会进行跨行的合并优化。
- 传返回值时的优化
class A
{
//……
void Print()
{
cout << "A::Print->" << _a1 << endl;
}
private:
int _a1 = 1;
};
A f2()
{
A aa(1);//构造
return aa;//传值返回会生成临时对象,会拷贝构造
}
int main()
{
f2().Print();
cout << endl;
return 0;
}
打印结果:
可以看到这是比较激进的优化。
这时候有一个问题,编译器是没有生成临时对象,还是没有生成aa?
我们再将代码该得更直观一些:
int main()
{
f2().Print();
cout <<"*****************"<<endl;
return 0;
}
我们看到,这个析构发生在Print之后说明构造的是临时对象而不是aa(如果是aa,应该在调用Print之前就析构了);这个析构发生在星号之前,说明生命周期只有一行,也符合临时对象的特性。
编译器在看到f2().Print();
这样的代码后决定不生成aa了,直接用1构造临时对象作为函数返回值。(原本是需要用1构造aa,然后再用aa拷贝构造临时对象,现在直接用1构造临时对象,打印的也是临时对象的_a1)
这算是非常激进的。
如果不是在这么激进的编译器下,应该是这样的打印结果:
A(int a)//构造aa
A(const A& aa)//用aa去拷贝构造临时对象进行返回
~A()//aa生命周期结束,调用析构
A::Print->1//临时对象调用打印
~A()//临时对象生命周期结束,调用析构
*****************
那么现在如果我们改为这样:
class A
{
//……
void Print()
{
cout << "A::Print->" << _a1 << endl;
}
A& operator++()//重载一个前置++
{
++_a1;
return *this;
}
private:
int _a1 = 1;
};
A f2()
{
A aa(1);
++aa;
return aa;
}
int main()
{
f2().Print();
cout << endl;
return 0;
}
可以看到,编译器还是合并优化了,而且很聪明地知道根据语法,临时对象的值应该为2。可以说它敢大胆地优化的同时也有能力保证结果的正确性不会因优化而出错。
我们再看这个不使用匿名对象,而是接收返回值的场景:
A f2()
{
A aa(1);
return aa;
}
int main()
{
A ret = f2();
ret.Print();
cout <<"*****************"<<endl;
return 0;
}
按照语法逻辑,应该是先构造aa,再用aa拷贝构造临时对象,再用临时对象拷贝构造ret。构造+拷贝构造+拷贝构造,会如何优化呢?
稍微老一点的编译器(如VS2019):
先看第一个构造的是aa,然后第一个析构析构的就是aa。
然后这只有一次的拷贝构造可能是aa去拷贝构造临时对象,或者是aa直接去拷贝构造ret。怎么判断? 这个在星号之后才析构的,析构的只能是ret,因为临时对象得在Print之前析构。
这个拷贝构造发生在aa的析构之前,由此可知在aa析构之前就先用aa拷贝构造了ret。
所以结论就是省掉的是临时对象。
原本我们要先用aa拷贝构造临时对象,再用临时对象拷贝构造ret,这个临时对象就相当于“中间商”,编译器把这个中间商优化掉了。
新一点的编译器(VS2022):
可以看到被编译器三步合一了,A ret = f2();
合为一个构造。
从析构在星号之后可以看出构造的是ret,用1提前算好结果,直接一步到位去构造我们最后要的这个ret。
连aa都省掉了。
我们再试试++
可以看到,不是用1直接去构造ret,而是用计算好的2去构造ret。
再看这个场景:
可以看到我们现在只优化了一次,也就是传参返回时临时对象的拷贝构造被省去了。再看aa是在赋值之后析构的,且在打印之前,也就是赋值时是直接用aa去赋值给ret,而不是用临时对象赋值。赋值完后aa析构,然后调用Print,然后打印星号,最后再把ret析构。
可以说aa充当了临时对象。因为赋值后才析构的应该是临时对象。也就是说构造aa时不在f2()的栈帧里,否则出了作用域就销毁了。
也就是说只优化了传值返回时的拷贝构造。
我们知道,大部分场景传值传参我们都可以采用引用传参来避免拷贝造成的效率变低,但是对于传值返回来说,不是所有场景都能传引用返回的。所以编译器就会这样激进。
本文到此结束=_=