目录
类通常由类成员变量和类成员方法组成。类的成员变量又被称为类属性,类的成员方法描述当前类所支持的操作,而操作对象一般也是类的成员变量。由此可以看出类的成员变量,是类定义中最关键所在。本文将重点讨论类成员变量的初始化,包括初始化方法,初始化规则等。
成员变量初始化
每个类都会定义一个或几个特殊的成员函数,它们负责类成员变量的初始化任务,我们称之为类构造函数。不论何时,只要创建类,类的构造函数都会被调用。类构造函数名与类名相同,和其他普通函数不一样的是,构造函数没有返回值。除此之外构造函数和其他普通函数类似,构造函数包含一个(可为空)的参数列表和一个(可为空)的函数体。
我们设计一个Person类,Person类包含两个成员变量:m_name描述Person的名称,m_age描述Person的年龄,类的最终定义如下
class Person
{
public:
Person(){}
Person(const std::string name)
{
m_name = name;
m_age = 0;
}
Person(const std::string name, const int age) : m_name(name), m_age(age)
{
}
virtual ~Person();
void show()
{
std::cout << "name is" << m_name << ","<< "age is" << m_age << std::endl;
}
private:
std::string m_name;
int m_age;
};
Person定义包含了三个构造函数,他们分别是:默认构造函数Person(),通过函数体初始化成员变量的构造函数Person(const std::string name),基于初始化列表初始化成员变量的构造函数Person(const std::string name, const int age) : m_name(name), m_age(age)。
通过构造函数体初始化成员变量
有了上文Person类定义,我们继续声明两个Person对象:一个通过默认构造函数Person(),另一个通过函数体初始化成员变量的构造函数Person(const std::string name),并编写下述示例代码:
int main()
{
Person firstPerson;
firstPerson.show();
Person secondPerson("李四");
secondPerson.show();
}
此示例代码的运行结果如下图所示:
第二行对大家来说是很容易理解的,第一行的运行结果也许对你来说有些奇怪。因为这里涉及到编译器的默认构造行为。编译器的编译行为可以通过如下伪代码的描述。
Person createPerson()
{
person = malloc(sizeof(Person));
person. m_name = std::string(); // 调用std::string默认构造函数空字符串
person.m_age = malloc内存区域数据; // C++标准未定义,编译器厂商自定义行为
return person;
}
因为m_age是未定义行为,Visual Studio IDE使用内存随机数据赋值,所以导致了-858993460。
基于初始化列表初始化成员变量
定义第三个Person对象thirdPerson:
Person thirdPerson ("王五", 35);
thirdPerson.show();
这种通过冒号以及冒号和花括号直接的代码(: m_name(name), m_age(age))实现成员变量初始化,我们称通过初始化列表初始化成员变量。运行上述代码,我们会得到这样的输出:
内部初始化
内部初始化是C++11新引入的特性,内部初始化会在成员变量声明时,同时实现成员变量的初始化工作,我们改造Person的成员变量定义,在类成员变量声明时,同时完成成员变量的初始化赋值。改造后的Person类定义如下:
class Person
{
public:
Person(){}
Person(const std::string name)
{
m_name = name;
m_age = 0;
}
Person(const std::string name, const int age) : m_name(name), m_age(age)
{
}
virtual ~Person();
void show()
{
std::cout << "name is" << m_name << ","<< "age is" << m_age << std::endl;
}
private:
std::string m_name = std::string("张三"); // C++11 新支持初始化方式
int m_age = 25; // C++11 新支持初始化方式
};
我们继续分析firstPerson的默认初始化行为,在计算机上运行代码段:
int main()
{
Person firstPerson;
firstPerson.show();
}
我们会得到这样的输出:
成员变量初始化规则
通过前面分析,我们得知成员变量可通过三种方式进行初始化,然而除了初始化方式,成员变量初始化还有一些必须遵守的规则。
成员变量初始化顺序
为了更好的分析成员变量的初始化顺序问题,笔者继续改造Person,为Person类添加一个m_level属性,此属性m_level = m_age/10。改造后的Person类定义如下:
class Person
{
public:
Person(){}
void show()
{
std::cout << "name is " << m_name << ",age is " << m_age;
std::cout << ",level is " << m_level << std::endl;
}
private:
std::string m_name;
int m_level;
int m_age;
};
初始化列表初始化成员变量
使用基于列表初始化的构造函数初始化成员变量,我们定义初始化列表构造函数:
Person(const std::string name, const int age) :m_name(name), m_age(age), m_level(m_age/10){}
int main()
{
Person me("lucas", 35);
me.show();
}
我猜测大多数人都会不假思索的回答,这个还不简单,输出:name is lucas, age is 35, leve is 3,但事实证明我们的猜测是错误的,真正的运行结果为name is lucas,age is 35,level is -85899346,此既是成员变量的初始化顺序问题。
C++标准规定
类成员变量通过初始化列表初始化时,与构造函数中的初始化列表中的变量顺序无关,只与定义成员变量的顺序有关。因为成员变量的初始化次序是根据变量在内存的次序有关系,而变量的内存排列顺序,早在编译期就根据变量的定义次序决定了。
现在我们根据C++标准分析上述输出,由于m_level定义位置比m_age靠前,所以m_level首先会得到初始化,但是此时m_age还没有完成初始化,按照编译要求未初始化的基本类型将会得到一个随机值,所以就导致m_level也变成了一个随机值。
构造函数体初始化成员变量
通过构造函数的函数体实现成员变量的初始化,这是平常大家最常用的成员变量初始化方式,为了验证这种方式的初始化顺序,我们编写如下代码段:
Person(const std::string name)
{
m_name = name;
m_age = 35;
m_level = m_age/10;
}
int main()
{
Person me("lucas");
me.show();
}
这段代码运行后将会输出:
C++标准规则
类成员变量通过构造函数体初始化时,初始化顺序由构造函数体中的变量初始化顺序决定,与类成员变量的定义顺序无关系。
初始化列表与构造函数结合
为说明初始化列表和构造函数体结合进行成员变量初始化的实现原理,我们看下面这个构造函数:
Person(const std::string name, const int age) :m_name(name), m_age(age), m_level(m_age/10)
{
m_level = m_age / 10;
}
此构造函数,在初始化列表中进行了一次m_level初始化,在构造函数体中又进行了一次m_level的初始化工作。
Person me("lucas", 35);
me.show();
这时候,me对象的输出变成了:name is lucas,age is 35,level is 3。可以看出m_level正常得到初始化了。
C++标准规定
在类构造函数中,如果初始化列表和构造函数体同时对一个变量进行了初始化,列表初始化会优先得到执行,接着才会执行构造函数体中的变量初始化。
按照上述规定,虽然在初始化列表中m_level被初始化成了随机值-85899346,但是m_age正常得到了初始化,所以在构造函数体中,m_level会被初始化为3。
成员变量内部初始化
为了更精确的描述内部初始化的初始化时机,以及与构造函数的协作关系,我们改造Person类的定义,并编写下述测试示例代码:
class CPerson
{
public:
Person(){}
CPerson(const std::string name): m_name(name){}
CPerson(const std::string name, const int age) : m_name(name), m_age(age){}
void show()
{
std::cout << "name is" << m_name << ", age is" << m_age << std::endl;
}
private:
std::string m_name = std::string("张三");
int m_age = 25;
};
int main()
{
CPerson firstPerson;
firstPerson.show();
CPerson secondPerson("李四");
secondPerson.show();
CPerson thirdPerson("王五", 35);
thirdPerson.show();
}
示例代码运行结果如图所示
C++11标准规定
C++11类内部初始化,优先于任何构造函数初始化成员变量。内部初始化后,如果构造函数不显示重新初始化成员变量默认值,成员变量将保持内部初始化值默认值;如果构造函数显示重新初始化成员变量默认值,成员变量将保持构造函数重新赋值。
对照C++11标准规定,firstPerson由默认构造函数生成,由于默认构造函数中没有显示的修改类内部初始化默认值,所以输出结果为内部初始化的默认值,secondPerson由CPerson(const std::string name)生成,此构造函数仅显示给m_name进行了赋值操作,所以输出结果就应该是m_name显示构造显示赋值,m_age显示类内部初始化默认值。而thirdPerson则是在构造函数中显示的对m_name和m_age进行了显示的重新赋值操作。所以最终的输出结构是构造函数中的新默认值。
类静态成员初始化
我们都知道类的静态成员变量仅与当前类型相关,不论此类型声明了多少对象,静态成员变量仅有一个备份;这也静态成员变量与成员变量最显著的不同之处。
一般情况下,静态成员变量不应该在内部初始化,需要在内外部显示初始化,除非声明此静态成员变量为static const或constexpr类型,例如:
class User
{
public:
User();
virtual ~User();
private:
static constexpr int MAX_AGE = 100;
int m_age;
};
而且声明为static const/constexpr 类型的静态成员变量,在内部初始化时初始值必须是常量或常量表达式。
总结
综上所述,针对类非静态成员变量,我们可得到下述规则:
- 当类成员变量被内部初始化后,如果成员变量未通过初始化列表或构造函数体显示重新赋值,成员变量将保留内部初始化值;就是说内部初始化成员变量具有最高优先级;
- 成员变量的初始化顺序仅与其定义顺序相关,与初始化列表顺序无关;而且初始化列表先与构造函数体执行;
- 当类成员变量在构造函数体中赋值初始化时,初始化的顺序由构造函数体中变量的赋值顺序决定,与其定义顺序无关。
针对静态成员变量,我们可总结如下规则:
- 静态成员变量一般需要在类外部显示初始化,除非声明为static const/constexpr类型;
- 声明为static const/constexpr类型,可在类内部初始化,但初始值必须是常量或常量表达式