类和对象
本小节开始进入面向对象编程基础,这一小节将会学习:
- 什么是类和对象;
- 类如何帮助整合数据和处理数据的函数;
- 构造函数、复制构造函数和析构函数;
- 移动构造函数时什么;
- 封装和抽象等面向对象的概念;
- this指针;
- 结构是什么,它与类有何不同。
1.1 类和对象
1.1.1 声明类
要声明类,可使用关键字class,并在它后面依次包含类名,一组放在{}内的成员属性和成员函数,以及结尾的分号。
类声明将类本身及其属性告诉编译器。类声明本身并不能改变程序的行为,必须要使用类,就像需要调用函数一样。
模拟人类的类类似于下面这样:
class Human
{
// Member attributes:
string name;
string dateOfBirth;
string placeOfBirth;
string gender;
// Member functions:
void Talk(string textToTalk);
void IntroduceSelf();
...
};
1.1.2 作为类实例的对象
类相当于蓝图,仅声明类并不会对程序的执行产生影响。在程序执行阶段,对象是类的化身。要使用类的功能,通常需要创建其实例——对象,并通过对象访问成员方法和属性。
创建Human对象与创建其他类型的实例类似:
double pi= 3.1415; // a variable of type double
Human firstMan; // firstMan: an object of class Human
就像可以为其他类型动态分配内存一样,也可使用new为Human对象动态地分配内存:
int* pointsToNum = new int; // an integer allocated dynamically
delete pointsToNum; // de-allocating memory when done using
Human* firstWoman = new Human(); // dynamically allocated Human
delete firstWoman; // de-allocating memory
1.1.3 使用句点运算符访问成员
一个人的例子是Adam,男性,1970年出生于阿拉巴马州。firstMan是Human类的对象,是这个类存在于现实世界(运行阶段)的化身:
Human firstMan; // an instance i.e. object of Human
类声明表明,firstMan有dataOfBirth等属性,可使用句点运算符(.)来访问。
firstMan.dateOfBirth = "1970";
对于类名和成员函数名,采用Pascal拼写法,如IntroduceSelf(),而对于成员属性,采用骆驼拼写法,如dataOfBirth。
1.1.4 使用指针运算符(->)访问成员
如果对象是使用new在自由存储区中实例化的,或者有指向对象的指针,则可使用指针运算符(->)来访问成员属性和方法:
Human* firstWoman = new Human();
firstWoman->dateOfBirth = "1970";
firstWoman->IntroduceSelf();
delete firstWoman;
1.2 关键字public和private
信息至少可以分两类:不介意别人知道的数据和保密的数据。
C++能够将类的属性和方法声明为公有的,这意味着有了对象后就可获取它们;也可将其声明为私有的,这意味着只能在类的内部(或其友元)中访问。可使用C++关键字public和private来执行哪些部分可从外部访问,哪些部分不能。
使用关键字private实现数据抽象。
在面向对象编程语言中,抽象是一个非常重要的概念,让程序员能够决定哪些属性只能让类及其成员知道,类外的任何人都不能访问(友元除外)。
1.3 构造函数
1.3.1 声明和实现构造函数
构造函数是一种特殊的函数,它与类同名且不返回任何值。
class Human
{
public:
Human(); // declaration of a constructor
};
这个构造函数可在类声明中实现,也可在类声明外实现。在类声明中实现(定义)构造函数的代码类似下面这样:
class Human
{
public:
Human()
{
// constructor code here
}
};
在类声明外定义构造函数的代码类似下面这样:
class Human
{
public:
Human(); // constructor declaration
};
// constructor implementation (definition)
Human::Human()
{
// constructor code here
}
::被称为作用域解析运算符。
1.3.2 何时及如何使用构造函数
构造函数总是在创建对象时被调用,这让构造函数成为类成员变量初始化为选定值的理想场所。
1.3.3 重载构造函数
与函数一样,构造函数也可重载,因此可创建一个将姓名作为参数的构造函数。
class Human
{
public:
Human()
{
// default constructor code here
}
Human(string humansName)
{
// overloaded constructor code here
}
};
1.3.4 没有默认构造函数的类
如果没有默认构造函数,在创建对象是必须提供成员属性的初始值。
1.3.5 带默认值的构造函数参数
就像函数可以有带默认值的参数一样,构造函数也可以。
class Human
{
private:
string name;
int age;
public:
// overloaded constructor (no default constructor)
Human(string humansName, int humansAge = 25)
{
name = humansName;
age = humansAge;
cout << "Overloaded constructor creates " << name;
cout << " of age " << age << endl;
}
// ... other members
};
实例化这个类时,可使用下面的语法
Human adam("Adam"); // adam.age is assigned a default value 25
Human eve("Eve, 18); // eve.age is assigned 18 as specified
1.3.6 包含初始化列表的构造函数
构造函数对初始化成员变量很有用。另一种初始化成员的方式是使用初始化列表。
class Human
{
private:
string name;
int age;
public:
// two parameters to initialize members age and name
Human(string humansName, int humansAge)
:name(humansName), age(humansAge)
{
cout << "Constructed a human called " << name;
cout << ", " << age << " years old" << endl;
}
// ... other class members
};
初始化列表由包含在括号中的参数声明后面的冒号标识,冒号后面列出了各个成员变量及其初始值。初始值可以是参数,也可以是固定的值。使用特定参数调用基类的构造函数时,初始化列表也很有用。
也可使用关键字constexpr将构造函数定义为常量表达式。
1.4 析构函数
析构函数也是一种特殊的函数。构造函数在实例化对象时被调用,而析构函数在对象销毁时自动被调用。
1.4.1 声明和实现析构函数
析构函数看起来像一个与类同名的函数,但前面有一个腭化符号(~)。因此,Human类的析构函数的声明类似于下面这样:
class Human
{
~Human(); // declaration of a destructor
};
这个析构函数可在类声明中实现,也可在类声明外实现。
在类声明中实现(定义)析构函数的代码类似于下面这样:
class Human
{
public:
~Human()
{
// destructor code here
}
};
在类声明外定义析构函数的代码类似于下面这样:
class Human
{
public:
~Human(); // destructor declaration
};
// destructor definition (implementation)
Human::~Human()
{
// destructor code here
}
1.4.2 何时及如何使用析构函数
每当对象不再在作用域内或通过delete被删除进而被销毁时,都将调用析构函数。这使得析构函数成为重置变量以及释放动态分配的内存和其他资源的理想场所。
析构函数不能重载,每个类都只能有一个析构函数。
1.5 复制构造函数
1.5.1 浅复制及其存在的问题
复制一个类的对象时,将复制其指针成员,但不复制指针指向的缓冲区,其结果是两个对象指向同一块动态分配的内存。销毁其中一个对象时,delete[]释放这个内存块,导致另一个对象存储的指针拷贝无效。这种复制被称为浅复制,会威胁程序的稳定性。
1.5.2 使用复制构造函数确保深复制
复制构造函数是一个重载的构造函数,由编写类的程序员提供。每当对象被复制时,编译器都将调用复制构造函数。
为MyString类声明复制构造函数的语法如下:
class MyString
{
MyString(const MyString& copySource); // copy constructor
};
MyString::MyString(const MyString& copySource)
{
// Copy constructor implementation code
}
复制构造函数接受一个以引用方式传入的当前类的对象作为参数。这个参数是源对象的别名,可以使用它来编写自定义的赋值代码,确保对所有缓冲区进行深复制。
拷贝中的buffer指向的内存地址不同,即两个对象并未指向同一个动态分配的内存地址。因此两个函数都执行完毕时,成功地销毁了各自的对象,没有导致应用程序崩溃。
通过在复制构造函数声明中使用 const ,可确保复制构造函数不会修改指向的源对象。另外,复制构造函数的参数必须按引用传递,否则复制构造函数将不断调用自己,直到耗尽系统的内存为止。
1.5.3 有助于改善性能的移动构造函数
C++编译器严格的调用复制构造函数反而降低了性能,如果复制的对象很大,对性能的影响将很严重。
为了避免这种性能瓶颈,C++11引入了移动构造函数。移动构造函数的语法如下:
// move constructor
MyString(MyString&& moveSource)
{
if(moveSource.buffer != NULL)
{
buffer = moveSource.buffer; // take ownership i.e. 'move'
moveSource.buffer = NULL; // set the move source to NULL
}
}
有移动构造函数时,编译器将自动使用它来“移动”临时资源,从而避免深复制。
1.6 this指针
在C++中,一个重要的概念是保留的关键字this。在类中,关键字this包含当前对象的地址,换句话说,其值为&object。当在类成员方法中调用其他成员方法时,编译器将隐式地传递this指针。
从编程的角度看,this的用途不多,且大多数情况下都是可选的。
调用静态方法时,不会隐式地传递this指针,因为静态函数不与类实例相关联,而由所有实例共享。
要在静态函数中使用实例变量,应显式地声明一个形参,并将实参设置为this指针。
1.7 将sizeof()用于类
这个运算符也可用于类,它将指出类声明中所有数据属性占用的总内存量,单位为字节。
1.8 结构不同于类的地方
关键字struct来自C语言,在C++编译器看来,它与类及其相似,差别在于程序员未指定时,默认的访问限定符(public和private)不同。结构中的成员默认为公有的(而类成员默认为私有的);另外,除非指定了,否则结构以公有方式继承基结构(而类为私有继承)。
1.9 声明友元
友元类和友元函数可以从外部访问类的私有数据成员和方法。要声明友元类或友元函数,可使用关键字friend。
1.10 共用体:一种特殊的数据存储机制、
共同体是一种特殊的类,每次只有一个非静态数据成员处于活跃状态。因此,共用体与类一样,可包含多个数据成员,但不同的是只能使用其中的一个。
1.10.1 声明共用体
要声明共同体,也使用关键字union,再在这个关键字后面指定共用体名称,然后在大括号内指定其数据成员。
union UnionName
{
Type1 member1;
Type2 member2;
…
TypeN memberN;
};
要实例化并使用共用体,可像下面这样做:
UnionName unionObject;
unionObject.member2 = value; // choose member2 as the active member
与结构类似,共用体的成员默认也是公有的,但不同的是,共用体不能继承。
另外,将sizeof()用于共用体时,结果总是为共用体最大成员的长度,即便该成员并不处于活跃状态。
1.10.2 在什么情况下使用共用体
在结构中,常使用共用体来模拟复杂的数据类型。共用体可将固定的内存空间解释为另一种类型,有些实现利用这一点进行类型转换或重新解释内存,但这种做法存在争议,而且可采用其他替代方式。
C++17能引入类型安全的共用体替代品。
1.11 对类和结构使用聚合初始化
下面的初始化语法被称为聚合初始化(aggregate initialization)语法:
Type objectName = {argument1, …, argumentN};
从C++11起,也可像下面这样做:
Type objectName = {argument1, …, argumentN};
聚合初始化可用于聚合类型,因此明白哪些数据类型属于聚合类型很重要。
初始化数组时,使用过聚合初始化:
int myNums[] = { 9, 5, -1 }; // myNums is int[3]
char hello[6] = { 'h', 'e', 'l', 'l', 'o', ' \0' };
然而,并非只有由整数或字符等简单类型组成的数组属于聚合类型,类(以及结构和共用体)也可能属于聚合类型。有关结构和类的规范标准指出了结构和类必须满足什么条件才属于聚合类型,但在不同的 C++标准中,需要满足的条件存在细微的差别,但完全可以这样说,即满足如下条件的类或结构为聚合类型,可作为一个整体进行初始化:只包含公有和非静态数据成员,而不包含私有或受保护的数据成员;不包含任何虚成员函数;只涉及公有继承(不涉及私有、受保护和虚拟继承);不包含用户定义的构造函数。
聚合初始化只初始化共用体的第一个非静态成员。
本章介绍了最重要的C++关键字和概念——class(类)。类封装了成员函数以及使用这些数据的成员函数;诸如public和private等访问限定符有助于对外部实体隐藏类的数据和功能。还学习了复制构造函数和C++11引入的移动构造函数,后者可帮助消除不必要的复制步骤。