前言
在C语言中,当我们定义了一个结构体时,通常需要编写一个函数来初始化它,否则在创建结构体变量时容易忘记调用初始化函数,导致程序出错。但在C++中,我们将不会有这样的烦恼,前提是编写了正确的构造函数。
构造函数
构造函数是类的特殊成员函数之一,在实例化对象时由编译器自动调用,且在对象整个生命周期内仅调用一次。尽管名为“构造”,但构造函数的主要任务并非是开空间创建对象,而是初始化对象。当对象被定义时,对象的整体空间被分配,但对象内的成员变量还未定义,构造函数的作用就是完成对象成员变量的初始化工作。需要注意的是,构造函数不能被显式调用,而是在对象创建时自动执行。
接下来以日期类为例,演示一下构造函数的一些书写规则:
class Date{public: // 构造函数 Date(int year, int month, int day) { _year = year; _month = month; _day = day; } // print 函数稍后用来测试,打印一下数据 void print() { cout << _year << "-" << _month << "-" << _day << endl; }private: int _year; int _month; int _day;};
函数名字与类名相同,没有返回值(也不用写 void),且可以被重载。需要注意的是,编译器会在每个成员函数中隐藏一个指向对象本身的指针,即 `this` 指针,以便在函数内部访问对象的成员。
来看下它的使用:
int main(){ Date d1(2024, 1, 29); d1.print(); return 0;}
程序运行起来没有任何问题,但是很显然现在的构造函数是有缺陷的,在定义对象时必须给参数,因此我们需要重载一个无参的构造:
// 在Date类中的,考虑到篇幅,就不每次都把整个类写出来 Date() { _year = 1; _month = 1; _day = 1; }
但是这样还是有些多余,我们完全可以利用缺省参数,把有参无参合二为一:
Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; }
这样Date类的构造函数就比较完善了。
能否不写?
如果我们不写构造函数会发生什么?程序会崩溃吗?这里先给一个结论:当没有显示定义构造函数时,编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。那么问题来了,编译器生成的默认构造是什么行为呢?它会怎么去初始化我们的对象?先通过下面这段代码来探究一下:
class Date{public: void print() { cout << _year << "-" << _month << "-" << _day << endl; }private: int _year; int _month; int _day;};int main(){ // 注意这里不传参数时不要带括号,不然成函数调用了!!! Date d1; d1.print(); return 0;}
现在我们的Date类完全是在裸奔,我们定义一个对象,并调用它的print函数,发现得到的全是随机值。可见编译器生成的默认构造对内置类型不处理,值是不确定的。那对于自定义类型呢?继续通过下面这一段代码来探究一下:
class Stack{public: Stack(int capacity = 4) { cout << "Stack(int capacity = 4)" << endl; }private: int* _a; int _size; int _capacity;};class MyQueue{public:private: Stack _s1; Stack _s2;};int main(){ MyQueue q;return 0;}
这里栈的构造函数我们让它简单打印一下信息,看它有没有被调用 。程序运行后我们发现打印了两次信息,可见栈的构造函数被调用了两次。可见编译器生成的默认构造,对于自定义类型会去调用它的默认构造。
这里提出了一个概念——默认构造,稍微解释一下什么是默认构造:1. 无参数的构造函数 2. 有参数但同时是全缺省的构造函数 3. 编译器生成的。这三种都能称为默认构造,但是很显然这三种在一个类里只会存在一个,简而言之,不需要传参就能调用的都可以称为默认构造。
倘若这个栈没有默认构造(把缺省值去掉),那么不好意思,编译器报错。这种情况怎么办呢?这时候就需要初始化列表登场了。
初始化列表
构造函数通常可以分为两个部分:初始化列表和函数体。在构造函数体内,我们可以进行变量的赋值操作,严格来说这并不是初始化,而是赋初值。因为初始化只能进行一次,而函数体内的赋值操作可以多次执行。因此成员变量的定义(初始化)是在初始化列表中进行的,
你可能会好奇为什么需要初始化列表,在函数体内赋值不也好好的,但是有些成员变量在定义时必须初始化,如:引用、const 修饰的成员变量。还有就是像上面提的没有默认构造函数的自定义类型成员也可以在初始化列表中通过传参来调用构造,或者不想用它的默认构造也可以在初始化列表里传参构造。
通过这段代码来看看用法:
class Stack{public: // 语法规定这样写 Stack(int capacity = 4) :_capacity(capacity) ,_size(0) ,_a(nullptr) { cout << "Stack(int capacity = 4)" << endl; }private: int* _a; int _size; int _capacity;};class MyQueue{public: MyQueue() {}private: Stack _s1; Stack _s2;};int main(){ MyQueue q;return 0;}
对于MyQueue来说,它的构造函数虽然什么都没有,但是它还是会去调用自定义类型成员的默认构造,因为初始化列表才是成员变量定义的地方,而初始化列表中又没有给自定义类型成员显示定义。
class Stack{public: Stack(int capacity) :_capacity(capacity) ,_size(0) ,_a(nullptr) { cout << "Stack(int capacity)" << endl; }private: int* _a; int _size; int _capacity;};class MyQueue{public: MyQueue() :_s1(1) ,_s2(2) {}private: Stack _s1; Stack _s2;};int main(){ MyQueue q;return 0;}
现在对于没有默认构造函数的自定义类型成员,只需要在初始化列表中传参数就行了。
C++11中新增了一个特性,可以在成员变量的声明那里给缺省值,这个缺省值实际上就是在构造函数的初始化列表中使用的初始值。如下所示:
class MyQueue{public: MyQueue() {}private: Stack _s1 = 1; Stack _s2 = 2;};
值得一提的是,这里除了缺省值,还涉及到隐式类型转换。
小题目
class A{public: A(int a) :_a1(a) ,_a2(_a1) {} void Print() { cout << _a1 << " " << _a2 << endl; }private: int _a2; int _a1;};int main(){ A aa(1); aa.Print(); return 0;}
这个程序在vs下运行得到的是1和随机值,因为成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
补充
一般情况下,我们需要自己编写构造函数。然而,如果成员变量在声明时已经有了缺省值,或者这些成员变量是自定义类型且具有默认构造函数,则可以考虑不编写构造函数。 值得注意的是,构造函数可以设为私有,这样做的话,外部代码将无法直接实例化对象,只能通过特殊手段如定位 new 来创建对象。