C++构造函数
构造函数初始化列表
构造函数格式
ClassName(形参列表)初始化列表{
函数体
}
区分初始化与赋值的概念
初始化和赋值是两个不同的概念,两者之间会涉及到底层效率的问题
int a = 10; // 这是对整型变量a进行初始化,就算后面没有=10,a还是会被初始化,初始值为0
int b; // 初始化整型变量b
b = 20; // 这是赋值操作
从上面的代码可以看到,初始化和赋值完全是两个概念,前者直接初始化数据成员,后者是先初始化再赋值,多了一个步骤,所以coding的过程中,小的细节还是需要注意!
#include<iostream>
using namespace std;
int main(int argc, char *argv[]){
auto start1 = clock();
for(int i = 0;i<1000000000;++i){
int j; // 初始化再赋值
j = 10;
}
auto end1 = clock();
cout<<"Time1: "<<(double)(end1 - start1)/CLOCKS_PER_SEC<<endl;
auto start2 = clock();
for(int i = 0;i<1000000000;++i){
int j = 10; // 直接初始化
}
auto end2 = clock();
cout<<"Time2: "<<(double)(end2 - start2)/CLOCKS_PER_SEC<<endl;
return 0;
}
Time1: 2.681
Time2: 2.542
两者相比,性能上还是有差异的,虽然是零点几秒的差异,但是对于计算机而言已经是一个很大的量级了,所以对于追求极致的coder,不仅得从大的整体上宏观出发,小的细节上也需要注意!!
由此引入构造函数的初始化列表,其引入原因有两方面的因素
- 由于初始化与赋值还是有一定的性能差异的,所以在对象构造的过程中,使用初始化列表会优化程序的运行速度
- 对于某些属性,比如const常量和引用,或者一些只能被初始化的对象,考虑到其特性以及其最后初始化时机,使用初始化列表就极其重要,而且是必须初始化,而不是赋值,这也是主要原因
其格式为
ClassName(形参列表):属性(初始化值),属性(初始化值),...{
函数体
}
class ConstRef{
public:
ConstRef(int ii):ci(ii),ri(ii),i(ii){}
private:
int i;
const int ci;
int &ri;
};
需要注意的是,其初始化顺序是按照变量定义顺序来的,而不是按照初始化列表中的顺序来初始化的,比如说上面的代码中,i先被初始化,然后是ci,最后是ri,虽然他们初始化列表顺序不是这样的,当然类型中的属性不仅可以通过传入参数进行初始化,也可以通过已初始化的参数进行初始化,比如
ConstRef(int ii):i(ii),ci(i),ri(i){}
但是**强烈不建议这样做!!!**因为初始化顺序的缘故很容易搞错,比如下面
ConstRef(int ii):ci(ii),i(ci),ri(i){}
就会因为i先初始化,它依靠ci初始化,但是在i初始化时,ci并未初始化而导致错误,当然如果编译器做的好的话这样的情况编译器会巧妙处理,关于这样就有以下几点注意
- 构造函数的初始化列表尽量与属性列表保持一致
- 尽量避免使用某些属性成员去初始化其它的属性成员,如果非必要的话
总结出的一点就是,别给自己找麻烦!!
委托构造函数
委托构造函数就是把构造函数自己的一些功能委托给其它的构造函数执行,其格式为
class ClassName{
public/private:
构造函数1(列表1){函数体}
构造函数2(列表2):构造函数1(参数){....}
private/public:
列表...
};
示例
#include<iostream>
#include<string>
using namespace std;
enum VICH{
CAR,
PLANE,
SUBWAY,
TRAIN
};
enum SPEED{
SLOW,
MID,
FAST,
};
class Vehicle{
public:
Vehicle(VICH vich, SPEED speed):vich_style(vich),speed(speed){
cout<<"construct init by itself"<<endl; // 1
}
Vehicle(): Vehicle(CAR, SLOW){
cout<<"construct init the entrust construct"<<endl; // 2
}
private:
VICH vich_style;
SPEED speed;
};
int main(int argc, char *argv[]){
Vehicle v(PLANE, FAST);
cout<<"======="<<endl;
Vehicle v1; // 借助委托构造函数构造
return 0;
}
construct init by itself
=======
construct init by itself
construct init the entrust construct
委托构造函数的理解就是一个构造函数可以借助类中其它已经定义的构造函数去实现构造,如果委托构造函数里面有语句块,那么这个语句块会在被委托构造函数的语句块之前运行,即先输出1再输出2
隐式的类类型转换
C++类中也有隐式的类型转换规则,如果一个类的构造函数只有一个参数,那么C++就默认为这个类生成了隐式的构造函数,可以看如下代码
#include<iostream>
#include<string>
using namespace std;
class ExpTypeChange{
public:
ExpTypeChange(string msg):my_msg(msg){}
void setExp(ExpTypeChange esp){ // mark 1
my_msg = esp.getExp();
}
void showExp(){
cout<<"MyExp: ["<<my_msg<<"]"<<endl;
}
string getExp(){
return my_msg;
}
private:
string my_msg;
};
int main(int argc, char *argv[]){
ExpTypeChange exp("HelloWorld");
exp.showExp();
string s = "helloworld"; // mark 2
exp.setExp(s); // mark 3
exp.showExp();
return 0;
}
MyExp: [HelloWorld]
MyExp: [helloworld]
可以看到mark1的这个函数提供了一个函数,要求传入的是一个ExpTypeChange对象,而再mark3往函数里面传入的是一个string,但是编译时却没有错误,而且能正常运行,这就是因为C++默认调用了隐式构造函数将传入的s作为一个传入参数,隐式的调用类的构造函数,并将字符串s作为参数传入构造函数,但是它只允许一步的类型转换,即如果将上面代码改成这样
exp.setExp("helloworld"); // 出错,无法将字符字面量直接转换成ExpTypeChange
exp.setExp(string("helloworld")); // 正确,先将字符字面量显式的转换为string,再隐式的调用构造函数
exp.setExp(ExpTypeChange("helloworld"))// 正确,先将字符字面量隐式的转换为string,再显式的调用构造函数
explicit取消隐式转换的声明
如果不想有这种隐式的转换存在,可以在类的构造函数前加上explicit
声明,禁用隐式的构造,即如果这样写
explicit ExpTypeChange(string msg):my_msg(msg){}
那么在编译的时候exp.setExp(s)
就会报错,因为不支持隐式的调用构造函数,所以explicit
声明的构造函数只能直接初始化
使用explicit的优缺点
- 优点:可以防止未名的隐式转换而带来意想不到的错误的发生
- 缺点:当用户真实需要这种隐式的转换存在时,不得不使用显式的声明方式略显繁琐
聚合类
聚合类需要满足
- 所有成员对象都是public的
- 没有定义任何构造函数
- 没有类内初始值
- 没有基类,没有virtual虚函数
聚合类可以通过类似于数组的形式初始化,但是顺序要保持声明顺序一致
struct Data{
int ival;
string s;
};
Data v1 = {100,"Anna"}; // 正确
Data v2 = {"Anna", 100}; // 错误,顺序不对,不能用“Anna”初始化ival,后者同理
初始化列表的长度不能大于类中成员变量的数量,并且得按顺序,并且如果长度小于成员变量的数据是,其缺省部分会被默认初始化,这样就需要保证缺省的那一部分必须有默认的初始化机制,即类必须要有默认的构造函数
struct Data{
int i1;
int i2;
int i3;
};
Data d1 = {1,2}; // i1 = 1, i2 = 2, i3 = 0(默认初始化);
struct Data1{
int i1;
int i2;
xxx e3; // 假设xxx没有默认的构造函数
}
Data d2 = {1, 2}; // 报错,无法对e3进行默认初始化
缺点
- 所有类都是public的
- 添加或删除掉一个成员之后,所有初始化语句都要更新
- 初始化过程用户可见,封闭性不好,并且可读性也不好
字面值常量类
数据成员都是字面值类型的类是聚合类,聚合类是字面值常量类,如果不符合聚合类的条件,但是符合下述条件,那么这个类就是字面值常量类
- 数据成员必须都是字面值类型
- 类中至少包含一个constexpr构造函数
- 类的析构函数必须使用默认定义,该成员负责销毁类的对象
- 如果一个数据成员还有类内初始值,则内置类型成员的初始值必须是一条常量表达式,或者如果成员属于某种类类型,则初始值必须使用成员自己的constexpr构造函数
constexpr声明的构造函数只能有返回语句,不能存在其它语句,并且constexpr构造函数因为没有返回值,所以其不要有代码体,或者说不能有代码体,constexpr构造函数必须初始化所有数据成员
#include<iostream>
using namespace std;
class Debug{
public:
constexpr Debug(bool b = true):hw(b),io(b),other(b){}
constexpr Debug(bool hw, bool io, bool other):hw(hw),io(io),other(other){}
constexpr bool any(){return hw || io || other;}
void set_hw(bool b){hw = b;}
void set_io(bool b){io = b;}
void set_other(bool b){other = b;}
private:
bool hw;
bool io;
bool other;
};
int main(){
constexpr Debug io_sub(false, true, false);
if(io_sub.any()){
cerr<<"It will have error occur!"<<endl;
}
// io_sub.set_hw(false); // 报错,声明为constexpr的对象无法对其对象属性进行修改,因为都为字面值常量
Debug other_sub(false);
if(!other_sub.any()){
cerr<<"other debug gate not open!"<<endl;
}
other_sub.set_other(true);
cout<<"==========\n";
if(other_sub.any()){
cout<<"other debug gate has open!"<<endl;
}
return 0;
}
It will have error occur!
other debug gate not open!
==========
other debug gate has open!
需要注意的是,被constexpr修饰的对象是constexpr对象,其数据类型无法修改