🌈个人主页:秋风起,再归来~
🔥系列专栏: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上编译器优化为直接构造a1
A 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、完结散花
好了,这期的分享到这里就结束了~
如果这篇博客对你有帮助的话,可以用你们的小手指点一个免费的赞并收藏起来哟~
如果期待博主下期内容的话,可以点点关注,避免找不到我了呢~
我们下期不见不散~~