【C++】类与对象 第二篇(构造函数,析构函数,拷贝构造,赋值重载)
目录
类的6个默认成员函数
初始化和清理
1.构造函数
2.析构函数
3.共同点
拷贝复制
1.拷贝构造
使用细节
2.赋值重载
运算符重载
== <= < >= > !=
连续赋值
C++入门 第一篇(C++关键字, 命名空间,C++输入&输出)-CSDN博客
C++入门 第二篇( 引用、内联函数、auto关键字、指针空值nullptr)-CSDN博客
【C++】类与对象 第一篇(class,this)-CSDN博客
类的6个默认成员函数
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
初始化和清理
1.构造函数
特征: 构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任 务并不是开空间创建对象,而是初始化对象。 其特征如下:
-
函数名与类名相同。
-
无返回值。
-
对象实例化时编译器自动调用对应的构造函数。
-
构造函数可以重载。
class Stack
{
public:
Stack()
{
_a=nullptr;
_size=_capacity=0;
}
Stack(int n)
{
_a=(int*)malloc(sizeof(int)*n);
_size=_capacity=0;
}
void Init(int n=4)
{
_a=(int*)malloc(sizeof(int)*n);
if (nullptr==_a)
{
perror("malloc is fail");
return;
}
_capacity=n;
_size=0;
}
void Push(int x)
{
//...
a[_size++]=x;
}
//...
void Dstory()
{
//...
}
private:
int _a;
int _size;
int _capacity;
};
int main()
{
Stack st;//无参
//Stack st();//有参
st.Push(1);
st.Push(2);
st.Push(3);
st.Push(4);
st.Dstory();
return 0;
}
以上述代码为例:
自动调用初始化
注意:调用无参时如:Data d;此处在后面不能➕(),否则编译器会调用有参的。
使用缺省值:
class Date
{
public:
Date(int year=1,int month=1,int day=1)
{
_year=1;
_month=1;
_day=1;
}
void print()
{
cout<<_year<<"/"<<_month<<"/"<<_day<<endl;
}
private:
//成员变量
int _year;
int _month;
int _day;
};
int main()
{
//Date d1;
Date d2(2077,2,3);
d2.print();
return 0;
}
2.析构函数
概念: 通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的? 析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由 编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
特性 析构函数是特殊的成员函数,其特征如下:
-
析构函数名是在类名前加上字符 ~。
-
无参数无返回值类型。
-
一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构 函数不能重载
-
对象生命周期结束时,C++编译系统系统自动调用析构函数。
例子:
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 3)
{
_array = (DataType *)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(DataType data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
// 其他方法...
~Stack()
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
DataType *_array;
int _capacity;
int _size;
};
void TestStack()
{
Stack s;
s.Push(1);
s.Push(2);
}
3.共同点
如果编译过程不写,那编译器会自动生成一个默认的,但是如果我们实现了任意一个,编译器就不会生成了。
若是自动初始化,那为什么这个地方会生成随机值呢?
C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类 型,如:int/char...,自定义类型就是我们使用class/struct/union等自己定义的类型,看看 下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员 函数。
内置类型(基本类型):int/char/double... /任意类型指针 自定义类型:class/structd定义的
默认生成构造函数:
1.内置类型成员不做处理。
2.自定义类型的成员,会去调用它的默认构造(不用传参数的构造)
private:
// 基本类型/内置类型 - 不进初始化
int _year;
int _month;
int _day;
实用场景:
class Date
{
public:
//内置类型成员不做处理
void print()
{
cout<<_year<<"/"<<_month<<"/"<<_day<<endl;
}
private:
// 基本类型/内置类型 - 不进初始化
int _year;
int _month;
int _day;
};
class MyQueue
{
// 默认生成构造函数,对自定义类型,会调用它的默认构造函数
void push(int x);
{
}
//...
Stack _pushST();
Stack _popST();
};
析构:
默认生成构造函数:
1.内置类型成员不做处理。
2.自定义类型的成员,会去调用它的析构函数
class MyQueue
{
// 默认生成析构函数,对自定义类型,会调用它的析构函数
void push(int x);
{
}
Stack _pushST();
Stack _popST();
};
int main()
{
//Date d1;
Date d1;
d1.print();
MyQueue q;
return 0;
}
内置类型并不会主动初始化,但可以通过给缺省值进行初始化:
private:
//声明位置给缺省值
int _year=1;
int _month=1;
int _day=1;
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。 注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为 是默认构造函数。
拷贝复制
1.拷贝构造
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存 在的类类型对象创建新对象时由编译器自动调用。
拷贝构造:内置类型,编译器直接拷贝,自定义类型拷贝需要调用拷贝构造
特征
拷贝构造函数也是特殊的成员函数,其特征如下:
-
拷贝构造函数是构造函数的一个重载形式。
(
为什么自定义类型要用拷贝构造,而内置类型编译器却可以直接拷贝(按字节拷贝)? 因为自定义类型进行拷贝容易出问题:若要拷贝栈,栈的成员变量都是指向相同的空间,若进行拷贝,则会导致两个Stack指向同一个空间,导致原本Stack执行析构函数时被销毁,而复制的那个则指向空,正确的应该是各有各的空间。
)
-
拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错, 因为会引发无穷递归调用。
class Date
{
public:
Date(int year=2077, int month=10, int day=12)
{
_year = year;
_month = month;
_day = day;
}
Date(Date d)//这样拷贝会造成无限递归
{
_year = d.year;
_month = d.month;
_day = d.day;
}
private:
int _year;
int _month;
int _day;
};
在拷贝构造函数 Date(Date d)
中,参数 d
是按值传递的,这意味着每次调用拷贝构造函数时都会创建一个新的 Date
对象,并将原始对象 d
复制到新的对象中。然而,在拷贝构造函数内部,对于拷贝构造函数的调用又会传递同样的参数 d
,导致不断地递归调用拷贝构造函数,从而产生无限递归。
则可以使用 & ,使其不在重新创建空间并使用原Date对象
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
这个拷贝构造函数将创建一个新的 Date
对象,并将原始对象的 _year
、_month
和 _day
成员变量的值分别拷贝到新对象的相应成员变量中。通过使用对象引用作为参数,我们可以避免无限递归调用拷贝构造函数的问题,并且确保在构造新对象时不会复制整个对象
class Date
{
public:
Date(int year = 2077, int month = 10, int day = 12)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)//const是为了防止写反->d._year = _year;加const缩小权限
{
cout<<"Date(Date& d);"<<endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 2, 3);
//两种拷贝方式
Date d2(d1); // 使用拷贝构造函数创建对象 d2,并将 d1 的值拷贝到 d2 中
Date d3=d1;
return 0;
}
RunCode:
若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按 字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
深拷贝是指在对象拷贝时,复制所有的数据和资源,使得新对象和原对象完全独立,互不影响。与之相对的是浅拷贝,浅拷贝只复制指针或引用,导致新旧对象共享同一份数据,修改一个可能会影响另一个。深拷贝能够保证对象之间的独立性和数据完整性。
分清楚是不是拷贝构造:
class Date
{
public:
Date(int year = 2077, int month = 10, int day = 12)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造函数
Date(const Date& d)
{
cout<<"Date(Date& d);"<<endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
//构造函数 不是拷贝构造
Date(const Date* d)
{
cout<<"Date(Date& d);"<<endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 2, 3);
//Date(const Date* d)
Date d2(&d1);
Date d3=&d1;
return 0;
}
拷贝构造函数典型调用场景:
-
使用已存在对象创建新对象
-
函数参数类型为类类型对象
-
函数返回值类型为类类型对象
使用细节
1使用拷贝构造时最好加上const,以防权限的放大
// 拷贝构造
Date(const Date &d)
{
_year = d.year;
_month = d.month;
_year = d.year;
}
2编译器可自动生成但存在问题
本文所用的日期类可以使用自动生成拷贝,但进行拷贝栈时就会出现问题
当进行拷贝时,用值拷贝,导致两个栈指向同一个空间,造成空间互相覆盖 指向同一块空间的问题:1.插入删除数据会互相影响 2.析构两次,程序崩溃
析构是也跟栈一样是后进先出,后定义的先析构,故st2先析构,对st2滞空是,并不影响st1,导致野指针。此时浅拷贝行不通,需要进行深拷贝:
代码实现:
class Stack
{
public:
void Push(const int &data)
{
_array[_size] = data;
_size++;
}
Stack(const Stack &st)
{
_array = (int *)malloc(sizeof(int) * st._capacity);
if (nullptr == _array)
{
perror("malloc is fail");
exit(-1);
}
memcpy(_array, st._array, sizeof(int) * st._size);
_size = st._size;
_capacity = st._capacity;
}
~Stack()
{
if (_array)
{
delete[] _array;
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
int *_array;
int _size;
int _capacity;
};
int main()
{
Stack st1;
st1.Push(1);
st1.Push(2);
st1.Push(3);
Stack st2(st1);
return 0;
}
什么情况下需要事项拷贝构造呢? 自己实现了析构释放空间,就需要实现拷贝构造
class Stack
{
public:
//构造函数
Stack(size_t _capacity = 10)
{
cout << "Stack(size_t _capacity)" << endl;
_array = (int *)malloc(sizeof(int) * _capacity);
if (nullptr == _array)
{
perror("malloc is fail");
exit(-1);
}
_size = 0;
_capacity = _capacity;
}
void Push(const int &data)
{
_array[_size] = data;
_size++;
}
//拷贝构造
Stack(const Stack &st)
{
_array = (int *)malloc(sizeof(int) * st._capacity);
if (nullptr == _array)
{
perror("malloc is fail");
exit(-1);
}
memcpy(_array, st._array, sizeof(int) * st._size);
_size = st._size;
_capacity = st._capacity;
}
~Stack()
{
if (_array)
{
delete[] _array;
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
int *_array;
int _size;
int _capacity;
};
class MyQueue
{
//默认生成构造 ->生成定义在Stack中的默认构造函数
//默认生成析构
//默认生成的拷贝构造
private:
Stack _pushST;
Stack _popST;
int _size = 0;
};
int main()
{
Stack st1;
st1.Push(1);
st1.Push(2);
st1.Push(3);
Stack st2(st1);
cout<<"====="<<endl;
MyQueue q;
return 0;
}
默认生成拷贝构造和赋值重载:
a.内置类型完成 浅/值 拷贝--按byte一个一个拷贝
b.自定义类型,去调用这个成员 拷贝构造/赋值重载
2.赋值重载
运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其 返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。 函数原型:返回值类型 operator操作符(参数列表)
自定义类型不能直接使用运算操作符 内置类型是语法定义,而自定义类型为人为定义,编译器并不知道该如何进行比较,这便出现了operator用函数完成
运算符重载:自定义类型对象可以使用运算符
函数重载:支持函数名相同,参数不同的函数
#include <iostream>
using namespace std;
class Date
{
public:
// 构造函数
Date(int year = 1, 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;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
// d1==d2 -> d1.operator==(d2)
bool operator==(const Date &d)
{
return _year == d._year && _month == d._month && _day == d._day;
// this->_year==d.year
}
private:
// 成员变量
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2077, 2, 4);
Date d2(2077, 2, 4);
// d1==d2;
cout << d1.operator==(d2) << endl;
cout << (d1 == d2) << endl;
return 0;
}
注意:
-
不能通过连接其他符号来创建新的操作符:比如operator
-
重载操作符必须有一个类类型参数
-
用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
-
作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐 藏的this现。
-
注意以下5个运算符不能重载
.* :: sizeof ?: .
== <= < >= > !=
// d1==d2 -> d1.operator==(d2)
bool operator==(const Date &d)
{
return _year == d._year && _month == d._month && _day == d._day;
// this->_year==d.year
}
// bool operator<(const Date &d)
// {
// if (_year < d._year)
// {
// return true;
// }
// else if (_year == d._year && _month < d._month)
// {
// return true;
// }
// else if (_year == d._year && _month == d._month && _day < d._day)
// {
// return true;
// }
// else
// return false;
// }
// d1<d2
bool operator<(const Date &d)
{
return _year < d._year && (_year == d._year &&
_month < d._month) && (_year == d._year &&
_month == d._month && _day < d._day);
}
任何一个类都适用于,当写了一个>=或<=那其他的就可以进行复用
// d1<=d2
bool operator<=(const Date &d)
{
return *this < d || *this == d;//*this就是d1
}
这个地方对operator<(const Date &d)
进行了复用。operator<=(const Date &d)
中的表达式*this < d
调用了operator<(const Date &d)
来判断两个日期对象的大小关系。同时,operator<=(const Date &d)
还利用operator==(const Date &d)
来判断两个日期对象是否相等。通过这样的复用方式,可以简化代码并提高代码的可读性
// d1>d2
bool operator>(const Date &d)
{
return !(*this<= d);
}
//d1>=d2
bool operator>=(const Date& d)
{
return !(*this<d);
}
//d1!=d2
bool operator!=(const Date& d)
{
return !(*this ==d);
}
当然赋值运算符不使用&并不会无限循环
// d1<=d2
bool operator<=(const Date d)
{
return *this < d || *this == d;
}
当然可以不用,但是最好还是加上
连续赋值
d3 = d1 = d2
// 因为Date operator=(const Date &d) 出了该函数的作用域*this/d1 还在此时返回的*this是临时拷贝,需要进行拷贝重新开空间造成浪费,不如直接进行&
Date operator=(const Date &d)//&d 引用
{
if (this != &d)//防止d1=d1进行赋值 此处的&为去地址 ,若this与d的地址一样则无需赋值
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this; //*this就是d1
} // 返回值为了支持连续赋值,保持运算符特性 d3=d1=d2;
当然要注意,赋值重载是针对已经创造的对象
Date d5=d1;//拷贝构造
像这样就是拷贝构造
同样作为默认成员函数,可以不用手动写
源码:
#include <iostream>
using namespace std;
class Date
{
public:
// 构造函数
Date(int year = 1, 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;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
// d1==d2 -> d1.operator==(d2)
bool operator==(const Date &d)
{
return _year == d._year && _month == d._month && _day == d._day;
// this->_year==d.year
}
// d1<d2
bool operator<(const Date &d)
{
return _year < d._year && (_year == d._year && _month < d._month) && (_year == d._year && _month == d._month && _day < d._day);
}
// d1<=d2
bool operator<=(const Date &d)
{
return *this < d || *this == d; //*this就是d1
}
// d1>d2
bool operator>(const Date &d)
{
return !(*this <= d);
}
// d1>=d2
bool operator>=(const Date &d)
{
return !(*this < d);
}
// d1!=d2
bool operator!=(const Date &d)
{
return !(*this == d);
}
// // 因为Date operator=(const Date &d) 出了该函数的作用域*this/d1 还在此时返回的*this是临时拷贝,需要进行拷贝重新开空间造成浪费,不如直接进行&
// Date operator=(const Date &d)//&d 引用
// {
// if (this != &d)//防止d1=d1进行赋值 此处的&为去地址 ,若this与d的地址一样则无需赋值
// {
// _year = d._year;
// _month = d._month;
// _day = d._day;
// }
// return *this; //*this就是d1
// } // 返回值为了支持连续赋值,保持运算符特性 d3=d1=d2;
private:
// 成员变量
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2077, 2, 4);
Date d2(2222, 3, 2);
Date d3(2078, 2, 4);
cout << (d1 < d2) << endl;
// d3 = d1 = d2;
d1 = d2;
d1.Print();
return 0;
}