1. 类与对象的基本概念
- 什么是类,什么是对象?(用自己的话描述)
- 类(Class):是一个蓝图、模板或自定义的数据类型。它定义了某类事物共有的属性(成员变量)和行为(成员函数)。它本身不占用实际的内存空间(除了类型信息)。
- 对象(Object):是类的一个具体实例。根据类这个模板在内存中实际分配了空间,拥有具体的属性值。比如“人”是类,而“张三”是一个对象。
2. 权限修饰符
- public的权限是什么? 公开的。类内部可以访问,类外部(通过对象)也可以直接访问。通常用于提供对外接口。
- private的权限是什么? 私有的。只能在类的内部(成员函数中)或者友元中被访问。类外部无法直接访问,通常用于隐藏数据(封装)。
(补充:还有一个protected,权限介于两者之间,类外部不能访问,但子类可以访问)
3. struct 与 class 的区别
- 在C++中,两者区别很小,主要在于默认的访问权限和默认的继承权限:
struct的默认访问权限和默认继承权限是public。class的默认访问权限和默认继承权限是private。
4. 构造函数 (Constructor)
- 什么是构造函数?有什么作用? 函数名与类名相同,且没有返回类型(连void都没有)的特殊成员函数。作用是初始化对象的数据成员,分配资源等。
- 什么时候会调用构造函数? 在创建对象时自动调用。包括:栈上创建对象、堆上
new对象、创建临时对象、以及作为成员对象被包含在另一个类中初始化时。
5. 对象的大小
- 一个对象的大小取决于什么? 取决于:
- 非静态成员变量的总大小(受内存对齐影响,可能会有padding填充)。
- 如果有虚函数,需要加上一个虚函数表指针(vptr)的大小(通常为8字节或4字节)。
- 注意:静态成员变量、成员函数、虚函数表本身不占用对象的内存空间。空类的大小通常为1字节。
6. 左值与右值
- 什么是左值?什么是右值?
- 左值(L-value):有明确的内存地址,有名字,表达式结束后依然存在的持久对象(如变量、数组元素、解引用的指针)。
- 右值(R-value):没有名字,临时存在的值,表达式结束后就销毁(如字面量、临时对象、返回非引用类型的函数返回值)。
- 什么样的引用可以绑定左值? 非常量左值引用(
T&)只能绑定左值;常量左值引用(const T&)可以绑定左值(也是万能引用的一种形式)。 - 什么样的引用可以绑定左值和右值? **常量左值引用(
const T&)既可以绑定左值,也可以绑定右值。
*(补充:C++11引入的右值引用(T&&)*只能绑定右值)
7. 拷贝构造函数
- 语法形式是什么?为什么? 形式:
ClassName(const ClassName& other)。必须是引用传参,因为如果按值传参,实参传给形参时会再次调用拷贝构造,导致无限递归死循环。加const是为了能接收常量对象和临时对象。 - 什么情况下会触发拷贝构造的调用?
- 用一个已存在的对象初始化新对象(
A a = b;或A a(b);)。 - 对象作为实参,以值传递的方式传给函数。
- 函数返回局部对象(以值方式返回,取决于编译器优化,未优化时会触发)。
- 用一个已存在的对象初始化新对象(
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;)。
主要特点:
- 不能修改成员变量:不能修改类的非静态数据成员(除非该成员被
mutable关键字修饰,mutable成员可以在const函数中被修改)。 - 不能调用非const成员函数:在 const 成员函数内部,只能调用其他的 const 成员函数,不能调用非 const 成员函数(因为非 const 函数可能会修改对象状态)。
- 可以修改静态成员:它可以修改类的静态数据成员,因为静态成员不属于特定的对象。
- 底层原理: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的底层机制:- 调用标准库函数
operator new分配足够大小的原始内存(通常底层调用malloc实现)。 - 在分配的内存上调用对象的构造函数,完成对象初始化。
- 返回指向该对象的指针。
- 调用标准库函数
delete的底层机制:- 先调用对象的析构函数,清理对象占用的资源。
- 调用标准库函数
operator delete释放内存(通常底层调用free实现)。
- 补充:如果是数组版本的
new[]和delete[],会在内存块头部额外记录数组长度,以便正确调用每个元素的构造/析构函数。
17. 单例设计模式的实现步骤是什么?
单例模式保证一个类仅有一个实例,并提供一个全局访问点。实现步骤通常如下:
- 私有化构造函数:防止外部通过
new创建对象。 - 禁用拷贝构造和赋值运算符:将拷贝构造函数和赋值运算符声明为私有(或使用
= delete,C++11起),防止通过拷贝创建新实例。 - 定义静态私有实例指针(或静态对象):用于存储唯一的实例。
- 提供公有静态方法:如
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 的底层结构通常由三个指针(或迭代器)组成,它们分别指向连续内存空间的不同位置:
_Myfirst(begin):指向当前已使用内存的起始位置。_Mylast(end):指向当前已使用内存的末尾的下一个位置。_Myend(cap_end):指向当前已分配内存的末尾的下一个位置。
通过这三个指针,可以计算出两个关键属性:
size():当前元素个数 =_Mylast - _Myfirstcapacity():当前可容纳的元素总数 =_Myend - _Myfirst
内存布局示意:
_Myfirst _Mylast _Myend
| | |
v v v
+------------------+------------------+
| 已构造元素 | 未使用但已分配 |
+------------------+------------------+
<---- size() ----->|<--- 剩余容量 --->|
<------------ capacity() ------------>
21. vector 的扩容机制是什么?
当 vector 的 size() 等于 capacity(),并且需要插入新元素(如 push_back)时,就会触发扩容。
扩容的步骤:
- 申请新内存:在堆上申请一块更大的内存空间(通常是原容量的 1.5 倍或 2 倍,具体取决于标准库实现:GCC/Clang 通常是 2 倍,MSVC 通常是 1.5 倍)。
- 迁移元素:将旧内存中的元素拷贝或移动(如果元素支持移动语义)到新内存中。
- 销毁旧元素:如果是非 POD(Plain Old Data)类型,需要调用旧内存中元素的析构函数。
- 释放旧内存:将原来的内存空间归还给系统。
- 更新指针:将
_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()。