前文:C++ 构造函数的奇葩问题
对象中的对象
在前文中的类Person对象用string作为数据成员(因为string本身就是一个类类型的数据类型)。 这种构造技术称为"组合(composition)"。
组合既不是非常规的也不是C ++特定的:在C中,struct或union字段通常用于合成其他数据类型。 在C ++中,它需要一些特殊的考虑,因为它们的初始化有时会受到限制,如以下几节所述。
组合和(const)对象:(const)成员初始化器
除非另有说明,否则类的对象数据成员由其默认构造函数初始化。 使用默认构造函数可能并不总是初始化对象的最佳方式,甚至可能不可能:类可能根本不定义默认构造函数。
我们之前前文定义了Person的构造函数
Person::Person(string const &pname, string const &paddr, size_t const &age)
{
d_address = paddr;
d_name = pname;
d_age = age;
}
这个构造函数中发生了什么? 在构造函数的内部,需要对Person实例中的字符串对象的赋值。 由于赋值在构造函数体中使用,因此它们的左侧对象必须存在。 但是当对象已经存在,必须调用构造函数。 然后,Person的构造函数的主体立即撤消这些对象的初始化。这不仅效率低下,而且有时完全不可能。假设类接口提到了一个字符串const数据成员:其值一经初始化根本不应该改变(比如出生日期,它通常不会发生很大变化,因此是字符串const数据成员的一个很好的字符串类型).
构造函数的数据成员初始化
构造函数的主体允许分配数据成员。 数据成员的初始化发生在此之前.
- C ++定义了成员初始化器语法,允许我们指定数据成员在构造时初始化的方式。
- 成员初始值设定项被指定为构造函数的参数列表后面的冒号和构造函数体的左大括号之间的构造函数规范列表(constructor specifications list),如下所示:
//带参数的构造函数的实现
Person::Person(string const &pname, string const &paddr, size_t age)
: d_name(pname),
d_address(paddr),
d_age(age)
{
}
在此示例中,数据成员初始化使用( )。 也可以使用花括号而不是括号。 例如d_name也可以这样初始化:
d_name{ pname },
当对象在类中组合时,总是发生成员初始化:如果在成员初始化列表中没有提到构造函数,则调用对象的默认构造函数。 请注意这仅适用于对象。
- 基本数据类型的数据成员不会自动初始化。
- 但成员初始化也可以用于基本数据成员,如int和double。
上面的示例显示了数据成员d_name接受参数pname的初始化。 当使用成员初始值设定项时,并且成员初始值设定项中使用的左标识符始终是初始化的数据成员,而括号内的标识符被解释为参数。
初始化类类型的数据成员的顺序由组合类接口中定义的这些成员的顺序定义的。 如果构造函数中的初始化顺序与类接口中的顺序不同,则编译器会抱怨,并重新排序初始化以匹配类接口的顺序。
应尽可能经常使用成员初始化器。 如图所示,例如初始化const数据成员,或初始化缺少默认构造函数的类的对象)。但不使用成员初始器也会导致代码效率低下,因为没有显式指定成员初始值,类对象始终会自动调用各数据成员的默认构造函数。 这些数据成员的默认构造构在造函数体中重新分配显然效率低下。
当然,有时可以使用默认构造函数,但在这些情况下,可以省略显式成员初始值设定项。
根据经验:如果将值赋给构造函数体中的数据成员,则有利于避免数据成员重新分配。
引用成员初始值设定项
除了使用成员初始值设定项来初始化组合对象(无论是否为const对象),还有另一种情况,即必须使用成员初始值设定项。考虑以下情况。
我们假设有一个教务系统的学生管理模块StudentMgr类,需要读取Tearcher类的信息
Teacher对象可以在构造时传递给StudentMgr对象。直接传递一个对象(即按值传递)可能不是一个好主意,因为必须将该对象复制到tc参数中,然后可以使用StudentMgr类的数据成员保存该Teacher对象,使StudentMgr中其他成员函数或者外部可访问。
-
按值传递如下列情况:
StudentMgr::StudentMgr(Tearcher tc){ d_teacher=tc; }
-
如果使用指向Teacher对象的指针,则可以避免复制指令,如下所示:
StudentMgr::StudentMgr(Teacher* tc){ d_teacher=tc; }
这样的结构是可以的,但是迫使我们使用- >字段选择器操作符,而不 是“.”访问操作符。 从概念上讲,人们倾向于将Teacher对象视为对象,而不是指向对象的指针。 在C中,这可能是首选方法,但在C ++中我们可以使用“&”引用运算符。
-
按引用传递
可以将Teacher参数定义为StudentMgr的构造函数的引用参数,而不是使用值或指针参数。 接下来,在StudentMgr类中使用Teacher引用的数据成员。但是无法使用赋值操作符初始化引用变量,因此以下代码不正确:StudentMgr::StudentMgr(Teacher &tc){ d_teacher=tc; }
上个示例中的d_teacher=tc之所以不正确,是因为它不是初始化,而是将参数Teacher对象的引用分配给d_teacher.对引用变量的赋值仅仅是多了对Teacher对象的引用的副本。当我们尝试初始化的数据成员是对某类型的引用,我们应该是成员初始化列表的方式。
StudentMgr::StudentMgr(Teacher &tc):d_teacher(tc); {}
备注:严格来说,引用不是一种数据类型,类型涉及特定的内存块尺寸的分配和对应的变量值,而引用就是对它指向的数据类型新建了一个快捷方式而已。
委托构造函数
在C ++ 11中引入的构造函数能够调用同一个类的另一个构造函数非常有用。 这个功能名为委托构造函数。
下面示例是表示维度的Dimensional类,其中第四个函数的原型从语意上更有代表性,其余三个不同程度上的特殊化。
- Dimensional()
- Dimensional(int x)
- Dimensional(int x, int y)
- Dimensional(int x, int y, int z)
除了第四个构造函数外,前面三个分别不同程度上存在代码冗余。第一个和第二个,第二个和第三个,我们这里可以使用委托构造函数的技术解决这个问题。
#include
class Dimensional
{
int d_x,d_y,d_z;
public:
Dimensional(): d_x(0),d_y(0),d_z(0){}
Dimensional(int x): d_x(x),d_y(0),d_z(0){}
Dimensional(int x, int y) : d_x(x), d_y(y),d_z(0){}
Dimensional(int x, int y, int z) : d_x(x), d_y(y), d_z(z) {}
void display(){
std::cout << "x=" << d_x << std::endl;
std::cout << "y=" << d_y << std::endl;
std::cout << "z=" << d_z << std::endl;
}
};
int main(int argc, char const *argv[])
{
Dimensional d{3};
d.display();
return 0;
}
C ++的委托构造函数提供了一个优雅的解决方案来处理这个问题,允许我们通过将构造函数放在其他构造函数的初始化列表中来调用它。 以下程序演示了如何完成:
class Dimensional
{
int d_x, d_y, d_z;
public:
Dimensional() : d_x(0), d_y(0), d_z(0) {}
//委托构造函数
Dimensional(int x) : Dimensional()
{
d_x = x;
}
//委托构造函数
Dimensional(int x, int y) : Dimensional(x, y, 0) {}
Dimensional(int x, int y, int z) : d_x(x), d_y(y), d_z(z) {}
void display()
{
std::cout << "x=" << d_x << std::endl;
std::cout << "y=" << d_y << std::endl;
std::cout << "z=" << d_z << std::endl;
}
};
int main(int argc, char const *argv[])
{
Dimensional d{3, 7};
d.display();
return 0;
}
聚合类型
聚合是具有以下内容的数组或类
- 没有用户提供的,显式的或继承的构造函数,
- 没有private或protected的非静态数据成员,
- 没有虚函数,并且没有virtual,private或protected的基类。
聚合类型的元素是:
- 对于数组,数组元素按下标顺序递增,或者
- 对于一个类,按声明顺序为直接基类,然后为声明顺序的非匿名union成员的直接非静态数据成员。
Trivial 类型
当C ++中的类或结构具有编译器提供的或显式默认的特殊成员函数时,则为普通类型。 它占用一个连续的内存区域。 它可以具有不同访问控制的成员。
Trivial类型具有普通默认构造函数,普通副本构造函数,普通副本赋值运算符和普通析构函数。 在每种情况下,简单类所做的事情都意味着构造函数/运算符/析构函数不是用户提供的,而是属于具有以下特征的类:
- 没有虚函数或虚基类
- 没有对应的非Trivial构造函数/操作者/解构函数的基类
- 没有与相应的非Trivial构造函数/操作符/解构函数的类类型的数据成员
//不存在显式的构造函数,编译器会使用默认的构造函数
class Triangle{
private:
int d_a,d_b,d_c;
}
另外关于聚合,Trivial类型的说明会在C++14,C++17以后后续被变更,具体可以参考stackflow该问题集,里面不定时会得到更新