文章目录
- 🎯引言
- 👓类和对象(2)
- 1.类的默认成员函数
- 2.构造函数
- 2.1构造函数概念
- 3.析构函数
- 3.1. **析构函数的定义**
- 3.2. **析构函数的特点**
- 4.拷贝构造函数
- 4.1. **拷贝构造函数的定义**
- 5.赋值运算符重载
- 5.1运算符重载
- 5.2赋值运算符重载
- 5.3日期类的实现
- 🥇结语
🎯引言
在C++中,类的成员函数可以由程序员自行定义,但编译器在某些情况下会自动为类生成一些特殊的默认成员函数。这些函数包括构造函数、析构函数、拷贝构造函数、赋值运算符等。了解这些默认成员函数的生成规则及其行为,对于编写高效、健壮的C++代码至关重要。本文将深入探讨C++中的默认成员函数,分析它们的用途、行为以及在实际开发中如何合理利用和控制这些函数。
👓类和对象(2)
1.类的默认成员函数
在C++中,当你定义一个类而没有提供某些成员函数的实现时,编译器会自动为该类生成一些默认的成员函数。这些默认的成员函数包括以下几种:
1.1初始化和清理:
构造函数主要完成初始化工作
析构函数主要完成清理工作
1.2拷贝复制
拷贝构造是使用同类对象初始化创建对象
赋值重载主要是把一个对象赋值给另一个对象
1.3取地址重载
主要是普通对象和const对象取地址,这两个很少会自己实现(可自行了解)
2.构造函数
2.1构造函数概念
什么是构造函数?
构造函数是一个特殊的成员函数,用于初始化对象。每当一个类的对象被创建时,构造函数会自动被调用,以确保对象在使用之前被正确地初始化。不要别构造二字所误导,构造函数不是为对象开辟空间,而是给对象进行初始化。
构造函数的基本特点:
- 名称:构造函数的名字与类名相同。
- 没有返回类型:构造函数没有返回类型,也不需要写
return
语句。 - 自动调用:构造函数在对象创建时自动被调用。
- **重载:**构造函数可以进行重载
- **默认生成:**若用户没有显示定义构造函数,那么编译器会自动生成一个无参的默认构造函数。
以上特点的综合案列:
前三点的代码示例:
#include <iostream>
using namespace std;
//构造函数的相关知识点
class Date
{
public:
//构造函数名字与类名相同
//无返回类型
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
int _year;
int _month;
int _day;
};
int main()
{
Date d1;//实列化对象后会自动调用对象
//因为实列d1对象是没有给实参,所以会用缺省参数进行赋值
cout<<d1._year<<"-"<<d1._month<<"-"<<d1._day<<endl;
Date d2(2024,9,14);//构造时代入实参,会使用实参进行初始化
cout<<d2._year<<"-"<<d2._month<<"-"<<d2._day<<endl;
return 0;
}
输出:
1-1-1
2024-9-14
第四点案列:
#include <iostream>
using namespace std;
class Example
{
public:
Example(int a, int b)
{
cout << "Example(int a,int b)" << endl;
}
Example(double a, double b)
{
cout << "Example(double a, double b)" << endl;
}
};
int main()
{
Example e1(1, 2);
Example e2(1.0, 2.0);
return 0;
}
输出:
Example(int a,int b)
Example(double a, double b)
第五点案列:
#include <iostream>
using namespace std;
class Date
{
public:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
return 0;
}
注:
在部分编译器下,上面的代码不会报错,但在较高的版本的编译器下,会检测到未初始化的错误,使用编译器默认生成的构造函数,成员变量将变成随机值
- **默认构造函数:**无参构造函数、全缺省构造函数、我们不写构造函数时编译器自动生成的构造函数,都是默认构造函数。上面三种默认构造函数只能有一个存在,不能同时存在。
案例:
#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()
{
_year = 2024;
_month = 9;
_day = 14;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
return 0;
}
- **自定义类型成员变量:**编译器自动生成的默认构造函数,对内置类型(如:
int
,double
)的成员变量的初始化不做要求,但对自定义类型(也就是类类型)会调用它的构造函数
案列:
#include <iostream>
using namespace std;
class Myclass
{
public:
Myclass(int x = 10)
{
_x = x;
cout << "内置类型的构造函数" << endl;
}
private:
int _x;
};
class Example
{
private:
int n;
Myclass n1;
};
int main()
{
Example n1;
return 0;
}
输出:
内置类型的构造函数
3.析构函数
在C++中,析构函数(Destructor)是一种特殊的成员函数,当对象的生命周期结束时,它被自动调用,用于清理对象的资源,释放内存,关闭文件等。析构函数的设计对于管理动态内存或其他系统资源的对象尤为重要。
3.1. 析构函数的定义
析构函数的名称与类名相同,但在前面加上一个波浪号 (~
),它没有返回值,也不能接受参数,也就是说析构函数不能被重载。
基本定义格式:
class MyClass {
public:
~MyClass() {
// 清理资源的代码
}
};
3.2. 析构函数的特点
- 函数名:析构函数名是在类名前加上字符~
- 自动调用:当对象生命周期结束时,析构函数会自动调用。
示例:
#include <iostream>
using namespace std;
class Myclass
{
public:
~Myclass()
{
cout << "调用Myclass析构函数" << endl;
}
private:
int a;
};
class Myclass1
{
public:
~Myclass1()
{
cout << "调用Myclass1析构函数" << endl;
}
private:
int a;
};
void test()
{
Myclass1 s2;
//s2的析构函数先调用
//因为s2的生命周期随这函数结束而结束
}
int main()
{
Myclass s1;
//s1的析构函数后调用
//因为s1要等程序结束后生命周期才会结束
test();
return 0;
}
输出:
调用Myclass1析构函数
调用Myclass析构函数
- 无参数、无返回值:析构函数不能有参数,也没有返回值,因此不能被重载。
- **自定义类型成员:**我们不写析构函数,而使用编译器自动生成的析构函数时,该析构函数对内置类型的成员不做处理,对自定义类型成员会调用他们的析构函数。
事例:
#include <iostream>
using namespace std;
class Myclass1
{
public:
~Myclass1()
{
cout << "调用Myclass1析构函数" << endl;
}
private:
int a;
};
class Myclass
{
public:
private:
int a;
Myclass1 s1;
};
int main()
{
Myclass s1;
return 0;
}
输出:
调用Myclass1析构函数
- 即使显示写了析构函数,对于自定义类型成员依然会调用他的析构函数
- **析构顺序:**一个局部有多个对象,C++规定后定义的先析构
4.拷贝构造函数
拷贝构造函数在C++中是一种特殊的构造函数,用于通过另一个同类型对象来初始化新对象。它在对象的值需要被复制时非常有用,特别是在对象中包含动态分配的资源时。
4.1. 拷贝构造函数的定义
拷贝构造函数的定义形式是接受自身类类型的一个 const
引用参数。这样可以避免构造新对象时产生无限递归调用。
定义格式:
class MyClass {
public:
MyClass(const MyClass& other); // 拷贝构造函数声明
};
拷贝构造特点:
1.拷贝构造函数的调用时机
拷贝构造函数在以下几种情况下会被调用:
- 对象以值传递的方式作为函数参数:当对象通过值传递给函数时,会调用拷贝构造函数。
- 对象作为函数的返回值:当函数返回一个对象(按值返回),会调用拷贝构造函数。
- 显式复制对象:当创建一个对象并显式将另一个对象赋给它时,会调用拷贝构造函数。
- 使用对象初始化另一个对象:例如在对象声明时使用等号进行赋值时,拷贝构造函数会被调用。
2.浅拷贝与深拷贝
- 浅拷贝(Shallow Copy)是编译器生成的默认行为,逐个拷贝对象中的每个成员变量。这对于值类型的成员变量是可行的,但如果对象持有指针或动态分配的内存,浅拷贝就可能引发问题。
示列:
#include <iostream>
using namespace std;
class MyClass {
public:
MyClass(int val) {
ptr = new int(val); // 动态分配内存
}
~MyClass() {
delete ptr; // 释放内存
}
// 默认的拷贝构造函数执行浅拷贝
//默认的拷贝构造函数不写编译器会自动生成
private:
int* ptr;
};
int main() {
MyClass obj1(10);
MyClass obj2 = obj1; // 调用默认拷贝构造函数,执行浅拷贝
// 此时 obj1.ptr 和 obj2.ptr 指向同一块内存
return 0;
}
在上例中,obj1
和 obj2
中的指针 ptr
指向同一块内存。如果析构时两个对象分别调用 delete ptr
,会导致 重复释放(double free) 错误。
- 深拷贝(Deep Copy)则需要在拷贝构造函数中手动编写代码,以正确地复制指向的内存等资源,确保每个对象拥有自己的独立资源。
示例:
#include <iostream>
using namespace std;
class MyClass {
public:
MyClass(int val) {
ptr = new int(val); // 动态分配内存
}
~MyClass() {
delete ptr; // 释放内存
}
// 深拷贝:分配新内存并复制内容
MyClass(const MyClass& other) {
ptr = new int(*other.ptr); // 深拷贝
std::cout << "调用深拷贝" << std::endl;
}
int* GetMem()
{
return ptr;
}
private:
int* ptr;
};
int main() {
MyClass obj1(10);
MyClass obj2 = obj1; // 调用深拷贝构造函数
std::cout << "obj1.ptr points to " << obj1.GetMem() << std::endl;
std::cout << "obj2.ptr points to " << obj2.GetMem() << std::endl;
return 0;
}
在深拷贝中,obj1
和 obj2
的 ptr
指针分别指向不同的内存地址,从而避免了重复释放内存的错误。
5.赋值运算符重载
5.1运算符重载
什么是运算符重载?
运算符重载(Operator Overloading)是C++中的一种功能,它允许开发者为用户定义的类型(如类和结构体)重新定义或“重载”现有的运算符,使其适用于这些类型。运算符重载可以使代码更直观、更易读,从而提高代码的可维护性。
运算符重载的基本原则
- 不能创建新的运算符:只能重载已有的运算符。
- 必须保留运算符的优先级和结合性:重载运算符时不能改变其固有的优先级和结合性。
- 至少有一个操作数是用户定义类型:防止对内置类型的运算符进行无意义的重载。
运算符重载的语法
运算符重载可以通过类的成员函数或友元函数来实现。基本语法如下:
成员函数
ReturnType operatorOp(const ClassName& other);
友元函数
friend ReturnType operatorOp(const ClassName& lhs, const ClassName& rhs);
示例:重载常见运算符
重载算术运算符(如+
)
class Complex {
private:
double real;
double imag;
public:
Complex(double r = 0, double i = 0) : real(r), imag(i) {}
// 重载+运算符,作为成员函数
Complex operator+(const Complex& other) const {
return Complex(real + other.real, imag + other.imag);
}
void print() const {
std::cout << real << " + " << imag << "i" << std::endl;
}
};
int main() {
Complex c1(3.0, 4.0);
Complex c2(1.0, 2.0);
Complex c3 = c1 + c2; // 使用重载的+运算符
c3.print(); // 输出:4 + 6i
return 0;
}
重载关系运算符(如==
)
class Complex {
private:
double real;
double imag;
public:
Complex(double r = 0, double i = 0) : real(r), imag(i) {}
// 重载==运算符,作为成员函数
bool operator==(const Complex& other) const {
return (real == other.real) && (imag == other.imag);
}
};
int main() {
Complex c1(3.0, 4.0);
Complex c2(3.0, 4.0);
if (c1 == c2) { // 使用重载的==运算符
std::cout << "c1 and c2 are equal" << std::endl;
}
return 0;
}
重载流插入和流提取运算符(<<
和>>
)
这些运算符通常重载为友元函数。
class Complex {
private:
double real;
double imag;
public:
Complex(double r = 0, double i = 0) : real(r), imag(i) {}
// 友元函数重载<<运算符
friend std::ostream& operator<<(std::ostream& os, const Complex& c) {
os << c.real << " + " << c.imag << "i";
return os;
}
// 友元函数重载>>运算符
friend std::istream& operator>>(std::istream& is, Complex& c) {
is >> c.real >> c.imag;
return is;
}
};
int main() {
Complex c;
std::cout << "Enter a complex number (real and imaginary parts): ";
std::cin >> c; // 使用重载的>>运算符
std::cout << "You entered: " << c << std::endl; // 使用重载的<<运算符
return 0;
}
5.2赋值运算符重载
重载赋值运算符(=
)
重载赋值运算符时,通常需要考虑自赋值、资源管理等问题。
class MyClass {
private:
int* data;
public:
MyClass(int val) {
data = new int(val);
}
~MyClass() {
delete data;
}
// 重载=运算符,作为成员函数
MyClass& operator=(const MyClass& other) {
if (this == &other) {
return *this; // 处理自赋值
}
delete data; // 释放旧内存
data = new int(*other.data); // 分配新内存并复制值
return *this;
}
};
5.3日期类的实现
//Date.cpp文件中
#include "Date.h"
Date::Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
int Date::GetMonthDay(int year, int month)
{
static int arr[13] = { -1,31,28,31,30,31,30,31,31,30,31,30,31 };
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
{
return 29;
}
else
{
return arr[month];
}
}
Date::Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
Date& Date::operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
void Date::Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
bool Date::operator==(const Date& d)
{
if (_year == d._year && _month == d._month && _day == d._day)
{
return true;
}
return false;
}
bool Date::operator>(const Date& d)
{
if (_year > d._year)
{
return true;
}
else if (_year == d._year && _month > d._month)
{
return true;
}
else if (_year == d._year && _month == d._month && _day > d._day)
{
return true;
}
return false;
}
bool Date::operator>=(const Date& d)
{
return *this > d || *this == d;
}
bool Date::operator<(const Date& d)
{
return !(*this >= d);
}
bool Date::operator<=(const Date& d)
{
return !(*this > d);
}
bool Date::operator!=(const Date& d)
{
return !(*this == d);
}
Date& Date::operator+=(int day)
{
if (day < 0)
{
*this -= -day;
}
else
{
_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)
{
Date temp(*this);
temp += day;
return temp;
}
//日期-=天数
Date& Date::operator-=(int day)
{
if (day < 0)
{
*this += -day;
}
else
{
_day -= day;
while (_day < 0)
{
_month--;
if (_month == 0)
{
_year--;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
}
return *this;
}
//日期-天数
Date Date::operator-(int day)
{
Date temp(*this);
temp -= day;
return temp;
}
//前置++
Date& Date::operator++()
{
*this += 1;
return *this;
}
//后置++
Date Date::operator++(int)
{
Date temp(*this);
*this += 1;
return temp;
}
//前置--
Date& Date::operator--()
{
*this -= 1;
return *this;
}
//后置--
Date Date::operator--(int)
{
Date temp(*this);
*this -= 1;
return temp;
}
//日期-日期返回天数
int Date::operator-(const Date& d)
{
int count = 0;
int flag = 1;
Date max(*this);
Date min(d);
if (max < min)
{
max = d;
min = *this;
flag = -1;
}
while (min!=max)
{
count++;
min++;
}
return flag*count;
}
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "-" << d._month << "-" << d._day ;
return out;
}
istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
//Date.h
#include <iostream>
using namespace std;
class Date
{
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
//打印日期
void Print();
//获取某年某月的天数
int GetMonthDay(int year, int month);
//全缺省的构造函数
Date(int year = 1, int month = 1, int day = 1);
//拷贝构造函数
Date(const Date& d);
//赋值运算符重载
Date& operator=(const Date& d);
bool operator==(const Date& d);
bool operator>(const Date& d);
bool operator>=(const Date& d);
bool operator<(const Date& d);
bool operator<=(const Date& d);
bool operator!=(const Date& d);
//日期+=天数
Date& operator+=(int day);
//日期+天数
Date operator+(int day);
//日期-=天数
Date& operator-=(int day);
//日期-天数
Date operator-(int day);
//前置++
Date& operator++();
//后置++
Date operator++(int);
//前置--
Date& operator--();
//后置--
Date operator--(int);
//日期-日期返回天数
int operator-(const Date& d);
private:
int _year;
int _month;
int _day;
};
🥇结语
C++中的默认成员函数为我们简化了许多常见的类操作,尤其是在类的基本功能设计上提供了便捷性。然而,在涉及资源管理、动态内存分配等复杂场景时,我们必须清楚了解编译器生成的默认行为,避免潜在的问题。通过深入掌握默认成员函数的工作机制,程序员可以更灵活地控制对象的构造、赋值和销毁,编写出更加健壮的C++程序。