这里放一下类于对象的前两篇链接,供大家对照本篇更加深入的参考学习。
话不多说,我们直接开始/
目录
一、再谈构造函数
深入初始化列表
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
中篇,我们简单讲了一下初始化列表,谈到虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为 对 对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
所以为了更有针对性的进行初始化,我们还有一种初始化方式,就是初始化列表。
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。

注意:
- 引用成员变量 , const 成员变量 , 没有默认构造的类 类型变量 , 必须放在初始化列表位置进行初始化 , 否则会编译报错 !
- 每个成员变量在初始化列表中只能出现一次 !语法理解上 初始化列表 可以认为是每个成员变量定义初始化的地方 。
- C++11 支持在成员变量声明的位置给缺省值 , 这个缺省值主要是给没有显示在初始化列表 初始化的成员使用的 。
- 尽量使用初始化列表初始化 , 因为不在初始化列表初始化的成员也会走初始化列表 。 如果这个成员在声明位置给了缺省值 , 初始化列表会用这个缺省值初始化 。 如果你没有给缺省值 , 对于没有显示在初始化列表初始化的内置类型成员是否初始化取决于编译器 , C++并没有规定 。 对于没有显示在初始化列表初始化的自定义类型成员会调用这个成员类型的默认构造函数 , 如果没有默认构造 会编译错误 。
- 初始化列表中按照成员变量在类中声明顺序进行初始化 , 跟成员在初始化列表出现的先后顺序无关 。 建议声明顺序和初始化列表顺序保持一致。

我们再来总结一下,什么情况下需要用初始化列表?
1)默认构造难以满足我们的需求的时候,
比如上图我的Myqueue需要1000个空间,但是默认生成只生成了四个。
2)自定义类型没有提供默认构造。
所以需要我们自己通过初始化列表来显式的写默认构造。

但是我们需要注意的是,因为在初始化列表中可以显示初始化,在声明中也可以给缺省值,但是我们尽量择一即可 , 否则太混乱,降低代码可读性。尽量使用初始化列表进行初始化,如果还需要进一步的检查和处理,就在函数体继续处理。
explicit关键字
构造函数不仅可以构造与初始化对象,对于接收单个参数的构造函数,还具有类型转换的作用。
接收单个参数的构造函数具体表现:
1. 构造函数只有一个参数
2. 构造函数有多个参数,除第一个参数没有默认值外,其余参数都有默认值
3. 全缺省构造函数
C++支持 内置类型 隐式转换为类 类型对象 , 需要有相关内置类型为参数的构造函数
构造函数前面加 explicit 就不再支持隐式类型转换(但是显式还是可以转换的)
类型转换的几种类型
1 ) 内置类型 : 能否进行类型转换需要看相互之间的关联
---> 整型之间 , 整型和浮点数之间 , 整型和指针之间 , 指针和指针之间
2 ) 内置类型 --> 自定义类型 :
---> 强转(显示)
---> 调用对应的构造函数
3 ) 内置类型 ---> 内置类型
---> 调用对应的构造函数
我们再来看看,为什么不要它隐式类型转换。

这里我们梳理一下,编译器做了什么。
编译器首先会构造一个类型为A的临时对象,因为1不是A类型,需要进行类型转换。这种类型转换会产生临时变量(图中用红色标注的“临时变量”)。
然后使用这个临时对象通过拷贝构造函数来构造a33。
而编译器在遇到连续的构造和拷贝构造时,会进行优化,直接使用1来构造a33,而不生成临时对象。这就减少了临时空间的占用,
同时使用类型转化 可以更加简洁的书写代码。

二、static成员
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化
static成员的特性
1.用 static 修饰的成员变量 , 称之为静态成员变量 , 静态成员变量一定要在类外进行初始化 。
2.静态成员变量 为所有类对象所共享 , 不属于某个具体的对象 , 不存在对象中 , 存放在静态区 。
3.用static 修饰的成员函数 , 称之为静态成员函数 , 静态成员函数没有this指针。
4.静态成员函数中可以访问其他的静态成员 , 但是不能访问非静态的 , 因为没有this指针。
5.非静态的成员函数 , 可以访问任意的静态成员变量 和 静态成员函数 。
6.突破类域可以访问静态成员 , 可以通过 类名::静态成员 或者 对象.静态成员 来访问静态成员变量 和 静态成员函数 。
7.静态成员也是类的成员 , 受public , protected , private 访问限定符的限制。
8.静态成员变量不能在声明位置给缺省值初始化 , 因为缺省值给构造函数初始化列表的 ,静态成员变量不属于某个对象 , 不走构造函数初始化列表 。
这里我们思考几个问题:
1)能在声明静态成员变量里面给缺省值吗 ?
----> 不能 , 因为缺省值是给构造函数初始化列表的 ,静态成员变量不属于某个对象 , 不走构造函数初始化列表 。
2)面试题:实现一个类,计算程序中创建出了多少个类对象。
#include <iostream>
using namespace std;
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;
int main()
{
cout << A::GetACount() << endl;
A a1, a2;
A a3(a1);
cout << A::GetACount() << endl;
cout << a1.GetACount() << endl;
return 0;
}
三、友元
我们在中篇也说过友元哈函数,友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元分为:友元函数和友元类
那么,为什么需要友元呢?
之前我们尝试去重载operator<<,然后发现没办法将operator<<重载成成员函数。因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以要把operator<<重载成全局函数。但又会导致类外没办法访问成员,此时就需要友元来解决。
特性:
1.友元提供了一个突破类访问限定符封装的方式 ,
2.声明或者类声明的前面加 friend , 并且把友元声明放在一个类的里面 。
3.外部友元函数可以访问类的私有和保护成员 , 友元函数仅仅是一种声明 , 并不是类的成员函数 。
4.友元函数可以在类定义的任何地方声明 , 不受访问限定符的限制 。
5.一个函数可以是多个类的友元函数
6.友元类中的成员函数都可以是另一个类的成员函数 , 都可以访问另一个类的私有和保护成员 。
7.友元类的关系是单向的 , 不具有交换性 , 比如A类是B类的友元 , 但是B类不是A类的友元
8.友元类的关系不能传递 , 如果A是B的友元 , B是C的友元 , 但是A不能是C的友元
9.友元提供了便利,但是会增强耦合度 , 破环了封装 , 所以友元不易多用
下面是使用示例:
#include<iostream>
using namespace std;
// 前置声明,否则A的友元函数声明编译器不认识B
class B;
class A
{
// 友元声明
friend void func(const A& aa, const B& bb);
private:
int _a1 = 1;
int _a2 = 2;
};
class B
{
// 友元声明
friend void func(const A & aa, const B & bb);
private:
int _b1 = 3;
int _b2 = 4;
};
void func(const A & aa, const B & bb)
{
cout << aa._a1 << endl;
cout << bb._b1 << endl;
}
int main()
{
A aa;
B bb;
func(aa, bb);
return 0;
}
四、内部类
1.如果一个类定义在另一个类的内部 , 这个内部类就叫做内部类 。 内部类是一个独立的类 , 跟定义在全局相比 , 他只是 受外部类类域限制 和 访问限定符限制 , 所以外部类定义的对象中不包含内部类 。
2.内部类默认是外部类的友元类 。
3.内部类本质也是一种封装 , 当 A类 与 B类紧密关联 , A类实现出来主要就是给B类使用 , 那么可以考虑把A类设计为 B 的内部类 , 如果放在private / protected 位置 , 那么 A 就是 B 类的专属内部类 , 其他地方都用不了了 。
1. 内部类可以定义在外部类的public、protected、private都是可以的。
2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
3. 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; //OK
cout << a._h << endl; //OK
}
};
};
int A::_k = 1;
int main()
{
cout << sizeof(A) << endl;
return 0;
}
五、匿名对象
1.用 类型(实参)定义出来的对象叫做匿名对象 , 相比之前我们定义的类型 对象名(实参) 定义出来的叫有名对象
2.匿名对象生命周期只在当前一行 , 一般临时定义一个对象当前用一下就可以了 , 这种情形就可以使用匿名对象 。注意 : 匿名对象 和 临时对象一样 都具有常性
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;
}
};
void func1(int i = 0)
{
}
//匿名对象可以作为缺省值给对象
void func(const A& aa = A())
{
}
int main()
{
A aa1;
// 不能这么定义对象,因为编译器⽆法识别下⾯是⼀个函数声明,还是对象定义
//A aa1();
// 但是我们可以这么定义匿名对象,匿名对象的特点不⽤取名字,
// 但是他的⽣命周期只有这⼀⾏,我们可以看到下⼀⾏他就会⾃动调⽤析构函数
A();
A(1);
A& ret1 = aa1;
//const 引用会延长匿名对象的生命周期,匿名对象跟者引用走
const A& ret2 = A();
A aa2(2);
// 匿名对象在这样场景下就很好⽤,当然还有⼀些其他使⽤场景,这个我们以后遇到了再说
Solution().Sum_Solution(10);
return 0;
}
六、对象拷⻉时的编译器优化
• 现代编译器会为了尽可能提高程序的效率,在不影响正确性的情况下会尽可能减少⼀些传参和传返回值的过程中可以省略的拷贝。
• 如何优化C++标准并没有严格规定,各个编译器会根据情况⾃⾏处理。当前主流的相对新⼀点的编译器对于连续⼀个表达式步骤中的连续拷⻉会进⾏合并优化,有些更新更"激进"的编译器还会进行跨行跨表达式的合并优化。

接下来我们就在不同编译器中看一下具体区别

第一列是 g++ test.cpp -fno-elide-constructors,表示禁用返回值优化。
七. 类和对象总结
现实生活中的实体计算机并不认识,计算机只认识二进制格式的数据。如果想要让计算机认识现实生活中的实体,用户必须通过某种面向对象的语言,对实体进行描述,然后通过编写程序,创建对象后计算机才可以认识。比如想要让计算机认识洗衣机,就需要:
1. 用户先要对现实中洗衣机实体进行抽象---即在人为思想层面对洗衣机进行认识,洗衣机有什么属性,有那些功能,即对洗衣机进行抽象认知的一个过程
2. 经过1之后,在人的头脑中已经对洗衣机有了一个清醒的认识,只不过此时计算机还不清楚,想要让计算机识别人想象中的洗衣机,就需要人通过某种面相对象的语言(比如:C++、Java、Python等)将洗衣机用类来进行描述,并输入到计算机中
3. 经过2之后,在计算机中就有了一个洗衣机类,但是洗衣机类只是站在计算机的角度对洗衣机对象进行描述的,通过洗衣机类,可以实例化出一个个具体的洗衣机对象,此时计算机才能洗衣机是什么东西。
4. 用户就可以借助计算机中洗衣机对象,来模拟现实中的洗衣机实体了。
所以,也就是说类是对某一类实体(对象)来进行描述的,描述该对象具有那些属性,那些方法,描述完成后就形成了一种新的自定义类型,才用该自定义类型就可以实例化具体的对象。


3878

被折叠的 条评论
为什么被折叠?



