类与接口

类是一种将抽象转换为用户定义类型的C++工具,它将数据表示和操作数据的方法组合成一个整洁的包。接口提供给我们从外部访问类与类内的成员和方法的一个途径。

一般对一个类的典型的实现策略是:将接口(类的定义)放在头文件中,将其实现(类方法的代码)放在源代码文件中

类的声明框架

1
2
3
4
5
6
7
8
class ClassName
{
private:
// some private variables and functions

public:
// some public variables and functions
};

访问控制

访问限定符有三个:privatepublicprotected,它们规定了修饰的变量和方法能够被访问的范围,在没有声明时,默认是private的。

这里先对三个访问限定词做一个比较全面的介绍:

  • private :
    • 类(基类)自身的成员函数
    • 类(基类)友元的成员函数
  • public :
    • 基类自身的成员函数
    • 基类友元的成员函数
    • 基类所产生派生类的成员函数
    • 基类所产生的派生类的友元函数
    • 其他的全局函数
  • protected :
    • 基类的成员函数
    • 基类的友元函数
    • 基类派生类的成员函数

例如一个类:

一个类的结构
一个类的结构

类的成员函数的实现

类的成员函数和一般的函数实现基本相同,还要增添如下两点:

  • 需要使用 :: 符号(作用域解析运算符)来标识这个函数是属于哪一个类的,因为不同的类可以有相同名称的函数
  • 类的方法可以访问类内的 private 的组件
1
int ClassName::myFunction(double a);
  • 类的成员函数也可以是内联的,只要加上关键词 inline 即可
  • 类的成员函数可以在类内定义时同时完成逻辑,也可以在类的外部定义

类的使用

类的实例化和一般的数据类型相同,调用类实例下的某个成员函数或者变量使用 . 点。

1
2
ClassName myClassInstance;
myClassInstance.aFunction();

类的构造函数和析构函数

构造函数

类的构造函数需要和类同名,是在类实例化的时候调用的,在实例化一个类的时候,虽然我们没有显示地声明,但是还是调用了构造函数,而C++对每一个类都有默认的构造函数,就是不接受任何参数,什么都不做,也无返回值。我们可以定义自己的构造函数并且调用它。

例如一个类MyClass的定义如下:

1
2
3
4
5
6
7
8
9
class MyClass
{
private:
int myInt;
double myDouble;
public:
MyClass(int mi, double md) { myInt = mi; myDouble = md;};
MyClass() { myInt = 1; myDouble = 0.2;};
}

这里我们使用了一个包含两个参数的构造函数,它的作用是对两个private的成员变量赋值。

构造函数不能像其他成员函数一样使用对象(类的实例)来用点调用,因为构造函数是在实例化类的时候就调用的,比如如下的调用方式:

1
2
3
MyClass myClass = MyClass(1, 0.2);
MyClass myClass(1, 0.2);
MyClass * myClassPoint = new MyClass(1, 0.2);

如果是使用的默认构造函数或者构造函数没有参数的话,可以直接声明对象而不显示地调用构造函数,比如我们的类中还有一个重载的没有参数的构造函数,它可以这样被调用:

1
2
3
4
MyClass myClass; // 隐式调用
MyClass myClass = MyClass(); // 显示调用
MyClass * myClass = new MyClass(); // 隐式调用
MyClass myClassFunction(); // 这是一个返回值是MyClass的函数

析构函数

用构造函数创建对象后,程序负责跟踪该对象,直到其过期为止,对象过期时,程序将自动调用一个特殊的成员函数,即析构函数。

析构函数用于完成清理工作,所以非常有用,例如如果构造函数用new分配了内存,则可以在析构函数中用delete释放内存。

默认的析构函数是什么都不做的。我们以可以显示地定义自己析构函数,析构函数是一个~符号加上类名来定义的,析构函数何时调用是取决于编译器的。

1
2
3
4
5
6
7
8
9
10
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限定符加在成员函数的后边:

1
2
3
4
5
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的引用,所以可以这样声明这个函数:

1
const MyClass & MyClass::compare(const MyClass & myClass) const;

函数定义中涉及到三个const:

  • 第一个const:表明返回值是一个MyClass,显然不能被改变,所以可以时const的
  • 第二个const:传入的MyClass实例只是用于比较的,不需要改变,所以使用const
  • 第三个const:由于成员函数不改变调用类对象,所以是const的成员函数

比较myInt的函数可以使用this来这样实现:

1
2
3
4
5
6
7
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指针

对象数组

类和其他数据结构一样,都可以创建数组,对象的数组即可以存储多个类对象,只需要像下边这样声明它们:

1
2
3
MyClass myClasses[3];
myClasses[0].show();
myClasses[1].compare(myClasses[2]);

运算符重载

运算符重载即将C++中的运算符重载扩展到用户自定义的类型,例如,+这个运算符,只能用于整形、浮点型、字符串等基本的数据结构相加,但是我们可以通过用户的定义,将其用于两个类的对象相加,两个数组相加等等,编译器会根据操作数和目的数的类型决定使用哪种定义。

运算符重载的写法

运算符重载的格式为:

1
operator op (arguments);

比如:

1
2
3
operator +( ); // 重载+运算符
operator *( ); // 重载*运算符
operator [ ]( ); // 重载[]运算符

一个运算符重载的例子

假设我们有一个时间类Time,由两个私有成员变量 hours、minutes 来代表小时和分钟,我们来实现Time类对象的相加逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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的对象像其他一般数据类型一样进行相加:

1
2
3
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类:

1
2
3
4
5
6
7
8
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;
}

显然调用上述*的重载需要这样:

1
2
3
Time A();
Time B(1, 20);
A = B * 2.5;

相当于调用了这样的运算符重载的成员函数:

1
A = B.operator*(2.5);

但是,问题来了,如果使用 A = 2.5 * B 就无法成功,这似乎违背了乘法的分配律,这一点虽然并不有违于C++的语法,但是貌似并不用户友好,我们需要告诉使用的人只能用第一种方式而不能用第二种方式。解决这个问题有两个方法:

  • 使用一个非成员函数来定义反写的情况:
    1
    2
    3
    4
    Time operator * (double d, const Time & t)
    {
    return t * m;
    }

这种方式不失为是一种非常好的方法,而且如果有所修改,只需要修改类内的运算符重载即可。

  • 使用友元函数
    和上述的思想类似,我们可以定义一个非成员函数,然后这样的重载运算符,从而定义一个double乘以一个Time类对象的操作:
1
Time operator * (double d, const Time & t);

但是问题在于类外的非成员函数无法访问类的私有变量。所以友元函数的作用在于可以访问类的私有成员,但是他是一个非成员函数。

创建友元函数

创建友元函数的第一步是将其原型放在类声明中,并在原型声明前加上关键字 friend:

1
friend Time operator*(double m, const Time t);

该原型意味着下面两点:

  • 虽然 operator*() 函数是在类声明中声明的,但它不是成员函数,因此不能使用成员运算符来调用
  • 虽然 operator*() 函数不是成员函数,但它与成员函数的访问权限相同

第二步是编写函数定义。因为它不是成员函数,所以不要使用 Time:: 限定符。另外,不能在定义中使用关键字 friend

1
2
3
4
5
6
7
8
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;
}

上述定义后即可使用如下的语句来使用乘法:

1
A = 2.5 * B;

相当于调用友元函数:

1
A = operator*(2.5, B);

成员函数和非成员函数的选择

对于一般的运算符重载,比如+和-这种不会出现乘法那种左右交换的问题的,有两种解决方式:

1
2
Time operator + (const Time & t) const;
friend Time operator + (const Time & t1, const Time & t2);

第一种方式是通过this隐式地传递一个参数,另一个使用函数参数显示地传递;第二种方式是两个参数都显示地通过参数传递。在调用 T1 = T2 + T3 时,会分别编译成如下的形式:

1
2
T1 = T2.operator+(T3);
T1 = operator+(T2, T3);

但是,两种方式不能同时定义,只能选择其中一个,否则会引发二义性的编译错误,基于乘法的例子,显然使用友元函数比较通用。

类的自动转换和强制类型转换

强制类型转换

C++允许一些强制类型转换,比如强制将double值转换成int值,把double的2.5转换成int会成为2从而丢失0.5。但是如果用户希望进行强制转换只需要使用如下的方式:

1
2
targetType valueName = (targetType) value;
targetType valueName = targetType (value);

使用构造函数进行类的自动转换

假设我们有一个类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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类对象:

1
2
MyClass myClass;
myClass = 2.5;

这是可以的,首先创建了一个MyClass的对象,然后使用2.5将其初始化,实际上是使用了第一个构造函数 MyClass(double),这是一个隐式转换的过程,不需要进行强制转换。

只有接受一个参数的构造函数才能作为转换函数,如果像第二个构造函数那样有两个参数,不能用来转换类型,但是如果第二个参数有默认参数,就可以:

1
MyClass(int i, double d = 1.5);

这个可以将一个int值隐式地转换成MyClass类型。

如果不希望编辑器进行这种隐式转换,可以使用explicit关键词修饰构造函数,这样就无法使用该构造函数进行类型转换:

1
explicit MyClass(double d);

这样会关闭隐式转换,但依然允许显示转换,即使用显式地强制转换:

1
2
3
MyClass myClass;
myClass = MyClass(2.5);
myClass = (MyClass)2.5;

转换函数

上边提到了隐式或者显式地将基本数据类型的数据转换成类对象,接下来的问题是如何将一个类对象转换成其他的基本数据类型,这一点可以通过转换函数来实现。转换函数是用户定义的强制类型转换,需要这样定义:

1
operator dateType();

需要注意的是:

  • 转换函数必须是类方法
  • 转换函数不能指定返回类型
  • 转换函数不能有参数

比如我们将MyClass转换为一个double类型的变量,需要这样一个成员函数:

1
2
3
4
5
6
operator double();

MyClass::operator double()
{
return myDoble;
}

然后就可以这样使用类型转换了:

1
2
3
MyClass myClass(1, 2.5);
double myDouble = (double) myClass;
double myDouble = double (myClass);

复制构造函数

复制构造函数接受其所属类的对象作为参数。例如,MyClass类的复制构造函数的原型如下:

1
MyClass(const MyClass &);

在下述情况下,将使用复制构造函数:

  • 将新对象初始化为一个同类对象
  • 按值将对象传递给函数
  • 函数按值返回对象
  • 编译器生成临时对象

如果程序没有使用(显式或隐式)复制构造函数,编译器将提供原型,但不提供函数定义;否则,程序将定义一个执行成员初始化的复制构造函数。也就是说,新对象的每个成员都被初始化为原始对象相应成员的值。如果成员为类对象,则初始化该成员时,将使用相应类的复制构造函数。