本篇文章,我们来学习构造函数
构造函数的概念:
对于以下Date类:
class Date
{
public:void Init(int year, int month, int day){_year = year;_month = month;_day = day;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}
private:int _year;int _month;int _day;
};
int main()
{Date d1;d1.Init(2022, 7, 5);d1.Print();Date d2;d2.Init(2022, 7, 6);d2.Print();return 0;
}
对于Date类,可以通过 Init 公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置信息,未免有点麻烦,我们可以使用构造函数在对象创建时就将信息设置进去。
构造函数是一个特殊的成员函数,名字与类名相同,用于给对象的成员变量赋初始值,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
构造函数的特性:
函数名与类名相同。
无返回值,但也没有被声明为void类型
对象实例化时编译器自动调用对应的构造函数。
构造函数可以重载,编译器根据参数选择调用合适的构造函数
构造函数可以无参,也可以带参。如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
class Date
{
public:// 1.无参构造函数Date(){}// 2.带参构造函数Date(int year, int month, int day){_year = year;_month = month;_day = day;}
private:int _year;int _month;int _day;
};
void TestDate()
{Date d1; // 调用无参构造函数Date d2(2015, 1, 1); // 调用带参的构造函数Date d3();//因为通过无参构造函数创建对象时带了括号,这成了函数声明,声明了d3函数,该函数无参,返回一个日期类型的对象,会报错// warning C4930: “Date d3(void)”: 未调用原型函数(是否是有意用变量定义的?)
}
如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义了构造函数,编译器将不再自动生成无参的默认构造函数,我们就必须自己为它提供默认构造函数
class Date
{
public:// 如果用户显式定义了构造函数,编译器将不再生成Date(int year, int month, int day){_year = year;_month = month;_day = day;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}
private:int _year;int _month;int _day;
};
int main()
{
// 将Date类中构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函数
// 将Date类中构造函数放开,代码编译失败,因为一旦显式定义任何构造函数,编译器将不再生成无参的默认 构造函数
// 放开后报错:error C2512: “Date”: 没有合适的默认构造函数可用Date d1;return 0;
}
如果我们提供了构造函数而没有提供默认构造函数,那么下面的声明将会出错
Date d;
因为我们显示定义了构造函数,此时编译器不再自动生成无参的默认构造函数,我们也没有为它提供构造函数,此时编译器就会找不到合适的构造函数,因此会出错。这样做的目的也有可能是禁止使用者创建未明确初始化的对象
如果要创建对象而不显示地初始化,则必须定义一个不接受任何参数的默认构造函数。默认构造函数有两种,一种是全缺省地默认构造函数,另一种是无参的默认构造函数。
无参构造函数、全缺省构造函数、我们没写编译器默认生成的无参构造函数,都是默认构造函数,总的来说默认构造函数有两种,分别是无参构造函数和全缺省构造函数,建议使用全缺省默认构造函数,因为它很灵活,你可以传参数,也可以只传部分参数,也可以不传参数。默认构造函数只能有一个。
以下测试函数能通过编译吗?
class Date
{
public:Date(){_year = 1900;_month = 1;_day = 1;}Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}private:int _year;int _month;int _day;
};void Test()
{Date d1;
}
显然是不可以通过编译的,因为编译器会调用默认构造函数,但是编译器不知道该调用哪个默认构造函数。

在不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数似乎没有什么用,d对象调用了编译器生成的默认构造函数,但是d对象_year/_month/_day,依旧是随机值。
默认构造函数真的没有用吗?
C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的基本数据类型,如:int/char…,自定义类型就是我们使用class/struct/union等自己定义的类型。
默认构造函数对内置类型成员变量不做处理,C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。
默认构造函数对于自定义类型成员变量才会处理,它会去调用自定义类型的默认构造函数。
何时使用默认构造函数就够了,何时需要自己实现构造函数呢?
满足下列两个条件使用默认构造函数就够了:
如果有内置类型的成员变量且需要显示传参初始化,或者自定义类型成员没有构造函数,那么就要自己实现构造函数。
我们运行下下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员函数。
class Time
{
public:Time(){cout << "Time()" << endl;_hour = 0;_minute = 0;_second = 0;}
private:int _hour;int _minute;int _second;
};
class Date
{
private:// 基本类型(内置类型)int _year;int _month;int _day;// 自定义类型Time _t;
};
int main()
{Date d;return 0;
}

与其他函数不同,构造函数除了有名字,参数列表和函数体之外,还可以有初始化列表,初始化列表以冒号开头,后跟一系列以逗号分隔的初始化字段。初始化列表是成员变量定义的地方,也就是为其开辟空间的地方。
具体我们来看下面代码,我们有一个日期类对象,我们可以这样来定义构造函数:
class Date
{ public: Date(int year,int month,int day) { _year = year; _month = month; _day = day; } protected: int _year; int _month; int _day;
};
也可以利用初始化列表:
class Date
{ public: Date(int year,int month,int day) :_year(year),_month(month),_day(day) //使用初始话列表初始化成员变量 { } protected: int _year; int _month; int _day;
};
如上两种方法来定义构造函数,一种是在构造函数体内对成员变量进行初始化,一种是使用初始化列表来初始化成员变量,都可以达到初始化成员变量的效果,两者有什么区别呢?
假如有如下需求,我们需要将日期类拓展为时间单位更精确。
//定义一个时间类
class Time
{public:Time(int hour = 0,int minute = 0,int second = 0){//定义一个全缺省构造函数_hour = hour;_minute = minute;_second = second;cout<<"Time();"<public:Date(int year,int month,int day,int hour,int minute,int second){_year = year;_month = month;_day = day;_time = Time(hour,minute,second);}protected:int _year;int _month;int _day;Time _time;//时间类作为日期类的成员变量
};int main()
{Date d(1999, 3, 26, 12, 18, 24);return 0;
}
运行结果如下:

在创建一个Time类匿名变量来对_time 进行初始化时,这里的完成动作应该是调用一次构造函数,而运行出来结果却是两次。
这是因为所有构造函数都要走初始化列表,初始化列表会给每个成员都进行一次初始化,如果你的初始化列表显示初始化了某个成员,它就会去初始化,如果你没有显式初始化某个成员,它也会初始化,对于内置类型的成员变量,它会按照默认值进行初始化,如果没有设置默认值,就会用随机值进行初始化,对于自定义类型成员会调用它的默认构造函数进行初始化。
也就是说尽管你在函数体内进行初始化操作,实际上人家已经在初始化列表中就已经初始化过了,所以还不如在初始化列表中直接进行初始化操作,这样就可以减少默认构造函数调用的次数,这样就会更高效一些。
除了性能问题之外,有些时候合初始化列表是不可或缺的,以下几种情况时必须使用初始化列表:
我们也无法像调用成员函数那样使用对象来调用构造函数,因为在构造函数构造出对象之前,对象是不存在的,因此构造函数被用来创建对象,而不能通过对象来调用。
C++提供了多种初始化对象的方式。
第一种方式是隐式地调用构造函数:
其功能可以描述为创建一个名为d的Date对象,并将其数据成员初始化为指定的值。
Date d(1999,3,26);
第二种方式是显示地调用构造函数:
这种方法不是对d进行初始化,而是将新值赋值给它。通过让构造函数创建一个新的临时对象,然后将其内容复制给d,然后程序调用析构函数,以删除该临时对象。(有些编译器可能要过一段时间才会删除临时对象,因此析构函数的调用将延迟)
Date d = Date(1999,3,26);
如果既可以通过初始化,也可以通过赋值来设置对象的值,则应采用初始化方式。通常这种方式的效率更高。
第三种方式是结合new运算符和隐式调用:
Date* d = new Date(1999,3,26);
第四种方式是使用列表初始化:
只要提供与某个构造函数的参数列表相匹配的内容,并用大括号将它们括起(C++11):
Date d = {1999,3,26};
Date d {1999,3,26};
Date* d = new d{1999,3,26};
这些列表与下面的构造函数相匹配:
Date(int year,int month,int date);
无参的时候不可以像上面那样隐式地调用构造函数,因为编译器无法区分这是函数的声明还是对象的定义。
Date d();//error
Date d;//correct
上一篇:形容“甜”,有哪些句子?
下一篇:有人和我一样讨厌早起吗?