文章目录
- 1.友元函数和友元类的概念
- 1.1友元函数
- 1.2友元类
- 2.构造函数知识补充
- 2.1初始化列表
- 2.2explicit关键字
- 3.static成员
- 3.1static成员概念
- 3.2static成员特性
1.友元函数和友元类的概念
在C++中,友元函数和友元类是指允许非成员函数或非成员类访问某个类中的私有成员或保护成员的一种机制,友元函数和友元类都是友元,只是类型不同。具体来说:
友元函数: 是在类声明中通过 friend 关键字声明的非成员函数,该函数能够访问该类中被声明为私有成员或者保护成员的数据或函数。 友元函数可以是普通函数、成员函数、类的静态成员函数等,其实现视为平等于类成员函数。
友元类:是指 使用 friend 关键字声明的其他类,可以访问该类中被声明为私有成员或者保护成员的数据和方法。
友元就和我们生活中的朋友伙伴一样。🙋♂️🙋
朋友之间相互信任,同样,友元函数和友元类的作用是为某些具有特殊访问需求的函数或类提供访问私有成员的权限,从而更灵活地实现程序的设计。朋友之间要有一定的隐私,同样,使用友元机制可能会破坏封装性和安全性,因此需要谨慎使用,遵循良好的设计原则。
友元函数的举例:
class MyClass
{
int dataA;
friend int MyFunc(MyClass obj); // 声明类外的函数为友元函数
public:
MyClass(int a) : dataA(a) {};
};
int MyFunc(MyClass obj)
{
return obj.dataA; // 可以访问 MyClass 的私有成员 dataA
}
int main()
{
MyClass obj(123);
int result = MyFunc(obj);
cout << result << endl; // 输出 123
}
友元类的举例:
class A
{
int x;
friend class B; // 声明类 B 是 A 的友元类
public:
A(int a) : x(a) {}
};
class B
{
public:
void print(A obj)
{
cout << obj.x << endl; // 可以访问 A 的私有成员 x
}
};
int main()
{
A objA(10);
B objB;
objB.print(objA); // 输出 10
return 0;
}
1.1友元函数
友元函数的特点:
(1)友元函数可访问类的私有和保护成员,但不是类的成员函数;
(2)友元函数不能用const修饰;
(3)友元函数可以在类定义的任何地方声明,不受类访问限定符限制;
(4) 一个函数可以是多个类的友元函数;
(5) 友元函数的调用与普通函数的调用原理相同。
问题:现在尝试去重载operator<<,然后发现没办法将operator<<重载成成员函数。
因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以要将operator<<重载成全局函数。但又会导致类外没办法访问成员,此时就需要友元来解决。
class Date
{
//这里是初始化列表的内容,下面会有
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
// d1 << cout; -> d1.operator<<(&d1, cout); 不符合常规调用
// 因为成员函数第一个参数一定是隐藏的this,所以d1必须放在<<的左侧
ostream& operator<<(ostream& _cout)
{
_cout << _year << "-" << _month << "-" << _day << endl;
return _cout;
}
private:
int _year;
int _month;
int _day;
};
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
我们定义了一个友元输入运算符operator>>,它的作用是重载了输入流(比如cin)中提取对象的操作符“>>”,可以将用户在控制台中输入的日期信息提取到Date对象中。这里也使用了友元的机制来实现对私有成员变量的访问。
这个例子展示了如何使用重载输入输出运算符,在控制台中进行输入输出自定义类型对象,并且使用友元机制来实现对私有成员变量的访问。友元机制可以在某些情况下方便地实现对私有成员变量的访问而不暴露给外界。
class Date
{
friend ostream& operator<<(ostream& _cout, const Date& d);
friend istream& operator>>(istream& _cin, Date& d);
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
istream& operator>>(istream& _cin, Date& d)
{
_cin >> d._year;
_cin >> d._month;
_cin >> d._day;
return _cin;
}
int main()
{
Date d;
cin >> d;
cout << d << endl;
return 0;
}
1.2友元类
友元类的特点:
(1)友元关系是单向的,不具有交换性;
(2)友元关系不能传递;
(3)友元关系不能继承。
总结:友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
我们定义了两个类Date和Time,其中Time类表示时间,Date类表示日期。在Time类的定义中,通过使用friend关键字声明Date类为Time类的友元类。这个声明的作用是,允许在Date类中直接访问Time类的私有成员变量。
Date类有一个名为SetTimeOfDate的公有成员函数,用于设置日期的时分秒信息。在SetTimeOfDate函数的实现中可以直接访问Time类的私有成员变量来设置时间的时分秒信息。
这个例子展示了如何在一个类中声明另一个类为友元类,使得友元类能够访问当前类中的私有成员,并且提供了一种简便的方法来完成复杂操作。同时需要注意的是,友元机制在一定程度上破坏了封装性,建议在实际编程中慎重使用,遵从信息隐蔽、分离的设计原则,保证程序的可维护性和可扩展性。
class Time
{
friend class Date; // 声明日期类为时间类的友元类,
//则在日期类中就直接访问Time类中的私有成员变量
public:
Time(int hour = 0, int minute = 0, int second = 0)
: _hour(hour)
_minute(minute)
, _second(second)
{}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
void SetTimeOfDate(int hour, int minute, int second)
{
// 直接访问时间类私有的成员变量
_t._hour = hour;
_t._minute = minute;
_t._second = second;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
2.构造函数知识补充
2.1初始化列表
初始化列表(Initialization list)是在创建对象时初始化成员变量的一种方式。在C++中,构造函数可以通过初始化列表来指定构造对象时的初值。初始化列表用一个冒号分隔,列出每个成员变量和初始值。
语法格式为:
class MyClass
{
public:
MyClass(int x, int y, int z)
: _x(x)
, _y(y)
, _z(z)
{}
private:
int _x;
int _y;
int _z;
};
在上面的代码示例中,MyClass类的构造函数需要三个参数,分别是x,y,z,并且每个参数都被用来初始化成员变量_x,_y,_z。构造函数通过初始化列表的方式将参数值赋给成员变量,这种写法比在构造函数体中显式地进行赋值更为高效,尤其在成员变量具有类类型时表现更为明显。
初始化列表总结:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
这个不就是显示赋值吗?为什么要弄一个初始化列表?
使用初始化列表和在构造函数中显式赋值的主要区别在于效率。
当一个类的成员变量本身是一个类对象时,在构造函数中对其进行赋值是需要先构造临时对象,然后再调用复制构造函数将复制这个临时对象到成员变量中。而使用初始化列表则可以直接在成员变量构造函数时调用复制构造函数,减少了临时对象的创建和销毁操作,提高代码效率。
此外,在使用初始化列表的情况下,成员变量的初始化顺序和它们在类中的声明顺序是一致的,而在构造函数体内部显式赋值的情况下,成员变量的初始化顺序则与其在初始化列表中的顺序无关。这在一些情况下可能会影响程序的正确性和性能。因此,在编写构造函数时尽可能使用初始化列表是一种提高程序效率和可读性的好习惯。
初始化列表需要注意:
(1)每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
(2)类中包含以下成员,必须放在初始化列表位置进行初始化:
1.引用成员变量
2.const成员变量
3.自定义类型成员(且该类没有默认构造函数时)
我们定义了两个类A和B,其中A类有一个整型成员变量_a,构造函数可以用传入的参数a为_a初始化。
B类有三个成员变量:一个A类对象_aobj,一个整型的引用变量_ref和一个const常量整数_n,分别通过初始化列表进行初始化。注意到A类没有默认构造函数,所以在B类中需要显式地传入一个int类型的参数用于初始化_aobj。
引用变量_ref必须在初始化列表中进行初始化,因为引用是一种没有独立地址的别名类型,必须在创建时初始化,不能后期再初始化。同时_n是一个const常量成员变量,也必须通过初始化列表进行初始化,因为const常量成员变量的值不能在构造函数体中被修改。
总之,这段代码演示了如何使用初始化列表对类的成员变量进行初始化,避免了在构造函数体中进行显式赋值的低效率问题,并且介绍了引用和const常量成员变量的初始化方法。
class A
{
public:
A(int a)
:_a(a)
{}
private:
int _a;
};
class B
{
public:
B(int a, int ref)
:_aobj(a)
,_ref(ref)
,_n(10)
{}
private:
A _aobj; // 没有默认构造函数
int& _ref; // 引用
const int _n; // const
};
(3)尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
(4)成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
class A
{
public:
A(int a)
:_a1(a)
,_a2(_a1)
{}
void Print()
{
cout<<_a1<<" "<<_a2<<endl;
}
private:
int _a2;
int _a1;
};
int main()
{
A aa(1);
aa.Print();
}
//成员变量在类中声明次序就是其在初始化列表中的初始化顺序,
//与其在初始化列表中的先后次序无关。
//因此,在上述程序中,_a2会比_a1先进行初始化,因此输出的结果应该是:
//1和随机值。
2.2explicit关键字
explicit是C++中的一个关键字,用于修饰单参数构造函数。它的作用是防止编译器进行隐式类型转换,只允许显式调用构造函数完成对象的初始化。
我们定义一个类Person,用于表示一个人员的姓名和年龄等信息。我们可以使用以下代码定义这个类:
class Person
{
public:
Person(const std::string& name, int age)
: m_name(name)
, m_age(age)
{}
// 使用 explicit 关键字禁止隐式类型转换
explicit Person(int age)
: m_name("Unknown")
, m_age(age) {}
std::string getName() const
{
return m_name;
}
int getAge() const
{
return m_age;
}
private:
std::string m_name;
int m_age;
};
上述代码中,我们定义了两个构造函数。第一个构造函数接受一个姓名和年龄,用于初始化一个Person对象。第二个构造函数只接受年龄作为参数,并将姓名的默认值设置为"Unknown"。最重要的是,我们将第二个构造函数声明为explicit,以避免隐式类型转换。
现在,我们可以在程序中使用这两个构造函数来创建Person对象,在 main() 函数中,我们首先用姓名和年龄初始化一个Person对象 p1,然后使用年龄初始化一个Person对象 p2,并将姓名默认设为"Unknown"。最后,我们注释掉了隐式类型转换的语句。如下所示:
int main()
{
// 直接使用姓名和年龄初始化一个Person对象
Person p1("Tom", 32);
std::cout << p1.getName() << " is " << p1.getAge() << " years old.\n";
// 输出 "Tom is 32 years old."
// 使用年龄初始化一个Person对象
Person p2(25);
std::cout << p2.getName() << " is " << p2.getAge() << " years old.\n";
// 输出 "Unknown is 25 years old."
// 隐式类型转换会导致编译错误
// Person p3 = 28;
return 0;
}
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。
3.static成员
3.1static成员概念
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化。
在上面的代码中,我们使用了一个静态局部变量count来统计函数foo()被调用的次数。由于它是静态变量,会在程序启动时被分配内存,只会被初始化一次,并且在每次函数调用时都会保留上次计数的结果。因此,每次调用该函数都会输出不同的计数结果。
另外,需要注意的是静态变量的作用域只限于包含它的函数,其他函数无法直接访问它。因此,我们可以使用静态变量来保持函数内部的数据并防止它们被意外地修改或破坏。
void foo() {
static int count = 0; // 声明一个静态局部变量
count++;
std::cout << "This function has been called " << count << " times." << std::endl;
}
int main() {
foo(); // 输出 "This function has been called 1 times."
foo(); // 输出 "This function has been called 2 times."
foo(); // 输出 "This function has been called 3 times."
return 0;
}
3.2static成员特性
(1)静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区;
(2)静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明;
(3) 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问;
(4) 静态成员函数没有隐藏的this指针,不能访问任何非静态成员;
(5) 静态成员也是类的成员,受public、protected、private 访问限定符的限制。
这些就是C++中类和对象中友元和初始化列表等的简单介绍了😉
如有错误❌望指正,最后祝大家学习进步✊天天开心✨🎉