目录
六大默认类成员函数
默认成员函数是指用户没有显式实现,编译器会自动生成的成员函数称为默认成员函数。
对于空类,并不是什么都没有,编译器会自动默认生成以下六个默认成员函数
由于取地址重载函数很少要自己实现,我们只重点介绍前4个默认成员函数
//看起来这个类里什么都没有,其实有很多默认的拷贝构造函数
class A
{
};
class A
{
public:
A();//构造函数
~A();//析构函数
A(const A& a);//拷贝构造函数
A& operator=(const A& a);//赋值运算符重载
A* operator &();//取地址运算符重载
const A* operator &() const;//const修饰的取地址运算符重载
};
构造函数
构造函数的特性
构造函数是特殊的成员函数,其中函数名与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次
构造函数目的:默认构造函数是为了解决创建对象,忘记对其对象进行初始化操作,同时解决麻烦地调用Init函数。
构造函数的特点:
1.无返回值
2.构造函数的函数名和类名相同
3.对象实例化时,编译器自动调用对应的构造函数且只调用一次
4.可以重载
构造函数的分类
构造函数分为全缺省构造函数,半缺省,传参构造函数,默认构造函数(本人比较推荐的是全缺省构造函数,因为它非常灵活)
以下代码片展现了各种构造函数
class Date
{
public:
//1.无参构造函数
Date()
{
_year = 2024;
_day = 6;
}
//2.带参构造函数
Date(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;//调用无参构造函数
Date d2(2024, 3, 6);//调用有参构造函数
d1.Print();
d2.Print();
Date d3();//未调用原型函数(是否有意用变量定义?)
return 0;
}
注意 :只有我们没写构造函数时,编译器才会生成默认的构造函数
构造函数对于内置类型和自定义类型处理方法
内置类型:
(int/double/float/)等语言提供的数据类型
自定义类型:
(class/struct/enum)自己实现的数据类型
1.类的成员是内置类型,编译器不会做任何处理
2.类的成员是自定义类型,编译器会调用其的构造函数(如果程序员没有自己实现那就调默认的构造函数)
class Server
{
public:
Server(uint16_t port = DEFAULTPORT)
:_port(port)
,_sock() //这里就会调用Sock默认构造函数
,_epoll() //这里就会调用Sock默认构造函数
{
_connectHash.clear();
}
void init()
{
_sock.setSocket();
_sock.Bind(_port);
_sock.Listen();
_epoll.create();
Util::setNonBlockSocket(_sock.getListenSocket());
_epoll.ctrlAdd(_sock.getListenSocket(), EPOLLIN | EPOLLET);
}
private:
uint16_t _port;
Sock _sock;
Epoll _epoll;
};
编译器会自动调用自定义类型的构造函数这个语法函数蛮爽的
我们就可以随便使用自定义类型里的自定义类型的成员了
注意构造函数的构造顺序
下面是一段构造函数的经典错误写法
构造时,不一定就先执行语句1
如果先执行的是语句2后,执行语句1,那么就会导致未定义行为
拷贝构造函数
拷贝构造函数的特性
拷贝构造函数特性:
拷贝构造函数是构造函数的一个重载,作用是利用一个已经存在的实例。创建一个新实例(所以实际参数通常传入的是自定义对象)
拷贝构造函数参数传值的后果
之前的博客我们介绍过,传值调用会先创建一个临时的对象
然后把这个临时的对象赋值给形式参数
但是我们今天要创建的是类的临时对象该临时对象的生成就是要依赖于拷贝构造函数,因此如此往复下去最终导致栈溢出
了解在栈创建对象和在堆上创建对象
在栈上创建对象,在栈上创建对象编译器,例如int a;或者定义一个自定义类的对象Myclass obj;编译器会在栈内存区域为其分配空间。当进行拷贝操作(如使用默认拷贝构造函数)时,对于栈上对象,是将源对象的数据按位复制到新对象的内存空间中。新对象有自己独立的内存区域,和源对象的内存相互独立,所以每个对象都有自己的一份数据。
在堆上创建对象
你得先new或malloc申请堆空间 ,然后编译器返回你申请的堆空间的地址。例如intptr = new int(5);,ptr 是一个指针,它存储的是堆上分配内存的地址。如果进行类似 intptr2 = ptr;这样的操作,实际上只是复制了指针的值,也就是地址。这就导致ptr和ptr2 都指向堆上同一块内存空间,所以看起来像是“两个人同时指向一份”。若后续通过其中一个指针修改堆上数据,另一个指针访问到的也是修改后的数据。
深拷贝与浅拷贝
浅拷贝
若未显示定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数对象按照内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝(值拷贝)
class Time
{
public:
Time()
{
_hour = 1;
_minute = 1;
_second = 1;
}
Time(const Time& t)
{
_hour = t._hour;
_minute = t._minute;
_second = t._second;
cout << "Time::Time(const Time&)" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d1;
// 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数
// 但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数
Date d2(d1);
return 0;
}
深拷贝
深拷贝是创建一个新的对象,并且递归地复制原对象的所有属性和子对象。也就是说,深拷贝会复制对象及其所有嵌套对象,新对象和原对象在内存中是完全独立的
某些情景浅拷贝的后果
以这段自定义的stack栈为例,其中_array是堆上成员!
class Stack
{
private:
DataType *_array;
size_t _size;
size_t _capacity;
};
再根据上文提到的:编译器在栈上定义对象和在堆上定义对象两个对象指向的位置相同会导致两个对象公用一块空间的现象
对于上述代码片,如果你在main函数这样调用
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2(s1);
return 0;
}
会导致s1和s2指向同一堆空间!!!
导致的问题
- 修改s1s2也跟着修改了
- 在析构函数时,s1s2都会析构同一段空间,即delete空指针
因此
对于要申请堆空间成员的类,拷贝构造函数不能使用编译器自带的浅拷贝构造函数,而应该自己实现深拷贝的版本
operator = 赋值函数
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数(不用我们取名字),也是具其返回值类型,函数名字以及参数列表,其返回值类型与参数列表于普通的函数类似
而 operator = 是类的默认成员函数
如何判断调用的是构造函数//拷贝构造函数还是operator赋值函数
以下是一段代码片,判断的关键就在于这个对象是第一次生成
还是一个已经存在的对象进行的赋值
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
std::cout << "构造函数被调用" << std::endl;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
std::cout << "拷贝构造函数被调用" << std::endl;
}
void operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
std::cout << "operator赋值函数被调用" << std::endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 1, 23);
Date d2 = (2024, 2, 28);
d2 = d1;
Date d3 = d1;
return 0;
}
Date d1(2024, 1, 23); //d1第一次出现,而且是传值创建所以是构造函数
Date d2 = (2024, 2, 28); //d2第一次出现,而且是传值创建所以是构造函数
d2 = d1; //d2不是第一次出现,所以是opreator赋值函数
Date d3 = d1; //d3是第一次出现,但他依赖于d1对象创建是拷贝构造函数
什么时候要自己写opreator函数什么时候用编译器自带的operator函数?
答:这个问题和什么时候自己写拷贝构造函数是一样的,由于深拷贝和浅拷贝
涉及动态资源管理 :如果类中有指针成员指向动态分配的内存(如通过new分配的数组 )、文件句柄等资源,必须自定义operator=函数。因为编译器自带的operator=函数执行浅拷贝,会导致多个对象指向同一块资源,析构时可能出现多次释放或资源泄漏问题。
class String {
private:
char* data;
public:
String(const char* str)
{
data = new char[strlen(str) + 1];
strcpy(data, str);
}
// 需自定义operator=函数
String& operator=(const String& other)
{
if (this != &other)
{
delete[] data;
data = new char[strlen(other.data) + 1];
strcpy(data, other.data);
}
return *this;
}
~String()
{
delete[] data;
}
};
析构函数
析构函数与构造函数功能相反,该函数任务并不是完成对象本身销毁(局部对象的销毁时由编译器完成),而是对象在销毁时自动调用析构函数,完成对象中动态开辟资源的清理工作
析构函数的特性
析构函数特性:
析构函数名为同类名前加上字符~
无参数无返回值类型,导致析构函数不支持重载函数
一个类只能有一个析构函数。若未显式定义,系统会在自动生成默认的析构函数。
对象生命周期结束时,C++编译系统自动调用析构函数
构造函数对于内置类型和自定义类型处理方法
1对于内置类型,不会做任何处理
2.对于自定义类型,会调用对应的析构函数
class Date
{
public:
Date(int year=1)
{
_year = year;
}
~Date()
{
cout << "~Date()->" <<_year<< endl;
}
private:
int _year;
int _month;
int _day;
};
Date d5(5);//全局对象
static Date d6(6);//全局对象
void func()
{
Date d3(3);//局部变量
static Date d4(4);//局部的静态
}
int main()
{
Date d1(1);//局部变量
Date d2(2);//局部变量
func();
return 0;
}
析构函数的顺序
从上图可以看到析构函数的顺序
1.对于同一生命周期,先创建的后析构,后创建的先析构
2.局部对象比全局对象先析构
如何写好析构函数
答:不同的类各司其职
对于本类的内置成员函数,如果涉及资源申请(如 文件描述符,动态申请的空间)务必要释放该资源防止资源泄漏
对应本类的自定义类型,因为编辑器会调用其构造函数,我们把该自定义类型的构造函数写好就行
// 自定义类型
class MyString
{
private:
char* data;
size_t length;
public:
// 构造函数
MyString(const char* str = "")
{
length = std::strlen(str);
data = new char[length + 1];
std::strcpy(data, str);
}
// 拷贝构造函数
MyString(const MyString& other)
{
length = other.length;
data = new char[length + 1];
std::strcpy(data, other.data);
}
// 赋值运算符重载
MyString& operator=(const MyString& other)
{
if (this != &other) {
delete[] data;
length = other.length;
data = new char[length + 1];
std::strcpy(data, other.data);
}
return *this;
}
// 析构函数
~MyString()
{
delete[] data;
}
// 打印字符串
void print() const {
std::cout << data << std::endl;
}
};
// 包含自定义类型和资源申请的类
class MyClass
{
private:
int* dynamicArray; // 动态申请的空间
MyString myString; // 自定义类型
public:
// 构造函数
MyClass(const char* str, size_t arraySize)
{
dynamicArray = new int[arraySize];
for (size_t i = 0; i < arraySize; ++i)
{
dynamicArray[i] = i;
}
myString = MyString(str);
}
// 析构函数
~MyClass()
{
delete[] dynamicArray; // 释放动态申请的空间
}
// 打印数组和字符串
void print() const {
for (size_t i = 0; i < 5; ++i)
{
std::cout << dynamicArray[i] << " ";
}
std::cout << std::endl;
myString.print();
}
};
int main()
{
MyClass obj("Hello, World!", 5);
obj.print();
return 0;
}
加油少年,今天的你又进步了一点点哟