【C++——类和对象(下)】

目录

一、运算符重载

C++ 允许对类类型对象重载运算符,给运算符赋予自定义行为

C++规定类类型对象使用运算符时,必须转换成调用对应运算符重载,若没有对应的运算符重载,则会编译报错

运算符重载: 本质是特殊函数,函数名格式:operator运算符

cpp 复制代码
返回值类型 operator运算符(参数列表)
{
    函数体;
}

规则:

  1. 参数个数和运算符操作数保持一致。二元运算符,左操作数对应第一个参数,右操作数对应第二个参数。
  2. 调用两种写法:显式调用operator==(x1,x2);简写x1==x2,编译器自动转换
  3. 重载之后运算符优先级、结合性不会改变,与对应内置类型运算符保持一致
  4. 不能创造全新运算符
  5. 五个运算符不能重载 :.*、::(域作用限定符)、sizeof、?:(三目运算符)、.(对象.成员)。
  6. 重载运算符,至少要有一个类类型参数,不能修改内置类型运算符行为。

Tips:一个类需要重载哪些运算符是看哪些运算符重载后有意义

1.全局运算符重载的访问问题

如果把运算符重载写成全局函数,无法直接访问类的 private 私有成员,三种解决方案:

  1. 修改成员为 public(破坏封装,不推荐)
  2. 提供GetXxx()get 接口获取私有成员,外部获取它的拷贝
    例:
cpp 复制代码
int GetYear()
{
    return _year;
}

3.将运算符重载作为类的成员函数

写成成员函数,第一个操作数直接由隐含this指针接管,参数列表参数数量相比全局版本少一个

重载运算符函数调用:

cpp 复制代码
//成员函数调用
x1.operator==(x2);
//简写形式
x1 == x2;

Tips:如果全局版本和成员版本同时存在,优先调用成员版本

2.++ 运算符重载

++分为前置++d1、后置d1++,函数名都是operator++。

C++ 语法约定:后置 ++ 多增加一个 int 类型哑元参数,用来做函数重载区分

cpp 复制代码
//后置++ d1++
Date operator++(int);
//前置++ ++d1
Date operator++();

3.流插入 <<、流提取>> 重载

cout << 对象 流插入、cin >> 对象流提取不能重载为成员函数

因为二元运算符左操作数是cout/cin,成员函数会抢占左操作数为对象本身,this指针默认抢占了第一个形参位置,调用语法变成对象 << cout,不符合习惯

所以<<、>>重载为全局函数,ostream/istream放到第一个形参位置,第二个形参位置当类类型对象;ostream、istream必须传引用,流对象不支持拷贝构造。

如果需要访问私有成员,可以使用friend友元声明

(1)流插入运算符重载

cpp 复制代码
//友元声明写在类内部
friend ostream& operator<<(ostream& out,const Date& d);
//全局实现
ostream& operator<<(ostream& out,const Date& d)
{
    out<<d._year<<"年"<<d._month<<"月"<<d._day<<"日"<<endl;
    return out;
}

返回值是左操作数,因此返回ostream&引用,目的支持连续输出:cout<<d1<<d2;

(2)流提取运算符重载

读取修改对象内容,第二个参数不加 const

cpp 复制代码
friend istream& operator>>(istream& in,Date& d);

istream& operator>>(istream& in,Date& d)
{
    cout<<"请依次输入年月日:";
    in>>d._year>>d._month>>d._day;
    return in;
}

4.赋值运算符重载

赋值重载是默认成员函数,用于两个已经存在的对象之间赋值拷贝

区分拷贝构造 vs 赋值重载:

  • 拷贝构造: 新对象正在创建,用已有对象初始化,例:Date d4 = d2;
  • 赋值重载: 两个对象都已经存在,例:d1 = d2;
cpp 复制代码
int main()
{
    Date d1(2026,9,26);
    Date d2(2026,9,27);
    d1=d2; //赋值重载

    Date d3(d2); //拷贝构造,创建新对象
    Date d4=d2;  //拷贝构造
    return 0;
}

赋值重载特点:

  1. 赋值运算符operator=必须重载为成员函数。参数推荐const 类名&,避免传值带来拷贝
  2. 返回值推荐类引用 ,支持连续赋值d1=d2=d3;,提升效率。返回*this
  3. 用户没有实现,编译器自动生成默认赋值重载,行为类似默认拷贝构造:内置类型浅拷贝逐字节复制;自定义类型调用它的赋值重载
  4. 如果一个类显式实现了析构并释放资源,也就是类内部管理堆资源(malloc/new),必须手写赋值重载,否则浅拷贝导致 double free 崩溃

连续赋值和返回*this:

赋值表达式的返回值是左操作数,操作数从右向左连续赋值

赋值运算符重载也应该实现这样的效果

cpp 复制代码
#include<iostream>
Date& operator=(const Date& d)
{
	_year=d._year;
	_month=d._month;
	_day=d._day;
	return *this;
}

如果要实现d3=d1,返回值应该为d3,d1传给d,d是d1的别名,d3传给this,this是存储d3地址的指针,所以这里返回*this

完整示例:

cpp 复制代码
#include<iostream>
using namespace std;
class Date
{
public:
    Date(int year=1,int month=1,int day=1)
    {
        _year=year;
        _month=month;
        _day=day;
    }

    //赋值运算符重载
    Date& operator=(const Date& d)
    {
        _year=d._year;
        _month=d._month;
        _day=d._day;
        return *this;
    }

    void Print()
    {
        cout<<_year<<"/"<<_month<<"/"<<_day<<endl;
    }

private:
    int _year;
    int _month;
    int _day;
};

5.取地址运算符重载

(1)const成员函数

const写在成员函数括号后面,称为 const 成员函数

cpp 复制代码
void Print() const;

底层含义: 修饰该函数的隐含this指针,此时this指针类型变成const Date* const this

const 成员函数内部不能修改类的成员变量

使用场景:

cpp 复制代码
const Date d1(2026,9,29);
d1.Print();

因为d1是 const 对象,传递给普通成员函数后发生权限放大(const 指针赋值给普通指针),编译报错

d1地址的类型是const Date*

指针类型:默认情况下void Date::Print();这里的指针是Date*,严格来说这里是Date* const this,const修饰指针本身

要想编译不报错,这里this指针的类型也应该为const Date*,但是由于this指针无法修改,因此这里采用const修饰的成员函数放到成员函数参数列表的后面

把void Date::Print();变为void Date::Print() const;

const实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员函数进行修改

**编码建议:**凡是不修改成员变量的成员函数,全部加上const修饰

(2)取地址运算符重载

&取地址也是默认成员函数,绝大多数场景编译器自动生成版本就够用,极少手动实现。

提供两套重载,分别适配普通对象、const 对象

普通对象取地址运算符重载:

cpp 复制代码
Date* operator&()
{
	return this;
}

const对象取地址运算符重载:

cpp 复制代码
const Date* operator&() const
{
	return this;
}

二、构造函数深入

构造函数初始化除了在函数体内赋值,还可以采用初始化列表的方式

初始化列表语法格式:

以一个冒号开始,接着以一个逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式

cpp 复制代码
类名(参数列表)
: 成员1(初始值),
  成员2(初始值),
  成员3(表达式)
{
    //函数体赋值
}

初始化列表核心要点:

  1. 可以理解为成员变量定义初始化的地方,比函数体内赋值更早执行。
  2. 每一个成员变量,初始化列表最多只能出现一次。
  3. 三类成员必须写在初始化列表初始化,否则编译报错
    • 引用成员变量
    • const 修饰成员变量
    • 没有默认构造函数的自定义类型成员
  4. C++11 支持类内给成员缺省值,这个缺省值给没有写在初始化列表的成员使用。
  5. ⚠️初始化列表初始化顺序按照类中成员声明顺序执行,和初始化列表书写先后无关!

建议初始化列表书写顺序和类里面声明顺序保持一致,规避 bug

每个成员都要走初始化列表,尽量使用初始化列表初始化

不在初始化列表初始化的成员:

  1. 这个成员在声明位置给了缺省值:初始化列表会用这个缺省值初始化
  2. 没有给缺省值:对于没有显示在初始化列表实现初始化的
    • 内置类型成员: 是否初始化取决于编译器,C++并没有规定,大概率是随机值
    • 自定义类型成员: 会调用这个成员类型的默认构造函数,如果没有默认构造会编译错误
cpp 复制代码
#include<iostream>
using namespace std;
class Time
{
public:
    Time(int hour=0)
        :_hour(hour)
    {
        cout<<"Time()"<<endl;
    }
private:
    int _hour;
};
class Date
{
public:
	Date(int year,int month,int day)
		:_year(year)
		,_month(month)
		,_day(day)
	{}
//声明 给初始化列表缺省值
private:
	int _year=1;
	int _month=1;
	int _day=1;

	int* ptr=(int*)malloc(12);//缺省值可以是表达式形式
	Time _t=1;
};
int main()
{
	//对象定义
	Date d1(2026,10,1);
	return 0;
}

初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表出现的先后顺序无关,建议声明顺序和初始化顺序保持一致

三、类型转换

单参数构造函数,支持内置类型隐式转换为自定义类对象

cpp 复制代码
#include<iostream>
using namespace std;
class A
{
public:
    A(int a)
    {
        _a1=a;
    }

    A(const A& aa)
    {
        _a1=aa._a1;
    }

    void Print()
    {
        cout<<_a1<<endl;
    }
private:
    int _a1;
};

int main()
{
    A aa1(1);
    aa1.Print();
    
    //隐式类型转换:先用2构造临时A对象,再拷贝构造aa2
    A aa2=2;
    aa2.Print();
    return 0;
}

现代编译器会做优化: 上面这种连续的「构造 + 拷贝构造」直接合并,变成直接构造对象

多参数支持列表初始化:

上面的是单个参数列表初始化,单参数直接转的本质是一个内置类型转为自定义类型

多个参数不支持上面的单参数写法,采用下面的写法:

cpp 复制代码
A aa5={1,1};
const A& raa6={2,2};

如果不想允许隐式类型转换,在构造函数前面加上explicit关键字。

explicit A(int a);,禁止单参数构造函数的隐式转换。

四、static 静态成员

静态成员变量: 用static修饰的成员变量

重点✨:静态成员变量必须在类外面初始化!不属于某个对象,存放在静态全局区,所有对象共享同一份。

cpp 复制代码
#include<iostream>
using namespace std;
class A
{
private:
    static int _scount; //类内只是声明
};
int A::_scount=0; //类外初始化定义

int main()
{
    cout<<sizeof(A)<<endl;
    return 0;
}

**静态成员函数:**用static修饰的成员函数

例:

cpp 复制代码
static int GetACount()
{
	return _scount;
}
  1. 静态成员函数没有 this 指针!
  2. 静态成员函数只能访问静态成员,不能直接访问非静态成员(没有 this)
  3. 普通非静态成员函数,可以访问静态成员

访问静态成员两种方式:

cpp 复制代码
//类名::静态成员 突破类域访问(推荐)
cout<<A::GetACount()<<endl;
//对象.静态成员 访问
A a;
cout<<a.GetACount()<<endl;

静态成员也是类的成员,依然受public/private/protected访问限定符限制

五、友元

友元提供了一种突破类访问限定符封装的方式,可以访问类的私有、保护成员

友元分为:友元函数和友元类,在函数声明或者类声明的前面加friend,并且把友元声明写在类内部

友元函数:

  1. 外部友元函数可以访问类的私有和保护成员,友元函数仅仅是一种声明,不是类的成员函数
  2. 友元函数可以在类的任何地方声明,不受类访问限定符限制
  3. 一个函数可以是多个类的友元函数
cpp 复制代码
friend ostream& operator<<(ostream& out,const Date& d);

友元类:

友元类中的成员函数都可以是另一个类的友元函数,都可以访问另一个类中的私有和保护成员

cpp 复制代码
class A
{
	friend class B;
	//...
};
class B
{
	//...
};
  1. 友元类的关系是单向的,不具有交换性
  2. 友元类的关系不能传递

Tips:友元有时提供了便利,但是会增加耦合度,破坏封装,所以友元不宜多用

六、内部类

如果一个类定义在另一个类的内部,那这个类就叫内部类

  • 内部类是独立的类,跟定义在全局相比,它只是受外部类类域和访问限定符管控
  • 外部类对象内存中,不会包含内部类对象
  • 内部类默认是外部类的友元类

适用场景: 内部类本质也是一种封装,当A类和B类紧密关联,A类实现出来主要就是给B类使用,那么可以考虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其他就不用考虑了

cpp 复制代码
#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;
			cout<<a.h<<endl;
		}
	private:
		int _b=1;
	};
};
int main()
{
	cout<<sizeof(A)<<endl;//只有_h
	A::B b;//受类域限制,无法直接定义,需要指定类域
	return 0;
}

1.匿名对象

没有变量名的临时对象,生命周期只在当前这一行语句结束

cpp 复制代码
#include<iostream>
using namespace std;
int main()
{
	A aa1;//有名对象
	A();//匿名对象
	A(1);//匿名对象
	solution st;
	cout<<st.Sum_Solution(10)<<endl;//有名对象调用
	cout<<Solution().Sum_Solution(10)<<endl;//匿名对象调用
	//排序
	int a[]={5,6,3,7,2,1};
	//有名对象
	greater<int> gt;
	sort(a,a+5,gt);
	//匿名对象
	sort(a,a+5,greater<int>());
	return 0;
}

Tips:A aa2();这种定义方法是错误的,因为这样编译器无法识别是函数声明还是对象定义

七、对象拷贝时的编译器优化

现代编译器会为了尽可能提高程序的效率,在不影响正确性的情况下尽可能减少一些传参和传返回值过程中可以省略的拷贝

如何优化C++标准并没有严格规定,各个编译器会根据情况自行处理:

当前主流的相对新一点的编译器对于连续一个表达式步骤中的连续拷贝会进行合并优化,有些更新更"激进"的编译还会进行跨行跨表达式的合并优化

1.直接构造优化

A aa1=1;语法上1先构造一个临时对象,临时对象再拷贝构造,编译器在这里优化为直接构造

const A& aa2=1;直接构造

2.传参场景优化

void f1(A aa){}变为void f1(A& aa){}减少中间拷贝构造

A aa1(1);f1(aa1);变为f1(A(1);匿名对象先构造在拷贝构造,在连续构造中,编译器优化合二为一直接构造

如果再改为f1(1);1走隐式类型转换,类型转换过程中会产生临时对象,临时对象再进行拷贝构造,这里编译器优化合二为一直接构造

3.传返回值

cpp 复制代码
A f2()
{
	A aa;
	return aa;
}

传值传参返回会产生临时对象,也是构造加拷贝构造,会被编译器合二为一优化

跨行、跨语句不一定会触发拷贝优化,不要业务逻辑依赖编译器拷贝优化行为

相关推荐
凯哥Java1 小时前
写代码怎么避免逻辑漏洞?
java·开发语言·人工智能·自动化
汉克老师1 小时前
GESP2026年9月认证C++二级( 第一部分选择题(1~7题)精讲
c++·gesp·小学生·学c++编程
Ai-_Man2 小时前
您您这可以把Dola的多个会话比如说。左侧的多个会话一次性导出吗?不是单条会话里面的多次会对话。用AI导出鸭,答案是可以的
开发语言·前端·人工智能·小程序
Java后端的Ai之路2 小时前
Python进阶探索23 - Python中的Time与Datetime模块
开发语言·数据库·python·datetime·time
沫璃染墨2 小时前
《从零入门Linux系统篇(五十四):线程篇·七——互斥锁底层原理:从原子交换到线程竞争与锁实现》
linux·运维·服务器·开发语言·c++·驱动开发·系统架构
Java后端的Ai之路2 小时前
Python进阶探索24_应用案例_linux系统的监控
linux·开发语言·python·应用·探索
程序员清风3 小时前
PydanticAI 实战:用类型安全构建可靠的 Python Agent
开发语言·python·安全
落魄实习生3 小时前
Agent Scope Java 2.x 系列【10】Middleware
java·开发语言·ai