构造函数详解
- 1.构造函数的概念与特性
- 2.默认构造函数
- (1)概念
- (2)分类
- (3)工作原理
- 3.初始化列表
- (1)定义
- (2)为什么使用初始化列表
- (3)必须使用初始化列表的情况:
- 4.构造函数的使用
如果一个类中什么成员都没有,那么该类简称为空类。而空类中其实并不是真的什么都没有,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
- 构造函数:主要完成初始化工作
- 析构函数:主要完成资源的清理工作
- 拷贝构造函数:主要用于使用同类对象初始化创建对象
- 赋值运算符重载:主要是把一个对象赋值给另一个对象
- 普通对象取地址重载
- const对象取地址重载(取地址重载很少会自己实现)
本篇文章,我们来学习构造函数
1.构造函数的概念与特性
构造函数的概念:
对于以下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)”: 未调用原型函数(是否是有意用变量定义的?)
}
2.默认构造函数
(1)概念
如果类中没有显式定义构造函数,则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;
因为我们显示定义了构造函数,此时编译器不再自动生成无参的默认构造函数,我们也没有为它提供构造函数,此时编译器就会找不到合适的构造函数,因此会出错。这样做的目的也有可能是禁止使用者创建未明确初始化的对象
如果要创建对象而不显示地初始化,则必须定义一个不接受任何参数的默认构造函数。默认构造函数有两种,一种是全缺省地默认构造函数,另一种是无参的默认构造函数。
(2)分类
无参构造函数、全缺省构造函数、我们没写编译器默认生成的无参构造函数,都是默认构造函数,总的来说默认构造函数有两种,分别是无参构造函数和全缺省构造函数,建议使用全缺省默认构造函数,因为它很灵活,你可以传参数,也可以只传部分参数,也可以不传参数。默认构造函数只能有一个。
以下测试函数能通过编译吗?
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;
}
显然是不可以通过编译的,因为编译器会调用默认构造函数,但是编译器不知道该调用哪个默认构造函数。
(3)工作原理
在不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数似乎没有什么用,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;
}
3.初始化列表
(1)定义
与其他函数不同,构造函数除了有名字,参数列表和函数体之外,还可以有初始化列表,初始化列表以冒号开头,后跟一系列以逗号分隔的初始化字段。初始化列表是成员变量定义的地方,也就是为其开辟空间的地方。
具体我们来看下面代码,我们有一个日期类对象,我们可以这样来定义构造函数:
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;
};
(2)为什么使用初始化列表
如上两种方法来定义构造函数,一种是在构造函数体内对成员变量进行初始化,一种是使用初始化列表来初始化成员变量,都可以达到初始化成员变量的效果,两者有什么区别呢?
假如有如下需求,我们需要将日期类拓展为时间单位更精确。
//定义一个时间类
class Time
{
public:
Time(int hour = 0,int minute = 0,int second = 0)
{//定义一个全缺省构造函数
_hour = hour;
_minute = minute;
_second = second;
cout<<"Time();"<<endl;
}
protected:
int _hour;
int _minute;
int _second;
};
class Date
{
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 进行初始化时,这里的完成动作应该是调用一次构造函数,而运行出来结果却是两次。
这是因为所有构造函数都要走初始化列表,初始化列表会给每个成员都进行一次初始化,如果你的初始化列表显示初始化了某个成员,它就会去初始化,如果你没有显式初始化某个成员,它也会初始化,对于内置类型的成员变量,它会按照默认值进行初始化,如果没有设置默认值,就会用随机值进行初始化,对于自定义类型成员会调用它的默认构造函数进行初始化。
也就是说尽管你在函数体内进行初始化操作,实际上人家已经在初始化列表中就已经初始化过了,所以还不如在初始化列表中直接进行初始化操作,这样就可以减少默认构造函数调用的次数,这样就会更高效一些。
(3)必须使用初始化列表的情况:
除了性能问题之外,有些时候合初始化列表是不可或缺的,以下几种情况时必须使用初始化列表:
- 常量成员,因为常量只能初始化不能赋值,所以必须放在初始化列表里面
- 引用类型,引用必须在定义的时候初始化,并且不能重新赋值,所以也要写在初始化列表里面
- 没有默认构造函数的类类型,因为当你没有在初始化列表中对类类型成员进行初始化的时候,编译器会调用默认构造函数在初始化列表中来对其进行初始化,如果我们没有默认构造函数,那么就必须在初始化列表中调用其它构造函数来对该类类型的成员进行初始化,否则编译器调用默认构造函数在初始化列表中对其进行初始化的时候就会报错。
4.构造函数的使用
我们也无法像调用成员函数那样使用对象来调用构造函数,因为在构造函数构造出对象之前,对象是不存在的,因此构造函数被用来创建对象,而不能通过对象来调用。
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