类是结构体的扩展概念:与结构体一样,它们可以包含数据成员,但也可以包含作为成员的函数。
对象是类的实例化。拿变量举例,类就是类型,对象就是变量。变量是类型的实例,对象是类的实例.
类是用关键字class或关键字struct定义的,语法如下:
class class_name { access_specifier_1: member1; access_specifier_2: member2; ... } object_names; |
其中class_name是类的有效标识符,object_names是该类对象的可选名称列表。 声明体可以包含成员(可以是数据声明或函数声明)和可选的访问修饰符。
类具有与普通结构体相同的格式,但是它也可以包含函数和这些称为访问修饰符的新东西。 访问修饰符是以下三个关键字之一:private、public或protected。这些修饰符限定它们后面成员的访问权限:
默认情况下,用class关键字声明的类的任一成员都对其它成员具有私有访问权。 因此,在任何访问修饰符之前声明的成员都自动具有私有访问权。例如:
class Rectangle { int width, height; public: void set_values (int,int); int area (void); } rect; |
声明一个类(即,一个类型) Rectangle和一个对象,(即,一个变量)这个类,叫做rect。 这个类包含四个成员:两个 private修饰的int类型的数据成员(width 和height),(因为private是默认的访问级别) 和两个public 修饰的成员函数:函数set_values和area,目前我们只有它们的声明,而没有它们的定义。
注意类名和对象名之间的区别:在上面的例子中,Rectangle是类名(即类型), 而rect是Rectangle类型的对象。它与下面声明中的int和 a 关系相同:
int a;其中int是类型名(类),a是变量名(对象)。
在声明了Rectangle和rect之后,可以像访问普通函数或普通变量一样访问对象的任何公共成员, 只需在对象名称和成员名称之间插入一个点(.)即可。这与访问普通数据结构的成员遵循相同的语法。
例如: rect.set_values (3,4); myarea = rect.area(); |
rect中唯不能从类外部访问的成员是width和height,因为它们具有私有访问权限, 只能从同一类的其他成员中引用。
下面是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只是在类中声明它的原型,但它的定义在类之外。 在这个外部定义中,scope(::)的操作符用于指定被定义的函数是Rectangle类的成员, 而不是普通的非成员函数。
作用域操作符(::)指定被定义的成员所属的类, 授予其与直接在类中定义的函数完全相同的作用域属性。 例如,前一个例子中的函数set_values可以访问变量width和height, width和height是Rectangle类的私有成员,因此只能从该类的其他成员中访问,比如set_values。
在类定义中定义成员函数和在类之外定义成员函数之间的唯一区别是,在第一种情况下, 编译器自动认为该函数是内联成员函数,而在第二种函数中, 它是一个普通的(非内联的)类成员函数。这不会导致行为上的差异,但只会导致可能的编译器优化。
成员width和height具有私有访问权限(请记住,如果没有访问修饰符, 则用关键字class定义的类的所有成员都具有 private私有访问权限)。 通过将它们声明为private,就不允许来自类外部的访问。 这是有意义的,因为我们已经定义了一个成员函数 set_values 来为对象内的成员设置值。 因此,程序的其余部分不能直接访问它们,而是通过set_values访问。也许在这样一个简单的例子中, 很难看出限制对这些变量的访问有什么用,但是在更大的项目中, 以可控的方式(从对象的角度来看是意外的)修改值是非常重要的。
类最重要的属性是它是一种类型,因此,我们可以声明它的多个对象。 例如,在前面的Rectangle类示例之后,我们可以声明对象rectb,而不是对象rect:
// 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,它有两个实例(即对象):rect和rectb。 每一个都有自己的成员变量和成员函数。
请注意,调用rect.area()并不会得到与调用rectb.area()相同的结果。 这是因为Rectangle类的每个对象都有自己的width 和height变量, 它们也有自己的函数成员set_value和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; } |
rect area: 12 rectb area: 30 |
这个例子的结果与前一个例子的结果相同。但是现在,矩形类没有成员函数set_values, 而是有一个构造函数来执行类似的操作:它用传递给它的参数初始化width和height的值。
请注意,在创建该类的对象时,这些参数是如何传递给构造函数的:
Rectangle rect (3,4); Rectangle rectb (5,6); |
不能像常规成员函数那样显式地调用构造函数。它们只在创建该类的新对象时执行一次。
注意构造函数原型声明(在类中)和后一个构造函数定义都没有返回值; 构造函数从不返回值,它们只是初始化对象。
与任何其他函数一样,构造函数也可以重载不同版本的形参:形参数量和/或形参类型不同。 编译器将自动调用形参与实参匹配的函数:
// 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类的两个对象: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; } |
foo's circumference: 62.8319 |
与函数形式相比,统一初始化的一个优点是: 与圆括号不同,大括号不会与函数声明混淆,因此可以用于显式调用默认构造函数:
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; } |
foo's volume: 6283.19 |
在这个例子中,类Cylinder 有一个成员对象,它的类型是另一个类((base 类型是Circle)。 因为Circle类的对象只能用形参来构造,所以Cylinder的构造函数需要调用base的构造函数, 而唯一的方法是在成员初始化列表中。
这些初始化也可以使用统一初始化语法,使用大括号{}代替圆括号():
Cylinder::Cylinder (double r, double h) : base{r}, height{h} { } |
对象也可以由指针指向:一旦声明,类就成为有效类型,因此它可以作为指针所指向的类型使用。例如:
Rectangle * prect; |
与普通数据结构类似,对象的成员可以通过使用箭头操作符(->)从指针直接访问。 下面是一个组合的例子:
// pointer to classes example #include // 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; }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指向的值 |
&x | x的地址 |
x.y | 对象x的成员y |
x->y | x指向的对象的元素y |
(*x).y | x所指向的对象的成员y(与前一个等价) |
x[0] | x指向的第一个对象 |
x[1] | 由x指向的第二个对象 |
x[n] | x指向的第(n+1)个对象 |
这些表达大部分已经在前面的章节中介绍过了。最值得注意的是, 关于数组的那一章介绍了偏移操作符([]), 关于普通数据结构的那一章介绍了箭头操作符(->)。
类不仅可以用关键字class定义,还可以用关键字struct和union来定义。
关键字struct通常用于声明纯数据结构,也可以用于声明具有成员函数的类, 其语法与关键字class相同。两者之间唯一的区别是,默认情况下, 使用关键字struct声明的类的成员具有公共访问权, 而使用关键字class声明的类的成员具有私有访问权。 这两个关键字在这个语境中是等价的。
相反,联合的概念不同于用struct和class声明的类, 因为联合一次只存储一个数据成员,但它们也是类, 因此也可以保存成员函数。联合类的默认访问是公共的。