归属专栏:浅尝C++
个人主页:Jammingpro
记录一句:好想摆烂,又好想学习~~
文章前言:本篇文章简要介绍C++类的构造函数、析构函数及拷贝构造函数,介绍每个小点时,都会附上对应的代码,如果可能的话。余下3大默认成员函数将于后续文章中叙述,要不,整个文章太长了。
我们知道,如果一个类什么都没有,那么它就是一个空类。空类真的什么都没有吗?答案是否定的。任何类在什么都不写时,编译器会自动生成以下6个默认成员函数,本文将先介绍构造函数、析构函数及拷贝构造函数。
ps:默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
在介绍构造函数之前,我们先回顾一下类的定义和调用。
#include
using namespace std;
class Date
{
public:
void Init(int year, int month, int day)
{
m_year = year;
m_month = month;
m_day = day;
}
void Print()
{
cout << m_year << " - " << m_month << " - " << m_day << endl;
}
private:
int m_year;
int m_month;
int m_day;
};
int main()
{
Date date;
date.Init(2023, 1, 1);
date.Print();
return 0;
}
对于Date类,可以通过 Init 公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
特征如下:
①函数名与类名相同。
②无返回值。
③对象实例化时编译器自动调用对应的构造函数。
④构造函数可以重载。
#include
using namespace std;
class Date
{
public:
//以下两个构造函数,参数个数不同,构成重载
Date(int year, int month, int day)
{
//定义对象时,会自动打印下面这句内容
cout << "Date(int year, int month, int day)被调用" << endl;
m_year = year;
m_month = month;
m_day = day;
}
//构造函数名称与类名相同,且无返回值
Date(int year)
{
cout << "Date(int year)被调用" << endl;
}
private:
int m_year;
int m_month;
int m_day;
};
int main()
{
Date d1(2022, 12, 12);
Date d3(2023);
return 0;
}
⑤如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
#include
using namespace std;
class Date
{
public:
/*提供该构造函数后,编译不再生成默认构造函数
Date(int year, int month, int day)
{
m_year = year;
m_month = month;
m_day = day;
}
*/
private:
int m_year;
int m_month;
int m_day;
};
int main()
{
Date d1;
//若将上面类中定义的构造函数注释掉,则此处不会出错,因为编译器自动生成了默认构造函数(不带参数的函数)
//若使用上面类中定义的构造函数,则此处定义d1对象会报错,因为编译器不再自动生成默认构造函数
return 0;
}
⑥关于编译器生成的默认成员函数,不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默认构造函数,但是d1对象m_year/m_month/m_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用??
答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char…,自定义类型就是我们使用class/struct/union等自己定义的类型,看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员m_t调用的它的默认成员函数。
#include
using namespace std;
class Time
{
public:
Time()
{
cout << "Time构造函数" << endl;
m_hour = m_minute = m_second = 0;
}
void Print()
{
cout << m_hour << ":" << m_minute << ":" << m_second << endl;
}
private;
int m_hour;
int m_minute;
int m_second;
};
class Date
{
public:
void Print()
{
cout << m_year << "-" << m_month << "-" << m_day << endl;
m_t.Print();
}
private:
int m_year;
int m_month;
int m_day;
};
int main()
{
Date d;
//d未调用Time的构造函数,但打印时,Time的各个成员变量均已初始化为1。说明编译器自动实现的Date的构造函数,会自动调用Time的构造函数。
d.Print();
return 0;
}
注意:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。
#include
#include
using namespace std;
class Jammingpro
{
public:
void Print()
{
cout << "我是 " << m_name << ",今年 " << m_age << " 岁" << endl;
}
private:
string m_name = "xioaming";
int m_age = "18";
};
int main()
{
Jammingpro jam;
jam.Print();//打印的是默认值
return 0;
}
⑦无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。下面代码演示的是无参、全缺省构造函数,两个函数只能留下其中一个,不能同时存在。
class Date
{
public:
Date()
{}
Date(int year = 2022, int month = 12, int day = 12)
{
m_year = year;
m_month = month;
m_day = day;
}
private:
int m _year;
int m_month;
int m_day;
}
与构造函数功能相反,析构函数是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
析构函数是特殊的成员函数,其特征如下:
①析构函数名是在类名前加上字符 ~。
②无参数无返回值类型。
③一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
④对象生命周期结束时,C++编译系统系统自动调用析构函数。
#include
using namespace std;
class Stack
{
public:
//对象生命周期结束时,需要调用下面的析构函数
~Stack()
{
cout << "~Stack()" << endl;
}
private:
char* m_mem;
int m_size;
int m_capacity;
};
int main()
{
Stack stk;
return 0;
}
⑤关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器生成的默认析构函数,对自定类型成员调用它的析构函数。
#include
using namespace std;
class Time
{
public:
//下面这句话会被打印,因为Date的自动生成的析构函数会调用自定义数据类型的析构函数
~Time()
{
cout << "Time析构函数" << endl;
}
private;
int m_hour;
int m_minute;
int m_second;
};
class Date
{
private:
int m_year;
int m_month;
int m_day;
};
int main()
{
Date d;
return 0;
}
⑥如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。
class Stack
{
public:
Stack()
{
m_mem = new char[100];//在堆区申请了内存空间
m_size = 0;
m_capcpity;
}
~Stack()
{
delete[] m_mem;//必须使用析构函数释放资源
}
private:
char* m_mem;
int m_size;
int m_capacity;
}
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
其特征如下:
①拷贝构造函数是构造函数的一个重载形式。
②拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
class Date
{
public:
Date(int year, int month, int day)
{
m_year = year;
m_month = month;
m_day = day;
}
//Date(const Date d) ->错误写法,将引发无穷递归
Date(const Date& d)//->正确写法
{
m_year = d.m_year;
m_month = d.m_month;
m_day = d.m_day;
}
private:
int m_year;
int m_month;
int m_day;
}
③若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
class Stack
{
public:
Stack()
{
m_mem = new char[100];
m_size = 0;
m_capacity = 100;
}
/*下面这个是编译器自动生成的
Stack(const Stack& s)
{
m_mem = s.m_mem;
m_size = s.m_size;
m_capacity = s.m_capacity;
}
*/
Stack(const Stack& s)
{
m_mem = new char[s.m_capacity];
strcpy(m_mem, s.m_mem);
m_size = s.m_size;
m_capacity = s.m_capacity;
}
private:
char* m_mem;
int m_size;
int m_capacity;
}
④编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?当然像Date
类这样的类是没必要的。而像上面的Stack
类是需要自己显示实现拷贝构造函数的。
编译器自动生成的拷贝构造函数是浅拷贝,也就是将属性值复制一份。对于下面图片中的s1
和s2
,由于s2
复制了s1
中m_mem
的地址,导致两个栈指向同一内存空间,两个栈生命周期结束后均会调用析构函数,此时将执行两次delete[] m_mem
,将导致内存重复释放而报错。
注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
⑤拷贝构造函数典型调用场景:
使用已存在对象创建新对象
函数参数类型为类类型对象
函数返回值类型为类类型对象
#include
using namespace std;
class Date
{
public:
Date(int year, int month, int day)
{
cout << "Date create" << endl;
m_year = year;
m_month = month;
m_day = day;
}
Date(const Date& d)
{
cout << "Date copy" << endl;
m_year = d.m_year;
m_month = d.m_month;
m_day = d.m_day;
}
~Date()
{
cout << "~Date()" << endl;
}
private:
int m_year;
int m_month;
int m_day;
};
Date Test(Date d)
{
return d;
}
int main()
{
Date date(2023, 11, 11);
Test(date);
Date cp(date);
return 0;
}
这段代码的执行结果如下,即调用1次构造函数,3次拷贝构造函数,4次析构函数:
Date create
Date copy
Date copy
~Date()
~Date()
Date copy
~Date()
~Date()
对于上面的代码,执行Date date(2023, 11, 11);
时调用1次构造函数;执行Test(date);
时传递参数,调用了1次拷贝构造函数,返回时,返回值又调用了1次拷贝构造函数,由于返回值产生的临时变量和Test的参数d离开Test函数后生命周期就结束了,因此分别调用了析构函数,即2次析构函数;执行Date cp(date)
构造cp对象时调用了1次拷贝构造函数,main函数执行结束,date与cp生命周期结束,分别调用析构函数,即2次析构函数。
为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用。
文章结语:这篇文章对C++中的构造函数、析构函数及拷贝构造函数进行了简要的介绍。后续文章将介绍余下3大默认成员函数。
欢迎进入浅尝C++专栏,查看更多文章。
如果上述内容有任何问题,欢迎在下方留言区指正b( ̄▽ ̄)d