【最全】C++面试题 (第四篇)

1.volatile、mutable和explicit

当谈到volatilemutableexplicit这三个关键字时,它们在各自的编程语言中扮演着不同的角色。以下是关于这三个关键字的详细解释:

1). volatile

作用

  • volatile是一个关键字,用于修饰变量。它告诉编译器这个变量是易变的,需要特殊对待。

  • 主要用于并发编程中,确保多个线程之间的数据一致性。当一个变量被volatile修饰时,任何对它的修改都会立即刷新到主内存中,同时当其他线程需要访问这个变量时,它们会从主内存中读取最新的值。

使用场景

  • 当多个线程共享某个变量,并且至少有一个线程会修改这个变量时,应该使用volatile来确保数据的一致性。

2). mutable

作用

  • 在C++中,mutable是一个关键字,用于修饰类的非静态成员变量。

  • mutable修饰的变量,将永远处于可变的状态,即使在一个const成员函数或const对象内部也可以被修改。

使用场景

  • 当需要在const成员函数或const对象内部修改某个与类状态无关的成员变量时,可以使用mutable来修饰这个变量。

3). explicit

注意

  • 在Java中,explicit并不是一个关键字,而是一个描述性的术语,用于指代在编程时需要明确指出的操作或声明。

  • Java中并没有一个名为explicit的关键字,但可以通过其他关键字和特性来实现显式操作。

常见显式操作

  • 显式类型转换:使用强制类型转换来将一种数据类型转换为另一种数据类型。

  • 显式构造方法调用:使用new关键字来显式地调用类的构造方法。

作用

  • 提高代码的可读性和明确性,避免因隐式推断导致的错误或混淆。

总结

  • volatilemutableexplicit在各自的编程语言中扮演着不同的角色。

  • volatile用于确保并发编程中数据的一致性。

  • mutable用于在C++中突破const的限制,允许在const上下文中修改成员变量。

  • explicit(虽然不是关键字)在编程中强调需要明确指出的操作或声明,提高代码的可读性和明确性。

​4.)示例
​
1. volatile (在C++中)
​

#include <iostream>  
#include <thread>  
#include <atomic> // 也可以使用 std::atomic 来代替 volatile  
  
volatile bool running = true; // volatile 变量  
  
void worker_thread() {  
    while (running) {  
        // 模拟工作  
        std::this_thread::sleep_for(std::chrono::milliseconds(100));  
    }  
    std::cout << "Worker thread stopped." << std::endl;  
}  
  
int main() {  
    std::thread t(worker_thread);  
  
    // 让线程运行一段时间  
    std::this_thread::sleep_for(std::chrono::seconds(5));  
  
    // 停止工作线程  
    running = false;  
  
    t.join();  
  
    return 0;  
}
注意:在现代C++中,更推荐使用std::atomic来替代volatile,因为std::atomic提供了更强大的原子操作和内存序保障。
​
2. mutable (在C++中)

#include <iostream>  
  
class MyClass {  
public:  
    MyClass(int val) : value(val) {}  
  
    void printValue() const {  
        // 由于 value 是 const 成员函数内部的,正常情况下不能修改  
        // 但因为 value 被 mutable 修饰,所以可以在这里修改它  
        value = 42; // 修改 value 是合法的  
        std::cout << "Value (mutable): " << value << std::endl;  
    }  
  
    int getValue() const {  
        return value;  
    }  
  
private:  
    mutable int value; // mutable 成员变量  
};  
  
int main() {  
    MyClass obj(10);  
    obj.printValue(); // 输出 "Value (mutable): 42"  
    std::cout << "Value (const): " << obj.getValue() << std::endl; // 输出 "Value (const): 42"  
    return 0;  
}
3. explicit (在C++中)
​

#include <iostream>  
  
class MyClass {  
public:  
    explicit MyClass(int val) : value(val) {} // explicit 构造函数  
  
    int getValue() const {  
        return value;  
    }  
  
private:  
    int value;  
};  
  
int main() {  
    MyClass obj1(10); // 正确,直接调用构造函数  
    // MyClass obj2 = 10; // 错误,因为构造函数是 explicit 的,不能隐式转换  
    MyClass obj2 = MyClass(10); // 正确,但显式地调用了构造函数  
  
    std::cout << "obj1: " << obj1.getValue() << std::endl; // 输出 "obj1: 10"  
    std::cout << "obj2: " << obj2.getValue() << std::endl; // 输出 "obj2: 10"  
  
    return 0;  
}
 

2.static作用

  1. 修饰变量

  • 全局变量:链接属性由外部变成内部,即对其他文件不可见,只能在本文件使用。

  • 局部变量:作用域不变,生命周期变成整个程序的执行周期,在内存中的位置由栈区变为全局静态区。退出作用域,该变量不可访问,但是仍在内存中;如果再次进入作用域,则读取上次的结果继续使用。

  • 成员变量:必须在类外初始化(static成员变量先于类的对象存在,static程序启动时就在内存中,而对象需要等到执行到的时候才会在内存中),该变量在类的所有对象中共享,只存在一份。可以通过类名+作用域的形式访问。

  1. 修饰函数

  • 普通函数:链接属性由外部变成内部,即对其他文件不可见,只能在本文件使用。

  • 成员函数:该函数在类的所有对象中共享,只存在一份,可以通过类名+作用域形式访问。没有this指针,无法声明为虚函数、常函数,无法直接访问非静态成员变量(可通过对象的方式访问),无法调用非静态成员函数

3.const作用

  1. 修饰变量:一旦初始化后不可修改

  • 全局变量:默认初始化为0。

  • 局部变量:必须立刻初始化,否则编译错误。

  • 成员变量:必须使用构造函数的初始化列表进行初始化。

  • 类的对象:const对象只能调用const成员函数

  1. 修饰函数

  • 函数返回值:返回值不可修改

  • 函数尾部:一般用在类中的成员函数,叫做常函数,无法修改类的成员变量,且无法调用非const成员函数。无法使用static修饰。

4.为什么列表初始化更快

构造函数体内部使用赋值操作来初始化成员变量,会涉及以下步骤:

  1. 分配内存空间来创建对象。

  2. 对成员变量进行默认初始化(如果有)。

  3. 执行构造函数体内的代码,并在其中进行赋值操作。

使用初始化列表的方式,初始化直接在分配内存空间之前完成,将初始化操作置于构造函数体之前,从而避免了默认初始化和赋值操作。

另外,初始化列表还可以避免临时对象的创建。有些情况下,如果在构造函数体内部使用赋值操作来初始化成员变量,会涉及临时对象的创建和拷贝操作。而使用初始化列表,则可以直接将初始值传递给成员变量,避免了额外的对象创建和拷贝。

5.哪些情况必须用列表初始化

  1. 初始化const成员

  2. 初始化引用成员

  3. 调用基类的有参构造

  4. 调用成员类的有参构造

6.构造函数类型

1.默认构造函数

如果定义某个对象的类的时候没有提供初始化式,则会使用默认构造函数

2.转换构造函数
为了实现其他类型到类类型的隐式转换, 需要定义合适的构造函数。可以**用单个实参调用的构造函数(称为转换构造函数)** 定义从形参类型到该类类型的隐式转换。下边例子展示了string类型到Data类型的转换。

禁止由构造函数定义的隐式转换, 方法是通过将构造函数声明为explicit(用在类内函数的声明上,类外定义不能重复它

3.复制构造函数(拷贝构造函数)

复制构造函数的作用是用一个已经生成的对象去初始化另一个同类的对象

    Point pt1(10,20); Point pt2 = pt1;
​
    此时会调用复制构造函数,此外函数参数按值传递或者返回对象的时候,也会调用复制构造函数。
直接初始化和复制初始化

直接初始化会调用与实参匹配的构造函数; 而复制初始化总是调用复制构造函数。

如果类中没有定义复制构造函数,那么编译器会自动合成一个,称为合成复制构造函数(浅拷贝的方式)。

7.转换构造函数和有参构造函数的区别

转换构造函数(Conversion Constructor)和有参构造函数(Parameterized Constructor)在C++中都是接受参数的构造函数,但它们在用途和语义上有所不同。

有参构造函数(Parameterized Constructor)

有参构造函数是一个接受一个或多个参数的构造函数,用于初始化对象的成员变量。这些参数通常直接对应于对象的某些属性或状态。使用有参构造函数创建对象时,必须显式地提供这些参数。

class MyClass {  
public:  
    MyClass(int x, float y) { // 有参构造函数  
        // 初始化成员变量...  
    }  
    // ... 其他成员函数 ...  
};  
  
int main() {  
    MyClass obj(10, 3.14); // 使用有参构造函数创建对象  
    // ...  
}

转换构造函数(Conversion Constructor)

转换构造函数是一个特殊的构造函数,它允许从其他数据类型(如基本数据类型、其他类的对象等)隐式或显式地创建一个类的对象。当转换构造函数被声明为explicit时,它只能用于显式转换;如果没有被声明为explicit,它也可以用于隐式转换。

转换构造函数通常接受一个单一参数,该参数的类型与构造函数所属的类不同。它的主要用途是允许对象之间的隐式或显式类型转换。

class MyClass {  
public:  
    MyClass(int value) { // 转换构造函数  
        // 初始化成员变量...  
    }  
    // ... 其他成员函数 ...  
};  
  
void functionTakingMyClass(MyClass obj) {  
    // ...  
}  
  
int main() {  
    int value = 10;  
    functionTakingMyClass(value); // 如果构造函数不是explicit,这里会隐式调用转换构造函数  
    // 如果构造函数是explicit,则必须显式调用:functionTakingMyClass(MyClass(value));  
    // ...  
}

区别

  1. 用途:有参构造函数主要用于初始化对象的成员变量;而转换构造函数主要用于允许从其他数据类型到类的隐式或显式转换。

  2. 参数数量:有参构造函数可以接受一个或多个参数;而转换构造函数通常接受一个参数(尽管这不是强制的,但接受多个参数的转换构造函数可能会使隐式转换变得复杂且难以预测)。

  3. explicit关键字:转换构造函数可以使用explicit关键字来防止隐式转换;而有参构造函数则没有这样的用法(尽管可以通过其他方式避免不期望的隐式转换)。

  4. 语义:从语义上讲,有参构造函数更直接地关联于对象的初始化;而转换构造函数更侧重于类型之间的转换关系。

8.派生类构造函数的执行过程

  1. 如果有虚基类先执行虚基类的构造函数,如果多个,则按照继承顺序执行。

  2. 执行普通基类构造函数,如果多继承,则按照继承顺序执行。

  3. 执行派生类内部成员对象的构造函数,如果多个,按照在类中定义的顺序执行。

  4. 执行派生类本身的构造函数。

初始化列表的初始化顺序由成员在类中定义的顺序决定,而不是参数顺序。

9.智能指针

智能指针(Smart Pointers)是C++中用于自动管理动态分配内存的对象。它们通过封装原始指针并提供额外的功能(如自动删除所指向的对象、引用计数等)来确保资源的正确释放,从而避免内存泄漏和其他与内存管理相关的问题。

智能指针是一种封装了原始指针的RAII类,它能够自动管理内存,避免了内存手动申请和释放的问题。C++标准库提供了三种类型的智能指针:shared_ptr、unique_ptr和weak_ptr。

  • shared_ptr:是一种共享所有权的智能指针,它可以被多个 shared_ptr 对象共享。它使用引用计数来跟踪有多少个 shared_ptr 指向同一个对象,当引用计数为 0 时,关联的对象会被自动删除。

  • unique_ptr:是一种独占所有权的智能指针,它确保在其生命周期结束时,关联的对象会被自动删除。它不能被复制或共享,只能通过移动语义来转移所有权

  • weak_ptr:是一种弱引用的智能指针,它指向一个由 shared_ptr 管理的对象,但不会增加引用计数。weak_ptr 主要用于检查所管理的资源是否已经被释放,以及协助 shared_ptr 防止循环引用。weak_ptr可以通过lock函数获取到shared_ptr,如果所管理的对象已经被销毁,会返回一个空的shared_ptr。还可以通过expired函数检查所管理的资源是否存在,如果不存在返回true。

  • std::auto_ptr(已弃用):在C++11之前被使用的智能指针,但由于其所有权转移语义和潜在的错误,在C++11中被弃用,并在C++17中被移除。

10.智能指针是线程安全的吗?

不是.

在多线程环境下使用智能指针,需要注意以下几点:

  • 一个线程拥有了指针所有权之后,不要让其他线程使用该指针。

  • 当使用一个指针时,需要使用锁来保护该指针。

  • 不要让一个指针在一个线程中 delete,而在另一个线程中访问它。

11.引用计数是线程安全的吗?

是,C++11中智能指针的引用计数使用的是原子操作,可以保证多线程环境下的原子性。

12.weak_ptr能用裸指针代替吗?

不能,因为weak_ptr是用于解决C++中的循环引用问题而设计的。在使用C++中的裸指针时,如果指针所指的内存被释放了,指针就会变成一个野指针,使用它会导致程序崩溃。而weak_ptr是一种智能指针,它可以让我们访问一个外部的对象,但是不会增加这个对象的引用计数,因此,即使这个对象已经被销毁,使用weak_ptr也不会出现野指针问题。因此,使用weak_ptr可以避免循环引用,并且可以安全地解除对被引用对象的所有权。

13.为什么auto_ptr在C++11中被废弃?

auto_ptr在C++11中被弃用,主要是因为它的所有权转移语义可能导致潜在的错误。当使用拷贝构造函数或赋值操作符时,auto_ptr会将其所有权转移到新的auto_ptr对象,并将原始auto_ptr设置为空。这种语义可能会导致意外的资源释放和悬空指针问题。因此,为了更安全地管理动态内存,推荐使用unique_ptrshared_ptr等更现代的智能指针。

 
​
​
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱编程的小猴

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值