C++类与对象—下:夯实面向对象编程的阶梯
9. 赋值运算符重载
9.1 运算符重载
在 C++ 里,运算符重载能够让自定义类型的对象像内置类型那样使用运算符,这极大地提升了代码的可读性与可维护性。运算符重载本质上是一种特殊的函数,其函数名是 operator
加上要重载的运算符。
下面是运算符重载的一般语法:
返回类型 operator 运算符 (参数列表) {// 函数体
}
运算符重载有一些规则和限制:
-
不能创建新的运算符,只能重载已有的运算符。
-
不改变运算符的优先级和结合性。
-
至少有一个操作数是自定义类型。重载操作符至少有⼀个类类型参数,不能通过运算符重载改变内置类型对象的含义,如:
int operator+(int x, int y)
-
.* :: sizeof ?: . 注意以上5个运算符不能重载。
-
一个类需要重载哪些运算符,是看哪些运算符重载后有意义,比如Date类重载
operator-
就有意义,但是重载operator+
就没有意义。 -
重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,无法很好的区分。C++规定,后置++重载时,增加一个int形参,跟前置++构成函数重载,方便区分。
-
重载<<和>>时,需要重载为全局函数,因为重载为成员函数,this指针默认抢占了第⼀个形参位置,第⼀个形参位置是左侧运算对象,调用时就变成了
对象<<cout
,不符合使用习惯和可读性。重载为全局函数把ostream/istream放到第⼀个形参位置就可以了,第二个形参位置当类类型对象。
#include<iostream>using namespace std;// 编译报错:“operator +”必须⾄少有⼀个类类型的形参
int operator+(int x, int y){return x - y;}class A{public:void func(){cout << "A::func()" << endl;}
};typedef void(A::*PF)(); //成员函数指针类型int main(){// C++规定成员函数要加&才能取到函数指针PF pf = &A::func;A obj;//定义ob类对象temp // 对象调⽤成员函数指针时,使⽤.*运算符(obj.*pf)();return 0;}
9.2 赋值运算符重载
赋值运算符重载是运算符重载的一种特殊情况,它允许我们自定义对象之间的赋值行为。赋值运算符的函数名是 operator=
。
下面是赋值运算符重载的一般语法:
类名& operator=(const 类名& other) {if (this != &other) {// 执行赋值操作}return *this;
}
这里需要注意几个要点:
- 返回值类型通常是当前类类型引用
类名&
,引用返回可以提高效率,这样也可以支持连续赋值,例如a = b = c
。 - 参数通常是
const 类名&
,这避免了不必要的拷贝(传值传参会有拷贝),同时保证不会修改传入的对象。 - 有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值目的是为了支持连续赋值场景。
- 没有显式实现时,编译器会自动生成一个默认赋值运算符重载,默认赋值运算符重载行为跟默认构造函数类似,对内置类型成员变量会完成值拷贝/浅拷贝(⼀个字节⼀个字节的拷贝),对自定义类型成员变量会调用他的拷贝构造。
class Date
{public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}Date(const Date& d){cout << " Date(const Date& d)" << endl;_year = d._year;_month = d._month;_day = d._day;}// 传引⽤返回减少拷⻉// d1 = d2;Date& operator=(const Date& d){// 不要检查⾃⼰给⾃⼰赋值的情况if (this != &d){_year = d._year;_month = d._month;_day = d._day;}// d1 = d2表达式的返回对象应该为d1,也就是*this return *this;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;};int main(){Date d1(2024, 7, 5);Date d2(d1);Date d3(2024, 7, 6);d1 = d3;// 需要注意这⾥是拷⻉构造,不是赋值重载// 请牢牢记住赋值重载完成两个已经存在的对象直接的拷⻉赋值// ⽽拷⻉构造⽤于⼀个对象拷⻉初始化给另⼀个要创建的对象Date d4 = d1;return 0;}
9.3 日期类的实现
下面我们来实现一个日期类 Date
,并对运算符进行重载:
#pragma once#include<iostream>using namespace std;#include<assert.h>class Date{// 友元函数声明 我们下面马上就会说friend ostream& operator<<(ostream& out, const Date& d);friend istream& operator>>(istream& in, Date& d);public:Date(int year = 1900, int month = 1, int day = 1);void Print() const;// 直接定义类里⾯,他默认是
inline // 频繁调⽤int GetMonthDay(int year, int month){assert(month > 0 && month < 13);static int monthDayArray[13] = { -1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };// 365天 5h + if (month == 2 && (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)){ return 29;}else{return monthDayArray[month];}}bool CheckDate();bool operator<(const Date& d) const;bool operator<=(const Date& d) const;bool operator>(const Date& d) const;bool operator>=(const Date& d) const;bool operator==(const Date& d) const;bool operator!=(const Date& d) const;// d1 += 天数Date& operator+=(int day);Date operator+(int day) const;// d1 -= 天数Date& operator-=(int day);Date operator-(int day) const;// d1 - d2int operator-(const Date& d) const;// ++d1 -> d1.operator++()Date& operator++();// d1++ -> d1.operator++(0)// 为了区分,构成重载,给后置++,强⾏增加了⼀个int形参// 这⾥不需要写形参名,因为接收值是多少不重要,也不需要⽤// 这个参数仅仅是为了跟前置++构成重载区分Date operator++(int);Date& operator--();Date operator--(int);// 流插⼊// 不建议,因为Date* this占据了⼀个参数位置,使⽤d<<cout不符合习惯//void operator<<(ostream& out);private:int _year;int _month;int _day;};// 重载ostream& operator<<(ostream& out, const Date& d);istream& operator>>(istream& in, Date& d);// Date.cpp#include"Date.h"bool Date::CheckDate(){if (_month < 1 || _month > 12|| _day < 1 || _day > GetMonthDay(_year, _month)){return false;}else{return true;}}Date::Date(int year, int month, int day){_year = year;_month = month;_day = day;if (!CheckDate()){cout << "⽇期⾮法" << endl;}}void Date::Print() const{cout << _year << "-" << _month << "-" << _day << endl;}// d1 < d2bool Date::operator<(const Date& d) const{if (_year < d._year){return true;}else if (_year == d._year){if (_month < d._month){return true;}else if (_month == d._month){return _day < d._day;}}return false;}// d1 <= d2bool Date::operator<=(const Date& d) const{return *this < d || *this == d;}bool Date::operator>(const Date& d) const{return !(*this <= d);}bool Date::operator>=(const Date& d) const{return !(*this < d);}bool Date::operator==(const Date& d) const{return _year == d._year&& _month == d._month&& _day == d._day;}bool Date::operator!=(const Date& d) const{return !(*this == d);}// d1 += 50// d1 += -50Date& Date::operator+=(int day){if (day < 0){return *this -= -day;}_day += day;while (_day > GetMonthDay(_year, _month)){_day -= GetMonthDay(_year, _month);++_month;if (_month == 13){++_year;_month = 1;}}return *this;}Date Date::operator+(int day) const{Date tmp = *this;tmp += day;return tmp;}// d1 -= 100Date& Date::operator-=(int day)
{if (day < 0){return *this += -day;}_day -= day;while (_day <= 0){--_month;if (_month == 0){_month = 12;_year--;}// 借上⼀个⽉的天数_day += GetMonthDay(_year, _month);}return *this;}Date Date::operator-(int day) const{Date tmp = *this;tmp -= day;return tmp;}//++d1Date& Date::operator++(){*this += 1;return *this;}// d1++Date Date::operator++(int){Date tmp(*this);*this += 1;return tmp;}Date& Date::operator--(){*this -= 1;return *this;}Date Date::operator--(int){Date tmp = *this;*this -= 1;return tmp;}// d1 - d2int Date::operator-(const Date& d) const{Date max = *this;Date min = d;int flag = 1;if (*this < d){max = d;min = *this;flag = -1;}int n = 0;while (min != max){++min;++n;}return n * flag;}ostream& operator<<(ostream& out, const Date& d){out << d._year << "年" << d._month << "月" << d._day << "日" << endl;return out;}istream& operator>>(istream& in, Date& d){cout << "请依次输⼊年月日:>";in >> d._year >> d._month >> d._day;if (!d.CheckDate()){cout << "日期⾮法" << endl;}return in;}
大家可以自写测试函数进行自行测试,再此我不过多赘述
10. 取地址运算符重载
10.1 const 成员函数
在 C++ 中,const
成员函数是指那些不会修改对象状态的成员函数。在函数声明和定义时,在参数列表后面加上 const
关键字,就可以将该函数声明为 const
成员函数。
下面是 const
成员函数的一般语法:
返回类型 函数名(参数列表) const {// 函数体
}
const
成员函数有以下几个特点:
- 只能调用其他的
const
成员函数。 const
实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进⾏修改。const 修饰Date类的Print成员函数,Print隐含的this指针由Date* const this
变为const Date* const this
以下是一个示例:
#include<iostream>using namespace std;class Date{
public:public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// void Print(const Date* const this) constvoid Print() const{cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;};int main(){// 这里非const对象也可以调⽤const成员函数是一种权限的缩⼩Date d1(2024, 7, 5);d1.Print();const Date d2(2024, 8, 5);d2.Print();return 0;}
在上述代码中,getData
函数被声明为 const
成员函数,因此可以被 const
对象调用。
10.2 取地址运算符重载
取地址运算符重载分为普通取地址运算符重载和const
取地址运算符重载,⼀般这两个函数编译器自动生成的就可以够我们用了,不需要去显示实现。除非⼀些很特殊的场景,比如我们不想让别人取到当前类对象的地址,就可以自己实现⼀份,胡乱返回一个地址。
class Date{
public :Date* operator&(){return this;// return nullptr;}const Date* operator&()const{return this;// return nullptr;}private :int _year ; // 年int _month ; // ⽉int _day ; // ⽇};
11. 友元
11.1 友元的概念
在 C++ 中,封装性是面向对象编程的重要特性之一,它通过访问限定符(public
、private
、protected
)来控制类的成员的访问权限。然而,在某些特殊情况下,我们可能需要让一个外部的函数或类能够访问某个类的私有成员。这时,就可以使用友元机制。友元机制提供了一种突破封装性的方式,允许特定的外部函数或类访问另一个类的私有和保护成员。
11.2 友元函数
友元函数是一种在类外部定义的普通函数,但通过在类内部使用 friend
关键字进行声明,使得该函数可以访问类的私有和保护成员。
示例代码:
#include <iostream>
class Point {
private:int x;int y;
public:Point(int x = 0, int y = 0) : x(x), y(y) {}// 声明友元函数friend void printPoint(const Point& p);
};// 友元函数的定义
void printPoint(const Point& p) {std::cout << "x: " << p.x << ", y: " << p.y << std::endl;
}int main() {Point p(3, 4);printPoint(p);return 0;
}
在上述代码中,printPoint
函数是 Point
类的友元函数,因此它可以直接访问 Point
类的私有成员 x
和 y
。
11.3 友元类
友元类是指一个类可以访问另一个类的私有和保护成员。在一个类中使用 friend
关键字声明另一个类为友元类,那么这个友元类的所有成员函数都可以访问该类的私有和保护成员。
示例代码:
#include <iostream>
class A {
private:int data;
public:A(int d = 0) : data(d) {}// 声明 B 为 A 的友元类friend class B;
};class B {
public:void accessA(A& a) {std::cout << "Accessing A's data: " << a.data << std::endl;}
};int main() {A a(10);B b;b.accessA(a);return 0;
}
在这个例子中,B
是 A
的友元类,所以 B
类的成员函数 accessA
可以访问 A
类的私有成员 data
。
11.4 友元的注意事项
- 友元关系是单向的,即如果
A
是B
的友元,并不意味着B
是A
的友元。 - 友元关系不具有传递性,即如果
A
是B
的友元,B
是C
的友元,并不意味着A
是C
的友元。 - 友元破坏了类的封装性,增加耦合度,使用时需要谨慎(不宜多用)。
12. 类型转换&&static成员
12.1 类型转换
C++支持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数、
构造函数前面加explicit
就不再支持隐式类型转换
比如:
#include<iostream>
using namespace std;
class A
{
public:// 构造函数explicit就不再⽀持隐式类型转换// explicit A(int a1)A(int a1):_a1(a1){}//explicit A(int a1, int a2)A(int a1, int a2):_a1(a1),_a2(a2){}void Print(){cout << _a1 << " " << _a2 << endl;}
private:int _a1 = 1;int _a2 = 2;
};
int main()
{// 1构造⼀个A的临时对象,再⽤这个临时对象拷⻉构造aa3// 编译器遇到连续构造+拷⻉构造->优化为直接构造A aa1 = 1;aa1.Print();const A& aa2 = 1;// C++11之后才⽀持多参数转化A aa3 = { 2,2 };return 0;
}
12.2 static成员
直接给出注意的点
- 用static修饰的成员变量,称之为静态成员变量,静态成员变量⼀定要在类外进行初始化。
- 静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区。
用static
修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针。 - 静态成员函数中可以访问其他的静态成员,但是不能访问非静态的,因为没有this指针。
- 非静态的成员函数,可以访问任意的静态成员变量和静态成员函数。
- 突破类域就可以访问静态成员,可以通过
类名::静态成员
或对象.静态成员
来访问静态成员变量和静态成员函数。 - 静态成员也是类的成员,受
public、protected、private
访问限定符的限制。 - 静态成员变量不能在声明位置给缺省值初始化,因为缺省值是个构造函数初始化列表的,静态成员变量不属于某个对象,不走构造函数初始化列表。
// 实现⼀个类,计算程序中创建出了多少个类对象?
#include<iostream>
using namespace std;
class A
{
public:A(){++_scount;} A(const A& t){++_scount;} ~A(){--_scount;} static int GetACount(){return _scount;}
private:// 类⾥⾯声明static int _scount;
};
// 类外⾯初始化
int A::_scount = 0;int main()
{cout << A::GetACount() << endl;A a1, a2;A a3(a1);cout << A::GetACount() << endl;cout << a1.GetACount() << endl;// 编译报错:error C2248: “A::_scount”: ⽆法访问 private 成员(在“A”类中声明)//cout << A::_scount << endl;return 0;
}
13. 内部类
如果⼀个类定义在另⼀个类的内部,这个内部类就叫做内部类。内部类是⼀个独立的类,跟定义在全局相比,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。
- 内部类默认是外部类的友元类。
- 内部类本质也是⼀种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使用,那么可以考虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其他地方都用不了。
#include<iostream>
using namespace std;
class A
{
private:static int _k;int _h = 1;
public:class B // B默认就是A的友元{ public:void foo(const A& a){cout << _k << endl; //OKcout << a._h << endl; //OK}};
};
int A::_k = 1;int main()
{cout << sizeof(A) << endl;A::B b;A aa;b.foo(aa);return 0;
}
这个题相信大家都不陌生,然而如果不让你使用循环你该怎么做呢???
求1+2+3+…+n_牛客
class Solution {private:static int ret;static int i;class Sum{public:Sum(){ret+=i;++i;}};
public:int Sum_Solution(int n) {Sum a[n];return ret;}
};
int Solution::ret=0;
int Solution::i=1;
是不是使用内部类的时候初始化直接求出来了哦,你就说这个方法巧不巧妙
14. 匿名对象
匿名对象是指没有名字的对象,它通常在创建后立即使用,使用完后就会被销毁。匿名对象可以用于临时传递参数或者调用函数。
匿名对象生命周期只在当前一行,一般临时定义一个对象当前用一下即可,就可以定义匿名对象。
比如:
#include <iostream>
class Test {
public:Test() {std::cout << "Constructor called" << std::endl;}~Test() {std::cout << "Destructor called" << std::endl;}void print() {std::cout << "Print function called" << std::endl;}
};int main() {// 创建匿名对象并调用 print 函数Test().print();//使用完后立即销毁return 0;
}
15. 对象拷贝时的编译器优化 *
- 现代编译器会为了尽可能提高程序的效率,在不影响正确性的情况下会尽可能减少一些传参和传参过程中可以省略的拷贝。
- 如何优化C++标准并没有严格规定,各个编译器会根据情况自行处理。当前主流的相对新一点的编译器对于连续⼀个表达式步骤中的连续拷贝会进行合并优化,有些更新更"激进"的编译还会进行跨行跨表达式的合并优化。
在此以VS为例简要说明
#include<iostream>
using namespace std;
class A
{
public:A(int a = 0):_a1(a){cout << "A(int a)" << endl;} A(const A& aa):_a1(aa._a1){cout << "A(const A& aa)" << endl;} A& operator=(const A& aa){cout << "A& operator=(const A& aa)" << endl;if (this != &aa){_a1 = aa._a1;} return *this;
} ~A(){cout << "~A()" << endl;}
private:int _a1 = 1;
};
void f1(A aa)
{}
A f2()
{A aa;return aa;
}
int main()
{// 传值传参A aa1;f1(aa1);cout << endl;// 隐式类型,连续构造+拷⻉构造->优化为直接构造f1(1);// ⼀个表达式中,连续构造+拷⻉构造->优化为⼀个构造f1(A(2));cout << endl;cout << "***********************************************" << endl;// 传值返回// 返回时⼀个表达式中,连续拷⻉构造+拷⻉构造->优化⼀个拷⻉构造 (vs2019)// ⼀些编译器会优化得更厉害,进⾏跨⾏合并优化,直接变为构造。(vs2022)f2();cout << endl;// 返回时⼀个表达式中,连续拷⻉构造+拷⻉构造->优化⼀个拷⻉构造 (vs2019)// ⼀些编译器会优化得更厉害,进⾏跨⾏合并优化,直接变为构造。(vs2022)A aa2 = f2();cout << endl;// ⼀个表达式中,连续拷⻉构造+赋值重载->⽆法优化aa1 = f2();cout << endl;return 0;
}
如果你看到了这,那么恭喜你,关于类与对象的大部分内容基本上说清楚了,至于其他的,咱们在后面慢慢说,期待与你的下次相遇~~~