构造函数
-
作用:
- 构造函数的工作是保证每个对象的数据成员具有合适的初始值
- 创建一个类类型的对象时,编译器会自动使用一个构造函数来初始化该对象。
-
形式:构造函数是一个特殊的、与类同名的成员函数,用于给每个数据成员设置适当的初始值。该函数无返回类型!(注意:是“无”! 而不是空!(void))
特点:
- 构造函数是特殊的成员函数,只要创建类类型的新对象,都要执行构造函数。
- 构造函数自动执行,只要创建该类型的一个对象,编译器就运行一个构造函数。
- 构造函数不能声明为 const
- 构造函数可以被重载。
如何定义构造函数
- 构造函数的名字与类的名字相同,并且不能指定返回类型。像其他任何函数一样,它们可以没有形参,也可以定义多个形参。
- 构造函数一般就使用一个构造函数初始化列表,来初始化对象的数据成员。
- 与任意的成员函数一样,构造函数可以定义在类的内部或外部。
构造函数示例代码:
class Sales_item {
public:
// operations on Sales_itemobjects
// default constructor needed to initialize members of built-in type
Sales_item(): units_sold(0), revenue(0.0) { } //构造函数初始化列
private:
std::string isbn; //isbn 成员由 string 的`默认构造函数` `隐式初始化`为空串。
unsigned units_sold;
double revenue;
};
说明:
- 这个构造函数使用构造函数初始化列表来初始化 units_sold 和 revenue成员。
- isbn 成员由 string 的
默认构造函数
隐式初始化
为空串。
构造函数初始化列表(constructor initializer list)
构造函数初始化列表负责构造函数中显式初始化的工作。初始化列表的形式见上面的代码。
特点:
- 构造函数初始化只在构造函数的定义中而不是声明中指定,也就是说初始化列表只能在定义中。
构造函数显式初始化
:构造函数初始化列表负责构造函数中显式初始化的工作。
构造函数隐式初始化
:没有构造函数初始化列表,构造函数隐式使用数据成员默认的构造函数来进行初始化(注意初始化是在执行构造函数的函数体之前)。而函数体中的赋值就已经算是覆盖之前的值了。
举例:
// legal but sloppier way to write the constructor:
// no constructor initializer
Sales_item::Sales_item(const string &book) //这里没有构造函数初始化列表,变量使用的是隐式初始化
{
isbn = book;
units_sold = 0;
revenue = 0.0;
}
说明:没有显式的初始化式时,在执行构造函数之前,要初始化 isbn 成员。这个构造函数隐式使用默认的 string 构造函数来初始化 isbn。执行构造函数的函数体时,isbn 成员已经有值了。该值被构造函数函数体中的赋值所覆盖。
从概念上讲,可以认为构造函数分两个阶段执行:
初始化阶段
;//也就是没有进入构造函数函数体之前
11. 不管成员是否在构造函数初始化列表中显式初始化,类类型
的据成员总是在初始化阶段初始化。初始化发生在计算阶段开始之前。普通的计算阶段
。计算阶段由构造函数函数体中的所有语句组成。
如果没有初始化列表,则使用与初始化变量相同的规则来进行初始化。
- 运行该类型的默认构造函数,初始化类类型的数据成员:
- 内置或复合类型的成员的初始值依赖于对象的作用域:在局部作用域中这些成员不被初始化,而在全局作用域中它们被初始化为0。(也就是说对象如果定义在局部作用域,则不初始化,在全局作域则初始化为0)
有时需要构造函数初始化列表
必须对任何 const 或引用类型成员以及没有默认构造函数的类类型的任何成员使用初始化式。
如果没有为类成员提供初始化式,则编译器会隐式地使用成员类型的默认构造函数。如果那个类没有默认构造函数,则编译器尝试使用默认构造函数将会失败。在这种情况下,为了初始化数据成员,必须提供初始化式。
有些成员必须在构造函数初始化列表中进行初始化。对于这样的成员,在构造函数函数体中对它们赋值不起作用:
- 没有默认构造函数的类类型的成员
- const 或引用类型的成员
- 记住,可以初始化 const 对象或引用类型的对象,但不能对它们赋值。在开始执行构造函数的函数体之前,要完成初始化。初始化 const 或引用类型数据成员的唯一机会是构造函数初始化列表中。
因为内置类型的成员不进行隐式初始化,所以对这些成员是进行初始化还是赋值似乎都无关紧要。除了两个例外,对非类类型的数据成员进行赋值或者使用初始化式在结果和性能上都是等价的。
成员初始化的次序
构造函数初始化列表仅指定用于初始化成员的值,并不指定这些初始化执行的次序。
成员被初始化的次序就是定义成员的次序
。第一个成员首先被初始化,然后是第二个,依次类推。
成员初始化的次序影响初始化
初始化的次序常常无关紧要。然而,如果一个成员是根据其他成员而初始化,则成员初始化的次序是至关重要的。
比如下面的例子:
class X {
int i;
int j;
public:
// run-time error: i is initialized before j
X(int val): j(val), i(j) { } //i根据j初始化的
};
说明:在这种情况下,构造函数初始化列表看起来似乎是用val 初始化 j,然后再用 j 来初始化 i。然而,i 首先被初始化。这个初始化列表的效果是用尚未初始化的 j 值来初始化 i!
如果数据成员在构造函数初始化列表中的列出次序与成员被声明的次序不同,那么有的编译器非常友好,会给出一个警告
如何解决
- 按照与成员声明一致的次序编写构造函数初始化列表是个好主意。
- 尽可能避免使用成员来初始化其他成员。
- 一般情况下,通过(重复)使用构造函数的形参而不是使用对象的数据成员,可以避免由初始化式的执行次序而引起的任何问题。
例如,下面这样为 X 编写构造函数可能更好:
X(int val): i(val), j(val) { }
在这个版本中,i 和 j 初始化的次序就是无关紧要的。
初始化式可以是任意表达式
一个初始化式可以是任意复杂的表达式。例如,可以给 Sales_item 类一个新的构造函数,该构造函数接受一个 string 表示 isbn,一个 usigned 表示售出书的数目,一个 double 表示每本书的售出价格:
Sales_item(const std::string &book, int cnt, double price):
isbn(book), units_sold(cnt), revenue(cnt * price) { } //revenue(cnt * price)使用了初始化式
revenue 的初始化式使用表示价格和售出数目的形参来计算对象的 revenue 成员
类类型的数据成员的初始化式
初始化类类型的成员时,要指定实参并传递给成员类型的一个构造函数。可以使用该类型的任意构造函数。例如,Sales_item 类可以使用任意一个 string构造函数来初始化 isbn。也可以用 ISBN 取值的极限值来表示isbn 的默认值,而不是用空字符串。可以将 isbn 初始化为由 10 个 9 构成的串:
// alternative definition for Sales_item default constructor
Sales_item(): isbn(10, '9'), units_sold(0), revenue(0.0) {}
默认构造函数
默认构造函数(default constructor)
定义:就是在没有显式提供初始化式时调用的构造函数。它由不带参数的构造函数,或者为所有的形参提供默认实参的构造函数定义。也就是说它包括了以下两种情况:
- 没有带明显形参的构造函数。
- 提供了默认实参的构造函数。
没有带明显形参的意思是,编译器总是会为我们的构造函数形参表插入一个隐含的this指针,所以”本质上”是没有不带形参的构造函数的,只有不带明显形参的构造函数,它就是默认构造函数。
单从定义很难对普通构造函数和默认构造函数进行区别,下面看代码进行区分:
class testClass
{
public:
testClass(); /* 默认构造函数,和下面的另一个构造函数只能二选一 */
testClass(int a, char b); /* 构造函数 */
testClass(int a=10,char b='c'); /* 默认构造函数 */
private:
int m_a;
char m_b;
};
说明:
- 一个类只能有一个默认构造函数!也就是说上述两个默认构造函数不能同时出现,一般选择 testClass(); 这种形式的默认构造函数 。
注意:
- 默认构造函数没有参数、对象建立的时候自动调用。也就是是可以不用实参进行调用的构造函数。
默认构造函数
和合成的默认构造函数
是不一样的。默认构造函数包含:- 我们自定义的
- 编译器创建的(合成的默认构造函数)。
类设计者可以自己写一个默认构造函数。也可以让编译器帮我们写,编译器帮我们写的默认构造函数,称为“合成的默认构造函数”。
合成的默认构造函数
Q:什么时候会合成默认构造函数?
A:只有当类没有声明任何构造函数时,编译器才会自动的生成默认构造函数。合成的默认构造函数只适合非常简单的类。
特点:
- 一个类哪怕只定义了一个构造函数,编译器也不会再生成默认构造函数。
- 这条规则的根据是,如果一个类在某种情况下需要控制对象初始化,则该类很可能在所有情况下都需要控制。
- 只有当一个类没有定义构造函数时,编译器才会自动生成一个默认构造函数。
合成的默认构造函数(synthesized default constructor)
使用与变量初始化相同的规则来初始化成员。那么与变量初始化相同的规则是什么呢?
- 具有类类型的成员通过运行各自的默认构造函数来进行初始化。
- 内置和复合类型的成员,如指针和数组,只对定义在全局作用域中的对象才初始化。当对象定义在局部作用域中时,内置或复合类型的成员不进行初始化。
Q:什么时候需要定义自己的构造函数而不去使用合成的默认构造函数呢呢?
A:如果类包含内置或复合类型的成员,则该类不应该依赖于合成的默认构造函数。它应该定义自己的构造函数来初始化这些成员。(合成的默认构造函数并不会初始化内置或复合类型的成员。)
默认构造函数的使用
使用默认构造函数定义一个对象的正确方式:
// ok: defines a class object ...
Sales_item myobj;
另一方面,下面这段代码也是正确的:
// ok: create an unnamed, empty Sales_item and use to initialize myobj
Sales_item myobj = Sales_item();
还可以使用一下方式:
Sales_item *myobj=new Sales_item (); //堆中分配 ,由管理者进行内存的分配和管理,用完必须delete(),否则可能造成内存泄漏
概括一下就是,
A a; //栈中分配
A b = A(); //栈中分配
A* c = new A(); //堆中分配
说明:第一种和第二种没什么区别,一个隐式调用,一个显式调用
初级 C++ 程序员常犯的一个错误是,采用以下方式声明一个用默认构造函数初始化的对象:
// oops! declares a function, not an object
Sales_item myobj();
实际上,上面这种形式是声明了一个函数,而不是一个对象。
类类型的隐式类型转换
为了定义到类类型的隐式转换,需要定义合适的构造函数。
- 可以用单个实参来调用的构造函数定义了从形参类型到该类类型的一个隐式转换。
conversion constructor(转换构造函数)
:可用单个实参调用的非 explicit 构造函数。
- 转换构造函数也是构造函数的一种。(也就是用于构造某一种类型的对象)
- 可以隐式使用转换构造函数将实参的类型转换为类类型。
代码示例:两个构造函数的 Sales_item 版本,这里的每个构造函数都定义了一个隐式转换。
class Sales_item {
public:
// default argument for book is the empty string
Sales_item(const std::string &book = ""):
isbn(book), units_sold(0), revenue(0.0) { }
Sales_item(std::istream &is);
// as before
};
如果某些地方需要一个 Sales_item类型对象(比如函数参数是一个Sales_item类型对象),就可以使用一个 string 或一个 istream类型的对象通过上面的转换构造函数进行隐式类型转换变成Sales_item类型对象。比如:
string null_book = "9-999-99999-9";
// ok: builds a Sales_itemwith 0 units_soldand revenue from
// and isbn equal to null_book
item.same_isbn(null_book); //该函数期待一个 Sales_item 对象作为实参
//这段程序使用一个 string 类型对象作为实参传给 Sales_item 的same_isbn 函数。
抑制由构造函数定义的隐式转换
可以通过将构造函数声明为 explicit,来防止在需要隐式转换的上下文中使用构造函数.
explicit 关键字只能用于类内部的构造函数声明上。在类的定义体外部所做的定义上不再重复它。
class Sales_item {
public:
// default argument for book is the empty string
explicit Sales_item(const std::string &book = ""):isbn(book), units_sold(0), revenue(0.0) { }
explicit Sales_item(std::istream &is);
// as before
};
为转换而显式地使用构造函数
只要显式地按下面这样做,就可以用显式的构造函数来生成转换:
string null_book = "9-999-99999-9";
// ok: builds a Sales_itemwith 0 units_soldand revenue from
// and isbn equal to null_book
item.same_isbn(Sales_item(null_book)); //显式的构造函数来生成转换
补充说明
C++类型说明:
C++类型分为三类:内置类型,复合类型和类类型。
内置类型
:编译器内置的基本类型,如int, char, float, double, bool等;复合类型
:根据其它类型定义的类型,主要分几类:数组,字符串(C-style),指针,引用,结构体(struct),联合体(union);类类型
:用struct和class定义的类。
复合类型的结构体指的也是c-style的结构体,和类型类一样,也是用struct和class定义;其和类类型的区别是,没有构造函数(包括默认构造函数)和析构函数。
C++ 对象实例化:
如果classA a; //a存在栈上
如果classA a = new classA(); //就存在堆中。
参考资料
C++ 类型:内置类型,复合类型和类类型
《C++Primer》第十二章-类-学习笔记(2)-作用域&构造函数
C++ 合成默认构造函数的真相
C++的默认构造函数与构造函数