秋招C++八股--类中的重要函数(持续更新)

大海之阔,非一流之归也

1 构造函数和析构函数的作用?

构造函数(Constructor):

  1. 构造函数是一种特殊的成员函数,用于在创建对象时进行初始化操作。-- 作用
  2. 构造函数的名称与类名相同,没有返回类型,不需要显式调用,会在对象创建时自动调用。-- 调用时机
  3. 可以有多个构造函数,通过参数的不同进行重载,允许对象以不同的方式进行初始化。-- 变化
  4. 可以设置默认参数,使部分参数变为可选。
  5. 构造函数可以用于执行一些初始化操作,如分配内存、初始化成员变量等。

析构函数(Destructor):

  1. 析构函数是一种特殊的成员函数,用于在对象销毁时进行清理操作。-- 作用
  2. 析构函数的名称与类名相同,前面加上一个波浪号(~),没有返回类型,不需要显式调用,会在对象销毁时自动调用。
  3. 与构造函数不同,类只有一个析构函数,不可以重载。
  4. 析构函数可以用于执行一些清理操作,如释放内存、关闭文件等。

一些注意事项:

  1. 如果一个类没有定义构造函数,编译器会自动生成一个默认构造函数,如果有构造函数定义,则默认构造函数不会被生成。
  2. 析构函数通常用来释放在对象生命周期内分配的资源,如动态分配的内存、打开的文件等。
  3. 当一个对象被删除、离开作用域或者显式销毁时,其析构函数会被自动调用。
  4. 在继承关系中,析构函数按照派生类到基类的顺序调用,即先派生类的析构函数再基类的析构函数。
  5. 构造函数和析构函数不允许被声明为虚函数,因为虚函数的调用需要虚函数表,而在对象的构造和析构过程中,虚函数表的状态可能不稳定。

构造函数和析构函数的执行顺序

#include <iostream>

class Base {
public:
    Base() {
        std::cout << "Base Constructor" << std::endl;
    }
    
    ~Base() {
        std::cout << "Base Destructor" << std::endl;
    }
};

class Derived : public Base {
public:
    Derived() {
        std::cout << "Derived Constructor" << std::endl;
    }
    
    ~Derived() {
        std::cout << "Derived Destructor" << std::endl;
    }
};

int main() {
    Derived derivedObj;
    return 0;
}

2 类什么时候会被析构?

  1. 对象生命周期结束,被销毁时;
  2. delete指向对象的指针时,或delete指向对象的基类类型指针,而其基类析构函数是虚函数时;
  3. 对象i是对象o的成员,o的析构函数被调用时,对象i的析构函数也被调用。
#include <iostream>

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

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

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

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

    ObjectI i; // ObjectI对象作为ObjectO的成员
};

int main() {
    std::cout << "Creating ObjectO o" << std::endl;
    ObjectO o; // 创建ObjectO对象,会触发ObjectO的构造函数,以及ObjectI的构造函数

    std::cout << "Exiting main" << std::endl;
    // 当main函数结束时,对象o会被销毁,触发ObjectO的析构函数,以及ObjectI的析构函数
    return 0;
}
Creating ObjectO o
ObjectI Constructor
ObjectO Constructor
Exiting main
ObjectO Destructor
ObjectI Destructor

3 构造函数和析构函数可以调用虚函数吗?

  1. 在C++中,提倡不在构造函数和析构函数中调用虚函数

  2. 构造函数和析构函数调用虚函数时都不使用动态联编,如果在构造函数或析构函数中调用虚函数,则,运行的是为构造函数或析构函数自身类型定义的版本;--> 不会呈现出多态

在C++中,动态联编是通过虚函数(Virtual Function)实现的。当一个成员函数被声明为虚函 时,它可以在派生类中被重写(覆盖)。然后,在基类的指针或引用指向派生类的对象时,调用该虚> 函数时会根据实际对象的类型调用正确的版本,而不是仅仅根据指针或引用的静态类型。

  1. 因为父类对象会在子类之前进行构造,此时子类部分的数据成员还未初始化,因此调用子类的虚函数 时不安全的,故而C++不会进行动态联编;--> 实际上调用的是父类的函数
#include <iostream>

class Base {
public:
    Base() {
        std::cout << "Base constructor" << std::endl;
        callVirtualFunction(); // 在基类构造函数中调用虚函数
    }

    virtual void callVirtualFunction() {
        std::cout << "Base::callVirtualFunction()" << std::endl;
    }
};

class Derived : public Base {
public:
    Derived() {
        std::cout << "Derived constructor" << std::endl;
    }

    void callVirtualFunction() override {
        std::cout << "Derived::callVirtualFunction()" << std::endl;
    }
};

int main() {
    Derived d;
    return 0;
}

  1. 析构函数是用来销毁一个对象的,在销毁一个对象时,先调用子类的析构函数,然后再调用基类的析 构函数。所以在调用基类的析构函数时,派生类对象的数据成员已经销毁,这个时候再调用子类的虚函数没有任何意义。
#include <iostream>

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

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

class Derived : public Base {
private:
    int data;

public:
    Derived(int value) : data(value) {
        std::cout << "Derived constructor" << std::endl;
    }

    ~Derived() override {
        std::cout << "Derived destructor, data = " << data << std::endl;
    }
};

int main() {
    Base* basePtr = new Derived(42);
    delete basePtr;
    
    return 0;
}

4 类成员初始化方式?构造函数的执行顺序 ?为什么用成员初始化列表会快一些?

4-1 有几种初始化方式?区别是?

类成员可以通过多种方式进行初始化:

  1. 默认构造函数初始化:如果类成员具有默认构造函数,它们将在类对象创建时自动调用,默认初始化。

  2. 成员初始化列表:在构造函数的初始化列表中,可以通过使用冒号来初始化类成员。这是一种推荐的方式,尤其是当类成员没有默认构造函数或需要特定值进行初始化时。

  3. 赋值初始化:在构造函数的函数体内,可以使用赋值操作来初始化类成员。这会在构造函数体内部进行初始化。

  • 赋值初始化:在构造函数体内进行初始化,是在所有数据成员被分配内存空间后才进行的。这意味着分配了内存空间后,会调用默认构造函数初始化成员,然后在构造函数体内使用赋值操作进行赋值。这样可能会涉及两次操作,一次是构造函数体内的默认初始化,一次是赋值。

    class MyClass {
        int x;
        double y;
    public:
        MyClass(int a, double b) {
            x = a;
            y = b;
            // 构造函数体
        }
    };
    
  • 成员初始化列表:这是一种更高效的方式,它在分配内存空间时就会调用适当的构造函数进行初始化。它适用于没有默认构造函数或需要特定值初始化的成员。这样可以避免不必要的初始化和赋值操作,提高性能。

    class MyClass {
        int x;
        double y;
    public:
        MyClass(int a, double b) : x(a), y(b) {
            // 构造函数体
        }
    };
    

C++的赋值操作是会产生临时对象的。临时对象的出现会降低程序的效率。

代码运行结果可以看出,在构造函数体内部初始化的对象b多了一次构造函数的调用过程,而对象a则 没有。由于对象成员变量的初始化动作发生在进入构造函数之前,对于内置类型没什么影响,

如果有些成员是类,那么在进入构造函数之前,会先调用一次默认构造函数,进入构造函数后所做的事其实是 一次赋值操作(对象已存在),

所以如果是在构造函数体内进行赋值的话,等于是一次默认构造加一次赋值,而初始化列表只做一次赋值操作。

#include <iostream>
using namespace std;
class A
{
public:
    A()
    {
        cout << "默认构造函数A()" << endl;
    }
    A(int a)
    {
        value = a;
        cout << "A(int " << value << ")" << endl;
    }
    A(const A& a)
    {
        value = a.value;
        cout << "拷贝构造函数A(A& a): " << value << endl;
    }
    int value;
};

class B
{
public:
    B() : a(1) // 一次赋值
    {
        b = A(2); // 构造 + 赋值
    }
    A a;
    A b;
};

int main()
{
    B b;
}
//输出结果:
//A(int 1)
//默认构造函数A()
//A(int 2)

4-2 一个派生类构造函数的执行顺序?

虚拟基类的构造函数(多个虚拟基类则按照继承的顺序执行构造函数)。

基类的构造函数(多个普通基类也按照继承的顺序执行构造函数)。

③ 类类型的成员对象的构造函数(按照初始化顺序)

派生类自己的构造函数

#include <iostream>

// 虚拟基类
class Base {
public:
    Base() {
        std::cout << "Base constructor" << std::endl;
    }
};

// 普通基类
class NormalBase {
public:
    NormalBase() {
        std::cout << "NormalBase constructor" << std::endl;
    }
};

// 类类型的成员对象
class Member {
public:
    Member() {
        std::cout << "Member constructor" << std::endl;
    }
};

// 派生类,继承虚拟基类和普通基类,包含类类型的成员对象
class Derived : virtual public Base, public NormalBase {
private:
    Member member;

public:
    Derived() {
        std::cout << "Derived constructor" << std::endl;
    }
};

int main() {
    Derived derived;  // 创建派生类对象
    return 0;
}

输出:
Base constructor
NormalBase constructor
Member constructor
Derived constructor

什么是虚拟基类?

虚拟基类是在C++中用于解决多重继承引发的菱形继承问题的一种机制。在多重继承中,如果一个派生类从多个类继承,而这些基类之间又存在共同的基类,那么在派生类中会存在多个共同基类的实例,导致数据重复和访问混乱。

虚拟基类通过在继承关系中指定虚拟继承,从而避免了共同基类在派生类中的重复实例化。当一个类被声明为虚拟基类时,在派生类的构造函数中,该虚拟基类只会被构造一次,确保了共同基类的唯一性。

#include <iostream>

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

class Intermediate1 : public virtual Base {
public:
    Intermediate1() {
        std::cout << "Intermediate1 constructor" << std::endl;
    }
};

class Intermediate2 : public virtual Base {
public:
    Intermediate2() {
        std::cout << "Intermediate2 constructor" << std::endl;
    }
};

class Derived : public Intermediate1, public Intermediate2 {
public:
    Derived() {
        std::cout << "Derived constructor" << std::endl;
    }
};

int main() {
    Derived d;  // 创建派生类对象
    return 0;
}

5 拷贝初始化和直接初始化

当用于类类型对象时,初始化的拷贝形式和直接形式有所不同:直接初始化直接调用与实参匹配的 构造函数,拷贝初始化总是调用拷贝构造函数。拷贝初始化首先使用指定构造函数创建一个临时对象,然后用拷贝构造函数将那个临时对象拷贝到正在创建的对象。举例如下

	string str1("I am a string");//语句1 直接初始化
	string str2(str1);//语句2 直接初始化,str1是已经存在的对象,直接调用构造函数对str2进行初始化
	string str3 = "I am a string";//语句3 拷贝初始化,先为字符串”I am a string“创建临时对象,再把临时对象作为参数,使用拷贝构造函数构造str3
	string str4 = str1;//语句4 拷贝初始化,这里相当于隐式调用拷贝构造函数,而不是调用赋值运算符函数

#include <iostream>
#include <cstring>
using namespace std;

class Person {
public:
    char* name; // 姓名
    int age;    // 年龄

    Person(const char* name, int age) {
        this->name = new char[strlen(name) + 1];
        strcpy_s(this->name, strlen(name) + 1, name); // 使用strcpy_s替代strcpy
        this->age = age;
    }

    // 拷贝构造函数
    // 通过使用常量引用,可以避免对象拷贝和数据修改的开销,同时保证拷贝构造函数的安全性和正确性。
    Person(const Person& p) {
        this->name = new char[strlen(p.name) + 1];
        strcpy_s(this->name, strlen(p.name) + 1, p.name);
        this->age = p.age;
    }
    void showPerson() {
        cout << name << " " << age << endl;
    }

    ~Person() {
        if (name != nullptr) {
            delete[] name;
            name = nullptr;
        }
    }

};

int main() {

    char name[100] = { 0 };
    int age;

    cin >> name;
    cin >> age;

    Person p1(name, age);
    Person p2 = p1;

    p2.showPerson();

    return 0;
}

6 为什么析构函数一般写成虚函数

当基类指针指向派生类对象并被用于删除对象时,如果析构函数不被声明为虚函数,编译器将使用静态绑定(静态联编),只会调用基类的析构函数,而不会调用派生类的析构函数。这会导致派生类对象的析构函数不会被正确调用,从而导致派生类中的资源无法得到释放,可能引发内存泄漏或其他错误。

通过将析构函数声明为虚函数,使用动态绑定(动态联编),在删除对象时会根据指针所指向的实际对象类型来调用正确的析构函数。这样可以确保派生类的析构函数被正确地调用,释放派生类中的资源,然后再调用基类的析构函数,实现完整的对象销毁过程。

C++的动态联编(Dynamic Binding)是一种在运行时确定调用的函数版本的机制,也称为运行时多态(Runtime Polymorphism)。它允许通过基类的指针或引用来调用派生类中重写的虚函数。

在使用动态联编时,通过将基类的函数声明为虚函数(使用virtual关键字),可以实现函数的动态绑定。当通过基类的指针或引用调用虚函数时,实际调用的是基类指针或引用所指向的对象的类型所对应的虚函数版本,而不是基类的函数版本。

#include <iostream>
using namespace std;

class Parent {
public:
    Parent() {
        cout << "Parent construct function" << endl;
    }
    ~Parent() {
        cout << "Parent destructor function" << endl;
    }
};

class Son : public Parent {
public:
    Son() {
        cout << "Son construct function" << endl;
    }
    ~Son() {
        cout << "Son destructor function" << endl;
    }
};

int main() {
    Parent* p = new Son();
    delete p;
    p = NULL;
    return 0;
}
32	//Parent construct function
33	//Son construct function
34	//Parent destructor function 
#include <iostream>
using namespace std;

class Parent {
public:
    Parent() {
        cout << "Parent construct function" << endl;
    }
    virtual ~Parent() {
        cout << "Parent destructor function" << endl;
    }
};

class Son : public Parent {
public:
    Son() {
        cout << "Son construct function" << endl;
    }
    ~Son() {
        cout << "Son destructor function" << endl;
    }
};

int main() {
    Parent* p = new Son();
    delete p;
    p = NULL;
    return 0;
}
32	//Parent construct function
33	//Son construct function
34	//Son destructor function
35	//Parent destructor function 

7 构造函数和析构函数能否声明为虚函数?

  1. 析构函数:
    • 析构函数可以声明为虚函数,一般情况下基类的析构函数应该声明为虚函数
    • 基类析构函数声明为虚函数可以确保通过基类指针删除派生类对象时,能够正确调用派生类和基类的析构函数,实现多态的析构行为。
    • 析构函数可以是纯虚函数,将含有纯虚函数的类称为抽象类,无法实例化抽象类的对象,但派生类可以根据自身需求重新实现基类中的纯虚函数。
  2. 构造函数:
    • 构造函数不能声明为虚函数。在构造函数中可以调用虚函数,但此时调用的是正在构造的类中的虚函数,而不是子类的虚函数。因为在对象构造过程中,子类尚未构造完成。
    • 虚函数对应一个虚函数表(vtable),类中存储一个虚函数指针(vptr)指向该虚函数表。但构造函数在对象尚未初始化完成之前,无法访问虚函数表和虚函数指针,因此构造函数不能是虚函数。

从存储空间角度来看,虚函数表指针(vptr)实际上存储在对象的内存空间中。虚函数表是用于存储虚函数的地址的数据结构,它使得在运行时能够动态地确定要调用的虚函数。

构造函数不能是虚函数的原因有多个方面:

  1. 对象在实例化之前,内存空间还不存在,因此无法通过虚函数表找到对应的虚函数。虚函数表的指针(vptr)是在对象实例化之后才会被设置。
  2. 虚函数主要用于在对象的类型不完全已知的情况下,能够根据实际对象的类型来调用对应的重载函数。而构造函数的目的是初始化对象实例,不存在对象类型不完全已知的情况。
  3. 构造函数是在创建对象时主动调用的,不会通过父类的指针或引用来调用,因此不需要虚函数的多态性。

另外,构造函数的作用是提供对象的初始化,它只在对象的生命周期中运行一次,不属于对象的动态行为,因此也没有必要将构造函数定义为虚函数。

在实现上,虚函数表指针(vptr)在构造函数调用后才会被建立。构造函数只能知道它是当前类的构造函数,并不考虑后续是否还有派生类。编译器为构造函数生成的代码是针对当前类的构造函数,不是针对基类或派生类的(因为类并不知道谁会继承它)。因此,在构造函数中使用的虚函数表指针必须是指向当前类的虚函数表。

综上所述,构造函数不需要是虚函数,并且也不应该是虚函数。创建对象时通常会明确指定对象的类型,虽然可以通过基类的指针或引用来访问对象,但析构函数往往是通过基类的指针来销毁对象的,此时如果析构函数不是虚函数,就无法正确识别对象的类型从而无法正确调用析构函数。

另外,需要注意的是,C++中基类采用虚析构函数的主要目的是为了防止内存泄漏。具体来说,如果派生类中有申请了内存空间的操作,并在派生类的析构函数中对这些内存空间进行释放,如果基类的析构函数不是虚函数,那么在删除基类指针指向的派生类对象时,只会调用基类的析构函数而不会调用派生类的析构函数,导致派生类中申请的空间无法被释放,从而产生内存泄漏。因此,基类的析构函数应该采用虚析构函数来确保在删除对象时正确调用派生类的析构函数。

8 什么是纯虚析构函数?为什么不要把虚析构函数定义为纯虚析构函数?

纯虚析构函数是指在基类中将析构函数声明为纯虚函数(纯虚函数是通过在函数声明中使用= 0来实现的)。纯虚析构函数的声明告诉编译器该类是一个抽象类,不能直接实例化对象,而只能被用作其他类的基类。

为什么不应该将虚析构函数定义为纯虚析构函数呢?

  1. 纯虚析构函数需要被派生类实现:由于纯虚函数需要在派生类中被实现,而析构函数是在对象销毁时自动调用的,因此无法保证在派生类中正确实现纯虚析构函数。这会导致派生类无法正常销毁对象,可能引发未定义的行为。
  2. 析构函数的职责:析构函数在销毁对象时执行清理操作,例如释放动态分配的资源。如果将析构函数定义为纯虚函数,就无法在基类中提供默认的清理操作,因为纯虚函数没有默认实现。派生类也无法继承基类的析构函数实现,这会破坏对象销毁的机制。

9 C++ 有哪几种构造函数

默认构造函数(Default Constructor):没有参数的构造函数被称为默认构造函数。如果在类定义中没有显式定义构造函数,编译器会自动生成默认构造函数。默认构造函数用于创建对象时不需要传递参数的情况。

带参数构造函数(Parameterized Constructor):带参数的构造函数接受一个或多个参数,并使用这些参数来初始化对象的成员变量。它允许在创建对象时传递参数并进行初始化。

拷贝构造函数(Copy Constructor):拷贝构造函数用于创建一个新对象,该对象是通过使用同一类的另一个对象进行初始化的。它通常以引用形式接受一个同类对象作为参数,并创建一个新的对象,将原始对象的值复制到新对象中。

移动构造函数(Move Constructor):移动构造函数是C++11引入的特性,用于高效地将资源(如动态分配的内存)从一个对象转移到另一个对象,而不需要执行深拷贝。移动构造函数通常以右值引用形式接受参数,并从源对象“窃取”资源。

#include <iostream>

using namespace std;

class Student {
public:
    Student() {// 默认
        this->age = 20;
        this->num = 1000;
    };

    Student(int a, int n) : age(a), num(n) {};// 初始化构造

    Student(const Student& s) {// 拷贝构造
        this->age = s.age;
        this->num = s.num;
    };

    Student(int r) {
        this->age = r;
        this->num = 1002;
    };

    ~Student() {}

public:
    int age;
    int num;
};

int main() {
    Student s1;
    Student s2(18, 1001);
    int a = 10;
    Student s3(a);
    Student s4(s3);

    printf("s1 age:%d, num:%d\n", s1.age, s1.num);// 20 1000
    printf("s2 age:%d, num:%d\n", s2.age, s2.num);// 18 1001
    printf("s3 age:%d, num:%d\n", s3.age, s3.num);// 10 1002
    printf("s2 age:%d, num:%d\n", s4.age, s4.num);// 10 1002

    return 0;
}

#include <iostream>

class MyClass {
public:
    int* data;

    // 构造函数
    MyClass(int value) {
        data = new int(value);
        std::cout << "Constructor called. Value: " << *data << std::endl;
    }

    // 移动构造函数
    MyClass(MyClass&& other) noexcept {
        data = other.data;
        other.data = nullptr;
        std::cout << "Move constructor called. Moved value: " << *data << std::endl;
    }

    // 析构函数
    ~MyClass() {
        if (data != nullptr) {
            delete data;
        }
        std::cout << "Destructor called." << std::endl;
    }
};

int main() {
    MyClass obj1(10);

    // 使用移动构造函数将 obj1 的资源移动给 obj2
    MyClass obj2(std::move(obj1));

    // 注意:此时 obj1 的 data 指针已经为 nullptr
    std::cout << "Value in obj2: " << *(obj2.data) << std::endl;

    return 0;
}
输出
Constructor called.Value: 10
Move constructor called.Moved value : 10
Value in obj2 : 10
Destructor called.
Destructor called.

1 拷贝构造函数和移动构造函数的区别?

拷贝构造函数和移动构造函数都是用于创建对象的特殊成员函数,它们之间的区别如下:

参数类型:

  • 拷贝构造函数的参数是一个常量引用(const T&),用于从同类型的对象进行拷贝。
  • 移动构造函数的参数是一个非常量引用(T&&),用于从同类型的对象进行移动。

语义:

  • 拷贝构造函数创建一个新对象,并将原始对象的值复制到新对象中。这涉及到对资源的复制,如堆内存的分配和数据的复制。
  • 移动构造函数将原始对象的资源所有权转移到新对象中,而不进行资源的复制。这通常涉及到指针的转移和资源的移动。

对象状态:

  • 拷贝构造函数会保留原始对象的状态,包括其值和所有权。
  • 移动构造函数将原始对象的状态转移给新对象,原始对象的状态变为有效但未定义的状态。

使用情境:

  • 拷贝构造函数通常用于对象的复制、传递参数和返回值等场景,其中需要创建对象的副本。
  • 移动构造函数通常用于性能优化,避免不必要的资源复制,特别是在临时对象的创建和销毁过程中,如函数返回值优化和使用 std::move() 进行显式移动语义。

10 什么时候调用拷贝构造函数?

  1. 用类的一个实例化对象去初始化另一个对象时,函数的参数是类的对象时(非引用传递)。
  2. 函数的返回值是函数体内局部对象的类的对象时,且返回方式是值传递。在某些编译器和平台下,可能会发生Named Return Value (NRV)优化,从而避免拷贝构造函数的调用。但在一些情况下,仍会调用拷贝构造函数。

需要注意的是,第二种情况中的优化行为可能因编译器和平台而异。在不同的编译器和平台下,对于返回局部对象的值传递方式,可能会有不同的优化行为。例如,在Linux上使用g++编译器时,即使发生NRV优化,值返回方式仍可能调用拷贝构造函数,而引用返回方式则不会调用拷贝构造函数。而在Windows上使用VS2019编译器时,在值返回方式下仍会调用拷贝构造函数。

总结来说,如果发生NRV优化,对于引用返回方式,不会调用拷贝构造函数;对于值返回方式,不同的编译器和平台可能会有不同的行为,有些情况下可能调用拷贝构造函数。

#include<iostream>
using namespace std;

class A {
public:
	A() {};
	A(const A& a)
	{
		cout << "copy constructor is called" << endl;
	};
	~A() {};
};

void useClassA(A a) {}

A getClassA()
{
	A a;
	return a;
}


int main()
{
	A a1, a3, a4;
	A a2 = a1;  //调用拷贝构造函数,对应情况1
	useClassA(a1);//调用拷贝构造函数,对应情况2
	a3 = getClassA();//虽然是 但是值返回,但是发生NRV优化,,不会有拷贝构造函数的调用
	return 0;
}

11 构造or析构函数可否抛出异常?

  1. 在C++中,只有已经完成构造的对象才会被析构。对象的构造函数在执行完毕后才算是完全构造妥当。如果在构造函数中发生异常,并且控制权离开构造函数,则对象的析构函数不会被调用。这可能导致内存泄漏或资源没有正确释放。
  2. 使用auto_ptr(已弃用)或unique_ptr等智能指针可以代替裸指针作为类成员,这样可以增强构造函数的安全性。智能指针会在对象生命周期结束时自动释放资源,避免了在析构函数中手动释放资源的问题。
  3. 如果异常从析构函数中抛出,并且没有在析构函数中进行捕获处理,那么C++会调用terminate函数终止程序的执行。这是因为析构函数不应该抛出异常,否则会导致程序处于不确定的状态
  4. 如果析构函数抛出异常并且没有被捕获处理,那么析构函数就会执行不完全。也就是说,析构函数没有完成其应该执行的每一项任务。这会导致程序无法正常清理资源,可能引发其他问题或导致程序的不可预测行为。

12 什么情况下会生成默认构造函数?

如果一个类没有任何构造函数,但是它包含一个成员对象,而该成员对象具有默认构造函数,那么编译器将会为该类合成一个默认构造函数。这个合成的构造函数只有在真正需要构造函数时才会生成。

如果一个派生类没有任何构造函数,并且派生自一个带有默认构造函数的基类,那么编译器会为该派生类合成一个构造函数,调用上一层基类的默认构造函数。

如果一个类带有一个虚函数(即至少有一个虚函数声明),编译器会为该类合成一个默认构造函数。这是为了支持动态绑定(运行时多态性)。

如果一个类带有一个虚基类(即在类层次结构中,通过多重继承共享相同的基类),编译器会为该类合成一个默认构造函数。这是为了正确初始化虚基类。

合成的默认构造函数只会初始化基类子对象和成员类对象,其他非静态数据成员不会被初始化。这意味着除了基类子对象和成员类对象之外,其他数据成员的初始值是未定义的。

13 静态函数能定义为虚函数吗?常函数呢?

静态成员函数无法定义为虚函数。虚函数的调用是通过对象的指针或引用来实现的,而静态成员函数不属于任何特定的对象或实例,它是与类本身相关联的。由于静态成员函数没有this指针,无法访问虚函数的调用机制,因此将静态成员函数定义为虚函数是没有意义的。

静态成员函数与非静态成员函数之间的主要区别在于静态成员函数没有this指针。

this指针是指向当前对象的指针,在非静态成员函数中可用于访问类的成员变量和其他成员函数。然而,静态成员函数不依赖于特定的对象,因此没有this指针可用。

虚函数的调用依赖于vptr(虚函数表指针)和vtable(虚函数表)的机制。vptr是一个指针,它在对象的内存布局中存在,并且通过this指针访问。vptr指向保存虚函数地址的vtable。由于静态成员函数没有this指针,无法访问vptr和vtable,因此无法实现虚函数的调用关系。

因此,静态成员函数不能定义为虚函数。虚函数的调用关系是通过this指针、vptr和vtable来处理的,而静态成员函数无法访问这些机制。

*什么是 this 指针?

当你定义一个类的成员函数时,编译器会为该函数隐式地添加一个额外的参数,这个参数被命名为 this,它是一个指向当前对象的指针。通过 this 指针,你可以在成员函数中访问当前对象的成员变量和调用其他成员函数。

#include <iostream>

class MyClass {
public:
    int x;

    void printX() {
        std::cout << "x = " << x << std::endl;
    }

    void setX(int value) {
        x = value;
    }

    void printAddress() {
        std::cout << "this = " << this << std::endl;
    }
};

int main() {
    MyClass obj1;
    obj1.x = 5;

    MyClass obj2;
    obj2.x = 10;

    obj1.printX();  // 输出: x = 5
    obj2.printX();  // 输出: x = 10

    obj1.setX(7);
    obj1.printX();  // 输出: x = 7

    obj1.printAddress();  // 输出: this = 0x7ffc0b92f6d0
    obj2.printAddress();  // 输出: this = 0x7ffc0b92f6d4

    return 0;
}

#秋招##23届找工作求助阵地#
C++ 校招面试精解 文章被收录于专栏

适用于 1.C++基础薄弱者 2.想快速上手C++者 3.秋招C++方向查漏补缺者 4.秋招C++短期冲刺者

全部评论
第6节标题不太准确,应该是只有作为多态基类或者带有virtual的类才需要虚析构函数,不是所有类的析构函数都应该是虚函数,声明一个虚析构函数会产生虚表,对于不是用于多态的类会有一些性能损失,这一块在Effective C++的条款7里写的比较明白。
1
送花
回复
分享
发布于 2023-06-25 17:24 广东
为啥找不到大厂实习,是不是大厂不用cpp啊,身边前端同学都有大厂实习😭😭
1
送花
回复
分享
发布于 2023-06-26 11:21 江苏
秋招专场
校招火热招聘中
官网直投
m
点赞
送花
回复
分享
发布于 2023-06-24 18:42 北京
m
点赞
送花
回复
分享
发布于 2023-06-25 16:37 浙江
m
点赞
送花
回复
分享
发布于 2023-06-25 22:26 美国
M
点赞
送花
回复
分享
发布于 2023-06-25 23:57 辽宁
m
点赞
送花
回复
分享
发布于 2023-06-26 17:30 北京
m
点赞
送花
回复
分享
发布于 2023-06-27 10:18 安徽
m
点赞
送花
回复
分享
发布于 2023-06-27 14:19 北京
m
点赞
送花
回复
分享
发布于 2023-06-27 20:11 江苏
😤
点赞
送花
回复
分享
发布于 2023-06-27 20:37 北京
博主,我经过测试,发现常函数能定义为虚函数 virtual getData const(){ return data;} 能编译和运行
点赞
送花
回复
分享
发布于 2023-06-30 22:58 江苏
m
点赞
送花
回复
分享
发布于 2023-10-06 16:12 上海
你好,第1个问题里的注意事项1和第12个问题不冲突吗
点赞
送花
回复
分享
发布于 03-29 14:22 浙江

相关推荐

16 116 评论
分享
牛客网
牛客企业服务