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)); //可以
下面介绍两个特殊的类,也不太常用。
聚合类
聚合类是一个概念,使得用户可以直接访问其成员,它有特殊的初始化语法,那么什么样的类是聚合类呢?满足下面四个条件:
- 所有成员都是public
- 没有定义任何构造函数
- 没有类内初始值
- 没有基类,也没有virtual函数(以后介绍) 聚合类举例:
struct Data { int val; string s; }; //实例化 Data a = {0, "a"}; //一定要按类内定义的顺序
字面值常量类
之前我们学过,constexpr函数的参数和返回类型都得是字面值类型。这回说到了类,就有字面值常量类: 数据成员都是字面值类型的聚合类是字面值常量类
或者
满足以下条件:
- 数据成员均为字面值类型
- 类至少有一个constexpr构造函数
- 有类内初始值的话,该值必须是常量表达式,即便该成员是类类型,这个类也要有自己的constexpr构造函数
- 类必须使用析构函数的默认定义,该成员负责销毁对象
constexpr构造函数
之前提过,构造函数不能是const的,但是字面值常量类的构造函数可以是constexpr的,且必须至少有一个constexpr构造函数。 如果不是默认的构造函数,那么constexpr类的构造函数基本就是空函数体,因为以下两点:
- constexpr函数的要求是唯一可执行语句时return语句(之前说过的规定,忘了自己回去翻)
- 构造函数不能包含返回语句 所以就只好函数体为空了。 constexpr构造函数必须初始化所有数据成员,初始值或者使用constexpr构造函数或者是一条常量表达式。 说了那么多,来举个字面值常量类的例子吧:
class Debug
{
public:
//带有一个默认实参,它也是默认构造函数哦
constexpr Debug(bool b = true): hw(b), io(b), other(b){}
private:
bool hw;
bool io;
bool other;
};
#C++工程师#