目录
[0x01. 提供灵活的访问方式](#0x01. 提供灵活的访问方式)
[0x02. 实现不同类之间的协作](#0x02. 实现不同类之间的协作)
一、再谈构造函数
1.构造函数体赋值
在创建对象时,编译器会通过调用构造函数,给对象中的各个成员变量一个合适的初始值:
cpp
class Date
{
public:
Date(int year=0, int month=1, int day=1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d(2024,10,1);
return 0;
}
需要注意的是:虽然通过调用上述的构造函数后,对象中的每个成员变量都有了一个初始值,但是构造函数中的语句只能将其称作为赋初值,而不能称作为初始化。因为初始化只能初始化一次,而构造函数体内可以进行多次赋值。
cpp
class Date
{
public:
// 构造函数
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;// 第一次赋值
_year = 2024;// 第二次赋值
//...
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
2.初始化列表
初始化列表定义:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个成员变量后面跟一个放在括号中的初始值或表达式。
cpp
class Date
{
public:
// 构造函数
Date(int year = 0, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
注意事项:
0x01.每个成员变量在初始化列表中只能出现一次
因为初始化只能进行一次,所以同一个成员变量在初始化列表中不能多次出现。
0x02.类中包含以下成员,必须放在初始化列表进行初始化:
引用成员变量
引用类型的变量在定义时就必须给其一个初始值,所以引用成员变量必须使用初始化列表对其进行初始化。
cpp
int a = 10;
int& b = a;// 创建时就初始化
const成员变量
被const修饰的变量也必须在定义时就给其一个初始值,也必须使用初始化列表进行初始化。
cpp
const int a = 10;//correct 创建时就初始化
const int b;//error 创建时未初始化
自定义类型成员(该类没有默认构造函数)
若一个类没有默认构造函数,那么我们在实例化该类对象时就需要传参对其进行初始化,所以实例化没有默认构造函数的类对象时必须使用初始化列表对其进行初始化。
在这里再声明一下,默认构造函数是指不用传参就可以调用的构造函数:
1.我们不写,编译器自动生成的构造函数。
2.无参的构造函数。
3.全缺省的构造函数。
cpp
class A //该类没有默认构造函数
{
public:
A(int val) //注:这个不叫默认构造函数(需要传参调用)
{
_val = val;
}
private:
int _val;
};
class B
{
public:
B()
:_a(2024) //必须使用初始化列表对其进行初始化
{}
private:
A _a; //自定义类型成员(该类没有默认构造函数)
};
总结一下:引⽤成员变量,const成员变量,没有默认构造的类类型变量,必须放在初始化列表位置进⾏初始 化,否则会编译报错。
说明:C++11⽀持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显⽰在初始化列表初始化的成员使⽤的。
0x03.尽量使用初始化列表初始化
因为初始化列表实际上就是当你实例化一个对象时,该对象的成员变量定义的地方,所以无论你是否使用初始化列表,都会走这么一个过程(成员变量需要定义出来)。
严格来说:
1.对于内置类型,使用初始化列表和在构造函数体内进行初始化实际上是没有差别的,其差别就类似于如下代码:
cpp
// 使用初始化列表
int a = 10
// 在构造函数体内初始化(不使用初始化列表)
int a;
a = 10;
2.对于自定义类型,使用初始化列表可以提高代码的效率
cpp
class Time
{
public:
Time(int hour = 0)
{
_hour = hour;
}
private:
int _hour;
};
class Test
{
public:
// 使用初始化列表
Test(int hour)
:_t(12)// 调用一次Time类的构造函数
{}
private:
Time _t;
};
对于以上代码,当我们要实例化一个Test类的对象时,我们使用了初始化列表,在实例化过程中只调用了一次Time类的构造函数。
我们若是想在不使用初始化列表的情况下,达到我们想要的效果,就不得不这样写了:
cpp
class Time
{
public:
Time(int hour = 0)
{
_hour = hour;
}
private:
int _hour;
};
class Test
{
public:
// 在构造函数体内初始化(不使用初始化列表)
Test(int hour)
{ //初始化列表调用一次Time类的构造函数(不使用初始化列表但也会走这个过程)
Time t(hour);// 调用一次Time类的构造函数
_t = t;// 调用一次Time类的赋值运算符重载函数
}
private:
Time _t;
};
当实例化Test
类对象时,虽未显式用初始化列表,但会先隐式调用Time
类默认构造函数初始化成员_t
。接着构造函数体内创建临时Time
对象t
又调用带参数构造函数。最后_t = t
会调用Time
类赋值运算符重载函数。相比直接在初始化列表初始化,这种方式多了一次构造函数调用和一次赋值操作,效率降低。
0x04.成员变量在类中声明的次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后顺序无关
举个例子:
cpp
class A
{
public:
A(int a)
:_a1(a)
, _a2(_a1)
{}
void Print() {
cout << _a1 << " " << _a2 << endl;
}
private:
//根据类声明顺序进行初始化 先初始化_a2 再初始化_a1
int _a2;
int _a1;
};
int main() {
A aa(1);
aa.Print();
}
代码中,A类构造函数中的初始化列表先初始化_`a1,然后_a2再初始化,根据代码应该输出1 1,但是实际的输出是1,然后是_a2的随机值,这是因为这里是按照声明的顺序来初始化的,因此先初始化_a2,也就是将_a1的值拷贝给_a2,但此时_a1还没有被初始化,因此这里输出为随机值,然后在初始化_a1,将a的值赋给_a1,因此_a1为1.
初始化列表总结: ⽆论是否显⽰写初始化列表,每个构造函数都有初始化列表; ⽆论是否在初始化列表显⽰初始化,每个成员变量都要⾛初始化列表初始化;
二、类型转换
1.隐式类型转换
构造函数不仅可以构造和初始化对象,对于单个参数的构造函数,还支持隐式类型转换。
这里是隐式类型的转换,为什么支持一个整型转换成日期类相关的类型呢?整型和日期类本来是没有关系的,但是你支持一个单参数的构造函数后,整型就可以去构造一个日期类的对象,这个日期类的对象自然可以赋值给他了。本来用 2024 构造成一个临时对象 Date(2024) ,在用这个对象拷贝构造 d2,但是 C++ 编译器在连续的一个过程中,编译器为了提高效率,多个构造会被优化,合二为一。
cpp
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year = 0) //单个参数的构造函数
:_year(year)
{}
void Print()
{
cout << _year << endl;
}
private:
int _year;
};
int main()
{
Date d1 = 2024; //支持该操作
d1.Print();
return 0;
}
在语法上,代码中Date d1 = 2024等价于以下两句代码:
cpp
Date tmp(2024); //先构造
Date d1(tmp); //再拷贝构造
所以在早期的编译器中,当编译器遇到Date d1 = 2024这句代码时,会先构造一个临时对象,再用临时对象拷贝构造d1;但是现在的编译器已经做了优化,当遇到Date d1 = 2024这句代码时,会按照Date d1(2024)这句代码处理,这就叫做隐式类型转换。
实际上,我们早就接触了隐式类型转换,只是我们不知道而已,以下代码也叫隐式类型转换:
cpp
int a = 10;
double b = a; //隐式类型转换
在这个过程中,编译器会先构建一个double类型的临时变量接收a的值,然后再将该临时变量的值赋值给b。这就是为什么函数可以返回局部变量的值,因为当函数被销毁后,虽然作为返回值的变量也被销毁了,但是隐式类型转换过程中所产生的临时变量并没有被销毁,所以该值仍然存在。
2.explicit关键字
对于单参数的自定义类型来说,Date d1 = 2021这种代码的可读性不是很好,我们若是想禁止单参数构造函数的隐式转换,可以用关键字explicit来修饰构造函数。
cpp
#include <iostream>
using namespace std;
class Date
{
public:
explicit Date(int year = 0) // 构造函数不再⽀持隐式类型转换
:_year(year)
{}
void Print()
{
cout << _year << endl;
}
private:
int _year;
};
int main()
{
Date d1 = 2024; //支持该操作
d1.Print();
return 0;
}
3.类类型之间的对象隐式转换
类类型的对象之间也可以隐式转换,需要相应的构造函数⽀持。
cpp
class ClassA
{
public:
int value;
// 构造函数,接受一个整数参数
ClassA(int num)
: value(num)
{}
// 定义一个成员函数用于输出类对象的值
void printValue() const
{
cout << "Value in ClassA: " << value <<endl;
}
};
// 定义另一个类B
class ClassB
{
public:
ClassA innerObj;
// 构造函数,接受一个ClassA类型的对象作为参数
ClassB(ClassA aObj)
: innerObj(aObj)
{}
// 定义一个成员函数用于输出内部ClassA对象的值
void printInnerValue() const
{
innerObj.printValue();
}
};
int main() {
// 创建一个ClassA的对象a1
ClassA a1(10);
// 这里发生了隐式转换,将ClassA类型的对象a1隐式转换为ClassB类型的对象b1
ClassB b1 = a1;
b1.printInnerValue();
return 0;
}
代码讲解:
- 首先定义了
ClassA
类,它有一个整数成员变量value
以及一个接受整数参数的构造函数,用于初始化value
。 - 接着定义了
ClassB
类,它包含一个ClassA
类型的成员变量innerObj
,并且有一个构造函数接受一个ClassA
类型的对象作为参数,用于初始化innerObj
。 - 在
main
函数中,先创建了一个ClassA
的对象a1
,然后在创建ClassB
的对象b1
时,直接将a1
赋值给b1
,此时就发生了隐式转换。编译器会自动调用ClassB
的构造函数,并将a1
作为参数传递进去,从而完成了从ClassA
类型对象到ClassB
类型对象的隐式转换,最后通过b1
的成员函数输出了内部ClassA
对象的值。
三、static成员函数
1.概念
声明为static的类成员称为类的静态成员。用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量通常是在类外进行初始化。
cpp
class Test
{
private:
static int _n;
};
// 静态成员变量的定义初始化
int Test::_n = 0;
2.特性
• 静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区。
cpp
#include <iostream>
using namespace std;
class Test
{
private:
static int _n;
};
int main()
{
cout << sizeof(Test) << endl;
return 0;
}
结果计算Test类的大小为1,因为静态成员_n是存储在静态区的,属于整个类,也属于类的所有对象。所以计算类的大小或是类对象的大小时,静态成员并不计入其总大小之和。
• ⽤static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针。 静态成员函数中可以访问其他的静态成员,但是不能访问⾮静态的,因为没有this指针。
cpp
class Test
{
public:
static void Fun()
{
cout << _a << endl; //error不能访问非静态成员
cout << _n << endl; //correct
}
private:
int _a; //非静态成员
static int _n; //静态成员
};
小贴士:含有静态成员变量的类,一般含有一个静态成员函数,用于访问静态成员变量。
• ⾮静态的成员函数,可以访问任意的静态成员变量和静态成员函数。
• 突破类域就可以访问静态成员,可以通过类名::静态成员或者对象.静态成员来访问静态成员变量 和静态成员函数。
3.访问静态成员变量的方法:
0x01.当静态成员变量为公有时,有以下几种访问方式:
cpp
#include <iostream>
using namespace std;
class Test
{
public:
static int _n; //公有
};
// 静态成员变量的定义初始化
int Test::_n = 0;
int main()
{
Test test;
cout << test._n << endl; //1.通过类对象突破类域进行访问
cout << Test()._n << endl; //3.通过匿名对象突破类域进行访问
cout << Test::_n << endl; //2.通过类名突破类域进行访问
return 0;
}
0x02.当静态成员变量为私有时,有以下几种访问方式:
cpp
#include <iostream>
using namespace std;
class Test
{
public:
static int GetN()
{
return _n;
}
private:
static int _n;
};
// 静态成员变量的定义初始化
int Test::_n = 0;
int main()
{
Test test;
cout << test.GetN() << endl; //1.通过对象调用成员函数进行访问
cout << Test().GetN() << endl; //2.通过匿名对象调用成员函数进行访问
cout << Test::GetN() << endl; //3.通过类名调用静态成员函数进行访问
return 0;
}
静态成员也是类的成员,受public、protected、private访问限定符的限制。
private情况讲解:
因为 private
访问权限是语言层面的一种强制约束,它确保了类的封装性。当编译器在处理代码时,会严格检查访问权限,如果发现是从类的外部对 private
静态成员变量进行访问,即使代码在语法上通过一些手段绕开了类域的常规限制,编译器也会判定这种访问是非法的,并报出相应的错误。
所以当静态成员变量设置为private时,尽管我们突破了类域,也不能对其进行访问。 上面的代码实际上是在类内通过成员函数(包括静态成员函数)来访问私有静态成员变量的,这是符合 C++ 语言规则的,因为成员函数本身就在类的 "内部",拥有访问类内私有成员的权限,而从类的外部直接去访问私有静态成员变量才是不被允许的。
注意区分两个问题:
1、静态成员函数可以调用非静态成员函数吗?
2、非静态成员函数可以调用静态成员函数吗?
问题1:不可以。因为非静态成员函数的第一个形参默认为this指针,而静态成员函数中没有this指针,故静态成员函数不可调用非静态成员函数。
问题2:可以。因为静态成员函数和非静态成员函数都在类中,在类中不受访问限定符的限制。
3.面试题:
为什么静态成员变量不能在声明位置用缺省值初始化(通过构造函数初始化列表的方式)
由于静态成员变量不属于某个具体对象,它的生命周期和整个类相关,在程序运行开始时就已经存在(在类被加载时就进行了相应的内存分配等操作),而不是随着对象的创建才进行初始化。构造函数是在创建对象时被调用的,其初始化列表也是针对对象的成员变量进行初始化操作的。所以静态成员变量不能按照对象的构造函数初始化列表的方式来给定缺省值初始化。
如果要对静态成员变量进行初始化,通常有以下几种正确的做法:
- 在类外进行初始化
在类的定义之外,在全局作用域或者某个合适的命名空间内,对静态成员变量进行初始化。
cpp
class MyClass {
public:
static int staticVar;
};
// 在类外初始化静态成员变量
int MyClass::staticVar = 10;
- 使用静态成员函数或者静态代码块进行初始化(在 C++11 及以后版本更灵活的方式)
可以利用静态成员函数或者静态代码块在程序运行时的合适时机对静态成员变量进行初始化。
例如,使用静态成员函数:
cpp
class MyClass {
public:
static int staticVar;
static void initializeStaticVar() {
staticVar = 20;
}
};
// 调用静态成员函数来初始化静态成员变量
MyClass::initializeStaticVar();
四、友元函数
1.基本介绍
友元函数,简单来说,就是在一个类中被声明为 "友元" 的非成员函数。虽然它本身不是类的成员函数,但却可以访问类中的私有成员和保护成员,就好像它是类内部的一员一样。这打破了 C++ 中通常严格的封装原则,为某些特殊的编程需求提供了一种灵活的访问机制。
2.回顾:
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
对于之前实现的日期类,我们现在尝试重载operator<<,但是我们发现没办法将其重载为成员函数,因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置:this指针默认是第一个参数,即左操作数,但是实际使用中cout需要是第一个形参对象才能正常使用。
所以我们要将operator<<重载为全局函数,但是这样的话,又会导致类外没办法访问成员,那么这里就需要友元来解决。(operator>>同理)
我们都知道C++的<<和>>很神奇,因为它们能够自动识别输入和输出变量的类型,我们使用它们时不必像C语言一样增加数据格式的控制。实际上,这一点也不神奇,内置类型的对象能直接使用cout和cin输入输出,是因为库里面已经将它们的<<和>>重载好了,<<和>>能够自动识别类型,是因为它们之间构成了函数重载。
所以,我们若是想让<<和>>也自动识别我们的日期类,就需要我们自己写出对应的运算符重载函数。
cpp
class Date
{
// 友元函数的声明
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
// <<运算符重载
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "-" << d._month << "-" << d._day<< endl;
return out;
}
// >>运算符重载
istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
注意:其中cout是ostream类的一个全局对象,cin是istream类的一个全局变量,<<和>>运算符的重载函数具有返回值是为了实现连续的输入和输出操作。
3.友元类:
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
cpp
#include <iostream>
using namespace std;
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 = 1999, 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;
};
4.作用:
0x01. 提供灵活的访问方式
友元函数打破了类的封装限制,使得外部函数能够访问类的私有和保护成员。这在一些特定场景下非常有用,比如当我们需要对类中的数据进行一些特定的计算或操作,而这些操作又不方便通过类的现有成员函数来实现时。
0x02. 实现不同类之间的协作
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中非公有成员。
cpp
// 前置声明ClassB类
class ClassB;
// 类A的定义
class ClassA
{
private:
int privateData;
public:
ClassA(int data) :
privateData(data)
{}
// 声明ClassB为ClassA的友元类
friend class ClassB;
};
// 类B的定义
class ClassB
{
public:
void funcB(ClassA& a)
{
// 由于ClassB是ClassA的友元类,所以可以直接访问ClassA的私有成员privateData
cout << "ClassA的私有数据: " << a.privateData << endl;
// 在这里还可以对privateData进行其他操作,比如修改它的值
a.privateData += 10;
cout << "修改后ClassA的私有数据: " << a.privateData << endl;
}
};
int main()
{
ClassA aObj(20);
ClassB bObj;
bObj.funcB(aObj);
return 0;
}
5.注意事项
0x01.破坏封装性
友元函数最大的问题就是它破坏了类的封装性。封装性是 C++ 面向对象编程的一个重要原则,它使得类的内部实现细节对外部世界是隐藏的,只有通过类的公共接口(成员函数)才能访问类的内部数据。而友元函数的存在使得外部函数可以直接访问类的私有和保护成员,这在一定程度上增加了代码的维护难度和潜在的错误风险。所以,在使用友元函数时,一定要谨慎考虑是否真的有必要打破封装。只有在确实无法通过其他更合理的方式(如扩展类的成员函数等)来实现所需功能时,才应该考虑使用友元函数。
0x02.单向访问性
友元函数的访问权限是单向的。也就是说,如果函数 A
是类 B
的友元函数,那么 A
可以访问 B
的私有和保护成员,但这并不意味着 B
的成员函数可以访问 A
的内部数据。
0x03.友元关系不可传递
友元关系是不可传递的。即如果函数 A
是类 B
的友元函数,函数 B
是类 C
的友元函数,这并不意味着函数 A
是类 C
的友元函数。
五、内部类
1.概念
如果一个类定义在另一个类的内部,则这个类被称为内部类。
注意:
• 此时的内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象区调用内部类。
• 外部类对内部类没有任何优越的访问权限。
• 内部类就是外部类的友元类,即内部类可以通过外部类的对象参数来访问外部类中的所有 成员。但是外部类不是内部类的友元。
2.特性
• 内部类可以定义在外部类的public、private以及protected这三个区域中的任一区域。
• 内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名。
• 外部类的大小与内部类的大小无关。
cpp
class A
{
private:
static int k;
int h;
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()
{
A::B b; // 内部类对象实例化需要加类域符
b.foo(A());
cout << sizeof(A) << endl; //计算外类A大小
cout << sizeof(A::B) << endl;// 计算内类B大小
return 0;
}
内部类默认可以访问其外部类(这里是
A
类)的所有成员,包括私有成员。所以在这里能够直接访问A
类的私有静态成员变量k
,并输出其值。由于在外部已经将k
初始化为1
,所以这里会输出1
。同样,在
fun
函数中,接着输出A
类对象a
的成员变量h
。虽然h
是A
类的私有成员变量,但因为B
类作为A
类的内部类有默认的访问权限,所以可以直接访问A
类对象a
的h
值。不过需要注意的是,这里传入的A
类对象是通过临时对象A()
创建的,而A
类的默认构造函数会对h
进行初始化(如果没有显式定义默认构造函数对h
进行特殊处理的话,h
会被初始化为一个不确定的值)。所以这里输出的h
的值取决于A
类默认构造函数的行为。A类的大小为4,因为k为静态变量是不占用类A的对象空间的,因为静态成员变量是属于整个类而不是类的某个具体对象的,它在程序的全局数据区有独立的存储位置(静态区)。所以它的大小由h来决定。
内部类B为1,因为它的内部并没有包含任何数据成员,在大多数常见的编译器和系统环境下,一个空类(没有数据成员和虚函数)通常会占用
1
个字节的空间。这是因为编译器需要为类对象分配一些必要的信息,比如指向虚函数表的指针(如果类中有虚函数的话,这里B
类没有虚函数,但编译器的处理方式类似)、对象的对齐填充等。
六、匿名对象
1.定义
在编程中,当我们使用类型名直接跟上括号并传入实参(如果需要的话)的方式来创建一个对象时,这样创建出来的对象就叫做匿名对象。
cpp
MyClass(5);
例如,在 C++ 中,如果有一个类 MyClass
,它有一个合适的构造函数接受某些参数(假设构造函数接受一个整数参数),那么我们可以这样创建匿名对象:
cpp
MyClass myObj(5);
2.匿名对象的生命周期特点
匿名对象的生命周期非常短暂,它仅仅存在于当前代码行的执行期间。一旦当前这行代码执行完毕,匿名对象就会被销毁。
这是因为匿名对象没有一个明确的名字来让我们在后续的代码中继续引用它,所以编译器会在当前行代码执行完相关操作后,自动清理掉这个匿名对象所占用的资源。
cpp
#include <iostream>
class MyClass
{
public:
MyClass(int num = 1)
{
std::cout << "Constructing MyClass with value: " << num << std::endl;
}
~MyClass()
{
std::cout << "Destructing MyClass" << std::endl;
}
};
int main() {
// 创建匿名对象,在这行代码执行时会调用构造函数创建对象
//可以这么定义
MyClass();
MyClass(10);
// 这行代码执行完后,匿名对象就会被销毁,会调用析构函数
return 0;
}
3.匿名对象的适用场景
匿名对象适用于那些只需要在当前行代码中临时使用一个对象来完成某项特定任务的情况。比如,我们有一个函数,它接受一个类类型的对象作为参数,并且我们只需要在调用这个函数时临时创建一个满足函数参数要求的对象,而不需要在后续的代码中再次引用这个对象,这时就可以使用匿名对象。
例如,假设有一个函数 void myFunction(MyClass obj)
,我们可以这样调用它:
cpp
myFunction(MyClass(15));
**小贴士:**匿名对象在一些特定的编程场景中提供了一种简洁、高效的临时使用对象的方式,但由于其生命周期短暂的特点,在使用时需要清楚地了解其适用场景和限制条件。
七、对象拷贝时的优化
现代编译器会为了尽可能提⾼程序的效率,在不影响正确性的情况下会尽可能减少⼀些传参和传返 回值的过程中可以省略的拷⻉。 如何优化C++标准并没有严格规定,各个编译器会根据情况⾃⾏处理。当前主流的相对新⼀点的编 译器对于连续⼀个表达式步骤中的连续拷⻉会进⾏合并优化,有些更新更"激进"的编译器还会进⾏ 跨⾏跨表达式的合并优化。
cpp
#include<iostream>
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;
}
private:
int _a1 = 1;
};
void f1(A aa)
{}
A f2()
{
A aa;
// cout << &aa << endl;
return aa;
}
int main()
{
f2();
cout << endl;
}
输出:
在VS2022下,这里并没有调用拷贝构造,编译器优化为直接构造了,如果是用VS2019的话可能会有不一样的结果哦~
cpp
int main()
{
A aa2 = f2();
cout << endl;
}
输出: 同上
如果编译器没有优化的情况下,从语法角度来讲。这里会有两次拷贝构造函数的调用:一次是在 f2
函数中创建返回的临时对象时,传值返回都会生成一个临时对象,这是语法规定的哦·。另一次是在 main
函数中初始化 aa2
时。
我们可以在g++环境下看到没有优化的情况
VS2022环境下:
cpp
A f2()
{
A aa;
cout << &aa << endl;
return aa;
}
int main()
{
A aa2 = f2();
cout << endl;
cout << &aa2 << endl;
}
输出:
aa2的地址居然和aa的地址一模一样?
这里VS2022编译器的优化思路是直接把aa看为aa2的别名,调用构造函数时是在main函数里直接创建了aa2,然后aa就不创建空间了,aa这时的底层就像一个指针指向aa2,也可以说是引用,引用了aa2,这样就全程无拷贝了。
知识补充:
出现这种情况是因为编译器进行了返回值优化(Return Value Optimization,RVO)或具名返回值优化(Named Return Value Optimization,NRVO)。
在这种优化下,编译器会避免创建临时对象进行拷贝,而是直接在目标位置(这里是创建
aa2
的地方)构造对象。所以看起来aa2
和在f2
函数中的局部对象aa
的地址一样,但实际上这只是编译器优化的结果,并不是真正的同一个对象。如果关闭编译器的返回值优化功能,你会看到不同的地址,并且会有拷贝构造函数的调用。不同的编译器可能有不同的方法来关闭返回值优化,例如在某些编译器中可以使用特定的编译选项来禁用优化。
题目加餐
加餐1:A类创建了多少个对象?
cpp
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;
void Func(A aa)
{
}
void Fxx() {
A aa3;
cout << A::GetaCount() << endl; // 修正函数调用语法
}
int main() {
A aa1;
A aa2 = aa1;
Func(aa1);
Fxx();
//cout << A::_scount << endl;//_scount是公有的时候,可以这么访问
//cout << aa1._scount << endl; // 这行是公共方法调用
cout << aa1.GetaCount() << endl;
return 0;
}
输出结果:3 2
这道题并不难,只需要分析什么时候使用构造和析构即可。
加餐2: 求1+2+3+...+n
题目描述:
求1+2+3+...+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。
示例:
输入:5
返回值:15
分析:
若是只看题目不管要求,这是一道非常简单的题目,我们有好几种方式可以得出最终结果,但加上题目限制条件,可能大多数博友都懵了。
我们来捋一捋:
1、不能使用乘除法,等差数列求和公式不能用了。
2、不能使用for、while,循环求解不能用了。
3、不能使用switch、case和A?B:C,递归求解也不能用了。
思路:
这道题用常规的方式确实解决不了,因为题目把我们要用到的东西都限制死了。解决这道题之前我们需要知道:当一个对象被创建的时候,该对象会自动调用其默认构造函数。
我们需要计算的是1-n这n个数的和,那么我们可以创建n个类对象,这样就可以调用n次构造函数,这就相当于代替了递归。每次需要被加的数都比上一次被加的数大一,我们可以借助于类的静态成员变量,在构造函数中设置该静态成员变量自增即可实现。特别注意,这里必须是静态成员变量,不能是普通的成员变量,因为每个对象被创建时都有属于自己的普通成员变量,而静态成员变量是属于整个类的,这样才能使得这n次调用构造函数时自增的是同一个变量,每个对象访问到的静态成员变量是同一个。同理,存储累加结果的变量也必须是静态成员变量。
cpp
class Solution {
class Sum
{
public:
Sum()
{
_ret+=_i;
++_i;
}
};
static int _i;
static int _ret;
public:
int Sum_Solution(int n) {
Sum arr[n];
return _ret;
}
};
int Solution::_i=1;
int Solution::_ret=0;
本篇博客到此结束,欢迎各位在评论区留言~