类和对象是 C++的重要特性,它们使得 C++ 成为面向对象的编程语言,可以用来开发中大型项目,本节重点讲解类和对象的语法。
类是创建对象的模板,一个类可以创建多个对象,每个对象都是类类型的一个变量;创建对象的过程也叫类的实例化。每个对象都是类的一个具体实例,拥有类的成员变量和成员函数。
有些教程将类的成员变量称为类的属性(Property),将类的成员函数称为类的方法(Method)。在面向对象的编程语言中,经常把函数(Function)称为方法(Method)。
与结构体一样,类只是一种复杂数据类型的声明,不占用内存空间。而对象是类这种数据类型的一个变量,或者说是通过类这种数据类型创建出来的一份实实在在的数据,所以占用内存空间。
类是用户自定义的类型,如果程序中要用到类,必须提前说明,或者使用已存在的类(别人写好的类、标准库中的类等),C++语法本身并不提供现成的类的名称、结构和内容。
在此之前我们或多或少的接触过这些方面的知识,直接看一个例子:
class Student{
public:
//成员变量
char *name;
int age;
float score;
//成员函数
void say(){
cout<
class
是 C++ 中新增的关键字,专门用来定义类。Student
是类的名称;类名的首字母一般大写,以和其他的标识符区分开。{ }
内部是类所包含的成员变量和成员函数,它们统称为类的成员(Member);由{ }
包围起来的部分有时也称为类体,和函数体的概念类似。public
也是 C++ 的新增关键字,它只能用在类的定义中,表示类的成员变量或成员函数具有“公开”的访问权限,这个到后面类的继承中在细说。
注意在类定义的最后有一个分号
;
,它是类定义的一部分,表示类定义结束了,不能省略。
整体上讲,上面的代码创建了一个 Student 类,它包含了 3 个成员变量和 1 个成员函数。
类只是一个模板(Template),编译后不占用内存空间,所以在定义类时不能对成员变量进行初始化,因为没有地方存储数据。只有在创建对象以后才会给成员变量分配内存,这个时候就可以赋值了。
有了 Student 类后,就可以通过它来创建对象了,例如:
Student xqs; //创建对象
Student
是类名,xqs
是对象名。
在创建对象时,class 关键字可要可不要,但是出于习惯我们通常会省略掉 class 关键字:
class Student xqs; //正确
Student xqs; //同样正确
除了创建单个对象,还可以创建对象数组:
Student allStu[100];
该语句创建了一个allStu
数组,它拥有100个元素,每个元素都是 Student 类型的对象。
创建对象以后,可以使用点号.
来访问成员变量和成员函数,这和通过结构体变量来访问它的成员类似,如下所示:
//创建对象
Student stu;
stu.name = "小明";
stu.age = 15;
stu.score = 98.5f;
stu.say();
C语言中经典的指针在 C++ 中仍然广泛使用,尤其是指向对象的指针,没有它就不能实现某些功能。
上面代码中创建的对象 stu
在栈上分配内存,需要使用&
获取它的地址,例如:
Student stu;
Student *pStu = &stu;
pStu
是一个指针,它指向 Student 类型的数据,也就是通过 Student 创建出来的对象。
当然,你也可以在堆上创建对象,这个时候就需要使用前面的new
关键字,例如:
Student *pStu = new Student;
这个应该好理解,类比结构体指针。
有了对象指针后,可以通过箭头->
来访问对象的成员变量和成员函数,这和通过结构体指针来访问它的成员类似,请看下面的示例:
pStu -> name = "小明";
pStu -> age = 15;
pStu -> score = 92.5f;
pStu -> say();
第二部分介绍了两种创建对象的方式:一种是在栈上创建,形式和定义普通变量类似;另外一种是在堆上使用 new 关键字创建,必须要用一个指针指向它,要记得 delete 掉不再使用的对象。
类的成员函数也和普通函数一样,都有返回值和参数列表,它与一般函数的区别是:成员函数是一个类的成员,出现在类体中,它的作用范围由类来决定;而普通函数是独立的,作用范围是全局的,或位于某个命名空间内。
在类体中定义了成员函数,你也可以只在类体中声明函数,而将函数定义放在类体外面:
class Student{
public:
//成员变量
char *name;
int age;
float score;
//成员函数
void say(); //函数声明
};
//函数定义
void Student::say(){
cout<
在类体中直接定义函数时,不需要在函数名前面加上类名,因为函数属于哪一个类是不言而喻的。但当成员函数定义在类外时,就必须在函数名前面加上类名予以限定。::
被称为域解析符(也称作用域运算符或作用域限定符),用来连接类名和函数名,指明当前函数属于哪个类。
在类体中和类体外定义成员函数的区别:
在类体中和类体外定义成员函数是有区别的:在类体中定义的成员函数会自动成为内联函数,在类体外定义的不会。当然,在类体内部定义的函数也可以加
inline
关键字,但这是多余的,因为类体内部定义的函数默认就是内联函数。 内联函数一般不是我们所期望的,它会将函数调用处用函数体替代,所以我建议在类体内部对成员函数作声明,而在类体外部进行定义,这是一种良好的编程习惯,实际开发中大家也是这样做的。
如果你既希望将函数定义在类体外部,又希望它是内联函数,那么可以在定义函数时加 inline
关键字。
C++通过 public、protected、private 三个关键字来控制成员变量和成员函数的访问权限,它们分别表示公有的、受保护的、私有的,被称为成员访问限定符。所谓访问权限,就是你能不能使用该类中的成员。
C++ 中的 public、private、protected 只能修饰类的成员,不能修饰类,C++中的类没有共有私有之分。
在类的内部(定义类的代码内部),无论成员被声明为 public、protected 还是 private,都是可以互相访问的,没有访问权限的限制。
在类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问 public 属性的成员,不能访问 private、protected 属性的成员。
访问权限:
public 公共 |
类内可以访问 | 类外可以访问 |
---|---|---|
protected 保护 |
类内可以访问 | 类外不可以访问 |
privite 私有 |
类内可以访问 | 类外不可以访问 |
#include
using namespace std;
//类的声明
class Student{
private: //私有的
char *m_name;
int m_age;
float m_score;
public: //共有的
void setname(char *name);
void setage(int age);
void setscore(float score);
void show();
};
//成员函数的定义
void Student::setname(char *name){
m_name = name;
}
void Student::setage(int age){
m_age = age;
}
void Student::setscore(float score){
m_score = score;
}
void Student::show(){
cout< setname("李华");
pstu -> setage(16);
pstu -> setscore(96);
pstu -> show();
return 0;
}
像这里的学生类,在main函数里我们不能在通过stu.name="小米"
来直接访问它的name属性,只能通过公共的方法setname
来访问。
- 成员变量大都以
m_
开头,这是约定成俗的写法,不是语法规定的内容。以m_
开头既可以一眼看出这是成员变量,又可以和成员函数中的形参名字区分开。- 类的声明和成员函数的定义都是类定义的一部分,在实际开发中,我们通常将类的声明放在头文件中,而将成员函数的定义放在源文件中。
以 setname()
为例,如果将成员变量m_name
的名字修改为name
,那么 setname()
的形参就不能再叫name
了,得换成诸如name1
、_name
这样没有明显含义的名字,否则name=name;
这样的语句就是给形参name
赋值,而不是给成员变量name
赋值。
private 关键字的作用在于更好地隐藏类的内部实现,该向外暴露的接口(能通过对象访问的成员)都声明为 public,不希望外部知道、或者只在类内部使用的、或者对外部没有影响的成员,都建议声明为 private。
根据C++软件设计规范,实际项目开发中的成员变量以及只在类内部使用的成员函数(只被成员函数调用的成员函数)都建议声明为 private,而只将允许通过对象调用的成员函数声明为 public。
我们可以额外添加两个 public 属性的成员函数,一个用来设置成员变量的值,一个用来获取成员变量的值。上面的代码中,setname()、setage()、setscore()
函数就用来设置成员变量的值;如果希望获取成员变量的值,可以再添加三个函数 getname()、getage()、getscore()
。
给成员变量赋值的函数通常称为 set 函数,它们的名字通常以
set
开头,后跟成员变量的名字;读取成员变量的值的函数通常称为 get 函数,它们的名字通常以get
开头,后跟成员变量的名字。
这种将成员变量声明为 private、将部分成员函数声明为 public 的做法体现了类的封装性。所谓封装,是指尽量隐藏类的内部实现,只向用户提供有用的成员函数。
声明为 private 的成员和声明为 public 的成员的次序任意,既可以先出现 private 部分,也可以先出现 public 部分。如果既不写 private 也不写 public,就默认为 private。
在一个类体中,private 和 public 可以分别出现多次。每个部分的有效范围到出现另一个访问限定符或类体结束时(最后一个右花括号)为止。但是为了使程序清晰,应该养成这样的习惯,使每一种成员访问限定符在类定义体中只出现一次。
类是创建对象的模板,不占用内存空间,不存在于编译后的可执行文件中;而对象是实实在在的数据,需要内存来存储。对象被创建时会在栈区或者堆区分配内存。直观的认识是,如果创建了 10 个对象,就要分别为这 10 个对象的成员变量和成员函数分配内存,如下图所示:
不同对象的成员变量的值可能不同,需要单独分配内存来存储。但是不同对象的成员函数的代码是一样的,上面的内存模型保存了 10 份相同的代码片段,浪费了不少空间,可以将这些代码片段压缩成一份。事实上编译器也是这样做的,编译器会将成员变量和成员函数分开存储:分别为每个对象的成员变量分配内存,但是所有对象都共享同一段函数代码。如下图所示:
所以值得注意的是,如果用
sizeof()
函数来计算一个类的大小时,只计算其成员变量的大小,而不考虑其成员函数的大小,当然也存在计算机组成原理中的边界对齐问题,就业笔试题目中会遇到类似问题。
要求:
判断两个立方体相等的方法:长宽高分别对应相等。
#include
#include
//声明命名空间std
using namespace std;
class Cube {
private:
//成员变量
int m_l;
int m_w;
int m_h;
public:
//成员函数
void SetCube(int length, int width, int high) {
m_l = length;
m_w = width;
m_h = high;
}
int get_l() {
return m_l;
}
int get_w() {
return m_w;
}
int get_h() {
return m_h;
}
int S() { //计算面积
return 2 * m_l * m_w + 2 * m_l * m_h + 2 * m_w * m_h;
}
int V() { //计算体积
return m_l * m_w * m_h;
}
//利用成员函数进行判断
bool issamebyclass(Cube c) {
if (m_l == c.get_l() && m_w == c.get_w() && m_h == c.get_h())
return true;
else
return false;
}
};
//利用全局函数判断是否相等
bool issame(Cube &c1, Cube &c2) {
if (c1.get_h() == c2.get_h() && c1.get_l() == c2.get_l() && c1.get_w() == c2.get_w())
return true;
else
return false;
}
int main() {
Cube num1,num2;
num1.SetCube(3, 3, 3);
int s1 = num1.S();
int v1 = num1.V();
cout << "第一个立方体的面积为:" << s1 << ";体积为:" << v1 << endl;
num2.SetCube(5, 4, 3);
int s2 = num2.S();
int v2 = num2.V();
cout << "第二个立方体的面积为:" << s2 << ";体积为:" << v2 << endl;
return 0;
}
本代码仅供参考,很多细节不完善。
要求:假设在二维坐标系中,设计一个圆类和一个点类,判断一个圆和一个点的位置关系(圆内、圆外、圆上)
#include
#include
#include "point.h"
//声明命名空间std
using namespace std;
//点类
class Point {
private:
int p_x;
int p_y;
public:
void SetPoint(int x, int y) {
p_x = x;
p_y = y;
}
int get_x() {
return p_x;
}
int get_y() {
return p_y;
}
};
class Circlu {
private:
Point m_p;
int m_r;
public:
//成员函数
void SetCirclu(Point p,int r) {
m_p = p;
m_r = r;
}
int get_r() {
return m_r;
}
Point get_center() {
return m_p;
}
};
//判断函数
void judge(Circlu& c, Point& p) {
int x1 = p.get_x();
int y1 = p.get_y();
int x2 =(c.get_center()).get_x();
int y2 = (c.get_center()).get_y();
int come = (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2);
int pux = c.get_r() * c.get_r();
if (come == pux)
cout << "点在圆上" << endl;
else if (come > pux)
cout << "点在圆外" << endl;
else
cout << "点在圆内" << endl;
}
int main() {
Point p,q;
p.SetPoint(10, 0);
q.SetPoint(0, 0);
Circlu c;
c.SetCirclu(q, 10);
judge(c, p);
return 0;
}
在一个
.cpp
文件里确实完成了上述功能,但是在大的项目中,这样写是肯定不行的,我们需要进行多文件编程。
point.h
文件:
#pragma once
#include
using namespace std;
//点类
class Point {
private:
int p_x;
int p_y;
public:
void SetPoint(int x, int y);
int get_x();
int get_y();
};
point.cpp
文件
#include "point.h"
void Point::SetPoint(int x, int y) {
p_x = x;
p_y = y;
}
int Point::get_x() {
return p_x;
}
int Point::get_y() {
return p_y;
}
main.cpp
文件:
#include
#include
#include "point.h"
//声明命名空间std
using namespace std;
class Circlu {
private:
Point m_p;
int m_r;
public:
//成员函数
void SetCirclu(Point p,int r) {
m_p = p;
m_r = r;
}
int get_r() {
return m_r;
}
Point get_center() {
return m_p;
}
};
//判断函数
void judge(Circlu& c, Point& p) {
int x1 = p.get_x();
int y1 = p.get_y();
int x2 =(c.get_center()).get_x();
int y2 = (c.get_center()).get_y();
int come = (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2);
int pux = c.get_r() * c.get_r();
if (come == pux)
cout << "点在圆上" << endl;
else if (come > pux)
cout << "点在圆外" << endl;
else
cout << "点在圆内" << endl;
}
int main() {
Point p,q;
p.SetPoint(10, 0);
q.SetPoint(0, 0);
Circlu c;
c.SetCirclu(q, 10);
judge(c, p);
return 0;
}
这里只是改写了
Point
类,我们还可以继续改写Circlu
类,留作练习。