【C++】02.对象的初始化和清理

【嵌入式八股】一、语言篇(本专栏)https://www.nowcoder.com/creation/manager/columnDetail/mwQPeM

【嵌入式八股】二、计算机基础篇https://www.nowcoder.com/creation/manager/columnDetail/Mg5Lym

【嵌入式八股】三、硬件篇https://www.nowcoder.com/creation/manager/columnDetail/MRVDlM

【嵌入式八股】四、嵌入式Linux篇https://www.nowcoder.com/creation/manager/columnDetail/MQ2bb0

对象的初始化和清理

构造函数和析构函数

虚函数相关可以先看多态部分

25.C++有哪几种构造函数
  1. 默认构造函数

    没有任何参数的构造函数被称为默认构造函数。如果没有定义构造函数,则编译器会自动提供默认构造函数。默认构造函数可以用来创建对象,但是不能传递任何参数。

#include <iostream>

class MyClass {
public:
    // 默认构造函数
    MyClass() {
        std::cout << "Default constructor called." << std::endl;
    }
};

int main() {
    // 使用默认构造函数创建对象
    MyClass obj; // 输出: "Default constructor called."
    return 0;
}
  1. 有参构造函数(初始化构造函数)

    带有一个或多个参数的构造函数被称为带参数的构造函数。带参数的构造函数可以用来初始化对象的成员变量,可以接受一个或多个参数。

class MyClass {
public:
    MyClass(int a, int b) {
        // 这里可以对成员变量进行初始化,使用参数a和b
    }
};
#include <iostream>

class MyClass {
public:
    // 带参数的构造函数
    MyClass(int value) : data(value) {
        std::cout << "Constructor with parameter called. Value: " << data << std::endl;
    }

private:
    int data;
};

int main() {
    // 使用带参数的构造函数创建对象
    MyClass obj(42); // 输出: "Constructor with parameter called. Value: 42"
    return 0;
}
  1. 拷贝构造函数

    用于从一个已经存在的对象中创建一个新的对象的构造函数被称为拷贝构造函数。拷贝构造函数接受一个参数,这个参数是同类型的另一个对象的引用。它通常用于在函数参数传递或返回对象时,或者在对象赋值时进行对象的拷贝。

class MyClass {
public:
    MyClass(const MyClass& other) {
        // 这里可以从另一个同类型的对象other中拷贝成员变量的值
    }
};
#include <iostream>

class MyClass {
public:
    // 拷贝构造函数
    MyClass(const MyClass& other) : data(other.data) {
        std::cout << "Copy constructor called. Value: " << data << std::endl;
    }

    // 带参数的构造函数
    MyClass(int value) : data(value) {}

private:
    int data;
};

int main() {
    // 使用拷贝构造函数创建对象
    MyClass obj1(42);
    MyClass obj2 = obj1; // 输出: "Copy constructor called. Value: 42"
    return 0;
}
  1. 移动构造函数

    用于从一个已经存在的临时对象中创建一个新的对象的构造函数被称为移动构造函数。它通常用于在对象的值被转移(比如将一个临时对象转移给一个新对象)时,避免不必要的拷贝操作,从而提高代码的性能。

class MyClass {
public:
    MyClass(MyClass&& other) {
        // 这里可以从另一个同类型的临时对象other中移动成员变量的值
    }
};
#include <iostream>

class MyClass {
public:
    // 移动构造函数
    MyClass(MyClass&& other) noexcept : data(other.data) {
        std::cout << "Move constructor called. Value: " << data << std::endl;
    }

    // 带参数的构造函数
    MyClass(int value) : data(value) {}

private:
    int data;
};

int main() {
    // 使用移动构造函数创建对象
    MyClass obj1(42);
    MyClass obj2 = std::move(obj1); // 输出: "Move constructor called. Value: 42"
    return 0;
}
  1. 转换构造函数 一个构造函数接收一个不同于其类类型的形参,可以视为将其形参转换成类的一个对象。像这样的构造函数称为转换构造函数。在 C++ string 类中可以找到使用转换构造函数的实用示例。string 类提供一个将 C 字符串转换为 string 的转换构造函数
class string
{
    //仅显示转换构造函数
    public:
        string(char *);//形参时其他类型变量,且只有一个形参
};
26.一个类中的全部构造函数的扩展过程是什么?
  1. 记录在成员初始化列表中的数据成员初始化操作会被放在构造函数的函数体内,并与成员的声明顺序为顺序;
  2. 如果一个成员并没有出现在成员初始化列表中,但它有一个默认构造函数,那么默认构造函数必须被调用;
  3. 如果class有虚表,那么它必须被设定初值;
  4. 所有上一层的基类构造函数必须被调用;
  5. 所有虚基类的构造函数必须被调用。

在 C++ 中,一个类中的构造函数包括默认构造函数、拷贝构造函数、移动构造函数和其他自定义构造函数等。这些构造函数在被调用时,都要经过扩展过程,扩展过程包括以下步骤:

  1. 访问控制:在调用构造函数之前,首先要进行访问控制检查,以确保调用方有权访问该构造函数。如果构造函数是私有或受保护的,则只有该类或其友元类才能调用。
  2. 空间分配:在调用构造函数之前,必须先为该类的对象分配空间。如果是在栈上创建对象,则分配的是栈空间;如果是在堆上创建对象,则分配的是堆空间。
  3. 成员变量初始化:在构造函数被调用之前,编译器会自动对成员变量进行初始化。对于内置类型的成员变量,它们会被初始化为默认值,而对于自定义类型的成员变量,它们会调用相应的构造函数进行初始化。
  4. 执行构造函数体:构造函数被调用时,会执行构造函数体内的语句,完成对象的初始化操作。构造函数体可以包括任何语句,例如赋值语句、函数调用语句等。
  5. 返回对象:当构造函数执行完毕后,返回该类的对象。在栈上创建的对象会自动被销毁,而在堆上创建的对象需要手动释放。

需要注意的是,如果一个类定义了多个构造函数,则在调用时会根据传递的参数类型和数量来匹配相应的构造函数。如果找不到匹配的构造函数,则会导致编译错误。另外,如果一个类定义了默认构造函数,则可以通过在创建对象时不传递任何参数来调用该构造函数。

27.什么情况会自动生成默认构造函数?

什么情况下会合成构造函数?

  1. 没有定义任何构造函数:如果我们没有定义任何构造函数,则编译器会自动生成一个默认构造函数。
  2. 定义了其他构造函数:如果我们定义了其他构造函数,但没有定义默认构造函数,则编译器会自动生成一个默认构造函数。
  3. 派生类没有显式定义构造函数:如果派生类没有显式定义构造函数,而基类没有提供带参数的构造函数,则编译器会自动生成默认构造函数。
  4. 带有虚函数的类,虚函数的引入需要进入虚表,指向虚表的指针,该指针是在构造函数中初始化的,所以没有构造函数的话该指针无法被初始化;
  5. 带有一个虚基类的类

还需要注意的是:

  1. 并不是任何没有构造函数的类都会合成一个构造函数
  2. 编译器合成出来的构造函数并不会显示设定类内的每一个成员变量
28.构造函数、拷贝构造函数和赋值操作符的区别

构造函数

对象不存在,没用别的对象初始化,在创建一个新的对象时调用构造函数

拷贝构造函数

对象不存在,但是使用别的已经存在的对象来进行初始化

赋值运算符

对象存在,用别的对象给它赋值,这属于重载“=”号运算符的范畴,“=”号两侧的对象都是已存在的

以下是C++中构造函数、拷贝构造函数和赋值操作符的代码示意:

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

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

// 创建对象
MyClass obj1; // 调用默认构造函数
MyClass obj2(10); // 调用带参数的构造函数
  1. 拷贝构造函数:
class MyClass {
public:
    // 拷贝构造函数
    MyClass(const MyClass& other) {
        // 复制值的代码
    }
};

// 创建对象
MyClass obj1;
MyClass obj2 = obj1; // 调用拷贝构造函数
  1. 赋值操作符:
class MyClass {
public:
    // 赋值操作符
    MyClass& operator=(const MyClass& other) {
        // 分配新值的代码
        return *this;
    }
};

// 创建对象
MyClass obj1;
MyClass obj2;
obj2 = obj1; // 调用赋值操作符
29.拷贝构造函数和赋值运算符重载的区别?

拷贝初始化和直接初始化(赋值)的区别

  • 拷贝构造函数是函数,赋值运算符是运算符重载。
  • 拷贝构造函数会生成新的类对象,赋值运算符不能。
  • 拷贝构造函数是直接构造一个新的类对象,所以在初始化对象前不需要检查源对象和新建对象是否相同;赋值运算符需要上述操作并提供两套不同的复制策略,另外赋值运算符中如果原来的对象有内存分配则需要先把内存释放掉。
30.为什么拷贝构造函数必须传引用不能传值?

因为传值会触发无限递归的调用,导致栈溢出或者程序崩溃

class MyClass {
public:
  MyClass(const MyClass& other) {
    //拷贝构造函数实现
  }
};

如果我们尝试传递一个MyClass对象作为参数来调用拷贝构造函数,例如:

MyClass obj1;
MyClass obj2 = obj1; //调用拷贝构造函数

如果拷贝构造函数采用值传递的方式,那么它会尝试复制obj1,这个过程中又会调用拷贝构造函数,而这个拷贝构造函数又会尝试复制obj1,以此类推,导致无限递归调用,最终程序崩溃。

31.C++中拷贝赋值函数的形参能否进行值传递?

如果是拷贝赋值函数

C++中拷贝赋值函数的形参可以进行值传递,这并不会影响拷贝赋值函数的正确性。但是,值传递可能会导致性能上的损失,因为它需要将参数的副本传递给函数,这可能会涉及到内存分配和复制操作,对于大型对象来说,这种开销会更加明显。

通常来说,对于自定义类型,建议采用传递引用或指针的方式进行拷贝赋值函数的参数传递。这样可以避免不必要的内存分配和复制,提高程序的性能。

下面是一个使用值传递方式进行拷贝赋值函数参数传递的示例代码:

#include <iostream>

using namespace std;

class Point {
public:
    Point(int x = 0, int y = 0) : x_(x), y_(y) {}
    Point(const Point& other) : x_(other.x_), y_(other.y_) {}
    Point& operator=(Point other) {  
        swap(x_, other.x_);
        swap(y_, other.y_);
        return *this;
    }
private:
    int x_;
    int y_;
};

int main() {
    Point p1(1, 2);
    Point p2(3, 4);
    p1 = p2; // 使用拷贝赋值函数进行赋值
    return 0;
}
32.什么情况下会调用拷贝构造函数

什么时候需要合成拷贝构造函数呢?

当满足以下条件之一时,会调用拷贝构造函数:

  1. 使用一个对象去初始化同类型的另一个对象,例如:

    MyClass obj1;           // 调用默认构造函数
    MyClass obj2(obj1);     // 调用拷贝构造函数
    MyClass obj3 = obj1;    // 调用拷贝构造函数
    
  2. 将一个对象作为实参传递给一个函数时,如果函数参数是按值传递的,则会调用拷贝构造函数来创建该参数的副本,例如:

    void func(MyClass obj);  // 参数按值传递
    // ...
    MyClass obj1;
    func(obj1);              // 调用拷贝构造函数
    
  3. 从一个函数返回一个对象时,如果返回值类型是该对象的类类型,并且返回的对象是在函数中创建的局部对象或匿名对象,则会调用拷贝构造函数来初始化返回值,例如:

    MyClass func() {
        MyClass obj;        // 创建局部对象
        return obj;         // 调用拷贝构造函数
    }
    

需要注意的是,如果一个类没有显式地定义拷贝构造函数,编译器会自动生成一个默认的拷贝构造函数。这个默认的拷贝构造函数会按照成员变量的拷贝方式逐个拷贝对象的成员变量。但是,如果类的成员变量是指针或引用类型,则默认的拷贝构造函数只会拷贝指针或引用本身,而不会拷贝指针或引用指向的对象。这可能会导致浅拷贝问题,因此在这种情况下,需要显式地定义拷贝构造函数来实现深拷贝。

其他情况:

  1. 如果一个类没有拷贝构造函数,但是含有一个类类型的成员变量,该类型含有拷贝构造函数,此时编译器会为该类合成一个拷贝构造函数;
  2. 如果一个类没有拷贝构造函数,但是该类继承自含有拷贝构造函数的基类,此时编译器会为该类合成一个拷贝构造函数;
  3. 如果一个类没有拷贝构造函数,但是该类声明或继承了虚函数,此时编译器会为该类合成一个拷贝构造函数;
  4. 如果一个类没有拷贝构造函数,但是该类含有虚基类,此时编译器会为该类合成一个拷贝构造函数;
33.如何禁止程序自动生成拷贝构造函数?

可以通过将拷贝构造函数声明为私有成员函数来禁止程序自动生成拷贝构造函数,具体的做法是:

class MyClass {
public:
    // 其他构造函数的声明

private:
    // 禁止拷贝构造函数自动生成
    MyClass(const MyClass&);
};

通过将拷贝构造函数声明为私有成员函数,可以防止程序在需要拷贝对象时自动调用拷贝构造函数。如果程序中需要拷贝该类的对象(类的成员函数和friend函数还是可以调用private函数,如果这个private函数只声明不定义,则会产生一个连接错误),编译器会在编译时报错。

需要注意的是,将拷贝构造函数声明为私有成员函数会影响该类对象的拷贝行为,可能会导致一些问题,因此应该慎重使用。如果确实需要禁止拷贝行为,建议使用 C++11 中引入的 =delete 关键字来禁止拷贝构造函数,具体做法如下:

class MyClass {
public:
    // 其他构造函数的声明

    // 禁止拷贝构造函数
    MyClass(const MyClass&) = delete;
};

这种方式可以在编译时直接阻止程序调用拷贝构造函数,避免了由于访问限制导致的问题。

34.说一说你了解到的移动构造函数?

结合右值引用部分知识学习。

有时候我们会遇到这样一种情况,我们用对象a初始化对象b后对象a我们就不在使用了,但是对象a的空间还在呀(在析构之前),既然拷贝构造函数,实际上就是把a对象的内容复制一份到b中,那么为什么我们不能直接使用a的空间呢?这样就避免了新的空间的分配,大大降低了构造的成本。这就是移动构造函数设计的初衷。

移动构造函数是C++11引入的一个新特性,它可以用来实现对象的移动语义。它通过将一个右值引用(即一个将要销毁的临时对象)的成员变量“移动”到一个新对象中,避免了对象的复制操作,从而提高了程序的性能。

移动构造函数的语法如下:

class MyClass {
public:
    MyClass(MyClass&& other) {
        // 将 other 中的资源移动到 this 中
    }
};

其中,参数 other 是一个右值引用,表示一个将要销毁的临时对象,而 this 则表示要移动资源的目标对象。

移动构造函数一般需要进行以下操作:

  1. 将 other 对象中的资源转移到 this 对象中,通常使用 std::move() 函数来实现:
MyClass(MyClass&& other) {
    m_resource = std::move(other.m_resource);
}
  1. 将 other 对象的状态置为“无效状态”,以避免资源被重复释放:
MyClass(MyClass&& other) {
    m_resource = std::move(other.m_resource);
    other.m_resource = nullptr; // 将 other 的状态置为无效
}

需要注意的是,移动构造函数只能接受右值引用作为参数,不能接受左值引用或常量引用。另外,如果我们定义了移动构造函数,则编译器不会自动生成拷贝构造函数,因为这两个函数的实现方式类似,容易出现二义性。

35.构造函数的几种关键字
  1. explicit:这个关键字用于将构造函数声明为显式构造函数,防止隐式转换发生。如果将构造函数声明为显式构造函数,则不能使用隐式转换来调用该函数。例如:
class MyClass {
public:
    explicit MyClass(int value) {
        // 构造函数代码
    }
};

MyC

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

【嵌入式八股】一、语言篇 文章被收录于专栏

查阅整理上千份嵌入式面经,将相关资料汇集于此,主要包括: 0.简历面试 1.语言篇【本专栏】 2.计算机基础 3.硬件篇 4.嵌入式Linux (建议PC端查看)

全部评论

相关推荐

评论
1
1
分享
牛客网
牛客企业服务