首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >零基础吃透 C++ 类默认成员函数:万字核心知识点汇总

零基础吃透 C++ 类默认成员函数:万字核心知识点汇总

作者头像
say-fall
发布2026-01-15 10:35:50
发布2026-01-15 10:35:50
1200
举报
欢迎来到say-fall的文章
在这里插入图片描述
在这里插入图片描述

🌈 say-fall:个人主页 🚀 专栏:《手把手教你学会C++》 | 《C语言从零开始到精通》 | 《数据结构与算法》 | 《小游戏与项目》 💪 格言:做好你自己,才能吸引更多人,与他们共赢,这才是最好的成长方式。


前言:

在C++面向对象学习中,类的默认成员函数是绕不开的核心知识点,也是夯实代码基础的关键。很多初学者会困惑:为什么编译器能自动生成函数,我们还需要手动实现?这些函数各自承担什么角色,又有哪些容易踩坑的细节?本文就从默认成员函数的基本概念出发,逐一拆解构造、析构、拷贝构造等六大函数的规则与用法,结合具体代码案例帮大家理清逻辑。最后还会通过完整的日期类实现,让理论落地实践,助力大家真正吃透这部分内容。

正文:

1. 类的默认成员函数

  • 用户在类中不去显示实现,编译器会自动生成的成员函数称为默认成员函数。
在这里插入图片描述
在这里插入图片描述

既然编译器能生成这些函数,那程序员在使用这些功能的时候还需要去写他们吗? 主要是因为一些编译器生成的函数不能满足我们的需求,这个时候就需要我们自己去实现,下面我们来一次看一下这些函数具体的规则。

2. 构造函数

构造函数是做初始化工作的,它有以下几个特点

定义方面:

  1. 函数名与类名相同
  2. 无返回值(并非void,而是什么都没有)
  3. 构造函数可以重载(可以在类里面写好几个构成函数重载的构造函数)
  4. 如果类中没有显示定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,显示定义就不再生成
代码语言:javascript
复制
#include<iostream>
using namespace std;
class Date
{
public:
	// 1.⽆参构造函数 
	Date()//这里显示定义构造函数,类名与函数名相同,没有返回值
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}
	// 2.带参构造函数 
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	// 3.全缺省构造函数 
	Date(int year = 1, int month = 1, int day = 1)
	{
	_year = year;
	_month = month;
	_day = day;
	}
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

使用方面:

  1. 对象实例化会自动调用对应构造函数
  2. 无参构造函数、全缺省构造函数、编译器默认生成的构造函数,都叫做默认构造函数。但是这三个函数有且仅有一个存在,不能同时存在。这些不传实参就可以调用的构造叫做默认构造
  3. 对于编译器默认生成的函数,不会对内置类型进行初始化,只是对类、结构体这样的自定义类型进行默认初始化。

如果留下三个构造中的第⼆个带参构造,第⼀个和第三个注释掉 然后运行Date d1;// 调⽤默认构造函数 会导致编译报错:error C2512: “Date”: 没有合适的默认构造函数可⽤ 因为这里的Date d1是没有传值的,使用默认构造,但是三个默认构造类里面一个都没有

代码语言:javascript
复制
int main()
{
 Date d1; // 调⽤默认构造函数 
 Date d2(2025, 1, 1); // 调⽤带参的构造函数 
 // 注意:如果通过⽆参构造函数创建对象时,对象后⾯不⽤跟括号,否则编译器⽆法 
 // 区分这⾥是函数声明还是实例化对象 
 // warning C4930: “Date d3(void)”: 未调⽤原型函数(是否是有意⽤变量定义的?) 
 Date d3();
 d1.Print();
 d2.Print();
 return 0;
}

3. 析构函数

析构函数是 “销毁函数” ,析构函数和栈和队列里面用的Destroy()函数类似,是用来销毁(释放)申请内存资源的函数,举个例子说上方的Date类并不许需要析构函数。

要销毁动态申请内存而不用销毁内置类型的原因是,动态申请内存在堆上,而函数内的内置类型在栈上,函数调用完会销毁栈帧

析构函数也有以下几个特点:

定义方面:

  1. 函数名是类名前加 ~
  2. 无参数返回值,与构造函数相同
  3. 一个类只能有一个析构函数,如果没有显示定义的话,系统会自动生成默认的析构函数
  4. 编译器自动生成的析构函数不会对内置类型成员进行处理,也没有必要进行处理
  5. 类中没有申请资源时,析构函数可以不写,使用默认的析构函数,如Date;如果存在申请资源,那就必须要写析构函数,如Stack;但是如果类里面有包含其他类型,而且其他类型全部有可用的析构函数,如MyQueue

这里引入MyQueue的类定义代码:

代码语言:javascript
复制
typedef int STDataType;
class Stack
{
public:
	Stack(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}
	~Stack()
	{
		cout << "~Stack()" << endl;
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};


// 两个Stack实现队列 
class MyQueue
{
public:
	//编译器默认⽣成 MyQueue的析构函数调⽤了Stack的析构,释放的Stack内部的资源 
	// 就算在这里显⽰写析构,return时候也会⾃动调⽤Stack的析构 
 /*
 ~MyQueue()
 {}
 */
private:
	Stack pushst;
	Stack popst;
};
int main()
{
	Stack st;
	MyQueue mq;
	return 0;
}

值得注意的是,如果前面Stack没有显示定义析构函数的话,MyQueue会生成析构函数的,但是调用stack的默认析构函数时并没有处理申请的内存资源,也就是说会造成内存泄漏,注意这里并不会报错(并非编译错误而是内存泄漏)

使用方面:

  1. 对象生命周期结束时,自动调用析构函数(就算没有手动调用)
  2. 一个局部域的多个对象,C++规定后定义的先析构(栈的性质)

4. 拷贝构造函数

  • 形如:
代码语言:javascript
复制
Date(const Date& d)
{
 _year = d._year;
 _month = d._month;
 _day = d._day;
 }

拷贝构造函数实际上是构造函数的重载,也就是说,拷贝构造函数是一个特殊的构造函数

拷贝构造函数的第一个参数是自身类类型的引用,并且其他参数都有默认值

拷贝构造函数是用来将一个类直接赋值给另一个类的,下面有关特点展开叙述:

  1. 拷贝构造函数是构造函数的重载
  2. 拷贝构造的第一个参数 必须 是自身类类型的引用

那么如果是传值传参的话会如何呢? 首先需要说明的一点是:

c++规定传值传参会引发拷贝构造:这是为了处理C语言中类似memcpy函数直接浅拷贝(一个字节一个字节拷贝),而如果拷贝的是含有指针的类类型或者是结构体类型的话,会造成拷贝出来的结果中也指向了同一块空间,只是变量名不同;而这又会导致析构时候连续释放两块相同空间会造成程序崩溃。

在这里插入图片描述
在这里插入图片描述

如果一个非拷贝构造函数去传值传参,他会在拷贝构造里去拷贝一个数据:

代码语言:javascript
复制
void Copy(Date d) { // 重点:传值传参
        this->_year = d._year;
        this->_month = d._month;
        this->_day = d._day;
        cout << "void Copy(Date d):普通拷贝函数调用(传值参数)" << endl;
    }//这会很正常的调用

但是如果拷贝构造函数去传值传参会导致如图无限递归:

在这里插入图片描述
在这里插入图片描述

3. 和其他默认成员函数一样,如果拷贝构造没有显式定义,那么会自动生成拷贝函数,自动生成的拷贝构造对内置类型变量会浅拷贝,对自定义类型会调用他的拷贝构造(myqueue调用stack

  1. 什么时候需要写拷贝构造:对于三种类成员变量:datastackmyqueuedata内只有内置类型,不需要写拷贝构造、stack需要程序员深拷贝、myqueue如果存在stack就不需要自己定义 小技巧:析构和拷贝构造函数的显式定义总是一起出现

拷贝构造使用时候有两种写法:

代码语言:javascript
复制
//写法一
Date d3(d1);
d2.Print();
//写法二
Date d4 = d1;
d2.Print();

5. 赋值运算符重载

5.1 运算符重载

C++中有函数重载,同样允许我们通过运算符重载的形式指定新的含义。C++规定类类型对象使用运算符时,必须转变成调用对应运算符重载,如果没有对应运算符重载,则会编译错误。

运算重载符是具有特殊名字的函数,名字由 函数返回类型 operator[运算符号](参数) 组成

重载运算符函数的参数个数必须和原符号的参数个数一样多,对于二元运算符来说第一个位于运算符左边,第二个参数位于运算符右边

由于成员函数第一个参数是隐式的this指针,重载运算符函数是成员函数的时候,参数少一个

重载运算符以后新运算符的优先性和结合性不变,并且也不能重载非法的符号

.*→成员函数的解引用 ::→作用域限定符 sizeof→计算变量的字节数 ?:→三目操作符 .→成员访问操作符这五个运算符不能重载,而且重载操作符必须有一个是类类型

这里介绍一下.*符号、函数指针和函数指针typedef 函数指针和typedef函数指针使用

    1. 无别名:直接使用函数指针
  • 形式:[返回值类型](*pf)(参数列表)//这里的pf类似形参
  • 样例:
代码语言:javascript
复制
#include <iostream>
using namespace std;

// 1. 回调函数1:求和
int sum(const int arr[], int len) {
    int total = 0;
    for (int i = 0; i < len; ++i) {
        total += arr[i];
    }
    return total;
}

// 2. 回调函数2:找最大值
int maxVal(const int arr[], int len) {
    int m = arr[0];
    for (int i = 1; i < len; ++i) {
        m = (arr[i] > m) ? arr[i] : m;
    }
    return m;
}

// 3. 数组处理函数:参数直接写函数指针类型(无别名,冗长)
void processArray(const int arr[], int len, int (*callback)(const int[], int)) {
    int result = callback(arr, len);
    cout << "结果:" << result << endl;
}

int main() {
    int arr[] = {1, 3, 5, 7, 9};
    int len = sizeof(arr) / sizeof(arr[0]);

    // 传递回调函数(函数名隐式转换为函数指针)
    processArray(arr, len, sum);    // 求和:25
    processArray(arr, len, maxVal); // 找最大值:9

    return 0;
}

    1. 有别名:用 typedef 给函数指针起别名
  • 形式:[返回值类型](*PF)(参数列表)//这里的PF是别名
  • 样例:
代码语言:javascript
复制
#include <iostream>
using namespace std;

// 方式1:typedef 定义函数指针别名(兼容C)
// typedef int (*Callback)(const int[], int);

// 方式2:using 定义别名(C++11+推荐,语法更清晰)
using Callback = int (*)(const int[], int);

// 1. 回调函数1:求和(与无别名版一致)
int sum(const int arr[], int len) {
    int total = 0;
    for (int i = 0; i < len; ++i) {
        total += arr[i];
    }
    return total;
}

// 2. 回调函数2:找最大值(与无别名版一致)
int maxVal(const int arr[], int len) {
    int m = arr[0];
    for (int i = 1; i < len; ++i) {
        m = (arr[i] > m) ? arr[i] : m;
    }
    return m;
}

// 3. 数组处理函数:用别名替代冗长的函数指针类型
void processArray(const int arr[], int len, Callback callback) {
    int result = callback(arr, len);
    cout << "结果:" << result << endl;
}

int main() {
    int arr[] = {1, 3, 5, 7, 9};
    int len = sizeof(arr) / sizeof(arr[0]);

    // 传递回调函数(用法不变,可读性更高)
    processArray(arr, len, sum);    // 求和:25
    processArray(arr, len, maxVal); // 找最大值:9

    // 额外优势:用别名直接定义函数指针变量
    Callback funcPtr = sum;
    cout << "单独调用回调(sum):" << funcPtr(arr, len) << endl; // 25

    funcPtr = maxVal;
    cout << "单独调用回调(maxVal):" << funcPtr(arr, len) << endl; // 9

    return 0;
}

以上两个样例由AI生成

对于普通函数正常去指针PF pf = func,因为函数名本来就是指针 对于成员函数,c++规定前面要加&:PF pf = &func 普通函数指针的使用有两种方式:pf(参数)(*pf)(参数) 而成员函数就需要解引用:d1.*pf 这里用到了.*,这个符号可以理解为成员函数的解引用

  • 下面还有几个特殊的运算符重载:
  1. 在重载++和–这类符号时,有前置和后置之分,由于在类的++/–时一般前置用的比较多,所以c++规定后置++重载时,增加一个int形参,跟前置++做区分。
  2. 重载一些运算符时,要看其在类内部有没有意义,例如说:在日期类中,d1 - d2有意义,是时间的差,而d1 + d2就没有意义,可以根据实际情况来赋值
  3. 重载<<和>>这两个符号时候,需要重载为全局函数,原因是成员函数第一个参数默认是this指针,而<<左操作数是cout,>>左操作数是cin,不符合操作逻辑,所以放在外面。 注意重载操作符的最后一个点:如果重载操作符函数要在类域外定义的话会有访问私有成员变量的问题,解决方案一般是把重载操作符函数放在类域内部,当然也有.get()等方法,但是不推荐。对于实在无法放在类内部的函数如<</>>,可以使用友元函数,关于友元函数我们下一篇文章详细讲解。
5.2 赋值运算符重载

赋值运算符重载是⼀个默认成员函数,⽤于完成两个已经存在的对象直接的拷⻉赋值,这⾥要注意跟拷⻉构造区分,拷⻉构造⽤于⼀个对象 拷⻉初始化 给另⼀个要创建的对象,即拷贝重载是携带初始化的,而赋值运算符重载必须在使用前初始化对象。

代码语言:javascript
复制
 Date d1(2024, 7, 5);
 Date d2(d1);
 //以上是拷贝构造
 //以下是赋值运算符重载
 Date d3(2024, 7, 6);
 d1 = d3;
  • 赋值运算符重载的特点
  1. 赋值运算符重载是一个运算符重载,规定必须重载为成员函数。它的参数 建议 写成const当前类类型引用,首先是它的参数并不修改,所以添加const,其次传值传参会有拷贝,所以用引用。
  2. 有返回值,建议写成当前类类型引用,同样是减少拷贝,提高效率,有返回值是为了支持连续赋值的场景(=具有右连续性)。
  3. 没有显示实现时,作为默认成员函数,也会自动生成一个默认的,其和默认拷贝构造类似,对内置类型会完成浅拷贝,自定义类型会调用他的赋值重载函数,也就是说,什么时候需要写的规则也是一样的。
  4. 与拷贝构造同理:对于三种类成员变量:datastackmyqueuedata内只有内置类型,不需要写、stack需要程序员深拷贝、myqueue如果存在stack就不需要自己定义 小技巧:析构和赋值重载的显式定义总是一起出现

6. 取地址运算符重载

6.1 const 成员函数

const修饰的成员函数称为const成员函数,const修饰成员函数放到成员函数的参数列表的后面。 const修饰的实际上是该成员函数隐含的this指针,表明在成员函数中不能对类的任何成员对象做出更改。 在成员函数中,以Print为例:void Print(Date* const this),实际内部是这个样子的,其中的 const是修饰this本身的,而如果用const修饰Print函数:void Print(Date* const this) const,其实际上是这样子的:void Print(const Date* const this),新增添的这个const是修饰*this指针的。

在这里插入图片描述
在这里插入图片描述
6.2 取地址运算符重载

取地址运算符重载分为普通的取地址运算符重载和const取地址运算符重载,一般情况下编译器默认生成的取地址运算符就可以使用,对于一些特殊情况:例如不想让使用者得到实际的地址,这个时候我们可以在取地址操作符重载函数内部去返回空值或者随便返回一个地址。

  • 以上就是c++中常见的六种默认成员函数了,最近一直在忙期末考试的事情,更新比较慢。

7.日期类的实现

下面基于以上六种默认成员函数和之前了解过的知识点写一个日期类

代码语言:javascript
复制
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include<iostream>
#include<cassert>

using namespace std;

class Date
{
	//友元函数声明
	friend ostream& operator<<(ostream&, const Date& d);
	friend istream& operator>>(istream& in, Date& d);
public:
	Date(int year = 1900, int month = 1, int day = 1);
	void Print() const;
	
	//获取月份对应天数,同时声明放在函数内部,默认inline
	int getmonthday(int year, int month)
	{
		static int day_array[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;
		}
		return day_array[month];
	}

	bool CheckDate();

	bool operator<(const Date& d) const;
	bool operator<=(const Date& d) const;
	bool operator>(const Date& d) const;
	bool operator>=(const Date& d) const;
	bool operator==(const Date& d) const;
	bool operator!=(const Date& d) const;

	// d1 += 天数 
	Date& operator+=(int day);
	Date operator+(int day);

	// d1 -= 天数 
	Date& operator-=(int day);
	Date operator-(int day);

	// d1 - d2
	int operator-(const Date& d);

	Date& operator++();
	Date operator++(int);

	Date& operator--();
	Date operator--(int);
private:
	int _year;
	int _month;
	int _day;
};
  • Date.cpp
代码语言:javascript
复制
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include"Date.h"


//打印函数
void Date::Print() const
{
	cout << _year << "年" << _month << "月" << _day << "日" << endl;
}


//检查日期
bool Date::CheckDate()
{
	if (_month < 1 || _month>12 || _day< 1 || _day>getmonthday(_year,_month))
	{
		return 0;
	}
	else
	{
		return 1;
	}
}


//重载 <
bool Date::operator<(const Date& d) const
{
	if(* this == d)
	{
		return false;
	}
	if (_year != d._year)
	{
		return _year < d._year;
	}
	if (_month != d._month)
	{
		return _month < d._month;
	}
	if (_day != d._day)
	{
		return _day < d._day;
	}
}


//重载 <=
bool Date::operator<=(const Date& d) const
{
	return *this == d || *this < d;
}


//重载 >
bool Date::operator>(const Date& d) const
{
	return !(*this <= d);
}


//重载 >= 
bool Date::operator>=(const Date& d) const
{
	return !(*this < d);
}


//重载 ==
bool Date::operator==(const Date& d) const
{
	return _year == d._year
		&& _month == d._month
		&& _day == d._day;
}


//重载 !=
bool Date::operator!=(const Date& d) const
{
	return !(*this == d);
}


//构造函数
Date::Date(int year, int month, int day)
{
	_year = year;
	_month = month;		
	_day = day;
	if (!CheckDate())
	{
		cout << "输入非法" << endl;
	}
}



//重载 +=
Date& Date::operator+=(int day)
{
	if (day < 0)
	{
		return *this -= -day; // 负天数转成减正数
	}
	_day += day;
	while (_day > Date::getmonthday(_year, _month))
	{
		_day -= Date::getmonthday(_year, _month);
		_month++;
		if (_month == 13)
		{
			_year++;
			_month = 1;
		}
	}
	return *this;
}


//重载 +
//可以复用+=来实现+,也可以复用+来实现+=,但是用+实现+=多了三次拷贝构造
Date Date::operator+(int day)
{
	Date tmp = *this; //拷贝构造,将d1的日期拷贝到tmp上去
	tmp += day;
	return tmp;
}


//重载 -=
Date& Date::operator-=(int day)
{
	if (day > 0)
	{
		_day -= day;
		while (_day < 1)
		{
			_month--;
			if (_month == 0)
			{
				_month = 12;
				_year--;
			}
			_day += getmonthday(_year, _month);
		}
		return *this;
	}
	else
	{
		day = -day;
		*this += day;
		return *this;
	}
}


//重载 -
Date Date::operator-(int day)
{
	Date tmp = *this; //拷贝构造
	tmp -= day;
	return tmp;
}


// d1 - d2
//int Date::operator-(const Date& d)
//{
//	if (*this < d)
//	{
//		assert("用小日期减大日期无意义");
//		return -1;
//	}
//	int count = 0;//闰年个数
//	for (int year = d._year;year < _year;year++)
//	{
//		if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)
//		{
//			count++;
//		}
//	}
//	int nums_day = 365*(_year - d._year) + count;//天数
//	int month = _month - 1;
//	int day = _day - 1;
//	for (int i = 1; i <= month; i++)
//	{
//		day += getmonthday(_year, i);
//	}
//	month = d._month - 1;
//	day -= (d._day - 1);
//	for (int i = 1; i <= month; i++)
//	{
//		day -= getmonthday(d._year, i);
//	}
//	return nums_day + day;
//}


//重载 d1 - d2
int Date::operator-(const Date& d)
{
	int flag = 1;
	Date max = *this;
	Date min = d;
	if (*this < d)
	{
		flag = -1;
		max = d;
		min = *this;
	}
	int n = 0;
	while (min != max)
	{
		++min;
		++n;
	}
	return n * flag;
}


//重载前置++
Date& Date::operator++()
{
	*this += 1;
	return *this;
}


//重载后置++
Date Date::operator++(int)
{
	Date tmp = *this;
	*this += 1;
	return tmp;
}


//重载前置--
Date& Date::operator--()
{
	*this -= 1;
	return *this;
}


//重载后置--
Date Date::operator--(int)
{
	Date tmp = *this;
	*this -= 1;
	return tmp;
}

//重载流插入
ostream& operator<<(ostream& out, const Date& d)
{
	cout << d._year << "年" << d._month << "月" << d._day << "日" << endl;
	return out;
}


//重载流提取
istream& operator>>(istream& in, Date& d)
{
	cout << "请输入年月日:";
	cin >> d._year >> d._month >> d._day;
	if (!d.CheckDate())
	{
		cout << "⽇期⾮法" << endl;
	}

	return in;
}

  • 本节完…
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2026-01-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言:
  • 正文:
    • 1. 类的默认成员函数
    • 2. 构造函数
    • 3. 析构函数
    • 4. 拷贝构造函数
    • 5. 赋值运算符重载
      • 5.1 运算符重载
      • 5.2 赋值运算符重载
    • 6. 取地址运算符重载
      • 6.1 const 成员函数
      • 6.2 取地址运算符重载
    • 7.日期类的实现
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档