1. 再谈构造函数
我们之前使用构造函数初始化:
#include using namespace std; class Date { public: Date(int year = 2023, int month = 7, int day = 1) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; }; int main() { Date d1; return 0; }
使用的是在构造函数的函数体内赋值,
实际上,构造函数还有一种初识化方法,就是列表初始化,
来看代码:
#include using namespace std; class Date { public: Date(int year = 2023, int month = 7, int day = 1) : _year(year) , _month(month) , _day(day) {} private: int _year; int _month; int _day; }; int main() { Date d1; return 0; }
实际上这两种初始化方式效果是一样的,
初识化列表就是以冒号开始,通过逗号分隔。
这里有几点要注意的:
1. 每个成员变量最多只能在初始化列表出现一次,
2. 有些成员必须在初始化列表初识化:
引用成员变量,const成员变量,自定义类型成员。
那这是为什么呢?
实际上,引用成员变量和const成员变量需要在定义时初始化,
而在函数体内赋值,编译器不认为是在定义时初始化,
而使用初始化列表则是在定义时初始化,
还记得我们之前学的,在函数成员变量声明时添加的缺省值吗?
那个就是在初始化列表的时候自动调用。
而对于自定义,使用列表初始化:
#include using namespace std; class A { public: A(int a) : _a(a) {} private: int _a; }; class B { public: B(int a = 10) : _a(a) {} private: A _a; }; int main() { B b; B b(20); return 0; }
这样就能做到在创建B的对象的时候,在外面控制A类对象的初始化,
当然你也可以选择不控制,可以感受到,这样的代码能更加灵活。
初始化列表还有一个需要注意的点:
来看这道题:
#include 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 随机值
这段代码,我们肯定不能选C,因为没有语法错误,
而我们也不能选B,因为只有像空指针,越界访问等等才可能崩溃,
而我们也不应该选A,因为一般来说,选A岂不是太简单了,
那为什么答案选D,输出1和随机值呢?
因为初始化列表的初始化顺序不是按照你初始化的顺序初始化的,
他是按照声明成员变量的顺序初始化的,
所以这段代码会先初始化_a2,而_a1还是随机值,所以_a2的值被初始化成随机值,
之后再初始化_a1为1,所以输出 1 和 随机值。
2. 构造函数中的隐式类型转换
在C++中支持这样一种操作:
#include using namespace std; class A { public: A(int a = 10) : _a(a) {} private: int _a; }; int main() { A a1(20); A a2 = 20; return 0; }
明明A是一个自定义类型,为啥他能 = 一个int类型的数呢?
实际上在C语言的时候我们学习过隐式类型转换:
double a = 10;
编译器将10这个整形拷贝生成一份double类型的临时变量,然后再赋值给a,
而上面那份代码也是类似的逻辑,
先用20作为参数构造生成一个A类型的临时变量,
然后a2拷贝构造形成参数为20的这个对象。
这个时候你可能会有疑问,这个特性有啥用呢?
来看这个例子:
#include #include using namespace std; int main() { string s1("你好"); string s2 = "你好"; //隐式类型转换 return 0; }
使用string类的时候,我们可以用构造函数将"你好"这个字符串,
转换成string类型,也可以使用 = 通过隐式类型转换直接使用,
这个时候你可能又有疑问了,这两种方法好像也没方便多少啊,
我们继续来看这个例子:
#include #include using namespace std; class list { public: void push_back(const string& str) { } }; int main() { list st; string s1("你好"); st.push_back(s1); st.push_back("你好"); //隐式类型转换 return 0; }
这个list类的push_back 接口需要传递string类型的参数,
如果我们没有隐式类型转换的话,就得先构造一个string类,在调用这个接口,
如果有隐式类型转换,我们就能直接传一个字符串,
然后编译器通过隐式类型转换帮我们转换成string类在传参,这就方便多了。
然后这里顺便介绍一个关键字:
关键字explicit:
使用了这个关键字就不能进行隐式类型转换了:
可以看到,再进行隐式类型转换就报错了。
3. static静态成员
来看这个例子:
#include using namespace std; class A { public: A(int a = 10) : _a(a) {} private: int _a; static int _num; }; int A::_num = 0; int main() { A a1(20); A a2 = 20; return 0; }
静态成员变量不能在类内定义,
需要在类外面定义,也就是这个:int A::_num = 0
而静态成员和普通成员变量有什么区别呢?
成员变量时属于每一个实例化出来的类对象的,存储在对象里面;
而静态成员变量属于每一个类,类的每个对象都是共享的,存储在静态区。
除了静态成员变量,还有静态成员函数,
来看例子:
#include using namespace std; class A { public: A(int a = 10) : _a(a) {} static void Print() { cout << "I am static" << endl; } private: int _a; static int _num; }; int A::_num = 0; int main() { A a1(20); A a2 = 20; A::Print(); return 0; }
输出:
I am static
静态成员函数没有this指针,
直接通过指定类域和访问限定符就可以访问。
那我们来实践一下:
如果我们想设计一个类,在类外面只能在栈/堆上创建对象该怎么做?
来看代码:
#include using namespace std; class A { public: A(int a = 10) : _a(a) {} private: int _a; }; int main() { static A a; return 0; }
如果只是一个普通的类,这就在静态区上创建出一个对象了,
那该怎么限制它呢?
我们可以把构造函数放到私有:
#include using namespace std; class A { public: private: A(int a = 10) : _a(a) {} private: int _a; }; int main() { A a; //在栈上开辟 A* a = new A; //在堆上开辟 return 0; }
这下好了,虽然静态区不能创建了,但是你在哪里都不能创建对象了,
因为构造函数只能在类内调用了,这该怎么办呢?
这个时候,static静态成员函数就可以出场了:
#include using namespace std; class A { public: static A* GetHeap() { return new A; } static A& GetStack() { A a; return a; } private: A(int a = 10) : _a(a) {} private: int _a; }; int main() { //A a; //在栈上开辟 //A* a = new A; //在堆上开辟 A::GetStack(); //在栈上开辟 A::GetHeap(); //在堆上开辟 return 0; }
你看,我们使用静态成员函数是不是一下子就解决了。
写在最后:
以上就是本篇文章的内容了,感谢你的阅读。
如果感到有所收获的话可以给博主点一个赞哦。
如果文章内容有遗漏或者错误的地方欢迎私信博主或者在评论区指出~