在C++中,对于一个类,C++的编译器都会为这个类提供四个默认函数,分别是:
A() //默认构造函数
~A() //默认析构函数
A(const A&) //默认拷贝构造函数
A& operator = (const A &) //默认赋值函数。
这四个函数如果我们不自行定义,将由编译器自动生成这四个缺省的函数,下面让我们来看看这四个函数(重点是后两个)。
一. 构造函数
构造函数是一种特殊的成员函数,与其他成员函数不同,不需要用户来调用它,而是在建立对象时自动执行。构造函数的功能是由用户定义的,用户根据初始化的要求设计函数体和函数参数,可以是一个,也可以是多个,可以把构造函数理解为重载的一种(函数名相同,不会返回任何类型,也不可以是void类型,参数类型个数可不同)。
class Animal
{
private:
string name;
public:
Animal();//默认构造函数
Animal(string n);//也可以自定义构造函数
};
Animal::Animal()
{
//什么都不做
}
Animal::Animal(string n)
{
this->name = n;
}
int main()
{
//第一种实例化对象的方法
Animal * a = new Animal(); //将调用默认构造函数
Animal * b = new Animal("花狗"); //将调用自定义的构造函数,对name变量初始化。
//第二种实例化对象的方法
Animal c; //将调用默认构造函数
//注意:对于无参构造函数,不可以使用Animal c(),
Animal c("花狗");//将调用自定义构造函数,对name变量初始化。
return 0;
}
构造函数的作用就是对当前类对象起到一个初始化的作用,类对象不像我们基本类型那样,在很多时候都需要初始化一些成员变量。
可以看到构造函数被声明在public里面,那么可以声明在private里面吗?是可以的,只不过不能被外部实例化了,在设计模式中有一种单例模式,就是这样设计的,有兴趣的可以了解一下。
二. 析构函数
与构造函数相对立的是析构函数,这个函数在对象销毁之前自动调用,例如在构造函数中,我们为成员变量申请了内存,我们就可以在析构函数中将申请的内存释放,析构函数的写法是在构造函数的基础上加一个~符号,并且只能有一个析构函数。
class Animal
{
private:
string name;
public:
Animal();//默认构造函数
~Animal(); //默认析构函数
};
三. 拷贝构造函数
1.浅拷贝
class Animal
{
private:
string name;
public:
Animal()
{
name = "花狗";
cout << "Animal" << endl;
}
~Animal()
{
cout << "~Animal:" << (int)&name << endl;
}
};
int main()
{
Animal a;
Animal b(a);
return 0;
}
运行结果:
这个例子调用的是默认的拷贝构造函数(注意看控制台显示,调用了一次构造函数和两次析构函数),可以看出两个对象的成员变量地址是不一样的,当成员变量不存在指针类型是,这样做没什么问题,当类中有指针变量,自动生成的拷贝函数注定会出错,往下看。
2.深拷贝
我们将成员变量换成指针变量,继续实验。
class Animal
{
private:
//string name;
string * name;
public:
Animal()
{
name = new string("花狗");
cout << "Animal" << endl;
}
~Animal()
{
cout << "~Animal:" << (int)name << endl;
}
};
int main()
{
Animal a;
Animal b(a);
return 0;
}
运行结果:
可以看到两个对象的指针成员所指的内存相同(内存里面存着字符串:花狗),还记得析构函数的作用吗,在对象销毁之前自动调用,在构造函数中,我们为成员变量申请了内存,我们就可以在析构函数中将申请的内存释放。
现在在析构函数中加上对name释放的代码:
~Animal()
{
cout << "~Animal:" << (int)name << endl;
delete name;
name = NULL;
}
再运行发现程序崩溃了,调用一次构造函数,调用两次析构函数,两个对象的指针成员所指内存相同,name指针被分配一次内存,但是程序结束时该内存却被释放了两次,导致程序崩溃
而且发现当重复释放的两个指针分别属于两个类或者说是两个变量的时候,会发生崩溃,如果对一个变量多次释放则不会崩溃。
例如下面的代码将不会发生奔溃
string * a = new string("花狗");
delete a;
a = NULL;
cout << "第一次完成\n";
delete a;
a = NULL;
cout << "第二次完成\n";
现在我们已经知道对于指针进行浅拷贝会出现的奔溃的问题,那么通过自定义拷贝构造函数来解决浅拷贝的问题。
Animal(const Animal & a)
{
//name = s.name;
name = new string(*a.name);
}
之后运行程序不会崩溃,总结起来就是先开辟出和源对象一样大的内存区域,然后将需要拷贝的数据复制到目标拷贝对象。
四. 赋值函数
四个默认函数,当赋值函数最为复杂。
Animal& operator=(const Animal&obj)
{
if(this !=&obj)
{
data=obj.data;
}
return *this;
}
这是它的原型,类似 Animal a(b); Animal a = b; 这样的写法会调用拷贝构造函数。
而赋值函数是在当年对象已经创建之后,对该对象进行赋值的时候调用的,Animal a; a = b。
和拷贝构造函数一样,若类中有指针变量,自动生成的赋值函数注定会出错,老样子,先申请内存,再复制值即可完美解决。
Animal& operator=(const Animal&obj)
{
if(this !=&obj)
{
//默认是 name = obj.name;
name = new string(*obj.name);
}
return *this;
}
还有一个知识点就是运算符重载这一块,一个自定义类型的对象,如果想要进行预期的加减乘除之类的运算,或者是像内置类型一样,用cout输出一个类对象,这些都是需要我们来用代码告诉机器怎么做,都是需要我们来指定的。
还是拿这个类举例子,例如运算符+重载
class Animal
{
private:
string * name;
int age;
int num;
public:
Animal()
{
name = new string("花狗");
age = 5;
num = 4;
}
Animal& operator+(const Animal&obj)
{
if(this !=&obj)
{
string * s = name;
name = new string(*name + *obj.name);
delete s;
s == NULL;
this->age+=obj.age;
this->num+=obj.num;
}
return *this;
}
};
int main()
{
Animal a;
Animal b;
a = a+b;
//这样对象a里面的age成员的值是5,num成员的值是8,而*name的值将是"花狗花狗";
return 0;
{
cout输出的定义,主要注意的是要用到友元函数。
class Animal
{
//中间代码略
friend ostream& operator << (ostream& os, Animal& a)
{
os << *a.name << ":" << a.age << ":" << a.num;
return os;
}
};
运行结果:
微信官方交流群: