什么是构造函数
对于一个普通的日期类:
class Date {
public:
void Init(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
void Display() {
cout <<_year<< "-" <<_month << "-"<< _day <<endl;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1,d2;
d1.Init(2022, 8, 14);
d1.Display();
Date d2;
d2.Init(2022, 8, 15);
d2.Display();
return 0;
}
我们专门写了一个 Init 初始化函数来完成对对象的初始化,但这样是否有些多余了呢?
C++提供了一个特殊的成员函数——构造函数,构造函数的名字与类名相同,没有返回类型,创建自定义类的对象时由编译器自动调用,且在对象的创建到销毁只调用一次,构造函数我们如果不写编译器会自动生成一个。
注意:构造函数也是可以重载的!
下面给 Date 类写一个构造函数:
class Date {
public:
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
void Display() {
cout <<_year<< "-" <<_month << "-"<< _day <<endl;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1(2022, 8, 15);
// Date d1(&d1, 2022, 8, 15)
return 0;
}
默认构造函数
像上面刚刚写的 Date 类的构造函数,有一点很麻烦,就是创建对象时必须要传参数:
像 Date d1(); 这种也算传参数,只是传的参数为空(void),而不传参数的意思指Date d1; 这种。
错误说明中提到了“默认构造函数”,那什么是默认构造函数呢?
通俗一点来理解,就是已经默认给好参数了,不需要你传参就能完成对象的初始化。
像这种不传参也能初始化对象的默认构造函数有三种,分别是无参构造函数、全缺省构造函数、我们不写编译器自动生成的构造函数。
下面分别演示一下:
class Date {
public:
Date() {
_year = 2022;
_month = 8;
_day = 15;
}
void Display() {
cout <<_year<< "-" <<_month << "-"<< _day <<endl;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1;
d1.Display();
return 0;
}
这种就属于无参构造函数,运行结果如下:
class Date {
public:
Date(int year = 2022, int month = 8, int day = 15) {
_year = year;
_month = month;
_day = day;
}
void Display() {
cout <<_year<< "-" <<_month << "-"<< _day <<endl;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1;
d1.Display();
return 0;
}
这种就属于全缺省构造函数,运行结果如下:
class Date {
public:
void Display() {
cout <<_year<< "-" <<_month << "-"<< _day <<endl;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1;
d1.Display();
return 0;
}
这种我们没写构造函数,但编译器自动会生成一个,运行结果如下:
这时就有问题了,打印出来的都是随机值。
这是因为像 int、char、double…这些属于内置类型,像我们用 struct、class 等定义出来的类就属于自定义类型,编译器自动生成的默认构造函数对内置类型不作处理,而对自定义类型则会自动去调用它的构造函数。
可以看下面的例子:
class Time {
public:
Time() {
cout << "Time()" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date {
private:
// 基本类型(内置类型)
int _year;
int _month;
int _day;
// 自定义类型
Time _t;
};
int main() {
Date d;
return 0;
}
运行结果如下:
总结一下,只有上述三种函数才是默认构造函数,而这三种函数显然是构成重载的,所以在写代码的时候需要注意,一般默认构造函数都推荐用全缺省的。
初始化列表
在讲初始化列表之前先想一下,如果成员变量中有 const 类型的变量,或者是引用,或者是没有默认构造函数的自定义类型,比如下面这样:
class A {
public:
A(int a) {
_a = a;
}
private:
int _a;
};
class B {
public:
private:
A _aa; // 没有默认构造函数
int& _ref; // 引用
const int _n; // const
};
此时写一个 B 的构造函数,比如下面这个:
B(int aa, int& ref, int n, int& tmp) {
_aa(aa);
_ref = ref;
_n = n;
_ref = tmp;
}
错误百出。
因为构造函数是在创建对象时由编译器自动调用的,哪有这里的_aa(aa) 这种写法?
再者,_ref 一开始是 ref 的别名,后来是把它变成 tmp 的别名了吗?其实是不经意间修改了 ref 的值!
还有,const 常变量是不能被修改的,只能在定义时就给好,而这里是赋值,不是定义时的初始化。
初始化只能初始化一次,而函数体内却可以多次赋值。
而解决这一问题的方法就是使用初始化列表。
还是以上面的 B 类为例,用初始化列表的方式写就是这样:
B(int a, int& ref, int n)
:_aobj(a)
,_ref(ref)
,_n(n)
{}
这里就不运行了,参数传的没问题就可以。
解释一下,初始化列表是以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个“成员变量”后面跟一个放在括号里的初始值或表达式,如果是自定义类型则根据自定义类型的构造函数或拷贝构造函数传入参数。
像我们不写编译器默认给的构造函数在处理自定义类型时会调用自定义类型的默认构造函数,实际上就是在初始化列表调用的,这也是为什么自定义类型成员变量没有默认构造函数时编不过的原因。
但是初始化列表中的初始化顺序也是有细节的,成员变量在初始化列表中的初始化次序是其在类中的声明次序,与其在初始化列表中的先后次序无关。
例如:
class A {
public:
A(int a) {
cout << "A(int a)" << endl;
_a = a;
}
private:
int _a;
};
class B {
public:
B(int b) {
cout << "B(int b)" << endl;
_b = b;
}
private:
int _b;
};
class C {
public:
C(int a, int b)
:_A(a)
, _B(b)
{}
private:
B _B;
A _A;
};
int main() {
C cc(1, 2);
return 0;
}
运行结果如下:
补充一点,初始化列表是成员变量初始化的地方,无论我们写不写,在定义对象时都会走一遍,包括前面提到的编译器自动生成的构造函数对内置类型不处理而对自定义类型去调用它的默认构造函数,原因就是初始化列表的作用。
通过构造函数发生隐式转换的条件
有下面一段代码:
class A {
public:
A(int a = 1)
:_a(a)
{}
A& operator+= (A AA) {
_a += AA._a;
return *this;
}
void Print() {
cout << _a << endl;
}
private:
int _a;
};
int main() {
A _A1(1);
A _A2(2);
_A1 += _A2;
_A1.Print();
_A1 += 1;
_A1.Print();
return 0;
}
其中 operator+= 是运算符重载函数,this 指针默认是左操作数,传过来的实参为右操作数,可以实现自定义类型之间的运算。
运行结果如下:
对于第一次 += 没什么疑问,确确实实是两个类的运算。
但第二次一个类 += 一个整数,却没有报错,结果还正确,这就说明发生了隐式类型转换,将 int 隐式转换成了 A 类,在隐式转换的过程中调用了一次构造函数,将 1 作为构造函数的参数。
为了验证这点,我们在构造函数的函数体中添加一句cout << "A(int a = 1)" << endl; 作为标记,再次运行程序,结果如下:
这一点是我在一篇博客中看到的,那篇博客专门讲了单参数构造函数的隐式类型转换,但这个单参数的意思是构造函数只有一个参数吗?
再看下面一段代码:
class A {
public:
A(int a1, int a2 = 1)
:_a1(a1)
,_a2(a2)
{
cout << "A(int a1, int a2 = 1)" << endl;
}
A& operator+= (A AA) {
_a1 += AA._a1;
_a2 += AA._a2;
return *this;
}
void Print() {
cout << "_a1 = " << _a1 << " ";
cout << "_a2 = " << _a2 << endl;
}
private:
int _a1;
int _a2;
};
int main() {
A _A1(1, 0);
_A1.Print();
_A1 += 1;
_A1.Print();
return 0;
}
运行结果如下:
这是构造函数有两个参数,但是只需要传一个参数就可以正常调用,这时发生了隐式转换,与上面情况相同。
可见,想要通过构造函数发生隐式转换的条件是只需要给构造函数传一个参数。
如果想要避免这种隐式转换的发生也很简单,有两种方法:
-
以第一段给出的代码为例,那里 operator+= 的参数类型是 A,如果把参数类型改为 ***A&***,就不会发生隐式转换,因为传过来的参数是一个 A 类的引用类型,没有实体操作数可以转换成引用类型: -
第二种方法就比较粗爆了,就是用 explicit 关键字修饰构造函数:
C++11的成员初始化新玩法
C++11支持非静态成员变量在声明时进行初始化赋值,但是要注意这里不是初始化,这里是给声明的成员变 量缺省值。
由于编译器默认给的构造函数不对内置类型进行处理,C++11就有一个新玩法可以弥补这一缺陷,玩法如下:
class A {
public:
void Print() {
cout << _a << endl;
}
private:
int _a = 0;
};
int main() {
A _A;
_A.Print();
return 0;
}
运行结果如下:
再次重申,这里不是初始化,而是给声明的成员变量缺省值。
尽管C++11支持这样的用法,但实际中还是更推荐我们自己写构造函数,用初始化列表去完成初始化,这样还是最安全的。
|