7.5 构造函数再探
7.5 构造函数再探
7.5.1 构造函数初始值列表
- 使用构造函数初始值列表显式地初始化成员
Sales_data: :Sales_data(const string &s,
unsigned cnt, double price)
: bookNo(s)
, units_sold(cnt)
, revenue(cnt * price)
{
}
- 对数据成员进行赋值操作
// Sales_data构造函数的一种写法,虽然合法但比较草率:没有使用构造函数初始值
Sales_data: :Sales_data(const string &s,
unsigned cnt, double price)
{
bookNo = s;
units_sold = cnt;
revenue = cnt * price;
}
以上两段代码定义效果是相同的:当构造函数完成后, 数据成员的值相同。区别是使用初始值列表的版本初始化了它的数据成员,而赋值版本是对数据成员执行了赋值操作。这一区别到j自己会有什么深层次的影响完全依赖于数据成员的类型。
构造函数的初始值有时必不可少
- 如果成员是const或引用, 必须将其初始化。
class ConstRef {
public:
ConstRef(int ii);
private:
int i;
const int ci; //和其他常量对象或者引用一样,成员ci和ri都必须被初始化
int &ri;
};
// 构造函数的正确形式
// 初始化const或者引用类型的数据成员的唯一机会就是通过构造函数初始值
ConstRef::ConstRef(int ii): i(ii), ci(ii), ri(i) {}
// 构造函数的错误形式
ConstRef::ConstRef(int ii)
{ // 随着构造函数体一开-始执行,初始化就完成了
i= ii; // 正确
ci = ii; // 错误:不能给const赋值
ri = i; // 错误:ri没有被初始化
}
- 当成员属于某种类类型且该类没有定义默认构造函数时,也必须将这个成员初始化。
成员初始化的顺序
成员的初始化顺序与它们在类定义中的出现顺序一致,构造函数初始值列表中初始值的前后位置关系不会影响实际的初始化顺序。
一般来说,初始化的顺序没什么特别要求。但是如果一个成员是用另一个成员来初始化的,那么这两个成员的初始化顺序就很关键了
class X {
int i;
int j;
public:
// 未定义的:i在j之前被初始化
X(int val): j (val), i(j) {}
// 最好用构造函数的参数作为成员的初始值,而尽量避免使用同一个对象的其它成员
X(int val): j (val), i(val) {}
};
默认实参和构造函数
如果一个构造函数为所有参数都提供了默认参数,则它实际上也定义了默认构造函数。
class Sales_data {
public:
// 定义默认构造函数,令其与只接受一个string实参的构造函数差不多
Sales_data(std::string s = "") : bookNo(s) {}
};
7.5.2 委托构造函数
一个委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程。在委托构造函数内,成员初始值列表只有一个,就是受其委托的构造函数。
class Sales_data {
public:
// 非委托构造函数使用对应的实参初始化成员
Sales_data(std::string s, unsigned cnt, double price) : bookNo(s), units_sold, revenue(cnt * price) {}
// 其余构造函数全都委托给另一个构造函数
Sales_data() : Sales_data("", 0, 0) {}
Sales_data(std::string s): Sales_data(s, 0, 0) {}
Sales data(std::istream &is): Sales data() { read(is, *this); } // 委托再委托
};
当一个构造函数委托给另一个构造函数时,受委托的构造函数的初始值列表和函数体被依次执行。执行完成后,控制权才会交还给委托者的函数体。
7.5.3 默认构造函数的作用
当对象被默认初始化或值初始化时自动执行默认构造函数。
默认初始化在以下情况下发生:
- 当我们在块作用域内不使用任何初始值定义一个非静态变量或者数组时;
int main()
{
int local_int; // 默认初始化
int local_ints[10]; // 默认初始化
}
-
当一个类本身含有类类型的成员且使用合成的默认构造函数时;
-
当类类型的成员没有在构造函数初始值列表中显式地初始化时。
值初始化在以下情况下发生:
- 在数组初始化的过程中,如果我们提供的初始值数量小于数组的大小时;
- 当我们不使用初始值定义一个局部静态变量时;
size_t count_calls()
{
// 局部静态变量,第一次经过对象定义语句时初始化,直到程序终止才销毁
static size_t ctr; // 执行值初始化,内置类型的局部静态变量初始化为0;
return ++ctr;
}
- 当我们通过书写形式如T()的表达式显式地请求值初始化时;
class A {
public:
unsigned count;
A() = default;
};
int main() {
A a1; // 默认初始化
A a2 = A(); // 显式请求值初始化
}
- vector只接受一个实参用于说明vector大小的构造函数;
vector<int> ivec(10); // 10个元素,值初始化为0
如果类的某些数据成员缺少默认构造函数
class NoDefault {
public:
NoDefault(const std::string&);
};
struct A {
NoDefault my_mem;
};
A a; // 错误:不能为A合成构造函数
struct B {
B() {} // 错误:b_member没有初始值
NoDefault b_member;
}
使用默认构造函数
Sales_data obj(); // 定义了一个函数,而非对象
Sales_data obj; // 定义了一个对象,执行默认初始化
7.5.4 隐式的类类型转换
如果构造函数只接受一个实参,则它实际上定义了转换为此类型的隐式转换机制。
class Sales_data {
public:
Sales_data& combine(const Sales_data&);
Sales_data() {}
Sales_data(const std::string &s): bookNo(s) {}
};
int main()
{
Sales_data item;
string null_book = "9-999-99999-9";
item.combine(null_book); // 构造一个临时的Sales_data对象,将这个对象传递给combine
}
只允许一步类类型转换
编译器只会自动地执行一步隐式类类型转换。
// 错误:需要用户定义两种转换
// (1) 把"9-999-99999-9"转换成string
// (2) 再把这个(临时的)string转换成Sales_data
item.combine("9-999-99999-9");
// 正确:显示转换成string,隐式转换成Sales_data
item.combine(string("9-999-99999-9"));
// 正确:隐式转换成string,显示转换成Sales_data
item.combine(Sales_data("9-999-99999-9"));
抑制构造函数定义的隐式转换
我们可以通过将构造函数声明为explicit加以阻止:
class Sales_data {
public:
Sales_data() = default;
Sales_data(std::string s, unsigned cnt, double price) : bookNo(s), units_sold, revenue(cnt * price) {}
explicit Sales_data(const std::string &s): bookNo(s) {}
explicit Sales data(std::istream &is);
};
item.combine(null_book); // 错误:string构造函数是explicit
item.combine(cin); // 错误:istream构造函数是explicit
关键字explicit只对一个实参的构造函数有效。且只能在类内声明构造函数时使用explicit关键字,在类外部定义时不应重复。
explicit构造函数只能用于直接初始化
发生隐式转换的一种情况是当我们执行拷贝形式的初始化时(使用=)。
class Sales_data {
public:
Sales_data() = default;
explicit Sales_data(const std::string &s): bookNo(s) {}
};
Sales_data item1(null_book); // 正确:直接初始化
Sales_data item2 = null_book; // 错误:不能将explicit构造函数用于拷贝形式的初始化过程
为转换显示地使用构造函数
尽管编译器不会将explicit的构造函数用于隐式转换过程,但是我们可以使用这样的构造函数显示地强制转换:
class Sales_data {
public:
Sales_data() = default;
explicit Sales_data(const std::string &s): bookNo(s) {}
explicit Sales data(std::istream &is);
};
// 正确:直接使用Sales_data的构造函数
item.combine(c(null_book));
// 正确:使用static_cast执行显示转化
item.combine(static_cast<Sales_data>(cin));
标准库中含有显示构造函数的类
- 接受一个单参数的const char*的string构造函数不是explicit的;
- 接受一个容量参数的vector构造函数是explicit的.
7.5.5 聚合类
当一个类满足如下条件时,我们说它是聚合的:
- 所有成员都是public;
- 没有定义任何的构造函数;
- 没有类内初始值;
- 没有基类,也没有virtual函数。
struct Data {
int ival;
string s;
};
// 我们可以提供一个花括号括起来的初始值列表,用它初始化聚合类的数据成员
Data vall = {0, "Anna"}; // 初始值的顺序必须与声明的数据一致
与初始化数组元素的规则一样,如果初始值列表中的元素个数少于类内的成员数量,则靠后的成员被值初始化。初始值列表的元素个数绝对不能超过类的成员数量。
7.5.6 字面值常量类
数据成员都是字面值类型的聚合类时字面值常量类。
如果一个类不是聚合类,但它符合下述要求,则它也是一个字面值常量类:
- 数据成员都必须是字面值类型;
- 类必须包含一个constexpr构造函数;
- 如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式;或者如果成员属于某种类类型, 则初始值必须使用成员自己的constexpr构造函数。
- 类必须使用析构函数的默认定义,该成员负责销毁类的对象
constexpr构造函数
constexpr构造函数可以声明成=default 的形式(或者是删除函数的形式)。否则,constexpr 构造函数就必须既符合构造函数的要求(意味着不能包含返回语句),又要符合constexpr函数的要求(意味着它能拥有的唯一可执行语句就是返回语句)。综合过两点可知,constexpr构造函数体一般来说应该是空的。
class Debug {
public:
// 我们通过前置关键字constexpr就可以声明一个constexpr构造函数
// constexpr构造函数必须初始化所有数据成员,初始值或者使用constexpr构造函数,或者是一条常量表达式
constexpr Debug(bool b =true): hw(b), io(b), other(b) { }
constexpr Debug(bool h, bool i, bool o): hw(h), io(i),ther(o) { }
constexpr bool any () { return hw I I工o I I other; }
void set io (bool b) { io = b; }
void set hw (bool b) { hw = b; }
void set other(bool b) { hw = b; }
private:
bool hw;
bool io;
bool other;
};
constexpr构造函数用于生成constexpr对象以及constexpr函数的参数或返回类型:
constexpr Debug io_sub(false,false,false);
if(io_sub.any())
cerr << "print appropriate error messages" << endl;
constexpr Debug prod(false);
if(prod.any())
cerr << "print an error messages" << endl;
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!