👉再谈构造函数👈
构造函数体赋值
在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。
class Date { public: Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; };
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
初始化列表
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个成员变量后面跟一个放在括号中的初始值或表达式。
class Date { public: Date(int year = 1, int month = 1, int day = 1) : _year(year) , _month(month) , _day(day) {} private: int _year; int _month; int _day; };
现在我们大概知道了初始化列表的大致玩法,那我们把之间写的栈Stack也用初始化列表来初始化一下。
class Stack { public: Stack(int capacity = 4) : _top(0) , _capacity(capacity) , _a((int*)malloc(sizeof(int) * capacity)) { if (_a == NULL) { perror("malloc fail"); exit(-1); } } private: int* _a; int _top; int _capacity; };
栈Stack的初始化列表除了可以像上面那样写,初始化列表还可以和函数体内赋初值一起使用,见下方代码:
#include <iostream> using namespace std; class Stack { public: Stack(int capacity = 4) : _top(0) , _capacity(capacity) { _a = (int*)malloc(sizeof(int) * capacity); if (_a == NULL) { perror("malloc fail"); exit(-1); } } private: int* _a; int _top; int _capacity; };
因为初始化列表有些事情做不了,所以可以在构造函数内做。比如:给申请的空间全部赋上初值 0 memset(_a, 0, sizeof(int) * capacity)。
注意:每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)。
以上的例子似乎都无法解释初始化列表为什么要存在,因为似乎初始化列表做的时期,在函数里面也能做。
那什么样的场景一定需要初始化列表,而函数体内赋初值无法完成呢?我们一起来看一下:
当类B的成员变量用const修饰时,如果在函数体内给const修饰的成员变量赋初值,这时候就会出现语法错误。这就相当于修改一个const修饰的只读变量。const关键字修饰的变量在定义的时候必须初始化,而且只能初始化一次。所以这时候就需要借助初始化列表来完成这个工作了。因为对象的实例化是调用构造函数来整体定义的,而对象中的成员变量是在初始化列表中定义初始化的。
#include <iostream> using namespace std; class B { public: B() :_n(10) {} private: const int _n; // const }; int main() { B b; return 0; }
注意:对象的每个成员变量都需要走初始化列表,就算显式在初始化列表写,也会走。 那么如果不在初始化列表中初始化类B的_n,就会报错。因为初始化列表是每个成员变量定义的地方,而const修饰的变量在定义的时候必须初始化且只能初始化一次。
如果你没有显式写初始化列表,也可以用缺省值的方式来解决。因为没有显式写的时候,初始化列表就会用上这个缺省值;如果写了,初始化列表就不会用这个缺省值。
class B { public: B() {} private: const int _n = 1; // const }; int main() { B b; return 0; }
注:如果没有在初始化列表显式初始化,对于内置类型,有缺省值就用缺省值,没有缺省值就用随机值;而对于自定义类型,就会去调用该自定义类型的默认构造函数。如果没有默认构造函数,就会报错。
可以看到,上面的例子就很好地验证了上面的结论。那我们只需要提高类A的默认构造函数就可以解决上面的问题了。
如果显示写了初始化列表,就会用初始化列表的初始化。
class A { public: A(int a = 1) :_a(a) {} private: int _a; }; class B { public: B() : _n(10) , _aa(100) {} private: const int _n = 1; // const A _aa; };
那么,我们来看一下队列MyQueue
的初始化列表。
#include <iostream> using namespace std; class Stack { public: Stack(int capacity = 4) { _a = (int*)malloc(sizeof(int) * capacity); if (_a == nullptr) { perror("malloc fail"); exit(-1); } _top = 0; _capacity = capacity; } // st2(st1) Stack(const Stack& st) { _a = (int*)malloc(sizeof(int) * st._capacity); if (_a == nullptr) { perror("malloc fail"); exit(-1); } memcpy(_a, st._a, sizeof(int) * st._top); _top = st._top; _capacity = st._capacity; } ~Stack() { cout << "~Stack()" << endl; free(_a); _a = nullptr; _top = _capacity = 0; } void Push(int x) { // 扩容 _a[_top++] = x; } private: int* _a; int _top; int _capacity; }; class MyQueue { public: MyQueue() {} void Push(int x) { _pushST.Push(x); } private: Stack _pushST; Stack _popST; int _size; }; int main() { MyQueue q; return 0; }
可以看到,队列MyQueue的初始化列表什么都不写也可以,因为对于自定义类型,会调用该自定义类型的默认构造函数。如果没有提供栈Stack的默认构造函数或者对队列MyQueue的初始空间有要求,就需要写初始化列表了。
#include <iostream> using namespace std; class Stack { public: Stack(int capacity = 4) { _a = (int*)malloc(sizeof(int) * capacity); if (_a == nullptr) { perror("malloc fail"); exit(-1); } _top = 0; _capacity = capacity; } // st2(st1) Stack(const Stack& st) { _a = (int*)malloc(sizeof(int) * st._capacity); if (_a == nullptr) { perror("malloc fail"); exit(-1); } memcpy(_a, st._a, sizeof(int) * st._top); _top = st._top; _capacity = st._capacity; } ~Stack() { cout << "~Stack()" << endl; free(_a); _a = nullptr; _top = _capacity = 0; } void Push(int x) { // 扩容 _a[_top++] = x; } private: int* _a; int _top; int _capacity; }; class MyQueue { public: MyQueue(int capacity) : _pushST(capacity) , _popST(capacity) {} void Push(int x) { _pushST.Push(x); } private: Stack _pushST; Stack _popST; int _size; }; int main() { MyQueue q(100); return 0; }
可以看到,队列MyQueue
的初始化列表什么都不写也可以,因为对于自定义类型,会调用该自定义类型的默认构造函数。如果没有提供栈Stack
的默认构造函数或者对队列MyQueue
的初始空间有要求,就需要写初始化列表了。
#include <iostream> using namespace std; class Stack { public: Stack(int capacity = 4) { _a = (int*)malloc(sizeof(int) * capacity); if (_a == nullptr) { perror("malloc fail"); exit(-1); } _top = 0; _capacity = capacity; } // st2(st1) Stack(const Stack& st) { _a = (int*)malloc(sizeof(int) * st._capacity); if (_a == nullptr) { perror("malloc fail"); exit(-1); } memcpy(_a, st._a, sizeof(int) * st._top); _top = st._top; _capacity = st._capacity; } ~Stack() { cout << "~Stack()" << endl; free(_a); _a = nullptr; _top = _capacity = 0; } void Push(int x) { // 扩容 _a[_top++] = x; } private: int* _a; int _top; int _capacity; }; class MyQueue { public: MyQueue(int capacity) : _pushST(capacity) , _popST(capacity) {} void Push(int x) { _pushST.Push(x); } private: Stack _pushST; Stack _popST; int _size; }; int main() { MyQueue q(100); return 0; }
这就是初始化列表的基本内容了。那我们再来想一个问题:什么样的成员变量一定需要初始化列表来初始化?是不是引用一定要在初始化列表中初始化,因为引用只有一次初始化的机会且也只能初始化一次。
结论:
类中包含以下成员,必须放在初始化列表位置进行初始化
- 引用成员变量
- const成员变量
- 自定义类型成员(且该类没有默认构造函数时)
注意:尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,
一定会先使用初始化列表初始化。
接下来我们来看一道题目:
#include <iostream> using namespace std; class A { public: A(int a) :_a1(a) , _a2(_a1) {} void Print() { cout << _a1 << " " << _a2 << endl; } private: int _a2; int _a1; }; int main() { A aa(1); aa.Print(); } //A.输出1 1 //B.程序崩溃 //C.编译不通过 //D.输出1 随机值
可能很多人的答案都是 A,但是正确答案是 D。
那为什么是这样呢?是因为成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
从这道题里可以看出,选择题是非常坑的。做选择题一定要小心,不小心一点就会掉进出题人埋的坑了。注:说得比较绝对的选项大概率是错的。 比如:一个类必须提供默认构造函数,不提供就会报错。这句话就是错的,只要我们不调用就不会报错,调用了就会报错。
#include <iostream> using namespace std; class A { public: A(int a) :_a1(a) , _a2(_a1) {} void Print() { cout << _a1 << " " << _a2 << endl; } private: int _a2; int _a1; }; int main() { return 0; }
explicit 关键字
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有隐式类型转换的作用。
所谓的隐式类型转换,就是用一个类型的数据给不同类型的数据进行赋值时产生的转换。进行隐式转换时,会产生临时变量,临时变量具有常属性。因为临时变量具有常属性,所以使用引用时需要加上const修饰。
那自定义类型呢,也支持隐式类型转换,但要求其构造函数只有单个参数或者除第一个参数无默认值其余均有默认值。
单参数构造函数隐式类型转换(C++98)
#include <iostream> using namespace std; class Date { public: Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; }; int main() { Date d1(2022); //隐式类型转换 Date d2 = 2022; const Date& d3 = 2022; return 0; }
explicit 修饰构造函数禁止隐式类型转换
如果我不想让自定义类型的隐式转换发生,我们就可以在构造函数前加上explicit
关键字。
那这种隐式类型转换有什么用呢?有了自定义类型的隐式类型转换就会非常地方便,见下方代码:
#include <iostream> using namespace std; #include <string> void push_back(const string& str) { //... } int main() { string s1("hello"); push_back(s1); string s2 = "hello"; push_back(s2); push_back("hello"); return 0; }
在编译器的优化过后,它们都只需要调用构造函数就行了,且用起来很方便。所以支持自定义类型的隐式类型转换还是非常必要的。
多参数构造函数隐式类型转换(C++11)
#include <iostream> using namespace std; class Date { public: Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; }; int main() { Date d1 = { 2022, 11, 13 }; // 等价于 Date d2(2022, 11, 13); const Date& d3 = { 2022, 11, 13 }; return 0; }