【C++】类中的6个默认成员函数详解

文章目录

  • 前言
  • 1. 类的六个默认成员函数
  • 2. 构造函数
    • 2.1 概念
    • 2.2 特性
  • 3. 析构函数
    • 3.1 概念
    • 3.2 特性
  • 4. 拷贝构造
    • 4.1 概念
    • 4.2 特征
  • 5. 赋值运算符重载
    • 5.1 运算符重载
    • 5.2 赋值运算符的重载
  • 6. 日期类的实现(综合练习)
  • 7. const修饰的成员
    • 7.1 const修饰类的成员函数
  • 8. 取地址及const取地址操作符重载
  • 总结


前言

提示:这里可以添加本文要记录的大概内容:

注意:重点是前四个默认成员函数。后面两个用的很少

在C++中,类的六个默认成员函数是构造函数、析构函数、拷贝构造函数、拷贝赋值运算符等。这些函数为类提供了基本的生命周期管理和对象复制行为。深入理解和善用这些默认成员函数是每个C++程序员的基本功,也是构建可靠、高效代码的关键一步。本博客将探讨这六个默认成员函数的核心概念,旨在帮助读者更好地利用C++中的面向对象编程特性。

1. 类的六个默认成员函数

如果一个类中什么成员也没有,简称为空类。
例如:

class stack{};

但是空类中真的什么也没有吗?事实上,并不是,任何一个类在我们不写的情况下,编译器都会自动生成下面的六个默认成员函数。
【C++】类中的6个默认成员函数详解_第1张图片

2. 构造函数

2.1 概念

对于如下的日期类

#include 
using namespace std;
class Date
{
public:
	void setDate(int year,int month,int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << " - " << _month << " - " << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	d1.setDate(2024, 2, 2);
	d1.Print();
	return 0;
}

如果我们想要初始化对象d1,可以通过setDate方法给对象设置内容,但是如果每次创建对象都调用该方法设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?
答案就是利用构造函数。构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次

2.2 特性

构造函数是特殊的成员函数,需要注意的是,构造函数的虽然名称叫构造,但是需要注意的是构造函数的主要任务并不是开空间创建对象,而是初始化对象
其特征如下:

  • 函数名和类名相同
  • 无返回值
  • 对象实例化时编译器自动调用对应的构造函数
  • 构造函数可以重载
// 1.无参构造函数
 Date ()
 {}
 
 // 2.带参构造函数
 Date (int year, int month , int day )
 {
 _year = year ;
 _month = month ;
 _day = day ;
 }
  • 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦我们显式定义之后,编译器将不再生成。
  • 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数有且只有一个。注意:自己实现的无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数
//自己实现的无参构造函数
Date(){};

//全缺省构造函数
Date(int year=0,int month=1,int day =1)
{}

//我们没写编译器默认生成的构造函数
...

关于编译器生成的默认无参构造函数,很多同学会有疑惑:在我们不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默认构造函数,但是d对象的 _year / _month / _day依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用????

答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语法已经定义好的类型:如int/char…,自定义类型就是我们使用class/struct/union自己定义的类型,看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用它的构造成员函数

#include 
using namespace std;
class Time
{
public:
	Time()
	{
		cout << " Time() " << endl;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
public:

private:
	int _year;
	int _month;
	int _day;
	Time _t;
};
int main()
{
	Date d1;
	return 0;
}
  • 成员变量的命名风格
// 所以我们一般都建议这样
class Date
{
public:
	Date(int year)
	{
		_year = year;
	}

private:
	int _year;
};

// 或者这样。
class Date
{
public:
	Date(int year)
	{
		m_year = year;
	}
private:
	int m_year;
};
// 其他方式也可以的,主要看公司要求。一般都是加个前缀或者后缀标识区分就行。

3. 析构函数

3.1 概念

析构函数(Destructor)是C++类中的一种特殊成员函数,用于在对象生命周期结束时执行清理和释放资源的操作。析构函数的名称与类名相同,前面加上波浪号 ~没有参数,也没有返回值。它在对象被销毁时自动调用,通常用于释放动态分配的内存、关闭文件、释放资源等清理工作。

析构函数的基本形式如下:

class MyClass {
public:
    // 构造函数
    MyClass() {
        // 构造函数的初始化操作
        // ...
    }

    // 析构函数
    ~MyClass() {
        // 析构函数的清理操作
        // ...
    }
};

析构函数在对象生命周期结束时被自动调用,它的执行时机包括:

  1. 当对象离开其作用域(局部对象)。
  2. 当对象是动态分配的,通过 new 创建,并在使用 delete 进行显式销毁时。
  3. 当对象是成员对象,而其包含的对象生命周期结束时。

示例:

#include 

class Example {
public:
    // 构造函数
    Example() {
        std::cout << "Constructor called." << std::endl;
    }

    // 析构函数
    ~Example() {
        std::cout << "Destructor called." << std::endl;
    }
};

int main() {
    // 创建局部对象
    Example obj1;

    // 创建动态分配的对象
    Example* obj2 = new Example();

    // 当 obj1 离开作用域时,其析构函数被调用
    // 输出:Destructor called.

    delete obj2; // 显式调用 delete,触发 obj2 的析构函数
    // 输出:Destructor called.

    return 0;
}

在上述示例中,Example 类的析构函数在对象生命周期结束时被自动调用,无论是在离开作用域、动态分配的对象被 delete 调用,还是在程序结束时。

3.2 特性

析构函数是特殊的成员函数。
其特征如下:

  1. 析构函数名是在类名前面加上字符~
  2. 无参数、无返回值
  3. 一个类中有且只有一个析构函数。若未显示定义,系统会自动生成默认的析构函数。
  4. 在对象生命周期结束时,C++编译系统自动调用析构函数。
#include 
using namespace std;
class stack
{
public:
	stack(int capacity = 10)
	{
		_arr = (int*)malloc(sizeof(int) * capacity);
		_size = 0;
		_capacity = capacity;
	}
	~stack()//析构函数
	{
		cout << "析构函数" << endl;
		free(_arr);
		_arr = NULL;
		_size = 0;
		_capacity = 0;
	}
private:
	int* _arr;
	int _size;
	int _capacity;
};
int main()
{
	stack s1;
	return 0;
}
  1. 关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器生成的默认析构函数,对会自定类型成员调用它的析构函数
#include 
using namespace std;
class String
{
public:
	String(const char* str = "dzj")
	{
		_str = (char*)malloc(strlen(str) + 1);
		strcpy(_str, str);
	}
	~String()
	{
		cout << "~String()" << endl;
		free(_str);
	}
private:
	char* _str;
};

class Person
{
private:
	String _name;
	int _age;
};

int main()
{
	Person p;
	return 0;
}

4. 拷贝构造

4.1 概念

拷贝构造函数(Copy Constructor)是C++中的一种特殊成员函数,用于创建一个新对象并使用已有对象的值来初始化它。拷贝构造函数通常在以下情况下被调用

  1. 对象以值传递的方式传递给函数。
  2. 对象作为函数的返回值返回。
  3. 通过一个对象初始化另一个对象。

拷贝构造函数的声明和实现形式如下:

class MyClass {
public:
    // 拷贝构造函数
    MyClass(const MyClass& other) {
        // 在此处进行成员变量的复制或资源的拷贝
        // ...
    }
};

其中,const MyClass& other 表示传递的参数是一个常引用,防止在拷贝过程中对原对象进行修改。

示例:


#include 
class Example {
public:
    // 构造函数
    Example(int value) {
        _data = value;
        std::cout << "Constructor called." << std::endl;
    }

    // 拷贝构造函数
    Example(const Example& other) {
        _data = other._data;
        std::cout << "Copy Constructor called." << std::endl;
    }

    // 获取数据的成员函数
    int getData() const {
        return _data;
    }

private:
    int _data;
};

int main() {
    // 创建对象 obj1
    Example obj1(42);

    // 使用拷贝构造函数创建新对象 obj2,其值与 obj1 相同
    Example obj2 = obj1;

    // 输出 obj2 的数据
    std::cout << "Data in obj2: " << obj2.getData() << std::endl;

    return 0;
}

在上述示例中,Example 类的拷贝构造函数被调用,用于创建新对象 obj2 并将其初始化为已有对象 obj1 的副本。这种拷贝构造的行为可以确保在创建新对象时,其初始值与已有对象相同。

4.2 特征

  1. 拷贝构造函数是构造函数的一个重载形式
  2. 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用

如果我们不采用引用传参,编译器在编译阶段就会检查出问题,来看下面的代码

#include 
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;
    }
private:
    int _year;
    int _month;
    int _day;
};
int main()
{
    Date d1;
    Date d2(d1);
    return 0;
}

那么原因就是会出现无穷递归调用的问题,在传参的过程中实际上又是一次调用拷贝构造,那么久无穷递归下去了,接下来看图解!!
【C++】类中的6个默认成员函数详解_第2张图片
【C++】类中的6个默认成员函数详解_第3张图片
3. 若未显示定义,系统会生成默认的拷贝构造函数。默认的拷贝构造函数按内存存储字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝。(后面讲到的赋值运算符重载也是存在浅拷贝的问题)

#include 
class Date
{
public:
    Date(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
private:
    int _year;
    int _month;
    int _day;
};
int main()
{
    Date d1;
    Date d2(d1);// 这里d2调用的默认拷贝构造完成拷贝,d2和d1的值也是一样的。
    return 0;
}
  1. 如果编译器默认生成的拷贝构造函数已经可以完成字节序的值拷贝了,那么我们为什么还要学习它并且自己去实现???在上述日期类中是没有必要的,再看一段代码,如下的情况能否采用值拷贝??
#include 
using namespace std;
class stack
{
public:
	stack(int capacity = 10)
	{
		_arr = (int*)malloc(sizeof(int) * capacity);
		_size = 0;
		_capacity = capacity;
	}
	~stack()//析构函数
	{
		cout << "析构函数" << endl;
		free(_arr);
		_arr = NULL;
		_size = 0;
		_capacity = 0;
	}
private:
	int* _arr;
	int _size;
	int _capacity;
};

int main()
{
	stack s1;
	stack s2(s1);
	return 0;
}

这里虽然编译可以通过,但是会出现运行时错误。
【C++】类中的6个默认成员函数详解_第4张图片
那么出现运行时错误的原因就是:
编译器默认的拷贝构造是值拷贝,那么两个对象的_arr会指向同一块空间,最后在进行资源清理时,会调用两次析构函数,这一块空间会被释放两次,但是这是不被允许的!!

5. 赋值运算符重载

5.1 运算符重载

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也是具有返回值类型、函数名字以及参数列表,其返回值类型与参数列表和普通的函数类似。

函数的名字:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)

注意:

  • 不能通过连接其他符号来创建新的操作符:比如operator@ ,重载操作符必须有一个类类型或者枚举类型的操作数
  • 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不能改变其含义, 作为类成员的重载函数时,其形参看起来比操作数数目少1
  • 成员函数的操作符有一个默认的形参this,限定为第一个形参
  • .* 、:: 、sizeof 、?: 、. 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
    示例(operator==的重载):
#include 
using namespace std;
class Date
{
public:
	//全缺省的构造函数
	Date(int year = 0,int month = 1,int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//operator==运算符重载
	// bool operator==(Date* this, const Date& d2)
	// 这里需要注意的是,左操作数是this指向的调用函数的对象
	bool operator==(const Date&d)
	{
		return _year == d._year &&
			_month == d._month &&
			_day == d._day;

	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2024, 2, 4);
	Date d2 = d1;
	cout << (d1 == d2) << endl;
	return 0;
}

5.2 赋值运算符的重载

#include 
using namespace std;
class Date
{
public:
	//全缺省的构造函数
	Date(int year = 0,int month = 1,int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//operator==运算符重载
	// bool operator==(Date* this, const Date& d2)
	// 这里需要注意的是,左操作数是this指向的调用函数的对象
	bool operator==(const Date&d)
	{
		return _year == d._year &&
			_month == d._month &&
			_day == d._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;
};
int main()
{
	Date d1(2024, 2, 4);
	Date d2(2023, 1, 1);
	d1 = d2;//注意这里不是拷贝构造而是调用赋值运算符重载,因为d1、d2之前早就已经被初始化好了
	d1.Print();
	return 0;
}

赋值运算符主要有四点:

  1. 参数类型
  2. 返回值
  3. 检测是否自己给自己赋值
  4. 返回*this
  5. 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝

如果我们屏蔽我们自己实现的赋值运算符重载,那么会发现编译器默认生成的也能完成任务。

#include 
using namespace std;
class Date
{
public:
	//全缺省的构造函数
	Date(int year = 0,int month = 1,int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//operator==运算符重载
	// bool operator==(Date* this, const Date& d2)
	// 这里需要注意的是,左操作数是this指向的调用函数的对象
	//bool operator==(const Date&d)
	//{
	//	return _year == d._year &&
	//		_month == d._month &&
	//		_day == d._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;
};
int main()
{
	Date d1(2024, 2, 4);
	Date d2(2023, 1, 1);
	d1 = d2;//注意这里不是拷贝构造而是调用赋值运算符重载,因为d1、d2之前早就已经被初始化好了
	d1.Print();
	return 0;
}

那么编译器生成的默认赋值重载函数已经可以完成字节序的值拷贝了,我们还需要自己实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?

#include 
using namespace std;
class stack
{
public:
	stack(int capacity = 10)
	{
		_arr = (int*)malloc(sizeof(int) * capacity);
		_size = 0;
		_capacity = capacity;
	}
	~stack()//析构函数
	{
		cout << "析构函数" << endl;
		free(_arr);
		_arr = NULL;
		_size = 0;
		_capacity = 0;
	}
private:
	int* _arr;
	int _size;
	int _capacity;
};

int main()
{
	stack s1;
	stack s2;
	s2=s1;
	return 0;
}
// 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。

这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝(后面在讲)去解决。

6. 日期类的实现(综合练习)

具体实现见此代码

class Date
{
public:
 // 获取某年某月的天数
 int GetMonthDay(int year, int month)
 {
	 static int days[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
	 int day = days[month];
 if (month == 2 &&((year % 4 == 0 && year % 100 != 0) || (year%400 == 0)))
 {
	 day += 1;
 }
	 return day;
 }
 
 // 全缺省的构造函数
 Date(int year = 1900, int month = 1, int day = 1);
 // 拷贝构造函数
 // d2(d1)
 Date(const Date& d);
 
 // 赋值运算符重载
 // d2 = d3 -> d2.operator=(&d2, d3)
 Date& operator=(const Date& d);
 // 析构函数
 ~Date();
 // 日期+=天数
 Date& operator+=(int day);
 // 日期+天数
 Date operator+(int day);
 // 日期-天数
 Date operator-(int day);
 // 日期-=天数
 Date& operator-=(int day);
 // 前置++
 Date& operator++();
 // 后置++
 Date operator++(int);
 // 后置--
 Date operator--(int);
 // 前置--
 Date& operator--();
 
 // >运算符重载
 bool operator>(const Date& d);
 // ==运算符重载
 bool operator==(const Date& d);
 // >=运算符重载
 inline bool operator >= (const Date& d);
 
 // <运算符重载
 bool operator < (const Date& d);
 // <=运算符重载
 bool operator <= (const Date& d);
 // !=运算符重载
 bool operator != (const Date& d);
 // 日期-日期 返回天数
 int operator-(const Date& d);
private:
	 int _year;
	 int _month;
	 int _day;
};

7. const修饰的成员

7.1 const修饰类的成员函数

将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
【C++】类中的6个默认成员函数详解_第5张图片
我们来看看下面的代码

#include 
using namespace std;
class Date
{
public:
	void Display()//Date* this
	{
		cout << "Display ()" << endl;
		cout << "year:" << _year << endl;
		cout << "month:" << _month << endl;
		cout << "day:" << _day << endl << endl;
	}

	//void Display() const//const Date* this
	//{
	//	cout << "Display () const" << endl;
	//}
Date(int year = 0, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
   }
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};
int main()
{
	Date d1;
	d1.Display();

	const Date d2;
	d2.Display();//这里会报错,因为d2是被const修饰的,d2&传参给this类型是const Date*,而this的类型是Date*this,属于权限的放大,不能编译通过
}

这里会报错,因为d2是被const修饰的,&d2传参给this类型是const Date*,而this的类型是Date*this,属于权限的放大,不能编译通过

请思考下面的几个问题:

  1. const对象可以调用非const成员函数吗?答案:不可以
  2. 非const对象可以调用const成员函数吗? 答案:可以
  3. const成员函数内可以调用其它的非const成员函数吗? 答案:不可以
  4. 非const成员函数内可以调用其它的const成员函数吗? 答案:可以

8. 取地址及const取地址操作符重载

这两个默认成员函数一般不用重新定义 ,编译器默认会生成。

class Date
{ 
public :
 Date* operator&()
 {
 	return this ;
 }
 
 const Date* operator&()const
 {
 	return this ;
 }
private :
 	int _year ; // 年
 	int _month ; // 月
 	int _day ; // 日
};

这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容

总结

C++中的类默认成员函数是面向对象编程的支柱,它们负责处理对象的构建、销毁和复制等关键任务。通过熟练掌握这些函数的使用方式和设计原则,我们能够提高代码的可维护性和性能。精心设计这六个默认成员函数,可以使代码更具清晰性和健壮性。在C++的世界里,深刻理解并熟练运用这些默认成员函数,将有助于写出高效、安全的类和程序。

你可能感兴趣的:(C++,c++,开发语言,类和对象,默认成员函数)