类的介绍
在C++中, 用 "类" 来描述 "对象", 所谓的"对象"是指现实世界中的一切事物。那么类就可以看做是对相似事物的抽象, 找到这些不同事物间的共同点, 如自行车和摩托车, 首先他们都属于"对象", 并且具有一定得相同点, 和一些不同点, 相同点如他们都有质量、都有两个轮子, 都是属于交通工具等。"都有质量"、"两个轮子"属于这个对象的属性, 而"都能够当做交通工具"属于该对象具有的行为, 也称方法。
类是属于用户自定义的数据类型, 并且该类型的数据具有一定的行为能力, 也就是类中说描述的方法。通常来说, 一个类的定义包含两部分的内容, 一是该类的属性, 另一部分是它所拥有的方法。以 "人类" 这个类来说, 每个人都有自己的姓名、年龄、出生日期、体重等, 为 人类 的属性部分, 此外, 人能够吃饭、睡觉、行走、说话等属于人类所具有的行为。
上面举例中所描述的 "人" 类仅仅是具有人这种对象的最基础的一些属性和行为, 可以称之为人的"基类"。 再说说一些具有一些职业的人, 例如学生, 一个学生还具有"基类"中所没有的属性, 如学校、班级、学号; 也可以具有基类所不具有的行为, 如每天需要去上课, 需要考试等。
学生类可以看做是基类的一个扩展, 因为他具有基类的所有属性和行为, 并且在此基础上增加了一些基类所没有的属性和行为, 像"学生"这样的类称为"人类"这个基类的"派生类"或者"子类"。在学生的基础上海可以进一步的扩展出其他更高级的类, 如"研究生"类。
类的定义
类只是一个模板(Template),编译后不占用内存空间,所以在定义类时不能对成员变量进行初始化,因为没有地方存储数据。只有在创建对象以后才会给成员变量分配内存,这个时候就可以赋值了。
C++中使用关键字 class 来定义类, 其基本形式如下:
class 类名
{
public:
//公共的行为或属性
private:
//公共的行为或属性
};
说明:
- 类名 需要遵循一般的命名规则;
- public 与 private 为属性/方法限制的关键字, private 表示该部分内容是私密的, 不能被外部所访问或调用, 只能被本类内部访问; 而 public 表示公开的属性和方法, 外界可以直接访问或者调用。 一般来说类的属性成员都应设置为private, public只留给那些被外界用来调用的函数接口, 但这并非是强制规定, 可以根据需要进行调整;
- 结束部分的分号不能省略。
类定义示例:
定义一个点(Point)类, 具有以下属性和方法:
■ 属性: x坐标, y坐标
■ 方法: 1.设置x,y的坐标值; 2.输出坐标的信息。
实现代码如下:
class Point
{
public:
void setPoint(int x, int y);
void printPoint();
private:
int xPos;
int yPos;
};
代码说明:
上段代码中定义了一个名为 Point 的类, 具有两个私密属性, int型的xPos和yPos, 分别用来表示x点和y点。在方法上, setPoint 用来设置属性, 也就是 xPos 和 yPos 的值; printPoint 用来输出点的信息。
类在定义时有以下几点需要注意:
- 类的数据成员中不能使用 auto、extern和register等进行修饰, 也不能在定义时进行初始化, 如 int xPos = 0; //错;
- 类定义时 private 和 public 关键词出现的顺序和次数可以是任意的;
- 结束时的分号不能省略, 切记!
C++类的实现
在上面的定义示例中我们只是定义了这个类的一些属性和方法声明, 并没有去实现它, 类的实现就是完成其方法的过程。类的实现有两种方式, 一种是在类定义时完成对成员函数的定义, 另一种是在类定义的外部进行完成。
- 在类定义时定义成员函数
成员函数的实现可以在类定义时同时完成, 如代码:
#include <iostream>
using namespace std;
class Point
{
public:
void setPoint(int x, int y) //实现setPoint函数
{
xPos = x;
yPos = y;
}
void printPoint() //实现printPoint函数
{
cout<< "x = " << xPos << endl;
cout<< "y = " << yPos << endl;
}
private:
int xPos;
int yPos;
};
int main()
{
Point M; //用定义好的类创建一个对象 点M
M.setPoint(10, 20); //设置 M点 的x,y值
M.printPoint(); //输出 M点 的信息
return 0;
}
运行输出;
x = 10
y = 20
与类的定义相比, 在类内实现成员函数不再是在类内进行声明, 而是直接将函数进行定义, 在类中定义成员函数时, 编译器默认会争取将其定义为inline 型函数。
- 在类外定义成员函数
在类外定义成员函数通过在类内进行声明, 然后在类外通过作用域操作符 :: 进行实现, 形式如下:
返回类型 类名::成员函数名(参数列表)
{
//函数体
}
将示例中的代码改用类外定义成员函数的代码:
#include <iostream>
using namespace std;
class Point
{
public:
void setPoint(int x, int y); //在类内对成员函数进行声明
void printPoint();
private:
int xPos;
int yPos;
};
void Point::setPoint(int x, int y) //通过作用域操作符 '::' 实现setPoint函数
{
xPos = x;
yPos = y;
}
void Point::printPoint() //实现printPoint函数
{
cout<< "x = " << xPos << endl;
cout<< "y = " << yPos << endl;
}
int main()
{
Point M; //用定义好的类创建一个对象 点M
M.setPoint(10, 20); //设置 M点 的x,y值
M.printPoint(); //输出 M点 的信息
return 0;
}
依 setPoint 成员函数来说, 在类内声明的形式为 void setPoint(int x, int y); 那么在类外对其定义时函数头就应该是 void Point::setPoint(int x, int y) 这种形式, 其返回类型、成员函数名、参数列表都要与类内声明的形式一致。
C++类的使用
将一个类定义并实现后, 就可以用该类来创建对象了, 创建的过程如同 int、char 等基本数据类型声明一个变量一样简单, 例如我们有一个Point类, 要创建一个Point的对象只需要:
point 对象名;
创建一个类的对象称为该类的实例化, 在创建时我们还可以对对象的属性进行相关的初始化, 这样在创建完成后该对象就已经具有了一定得属性, 这种创建方式将在下一篇博文中进行学习。
将类进行实例化后系统才会根据该对象的实际需要分配一定的存储空间。这样就可以使用该对象来访问或调用该对象所能提供的属性或方法了。
还以上面的代码为例, 为了减少篇幅, 我们把 Point 类的实现放在 Point.h 头文件中, 这里不再贴出 Point 类的实现代码。
#include <iostream>
#include "Point.h"
using namespace std;
int main()
{
Point M; //用定义好的类创建一个对象 点M
M.setPoint(10, 20); //设置 M点 的x,y值
M.printPoint(); //输出 M点 的信息
cout<< M.xPos <<endl; //尝试通过对象M访问属性xPos
return 0;
}
代码在编译时会出现错误, 提示 error: 'int Point::xPos' is private, 这是 cout<< M.xPos <<endl; 这行造成的, 他试图访问一个 private 对象中的私密数据 xPos, 如果将这行去掉便可正常运行。
通过 对象名.公有函数名(参数列表); 的形式就可以调用该类对象所具有的方法, 通过对象名.公有数据成员;的形式可以访问对象中的数据成员。
- 使用对象指针调用成员函数
C语言中经典的指针在 C++ 中仍然广泛使用,尤其是指向对象的指针,没有它就不能实现某些功能。
上面代码中创建的对象 stu 在栈上分配内存,需要使用&
获取它的地址,例如:
Student stu;
Student *pStu = &stu;
pStu 是一个指针,它指向 Student 类型的数据,也就是通过 Student 创建出来的对象。
当然,你也可以在堆上创建对象,这个时候就需要使用前面讲到的new
关键字,例如:
Student *pStu = new Student;
在栈上创建出来的对象都有一个名字,比如 stu,使用指针指向它不是必须的。但是通过 new 创建出来的对象就不一样了,它在堆上分配内存,没有名字,只能得到一个指向它的指针,所以必须使用一个指针变量来接收这个指针,否则以后再也无法找到这个对象了,更没有办法使用它。也就是说,使用 new 在堆上创建出来的对象是匿名的,没法直接使用,必须要用一个指针指向它,再借助指针来访问它的成员变量或成员函数。
栈内存是程序自动管理的,不能使用 delete 删除在栈上创建的对象;堆内存由程序员管理,对象使用完毕后可以通过 delete 删除。在实际开发中,new 和 delete 往往成对出现,以保证及时删除不再使用的对象,防止无用内存堆积。
有了对象指针后,可以通过箭头->
来访问对象的成员变量和成员函数,这和通过结构体指针来访问它的成员类似,请看下面的示例:
pStu -> name = "小明";
pStu -> age = 15;
pStu -> score = 92.5f;
pStu -> say();
请看一个完整的例子:
#include <iostream>
using namespace std;
class Student{
public:
char *name;
int age;
float score;
void say(){
cout<<name<<"的年龄是"<<age<<",成绩是"<<score<<endl;
}
};
int main(){
Student *pStu = new Student;
pStu -> name = "小明";
pStu -> age = 15;
pStu -> score = 92.5f;
pStu -> say();
delete pStu; //删除对象
return 0;
}
运行结果:
小明的年龄是15,成绩是92.5
虽然在一般的程序中无视垃圾内存影响不大,但记得 delete 掉不再使用的对象依然是一种良好的编程习惯。
总结:
通过对象名字访问成员使用点号.,通过对象指针访问成员使用箭头->,这和结构体非常类似。
对象的作用域、可见域与生存周期
类对象的作用域、可见域以及生存周期与普通变量的保持相同, 当对象生存周期结束时对象被自动撤销, 所占用的内存被回收, 需要注意的是, 如果对象的成员函数中有使用 new 或者 malloc 申请的动态内存程序不会对其进行释放, 需要我们手动进行清理, 否则会造成内存泄露。
- 类的作用域
一个类的所以成员位于这个类的作用域内,一个类的任何成员都能访问同一类的任何一个其他成员,C++认为一个类的全部成员都是一个整体的相关部分。
类作用域是指类定义和相应的成员函数定义范围。在该范围内,一个类的成员函数对同一类的数据成员具有无限制的访问权。
对类作用域外的一个数据成员和成员函数的访问受到程序员的控制。这种思想是要把一个类的数据结构和功能封装起来,从而使得在类的成员函数之外的对类的数据进行访问是有限的。
- 可见性
- 如果一个非类型名隐藏了类型名,则类型名通过家前缀即可。
例如,一个类名被在后面的函数中的形参所覆盖,在该函数内,要定义一个类对象,则加上class即可:
class Sample //定义类
{
...
};
void func(int Sample) //函数形参隐藏了类名
{
class Sample a; //定义一个对象要用到类名
Sample++; //形参的算术操作
//...
}
- 如果一个类型名隐藏了一个非类型名,则用一般作用域规则即可。
例如:
int s= 0; //全局变量
void func()
{
class s{//...}; //类s隐藏了全局变量s
s a; //定义一个类对象
::s=3; //引用全局变量
} //class s作用域结束
int g=s; //用全局变量s给变量g初始化
在函数中定义的类称为局部类,局部类在面向对象程序设计中并不多见。类作为类型也有其作用域,局部类的作用在定义该类的函数块中。
局部类的成员函数必须在类定义内部定义,因为若在类外部的包含该类的函数内部定义,则导致在函数内部定义函数的矛盾。若在包含类的函数外定义,则局部类无法与其取得联系。
C++规定:
- 一个名字不能同时指两种类型。
- 非类型名(变量名,常量名,函数名,对象名或枚举成员)不能重名。
- 类型和非类型不在同一空间。也即在一个作用域中,一个名字可以声明为一个类型,又可声明为一个非类型。当两者同时登场时,类型名要加前缀,以区别非类型名。
- 类的封装
封装的思想就是将实现的细节隐藏,而暴露公有接口;C++中的访问标识符,可以实现在类中的封装;通常是将所有的成员变量私有化;尽管看起来访问成员变量的不直接,但使程序更有可重用性和可维护性
- 封装实现,无论类的实现如何改变,只要对外的接口不发生变化即可。
- 隐藏了类的实现,类的使用者只需知道公共的接口,就可以使用该类;
- 封装帮助防止意外的改变和误用;
- 对程序调试有很大的帮助,因为改变类的成员变量只用通过公共接口。