7.5构造函数再探
7.5.1 构造函数初始值列表
这一节验证了我之前的猜想,我们可以在初始值列表中对数据成员初始化,也可以在函数体中对数据成员“初始化”,但是在函数体中的“初始化”实际是赋值,如果没有初始值列表,数据成员会执行一次默认初始化。
大部分时候这两者是差不多的,但是有时,我们必须在初始值列表中进行初始化,而不能在函数体中赋值。
比如
class A{
const int a;
int &b;
int c;
};
上面类中的a和b必须在初始值列表中进行初始化。
这和平时接触的常量和引用必须初始化没有什么却别。
还有一种情况是,类中包含了其他的类的对象,而这个类没有默认构造函数。这种情况下也必须使用初始值列表进行初始化。
注意
我们在初始值列表中定义的数据成员初始化顺序和数据成员实际的初始化顺序是无关的。
数据成员在初始值列表中实际的初始化顺序是在类中的定义顺序。
class A{
private:
int a;
int b;
int c;
public:
//数据成员初始化的顺序和其在初始值列表中的顺序并没有关系
A(int _a,int _b,int _c):c(_c),a(_a),b(_b){};
};
但是我们最好是将初始值列表中的顺序和数据成员声明的顺序写成一样的,防止出现一些莫名奇妙的错误。
比如使用一个数据成员,初始化另一个数据成员。
//使用c来初始化a,但是其实a最先执行,而c目前的值是未定义的。这样可能会导致莫名其妙的错误
A(int _a,int _b,int _c):c(_c),a(c),b(_b){};
所以我们在平时写代码的时候,将初始值列表的初始化顺序和数据成员的声明顺序保持一致。
且不再初始值列表中,使用一个数据成员来初始化另一个数据成员。
关于默认实参和构造函数
如果我们把一个构造函数的形参列表全都写了默认实参,那么调用的时候,我们可以实参都不传入。
练习
7.36
数据成员先声明rem,在声明base,但是在初始值列表中,使用了base的值来初始化rem,这样会导致rem的值未定义。
int base,rem;
7.37
都是调用的第1个。
"" 0 0
"9-999-99999-9" 0 0
这个两个符号在实际输出中是没有的,因为第一个输出的字符串为空,为了看的清楚一些,我就这样写了。
7.38
Sales_data(istream& temp_input = cin)
7.39
逻辑上是合法的,但是从设计上考虑是不对的,因为我们使用了istream&作为输入,肯定是想使用输入流为对象赋值,而不是使用默认的值
7.40
class Date {
private:
int year;
int month;
int day;
int minuter=0;
int second=0;
public:
//只考虑年月日
Date(int _year, int _month, int _day) : year(_year), month(_month), day(_day) {};
//考虑年月日分秒
Date(int _year, int _month, int _day, int _minuter, int _second) :year(_year), month(_month), day(_day) ,minuter(_minuter),second(_second){};
//考虑从输入流中获取日期
Date(istream& temp_input);
//考虑从字符串中获取日期
Date(const string& date_string);
};
7.5.3 委托构造函数
这是C++11的新标准,委托构造函数,顾名思义,就是一个构造函数让另一个构造函数来帮助自己完成数据成员的初始化,委托构造函数是对构造函数初始值列表的拓展。
即以下的方式
class A{
public:
A(int _a,int _b,int _c):a(_a),b(_b),c(_c){};
//委托第一个构造函数
A():A(0,0,0){};
//委托第二个构造函数
A(istream&)A(){};
private:
int a;
int b;
int c;
}
委托构造函数可以让另一个构造函数把它完成初始化的任务,所以受委托的函数会被执行,等受委托的函数执行完之后,再执行委托构造函数。
*为什么需要委托构造函数?我的猜想是为了提高代码的复用率,因为在写构造函数的初始值列表的时候,如果构造函数有很多个,一个一个的写初始值列表比较繁琐,所以就使用委托构造函数来替其他的构造函数执行初始化任务
*
练习
struct Sales_data
{
friend istream& input(istream& temp_input, Sales_data& data);
friend ostream& print(ostream& temp_cout, const Sales_data& data);
friend Sales_data add(Sales_data &data1, const Sales_data & data2);
private:
string bookNo;
unsigned units_sold = 0;
double revenue = { 0 };
public:
string isbn() const { return bookNo; };
Sales_data& combine(const Sales_data&);
inline double avg_price()const;
Sales_data(const string& isbn, unsigned u, double p) :bookNo(isbn), units_sold(u), revenue(u*p) {
cout << "三参数构造函数" << endl;
};
//构造函数
//1.委托构造函数
Sales_data() :Sales_data("", 0, 0) {
cout<<"空参数构造函数"<<endl;
};
//2.仅初始化isbn
Sales_data(const string& isbn) : Sales_data(isbn,0,0){
cout << "isbn 构造函数" << endl;
};
//3.使用流来初始化
Sales_data(istream& temp_input):Sales_data(){
input(temp_input, *this);
};
};
测试用例
/*Sales_data data1;
Sales_data data2("10086");
Sales_data date3("10086",1,1);
Sales_data date4(cin);*/
这是输出的值
7.42
class Date {
private:
int year;
int month;
int day;
int minuter=0;
int second=0;
public:
Date(int _year, int _month, int _day, int _minuter, int _second) :year(_year), month(_month), day(_day), minuter(_minuter), second(_second) {};
Date(int _year, int _month, int _day) : Date(_year,_month,_day,0,0) {};
Date(istream& temp_input):Date(0, 0, 0, 0, 0) {};
Date(const string& date_string);
};
7.5.3 默认构造函数的作用
在了解默认构造函数的作用时,首先先了解一下,默认初始化和值初始化。
内置类型的默认初始化的值,通常都是位置。
内置类型的值初始化通常为0
默认初始化发生在
1.不使用用初始值定义非静态变量或者数组时
2.一个类包含另一个类的类型且使用编译器合成的默认构造函数时
3.在构造函数的初始值列表中,没有显式初始化
什么执行值初始化
1.数组初始化的过程中,提供的值小于数组的维度
2.不使用初始值定义一个局部静态变量
3.
第三点我暂时还没有理解,但是需要知道的时,当容器vector使用一个值进行初始化值,vector vec(10);vec会执行值初始化。
默认初始化和值初始化都要用到默认构造函数。
所以我们在定义构造函数的时候,记得构造一个默认构造函数。
即,我们需要定义一个不需要传入任何实参也可以对数据成员进行初始化的函数。
想要使用默认构造函数,在定义类的变量时,我们给变量名后面什么都不加就可以了。
class A{
public:
A(){};
A(int _a):a(_a){};
private:
int a;
}
A a;//调用默认构造函数
A a();这是在声明一个名字为a的函数,返回值类型为A
练习
7.43
class NoDefault {
public:
NoDefault(int i) {};
};
class C {
public:
C(int n):no_default(n) {};
C() :no_default(0){};
private:
NoDefault no_default;
};
7.44
不合法,因为NoDefault没有默认构造函数
7.45
合法
7.46
a。不正确,不提供的化,编译器会生成一个默认构造函数
b。不正确,因为如果形参列表的所有形参都有默认实参,这样调用也不用传入任何实参。
c。正确,因为不存在有意义的默认值,那么就一定会为他赋值,那么我们没有必要提供默认构造函数
d。不确定,生成的默认构造函数为数据成员的初始化通常是未定义不可预知的。
7.5.4 隐式的类类型转换
如果构造函数只接受一个实参,那么这个构造函数其实定义了该类型,转换为类类型的隐式转化机制。
即,如果形参列表只有一个形参,那么这个形参可以隐式的转化为类类型
struct A {
A(int i) {};
//可以隐式转换
//void combine(A a) { cout << "A a" << endl; };
//可以隐式转换
//void combine(const A a) { cout << "const A a" << endl; };
//不可以隐式转换
void combine(A& a) { cout << "A& a" << endl; };
//可以隐式转换
//void combine(const A& a) { cout << "const A& a" << endl; };
int a;
int b;
int c;
};
测试用例
A a(1);
int i = 1;
a.combine(i);
经过验证,发现调用的函数为非常量引用时,是会报错的,无法进行隐式转换。
注意这里说的是调用的函数,不是构造函数。
书上说,
隐式转换只允许一步类类型转换,即将只有一个形参的构造函数的类型,转化为类类型。
但是我发现内置的数值类型,依旧可以转换。
测试用例如下
A a(1);
int i = 1;
a.combine(123.3);
这里传入的实参为double类型。double需要转化为int类型,int类型再转化为A类型,但是这个测试用例在vs2017中式通过的。
但是自定义的类型就不行了
struct A {
A(string i) {};
void combine(const A& a) { cout << "const A& a" << endl; };
};
测试用例:
A a("123");
a.combine("123");//报错
struct B
{
B(int i) {};
};
struct A {
A(B i) {};
void combine(const A& a) { cout << "const A& a" << endl; };
};
测试用例
B b1(1);
A a(b1);
//通过,一次转换
B b2(2);
a.combine(b2);
//需要两次转换,错误
a.combine(123);
所以我的结论是除了数值类型,自定义类型的在构造函数中的隐式转换只能转化一次。
这种隐式转化,我认为大部分时候都是存在风险的,有可能让人检查不到错误出现的地方。
所以C++提供了explicit关键字,修饰构造函数,让构造函数无法进行隐式转换。
struct A {
A(string i) {};
explicit void combine(const A& a) { cout << "const A& a" << endl; };
};
explicit仅对一个实参的构造函数有效,但是你也可以将有多个实参的构造函数上使用explicit关键字,编译器不会报错,不过这样做并没有什么意义,因为多个实参的构造函数并不会发生隐式转换。
explicit仅能修饰构造函数
explicit只能在类的内部使用,不能再类外部使用。
因为存在隐式转换,我们也可以使用强制类型转换,把只有一个实参的构造函数的类型,转化为类类型。即使这个构造函数已经被声明为explicit也没用。
struct A {
explicit A(int i) {};
void combine(const A& a) { cout << "const A& a" << endl; };
};
测试用例
A a(1);
a.combine(static_cast<A>(1));
标准库的string类型就可以将C风格字符串转换为string类型
练习
7.47
我觉得不应该,这样做的好处是,代码更加的灵活,但是缺点是,可读性变差了,维护起来的难度也要变得更大。
7.48
如果不是explicit的话,则都能执行成功。
如果是explicti的话,也都可以执行成功。
因为这里唯一涉及的隐式转换是把C风格字符串转化为string类型,和Sales_date没有关系
7.49
a和c都是合法的
b是违法的
7.50
explicit Date(istream& temp_input):Date(0, 0, 0, 0, 0) {};
explicit Date(const string& date_string);
7.51
因为C风格字符串和string类型的本质上都表示字符串,而且C风格字符串使用得很频繁,不把构造函数定义为explicit可以灵活的将C风格字符串变成string字符串。
但是vector把单参数得构造函数声明为explicit可以有效得提高可读性,因为vector本身是一个列表,如果用一个值能转化为列表,这让代码得可读性变得非常差。
7.5.5 聚合类
如果一个类满足一下条件,那么我们说这个类是聚合的
1.所有的成员都是public
2.没有定义任何构造函数
3.没有类内初始值
4.没有基类,没有virtual函数
struct A{
int a;
string n;
};
测试用例
A a = {0,"123"};
聚合类可以使用花括号括起来的初始值列表进行初始化。但是初始值列表中值的顺序必须和类中数据成员声明的顺序是一样的。
如果初始化列表中的值的数量少于类中数据成员的数量,那么剩余的数据成员执行值初始化。
聚合类存在着一定的缺点
1.所有的成员都必须是public,这表示没有了封装
2.初始化交给了用户,但是用户可能会提供错误的初始值
3.删除掉一个成员之后,所有的初始化语句都要更新
练习
7.52
2.6.1中的Sales_date长这个样子:
struct Sales_data{
std::string bookNo;
unsigned units_sold =0;
double revenue = 0.0;
};
很显然,它包含了类内初始值,所以它不是聚合类。
修改成下面这样就可以了
struct Sales_data{
std::string bookNo;
unsigned units_sold ;
double revenue;
};
7.5.6 字面值常量类
这部分的内容没太看懂。
首先复习一个constexpr函数。constexpr函数要求参数和返回值都是字面值常量。且其中唯一可以操作的语句为return语句。
同时constexpr函数允许返回非常量表达式的值 。
那么什么是字面值常量类。
C++ Primer中说到。如果聚合类的所有数据成员都是字面值类型则它是字面值常量类。
什么是字面值类型。
123
“123”
123L
这些是字面值常量,字面值常量都对应着一种字面值类型,比如,int,char型数组,long参见C++ Primer的2.1.3节
但是如果一个类不是聚合类,那么怎么才算是字面值常量类呢?
1.数据成员都必须是字面值类型
2.类至少还有一个conexpr构造函数
3.如果数据成员有类内初始值,则类内初始值必须是常量表达式。如果是某种自定义类型的变量,则初始值必须使用自己的constexpr构造函数
4.类必须使用默认的析构函数
constexpr构造函数,要求构造函数的函数体内容为空。且必须初始化所有的数据成员,数组成员的初始值为常量表达式。
struct A {
constexpr A(int _a, bool _b) :a(_a), b(_b) {};
private:
int a;
bool b;
};
//初始化时,必须全为常量表达式
A a(123, true);
如果我们想要生成一个constexpr对象,就必须使用constexpr构造函数。
struct A {
explicit A(int i) {};
//constexpr A() {};
void combine(const A& a) { cout << "const A& a" << endl; };
};
测试用例
constexpr A a(i);//报错
struct A {
//explicit A(int i) {};
constexpr A() {};
void combine(const A& a) { cout << "const A& a" << endl; };
};
测试用例
constexpr A a;//通过
练习
7.53
这一题就不做了,暂时觉得字面值常量类用处不大
7.54
不应该,因为set_开头的成员没有返回值,声明为constexpr没有意义
7.55
不是,书上说,数据成员都是字面值类型的聚合类才是字面值常量类,注入123,‘1’,123L这样子的字面值,在C++中都有对应的内置类型。Data中包含了string类,他没有对应的字面值类型。