在C++中,有六个默认成员函数,它们是编译器在需要的情况下自动生成的成员函数,如果你不显式地定义它们,编译器会自动提供默认实现。这些默认成员函数包括:
默认构造函数 (Default Constructor): 如果你没有为类显式定义任何构造函数,编译器将生成一个无参的默认构造函数。用于创建对象而不需要提供任何参数。例如:ClassName obj;
析构函数 (Destructor): 如果你没有为类定义析构函数,编译器将生成一个默认的析构函数。用于在对象被销毁时释放资源、进行清理工作等操作。例如:当对象超出作用域、被删除或者动态分配的对象被 delete
时。
拷贝构造函数 (Copy Constructor):(具有缺陷---浅拷贝) 如果你没有为类定义拷贝构造函数,编译器将生成一个默认的拷贝构造函数。用于创建一个对象作为另一个对象的副本,通常在赋值、传递参数等情况下调用。例如:ClassName obj2 = obj1;
赋值运算符重载 (Copy Assignment Operator):(具有缺陷---浅拷贝) 如果你没有为类定义赋值运算符重载函数,编译器将生成一个默认的赋值运算符重载函数。用于将一个对象的值复制给另一个对象,通常在赋值操作中调用。例如:obj2 = obj1;
移动构造函数 (Move Constructor)
移动赋值运算符 (Move Assignment Operator)
主要常见的是前四个,本篇文章只介绍前四个默认成员函数,最后两个有兴趣的小伙伴可以自己去查一下别的资料。
/*1.正常使用Data类初始化*/
#include
using namespace std;
class Data {
public:
void Init(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
void ShowInfo() {
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Data d1;
d1.Init(2024, 1, 29);
d1.ShowInfo();
Data d2;
d2.Init(2024, 1, 28);
d2.ShowInfo();
return 0;
}
每当我们定义一个自定义类型,创造对象的时候,一定需要对其进行初始化操作。既然对于所有的对象都需要进行初始化操作,那么每次显示调用Init函数就显得非常冗余。如果每当我们创建一个对象,自动对其进行初始化,就省去显示调用Init函数的步骤。
C++引入了一个新的概念,构造函数。构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象的整个生命周期内只调用一次。
/*2.使用构造函数进行初始化*/
#include
using namespace std;
class Data {
public:
Data(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
void ShowInfo() {
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Data d1(2024,1,29);
d1.ShowInfo();
Data d2(2024,1,28);
d2.ShowInfo();
return 0;
}
这样我们每创建一个对象,编译器就会自动调用对应的构造函数对其进行初始化操作。也就是说构造函数是为了方便我们对实例化对象进行初始化而创造出来的。
构造函数是一种特殊的成员函数。它的特性是:
函数名与类名相同。
无返回值。
对象实例化时编译器自动调用对应的构造函数。
构造函数可以重载。
/*3.初步介绍构造函数*/
#include
using namespace std;
class Data {
public:
Data(){
}
Data(int year,int month,int day){
_year=year;
_month=month;
_day=day;
}
void ShowInfo() {
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
void TestDate(){
Data d1;
Data d2(2024,1,28);
}
int main() {
TestDate();
return 0;
}
/*4.探究构造函数的规则---创建对象自动调用构造函数*/
#include
using namespace std;
class Data {
public:
Data(){
cout<<"Data()"<
当我们创建d1对象时,后面不加括号表示调用无参的构造函数。后面带括号,括号里带参数,表示调用有参的构造函数。创建对象的时候系统会自动调用对应的构造函数对其进行初始化操作。
/*5.探究构造函数的规则---编辑器默认有无参构造函数*/
#include
using namespace std;
class Data {
public:
void ShowInfo() {
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
void TestDate(){
Data d1;
d1.ShowInfo();
}
int main() {
TestDate();
return 0;
}
当我们没有显示编写构造函数的时候,创建完对象后我们发现对象内年月日的初始值是0。
如果类中没有显示定义构造函数,编译器会自动生成一个无参的默认构造函数,但是一旦我们显示定义了构造函数,编译器就不会自动生成无参构造函数。
/*6.探究构造函数的规则---自己创建有参的构造函数时,编辑器不会自动创建无参构造函数*/
#include
using namespace std;
class Data {
public:
// Data(){
// cout<<"Data()"<
我们显示定义了有参的构造函数,此时编译器不会自动生成无参的构造函数。当我们创建对象,后面不加括号的时候,系统会自动调用无参的构造函数,但是类类型中没有无参的构造函数,编译器没有生成,所以就报错了。解决的办法就是自己再写一个无参的构造函数。
/*7.探究构造函数的规则---默认构造函数具体情况---无参,全缺省*/
#include
using namespace std;
class Data {
public:
Data(){
cout<<"Data()"<
当我们没有显示的定义构造函数时,编译器会默认帮我们定义一个无参的构造函数。全缺省的构造函数等价于无参的构造函数。因此不可以同时存在,当我们创建对象后面不加括号时,系统调用无参的构造函数,此时编译器不知道调用无参的构造函数还是全缺省的构造函数。
问题产生的本质是缺省参数导致重载模棱两可,可以理解为缺省参数不存在,此时含有缺省参数的函数与另一个函数不构成重载,即使编译器没有报错。
/*7.缺省参数形成冲突*/
#include
using namespace std;
class Data {
public:
Data(int year, int month = 2, int day = 1) {
cout << "Data(int year,int month,int day)" << endl;
_year = year;
_month = month;
_day = day;
}
Data(int year) {
_year = year;
_month = 1;
_day = 1;
}
void ShowInfo() {
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
void TestDate() {
Data d1(2024);
d1.ShowInfo();
}
int main() {
TestDate();
return 0;
}
当我们创建对象传入2024一个参数的时候,编译器不知道调用有缺省参数的函数还是没有缺省参数的函数,缺省参数导致重载模棱两可,可以理解为缺省参数不存在,即Data
(int
year
,
int
month
=
2
,
int
day
=
1
)
等价于Data
(int
year
)
与下面的 Data
(int
year
)
不构成重载,即使编译器不会报错。
/*8.正常使用栈,进行清理工作*/
#include
using namespace std;
typedef int DataType;
class Stack {
public:
Stack(int capacity = 4) {
_array = (DataType*)malloc(sizeof(DataType) * capacity);
_capacity = capacity;
_size = 0;
}
void CheckCapacity(){
if(_capacity==_size){
int newcapacity=_capacity*2;
_array=(DataType*)realloc(_array,sizeof(DataType)*newcapacity);
_capacity=newcapacity;
}
}
void push(DataType data) {
CheckCapacity();
_array[_size++] = data;
}
void pop() {
_size--;
}
void Destroyed() {
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
bool empty() {
return _size == 0;
}
DataType top() {
return _array[_size - 1];
}
private:
DataType* _array;
int _capacity;
int _size;
};
void TestStack() {
Stack st;
st.push(1);
st.push(2);
st.push(3);
st.push(4);
st.push(5);
while (!st.empty()) {
cout << st.top() << endl;
st.pop();
}
st.Destroyed();
}
int main() {
TestStack();
return 0;
}
当我们使用创建对象时,使用完之后都需要显示调用Destroyed销毁函数进行清理工作,防止内存泄漏。既然对于所有的对象都需要进行清理操作,那么每次显示调用Destroyed函数就显得非常冗余。如果每当我们创建一个对象,对象出了作用域之后自动对其进行销毁,就省去显示调用Destroyed函数的步骤。
/*9.使用析构函数,进行清理工作*/
#include
using namespace std;
typedef int DataType;
class Stack {
public:
Stack(int capacity = 4) {
_array = (DataType*)malloc(sizeof(DataType) * capacity);
_capacity = capacity;
_size = 0;
}
void CheckCapacity() {
if (_capacity == _size) {
int newcapacity = _capacity * 2;
_array = (DataType*)realloc(_array, sizeof(DataType) * newcapacity);
_capacity = newcapacity;
}
}
void push(DataType data) {
CheckCapacity();
_array[_size++] = data;
}
void pop() {
_size--;
}
~Stack() {
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
bool empty() {
return _size == 0;
}
DataType top() {
return _array[_size - 1];
}
private:
DataType* _array;
int _capacity;
int _size;
};
void TestStack() {
Stack st;
st.push(1);
st.push(2);
st.push(3);
st.push(4);
st.push(5);
while (!st.empty()) {
cout << st.top() << endl;
st.pop();
}
}
int main() {
TestStack();
return 0;
}
C++引入了一个新的概念,析构函数。析构函数是一个特殊的成员函数,名字与类名相同,名字前面添加‘~’,类类型对象出作用域时由编译器自动调用。
析构函数是特殊的成员函数,其特征如下:
析构函数名是在类名前加上字符 ~。
无参数无返回值类型。
一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载。
对象生命周期结束时,C++编译系统系统自动调用析构函数。
/*10.初步介绍析构函数*/
#include
using namespace std;
class Date {
public:
Date(int year, int month, int day) {
cout << "Date(int year, int month, int day)" << endl;
_year = year;
_month = month;
_day = day;
}
void Show() {
cout << _year << "-" << _month << "-" << _day << endl;
}
~Date() {
cout << "~Date()" << endl;
}
private:
int _year;
int _month;
int _day;
};
void Test() {
Date d1(2024, 1, 29);
d1.Show();
}
int main() {
Test();
return 0;
}
/*11.正常用一个已有的变量拷贝构造一个新变量---内置类型*/
#include
using namespace std;
int main() {
int a=10;
int b=a;
cout<
我们用已经存在的a变量去构造新的b变量,很明显如果a、b都是内置类型,我们很容易理解。但是如果我们希望用一个已经存在的自定义类型去构造新的自定义类型对象,应该如何操作?
/*12.正常用一个已有的变量拷贝构造一个新变量---自定义类型*/
#include
using namespace std;
class Date {
public:
Date() {
}
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
void Show() {
cout << _year << "-" << _month << "-" << _day << endl;
}
~Date() {
cout << "~Date()" << endl;
}
void assignment(const Date& d) {
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
void Test() {
Date d1(2024, 1, 29);
//d2=d1?
Date d2;
d2.assignment(d1);
d2.Show();
}
int main() {
Test();
return 0;
}
我们可以在类里面定义一个函数,函数的参数是自定义类型的引用,作用是进行赋值,这样我们创建一个新对象后,调用这个对象的赋值函数,就可以实现我们想要的效果。我们可以发现对于每一个自定义类型来说,我们都希望可以这样实现"自定义类型 d2=d1",每个自定义类型都在类里面定义一个函数,外部调用会显得特别冗余,且外部调用形式是 Date d2
;
d2
.
assignment
(
d1
);
先创建对象再进行赋值,而我们的期望是创建对象和赋值一体化,这两者似乎有所区别。
/*12.使用拷贝构造函数拷贝构造一个新变量---自定义类型*/
#include
using namespace std;
class Date {
public:
Date() {
}
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
void Show() {
cout << _year << "-" << _month << "-" << _day << endl;
}
~Date() {
cout << "~Date()" << endl;
}
Date(const Date& d) {
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
void Test() {
Date d1(2024, 1, 29);
//d2=d1?
Date d2(d1);
d2.Show();
}
int main() {
Test();
return 0;
}
C++引入了一个新的概念,拷贝构造函数,只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
拷贝构造函数也是特殊的成员函数,其特征如下:
拷贝构造函数是构造函数的一个重载形式。
拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
/*13.默认生成的拷贝构造函数*/
#include
using namespace std;
class Date {
public:
Date() {
}
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
void Show() {
cout << _year << "-" << _month << "-" << _day << endl;
}
~Date() {
cout << "~Date()" << endl;
}
private:
int _year;
int _month;
int _day;
};
void Test() {
Date d1(2024, 1, 29);
//d2=d1?
Date d2(d1);
d2.Show();
}
int main() {
Test();
return 0;
}
即使我们没有显示编写拷贝构造函数,我们依旧可以用Date d2
(
d1
);
拷贝构造d2对象。因为编译器会自动生成对应的拷贝构造函数。
/*14.默认生成的拷贝构造函数---本质*/
#include
using namespace std;
class f1 {
public:
f1(int x) {
p = (int*)malloc(sizeof(int));
*p = x;
}
void Show() {
cout << *p << endl;
cout << p << endl;
}
~f1() {
if (p) {
free(p);
}
}
private:
int* p;
};
void Test() {
f1 x1(1);
f1 x2(x1);
x1.Show();
x2.Show();
}
int main() {
Test();
return 0;
}
运行上面的代码我们会发现程序崩掉了。如果在28行打一个断点,进入调试模式,我们发现一直运行到28行处程序都没有发生问题。此时我们得到,
我们发现x1的地址和x2的地址是一样的。很容易知道接下来出Test函数后,x1和x2对象都需要调用析构函数,此时同一个地址会释放两次空间,程序就崩了。
编译器默认生成的拷贝构造函数按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。可以理解为浅拷贝是将自定义类型类内成员变量进行简单的等号赋值。即x2.p=x1.p;
。此时指针就会指向同一段空间。而我们希望的是指针会新开辟一个空间,只是指针的值是相同的,空间不是相同的。
/*15.默认生成的拷贝构造函数---本质*/
#include
using namespace std;
class f1 {
public:
f1(int x) {
p = (int*)malloc(sizeof(int));
*p = x;
}
void Show() {
cout << p << endl;
cout << *p << endl;
}
~f1() {
if (p) {
free(p);
}
}
f1(const f1& x) {
p = (int*)malloc(sizeof(int));
*p = *x.p;
}
private:
int* p;
};
void Test() {
f1 x1(1);
f1 x2(x1);
x1.Show();
x2.Show();
}
int main() {
Test();
return 0;
}
此时我们显示定义拷贝构造函数,新开辟空间,只是让指针上的值相等。这样就可以达到我们想要的效果,指针指向的空间不同,而指针上的值是相等的。
拷贝构造函数是C++中的一个特殊函数,用于创建一个新对象并将其初始化为另一个对象的副本。它通常在以下情况下被调用:
对象的初始化:当你创建一个新对象并将其初始化为另一个对象的副本时,拷贝构造函数被调用。
函数参数传递:当你将一个对象作为参数传递给一个函数时,拷贝构造函数可以被用来创建传递给函数的副本。
函数返回值:当函数返回一个对象的副本时,拷贝构造函数被用来创建返回的副本。(编译器优化可能省略)
/*16.拷贝构造函数---的使用场景*/
#include
using namespace std;
class Date {
public:
Date(int year, int month, int day) {
cout << "Date(int year,int month,int day):" << this << endl;
}
Date(const Date&d) {
cout << "Date(const Date&d);" << this << endl;
}
~Date() {
cout << "~Date():" << this << endl;
}
private:
int _year;
int _month;
int _day;
};
Date Test(Date d) {
Date temp(d);
return temp;
}
int main() {
Date d1(2024, 1, 29);
Date d2 =Test(d1);
return 0;
}
Date d1
(
2024
,
1
,
29
);
我们调用构造函数创建对象d1。Test
(
d1
)
此时调用Test函数,将d1传给形成d这一过程,调用拷贝构造函数,用已经存在的d1创建并初始化d对象。(函数参数传递:当你将一个对象作为参数传递给一个函数时,拷贝构造函数可以被用来创建传递给函数的副本。)在Test函数中,显示调用拷贝构造函数,用已经存在的d创建并初始化temp对象。(对象的初始化:当你创建一个新对象并将其初始化为另一个对象的副本时,拷贝构造函数被调用。在Test函数作用域结束,调用对象d的析构函数。在主函数作用域结束,调用temp的析构函数接着调用d2的析构函数。
你可能会产生疑问,在Test函数调用结束,temp作为函数返回值,此时应该还会调用拷贝构造函数。(函数返回值:当函数返回一个对象的副本时,拷贝构造函数被用来创建返回的副本。)为何运行代码并没有显示拷贝构造函数被调用。
实际上temp作为返回值,此时还会调用拷贝构造函数,但是经过编译器优化过,这一步的拷贝构造函数被省略掉了。尽管你在 Test
函数中创建了一个 temp
对象并返回它,但在实际的编译过程中,编译器会进行优化,避免调用拷贝构造函数,直接将 temp
对象的值传递给 d2
。这是一种性能优化,可以减少不必要的对象拷贝操作,提高程序效率。
我对代码进行调试,注意看我的光标的位置,我的光标处于主函数中,但是析构函数只调用了一次,显然这个析构函数是对Test中d对象进行析构,而temp和d2对象并没有发生析构。显然temp在Test函数中作为返回值,此时temp对象的生命周期得到延伸,与d2同步,但最后编译器会依照前后顺序先调用temp的析构函数再调用d2的析构函数。虽然拷贝构造函数被省略了,但是 temp
对象的生命周期和 d2
对象的初始化完全一致,因此 temp
对象的析构函数会在 d2
对象的析构函数之前被调用,从而保证资源的正确释放。
运算符重载(Operator Overloading)是C++的一个重要特性,允许你为用户自定义的数据类型定义和重新定义运算符的行为。通过运算符重载,你可以使用C++内置运算符来执行自定义数据类型的操作,使代码更具可读性和表现力。
内置类型判断相等很简单,例如a,b变量都是int类型,判断相等只需要a==b?即可。但是如果我们想要定义自定义类型进行判断是否相等。我们希望也可以像“a==b”这样使用,对于ab都属于自定义类型,此时编译器显然不能像内置类型那样操作。于是我们引入运算符重载的概念,使得自定义类型也可以像内置类型那样进行操作。
/*17.运算符重载*/
#include
using namespace std;
class Date {
public:
Date(int year,int month,int day){
_year=year;
_month=month;
_day=day;
}
bool operator==(const Date& d2){
return _year==d2._year
&& _month==d2._month
&& _day==d2._day;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1(2024,1,29);
Date d2(d1);
cout<<(d1==d2)<
当我们使用d1
==
d2
这段代码的时候,实际上会转化为d1.operator==(d2)
此时调用d1对象的operator==函数,并传入d2对象作为参数。此时d1的地址会作为this指针,operator==函数会隐藏一个this指针,this指针指向的地址是d1的地址,_year等价于this->_year,_month等价于this->_month,_day等价于this->_day。
如果我们想要以这种形式“a=b”对一个自定义类型进行赋值,那么我们就需要对“=”进行运算符重载。
赋值运算符重载格式:
参数类型:const T&,传递引用可以提高传参效率
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值检测是否自己给自己赋值
返回*this :要符合连续赋值的含义
/*18.赋值运算符重载*/
#include
using namespace std;
class Date {
public:
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
Date& operator=(const Date&d) {
if (this != &d) {
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
void Show() {
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1(2024, 1, 29);
Date d2 = d1;
d2.Show();
return 0;
}
实际上当我们没有显示定义赋值运算符重载的时候,编译器会生成一个默认的赋值运算符重载,以值的方式逐字节拷贝,即浅拷贝。我们知道浅拷贝当成员变量是指针的时候,p1=p2
此时指针指向的地址会相同,而我们希望的是指针指向的地址不同,指针指向地址上的值相同,因为往往涉及到指针问题我们都需要显示定义赋值运算符重载,将浅拷贝变成深拷贝。
/*19.默认赋值运算符重载的缺陷*/
#include
using namespace std;
class f1 {
public:
f1(int x) {
p = (int*)malloc(sizeof(int));
*p = x;
}
void Show() {
cout << *p << endl;
cout << p << endl;
}
~f1() {
if (p) {
free(p);
}
}
private:
int* p;
};
void Test() {
f1 x1(1);
f1 x2 = x1;
x1.Show();
x2.Show();
}
int main() {
Test();
return 0;
}
x1,x2对象中p成员指向的地址是相同的,当x1,x2对象出作用域时,会对同一块地址调用两次析构函数,此时程序就会崩掉。此时就需要显示定义赋值运算符重载,将浅拷贝变成深拷贝。
/*20.前置++和后置++的重载(代码逻辑有缺陷)*/
#include
using namespace std;
class Date {
public:
Date() {}
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
Date& operator++() {
_day += 1;
return *this;
}
Date operator++(int) {
Date temp(*this);
_day++;
return temp;
}
void Show() {
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
void Test() {
Date d;
Date d1(2024, 1, 29);
d = d1++;
d.Show();
d1.Show();
d = ++d1;
d.Show();
d1.Show();
}
int main() {
Test();
return 0;
}
Date operator++(int) {
Date temp(*this);
_day++;
return temp;
}
这是后置++,为了区分前置++和后置++,规定后置++参数中写一个int,用来表示后置++。
前置++先对day进行+1操作,然后把处理过的自定义类型返回,此时返回引用,用引用可以提高效率,因为不用引用还需要进行一次拷贝构造操作,用引用就不需要进行拷贝构造操作。
后缀++返回的值是++前的值,因此我们先拷贝构造一个副本,对本体的day进行++操作,返回副本的值,注意此时不能够返回引用,因为副本出了作用域就消失了,所以只能不传引用。
d1
++
会被转化为d1.operator++(int),从而调用后置++的函数。++
d1
会被转化为d1.operator++(),从而调用前置++的函数。
最后,感谢您阅读我的文章,希望这些内容能够对您有所启发和帮助。如果您有任何问题或想要分享您的观点,请随时在评论区留言。
同时,不要忘记订阅我的博客以获取更多有趣的内容。在未来的文章中,我将继续探讨这个话题的不同方面,为您呈现更多深度和见解。
谢谢您的支持,期待与您在下一篇文章中再次相遇!