在C++中,系统给了6个默认的函数:构造函数、析构函数、拷贝构造函数u、赋值运算符的重载函数、取地址操作符的重载函数和const修饰的取地址操作符的重载函数。
1、构造函数
构造函数的函数名与类名相同,比如class Goods{};它的默认构造函数就是Goods(),没有返回值。
构造函数时系统调用的,如果自己在类中写了,系统就不会提供,系统提供的构造函数都是共有的(public)、内联的。
构造函数的作用就是,生成新对象的时候对“对象”进行初始化用的。
对象的生成有两个步骤:1、开辟内存。2、内存空间进行初始化。构造函数就内存空间是在初始化的时候系统调用的。所以构造函数不能手动调用。
#include<iostream>
#pragma warning(disable:4996);
class Goods
{
public:
Goods()
{
std::cout<<"Goods::Goods()"<<std::endl;
}
Goods(int amount)
{
std::cout<<"Goods::Goods(int )"<<std::endl;
mname = new char[1]();
mamount = amount;
}
Goods(char *name,float price)
{
std::cout<<"Goods::Goods(char ,float )"<<std::endl;
mname = new char[strlen(name)+1]();
strcpy(mname,name);
mprice = price;
}
Goods(char *name,float price,int amount)
{
std::cout<<"Goods::Goods(char ,float ,int )"<<std::endl;
mname = new char[strlen(name)+1]();
strcpy(mname,name);
mprice = price;
mamount = amount;
}
private:
char *mname;
float mprice;
int mamount;
};
int main()
{
Goods goods1;
Goods goods2("goods2",10.5);
Goods goods3(20);
Goods goods4("goods4",10.5,20);
return 0;
}
从上面的代码可以看出,一个类中可以有多个构造函数,这也就是说,构造函数是可以重载的。一个对象的生成可以有不同的方式,比如上面的goods1到goods4每一个对象都是调用不同的构造函数生成的。
2、析构函数
析构函数的函数名是在类名前加~,比如class Goods{};它的默认析构函数就是~Goods(),没有返回值。
我们通过观察上面的代码就会发现,上面每一个构造函数都会申请了堆上的资源,但是最后并没有释放,是不是造成了内存泄露?没存,在类中每一个对象都有它自己的资源,在这个对象生存周期结束要销毁的时候系统就会将对象的资源释放掉,但是它是如何释放的呢,这就是我们要说的析构函数了。
析构函数就是专门用来释放资源的,需要注意的是,它不可以重载,但是可以手动调用。手动调用时一定要注意自己写的代码有没有使用堆内存,如果使用了堆内存,手动调用之后,对象的生存周期并不会缩短,在对象生存周期结束的时候,系统还会调用一次。就会造成重复释放资源。
#include<iostream>
#pragma warning(disable:4996)
class Goods
{
public:
Goods()
{
std::cout<<"Goods::Goods()"<<std::endl;
mname = new char[1]();
}
Goods(int amount)
{
std::cout<<"Goods::Goods(int )"<<std::endl;
mname = new char[1]();
mamount = amount;
}
Goods(char *name,float price)
{
std::cout<<"Goods::Goods(char ,float )"<<std::endl;
mname = new char[strlen(name)+1]();
strcpy(mname,name);
mprice = price;
}
Goods(char *name,float price,int amount)
{
std::cout<<"Goods::Goods(char ,float ,int )"<<std::endl;
mname = new char[strlen(name)+1]();
strcpy(mname,name);
mprice = price;
mamount = amount;
}
~Goods()
{
std::cout<<"Goods::~Goods()"<<std::endl;
delete[] mname;
}
private:
char *mname;
float mprice;
int mamount;
};
int main()
{
Goods goods1;
// goods1.~Goods();
// std::cout<<"------------------"<<std::endl;
// goods1.shell();
Goods goods2("goods2",10.5);
Goods goods3(20);
Goods goods4("goods4",10.5,20);
return 0;
}
我们可以看出,创建了几个对象,最后就调用了几个析构函数。最后要说的一点就是:先构造的对象后析构,后构造的对象先析构。因为对象的信息是在栈上存储的,先构造的先压栈后构造的后压栈。
3、拷贝构造函数。
拷贝构造函数的函数名与构造函数一样,它的作用是用一个已存在的对象来生成一个新的对象。比如class Goods{};它的默认构造函数就是Goods(const Goods& rhs),没有返回值。
如果程序员在代码中自己没有写拷贝构造函数,系统也会提供默认的拷贝构造函数。但是系统提供的拷贝构造函数,它只能进行浅拷贝,不会对堆区资源进行复制。所以在类中,如果没有用到堆区资源的时候可以使用默认的拷贝构造函数,如果使用了堆区资源,建议自己写拷贝构造函数,防止浅拷贝的发生。
#include<iostream>
#pragma warning(disable:4996)
class Goods
{
public:
Goods()
{
std::cout<<"Goods::Goods()"<<std::endl;
mname = new char[1]();
}
Goods(int amount)
{
std::cout<<"Goods::Goods(int )"<<std::endl;
mname = new char[1]();
mamount = amount;
}
Goods(char *name,float price)
{
std::cout<<"Goods::Goods(char ,float )"<<std::endl;
mname = new char[strlen(name)+1]();
strcpy(mname,name);
mprice = price;
}
Goods(char *name,float price,int amount)
{
std::cout<<"Goods::Goods(char ,float ,int )"<<std::endl;
mname = new char[strlen(name)+1]();
strcpy(mname,name);
mprice = price;
mamount = amount;
}
Goods(const Goods& rhs)
{
std::cout<<"Goods::Goods(const Goods& )"<<std::endl;
mname = new char[strlen(rhs.mname)+1];
strcpy(mname,rhs.mname);
mprice = rhs.mprice;
mamount = rhs.mamount;
}
~Goods()
{
std::cout<<"Goods::~Goods()"<<std::endl;
delete[] mname;
}
private:
char *mname;
float mprice;
int mamount;
};
int main()
{
Goods goods4("goods4",10.5,20);
Goods goods5(goods4);
return 0;
}
我们可以看到,这段代码先是调用了三个参数的构造函数生成了goods4然后再调用拷贝构造函数用goods4生成goods5。拷贝构造函数的形参使用了,const Goods& rhs,而不使用Goods rhs或者Goods *rhs是有原因的。
形参如果用Goods rhs 来传递,在实参往形参传递的过程中又是一个用已存在对象来生成新对象的过程,又得调用拷贝构造函数,如此递归下去,从而导致栈溢出。
形参如果用Goods *rhs来传递实参与形参的值,那不就成了构造函数,而不是拷贝构造函数。
4、赋值运算符的重载函数
我们就以Goods类为例。赋值运算符的重载函数的函数原型为Goods& operator=(const Goods&rhs)
赋值运算符的重载函数的作用是:把一个已生成的对象赋值给相同类型的已存在对象。
实现赋值运算符的重载函数需要实现以下4个需求:
(1)、自赋值问题。赋值时如果发现是自己给自己赋值,就没有必要进行复制,直接返回原对象。
(2)、释放旧资源。一定要把已存在目的对象所占用资源释放掉,如果不释放,就会造成内存泄露问题。
(3)、申请新资源。如果原对象使用了堆资源,一定也要给目的对象申请同样的对资源,将原对象的堆资源复制给目的对象,否则就会出现浅拷贝问题。
(4)、赋值。最后一步才是将原对象的栈资源赋值给目的对象。
#include<iostream>
#pragma warning(disable:4996)
class Goods
{
public:
Goods()
{
std::cout<<"Goods::Goods()"<<std::endl;
mname = new char[1]();
}
Goods(int amount)
{
std::cout<<"Goods::Goods(int )"<<std::endl;
mname = new char[1]();
mamount = amount;
}
Goods(char *name,float price)
{
std::cout<<"Goods::Goods(char ,float )"<<std::endl;
mname = new char[strlen(name)+1]();
strcpy(mname,name);
mprice = price;
}
Goods(char *name,float price,int amount)
{
std::cout<<"Goods::Goods(char ,float ,int )"<<std::endl;
mname = new char[strlen(name)+1]();
strcpy(mname,name);
mprice = price;
mamount = amount;
}
Goods(const Goods& rhs)
{
std::cout<<"Goods::Goods(const Goods& )"<<std::endl;
mname = new char[strlen(rhs.mname)+1];
strcpy(mname,rhs.mname);
mprice = rhs.mprice;
mamount = rhs.mamount;
}
Goods& operator=(const Goods& rhs)
{
std::cout<<"Goods::Goods& operator=(const Goods& )"<<std::endl;
if(this == &rhs)
{
return *this;
}
delete[] mname;
mname = new char[strlen(rhs.mname)+1];
strcpy(mname,rhs.mname);
mprice = rhs.mprice;
mamount = rhs.mamount;
return *this;
}
~Goods()
{
std::cout<<"Goods::~Goods()"<<std::endl;
delete[] mname;
}
void shell()
{
std::cout<<"shell"<<std::endl;
}
private:
char *mname;
float mprice;
int mamount;
};
int main()
{
Goods goods1;
Goods goods4("goods4",10.5,20);
goods1 = goods4;
return 0;
}
我们可以看到,赋值运算符的重载函数在传递参数的时候也用了const.原因有两个:1、防止修改了实参的值。在赋值的时候不能把原对象的数据做了更改。2、接收隐式生成的临时变量。如果赋值运算符有变是一个隐式生成的临时对象,隐式生成的临时对象是一个常量,常量的引用必须用const来修饰。
最后要提的一点就是临时量问题。
(1)、内置类型的临时量都是常量。
(2)、自定义类型的临时量是变量。
(3)、隐式生成对象的临时量是常量。
(4)、显示生成对象的临时量是变量。