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

类构造函数:

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

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

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

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

 

 

声明和定义构造函数:

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

例如:

void Player::Player(std::string name, double hps, double atks)

{

Name = name;

Hp_s = hps;

Atk_s = atks;

hp_();

atk_();

}

 

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

 

需要注意的是,传递的参数名(std::string name, double hps, double 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();  是不能通过编译的。

 

时间: 2024-12-09 09:21:05

(一一六)类的构造函数和析构函数的相关文章

高质量C++/C编程指南-第9章-类的构造函数、析构函数与赋值函数(4)

类String的赋值函数比构造函数复杂得多,分四步实现: (1)第一步,检查自赋值.你可能会认为多此一举,难道有人会愚蠢到写出 a = a 这样的自赋值语句!的确不会.但是间接的自赋值仍有可能出现,例如 // 内容自赋值 b = a; - c = b; - a = c; // 地址自赋值 b = &a; - a = *b; 也许有人会说:"即使出现自赋值,我也可以不理睬,大不了化点时间让对象复制自己而已,反正不会出错!" 他真的说错了.看看第二步的delete,自杀后还能复制自

c++ 类 复制构造函数,析构函数

问题描述 c++ 类 复制构造函数,析构函数 c++类, 复制构造函数中产生的对象在程序结束后或运行中 会不会被析构 解决方案 只要程序正常运行,正常关闭,都会执行析构函数.如果你有疑问,你可以自己试验下. 解决方案二: 贴出你完整的代码 析构函数对于每个对象的实例只调用一次. 解决方案三: 1.如果没有显示定义复制构造函数或赋值操作符,编译器通常会为我们定义. 2.复制构造函数.赋值操作符.析构函数总称复制控制.编译器自动实现这些操作,蛋类也可以定义自己的版本. 3.有一种特别常见的情况需要类

C++语言基础 例程 派生类的构造函数和析构函数

贺老师的教学链接  本课讲解 一个简单派生类的定义 #include <iostream> #include<cstring> using namespace std; class Student //声明基类Student { public: Student(int n,string nam,char s):num(n),name(nam),sex(s) {} //基类构造函数 ~Student( ) { } //基类析构函数 void show( ) { cout<<

C++类成员构造函数和析构函数顺序示例详细讲解_C 语言

对象并不是突然建立起来的,创建对象必须时必须同时创建父类以及包含于其中的对象.C++遵循如下的创建顺序: (1)如果某个类具体基类,执行基类的默认构造函数. (2)类的非静态数据成员,按照声明的顺序创建. (3)执行该类的构造函数. 即构造类时,会先构造其父类,然后创建类成员,最后调用本身的构造函数. 下面看一个例子吧 复制代码 代码如下: class c{public:    c(){ printf("c\n"); }protected:private:}; class b {pub

模板类构造函数与析构函数无法访问私有成员(明明就是公有的)

问题描述 模板类构造函数与析构函数无法访问私有成员(明明就是公有的) 模板类构造函数与析构函数无法访问私有成员(明明就是公有的) 写成这样: #ifndef __SINGLETON__H__ #define __SINGLETON__H__ template <typename T> class Worker; template <typename T> class Singleton { friend class Worker<T>; public: static T

拷贝构造,深度拷贝,关于delete和default相关的操作,explicit,类赋初值,构造函数和析构函数,成员函数和内联函数,关于内存存储,默认参数,静态函数和普通函数,const函数,友元

 1.拷贝构造 //拷贝构造的规则,有两种方式实现初始化. //1.一个是通过在后面:a(x),b(y)的方式实现初始化. //2.第二种初始化的方式是直接在构造方法里面实现初始化. 案例如下: #include<iostream> //如果声明已经定义,边不会生成 class classA { private: int a; int b; public: //拷贝构造的规则,有两种方式实现初始化 //1.一个是通过在后面:a(x),b(y)的方式实现初始化 //2.第二种初始化的方式是直

php基础知识:类与对象(3) 构造函数和析构函数_php技巧

构造函数 PHP 5 允行开发者在一个类中定义一个方法作为构造函数.具有构造函数的类会在每次创建对象时先调用此方法,所以非常适合在使用对象之前做一些初始化工作.  注意:  如果子类中定义了构造函数则不会暗中调用其父类的构造函数.要执行父类的构造函数,需要在子类的构造函数中调用 parent::__construct().(??和其他语言明显不同??) 例10.8.使用新标准的构造函数 class BaseClass {   function __construct() {       prin

C#中构造函数和析构函数的用法

函数 摘 要:构造函数与析构函数是一个类中看似较为简单的两类函数,但在实际运用过程中总会出现一些意想不到的运行错误.本文将较系统的介绍构造函数与析构函数的原理及在C#中的运用,以及在使用过程中需要注意的若干事项. 关键字:构造函数:析构函数:垃圾回收器:非托管资源:托管资源一.构造函数与析构函数的原理 作为比C更先进的语言,C#提供了更好的机制来增强程序的安全性.C#编译器具有严格的类型安全检查功能,它几乎能找出程序中所有的语法问题,这的确帮了程序员的大忙.但是程序通过了编译检查并不表示错误已经

C#构造函数和析构函数的用法

构造函数与析构函数是一个类中看似较为简单的两类函数,但在实际运用过程中总会出现一些意想不到的运行错误.本文将较系统的介绍构造函数与析构函数的原理及在C#中的运用,以及在使用过程中需要注意的若干事项. 一.构造函数与析构函数的原理 作为比C更先进的语言,C#提供了更好的机制来增强程序的安全性.C#编译器具有严格的类型安全检查功能,它几乎能找出程序中所有的语法问题,这的确帮了程序员的大忙.但是程序通过了编译检查并不表示错误已经不存在了,在"错误"的大家庭里,"语法错误"