C++ Primer 学习笔记_52_类与数据抽象 -构造函数【下】

C++ Primer 学习笔记_52_类与数据抽象 --构造函数【下】

--构造函数【下】



二、默认实参与构造函数

一个重载构造函数:

    Sales_item():units_sold(0),revenue(0){}
    Sales_item(const std::string &book):
        isbn(book),units_sold(0),revenue(0) {}

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

    Sales_item(const string &book = " "):
        isbn(book),units_sold(0),revenue(0) {}

因此:

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

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

【最佳实践】

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

//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、使用默认构造函数

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

    Sales_item myObj;

或者是:

    Sales_item myObj = Sales_item();

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

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

四、隐式类类型转换

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

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

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

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

    string null_book("9-999-99999-9");
    item.same_isbn(null_book);
    item.same_isbn(cin);

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

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


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

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

    explicit Sales_item(const std::string &book):
        isbn(book),units_sold(0),revenue(0) {}
    explicit Sales_item(istream &in);

调用:

    item.same_isbn(null_book);	//Error
    item.same_isbn(cin);			//Error

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

//Error
explicit Sales_item::Sales_item(istream &is)    
{
    is >> *this;
}

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

    item.same_isbn(Sales_item(null_book));
    item.same_isbn(Sales_item(cin));

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

【最佳实践】

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

//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的类,可以采用与初始化数组元素相同的方式初始化其成员:

struct Data
{
    int ival;
    char *ptr;
};

int main()
{
    Data val1 = {0,0};
    Data val2 = {1,"Hello World"};
}

缺点:

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

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

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

【最佳实践】

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

    //P396 习题12.31
    //不能通过编译,为什么?
    pair<int,int> p = {0,2};
    /*
    *因为pair类型定义了构造函数
    *所以尽管其数据成员为public,但还是不能显式的初始化
    */