前言:类的6个默认成员函数,我们了解三个,讲完剩下的成员函数,其实类和对象的大致内容已经结束,最后我们在了解一些C++类和对象的剩下的的细节,我们就正式结束类和对象
如果你还对前面三个默认成员函数不太了解,建议先阅读这篇博客
类的成员函数
1. 运算符重载
运算符重载
在一个自定义变量里,如果我们想实现对它的加减乘除,是无法直接使用的,因此C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数
关键字operator 后面接需要重载的运算符符号
函数原型: 返回值类型 operator操作符(参数列表)
举个例子:
// 重载 == bool operator==(const Date& d) { return _year = d._year; && _month = d._month; && _day = d._day; }
注意:
- 重载操作符必须有一个自定义类型参数
- 运算符重载定义在类外时不能访问类中的私有成员,因此重载成成员函数
- 作为类成员函数重载时,成员函数的第一个参数为隐藏的this
赋值运算符重载
1. 关于赋值运算符重载:
- 参数类型:
const T&
,传递引用可以提高传参效率- 返回值类型:
T&
,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值- 检测是否自己给自己赋值
- 返回
*this
我们以下例子将使用日期类
例如:
class Date { public: Date() {} Date(int year, int month, int day) { _year = year; _month = month; _day = day; } void Print() { cout << _year << ' ' << _month << ' ' << _day << endl; } Date& operator=(const Date& d) { // 检查是否给直接赋值 if (this != &d) { _year = d._year; _month = d._month; _day = d._day; } return *this; } private: int _year; int _month; int _day; }; int main() { Date d1(2024, 5, 23); Date d2 = d1; // Date d2; // 实际上operator=的调用 // d2.operator=(d1); d1.Print(); d2.Print(); return 0; }
2. 赋值运算符只能重载成类的成员函数不能重载成全局函数
// 假设我们在类外面重载成全局函数 // 注意:在类外是没有 this 指针的 Date& operator=(Date& this, const Date& d) { if (&this != &d) { this._year = d._year; this._month = d._month; this._day = d._day; } return this; }
我们将写好的代码拿去运行一下,我们发现无法编译
其实,赋值运算符比较特殊如果不显式实现,编译器会生成一个默认的。如果在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了
3. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝
这里我们要格外注意:
系统默认生成一个默认赋值运算符重载它和之前的拷贝构造一样,发生的是浅拷贝,内置类型成员变量可以直接使用,而自定义类型成员变量需要我们自己调用对应类的赋值运算符重载
前置++和后置++重载
关于前置++和后置++:
- 前置++:返回+1之后的结果
- 后置++:是先使用后+1,因此需要返回+1之前的旧值
格式:
- 因为前置++和后置++符号一样,我们为了要想正确完成重载,C++规定,后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递
// 前置++ Date& operator++() { _day += 1; return *this } // 后置++ Date& operator++(int) { Date temp(*this); _day += 1; return temp; } // 前置-- Date& operator--() { _day -= 1; return *this } // 后置-- Date& operator--(int) { Date temp(*this); _day -= 1; return temp; }
最后补充一点,关于运算符重载,并不是所有的运算符都需要重载,而是要根据自定义的类需要重载哪些运算符!
注意以下运算符不能重载:
.*
::
sizeof
?:
.
讲到这里类和对象的大致内容已经结束,剩下两个成员函数,我们简单了解一下
2. 成员函数的补充
const成员
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改
例如:
class Date { public: Date(int year, int month, int day) { _year = year; _month = month; _day = day; } void Print() { cout << "Print()" << endl; cout << "year:" << _year << endl; cout << "month:" << _month << endl; cout << "day:" << _day << endl << endl; } void Print() const { cout << "Print()const" << endl; cout << "year:" << _year << endl; cout << "month:" << _month << endl; cout << "day:" << _day << endl << endl; } private: int _year; // 年 int _month; // 月 int _day; // 日 }; int main() { // 编译器会优先调用符合的函数,如果没有则会根据权限来调用 // 本质是:权限能缩小,但是不能放大 // 及非const对象可以调用const成员函数 // 非const成员函数内可以调用其它的const成员函数 Date d1(2024,5,23); d1.Print(); const Date d2(2024,5,23); d2.Print(); }
取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义 ,编译器默认会生成!
Date* operator&() { return this ; } const Date* operator&()const { return this ; }
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,我们能够修改别人获取的地址
3. 初始化列表
- 在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值
- 对象中有了一个初始值,因此构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值
初始化列表的概念
初始化列表: 以一个冒号开始
,接着是一个以逗号分隔的数据成员列表
,每个"成员变量"后面跟一个放在括号中的初始值或表达式
Date(int year, int month, int day) : _year(year) , _month(month) { _day = day } // 函数体里面能够放数据 //Date(int year, int month, int day) //{ // _year = year; // _month = month; // _day = day; //}
初始化列表的特征
使用初始化列表时注意:
- 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
- 类中包含以下成员,必须放在初始化列表位置进行初始化:
- 引用成员变量
- 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 }; int main() { B bb(1,2); A aa(1); }
特征:
1. 尽量使用初始化列表初始化
因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先经过初始化列表初始化
2. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
3. 当用户没有显示传参初始化时,编译器会用用户定义的缺省值
public: B(int a) :b(a) // b = 1; {} private: int b = 1;
explicit关键字
构造函数不仅能构造和初始化对象,对于单个参数或除第一个参数无默认值,其余均有默认值的构造函数,还有隐式类型转换的作用,隐式类型转换是在编程中编译器自动进行的一种类型转换方式
class pxt { public: explicit pxt(int a = 0) :_a(a) { cout << "pxt(int a)" << endl; } ~pxt() { cout << "~pxt()" << endl; } private: int _a; }; int main() { pxt a1 = 2024; // 用一个整形变量给自定义类型对象赋值 // 编译器会用2024构造一个无名对象,最后用无名对象给a1对象进行赋值 // 正常情景是能赋值的,但是explicit修饰构造函数后,会禁止构造函数的隐式转换 return 0; }
关键字explicit
修饰构造函数,将会禁止构造函数的隐式转换
4. static成员
static成员的概念
概念:
- 声明为
static的类成员
称为类的静态成员
,
用static
修饰的成员变量
,称之为静态成员变量
,
用static
修饰的成员函数
,称之为静态成员函数
- 静态成员变量一定要在
类外进行初始化
class pxt { public: void Print() { cout << _a << endl; } private: // 在类中声明 static int _a; //如果是静态成员函数,则没有this指针 }; // 在类外定义 int pxt:: _a = 100; int main() { pxt A; A.Print(); }
static成员的特征
特性:
静态成员为所有类对象所共享
,存放在静态区静态成员变量必须在类外定义
,类中只是声明- 类静态成员可用
类名::静态成员
或者对象.静态成员
来访问- 静态成员函数
没有隐藏的this指针
,不能访问任何非静态成员- 静态成员也是类的成员,受访问限定符的限制
5. 友元
友元: 提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元分为:
- 友元函数
- 友元类
友元函数
如果尝试去重载operator<<,我们发现没办法将operator<<重载成成员函数,因为函数的参数位置不一样,cout的输出流对象和隐含的this指针在抢占第一个参数的位置,
重载operator>>同理
d << cout; -> d.operator<<(&d, cout); 不符合常规调用
因为成员函数第一个参数一定是隐藏的this,所以d必须放在<<的左侧
但是问题来了,如果我们写成全局函数,又无法使用私有的成员变量,这时友元的作用就凸显出来了!
友元函数: 可以直接访问类的私有成员
,它是定义在类外部的普通函数
,不属于任何类,但需要在类的内部声明,声明时需要加friend
关键字
例如:
class Date { // 不声明友元,将无法调用私有成员 friend ostream& operator<<(ostream& _out, const Date& d); public: Date(int year, int month, int day) : _year(year) , _month(month) , _day(day) {} //void operator<< (ostream& _out) //{ // _out << _year << " " << _month << " " << _day; //} private: int _year; // 年 int _month; // 月 int _day; // 日 }; ostream& operator<< (ostream& _out, const Date& d) { _out << d._year << " " << d._month << " " << d._day; return _out; } int main() { Date d(2024,5,23); cout << d << endl; // d << cout; }
关于友元函数有以下几点:
- 友元函数可访问类的私有和保护成员,但
不是类的成员函数
- 友元函数
不能用const修饰
- 友元函数可以在类定义的任何地方声明,
不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用原理相同
友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员
友元类的特征:
友元关系是单向的,不具有交换性
友元关系不能传递
如果C是B的友元, B是A的友元,则不能说明C时A的友元
友元关系不能继承,在继承位置再给大家详细介绍
关于友元关系的单向性我举个例子:
class A { friend class B; public: // ...... private: int _year; int _month; int _day; }; class B { public: void test(int year, int month, int day) { // 直接访问A类私有的成员变量 // 但是A 不能访问B 中私有的成员变量 _d._year = year; _d._month = month; _d._day = day; } private: int good; A _d; };
B能直接访问A类私有的成员变量,但是A 不能访问B 中私有的成员变量
讲到友元类,我们再来介绍一下一个跟友元类有很大关系的内部类
内部类
概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限
注意:内部类就是外部类的友元类,内部类能访问外部类中的所有成员,反之则不能!
class A { public: // ...... A(int year = 2024, int month = 5, int day = 20) : _year(year) ,_month(month) ,_day(day) {} class B { public: void test(const A& _d) { cout << _d._year << " " << _d._month << " " << _d._day << endl; } }; private: int _year; int _month; int _day; }; int main() { A::B b; b.test(A()); }
内部类的特征
特性:
- 内部类可以定义在外部类的所有成员
- 内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名
- sizeof(外部类)=外部类,和内部类没有任何关系
6. 类的匿名对象
class pxt { public: pxt(int a = 0) :_a(a) { cout << "pxt(int a)" << endl; } ~pxt() { cout << "~pxt()" << endl; } private: int _a; }; int main() { // 但是我们可以这么定义匿名对象,匿名对象的特点不用取名字, // 但是他的生命周期只有这一行,我们可以看到下一行他就会自动调用析构函数 // 匿名对象 pxt(); // 隐式类型转换 pxt a1 = 2024; return 0; }
- 生命周期只有一行,会自动调用析构函数
- 匿名对象的特点不用取名字
因此当我们只是想使用类中的某一个函数时,我们能创建匿名对象!
7. 总结
类和对象的所有内容已经了解完毕,类和对象在整个C++上都有举足轻重的作用,大家千万不要忽视,而类和对象的重点在四个成员函数上,下节我将学习C++的内存管理
谢谢大家支持本篇到这里就结束了,祝大家天天开心!