对于下面的日期类:
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, d2;
d1.Init(2022, 5, 15);
d1.Print();
Date d2;
d2.Init(2022, 5, 16);
d2.Print();
return 0;
}
对于Date类,可以通过SetDate公有的方法给对象设置内容,但是如果每次创建对象都调用该方法设置信 息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢? 出于这个目的,出现了构造函数
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员 都有一个合适的初始值,并且在对象的生命周期内只调用一次。
-
函数名与类名相同。
-
无返回值。
Date()
{}
-
对象实例化时编译器自动调用对应的构造函数。 (不需要在构造函数前加void,因为我们一旦加上之后,编译器一方面会认为我们定义的是普通的成员函数,但是编译器同时又认为与其类名相同的成员函数一定是构造函数,此时就出现了命名冲突现象,即我们定义的函数名,一定不可与编译器的6个默认成员函数名相同)
-
构造函数可以重载。
class Date
{
public:
Date()
{
_year = 0;
_month = 0;
_day = 0;
}
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
Date(int year = 0, int month = 0, int day = 0)
{
_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.Print();
Date d2(2022, 5, 15);
d2.Print();
return 0;
}
输出结果:(保留第一种和第二种或者只保留第三种的结果)
注意:我们一般在使用构造函数的时候使用全缺省,当然,我们也可以使用半缺省。
-
如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
-
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
注意:无参构造函数、全缺省构造函数、我们没写时编译器默认生成的构造函数,都可以认为是默认成员函数。
注意:并不是我们不写构造函数时系统自动调用的才是默认构造函数,而是我们不需要传参数就能调用的才是默认构造函数。
比如下面的代码中程序就会报错:
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
return 0;
}
此时程序就会出现问题,如下图所提示:
因为我们自己定义了构造函数,所以编译器将隐藏或者说不再调用编译器自己的默认构造函数,此时我们再像上面那样去调用默认构造函数程序将无法正确运行了,除非这样进行修改:Date d1(2022, 5, 15);
类名()
{
}
类名()
{
_year = 0;
_month = 0;
_day = 0;
}
类名(year = 0, month = 0, day = 0)
{
_year = year;
_month = month;
_day = day;
}
-
关于编译器生成的默认成员函数,很多人会有疑惑:在我们不实现构造函数的情况下,编译器会生成 默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默认构造函数,但是d对象_year/_month/_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用??
解答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语法已经定义好的类型:如 int/char…,自定义类型就是我们使用class/struct/union自己定义的类型,看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员函数
class Time
{
public:
Time()
{
_hour = 0;
_minute = 0;
_second = 0;
}
int _hour;
int _minute;
int _second;
};
class Date
{
public:
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
cout << _time._hour << '-' << _time._minute << "-" << _time._second << endl;
}
private:
int _year;
int _month;
int _day;
Time _time;
};
int main()
{
Date d1;
d1.Print();
return 0;
}
经过打印发现输出结果为_year、_month、_day为随机数,而_time._hour、_time._minute、_time._second为0。
打印截图:
总结:一般情况下一个C++类,都要自己写构造函数。一般只有少数情况可以让编译器默认生成。
1、类里面都是自定义类型成员,并且这些成员都提供了默认构造函数。
2、如果还有内置类型成员,声明时给了缺省值,也可以使用默认构造函数,当然这种情况相对来说比较少。
3、使用C++库里的STL数据结构时可以使用编译器自动生成的默认构造函数。
注意:构造函数必须定义在public权限中,否则我们无法在主函数中定义相关的变量,因为如果我们将其放在private权限中,我们在类外是无法访问的,自然也就无法进行定义该种类型的变量。
注意:看下面的例子:
class Time
{
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date()
{
_year = 0;
_month = 0;
_day = 0;
_time._hour = 0;
_time.minute = 0;
_time.second = 0;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
cout << _time._hour << '-' << _time._minute << "-" << _time._second << endl;
}
private:
int _year;
int _month;
int _day;
Time _time;
};
在上面的例子中,我们试图在Date的构造函数中将_time._hour、_time._minute、_time._second进行初始化,但这是无法实现的,因为在Time类中,我们将这三个变量定义为私有权限,在类外无法进行访问,所以自然也就不能在Date的构造函数中进行初始化。
结论:各个类中应当有自己的构造函数来初始化自己的成员变量,而不应该越俎代庖,即使另一个类中定义的成员变量的权限是public,我们也不应该如此去做,因为我们无法保证我们通常使用的类类型的成员变量是public,而且一般来说,成员变量的权限也都是private,这是一个好习惯。
-
在上面的第6点中,默认构造函数有3种,需要注意的是,无论是三种中的哪一种默认构造函数,都能够对类中定义的自定义类型种的构造函数进行调用,例如下面的例子:
class Time
{
public:
Time()
{
_hour = 0;
_minute = 0;
_second = 0;
}
void Print()
{
cout << _hour << "-" << _minute << "-" << _second << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date()
{
_year = 0;
_month = 0;
_day = 0;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
_time.Print();
}
private:
int _year;
int _month;
int _day;
Time _time;
};
int main()
{
Date d1;
d1.Print();
return 0;
}
打印结果:
-
注意看下面这段代码:
class Time
{
public:
Time(int hour)
{
_hour = 0;
_minute = 0;
_second = 0;
}
void Print()
{
cout << _hour << "-" << _minute << "-" << _second << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
_time.Print();
}
private:
int _year;
int _month;
int _day;
Time _time;
};
int main()
{
Date d1;
return 0;
}
此时程序会报错,因为Date类型中的Time类型的变量没有默认构造函数(即我们自己手动定义的构造函数把编译器默认生成的构造函数给掩盖了),所以程序无法通过。那么这种情况下有没有解决方案呢?答案是有的,解决方案就是我们需要初始化列表,后面我们会学到。
-
成员变量的命名风格:我们一般习惯在成员变量的前面加一个_
,或者在后面加也可以,或者在成员变量的前面加一个m(member的意思)。这是为了防止下面情况的出现:
class Date
{
public:
Date(int year, int month, int day)
{
year = year;
month = month;
day = day;
}
private:
int year;
int month;
int day;
Time _time;
};
此时编译器将无法区分year是形参还是Date类中的成员变量了。
当然,我们可以像下面这样定义构造函数来避免上面存在的问题:
class Date
{
public:
Date(int year, int month, int day)
{
this -> year = year;
this -> month = month;
this -> day = day;
}
private:
int year;
int month;
int day;
Time _time;
};
虽然上面的这种方法能够很好的解决问题,但是我们还是推荐在成员变量的前面或后面加上_
的方式或者在前面加上一个m的方式来避免上面存在的这种问题。
我们知道C++的默认构造函数会对内置类型初始化为随机值,这常常会使我们无法或者很难真正使用到默认构造函数,所以C++11中出现了相关的补丁:在成员变量(内置类型)的定义时进行初始化。
==注意:这个地方我们不加初始化(因为那个地方是声明,而不是定义,所以不能称为初始化,只有在定义的时候赋值才成叫初始化),而是叫做缺省值,默认构造函数会进行使用。即等同于下面的代码:
-
析构函数名是在类名前加上字符 ~
。
-
无参数无返回值。
-
一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
-
对象生命周期结束时,C++编译系统系统自动调用析构函数。
class Date
{
public:
Date(int year = 0,int month = 0,int day = 0)
{
_year = year;
_month = month;
_day = day;
arr = (int*)malloc(sizeof(int) * 10);
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
~Date()
{
cout << "~Date()函数的调用" << endl;
free(arr);
arr = NULL;
}
private:
int _year;
int _month;
int _day;
int* arr;
};
int main()
{
Date d1;
return 0;
}
-
构造与析构的顺序:符合栈的顺序,后定义的先析构,先定义的后析构(析构顺序和构造顺序是相反的)。
例如:
int main()
{
Date d1;
Date d2;
return 0;
}
像上面这样进行定义,d2先析构,d1后析构。
嵌套的类的析构顺序也是如此,我们来看下面这个代码:
class Time
{
public:
~Time()
{
cout << "~Time()函数的调用" << _hour << endl;
}
int _hour;
};
class Date
{
public:
~Date()
{
cout << "~Date()函数的调用" << endl;
}
Time _time1;
Time _time2;
};
int main()
{
Date d1;
d1._time1._hour = 1;
d1._time2._hour = 2;
return 0;
}
运行截图:
接下来看下一段代码:
class Time
{
public:
~Time()
{
cout << "~Time()函数的调用" << _hour << endl;
}
int _hour;
};
class Date
{
public:
~Date()
{
cout << "~Date()函数的调用" << endl;
}
Time _time2;
Time _time1;
};
int main()
{
Date d1;
d1._time1._hour = 1;
d1._time2._hour = 2;
return 0;
}
运行截图:
-
编译器生成的默认析构函数,自定义类型将不做处理,但是对会自定类型成员调用它的析构函数。
class Time
{
public:
Time()
{
_hour = 0;
_minute = 0;
_second = 0;
}
~Time()
{
cout << "~Time()函数的调用" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year = 0, int month = 0, int day = 0)
{
_year = year;
_month = month;
_day = day;
arr = (int*)malloc(sizeof(int) * 10);
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
~Date()
{
cout << "~Date()函数的调用" << endl;
free(arr);
arr = NULL;
}
private:
int _year;
int _month;
int _day;
int* arr;
Time _time;
};
int main()
{
Date d1;
return 0;
}
输出结果:
此时我们也能够发现一个规则:在上面的例子中,Date类中定义了Time类类型的成员变量,所以要先调用Date析构函数,然后再调用Time析构函数,这是为什么呢,这是因为编译器生成的默认析构函数,自定义类型将不做处理,但是对会自定类型成员调用它的析构函数。这样,调用的顺序就出来了!