【C++】构造函数初始化详解
0. 前篇
【C++】揭秘构造函数六大特性-CSDN博客
1. 再谈构造函数
1.1 构造函数体赋值 (Assignment) vs 初始化 (Initialization)
-
核心区别:在构造函数体内使用
=
为成员变量赋值,这叫做赋值,而不是初始化。成员变量在进入函数体前其实已经被默认初始化了。 -
关键点:初始化只能发生一次,而赋值可以进行多次。对于
const
成员、引用成员等必须初始化且不能再次赋值的类型,就无法在函数体内进行“初始化”操作。
class Date {
public:// 这是赋值,不是初始化Date(int year, int month, int day) {_year = year; // 赋值_month = month; // 赋值_day = day; // 赋值}
private:int _year;int _month;int _day;
};
有些变量,在对象创建时就需要被初始化,因此有了初始化列表,专门用来对某些成员变量进行初始化。
1.2 初始化列表
为了解决上述问题,C++引入了初始化列表。它是真正的初始化。
-
语法:在构造函数参数列表后以冒号
:
开始,用逗号,
分隔,每个成员变量后跟用括号()
包裹的初始值。
class Date
{
public:Date(int year, int month, int day): _year(year), _month(month), _day(day){}
private:int _year;int _month;int _day;
};
【注意】
1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
2. 类中包含以下成员,必须放在初始化列表位置进行初始化:
-
引用成员变量
-
const成员变量
-
没有默认构造函数的自定义类型成员 (编译器无法自己调用无参构造)
class A {
public:A(int a): _a(a) {} // A没有默认构造函数private:int _a;
};class B {
public:B(int aVal, int& refVal): _aObj(aVal) // 必须用初始化列表初始化A, _ref(refVal) // 必须用初始化列表初始化引用, _n(10) // 必须用初始化列表初始化const成员{}
private:A _aObj;int& _ref;const int _n;
};
3. 强烈建议:所有成员变量都应优先使用初始化列表进行初始化。对于内置类型(如int
),初始化列表和函数体内赋值性能差别不大。但对于自定义类型,使用初始化列表通常效率更高,因为它直接调用拷贝构造函数初始化,省去了默认初始化再赋值的过程。
class Time
{
public:Time(int hour = 0):_hour(hour){cout << "Time()" << endl;}
private:int _hour;
};
class Date
{
public:Date(int day){}
private:int _day;Time _t;
};
int main()
{Date d(1);
}
1.3 成员变量的初始化顺序
-
规则:成员变量的初始化顺序只取决于它们在类中声明的顺序,而与它们在初始化列表中的书写顺序无关。
-
陷阱:如果初始化一个成员时用了另一个尚未初始化的成员,会导致未定义行为(通常是随机值)。
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();
}
上述代码的输出结果是: 1 随机值
1.4 explicit 关键字
-
作用:禁止编译器进行通过构造函数进行的隐式类型转换。
-
使用场景:修饰单参构造函数或多参构造函数(但除第一个参数外都有默认值)。
class Date
{
public:// 1. 单参构造函数,没有使用explicit修饰,具有隐式类型转换作用Date(int year):_year(year){}private:int _year;int _month;int _day;
};void Test()
{Date d1(2022);// 用一个整形变量给日期类型对象赋值// 实际编译器背后会用2023构造一个无名对象,最后用无名对象给d1对象进行赋值// 这是一种隐式类型转换,构造出tmp(2022)再用tmp拷贝构造出d1(tmp)
}
如果使用 explicit 修饰构造函数,将会禁止构造函数的隐式转换:
class Date
{
public:// 1. 单参构造函数,没有使用explicit修饰,具有类型转换作用// explicit修饰构造函数,禁止类型转换---explicit去掉之后,代码可以通过编译explicit Date(int year):_year(year){}// 2. 虽然有多个参数,但是创建对象时后两个参数可以不传递,没有使用explicit修饰,具// 有类型转换作用// explicit修饰构造函数,禁止类型转换explicit Date(int year, int month = 1, int day = 1): _year(year), _month(month), _day(day){}Date& operator=(const Date& d){if (this != &d){_year = d._year;_month = d._month;_day = d._day;}return *this;}
private:int _year;int _month;int _day;
};
void Test()
{Date d1(2022);// 用一个整形变量给日期类型对象赋值// 实际编译器背后会用2023构造一个无名对象,最后用无名对象给d1对象进行赋值d1 = 2023;// 将1屏蔽掉,2放开时则编译失败,因为explicit修饰构造函数,禁止了单参构造函数类型转// 换的作用
}
补充:explicit
关键字能提高代码的清晰度和安全性,避免意外的、难以察觉的类型转换。
2. static成员
2.1 概念与定义
-
概念:被
static
修饰的类成员称为静态成员。它属于整个类,而不是某个具体的对象,所有对象共享同一份静态成员。 -
定义规则:静态成员变量必须在类外进行定义和初始化(分配内存),定义时不再加
static
关键字。
比如:实现一个类,计算程序中创建出了多少个类对象
class A
{
public:A() { ++_scount; }A(const A& t) { ++_scount; }~A() { --_scount; }static int GetACount() { return _scount; }
private:static int _scount; // 成员变量的声明
};
int A::_scount = 0; // 成员变量的定义
void TestA()
{cout << A::GetACount() << endl;A a1, a2;A a3(a1);cout << A::GetACount() << endl;
}
2.2 特性与使用
1. 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
3. 访问方式:
-
通过类名作用域访问(首选):
A::GetACount()
-
通过对象访问:
a1.GetACount()
(ps:静态成员包括静态成员变量和静态成员函数。)
4. 静态成员函数:
-
没有
this
指针,因此不能直接访问类的非静态成员(变量或函数)。 -
可以访问静态成员变量和其他静态成员函数。
5. 静态成员也是类的成员,访问权限受public
, protected
, private
访问限定符的限制。
所以:静态成员函数不能调用非静态成员变量和函数(因为没有this指针),而非静态成员函数可以调用静态成员变量
3. C++11的成员初始化新玩法
C++11 允许在类内声明非静态成员变量时直接为其指定一个缺省值。
-
本质:这个缺省值的作用是提供给构造函数初始化列表的。如果构造函数的初始化列表没有显式初始化该成员,编译器就会使用这个类内缺省值来初始化它。
-
注意:静态成员变量不享受此特性,它仍然必须在类外定义和初始化。
class Date
{
public:void Print() { /* ... */ }// 如果这样写构造函数:Date() {},则_year,_month,_day会使用下面的缺省值// 如果这样写:Date(int day) : _day(day) {},则_year和_month使用缺省值,_day使用参数值private:// C++11 才允许这样操作// 声明时给缺省值int _year = 0;int _month = 1;int _day = 1;// 静态成员变量不可以给缺省值,必须要在类外面定义static int _n;// static int _n = 10; // 错误!静态成员不能在类内初始化
};
C++11 类内初始化的好处:
-
提高了代码的可读性,成员变量的默认值一目了然。
-
减少了多个构造函数中重复的初始化代码。如果一个成员在大多数情况下初始值都一样,可以在类内指定缺省值,只在特殊的构造函数中覆盖它即可。