目录
前言
一、再探构造函数
二、类型转换
三、static 成员
四、友元
五、内部类
六、匿名对象
七、对象拷贝时的编译器优化
总结
前言
本文主要内容:构造函数的再探--初始化列表、内置类型与自定义类型之间的转换、类的static成员、友元、内部类、匿名对象。最后还会了解一下编译器对类对象拷贝时的优化。
一、再探构造函数
- 1. 之前我们实现构造函数时,初始化成员变量主要使用函数体内赋值,构造函数初始化还有一种方式,就是初始化列表,初始化列表的使用方式是以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
- 2. 注意:每个成员变量在初始化列表中只能出现一次,语法理解上初始化列表可以认为是每个成员变量定义初始化的地方。
初始化列表:
#include <iostream>
using namespace std;class Date
{
public://初始化列表Date(int year = 1, int month = 1, int day = 1):_year(year),_month(month),_day(day){}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;
};int main()
{Date d1(2024, 8, 20);d1.Print();return 0;
}
运行结果:
相当于我们之前在函数体中进行初始化成员变量,现在可以直接在初始化列表中进行初始化成员变量。注意一个成员变量在初始化列表中只能出现一次
- 3. 必须放在初始化列表位置进行初始化的3类成员变量:
- 引用成员变量
- const成员变量
- 没有默认构造的类类型(自定义类型)成员变量
我们首先理解第三点,什么是没有默认构造的成员变量?
答:我们回顾上一篇文章,默认构造有三种:无参构造函数、全缺省构造函数、我们不写构造时编译器默认生成的无参的构造函数。总结一下就是不传实参就可以调用的构造就叫默认构造。自定义类型的成员变量的构造不是默认构造就叫没有默认构造的成员变量
我们观察以下代码:(栈没有默认构造)
#include <iostream>
using namespace std;typedef int STDataType;
class Stack
{
public://需要传参,非默认构造函数Stack(int n){_a = (STDataType*)malloc(sizeof(STDataType) * n);if (_a == nullptr){perror("malloc fail!");exit(1);}_capacity = n;_top = 0;}~Stack(){free(_a);_a = nullptr;_capacity = _top = 0;}private:STDataType* _a;size_t _capacity;size_t _top;
};class MyQueue
{
public://编译器自动生成的无参默认构造会自动调用 Stack 类的构造初始化成员变量//假如 Stack 没有默认构造
private:Stack _pushst;Stack _popst;
};
我们之前写的 Stack 构造函数是有缺省参数的,因此在 MyQueue 类中,我们不用显示的去写构造函数,编译器自动生成的就会去调用Stack的默认构造。但是现在,Stack 类的构造不是默认构造,我们创建一个 MyQueue 类对象就会报错:
这是因为不是默认构造创建对象时就需要传参,而编译器自动为 MyQueue 生成的默认构造是不支持传参的,因此为了 MyQueue 能够正常实例化对象,我们需要用到初始化列表:
解决方法:
class MyQueue
{
public://编译器自动生成的无参默认构造会自动调用 Stack 类的构造初始化成员变量//假如 Stack 没有默认构造MyQueue():_pushst(4),_popst(4){}private:Stack _pushst;Stack _popst;
};
当然也可以写成全缺省的默认构造:
class MyQueue
{
public://编译器自动生成的无参默认构造会自动调用 Stack 类的构造初始化成员变量//假如 Stack 没有默认构造MyQueue(int n = 4):_pushst(n),_popst(n){}private:Stack _pushst;Stack _popst;
};int main()
{MyQueue q1;MyQueue q2(100);return 0;
}
以上就是为什么没有默认构造的类类型成员变量必须走初始化列表的原因。
接下来就是 引用类型成员变量 和 const成员变量:
报错信息:
正确写法:
对于普通对象 _a 来说,在初始化列表或者函数体中初始化都可以。
小结:加深理解为什么这三种成员变量必须走初始化列表:
- &引用类型成员变量:我们知道引用在定义时必须初始化的,不然就是类似野指针的野引用
- const类型成员变量:const 类型的变量,只有一次修改的机会,就是在第一次赋值时。
- 没有默认构造的自定义类型成员变量:没有默认构造的自定义类型成员变量,那么初始化时必须传参
因为这三种有这些特殊需求,因此必须走初始化列表
- 4. 尽量使用初始化列表初始化,因为那些你不在初始化列表初始化的成员也会走初始化列表,如果这个成员在声明位置给了缺省值,初始化列表会用这个缺省值初始化。如果你没有给缺省值,对于没有显示在初始化列表初始化的内置类型成员是否初始化取决于编译器,C++并没有规定。对于没有显示在初始化列表初始化的自定义类型成员会调用这个成员类型的默认构造函数,如果没有默认构造会编译错误。
- 5. C++11支持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显示在初始化列表初始化的成员使用的。
思维导图:
演示:
class Date
{
public:Date(int year = 2, int month = 2, int day = 2):_year(year), _month(month){}private://注意这里不是初始化,这里给的是缺省值,这个缺省值是给初始化列表的//如果初始化列表没有显示初始化,默认就会用这个缺省值初始化int _year = 1;int _month = 1;int _day = 1;// const 类型成员变量也可以在声明的时候给。const int n = 1;
};
监视窗口:
没有默认构造的自定义类型成员变量也可以使用缺省值初始化:
class Stack
{
public:Stack(int n):_a((STDataType*)malloc(sizeof(STDataType)*n)),_capacity(n),_top(0){}~Stack(){free(_a);_a = nullptr;_capacity = _top = 0;}private:STDataType* _a;size_t _capacity;size_t _top;
};class MyQueue
{
public:private://哪怕 Stack 没有默认构造也可以在定义时给缺省值//这和显示的去写初始化列表效果一样//当然如果显示的写了初始化列表,优先调用初始化列表初始化Stack _pushst = 4;Stack _pophst = 4;
};
- 6. 初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表出现的的先后顺序无关。建议声明顺序和初始化列表顺序保持一致。
例如:下面程序的运行结果是什么()
A.输出1 1
B.输出2 2
C.编译报错
D.输出1 随机值
E.输出1 2
F.输出2 1
#include <iostream>
using namespace std;class A
{
public:A(int a):_a1(a),_a2(_a1){}void Print() {cout << _a1 << " " << _a2 << endl;}
private:int _a2 = 2;int _a1 = 2;
};int main()
{A aa(1);aa.Print();
}
答案:D
解析:
- 首先如开头所说:初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表出现的的先后顺序无关
- 因为显示的写了_a1和_a2的初始化列表,因此_a1和_a2的缺省值用不上。
- 而在初始化列表中,_a2(_a1)先执行,此时_a1为随机值,因此_a2被赋值为随机值,再执行_a1(a),_a1被赋值为1。
- 打印结果:
初始化列表总结:
- 无论是否显示写初始化列表,每个构造函数都有初始化列表;
- 无论是否在初始化列表显示初始化,每个成员变量都要走初始化列表初始化;
二、类型转换
- C++支持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数。
- 构造函数前面加 explicit 就不再支持隐式类型转换。
- 类类型的对象之间也可以隐式转换,需要相应的构造函数支持。
演示:
#include <iostream>
using namespace std;class A
{
public://普通构造A(int a1):_a1(a1){cout << "A(int a1)" << endl;}//构造重载A(int a1, int a2):_a1(a1),_a2(a2){cout << "A(int a1, int a2)" << endl;}//拷贝构造A(const A& a):_a1(a._a1), _a2(a._a2){cout << "A(const A& a)" << endl;}void Print() const{cout << _a1 << " " << _a2 << endl;}private:int _a1 = -1;int _a2 = -1;
};int main()
{//1构造⼀个A的临时对象,再用这个临时对象拷贝构造aa1//编译器遇到连续构造+拷贝构造->优化为直接构造A aa1 = 1;aa1.Print();return 0;
}
运行结果:
解释:
- 上述中,将 1 赋值给 aa1 进行构造的过程就涉及隐式类型转换,理论上是首先将 1 构造成一个 A 类型的临时对象,aa1 再通过这个临时对象调用拷贝构造进行构造。
- 但是实际过程中,编译器进行了优化,直接将 1 构造A类对象并给到 aa1,省去了拷贝构造环节。
- 注意:1 调用的是单参数的构造,因此1被赋值到了成员变量_a1中。
这其中 1 构造过程中的临时对象是存在的,并且也具有常性,对其引用需要加 const 修饰。以下r引用的是1的一个临时对象。内置类型直接的转换一样。
以上是单参数隐式类型转换,其实C++还支持多参数隐式类型转换,只要有支持的构造函数即可,多参数转换使用大括号{},如:
如果不想让该类能够实现类型转化,可以在构造函数前加上 explicit 关键字就行了:
#include <iostream>
using namespace std;class A
{
public://普通构造explicit A(int a1):_a1(a1){cout << "A(int a1)" << endl;}//构造重载explicit A(int a1, int a2):_a1(a1),_a2(a2){cout << "A(int a1, int a2)" << endl;}//拷贝构造A(const A& a):_a1(a._a1), _a2(a._a2){cout << "A(const A& a)" << endl;}void Print() const{cout << _a1 << " " << _a2 << endl;}private:int _a1 = -1;int _a2 = -1;
};
这时就不支持内置类型转化为该类类型了:
不同类类型直接也可以相互转化,只要提供了对应的构造函数即可:
#include <iostream>
using namespace std;class A
{
public://普通构造A(int a1):_a1(a1){cout << "A(int a1)" << endl;}//构造重载A(int a1, int a2):_a1(a1),_a2(a2){cout << "A(int a1, int a2)" << endl;}//拷贝构造A(const A& a):_a1(a._a1), _a2(a._a2){cout << "A(const A& a)" << endl;}void Print() const{cout << _a1 << " " << _a2 << endl;}int Get() const{return _a1 + _a2;}private:int _a1 = -1;int _a2 = -1;
};class B
{
public://提高对A类的构造B(const A& a):_b(a.Get()){cout << "B(const A& a)" << endl;}private:int _b = 0;
};int main()
{A aa1 = { 1,1 };B bb1 = aa1;return 0;
}
运行结果:
类型转换应用场景:像Stack入栈A类对象就方便很多
#include <iostream>
using namespace std;class A
{
public://普通构造A(int a1):_a1(a1){cout << "A(int a1)" << endl;}//构造重载A(int a1, int a2):_a1(a1),_a2(a2){cout << "A(int a1, int a2)" << endl;}//拷贝构造A(const A& a):_a1(a._a1), _a2(a._a2){cout << "A(const A& a)" << endl;}void Print() const{cout << _a1 << " " << _a2 << endl;}int Get() const{return _a1 + _a2;}private:int _a1 = -1;int _a2 = -1;
};class Stack
{
public:Stack(int n = 4):_a((A*)malloc(sizeof(A)* n)), _capacity(n),_top(0){}void Push(const A& a){_a[_top++] = a;}~Stack(){free(_a);_a = nullptr;_capacity = _top = 0;}private:A* _a;size_t _capacity;size_t _top;
};int main()
{Stack st;//Push的参数加上 const 的原因就是传过去的都是临时对象的引用//像这样入栈是不是很方便st.Push({ 1,1 });st.Push({ 2,2 });st.Push({ 3,3 });return 0;
}
监视窗口:
三、static 成员
1.static成员变量
- 用 static 修饰的成员变量,称之为静态成员变量,静态成员变量一定要在类外进行初始化。
- 静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区。
- 静态成员也是类的成员,受public、protected、private访问限定符的限制。
- 静态成员变量不能在声明位置给缺省值初始化,因为缺省值是给构造函数初始化列表的,静态成员变量不属于某个对象,不走构造函数初始化列表。
例:统计该类实例化了多少个对象
#include <iostream>
using namespace std;class A
{
public:A(){++_scount;}A(const A& a){++_scount;}int GetAcount(){return _scount;}private://类里面声明static int _scount;
};//类外面初始化
int A::_scount = 0;void Func(A aa)
{}int main()
{A aa1;A aa2;Func(aa1);cout << aa1.GetAcount() << endl;return 0;
}
运行结果:
如果想统计现存的该类实例化对象个数,就在析构中 --_scount:
#include <iostream>
using namespace std;class A
{
public:A(){++_scount;}A(const A& a){++_scount;}~A(){--_scount;}int GetAcount(){return _scount;}private://类里面声明static int _scount;
};//类外面初始化
int A::_scount = 0;void Func(A aa)
{}int main()
{A aa1;A aa2;Func(aa1);cout << aa1.GetAcount() << endl;return 0;
}
运行结果:
2.static成员函数
- 用 static 修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针。
- 静态成员函数中可以访问其他的静态成员,但是不能访问非静态的,因为没有this指针。
- 非静态的成员函数,可以访问任意的静态成员变量和静态成员函数。
- 突破类域就可以访问静态成员,可以通过类名::静态成员或者 对象.静态成员 来访问静态成员变量和静态成员函数。
演示:
#include <iostream>
using namespace std;class A
{
public:A(){++_scount;}A(const A& a){++_scount;}~A(){--_scount;}//静态成员函数static int GetAcount(){return _scount;}//类里面声明static int _scount;
};//类外面初始化
int A::_scount = 0;void Func(A aa)
{}//突破类域就可以访问静态成员函数或变量
void Fxx()
{cout << A::GetAcount() << endl;cout << A::_scount << endl;
}int main()
{A aa1;A aa2;Func(aa1);Fxx();return 0;
}
运行结果:
3.两道练习题
1. 题目链接:求1+2+3+...+n_牛客题霸_牛客网
解析:
- 题目要求不能使用乘除法,因此等差数列的公式不能用,接着不能使用循环和分支结构,因此迭代和递归都不能使用。
- 解决方法就是刚刚所学的静态成员变量:
- 使用构造函数搭配静态成员变量解决。
class Sum {public:Sum() {//每创建一个Sum类对象,就让_ret+=_i一次//随后++_i即可_ret += _i;++_i;}//定义两个静态成员变量static int _ret;static int _i;
};
//初始化为0和1
int Sum::_ret = 0;
int Sum::_i = 1;class Solution {public:int Sum_Solution(int n) {//变长数组一次创建n个Sum类对象Sum arr[n];return Sum::_ret;}
};
2.选择题:
设已经有A,B,C,D 4个类的定义,程序中A,B,C,D构造函数调用顺序为?()
设已经有A,B,C,D 4个类的定义,程序中A,B,C,D析构函数调用顺序为?()
A :D B A C
B :B A D C
C :C D B A
D :A B D C
E :C A B D
F :C D A B
#include <iostream>
using namespace std;class A
{
public:A(){cout << "A()" << endl;}~A(){cout << "~A()" << endl;}
};
class B
{
public:B(){cout << "B()" << endl;}~B(){cout << "~B()" << endl;}
};
class C
{
public:C(){cout << "C()" << endl;}~C(){cout << "~C()" << endl;}
};
class D
{
public:D(){cout << "D()" << endl;}~D(){cout << "~D()" << endl;}
};C c;
int main()
{A a;B b;static D d;return 0;
}
答案:B、E
运行结果:
解释:
- 对于构造顺序,全局对象先构造,因为在主函数之前需要确保对象的可用性,主函数中的局部对象 a,b 就按照先后顺序构造,而静态对象d即使是储存在静态区,也是要在定义处才进行构造。
- 析构是后定义先析构,而c、d都是储存在静态区的,生命周期比a、b长,则a和b先按照后定义的先析构顺序析构,对于c、d,因为d是局部的静态对象,因此先析构,全局的对象c则最后析构。
四、友元
- 友元提供了一种突破类访问限定符封装的方式,友元分为:友元函数和友元类,在函数声明或者类声明的前面加 friend,并且把友元声明放到一个类的里面。
- 外部友元函数可访问类的私有和保护成员,友元函数仅仅是一种声明,他不是类的成员函数。
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制。
- 一个函数可以是多个类的友元函数。
例:上一篇中实现 <<和>>的运算符重载就运用了友元
#include <iostream>
using namespace std;class Date
{
public:Date(int year = 1970, int month = 1, int day = 1){_year = year;_month = month;_day = day;}//友元声明friend ostream& operator<<(ostream& cout, const Date& d);friend istream& operator>>(istream& in, Date& d);private:int _year;int _month;int _day;
};ostream& operator<<(ostream& out, const Date& d)
{out << d._year << "年" << d._month << "月" << d._day << "日" << endl;return out;
}istream& operator>>(istream& in, Date& d)
{cout << "请输入年月日:>";in >> d._year >> d._month >> d._day;return in;
}int main()
{Date d1, d2;cin >> d1 >> d2;cout << d1 << d2 << endl;return 0;
}
- 友元类中的成员函数都可以是另一个类的友元函数,都可以访问另一个类中的私有和保护成员。
- 友元类的关系是单向的,不具有交换性,比如A类是B类的友元,但是B类不是A类的友元。
- 友元类关系不能传递,如果A是B的友元,B是C的友元,但是A不是C的友元。
友元类:
#include <iostream>
using namespace std;class A
{
public://友元声明(放在A类任意位置都行)friend class B;private:int _a1 = 1;int _a2 = 2;
};class B
{
public:void Func1(const A& a){cout << a._a1 << endl;cout << _b1 << endl;}void Func2(const A& a){cout << a._a2 << endl;cout << _b2 << endl;}private:int _b1 = 3;int _b2 = 4;
};int main()
{B b1;A a1;b1.Func1(a1);b1.Func2(a1);return 0;
}
运行结果:
友元使用注意:
- 友元有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
五、内部类
- 1. 如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,跟定义在全局相比,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。
这一点可以通过sizeof计算验证:
#include <iostream>
using namespace std;class A
{
private:static int _k;int _h = 1;public://内部类class B //B默认就是A的友元{public:void foo(const A& a){cout << _k << endl; cout << a._h << endl;}};
};
int A::_k = 1;int main()
{//计算A类大小cout << sizeof(A) << endl;//创建B类对象受到A类域限制A::B b1;return 0;
}
运行结果:
解释:
- A类大小为4字节,因为 static 修饰的成员变量不在类对象中,在静态区,而内部类也不在外部类中,内部类实际和全局类一样,只不过受到外部类限制。
- 创建内部类对象需要外部类声明,如b1,同时也受到访问限定符限制。
- 内部类默认是外部类的友元类。因此内部类可以访问外部类的私有成员或函数,但是外部类默认是不能访问内部类的私有成员或函数的,因为友元是单向的。
- 内部类本质也是一种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使用,那么可以考虑把A类设计为B的内部类,如果放到 private/protected 位置,那么A类就是B类的专属内部类,其它地方都用不了。
如:外部类默认不能访问内部类私有成员
适用场景:
- 内部类使用较少,但在前面一道题中使用内部类相对合理一些
题目链接:求1+2+3+...+n_牛客题霸_牛客网
class Solution {//内部类class Sum {public:Sum() {_ret += _i;++_i;}};static int _ret;static int _i;
public:int Sum_Solution(int n) {//变长数组Sum arr[n];return _ret;}
};
int Solution::_ret = 0;
int Solution::_i = 1;
六、匿名对象
- 用类型 (实参) 定义出来的对象叫做匿名对象,相比之前我们定义的类型对象名 (实参) 定义出来的叫有名对象
- 匿名对象生命周期只在当前一行,一般临时定义一个对象当前用一下即可,就可以定义匿名对象。
演示:
#include <iostream>
using namespace std;class A
{
public:A(int a = 0):_a(a){cout << "A(int a = 0)" << endl;}~A(){cout << "~A()" << endl;}private:int _a;
};int main()
{//有名对象A aa1(1);A aa2;//匿名对象,其生命周期只有其当前一行A(1);A();//不传参也要有个括号return 0;
}
运行结果:
解释:
- 运行结果中,第一个析构就是匿名对象的析构,第二个析构就是第二个匿名对象的析构,因为它们两个生命周期只有一行。
匿名对象的使用:
1.作为参数调用类的成员函数:
class Solution {
public:int Sum_Solution(int n) {//...return n;}
};int main()
{//匿名对象调用类函数cout << Solution().Sum_Solution(1) << endl;return 0;
}
2.作为缺省值:
void Func(A a = A(0))
{//...
}
其余场景因为现在所学较少,不好列举
const 引用匿名对象注意:
int main()
{const A& ra = A(0);return 0;
}
- 首先匿名对象也具有常性,因此引用时需要 const 修饰
- 此时匿名对象的生命周期会因为引用而延迟,延迟的声明周期与引用一样。
总之,无论const引用的临时对象还是匿名对象,它们的生命周期都会因为const引用而延长,直至const引用结束。
七、对象拷贝时的编译器优化
- 现代编译器会为了尽可能提高程序的效率,在不影响正确性的情况下会尽可能减少一些传参和传返回值的过程中可以省略的拷贝。
- 如何优化C++标准并没有严格规定,各个编译器会根据情况自行处理。当前主流的相对新一点的编译器对于连续一个表达式步骤中的连续拷贝会进行合并优化,有些更新更 "激进" 的编译器还会进行跨行跨表达式的合并优化。
简单来说:原本为了达到一个效果需要多条指令,经过编译器优化后,效果一样,但指令大大减少,效率提升。
如:
#include<iostream>
using namespace std;class A
{
public://默认构造A(int a = 0):_a1(a){cout << "A(int a)" << endl;}//拷贝构造A(const A& aa):_a1(aa._a1){cout << "A(const A& aa)" << endl;}//运算符重载A& operator=(const A& aa){cout << "A& operator=(const A& aa)" << endl;if (this != &aa){_a1 = aa._a1;}return *this;}//析构~A(){cout << "~A()" << endl;}private:int _a1 = 1;
};A f()
{A aa;return aa;
}int main()
{A aa1 = f();cout << endl;return 0;
}
vs2022运行结果:
我们发现vs2022下,这段代码只调用了默认构造和析构,这就是编译器优化的结果,vs2022的优化还是非常激进的。
我们画图展示理论上这段代码无优化调用的完整流程:
(注:小的临时对象(4/8字节)会存在寄存器中,大的临时对象则是存在main函数的栈帧上的)
VS2019的优化:
(注:由于我没有安装2019版的vs因此这里没有展示2019版的vs运行结果)
直接说结果:在vs2019下,这段代码会调用aa的构造和aa1的拷贝构造和它们两个的析构,比vs2022多了一个拷贝构造和一个析构
我们可以想到vs2022的优化有多么夸张。
我们可以在 Linux 下可以看到无优化的运行结果:指令g++ test.cpp -fno-elide-constructors
我们将无优化结果一一对比原代码,验证临时对象的存在:
我们再看看 VS2022 的优化:
验证VS2022的优化:
验证很简单,只需要证明 f() 函数中的 aa 对象地址与 aa1的地址相同即可:
#include<iostream>
using namespace std;class A
{
public://默认构造A(int a = 0):_a1(a){cout << "A(int a)" << endl;}//拷贝构造A(const A& aa):_a1(aa._a1){cout << "A(const A& aa)" << endl;}//运算符重载A& operator=(const A& aa){cout << "A& operator=(const A& aa)" << endl;if (this != &aa){_a1 = aa._a1;}return *this;}//析构~A(){cout << "~A()" << endl;}private:int _a1 = 1;
};A f()
{A aa;cout << &aa << endl;return aa;
}int main()
{A aa1 = f();cout << &aa1 << endl;return 0;
}
运行结果:
成功验证
总结
以上就是本文的全部内容,感谢支持!