xml
hello,这里是AuroraWanderll。
兴趣方向:C++,算法,Linux系统,游戏客户端开发
欢迎关注,我将更新更多相关内容!
这是类和对象系列的第四篇文章,上篇指引:类和对象三:默认成员函数与运算符重载
类和对象(四)-默认成员函数详解与运算符重载(下)
简易目录
- 拷贝构造函数详解
- 赋值运算符重载详解
- const成员详解
- 取地址及const取地址操作符重载
4. 拷贝构造函数详解
4.1 拷贝构造函数的概念
核心思想:创建一个与已存在对象一模一样的新对象,就像"克隆"、"复制"一样。
定义:只有单个形参,该形参是对本类类型对象的引用(通常用const修饰,如const Date& d),在用已存在的类类型对象创建新对象时由编译器自动调用。
4.2 拷贝构造函数的特征
1. 是构造函数的重载形式(可以认为拷贝构造就是我们构造函数的一种)
class Date
{
public:
// 普通构造函数
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// 拷贝构造函数(构造函数的重载)
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
};
2. 参数必须是引用类型
错误写法会导致无限递归:
class Date
{
public:
// 错误:传值方式会引发无限递归
Date(Date d) // 编译报错
{
_year = d._year;
_month = d._month;
_day = d._day;
}
};
给函数本身传值就是一种构造,会导致调用拷贝构造,拷贝构造里又要传值,就会无限递归
所以必须用&引用写法来避免。
正确写法:
class Date
{
public:
// 正确:使用const引用
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
};
int main()
{
Date d1;
Date d2(d1); // 调用拷贝构造函数
return 0;
}
为什么必须用引用:
- 如果传值,需要先拷贝实参,而拷贝实参又需要调用拷贝构造函数...
- 这就形成了无限递归,编译器会直接报错
3. 编译器生成的默认拷贝构造函数(未显式定义时)
浅拷贝(值拷贝):默认拷贝构造函数按内存字节序完成拷贝。
class Time
{
public:
Time()
{
_hour = 1;
_minute = 1;
_second = 1;
}
Time(const Time& t)
{
_hour = t._hour;
_minute = t._minute;
_second = t._second;
cout << "Time::Time(const Time&)" << endl;
}//拷贝构造
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 内置类型:按字节拷贝
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型:调用其拷贝构造函数
Time _t;
};
int main()
{
Date d1;
Date d2(d1); // 调用Date的默认拷贝构造函数
return 0;
}
运行结果 :会输出 Time::Time(const Time&),证明调用了Time类的拷贝构造函数。
4. 何时需要显式定义拷贝构造函数
不需要显式定义的情况(简单类):
class Date
{
private:
int _year;
int _month;
int _day;
// 编译器生成的默认拷贝构造函数足够用
};
必须显式定义的情况(管理资源的类):
class Stack
{
public:
Stack(size_t capacity = 10)
{
_array = (int*)malloc(capacity * sizeof(int));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_size = 0;
_capacity = capacity;
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
// 必须显式定义拷贝构造函数(深拷贝)
Stack(const Stack& st)
{
_array = (int*)malloc(st._capacity * sizeof(int));
if (_array == nullptr)
{
perror("malloc失败");
exit(-1);
}
memcpy(_array, st._array, st._size * sizeof(int));
_size = st._size;
_capacity = st._capacity;
}
private:
int* _array;
size_t _size;
size_t _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
Stack s2(s1); // 深拷贝:s2有自己的独立内存空间
return 0;
}
浅拷贝的问题:
- 如果使用编译器生成的默认拷贝构造函数
- s1和s2的
_array指向同一块内存(编译器直接粗略的拷贝了一份一样的成员变量,导致指针根本就指向同一块内存) - 析构时同一块内存会被释放两次 → 程序崩溃
5. 拷贝构造函数的典型调用场景
class Date
{
public:
Date(int year, int month, int day)
{
cout << "Date(int,int,int):" << this << endl;
}
Date(const Date& d)
{
cout << "Date(const Date& d):" << this << endl;
}
~Date()
{
cout << "~Date():" << this << endl;
}
private:
int _year;
int _month;
int _day;
};
// 场景1:函数参数为类类型对象(传值)
Date Test(Date d) // 调用拷贝构造函数
{
// 场景2:用已存在对象创建新对象
Date temp(d); // 调用拷贝构造函数
return temp; // 场景3:函数返回值(可能调用拷贝构造,取决于编译器优化)
}
int main()
{
Date d1(2022, 1, 13); // 调用普通构造函数
Test(d1); // 调用拷贝构造函数(参数传递)
return 0;
}
4.3 深拷贝 vs 浅拷贝
| 特性 | 浅拷贝 | 深拷贝 |
|---|---|---|
| 定义 | 只拷贝指针值 | 创建新的资源副本 |
| 内存关系 | 多个对象共享同一资源 | 每个对象有独立资源 |
| 适用场景 | 无动态资源的简单类 | 管理动态资源的类 |
| 风险 | 双重释放、悬空指针 | 无资源共享问题 |
| 性能 | 快 | 相对较慢 |
总结要点:
- 拷贝构造函数用途:创建对象的副本,复制原有对象
- 语法要求:参数必须是同类对象的const引用
- 默认行为:编译器生成浅拷贝版本
- 深拷贝必要:管理动态资源的类必须自定义拷贝构造函数
- 调用时机:对象初始化、函数传参、函数返回
核心原则:
- 简单数据成员 → 使用编译器生成的拷贝构造函数
- 管理动态资源 → 必须自定义深拷贝构造函数
- 传递对象参数时尽量使用引用,避免不必要的拷贝
5. 赋值运算符重载详解
5.1 运算符重载基础
概念
C++允许为自定义类型重载运算符,使其具有与内置类型相似的语法特性,增强代码可读性。
可以将运算符重载理解成具有特殊函数名的函数
基本语法
返回值类型 operator运算符(参数列表)
重载限制
- 不能创建新运算符(如
operator@,意思是说重载的操作符必须是语言标准原来就有的) - 必须至少有一个类类型参数
- 不能改变内置类型运算符的含义(只是用于改变当前对象的运算符,原来内置类型的不能改)
- 不能重载的运算符:
.*、::、sizeof、?:、. - 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐 藏的this
两种重载方式
1. 全局函数重载
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// 为了让全局函数访问私有成员,需要设置为友元
friend bool operator==(const Date& d1, const Date& d2);
private:
int _year;
int _month;
int _day;
};
// 全局运算符重载
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
运算符重载成全局的就需要成员变量是公有的
但是我们又必须保证我们的封装性:
所以这里需要用我们后面学习的友元解决,或者干脆重载成成员函数。
2. 成员函数重载(推荐)
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// 成员函数重载 == 运算符
// 编译器转换为:bool operator==(Date* this, const Date& d2)//this为隐藏的第一个参数
bool operator==(const Date& d2)
{
return _year == d2._year
&& _month == d2._month
&& _day == d2._day;
}
private:
int _year;
int _month;
int _day;
};
void Test()
{
Date d1(2018, 9, 26);
Date d2(2018, 9, 27);
cout << (d1 == d2) << endl; // 输出:0 (false)
}
5.2 赋值运算符重载
基本格式和要求
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
// 赋值运算符重载
Date& operator=(const Date& d)
{
// 1. 检查自赋值
if(this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
// 2. 返回*this以支持连续赋值
return *this;
}
private:
int _year;
int _month;
int _day;
};
关键点:
- 参数:const T&引用,传递引用减少拷贝次数,以此来提高效率
- 返回值 :T&引用,一样是利用引用提高返回的效率,同时有返回值是为了支持连续赋值(如
a = b = c) - 自赋值检查:避免不必要的操作和潜在错误
- 返回*this:可以符合连续赋值的要求
为什么赋值运算符重载必须是成员函数,不能重载成全局函数
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
int _year;
int _month;
int _day;
};
// 错误:赋值运算符不能重载为全局函数
// 编译错误:operator= must be a non-static member function
Date& operator=(Date& left, const Date& right)
{
if (&left != &right)
{
left._year = right._year;
left._month = right._month;
left._day = right._day;
}
return left;
}
注意不要误解成运算符重载只能重载成成员函数,这里说的是赋值运算符重载
原因:编译器会自动生成默认的赋值运算符(成员函数),如果用户再定义全局版本,会产生冲突。所以赋值运算符重载
编译器生成的默认赋值运算符(未显式实现时)
浅拷贝行为:(以值的方式逐字节拷贝)
-
内置类型:直接赋值
-
自定义类型:调用其赋值运算符
class Time
{
public:
Time()
{
_hour = 1;
_minute = 1;
_second = 1;
}Time& operator=(const Time& t) { if (this != &t) { _hour = t._hour; _minute = t._minute; _second = t._second; } return *this; }private:
int _hour;
int _minute;
int _second;
};class Date
{
private:
// 内置类型:直接赋值
int _year = 1970;
int _month = 1;
int _day = 1;// 自定义类型:调用Time的赋值运算符 Time _t;};
int main()
{
Date d1;
Date d2;
d1 = d2; // 调用Date的默认赋值运算符
return 0;
}
何时需要自定义赋值运算符
不需要的情况(简单类,例如日期类):
class Date
{
private:
int _year;
int _month;
int _day;
// 编译器生成的足够用
};
必须自定义的情况(管理资源的类):
class Stack
{
public:
Stack(size_t capacity = 10)
{
_array = (int*)malloc(capacity * sizeof(int));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_size = 0;
_capacity = capacity;
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
// 必须自定义赋值运算符(深拷贝)
Stack& operator=(const Stack& st)
{
// 1. 检查自赋值
if (this != &st)
{
// 2. 释放原有资源
free(_array);
// 3. 分配新资源并拷贝数据
_array = (int*)malloc(st._capacity * sizeof(int));
if (_array == nullptr)
{
perror("malloc失败");
exit(-1);
}
memcpy(_array, st._array, st._size * sizeof(int));
_size = st._size;
_capacity = st._capacity;
}
return *this;
}
private:
int* _array;
size_t _size;
size_t _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
Stack s2;
s2 = s1; // 深拷贝赋值
return 0;
}
浅拷贝赋值的问题:
- 两个对象指向同一块内存
- 析构时同一内存被释放两次 (因为两个指针指向同一块空间,并且每个指针指向的空间都会被释放一次)→ 程序崩溃
- 内存泄漏(原s2的内存未被释放)
5.3 前置++和后置++重载
语法区别
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// 前置++:返回+1后的引用
Date& operator++()
{
_day += 1;
// 实际中应该处理月份和年份的进位
return *this;
}
// 后置++:int参数为占位符,用于区分
// 返回+1前的临时对象(值返回)
Date operator++(int)
{
Date temp(*this); // 保存原值
_day += 1;
return temp; // 返回原值
}
private:
int _year;
int _month;
int _day;
};
使用示例
int main()
{
Date d1(2022, 1, 13);
Date d2;
d2 = d1++; // 后置++:d2为2022,1,13,d1变为2022,1,14
d2 = ++d1; // 前置++:d1先变为2022,1,15,然后d2为2022,1,15
return 0;
}
关键区别
| 特性 | 前置++ | 后置++ |
|---|---|---|
| 参数 | 无 | int(占位参数)这是最重要的 |
| 返回值 | 引用 | 值(临时对象)这是最重要的 |
| 效率 | 高 | 较低(创建临时对象) |
| 语义 | 先加1后使用 | 先使用后加1 |
总结要点:
- 运算符重载目的:让自定义类型使用更自然
- 赋值运算符必须:是成员函数,返回引用,检查自赋值
- 默认行为:编译器生成浅拷贝版本
- 深拷贝必要:管理动态资源的类必须自定义赋值运算符
- 前后置++:通过int参数区分,前置返回引用,后置返回值
核心原则:
- 简单数据成员 → 使用编译器生成的赋值运算符
- 管理动态资源 → 必须自定义深拷贝赋值运算符
6. const成员详解
6.1 const成员函数的概念
核心理解 :const修饰成员函数,就叫做"const成员函数",实际上是修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
6.2 const成员函数的使用
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;
}
// const成员函数
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; // 日
};
void Test()
{
Date d1(2022, 1, 13);
d1.Print(); // 调用普通版本
const Date d2(2022, 1, 13);
d2.Print(); // 调用const版本
}
6.3 四个关键问题解析
1. const对象可以调用非const成员函数吗?
答案:不可以
const Date d2(2022, 1, 13);
d2.Print(); // 只能调用const版本的Print()
// d2.非const函数(); // 错误:const对象不能调用非const成员函数
原因 :const对象的this指针是const Date*类型,而非const成员函数期望的是Date*类型,类型不匹配。属于是一种权限被放大了,所以不行(权限可以被缩小,但是一定不能被放大)
2. 非const对象可以调用const成员函数吗?
答案:可以
Date d1(2022, 1, 13);
d1.Print(); // 可以调用const版本的Print()
原因:非const对象可以自动转换为const类型,这是安全的。(这是一种权限的缩小)
3. const成员函数内可以调用其它的非const成员函数吗?
答案:不可以
class Date
{
public:
void Modify()
{
_year = 2023; // 修改成员
}
void Print() const
{
// Modify(); // 错误:const成员函数内不能调用非const成员函数(同理的,权限被放大)
cout << _year << endl;
}
};
原因:const成员函数承诺不修改对象状态,而非const成员函数可能修改对象,这违背了const的承诺。
4. 非const成员函数内可以调用其它的const成员函数吗?
答案:可以
class Date
{
public:
void Display() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
void Print()
{
Display(); // 正确:非const成员函数可以调用const成员函数
// 其他操作...
}
};
原因:这是安全的,const成员函数不会修改对象状态。
7. 取地址及const取地址操作符重
7.1 基本概念
这两个运算符一般不需要重载,编译器会默认生成。只有在特殊情况下才需要自定义。
7.2 默认实现
class Date
{
public:
// 普通取地址操作符重载(编译器默认生成类似这样)
Date* operator&()
{
return this;
}
// const取地址操作符重载(编译器默认生成类似这样)
const Date* operator&() const
{
return this;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
7.3 使用示例
void Test()
{
Date d1(2022, 1, 13);
const Date d2(2022, 1, 14);
Date* p1 = &d1; // 调用普通版本的operator&
const Date* p2 = &d2; // 调用const版本的operator&
}
7.4 特殊情况下的重载
场景:隐藏真实地址或返回特定内容
class SecureData
{
private:
int _data;
static int _fakeAddress; // 假地址
public:
// 重载取地址运算符,不返回真实地址
SecureData* operator&()
{
return (SecureData*)&_fakeAddress; // 返回假地址
}
const SecureData* operator&() const
{
return (const SecureData*)&_fakeAddress; // 返回假地址
}
};
int SecureData::_fakeAddress = 0;
void Test()
{
SecureData obj;
SecureData* p = &obj; // 得到的是假地址,不是obj的真实地址
}
7.5 实际应用建议
99%的情况:使用编译器生成的默认版本
class Date
{
// 不需要写operator&,编译器会自动生成
// 生成的版本就是返回this指针
};
1%的特殊情况:
- 实现智能指针类
- 需要隐藏对象真实地址的安全类
- 返回代理对象的特定设计模式
总结要点:
const成员函数:
- 实质是修饰
this指针为const T* - 保证函数内不修改对象状态
- const对象只能调用const成员函数
- 合理使用const成员函数可以提高代码的安全性和可读性
取地址操作符重载:
- 通常使用编译器默认生成的版本
- 包括普通版本和const版本
- 只有在特殊需求时才需要自定义重载
- 自定义时要注意保持语义的合理性
最佳实践:
- 对于不修改对象状态的成员函数,都应该声明为const
- 除非有特殊需求,否则不要重载取地址运算符
- 使用const正确性可以提高代码的健壮性。
xml
感谢你能够阅读到这里,如果本篇文章对你有帮助,欢迎点赞收藏支持,关注我,
我将更新更多有关C++,Linux系统·网络部分的知识。