当前位置 博文首页 > 落禅的博客:深入理解C++类和对象(中)
如果一个类里面什么也不写,那么它里面真的什么都没有吗?
答案是否定的,如果一个类是空类,那么编译器会为我们提供6个默认成员函数,分别为构造函数,析构函数拷贝构造函数,赋值用算符重载函数,&操作符重载,const修饰的取地址操作符重载,那么接下来本片文章将带你来理解这6个默认成员函数
构造函数是一个特殊的成员函数,类名与函数名相同,主要完成对对象的初始化
特点:
1.函数名与类名相同
2.没有返回值,也不写void
3.对象实例化时编译器会自动调用对应的构造函数
4.构造函数可以重载
5.如果类中没有显式定义构造函数,则C++编译器会自动生成一个默认的构造函数,一旦用户显式定义了编译器就不会自动生成
include<iostream>
using namespace std;
class Person
{
public:
//1.编译器提供的默认构造函数,什么都不做
Person()
{
}
//2.带参数的默认构造函数
Person(string name,int age )
{
this->_name = name;
this->_age = age;
}
void Print()
{
cout << "姓名:" << this->_name << " " << "年龄:" << this->_age << endl;
}
private:
string _name;
int _age;
};
int main()
{
Person P1("小王",16);
P1.Print();
Person P2;
P2.Print;
system("pause");
return 0;
}
如上所示,P1调用的即为我们写的构造函数,它能够对对象进行初始化,结果如下
当我们调用默认的构造函数时,结果如下:
编译器提供的默认构造函数里面什么都没有,注意调用默认构造函数时写成:P2,不要写成P2(),编译器会将P2()当成函数的声明
当然关于构造函数的书写,我们更加推荐下面这种写法:写成缺省函数,这样在我们为其赋值时就会方便很多
//3.全缺省的默认构造函数
Person(string name = "小王", int age = 16)
{
this->_name = name;
this->_age = age;
}
Ps:无参的构造函数和全缺省的构造函数都称为默认构造函数,标签默认构造函数只能有一个。注意:无参构造函数,全缺省构造函数,我们没写编译器默认生成的构造函数,都可以认为是默认成员函数
在对象销毁时自动调用析构函数,完成对类的一些资源的清理工作
特点:
1.析构函数名是在类名前面加上字符~
2.无参数,没有返回值,不写void
3.一个类有且只有一个析构函数,系统会自动生成默认的析构函数
4.对象生命周期结束时,C++编译系统自动调用析构函数
5.如果有成员变量是在堆区创建出来的,我们要在析构函数中手动释放
例如:
//默认构造函数
~Person()
{
}
#include<iostream>
using namespace std;
class Person
{
public:
Person(string name = "小王", int age = 16)
{
this->_name = name;
this->_age = age;
}
void Print()
{
cout << "姓名:" << this->_name << " " << "年龄:" << this->_age << endl;
}
~Person()
{
cout << "person析构函数的调用" << endl;
}
private:
string _name;
int _age;
};
int main()
{
Person P2("小王",16);
P2.Print();
system("pause");
return 0;
}
效果如下:
在这种在栈上定义的变量析构函数可能没什么用,但是要是该对象中有变量在堆区被malloc/new出来,很多人会忘记最后在函数结束时free/delete,这就造成了内存泄漏问题,但是有了析构函数,我们就可以将free或者delete函数写到析构函数中去,然后在程序结束的时候编译器会自动free/delete掉这块空间,就会很方便
#include<iostream>
using namespace std;
class Person
{
public:
Person()
{
arr = (int*)malloc(sizeof(int) * 1);
}
~Person()
{
free(arr);
cout << "Person的析构函数的调用" << endl;
}
int* arr;
};
int main()
{
Person p;
return 0;
}
如上所示我们在堆区开辟成员变量,到结束时析构函数自动释放
完成对一个对象的拷贝,例如:
//系统提供的拷贝构造函数
#include<iostream>
using namespace std;
class Person
{
public:
Person(int a)
{
this->_a = a;
}
void Printf()
{
cout << "_a=" << this->_a << endl;
}
private:
int _a;
};
int main()
{
Person P1(10);
P1.Printf();
Person P2(P1);
P2.Printf();
return 0;
}
如上所示,我们定义了一个person类,创建了P1,P2两个对象,并将P1->_a初始化为10,然后我们写了P2(P1),然后打印P2->_a发现也变成了10,这其实是调用了拷贝构造函数,下面我们先来了解一下什么是拷贝构造函数:
1.拷贝构造函数是构造函数的一个重载形式
2.拷贝构造函数的参数有且只有一个必须使用引用传参,使用传值方式会引发无穷递归调用
3.若显示定义,系统生成默认的拷贝构造函数,默认的拷贝构造函数对象按内存存储字节序完成拷贝,这种拷贝我们称为浅拷贝,或者值拷贝
4.编译器提供的构造函数是按照字节序进行拷贝的,因此又叫做值拷贝或者浅拷贝
当我们写下Person P2(P1)时,编译器调用了P2.Person(P1),以上面例子为例,编译器提供的默认拷贝构造函数如下所示:
Person(const Person& p)
{
this->_a = p._a;
}
运行结果如下所示:
看上面的例子,这是最简单的拷贝构造函数,大家仔细看上面的函数原型,要是我把里面的Person(const Person&p)换成Person(const Person p)可以吗,答案不行,要是换了,那么要调用拷贝构造函数,形参在传递的时候又要调用拷贝构造函数,之后一直调用拷贝构造函数,陷入无限的递归当中,最终使程序奔溃,而当使用引用时就不会存在这样的问题,看下面的实例:
有了拷贝构造函数我们在对象的拷贝上就会省去大量的时间,但是在有些情况下我们仍然需要自己写拷贝构造函数,例如:
#include<iostream>
using namespace std;
class Person
{
public:
Person(int a)
{
_a = (int*)malloc(sizeof(int) * a);
}
~Person()
{
if (_a != NULL)
{
free(_a);
_a = NULL;
}
}
private:
int *_a;
};
int main()
{
Person P1(10);
Person P2(P1);
return 0;
}
当我启动程序时程序直接奔溃,上面的例子我们将变量创建到堆区,在构造函数中我们初始化这块内存空间,在析构函数中释放这块空间,然后创建P1,P2,将P1拷贝给P1,这是错误的,程序在执行时,首先调用P1的构造函数,再调用P2的构造函数,在程序结束时,调用P2的析构函数,再调用P1的析构函数,但是此时P1->_a和P2->_a指向同一块内存空间,我们首先调用P2的析构函数释放这块内存空间,此时_a所指向的这块空间已经被释放,然后我们又调用P1的析构函数对这块已经释放过的空间再次进行释放,那么毫无疑问会出现错误,这就是C++中非常经典的深浅拷贝问题,关于这个问题下次我会专门开一个专题详细探讨这个问题,这里就不在赘述了
1.赋值运算符重载:
在C++中默认提供对赋值运算符的重载
例如:
#include<iostream>
using namespace std;
class Person
{
public:
Person()
{
}
Person(int a)
{
this->a = a;
}
void Print()
{
cout << a << endl;
}
int a;
};
int main()
{
Person P1(10);
Person P2;
P2 = P1;
P1.Print();
P2.Print();
return 0;
}
上述代码中我们直接将P1赋值给P2(P2=P1),实现了自定义类型的赋值,这是由于C++中成员中默认提供承载的赋值函数,它的主要特点如下所示:
1.参数类型
2.返回值
3.检测是否自己给自己赋值
4.返回*this
5.一个类如果没有显示定义赋值运算符,编译器也会自动生成一个,完成对象按字节序的值拷贝
它的函数原型如下所示:
当时,上述只是最简单的运算符重载,我们还可以写点稍微复杂一点的
2.其它运算符的重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有返回值类型,函数名及其参数列表,其返回值类型与参数列表与普通的函数类似
函数名字:关键字operator+操作符+(参数列表)
函数原型:返回值类型 operator+操作符+(参数列表)
注意:
1.不能通过连接其它符号创建新的操作符
2.重载操作符必须有一个类类型或者枚举类型的操作数
3.用于内置类型的操作数,其含义不能改变,例如:+,不能改变其含义
4.作为类成员的重载函数时,其形式看起来比操作数数目少1的成员函数的操作符有一个默认的形参this,限定为第一个参数
5.* 、. 、 ::、sizeof、 ? :、 注意以上5个运算符不能重载
下面我们将对几种比较常见的运算符进行重载操作:
+运算符重载:实现两个自定义类型的相加
#include<iostream>
using namespace std;
class person
{
public :
person()
{
}