目录
一、再谈构造函数
🌍 构造函数体赋值
在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。
class Date
{
public:
Date(int year,int month,int day)
{
_year = year;
_month = month;
_day = day;
/*_year = ++year;
_month = ++month;
_day = ++day;*/
//多次修改初值
}
private:
int _year;
int _month;
int _day;
};
上述的调用构造函数给类成员变量赋值的方法并不能称为对象成员的初始化,构造函数体中的语句只能称为赋初值。因为初始化只能初始化一次,而赋初值可以多次赋值。
🌎 初始化列表
概念
构造函数除了有函数名(类名),参数列表,函数体之外,还有初始化列表。初始化列表由一个冒号:
开始,后跟以逗号,
为分割的数据成员,使用()
给数据成员初始化。
class Date
{
public:
Date(int year,int month,int day)
:_year(year)
,_month(month)
,_day(day)
{}
private:
int _year;
int _month;
int _day;
};
特性
- 每个成员变量在初始化列表中只能出现一次,因为只能初始化一次
- 当成员变量既在初始化列表里,也有缺省参数,成员变量在初始化列表初始化
✔️缺省参数只是一个备份,如果没有给值初始化,那么编译器就会用这个缺省值初始化;若是我们给了明确的值,就不会使用这个缺省值。
(1)成员变量只有缺省数,由缺省参数赋初值
(2)成员变量只有初始化列表,在初始化列表初始化
(3)成员变量既有缺省参数又有在初始化列表,在初始化列表初始化
(4)成员变量没有缺省参数也没有在初始化列表,编译器给随机值
❔如果我在函数体中修改_x
,编译器会如何走呢?
- 对于构造函数我不仅写了初始化列表和缺省值,还在函数体中给
_x
自增1
class A
{
public:
A()
:_x(3)
{
_x++;
}
private:
int _x=1; //声明
};
☑️ 可以看到无论是否给缺省值初始化列表都会走一次,若是构造函数函数体有语句就会执行。
- 类中的以下几个成员变量,必须在声明时进行初始化(初始化列表初始化)
- const类型成员变量
const
修饰的成员变量和构造函数对于内置类型不做处理产生了一个冲突,因此C++11出现了初始化列表去解决这个问题。
- 引用类型成员变量
⭕️不记得的同学可以看看这篇博文,有引用的相关内容——C++入门
通过编译可以看到,引用成员变量
y
需要被初始化
- 没有默认构造函数的自定义类型成员变量
🟣 此时我让A类的对象_d作为B类的自定义成员变量,A的构造函数是无参构造,写了初始化列表给_d赋初值。
class A
{
public:
A()
:_x(0)
{}
private:
int _x;
};
class B
{
public:
//初始化列表初始化
B()
:_b1(1)
,_b2(2)
,_a(3)
,_c(_b1)
{}
private:
int _b1;
int _b2;
//以下类型必须在声明时初始化
const int _a = 2;
//const类型成员变量
int& _c;
//引用类型成员变量
A _d;
//自定义类型成员变量
};
✔️通过测试可以发现,编译器对自定义类型的成员变量自动调用了它的默认构造函数。
❔那么如果我对A的默认构造进行一些改动,将其变为有参构造函数,编译器能成功编译吗?
class A
{
public:
A(int x)//有参构造函数
:_x(0)
{}
private:
int _x;
};
- 通过测试可以发现此时编译器出现了【没有合适的默认构造函数可用】的错误。
☑️这是因为默认构造函数只有三类:无参构造函数,全缺省构造函数和编译器默认生成的构造函数。只有默认构造函数编译器才会自动调用。
- 因此只需要给A类构造函数改为全缺省构造函数即可。
class A
{
public:
A(int x = 0)//全缺省构造函数
:_x(x)
{}
private:
int _x;
};
❔那么有参构造函数的自定义类型作为成员变量要如何初始化呢?
☑️有参构造函数则需要我们显示的给初值,此时我们可以在初始化列表给有参构造函数的自定义类型初始化。
调试看看是如何走的
✔️因此可以看出,对【内置类型】不做处理,【自定义类型】调用它的默认构造函数其实走的也是初始化列表。
- 尽量使用初始化列表进行初始化,提高程序的效率和安全性。
- 成员变量的声明顺序就是其在初始化列表的初始化顺序,和初始化列表的顺序无关。
#include<iostream>
using namespace std;
class A
{
public :
//定义和声明顺序最好相同,防止出现bug
A(int n)
:data1(n)
,data2(data1)
{}
void Print()
{
cout << data1 << ' ' <<data2 << endl;
}
private:
//初始化列表的顺序由变量的声明顺序决定
//(例如这里先初始化data2再初始化data1)
int data2;
int data1;
};
int main()
{
A t1(1);
t1.Print();
return 0;
}
✔️由于data2先在初始化列表初始化,data1
的值赋给data2
,此时data1
还没初始化,因此data2
的值是随机数。
- 因此只需要先声明
data1
就可以了,此时初始化列表就会先给data1赋值,就不会出现随机值的情况了
✅通过测试可以发现初始化列表定义顺序是由成员变量的声明顺序决定的。
🌍 explicit关键字
概念
在C++中,explicit关键字用于类定义中,以防止某些隐式的类型转换。这通常用于构造函数,以防止编译器在不需要的地方自动创建对象。
使用场景
1)无explicit关键字构造函数
✔️类的单参数构造函数支持隐式转换
#include<iostream>
using namespace std;
class Date
{
public:
//单参数构造
Date(int day)
{
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//单参数构造无explicit关键字
Date d1(5);
Date d2(6);
d1 = d2;
//编译器提供默认赋值操作符,适用于类中数据成员的逐个赋值
Date d3 = 3;
//3发生隐式转换,自动调用单参构造函数构造无名对象并用3初始化
//相当于 Date d3= notname (notname是Date类的对象)
return 0;
}
✔️类中只有第一个参数无缺省值的半缺省构造函数支持隐式转换
#include<iostream>
using namespace std;
class Date
{
public:
//多参数构造(只有第一个参数无缺省值->相当于单参数构造函数)
Date(int year, int month=4, int day=5)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//无explicit关键字
Date d1(5);
Date d2(6);
d1 = d2;
//编译器提供默认赋值操作符,适用于类中数据成员的逐个赋值
Date d3 = 3;
//3发生隐式转换,自动调用半缺省构造函数构造无名对象并用3初始化
//相当于 Date d3= notname (notname是Date类的对象)
return 0;
}
从上述代码可以看到,Date d3=3
之所以不会报错,是因为编译器自动调用了构造函数,以3为参数生成对象再调用拷贝构造函数给d3赋值。这个过程调用了一次构造函数和一次拷贝构造。
❔为了避免影响性能和可读性,有什么办法取消隐式类型转换呢?
✔️C++11后,多参数构造函数也支持隐式转换。如果想使用多参数隐式类型转换,用{}
括住传入的参数,逗号,
分割即可。
#include<iostream>
using namespace std;
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()
{
Date d3 = { 1,2,3 };
return 0;
}
2)有explicit关键字构造函数
使用了explicit关键字,构造函数不再支持隐式类型转换
单参数构造函数
只有第一个参数无缺省值的半缺省构造函数
多参数构造函数
✅对于可读性不是很好的代码,可以使用explicit
修饰构造函数,将会禁止构造函数的隐式转换。
二、static成员
🚘 概念
C++类中,static可以用来修饰类的成员函数和成员变量,使其成为静态成员。被static修饰的成员属于整个类,而不是属于某个对象。
🚖 静态成员变量
静态成员变量是指在类中被关键字static
修饰的变量,其本质上就是一个全局变量,只不过受 类名作用域 和 权限 控制。
(1)定义和声明
静态成员变量一般在类外定义并初始化,且不需要使用static
,使用域访问操作符::
即可
class Date
{
public:
Date(int day)
{
_day = day;
}
//静态成员变量的声明
static int a;
private:
//int _year;
//int _month;
int _day;
static int b;
};
//类中静态成员变量的定义
int Date::a = 0;
int Date::b = 1;
注意:静态成员变量不能给缺省值!
为什么?因为静态成员变量存放在静态区,不属于具体的对象,所以不会走初始化列表
(2)特性
- 静态成员变量属于整个类,而不属于某个具体的对象,所有对象共享静态成员变量。
当我们尝试给静态成员变量缺省值会发生什么呢?
- 通过测试可以发现,似乎不能这样初始化。
☑️在前面我们学习了初始化列表,给缺省值是给构造函数的初始化列表使用的,因此给缺省值就相当于在类中定义,初始化列表是类中非静态成员,而静态成员变量是所有对象共有的。
那么就引出了static
的下一条特性👇🏽
- 静态成员变量一般在类内声明、在类外定义,声明时加static修饰,定义时不加static。
- 我们在类外定义它,但是出了作用域无法访问,此时就需要用到域作用限定符
::
了。
int Date::a=0;
那么如果要在类外访问静态成员变量呢?
- 通过测试可以发现,直接访问是不行的
☑️静态成员变量是属于类的,因此出了了类作用域后需要用域作用限定符访问::
- 不过加上域作用限定符后又出现报错,无法访问私有的静态成员变量。
那么就引出了static
的下一条特性👇🏽
- 静态成员变量受访问限定符的限制,可以是
public
、private
、protected
的
那么要如何访问呢?
✔️public的静态成员变量可以通过两种方式访问
(1)类名+域操作符::
访问
cout<<Date::b<<endl;
(2)对象名+点操作符.
访问
cout<<d1.b<<endl;
❔看到这里你可能会有疑问,静态成员变量属于整个类,而不属于某个具体的对象,为什么还能通过对象去访问呢?
☑️这是因为当我们通过对象访问静态成员变量时,编译器会自动优化为通过类去访问静态成员变量,即d1.a
和Date::a
效用一致。
【拓展】对于这种方式也可以访问,通过指向对象的指针,此处为该对象的this
指针传递了一个空地址nullptr
,虽然出现了->,但实际上没有发生解引用,因为静态成员变量不存在在类或某个具体对象中,而是存放在静态区,所以无法通过this
指针去访问b
。
Date* d2 = nullptr;
cout << d2->b;
☑️和上面一样,这里编译器会将通过对象指针访问静态成员变量的请求转换为通过类名访问的请求。
那么就引出了第四条特性👇🏽
- 类的静态成员变量可以通过【
类名::静态成员
】或【对象.静态成员
】在类外访问
那么将静态成员变量设置为私有
private
的还能访问吗?
注意:private的静态成员变量不能在类外直接访问
那么私有静态成员变量要如何访问呢?
- 此时我们可以写一个对外公开的接口,在Java中经常这样用,但是在C++中用的比较少,因为C++在这里会使用友元,下面的内容会讲到它。
int GetStatic()
{
return b;
}
可以看到成员函数都是通过对象去调用的,那如果此时还没有对象呢【确实没有😥】,那还有办法访问么?
🔽此时静态成员变量的好兄弟静态成员函数出场了
✔️只需在成员函数GetStatic()前加上一个static
作为修饰即可
static int GetStatic()
{
return a;
}
- 此时就能直接通过类去访问静态成员变量了,使用【
类名::函数名()
】即可
可以说静态成员函数是专门为静态成员变量打造的,那么它和普通成员函数的区别还有哪些呢?
Q:可以通过静态成员函数修改或访问类成员吗?
- 通过测试可以发现这是不行的
那么就引出了static的第五条特性👇🏽
- 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
✔️静态成员函数在静态区,不在类中,因此没有自带的this指针。而类中普通成员变量需要this指针去访问,因为它们是属于当前对象的。
❗️注意:静态成员变量可以是类类型,而非静态成员变量则需要通过指针或引用来包含类类型
class Date
{
public:
//只有第一个参数无缺省值的半缺省构造函数
static int a;
//静态成员变量数据类型可以为类名
static Date staticmember;
//非静态成员变量数据类型不能为类名
//Date nostaticmember; (报错)
//只能通过指针或引用包含类类型
Date* notstaticmember1;
Date& notstaticmember2;
private:
int _day;
static int b;
};
//静态数据成员的定义
int Date::a = 0;
int Date::b = 1;
Date Date::staticmember;
-
为什么有这种区别?
☑️因为静态成员变量存放在静态区,属于整个类,而非静态成员变量只属于类中的某个对象。总而言之静态数据成员可以在类级别上存在,而非静态数据成员只能在对象级别上存在。 -
那么为什么只能通过指针或引用包含类类型呢?
☑️这是因为指针或引用不要求在声明时就有一个完整的对象。
🚔 静态成员函数
静态成员函数是被关键字static
修饰成员函数,分别有公有静态成员函数(public
)和私有静态成员函数(private
)
(1)定义和声明
静态成员函数通常在类中声明,定义可以在类内部也可以在外部
类内声明用关键字static
修饰,类外不需要使用static,使用域访问操作符定义::
即可
class Date
{
public:
static int getstaticmember1();
private:
int _day;
static int b;
//类内声明
static int getstaticmember3();
//类内声明定义
static int getstaticmember4()
{
return b;
}
};
int Date::b = 1;
//公有静态成员函数类外定义
int Date::getstaticmember1()
{
return b;
}
//私有静态成员函数在类外定义
int Date::getstaticmember3()
{
return b;
}
(2)特性
- 静态成员函数属于整个类,而不属于某个对象,所有对象共享静态成员函数。
- 静态成员函数内部没有隐藏的this指针,无法访问类的非静态成员
- 静态成员函数受访问限定符public、private、protected的限制
public 的静态成员函数可以使用类名作用域直接访问,private 的静态成员函数只能在类内访问
- 静态成员函数可以直接访问静态成员变量(public和private都可以)
#include<iostream>
using namespace std;
class Date
{
public:
//只有第一个参数无缺省值的半缺省构造函数
Date(int day)
{
_day = day;
}
static void Print()
{
cout<<"public静态成员变量:" << a << endl;
cout << "private静态成员变量:" << b << endl;
}
static int a;
private:
//int _year;
//int _month;
int _day;
static int b;
};
//类中静态成员变量的定义
int Date::a = 0;
int Date::b = 1;
int main()
{
Date d1(1);
d1.Print();
return 0;
}
测试结果
🚍 OJ题练习
- 实现一个类,计算程序中创建了多少个类对象
- 这题实际上是一家公司的面试题,要求计算创建了多少个类对象,分析可以知道,类对象的实例化无非是调用构造函数或者拷贝构造函数,因此思路就很简单了。
- 通过定义一个静态成员变量count统计被实例化的对象个数,在构造函数和拷贝构造中计数即可。
//利用静态成员变量 在构造函数和拷贝构造寒素和析构函数计数
//实现计算类的对象个数(也可以实现动态计算)
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year=1900, int month=1, int day=1)
:_year(year)
,_month(month)
,_day(day)
{
count++;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
count++;
}
~Date()
{
//count--;//加上这个可以实时计算程序存在的对象
}
private:
int _year;
int _month;
int _day;
public:
static int count;
};
int Date::count = 0;
void fun1()
{
Date d3;
cout << "此时类有" << Date::count << "个对象" << endl;
}
Date fun2()
{
Date d4;
return d4;
}
int main()
{
Date d1;
Date d2;
fun2();
//传值没有返回 vs2022自动优化了,因此会少一个拷贝的对象
Date d3 = d1;
cout << "类在程序中创建了" << Date::count << "个对象" << endl;
return 0;
}
☑️通过调试可以观察此时创建了多少个对象
测试结果
题目描述
思路分析
可以看到,题目要求不能使用一系列关键字,那么要如何计算呢?
- 既然不能递推,那么用递归不就行了,想到这里我马上有了思路。
(1)全局定义ans用于计算答案
int ans=0;
(2)递归函数cnt
void cnt(int n)
{
}
(3)递归体和递归结束条件
if(n==0) //结束条件
return;
//递归体
ans+=n;
cnt(n-1);
完整代码和运行结果
int ans=0;
void cnt(int n)
{
if(n==0)
return;
ans+=n;
cnt(n-1);
}
class Solution {
public:
int Sum_Solution(int n) {
cnt(n);
return ans;
}
};
- 同时,我们也可以结合今天所学,使用构造函数和static成员解决这道题目。
☑️思路很简单,利用变长数组调用n次构造,在构造函数内部设置累加的逻辑即可。
✔️具体步骤如下
(1)定义一个类,声明并定义两个静态成员变量,用编写于累加的逻辑
class A
{
public:
private:
static int cnt;//用于累加的数
static int ret;//被累加的返回值
};
int A::cnt=1;
int A::ret=0;
(2)编写构造函数累加逻辑
每调用一次构造函数,ret加上cnt,cnt自增一次,这样就实现了1~n的累加。
A()
{
ret+=cnt;//累加
++cnt;//自增
}
❔那么要如何调用n次构造函数呢?
这里就要用到变长数组了👇🏼
(3)利用变长数组调用n次构造函数
class Solution {
public:
int Sum_Solution(int n) {
//变长数组,调用n次构造
A arr[n];
}
};
到这里我们已经完成了代码逻辑的编写,但是要如何返回ret呢?
☑️很简单,我们可以使用刚刚所学的静态成员函数去访问ret
(4)通过静态成员函数获取答案ret
- 在类内写一个公有的静态成员函数去返回ret
static int getret()
{
return ret;
}
- 在主函数中返回ret
return A::getret();
完整代码和运行结果
//利用类中 对象变长数组使用指定次数的构造函数,完成计数
class A
{
public:
A()
{
ret+=cnt;
++cnt;
}
static int getret()
{
return ret;
}
private:
static int cnt;
static int ret;
};
int A::cnt=1;
int A::ret=0;
class Solution {
public:
int Sum_Solution(int n) {
//变长数组,调用n次构造
A arr[n];
//Sum_Solution(n-1);
return A::getret();
}
};
🚊 static修饰的注意要点
- 静态成员变量不走初始化列表
✔️因为静态成员变量不属于类而属于静态区,并在类外定义。
- 静态成员变量析构顺序辨析
- 通过调试可以清晰的发现,本应该第一个被析构的对象a3在a1和a2析构后才析构,但还是在a4析构前析构,因为a4早于a3被实例化出来。
✔️说明static关键字让对象a3的生命周期延长。
对于析构顺序特性不了解的同学可以看看这篇博文—— 类和对象(中)
❕注意:static虽然能改变对象的生命周期,但是不会改变其作用域。
- static 修饰全局变量或者函数后只在当前源文件内有效
至于为什么,涉及到了编译链接的知识,可以看看这篇博文。
三、友元
在C++中,友元是一种特殊的类成员或函数,它被赋予了访问类的private成员和protected成员的权限。友元不是类的成员,但它可以像类的成员一样访问类的内部数据。
友元分为友元函数和友元类。
- 注意:友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
🌈 友元函数
在前面的学习中,我们知道cout和流插入运算符可以实现将默认内置类型打印的效果。那如何实现打印自定义类型呢?
我们尝试在类中重载流插入运算符
可以看到,cout<<d1
报错了,这是为什么呢
这是因为,C++规定成员函数第一个参数是隐藏的this,所以d1必须放在<<的左侧,因此使用d1<<cout
才能正常打印。
d1<<cout
—d1.operator<<(&d1, cout)
❔但是这并不符合我们日常的使用习惯,有没有什么方法可以达到我们的预期cout<<d1
呢?
我们尝试在全局重载流插入,但是这样又无法访问类的私有成员
这类必须定义在类外,但是又需要访问类内的私有成员的函数就需要使用关键字友元friend
来解决了
☑️友元函数在类内部声明,它可以访问类的私有成员,用关键字friend
修饰即可
#include<iostream>
using namespace std;
class Date
{
public:
//友元函数可以访问类私有成员(类内声明)
friend ostream& operator<<(ostream& out, const Date& d);
Date(int year, int month, int day)
:_year(year)
,_month(month)
,_day(day)
{}
private:
int _year;
int _month;
int _day;
};
//类外定义,避免受到this指针影响,实现cout<<d1
ostream& operator<<(ostream& out,const Date&d)
{
out << d._year << " " << d._month << " " << d._day;
return out;
}
int main()
{
Date d1(1,2,3);
cout << d1;
return 0;
}
测试结果
注意
- 友元函数可以在类的任何地方声明,不受类访问限定符限制
友元函数被声明为public或private实现的功能相同,但是它们仍有些许不同点
1)将友元函数声明放在类的私有部分可以看作是一种封装的实践,表明这个友元函数是类的一个内部细节,不应该被类的外部用户直接调用。这种做法有助于隐藏实现细节,并且可以防止类的外部用户错误地调用该函数。
2)友元函数放在公有部分意味着这个函数是类的一个公共接口的一部分,而放在私有部分则强调了其实现细节的性质
- 友元函数不属于任何类的成员函数,它是一个独立的函数,因此它不能访问类的成员函数
- 一个函数可以是多个类的友元函数
- 友元函数不能用const修饰
☑️因为友元函数并不是类的成员函数,类的成员函数中有this指针,友元函数不会有this指针,this指针才需要被const修饰。
- 友元函数的调用和普通函数的调用方法一致
✨ 友元类
友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的非公有成员。
注意
- 友元关系是单向的,不具有交换性
譬如A是B的友元,但并不意味着B是A的友元
- 友元关系不能传递
每个类负责管理自己的友元函数和友元类
- 友元不能继承
友元函数和类都不能被继承
四、内部类
🥥 概念
内部类是一个类内部定义的类,也称为嵌套类。内部类是一个独立的类,外部类不具有访问内部类的权限。内部类的成员函数相当于外部类的友元函数,可以通过访问外部类的对象参数来访问外部类的数据成员变量和数据成员函数。
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1970, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
class Time
{
public:
Time(int hour, int minute, int second)
:_hour(hour)
,_minute(minute)
,_second(second)
{}
//直接访问外部类的静态成员变量
void Printstatic()
{
cout << a << endl;
}
//通过对象访问外部类的私有成员
void Printpri(const Date& d)
{
cout << d._year << ' ' << d._month << ' ' << d._day << endl;
}
private:
int _hour;
int _minute;
int _second;
};
private:
int _year;
int _month;
int _day;
static int a;
};
int Date::a = 0;
int main()
{
Date d;
Date::Time t(1,2,3);
t.Printstatic();
//out: 0
t.Printpri(d);
//out: 1970 1 1
return 0;
}
🥝 特性
(1)内部类受外部类访问权限限制,即内部类可以定义为public,private,protected的。
每种定义方式都会影响内部类的可访问性和作用域
(2)内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
(3)sizeof(外部类)==外部类,和内部类无关系
五、匿名对象
匿名对象是指没有明确名称的对象。匿名对象通常是临时对象,它们在表达式求值后或执行完当前语句时立即被销毁,除非它们被绑定到一个引用上。
- 有名对象:生命周期在当前函数局部域
- 匿名对象:生命周期在当前行
#include<iostream>
using namespace std;
class A
{
public:
void Print()
{
cout << _a << endl;
}
A(int x)
:_a(x)
{
cout << "构造函数" << endl;
}
~A()
{
cout << "析构函数" << endl;
}
private:
int _a;
};
int main()
{
//有名对象 -- 生命周期在当前函数局部域
A a(10);//out:构造函数
//匿名对象 -- 生命周期在当前行
A(20);//out:构造函数 析构函数
return 0;//out:a对象的析构函数
}
调试过程
- 创建引用可以延长匿名对象的生命周期
此外,匿名对象具有常性,因此引用必须要加const,否则会造成权限放大。
//加const防止权限放大
const A& ref = A(100);
使用场景
- 需要使用类中的某个成员数据时
使用匿名对象.函数名()
调用
A(20).Print();
使用匿名对象创建,使用后会立即销毁,就无需新建一个对象了
- 返回临时对象
用匿名对象返回临时对象,用变量存储或直接使用
std::string createString() {
return std::string("Temporary");
}
int main() {
auto str = createString(); // 匿名对象被移动到str
std::cout << str << std::endl;
return 0;
}
✅各种一次性的对象使用,都可以使用匿名对象
六、拷贝对象时的一些编译器优化
在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝,在一些场景下还是非常有用的
正如讲解explicit
关键字时说到的隐式类型转换Date d1=1;
1
发生了拷贝构造构造出无名对象
并赋值给对象d1
在这一块,编译器对其进行了优化,将【构造+拷贝构造】优化为了【直接构造】
🔴 传值传参
当使用对象a1传值传参
☑️可以看到,还是发生了拷贝构造,这是为什么呢?因为像这样传参传参不在同一行表达式的,编译器不会对其进行优化。
当我们直接传入一个1时呢,会优化吗?
☑️可以看到,编译器对其进行了优化,没有再调用拷贝构造,而是把【构造+拷贝构造】转化为了【直接构造】
当使用匿名对象传值传参,会发生优化吗?
☑️可以看到,A(3)
是一个有参构造,实例化出一个匿名对象传值传参是拷贝构造,编辑器优化为了直接构造。
🟠 传引用传参
和传值传参一样,我们分别传入不在同一行表达式的对象,编译器的内置类型和匿名对象。只不过这里使用引用&
传递参数。
- 在上文中提到,如果能使用引用传参尽量使用引用,原因是可以减少拷贝,提升程序运行效率
void func2(const A& a)//需要加const,防止权限放大
{
}
通过引用接收对象a1会发生什么呢?
func2(a1);
- 通过测试发现,无论是构造还是拷贝构造,都没有去调用。
☑️原因就在于,这里传递的是引用,形参的a
其实就是a1
的别名,对象a1
在之前已经被创建,因此无需构造一个新的对象,也就不需要调用构造和拷贝构造生成一个临时对象。
那传入内置类型1呢?
func2(1);
- 通过测试发现,调用了构造但没有调用拷贝构造
☑️临时对象还是会调用构造,但由于引用接收,形参a
就是这个临时对象的别名,因此不会调用拷贝构造,回到主函数再去调用这个临时对象的析构。(tip:至于多出来的一个析构,是a1对象的析构emmmm)
- 注意:临时对象具有常性,因此拷贝构造函数的参数一定要加const,否则会权限放大
那传入匿名对象A(1)呢?
func2(A(2));
- 通过测试可以发现,和上面的传内置类型一样,调用了构造没有调用拷贝构造
☑️匿名对象也是临时对象的一种,匿名对象的创建调用了构造,由于用引用接收,因此此时的形参a
实际上是匿名对象的别名(即为实参),因此不会调用拷贝构造,同样的,回到主函数后该匿名对象的生命周期结束,调用匿名对象的析构
🟡 传值返回
函数返回的场景,编译器会如何优化呢?
//传值返回
A func3()
{
A aa;
return aa;
}
如果直接调用func3函数会是什么样呢?
func3();
- 通过测试可以发现,只发生了构造和析构
☑️ func3中的对象创建和返回不在同一个表达式,但是和传值传参不一样,返回该对象时并没有调用拷贝构造,编译器把这个拷贝构造优化了。(当一个函数返回一个局部对象时,编译器可能会直接在调用者的内存上构建这个对象,而不是首先在函数内部构建对象然后再拷贝或移动它)回到主函数后,生命周期在当前表达式结束,发生析构。
- 注意:在没有优化的情况,函数返回不会直接返回对象aa,而是创建一个临时对象拷贝它,如果临时对象较小在寄存器存,否则会在两个栈帧间存。
如果创建对象接收返回对象,编译器会怎么处理呢?
A ret=func3();
- 通过测试可以发现,函数内部和上面的测试结果一样,只调用了构造和析构,但是回到主函数有所不同,析构发生在程序结束。
☑️同样的,函数返回该对象时并没有调用拷贝构造,编译器把这个拷贝构造优化了。回到主函数后,原本返回的对象赋值给ret要进行一次拷贝构造,但是也被编译器优化了,那么它是如何把返回对象的内容赋值给ret呢?这里涉及到了移动构造函数(C++11标准增加的在创建对象时移动旧对象资源的构造函数),相当于把返回对象的资源直接搬运给了ret对象。程序运行结束时,ret对象发生析构。
对于移动构造有疑问的同学可以先看看这篇博文——— C++移动构造函数
那么问题来了,如果接收返回值的对象在函数调用前已经被创建了呢?
这种情况就不只是拷贝构造了,还涉及到了赋值运算符重载。
🔷赋值运算符重载和拷贝构造辨析
- 在之前的学习中,我们学习过赋值运算符重载,它和拷贝构造的形式很像,作用很相似。如果被赋值的对象在当前表达式之前已经创建的情况使用
=
,就是赋值运算符重载,否则是拷贝构造。
A a1;
a1 = func3();//a1对象在当前表达式之前被创建,此时的=是赋值运算符重载
- 通过测试可以发现,当【赋值运算符重载】和【拷贝构造】一起使用时,发生了两次构造,一次运算符重载,两次析构,拷贝构造被优化了。
☑️主函数中调用【构造】函数创建a1
对象,随后用a1
对象接收func3()
函数返回值,进入func3
函数内部,可以发现,和A a=func3();
这类没用到赋值运算符重载的情况不同,func3
函数内部调用了【构造】函数真实的创建了对象aa
,返回对象aa
的过程没有调用【拷贝构造】,【拷贝构造】被编译器优化了。回到主函数发生赋值运算符重载,将对象aa赋值给了已经创建好的对象a1,aa对象生命周期在当前表达式结束,发生析构,对象a1在程序运行结束时也发生【析构】。
- 显然,在赋值运算符重载参与进来后,编译器不得不让步将函数内部的对象真实的创建出来,因为赋值运算符需要两个对象共同完成。
🟢 传引用返回
然后来说说传引用返回,这里有一个需要关注的点,传引用返回的函数如果返回一个局部元素,这个元素出了当前函数作用域就会被销毁,就像下面这个例子
A& func4()
{
A aa;//局部对象,出了func4函数作用域就会被销毁
return aa;
}
- 直接调用func4(),测试可以发现,发生了一次构造,一次拷贝构造,两次析构。
☑️func4函数内调用【构造】函数创建对象aa,func4函数结束时对象aa调用【析构】销毁,此时返回的是一个资源为随机值的临时对象,由于是引用返回,减少了中间的一份临时对象的拷贝,因此没有调用【拷贝构造】。回到主函数后,调用【拷贝构造】将临时对象赋值给a4,程序运行结束时a4调用【析构】。
- 若是用一个返回值接收的话,就能看出引用返回的问题了
A a4 = func4();
可以看到经过【拷贝构造】后,a4对象的成员变量_x被赋值为随机值。
因此,如果要使用传引用返回,应该把函数内部的局部对象修改为全局对象。
☑️使用static
关键字即可
A& func4()
{
static A aa;//使用static修饰,全局对象,作用域在全局
return aa;
}
此时函数内部的对象在程序运行结束时【析构】
🔵 传匿名对象返回
如果使用前面学习的匿名对象返回会发生什么呢
A func5()
{
return A();
}
- 调用func5函数
func5();
- 通过测试可以发现调用了一次【构造】和一次【析构】
☑️和传值返回一样,调用func5函数,返回时使用【构造】函数创建匿名对象并赋值给返回的临时对象,这个赋值操作本应有【拷贝】构造,但是编译器将它优化了,回到主函数后,由于临时对象生命周期在当前表达式,因此在当前表达式结束后调用【析构】销毁。
如果对这个返回值接收,编译器会如何优化呢?
A a5 = func5();
- 通过测试可以发现,和不用返回值接收的结果一样,调用了一个构造和一个析构。
☑️调用func5()
函数,返回匿名对象时调用【构造】函数创建,【拷贝构造】被编译器优化,因此【构造+拷贝构造】被优化为了【直接构造】。回到主函数赋值给a5
的【构造+拷贝构造】又引来编译器的优化,因此只剩一个【构造】了。程序运行结束时调用【析构】。
- 可以观察到,使用匿名对象返回并不会出现随机值,这是因为这里匿名对象用了【传值返回】,需要注意的时匿名对象不能用【引用返回】,因为匿名对象只存在当前表达式,当前表达式结束后会立即销毁,接收对象指向一个已经被销毁的对象,这是不安全的。编译器为了防止代码出现这类问题,会阻止这种代码的编译,出现报错。
🟤 小结
经过编译器拷贝优化的一系列学习,我们来总结一下
-
传参总结
☑️尽量使用引用&
传参,减少拷贝,提高程序运行效率 -
对象返回总结
☑️函数内部返回时尽量使用匿名对象返回【增加编译器优化】
☑️接收函数返回值在同一表达式【增加编译器优化】
☑️接收返回值尽量使用拷贝构造,不要用赋值重载【会干扰编译器优化】
七、重新理解类和对象
现实生活中的实体计算机并不认识,计算机只认识二进制格式的数据。如果想要让计算机认识现实生活中的实体,用户必须通过某种面向对象的语言,对实体进行描述,然后通过编写程序,创建对象后计算机才可以认识。比如想要让计算机认识洗衣机,就需要:
- 用户先要对现实中洗衣机实体进行抽象—即在人为思想层面对洗衣机进行认识,洗衣机有什么属性,有那些功能,即对洗衣机进行抽象认知的一个过程。
- 经过1之后,在人的头脑中已经对洗衣机有了一个清醒的认识,只不过此时计算机还不清楚,想要让计算机识别人想象中的洗衣机,就需要人通过某种面相对象的语言(比如:C++、Java、Python等)将洗衣机用类来进行描述,并输入到计算机中
- 经过2之后,在计算机中就有了一个洗衣机类,但是洗衣机类只是站在计算机的角度对洗衣机对象进行描述的,通过洗衣机类,可以实例化出一个个具体的洗衣机对象,此时算机才能洗衣机是什么东西。
- 用户就可以借助计算机中洗衣机对象,来模拟现实中的洗衣机实体了。
举个形象的例子————旅行app系统
-
一个旅行app系统一定包含两个类。订酒店、高铁票、机票、火车票、景点门票等等一系列的统称为商品类;另一个就是花钱消费的,即用户群体,称为用户类。全球各地有数不清的景点、酒店、餐馆,还有用户,当我们的商品类和用户类创建好,就可以通过类去实例化它们。
-
接下来就是匹配/推荐算法。系统通过卫星定位用户所在区域,把距离用户近的景点、餐馆、酒店推送。从上面这张截图可以看到,博主本人在中山,它的推送和中山这篇区域高度相关。这里运用了距离匹配相关算法。其次,系统会根据用户的喜好,如通过监控用户的搜索记录,浏览记录,收藏等途径来了解个体用户的喜好,实现个性化的推送。这就是所谓的推荐算法。
☑️对于上面的这个旅行app系统,【后端】的任务就是设计这些类,并实现每个类的功能,类之间的关联和耦合;而匹配/推荐算法的实现自然是交给【算法岗】;用户手机或网页上旅行app的显示就是交给【前端】制作。
在类和对象阶段,大家一定要体会到,类是对某一类实体(对象)来进行描述的,描述该对象具有那些属性,那些方法,描述完成后就形成了一种新的自定义类型,才用该自定义类型就可以实例化具体的对象。
八、总结和提炼
最后我们总结一下本文学到的内容
- 进入类和对象(下)的学习,又谈到了【构造函数】,这次我们学习了如何在构造函数函数体赋值,不过要注意哦,函数体赋值并不是初始化。随后就来到了本章节的重点——初始化列表。默认构造函数对内置类型不做处理,为了解决这个问题,可以使用缺省值,但是我们关注的是在C++11之前是如何解决的,因此出现了初始化列表。缺省值本质是一个备份,当初始化列表没有给值才会给缺省值。所有成员变量无论是否有缺省值都会走初始化列表。
- 编译器各种隐式转换让人头大,应对方法很简单,只需要【explicit关键字】就可以让编译器不敢轻举妄动 (─‿‿─)。
- 接下来我们谈到了【static成员】,要注意
static
存放在静态区,属于类但不属于某个具体的对象,可是当static
成员变量为私有时属实访问不到,此时它的好兄弟静态成员函数出手了,解决了访问private
静态成员的棘手难题。 - 随后我们谈到了【友元】
friend
。我把你当朋友,但你不一定把我当朋友,友元关系是单向的(┬_┬);我和你是好朋友,他是你的好朋友,但他不一定是我的好朋友,友元关系不可以传递。友元虽好,但是不建议多用,朋友不是越多越好的哦(●´д`)。 - 【内部类】天生就是外部类的好朋友,但是又不把外部类当好朋友(▼ _ ▼) 。
- 【匿名对象】是没有名字的对象,连对象名字都不知道,会很快就忘记的吧(→_←) 。匿名对象生命周期只在当前表达式哦。
- 对象太多可不好哦,要做一个专一的人,否则【编译器】一定会出手💣。
好,这就是本文的所有内容了,有不足的地方可以在评论区提出,如果觉得还不错可以点个赞(ô‿ô)