一、类模板与具体化类的关系
1. 核心概念
类模板是对 "数据类型" 的抽象,允许定义通用类,其成员变量 / 函数的类型可任意指定。
-
定义语法:
cpptemplate<class T> // 或 template<typename T>,二者基本等价 class 类名 { T member; // 使用 T 作为类型占位符 }; -
原代码示例:
cstemplate<class T> class Array { private: T* p; // 通用类型指针 int size, idx; public: Array(int max_size) : size(max_size) { p = new T[max_size]{ T() }; // 调用 T 的默认构造 idx = 0; } void add(T item); // 通用类型参数 };
2. 具体化类(模板实例化)
类模板本身不是具体类,需用具体类型填充模板参数,编译器才会生成针对该类型的具体类(即 "具体化类")。
-
实例化语法:
类名<具体类型> 对象名; -
原代码示例:
Array<int> arr1(10); // 生成 Array<int> 具体类 Array<string> arr2(5); // 生成 Array<string> 具体类 -
面试重点:模板的 "两次编译"
- 第一次编译:检查模板本身的语法错误(如拼写、分号),不生成具体代码。
- 第二次编译:实例化时(如
Array<int>),用具体类型替换 T,生成类型相关的具体代码并再次检查。→ 这就是为什么模板代码通常放在头文件中(实例化时需看到完整定义)。
3. 类模板的特化(面试高频考点)
当通用模板对某些特殊类型(如指针、引用)处理不佳时,可提供 "定制化实现",即模板特化。
全特化(所有模板参数指定具体类型)
针对 Array<char*> 特化,解决浅拷贝问题:
cpp
template<>
class Array<char*> { // 全特化:T = char*
private:
char** p;
int size, idx;
public:
Array(int max_size) : size(max_size) {
p = new char*[max_size]{ nullptr };
idx = 0;
}
void add(char* item) {
if (idx >= size) return;
// 深拷贝:分配新内存并拷贝字符串内容
p[idx] = new char[strlen(item) + 1];
strcpy(p[idx], item);
idx++;
}
~Array() {
for (int i = 0; i < idx; i++) delete[] p[i];
delete[] p;
}
};
偏特化(部分模板参数指定类型,或对参数限制)
针对任意指针类型的偏特化:
template<class T>
class Array<T*> { // 偏特化:T 是指针类型
private:
T** p;
int size, idx;
public:
// 类似全特化的深拷贝实现,适用于任意指针类型
};
4. 类模板中的静态成员(面试考点)
类模板的静态成员不是所有具体化类共享,而是每个具体化类有独立的静态成员:
cpp
template<class T>
class Test {
public:
static int count;
Test() { count++; }
};
template<class T> int Test<T>::count = 0;
int main() {
Test<int> t1, t2;
Test<string> t3;
cout << Test<int>::count << endl; // 输出 2
cout << Test<string>::count << endl; // 输出 1
return 0;
}
5. 类模板的友元函数(面试易错点)
类模板的友元函数若也是模板,需正确前向声明:
cpp
template<class T> class Array; // 前向声明类模板
template<class E> ostream& operator<<(ostream&, Array<E>&); // 前向声明友元函数模板
template<class T>
class Array {
template<class E>
friend ostream& operator<<(ostream&, Array<E>&); // 声明友元
private:
T* p;
int size, idx;
};
template<class E>
ostream& operator<<(ostream& out, Array<E>& arr) {
out << "length: " << arr.idx << ", max size: " << arr.size << endl;
for (int i = 0; i < arr.idx; i++) out << arr.p[i] << " ";
return out;
}
二、继承关系
1. 继承的语法与访问限定(面试必问)
-
语法:
class 派生类名 : 继承方式 基类名 { // 派生类成员 }; -
访问权限对比表(必须背熟):
表格
| 基类成员访问限定 | public 继承后 | protected 继承后 | private 继承后 |
|---|---|---|---|
| public | public | protected | private |
| protected | protected | protected | private |
| private | 不可访问 | 不可访问 | 不可访问 |
-
原代码示例 :
cppclass Computer { private: float price; // 派生类内部也不可访问 protected: int cid; // 派生类内部可访问 public: Computer(int cid, float price) : cid(cid), price(price) {} void work(string task) { cout << cid << "正在完成 " << task << endl; } }; class DellComputer : private Computer { // private继承 public: DellComputer(int cid, float price, int memory) : Computer(cid, price) {} void updateCid(int cid) { this->cid = cid; } // 可访问基类 protected 成员 };
2. 派生类的构造与析构(面试重点)
-
构造顺序:先基类构造 → 再派生类构造。
-
析构顺序:先派生类析构 → 再基类析构(与构造顺序相反)。
-
面试重中之重:虚析构函数 若用基类指针指向派生类对象,delete 基类指针时,若基类析构非虚,只会调用基类析构,导致派生类资源泄漏!
解决方法:将基类析构声明为
virtual:cppclass Person { public: virtual ~Person() { cout << "Person析构" << endl; } // 虚析构 }; class Student : public Person { private: int* data; public: Student() { data = new int[10]; } ~Student() { delete[] data; cout << "Student析构" << endl; } }; int main() { Person* p = new Student(); delete p; // 先调用 Student 析构,再调用 Person 析构,资源正确释放 return 0; }
3. 重写、重载、隐藏的区别(面试必问,必须区分)
| 概念 | 作用域 | 函数名 | 参数列表 | 虚函数要求 | 作用 |
|---|---|---|---|---|---|
| 重载 | 同一类的同一作用域 | 相同 | 不同 | 无 | 同名函数的不同实现 |
| 重写 | 基类和派生类(不同作用域) | 相同 | 相同 | 基类需 virtual | 覆盖基类虚函数,实现多态 |
| 隐藏 | 基类和派生类(不同作用域) | 相同 | 可同可不同 | 无 | 派生类同名函数隐藏基类的 |
-
示例 :
cppclass Base { public: void func(int a) { cout << "Base::func(int)" << endl; } // 重载1 void func(double a) { cout << "Base::func(double)" << endl; } // 重载2 virtual void virtualFunc() { cout << "Base::virtualFunc()" << endl; } }; class Derived : public Base { public: void func(int a) { cout << "Derived::func(int)" << endl; } // 隐藏基类 func void virtualFunc() override { cout << "Derived::virtualFunc()" << endl; } // 重写 }; int main() { Derived d; d.func(1); // 调用 Derived::func(int)(隐藏) d.Base::func(1.5); // 显式调用基类 func(double) Base* p = &d; p->virtualFunc(); // 多态:调用 Derived::virtualFunc() return 0; }
4. 多态与虚函数的底层实现(字节跳动核心考点)
多态分为编译时多态 (重载、模板)和运行时多态(虚函数重写)。
运行时多态的条件
- 基类有虚函数(
virtual修饰)。 - 派生类重写基类虚函数(函数名、参数、返回值相同,C++11 可加
override检查)。 - 用基类指针 / 引用指向派生类对象。
底层原理:虚函数表(vtable)与虚指针(vptr)
-
每个含虚函数的类,编译器生成一个虚函数表(vtable),存储该类所有虚函数的地址。
-
每个对象含一个隐藏的虚指针(vptr),指向该类的 vtable。
-
示例内存布局:
cppclass Base { public: virtual void func1() {} virtual void func2() {} }; class Derived : public Base { public: void func1() override {} // 重写 func1 virtual void func3() {} // 新增虚函数 };- Base 对象:
[vptr] → Base vtable: [&Base::func1, &Base::func2] - Derived 对象:
[vptr] → Derived vtable: [&Derived::func1, &Base::func2, &Derived::func3]
- Base 对象:
-
面试常问:
- 虚函数表是每个类一个还是每个对象一个?→ 每个类一个,同一类的所有对象共享。
- 构造函数可以是虚函数吗?→ 不可以,构造时 vptr 未初始化,无法实现多态。
- 静态函数可以是虚函数吗?→ 不可以,静态函数无 this 指针,无法通过 vptr 调用。
5. 纯虚函数与抽象类(面试考点)
-
纯虚函数 :基类中声明但不实现的虚函数,语法:
virtual 返回值 函数名(参数) = 0;。 -
抽象类 :含至少一个纯虚函数的类,不能实例化对象。
-
派生类必须实现所有纯虚函数,否则也是抽象类。
-
示例:
cppclass Shape { // 抽象类 public: virtual double getArea() = 0; // 纯虚函数 virtual ~Shape() {} }; class Circle : public Shape { private: double r; public: Circle(double r) : r(r) {} double getArea() override { return 3.14 * r * r; } // 实现纯虚函数 }; class Rectangle : public Shape { private: double w, h; public: Rectangle(double w, double h) : w(w), h(h) {} double getArea() override { return w * h; } // 实现纯虚函数 }; int main() { Shape* s1 = new Circle(5); Shape* s2 = new Rectangle(3, 4); cout << s1->getArea() << endl; // 多态:Circle::getArea() cout << s2->getArea() << endl; // 多态:Rectangle::getArea() delete s1; delete s2; return 0; }
6. 菱形继承与虚继承(面试难点)
-
菱形继承问题:基类 A 被 B、C 继承,D 继承 B、C,导致 D 中有两份 A 的成员(二义性 + 数据冗余)。
-
解决方法 :B、C 虚继承 A(
virtual public A),D 中仅保留一份 A 的成员。 -
示例:
cppclass A { public: int a; }; class B : virtual public A {}; // 虚继承 class C : virtual public A {}; // 虚继承 class D : public B, public C {}; int main() { D d; d.a = 10; // 无歧义,仅一份 a cout << d.a << endl; // 10 return 0; }