类 (I)
类是数据结构的扩展概念:它不仅可以保存数据,还可以保存函数。
对象是类的实例化。就变量而言,类是类型,对象是变量。
类通常使用关键字声明
类,格式如下
class class_name {
access_specifier_1:
member1;
access_specifier_2:
member2;
...
} object_names;
其中
class_name是类的有效标识符,
object_names是此类的对象的可选名称列表。 声明的主体可以包含成员,成员可以是数据或函数声明,以及可选的访问说明符。
所有这些都与数据结构的声明非常相似,只不过我们现在还可以包含函数和成员,以及一个名为
访问说明符的新事物。 访问说明符是以下三个关键字之一
private,
public或者
protected。 这些说明符修改了其后成员获得的访问权限
- private类的成员只能从同一类的其他成员或其友元中访问。
- protected成员可以从其同一类的成员及其友元访问,也可以从其派生类的成员访问。
- 最后,public成员可以从对象可见的任何位置访问。
默认情况下,使用
类关键字声明的类的所有成员都对其所有成员具有私有访问权限。 因此,在另一个类说明符之前声明的任何成员都会自动具有私有访问权限。 例如
1 2 3 4 5 6
|
class CRectangle {
int x, y;
public:
void set_values (int,int);
int area (void);
} rect;
|
声明一个名为
CRectangle的类(即,一种类型)和一个名为
rect的此类的对象(即,一个变量)。 此类包含四个成员:两个类型为
int的数据成员(成员
x和成员
y),具有私有访问权限(因为 private 是默认访问级别),以及两个具有公共访问权限的成员函数
set_values()和
area(),目前我们仅包含它们的声明,而没有包含它们的定义。
请注意类名和对象名之间的区别:在前面的示例中,
CRectangle是类名(即,类型),而
rect是
CRectangle类型的对象。 它们的关系与
int和
a在以下声明中具有的关系相同
,其中
int是类型名称(类),
a是变量名称(对象)。
在前面的
CRectangle和
rect声明之后,我们可以在程序主体中引用对象
rect的任何公共成员,就像它们是普通函数或普通变量一样,只需放置对象的名称,后跟一个点(
.),然后是成员的名称。 这与我们之前对普通数据结构所做的非常相似。 例如
1 2
|
rect.set_values (3,4);
myarea = rect.area();
|
我们无法从类外部的程序主体访问 rect 的唯一成员是
x和
y,因为它们具有私有访问权限,并且只能从同一类的其他成员中引用它们。
这是类 CRectangle 的完整示例
|
// classes example
#include <iostream>
using namespace std;
class CRectangle {
int x, y;
public:
void set_values (int,int);
int area () {return (x*y);}
};
void CRectangle::set_values (int a, int b) {
x = a;
y = b;
}
int main () {
CRectangle rect;
rect.set_values (3,4);
cout << "area: " << rect.area();
return 0;
}
|
area: 12 |
此代码中最重要的新的内容是作用域运算符(
::,两个冒号),包含在
set_values()的定义中。 它用于从类定义本身之外定义类的成员。
您可能会注意到,成员函数
area()的定义已直接包含在
CRectangle类的定义中,因为它非常简单,而
set_values()仅在类中声明了其原型,但其定义在类之外。 在此外部定义中,我们必须使用作用域运算符(
::)来指定我们正在定义一个函数,该函数是类
CRectangle的成员,而不是常规的全局函数。
作用域运算符(
::)指定声明的成员所属的类,从而授予与此函数定义直接包含在类定义中完全相同的作用域属性。 例如,在前一个代码的函数
set_values()中,我们能够使用变量
x和
y,它们是类
CRectangle的私有成员,这意味着它们只能从其类的其他成员访问。
在类中完全定义一个类成员函数,还是仅包含原型并在以后包含其定义之间的唯一区别是,在第一种情况下,该函数将自动被编译器视为内联成员函数,而在第二种情况下,它将是一个普通的(非内联)类成员函数,实际上这不会在行为上产生任何差异。
成员
x和
y具有私有访问权限(请记住,如果没有其他说明,则使用关键字 class 定义的类的所有成员都具有私有访问权限)。 通过将它们声明为私有,我们拒绝从类外部的任何位置访问它们。 这是有道理的,因为我们已经定义了一个成员函数来为对象内的这些成员设置值:成员函数
set_values()。 因此,程序的其余部分不需要直接访问它们。 也许在这样一个简单的例子中,很难看到保护这两个变量的任何效用,但是在更大的项目中,重要的是不能以意外的方式(从对象的角度来看是意外的)修改值。
类的一大优势是,与任何其他类型一样,我们可以声明它的多个对象。 例如,继续前面的类
CRectangle的示例,除了对象
rectb之外,我们还可以声明对象
rect:
|
// example: one class, two objects
#include <iostream>
using namespace std;
class CRectangle {
int x, y;
public:
void set_values (int,int);
int area () {return (x*y);}
};
void CRectangle::set_values (int a, int b) {
x = a;
y = b;
}
int main () {
CRectangle 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 |
。 在这种具体情况下,我们正在谈论的类(对象的类型)是
CRectangle,它有两个实例或对象
rect和
rectb。 它们中的每一个都有自己的成员变量和成员函数。
请注意,对
rect.area()的调用与对
rectb.area()的调用给出的结果不同。 这是因为 CRectangle 类的每个对象都有自己的变量
x和
y,就像它们在某种程度上也有自己的函数成员
set_value()和
area(),每个函数都使用其对象自己的变量进行操作。
这就是
面向对象编程的基本概念:数据和函数都是对象的成员。 我们不再使用作为参数从一个函数传递到另一个函数的全局变量集,而是处理具有嵌入为其成员的自有数据和函数的对象。 请注意,我们不必在对
rect.area或者
rectb.area的任何调用中提供任何参数。 这些成员函数直接使用其各自对象的数据成员
rect和
rectb.
构造函数和析构函数
对象通常需要在创建过程中初始化变量或分配动态内存,才能变得可操作并避免在执行过程中返回意外的值。 例如,如果在之前的示例中,我们在调用函数
area()之前调用成员函数
set_values()会发生什么? 可能会得到一个不确定的结果,因为成员
x和
y将永远不会被赋值。
为了避免这种情况,类可以包含一个特殊的函数,称为
构造函数,每当创建该类的新对象时,都会自动调用该函数。 此构造函数函数必须与类具有相同的名称,并且不能具有任何返回类型; 甚至不能是
void.
我们将要实现
CRectangle,包括一个构造函数
|
// example: class constructor
#include <iostream>
using namespace std;
class CRectangle {
int width, height;
public:
CRectangle (int,int);
int area () {return (width*height);}
};
CRectangle::CRectangle (int a, int b) {
width = a;
height = b;
}
int main () {
CRectangle rect (3,4);
CRectangle 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(),而是包含了一个构造函数,该构造函数执行类似的操作:它使用传递给它的参数来初始化
宽度和
height的值。
请注意,在创建该类的对象时,这些参数是如何传递给构造函数的
1 2
|
CRectangle rect (3,4);
CRectangle rectb (5,6);
|
不能像常规成员函数一样显式调用构造函数。 它们仅在创建该类的新对象时执行。
您还可以看到,构造函数原型声明(在类中)和后面的构造函数定义都没有包括返回值; 甚至没有
void.
析构函数实现了相反的功能。 当对象被销毁时,它会自动被调用,或者是因为它的存在范围已经结束(例如,如果它被定义为函数中的局部对象并且函数结束了),或者是因为它是一个动态分配的对象,并且使用运算符 delete 释放了它。
析构函数必须与类具有相同的名称,但在前面有一个波浪线符号(
~),并且它也必须不返回值。
当对象在其生命周期内分配动态内存,并且在销毁时我们想要释放对象分配的内存时,析构函数的使用尤其适合。
|
// example on constructors and destructors
#include <iostream>
using namespace std;
class CRectangle {
int *width, *height;
public:
CRectangle (int,int);
~CRectangle ();
int area () {return (*width * *height);}
};
CRectangle::CRectangle (int a, int b) {
width = new int;
height = new int;
*width = a;
*height = b;
}
CRectangle::~CRectangle () {
delete width;
delete height;
}
int main () {
CRectangle rect (3,4), rectb (5,6);
cout << "rect area: " << rect.area() << endl;
cout << "rectb area: " << rectb.area() << endl;
return 0;
}
|
rect area: 12
rectb area: 30 |
重载构造函数
与任何其他函数一样,构造函数也可以重载为具有相同名称但类型或参数数量不同的多个函数。 请记住,对于重载函数,编译器将调用其参数与函数调用中使用的参数匹配的函数。 对于在创建对象时自动调用的构造函数,执行的构造函数是与对象声明中传递的参数匹配的构造函数
|
// overloading class constructors
#include <iostream>
using namespace std;
class CRectangle {
int width, height;
public:
CRectangle ();
CRectangle (int,int);
int area (void) {return (width*height);}
};
CRectangle::CRectangle () {
width = 5;
height = 5;
}
CRectangle::CRectangle (int a, int b) {
width = a;
height = b;
}
int main () {
CRectangle rect (3,4);
CRectangle rectb;
cout << "rect area: " << rect.area() << endl;
cout << "rectb area: " << rectb.area() << endl;
return 0;
}
|
rect area: 12
rectb area: 25 |
在这种情况下,
rectb声明时没有任何参数,因此它已使用没有参数的构造函数进行初始化,该构造函数将
宽度和
height都初始化为值 5。
重要提示:请注意,如果我们声明一个新对象并且想要使用其默认构造函数(没有参数的构造函数),我们不要包含括号
():
1 2
|
CRectangle rectb; // right
CRectangle rectb(); // wrong!
|
默认构造函数
如果您未在类定义中声明任何构造函数,则编译器假定该类具有一个没有参数的默认构造函数。 因此,在声明了像这样的类之后
1 2 3 4 5
|
class CExample {
public:
int a,b,c;
void multiply (int n, int m) { a=n; b=m; c=a*b; }
};
|
编译器假设
CExample具有默认构造函数,因此您可以通过简单地声明它们而不带任何参数来声明此类的对象
但是,只要您为类声明了自己的构造函数,编译器就不再提供隐式默认构造函数。 因此,您必须根据您为类定义的构造函数原型来声明该类的所有对象
1 2 3 4 5 6
|
class CExample {
public:
int a,b,c;
CExample (int n, int m) { a=n; b=m; };
void multiply () { c=a*b; };
};
|
在这里,我们声明了一个接受两个 int 类型参数的构造函数。 因此,以下对象声明将是正确的
但是,
将
不正确,因为我们已声明该类具有显式构造函数,从而替换了默认构造函数。
但是,如果您没有指定自己的构造函数,则编译器不仅会为您创建一个默认构造函数。 它总共提供了三个特殊的成员函数,如果您没有声明自己的构造函数,则会隐式声明这些函数。 这些是
复制构造函数、
复制赋值运算符和默认析构函数。
复制构造函数和复制赋值运算符将另一个对象中包含的所有数据复制到当前对象的数据成员中。 对于
CExample,编译器隐式声明的复制构造函数将类似于
1 2 3
|
CExample::CExample (const CExample& rv) {
a=rv.a; b=rv.b; c=rv.c;
}
|
因此,以下两个对象声明将是正确的
1 2
|
CExample ex (2,3);
CExample ex2 (ex); // copy constructor (data copied from ex)
|
指向类的指针
创建指向类的指针是完全有效的。 我们只需考虑一旦声明,一个类就变成了一个有效的类型,因此我们可以使用类名作为指针的类型。 例如
是指向类
CRectangle.
的对象的指针。 与数据结构的情况一样,为了直接引用指针指向的对象的成员,我们可以使用间接寻址的箭头运算符(
->)。 这是一个包含一些可能组合的示例
|
// pointer to classes example
#include <iostream>
using namespace std;
class CRectangle {
int width, height;
public:
void set_values (int, int);
int area (void) {return (width * height);}
};
void CRectangle::set_values (int a, int b) {
width = a;
height = b;
}
int main () {
CRectangle a, *b, *c;
CRectangle * d = new CRectangle[2];
b= new CRectangle;
c= &a;
a.set_values (1,2);
b->set_values (3,4);
d->set_values (5,6);
d[1].set_values (7,8);
cout << "a area: " << a.area() << endl;
cout << "*b area: " << b->area() << endl;
cout << "*c area: " << c->area() << endl;
cout << "d[0] area: " << d[0].area() << endl;
cout << "d[1] area: " << d[1].area() << endl;
delete[] d;
delete b;
return 0;
}
|
a area: 2
*b area: 12
*c area: 2
d[0] area: 30
d[1] area: 56 |
接下来,您将获得一个关于如何读取先前示例中出现的一些指针和类运算符(
*,
&,
.,
->,
[ ])的摘要
表达式 | 可以读作 |
*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) 个对象 |
在继续下一节之前,请务必了解所有这些表达式下的逻辑。 如果您有疑问,请再次阅读本节和/或查阅有关指针和数据结构的前几节。
使用 struct 和 union 定义的类
类不仅可以使用关键字
类定义,还可以使用关键字
struct和
联合体.
类和数据结构的概念非常相似,因此这两个关键字(
struct和
类)都可以在 C++ 中用于声明类(即,
structs 也可以在 C++ 中具有函数成员,而不仅仅是数据成员)。 两者之间的唯一区别是,使用关键字
struct声明的类的成员默认具有公共访问权限,而使用关键字
类声明的类的成员具有私有访问权限。 对于所有其他目的,这两个关键字都是等效的。
联合的概念与使用
struct和
类声明的类的概念不同,因为联合一次只存储一个数据成员,但尽管如此,它们也是类,因此也可以容纳函数成员。 联合类中的默认访问权限是公共的。