C++ Primer 第七章⑤

C++ Primer

第七章 类

构造函数再探

特殊的成员变量

构造函数的初始值有时候必不可少,什么时候必不可少呢?当成员变量是const或者引用的时候,原因么,你懂的:

class ConstRef
{
public:
    ConstRef(int ii);
private:
    int i;
    const int ci;
    int &ri;
};

这样去构造一个类的时候很容易犯错,比如你写这样一个构造函数:

ConstRef::ConstRef(int ii)
{
    i = ii; //正确
    ci = ii; //错误:不能给const赋值
    ri = i; //错误:引用没有初始化
}

所以说啊:我们初始化const或者引用类型的数据成员的唯一机会就是通过构造函数初始值,于是我们可以这么写:

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

书上有一句话可以称为金玉良言:如果成员是const、引用或者属于某种未提供默认构造函数的类类型,我们必须通过构造函数初始值列表为这些成员提供初值。

成员的初始化顺序与它们在类定义中的出现顺序一致,而与初始化列表无关,这句话经常考,什么意思呢,看例子:

class X
{
    int i;
    int j;
public:
    X(int val) : j(val), i(j){}
};

这样是错的,对吧?因为i和j的初始化顺序是按照定义来的,先有i后有j,所以,你怎么能用j去初始化i呢?
最好的方式是用构造函数传进来的参数去初始化成员,这样我们就不用考虑初始化顺序了:

X(int val) : i(val), j(val){}
默认实参和构造函数

通常,我们说默认构造函数是没有参数的,但我们也可以这么干:

class Sales_data
{
public:
    Sales_data(string s = "") : bookNo(s){}
}

你可能觉得这个函数是一个普通的构造函数,但是,其实它是这个类的默认构造函数,它用到了默认实参,这么写的好处是,你可以不传参数,也能调用(不就相当于默认构造函数了吗),你不传参数的时候,它就用空字符串s去初始化bookNo,传的话就按照传的来,一式两用,还是很不错的。

委托构造函数

这个偷懒方法也是丧心病狂,我给个例子吧,就以Sales_data类为例:

class Sales_data
{
public:
    //这是函数一,是一个普通的构造函数
    Sales_data(string s, unsigned cnt, double price) : bookNo(s), units_sold(cnt), revenue(cnt*price){}

    //接下来就是各种偷懒方法了,注意看
    Sales_data(): Sales_data("", 0, 0){} //函数二是默认构造函数,委托函数一帮忙初始化,也可以认为是调用了函数一
    Sales_data(string s): Sales_data(s, 0, 0){} //函数三接受一个string参数,委托函数一帮忙初始化
    Sales_data(istream &is): Sales_data()
    {
        read(is, *this);
    }
    //函数四复杂些,它先委托函数二,就是默认构造函数,函数二去委托函数一,这些函数执行完成后,再执行函数四的函数体
    //调用read函数读取给定的istream
};

默认构造函数的作用

记住一条好习惯:如果定义了其他的构造函数,最好也提供一个默认构造函数,下面来两个没有默认构造函数的经典错误:

class NoDefault
{
public:
    NoDefault(const string &);
    //定义了构造函数,但没有默认构造函数,而且编译器不会生成,这样这个类本身不会有问题,但是用起来很容易出问题
};

struct A
{
    NoDefault a;
};
A a; //你这样看着挺正常,A没有写构造函数,于是编译器会自动生成默认构造函数去初始化成员变量,可惜啊,这个成员变量的
//类型是类类型NoDefault,而这个该死的类类型又没有默认构造函数,还不让编译器自己生成,于是就报错了

//还有一种错误是这样的:
struct B
{
    B(){}
    NoDefault b;
    //b作为类类型成员,在构造函数没有被初始化,于是它会调用默认构造函数初始化,结果该死的NoDefault没有默认构造函数
};
使用默认构造函数
Sales_data obj; //注意不要加括号啊,加了括号就成了函数声明了

隐式的类类型转换

如果构造函数只接受一个实参,那它实际上定义了一种隐式转换机制,什么意思呢,以Sales_data为例,该类有一个只接受一个string的构造函数,所以啊,我们可以把string转换为Sales_data类的对象:

Sales_data b;
string a = "1";
b.combine(a); //这里a被转换为Sales_data对象,编译器创建了一个临时对象

这种只带一个参数的构造函数,我们也把它称为转换构造函数。

但是我们只允许一步类类型转换,举个例子:

item.combine("1");
//这样是错的,因为字符串常量到string是一步,string到类对象时一步,两步不行的
//不过我们可以用显式地写
item.combine(string("1"));
item.combine(Sales_data("1"))
//总之,隐式的只能帮你一步
抑制在构造函数中的隐式转换

这个又是C++牛逼也蛋疼的地方,提出了类类型隐式转换,我又可以阻止它,不得不说给了程序员极大的权力,也是需要极大的责任心的,在转换构造函数(只接受一个实参的构造函数)前加explicit关键字就可以阻止隐式转换,注意,explicit关键字只对类内的转换构造函数有用,而且被声明为explicit的构造函数只能用于直接初始化,不能拷贝初始化:

class Sales_data
{
public:
    Sales_data() = default;
    explicit Sales_data(const string &s): bookNo(s){}
    explicit Sales_data(istream&);
};

Sales_data item;
string b = "1"
item.combine(b); //这样就不行了
explicit Sales_data::Sales_data(istream& is){} //这样也不行,在外面了
Sales_data item1(b); //这样可以
Sales_data item2 = item1; //不行,explicit声明的不能拷贝初始化

//但是,多事C++又说我们还是可以强行转换:
item.combine(Sales_data(b)); //可以
item.combine(static_cats<Sales_data>(cin)); //可以

下面介绍两个特殊的类,也不太常用。

聚合类

聚合类是一个概念,使得用户可以直接访问其成员,它有特殊的初始化语法,那么什么样的类是聚合类呢?满足下面四个条件:

  1. 所有成员都是public
  2. 没有定义任何构造函数
  3. 没有类内初始值
  4. 没有基类,也没有virtual函数(以后介绍) 聚合类举例:
    struct Data
    {
     int val;
     string s;
    };
    //实例化
    Data a = {0, "a"}; //一定要按类内定义的顺序
    

字面值常量类

之前我们学过,constexpr函数的参数和返回类型都得是字面值类型。这回说到了类,就有字面值常量类: 数据成员都是字面值类型的聚合类是字面值常量类
或者
满足以下条件:

  1. 数据成员均为字面值类型
  2. 类至少有一个constexpr构造函数
  3. 有类内初始值的话,该值必须是常量表达式,即便该成员是类类型,这个类也要有自己的constexpr构造函数
  4. 类必须使用析构函数的默认定义,该成员负责销毁对象
constexpr构造函数

之前提过,构造函数不能是const的,但是字面值常量类的构造函数可以是constexpr的,且必须至少有一个constexpr构造函数。 如果不是默认的构造函数,那么constexpr类的构造函数基本就是空函数体,因为以下两点:

  1. constexpr函数的要求是唯一可执行语句时return语句(之前说过的规定,忘了自己回去翻)
  2. 构造函数不能包含返回语句 所以就只好函数体为空了。 constexpr构造函数必须初始化所有数据成员,初始值或者使用constexpr构造函数或者是一条常量表达式。 说了那么多,来举个字面值常量类的例子吧:
class Debug
{
public:
    //带有一个默认实参,它也是默认构造函数哦
    constexpr Debug(bool b = true): hw(b), io(b), other(b){}

private:
    bool hw;
    bool io;
    bool other;
};
#C++工程师#
全部评论
class X { int i; int j; public: X(int val) : j(val), i(j){} }; 这样是错的,对吧? 这个应该不是错的,亲测可以的,只不过 i 的值是个垃圾值,语法上是没问题的
点赞 回复 分享
发布于 2017-01-19 17:50
👍👍👍
点赞 回复 分享
发布于 2016-12-12 20:28
点赞 回复 分享
发布于 2017-01-19 17:56
c++真的好烦啊,最后explicit开始就觉得烦了,这个实际常用吗
点赞 回复 分享
发布于 2019-09-13 22:06

相关推荐

11-01 08:48
门头沟学院 C++
伤心的候选人在吵架:佬你不要的,能不能拿户口本证明过户给我。。球球了
点赞 评论 收藏
分享
牛客963010790号:为什么还要收藏
点赞 评论 收藏
分享
6 3 评论
分享
牛客网
牛客企业服务