(一一六)类的构造函数和析构函数

简介:

类构造函数:

构造函数 是专门用于构造新对象、将值赋给它们的数据成员。

C++为这些成员提供了名称和使用语法,而程序员需要提供方法定义。名称与类名相同。

例如:Stock类的一个可能的构造函数是名为Stock()的成员函数。

构造函数的原型和函数头有一个有趣的特征——虽然没有返回值,但没有被声明为void类型。实际上,构造函数没有声明类型

 

 

声明和定义构造函数:

和使用普通函数(根据传递的参数给私有成员赋值)几乎一样,除了构造函数的函数名需要和类名保持一致。

例如:

void Player::Player(std::string namedouble hpsdouble atks)

{

Name = name;

Hp_s = hps;

Atk_s = atks;

hp_();

atk_();

}

 

这里将(一一五)的一个源代码文件中,Player类的start函数名改为了Player(和类名保持一致),这样的话,就可以在声明类对象的时候,同时对其初始化了。。

 

需要注意的是,传递的参数名(std::string namedouble hpsdouble atks)不能是私有成员的名字。否则调用本函数,实际上便是将私有成员的值赋给自己。

 

如代码:

#include<iostream>
#include<string>

class man	//创建类
{
private:
	std::string name;	//私有成员两个
	int year;
public:
	man(const char*na, int a)	//构造函数名同类名
	{
		name = na;	//传递参数来赋值
		year = a;
	}
	void show()	//显示
	{
		using namespace std;
		cout << name << " is " << year << " years old." << endl;
	}
};

int main()
{
	using namespace std;
	man a = { "wd",27 };
	a.show();

	system("pause");
	return 0;
}


输出:

wd is 27 years old.
请按任意键继续. . .

构造函数的调用方式:

①显式的:如man a = man("aa", 1); //注意,这里是小括号(圆的)

 

②隐式的:如man b("bb", 1); //注意,这里也是小括号(圆的)

 

C++11的列表初始化:如

man c = man"cc",1 };

man d = { "dd",1 };

man f{ "ff",1 };

以上三个是大括号,区别与小括号。小括号不支持第二种方式

 

④使用new时:如

man *a = new man"wd",27 }; //初始化

(*a).show(); //调用需要加括号,不能是*a.show()这样

 

⑤使用构造函数时,必须在声明的时候进行初始化,否则会提示类不存在默认构造函数。例如上面代码中,直接写man a; 编译器是会提示错误的;

 

⑥注意:无法使用对象名来调用构造函数(会提示使用的类型名);

一般来说,构造函数是创建对象时使用的。

 

⑦构造函数的参数,可以设置默认参数,就像使用带默认参数的函数那样使用(遵守相关规定)。如:man(const char*na = "xxx"int a = 10)

 

 

 

默认构造函数:

所谓默认构造函数,指的是在未提供显式初始值时,用来创建对象的构造函数。

 

例如,一个类,只有私有成员和公有成员(数据和函数),但未提供默认构造函数。那么C++将自动提供默认构造函数的隐式版本,不做任何工作(即可以只声明对象,但是不赋值)。

例如,若上面的man类未提供构造函数的话,那么其默认构造函数可能是:

man(){}

即既无参数,也无函数代码。

 

如果要使用默认构造函数,那么就应该给对象的私有成员进行赋值(至少,要有函数定义,不能只有函数原型),否则编译器就会提示出错(实际上也容易出问题)。

例如,至少是这样:

man(const char*na = "xxx"int a = 10) {}//使用默认构造函数的最简形式,注意,这里的参数并没有意义

和man() {}这种隐式的默认构造函数,是等价的;

 

推荐是这样:

man(const char*na = "xxx"int a = 10) //构造函数名同类名

{

name = na; //传递参数来赋值

year = a;

}

或者这样(效果相同,但前者可以多一个选择,以便在初始化时赋值):

man() //默认构造函数

{

name = "xxx"; //提供默认值

year = 1;

}

这样的话,假如用户在初始化的时候,若不赋值,则自动使用默认值。

 

 

 

 

析构函数:

用构造函数(无论是默认的还是用户自己定义的)创建对象后,程序负责跟踪该对象,直到过期为止。

 

对象过期时,程序将自动调用一个特殊的成员函数——析构函数

 

析构函数负责完成清理工作,按照教程所说,其很有用。

 

例如,如果构造函数使用new来分配内存,那么析构函数将使用delete来释放这些内存。(但貌似如果没有使用new,那么析构函数就将无事可做)

 

如果析构函数无事可做,那么就让编译器生成一个什么都不做的隐式析构函数。

 

 

构造名称的函数名,和类名是一样的;

而析构函数的名字,和类名也一样,不过前面还要额外加“~”。

 

例如:~man() {}这样

 

析构函数的原型:

析构函数没有参数,所以其函数原型必然是: ~类名();  这样

 

因为析构函数在上面那段代码里,并没有做什么事,所以使用的是隐式析构函数。但如果要展现,也可以为析构函数编写代码。如:

~man() { std::cout << "end" << std::endl; }

 

为了方便查看,我们创建一个块内的类对象,然后观察其在块结束时析构函数的作用,如代码:

#include<iostream>
#include<string>

class man	//创建类
{
private:
	std::string name;	//私有成员两个
	int year;
public:
	
	man()	//默认构造函数
	{
		name = "xxx";	//提供默认值
		year = 1;
	}
	void ab() { name = "aaa";year = 5; }
	void show()	//显示
	{
		using namespace std;
		cout << name << " is " << year << " years old." << endl;
	}
	~man() { std::cout << "end" << std::endl; }
};

int main()
{
	using namespace std;
	{	//用括号括起来的块,类对象只在块内存在
		man a;
		a.show();
		cout << "这里还在块内" << endl;
	}
	cout << "这里在块外了" << endl;


	system("pause");
	return 0;
}

输出:


xxx is 1 years old.
这里还在块内
end
这里在块外了
请按任意键继续. . .

在离开块后,析构函数执行了,因此多了一行end

 

 

 

何时调用析构函数:

通常由编译器决定,通常不应在代码中显式的调用析构函数(有关例外情况,需要到12章的“再谈定位new运算符”)。

 

①如果创建的是静态存储对象,则析构函数将在程序结束时自动调用(因为静态存储对象持续到程序结束)。

 

②如果创建的是自动存储类对象,那么析构函数将在程序执行完代码块时自动被调用(如上面那段函数)。

 

③如果对象是通过new创建的,则它将驻留在栈内存或自由存储区之中,当使用delete时,其将被自动调用。

 

④程序可以创建临时对象来完成特定的操作,在这种情况下,程序在结束对该对象的使用时,自动调用其析构函数(这个没搞明白是什么)。

 

 

另外,程序必然有一个析构函数,要么类的编写者提供,要么有程序提供一个隐式析构函数(什么都不干,但需要有)。

 

 

 

另外:析构函数和构造函数的定义,都可以在类外进行定义,只需要在函数内部有一个函数声明即可。

 

 

 

优化类代码,以及类成员函数的各种使用:

如代码:

//1.h	存放类的定义、类成员函数的原型等
#pragma once
#include<iostream>
#include<string>
class man	//创建类
{
private:
	std::string name;	//私有成员两个
	int year;
public:
	man();	//默认构造函数
	man(const std::string a, int b = 0);	//因为存在重载析构函数,因此不能提供2个默认参数
	inline void update()	//成员更新,使用内联函数
	{
		name = "帅", year = 99;
	}
	void show();	//显示
	~man();	//析构函数,输出文字,表示析构函数确实运行了
};


//1.cpp main()函数,赋值,及调用类方法
#include<iostream>
#include<string>
#include"1.h"	//调用包含类定义的头文件1.h

int main()
{
	using namespace std;
	man b;	//使用默认析构函数
	b.show();	//调用类方法,输出对象的值

	{	//用括号括起来的块,类对象只在块内存在
		cout << "这里开始在块内" << endl;
		man a("wd",27);	//使用自定义析构函数,并不使用默认参数
		a.show();
		man b("mmmm");	//使用自定义析构函数,并使用默认参数。另外,这里的b隐藏了块外的b
		b.show();
		b.update();	//更新对象的数据
		b.show();
	}
	cout << "这里在块外了" << endl;


	system("pause");
	return 0;
}

//2.cpp	存放类的函数定义
#include<iostream>
#include"1.h"

//因为是类的函数定义,因此要加上作用域解析运算符::

man::man()	//默认构造函数
{
	name = "\"NO NAME\"";	//提供默认值
	year = 0;
}
man::man(const std::string a, int b)	//因为存在重载析构函数,因此不能提供2个默认参数
{
	name = a;
	if (b < 0)
	{
		std::cout << "人不可能小于0岁,因此,设置为0岁" << std::endl;
		b = 0;
	}
	else year = b;
}
void man::show()	//显示
{
	using namespace std;
	cout << name << " is " << year << " years old." << endl;
}
man::~man()	//析构函数,输出文字,表示析构函数确实运行了
{
	std::cout << name << " end." << std::endl;
}

输出:

"NO NAME" is 0 years old.
这里开始在块内
wd is 27 years old.
mmmm is 0 years old.
帅 is 99 years old.
帅 end.
wd end.
这里在块外了
请按任意键继续. . .


总结:

①块内,类对象a先声明并定义,类对象b其后。由于是自动变量,遵循了LIFO原则,因此后定义的类对象b被首先执行析构函数;

 

②可以在头文件声明类,然后在源代码文件中定义类的成员函数;

 

③类的构造函数可以使用重载函数,也可以给重载函数使用默认参数;

但是需要注意的是,如果一个重载函数无参数,那么另一个重载函数就不能同时给所有参数带默认值(会引起重载函数调用不明确,而导致冲突问题);

 

④不知道为何,似乎没办法在这里使用内联函数,例如: inline void man::update() 会提示说该函数在main()函数中被调用。

 

⑤使用内联函数的话,那么函数定义应放在类声明里面(即放弃使用函数原型,直接把函数定义放在函数原型原本的位置)。

 

 

 

C++11的列表初始化:

如代码:

	//这两个是小括号,构造函数可用
	man a = man("aa", 1);	//注意,这里是小括号(圆的)
	man b("bb", 1);	//注意,这里也是小括号(圆的)

	//这三个是大括号,是C++11增加的列表初始化
	man c = man{ "cc",1 };
	man d = { "dd",1 };
	man f{ "ff",1 };

另外,C++11还提供了名为std::initialize_list的类,可将其用做函数参数或方法参数的类型。这个类可以表示任意长度的列表,只要所有列表项的类型都相同或可转换为相同的类型(第16章,所以我完全看不懂)。

 

 

 

const成员函数:

假如一个类在声明的时候,被const所限制,例如:const man a = man("aa", 1);

那么在使用类时,有一些函数将被拒绝使用,原因是编译器不确定你调用的函数是否会修改类对象的数据(比如将某个私有对象成员的值加一)。

 

由于要确保私有对象成员的值不被修改,因此函数声明和函数定义也应做一定的变化(用const关键字进行限定)。

 

但类对象的限定方法不同于一般函数,其const关键字应后置于括号之后。

如代码:

void show()const; //函数原型

void man::show()const //函数定义,const后置于小括号之后

{

using namespace std;

cout << name << " is " << year << " years old." << endl;

}

 

这样的话,假如某个对象在声明的时候被const所限定,那么他依然可以执行show()函数,但是无法执行其他函数(假如之前那段多文件代码只改了以上这些的话)。

 

如代码:

const man b; //使用默认析构函数

b.show(); //调用类方法,输出对象的值

 

①假如你需要修改对象的值,那么就不要把其声明为const对象;

②假如某个类成员函数不会修改成员对象的值,那么应该将其声明为被const关键字所限定的函数(方法是const后置于小括号后);

③假如你声明了一个const类对象,那么就只能使用那些被const所限定的类成员函数。例如:b.update();  是不能通过编译的。

 


目录
相关文章
|
5月前
|
安全 编译器 C++
C++一分钟之-构造函数与析构函数
【6月更文挑战第20天】C++中的构造函数初始化对象,析构函数负责资源清理。构造函数有默认、参数化和拷贝形式,需注意异常安全和成员初始化。析构确保资源释放,避免内存泄漏,要防止重复析构。示例代码展示了不同构造函数和析构函数的调用情况。掌握构造和析构是有效管理对象生命周期和资源的关键。
48 2
|
6月前
|
C++ Linux
|
5月前
|
编译器 C语言 C++
【C++】:构造函数和析构函数
【C++】:构造函数和析构函数
49 0
|
6月前
|
编译器 C#
C#构造函数详解
C#构造函数详解
58 0
|
6月前
|
编译器 C++
【c++】构造函数和析构函数
【c++】构造函数和析构函数
【c++】构造函数和析构函数
|
6月前
|
编译器 C语言 C++
C++构造函数,析构函数
C++构造函数,析构函数
|
6月前
|
存储 编译器 Linux
构造函数与析构函数的问题总结
构造函数与析构函数的问题总结
67 0
|
编译器 C++
<c++> 类的构造函数与类的析构函数
<c++> 类的构造函数与类的析构函数
93 0
|
编译器 C语言 C++
C++ 继承,构造函数,析构函数(上)
C++ 继承,构造函数,析构函数