目录
一、再探构造函数:
① 之前实现构造函数时,初始化成员变量主要使⽤函数体内赋值,构造函数初始化还有⼀种⽅ 式,就是初始化列表,初始化列表的使⽤⽅式是以⼀个冒号开始,接着是⼀个以逗号分隔的数据成 员列表,每个"成员变量"后⾯跟⼀个放在括号中的初始值或表达式(比如:malloc)
②每个成员变量在初始化列表中只能出现⼀次,语法理解上初始化列表可以认为是🐸每个成员变量定义初始化的地⽅🐸。(定义:需要占用内存空间🪰)
✨这里因为已经初始化了一次day,不能重复写了,会报错了;当然了,下面这个写法是没有问题的,书写自由嘛,其实这个看着更舒服,一行写一个变量初始化🥰
③引⽤成员变量、const成员变量、没有默认构造的自定义类型变量(有默认构造会调用自己的自定义类型的默认构造,在初始化列表可以不写)这三种,必须放在初始化列表位置进⾏初始化,否则会编译报错。其他的可以在函数体内进行初始化。
#include<iostream>
using namespace std;
class Time
{
public:
//Time的初始化列表
Time(int hour)
:_hour(hour)
{
cout << "Time()" << endl;
}
private:
int _hour;
};
class Date
{
public:
Date(int& xx,int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
,_t(1)
,_ref(xx)
,_n(1)
{
cout << "Date()" << endl;
}
private:
int _year;
int _month;
int _day;
Time _t; //没有默认构造的自定义类型的成员变量
int& _ref; // 引⽤成员变量
const int _n;//const修饰的成员变量
};
🪰Time类类型的变量 _t 在初始化列表初始化,然后又会去调用自己的初始化列表给该类型的变量进行初始化。
🪰const修饰的和引用成员,const修饰后不允许更改,而引用在创建时就初始化好了(给好了别名)
④不是所有成员必须在初始化列表进行初始化的,也可以在函数体内进行初始化:相当于在里面进行赋值初始化嘛 ,但是第③点说的那几种情况必须在初始化列表进行初始化
👉下面对指针ptr进行初始化(申请空间)也是在函数体写
⑤我们不是所有成员一定要在对象定义时进行初始化,有些成员变量未初始化,编译器也不会拿你怎么样,给了个随机值,因为C++未规定标准,这里的值是不确定的,看编译器的,了解一下即可🪰🪰尽量能初始化,还是都进行初始化
⑥C++11⽀持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显⽰在初始化列表的成员变量进行初始化使用的
✨ 首先:这里成员变量还是声明,不要认为给了值就是定义了(🪰定义的本质就是需要占用内存,这里并未占用内存,仍是声明),因为这里给的是缺省值,其实是在进行初始化的时候使用的,你没有给它进行初始化,编译器就默认将缺省值作为它的初始化值交给他使用
👉👉同样的情况,因为我们加了缺省值,所以未显示初始化_day 编译器会将缺省值给它
✨其次,缺省值甚至还可以写表达式
错误的思想:有人或许会认为在函数里面写缺省值,是不是就可以代替在声明处写缺省值🤔🤔🧑🎓🤔这里函数传参写缺省值,只是你不传参,该参数可以用缺省值去使用,你_day不使用这个参数,和_day是没有半毛钱关系🐸🐸当然_day是不确定的,所以并非一定是下面的0
总结:
尽量使⽤初始化列表初始化,因为那些你不在初始化列表初始化的成员也会⾛初始化列表,如果这 个成员在声明位置给了缺省值,初始化列表会⽤这个缺省值初始化。如果你没有给缺省值,对于没有显⽰在初始化列表初始化的内置类型成员是否初始化取决于编译器,C++并没有规定。对于没有 显⽰在初始化列表初始化的⾃定义类型成员会调⽤这个成员类型的默认构造函数,如果没有默认构造会编译错误!!!
👉👉看下图即可:
补充一点:
🪰🪰初始化列表中按照成员变量在类中声明顺序进⾏初始化,跟成员在初始化列表出现的的先后顺序⽆关。建议声明顺序和初始化列表顺序保持⼀致。
class A
{
public:
A(int a)
:_a1(a)
,_a2(_a1)
{
cout << _a1 << " " << _a2 << endl;
}
private:
int _a2 = 1;
int _a1 = 1;
};
int main()
{
A a(1);
return 0;
}
🪰🪰这里打印的情况如下:
因为这个是按照声明顺序进行初始化,所以先初始化的是a2,再初始化的是a1
✨✨声明顺序其实也就是变量再内存中的存放顺序!!
试着调试,通过看内存就能发现先调用的_a2,再是_a1,这里调试顺序也是先102行,再101行
二、类型转换:
C++⽀持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数
{
public:
A(int a)
:_a1(a)
{}
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
private:
int _a2 = 1;
int _a1 = 1;
};
int main()
{
A a(1);
//隐式类型转换:
//2构造一个A的临时对象,然后用这个临时对象拷贝构造aa
//编译器遇到连续构造+拷贝构造——>优化为直接构造
A aa = 2;
return 0;
}
🪰🪰 第一行代码是构造,第二行则是隐式类型的转换(不是任意类型都支持转换);
✨✨ 在上面可以看出,我们的构造函数只接收一个参数,然后在这里发生转换,用2去构造A类型的临时对象,2作为参数传给构造函数,然后这个临时对象在作为拷贝构造给给aa
✨✨可能有些人会认为这个有什么意义??可以这么玩,引用类型的,因为2转换成A类型,需要生产临时对象,临时对象具有常性,A&不能直接接收,要权限缩小,加const
✨✨一般是这么玩的:创建变量那里报错是因为我没写默认构造/初始化列表,主要还是希望能理解隐式类型转换,接收类型是const A& 是因为隐式类型转换具有常性;只允许进行单参数构造的这么用
✨✨C++11支持多参数的写法,A的构造函数接收2个参数,就可以用{}来进行转换,一个参数一个“,”可以写多个
②构造函数前⾯加explicit(关键字)就不再⽀持隐式类型转换
三、static成员:
①⽤static修饰的成员变量,称之为静态成员变量,静态成员变量⼀定要在类外进⾏初始化
🪰🪰静态成员变量不能在声明位置给缺省值初始化,因为缺省值是在构造函数初始化列表使用的,静态成员变量不属于某个对象,不会⾛构造函数初始化列表。
②静态成员变量为类的所有对象共享,不属于某个具体的对象,不存在对象中,存放在静态区。可以认为是静态的全局变量,受到类域的限制
🪰🪰从计算大小就能知道静态成员变量并未存在类中
③⽤static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针
④静态成员函数中可以访问其他的静态成员,但是不能访问⾮静态的,因为没有this指针。找不到该对象;如下:因为没有this指针,所以找不到_a
⑤⾮静态的成员函数,可以访问任意的静态成员变量和静态成员函数。不会发生报错
⑥突破类域就可以访问静态成员,可以通过类名::静态成员 或者 对象.静态成员来访问静态成员变量和静态成员函数(🤔🧑🎓说的公有的情况进行访问)
⑦静态成员也是类的成员,受public、protected、private访问限定符的限制。
🪰🪰🪰做一道相关题目:求1+2+3+……+n
🤔可以考虑用static成员变量:
class Sum
{
public:
Sum()
{
_num+=_i;
_i++;
}
static int GetSnum()
{
return _num;
}
private:
static int _i;
static int _num;
};
int Sum::_i = 1;
int Sum::_num = 0;
class Solution {
public:
int Sum_Solution(int n) {
Sum a[n];//边长数组,这里会构造n次
return Sum::GetSnum();
}
};
四、友元:
✨✨✨✨✨✨✨✨✨我作为你的朋友,可以拥有你家的权限✨✨✨✨✨✨✨✨✨
①友元提供了⼀种突破类访问限定符封装的⽅式,友元分为:友元函数和友元类,在函数声明或者类声明的前⾯加friend,并且把友元声明放到⼀个类的⾥⾯。(友元声明的位置是任意的)
②外部友元函数可访问类的私有和保护成员,友元函数仅仅是⼀种声明,他不是类的成员函数。
✨如下:因为B类写在下面,编译器是从上往下进行识别的,这里用个前置声明,告诉编译器我有这么个东西
③友元函数可以在类定义的任何地⽅声明,不受类访问限定符限制。
④⼀个函数可以是多个类的友元函数。
✨✨这个函数可以是A和B的友元函数✨✨
⑤友元类中的成员函数都可以是另⼀个类的友元函数,都可以访问另⼀个类中的私有和保护成员。
✨✨将B类作为A的友元类
⑥友元类的关系是单向的,不具有交换性,⽐如A类是B类的友元,但是B类不是A类的友元。
🤔🤔就是说我是你的友元(你认为我是你的朋友),然而我不认为你是我的朋友,只是你单方面认为我们是朋友关系🤔🤔
🪰🪰在这里:用不了,A是我的朋友,但是A认为我不是她的朋友,所以调用不能访问A的成员变量
🐸除非,她也认为我是她的朋友,她终于认可我了😭(哈哈哈,有点像tian🐕的感觉)
⑦友元类关系不能传递,如果A是B的友元,B是C的友元,但是A不是C的友元。
🪰🪰如下:A是我的朋友,我是C的朋友,但是C不是A的朋友
⑧有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多⽤。
五、内部类:
定义:如果⼀个类定义在另⼀个类的内部,这个内部类就叫做内部类。
①内部类是⼀个独⽴的类,跟定义在全局相⽐,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。
🪰🪰如下:内部类B不是A的对象
class A
{
private:
int _h;
static int _scount;
class B
{
private:
int _b;
};
};
int main()
{
A aa;
cout << sizeof(aa) << endl;
return 0;
}
🧑🎓🧑🎓 算出来是A实例化的对象大小为4
②内部类默认是外部类的友元(内部类可以使用外部类的所有对象)
③内部类本质也是⼀种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使⽤,那么可以考虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其 他地⽅都⽤不了。
六、匿名对象:
》⽤类型(实参)定义出来的对象叫做匿名对象(没有名字),相⽐之前我们定义的 类型 对象名(实参) 定义出来的 叫有名对象
》匿名对象⽣命周期只在当前⼀⾏,⼀般临时定义⼀个对象当前⽤⼀下即可,就可以定义匿名对象(一次性杯子,喝一口就扔了)
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
class Solution {
public:
int Sum_Solution(int n) {
//...
return n;
}
};
int main()
{
A aa1;
// 不能这么定义对象,因为编译器⽆法识别下⾯是⼀个函数声明,还是对象定义
//A aa1();
// 但是我们可以这么定义匿名对象,匿名对象的特点不⽤取名字,
// 但是他的⽣命周期只有这⼀⾏,我们可以看到下⼀⾏他就会⾃动调⽤析构函数
A();
A(1);
A aa2(2);
// 匿名对象在这样场景下就很好⽤,当然还有⼀些其他使⽤场景,这个我们以后遇到了再说
Solution().Sum_Solution(10);
return 0;
}
🐸🐸 如下:在下面我们只是要单纯用一下,匿名对象的写法更好;更有性价比
🐸🐸看下面生命周期:匿名对象用完就结束了,而有名对象到函数销毁才析构。
🐸🐸局部变量,先构造的后析构,后析构的先构造
七、对象拷⻉时的编译器优化:
1、现代编译器会为了尽可能提⾼程序的效率,在不影响正确性的情况下会尽可能减少⼀些传参和传参过程中可以省略的拷⻉。 • 如何优化C++标准并没有严格规定,各个编译器会根据情况⾃⾏处理。
2、当前主流的相对新⼀点的编 译器对于连续⼀个表达式步骤中的连续拷⻉会进⾏合并优化,有些更新更"激进"的编译还会进⾏跨 ⾏跨表达式的合并优化。
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;
}
void Print()
{
cout << "A::Print()" << _a1 << endl;
}
private:
int _a1 = 1;
};
void f1(A aa)
{}
A f2()
{
A aa(1);
return aa;
}
int main()
{
A aa1(1);
f1(aa1);// 传值传参
cout << endl;
//f1(A(1));//匿名对象传参
//cout << endl;
return 0;
}
✨✨传值传参 临时对象要拷贝构造给f1的形参(有些编译器会优化掉)
✨✨形参是实参的别名时,就不会发生拷贝构造
✨✨匿名对象创建要是构造,然后拷贝构造给形参,但是被优化了,同理,类型转换也是
✨✨返回值,拷贝到临时对象,然后临时对象再被销毁
这个VS2019的:
✨✨ VS2022:这个aa被优化掉了,只对临时对象进行了构造和析构(下面具体点就能更好看出来)
✨✨对返回的对象进行访问:我们的临时对象可以拿来去访问A类里面的Print,再析构
🧑🎓🧑🎓VS2019:
🧑🎓🧑🎓而VS2022中:aa被优化的了,这里没有生成aa,直接就是生成临时对象,将aa和临时对象进行的合二为一;为啥优化的aa??看析构就可以看出来,这里是先打印再析构的,//aa在出了函数就应该会被析构了,不应该在Print之后,而应在Print之前,所以优化了aa
🪰 接下来,把f2换一下
A f2()
{
A aa(1);
++aa;
return aa;
}
🪰类里面加一个前置++重载
A& operator++()
{
++_a1;
return *this;
}
✨✨在VS2022中:继续敢优化,还是给aa优化了,关于VS2019的,不继续了
真的nb,优化了照样会根据++a
✨✨跨行合并优化,这里VS2022甚至连临时对象都优化了,直接变成了构造ret和析构ret
这个属于优化二级了,但是老的优化没有这么激进的
✨✨ 赋值重载+连续构造
VS2019:没有进行任何优化,
VS2022:相比于19进行了一部分优化
总结:是否优化以及优化程度都取决于编译器,但是一般来说,编译器进行了优化不会影响正确性
八、总结:
继 续 努 力