1、C++高频面试真题|1-10
C++ 面试真题(1-10)
1. C 和 C++ 之间的区别是什么?
C 和 C++ 是两种广泛使用的编程语言,二者既有联系也存在诸多区别.
语言范式
C 语言:是一种面向过程的编程语言。它主要关注解决问题的步骤和过程,通过函数将不同的任务模块化。
程序的执行流程是按照函数的调用顺序依次进行。
比如,我们编写一个计算两个数之和的程序,会定义一个求和函数,然后在主函数中调用该函数完成计算。
include <stdio.h>
// 定义求和函数
int add(int a, int b) {
return a + b;
}
int main() {
int num1 = 3, num2 = 5;
int result = add(num1, num2);
printf("两数之和为: %d\n", result);
return 0;
}
C++ 语言:是一种多范式编程语言,支持面向过程、面向对象和泛型编程。面向对象编程是 C++ 的重要特性,它将数据和操作数据的方法封装在类中,通过类的实例(对象)来实现程序的功能。
泛型编程则允许编写与数据类型无关的代码,提高代码的复用性。例如,使用 C++ 的类来实现上述求和功能:
#include <iostream>
// 定义一个求和类
class Adder {
public:
int add(int a, int b) {
return a + b;
}
};
int main() {
Adder adder;
int num1 = 3, num2 = 5;
int result = adder.add(num1, num2);
std::cout << "两数之和为: " << result << std::endl;
return 0;
}
数据抽象和封装
C 语言:没有内置的机制来实现数据抽象和封装。虽然可以使用结构体来组织数据,但结构体中的成员通常是公开的,外部代码可以直接访问和修改这些成员,这可能导致数据的不一致性和安全性问题。
#include <stdio.h>
// 定义一个结构体
struct Point {
int x;
int y;
};
int main() {
struct Point p;
p.x = 10;
p.y = 20;
printf("点的坐标为: (%d, %d)\n", p.x, p.y);
return 0;
}
C++ 语言:通过类和访问修饰符(如 private
、protected
、public
)实现了数据抽象和封装。类中的私有成员只能通过类的公有成员函数来访问和修改,从而隐藏了数据的实现细节。
#include <iostream>
// 定义一个类
class Point {
private:
int x;
int y;
public:
void setCoordinates(int a, int b) {
x = a;
y = b;
}
void printCoordinates() {
std::cout << "点的坐标为: (" << x << ", " << y << ")" << std::endl;
}
};
int main() {
Point p;
p.setCoordinates(10, 20);
p.printCoordinates();
return 0;
}
继承和多态
- C 语言:不支持继承和多态的概念。继承是指一个类可以继承另一个类的属性和方法,多态是指不同的对象可以对同一消息做出不同的响应。
- C++ 语言:支持继承和多态。通过继承,子类可以复用父类的代码,并且可以添加自己的特性。多态可以通过虚函数和指针或引用实现,使得程序在运行时能够根据对象的实际类型来调用相应的函数。
#include <iostream>
// 定义基类
class Shape {
public:
virtual void draw() {
std::cout << "绘制形状" << std::endl;
}
};
// 定义派生类
class Circle : public Shape {
public:
void draw() override {
std::cout << "绘制圆形" << std::endl;
}
};
int main() {
Shape* shape = new Circle();
shape->draw();
delete shape;
return 0;
}
标准库
- C 语言:标准库主要提供了一些基本的输入输出、字符串处理、数学运算等函数,如
printf
、scanf
、strlen
、sqrt
等。这些函数以头文件的形式提供,使用时需要包含相应的头文件。
#include <stdio.h>
#include <math.h>
int main() {
double num = 16.0;
double result = sqrt(num);
printf("16的平方根是: %f\n", result);
return 0;
}
- C++ 语言:除了兼容 C 语言的标准库外,还拥有自己的标准模板库(STL)。STL 提供了丰富的容器(如
vector
、list
、map
等)、算法(如sort
、find
等)和迭代器,大大提高了开发效率。
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> numbers = {3, 1, 4, 1, 5, 9};
std::sort(numbers.begin(), numbers.end());
for (int num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
错误处理
- C 语言:通常使用返回错误码的方式来处理错误。函数在执行过程中如果出现错误,会返回一个特定的错误码,调用者需要根据这个错误码来判断函数的执行情况。这种方式比较繁琐,容易出错。
#include <stdio.h>
#include <errno.h>
int divide(int a, int b, int* result) {
if (b == 0) {
errno = EDOM;
return -1;
}
*result = a / b;
return 0;
}
int main() {
int num1 = 10, num2 = 0;
int result;
if (divide(num1, num2, &result) == -1) {
perror("除法运算出错");
} else {
printf("结果为: %d\n", result);
}
return 0;
}
- C++ 语言:引入了异常处理机制,使用
try
、catch
和throw
关键字来处理异常。当程序出现异常时,可以抛出一个异常对象,调用者可以在catch
块中捕获并处理这个异常,使错误处理更加清晰和灵活。(但,平时编程中,还是建议使用错误码方式)
#include <iostream>
int divide(int a, int b) {
if (b == 0) {
throw std::runtime_error("除数不能为零");
}
return a / b;
}
int main() {
int num1 = 10, num2 = 0;
try {
int result = divide(num1, num2);
std::cout << "结果为: " << result << std::endl;
} catch (const std::runtime_error& e) {
std::cerr << "错误: " << e.what() << std::endl;
}
return 0;
}
C 与 C++ 核心区别对比
对比维度 | C 语言 | C++ 语言 |
语言范式 | 纯面向过程 | 多范式(面向过程+面向对象+泛型) |
代码组织 | 函数为核心 | 类(Class)为核心 |
数据安全 | 结构体成员默认公开 | 支持 `private`/`protected` 封装 |
继承/多态 | ❌ 不支持 | ✅ 支持(虚函数、`override`) |
内存管理 | `malloc`/`free` | `new`/`delete` + 智能指针 |
标准库 | 基础库(`stdio.h`, `math.h`) | STL(容器、算法、迭代器) |
错误处理 | 返回错误码(如 `-1`) | 异常机制(`try`/`catch`) |
函数特性 | 无重载、无默认参数 | 函数重载、默认参数、Lambda |
编译方式 | 直接编译 | 支持模板元编程 |
典型用途 | 嵌入式、操作系统底层 | 游戏、高性能应用、大型软件 |
2. C++ 中有哪些不同的数据类型?
基本数据类型
整数类型:用于存储整数数值,有不同的长度和符号属性。
int
:最常用的整数类型,一般占 32 位,可存储范围约为 -21 亿到 21 亿,适合存储日常使用的整数。short
:通常为 16 位,存储范围较小,适用于存储数值范围明确且较小的整数,能节省内存。long
:一般为 32 位或 64 位,存储范围比int
更大,用于存储较大的整数。long long
:通常占 64 位,可存储非常大的整数,适用于处理大数值计算。- 这些类型都有对应的无符号版本(如
unsigned int
),只能存储非负整数,存储的正数范围比有符号类型更大。
浮点类型:用于存储小数。
float
:单精度浮点型,占 32 位,精度相对较低,适用于对精度要求不高的计算场景。double
:双精度浮点型,占 64 位,精度比float
高,是处理浮点运算时更常用的类型。long double
:扩展精度浮点型,精度更高,占用位数因编译器而异,用于对精度要求极高的科学计算等场景。
字符类型:用于存储单个字符。
char
:通常占 1 个字节,可表示 ASCII 字符集里的字符,是最常用的字符类型。wchar_t
:宽字符类型,用于表示宽字符集,如 Unicode 字符,可处理多种语言的字符。char16_t
和char32_t
:分别用于表示 16 位和 32 位的字符,常用于处理 UTF - 16 和 UTF - 32 编码的字符。
布尔类型:只有两个取值,
true
(真)和false
(假),常用于逻辑判断。bool
:占一个字节,让代码中的逻辑表达更清晰。
派生数据类型
数组:由相同类型元素组成的集合,元素存储在连续的内存位置。
- 例如:
int arr[5];
定义了一个包含 5 个整数的数组,可通过下标访问每个元素。
- 例如:
指针:存储变量的内存地址,通过指针可以间接访问和操作内存中的数据。
- 例如:
int* ptr;
定义了一个指向整数的指针,可用于动态内存分配和操作。
- 例如:
引用:变量的别名,对引用的操作等同于对被引用变量的操作。
- 例如:
int num = 10; int& ref = num;
这里ref
就是num
的引用,改变ref
的值,num
的值也会改变。
- 例如:
函数:实现特定功能的代码块,可通过参数传递数据,执行相应操作并返回结果。
- 例如:
int add(int a, int b) { return a + b; }
定义了一个返回两个整数之和的函数。
- 例如:
用户自定义数据类型
- 结构体:可以包含不同类型的数据成员,将相关的数据组织在一起。
- 例如:
struct Person {
std::string name;
int age;
};
这里定义了一个`Person`结构体,包含姓名和年龄两个成员。
- 类:面向对象编程的核心,类中可包含数据成员和成员函数,用于实现数据的封装和操作。
- 例如:
class Rectangle {
private:
int length;
int width;
public:
Rectangle(int l, int w) : length(l), width(w) {}
int area() { return length * width; }
};
定义了一个`Rectangle`类,有长和宽两个私有成员,以及计算面积的成员函数。
- 枚举:定义了一组命名的整数常量,使代码更具可读性。
- 例如:
enum Color { RED, GREEN, BLUE };
定义了一个`Color`枚举,包含红、绿、蓝三种颜色的常量。
3. C++ 中的引用是什么?
在 C++ 中,引用是为另一个变量创建别名的另一种方式。
引用作为变量的同义词,允许你直接访问变量而不需要任何额外的语法。
它们必须在创建时初始化,并且之后不能改变以引用另一个变量。这个特性使得在函数中操作变量时避免了复制大对象的开销。
引用变量前面有一个 &
符号。
语法:
int tmp = 10;
// 引用变量
int& ref = tmp;
4. C++ 中值传递和引用传递的区别?
回答重点
1)值传递:在函数调用时,会触发一次参数的拷贝动作,所以对参数的修改不会影响原始的值。如果是较大的对象,复制整个对象,效率较低。
2)引用传递:函数调用时,函数接收的就是参数的引用,不会触发参数的拷贝动作,效率较高,但对参数的修改会直接作用于原始的值。
扩展知识
看两种传递方式的示例代码:
值传递
void modify_value(int value) {
value = 100; // 只会修改函数内部的副本,不会影响原始变量
}
int main() {
int a = 20;
modify_value(a);
std::cout << a; // 20,没变
return 0;
}
引用传递
void modify_value(int& value) {
value = 100; // 修改引用指向的原始变量
}
int main() {
int a = 20;
modify_value(a);
std::cout << a; // 100,因为是引用传递,所以这里已经改为了100
return 0;
}
深入理解
######## 什么场景下使用引用传递?
- 避免不必要的数据拷贝:对于比较大的对象参数(比如 std::vector、std::string、std::list),因为拷贝会导致大量的内存和时间开销。而引用传递可以避免这些开销。
- 允许函数修改实参原始值:有时候,我们就是希望函数能够直接修改传入的变量值,这时使用引用传递很合理。
######## 什么场景下使用值传递?
- 小型数据结构:对于 int、char、double、float 这种基础数据类型,可以直接简单的使用值传递。
- 不希望函数修改实参:有时候,我们需要修改变量数据,但是又不希望修改原始值,可以考虑使用值传递。
值定义和引用定义
栈和链表,是在栈上分配内存还是堆上分配内存呢?两种情况都有可能,如果我们是在函数内直接创建的话,则是栈分配内存,如果是通过指针创建的话,则是在堆上分配内存,并且需要手动删除
栈上分配
#include<iostream>
int main() {
int arr[5] = {1, 2, 3, 4, 5}; // 在栈上分配一个包含 5 个整数的数组
for (int i = 0; i < 5; ++i) {
std::cout<< arr[i] << " ";
}
return 0;
}
堆上分配
#include<iostream>
int main() {
int* arr = new int[5]{1, 2, 3, 4, 5}; // 在堆上分配一个包含 5 个整数的数组
for (int i = 0; i < 5; ++i) {
std::cout<< arr[i] << " ";
}
delete[] arr; // 释放堆上分配的数组内存
return 0;
}
当然链表也可以这样,无论是结构体还是类,都有两种定义方式,值定义和引用定义,值定义则是栈分配内存,引用定义则是堆分配内存。
#include<iostream>
struct MyStruct {
int x;
};
class MyClass {
public:
int x;
};
int main() {
// 结构体的值定义
MyStruct structVar1{42};
// 结构体的引用定义
MyStruct* structVar2 = new MyStruct{42};
// 类的值定义
MyClass classVar1{42};
// 类的引用定义
MyClass* classVar2 = new MyClass{42};
// 释放堆上分配的内存
delete structVar2;
delete classVar2;
return 0;
}
C++ 值传递 vs 引用传递对比
对比维度 | 值传递 | 引用传递 |
定义 | 传递参数的副本 | 传递参数的别名(原始变量的引用) |
语法 | `void func(Type param)` | `void func(Type& param)` |
内存开销 | 复制整个对象(可能高开销) | 仅传递地址(固定大小,低开销) |
修改影响 | 不影响原始值 | 直接修改原始值 |
性能 | 低效(大型对象拷贝耗时) | 高效(无拷贝操作) |
安全性 | 高(隔离原始数据) | 低(可能意外修改外部变量) |
5. 🌟C++ 中 static 的作用?什么场景下用到 static?
谈到 C++ 的 static
,可以重点回答以下几个方面:
1)修饰局部变量:当 static
用于修饰局部变量时,这个变量的存储位置会在程序执行期间保持不变,且只在程序执行到该变量的声明处时初始化一次。
即使函数被多次调用,static
局部变量也只在第一次调用时初始化,之后的调用将不会重新初始化它。
2)修饰全局变量或函数:当 static
用于修饰全局变量或函数时,限制了这些变量或函数的作用域,它们只能在定义它们的文件内部访问。有助于避免在不同文件之间的命名冲突。
3)修饰类的成员变量或函数:在类内部,static
成员变量或函数属于类本身,而不是类的任何特定对象。这意味着所有对象共享同一个 static
成员变量,无需每个对象都存储一份拷贝。static
成员函数可以在没有类实例的情况下调用。
static 的用法
static
局部变量
#include <iostream>
using namespace std;
void func() {
static int count = 0; // 只在第一次调用func时初始化
cout << "Count is: " << count << endl;
count++;
}
int main() {
for(int i = 0; i < 5; i++) {
func(); // 每次调用都会显示增加的count值
}
return 0;
}
static
全局变量或函数
// file1.cpp
static int count = 10; // count变量只能在file1.cpp中访问
static void func() { // func函数只能在file1.cpp中访问
cout << "Function in file1" << endl;
}
// file2.cpp
extern int count; // 这里会导致编译错误,因为count是static的,不能在file2.cpp中访问
void anotherFunc() {
func(); // 这里也会导致编译错误,因为func是static的,不能在file2.cpp中访问
}
static
类的成员变量或函数
#include <iostream>
using namespace std;
class MyClass {
public:
static int staticValue; // 静态成员变量
static void staticFunction() { // 静态成员函数
cout << "Static function called" << endl;
}
};
int MyClass::staticValue = 10; // 静态成员变量的初始化
int main() {
MyClass::staticFunction(); // 调用静态成员函数
cout << MyClass::staticValue << endl; // 访问静态成员变量
return 0;
}
使用场景总结
static
局部变量:当你需要在函数的多次调用之间保持某个变量的值时。static
全局变量或函数:当你想要限制变量或函数的作用域,防止它们在其他文件中被访问时。static
类的成员变量或函数:当你想要类的所有对象共享某个变量或函数时,或者当你想要在没有类实例的情况下访问某个函数时。
作用范围 | 行为特性 | 线程安全 |
局部变量 | 生命周期延长(程序全程),作用域不变 | C++11后初始化线程安全,但多线程访问需同步 |
全局变量/函数 | 限制作用域为当前文件(内部链接性) | 初始化线程安全(main前完成),多线程访问需同步 |
类成员 | 属于类而非对象,所有实例共享 可通过类名直接访问 | 初始化线程安全(首次访问时),多线程访问需同步 |
static 线程安全吗
static
局部变量在 C++11 及之后的初始化是线程安全的,在 C++11 之前非线程安全,需要手动加锁。
static
的全局变量在程序启动前(main
之前)完成的,默认就是线程安全的。
而无论初始化是否安全,多线程访问 static
变量都不是线程安全的,都需要显式的处理同步问题。
6. 🌟C++ 中 const 的作用?谈谈你对 const 的理解?
回答重点
const
最主要的作用就是声明一个变量为常量,即这个变量的值在初始化之后就不能被修改。
但 const
不仅可以用作普通常量,还可以用于指针、引用、成员函数、成员变量等。
具体作用如下:
1)定义普通常量:当修饰基本数据类型的变量时,表示常量含义,对应的值不能被修改。
const int MAX_SIZE = 100; // MAX_SIZE是一个常量,其值不能被修改
2)修饰指针:这里分多种情况,比如指针本身是常量,指针指向的数据是常量,或者指针本身和其指向的数据都是常量。
3)修饰引用:const
修饰引用时,一般用作函数参数,表示函数不会修改传递的参数值。
void func(const int& a) { // a是一个对常量的引用,不能通过a修改其值
// ...
}
4)修饰类成员函数:const
修饰成员函数,表示函数不会修改类的任何成员变量,除非这些成员变量被声明为 mutable
。
class MyClass {
public:
void myFunc() const { // myFunc是一个const成员函数,它不会修改类的任何成员变量
// ...
}
};
5)修饰类成员变量:const
修饰成员变量,表示生命期内不可改动此值。
class MyClass {
public:
const int a = 5;
};
扩展知识
const
修饰指针时,可以分多种情况:
1)指向常量的指针:指针指向的内容是常量,不能通过该指针修改其所指向的值。
const intptr = &x; // ptr是一个指向常量的指针,不能通过ptr修改x的值
2)指针常量:指针本身是常量,指针的值(即指向的地址)不能被修改,但可以通过该指针修改其所指向的值(如果所指向的不是常量)。
int* const ptr = &x; // ptr是一个常量,ptr的值(地址)不能被修改,但x的值可以被修改
3)指向常量的常量指针:指针本身是常量,且指针指向的内容也是常量。
const int* const ptr = &x; // ptr的值和ptr指向的值都不能被修改
7. 解释 C++ 中的构造函数。
C++ 中的构造函数是一种特殊的成员函数,与类同名,无返回类型,用于在创建对象时,对对象进行初始化,有默认、带参、拷贝等多种类型,保证对象创建后处于有效状态。
8. C++ 中的析构函数是什么?
析构函数是 C++ 里的一种特殊成员函数,它和类同名,不过前面得加个波浪线 ~
,没有返回值,也不能有参数。
它的主要作用是在对象的生命周期结束时,做一些清理工作,像释放对象占用的动态内存、关闭打开的文件之类的。
当对象离开作用域或者用 delete
删除动态分配的对象时,析构函数就会自动被调用,保证资源能被正确释放,避免内存泄漏等问题。就好比你离开房间的时候,要把房间里的东西收拾好,把灯关掉一样。
9. 什么是虚析构函数?
虚析构函数就是在基类里用 virtual
关键字声明的析构函数。当用基类指针指向派生类对象,且通过该指针删除对象时,若基类析构函数不是虚拟的,那就只会调用基类析构函数,派生类析构函数不会被调用,可能造成资源泄漏;但如果基类析构函数是虚拟的,程序在运行时会先调用派生类析构函数,再调用基类析构函数,确保派生类对象能被完整地销毁。
10. 析构函数重载是否可能?如果可以,那么解释;如果不可以,那么为什么?
答案是不可以,我们不能重载析构函数。每个类中只能有一个析构函数。同样需要提到的是,析构函数既不接受参数,也没有可能帮助重载的参数。
