类与对象
类与对象
尤其要注意,声明类的时候一定要在最后面加上分号,因为声明类也算是一种语句。语句结束的时候一定要加上分号的。
9.3构造函数
构造函数是对象在被创建时调用的函数,顾名思义,它的作用是帮助创建一个类的实例,即对象。创建对象本质上和创建简单变量没什么区别,都必须占用存储空间,都有名字,只不过对象更复杂,更智能。
它一般用在初始化对象,动态分配内存上。
一般形式如下:
Human (string InputName = "Adam",int InputAge = "25")
{
//构造函数语句
}
构造函数有默认的,也有重载的(Overloaded)。在一个类的定义中,可以同时存在多个构造函数的定义(或者说实现)。可以有默认构造函数,也可以没有默认构造函数。但是,如果没有构造函数的话,一般要求在实例化对象的同时初始化某些私有变量的值(类似于使用函数时输入的参数)。
比如:
class Human{
private:
string Name;
int Age;
public:
Human(){//没有参数
//构造函数实现
}
Human(string HumansName){//需要初始化一个变量
//构造函数实现
}
Human(string HumansName, int HumansAge){//初始化两个
//构造函数实现
}
}
类似于这样,三个同名的重载的构造函数,能够实现灵活的参数初始化选项。也就使得对象更容易使用了。
9.4 析构函数
什么是析构函数?与构造函数相反,它的作用是销毁对象,在对象作用域结束时调用。
~Human()
像这样。
...
~Human(){
cout << "调用析构函数,正在清除..." << endl;
if(Buffer != NULL)
delete [] Buffer;
}
...
上面的析构函数的作用是在对象作用域结束时解除Buffer的内存占用。(Buffer是一个char指针,指向一个C风格string数组的开头)。
析构函数有很多用途,上面提到的只是其中一种。
析构函数只能有一个,不能重载。
没有设置它的话,编译器会自动创建一个dummy(伪)析构函数,但这个dummy不释放用new命令动态分配的内存。
9.5 复制构造函数
浅复制与深复制
浅复制指的是直接把对象当参数传递到函数中。这样,函数会创建一个新对象,但是这个新对象的内存地址与作为实参的对象的内存地址是一样的。这本身不会导致程序崩溃。但是,因为有两个对象,会调用两次析构函数,所以会delete两次,但是同一个内存是不能被delete两次的。所以程序会崩溃。
void UseMyString(MyString Input){} //这是浅复制
相比之下,深复制就安全得多。深复制时,函数会在另外一个内存空间创建一个复制的对象。下面是通过设置一个复制构造函数来实现的。因为函数会创建一个复制的对象,这个新的对象需要一个构造函数。
MyString(const MyString& CopySource) //这是深复制
{ //const 表示不允许改变原来的对象
} //符号 & 表示引用
...
//然后是函数的定义,可见这里的定义与浅复制是是一样的
void UseMyString(MyString Input){}
//但不同的是传对象进去时,因为有“复制析构函数”的存在,会创建另外的空间
另外一种深复制的方法是,先实现复制赋值运算符,然后直接令新对象等于就对象。But, that's another story at Chatpet 12.
移动构造函数
???
9.6 构造和析构的其它用途
- 通过声明私有的复制构造函数或赋值运算符,可以禁止对该对象的复制操作,而无需给这两者提供实现。
- 上面的方法可以保证一个对象被创建后就无法复制,但有时候还要确保一个类只能有一个实例,这种类可以称做单例类。它的声明方法是将构造函数(包括默认和复制)、赋值运算符声明为私有的,并且在公有成员里面声明一个静态实例成员。
如下所示:
#include <iostream>
#include <string>
using namespace std;
class President{
private:
President(){};
President(const President&);
const President& operator=(const President);
string Name;
public:
//受控的实例化
//下面这条语句声明了这个单例类的“单例”。
static President& GetInstance(){
static President OnlyInstance;
return OnlyInstance;
}
string GetName(){
return Name;
}
void SetName(string InputName){
Name = InputName;
}
void ShowName(){
cout << "总统的名字是" << Name << endl;
}
};
int main(void)
{
President& OnlyPresident = President::GetInstance();
OnlyPresident.SetName("***");
//被注释掉的是被禁止的复制操作
//President Second;
//President* Third = new President;
//President fourth = OnlyPresident;
//OnlyPresident = President::GetInstance();
cout << "The Name of President is: ";
cout << President::GetInstance().GetName() << endl;
return 0;
}
这些语句都是在类外面的语句。而因为所有构造函数都被声明在了private里面,所以从main()这里是无法使用这些构造函数的。也就是说,要声明唯一的那个单例类,只能在类President的public里面实现。
所以,我们可以发现,之所以构造函数一般都声明在public里面,就是因为这样才能让main()可以访问它们,通过它们声明对象。到这里,更加理解了类的特性——私有性的含义了。
- 禁止在栈(stack)中实例化的类
通过将析构函数声明为private,可以禁止程序员(我自己—_—)在栈(stack)中创建该类的实例。但是可以在堆(heap)中创建。
未解决的问题:
- 什么是堆(heap)?什么是栈(stack)?两者的区别?
参见CSDN文章:堆和栈的区别(转过无数次的文章) - 什么是内存泄漏?
- 仍然不太理解静态函数。
心得一:没有创建对象时,就只能通过类的定义(或实现)直接访问成员函数,比如:
MonsterDB::DestroyInstance(pMyDatabase);
9.7 This指针
this是一个指针,它指向当前对象的地址。一般来说,如果使用一个成员函数调用另一个成员函数,this指针会被隐式地嵌入到所调用的那个成员函数的参数列表中。比如说
class Human{
private:
void Talk(string Statement){
cout << Statement;
}
public:
void InstroduceSelf(){
Talk("Bla bla");
}
}
中的Talk("Bla bla"),其实是Talk(this, "Bla bla")
但是,需要注意:调用静态函数时,不会隐式地传递this指针,因为静态函数不与类实例相关联,而由所有实例共享。如果要在静态函数中使用实例变量,则应显式地声明一个形参,让调用者将实参设置为this指针。
9.8 将sizeof()用于类
sizeof()一个是个运算符,它不仅用于之前的数据类型,也同样可以用于确定类这种数据类型所需要的内存(单位:字节),但是只计算数据,而不计算成员函数以及成员函数中定义的局部变量。
sizeof()的结果受字填充(word padding)的影响。比如对象中有一个int,一个bool,一个占用4字节的MyString对象,那么人工计算的话应该是9字节,但是编译时显示了12字节,这说明编译器对bool变量进行了字填充。
9.10 声明友元
可以从外部访问类的私有数据成员和方法——通过友元。使用关键字friend声明友元。将加上了friend关键字的类或函数原型添加到private中,就声明了友元。如:
private:
string Name;
int Age;
//Display()是外部函数。
friend void DisplayAge(const Human& Person);
//或者
...
private:
string Name;
int Age;
//Utility是一个外部类
friend class Utility;-
总结
类封装了成员数据以及使用这些数据的方法,这有助于类对外隐藏它的私有数据和方法,只对外留出一个接口。外部的客体只需了解这个接口的特性即可使用它,而无需知道其内部的细节——这是抽象的方法。自然界目前所知的最小的对象时夸克(虽是理论,但被被广泛认可),由夸克这种数据以某种方法封装成中子或质子,这是一个抽象过程。再由中子和质子经过抽象,成为原子。层次逐渐上升,分子,大分子,到蛋白质等生物大分子,到亚细胞级,到细胞,到器官,最后到人这样一个复杂的主体。
抽象,无处不在。
十分重要的是构造函数与析构函数的用法,前者用于创建类,后者用于销毁类,他们通常设置在公有区。复制构造函数利用了构造函数的重载特性,可实现灵活的初始化选项。而析构函数主要用于自动清除内存。
另外,学习了,复制构造函数的概念,需要采取一些措施,防止不好的复制、创建单例类,以及为了保证不在栈中创建存储过大数据的类等。
也了解了类的其它方面的内容,比如this指针,与结构体的相似点和区别,友元。这些不太重要。