详细技术视频请看我的主页

C++教程视频

什么是默认构造

默认构造就是不带参数的构造函数,如果我们不实现任何构造函数,系统会为我们生成一个默认的构造函数

比如下面

#include <thread>
class JoiningThread {
public:
    int GetIndex() const { return _i; }
private:
    std::thread _t;
    int _i;
};

所以我们可以直接使用默认构造函数构造一个对象,并且打印成员_i

//测试默认合成
JoiningThread jt;
std::cout << "member _i is " << jt.GetIndex() << std::endl;

输出

member _i is -1284874240

可以看到默认构造函数并不会帮我们初始化类成员变量。

什么是有参构造

有参构造就是传递参数的构造函数,可以根据参数构造对象

#include <thread>
class JoiningThread {
public:
    JoiningThread(int i) : _i{i} {}
    int GetIndex() const { return _i; }
private:
    std::thread _t;
    int _i;
};

我们可以通过如下方式构造

JoiningThread jt(1);
std::cout << "member _i is " << jt.GetIndex() << std::endl;

当我们执行程序,会输出

member _i is 1

但如果这样构造会产生问题

JoiningThread jt;
std::cout << "member _i is " << jt.GetIndex() << std::endl;

注意

如果我们实现了参数构造而不实现无参构造,系统将不会为我们实现默认构造,导致无法使用默认构造生成对象。

所以稳妥一点,我们基本都会实现无参构造

#include <thread>
class JoiningThread {
public:
    JoiningThread() :_i(0){}
    JoiningThread(int i) : _i{i} {}
    int GetIndex() const { return _i; }
private:
    std::thread _t;
    int _i;
};

拷贝构造函数是什么

回答要点:

  • 定义:拷贝构造函数用于创建一个对象,该对象是通过复制另一个同类型对象来初始化的。

  • 调用时机

    • 使用现有对象初始化新对象。
    • 按值传递对象作为函数参数。
    • 按值返回对象。
  • 默认拷贝构造函数:成员逐个拷贝。

示例:

class MyClass {
public:
    MyClass(const MyClass& other) { // 拷贝构造函数
        // 复制代码
    }
};

是否会默认生成拷贝构造

在 C++ 中,如果你没有为一个类显式定义拷贝构造函数,编译器会自动生成一个默认的拷贝构造函数。这个默认拷贝构造函数会按成员的逐个拷贝(member-wise copy)方式来复制对象的每个成员变量。

默认拷贝构造函数的行为

  1. 逐个拷贝:默认拷贝构造函数会逐个拷贝所有的非静态成员变量。
  2. 指针成员:如果类中有指针成员,默认拷贝构造函数只会拷贝指针的值(地址),而不会拷贝指针所指向的对象。这可能会导致多个对象指向同一块内存,进而引发问题(如双重释放、内存泄漏等)。
  3. const引用成员:如果类中有 const 成员或引用成员,编译器不会生成默认的拷贝构造函数,因为这些成员不能被复制。
  4. 类中包含不可拷贝对象时,无法合成默认拷贝构造函数

拷贝构造是否必须实现

当一个类A中有成员变量是另一个类类型B的时候,有时候拷贝构造会失效。

比如一个类JoiningThread中有成员变量std::threadstd::thread没有构造函数,所以A类的拷贝构造无法合成,需要显示编写。

比如我们这样调用

JoiningThread jt(1);
JoiningThread jt2(jt);
std::cout << "member _i is " << jt.GetIndex() << std::endl;

上面代码报错

error: use of deleted function 'std::thread::thread(const std::thread&)'

所以我们要显示实现拷贝构造,指定一个拷贝规则

JoiningThread(const JoiningThread & other): _i(other._i){}

什么是浅拷贝和深拷贝

类在拷贝构造或者拷贝赋值的时候,将被拷贝的类中的成员值拷贝到目的类,如果被拷贝的类中包含指针成员,只是简单的拷贝指针的值。

同样析构也要显示编写,等待线程完成。

除此之外我们可以自己实现拷贝构造,进而实现浅拷贝和深拷贝的不同效果

https://cdn.llfc.club/1731294878994.jpg

https://cdn.llfc.club/1731295328032.jpg-llfc

构造顺序和析构顺序

类A中包含成员变量是类B的类型,如果是先调用A的构造还是B的构造呢?

如果析构的时候是A先析构还是B先析构呢?

class InnerB {
public:
    InnerB() {
        std::cout << "InnerB()" << std::endl;
    }

    ~InnerB(){
        std::cout << "~InnerB()" << std::endl;
    }
};

class WrapperC {
public:
    WrapperC(){
        std::cout << "WrapperC()" << std::endl;
    }
    ~WrapperC(){
        std::cout << "~WrapperC()" << std::endl;
    }
    InnerB _inner;
};

执行结果,先调用B的构造,在调用C的构造。

析构时,先析构C再析构B

InnerB()
WrapperC()
~WrapperC()
~InnerB()

类默认构造是否必须实现

如果类中有继承关系或者其他类型的成员,默认构造函数是很有必要实现的。

系统提供的合成的默认构造函数不会对成员做初始化操作。

比如我们之后要学到的继承

class DerivedA: public BaseA {
public:
    DerivedA(std::string name,std::string num) :
    BaseA(name), _num(num) {
        std::cout << "DerivedA()" << std::endl;
    }
private:
   std::string _num;
};

调用

 DerivedA a("zack","1001");

this 指针的特性和用途

  1. 指向当前对象

    • this 指针是一个隐式参数,指向调用成员函数的对象。通过 this,你可以访问当前对象的属性和方法。
  2. 区分成员变量和参数

    • 在构造函数或成员函数中,参数名和成员变量可能同名。使用
 ```
 this
 ```



 可以明确指代成员变量。例如:

 ```cpp
 class MyClass {
 private:
     int value;
 public:
     MyClass(int value) {
         this->value = value; // 使用 this 指针区分成员变量和参数
     }
 };
 ```
  1. 返回当前对象

    • this
      
 可以用于返回当前对象的引用,以支持链式调用。例如:

 ```cpp
 class MyClass {
 private:
     int value;
 public:
     MyClass& setValue(int value) {
         this->value = value;
         return *this; // 返回当前对象的引用
     }
 };

 MyClass obj;
 obj.setValue(10).setValue(20); // 链式调用
 ```
  1. 在 const 成员函数中的使用

    • const 成员函数中,this 的类型为 const MyClass*,这意味着你不能通过 this 修改成员变量。这有助于确保对象的状态不被改变。
  2. 在静态成员函数中的不可用性

    • 静态成员函数没有 this 指针,因为它们不属于任何特定对象,而是属于类本身。因此,静态成员函数不能访问非静态成员变量和成员函数。

示例代码

以下是一个简单的示例,展示了 this 指针的用法:

#include <iostream>

class MyClass {
private:
    int value;

public:
    // 构造函数
    MyClass(int value) {
        this->value = value; // 使用 this 指针区分成员变量和参数
    }

    // 成员函数
    MyClass& setValue(int value) {
        this->value = value; // 使用 this 指针
        return *this; // 返回当前对象的引用
    }

    // 输出值
    void printValue() const {
        std::cout << "Value: " << this->value << std::endl; // 使用 this 指针
    }
};

int main() {
    MyClass obj(10);
    obj.printValue(); // 输出: Value: 10

    obj.setValue(20).printValue(); // 链式调用,输出: Value: 20

    return 0;
}

delete和default

C++11用法:

delete可以删除指定的构造函数。

default可以指定某个构造函数为系统默认合成。

class DefaultClass {
public:
    DefaultClass() = default;
    ~DefaultClass() = default;
    DefaultClass(const DefaultClass &) = delete;
    DefaultClass &operator=(const DefaultClass &) = delete;
    friend std::ostream& operator << (std::ostream &out, const DefaultClass &defaultClass);
private:
    int _num ;
};

主函数中调用

DefaultClass b;
std::cout << b << std::endl;

输出num是一个随机数

DefaultClass num is 331

什么是移动构造函数?与拷贝构造函数有何不同?

回答要点:

  • 定义:移动构造函数用于通过“移动”资源来初始化对象,而不是复制资源。

  • 语法:使用右值引用作为参数 (MyClass(MyClass&& other)).

  • 优势

    • 性能更高,避免不必要的深拷贝。
    • 适用于临时对象。
  • 区别

    • 拷贝构造函数复制资源,移动构造函数转移资源所有权。

示例:

class MyClass {
public:
    MyClass(MyClass&& other) noexcept { // 移动构造函数
        // 移动资源
    }
};

默认构造函数和用户定义的构造函数有什么区别?

回答要点:

  • 默认构造函数

    • 无参数的构造函数。
    • 如果没有用户定义的构造函数,编译器会自动生成一个默认构造函数。
  • 用户定义的构造函数

    • 开发者自定义的构造函数,可以有参数。
    • 一旦定义了任何构造函数,编译器不会再生成默认构造函数,除非显式声明。

示例:

class MyClass {
public:
    MyClass() { // 默认构造函数
        // 初始化代码
    }

    MyClass(int x) { // 有参数的构造函数
        // 初始化代码
    }
};

什么是初始化列表?为什么在构造函数中使用它?

回答要点:

  • 定义:初始化列表是在构造函数的参数列表之后,函数体之前,用于初始化成员变量的语法。

  • 优点

    • 提高性能,特别是对于常量成员或引用成员。
    • 必须用于初始化常量成员、引用成员以及基类。
    • 避免对象先默认构造再赋值,减少不必要的操作。

示例:

class MyClass {
    int x;
    const int y;
public:
    MyClass(int a, int b) : x(a), y(b) { // 初始化列表
        // 其他初始化代码
    }
};

什么是虚析构函数?为什么需要它?

回答要点:

  • 定义:在基类中将析构函数声明为virtual,以确保通过基类指针删除派生类对象时,能正确调用派生类的析构函数。

  • 用途

    • 防止内存泄漏。
    • 确保派生类的资源被正确释放。
  • 不使用虚析构函数的风险

    • 仅调用基类析构函数,导致派生类资源未释放。

示例:

如果BaseA的析构不写成虚析构,则主函数开辟子类对象赋值给基类指针,以后delete基类指针的时候会发现没有析构子类

class BaseA{
public:
    BaseA(std::string name):_name(name){
        std::cout << "BaseA()" << std::endl;
    }

    ~BaseA(){
        std::cout << "~BaseA()" << std::endl;
    }

private:
    std::string _name;
};

class DerivedA: public BaseA {
public:
    DerivedA(std::string name,std::string num) :
    BaseA(name), _num(num) {
        std::cout << "DerivedA()" << std::endl;
    }

    ~DerivedA(){
        std::cout << "~DerivedA()" << std::endl;
    }
private:
   std::string _num;
};

主函数回收内存

BaseA* base = new DerivedA("zack","1002");
delete base;

会看到只调用了基类BaseA的析构函数。

当BaseA的析构改为虚析构的时候,才会回收子类DerivedA

class BaseA{
public:
    BaseA(std::string name):_name(name){
        std::cout << "BaseA()" << std::endl;
    }

    virtual ~BaseA(){
        std::cout << "~BaseA()" << std::endl;
    }

private:
    std::string _name;
};

什么是委托构造函数?它是如何工作的?(C++11引入的特性)

回答要点:

  • 定义:一个构造函数可以调用同一类中的另一个构造函数,从而委托初始化任务。

  • 优点

    • 避免代码重复,提升代码可维护性。
  • 语法:使用构造函数初始化列表中的类名和参数。

示例:

class MyClass {
    int x;
    int y;
public:
    MyClass() : MyClass(0, 0) { } // 委托构造函数

    MyClass(int a, int b) : x(a), y(b) { }
};

什么是析构函数的顺序?

回答要点:

  • 成员变量的析构顺序:按照声明的逆序析构。
  • 继承关系的析构顺序:先析构派生类的成员和资源,再析构基类。
  • 全局/静态对象:按照创建的逆序析构。

示例说明:

class Base {
public:
    ~Base() { cout << "Base析构\n"; }
};

class Derived : public Base {
public:
    ~Derived() { cout << "Derived析构\n"; }
};

int main() {
    Derived obj;
    // 当obj被销毁时,首先调用Derived的析构函数,然后调用Base的析构函数。
}

输出:

Derived析构
Base析构

如何防止对象被复制?

回答要点:

  • C++11及以上:使用delete关键字显式删除拷贝构造函数和拷贝赋值运算符。
  • C++11之前:将拷贝构造函数和拷贝赋值运算符声明为私有且不实现。

示例(C++11及以上):

class NonCopyable {
public:
    NonCopyable() = default;
    NonCopyable(const NonCopyable&) = delete; // 禁止拷贝构造
    NonCopyable& operator=(const NonCopyable&) = delete; // 禁止拷贝赋值
};

构造函数中抛出异常会发生什么?

回答要点:

  • 对象未完全构造:如果构造函数中抛出异常,析构函数不会被调用,因为对象尚未完全构造。
  • 资源泄漏风险:如果在构造函数中分配了资源,需使用RAII(资源获取即初始化)类或智能指针来确保资源被正确释放。
  • 异常安全:确保在构造函数抛出异常时,任何已经初始化的成员都会被正确析构。

示例:

class MyClass {
    std::vector<int> data;
public:
    MyClass() {
        data.reserve(100);
        if (/* some condition */) {
            throw std::runtime_error("构造函数异常");
        }
    }
};

解释RAII及其与构造函数、析构函数的关系

回答要点:

  • RAII(资源获取即初始化)

    • 编程范式,通过对象的生命周期管理资源。
    • 资源在对象构造时获取,在对象析构时释放。
  • 关系

    • 构造函数负责获取资源。
    • 析构函数负责释放资源。
  • 优点

    • 自动管理资源,防止内存泄漏。
    • 异常安全,确保资源在异常发生时被释放。

示例:

class FileHandler {
    FILE* file;
public:
    FileHandler(const char* filename) {
        file = fopen(filename, "r");
        if (!file) throw std::runtime_error("打开文件失败");
    }
    ~FileHandler() {
        if (file) fclose(file);
    }
    // 禁止拷贝和移动
    FileHandler(const FileHandler&) = delete;
    FileHandler& operator=(const FileHandler&) = delete;
};

解释什么是赋值运算符重载?与拷贝构造函数有何不同?

回答要点:

  • 赋值运算符重载:通过重载operator=,定义对象之间的赋值行为。

  • 区别与拷贝构造函数

    • 拷贝构造函数用于初始化新对象。
    • 赋值运算符用于将一个已存在的对象赋值给另一个已存在的对象。
  • 常规实现

    • 检查自赋值。
    • 释放已有资源。
    • 复制资源。
    • 返回*this

示例:

class MyClass {
    int* data;
public:
    MyClass& operator=(const MyClass& other) { // 赋值运算符重载
        if (this == &other) return *this; // 自赋值检查
        delete data; // 释放已有资源
        data = new int(*other.data); // 复制资源
        return *this;
    }
};

解释静态成员变量在构造和析构中的处理

回答要点:

  • 静态成员变量

    • 属于类本身,而非任何对象实例。
    • 在程序开始时初始化,在程序结束时析构。
  • 构造顺序

    • 单例模式中,静态成员在第一次使用时构造。
  • 析构顺序

    • 按逆序构造顺序析构,确保依赖关系被正确处理。

示例:

class MyClass {
public:
    static MyClass instance;
    MyClass() { cout << "构造\n"; }
    ~MyClass() { cout << "析构\n"; }
};
//放在cpp中
MyClass MyClass::instance; // 静态成员变量定义

输出:

构造
析构

虚函数原理

包含虚函数的类构成

参考我得另一篇文章,https://llfc.club/category?catid=225RaiVNI8pFDD5L4m807g7ZwmF#!aid/2CmcOeP6BZMbtNiglPTkbmnpb73

虚继承与菱形继承问题

#include <iostream>
#include <string>

// 基类 Device
class Device {
public:
    std::string brand;

    Device(const std::string& brand_) : brand(brand_) {}

    void showBrand() const {
        std::cout << "Brand: " << brand << std::endl;
    }
};

// 派生类 Laptop,虚继承 Device
class Laptop : virtual public Device {
public:
    Laptop(const std::string& brand_) : Device(brand_) {}
};

// 派生类 Tablet,虚继承 Device
class Tablet : virtual public Device {
public:
    Tablet(const std::string& brand_) : Device(brand_) {}
};

// 派生类 Convertible
class Convertible : public Laptop, public Tablet {
public:
    Convertible(const std::string& brand_) : Device(brand_), Laptop(brand_), Tablet(brand_) {}
};

int main() {
    Convertible c("TechBrand");
    c.showBrand();
    return 0;
}

输出:

Brand: TechBrand

解析:

  • 在无虚继承的情况下,Convertible 类将拥有两份 Device 的成员变量,这会导致二义性问题。
  • 通过使用虚继承(virtual public),确保 Convertible 类只有一份 Device 的成员。
  • Convertible 的构造函数中,需要明确调用基类 Device 的构造函数,避免二义性。
  • main 函数中,创建一个 Convertible 对象,并调用 showBrand() 函数,正确显示品牌名称。

注意事项:

  • 菱形继承(多重继承导致的重复基类)可以通过虚继承来解决,确保共享同一份基类成员。
  • 虚继承会增加一定的开销,需根据具体需求权衡使用。

协变返回类型

#include <iostream>

// 基类
class Base {
public:
    virtual Base* clone() const {
        std::cout << "Base cloned." << std::endl;
        return new Base(*this);
    }

    virtual ~Base() {}
};

// 派生类
class Derived : public Base {
public:
    Derived* clone() const override { // 协变返回类型
        std::cout << "Derived cloned." << std::endl;
        return new Derived(*this);
    }
};

int main() {
    Base* b = new Base();
    Base* d = new Derived();

    Base* bClone = b->clone(); // 输出: Base cloned.
    Base* dClone = d->clone(); // 输出: Derived cloned.

    delete b;
    delete d;
    delete bClone;
    delete dClone;

    return 0;
}

输出:

Base cloned.
Derived cloned.

解析:

  • 基类 Base 定义了一个虚函数 clone(),返回 Base* 类型的指针。

  • 派生类 Derived 重写了 clone() 函数,返回类型为 Derived*,这是一种协变返回类型。

  main

函数中,通过基类指针调用

  clone()

函数:

  • 对于 Base 对象,调用 Base::clone()
  • 对于 Derived 对象,由于虚函数机制,调用 Derived::clone()
  • 协变返回类型允许派生类的重写函数返回更具体的类型,增强类型安全性和代码可读性。

注意事项:

  • 协变返回类型必须满足派生类返回类型是基类返回类型的派生类。
  • 编译器会检查协变返回类型的正确性,确保类型安全。

results matching ""

    No results matching ""