【C++温故知新】详解C++中的类和对象
这是C++类重新复习学习笔记的第 六 篇,同专题的其他文章可以移步:https://www.jianshu.com/nb/39156122
类与接口
类是一种将抽象转换为用户定义类型的C++工具,它将数据表示和操作数据的方法组合成一个整洁的包。接口提供给我们从外部访问类与类内的成员和方法的一个途径。
一般对一个类的典型的实现策略是:将接口(类的定义)放在头文件中,将其实现(类方法的代码)放在源代码文件中
类的声明框架
class ClassName
{
private:
// some private variables and functions
public:
// some public variables and functions
};
访问控制
访问限定符有三个:private
、public
、protected
,它们规定了修饰的变量和方法能够被访问的范围,在没有声明时,默认是private的。
这里先对三个访问限定词做一个比较全面的介绍:
-
private
:- 类(基类)自身的成员函数
- 类(基类)友元的成员函数
-
public
:- 基类自身的成员函数
- 基类友元的成员函数
- 基类所产生派生类的成员函数
- 基类所产生的派生类的友元函数
- 其他的全局函数
-
protected
:- 基类的成员函数
- 基类的友元函数
- 基类派生类的成员函数
例如一个类:
一个类的结构类的成员函数的实现
类的成员函数和一般的函数实现基本相同,还要增添如下两点:
- 需要使用
::
符号(作用域解析运算符)来标识这个函数是属于哪一个类的,因为不同的类可以有相同名称的函数 - 类的方法可以访问类内的
private
的组件
int ClassName::myFunction(double a);
- 类的成员函数也可以是内联的,只要加上关键词
inline
即可 - 类的成员函数可以在类内定义时同时完成逻辑,也可以在类的外部定义
类的使用
类的实例化和一般的数据类型相同,调用类实例下的某个成员函数或者变量使用 .
点。
ClassName myClassInstance;
myClassInstance.aFunction();
类的构造函数和析构函数
构造函数
类的构造函数需要和类同名,是在类实例化的时候调用的,在实例化一个类的时候,虽然我们没有显示地声明,但是还是调用了构造函数,而C++对每一个类都有默认的构造函数,就是不接受任何参数,什么都不做,也无返回值。我们可以定义自己的构造函数并且调用它。
例如一个类MyClass的定义如下:
class MyClass
{
private:
int myInt;
double myDouble;
public:
MyClass(int mi, double md) { myInt = mi; myDouble = md;};
MyClass() { myInt = 1; myDouble = 0.2;};
}
这里我们使用了一个包含两个参数的构造函数,它的作用是对两个private的成员变量赋值。
构造函数不能像其他成员函数一样使用对象(类的实例)来用点调用,因为构造函数是在实例化类的时候就调用的,比如如下的调用方式:
MyClass myClass = MyClass(1, 0.2);
MyClass myClass(1, 0.2);
MyClass * myClassPoint = new MyClass(1, 0.2);
如果是使用的默认构造函数或者构造函数没有参数的话,可以直接声明对象而不显示地调用构造函数,比如我们的类中还有一个重载的没有参数的构造函数,它可以这样被调用:
MyClass myClass; // 隐式调用
MyClass myClass = MyClass(); // 显示调用
MyClass * myClass = new MyClass(); // 隐式调用
MyClass myClassFunction(); // 这是一个返回值是MyClass的函数
析构函数
用构造函数创建对象后,程序负责跟踪该对象,直到其过期为止,对象过期时,程序将自动调用一个特殊的成员函数,即析构函数。
析构函数用于完成清理工作,所以非常有用,例如如果构造函数用new
分配了内存,则可以在析构函数中用delete
释放内存。
默认的析构函数是什么都不做的。我们以可以显示地定义自己析构函数,析构函数是一个~符号加上类名来定义的,析构函数何时调用是取决于编译器的。
class MyClass
{
private:
int myInt;
double myDouble;
public:
MyClass(int mi, double md) { myInt = mi; myDouble = md;};
MyClass() { myInt = 1; myDouble = 0.2;};
~MyClass() { cout << "bye!"; };
}
const成员函数
const成员函数是指,保证该成员函数不会改变调用的对象,声明和定义const成员函数需要将const限定符加在成员函数的后边:
void show() const;
void MyClass::show() const
{
// function body
}
以这种方式声明和定义的类函数即const成员函数,应该尽可能地将成员函数修饰为const,只要该类的方法不修改调用对象。
this指针
this
指针在类的成员函数中,用来作为指向调用类对象自身的指针,即它指向自己的类的地址。我们上面的构造函数中的 myInt = mi;
这一语句,其实这里的 myInt
就是 this->myInt
的简写,因为在类中,可以直接用成员变量简单地替换 this->
成员变量。
this指针在只操作自身类内成员的时候不会有特别多的作用,因为都可以省略它,但是一旦我们的成员函数涉及到两个及以上的类的对象时,this就发挥了很大的作用。例如我们有一个compare函数,用于比较两个MyClass类的实例的哪一个的myInt值更大,那么我们必然需要另一个MyClass的实例作为参数,然后让它的myInt和自己的myInt比较,然后返回myInt较大的那个MyClass的引用,所以可以这样声明这个函数:
const MyClass & MyClass::compare(const MyClass & myClass) const;
函数定义中涉及到三个const:
- 第一个const:表明返回值是一个MyClass,显然不能被改变,所以可以时const的
- 第二个const:传入的MyClass实例只是用于比较的,不需要改变,所以使用const
- 第三个const:由于成员函数不改变调用类对象,所以是const的成员函数
比较myInt的函数可以使用this来这样实现:
const MyClass & MyClass::compare(const MyClass & myClass) const
{
if(myClass.myInt > this->myInt)
return myClass;
else
return *this;
}
很显然,上边的 this->myInt
可以使用 myInt
直接简写,而返回自己调用类对象的时候,就只能用 this 来称呼了,而且需要注意的是,返回的是一个MyClass的引用,从而需要使用*this
而不是直接返回this
,因为this
是指针
对象数组
类和其他数据结构一样,都可以创建数组,对象的数组即可以存储多个类对象,只需要像下边这样声明它们:
MyClass myClasses[3];
myClasses[0].show();
myClasses[1].compare(myClasses[2]);
运算符重载
运算符重载即将C++中的运算符重载扩展到用户自定义的类型,例如,+这个运算符,只能用于整形、浮点型、字符串等基本的数据结构相加,但是我们可以通过用户的定义,将其用于两个类的对象相加,两个数组相加等等,编译器会根据操作数和目的数的类型决定使用哪种定义。
运算符重载的写法
运算符重载的格式为:
operator op (arguments);
比如:
operator +( ); // 重载+运算符
operator *( ); // 重载*运算符
operator [ ]( ); // 重载[]运算符
一个运算符重载的例子
假设我们有一个时间类Time,由两个私有成员变量 hours、minutes 来代表小时和分钟,我们来实现Time类对象的相加逻辑。
class Time
{
private:
int hours;
int minutes;
public:
Time;
Time(int h, int m=0);
Time operator + (const Time & t) const;
};
Time::Time()
{
hours = minutes = 0;
}
Time::Time(int h, int m)
{
hours = h;
minutes = m;
}
Time Time::operator + (const Time & t) const
{
Time sum;
sum.minutes = minutes + t.minutes;
sum.hours = hours + t.hours + sum.minutes / 60;
sum.minutes %= 60;
return sum;
}
使用这个重载的+运算符可以将两个Time的对象像其他一般数据类型一样进行相加:
Time time1;
Time time2;
Time total = time1 + time2;
运算符重载的限制
多数C++运算符都可以用这样的方式重载。重载的运算符(有些例外情况)不必是成员函数,但必须至少有一个操作数是用户定义的类型。C++运算符重载的限制如下:
- 重载后的运算符必须至少有一个操作数是用户定义的类型,这将防止用户为标准类型重载运算符因此,例如不能将减法运算符重载为计算两个 double 值的和,而不是它们的差。
- 使用运算符时不能违反运算符原来的句法规则。例如,不能将求模运算符(%)重载成使用一个操作数的运算
- 不能修改运算符的优先级。例如,如果将加号运算符重载成将两个类相加,则新的运算符与原来的加号具有相同的优先级
- 不能创建新运算符。例如,不能定义
operator **()
函数来表示求幂 - 不能重载下面的运算符
-
sizeof
:sizeof 运算符 -
.
:成员运算符 -
*
:成员指针运算符 -
::
:作用域解析运算符 -
? :
:条件运算符 -
typeid
:一个RTTI运算符 -
const_cast
:强制类型转换运算符 -
dynamic_cast
:强制类型转换运算符 -
reinterpret_cast
:强制类型转换运算符 -
static_cast
:强制类型转换运算符
-
- 大多数运算符都可以通过成员函数或者非成员函数进行重载,但是如下的运算符只能通过成员函数进行重载:
-
=
:赋值运算符 -
( )
:函数调用运算符 -
[ ]
:下标运算符 -
->
:通过指针访问类成员运算符
-
可以重载的运算符
+ |
- |
* |
/ |
% |
^ |
||
---|---|---|---|---|---|---|---|
& |
` | ` | ~= |
! |
= |
< |
|
> |
+= |
-= |
*= |
/= |
%= |
||
^= |
&= |
` | =` | << |
>> |
>>= |
|
<<= |
== |
!= |
<= |
>= |
&& |
||
` | ` | ++ |
-- |
, |
->* |
-> |
|
() |
[] |
new |
delete |
new[] |
delete[] |
友元函数
类的友元函数是非成员函数,其访问权限与成员函数相同。
一个友元函数的例子
回到上面的Time类,我们重载运算符:将运算符重载成一个double值乘以一个Time类:
Time Time::operator * (const double d) const
{
Time result;
long totalMinutes = hours * d * 60 + minutes * d;
result.hours = totalMinutes / 60;
result.minutes = totalMinutes % 60;
return result;
}
显然调用上述*
的重载需要这样:
Time A();
Time B(1, 20);
A = B * 2.5;
相当于调用了这样的运算符重载的成员函数:
A = B.operator*(2.5);
但是,问题来了,如果使用 A = 2.5 * B 就无法成功,这似乎违背了乘法的分配律,这一点虽然并不有违于C++的语法,但是貌似并不用户友好,我们需要告诉使用的人只能用第一种方式而不能用第二种方式。解决这个问题有两个方法:
- 使用一个非成员函数来定义反写的情况:
Time operator * (double d, const Time & t)
{
return t * m;
}
这种方式不失为是一种非常好的方法,而且如果有所修改,只需要修改类内的运算符重载即可。
- 使用友元函数
和上述的思想类似,我们可以定义一个非成员函数,然后这样的重载运算符,从而定义一个double乘以一个Time类对象的操作:
Time operator * (double d, const Time & t);
但是问题在于类外的非成员函数无法访问类的私有变量。所以友元函数的作用在于可以访问类的私有成员,但是他是一个非成员函数。
创建友元函数
创建友元函数的第一步是将其原型放在类声明中,并在原型声明前加上关键字 friend:
friend Time operator*(double m, const Time t);
该原型意味着下面两点:
- 虽然
operator*()
函数是在类声明中声明的,但它不是成员函数,因此不能使用成员运算符来调用 - 虽然
operator*()
函数不是成员函数,但它与成员函数的访问权限相同
第二步是编写函数定义。因为它不是成员函数,所以不要使用 Time::
限定符。另外,不能在定义中使用关键字 friend
:
Time operator * (double d, const Time & t)
{
Time result;
long totalMinutes = hours * d * 60 + minutes * d;
result.hours = totalMinutes / 60;
result.minutes = totalMinutes % 60;
return result;
}
上述定义后即可使用如下的语句来使用乘法:
A = 2.5 * B;
相当于调用友元函数:
A = operator*(2.5, B);
成员函数和非成员函数的选择
对于一般的运算符重载,比如+和-这种不会出现乘法那种左右交换的问题的,有两种解决方式:
Time operator + (const Time & t) const;
friend Time operator + (const Time & t1, const Time & t2);
第一种方式是通过this隐式地传递一个参数,另一个使用函数参数显示地传递;第二种方式是两个参数都显示地通过参数传递。在调用 T1 = T2 + T3 时,会分别编译成如下的形式:
T1 = T2.operator+(T3);
T1 = operator+(T2, T3);
但是,两种方式不能同时定义,只能选择其中一个,否则会引发二义性的编译错误,基于乘法的例子,显然使用友元函数比较通用。
类的自动转换和强制类型转换
强制类型转换
C++允许一些强制类型转换,比如强制将double值转换成int值,把double的2.5转换成int会成为2从而丢失0.5。但是如果用户希望进行强制转换只需要使用如下的方式:
targetType valueName = (targetType) value;
targetType valueName = targetType (value);
使用构造函数进行类的自动转换
假设我们有一个类
class MyClass
{
private:
int myInt;
double myDouble;
public:
MyClass(double d);
MyClass(int i, double d);
MyClass();
~MyClass();
}
MyClass::MyClass(double d)
{
myDouble = d;
myInt = 0;
}
MyClass::MyClass(int i, double d)
{
myDouble = d;
myInt = i;
}
MyClass::MyClass()
{
}
然后我们尝试将一个double值赋给一个MyClass类对象:
MyClass myClass;
myClass = 2.5;
这是可以的,首先创建了一个MyClass的对象,然后使用2.5将其初始化,实际上是使用了第一个构造函数 MyClass(double),这是一个隐式转换的过程,不需要进行强制转换。
只有接受一个参数的构造函数才能作为转换函数,如果像第二个构造函数那样有两个参数,不能用来转换类型,但是如果第二个参数有默认参数,就可以:
MyClass(int i, double d = 1.5);
这个可以将一个int值隐式地转换成MyClass类型。
如果不希望编辑器进行这种隐式转换,可以使用explicit关键词修饰构造函数,这样就无法使用该构造函数进行类型转换:
explicit MyClass(double d);
这样会关闭隐式转换,但依然允许显示转换,即使用显式地强制转换:
MyClass myClass;
myClass = MyClass(2.5);
myClass = (MyClass)2.5;
转换函数
上边提到了隐式或者显式地将基本数据类型的数据转换成类对象,接下来的问题是如何将一个类对象转换成其他的基本数据类型,这一点可以通过转换函数来实现。转换函数是用户定义的强制类型转换,需要这样定义:
operator dateType();
需要注意的是:
- 转换函数必须是类方法
- 转换函数不能指定返回类型
- 转换函数不能有参数
比如我们将MyClass转换为一个double类型的变量,需要这样一个成员函数:
operator double();
MyClass::operator double()
{
return myDoble;
}
然后就可以这样使用类型转换了:
MyClass myClass(1, 2.5);
double myDouble = (double) myClass;
double myDouble = double (myClass);
复制构造函数
复制构造函数接受其所属类的对象作为参数。例如,MyClass类的复制构造函数的原型如下:
MyClass(const MyClass &);
在下述情况下,将使用复制构造函数:
- 将新对象初始化为一个同类对象
- 按值将对象传递给函数
- 函数按值返回对象
- 编译器生成临时对象
如果程序没有使用(显式或隐式)复制构造函数,编译器将提供原型,但不提供函数定义;否则,程序将定义一个执行成员初始化的复制构造函数。也就是说,新对象的每个成员都被初始化为原始对象相应成员的值。如果成员为类对象,则初始化该成员时,将使用相应类的复制构造函数。
复制构造函数
复制构造函数接受其所属类的对象作为参数。例如,MyClass类的复制构造函数的原型如下:
MyClass(const MyClass &);
在下述情况下,将使用复制构造函数:
- 将新对象初始化为一个同类对象
- 按值将对象传递给函数
- 函数按值返回对象
- 编译器生成临时对象
如果程序没有使用(显式或隐式)复制构造函数,编译器将提供原型,但不提供函数定义;否则,程序将定义一个执行成员初始化的复制构造函数。也就是说,新对象的每个成员都被初始化为原始对象相应成员的值。如果成员为类对象,则初始化该成员时,将使用相应类的复制构造函数。
转载请注明出处,本文永久更新链接:https://blogs.littlegenius.xin/2019/08/27/【C-温故知新】六类和对象/