构造函数:
每个类都定义了它的对象被初始化的方式,类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数。构造函数的任务是初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数。
构造函数的名字和类名相同。和其他函数不一样的是,构造函数没有返回类型。除此之外类似于其他的函数,构造函数也有一个(可能为空的)参数列表和一个(可能为空的)函数体。类可以包含多个构造函数,和其他重载函数差不多,不同的构造函数之间必须在参数数量或者参数类型上有所区别(类的其他成员函数也可以重载,规则和一般函数重载一样)。
不同与其他成员函数,构造函数不能被声明成 const 的,当我们创建类的一个 const 对象时,直到构造函数完成初始化过程,对象才能真正取得其 "常量" 属性。因此,构造函数在 const 对象的构造过程中可以向其写值。
合成的默认构造函数:
如果我们没有显示地定义构造函数,那么编译器就会隐式地定义一个默认构造函数(合成的默认构造函数)。一般情况下合成的构造函数按照如下规则初始化类的数据成员:
1.如果存在类内的初始值,用它来初始化成员
2.否则,默认初始化
合成的默认构造函数只适合非常简单的类,比如上一篇博客中的 Sales_data 类。对于一般类来说是必须要定义默认构造函数的:
1.编译器只有在发现类不包含任何构造函数的情况下才会生成一个默认的构造函数。一旦我们定义了一些其他的构造函数,那么除非我们再定义一个默认的构造函数,否则类将没有默认构造函数。
2.对于某些类来说,合成默认构造函数可能执行错误的操作。如果定义在块中的内置类型或复合类型(比如数组和指针)的对象被默认初始化,则它们的值将是未定义的。这条准则同样适用于默认初始化的内置类型成员。因此,含有内置类型或复合类型成员的类应该在类的内部初始化这些成员,或者定义一个自己的默认构造函数。否则,用户在创建类的对象时就可能得到未定义的值。
3.有的时候编译器不能为某些类合成默认的构造函数。如:如果类中包含一个其他类类型的成员且这个成员的类型没有默认构造函数,那么编译器将无法初始化该成员。
现在我们来给上篇博客中的 Sales_data 类添加几个构造函数:
1 struct Sales_data{ 2 //数据成员 3 std::string book_no; 4 unsigned units_sold = 0; 5 double revenue = 0.0; 6 7 //新增的构造函数 8 Sales_data() = default;//不接受任何实参,默认构造函数 9 Sales_data(const std::string &s): book_no(s){}//除了book_no外其他成员将使用类内初始值 10 Sales_data(const std::string &s, unsigned n, double p): book_no(s), units_sold(n), revenue(p * n){} 11 Sales_data(std::istream); 12 13 //之前的函数成员 14 std::string isbn() const { 15 return book_no; 16 // return this->book_no;//等价语句 17 } 18 Sales_data& combine(const Sales_data&); 19 double avg_price() const; 20 };
首先我们定义了其他构造函数,如前面所述,编译器不会再合成默认构造函数,所以我们通过 Sales_data() = default;显示地定义了一个默认构造函数。在 c++11 标准中,如果我们需要默认的行为,那么可以通过在参数列表后面写上 = default;来要求编译器生默认成构造函数。其中,= default 既可以和声明一起出现在类的内部,也可以作为定义出现在类的外部。和其他函数一样,如果 = default 在类内部,则默认构造函数是内联的,反之则不是内联的。(关于default:http://blog.csdn.net/a1875566250/article/details/40406883)
构造函数初始值列表:
1 Sales_data(const std::string &s): book_no(s){}//除了book_no外其他成员将使用类内初始值 2 Sales_data(const std::string &s, unsigned n, double p): book_no(s), units_sold(n), revenue(p * n){}
其中,和一般函数一样,花括号里面是函数体,不过这些构造函数唯一的目的就是为数据成员赋初值。不需要执行其他任务,所以函数体为空。冒号和花括号之间的是构造函数初始值列表,每个数据成员后面括号里的就是该数据成员的初始值,不同成员的初始化通过逗号分隔开来。
在类外部定义构造函数:
1 Sales_data::Sales_data(std::istream &is){ 2 read(is, *this);//read 函数的作用是从 is 中读取一条信息然后存入 this 中 3 }
和其他定义在外部的成员函数一样,我们需要用 :: 来指明该构造函数是哪个类的成员。
还需要注意的是该构造函数调用了 read 函数,所以该构造函数的定义必须写在 read 函数的声明之后。
成员初始化的顺序:
构造函数初始值列表只说明用于初始化成员的值,而不限定初始化的具体执行顺序。成员初始化的顺序与他们在类中的出现顺序一致:第一个成员先被初始化,然后第二个,以此类推。构造函数初始值列表中初始值的前后位置关系不会影响实际初始化的顺序。
一般来说,初始化的顺序没什么特别要求。不过如果一个成员是用另一个成员来初始化的,那么这两个成员初始化顺序就很重要了:
1 #include <iostream> 2 using namespace std; 3 4 class gel{ 5 private: 6 int x; 7 int y; 8 9 public: 10 gel(int val): y(val), x(y){};//这里是先给x初始化,在给y初始化的 11 void print(std::ostream &os) const{ 12 os << x << " " << y; 13 } 14 15 }; 16 17 int main(void){ 18 gel lou(1); 19 lou.print(cout); 20 return 0; 21 }
在这个例子中,先初始化的 x,x 的初始值为 y,但此时 y 并没有初始化,所以 x 的初始值会是一个未定义的值。然后再将 y 初始化为 1。
写代码时,最好令构造函数初始值的顺序与成员声明的顺序保持一致。而且尽量避免使用某些成员初始化其他成员。
默认实参构造函数:
和一般函数一样,构造函数也可以使用默认实参,并且规则一致:
1 #include <iostream> 2 using namespace std; 3 4 class gel{ 5 private: 6 int x, y, z; 7 8 public: 9 gel() = default; 10 gel(int a, int b = 2, int c = 3): x(a), y(b), z(c){};//b和c用了默认实参 11 void output(std::ostream &os) const{ 12 os << x << " " << y << " " << z << std::endl; 13 } 14 15 }; 16 17 int main(void){ 18 gel lou1(1), lou2(1, 1), lou3(1, 1, 1); 19 lou1.output(cout);//输出1 2 3 20 lou2.output(cout);//输出1 1 3 21 lou3.output(cout);//输出1 1 1 22 return 0; 23 }
委托构造函数:
C++11新标准扩展了构造函数初始值的功能,所以委托构造函数就出现了,一个委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程,或者说他把自己的一些(或全部)职责委托给了其他构造函数。和其他构造函数一样,一个委托构造函数也有一个成员初始值列表和一个函数体。在委托构造函数内,成员初始值列表只有一个唯一的入口,就是类名本身。和其他成员初始值一样,类名后面紧跟圆括号括起来的参数列表,参数列表必须跟类中另外一个函数匹配:
1 #include <iostream> 2 using namespace std; 3 5 class Sales_data; 6 7 std::istream &read(std::istream&, Sales_data&); 8 9 class Sales_data{ 10 friend std::istream &read(std::istream&, Sales_data&); 11 //数据成员 12 private: 13 std::string book_no; 14 unsigned units_sold = 1; 15 double revenue = 1.0; 16 17 public: 18 Sales_data(const std::string &s, unsigned n, double p): book_no(s), units_sold(n), revenue(p * n){} 19 Sales_data() : Sales_data("", 0, 0){}//委托给参数列表对于的构造函数,第一个构造函数 20 Sales_data(std::string s) : Sales_data(s, 0, 0){};//委托给参数列表对于的构造函数,第一个构造函数 21 Sales_data(std::istream &is) : Sales_data(){//委托给参数列表对于的构造函数,第二个构造函数(被委托的构造函数本身也是一个委托构造函数) 22 read(is, *this); 23 } 24 void print(std::ostream &os) const{ 25 os << book_no << " " << units_sold << " " << revenue << endl; 26 } 27 28 }; 29 30 std::istream &read(std::istream &is, Sales_data &item){//从给定流中将数据读到给定的对象里 31 double price = 0; 32 is >> item.book_no >> item.units_sold >> price; 33 item.revenue = price * item.units_sold; 34 return is; 35 } 36 37 int main(void){ 38 Sales_data total(cin); 39 Sales_data cnt("hello"); 40 total.print(cout); 41 cnt.print(cout); 42 return 0; 43 }
在此例中并不能体现委托构造函数的优势,但是如果构造函数本身很长很麻烦时,使用委托构造函数显然能大大简化代码。
需要注意的是:当一个构造函数委托给另一个构造函数时,受委托的构造函数的初始值列表和函数体一次被执行。在此例中,受委托的构造函数体恰好是空的。假如函数体包含有代码的话,将先执行这些代码,然后控制权才会交还给委托者的函数体。