华汉伟业科技C++面试经验:结构体与类区别、继承、排序及手写String类

6 min

第二篇八股文,整体难度不高

1. 结构体和类的区别

核心区别:默认访问权限不同。

  • 默认成员权限
    • struct(结构体):默认为 public
    • class(类):默认为 private
  • 默认继承权限
    • struct 继承:默认为 public 继承。
    • class 继承:默认为 private 继承。
  • 使用习惯
    • struct 通常用于封装数据(POD,纯数据结构),不涉及复杂的逻辑。
    • class 通常用于面向对象编程,强调封装性,包含数据和操作数据的方法。

记忆口诀:Struct 像开放的箱子,默认大家都能看;Class 像保险柜,默认锁起来只有自己能用。

2. 继承的兼容性(Struct 与 Class 互继)

答案:完全可以互相继承。

在 C++ 中,structclass 本质上几乎等价(除了默认权限),它们可以互相继承。

  • 结构体继承类:可以。
  • 类继承结构体:可以。
  • 结构体继承结构体:可以。
  • 类继承类:可以。

注意点: 继承时的默认访问权限取决于派生类(子类)是用 struct 还是 class 定义的。

  • 如果子类是 struct,默认继承方式是 public
  • 如果子类是 class,默认继承方式是 private

3. 单继承下的构造与析构顺序

口诀:构造“由内向外”,析构“由外向内”。

假设有父类 Base,子类 Derived,且子类中包含成员变量 Member

构造函数调用顺序:

  1. 父类构造函数(先有父亲,再有儿子)。
  2. 成员变量构造函数(先组装零件,再组装整体)。
    • 注意:成员变量的初始化顺序取决于在类中声明的顺序,而与初始化列表中的书写顺序无关。
  3. 子类自身的构造函数体

析构函数调用顺序(完全相反):

  1. 子类自身的析构函数体
  2. 成员变量析构函数
  3. 父类析构函数

记忆:穿衣服(构造)先穿内衣再穿外套;脱衣服(析构)先脱外套再脱内衣。

4. 纯虚函数怎么写

语法:在虚函数声明的末尾加 = 0

class AbstractClass {
public:
    // 纯虚函数声明
    virtual void function() = 0; 
};

特点

  • 含有纯虚函数的类称为抽象类
  • 抽象类不能实例化对象。
  • 子类必须重写该纯虚函数,否则子类也依然是抽象类。

5. 类的多个实例如何共享数据

答案:使用 static 静态成员变量。

  • 定义:在类中使用 static 修饰的成员变量,属于类本身,而不属于某个具体的对象。
  • 存储:存储在全局/静态数据区,所有对象共享同一份内存地址。
  • 初始化:必须在类外进行初始化(不包括常量整型静态成员)。

代码示例

class MyClass {
public:
    static int shared_count; // 声明
};

// 初始化(类外,不加 static)
int MyClass::shared_count = 0; 

int main() {
    MyClass a, b;
    a.shared_count = 10;
    // b.shared_count 也是 10,因为它们指向同一块内存
}

6. 希尔排序与稳定性

一定间隔的希尔排序,直到为1,不稳定

希尔排序 原理:

  • 是插入排序的改进版(也叫缩小增量排序)。
  • 将数组按一定间隔分组,对每组使用插入排序;随着间隔逐渐减小,最后间隔为 1 时进行一次直接插入排序。
  • 目的是让元素尽早移动到最终位置附近,减少移动次数。

稳定性概念:

  • 定义:如果排序前有两个相等的元素 A 和 B,A 在 B 前面。排序后,如果 A 依然在 B 前面,则该排序是稳定的;否则是不稳定的。

希尔排序是否稳定?

  • 不稳定
  • 原因:希尔排序涉及跳跃式交换。相同元素的相对位置可能会因为被分到不同的组中进行排序而打乱。

7. 手写 String 类(构造与拷贝构造)

面试中重点考察深拷贝(Deep Copy)和内存管理。

class String {
private:
    char* m_data; // 指向字符串数据的指针

public:
    // 1. 普通构造函数
    String(const char* str = nullptr) {
        if (str == nullptr) {
            m_data = new char[1];
            *m_data = '\0'; // 空字符串
        } else {
            m_data = new char[strlen(str) + 1]; // +1 是为了存放 '\0'
            strcpy(m_data, str);
        }
    }

    // 2. 拷贝构造函数(核心:深拷贝)
    String(const String& other) {
        // 重新分配内存,防止两个指针指向同一块地址(浅拷贝)
        m_data = new char[strlen(other.m_data) + 1];
        strcpy(m_data, other.m_data);
    }

    // 3. 析构函数(必须释放内存)
    ~String() {
        delete[] m_data;
    }
    
    // 面试补充:通常还需要重载赋值运算符=,这也是深拷贝的一部分
    // String& operator=(const String& other) { ... }
};

关键点解析

  1. 构造函数:要处理传入 nullptr 的情况,防止 strlen 崩溃;记得 +1 给结束符。
  2. 拷贝构造:必须是深拷贝。如果只是简单的 m_data = other.m_data(浅拷贝),两个对象会指向同一块内存,析构时会导致同一块内存被 delete 两次,程序崩溃。
  3. 析构函数:使用了 new[],析构时必须用 delete[]