构造函数
构造函数的任务是初始化对象的数据成员,构造函数不应该轻易覆盖掉类内的初始值,除非新赋的值与原值不同,如果你不能使用类内初始值,则所有构造函数都应该显式地初始化每个内置类型的成员。
默认构造函数
无需任何实参的构造函数为默认构造函数,类没有声明其它构造函数时编译器会生成一个合成的默认构造函数,如果存在类内初始值,则用它来初始化成员,否则默认初始化该成员。
有时候我们希望既有合成的默认构造函数也有自定义构造函数,可以使用=default要求编译器生成默认构造函数。=default既可以出现在声明,也可以出现在定义中,如果在类的内部,则默认构造函数是内联的。
class Person
{
private:
int height_;
public:
Person() = default;
};
class Person
{
private:
int height_;
public:
Person();
};
Person::Person() = default;
构造函数初始值列表
构造函数初始值列表使用:成员名(参数名)..{}的方式为成员赋初始值,当某个成员不在初始值列表中时,它将以默认构造函数相同的方式隐式初始化。
class Person
{
public:
int height_;
int width_;
Person(int height, int width) :height_(height),width_(width) {};
};
我们在定义变量的时候习惯于立即对其初始化,而非先定义(初始化),再赋值,对于类的数据成员,也有初始化和赋值的区别。
std::string str= "hello";//定义并初始化
std::string str2;//默认初始化为空字符串
str2 = "111";//赋值
如果没有在构造函数的初始值列表中显示地初始化成员,则该成员函数将在构造函数体之前执行默认初始化。下面两段构造函数中,前者初始化了它的数据成员,后者先默认初始化它的数据成员再赋值。这一区别到底会有什么影响取决于数据成员的类型。
class Person
{
public:
std::string name_;
Person(const std::string& name) :name_(name) {};
};
class Person
{
public:
std::string name_;
Person(const std::string& name);
};
Person::Person(const std::string& name)
{
name_ = name;
}
构造函数的初始值有时候必不可少
如果数据成员时const或引用,必须将其初始化,类似的,当数据成员属于某种类类型且该类型没有定义默认构造函数时,也必须将这个成员初始化。
如果数据成员是const,引用或者某种未提供默认构造函数的类类型,我们必须通过构造函数初始值列表为这些成员提供初始值。
数据成员初始化的顺序
数据成员的初始化顺序与它们在类定义中的出现顺序一致,而与初始值列表中的顺序没有关系。最好令构造函数初始值的顺序与成员声明的顺序保持一致,如果可能的话,尽量避免用某些成员初始化其它成员。
委托构造函数
C++11引入了委托构造函数,一个委托构造函数可使用它所属类的其它构造函数执行它自己的初始化过程。
当一个委托构造函数委托其它构造函数时,被委托的构造函数的初始值列表和函数体被依次执行,最后控制权回到委托构造函数的函数体。
class Person
{
public:
int height_;
int width_;
const char* name_;
Person(int height, int width,const char* name) :height_(height), width_(width), name_(name)
{
std::cout << "init1" << std::endl;
};
Person() :Person(10,10,"123")
{
std::cout << "init2" << std::endl;
};
Person(int height) :Person()
{
std::cout << "init3" << std::endl;
};
};
int main(void)
{
Person localPerson(80);
std::cout << localPerson.height_ << std::endl;
std::cout << localPerson.width_ << std::endl;
std::cout << localPerson.name_ << std::endl;
system("pause");
return 0;
}
init1
init2
init3
10
10
123
转换构造函数
如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐含转换机制,我们把这种构造函数称作转换构造函数。
这种类型转换只允许一步转换,如果把下面代码中Person构造函数的形参类型改为const std::string& 就不能转换了,因为此时转换需要两步,const char*=>std::string,std::string=>Person。
class Person
{
public:
const char* name_;
Person(const char* name):name_(name) {};
};
void testFunc(const Person& person)
{
std::cout << person.name_<< std::endl;
}
int main(void)
{
testFunc("xiao hong");
system("pause");
return 0;
}
int main(void)
{
Person person = "xiao hong";
testFunc(person);
system("pause");
return 0;
}
class Person
{
public:
const char* name_;
Person(const std::string& name):name_(name.c_str()) {};
};
void testFunc(const Person& person)
{
std::cout << person.name_<< std::endl;
}
int main(void)
{
testFunc(std::string("xiao hong"));
system("pause");
return 0;
}
explicit构造函数
我们可以在只有一个实参的构造函数前面添加explicit关键字阻止隐式转换,由于多个实参的构造函数不能用于执行隐式转换,所以无需将这些构造函数指定为explicit。
explicit关键字只能在类内声明构造函数时使用,不能在类外部定义中使用。
继承的构造函数
派生类能够重用其直接基类定义的构造函数,但不包括默认,拷贝,移动构造函数,如果派生类没有这三种构造函数,编译器将为派生类合成它们。
派生类继承基类构造函数的方式是提供直接基类名的using声明语句,对于基类的每个构造函数,编译器都在派生类中生成一个形参列表完全相同的构造函数。如果派生类有直接的数据成员,则这些成员会被默认初始化。
class Person
{
public:
const char* name_;
Person(const char* name):name_(name) {};
};
class Worker :public Person
{
public:
using Person::Person;
};
除此之外,我们还可以显式调用基类的构造函数,但是似乎没有办法在函数体内调用基类的构造函数。
class Person
{
public:
const char* name_;
Person(const char* name):name_(name) {};
};
class Worker :public Person
{
public:
Worker(const char* name);
};
Worker::Worker(const char* name):Person(name)
{
}