目录
C++构造函数
众所周知,C++是一门面向对象的编程语言。既然面向对象,就不可避免的要经常涉及到对于对象的初始化操作。作为一门高级语言,并不应该在此类操作中花费开发人员太多时间,因此在c++中提供了构造函数来负责此类操作。
什么是构造函数?构造函数是一种特殊的成员函数,不需要用户进行显示调用,而是在建立对象时自动被执行的一类函数。
class Test {
private:
int amount;
public:
Test(int temp) : amount(temp) {
//test类构造函数,创建对象时自动被调用。
std::cout << "Constructor Called" << std::endl;
}
};
int main(int argc,const char* argv[]) {
Test t(10); //实例化对象
return 0;
}
// Execute:
Constructor Called
以上为c++构造函数的一个简单实例。对于构造函数我们在此只简单回顾概念,不做过多深入。
委托构造
C++11之前构造函数所暴露出的问题
先来考虑以下代码片段(此处为了突出问题,采用在函数体内赋值而非参数初始列表!):
class Person {
private:
std::string name;
std::string address;
unsigned int age;
public:
Person(std::string name, std::string address, unsigned int age) {
this->name = name;
this->age = age;
this->address = address;
}
Person(std::string name, unsigned int age) {
this->name = name;
this->age = age;
this->address = "Unknown";
}
Person(std::string name) {
this->name = name;
this->address = "Unknown";
this->age = 0;
}
};
此处塑造了一个person类,并给出了构造函数的多个重载版本,是不是感觉哪里怪怪的?没错!代码重复的频率太高了。这样写既不美观,效率也低,因为不断在重复之前的行为。
在新标准出来以前,对于此类问题的解决,我们通常会想到将其重复的部分放入一个函数之中,并在构造函数之中进行调用:
class Person {
private:
std::string name;
std::string address;
unsigned int age;
void createPerson(std::string& name, unsigned int age = 0,std::string address = "Unknown") {
this->name = name;
this->address = address;
this->age = age;
}
public:
Person(std::string name, unsigned int age,std::string address) {
createPerson(name, age,address);
}
Person(std::string name, unsigned int age) {
createPerson(name, age);
}
Person(std::string name) {
createPerson(name);
}
};
而在新标准(C++11)之后,对于诸如此类问题的解决提供了一个全新的解决思路: 委托构造函数。
类中委托构造函数
顾名思义,委托构造函数允许一个构造函数调用同类中的另一个构造函数来执行部分或全部的构造工作。这种设计模式避免了代码重复,尤其在类中有多个构造函数时,可以减少冗余代码。
正如其名所言,对于构造中有重复交叉的部分,用户可以将重复的这一部分工作委托给一个特定的构造函数进行完成。
于用户而言,只需写一份代码就足够,而不用过多堆积冗余的代码。正如下面这一示例:
class MyClass {
private:
int a;
int b;
public:
MyClass() :MyClass(0, 0){
//将默认构造函数委托给参数化构造函数
}
MyClass(int x, int y):a(x),b(y) {
std::cout << "Parameterized constructor" << std::endl;
}
};
对于之前的示例,我们可以用委托构造修改如下:
class Person {
private:
std::string name;
std::string address;
unsigned int age;
public:
Person(std::string name, unsigned int age,std::string address)
:name(name),age(age),address(address) {}
Person(std::string name, unsigned int age)
:Person(name,age,"Unknown") {} //委托给构造函数1
Person(std::string name)
:Person(name,0,"Unknown"){} //委托给构造函数1
};
是不是简明了很多^_^ ?实际运用中,也建议大家多采用这种方式来应对此类状况。
下面来聊一聊关于使用委托构造需要注意些什么。
委托构造的注意事项
-
委托构造应将其放在参数初始化列表中,而不是放入函数体中!
原因是在函数体中调用构造,实际上是创建了一个临时对象,而在这其间,出现了不必要的构造和赋值操作,降低了效率。而使用参数初始化列表则不会出现这种问题。
使用参数初始化列表可以确保所有成员变量在对象创建时被正确初始化。如果在函数体中进行赋值,可能会导致某些成员变量在对象创建时未被正确初始化,进而引发未定义行为。
例如下例:
class MyWidget {
private:
const int const_int;
public:
MyWidget() {
MyWidget(10); //报错,必须初始化常量对象
}
MyWidget(int x):const_int(x) {
std::cout<<"Parameterized constructor"<<std::endl;
}
};
//改为使用参数初始化列表
MyWidget():MyWidget(10){} //成功通过编译
-
委托构造是链式调用,绝不允许环式调用!
什么是链式调用?什么是环式调用?在这里我们不拽复杂的概念,简单来说,就是构造的调用不能成环。
例如,构造函数 A()
不能委托给 B()
,而 B()
再委托回 A()
,否则会导致编译错误或无限递归。这点相信大家都能够想明白,这和shared指针相互持有对方而导致双方都无法释放内存是类似情况(应该不会有人真这么写吧 doge)。
class Exam {
public:
Exam():Exam(0) {}
Exam(int x) :Exam(){}
//报错:构造函数间接委托给自身,无法通过编译
};
委托构造的优点
-
减少代码重复:多个构造函数可以共享代码逻辑,避免了同样的初始化逻辑在多个构造函数中重复编写。
-
增强代码可维护性:如果需要更改初始化逻辑,只需修改一个构造函数,减少了维护时出错的可能性。
继承构造
继承构造函数是 C++11 中引入的另一个非常有用的特性。它允许派生类继承其基类的构造函数,而不需要在派生类中显式地定义这些构造函数。
class MyWidget_Base {
protected:
std::string str;
size_t mode;
public:
MyWidget_Base()
: MyWidget_Base(0, "Unknown") {}
MyWidget_Base(size_t mode)
: MyWidget_Base(mode, "Unknown") {}
MyWidget_Base(size_t mode, const std::string& str)
: mode(mode), str(str) {}
};
class Derived : protected MyWidget_Base {
public:
Derived() : MyWidget_Base() {}
Derived(int x) : MyWidget_Base(x) {}
Derived(int x, const std::string& str) : MyWidget_Base(x, str) {}
};
如上述代码所示,在 C++11 之前,如果派生类需要使用基类的构造函数,必须在派生类中显式地定义这些构造函数,并在初始化列表中调用基类的构造函数。
这意味着每个派生类构造函数都需要手动传递参数给基类构造函数,增加了代码的冗余和维护成本。
而在c++11之后,引入了继承构造这一概念:允许派生类自动继承父类所有的构造函数。
继承构造函数使用 using
关键字来继承基类的构造函数。使用非常简单:
class MyWidget_Base {
protected:
std::string str;
size_t mode;
public:
MyWidget_Base()
: MyWidget_Base(0, "Unknown") {}
MyWidget_Base(size_t mode)
: MyWidget_Base(mode, "Unknown") {}
MyWidget_Base(size_t mode, const std::string& str)
: mode(mode), str(str) {}
};
class Derived : protected MyWidget_Base {
public:
using MyWidget_Base::MyWidget_Base; //自动继承父类所有构造函数
void display() {
std::cout << mode << " " << str << std::endl;
}
};
int main() {
Derived d(0, "Hello");
d.display();
return 0;
}
--Execute:
0 Hello
继承构造的注意事项
-
析构函数是无法继承的!如果在派生类中想要实现特殊的析构行为,必须在自己类中进行定义。
-
默认构造函数是无法继承的!
如果基类的构造函数有默认参数,而派生类不定义自己的构造函数,那么派生类也会有一个默认构造函数。但如果基类没有默认构造函数,则继承构造函数不会生成默认构造函数。
class MyWidget_Base {
public:
MyWidget_Base(int x) :MyWidget_Base() {}
};
class Derived : public MyWidget_Base {
using MyWidget_Base::MyWidget_Base;
};
int main() {
Derived d; //报错:无默认构造
return 0;
}
继承构造的优点
-
减少代码冗余:如果派生类不需要额外的构造逻辑,可以直接继承基类的构造函数,避免在派生类中重复编写构造函数。
-
提高代码可读性:通过继承构造函数,派生类可以更加简洁明了地展示它继承了哪些构造逻辑。
感谢支持!如有需要可关注专栏,长期更新。