抽象
对象大致意味着一系列数据(属性)以及一套访问和操作这些数据的方法。使用对象而非全局变量和函数的原因有多个,下面列出了使用对象的最重要的好处。
- 多态:可对不同类型的对象执行相同的操作,而这些操作就像“被施了魔法”一样能够正常运行。
- 封装:对外部隐藏有关对象工作原理的细节。
- 继承:可基于通用类创建出专用类。
isinstance(object, tuple)
这里使用isinstance来执行类型/类检查旨在说明:使用类型检查通常是馊主意,应尽可能避免;
简介
类是数据结构的扩展概念:与数据结构一样,它们可以包含数据成员,但它们也可以包含作为成员的函数。
一个对象是一个类的实例化。就变量而言,类是类型,对象是变量。
使用关键字class或关键字定义类struct,使用以下语法:
class class_name {
access_specifier_1:
member1;
access_specifier_2:
member2;
...
} object_names;
class_name
是类的有效标识符,object_names
是此类对象的可选名称列表。声明的主体可以包含成员,可以是数据或函数声明,也可以是访问说明符。
类具有与普通数据结构相同的格式,除了它们还可以包含函数并具有称为访问说明符的这些新东西。一个访问说明符是以下三个关键字之一:private
,public
或protected
。这些说明符修改了跟随它们的成员的访问权限:
-
private
类成员只能从同一个类的其他成员(或他们的“朋友”)中访问。 -
protected
成员可以从同一类的其他成员(或来自他们的“朋友”)访问,也可以从其派生类的成员访问。 - 最后,
public
可以从对象可见的任何位置访问成员。
默认情况下,使用class
关键字声明的类的所有成员都对其所有成员具有私有访问权限。因此,在任何其他访问说明符之前声明的任何成员都会自动拥有私有访问权限。例如:
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
,现在只有他们的声明,没有他们的定义。
注意类名和对象名之间的区别:在前面的例子中,Rectangle
是类名(即类型),而是rect
类型的对象Rectangle
。它是相同的关系int
,a
并在以下声明中:
int a;
int
是类型名称(类),a
是变量名称(对象)。
声明之后Rectangle
和rect
,任何对象的公共成员rect
可以如同它们是正常功能或正常变量进行访问,通过简单地插入一个.
在对象名称和成员名称之间。这遵循与访问纯数据结构成员相同的语法。例如:
rect.set_values (3,4);
myarea = rect.area();
唯一的成员rect
不能从类的外部访问是width
和height
,因为他们有private
,他们只能从同一类的其他成员中提到。
// 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;
}
此示例重新引入了scope operator::
,在前面的章节中与名称空间相关。这里它用于函数的定义,set_values
以定义类本身之外的类的成员。
set_values
它仅在类中声明其原型,但其定义不在其中。在此外部定义中,::
用于指定要定义的函数是类Rectangle
的成员而不是常规的非成员函数。
::
指定要定义的成员所属的类,授予完全相同的作用域属性,就好像此函数定义直接包含在类定义中一样。例如,该功能set_values
在前面的例子中可以访问private
的变量width
和height
。
在类定义中完全定义成员函数或仅在函数中包含其声明并在稍后在类外定义它的唯一区别是:
- 在第一种情况下,函数自动被视为内联函数成员函数由编译器;
- 而在第二个它是一个普通(非内联)类成员函数。这不会导致行为差异,但仅限于可能的编译器优化。
成员width
并height
具有私有访问权限(请记住,如果未指定任何其他内容,则使用关键字定义的类的所有成员class都具有私有访问权限)。通过声明它们是私有的,不允许从类外访问。因为我们已经定义了一个成员函数来为对象中的那些成员设置值:成员函数set_values
。因此,程序的其余部分不需要直接访问它们。也许在这样一个如此简单的例子中,很难看出对这些变量的限制访问是如何有用的,但在更大的项目中,不能以意想不到的方式修改值可能是非常重要的(从意图的角度来看是意外的)物体)。
构造函数-Constructors
如果我们在调用area
之前调用成员函数,前一个例子会发生什么set_values
?一个未确定的结果,因为成员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;
}
该示例的结果与前一示例的结果相同。但是现在,class 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
。
统一初始化 uniform init
如上所示,通过将其参数括在括号中来调用构造函数的方式称为函数形式的初始化(function form)。但是也可以使用其他语法调用构造函数:首先,可以使用变量初始化语法(即等号后跟参数)调用具有单个参数的构造函数:
class_name object_name = initialization_value;
最近,C++引入了使用统一初始化调用构造函数的可能性,它基本上与函数形式相同,但使用大括号{}
而不是括号()
:
class_name object_name { value, value, value, ... }
最后一个语法形式上同上,但需要在大括号之前包含等号。
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;
}
在这个例子中,class Cylinder
有一个成员对象,其类型是另一个类(base
的类型是Circle
)。因为类的对象Circle
只能用参数构造,所以Cylinder
构造函数需要调用base
构造函数,唯一的方法是在成员初始化列表中。
这些初始化也可以使用统一的初始化语法,使用大括号{}
而不是括号()
:
Cylinder::Cylinder (double r, double h) : base{r}, height{h} { }
类指针
对象也可以通过指针指向:一旦声明,类就成为有效类型,因此它可以用作指针指向的类型。例如:
Rectangle * prect;
是指向类对象的指针Rectangle
。
与纯数据结构struct
类似,可以使用箭头操作符->
直接从指针访问对象的成员。以下是一些可能的组合示例:
// 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;
}
使用关键字struct和union定义的类
类不仅可以使用关键字class
,还可以使用关键字struct
和union
定义。
通常用于声明普通数据结构的关键字struct
也可用于声明具有成员函数的类,其语法与关键字class
声明相似。两者之间的唯一区别是,使用关键字struct
声明的类成员默认具有public
访问权限,而使用该关键字class
声明的类成员默认具有private
访问权限。对于所有其他目的,两个关键字在此上下文中是等效的。
关键字union
的声明与关键字struct
和class
不同,因为union
一次只存储一个数据成员,但是它们也是类,因此也可以保存成员函数。union
类中的默认访问权限是public
。