🌈初始化列表
☀️1.用途:
给对象的所有内部的成员变量初始化
☀️2.格式:
以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个成员变量后面跟一个放在括号中的初始值或表达式:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
☀️3.特性:
-
每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)。
-
类中包含以下成员时,必须放在初始化列表位置进行初始化:引用成员变量、const成员变量、自定义类型成员(且该类没有默认构造函数时)。这些成员无法在构造函数中被赋值。
-
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
🎈例题:
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 随机值
答案:D
分析:类中两变量的声明顺序是先_a2再_a1,因此在初始化列表中先执行_a2(_a1),将_a2赋值为随机值(因为此时的_a1是随机值),再执行_a1(a),将_a1赋值为1。最终打印的顺序是先_a1再_a2,因此输出“1 随机值”。
- 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化
☀️4.举例:
class A
{
public:
A(int a)
:_a(a)
{}
private:
int _a;
};
class B
{
public:
B(int a, int ref)
:_aobj(a)
,_ref(ref)
,_n(10)
{}
private:
A _aobj; // 没有默认构造函数
int& _ref; // 引用
const int _n; // const
};
☀️5.初始化列表和构造函数体赋值的区别
构造函数体赋值本质上不是初始化,初始化列表是初始化:
初始化只能进行一次,但在函数体内可以实现对一个变量多次赋值,因此构造函数体内赋值不是初始化;
初始化列表要求每个成员变量在列表中只能出现一次,就是在保证初始化只能进行一次,因此初始化列表是初始化。
🌈隐式转换成类类型
对于内置类型,可以进行类型转换,如double d = 1语句就是将整型1隐式转换为double类型。同样的,也可以将内置类型隐式转换成自定义类型。
☀️1.隐式转换成为类对象的前提
对任何编译器而言,转换成的这个类对象的构造函数只有单个参数,或者除第一个参数无默认值其余均有默认值。
构造函数只有单个参数的情况:
Date(int year)
:_year(year)
{}
除第一个参数无默认值其余均有默认值的情况:
Date(int year, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
☀️2.隐式转换内部过程
内置类型转自定义类型是由构造函数支持的,构造函数中的参数是什么类型,就支持什么类型的数据转成类类型。
🎈例1:A aa3 = 3;(整型->类类型)
构造函数以一个整型作为参数;
内部过程:
①用3构造一个临时对象
②用这个临时对象拷贝构造对象aa3
🎈例2:const A& ra = 3;(整型->类引用)
(和例1一样,构造函数以一个整型作为参数)
内部过程:
①先用3构造一个临时对象
②ra就是这个临时对象的别名
注:引用起别名时,需要注意权限对等(可平移和缩小,不可放大)。隐式转换成引用类型的话需要在引用前加上const,因为本质上是将一个临时变量转换成引用,临时变量具有常属性,需要用const修饰的变量来接收临时变量(权限平移)
🎈例3:浮点型->整型->类类型
(和例1例2一样,构造函数以一个整型作为参数)
内部过程:(隐式转换了两次)
①先将double类型的3.33隐式转换成int类型的3
②再用3构造一个临时对象
③用这个临时对象拷贝构造对象aa4
🎈例4:指针->类类型
注意:和整型->类类型不同:整型->类类型需要的构造函数是以整型作为参数的;而指针->类类型需要以指针类型的变量作为该构造函数的参数
构造函数以一个整型指针作为参数;
🌈explicit关键字
☀️1.用途:
给构造函数前加上explicit关键字后,无法进行隐式转换
☀️2.强制转换
如果在有explicit关键字的情况下,非要进行类型转换,只能强制转换:
🌈static成员
☀️1.概念
声明为static的类成员(包含成员变量和成员函数)称为类的静态成员;
用static修饰的成员变量称为静态成员变量;
用static修饰的成员函数称为静态成员函数。
☀️ 2.特性
🎈(1)静态成员(变量+函数)为所有类对象所共享,不属于某个具体的对象,存放在静态区。
🎈(2)静态成员变量必须在类内声明,声明时要加static关键字,声明时不可以有初始值;类外定义,定义时不添加static关键字,但要指明类域,定义时再设置初始值。静态成员函数的声明和定义不用分开。
🎈(3)静态成员(变量+函数)可用“类名::静态成员”或者“对象.静态成员”来访问或调用。但是“对象.静态成员”这个方法并不是让编译器从这个对象中来找到count变量,而是告诉编译器去对象所属的类域中去找count。
对类内静态成员变量的访问:
可见二者访问的是同一个东西,同一块地址。
对类内静态成员函数的调用(注意要加类域限定符):
🎈(4)静态成员(变量+函数)也是类的成员,受public、protected、private 访问限定符的限制
🎈(5)静态成员变量不会走初始化列表程序,该变量的本质就是全局变量。初始化列表时初始化某一个对象的,因此普通成员变量要走初始化列表程序,缺省值也是给初始化列表的。被static修饰的变量是共享的,因此不走初始化列表。
🎈(6)静态成员函数的优势:
- 普通成员函数必须用对象调用,其实对象是为了提供this指针,而静态成员函数没有this指针,可以在没有对象的情况下尽情调用;
- 提高运行效率,告诉编译器你只需要去这个类域里面找这个函数就好,不用去别处。
🎈(7)静态成员函数没有隐藏的this指针,内部不可以访问类里面的非静态成员变量
☀️3.实现一个类,计算程序中创建出了多少个类对象。
思路:
借助类对象在生成和销毁时自动调用构造函数(或拷贝构造函数)、析构函数的特性,可以定义一个全局变量用来计数,每调用一次构造函数或析构函数,就+1,每调用一次析构函数就-1。
🎈(1)❌计数变量的命名不要用count:
因为库里面count是一个函数,命名冲突。
解决命名冲突需要注释掉“using namespace std”,但这样后序比较麻烦,如果想使用库里的东西的话需要加限定符“std::”。
🎈(2)❌不能将count弄成类内部的成员变量:
这样每新生成一个对象,都会新生成一个count,count的初始值都是0,无法起到计数作用。
🎈(3)❌可以将count放进一个新的命名空间,但有风险:
外部可以随意访问这个新命名空间内的变量,可能导致变量被恶意修改从而无法得到正确的值。
🎈(4)✅最好地方式还是用静态变量(private)计数,用静态函数得到这个数(public):
class A
{
public:
A() { ++_scount; }
A(const A& t) { ++_scount; }
~A() { --_scount; }
//声明
static int GetACount() { return _scount; }
private:
//声明
static int _scount;
};
//定义
int A::_scount = 0;
void TestA()
{
cout << A::GetACount() << endl;
A a1, a2;
A a3(a1);
cout << A::GetACount() << endl;
}
①由于变量是静态的,被所有对象共同拥有,从而可以累加计数;
②变量被private修饰,外界无法直接得到,从而保证结果不被恶意篡改;
③外部想要得到变量计算出来的值,需要一个public函数;
④用函数得到这个累加值,因此该函数也要是静态的才能知道类加值是多少;
⑤静态成员函数内无this指针,只能通过返回值的方式得到值。
只能通过这个静态函数得到值,不可以通过其修改,进一步保证了封装性和数据的准确。
☀️4.oj:求1+2+···+n
链接: https://www.nowcoder.com/share/jump/7711188001706953836169
🎈题目描述:
要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句
🎈思路:
定义一个类类型的数组,数组有多少个元素就要创建多少个类对象,借助每创建一个对象自动调用构造函数特性,实现求和。需要定义两个静态变量_i和_ret,_i用来自动加一,_ret用来将所有_i加和。
🎈初始代码:
class Solution {
public:
int Sum_Solution(int n) {
}
};
🎈提交代码:
#include <climits>
class sum
{
public:
sum(){
_ret+=_i;
_i++;
}
static int GetRet(){
return _ret;
}
private:
static int _i;
static int _ret;
};
int sum::_i=1;
int sum::_ret=0;
class Solution {
public:
int Sum_Solution(int n) {
sum arr[n];
return sum::GetRet() ;
}
};
🌈友元(友元函数+友元类)
友元提供了一种突破封装的方式,有时提供了便利。
但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元分为:友元函数和友元类
☀️1.友元函数
🎈(1)概念
在类的内部声明,声明时需要加friend关键字;在类外部定义。
友元函数可以直接访问类的私有成员。
🎈(2)特性
- 友元函数可访问类的私有和保护成员,但不是类的成员函数;
- 友元函数不能用const修饰;
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制;
- 一个函数可以是多个类的友元函数;
- 友元函数的调用与普通函数的调用原理相同。
🎈以流输入操作符重载为例,学习友元函数:
重载流插入操作符函数operator<<,符号<<左边必须是cout,右边是自定义的对象,因此为了参数顺序正确,只能将函数定义在类的外部(定义在类内的话,默认第一个参数为this指针,使得参数顺序错误),然而这个重载的函数需要用到类对象的私有成员变量,因此需要友元这种突破封装的方式,使得类外函数可以自由使用类内成员变量。
class Date
{
//类内部声明
friend ostream& operator<<(ostream& _cout, const Date& d);
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
//类外部定义
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
int main()
{
Date d;
cout << d << endl;
return 0;
}
成功调用重载的<<操作符函数:
☀️2.友元类
🎈(1)概念
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
🎈(2)特性
-
友元关系是单向的,不具有交换性。A是B的友元类,A可以自由访问B类的所有成员变量,但B不可以访问A。
-
友元关系不能传递。如果C是B的友元, B是A的友元,则不能说明C时A的友元。
🌈内部类
☀️1.概念
如果一个类A定义在另一个类B的内部,类A就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
☀️2.特性
🎈(1)内部类天生就是外部类的友元类,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
🎈(2)内部类可以定义在外部类的public、protected、private,受访问限定符和权限的限制。private修饰的类始终无法被外部使用。
🎈(3)内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
🎈(4)sizeof(外部类)=外部类,和内部类没有任何关系。
证明:类B定义在类A内部,两个类各自有一个整型的成员变量,计算此时B类型的大小:
class A
{
private:
int a;
public:
class B
{
public:
int b;
};
};
int main() {
cout << sizeof(A) << endl;
return 0;
}
说明计算外部类A的时候不会计算上B。
☀️ 3.与“有一个类类型成员变量”概念的区分
类B定义在类A外部,在类A内部有一个B类型的成员变量,计算类A的大小:
class B
{
public:
int b;
};
class A
{
int a;
B b;
};
int main() {
cout << sizeof(A) << endl;
return 0;
}
说明计算类A的时候会计算上B,类B的大小是类A的大小的一部分。
☀️4.对oj“求1+2+···+n”的优化
在最初版本的基础上进行了如下几方面的优化:
- 将计数的静态变量i和ret定义到了外部类Sum_Solution中
- 将类sum定义到了类Sum_Solution的内部,sum作为内部类,从而sum的构造函数可以直接使用两个静态变量进行计数
- sum中不需要增加一个public函数来返回运算结果,ret反正在外部类内呢,直接返回ret就行
class Solution {
class sum
{
public:
sum(){
ret+=i++;
}
};
public:
int Sum_Solution(int n) {
sum arr[n];
return ret;
}
private:
static int i;
static int ret;
};
int Solution::i=1;
int Solution::ret=0;
🌈拷贝对象时的一些编译器优化
☀️1.补充知识:区分拷贝构造和赋值拷贝
🎈(1)拷贝构造:已经存在了对象aa1,然后用aa1的模板创建新对象aa2和aa3。
(aa2和aa3分别代表拷贝构造的两种写法)
A aa1(1);
A aa2(aa1);
A aa3=aa1;
🎈(2)赋值拷贝:存在两个对象aa1和aa2,将一个的值赋给另一个。
A aa1(1);
A aa2(2);
aa1=aa2;
☀️2.新编译器的优化原则:
在同一个表达式中,先后出现两次构造函数(或拷贝构造)时,编译器会将两函数合并成为一个函数,即两个步骤合并为一步。
具体哪两个函数合并为哪一个函数如下图:
☀️3.新编译器优化案例
现在有一个类A,其中有1个整型成员变量,public部分显式写出来4个函数,依次是构造、拷贝构造、=运算符重载函数、析构函数:
(增加了打印的语句,方便得知编译器内部到底用了哪些函数及函数调用的过程)
class A
{
public:
A(int a) {
_a = a;
cout << "A(int a)" << endl;
}
A(const A& aa) {
_a = aa._a;
cout << "A(const A& aa)" << endl;
}
A& operator=(const A& aa) {
_a = aa._a;
cout << "A& operator=(const A& aa)" << endl;
return *this;
}
~A() {
cout << "~A()" << endl;
}
private:
int _a = 1;
};
基于类A展示以下优化案例
🎈例1:
int main() {
A aa1 = 1;
}
🌟原本过程:
- 将整型1隐式转换为A类型,即用1创建一个临时对象
(构造函数) - 将临时对象拷贝赋值给对象aa1
(拷贝构造函数) - 临时对象被复制完毕,就销毁临时对象
(析构函数) - main函数结束,销毁aa1对象
(析构函数)
🌟编译器优化:
构造+拷贝构造->构造,最终会被优化为一个构造函数,即直接用1构造对象aa1,省去了构造临时对象和销毁临时对象的过程
🌟最终运行结果:
🎈例2:
int main() {
const A& aa2 = 1;
}
🌟内部过程:
- A& aa2代表aa2是一个对象的别名
- 这个对象肯定不能是整形数据2了,只能是由2隐式转换的对象
- 隐式转换即用2构造临时对象,aa2就是这个临时对象的别名
(构造函数) - 语句运行完后,就销毁临时对象
(析构函数)
🌟最终运行结果:
🎈例3:
void func1(A aa) {
}
int main() {
A aa3(1);
func1(aa3);
}
🌟内部过程:
- 用1构造一个对象aa3
(构造函数) - 传值调用func1,以aa3的拷贝作为参数,即用aa3拷贝构造一个临时对象
(拷贝构造函数) - 调用完func1函数,销毁临时对象
(析构函数) - main函数运行结束,销毁对象aa3
(析构函数)
🌟最终运行结果:
🎈例4:
void func1(A aa) {
}
int main() {
A aa3(1);
func1(aa3);
func1(A(1));
}
🌟原本过程:
- 用1构造对象aa3
(构造函数) - 以aa3作为参数,传值调用func1,即生成用aa3拷贝构造而成的临时变量
(拷贝构造函数) - 调用完func1,销毁临时变量
(析构函数) - 又调用一次func1,这次参数为A(A(1))。先用1构造临时变量A(1),再用这个临时变量拷贝构造另一个临时变量,作为func1的参数
(构造函数+拷贝构造函数) - 运行完这句话,销毁临时变量A(1)和另一个临时变量
(析构函数+析构函数) - main函数结束,销毁aa3
(析构函数)
🌟编译器优化:
构造函数+拷贝构造函数->构造函数。将原本的第4步过程中先后出现的构造函数+拷贝构造函数,优化成1个构造函数;对应的,将原本的第5步过程中的两个析构函数优化的只剩1个析构函数(构造函数从2个变成1个了,则析构肯定要跟着变)。
🌟最终运行结果:
🎈例5:
A func2() {
A aa(1);
return aa;
}
int main() {
A aa4(2);
aa4 = func2();
return 0;
}
🌟内部过程:
- 用2构造对象aa4
(构造函数) - 需要给对象aa4重新赋值,赋func2函数返回的值,调用func2函数
- func2函数内,用1创建对象aa
(构造函数) - func2函数内,传值返回aa,传值返回的是由aa拷贝构造的临时对象
(拷贝构造函数) - func2函数调用结束,销毁aa对象
(析构函数) - 使用operator=函数,使得aa4接收到返回值,然后临时对象销毁
(析构函数) - main函数运行结束,销毁aa4对象
(析构函数)
🌟最终运行结果:
🎈例6:
A func2() {
A aa(1);
return aa;
}
int main() {
A aa5=func2();
func2();
return 0;
}
🌟原本过程:
对于语句1:A aa5=func2();
- 需要用func2的返回值来拷贝构造aa5,因此需要先进入func2
- func2中,先用1构造aa
(构造函数) - func2中,传值返回aa,因此需要用aa拷贝构造一个临时对象
(拷贝构造函数) - 用临时对象拷贝构造main中的对象aa5
(拷贝构造函数) - func2函数结束,销毁aa
(析构函数) - 拷贝构造结束,销毁临时对象
(析构函数)
对于语句2:func2();
- 进入func2,先构造aa
(构造函数) - 传值返回,用aa拷贝构造临时对象
(拷贝构造函数) - func调用结束,销毁aa
(析构函数) - main函数结束,销毁aa5和func2返回的临时对象
(析构函数)
🌟编译器优化:
拷贝构造+拷贝构造=拷贝构造。对于语句1:A aa5=func2();,会将3和4两个连续的拷贝构造函数合并成1个拷贝构造函数,相当于跳过了拷贝构造临时对象的过程,因此也不需要析构临时对象
🌟最终运行结果:
☀️注:不同编译器或不同版本有优化差异
- 上述的案例都是vs2019的debug版本下的结果,如果换成release版本,会优化地更多。
比如,对于案例5,优化成以下样子:
- 不同编译器运行的结果可能会根据优化的程度有不同
- 优化等级越高,则拷贝构造次数越少,但可能因为激进的优化,导致原先能运行的程序可能运行不了