通俗的讲,类就是数据结构的扩展概念:跟数据结构一样,它们包含数据成员,但是也包含函数作为成员。
对象就是类的实例化。类似于变量的使用,类就是type,对象就是变量。
类可以使用关键字class或者struct,按照下面的语法进行定义:
class class_name {
access_specifier_1:
member1;
access_specifier_2:
member2;
...
} object_names;
在这儿,class_name是类的合法标示符,object_name是这个类的对象的名称的列表。声明体内包含成员,既可以是数据声明,或函数声明,还可以有可选的访问限制符。
类和简单数据结构具有相同的格式,例外的是它们包含函数,还有被称为访问限定符的新事物。访问限定符包含三个关键字:private,public,或protected。这些限定符修改成员的访问权限,如下所示:
1. 类的private 成员只能被同一个类中的其它成员 (或者友邻类的成员)访问.
2. 类的protected 成员只能被同一个类中的其它成员 (或者友邻类的成员)访问, 但是也可以被他们的派生类访问。
3. 最后,public成员能够在任何地方被任何可见的对象进行访问。
默认情况下,使用关键字class声明的类所有成员对它们的所有成员具有私有访问权限。因此,在任何别的访问限定符之前被声明的类成员自动具有私有访问权限。例如:
class Rectangle {
int width, height;
public:
void set_values (int,int);
int area (void);
} rect;
上面的代码,就像类型声明那样,声明了类Rectangle和这个类的对象,我们命名为rect。这个类包含了四个成员:两个int型的数据成员(分别命名为width和height),它们具有私有访问权限(因为private是默认的访问级别);还有两个具有public访问权限的函数成员,分别命名为set_values()和area(),到目前为止,我们只实现了声明,还没有实现它们的定义。
注意类名和对象名称的区别:在前面的例子里,Rectangle是类名(就像type类型名),同时,rect是类型Rectangle的对象名。它们和下面的声明语句具有相同的关系:
int a;
在这里,int是类型名,相当于类名;a是变量名称(相当于上面的对象)。
声明Rectangle和rect之后,对象rect里的任何公共成员都可以通过在对象名和成员名之间插入“.“作用符实现访问,仿佛它们就是正常的函数或正常的变量。例如:
rect.set_values (3,4);
myarea = rect.area();
但是,成员变量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;
}
这个例子重新定义了范围作用符(::),在前面介绍命名空间的时候出现过。在这里,它被用来实现函数set_values的定义,定义一个类之外的类成员。
而函数成员area()的定义,直接在类Rectangle中给出的,实现也是非常简单的。相反地,set_values仅仅在类中声明了它的原型,但是它的定义是在类外面实现的。作用域操作符(::)被用来实现上面的这种定义。
作用域操作符(::)指定了定义的成员属于哪个类,其产生的作用就像函数的定义是在类内完成的一样。例如,函数set_values能够访问变量width和height,尽管它们是类Rectangle的私有成员。
这两种定义函数的方法的不同之处在于:在类内直接实现其定义,编译器在编译过程过程中把该函数考虑为inline内嵌函数类型,而在类外定义函数,就是定义了一个非内嵌的正常函数。这在行为上没有什么差别,仅仅是编译器的优化选项上有差别。
成员width和height具有私有访问权限(记住,如果没有特别指定,类中所有成员都具有私有访问权限)。通过声明为private,阻止类外访问该成员。这是非常有意义的,因为我们已经定义了一个函数set_values去访问这些成员。因此,程序的其余部分不需要访问它们。或许,这么一个简单的例子,很难看出严格限制访问变量有什么用,在更大的工程中,当不想意外的方式修改变量时,这种方法就变得非常有意义了(这里的未期望是以这个对象的视角来说的)。
类最重要的属性就是它是类型,正因为这样,我们可以声明它的多个对象。例如:
// 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;
}
在上面的例子中,class是Rectangle,有两个它的实例:rect 和 rectb。但是它们分别具有自己的变量和函数。类使得编程具有面向对象的思想:数据和函数都是对象的成员,减少传递和携带函数句柄和其它状态变量给函数作为参数。
构造函数
在上面的例子中,如果我们在调用set_values之前调用area函数会发生什么呢?会发生未料的结果,因为变量width和height还没有赋值。为了避免这种情况的发生,class引入了构造函数的概念,构造函数实现类的对象在创建时自动调用构造函数,初始化成员变量或者分配内存。
构造函数的声明就像普通类成员函数一样,但是有一个和class相同的名称,且没有任何返回类型,即使void。
请看下面的例子:
// 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了,构造函数实现了它的功能。
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
在上面的例子中,构造了class Rectangle的两个对象:rect和rectb。rect使用两个参数构造。
但是,上面的例子中引入了一种特殊类型的构造函数:默认构造函数。默认构造函数是没有参数的构造函数,它是特殊的,因为它在对象被声明但是还没有初始化时被调用。在上面的例子里,rectb调用了默认构造函数。注意,上面rectb在被构造时没有使用空括号-事实上,空括号不会调用默认构造函数:
Rectangle rectb; // 调用默认构造函数
Rectangle rectc(); // oops, 不调用默认构造函数
这是因为空括号使rectc成为一个函数声明而不是对象声明:它将是一个没有任何参数,且返回类型Rectangle值的函数。
统一初始化
如上所示,通过函数传递参数调用构造函数的方式称为函数形式。但是,构造函数调用也可以通过其它语法的形式。
首先,单参数的构造函数可以通过变量初始化的语法进行调用(等号后面紧跟参数):
类名称 对象名称 = 初始化值;
近来,C++引入了使用统一初始化调用构造函数的方式,本质上与函数参数列表方式是一样的,但是使用大括号{}代替括号:
类名称 对象名称 { 值, 值, 值, … };
可选的,最新的语法也可以在大括号前包含“=“等号。
下面是一个例子,使用了四种方法构造类的对象,这个类的构造函数使用单参数。
// 类和统一初始化
#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); // 函数形式
Circle bar = 20.0; // 赋值初始化.
Circle baz {30.0}; // 统一初始化.
Circle qux = {40.0}; // POD-like
cout << "foo's circumference: " << foo.circum() << '\n';
return 0;
}
运行结果:
foo's circumference: 62.8319
相比函数方式,使用统一初始化的优点是,不像括号,大括号不会被函数声明混淆,因而能够被用来显式的调用默认构造函数:
Rectangle rectb; // 调用默认构造函数
Rectangle rectc(); // 函数声明 (没有调用默认构造函数)
Rectangle rectd{}; // 调用默认构造函数
调用构造函数的语法选择主要是编程风格的事。最近大部分已有的代码还是主要使用函数的形式,最新风格的编程规范中建议选择统一初始化的方式,尽管因为它优先使用初始化列表而造成自己潜在的缺陷。
构造函数的成员初始化
当构造函数被用来初始化其它成员时,这些所谓的其它成员可以被直接初始化,而不用采取在它的函数体内进行任何的声明。这可以通过在构造函数体前面插入一个冒号“:“和类成员的初始化列表。例如,考虑使用下面声明方式的类:
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) { }
注意,最下面的例子,构造函数除了初始化成员外没有做任何事情,因而它有一个空的函数体。
对于基本类型的成员,上面定义构造函数的方式是没有什么太大意义的,因为它们默认情况下不用初始化,但是对于对象成员(那些类型是类的),如果它们没有在“:“后面被初始化,它们会被默认构造。
默认构造类所有的成员,可能会不方便:在某些情况下,这是一种浪费(当这些成员不得不在构造函数中被重新初始化),但是,在另外一些情况下,甚至没有默认构造函数。在这些情况下,成员应该使用成员初始化列表的方式进行初始化。例如:
// 成员初始化
#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的构造函数需要调用基类的构造函数,且这种实现的唯一方法就是成员初始化列表。
这些初始化也可以使用统一初始化的语法,使用大括号”{}”代替括号”()”。
Cylinder::Cylinder(double r,double h) : base{r}, height{h}{ }
类指针
对象的访问可以使用指针:一旦被声明,类就成为了一种合法的类型,所以可以被用作一个指针指向的类型。例如:
Rectangle * prect;
指向类Rectangle对象的指针。
看下面的例子:
// 类指针的例子
#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 pointed to by 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。
class和struct关键字声明的类的不同之处在于,struct声明的类的成员默认访问权限是public的,而class声明的类默认具有private访问权限的成员。
相反,union声明的类概念和struct和class声明的不同,因为共同体一次只能存储一个数据成员,然而,这些成员也可以是类,这些类可以保存成员函数。union类的默认访问权限是public。