C++高频面试题精选 - 第一部分(题目1-10)

C++高频面试题精选 - 第一部分(题目1-10)

基础知识

题目1: 简述C++中newmalloc的区别

解答:

  1. 类型new是C++操作符,malloc是C库函数
  2. 返回类型new返回具体类型指针,malloc返回void*需要强制转换
  3. 构造/析构new会调用构造函数,delete调用析构函数;malloc不会
  4. 内存分配失败new抛出std::bad_alloc异常,malloc返回NULL
  5. 大小计算new自动计算大小,malloc需要手动指定字节数
  6. 重载new可以重载,malloc不可以
  7. 内存来源new从自由存储区分配,malloc从堆分配(通常相同但概念不同)

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// new的使用
int* p1 = new int(10); // 分配并初始化
int* arr1 = new int[5]; // 分配数组
delete p1;
delete[] arr1;

// malloc的使用
int* p2 = (int*)malloc(sizeof(int)); // 需要类型转换
int* arr2 = (int*)malloc(5 * sizeof(int)); // 手动计算大小
if (p2 == NULL) { // 需要检查NULL
// 处理错误
}
free(p2);
free(arr2);

// 对象的区别
class Widget {
public:
Widget() { cout << "构造函数\n"; }
~Widget() { cout << "析构函数\n"; }
};

Widget* w1 = new Widget; // 输出"构造函数"
delete w1; // 输出"析构函数"

Widget* w2 = (Widget*)malloc(sizeof(Widget)); // 不调用构造函数
free(w2); // 不调用析构函数(危险!)

口头解答:
“这是个很经典的问题。首先,new是C++的操作符,而malloc是C语言的库函数。最关键的区别是new会自动调用对象的构造函数,delete会调用析构函数,这对于C++的对象管理非常重要;而malloc只是分配一块原始内存,不会做任何初始化。另外,new返回的是具体类型的指针,类型安全;malloc返回void指针,需要强制转换。在错误处理上,new失败会抛出异常,malloc失败返回NULL。所以在C++中,我们应该优先使用new而不是malloc。”


题目2: C++中的引用和指针有什么区别?

解答:

  1. 本质:引用是别名,指针是地址变量
  2. 初始化:引用必须在声明时初始化,指针可以不初始化
  3. 可变性:引用一旦绑定不可改变,指针可以指向不同对象
  4. 空值:引用不能为空,指针可以为nullptr
  5. 运算:指针支持自增、自减等运算,引用不支持
  6. 内存占用:引用通常不占额外空间(编译器优化),指针占用空间存储地址
  7. 多级:可以有多级指针,但引用只有一级
  8. 使用:引用使用更安全简洁,指针更灵活强大

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
int x = 10;

// 引用的使用
int& ref = x; // 必须初始化
ref = 20; // x变成20
// int& ref2; // 错误!引用必须初始化
// ref = y; // 这是赋值,不是重新绑定

// 指针的使用
int* ptr; // 可以不初始化
ptr = &x; // 指向x
*ptr = 30; // x变成30
ptr = nullptr; // 可以为空
int y = 40;
ptr = &y; // 可以改变指向

// 多级指针
int** pp = &ptr; // 指向指针的指针
// int&& rr = ref; // 错误!不能有引用的引用(C++11的&&是右值引用,不同概念)

// 函数参数中的使用
void func1(int& r) {
r = 100; // 修改原变量
}

void func2(int* p) {
if (p != nullptr) { // 需要检查
*p = 100;
}
}

func1(x); // 直接传变量
func2(&x); // 需要取地址

口头解答:
“引用和指针都可以实现间接访问,但它们有本质区别。引用可以理解为变量的别名,一旦绑定就不能改变,而且必须在声明时初始化,不能为空。指针则是一个存储地址的变量,可以随时改变指向,也可以为空。在使用上,引用更安全也更简洁,因为不需要解引用操作,编译器会帮我们处理;指针则更灵活,可以进行算术运算,可以实现多级间接访问。一般来说,能用引用的地方优先用引用,需要重新绑定或可能为空的情况才用指针。”


题目3: 什么是左值和右值?C++11中的右值引用有什么作用?

解答:

  1. 左值(lvalue):有持久地址的表达式,可以取地址,可以出现在赋值号左边
  2. 右值(rvalue):临时对象或字面量,不能取地址,只能出现在赋值号右边
  3. 右值引用(&&):C++11引入,用于绑定到右值
  4. 作用
    • 实现移动语义,避免不必要的拷贝
    • 完美转发(perfect forwarding)
    • 提高性能,特别是对于临时对象
  5. 移动构造/赋值:通过”窃取”资源而非拷贝来提高效率

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
int x = 10;          // x是左值
int* p = &x; // 可以取地址
// int* p2 = &10; // 错误!10是右值,不能取地址

// 右值引用
int&& rref = 10; // 绑定到右值
int&& rref2 = x + 5; // 绑定到临时结果
// int&& rref3 = x; // 错误!x是左值

// 移动语义示例
class String {
char* data;
size_t len;
public:
// 拷贝构造:深拷贝
String(const String& s) {
len = s.len;
data = new char[len + 1];
strcpy(data, s.data);
cout << "拷贝构造\n";
}

// 移动构造:转移资源
String(String&& s) noexcept {
data = s.data; // 窃取资源
len = s.len;
s.data = nullptr; // 清空源对象
s.len = 0;
cout << "移动构造\n";
}

~String() { delete[] data; }
};

String createString() {
String s("hello");
return s; // 返回临时对象
}

String s1("world");
String s2 = s1; // 拷贝构造
String s3 = createString(); // 移动构造(临时对象)
String s4 = std::move(s1); // 显式移动

// 实际性能差异
vector<int> v1(1000000, 1);
vector<int> v2 = v1; // 拷贝:分配新内存,复制100万个int
vector<int> v3 = std::move(v1); // 移动:只复制几个指针

口头解答:
“简单来说,左值就是有名字、有持久地址的对象,比如变量;右值就是临时的、即将销毁的对象,比如函数返回的临时对象或字面量。C++11引入的右值引用是个重大特性,它允许我们区分对待临时对象。最重要的应用就是移动语义——当我们知道一个对象是临时的、马上要销毁时,与其费力地拷贝它的资源,不如直接’偷’过来用。这在处理大对象,比如vector、string时,性能提升非常明显。我们通过实现移动构造函数和移动赋值运算符来支持这个特性。”


题目4: 解释const关键字的各种用法

解答:

1. const变量:

1
2
const int a = 10;    // 常量,不可修改
// a = 20; // 错误!

2. const指针:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int x = 10, y = 20;

// 指向常量的指针:指向的内容不可变
const int* p1 = &x;
// *p1 = 30; // 错误!不能修改指向的内容
p1 = &y; // 正确!可以改变指向

// 指针常量:指针本身不可变
int* const p2 = &x;
*p2 = 30; // 正确!可以修改指向的内容
// p2 = &y; // 错误!不能改变指向

// 都不可变
const int* const p3 = &x;
// *p3 = 30; // 错误!
// p3 = &y; // 错误!

// 记忆技巧:const在*左边,指向的内容不可变;在*右边,指针本身不可变

3. const成员函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Rectangle {
int width, height;
public:
Rectangle(int w, int h) : width(w), height(h) {}

// const成员函数:不修改成员变量
int getArea() const {
return width * height; // 只读操作
// width = 10; // 错误!不能修改成员
}

void setWidth(int w) { // 非const函数
width = w;
}

// const对象只能调用const成员函数
void display() const {
cout << width << "x" << height;
}
};

const Rectangle rect(10, 20);
cout << rect.getArea(); // 正确
// rect.setWidth(15); // 错误!const对象不能调用非const函数

4. const引用:

1
2
3
4
5
6
7
8
9
10
11
void process(const string& str) {
cout << str; // 正确:可以读取
// str += "!"; // 错误:不能修改
}

string s = "hello";
process(s); // 避免拷贝,又保证不被修改

// const引用可以绑定到临时对象
const int& ref = 10; // 正确!延长临时对象生命周期
// int& ref2 = 10; // 错误!非const引用不能绑定到临时对象

5. const返回值:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Data {
string name;
public:
// 返回const引用:防止修改,避免拷贝
const string& getName() const {
return name;
}

// 返回const指针
const Data* getPtr() const {
return this;
}
};

6. mutable关键字:

1
2
3
4
5
6
7
8
9
class Counter {
mutable int accessCount; // mutable允许在const函数中修改
int value;
public:
int getValue() const {
++accessCount; // 正确!mutable成员可以修改
return value;
}
};

口头解答:
“const在C++中非常常用,主要表示’不可修改’的语义。最基础的是const变量,声明后不能改变。在指针中,const的位置很关键:const在星号左边表示指向的内容不可变,在星号右边表示指针本身不可变。在类中,const成员函数承诺不修改对象的成员变量,这样const对象也能调用这些函数。在函数参数中,const引用是个很好的实践,既避免了拷贝开销,又保证了参数不被修改。总的来说,合理使用const可以提高代码的安全性和可读性,是现代C++的重要实践。”


题目5: static关键字在C++中有哪些用法?

解答:

1. 静态局部变量:

1
2
3
4
5
6
7
8
9
10
void counter() {
static int count = 0; // 只初始化一次
count++;
cout << count << endl;
}

counter(); // 输出:1
counter(); // 输出:2
counter(); // 输出:3
// count在函数调用之间保持值

2. 静态全局变量/函数(文件作用域):

1
2
3
4
5
6
7
8
9
// file1.cpp
static int internal_var = 10; // 只在file1.cpp可见
static void helper() { // 只在file1.cpp可见
// ...
}

// file2.cpp
// extern int internal_var; // 错误!无法访问
// 避免了全局命名冲突

3. 静态成员变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Counter {
static int totalCount; // 声明(所有对象共享)
int instanceCount;
public:
Counter() {
totalCount++;
instanceCount = totalCount;
}

static int getTotalCount() { // 静态成员函数
return totalCount;
// return instanceCount; // 错误!不能访问非静态成员
}
};

// 定义(在类外)
int Counter::totalCount = 0;

// 使用
Counter c1, c2, c3;
cout << Counter::getTotalCount(); // 输出:3(通过类名调用)
cout << c1.getTotalCount(); // 也可以通过对象调用

4. 静态成员函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Math {
public:
static int add(int a, int b) { // 不依赖对象实例
return a + b;
}

static double pi() {
return 3.14159;
}
};

// 使用:不需要创建对象
int result = Math::add(10, 20);
double p = Math::pi();

5. 类内静态常量:

1
2
3
4
5
6
7
8
9
10
class Config {
public:
static const int MAX_SIZE = 100; // 可以类内初始化
static const string DEFAULT_NAME; // 需要类外定义

// C++11:constexpr静态成员
static constexpr double PI = 3.14159;
};

const string Config::DEFAULT_NAME = "default";

6. 单例模式应用:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Singleton {
static Singleton* instance;
Singleton() {} // 私有构造
public:
static Singleton* getInstance() {
if (instance == nullptr) {
instance = new Singleton();
}
return instance;
}
};

Singleton* Singleton::instance = nullptr;

完整示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class BankAccount {
static double interestRate; // 所有账户共享的利率
static int accountCount; // 账户总数
string accountNumber;
double balance;

public:
BankAccount(double initial) : balance(initial) {
accountCount++;
accountNumber = "ACC" + to_string(accountCount);
}

static void setInterestRate(double rate) {
interestRate = rate;
}

static int getAccountCount() {
return accountCount;
}

double calculateInterest() const {
return balance * interestRate; // 可以访问静态成员
}
};

double BankAccount::interestRate = 0.05;
int BankAccount::accountCount = 0;

// 使用
BankAccount::setInterestRate(0.06); // 设置全局利率
BankAccount acc1(1000), acc2(2000);
cout << BankAccount::getAccountCount(); // 2

口头解答:
“static在C++中有多种用途。在函数内部,static变量只会初始化一次,之后调用函数时会保持上次的值,这常用于计数或缓存。在类中,static成员属于整个类而不是某个对象,所有对象共享同一份数据。静态成员函数不需要对象就能调用,但也因此只能访问静态成员。另外,在全局作用域用static可以限制变量或函数的链接性,让它只在当前文件可见,这在大项目中有助于避免命名冲突。总的来说,static关键字用于控制变量的生命周期、作用域和所属关系。”


面向对象编程

题目6: 解释C++中的三大特性:封装、继承、多态

解答:

1. 封装(Encapsulation):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class BankAccount {
private:
string accountNumber; // 私有数据,外部不可访问
double balance;

public:
// 公共接口
BankAccount(string accNum, double initial)
: accountNumber(accNum), balance(initial) {}

void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}

bool withdraw(double amount) {
if (amount > 0 && balance >= amount) {
balance -= amount;
return true;
}
return false;
}

double getBalance() const {
return balance;
}
};

// 使用
BankAccount account("123456", 1000);
account.deposit(500);
// account.balance = 10000; // 错误!无法直接访问私有成员
cout << account.getBalance(); // 通过公共接口访问

2. 继承(Inheritance):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 基类
class Animal {
protected:
string name;
int age;

public:
Animal(string n, int a) : name(n), age(a) {}

void eat() {
cout << name << " is eating\n";
}

virtual void makeSound() {
cout << "Some sound\n";
}
};

// 派生类
class Dog : public Animal {
string breed;

public:
Dog(string n, int a, string b)
: Animal(n, a), breed(b) {} // 调用基类构造函数

void makeSound() override { // 重写虚函数
cout << "Woof!\n";
}

void wagTail() { // 新增方法
cout << name << " is wagging tail\n";
}
};

class Cat : public Animal {
public:
Cat(string n, int a) : Animal(n, a) {}

void makeSound() override {
cout << "Meow!\n";
}
};

// 使用
Dog dog("Buddy", 3, "Golden Retriever");
dog.eat(); // 继承自Animal
dog.makeSound(); // Dog的实现
dog.wagTail(); // Dog特有的方法

3. 多态(Polymorphism):

编译时多态(静态多态):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 函数重载
class Calculator {
public:
int add(int a, int b) {
return a + b;
}

double add(double a, double b) {
return a + b;
}

int add(int a, int b, int c) {
return a + b + c;
}
};

// 运算符重载
class Complex {
double real, imag;
public:
Complex(double r, double i) : real(r), imag(i) {}

Complex operator+(const Complex& other) {
return Complex(real + other.real, imag + other.imag);
}
};

// 模板(泛型编程)
template<typename T>
T maximum(T a, T b) {
return (a > b) ? a : b;
}

运行时多态(动态多态):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
class Shape {
public:
virtual double area() const = 0; // 纯虚函数
virtual void draw() const = 0;
virtual ~Shape() {}
};

class Circle : public Shape {
double radius;
public:
Circle(double r) : radius(r) {}

double area() const override {
return 3.14159 * radius * radius;
}

void draw() const override {
cout << "Drawing circle\n";
}
};

class Rectangle : public Shape {
double width, height;
public:
Rectangle(double w, double h) : width(w), height(h) {}

double area() const override {
return width * height;
}

void draw() const override {
cout << "Drawing rectangle\n";
}
};

// 多态的威力
void processShape(const Shape& shape) {
cout << "Area: " << shape.area() << endl;
shape.draw();
}

// 使用
Circle circle(5);
Rectangle rect(4, 6);

processShape(circle); // 调用Circle的实现
processShape(rect); // 调用Rectangle的实现

// 通过基类指针/引用实现多态
vector<Shape*> shapes;
shapes.push_back(new Circle(3));
shapes.push_back(new Rectangle(4, 5));

for (Shape* shape : shapes) {
shape->draw(); // 根据实际类型调用对应的draw
cout << "Area: " << shape->area() << endl;
}

// 清理
for (Shape* shape : shapes) {
delete shape;
}

三大特性协同工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 实际应用示例:图形编辑器
class GraphicsEditor {
vector<unique_ptr<Shape>> shapes; // 封装:私有数据

public:
void addShape(unique_ptr<Shape> shape) { // 接口
shapes.push_back(std::move(shape));
}

void renderAll() { // 多态:统一接口,不同行为
for (const auto& shape : shapes) {
shape->draw();
}
}

double totalArea() {
double total = 0;
for (const auto& shape : shapes) {
total += shape->area(); // 多态调用
}
return total;
}
};

// 使用
GraphicsEditor editor;
editor.addShape(make_unique<Circle>(5));
editor.addShape(make_unique<Rectangle>(4, 6));
editor.renderAll();

口头解答:
“面向对象的三大特性是C++的核心。封装就是把数据和方法包装在类里,通过访问控制符来保护数据,只暴露必要的接口,这样可以隐藏实现细节,提高安全性。继承让我们可以基于已有的类创建新类,实现代码复用,建立类之间的层次关系。多态是最强大的特性,它允许用统一的接口调用不同的实现。C++既支持编译时多态,比如函数重载和模板;也支持运行时多态,主要通过虚函数实现。这三个特性结合起来,让我们能够设计出灵活、可扩展、易维护的代码。”


题目7: 什么是虚函数?virtual关键字的作用是什么?

解答:

1. 虚函数的定义和作用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Animal {
public:
// 虚函数:允许派生类重写
virtual void makeSound() {
cout << "Some generic sound\n";
}

// 非虚函数:不能多态
void breathe() {
cout << "Breathing...\n";
}

virtual ~Animal() {} // 虚析构函数
};

class Dog : public Animal {
public:
void makeSound() override { // 重写虚函数
cout << "Woof!\n";
}

void breathe() { // 隐藏基类函数(不是重写)
cout << "Dog breathing\n";
}
};

// 多态演示
Animal* animal1 = new Animal();
Animal* animal2 = new Dog();

animal1->makeSound(); // 输出:Some generic sound
animal2->makeSound(); // 输出:Woof!(动态绑定)

animal1->breathe(); // 输出:Breathing...
animal2->breathe(); // 输出:Breathing...(静态绑定,调用基类版本)

delete animal1;
delete animal2;

2. 虚函数的实现原理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Base {
public:
virtual void func1() { cout << "Base::func1\n"; }
virtual void func2() { cout << "Base::func2\n"; }
void func3() { cout << "Base::func3\n"; }
};

class Derived : public Base {
public:
void func1() override { cout << "Derived::func1\n"; }
// func2未重写,使用基类版本
};

// 内存布局(简化):
// Base对象:
// +-------------------+
// | vptr (虚函数表指针) | → 指向虚函数表
// +-------------------+
//
// 虚函数表:
// +-------------------+
// | &Base::func1 |
// | &Base::func2 |
// +-------------------+

// Derived对象:
// +-------------------+
// | vptr | → 指向Derived的虚函数表
// +-------------------+
//
// Derived虚函数表:
// +-------------------+
// | &Derived::func1 | ← 被重写
// | &Base::func2 | ← 未重写,用基类版本
// +-------------------+

// 虚函数调用过程:
Base* ptr = new Derived();
ptr->func1();
// 1. 通过ptr找到对象
// 2. 读取对象的vptr
// 3. 在虚函数表中查找func1的地址
// 4. 调用找到的函数(Derived::func1)

3. 虚析构函数的重要性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class Base {
public:
Base() { cout << "Base构造\n"; }

// 如果没有virtual,会导致问题
~Base() { cout << "Base析构\n"; }
};

class Derived : public Base {
int* data;
public:
Derived() : data(new int[100]) {
cout << "Derived构造\n";
}

~Derived() {
delete[] data;
cout << "Derived析构\n";
}
};

// 问题演示
Base* ptr = new Derived();
// 输出:Base构造
// Derived构造

delete ptr; // 危险!
// 如果Base::~Base()不是virtual:
// 只输出:Base析构
// Derived的析构函数不会被调用!内存泄漏!

// 正确做法:
class BaseCorrect {
public:
virtual ~BaseCorrect() { cout << "Base析构\n"; }
};

class DerivedCorrect : public BaseCorrect {
int* data;
public:
DerivedCorrect() : data(new int[100]) {}
~DerivedCorrect() override {
delete[] data;
cout << "Derived析构\n";
}
};

BaseCorrect* ptr2 = new DerivedCorrect();
delete ptr2;
// 正确输出:Derived析构
// Base析构

4. override和final关键字(C++11):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Base {
public:
virtual void func1() {}
virtual void func2() {}
virtual void func3() final {} // final:不能被重写
};

class Derived : public Base {
public:
void func1() override {} // override:明确标记是重写

// void func2() const override {} // 错误!签名不匹配,编译器会报错

// void func3() override {} // 错误!func3是final的
};

class FinalClass final { // final类:不能被继承
// ...
};

// class CannotDerive : public FinalClass {}; // 错误!

5. 纯虚函数和抽象类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class AbstractShape {
public:
// 纯虚函数:= 0
virtual double area() const = 0;
virtual void draw() const = 0;

// 可以有普通成员函数
void printInfo() const {
cout << "Area: " << area() << endl;
}

virtual ~AbstractShape() {}
};

// AbstractShape shape; // 错误!抽象类不能实例化

class ConcreteCircle : public AbstractShape {
double radius;
public:
ConcreteCircle(double r) : radius(r) {}

// 必须实现所有纯虚函数
double area() const override {
return 3.14159 * radius * radius;
}

void draw() const override {
cout << "Drawing circle\n";
}
};

// 使用
ConcreteCircle circle(5); // 正确
AbstractShape* shape = &circle; // 多态
shape->draw();

6. 虚函数的性能开销:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class WithVirtual {
public:
virtual void func() {}
};

class WithoutVirtual {
public:
void func() {}
};

// 大小比较
cout << sizeof(WithVirtual); // 通常是8字节(64位系统,一个vptr)
cout << sizeof(WithoutVirtual); // 通常是1字节(空类优化)

// 调用开销:
// 非虚函数:直接调用,编译期确定地址
// 虚函数:通过虚函数表间接调用,运行期确定地址(略慢,但通常可忽略)

口头解答:
“虚函数是实现C++运行时多态的关键机制。当你把基类的函数声明为virtual,派生类可以重写它,这样通过基类指针或引用调用时,会根据对象的实际类型来决定调用哪个版本,而不是根据指针类型。实现原理是通过虚函数表,编译器为每个包含虚函数的类生成一个函数指针表,对象里会有个隐藏的指针指向这个表。这样运行时就能找到正确的函数。特别要注意的是,如果一个类会被继承,它的析构函数一定要是虚函数,否则通过基类指针删除派生类对象时,不会调用派生类的析构函数,导致资源泄漏。虚函数有轻微的性能开销,但换来了极大的灵活性。”


题目8: 纯虚函数和抽象类是什么?

解答:

1. 纯虚函数的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Shape {
public:
// 纯虚函数:在基类中没有实现,赋值为0
virtual double area() const = 0;
virtual double perimeter() const = 0;
virtual void draw() const = 0;

// 虚析构函数(不是纯虚)
virtual ~Shape() {}

// 可以有普通成员函数
void printArea() const {
cout << "Area: " << area() << endl; // 可以调用纯虚函数
}
};

2. 抽象类的特点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 包含至少一个纯虚函数的类是抽象类
class AbstractBase {
public:
virtual void pureVirtual() = 0; // 纯虚函数

virtual void normalVirtual() { // 普通虚函数
cout << "Normal virtual\n";
}

void nonVirtual() { // 普通函数
cout << "Non-virtual\n";
}

int data; // 可以有成员变量
};

// 抽象类不能实例化
// AbstractBase obj; // 错误!

// 但可以有指针和引用
AbstractBase* ptr; // 正确
AbstractBase& ref = ...; // 正确(需要绑定到派生类对象)

3. 实现抽象类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// 派生类必须实现所有纯虚函数才能实例化
class Circle : public Shape {
double radius;
public:
Circle(double r) : radius(r) {}

// 实现所有纯虚函数
double area() const override {
return 3.14159 * radius * radius;
}

double perimeter() const override {
return 2 * 3.14159 * radius;
}

void draw() const override {
cout << "Drawing circle with radius " << radius << endl;
}
};

class Rectangle : public Shape {
double width, height;
public:
Rectangle(double w, double h) : width(w), height(h) {}

double area() const override {
return width * height;
}

double perimeter() const override {
return 2 * (width + height);
}

void draw() const override {
cout << "Drawing rectangle " << width << "x" << height << endl;
}
};

// 使用
Circle circle(5);
Rectangle rect(4, 6);

circle.printArea(); // 使用继承的普通函数
rect.draw();

// 多态
vector<Shape*> shapes;
shapes.push_back(&circle);
shapes.push_back(&rect);

for (Shape* shape : shapes) {
shape->draw();
cout << "Perimeter: " << shape->perimeter() << endl;
}

4. 部分实现的抽象类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class PartiallyAbstract : public Shape {
public:
// 只实现部分纯虚函数
double area() const override {
return 0;
}

// perimeter和draw仍未实现
// 所以PartiallyAbstract仍然是抽象类
};

// PartiallyAbstract obj; // 错误!仍然不能实例化

class FullyImplemented : public PartiallyAbstract {
public:
// 实现剩余的纯虚函数
double perimeter() const override {
return 0;
}

void draw() const override {
cout << "Drawing\n";
}
};

FullyImplemented obj; // 正确!所有纯虚函数都已实现

5. 接口类(只有纯虚函数):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// C++中的接口设计模式
class IDrawable {
public:
virtual void draw() const = 0;
virtual ~IDrawable() {}
};

class IPrintable {
public:
virtual void print() const = 0;
virtual ~IPrintable() {}
};

// 多重继承实现多个接口
class Document : public IDrawable, public IPrintable {
string content;
public:
void draw() const override {
cout << "Drawing document\n";
}

void print() const override {
cout << "Printing: " << content << endl;
}
};

6. 纯虚函数也可以有实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Base {
public:
// 纯虚函数也可以有默认实现
virtual void func() = 0;
};

// 在类外提供实现
void Base::func() {
cout << "Default implementation\n";
}

class Derived : public Base {
public:
void func() override {
Base::func(); // 可以调用基类的实现
cout << "Derived implementation\n";
}
};

Derived d;
d.func();
// 输出:Default implementation
// Derived implementation

7. 实际应用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 数据库访问层抽象
class IDatabase {
public:
virtual bool connect(const string& connectionString) = 0;
virtual bool execute(const string& query) = 0;
virtual void disconnect() = 0;
virtual ~IDatabase() {}
};

class MySQLDatabase : public IDatabase {
public:
bool connect(const string& connectionString) override {
cout << "Connecting to MySQL...\n";
return true;
}

bool execute(const string& query) override {
cout << "Executing MySQL query: " << query << endl;
return true;
}

void disconnect() override {
cout << "Disconnecting from MySQL\n";
}
};

class PostgreSQLDatabase : public IDatabase {
public:
bool connect(const string& connectionString) override {
cout << "Connecting to PostgreSQL...\n";
return true;
}

bool execute(const string& query) override {
cout << "Executing PostgreSQL query: " << query << endl;
return true;
}

void disconnect() override {
cout << "Disconnecting from PostgreSQL\n";
}
};

// 使用
void performDatabaseOperation(IDatabase& db) {
db.connect("server=localhost");
db.execute("SELECT * FROM users");
db.disconnect();
}

MySQLDatabase mysql;
PostgreSQLDatabase postgres;

performDatabaseOperation(mysql); // 使用MySQL
performDatabaseOperation(postgres); // 使用PostgreSQL

口头解答:
“纯虚函数就是在声明时赋值为0的虚函数,它在基类中没有实现,强制要求派生类必须提供实现。包含纯虚函数的类就是抽象类,抽象类不能直接创建对象,只能用作基类。这个机制在C++中用来定义接口,规定派生类必须实现哪些功能。比如我们定义一个Shape抽象类,规定所有形状都必须实现计算面积和绘制的功能,具体的Circle、Rectangle类就必须提供这些实现。这样可以确保接口的统一性,也体现了设计模式中的依赖倒置原则。虽然纯虚函数通常没有实现,但C++允许为它提供默认实现,派生类可以选择调用。”


题目9: 构造函数和析构函数的调用顺序是怎样的?

解答:

1. 单个类的构造和析构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Widget {
int* data;
public:
Widget() {
cout << "Widget构造\n";
data = new int[10];
}

~Widget() {
cout << "Widget析构\n";
delete[] data;
}
};

Widget w;
// 输出:Widget构造
// 程序结束时输出:Widget析构

2. 有成员对象的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Member {
public:
Member() { cout << "Member构造\n"; }
~Member() { cout << "Member析构\n"; }
};

class Container {
Member m1;
Member m2;
public:
Container() {
cout << "Container构造\n";
}

~Container() {
cout << "Container析构\n";
}
};

Container c;
// 输出:
// Member构造 (m1)
// Member构造 (m2)
// Container构造
//
// 析构时(相反顺序):
// Container析构
// Member析构 (m2)
// Member析构 (m1)

3. 继承关系的构造和析构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Base {
public:
Base() { cout << "Base构造\n"; }
~Base() { cout << "Base析构\n"; }
};

class Derived : public Base {
public:
Derived() { cout << "Derived构造\n"; }
~Derived() { cout << "Derived析构\n"; }
};

Derived d;
// 输出:
// Base构造
// Derived构造
//
// 析构时:
// Derived析构
// Base析构

4. 复杂的继承+成员对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Base {
public:
Base() { cout << "Base构造\n"; }
~Base() { cout << "Base析构\n"; }
};

class Member {
public:
Member() { cout << "Member构造\n"; }
~Member() { cout << "Member析构\n"; }
};

class Derived : public Base {
Member m;
public:
Derived() { cout << "Derived构造\n"; }
~Derived() { cout << "Derived析构\n"; }
};

Derived d;
// 输出:
// Base构造 1. 先构造基类
// Member构造 2. 再构造成员对象
// Derived构造 3. 最后执行派生类构造函数体
//
// 析构时(完全相反):
// Derived析构 1. 先执行派生类析构函数体
// Member析构 2. 再析构成员对象
// Base析构 3. 最后析构基类

5. 多重继承:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Base1 {
public:
Base1() { cout << "Base1构造\n"; }
~Base1() { cout << "Base1析构\n"; }
};

class Base2 {
public:
Base2() { cout << "Base2构造\n"; }
~Base2() { cout << "Base2析构\n"; }
};

class Derived : public Base1, public Base2 {
public:
Derived() { cout << "Derived构造\n"; }
~Derived() { cout << "Derived析构\n"; }
};

Derived d;
// 输出(按声明顺序):
// Base1构造
// Base2构造
// Derived构造
//
// 析构时(相反):
// Derived析构
// Base2析构
// Base1析构

6. 虚继承:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Virtual {
public:
Virtual() { cout << "Virtual构造\n"; }
~Virtual() { cout << "Virtual析构\n"; }
};

class Base1 : virtual public Virtual {
public:
Base1() { cout << "Base1构造\n"; }
~Base1() { cout << "Base1析构\n"; }
};

class Base2 : virtual public Virtual {
public:
Base2() { cout << "Base2构造\n"; }
~Base2() { cout << "Base2析构\n"; }
};

class Derived : public Base1, public Base2 {
public:
Derived() { cout << "Derived构造\n"; }
~Derived() { cout << "Derived析构\n"; }
};

Derived d;
// 输出:
// Virtual构造 虚基类最先构造!
// Base1构造
// Base2构造
// Derived构造
//
// 析构时:
// Derived析构
// Base2析构
// Base1析构
// Virtual析构 虚基类最后析构

7. 初始化列表的顺序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Example {
int a;
int b;
int c;
public:
// 初始化顺序由成员声明顺序决定,不是初始化列表顺序!
Example(int x) : c(x), b(c), a(b) {
// 实际顺序:a(b), b(c), c(x)
// 问题:a用未初始化的b初始化!
}

// 正确做法:按声明顺序初始化
Example(int x) : a(x), b(x), c(x) {}
};

8. 静态成员的初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class WithStatic {
static int staticMember;
int normalMember;
public:
WithStatic() {
cout << "WithStatic构造\n";
}
};

int WithStatic::staticMember = [](){
cout << "静态成员初始化\n";
return 42;
}();

// 程序启动时:
// 静态成员初始化
//
// 创建对象时:
// WithStatic构造

9. 完整示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class Resource {
public:
Resource() { cout << "Resource构造\n"; }
~Resource() { cout << "Resource析构\n"; }
};

class Base {
protected:
Resource baseRes;
public:
Base() { cout << "Base构造\n"; }
virtual ~Base() { cout << "Base析构\n"; }
};

class Member {
public:
Member() { cout << "Member构造\n"; }
~Member() { cout << "Member析构\n"; }
};

class Derived : public Base {
Member m1;
Member m2;
public:
Derived() { cout << "Derived构造\n"; }
~Derived() override { cout << "Derived析构\n"; }
};

void test() {
cout << "创建对象...\n";
Derived d;
cout << "对象使用中...\n";
}

test();

// 完整输出:
// 创建对象...
// Resource构造 (Base的成员baseRes)
// Base构造
// Member构造 (Derived的成员m1)
// Member构造 (Derived的成员m2)
// Derived构造
// 对象使用中...
// Derived析构
// Member析构 (m2)
// Member析构 (m1)
// Base析构
// Resource析构 (baseRes)

口头解答:
“这个顺序很重要,记住一个原则:构造从根到叶,析构从叶到根,严格相反。对于单个对象,先构造成员变量,再执行构造函数体;析构时相反。在继承中,先构造基类,保证基类的部分准备好了,派生类才能在此基础上构造;然后是派生类的成员;最后执行派生类构造函数体。析构时完全倒过来。虚继承是个特例,虚基类由最底层的派生类负责构造,所以虚基类最先构造,最后析构。还要注意,成员的初始化顺序由它们在类中的声明顺序决定,不是初始化列表的顺序。理解这个顺序对于避免资源管理问题很关键。”


题目10: 什么是拷贝构造函数?深拷贝和浅拷贝的区别?

解答:

1. 拷贝构造函数的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class String {
char* data;
size_t length;
public:
// 拷贝构造函数
String(const String& other) {
length = other.length;
data = new char[length + 1];
strcpy(data, other.data);
cout << "拷贝构造\n";
}

// 其他构造函数
String(const char* str) {
length = strlen(str);
data = new char[length + 1];
strcpy(data, str);
}

~String() {
delete[] data;
}
};

// 何时调用拷贝构造?
String s1("hello");
String s2 = s1; // 1. 用对象初始化新对象
String s3(s1); // 2. 直接调用

void func(String s) {} // 3. 按值传参
func(s1);

String getStr() {
String temp("world");
return temp; // 4. 返回对象(可能,取决于优化)
}
String s4 = getStr();

2. 浅拷贝的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class ShallowCopy {
int* data;
public:
ShallowCopy(int value) {
data = new int(value);
}

// 编译器默认生成的拷贝构造(浅拷贝)
// ShallowCopy(const ShallowCopy& other) : data(other.data) {}
// 只拷贝指针的值!

~ShallowCopy() {
delete data;
}
};

// 问题演示
ShallowCopy obj1(10);
ShallowCopy obj2 = obj1; // 浅拷贝:obj1.data和obj2.data指向同一块内存

// obj1和obj2析构时都会delete同一块内存 → 崩溃!
// 而且修改obj2会影响obj1:
*obj2.data = 20;
cout << *obj1.data; // 输出20(意外的修改!)

3. 深拷贝的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class DeepCopy {
int* data;
size_t size;
public:
DeepCopy(int value, size_t s = 1) : size(s) {
data = new int[size];
for (size_t i = 0; i < size; ++i) {
data[i] = value;
}
}

// 深拷贝:复制指针指向的内容
DeepCopy(const DeepCopy& other) : size(other.size) {
data = new int[size]; // 分配新内存
for (size_t i = 0; i < size; ++i) {
data[i] = other.data[i]; // 复制内容
}
cout << "深拷贝\n";
}

// 拷贝赋值运算符(也需要深拷贝)
DeepCopy& operator=(const DeepCopy& other) {
if (this != &other) { // 检查自赋值
delete[] data; // 释放旧资源

size = other.size;
data = new int[size]; // 分配新资源
for (size_t i = 0; i < size; ++i) {
data[i] = other.data[i];
}
}
return *this;
}

~DeepCopy() {
delete[] data;
}

void setValue(size_t index, int value) {
if (index < size) data[index] = value;
}

int getValue(size_t index) const {
return (index < size) ? data[index] : 0;
}
};

// 使用深拷贝
DeepCopy obj1(10, 5);
DeepCopy obj2 = obj1; // 深拷贝:obj2有自己的data

obj2.setValue(0, 99);
cout << obj1.getValue(0); // 输出10(obj1未受影响)
cout << obj2.getValue(0); // 输出99

// 析构时安全:各自释放各自的内存

4. 三法则(Rule of Three):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 如果需要自定义以下三个之一,通常需要自定义全部三个:
class Resource {
int* data;
public:
// 1. 析构函数
~Resource() {
delete data;
}

// 2. 拷贝构造函数
Resource(const Resource& other) {
data = new int(*other.data);
}

// 3. 拷贝赋值运算符
Resource& operator=(const Resource& other) {
if (this != &other) {
delete data;
data = new int(*other.data);
}
return *this;
}
};

5. 五法则(Rule of Five,C++11):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class ModernResource {
int* data;
public:
// 三法则的三个
~ModernResource() { delete data; }

ModernResource(const ModernResource& other) {
data = new int(*other.data);
}

ModernResource& operator=(const ModernResource& other) {
if (this != &other) {
delete data;
data = new int(*other.data);
}
return *this;
}

// C++11新增:移动构造和移动赋值
ModernResource(ModernResource&& other) noexcept {
data = other.data;
other.data = nullptr; // 转移所有权
}

ModernResource& operator=(ModernResource&& other) noexcept {
if (this != &other) {
delete data;
data = other.data;
other.data = nullptr;
}
return *this;
}
};

6. 禁止拷贝:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 某些类不应该被拷贝(如互斥锁、文件句柄)
class NoCopy {
public:
NoCopy() = default;

// C++11方式:删除拷贝操作
NoCopy(const NoCopy&) = delete;
NoCopy& operator=(const NoCopy&) = delete;

// 也可以删除移动操作
// NoCopy(NoCopy&&) = delete;
// NoCopy& operator=(NoCopy&&) = delete;
};

// C++03方式:声明为私有且不实现
class NoCopyOld {
private:
NoCopyOld(const NoCopyOld&);
NoCopyOld& operator=(const NoCopyOld&);
public:
NoCopyOld() {}
};

7. 实际案例对比:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 浅拷贝导致的问题
class BadVector {
int* arr;
size_t sz;
public:
BadVector(size_t size) : sz(size), arr(new int[size]) {}
// 使用默认拷贝构造(浅拷贝)
~BadVector() { delete[] arr; }
};

void problem() {
BadVector v1(100);
BadVector v2 = v1; // 浅拷贝
} // 崩溃!两次delete同一内存

// 深拷贝的正确实现
class GoodVector {
int* arr;
size_t sz;
public:
GoodVector(size_t size) : sz(size), arr(new int[size]) {}

GoodVector(const GoodVector& other) : sz(other.sz) {
arr = new int[sz];
memcpy(arr, other.arr, sz * sizeof(int));
}

GoodVector& operator=(const GoodVector& other) {
if (this != &other) {
int* new_arr = new int[other.sz]; // 先分配新的
memcpy(new_arr, other.arr, other.sz * sizeof(int));
delete[] arr; // 再释放旧的(异常安全)
arr = new_arr;
sz = other.sz;
}
return *this;
}

~GoodVector() { delete[] arr; }
};

void safe() {
GoodVector v1(100);
GoodVector v2 = v1; // 深拷贝,安全
} // 正常析构

口头解答:
“拷贝构造函数在用一个对象初始化另一个对象时调用。浅拷贝和深拷贝的区别主要体现在对指针成员的处理上。浅拷贝只拷贝指针的值,结果是两个对象的指针指向同一块内存,这在析构时会出大问题——同一块内存被释放两次,程序会崩溃。深拷贝则是为新对象分配新的内存,把内容复制过去,保证每个对象有自己独立的资源。如果类里有指针、文件句柄等资源,一定要实现深拷贝。这也是’三法则’的由来:如果需要自定义析构函数,通常也需要自定义拷贝构造函数和拷贝赋值运算符。C++11又加入了移动操作,变成了’五法则’。现代C++推荐用智能指针来自动管理资源,避免手动处理这些问题。”


第一部分总结:

本部分涵盖了C++的基础知识(题目1-5)和面向对象编程(题目6-10)的核心概念:

  • new vs malloc、引用vs指针等基本概念
  • const和static关键字的多种用法
  • 面向对象三大特性的实现
  • 虚函数和多态机制
  • 构造析构顺序和拷贝语义

这些是C++面试的高频考点,理解这些概念对后续学习更高级的特性至关重要。

下一部分预告:
第二部分(题目11-20)将涵盖内存管理和STL容器,包括内存分区、RAII、vector实现、迭代器等内容。