【C++】类和对象(下):友元、static成员、内部类、explicit 和 匿名对象
文章目录
- 前言
- 一、友元
- 二、static成员
- 三、内部类
- 四、隐式类型转换(加explicit可以阻止隐式类型转换)
- 五、匿名对象
前言
一、友元(友元函数 和 友元类)
二、static成员(类中被static修饰的成员变量 和 成员函数)
三、内部类( 如果⼀个类定义在另⼀个类的内部,这个类就叫做内部类)
四、隐式类型转换(加explicit可以阻止隐式类型转换)
五、匿名对象
一、友元
友元提供了⼀种突破类访问限定符封装的方式,友元分为:友元函数和友元类,在函数声明或者类声明的前面加friend,并且把友元声明放到⼀个类的里面。
友元函数的特点:
• 外部友元函数可访问类的私有和保护成员,友元函数仅仅是⼀种声明,他不是类的成员函数。
• 友元函数可以在类定义的任何地方声明,不受类访问限定符限制。
• 一个函数可以是多个类的友元函数。
友元类的特点:
• 友元类中的成员函数都可以是另⼀个类的友元函数,都可以访问另⼀个类中的私有和保护成员。
• 友元类的关系是单向的,不具有交换性,比如声明了A类是B类的友元,但是B类不是A类的友元。
• 友元类关系不能传递,如果A是B的友元,B是C的友元,但是A不是C的友元。
友元有时能为我们提供便利,但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
(1)报错的示例(外部函数无法直接访问类对象的private部分):
#include<iostream>
using namespace std;
class A
{
private:
int _a1 = 5;
};
class B
{
private:
int _b1 = 3;
};
void func(const A& aa, const B& bb)
{
cout << aa._a1 << endl;
cout << bb._b1 << endl;
}
int main()
{
A aa;
B bb;
func(aa, bb);
return 0;
}
那么外部函数要如何才能访问类对象的private部分,有一个办法就是在类中声明这个外部函数是它的友元函数,外部友元函数可以访问类对象的私有和保护成员,如下:
#include<iostream>
using namespace std;
class B;// 前置声明,防止A中友元函数声明时,编译器不认识B
class A
{
// 友元函数声明
friend void func(const A& aa, const B& bb);
private:
int _a1 = 5;
};
class B
{
// 友元函数声明
friend void func(const A& aa, const B& bb);
private:
int _b1 = 3;
};
void func(const A& aa, const B& bb)
{
cout << aa._a1 << endl;
cout << bb._b1 << endl;
}
int main()
{
A aa;
B bb;
func(aa, bb);
return 0;
}
(2)示例(友元类中的成员函数都可以是另⼀个类的友元函数,都可以访问另⼀个类中的私有和保护成员。):
#include<iostream>
using namespace std;
class A
{
friend class B;// 友元类声明
private:
int _a1 = 1;
int _a2 = 2;
};
class B
{
public:
void func1(const A& aa)
{
cout << aa._a1 << endl;
cout << _b1 << endl;
}
void func2(const A& aa)
{
cout << aa._a2 << endl;
cout << _b2 << endl;
}
private:
int _b1 = 3;
int _b2 = 4;
};
int main()
{
A aa;
B bb;
bb.func1(aa);
bb.func2(aa);
return 0;
}
二、static成员
static成员指的是类中被static修饰的成员变量 和 成员函数。
static修饰的成员变量:
• 用static修饰的成员变量,称之为静态成员变量。静态成员变量⼀定要在类外进行初始化,且其初始化时不受访问限定符的限制。
• 静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区。
• 静态成员变量不能在声明位置给缺省值初始化,因为缺省值是个构造函数初始化列表的,静态成员变量不属于某个对象,不走构造函数初始化列表。
static修饰的成员函数:
• 用static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针。
• 静态成员函数中可以访问其他的静态成员,但是不能访问非静态的成员,因为没有this指针。
• 非静态的成员函数,可以访问任意的静态成员变量和静态成员函数。
• 静态成员也是类的成员,受public、protected、private 访问限定符的限制。
• 突破类域就可以访问静态成员(前提是静态成员被public限定),可以通过类名::静态成员 或者 对象.静态成员 来访问静态成员变量和静态成员函数。
(1)示例(static修饰的成员变量):
#include<iostream>
using namespace std;
class A
{
public:
private:
static int _scount;// 类里⾯声明
};
// 静态成员变量⼀定要在类外进行初始化,且其初始化时不受访问限定符的限制
int A::_scount = 0;
int main()
{
A a;
cout << sizeof(a) << endl;
// A类对象大小为1,证明静态成员变量不存在对象中
return 0;
}
(2)示例(static修饰的成员函数;突破类域就可以访问静态成员):
#include<iostream>
using namespace std;
class A
{
public:
static int GetACount()
{
return _scount++;// 静态成员函数中可以访问其他的静态成员
}
private:
static int _scount;// 类⾥⾯声明
};
// 静态成员变量⼀定要在类外进行初始化,且其初始化时不受访问限定符的限制
int A::_scount = 0;
int main()
{
cout << A::GetACount() << endl;
A a1;
cout << a1.GetACount() << endl;
// 突破类域就可以访问静态成员(前提是静态成员被public限定),
// 可以通过类名::静态成员 或者 对象.静态成员 来访问静态成员变量和静态成员函数。
return 0;
}
(3)示例(非静态的成员函数,可以访问任意的静态成员变量和静态成员函数):
// 实现⼀个类,能计算程序中创建出了多少个类对象?
#include<iostream>
using namespace std;
class A
{
public:
A()
{
++_scount;// 非静态的成员函数,可以访问任意的静态成员变量和静态成员函数
}
static int GetACount()
{
return _scount;
}
private:
static int _scount;
};
int A::_scount = 0;
int main()
{
cout << A::GetACount() << endl;
A a1;
cout << a1.GetACount() << endl;
A a2, a3;
cout << a2.GetACount() << endl;
return 0;
}
三、内部类
• 如果⼀个类定义在另⼀个类的内部,这个类就叫做内部类。内部类是⼀个独立的类,跟定义在全局相比,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。
• 内部类默认是外部类的友元类。
• 内部类本质也是⼀种封装,当A类跟B类紧密关联,A类实现出来就是给B类使用,那么可以考虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其他地方都用不了。
#include<iostream>
using namespace std;
class A
{
private:
static int _k;
int _h = 5;
public:
class B // 内部类默认是外部类的友元类,所以B默认就是A的友元
{
public:
void fun(const A& a)
{
cout << _k << endl;
cout << a._h << endl;
cout << _b << endl;
}
private:
int _b = 1;
};
};
int A::_k = 10;
int main()
{
A a;
// A类对象大小大小为4,所以外部类定义的对象中不包含内部类
cout << "A类对象大小:" << sizeof(a) << endl;
// 内部类受到外部类的类域和访问限定符限制,定义内部类对象要指定类域
A::B b;
b.fun(a);
return 0;
}
四、隐式类型转换(加explicit可以阻止隐式类型转换)
• C++支持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数。
• 类类型的对象之间也可以隐式转换,需要相应的构造函数支持
• 构造函数前面加explicit就不再支持隐式类型转换。
(1)示例(只要有相关内置类型为参数的构造函数,内置类型就可以隐式类型转换为类类型对象):
#include<iostream>
using namespace std;
class A
{
public:
A(int a1)// A类的单参数构造函数
:_a1(a1)
{}
A(int a1, int a2)// A类的双参数构造函数
:_a1(a1)
, _a2(a2)
{}
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
private:
int _a1 = 10;
int _a2 = 5;
};
int main()
{
// 1 被传给A类的单参数构造函数,来构造⼀个A的临时对象,再⽤这个临时对象拷⻉构造aa1
A aa1 = 1;
aa1.Print();
// { 2,2 } 被传给A类的双参数构造函数,来构造⼀个A的临时对象,再⽤这个临时对象拷⻉构造aa2
A aa2 = { 2,2 };
aa2.Print();
// 由于 1 构造的是⼀个A的临时对象,而临时对象是具有常属性的,
// 如果想要引用这个临时对象的话,必须使用const引用,使用普通引用会报错
const A& aa3 = 1;
return 0;
}
(2)示例(类类型的对象之间也可以隐式转换,但需要相应的构造函数支持):
#include<iostream>
using namespace std;
class A
{
public:
A(int a1)
:_a1(a1)
{}
int Get() const
{
return _a1 + _a2;
}
private:
int _a1 = 10;
int _a2 = 5;
};
class B
{
public:
B(const A& a)// B类的构造函数
:_b(a.Get())
{}
void Print()
{
cout << _b << endl;
}
private:
int _b = 0;
};
int main()
{
A aa1 = 2;
// aa1 被传给B类的构造函数,来构造⼀个B的临时对象,再⽤这个临时对象拷⻉构造bb1
B bb1 = aa1;
bb1.Print();
return 0;
}
(3)报错的示例(构造函数前面加explicit就不再支持隐式类型转换):
#include<iostream>
using namespace std;
class A
{
public:
// 构造函数前加explicit就不再⽀持隐式类型转换
explicit A(int a1)
:_a1(a1)
{}
int Get() const
{
return _a1 + _a2;
}
private:
int _a1 = 10;
int _a2 = 5;
};
class B
{
public:
// 构造函数前加explicit就不再⽀持隐式类型转换
explicit B(const A& a)
:_b(a.Get())
{}
void Print()
{
cout << _b << endl;
}
private:
int _b = 0;
};
int main()
{
A aa1 = 2;// A类构造函数不再支持这种隐式类型转换,直接报错
B bb1 = aa1;// B类构造函数不再支持这种隐式类型转换,直接报错
bb1.Print();
return 0;
}
explicit 关键字只阻止隐式类型转换,但不阻止显式类型转换。我们使用显式类型转换可以突破explicit 的限制,之前的那些转换过程照常进行:
#include<iostream>
using namespace std;
class A
{
public:
// 构造函数前加explicit就不再⽀持隐式类型转换
explicit A(int a1)
:_a1(a1)
{}
int Get() const
{
return _a1 + _a2;
}
private:
int _a1 = 10;
int _a2 = 5;
};
class B
{
public:
// 构造函数前加explicit就不再⽀持隐式类型转换
explicit B(const A& a)
:_b(a.Get())
{}
void Print()
{
cout << _b << endl;
}
private:
int _b = 0;
};
int main()
{
// 在 C++ 中,当一个构造函数被声明为 explicit,它阻止了编译器调用它进行隐式类型转换。
// 这里使用了显式类型转换 (A) 和 2 来创建A类的临时对象,再去拷贝构造aa1,这是合法的,
// 因为 explicit 关键字只阻止隐式类型转换,但不阻止显式类型转换.
A aa1 = (A)2;
B bb1 = (B)aa1;
bb1.Print();
return 0;
}
五、匿名对象
• 用类型(实参) 定义出来的对象叫做匿名对象,相比之前我们定义的 类型 对象名(实参) 定义出来的叫有名对象
• 匿名对象生命周期只在当前一行,一般临时定义一个对象当前用一下即可,就可以定义匿名对象。
(1)示例(匿名对象的定义及其生命周期):
#include<iostream>
using namespace std;
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
int main()
{
// A aa1(); 不能这么定义对象,因为编译器⽆法识别这是⼀个函数声明,还是对象定义
// 但是我们可以这么定义匿名对象,匿名对象的特点是不⽤取名字,
// 但是他的⽣命周期只有这⼀⾏,我们可以看到下⼀⾏他就会⾃动调⽤析构函数
A();
A(1);
A(2);
return 0;
}
(2)示例(引用匿名对象可以延长它的生命周期):
#include<iostream>
using namespace std;
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
int main()
{
// 匿名对象的⽣命周期只有这⼀⾏,我们可以看到下⼀⾏他就会⾃动调⽤析构函数
A();
// 我们可以引用匿名对象,不过必须要使用const引用,
// 因为匿名对象和临时变量类似,都具有常属性。
// 匿名对象被引用后,⽣命周期不再只有⼀⾏,它的生命周期跟引用其的aa一致
const A& aa = A(1);
A(2);
return 0;
}
(3)示例(匿名对象在创建的同时可以调用成员函数):
#include<iostream>
using namespace std;
class Solution
{
public:
int Sum_Solution(int n)
{
cout << n << endl;
return n;
}
};
int main()
{
Solution s1;
s1.Sum_Solution(10);
// 匿名对象在创建的同时可以调用成员函数
Solution().Sum_Solution(10);
return 0;
}