OOP Lec3: Defining Class

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;
}

publicprivate

与 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.hc.h 都用到了这个类,现在需要在 d.cpp 中同时 include b.hc.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;
}
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇