一、再谈构造函数
在深度探索类的六大天选之子中,我们学习了类和对象的构造函数,知道了其可以用来初始化成员变量,也学了一些它的相关语法特性,但是C++中的构造函数真的就只是这样吗?本模块我们继续来谈谈有关构造函数的一些知识点
1、初始化列表
引入
- 我们知道,对于下面这个类A的成员变量
_a1
和_a2
属于【声明】,还没有在内存中为其开辟出一块空间以供存放,真正开出空间则是在【定义】的时候,那何时定义呢?也就是使用这个类A去实例化出对象的时候 - 这个对象的空间被开出来了,难道里面的成员变量就一定开出空间了吗?这一点我们很难去通过调试观察
class A {
public:
int _a1; //声明
int _a2;
};
int main(void)
{
A aa; // 对象整体的定义,每个成员什么时候定义?
return 0;
}
- 如果现在我在类A中加上一个const成员变量的话,初始化的时候似乎就出现了问题
const int _x;
- 在搞清楚上面的问题之前你要明白
const
修饰的变量有哪些特点
const int i;
- 可以看到我在这里定义了一个整型变量i,它前面是用
const
进行修饰的,不过编译后报出了错误说【必须初始化常量对象】,因为对于const
修饰的变量在声明的时候是必须要去进行初始化的,也就是要给到一个值
现在我们就可以来聊聊有关上面的成员变量
_x
为什么没有被初始化的原因了👇
- 之前有讲过,若是我们自己不去实现构造函数的话,类中会默认提供一个构造函数来初始化成员变量,对于【内置类型】的变量不会处理,对【自定义类型】的变量会去调用它的构造函数。那么对于这里的
_a1
、_a2
、_x
都属于内置类型的数据,所以编译器不会理睬,可是呢const
修饰的变量又必须要初始化,这个时候该怎么办呢╮(╯▽╰)╭
💬有同学说:这还不简单,给个缺省值不就好了
- 这位同学说的不错,这个办法确实是可以解决我们现在的问题,因为C++11里面为内置类型不初始化打了一个补丁,在声明的位置给到一个初始化值,就可以很好地防止编译器不处理的问题
但是现在我想问一个问题:如果不使用这个办法呢?你有其他方法吗?难道C++11以前就那它没办法了吗?
- 底下的同学确实想不出什么很好的解决办法,于是这个时候就要使用到本模块要学习的【初始化列表】了
初始化的概念区分
- 在了解【初始化列表】前,你要先知道初始化的真正含义是什么
概念:在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
- 上面这个Date类是我们之前写过的,这里有一个它的有参构造函数,虽然在这个构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化。构造函数体中的语句只能将其称为【赋初值】,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
语法格式及使用
【初始化列表】:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式
- 下面就是它的具体用法,这样便可以通过外界传入一些参数对年、月、日进行初始化
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
int main(void)
{
Date d(2023, 3, 30);
return 0;
}
可以通过调试来观察一下它到底是怎么走的
接下去我再来说说这一块的难点所在,准备好头脑风暴🌊
- 还是看回到我们上面的这个类A,知道了【初始化列表】这个东西,此时就不需要再声明的部分给缺省值了,直接使用初始化列表即可。不过可以看到,对于
_a1
和_a2
我给到了缺省值,写了初始化列表后,它们还会被初始化吗?
class A {
public:
A()
:_x(1)
{}
private:
int _a1 = 1; //声明
int _a2 = 1;
const int _x;
};
也通过调试来看一下
-
可以看到,即使在初始化列表没有给到
_a1
和_a2
的初始化,还是会通过给到的默认缺省值去进行一个初始化。根据上面所学,我给出以下的结论- 哪个对象调用构造函数,初始化列表是它所有成员变量定义的位置
- 不管是否显式在初始化列表写,编译器都会为每个变量在初始化列表进行初始化
好,接下去难度升级,请问初始化列表修改成这样后三个成员变量初始化后的结果会是什么呢? 会是1、2、1吗?
class A {
public:
A()
:_x(1)
,_a2(1)
{}
private:
int _a1 = 1; //声明
int _a2 = 2;
const int _x;
};
一样通过调试来看看
- 可以观察到,最后初始化完后的结果为1、1、1,最令你困惑的应该就是这个
_a2
了,因为我在声明的时候给到了缺省值,然后初始化列表去进行定义的时候又去进行了一次初始化,最后的结果以初始化列表的方式为主 - 这里要明确的一个概念是,缺省参数只是一个备份,若是我们没有去给到值初始化的话,编译器就会使用这个初始值,若是我们自己给到了明确的值的话,不会去使用这个缺省值了【如果不清楚看看C++缺省参数】
接下去难度继续升级,请问下面这样初始化后的结果是多少?
- 可以看到对于构造函数我不仅写了【初始化列表】,而且在函数体内部还对
_a1
和_a2
进行了++和- -,那此时会有什么变化呢?
class A {
public:
A()
:_x(1)
,_a2(1)
{
_a1++;
_a2--;
}
private:
int _a1 = 1; //声明
int _a2 = 2;
const int _x;
};
如果对于上面的原理搞清楚了,那看这个就相当于是再巩固了一遍。也是一样,无论是否给到缺省值都会去初始化列表走一遍,若是构造函数内部有语句的话就会执行
注意事项
清楚了初始化列表该如何使用,接下去我们来说说其相关的注意事项
-
每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
- 可以看到,若是一个成员变量在初始化列表的地方出现了两次,编译器在编译的时候就会报出【xxx已初始化】
- 类中包含以下成员,必须放在初始化列表位置进行初始化:
-
const成员变量
- 这个在前面已经说到过了,
const
修饰的成员变量和构造函数对于内置类型不做处理产生了一个冲突,因此祖师爷就提出了【初始化列表】这个概念
- 这个在前面已经说到过了,
-
引用成员变量
- 第二点就是对于引用成员变量,如果有点忘记了看看C++引用
- 通过编译可以看出,这个引用型成员变量
_z
需要被初始化,它必须要引用一个值
-
没有默认构造的自定义类型成员(写了有参构造编译器就不会提供默认构造)
- 此时,我又写了一个类B,将它定义出的对象作为类A的成员变量,在类B中,有一个无参的默认构造,也写了相关的初始化列表去初始化
_b
- 此时,我又写了一个类B,将它定义出的对象作为类A的成员变量,在类B中,有一个无参的默认构造,也写了相关的初始化列表去初始化
class B {
public:
B()
:_b(0)
{}
private:
int _b;
};
class A {
public:
A()
:_x(1)
,_a1(3)
,_a2(1)
,_z(_a1)
{
_a1++;
_a2--;
}
private:
int _a1 = 1; //声明
int _a2 = 2;
const int _x;
int& _z;
B _bb;
};
- 通过调试来观察就可以看到,完全符合我们前面所学的知识,若是当前类中有自定义类型的成员变量,那在为其进行初始化的时候会去调用它的默认构造函数
- 但是现在我对这个构造函数做了一些改动,将其变为了有参的构造函数,此时编译时就报出了【没有合适的默认构造函数可用】
- 我们知道默认构造有:无参、全缺省和编译器自动生成的,都是不需要我们手动去调的。可以看到若是我在这里将其改为全缺省的话,就不会出问题了,因为它属于默认构造函数
💬那对于有参构造该如何去初始化呢?
- 还是可以利用到我们的【初始化列表】
通过调试来看看编译器是如何走的
- 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化
看完了上面这一种,我们再来看看稍微复杂一些的自定义类型是否也遵循这个规则
- 也就是我们之前写过的Stack和MyQueue类
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 10) //全缺省构造
{
cout << "Stack()构造函数调用" << endl;
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_size = 0;
_capacity = capacity;
}
//....
private:
DataType* _array;
size_t _size;
size_t _capacity;
};
- 此处我们主要观察Stack类的构造函数,因为在MyQueue中我没有写构造函数,为的就是使用它默认生成的构造函数去进行初始化。对于【内置类型】不做处理,不过我这里给到了一个缺省值,对于【自定义类型】会去调用它的默认构造
class MyQueue{
public:
//默认生成构造函数
private:
Stack _pushST;
Stack _popST;
size_t _t = 1;
};
int main(void)
{
MyQueue mq;
return 0;
}
可能读者有所忘却,我们再通过调试来看一下
- 可以观察到在初始化MyQueue类的对象时,因为内部有两个Stack类型的对象,所以就会去调用两次Stack类默认构造来进行初始化
- 那此时我若是将这个默认构造(全缺省构造)改为有参构造吗,它还调得动吗?
Stack(size_t capacity)
- 可以看到,此时就报出了我们上面有类似遇到过的【无法引用默认构造函数】,为什么呢?原因就在于我们写了,编译器自动生成的也就不存在了,但是我又没有传入对应的参数
- 此时就可以使用到我们本模块所学习的【初始化列表】了,将需要定义的值放在初始化列表,相当于就是为Stack类传入了一个有参构造的参数,不过对于没有写在这里的
_t
,依旧会使用我给到的初始值1
MyQueue()
:_pushST(10)
,_popST(10)
{}
可以通过调试再来看看
- 当然,如果你觉得不想要这个固定的10作为栈容量的话,也可以将这个MyQueue的构造函数设定为有参,自己传递进去也是可以的
- 最后再来看一下无参构造,也是默认构造的一种,在这里编译器也会去走MyQueue的初始化列表进行初始化
//无参构造
MyQueue()
{}
所以可以看出,对于【内置类型】不做处理,【自定义类型】会调用它的默认构造可以看出其实就是当前类构造函数的初始化列表在起作用
在看了MyQueue类各种初始化列表的方式后,其实也可以总结出一点,无论如何不管有没有给到缺省值,只要是显式地写了一个构造函数,就可以通过调试去看出编译器都会通过【初始化列表】去进行一个初始化
- 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
- 最后再来看第四点,你认为下面这段代码最后打印的结果会是多少呢?1 1 吗?
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();
}
但结果却和我们想象的不一样,_a1
是1,_a2
却是一个随机值,这是为什么呢?
- 通过调试可以发现,似乎是先初始化的
_a2
再去初始化的_a1
,对于【内置类型】我们可以知道是编译器是不会去进行初始化的,那若是一开始使用_a1
去初始化_a2
的时候,那_a2
就会是一个随机值,但是_a1
却使用传入进来的形参a进行了初始化,那它的值就是1
- 此时我们只需要让
_a1
先进行初始化即可,就不会造成随机值的现象了
现在你在翻上去把所有的调试图一幅幅看下来就可以发现出丝滑列表是存在顺序的,它的顺序不是在列表中谁先谁后的顺序,而是类的成员变量声明的顺序
2、explict关键字
👉构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用
单参构造函数
- 还是老朋友,我们通过下面这个日期类进行讲解
class Date
{
public:
Date(int year)
:_year(year)
{}
private:
int _year;
int _month = 3;
int _day = 31;
};
- 对于下面的d1很清楚一定是调用了有参构造进行初始化,不过对于d2来说,也是一种构造方式
int main()
{
Date d1(2022);
Date d2 = 2023;
return 0;
}
依旧通过调试来看就会非常清晰,这种写法也会去调用构造函数
- 在操作符章节,我有提到过【隐式类型转换】这个概念,像下面将一个int类型的数值赋值给到一个double类型的数据,此时就会产生一个隐式类型转换
int i = 1;
double d = i;
- 对于类型转换而言,我在C++引用一文中也有提到过,这里并不是将值直接赋值给到左边的对象,而是在中间呢会产生一个临时变量,例如右边的这个
i
会先去构造一个临时变量,这个临时变量的类型是[double]
。把它里面的值初始化为1,然后再通过这个临时对象进行拷贝构造给d
,这就是编译器会做的一件事 - 那对于这个d2其实也是一样,2023会先去构造一个临时对象,这个临时对象的类型是
[Date]
把它里面的year初始化为2023,然后再通过这个临时对象进行拷贝构造给到d2
,
💬小陈:不是说构造函数有初始化列表吗?拷贝构造怎么去初始化呢?
//拷贝构造
Date(const Date& d)
:_year(d._year)
,_month(d._month)
,_day(d._day)
{}
- 同学,别忘了【拷贝构造】也是属于构造函数的一种哦,也是会有初始化列表的
刚才说到了中间会产生一个临时对象,而且会调用构造 + 拷贝构造,那此时我们在Date类中写一个拷贝构造函数,调试再去看看会不会去进行调用
- 很明显没有,我在进入Date类后一直在按F11,但是却进不到拷贝构造中,这是为什么呢?
- 原因其实在于编译器在这里地方做了一个优化,将【构造 + 拷贝构造】优化成了【一个构造】,因为编译器在这里觉得构造再加拷贝构造太费事了,干脆就合二为一了。其实对于这里的优化不同编译器是有区别的,像一下VC++、DevC++可能就不会去优化,越是新的编译器越可能去进行这种优化。在本文的最后一个模块我还会详细展开分析
💬小叶:但您是怎么知道中间赋值这一块产生了临时对象呢?如果不清楚编译器的优化机制这一块肯定就会认为这里只有一个构造
- 这点确实是,若是我现在不是直接赋值了,而是去做一个引用,此时会发生什么呢?
Date& d3 = 2024;
- 可以看到,报出了一个错误,原因就在于d3是一个Date类型,2024则是一个内置类型的数据
- 但若是我在前面加一个
const
做修饰后,就不会出现问题了,这是为什么呢?
-
其实这里的真正原因就在于产生的这个【临时变量】,它就是通过Date类的构造函数构造出来的,同类型之间可以做引用。还有一点就是临时变量具有常性,所以给到一个
const
类型修饰对象不会有问题
但若是你不想让这种隐式类型转换发生怎么办呢?此时就可以使用到C++中的一个关键字叫做
explicit
- 它加在构造函数的前面进行修饰,有了它就不会发生上面的这一系列事儿了,它会【禁止类型转换】
explicit Date(int year)
:_year(year)
{}
多参构造函数
对于上面所讲的都是基于单参的构造函数,接下去我们来瞧瞧多参的构造函数
//多参构造函数
Date(int year, int month ,int day = 31)
:_year(year)
,_month(month)
,_day(day)
{}
- 根据从右往左缺省的规则,我们在初始化构造的时候要给到2个参数,
d1
没有问题传入了两个参数,但是若是像上面那样沿袭单参构造函数这么去初始化还行得通吗?很明显不行,编译器报出了错误
💬小冯:那要怎么办呀,对于一定要传入多参数进行构造的场景
-
这个时候就要使用到我们C++11中的新特性了,在对多参构造进行初始化的时候在外面加上一个
{}
就可以了,可能你觉得这种写法像是C语言里面结构体的初始化,但实际不是,而是在调用多参构造函数
Date d2 = { 2023, 3 };
- 不仅如此,对于下面这种也同样适用,调用构造去产生一个临时对象
const Date& d3 = { 2024, 4 };
那要如何去防止这样的隐式类型转换发生呢,还是可以使用到
explicit
关键字吗?
//多参构造函数
explicit Date(int year, int month ,int day = 31)
:_year(year)
,_month(month)
,_day(day)
{}
- 可以看到,加上
explicit
关键字做修饰,同样可以起到【禁止类型转换】的作用
- 还有一种例外,当缺省参数从右往左给到两个的时候,此时只需要传入一个实参即可,那也就相当于是单参构造
explicit
关键字依旧可以起到作用·
explicit Date(int year, int month = 3,int day = 31)
:_year(year)
,_month(month)
,_day(day)
{}
-
所以对于可读性不是很好的代码,可以使用
explicit
修饰构造函数,将会禁止构造函数的隐式转换
二、static成员
【概念】:声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化
1、面试题引入
💬面试题:实现一个类,计算程序中创建出了多少个类对象
- 上面这个是曾经一家公司的面试题,要你用一个类去计算创建出多少个对象。分析一下可以知道我们去实例化出一个对象的时候,无非是调用构造或者是拷贝构造,或者是通过一些传参返回的方式去构造对象
- 那第一时间就会想到在全局定义一个
count
,然后在可能产生构造的地方进行累加
int count = 0;
class A {
A()
{
count++;
}
A(const A& a)
{
count++;
}
};
void func(A a)
{
count++;
}
int main(void)
{
A aa1;
A aa2(aa1);
func(aa1);
return 0;
}
但是编译后可以发现,count++
的地方都出现了报错,说是不明确的问题
- 但是你仔细去看输出窗口的话可以发现其实这个
count
是和【std库】中的count发生了冲突
那这个时候该怎么办呢?
💬有同学说:这还不简单,不要写成count不就可以了,改成Count
都可以
- 确实,这不乏是一种直截了当的方法。但是同学,我们现在学习的是C++而不是C语言,改成大写的这种方式是C语言思路
- 还记得我们在细谈namespace命名空间说到的部分展开命名空间吗?和std库里发生冲突就是因为
using namespace std;
展开了std的命名空间,在这里我们只需要部分展开即可
using std::cout;
using std::endl;
此时通过调试再去观察就可以看出创建了多少个对象
- 但这个时候呢,我手比较欠缺,对这个
count
又去++了几下,此时最后的结果就出现问题了
2、static特性细述
可以看到对于上面这种问题在C语言中是无法避免的,因为count是一个全局变量,那么它的生命周期就是从定义开始到main函数结束的时候销毁,这任何地方都是可以访问到的,并且它还不具有常性可以做任意修改,这其实也就缺乏了一定的安全性
- 于是呢,在C++中就引入了这样一个东西,把count作为类的成员函数
class A {
public:
A(int a = 0)
{
count++;
}
A(const A& a)
{
count++;
}
private:
int count = 0;
};
- 但是对于这个count而言还是属于某个对象的,但我们若要去统计的话它一定不是专属于某个对象的 ,而是要属于所有对象。此时我们就可以用
static
来修饰一下这个成员变量
static int count = 0;
- 此时这个count便被包裹在了这个类中,我们知道类也是一个作用域,可以起到隔绝外界的作用,那么此时我们的count就不会和库里面的count冲突了,直接展开std命名空间也不会存在问题
- static其实我们在C语言中也有学到过,若一个变量被
static
修饰的话,它就不会放到在【栈区】了,而是在【静态区中】
此时就引出了static的第一条特性如下👇
- 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
但此刻我为其设定缺省值的时候却报出了这样的错误,似乎不能在这里为它初始化呢?
- 上面刚学习了构造函数的【初始化列表】,在类内直接定义是因为缺省值是给到构造函数的初始化列表用的,初始化列表初始化的是非静态成员,是属于当前对象的;而静态成员是属于所有对象的,是共有的
- 所以我们考虑把它放到全局去进行一个定义,但是出了当前作用域又无法访问,此时就可以使用我们学习过的域作用限定符
::
int A::count = 0;
那么就引出了static
的第二条特性👇
- 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
那此时我若是要在外界去访问一下这个静态成员呢?能不能做到
- 可以看到,直接打印访问是不可以的,因为需要域作用限定符
::
- 不过呢,加上域作用限定符
::
又说它是私有的无法访问
那么就引出了static
的第三条特性👇
- 静态成员也是类的成员,受public、protected、private 访问限定符的限制
那要怎么去访问呢?这里有两种方式
- 将count改为公有的之后就可以通过下面这两种方式去访问类中的静态成员变量
cout << A::count << endl;
cout << aa1.count << endl;
cout << aa2.count << endl;
【拓展一下】
- 对于这种方式也是可以的,如果你有认真看过详解类封装中的this指针就可以知道下面这种做法就是为当前的this指针传递了一个空的地址,虽然我们看到了
->
,但其实并没有去产生一个解引用,因为count是一个静态成员变量,虽然形式上放在类内,但上面说过了它是存放在内存中的静态区,所以无法用this指针访问到这个count
A* aa3 = nullptr;
aa3->count;
那么就引出了static
的第四条特性👇
- 类静态成员即可用
[类名::静态成员]
或者[对象.静态成员]
这些指令在代码段
上面这样都可以访问是因为我将静态变量count设置成了公有,若一旦设置为私有的话,上面这些访问形式就都是非法的了
- 此时我们可以在类中写一个对外公开的接口,那么外界就可以通过这个接口访问到类内的私有成员变量了,在Java里面很喜欢这么去写,不过在C++中一般很少这样,这点我在下面的【友元】会细述
int GetCount()
{
return count;
}
可以看到成员函数都是用对象去调的,那我现在假设我没有对象呢【博主确实没有🐶】。此时还有办法获取到类内的静态成员变量吗?
- 此时就要使用到一个静态成员变量的双胞胎兄弟 —— 【静态成员函数】
-
只需要在GetCount()成员函数前加一个
static
作为修饰即可
static int GetCount()
{
return count;
}
- 在外界还是使用域作用限定符
::
便可以访问到,可以说这个【静态成员函数】是专门为静态成员变量而生的
看来这个静态成员函数还挺厉害的,若是我现在类中又新增了一个普通成员变量,可以在里面进行访问吗?
通过运行可以看出似乎是不可以
那么就引出了static
的第五条特性👇
- 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
- 这其实很清楚,因为静态成员函数在静态区没有this指针,但普通的成员变量都是属于当前对象的,需要通过this指针来访问
3、疑难解惑
学习完上了上面这五条特性之后,来回答一下下面这三个问题吧
💬静态成员函数可以调用非静态成员函数吗?
- 这个当然也是不可以的,记住一点!在静态成员函数内部只能调用静态的成员变量或者函数
可以看到静态成员函数也是调用静态成员函数的
💬非静态成员函数可以调用类的静态成员函数吗?
- 这个是可以的,因为静态成员函数是公共的,公共的大家当然都可以用。包括像count也是可以使用的,这里不做演示
💬请问静态成员函数存放在哪里,静态区还是公共代码区?
- 答案揭晓,静态成员函数是存放在公共代码段的,我们在类和对象的封装思想中有谈到过,在一个类中对于成员变量而言和对象一起存放在【栈区】,但对于类内的成员函数而言,则不属于类,而是存放在【公共代码区】,那对于静态成员函数来说也是属于成员函数的一种,在编译过后都会被解析成指令,这些指令都是存放在公共代码区的
- 而对于静态成员变量来说是存放在静态区的,若是你去计算一下类的大小其实就一目了然了
4、在线OJ实训
在学习了C++中的静态成员相关知识后,我们通过一道在线OJ来练练手
链接:牛客JZ64 求1+2+3+…+n
1. 题目描述
2. 思路分析
- 来分析一下这道题该怎么去做,题目本身要求的意思很简单,就是求解1 ~ n的阶乘,但是限制条件有很多,例如:不能用分支判断、循环、条件判断、乘除法等等,这就令很多同学抓耳挠腮了,这要怎么去写呀?
💬有同学说:这还不简单,用个递归呗
- 递归是我们求解阶乘以及前n项和最常见的,不过既然是递归,那一定存在递归出口,那你肯定得去用if条件判断一下是否到底了递归出口吧
在同学们冥思苦想后,有一位同学提到了我们刚学习的static成员,那我们就用它来试试💪
- 首先要考虑清楚,此处是有两个变量在一直递增的,一个是1 ~ n这些数,一个则是累加和,不过它们都是上一轮的基础上去进行一个递增,我们知道对于静态成员变量只会在定义的时候初始化一次,后面每一次的值都是在上一次的基础上变化的
static int sum;
static int i;
- 所以此刻便可以将这个递增的变量设置为
i
,将累加和设置为sum
,它们均为静态变量,根据我们上面所学知识要将其在类的外部进行定义初始化
int Count::sum = 0;
int Count::i = 1;
- 可以把这个累加的逻辑放在类的构造函数中,然后考虑去实例化具有n个大小的对象数组,那么就会调用n次构造函数去进行一个累加
Count()
{
sum += i;
i++;
}
Count c[n];
- 最后在类的内部提供的一个静态成员函数,外界便可以通过它来获取到静态成员变量
sum
static int GetSum()
{
return sum;
}
return Count::GetSum();
3. 代码展示
最后展示一下整体代码和运行结果
class Count{
public:
Count()
{
sum += i;
i++;
}
static int GetSum()
{
return sum;
}
private:
static int sum;
static int i;
};
int Count::sum = 0;
int Count::i = 1;
class Solution {
public:
int Sum_Solution(int n) {
Count c[n];
return Count::GetSum();
}
};
5、有关static修饰变量的一些注意要点
说完static修饰成员变量和成员函数,这里再来补充一点有关static修饰变量的注意点,我们主要通过题目来进行讲解
- 有一个类A,其数据成员如下: 则构造函数中,成员变量一定要通过初始化列表来初始化的是:( )
class A {
private:
int a;
public:
const int b;
float* &c;
static const char* d;
static double* e;
};
A.a b c
B.b c
C.b c d e
D.b c d
E.b
F.c
【答案】:B
【解析】:
对初始化列表不了解的可以先看看C++ | 谈谈构造函数的初始化列表
- 对于【const成员变量】、【引用成员变量】、【无默认构造函数的自定义类型】都是必须通过初始化列表进行初始化的,因此
b
、c
很明确要选上,对于d
而言,虽然它有const修饰,但前面又有[static]
作为修饰,所以是一个静态成员函数,不属于类,存放在静态区中,当程序开始执行的时候就被初始化了,对于e
而言也是同理,所以答案选择B
- 设已经有A,B,C,D4个类的定义,程序中A,B,C,D析构函数调用顺序为?( )
C c;
int main()
{
A a;
B b;
static D d;
return 0;
}
A.D B A C
B.B A D C
C.C D B A
D.A B D C
【答案】:B
【解析】:
这题的话通过调试来看一下就很清楚了,主要是观察static
的修饰对局部变量的作用域和生命周期的更改
- 可以观察到,因为对象d有static修饰,它的生命周期发生了变化,本来应该是最早析构的,却等到了b和a析构完了之后它才去析构,所以生命周期发生了延长,但还是比最先定义出来的对象d先析构,因为d后于c被实例化出来
- 虽然它的生命周期发生了变化,但是作用域却没有发生改变,从下图可以看出在fun()函数中访问不到main函数中定义的对象d
-
在一个cpp文件里面,定义了一个static类型的全局变量,下面一个正确的描述是:( )
A. 只能在该cpp所在的编译模块中使用该变量
B. 该变量的值是不可改变的
C. 该变量不能在类的成员函数中引用
D. 这种变量只能是基本类型(如int,char)不能是C++类类型
【答案】:A
【分析】:
- 首先那来看一下本题的代码,定义了一个静态的全局变量c,还有两个类,class A 和 class B
static int c = 1;
class A {
public:
A()
{
cout << "A的构造函数" << endl;
}
void func()
{
cout << "A的成员函数" << endl;
cout << c << endl;
}
~A()
{
cout << "A析构函数" << endl;
}
private:
int _a = 1;
};
class B {
public:
B()
{
cout << "B的构造函数" << endl;
}
void func()
{
cout << "B的成员函数" << endl;
}
~B()
{
cout << "B析构函数" << endl;
}
private:
int _b = 1;
};
- 然后通过一些测试看到,静态全局变量是可以更改的,而且可以在类的成员函数中进行调用,并且看大这个
static B b
就可以知道其也可以为自定义类型即C++的类类型
- 但是对于A选项确实就是这样,涉及到一些链接属性的解析,可以看看这篇文章
【总结一下】:
-
做了上面的三题,我们来总结一下:
- 对于
static
修饰的成员变量,存放在静态区,而不在栈区,是不属于当前类的,因此需要在类外初始化 - 对于
static
修饰的局部变量,其生命周期会延长,但作用域不会发生变化 - 对于
static
修饰的全局变量,只在当前编译模块中(即当前文件内)生效,其他文件不可访问。因此其作用域发生了变化,但是生命周期没有变化(从定义到结束都不会被释放)
- 对于
三、匿名对象
顺着上面这道OJ题,顺带讲一个语法,就是这个【匿名对象】
- 把上面这段代码放到VS上来进行讲说,不过有一点VS上是会报错的,也就是这个
Count c[n]
,这是C99里面的变长数组,在VS上是不支持的,语言可以提出规范,但是编译器支不支持是编译器自己的选择 - 可能是微软在VS里面编译器这一块的开发,即使是比较新一点的VS都是不支持C99标准的,之前在C语言中也有带大家看过,不过Linux下gcc是支持C99标准的,读者有兴趣可以自己去试一试,这里不做演示
int Sum_Solution(int n) {
//Count c[n];
Count* c = new Count[n];
return Count::GetSum();
}
- 把OJ上的题目放到VS里调试,对类来说就是实例化出它的一个对象,然后用对象去调用我们刚才实现的主接口函数即可,接收一下返回值【LeetCode转VS调试技巧】
int main(void)
{
Solution s;
int ret = s.Sum_Solution(100);
cout << ret << endl;
return 0;
}
但是你觉得为了调用一个函数而去定义一个对象,不觉得浪费资源吗?如果可以不实例化对象就调用函数该多好O(∩_∩)O
- 这里就可以使用到本模块所要介绍的【匿名对象】了,
【语法格式】:类名()
int ret = Solution().Sum_Solution(100);
- 还有一个,像下面这种
A(n)
返回的其实也是一个匿名对象,没有实例化出一个对象名
A func(int n)
{
return A(n);
}
除了使用方法,还有一点要牢记的是:匿名对象的声明周期只有一行
- 普通对象析构
2. 匿名对象析构
- 通过观察便可以发现,对于普通对象来说,是在当前main函数即将销毁的时候才去调用的析构函数析构实例化出的对象;但是对于匿名对象而言,则是在调用函数结束后便会去调用其析构函数
四、友元
接下去我们来聊聊有关C++中友元的一些知识
温馨提示:友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
1、友元函数
对于友元函数来说,我在Date日期类综合实战中有讲起过
- 因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以要将
operator<<
重载成全局函数。但又会导致类外没办法访问成员,此时就需要友元来解决。operator>>
同理 -
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加
friend
关键字
class Date
{
friend ostream& operator<<(ostream& _cout, const Date& d);
friend istream& operator>>(istream& _cin, 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;
}
istream& operator>>(istream& _cin, Date& d)
{
_cin >> d._year;
_cin >> d._month;
_cin >> d._day;
return _cin;
}
int main()
{
Date d;
cin >> d;
cout << d << endl;
return 0;
}
以下是它的一些特性【了解一下即可,友元这一块其实用太多不好】
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰
- 因为友元函数只是一个全局函数,不属于类的成员函数,所以它没有隐藏的this指针,而
const
修饰的就是this指针,只有非静态的成员函数才能用const修饰
- 因为友元函数只是一个全局函数,不属于类的成员函数,所以它没有隐藏的this指针,而
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 比如说一个函数需要访问多个类中的私有成员,那可以在那几个类中设置这个函数为他们的友元函数,这样就都可以访问了
- 友元函数的调用与普通函数的调用原理相同
2、友元类
除了友元函数以外,还有一个东西叫做友元类,也就是一个类也可以是另一个类的友元
- 例如下面有一个Tiime和Date类,在Date类中呢有一个Time类的对象,然后在
SetTimeOfDate
的这个函数中初始化Time类中的三个成员变量,可是呢_hour
、_minite
、_second
是属于Time类中私有的成员变量,那Date类要如何访问到呢? -
此时就可以使用【友元类】了,
friend class Date
表示在Time类中声明Date类是我的友元类,它可以访问我的私有成员变量
class Time
{
friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
public:
Time(int hour = 0, int minute = 0, int second = 0)
: _hour(hour)
, _minute(minute)
, _second(second)
{}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
void SetTimeOfDate(int hour, int minute, int second)
{
// 直接访问时间类私有的成员变量
_t._hour = hour;
_t._minute = minute;
_t._second = second;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
然后来说说友元类的一些特性
-
友元关系是单向的,不具有交换性
- 【单向】这个词很重要,一样还是上面的Date类,因为在Time类中声明了其为它的友元类,但是在Date类中没有声明Time为它的友元类,所以Time是无法访问到Date类中私有成员的
- 若是我在Date类中声明Time类为它的友元类,此时访问就不会受限了
可能以上面这样去理解太枯燥了,我通过几个现实生活中的小案例来
- 对于友元类的这个【单向性】你可以看作是微博粉丝的关系,我是你的粉丝,我可以看你的内容,但是你不能看我的内容
- 读者大部分应该都是大学生,也可能会谈恋爱,我们知道在谈恋爱的时候可能会有一方出现【单相思】,也就是我喜欢你,但是你不喜欢我,这也是一种单向性
- 就像是舔狗一样,一个男生很喜欢一个女生,但是那个女生呢不喜欢他,所以只好当舔狗🐶
好,回归正题,我们继续来说友元类的一些特性
-
友元关系不能传递
- 这个很简单,比方说如果C是B的友元, B是A的友元,则不能说明C时A的友元
- 小C是小B的朋友,小A是小B的朋友,那可不能说明小C是小A的朋友哦🙅
-
友元关系不能继承
- 这一块我放到类的继承中细述
以上就是关于友元的一些小知识,了解一下即可,这个东西用是可以用,但是在一开始说了,友元会破坏类的封装性,因为C++使用类将成员变量和成员函数封装起来,就是为了不让外界轻易访问,但若是设置了友元的话就可以访问了,在我看来这其实是比较荒谬的
五、内部类
接下去我们来讲讲一个东西叫做【内部类】
1、概念引入
【概念】:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限
- 看到下面的类A,有一个成员变量
_a
,其内部还有一个类B,这个类B就叫做内部类
class A
{
private:
static int k;
int h;
public:
class B // B天生就是A的友元
{
private:
int _b;
public:
void foo(const A& a)
{
cout << k << endl; //OK
cout << a.h << endl; //OK
}
};
};
注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元
- 不过有一点要额外提醒的是因为类B包在类A里面,所以要去实例化类B的对象时会受到类域的限制,此时就可以使用到之前所学的域作用限定符
::
2、特性讲解
接下去再来说说它的特性
- 内部类可以定义在外部类的public、protected、private都是可以的
- 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名
cout << a.h << endl;
- sizeof(外部类)=外部类,和内部类没有任何关系
- 这点尤其要注意,对于内部类来说其实和在外部直接定义一个类是一样的,不像C语言里面的结构体那样,若是在内嵌一个结构体的话
sizeof()
的大小就会不一样了
举个例子帮助理解🏠
- 可以把内部类想想成为一个别墅小区中的个人别墅,而外部类就是这个小区。每栋别墅都是别人的私人领地,没有经过允许时不可以私自闯入的。不过小区里面的业主却可以使用小区内的各种公共设施。
- 对应的就是内部类天生就是外部类的友元,可以访问外部类的私有成员,但是外部类却不可以访问内部类中的成员
3、OJ题优化
- 对于上面在static成员中讲到的一道OJ题,其实可以把这个Count类放到Solution内部作为其内部类,而且放到private里表示这个内部类只有我能访问,其他外部的类统统不可以访问
class Solution {
private:
//作为Solution的内部类
class Count
{
public:
Count()
{
sum += i;
i++;
}
static int GetSum() {
return sum;
}
private:
static int sum;
static int i;
};
public:
int Sum_Solution(int n)
{
Count c[n];
return Count::GetSum();
}
};
- 当然对于静态成员变量这一块的初始化就要再加上一层Solution域的访问
int Solution::Count::sum = 0;
int Solution::Count::i = 1;
不过这样修改其实还不够优,可以通过我们上面说到过的,内部类天生就是外部类的友元这一特点,去再进行一度优化
- 将两个静态成员变量直接作为Solution类的成员变量,因为Count可以访问到外部类中的私有成员变量,所以不需要加
::
- 而且在最后return返回结果的时候,直接
return sum
即可,因为这就是Solution自身的成员变量,它当然可以访问,都不需要内部类向外提供静态成员函数
class Solution {
private:
//作为Solution的内部类
class Count
{
public:
Count()
{
sum += i;
i++;
}
};
static int sum;
static int i;
public:
int Sum_Solution(int n)
{
Count c[n];
return sum;
}
};
int Solution::sum = 0;
int Solution::i = 1;
六、匿名对象
接下去呢我们来谈一谈C++中的匿名对象,可能你在C语言结构体章节中听说过【匿名结构体】,但是【匿名对象】你有听说过吗?
【语法格式】:类名()
class A {
public:
A(int a)
{
cout << "A构造函数的调用" << endl;
_a = a;
}
~A()
{
cout << "A析构函数的调用" << endl;
}
private:
int _a;
};
- 首先看到这里有一个类A,然后在下面实例化了两个类A的对象,分别使用的是【有名对象】和【匿名对象】,此时就可以很明显地看出它们的区别之所在,匿名对象很明显就是没有名字
int main(void)
{
A a1(10); // 有名对象 -- 生命周期在当前函数局部域
A(20); // 匿名对象 -- 生命周期在当前行
return 0;
}
👉对于【有名对象】而言,其生命周期在当前函数局部域
👉对于【匿名对象】而言,其生命周期在当前行
- 这一块我们可以通过调试来进行观察,可以发现对于匿名对象执行完当前的这一行就直接去调用析构了,所以它的生命周期之后一行(其实不一定,看下去你就知道了)
那知道了其特性后我们便可以去用一用这个匿名对象呢
- 下面有一个类,现在我们要去调用这个类中的成员函数,你会如何去进行调用呢?
class Solution {
public:
int Sum_Solution(int n) {
cout << "Sum_Solution" << endl;
return n;
}
};
- 相信最常规的做法就是像下面这样,实例化出一个对象,然后通过
对象.函数名()
的形式进行调用
Solution s;
s.Sum_Solution(1);
- 但是呢,我直接使用下面这一种形式也可以做到,即【匿名对象】去进行调用,虽然这一种调用形式比较方便,但是呢是存在局限性的,我们只能调用这么一次,若是你想要多次调用类中的这个函数时,就需要去构造【有名对象】了,其生命周期是到程序结束为止的
Solution().Sum_Solution(2);
学习完基础的概念后我们再来看一些复杂的场景,加深对语法的理解
💬 请问下面这匿名对象可以被引用吗,快速回忆一下C++引用的相关知识
A& ra = A(1);
- 编译一下可以发现是做不到的,如果你【C++引用】这一块学习扎实的话,相信很快就能想到是 权限放大 的问题,那这里为什么会造成权限放大呢?原因就在于这个
匿名对象
,它和临时对象一样是具有常性的,那一个非常性的对象去引用常性的就会有问题了
- 此时我们只需要在前面加上一个
const
即可
const A& ra = A(1);
💬 那我现在还想问题,这一块也是涉及引用相关的知识,因为这个匿名对象
的生命周期只在这一行,那么此时这个ra
是否会变成【野引用】呢?即引用了一块已经不存在的空间?
- 马上,我们来看看结果,可以发现这个匿名对象结束完后这一行并没有像上面那样立即调用【析构函数】,而是在在程序结束之后才去调用,那有同学又产生疑惑了,这是为什么呢???
-
其实就是因为这个
const
常引用的原因,延长了匿名对象的生命周期,那么前面的对象ra
是正常的对象,对这个匿名对象进行了引用即取了别名,那么就会在程序结束的时候才进行销毁
接下去我们再来看第二块,有关STL中的
string
,没学过的同学没关系,可以看看STL容器快速入门
- 看到我这里是写了一个函数,形参部分是一个
string
的常引用
void push_back(const string &s)
{
cout << s << endl;
}
- 然后我在下面写了调用这个函数的三种形式
- 第一种是实例化对象然后去进行调用
- 第二种采用的就是我们上面所学习的【匿名对象】
- 第三种是直接传递了一个【常量字符串】进去,这中间会产生一个【临时对象】,然后进行隐式类型转换,
- 那你会更喜欢哪种方式呢,不用猜,一定是这第三种方式
// 1
string str("11111");
push_back(str);
// 2
push_back(string("22222"));
// 3
push_back("33333");
-
对于第二种的【匿名对象】和第三种的【临时对象】,它们都具有一个常属性,所以在函数的形参部分不可以是普通的引用,不然就会产生一个权限放大的问题,加上
const
后就是权限平移,此时既可以接受普通引用也可以接收常引用,提高了代码的健壮性
void push_back(string &s)
好,匿名独对象我们就讲上面这些内容,之后在深入学习STL的过程中继续补充
七、拷贝对象时的一些编译器优化
在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝,这个在一些场景下还是非常有用的
【在看本模块之前若是有读者还不了解拷贝构造机制的先去看看】
- 在上面讲
explicit
关键字的时候,我有提到下面这种写法会引发【隐式类型转换】,而且还画了对应的图示,中间会通过1构造产生一个A类型的临时对象,然后用再去调用拷贝构造完成拷贝。 - 不过这一块编译器做了一个优化,将【构造 + 拷贝构造】直接转换为【构造】的形式,本模块我们就这一块来进行一个拓展延伸,讲一讲编译器在拷贝对象时期的一些优化
A aa1 = 1;
1、传值传参
首先来看到的场景是【传值传参】,对象均是使用上面
aa1
//传值传参
void func1(A aa)
{
}
💬请你思考一下这种形式编译器还会像上面那样去优化拷贝构造吗
func1(aa1);
- 可以看到,还是发生了拷贝构造,为什么呢?因为对于这种形式编译器不会去随意优化,只有像上面在一个表达式内才会去进行一个优化。【这里的析构是针对函数内部的形参
aa
,main函数中的aa1
也会析构,不做展示】
💬那如果直接传入一个3呢,会做优化吗?
func1(3);
- 可以看到,若是直接传入3的话,就不会去调用拷贝构造了,这个其实和一开始我们讲得那个差不多,把构造 + 拷贝构造优化成了直接构造,【一开始的构造不用理他,为了构造出
aa1
对象】
💬接下去我传入一个A(3)
,会发生什么呢?
func1(A(3));
- 通过观察可以发现,和上面那个是一样的,其实读者去演算一下就可以很清楚,
A(3)
就是一个很明显的有参构造,实例化出一个对象后那就是拷贝构造,但是这里因为编译器的优化,所以直接变成了一个构造
2、传引用传参
接下去来看到的场景是【传引用传参】,传入的值还是上面的这三种,只是会通过传引用来接收
- 之前的文章里有说过为什么在传参的时候最好使用【传引用传参】,原因就是在于可以减少拷贝,提高程序运行效率
//传引用传参
void func2(const A& aa) //不加const会造成权限放大
{
}
💬那通过引用接收aa1
会发生什么呢?
func2(aa1);
- 通过观察可以发现,无论是【构造】还是【拷贝构造】,都不会去调用,这是为什么呢?
-
原因就在于这里使用的是引用接收,那么形参部分的
aa
就是aa1
的别名,无需构造产生一个新的对象,也不用去拷贝产生一个,直接用形参部分这个就可以了,现在知道引用传参的好处了吧👈
💬那直接传3呢?又会发生什么?
func2(3);
- 观察得到,临时对象还是会去进行构造,不过因为引用接收的缘故,我里面的
aa
就是这个临时对象的别名,所以无需调用拷贝构造,所以也是当回到主函数中才调用析构函数,此时析构的就是这个临时对象 - 这里要顺便提一句的是,因为这个临时对象,临时对象具有常性,所以在拷贝构造的参数中一定要使用
const
做修饰,否则就会造成权限放大
💬那么A(3)
也是和上面同样的道理
func2(A(3));
- 这里操作得有点快了,读者可以试着自己去慢慢调试观察,因为函数内部的对象
a
是外部匿名对象的引用,所以可以看到在函数内部并没有去调用析构,而是在创建匿名对象这一行结束才去调用的析构,那这两个对象就一同被析构了
看完【传值传参】和【传引用传参】,我们来总结一下
- 在为函数传递参数的时候,尽量使用引用传参,可以减少拷贝的工作
3、传值返回
接下去我们来讲讲函数返回时候编译器优化的场景,首先是【传值返回】
//传值返回
A func3()
{
A aa;
return aa;
}
💬若是直接去调用上面这个func3(),会发生什么呢?
func3();
- 这个其实和传值传参的第一个是一样的,因为在函数中对象和返回不是处于同一个表达式,所以编译器不会产生优化,调试结果如下
- 再补充一句,这里我们可以看到两个析构,第一个是func3()函数内部的局部对象
aa
的析构,第二个则是对返回的时候对那个临时对象的一个析构
💬此处在函数调用的地方我使用一个对象去做了接收,那在上面【构造 + 拷贝构造】的基础上就会再多出一个【拷贝构造】,即为【构造 + 拷贝构造 + 拷贝构造】
A aa2 = func3();
- 不过通过调试可以看出,只进行了一次拷贝构造,这里其实就存在编译器对于【拷贝构造】的一个优化工作,将两个拷贝构造优化成了一个拷贝构造
这里可能比较抽象,我画个图来解说一下
-
可以看到,因为这是一个传值返回,所以一定会在构造产生临时对象。第一个是因为
aa
与A
不是同一个表示式,所以不会引发编译器的优化;对于第二个来说,因为又拿了一个A的对象作为接收,所以又会产生一个拷贝构造。在这里编译器就要出手了,它会觉得两个拷贝构造过于麻烦,所以会直接优化成一个
拷贝构造和赋值重载的辨析
- 学习过【拷贝构造】和【赋值重载】的同学应该可以知道,它们的形式很像,若是一个对象还没有被实例化出来,使用
=
便是拷贝构造;若是一个对象已经实例化出来了,使用=
便是赋值重载
A aa2;
aa2 = func3();
- 仔细观察便可以发现,在拷贝构造完成之后又去进行了一次【赋值重载】,那看上面的代码其实就很明显了,那若是一个【拷贝构造】+【赋值重载】的话,编译器其实不会去做一个优化,那这其实相当于干扰了编译器
4、传引用返回【❌】
然后来说说【传引用返回】,不过若是你知道引用返回的一些机制的话,就可以清楚我下面这样其实是错误的,因为
aa
属于局部变量,出了当前作用域会销毁,所以不可以使用传引用返回,具体以下细述
A& func4()
{
A aa;
return aa;
}
💬首先来看下直接调用的结果会是怎样的
func4();
- 可以看到因为传引用返回了,所以就减少了中间的一份临时对象的拷贝,也就没有去调用拷贝构造
💬那我若是用一个返回值去接收的话,此时就可以看出引用返回临时对象的问题了
A aa3 = func4();
- 可以看到,最后在拷贝构造结束后,对象aa3内部的
_a
就是一个随机值
- 若是使用传值返回的话,去观察就可以发现并不是一个随机值
A func4()
5、传匿名对象返回
还记得上面讲到的【匿名对象】吗,也可以使用它返回哦,效率还不低呢!
//匿名对象返回
A func5()
{
return A(); //返回一个A的匿名对象
}
💬先调用一下看看会怎么样
func5();
- 可以看到本质还是传值返回,照理来说会构造出一个临时对象然后在拷贝构造,但是却没有调用拷贝构造,原因就是匿名对象起到的作用,对于
A()
你可以就把它看做是一个表达式,一个【构造】+【拷贝构造】就被优化成了直接构造
💬如果用返回值去接收呢?编译器会优化到何种程度
A aa4 = func5();
- 可以看到,竟然也是只有一个构造。照道理分析来看的话应该是【构造 + 拷贝构造 + 拷贝构造】,不过在匿名对象返回那里已经优化成【直接构造】了,然后再外面的【构造 + 拷贝构造】由引起来编译器的优化,所以最终就只有一个构造了
- 可以看到,最后我还去调了三次析构函数,第一次就是当然就是
aa4
,第二次是aa3
,第三次便是一开始就有的aa1
了,通过这么调试观察,希望你能真正看懂编译器的思维
而且可以观察到匿名对象返回也不会造成随机值现象,因为本质使用的还是【传值返回】,这里不可以使用【传引用返回】,因为匿名对象构建出来的也是一个临时对象,具有常性,会造成权限放大
6、小结
看完了上面这一些系列拷贝对象时编译器的优化,我们来做一个总结
函数传参总结
-
尽量使用
const
+&
传参,减少拷贝的同时防止权限放大
对象返回总结
- 接收返回值对象,尽量拷贝构造方式接收,不要赋值接收【会干扰编译器优化】
- 函数中返回对象时,尽量返回匿名对象【可以增加编译器优化】
八、再次理解类和对象
现实生活中的实体计算机并不认识,计算机只认识二进制格式的数据。如果想要让计算机认识现实生活中的实体,用户必须通过某种面向对象的语言,对实体进行描述,然后通过编写程序,创建对象后计算机才可以认识。比如想要让计算机认识洗衣机,就需要:
- 用户先要对现实中洗衣机实体进行抽象—即在人为思想层面对洗衣机进行认识,洗衣机有什么属性,有那些功能,即对洗衣机进行抽象认知的一个过程
- 经过1之后,在人的头脑中已经对洗衣机有了一个清醒的认识,只不过此时计算机还不清楚,想要让计算机识别人想象中的洗衣机,就需要人通过某种面相对象的语言(比如:C++、Java、Python等)将洗衣机用类来进行描述,并输入到计算机中
- 经过2之后,在计算机中就有了一个洗衣机类,但是洗衣机类只是站在计算机的角度对洗衣机对象进行描述的,通过洗衣机类,可以实例化出一个个具体的洗衣机对象,此时算机才能洗衣机是什么东西。
- 用户就可以借助计算机中洗衣机对象,来模拟现实中的洗衣机实体了。
举个形象点的案例,这里我在初步讲解类和对象的封装思想中也有提到,也就是外卖系统的设计
- 那么这个外卖系统中就一定存在三个类,一个是商家、一个用户,还有一个就是骑手。那我们知道对于一个类来说可以实例化出很多对象,因为全国各地都有着很多的商家、用户和骑手,当这些对象被实例化出来后,接下来要执行的就是一个匹配算法
- 也就是我在宁波,那我进到外卖平台点单的时候就会通过一些距离向量算法匹配较近一些的店,不会去匹配一些杭州、成都、西安等等的地方,不同的地方有不同的商铺。那除了距离算法之外呢,还有一些匹配算法就可以根据你日常的喜好,你喜欢吃什么?米饭 or 面条。对于用户而言有匹配算法,骑手也会存在匹配算法,肯定是给他匹配一些较近的店铺单子,就在附近几条街内的,这样也能提高效率
- 对于上面这个外卖系统,【后端】的要设计的就是这些类、功能逻辑耦合之间的关系,而距离匹配算法这些内容自然是交给【算法岗】的同学来完成,而外卖界面的设计,用户使用的界面就是交给【UI和前端】来制作
在类和对象阶段,大家一定要体会到,类是对某一类实体(对象)来进行描述的,描述该对象具有那些属性,那些方法,描述完成后就形成了一种新的自定义类型,才用该自定义类型就可以实例化具体的对象
九、总结与提炼
好,最后来总结一下本文所学习的内容📖
- 在文章的起始,我们又谈到了构造函数,在上一文学习了构造函数的基本特性后,这次我们学习的是构造函数中的一个难点,叫做【初始化列表】,面对默认构造函数对于内置类型的数据不做处理这个规则,虽然可以使用C++11中的新特性给到一个缺省值,但还是思考到了这个特性出来前此问题又是如何解决的,这就引出了初始化列表的概念。在学习了它的一些列特性和语法规约后,明白了原来声明处的缺省值就是给到初始化列表用的,无论是否有给到缺省值,编译器都会去走构造函数的初始化列表完成定义初始化的工作
- 说完初始化列表后,又研究了一会构造函数,面对隐式类型转换所带来的种种麻烦,一个【
explicit
】就可以让你服服帖帖的 - 接下去谈到了C++中的【static成员】,对于如何去计算实例化出多个个对象,确实感到有些棘手,命名空间的冲突只好将统计的变量放到类内,奇想之中就引出了
static
关键字,它可帮了大忙,但面对类内的私有成员变量,属实访问不到。此时双胞胎兄弟静态成员函数就来了,解决了我们的燃眉之急。也用这两个兄弟解决了一道在线OJ题 - 马上又来谈谈【友元】,你把我当朋友,我可不把你当朋友,单向关系可以搞清楚哦,友元虽好,然不要用多了哦,交朋友也是要适当的👈
- 藏在里面的【内部类】真是真让人琢磨不透╮(╯▽╰)╭天生就是人家的朋友的,但又不别人当朋友,这是又自闭症呀!
- 随之而来的便是【匿名对象】,取名字可太难了,博客名都是想了好久才取出来的,匿名对象真的蛮不错,值得学一学
- 对象太多可不好哦,要做一个专一的人,否则【编译器】一定会出手🗡
以上就是本文要介绍的所有内容,感谢您的阅读🌹文章来源:https://www.toymoban.com/news/detail-443922.html
文章来源地址https://www.toymoban.com/news/detail-443922.html
到了这里,关于C++ | 一些你所忽略的类和对象小知识的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!