C++类基础2——构造函数和析构函数

为什么要引入构造函数

我们先创建一个类AA

#include<iostream>
using namespace std;
class AA
{
private:
int a;
public:
void A()
{
cout<<a<<endl;
}
};
int main()
{
AA e;
}

C++的目标之一是让使用类对象就像使用标准类型一样,就像下面这样子使用

int a;//第一步
int b=9;//第二步
int c=b;//第三步

但是我们发现就上面的e对象只能实现第一步,不能给e赋值,也不能将另一个对象赋给e,也就是说,常规的初始化语法不适用于类型AA

#include<iostream>
using namespace std;
class AA
{
private:
	int a;
public:
	void A()
	{
		cout << a << endl;
	}
};
int main()
{
	AA e;
	AA w = { 6 };//这是不行的
	AA g = e;//这是不行的
}

不能这么初始化的原因是因为数据部分的访问状态是私有的,这意味着程序不能直接访问数据成员,只能通过公有函数来初始化

有人想说了啊,把数据部分设为公有的不就好了嘛!!我们先试试

#include<iostream>
using namespace std;
class AA
{
public:
	int a;

	void A()
	{
		cout << a << endl;
	}
};
int main()
{
	AA e;
	AA w = { 6 };//完全可以
	AA g = w;//可以
}

可以发现使数据成员变为公有的,就可以按刚才的方式进行初始化了

但是使数据成员变为公有违背了类的一个主要初衷:数据隐藏

为了使类对象能正常使用常规初始化语法,我们引入了构造函数

声明和定义构造函数

c++专门提供了一个特殊的成员函数-------类构造函数,专门用于构造新对象,将值赋给它们的数据成员。更准确的说,c++为这些成员函数提供了名称和使用语法,而程序员需要提供方法定义,名称与类名相同。

c++的构造函数的名称与它所在类的类名相同,且它有一个有趣的特征,虽然没有返回值,但没有被声明为void类型。实际上构造函数没有声明类型。

构造函数的结构如下:

它所在类的类名 (参数列表)
{
函数体
}

我们可以看个例子

#include<iostream>
using namespace std;
class AA
{
private:
	int a;
public:
	//构造函数
	AA(int a_)//注意没有返回类型,原型位于类声明的公有部分
	{
		a = a_;
	}
};
int main()
{

	AA w = { 6 };//完全可以
	AA g = w;//可以
}

我们也可以通过构造函数来初始化其中的私有数据了,注意没有返回类型,原型位于类声明的公有部分

成员名和参数名

不熟悉构造函数的你可能会试图将类成员名称用作构造函数的参数名

就像下面这样子

class AA
{
private:
	int a;
public:
	//构造函数
	AA(int a)
	{
		....
	}
};

这是错误的。构造函数的参数表示的不是类成员,是赋给类成员的值。因此,参数名不能与类成员相同,否则会出现下面这样子的错误

class AA
{
private:
	int a;
public:
	//构造函数
	AA(int a)
	{
		a=a;//这种错误
	}
};

为了避免这种混乱,一种常见的作法是在数据成员名中使用m_前缀

class AA
{
private:
	int m_a;
public:
	//构造函数
	AA(int a)
	{
		m_a=a;
	}
};

另外一种就是在成员名后加_

class AA
{
private:
	int a_;
public:
	//构造函数
	AA(int a)
	{
		a_=a;
	}
};

使用构造函数

每次创建类对象(甚至使用new动态内存分配)时,c++都使用类构造函数

c++提供了两种使用构造函数来初始化对象的方式。

第一种是显式地调用构造函数

#include<iostream>
using namespace std;
class AA
{
private:
	int a_;
public:
	//构造函数
	AA(int a)//注意没有返回类型,原型位于类声明的公有部分
	{
		a_ = a;
	}
};
int main()
{
 AA w=AA(3);//显式调用构造函数
}

第二种是隐式调用构造函数

#include<iostream>
using namespace std;
class AA
{
private:
	int a_;
public:
	//构造函数
	AA(int a)//注意没有返回类型,原型位于类声明的公有部分
	{
		a_ = a;
	}
};
int main()
{
 AA w(3);//隐式调用构造函数
//和AA w=AA(3)是等价的
}

第三种,使用new

#include<iostream>
using namespace std;
class AA
{
private:
	int a_;
public:
	//构造函数
	AA(int a)//注意没有返回类型,原型位于类声明的公有部分
	{
		a_ = a;
	}
};
int main()
{
AA*e=new AA(3);
}

这创建一个AA对象,将其初始化为参数提供的值,并将该对象的地址赋给e指针。在这种情况下,对象没有名称,但可以使用指针来管理对象。

不能通过对象来调用构造函数

可以使用对象来调用一般函数,但是不能使用对象来调用构造函数

#include<iostream>
using namespace std;
class AA
{
private:
	int a_;
public:
	//构造函数
	AA(int a)//注意没有返回类型,原型位于类声明的公有部分
	{
		a_ = a;
	}
};
int main()
{
 AA w(3);
w.AA(2);//这是不可以的
}

构造函数的其他用处

构造函数不仅仅可以用来初始化新对象,还可以用来赋值

我们可以看个例子

#include<iostream>
using namespace std;
class AA
{
private:
	int a_;
public:
	
	AA(int a)
	{
		a_ = a;
	}
	AA()
	{}
};
int main()
{
	AA w;
	w = AA(2);//用构造函数给已有对象赋值

}

实际上下面这两种方式有根本性差别

AA w=AA(2);
w= AA(2);

 第一条是初始化,它创建有指定值的对象,可能会创建临时对象(也可能不会)

第二条语句是赋值,在赋值前总会导致在赋值前创建一个临时对象,再将临时对象复制到w里

所以我们尽量使用初始化语句 

成员初始化列表的语法

如果AA是一个类,而a_,b_,c_都是这个类的数据成员,则类构造函数可以使用如下语法来初始化数据成员

AA::AA(int a,int b):a_(a),b_(b),c_(a*b)
{}

上述代码将a_初始化为a,将b_初始化为b,将c_初始化为a*b。从概念上说,这些初始化工作都是在对象创建时完成的,此时还没执行括号内的任何代码。请注意下面几点:

1.这种格式只能用于构造函数

2.必须使用这种方式来初始化非静态const数据成员

3.必须用这种格式来初始化引用数据成员

数据成员被初始化的顺序与它们出现在类声明中的顺序相同,与初始化器中的排列顺序无关

比如

#include<iostream>
using namespace std;
class AA
{
private:
	int a_;
	int b_;
public:
	AA(int a, int b) :b_(b),a_(a)
	{}

};
int main()
{
	AA w(2, 3);
}

在上面这个例子中,先声明的是a_,所以先初始化a_注意不能将成员初始化列表语法用于构造函数之外的其他类方法

 

默认构造函数

默认构造函数是在未提供显式初始值时,用来创建对象的构造函数。也就是说,它是用于下面这种声明的构造函数。

AA e;

这就有点像下面这种情况一样

int a;

默认构造函数有三种,分为隐式版本和显式版本,其中隐式版本有一种,显式版本有两种

隐式版本

如果在一个类里没有提供任何构造函数,则c++将自动提供默认构造函数,这就是构造函数的隐式版本,不做任何工作。

举个例子 

#include<iostream>
using namespace std;
class AA
{
private:
int a;
public:
void A()
{
cout<<a<<endl;
}
};
int main()
{
AA e;//c++自动提供默认构造函数
}

AA类没有提供任何构造函数, 则在调用AA e;时编译器将自动提供默认构造函数,这个c++自动提供的默认构造函数可能是下面这样子的;

AA()
{}
//什么都没有

c++自动提供的默认构造函数没有参数,因为声明中不包含任何值

显式版本

注意了:当且仅当没有定义任何构造函数时(注意注意),编译器才会自动提供默认构造函数

为类定义了非默认构造函数后(注意注意)如果我们还想用下面这种语句,程序员就必须为它提供显式的默认构造函数,否则会出错

AA w;

看个例子

#include<iostream>
using namespace std;
class AA
{
private:
	int a_;
public:
	//构造函数
	AA(int a)
	{
		a_ = a;
	}
};
int main()
{
 AA w;//编译器会报错
}

如果想像上面这么使用的话,就必须提供显式的默认构造函数

显式的默认构造函数有两种

第一种


给已有的构造函数的所有参数提供默认值;

如果对函数参数默认值不熟悉的可以看看这个:http://t.csdnimg.cn/vMUTT

我们可以看个例子

#include<iostream>
using namespace std;
class AA
{
private:
	int a_;
    int b_
public:
	
	AA(int a=4,int b=3)//注意是所有参数都有默认值
	{
		a_ = a;
        b_ = b;
	}
};
int main()
{
 AA w;
}

这没问题

如果不是所有参数都有默认值的话,那就不是默认构造函数

#include<iostream>
using namespace std;
class AA
{
private:
	int a_;
    int b_
public:
	
	AA(int a,int b=3)//这不是默认构造函数
	{
		a_ = a;
        b_ = b;
	}
};
int main()
{
 AA w;//系统会报错
}

这就会出现问题

第二种

用函数重载来定义另一个构造函数——一个没有参数(函数体可以自己写点东西,也可以说明也不写)的构造函数

就像这个一样

AA()
{}

对函数重载不熟悉的可以看看这个:http://t.csdnimg.cn/341PH

举个例子

#include<iostream>
using namespace std;
class AA
{
private:
	int a_;
public:
	//构造函数
	AA(int a)
	{
		a_ = a;
	}
    AA()
    {}
};
int main()
{
 AA w;//完全没问题
}

注意点

一个类里只能有一个显式的默认构造函数

也就是说,下面这种写法是错的

class AA
{
private:
	int a_;
    int b_
public:
	
	AA(int a=7,int b=3)//默认构造函数
	{
		a_ = a;
        b_ = b;
	}
    AA()//默认构造函数
    {}
};

默认构造函数的函数体

默认构造函数的函数体可以为空,也可以自己写点东西进去

一般来说默认构造函数的函数体内容应该对所有的类成员进行初始化

就像下面这样子

class AA
{
private:
	int a_;
    int b_
public:
	
	AA(int a,int b=3)//非默认构造函数
	{
		a_ = a;
        b_ = b;
	}
    AA()//默认构造函数
    {
       a_=7;
       b_=3;
     }
};

析构函数

为什么要引入析构函数

析构函数执行与构造函数相反的操作;构造函数初始化对象的非static数据成员,还可能做一些其他工作;析构函数释放对象使用的资源,并销毁对象的非static数据成员。

析构函数是类的一个成员函数,名字由波浪号接类名构成。它没有返回值,也不接受参数:

也就是说AA类的析构函数原型应该是~AA()

我们看个例子

class AA
{
private:
	int a_;
    int b_
public:
	
	AA(int a=7,int b=3)//默认构造函数
	{
		a_ = a;
        b_ = b;
	}
    ~AA()//析构函数
    {}
};

由于析构函数不接受参数,因此它不能被重载。对一个给定类,只会有唯一一个析构函数。 

析构函数完成什么工作

如同构造函数有一个初始化部分和一个函数体,析构函数也有一个函数体和一个析构部分。

在一个构造函数中,成员的初始化是在函数体执行之前完成的,且按照它们在类中出现的顺序进行初始化。

在一个析构函数中,首先执行函数体,然后销毁成员。成员按初始化顺序的逆序销毁。

在对象最后一次使用之后,析构函数的函数体可执行类设计者希望执行的任何收尾工作。通常,析构函数释放对象在生存期分配的所有资源。

在一个析构函数中,不存在类似构造函数中初始化列表的东西来控制成员如何销毁,析构部分是隐式的

成员销毁时发生什么完全依赖于成员的类型。销毁类类型的成员需要执行成员自己的析构函数。内置类型没有析构函数,因此销毁内置类型成员什么也不需要做

隐式销毁一个内置指针类型的成员不会delete它所指向的对象

与普通指针不同,智能指针是类类型,所以具有析构函数。因此,与普通指针不同,智能指针成员在析构阶段会被自动销毁。

我们来看些例子

例如构造函数用new来分配内存时,析构函数则用delete来释放内存。

如果说构造函数没有使用new,我们可以不写析构函数,这时编译器会自动生成一个什么都不做的隐式析构函数

class AA
{
private:
	int a_;
    int b_
public:
	
	AA(int a,int b=3)//这不是默认构造函数
	{
		a_ = a;
        b_ = b;
	}
    ~AA()
    {}
//因为构造函数没有使用new,所以可以什么都不写
};

构造函数不用new,那我们也可以不写析构函数,系统会自己提供一个什么都不做的析构函数

class AA
{
private:
	int a_;
    int b_
public:
	
	AA(int a,int b=3)//这不是默认构造函数
	{
		a_ = a;
        b_ = b;
	}
  
//因为构造函数没有使用new,所以可以什么都不写
};

什么时候会调用析构函数

无论何时一个对象被销毁,就会自动调用其析构函数:

  1. 变量在离开其作用域时被销毁。
  2. 当一个对象被销毁时,其成员被销毁。
  3. 容器(无论是标准库容器还是数组)被销毁时,其元素被销毁。
  4. 对于动态分配的对象,当对指向它的指针应用delete运算符时被销毁
  5. 对于临时对象,当创建它的完整表达式结束时被销毁。

由于析构函数自动运行,我们的程序可以按需要分配资源,而(通常)无须担心何时释放这些资源。

例如,下面代码片段定义了四个Sales_data对象:

{ // 新作用城
// p和p2指向动态分配的对象
Sales data *p = new Sales data; // p是一个内置指针
auto p2 = make_shared<Sales data>();// p2是一个shared _ptr 
Sales data item(*p); //拷贝构造函数将*p拷贝到item中
vector<Sales data> vec; //局部对象
vec,push back(*p2); //拷贝p2指向的对象
//对p指向的对象执行析构函数
delete p;
} // 退出局部作用城;对item、p2和vec调用析构函数
// 销毁p2 会递减其引用计数;如果引用计数变为 0,对象被释放
// 销毁vec会销毁它的元素

 每个Sales_data 对象都包含一个string成员,它分配动态内存来保存bookNo成员中的字符。但是,我们的代码唯一需要直接管理的内存就是我们直接分配的Sales_data对象。我们的代码只需直接释放绑定到p的动态分配对象。

其他Sales_data对象会在离开作用域时被自动销毁。当程序块结束时,vec、p2和item 都离开了作用域,意味着在这些对象上分别会执行vector、shared_ptr和Sales_data 的析构函数。vector的析构函数会销毁我们添加到vec的元素。shared_ptr的析构函数会递减p2指向的对象的引用计数。在本例中,引用计数会变为0,因此shared ptr的析构函数会deletep2分配的Sales data对象。

在所有情况下,Sales_data的析构函数都会隐式地销毁bookNo成员。销毁bookNo会调用string的析构函数,它会释放用来保存ISBN的内存。

当指向一个对象的引用或指针离开作用域时,析构函数不会执行。

我们可以再看个例子

注意:对象消失时,编译器会自动调用析构函数,不必手动调用

我们可以看个例子

#include<iostream>
using namespace std;
class AA
{
private:
	int a_;
public:
	AA(int a)
	{
		a_ = a;
	}
    ~AA()
    {
cout<<"析构函数被调用"<<endl;
    }
};
int main()
{
 AA w(2);
}

我们运行程序,发现什么也没打印,难道是析构函数不会被调用? 

实际上,当类对象被销毁时才会调用析构函数,上面这个例子编译器只会在mian()执行完毕后才会调用析构函数,如果我们想看到析构函数被调用,可以改进这个代码

#include<iostream>
using namespace std;
class AA
{
private:
	int a_;
public:
	AA(int a)
	{
		a_ = a;
	}
    ~AA()
    {
cout<<"析构函数被调用"<<endl;
    }
};
int main()
{
{
 AA w(2);
}
//w在此处已经被销毁
return 0;
}

这时我们就能看到析构函数被调用了 

 

合成析构函数

当一个类未定义自己的析构函数时,编译器会为它定义一个合成析构函数。

类似拷贝构造函数和拷贝赋值运算符,对于某些类,合成析构函数被用来阻止该类型的对象被销毁。

如果不是这种情况,合成析构函数的函数体就为空。

例如,下面的代码片段等价于Sales data的合成析构函数:

class Sales data {
public:
//成员会被自动销毁,除此之外不需要做其他事情
~Sales data(){}
// 其他成员的定义,如前
};


在(空)析构函数体执行完毕后,成员会被自动销毁。特别的,string的析构函数会数调用,它将释放bookNo成员所用的内存。

认识到析构函数体自身并不直接销毁成员是非常重要的。

成员是在析构函数体之后隐含的析构阶段中被销毁的

在整个对象销毁过程中,析构函数体是作为成员销毁步骤之外的另一部分而进行的。

在构造函数里使用new时的注意事项

如果在构造函数中使用new来初始化指针成员,应该在析构函数里使用delete

new和delete必须相互兼容。new对应delete,new[]对应delete[].

如果有多个构造函数,必须以相同的方式使用new。要么都带中括号,要么都不带



 

  • 37
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值