提示:这里可以添加本文要记录的大概内容:
注意:重点是前四个默认成员函数。后面两个用的很少
在C++中,类的六个默认成员函数是构造函数、析构函数、拷贝构造函数、拷贝赋值运算符等。这些函数为类提供了基本的生命周期管理和对象复制行为。深入理解和善用这些默认成员函数是每个C++程序员的基本功,也是构建可靠、高效代码的关键一步。本博客将探讨这六个默认成员函数的核心概念,旨在帮助读者更好地利用C++中的面向对象编程特性。
如果一个类中什么成员也没有,简称为空类。
例如:
class stack{};
但是空类中真的什么也没有吗?事实上,并不是,任何一个类在我们不写的情况下,编译器都会自动生成下面的六个默认成员函数。
对于如下的日期类
#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方法给对象设置内容,但是如果每次创建对象都调用该方法设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?
答案就是利用构造函数。构造函数
是一个特殊的成员函数,名字与类名相同
,创建类类型对象时由编译器自动调用
,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次
。
构造函数是特殊的成员函数,需要注意的是,构造函数的虽然名称叫构造,但是需要注意的是构造函数的主要任务并不是开空间创建对象,而是初始化对象
。
其特征如下:
// 1.无参构造函数
Date ()
{}
// 2.带参构造函数
Date (int year, int month , int day )
{
_year = year ;
_month = month ;
_day = day ;
}
注意:自己实现的无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数
//自己实现的无参构造函数
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;
};
// 其他方式也可以的,主要看公司要求。一般都是加个前缀或者后缀标识区分就行。
析构函数(Destructor)是C++类中的一种特殊成员函数,用于在对象生命周期结束时执行清理和释放资源的操作
。析构函数的名称与类名相同,前面加上波浪号 ~
,没有参数,也没有返回值
。它在对象被销毁时自动调用
,通常用于释放动态分配的内存、关闭文件、释放资源等清理工作。
析构函数的基本形式如下:
class MyClass {
public:
// 构造函数
MyClass() {
// 构造函数的初始化操作
// ...
}
// 析构函数
~MyClass() {
// 析构函数的清理操作
// ...
}
};
析构函数在对象生命周期结束时被自动调用,它的执行时机包括:
new
创建,并在使用 delete
进行显式销毁时。示例:
#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
调用,还是在程序结束时。
析构函数是特殊的成员函数。
其特征如下:
#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;
}
自定类型成员
调用它的析构函数#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;
}
拷贝构造函数(Copy Constructor)是C++中的一种特殊成员函数,用于创建一个新对象并使用已有对象的值来初始化它。拷贝构造函数通常在以下情况下被调用
:
拷贝构造函数的声明和实现形式如下:
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
的副本。这种拷贝构造的行为可以确保在创建新对象时,其初始值与已有对象相同。
必须使用引用传参,使用传值方式会引发无穷递归调用
如果我们不采用引用传参,编译器在编译阶段就会检查出问题,来看下面的代码
#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;
}
那么原因就是会出现无穷递归调用的问题,在传参的过程中实际上又是一次调用拷贝构造,那么久无穷递归下去了,接下来看图解!!
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;
}
#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;
}
这里虽然编译可以通过,但是会出现运行时错误。
那么出现运行时错误的原因就是:
编译器默认的拷贝构造是值拷贝,那么两个对象的_arr会指向同一块空间,最后在进行资源清理时,会调用两次析构函数,这一块空间会被释放两次
,但是这是不被允许的!!
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也是具有返回值类型、函数名字以及参数列表,其返回值类型与参数列表和普通的函数类似。
函数的名字:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
作为类成员的重载函数时,其形参看起来比操作数数目少1
成员函数的操作符有一个默认的形参this,限定为第一个形参
#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;
}
#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 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;
}
// 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。
这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝(后面在讲)去解决。
具体实现见此代码
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;
};
将const修饰的类成员函数称之为const成员函数
,const修饰类成员函数,实际修饰该成员函数隐含的this指针
,表明在该成员函数中不能对类的任何成员进行修改。
我们来看看下面的代码
#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,属于权限的放大,不能编译通过
请思考下面的几个问题:
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
class Date
{
public :
Date* operator&()
{
return this ;
}
const Date* operator&()const
{
return this ;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容
C++中的类默认成员函数是面向对象编程的支柱,它们负责处理对象的构建、销毁和复制等关键任务。通过熟练掌握这些函数的使用方式和设计原则,我们能够提高代码的可维护性和性能。精心设计这六个默认成员函数,可以使代码更具清晰性和健壮性。在C++的世界里,深刻理解并熟练运用这些默认成员函数,将有助于写出高效、安全的类和程序。