MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++类构造函数自动调用的原理

2021-05-145.7k 阅读

C++类构造函数自动调用的原理

构造函数的基本概念

在C++ 中,构造函数是一种特殊的成员函数,它与类同名,并且没有返回类型(包括void也没有)。构造函数主要用于初始化类对象的数据成员。当一个类的对象被创建时,构造函数会自动被调用,为对象分配内存并初始化其成员变量。

例如,我们定义一个简单的Point类:

class Point {
public:
    int x;
    int y;
    Point() {
        x = 0;
        y = 0;
    }
};

在上述代码中,Point()就是Point类的构造函数。当创建Point类的对象时,这个构造函数会自动被调用,将xy初始化为0。

自动调用构造函数的时机

  1. 对象声明时:当在代码中声明一个类的对象时,构造函数会立即被调用。
class Circle {
public:
    double radius;
    Circle() {
        radius = 1.0;
    }
};

int main() {
    Circle myCircle;
    return 0;
}

main函数中,声明Circle myCircle;时,Circle类的构造函数Circle()会自动被调用,将myCircleradius初始化为1.0。

  1. 动态分配内存时:使用new关键字动态分配类对象的内存时,构造函数同样会被调用。
class Rectangle {
public:
    int width;
    int height;
    Rectangle() {
        width = 10;
        height = 5;
    }
};

int main() {
    Rectangle* myRectangle = new Rectangle();
    delete myRectangle;
    return 0;
}

这里new Rectangle()会调用Rectangle类的构造函数,为新分配的Rectangle对象初始化widthheight

  1. 函数参数传递时:当类对象作为函数参数按值传递时,构造函数会被调用,用于创建参数的副本。
class Triangle {
public:
    int side1;
    int side2;
    int side3;
    Triangle() {
        side1 = side2 = side3 = 1;
    }
};

void printTriangle(Triangle t) {
    std::cout << "Sides: " << t.side1 << " " << t.side2 << " " << t.side3 << std::endl;
}

int main() {
    Triangle myTriangle;
    printTriangle(myTriangle);
    return 0;
}

printTriangle(myTriangle)调用中,myTriangle按值传递给函数,会调用Triangle类的构造函数创建一个副本。

  1. 函数返回类对象时:当函数返回一个类对象时,构造函数会被调用,用于创建返回的对象。
class Square {
public:
    int side;
    Square() {
        side = 1;
    }
};

Square createSquare() {
    Square s;
    return s;
}

int main() {
    Square mySquare = createSquare();
    return 0;
}

createSquare函数返回Square对象时,会调用Square类的构造函数创建返回的对象。

构造函数自动调用的底层机制

  1. 栈对象创建:当在栈上声明一个类对象时,编译器会在对象声明处插入代码来调用构造函数。例如,对于前面的Point类:
int main() {
    Point p;
    return 0;
}

编译器会生成类似这样的汇编代码(简化示意,实际汇编会更复杂且依赖于具体编译器和平台):

main:
    push rbp
    mov rbp, rsp
    sub rsp, 16 ; 为Point对象p分配空间
    lea rax, [rbp - 16] ; 获取p的地址
    call Point::Point() ; 调用构造函数
    xor eax, eax
    leave
    ret

这里,编译器在为Point对象p分配栈空间后,通过call Point::Point()调用了构造函数。

  1. 堆对象创建:使用new关键字创建堆对象时,new操作符首先会调用operator new函数来分配内存,然后在分配的内存上调用构造函数。
class Book {
public:
    std::string title;
    Book() {
        title = "Default Book";
    }
};

int main() {
    Book* myBook = new Book();
    delete myBook;
    return 0;
}

new Book()操作大致分为两步:

  • 调用operator new分配内存,例如void* mem = operator new(sizeof(Book));
  • 在分配的内存mem上调用Book类的构造函数,::new(mem) Book();
  1. 对象拷贝:在对象按值传递或函数返回对象时,会涉及对象的拷贝构造函数调用。编译器会在需要创建副本的地方插入代码来调用拷贝构造函数。例如,对于前面Triangle类的函数参数传递:
void printTriangle(Triangle t) {
    std::cout << "Sides: " << t.side1 << " " << t.side2 << " " << t.side3 << std::endl;
}

int main() {
    Triangle myTriangle;
    printTriangle(myTriangle);
    return 0;
}

编译器会生成代码来调用Triangle类的拷贝构造函数(如果没有自定义拷贝构造函数,编译器会生成默认的拷贝构造函数),在printTriangle函数栈帧中创建myTriangle的副本。

构造函数调用顺序

  1. 基类与派生类:当一个派生类对象被创建时,首先会调用基类的构造函数,然后再调用派生类自身的构造函数。
class Animal {
public:
    Animal() {
        std::cout << "Animal constructor called" << std::endl;
    }
};

class Dog : public Animal {
public:
    Dog() {
        std::cout << "Dog constructor called" << std::endl;
    }
};

int main() {
    Dog myDog;
    return 0;
}

上述代码中,创建Dog对象myDog时,会先调用Animal类的构造函数,输出Animal constructor called,然后调用Dog类的构造函数,输出Dog constructor called

  1. 成员对象初始化顺序:如果一个类包含其他类类型的成员对象,这些成员对象的构造函数会按照它们在类中声明的顺序被调用,而不是按照在构造函数初始化列表中的顺序。
class Engine {
public:
    Engine() {
        std::cout << "Engine constructor called" << std::endl;
    }
};

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

class Car {
public:
    Engine engine;
    Wheel wheel;
    Car() : wheel(), engine() {
        std::cout << "Car constructor called" << std::endl;
    }
};

int main() {
    Car myCar;
    return 0;
}

Car类中,engine声明在wheel之前,所以创建Car对象myCar时,会先调用Engine类的构造函数,输出Engine constructor called,然后调用Wheel类的构造函数,输出Wheel constructor called,最后调用Car类的构造函数,输出Car constructor called

初始化列表与构造函数体

  1. 初始化列表:构造函数的初始化列表用于在构造函数体执行之前初始化类的成员变量。
class Person {
public:
    std::string name;
    int age;
    Person(const std::string& n, int a) : name(n), age(a) {
        // 构造函数体可以有其他逻辑代码
    }
};

在上述Person类的构造函数中,name(n)age(a)是初始化列表,在构造函数体执行之前,name被初始化为nage被初始化为a

  1. 构造函数体赋值:也可以在构造函数体中对成员变量进行赋值,但这与初始化列表有本质区别。
class Rectangle {
public:
    int width;
    int height;
    Rectangle(int w, int h) {
        width = w;
        height = h;
    }
};

这里在构造函数体中对widthheight进行赋值。对于基本数据类型,这种方式和使用初始化列表效果类似,但对于类类型成员变量,使用初始化列表效率更高,因为初始化列表直接调用成员对象的构造函数进行初始化,而在构造函数体中赋值会先调用成员对象的默认构造函数,然后再进行赋值操作。

例如:

class MyString {
public:
    char* data;
    MyString(const char* str) {
        // 这里假设简单的字符串复制实现
        data = new char[strlen(str) + 1];
        strcpy(data, str);
    }
    ~MyString() {
        delete[] data;
    }
};

class User {
public:
    MyString username;
    User(const char* uname) {
        username = MyString(uname); // 先调用MyString默认构造函数,再赋值
    }
};

更好的方式是使用初始化列表:

class User {
public:
    MyString username;
    User(const char* uname) : username(uname) {
        // 直接调用MyString构造函数初始化username
    }
};

这样可以避免不必要的默认构造函数调用和赋值操作,提高效率。

构造函数的重载

  1. 概念:一个类可以有多个构造函数,只要它们的参数列表不同,这就是构造函数的重载。
class Box {
public:
    int length;
    int width;
    int height;
    Box() {
        length = width = height = 1;
    }
    Box(int l, int w, int h) {
        length = l;
        width = w;
        height = h;
    }
};

Box类中,有两个构造函数,一个无参数,一个有三个参数。这两个构造函数构成重载关系。

  1. 调用时机:根据对象创建时提供的参数,编译器会自动选择合适的构造函数进行调用。
int main() {
    Box box1; // 调用无参数构造函数
    Box box2(10, 5, 3); // 调用有三个参数的构造函数
    return 0;
}

委托构造函数

  1. 概念:C++11 引入了委托构造函数,允许一个构造函数调用同一个类的其他构造函数。
class Employee {
public:
    std::string name;
    int age;
    double salary;
    Employee(const std::string& n, int a, double s) : name(n), age(a), salary(s) {}
    Employee(const std::string& n, int a) : Employee(n, a, 0.0) {}
};

Employee类中,Employee(const std::string& n, int a)构造函数委托了Employee(const std::string& n, int a, double s)构造函数,通过这种方式可以复用代码,减少重复逻辑。

  1. 执行顺序:委托构造函数会先调用被委托的构造函数,然后再执行自身构造函数体中的代码(如果有)。
class Product {
public:
    std::string name;
    double price;
    Product(const std::string& n, double p) : name(n), price(p) {
        std::cout << "Main constructor called" << std::endl;
    }
    Product(const std::string& n) : Product(n, 0.0) {
        std::cout << "Delegating constructor called" << std::endl;
    }
};

int main() {
    Product product("Widget");
    return 0;
}

上述代码中,创建Product对象product时,先调用被委托的构造函数Product(const std::string& n, double p),输出Main constructor called,然后调用委托构造函数Product(const std::string& n),输出Delegating constructor called

构造函数与异常处理

  1. 异常抛出:构造函数可以抛出异常,用于表示对象初始化过程中出现的错误。
class DatabaseConnection {
public:
    DatabaseConnection(const std::string& url) {
        // 假设这里尝试连接数据库
        if (!connectToDatabase(url)) {
            throw std::runtime_error("Failed to connect to database");
        }
    }
    ~DatabaseConnection() {
        // 关闭数据库连接
        closeDatabaseConnection();
    }
private:
    bool connectToDatabase(const std::string& url);
    void closeDatabaseConnection();
};

DatabaseConnection类的构造函数中,如果连接数据库失败,会抛出std::runtime_error异常。

  1. 异常处理:当构造函数抛出异常时,已经部分构造的对象会被析构(如果对象部分构造成功,其已构造的成员对象会调用各自的析构函数)。
int main() {
    try {
        DatabaseConnection conn("invalid_url");
    } catch (const std::runtime_error& e) {
        std::cerr << "Exception: " << e.what() << std::endl;
    }
    return 0;
}

main函数中,try - catch块捕获构造函数抛出的异常,并输出错误信息。

总结构造函数自动调用原理的要点

  1. 对象创建触发:无论是在栈上声明对象、使用new动态分配内存,还是在函数参数传递和返回时创建对象,构造函数都会自动被调用。
  2. 底层机制:栈对象创建时编译器在声明处插入调用构造函数的代码;堆对象创建通过operator new分配内存后调用构造函数;对象拷贝涉及拷贝构造函数调用。
  3. 调用顺序:基类构造函数先于派生类构造函数调用,类中成员对象按声明顺序调用构造函数。
  4. 初始化方式:初始化列表在构造函数体之前执行,对于类类型成员变量使用初始化列表效率更高。
  5. 重载与委托:构造函数可以重载以满足不同的初始化需求,委托构造函数可以复用代码。
  6. 异常处理:构造函数可抛出异常,异常抛出时部分构造的对象会被析构。

通过深入理解C++类构造函数自动调用的原理,开发者能够更好地控制对象的初始化过程,编写出更健壮、高效的代码。无论是简单的类还是复杂的继承体系和包含多个成员对象的类,都能正确地进行初始化和处理可能出现的异常情况。同时,合理利用构造函数的特性,如初始化列表、重载和委托构造函数等,可以提高代码的可读性和性能。