在 C++ 的面向对象编程中,构造函数是每个类都绕不开的核心话题。它负责在对象创建时完成初始化,是对象生命周期中第一个被调用的成员函数。本文将继续深入探讨构造函数的进阶用法------初始化列表 ,并进一步讲解类型转换 与 static 成员这两个与对象创建、访问密切相关的知识点。通过本文的学习,你将理解为什么某些成员变量必须放在初始化列表中初始化,以及如何利用构造函数实现类型之间的转换、如何借助 static 成员实现跨对象的共享数据。
构造函数(续)
之前我们所讲的构造函数,是用于在实例化对象时对这个对象进行初始化。我们可以显式地定义构造函数,不定义时编译器也会自动生成。这里我们再深入讲解构造函数中最重要的部分:初始化列表。
关于我们之前 Date 类构造函数的写法是这样的:
cpp
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
如果我们写成初始化列表,那么这个构造函数应该是这样的:
cpp
Date(int year = 1, int month = 1, int day = 1)
//初始化列表的格式
:_year(year)//将year的值赋给_year
,_month(month)//同理
,_day(day)
{
;
}
初始化列表的写法是:一个冒号后跟成员变量,括号内部是表达式或变量,用于初始化前面的成员变量,后面的成员变量初始化都跟在逗号后面。而且在初始化列表中每个成员只能出现一次。
函数体内部可以什么都不写,我们一般建议将初始化都写到初始化列表当中,因为所有的成员变量都要进入初始化列表。
成员变量进入初始化列表的顺序与成员变量声明的顺序完全一致,这里先进入初始化列表的是_year,然后是_month,最后是_day。
C++11 规定我们可以给成员变量设定一个缺省值,比如这里,我们给 _year 设定默认值为 2026,然后不在初始化列表里面初始化 _year。
cpp
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
:_month(month)
,_day(day)
{
;
}
private:
int _year = 2026;//设定缺省值,而非赋值
int _month;
int _day;
};
对于这样一个构造函数,是如何进入初始化列表的呢?
这里 _year 先进入初始化列表,但是初始化列表没有显式写 _year 的初始化,所以编译器会去找 _year 的缺省值,将它的缺省值赋给 _year。

后面的成员变量在初始化列表中都显式地写了,会用我们设定的值正常初始化。

像这样的成员变量,我们可以把它的初始化写在初始化列表,也可以写在函数体内,但有三种变量我们必须写在初始化列表中。
引用、const 修饰的变量、没有默认构造函数的类我们都必须在初始化列表中进行初始化。
这三类变量存在一个共同点,那就是必须在创建时就进行初始化,且只能初始化这一次。
cpp
class Time
{
public:
Time(int hours, int minutes)
:_hours(hours)
, _minutes(minutes)
{
;
}
private:
int _hours;
int _minutes;
};
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
:_month(month)
,_day(day)
//必须写在初始化列表中
,_i(day)
,_a(1)
,_t(4,5)
{
;
}
private:
int _year = 2026;//设定缺省值,而非赋值
int _month;
int _day;
int& _i;
const int _a;
Time _t;
};
这三类对象必须进入初始化列表的原因都很好理解。关于成员变量进入初始化列表初始化的逻辑,我们可以用以下这张图来概括:

这也就是初始化列表的所有内容了,那么在构造函数的函数体内我们可以干什么呢?我们可以完成初始化列表做不到的事。
比如我们用 malloc 函数申请一块空间的时候,都要判断一下是否申请成功,这个判断我们就可以放到函数体内完成,因为初始化列表做不到。
cpp
class Stack
{
public:
Stack(int n = 4)
:_a((int*)malloc(n * sizeof(int)))
, _top(0)
, _capacity(n)
{
//判断是否申请成功
if (_a == NULL)
{
perror("malloc");
exit(-1);
}
}
private:
int* _a;
int _top;
int _capacity;
};
类型转换
我们来看一个类:
cpp
class A
{
public:
A(int a = 1)
:_a1(a)
{
;
}
void Print()
{
cout<<_a1<<endl;
}
private:
int _a1;
};
然后我们用 2 去构造一个类对象,能成功吗?
cpp
int main()
{
A a1 = 2;
a1.Print();
return 0;
}

结果发现成功了,为什么呢?这是因为发生了类型转换。
在 C 语言中,我们知道它支持内置类型转换,比如整型和浮点型、指针和整型等等,但不支持自定义类型的转换。
在 C++ 中是支持的,我们可以把内置类型转换为自定义类型,同时也支持自定义类型和自定义类型之间进行转换,但需要通过构造函数来实现。
内置类型转换为自定义类型
这里 A a1 = 2 就发生了类型转换,2先传过去构造了一个临时对象,该临时对象再拷贝构造a1,对于这种连续的构造+拷贝构造,会被编译器优化为直接构造。
我们知道临时对象具有常性,在之前的 const 引用中我们也知道,发生了类型转换的表达式必须使用 const 引用,所以我们想给它取别名应该这样写:
cpp
const A& a2 = 2;
不使用 const 修饰就会出现权限放大的问题,编译也不通过。
这是传一个参数的构造函数,那么对于要传两个参数以及更多参数的构造函数呢?我们应该如何用内置类型去直接构造?
cpp
class A
{
public:
A(int a1 = 1,int a2 = 1)
:_a1(a1)
,_a2(a2)
{
;
}
void Print()
{
cout << _a1 << ' '<< _a2<< endl;
}
private:
int _a1;
int _a2;
};
如果我们直接用括号括起来,写成A a = (1,2),就相当于逗号表达式了,实际传过去的参数就变成了一个,所以要用大括号括起来:
cpp
A a1 = {1,2};
a1.Print();

也可以写成这样:
cpp
A a1({1,2});
自定义类型之间的转换
对于自定义类型之间的转换,则需要通过相应的构造函数来实现。
cpp
class A
{
friend class B;
public:
A(int a1 = 1,int a2 = 1)
:_a1(a1)
,_a2(a2)
{
;
}
void Print()
{
cout << _a1 << ' '<< _a2<< endl;
}
private:
int _a1;
int _a2;
};
class B
{
public:
B(A& a)
:_b1(a._a1)
,_b2(a._a2)
{
;
}
private:
int _b1;
int _b2;
};
int main()
{
A a1({1,2});
B b1 = a1;//类A去构造B
return 0;
}
static成员
我们知道,每个类对象中都有自己的成员变量,它们都是相互独立的,那么存在 static 成员会是什么情况?
这里我们想统计类 C 对象的创建次数,但又不想外部可以直接修改,那么定义成全局变量显然不合理,考虑到类的封装性,我们把它定义成 static 成员变量。
cpp
//统计类C的创建次数
class C
{
public:
C()
{
_ret++;
}
C(const C& c)
{
_ret++;
}
private:
//定义成静态成员变量
static int _ret;
};
因为 _ret 属于整个类,是所有对象共有的,所以它不会进入初始化列表,我们需要在类外面初始化它,且只能初始化一次,需要指定类域来查找。
cpp
int C::_ret = 0;//初始化成0
然后我们给外部提供一个接口,使外部可访问 _ret 的值,从而拿到类对象的创建次数。
cpp
static int Get_ret()
{
return _ret;
}
因为要访问的成员被 static 修饰了,所以我们也用 static 成员函数来访问该成员,它与普通成员函数的区别在于,内部不含有 this 指针,并且只能访问 static 成员(普通成员函数静态和非静态成员变量都可访问)。
cpp
int main()
{
C c1;
C c2(c1);
C c3;
cout << c3.Get_ret() << endl;
return 0;
}
