Classes (I)

重要的事情说三遍:

强烈建议按照目录结构中的顺序学习!!!点我查看教程目录结构

强烈建议按照目录结构中的顺序学习!!!点我查看教程目录结构

强烈建议按照目录结构中的顺序学习!!!点我查看教程目录结构

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

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

使用关键字classstruct定义类,语法如下:

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

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

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

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

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

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的关系相同:

int a;

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

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

rect.set_values(3, 4);
int 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;
}

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

注意,成员函数area的定义直接包含在Rectangle类的定义中,因为它非常简单。相反,set_values只在类内声明了其原型,但定义在类外部。在这个外部定义中,使用作用域运算符(::)来指定定义的函数是类Rectangle的成员,而不是普通的非成员函数。

作用域运算符(::)指定正在定义的成员所属的类,赋予与直接包含在类定义中相同的作用域属性。例如,上例中的函数set_values可以访问类Rectangle的私有成员变量widthheight,因此只能从该类的其他成员(如此函数)访问。

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

成员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;
}

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

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

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

构造函数

如果在前面的示例中调用成员函数area之前没有调用set_values,会发生什么?结果是不确定的,因为成员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;
}

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

注意在创建此类对象时如何将这些参数传递给构造函数:

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类的对象:rectrectbrect是用两个参数构造的,就像前面的例子一样。

但这个例子还引入了一种特殊的构造函数:默认构造函数。默认构造函数是指不带参数的构造函数,它是特殊的,因为当对象被声明但没有使用任何参数初始化时调用。在上面的例子中,默认构造函数被调用用于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:
 // 去掉base(r)会导致编译器报错,因为Circle没有默认构造函数
    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';
  delete bar;
  delete[] baz;
  return 0;
}

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

表达式可以解释为
*x指向x的对象
&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声明的类的成员默认具有公共访问权限,而用关键字class声明的类的成员默认具有私有访问权限。在这个上下文中,除了访问权限外,两者在其他所有方面都是等价的。

相反,联合体的概念与使用structclass声明的类不同,因为联合体一次只存储一个数据成员,但它们也是类,因此也可以包含成员函数。联合类的默认访问权限是public

  • 22
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值