Class
Every object has a type. -- Alan Kay
引入
回顾在 C 语言中使用的结构体:
typedef struct point{
float x, y;
} Point;
Point a;
void print(Point* p){
printf("%f %f\n", p->x, p->y);
}
void move(Point* p, int dx, int dy){
p->x += dx;
p->y += dy;
}
int main(){
a.x = 1; a.y = 2;
print(&a);
move(&a, 2, 2);
print(&a);
}
可以发现一个问题:我们的函数总是要传入一个参数 &a
,这是不是不能方便地显示出函数和类型的关系?
为了简化写法,以及更明显地刻画函数和类型的关系,我们就需要 C++ 的类。
语法
声明与定义
声明一个类:
class Point{
public:
void init(int ix, int iy);
void move(int dx, int dy);
void print();
private:
int x, y;
};
在类外,实现类的成员函数:
void Point::init(int ix, int iy){
x = ix; y = iy;
}
void Point::move(int dx, int dy){
x += dx; y += dy;
}
void Point::print(){
std::cout << x << ' ' << y << std::endl;
}
public
和 private
与 C 不同,C++ 对类型有更严格的安全管理。一些内容在类外是不能访问的,它们声明在 private
后;一些内容是允许的,它们声明在 public
后。当声明前面两者均未出现,类默认是 private
,而结构体默认是 public
。
为了数据安全,请把所有的成员变量声明为 private
。
::
符号
表示在某个类 / 命名空间下的成员。例如,Point::
表示在 Point
类下的成员,std::
表示 std
命名空间下的内容,::f()
表示一个全局函数 f()
。
使用
回顾我们学习的 STL 容器,使用方法是一样的。
Point p;
int main(){
p.init(1, 2);
p.print();
p.move(2, 2);
p.print();
}
实现的效果和前面的结构体是一样的。
原理
实际上类的原理和上面的结构体是一样的,只不过隐式地传递了一个 this 指针,指向这个对象本身。
同时所有的变量都会优先定位到对象的成员变量。 x == this->x
三个函数实质上是一样的:
void Point::move(int dx, int dy){
x += dx; y += dy;
}
void Point::move(int dx, int dy){
this->x += dx; this->y += dy;
}
void move(Point* this, int dx, int dy){
this->x += dx; this->y += dy;
}
同理,在成员函数内部调用同一个对象的成员函数,不需要显式地指出对象:
void Point::move_and_print(int dx, int dy){
move(dx, dy);// this->move(dx, dy)
print();// this->print()
}
但也有人喜欢敲 this->
,这样可以通过自动补全方便地找到类的成员。
什么是对象
对象(Objects),就是数据(Data)加操作(Operation)。
数据由各类成员变量维护,操作则是由公开的和私有的函数完成。
头文件
在定义一个类时,应该将成员变量和成员函数的声明,放在头文件 .h
中;将成员函数的实现,放在另一个源文件 .cpp
中。
在编译时,编译器同时只能处理一个 .cpp
文件,把它编译成 .obj
文件。
在链接时,链接器会把给定的 .obj
文件链接在一起,形成一个可执行文件。
.h
文件的作用就是提供函数的接口,所以只能包含外部变量、函数原型和类声明。
include 机制
#include
语句的作用是将某个文件插入到语句所在位置。根据搜索的顺序,可以划分不同的用法。
#include "xx.h"
:先搜索当前文件夹,再搜索系统库#include <xx.h>
:搜索系统库#include <xx>
:搜索系统库
标准头文件
假设 a.h
定义了一个类,b.h
和 c.h
都用到了这个类,现在需要在 d.cpp
中同时 include b.h
和 c.h
。这样的话,a.h
中的内容就会被 include 两遍,造成重复声明,编译失败。为了保证同一个头文件只出现一次,我们需要在头文件上添加一些内容。
#ifndef HEADER_FLAG
#define HEADER_FLAG
// header file content
#endif
含有这个内容的头文件就称为标准头文件,内容称为标准头文件结构。
在检测到 HEADER_FLAG
这个宏的时候,我们就认为包含了 a.h
,头文件内容就不会再被插入。
类的定义
当我们用类定义对象的时候,它的成员变量需要初始值。如何确定它的初始值?就需要使用构造函数。
在对象结束声明周期的时候,我们也许想定制其动作,就需要析构函数。
构造函数
构造函数是和类同名的成员函数,用于指定对象被构造时的行为。
如果类中没有构造函数,编译器会自动生成一个默认构造函数,将所有的成员赋值为广义的零。
class Point{
public:
Point(int _x, int _y);
private:
int x, y;
};
Point::Point(int _x, int _y){// 1
x = _x; y = _y;
}
Point::Point(int _x, int _y): x(_x), y(_y){// 2
}
方法 1 和 方法 2 在本例中是等效的。方法二称为列表初始化,冒号后的内容称为初始化列表。
区别在于:方法 1 会先将 x
赋为 0
,再执行函数体,赋为 _x
;方法 2 会直接将 x
赋为 _x
。
注意:列表初始化是按成员的声明顺序执行的,和成员在列表中的顺序无关。
如果 Point
有自定义的构造函数,那么应当确保有默认构造函数 Point()
的声明和实现,或者确保不会调用默认构造函数。
struct Y{
float f;
int i;
Y(int a);
};
Y::Y(int a): f(0), i(a){
}
Y y1[3] = {Y(1), Y(2), Y(3)}; // Ok
Y y2[3] = {Y(1)}; // Error
Y y3[3]; // Error
析构函数
析构函数是对象被销毁时调用的函数,名字是类名前加上一个 ~
。
默认的析构函数,就是释放所有成员变量的空间。
可以从下面的代码了解构造函数、析构函数的调用顺序。
struct Y{
int id;
Y(int x);
~Y();
};
Y::Y(int x): id(x){printf("An object has been constructed.#%d\n", id);}
Y::~Y(){printf("An object has been destructed.#%d\n", id);}
Y a(1);
int main(){
Y b(2);
return 0;
}