一、再探构造函数
之前我们实现构造函数时,初始化成员变量主要使用函数体内赋值,构造函数初始化还有⼀种方式,就是初始化列表,初始化列表的使用方式是以⼀个冒号":"
开始,接着是⼀个以逗号","
分隔的数据成员列表,每个"成员变量"后面跟⼀个放在括号"()"
中的初始值或表达式。
每个成员变量在初始化列表中只能出现⼀次,语法理解上初始化列表可以认为是每个成员变量定义初始化的地方。
引用成员变量,const
成员变量,没有默认构造的类类型变量,必须放在初始化列表位置进行初始化,否则会编译报错。
我们现在来看一下初始化列表怎么写:
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
,_month(2)
,_day(day)
{
_day = 15;
}
void Print() const
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print();
return 0;
}
这就是初始化列表,它在构造函数中是成员变量定义初始化的地方,这里_year
定义初始化为1
,_month
定义初始化为2
,_day
定义初始化为1
,但函数体内又对_day
进行了一次赋值所以变为15
,我们打印输出一下:
这里如果你不写初始化列表的话,也不会报错:
但_year
和_month
会变为随机值,为了不让它们是随机值,我们可以在private
的声明中给它们加上缺省值,就像函数一样,声明和定义分离时,规定声明是可以给缺省值的。(C++11
支持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显示在初始化列表初始化的成员使用的。)例如:
private:
int _year = 2025;
int _month = 2;
int _day = 2;
这样如果没有给值的情况下,会默认使用缺省值,不会导致是随机值的情况。
假设我们的成员变量中存在const
成员变量const int i;
和引用成员变量int& ret;
,我们再来看一下:
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
,_month(month)
{
_day = 15;
ret = year;
r = day;
}
如果这样写程序会报这样的错误:
它们都有一个共同的特点就是:必须在定义时就完成初始化。
而初始化列表可以认为是每个成员变量定义初始化的地方,所以它们必须在初始化列表上初始化。
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
,_month(month)
,ret(year)
,r(2)
{
_day = 15;
}
这样程序就可以正常运行,语法上是没有错误的,但这里存在一个问题:ret
的引用对象是year
而year
当Date
结束的时候就会被销毁,此时ret
就相当于野引用,有风险,所以ret
最好引用全局变量,或者定义一个变量让它引用。
Date(int& x,int year = 1, int month = 1, int day = 1)
:_year(year)
,_month(month)
,ret(x)
,r(2)
{
_day = 15;
}
int n = 1;
Date d1(n);
d1.Print();
这样就解决问题了。
还有一种情况就是成员变量中存在没有默认构造的类类型变量,我们给出以下类:
class Phone
{
public:
Phone(int price)
:_price(price)
{
}
private:
int _price;
};
我们在我们的成员变量中定义一个Phone
类对象pri
:
private:
//声明
int _year = 2025;
int _month = 2;
int _day = 2;
int& ret;
const int r = 1;
Phone pri;
此时运行代码就会发生以下报错:
没有合适的默认构造函数可用,也就是没有不需要传参的就可以使用的构造函数,所以它也必须在初始化列表初始化。
这样才能完成它的初始化,相当于调用它的构造函数。
只要类中的成员变量中出现了这三类成员变量,只能使用初始化列表初始化,否则会编译报错。
初始化列表还有一个特点:
Date(int& x,int year = 1, int month = 1, int day = 1)
:ret(x)
,r(2)
,pri(2100)
{
_day = 15;
}
这里我没有写_year、_month、_day
的初始化列表,但这三个成员变量也会走初始化列表。
即使没有写在初始化列表的成员变量,也要走初始化列表,所有的成员变量都要走初始化列表,其中我们上面讲到的三种特殊成员变量必须写在初始化列表上。
初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表出现的的先后顺序无关。建议声明顺序和初始化列表顺序保持⼀致。
初始化列表总结:
- 无论是否显示写初始化列表,每个构造函数都有初始化列表;
- 无论是否在初始化列表显示初始化成员变量,每个成员变量都要走初始化列表初始化;
1、测试题
下面程序的运行结果是什么?()
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();
return 0;
}
答案:D
原因:首先,初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表出现的的先后顺序无关,所以初始化列表中会先初始化_a2,因为它是先声明的,而_a2由_a1初始化,此时_a1是随机值,所以_a2就是随机值,之后_a1进行初始化,_a1根据a进行初始化,此时a是1,所以_a1就是1了,综上,答案是D:输出 1 随机值
。
二、类型转换
C++
支持内置类型隐式类型转换为类类型对象,需要有相关参数为内置类型的构造函数。- 构造函数前面加
explicit
就不再支持隐式类型转换。 - 类类型的对象之间也可以隐式转换,需要相应的构造函数支持。
我们来看看类型转换如何实现:
#include <iostream>
using namespace std;
class A
{
public:
A(int n)
:_a(n)
,_b(n)
{
}
A(const A& d)
{
_a = d._a;
_b = d._b;
}
void Print()
{
cout << _a << " " << _b << endl;
}
private:
int _a = 1;
int _b = 2;
};
int main()
{
return 0;
}
这里我简单定义了一个框架,根据第一个前提条件我们知道内置类型转化为类类型,需要有相关参数为内置类型的构造函数。也就是实现的A(int n)
,所以现在能够进行用int
类型隐式转化为类类型了。
A a1 = 1;
a1.Print();
运行结果:
代码能够正常运行,也就是发生了隐式类型转换,过程:先用1
构造一个A
的临时对象,再用这个临时对象拷贝构造a1
,这也是为什么拷贝构造函数加const
的原因,因为临时对象具有常性。
当然新编译器遇到构造+拷贝构造
会优化为直接构造
。但在老编译器下上面的过程是真实发生的。
注意: A& r = 1;
这段代码是错的,因为会先转化为临时对象而临时对象具有常性,A
不能引用const A
涉及到了权限的放大,所以A
之前要加const
。
这里是构造函数中是一个形参的情况,那要是两个形参该如何更改才能进行隐式类型转换呢?
A(int n,int m)
:_a(n)
,_b(m)
{
}
如果我们还按照之前那样写就会报以下错误:
因为是两个参数,所以报错也正常,应该这样写:
A a1 = { 1,2 };
a1.Print();
不想支持隐式类型转换,只要在构造函数前面加上explicit
就不再支持隐式类型转换了explicit A(int n,int m)
。
我们再来看一下类类型和类类型之间的转换:
A:
class A
{
public:
//explicit A(int n,int m)
A(int n, int m)
:_a(n)
, _b(m)
{
}
A(const A& d)
{
_a = d._a;
_b = d._b;
}
void Print()
{
cout << _a << " " << _b << endl;
}
int Get() const
{
return _a * _b;
}
private:
int _a = 1;
int _b = 2;
};
增加了Get
函数方便B
获取值。
B:
class B
{
public:
B(const A& d)
:_c(d.Get())
{
}
void Print()
{
cout << _c << endl;
}
private:
int _c = 2;
};
A a1 = { 100,200 };
B b1 = a1;
b1.Print();
运行测试:
这里也会发生构造临时变量(用a1构造B类型的临时对象)
,并用临时变量拷贝构造b1
的过程。
三、static
成员
1. 静态成员变量
用static
修饰的成员变量,称之为静态成员变量,静态成员也是类的成员,受public、protected、private
访问限定符的限制。静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区。所以静态成员变量不能在声明位置给缺省值初始化,因为缺省值是给构造函数初始化列表的,静态成员变量不属于某个对象,不走构造函数初始化列表,因此静态成员变量⼀定要在类外进行初始化。
我们一起来看一下静态成员变量是如何定义使用的:
#include <iostream>
using namespace std;
class A
{
public:
private:
//不属于某个对象,属于整个类
//不能给缺省值
//声明
static int _count;
int _a;
};
//定义并初始化
int A::_count = 0;
int main()
{
return 0;
}
这就是静态成员变量定义的代码,那接下来假设我要计算程序一共创建了多少个类,给如何计算呢?
A(int a)
:_a(a)
{
++_count;
}
A(const A& d)
{
_a = d._a;
++_count;
}
创建类对象肯定要经过这两个成员函数,所以调用这两个成员函数时我让_count
加加即可。
为了获取count
的值我们可以再写一个成员函数:
int GetCount()
{
return _count;
}
我们执行以下代码:
A a1(1);
A a2 = a1;
cout << a1.GetCount() << endl;
运行结果:
用全局变量也可以统计,但是用静态成员变量更好,它有一个优点,就是别人不能修改,能减少出错。
注意:这里打印不能写成cout << A::GetCount() << endl;
,因为GetCount
函数是某一个类对象的,不是整个类的。
2. 静态成员函数
- 用
static
修饰的成员函数,称之为静态成员函数,静态成员函数没有this
指针。 - 静态成员函数中可以访问其他的静态成员,但是不能访问非静态的,因为没有
this
指针。 - 非静态的成员函数,可以访问任意的静态成员变量和静态成员函数。
依旧以统计类对象个数为例,我们可以把GetCount
函数换成静态的。
//没有this指针
static int GetCount()
{
//_a++;不能访问非静态的,没有this指针
return _count;
}
A a1(1);
A a2 = a1;
cout << A::GetCount() << endl;
cout << a1.GetCount() << endl;
运行结果:
四、友元
- 友元提供了⼀种突破类访问限定符封装的方式,友元分为:友元函数和友元类,在函数声明或者类声明的前面加
friend
,并且把友元声明放到⼀个类的里面。 - 外部友元函数可访问类的私有和保护成员,友元函数仅仅是⼀种声明,它不是类的成员函数。
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制。
在上期博客讲解流插入、流提取运算符重载时,我们已经用过友元函数了:
补充:
- ⼀个函数可以是多个类的友元函数。比如一个函数
void func(const A& a, const B& b);
,它想要访问A类对象
和B类对象
中的私有成员就要分别在A类
和B类
中声明友元函数。
#include <iostream>
using namespace std;
// 前置声明,否则A的友元函数声明编译器不认识B
class B;
class A
{
friend void func(const A& a, const B& b);
public:
A(int a)
:_a(a)
{ }
private:
int _a = 1;
};
class B
{
friend void func(const A& a, const B& b);
public:
B(int b)
:_b(b)
{ }
private:
int _b = 2;
};
void func(const A& a, const B& b)
{
cout << a._a << endl;
cout << b._b << endl;
}
int main()
{
A a(2);
B b(3);
func(a, b);
return 0;
}
运行结果:
- 友元类的关系是单向的,不具有交换性,比如
B类
是A类
的友元,但是A类
不是B类
的友元,即B类
可以访问A类
,但A类不能访问B类。
A
:
class A
{
friend class B;
public:
A(int a)
:_a(a)
{
}
private:
int _a = 1;
};
B
:
class B
{
public:
B(int b)
:_b(b)
{
}
void Print(const A& d)
{
cout << "A._a: " << d._a << endl;
cout << "B._b: " << _b << endl;
}
private:
int _b = 2;
};
main
:
int main()
{
A a(2);
B b(3);
b.Print(a);
return 0;
}
运行结果:
- 友元类关系不能传递,如果A是B的友元, B是C的友元,但是A不是C的友元。
- 友元有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
五、内部类
- 如果⼀个类定义在另⼀个类的内部,这个内部类就叫做内部类。内部类是⼀个独立的类,跟定义在全局相比,它只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。简单来讲,它们是平行关系不是包含关系。
#include <iostream>
using namespace std;
class A
{
public:
class B
{
private:
int _b = 2;
};
private:
int _a = 3;
};
int main()
{
A a;
return 0;
}
定义一个A类对象
可以看到内部只有成员变量,所以A类
与B类
之间不是包含关系。
此时我们的B类
是public
也就是对外开放的我们可以这样定义一个B类对象
:A::B b;
,这也从侧面说明它们之间并不是包含关系,而是平行关系。如果B类
在隐私或者保护下,就不能再访问B类
了。
- 内部类默认是外部类的友元类。
class A
{
public:
class B
{
public:
B(const A& d)
:_b(d._a)
{ }
private:
int _b = 2;
};
private:
int _a = 3;
};
B类
中可以访问A类
的私有成员变量。
- 内部类本质也是⼀种封装,当
B类
跟A类
紧密关联,B类
实现出来主要就是给A类
使用,那么可以考虑把B类
设计为A
的内部类,如果放到private/protected
位置,那么B类
就是A类
的专属内部类,其他地方都用不了。
六、匿名对象
- 用
类型(实参)
定义出来的对象叫做匿名对象,相比之前我们定义的类型 对象名(实参)
定义出来的叫有名对象。只要给它命名了,它就是有名对象。 - 匿名对象生命周期只在当前⼀行,⼀般临时定义⼀个对象当前用⼀下即可,就可以定义匿名对象。
下面我们写一个带有构造和析构函数的类来学习一下匿名对象。
class A
{
public:
A(int a=1)
:_a(a)
{
cout << "A(int a=1)" << endl;
}
~A()
{
cout << "~A" << endl;
}
private:
int _a = 1;
};
如此一来函数有没有执行构造和析构我们看一下打印窗口就知道了。
// 不能这么定义对象,因为编译器⽆法
// 识别下⾯是⼀个函数声明,还是对象定义
//A a();
//匿名对象
A();//声明周期只在当前一行
A(1);
调试代码到最后一行:
我们可以看到刚刚调试运行到A(1);
时,A();
完成了构造和析构,说明它的生命周期已经结束了。
匿名对象的使用场景在于当你想要调用或者使用类中的东西又不想过多定义类对象时,就可以使用匿名对象,用完之后下一行它就销毁了。
例如:
class sum
{
public:
int sum_count(int n)
{
//...
return n;
}
};
七、对象拷贝时的编译器优化
现代编译器会为了尽可能提高程序的效率,在不影响正确性的情况下会尽可能减少一些传参和传返回值的过程中可以省略的拷贝。
如何优化,C++
标准并没有严格规定,各个编译器会根据情况自行处理。当前主流的相对新一点的编译器对于连续一个表达式步骤中的连续拷贝会进行合并优化,有些更新更"激进"
的编译器还会进行跨行跨表达式的合并优化。例如:Visual Studio2022
在优化方面就非常先进。
linux
下可以将下⾯代码拷贝到test.cpp
文件,编译时用 g++ test.cpp -fno-elide-constructors
的方式关闭构造相关的优化。
总结:
以上就是本期博客分享的全部内容啦!如果觉得文章还不错的话可以三连支持一下,你的支持就是我前进最大的动力!
技术的探索永无止境! 道阻且长,行则将至!后续我会给大家带来更多优质博客内容,欢迎关注我的CSDN账号,我们一同成长!
(~ ̄▽ ̄)~