C++基础面试题

avatar
作者
筋斗云
阅读量:0

1. 什么是c++的左值和右值?有什么区别?

在C++中,左值(lvalue)和右值(rvalue)是指表达式的价值分类,这种分类对理解对象的生命周期和内存管理很重要。

左值(lvalue)

  • 定义:左值是指表达式可以出现在赋值语句的左侧的值,通常表示一个持久的对象,可以在内存中持有地址。

  • 特性:左值可以引用(或取地址),可以被赋值。

  • 示例

    int x = 10;  // x是左值   x = 20;      // 可以将新值20赋给x   int* p = &x; // 可以获取x的地址   

右值(rvalue)

  • 定义:右值是指表达式的值可以出现在赋值语句的右侧,通常表示临时对象或字面量,不拥有持久的内存地址。

  • 特性:右值不能被取地址,也不能在赋值语句的左侧使用。

  • 示例

    int y = 10;  // 10是右值   int z = x + y; // x + y的结果是一个右值   

区别

  1. 存储位置
    • 左值拥有固定的内存地址,代表某个可以在程序中访问的对象。
    • 右值通常是临时的,不具有持久的内存地址。
  2. 可赋值性
    • 左值可以接收赋值操作(出现在赋值的左边)。
    • 右值不能接收赋值操作(不能出现在赋值的左边)。
  3. 取地址
    • 左值可以使用&操作符取地址。
    • 右值不能取地址,因为它们是临时的,不存在确切的内存地址。

C++11中的右值引用

C++11引入了右值引用(&&),使得程序员能够更加高效地管理资源,尤其是在实现移动语义时,允许通过右值引用来获取资源的所有权,这样可以避免不必要的复制,提升性能。

总结

  • 左值是有持久性的对象,能出现在赋值操作的左侧并可以取地址。
  • 右值是临时的、无持久性的值,不能取地址,通常出现在赋值操作的右侧。

2. C和C++区别

C和C++都是广泛使用的编程语言,但它们之间有一些显著的区别。以下是一些主要的区别:

  1. 编程范式
  • C:主要是一种过程式编程语言,强调功能的分解和顺序执行。
  • C++:是一种多范式编程语言,支持面向对象编程(OOP),同时也支持过程式编程。
  1. 面向对象
  • C:不支持面向对象的特性。
  • C++:支持类和对象,从而允许封装、继承和多态等特性。
  1. 标准库
  • C:拥有较小的标准库,主要提供基础的输入输出、字符串操作和内存管理等功能。
  • C++:拥有更丰富的标准库,包括STL(标准模板库),提供了许多数据结构和算法的实现。
  1. 数据类型
  • C:基本数据类型相比较少。
  • C++:除了C中的基本数据类型外,还引入了自定义类型(如类)、引用类型和模板。
  1. 内存管理
  • C:通过malloc/free等函数手动管理内存。
  • C++:提供了new/delete运算符来动态分配和释放内存,同时也支持构造函数和析构函数来管理对象的生命周期。
  1. 异常处理
  • C:没有内置的异常处理机制。
  • C++:提供了异常处理机制(try/catch),用于处理运行时错误。
  1. 函数重载和默认参数
  • C:不支持函数重载,也不支持默认参数。
  • C++:支持函数重载和默认参数,使得函数定义更加灵活。
  1. 命名空间
  • C:没有命名空间,容易造成名称冲突。
  • C++:引入了命名空间,帮助组织代码并减少名称冲突的可能性。
  1. 引用
  • C:仅支持指针来实现间接引用。
  • C++:支持引用(&),提供了更简单、更安全的方式来传递参数。
  1. 模板
  • C:不支持模板。
  • C++:支持模板,允许开发者编写通用代码,可以用于类和函数,增强了代码的复用性。

结论

C是一种功能强大且高效的过程式编程语言,适合系统编程和嵌入式开发。C++在此基础上增加了面向对象编程的特性,并且拥有更丰富的标准库,非常适合复杂的应用程序开发。选择使用哪种语言通常取决于具体的项目需求和团队技能。

3. 什么是C++的移动语意和完美转发?

移动语义(Move Semantics)

移动语义是C++11引入的一项特性,旨在提高资源管理的效率,尤其在处理临时对象(右值)时。与传统的复制语义相比,移动语义允许通过“移动”资源的所有权,而不是复制资源,这样可以减少内存分配的开销,从而提高性能。

关键概念:

  1. 右值引用:使用&&来定义右值引用,使得可以接受右值(如临时对象),而不需要复制它们。
  2. 移动构造函数:一个特殊的构造函数,接受一个右值引用,并将其资源(如动态分配的内存)“移动”到新对象中。
  3. 移动赋值运算符:一个特殊的赋值运算符,接受一个右值引用,并将其资源移入现有对象。

示例:

class MyClass {   public:       MyClass(const MyClass& other) { /* 复制构造函数 */ }       MyClass(MyClass&& other) { /* 移动构造函数 */ }            MyClass& operator=(const MyClass& other) { /* 复制赋值运算符 */ }       MyClass& operator=(MyClass&& other) { /* 移动赋值运算符 */ }   };   

完美转发(Perfect Forwarding)

完美转发是C++11引入的另一项特性,旨在解决在函数模板中传递参数时保存参数的价值类别(左值或右值)。它允许模板函数以调用者的上下文来传递参数,从而避免不必要的拷贝或移动。

关键概念:

  1. 通用引用(Universal Reference):使用T&&作为函数参数类型,结合typename T,可以接收左值和右值。此时,T的类型决定了引用的实际类型。
  2. std::forward:一个函数模板,用于保持传递参数的原始值类别,可以在转发参数时保持其左值或右值性质。

示例:

#include <utility>    template<typename T>   void wrapper(T&& arg) {       // 完美转发       process(std::forward<T>(arg));   }    void process(MyClass&& obj) {       // 对右值进行处理   }    void process(const MyClass& obj) {       // 对左值进行处理   }    // 使用   MyClass obj;   wrapper(obj);        // 将 obj 作为左值转发   wrapper(MyClass());  // 将临时对象作为右值转发   

总结

  • 移动语义通过允许资源的移动而不是复制来提高性能,特别是在处理临时对象时。
  • 完美转发确保在模板函数中保持参数的原始值类别,使得调用者传递的左值或右值能够被正确处理。这两项特性结合在一起,大幅提升了C++的性能和灵活性。

4. 什么是C++的列表初始化?

C++的列表初始化

列表初始化(List Initialization)是C++11引入的一种新方法,用于初始化对象和数组。其主要目的是提供一种直观且一致的语法来初始化变量,从而减少潜在的错误。

特点和优点

  1. 简洁性:使用花括号 {} 来进行初始化,语法简洁明了。
  2. 防止窄化:列表初始化会严格检查类型转换,避免类型窄化的问题(如从 double 转换为 int),如果发生不安全的窄化转换,编译器会报错。
  3. 默认初始化:未提供初始值时,列表初始化会将基本类型初始化为0。

初始化方式

  1. 对象初始化

    struct Point {       int x;       int y;   };    Point p1 {1, 2};  // 使用列表初始化   
  2. 数组初始化

    int arr[] {1, 2, 3, 4};  // 初始化数组 ,新的初始化方式  
  3. 类类型初始化

    class MyClass {   public:       MyClass(int a, int b) {}   };    MyClass obj {1, 2};  // 使用列表初始化   
  4. 标准容器初始化

    std::vector<int> vec {1, 2, 3, 4};  // 列表初始化 std::vector   

注意事项

  • 聚合类型初始化:如果有一个聚合类型(例如没有用户定义构造函数的结构体),可以使用列表初始化。

  • 构造函数重载:如果类中定义了构造函数,列表初始化会调用对应的构造函数。

  • 避免重复初始化

    :如果使用列表初始化同时又定义了某个成员变量的初始值,可能会引发错误。例如:

    struct S {       int x = 0;  // 默认初始化   };    S s {1};  // 错误:尝试同时使用默认值和列表初始化   

列表初始化的优劣

  • 优点

    • 简洁直观。
    • 更安全,避免类型窄化。
  • 缺点

    • 对于某些复杂情况,可能限制了初始化的灵活性,尤其在使用类型转换时。

总结

C++的列表初始化是一种强大且用途广泛的初始化机制,提供了一种简洁、安全的方式来创建和初始化对象。通过使用 {},程序员能够避免许多常见的初始化错误,使得代码更加可读且容易维护。

5. 介绍C++三种智能指针的使用场景?

C++中有三种主要的智能指针,分别是std::unique_ptrstd::shared_ptrstd::weak_ptr。它们各自有不同的使用场景和特点。

  1. std::unique_ptr

特点

  • 独占所有权:一个 unique_ptr 只能有一个拥有者,无法复制,但可以移动。
  • 自动释放:当 unique_ptr 超出作用域,或被销毁时,自动释放其所管理的对象。

使用场景

  • 独占资源:当你需要一个对象的唯一所有权,并且不希望有人共享这个对象时,使用 unique_ptr。例如,管理动态分配的对象。
  • 高效的资源管理:在性能要求高的场景下,unique_ptr 不会有引用计数的开销,适用需要频繁创建和销毁对象的场合。
#include <memory>    void example() {       std::unique_ptr<int> ptr = std::make_unique<int>(10);       // 使用 ptr ...   }  // ptr 会在此处自动释放   
  1. std::shared_ptr

特点

  • 共享所有权:多个 shared_ptr 可以共享同一个对象,引用计数机制会确保当最后一个指针被销毁时,对象才会被释放。
  • 适合多次引用:当多个部分的代码需要访问同一资源时使用。

使用场景

  • 多个所有者:在需要多个对象共享同一个资源时,比如图形界面中的共享数据、池中的对象等。
  • 生命周期管理:适用于对象生命周期难以预测的场景,比如动态创建多个共享的工作线程。
#include <memory>    void example() {       auto ptr1 = std::make_shared<int>(10);       std::shared_ptr<int> ptr2 = ptr1;  // ptr1 和 ptr2 共享同一个资源   }  // 当 ptr1 和 ptr2 超出作用域时,资源会被释放   
  1. std::weak_ptr

特点

  • 不拥有资源:weak_ptr 不增加引用计数,不影响被管理对象的生命周期。
  • 用于解决循环引用:与 shared_ptr 搭配使用,可以避免内存泄漏。

使用场景

  • 观察者模式:在需要观察某个对象而不希望影响其生命周期时,使用 weak_ptr。比如,有一个对象需要知道某些资源的状态,但这些资源释放后,不需要保持对它们的强引用。
  • 缓存实现:当需要缓存某些数据但不希望阻止它们被释放时。
#include <memory>   #include <iostream>    class Resource {   public:       Resource() { std::cout << "Resource created\n"; }       ~Resource() { std::cout << "Resource destroyed\n"; }   };    void example() {       std::shared_ptr<Resource> sharedPtr = std::make_shared<Resource>();       std::weak_ptr<Resource> weakPtr = sharedPtr;  // weak_ptr 不增加引用计数        if (auto sp = weakPtr.lock()) {  // 尝试获取 shared_ptr           // 成功获取资源       } else {           // 资源已经被释放       }   }   

总结

  • std::unique_ptr:用于独占资源管理,提高性能。
  • std::shared_ptr:用于共享多个所有者之间的对象。
  • std::weak_ptr:用于解决循环引用问题并观察对象的状态。这三种智能指针的合理使用可以大幅提升C++程序的内存管理和代码安全性。

6. std::shared_ptr解析

std::shared_ptr 是 C++11 引入的一种智能指针,它提供了共享所有权的机制。下面是对 shared_ptr 的详细解释:

  1. 共享所有权

std::shared_ptr 允许多个指针同时指向同一个动态分配的对象,每个 shared_ptr 都持有一个引用计数,标记有多少个 shared_ptr 指向同一个对象。

  1. 引用计数

当你创建一个 shared_ptr 时,它的引用计数会被初始化为 1。当你将一个 shared_ptr 赋值给另一个 shared_ptr(如上例的 ptr2 = ptr1)时,引用计数会增加,表明现在有两个指针指向同一个对象。当一个 shared_ptr 被销毁(超出作用域或调用 reset)时,引用计数会减少。只有当引用计数降为 0 时,所指向的对象才会被释放,释放相应的内存。

  1. 自动管理生命周期

shared_ptr 通过对引用计数的管理,避免了内存泄漏(即分配内存后没有释放)的问题。当所有指向同一对象的 shared_ptr 都超出作用域或被重置时,该对象的内存会自动被释放。

代码示例解析

#include <iostream>   #include <memory>    void example() {       // 创建一个 shared_ptr,指向一个动态分配的整数 10       auto ptr1 = std::make_shared<int>(10);        // 创建一个新的 shared_ptr ptr2,指向同一对象       std::shared_ptr<int> ptr2 = ptr1;  // 此时指向同一个 int 对象,引用计数变为 2        // 输出当前值和引用计数       std::cout << "Value: " << *ptr1 << ", Reference Count: " << ptr1.use_count() << std::endl;  // 引用计数为 2       std::cout << "Value: " << *ptr2 << ", Reference Count: " << ptr2.use_count() << std::endl;  // 引用计数为 2   }  // 当 ptr1 和 ptr2 超出作用域,引用计数降为 0,负责内存释放   

重要方法

  • use_count():返回当前有多少个 shared_ptr 实例共享同一个对象。
  • reset():可以用来重置 shared_ptr,解除与当前对象的关联,并降低引用计数。

注意事项

  • 循环引用:如果两个或多个对象通过 shared_ptr 互相引用,它们的引用计数将永远不为 0,从而导致内存泄漏。解决方案是使用 std::weak_ptr 来打破循环。

总结

std::shared_ptr 是一个用于共享对象所有权的智能指针,让程序员可以更方便地管理动态分配的内存资源。适当地使用 shared_ptr 可以显著降低内存管理的复杂性,提高代码的安全性和可读性。

7. C++中static的作用?什么场景下使用static?

在 C++ 中,static 关键字的作用主要有以下几点:

  1. 静态变量
  • 在函数内部
    static 用在函数内部时,定义的变量在函数调用结束后不会被销毁,其值会被保留在后续调用中。

    void counter() {       static int count = 0; // 静态变量,只会初始化一次       count++;       std::cout << "Count: " << count << std::endl;   }   

    在这个例子中,count 的值会在每次调用 counter 时累加,直到程序结束。

  • 在类内部
    static 用于类的成员变量时,该变量是所有类对象共享的,而不是每个对象都有自己的副本。

    class Example {   public:       static int instanceCount; // 声明静态成员       Example() {           instanceCount++; // 每当创建一个对象时,增加计数       }   };    int Example::instanceCount = 0; // 定义静态成员   
  1. 静态函数
  • 在类内部
    静态成员函数属于类本身,而不是某个对象,因此可以在没有类实例的情况下调用。静态函数只能访问静态成员变量,不能访问非静态成员变量。

    class Example {   public:       static int count;       static void increment() {           count++;       }   };    int Example::count = 0;   
  • 在全局/命名空间作用域
    声明为 static 的全局函数或变量只在定义它的文件中可见,无法在其他文件中访问。这用于限制作用域,避免命名冲突。

    static void helperFunction() {       // 该函数仅在此文件可见   }   

使用场景

  1. 持久化数据:在函数内部的静态变量可用于持久化数据(例如计数器),在函数调用之间保持状态。
  2. 共享数据:在类中,静态成员用于共享数据(例如统计对象数量)。适合需要共享状态或计数的情况。
  3. 限制作用域:当希望一个函数或变量只在一个翻译单元中可用时,可以使用全局静态来避免与其他文件中的符号冲突。
  4. 性能:使用静态变量或函数可以减少重复计算或对象创建的开销,例如在单次初始化后,多次访问共享的资源。

总结

static 是一个强大而灵活的关键字,其用途包括持久化数据、共享状态、限制作用域以及优化性能。当使用 static 时,需要清晰理解其作用范围及生命周期,以确保程序的正确性与可维护性。

8. C++中const的作用?,谈谈你对const的理解?

在 C++ 中,const 关键字用于声明不可修改的对象或数据。它能够帮助程序员更好地管理数据的可变性,提高代码的安全性和可读性。下面是对 const 的具体作用和使用场景的总结。

  1. const 的基本用法
  • 常量变量
    使用 const 可以定义一个不可修改的变量,一旦赋值后,该变量不能被改变。

    const int maxValue = 100;   // maxValue = 101;  // 编译错误   
  • 常量指针与指针常量
    可以使用 const 来修饰指针,定义指针所指向的内容是否可以被修改。

    int x = 10;   const int* ptr1 = &x;   // 指向常量的指针,无法修改 *ptr1   int* const ptr2 = &x;   // 常量指针,无法修改 ptr2 指向的地址    // *ptr1 = 20;  // 编译错误   // ptr2 = &y;   // 编译错误   
  1. const 与类
  • 常量成员函数
    如果一个成员函数被声明为 const,那么这个函数不能修改类的任何非静态成员数据。

    class Example {   public:       void show() const {  // 不能修改成员变量           // memberVariable = 10;  // 编译错误       }   };   
  • 常量对象
    当对象被声明为常量时,该对象的非静态成员变量不能被修改。

    const Example obj;  // obj 的成员不能被修改   
  1. 使用场景
  • 增强代码的可读性与维护性
    使用 const 提示其他程序员某些数据是不应被修改的,这样可以提高代码的可读性并减少潜在的错误。

  • 避免意外修改
    当函数参数被声明为 const 时,表明该函数不会修改传入的参数,这对于大型项目尤为重要,防止意外的副作用。

    void processValue(const int value);   
  • 接口设计
    在设计公共 API 时,使用 const 可以确保用户不修改数据,提高接口的安全性。

  • 优化
    编译器在知道某个变量不会被修改的情况下,能够进行更优化的编译,这可能有助于提升性能。

  1. 总结

const 是 C++ 中一个非常重要的修饰符,用于提高代码的安全性和可读性。通过明确指定不可变的数据和函数,程序员可以更清晰地理解代码的逻辑和意图。合理使用 const 不仅能减少错误的发生,也能提高代码的效率和可维护性。

9 .C++中define 和const的区别?

在C++中,#defineconst都是用来定义常量的,但它们有一些重要的区别:

  1. 定义方式
  • #define:这是一个预处理指令,用于在编译前进行文本替换。

    #define PI 3.14         
  • const:这是一个关键字,用于定义常量变量。它在编译时具有类型,具有作用域和生命周期。

    const double PI = 3.14;             
  1. 类型安全
  • #define:没有类型信息,编译器在预处理阶段仅进行文本替换,因此没有类型检查。

    #define SQUARE(x) (x * x) // 这里没有类型检查         
  • const:有明确的类型,因此编译器会进行类型检查,能帮助发现一些潜在的错误。

    const int maxSize = 100; // 有类型,编译器会检查        
  1. 作用域
  • #define:是全局的,直到遇到#undef或编译单元结束。
  • const:具有块作用域,定义在某个函数内的const变量只在该函数内有效。
  1. 调试和错误信息
  • #define:调试信息较少,错误信息可能不太清晰,因为它仅在预处理阶段替换文本。
  • const:错误信息清晰,能够提供更多上下文,因为编译器知道变量的类型和作用域。
  1. 性能和优化
  • #define:可以在某些情况下导致代码膨胀,因为每次出现宏时都会进行替换。
  • const:编译器可以对const变量进行更好的优化,因为它们有明确的类型和存储位置。

总结

#define适合用于简单的宏定义和常量替代,而const则是更推荐的方式来定义常量,因为它提供了类型安全、作用域控制和更好的调试信息。在现代C++编程中,建议使用constconstexpr来定义常量,而尽量避免使用#define

10.C++inline的作用?它有什么优缺点?

在C++中,inline(内联函数)关键字用于建议编译器将函数的调用直接替换为函数的实现,以减少函数调用的开销。下面我们将详细介绍inline的作用、优缺点,并提供代码示例。

作用

  1. 减少函数调用开销
    • 使用inline函数时,编译器可以在调用该函数时直接插入函数的代码,从而避免传统的函数调用开销(如参数传递、栈帧创建等)。
  2. 避免链接错误
    • 在头文件中定义的inline函数可以避免在多个源文件中包含同一函数定义时引起的链接错误。

示例代码

#include <iostream>  inline int add(int a, int b) {     return a + b; }  int main() {     int x = 5, y = 10;     // 在这里调用 add 函数     std::cout << "Sum: " << add(x, y) << std::endl;     return 0; }         

在这个例子中,add函数被定义为inline。编译器在调用add(x, y)时可以将其替换为return x + y;,从而消除函数调用的开销。

优点

  1. 性能提升
    • 对于小型和频繁调用的函数,内联化可以提高执行效率,特别是在循环或递归中调用的函数。
  2. 避免重复定义
    • inline函数可以在头文件中定义,避免在多个源文件中出现同一函数的重复定义,从而避免链接错误。
  3. 代码可读性
    • 将函数实现放在头文件中可以提高代码的可读性,因为它允许在使用函数的地方同时看到其实现。

缺点

  1. 代码膨胀

    • 如果一个大的inline函数在多个地方被调用,可能会导致生成的代码膨胀(即可执行文件变大),因为每次调用都插入了函数的代码。
    inline void largeFunction() {     // 假设这个函数很大     // ... }         
  2. 编译器的自由裁量权

    • inline只是对编译器的建议,编译器可以选择不将函数内联化,因此不一定能实现性能提升。
  3. 调试困难

    • 内联函数在调试时可能会使堆栈跟踪变得不直观,因为在堆栈中不会看到实际的函数调用。
  4. 影响优化

    • 过度使用inline可能会影响其他编译优化,因为内联化可能导致更复杂的代码结构。

总结

inline关键字在C++中可以用于提高性能和避免链接错误,但在使用时要谨慎。通常,适合将小且频繁调用的函数声明为inline,而大而复杂的函数则不适合使用inline。现代编译器已经具备了相当强大的优化能力,因此在很多情况下,手动使用inline并不是必要的。

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!