🌈个人主页:秋风起,再归来~
🔥系列专栏:C++从入门到起飞
🔖克心守己,律己则安
目录
1、友元
2、内部类
3、 匿名对象
4、对象拷⻉时的编译器优化
5、完结散花
1、友元
• 友元提供了⼀种突破类访问限定符封装的⽅式,友元分为:友元函数和友元类,在函数声明或者类 声明的前⾯加friend,并且把友元声明放到⼀个类的⾥⾯。
友元类:
class A
{
public://B是A的友元类friend class B;
private:int _a1 = 1;int _a2 = 2;
};class B
{
public:B(){//......}void func(const A& aa){//访问A的私有成员cout << aa._a1 << endl;cout << aa._a2 << endl;}
private:int _b1 = 3;int _b2 = 4;
};int main()
{A aa1;B bb1;bb1.func(aa1);return 0;
}
友元函数:
class A
{
public://B是A的友元类(友元声明)friend class B;//func是A类的友元函数(友元声明)friend void func(const A& aa);
private:int _a1 = 1;int _a2 = 2;
};void func(const A& aa)
{cout << aa._a1 << endl;cout << aa._a2 << endl;
}
• 外部友元函数可访问类的私有和保护成员,友元函数仅仅是⼀种声明,他不是类的成员函数。
• 友元函数可以在类定义的任何地⽅声明,不受类访问限定符限制。
• ⼀个函数可以是多个类的友元函数。
// 前置声明,不然A的友元函数声明编译器不认识B
class B;class A
{
public://B是A的友元类(友元声明)friend class B;//func是A类的友元函数(友元声明)friend void func(const A& aa,const B& bb);
private:int _a1 = 1;int _a2 = 2;
};class B
{
public: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 << aa._a2 << endl;cout << bb._b1 << endl;cout << bb._b2 << endl;
}int main()
{A aa1;B bb1;func(aa1,bb1);return 0;
}
• 友元类中的成员函数都可以是另⼀个类的友元函数,都可以访问另⼀个类中的私有和保护成员。 • 友元类的关系是单向的,不具有交换性,⽐如A类是B类的友元(A可以访问B的私有或保护成员,但B不可以),但是B类不是A类的友元。
• 友元类关系不能传递,如果A是B的友元,B是C的友元,但是A不是B的友元。
• 有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多⽤。
2、内部类
• 如果⼀个类定义在另⼀个类的内部,这个内部类就叫做内部类。内部类是⼀个独⽴的类,跟定义在 全局相⽐,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。
• 内部类默认是外部类的友元类(即内部类可以访问外部类的私有和保护成员)。
class A
{
public://内部类class B//默认是A的友元类{public:void func(A& aa){A::a++;aa.b++;//访问A类的私有成员}private:int b1 = 1;int b2 = 2;};
private:static int a;int b = 1;
};int main()
{A a;cout << sizeof(a) << endl;return 0;
}
a对象的大小是4说明B类是一个独立的类 ,外部类定义的对象中不包含内部类!
• 内部类本质也是⼀种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使⽤,那么可以考 虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其 他地⽅都⽤不了。
上篇文章的OJ题就可以用内部类来进行封装!
求1+2+3+...+n_⽜客题霸_⽜客⽹
描述
求1+2+3+...+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。
数据范围:0<n≤200
进阶: 空间复杂度 O(1) ,时间复杂度 O(n)示例1
输入:
5复制返回值:
15示例2
输入:
1复制返回值:
1OJ链接
class Solution
{class Sum{public:Sum(){ret+=i;++i;}};static int i;static int ret;
public:int Sum_Solution(int n){//Sum a[n];变长数组//创建n个对象来调用n次构造函数Sum* p=new Sum[n];return ret;}};
//用静态的成员变量来记录结果
int Solution::i=1;
int Solution::ret=0;
3、 匿名对象
• ⽤类型(实参)定义出来的对象叫做匿名对象,相⽐之前我们定义的类型对象名(实参)定义出来的 叫有名对象
• 匿名对象⽣命周期只在当前⼀⾏,⼀般临时定义⼀个对象当前⽤⼀下即可,就可以定义匿名对象。
class A
{};int main()
{A();//定义的匿名对象,生命周期只存在当前这一行
}
class A
{
public:A(int a = 0):_a(a){cout << "A(int a)" << endl;}~A(){cout << "~A()" << endl;}
private:int _a;
};
class Solution {
public:int Sum_Solution(int n) {//...return n;}
};
int main()
{A aa1;// 不能这么定义对象,因为编译器⽆法识别下⾯是⼀个函数声明,还是对象定义//A aa1();// 但是我们可以这么定义匿名对象,匿名对象的特点不⽤取名字,// 但是他的⽣命周期只有这⼀⾏,我们可以看到下⼀⾏他就会⾃动调⽤析构函数A();A(1);A aa2(2);// 匿名对象在这样场景下就很好⽤,当然还有⼀些其他使⽤场景,这个我们以后遇到了再说Solution().Sum_Solution(10);return 0;
}
4、对象拷⻉时的编译器优化
• 现代编译器会为了尽可能提⾼程序的效率,在不影响正确性的情况下会尽可能减少⼀些传参和传参 过程中可以省略的拷⻉。
• 如何优化C++标准并没有严格规定,各个编译器会根据情况⾃⾏处理。当前主流的相对新⼀点的编 译器对于连续⼀个表达式步骤中的连续拷⻉会进⾏合并优化,有些更新更"激进"的编译还会进⾏跨 ⾏跨表达式的合并优化。
class A
{
public:A(int a=0){cout << "调用构造!" << endl;}A(const A& a){cout << "调用拷贝构造!" << endl;}~A(){cout << "调用析构!" << endl;}
private:int _a;
};int main()
{//原来是先用1调用构造创建一个临时对象,再用临时对象拷贝构造对象a1//在VS2022上编译器优化为直接构造a1A a1 = 1;return 0;
}
原来是先用1调用构造创建一个临时对象,再用临时对象拷贝构造对象a1,在VS2022上编译器优化为直接构造a1!
跨 ⾏跨表达式的没有优化:
class A
{
public:A(int a=0){cout << "调用构造!" << endl;}A(const A& a){cout << "调用拷贝构造!" << endl;}~A(){cout << "调用析构!" << endl;}
private:int _a;
};void f(A aa)
{//......
}int main()
{A a(1);//构造f(a);//传值传参,拷贝构造return 0;
}
构造与拷贝构造并没有在一个连续的步骤当中,所以编译器并没有进行优化!
匿名对象触发优化&隐式类型转换触发优化:
int main()
{//A a(1);//构造//f(a);//传值传参,拷贝构造f(A(1));//匿名对象触发优化cout << endl;f(1);//隐式类型转换触发优化return 0;
}
传值返回进行优化:
class A
{
public:A(int a=0){_a = a;cout << "调用构造!" << endl;}A(const A& a){cout << "调用拷贝构造!" << endl;}~A(){cout << "调用析构!" << endl;}void Print(){cout << "_a->" << _a << endl;}
private:int _a;
};void f(A aa)
{//......
}//传值返回
A f2()
{A aa(1);//调用构造return aa;//调用拷贝构造创建临时对象
}int main()
{f2().Print();return 0;
}
在VS2019debug模式下,f2函数体内,先调用构造函数初始化aa, 再用aa调用拷贝构造创建临时对象作为返回值,函数调用结束后,aa生命周期结束,调用析构函数。临时对象作为返回值其生命周期在当前一行,调用Print函数后,生命周期结束,再次调用析构。
而在在VS2022debug模式下,其优化更为激进,编译器并没有构造aa,而是直接构造临时对象。为什么是没有构造aa呢?原因就在于析构函数的调用是在Print函数调用之后才调用的!
既然编译器敢这么优化,就不怕出bug吗?下面我们来写一个程序测试一下!
class A
{
public:A(int a=0){_a = a;cout << "调用构造!" << endl;}A(const A& a){cout << "调用拷贝构造!" << endl;}~A(){cout << "调用析构!" << endl;}void Print(){cout << "_a->" << _a << endl;}//这里重载一个前置++A& operator++(){_a += 100;return *this;}
private:int _a;
};void f(A aa)
{//......
}//传值返回
A f2()
{A aa(1);//调用构造++aa;//如果编译器还像之前那样优化,并且打印的_a是101,就算他牛逼!return aa;//调用拷贝构造创建临时对象
}int main()
{f2().Print();return 0;
}
这里没有构造aa,却在构造临时对象时根据程序员的想法,对_a进行了++操作,我们可以看到编译器敢这么优化,他还是敢作敢当的!
5、完结散花
好了,这期的分享到这里就结束了~
如果这篇博客对你有帮助的话,可以用你们的小手指点一个免费的赞并收藏起来哟~
如果期待博主下期内容的话,可以点点关注,避免找不到我了呢~
我们下期不见不散~~