五、类(未完)
1. 数据类型、对象、类、实例
面向对象编程(OOP:Object-Oriented Programming)的三个基本概念:
- 封装
- 多态性
- 继承性
在C++中,除了成员的访问控制外,关键字struct和class几乎是等同的。
结构只是其成员默认public的类。而类成员默认为private。
类(以及结构)不仅限于容纳数据,还可以定义成员函数。(C中结构只能包含数据)
类对象之间可以加、减、乘。
1.1. 术语
- 类是用户定义的数据类型。
- 声明类的对象有时称为实例化,因为这是在创建类的实例。
- 类的实例称为对象。
- 对象在定义中隐式地包含数据和操作数据的函数,这种思想成为封装。
如下述的Cbox 变量名
即实例化,而变量名
即为对象(也即实例)。
2. 理解类
类的数据元素可以是:单数据元素、数组、指针、指针数组、其他类的对象等。
类中包含:构成对象的元素数据定义、处理本类对象中数据的方法。
类中的数据和函数称为类的成员。数据为数据成员,函数为成员函数或函数成员。
2.1. 定义类
class 类名称
{
public:
...
private:
...
protected:
...
};
注意:类描述之后的分号是必须的,所有类成员都是该类的局部变量。(也即,每个人的名字都可以有“三”,但是姓不同,表示的人就不同)
public
意味着该成员的在类对象的作用域内都可以访问。
2.2. 声明类对象
类名称 类对象名称;
类的定义是自定义数据类型的过程。
2.3. 访问类的数据成员
访问类的成员指的是访问该类的一个具体对象的数据成员。必须在某个地方定义过该变量。
直接使用访问结构成员的直接成员选择操作符,即.
,引用类对象的数据成员。
访问一个成员:
如果使用类定义访问,则使用::
;(静态成员)
如果使用类对象访问。则使用.
;
2.4. 类的成员函数
class CArea
{
private:
int m_len;
int m_wid;
public:
int Area_Cal(int x, int y)
{
return (x*y);
}
}
类的成员函数是定义或原型在类定义内部的函数。它可以处理本类的任何对象,有权访问类对象的所有成员(public/private/protected)。
类的成员函数不会影响该类对象的大小,即sizeof
只计算数据成员。但不表示函数不存在内存中,而是类中的成员函数只是一个副本。
成员函数的定义不一定要放在类内部,可以在类外部定义,但类内部必须有该成员函数的原型。
class CArea
{
public:
int m_len;
int m_wid;
int Area_Cal(int, int);
}
int CArea::Area_Cal(int x, int y)
{
return (x*y);
}
2.4.1. 内联函数(关键字:inline
)
在内联函数中,编译器设法以函数体代码代替函数调用。这样可以避免调用函数造成的开销,加速代码运行,但应仅用于简短的函数。一般在类中定义的函数。
也可以自行告诉编译器将一个函数视为内联函数,使用inline
关键字。(此时需要在类定义中至少加入函数原型,即函数声明,且类定义外的内联函数定义的函数名前加::)
inline int ABC(...)
{
...
}
2.4.2. 类的构造函数
2.4.2.1. 类的构造函数
该函数提供创建对象时进行初始化的机会,并确保数据成员只包含有效值。
类中可以有多个构造函数。(函数重构)
构造函数的命名必须与类名称相同,没有返回类型(即使写成void也不允许)。
class CPoint
{
public:
int m_x;
int m_y;
CPoint(int x, int y)
{
cout << "Constructor Initializing." << endl;
m_x = x;
m_y = y;
}
CPoint()
{}
};
CPoint CPoint_1(0,0);
CPoint CPoint_2;
CPoint CPoint_1(0,0);
为初始化。其赋值过程是调用构造函数的过程,所以不是用”=”进行赋值。
CPoint()
为默认的构造函数。不要求提供实参,既可以是定义中没有形参,也可以是实参具有默认值。
如果上例中不加
CPoint()
默认构造函数的定义,编译器会由于CPoint CPoint_2;
这条语句而报错。原因是如果只提供了CPoint(int x, int y)
的定义,则编译器会认为该构造函数负责处理所有问题,从而不会自动提供默认构造函数。而CPoint CPoint_2;
这条语句丢失了CPoint(int x, int y)
函数处理需要的两个形参,显然不能使用CPoint(int x, int y)
函数来进行初始化。
2.4.2.2. 默认形参值
可以在函数原型中给函数的形参指定默认值。
如果成员函数(包括构造函数)的定义在类的内部,可以直接将形参默认值放在函数头中;而如果成员函数在类外部定义,必须将默认形参值放在函数原型中。也即:默认形参值必须在类内部。
class CPoint
{
public:
int m_x;
int m_y;
CPoint(int x = 0, int y = 0)
{
cout << "Constructor Initializing." << endl;
m_x = x;
m_y = y;
}
/*
CPoint()
{}
*/
};
CPoint CPoint_1(0,0);
//CPoint CPoint_2;
在给定了默认形参数值之后,如果将注释符去掉,编译器又会出现错误。原因是:在给定默认形参值之后,
CPoint(int x = 0, int y = 0)
构造函数和CPoint()
默认构造函数都可以被不带参数地调用了,此时编译器将不知道该选择哪一个构造函数。
2.4.2.3. 构造函数中使用初始化列表
该种初始化方式只适用于函数定义在类内部。
class CPoint
{
public:
int m_x;
int m_y;
CPoint(int x = 0, int y = 0) : m_x(x) , m_y(y)
{
cout << "Constructor Initializing." << endl;
}
};
2.4.2.4. 声明显式的构造函数 explicit
如果定义一个具有单一形参的构造函数,则编译器使用此构造函数时需要隐式转换(其他类型转换成类类型)。(同样,如果类构造函数具有默认形参值,则也会出现隐式转换)
class CBox
{
public:
int m_x;
CBox(int x)
{
m_x = x;
}
CBox()
{
}
};
CBox box;
box = 99;
CBox box;
使用默认构造函数创建了box;box = 99;
将用99
做为实参,先调用构造函数CBox(int x)
创建一个类,再将这个类赋值给box
;即完成int
型到CBox
类的一个隐式转换。
而要避免这样一个隐式转化可以使用explicit
关键字进行限定。
class CBox
{
public:
int m_x;
explicit CBox(int x)
{
m_x = x;
}
/*
CBox()
{
}
*/
};
/*
CBox box;
box = 99;
*/
CBox box(99);
这时如果在使用注释中的语句box = 99;
,编译器将发生错误,因为int
型不能赋值给CBox
类。而此时CBox box(99)
仅仅是显式调用了含单一形参的构造函数。
2.5. 类的私有成员 private
私有类成员只能被类的成员函数访问。
在没有明确指定下(public/private/protected),将默认为private,即类成员均为类私有。
指定私有的类成员能将类的接口和类的内部实现分开。这样,操作类的私有成员只能通过公共的成员函数进行。
class CBox
{
pubilc:
explicit CBox(int x = 1) : m_x(x)
{}
int Get_value(void)
{
return (m_x);
}
private:
int m_x;
};
CBox box(5);
注意:
类的构造函数最好不要放入private中,不然将无法声明该类的任何对象。
在类成员为私有的情况下,如果需要获取该私有成员的值:(至少需有函数原型包含在类定义中)
inline int CBox::Get_Val(void)
{
return m_x;
}
可以为每一个私有数据成员都编写这样一个函数。如果该定义在类定义中,则默认为内联函数,可以去掉inline
关键字。
2.5.1 类的友元函数 friend
class CBox
{
pubilc:
explicit CBox(int x = 1) : m_x(x)
{}
int Get_value(void)
{
return (m_x);
}
private:
int m_x;
friend int Scale_val(int);
};
int Scale_val(int time)
{
return (CBox.m_x * time);
}
CBox box(5);
cout << Scale_val(10) << endl;
友元函数:不是类成员函数,但可以访问类的所有成员。因为不是类的成员,所以访问属性不适用,即:不受private/public/protected限制。如上代码中,Scale_val
的调用前不需要box.
。但正因为其不是类成员函数,其引用类的数据成员也得加对象名进行限定:CBox.m_x
。除了可以不加限制地访问友元类的所有成员外,与普通函数完全相同。
使用friend
关键字定义,在类中声明其函数原型即可(也可以在类中定义)。默认为内联函数。
2.5.2 默认“复制构造函数”
class CBox
{
public:
explicit CBox(int x):m_x(x)
{}
private:
int m_x;
};
CBox box1(0);
CBox box2 = box1;
CBox box2 = box1;
中box2
的创建类似之前的默认构造函数。这里使用的是编译器提供的默认复制构造函数。
默认复制构造函数作用:通过用同类的现有对象进行初始化来创建类对象。复制构造函数的默认版本通过逐个成员地复制现有的对象来创建新对象。
2.6 this指针
任何成员函数执行时,都自动包含一个名为this
的隐藏指针,指向调用该函数时使用的对象。(如上例中CBox box1
中box1
即为CBox
的一个对象)
如上例中,Get_value(void)
中return (m_x);
的m_x
实际上是引用this -> m_x
,这才是被使用的对象成员的全称,由编译器自动补全。当然,也可以显示使用this指针。
2.7 类的const对象
指定const
成员函数,只需在函数头后填加const
。这样目的是使该函数中的this
指针成为const
,这意味着该成员函数定义中,类对象的数据成员不能出现在赋值语句的左侧。该操作只能对类成员函数使用。
将某个类对象声明为const
型之后,该对象可以调用的成员函数也都必须声明为const
。
const
成员函数不能调用非const
成员函数。
应将所有不修改当前类对象的成员函数声明为const。
class CBox
{
pubilc:
explicit CBox(int x = 1) : m_x(x)
{}
int Get_value(void) const
{
return (m_x);
}
int Display_value(void) const;
private:
int m_x;
};
int CBox::Display_value(void) const
{
cout << m_x << endl;
}
可以在类中定义某个函数的const
和非const
版本。
2.8 类对象的数组
类对象的数组是有类对象组成的数组,每个数组元素都是类对象。
每个数组元素的初始化都将调用构造函数。
class CPoint
{
public:
CPoint():m_x(0),m_y(0)
{}
CPoint(int x, int y):m_x(x),m_y(y)
{}
private:
int m_x;
int m_y;
};
CPoint point1[2];
CPoint point2[2] = {CPoint(1,1),CPoint(2,2)};
2.9 类的静态成员
2.9.1 类的静态数据成员
将类的数据成员声明为static
,只能定义一次,而且要被同类的所有对象共享。
对于普通数据成员,各个对象都拥有副本。
但对静态数据成员,只有一个实例存在,与定义了多少类对象无关。
class CBox
{
public:
static int object;
private:
int m_x;
};
CBox box1;
CBox box2;
CBox box3;
在这里,box1/box2/box3
都拥有m_x
副本,互不干扰,但是object
是box1/box2/box3
共享的。
静态数据成员的初始化:类定义外部
静态数据成员在程序启动时就自动创建了,并被编译器初始化为0。但需要在类外部定义它(如下)。
int CBox::object = 5;
cout << CBox::object << box1.object << endl;
对静态成员的引用可以通过类定义,也可以通过类对象。
2.9.2 类的静态函数成员
静态函数成员独立于本类的任何具体对象,没有this
指针。
即使本类没有任何对象,静态函数成员仍然存在。只能访问静态数据成员。
2.10 类对象的指针和引用
一个类定义中,常会包含大量的数据。对与该类的对象,如果按照值传递机机制,需要复制每一个实参对象,非常耗时和低效。
2.10.1 类对象的指针
使用类对象的指针与使用基本类型的指针之间没有实质上的区别。
class CBox
{
public:
CBox(int x):m_x(x)
{}
int m_x;
};
CBox box1;
CBox* pBox = nullptr;
pBox = &box1;
cout<< pBox->m_x <<endl;
2.10.2 类对象的引用
如果函数的形参是引用,则调用该函数时不需要复制实参,直接访问被调用函数中的实参变量。const
限定符可以用来确保该函数不能修改实参。
CBox(const CBox& box);
{
m_x = box.m_x;
...
...
}
我们应将函数的引用形参声明为const
函数,除非该函数将修改实参。
3.深入理解类
3.1.类的析构函数
析构函数用于销毁不再需要或超出其作用域的对象。当对象超出其作用域时,程序将自动调用。销毁对象需要释放该对象的数据成员(除静态成员)占用的内存。
析构函数与类同名:
class CBox
{
CBox(); //默认构造函数
~CBox(); //默认析构函数
}
如果类成员占用的空间是在构造函数中动态分配的,必须自定义使用delete
来释放。
3.1.1.析构函数与动态内存分配
尽可能高效地利用内存。
编译器不负责删除在空闲存储器中创建的对象(即调用new
创建的类对象)。
class CMessage
{
private:
char* pmessage;
public:
void show_message() const
{
cout << endl << pmessage;
}
CMessage(const char* text="Default")
{
pmessage = new char[strlen(text)+1];
strcpy_s(pmessage, strlen(text)+1, text);
cout << "Constructor called." << endl;
}
~CMessage()
{
cout << "Destructor called." << endl;
delete [] pmessage;
}
};
int main()
{
CMessage motto("If not me, who?");
CMessage* pM(new CMessage("If not now, when?"));
motto.show_message();
pM->show_message();
cout << endl;
delete pM;
return 1;
}
//输出结果为
Constructor called. //创建motto
Constructor called. //创建"If not now, when?"
If not me, who?
If not now, when?
Destructor called.
Destructor called.
delete
只处理main()
函数中new
操作符分配的内存,即只释放指针pM
指向的内存。过程中调用了类对象自身的析构函数,确保释放为类成员动态分配的任何内存。
3.2.实现复制构造函数
对于3.1.1中的例子,如果实现类对象间的复制:
CMessage motto1("Stay hungry, Stay foolish.")
CMessage motto2("motto1");
这里类对象间的复制是将类对象motto1
的指针成员存储的地址赋值到motto2
中,两者共享同一字符串。在任意对象中对字符串进行修改都会修改另一个对象。如果motto1
被释放了,那motto2
就指向了一个已经被释放、现在可能用于其他对象的内存区域,可能会发生混乱。
如果某个类拥有动态定义的成员,有利用按值传递机制给函数传递该类的对象,那么对这个函数的任何调用都将出错。此时必须自定义复制构造函数。因为动态分配的类成员会出现原本与副本共享一个内存位置(指针指向的位置),这时,无论哪一个被释放,另一个都会出错。
3.3.联合
多个变量共享相同的内存。
3.3.1.定义联合
union 标记名
{
数据类型1 变量名1;
...
...
}对象名;
标记名 对象名;
对象名.变量名x=1;
允许多个数据类型的变量共享内存。另外,正因如此
成员引用使用“成员选择操作符”:“.”。
联合占用内存的大小取决于最大的成员。
当声明一个联合的对象时,只能将其初始化为第一个变量类型(上例中的数据类型1)。
3.3.2.匿名联合
union
{
数据类型1 变量名1;
...
...
};
变量名1 = a; \\a为任意值
定义没有联合类型名的联合,程序会自动声明该联合的一个实例(对象)。
此时引用该联合实例中的成员,直接使用变量名
,但需要避免其与普通变量名之间混淆。
3.4.运算符重载
该功能允许编写重新定义特定运算符的函数,但不允许使用新的运算符(自创),不允许改变运算符优先级。
有一些运算符不允许重载:
运算符 | 说明 |
---|---|
:: | 作用域解析符 |
?: | 条件运算符 |
. | 直接成员选择操作符 |
.* | 指向类成员的指针解除引用操作符 |
sizeof |
运算符重载函数形式:
operator 运算符 (形参)
{
...
}
实参1 运算符 实参2; //调用格式,此处为双目运算符的格式
如:
class Box
{
public:
explicit Box(float x=0,float y=0):length(x),height(y)
{
...
}
bool operator < (const CBox& aBox) const
{
return this->Volume() < aBox.Volume();
}
float Volume()
{
return length*height;
}
private:
float length;
float height;
friend bool operate > (const float& value, const Box& bBox);
}Box1,Box2;
bool operate > (const float& value, const Box& bBox) //这里访问的内容均为public,所以使用普通函数即可
{
return value > bBox.Volume();
}
if(Box1 < Box2)
{
...
}
if(2.0 > Box2)
{
...
}
若运算符重载函数为类成员函数,则总是将左边的实参(即上例中的实参1
)作为指针this
。
若运算符重载函数非类成员函数,可以使用普通函数(注意类类型作用域问题即可)、友元函数(操作的成员为类对象私有)。
类的前向声明:
假设这样一种情况:
有两个类,没人类中都有一个指针成员,指向对方类的对象(意味着两个类对象声明相互要在对象之前),怎么办?
前向声明的意义有点类似与普通变量的
extern
声明。
class Box1; //前向声明 (未完成的类声明)
class Box2; //前向声明 (未完成的类声明)
class Box1 //具体定义
{
...
};
class Box2 //具体定义
{
...
};
3.4.1.赋值运算符重载
如果不自定义,编译器会提供默认赋值运算重载函数,但功能仅仅是将成员逐个复制。与默认复制构造函数功能类似但却不同。
默认构造函数:当以现有的同类对象进行初始化或传值调用方式时,调用
默认赋值重载函数:左右两边是同类类型的对象时,调用。
个人理解:调用构造函数,是当创建一个新对象且要对它进行初始化时才调用;而赋值重载的调用是当左右两个同类对象都已经初始化之后。下面的例子可用与理解:
int a = 3; int b = a; //b此时初始化,调用构造函数 int a = b + 1; //此时a,b都已经初始化过了,调用的是赋值重载
CMessage& operator = (const CMessage& amess)
{
//if(this != &amess)
//{
delete [] pmessage;
pmessage = new char[strlen(amess.pmessage)+1];
strcpy_s(pmessage, strlen(amess.pmessage)+1, amess.pmessage);
//}
return *this;
}
motto1 = motto2 = motto3; //1
(motto1 = motto2) = motto3; //2
motto1 = motto1; //3
a = 3; //4
- 例中语句
//1
,此处返回类型可以是CMessage&
,也可以是CMessage
; - 例中语句
//2
,此处返回类型只能是CMessage&
,如果返回为CMessage
,(motto1 = motto2)
所返回的是一个rvlaue
,是不允许使用rvlaue
中的成员的。 - 例中语句
//3
,此时delete [] pmessage;
就造成了混乱。解决方法是将注释去掉。
如果希望赋值运算符灵活处理类,应将其返回值类型定义为类引用:类名&
。
重载运算符op1
之后,对于含有op1
的运算符都必须相应重载。如=
重载后,+=,-=,>=,<=,!=
等都需重载。(只针对类对象,对于普通的变量没有变化,//4
)
3.4.2.递增递减运算符重载
区分前缀与后缀运算符重载形式可以利用形参表:前缀有(无)形参,后缀无(有)形参。
这里使用的形参没有任何用处,仅仅是为了实现区分两种重载形式,让编译器知道如何调用。(具体可参看函数重载原理)
class CNum
{
public:
CNum(int a = 0):m_num(a) //构造函数
{}
//前缀重载
CNum& operator ++ ()
{
++(this->m_num);
return *this;
}
//后缀重载
const CNum operator ++ (int b) //const限定返回值不可修改,防止a++++形式
{
CNum num00 = *this;
++(this->m_num);
return num00;
}
private:
int m_num;
};
3.4.3.函数调用操作符重载
函数调用操作符为()
;重载函数调用操作符的类对象成为:函数对象或仿函数(functor)。
重载之后,可以像使用函数名一样使用类对象名:
class Area
{
public:
int operator () (int a, int b)
{
return a*b;
}
};
Area area00;
cout << area00(3,2) << endl;
函数对象提供一种将函数作为实参传递给另一个函数的方法。
void printArea(int a, int b, Area& c)
{
cout << area(a,b) << endl;
}
printArea(20,30,Area());
函数对象的类一般不需要数据成员,也没有定义的构造函数。所以这种函数对象的类开销最小。函数对象类也常定义为模板。