14.1 C++类-成员函数、对象复制与私有成员
14.2 C++类-构造函数详解、explicit与初始化列表
14.3 C++类-inline、const、mutable、this与static
14.4 C++类-类内初始化、默认构造函数、“=default;”和“=delete;”
14.5 C++类-拷贝构造函数
14.6 C++类-重载运算符、拷贝赋值运算符与析构函数
14.7 C++类-子类、调用顺序、访问等级与函数遮蔽
14.8 C++类-父类指针、虚/纯虚函数、多态性与析构函数
14.9 C++类-友元函数、友元类与友元成员函数
14.10 C++类-RTTI、dynamic_cast、typeid、type-info与虚函数表
14.11 C++类-基类与派生类关系的详细再探讨
14.12 C++类-左值、右值、左值引用、右值引用与move
14.13 C++类-临时对象深入探讨、解析与提高性能手段
14.14 C++类-对象移动、移动构造函数与移动赋值运算符
14.15 C++类-继承的构造函数、多重继承、类型转换与虚继承
14.16 C++类-类型转换构造函数、运算符与类成员指针
1.成员函数、对象复制与私有成员
1.1 总述
类是一种自定义的数据类型,也就是一个新类型。类与类之间,又不是彼此孤立的,例如说一个类可以派生出子类,那么这个派生出子类的类就变成了该子类的父类。
在设计一个类的时候要站在很多角度去考虑,这里先列举出几个比较简单容易理解的角度。
(1)站在设计和实现者的角度来考虑,也就是自己。如何理顺在设计一个类的时候这个类里的数据存储布局,有哪些必要的成员变量和成员函数要定义和实现。
(2)站在使用者的角度来考虑,需要给使用者提供哪些可以访问的接口,而哪些接口不对外开放,只供类内的其他成员函数使用。
(3)在设计一个父类供子类继承的时候,如何设计这个父类。例如设计一个车的类,然后将来可能很多子类会继承该类,如轿车、卡车、摩托车等,那么,设计父类时,可能就要把一些车的公共特性抽取出来放到父类中,如都用油来驱动、都要在机动车道上行驶等这些公共特性。
因为可以有很多的角度来考虑类的设计,所以就产生了很多的设计思想和设计概念,这些设计概念比较抽象,读者刚刚接触类,先不必了解这些思想,而是把类的基础知识学好。
1.2 类基础
(1)一个类就是一个用户自己定义的数据类型,可以把类想象成一个名字空间,包着一堆内容(成员函数、成员变量)。
(2)一个类的构成,最常见的就是成员变量、成员函数这两种。当然,也有很多特殊的成员函数,它们的名字也特殊,功能也特殊,后续会慢慢讲。
(3)访问类的成员(成员函数、成员变量)时,如果用类的对象来访问,就使用“对象名.成员名”来访问成员。如果用指向这个对象的指针来访问,就使用“指针名->成员名”来访问成员。
struct student
{
int number;
char name[100];
void func() {};
};
int main()
{
student student1; //定义结构变量。这里可以省略struct,直接用结构名student
student1.number = 1001;
strcpy_s(student1.name, sizeof(student1.name), "zhangsan");
student1.func(); //调用成员函数
student* pstudent1 = &student1;
pstudent1->number = 1005;
cout << student1.number << endl; //1005
}
(4)类中public修饰符修饰的成员提供类的访问接口,暴露给外界,供外界调用,private成员提供各种实现类功能的细节方法,但不暴露给外界(使用者),外界无法使用这些private成员。
(5)struct(结构)是成员默认为public的class(类)。struct的定义一般如下:
struct A{...};
(6) class成员默认是private的。class的定义一般如下:
class A{...};
上面定义了一个名为A的类,所以,A就成了一个类型,后续就可以拿来使用。如此看来:
struct A{...};
等价于
class A{public:...};
建议写代码时尽量不要将class和struct混用,否则代码会显得比较混乱。当然,如果把没有成员函数只有成员变量的数据结构定义为struct,而把有成员函数的数据结构定义为class也是可以的,那就在编写代码中一直遵循这个规则。总之,有一个共同遵循的标准,而不是随意混用struct和class就好。
1.3 成员函数
看如下代码:
//定义一个Time类
class Time
{
public:
int Hour;
int Minute;
int Second;
};
//定义一个函数InitTime
void initTime(Time& stmptime, int tmphour, int tmpmin, int tmpsec)
{
stmptime.Hour = tmphour;
stmptime.Minute = tmpmin;
stmptime.Second = tmpsec;
}
int main()
{
Time myTime;
initTime(myTime, 11, 14, 5);
cout << myTime.Hour << endl; //11
cout << myTime.Minute << endl; //14
cout << myTime.Second << endl; //5
}
可以注意到,这样写程序是C语言中的写法,类Time和initTime函数之间没有什么直接的关联关系。但显然,Time类和initTime函数之间应该有关联关系。如果把initTime函数设计为类Time的成员函数,那么两者就有关联关系了。调整Time类代码:
class Time {
public:
int Hour;
int Minute;
int Second;
void initTime(int tmphour, int tmpmin, int tmpsec) //成员函数
{
Hour = tmphour;
Minute = tmpmin;
Second = tmpsec;
}
};
int main()
{
Time myTime;
myTime.initTime(11, 14, 5); //这就是调用成员函数(使用成员函数)
cout << myTime.Hour << endl; //11
cout << myTime.Minute << endl; //14
cout << myTime.Second << endl; //5
}
如果遵从常规的书写规范,把类定义和类实现放在分开的.h头文件和.cpp源文件中,看看应该怎样写。在Time.h文件中,内容如下:
#ifndef __MYTIME__
#define __MYTIME__
class Time {
public:
int Hour;
int Minute;
int Second;
void initTime(int tmphour, int tmpmin, int tmpsec);
};
#endif
在Time.cpp文件中,内容如下(记得要把Time.cpp加入到项目中来):
#include "Time.h"
//其中这两个冒号叫:作用域运算符,表示initTime函数属于Time类。可能有多个不同的类,其他类中也可能有叫initTime()的成员函数,所以这里必须用Time::来表明该函数属于Time类
void Time::initTime(int tmphour, int tmpmin, int tmpsec)
{
Hour = tmphour; //注意到,成员函数中可以直接使用成员变量名
//哪个对象调用的该成员函数,那么这些成员变量就属于哪个对象。可以理解成类成员函数知道哪个对象调用的自己
Minute = tmpmin;
Second = tmpsec;
}
主cpp文件(MyProject.cpp)的上面位置也要增加如下代码:
#include "Time.h"
其他代码都不变,编译链接后,程序可以正常执行。
读者可能会有一点疑惑:类定义放在一个头文件中,多个.cpp文件都包含这个头文件,那不就相当于这个类定义了多次吗?读者都知道,一个全局变量不允许定义多次,一个类难道允许定义多次?
确实允许定义多次,类是一个特殊的存在,在多个不同的.cpp源文件中用#include重复类定义是被系统允许的,这一点与定义一个全局变量不同。所以许多人把类定义也称为“类声明”。
1.4 对象的复制
看如下代码:
{
Time myTime2 = myTime;
Time myTime3(myTime);
Time myTime4{ myTime };
Time myTime5 = { myTime };
myTime5.Hour = 8;
Time myTime6;
myTime6 = myTime5; //通过赋值操作来拷贝对象
}
可以注意到,对象是可以复制的,上面这几种写法都是对象复制,复制后,每个对象都有不同的地址(每个对象的内容都保存在不同的内存中,彼此互不影响),而且成员变量的值都相等。
对象的复制,就是定义一个新对象时,用另外一个老对象里面的内容进行初始化。在写法上,观察上面的代码,对象的复制可以使用“=”“()”“{}”“={}”等运算符进行。同时,上面的代码也演示了通过赋值运算符来做对象的复制。
默认情况下,这种类对象的复制是每个成员变量逐个复制,如Time类中的Hour、Minute、Second。那能否控制对象的这种逐个成员变量复制的行为呢?能!只要给这个类定义一个恰当的“赋值运算符(或类似的内容)”,就能够控制对象的复制行为,这个后面再讲,读者先有个印象,这里提到的这个恰当的赋值运算符可以理解为一个成员函数,这个成员函数负责控制复制哪些成员变量。
1.5 私有成员
在Time类(Time.h)中增加一些成员,看如下代码:
class Time {
public:
//......
private:
int Millisecond;
private:
void initMillTime(int mls);
};
类的私有成员变量和私有成员函数都只能在类的成员函数内调用,外界是无法直接调用的。
修改Time.cpp中的Time::initTime成员函数,在其中增加对initMillTime成员函数的调用,同时,增加initMillTime成员函数的实现代码:
void Time::initTime(int tmphour, int tmpmin, int tmpsec)
{
Hour = tmphour;
Minute = tmpmin;
Second = tmpsec;
initMillTime(0);
}
void Time::initMillTime(int mls)
{
Millisecond = mls;
}
私有成员的设置目的,主要是希望这些接口不暴露在外面,不被其他使用者所知和所用(假设自己所开发的类即将被他人使用),只为类内部的其他成员函数使用。
所以,在设计类成员的时候要好好思考,对外暴露哪些接口,哪些接口是类内部使用的。这样才更利于设计出优质的类结构和写出优质的代码。
在类定义内部,private和public修饰符修饰其下面的所有成员,一直遇到其他的public或者private修饰符。因为定义class时,默认所有成员为private,所以不加public修饰的成员全部都是private的。
此外,一个类的定义中可以出现多个public、多个private,这都被系统所允许。