1、C++高频面试真题|1-10

厨子大约 20 分钟编程语言原创技术面试题解析C++面试程序员求职程序厨

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++ 语言:通过类和访问修饰符(如 privateprotectedpublic)实现了数据抽象和封装。类中的私有成员只能通过类的公有成员函数来访问和修改,从而隐藏了数据的实现细节。

#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 语言:标准库主要提供了一些基本的输入输出、字符串处理、数学运算等函数,如 printfscanfstrlensqrt 等。这些函数以头文件的形式提供,使用时需要包含相应的头文件。
#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 提供了丰富的容器(如 vectorlistmap 等)、算法(如 sortfind 等)和迭代器,大大提高了开发效率。
#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++ 语言:引入了异常处理机制,使用 trycatchthrow 关键字来处理异常。当程序出现异常时,可以抛出一个异常对象,调用者可以在 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_tchar32_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 的用法
  1. 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;
}
  1. 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中访问
}
  1. 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. 析构函数重载是否可能?如果可以,那么解释;如果不可以,那么为什么?

答案是不可以,我们不能重载析构函数。每个类中只能有一个析构函数。同样需要提到的是,析构函数既不接受参数,也没有可能帮助重载的参数。