Limesn
Limesn
发布于 2026-09-11 / 25 阅读
0

第二章 类与对象

1. 类与对象的基本概念

  • 什么是类,什么是对象?(用自己的话描述)
    • 类(Class):是一个蓝图、模板或自定义的数据类型。它定义了某类事物共有的属性(成员变量)和行为(成员函数)。它本身不占用实际的内存空间(除了类型信息)。
    • 对象(Object):是类的一个具体实例。根据类这个模板在内存中实际分配了空间,拥有具体的属性值。比如“人”是类,而“张三”是一个对象。

2. 权限修饰符

  • public的权限是什么? 公开的。类内部可以访问,类外部(通过对象)也可以直接访问。通常用于提供对外接口。
  • private的权限是什么? 私有的。只能在类的内部(成员函数中)或者友元中被访问。类外部无法直接访问,通常用于隐藏数据(封装)。
    (补充:还有一个protected,权限介于两者之间,类外部不能访问,但子类可以访问)

3. struct 与 class 的区别

  • 在C++中,两者区别很小,主要在于默认的访问权限和默认的继承权限:
    • struct 的默认访问权限和默认继承权限是 public。
    • class 的默认访问权限和默认继承权限是 private。

4. 构造函数 (Constructor)

  • 什么是构造函数?有什么作用? 函数名与类名相同,且没有返回类型(连void都没有)的特殊成员函数。作用是初始化对象的数据成员,分配资源等。
  • 什么时候会调用构造函数? 在创建对象时自动调用。包括:栈上创建对象、堆上 new 对象、创建临时对象、以及作为成员对象被包含在另一个类中初始化时。

5. 对象的大小

  • 一个对象的大小取决于什么? 取决于:
    1. 非静态成员变量的总大小(受内存对齐影响,可能会有padding填充)。
    2. 如果有虚函数,需要加上一个虚函数表指针(vptr)的大小(通常为8字节或4字节)。
    3. 注意:静态成员变量、成员函数、虚函数表本身不占用对象的内存空间。空类的大小通常为1字节。

6. 左值与右值

  • 什么是左值?什么是右值?
    • 左值(L-value):有明确的内存地址,有名字,表达式结束后依然存在的持久对象(如变量、数组元素、解引用的指针)。
    • 右值(R-value):没有名字,临时存在的值,表达式结束后就销毁(如字面量、临时对象、返回非引用类型的函数返回值)。
  • 什么样的引用可以绑定左值? 非常量左值引用(T&)只能绑定左值;常量左值引用(const T&)可以绑定左值(也是万能引用的一种形式)。
  • 什么样的引用可以绑定左值和右值? **常量左值引用(const T&)既可以绑定左值,也可以绑定右值。
    *(补充:C++11引入的
    右值引用(T&&)*只能绑定右值)

7. 拷贝构造函数

  • 语法形式是什么?为什么? 形式:ClassName(const ClassName& other)。必须是引用传参,因为如果按值传参,实参传给形参时会再次调用拷贝构造,导致无限递归死循环。加const是为了能接收常量对象和临时对象。
  • 什么情况下会触发拷贝构造的调用?
    1. 用一个已存在的对象初始化新对象(A a = b; 或 A a(b);)。
    2. 对象作为实参,以值传递的方式传给函数。
    3. 函数返回局部对象(以值方式返回,取决于编译器优化,未优化时会触发)。

8. 深拷贝与浅拷贝

  • 什么是深拷贝,什么是浅拷贝?
    • 浅拷贝:仅仅复制对象的所有成员变量的值(如果是指针,就只复制指针地址)。这会导致两个对象共享同一块堆内存,容易引发“双重释放(Double Free)”问题。默认生成的拷贝构造函数就是浅拷贝。
    • 深拷贝:在复制指针成员时,不是复制地址,而是重新在堆区分配一块新的内存,并将原指针指向的内容复制过去。这样两个对象各自拥有独立的资源,互不影响。

9. 析构函数 (Destructor)

  • 语法形式是什么? ~ClassName(),没有参数,没有返回值,不能重载,一个类只能有一个析构函数。
  • 析构函数一般用来干什么? 在对象生命周期结束时自动调用。主要用于清理资源,比如释放动态分配的堆内存(delete)、关闭文件句柄、释放网络连接等,防止内存泄漏。

10. this 指针

  • this是什么?this的本质是什么?
    • this 是当前对象的指针。
    • 它的本质是一个隐含的形参(编译器自动添加的),指向调用该成员函数的对象。在底层,它是 ClassName* const this(一个指针常量,即指向不能改变,但指向的内容可以改变)。静态成员函数没有 this 指针。

11. 赋值运算符重载函数

  • 语法形式是什么?为什么? ClassName& operator=(const ClassName& other)。
    • 为什么返回引用 ClassName&? 为了支持链式赋值(如 a = b = c),并且避免返回时调用一次拷贝构造。
    • 为什么参数是 const ClassName&? 防止在赋值过程中修改原对象(const),且引用传参避免一次拷贝(&)。
  • 赋值运算符函数的本质是什么? 是对已存在的两个对象进行数据成员的覆盖和资源的重新分配。它与拷贝构造的区别在于:拷贝构造是在对象初始化时调用,而赋值运算符是在对象已经存在时调用。
    (注意点:在实现赋值运算符时,必须检查自我赋值 if (this != &other),并且如果是深拷贝,需要先释放原有的资源,再分配新资源)。

图片中列出了C++面向对象编程中关于类成员(数据成员、成员函数、const修饰符)的四个经典面试/考试题。以下是针对这四个问题的详细解答:

12. 特殊的数据成员有哪些?如何进行初始化?

特殊的数据成员主要有:

  • const 数据成员(常量成员)
  • 引用数据成员(& 引用)
  • 静态数据成员(static 修饰)

初始化方式:

  • const 数据成员和引用数据成员:必须在构造函数的初始化列表(Initializer List)中进行初始化,不能在构造函数体内赋值。因为这两类成员在定义时就必须有初始值。
    class MyClass {
        const int a;
        int& ref;
    public:
        MyClass(int x, int& y) : a(x), ref(y) {} // 正确,在初始化列表中
    };
    
  • 静态数据成员:必须在类定义外部进行定义和初始化(C++17起引入了 inline static,允许在类内直接初始化)。静态成员不属于任何对象,属于整个类。
    class MyClass {
        static int count;
    };
    int MyClass::count = 0; // 类外初始化
    
  • (补充) 常量静态数据成员(如 static const int 或 static constexpr int)如果是整型或枚举类型,通常允许在类内直接声明并初始化。

13. 静态的成员函数中能访问什么?非静态的成员函数能访问什么?

  • 静态成员函数:
    • 它没有 this 指针。
    • 只能访问:类的静态数据成员和静态成员函数。
    • 不能直接访问:类的非静态数据成员和非静态成员函数(除非通过传入对象引用或指针显式调用)。
  • 非静态成员函数:
    • 它隐含 this 指针。
    • 能访问:类的所有成员,包括非静态数据成员、静态数据成员、非静态成员函数和静态成员函数。

14. const成员函数的特点是什么?

const成员函数是在函数声明末尾加上 const 关键字的成员函数(如 void func() const;)。
主要特点:

  1. 不能修改成员变量:不能修改类的非静态数据成员(除非该成员被 mutable 关键字修饰,mutable 成员可以在const函数中被修改)。
  2. 不能调用非const成员函数:在 const 成员函数内部,只能调用其他的 const 成员函数,不能调用非 const 成员函数(因为非 const 函数可能会修改对象状态)。
  3. 可以修改静态成员:它可以修改类的静态数据成员,因为静态成员不属于特定的对象。
  4. 底层原理:const 成员函数实际上修饰的是隐含的 this 指针,将其变为 const ClassName* const this,这意味着既不能改变 this 指针的指向,也不能通过 this 修改对象的内容。

15. const成员函数可以被什么对象调用?非const成员函数可以被什么对象调用?

这取决于对象的常量性(权限匹配问题,遵循“权限只能缩小不能放大”的原则):

  • const 对象(常量对象):
    • 只能调用 const 成员函数。
    • 不能调用非 const 成员函数,因为非 const 成员函数可能会修改对象的状态,违反了 const 对象的只读属性。
  • 非 const 对象(普通对象):
    • 既可以调用 const 成员函数,也可以调用非 const 成员函数。
    • 如果同时存在同名的 const 和非 const 成员函数(重载),非 const 对象会优先调用非 const 版本。

16. new, delete底层用的是什么?

  • new 的底层机制:
    1. 调用标准库函数 operator new 分配足够大小的原始内存(通常底层调用 malloc 实现)。
    2. 在分配的内存上调用对象的构造函数,完成对象初始化。
    3. 返回指向该对象的指针。
  • delete 的底层机制:
    1. 先调用对象的析构函数,清理对象占用的资源。
    2. 调用标准库函数 operator delete 释放内存(通常底层调用 free 实现)。
  • 补充:如果是数组版本的 new[] 和 delete[],会在内存块头部额外记录数组长度,以便正确调用每个元素的构造/析构函数。

17. 单例设计模式的实现步骤是什么?

单例模式保证一个类仅有一个实例,并提供一个全局访问点。实现步骤通常如下:

  1. 私有化构造函数:防止外部通过 new 创建对象。
  2. 禁用拷贝构造和赋值运算符:将拷贝构造函数和赋值运算符声明为私有(或使用 = delete,C++11起),防止通过拷贝创建新实例。
  3. 定义静态私有实例指针(或静态对象):用于存储唯一的实例。
  4. 提供公有静态方法:如 getInstance(),作为全局访问点,返回唯一的实例。

常见实现方式(C++11 推荐的 Meyers Singleton 懒汉模式):

class Singleton {
private:
    Singleton() {} // 私有构造
    Singleton(const Singleton&) = delete; // 禁用拷贝
    Singleton& operator=(const Singleton&) = delete; // 禁用赋值

public:
    static Singleton& getInstance() {
        static Singleton instance; // C++11 保证局部静态变量的线程安全性
        return instance;
    }
};

(另有饿汉模式:在程序启动时就初始化静态实例,天生线程安全,但可能影响启动速度。)

18. 掌握std::string的基本使用,掌握常见的成员函数的使用(会查CPP参考手册)

这其实是一个学习目标。你需要熟练掌握 std::string 的常用API,并知道遇到不熟悉的函数时去查阅 cppreference.com 等手册。常见的成员函数分类如下:

  • 构造与赋值:string(), string(const char*), string(size_t, char), operator=
  • 容量/大小:size(), length(), empty(), capacity(), reserve(), resize()
  • 元素访问:operator[], at(), front(), back()
  • 修改操作:
    • append(), push_back(), pop_back()
    • insert(), erase(), clear()
    • replace(), swap()
  • 子串与查找:
    • substr(pos, len):获取子串
    • find(), rfind():查找子串或字符
    • find_first_of(), find_last_of() 等
  • 迭代器:begin(), end(), cbegin(), cend()
  • 字符串拼接:operator+, operator+=

建议:重点了解 size() 和 length() 的区别(无区别)、reserve() 对性能的优化、以及 find() 找不到时返回 std::string::npos 的机制。在实际开发中,熟练查阅参考手册是必备技能。

图片中列出了关于 C++ std::vector 的三个经典问题。以下是详细的解答:


19. vector 是什么?

std::vector 是 C++ 标准模板库(STL)中的一种序列容器,本质是一个动态数组。

核心特点:

  • 连续内存:底层在堆上分配一块连续的内存空间,因此支持O(1) 的随机访问(通过下标 [] 或 at())。
  • 动态大小:不需要像原生数组那样在编译期确定大小,可以在运行时自动扩容。
  • 尾部高效:在末尾添加或删除元素(push_back / pop_back)效率极高(均摊 O(1))。
  • 中间插入/删除低效:在头部或中间插入/删除元素需要移动后续所有元素,时间复杂度为 O(N)。
  • 自动管理内存:基于 RAII 机制,超出作用域时自动释放底层内存。

20. vector 的底层结构是什么?

vector 的底层结构通常由三个指针(或迭代器)组成,它们分别指向连续内存空间的不同位置:

  1. _Myfirst (begin):指向当前已使用内存的起始位置。
  2. _Mylast (end):指向当前已使用内存的末尾的下一个位置。
  3. _Myend (cap_end):指向当前已分配内存的末尾的下一个位置。

通过这三个指针,可以计算出两个关键属性:

  • size():当前元素个数 = _Mylast - _Myfirst
  • capacity():当前可容纳的元素总数 = _Myend - _Myfirst

内存布局示意:

_Myfirst           _Mylast            _Myend
   |                  |                  |
   v                  v                  v
   +------------------+------------------+
   |   已构造元素     |   未使用但已分配 |
   +------------------+------------------+
   <---- size() ----->|<--- 剩余容量 --->|
   <------------ capacity() ------------>

21. vector 的扩容机制是什么?

当 vector 的 size() 等于 capacity(),并且需要插入新元素(如 push_back)时,就会触发扩容。

扩容的步骤:

  1. 申请新内存:在堆上申请一块更大的内存空间(通常是原容量的 1.5 倍或 2 倍,具体取决于标准库实现:GCC/Clang 通常是 2 倍,MSVC 通常是 1.5 倍)。
  2. 迁移元素:将旧内存中的元素拷贝或移动(如果元素支持移动语义)到新内存中。
  3. 销毁旧元素:如果是非 POD(Plain Old Data)类型,需要调用旧内存中元素的析构函数。
  4. 释放旧内存:将原来的内存空间归还给系统。
  5. 更新指针:将 _Myfirst、_Mylast、_Myend 更新指向新内存。

为什么是成倍扩容?(均摊时间复杂度)
如果每次只扩容 1 个元素,那么每次 push_back 都要进行 O(N) 的拷贝,总时间复杂度退化为 O(N²)。成倍扩容可以保证 push_back 的均摊时间复杂度为 O(1)。

扩容带来的问题与优化:

  • 迭代器失效:扩容后内存地址改变,原有的所有迭代器、指针、引用全部失效。
  • 性能损耗:扩容涉及大量元素的拷贝/移动和内存申请,开销很大。
  • 优化建议:如果提前知道元素的大致数量,可以使用 reserve(n) 一次性预分配足够的内存,避免多次扩容带来的性能损耗。

补充:reserve() vs resize()

  • reserve(n):只改变 capacity(),不改变 size(),不创建元素。
  • resize(n):改变 size(),会创建或销毁元素,可能同时改变 capacity()。