2、C++高频面试真题|11-20

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

C++ 面试真题(11-20)

11. 🌟C++ 中都有哪些构造函数?

回答重点

  • 默认构造函数:这就是普通的构造函数,如果没有构建的话,则会默认帮你创建
  • 参数构造函数:带有参数的构造函数,允许创建对象时传递参数,以便根据这些参数初始化对象
  • 拷贝构造函数:拷贝构造函数用于通过已有对象初始化新对象,接收一个同类型对象的引用作为参数,可以理解为拷贝了一个情况相同的对象,但是分别位于不同的内存块,修改新对象的状态不会影响源对象,反之亦然。
  • 委托构造函数:允许在一个构造函数内部调用另一个构造函数,从而减少代码重复,这通过在构造函数的初始化列表中使用 this 指针调用另一个构造函数来实现

扩展知识

直接看代码,方便我们理解:

#include<iostream>
class MyClass {
public:
    // 默认构造函数
    MyClass() {
        std::cout << "Default constructor called."<< std::endl;
    }
    // 参数化构造函数
    MyClass(int a) : x(a) {
        std::cout << "Parameterized constructor called."<< std::endl;
    }
    // 拷贝构造函数
    MyClass(const MyClass& other) {
        x = other.x;
        std::cout << "Copy constructor called."<< std::endl;
    }
    // 委托构造函数
    MyClass() : MyClass(0) {
        std::cout << "Delegating constructor called."<< std::endl;
    }
    MyClass(int a) : x(a) {
        std::cout << "Parameterized constructor called."<< std::endl;
    }
private:
    int x;
};
int main() {
    // 使用默认构造函数创建对象
    MyClass obj1;
    // 使用参数化构造函数创建对象
    MyClass obj2(42);
    // 使用拷贝构造函数创建对象
    MyClass obj3(obj2);
    // 使用委托构造函数创建对象
    MyClass obj4;
    return 0;
}
image-20250511155439834
image-20250511155439834

12. 哪些操作允许在指针上进行?

在 C++ 里,指针允许进行下面这些操作:

赋值操作

可以把一个变量的地址赋给指针,或者让一个指针指向另一个指针所指向的地址。比如:

int num = 10;
int* ptr = &num; // 把num的地址赋给ptr
int* anotherPtr = ptr; // anotherPtr指向ptr所指向的地址

解引用操作

使用解引用操作符 * 能访问指针所指向的变量的值。例如:

int num = 10;
int* ptr = &num;
cout << *ptr; // 输出10

指针算术运算

  • 指针加整数:可以让指针向后移动若干个元素的位置。假设 ptr 是指向数组元素的指针,ptr + n 就表示向后移动 n 个元素。
int arr[5] = {1, 2, 3, 4, 5};
int* ptr = arr;
int* newPtr = ptr + 2; // newPtr指向arr[2]
  • 指针减整数:和加整数相反,是让指针向前移动若干个元素的位置。
  • 指针减指针:两个指向同一个数组元素的指针相减,得到的是它们之间元素的个数。

比较操作

可以对指针进行比较,比如判断两个指针是否指向同一个地址,或者一个指针是否大于、小于另一个指针。

int arr[5] = {1, 2, 3, 4, 5};
int* ptr1 = &arr[0];
int* ptr2 = &arr[2];
if (ptr1 < ptr2) {
    cout << "ptr1在ptr2前面";
}

######## 类型转换

能把指针从一种类型转换为另一种类型,但要注意这样做可能会带来风险,得谨慎使用。

int num = 10;
int* intPtr = &num;
char* charPtr = reinterpret_cast<char*>(intPtr); // 强制类型转换

13. delete 运算符的目的是什么?

delete 运算符用于释放通过 new 运算符在堆上动态分配的内存,调用对象的析构函数完成资源清理,防止内存泄漏。

14. delete []与 delete 的区别是什么?

delete 用于释放单个由 new 分配的对象的内存并调用其析构函数,而 delete [] 用于释放由 new[] 分配的数组对象的内存,会依次调用数组中每个对象的析构函数。

15. C++ 中 char*、const char*、char* const、const char* const 的区别?

回答重点

一个小技巧,从后往前读:

1)char*:这是一个指向 char 类型数据的指针,指针以及它指向的数据都是可变的。可以改变指针的指向和指向的数据。

2)const char*:*指向 const char,这是一个指向 const char 类型数据的指针。指针本身是可变的,但指针指向的数据是不可变的。简单来说,可以改变指针的指向,但不能改变它指向的数据内容。

3)char* constconst 修饰 char*,这是一个指向 char 类型数据的常量指针。指向的数据是可变的,但指针本身是不可变的。也就是说,不能改变指针的指向,但能修改指向的数据。

4)const char* const:这是一个指向 const char 类型数据的常量指针。指针和指向的数据都是不可变的,也就是既不能改变指针的指向,也不能修改指针指向的数据。

扩展知识

看一些代码示例。

1)char*:

char data[] = "Hello";
char* p = data;
p[0] = 'h'; // 修改数据内容,是允许的
p = nullptr; // 改变指针指向,是允许的

2)const char*:

const char* p = "Hello";
p[0] = 'h'; // 错误:不能修改数据内容
p = "World"; // 允许:改变指针指向

3)char* const:

char data[] = "Hello";
char* const p = data;
p[0] = 'h'; // 允许:修改数据内容
p = nullptr; // 错误:不能改变指针指向

4)const char* const:

const char* const p = "Hello";
p[0] = 'h'; // 错误:不能修改数据内容
p = "World"; // 错误:不能改变指针指向

除了这些类型修饰符,理解指针和引用在函数参数中的应用也很重要。

比如在接口设计中,通过使用 const char* 可以保证函数不会修改传入的字符串数据,从而提高代码的安全性。

合理使用 const 也有助于避免潜在的错误。例如,较为复杂的类和对象也可以通过 const 修饰,来确保在调用成员函数时不会意外修改对象的状态。

16. 🌟C++ 中数组和指针的区别?

详见:数组不是指针,指针也不是数组open in new window

回答重点

主要的区别可以总结为以下几点:

1)内存分配:

  • 数组:编译器会在栈上为数组的所有元素分配连续的内存空间。
  • 指针:指针本身只占用一个内存单元(通常是 4 或 8 字节),它存储的是一个地址。初始化指针之后,可以通过动态内存分配(例如使用 newmalloc)来分配内存。

2)固定与动态大小:

  • 数组:数组的大小在声明时就确定了,数组的大小需要是常量,无法在运行时改变。
  • 指针:指针比较灵活,它指向的内存如果是堆内存,可以在运行时动态分配和释放内存,灵活性更好。

3)类型安全性:

  • 数组:数组在声明时绑定了具体的类型,编译器在访问数组时可以进行类型检查。
  • 指针:指针声明时也有类型,但指针所指向的内存地址可以重新赋值,容易引起类型不匹配的问题,可能导致运行时错误,特别是指针类型经常转换的场景,比如 int*void* 等等。

4)运算操作:

  • 数组:数组名可以看作是数组首元素的常量指针,但不能直接进行算术运算(如 ++--)。
  • 指针:指针变量可以直接进行算术运算,比如递增、递减操作,从而访问不同的位置。

扩展知识

我们可以从几个方面进一步探讨:

1)数组和指针的转换:

  • 在表达式中,数组名会被自动转换为指向数组首元素的地址。例如,假设 int arr[5],则 arr 会被转换为 &arr[0]

2)动态数组:

  • 动态数组的实现需要使用指针。例如,int* arr = new int[5]; 这种方式在运行时分配的内存可以随意调整大小。

3)内存管理:

  • 编程时需要注意内存泄漏问题。使用指针分配的内存(例如使用 new)需要显式地释放(例如使用 deletedelete[]),否则会导致内存泄漏。

4)多维数组与指针:

  • 多维数组在内存中是按行优先顺序存储的,理解这一点有助于使用指针遍历多维数组。

5)高效代码:

  • 在写高性能代码时,指针有时可以比数组更高效,因为指针的算术运算更加灵活。

17. 🌟C++ 中 sizeof 和 strlen 的区别?

回答重点

两者的功能其实有很大区别:

1)sizeof 是一个编译时运算符,用于获取一个类型或者对象的大小(以字节为单位)。sizeof 在编译时计算结果,不涉及实际内容。

2)strlen 是一个库函数,用于计算 C 风格字符串的长度(不包括终止字符'\0')。strlen 是在运行时计算结果的,因为它需要遍历字符串内容。

扩展知识

1)sizeof 的应用:

  • 用于计算基础类型的大小,比如 sizeof(int)
  • 用来计算结构体或类的内存占用,比如 sizeof(MyClass)
  • 对于静态数组,可以获取整个数组的内存大小,比如 sizeof(arr),其中 arr 是一个静态数组。

注意,对于指针,sizeof 返回的是指针本身的大小,而不是指针所指向的内存区域的大小。例如:

int *p = new int[10];
std::cout << sizeof(p); // 这会返回指针的大小,通常是4或8个字节,具体取决于系统架构。

2)strlen 的应用:

  • 常用于计算 C 风格字符串的长度。注意,strlen 是不包括字符串末尾的终止符'\0'的。
  • 对于一些特定字符数组,strlen 非常有用,比如 char arr[] = "Hello";strlen(arr) 返回的是 5。

注意,strlen 不能用于未以 \0 结尾的字符数组,否则会导致未定义行为。例如:

char arr[5] = {'H', 'e', 'l', 'l', 'o'};
std::cout << strlen(arr); // 这会导致未定义行为,因为没有终止符'\0'。

3)获取动态分配内存的大小:

对于使用 new 动态分配的内存,sizeof 不能直接获取分配的内存大小。在这种情况下,需要在分配内存时显式记录内存大小。

int main() {
    int n = 10;
    int* arr = new int[n];
    // std::cout << "Size of dynamic array: "<< sizeof(arr) << " bytes"<< std::endl; // 错误:这将输出指针的大小,而不是数组的大小
    std::cout << "Size of dynamic array: " << n * sizeof(int) << " bytes"<< std::endl; // 正确:手动计算数组大小
    delete[] arr;
    return 0;
}

4)空类的大小

即使是一个空类(不包含任何数据成员和成员函数的类),sizeof 返回的值也至少为一个字节。这是为了确保在不同的编译器和平台上,空类的对象具有唯一标识。

class EmptyClass {};
int main() {
    std::cout << "Size of EmptyClass: "<< sizeof(EmptyClass) << " bytes"<< std::endl;
    return 0;
}

总结,sizeofstrlen 有其各自的用途和使用场景,一个用于计算类型大小,一个用于计算字符串长度。

18. 🌟C++ 中四种类型转换的使用场景?

回答重点

在 C++ 中,有四种常用的类型转换:

1)static_cast:用于在有明确定义的类型之间进行转换,如基本数据类型之间的转换以及指针或引用的上行转换(从派生类到基类)。

2)dynamic_cast:主要用于多态类型的指针或引用转换,特别适用于需要安全地执行下行转换(从基类到派生类)。

3)const_cast:用于移除对象的 const 或添加 const 属性,这在需要更改常量数据时非常有用。

4)reinterpret_cast:提供了一种最底层的转换方式,类似于 C 语言中的强转,通常用于指针类型之间的转换。

扩展知识

我们详细地讨论下每种类型转换的更多细节和注意事项。

1)static_cast

  • **用法:**主要用于已知类型之间的转换,比如 int 转换为 float 或者从派生类指针转换为基类指针。
  • 示例:
int a = 10;
float b = static_cast<float>(a);  // 将 int 转换为 float
  • 注意事项:static_cast 进行的类型转换在编译时检查,但不会进行运行时检查。

2)dynamic_cast

  • **用法:**主要用于多态基类,能够基于运行时类型信息将基类指针或引用转换为派生类指针或引用。
  • 示例:
Base* basePtr = new Derived();
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);  // 运行时检查并转换
  • **注意事项:**只有在类包含虚函数时才能使用 dynamic_cast,而如果转换失败,指针会返回 nullptr,引用则会抛出 std::bad_cast 异常。

3)const_cast

  • **用法:**移除或者添加常量修饰,通常在需要修改被标记为 const 的数据时使用。
  • 示例:
const int a = 10;
int *b = const_cast<int*>(&a);  // 移除 const 属性
*b = 20;  // 修改原本为 const 的值
  • 注意事项:const_cast 仅影响底层 const 属性,而并不影响顶层 const。同样,修改原本为 const 的变量可能会引发未定义行为,应该谨慎使用。

4)reinterpret_cast

  • **用法:**主要用于在几乎无关的类型之间进行转换,比如将指针类型转换为整型,或相反。这通常用于底层操作,比如硬件编程,或某些预期内的操作。
  • 示例:
long p = 0x12345678;
inti = reinterpret_cast<int*>(p);  // 将 long 转换为 int 指针
  • **注意事项:**这种转换不会进行类型安全检查,可能会改变位模式,应尽量避免使用,除非确实需要进行底层操作。
image-20250511155519335
image-20250511155519335

19. C++ 中 struct 和 class 的区别?

回答重点

在 C++ 中,structclass 的主要区别就在于它们的默认访问级别:

1)struct 的默认成员访问级别是 public

2)class 的默认成员访问级别是 private

扩展知识

虽然默认访问级别是 structclass 之间的主要区别,但在实际编程中,还有一些方面也值得注意:

1)内存布局和性能:

  • 在大多数情况下,structclass 的内存布局是一样的,因为它们本质上除了访问级别之外,其他都是相同的。
  • 在访问级别相同的情况下,二者的性能并无区别。

2)习惯用法:

  • struct 一般用于表示简单的数据结构或 POD(Plain Old Data) 类型。POD 类是所有非静态数据成员共享相同访问级别、没有虚函数、没有继承的类。
  • class 一般用于表示复杂的数据类型,特别是在对象需要封装和抽象,以及需要使用功能性的成员函数时。

3)继承模型:

  • 类可以有继承关系,通过 privateprotectedpublic 指定继承的访问级别,默认是 private 继承。
  • 结构体同样可以有继承关系,默认是 public 继承。
  • 上述特指在 C++ 中,在 C 语言中的 struct 是没有继承能力的。

4)编程风格:

  • 代码风格和团队规范可能对 structclass 的使用有明确的指示。例如,在一个面向对象的项目中,使用 class 定义所有对象可能更为规范,而 struct 则更多地用于数据传输对象(DTO)。

5)友元函数:

  • 友元函数或友元类在 structclass 中都适用,用法完全一样,唯一的区别是,如果你在 struct 里通常不需要那么多封装,在这种情况下友元可能用的不多。

20. 🌟C++ 中 new 和 malloc 的区别?delete 和 free 的区别?

回答重点

在 C++ 中,newmalloc 以及 deletefree 是内存管理的两对主要操作符和函数。它们虽然都有分配和释放内存的功能,但在很多方面都有区别。

  1. new vs malloc:

    • new 是 C++ 的操作符,而 malloc 是 C 标准库的函数。
    • new 分配内存并调用构造函数,而 malloc 仅仅分配内存,不调用构造函数。
    • new 返回一个类型安全的指针,而 malloc 返回 void*,需要显式类型转换。
    • new 在分配失败时抛出 std::bad_alloc 异常,而 malloc 返回 NULL
  2. delete vs free:

    • delete 是 C++ 的操作符,而 free 是 C 标准库的函数。
    • delete 销毁对象并调用析构函数,然后释放内存,而 free 仅仅释放内存,不调用析构函数。
    • delete 必须与 new 配对使用,而 free 必须与 malloc 配对使用。
    • deletedelete[] 是不同的,前者用于单一对象,后者用于数组。free 没有这种区分。

扩展知识

  1. 更多关于new和malloc的不同:

    • 异常处理: 在 new 语句中,如果内存分配失败,会抛出 std::bad_alloc 异常,你可以使用 try-catch 块处理这个异常。相比之下,malloc 返回 NULL 值,需要程序员手动检查并处理。
    • 类型兼容: new 更适合 C++ 中的类对象,因为它自动调用构造函数进行初始化,而 malloc 更适合简单的数据类型或 C 风格编程。
  2. 更多关于delete和free的不同:

    • 使用安全性: 使用 delete 时,不会像 free 那样导致未定义行为,因为它会调用析构函数来清理对象。在涉及复杂对象管理时,这种自动调用析构函数的特性非常有用。
    • 灵活性和匹配: 使用不同类型的 delete 操作符(deletedelete[])来区分释放单个对象和对象数组。free 函数则没有这种灵活性。
  3. 开发建议:

    • 建议尽量使用智能指针(如 std::unique_ptrstd::shared_ptr),它们可以自动管理内存,减少内存泄漏和其他潜在的内存管理问题。