【C++入门笔记】对象、构造器和析构器

前言

这篇是FishC的《C++快速入门》中第十四讲、第十五讲和第十七讲的笔记整理,因为OneNote的代码插件记笔记太难过了,就把内容写在CSDN上吧。

对象

我们从给一个类开始,首先是一个模型。当我们为这个类创建实例的时候,也就是对象本身。这跟我们之前讲解的定义和使用结构的过程很相似,但是这更具有扩展性和前瞻性。

对象的内部可以有变量和函数,而结构通常只由各种变量组成。

因此,我们需要知道的是是如何编写一个简单对象的模型 - 类

类(class)就像是一副蓝图,它决定给一个对象将是什么样的(具备什么样的属性和功能)所以OOP过程的第一步是创建一个类,而每个类跟变量一样都有一个名字,我们就从如何声明一个类说起:

class MyFirstClass
{
};

注意,类名的第一个字母采用大写是一种习惯上的标准。还有在类声明的某位,必须有一个分号,这一点跟C++的结构情况相同。

类由变量和函数组成,对象将使用那些变量来存储信息,调用那些函数来完成操作。所以人们常常会看到一些专业术语:

类里边的变量成为属性,函数成为方法。注意:他们的本质没有发生改变。

给类添加方法如出一辙:

  • 先给类的声明创建一个方法的原型;
  • 稍后再实现这个方法 
#include

class Car
{
	public:
		std::string color;
		std::string engine;
		float gas_tank;
		unsigned int Wheel;
		
		void fill_tank(float liter);
		//方法的声明:方法是“加油”,参数是“公升” 
		
		void running(void); 
 }; 
 
 void Car::fill_tank(float liter)
 //作用域解析符(::),作用是告诉编译器这个方法存在于何处,或者说属于哪一类 
 {
 	gas_tank += liter;
 }
 
 
 
 int main()
 {
 	return 0;
 }

 构造器

上一讲中讨论了使用面向对象编程技术开发程序最基本步骤:

  • 定义一个有属性和方法的类(模板)
  • 为该类创建一个变量(实现)

而构造器是类中的一种特殊的方法。

构造器和通常方法的主要区别:

  • 构造器的名字必须和它所在的类的名字一样。
  • 系统在创建某个类的实例时会第一时间自动调用这个类的构造器。
  • 构造器永远不会返回任何值。

例如:

class Car{
	Car(void);
}

注意大小写与类名保持一致。在结束声明之后开始定义构造器本身:

Car::Car(void)//不用写 void Car::Car(void)
{
	color = "WHITE";
	engine = "V8";
	wheel = 4;
	gas_tank = FULL_GAS;
 } 

构造对象数组:之前我们已经说过,数组可以是任何一种数据类型,当然也包括对象。

如:Car mycar[10];

调用方法依旧是:mycar[x].running;

注:x表示给定数组元素的下标。

Pay Attention

每个类至少有一个构造器,如果你没有在类里面定义一个构造器,那么编译器就会使用如下语法替你定义一个:

ClassName::ClassName(){}

这是一个没有代码内容的空构造器,除此之外,编译器还会替你创建一个副本构造器(CopyConstructor)

 析构器

从前面的内容我们了解到,在创建对象时,系统都会自动调用一个特殊的方法,即构造器。

相应地,在销毁一个对象时,系统也应该会调用另一个特殊方法达到效果。没错,这就是析构器。

一般来说,构造器用来完成事先的初始化和准备工作(申请分配内存),析构器用来事后时候所必须的清理工作(清理内存)。

构造器和析构器二者相辅相成,有许多共同之处。首先,析构器有着和构造器/类一样的名字,只不过千变多了一个波浪符"~"前缀。

class Car
{
	Car(void);
	~Car();
}

其次,析构器也永远不带任何返回值。

另外,析构器是不带任何参数的。所以析构器的声明永远是如下格式:

~ClassName();

 FishC在课上给出一个完整栗子,如下所示:

#include 
#include 
#include 

class StoreQuote
{
public:
	std::string quote, speaker;
	std::ofstream fileOutput;

	StoreQuote();
	~StoreQuote();

	void inputQuote();
	void inputSpeaker();
	bool write();
};

StoreQuote::StoreQuote()//构造器
{
	fileOutput.open("test.txt", std::ios::app);
}

StoreQuote::~StoreQuote()//析构器
{
	fileOutput.close();
}

void StoreQuote::inputQuote()
{
	std::getline(std::cin, quote);
}

void StoreQuote::inputSpeaker()
{
	std::getline(std::cin, speaker);
}

bool StoreQuote::write()
{
	if (fileOutput.is_open())
	{
		fileOutput << quote << "|" << speaker << "\n";
		return true;
	}
	else
	{
		return false;
	}
}

int main()
{
	StoreQuote quote;

	std::cout << "请输入一句名言:\n";
	quote.inputQuote();

	std::cout << "请输入作者:\n";
	quote.inputSpeaker();

	if (quote.write())
	{
		std::cout << "成功写入文件^_^";
	}
	else
	{
		std::cout << "写入文件失败T_T";
		return 1;
	}

	return 0;
}

 其中用到了本次所讲的构造器和析构器用来打开和关闭文件,其中C++打开和关闭文件可见之前的笔记《【C++入门笔记】文件操作》

 

继承机制中的构造器

以上都是在没有继承机制下的构造器和析构器,很容易理解。但如果使用了继承机制,构造器和析构器就变得有些复杂了。

比如基类有个构造器,如Animal( ),它将在创造Pig类型的对象时最先被调用,如果Pig类也有一个构造器,它将排在第二个被调用。因此,基类必须在子类之前初始化!!

然后我们继续讨论:如果构造器带着输入参数,事情就变得稍微复杂起来。

class Animal{
	public:
		Animal(std::string theName);
		std::string name;
}; 
class Pig:public Animal{
	public:
		Pig(std::string theName);
};

Animal::Animal(std::string theName){
	name = theName;
}
Pig::Pig(std::string theName):Animal(theName){
}

注意在子类的构造器中定义的":Animal(theName)"语法含义是:

  • 当调用Pig( )构造器时(以theName作为输入参数),Animal( )构造器也会被调用(theName作为输入参数将传递给它)
  • 于是,当我们调用Pig pig(“小猪猪”);将把字符串“小猪猪”传递给Pig( )和Animal( ),复制动作将实际发生在Animal( )方法里。

看下面这个栗子:

#include 
#include 

class Animal
{
public:
	std::string mouth;
	std::string name;

	Animal(std::string theName);
	void eat();
	void sleep();
	void drool();
};

class Pig : public Animal
{
public:
	void climb();
	Pig(std::string theName);
};

class Turtle : public Animal
{
public:
	void swim();
	Turtle(std::string theName);
};

Animal::Animal(std::string theName)
{
	name = theName;
}

void Animal::eat()
{
	std::cout << "I'm eatting!" << std::endl;
}

void Animal::sleep()
{
	std::cout << "I'm sleeping!Don't disturb me!" << std::endl;
}

void Animal::drool()
{
	std::cout << "我是公的,看到母的我会流口水,我正在流口水。。。" << std::endl;
}

Pig::Pig(std::string theName) : Animal(theName)//Pig的构造器 
{
}

void Pig::climb()
{
	std::cout << "我是一个只漂亮的小母猪猪,我会上树,我正在爬树,嘘。。。" << std::endl;
}

Turtle::Turtle(std::string theName) : Animal(theName)//Turtle的构造器 
{
}

void Turtle::swim()
{
	std::cout << "我是一只小甲鱼,当母猪想抓我的时候,我就游到海里。。哈哈。。" << std::endl;
}

int main()
{
	Pig pig("小猪猪");
	Turtle turtle("小甲鱼");

	std::cout << "这只猪的名字是: " << pig.name << std::endl;
	std::cout << "每只乌龟都有个伟大的名字: " << turtle.name << std::endl;

	pig.eat();
	turtle.eat();
	pig.climb();
	turtle.swim();

	return 0;
}

这只猪的名字是: 小猪猪
每只乌龟都有个伟大的名字: 小甲鱼
I'm eatting!
I'm eatting!
我是一个只漂亮的小母猪猪,我会上树,我正在爬树,嘘。。。
我是一只小甲鱼,当母猪想抓我的时候,我就游到海里。。哈哈。。 

 继承机制中的析构器

在销毁某个对象时,基类的析构器也会被自动调用,但这些事情编译器会自动帮你处理。

因为析构器不需要输入参数,所以根本用不着使用:SuperClassMethod(arguments)语法

与构造器的情况相反,基类的析构器将在子类的最后一条语句执行完毕后才被调用。

为了清晰地看出在继承机制中构造器和析构器调用的次序,让我们看下一个栗子。

#include 
#include 

class BaseClass
{
public:
	BaseClass();
	~BaseClass();

	void doSomething();

};

class SubClass : public BaseClass
{
public:
	SubClass();
	~SubClass();
};

BaseClass::BaseClass()
{
	std::cout << "进入基类构造器\n";
	std::cout << "我在基类构造器里做了某些事情\n\n";
};

BaseClass :: ~BaseClass()
{
	std::cout << "进入基类析构器\n";
	std::cout << "我在基类析构器里也做了某些事情\n\n";
}

void BaseClass::doSomething()
{
	std::cout << "我干了某些事情\n\n";
}

SubClass::SubClass()
{
	std::cout << "进入子类构造器\n";
	std::cout << "我在子类构造器里还做了某些事情\n\n";
}

SubClass :: ~SubClass()
{
	std::cout << "进入子类析构器\n";
	std::cout << "我在子类析构器里还做了某些事情\n\n";
}

int main()
{
	SubClass subclass;
	subclass.doSomething();

	std::cout << "完事,收工!\n";
	return 0;
}

 

进入基类构造器
我在基类构造器里做了某些事情

进入子类构造器
我在子类构造器里还做了某些事情

我干了某些事情

完事,收工!
进入子类析构器
我在子类析构器里还做了某些事情

进入基类析构器
我在基类析构器里也做了某些事情

 

由结果可见,首先进入基类的构造器 --> 子类构造器 --> 方法 --> 子类析构器 --> 基类析构器

总结

这两节课主要是讲C++作为一门面向对象的程序语言的“对象”问题。

首先“类”是一个抽象的模型,“类”里边的变量称为属性,函数称为方法。而对“类”实例化后就有了“对象”。

一般来说,构造器用来完成事先的初始化和准备工作(申请分配内存),析构器用来事后时候所必须的清理工作(清理内存)。

而在继承机制中的构造器和析构器,调用次序是:基类的构造器 --> 子类构造器  ; 子类析构器 --> 基类析构器

 

参考视频

《C++快速入门--小甲鱼》https://www.bilibili.com/video/av7595819/?p=16

 

 

你可能感兴趣的:(C++入门)