类 (I)

数据结构概念的扩展:与数据结构一样,类可以包含数据成员,但它们也可以包含函数作为成员。

一个对象是类的一个实例。从变量的角度来看,类就是类型,而对象就是变量。

类使用关键字class或关键字struct来定义,语法如下:

class class_name {
  access_specifier_1:
    member1;
  access_specifier_2:
    member2;
  ...
} object_names;


其中class_name是类的有效标识符,object_names是此类的对象的名称的可选列表。声明的体可以包含成员,这些成员可以是数据声明或函数声明,还可以包含可选的访问说明符

类具有与普通数据结构相同的格式,不同之处在于它们还可以包含函数,并且具有称为访问说明符的新内容。访问说明符是以下三个关键字之一:privatepublicprotected。这些说明符会修改它们后面的成员的访问权限。

  • 类的private成员只能从同一类的其他成员(或其“朋友”)内部访问。
  • protected成员可以从同一类的其他成员(或其“朋友”)访问,也可以从其派生类的成员访问。
  • 最后,public成员可以在任何可见对象的地方访问。

默认情况下,使用class关键字声明的类的所有成员都具有私有访问权限。因此,在任何其他访问说明符之前声明的任何成员都自动具有私有访问权限。例如:

1
2
3
4
5
6
class Rectangle {
    int width, height;
  public:
    void set_values (int,int);
    int area (void);
} rect;

声明了一个名为Rectangle的类(即类型),以及该类的一个名为rect的对象(即变量)。该类包含四个成员:两个int类型的数据成员(成员width和成员height),具有私有访问权限(因为私有是默认的访问级别),以及两个具有公共访问权限成员函数:函数set_valuesarea。目前我们只包含了它们的声明,而没有包含它们的定义。

请注意类名对象名之间的区别:在前面的示例中,Rectangle类名(即类型),而rectRectangle类型的对象。这与以下声明中的inta的关系相同:

1
int a;

其中int是类型名(类),a是变量名(对象)。

在声明了Rectanglerect之后,对象rect的任何公共成员都可以像访问普通函数或普通变量一样进行访问,只需在对象名成员名之间插入一个点(.)即可。这与访问普通数据结构的成员的语法相同。例如:

1
2
rect.set_values (3,4);
myarea = rect.area();

rect中唯一不能从类外部访问的成员是widthheight,因为它们具有私有访问权限,只能在该类的其他成员内部引用。

以下是Rectangle类的完整示例:
// classes example
#include <iostream>
using namespace std;

class Rectangle {
    int width, height;
  public:
    void set_values (int,int);
    int area() {return width*height;}
};

void Rectangle::set_values (int x, int y) {
  width = x;
  height = y;
}

int main () {
  Rectangle rect;
  rect.set_values (3,4);
  cout << "area: " << rect.area();
  return 0;
}
area: 12

此示例重新引入了作用域运算符::,两个冒号),该运算符在前面章节中与命名空间相关。此处用于在类外部定义类的成员函数set_values

请注意,成员函数area的定义已直接包含在类Rectangle的定义中,因为其极其简单。相反,set_values仅在类中用其原型声明,但其定义在类外部。在此外部定义中,使用作用域运算符(::)来指定正在定义的函数是Rectangle类的成员,而不是常规的非成员函数。

作用域运算符(::)指定了正在定义的成员所属的类,提供了与将此函数定义直接包含在类定义中完全相同的范围属性。例如,上例中的函数set_values可以访问变量widthheight,它们是Rectangle类的私有成员,因此只能从该类的其他成员(例如这个函数)访问。

在类定义中完全定义成员函数与仅在函数中包含其声明并在类外部稍后定义它的唯一区别是,第一种情况下编译器自动认为该函数是内联成员函数,而第二种情况则是普通(非内联)类成员函数。这不会造成行为上的差异,只会影响可能的编译器优化。

成员widthheight具有私有访问权限(请记住,如果没有指定其他内容,使用class关键字定义的类的所有成员都具有私有访问权限)。通过将它们声明为私有,不允许从类外部访问。这是有意义的,因为我们已经定义了一个成员函数来为对象中的这些成员设置值:成员函数set_values。因此,程序的其余部分不需要直接访问它们。也许在这个如此简单的例子中,很难看出限制对这些变量的访问有何用处,但在更大的项目中,防止值以意外的方式(从对象的角度来看是意外的)修改可能非常重要。

类的最重要属性是它是一个类型,因此我们可以声明它的多个对象。例如,以Rectangle类的先前示例为例,我们可以除了对象rect之外,还可以声明对象rectb

// example: one class, two objects
#include <iostream>
using namespace std;

class Rectangle {
    int width, height;
  public:
    void set_values (int,int);
    int area () {return width*height;}
};

void Rectangle::set_values (int x, int y) {
  width = x;
  height = y;
}

int main () {
  Rectangle rect, rectb;
  rect.set_values (3,4);
  rectb.set_values (5,6);
  cout << "rect area: " << rect.area() << endl;
  cout << "rectb area: " << rectb.area() << endl;
  return 0;
}
rect area: 12
rectb area: 30  

在这种特定情况下,类(对象的类型)是Rectangle,它有两个实例(即对象):rectrectb。它们中的每一个都有自己的成员变量和成员函数。

请注意,调用rect.area()的结果与调用rectb.area()的结果不同。这是因为Rectangle类的每个对象都有自己的变量widthheight,因为它们(以某种方式)也有自己的函数成员set_valuearea,这些函数作用于对象自己的成员变量。

类允许使用面向对象范式进行编程:数据和函数都是对象的成员,从而减少了将处理程序或其他状态变量作为参数传递给函数的需要,因为它们是调用其成员的对象的一部分。请注意,在调用rect.arearectb.area时没有传递任何参数。这些成员函数直接使用了它们各自对象rectrectb的数据成员。

构造函数

在前面的示例中,如果我们先调用set_values之前调用成员函数area会发生什么?结果不确定,因为成员widthheight从未被赋值。

为了避免这种情况,类可以包含一个称为其构造函数的特殊函数,当创建该类的新对象时,该函数会自动调用,允许类初始化成员变量或分配存储空间。

此构造函数声明方式与常规成员函数一样,但函数名与类名匹配,并且没有任何返回类型;甚至不是void

上述Rectangle类可以通过实现构造函数来轻松改进:

// example: class constructor
#include <iostream>
using namespace std;

class Rectangle {
    int width, height;
  public:
    Rectangle (int,int);
    int area () {return (width*height);}
};

Rectangle::Rectangle (int a, int b) {
  width = a;
  height = b;
}

int main () {
  Rectangle rect (3,4);
  Rectangle rectb (5,6);
  cout << "rect area: " << rect.area() << endl;
  cout << "rectb area: " << rectb.area() << endl;
  return 0;
}
rect area: 12
rectb area: 30  

此示例的结果与先前示例的结果相同。但现在,Rectangle类没有成员函数set_values,而是有一个构造函数执行类似的操作:它使用传递给它的参数来初始化widthheight的值。

请注意,在创建该类的对象时,这些参数是如何传递给构造函数的:

1
2
Rectangle rect (3,4);
Rectangle rectb (5,6);

构造函数不能像常规成员函数那样显式调用。它们只在创建该类的新对象时执行一次。

请注意,构造函数的原型声明(在类内)和后续的构造函数定义都没有返回值;甚至不是void:构造函数从不返回值,它们只是初始化对象。

重载构造函数

与任何其他函数一样,构造函数也可以通过采用不同参数的不同版本来重载:参数的数量不同和/或参数的类型不同。编译器会自动调用参数匹配的那个:

// overloading class constructors
#include <iostream>
using namespace std;

class Rectangle {
    int width, height;
  public:
    Rectangle ();
    Rectangle (int,int);
    int area (void) {return (width*height);}
};

Rectangle::Rectangle () {
  width = 5;
  height = 5;
}

Rectangle::Rectangle (int a, int b) {
  width = a;
  height = b;
}

int main () {
  Rectangle rect (3,4);
  Rectangle rectb;
  cout << "rect area: " << rect.area() << endl;
  cout << "rectb area: " << rectb.area() << endl;
  return 0;
}
rect area: 12
rectb area: 25  

在上面的示例中,构造了两个Rectangle类的对象:rectrectbrect使用两个参数构造,如之前的示例所示。

但此示例还引入了一种特殊的构造函数:默认构造函数默认构造函数是没有参数的构造函数,它之所以特殊,是因为当一个对象被声明但没有用任何参数进行初始化时,它就会被调用。在上面的示例中,为rectb调用了默认构造函数。请注意,rectb甚至没有用一对空括号构造——实际上,空括号不能用于调用默认构造函数。

1
2
Rectangle rectb;   // ok, default constructor called
Rectangle rectc(); // oops, default constructor NOT called 

这是因为一对空括号会将rectc声明为函数,而不是对象声明:它将是一个不接受任何参数并返回Rectangle类型值的函数。

统一初始化

如上所示,通过将参数括在括号中调用构造函数的方式被称为函数式。但构造函数也可以使用其他语法调用:

首先,带单个参数的构造函数可以使用变量初始化语法(等号后跟参数)调用:

class_name object_name = initialization_value;

最近,C++引入了使用统一初始化调用构造函数的方式,这本质上与函数式相同,但使用大括号({})而不是圆括号(()):

class_name object_name { value, value, value, ... }

可选地,最后一种语法可以在大括号前加上等号。

这里有一个构造函数接受单个参数的类的对象构造的四种方法的示例:

// classes and uniform initialization
#include <iostream>
using namespace std;

class Circle {
    double radius;
  public:
    Circle(double r) { radius = r; }
    double circum() {return 2*radius*3.14159265;}
};

int main () {
  Circle foo (10.0);   // functional form
  Circle bar = 20.0;   // assignment init.
  Circle baz {30.0};   // uniform init.
  Circle qux = {40.0}; // POD-like

  cout << "foo's circumference: " << foo.circum() << '\n';
  return 0;
}
foo's circumference: 62.8319

统一初始化相对于函数式的优势在于,与圆括号不同,大括号不会与函数声明混淆,因此可以用于显式调用默认构造函数:

1
2
3
Rectangle rectb;   // default constructor called
Rectangle rectc(); // function declaration (default constructor NOT called)
Rectangle rectd{}; // default constructor called 

调用构造函数的语法选择很大程度上是风格问题。大多数现有代码目前使用函数式,一些较新的风格指南建议优先选择统一初始化,尽管它也有其潜在的陷阱,因为其对initializer_list作为其类型的偏好。

构造函数中的成员初始化

当构造函数用于初始化其他成员时,这些其他成员可以直接初始化,而无需使用其体内的语句。这是通过在构造函数体之前插入冒号(:)和类成员的初始化列表来完成的。例如,考虑一个具有以下声明的类:

1
2
3
4
5
6
class Rectangle {
    int width,height;
  public:
    Rectangle(int,int);
    int area() {return width*height;}
};

该类的构造函数可以像往常一样定义为:

1
Rectangle::Rectangle (int x, int y) { width=x; height=y; }

但也可以使用成员初始化定义为:

1
Rectangle::Rectangle (int x, int y) : width(x) { height=y; }

甚至:

1
Rectangle::Rectangle (int x, int y) : width(x), height(y) { }

请注意,在最后一种情况下,构造函数除了初始化其成员外,什么也不做,因此它有一个空的函数体。

对于基本类型的成员,以上任何一种构造函数定义方式都没有区别,因为它们默认不初始化,但对于成员对象(类型是类的那些),如果它们没有在冒号后面初始化,则它们会被默认构造。

默认构造类的所有成员可能方便也可能不方便:在某些情况下,这是浪费(当成员之后在构造函数中重新初始化时),但在其他情况下,默认构造甚至不可行(当类没有默认构造函数时)。在这些情况下,成员应在成员初始化列表中初始化。例如:

// member initialization
#include <iostream>
using namespace std;

class Circle {
    double radius;
  public:
    Circle(double r) : radius(r) { }
    double area() {return radius*radius*3.14159265;}
};

class Cylinder {
    Circle base;
    double height;
  public:
    Cylinder(double r, double h) : base (r), height(h) {}
    double volume() {return base.area() * height;}
};

int main () {
  Cylinder foo (10,20);

  cout << "foo's volume: " << foo.volume() << '\n';
  return 0;
}
foo's volume: 6283.19

在此示例中,Cylinder类有一个成员对象,其类型是另一个类(base的类型是Circle)。由于Circle类的对象只能通过参数构造,因此Cylinder的构造函数需要调用base的构造函数,而唯一的方法是在成员初始化列表中进行。

这些初始化也可以使用统一初始化语法,使用大括号({})而不是圆括号(()):

1
Cylinder::Cylinder (double r, double h) : base{r}, height{h} { }

类指针

对象也可以被指针指向:一旦声明,类就成为一个有效的类型,因此它可以被用作指针指向的类型。例如:

1
Rectangle * prect;

是指向Rectangle类对象的指针。

与普通数据结构类似,可以通过使用箭头运算符(->)从指针直接访问对象成员。以下是一个包含一些可能组合的示例:

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
// pointer to classes example
#include <iostream>
using namespace std;

class Rectangle {
  int width, height;
public:
  Rectangle(int x, int y) : width(x), height(y) {}
  int area(void) { return width * height; }
};


int main() {
  Rectangle obj (3, 4);
  Rectangle * foo, * bar, * baz;
  foo = &obj;
  bar = new Rectangle (5, 6);
  baz = new Rectangle[2] { {2,5}, {3,6} };
  cout << "obj's area: " << obj.area() << '\n';
  cout << "*foo's area: " << foo->area() << '\n';
  cout << "*bar's area: " << bar->area() << '\n';
  cout << "baz[0]'s area:" << baz[0].area() << '\n';
  cout << "baz[1]'s area:" << baz[1].area() << '\n';       
  delete bar;
  delete[] baz;
  return 0;
}	

此示例使用了几个运算符来操作对象和指针(运算符*&.->[])。它们可以解释为:

表达式可以读作
*xx指向的对象
&xx的地址
x.y对象x的成员y
x->y指向x的对象成员y
(*x).y指向x的对象成员y(与上一行等效)
x[0]x指向的第一个对象
x[1]x指向的第二个对象
x[n]x指向的第(n+1)个对象

这些表达式中的大多数都在前面的章节中介绍过。最值得注意的是,数组章节介绍了偏移运算符([]),而普通数据结构章节介绍了箭头运算符(->)。

使用struct和union定义的类

类不仅可以使用关键字class定义,还可以使用关键字structunion定义。

关键字struct通常用于声明普通数据结构,但也可以用于声明具有成员函数的类,语法与关键字class相同。两者之间的唯一区别是,使用关键字struct声明的类的成员默认具有public访问权限,而使用关键字class声明的类的成员默认具有private访问权限。在所有其他方面,在这方面这两个关键字是等效的。

相反,联合的概念与使用structclass定义的类不同,因为联合一次只能存储一个数据成员,但尽管如此,它们也是类,因此也可以包含成员函数。联合类中的默认访问权限是public
Index
目录