C++初阶:类和对象(二)
大家好,我是小卡皮巴拉
文章目录
目录
一.运算符重载
1.1 基本概念
定义
参数规则
特性
选择原则
重载要点
二.类的默认成员函数
2.1 构造函数
构造函数的特点
2.2 析构函数
析构函数的特点
2.3 拷贝构造函数
拷贝构造的特点
2.4 拷贝赋值运算符重载函数(operator=)
基本概念
避免浅拷贝问题
与拷贝构造函数的区别
2.5 取地址运算符重载
基本概念
普通对象的取地址运算符重载
常量对象的取地址运算符重载
使用场景
注意事项
兄弟们共勉 !!!
每篇前言
博客主页:小卡皮巴拉
咱的口号:🌹小比特,大梦想🌹
作者请求:由于博主水平有限,难免会有错误和不准之处,我也非常渴望知道这些错误,恳请大佬们批评斧正。
一.运算符重载
1.1 基本概念
在 C++ 里,当运算符用于类类型对象时,能通过运算符重载赋予其新的含义。这是因为 C++ 规定类类型对象使用运算符时,必须转换为调用对应的运算符重载函数,若没有合适的重载函数,编译就会报错。
定义
-
函数命名:运算符重载函数有着特殊的名字,由
operator
加上要定义的运算符组成。例如,要重载+
运算符,函数名就是operator+
。 -
函数结构:和普通函数一样,运算符重载函数有返回类型、参数列表和函数体。返回类型取决于运算符重载后的功能需求,参数列表与运算符作用的运算对象数量相关。
参数规则
-
参数数量与运算符类型:运算符的类型决定了重载函数的参数数量。一元运算符(如
++
、--
、!
等)有一个参数,二元运算符(如+
、-
、*
、/
等)有两个参数。对于二元运算符,左侧运算对象会传给第一个参数,右侧运算对象传给第二个参数。 -
成员函数的参数特点:如果运算符重载函数是类的成员函数,第一个运算对象会默认传给隐式的
this
指针,所以成员函数形式的运算符重载参数比运算对象少一个。例如,重载+
运算符作为成员函数时,只需要一个参数来表示另一个操作数。
特性
-
优先级和结合性:运算符重载后,其优先级和结合性与对应的内置类型运算符保持一致。这保证了重载运算符在表达式中的运算顺序符合预期,不会因为重载而改变基本的运算规则。
-
不能创建新运算符:不能通过连接语法中不存在的符号来创建新的运算符,例如
operator@
是不被允许的。 -
不可重载的运算符:有 5 个运算符不能被重载,分别是
.*
、::
、sizeof
、?:
和.
。在学习和考试的选择题中,这是常见的考点。 -
类类型参数要求:运算符重载函数至少要有一个类类型参数,不能通过运算符重载改变内置类型对象的含义。例如,不能定义
int operator+(int x, int y)
来改变int
类型加法的行为。
选择原则
一个类需要重载哪些运算符,要考虑重载后是否有实际意义。例如,Date
类重载 operator-
可以计算两个日期之间的天数差,是有意义的;但重载 operator+
可能没有明确的实际意义,就不需要进行重载。
重载要点
-
前置和后置
++
运算符:重载++
运算符时,存在前置++
和后置++
的区别。它们的运算符重载函数名都是operator++
,为了区分,C++ 规定后置++
重载时增加一个int
形参,以此和前置++
构成函数重载。
二.类的默认成员函数
默认成员函数就是用户没有显式实现,编译器会自动生成的成员函数称为默认成员函数。
C++ 中的类有六个默认成员函数,分别是默认构造函数、拷贝构造函数、析构函数、拷贝赋值运算符重载函数、后面的C++11中还有移动构造函数和移动赋值运算符重载函数,重点是前四个,后面两个我们在这里仅作简单了解。
2.1 构造函数
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象(我们常使用的局部对象是栈帧创建时,空间就开好了),而是对象实例化时初始化对象。
构造函数的本质是要替代我们以前Stack和Date类中写的Init函数的功能,构造函数自动调用的特点就完美的替代的了Init。
构造函数的特点
-
函数名与类名相同:构造函数名称和所在类名称一致。
-
无返回类型:不允许有返回类型,也不能用
void
。 -
可重载:一个类中能定义多个参数列表不同的构造函数。
-
自动调用:创建对象时自动调用,完成对象初始化。
-
可使用初始化列表:比在构造函数体内赋值更高效。
-
编译器提供默认构造函数:类中未定义任何构造函数时,编译器自动提供默认无参构造函数;自定义构造函数后则不再提供。
-
用于动态内存分配:使用
new
运算符动态分配内存创建对象时,构造函数会被调用。
需要注意的是:
- 若未定义构造函数,编译器生成的默认构造函数:对内置类型成员变量的初始化无明确要求,其是否初始化取决于编译器;对自定义类型成员变量,则会调用其默认构造函数完成初始化。
- 无参构造函数、全缺省构造函数以及编译器默认生成的构造函数都属于默认构造函数,这三者有且只能存在一个,不能同时共存。无参和全缺省构造函数虽能构成重载,调用时却会产生歧义。需注意,很多人误以为只有编译器默认生成的才是默认构造函数,实际上,只要无需传入实参就能调用的构造函数,都可称为默认构造函数。
下面的示例代码中展示了构造函数的一些常见特点:
#include <iostream>class Rectangle {
private:double length;double width;public:// 1. 函数名与类名相同,无返回类型// 2. 无参构造函数Rectangle() {length = 0;width = 0;std::cout << "无参构造函数被调用" << std::endl;}// 3. 带参数的构造函数,体现构造函数重载Rectangle(double l, double w) : length(l), width(w) {std::cout << "带参数的构造函数被调用" << std::endl;}// 计算矩形面积double area() {return length * width;}
};int main() {// 4. 创建对象时自动调用无参构造函数Rectangle rect1;std::cout << "rect1的面积: " << rect1.area() << std::endl;// 5. 创建对象时自动调用带参数的构造函数Rectangle rect2(5, 3);std::cout << "rect2的面积: " << rect2.area() << std::endl;// 6. 动态内存分配时调用构造函数Rectangle* rect3 = new Rectangle(7, 4);std::cout << "rect3的面积: " << rect3->area() << std::endl;delete rect3;return 0;
}
2.2 析构函数
析构函数与构造函数功能相反,C++规定对象在销毁时会自动调用析构函数,完成对象中资源的清理释放工作
析构函数的特点
-
函数名与类名相同且前加波浪号
~
:析构函数的名称由类名前加波浪号~
构成,以此来和构造函数区分开。 -
无返回类型和参数:析构函数没有返回类型,也不能有参数,这就意味着析构函数不能被重载,一个类只能有一个析构函数。
-
自动调用:当对象的生命周期结束时,比如对象离开其作用域或者使用
delete
运算符释放动态分配的对象时,析构函数会自动被调用。 -
完成资源清理:析构函数主要用于释放对象在其生命周期内所占用的资源,像动态分配的内存、打开的文件、网络连接等。
需要注意的是:
与构造函数类似,若未显式定义析构函数,编译器自动生成的析构函数不会处理内置类型成员,而会调用自定义类型成员自身的析构函数。并且,即便我们显式编写析构函数,自定义类型成员的析构函数仍会被自动调用,即自定义类型成员的析构调用不受影响。
对于析构函数的编写,若类无需申请资源,如 Date
类,可直接使用编译器生成的默认析构函数;若默认析构函数已能满足资源清理需求,像 MyQueue
类,也不必显式定义。然而,一旦类中涉及资源申请,如 Stack
类,就必须自行编写析构函数,以防资源泄漏。
下面的示例代码中展示了析构函数的一些常见特点
#include <iostream>
#include <cstring>class MyString {
private:char* data;size_t length;public:// 构造函数MyString(const char* str = "") {length = strlen(str);data = new char[length + 1];strcpy(data, str);std::cout << "构造函数被调用,创建字符串: " << data << std::endl;}// 析构函数~MyString() {std::cout << "析构函数被调用,释放字符串: " << data << std::endl;delete[] data;}// 打印字符串void print() {std::cout << data << std::endl;}
};int main() {{// 创建对象,调用构造函数MyString str("Hello, World!");str.print();} // 对象离开作用域,调用析构函数// 动态分配对象MyString* strPtr = new MyString("Dynamic String");strPtr->print();// 释放动态分配的对象,调用析构函数delete strPtr;return 0;
}
2.3 拷贝构造函数
在 C++ 中,拷贝构造函数是一种特殊的构造函数,用于创建一个新对象,该对象是另一个同类型对象的副本。
拷贝构造的特点
-
重载构造函数:拷贝构造函数是构造函数的一种重载形式。
-
参数要求:第一个参数必须是类类型对象的引用,使用传值方式会导致无穷递归调用而编译报错。可以有多个参数,但后续参数需有缺省值。
-
拷贝行为调用:C++ 规定自定义类型对象进行拷贝时必须调用拷贝构造函数,传值传参和传值返回都会触发调用。
-
默认生成规则:若未显式定义,编译器会自动生成拷贝构造函数。对内置类型成员进行浅拷贝(逐字节复制),对自定义类型成员调用其拷贝构造函数。
-
显式定义判断:若类成员全为内置类型且无资源管理,编译器自动生成的拷贝构造通常足够;若类中有指向资源的指针,需显式实现深拷贝;若类内部主要是自定义类型成员,且这些成员已有合适的拷贝构造,可不显式定义。一般来说,若类显式实现了析构函数释放资源,则需要显式编写拷贝构造函数。
-
返回类型影响:传值返回会创建临时对象并调用拷贝构造函数;传引用返回返回对象的别名,不产生拷贝。但要确保返回对象在函数结束后仍然有效,否则会产生野引用。
下面的示例代码中展示了拷贝构造函数的一些常见特点
#include <iostream>
#include <cstring>// 简单的日期类,成员为内置类型,无需管理资源
class Date {
public:int year;int month;int day;// 构造函数,用于初始化日期Date(int y = 2000, int m = 1, int d = 1) : year(y), month(m), day(d) {std::cout << "Date 构造函数被调用" << std::endl;}// 未显式定义拷贝构造函数,编译器自动生成,可完成内置类型的浅拷贝void print() {std::cout << year << "-" << month << "-" << day << std::endl;}
};// 字符串类,包含动态分配的内存,需要管理资源
class MyString {
public:char* data;// 构造函数,接收字符串并动态分配内存存储MyString(const char* str = "") {if (str) {data = new char[strlen(str) + 1];strcpy(data, str);} else {data = new char[1];data[0] = '\0';}std::cout << "MyString 构造函数被调用" << std::endl;}// 拷贝构造函数,实现深拷贝MyString(const MyString& other) {std::cout << "MyString 拷贝构造函数被调用" << std::endl;data = new char[strlen(other.data) + 1];strcpy(data, other.data);}// 析构函数,释放动态分配的内存~MyString() {delete[] data;std::cout << "MyString 析构函数被调用" << std::endl;}void print() {std::cout << data << std::endl;}
};// 传值返回函数,返回时会调用拷贝构造函数
Date returnDate() {Date d(2025, 5, 20);return d;
}// 传引用返回函数,不调用拷贝构造函数
Date& returnDateRef(Date& d) {return d;
}int main() {// 1. 拷贝构造函数是构造函数的重载形式,第一个参数是类对象引用Date d1(2024, 10, 10);// 调用编译器自动生成的拷贝构造函数Date d2(d1); std::cout << "d2 日期: ";d2.print();MyString s1("Hello");// 调用显式定义的拷贝构造函数进行深拷贝MyString s2(s1); std::cout << "s2 字符串: ";s2.print();// 2. 传值返回会调用拷贝构造函数Date result = returnDate();std::cout << "传值返回的日期: ";result.print();// 3. 传引用返回不调用拷贝构造函数Date d3(2026, 1, 1);Date& ref = returnDateRef(d3);std::cout << "传引用返回的日期: ";ref.print();return 0;
}
2.4 拷贝赋值运算符重载函数(operator=
)
基本概念
赋值运算符重载用于实现对象之间的赋值操作。当使用=
将一个对象赋值给另一个对象时,编译器会调用赋值运算符重载函数。默认情况下,编译器会为类生成一个浅拷贝的赋值运算符,但在类中包含动态分配的资源(如指针)时,浅拷贝可能会导致问题,因此需要自定义赋值运算符重载函数来实现深拷贝。
避免浅拷贝问题
在类中如果包含动态分配的资源(如动态数组、指针等),默认的赋值运算符执行的是浅拷贝。浅拷贝仅复制指针的值,而不复制指针所指向的内容,这会导致多个对象共享同一块内存,当其中一个对象被销毁时,释放该内存会使其他对象的指针变为悬空指针,进而引发程序崩溃或产生未定义行为。
通过重载赋值运算符,可以实现深拷贝,即复制指针所指向的内容,为每个对象分配独立的内存空间,避免上述问题。以下是一个示例:
#include <iostream>
#include <cstring>class MyString {
private:char* data;size_t length;
public:// 构造函数MyString(const char* str = "") {length = strlen(str);data = new char[length + 1];strcpy(data, str);}// 析构函数~MyString() {delete[] data;}// 赋值运算符重载,实现深拷贝MyString& operator=(const MyString& other) {if (this != &other) {delete[] data;length = other.length;data = new char[length + 1];strcpy(data, other.data);}return *this;}void print() const {std::cout << data << std::endl;}
};int main() {MyString s1("Hello");MyString s2("World");s2 = s1;s2.print();return 0;
}
与拷贝构造函数的区别
拷贝构造函数用于在创建新对象时,使用已存在的对象来初始化它;而赋值运算符重载用于将一个已存在对象的值赋给另一个已存在的对象。虽然它们都涉及对象的复制,但应用场景不同。例如:
MyClass obj1;
MyClass obj2 = obj1; // 调用拷贝构造函数
MyClass obj3;
obj3 = obj1; // 调用赋值运算符重载函数
2.5 取地址运算符重载
基本概念
取地址运算符 &
是一个一元运算符,在默认情况下,对对象使用 &
运算符会返回该对象的内存地址。但通过重载取地址运算符,你可以改变这个默认行为,返回一个自定义的地址或者其他类型的值。取地址运算符重载有两种形式:普通对象的取地址运算符重载和常量对象的取地址运算符重载。
普通对象的取地址运算符重载
返回类型 operator&();
这里的 返回类型
可以是任意合法的 C++ 类型,通常是指针类型,但也可以是其他类型。该函数没有参数,因为它是一元运算符。
常量对象的取地址运算符重载
返回类型 operator&() const;
此形式用于常量对象,函数声明后面的 const
关键字表明该函数不会修改对象的状态。
使用场景
- 封装指针:在某些情况下,你可能不希望直接暴露对象的真实地址,而是返回一个封装后的指针或者代理对象。
- 调试和日志记录:在重载函数中添加额外的逻辑,如记录对象被取地址的操作,方便调试。
- 智能指针:智能指针类可以重载取地址运算符,返回内部管理的原始指针。
下面是一个简单的示例,展示了如何重载取地址运算符:
#include <iostream>class MyClass {
private:int data;
public:MyClass(int value) : data(value) {}// 普通对象的取地址运算符重载MyClass* operator&() {std::cout << "普通对象取地址操作" << std::endl;return this;}// 常量对象的取地址运算符重载const MyClass* operator&() const {std::cout << "常量对象取地址操作" << std::endl;return this;}void printData() const {std::cout << "Data: " << data << std::endl;}
};int main() {MyClass obj(42);MyClass* ptr = &obj;ptr->printData();const MyClass constObj(100);const MyClass* constPtr = &constObj;constPtr->printData();return 0;
}
- 普通对象的取地址运算符重载:在
MyClass
类中,MyClass* operator&()
函数被重载。当对普通的MyClass
对象使用&
运算符时,会调用该函数,函数内部输出一条信息并返回this
指针。 - 常量对象的取地址运算符重载:
const MyClass* operator&() const
函数用于常量对象。当对常量的MyClass
对象使用&
运算符时,会调用该函数,同样输出一条信息并返回this
指针。
注意事项
- 返回类型:虽然返回类型可以是任意合法的 C++ 类型,但通常建议返回指针类型,以保持与取地址操作的语义一致。
- 常量性:要同时提供普通对象和常量对象的取地址运算符重载版本,以确保对常量对象的取地址操作也能正确处理。
- 慎用重载:取地址运算符是一个非常基础的运算符,重载它可能会让代码的行为变得复杂,增加理解和维护的难度。因此,只有在确实有必要的情况下才进行重载。
兄弟们共勉 !!!
码字不易,求个三连
抱拳了兄弟们!