专栏首页灵魂画师牧码我揭开了「对象」的神秘面纱,就这?

我揭开了「对象」的神秘面纱,就这?

别误会,今天不是要写我对象......

这篇文章主要是聊聊我对于编程语言中「对象」的一些简单认识,Go!

一、面向过程 VS 面向对象

为什么 C 叫面向过程(Procedure Oriented)的语言,而 Java、C++ 之类叫面向对象(Object Oriented)呢?

之前听到一个有趣的说法:

在 C 语言中我们是这样写代码的:

function_a(yyy);
function_b(xxx);

从左往右看过去,最先看到的是函数,也就是 Procedure,故叫做「Procedure Oriented」。

而在 Java 这类语言我们通常是这样的:

Worker worker = new Woker("小北");
worker.touchFish("5分钟");
worker.coding("1小时");

第一眼看到的就是一个个的对象,所以叫做面向对象「Object Oriented」。

回到正题,在 C 语言,「数据」和「操作数据的函数」是互相分开的,你并不知道数据和函数之间有什么关联,这在语言层面上是不支持的。

在 C 语言中,编程就是将一堆以功能为核心导向的函数进行组合,依次调用这些函数就可以了。

这就叫面向过程,其实和我们思考问题的方式是吻合的,比如要实现一个贪吃蛇,那面向过程的设计思路就是首先分析问题的步骤:

1、开始游戏

2、随机生成食物

3、绘制画面

4、接收输入并改变方向

5、判断是否碰到墙壁和食物等

6、...

而用面向对象的思路则是:

首先,将整个游戏拆解为一个个的实体:蛇、食物、障碍物、规则系统、动画系统。

然后分别去实现这些实体应该具有的功能(即成员函数),然后你还要考虑不同实体之间如何交互和传递消息,说白了就是调用关系和传参。

比如规则系统接收蛇、食物、障碍物作为参数,可以判定是否吃到食物或者碰到墙壁。

动画系统则可以接收蛇、食物、障碍物等作为参数,然后在屏幕上动态的显示出来。

这样做的好处便是,可以利用面向对象有封装、继承、多态性的特性,设计出低耦合的系统,使系统更加灵活、更加易于维护。

好了,上面这段大概可以看做八股文,你分别用 C 和 Java/C++ 写过程序自然知道二者区别,没写过,我在这说高内聚、低耦合也没啥用。

二、那么对象是如何实现的呢?

对象的本质就是一堆的属性(成员变量)和一系列的方法(成员函数)组成,在讲这个之前,先补充说明一个「函数指针」。

我们都知道函数在 C/C++、Java 这类语言中都不是一等公民,一等公民的意思就是能够像其它整数、字符串变量一样,可以被赋值或者作为函数参数、返回值等。

但是在 JS、Python 这类动态语言中,函数却是一等公民,可以作为参数、返回值等等。

究其原因,这类语言底层实现中,一切东西皆是对象,函数、整数、字符串、浮点数都是对象,函数才因此具备同其它基本类型一样的一等公民的身份。

但是!在 C/C++ 中函数虽然是二等公民, 但我们可以通过函数指针来变相的实现将函数用于变量赋值、函数参数、返回值场景。

三、函数指针是啥?

我们知道普通变量申明后,编译器就会自动分配一块适合的内存,那么函数也是同样的,编译的时候会将一个函数编译好,然后放在一块内存中。

(上面这段说法实际很不准确,因为编译器不会分配内存,编译好的代码也是以二进制的形式放在磁盘上,只有程序开始运行时才会加载到内存)

如果我们把函数的首地址也存储在某个指针变量里,就可以通过这个指针变量来调用所指向的函数了,这个存储函数首地址的特殊指针就叫做「函数指针」。

比如有一个函数int func(int a);

我们如何申明一个可以指向func的函数指针呢?

int (*func_p)(int);

看起来有点奇怪,其实函数指针变量的声明格式如同函数func的声明一样,只不过把 func换成了 (*func_p)罢了。

为什么要括号呢?因为不要括号的话int *func_p(int);就是申明一个返回指针的函数了,括号就是为了避免这种歧义。

我们来多看几个函数指针的申明吧:

int (*f1)(int); // 传入int,返回int 
void (*f2)(char*); //传入char指针,没有返回值 
double* (*f3)(int, int); //传递两个整数,返回 double指针

来看一个函数指针的具体用处吧:

# include <stdio.h>

typedef void (*work)() Work; // typedef 定义一种函数指针类型

void xiaobei_work() {
 printf("小北工作就是写代码");
}

void shuaibei_work() {
 printf("帅北工作就是摸鱼")
}

void do_work(Work worker) {
  worker();
}
int main(void)
{
  Work x_work = xiaobei_work;
  Work s_work = shuaibei_work;
  do_work(x_work);
  do_work(s_work);
  return 0;
}

输出:

小北工作就是写代码

帅北工作就是摸鱼

其实这里有点为了用函数指针而用了,不过大家应该体会到了,函数指针最大的优点就是将函数变量化了。

我们可以将函数作为参数传递给其它函数,那么这里其实就有了多态的雏形,我们可以传递不同的函数来实现不同的行为。

void qsort(void* base, size_t num, size_t width, int(*compare)(const void*,const void*))

这是 C 标准库中 qsort 函数的申明,它最后一个参数就要求传入一个函数指针,这个函数指针负责比较两个 element。

因为两个元素的比较方式只有调用者才知道,所以这里需要以函数指针的形式告诉 qsort 如何去判定两个元素的大小。

好了,函数指针就简单介绍到这里,接下来回到主题,对象。

四、对象

那么在 C 语言中如何简单模拟一个对象呢?

当然只能靠结构体啦,而成员函数就可以通过函数指针来实现,其它的比如访问控制、继承等我们暂时不考虑。

struct Animal {
    char name[20];
    void (*eat)(struct Animal* this, char *food); // 成员方法 eat
    int (*work)(struct Animal* this);    // 成员方法 工作
};

但是eatwork都还没有任何具体实现,所以我们可以在一个初始化函数中构造这个 Animal 对象。

void eat(struct Animal* this, char *food) {
    printf("%s 在吃 %s\n", this->name, food);
};

void work(struct Animal* this) {
    printf("%s 在工作\n", this->name);
}
struct Animal* Init(const char *name) {
    struct Animal *animal = (struct Animal *)malloc(sizeof(struct Animal));
    strcpy(animal->name, name);
    animal->eat = eat;
    animal->work = work;
    return animal;
}

Init函数内部我们就完成了“成员函数”的赋值和一些初始化工作,并且给 eatwork两个函数指针都绑定了具体的实现。

接下来我们可以使用一下这个对象:

int main() {
 struct Animal *animal = Init("小狗");
 animal->eat(animal, "牛肉");
 animal->work(animal);
 return 0;
}

输出:

小狗 在吃 牛肉

小狗 在工作

为什么明明animal调用的eat方法却还要把animal当参数传递给eat方法呢,难道eat不知道是哪一个Animal调用的它吗?

确实不知道,对象其实就是在内存中一段有意义的区域,每一个不同的对象都有各自的内存位置。

而他们的成员函数却存放在代码段,而且只会存在一份副本。

所以animal->eat(...)调用方式和直接调用eat(...),效果完全等同,那个animal存在的意义就是让你从面向过程转变为面向对象思考,将方法调用转变为对象间消息传递。

所以当调用成员函数的时候,我们还需要传入一个参数 this,用来指代当前是哪个对象在调用。

由于 C 语言不支持面向对象,所以我们需要手动将 animal 作为参数传递给 eatwork 函数。

如果是在 C++ 这种面向对象的语言中,我们直接不用手动传递这个参数,就像下面这样:

animal->eat(“牛肉”);
animal->work();

实际上这是编译器帮我们去做这个事,上面这两行代码,经过编译器之后会变成下面这个样子:

eat(animal, "牛肉");
work(animal);

然后,编译器还会在编译阶段默默地将 this 作为成员函数的一个形参添加到参数列表。

并且哪个对象调用的方法,那个对象就会被当做参数赋值给this

学习 Java 的的同学也一定对这个this非常熟悉吧,Java 中和 C++ 中的 this 基本都是一样的作用。

或者说,几乎所有的面向对象语言,都会存在一个类似的机制,来将调用对象隐式的传递给成员函数,比如 Python 中的对象定义:

class Stu:
   def __init__(self, name, age):
      self.name = name
      self.age = age
      
   def displayStu(self):
      print "Name : ", self.name,  ", Age: ", self.age

可以看到每个成员函数第一个参数都必须叫self,这个self实际上就是和this是一样的作用。

只有这样,当你在成员函数内访问成员变量的时候,编译器才知道你访问的是哪一个对象。

诶,别忙,按照这样说,那岂不是,如果我在成员函数内不访问任何成员变量,就不需要传递这个this指针?

或者说可以传递一个空指针?

理论上确实成立,并且在 C++ 中也是可行的,比如下面这段代码:

class Stu{
public:
    void Hello() {
     cout << "hello world" << endl;
    }
private:
    char *name;
    int age;
    float score;
};

由于,在 Hello 函数中没有用到任何成员变量,所以我们甚至可以这样玩:

Stu *stu = new Stu;
stu->Hello(); // 正常对象,正常调用
stu = NULL;
stu->Hello() // 虽然 stu 为 NULL,但是依然不会发送运行时错误

这里实际上可以这样看:

stu->Hello(); 等价于Hello(NULL);

由于在 Hello 函数内部,没有使用任何的成员变量,所以就不需要用 this 指针去定位成员变量的内存位置,在这种情况下,调用对象为不为 NULL 其实是不重要的。

但是如果 Hello 函数访问了成员变量,比如:

void Hello() {
 cout << "Hello " << this->name << endl;
}

这里需要用到 this 去访问 name 成员变量, 那么就会导致运行时程序发生 coredump,因为我们访问了一个 NULL 地址,或者说是基于 NULL 偏移一定位置的地址,这段空间绝对是没有访问权限的。

之前,恰好也有位同学在群里问了这个问题:

这个问题解释就和上面的一样,但是这个结论不能推广到其它语言,比如 Java、Python,这些语言的虚拟机一般会做一些额外的检查,比如判断调用对象是否是空指针等,是的话就会触发空指针异常。

而 C++ 就真的是很纯粹的编译成汇编,只要从汇编层面能跑通,那就没问题,所以才能利用这个“奇技淫巧”。

那写这篇文章得目的呢,就是想让大家对「对象」有一个具体的认识,最好是明白对象在内存中或者 JVM 中是如何布局的。

我以前就会觉得对象挺神奇的,一堆的功能,后来才后知后觉,特么的不就是一个结构体再加上编译器的语法糖吗?

文章分享自微信公众号:
编程如画

本文参与 腾讯云自媒体分享计划 ,欢迎热爱写作的你一起参与!

原始发表时间:2020-12-02
如有侵权,请联系 cloudcommunity@tencent.com 删除。
登录 后参与评论
0 条评论

相关文章

  • Magic Leap和ILMxLab合作,一起把《星球大战》搬到你家客厅

    镁客网
  • kotlin源码阅读——函数式编程

    我主要写Kotlin源码阅读,函数式编程的基本概念,概念大家可以在网上做一些了解,这里推荐一下百度百科的定义,函数式编程概念,蛮清晰的。

    Runhwguo
  • 我天!xx.equals(null) 是什么骚操作??

    我的天,最近做 Code Review 看到一个同事的骚操作,他写了一个工具类,大概是这样的:

    Java技术栈
  • 【编程人生】编程到底是啥

    对于小白来说,编程是个神秘的事儿;对于初学者来说,编程是个高大上的事儿;对于小编来说,编程是我份内的事儿。 编程这么受欢迎?引用宝强的一句话:啥啥啥?编程是个啥...

    程序员互动联盟
  • 2019 年白酒行业黑灰产研究白皮书 | 干货

    “天若不爱酒,酒星不在天。地若不爱酒,地应无酒泉”。 中国人对白酒的钟情贯穿着历史长河,更是支撑起了一个近万亿市场规模的庞大产业。但闻着“酒香”而来的也有别有用...

    腾讯云安全
  • 【容错篇】WAL在Spark Streaming中的应用【容错篇】WAL在Spark Streaming中的应用

    WAL 即 write ahead log(预写日志),是在 1.2 版本中就添加的特性。作用就是,将数据通过日志的方式写到可靠的存储,比如 HDFS、s3,在...

    codingforfun
  • 揭开Spark Streaming神秘面纱⑥ - Spark Streaming结合 Kafka 两种不同的数据接收方式比较

    DirectKafkaInputDStream 只在 driver 端接收数据,所以继承了 InputDStream,是没有 receivers 的

    codingforfun
  • 黑客利用 Excel 文档来执行 ChainShot 恶意软件攻击

    针对近日曝光的 Adobe Flash 零日漏洞(CVE-2018-5002),已经出现了一款名叫 CHAINSHOT 的恶意软件攻击。其利用微软 Excel ...

    C4rpeDime
  • .NET简谈互操作(二:先睹为快)

    我们继续.NET互操作学习,为了揭开互操作的神秘面纱,今天这篇文章我们就来先睹为快,让我们先来做个例子,基础的东西,我们陆续进行讲解;由于互操作牵扯到的东西非常...

    王清培
  • JWT 的 Token 过期时间为什么没有生效

    在我第一次在 DRF(Django REST Framework)中使用 JWT 时,感觉 JWT 非常神奇,它即没有使用 session、cookie,也不使...

    somenzz
  • New PMC 田原: 开源给了我一个接触非业务系统的机会

    来自清华大学软件学院,参与 Apache IoTDB 开源贡献已近3年,贡献了259个 PR ,Github 上累积贡献度排名第四。

    Apache IoTDB
  • 短信代收,一个年产数亿的黑产链条

    陈梦 腾讯安全平台部高级安全产品经理 我将会通过一个系列文章来给大家揭开这里的神秘面纱,看看究竟是谁坑走了创业者的钱。今天来看第一篇——年产值过亿的短信代收...

    腾讯研究院
  • JS中prototype介绍

    转载 原文点这里 用过JavaScript的同学们肯定都对prototype如雷贯耳,但是这究竟是个什么东西却让初学者莫衷一是,只知道函数都会有一个proto...

    全栈程序员站长
  • 【评论】高冷的苹果,能在人工智能上有所作为吗?

    【新智元导读】苹果最近邀请科技记者Steven Levy到总部走了一圈,希望向外界介绍其机器学习技术,一反过去低调的姿态。其实以机器学习为代表的人工智能技术在苹...

    新智元
  • 半夜,F盘里传来了一阵响声···

    突然,一个巨大的东西从远处飞了过来,在阿飞头顶不远处的地方快速掠过,吓得阿飞下意识一闪。

    轩辕之风
  • Spark Streaming + Kakfa 编程指北

    本文简述如何结合 Spark Streaming 和 Kakfa 来做实时计算。截止目前(2016-03-27)有两种方式:

    codingforfun
  • Python技术周刊:第 13 期

    1、揭开Python args和kwargs的神秘面纱[1] 学习如何在Python中使用args和kwargs来为你的函数添加更多灵活性。

    TalkPython

扫码关注腾讯云开发者

领取腾讯云代金券