C++ Tutorials: C++ Language: Classes: Classes (I)

发布于:2022-12-15 ⋅ 阅读:(397) ⋅ 点赞:(0)

C++官方参考链接:Classes (I) - C++ Tutorials (cplusplus.com)

类(I)
类是数据结构的扩展概念:与数据结构一样,它们可以包含数据成员,但也可以包含作为成员的函数。
对象是类的实例化。就变量而言,类是类型,对象是变量。
使用关键字class或关键字struct定义类,其语法如下: 
class class_name {
  access_specifier_1:
    member1;
  access_specifier_2:
    member2;
  ...
} object_names;

其中class_name类的有效标识符object_names是该类对象的可选名称列表。声明体可以包含membersmembers可以是数据函数声明,和可选的access_specifier
类具有与普通数据结构相同的格式,只是它们也可以包含函数并具有称为access_specifier的新东西。access_specifier是以下三个关键字之一:privatepublicprotected。这些说明符为它们后面的成员修改访问权限:
类的private成员只能从同一类的其他成员(或从它们的“友元”)中访问。
protected的成员可以从同一类的其他成员(或从它们的“友元”)访问,也可以从它们的派生类的成员访问。
最后,public成员可以从对象可见的任何地方访问。 

默认情况下,用class关键字声明的类的所有成员都对其所有成员具有private访问权。因此,在任何其他access_specifier之前声明的任何成员自动具有private访问权限。例如:
class Rectangle {
    int width, height;
  public:
    void set_values (int,int);
    int area (void);
} rect;
声明一个名为Rectangle的类(即类型)和该类的一个名为rect的对象(即变量),该类包含四个成员:两个int类型的数据成员(成员width和成员height)具有private访问权限(因为private是默认访问级别),以及两个具有public访问权限的成员函数:函数set_values和area,目前我们只包含了它们的声明,而没有包含它们的定义。 
注意class name和object name之间的区别:在前面的示例中,Rectangle是class name(即类型),而rect是Rectangle类型的对象。它与下面声明中的int和a具有的关系相同:
int a;
其中int是类型名(类名),a是变量名(对象名)。

在Rectangle和rect声明之后,只要在object name和member name之间插入一个点(.),就可以像访问普通函数或普通变量一样访问对象rect的任何public成员。这遵循与访问普通数据结构成员相同的语法。例如:
rect.set_values (3,4);
myarea = rect.area();
rect中唯一不能从类外部访问的成员是width和height,因为它们具有private访问权限,并且只能从同一类的其他成员中引用它们。
下面是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;
}

这个例子重新引入了作用域操作符(::,两个冒号),在前面的章节中见过它与命名空间的关系。这里在函数set_values的定义中使用它来定义类本身以外的类的成员
注意,成员函数area的定义直接包含在Rectangle类的定义中,因为它非常简单。相反,set_values它只是在类内部用它的原型声明,但它的定义在类外部。在这个外部定义中,作用域(::)操作符用于指定被定义的函数是Rectangle类的成员,而不是普通的非成员函数。
作用域操作符(::)指定被定义的成员所属的类,授予完全相同的作用域属性,就像这个函数定义直接包含在类定义中一样。例如,前一个例子中的函数set_values可以访问变量width和height,它们是Rectangle类的private成员,因此只能从类的其他成员访问,比如这个。
完全在类定义中定义成员函数和只在函数中包含其声明并稍后在类外部定义成员函数之间的唯一区别是,在第一种情况下,编译器自动将函数视为内联(inline)成员函数,而在第二种情况下,它是普通(非内联)类成员函数。这不会导致行为上的差异,而只会导致可能的编译器优化。
成员width和height具有private访问权限(请记住,如果没有指定其他内容,则用关键字class定义的类的所有成员都具有private访问权限)。通过将它们声明为private,不允许从类外部访问它们。这是有意义的,因为我们已经定义了一个成员函数来为对象中的这些成员设置值:成员函数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;
}

在本例中,类(对象的类型)是Rectangle,它有两个实例(即对象):rect和rectb。它们中的每一个都有自己的成员变量和成员函数。
注意,调用rect.area()不会得到与调用rectb.area()相同的结果。这是因为Rectangle类的每个对象都有自己的变量width和height,因为它们在某种程度上也有自己的函数成员set_values和area,它们操作对象自己的成员变量。 
类允许使用面向对象范式进行编程:数据和函数都是对象的成员,减少了将处理程序或其他状态变量作为函数的实参传递和携带的需要,因为它们是调用其成员的对象的一部分。注意,在调用rect.area或rectb.area时没有传递任何实参。这些成员函数直接使用各自对象rect和rectb的数据成员。

构造函数
在前面的例子中,如果在调用set_values之前调用成员函数area,会发生什么情况?一个未确定的结果,因为成员的width和height从未被分配值。 
为了避免这种情况,类可以包含一个称为其构造函数的特殊函数,每当创建该类的新对象时,就会自动调用它,从而允许类初始化成员变量或分配存储空间。
这个构造函数的声明就像一个普通的成员函数,但是它的名称与类名匹配,并且没有任何返回类型;甚至不是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;

此示例的结果与前一个示例的结果相同。但是现在,类Rectangle没有成员函数set_values,而是有一个执行类似操作的构造函数:它使用传递给它的实参初始化width和height的值。
注意在创建该类的对象时,这些实参是如何传递给构造函数的:
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;
}

在上面的例子中,构造了Rectangle类的两个对象:rect和rectb。rect是用两个实参构造的,就像前面的例子一样。
但是这个例子还引入了一个特殊类型的构造函数:默认构造函数。默认构造函数是不接受形参的构造函数,它的特殊之处在于,它在声明对象但不使用任何实参初始化时被调用。在上面的例子中,rectb调用默认构造函数。注意,rectb甚至不是用空括号集构造的---事实上,空括号不能用来调用默认构造函数:
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;
}

与函数形式相比,统一初始化的一个优点是,与圆括号不同,大括号不会与函数声明混淆,因此可以用于显式调用默认构造函数:
Rectangle rectb;   // default constructor called
Rectangle rectc(); // function declaration (default constructor NOT called)
Rectangle rectd{}; // default constructor called 
调用构造函数的语法选择很大程度上取决于风格。大多数现有代码目前使用函数形式,一些较新的风格指南建议选择统一初始化而不是其他初始化,尽管它也有其潜在的缺陷,因为它优先选择initializer_list作为其类型。 

在构造函数中初始化成员
当使用构造函数初始化其他成员时,可以直接初始化这些其他成员,而不需要在构造函数体中使用语句。这是通过在构造函数的主体之前插入冒号(:)和类成员的初始化列表来实现的。例如,考虑一个具有以下声明的类:
class Rectangle {
    int width,height;
  public:
    Rectangle(int,int);
    int area() {return width*height;}
};
这个类的构造函数可以像往常一样定义为:
Rectangle::Rectangle (int x, int y) { width=x; height=y; }
但它也可以使用成员初始化定义为:
Rectangle::Rectangle (int x, int y) : width(x) { height=y; }
或者甚至: 
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;

在本例中,类Cylinder有一个类型为另一个类(base的类型为Circle)的成员对象。因为Circle类的对象只能用形参构造,所以Cylinder的构造函数需要调用base的构造函数,而惟一的方法是在成员初始化式列表中进行调用。
这些初始化也可以使用统一的初始化语法,使用大括号{}代替圆括号():
Cylinder::Cylinder (double r, double h) : base{r}, height{h} { } 

类的指针
对象也可以由指针指向:一旦声明,类就成为有效类型,因此它可以用作指针指向的类型。例如:
Rectangle * prect;
是一个指向Rectangle类对象的指针。
与普通数据结构类似,可以使用箭头操作符(->)直接从指针访问对象的成员。下面是一些可能的组合:
// 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';    
  cout << "(*baz)'s area:" << (*baz).area() << '\n';
  cout << "*(baz+1)'s area:" << (*(baz+1)).area() << '\n';  
  delete bar;
  delete[] baz;
  return 0;

本例使用了几个操作符对对象和指针进行操作(操作符*,&,.,->,[])。它们可以解释为:

expression(表达式) can be read as(能够被解释为)
*x pointed to by x(x所指向)
&x address of x(取x的地址)
x.y member y of object x(对象x的成员y)
x->y member y of object pointed to by x(x所指向的对象的成员y)
(*x).y member y of object pointed to by x (equivalent to the previous one) (x指向的对象的成员y(相当于前一个))
x[0] first object pointed to by x(x所指向的第1个对象)
x[1] second object pointed to by x(x所指向的第2个对象)
x[n] (n+1)th object pointed to by x(x所指向的第n+1个对象)

这些表达在前面的章节中已经介绍过了。最值得注意的是,关于数组的章节介绍了偏移操作符([]),关于普通数据结构的章节介绍了箭头操作符(->)。

用struct和union定义的类
类不仅可以用关键字class定义,还可以用关键字struct和union定义。
关键字struct通常用于声明普通数据结构,也可用于声明具有成员函数的类,其语法与关键字class相同。两者之间的唯一区别是,默认情况下,使用关键字struct声明的类的成员具有public访问权限,而使用关键字class声明的类的成员具有private访问权限。出于其他目的,这两个关键字在这个上下文中是等价的。
相反,union的概念不同于用struct和class声明的类,因为union每次只存储一个数据成员,但它们也是类,因此也可以保存成员函数。union类中的默认访问是public。