软件开发面试准备

20 阅读12分钟

设计模式

设计模式是在软件开发中用于解决常见问题的经过验证的最佳实践和通用解决方案。以下是一些常见的设计模式,我将简要解释它们的原理:

  • 单例模式(Singleton Pattern)

    • 原理:确保一个类只有一个实例,并提供一个全局访问点来获取该实例。通常通过私有化构造函数和静态方法来实现。
    • 用途:适用于需要全局唯一实例的情况,如配置管理器、日志记录器等。
  • 工厂模式(Factory Pattern)

    • 原理:定义一个创建对象的接口,但将具体对象的创建延迟到子类中。客户端通过工厂类来创建对象,而不是直接实例化对象。
    • 用途:用于解耦客户端代码与具体对象的创建,支持多态和扩展。
  • 观察者模式(Observer Pattern)

    • 原理:定义一种一对多的依赖关系,使一个对象的状态改变时,所有依赖于它的对象都会得到通知并自动更新。
    • 用途:用于建立对象之间的松散耦合关系,通常用于事件处理和通知机制。
  • 装饰者模式(Decorator Pattern)

    • 原理:允许在不改变对象结构的情况下动态地添加功能。通过包装(装饰)对象来扩展其功能。
    • 用途:用于在不修改现有代码的情况下添加新功能,如IO流、图形库等。
  • 策略模式(Strategy Pattern)

    • 原理:定义一系列算法,将每个算法封装为一个独立的对象,使它们可以互相替换。客户端可以在运行时选择要使用的算法。
    • 用途:用于使算法可互换,提高代码的可维护性和扩展性。
  • 模板方法模式(Template Method Pattern)

    • 原理:定义一个算法骨架,将一些步骤延迟到子类中实现。子类可以改变算法的某些步骤,但不改变算法的结构。
    • 用途:用于定义算法的框架,允许子类实现特定步骤。
  • 命令模式(Command Pattern)

    • 原理:将请求封装为一个对象,从而允许使用不同的请求、队列或者日志请求来参数化客户端对象。
    • 用途:用于将请求发送者和请求接收者解耦,支持撤销和重做操作。
  • 适配器模式(Adapter Pattern)

    • 原理:将一个接口转换成另一个客户端希望使用的接口。适配器允许不兼容的接口协同工作。
    • 用途:用于解决接口不匹配的问题,允许不同接口的类一起工作。

函数重载和函数重写

  • 函数重载(Function Overloading)

    函数重载是指在同一个作用域内,可以定义多个具有相同名称但不同参数列表的函数。编译器根据函数调用时提供的参数列表来选择正确的函数进行调用。函数重载通常在同一个类或命名空间中使用,以提供不同版本的相似功能。

  • 函数重写(Function Overriding)

    函数重写是面向对象编程中的概念,通常与继承和多态性相关。它允许子类覆盖(重写)父类中的函数,以提供自己的实现,但函数的名称、参数列表和返回类型必须与父类中的函数匹配。

    在面向对象的语言中,如C++和Java,你可以创建一个派生类,重写父类中的虚函数(C++中的虚函数,Java中的方法覆盖),以实现多态性。这意味着在运行时,可以根据对象的实际类型来调用适当的函数版本。

野指针、悬空指针

野指针是指引用已释放或超出作用域的内存的指针,通常是一个编程错误。悬空指针是指包含空值的指针,可能是有意设置为空值或者因未初始化而包含空值,它可能是为了避免野指针而故意设置的。在编程中,应避免野指针,但悬空指针可以在某些情况下被用于标识指针没有引用任何内存。

  • 野指针(Dangling Pointer)

    • 野指针是指指针仍然引用先前分配的内存位置,但这些内存位置已经被释放或超出了作用域。
    • 通常,野指针是由于程序员未正确管理指针的生命周期而导致的。这可能是因为指针引用的对象已经被销毁,或者指针指向的内存已经被释放。
    • 野指针通常会导致程序崩溃、未定义行为或数据损坏。
  • 悬空指针(Null Pointer)

    • 悬空指针是指指针包含空值(nullptrNULL),即指针没有引用任何有效的内存地址。
    • 悬空指针可以是有意的,也可以是为了避免野指针而故意设置的。在某些情况下,将指针设置为空值可以避免悬空指针引起的问题。
    • 悬空指针不一定导致程序崩溃,但如果试图访问悬空指针所引用的内存,可能会导致崩溃或未定义行为。

new、malloc区别

new 更适用于 C++ 中,用于分配内存和构造对象,具有更好的类型安全性和异常处理机制。而 malloc 是 C 语言中的库函数,更底层,需要手动处理内存分配和释放,不涉及对象构造。

  • 语言关联
    • new 是 C++ 中的运算符,用于动态分配内存并构造对象(如果是类类型)。
    • malloc 是 C 语言中的库函数,用于动态分配内存,不涉及对象构造。
  • 返回类型
    • new 返回分配的内存的指针,可以直接用于创建对象,并会自动调用对象的构造函数。
    • malloc 返回 void* 类型的指针,需要进行显式的类型转换,并不会调用对象的构造函数。你需要手动调用构造函数来初始化对象。
  • 类型安全
    • new 在分配内存时可以确保类型安全,因为它知道要分配的对象的类型并调用适当的构造函数。
    • malloc 不涉及类型信息,因此需要程序员显式处理对象的构造和析构,容易出现类型错误。
  • 头文件
    • new 不需要包含额外的头文件,因为它是 C++ 的一部分。
    • malloc 需要包含 <stdlib.h><malloc.h> 头文件。
  • 错误处理
    • new 可以抛出异常(std::bad_alloc 异常)以指示分配失败,可以用异常处理机制来捕获和处理。
    • malloc 返回 NULL 指针来指示分配失败,需要程序员显式检查并处理。
  • 大小指定
    • new 根据对象的类型自动计算所需的内存大小,无需显式指定。
    • malloc 需要显式指定要分配的内存大小,通常使用 sizeof 运算符来计算。

堆和栈的区别

堆(Heap)和栈(Stack)是计算机内存中用于存储数据的两种不同方式,它们在数据存储和访问的方式、生命周期和用途上有明显的区别。以下是它们的主要区别:

  • 存储方式
    • :栈是一种线性数据结构,采用先进后出(FILO)的方式存储数据。在栈中,数据项以顺序方式排列,最后进入的数据项最先被访问和移除。
    • :堆是一种树状数据结构,没有明确的顺序,数据项可以在堆中随机存储。数据项在堆中通常以树状结构组织,具有父子关系。
  • 数据类型
    • :主要用于存储基本数据类型,如整数、浮点数、指针等。栈内存通常较小,因为它的生命周期相对较短。
    • :用于动态分配内存,通常用于存储对象、数据结构、类实例等复杂数据类型。堆内存通常较大,但需要手动分配和释放。
  • 生命周期
    • :栈内存的生命周期与程序的函数调用关系密切相关。当函数调用结束时,栈上的局部变量会自动销毁。栈的生命周期是短暂的。
    • :堆内存的生命周期通常取决于手动分配和释放。程序员负责在适当的时候分配内存,并在不再需要时手动释放它。因此,堆的生命周期可以是长时间的。
  • 内存管理
    • :栈内存的分配和释放由编译器自动管理,不需要程序员干预。这降低了程序出错的可能性。
    • :堆内存的分配和释放需要程序员显式控制。如果不正确管理堆内存,可能会导致内存泄漏或内存溢出等问题。
  • 效率
    • :栈内存的分配和访问速度较快,因为它采用简单的指针移动来管理数据。
    • :堆内存的分配和访问速度较慢,因为需要在堆中查找合适的空间来存储数据,并且需要维护堆中的数据结构。

总结来说,栈用于存储局部变量和函数调用信息,生命周期短暂,管理简单。堆用于动态分配内存,适用于复杂数据类型,生命周期较长,需要程序员显式管理。栈和堆都有各自的用途,程序员在选择存储数据的方式时需要根据需求和性能要求进行考虑。

Python实现栈

class Stack(object):

    def __init__(self):
        self.items = []

    def is_empty(self):
        """判断是否为空集"""
        return self.items == []

    def push(self, item):
        """添加新元素到栈顶"""
        self.items.append(item)

    def pop(self):
        """删除栈顶元素"""
        return self.items.pop()

    def peek(self):
        """窥探栈顶元素"""
        return self.items[-1]

    def size(self):
        """查看栈的大小"""
        return len(self.items)

if __name__ == '__main__':

    stack = Stack()
    print(stack.is_empty())
    stack.push(2# 入栈
    stack.push(3# 入栈
    print(stack.size())
    print(stack.peek())
    stack.pop()  # 出栈
    print(stack.size())

代码编译的全过程

  • 源代码编写: 开发人员首先使用高级编程语言(例如C、C++、Java等)编写源代码。这是开发人员编写程序的初始阶段。
  • 预处理(Preprocessing): 在编译过程开始之前,源代码中的预处理指令(以#开头)会被执行。这些指令用于包含头文件、宏展开和条件编译等操作。
  • 编译(Compilation): 预处理后的代码被编译器翻译成中间代码(通常是汇编语言或机器码的形式),这个中间代码包含了程序的原始逻辑。
  • 汇编(Assembling): 如果源代码被翻译成了汇编语言,接下来会将汇编代码翻译成机器代码。这个过程由汇编器完成。
  • 链接(Linking): 在编译多个源文件的项目时,编译器生成多个目标文件。链接器负责将这些目标文件合并成一个可执行程序。它还会解析和解决外部引用,例如库函数和其他模块。
  • 优化(Optimization): 优化器可以在生成最终可执行文件之前对中间代码进行优化,以提高程序的性能和效率。
  • 生成可执行文件: 最终阶段,链接器将所有的目标文件和库文件组合在一起,生成一个可执行文件。这个文件包含了机器码和程序的数据,可以在计算机上运行。
  • 运行程序: 最终的可执行文件可以在计算机上运行,执行程序的逻辑,并产生所期望的输出。

线程和进程

  • 定义

    • 进程:进程是程序的执行实例,拥有独立的内存空间、文件描述符和系统资源。它是操作系统进行任务管理和资源分配的基本单位。
    • 线程:线程是进程内的一个独立执行流,与同一进程内的其他线程共享相同的内存空间和资源。线程是进程的一个组成部分,用于执行进程中的任务。
  • 资源占用

    • 进程:每个进程都拥有独立的内存空间,因此进程之间的资源隔离较好。但创建和销毁进程需要较多的系统开销。
    • 线程:线程共享进程的内存空间和资源,因此线程之间的资源共享较容易。但线程之间也需要进行同步和互斥,以避免竞态条件。
  • 通信

    • 进程:进程之间通信较为复杂,通常需要使用进程间通信(IPC)机制,如管道、消息队列、共享内存等。
    • 线程:线程之间通信相对容易,因为它们共享相同的内存空间,可以通过共享变量等方式来进行通信。
  • 创建和销毁

    • 进程:创建和销毁进程通常比较耗时,需要分配和回收独立的内存空间。
    • 线程:创建和销毁线程通常比较轻量,因为它们共享进程的内存空间。
  • 并发性

    • 进程:进程之间并发执行,每个进程有独立的地址空间,因此更适合在多核处理器上进行并行计算。
    • 线程:线程之间并发执行,但它们共享相同的地址空间,因此更适合在单核处理器上进行并发计算。
  • 稳定性

    • 进程:由于进程之间资源隔离,一个进程的崩溃通常不会影响其他进程。
    • 线程:线程之间共享资源,因此一个线程的错误或崩溃可能会影响整个进程。

同步和异步

  • 同步(Synchronous)
    • 同步操作是指程序按照严格的顺序执行,每个操作都会等待上一个操作完成后才能开始执行。
    • 当执行一个同步操作时,程序会阻塞(暂停)当前线程或进程,直到操作完成为止。
    • 同步操作通常用于简单的顺序执行任务,易于理解和调试,但可能会导致性能问题,因为程序会等待操作完成。
  • 异步(Asynchronous)
    • 异步操作是指程序不会等待某个操作完成,而是继续执行其他任务,然后在操作完成时获得通知。
    • 异步操作通常涉及回调函数、事件处理或异步任务。程序会在后台执行操作,同时继续执行其他任务。
    • 异步操作通常用于需要长时间执行的任务,如文件读写、网络请求等,以避免阻塞程序的执行。