您的位置:首页 > 编程语言 > C语言/C++

C++(20)构造函数

2015-08-08 19:45 465 查看



--构造函数【上】

引言:

构造函数确保每个对象在创建时自动调用,以确保每个对象的数据成员都有合适的初始值。

[cpp] view
plaincopy





class Sales_item

{

public:

//其中isbn由string的默认构造函数提供初始化

Sales_item():units_sold(0),revenue(0){}



private:

std::string isbn;

unsigned units_sold;

double revenue;

};

构造函数的几大特征:

1、构造函数可以被重载

一般而言,不同的构造函数允许用户指定不同的方式来初始化数据成员。

[cpp] view
plaincopy





class Sales_item

{

//other members as before

public:

Sales_item();

Sales_item(const string &);

Sales_item(std::istream &);

};

2、实参决定使用哪个构造函数

上面我们定义了三个构造函数,在定义新对象时,可以使用这些构造函数中的任意一个:

[cpp] view
plaincopy





Sales_item empty;

Sales_item Primer_3td_Ed("0-201-82470-1");

Sales_item Primer_4th_Ed(cin);

3、构造函数自动执行

只要创建该类型的一个对象,编译器就自动运行一个构造函数:

[cpp] view
plaincopy





//调用含有一个string形参的构造函数

Sales_item Primer_2cn_Ed("0-201-54848-8");

//调用默认构造函数初始化该对象

Sales_item *p = new Sales_item();

4、用于const对象的构造函数

[cpp] view
plaincopy





class Sales_item

{

public:

//构造函数不能是const

Sales_item() const; //Error

};

创建类类型的const对象时,运行一个普通构造函数就可以初始化该const对象。构造函数的工作就是初始化对象,不管对象是否为const,都用一个构造函数来初始化对象(瞬间对构造函数有种敬仰之情...).

[cpp] view
plaincopy





//P387 习题12.19

class NoName

{

public:

NoName():pstring(0),ival(0),dval(0){}

NoName(std::string *Pstr,int Ival,double Dval):pstring(Pstr),ival(Ival),dval(Dval){}

private:

std::string *pstring;

int ival;

double dval;

};

一、构造函数初始化式

构造函数的初始化列表以一个冒号可是,接着是一个以逗号分割的数据成员列表,每个数据成员跟一个放在圆括号中的初始化式。

构造函数可以定义在类的内部或外部,但是构造函数的初始化式只在构造函数的定义中而不是在声明中指定。

【小心O(∩_∩)O~】

构造函数初始化列表是许多相当有经验的C++程序员都没有掌握的一个特性。

[cpp] view
plaincopy





Sales_item(const std::string &book)

{

//其实在执行这一条语句之前,isbn已经有值了

//这个构造函数隐式使用string构造函数初始化isbn

isbn = book;

units_sold = 0;

revenue = 0;

}

从概念上将,可以认为构造函数分为两个阶段执行:

1)初始化阶段;

2)普通的计算阶段。计算阶段由构造函数函数体中的所有语句组成。

不管成员是否在构造函数初始化列表中显式初始化,类类型的数据成员总是在初始化阶段初始化。初始化发生在计算阶段开始之前

在构造函数初始化列表中没有显式提及的每个成员,使用与初始化变量相同的规则来进行初始化。运行该类型的默认构造函数,来初始化类类型的数据成员。内置或复合类型的成员的初始值依赖于对象的作用域:在局部作用域中这些成员不被初始化,而在全局作用域中它们被初始化为0

使用构造函数初始化列表的版本初始化【重点是“初始化”】数据成员,没有定义初始化列表的构造函数版本在构造函数函数体中对数据成员赋值【亮点在”赋值“】。这个区别的重要性取决于数据成员的类型。

1、有时需要构造函数初始化列表

有些成员必须在构造函数初始化列表中进行初始化。对于这样的成员,在构造函数函数体中对它们赋值不起作用。没有默认构造函数的类类型的成员,以及const或引用类型的成员,不管是哪种类型,都必须在构造函数初始化列表中进行初始化。

[cpp] view
plaincopy





class ConstRef

{

public:

ConstRef(int ii);



private:

int i;

const int ci;

int &ri;

};

ConstRef::ConstRef(int ii)

{

i = ii; //OK

ci = ii; //Error

ri = ii; //没有编译错误,但是ri事实上根本没有绑定任何对象

}

谨记:可以初始化const对象或引用类型的对象,初始化const或引用类型数据成员的唯一机会是构造函数初始化列表中。编写该构造函数的正确方式为:

[cpp] view
plaincopy





class ConstRef

{

public:

ConstRef(int ii);



private:

int i;

const int ci;

int &ri;

};

ConstRef::ConstRef(int ii):i(ii),ci(ii),ri(ii){}

【建议:使用构造函数初始化列表,P389】

在许多类中,初始化和赋值严格来讲都是低效率的:数据成员可能已经被直接初始化了,还要对它进行初始化和赋值。比较率问题更重要的是,某些数据成员必须要初始化,这是一个事实

因此,必须对任何const或引用类型成员以及没有默认构造函数的类类型的任何成员使用初始化式。

当类成员需要使用初始化列表时,通过常规地使用构造函数初始化列表,就可以避免发生编译时错误。

2、成员初始化的次序

每个成员在构造函数初始化列表中只能指定一次;

构造函数初始化列表仅指定用于初始化成员的值,并不指定这些初始化执行的次序。成员被初始化的次序就是定义成员的次序!

初始化的次序常常无关紧要。然而,如果一个成员是根据其他成员而初始化,则成员初始化的次序是至关重要的

[cpp] view
plaincopy





class X

{

int i;

int j;



public:

X(int val):j(val),i(j){} //在GCC编译器上会给出警告

};

按照与成员声明一致的次序编写构造函数初始化列表是个好主意。此外,尽可能避免使用成员来初始化其他成员。因此,一般情况下,通过(重复)使用构造函数的形参而不是使用对象的数据成员!

[cpp] view
plaincopy





X(int val):j(val),i(val){}

3、初始化式可以是任意复杂的表达式

[cpp] view
plaincopy





Sales_item(const std::string &book,int cnt,double price):

isbn(book),units_sold(cnt),revenue(cnt * price) {}

4、类类型的数据成员的初始化式

初始化类类型的成员时,要指定实参并传递给成员类型的一个构造函数。可以使用该类型的任意构造函数。

[cpp] view
plaincopy





Sales_item():isbn(10,'a'),units_sold(0),revenue(0){}

[cpp] view
plaincopy





//P390 习题12.21

class DemoClass

{

public:

DemoClass():str("DemoClass"),ival(0),pdou(0),in(inFile){}



private:

const string str;

int ival;

double *pdou;

ifstream ∈

};

[cpp] view
plaincopy





//习题12.23

class NoDefault

{

public:

NoDefault(int);

};



class C

{

public:

C(int ival):no(ival){}



private:

NoDefault no;

};



--构造函数【下】

二、默认实参与构造函数

一个重载构造函数:

[cpp] view
plaincopy





Sales_item():units_sold(0),revenue(0){}

Sales_item(const std::string &book):

isbn(book),units_sold(0),revenue(0) {}

可以通过给string初始化式提供一个默认实参将这些构造函数组合起来:

[cpp] view
plaincopy





Sales_item(const string &book = " "):

isbn(book),units_sold(0),revenue(0) {}

因此:

[cpp] view
plaincopy





Sales_itemempty;

Sales_itemPrimer_3rd_Ed("0-201-82470-1");

都将执行为其string形参接受默认实参的那个构造函数。

【最佳实践】

我们更喜欢使用默认实参,因为它减少代码重复!

[cpp] view
plaincopy





//P391 习题12.25

Sales_item(std::istream &in = std::cin);

三、默认构造函数

只要定义一个对象时没有提供初始化式,就使用默认构造函数。为所有形参提供默认实参的构造函数也定义了默认构造函数。

1、合成的构造函数

一个类哪怕只是定义了一个构造函数,编译器也不会再生成默认构造函数。这条规则的根据是,如果一个类在某种情况下需要控制对象初始化,则该类很可能在所有情况下都需要控制。

只有当一个类没有定义构造函数时,编译器才会自动生成一个默认构造函数

【最佳实践】

如果类包含内置或复合数据类型的成员,则该类不应该依赖于合成的默认构造函数。他应该定义自己的构造函数来初始化这些成员!

如果每个构造函数将每个成员设置为明确的已知状态,则成员函数可以区分空对象和具有实际值的对象。

2、类通常定义一个默认构造函数

假定有一个NoDefault类,它没有定义自己的默认构造函数,却有一个接受一个string实参的构造函数。因为该类定义了一个构造函数,因此编译器将不合成默认构造函数。NoDefault没有默认构造函数,意味着:

1)具有NoDefault成员的每个类的每个构造函数,必须通过传递一个初始的string值给NoDefault构造函数来显式地初始化NoDefault成员。

2)编译器将不会为具有NoDefault类型成员的类合成默认构造函数。如果这样的类希望提供默认构造函数,就必须显式地定义,并且默认构造函数必须显式地初始化其NoDefault成员。

3)NoDefault类型不能用作动态分配数组的元素类型。

4)NoDefault类型的静态分配数组必须为每个元素提供一个显式的初始化式。

5)如果有一个保存NoDefault对象的容器,例如vector,就不能使用接受容器大小而没有同时提供一个元素初始化式的构造函数。

实际上,如果定义了其他的构造函数,则提供一个默认构造函数几乎总是对的。通常,在默认构造函数中给成员提供的初始值指出该对象是空的!

3、使用默认构造函数

使用默认构造函数定义一个对象的:

[cpp] view
plaincopy





Sales_item myObj;

或者是:

[cpp] view
plaincopy





Sales_item myObj = Sales_item();

编译器创建并初始化一个Sales_item对象,然后用它来按值初始化myObj。但是不能是下面这种形式:

[cpp] view
plaincopy





//myObj的定义被编译器解释为一个函数的声明!!!

Sales_item myObj();

四、隐式类类型转换

为了定义到类类型的隐式转换,需要定义合适的构造函数。

可以使用单个实参来调用的构造函数定义了从形参类型到该类型的一个隐式转换!

我们以前定义的两个构造函数:

[cpp] view
plaincopy





class Sales_item

{

public:

Sales_item(const std::string &book):

isbn(book),units_sold(0),revenue(0) {}

Sales_item(istream &in);



bool same_isbn(const Sales_item &item)

{

return isbn == item.isbn;

}



private:

std::string isbn;

unsigned units_sold;

double revenue;

};

在这儿其实每个构造函数都定义了一个隐式转换!!!因此,在期待Sales_item类型对象的地方,可以使用一个string或者istream:

[cpp] view
plaincopy





string null_book("9-999-99999-9");

item.same_isbn(null_book);

item.same_isbn(cin);

该函数期待一个Sales_item对象作为实参。编译器使用接受一个 string或 istream的Sales_item构造函数生成一个新的Sales_item对象。新生成的(临时的)Sales_item被传递给same_isbn。

由于这个Sales_item对象是一个临时对象。一旦same_isbn结束,就不能再访问它。实际上,我们构造了一个在测试完成后被丢弃的对象。这个行为几乎肯定是一个错误。

1、抑制由构造函数定义的隐式转换

可以通过将构造函数声明为explicit,来防止在需要隐式转换的上下文中使用构造函数:

[cpp] view
plaincopy





explicit Sales_item(const std::string &book):

isbn(book),units_sold(0),revenue(0) {}

explicit Sales_item(istream &in);

调用:

[cpp] view
plaincopy





item.same_isbn(null_book); //Error

item.same_isbn(cin); //Error

说明:explicit只能用于类内部的构造函数的声明:

[cpp] view
plaincopy





//Error

explicit Sales_item::Sales_item(istream &is)

{

is >> *this;

}

2、为转换而显式地使用构造函数

[cpp] view
plaincopy





item.same_isbn(Sales_item(null_book));

item.same_isbn(Sales_item(cin));

显式使用构造函数只是终止了隐式地使用构造函数。任何构造函数都可以用来显式地创建临时对象!

【最佳实践】

通常,除非有明显的理由想要定义隐式转换,否则,单形参构造函数应该为explicit。将构造函数设置为explicit可以避免错误,并且当转换有用时,用户可以显式地构造对象

[cpp] view
plaincopy





//P395 习题12.30

//下面程序说明了什么?

void f(const vector<int> &);

int main()

{

vector<int> v;

f(v); //OK

f(42); //Error

f(vector<int>(42)); //OK

return 0;

}

五、类成员的显式初始化

对于没有定义构造函数并且其全体数据成员均为public的类,可以采用与初始化数组元素相同的方式初始化其成员:

[cpp] view
plaincopy





struct Data

{

int ival;

char *ptr;

};



int main()

{

Data val1 = {0,0};

Data val2 = {1,"Hello World"};

}

缺点:

1)要求类的全体数据成员都是public。

2)将初始化每个对象的每个成员的负担放在程序员身上。这样的初始化是乏味且易于出错的,因为容易遗忘初始化式或提供不适当的初始化式。

3)如果增加或删除一个成员,必须找到所有的初始化并正确更新。

【最佳实践】

定义和使用构造函数几乎总是较好的。当我们为自己定义的类型提供一个默认构造函数时,允许编译器自动运行那个构造函数,以保证每个类对象在初次使用之前正确地初始化

[cpp] view
plaincopy





//P396 习题12.31

//不能通过编译,为什么?

pair<int,int> p = {0,2};

/*

*因为pair类型定义了构造函数

*所以尽管其数据成员为public,但还是不能显式的初始化

*/
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: