文章目录
类和对象------下
隐式类型转换
介绍
隐式类型转换 = 编译器自动帮你做的类型转换,不需要你写强制转换语法。一般来说常见的隐式类型转换一般就那几种.
常见的隐式类型转换
1关系密切可以转换 :比如int和double都是表示大小的数字,他们之间关系密切可以进行隐式类型转换
2构造时的隐式类型转换:
单变量
来看下面的代码,当只有一个成员变量时我们可以直接用=初始化
cpp
`class A
{
public:
A(int a )
{
_a = a;
}
private:
int _a = 0;
};
int main()
{
A a1(1);
A a2 = 1;//隐式类型转换
return 0;
}`
其中涉及到的隐式类型转换的逻辑如下图:

从语法的角度上来说,逻辑是像上面这样的,但是当有连续的构造出现的时候编译器会进行优化,所以在调试的时候,是直接进行构造的。
多变量
转换的逻辑是类似的,只是形式不同,用=={ }==多变量隐式类型转换的形式如下
cpp
class A
{
public:
A(int a ,int b)
{
_a = a;
_b = b;
}
private:
int _a = 0;
int _b = 0;
};
int main()
{
A a1(1,2);
A a2 = {1,2};//隐式类型转换
return 0;
}
隐式类型转换的真正用处
如下面的场景:想要A入栈,用隐式类型转换会变得更加简单。原本我们需要先定义A再入栈 ,现在有了隐式类型转换我们就可以直接入栈 了
对比如下图

cpp
class A
{
public:
A(int a=1 ,int b=1)
{
_a = a;
_b = b;
}
private:
int _a = 0;
int _b = 0;
};
class Stack
{
public:
void Push(const A& a)
{
//........
}
Stack()
{
//.......
}
private:
A a[10];
int _size;
int _capacity;
};
int main()
{
Stack st;
//我想要{3,3}入栈该怎么做?
//没有隐式类型转换
A aa1(3, 3);
st.Push(aa1);
//隐式类型转换 用{3,3}构造一个A
st.Push({3, 3});
}
再探构造函数
介绍
|------------------------------------------------------------------------------------------------------|
| 1 之前我们在实现构造函数的时候,一般是在函数体内直接赋值初始化的,其实还有另一种方法,就是用初始化列表初始化,具体形式::号开始**,**逗号,隔开,中间放成员变量后面有()里面放值或表达式 |
示例
cpp
Date(int year = 2026, int month = 5, int day = 8)
:_year(year)//year+1也行,因为()里面可以放表达式
,_month(month)
,_day(day)
{
}
|---------------------------------------------------|
| 2 每个成员在初始化列表里面只能出现一次,因为在语法上初始化列表可以认为是成员变量定义初始化的地方 |
|---------------------------------------------------------------------------------------------------------------------|
| 3 所以引用成员变量,const成员变量必须在参数列表处初始化,因为引用和const变量都要求必须在定义时初始化 ,而参数列表是成员变量定义的地方。还有没有默认构造的自定义类型也要在参数列表初始化,因为自定义类型也要在定义时初始化 |
|--------------------------------------------------------------|
| 尽量在初始化列表初始化,因为任何成员变量都要走初始化列表,不管你有没有明显示地写出来,初始化列表是每个成员变量定义的地方 |
成员变量的缺省值
介绍
C++11新增了对成员变量的缺省值,主要给没有初始化的成员变量用。注意要与函数的缺省值做区分:一个是未初始化时给缺省值,一个是未传参时给缺省值
cpp
private:
int _year=2026;
int _month=5;
int _day=17;
初始化的顺序
初始化列表按照成员变量在声明中的顺序初始化 ,与在初始化列表中的出现顺序无关。建议声明顺序与初始化列表的顺序保持一致。下面的就是例子
我们来看一下,下面_a1和_a2的值分别是多少
cpp
class A
{
public:
A(int a)
: _a1(a)
,_a2 (_a1)
{ }
void Print()
{
std::cout << "_a1=" << _a1 << "_a2=" << _a2<<std::endl;
}
private:
int _a2 = 2;
int _a1 = 2;
};
int main()
{
A a(1);
a.Print();
}
在VS上,输出结果如下图,_a2是随机值,因为_a2初始化时_a1还没有初始化。
|-----------------------------------------------------------------------------------------------------------------------------------------------|
| 注意:有人可能会认为_a2在初始化时_a1还没有定义呀,这样不应该会报错吗?但其实不然,因为空间早就已经开好了,可以认为已经定义了,虽然我上面说到初始化列表在语法理解上可以认为是成员变量定义的地方,但也仅仅是语法上,底层其实已经早已定义好了,总之判断是否定义,就看空间是否已经开好了 |
如图

static 成员
介绍
|-----------------------------------------|
| static修饰的成员变量称为静态成员变量,只能在类外面初始化。给缺省值也不行 |
如图,直接报错

|--------------------------------------------------------------------------------|
| 静态成员变量为所有类对象所共享,不属于某个具体的对象,存放在静态区。这就解释了为什么静态成员变量只能在类外初始化了,因为它不属于任何一个具体的对象,是共有的 |
|--------------------------------------------------------------------------------|
| 可以这么认为,静态成员变量就是被类域限定了的全局变量 ,一般静态成员变量都被放在了private:里面,我们在类外面想要访问就只能通过静态成员函数来进行访问 |
|-----------------------------------------------------------------------------------------------------|
| static修饰的函数称为静态成员函数,没有this指针,所以只能访问静态成员变量。而非静态成员函数可以访问任意的静态成员变量和静态成员函数。值得一提的是静态成员函数可以直接由类名+作用域限定符访问 |
static的作用
一个很经典的作用就是计数,统计有多少个对象。如下面的代码。
输出结果是12,你们可以复制这个代码去验证一下
cpp
class A
{
public:
A(int a=1)
{
_a = a;
_count++;
}
A(const A& a)
{
_a = a._a;
_count++;
}
static const int& Count()
{
return _count;
}
private:
int _a = 1;
static int _count ;
};
int A::_count = 0;//在类外定义初始化
int main()
{
A a[12];
std::cout << A::Count() << std::endl;
}
一道经典题目
题目:链接: 牛客网:1+2+3...+n

解答:创建一个有n个类A的数组调用n次拷贝构造,从而实现n次循环

另一道经典题目
对于下面程序,调用构造函数的顺序是:() 调用析构函数的顺序是:()
cpp
C c;
int main()
{
A a;
B b;
static D d;
return 0;
}
答案:调用构造函数的顺序:C A B D
调用析构函数的顺序:B A D C
|-----------------------------------------------------------------------------------------------|
| 解释:1 全局变量初始化在main函数之前 2静态在初始化时和其他变量也没有什么不同,都是在执行到该语句时才会初始化。但是静态变量的生命周期是全局的,只有当main函数销毁时它才会销毁。 |
|------------------------------------------------|
| 3 调用析构函数的顺序为什么是B在在A的前面呢?这其实就是栈后进先出的特点,在后面的先销毁。 |
友元
介绍
|--------------------------------------------------------------------------------|
| 友元提供了一种突破访问限定符的方式,,分为:友元函数(我在前一章的<<运算符重载中用到过)和友元类 ,方式为在函数声明和类前面加friend并放到类里面 |
一个友元函数声明

友元类

(Solution是A的友元)
|----------------------------------------------|
| 外部的友元函数可以访问类里的私有成员和保护成员,友元函数仅仅是一种声明,它不是成员函数。 |
|---------------------------------------------|
| 友元函数可以在类的任何地方声明,不受访问限定符的限制,一个函数可以是多个类的友元函数。 |
|-----------------------------------|
| 友元的关系是单向的,不具有交换性,如:A是B的友元的B不是A的友元 |
|------------------------------------------|
| 友元的关系不具有传递性,如:A是B的友元,B是C的友元,但是A不一定是C的友元。 |
|-------------------------|
| 友元虽然提供了便利,但也破坏了封装,不宜多用。 |
内部类(C++中较少用到)
|-----------------------------------------------------------------------------------|
| 如果类定义在一个类的内部,那么就称为内部类,内部类是一个独立的类,跟全局的类相比,没有什么不同,只是受类域和访问限定符限制。因此外部类对象定义的时候不包含内部类。 |
我们可以用如下代码验证一下

|---------------|
| 内部类默认是外部类的友元。 |
前面的牛客网题目也可以用内部类写

匿名对象
用类型(实参)定义出来的对象称为匿名对象,用类型 变量名 (实参)定义出来的是有名对象。
以Date 举例:

|--------------------------------------------------------------------|
| 匿名对象的周期只在当前一行,一般就是临时定义出来用一下,其实就是为了方便,比如我只想调用一下类里面的函数时,就可以直接用匿名对象调用 |

编译器对构造的优化(了解即可)
对于连续的构造和拷贝构造,在一些比较新的编译器中,在不影响正确性的前提下,可能会省略一些步骤
验证
以Date类型为例
我们先在构造函数和析构函数中做一下手脚,每调用一次就打印一次,用以观察调用的次数。
如图

场景1
传值传参,构造+拷贝构造。在VS2022上未作优化

场景2
隐式类型转换。可以看到编译器直接把拷贝构造给优化掉了。

场景3
连续构造+拷贝构造,直接优化为构造。

完